From b6ad353199de27405c4f4bf6e93896ee1833abf1 Mon Sep 17 00:00:00 2001 From: Eric Theise Date: Thu, 28 May 2026 15:20:59 -0700 Subject: [PATCH] Forget to update /dist --- dist/iD.js | 4 ++-- dist/iD.js.map | 2 +- dist/iD.min.js | 2 +- dist/iD.min.js.map | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dist/iD.js b/dist/iD.js index 910607428e..3dea8d4c4c 100644 --- a/dist/iD.js +++ b/dist/iD.js @@ -25894,7 +25894,7 @@ ${source} defaultOsmApiConnections = { live: { url: "https://www.openhistoricalmap.org", - apiUrl: "https://api.openhistoricalmap.org", + apiUrl: "https://www.openhistoricalmap.org", client_id: "0tmNTmd0Jo1dQp4AUmMBLtGiD9YpMuXzHefitcuVStc", client_secret: "BTlNrNxIPitHdL4sP2clHw5KLoee9aKkA7dQbc0Bj7Q" }, @@ -38365,7 +38365,7 @@ ${source} "package.json"() { package_default = { name: "@openhistoricalmap/id", - version: "2.39.5", + version: "2.39.5-ohm.2", description: "The OpenHistoricalMap fork of a friendly editor for OpenStreetMap", main: "dist/iD.min.js", repository: { diff --git a/dist/iD.js.map b/dist/iD.js.map index ff59b2ed63..2c566484d8 100644 --- a/dist/iD.js.map +++ b/dist/iD.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../node_modules/whatwg-fetch/fetch.js", "../node_modules/abortcontroller-polyfill/dist/polyfill-patch-fetch.js", "../modules/actions/add_entity.js", "../node_modules/d3-dispatch/src/dispatch.js", "../node_modules/d3-dispatch/src/index.js", "../node_modules/idb-keyval/dist/index.js", "../modules/core/preferences.js", "../modules/geo/geo.ts", "../modules/geo/extent.js", "../node_modules/d3-polygon/src/area.js", "../node_modules/d3-polygon/src/centroid.js", "../node_modules/d3-polygon/src/cross.js", "../node_modules/d3-polygon/src/hull.js", "../node_modules/d3-polygon/src/contains.js", "../node_modules/d3-polygon/src/length.js", "../node_modules/d3-polygon/src/index.js", "../modules/geo/vector.ts", "../modules/geo/geom.ts", "../node_modules/d3-array/src/ascending.js", "../node_modules/d3-array/src/descending.js", "../node_modules/d3-array/src/bisector.js", "../node_modules/d3-array/src/number.js", "../node_modules/d3-array/src/bisect.js", "../node_modules/d3-array/src/blur.js", "../node_modules/d3-array/src/count.js", "../node_modules/d3-array/src/cross.js", "../node_modules/d3-array/src/cumsum.js", "../node_modules/d3-array/src/variance.js", "../node_modules/d3-array/src/deviation.js", "../node_modules/d3-array/src/extent.js", "../node_modules/d3-array/src/fsum.js", "../node_modules/internmap/src/index.js", "../node_modules/d3-array/src/group.js", "../node_modules/d3-array/src/permute.js", "../node_modules/d3-array/src/sort.js", "../node_modules/d3-array/src/groupSort.js", "../node_modules/d3-array/src/ticks.js", "../node_modules/d3-array/src/nice.js", "../node_modules/d3-array/src/threshold/sturges.js", "../node_modules/d3-array/src/bin.js", "../node_modules/d3-array/src/max.js", "../node_modules/d3-array/src/maxIndex.js", "../node_modules/d3-array/src/min.js", "../node_modules/d3-array/src/minIndex.js", "../node_modules/d3-array/src/quickselect.js", "../node_modules/d3-array/src/greatest.js", "../node_modules/d3-array/src/quantile.js", "../node_modules/d3-array/src/threshold/freedmanDiaconis.js", "../node_modules/d3-array/src/threshold/scott.js", "../node_modules/d3-array/src/mean.js", "../node_modules/d3-array/src/median.js", "../node_modules/d3-array/src/merge.js", "../node_modules/d3-array/src/mode.js", "../node_modules/d3-array/src/pairs.js", "../node_modules/d3-array/src/range.js", "../node_modules/d3-array/src/rank.js", "../node_modules/d3-array/src/least.js", "../node_modules/d3-array/src/leastIndex.js", "../node_modules/d3-array/src/greatestIndex.js", "../node_modules/d3-array/src/scan.js", "../node_modules/d3-array/src/shuffle.js", "../node_modules/d3-array/src/sum.js", "../node_modules/d3-array/src/transpose.js", "../node_modules/d3-array/src/zip.js", "../node_modules/d3-array/src/every.js", "../node_modules/d3-array/src/some.js", "../node_modules/d3-array/src/filter.js", "../node_modules/d3-array/src/map.js", "../node_modules/d3-array/src/reduce.js", "../node_modules/d3-array/src/reverse.js", "../node_modules/d3-array/src/difference.js", "../node_modules/d3-array/src/disjoint.js", "../node_modules/d3-array/src/intersection.js", "../node_modules/d3-array/src/superset.js", "../node_modules/d3-array/src/subset.js", "../node_modules/d3-array/src/union.js", "../node_modules/d3-array/src/index.js", "../node_modules/d3-geo/src/math.js", "../node_modules/d3-geo/src/noop.js", "../node_modules/d3-geo/src/stream.js", "../node_modules/d3-geo/src/area.js", "../node_modules/d3-geo/src/cartesian.js", "../node_modules/d3-geo/src/bounds.js", "../node_modules/d3-geo/src/centroid.js", "../node_modules/d3-geo/src/constant.js", "../node_modules/d3-geo/src/compose.js", "../node_modules/d3-geo/src/rotation.js", "../node_modules/d3-geo/src/circle.js", "../node_modules/d3-geo/src/clip/buffer.js", "../node_modules/d3-geo/src/pointEqual.js", "../node_modules/d3-geo/src/clip/rejoin.js", "../node_modules/d3-geo/src/polygonContains.js", "../node_modules/d3-geo/src/clip/index.js", "../node_modules/d3-geo/src/clip/antimeridian.js", "../node_modules/d3-geo/src/clip/circle.js", "../node_modules/d3-geo/src/clip/line.js", "../node_modules/d3-geo/src/clip/rectangle.js", "../node_modules/d3-geo/src/clip/extent.js", "../node_modules/d3-geo/src/length.js", "../node_modules/d3-geo/src/distance.js", "../node_modules/d3-geo/src/contains.js", "../node_modules/d3-geo/src/graticule.js", "../node_modules/d3-geo/src/interpolate.js", "../node_modules/d3-geo/src/identity.js", "../node_modules/d3-geo/src/path/area.js", "../node_modules/d3-geo/src/path/bounds.js", "../node_modules/d3-geo/src/path/centroid.js", "../node_modules/d3-geo/src/path/context.js", "../node_modules/d3-geo/src/path/measure.js", "../node_modules/d3-geo/src/path/string.js", "../node_modules/d3-geo/src/path/index.js", "../node_modules/d3-geo/src/transform.js", "../node_modules/d3-geo/src/projection/fit.js", "../node_modules/d3-geo/src/projection/resample.js", "../node_modules/d3-geo/src/projection/index.js", "../node_modules/d3-geo/src/projection/conicEqualArea.js", "../node_modules/d3-geo/src/projection/albers.js", "../node_modules/d3-geo/src/projection/albersUsa.js", "../node_modules/d3-geo/src/projection/azimuthal.js", "../node_modules/d3-geo/src/projection/azimuthalEqualArea.js", "../node_modules/d3-geo/src/projection/azimuthalEquidistant.js", "../node_modules/d3-geo/src/projection/mercator.js", "../node_modules/d3-geo/src/projection/conicConformal.js", "../node_modules/d3-geo/src/projection/equirectangular.js", "../node_modules/d3-geo/src/projection/conicEquidistant.js", "../node_modules/d3-geo/src/projection/equalEarth.js", "../node_modules/d3-geo/src/projection/gnomonic.js", "../node_modules/d3-geo/src/projection/identity.js", "../node_modules/d3-geo/src/projection/naturalEarth1.js", "../node_modules/d3-geo/src/projection/orthographic.js", "../node_modules/d3-geo/src/projection/stereographic.js", "../node_modules/d3-geo/src/projection/transverseMercator.js", "../node_modules/d3-geo/src/index.js", "../node_modules/d3-selection/src/namespaces.js", "../node_modules/d3-selection/src/namespace.js", "../node_modules/d3-selection/src/creator.js", "../node_modules/d3-selection/src/selector.js", "../node_modules/d3-selection/src/selection/select.js", "../node_modules/d3-selection/src/array.js", "../node_modules/d3-selection/src/selectorAll.js", "../node_modules/d3-selection/src/selection/selectAll.js", "../node_modules/d3-selection/src/matcher.js", "../node_modules/d3-selection/src/selection/selectChild.js", "../node_modules/d3-selection/src/selection/selectChildren.js", "../node_modules/d3-selection/src/selection/filter.js", "../node_modules/d3-selection/src/selection/sparse.js", "../node_modules/d3-selection/src/selection/enter.js", "../node_modules/d3-selection/src/constant.js", "../node_modules/d3-selection/src/selection/data.js", "../node_modules/d3-selection/src/selection/exit.js", "../node_modules/d3-selection/src/selection/join.js", "../node_modules/d3-selection/src/selection/merge.js", "../node_modules/d3-selection/src/selection/order.js", "../node_modules/d3-selection/src/selection/sort.js", "../node_modules/d3-selection/src/selection/call.js", "../node_modules/d3-selection/src/selection/nodes.js", "../node_modules/d3-selection/src/selection/node.js", "../node_modules/d3-selection/src/selection/size.js", "../node_modules/d3-selection/src/selection/empty.js", "../node_modules/d3-selection/src/selection/each.js", "../node_modules/d3-selection/src/selection/attr.js", "../node_modules/d3-selection/src/window.js", "../node_modules/d3-selection/src/selection/style.js", "../node_modules/d3-selection/src/selection/property.js", "../node_modules/d3-selection/src/selection/classed.js", "../node_modules/d3-selection/src/selection/text.js", "../node_modules/d3-selection/src/selection/html.js", "../node_modules/d3-selection/src/selection/raise.js", "../node_modules/d3-selection/src/selection/lower.js", "../node_modules/d3-selection/src/selection/append.js", "../node_modules/d3-selection/src/selection/insert.js", "../node_modules/d3-selection/src/selection/remove.js", "../node_modules/d3-selection/src/selection/clone.js", "../node_modules/d3-selection/src/selection/datum.js", "../node_modules/d3-selection/src/selection/on.js", "../node_modules/d3-selection/src/selection/dispatch.js", "../node_modules/d3-selection/src/selection/iterator.js", "../node_modules/d3-selection/src/selection/index.js", "../node_modules/d3-selection/src/select.js", "../node_modules/d3-selection/src/create.js", "../node_modules/d3-selection/src/local.js", "../node_modules/d3-selection/src/sourceEvent.js", "../node_modules/d3-selection/src/pointer.js", "../node_modules/d3-selection/src/pointers.js", "../node_modules/d3-selection/src/selectAll.js", "../node_modules/d3-selection/src/index.js", "../node_modules/d3-drag/src/noevent.js", "../node_modules/d3-drag/src/nodrag.js", "../node_modules/d3-drag/src/constant.js", "../node_modules/d3-drag/src/event.js", "../node_modules/d3-drag/src/drag.js", "../node_modules/d3-drag/src/index.js", "../node_modules/d3-color/src/define.js", "../node_modules/d3-color/src/color.js", "../node_modules/d3-color/src/math.js", "../node_modules/d3-color/src/lab.js", "../node_modules/d3-color/src/cubehelix.js", "../node_modules/d3-color/src/index.js", "../node_modules/d3-interpolate/src/basis.js", "../node_modules/d3-interpolate/src/basisClosed.js", "../node_modules/d3-interpolate/src/constant.js", "../node_modules/d3-interpolate/src/color.js", "../node_modules/d3-interpolate/src/rgb.js", "../node_modules/d3-interpolate/src/numberArray.js", "../node_modules/d3-interpolate/src/array.js", "../node_modules/d3-interpolate/src/date.js", "../node_modules/d3-interpolate/src/number.js", "../node_modules/d3-interpolate/src/object.js", "../node_modules/d3-interpolate/src/string.js", "../node_modules/d3-interpolate/src/value.js", "../node_modules/d3-interpolate/src/discrete.js", "../node_modules/d3-interpolate/src/hue.js", "../node_modules/d3-interpolate/src/round.js", "../node_modules/d3-interpolate/src/transform/decompose.js", "../node_modules/d3-interpolate/src/transform/parse.js", "../node_modules/d3-interpolate/src/transform/index.js", "../node_modules/d3-interpolate/src/zoom.js", "../node_modules/d3-interpolate/src/hsl.js", "../node_modules/d3-interpolate/src/lab.js", "../node_modules/d3-interpolate/src/hcl.js", "../node_modules/d3-interpolate/src/cubehelix.js", "../node_modules/d3-interpolate/src/piecewise.js", "../node_modules/d3-interpolate/src/quantize.js", "../node_modules/d3-interpolate/src/index.js", "../node_modules/d3-timer/src/timer.js", "../node_modules/d3-timer/src/timeout.js", "../node_modules/d3-timer/src/interval.js", "../node_modules/d3-timer/src/index.js", "../node_modules/d3-transition/src/transition/schedule.js", "../node_modules/d3-transition/src/interrupt.js", "../node_modules/d3-transition/src/selection/interrupt.js", "../node_modules/d3-transition/src/transition/tween.js", "../node_modules/d3-transition/src/transition/interpolate.js", "../node_modules/d3-transition/src/transition/attr.js", "../node_modules/d3-transition/src/transition/attrTween.js", "../node_modules/d3-transition/src/transition/delay.js", "../node_modules/d3-transition/src/transition/duration.js", "../node_modules/d3-transition/src/transition/ease.js", "../node_modules/d3-transition/src/transition/easeVarying.js", "../node_modules/d3-transition/src/transition/filter.js", "../node_modules/d3-transition/src/transition/merge.js", "../node_modules/d3-transition/src/transition/on.js", "../node_modules/d3-transition/src/transition/remove.js", "../node_modules/d3-transition/src/transition/select.js", "../node_modules/d3-transition/src/transition/selectAll.js", "../node_modules/d3-transition/src/transition/selection.js", "../node_modules/d3-transition/src/transition/style.js", "../node_modules/d3-transition/src/transition/styleTween.js", "../node_modules/d3-transition/src/transition/text.js", "../node_modules/d3-transition/src/transition/textTween.js", "../node_modules/d3-transition/src/transition/transition.js", "../node_modules/d3-transition/src/transition/end.js", "../node_modules/d3-transition/src/transition/index.js", "../node_modules/d3-ease/src/linear.js", "../node_modules/d3-ease/src/quad.js", "../node_modules/d3-ease/src/cubic.js", "../node_modules/d3-ease/src/poly.js", "../node_modules/d3-ease/src/sin.js", "../node_modules/d3-ease/src/math.js", "../node_modules/d3-ease/src/exp.js", "../node_modules/d3-ease/src/circle.js", "../node_modules/d3-ease/src/bounce.js", "../node_modules/d3-ease/src/back.js", "../node_modules/d3-ease/src/elastic.js", "../node_modules/d3-ease/src/index.js", "../node_modules/d3-transition/src/selection/transition.js", "../node_modules/d3-transition/src/selection/index.js", "../node_modules/d3-transition/src/active.js", "../node_modules/d3-transition/src/index.js", "../node_modules/d3-zoom/src/constant.js", "../node_modules/d3-zoom/src/event.js", "../node_modules/d3-zoom/src/transform.js", "../node_modules/d3-zoom/src/noevent.js", "../node_modules/d3-zoom/src/zoom.js", "../node_modules/d3-zoom/src/index.js", "../modules/geo/raw_mercator.js", "../modules/geo/ortho.ts", "../modules/geo/index.ts", "../node_modules/aes-js/index.js", "../modules/util/aes.ts", "../modules/util/array.js", "../node_modules/d3-axis/src/index.js", "../node_modules/d3-brush/src/constant.js", "../node_modules/d3-brush/src/event.js", "../node_modules/d3-brush/src/noevent.js", "../node_modules/d3-brush/src/brush.js", "../node_modules/d3-brush/src/index.js", "../node_modules/d3-path/src/index.js", "../node_modules/d3-chord/src/index.js", "../node_modules/d3-contour/src/index.js", "../node_modules/d3-delaunay/src/index.js", "../node_modules/d3-dsv/src/dsv.js", "../node_modules/d3-dsv/src/csv.js", "../node_modules/d3-dsv/src/tsv.js", "../node_modules/d3-dsv/src/autoType.js", "../node_modules/d3-dsv/src/index.js", "../node_modules/d3-fetch/src/blob.js", "../node_modules/d3-fetch/src/buffer.js", "../node_modules/d3-fetch/src/text.js", "../node_modules/d3-fetch/src/dsv.js", "../node_modules/d3-fetch/src/image.js", "../node_modules/d3-fetch/src/json.js", "../node_modules/d3-fetch/src/xml.js", "../node_modules/d3-fetch/src/index.js", "../node_modules/d3-quadtree/src/index.js", "../node_modules/d3-force/src/index.js", "../node_modules/d3-format/src/formatDecimal.js", "../node_modules/d3-format/src/exponent.js", "../node_modules/d3-format/src/formatGroup.js", "../node_modules/d3-format/src/formatNumerals.js", "../node_modules/d3-format/src/formatSpecifier.js", "../node_modules/d3-format/src/formatTrim.js", "../node_modules/d3-format/src/formatPrefixAuto.js", "../node_modules/d3-format/src/formatRounded.js", "../node_modules/d3-format/src/formatTypes.js", "../node_modules/d3-format/src/identity.js", "../node_modules/d3-format/src/locale.js", "../node_modules/d3-format/src/defaultLocale.js", "../node_modules/d3-format/src/precisionFixed.js", "../node_modules/d3-format/src/precisionPrefix.js", "../node_modules/d3-format/src/precisionRound.js", "../node_modules/d3-format/src/index.js", "../node_modules/d3-hierarchy/src/index.js", "../node_modules/d3-random/src/index.js", "../node_modules/d3-scale/src/init.js", "../node_modules/d3-scale/src/ordinal.js", "../node_modules/d3-scale/src/band.js", "../node_modules/d3-scale/src/constant.js", "../node_modules/d3-scale/src/number.js", "../node_modules/d3-scale/src/continuous.js", "../node_modules/d3-scale/src/tickFormat.js", "../node_modules/d3-scale/src/linear.js", "../node_modules/d3-scale/src/identity.js", "../node_modules/d3-scale/src/log.js", "../node_modules/d3-scale/src/symlog.js", "../node_modules/d3-scale/src/pow.js", "../node_modules/d3-scale/src/radial.js", "../node_modules/d3-scale/src/quantile.js", "../node_modules/d3-scale/src/quantize.js", "../node_modules/d3-scale/src/threshold.js", "../node_modules/d3-time/src/interval.js", "../node_modules/d3-time/src/millisecond.js", "../node_modules/d3-time/src/duration.js", "../node_modules/d3-time/src/second.js", "../node_modules/d3-time/src/minute.js", "../node_modules/d3-time/src/hour.js", "../node_modules/d3-time/src/day.js", "../node_modules/d3-time/src/week.js", "../node_modules/d3-time/src/month.js", "../node_modules/d3-time/src/year.js", "../node_modules/d3-time/src/ticks.js", "../node_modules/d3-time/src/index.js", "../node_modules/d3-time-format/src/locale.js", "../node_modules/d3-time-format/src/defaultLocale.js", "../node_modules/d3-time-format/src/isoFormat.js", "../node_modules/d3-time-format/src/isoParse.js", "../node_modules/d3-time-format/src/index.js", "../node_modules/d3-scale/src/time.js", "../node_modules/d3-scale/src/utcTime.js", "../node_modules/d3-scale/src/sequential.js", "../node_modules/d3-scale/src/sequentialQuantile.js", "../node_modules/d3-scale/src/diverging.js", "../node_modules/d3-scale/src/index.js", "../node_modules/d3-scale-chromatic/src/index.js", "../node_modules/d3-shape/src/index.js", "../node_modules/d3/src/index.js", "../node_modules/diacritics/index.js", "../node_modules/alif-toolkit/lib/isArabic.js", "../node_modules/alif-toolkit/lib/unicode-arabic.js", "../node_modules/alif-toolkit/lib/unicode-ligatures.js", "../node_modules/alif-toolkit/lib/reference.js", "../node_modules/alif-toolkit/lib/GlyphSplitter.js", "../node_modules/alif-toolkit/lib/BaselineSplitter.js", "../node_modules/alif-toolkit/lib/Normalization.js", "../node_modules/alif-toolkit/lib/CharShaper.js", "../node_modules/alif-toolkit/lib/WordShaper.js", "../node_modules/alif-toolkit/lib/ParentLetter.js", "../node_modules/alif-toolkit/lib/index.js", "../modules/util/svg_paths_rtl_fix.ts", "../node_modules/es-toolkit/dist/compat/array/castArray.mjs", "../node_modules/es-toolkit/dist/array/chunk.mjs", "../node_modules/es-toolkit/dist/compat/_internal/toArray.mjs", "../node_modules/es-toolkit/dist/predicate/isLength.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isArrayLike.mjs", "../node_modules/es-toolkit/dist/compat/array/chunk.mjs", "../node_modules/es-toolkit/dist/array/compact.mjs", "../node_modules/es-toolkit/dist/compat/array/compact.mjs", "../node_modules/es-toolkit/dist/array/flatten.mjs", "../node_modules/es-toolkit/dist/compat/array/concat.mjs", "../node_modules/es-toolkit/dist/function/identity.mjs", "../node_modules/es-toolkit/dist/_internal/isUnsafeProperty.mjs", "../node_modules/es-toolkit/dist/compat/_internal/isDeepKey.mjs", "../node_modules/es-toolkit/dist/compat/_internal/toKey.mjs", "../node_modules/es-toolkit/dist/compat/util/toString.mjs", "../node_modules/es-toolkit/dist/compat/util/toPath.mjs", "../node_modules/es-toolkit/dist/compat/object/get.mjs", "../node_modules/es-toolkit/dist/compat/object/property.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isObject.mjs", "../node_modules/es-toolkit/dist/predicate/isPrimitive.mjs", "../node_modules/es-toolkit/dist/_internal/isEqualsSameValueZero.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isMatchWith.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isMatch.mjs", "../node_modules/es-toolkit/dist/compat/_internal/getSymbols.mjs", "../node_modules/es-toolkit/dist/compat/_internal/getTag.mjs", "../node_modules/es-toolkit/dist/compat/_internal/tags.mjs", "../node_modules/es-toolkit/dist/predicate/isTypedArray.mjs", "../node_modules/es-toolkit/dist/object/cloneDeepWith.mjs", "../node_modules/es-toolkit/dist/object/cloneDeep.mjs", "../node_modules/es-toolkit/dist/compat/predicate/matches.mjs", "../node_modules/es-toolkit/dist/compat/object/cloneDeepWith.mjs", "../node_modules/es-toolkit/dist/compat/object/cloneDeep.mjs", "../node_modules/es-toolkit/dist/compat/_internal/isIndex.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isArguments.mjs", "../node_modules/es-toolkit/dist/compat/object/has.mjs", "../node_modules/es-toolkit/dist/compat/predicate/matchesProperty.mjs", "../node_modules/es-toolkit/dist/compat/util/iteratee.mjs", "../node_modules/es-toolkit/dist/compat/array/countBy.mjs", "../node_modules/es-toolkit/dist/array/difference.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isObjectLike.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isArrayLikeObject.mjs", "../node_modules/es-toolkit/dist/compat/array/difference.mjs", "../node_modules/es-toolkit/dist/array/last.mjs", "../node_modules/es-toolkit/dist/compat/array/last.mjs", "../node_modules/es-toolkit/dist/array/differenceBy.mjs", "../node_modules/es-toolkit/dist/compat/_internal/flattenArrayLike.mjs", "../node_modules/es-toolkit/dist/compat/array/differenceBy.mjs", "../node_modules/es-toolkit/dist/array/differenceWith.mjs", "../node_modules/es-toolkit/dist/compat/array/differenceWith.mjs", "../node_modules/es-toolkit/dist/array/drop.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isSymbol.mjs", "../node_modules/es-toolkit/dist/compat/util/toNumber.mjs", "../node_modules/es-toolkit/dist/compat/util/toFinite.mjs", "../node_modules/es-toolkit/dist/compat/util/toInteger.mjs", "../node_modules/es-toolkit/dist/compat/array/drop.mjs", "../node_modules/es-toolkit/dist/array/dropRight.mjs", "../node_modules/es-toolkit/dist/compat/array/dropRight.mjs", "../node_modules/es-toolkit/dist/array/dropRightWhile.mjs", "../node_modules/es-toolkit/dist/compat/array/dropRightWhile.mjs", "../node_modules/es-toolkit/dist/array/dropWhile.mjs", "../node_modules/es-toolkit/dist/compat/array/dropWhile.mjs", "../node_modules/es-toolkit/dist/math/range.mjs", "../node_modules/es-toolkit/dist/compat/array/forEach.mjs", "../node_modules/es-toolkit/dist/compat/array/forEachRight.mjs", "../node_modules/es-toolkit/dist/compat/_internal/isIterateeCall.mjs", "../node_modules/es-toolkit/dist/compat/array/every.mjs", "../node_modules/es-toolkit/dist/array/fill.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isString.mjs", "../node_modules/es-toolkit/dist/compat/array/fill.mjs", "../node_modules/es-toolkit/dist/compat/array/filter.mjs", "../node_modules/es-toolkit/dist/compat/array/find.mjs", "../node_modules/es-toolkit/dist/compat/function/identity.mjs", "../node_modules/es-toolkit/dist/compat/array/findIndex.mjs", "../node_modules/es-toolkit/dist/compat/array/findLast.mjs", "../node_modules/es-toolkit/dist/compat/array/findLastIndex.mjs", "../node_modules/es-toolkit/dist/array/head.mjs", "../node_modules/es-toolkit/dist/compat/array/head.mjs", "../node_modules/es-toolkit/dist/compat/array/flatten.mjs", "../node_modules/es-toolkit/dist/compat/array/flattenDepth.mjs", "../node_modules/es-toolkit/dist/compat/array/map.mjs", "../node_modules/es-toolkit/dist/predicate/isNil.mjs", "../node_modules/es-toolkit/dist/compat/array/flatMap.mjs", "../node_modules/es-toolkit/dist/compat/array/flatMapDepth.mjs", "../node_modules/es-toolkit/dist/compat/array/flatMapDeep.mjs", "../node_modules/es-toolkit/dist/compat/array/flattenDeep.mjs", "../node_modules/es-toolkit/dist/array/groupBy.mjs", "../node_modules/es-toolkit/dist/compat/array/groupBy.mjs", "../node_modules/es-toolkit/dist/compat/array/includes.mjs", "../node_modules/es-toolkit/dist/compat/array/indexOf.mjs", "../node_modules/es-toolkit/dist/array/initial.mjs", "../node_modules/es-toolkit/dist/compat/array/initial.mjs", "../node_modules/es-toolkit/dist/array/intersection.mjs", "../node_modules/es-toolkit/dist/array/uniq.mjs", "../node_modules/es-toolkit/dist/compat/array/intersection.mjs", "../node_modules/es-toolkit/dist/array/intersectionBy.mjs", "../node_modules/es-toolkit/dist/compat/array/intersectionBy.mjs", "../node_modules/es-toolkit/dist/array/intersectionWith.mjs", "../node_modules/es-toolkit/dist/compat/array/uniq.mjs", "../node_modules/es-toolkit/dist/compat/array/intersectionWith.mjs", "../node_modules/es-toolkit/dist/predicate/isFunction.mjs", "../node_modules/es-toolkit/dist/compat/array/invokeMap.mjs", "../node_modules/es-toolkit/dist/compat/array/join.mjs", "../node_modules/es-toolkit/dist/compat/array/reduce.mjs", "../node_modules/es-toolkit/dist/compat/array/keyBy.mjs", "../node_modules/es-toolkit/dist/compat/array/lastIndexOf.mjs", "../node_modules/es-toolkit/dist/compat/array/nth.mjs", "../node_modules/es-toolkit/dist/compat/_internal/compareValues.mjs", "../node_modules/es-toolkit/dist/compat/_internal/isKey.mjs", "../node_modules/es-toolkit/dist/compat/array/orderBy.mjs", "../node_modules/es-toolkit/dist/compat/array/partition.mjs", "../node_modules/es-toolkit/dist/array/pull.mjs", "../node_modules/es-toolkit/dist/compat/array/pull.mjs", "../node_modules/es-toolkit/dist/compat/array/pullAll.mjs", "../node_modules/es-toolkit/dist/compat/array/pullAllBy.mjs", "../node_modules/es-toolkit/dist/compat/_internal/copyArray.mjs", "../node_modules/es-toolkit/dist/compat/array/pullAllWith.mjs", "../node_modules/es-toolkit/dist/compat/object/at.mjs", "../node_modules/es-toolkit/dist/compat/object/unset.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isArray.mjs", "../node_modules/es-toolkit/dist/compat/array/pullAt.mjs", "../node_modules/es-toolkit/dist/compat/array/reduceRight.mjs", "../node_modules/es-toolkit/dist/compat/function/negate.mjs", "../node_modules/es-toolkit/dist/compat/array/reject.mjs", "../node_modules/es-toolkit/dist/array/remove.mjs", "../node_modules/es-toolkit/dist/compat/array/remove.mjs", "../node_modules/es-toolkit/dist/compat/array/reverse.mjs", "../node_modules/es-toolkit/dist/array/sample.mjs", "../node_modules/es-toolkit/dist/compat/array/sample.mjs", "../node_modules/es-toolkit/dist/math/random.mjs", "../node_modules/es-toolkit/dist/math/randomInt.mjs", "../node_modules/es-toolkit/dist/array/sampleSize.mjs", "../node_modules/es-toolkit/dist/compat/math/clamp.mjs", "../node_modules/es-toolkit/dist/predicate/isMap.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isMap.mjs", "../node_modules/es-toolkit/dist/compat/util/toArray.mjs", "../node_modules/es-toolkit/dist/compat/array/sampleSize.mjs", "../node_modules/es-toolkit/dist/array/shuffle.mjs", "../node_modules/es-toolkit/dist/compat/object/values.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isNil.mjs", "../node_modules/es-toolkit/dist/compat/array/shuffle.mjs", "../node_modules/es-toolkit/dist/compat/array/size.mjs", "../node_modules/es-toolkit/dist/compat/array/slice.mjs", "../node_modules/es-toolkit/dist/compat/array/some.mjs", "../node_modules/es-toolkit/dist/compat/array/sortBy.mjs", "../node_modules/es-toolkit/dist/predicate/isNull.mjs", "../node_modules/es-toolkit/dist/predicate/isUndefined.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isNaN.mjs", "../node_modules/es-toolkit/dist/compat/array/sortedIndexBy.mjs", "../node_modules/es-toolkit/dist/predicate/isSymbol.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isNumber.mjs", "../node_modules/es-toolkit/dist/compat/array/sortedIndex.mjs", "../node_modules/es-toolkit/dist/compat/array/sortedIndexOf.mjs", "../node_modules/es-toolkit/dist/compat/array/sortedLastIndexBy.mjs", "../node_modules/es-toolkit/dist/compat/array/sortedLastIndex.mjs", "../node_modules/es-toolkit/dist/compat/array/sortedLastIndexOf.mjs", "../node_modules/es-toolkit/dist/array/tail.mjs", "../node_modules/es-toolkit/dist/compat/array/tail.mjs", "../node_modules/es-toolkit/dist/array/take.mjs", "../node_modules/es-toolkit/dist/compat/array/take.mjs", "../node_modules/es-toolkit/dist/array/takeRight.mjs", "../node_modules/es-toolkit/dist/compat/array/takeRight.mjs", "../node_modules/es-toolkit/dist/function/negate.mjs", "../node_modules/es-toolkit/dist/compat/array/takeRightWhile.mjs", "../node_modules/es-toolkit/dist/compat/array/takeWhile.mjs", "../node_modules/es-toolkit/dist/compat/array/union.mjs", "../node_modules/es-toolkit/dist/array/uniqBy.mjs", "../node_modules/es-toolkit/dist/function/ary.mjs", "../node_modules/es-toolkit/dist/compat/array/unionBy.mjs", "../node_modules/es-toolkit/dist/array/uniqWith.mjs", "../node_modules/es-toolkit/dist/compat/array/unionWith.mjs", "../node_modules/es-toolkit/dist/compat/array/uniqBy.mjs", "../node_modules/es-toolkit/dist/compat/array/uniqWith.mjs", "../node_modules/es-toolkit/dist/array/unzip.mjs", "../node_modules/es-toolkit/dist/compat/array/unzip.mjs", "../node_modules/es-toolkit/dist/compat/array/unzipWith.mjs", "../node_modules/es-toolkit/dist/array/without.mjs", "../node_modules/es-toolkit/dist/compat/array/without.mjs", "../node_modules/es-toolkit/dist/compat/array/xor.mjs", "../node_modules/es-toolkit/dist/array/windowed.mjs", "../node_modules/es-toolkit/dist/compat/array/xorBy.mjs", "../node_modules/es-toolkit/dist/compat/array/xorWith.mjs", "../node_modules/es-toolkit/dist/array/zip.mjs", "../node_modules/es-toolkit/dist/compat/array/zip.mjs", "../node_modules/es-toolkit/dist/compat/_internal/assignValue.mjs", "../node_modules/es-toolkit/dist/compat/array/zipObject.mjs", "../node_modules/es-toolkit/dist/compat/object/updateWith.mjs", "../node_modules/es-toolkit/dist/compat/object/set.mjs", "../node_modules/es-toolkit/dist/compat/array/zipObjectDeep.mjs", "../node_modules/es-toolkit/dist/compat/array/zipWith.mjs", "../node_modules/es-toolkit/dist/compat/function/after.mjs", "../node_modules/es-toolkit/dist/compat/function/ary.mjs", "../node_modules/es-toolkit/dist/compat/function/attempt.mjs", "../node_modules/es-toolkit/dist/compat/function/before.mjs", "../node_modules/es-toolkit/dist/compat/function/bind.mjs", "../node_modules/es-toolkit/dist/compat/function/bindKey.mjs", "../node_modules/es-toolkit/dist/compat/function/curry.mjs", "../node_modules/es-toolkit/dist/compat/function/curryRight.mjs", "../node_modules/es-toolkit/dist/function/debounce.mjs", "../node_modules/es-toolkit/dist/compat/function/debounce.mjs", "../node_modules/es-toolkit/dist/compat/function/defer.mjs", "../node_modules/es-toolkit/dist/compat/function/delay.mjs", "../node_modules/es-toolkit/dist/compat/function/flip.mjs", "../node_modules/es-toolkit/dist/function/flow.mjs", "../node_modules/es-toolkit/dist/compat/function/flow.mjs", "../node_modules/es-toolkit/dist/function/flowRight.mjs", "../node_modules/es-toolkit/dist/compat/function/flowRight.mjs", "../node_modules/es-toolkit/dist/compat/function/memoize.mjs", "../node_modules/es-toolkit/dist/compat/function/nthArg.mjs", "../node_modules/es-toolkit/dist/function/once.mjs", "../node_modules/es-toolkit/dist/compat/function/once.mjs", "../node_modules/es-toolkit/dist/compat/function/overArgs.mjs", "../node_modules/es-toolkit/dist/function/partial.mjs", "../node_modules/es-toolkit/dist/compat/function/partial.mjs", "../node_modules/es-toolkit/dist/function/partialRight.mjs", "../node_modules/es-toolkit/dist/compat/function/partialRight.mjs", "../node_modules/es-toolkit/dist/compat/function/rearg.mjs", "../node_modules/es-toolkit/dist/function/rest.mjs", "../node_modules/es-toolkit/dist/compat/function/rest.mjs", "../node_modules/es-toolkit/dist/compat/function/spread.mjs", "../node_modules/es-toolkit/dist/compat/function/throttle.mjs", "../node_modules/es-toolkit/dist/compat/function/unary.mjs", "../node_modules/es-toolkit/dist/compat/function/wrap.mjs", "../node_modules/es-toolkit/dist/compat/math/add.mjs", "../node_modules/es-toolkit/dist/compat/_internal/decimalAdjust.mjs", "../node_modules/es-toolkit/dist/compat/math/ceil.mjs", "../node_modules/es-toolkit/dist/compat/math/divide.mjs", "../node_modules/es-toolkit/dist/compat/math/floor.mjs", "../node_modules/es-toolkit/dist/math/inRange.mjs", "../node_modules/es-toolkit/dist/compat/math/inRange.mjs", "../node_modules/es-toolkit/dist/compat/math/max.mjs", "../node_modules/es-toolkit/dist/array/maxBy.mjs", "../node_modules/es-toolkit/dist/compat/math/maxBy.mjs", "../node_modules/es-toolkit/dist/compat/math/sumBy.mjs", "../node_modules/es-toolkit/dist/compat/math/sum.mjs", "../node_modules/es-toolkit/dist/compat/math/mean.mjs", "../node_modules/es-toolkit/dist/math/sumBy.mjs", "../node_modules/es-toolkit/dist/math/meanBy.mjs", "../node_modules/es-toolkit/dist/compat/math/meanBy.mjs", "../node_modules/es-toolkit/dist/compat/math/min.mjs", "../node_modules/es-toolkit/dist/array/minBy.mjs", "../node_modules/es-toolkit/dist/compat/math/minBy.mjs", "../node_modules/es-toolkit/dist/compat/math/multiply.mjs", "../node_modules/es-toolkit/dist/compat/math/parseInt.mjs", "../node_modules/es-toolkit/dist/compat/math/random.mjs", "../node_modules/es-toolkit/dist/compat/math/range.mjs", "../node_modules/es-toolkit/dist/compat/math/rangeRight.mjs", "../node_modules/es-toolkit/dist/compat/math/round.mjs", "../node_modules/es-toolkit/dist/compat/math/subtract.mjs", "../node_modules/es-toolkit/dist/predicate/isPlainObject.mjs", "../node_modules/es-toolkit/dist/predicate/isEqualWith.mjs", "../node_modules/es-toolkit/dist/function/noop.mjs", "../node_modules/es-toolkit/dist/predicate/isEqual.mjs", "../node_modules/es-toolkit/dist/compat/function/noop.mjs", "../node_modules/es-toolkit/dist/predicate/isBuffer.mjs", "../node_modules/es-toolkit/dist/compat/_internal/isPrototype.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isTypedArray.mjs", "../node_modules/es-toolkit/dist/compat/util/times.mjs", "../node_modules/es-toolkit/dist/compat/object/keys.mjs", "../node_modules/es-toolkit/dist/compat/object/assign.mjs", "../node_modules/es-toolkit/dist/compat/object/keysIn.mjs", "../node_modules/es-toolkit/dist/compat/object/assignIn.mjs", "../node_modules/es-toolkit/dist/compat/object/assignInWith.mjs", "../node_modules/es-toolkit/dist/compat/object/assignWith.mjs", "../node_modules/es-toolkit/dist/compat/object/clone.mjs", "../node_modules/es-toolkit/dist/compat/object/cloneWith.mjs", "../node_modules/es-toolkit/dist/compat/object/create.mjs", "../node_modules/es-toolkit/dist/compat/object/defaults.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isPlainObject.mjs", "../node_modules/es-toolkit/dist/compat/object/defaultsDeep.mjs", "../node_modules/es-toolkit/dist/object/findKey.mjs", "../node_modules/es-toolkit/dist/compat/object/findKey.mjs", "../node_modules/es-toolkit/dist/compat/object/findLastKey.mjs", "../node_modules/es-toolkit/dist/compat/object/forIn.mjs", "../node_modules/es-toolkit/dist/compat/object/forInRight.mjs", "../node_modules/es-toolkit/dist/compat/object/forOwn.mjs", "../node_modules/es-toolkit/dist/compat/object/forOwnRight.mjs", "../node_modules/es-toolkit/dist/compat/object/fromPairs.mjs", "../node_modules/es-toolkit/dist/compat/object/functions.mjs", "../node_modules/es-toolkit/dist/compat/object/functionsIn.mjs", "../node_modules/es-toolkit/dist/compat/object/hasIn.mjs", "../node_modules/es-toolkit/dist/object/invert.mjs", "../node_modules/es-toolkit/dist/compat/object/invert.mjs", "../node_modules/es-toolkit/dist/compat/object/invertBy.mjs", "../node_modules/es-toolkit/dist/object/mapKeys.mjs", "../node_modules/es-toolkit/dist/compat/object/mapKeys.mjs", "../node_modules/es-toolkit/dist/object/mapValues.mjs", "../node_modules/es-toolkit/dist/compat/object/mapValues.mjs", "../node_modules/es-toolkit/dist/object/clone.mjs", "../node_modules/es-toolkit/dist/compat/object/mergeWith.mjs", "../node_modules/es-toolkit/dist/compat/object/merge.mjs", "../node_modules/es-toolkit/dist/compat/_internal/getSymbolsIn.mjs", "../node_modules/es-toolkit/dist/compat/object/omit.mjs", "../node_modules/es-toolkit/dist/compat/object/omitBy.mjs", "../node_modules/es-toolkit/dist/compat/object/pick.mjs", "../node_modules/es-toolkit/dist/compat/object/pickBy.mjs", "../node_modules/es-toolkit/dist/compat/object/propertyOf.mjs", "../node_modules/es-toolkit/dist/compat/object/result.mjs", "../node_modules/es-toolkit/dist/compat/object/setWith.mjs", "../node_modules/es-toolkit/dist/compat/object/toDefaulted.mjs", "../node_modules/es-toolkit/dist/compat/_internal/mapToEntries.mjs", "../node_modules/es-toolkit/dist/compat/_internal/setToEntries.mjs", "../node_modules/es-toolkit/dist/compat/object/toPairs.mjs", "../node_modules/es-toolkit/dist/compat/object/toPairsIn.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isBuffer.mjs", "../node_modules/es-toolkit/dist/compat/object/transform.mjs", "../node_modules/es-toolkit/dist/compat/object/update.mjs", "../node_modules/es-toolkit/dist/compat/object/valuesIn.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isFunction.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isLength.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isNative.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isNull.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isUndefined.mjs", "../node_modules/es-toolkit/dist/compat/predicate/conformsTo.mjs", "../node_modules/es-toolkit/dist/compat/predicate/conforms.mjs", "../node_modules/es-toolkit/dist/predicate/isArrayBuffer.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isArrayBuffer.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isBoolean.mjs", "../node_modules/es-toolkit/dist/predicate/isDate.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isDate.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isElement.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isEmpty.mjs", "../node_modules/es-toolkit/dist/function/after.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isEqualWith.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isError.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isFinite.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isInteger.mjs", "../node_modules/es-toolkit/dist/predicate/isRegExp.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isRegExp.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isSafeInteger.mjs", "../node_modules/es-toolkit/dist/predicate/isSet.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isSet.mjs", "../node_modules/es-toolkit/dist/predicate/isWeakMap.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isWeakMap.mjs", "../node_modules/es-toolkit/dist/predicate/isWeakSet.mjs", "../node_modules/es-toolkit/dist/compat/predicate/isWeakSet.mjs", "../node_modules/es-toolkit/dist/string/capitalize.mjs", "../node_modules/es-toolkit/dist/compat/string/capitalize.mjs", "../node_modules/es-toolkit/dist/compat/util/bindAll.mjs", "../node_modules/es-toolkit/dist/string/deburr.mjs", "../node_modules/es-toolkit/dist/compat/string/deburr.mjs", "../node_modules/es-toolkit/dist/string/words.mjs", "../node_modules/es-toolkit/dist/string/camelCase.mjs", "../node_modules/es-toolkit/dist/compat/_internal/normalizeForCase.mjs", "../node_modules/es-toolkit/dist/compat/string/camelCase.mjs", "../node_modules/es-toolkit/dist/compat/string/endsWith.mjs", "../node_modules/es-toolkit/dist/string/escape.mjs", "../node_modules/es-toolkit/dist/compat/string/escape.mjs", "../node_modules/es-toolkit/dist/string/escapeRegExp.mjs", "../node_modules/es-toolkit/dist/compat/string/escapeRegExp.mjs", "../node_modules/es-toolkit/dist/string/kebabCase.mjs", "../node_modules/es-toolkit/dist/compat/string/kebabCase.mjs", "../node_modules/es-toolkit/dist/string/lowerCase.mjs", "../node_modules/es-toolkit/dist/compat/string/lowerCase.mjs", "../node_modules/es-toolkit/dist/string/lowerFirst.mjs", "../node_modules/es-toolkit/dist/compat/string/lowerFirst.mjs", "../node_modules/es-toolkit/dist/string/pad.mjs", "../node_modules/es-toolkit/dist/compat/string/pad.mjs", "../node_modules/es-toolkit/dist/compat/string/padEnd.mjs", "../node_modules/es-toolkit/dist/compat/string/padStart.mjs", "../node_modules/es-toolkit/dist/compat/_internal/MAX_SAFE_INTEGER.mjs", "../node_modules/es-toolkit/dist/compat/string/repeat.mjs", "../node_modules/es-toolkit/dist/compat/string/replace.mjs", "../node_modules/es-toolkit/dist/string/snakeCase.mjs", "../node_modules/es-toolkit/dist/compat/string/snakeCase.mjs", "../node_modules/es-toolkit/dist/compat/string/split.mjs", "../node_modules/es-toolkit/dist/compat/string/startCase.mjs", "../node_modules/es-toolkit/dist/compat/string/startsWith.mjs", "../node_modules/es-toolkit/dist/compat/string/template.mjs", "../node_modules/es-toolkit/dist/compat/string/toLower.mjs", "../node_modules/es-toolkit/dist/compat/string/toUpper.mjs", "../node_modules/es-toolkit/dist/string/trimEnd.mjs", "../node_modules/es-toolkit/dist/string/trimStart.mjs", "../node_modules/es-toolkit/dist/string/trim.mjs", "../node_modules/es-toolkit/dist/compat/string/trim.mjs", "../node_modules/es-toolkit/dist/compat/string/trimEnd.mjs", "../node_modules/es-toolkit/dist/compat/string/trimStart.mjs", "../node_modules/es-toolkit/dist/compat/string/truncate.mjs", "../node_modules/es-toolkit/dist/string/unescape.mjs", "../node_modules/es-toolkit/dist/compat/string/unescape.mjs", "../node_modules/es-toolkit/dist/string/upperCase.mjs", "../node_modules/es-toolkit/dist/compat/string/upperCase.mjs", "../node_modules/es-toolkit/dist/string/upperFirst.mjs", "../node_modules/es-toolkit/dist/compat/string/upperFirst.mjs", "../node_modules/es-toolkit/dist/compat/string/words.mjs", "../node_modules/es-toolkit/dist/compat/util/cond.mjs", "../node_modules/es-toolkit/dist/compat/util/constant.mjs", "../node_modules/es-toolkit/dist/compat/util/defaultTo.mjs", "../node_modules/es-toolkit/dist/compat/util/gt.mjs", "../node_modules/es-toolkit/dist/compat/util/gte.mjs", "../node_modules/es-toolkit/dist/compat/util/invoke.mjs", "../node_modules/es-toolkit/dist/compat/util/lt.mjs", "../node_modules/es-toolkit/dist/compat/util/lte.mjs", "../node_modules/es-toolkit/dist/compat/util/method.mjs", "../node_modules/es-toolkit/dist/compat/util/methodOf.mjs", "../node_modules/es-toolkit/dist/compat/util/now.mjs", "../node_modules/es-toolkit/dist/compat/util/over.mjs", "../node_modules/es-toolkit/dist/compat/util/overEvery.mjs", "../node_modules/es-toolkit/dist/compat/util/overSome.mjs", "../node_modules/es-toolkit/dist/compat/util/stubArray.mjs", "../node_modules/es-toolkit/dist/compat/util/stubFalse.mjs", "../node_modules/es-toolkit/dist/compat/util/stubObject.mjs", "../node_modules/es-toolkit/dist/compat/util/stubString.mjs", "../node_modules/es-toolkit/dist/compat/util/stubTrue.mjs", "../node_modules/es-toolkit/dist/compat/_internal/MAX_ARRAY_LENGTH.mjs", "../node_modules/es-toolkit/dist/compat/util/toLength.mjs", "../node_modules/es-toolkit/dist/compat/util/toPlainObject.mjs", "../node_modules/es-toolkit/dist/compat/util/toSafeInteger.mjs", "../node_modules/es-toolkit/dist/compat/util/uniqueId.mjs", "../node_modules/es-toolkit/dist/compat/compat.mjs", "../node_modules/es-toolkit/dist/compat/toolkit.mjs", "../node_modules/es-toolkit/dist/compat/index.mjs", "../modules/util/detect.ts", "../config/id.js", "../modules/core/localizer.js", "../node_modules/edtf/src/assert.js", "../node_modules/edtf/src/bitmask.js", "../node_modules/nearley/lib/nearley.js", "../node_modules/edtf/src/defaults.js", "../node_modules/edtf/src/util.js", "../node_modules/edtf/src/grammar.js", "../node_modules/edtf/src/parser.js", "../node_modules/edtf/src/interface.js", "../node_modules/edtf/src/mixin.js", "../node_modules/edtf/locale-data/en-US.json", "../node_modules/edtf/locale-data/es-ES.json", "../node_modules/edtf/locale-data/de-DE.json", "../node_modules/edtf/locale-data/fr-FR.json", "../node_modules/edtf/locale-data/it-IT.json", "../node_modules/edtf/locale-data/ja-JA.json", "../node_modules/edtf/locale-data/index.cjs", "../node_modules/edtf/src/format.js", "../node_modules/edtf/src/date.js", "../node_modules/edtf/src/year.js", "../node_modules/edtf/src/decade.js", "../node_modules/edtf/src/century.js", "../node_modules/edtf/src/season.js", "../node_modules/edtf/src/interval.js", "../node_modules/edtf/src/list.js", "../node_modules/edtf/src/set.js", "../node_modules/edtf/src/types.js", "../node_modules/edtf/src/edtf.js", "../node_modules/edtf/index.js", "../modules/util/ohm_date.js", "../modules/util/util.js", "../modules/util/clean_tags.ts", "../modules/util/get_set_value.js", "../modules/util/keybinding.js", "../modules/util/object.js", "../modules/util/rebind.js", "../modules/util/session_mutex.js", "../modules/util/tiler.js", "../modules/util/trigger_event.js", "../modules/util/units.ts", "../modules/util/index.ts", "../modules/actions/add_midpoint.js", "../modules/actions/add_vertex.js", "../modules/actions/change_member.js", "../modules/actions/change_preset.js", "../modules/actions/change_tags.js", "../modules/osm/tags.js", "../modules/osm/entity.js", "../modules/osm/node.js", "../modules/actions/circularize.js", "../modules/actions/delete_way.js", "../modules/actions/delete_multiple.js", "../modules/actions/delete_relation.js", "../modules/actions/delete_node.js", "../modules/actions/connect.js", "../modules/actions/copy_entities.js", "../modules/actions/delete_member.js", "../modules/actions/delete_members.js", "../modules/actions/discard_tags.js", "../modules/actions/disconnect.js", "../modules/actions/extract.js", "../modules/actions/join.js", "../modules/actions/merge.js", "../modules/actions/merge_nodes.js", "../modules/osm/changeset.js", "../modules/osm/note.js", "../modules/osm/relation.js", "../modules/osm/lanes.js", "../modules/osm/way.js", "../modules/osm/qa_item.js", "../modules/actions/split.js", "../modules/core/graph.js", "../modules/osm/intersection.js", "../modules/osm/index.js", "../modules/actions/merge_polygon.js", "../node_modules/fast-equals/dist/es/index.mjs", "../node_modules/node-diff3/src/diff3.mjs", "../modules/actions/merge_remote_changes.js", "../modules/actions/move.js", "../modules/actions/move_member.js", "../modules/actions/move_node.js", "../modules/actions/noop.js", "../modules/actions/orthogonalize.js", "../modules/actions/reflect.js", "../modules/actions/restrict_turn.js", "../modules/actions/revert.js", "../modules/actions/rotate.js", "../modules/actions/scale.js", "../modules/actions/straighten_nodes.js", "../modules/actions/straighten_way.js", "../modules/actions/unrestrict_turn.js", "../modules/actions/upgrade_tags.js", "../modules/behavior/edit.js", "../modules/behavior/hover.js", "../modules/behavior/draw.js", "../modules/behavior/breathe.js", "../package.json", "../modules/core/difference.js", "../node_modules/quickselect/index.js", "../node_modules/rbush/index.js", "../modules/core/tree.js", "../modules/svg/icon.js", "../modules/ui/modal.js", "../modules/ui/loading.js", "../modules/core/history.js", "../modules/util/utilDisplayLabel.js", "../modules/core/validation/models.js", "../modules/core/validation/index.js", "../node_modules/marked/src/defaults.ts", "../node_modules/marked/src/rules.ts", "../node_modules/marked/src/helpers.ts", "../node_modules/marked/src/Tokenizer.ts", "../node_modules/marked/src/Lexer.ts", "../node_modules/marked/src/Renderer.ts", "../node_modules/marked/src/TextRenderer.ts", "../node_modules/marked/src/Parser.ts", "../node_modules/marked/src/Hooks.ts", "../node_modules/marked/src/Instance.ts", "../node_modules/marked/src/marked.ts", "../node_modules/pbf/index.js", "../node_modules/@mapbox/point-geometry/index.js", "../node_modules/@mapbox/vector-tile/index.js", "../modules/services/osmose.js", "../modules/util/partition.ts", "../modules/services/mapillary.js", "../modules/services/maprules.js", "../modules/services/nominatim.js", "../node_modules/which-polygon/node_modules/quickselect/quickselect.js", "../node_modules/which-polygon/node_modules/rbush/index.js", "../node_modules/lineclip/index.js", "../node_modules/which-polygon/index.js", "../node_modules/name-suggestion-index/lib/matcher.ts", "../node_modules/name-suggestion-index/lib/simplify.ts", "../node_modules/name-suggestion-index/lib/stemmer.ts", "../modules/services/nsi.js", "../modules/util/date.ts", "../modules/services/kartaview.js", "../node_modules/@rapideditor/country-coder/src/country-coder.ts", "../modules/services/pannellum_photo.js", "../modules/services/plane_photo.js", "../modules/services/vegbilder.js", "../node_modules/osm-auth/src/osm-auth.mjs", "../modules/util/jxon.js", "../modules/services/osm.js", "../modules/services/osm_wikibase.js", "../modules/services/streetside.js", "../modules/services/taginfo.js", "../node_modules/@turf/helpers/index.ts", "../node_modules/@turf/invariant/index.ts", "../node_modules/@turf/bbox-clip/index.ts", "../node_modules/@turf/bbox-clip/lib/lineclip.ts", "../node_modules/fast-json-stable-stringify/index.js", "../node_modules/polygon-clipping/dist/polygon-clipping.umd.js", "../modules/services/vector_tile.js", "../modules/services/wikidata.js", "../modules/services/wikipedia.js", "../modules/services/mapilio.js", "../modules/services/panoramax.js", "../modules/services/index.js", "../modules/validations/almost_junction.js", "../modules/validations/close_nodes.js", "../modules/validations/crossing_ways.js", "../modules/behavior/draw_way.js", "../modules/modes/draw_line.js", "../modules/ui/cmd.js", "../modules/operations/delete.js", "../modules/validations/disconnected_way.js", "../modules/validations/missing_start_date.js", "../modules/validations/invalid_format.js", "../modules/validations/help_request.js", "../modules/validations/impossible_oneway.js", "../modules/validations/incompatible_source.js", "../modules/validations/maprules.js", "../modules/validations/mismatched_dates.js", "../modules/validations/mismatched_geometry.js", "../modules/validations/missing_role.js", "../modules/validations/missing_tag.js", "../modules/validations/mutually_exclusive_tags.js", "../modules/operations/split.js", "../modules/validations/osm_api_limits.js", "../modules/osm/deprecated.js", "../modules/validations/outdated_tags.js", "../modules/validations/private_data.js", "../modules/validations/suspicious_name.js", "../modules/validations/unsquare_way.js", "../modules/validations/index.js", "../modules/core/validator.js", "../modules/core/uploader.js", "../modules/modes/draw_area.js", "../modules/modes/add_area.js", "../modules/modes/add_line.js", "../modules/modes/add_point.js", "../modules/behavior/drag.js", "../modules/modes/drag_node.js", "../modules/modes/drag_note.js", "../modules/ui/data_header.js", "../modules/ui/combobox.js", "../modules/ui/toggle.js", "../modules/ui/disclosure.js", "../modules/ui/section.js", "../modules/ui/tag_reference.js", "../modules/ui/sections/raw_tag_editor.js", "../modules/ui/data_editor.js", "../modules/modes/select_data.js", "../modules/ui/osmose_details.js", "../modules/ui/osmose_header.js", "../modules/ui/view_on_osmose.js", "../modules/ui/osmose_editor.js", "../modules/modes/select_error.js", "../modules/behavior/select.js", "../modules/ui/note_comments.js", "../modules/ui/note_header.js", "../modules/ui/note_report.js", "../modules/ui/view_on_osm.js", "../modules/ui/note_editor.js", "../modules/modes/select_note.js", "../modules/modes/add_note.js", "../modules/operations/move.js", "../modules/svg/helpers.js", "../modules/svg/tag_classes.js", "../modules/svg/tag_pattern.js", "../modules/svg/areas.js", "../node_modules/@tmcw/togeojson/lib/lib/shared.ts", "../node_modules/@tmcw/togeojson/lib/lib/gpx/extensions.ts", "../node_modules/@tmcw/togeojson/lib/lib/gpx/coord_pair.ts", "../node_modules/@tmcw/togeojson/lib/lib/gpx/line.ts", "../node_modules/@tmcw/togeojson/lib/lib/gpx/properties.ts", "../node_modules/@tmcw/togeojson/lib/lib/gpx.ts", "../node_modules/@tmcw/togeojson/lib/lib/tcx.ts", "../node_modules/@tmcw/togeojson/lib/lib/kml/fixColor.ts", "../node_modules/@tmcw/togeojson/lib/lib/kml/extractStyle.ts", "../node_modules/@tmcw/togeojson/lib/lib/kml/geometry.ts", "../node_modules/@tmcw/togeojson/lib/lib/kml/shared.ts", "../node_modules/@tmcw/togeojson/lib/lib/kml/ground_overlay.ts", "../node_modules/@tmcw/togeojson/lib/lib/kml/networklink.ts", "../node_modules/@tmcw/togeojson/lib/lib/kml/placemark.ts", "../node_modules/@tmcw/togeojson/lib/lib/kml.ts", "../modules/svg/data.js", "../modules/svg/debug.js", "../modules/svg/defs.js", "../modules/svg/geolocate.js", "../modules/svg/labels.js", "../node_modules/exifr/dist/full.esm.mjs", "../modules/svg/local_photos.js", "../modules/svg/osmose.js", "../modules/svg/streetside.js", "../modules/svg/vegbilder.js", "../modules/svg/mapillary_images.js", "../modules/svg/mapillary_position.js", "../modules/svg/mapillary_signs.js", "../modules/svg/mapillary_map_features.js", "../modules/svg/kartaview_images.js", "../modules/svg/mapilio_images.js", "../modules/svg/panoramax_images.js", "../modules/svg/osm.js", "../modules/svg/notes.js", "../modules/svg/touch.js", "../modules/util/dimensions.js", "../modules/svg/layers.js", "../modules/svg/lines.js", "../modules/svg/midpoints.js", "../modules/svg/points.js", "../modules/svg/turns.js", "../modules/svg/vertices.js", "../modules/svg/index.js", "../modules/operations/orthogonalize.js", "../modules/operations/reflect.js", "../modules/modes/rotate.js", "../modules/ui/conflicts.js", "../modules/ui/confirm.js", "../modules/ui/popover.js", "../modules/ui/tooltip.js", "../node_modules/bignumber.js/bignumber.mjs", "../node_modules/splaytree-ts/src/index.ts", "../node_modules/polyclip-ts/src/geom-in.ts", "../node_modules/polyclip-ts/src/constant.ts", "../node_modules/polyclip-ts/src/compare.ts", "../node_modules/polyclip-ts/src/orient.ts", "../node_modules/polyclip-ts/src/snap.ts", "../node_modules/polyclip-ts/src/identity.ts", "../node_modules/polyclip-ts/src/precision.ts", "../node_modules/polyclip-ts/src/bbox.ts", "../node_modules/polyclip-ts/src/operation.ts", "../node_modules/polyclip-ts/src/vector.ts", "../node_modules/polyclip-ts/src/sweep-event.ts", "../node_modules/polyclip-ts/src/geom-out.ts", "../node_modules/polyclip-ts/src/sweep-line.ts", "../node_modules/polyclip-ts/src/segment.ts", "../node_modules/polyclip-ts/src/index.ts", "../node_modules/wgs84/index.js", "../node_modules/@mapbox/geojson-area/index.js", "../node_modules/circle-to-polygon/input-validation/validateCenter.js", "../node_modules/circle-to-polygon/input-validation/validateRadius.js", "../node_modules/circle-to-polygon/input-validation/validateNumberOfEdges.js", "../node_modules/circle-to-polygon/input-validation/validateEarthRadius.js", "../node_modules/circle-to-polygon/input-validation/validateBearing.js", "../node_modules/circle-to-polygon/input-validation/index.js", "../node_modules/circle-to-polygon/index.js", "../node_modules/geojson-precision/index.js", "../node_modules/@rapideditor/location-conflation/node_modules/json-stringify-pretty-compact/index.js", "../node_modules/@rapideditor/location-conflation/src/location-conflation.ts", "../modules/core/LocationManager.ts", "../modules/ui/intro/helper.js", "../modules/ui/field_help.js", "../modules/ui/fields/check.js", "../modules/ui/length_indicator.js", "../modules/ui/fields/combo.js", "../modules/behavior/hash.js", "../modules/behavior/index.js", "../modules/ui/account.js", "../modules/ui/attribution.js", "../modules/ui/contributors.js", "../modules/ui/edit_menu.js", "../modules/ui/feature_info.js", "../modules/ui/flash.js", "../modules/ui/full_screen.js", "../modules/ui/geolocate.js", "../modules/ui/panels/background.js", "../modules/ui/panels/history.js", "../modules/ui/panels/location.js", "../modules/ui/panels/measurement.js", "../modules/ui/panels/index.js", "../modules/ui/info.js", "../modules/ui/curtain.js", "../modules/ui/intro/welcome.js", "../modules/ui/intro/navigation.js", "../modules/ui/intro/point.js", "../modules/ui/intro/area.js", "../modules/ui/intro/line.js", "../modules/ui/intro/building.js", "../modules/ui/intro/start_editing.js", "../modules/ui/intro/intro.js", "../modules/ui/intro/index.js", "../modules/ui/issues_info.js", "../modules/util/IntervalTasksQueue.ts", "../modules/renderer/background_source.js", "../node_modules/@turf/meta/index.ts", "../node_modules/@turf/bbox/index.ts", "../modules/renderer/tile_layer.js", "../modules/renderer/background.js", "../modules/renderer/features.js", "../modules/util/bind_once.js", "../modules/util/zoom_pan.js", "../modules/util/double_up.js", "../modules/renderer/map.js", "../modules/renderer/photos.js", "../modules/renderer/index.js", "../modules/ui/map_in_map.js", "../modules/ui/notice.js", "../modules/ui/photoviewer.js", "../modules/ui/restore.js", "../modules/ui/scale.js", "../modules/ui/shortcuts.js", "../node_modules/@mapbox/sexagesimal/index.js", "../modules/ui/feature_list.js", "../modules/ui/sections/entity_issues.js", "../modules/ui/preset_icon.js", "../modules/ui/sections/feature_type.js", "../modules/ui/form_fields.js", "../modules/ui/sections/preset_fields.js", "../modules/ui/sections/raw_member_editor.js", "../modules/ui/sections/raw_membership_editor.js", "../modules/ui/sections/selection_list.js", "../modules/ui/entity_editor.js", "../modules/ui/preset_list.js", "../modules/ui/inspector.js", "../modules/ui/sidebar.js", "../modules/ui/source_switch.js", "../modules/ui/spinner.js", "../modules/ui/sections/privacy.js", "../modules/ui/splash.js", "../modules/ui/status.js", "../modules/ui/tools/modes.js", "../modules/ui/tools/notes.js", "../modules/ui/tools/save.js", "../modules/ui/tools/sidebar_toggle.js", "../modules/ui/tools/undo_redo.js", "../modules/ui/tools/index.js", "../modules/ui/top_toolbar.js", "../modules/ui/version.js", "../modules/ui/zoom.js", "../modules/ui/zoom_to_selection.js", "../modules/ui/pane.js", "../modules/ui/sections/background_display_options.js", "../modules/ui/settings/custom_background.js", "../modules/ui/sections/background_list.js", "../modules/ui/sections/background_offset.js", "../modules/ui/sections/overlay_list.js", "../modules/ui/panes/background.js", "../modules/ui/panes/help.js", "../modules/ui/sections/validation_issues.js", "../modules/ui/sections/validation_options.js", "../modules/ui/sections/validation_rules.js", "../modules/ui/sections/validation_status.js", "../modules/ui/panes/issues.js", "../modules/ui/settings/custom_data.js", "../modules/ui/sections/data_layers.js", "../modules/ui/sections/map_features.js", "../modules/ui/sections/map_style_options.js", "../modules/ui/settings/local_photos.js", "../modules/ui/sections/photo_overlays.js", "../modules/ui/sections/map_daterange.js", "../modules/ui/panes/map_data.js", "../modules/ui/panes/preferences.js", "../modules/ui/init.js", "../modules/ui/commit_warnings.js", "../modules/ui/lasso.js", "../modules/ui/source_subfield.js", "../node_modules/osm-community-index/lib/simplify.ts", "../node_modules/osm-community-index/lib/resolve_strings.ts", "../modules/ui/success.js", "../modules/ui/index.js", "../modules/ui/fields/input.js", "../modules/ui/fields/access.js", "../modules/ui/fields/address.js", "../modules/ui/fields/date.js", "../modules/ui/fields/directional_combo.js", "../modules/ui/fields/lanes.js", "../modules/ui/fields/localized.js", "../modules/ui/fields/roadheight.js", "../modules/ui/fields/roadspeed.js", "../modules/ui/fields/radio.js", "../modules/ui/fields/restrictions.js", "../modules/ui/fields/textarea.js", "../modules/ui/fields/wikidata.js", "../modules/ui/fields/wikipedia.js", "../modules/ui/fields/sources.js", "../modules/ui/fields/index.js", "../modules/ui/field.js", "../modules/ui/changeset_editor.js", "../modules/ui/sections/changes.js", "../modules/ui/commit.js", "../modules/modes/save.js", "../modules/modes/index.js", "../modules/core/context.js", "../modules/core/index.ts", "../modules/behavior/operation.js", "../modules/operations/circularize.js", "../modules/operations/rotate.js", "../modules/modes/move.js", "../modules/operations/continue.js", "../modules/operations/copy.js", "../modules/operations/disconnect.js", "../modules/operations/downgrade.js", "../modules/operations/extract.js", "../modules/operations/merge.js", "../modules/operations/paste.js", "../modules/operations/reverse.js", "../modules/operations/straighten.js", "../modules/operations/index.js", "../modules/behavior/paste.js", "../modules/modes/select.js", "../modules/behavior/lasso.js", "../modules/modes/browse.js", "../modules/behavior/add_way.js", "../modules/globals.d.ts", "../modules/presets/collection.js", "../modules/presets/category.js", "../modules/presets/field.js", "../modules/presets/preset.js", "../modules/ui/panes/index.js", "../modules/ui/sections/index.js", "../modules/ui/settings/index.js", "../modules/core/file_fetcher.js", "../modules/presets/index.js", "../modules/actions/reverse.js", "../modules/osm/multipolygon.js", "../modules/actions/add_member.js", "../modules/actions/index.js", "../modules/index.js", "../modules/id.js"], - "sourcesContent": ["/* eslint-disable no-prototype-builtins */\nvar g =\n (typeof globalThis !== 'undefined' && globalThis) ||\n (typeof self !== 'undefined' && self) ||\n // eslint-disable-next-line no-undef\n (typeof global !== 'undefined' && global) ||\n {}\n\nvar support = {\n searchParams: 'URLSearchParams' in g,\n iterable: 'Symbol' in g && 'iterator' in Symbol,\n blob:\n 'FileReader' in g &&\n 'Blob' in g &&\n (function() {\n try {\n new Blob()\n return true\n } catch (e) {\n return false\n }\n })(),\n formData: 'FormData' in g,\n arrayBuffer: 'ArrayBuffer' in g\n}\n\nfunction isDataView(obj) {\n return obj && DataView.prototype.isPrototypeOf(obj)\n}\n\nif (support.arrayBuffer) {\n var viewClasses = [\n '[object Int8Array]',\n '[object Uint8Array]',\n '[object Uint8ClampedArray]',\n '[object Int16Array]',\n '[object Uint16Array]',\n '[object Int32Array]',\n '[object Uint32Array]',\n '[object Float32Array]',\n '[object Float64Array]'\n ]\n\n var isArrayBufferView =\n ArrayBuffer.isView ||\n function(obj) {\n return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1\n }\n}\n\nfunction normalizeName(name) {\n if (typeof name !== 'string') {\n name = String(name)\n }\n if (/[^a-z0-9\\-#$%&'*+.^_`|~!]/i.test(name) || name === '') {\n throw new TypeError('Invalid character in header field name: \"' + name + '\"')\n }\n return name.toLowerCase()\n}\n\nfunction normalizeValue(value) {\n if (typeof value !== 'string') {\n value = String(value)\n }\n return value\n}\n\n// Build a destructive iterator for the value list\nfunction iteratorFor(items) {\n var iterator = {\n next: function() {\n var value = items.shift()\n return {done: value === undefined, value: value}\n }\n }\n\n if (support.iterable) {\n iterator[Symbol.iterator] = function() {\n return iterator\n }\n }\n\n return iterator\n}\n\nexport function Headers(headers) {\n this.map = {}\n\n if (headers instanceof Headers) {\n headers.forEach(function(value, name) {\n this.append(name, value)\n }, this)\n } else if (Array.isArray(headers)) {\n headers.forEach(function(header) {\n if (header.length != 2) {\n throw new TypeError('Headers constructor: expected name/value pair to be length 2, found' + header.length)\n }\n this.append(header[0], header[1])\n }, this)\n } else if (headers) {\n Object.getOwnPropertyNames(headers).forEach(function(name) {\n this.append(name, headers[name])\n }, this)\n }\n}\n\nHeaders.prototype.append = function(name, value) {\n name = normalizeName(name)\n value = normalizeValue(value)\n var oldValue = this.map[name]\n this.map[name] = oldValue ? oldValue + ', ' + value : value\n}\n\nHeaders.prototype['delete'] = function(name) {\n delete this.map[normalizeName(name)]\n}\n\nHeaders.prototype.get = function(name) {\n name = normalizeName(name)\n return this.has(name) ? this.map[name] : null\n}\n\nHeaders.prototype.has = function(name) {\n return this.map.hasOwnProperty(normalizeName(name))\n}\n\nHeaders.prototype.set = function(name, value) {\n this.map[normalizeName(name)] = normalizeValue(value)\n}\n\nHeaders.prototype.forEach = function(callback, thisArg) {\n for (var name in this.map) {\n if (this.map.hasOwnProperty(name)) {\n callback.call(thisArg, this.map[name], name, this)\n }\n }\n}\n\nHeaders.prototype.keys = function() {\n var items = []\n this.forEach(function(value, name) {\n items.push(name)\n })\n return iteratorFor(items)\n}\n\nHeaders.prototype.values = function() {\n var items = []\n this.forEach(function(value) {\n items.push(value)\n })\n return iteratorFor(items)\n}\n\nHeaders.prototype.entries = function() {\n var items = []\n this.forEach(function(value, name) {\n items.push([name, value])\n })\n return iteratorFor(items)\n}\n\nif (support.iterable) {\n Headers.prototype[Symbol.iterator] = Headers.prototype.entries\n}\n\nfunction consumed(body) {\n if (body._noBody) return\n if (body.bodyUsed) {\n return Promise.reject(new TypeError('Already read'))\n }\n body.bodyUsed = true\n}\n\nfunction fileReaderReady(reader) {\n return new Promise(function(resolve, reject) {\n reader.onload = function() {\n resolve(reader.result)\n }\n reader.onerror = function() {\n reject(reader.error)\n }\n })\n}\n\nfunction readBlobAsArrayBuffer(blob) {\n var reader = new FileReader()\n var promise = fileReaderReady(reader)\n reader.readAsArrayBuffer(blob)\n return promise\n}\n\nfunction readBlobAsText(blob) {\n var reader = new FileReader()\n var promise = fileReaderReady(reader)\n var match = /charset=([A-Za-z0-9_-]+)/.exec(blob.type)\n var encoding = match ? match[1] : 'utf-8'\n reader.readAsText(blob, encoding)\n return promise\n}\n\nfunction readArrayBufferAsText(buf) {\n var view = new Uint8Array(buf)\n var chars = new Array(view.length)\n\n for (var i = 0; i < view.length; i++) {\n chars[i] = String.fromCharCode(view[i])\n }\n return chars.join('')\n}\n\nfunction bufferClone(buf) {\n if (buf.slice) {\n return buf.slice(0)\n } else {\n var view = new Uint8Array(buf.byteLength)\n view.set(new Uint8Array(buf))\n return view.buffer\n }\n}\n\nfunction Body() {\n this.bodyUsed = false\n\n this._initBody = function(body) {\n /*\n fetch-mock wraps the Response object in an ES6 Proxy to\n provide useful test harness features such as flush. However, on\n ES5 browsers without fetch or Proxy support pollyfills must be used;\n the proxy-pollyfill is unable to proxy an attribute unless it exists\n on the object before the Proxy is created. This change ensures\n Response.bodyUsed exists on the instance, while maintaining the\n semantic of setting Request.bodyUsed in the constructor before\n _initBody is called.\n */\n // eslint-disable-next-line no-self-assign\n this.bodyUsed = this.bodyUsed\n this._bodyInit = body\n if (!body) {\n this._noBody = true;\n this._bodyText = ''\n } else if (typeof body === 'string') {\n this._bodyText = body\n } else if (support.blob && Blob.prototype.isPrototypeOf(body)) {\n this._bodyBlob = body\n } else if (support.formData && FormData.prototype.isPrototypeOf(body)) {\n this._bodyFormData = body\n } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {\n this._bodyText = body.toString()\n } else if (support.arrayBuffer && support.blob && isDataView(body)) {\n this._bodyArrayBuffer = bufferClone(body.buffer)\n // IE 10-11 can't handle a DataView body.\n this._bodyInit = new Blob([this._bodyArrayBuffer])\n } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {\n this._bodyArrayBuffer = bufferClone(body)\n } else {\n this._bodyText = body = Object.prototype.toString.call(body)\n }\n\n if (!this.headers.get('content-type')) {\n if (typeof body === 'string') {\n this.headers.set('content-type', 'text/plain;charset=UTF-8')\n } else if (this._bodyBlob && this._bodyBlob.type) {\n this.headers.set('content-type', this._bodyBlob.type)\n } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {\n this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8')\n }\n }\n }\n\n if (support.blob) {\n this.blob = function() {\n var rejected = consumed(this)\n if (rejected) {\n return rejected\n }\n\n if (this._bodyBlob) {\n return Promise.resolve(this._bodyBlob)\n } else if (this._bodyArrayBuffer) {\n return Promise.resolve(new Blob([this._bodyArrayBuffer]))\n } else if (this._bodyFormData) {\n throw new Error('could not read FormData body as blob')\n } else {\n return Promise.resolve(new Blob([this._bodyText]))\n }\n }\n }\n\n this.arrayBuffer = function() {\n if (this._bodyArrayBuffer) {\n var isConsumed = consumed(this)\n if (isConsumed) {\n return isConsumed\n } else if (ArrayBuffer.isView(this._bodyArrayBuffer)) {\n return Promise.resolve(\n this._bodyArrayBuffer.buffer.slice(\n this._bodyArrayBuffer.byteOffset,\n this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength\n )\n )\n } else {\n return Promise.resolve(this._bodyArrayBuffer)\n }\n } else if (support.blob) {\n return this.blob().then(readBlobAsArrayBuffer)\n } else {\n throw new Error('could not read as ArrayBuffer')\n }\n }\n\n this.text = function() {\n var rejected = consumed(this)\n if (rejected) {\n return rejected\n }\n\n if (this._bodyBlob) {\n return readBlobAsText(this._bodyBlob)\n } else if (this._bodyArrayBuffer) {\n return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))\n } else if (this._bodyFormData) {\n throw new Error('could not read FormData body as text')\n } else {\n return Promise.resolve(this._bodyText)\n }\n }\n\n if (support.formData) {\n this.formData = function() {\n return this.text().then(decode)\n }\n }\n\n this.json = function() {\n return this.text().then(JSON.parse)\n }\n\n return this\n}\n\n// HTTP methods whose capitalization should be normalized\nvar methods = ['CONNECT', 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'TRACE']\n\nfunction normalizeMethod(method) {\n var upcased = method.toUpperCase()\n return methods.indexOf(upcased) > -1 ? upcased : method\n}\n\nexport function Request(input, options) {\n if (!(this instanceof Request)) {\n throw new TypeError('Please use the \"new\" operator, this DOM object constructor cannot be called as a function.')\n }\n\n options = options || {}\n var body = options.body\n\n if (input instanceof Request) {\n if (input.bodyUsed) {\n throw new TypeError('Already read')\n }\n this.url = input.url\n this.credentials = input.credentials\n if (!options.headers) {\n this.headers = new Headers(input.headers)\n }\n this.method = input.method\n this.mode = input.mode\n this.signal = input.signal\n if (!body && input._bodyInit != null) {\n body = input._bodyInit\n input.bodyUsed = true\n }\n } else {\n this.url = String(input)\n }\n\n this.credentials = options.credentials || this.credentials || 'same-origin'\n if (options.headers || !this.headers) {\n this.headers = new Headers(options.headers)\n }\n this.method = normalizeMethod(options.method || this.method || 'GET')\n this.mode = options.mode || this.mode || null\n this.signal = options.signal || this.signal || (function () {\n if ('AbortController' in g) {\n var ctrl = new AbortController();\n return ctrl.signal;\n }\n }());\n this.referrer = null\n\n if ((this.method === 'GET' || this.method === 'HEAD') && body) {\n throw new TypeError('Body not allowed for GET or HEAD requests')\n }\n this._initBody(body)\n\n if (this.method === 'GET' || this.method === 'HEAD') {\n if (options.cache === 'no-store' || options.cache === 'no-cache') {\n // Search for a '_' parameter in the query string\n var reParamSearch = /([?&])_=[^&]*/\n if (reParamSearch.test(this.url)) {\n // If it already exists then set the value with the current time\n this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime())\n } else {\n // Otherwise add a new '_' parameter to the end with the current time\n var reQueryString = /\\?/\n this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime()\n }\n }\n }\n}\n\nRequest.prototype.clone = function() {\n return new Request(this, {body: this._bodyInit})\n}\n\nfunction decode(body) {\n var form = new FormData()\n body\n .trim()\n .split('&')\n .forEach(function(bytes) {\n if (bytes) {\n var split = bytes.split('=')\n var name = split.shift().replace(/\\+/g, ' ')\n var value = split.join('=').replace(/\\+/g, ' ')\n form.append(decodeURIComponent(name), decodeURIComponent(value))\n }\n })\n return form\n}\n\nfunction parseHeaders(rawHeaders) {\n var headers = new Headers()\n // Replace instances of \\r\\n and \\n followed by at least one space or horizontal tab with a space\n // https://tools.ietf.org/html/rfc7230#section-3.2\n var preProcessedHeaders = rawHeaders.replace(/\\r?\\n[\\t ]+/g, ' ')\n // Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill\n // https://github.com/github/fetch/issues/748\n // https://github.com/zloirock/core-js/issues/751\n preProcessedHeaders\n .split('\\r')\n .map(function(header) {\n return header.indexOf('\\n') === 0 ? header.substr(1, header.length) : header\n })\n .forEach(function(line) {\n var parts = line.split(':')\n var key = parts.shift().trim()\n if (key) {\n var value = parts.join(':').trim()\n try {\n headers.append(key, value)\n } catch (error) {\n console.warn('Response ' + error.message)\n }\n }\n })\n return headers\n}\n\nBody.call(Request.prototype)\n\nexport function Response(bodyInit, options) {\n if (!(this instanceof Response)) {\n throw new TypeError('Please use the \"new\" operator, this DOM object constructor cannot be called as a function.')\n }\n if (!options) {\n options = {}\n }\n\n this.type = 'default'\n this.status = options.status === undefined ? 200 : options.status\n if (this.status < 200 || this.status > 599) {\n throw new RangeError(\"Failed to construct 'Response': The status provided (0) is outside the range [200, 599].\")\n }\n this.ok = this.status >= 200 && this.status < 300\n this.statusText = options.statusText === undefined ? '' : '' + options.statusText\n this.headers = new Headers(options.headers)\n this.url = options.url || ''\n this._initBody(bodyInit)\n}\n\nBody.call(Response.prototype)\n\nResponse.prototype.clone = function() {\n return new Response(this._bodyInit, {\n status: this.status,\n statusText: this.statusText,\n headers: new Headers(this.headers),\n url: this.url\n })\n}\n\nResponse.error = function() {\n var response = new Response(null, {status: 200, statusText: ''})\n response.ok = false\n response.status = 0\n response.type = 'error'\n return response\n}\n\nvar redirectStatuses = [301, 302, 303, 307, 308]\n\nResponse.redirect = function(url, status) {\n if (redirectStatuses.indexOf(status) === -1) {\n throw new RangeError('Invalid status code')\n }\n\n return new Response(null, {status: status, headers: {location: url}})\n}\n\nexport var DOMException = g.DOMException\ntry {\n new DOMException()\n} catch (err) {\n DOMException = function(message, name) {\n this.message = message\n this.name = name\n var error = Error(message)\n this.stack = error.stack\n }\n DOMException.prototype = Object.create(Error.prototype)\n DOMException.prototype.constructor = DOMException\n}\n\nexport function fetch(input, init) {\n return new Promise(function(resolve, reject) {\n var request = new Request(input, init)\n\n if (request.signal && request.signal.aborted) {\n return reject(new DOMException('Aborted', 'AbortError'))\n }\n\n var xhr = new XMLHttpRequest()\n\n function abortXhr() {\n xhr.abort()\n }\n\n xhr.onload = function() {\n var options = {\n statusText: xhr.statusText,\n headers: parseHeaders(xhr.getAllResponseHeaders() || '')\n }\n // This check if specifically for when a user fetches a file locally from the file system\n // Only if the status is out of a normal range\n if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) {\n options.status = 200;\n } else {\n options.status = xhr.status;\n }\n options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL')\n var body = 'response' in xhr ? xhr.response : xhr.responseText\n setTimeout(function() {\n resolve(new Response(body, options))\n }, 0)\n }\n\n xhr.onerror = function() {\n setTimeout(function() {\n reject(new TypeError('Network request failed'))\n }, 0)\n }\n\n xhr.ontimeout = function() {\n setTimeout(function() {\n reject(new TypeError('Network request timed out'))\n }, 0)\n }\n\n xhr.onabort = function() {\n setTimeout(function() {\n reject(new DOMException('Aborted', 'AbortError'))\n }, 0)\n }\n\n function fixUrl(url) {\n try {\n return url === '' && g.location.href ? g.location.href : url\n } catch (e) {\n return url\n }\n }\n\n xhr.open(request.method, fixUrl(request.url), true)\n\n if (request.credentials === 'include') {\n xhr.withCredentials = true\n } else if (request.credentials === 'omit') {\n xhr.withCredentials = false\n }\n\n if ('responseType' in xhr) {\n if (support.blob) {\n xhr.responseType = 'blob'\n } else if (\n support.arrayBuffer\n ) {\n xhr.responseType = 'arraybuffer'\n }\n }\n\n if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers || (g.Headers && init.headers instanceof g.Headers))) {\n var names = [];\n Object.getOwnPropertyNames(init.headers).forEach(function(name) {\n names.push(normalizeName(name))\n xhr.setRequestHeader(name, normalizeValue(init.headers[name]))\n })\n request.headers.forEach(function(value, name) {\n if (names.indexOf(name) === -1) {\n xhr.setRequestHeader(name, value)\n }\n })\n } else {\n request.headers.forEach(function(value, name) {\n xhr.setRequestHeader(name, value)\n })\n }\n\n if (request.signal) {\n request.signal.addEventListener('abort', abortXhr)\n\n xhr.onreadystatechange = function() {\n // DONE (success or failure)\n if (xhr.readyState === 4) {\n request.signal.removeEventListener('abort', abortXhr)\n }\n }\n }\n\n xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)\n })\n}\n\nfetch.polyfill = true\n\nif (!g.fetch) {\n g.fetch = fetch\n g.Headers = Headers\n g.Request = Request\n g.Response = Response\n}\n", "(function (factory) {\n typeof define === 'function' && define.amd ? define(factory) :\n factory();\n})((function () { 'use strict';\n\n function _arrayLikeToArray(r, a) {\n (null == a || a > r.length) && (a = r.length);\n for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];\n return n;\n }\n function _assertThisInitialized(e) {\n if (void 0 === e) throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");\n return e;\n }\n function _callSuper(t, o, e) {\n return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e));\n }\n function _classCallCheck(a, n) {\n if (!(a instanceof n)) throw new TypeError(\"Cannot call a class as a function\");\n }\n function _defineProperties(e, r) {\n for (var t = 0; t < r.length; t++) {\n var o = r[t];\n o.enumerable = o.enumerable || !1, o.configurable = !0, \"value\" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o);\n }\n }\n function _createClass(e, r, t) {\n return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, \"prototype\", {\n writable: !1\n }), e;\n }\n function _createForOfIteratorHelper(r, e) {\n var t = \"undefined\" != typeof Symbol && r[Symbol.iterator] || r[\"@@iterator\"];\n if (!t) {\n if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && \"number\" == typeof r.length) {\n t && (r = t);\n var n = 0,\n F = function () {};\n return {\n s: F,\n n: function () {\n return n >= r.length ? {\n done: !0\n } : {\n done: !1,\n value: r[n++]\n };\n },\n e: function (r) {\n throw r;\n },\n f: F\n };\n }\n throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\");\n }\n var o,\n a = !0,\n u = !1;\n return {\n s: function () {\n t = t.call(r);\n },\n n: function () {\n var r = t.next();\n return a = r.done, r;\n },\n e: function (r) {\n u = !0, o = r;\n },\n f: function () {\n try {\n a || null == t.return || t.return();\n } finally {\n if (u) throw o;\n }\n }\n };\n }\n function _get() {\n return _get = \"undefined\" != typeof Reflect && Reflect.get ? Reflect.get.bind() : function (e, t, r) {\n var p = _superPropBase(e, t);\n if (p) {\n var n = Object.getOwnPropertyDescriptor(p, t);\n return n.get ? n.get.call(arguments.length < 3 ? e : r) : n.value;\n }\n }, _get.apply(null, arguments);\n }\n function _getPrototypeOf(t) {\n return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) {\n return t.__proto__ || Object.getPrototypeOf(t);\n }, _getPrototypeOf(t);\n }\n function _inherits(t, e) {\n if (\"function\" != typeof e && null !== e) throw new TypeError(\"Super expression must either be null or a function\");\n t.prototype = Object.create(e && e.prototype, {\n constructor: {\n value: t,\n writable: !0,\n configurable: !0\n }\n }), Object.defineProperty(t, \"prototype\", {\n writable: !1\n }), e && _setPrototypeOf(t, e);\n }\n function _isNativeReflectConstruct() {\n try {\n var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));\n } catch (t) {}\n return (_isNativeReflectConstruct = function () {\n return !!t;\n })();\n }\n function _possibleConstructorReturn(t, e) {\n if (e && (\"object\" == typeof e || \"function\" == typeof e)) return e;\n if (void 0 !== e) throw new TypeError(\"Derived constructors may only return object or undefined\");\n return _assertThisInitialized(t);\n }\n function _setPrototypeOf(t, e) {\n return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) {\n return t.__proto__ = e, t;\n }, _setPrototypeOf(t, e);\n }\n function _superPropBase(t, o) {\n for (; !{}.hasOwnProperty.call(t, o) && null !== (t = _getPrototypeOf(t)););\n return t;\n }\n function _superPropGet(t, o, e, r) {\n var p = _get(_getPrototypeOf(1 & r ? t.prototype : t), o, e);\n return 2 & r && \"function\" == typeof p ? function (t) {\n return p.apply(e, t);\n } : p;\n }\n function _toPrimitive(t, r) {\n if (\"object\" != typeof t || !t) return t;\n var e = t[Symbol.toPrimitive];\n if (void 0 !== e) {\n var i = e.call(t, r || \"default\");\n if (\"object\" != typeof i) return i;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (\"string\" === r ? String : Number)(t);\n }\n function _toPropertyKey(t) {\n var i = _toPrimitive(t, \"string\");\n return \"symbol\" == typeof i ? i : i + \"\";\n }\n function _unsupportedIterableToArray(r, a) {\n if (r) {\n if (\"string\" == typeof r) return _arrayLikeToArray(r, a);\n var t = {}.toString.call(r).slice(8, -1);\n return \"Object\" === t && r.constructor && (t = r.constructor.name), \"Map\" === t || \"Set\" === t ? Array.from(r) : \"Arguments\" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;\n }\n }\n\n (function (self) {\n return {\n NativeAbortSignal: self.AbortSignal,\n NativeAbortController: self.AbortController\n };\n })(typeof self !== 'undefined' ? self : global);\n\n /**\n * @param {any} reason abort reason\n */\n function createAbortEvent(reason) {\n var event;\n try {\n event = new Event('abort');\n } catch (e) {\n if (typeof document !== 'undefined') {\n if (!document.createEvent) {\n // For Internet Explorer 8:\n event = document.createEventObject();\n event.type = 'abort';\n } else {\n // For Internet Explorer 11:\n event = document.createEvent('Event');\n event.initEvent('abort', false, false);\n }\n } else {\n // Fallback where document isn't available:\n event = {\n type: 'abort',\n bubbles: false,\n cancelable: false\n };\n }\n }\n event.reason = reason;\n return event;\n }\n\n /**\n * @param {any} reason abort reason\n */\n function normalizeAbortReason(reason) {\n if (reason === undefined) {\n if (typeof document === 'undefined') {\n reason = new Error('This operation was aborted');\n reason.name = 'AbortError';\n } else {\n try {\n reason = new DOMException('signal is aborted without reason');\n // The DOMException does not support setting the name property directly.\n Object.defineProperty(reason, 'name', {\n value: 'AbortError'\n });\n } catch (err) {\n // IE 11 does not support calling the DOMException constructor, use a\n // regular error object on it instead.\n reason = new Error('This operation was aborted');\n reason.name = 'AbortError';\n }\n }\n }\n return reason;\n }\n\n var Emitter = /*#__PURE__*/function () {\n function Emitter() {\n _classCallCheck(this, Emitter);\n Object.defineProperty(this, 'listeners', {\n value: {},\n writable: true,\n configurable: true\n });\n }\n return _createClass(Emitter, [{\n key: \"addEventListener\",\n value: function addEventListener(type, callback, options) {\n if (!(type in this.listeners)) {\n this.listeners[type] = [];\n }\n this.listeners[type].push({\n callback: callback,\n options: options\n });\n }\n }, {\n key: \"removeEventListener\",\n value: function removeEventListener(type, callback) {\n if (!(type in this.listeners)) {\n return;\n }\n var stack = this.listeners[type];\n for (var i = 0, l = stack.length; i < l; i++) {\n if (stack[i].callback === callback) {\n stack.splice(i, 1);\n return;\n }\n }\n }\n }, {\n key: \"dispatchEvent\",\n value: function dispatchEvent(event) {\n var _this = this;\n if (!(event.type in this.listeners)) {\n return;\n }\n var stack = this.listeners[event.type];\n var stackToCall = stack.slice();\n var _loop = function _loop() {\n var listener = stackToCall[i];\n try {\n listener.callback.call(_this, event);\n } catch (e) {\n Promise.resolve().then(function () {\n throw e;\n });\n }\n if (listener.options && listener.options.once) {\n _this.removeEventListener(event.type, listener.callback);\n }\n };\n for (var i = 0, l = stackToCall.length; i < l; i++) {\n _loop();\n }\n return !event.defaultPrevented;\n }\n }]);\n }();\n var AbortSignal = /*#__PURE__*/function (_Emitter) {\n function AbortSignal() {\n var _this2;\n _classCallCheck(this, AbortSignal);\n _this2 = _callSuper(this, AbortSignal);\n // Some versions of babel does not transpile super() correctly for IE <= 10, if the parent\n // constructor has failed to run, then \"this.listeners\" will still be undefined and then we call\n // the parent constructor directly instead as a workaround. For general details, see babel bug:\n // https://github.com/babel/babel/issues/3041\n // This hack was added as a fix for the issue described here:\n // https://github.com/Financial-Times/polyfill-library/pull/59#issuecomment-477558042\n if (!_this2.listeners) {\n Emitter.call(_this2);\n }\n\n // Compared to assignment, Object.defineProperty makes properties non-enumerable by default and\n // we want Object.keys(new AbortController().signal) to be [] for compat with the native impl\n Object.defineProperty(_this2, 'aborted', {\n value: false,\n writable: true,\n configurable: true\n });\n Object.defineProperty(_this2, 'onabort', {\n value: null,\n writable: true,\n configurable: true\n });\n Object.defineProperty(_this2, 'reason', {\n value: undefined,\n writable: true,\n configurable: true\n });\n return _this2;\n }\n _inherits(AbortSignal, _Emitter);\n return _createClass(AbortSignal, [{\n key: \"toString\",\n value: function toString() {\n return '[object AbortSignal]';\n }\n }, {\n key: \"dispatchEvent\",\n value: function dispatchEvent(event) {\n if (event.type === 'abort') {\n this.aborted = true;\n if (typeof this.onabort === 'function') {\n this.onabort.call(this, event);\n }\n }\n _superPropGet(AbortSignal, \"dispatchEvent\", this, 3)([event]);\n }\n\n /**\n * @see {@link https://developer.mozilla.org/zh-CN/docs/Web/API/AbortSignal/throwIfAborted}\n */\n }, {\n key: \"throwIfAborted\",\n value: function throwIfAborted() {\n var aborted = this.aborted,\n _this$reason = this.reason,\n reason = _this$reason === void 0 ? 'Aborted' : _this$reason;\n if (!aborted) return;\n throw reason;\n }\n\n /**\n * @see {@link https://developer.mozilla.org/zh-CN/docs/Web/API/AbortSignal/timeout_static}\n * @param {number} time The \"active\" time in milliseconds before the returned {@link AbortSignal} will abort.\n * The value must be within range of 0 and {@link Number.MAX_SAFE_INTEGER}.\n * @returns {AbortSignal} The signal will abort with its {@link AbortSignal.reason} property set to a `TimeoutError` {@link DOMException} on timeout,\n * or an `AbortError` {@link DOMException} if the operation was user-triggered.\n */\n }], [{\n key: \"timeout\",\n value: function timeout(time) {\n var controller = new AbortController();\n setTimeout(function () {\n return controller.abort(new DOMException(\"This signal is timeout in \".concat(time, \"ms\"), 'TimeoutError'));\n }, time);\n return controller.signal;\n }\n\n /**\n * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static}\n * @param {Iterable} iterable An {@link Iterable} (such as an {@link Array}) of abort signals.\n * @returns {AbortSignal} - **Already aborted**, if any of the abort signals given is already aborted.\n * The returned {@link AbortSignal}'s reason will be already set to the `reason` of the first abort signal that was already aborted.\n * - **Asynchronously aborted**, when any abort signal in `iterable` aborts.\n * The `reason` will be set to the reason of the first abort signal that is aborted.\n */\n }, {\n key: \"any\",\n value: function any(iterable) {\n var controller = new AbortController();\n /**\n * @this AbortSignal\n */\n function abort() {\n controller.abort(this.reason);\n clean();\n }\n function clean() {\n var _iterator = _createForOfIteratorHelper(iterable),\n _step;\n try {\n for (_iterator.s(); !(_step = _iterator.n()).done;) {\n var signal = _step.value;\n signal.removeEventListener('abort', abort);\n }\n } catch (err) {\n _iterator.e(err);\n } finally {\n _iterator.f();\n }\n }\n var _iterator2 = _createForOfIteratorHelper(iterable),\n _step2;\n try {\n for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {\n var signal = _step2.value;\n if (signal.aborted) {\n controller.abort(signal.reason);\n break;\n } else signal.addEventListener('abort', abort);\n }\n } catch (err) {\n _iterator2.e(err);\n } finally {\n _iterator2.f();\n }\n return controller.signal;\n }\n }]);\n }(Emitter);\n var AbortController = /*#__PURE__*/function () {\n function AbortController() {\n _classCallCheck(this, AbortController);\n // Compared to assignment, Object.defineProperty makes properties non-enumerable by default and\n // we want Object.keys(new AbortController()) to be [] for compat with the native impl\n Object.defineProperty(this, 'signal', {\n value: new AbortSignal(),\n writable: true,\n configurable: true\n });\n }\n return _createClass(AbortController, [{\n key: \"abort\",\n value: function abort(reason) {\n var signalReason = normalizeAbortReason(reason);\n var event = createAbortEvent(signalReason);\n this.signal.reason = signalReason;\n this.signal.dispatchEvent(event);\n }\n }, {\n key: \"toString\",\n value: function toString() {\n return '[object AbortController]';\n }\n }]);\n }();\n if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n // These are necessary to make sure that we get correct output for:\n // Object.prototype.toString.call(new AbortController())\n AbortController.prototype[Symbol.toStringTag] = 'AbortController';\n AbortSignal.prototype[Symbol.toStringTag] = 'AbortSignal';\n }\n\n function polyfillNeeded(self) {\n if (self.__FORCE_INSTALL_ABORTCONTROLLER_POLYFILL) {\n console.log('__FORCE_INSTALL_ABORTCONTROLLER_POLYFILL=true is set, will force install polyfill');\n return true;\n }\n\n // Note that the \"unfetch\" minimal fetch polyfill defines fetch() without\n // defining window.Request, and this polyfill need to work on top of unfetch\n // so the below feature detection needs the !self.AbortController part.\n // The Request.prototype check is also needed because Safari versions 11.1.2\n // up to and including 12.1.x has a window.AbortController present but still\n // does NOT correctly implement abortable fetch:\n // https://bugs.webkit.org/show_bug.cgi?id=174980#c2\n return typeof self.Request === 'function' && !self.Request.prototype.hasOwnProperty('signal') || !self.AbortController;\n }\n\n /**\n * Note: the \"fetch.Request\" default value is available for fetch imported from\n * the \"node-fetch\" package and not in browsers. This is OK since browsers\n * will be importing umd-polyfill.js from that path \"self\" is passed the\n * decorator so the default value will not be used (because browsers that define\n * fetch also has Request). One quirky setup where self.fetch exists but\n * self.Request does not is when the \"unfetch\" minimal fetch polyfill is used\n * on top of IE11; for this case the browser will try to use the fetch.Request\n * default value which in turn will be undefined but then then \"if (Request)\"\n * will ensure that you get a patched fetch but still no Request (as expected).\n * @param {fetch, Request = fetch.Request}\n * @returns {fetch: abortableFetch, Request: AbortableRequest}\n */\n function abortableFetchDecorator(patchTargets) {\n if ('function' === typeof patchTargets) {\n patchTargets = {\n fetch: patchTargets\n };\n }\n var _patchTargets = patchTargets,\n fetch = _patchTargets.fetch,\n _patchTargets$Request = _patchTargets.Request,\n NativeRequest = _patchTargets$Request === void 0 ? fetch.Request : _patchTargets$Request,\n NativeAbortController = _patchTargets.AbortController,\n _patchTargets$__FORCE = _patchTargets.__FORCE_INSTALL_ABORTCONTROLLER_POLYFILL,\n __FORCE_INSTALL_ABORTCONTROLLER_POLYFILL = _patchTargets$__FORCE === void 0 ? false : _patchTargets$__FORCE;\n if (!polyfillNeeded({\n fetch: fetch,\n Request: NativeRequest,\n AbortController: NativeAbortController,\n __FORCE_INSTALL_ABORTCONTROLLER_POLYFILL: __FORCE_INSTALL_ABORTCONTROLLER_POLYFILL\n })) {\n return {\n fetch: fetch,\n Request: Request\n };\n }\n var Request = NativeRequest;\n // Note that the \"unfetch\" minimal fetch polyfill defines fetch() without\n // defining window.Request, and this polyfill need to work on top of unfetch\n // hence we only patch it if it's available. Also we don't patch it if signal\n // is already available on the Request prototype because in this case support\n // is present and the patching below can cause a crash since it assigns to\n // request.signal which is technically a read-only property. This latter error\n // happens when you run the main5.js node-fetch example in the repo\n // \"abortcontroller-polyfill-examples\". The exact error is:\n // request.signal = init.signal;\n // ^\n // TypeError: Cannot set property signal of # which has only a getter\n if (Request && !Request.prototype.hasOwnProperty('signal') || __FORCE_INSTALL_ABORTCONTROLLER_POLYFILL) {\n Request = function Request(input, init) {\n var signal;\n if (init && init.signal) {\n signal = init.signal;\n // Never pass init.signal to the native Request implementation when the polyfill has\n // been installed because if we're running on top of a browser with a\n // working native AbortController (i.e. the polyfill was installed due to\n // __FORCE_INSTALL_ABORTCONTROLLER_POLYFILL being set), then passing our\n // fake AbortSignal to the native fetch will trigger:\n // TypeError: Failed to construct 'Request': member signal is not of type AbortSignal.\n delete init.signal;\n }\n var request = new NativeRequest(input, init);\n if (signal) {\n Object.defineProperty(request, 'signal', {\n writable: false,\n enumerable: false,\n configurable: true,\n value: signal\n });\n }\n return request;\n };\n Request.prototype = NativeRequest.prototype;\n }\n var realFetch = fetch;\n var abortableFetch = function abortableFetch(input, init) {\n var signal = Request && Request.prototype.isPrototypeOf(input) ? input.signal : init ? init.signal : undefined;\n if (signal) {\n var abortError;\n try {\n abortError = new DOMException('Aborted', 'AbortError');\n } catch (err) {\n // IE 11 does not support calling the DOMException constructor, use a\n // regular error object on it instead.\n abortError = new Error('Aborted');\n abortError.name = 'AbortError';\n }\n\n // Return early if already aborted, thus avoiding making an HTTP request\n if (signal.aborted) {\n return Promise.reject(abortError);\n }\n\n // Turn an event into a promise, reject it once `abort` is dispatched\n var cancellation = new Promise(function (_, reject) {\n signal.addEventListener('abort', function () {\n return reject(abortError);\n }, {\n once: true\n });\n });\n if (init && init.signal) {\n // Never pass .signal to the native implementation when the polyfill has\n // been installed because if we're running on top of a browser with a\n // working native AbortController (i.e. the polyfill was installed due to\n // __FORCE_INSTALL_ABORTCONTROLLER_POLYFILL being set), then passing our\n // fake AbortSignal to the native fetch will trigger:\n // TypeError: Failed to execute 'fetch' on 'Window': member signal is not of type AbortSignal.\n delete init.signal;\n }\n // Return the fastest promise (don't need to wait for request to finish)\n return Promise.race([cancellation, realFetch(input, init)]);\n }\n return realFetch(input, init);\n };\n return {\n fetch: abortableFetch,\n Request: Request\n };\n }\n\n (function (self) {\n\n if (!polyfillNeeded(self)) {\n return;\n }\n if (!self.fetch) {\n console.warn('fetch() is not available, cannot install abortcontroller-polyfill');\n return;\n }\n var _abortableFetch = abortableFetchDecorator(self),\n fetch = _abortableFetch.fetch,\n Request = _abortableFetch.Request;\n self.fetch = fetch;\n self.Request = Request;\n Object.defineProperty(self, 'AbortController', {\n writable: true,\n enumerable: false,\n configurable: true,\n value: AbortController\n });\n Object.defineProperty(self, 'AbortSignal', {\n writable: true,\n enumerable: false,\n configurable: true,\n value: AbortSignal\n });\n })(typeof self !== 'undefined' ? self : global);\n\n}));\n", "export function actionAddEntity(way) {\n return function(graph) {\n return graph.replace(way);\n };\n}\n", "var noop = {value: () => {}};\n\nfunction dispatch() {\n for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) {\n if (!(t = arguments[i] + \"\") || (t in _) || /[\\s.]/.test(t)) throw new Error(\"illegal type: \" + t);\n _[t] = [];\n }\n return new Dispatch(_);\n}\n\nfunction Dispatch(_) {\n this._ = _;\n}\n\nfunction parseTypenames(typenames, types) {\n return typenames.trim().split(/^|\\s+/).map(function(t) {\n var name = \"\", i = t.indexOf(\".\");\n if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);\n if (t && !types.hasOwnProperty(t)) throw new Error(\"unknown type: \" + t);\n return {type: t, name: name};\n });\n}\n\nDispatch.prototype = dispatch.prototype = {\n constructor: Dispatch,\n on: function(typename, callback) {\n var _ = this._,\n T = parseTypenames(typename + \"\", _),\n t,\n i = -1,\n n = T.length;\n\n // If no callback was specified, return the callback of the given type and name.\n if (arguments.length < 2) {\n while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t;\n return;\n }\n\n // If a type was specified, set the callback for the given type and name.\n // Otherwise, if a null callback was specified, remove callbacks of the given name.\n if (callback != null && typeof callback !== \"function\") throw new Error(\"invalid callback: \" + callback);\n while (++i < n) {\n if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback);\n else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null);\n }\n\n return this;\n },\n copy: function() {\n var copy = {}, _ = this._;\n for (var t in _) copy[t] = _[t].slice();\n return new Dispatch(copy);\n },\n call: function(type, that) {\n if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2];\n if (!this._.hasOwnProperty(type)) throw new Error(\"unknown type: \" + type);\n for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);\n },\n apply: function(type, that, args) {\n if (!this._.hasOwnProperty(type)) throw new Error(\"unknown type: \" + type);\n for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);\n }\n};\n\nfunction get(type, name) {\n for (var i = 0, n = type.length, c; i < n; ++i) {\n if ((c = type[i]).name === name) {\n return c.value;\n }\n }\n}\n\nfunction set(type, name, callback) {\n for (var i = 0, n = type.length; i < n; ++i) {\n if (type[i].name === name) {\n type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1));\n break;\n }\n }\n if (callback != null) type.push({name: name, value: callback});\n return type;\n}\n\nexport default dispatch;\n", "export {default as dispatch} from \"./dispatch.js\";\n", "function promisifyRequest(request) {\n return new Promise((resolve, reject) => {\n // @ts-ignore - file size hacks\n request.oncomplete = request.onsuccess = () => resolve(request.result);\n // @ts-ignore - file size hacks\n request.onabort = request.onerror = () => reject(request.error);\n });\n}\nfunction createStore(dbName, storeName) {\n let dbp;\n const getDB = () => {\n if (dbp)\n return dbp;\n const request = indexedDB.open(dbName);\n request.onupgradeneeded = () => request.result.createObjectStore(storeName);\n dbp = promisifyRequest(request);\n dbp.then((db) => {\n // It seems like Safari sometimes likes to just close the connection.\n // It's supposed to fire this event when that happens. Let's hope it does!\n db.onclose = () => (dbp = undefined);\n }, () => { });\n return dbp;\n };\n return (txMode, callback) => getDB().then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName)));\n}\nlet defaultGetStoreFunc;\nfunction defaultGetStore() {\n if (!defaultGetStoreFunc) {\n defaultGetStoreFunc = createStore('keyval-store', 'keyval');\n }\n return defaultGetStoreFunc;\n}\n/**\n * Get a value by its key.\n *\n * @param key\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nfunction get(key, customStore = defaultGetStore()) {\n return customStore('readonly', (store) => promisifyRequest(store.get(key)));\n}\n/**\n * Set a value with a key.\n *\n * @param key\n * @param value\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nfunction set(key, value, customStore = defaultGetStore()) {\n return customStore('readwrite', (store) => {\n store.put(value, key);\n return promisifyRequest(store.transaction);\n });\n}\n/**\n * Set multiple values at once. This is faster than calling set() multiple times.\n * It's also atomic \u2013 if one of the pairs can't be added, none will be added.\n *\n * @param entries Array of entries, where each entry is an array of `[key, value]`.\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nfunction setMany(entries, customStore = defaultGetStore()) {\n return customStore('readwrite', (store) => {\n entries.forEach((entry) => store.put(entry[1], entry[0]));\n return promisifyRequest(store.transaction);\n });\n}\n/**\n * Get multiple values by their keys\n *\n * @param keys\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nfunction getMany(keys, customStore = defaultGetStore()) {\n return customStore('readonly', (store) => Promise.all(keys.map((key) => promisifyRequest(store.get(key)))));\n}\n/**\n * Update a value. This lets you see the old value and update it as an atomic operation.\n *\n * @param key\n * @param updater A callback that takes the old value and returns a new value.\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nfunction update(key, updater, customStore = defaultGetStore()) {\n return customStore('readwrite', (store) => \n // Need to create the promise manually.\n // If I try to chain promises, the transaction closes in browsers\n // that use a promise polyfill (IE10/11).\n new Promise((resolve, reject) => {\n store.get(key).onsuccess = function () {\n try {\n store.put(updater(this.result), key);\n resolve(promisifyRequest(store.transaction));\n }\n catch (err) {\n reject(err);\n }\n };\n }));\n}\n/**\n * Delete a particular key from the store.\n *\n * @param key\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nfunction del(key, customStore = defaultGetStore()) {\n return customStore('readwrite', (store) => {\n store.delete(key);\n return promisifyRequest(store.transaction);\n });\n}\n/**\n * Delete multiple keys at once.\n *\n * @param keys List of keys to delete.\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nfunction delMany(keys, customStore = defaultGetStore()) {\n return customStore('readwrite', (store) => {\n keys.forEach((key) => store.delete(key));\n return promisifyRequest(store.transaction);\n });\n}\n/**\n * Clear all values in the store.\n *\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nfunction clear(customStore = defaultGetStore()) {\n return customStore('readwrite', (store) => {\n store.clear();\n return promisifyRequest(store.transaction);\n });\n}\nfunction eachCursor(store, callback) {\n store.openCursor().onsuccess = function () {\n if (!this.result)\n return;\n callback(this.result);\n this.result.continue();\n };\n return promisifyRequest(store.transaction);\n}\n/**\n * Get all keys in the store.\n *\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nfunction keys(customStore = defaultGetStore()) {\n return customStore('readonly', (store) => {\n // Fast path for modern browsers\n if (store.getAllKeys) {\n return promisifyRequest(store.getAllKeys());\n }\n const items = [];\n return eachCursor(store, (cursor) => items.push(cursor.key)).then(() => items);\n });\n}\n/**\n * Get all values in the store.\n *\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nfunction values(customStore = defaultGetStore()) {\n return customStore('readonly', (store) => {\n // Fast path for modern browsers\n if (store.getAll) {\n return promisifyRequest(store.getAll());\n }\n const items = [];\n return eachCursor(store, (cursor) => items.push(cursor.value)).then(() => items);\n });\n}\n/**\n * Get all entries in the store. Each entry is an array of `[key, value]`.\n *\n * @param customStore Method to get a custom store. Use with caution (see the docs).\n */\nfunction entries(customStore = defaultGetStore()) {\n return customStore('readonly', (store) => {\n // Fast path for modern browsers\n // (although, hopefully we'll get a simpler path some day)\n if (store.getAll && store.getAllKeys) {\n return Promise.all([\n promisifyRequest(store.getAllKeys()),\n promisifyRequest(store.getAll()),\n ]).then(([keys, values]) => keys.map((key, i) => [key, values[i]]));\n }\n const items = [];\n return customStore('readonly', (store) => eachCursor(store, (cursor) => items.push([cursor.key, cursor.value])).then(() => items));\n });\n}\n\nexport { clear, createStore, del, delMany, entries, get, getMany, keys, promisifyRequest, set, setMany, update, values };\n", "// https://github.com/openstreetmap/iD/issues/772\nimport { get, set, del } from 'idb-keyval';\n\n/** @type {Storage} */\nlet _storage;\ntry { _storage = localStorage; } catch {} // eslint-disable-line no-empty\n_storage = _storage || (() => {\n let s = {};\n return {\n getItem: (k) => s[k],\n setItem: (k, v) => s[k] = v,\n removeItem: (k) => delete s[k]\n };\n})();\n\nconst _listeners = {};\n\n//\n// corePreferences is an interface for persisting basic key-value strings\n// within and between iD sessions on the same site.\n//\n/**\n * @param {string} k\n * @param {string?} [v]\n * @returns {boolean} true if the action succeeded\n */\nfunction corePreferences(k, v) {\n try {\n if (v === undefined) return _storage.getItem(k);\n else if (v === null) _storage.removeItem(k);\n else _storage.setItem(k, v);\n\n if (_listeners[k]) {\n _listeners[k].forEach(handler => handler(v));\n }\n\n return true;\n } catch {\n /* eslint-disable no-console */\n if (typeof console !== 'undefined') {\n console.error('localStorage quota exceeded');\n }\n /* eslint-enable no-console */\n return false;\n }\n}\n\n// adds an event listener which is triggered whenever a preference changes\ncorePreferences.onChange = function(k, handler) {\n _listeners[k] = _listeners[k] || [];\n _listeners[k].push(handler);\n};\n\nexport { corePreferences as prefs };\n\nexport const asyncPrefs = {\n get,\n set,\n del\n};\n", "import type { Vec2 } from './vector';\n\n// constants\nvar TAU = 2 * Math.PI;\nconst EQUATORIAL_RADIUS = 6.3781370E6;\nconst POLAR_RADIUS = 6.3567523E6;\n\n\nexport function geoLatToMeters(dLat: number) {\n return dLat * (TAU * POLAR_RADIUS / 360);\n}\n\n\nexport function geoLonToMeters(dLon: number, atLat: number) {\n return Math.abs(atLat) >= 90 ? 0 :\n dLon * (TAU * EQUATORIAL_RADIUS / 360) * Math.abs(Math.cos(atLat * (Math.PI / 180)));\n}\n\n\nexport function geoMetersToLat(m: number) {\n return m / (TAU * POLAR_RADIUS / 360);\n}\n\n\nexport function geoMetersToLon(m: number, atLat: number) {\n return Math.abs(atLat) >= 90 ? 0 :\n m / (TAU * EQUATORIAL_RADIUS / 360) / Math.abs(Math.cos(atLat * (Math.PI / 180)));\n}\n\n\nexport function geoMetersToOffset(meters: Vec2, tileSize?: number): Vec2 {\n tileSize = tileSize || 256;\n return [\n meters[0] * tileSize / (TAU * EQUATORIAL_RADIUS),\n -meters[1] * tileSize / (TAU * POLAR_RADIUS)\n ];\n}\n\n\nexport function geoOffsetToMeters(offset: Vec2, tileSize?: number): Vec2 {\n tileSize = tileSize || 256;\n return [\n offset[0] * TAU * EQUATORIAL_RADIUS / tileSize,\n -offset[1] * TAU * POLAR_RADIUS / tileSize\n ];\n}\n\n\n// Equirectangular approximation of spherical distances on Earth\nexport function geoSphericalDistance(a: Vec2, b: Vec2) {\n var x = geoLonToMeters(a[0] - b[0], (a[1] + b[1]) / 2);\n var y = geoLatToMeters(a[1] - b[1]);\n return Math.sqrt((x * x) + (y * y));\n}\n\n\n// scale to zoom\nexport function geoScaleToZoom(k: number, tileSize?: number) {\n tileSize = tileSize || 256;\n var log2ts = Math.log(tileSize) * Math.LOG2E;\n return Math.log(k * TAU) / Math.LN2 - log2ts;\n}\n\n\n// zoom to scale\nexport function geoZoomToScale(z: number, tileSize?: number) {\n tileSize = tileSize || 256;\n return tileSize * Math.pow(2, z) / TAU;\n}\n\n\n// returns info about the node from `nodes` closest to the given `point`\nexport function geoSphericalClosestNode(nodes: iD.OsmNode[], point: Vec2) {\n var minDistance = Infinity, distance;\n var indexOfMin;\n\n for (var i in nodes) {\n distance = geoSphericalDistance(nodes[i].loc, point);\n if (distance < minDistance) {\n minDistance = distance;\n indexOfMin = +i;\n }\n }\n\n if (indexOfMin !== undefined) {\n return { index: indexOfMin, distance: minDistance, node: nodes[indexOfMin] };\n } else {\n return null;\n }\n}\n", "import { geoMetersToLat, geoMetersToLon } from './geo';\n\n/** @import { Vec2 } from './vector' */\n\n/**\n * @param {geoExtent | Vec2 | [Vec2, Vec2]} [min]\n * @param {Vec2} [max]\n */\nexport function geoExtent(min, max) {\n if (!(this instanceof geoExtent)) {\n return new geoExtent(min, max);\n } else if (min instanceof geoExtent) {\n return /** @type {geoExtent} */ (min);\n } else if (min && min.length === 2 && min[0].length === 2 && min[1].length === 2) {\n this[0] = /** @type {Vec2} */ (min[0]);\n this[1] = /** @type {Vec2} */ (min[1]);\n } else {\n this[0] = /** @type {Vec2} */ (min || [ Infinity, Infinity]);\n this[1] = /** @type {Vec2} */ (max || min || [-Infinity, -Infinity]);\n }\n}\n\ngeoExtent.prototype = new Array(2);\n\n /** @param {geoExtent} obj */\n geoExtent.prototype.equals = function (obj) {\n return this[0][0] === obj[0][0] &&\n this[0][1] === obj[0][1] &&\n this[1][0] === obj[1][0] &&\n this[1][1] === obj[1][1];\n };\n\n\n /** @param {geoExtent | Vec2 | [Vec2, Vec2]} obj */\n geoExtent.prototype.extend = function(obj) {\n if (!(obj instanceof geoExtent)) obj = new geoExtent(obj);\n return geoExtent(\n [Math.min(obj[0][0], this[0][0]), Math.min(obj[0][1], this[0][1])],\n [Math.max(obj[1][0], this[1][0]), Math.max(obj[1][1], this[1][1])]\n );\n };\n\n\n /** @param {geoExtent} extent */\n geoExtent.prototype._extend = function(extent) {\n this[0][0] = Math.min(extent[0][0], this[0][0]);\n this[0][1] = Math.min(extent[0][1], this[0][1]);\n this[1][0] = Math.max(extent[1][0], this[1][0]);\n this[1][1] = Math.max(extent[1][1], this[1][1]);\n };\n\n\n geoExtent.prototype.area = function() {\n return Math.abs((this[1][0] - this[0][0]) * (this[1][1] - this[0][1]));\n };\n\n\n /** @returns {Vec2} */\n geoExtent.prototype.center = function() {\n return [(this[0][0] + this[1][0]) / 2, (this[0][1] + this[1][1]) / 2];\n };\n\n\n geoExtent.prototype.rectangle = function() {\n return [this[0][0], this[0][1], this[1][0], this[1][1]];\n };\n\n\n geoExtent.prototype.bbox = function() {\n return { minX: this[0][0], minY: this[0][1], maxX: this[1][0], maxY: this[1][1] };\n };\n\n\n /** @returns {Vec2[]} */\n geoExtent.prototype.polygon = function() {\n return [\n [this[0][0], this[0][1]],\n [this[0][0], this[1][1]],\n [this[1][0], this[1][1]],\n [this[1][0], this[0][1]],\n [this[0][0], this[0][1]]\n ];\n };\n\n\n /** @param {geoExtent | Vec2 | [Vec2, Vec2]} obj */\n geoExtent.prototype.contains = function(obj) {\n if (!(obj instanceof geoExtent)) obj = new geoExtent(obj);\n return obj[0][0] >= this[0][0] &&\n obj[0][1] >= this[0][1] &&\n obj[1][0] <= this[1][0] &&\n obj[1][1] <= this[1][1];\n };\n\n\n /** @param {geoExtent | Vec2 | [Vec2, Vec2]} obj */\n geoExtent.prototype.intersects = function(obj) {\n if (!(obj instanceof geoExtent)) obj = new geoExtent(obj);\n return obj[0][0] <= this[1][0] &&\n obj[0][1] <= this[1][1] &&\n obj[1][0] >= this[0][0] &&\n obj[1][1] >= this[0][1];\n };\n\n\n /** @param {geoExtent | [Vec2, Vec2]} obj */\n geoExtent.prototype.intersection = function(obj) {\n if (!this.intersects(obj)) return new geoExtent();\n return new geoExtent(\n [Math.max(obj[0][0], this[0][0]), Math.max(obj[0][1], this[0][1])],\n [Math.min(obj[1][0], this[1][0]), Math.min(obj[1][1], this[1][1])]\n );\n };\n\n\n /** @param {geoExtent | Vec2 | [Vec2, Vec2]} obj */\n geoExtent.prototype.percentContainedIn = function(obj) {\n if (!(obj instanceof geoExtent)) obj = new geoExtent(obj);\n var a1 = this.intersection(obj).area();\n var a2 = this.area();\n\n if (a1 === Infinity || a2 === Infinity) {\n return 0;\n } else if (a1 === 0 || a2 === 0) {\n if (obj.contains(this)) {\n return 1;\n }\n return 0;\n } else {\n return a1 / a2;\n }\n };\n\n\n /** @param {number} meters */\n geoExtent.prototype.padByMeters = function(meters) {\n var dLat = geoMetersToLat(meters);\n var dLon = geoMetersToLon(meters, this.center()[1]);\n return geoExtent(\n [this[0][0] - dLon, this[0][1] - dLat],\n [this[1][0] + dLon, this[1][1] + dLat]\n );\n };\n\n\n geoExtent.prototype.toParam = function() {\n return this.rectangle().join(',');\n };\n\n geoExtent.prototype.split = function() {\n const center = this.center();\n return [\n geoExtent(this[0], center),\n geoExtent([center[0], this[0][1]], [this[1][0], center[1]]),\n geoExtent(center, this[1]),\n geoExtent([this[0][0], center[1]], [center[0], this[1][1]])\n ];\n };\n", "export default function(polygon) {\n var i = -1,\n n = polygon.length,\n a,\n b = polygon[n - 1],\n area = 0;\n\n while (++i < n) {\n a = b;\n b = polygon[i];\n area += a[1] * b[0] - a[0] * b[1];\n }\n\n return area / 2;\n}\n", "export default function(polygon) {\n var i = -1,\n n = polygon.length,\n x = 0,\n y = 0,\n a,\n b = polygon[n - 1],\n c,\n k = 0;\n\n while (++i < n) {\n a = b;\n b = polygon[i];\n k += c = a[0] * b[1] - b[0] * a[1];\n x += (a[0] + b[0]) * c;\n y += (a[1] + b[1]) * c;\n }\n\n return k *= 3, [x / k, y / k];\n}\n", "// Returns the 2D cross product of AB and AC vectors, i.e., the z-component of\n// the 3D cross product in a quadrant I Cartesian coordinate system (+x is\n// right, +y is up). Returns a positive value if ABC is counter-clockwise,\n// negative if clockwise, and zero if the points are collinear.\nexport default function(a, b, c) {\n return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);\n}\n", "import cross from \"./cross.js\";\n\nfunction lexicographicOrder(a, b) {\n return a[0] - b[0] || a[1] - b[1];\n}\n\n// Computes the upper convex hull per the monotone chain algorithm.\n// Assumes points.length >= 3, is sorted by x, unique in y.\n// Returns an array of indices into points in left-to-right order.\nfunction computeUpperHullIndexes(points) {\n const n = points.length,\n indexes = [0, 1];\n let size = 2, i;\n\n for (i = 2; i < n; ++i) {\n while (size > 1 && cross(points[indexes[size - 2]], points[indexes[size - 1]], points[i]) <= 0) --size;\n indexes[size++] = i;\n }\n\n return indexes.slice(0, size); // remove popped points\n}\n\nexport default function(points) {\n if ((n = points.length) < 3) return null;\n\n var i,\n n,\n sortedPoints = new Array(n),\n flippedPoints = new Array(n);\n\n for (i = 0; i < n; ++i) sortedPoints[i] = [+points[i][0], +points[i][1], i];\n sortedPoints.sort(lexicographicOrder);\n for (i = 0; i < n; ++i) flippedPoints[i] = [sortedPoints[i][0], -sortedPoints[i][1]];\n\n var upperIndexes = computeUpperHullIndexes(sortedPoints),\n lowerIndexes = computeUpperHullIndexes(flippedPoints);\n\n // Construct the hull polygon, removing possible duplicate endpoints.\n var skipLeft = lowerIndexes[0] === upperIndexes[0],\n skipRight = lowerIndexes[lowerIndexes.length - 1] === upperIndexes[upperIndexes.length - 1],\n hull = [];\n\n // Add upper hull in right-to-l order.\n // Then add lower hull in left-to-right order.\n for (i = upperIndexes.length - 1; i >= 0; --i) hull.push(points[sortedPoints[upperIndexes[i]][2]]);\n for (i = +skipLeft; i < lowerIndexes.length - skipRight; ++i) hull.push(points[sortedPoints[lowerIndexes[i]][2]]);\n\n return hull;\n}\n", "export default function(polygon, point) {\n var n = polygon.length,\n p = polygon[n - 1],\n x = point[0], y = point[1],\n x0 = p[0], y0 = p[1],\n x1, y1,\n inside = false;\n\n for (var i = 0; i < n; ++i) {\n p = polygon[i], x1 = p[0], y1 = p[1];\n if (((y1 > y) !== (y0 > y)) && (x < (x0 - x1) * (y - y1) / (y0 - y1) + x1)) inside = !inside;\n x0 = x1, y0 = y1;\n }\n\n return inside;\n}\n", "export default function(polygon) {\n var i = -1,\n n = polygon.length,\n b = polygon[n - 1],\n xa,\n ya,\n xb = b[0],\n yb = b[1],\n perimeter = 0;\n\n while (++i < n) {\n xa = xb;\n ya = yb;\n b = polygon[i];\n xb = b[0];\n yb = b[1];\n xa -= xb;\n ya -= yb;\n perimeter += Math.hypot(xa, ya);\n }\n\n return perimeter;\n}\n", "export {default as polygonArea} from \"./area.js\";\nexport {default as polygonCentroid} from \"./centroid.js\";\nexport {default as polygonHull} from \"./hull.js\";\nexport {default as polygonContains} from \"./contains.js\";\nexport {default as polygonLength} from \"./length.js\";\n", "export type Vec2 = [x: number, y: number];\nexport type Vec3 = [x: number, y: number, z: number];\n\n// vector equals\nexport function geoVecEqual(a: Vec2, b: Vec2, epsilon?: number) {\n if (epsilon) {\n return (Math.abs(a[0] - b[0]) <= epsilon) && (Math.abs(a[1] - b[1]) <= epsilon);\n } else {\n return (a[0] === b[0]) && (a[1] === b[1]);\n }\n}\n\n// vector addition\nexport function geoVecAdd(a: Vec2, b: Vec2): Vec2 {\n return [ a[0] + b[0], a[1] + b[1] ];\n}\n\n// vector subtraction\nexport function geoVecSubtract(a: Vec2, b: Vec2): Vec2 {\n return [ a[0] - b[0], a[1] - b[1] ];\n}\n\n// vector scaling\nexport function geoVecScale(a: Vec2, mag: number): Vec2 {\n return [ a[0] * mag, a[1] * mag ];\n}\n\n// vector rounding (was: geoRoundCoordinates)\nexport function geoVecFloor(a: Vec2): Vec2 {\n return [ Math.floor(a[0]), Math.floor(a[1]) ];\n}\n\n// linear interpolation\nexport function geoVecInterp(a: Vec2, b: Vec2, t: number): Vec2 {\n return [\n a[0] + (b[0] - a[0]) * t,\n a[1] + (b[1] - a[1]) * t\n ];\n}\n\n// http://jsperf.com/id-dist-optimization\nexport function geoVecLength(a: Vec2, b: Vec2) {\n return Math.sqrt(geoVecLengthSquare(a,b));\n}\n\n// length of vector raised to the power two\nexport function geoVecLengthSquare(a: Vec2, b?: Vec2) {\n b = b || [0, 0];\n var x = a[0] - b[0];\n var y = a[1] - b[1];\n return (x * x) + (y * y);\n}\n\n// get a unit vector\nexport function geoVecNormalize(a: Vec2): Vec2 {\n var length = Math.sqrt((a[0] * a[0]) + (a[1] * a[1]));\n if (length !== 0) {\n return geoVecScale(a, 1 / length);\n }\n return [0, 0];\n}\n\n// Return the counterclockwise angle in the range (-pi, pi)\n// between the positive X axis and the line intersecting a and b.\nexport function geoVecAngle(a: Vec2, b: Vec2) {\n return Math.atan2(b[1] - a[1], b[0] - a[0]);\n}\n\n// dot product\nexport function geoVecDot(a: Vec2, b: Vec2, origin?: Vec2) {\n origin = origin || [0, 0];\n var p = geoVecSubtract(a, origin);\n var q = geoVecSubtract(b, origin);\n return (p[0]) * (q[0]) + (p[1]) * (q[1]);\n}\n\n// normalized dot product\nexport function geoVecNormalizedDot(a: Vec2, b: Vec2, origin?: Vec2) {\n origin = origin || [0, 0];\n var p = geoVecNormalize(geoVecSubtract(a, origin));\n var q = geoVecNormalize(geoVecSubtract(b, origin));\n return geoVecDot(p, q);\n}\n\n// 2D cross product of OA and OB vectors, returns magnitude of Z vector\n// Returns a positive value, if OAB makes a counter-clockwise turn,\n// negative for clockwise turn, and zero if the points are collinear.\nexport function geoVecCross(a: Vec2, b: Vec2, origin?: Vec2) {\n origin = origin || [0, 0];\n var p = geoVecSubtract(a, origin);\n var q = geoVecSubtract(b, origin);\n return (p[0]) * (q[1]) - (p[1]) * (q[0]);\n}\n\n\n// find closest orthogonal projection of point onto points array\nexport function geoVecProject(a: Vec2, points: Vec2[]) {\n var min = Infinity;\n var idx;\n var target;\n\n for (var i = 0; i < points.length - 1; i++) {\n var o = points[i];\n var s = geoVecSubtract(points[i + 1], o);\n var v = geoVecSubtract(a, o);\n var proj = geoVecDot(v, s) / geoVecDot(s, s);\n var p: Vec2;\n\n if (proj < 0) {\n p = o;\n } else if (proj > 1) {\n p = points[i + 1];\n } else {\n p = [o[0] + proj * s[0], o[1] + proj * s[1]];\n }\n\n var dist = geoVecLength(p, a);\n if (dist < min) {\n min = dist;\n idx = i + 1;\n target = p;\n }\n }\n\n if (idx !== undefined) {\n return { index: idx, distance: min, target: target };\n } else {\n return null;\n }\n}\n\n", "import {\n polygonHull as d3_polygonHull,\n polygonCentroid as d3_polygonCentroid\n} from 'd3-polygon';\n\nimport { geoExtent } from './extent.js';\n\nimport {\n geoVecAngle, geoVecCross, geoVecDot, geoVecEqual,\n geoVecInterp, geoVecLength, geoVecSubtract,\n type Vec2\n} from './vector.js';\nimport type { OsmNode } from '../osm/node.js';\nimport type { Projection } from './raw_mercator.js';\n\n\n// Return the counterclockwise angle in the range (-pi, pi)\n// between the positive X axis and the line intersecting a and b.\nexport function geoAngle(a: OsmNode, b: OsmNode, projection: Projection) {\n return geoVecAngle(projection(a.loc), projection(b.loc));\n}\n\n\nexport function geoEdgeEqual(a: [T, T], b: [T, T]) {\n return (a[0] === b[0] && a[1] === b[1]) ||\n (a[0] === b[1] && a[1] === b[0]);\n}\n\n\n// Rotate all points counterclockwise around a pivot point by given angle\nexport function geoRotate(points: Vec2[], angle: number, around: Vec2): Vec2[] {\n return points.map(function(point) {\n var radial = geoVecSubtract(point, around);\n return [\n radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + around[0],\n radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + around[1]\n ];\n });\n}\n\n\n// Choose the edge with the minimal distance from `point` to its orthogonal\n// projection onto that edge, if such a projection exists, or the distance to\n// the closest vertex on that edge. Returns an object with the `index` of the\n// chosen edge, the chosen `loc` on that edge, and the `distance` to to it.\nexport function geoChooseEdge(nodes: OsmNode[], point: Vec2, projection: Projection, activeID: string) {\n var dist = geoVecLength;\n var points = nodes.map(function(n) { return projection(n.loc); });\n var ids = nodes.map(function(n) { return n.id; });\n var min = Infinity;\n var idx;\n var loc;\n\n for (var i = 0; i < points.length - 1; i++) {\n if (ids[i] === activeID || ids[i + 1] === activeID) continue;\n\n var o = points[i];\n var s = geoVecSubtract(points[i + 1], o);\n var v = geoVecSubtract(point, o);\n var proj = geoVecDot(v, s) / geoVecDot(s, s);\n var p: Vec2;\n\n if (proj < 0) {\n p = o;\n } else if (proj > 1) {\n p = points[i + 1];\n } else {\n p = [o[0] + proj * s[0], o[1] + proj * s[1]];\n }\n\n var d = dist(p, point);\n if (d < min) {\n min = d;\n idx = i + 1;\n loc = projection.invert(p);\n }\n }\n\n if (idx !== undefined) {\n return { index: idx, distance: min, loc: loc };\n } else {\n return null;\n }\n}\n\n\n// Test active (dragged or drawing) segments against inactive segments\n// This is used to test e.g. multipolygon rings that cross\n// `activeNodes` is the ring containing the activeID being dragged.\n// `inactiveNodes` is the other ring to test against\nexport function geoHasLineIntersections(activeNodes: OsmNode[], inactiveNodes: OsmNode[], activeID: string) {\n var actives = [];\n var inactives = [];\n var j, k, n1, n2, segment;\n\n // gather active segments (only segments in activeNodes that contain the activeID)\n for (j = 0; j < activeNodes.length - 1; j++) {\n n1 = activeNodes[j];\n n2 = activeNodes[j+1];\n segment = [n1.loc, n2.loc];\n if (n1.id === activeID || n2.id === activeID) {\n actives.push(segment);\n }\n }\n\n // gather inactive segments\n for (j = 0; j < inactiveNodes.length - 1; j++) {\n n1 = inactiveNodes[j];\n n2 = inactiveNodes[j+1];\n segment = [n1.loc, n2.loc];\n inactives.push(segment);\n }\n\n // test\n for (j = 0; j < actives.length; j++) {\n for (k = 0; k < inactives.length; k++) {\n var p = actives[j];\n var q = inactives[k];\n var hit = geoLineIntersection(p, q);\n if (hit) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n\n// Test active (dragged or drawing) segments against inactive segments\n// This is used to test whether a way intersects with itself.\nexport function geoHasSelfIntersections(nodes: OsmNode[], activeID: string) {\n var actives = [];\n var inactives = [];\n var j, k;\n\n // group active and passive segments along the nodes\n for (j = 0; j < nodes.length - 1; j++) {\n var n1 = nodes[j];\n var n2 = nodes[j+1];\n var segment = [n1.loc, n2.loc];\n if (n1.id === activeID || n2.id === activeID) {\n actives.push(segment);\n } else {\n inactives.push(segment);\n }\n }\n\n // test\n for (j = 0; j < actives.length; j++) {\n for (k = 0; k < inactives.length; k++) {\n var p = actives[j];\n var q = inactives[k];\n // skip if segments share an endpoint\n if (geoVecEqual(p[1], q[0]) || geoVecEqual(p[0], q[1]) ||\n geoVecEqual(p[0], q[0]) || geoVecEqual(p[1], q[1]) ) {\n continue;\n }\n\n var hit = geoLineIntersection(p, q);\n if (hit) {\n var epsilon = 1e-8;\n // skip if the hit is at the segment's endpoint\n if (geoVecEqual(p[1], hit, epsilon) || geoVecEqual(p[0], hit, epsilon) ||\n geoVecEqual(q[1], hit, epsilon) || geoVecEqual(q[0], hit, epsilon) ) {\n continue;\n } else {\n return true;\n }\n }\n }\n }\n\n return false;\n}\n\n\n// Return the intersection point of 2 line segments.\n// From https://github.com/pgkelley4/line-segments-intersect\n// This uses the vector cross product approach described below:\n// http://stackoverflow.com/a/565282/786339\nexport function geoLineIntersection(a: Vec2[], b: Vec2[]) {\n var p: Vec2 = [a[0][0], a[0][1]];\n var p2: Vec2 = [a[1][0], a[1][1]];\n var q: Vec2 = [b[0][0], b[0][1]];\n var q2: Vec2 = [b[1][0], b[1][1]];\n var r = geoVecSubtract(p2, p);\n var s = geoVecSubtract(q2, q);\n var uNumerator = geoVecCross(geoVecSubtract(q, p), r);\n var denominator = geoVecCross(r, s);\n\n if (uNumerator && denominator) {\n var u = uNumerator / denominator;\n var t = geoVecCross(geoVecSubtract(q, p), s) / denominator;\n\n if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) {\n return geoVecInterp(p, p2, t);\n }\n }\n\n return null;\n}\n\n\nexport function geoPathIntersections(path1: Vec2[], path2: Vec2[]) {\n var intersections = [];\n for (var i = 0; i < path1.length - 1; i++) {\n for (var j = 0; j < path2.length - 1; j++) {\n var a = [ path1[i], path1[i+1] ];\n var b = [ path2[j], path2[j+1] ];\n var hit = geoLineIntersection(a, b);\n if (hit) {\n intersections.push(hit);\n }\n }\n }\n return intersections;\n}\n\nexport function geoPathHasIntersections(path1: Vec2[], path2: Vec2[]) {\n for (var i = 0; i < path1.length - 1; i++) {\n for (var j = 0; j < path2.length - 1; j++) {\n var a = [ path1[i], path1[i+1] ];\n var b = [ path2[j], path2[j+1] ];\n var hit = geoLineIntersection(a, b);\n if (hit) {\n return true;\n }\n }\n }\n return false;\n}\n\n\n// Return whether point is contained in polygon.\n//\n// `point` should be a 2-item array of coordinates.\n// `polygon` should be an array of 2-item arrays of coordinates.\n//\n// From https://github.com/substack/point-in-polygon.\n// ray-casting algorithm based on\n// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html\n//\nexport function geoPointInPolygon(point: Vec2, polygon: Vec2[]) {\n var x = point[0];\n var y = point[1];\n var inside = false;\n\n for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n var xi = polygon[i][0];\n var yi = polygon[i][1];\n var xj = polygon[j][0];\n var yj = polygon[j][1];\n\n var intersect = ((yi > y) !== (yj > y)) &&\n (x < (xj - xi) * (y - yi) / (yj - yi) + xi);\n if (intersect) inside = !inside;\n }\n\n return inside;\n}\n\n\nexport function geoPolygonContainsPolygon(outer: Vec2[], inner: Vec2[]) {\n return inner.every(function(point) {\n return geoPointInPolygon(point, outer);\n });\n}\n\n\nexport function geoPolygonIntersectsPolygon(outer: Vec2[], inner: Vec2[], checkSegments?: boolean) {\n function testPoints(outer: Vec2[], inner: Vec2[]) {\n return inner.some(function(point) {\n return geoPointInPolygon(point, outer);\n });\n }\n\n return testPoints(outer, inner) || (!!checkSegments && geoPathHasIntersections(outer, inner));\n}\n\n\n// http://gis.stackexchange.com/questions/22895/finding-minimum-area-rectangle-for-given-points\n// http://gis.stackexchange.com/questions/3739/generalisation-strategies-for-building-outlines/3756#3756\nexport function geoGetSmallestSurroundingRectangle(points: Vec2[]) {\n var hull = d3_polygonHull(points)!;\n var centroid = d3_polygonCentroid(hull);\n var minArea = Infinity;\n var ssrExtent!: geoExtent;\n var ssrAngle = 0;\n var c1 = hull[0];\n\n for (var i = 0; i <= hull.length - 1; i++) {\n var c2 = (i === hull.length - 1) ? hull[0] : hull[i + 1];\n var angle = Math.atan2(c2[1] - c1[1], c2[0] - c1[0]);\n var poly = geoRotate(hull, -angle, centroid);\n var extent = poly.reduce(function(extent, point) {\n return extent.extend(geoExtent(point));\n }, geoExtent());\n\n var area = extent.area();\n if (area < minArea) {\n minArea = area;\n ssrExtent = extent;\n ssrAngle = angle;\n }\n c1 = c2;\n }\n\n return {\n poly: geoRotate(ssrExtent.polygon(), ssrAngle, centroid),\n angle: ssrAngle\n };\n}\n\n\nexport function geoPathLength(path: Vec2[]) {\n var length = 0;\n for (var i = 0; i < path.length - 1; i++) {\n length += geoVecLength(path[i], path[i + 1]);\n }\n return length;\n}\n\n\n// If the given point is at the edge of the padded viewport,\n// return a vector that will nudge the viewport in that direction\nexport function geoViewportEdge(point: Vec2, dimensions: Vec2): Vec2 | null {\n var pad = [80, 20, 50, 20]; // top, right, bottom, left\n var x = 0;\n var y = 0;\n\n if (point[0] > dimensions[0] - pad[1]) {\n x = -10;\n }\n if (point[0] < pad[3]) {\n x = 10;\n }\n if (point[1] > dimensions[1] - pad[2]) {\n y = -10;\n }\n if (point[1] < pad[0]) {\n y = 10;\n }\n\n if (x || y) {\n return [x, y];\n } else {\n return null;\n }\n}\n", "export default function ascending(a, b) {\n return a == null || b == null ? NaN : a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;\n}\n", "export default function descending(a, b) {\n return a == null || b == null ? NaN\n : b < a ? -1\n : b > a ? 1\n : b >= a ? 0\n : NaN;\n}\n", "import ascending from \"./ascending.js\";\nimport descending from \"./descending.js\";\n\nexport default function bisector(f) {\n let compare1, compare2, delta;\n\n // If an accessor is specified, promote it to a comparator. In this case we\n // can test whether the search value is (self-) comparable. We can\u2019t do this\n // for a comparator (except for specific, known comparators) because we can\u2019t\n // tell if the comparator is symmetric, and an asymmetric comparator can\u2019t be\n // used to test whether a single value is comparable.\n if (f.length !== 2) {\n compare1 = ascending;\n compare2 = (d, x) => ascending(f(d), x);\n delta = (d, x) => f(d) - x;\n } else {\n compare1 = f === ascending || f === descending ? f : zero;\n compare2 = f;\n delta = f;\n }\n\n function left(a, x, lo = 0, hi = a.length) {\n if (lo < hi) {\n if (compare1(x, x) !== 0) return hi;\n do {\n const mid = (lo + hi) >>> 1;\n if (compare2(a[mid], x) < 0) lo = mid + 1;\n else hi = mid;\n } while (lo < hi);\n }\n return lo;\n }\n\n function right(a, x, lo = 0, hi = a.length) {\n if (lo < hi) {\n if (compare1(x, x) !== 0) return hi;\n do {\n const mid = (lo + hi) >>> 1;\n if (compare2(a[mid], x) <= 0) lo = mid + 1;\n else hi = mid;\n } while (lo < hi);\n }\n return lo;\n }\n\n function center(a, x, lo = 0, hi = a.length) {\n const i = left(a, x, lo, hi - 1);\n return i > lo && delta(a[i - 1], x) > -delta(a[i], x) ? i - 1 : i;\n }\n\n return {left, center, right};\n}\n\nfunction zero() {\n return 0;\n}\n", "export default function number(x) {\n return x === null ? NaN : +x;\n}\n\nexport function* numbers(values, valueof) {\n if (valueof === undefined) {\n for (let value of values) {\n if (value != null && (value = +value) >= value) {\n yield value;\n }\n }\n } else {\n let index = -1;\n for (let value of values) {\n if ((value = valueof(value, ++index, values)) != null && (value = +value) >= value) {\n yield value;\n }\n }\n }\n}\n", "import ascending from \"./ascending.js\";\nimport bisector from \"./bisector.js\";\nimport number from \"./number.js\";\n\nconst ascendingBisect = bisector(ascending);\nexport const bisectRight = ascendingBisect.right;\nexport const bisectLeft = ascendingBisect.left;\nexport const bisectCenter = bisector(number).center;\nexport default bisectRight;\n", "export function blur(values, r) {\n if (!((r = +r) >= 0)) throw new RangeError(\"invalid r\");\n let length = values.length;\n if (!((length = Math.floor(length)) >= 0)) throw new RangeError(\"invalid length\");\n if (!length || !r) return values;\n const blur = blurf(r);\n const temp = values.slice();\n blur(values, temp, 0, length, 1);\n blur(temp, values, 0, length, 1);\n blur(values, temp, 0, length, 1);\n return values;\n}\n\nexport const blur2 = Blur2(blurf);\n\nexport const blurImage = Blur2(blurfImage);\n\nfunction Blur2(blur) {\n return function(data, rx, ry = rx) {\n if (!((rx = +rx) >= 0)) throw new RangeError(\"invalid rx\");\n if (!((ry = +ry) >= 0)) throw new RangeError(\"invalid ry\");\n let {data: values, width, height} = data;\n if (!((width = Math.floor(width)) >= 0)) throw new RangeError(\"invalid width\");\n if (!((height = Math.floor(height !== undefined ? height : values.length / width)) >= 0)) throw new RangeError(\"invalid height\");\n if (!width || !height || (!rx && !ry)) return data;\n const blurx = rx && blur(rx);\n const blury = ry && blur(ry);\n const temp = values.slice();\n if (blurx && blury) {\n blurh(blurx, temp, values, width, height);\n blurh(blurx, values, temp, width, height);\n blurh(blurx, temp, values, width, height);\n blurv(blury, values, temp, width, height);\n blurv(blury, temp, values, width, height);\n blurv(blury, values, temp, width, height);\n } else if (blurx) {\n blurh(blurx, values, temp, width, height);\n blurh(blurx, temp, values, width, height);\n blurh(blurx, values, temp, width, height);\n } else if (blury) {\n blurv(blury, values, temp, width, height);\n blurv(blury, temp, values, width, height);\n blurv(blury, values, temp, width, height);\n }\n return data;\n };\n}\n\nfunction blurh(blur, T, S, w, h) {\n for (let y = 0, n = w * h; y < n;) {\n blur(T, S, y, y += w, 1);\n }\n}\n\nfunction blurv(blur, T, S, w, h) {\n for (let x = 0, n = w * h; x < w; ++x) {\n blur(T, S, x, x + n, w);\n }\n}\n\nfunction blurfImage(radius) {\n const blur = blurf(radius);\n return (T, S, start, stop, step) => {\n start <<= 2, stop <<= 2, step <<= 2;\n blur(T, S, start + 0, stop + 0, step);\n blur(T, S, start + 1, stop + 1, step);\n blur(T, S, start + 2, stop + 2, step);\n blur(T, S, start + 3, stop + 3, step);\n };\n}\n\n// Given a target array T, a source array S, sets each value T[i] to the average\n// of {S[i - r], \u2026, S[i], \u2026, S[i + r]}, where r = \u230Aradius\u230B, start <= i < stop,\n// for each i, i + step, i + 2 * step, etc., and where S[j] is clamped between\n// S[start] (inclusive) and S[stop] (exclusive). If the given radius is not an\n// integer, S[i - r - 1] and S[i + r + 1] are added to the sum, each weighted\n// according to r - \u230Aradius\u230B.\nfunction blurf(radius) {\n const radius0 = Math.floor(radius);\n if (radius0 === radius) return bluri(radius);\n const t = radius - radius0;\n const w = 2 * radius + 1;\n return (T, S, start, stop, step) => { // stop must be aligned!\n if (!((stop -= step) >= start)) return; // inclusive stop\n let sum = radius0 * S[start];\n const s0 = step * radius0;\n const s1 = s0 + step;\n for (let i = start, j = start + s0; i < j; i += step) {\n sum += S[Math.min(stop, i)];\n }\n for (let i = start, j = stop; i <= j; i += step) {\n sum += S[Math.min(stop, i + s0)];\n T[i] = (sum + t * (S[Math.max(start, i - s1)] + S[Math.min(stop, i + s1)])) / w;\n sum -= S[Math.max(start, i - s0)];\n }\n };\n}\n\n// Like blurf, but optimized for integer radius.\nfunction bluri(radius) {\n const w = 2 * radius + 1;\n return (T, S, start, stop, step) => { // stop must be aligned!\n if (!((stop -= step) >= start)) return; // inclusive stop\n let sum = radius * S[start];\n const s = step * radius;\n for (let i = start, j = start + s; i < j; i += step) {\n sum += S[Math.min(stop, i)];\n }\n for (let i = start, j = stop; i <= j; i += step) {\n sum += S[Math.min(stop, i + s)];\n T[i] = sum / w;\n sum -= S[Math.max(start, i - s)];\n }\n };\n}\n", "export default function count(values, valueof) {\n let count = 0;\n if (valueof === undefined) {\n for (let value of values) {\n if (value != null && (value = +value) >= value) {\n ++count;\n }\n }\n } else {\n let index = -1;\n for (let value of values) {\n if ((value = valueof(value, ++index, values)) != null && (value = +value) >= value) {\n ++count;\n }\n }\n }\n return count;\n}\n", "function length(array) {\n return array.length | 0;\n}\n\nfunction empty(length) {\n return !(length > 0);\n}\n\nfunction arrayify(values) {\n return typeof values !== \"object\" || \"length\" in values ? values : Array.from(values);\n}\n\nfunction reducer(reduce) {\n return values => reduce(...values);\n}\n\nexport default function cross(...values) {\n const reduce = typeof values[values.length - 1] === \"function\" && reducer(values.pop());\n values = values.map(arrayify);\n const lengths = values.map(length);\n const j = values.length - 1;\n const index = new Array(j + 1).fill(0);\n const product = [];\n if (j < 0 || lengths.some(empty)) return product;\n while (true) {\n product.push(index.map((j, i) => values[i][j]));\n let i = j;\n while (++index[i] === lengths[i]) {\n if (i === 0) return reduce ? product.map(reduce) : product;\n index[i--] = 0;\n }\n }\n}\n", "export default function cumsum(values, valueof) {\n var sum = 0, index = 0;\n return Float64Array.from(values, valueof === undefined\n ? v => (sum += +v || 0)\n : v => (sum += +valueof(v, index++, values) || 0));\n}", "export default function variance(values, valueof) {\n let count = 0;\n let delta;\n let mean = 0;\n let sum = 0;\n if (valueof === undefined) {\n for (let value of values) {\n if (value != null && (value = +value) >= value) {\n delta = value - mean;\n mean += delta / ++count;\n sum += delta * (value - mean);\n }\n }\n } else {\n let index = -1;\n for (let value of values) {\n if ((value = valueof(value, ++index, values)) != null && (value = +value) >= value) {\n delta = value - mean;\n mean += delta / ++count;\n sum += delta * (value - mean);\n }\n }\n }\n if (count > 1) return sum / (count - 1);\n}\n", "import variance from \"./variance.js\";\n\nexport default function deviation(values, valueof) {\n const v = variance(values, valueof);\n return v ? Math.sqrt(v) : v;\n}\n", "export default function extent(values, valueof) {\n let min;\n let max;\n if (valueof === undefined) {\n for (const value of values) {\n if (value != null) {\n if (min === undefined) {\n if (value >= value) min = max = value;\n } else {\n if (min > value) min = value;\n if (max < value) max = value;\n }\n }\n }\n } else {\n let index = -1;\n for (let value of values) {\n if ((value = valueof(value, ++index, values)) != null) {\n if (min === undefined) {\n if (value >= value) min = max = value;\n } else {\n if (min > value) min = value;\n if (max < value) max = value;\n }\n }\n }\n }\n return [min, max];\n}\n", "// https://github.com/python/cpython/blob/a74eea238f5baba15797e2e8b570d153bc8690a7/Modules/mathmodule.c#L1423\nexport class Adder {\n constructor() {\n this._partials = new Float64Array(32);\n this._n = 0;\n }\n add(x) {\n const p = this._partials;\n let i = 0;\n for (let j = 0; j < this._n && j < 32; j++) {\n const y = p[j],\n hi = x + y,\n lo = Math.abs(x) < Math.abs(y) ? x - (hi - y) : y - (hi - x);\n if (lo) p[i++] = lo;\n x = hi;\n }\n p[i] = x;\n this._n = i + 1;\n return this;\n }\n valueOf() {\n const p = this._partials;\n let n = this._n, x, y, lo, hi = 0;\n if (n > 0) {\n hi = p[--n];\n while (n > 0) {\n x = hi;\n y = p[--n];\n hi = x + y;\n lo = y - (hi - x);\n if (lo) break;\n }\n if (n > 0 && ((lo < 0 && p[n - 1] < 0) || (lo > 0 && p[n - 1] > 0))) {\n y = lo * 2;\n x = hi + y;\n if (y == x - hi) hi = x;\n }\n }\n return hi;\n }\n}\n\nexport function fsum(values, valueof) {\n const adder = new Adder();\n if (valueof === undefined) {\n for (let value of values) {\n if (value = +value) {\n adder.add(value);\n }\n }\n } else {\n let index = -1;\n for (let value of values) {\n if (value = +valueof(value, ++index, values)) {\n adder.add(value);\n }\n }\n }\n return +adder;\n}\n\nexport function fcumsum(values, valueof) {\n const adder = new Adder();\n let index = -1;\n return Float64Array.from(values, valueof === undefined\n ? v => adder.add(+v || 0)\n : v => adder.add(+valueof(v, ++index, values) || 0)\n );\n}\n", "export class InternMap extends Map {\n constructor(entries, key = keyof) {\n super();\n Object.defineProperties(this, {_intern: {value: new Map()}, _key: {value: key}});\n if (entries != null) for (const [key, value] of entries) this.set(key, value);\n }\n get(key) {\n return super.get(intern_get(this, key));\n }\n has(key) {\n return super.has(intern_get(this, key));\n }\n set(key, value) {\n return super.set(intern_set(this, key), value);\n }\n delete(key) {\n return super.delete(intern_delete(this, key));\n }\n}\n\nexport class InternSet extends Set {\n constructor(values, key = keyof) {\n super();\n Object.defineProperties(this, {_intern: {value: new Map()}, _key: {value: key}});\n if (values != null) for (const value of values) this.add(value);\n }\n has(value) {\n return super.has(intern_get(this, value));\n }\n add(value) {\n return super.add(intern_set(this, value));\n }\n delete(value) {\n return super.delete(intern_delete(this, value));\n }\n}\n\nfunction intern_get({_intern, _key}, value) {\n const key = _key(value);\n return _intern.has(key) ? _intern.get(key) : value;\n}\n\nfunction intern_set({_intern, _key}, value) {\n const key = _key(value);\n if (_intern.has(key)) return _intern.get(key);\n _intern.set(key, value);\n return value;\n}\n\nfunction intern_delete({_intern, _key}, value) {\n const key = _key(value);\n if (_intern.has(key)) {\n value = _intern.get(key);\n _intern.delete(key);\n }\n return value;\n}\n\nfunction keyof(value) {\n return value !== null && typeof value === \"object\" ? value.valueOf() : value;\n}\n", "import {InternMap} from \"internmap\";\nimport identity from \"./identity.js\";\n\nexport default function group(values, ...keys) {\n return nest(values, identity, identity, keys);\n}\n\nexport function groups(values, ...keys) {\n return nest(values, Array.from, identity, keys);\n}\n\nfunction flatten(groups, keys) {\n for (let i = 1, n = keys.length; i < n; ++i) {\n groups = groups.flatMap(g => g.pop().map(([key, value]) => [...g, key, value]));\n }\n return groups;\n}\n\nexport function flatGroup(values, ...keys) {\n return flatten(groups(values, ...keys), keys);\n}\n\nexport function flatRollup(values, reduce, ...keys) {\n return flatten(rollups(values, reduce, ...keys), keys);\n}\n\nexport function rollup(values, reduce, ...keys) {\n return nest(values, identity, reduce, keys);\n}\n\nexport function rollups(values, reduce, ...keys) {\n return nest(values, Array.from, reduce, keys);\n}\n\nexport function index(values, ...keys) {\n return nest(values, identity, unique, keys);\n}\n\nexport function indexes(values, ...keys) {\n return nest(values, Array.from, unique, keys);\n}\n\nfunction unique(values) {\n if (values.length !== 1) throw new Error(\"duplicate key\");\n return values[0];\n}\n\nfunction nest(values, map, reduce, keys) {\n return (function regroup(values, i) {\n if (i >= keys.length) return reduce(values);\n const groups = new InternMap();\n const keyof = keys[i++];\n let index = -1;\n for (const value of values) {\n const key = keyof(value, ++index, values);\n const group = groups.get(key);\n if (group) group.push(value);\n else groups.set(key, [value]);\n }\n for (const [key, values] of groups) {\n groups.set(key, regroup(values, i));\n }\n return map(groups);\n })(values, 0);\n}\n", "export default function permute(source, keys) {\n return Array.from(keys, key => source[key]);\n}\n", "import ascending from \"./ascending.js\";\nimport permute from \"./permute.js\";\n\nexport default function sort(values, ...F) {\n if (typeof values[Symbol.iterator] !== \"function\") throw new TypeError(\"values is not iterable\");\n values = Array.from(values);\n let [f] = F;\n if ((f && f.length !== 2) || F.length > 1) {\n const index = Uint32Array.from(values, (d, i) => i);\n if (F.length > 1) {\n F = F.map(f => values.map(f));\n index.sort((i, j) => {\n for (const f of F) {\n const c = ascendingDefined(f[i], f[j]);\n if (c) return c;\n }\n });\n } else {\n f = values.map(f);\n index.sort((i, j) => ascendingDefined(f[i], f[j]));\n }\n return permute(values, index);\n }\n return values.sort(compareDefined(f));\n}\n\nexport function compareDefined(compare = ascending) {\n if (compare === ascending) return ascendingDefined;\n if (typeof compare !== \"function\") throw new TypeError(\"compare is not a function\");\n return (a, b) => {\n const x = compare(a, b);\n if (x || x === 0) return x;\n return (compare(b, b) === 0) - (compare(a, a) === 0);\n };\n}\n\nexport function ascendingDefined(a, b) {\n return (a == null || !(a >= a)) - (b == null || !(b >= b)) || (a < b ? -1 : a > b ? 1 : 0);\n}\n", "import ascending from \"./ascending.js\";\nimport group, {rollup} from \"./group.js\";\nimport sort from \"./sort.js\";\n\nexport default function groupSort(values, reduce, key) {\n return (reduce.length !== 2\n ? sort(rollup(values, reduce, key), (([ak, av], [bk, bv]) => ascending(av, bv) || ascending(ak, bk)))\n : sort(group(values, key), (([ak, av], [bk, bv]) => reduce(av, bv) || ascending(ak, bk))))\n .map(([key]) => key);\n}\n", "const e10 = Math.sqrt(50),\n e5 = Math.sqrt(10),\n e2 = Math.sqrt(2);\n\nfunction tickSpec(start, stop, count) {\n const step = (stop - start) / Math.max(0, count),\n power = Math.floor(Math.log10(step)),\n error = step / Math.pow(10, power),\n factor = error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1;\n let i1, i2, inc;\n if (power < 0) {\n inc = Math.pow(10, -power) / factor;\n i1 = Math.round(start * inc);\n i2 = Math.round(stop * inc);\n if (i1 / inc < start) ++i1;\n if (i2 / inc > stop) --i2;\n inc = -inc;\n } else {\n inc = Math.pow(10, power) * factor;\n i1 = Math.round(start / inc);\n i2 = Math.round(stop / inc);\n if (i1 * inc < start) ++i1;\n if (i2 * inc > stop) --i2;\n }\n if (i2 < i1 && 0.5 <= count && count < 2) return tickSpec(start, stop, count * 2);\n return [i1, i2, inc];\n}\n\nexport default function ticks(start, stop, count) {\n stop = +stop, start = +start, count = +count;\n if (!(count > 0)) return [];\n if (start === stop) return [start];\n const reverse = stop < start, [i1, i2, inc] = reverse ? tickSpec(stop, start, count) : tickSpec(start, stop, count);\n if (!(i2 >= i1)) return [];\n const n = i2 - i1 + 1, ticks = new Array(n);\n if (reverse) {\n if (inc < 0) for (let i = 0; i < n; ++i) ticks[i] = (i2 - i) / -inc;\n else for (let i = 0; i < n; ++i) ticks[i] = (i2 - i) * inc;\n } else {\n if (inc < 0) for (let i = 0; i < n; ++i) ticks[i] = (i1 + i) / -inc;\n else for (let i = 0; i < n; ++i) ticks[i] = (i1 + i) * inc;\n }\n return ticks;\n}\n\nexport function tickIncrement(start, stop, count) {\n stop = +stop, start = +start, count = +count;\n return tickSpec(start, stop, count)[2];\n}\n\nexport function tickStep(start, stop, count) {\n stop = +stop, start = +start, count = +count;\n const reverse = stop < start, inc = reverse ? tickIncrement(stop, start, count) : tickIncrement(start, stop, count);\n return (reverse ? -1 : 1) * (inc < 0 ? 1 / -inc : inc);\n}\n", "import {tickIncrement} from \"./ticks.js\";\n\nexport default function nice(start, stop, count) {\n let prestep;\n while (true) {\n const step = tickIncrement(start, stop, count);\n if (step === prestep || step === 0 || !isFinite(step)) {\n return [start, stop];\n } else if (step > 0) {\n start = Math.floor(start / step) * step;\n stop = Math.ceil(stop / step) * step;\n } else if (step < 0) {\n start = Math.ceil(start * step) / step;\n stop = Math.floor(stop * step) / step;\n }\n prestep = step;\n }\n}\n", "import count from \"../count.js\";\n\nexport default function thresholdSturges(values) {\n return Math.max(1, Math.ceil(Math.log(count(values)) / Math.LN2) + 1);\n}\n", "import {slice} from \"./array.js\";\nimport bisect from \"./bisect.js\";\nimport constant from \"./constant.js\";\nimport extent from \"./extent.js\";\nimport identity from \"./identity.js\";\nimport nice from \"./nice.js\";\nimport ticks, {tickIncrement} from \"./ticks.js\";\nimport sturges from \"./threshold/sturges.js\";\n\nexport default function bin() {\n var value = identity,\n domain = extent,\n threshold = sturges;\n\n function histogram(data) {\n if (!Array.isArray(data)) data = Array.from(data);\n\n var i,\n n = data.length,\n x,\n step,\n values = new Array(n);\n\n for (i = 0; i < n; ++i) {\n values[i] = value(data[i], i, data);\n }\n\n var xz = domain(values),\n x0 = xz[0],\n x1 = xz[1],\n tz = threshold(values, x0, x1);\n\n // Convert number of thresholds into uniform thresholds, and nice the\n // default domain accordingly.\n if (!Array.isArray(tz)) {\n const max = x1, tn = +tz;\n if (domain === extent) [x0, x1] = nice(x0, x1, tn);\n tz = ticks(x0, x1, tn);\n\n // If the domain is aligned with the first tick (which it will by\n // default), then we can use quantization rather than bisection to bin\n // values, which is substantially faster.\n if (tz[0] <= x0) step = tickIncrement(x0, x1, tn);\n\n // If the last threshold is coincident with the domain\u2019s upper bound, the\n // last bin will be zero-width. If the default domain is used, and this\n // last threshold is coincident with the maximum input value, we can\n // extend the niced upper bound by one tick to ensure uniform bin widths;\n // otherwise, we simply remove the last threshold. Note that we don\u2019t\n // coerce values or the domain to numbers, and thus must be careful to\n // compare order (>=) rather than strict equality (===)!\n if (tz[tz.length - 1] >= x1) {\n if (max >= x1 && domain === extent) {\n const step = tickIncrement(x0, x1, tn);\n if (isFinite(step)) {\n if (step > 0) {\n x1 = (Math.floor(x1 / step) + 1) * step;\n } else if (step < 0) {\n x1 = (Math.ceil(x1 * -step) + 1) / -step;\n }\n }\n } else {\n tz.pop();\n }\n }\n }\n\n // Remove any thresholds outside the domain.\n // Be careful not to mutate an array owned by the user!\n var m = tz.length, a = 0, b = m;\n while (tz[a] <= x0) ++a;\n while (tz[b - 1] > x1) --b;\n if (a || b < m) tz = tz.slice(a, b), m = b - a;\n\n var bins = new Array(m + 1),\n bin;\n\n // Initialize bins.\n for (i = 0; i <= m; ++i) {\n bin = bins[i] = [];\n bin.x0 = i > 0 ? tz[i - 1] : x0;\n bin.x1 = i < m ? tz[i] : x1;\n }\n\n // Assign data to bins by value, ignoring any outside the domain.\n if (isFinite(step)) {\n if (step > 0) {\n for (i = 0; i < n; ++i) {\n if ((x = values[i]) != null && x0 <= x && x <= x1) {\n bins[Math.min(m, Math.floor((x - x0) / step))].push(data[i]);\n }\n }\n } else if (step < 0) {\n for (i = 0; i < n; ++i) {\n if ((x = values[i]) != null && x0 <= x && x <= x1) {\n const j = Math.floor((x0 - x) * step);\n bins[Math.min(m, j + (tz[j] <= x))].push(data[i]); // handle off-by-one due to rounding\n }\n }\n }\n } else {\n for (i = 0; i < n; ++i) {\n if ((x = values[i]) != null && x0 <= x && x <= x1) {\n bins[bisect(tz, x, 0, m)].push(data[i]);\n }\n }\n }\n\n return bins;\n }\n\n histogram.value = function(_) {\n return arguments.length ? (value = typeof _ === \"function\" ? _ : constant(_), histogram) : value;\n };\n\n histogram.domain = function(_) {\n return arguments.length ? (domain = typeof _ === \"function\" ? _ : constant([_[0], _[1]]), histogram) : domain;\n };\n\n histogram.thresholds = function(_) {\n return arguments.length ? (threshold = typeof _ === \"function\" ? _ : constant(Array.isArray(_) ? slice.call(_) : _), histogram) : threshold;\n };\n\n return histogram;\n}\n", "export default function max(values, valueof) {\n let max;\n if (valueof === undefined) {\n for (const value of values) {\n if (value != null\n && (max < value || (max === undefined && value >= value))) {\n max = value;\n }\n }\n } else {\n let index = -1;\n for (let value of values) {\n if ((value = valueof(value, ++index, values)) != null\n && (max < value || (max === undefined && value >= value))) {\n max = value;\n }\n }\n }\n return max;\n}\n", "export default function maxIndex(values, valueof) {\n let max;\n let maxIndex = -1;\n let index = -1;\n if (valueof === undefined) {\n for (const value of values) {\n ++index;\n if (value != null\n && (max < value || (max === undefined && value >= value))) {\n max = value, maxIndex = index;\n }\n }\n } else {\n for (let value of values) {\n if ((value = valueof(value, ++index, values)) != null\n && (max < value || (max === undefined && value >= value))) {\n max = value, maxIndex = index;\n }\n }\n }\n return maxIndex;\n}\n", "export default function min(values, valueof) {\n let min;\n if (valueof === undefined) {\n for (const value of values) {\n if (value != null\n && (min > value || (min === undefined && value >= value))) {\n min = value;\n }\n }\n } else {\n let index = -1;\n for (let value of values) {\n if ((value = valueof(value, ++index, values)) != null\n && (min > value || (min === undefined && value >= value))) {\n min = value;\n }\n }\n }\n return min;\n}\n", "export default function minIndex(values, valueof) {\n let min;\n let minIndex = -1;\n let index = -1;\n if (valueof === undefined) {\n for (const value of values) {\n ++index;\n if (value != null\n && (min > value || (min === undefined && value >= value))) {\n min = value, minIndex = index;\n }\n }\n } else {\n for (let value of values) {\n if ((value = valueof(value, ++index, values)) != null\n && (min > value || (min === undefined && value >= value))) {\n min = value, minIndex = index;\n }\n }\n }\n return minIndex;\n}\n", "import {ascendingDefined, compareDefined} from \"./sort.js\";\n\n// Based on https://github.com/mourner/quickselect\n// ISC license, Copyright 2018 Vladimir Agafonkin.\nexport default function quickselect(array, k, left = 0, right = Infinity, compare) {\n k = Math.floor(k);\n left = Math.floor(Math.max(0, left));\n right = Math.floor(Math.min(array.length - 1, right));\n\n if (!(left <= k && k <= right)) return array;\n\n compare = compare === undefined ? ascendingDefined : compareDefined(compare);\n\n while (right > left) {\n if (right - left > 600) {\n const n = right - left + 1;\n const m = k - left + 1;\n const z = Math.log(n);\n const s = 0.5 * Math.exp(2 * z / 3);\n const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);\n const newLeft = Math.max(left, Math.floor(k - m * s / n + sd));\n const newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));\n quickselect(array, k, newLeft, newRight, compare);\n }\n\n const t = array[k];\n let i = left;\n let j = right;\n\n swap(array, left, k);\n if (compare(array[right], t) > 0) swap(array, left, right);\n\n while (i < j) {\n swap(array, i, j), ++i, --j;\n while (compare(array[i], t) < 0) ++i;\n while (compare(array[j], t) > 0) --j;\n }\n\n if (compare(array[left], t) === 0) swap(array, left, j);\n else ++j, swap(array, j, right);\n\n if (j <= k) left = j + 1;\n if (k <= j) right = j - 1;\n }\n\n return array;\n}\n\nfunction swap(array, i, j) {\n const t = array[i];\n array[i] = array[j];\n array[j] = t;\n}\n", "import ascending from \"./ascending.js\";\n\nexport default function greatest(values, compare = ascending) {\n let max;\n let defined = false;\n if (compare.length === 1) {\n let maxValue;\n for (const element of values) {\n const value = compare(element);\n if (defined\n ? ascending(value, maxValue) > 0\n : ascending(value, value) === 0) {\n max = element;\n maxValue = value;\n defined = true;\n }\n }\n } else {\n for (const value of values) {\n if (defined\n ? compare(value, max) > 0\n : compare(value, value) === 0) {\n max = value;\n defined = true;\n }\n }\n }\n return max;\n}\n", "import max from \"./max.js\";\nimport maxIndex from \"./maxIndex.js\";\nimport min from \"./min.js\";\nimport minIndex from \"./minIndex.js\";\nimport quickselect from \"./quickselect.js\";\nimport number, {numbers} from \"./number.js\";\nimport {ascendingDefined} from \"./sort.js\";\nimport greatest from \"./greatest.js\";\n\nexport default function quantile(values, p, valueof) {\n values = Float64Array.from(numbers(values, valueof));\n if (!(n = values.length) || isNaN(p = +p)) return;\n if (p <= 0 || n < 2) return min(values);\n if (p >= 1) return max(values);\n var n,\n i = (n - 1) * p,\n i0 = Math.floor(i),\n value0 = max(quickselect(values, i0).subarray(0, i0 + 1)),\n value1 = min(values.subarray(i0 + 1));\n return value0 + (value1 - value0) * (i - i0);\n}\n\nexport function quantileSorted(values, p, valueof = number) {\n if (!(n = values.length) || isNaN(p = +p)) return;\n if (p <= 0 || n < 2) return +valueof(values[0], 0, values);\n if (p >= 1) return +valueof(values[n - 1], n - 1, values);\n var n,\n i = (n - 1) * p,\n i0 = Math.floor(i),\n value0 = +valueof(values[i0], i0, values),\n value1 = +valueof(values[i0 + 1], i0 + 1, values);\n return value0 + (value1 - value0) * (i - i0);\n}\n\nexport function quantileIndex(values, p, valueof = number) {\n if (isNaN(p = +p)) return;\n numbers = Float64Array.from(values, (_, i) => number(valueof(values[i], i, values)));\n if (p <= 0) return minIndex(numbers);\n if (p >= 1) return maxIndex(numbers);\n var numbers,\n index = Uint32Array.from(values, (_, i) => i),\n j = numbers.length - 1,\n i = Math.floor(j * p);\n quickselect(index, i, 0, j, (i, j) => ascendingDefined(numbers[i], numbers[j]));\n i = greatest(index.subarray(0, i + 1), (i) => numbers[i]);\n return i >= 0 ? i : -1;\n}\n", "import count from \"../count.js\";\nimport quantile from \"../quantile.js\";\n\nexport default function thresholdFreedmanDiaconis(values, min, max) {\n const c = count(values), d = quantile(values, 0.75) - quantile(values, 0.25);\n return c && d ? Math.ceil((max - min) / (2 * d * Math.pow(c, -1 / 3))) : 1;\n}\n", "import count from \"../count.js\";\nimport deviation from \"../deviation.js\";\n\nexport default function thresholdScott(values, min, max) {\n const c = count(values), d = deviation(values);\n return c && d ? Math.ceil((max - min) * Math.cbrt(c) / (3.49 * d)) : 1;\n}\n", "export default function mean(values, valueof) {\n let count = 0;\n let sum = 0;\n if (valueof === undefined) {\n for (let value of values) {\n if (value != null && (value = +value) >= value) {\n ++count, sum += value;\n }\n }\n } else {\n let index = -1;\n for (let value of values) {\n if ((value = valueof(value, ++index, values)) != null && (value = +value) >= value) {\n ++count, sum += value;\n }\n }\n }\n if (count) return sum / count;\n}\n", "import quantile, {quantileIndex} from \"./quantile.js\";\n\nexport default function median(values, valueof) {\n return quantile(values, 0.5, valueof);\n}\n\nexport function medianIndex(values, valueof) {\n return quantileIndex(values, 0.5, valueof);\n}\n", "function* flatten(arrays) {\n for (const array of arrays) {\n yield* array;\n }\n}\n\nexport default function merge(arrays) {\n return Array.from(flatten(arrays));\n}\n", "import {InternMap} from \"internmap\";\n\nexport default function mode(values, valueof) {\n const counts = new InternMap();\n if (valueof === undefined) {\n for (let value of values) {\n if (value != null && value >= value) {\n counts.set(value, (counts.get(value) || 0) + 1);\n }\n }\n } else {\n let index = -1;\n for (let value of values) {\n if ((value = valueof(value, ++index, values)) != null && value >= value) {\n counts.set(value, (counts.get(value) || 0) + 1);\n }\n }\n }\n let modeValue;\n let modeCount = 0;\n for (const [value, count] of counts) {\n if (count > modeCount) {\n modeCount = count;\n modeValue = value;\n }\n }\n return modeValue;\n}\n", "export default function pairs(values, pairof = pair) {\n const pairs = [];\n let previous;\n let first = false;\n for (const value of values) {\n if (first) pairs.push(pairof(previous, value));\n previous = value;\n first = true;\n }\n return pairs;\n}\n\nexport function pair(a, b) {\n return [a, b];\n}\n", "export default function range(start, stop, step) {\n start = +start, stop = +stop, step = (n = arguments.length) < 2 ? (stop = start, start = 0, 1) : n < 3 ? 1 : +step;\n\n var i = -1,\n n = Math.max(0, Math.ceil((stop - start) / step)) | 0,\n range = new Array(n);\n\n while (++i < n) {\n range[i] = start + i * step;\n }\n\n return range;\n}\n", "import ascending from \"./ascending.js\";\nimport {ascendingDefined, compareDefined} from \"./sort.js\";\n\nexport default function rank(values, valueof = ascending) {\n if (typeof values[Symbol.iterator] !== \"function\") throw new TypeError(\"values is not iterable\");\n let V = Array.from(values);\n const R = new Float64Array(V.length);\n if (valueof.length !== 2) V = V.map(valueof), valueof = ascending;\n const compareIndex = (i, j) => valueof(V[i], V[j]);\n let k, r;\n values = Uint32Array.from(V, (_, i) => i);\n // Risky chaining due to Safari 14 https://github.com/d3/d3-array/issues/123\n values.sort(valueof === ascending ? (i, j) => ascendingDefined(V[i], V[j]) : compareDefined(compareIndex));\n values.forEach((j, i) => {\n const c = compareIndex(j, k === undefined ? j : k);\n if (c >= 0) {\n if (k === undefined || c > 0) k = j, r = i;\n R[j] = r;\n } else {\n R[j] = NaN;\n }\n });\n return R;\n}\n", "import ascending from \"./ascending.js\";\n\nexport default function least(values, compare = ascending) {\n let min;\n let defined = false;\n if (compare.length === 1) {\n let minValue;\n for (const element of values) {\n const value = compare(element);\n if (defined\n ? ascending(value, minValue) < 0\n : ascending(value, value) === 0) {\n min = element;\n minValue = value;\n defined = true;\n }\n }\n } else {\n for (const value of values) {\n if (defined\n ? compare(value, min) < 0\n : compare(value, value) === 0) {\n min = value;\n defined = true;\n }\n }\n }\n return min;\n}\n", "import ascending from \"./ascending.js\";\nimport minIndex from \"./minIndex.js\";\n\nexport default function leastIndex(values, compare = ascending) {\n if (compare.length === 1) return minIndex(values, compare);\n let minValue;\n let min = -1;\n let index = -1;\n for (const value of values) {\n ++index;\n if (min < 0\n ? compare(value, value) === 0\n : compare(value, minValue) < 0) {\n minValue = value;\n min = index;\n }\n }\n return min;\n}\n", "import ascending from \"./ascending.js\";\nimport maxIndex from \"./maxIndex.js\";\n\nexport default function greatestIndex(values, compare = ascending) {\n if (compare.length === 1) return maxIndex(values, compare);\n let maxValue;\n let max = -1;\n let index = -1;\n for (const value of values) {\n ++index;\n if (max < 0\n ? compare(value, value) === 0\n : compare(value, maxValue) > 0) {\n maxValue = value;\n max = index;\n }\n }\n return max;\n}\n", "import leastIndex from \"./leastIndex.js\";\n\nexport default function scan(values, compare) {\n const index = leastIndex(values, compare);\n return index < 0 ? undefined : index;\n}\n", "export default shuffler(Math.random);\n\nexport function shuffler(random) {\n return function shuffle(array, i0 = 0, i1 = array.length) {\n let m = i1 - (i0 = +i0);\n while (m) {\n const i = random() * m-- | 0, t = array[m + i0];\n array[m + i0] = array[i + i0];\n array[i + i0] = t;\n }\n return array;\n };\n}\n", "export default function sum(values, valueof) {\n let sum = 0;\n if (valueof === undefined) {\n for (let value of values) {\n if (value = +value) {\n sum += value;\n }\n }\n } else {\n let index = -1;\n for (let value of values) {\n if (value = +valueof(value, ++index, values)) {\n sum += value;\n }\n }\n }\n return sum;\n}\n", "import min from \"./min.js\";\n\nexport default function transpose(matrix) {\n if (!(n = matrix.length)) return [];\n for (var i = -1, m = min(matrix, length), transpose = new Array(m); ++i < m;) {\n for (var j = -1, n, row = transpose[i] = new Array(n); ++j < n;) {\n row[j] = matrix[j][i];\n }\n }\n return transpose;\n}\n\nfunction length(d) {\n return d.length;\n}\n", "import transpose from \"./transpose.js\";\n\nexport default function zip() {\n return transpose(arguments);\n}\n", "export default function every(values, test) {\n if (typeof test !== \"function\") throw new TypeError(\"test is not a function\");\n let index = -1;\n for (const value of values) {\n if (!test(value, ++index, values)) {\n return false;\n }\n }\n return true;\n}\n", "export default function some(values, test) {\n if (typeof test !== \"function\") throw new TypeError(\"test is not a function\");\n let index = -1;\n for (const value of values) {\n if (test(value, ++index, values)) {\n return true;\n }\n }\n return false;\n}\n", "export default function filter(values, test) {\n if (typeof test !== \"function\") throw new TypeError(\"test is not a function\");\n const array = [];\n let index = -1;\n for (const value of values) {\n if (test(value, ++index, values)) {\n array.push(value);\n }\n }\n return array;\n}\n", "export default function map(values, mapper) {\n if (typeof values[Symbol.iterator] !== \"function\") throw new TypeError(\"values is not iterable\");\n if (typeof mapper !== \"function\") throw new TypeError(\"mapper is not a function\");\n return Array.from(values, (value, index) => mapper(value, index, values));\n}\n", "export default function reduce(values, reducer, value) {\n if (typeof reducer !== \"function\") throw new TypeError(\"reducer is not a function\");\n const iterator = values[Symbol.iterator]();\n let done, next, index = -1;\n if (arguments.length < 3) {\n ({done, value} = iterator.next());\n if (done) return;\n ++index;\n }\n while (({done, value: next} = iterator.next()), !done) {\n value = reducer(value, next, ++index, values);\n }\n return value;\n}\n", "export default function reverse(values) {\n if (typeof values[Symbol.iterator] !== \"function\") throw new TypeError(\"values is not iterable\");\n return Array.from(values).reverse();\n}\n", "import {InternSet} from \"internmap\";\n\nexport default function difference(values, ...others) {\n values = new InternSet(values);\n for (const other of others) {\n for (const value of other) {\n values.delete(value);\n }\n }\n return values;\n}\n", "import {InternSet} from \"internmap\";\n\nexport default function disjoint(values, other) {\n const iterator = other[Symbol.iterator](), set = new InternSet();\n for (const v of values) {\n if (set.has(v)) return false;\n let value, done;\n while (({value, done} = iterator.next())) {\n if (done) break;\n if (Object.is(v, value)) return false;\n set.add(value);\n }\n }\n return true;\n}\n", "import {InternSet} from \"internmap\";\n\nexport default function intersection(values, ...others) {\n values = new InternSet(values);\n others = others.map(set);\n out: for (const value of values) {\n for (const other of others) {\n if (!other.has(value)) {\n values.delete(value);\n continue out;\n }\n }\n }\n return values;\n}\n\nfunction set(values) {\n return values instanceof InternSet ? values : new InternSet(values);\n}\n", "export default function superset(values, other) {\n const iterator = values[Symbol.iterator](), set = new Set();\n for (const o of other) {\n const io = intern(o);\n if (set.has(io)) continue;\n let value, done;\n while (({value, done} = iterator.next())) {\n if (done) return false;\n const ivalue = intern(value);\n set.add(ivalue);\n if (Object.is(io, ivalue)) break;\n }\n }\n return true;\n}\n\nfunction intern(value) {\n return value !== null && typeof value === \"object\" ? value.valueOf() : value;\n}\n", "import superset from \"./superset.js\";\n\nexport default function subset(values, other) {\n return superset(other, values);\n}\n", "import {InternSet} from \"internmap\";\n\nexport default function union(...others) {\n const set = new InternSet();\n for (const other of others) {\n for (const o of other) {\n set.add(o);\n }\n }\n return set;\n}\n", "export {default as bisect, bisectRight, bisectLeft, bisectCenter} from \"./bisect.js\";\nexport {default as ascending} from \"./ascending.js\";\nexport {default as bisector} from \"./bisector.js\";\nexport {blur, blur2, blurImage} from \"./blur.js\";\nexport {default as count} from \"./count.js\";\nexport {default as cross} from \"./cross.js\";\nexport {default as cumsum} from \"./cumsum.js\";\nexport {default as descending} from \"./descending.js\";\nexport {default as deviation} from \"./deviation.js\";\nexport {default as extent} from \"./extent.js\";\nexport {Adder, fsum, fcumsum} from \"./fsum.js\";\nexport {default as group, flatGroup, flatRollup, groups, index, indexes, rollup, rollups} from \"./group.js\";\nexport {default as groupSort} from \"./groupSort.js\";\nexport {default as bin, default as histogram} from \"./bin.js\"; // Deprecated; use bin.\nexport {default as thresholdFreedmanDiaconis} from \"./threshold/freedmanDiaconis.js\";\nexport {default as thresholdScott} from \"./threshold/scott.js\";\nexport {default as thresholdSturges} from \"./threshold/sturges.js\";\nexport {default as max} from \"./max.js\";\nexport {default as maxIndex} from \"./maxIndex.js\";\nexport {default as mean} from \"./mean.js\";\nexport {default as median, medianIndex} from \"./median.js\";\nexport {default as merge} from \"./merge.js\";\nexport {default as min} from \"./min.js\";\nexport {default as minIndex} from \"./minIndex.js\";\nexport {default as mode} from \"./mode.js\";\nexport {default as nice} from \"./nice.js\";\nexport {default as pairs} from \"./pairs.js\";\nexport {default as permute} from \"./permute.js\";\nexport {default as quantile, quantileIndex, quantileSorted} from \"./quantile.js\";\nexport {default as quickselect} from \"./quickselect.js\";\nexport {default as range} from \"./range.js\";\nexport {default as rank} from \"./rank.js\";\nexport {default as least} from \"./least.js\";\nexport {default as leastIndex} from \"./leastIndex.js\";\nexport {default as greatest} from \"./greatest.js\";\nexport {default as greatestIndex} from \"./greatestIndex.js\";\nexport {default as scan} from \"./scan.js\"; // Deprecated; use leastIndex.\nexport {default as shuffle, shuffler} from \"./shuffle.js\";\nexport {default as sum} from \"./sum.js\";\nexport {default as ticks, tickIncrement, tickStep} from \"./ticks.js\";\nexport {default as transpose} from \"./transpose.js\";\nexport {default as variance} from \"./variance.js\";\nexport {default as zip} from \"./zip.js\";\nexport {default as every} from \"./every.js\";\nexport {default as some} from \"./some.js\";\nexport {default as filter} from \"./filter.js\";\nexport {default as map} from \"./map.js\";\nexport {default as reduce} from \"./reduce.js\";\nexport {default as reverse} from \"./reverse.js\";\nexport {default as sort} from \"./sort.js\";\nexport {default as difference} from \"./difference.js\";\nexport {default as disjoint} from \"./disjoint.js\";\nexport {default as intersection} from \"./intersection.js\";\nexport {default as subset} from \"./subset.js\";\nexport {default as superset} from \"./superset.js\";\nexport {default as union} from \"./union.js\";\nexport {InternMap, InternSet} from \"internmap\";\n", "export var epsilon = 1e-6;\nexport var epsilon2 = 1e-12;\nexport var pi = Math.PI;\nexport var halfPi = pi / 2;\nexport var quarterPi = pi / 4;\nexport var tau = pi * 2;\n\nexport var degrees = 180 / pi;\nexport var radians = pi / 180;\n\nexport var abs = Math.abs;\nexport var atan = Math.atan;\nexport var atan2 = Math.atan2;\nexport var cos = Math.cos;\nexport var ceil = Math.ceil;\nexport var exp = Math.exp;\nexport var floor = Math.floor;\nexport var hypot = Math.hypot;\nexport var log = Math.log;\nexport var pow = Math.pow;\nexport var sin = Math.sin;\nexport var sign = Math.sign || function(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; };\nexport var sqrt = Math.sqrt;\nexport var tan = Math.tan;\n\nexport function acos(x) {\n return x > 1 ? 0 : x < -1 ? pi : Math.acos(x);\n}\n\nexport function asin(x) {\n return x > 1 ? halfPi : x < -1 ? -halfPi : Math.asin(x);\n}\n\nexport function haversin(x) {\n return (x = sin(x / 2)) * x;\n}\n", "export default function noop() {}\n", "function streamGeometry(geometry, stream) {\n if (geometry && streamGeometryType.hasOwnProperty(geometry.type)) {\n streamGeometryType[geometry.type](geometry, stream);\n }\n}\n\nvar streamObjectType = {\n Feature: function(object, stream) {\n streamGeometry(object.geometry, stream);\n },\n FeatureCollection: function(object, stream) {\n var features = object.features, i = -1, n = features.length;\n while (++i < n) streamGeometry(features[i].geometry, stream);\n }\n};\n\nvar streamGeometryType = {\n Sphere: function(object, stream) {\n stream.sphere();\n },\n Point: function(object, stream) {\n object = object.coordinates;\n stream.point(object[0], object[1], object[2]);\n },\n MultiPoint: function(object, stream) {\n var coordinates = object.coordinates, i = -1, n = coordinates.length;\n while (++i < n) object = coordinates[i], stream.point(object[0], object[1], object[2]);\n },\n LineString: function(object, stream) {\n streamLine(object.coordinates, stream, 0);\n },\n MultiLineString: function(object, stream) {\n var coordinates = object.coordinates, i = -1, n = coordinates.length;\n while (++i < n) streamLine(coordinates[i], stream, 0);\n },\n Polygon: function(object, stream) {\n streamPolygon(object.coordinates, stream);\n },\n MultiPolygon: function(object, stream) {\n var coordinates = object.coordinates, i = -1, n = coordinates.length;\n while (++i < n) streamPolygon(coordinates[i], stream);\n },\n GeometryCollection: function(object, stream) {\n var geometries = object.geometries, i = -1, n = geometries.length;\n while (++i < n) streamGeometry(geometries[i], stream);\n }\n};\n\nfunction streamLine(coordinates, stream, closed) {\n var i = -1, n = coordinates.length - closed, coordinate;\n stream.lineStart();\n while (++i < n) coordinate = coordinates[i], stream.point(coordinate[0], coordinate[1], coordinate[2]);\n stream.lineEnd();\n}\n\nfunction streamPolygon(coordinates, stream) {\n var i = -1, n = coordinates.length;\n stream.polygonStart();\n while (++i < n) streamLine(coordinates[i], stream, 1);\n stream.polygonEnd();\n}\n\nexport default function(object, stream) {\n if (object && streamObjectType.hasOwnProperty(object.type)) {\n streamObjectType[object.type](object, stream);\n } else {\n streamGeometry(object, stream);\n }\n}\n", "import {Adder} from \"d3-array\";\nimport {atan2, cos, quarterPi, radians, sin, tau} from \"./math.js\";\nimport noop from \"./noop.js\";\nimport stream from \"./stream.js\";\n\nexport var areaRingSum = new Adder();\n\n// hello?\n\nvar areaSum = new Adder(),\n lambda00,\n phi00,\n lambda0,\n cosPhi0,\n sinPhi0;\n\nexport var areaStream = {\n point: noop,\n lineStart: noop,\n lineEnd: noop,\n polygonStart: function() {\n areaRingSum = new Adder();\n areaStream.lineStart = areaRingStart;\n areaStream.lineEnd = areaRingEnd;\n },\n polygonEnd: function() {\n var areaRing = +areaRingSum;\n areaSum.add(areaRing < 0 ? tau + areaRing : areaRing);\n this.lineStart = this.lineEnd = this.point = noop;\n },\n sphere: function() {\n areaSum.add(tau);\n }\n};\n\nfunction areaRingStart() {\n areaStream.point = areaPointFirst;\n}\n\nfunction areaRingEnd() {\n areaPoint(lambda00, phi00);\n}\n\nfunction areaPointFirst(lambda, phi) {\n areaStream.point = areaPoint;\n lambda00 = lambda, phi00 = phi;\n lambda *= radians, phi *= radians;\n lambda0 = lambda, cosPhi0 = cos(phi = phi / 2 + quarterPi), sinPhi0 = sin(phi);\n}\n\nfunction areaPoint(lambda, phi) {\n lambda *= radians, phi *= radians;\n phi = phi / 2 + quarterPi; // half the angular distance from south pole\n\n // Spherical excess E for a spherical triangle with vertices: south pole,\n // previous point, current point. Uses a formula derived from Cagnoli\u2019s\n // theorem. See Todhunter, Spherical Trig. (1871), Sec. 103, Eq. (2).\n var dLambda = lambda - lambda0,\n sdLambda = dLambda >= 0 ? 1 : -1,\n adLambda = sdLambda * dLambda,\n cosPhi = cos(phi),\n sinPhi = sin(phi),\n k = sinPhi0 * sinPhi,\n u = cosPhi0 * cosPhi + k * cos(adLambda),\n v = k * sdLambda * sin(adLambda);\n areaRingSum.add(atan2(v, u));\n\n // Advance the previous points.\n lambda0 = lambda, cosPhi0 = cosPhi, sinPhi0 = sinPhi;\n}\n\nexport default function(object) {\n areaSum = new Adder();\n stream(object, areaStream);\n return areaSum * 2;\n}\n", "import {asin, atan2, cos, sin, sqrt} from \"./math.js\";\n\nexport function spherical(cartesian) {\n return [atan2(cartesian[1], cartesian[0]), asin(cartesian[2])];\n}\n\nexport function cartesian(spherical) {\n var lambda = spherical[0], phi = spherical[1], cosPhi = cos(phi);\n return [cosPhi * cos(lambda), cosPhi * sin(lambda), sin(phi)];\n}\n\nexport function cartesianDot(a, b) {\n return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];\n}\n\nexport function cartesianCross(a, b) {\n return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];\n}\n\n// TODO return a\nexport function cartesianAddInPlace(a, b) {\n a[0] += b[0], a[1] += b[1], a[2] += b[2];\n}\n\nexport function cartesianScale(vector, k) {\n return [vector[0] * k, vector[1] * k, vector[2] * k];\n}\n\n// TODO return d\nexport function cartesianNormalizeInPlace(d) {\n var l = sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);\n d[0] /= l, d[1] /= l, d[2] /= l;\n}\n", "import {Adder} from \"d3-array\";\nimport {areaStream, areaRingSum} from \"./area.js\";\nimport {cartesian, cartesianCross, cartesianNormalizeInPlace, spherical} from \"./cartesian.js\";\nimport {abs, degrees, epsilon, radians} from \"./math.js\";\nimport stream from \"./stream.js\";\n\nvar lambda0, phi0, lambda1, phi1, // bounds\n lambda2, // previous lambda-coordinate\n lambda00, phi00, // first point\n p0, // previous 3D point\n deltaSum,\n ranges,\n range;\n\nvar boundsStream = {\n point: boundsPoint,\n lineStart: boundsLineStart,\n lineEnd: boundsLineEnd,\n polygonStart: function() {\n boundsStream.point = boundsRingPoint;\n boundsStream.lineStart = boundsRingStart;\n boundsStream.lineEnd = boundsRingEnd;\n deltaSum = new Adder();\n areaStream.polygonStart();\n },\n polygonEnd: function() {\n areaStream.polygonEnd();\n boundsStream.point = boundsPoint;\n boundsStream.lineStart = boundsLineStart;\n boundsStream.lineEnd = boundsLineEnd;\n if (areaRingSum < 0) lambda0 = -(lambda1 = 180), phi0 = -(phi1 = 90);\n else if (deltaSum > epsilon) phi1 = 90;\n else if (deltaSum < -epsilon) phi0 = -90;\n range[0] = lambda0, range[1] = lambda1;\n },\n sphere: function() {\n lambda0 = -(lambda1 = 180), phi0 = -(phi1 = 90);\n }\n};\n\nfunction boundsPoint(lambda, phi) {\n ranges.push(range = [lambda0 = lambda, lambda1 = lambda]);\n if (phi < phi0) phi0 = phi;\n if (phi > phi1) phi1 = phi;\n}\n\nfunction linePoint(lambda, phi) {\n var p = cartesian([lambda * radians, phi * radians]);\n if (p0) {\n var normal = cartesianCross(p0, p),\n equatorial = [normal[1], -normal[0], 0],\n inflection = cartesianCross(equatorial, normal);\n cartesianNormalizeInPlace(inflection);\n inflection = spherical(inflection);\n var delta = lambda - lambda2,\n sign = delta > 0 ? 1 : -1,\n lambdai = inflection[0] * degrees * sign,\n phii,\n antimeridian = abs(delta) > 180;\n if (antimeridian ^ (sign * lambda2 < lambdai && lambdai < sign * lambda)) {\n phii = inflection[1] * degrees;\n if (phii > phi1) phi1 = phii;\n } else if (lambdai = (lambdai + 360) % 360 - 180, antimeridian ^ (sign * lambda2 < lambdai && lambdai < sign * lambda)) {\n phii = -inflection[1] * degrees;\n if (phii < phi0) phi0 = phii;\n } else {\n if (phi < phi0) phi0 = phi;\n if (phi > phi1) phi1 = phi;\n }\n if (antimeridian) {\n if (lambda < lambda2) {\n if (angle(lambda0, lambda) > angle(lambda0, lambda1)) lambda1 = lambda;\n } else {\n if (angle(lambda, lambda1) > angle(lambda0, lambda1)) lambda0 = lambda;\n }\n } else {\n if (lambda1 >= lambda0) {\n if (lambda < lambda0) lambda0 = lambda;\n if (lambda > lambda1) lambda1 = lambda;\n } else {\n if (lambda > lambda2) {\n if (angle(lambda0, lambda) > angle(lambda0, lambda1)) lambda1 = lambda;\n } else {\n if (angle(lambda, lambda1) > angle(lambda0, lambda1)) lambda0 = lambda;\n }\n }\n }\n } else {\n ranges.push(range = [lambda0 = lambda, lambda1 = lambda]);\n }\n if (phi < phi0) phi0 = phi;\n if (phi > phi1) phi1 = phi;\n p0 = p, lambda2 = lambda;\n}\n\nfunction boundsLineStart() {\n boundsStream.point = linePoint;\n}\n\nfunction boundsLineEnd() {\n range[0] = lambda0, range[1] = lambda1;\n boundsStream.point = boundsPoint;\n p0 = null;\n}\n\nfunction boundsRingPoint(lambda, phi) {\n if (p0) {\n var delta = lambda - lambda2;\n deltaSum.add(abs(delta) > 180 ? delta + (delta > 0 ? 360 : -360) : delta);\n } else {\n lambda00 = lambda, phi00 = phi;\n }\n areaStream.point(lambda, phi);\n linePoint(lambda, phi);\n}\n\nfunction boundsRingStart() {\n areaStream.lineStart();\n}\n\nfunction boundsRingEnd() {\n boundsRingPoint(lambda00, phi00);\n areaStream.lineEnd();\n if (abs(deltaSum) > epsilon) lambda0 = -(lambda1 = 180);\n range[0] = lambda0, range[1] = lambda1;\n p0 = null;\n}\n\n// Finds the left-right distance between two longitudes.\n// This is almost the same as (lambda1 - lambda0 + 360\u00B0) % 360\u00B0, except that we want\n// the distance between \u00B1180\u00B0 to be 360\u00B0.\nfunction angle(lambda0, lambda1) {\n return (lambda1 -= lambda0) < 0 ? lambda1 + 360 : lambda1;\n}\n\nfunction rangeCompare(a, b) {\n return a[0] - b[0];\n}\n\nfunction rangeContains(range, x) {\n return range[0] <= range[1] ? range[0] <= x && x <= range[1] : x < range[0] || range[1] < x;\n}\n\nexport default function(feature) {\n var i, n, a, b, merged, deltaMax, delta;\n\n phi1 = lambda1 = -(lambda0 = phi0 = Infinity);\n ranges = [];\n stream(feature, boundsStream);\n\n // First, sort ranges by their minimum longitudes.\n if (n = ranges.length) {\n ranges.sort(rangeCompare);\n\n // Then, merge any ranges that overlap.\n for (i = 1, a = ranges[0], merged = [a]; i < n; ++i) {\n b = ranges[i];\n if (rangeContains(a, b[0]) || rangeContains(a, b[1])) {\n if (angle(a[0], b[1]) > angle(a[0], a[1])) a[1] = b[1];\n if (angle(b[0], a[1]) > angle(a[0], a[1])) a[0] = b[0];\n } else {\n merged.push(a = b);\n }\n }\n\n // Finally, find the largest gap between the merged ranges.\n // The final bounding box will be the inverse of this gap.\n for (deltaMax = -Infinity, n = merged.length - 1, i = 0, a = merged[n]; i <= n; a = b, ++i) {\n b = merged[i];\n if ((delta = angle(a[1], b[0])) > deltaMax) deltaMax = delta, lambda0 = b[0], lambda1 = a[1];\n }\n }\n\n ranges = range = null;\n\n return lambda0 === Infinity || phi0 === Infinity\n ? [[NaN, NaN], [NaN, NaN]]\n : [[lambda0, phi0], [lambda1, phi1]];\n}\n", "import {Adder} from \"d3-array\";\nimport {asin, atan2, cos, degrees, epsilon, epsilon2, hypot, radians, sin, sqrt} from \"./math.js\";\nimport noop from \"./noop.js\";\nimport stream from \"./stream.js\";\n\nvar W0, W1,\n X0, Y0, Z0,\n X1, Y1, Z1,\n X2, Y2, Z2,\n lambda00, phi00, // first point\n x0, y0, z0; // previous point\n\nvar centroidStream = {\n sphere: noop,\n point: centroidPoint,\n lineStart: centroidLineStart,\n lineEnd: centroidLineEnd,\n polygonStart: function() {\n centroidStream.lineStart = centroidRingStart;\n centroidStream.lineEnd = centroidRingEnd;\n },\n polygonEnd: function() {\n centroidStream.lineStart = centroidLineStart;\n centroidStream.lineEnd = centroidLineEnd;\n }\n};\n\n// Arithmetic mean of Cartesian vectors.\nfunction centroidPoint(lambda, phi) {\n lambda *= radians, phi *= radians;\n var cosPhi = cos(phi);\n centroidPointCartesian(cosPhi * cos(lambda), cosPhi * sin(lambda), sin(phi));\n}\n\nfunction centroidPointCartesian(x, y, z) {\n ++W0;\n X0 += (x - X0) / W0;\n Y0 += (y - Y0) / W0;\n Z0 += (z - Z0) / W0;\n}\n\nfunction centroidLineStart() {\n centroidStream.point = centroidLinePointFirst;\n}\n\nfunction centroidLinePointFirst(lambda, phi) {\n lambda *= radians, phi *= radians;\n var cosPhi = cos(phi);\n x0 = cosPhi * cos(lambda);\n y0 = cosPhi * sin(lambda);\n z0 = sin(phi);\n centroidStream.point = centroidLinePoint;\n centroidPointCartesian(x0, y0, z0);\n}\n\nfunction centroidLinePoint(lambda, phi) {\n lambda *= radians, phi *= radians;\n var cosPhi = cos(phi),\n x = cosPhi * cos(lambda),\n y = cosPhi * sin(lambda),\n z = sin(phi),\n w = atan2(sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z);\n W1 += w;\n X1 += w * (x0 + (x0 = x));\n Y1 += w * (y0 + (y0 = y));\n Z1 += w * (z0 + (z0 = z));\n centroidPointCartesian(x0, y0, z0);\n}\n\nfunction centroidLineEnd() {\n centroidStream.point = centroidPoint;\n}\n\n// See J. E. Brock, The Inertia Tensor for a Spherical Triangle,\n// J. Applied Mechanics 42, 239 (1975).\nfunction centroidRingStart() {\n centroidStream.point = centroidRingPointFirst;\n}\n\nfunction centroidRingEnd() {\n centroidRingPoint(lambda00, phi00);\n centroidStream.point = centroidPoint;\n}\n\nfunction centroidRingPointFirst(lambda, phi) {\n lambda00 = lambda, phi00 = phi;\n lambda *= radians, phi *= radians;\n centroidStream.point = centroidRingPoint;\n var cosPhi = cos(phi);\n x0 = cosPhi * cos(lambda);\n y0 = cosPhi * sin(lambda);\n z0 = sin(phi);\n centroidPointCartesian(x0, y0, z0);\n}\n\nfunction centroidRingPoint(lambda, phi) {\n lambda *= radians, phi *= radians;\n var cosPhi = cos(phi),\n x = cosPhi * cos(lambda),\n y = cosPhi * sin(lambda),\n z = sin(phi),\n cx = y0 * z - z0 * y,\n cy = z0 * x - x0 * z,\n cz = x0 * y - y0 * x,\n m = hypot(cx, cy, cz),\n w = asin(m), // line weight = angle\n v = m && -w / m; // area weight multiplier\n X2.add(v * cx);\n Y2.add(v * cy);\n Z2.add(v * cz);\n W1 += w;\n X1 += w * (x0 + (x0 = x));\n Y1 += w * (y0 + (y0 = y));\n Z1 += w * (z0 + (z0 = z));\n centroidPointCartesian(x0, y0, z0);\n}\n\nexport default function(object) {\n W0 = W1 =\n X0 = Y0 = Z0 =\n X1 = Y1 = Z1 = 0;\n X2 = new Adder();\n Y2 = new Adder();\n Z2 = new Adder();\n stream(object, centroidStream);\n\n var x = +X2,\n y = +Y2,\n z = +Z2,\n m = hypot(x, y, z);\n\n // If the area-weighted ccentroid is undefined, fall back to length-weighted ccentroid.\n if (m < epsilon2) {\n x = X1, y = Y1, z = Z1;\n // If the feature has zero length, fall back to arithmetic mean of point vectors.\n if (W1 < epsilon) x = X0, y = Y0, z = Z0;\n m = hypot(x, y, z);\n // If the feature still has an undefined ccentroid, then return.\n if (m < epsilon2) return [NaN, NaN];\n }\n\n return [atan2(y, x) * degrees, asin(z / m) * degrees];\n}\n", "export default function(x) {\n return function() {\n return x;\n };\n}\n", "export default function(a, b) {\n\n function compose(x, y) {\n return x = a(x, y), b(x[0], x[1]);\n }\n\n if (a.invert && b.invert) compose.invert = function(x, y) {\n return x = b.invert(x, y), x && a.invert(x[0], x[1]);\n };\n\n return compose;\n}\n", "import compose from \"./compose.js\";\nimport {abs, asin, atan2, cos, degrees, pi, radians, sin, tau} from \"./math.js\";\n\nfunction rotationIdentity(lambda, phi) {\n if (abs(lambda) > pi) lambda -= Math.round(lambda / tau) * tau;\n return [lambda, phi];\n}\n\nrotationIdentity.invert = rotationIdentity;\n\nexport function rotateRadians(deltaLambda, deltaPhi, deltaGamma) {\n return (deltaLambda %= tau) ? (deltaPhi || deltaGamma ? compose(rotationLambda(deltaLambda), rotationPhiGamma(deltaPhi, deltaGamma))\n : rotationLambda(deltaLambda))\n : (deltaPhi || deltaGamma ? rotationPhiGamma(deltaPhi, deltaGamma)\n : rotationIdentity);\n}\n\nfunction forwardRotationLambda(deltaLambda) {\n return function(lambda, phi) {\n lambda += deltaLambda;\n if (abs(lambda) > pi) lambda -= Math.round(lambda / tau) * tau;\n return [lambda, phi];\n };\n}\n\nfunction rotationLambda(deltaLambda) {\n var rotation = forwardRotationLambda(deltaLambda);\n rotation.invert = forwardRotationLambda(-deltaLambda);\n return rotation;\n}\n\nfunction rotationPhiGamma(deltaPhi, deltaGamma) {\n var cosDeltaPhi = cos(deltaPhi),\n sinDeltaPhi = sin(deltaPhi),\n cosDeltaGamma = cos(deltaGamma),\n sinDeltaGamma = sin(deltaGamma);\n\n function rotation(lambda, phi) {\n var cosPhi = cos(phi),\n x = cos(lambda) * cosPhi,\n y = sin(lambda) * cosPhi,\n z = sin(phi),\n k = z * cosDeltaPhi + x * sinDeltaPhi;\n return [\n atan2(y * cosDeltaGamma - k * sinDeltaGamma, x * cosDeltaPhi - z * sinDeltaPhi),\n asin(k * cosDeltaGamma + y * sinDeltaGamma)\n ];\n }\n\n rotation.invert = function(lambda, phi) {\n var cosPhi = cos(phi),\n x = cos(lambda) * cosPhi,\n y = sin(lambda) * cosPhi,\n z = sin(phi),\n k = z * cosDeltaGamma - y * sinDeltaGamma;\n return [\n atan2(y * cosDeltaGamma + z * sinDeltaGamma, x * cosDeltaPhi + k * sinDeltaPhi),\n asin(k * cosDeltaPhi - x * sinDeltaPhi)\n ];\n };\n\n return rotation;\n}\n\nexport default function(rotate) {\n rotate = rotateRadians(rotate[0] * radians, rotate[1] * radians, rotate.length > 2 ? rotate[2] * radians : 0);\n\n function forward(coordinates) {\n coordinates = rotate(coordinates[0] * radians, coordinates[1] * radians);\n return coordinates[0] *= degrees, coordinates[1] *= degrees, coordinates;\n }\n\n forward.invert = function(coordinates) {\n coordinates = rotate.invert(coordinates[0] * radians, coordinates[1] * radians);\n return coordinates[0] *= degrees, coordinates[1] *= degrees, coordinates;\n };\n\n return forward;\n}\n", "import {cartesian, cartesianNormalizeInPlace, spherical} from \"./cartesian.js\";\nimport constant from \"./constant.js\";\nimport {acos, cos, degrees, epsilon, radians, sin, tau} from \"./math.js\";\nimport {rotateRadians} from \"./rotation.js\";\n\n// Generates a circle centered at [0\u00B0, 0\u00B0], with a given radius and precision.\nexport function circleStream(stream, radius, delta, direction, t0, t1) {\n if (!delta) return;\n var cosRadius = cos(radius),\n sinRadius = sin(radius),\n step = direction * delta;\n if (t0 == null) {\n t0 = radius + direction * tau;\n t1 = radius - step / 2;\n } else {\n t0 = circleRadius(cosRadius, t0);\n t1 = circleRadius(cosRadius, t1);\n if (direction > 0 ? t0 < t1 : t0 > t1) t0 += direction * tau;\n }\n for (var point, t = t0; direction > 0 ? t > t1 : t < t1; t -= step) {\n point = spherical([cosRadius, -sinRadius * cos(t), -sinRadius * sin(t)]);\n stream.point(point[0], point[1]);\n }\n}\n\n// Returns the signed angle of a cartesian point relative to [cosRadius, 0, 0].\nfunction circleRadius(cosRadius, point) {\n point = cartesian(point), point[0] -= cosRadius;\n cartesianNormalizeInPlace(point);\n var radius = acos(-point[1]);\n return ((-point[2] < 0 ? -radius : radius) + tau - epsilon) % tau;\n}\n\nexport default function() {\n var center = constant([0, 0]),\n radius = constant(90),\n precision = constant(2),\n ring,\n rotate,\n stream = {point: point};\n\n function point(x, y) {\n ring.push(x = rotate(x, y));\n x[0] *= degrees, x[1] *= degrees;\n }\n\n function circle() {\n var c = center.apply(this, arguments),\n r = radius.apply(this, arguments) * radians,\n p = precision.apply(this, arguments) * radians;\n ring = [];\n rotate = rotateRadians(-c[0] * radians, -c[1] * radians, 0).invert;\n circleStream(stream, r, p, 1);\n c = {type: \"Polygon\", coordinates: [ring]};\n ring = rotate = null;\n return c;\n }\n\n circle.center = function(_) {\n return arguments.length ? (center = typeof _ === \"function\" ? _ : constant([+_[0], +_[1]]), circle) : center;\n };\n\n circle.radius = function(_) {\n return arguments.length ? (radius = typeof _ === \"function\" ? _ : constant(+_), circle) : radius;\n };\n\n circle.precision = function(_) {\n return arguments.length ? (precision = typeof _ === \"function\" ? _ : constant(+_), circle) : precision;\n };\n\n return circle;\n}\n", "import noop from \"../noop.js\";\n\nexport default function() {\n var lines = [],\n line;\n return {\n point: function(x, y, m) {\n line.push([x, y, m]);\n },\n lineStart: function() {\n lines.push(line = []);\n },\n lineEnd: noop,\n rejoin: function() {\n if (lines.length > 1) lines.push(lines.pop().concat(lines.shift()));\n },\n result: function() {\n var result = lines;\n lines = [];\n line = null;\n return result;\n }\n };\n}\n", "import {abs, epsilon} from \"./math.js\";\n\nexport default function(a, b) {\n return abs(a[0] - b[0]) < epsilon && abs(a[1] - b[1]) < epsilon;\n}\n", "import pointEqual from \"../pointEqual.js\";\nimport {epsilon} from \"../math.js\";\n\nfunction Intersection(point, points, other, entry) {\n this.x = point;\n this.z = points;\n this.o = other; // another intersection\n this.e = entry; // is an entry?\n this.v = false; // visited\n this.n = this.p = null; // next & previous\n}\n\n// A generalized polygon clipping algorithm: given a polygon that has been cut\n// into its visible line segments, and rejoins the segments by interpolating\n// along the clip edge.\nexport default function(segments, compareIntersection, startInside, interpolate, stream) {\n var subject = [],\n clip = [],\n i,\n n;\n\n segments.forEach(function(segment) {\n if ((n = segment.length - 1) <= 0) return;\n var n, p0 = segment[0], p1 = segment[n], x;\n\n if (pointEqual(p0, p1)) {\n if (!p0[2] && !p1[2]) {\n stream.lineStart();\n for (i = 0; i < n; ++i) stream.point((p0 = segment[i])[0], p0[1]);\n stream.lineEnd();\n return;\n }\n // handle degenerate cases by moving the point\n p1[0] += 2 * epsilon;\n }\n\n subject.push(x = new Intersection(p0, segment, null, true));\n clip.push(x.o = new Intersection(p0, null, x, false));\n subject.push(x = new Intersection(p1, segment, null, false));\n clip.push(x.o = new Intersection(p1, null, x, true));\n });\n\n if (!subject.length) return;\n\n clip.sort(compareIntersection);\n link(subject);\n link(clip);\n\n for (i = 0, n = clip.length; i < n; ++i) {\n clip[i].e = startInside = !startInside;\n }\n\n var start = subject[0],\n points,\n point;\n\n while (1) {\n // Find first unvisited intersection.\n var current = start,\n isSubject = true;\n while (current.v) if ((current = current.n) === start) return;\n points = current.z;\n stream.lineStart();\n do {\n current.v = current.o.v = true;\n if (current.e) {\n if (isSubject) {\n for (i = 0, n = points.length; i < n; ++i) stream.point((point = points[i])[0], point[1]);\n } else {\n interpolate(current.x, current.n.x, 1, stream);\n }\n current = current.n;\n } else {\n if (isSubject) {\n points = current.p.z;\n for (i = points.length - 1; i >= 0; --i) stream.point((point = points[i])[0], point[1]);\n } else {\n interpolate(current.x, current.p.x, -1, stream);\n }\n current = current.p;\n }\n current = current.o;\n points = current.z;\n isSubject = !isSubject;\n } while (!current.v);\n stream.lineEnd();\n }\n}\n\nfunction link(array) {\n if (!(n = array.length)) return;\n var n,\n i = 0,\n a = array[0],\n b;\n while (++i < n) {\n a.n = b = array[i];\n b.p = a;\n a = b;\n }\n a.n = b = array[0];\n b.p = a;\n}\n", "import {Adder} from \"d3-array\";\nimport {cartesian, cartesianCross, cartesianNormalizeInPlace} from \"./cartesian.js\";\nimport {abs, asin, atan2, cos, epsilon, epsilon2, halfPi, pi, quarterPi, sign, sin, tau} from \"./math.js\";\n\nfunction longitude(point) {\n return abs(point[0]) <= pi ? point[0] : sign(point[0]) * ((abs(point[0]) + pi) % tau - pi);\n}\n\nexport default function(polygon, point) {\n var lambda = longitude(point),\n phi = point[1],\n sinPhi = sin(phi),\n normal = [sin(lambda), -cos(lambda), 0],\n angle = 0,\n winding = 0;\n\n var sum = new Adder();\n\n if (sinPhi === 1) phi = halfPi + epsilon;\n else if (sinPhi === -1) phi = -halfPi - epsilon;\n\n for (var i = 0, n = polygon.length; i < n; ++i) {\n if (!(m = (ring = polygon[i]).length)) continue;\n var ring,\n m,\n point0 = ring[m - 1],\n lambda0 = longitude(point0),\n phi0 = point0[1] / 2 + quarterPi,\n sinPhi0 = sin(phi0),\n cosPhi0 = cos(phi0);\n\n for (var j = 0; j < m; ++j, lambda0 = lambda1, sinPhi0 = sinPhi1, cosPhi0 = cosPhi1, point0 = point1) {\n var point1 = ring[j],\n lambda1 = longitude(point1),\n phi1 = point1[1] / 2 + quarterPi,\n sinPhi1 = sin(phi1),\n cosPhi1 = cos(phi1),\n delta = lambda1 - lambda0,\n sign = delta >= 0 ? 1 : -1,\n absDelta = sign * delta,\n antimeridian = absDelta > pi,\n k = sinPhi0 * sinPhi1;\n\n sum.add(atan2(k * sign * sin(absDelta), cosPhi0 * cosPhi1 + k * cos(absDelta)));\n angle += antimeridian ? delta + sign * tau : delta;\n\n // Are the longitudes either side of the point\u2019s meridian (lambda),\n // and are the latitudes smaller than the parallel (phi)?\n if (antimeridian ^ lambda0 >= lambda ^ lambda1 >= lambda) {\n var arc = cartesianCross(cartesian(point0), cartesian(point1));\n cartesianNormalizeInPlace(arc);\n var intersection = cartesianCross(normal, arc);\n cartesianNormalizeInPlace(intersection);\n var phiArc = (antimeridian ^ delta >= 0 ? -1 : 1) * asin(intersection[2]);\n if (phi > phiArc || phi === phiArc && (arc[0] || arc[1])) {\n winding += antimeridian ^ delta >= 0 ? 1 : -1;\n }\n }\n }\n }\n\n // First, determine whether the South pole is inside or outside:\n //\n // It is inside if:\n // * the polygon winds around it in a clockwise direction.\n // * the polygon does not (cumulatively) wind around it, but has a negative\n // (counter-clockwise) area.\n //\n // Second, count the (signed) number of times a segment crosses a lambda\n // from the point to the South pole. If it is zero, then the point is the\n // same side as the South pole.\n\n return (angle < -epsilon || angle < epsilon && sum < -epsilon2) ^ (winding & 1);\n}\n", "import clipBuffer from \"./buffer.js\";\nimport clipRejoin from \"./rejoin.js\";\nimport {epsilon, halfPi} from \"../math.js\";\nimport polygonContains from \"../polygonContains.js\";\nimport {merge} from \"d3-array\";\n\nexport default function(pointVisible, clipLine, interpolate, start) {\n return function(sink) {\n var line = clipLine(sink),\n ringBuffer = clipBuffer(),\n ringSink = clipLine(ringBuffer),\n polygonStarted = false,\n polygon,\n segments,\n ring;\n\n var clip = {\n point: point,\n lineStart: lineStart,\n lineEnd: lineEnd,\n polygonStart: function() {\n clip.point = pointRing;\n clip.lineStart = ringStart;\n clip.lineEnd = ringEnd;\n segments = [];\n polygon = [];\n },\n polygonEnd: function() {\n clip.point = point;\n clip.lineStart = lineStart;\n clip.lineEnd = lineEnd;\n segments = merge(segments);\n var startInside = polygonContains(polygon, start);\n if (segments.length) {\n if (!polygonStarted) sink.polygonStart(), polygonStarted = true;\n clipRejoin(segments, compareIntersection, startInside, interpolate, sink);\n } else if (startInside) {\n if (!polygonStarted) sink.polygonStart(), polygonStarted = true;\n sink.lineStart();\n interpolate(null, null, 1, sink);\n sink.lineEnd();\n }\n if (polygonStarted) sink.polygonEnd(), polygonStarted = false;\n segments = polygon = null;\n },\n sphere: function() {\n sink.polygonStart();\n sink.lineStart();\n interpolate(null, null, 1, sink);\n sink.lineEnd();\n sink.polygonEnd();\n }\n };\n\n function point(lambda, phi) {\n if (pointVisible(lambda, phi)) sink.point(lambda, phi);\n }\n\n function pointLine(lambda, phi) {\n line.point(lambda, phi);\n }\n\n function lineStart() {\n clip.point = pointLine;\n line.lineStart();\n }\n\n function lineEnd() {\n clip.point = point;\n line.lineEnd();\n }\n\n function pointRing(lambda, phi) {\n ring.push([lambda, phi]);\n ringSink.point(lambda, phi);\n }\n\n function ringStart() {\n ringSink.lineStart();\n ring = [];\n }\n\n function ringEnd() {\n pointRing(ring[0][0], ring[0][1]);\n ringSink.lineEnd();\n\n var clean = ringSink.clean(),\n ringSegments = ringBuffer.result(),\n i, n = ringSegments.length, m,\n segment,\n point;\n\n ring.pop();\n polygon.push(ring);\n ring = null;\n\n if (!n) return;\n\n // No intersections.\n if (clean & 1) {\n segment = ringSegments[0];\n if ((m = segment.length - 1) > 0) {\n if (!polygonStarted) sink.polygonStart(), polygonStarted = true;\n sink.lineStart();\n for (i = 0; i < m; ++i) sink.point((point = segment[i])[0], point[1]);\n sink.lineEnd();\n }\n return;\n }\n\n // Rejoin connected segments.\n // TODO reuse ringBuffer.rejoin()?\n if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift()));\n\n segments.push(ringSegments.filter(validSegment));\n }\n\n return clip;\n };\n}\n\nfunction validSegment(segment) {\n return segment.length > 1;\n}\n\n// Intersections are sorted along the clip edge. For both antimeridian cutting\n// and circle clipping, the same comparison is used.\nfunction compareIntersection(a, b) {\n return ((a = a.x)[0] < 0 ? a[1] - halfPi - epsilon : halfPi - a[1])\n - ((b = b.x)[0] < 0 ? b[1] - halfPi - epsilon : halfPi - b[1]);\n}\n", "import clip from \"./index.js\";\nimport {abs, atan, cos, epsilon, halfPi, pi, sin} from \"../math.js\";\n\nexport default clip(\n function() { return true; },\n clipAntimeridianLine,\n clipAntimeridianInterpolate,\n [-pi, -halfPi]\n);\n\n// Takes a line and cuts into visible segments. Return values: 0 - there were\n// intersections or the line was empty; 1 - no intersections; 2 - there were\n// intersections, and the first and last segments should be rejoined.\nfunction clipAntimeridianLine(stream) {\n var lambda0 = NaN,\n phi0 = NaN,\n sign0 = NaN,\n clean; // no intersections\n\n return {\n lineStart: function() {\n stream.lineStart();\n clean = 1;\n },\n point: function(lambda1, phi1) {\n var sign1 = lambda1 > 0 ? pi : -pi,\n delta = abs(lambda1 - lambda0);\n if (abs(delta - pi) < epsilon) { // line crosses a pole\n stream.point(lambda0, phi0 = (phi0 + phi1) / 2 > 0 ? halfPi : -halfPi);\n stream.point(sign0, phi0);\n stream.lineEnd();\n stream.lineStart();\n stream.point(sign1, phi0);\n stream.point(lambda1, phi0);\n clean = 0;\n } else if (sign0 !== sign1 && delta >= pi) { // line crosses antimeridian\n if (abs(lambda0 - sign0) < epsilon) lambda0 -= sign0 * epsilon; // handle degeneracies\n if (abs(lambda1 - sign1) < epsilon) lambda1 -= sign1 * epsilon;\n phi0 = clipAntimeridianIntersect(lambda0, phi0, lambda1, phi1);\n stream.point(sign0, phi0);\n stream.lineEnd();\n stream.lineStart();\n stream.point(sign1, phi0);\n clean = 0;\n }\n stream.point(lambda0 = lambda1, phi0 = phi1);\n sign0 = sign1;\n },\n lineEnd: function() {\n stream.lineEnd();\n lambda0 = phi0 = NaN;\n },\n clean: function() {\n return 2 - clean; // if intersections, rejoin first and last segments\n }\n };\n}\n\nfunction clipAntimeridianIntersect(lambda0, phi0, lambda1, phi1) {\n var cosPhi0,\n cosPhi1,\n sinLambda0Lambda1 = sin(lambda0 - lambda1);\n return abs(sinLambda0Lambda1) > epsilon\n ? atan((sin(phi0) * (cosPhi1 = cos(phi1)) * sin(lambda1)\n - sin(phi1) * (cosPhi0 = cos(phi0)) * sin(lambda0))\n / (cosPhi0 * cosPhi1 * sinLambda0Lambda1))\n : (phi0 + phi1) / 2;\n}\n\nfunction clipAntimeridianInterpolate(from, to, direction, stream) {\n var phi;\n if (from == null) {\n phi = direction * halfPi;\n stream.point(-pi, phi);\n stream.point(0, phi);\n stream.point(pi, phi);\n stream.point(pi, 0);\n stream.point(pi, -phi);\n stream.point(0, -phi);\n stream.point(-pi, -phi);\n stream.point(-pi, 0);\n stream.point(-pi, phi);\n } else if (abs(from[0] - to[0]) > epsilon) {\n var lambda = from[0] < to[0] ? pi : -pi;\n phi = direction * lambda / 2;\n stream.point(-lambda, phi);\n stream.point(0, phi);\n stream.point(lambda, phi);\n } else {\n stream.point(to[0], to[1]);\n }\n}\n", "import {cartesian, cartesianAddInPlace, cartesianCross, cartesianDot, cartesianScale, spherical} from \"../cartesian.js\";\nimport {circleStream} from \"../circle.js\";\nimport {abs, cos, epsilon, pi, radians, sqrt} from \"../math.js\";\nimport pointEqual from \"../pointEqual.js\";\nimport clip from \"./index.js\";\n\nexport default function(radius) {\n var cr = cos(radius),\n delta = 2 * radians,\n smallRadius = cr > 0,\n notHemisphere = abs(cr) > epsilon; // TODO optimise for this common case\n\n function interpolate(from, to, direction, stream) {\n circleStream(stream, radius, delta, direction, from, to);\n }\n\n function visible(lambda, phi) {\n return cos(lambda) * cos(phi) > cr;\n }\n\n // Takes a line and cuts into visible segments. Return values used for polygon\n // clipping: 0 - there were intersections or the line was empty; 1 - no\n // intersections 2 - there were intersections, and the first and last segments\n // should be rejoined.\n function clipLine(stream) {\n var point0, // previous point\n c0, // code for previous point\n v0, // visibility of previous point\n v00, // visibility of first point\n clean; // no intersections\n return {\n lineStart: function() {\n v00 = v0 = false;\n clean = 1;\n },\n point: function(lambda, phi) {\n var point1 = [lambda, phi],\n point2,\n v = visible(lambda, phi),\n c = smallRadius\n ? v ? 0 : code(lambda, phi)\n : v ? code(lambda + (lambda < 0 ? pi : -pi), phi) : 0;\n if (!point0 && (v00 = v0 = v)) stream.lineStart();\n if (v !== v0) {\n point2 = intersect(point0, point1);\n if (!point2 || pointEqual(point0, point2) || pointEqual(point1, point2))\n point1[2] = 1;\n }\n if (v !== v0) {\n clean = 0;\n if (v) {\n // outside going in\n stream.lineStart();\n point2 = intersect(point1, point0);\n stream.point(point2[0], point2[1]);\n } else {\n // inside going out\n point2 = intersect(point0, point1);\n stream.point(point2[0], point2[1], 2);\n stream.lineEnd();\n }\n point0 = point2;\n } else if (notHemisphere && point0 && smallRadius ^ v) {\n var t;\n // If the codes for two points are different, or are both zero,\n // and there this segment intersects with the small circle.\n if (!(c & c0) && (t = intersect(point1, point0, true))) {\n clean = 0;\n if (smallRadius) {\n stream.lineStart();\n stream.point(t[0][0], t[0][1]);\n stream.point(t[1][0], t[1][1]);\n stream.lineEnd();\n } else {\n stream.point(t[1][0], t[1][1]);\n stream.lineEnd();\n stream.lineStart();\n stream.point(t[0][0], t[0][1], 3);\n }\n }\n }\n if (v && (!point0 || !pointEqual(point0, point1))) {\n stream.point(point1[0], point1[1]);\n }\n point0 = point1, v0 = v, c0 = c;\n },\n lineEnd: function() {\n if (v0) stream.lineEnd();\n point0 = null;\n },\n // Rejoin first and last segments if there were intersections and the first\n // and last points were visible.\n clean: function() {\n return clean | ((v00 && v0) << 1);\n }\n };\n }\n\n // Intersects the great circle between a and b with the clip circle.\n function intersect(a, b, two) {\n var pa = cartesian(a),\n pb = cartesian(b);\n\n // We have two planes, n1.p = d1 and n2.p = d2.\n // Find intersection line p(t) = c1 n1 + c2 n2 + t (n1 \u2A2F n2).\n var n1 = [1, 0, 0], // normal\n n2 = cartesianCross(pa, pb),\n n2n2 = cartesianDot(n2, n2),\n n1n2 = n2[0], // cartesianDot(n1, n2),\n determinant = n2n2 - n1n2 * n1n2;\n\n // Two polar points.\n if (!determinant) return !two && a;\n\n var c1 = cr * n2n2 / determinant,\n c2 = -cr * n1n2 / determinant,\n n1xn2 = cartesianCross(n1, n2),\n A = cartesianScale(n1, c1),\n B = cartesianScale(n2, c2);\n cartesianAddInPlace(A, B);\n\n // Solve |p(t)|^2 = 1.\n var u = n1xn2,\n w = cartesianDot(A, u),\n uu = cartesianDot(u, u),\n t2 = w * w - uu * (cartesianDot(A, A) - 1);\n\n if (t2 < 0) return;\n\n var t = sqrt(t2),\n q = cartesianScale(u, (-w - t) / uu);\n cartesianAddInPlace(q, A);\n q = spherical(q);\n\n if (!two) return q;\n\n // Two intersection points.\n var lambda0 = a[0],\n lambda1 = b[0],\n phi0 = a[1],\n phi1 = b[1],\n z;\n\n if (lambda1 < lambda0) z = lambda0, lambda0 = lambda1, lambda1 = z;\n\n var delta = lambda1 - lambda0,\n polar = abs(delta - pi) < epsilon,\n meridian = polar || delta < epsilon;\n\n if (!polar && phi1 < phi0) z = phi0, phi0 = phi1, phi1 = z;\n\n // Check that the first point is between a and b.\n if (meridian\n ? polar\n ? phi0 + phi1 > 0 ^ q[1] < (abs(q[0] - lambda0) < epsilon ? phi0 : phi1)\n : phi0 <= q[1] && q[1] <= phi1\n : delta > pi ^ (lambda0 <= q[0] && q[0] <= lambda1)) {\n var q1 = cartesianScale(u, (-w + t) / uu);\n cartesianAddInPlace(q1, A);\n return [q, spherical(q1)];\n }\n }\n\n // Generates a 4-bit vector representing the location of a point relative to\n // the small circle's bounding box.\n function code(lambda, phi) {\n var r = smallRadius ? radius : pi - radius,\n code = 0;\n if (lambda < -r) code |= 1; // left\n else if (lambda > r) code |= 2; // right\n if (phi < -r) code |= 4; // below\n else if (phi > r) code |= 8; // above\n return code;\n }\n\n return clip(visible, clipLine, interpolate, smallRadius ? [0, -radius] : [-pi, radius - pi]);\n}\n", "export default function(a, b, x0, y0, x1, y1) {\n var ax = a[0],\n ay = a[1],\n bx = b[0],\n by = b[1],\n t0 = 0,\n t1 = 1,\n dx = bx - ax,\n dy = by - ay,\n r;\n\n r = x0 - ax;\n if (!dx && r > 0) return;\n r /= dx;\n if (dx < 0) {\n if (r < t0) return;\n if (r < t1) t1 = r;\n } else if (dx > 0) {\n if (r > t1) return;\n if (r > t0) t0 = r;\n }\n\n r = x1 - ax;\n if (!dx && r < 0) return;\n r /= dx;\n if (dx < 0) {\n if (r > t1) return;\n if (r > t0) t0 = r;\n } else if (dx > 0) {\n if (r < t0) return;\n if (r < t1) t1 = r;\n }\n\n r = y0 - ay;\n if (!dy && r > 0) return;\n r /= dy;\n if (dy < 0) {\n if (r < t0) return;\n if (r < t1) t1 = r;\n } else if (dy > 0) {\n if (r > t1) return;\n if (r > t0) t0 = r;\n }\n\n r = y1 - ay;\n if (!dy && r < 0) return;\n r /= dy;\n if (dy < 0) {\n if (r > t1) return;\n if (r > t0) t0 = r;\n } else if (dy > 0) {\n if (r < t0) return;\n if (r < t1) t1 = r;\n }\n\n if (t0 > 0) a[0] = ax + t0 * dx, a[1] = ay + t0 * dy;\n if (t1 < 1) b[0] = ax + t1 * dx, b[1] = ay + t1 * dy;\n return true;\n}\n", "import {abs, epsilon} from \"../math.js\";\nimport clipBuffer from \"./buffer.js\";\nimport clipLine from \"./line.js\";\nimport clipRejoin from \"./rejoin.js\";\nimport {merge} from \"d3-array\";\n\nvar clipMax = 1e9, clipMin = -clipMax;\n\n// TODO Use d3-polygon\u2019s polygonContains here for the ring check?\n// TODO Eliminate duplicate buffering in clipBuffer and polygon.push?\n\nexport default function clipRectangle(x0, y0, x1, y1) {\n\n function visible(x, y) {\n return x0 <= x && x <= x1 && y0 <= y && y <= y1;\n }\n\n function interpolate(from, to, direction, stream) {\n var a = 0, a1 = 0;\n if (from == null\n || (a = corner(from, direction)) !== (a1 = corner(to, direction))\n || comparePoint(from, to) < 0 ^ direction > 0) {\n do stream.point(a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0);\n while ((a = (a + direction + 4) % 4) !== a1);\n } else {\n stream.point(to[0], to[1]);\n }\n }\n\n function corner(p, direction) {\n return abs(p[0] - x0) < epsilon ? direction > 0 ? 0 : 3\n : abs(p[0] - x1) < epsilon ? direction > 0 ? 2 : 1\n : abs(p[1] - y0) < epsilon ? direction > 0 ? 1 : 0\n : direction > 0 ? 3 : 2; // abs(p[1] - y1) < epsilon\n }\n\n function compareIntersection(a, b) {\n return comparePoint(a.x, b.x);\n }\n\n function comparePoint(a, b) {\n var ca = corner(a, 1),\n cb = corner(b, 1);\n return ca !== cb ? ca - cb\n : ca === 0 ? b[1] - a[1]\n : ca === 1 ? a[0] - b[0]\n : ca === 2 ? a[1] - b[1]\n : b[0] - a[0];\n }\n\n return function(stream) {\n var activeStream = stream,\n bufferStream = clipBuffer(),\n segments,\n polygon,\n ring,\n x__, y__, v__, // first point\n x_, y_, v_, // previous point\n first,\n clean;\n\n var clipStream = {\n point: point,\n lineStart: lineStart,\n lineEnd: lineEnd,\n polygonStart: polygonStart,\n polygonEnd: polygonEnd\n };\n\n function point(x, y) {\n if (visible(x, y)) activeStream.point(x, y);\n }\n\n function polygonInside() {\n var winding = 0;\n\n for (var i = 0, n = polygon.length; i < n; ++i) {\n for (var ring = polygon[i], j = 1, m = ring.length, point = ring[0], a0, a1, b0 = point[0], b1 = point[1]; j < m; ++j) {\n a0 = b0, a1 = b1, point = ring[j], b0 = point[0], b1 = point[1];\n if (a1 <= y1) { if (b1 > y1 && (b0 - a0) * (y1 - a1) > (b1 - a1) * (x0 - a0)) ++winding; }\n else { if (b1 <= y1 && (b0 - a0) * (y1 - a1) < (b1 - a1) * (x0 - a0)) --winding; }\n }\n }\n\n return winding;\n }\n\n // Buffer geometry within a polygon and then clip it en masse.\n function polygonStart() {\n activeStream = bufferStream, segments = [], polygon = [], clean = true;\n }\n\n function polygonEnd() {\n var startInside = polygonInside(),\n cleanInside = clean && startInside,\n visible = (segments = merge(segments)).length;\n if (cleanInside || visible) {\n stream.polygonStart();\n if (cleanInside) {\n stream.lineStart();\n interpolate(null, null, 1, stream);\n stream.lineEnd();\n }\n if (visible) {\n clipRejoin(segments, compareIntersection, startInside, interpolate, stream);\n }\n stream.polygonEnd();\n }\n activeStream = stream, segments = polygon = ring = null;\n }\n\n function lineStart() {\n clipStream.point = linePoint;\n if (polygon) polygon.push(ring = []);\n first = true;\n v_ = false;\n x_ = y_ = NaN;\n }\n\n // TODO rather than special-case polygons, simply handle them separately.\n // Ideally, coincident intersection points should be jittered to avoid\n // clipping issues.\n function lineEnd() {\n if (segments) {\n linePoint(x__, y__);\n if (v__ && v_) bufferStream.rejoin();\n segments.push(bufferStream.result());\n }\n clipStream.point = point;\n if (v_) activeStream.lineEnd();\n }\n\n function linePoint(x, y) {\n var v = visible(x, y);\n if (polygon) ring.push([x, y]);\n if (first) {\n x__ = x, y__ = y, v__ = v;\n first = false;\n if (v) {\n activeStream.lineStart();\n activeStream.point(x, y);\n }\n } else {\n if (v && v_) activeStream.point(x, y);\n else {\n var a = [x_ = Math.max(clipMin, Math.min(clipMax, x_)), y_ = Math.max(clipMin, Math.min(clipMax, y_))],\n b = [x = Math.max(clipMin, Math.min(clipMax, x)), y = Math.max(clipMin, Math.min(clipMax, y))];\n if (clipLine(a, b, x0, y0, x1, y1)) {\n if (!v_) {\n activeStream.lineStart();\n activeStream.point(a[0], a[1]);\n }\n activeStream.point(b[0], b[1]);\n if (!v) activeStream.lineEnd();\n clean = false;\n } else if (v) {\n activeStream.lineStart();\n activeStream.point(x, y);\n clean = false;\n }\n }\n }\n x_ = x, y_ = y, v_ = v;\n }\n\n return clipStream;\n };\n}\n", "import clipRectangle from \"./rectangle.js\";\n\nexport default function() {\n var x0 = 0,\n y0 = 0,\n x1 = 960,\n y1 = 500,\n cache,\n cacheStream,\n clip;\n\n return clip = {\n stream: function(stream) {\n return cache && cacheStream === stream ? cache : cache = clipRectangle(x0, y0, x1, y1)(cacheStream = stream);\n },\n extent: function(_) {\n return arguments.length ? (x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1], cache = cacheStream = null, clip) : [[x0, y0], [x1, y1]];\n }\n };\n}\n", "import {Adder} from \"d3-array\";\nimport {abs, atan2, cos, radians, sin, sqrt} from \"./math.js\";\nimport noop from \"./noop.js\";\nimport stream from \"./stream.js\";\n\nvar lengthSum,\n lambda0,\n sinPhi0,\n cosPhi0;\n\nvar lengthStream = {\n sphere: noop,\n point: noop,\n lineStart: lengthLineStart,\n lineEnd: noop,\n polygonStart: noop,\n polygonEnd: noop\n};\n\nfunction lengthLineStart() {\n lengthStream.point = lengthPointFirst;\n lengthStream.lineEnd = lengthLineEnd;\n}\n\nfunction lengthLineEnd() {\n lengthStream.point = lengthStream.lineEnd = noop;\n}\n\nfunction lengthPointFirst(lambda, phi) {\n lambda *= radians, phi *= radians;\n lambda0 = lambda, sinPhi0 = sin(phi), cosPhi0 = cos(phi);\n lengthStream.point = lengthPoint;\n}\n\nfunction lengthPoint(lambda, phi) {\n lambda *= radians, phi *= radians;\n var sinPhi = sin(phi),\n cosPhi = cos(phi),\n delta = abs(lambda - lambda0),\n cosDelta = cos(delta),\n sinDelta = sin(delta),\n x = cosPhi * sinDelta,\n y = cosPhi0 * sinPhi - sinPhi0 * cosPhi * cosDelta,\n z = sinPhi0 * sinPhi + cosPhi0 * cosPhi * cosDelta;\n lengthSum.add(atan2(sqrt(x * x + y * y), z));\n lambda0 = lambda, sinPhi0 = sinPhi, cosPhi0 = cosPhi;\n}\n\nexport default function(object) {\n lengthSum = new Adder();\n stream(object, lengthStream);\n return +lengthSum;\n}\n", "import length from \"./length.js\";\n\nvar coordinates = [null, null],\n object = {type: \"LineString\", coordinates: coordinates};\n\nexport default function(a, b) {\n coordinates[0] = a;\n coordinates[1] = b;\n return length(object);\n}\n", "import {default as polygonContains} from \"./polygonContains.js\";\nimport {default as distance} from \"./distance.js\";\nimport {epsilon2, radians} from \"./math.js\";\n\nvar containsObjectType = {\n Feature: function(object, point) {\n return containsGeometry(object.geometry, point);\n },\n FeatureCollection: function(object, point) {\n var features = object.features, i = -1, n = features.length;\n while (++i < n) if (containsGeometry(features[i].geometry, point)) return true;\n return false;\n }\n};\n\nvar containsGeometryType = {\n Sphere: function() {\n return true;\n },\n Point: function(object, point) {\n return containsPoint(object.coordinates, point);\n },\n MultiPoint: function(object, point) {\n var coordinates = object.coordinates, i = -1, n = coordinates.length;\n while (++i < n) if (containsPoint(coordinates[i], point)) return true;\n return false;\n },\n LineString: function(object, point) {\n return containsLine(object.coordinates, point);\n },\n MultiLineString: function(object, point) {\n var coordinates = object.coordinates, i = -1, n = coordinates.length;\n while (++i < n) if (containsLine(coordinates[i], point)) return true;\n return false;\n },\n Polygon: function(object, point) {\n return containsPolygon(object.coordinates, point);\n },\n MultiPolygon: function(object, point) {\n var coordinates = object.coordinates, i = -1, n = coordinates.length;\n while (++i < n) if (containsPolygon(coordinates[i], point)) return true;\n return false;\n },\n GeometryCollection: function(object, point) {\n var geometries = object.geometries, i = -1, n = geometries.length;\n while (++i < n) if (containsGeometry(geometries[i], point)) return true;\n return false;\n }\n};\n\nfunction containsGeometry(geometry, point) {\n return geometry && containsGeometryType.hasOwnProperty(geometry.type)\n ? containsGeometryType[geometry.type](geometry, point)\n : false;\n}\n\nfunction containsPoint(coordinates, point) {\n return distance(coordinates, point) === 0;\n}\n\nfunction containsLine(coordinates, point) {\n var ao, bo, ab;\n for (var i = 0, n = coordinates.length; i < n; i++) {\n bo = distance(coordinates[i], point);\n if (bo === 0) return true;\n if (i > 0) {\n ab = distance(coordinates[i], coordinates[i - 1]);\n if (\n ab > 0 &&\n ao <= ab &&\n bo <= ab &&\n (ao + bo - ab) * (1 - Math.pow((ao - bo) / ab, 2)) < epsilon2 * ab\n )\n return true;\n }\n ao = bo;\n }\n return false;\n}\n\nfunction containsPolygon(coordinates, point) {\n return !!polygonContains(coordinates.map(ringRadians), pointRadians(point));\n}\n\nfunction ringRadians(ring) {\n return ring = ring.map(pointRadians), ring.pop(), ring;\n}\n\nfunction pointRadians(point) {\n return [point[0] * radians, point[1] * radians];\n}\n\nexport default function(object, point) {\n return (object && containsObjectType.hasOwnProperty(object.type)\n ? containsObjectType[object.type]\n : containsGeometry)(object, point);\n}\n", "import {range} from \"d3-array\";\nimport {abs, ceil, epsilon} from \"./math.js\";\n\nfunction graticuleX(y0, y1, dy) {\n var y = range(y0, y1 - epsilon, dy).concat(y1);\n return function(x) { return y.map(function(y) { return [x, y]; }); };\n}\n\nfunction graticuleY(x0, x1, dx) {\n var x = range(x0, x1 - epsilon, dx).concat(x1);\n return function(y) { return x.map(function(x) { return [x, y]; }); };\n}\n\nexport default function graticule() {\n var x1, x0, X1, X0,\n y1, y0, Y1, Y0,\n dx = 10, dy = dx, DX = 90, DY = 360,\n x, y, X, Y,\n precision = 2.5;\n\n function graticule() {\n return {type: \"MultiLineString\", coordinates: lines()};\n }\n\n function lines() {\n return range(ceil(X0 / DX) * DX, X1, DX).map(X)\n .concat(range(ceil(Y0 / DY) * DY, Y1, DY).map(Y))\n .concat(range(ceil(x0 / dx) * dx, x1, dx).filter(function(x) { return abs(x % DX) > epsilon; }).map(x))\n .concat(range(ceil(y0 / dy) * dy, y1, dy).filter(function(y) { return abs(y % DY) > epsilon; }).map(y));\n }\n\n graticule.lines = function() {\n return lines().map(function(coordinates) { return {type: \"LineString\", coordinates: coordinates}; });\n };\n\n graticule.outline = function() {\n return {\n type: \"Polygon\",\n coordinates: [\n X(X0).concat(\n Y(Y1).slice(1),\n X(X1).reverse().slice(1),\n Y(Y0).reverse().slice(1))\n ]\n };\n };\n\n graticule.extent = function(_) {\n if (!arguments.length) return graticule.extentMinor();\n return graticule.extentMajor(_).extentMinor(_);\n };\n\n graticule.extentMajor = function(_) {\n if (!arguments.length) return [[X0, Y0], [X1, Y1]];\n X0 = +_[0][0], X1 = +_[1][0];\n Y0 = +_[0][1], Y1 = +_[1][1];\n if (X0 > X1) _ = X0, X0 = X1, X1 = _;\n if (Y0 > Y1) _ = Y0, Y0 = Y1, Y1 = _;\n return graticule.precision(precision);\n };\n\n graticule.extentMinor = function(_) {\n if (!arguments.length) return [[x0, y0], [x1, y1]];\n x0 = +_[0][0], x1 = +_[1][0];\n y0 = +_[0][1], y1 = +_[1][1];\n if (x0 > x1) _ = x0, x0 = x1, x1 = _;\n if (y0 > y1) _ = y0, y0 = y1, y1 = _;\n return graticule.precision(precision);\n };\n\n graticule.step = function(_) {\n if (!arguments.length) return graticule.stepMinor();\n return graticule.stepMajor(_).stepMinor(_);\n };\n\n graticule.stepMajor = function(_) {\n if (!arguments.length) return [DX, DY];\n DX = +_[0], DY = +_[1];\n return graticule;\n };\n\n graticule.stepMinor = function(_) {\n if (!arguments.length) return [dx, dy];\n dx = +_[0], dy = +_[1];\n return graticule;\n };\n\n graticule.precision = function(_) {\n if (!arguments.length) return precision;\n precision = +_;\n x = graticuleX(y0, y1, 90);\n y = graticuleY(x0, x1, precision);\n X = graticuleX(Y0, Y1, 90);\n Y = graticuleY(X0, X1, precision);\n return graticule;\n };\n\n return graticule\n .extentMajor([[-180, -90 + epsilon], [180, 90 - epsilon]])\n .extentMinor([[-180, -80 - epsilon], [180, 80 + epsilon]]);\n}\n\nexport function graticule10() {\n return graticule()();\n}\n", "import {asin, atan2, cos, degrees, haversin, radians, sin, sqrt} from \"./math.js\";\n\nexport default function(a, b) {\n var x0 = a[0] * radians,\n y0 = a[1] * radians,\n x1 = b[0] * radians,\n y1 = b[1] * radians,\n cy0 = cos(y0),\n sy0 = sin(y0),\n cy1 = cos(y1),\n sy1 = sin(y1),\n kx0 = cy0 * cos(x0),\n ky0 = cy0 * sin(x0),\n kx1 = cy1 * cos(x1),\n ky1 = cy1 * sin(x1),\n d = 2 * asin(sqrt(haversin(y1 - y0) + cy0 * cy1 * haversin(x1 - x0))),\n k = sin(d);\n\n var interpolate = d ? function(t) {\n var B = sin(t *= d) / k,\n A = sin(d - t) / k,\n x = A * kx0 + B * kx1,\n y = A * ky0 + B * ky1,\n z = A * sy0 + B * sy1;\n return [\n atan2(y, x) * degrees,\n atan2(z, sqrt(x * x + y * y)) * degrees\n ];\n } : function() {\n return [x0 * degrees, y0 * degrees];\n };\n\n interpolate.distance = d;\n\n return interpolate;\n}\n", "export default x => x;\n", "import {Adder} from \"d3-array\";\nimport {abs} from \"../math.js\";\nimport noop from \"../noop.js\";\n\nvar areaSum = new Adder(),\n areaRingSum = new Adder(),\n x00,\n y00,\n x0,\n y0;\n\nvar areaStream = {\n point: noop,\n lineStart: noop,\n lineEnd: noop,\n polygonStart: function() {\n areaStream.lineStart = areaRingStart;\n areaStream.lineEnd = areaRingEnd;\n },\n polygonEnd: function() {\n areaStream.lineStart = areaStream.lineEnd = areaStream.point = noop;\n areaSum.add(abs(areaRingSum));\n areaRingSum = new Adder();\n },\n result: function() {\n var area = areaSum / 2;\n areaSum = new Adder();\n return area;\n }\n};\n\nfunction areaRingStart() {\n areaStream.point = areaPointFirst;\n}\n\nfunction areaPointFirst(x, y) {\n areaStream.point = areaPoint;\n x00 = x0 = x, y00 = y0 = y;\n}\n\nfunction areaPoint(x, y) {\n areaRingSum.add(y0 * x - x0 * y);\n x0 = x, y0 = y;\n}\n\nfunction areaRingEnd() {\n areaPoint(x00, y00);\n}\n\nexport default areaStream;\n", "import noop from \"../noop.js\";\n\nvar x0 = Infinity,\n y0 = x0,\n x1 = -x0,\n y1 = x1;\n\nvar boundsStream = {\n point: boundsPoint,\n lineStart: noop,\n lineEnd: noop,\n polygonStart: noop,\n polygonEnd: noop,\n result: function() {\n var bounds = [[x0, y0], [x1, y1]];\n x1 = y1 = -(y0 = x0 = Infinity);\n return bounds;\n }\n};\n\nfunction boundsPoint(x, y) {\n if (x < x0) x0 = x;\n if (x > x1) x1 = x;\n if (y < y0) y0 = y;\n if (y > y1) y1 = y;\n}\n\nexport default boundsStream;\n", "import {sqrt} from \"../math.js\";\n\n// TODO Enforce positive area for exterior, negative area for interior?\n\nvar X0 = 0,\n Y0 = 0,\n Z0 = 0,\n X1 = 0,\n Y1 = 0,\n Z1 = 0,\n X2 = 0,\n Y2 = 0,\n Z2 = 0,\n x00,\n y00,\n x0,\n y0;\n\nvar centroidStream = {\n point: centroidPoint,\n lineStart: centroidLineStart,\n lineEnd: centroidLineEnd,\n polygonStart: function() {\n centroidStream.lineStart = centroidRingStart;\n centroidStream.lineEnd = centroidRingEnd;\n },\n polygonEnd: function() {\n centroidStream.point = centroidPoint;\n centroidStream.lineStart = centroidLineStart;\n centroidStream.lineEnd = centroidLineEnd;\n },\n result: function() {\n var centroid = Z2 ? [X2 / Z2, Y2 / Z2]\n : Z1 ? [X1 / Z1, Y1 / Z1]\n : Z0 ? [X0 / Z0, Y0 / Z0]\n : [NaN, NaN];\n X0 = Y0 = Z0 =\n X1 = Y1 = Z1 =\n X2 = Y2 = Z2 = 0;\n return centroid;\n }\n};\n\nfunction centroidPoint(x, y) {\n X0 += x;\n Y0 += y;\n ++Z0;\n}\n\nfunction centroidLineStart() {\n centroidStream.point = centroidPointFirstLine;\n}\n\nfunction centroidPointFirstLine(x, y) {\n centroidStream.point = centroidPointLine;\n centroidPoint(x0 = x, y0 = y);\n}\n\nfunction centroidPointLine(x, y) {\n var dx = x - x0, dy = y - y0, z = sqrt(dx * dx + dy * dy);\n X1 += z * (x0 + x) / 2;\n Y1 += z * (y0 + y) / 2;\n Z1 += z;\n centroidPoint(x0 = x, y0 = y);\n}\n\nfunction centroidLineEnd() {\n centroidStream.point = centroidPoint;\n}\n\nfunction centroidRingStart() {\n centroidStream.point = centroidPointFirstRing;\n}\n\nfunction centroidRingEnd() {\n centroidPointRing(x00, y00);\n}\n\nfunction centroidPointFirstRing(x, y) {\n centroidStream.point = centroidPointRing;\n centroidPoint(x00 = x0 = x, y00 = y0 = y);\n}\n\nfunction centroidPointRing(x, y) {\n var dx = x - x0,\n dy = y - y0,\n z = sqrt(dx * dx + dy * dy);\n\n X1 += z * (x0 + x) / 2;\n Y1 += z * (y0 + y) / 2;\n Z1 += z;\n\n z = y0 * x - x0 * y;\n X2 += z * (x0 + x);\n Y2 += z * (y0 + y);\n Z2 += z * 3;\n centroidPoint(x0 = x, y0 = y);\n}\n\nexport default centroidStream;\n", "import {tau} from \"../math.js\";\nimport noop from \"../noop.js\";\n\nexport default function PathContext(context) {\n this._context = context;\n}\n\nPathContext.prototype = {\n _radius: 4.5,\n pointRadius: function(_) {\n return this._radius = _, this;\n },\n polygonStart: function() {\n this._line = 0;\n },\n polygonEnd: function() {\n this._line = NaN;\n },\n lineStart: function() {\n this._point = 0;\n },\n lineEnd: function() {\n if (this._line === 0) this._context.closePath();\n this._point = NaN;\n },\n point: function(x, y) {\n switch (this._point) {\n case 0: {\n this._context.moveTo(x, y);\n this._point = 1;\n break;\n }\n case 1: {\n this._context.lineTo(x, y);\n break;\n }\n default: {\n this._context.moveTo(x + this._radius, y);\n this._context.arc(x, y, this._radius, 0, tau);\n break;\n }\n }\n },\n result: noop\n};\n", "import {Adder} from \"d3-array\";\nimport {sqrt} from \"../math.js\";\nimport noop from \"../noop.js\";\n\nvar lengthSum = new Adder(),\n lengthRing,\n x00,\n y00,\n x0,\n y0;\n\nvar lengthStream = {\n point: noop,\n lineStart: function() {\n lengthStream.point = lengthPointFirst;\n },\n lineEnd: function() {\n if (lengthRing) lengthPoint(x00, y00);\n lengthStream.point = noop;\n },\n polygonStart: function() {\n lengthRing = true;\n },\n polygonEnd: function() {\n lengthRing = null;\n },\n result: function() {\n var length = +lengthSum;\n lengthSum = new Adder();\n return length;\n }\n};\n\nfunction lengthPointFirst(x, y) {\n lengthStream.point = lengthPoint;\n x00 = x0 = x, y00 = y0 = y;\n}\n\nfunction lengthPoint(x, y) {\n x0 -= x, y0 -= y;\n lengthSum.add(sqrt(x0 * x0 + y0 * y0));\n x0 = x, y0 = y;\n}\n\nexport default lengthStream;\n", "// Simple caching for constant-radius points.\nlet cacheDigits, cacheAppend, cacheRadius, cacheCircle;\n\nexport default class PathString {\n constructor(digits) {\n this._append = digits == null ? append : appendRound(digits);\n this._radius = 4.5;\n this._ = \"\";\n }\n pointRadius(_) {\n this._radius = +_;\n return this;\n }\n polygonStart() {\n this._line = 0;\n }\n polygonEnd() {\n this._line = NaN;\n }\n lineStart() {\n this._point = 0;\n }\n lineEnd() {\n if (this._line === 0) this._ += \"Z\";\n this._point = NaN;\n }\n point(x, y) {\n switch (this._point) {\n case 0: {\n this._append`M${x},${y}`;\n this._point = 1;\n break;\n }\n case 1: {\n this._append`L${x},${y}`;\n break;\n }\n default: {\n this._append`M${x},${y}`;\n if (this._radius !== cacheRadius || this._append !== cacheAppend) {\n const r = this._radius;\n const s = this._;\n this._ = \"\"; // stash the old string so we can cache the circle path fragment\n this._append`m0,${r}a${r},${r} 0 1,1 0,${-2 * r}a${r},${r} 0 1,1 0,${2 * r}z`;\n cacheRadius = r;\n cacheAppend = this._append;\n cacheCircle = this._;\n this._ = s;\n }\n this._ += cacheCircle;\n break;\n }\n }\n }\n result() {\n const result = this._;\n this._ = \"\";\n return result.length ? result : null;\n }\n}\n\nfunction append(strings) {\n let i = 1;\n this._ += strings[0];\n for (const j = strings.length; i < j; ++i) {\n this._ += arguments[i] + strings[i];\n }\n}\n\nfunction appendRound(digits) {\n const d = Math.floor(digits);\n if (!(d >= 0)) throw new RangeError(`invalid digits: ${digits}`);\n if (d > 15) return append;\n if (d !== cacheDigits) {\n const k = 10 ** d;\n cacheDigits = d;\n cacheAppend = function append(strings) {\n let i = 1;\n this._ += strings[0];\n for (const j = strings.length; i < j; ++i) {\n this._ += Math.round(arguments[i] * k) / k + strings[i];\n }\n };\n }\n return cacheAppend;\n}\n", "import identity from \"../identity.js\";\nimport stream from \"../stream.js\";\nimport pathArea from \"./area.js\";\nimport pathBounds from \"./bounds.js\";\nimport pathCentroid from \"./centroid.js\";\nimport PathContext from \"./context.js\";\nimport pathMeasure from \"./measure.js\";\nimport PathString from \"./string.js\";\n\nexport default function(projection, context) {\n let digits = 3,\n pointRadius = 4.5,\n projectionStream,\n contextStream;\n\n function path(object) {\n if (object) {\n if (typeof pointRadius === \"function\") contextStream.pointRadius(+pointRadius.apply(this, arguments));\n stream(object, projectionStream(contextStream));\n }\n return contextStream.result();\n }\n\n path.area = function(object) {\n stream(object, projectionStream(pathArea));\n return pathArea.result();\n };\n\n path.measure = function(object) {\n stream(object, projectionStream(pathMeasure));\n return pathMeasure.result();\n };\n\n path.bounds = function(object) {\n stream(object, projectionStream(pathBounds));\n return pathBounds.result();\n };\n\n path.centroid = function(object) {\n stream(object, projectionStream(pathCentroid));\n return pathCentroid.result();\n };\n\n path.projection = function(_) {\n if (!arguments.length) return projection;\n projectionStream = _ == null ? (projection = null, identity) : (projection = _).stream;\n return path;\n };\n\n path.context = function(_) {\n if (!arguments.length) return context;\n contextStream = _ == null ? (context = null, new PathString(digits)) : new PathContext(context = _);\n if (typeof pointRadius !== \"function\") contextStream.pointRadius(pointRadius);\n return path;\n };\n\n path.pointRadius = function(_) {\n if (!arguments.length) return pointRadius;\n pointRadius = typeof _ === \"function\" ? _ : (contextStream.pointRadius(+_), +_);\n return path;\n };\n\n path.digits = function(_) {\n if (!arguments.length) return digits;\n if (_ == null) digits = null;\n else {\n const d = Math.floor(_);\n if (!(d >= 0)) throw new RangeError(`invalid digits: ${_}`);\n digits = d;\n }\n if (context === null) contextStream = new PathString(digits);\n return path;\n };\n\n return path.projection(projection).digits(digits).context(context);\n}\n", "export default function(methods) {\n return {\n stream: transformer(methods)\n };\n}\n\nexport function transformer(methods) {\n return function(stream) {\n var s = new TransformStream;\n for (var key in methods) s[key] = methods[key];\n s.stream = stream;\n return s;\n };\n}\n\nfunction TransformStream() {}\n\nTransformStream.prototype = {\n constructor: TransformStream,\n point: function(x, y) { this.stream.point(x, y); },\n sphere: function() { this.stream.sphere(); },\n lineStart: function() { this.stream.lineStart(); },\n lineEnd: function() { this.stream.lineEnd(); },\n polygonStart: function() { this.stream.polygonStart(); },\n polygonEnd: function() { this.stream.polygonEnd(); }\n};\n", "import {default as geoStream} from \"../stream.js\";\nimport boundsStream from \"../path/bounds.js\";\n\nfunction fit(projection, fitBounds, object) {\n var clip = projection.clipExtent && projection.clipExtent();\n projection.scale(150).translate([0, 0]);\n if (clip != null) projection.clipExtent(null);\n geoStream(object, projection.stream(boundsStream));\n fitBounds(boundsStream.result());\n if (clip != null) projection.clipExtent(clip);\n return projection;\n}\n\nexport function fitExtent(projection, extent, object) {\n return fit(projection, function(b) {\n var w = extent[1][0] - extent[0][0],\n h = extent[1][1] - extent[0][1],\n k = Math.min(w / (b[1][0] - b[0][0]), h / (b[1][1] - b[0][1])),\n x = +extent[0][0] + (w - k * (b[1][0] + b[0][0])) / 2,\n y = +extent[0][1] + (h - k * (b[1][1] + b[0][1])) / 2;\n projection.scale(150 * k).translate([x, y]);\n }, object);\n}\n\nexport function fitSize(projection, size, object) {\n return fitExtent(projection, [[0, 0], size], object);\n}\n\nexport function fitWidth(projection, width, object) {\n return fit(projection, function(b) {\n var w = +width,\n k = w / (b[1][0] - b[0][0]),\n x = (w - k * (b[1][0] + b[0][0])) / 2,\n y = -k * b[0][1];\n projection.scale(150 * k).translate([x, y]);\n }, object);\n}\n\nexport function fitHeight(projection, height, object) {\n return fit(projection, function(b) {\n var h = +height,\n k = h / (b[1][1] - b[0][1]),\n x = -k * b[0][0],\n y = (h - k * (b[1][1] + b[0][1])) / 2;\n projection.scale(150 * k).translate([x, y]);\n }, object);\n}\n", "import {cartesian} from \"../cartesian.js\";\nimport {abs, asin, atan2, cos, epsilon, radians, sqrt} from \"../math.js\";\nimport {transformer} from \"../transform.js\";\n\nvar maxDepth = 16, // maximum depth of subdivision\n cosMinDistance = cos(30 * radians); // cos(minimum angular distance)\n\nexport default function(project, delta2) {\n return +delta2 ? resample(project, delta2) : resampleNone(project);\n}\n\nfunction resampleNone(project) {\n return transformer({\n point: function(x, y) {\n x = project(x, y);\n this.stream.point(x[0], x[1]);\n }\n });\n}\n\nfunction resample(project, delta2) {\n\n function resampleLineTo(x0, y0, lambda0, a0, b0, c0, x1, y1, lambda1, a1, b1, c1, depth, stream) {\n var dx = x1 - x0,\n dy = y1 - y0,\n d2 = dx * dx + dy * dy;\n if (d2 > 4 * delta2 && depth--) {\n var a = a0 + a1,\n b = b0 + b1,\n c = c0 + c1,\n m = sqrt(a * a + b * b + c * c),\n phi2 = asin(c /= m),\n lambda2 = abs(abs(c) - 1) < epsilon || abs(lambda0 - lambda1) < epsilon ? (lambda0 + lambda1) / 2 : atan2(b, a),\n p = project(lambda2, phi2),\n x2 = p[0],\n y2 = p[1],\n dx2 = x2 - x0,\n dy2 = y2 - y0,\n dz = dy * dx2 - dx * dy2;\n if (dz * dz / d2 > delta2 // perpendicular projected distance\n || abs((dx * dx2 + dy * dy2) / d2 - 0.5) > 0.3 // midpoint close to an end\n || a0 * a1 + b0 * b1 + c0 * c1 < cosMinDistance) { // angular distance\n resampleLineTo(x0, y0, lambda0, a0, b0, c0, x2, y2, lambda2, a /= m, b /= m, c, depth, stream);\n stream.point(x2, y2);\n resampleLineTo(x2, y2, lambda2, a, b, c, x1, y1, lambda1, a1, b1, c1, depth, stream);\n }\n }\n }\n return function(stream) {\n var lambda00, x00, y00, a00, b00, c00, // first point\n lambda0, x0, y0, a0, b0, c0; // previous point\n\n var resampleStream = {\n point: point,\n lineStart: lineStart,\n lineEnd: lineEnd,\n polygonStart: function() { stream.polygonStart(); resampleStream.lineStart = ringStart; },\n polygonEnd: function() { stream.polygonEnd(); resampleStream.lineStart = lineStart; }\n };\n\n function point(x, y) {\n x = project(x, y);\n stream.point(x[0], x[1]);\n }\n\n function lineStart() {\n x0 = NaN;\n resampleStream.point = linePoint;\n stream.lineStart();\n }\n\n function linePoint(lambda, phi) {\n var c = cartesian([lambda, phi]), p = project(lambda, phi);\n resampleLineTo(x0, y0, lambda0, a0, b0, c0, x0 = p[0], y0 = p[1], lambda0 = lambda, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream);\n stream.point(x0, y0);\n }\n\n function lineEnd() {\n resampleStream.point = point;\n stream.lineEnd();\n }\n\n function ringStart() {\n lineStart();\n resampleStream.point = ringPoint;\n resampleStream.lineEnd = ringEnd;\n }\n\n function ringPoint(lambda, phi) {\n linePoint(lambda00 = lambda, phi), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0;\n resampleStream.point = linePoint;\n }\n\n function ringEnd() {\n resampleLineTo(x0, y0, lambda0, a0, b0, c0, x00, y00, lambda00, a00, b00, c00, maxDepth, stream);\n resampleStream.lineEnd = lineEnd;\n lineEnd();\n }\n\n return resampleStream;\n };\n}\n", "import clipAntimeridian from \"../clip/antimeridian.js\";\nimport clipCircle from \"../clip/circle.js\";\nimport clipRectangle from \"../clip/rectangle.js\";\nimport compose from \"../compose.js\";\nimport identity from \"../identity.js\";\nimport {cos, degrees, radians, sin, sqrt} from \"../math.js\";\nimport {rotateRadians} from \"../rotation.js\";\nimport {transformer} from \"../transform.js\";\nimport {fitExtent, fitSize, fitWidth, fitHeight} from \"./fit.js\";\nimport resample from \"./resample.js\";\n\nvar transformRadians = transformer({\n point: function(x, y) {\n this.stream.point(x * radians, y * radians);\n }\n});\n\nfunction transformRotate(rotate) {\n return transformer({\n point: function(x, y) {\n var r = rotate(x, y);\n return this.stream.point(r[0], r[1]);\n }\n });\n}\n\nfunction scaleTranslate(k, dx, dy, sx, sy) {\n function transform(x, y) {\n x *= sx; y *= sy;\n return [dx + k * x, dy - k * y];\n }\n transform.invert = function(x, y) {\n return [(x - dx) / k * sx, (dy - y) / k * sy];\n };\n return transform;\n}\n\nfunction scaleTranslateRotate(k, dx, dy, sx, sy, alpha) {\n if (!alpha) return scaleTranslate(k, dx, dy, sx, sy);\n var cosAlpha = cos(alpha),\n sinAlpha = sin(alpha),\n a = cosAlpha * k,\n b = sinAlpha * k,\n ai = cosAlpha / k,\n bi = sinAlpha / k,\n ci = (sinAlpha * dy - cosAlpha * dx) / k,\n fi = (sinAlpha * dx + cosAlpha * dy) / k;\n function transform(x, y) {\n x *= sx; y *= sy;\n return [a * x - b * y + dx, dy - b * x - a * y];\n }\n transform.invert = function(x, y) {\n return [sx * (ai * x - bi * y + ci), sy * (fi - bi * x - ai * y)];\n };\n return transform;\n}\n\nexport default function projection(project) {\n return projectionMutator(function() { return project; })();\n}\n\nexport function projectionMutator(projectAt) {\n var project,\n k = 150, // scale\n x = 480, y = 250, // translate\n lambda = 0, phi = 0, // center\n deltaLambda = 0, deltaPhi = 0, deltaGamma = 0, rotate, // pre-rotate\n alpha = 0, // post-rotate angle\n sx = 1, // reflectX\n sy = 1, // reflectX\n theta = null, preclip = clipAntimeridian, // pre-clip angle\n x0 = null, y0, x1, y1, postclip = identity, // post-clip extent\n delta2 = 0.5, // precision\n projectResample,\n projectTransform,\n projectRotateTransform,\n cache,\n cacheStream;\n\n function projection(point) {\n return projectRotateTransform(point[0] * radians, point[1] * radians);\n }\n\n function invert(point) {\n point = projectRotateTransform.invert(point[0], point[1]);\n return point && [point[0] * degrees, point[1] * degrees];\n }\n\n projection.stream = function(stream) {\n return cache && cacheStream === stream ? cache : cache = transformRadians(transformRotate(rotate)(preclip(projectResample(postclip(cacheStream = stream)))));\n };\n\n projection.preclip = function(_) {\n return arguments.length ? (preclip = _, theta = undefined, reset()) : preclip;\n };\n\n projection.postclip = function(_) {\n return arguments.length ? (postclip = _, x0 = y0 = x1 = y1 = null, reset()) : postclip;\n };\n\n projection.clipAngle = function(_) {\n return arguments.length ? (preclip = +_ ? clipCircle(theta = _ * radians) : (theta = null, clipAntimeridian), reset()) : theta * degrees;\n };\n\n projection.clipExtent = function(_) {\n return arguments.length ? (postclip = _ == null ? (x0 = y0 = x1 = y1 = null, identity) : clipRectangle(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]), reset()) : x0 == null ? null : [[x0, y0], [x1, y1]];\n };\n\n projection.scale = function(_) {\n return arguments.length ? (k = +_, recenter()) : k;\n };\n\n projection.translate = function(_) {\n return arguments.length ? (x = +_[0], y = +_[1], recenter()) : [x, y];\n };\n\n projection.center = function(_) {\n return arguments.length ? (lambda = _[0] % 360 * radians, phi = _[1] % 360 * radians, recenter()) : [lambda * degrees, phi * degrees];\n };\n\n projection.rotate = function(_) {\n return arguments.length ? (deltaLambda = _[0] % 360 * radians, deltaPhi = _[1] % 360 * radians, deltaGamma = _.length > 2 ? _[2] % 360 * radians : 0, recenter()) : [deltaLambda * degrees, deltaPhi * degrees, deltaGamma * degrees];\n };\n\n projection.angle = function(_) {\n return arguments.length ? (alpha = _ % 360 * radians, recenter()) : alpha * degrees;\n };\n\n projection.reflectX = function(_) {\n return arguments.length ? (sx = _ ? -1 : 1, recenter()) : sx < 0;\n };\n\n projection.reflectY = function(_) {\n return arguments.length ? (sy = _ ? -1 : 1, recenter()) : sy < 0;\n };\n\n projection.precision = function(_) {\n return arguments.length ? (projectResample = resample(projectTransform, delta2 = _ * _), reset()) : sqrt(delta2);\n };\n\n projection.fitExtent = function(extent, object) {\n return fitExtent(projection, extent, object);\n };\n\n projection.fitSize = function(size, object) {\n return fitSize(projection, size, object);\n };\n\n projection.fitWidth = function(width, object) {\n return fitWidth(projection, width, object);\n };\n\n projection.fitHeight = function(height, object) {\n return fitHeight(projection, height, object);\n };\n\n function recenter() {\n var center = scaleTranslateRotate(k, 0, 0, sx, sy, alpha).apply(null, project(lambda, phi)),\n transform = scaleTranslateRotate(k, x - center[0], y - center[1], sx, sy, alpha);\n rotate = rotateRadians(deltaLambda, deltaPhi, deltaGamma);\n projectTransform = compose(project, transform);\n projectRotateTransform = compose(rotate, projectTransform);\n projectResample = resample(projectTransform, delta2);\n return reset();\n }\n\n function reset() {\n cache = cacheStream = null;\n return projection;\n }\n\n return function() {\n project = projectAt.apply(this, arguments);\n projection.invert = project.invert && invert;\n return recenter();\n };\n}\n", "import {abs, asin, atan2, cos, epsilon, pi, sign, sin, sqrt} from \"../math.js\";\nimport {conicProjection} from \"./conic.js\";\nimport {cylindricalEqualAreaRaw} from \"./cylindricalEqualArea.js\";\n\nexport function conicEqualAreaRaw(y0, y1) {\n var sy0 = sin(y0), n = (sy0 + sin(y1)) / 2;\n\n // Are the parallels symmetrical around the Equator?\n if (abs(n) < epsilon) return cylindricalEqualAreaRaw(y0);\n\n var c = 1 + sy0 * (2 * n - sy0), r0 = sqrt(c) / n;\n\n function project(x, y) {\n var r = sqrt(c - 2 * n * sin(y)) / n;\n return [r * sin(x *= n), r0 - r * cos(x)];\n }\n\n project.invert = function(x, y) {\n var r0y = r0 - y,\n l = atan2(x, abs(r0y)) * sign(r0y);\n if (r0y * n < 0)\n l -= pi * sign(x) * sign(r0y);\n return [l / n, asin((c - (x * x + r0y * r0y) * n * n) / (2 * n))];\n };\n\n return project;\n}\n\nexport default function() {\n return conicProjection(conicEqualAreaRaw)\n .scale(155.424)\n .center([0, 33.6442]);\n}\n", "import conicEqualArea from \"./conicEqualArea.js\";\n\nexport default function() {\n return conicEqualArea()\n .parallels([29.5, 45.5])\n .scale(1070)\n .translate([480, 250])\n .rotate([96, 0])\n .center([-0.6, 38.7]);\n}\n", "import {epsilon} from \"../math.js\";\nimport albers from \"./albers.js\";\nimport conicEqualArea from \"./conicEqualArea.js\";\nimport {fitExtent, fitSize, fitWidth, fitHeight} from \"./fit.js\";\n\n// The projections must have mutually exclusive clip regions on the sphere,\n// as this will avoid emitting interleaving lines and polygons.\nfunction multiplex(streams) {\n var n = streams.length;\n return {\n point: function(x, y) { var i = -1; while (++i < n) streams[i].point(x, y); },\n sphere: function() { var i = -1; while (++i < n) streams[i].sphere(); },\n lineStart: function() { var i = -1; while (++i < n) streams[i].lineStart(); },\n lineEnd: function() { var i = -1; while (++i < n) streams[i].lineEnd(); },\n polygonStart: function() { var i = -1; while (++i < n) streams[i].polygonStart(); },\n polygonEnd: function() { var i = -1; while (++i < n) streams[i].polygonEnd(); }\n };\n}\n\n// A composite projection for the United States, configured by default for\n// 960\u00D7500. The projection also works quite well at 960\u00D7600 if you change the\n// scale to 1285 and adjust the translate accordingly. The set of standard\n// parallels for each region comes from USGS, which is published here:\n// http://egsc.usgs.gov/isb/pubs/MapProjections/projections.html#albers\nexport default function() {\n var cache,\n cacheStream,\n lower48 = albers(), lower48Point,\n alaska = conicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]), alaskaPoint, // EPSG:3338\n hawaii = conicEqualArea().rotate([157, 0]).center([-3, 19.9]).parallels([8, 18]), hawaiiPoint, // ESRI:102007\n point, pointStream = {point: function(x, y) { point = [x, y]; }};\n\n function albersUsa(coordinates) {\n var x = coordinates[0], y = coordinates[1];\n return point = null,\n (lower48Point.point(x, y), point)\n || (alaskaPoint.point(x, y), point)\n || (hawaiiPoint.point(x, y), point);\n }\n\n albersUsa.invert = function(coordinates) {\n var k = lower48.scale(),\n t = lower48.translate(),\n x = (coordinates[0] - t[0]) / k,\n y = (coordinates[1] - t[1]) / k;\n return (y >= 0.120 && y < 0.234 && x >= -0.425 && x < -0.214 ? alaska\n : y >= 0.166 && y < 0.234 && x >= -0.214 && x < -0.115 ? hawaii\n : lower48).invert(coordinates);\n };\n\n albersUsa.stream = function(stream) {\n return cache && cacheStream === stream ? cache : cache = multiplex([lower48.stream(cacheStream = stream), alaska.stream(stream), hawaii.stream(stream)]);\n };\n\n albersUsa.precision = function(_) {\n if (!arguments.length) return lower48.precision();\n lower48.precision(_), alaska.precision(_), hawaii.precision(_);\n return reset();\n };\n\n albersUsa.scale = function(_) {\n if (!arguments.length) return lower48.scale();\n lower48.scale(_), alaska.scale(_ * 0.35), hawaii.scale(_);\n return albersUsa.translate(lower48.translate());\n };\n\n albersUsa.translate = function(_) {\n if (!arguments.length) return lower48.translate();\n var k = lower48.scale(), x = +_[0], y = +_[1];\n\n lower48Point = lower48\n .translate(_)\n .clipExtent([[x - 0.455 * k, y - 0.238 * k], [x + 0.455 * k, y + 0.238 * k]])\n .stream(pointStream);\n\n alaskaPoint = alaska\n .translate([x - 0.307 * k, y + 0.201 * k])\n .clipExtent([[x - 0.425 * k + epsilon, y + 0.120 * k + epsilon], [x - 0.214 * k - epsilon, y + 0.234 * k - epsilon]])\n .stream(pointStream);\n\n hawaiiPoint = hawaii\n .translate([x - 0.205 * k, y + 0.212 * k])\n .clipExtent([[x - 0.214 * k + epsilon, y + 0.166 * k + epsilon], [x - 0.115 * k - epsilon, y + 0.234 * k - epsilon]])\n .stream(pointStream);\n\n return reset();\n };\n\n albersUsa.fitExtent = function(extent, object) {\n return fitExtent(albersUsa, extent, object);\n };\n\n albersUsa.fitSize = function(size, object) {\n return fitSize(albersUsa, size, object);\n };\n\n albersUsa.fitWidth = function(width, object) {\n return fitWidth(albersUsa, width, object);\n };\n\n albersUsa.fitHeight = function(height, object) {\n return fitHeight(albersUsa, height, object);\n };\n\n function reset() {\n cache = cacheStream = null;\n return albersUsa;\n }\n\n return albersUsa.scale(1070);\n}\n", "import {asin, atan2, cos, sin, sqrt} from \"../math.js\";\n\nexport function azimuthalRaw(scale) {\n return function(x, y) {\n var cx = cos(x),\n cy = cos(y),\n k = scale(cx * cy);\n if (k === Infinity) return [2, 0];\n return [\n k * cy * sin(x),\n k * sin(y)\n ];\n }\n}\n\nexport function azimuthalInvert(angle) {\n return function(x, y) {\n var z = sqrt(x * x + y * y),\n c = angle(z),\n sc = sin(c),\n cc = cos(c);\n return [\n atan2(x * sc, z * cc),\n asin(z && y * sc / z)\n ];\n }\n}\n", "import {asin, sqrt} from \"../math.js\";\nimport {azimuthalRaw, azimuthalInvert} from \"./azimuthal.js\";\nimport projection from \"./index.js\";\n\nexport var azimuthalEqualAreaRaw = azimuthalRaw(function(cxcy) {\n return sqrt(2 / (1 + cxcy));\n});\n\nazimuthalEqualAreaRaw.invert = azimuthalInvert(function(z) {\n return 2 * asin(z / 2);\n});\n\nexport default function() {\n return projection(azimuthalEqualAreaRaw)\n .scale(124.75)\n .clipAngle(180 - 1e-3);\n}\n", "import {acos, sin} from \"../math.js\";\nimport {azimuthalRaw, azimuthalInvert} from \"./azimuthal.js\";\nimport projection from \"./index.js\";\n\nexport var azimuthalEquidistantRaw = azimuthalRaw(function(c) {\n return (c = acos(c)) && c / sin(c);\n});\n\nazimuthalEquidistantRaw.invert = azimuthalInvert(function(z) {\n return z;\n});\n\nexport default function() {\n return projection(azimuthalEquidistantRaw)\n .scale(79.4188)\n .clipAngle(180 - 1e-3);\n}\n", "import {atan, exp, halfPi, log, pi, tan, tau} from \"../math.js\";\nimport rotation from \"../rotation.js\";\nimport projection from \"./index.js\";\n\nexport function mercatorRaw(lambda, phi) {\n return [lambda, log(tan((halfPi + phi) / 2))];\n}\n\nmercatorRaw.invert = function(x, y) {\n return [x, 2 * atan(exp(y)) - halfPi];\n};\n\nexport default function() {\n return mercatorProjection(mercatorRaw)\n .scale(961 / tau);\n}\n\nexport function mercatorProjection(project) {\n var m = projection(project),\n center = m.center,\n scale = m.scale,\n translate = m.translate,\n clipExtent = m.clipExtent,\n x0 = null, y0, x1, y1; // clip extent\n\n m.scale = function(_) {\n return arguments.length ? (scale(_), reclip()) : scale();\n };\n\n m.translate = function(_) {\n return arguments.length ? (translate(_), reclip()) : translate();\n };\n\n m.center = function(_) {\n return arguments.length ? (center(_), reclip()) : center();\n };\n\n m.clipExtent = function(_) {\n return arguments.length ? ((_ == null ? x0 = y0 = x1 = y1 = null : (x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1])), reclip()) : x0 == null ? null : [[x0, y0], [x1, y1]];\n };\n\n function reclip() {\n var k = pi * scale(),\n t = m(rotation(m.rotate()).invert([0, 0]));\n return clipExtent(x0 == null\n ? [[t[0] - k, t[1] - k], [t[0] + k, t[1] + k]] : project === mercatorRaw\n ? [[Math.max(t[0] - k, x0), y0], [Math.min(t[0] + k, x1), y1]]\n : [[x0, Math.max(t[1] - k, y0)], [x1, Math.min(t[1] + k, y1)]]);\n }\n\n return reclip();\n}\n", "import {abs, atan, atan2, cos, epsilon, halfPi, log, pi, pow, sign, sin, sqrt, tan} from \"../math.js\";\nimport {conicProjection} from \"./conic.js\";\nimport {mercatorRaw} from \"./mercator.js\";\n\nfunction tany(y) {\n return tan((halfPi + y) / 2);\n}\n\nexport function conicConformalRaw(y0, y1) {\n var cy0 = cos(y0),\n n = y0 === y1 ? sin(y0) : log(cy0 / cos(y1)) / log(tany(y1) / tany(y0)),\n f = cy0 * pow(tany(y0), n) / n;\n\n if (!n) return mercatorRaw;\n\n function project(x, y) {\n if (f > 0) { if (y < -halfPi + epsilon) y = -halfPi + epsilon; }\n else { if (y > halfPi - epsilon) y = halfPi - epsilon; }\n var r = f / pow(tany(y), n);\n return [r * sin(n * x), f - r * cos(n * x)];\n }\n\n project.invert = function(x, y) {\n var fy = f - y, r = sign(n) * sqrt(x * x + fy * fy),\n l = atan2(x, abs(fy)) * sign(fy);\n if (fy * n < 0)\n l -= pi * sign(x) * sign(fy);\n return [l / n, 2 * atan(pow(f / r, 1 / n)) - halfPi];\n };\n\n return project;\n}\n\nexport default function() {\n return conicProjection(conicConformalRaw)\n .scale(109.5)\n .parallels([30, 30]);\n}\n", "import projection from \"./index.js\";\n\nexport function equirectangularRaw(lambda, phi) {\n return [lambda, phi];\n}\n\nequirectangularRaw.invert = equirectangularRaw;\n\nexport default function() {\n return projection(equirectangularRaw)\n .scale(152.63);\n}\n", "import {abs, atan2, cos, epsilon, pi, sign, sin, sqrt} from \"../math.js\";\nimport {conicProjection} from \"./conic.js\";\nimport {equirectangularRaw} from \"./equirectangular.js\";\n\nexport function conicEquidistantRaw(y0, y1) {\n var cy0 = cos(y0),\n n = y0 === y1 ? sin(y0) : (cy0 - cos(y1)) / (y1 - y0),\n g = cy0 / n + y0;\n\n if (abs(n) < epsilon) return equirectangularRaw;\n\n function project(x, y) {\n var gy = g - y, nx = n * x;\n return [gy * sin(nx), g - gy * cos(nx)];\n }\n\n project.invert = function(x, y) {\n var gy = g - y,\n l = atan2(x, abs(gy)) * sign(gy);\n if (gy * n < 0)\n l -= pi * sign(x) * sign(gy);\n return [l / n, g - sign(n) * sqrt(x * x + gy * gy)];\n };\n\n return project;\n}\n\nexport default function() {\n return conicProjection(conicEquidistantRaw)\n .scale(131.154)\n .center([0, 13.9389]);\n}\n", "import projection from \"./index.js\";\nimport {abs, asin, cos, epsilon2, sin, sqrt} from \"../math.js\";\n\nvar A1 = 1.340264,\n A2 = -0.081106,\n A3 = 0.000893,\n A4 = 0.003796,\n M = sqrt(3) / 2,\n iterations = 12;\n\nexport function equalEarthRaw(lambda, phi) {\n var l = asin(M * sin(phi)), l2 = l * l, l6 = l2 * l2 * l2;\n return [\n lambda * cos(l) / (M * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2))),\n l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2))\n ];\n}\n\nequalEarthRaw.invert = function(x, y) {\n var l = y, l2 = l * l, l6 = l2 * l2 * l2;\n for (var i = 0, delta, fy, fpy; i < iterations; ++i) {\n fy = l * (A1 + A2 * l2 + l6 * (A3 + A4 * l2)) - y;\n fpy = A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2);\n l -= delta = fy / fpy, l2 = l * l, l6 = l2 * l2 * l2;\n if (abs(delta) < epsilon2) break;\n }\n return [\n M * x * (A1 + 3 * A2 * l2 + l6 * (7 * A3 + 9 * A4 * l2)) / cos(l),\n asin(sin(l) / M)\n ];\n};\n\nexport default function() {\n return projection(equalEarthRaw)\n .scale(177.158);\n}\n", "import {atan, cos, sin} from \"../math.js\";\nimport {azimuthalInvert} from \"./azimuthal.js\";\nimport projection from \"./index.js\";\n\nexport function gnomonicRaw(x, y) {\n var cy = cos(y), k = cos(x) * cy;\n return [cy * sin(x) / k, sin(y) / k];\n}\n\ngnomonicRaw.invert = azimuthalInvert(atan);\n\nexport default function() {\n return projection(gnomonicRaw)\n .scale(144.049)\n .clipAngle(60);\n}\n", "import clipRectangle from \"../clip/rectangle.js\";\nimport identity from \"../identity.js\";\nimport {transformer} from \"../transform.js\";\nimport {fitExtent, fitSize, fitWidth, fitHeight} from \"./fit.js\";\nimport {cos, degrees, radians, sin} from \"../math.js\";\n\nexport default function() {\n var k = 1, tx = 0, ty = 0, sx = 1, sy = 1, // scale, translate and reflect\n alpha = 0, ca, sa, // angle\n x0 = null, y0, x1, y1, // clip extent\n kx = 1, ky = 1,\n transform = transformer({\n point: function(x, y) {\n var p = projection([x, y])\n this.stream.point(p[0], p[1]);\n }\n }),\n postclip = identity,\n cache,\n cacheStream;\n\n function reset() {\n kx = k * sx;\n ky = k * sy;\n cache = cacheStream = null;\n return projection;\n }\n\n function projection (p) {\n var x = p[0] * kx, y = p[1] * ky;\n if (alpha) {\n var t = y * ca - x * sa;\n x = x * ca + y * sa;\n y = t;\n } \n return [x + tx, y + ty];\n }\n projection.invert = function(p) {\n var x = p[0] - tx, y = p[1] - ty;\n if (alpha) {\n var t = y * ca + x * sa;\n x = x * ca - y * sa;\n y = t;\n }\n return [x / kx, y / ky];\n };\n projection.stream = function(stream) {\n return cache && cacheStream === stream ? cache : cache = transform(postclip(cacheStream = stream));\n };\n projection.postclip = function(_) {\n return arguments.length ? (postclip = _, x0 = y0 = x1 = y1 = null, reset()) : postclip;\n };\n projection.clipExtent = function(_) {\n return arguments.length ? (postclip = _ == null ? (x0 = y0 = x1 = y1 = null, identity) : clipRectangle(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]), reset()) : x0 == null ? null : [[x0, y0], [x1, y1]];\n };\n projection.scale = function(_) {\n return arguments.length ? (k = +_, reset()) : k;\n };\n projection.translate = function(_) {\n return arguments.length ? (tx = +_[0], ty = +_[1], reset()) : [tx, ty];\n }\n projection.angle = function(_) {\n return arguments.length ? (alpha = _ % 360 * radians, sa = sin(alpha), ca = cos(alpha), reset()) : alpha * degrees;\n };\n projection.reflectX = function(_) {\n return arguments.length ? (sx = _ ? -1 : 1, reset()) : sx < 0;\n };\n projection.reflectY = function(_) {\n return arguments.length ? (sy = _ ? -1 : 1, reset()) : sy < 0;\n };\n projection.fitExtent = function(extent, object) {\n return fitExtent(projection, extent, object);\n };\n projection.fitSize = function(size, object) {\n return fitSize(projection, size, object);\n };\n projection.fitWidth = function(width, object) {\n return fitWidth(projection, width, object);\n };\n projection.fitHeight = function(height, object) {\n return fitHeight(projection, height, object);\n };\n\n return projection;\n}\n", "import projection from \"./index.js\";\nimport {abs, epsilon} from \"../math.js\";\n\nexport function naturalEarth1Raw(lambda, phi) {\n var phi2 = phi * phi, phi4 = phi2 * phi2;\n return [\n lambda * (0.8707 - 0.131979 * phi2 + phi4 * (-0.013791 + phi4 * (0.003971 * phi2 - 0.001529 * phi4))),\n phi * (1.007226 + phi2 * (0.015085 + phi4 * (-0.044475 + 0.028874 * phi2 - 0.005916 * phi4)))\n ];\n}\n\nnaturalEarth1Raw.invert = function(x, y) {\n var phi = y, i = 25, delta;\n do {\n var phi2 = phi * phi, phi4 = phi2 * phi2;\n phi -= delta = (phi * (1.007226 + phi2 * (0.015085 + phi4 * (-0.044475 + 0.028874 * phi2 - 0.005916 * phi4))) - y) /\n (1.007226 + phi2 * (0.015085 * 3 + phi4 * (-0.044475 * 7 + 0.028874 * 9 * phi2 - 0.005916 * 11 * phi4)));\n } while (abs(delta) > epsilon && --i > 0);\n return [\n x / (0.8707 + (phi2 = phi * phi) * (-0.131979 + phi2 * (-0.013791 + phi2 * phi2 * phi2 * (0.003971 - 0.001529 * phi2)))),\n phi\n ];\n};\n\nexport default function() {\n return projection(naturalEarth1Raw)\n .scale(175.295);\n}\n", "import {asin, cos, epsilon, sin} from \"../math.js\";\nimport {azimuthalInvert} from \"./azimuthal.js\";\nimport projection from \"./index.js\";\n\nexport function orthographicRaw(x, y) {\n return [cos(y) * sin(x), sin(y)];\n}\n\northographicRaw.invert = azimuthalInvert(asin);\n\nexport default function() {\n return projection(orthographicRaw)\n .scale(249.5)\n .clipAngle(90 + epsilon);\n}\n", "import {atan, cos, sin} from \"../math.js\";\nimport {azimuthalInvert} from \"./azimuthal.js\";\nimport projection from \"./index.js\";\n\nexport function stereographicRaw(x, y) {\n var cy = cos(y), k = 1 + cos(x) * cy;\n return [cy * sin(x) / k, sin(y) / k];\n}\n\nstereographicRaw.invert = azimuthalInvert(function(z) {\n return 2 * atan(z);\n});\n\nexport default function() {\n return projection(stereographicRaw)\n .scale(250)\n .clipAngle(142);\n}\n", "import {atan, exp, halfPi, log, tan} from \"../math.js\";\nimport {mercatorProjection} from \"./mercator.js\";\n\nexport function transverseMercatorRaw(lambda, phi) {\n return [log(tan((halfPi + phi) / 2)), -lambda];\n}\n\ntransverseMercatorRaw.invert = function(x, y) {\n return [-y, 2 * atan(exp(x)) - halfPi];\n};\n\nexport default function() {\n var m = mercatorProjection(transverseMercatorRaw),\n center = m.center,\n rotate = m.rotate;\n\n m.center = function(_) {\n return arguments.length ? center([-_[1], _[0]]) : (_ = center(), [_[1], -_[0]]);\n };\n\n m.rotate = function(_) {\n return arguments.length ? rotate([_[0], _[1], _.length > 2 ? _[2] + 90 : 90]) : (_ = rotate(), [_[0], _[1], _[2] - 90]);\n };\n\n return rotate([0, 0, 90])\n .scale(159.155);\n}\n", "export {default as geoArea} from \"./area.js\";\nexport {default as geoBounds} from \"./bounds.js\";\nexport {default as geoCentroid} from \"./centroid.js\";\nexport {default as geoCircle} from \"./circle.js\";\nexport {default as geoClipAntimeridian} from \"./clip/antimeridian.js\";\nexport {default as geoClipCircle} from \"./clip/circle.js\";\nexport {default as geoClipExtent} from \"./clip/extent.js\"; // DEPRECATED! Use d3.geoIdentity().clipExtent(\u2026).\nexport {default as geoClipRectangle} from \"./clip/rectangle.js\";\nexport {default as geoContains} from \"./contains.js\";\nexport {default as geoDistance} from \"./distance.js\";\nexport {default as geoGraticule, graticule10 as geoGraticule10} from \"./graticule.js\";\nexport {default as geoInterpolate} from \"./interpolate.js\";\nexport {default as geoLength} from \"./length.js\";\nexport {default as geoPath} from \"./path/index.js\";\nexport {default as geoAlbers} from \"./projection/albers.js\";\nexport {default as geoAlbersUsa} from \"./projection/albersUsa.js\";\nexport {default as geoAzimuthalEqualArea, azimuthalEqualAreaRaw as geoAzimuthalEqualAreaRaw} from \"./projection/azimuthalEqualArea.js\";\nexport {default as geoAzimuthalEquidistant, azimuthalEquidistantRaw as geoAzimuthalEquidistantRaw} from \"./projection/azimuthalEquidistant.js\";\nexport {default as geoConicConformal, conicConformalRaw as geoConicConformalRaw} from \"./projection/conicConformal.js\";\nexport {default as geoConicEqualArea, conicEqualAreaRaw as geoConicEqualAreaRaw} from \"./projection/conicEqualArea.js\";\nexport {default as geoConicEquidistant, conicEquidistantRaw as geoConicEquidistantRaw} from \"./projection/conicEquidistant.js\";\nexport {default as geoEqualEarth, equalEarthRaw as geoEqualEarthRaw} from \"./projection/equalEarth.js\";\nexport {default as geoEquirectangular, equirectangularRaw as geoEquirectangularRaw} from \"./projection/equirectangular.js\";\nexport {default as geoGnomonic, gnomonicRaw as geoGnomonicRaw} from \"./projection/gnomonic.js\";\nexport {default as geoIdentity} from \"./projection/identity.js\";\nexport {default as geoProjection, projectionMutator as geoProjectionMutator} from \"./projection/index.js\";\nexport {default as geoMercator, mercatorRaw as geoMercatorRaw} from \"./projection/mercator.js\";\nexport {default as geoNaturalEarth1, naturalEarth1Raw as geoNaturalEarth1Raw} from \"./projection/naturalEarth1.js\";\nexport {default as geoOrthographic, orthographicRaw as geoOrthographicRaw} from \"./projection/orthographic.js\";\nexport {default as geoStereographic, stereographicRaw as geoStereographicRaw} from \"./projection/stereographic.js\";\nexport {default as geoTransverseMercator, transverseMercatorRaw as geoTransverseMercatorRaw} from \"./projection/transverseMercator.js\";\nexport {default as geoRotation} from \"./rotation.js\";\nexport {default as geoStream} from \"./stream.js\";\nexport {default as geoTransform} from \"./transform.js\";\n", "export var xhtml = \"http://www.w3.org/1999/xhtml\";\n\nexport default {\n svg: \"http://www.w3.org/2000/svg\",\n xhtml: xhtml,\n xlink: \"http://www.w3.org/1999/xlink\",\n xml: \"http://www.w3.org/XML/1998/namespace\",\n xmlns: \"http://www.w3.org/2000/xmlns/\"\n};\n", "import namespaces from \"./namespaces.js\";\n\nexport default function(name) {\n var prefix = name += \"\", i = prefix.indexOf(\":\");\n if (i >= 0 && (prefix = name.slice(0, i)) !== \"xmlns\") name = name.slice(i + 1);\n return namespaces.hasOwnProperty(prefix) ? {space: namespaces[prefix], local: name} : name; // eslint-disable-line no-prototype-builtins\n}\n", "import namespace from \"./namespace.js\";\nimport {xhtml} from \"./namespaces.js\";\n\nfunction creatorInherit(name) {\n return function() {\n var document = this.ownerDocument,\n uri = this.namespaceURI;\n return uri === xhtml && document.documentElement.namespaceURI === xhtml\n ? document.createElement(name)\n : document.createElementNS(uri, name);\n };\n}\n\nfunction creatorFixed(fullname) {\n return function() {\n return this.ownerDocument.createElementNS(fullname.space, fullname.local);\n };\n}\n\nexport default function(name) {\n var fullname = namespace(name);\n return (fullname.local\n ? creatorFixed\n : creatorInherit)(fullname);\n}\n", "function none() {}\n\nexport default function(selector) {\n return selector == null ? none : function() {\n return this.querySelector(selector);\n };\n}\n", "import {Selection} from \"./index.js\";\nimport selector from \"../selector.js\";\n\nexport default function(select) {\n if (typeof select !== \"function\") select = selector(select);\n\n for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {\n if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {\n if (\"__data__\" in node) subnode.__data__ = node.__data__;\n subgroup[i] = subnode;\n }\n }\n }\n\n return new Selection(subgroups, this._parents);\n}\n", "// Given something array like (or null), returns something that is strictly an\n// array. This is used to ensure that array-like objects passed to d3.selectAll\n// or selection.selectAll are converted into proper arrays when creating a\n// selection; we don\u2019t ever want to create a selection backed by a live\n// HTMLCollection or NodeList. However, note that selection.selectAll will use a\n// static NodeList as a group, since it safely derived from querySelectorAll.\nexport default function array(x) {\n return x == null ? [] : Array.isArray(x) ? x : Array.from(x);\n}\n", "function empty() {\n return [];\n}\n\nexport default function(selector) {\n return selector == null ? empty : function() {\n return this.querySelectorAll(selector);\n };\n}\n", "import {Selection} from \"./index.js\";\nimport array from \"../array.js\";\nimport selectorAll from \"../selectorAll.js\";\n\nfunction arrayAll(select) {\n return function() {\n return array(select.apply(this, arguments));\n };\n}\n\nexport default function(select) {\n if (typeof select === \"function\") select = arrayAll(select);\n else select = selectorAll(select);\n\n for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {\n if (node = group[i]) {\n subgroups.push(select.call(node, node.__data__, i, group));\n parents.push(node);\n }\n }\n }\n\n return new Selection(subgroups, parents);\n}\n", "export default function(selector) {\n return function() {\n return this.matches(selector);\n };\n}\n\nexport function childMatcher(selector) {\n return function(node) {\n return node.matches(selector);\n };\n}\n\n", "import {childMatcher} from \"../matcher.js\";\n\nvar find = Array.prototype.find;\n\nfunction childFind(match) {\n return function() {\n return find.call(this.children, match);\n };\n}\n\nfunction childFirst() {\n return this.firstElementChild;\n}\n\nexport default function(match) {\n return this.select(match == null ? childFirst\n : childFind(typeof match === \"function\" ? match : childMatcher(match)));\n}\n", "import {childMatcher} from \"../matcher.js\";\n\nvar filter = Array.prototype.filter;\n\nfunction children() {\n return Array.from(this.children);\n}\n\nfunction childrenFilter(match) {\n return function() {\n return filter.call(this.children, match);\n };\n}\n\nexport default function(match) {\n return this.selectAll(match == null ? children\n : childrenFilter(typeof match === \"function\" ? match : childMatcher(match)));\n}\n", "import {Selection} from \"./index.js\";\nimport matcher from \"../matcher.js\";\n\nexport default function(match) {\n if (typeof match !== \"function\") match = matcher(match);\n\n for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {\n if ((node = group[i]) && match.call(node, node.__data__, i, group)) {\n subgroup.push(node);\n }\n }\n }\n\n return new Selection(subgroups, this._parents);\n}\n", "export default function(update) {\n return new Array(update.length);\n}\n", "import sparse from \"./sparse.js\";\nimport {Selection} from \"./index.js\";\n\nexport default function() {\n return new Selection(this._enter || this._groups.map(sparse), this._parents);\n}\n\nexport function EnterNode(parent, datum) {\n this.ownerDocument = parent.ownerDocument;\n this.namespaceURI = parent.namespaceURI;\n this._next = null;\n this._parent = parent;\n this.__data__ = datum;\n}\n\nEnterNode.prototype = {\n constructor: EnterNode,\n appendChild: function(child) { return this._parent.insertBefore(child, this._next); },\n insertBefore: function(child, next) { return this._parent.insertBefore(child, next); },\n querySelector: function(selector) { return this._parent.querySelector(selector); },\n querySelectorAll: function(selector) { return this._parent.querySelectorAll(selector); }\n};\n", "export default function(x) {\n return function() {\n return x;\n };\n}\n", "import {Selection} from \"./index.js\";\nimport {EnterNode} from \"./enter.js\";\nimport constant from \"../constant.js\";\n\nfunction bindIndex(parent, group, enter, update, exit, data) {\n var i = 0,\n node,\n groupLength = group.length,\n dataLength = data.length;\n\n // Put any non-null nodes that fit into update.\n // Put any null nodes into enter.\n // Put any remaining data into enter.\n for (; i < dataLength; ++i) {\n if (node = group[i]) {\n node.__data__ = data[i];\n update[i] = node;\n } else {\n enter[i] = new EnterNode(parent, data[i]);\n }\n }\n\n // Put any non-null nodes that don\u2019t fit into exit.\n for (; i < groupLength; ++i) {\n if (node = group[i]) {\n exit[i] = node;\n }\n }\n}\n\nfunction bindKey(parent, group, enter, update, exit, data, key) {\n var i,\n node,\n nodeByKeyValue = new Map,\n groupLength = group.length,\n dataLength = data.length,\n keyValues = new Array(groupLength),\n keyValue;\n\n // Compute the key for each node.\n // If multiple nodes have the same key, the duplicates are added to exit.\n for (i = 0; i < groupLength; ++i) {\n if (node = group[i]) {\n keyValues[i] = keyValue = key.call(node, node.__data__, i, group) + \"\";\n if (nodeByKeyValue.has(keyValue)) {\n exit[i] = node;\n } else {\n nodeByKeyValue.set(keyValue, node);\n }\n }\n }\n\n // Compute the key for each datum.\n // If there a node associated with this key, join and add it to update.\n // If there is not (or the key is a duplicate), add it to enter.\n for (i = 0; i < dataLength; ++i) {\n keyValue = key.call(parent, data[i], i, data) + \"\";\n if (node = nodeByKeyValue.get(keyValue)) {\n update[i] = node;\n node.__data__ = data[i];\n nodeByKeyValue.delete(keyValue);\n } else {\n enter[i] = new EnterNode(parent, data[i]);\n }\n }\n\n // Add any remaining nodes that were not bound to data to exit.\n for (i = 0; i < groupLength; ++i) {\n if ((node = group[i]) && (nodeByKeyValue.get(keyValues[i]) === node)) {\n exit[i] = node;\n }\n }\n}\n\nfunction datum(node) {\n return node.__data__;\n}\n\nexport default function(value, key) {\n if (!arguments.length) return Array.from(this, datum);\n\n var bind = key ? bindKey : bindIndex,\n parents = this._parents,\n groups = this._groups;\n\n if (typeof value !== \"function\") value = constant(value);\n\n for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {\n var parent = parents[j],\n group = groups[j],\n groupLength = group.length,\n data = arraylike(value.call(parent, parent && parent.__data__, j, parents)),\n dataLength = data.length,\n enterGroup = enter[j] = new Array(dataLength),\n updateGroup = update[j] = new Array(dataLength),\n exitGroup = exit[j] = new Array(groupLength);\n\n bind(parent, group, enterGroup, updateGroup, exitGroup, data, key);\n\n // Now connect the enter nodes to their following update node, such that\n // appendChild can insert the materialized enter node before this node,\n // rather than at the end of the parent node.\n for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {\n if (previous = enterGroup[i0]) {\n if (i0 >= i1) i1 = i0 + 1;\n while (!(next = updateGroup[i1]) && ++i1 < dataLength);\n previous._next = next || null;\n }\n }\n }\n\n update = new Selection(update, parents);\n update._enter = enter;\n update._exit = exit;\n return update;\n}\n\n// Given some data, this returns an array-like view of it: an object that\n// exposes a length property and allows numeric indexing. Note that unlike\n// selectAll, this isn\u2019t worried about \u201Clive\u201D collections because the resulting\n// array will only be used briefly while data is being bound. (It is possible to\n// cause the data to change while iterating by using a key function, but please\n// don\u2019t; we\u2019d rather avoid a gratuitous copy.)\nfunction arraylike(data) {\n return typeof data === \"object\" && \"length\" in data\n ? data // Array, TypedArray, NodeList, array-like\n : Array.from(data); // Map, Set, iterable, string, or anything else\n}\n", "import sparse from \"./sparse.js\";\nimport {Selection} from \"./index.js\";\n\nexport default function() {\n return new Selection(this._exit || this._groups.map(sparse), this._parents);\n}\n", "export default function(onenter, onupdate, onexit) {\n var enter = this.enter(), update = this, exit = this.exit();\n if (typeof onenter === \"function\") {\n enter = onenter(enter);\n if (enter) enter = enter.selection();\n } else {\n enter = enter.append(onenter + \"\");\n }\n if (onupdate != null) {\n update = onupdate(update);\n if (update) update = update.selection();\n }\n if (onexit == null) exit.remove(); else onexit(exit);\n return enter && update ? enter.merge(update).order() : update;\n}\n", "import {Selection} from \"./index.js\";\n\nexport default function(context) {\n var selection = context.selection ? context.selection() : context;\n\n for (var groups0 = this._groups, groups1 = selection._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {\n for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {\n if (node = group0[i] || group1[i]) {\n merge[i] = node;\n }\n }\n }\n\n for (; j < m0; ++j) {\n merges[j] = groups0[j];\n }\n\n return new Selection(merges, this._parents);\n}\n", "export default function() {\n\n for (var groups = this._groups, j = -1, m = groups.length; ++j < m;) {\n for (var group = groups[j], i = group.length - 1, next = group[i], node; --i >= 0;) {\n if (node = group[i]) {\n if (next && node.compareDocumentPosition(next) ^ 4) next.parentNode.insertBefore(node, next);\n next = node;\n }\n }\n }\n\n return this;\n}\n", "import {Selection} from \"./index.js\";\n\nexport default function(compare) {\n if (!compare) compare = ascending;\n\n function compareNode(a, b) {\n return a && b ? compare(a.__data__, b.__data__) : !a - !b;\n }\n\n for (var groups = this._groups, m = groups.length, sortgroups = new Array(m), j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, sortgroup = sortgroups[j] = new Array(n), node, i = 0; i < n; ++i) {\n if (node = group[i]) {\n sortgroup[i] = node;\n }\n }\n sortgroup.sort(compareNode);\n }\n\n return new Selection(sortgroups, this._parents).order();\n}\n\nfunction ascending(a, b) {\n return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;\n}\n", "export default function() {\n var callback = arguments[0];\n arguments[0] = this;\n callback.apply(null, arguments);\n return this;\n}\n", "export default function() {\n return Array.from(this);\n}\n", "export default function() {\n\n for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {\n for (var group = groups[j], i = 0, n = group.length; i < n; ++i) {\n var node = group[i];\n if (node) return node;\n }\n }\n\n return null;\n}\n", "export default function() {\n let size = 0;\n for (const node of this) ++size; // eslint-disable-line no-unused-vars\n return size;\n}\n", "export default function() {\n return !this.node();\n}\n", "export default function(callback) {\n\n for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {\n for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {\n if (node = group[i]) callback.call(node, node.__data__, i, group);\n }\n }\n\n return this;\n}\n", "import namespace from \"../namespace.js\";\n\nfunction attrRemove(name) {\n return function() {\n this.removeAttribute(name);\n };\n}\n\nfunction attrRemoveNS(fullname) {\n return function() {\n this.removeAttributeNS(fullname.space, fullname.local);\n };\n}\n\nfunction attrConstant(name, value) {\n return function() {\n this.setAttribute(name, value);\n };\n}\n\nfunction attrConstantNS(fullname, value) {\n return function() {\n this.setAttributeNS(fullname.space, fullname.local, value);\n };\n}\n\nfunction attrFunction(name, value) {\n return function() {\n var v = value.apply(this, arguments);\n if (v == null) this.removeAttribute(name);\n else this.setAttribute(name, v);\n };\n}\n\nfunction attrFunctionNS(fullname, value) {\n return function() {\n var v = value.apply(this, arguments);\n if (v == null) this.removeAttributeNS(fullname.space, fullname.local);\n else this.setAttributeNS(fullname.space, fullname.local, v);\n };\n}\n\nexport default function(name, value) {\n var fullname = namespace(name);\n\n if (arguments.length < 2) {\n var node = this.node();\n return fullname.local\n ? node.getAttributeNS(fullname.space, fullname.local)\n : node.getAttribute(fullname);\n }\n\n return this.each((value == null\n ? (fullname.local ? attrRemoveNS : attrRemove) : (typeof value === \"function\"\n ? (fullname.local ? attrFunctionNS : attrFunction)\n : (fullname.local ? attrConstantNS : attrConstant)))(fullname, value));\n}\n", "export default function(node) {\n return (node.ownerDocument && node.ownerDocument.defaultView) // node is a Node\n || (node.document && node) // node is a Window\n || node.defaultView; // node is a Document\n}\n", "import defaultView from \"../window.js\";\n\nfunction styleRemove(name) {\n return function() {\n this.style.removeProperty(name);\n };\n}\n\nfunction styleConstant(name, value, priority) {\n return function() {\n this.style.setProperty(name, value, priority);\n };\n}\n\nfunction styleFunction(name, value, priority) {\n return function() {\n var v = value.apply(this, arguments);\n if (v == null) this.style.removeProperty(name);\n else this.style.setProperty(name, v, priority);\n };\n}\n\nexport default function(name, value, priority) {\n return arguments.length > 1\n ? this.each((value == null\n ? styleRemove : typeof value === \"function\"\n ? styleFunction\n : styleConstant)(name, value, priority == null ? \"\" : priority))\n : styleValue(this.node(), name);\n}\n\nexport function styleValue(node, name) {\n return node.style.getPropertyValue(name)\n || defaultView(node).getComputedStyle(node, null).getPropertyValue(name);\n}\n", "function propertyRemove(name) {\n return function() {\n delete this[name];\n };\n}\n\nfunction propertyConstant(name, value) {\n return function() {\n this[name] = value;\n };\n}\n\nfunction propertyFunction(name, value) {\n return function() {\n var v = value.apply(this, arguments);\n if (v == null) delete this[name];\n else this[name] = v;\n };\n}\n\nexport default function(name, value) {\n return arguments.length > 1\n ? this.each((value == null\n ? propertyRemove : typeof value === \"function\"\n ? propertyFunction\n : propertyConstant)(name, value))\n : this.node()[name];\n}\n", "function classArray(string) {\n return string.trim().split(/^|\\s+/);\n}\n\nfunction classList(node) {\n return node.classList || new ClassList(node);\n}\n\nfunction ClassList(node) {\n this._node = node;\n this._names = classArray(node.getAttribute(\"class\") || \"\");\n}\n\nClassList.prototype = {\n add: function(name) {\n var i = this._names.indexOf(name);\n if (i < 0) {\n this._names.push(name);\n this._node.setAttribute(\"class\", this._names.join(\" \"));\n }\n },\n remove: function(name) {\n var i = this._names.indexOf(name);\n if (i >= 0) {\n this._names.splice(i, 1);\n this._node.setAttribute(\"class\", this._names.join(\" \"));\n }\n },\n contains: function(name) {\n return this._names.indexOf(name) >= 0;\n }\n};\n\nfunction classedAdd(node, names) {\n var list = classList(node), i = -1, n = names.length;\n while (++i < n) list.add(names[i]);\n}\n\nfunction classedRemove(node, names) {\n var list = classList(node), i = -1, n = names.length;\n while (++i < n) list.remove(names[i]);\n}\n\nfunction classedTrue(names) {\n return function() {\n classedAdd(this, names);\n };\n}\n\nfunction classedFalse(names) {\n return function() {\n classedRemove(this, names);\n };\n}\n\nfunction classedFunction(names, value) {\n return function() {\n (value.apply(this, arguments) ? classedAdd : classedRemove)(this, names);\n };\n}\n\nexport default function(name, value) {\n var names = classArray(name + \"\");\n\n if (arguments.length < 2) {\n var list = classList(this.node()), i = -1, n = names.length;\n while (++i < n) if (!list.contains(names[i])) return false;\n return true;\n }\n\n return this.each((typeof value === \"function\"\n ? classedFunction : value\n ? classedTrue\n : classedFalse)(names, value));\n}\n", "function textRemove() {\n this.textContent = \"\";\n}\n\nfunction textConstant(value) {\n return function() {\n this.textContent = value;\n };\n}\n\nfunction textFunction(value) {\n return function() {\n var v = value.apply(this, arguments);\n this.textContent = v == null ? \"\" : v;\n };\n}\n\nexport default function(value) {\n return arguments.length\n ? this.each(value == null\n ? textRemove : (typeof value === \"function\"\n ? textFunction\n : textConstant)(value))\n : this.node().textContent;\n}\n", "function htmlRemove() {\n this.innerHTML = \"\";\n}\n\nfunction htmlConstant(value) {\n return function() {\n this.innerHTML = value;\n };\n}\n\nfunction htmlFunction(value) {\n return function() {\n var v = value.apply(this, arguments);\n this.innerHTML = v == null ? \"\" : v;\n };\n}\n\nexport default function(value) {\n return arguments.length\n ? this.each(value == null\n ? htmlRemove : (typeof value === \"function\"\n ? htmlFunction\n : htmlConstant)(value))\n : this.node().innerHTML;\n}\n", "function raise() {\n if (this.nextSibling) this.parentNode.appendChild(this);\n}\n\nexport default function() {\n return this.each(raise);\n}\n", "function lower() {\n if (this.previousSibling) this.parentNode.insertBefore(this, this.parentNode.firstChild);\n}\n\nexport default function() {\n return this.each(lower);\n}\n", "import creator from \"../creator.js\";\n\nexport default function(name) {\n var create = typeof name === \"function\" ? name : creator(name);\n return this.select(function() {\n return this.appendChild(create.apply(this, arguments));\n });\n}\n", "import creator from \"../creator.js\";\nimport selector from \"../selector.js\";\n\nfunction constantNull() {\n return null;\n}\n\nexport default function(name, before) {\n var create = typeof name === \"function\" ? name : creator(name),\n select = before == null ? constantNull : typeof before === \"function\" ? before : selector(before);\n return this.select(function() {\n return this.insertBefore(create.apply(this, arguments), select.apply(this, arguments) || null);\n });\n}\n", "function remove() {\n var parent = this.parentNode;\n if (parent) parent.removeChild(this);\n}\n\nexport default function() {\n return this.each(remove);\n}\n", "function selection_cloneShallow() {\n var clone = this.cloneNode(false), parent = this.parentNode;\n return parent ? parent.insertBefore(clone, this.nextSibling) : clone;\n}\n\nfunction selection_cloneDeep() {\n var clone = this.cloneNode(true), parent = this.parentNode;\n return parent ? parent.insertBefore(clone, this.nextSibling) : clone;\n}\n\nexport default function(deep) {\n return this.select(deep ? selection_cloneDeep : selection_cloneShallow);\n}\n", "export default function(value) {\n return arguments.length\n ? this.property(\"__data__\", value)\n : this.node().__data__;\n}\n", "function contextListener(listener) {\n return function(event) {\n listener.call(this, event, this.__data__);\n };\n}\n\nfunction parseTypenames(typenames) {\n return typenames.trim().split(/^|\\s+/).map(function(t) {\n var name = \"\", i = t.indexOf(\".\");\n if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);\n return {type: t, name: name};\n });\n}\n\nfunction onRemove(typename) {\n return function() {\n var on = this.__on;\n if (!on) return;\n for (var j = 0, i = -1, m = on.length, o; j < m; ++j) {\n if (o = on[j], (!typename.type || o.type === typename.type) && o.name === typename.name) {\n this.removeEventListener(o.type, o.listener, o.options);\n } else {\n on[++i] = o;\n }\n }\n if (++i) on.length = i;\n else delete this.__on;\n };\n}\n\nfunction onAdd(typename, value, options) {\n return function() {\n var on = this.__on, o, listener = contextListener(value);\n if (on) for (var j = 0, m = on.length; j < m; ++j) {\n if ((o = on[j]).type === typename.type && o.name === typename.name) {\n this.removeEventListener(o.type, o.listener, o.options);\n this.addEventListener(o.type, o.listener = listener, o.options = options);\n o.value = value;\n return;\n }\n }\n this.addEventListener(typename.type, listener, options);\n o = {type: typename.type, name: typename.name, value: value, listener: listener, options: options};\n if (!on) this.__on = [o];\n else on.push(o);\n };\n}\n\nexport default function(typename, value, options) {\n var typenames = parseTypenames(typename + \"\"), i, n = typenames.length, t;\n\n if (arguments.length < 2) {\n var on = this.node().__on;\n if (on) for (var j = 0, m = on.length, o; j < m; ++j) {\n for (i = 0, o = on[j]; i < n; ++i) {\n if ((t = typenames[i]).type === o.type && t.name === o.name) {\n return o.value;\n }\n }\n }\n return;\n }\n\n on = value ? onAdd : onRemove;\n for (i = 0; i < n; ++i) this.each(on(typenames[i], value, options));\n return this;\n}\n", "import defaultView from \"../window.js\";\n\nfunction dispatchEvent(node, type, params) {\n var window = defaultView(node),\n event = window.CustomEvent;\n\n if (typeof event === \"function\") {\n event = new event(type, params);\n } else {\n event = window.document.createEvent(\"Event\");\n if (params) event.initEvent(type, params.bubbles, params.cancelable), event.detail = params.detail;\n else event.initEvent(type, false, false);\n }\n\n node.dispatchEvent(event);\n}\n\nfunction dispatchConstant(type, params) {\n return function() {\n return dispatchEvent(this, type, params);\n };\n}\n\nfunction dispatchFunction(type, params) {\n return function() {\n return dispatchEvent(this, type, params.apply(this, arguments));\n };\n}\n\nexport default function(type, params) {\n return this.each((typeof params === \"function\"\n ? dispatchFunction\n : dispatchConstant)(type, params));\n}\n", "export default function*() {\n for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {\n for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {\n if (node = group[i]) yield node;\n }\n }\n}\n", "import selection_select from \"./select.js\";\nimport selection_selectAll from \"./selectAll.js\";\nimport selection_selectChild from \"./selectChild.js\";\nimport selection_selectChildren from \"./selectChildren.js\";\nimport selection_filter from \"./filter.js\";\nimport selection_data from \"./data.js\";\nimport selection_enter from \"./enter.js\";\nimport selection_exit from \"./exit.js\";\nimport selection_join from \"./join.js\";\nimport selection_merge from \"./merge.js\";\nimport selection_order from \"./order.js\";\nimport selection_sort from \"./sort.js\";\nimport selection_call from \"./call.js\";\nimport selection_nodes from \"./nodes.js\";\nimport selection_node from \"./node.js\";\nimport selection_size from \"./size.js\";\nimport selection_empty from \"./empty.js\";\nimport selection_each from \"./each.js\";\nimport selection_attr from \"./attr.js\";\nimport selection_style from \"./style.js\";\nimport selection_property from \"./property.js\";\nimport selection_classed from \"./classed.js\";\nimport selection_text from \"./text.js\";\nimport selection_html from \"./html.js\";\nimport selection_raise from \"./raise.js\";\nimport selection_lower from \"./lower.js\";\nimport selection_append from \"./append.js\";\nimport selection_insert from \"./insert.js\";\nimport selection_remove from \"./remove.js\";\nimport selection_clone from \"./clone.js\";\nimport selection_datum from \"./datum.js\";\nimport selection_on from \"./on.js\";\nimport selection_dispatch from \"./dispatch.js\";\nimport selection_iterator from \"./iterator.js\";\n\nexport var root = [null];\n\nexport function Selection(groups, parents) {\n this._groups = groups;\n this._parents = parents;\n}\n\nfunction selection() {\n return new Selection([[document.documentElement]], root);\n}\n\nfunction selection_selection() {\n return this;\n}\n\nSelection.prototype = selection.prototype = {\n constructor: Selection,\n select: selection_select,\n selectAll: selection_selectAll,\n selectChild: selection_selectChild,\n selectChildren: selection_selectChildren,\n filter: selection_filter,\n data: selection_data,\n enter: selection_enter,\n exit: selection_exit,\n join: selection_join,\n merge: selection_merge,\n selection: selection_selection,\n order: selection_order,\n sort: selection_sort,\n call: selection_call,\n nodes: selection_nodes,\n node: selection_node,\n size: selection_size,\n empty: selection_empty,\n each: selection_each,\n attr: selection_attr,\n style: selection_style,\n property: selection_property,\n classed: selection_classed,\n text: selection_text,\n html: selection_html,\n raise: selection_raise,\n lower: selection_lower,\n append: selection_append,\n insert: selection_insert,\n remove: selection_remove,\n clone: selection_clone,\n datum: selection_datum,\n on: selection_on,\n dispatch: selection_dispatch,\n [Symbol.iterator]: selection_iterator\n};\n\nexport default selection;\n", "import {Selection, root} from \"./selection/index.js\";\n\nexport default function(selector) {\n return typeof selector === \"string\"\n ? new Selection([[document.querySelector(selector)]], [document.documentElement])\n : new Selection([[selector]], root);\n}\n", "import creator from \"./creator.js\";\nimport select from \"./select.js\";\n\nexport default function(name) {\n return select(creator(name).call(document.documentElement));\n}\n", "var nextId = 0;\n\nexport default function local() {\n return new Local;\n}\n\nfunction Local() {\n this._ = \"@\" + (++nextId).toString(36);\n}\n\nLocal.prototype = local.prototype = {\n constructor: Local,\n get: function(node) {\n var id = this._;\n while (!(id in node)) if (!(node = node.parentNode)) return;\n return node[id];\n },\n set: function(node, value) {\n return node[this._] = value;\n },\n remove: function(node) {\n return this._ in node && delete node[this._];\n },\n toString: function() {\n return this._;\n }\n};\n", "export default function(event) {\n let sourceEvent;\n while (sourceEvent = event.sourceEvent) event = sourceEvent;\n return event;\n}\n", "import sourceEvent from \"./sourceEvent.js\";\n\nexport default function(event, node) {\n event = sourceEvent(event);\n if (node === undefined) node = event.currentTarget;\n if (node) {\n var svg = node.ownerSVGElement || node;\n if (svg.createSVGPoint) {\n var point = svg.createSVGPoint();\n point.x = event.clientX, point.y = event.clientY;\n point = point.matrixTransform(node.getScreenCTM().inverse());\n return [point.x, point.y];\n }\n if (node.getBoundingClientRect) {\n var rect = node.getBoundingClientRect();\n return [event.clientX - rect.left - node.clientLeft, event.clientY - rect.top - node.clientTop];\n }\n }\n return [event.pageX, event.pageY];\n}\n", "import pointer from \"./pointer.js\";\nimport sourceEvent from \"./sourceEvent.js\";\n\nexport default function(events, node) {\n if (events.target) { // i.e., instanceof Event, not TouchList or iterable\n events = sourceEvent(events);\n if (node === undefined) node = events.currentTarget;\n events = events.touches || [events];\n }\n return Array.from(events, event => pointer(event, node));\n}\n", "import array from \"./array.js\";\nimport {Selection, root} from \"./selection/index.js\";\n\nexport default function(selector) {\n return typeof selector === \"string\"\n ? new Selection([document.querySelectorAll(selector)], [document.documentElement])\n : new Selection([array(selector)], root);\n}\n", "export {default as create} from \"./create.js\";\nexport {default as creator} from \"./creator.js\";\nexport {default as local} from \"./local.js\";\nexport {default as matcher} from \"./matcher.js\";\nexport {default as namespace} from \"./namespace.js\";\nexport {default as namespaces} from \"./namespaces.js\";\nexport {default as pointer} from \"./pointer.js\";\nexport {default as pointers} from \"./pointers.js\";\nexport {default as select} from \"./select.js\";\nexport {default as selectAll} from \"./selectAll.js\";\nexport {default as selection} from \"./selection/index.js\";\nexport {default as selector} from \"./selector.js\";\nexport {default as selectorAll} from \"./selectorAll.js\";\nexport {styleValue as style} from \"./selection/style.js\";\nexport {default as window} from \"./window.js\";\n", "// These are typically used in conjunction with noevent to ensure that we can\n// preventDefault on the event.\nexport const nonpassive = {passive: false};\nexport const nonpassivecapture = {capture: true, passive: false};\n\nexport function nopropagation(event) {\n event.stopImmediatePropagation();\n}\n\nexport default function(event) {\n event.preventDefault();\n event.stopImmediatePropagation();\n}\n", "import {select} from \"d3-selection\";\nimport noevent, {nonpassivecapture} from \"./noevent.js\";\n\nexport default function(view) {\n var root = view.document.documentElement,\n selection = select(view).on(\"dragstart.drag\", noevent, nonpassivecapture);\n if (\"onselectstart\" in root) {\n selection.on(\"selectstart.drag\", noevent, nonpassivecapture);\n } else {\n root.__noselect = root.style.MozUserSelect;\n root.style.MozUserSelect = \"none\";\n }\n}\n\nexport function yesdrag(view, noclick) {\n var root = view.document.documentElement,\n selection = select(view).on(\"dragstart.drag\", null);\n if (noclick) {\n selection.on(\"click.drag\", noevent, nonpassivecapture);\n setTimeout(function() { selection.on(\"click.drag\", null); }, 0);\n }\n if (\"onselectstart\" in root) {\n selection.on(\"selectstart.drag\", null);\n } else {\n root.style.MozUserSelect = root.__noselect;\n delete root.__noselect;\n }\n}\n", "export default x => () => x;\n", "export default function DragEvent(type, {\n sourceEvent,\n subject,\n target,\n identifier,\n active,\n x, y, dx, dy,\n dispatch\n}) {\n Object.defineProperties(this, {\n type: {value: type, enumerable: true, configurable: true},\n sourceEvent: {value: sourceEvent, enumerable: true, configurable: true},\n subject: {value: subject, enumerable: true, configurable: true},\n target: {value: target, enumerable: true, configurable: true},\n identifier: {value: identifier, enumerable: true, configurable: true},\n active: {value: active, enumerable: true, configurable: true},\n x: {value: x, enumerable: true, configurable: true},\n y: {value: y, enumerable: true, configurable: true},\n dx: {value: dx, enumerable: true, configurable: true},\n dy: {value: dy, enumerable: true, configurable: true},\n _: {value: dispatch}\n });\n}\n\nDragEvent.prototype.on = function() {\n var value = this._.on.apply(this._, arguments);\n return value === this._ ? this : value;\n};\n", "import {dispatch} from \"d3-dispatch\";\nimport {select, pointer} from \"d3-selection\";\nimport nodrag, {yesdrag} from \"./nodrag.js\";\nimport noevent, {nonpassive, nonpassivecapture, nopropagation} from \"./noevent.js\";\nimport constant from \"./constant.js\";\nimport DragEvent from \"./event.js\";\n\n// Ignore right-click, since that should open the context menu.\nfunction defaultFilter(event) {\n return !event.ctrlKey && !event.button;\n}\n\nfunction defaultContainer() {\n return this.parentNode;\n}\n\nfunction defaultSubject(event, d) {\n return d == null ? {x: event.x, y: event.y} : d;\n}\n\nfunction defaultTouchable() {\n return navigator.maxTouchPoints || (\"ontouchstart\" in this);\n}\n\nexport default function() {\n var filter = defaultFilter,\n container = defaultContainer,\n subject = defaultSubject,\n touchable = defaultTouchable,\n gestures = {},\n listeners = dispatch(\"start\", \"drag\", \"end\"),\n active = 0,\n mousedownx,\n mousedowny,\n mousemoving,\n touchending,\n clickDistance2 = 0;\n\n function drag(selection) {\n selection\n .on(\"mousedown.drag\", mousedowned)\n .filter(touchable)\n .on(\"touchstart.drag\", touchstarted)\n .on(\"touchmove.drag\", touchmoved, nonpassive)\n .on(\"touchend.drag touchcancel.drag\", touchended)\n .style(\"touch-action\", \"none\")\n .style(\"-webkit-tap-highlight-color\", \"rgba(0,0,0,0)\");\n }\n\n function mousedowned(event, d) {\n if (touchending || !filter.call(this, event, d)) return;\n var gesture = beforestart(this, container.call(this, event, d), event, d, \"mouse\");\n if (!gesture) return;\n select(event.view)\n .on(\"mousemove.drag\", mousemoved, nonpassivecapture)\n .on(\"mouseup.drag\", mouseupped, nonpassivecapture);\n nodrag(event.view);\n nopropagation(event);\n mousemoving = false;\n mousedownx = event.clientX;\n mousedowny = event.clientY;\n gesture(\"start\", event);\n }\n\n function mousemoved(event) {\n noevent(event);\n if (!mousemoving) {\n var dx = event.clientX - mousedownx, dy = event.clientY - mousedowny;\n mousemoving = dx * dx + dy * dy > clickDistance2;\n }\n gestures.mouse(\"drag\", event);\n }\n\n function mouseupped(event) {\n select(event.view).on(\"mousemove.drag mouseup.drag\", null);\n yesdrag(event.view, mousemoving);\n noevent(event);\n gestures.mouse(\"end\", event);\n }\n\n function touchstarted(event, d) {\n if (!filter.call(this, event, d)) return;\n var touches = event.changedTouches,\n c = container.call(this, event, d),\n n = touches.length, i, gesture;\n\n for (i = 0; i < n; ++i) {\n if (gesture = beforestart(this, c, event, d, touches[i].identifier, touches[i])) {\n nopropagation(event);\n gesture(\"start\", event, touches[i]);\n }\n }\n }\n\n function touchmoved(event) {\n var touches = event.changedTouches,\n n = touches.length, i, gesture;\n\n for (i = 0; i < n; ++i) {\n if (gesture = gestures[touches[i].identifier]) {\n noevent(event);\n gesture(\"drag\", event, touches[i]);\n }\n }\n }\n\n function touchended(event) {\n var touches = event.changedTouches,\n n = touches.length, i, gesture;\n\n if (touchending) clearTimeout(touchending);\n touchending = setTimeout(function() { touchending = null; }, 500); // Ghost clicks are delayed!\n for (i = 0; i < n; ++i) {\n if (gesture = gestures[touches[i].identifier]) {\n nopropagation(event);\n gesture(\"end\", event, touches[i]);\n }\n }\n }\n\n function beforestart(that, container, event, d, identifier, touch) {\n var dispatch = listeners.copy(),\n p = pointer(touch || event, container), dx, dy,\n s;\n\n if ((s = subject.call(that, new DragEvent(\"beforestart\", {\n sourceEvent: event,\n target: drag,\n identifier,\n active,\n x: p[0],\n y: p[1],\n dx: 0,\n dy: 0,\n dispatch\n }), d)) == null) return;\n\n dx = s.x - p[0] || 0;\n dy = s.y - p[1] || 0;\n\n return function gesture(type, event, touch) {\n var p0 = p, n;\n switch (type) {\n case \"start\": gestures[identifier] = gesture, n = active++; break;\n case \"end\": delete gestures[identifier], --active; // falls through\n case \"drag\": p = pointer(touch || event, container), n = active; break;\n }\n dispatch.call(\n type,\n that,\n new DragEvent(type, {\n sourceEvent: event,\n subject: s,\n target: drag,\n identifier,\n active: n,\n x: p[0] + dx,\n y: p[1] + dy,\n dx: p[0] - p0[0],\n dy: p[1] - p0[1],\n dispatch\n }),\n d\n );\n };\n }\n\n drag.filter = function(_) {\n return arguments.length ? (filter = typeof _ === \"function\" ? _ : constant(!!_), drag) : filter;\n };\n\n drag.container = function(_) {\n return arguments.length ? (container = typeof _ === \"function\" ? _ : constant(_), drag) : container;\n };\n\n drag.subject = function(_) {\n return arguments.length ? (subject = typeof _ === \"function\" ? _ : constant(_), drag) : subject;\n };\n\n drag.touchable = function(_) {\n return arguments.length ? (touchable = typeof _ === \"function\" ? _ : constant(!!_), drag) : touchable;\n };\n\n drag.on = function() {\n var value = listeners.on.apply(listeners, arguments);\n return value === listeners ? drag : value;\n };\n\n drag.clickDistance = function(_) {\n return arguments.length ? (clickDistance2 = (_ = +_) * _, drag) : Math.sqrt(clickDistance2);\n };\n\n return drag;\n}\n", "export {default as drag} from \"./drag.js\";\nexport {default as dragDisable, yesdrag as dragEnable} from \"./nodrag.js\";\n", "export default function(constructor, factory, prototype) {\n constructor.prototype = factory.prototype = prototype;\n prototype.constructor = constructor;\n}\n\nexport function extend(parent, definition) {\n var prototype = Object.create(parent.prototype);\n for (var key in definition) prototype[key] = definition[key];\n return prototype;\n}\n", "import define, {extend} from \"./define.js\";\n\nexport function Color() {}\n\nexport var darker = 0.7;\nexport var brighter = 1 / darker;\n\nvar reI = \"\\\\s*([+-]?\\\\d+)\\\\s*\",\n reN = \"\\\\s*([+-]?(?:\\\\d*\\\\.)?\\\\d+(?:[eE][+-]?\\\\d+)?)\\\\s*\",\n reP = \"\\\\s*([+-]?(?:\\\\d*\\\\.)?\\\\d+(?:[eE][+-]?\\\\d+)?)%\\\\s*\",\n reHex = /^#([0-9a-f]{3,8})$/,\n reRgbInteger = new RegExp(`^rgb\\\\(${reI},${reI},${reI}\\\\)$`),\n reRgbPercent = new RegExp(`^rgb\\\\(${reP},${reP},${reP}\\\\)$`),\n reRgbaInteger = new RegExp(`^rgba\\\\(${reI},${reI},${reI},${reN}\\\\)$`),\n reRgbaPercent = new RegExp(`^rgba\\\\(${reP},${reP},${reP},${reN}\\\\)$`),\n reHslPercent = new RegExp(`^hsl\\\\(${reN},${reP},${reP}\\\\)$`),\n reHslaPercent = new RegExp(`^hsla\\\\(${reN},${reP},${reP},${reN}\\\\)$`);\n\nvar named = {\n aliceblue: 0xf0f8ff,\n antiquewhite: 0xfaebd7,\n aqua: 0x00ffff,\n aquamarine: 0x7fffd4,\n azure: 0xf0ffff,\n beige: 0xf5f5dc,\n bisque: 0xffe4c4,\n black: 0x000000,\n blanchedalmond: 0xffebcd,\n blue: 0x0000ff,\n blueviolet: 0x8a2be2,\n brown: 0xa52a2a,\n burlywood: 0xdeb887,\n cadetblue: 0x5f9ea0,\n chartreuse: 0x7fff00,\n chocolate: 0xd2691e,\n coral: 0xff7f50,\n cornflowerblue: 0x6495ed,\n cornsilk: 0xfff8dc,\n crimson: 0xdc143c,\n cyan: 0x00ffff,\n darkblue: 0x00008b,\n darkcyan: 0x008b8b,\n darkgoldenrod: 0xb8860b,\n darkgray: 0xa9a9a9,\n darkgreen: 0x006400,\n darkgrey: 0xa9a9a9,\n darkkhaki: 0xbdb76b,\n darkmagenta: 0x8b008b,\n darkolivegreen: 0x556b2f,\n darkorange: 0xff8c00,\n darkorchid: 0x9932cc,\n darkred: 0x8b0000,\n darksalmon: 0xe9967a,\n darkseagreen: 0x8fbc8f,\n darkslateblue: 0x483d8b,\n darkslategray: 0x2f4f4f,\n darkslategrey: 0x2f4f4f,\n darkturquoise: 0x00ced1,\n darkviolet: 0x9400d3,\n deeppink: 0xff1493,\n deepskyblue: 0x00bfff,\n dimgray: 0x696969,\n dimgrey: 0x696969,\n dodgerblue: 0x1e90ff,\n firebrick: 0xb22222,\n floralwhite: 0xfffaf0,\n forestgreen: 0x228b22,\n fuchsia: 0xff00ff,\n gainsboro: 0xdcdcdc,\n ghostwhite: 0xf8f8ff,\n gold: 0xffd700,\n goldenrod: 0xdaa520,\n gray: 0x808080,\n green: 0x008000,\n greenyellow: 0xadff2f,\n grey: 0x808080,\n honeydew: 0xf0fff0,\n hotpink: 0xff69b4,\n indianred: 0xcd5c5c,\n indigo: 0x4b0082,\n ivory: 0xfffff0,\n khaki: 0xf0e68c,\n lavender: 0xe6e6fa,\n lavenderblush: 0xfff0f5,\n lawngreen: 0x7cfc00,\n lemonchiffon: 0xfffacd,\n lightblue: 0xadd8e6,\n lightcoral: 0xf08080,\n lightcyan: 0xe0ffff,\n lightgoldenrodyellow: 0xfafad2,\n lightgray: 0xd3d3d3,\n lightgreen: 0x90ee90,\n lightgrey: 0xd3d3d3,\n lightpink: 0xffb6c1,\n lightsalmon: 0xffa07a,\n lightseagreen: 0x20b2aa,\n lightskyblue: 0x87cefa,\n lightslategray: 0x778899,\n lightslategrey: 0x778899,\n lightsteelblue: 0xb0c4de,\n lightyellow: 0xffffe0,\n lime: 0x00ff00,\n limegreen: 0x32cd32,\n linen: 0xfaf0e6,\n magenta: 0xff00ff,\n maroon: 0x800000,\n mediumaquamarine: 0x66cdaa,\n mediumblue: 0x0000cd,\n mediumorchid: 0xba55d3,\n mediumpurple: 0x9370db,\n mediumseagreen: 0x3cb371,\n mediumslateblue: 0x7b68ee,\n mediumspringgreen: 0x00fa9a,\n mediumturquoise: 0x48d1cc,\n mediumvioletred: 0xc71585,\n midnightblue: 0x191970,\n mintcream: 0xf5fffa,\n mistyrose: 0xffe4e1,\n moccasin: 0xffe4b5,\n navajowhite: 0xffdead,\n navy: 0x000080,\n oldlace: 0xfdf5e6,\n olive: 0x808000,\n olivedrab: 0x6b8e23,\n orange: 0xffa500,\n orangered: 0xff4500,\n orchid: 0xda70d6,\n palegoldenrod: 0xeee8aa,\n palegreen: 0x98fb98,\n paleturquoise: 0xafeeee,\n palevioletred: 0xdb7093,\n papayawhip: 0xffefd5,\n peachpuff: 0xffdab9,\n peru: 0xcd853f,\n pink: 0xffc0cb,\n plum: 0xdda0dd,\n powderblue: 0xb0e0e6,\n purple: 0x800080,\n rebeccapurple: 0x663399,\n red: 0xff0000,\n rosybrown: 0xbc8f8f,\n royalblue: 0x4169e1,\n saddlebrown: 0x8b4513,\n salmon: 0xfa8072,\n sandybrown: 0xf4a460,\n seagreen: 0x2e8b57,\n seashell: 0xfff5ee,\n sienna: 0xa0522d,\n silver: 0xc0c0c0,\n skyblue: 0x87ceeb,\n slateblue: 0x6a5acd,\n slategray: 0x708090,\n slategrey: 0x708090,\n snow: 0xfffafa,\n springgreen: 0x00ff7f,\n steelblue: 0x4682b4,\n tan: 0xd2b48c,\n teal: 0x008080,\n thistle: 0xd8bfd8,\n tomato: 0xff6347,\n turquoise: 0x40e0d0,\n violet: 0xee82ee,\n wheat: 0xf5deb3,\n white: 0xffffff,\n whitesmoke: 0xf5f5f5,\n yellow: 0xffff00,\n yellowgreen: 0x9acd32\n};\n\ndefine(Color, color, {\n copy(channels) {\n return Object.assign(new this.constructor, this, channels);\n },\n displayable() {\n return this.rgb().displayable();\n },\n hex: color_formatHex, // Deprecated! Use color.formatHex.\n formatHex: color_formatHex,\n formatHex8: color_formatHex8,\n formatHsl: color_formatHsl,\n formatRgb: color_formatRgb,\n toString: color_formatRgb\n});\n\nfunction color_formatHex() {\n return this.rgb().formatHex();\n}\n\nfunction color_formatHex8() {\n return this.rgb().formatHex8();\n}\n\nfunction color_formatHsl() {\n return hslConvert(this).formatHsl();\n}\n\nfunction color_formatRgb() {\n return this.rgb().formatRgb();\n}\n\nexport default function color(format) {\n var m, l;\n format = (format + \"\").trim().toLowerCase();\n return (m = reHex.exec(format)) ? (l = m[1].length, m = parseInt(m[1], 16), l === 6 ? rgbn(m) // #ff0000\n : l === 3 ? new Rgb((m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), ((m & 0xf) << 4) | (m & 0xf), 1) // #f00\n : l === 8 ? rgba(m >> 24 & 0xff, m >> 16 & 0xff, m >> 8 & 0xff, (m & 0xff) / 0xff) // #ff000000\n : l === 4 ? rgba((m >> 12 & 0xf) | (m >> 8 & 0xf0), (m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), (((m & 0xf) << 4) | (m & 0xf)) / 0xff) // #f000\n : null) // invalid hex\n : (m = reRgbInteger.exec(format)) ? new Rgb(m[1], m[2], m[3], 1) // rgb(255, 0, 0)\n : (m = reRgbPercent.exec(format)) ? new Rgb(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, 1) // rgb(100%, 0%, 0%)\n : (m = reRgbaInteger.exec(format)) ? rgba(m[1], m[2], m[3], m[4]) // rgba(255, 0, 0, 1)\n : (m = reRgbaPercent.exec(format)) ? rgba(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, m[4]) // rgb(100%, 0%, 0%, 1)\n : (m = reHslPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, 1) // hsl(120, 50%, 50%)\n : (m = reHslaPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, m[4]) // hsla(120, 50%, 50%, 1)\n : named.hasOwnProperty(format) ? rgbn(named[format]) // eslint-disable-line no-prototype-builtins\n : format === \"transparent\" ? new Rgb(NaN, NaN, NaN, 0)\n : null;\n}\n\nfunction rgbn(n) {\n return new Rgb(n >> 16 & 0xff, n >> 8 & 0xff, n & 0xff, 1);\n}\n\nfunction rgba(r, g, b, a) {\n if (a <= 0) r = g = b = NaN;\n return new Rgb(r, g, b, a);\n}\n\nexport function rgbConvert(o) {\n if (!(o instanceof Color)) o = color(o);\n if (!o) return new Rgb;\n o = o.rgb();\n return new Rgb(o.r, o.g, o.b, o.opacity);\n}\n\nexport function rgb(r, g, b, opacity) {\n return arguments.length === 1 ? rgbConvert(r) : new Rgb(r, g, b, opacity == null ? 1 : opacity);\n}\n\nexport function Rgb(r, g, b, opacity) {\n this.r = +r;\n this.g = +g;\n this.b = +b;\n this.opacity = +opacity;\n}\n\ndefine(Rgb, rgb, extend(Color, {\n brighter(k) {\n k = k == null ? brighter : Math.pow(brighter, k);\n return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);\n },\n darker(k) {\n k = k == null ? darker : Math.pow(darker, k);\n return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);\n },\n rgb() {\n return this;\n },\n clamp() {\n return new Rgb(clampi(this.r), clampi(this.g), clampi(this.b), clampa(this.opacity));\n },\n displayable() {\n return (-0.5 <= this.r && this.r < 255.5)\n && (-0.5 <= this.g && this.g < 255.5)\n && (-0.5 <= this.b && this.b < 255.5)\n && (0 <= this.opacity && this.opacity <= 1);\n },\n hex: rgb_formatHex, // Deprecated! Use color.formatHex.\n formatHex: rgb_formatHex,\n formatHex8: rgb_formatHex8,\n formatRgb: rgb_formatRgb,\n toString: rgb_formatRgb\n}));\n\nfunction rgb_formatHex() {\n return `#${hex(this.r)}${hex(this.g)}${hex(this.b)}`;\n}\n\nfunction rgb_formatHex8() {\n return `#${hex(this.r)}${hex(this.g)}${hex(this.b)}${hex((isNaN(this.opacity) ? 1 : this.opacity) * 255)}`;\n}\n\nfunction rgb_formatRgb() {\n const a = clampa(this.opacity);\n return `${a === 1 ? \"rgb(\" : \"rgba(\"}${clampi(this.r)}, ${clampi(this.g)}, ${clampi(this.b)}${a === 1 ? \")\" : `, ${a})`}`;\n}\n\nfunction clampa(opacity) {\n return isNaN(opacity) ? 1 : Math.max(0, Math.min(1, opacity));\n}\n\nfunction clampi(value) {\n return Math.max(0, Math.min(255, Math.round(value) || 0));\n}\n\nfunction hex(value) {\n value = clampi(value);\n return (value < 16 ? \"0\" : \"\") + value.toString(16);\n}\n\nfunction hsla(h, s, l, a) {\n if (a <= 0) h = s = l = NaN;\n else if (l <= 0 || l >= 1) h = s = NaN;\n else if (s <= 0) h = NaN;\n return new Hsl(h, s, l, a);\n}\n\nexport function hslConvert(o) {\n if (o instanceof Hsl) return new Hsl(o.h, o.s, o.l, o.opacity);\n if (!(o instanceof Color)) o = color(o);\n if (!o) return new Hsl;\n if (o instanceof Hsl) return o;\n o = o.rgb();\n var r = o.r / 255,\n g = o.g / 255,\n b = o.b / 255,\n min = Math.min(r, g, b),\n max = Math.max(r, g, b),\n h = NaN,\n s = max - min,\n l = (max + min) / 2;\n if (s) {\n if (r === max) h = (g - b) / s + (g < b) * 6;\n else if (g === max) h = (b - r) / s + 2;\n else h = (r - g) / s + 4;\n s /= l < 0.5 ? max + min : 2 - max - min;\n h *= 60;\n } else {\n s = l > 0 && l < 1 ? 0 : h;\n }\n return new Hsl(h, s, l, o.opacity);\n}\n\nexport function hsl(h, s, l, opacity) {\n return arguments.length === 1 ? hslConvert(h) : new Hsl(h, s, l, opacity == null ? 1 : opacity);\n}\n\nfunction Hsl(h, s, l, opacity) {\n this.h = +h;\n this.s = +s;\n this.l = +l;\n this.opacity = +opacity;\n}\n\ndefine(Hsl, hsl, extend(Color, {\n brighter(k) {\n k = k == null ? brighter : Math.pow(brighter, k);\n return new Hsl(this.h, this.s, this.l * k, this.opacity);\n },\n darker(k) {\n k = k == null ? darker : Math.pow(darker, k);\n return new Hsl(this.h, this.s, this.l * k, this.opacity);\n },\n rgb() {\n var h = this.h % 360 + (this.h < 0) * 360,\n s = isNaN(h) || isNaN(this.s) ? 0 : this.s,\n l = this.l,\n m2 = l + (l < 0.5 ? l : 1 - l) * s,\n m1 = 2 * l - m2;\n return new Rgb(\n hsl2rgb(h >= 240 ? h - 240 : h + 120, m1, m2),\n hsl2rgb(h, m1, m2),\n hsl2rgb(h < 120 ? h + 240 : h - 120, m1, m2),\n this.opacity\n );\n },\n clamp() {\n return new Hsl(clamph(this.h), clampt(this.s), clampt(this.l), clampa(this.opacity));\n },\n displayable() {\n return (0 <= this.s && this.s <= 1 || isNaN(this.s))\n && (0 <= this.l && this.l <= 1)\n && (0 <= this.opacity && this.opacity <= 1);\n },\n formatHsl() {\n const a = clampa(this.opacity);\n return `${a === 1 ? \"hsl(\" : \"hsla(\"}${clamph(this.h)}, ${clampt(this.s) * 100}%, ${clampt(this.l) * 100}%${a === 1 ? \")\" : `, ${a})`}`;\n }\n}));\n\nfunction clamph(value) {\n value = (value || 0) % 360;\n return value < 0 ? value + 360 : value;\n}\n\nfunction clampt(value) {\n return Math.max(0, Math.min(1, value || 0));\n}\n\n/* From FvD 13.37, CSS Color Module Level 3 */\nfunction hsl2rgb(h, m1, m2) {\n return (h < 60 ? m1 + (m2 - m1) * h / 60\n : h < 180 ? m2\n : h < 240 ? m1 + (m2 - m1) * (240 - h) / 60\n : m1) * 255;\n}\n", "export const radians = Math.PI / 180;\nexport const degrees = 180 / Math.PI;\n", "import define, {extend} from \"./define.js\";\nimport {Color, rgbConvert, Rgb} from \"./color.js\";\nimport {degrees, radians} from \"./math.js\";\n\n// https://observablehq.com/@mbostock/lab-and-rgb\nconst K = 18,\n Xn = 0.96422,\n Yn = 1,\n Zn = 0.82521,\n t0 = 4 / 29,\n t1 = 6 / 29,\n t2 = 3 * t1 * t1,\n t3 = t1 * t1 * t1;\n\nfunction labConvert(o) {\n if (o instanceof Lab) return new Lab(o.l, o.a, o.b, o.opacity);\n if (o instanceof Hcl) return hcl2lab(o);\n if (!(o instanceof Rgb)) o = rgbConvert(o);\n var r = rgb2lrgb(o.r),\n g = rgb2lrgb(o.g),\n b = rgb2lrgb(o.b),\n y = xyz2lab((0.2225045 * r + 0.7168786 * g + 0.0606169 * b) / Yn), x, z;\n if (r === g && g === b) x = z = y; else {\n x = xyz2lab((0.4360747 * r + 0.3850649 * g + 0.1430804 * b) / Xn);\n z = xyz2lab((0.0139322 * r + 0.0971045 * g + 0.7141733 * b) / Zn);\n }\n return new Lab(116 * y - 16, 500 * (x - y), 200 * (y - z), o.opacity);\n}\n\nexport function gray(l, opacity) {\n return new Lab(l, 0, 0, opacity == null ? 1 : opacity);\n}\n\nexport default function lab(l, a, b, opacity) {\n return arguments.length === 1 ? labConvert(l) : new Lab(l, a, b, opacity == null ? 1 : opacity);\n}\n\nexport function Lab(l, a, b, opacity) {\n this.l = +l;\n this.a = +a;\n this.b = +b;\n this.opacity = +opacity;\n}\n\ndefine(Lab, lab, extend(Color, {\n brighter(k) {\n return new Lab(this.l + K * (k == null ? 1 : k), this.a, this.b, this.opacity);\n },\n darker(k) {\n return new Lab(this.l - K * (k == null ? 1 : k), this.a, this.b, this.opacity);\n },\n rgb() {\n var y = (this.l + 16) / 116,\n x = isNaN(this.a) ? y : y + this.a / 500,\n z = isNaN(this.b) ? y : y - this.b / 200;\n x = Xn * lab2xyz(x);\n y = Yn * lab2xyz(y);\n z = Zn * lab2xyz(z);\n return new Rgb(\n lrgb2rgb( 3.1338561 * x - 1.6168667 * y - 0.4906146 * z),\n lrgb2rgb(-0.9787684 * x + 1.9161415 * y + 0.0334540 * z),\n lrgb2rgb( 0.0719453 * x - 0.2289914 * y + 1.4052427 * z),\n this.opacity\n );\n }\n}));\n\nfunction xyz2lab(t) {\n return t > t3 ? Math.pow(t, 1 / 3) : t / t2 + t0;\n}\n\nfunction lab2xyz(t) {\n return t > t1 ? t * t * t : t2 * (t - t0);\n}\n\nfunction lrgb2rgb(x) {\n return 255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055);\n}\n\nfunction rgb2lrgb(x) {\n return (x /= 255) <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);\n}\n\nfunction hclConvert(o) {\n if (o instanceof Hcl) return new Hcl(o.h, o.c, o.l, o.opacity);\n if (!(o instanceof Lab)) o = labConvert(o);\n if (o.a === 0 && o.b === 0) return new Hcl(NaN, 0 < o.l && o.l < 100 ? 0 : NaN, o.l, o.opacity);\n var h = Math.atan2(o.b, o.a) * degrees;\n return new Hcl(h < 0 ? h + 360 : h, Math.sqrt(o.a * o.a + o.b * o.b), o.l, o.opacity);\n}\n\nexport function lch(l, c, h, opacity) {\n return arguments.length === 1 ? hclConvert(l) : new Hcl(h, c, l, opacity == null ? 1 : opacity);\n}\n\nexport function hcl(h, c, l, opacity) {\n return arguments.length === 1 ? hclConvert(h) : new Hcl(h, c, l, opacity == null ? 1 : opacity);\n}\n\nexport function Hcl(h, c, l, opacity) {\n this.h = +h;\n this.c = +c;\n this.l = +l;\n this.opacity = +opacity;\n}\n\nfunction hcl2lab(o) {\n if (isNaN(o.h)) return new Lab(o.l, 0, 0, o.opacity);\n var h = o.h * radians;\n return new Lab(o.l, Math.cos(h) * o.c, Math.sin(h) * o.c, o.opacity);\n}\n\ndefine(Hcl, hcl, extend(Color, {\n brighter(k) {\n return new Hcl(this.h, this.c, this.l + K * (k == null ? 1 : k), this.opacity);\n },\n darker(k) {\n return new Hcl(this.h, this.c, this.l - K * (k == null ? 1 : k), this.opacity);\n },\n rgb() {\n return hcl2lab(this).rgb();\n }\n}));\n", "import define, {extend} from \"./define.js\";\nimport {Color, rgbConvert, Rgb, darker, brighter} from \"./color.js\";\nimport {degrees, radians} from \"./math.js\";\n\nvar A = -0.14861,\n B = +1.78277,\n C = -0.29227,\n D = -0.90649,\n E = +1.97294,\n ED = E * D,\n EB = E * B,\n BC_DA = B * C - D * A;\n\nfunction cubehelixConvert(o) {\n if (o instanceof Cubehelix) return new Cubehelix(o.h, o.s, o.l, o.opacity);\n if (!(o instanceof Rgb)) o = rgbConvert(o);\n var r = o.r / 255,\n g = o.g / 255,\n b = o.b / 255,\n l = (BC_DA * b + ED * r - EB * g) / (BC_DA + ED - EB),\n bl = b - l,\n k = (E * (g - l) - C * bl) / D,\n s = Math.sqrt(k * k + bl * bl) / (E * l * (1 - l)), // NaN if l=0 or l=1\n h = s ? Math.atan2(k, bl) * degrees - 120 : NaN;\n return new Cubehelix(h < 0 ? h + 360 : h, s, l, o.opacity);\n}\n\nexport default function cubehelix(h, s, l, opacity) {\n return arguments.length === 1 ? cubehelixConvert(h) : new Cubehelix(h, s, l, opacity == null ? 1 : opacity);\n}\n\nexport function Cubehelix(h, s, l, opacity) {\n this.h = +h;\n this.s = +s;\n this.l = +l;\n this.opacity = +opacity;\n}\n\ndefine(Cubehelix, cubehelix, extend(Color, {\n brighter(k) {\n k = k == null ? brighter : Math.pow(brighter, k);\n return new Cubehelix(this.h, this.s, this.l * k, this.opacity);\n },\n darker(k) {\n k = k == null ? darker : Math.pow(darker, k);\n return new Cubehelix(this.h, this.s, this.l * k, this.opacity);\n },\n rgb() {\n var h = isNaN(this.h) ? 0 : (this.h + 120) * radians,\n l = +this.l,\n a = isNaN(this.s) ? 0 : this.s * l * (1 - l),\n cosh = Math.cos(h),\n sinh = Math.sin(h);\n return new Rgb(\n 255 * (l + a * (A * cosh + B * sinh)),\n 255 * (l + a * (C * cosh + D * sinh)),\n 255 * (l + a * (E * cosh)),\n this.opacity\n );\n }\n}));\n", "export {default as color, rgb, hsl} from \"./color.js\";\nexport {default as lab, hcl, lch, gray} from \"./lab.js\";\nexport {default as cubehelix} from \"./cubehelix.js\";\n", "export function basis(t1, v0, v1, v2, v3) {\n var t2 = t1 * t1, t3 = t2 * t1;\n return ((1 - 3 * t1 + 3 * t2 - t3) * v0\n + (4 - 6 * t2 + 3 * t3) * v1\n + (1 + 3 * t1 + 3 * t2 - 3 * t3) * v2\n + t3 * v3) / 6;\n}\n\nexport default function(values) {\n var n = values.length - 1;\n return function(t) {\n var i = t <= 0 ? (t = 0) : t >= 1 ? (t = 1, n - 1) : Math.floor(t * n),\n v1 = values[i],\n v2 = values[i + 1],\n v0 = i > 0 ? values[i - 1] : 2 * v1 - v2,\n v3 = i < n - 1 ? values[i + 2] : 2 * v2 - v1;\n return basis((t - i / n) * n, v0, v1, v2, v3);\n };\n}\n", "import {basis} from \"./basis.js\";\n\nexport default function(values) {\n var n = values.length;\n return function(t) {\n var i = Math.floor(((t %= 1) < 0 ? ++t : t) * n),\n v0 = values[(i + n - 1) % n],\n v1 = values[i % n],\n v2 = values[(i + 1) % n],\n v3 = values[(i + 2) % n];\n return basis((t - i / n) * n, v0, v1, v2, v3);\n };\n}\n", "export default x => () => x;\n", "import constant from \"./constant.js\";\n\nfunction linear(a, d) {\n return function(t) {\n return a + t * d;\n };\n}\n\nfunction exponential(a, b, y) {\n return a = Math.pow(a, y), b = Math.pow(b, y) - a, y = 1 / y, function(t) {\n return Math.pow(a + t * b, y);\n };\n}\n\nexport function hue(a, b) {\n var d = b - a;\n return d ? linear(a, d > 180 || d < -180 ? d - 360 * Math.round(d / 360) : d) : constant(isNaN(a) ? b : a);\n}\n\nexport function gamma(y) {\n return (y = +y) === 1 ? nogamma : function(a, b) {\n return b - a ? exponential(a, b, y) : constant(isNaN(a) ? b : a);\n };\n}\n\nexport default function nogamma(a, b) {\n var d = b - a;\n return d ? linear(a, d) : constant(isNaN(a) ? b : a);\n}\n", "import {rgb as colorRgb} from \"d3-color\";\nimport basis from \"./basis.js\";\nimport basisClosed from \"./basisClosed.js\";\nimport nogamma, {gamma} from \"./color.js\";\n\nexport default (function rgbGamma(y) {\n var color = gamma(y);\n\n function rgb(start, end) {\n var r = color((start = colorRgb(start)).r, (end = colorRgb(end)).r),\n g = color(start.g, end.g),\n b = color(start.b, end.b),\n opacity = nogamma(start.opacity, end.opacity);\n return function(t) {\n start.r = r(t);\n start.g = g(t);\n start.b = b(t);\n start.opacity = opacity(t);\n return start + \"\";\n };\n }\n\n rgb.gamma = rgbGamma;\n\n return rgb;\n})(1);\n\nfunction rgbSpline(spline) {\n return function(colors) {\n var n = colors.length,\n r = new Array(n),\n g = new Array(n),\n b = new Array(n),\n i, color;\n for (i = 0; i < n; ++i) {\n color = colorRgb(colors[i]);\n r[i] = color.r || 0;\n g[i] = color.g || 0;\n b[i] = color.b || 0;\n }\n r = spline(r);\n g = spline(g);\n b = spline(b);\n color.opacity = 1;\n return function(t) {\n color.r = r(t);\n color.g = g(t);\n color.b = b(t);\n return color + \"\";\n };\n };\n}\n\nexport var rgbBasis = rgbSpline(basis);\nexport var rgbBasisClosed = rgbSpline(basisClosed);\n", "export default function(a, b) {\n if (!b) b = [];\n var n = a ? Math.min(b.length, a.length) : 0,\n c = b.slice(),\n i;\n return function(t) {\n for (i = 0; i < n; ++i) c[i] = a[i] * (1 - t) + b[i] * t;\n return c;\n };\n}\n\nexport function isNumberArray(x) {\n return ArrayBuffer.isView(x) && !(x instanceof DataView);\n}\n", "import value from \"./value.js\";\nimport numberArray, {isNumberArray} from \"./numberArray.js\";\n\nexport default function(a, b) {\n return (isNumberArray(b) ? numberArray : genericArray)(a, b);\n}\n\nexport function genericArray(a, b) {\n var nb = b ? b.length : 0,\n na = a ? Math.min(nb, a.length) : 0,\n x = new Array(na),\n c = new Array(nb),\n i;\n\n for (i = 0; i < na; ++i) x[i] = value(a[i], b[i]);\n for (; i < nb; ++i) c[i] = b[i];\n\n return function(t) {\n for (i = 0; i < na; ++i) c[i] = x[i](t);\n return c;\n };\n}\n", "export default function(a, b) {\n var d = new Date;\n return a = +a, b = +b, function(t) {\n return d.setTime(a * (1 - t) + b * t), d;\n };\n}\n", "export default function(a, b) {\n return a = +a, b = +b, function(t) {\n return a * (1 - t) + b * t;\n };\n}\n", "import value from \"./value.js\";\n\nexport default function(a, b) {\n var i = {},\n c = {},\n k;\n\n if (a === null || typeof a !== \"object\") a = {};\n if (b === null || typeof b !== \"object\") b = {};\n\n for (k in b) {\n if (k in a) {\n i[k] = value(a[k], b[k]);\n } else {\n c[k] = b[k];\n }\n }\n\n return function(t) {\n for (k in i) c[k] = i[k](t);\n return c;\n };\n}\n", "import number from \"./number.js\";\n\nvar reA = /[-+]?(?:\\d+\\.?\\d*|\\.?\\d+)(?:[eE][-+]?\\d+)?/g,\n reB = new RegExp(reA.source, \"g\");\n\nfunction zero(b) {\n return function() {\n return b;\n };\n}\n\nfunction one(b) {\n return function(t) {\n return b(t) + \"\";\n };\n}\n\nexport default function(a, b) {\n var bi = reA.lastIndex = reB.lastIndex = 0, // scan index for next number in b\n am, // current match in a\n bm, // current match in b\n bs, // string preceding current number in b, if any\n i = -1, // index in s\n s = [], // string constants and placeholders\n q = []; // number interpolators\n\n // Coerce inputs to strings.\n a = a + \"\", b = b + \"\";\n\n // Interpolate pairs of numbers in a & b.\n while ((am = reA.exec(a))\n && (bm = reB.exec(b))) {\n if ((bs = bm.index) > bi) { // a string precedes the next number in b\n bs = b.slice(bi, bs);\n if (s[i]) s[i] += bs; // coalesce with previous string\n else s[++i] = bs;\n }\n if ((am = am[0]) === (bm = bm[0])) { // numbers in a & b match\n if (s[i]) s[i] += bm; // coalesce with previous string\n else s[++i] = bm;\n } else { // interpolate non-matching numbers\n s[++i] = null;\n q.push({i: i, x: number(am, bm)});\n }\n bi = reB.lastIndex;\n }\n\n // Add remains of b.\n if (bi < b.length) {\n bs = b.slice(bi);\n if (s[i]) s[i] += bs; // coalesce with previous string\n else s[++i] = bs;\n }\n\n // Special optimization for only a single match.\n // Otherwise, interpolate each of the numbers and rejoin the string.\n return s.length < 2 ? (q[0]\n ? one(q[0].x)\n : zero(b))\n : (b = q.length, function(t) {\n for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t);\n return s.join(\"\");\n });\n}\n", "import {color} from \"d3-color\";\nimport rgb from \"./rgb.js\";\nimport {genericArray} from \"./array.js\";\nimport date from \"./date.js\";\nimport number from \"./number.js\";\nimport object from \"./object.js\";\nimport string from \"./string.js\";\nimport constant from \"./constant.js\";\nimport numberArray, {isNumberArray} from \"./numberArray.js\";\n\nexport default function(a, b) {\n var t = typeof b, c;\n return b == null || t === \"boolean\" ? constant(b)\n : (t === \"number\" ? number\n : t === \"string\" ? ((c = color(b)) ? (b = c, rgb) : string)\n : b instanceof color ? rgb\n : b instanceof Date ? date\n : isNumberArray(b) ? numberArray\n : Array.isArray(b) ? genericArray\n : typeof b.valueOf !== \"function\" && typeof b.toString !== \"function\" || isNaN(b) ? object\n : number)(a, b);\n}\n", "export default function(range) {\n var n = range.length;\n return function(t) {\n return range[Math.max(0, Math.min(n - 1, Math.floor(t * n)))];\n };\n}\n", "import {hue} from \"./color.js\";\n\nexport default function(a, b) {\n var i = hue(+a, +b);\n return function(t) {\n var x = i(t);\n return x - 360 * Math.floor(x / 360);\n };\n}\n", "export default function(a, b) {\n return a = +a, b = +b, function(t) {\n return Math.round(a * (1 - t) + b * t);\n };\n}\n", "var degrees = 180 / Math.PI;\n\nexport var identity = {\n translateX: 0,\n translateY: 0,\n rotate: 0,\n skewX: 0,\n scaleX: 1,\n scaleY: 1\n};\n\nexport default function(a, b, c, d, e, f) {\n var scaleX, scaleY, skewX;\n if (scaleX = Math.sqrt(a * a + b * b)) a /= scaleX, b /= scaleX;\n if (skewX = a * c + b * d) c -= a * skewX, d -= b * skewX;\n if (scaleY = Math.sqrt(c * c + d * d)) c /= scaleY, d /= scaleY, skewX /= scaleY;\n if (a * d < b * c) a = -a, b = -b, skewX = -skewX, scaleX = -scaleX;\n return {\n translateX: e,\n translateY: f,\n rotate: Math.atan2(b, a) * degrees,\n skewX: Math.atan(skewX) * degrees,\n scaleX: scaleX,\n scaleY: scaleY\n };\n}\n", "import decompose, {identity} from \"./decompose.js\";\n\nvar svgNode;\n\n/* eslint-disable no-undef */\nexport function parseCss(value) {\n const m = new (typeof DOMMatrix === \"function\" ? DOMMatrix : WebKitCSSMatrix)(value + \"\");\n return m.isIdentity ? identity : decompose(m.a, m.b, m.c, m.d, m.e, m.f);\n}\n\nexport function parseSvg(value) {\n if (value == null) return identity;\n if (!svgNode) svgNode = document.createElementNS(\"http://www.w3.org/2000/svg\", \"g\");\n svgNode.setAttribute(\"transform\", value);\n if (!(value = svgNode.transform.baseVal.consolidate())) return identity;\n value = value.matrix;\n return decompose(value.a, value.b, value.c, value.d, value.e, value.f);\n}\n", "import number from \"../number.js\";\nimport {parseCss, parseSvg} from \"./parse.js\";\n\nfunction interpolateTransform(parse, pxComma, pxParen, degParen) {\n\n function pop(s) {\n return s.length ? s.pop() + \" \" : \"\";\n }\n\n function translate(xa, ya, xb, yb, s, q) {\n if (xa !== xb || ya !== yb) {\n var i = s.push(\"translate(\", null, pxComma, null, pxParen);\n q.push({i: i - 4, x: number(xa, xb)}, {i: i - 2, x: number(ya, yb)});\n } else if (xb || yb) {\n s.push(\"translate(\" + xb + pxComma + yb + pxParen);\n }\n }\n\n function rotate(a, b, s, q) {\n if (a !== b) {\n if (a - b > 180) b += 360; else if (b - a > 180) a += 360; // shortest path\n q.push({i: s.push(pop(s) + \"rotate(\", null, degParen) - 2, x: number(a, b)});\n } else if (b) {\n s.push(pop(s) + \"rotate(\" + b + degParen);\n }\n }\n\n function skewX(a, b, s, q) {\n if (a !== b) {\n q.push({i: s.push(pop(s) + \"skewX(\", null, degParen) - 2, x: number(a, b)});\n } else if (b) {\n s.push(pop(s) + \"skewX(\" + b + degParen);\n }\n }\n\n function scale(xa, ya, xb, yb, s, q) {\n if (xa !== xb || ya !== yb) {\n var i = s.push(pop(s) + \"scale(\", null, \",\", null, \")\");\n q.push({i: i - 4, x: number(xa, xb)}, {i: i - 2, x: number(ya, yb)});\n } else if (xb !== 1 || yb !== 1) {\n s.push(pop(s) + \"scale(\" + xb + \",\" + yb + \")\");\n }\n }\n\n return function(a, b) {\n var s = [], // string constants and placeholders\n q = []; // number interpolators\n a = parse(a), b = parse(b);\n translate(a.translateX, a.translateY, b.translateX, b.translateY, s, q);\n rotate(a.rotate, b.rotate, s, q);\n skewX(a.skewX, b.skewX, s, q);\n scale(a.scaleX, a.scaleY, b.scaleX, b.scaleY, s, q);\n a = b = null; // gc\n return function(t) {\n var i = -1, n = q.length, o;\n while (++i < n) s[(o = q[i]).i] = o.x(t);\n return s.join(\"\");\n };\n };\n}\n\nexport var interpolateTransformCss = interpolateTransform(parseCss, \"px, \", \"px)\", \"deg)\");\nexport var interpolateTransformSvg = interpolateTransform(parseSvg, \", \", \")\", \")\");\n", "var epsilon2 = 1e-12;\n\nfunction cosh(x) {\n return ((x = Math.exp(x)) + 1 / x) / 2;\n}\n\nfunction sinh(x) {\n return ((x = Math.exp(x)) - 1 / x) / 2;\n}\n\nfunction tanh(x) {\n return ((x = Math.exp(2 * x)) - 1) / (x + 1);\n}\n\nexport default (function zoomRho(rho, rho2, rho4) {\n\n // p0 = [ux0, uy0, w0]\n // p1 = [ux1, uy1, w1]\n function zoom(p0, p1) {\n var ux0 = p0[0], uy0 = p0[1], w0 = p0[2],\n ux1 = p1[0], uy1 = p1[1], w1 = p1[2],\n dx = ux1 - ux0,\n dy = uy1 - uy0,\n d2 = dx * dx + dy * dy,\n i,\n S;\n\n // Special case for u0 \u2245 u1.\n if (d2 < epsilon2) {\n S = Math.log(w1 / w0) / rho;\n i = function(t) {\n return [\n ux0 + t * dx,\n uy0 + t * dy,\n w0 * Math.exp(rho * t * S)\n ];\n }\n }\n\n // General case.\n else {\n var d1 = Math.sqrt(d2),\n b0 = (w1 * w1 - w0 * w0 + rho4 * d2) / (2 * w0 * rho2 * d1),\n b1 = (w1 * w1 - w0 * w0 - rho4 * d2) / (2 * w1 * rho2 * d1),\n r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0),\n r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1);\n S = (r1 - r0) / rho;\n i = function(t) {\n var s = t * S,\n coshr0 = cosh(r0),\n u = w0 / (rho2 * d1) * (coshr0 * tanh(rho * s + r0) - sinh(r0));\n return [\n ux0 + u * dx,\n uy0 + u * dy,\n w0 * coshr0 / cosh(rho * s + r0)\n ];\n }\n }\n\n i.duration = S * 1000 * rho / Math.SQRT2;\n\n return i;\n }\n\n zoom.rho = function(_) {\n var _1 = Math.max(1e-3, +_), _2 = _1 * _1, _4 = _2 * _2;\n return zoomRho(_1, _2, _4);\n };\n\n return zoom;\n})(Math.SQRT2, 2, 4);\n", "import {hsl as colorHsl} from \"d3-color\";\nimport color, {hue} from \"./color.js\";\n\nfunction hsl(hue) {\n return function(start, end) {\n var h = hue((start = colorHsl(start)).h, (end = colorHsl(end)).h),\n s = color(start.s, end.s),\n l = color(start.l, end.l),\n opacity = color(start.opacity, end.opacity);\n return function(t) {\n start.h = h(t);\n start.s = s(t);\n start.l = l(t);\n start.opacity = opacity(t);\n return start + \"\";\n };\n }\n}\n\nexport default hsl(hue);\nexport var hslLong = hsl(color);\n", "import {lab as colorLab} from \"d3-color\";\nimport color from \"./color.js\";\n\nexport default function lab(start, end) {\n var l = color((start = colorLab(start)).l, (end = colorLab(end)).l),\n a = color(start.a, end.a),\n b = color(start.b, end.b),\n opacity = color(start.opacity, end.opacity);\n return function(t) {\n start.l = l(t);\n start.a = a(t);\n start.b = b(t);\n start.opacity = opacity(t);\n return start + \"\";\n };\n}\n", "import {hcl as colorHcl} from \"d3-color\";\nimport color, {hue} from \"./color.js\";\n\nfunction hcl(hue) {\n return function(start, end) {\n var h = hue((start = colorHcl(start)).h, (end = colorHcl(end)).h),\n c = color(start.c, end.c),\n l = color(start.l, end.l),\n opacity = color(start.opacity, end.opacity);\n return function(t) {\n start.h = h(t);\n start.c = c(t);\n start.l = l(t);\n start.opacity = opacity(t);\n return start + \"\";\n };\n }\n}\n\nexport default hcl(hue);\nexport var hclLong = hcl(color);\n", "import {cubehelix as colorCubehelix} from \"d3-color\";\nimport color, {hue} from \"./color.js\";\n\nfunction cubehelix(hue) {\n return (function cubehelixGamma(y) {\n y = +y;\n\n function cubehelix(start, end) {\n var h = hue((start = colorCubehelix(start)).h, (end = colorCubehelix(end)).h),\n s = color(start.s, end.s),\n l = color(start.l, end.l),\n opacity = color(start.opacity, end.opacity);\n return function(t) {\n start.h = h(t);\n start.s = s(t);\n start.l = l(Math.pow(t, y));\n start.opacity = opacity(t);\n return start + \"\";\n };\n }\n\n cubehelix.gamma = cubehelixGamma;\n\n return cubehelix;\n })(1);\n}\n\nexport default cubehelix(hue);\nexport var cubehelixLong = cubehelix(color);\n", "import {default as value} from \"./value.js\";\n\nexport default function piecewise(interpolate, values) {\n if (values === undefined) values = interpolate, interpolate = value;\n var i = 0, n = values.length - 1, v = values[0], I = new Array(n < 0 ? 0 : n);\n while (i < n) I[i] = interpolate(v, v = values[++i]);\n return function(t) {\n var i = Math.max(0, Math.min(n - 1, Math.floor(t *= n)));\n return I[i](t - i);\n };\n}\n", "export default function(interpolator, n) {\n var samples = new Array(n);\n for (var i = 0; i < n; ++i) samples[i] = interpolator(i / (n - 1));\n return samples;\n}\n", "export {default as interpolate} from \"./value.js\";\nexport {default as interpolateArray} from \"./array.js\";\nexport {default as interpolateBasis} from \"./basis.js\";\nexport {default as interpolateBasisClosed} from \"./basisClosed.js\";\nexport {default as interpolateDate} from \"./date.js\";\nexport {default as interpolateDiscrete} from \"./discrete.js\";\nexport {default as interpolateHue} from \"./hue.js\";\nexport {default as interpolateNumber} from \"./number.js\";\nexport {default as interpolateNumberArray} from \"./numberArray.js\";\nexport {default as interpolateObject} from \"./object.js\";\nexport {default as interpolateRound} from \"./round.js\";\nexport {default as interpolateString} from \"./string.js\";\nexport {interpolateTransformCss, interpolateTransformSvg} from \"./transform/index.js\";\nexport {default as interpolateZoom} from \"./zoom.js\";\nexport {default as interpolateRgb, rgbBasis as interpolateRgbBasis, rgbBasisClosed as interpolateRgbBasisClosed} from \"./rgb.js\";\nexport {default as interpolateHsl, hslLong as interpolateHslLong} from \"./hsl.js\";\nexport {default as interpolateLab} from \"./lab.js\";\nexport {default as interpolateHcl, hclLong as interpolateHclLong} from \"./hcl.js\";\nexport {default as interpolateCubehelix, cubehelixLong as interpolateCubehelixLong} from \"./cubehelix.js\";\nexport {default as piecewise} from \"./piecewise.js\";\nexport {default as quantize} from \"./quantize.js\";\n", "var frame = 0, // is an animation frame pending?\n timeout = 0, // is a timeout pending?\n interval = 0, // are any timers active?\n pokeDelay = 1000, // how frequently we check for clock skew\n taskHead,\n taskTail,\n clockLast = 0,\n clockNow = 0,\n clockSkew = 0,\n clock = typeof performance === \"object\" && performance.now ? performance : Date,\n setFrame = typeof window === \"object\" && window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : function(f) { setTimeout(f, 17); };\n\nexport function now() {\n return clockNow || (setFrame(clearNow), clockNow = clock.now() + clockSkew);\n}\n\nfunction clearNow() {\n clockNow = 0;\n}\n\nexport function Timer() {\n this._call =\n this._time =\n this._next = null;\n}\n\nTimer.prototype = timer.prototype = {\n constructor: Timer,\n restart: function(callback, delay, time) {\n if (typeof callback !== \"function\") throw new TypeError(\"callback is not a function\");\n time = (time == null ? now() : +time) + (delay == null ? 0 : +delay);\n if (!this._next && taskTail !== this) {\n if (taskTail) taskTail._next = this;\n else taskHead = this;\n taskTail = this;\n }\n this._call = callback;\n this._time = time;\n sleep();\n },\n stop: function() {\n if (this._call) {\n this._call = null;\n this._time = Infinity;\n sleep();\n }\n }\n};\n\nexport function timer(callback, delay, time) {\n var t = new Timer;\n t.restart(callback, delay, time);\n return t;\n}\n\nexport function timerFlush() {\n now(); // Get the current time, if not already set.\n ++frame; // Pretend we\u2019ve set an alarm, if we haven\u2019t already.\n var t = taskHead, e;\n while (t) {\n if ((e = clockNow - t._time) >= 0) t._call.call(undefined, e);\n t = t._next;\n }\n --frame;\n}\n\nfunction wake() {\n clockNow = (clockLast = clock.now()) + clockSkew;\n frame = timeout = 0;\n try {\n timerFlush();\n } finally {\n frame = 0;\n nap();\n clockNow = 0;\n }\n}\n\nfunction poke() {\n var now = clock.now(), delay = now - clockLast;\n if (delay > pokeDelay) clockSkew -= delay, clockLast = now;\n}\n\nfunction nap() {\n var t0, t1 = taskHead, t2, time = Infinity;\n while (t1) {\n if (t1._call) {\n if (time > t1._time) time = t1._time;\n t0 = t1, t1 = t1._next;\n } else {\n t2 = t1._next, t1._next = null;\n t1 = t0 ? t0._next = t2 : taskHead = t2;\n }\n }\n taskTail = t0;\n sleep(time);\n}\n\nfunction sleep(time) {\n if (frame) return; // Soonest alarm already set, or will be.\n if (timeout) timeout = clearTimeout(timeout);\n var delay = time - clockNow; // Strictly less than if we recomputed clockNow.\n if (delay > 24) {\n if (time < Infinity) timeout = setTimeout(wake, time - clock.now() - clockSkew);\n if (interval) interval = clearInterval(interval);\n } else {\n if (!interval) clockLast = clock.now(), interval = setInterval(poke, pokeDelay);\n frame = 1, setFrame(wake);\n }\n}\n", "import {Timer} from \"./timer.js\";\n\nexport default function(callback, delay, time) {\n var t = new Timer;\n delay = delay == null ? 0 : +delay;\n t.restart(elapsed => {\n t.stop();\n callback(elapsed + delay);\n }, delay, time);\n return t;\n}\n", "import {Timer, now} from \"./timer.js\";\n\nexport default function(callback, delay, time) {\n var t = new Timer, total = delay;\n if (delay == null) return t.restart(callback, delay, time), t;\n t._restart = t.restart;\n t.restart = function(callback, delay, time) {\n delay = +delay, time = time == null ? now() : +time;\n t._restart(function tick(elapsed) {\n elapsed += total;\n t._restart(tick, total += delay, time);\n callback(elapsed);\n }, delay, time);\n }\n t.restart(callback, delay, time);\n return t;\n}\n", "export {\n now,\n timer,\n timerFlush\n} from \"./timer.js\";\n\nexport {\n default as timeout\n} from \"./timeout.js\";\n\nexport {\n default as interval\n} from \"./interval.js\";\n", "import {dispatch} from \"d3-dispatch\";\nimport {timer, timeout} from \"d3-timer\";\n\nvar emptyOn = dispatch(\"start\", \"end\", \"cancel\", \"interrupt\");\nvar emptyTween = [];\n\nexport var CREATED = 0;\nexport var SCHEDULED = 1;\nexport var STARTING = 2;\nexport var STARTED = 3;\nexport var RUNNING = 4;\nexport var ENDING = 5;\nexport var ENDED = 6;\n\nexport default function(node, name, id, index, group, timing) {\n var schedules = node.__transition;\n if (!schedules) node.__transition = {};\n else if (id in schedules) return;\n create(node, id, {\n name: name,\n index: index, // For context during callback.\n group: group, // For context during callback.\n on: emptyOn,\n tween: emptyTween,\n time: timing.time,\n delay: timing.delay,\n duration: timing.duration,\n ease: timing.ease,\n timer: null,\n state: CREATED\n });\n}\n\nexport function init(node, id) {\n var schedule = get(node, id);\n if (schedule.state > CREATED) throw new Error(\"too late; already scheduled\");\n return schedule;\n}\n\nexport function set(node, id) {\n var schedule = get(node, id);\n if (schedule.state > STARTED) throw new Error(\"too late; already running\");\n return schedule;\n}\n\nexport function get(node, id) {\n var schedule = node.__transition;\n if (!schedule || !(schedule = schedule[id])) throw new Error(\"transition not found\");\n return schedule;\n}\n\nfunction create(node, id, self) {\n var schedules = node.__transition,\n tween;\n\n // Initialize the self timer when the transition is created.\n // Note the actual delay is not known until the first callback!\n schedules[id] = self;\n self.timer = timer(schedule, 0, self.time);\n\n function schedule(elapsed) {\n self.state = SCHEDULED;\n self.timer.restart(start, self.delay, self.time);\n\n // If the elapsed delay is less than our first sleep, start immediately.\n if (self.delay <= elapsed) start(elapsed - self.delay);\n }\n\n function start(elapsed) {\n var i, j, n, o;\n\n // If the state is not SCHEDULED, then we previously errored on start.\n if (self.state !== SCHEDULED) return stop();\n\n for (i in schedules) {\n o = schedules[i];\n if (o.name !== self.name) continue;\n\n // While this element already has a starting transition during this frame,\n // defer starting an interrupting transition until that transition has a\n // chance to tick (and possibly end); see d3/d3-transition#54!\n if (o.state === STARTED) return timeout(start);\n\n // Interrupt the active transition, if any.\n if (o.state === RUNNING) {\n o.state = ENDED;\n o.timer.stop();\n o.on.call(\"interrupt\", node, node.__data__, o.index, o.group);\n delete schedules[i];\n }\n\n // Cancel any pre-empted transitions.\n else if (+i < id) {\n o.state = ENDED;\n o.timer.stop();\n o.on.call(\"cancel\", node, node.__data__, o.index, o.group);\n delete schedules[i];\n }\n }\n\n // Defer the first tick to end of the current frame; see d3/d3#1576.\n // Note the transition may be canceled after start and before the first tick!\n // Note this must be scheduled before the start event; see d3/d3-transition#16!\n // Assuming this is successful, subsequent callbacks go straight to tick.\n timeout(function() {\n if (self.state === STARTED) {\n self.state = RUNNING;\n self.timer.restart(tick, self.delay, self.time);\n tick(elapsed);\n }\n });\n\n // Dispatch the start event.\n // Note this must be done before the tween are initialized.\n self.state = STARTING;\n self.on.call(\"start\", node, node.__data__, self.index, self.group);\n if (self.state !== STARTING) return; // interrupted\n self.state = STARTED;\n\n // Initialize the tween, deleting null tween.\n tween = new Array(n = self.tween.length);\n for (i = 0, j = -1; i < n; ++i) {\n if (o = self.tween[i].value.call(node, node.__data__, self.index, self.group)) {\n tween[++j] = o;\n }\n }\n tween.length = j + 1;\n }\n\n function tick(elapsed) {\n var t = elapsed < self.duration ? self.ease.call(null, elapsed / self.duration) : (self.timer.restart(stop), self.state = ENDING, 1),\n i = -1,\n n = tween.length;\n\n while (++i < n) {\n tween[i].call(node, t);\n }\n\n // Dispatch the end event.\n if (self.state === ENDING) {\n self.on.call(\"end\", node, node.__data__, self.index, self.group);\n stop();\n }\n }\n\n function stop() {\n self.state = ENDED;\n self.timer.stop();\n delete schedules[id];\n for (var i in schedules) return; // eslint-disable-line no-unused-vars\n delete node.__transition;\n }\n}\n", "import {STARTING, ENDING, ENDED} from \"./transition/schedule.js\";\n\nexport default function(node, name) {\n var schedules = node.__transition,\n schedule,\n active,\n empty = true,\n i;\n\n if (!schedules) return;\n\n name = name == null ? null : name + \"\";\n\n for (i in schedules) {\n if ((schedule = schedules[i]).name !== name) { empty = false; continue; }\n active = schedule.state > STARTING && schedule.state < ENDING;\n schedule.state = ENDED;\n schedule.timer.stop();\n schedule.on.call(active ? \"interrupt\" : \"cancel\", node, node.__data__, schedule.index, schedule.group);\n delete schedules[i];\n }\n\n if (empty) delete node.__transition;\n}\n", "import interrupt from \"../interrupt.js\";\n\nexport default function(name) {\n return this.each(function() {\n interrupt(this, name);\n });\n}\n", "import {get, set} from \"./schedule.js\";\n\nfunction tweenRemove(id, name) {\n var tween0, tween1;\n return function() {\n var schedule = set(this, id),\n tween = schedule.tween;\n\n // If this node shared tween with the previous node,\n // just assign the updated shared tween and we\u2019re done!\n // Otherwise, copy-on-write.\n if (tween !== tween0) {\n tween1 = tween0 = tween;\n for (var i = 0, n = tween1.length; i < n; ++i) {\n if (tween1[i].name === name) {\n tween1 = tween1.slice();\n tween1.splice(i, 1);\n break;\n }\n }\n }\n\n schedule.tween = tween1;\n };\n}\n\nfunction tweenFunction(id, name, value) {\n var tween0, tween1;\n if (typeof value !== \"function\") throw new Error;\n return function() {\n var schedule = set(this, id),\n tween = schedule.tween;\n\n // If this node shared tween with the previous node,\n // just assign the updated shared tween and we\u2019re done!\n // Otherwise, copy-on-write.\n if (tween !== tween0) {\n tween1 = (tween0 = tween).slice();\n for (var t = {name: name, value: value}, i = 0, n = tween1.length; i < n; ++i) {\n if (tween1[i].name === name) {\n tween1[i] = t;\n break;\n }\n }\n if (i === n) tween1.push(t);\n }\n\n schedule.tween = tween1;\n };\n}\n\nexport default function(name, value) {\n var id = this._id;\n\n name += \"\";\n\n if (arguments.length < 2) {\n var tween = get(this.node(), id).tween;\n for (var i = 0, n = tween.length, t; i < n; ++i) {\n if ((t = tween[i]).name === name) {\n return t.value;\n }\n }\n return null;\n }\n\n return this.each((value == null ? tweenRemove : tweenFunction)(id, name, value));\n}\n\nexport function tweenValue(transition, name, value) {\n var id = transition._id;\n\n transition.each(function() {\n var schedule = set(this, id);\n (schedule.value || (schedule.value = {}))[name] = value.apply(this, arguments);\n });\n\n return function(node) {\n return get(node, id).value[name];\n };\n}\n", "import {color} from \"d3-color\";\nimport {interpolateNumber, interpolateRgb, interpolateString} from \"d3-interpolate\";\n\nexport default function(a, b) {\n var c;\n return (typeof b === \"number\" ? interpolateNumber\n : b instanceof color ? interpolateRgb\n : (c = color(b)) ? (b = c, interpolateRgb)\n : interpolateString)(a, b);\n}\n", "import {interpolateTransformSvg as interpolateTransform} from \"d3-interpolate\";\nimport {namespace} from \"d3-selection\";\nimport {tweenValue} from \"./tween.js\";\nimport interpolate from \"./interpolate.js\";\n\nfunction attrRemove(name) {\n return function() {\n this.removeAttribute(name);\n };\n}\n\nfunction attrRemoveNS(fullname) {\n return function() {\n this.removeAttributeNS(fullname.space, fullname.local);\n };\n}\n\nfunction attrConstant(name, interpolate, value1) {\n var string00,\n string1 = value1 + \"\",\n interpolate0;\n return function() {\n var string0 = this.getAttribute(name);\n return string0 === string1 ? null\n : string0 === string00 ? interpolate0\n : interpolate0 = interpolate(string00 = string0, value1);\n };\n}\n\nfunction attrConstantNS(fullname, interpolate, value1) {\n var string00,\n string1 = value1 + \"\",\n interpolate0;\n return function() {\n var string0 = this.getAttributeNS(fullname.space, fullname.local);\n return string0 === string1 ? null\n : string0 === string00 ? interpolate0\n : interpolate0 = interpolate(string00 = string0, value1);\n };\n}\n\nfunction attrFunction(name, interpolate, value) {\n var string00,\n string10,\n interpolate0;\n return function() {\n var string0, value1 = value(this), string1;\n if (value1 == null) return void this.removeAttribute(name);\n string0 = this.getAttribute(name);\n string1 = value1 + \"\";\n return string0 === string1 ? null\n : string0 === string00 && string1 === string10 ? interpolate0\n : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));\n };\n}\n\nfunction attrFunctionNS(fullname, interpolate, value) {\n var string00,\n string10,\n interpolate0;\n return function() {\n var string0, value1 = value(this), string1;\n if (value1 == null) return void this.removeAttributeNS(fullname.space, fullname.local);\n string0 = this.getAttributeNS(fullname.space, fullname.local);\n string1 = value1 + \"\";\n return string0 === string1 ? null\n : string0 === string00 && string1 === string10 ? interpolate0\n : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));\n };\n}\n\nexport default function(name, value) {\n var fullname = namespace(name), i = fullname === \"transform\" ? interpolateTransform : interpolate;\n return this.attrTween(name, typeof value === \"function\"\n ? (fullname.local ? attrFunctionNS : attrFunction)(fullname, i, tweenValue(this, \"attr.\" + name, value))\n : value == null ? (fullname.local ? attrRemoveNS : attrRemove)(fullname)\n : (fullname.local ? attrConstantNS : attrConstant)(fullname, i, value));\n}\n", "import {namespace} from \"d3-selection\";\n\nfunction attrInterpolate(name, i) {\n return function(t) {\n this.setAttribute(name, i.call(this, t));\n };\n}\n\nfunction attrInterpolateNS(fullname, i) {\n return function(t) {\n this.setAttributeNS(fullname.space, fullname.local, i.call(this, t));\n };\n}\n\nfunction attrTweenNS(fullname, value) {\n var t0, i0;\n function tween() {\n var i = value.apply(this, arguments);\n if (i !== i0) t0 = (i0 = i) && attrInterpolateNS(fullname, i);\n return t0;\n }\n tween._value = value;\n return tween;\n}\n\nfunction attrTween(name, value) {\n var t0, i0;\n function tween() {\n var i = value.apply(this, arguments);\n if (i !== i0) t0 = (i0 = i) && attrInterpolate(name, i);\n return t0;\n }\n tween._value = value;\n return tween;\n}\n\nexport default function(name, value) {\n var key = \"attr.\" + name;\n if (arguments.length < 2) return (key = this.tween(key)) && key._value;\n if (value == null) return this.tween(key, null);\n if (typeof value !== \"function\") throw new Error;\n var fullname = namespace(name);\n return this.tween(key, (fullname.local ? attrTweenNS : attrTween)(fullname, value));\n}\n", "import {get, init} from \"./schedule.js\";\n\nfunction delayFunction(id, value) {\n return function() {\n init(this, id).delay = +value.apply(this, arguments);\n };\n}\n\nfunction delayConstant(id, value) {\n return value = +value, function() {\n init(this, id).delay = value;\n };\n}\n\nexport default function(value) {\n var id = this._id;\n\n return arguments.length\n ? this.each((typeof value === \"function\"\n ? delayFunction\n : delayConstant)(id, value))\n : get(this.node(), id).delay;\n}\n", "import {get, set} from \"./schedule.js\";\n\nfunction durationFunction(id, value) {\n return function() {\n set(this, id).duration = +value.apply(this, arguments);\n };\n}\n\nfunction durationConstant(id, value) {\n return value = +value, function() {\n set(this, id).duration = value;\n };\n}\n\nexport default function(value) {\n var id = this._id;\n\n return arguments.length\n ? this.each((typeof value === \"function\"\n ? durationFunction\n : durationConstant)(id, value))\n : get(this.node(), id).duration;\n}\n", "import {get, set} from \"./schedule.js\";\n\nfunction easeConstant(id, value) {\n if (typeof value !== \"function\") throw new Error;\n return function() {\n set(this, id).ease = value;\n };\n}\n\nexport default function(value) {\n var id = this._id;\n\n return arguments.length\n ? this.each(easeConstant(id, value))\n : get(this.node(), id).ease;\n}\n", "import {set} from \"./schedule.js\";\n\nfunction easeVarying(id, value) {\n return function() {\n var v = value.apply(this, arguments);\n if (typeof v !== \"function\") throw new Error;\n set(this, id).ease = v;\n };\n}\n\nexport default function(value) {\n if (typeof value !== \"function\") throw new Error;\n return this.each(easeVarying(this._id, value));\n}\n", "import {matcher} from \"d3-selection\";\nimport {Transition} from \"./index.js\";\n\nexport default function(match) {\n if (typeof match !== \"function\") match = matcher(match);\n\n for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {\n if ((node = group[i]) && match.call(node, node.__data__, i, group)) {\n subgroup.push(node);\n }\n }\n }\n\n return new Transition(subgroups, this._parents, this._name, this._id);\n}\n", "import {Transition} from \"./index.js\";\n\nexport default function(transition) {\n if (transition._id !== this._id) throw new Error;\n\n for (var groups0 = this._groups, groups1 = transition._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {\n for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {\n if (node = group0[i] || group1[i]) {\n merge[i] = node;\n }\n }\n }\n\n for (; j < m0; ++j) {\n merges[j] = groups0[j];\n }\n\n return new Transition(merges, this._parents, this._name, this._id);\n}\n", "import {get, set, init} from \"./schedule.js\";\n\nfunction start(name) {\n return (name + \"\").trim().split(/^|\\s+/).every(function(t) {\n var i = t.indexOf(\".\");\n if (i >= 0) t = t.slice(0, i);\n return !t || t === \"start\";\n });\n}\n\nfunction onFunction(id, name, listener) {\n var on0, on1, sit = start(name) ? init : set;\n return function() {\n var schedule = sit(this, id),\n on = schedule.on;\n\n // If this node shared a dispatch with the previous node,\n // just assign the updated shared dispatch and we\u2019re done!\n // Otherwise, copy-on-write.\n if (on !== on0) (on1 = (on0 = on).copy()).on(name, listener);\n\n schedule.on = on1;\n };\n}\n\nexport default function(name, listener) {\n var id = this._id;\n\n return arguments.length < 2\n ? get(this.node(), id).on.on(name)\n : this.each(onFunction(id, name, listener));\n}\n", "function removeFunction(id) {\n return function() {\n var parent = this.parentNode;\n for (var i in this.__transition) if (+i !== id) return;\n if (parent) parent.removeChild(this);\n };\n}\n\nexport default function() {\n return this.on(\"end.remove\", removeFunction(this._id));\n}\n", "import {selector} from \"d3-selection\";\nimport {Transition} from \"./index.js\";\nimport schedule, {get} from \"./schedule.js\";\n\nexport default function(select) {\n var name = this._name,\n id = this._id;\n\n if (typeof select !== \"function\") select = selector(select);\n\n for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {\n if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {\n if (\"__data__\" in node) subnode.__data__ = node.__data__;\n subgroup[i] = subnode;\n schedule(subgroup[i], name, id, i, subgroup, get(node, id));\n }\n }\n }\n\n return new Transition(subgroups, this._parents, name, id);\n}\n", "import {selectorAll} from \"d3-selection\";\nimport {Transition} from \"./index.js\";\nimport schedule, {get} from \"./schedule.js\";\n\nexport default function(select) {\n var name = this._name,\n id = this._id;\n\n if (typeof select !== \"function\") select = selectorAll(select);\n\n for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {\n if (node = group[i]) {\n for (var children = select.call(node, node.__data__, i, group), child, inherit = get(node, id), k = 0, l = children.length; k < l; ++k) {\n if (child = children[k]) {\n schedule(child, name, id, k, children, inherit);\n }\n }\n subgroups.push(children);\n parents.push(node);\n }\n }\n }\n\n return new Transition(subgroups, parents, name, id);\n}\n", "import {selection} from \"d3-selection\";\n\nvar Selection = selection.prototype.constructor;\n\nexport default function() {\n return new Selection(this._groups, this._parents);\n}\n", "import {interpolateTransformCss as interpolateTransform} from \"d3-interpolate\";\nimport {style} from \"d3-selection\";\nimport {set} from \"./schedule.js\";\nimport {tweenValue} from \"./tween.js\";\nimport interpolate from \"./interpolate.js\";\n\nfunction styleNull(name, interpolate) {\n var string00,\n string10,\n interpolate0;\n return function() {\n var string0 = style(this, name),\n string1 = (this.style.removeProperty(name), style(this, name));\n return string0 === string1 ? null\n : string0 === string00 && string1 === string10 ? interpolate0\n : interpolate0 = interpolate(string00 = string0, string10 = string1);\n };\n}\n\nfunction styleRemove(name) {\n return function() {\n this.style.removeProperty(name);\n };\n}\n\nfunction styleConstant(name, interpolate, value1) {\n var string00,\n string1 = value1 + \"\",\n interpolate0;\n return function() {\n var string0 = style(this, name);\n return string0 === string1 ? null\n : string0 === string00 ? interpolate0\n : interpolate0 = interpolate(string00 = string0, value1);\n };\n}\n\nfunction styleFunction(name, interpolate, value) {\n var string00,\n string10,\n interpolate0;\n return function() {\n var string0 = style(this, name),\n value1 = value(this),\n string1 = value1 + \"\";\n if (value1 == null) string1 = value1 = (this.style.removeProperty(name), style(this, name));\n return string0 === string1 ? null\n : string0 === string00 && string1 === string10 ? interpolate0\n : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));\n };\n}\n\nfunction styleMaybeRemove(id, name) {\n var on0, on1, listener0, key = \"style.\" + name, event = \"end.\" + key, remove;\n return function() {\n var schedule = set(this, id),\n on = schedule.on,\n listener = schedule.value[key] == null ? remove || (remove = styleRemove(name)) : undefined;\n\n // If this node shared a dispatch with the previous node,\n // just assign the updated shared dispatch and we\u2019re done!\n // Otherwise, copy-on-write.\n if (on !== on0 || listener0 !== listener) (on1 = (on0 = on).copy()).on(event, listener0 = listener);\n\n schedule.on = on1;\n };\n}\n\nexport default function(name, value, priority) {\n var i = (name += \"\") === \"transform\" ? interpolateTransform : interpolate;\n return value == null ? this\n .styleTween(name, styleNull(name, i))\n .on(\"end.style.\" + name, styleRemove(name))\n : typeof value === \"function\" ? this\n .styleTween(name, styleFunction(name, i, tweenValue(this, \"style.\" + name, value)))\n .each(styleMaybeRemove(this._id, name))\n : this\n .styleTween(name, styleConstant(name, i, value), priority)\n .on(\"end.style.\" + name, null);\n}\n", "function styleInterpolate(name, i, priority) {\n return function(t) {\n this.style.setProperty(name, i.call(this, t), priority);\n };\n}\n\nfunction styleTween(name, value, priority) {\n var t, i0;\n function tween() {\n var i = value.apply(this, arguments);\n if (i !== i0) t = (i0 = i) && styleInterpolate(name, i, priority);\n return t;\n }\n tween._value = value;\n return tween;\n}\n\nexport default function(name, value, priority) {\n var key = \"style.\" + (name += \"\");\n if (arguments.length < 2) return (key = this.tween(key)) && key._value;\n if (value == null) return this.tween(key, null);\n if (typeof value !== \"function\") throw new Error;\n return this.tween(key, styleTween(name, value, priority == null ? \"\" : priority));\n}\n", "import {tweenValue} from \"./tween.js\";\n\nfunction textConstant(value) {\n return function() {\n this.textContent = value;\n };\n}\n\nfunction textFunction(value) {\n return function() {\n var value1 = value(this);\n this.textContent = value1 == null ? \"\" : value1;\n };\n}\n\nexport default function(value) {\n return this.tween(\"text\", typeof value === \"function\"\n ? textFunction(tweenValue(this, \"text\", value))\n : textConstant(value == null ? \"\" : value + \"\"));\n}\n", "function textInterpolate(i) {\n return function(t) {\n this.textContent = i.call(this, t);\n };\n}\n\nfunction textTween(value) {\n var t0, i0;\n function tween() {\n var i = value.apply(this, arguments);\n if (i !== i0) t0 = (i0 = i) && textInterpolate(i);\n return t0;\n }\n tween._value = value;\n return tween;\n}\n\nexport default function(value) {\n var key = \"text\";\n if (arguments.length < 1) return (key = this.tween(key)) && key._value;\n if (value == null) return this.tween(key, null);\n if (typeof value !== \"function\") throw new Error;\n return this.tween(key, textTween(value));\n}\n", "import {Transition, newId} from \"./index.js\";\nimport schedule, {get} from \"./schedule.js\";\n\nexport default function() {\n var name = this._name,\n id0 = this._id,\n id1 = newId();\n\n for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {\n if (node = group[i]) {\n var inherit = get(node, id0);\n schedule(node, name, id1, i, group, {\n time: inherit.time + inherit.delay + inherit.duration,\n delay: 0,\n duration: inherit.duration,\n ease: inherit.ease\n });\n }\n }\n }\n\n return new Transition(groups, this._parents, name, id1);\n}\n", "import {set} from \"./schedule.js\";\n\nexport default function() {\n var on0, on1, that = this, id = that._id, size = that.size();\n return new Promise(function(resolve, reject) {\n var cancel = {value: reject},\n end = {value: function() { if (--size === 0) resolve(); }};\n\n that.each(function() {\n var schedule = set(this, id),\n on = schedule.on;\n\n // If this node shared a dispatch with the previous node,\n // just assign the updated shared dispatch and we\u2019re done!\n // Otherwise, copy-on-write.\n if (on !== on0) {\n on1 = (on0 = on).copy();\n on1._.cancel.push(cancel);\n on1._.interrupt.push(cancel);\n on1._.end.push(end);\n }\n\n schedule.on = on1;\n });\n\n // The selection was empty, resolve end immediately\n if (size === 0) resolve();\n });\n}\n", "import {selection} from \"d3-selection\";\nimport transition_attr from \"./attr.js\";\nimport transition_attrTween from \"./attrTween.js\";\nimport transition_delay from \"./delay.js\";\nimport transition_duration from \"./duration.js\";\nimport transition_ease from \"./ease.js\";\nimport transition_easeVarying from \"./easeVarying.js\";\nimport transition_filter from \"./filter.js\";\nimport transition_merge from \"./merge.js\";\nimport transition_on from \"./on.js\";\nimport transition_remove from \"./remove.js\";\nimport transition_select from \"./select.js\";\nimport transition_selectAll from \"./selectAll.js\";\nimport transition_selection from \"./selection.js\";\nimport transition_style from \"./style.js\";\nimport transition_styleTween from \"./styleTween.js\";\nimport transition_text from \"./text.js\";\nimport transition_textTween from \"./textTween.js\";\nimport transition_transition from \"./transition.js\";\nimport transition_tween from \"./tween.js\";\nimport transition_end from \"./end.js\";\n\nvar id = 0;\n\nexport function Transition(groups, parents, name, id) {\n this._groups = groups;\n this._parents = parents;\n this._name = name;\n this._id = id;\n}\n\nexport default function transition(name) {\n return selection().transition(name);\n}\n\nexport function newId() {\n return ++id;\n}\n\nvar selection_prototype = selection.prototype;\n\nTransition.prototype = transition.prototype = {\n constructor: Transition,\n select: transition_select,\n selectAll: transition_selectAll,\n selectChild: selection_prototype.selectChild,\n selectChildren: selection_prototype.selectChildren,\n filter: transition_filter,\n merge: transition_merge,\n selection: transition_selection,\n transition: transition_transition,\n call: selection_prototype.call,\n nodes: selection_prototype.nodes,\n node: selection_prototype.node,\n size: selection_prototype.size,\n empty: selection_prototype.empty,\n each: selection_prototype.each,\n on: transition_on,\n attr: transition_attr,\n attrTween: transition_attrTween,\n style: transition_style,\n styleTween: transition_styleTween,\n text: transition_text,\n textTween: transition_textTween,\n remove: transition_remove,\n tween: transition_tween,\n delay: transition_delay,\n duration: transition_duration,\n ease: transition_ease,\n easeVarying: transition_easeVarying,\n end: transition_end,\n [Symbol.iterator]: selection_prototype[Symbol.iterator]\n};\n", "export const linear = t => +t;\n", "export function quadIn(t) {\n return t * t;\n}\n\nexport function quadOut(t) {\n return t * (2 - t);\n}\n\nexport function quadInOut(t) {\n return ((t *= 2) <= 1 ? t * t : --t * (2 - t) + 1) / 2;\n}\n", "export function cubicIn(t) {\n return t * t * t;\n}\n\nexport function cubicOut(t) {\n return --t * t * t + 1;\n}\n\nexport function cubicInOut(t) {\n return ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2;\n}\n", "var exponent = 3;\n\nexport var polyIn = (function custom(e) {\n e = +e;\n\n function polyIn(t) {\n return Math.pow(t, e);\n }\n\n polyIn.exponent = custom;\n\n return polyIn;\n})(exponent);\n\nexport var polyOut = (function custom(e) {\n e = +e;\n\n function polyOut(t) {\n return 1 - Math.pow(1 - t, e);\n }\n\n polyOut.exponent = custom;\n\n return polyOut;\n})(exponent);\n\nexport var polyInOut = (function custom(e) {\n e = +e;\n\n function polyInOut(t) {\n return ((t *= 2) <= 1 ? Math.pow(t, e) : 2 - Math.pow(2 - t, e)) / 2;\n }\n\n polyInOut.exponent = custom;\n\n return polyInOut;\n})(exponent);\n", "var pi = Math.PI,\n halfPi = pi / 2;\n\nexport function sinIn(t) {\n return (+t === 1) ? 1 : 1 - Math.cos(t * halfPi);\n}\n\nexport function sinOut(t) {\n return Math.sin(t * halfPi);\n}\n\nexport function sinInOut(t) {\n return (1 - Math.cos(pi * t)) / 2;\n}\n", "// tpmt is two power minus ten times t scaled to [0,1]\nexport function tpmt(x) {\n return (Math.pow(2, -10 * x) - 0.0009765625) * 1.0009775171065494;\n}\n", "import {tpmt} from \"./math.js\";\n\nexport function expIn(t) {\n return tpmt(1 - +t);\n}\n\nexport function expOut(t) {\n return 1 - tpmt(t);\n}\n\nexport function expInOut(t) {\n return ((t *= 2) <= 1 ? tpmt(1 - t) : 2 - tpmt(t - 1)) / 2;\n}\n", "export function circleIn(t) {\n return 1 - Math.sqrt(1 - t * t);\n}\n\nexport function circleOut(t) {\n return Math.sqrt(1 - --t * t);\n}\n\nexport function circleInOut(t) {\n return ((t *= 2) <= 1 ? 1 - Math.sqrt(1 - t * t) : Math.sqrt(1 - (t -= 2) * t) + 1) / 2;\n}\n", "var b1 = 4 / 11,\n b2 = 6 / 11,\n b3 = 8 / 11,\n b4 = 3 / 4,\n b5 = 9 / 11,\n b6 = 10 / 11,\n b7 = 15 / 16,\n b8 = 21 / 22,\n b9 = 63 / 64,\n b0 = 1 / b1 / b1;\n\nexport function bounceIn(t) {\n return 1 - bounceOut(1 - t);\n}\n\nexport function bounceOut(t) {\n return (t = +t) < b1 ? b0 * t * t : t < b3 ? b0 * (t -= b2) * t + b4 : t < b6 ? b0 * (t -= b5) * t + b7 : b0 * (t -= b8) * t + b9;\n}\n\nexport function bounceInOut(t) {\n return ((t *= 2) <= 1 ? 1 - bounceOut(1 - t) : bounceOut(t - 1) + 1) / 2;\n}\n", "var overshoot = 1.70158;\n\nexport var backIn = (function custom(s) {\n s = +s;\n\n function backIn(t) {\n return (t = +t) * t * (s * (t - 1) + t);\n }\n\n backIn.overshoot = custom;\n\n return backIn;\n})(overshoot);\n\nexport var backOut = (function custom(s) {\n s = +s;\n\n function backOut(t) {\n return --t * t * ((t + 1) * s + t) + 1;\n }\n\n backOut.overshoot = custom;\n\n return backOut;\n})(overshoot);\n\nexport var backInOut = (function custom(s) {\n s = +s;\n\n function backInOut(t) {\n return ((t *= 2) < 1 ? t * t * ((s + 1) * t - s) : (t -= 2) * t * ((s + 1) * t + s) + 2) / 2;\n }\n\n backInOut.overshoot = custom;\n\n return backInOut;\n})(overshoot);\n", "import {tpmt} from \"./math.js\";\n\nvar tau = 2 * Math.PI,\n amplitude = 1,\n period = 0.3;\n\nexport var elasticIn = (function custom(a, p) {\n var s = Math.asin(1 / (a = Math.max(1, a))) * (p /= tau);\n\n function elasticIn(t) {\n return a * tpmt(-(--t)) * Math.sin((s - t) / p);\n }\n\n elasticIn.amplitude = function(a) { return custom(a, p * tau); };\n elasticIn.period = function(p) { return custom(a, p); };\n\n return elasticIn;\n})(amplitude, period);\n\nexport var elasticOut = (function custom(a, p) {\n var s = Math.asin(1 / (a = Math.max(1, a))) * (p /= tau);\n\n function elasticOut(t) {\n return 1 - a * tpmt(t = +t) * Math.sin((t + s) / p);\n }\n\n elasticOut.amplitude = function(a) { return custom(a, p * tau); };\n elasticOut.period = function(p) { return custom(a, p); };\n\n return elasticOut;\n})(amplitude, period);\n\nexport var elasticInOut = (function custom(a, p) {\n var s = Math.asin(1 / (a = Math.max(1, a))) * (p /= tau);\n\n function elasticInOut(t) {\n return ((t = t * 2 - 1) < 0\n ? a * tpmt(-t) * Math.sin((s - t) / p)\n : 2 - a * tpmt(t) * Math.sin((s + t) / p)) / 2;\n }\n\n elasticInOut.amplitude = function(a) { return custom(a, p * tau); };\n elasticInOut.period = function(p) { return custom(a, p); };\n\n return elasticInOut;\n})(amplitude, period);\n", "export {\n linear as easeLinear\n} from \"./linear.js\";\n\nexport {\n quadInOut as easeQuad,\n quadIn as easeQuadIn,\n quadOut as easeQuadOut,\n quadInOut as easeQuadInOut\n} from \"./quad.js\";\n\nexport {\n cubicInOut as easeCubic,\n cubicIn as easeCubicIn,\n cubicOut as easeCubicOut,\n cubicInOut as easeCubicInOut\n} from \"./cubic.js\";\n\nexport {\n polyInOut as easePoly,\n polyIn as easePolyIn,\n polyOut as easePolyOut,\n polyInOut as easePolyInOut\n} from \"./poly.js\";\n\nexport {\n sinInOut as easeSin,\n sinIn as easeSinIn,\n sinOut as easeSinOut,\n sinInOut as easeSinInOut\n} from \"./sin.js\";\n\nexport {\n expInOut as easeExp,\n expIn as easeExpIn,\n expOut as easeExpOut,\n expInOut as easeExpInOut\n} from \"./exp.js\";\n\nexport {\n circleInOut as easeCircle,\n circleIn as easeCircleIn,\n circleOut as easeCircleOut,\n circleInOut as easeCircleInOut\n} from \"./circle.js\";\n\nexport {\n bounceOut as easeBounce,\n bounceIn as easeBounceIn,\n bounceOut as easeBounceOut,\n bounceInOut as easeBounceInOut\n} from \"./bounce.js\";\n\nexport {\n backInOut as easeBack,\n backIn as easeBackIn,\n backOut as easeBackOut,\n backInOut as easeBackInOut\n} from \"./back.js\";\n\nexport {\n elasticOut as easeElastic,\n elasticIn as easeElasticIn,\n elasticOut as easeElasticOut,\n elasticInOut as easeElasticInOut\n} from \"./elastic.js\";\n", "import {Transition, newId} from \"../transition/index.js\";\nimport schedule from \"../transition/schedule.js\";\nimport {easeCubicInOut} from \"d3-ease\";\nimport {now} from \"d3-timer\";\n\nvar defaultTiming = {\n time: null, // Set on use.\n delay: 0,\n duration: 250,\n ease: easeCubicInOut\n};\n\nfunction inherit(node, id) {\n var timing;\n while (!(timing = node.__transition) || !(timing = timing[id])) {\n if (!(node = node.parentNode)) {\n throw new Error(`transition ${id} not found`);\n }\n }\n return timing;\n}\n\nexport default function(name) {\n var id,\n timing;\n\n if (name instanceof Transition) {\n id = name._id, name = name._name;\n } else {\n id = newId(), (timing = defaultTiming).time = now(), name = name == null ? null : name + \"\";\n }\n\n for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {\n if (node = group[i]) {\n schedule(node, name, id, i, group, timing || inherit(node, id));\n }\n }\n }\n\n return new Transition(groups, this._parents, name, id);\n}\n", "import {selection} from \"d3-selection\";\nimport selection_interrupt from \"./interrupt.js\";\nimport selection_transition from \"./transition.js\";\n\nselection.prototype.interrupt = selection_interrupt;\nselection.prototype.transition = selection_transition;\n", "import {Transition} from \"./transition/index.js\";\nimport {SCHEDULED} from \"./transition/schedule.js\";\n\nvar root = [null];\n\nexport default function(node, name) {\n var schedules = node.__transition,\n schedule,\n i;\n\n if (schedules) {\n name = name == null ? null : name + \"\";\n for (i in schedules) {\n if ((schedule = schedules[i]).state > SCHEDULED && schedule.name === name) {\n return new Transition([[node]], root, name, +i);\n }\n }\n }\n\n return null;\n}\n", "import \"./selection/index.js\";\nexport {default as transition} from \"./transition/index.js\";\nexport {default as active} from \"./active.js\";\nexport {default as interrupt} from \"./interrupt.js\";\n", "export default x => () => x;\n", "export default function ZoomEvent(type, {\n sourceEvent,\n target,\n transform,\n dispatch\n}) {\n Object.defineProperties(this, {\n type: {value: type, enumerable: true, configurable: true},\n sourceEvent: {value: sourceEvent, enumerable: true, configurable: true},\n target: {value: target, enumerable: true, configurable: true},\n transform: {value: transform, enumerable: true, configurable: true},\n _: {value: dispatch}\n });\n}\n", "export function Transform(k, x, y) {\n this.k = k;\n this.x = x;\n this.y = y;\n}\n\nTransform.prototype = {\n constructor: Transform,\n scale: function(k) {\n return k === 1 ? this : new Transform(this.k * k, this.x, this.y);\n },\n translate: function(x, y) {\n return x === 0 & y === 0 ? this : new Transform(this.k, this.x + this.k * x, this.y + this.k * y);\n },\n apply: function(point) {\n return [point[0] * this.k + this.x, point[1] * this.k + this.y];\n },\n applyX: function(x) {\n return x * this.k + this.x;\n },\n applyY: function(y) {\n return y * this.k + this.y;\n },\n invert: function(location) {\n return [(location[0] - this.x) / this.k, (location[1] - this.y) / this.k];\n },\n invertX: function(x) {\n return (x - this.x) / this.k;\n },\n invertY: function(y) {\n return (y - this.y) / this.k;\n },\n rescaleX: function(x) {\n return x.copy().domain(x.range().map(this.invertX, this).map(x.invert, x));\n },\n rescaleY: function(y) {\n return y.copy().domain(y.range().map(this.invertY, this).map(y.invert, y));\n },\n toString: function() {\n return \"translate(\" + this.x + \",\" + this.y + \") scale(\" + this.k + \")\";\n }\n};\n\nexport var identity = new Transform(1, 0, 0);\n\ntransform.prototype = Transform.prototype;\n\nexport default function transform(node) {\n while (!node.__zoom) if (!(node = node.parentNode)) return identity;\n return node.__zoom;\n}\n", "export function nopropagation(event) {\n event.stopImmediatePropagation();\n}\n\nexport default function(event) {\n event.preventDefault();\n event.stopImmediatePropagation();\n}\n", "import {dispatch} from \"d3-dispatch\";\nimport {dragDisable, dragEnable} from \"d3-drag\";\nimport {interpolateZoom} from \"d3-interpolate\";\nimport {select, pointer} from \"d3-selection\";\nimport {interrupt} from \"d3-transition\";\nimport constant from \"./constant.js\";\nimport ZoomEvent from \"./event.js\";\nimport {Transform, identity} from \"./transform.js\";\nimport noevent, {nopropagation} from \"./noevent.js\";\n\n// Ignore right-click, since that should open the context menu.\n// except for pinch-to-zoom, which is sent as a wheel+ctrlKey event\nfunction defaultFilter(event) {\n return (!event.ctrlKey || event.type === 'wheel') && !event.button;\n}\n\nfunction defaultExtent() {\n var e = this;\n if (e instanceof SVGElement) {\n e = e.ownerSVGElement || e;\n if (e.hasAttribute(\"viewBox\")) {\n e = e.viewBox.baseVal;\n return [[e.x, e.y], [e.x + e.width, e.y + e.height]];\n }\n return [[0, 0], [e.width.baseVal.value, e.height.baseVal.value]];\n }\n return [[0, 0], [e.clientWidth, e.clientHeight]];\n}\n\nfunction defaultTransform() {\n return this.__zoom || identity;\n}\n\nfunction defaultWheelDelta(event) {\n return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * (event.ctrlKey ? 10 : 1);\n}\n\nfunction defaultTouchable() {\n return navigator.maxTouchPoints || (\"ontouchstart\" in this);\n}\n\nfunction defaultConstrain(transform, extent, translateExtent) {\n var dx0 = transform.invertX(extent[0][0]) - translateExtent[0][0],\n dx1 = transform.invertX(extent[1][0]) - translateExtent[1][0],\n dy0 = transform.invertY(extent[0][1]) - translateExtent[0][1],\n dy1 = transform.invertY(extent[1][1]) - translateExtent[1][1];\n return transform.translate(\n dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),\n dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)\n );\n}\n\nexport default function() {\n var filter = defaultFilter,\n extent = defaultExtent,\n constrain = defaultConstrain,\n wheelDelta = defaultWheelDelta,\n touchable = defaultTouchable,\n scaleExtent = [0, Infinity],\n translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]],\n duration = 250,\n interpolate = interpolateZoom,\n listeners = dispatch(\"start\", \"zoom\", \"end\"),\n touchstarting,\n touchfirst,\n touchending,\n touchDelay = 500,\n wheelDelay = 150,\n clickDistance2 = 0,\n tapDistance = 10;\n\n function zoom(selection) {\n selection\n .property(\"__zoom\", defaultTransform)\n .on(\"wheel.zoom\", wheeled, {passive: false})\n .on(\"mousedown.zoom\", mousedowned)\n .on(\"dblclick.zoom\", dblclicked)\n .filter(touchable)\n .on(\"touchstart.zoom\", touchstarted)\n .on(\"touchmove.zoom\", touchmoved)\n .on(\"touchend.zoom touchcancel.zoom\", touchended)\n .style(\"-webkit-tap-highlight-color\", \"rgba(0,0,0,0)\");\n }\n\n zoom.transform = function(collection, transform, point, event) {\n var selection = collection.selection ? collection.selection() : collection;\n selection.property(\"__zoom\", defaultTransform);\n if (collection !== selection) {\n schedule(collection, transform, point, event);\n } else {\n selection.interrupt().each(function() {\n gesture(this, arguments)\n .event(event)\n .start()\n .zoom(null, typeof transform === \"function\" ? transform.apply(this, arguments) : transform)\n .end();\n });\n }\n };\n\n zoom.scaleBy = function(selection, k, p, event) {\n zoom.scaleTo(selection, function() {\n var k0 = this.__zoom.k,\n k1 = typeof k === \"function\" ? k.apply(this, arguments) : k;\n return k0 * k1;\n }, p, event);\n };\n\n zoom.scaleTo = function(selection, k, p, event) {\n zoom.transform(selection, function() {\n var e = extent.apply(this, arguments),\n t0 = this.__zoom,\n p0 = p == null ? centroid(e) : typeof p === \"function\" ? p.apply(this, arguments) : p,\n p1 = t0.invert(p0),\n k1 = typeof k === \"function\" ? k.apply(this, arguments) : k;\n return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent);\n }, p, event);\n };\n\n zoom.translateBy = function(selection, x, y, event) {\n zoom.transform(selection, function() {\n return constrain(this.__zoom.translate(\n typeof x === \"function\" ? x.apply(this, arguments) : x,\n typeof y === \"function\" ? y.apply(this, arguments) : y\n ), extent.apply(this, arguments), translateExtent);\n }, null, event);\n };\n\n zoom.translateTo = function(selection, x, y, p, event) {\n zoom.transform(selection, function() {\n var e = extent.apply(this, arguments),\n t = this.__zoom,\n p0 = p == null ? centroid(e) : typeof p === \"function\" ? p.apply(this, arguments) : p;\n return constrain(identity.translate(p0[0], p0[1]).scale(t.k).translate(\n typeof x === \"function\" ? -x.apply(this, arguments) : -x,\n typeof y === \"function\" ? -y.apply(this, arguments) : -y\n ), e, translateExtent);\n }, p, event);\n };\n\n function scale(transform, k) {\n k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], k));\n return k === transform.k ? transform : new Transform(k, transform.x, transform.y);\n }\n\n function translate(transform, p0, p1) {\n var x = p0[0] - p1[0] * transform.k, y = p0[1] - p1[1] * transform.k;\n return x === transform.x && y === transform.y ? transform : new Transform(transform.k, x, y);\n }\n\n function centroid(extent) {\n return [(+extent[0][0] + +extent[1][0]) / 2, (+extent[0][1] + +extent[1][1]) / 2];\n }\n\n function schedule(transition, transform, point, event) {\n transition\n .on(\"start.zoom\", function() { gesture(this, arguments).event(event).start(); })\n .on(\"interrupt.zoom end.zoom\", function() { gesture(this, arguments).event(event).end(); })\n .tween(\"zoom\", function() {\n var that = this,\n args = arguments,\n g = gesture(that, args).event(event),\n e = extent.apply(that, args),\n p = point == null ? centroid(e) : typeof point === \"function\" ? point.apply(that, args) : point,\n w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]),\n a = that.__zoom,\n b = typeof transform === \"function\" ? transform.apply(that, args) : transform,\n i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k));\n return function(t) {\n if (t === 1) t = b; // Avoid rounding error on end.\n else { var l = i(t), k = w / l[2]; t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k); }\n g.zoom(null, t);\n };\n });\n }\n\n function gesture(that, args, clean) {\n return (!clean && that.__zooming) || new Gesture(that, args);\n }\n\n function Gesture(that, args) {\n this.that = that;\n this.args = args;\n this.active = 0;\n this.sourceEvent = null;\n this.extent = extent.apply(that, args);\n this.taps = 0;\n }\n\n Gesture.prototype = {\n event: function(event) {\n if (event) this.sourceEvent = event;\n return this;\n },\n start: function() {\n if (++this.active === 1) {\n this.that.__zooming = this;\n this.emit(\"start\");\n }\n return this;\n },\n zoom: function(key, transform) {\n if (this.mouse && key !== \"mouse\") this.mouse[1] = transform.invert(this.mouse[0]);\n if (this.touch0 && key !== \"touch\") this.touch0[1] = transform.invert(this.touch0[0]);\n if (this.touch1 && key !== \"touch\") this.touch1[1] = transform.invert(this.touch1[0]);\n this.that.__zoom = transform;\n this.emit(\"zoom\");\n return this;\n },\n end: function() {\n if (--this.active === 0) {\n delete this.that.__zooming;\n this.emit(\"end\");\n }\n return this;\n },\n emit: function(type) {\n var d = select(this.that).datum();\n listeners.call(\n type,\n this.that,\n new ZoomEvent(type, {\n sourceEvent: this.sourceEvent,\n target: zoom,\n type,\n transform: this.that.__zoom,\n dispatch: listeners\n }),\n d\n );\n }\n };\n\n function wheeled(event, ...args) {\n if (!filter.apply(this, arguments)) return;\n var g = gesture(this, args).event(event),\n t = this.__zoom,\n k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k * Math.pow(2, wheelDelta.apply(this, arguments)))),\n p = pointer(event);\n\n // If the mouse is in the same location as before, reuse it.\n // If there were recent wheel events, reset the wheel idle timeout.\n if (g.wheel) {\n if (g.mouse[0][0] !== p[0] || g.mouse[0][1] !== p[1]) {\n g.mouse[1] = t.invert(g.mouse[0] = p);\n }\n clearTimeout(g.wheel);\n }\n\n // If this wheel event won\u2019t trigger a transform change, ignore it.\n else if (t.k === k) return;\n\n // Otherwise, capture the mouse point and location at the start.\n else {\n g.mouse = [p, t.invert(p)];\n interrupt(this);\n g.start();\n }\n\n noevent(event);\n g.wheel = setTimeout(wheelidled, wheelDelay);\n g.zoom(\"mouse\", constrain(translate(scale(t, k), g.mouse[0], g.mouse[1]), g.extent, translateExtent));\n\n function wheelidled() {\n g.wheel = null;\n g.end();\n }\n }\n\n function mousedowned(event, ...args) {\n if (touchending || !filter.apply(this, arguments)) return;\n var currentTarget = event.currentTarget,\n g = gesture(this, args, true).event(event),\n v = select(event.view).on(\"mousemove.zoom\", mousemoved, true).on(\"mouseup.zoom\", mouseupped, true),\n p = pointer(event, currentTarget),\n x0 = event.clientX,\n y0 = event.clientY;\n\n dragDisable(event.view);\n nopropagation(event);\n g.mouse = [p, this.__zoom.invert(p)];\n interrupt(this);\n g.start();\n\n function mousemoved(event) {\n noevent(event);\n if (!g.moved) {\n var dx = event.clientX - x0, dy = event.clientY - y0;\n g.moved = dx * dx + dy * dy > clickDistance2;\n }\n g.event(event)\n .zoom(\"mouse\", constrain(translate(g.that.__zoom, g.mouse[0] = pointer(event, currentTarget), g.mouse[1]), g.extent, translateExtent));\n }\n\n function mouseupped(event) {\n v.on(\"mousemove.zoom mouseup.zoom\", null);\n dragEnable(event.view, g.moved);\n noevent(event);\n g.event(event).end();\n }\n }\n\n function dblclicked(event, ...args) {\n if (!filter.apply(this, arguments)) return;\n var t0 = this.__zoom,\n p0 = pointer(event.changedTouches ? event.changedTouches[0] : event, this),\n p1 = t0.invert(p0),\n k1 = t0.k * (event.shiftKey ? 0.5 : 2),\n t1 = constrain(translate(scale(t0, k1), p0, p1), extent.apply(this, args), translateExtent);\n\n noevent(event);\n if (duration > 0) select(this).transition().duration(duration).call(schedule, t1, p0, event);\n else select(this).call(zoom.transform, t1, p0, event);\n }\n\n function touchstarted(event, ...args) {\n if (!filter.apply(this, arguments)) return;\n var touches = event.touches,\n n = touches.length,\n g = gesture(this, args, event.changedTouches.length === n).event(event),\n started, i, t, p;\n\n nopropagation(event);\n for (i = 0; i < n; ++i) {\n t = touches[i], p = pointer(t, this);\n p = [p, this.__zoom.invert(p), t.identifier];\n if (!g.touch0) g.touch0 = p, started = true, g.taps = 1 + !!touchstarting;\n else if (!g.touch1 && g.touch0[2] !== p[2]) g.touch1 = p, g.taps = 0;\n }\n\n if (touchstarting) touchstarting = clearTimeout(touchstarting);\n\n if (started) {\n if (g.taps < 2) touchfirst = p[0], touchstarting = setTimeout(function() { touchstarting = null; }, touchDelay);\n interrupt(this);\n g.start();\n }\n }\n\n function touchmoved(event, ...args) {\n if (!this.__zooming) return;\n var g = gesture(this, args).event(event),\n touches = event.changedTouches,\n n = touches.length, i, t, p, l;\n\n noevent(event);\n for (i = 0; i < n; ++i) {\n t = touches[i], p = pointer(t, this);\n if (g.touch0 && g.touch0[2] === t.identifier) g.touch0[0] = p;\n else if (g.touch1 && g.touch1[2] === t.identifier) g.touch1[0] = p;\n }\n t = g.that.__zoom;\n if (g.touch1) {\n var p0 = g.touch0[0], l0 = g.touch0[1],\n p1 = g.touch1[0], l1 = g.touch1[1],\n dp = (dp = p1[0] - p0[0]) * dp + (dp = p1[1] - p0[1]) * dp,\n dl = (dl = l1[0] - l0[0]) * dl + (dl = l1[1] - l0[1]) * dl;\n t = scale(t, Math.sqrt(dp / dl));\n p = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];\n l = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2];\n }\n else if (g.touch0) p = g.touch0[0], l = g.touch0[1];\n else return;\n\n g.zoom(\"touch\", constrain(translate(t, p, l), g.extent, translateExtent));\n }\n\n function touchended(event, ...args) {\n if (!this.__zooming) return;\n var g = gesture(this, args).event(event),\n touches = event.changedTouches,\n n = touches.length, i, t;\n\n nopropagation(event);\n if (touchending) clearTimeout(touchending);\n touchending = setTimeout(function() { touchending = null; }, touchDelay);\n for (i = 0; i < n; ++i) {\n t = touches[i];\n if (g.touch0 && g.touch0[2] === t.identifier) delete g.touch0;\n else if (g.touch1 && g.touch1[2] === t.identifier) delete g.touch1;\n }\n if (g.touch1 && !g.touch0) g.touch0 = g.touch1, delete g.touch1;\n if (g.touch0) g.touch0[1] = this.__zoom.invert(g.touch0[0]);\n else {\n g.end();\n // If this was a dbltap, reroute to the (optional) dblclick.zoom handler.\n if (g.taps === 2) {\n t = pointer(t, this);\n if (Math.hypot(touchfirst[0] - t[0], touchfirst[1] - t[1]) < tapDistance) {\n var p = select(this).on(\"dblclick.zoom\");\n if (p) p.apply(this, arguments);\n }\n }\n }\n }\n\n zoom.wheelDelta = function(_) {\n return arguments.length ? (wheelDelta = typeof _ === \"function\" ? _ : constant(+_), zoom) : wheelDelta;\n };\n\n zoom.filter = function(_) {\n return arguments.length ? (filter = typeof _ === \"function\" ? _ : constant(!!_), zoom) : filter;\n };\n\n zoom.touchable = function(_) {\n return arguments.length ? (touchable = typeof _ === \"function\" ? _ : constant(!!_), zoom) : touchable;\n };\n\n zoom.extent = function(_) {\n return arguments.length ? (extent = typeof _ === \"function\" ? _ : constant([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;\n };\n\n zoom.scaleExtent = function(_) {\n return arguments.length ? (scaleExtent[0] = +_[0], scaleExtent[1] = +_[1], zoom) : [scaleExtent[0], scaleExtent[1]];\n };\n\n zoom.translateExtent = function(_) {\n return arguments.length ? (translateExtent[0][0] = +_[0][0], translateExtent[1][0] = +_[1][0], translateExtent[0][1] = +_[0][1], translateExtent[1][1] = +_[1][1], zoom) : [[translateExtent[0][0], translateExtent[0][1]], [translateExtent[1][0], translateExtent[1][1]]];\n };\n\n zoom.constrain = function(_) {\n return arguments.length ? (constrain = _, zoom) : constrain;\n };\n\n zoom.duration = function(_) {\n return arguments.length ? (duration = +_, zoom) : duration;\n };\n\n zoom.interpolate = function(_) {\n return arguments.length ? (interpolate = _, zoom) : interpolate;\n };\n\n zoom.on = function() {\n var value = listeners.on.apply(listeners, arguments);\n return value === listeners ? zoom : value;\n };\n\n zoom.clickDistance = function(_) {\n return arguments.length ? (clickDistance2 = (_ = +_) * _, zoom) : Math.sqrt(clickDistance2);\n };\n\n zoom.tapDistance = function(_) {\n return arguments.length ? (tapDistance = +_, zoom) : tapDistance;\n };\n\n return zoom;\n}\n", "export {default as zoom} from \"./zoom.js\";\nexport {default as zoomTransform, identity as zoomIdentity, Transform as ZoomTransform} from \"./transform.js\";\n", "import {\n geoMercatorRaw as d3_geoMercatorRaw,\n geoTransform as d3_geoTransform\n} from 'd3-geo';\n\nimport {\n zoomIdentity as d3_zoomIdentity\n} from 'd3-zoom';\n\n/**\n * @import { Vec2 } from './vector';\n * @typedef {[Vec2, Vec2]} ClipExtent\n */\n\n/**\n Bypasses features of D3's default projection stream pipeline that are unnecessary:\n * Antimeridian clipping\n * Spherical rotation\n * Resampling\n*/\nexport function geoRawMercator() {\n const project = d3_geoMercatorRaw;\n let k = 512 / Math.PI; // scale\n let x = 0;\n let y = 0; // translate\n /** @type {ClipExtent} */\n let clipExtent = [[0, 0], [0, 0]];\n\n /**\n * @param {Vec2} point\n * @returns {Vec2}\n */\n function projection(point) {\n point = project(point[0] * Math.PI / 180, point[1] * Math.PI / 180);\n return [point[0] * k + x, y - point[1] * k];\n }\n\n /**\n * @param {Vec2} point\n * @returns {Vec2}\n */\n projection.invert = function(point) {\n point = project.invert((point[0] - x) / k, (y - point[1]) / k);\n return point && [point[0] * 180 / Math.PI, point[1] * 180 / Math.PI];\n };\n\n\n /** @type {GetSet} */\n projection.scale = function(_) {\n if (!arguments.length) return k;\n k = +_;\n return projection;\n };\n\n\n /** @type {GetSet} */\n projection.translate = function(_) {\n if (!arguments.length) return [x, y];\n x = +_[0];\n y = +_[1];\n return projection;\n };\n\n\n /** @type {GetSet} */\n projection.clipExtent = function(_) {\n if (!arguments.length) return clipExtent;\n clipExtent = _;\n return projection;\n };\n\n\n /** @type {GetSet} */\n projection.transform = function(obj) {\n if (!arguments.length) return d3_zoomIdentity.translate(x, y).scale(k);\n x = +obj.x;\n y = +obj.y;\n k = +obj.k;\n return projection;\n };\n\n\n projection.stream = d3_geoTransform({\n point: function(x, y) {\n const vec = projection([x, y]);\n this.stream.point(vec[0], vec[1]);\n }\n }).stream;\n\n\n return projection;\n}\n/**\n * @typedef {ReturnType} Projection\n */\n", "import { geoVecNormalizedDot, type Vec2 } from './vector';\n\nexport interface Coord {\n coord: Vec2\n};\n\n\nfunction geoOrthoFilterDotProduct(dotp: number, epsilon: number, lowerThreshold: number, upperThreshold: number, allowStraightAngles?: boolean) {\n var val = Math.abs(dotp);\n if (val < epsilon) {\n return 0; // already orthogonal\n } else if (allowStraightAngles && Math.abs(val-1) < epsilon) {\n return 0; // straight angle, which is okay in this case\n } else if (val < lowerThreshold || val > upperThreshold) {\n return dotp; // can be adjusted\n } else {\n return null; // ignore vertex\n }\n}\n\n\nexport function geoOrthoCalcScore(points: Coord[], isClosed: boolean, epsilon: number, threshold: number) {\n var score = 0;\n var first = isClosed ? 0 : 1;\n var last = isClosed ? points.length : points.length - 1;\n var coords = points.map(function(p) { return p.coord; });\n\n var lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180);\n var upperThreshold = Math.cos(threshold * Math.PI / 180);\n\n for (var i = first; i < last; i++) {\n var a = coords[(i - 1 + coords.length) % coords.length];\n var origin = coords[i];\n var b = coords[(i + 1) % coords.length];\n\n var dotp = geoOrthoFilterDotProduct(geoVecNormalizedDot(a, b, origin), epsilon, lowerThreshold, upperThreshold);\n if (dotp === null) continue; // ignore vertex\n score = score + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1)));\n }\n\n return score;\n}\n\n// returns the maximum angle less than `lessThan` between the actual corner and a 0\u00B0 or 90\u00B0 corner\nexport function geoOrthoMaxOffsetAngle(coords: Vec2[], isClosed: boolean, lessThan: number) {\n var max = -Infinity;\n\n var first = isClosed ? 0 : 1;\n var last = isClosed ? coords.length : coords.length - 1;\n\n for (var i = first; i < last; i++) {\n var a = coords[(i - 1 + coords.length) % coords.length];\n var origin = coords[i];\n var b = coords[(i + 1) % coords.length];\n var normalizedDotP = geoVecNormalizedDot(a, b, origin);\n\n var angle = Math.acos(Math.abs(normalizedDotP)) * 180 / Math.PI;\n\n if (angle > 45) angle = 90 - angle;\n\n if (angle >= lessThan) continue;\n\n if (angle > max) max = angle;\n }\n\n if (max === -Infinity) return null;\n\n return max;\n}\n\n\n// similar to geoOrthoCalcScore, but returns quickly if there is something to do\nexport function geoOrthoCanOrthogonalize(coords: Vec2[], isClosed: boolean, epsilon: number, threshold: number, allowStraightAngles: boolean) {\n var score = null;\n var first = isClosed ? 0 : 1;\n var last = isClosed ? coords.length : coords.length - 1;\n\n var lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180);\n var upperThreshold = Math.cos(threshold * Math.PI / 180);\n\n for (var i = first; i < last; i++) {\n var a = coords[(i - 1 + coords.length) % coords.length];\n var origin = coords[i];\n var b = coords[(i + 1) % coords.length];\n\n var dotp = geoOrthoFilterDotProduct(geoVecNormalizedDot(a, b, origin), epsilon, lowerThreshold, upperThreshold, allowStraightAngles);\n if (dotp === null) continue; // ignore vertex\n if (Math.abs(dotp) > 0) return 1; // something to do\n score = 0; // already square\n }\n\n return score;\n}\n", "export { geoExtent } from './extent.js';\n\nexport { geoLatToMeters } from './geo.js';\nexport { geoLonToMeters } from './geo.js';\nexport { geoMetersToLat } from './geo.js';\nexport { geoMetersToLon } from './geo.js';\nexport { geoMetersToOffset } from './geo.js';\nexport { geoOffsetToMeters } from './geo.js';\nexport { geoScaleToZoom } from './geo.js';\nexport { geoSphericalClosestNode } from './geo.js';\nexport { geoSphericalDistance } from './geo.js';\nexport { geoZoomToScale } from './geo.js';\n\nexport { geoAngle } from './geom.js';\nexport { geoChooseEdge } from './geom.js';\nexport { geoEdgeEqual } from './geom.js';\nexport { geoGetSmallestSurroundingRectangle } from './geom.js';\nexport { geoHasLineIntersections } from './geom.js';\nexport { geoHasSelfIntersections } from './geom.js';\nexport { geoRotate } from './geom.js';\nexport { geoLineIntersection } from './geom.js';\nexport { geoPathHasIntersections } from './geom.js';\nexport { geoPathIntersections } from './geom.js';\nexport { geoPathLength } from './geom.js';\nexport { geoPointInPolygon } from './geom.js';\nexport { geoPolygonContainsPolygon } from './geom.js';\nexport { geoPolygonIntersectsPolygon } from './geom.js';\nexport { geoViewportEdge } from './geom.js';\n\nexport { geoRawMercator } from './raw_mercator.js';\n\nexport { geoVecAdd } from './vector.js';\nexport { geoVecAngle } from './vector.js';\nexport { geoVecCross } from './vector.js';\nexport { geoVecDot } from './vector.js';\nexport { geoVecEqual } from './vector.js';\nexport { geoVecFloor } from './vector.js';\nexport { geoVecInterp } from './vector.js';\nexport { geoVecLength } from './vector.js';\nexport { geoVecLengthSquare } from './vector.js';\nexport { geoVecNormalize } from './vector.js';\nexport { geoVecNormalizedDot } from './vector.js';\nexport { geoVecProject } from './vector.js';\nexport { geoVecSubtract } from './vector.js';\nexport { geoVecScale } from './vector.js';\n\nexport { geoOrthoCalcScore } from './ortho.js';\nexport { geoOrthoMaxOffsetAngle } from './ortho.js';\nexport { geoOrthoCanOrthogonalize } from './ortho.js';\n", "/*! MIT License. Copyright 2015-2018 Richard Moore . See LICENSE.txt. */\n(function(root) {\n \"use strict\";\n\n function checkInt(value) {\n return (parseInt(value) === value);\n }\n\n function checkInts(arrayish) {\n if (!checkInt(arrayish.length)) { return false; }\n\n for (var i = 0; i < arrayish.length; i++) {\n if (!checkInt(arrayish[i]) || arrayish[i] < 0 || arrayish[i] > 255) {\n return false;\n }\n }\n\n return true;\n }\n\n function coerceArray(arg, copy) {\n\n // ArrayBuffer view\n if (arg.buffer && arg.name === 'Uint8Array') {\n\n if (copy) {\n if (arg.slice) {\n arg = arg.slice();\n } else {\n arg = Array.prototype.slice.call(arg);\n }\n }\n\n return arg;\n }\n\n // It's an array; check it is a valid representation of a byte\n if (Array.isArray(arg)) {\n if (!checkInts(arg)) {\n throw new Error('Array contains invalid value: ' + arg);\n }\n\n return new Uint8Array(arg);\n }\n\n // Something else, but behaves like an array (maybe a Buffer? Arguments?)\n if (checkInt(arg.length) && checkInts(arg)) {\n return new Uint8Array(arg);\n }\n\n throw new Error('unsupported array-like object');\n }\n\n function createArray(length) {\n return new Uint8Array(length);\n }\n\n function copyArray(sourceArray, targetArray, targetStart, sourceStart, sourceEnd) {\n if (sourceStart != null || sourceEnd != null) {\n if (sourceArray.slice) {\n sourceArray = sourceArray.slice(sourceStart, sourceEnd);\n } else {\n sourceArray = Array.prototype.slice.call(sourceArray, sourceStart, sourceEnd);\n }\n }\n targetArray.set(sourceArray, targetStart);\n }\n\n\n\n var convertUtf8 = (function() {\n function toBytes(text) {\n var result = [], i = 0;\n text = encodeURI(text);\n while (i < text.length) {\n var c = text.charCodeAt(i++);\n\n // if it is a % sign, encode the following 2 bytes as a hex value\n if (c === 37) {\n result.push(parseInt(text.substr(i, 2), 16))\n i += 2;\n\n // otherwise, just the actual byte\n } else {\n result.push(c)\n }\n }\n\n return coerceArray(result);\n }\n\n function fromBytes(bytes) {\n var result = [], i = 0;\n\n while (i < bytes.length) {\n var c = bytes[i];\n\n if (c < 128) {\n result.push(String.fromCharCode(c));\n i++;\n } else if (c > 191 && c < 224) {\n result.push(String.fromCharCode(((c & 0x1f) << 6) | (bytes[i + 1] & 0x3f)));\n i += 2;\n } else {\n result.push(String.fromCharCode(((c & 0x0f) << 12) | ((bytes[i + 1] & 0x3f) << 6) | (bytes[i + 2] & 0x3f)));\n i += 3;\n }\n }\n\n return result.join('');\n }\n\n return {\n toBytes: toBytes,\n fromBytes: fromBytes,\n }\n })();\n\n var convertHex = (function() {\n function toBytes(text) {\n var result = [];\n for (var i = 0; i < text.length; i += 2) {\n result.push(parseInt(text.substr(i, 2), 16));\n }\n\n return result;\n }\n\n // http://ixti.net/development/javascript/2011/11/11/base64-encodedecode-of-utf8-in-browser-with-js.html\n var Hex = '0123456789abcdef';\n\n function fromBytes(bytes) {\n var result = [];\n for (var i = 0; i < bytes.length; i++) {\n var v = bytes[i];\n result.push(Hex[(v & 0xf0) >> 4] + Hex[v & 0x0f]);\n }\n return result.join('');\n }\n\n return {\n toBytes: toBytes,\n fromBytes: fromBytes,\n }\n })();\n\n\n // Number of rounds by keysize\n var numberOfRounds = {16: 10, 24: 12, 32: 14}\n\n // Round constant words\n var rcon = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91];\n\n // S-box and Inverse S-box (S is for Substitution)\n var S = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16];\n var Si =[0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d];\n\n // Transformations for encryption\n var T1 = [0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a];\n var T2 = [0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616];\n var T3 = [0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16];\n var T4 = [0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c];\n\n // Transformations for decryption\n var T5 = [0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742];\n var T6 = [0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857];\n var T7 = [0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8];\n var T8 = [0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0];\n\n // Transformations for decryption key expansion\n var U1 = [0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3];\n var U2 = [0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697];\n var U3 = [0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46];\n var U4 = [0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d];\n\n function convertToInt32(bytes) {\n var result = [];\n for (var i = 0; i < bytes.length; i += 4) {\n result.push(\n (bytes[i ] << 24) |\n (bytes[i + 1] << 16) |\n (bytes[i + 2] << 8) |\n bytes[i + 3]\n );\n }\n return result;\n }\n\n var AES = function(key) {\n if (!(this instanceof AES)) {\n throw Error('AES must be instanitated with `new`');\n }\n\n Object.defineProperty(this, 'key', {\n value: coerceArray(key, true)\n });\n\n this._prepare();\n }\n\n\n AES.prototype._prepare = function() {\n\n var rounds = numberOfRounds[this.key.length];\n if (rounds == null) {\n throw new Error('invalid key size (must be 16, 24 or 32 bytes)');\n }\n\n // encryption round keys\n this._Ke = [];\n\n // decryption round keys\n this._Kd = [];\n\n for (var i = 0; i <= rounds; i++) {\n this._Ke.push([0, 0, 0, 0]);\n this._Kd.push([0, 0, 0, 0]);\n }\n\n var roundKeyCount = (rounds + 1) * 4;\n var KC = this.key.length / 4;\n\n // convert the key into ints\n var tk = convertToInt32(this.key);\n\n // copy values into round key arrays\n var index;\n for (var i = 0; i < KC; i++) {\n index = i >> 2;\n this._Ke[index][i % 4] = tk[i];\n this._Kd[rounds - index][i % 4] = tk[i];\n }\n\n // key expansion (fips-197 section 5.2)\n var rconpointer = 0;\n var t = KC, tt;\n while (t < roundKeyCount) {\n tt = tk[KC - 1];\n tk[0] ^= ((S[(tt >> 16) & 0xFF] << 24) ^\n (S[(tt >> 8) & 0xFF] << 16) ^\n (S[ tt & 0xFF] << 8) ^\n S[(tt >> 24) & 0xFF] ^\n (rcon[rconpointer] << 24));\n rconpointer += 1;\n\n // key expansion (for non-256 bit)\n if (KC != 8) {\n for (var i = 1; i < KC; i++) {\n tk[i] ^= tk[i - 1];\n }\n\n // key expansion for 256-bit keys is \"slightly different\" (fips-197)\n } else {\n for (var i = 1; i < (KC / 2); i++) {\n tk[i] ^= tk[i - 1];\n }\n tt = tk[(KC / 2) - 1];\n\n tk[KC / 2] ^= (S[ tt & 0xFF] ^\n (S[(tt >> 8) & 0xFF] << 8) ^\n (S[(tt >> 16) & 0xFF] << 16) ^\n (S[(tt >> 24) & 0xFF] << 24));\n\n for (var i = (KC / 2) + 1; i < KC; i++) {\n tk[i] ^= tk[i - 1];\n }\n }\n\n // copy values into round key arrays\n var i = 0, r, c;\n while (i < KC && t < roundKeyCount) {\n r = t >> 2;\n c = t % 4;\n this._Ke[r][c] = tk[i];\n this._Kd[rounds - r][c] = tk[i++];\n t++;\n }\n }\n\n // inverse-cipher-ify the decryption round key (fips-197 section 5.3)\n for (var r = 1; r < rounds; r++) {\n for (var c = 0; c < 4; c++) {\n tt = this._Kd[r][c];\n this._Kd[r][c] = (U1[(tt >> 24) & 0xFF] ^\n U2[(tt >> 16) & 0xFF] ^\n U3[(tt >> 8) & 0xFF] ^\n U4[ tt & 0xFF]);\n }\n }\n }\n\n AES.prototype.encrypt = function(plaintext) {\n if (plaintext.length != 16) {\n throw new Error('invalid plaintext size (must be 16 bytes)');\n }\n\n var rounds = this._Ke.length - 1;\n var a = [0, 0, 0, 0];\n\n // convert plaintext to (ints ^ key)\n var t = convertToInt32(plaintext);\n for (var i = 0; i < 4; i++) {\n t[i] ^= this._Ke[0][i];\n }\n\n // apply round transforms\n for (var r = 1; r < rounds; r++) {\n for (var i = 0; i < 4; i++) {\n a[i] = (T1[(t[ i ] >> 24) & 0xff] ^\n T2[(t[(i + 1) % 4] >> 16) & 0xff] ^\n T3[(t[(i + 2) % 4] >> 8) & 0xff] ^\n T4[ t[(i + 3) % 4] & 0xff] ^\n this._Ke[r][i]);\n }\n t = a.slice();\n }\n\n // the last round is special\n var result = createArray(16), tt;\n for (var i = 0; i < 4; i++) {\n tt = this._Ke[rounds][i];\n result[4 * i ] = (S[(t[ i ] >> 24) & 0xff] ^ (tt >> 24)) & 0xff;\n result[4 * i + 1] = (S[(t[(i + 1) % 4] >> 16) & 0xff] ^ (tt >> 16)) & 0xff;\n result[4 * i + 2] = (S[(t[(i + 2) % 4] >> 8) & 0xff] ^ (tt >> 8)) & 0xff;\n result[4 * i + 3] = (S[ t[(i + 3) % 4] & 0xff] ^ tt ) & 0xff;\n }\n\n return result;\n }\n\n AES.prototype.decrypt = function(ciphertext) {\n if (ciphertext.length != 16) {\n throw new Error('invalid ciphertext size (must be 16 bytes)');\n }\n\n var rounds = this._Kd.length - 1;\n var a = [0, 0, 0, 0];\n\n // convert plaintext to (ints ^ key)\n var t = convertToInt32(ciphertext);\n for (var i = 0; i < 4; i++) {\n t[i] ^= this._Kd[0][i];\n }\n\n // apply round transforms\n for (var r = 1; r < rounds; r++) {\n for (var i = 0; i < 4; i++) {\n a[i] = (T5[(t[ i ] >> 24) & 0xff] ^\n T6[(t[(i + 3) % 4] >> 16) & 0xff] ^\n T7[(t[(i + 2) % 4] >> 8) & 0xff] ^\n T8[ t[(i + 1) % 4] & 0xff] ^\n this._Kd[r][i]);\n }\n t = a.slice();\n }\n\n // the last round is special\n var result = createArray(16), tt;\n for (var i = 0; i < 4; i++) {\n tt = this._Kd[rounds][i];\n result[4 * i ] = (Si[(t[ i ] >> 24) & 0xff] ^ (tt >> 24)) & 0xff;\n result[4 * i + 1] = (Si[(t[(i + 3) % 4] >> 16) & 0xff] ^ (tt >> 16)) & 0xff;\n result[4 * i + 2] = (Si[(t[(i + 2) % 4] >> 8) & 0xff] ^ (tt >> 8)) & 0xff;\n result[4 * i + 3] = (Si[ t[(i + 1) % 4] & 0xff] ^ tt ) & 0xff;\n }\n\n return result;\n }\n\n\n /**\n * Mode Of Operation - Electonic Codebook (ECB)\n */\n var ModeOfOperationECB = function(key) {\n if (!(this instanceof ModeOfOperationECB)) {\n throw Error('AES must be instanitated with `new`');\n }\n\n this.description = \"Electronic Code Block\";\n this.name = \"ecb\";\n\n this._aes = new AES(key);\n }\n\n ModeOfOperationECB.prototype.encrypt = function(plaintext) {\n plaintext = coerceArray(plaintext);\n\n if ((plaintext.length % 16) !== 0) {\n throw new Error('invalid plaintext size (must be multiple of 16 bytes)');\n }\n\n var ciphertext = createArray(plaintext.length);\n var block = createArray(16);\n\n for (var i = 0; i < plaintext.length; i += 16) {\n copyArray(plaintext, block, 0, i, i + 16);\n block = this._aes.encrypt(block);\n copyArray(block, ciphertext, i);\n }\n\n return ciphertext;\n }\n\n ModeOfOperationECB.prototype.decrypt = function(ciphertext) {\n ciphertext = coerceArray(ciphertext);\n\n if ((ciphertext.length % 16) !== 0) {\n throw new Error('invalid ciphertext size (must be multiple of 16 bytes)');\n }\n\n var plaintext = createArray(ciphertext.length);\n var block = createArray(16);\n\n for (var i = 0; i < ciphertext.length; i += 16) {\n copyArray(ciphertext, block, 0, i, i + 16);\n block = this._aes.decrypt(block);\n copyArray(block, plaintext, i);\n }\n\n return plaintext;\n }\n\n\n /**\n * Mode Of Operation - Cipher Block Chaining (CBC)\n */\n var ModeOfOperationCBC = function(key, iv) {\n if (!(this instanceof ModeOfOperationCBC)) {\n throw Error('AES must be instanitated with `new`');\n }\n\n this.description = \"Cipher Block Chaining\";\n this.name = \"cbc\";\n\n if (!iv) {\n iv = createArray(16);\n\n } else if (iv.length != 16) {\n throw new Error('invalid initialation vector size (must be 16 bytes)');\n }\n\n this._lastCipherblock = coerceArray(iv, true);\n\n this._aes = new AES(key);\n }\n\n ModeOfOperationCBC.prototype.encrypt = function(plaintext) {\n plaintext = coerceArray(plaintext);\n\n if ((plaintext.length % 16) !== 0) {\n throw new Error('invalid plaintext size (must be multiple of 16 bytes)');\n }\n\n var ciphertext = createArray(plaintext.length);\n var block = createArray(16);\n\n for (var i = 0; i < plaintext.length; i += 16) {\n copyArray(plaintext, block, 0, i, i + 16);\n\n for (var j = 0; j < 16; j++) {\n block[j] ^= this._lastCipherblock[j];\n }\n\n this._lastCipherblock = this._aes.encrypt(block);\n copyArray(this._lastCipherblock, ciphertext, i);\n }\n\n return ciphertext;\n }\n\n ModeOfOperationCBC.prototype.decrypt = function(ciphertext) {\n ciphertext = coerceArray(ciphertext);\n\n if ((ciphertext.length % 16) !== 0) {\n throw new Error('invalid ciphertext size (must be multiple of 16 bytes)');\n }\n\n var plaintext = createArray(ciphertext.length);\n var block = createArray(16);\n\n for (var i = 0; i < ciphertext.length; i += 16) {\n copyArray(ciphertext, block, 0, i, i + 16);\n block = this._aes.decrypt(block);\n\n for (var j = 0; j < 16; j++) {\n plaintext[i + j] = block[j] ^ this._lastCipherblock[j];\n }\n\n copyArray(ciphertext, this._lastCipherblock, 0, i, i + 16);\n }\n\n return plaintext;\n }\n\n\n /**\n * Mode Of Operation - Cipher Feedback (CFB)\n */\n var ModeOfOperationCFB = function(key, iv, segmentSize) {\n if (!(this instanceof ModeOfOperationCFB)) {\n throw Error('AES must be instanitated with `new`');\n }\n\n this.description = \"Cipher Feedback\";\n this.name = \"cfb\";\n\n if (!iv) {\n iv = createArray(16);\n\n } else if (iv.length != 16) {\n throw new Error('invalid initialation vector size (must be 16 size)');\n }\n\n if (!segmentSize) { segmentSize = 1; }\n\n this.segmentSize = segmentSize;\n\n this._shiftRegister = coerceArray(iv, true);\n\n this._aes = new AES(key);\n }\n\n ModeOfOperationCFB.prototype.encrypt = function(plaintext) {\n if ((plaintext.length % this.segmentSize) != 0) {\n throw new Error('invalid plaintext size (must be segmentSize bytes)');\n }\n\n var encrypted = coerceArray(plaintext, true);\n\n var xorSegment;\n for (var i = 0; i < encrypted.length; i += this.segmentSize) {\n xorSegment = this._aes.encrypt(this._shiftRegister);\n for (var j = 0; j < this.segmentSize; j++) {\n encrypted[i + j] ^= xorSegment[j];\n }\n\n // Shift the register\n copyArray(this._shiftRegister, this._shiftRegister, 0, this.segmentSize);\n copyArray(encrypted, this._shiftRegister, 16 - this.segmentSize, i, i + this.segmentSize);\n }\n\n return encrypted;\n }\n\n ModeOfOperationCFB.prototype.decrypt = function(ciphertext) {\n if ((ciphertext.length % this.segmentSize) != 0) {\n throw new Error('invalid ciphertext size (must be segmentSize bytes)');\n }\n\n var plaintext = coerceArray(ciphertext, true);\n\n var xorSegment;\n for (var i = 0; i < plaintext.length; i += this.segmentSize) {\n xorSegment = this._aes.encrypt(this._shiftRegister);\n\n for (var j = 0; j < this.segmentSize; j++) {\n plaintext[i + j] ^= xorSegment[j];\n }\n\n // Shift the register\n copyArray(this._shiftRegister, this._shiftRegister, 0, this.segmentSize);\n copyArray(ciphertext, this._shiftRegister, 16 - this.segmentSize, i, i + this.segmentSize);\n }\n\n return plaintext;\n }\n\n /**\n * Mode Of Operation - Output Feedback (OFB)\n */\n var ModeOfOperationOFB = function(key, iv) {\n if (!(this instanceof ModeOfOperationOFB)) {\n throw Error('AES must be instanitated with `new`');\n }\n\n this.description = \"Output Feedback\";\n this.name = \"ofb\";\n\n if (!iv) {\n iv = createArray(16);\n\n } else if (iv.length != 16) {\n throw new Error('invalid initialation vector size (must be 16 bytes)');\n }\n\n this._lastPrecipher = coerceArray(iv, true);\n this._lastPrecipherIndex = 16;\n\n this._aes = new AES(key);\n }\n\n ModeOfOperationOFB.prototype.encrypt = function(plaintext) {\n var encrypted = coerceArray(plaintext, true);\n\n for (var i = 0; i < encrypted.length; i++) {\n if (this._lastPrecipherIndex === 16) {\n this._lastPrecipher = this._aes.encrypt(this._lastPrecipher);\n this._lastPrecipherIndex = 0;\n }\n encrypted[i] ^= this._lastPrecipher[this._lastPrecipherIndex++];\n }\n\n return encrypted;\n }\n\n // Decryption is symetric\n ModeOfOperationOFB.prototype.decrypt = ModeOfOperationOFB.prototype.encrypt;\n\n\n /**\n * Counter object for CTR common mode of operation\n */\n var Counter = function(initialValue) {\n if (!(this instanceof Counter)) {\n throw Error('Counter must be instanitated with `new`');\n }\n\n // We allow 0, but anything false-ish uses the default 1\n if (initialValue !== 0 && !initialValue) { initialValue = 1; }\n\n if (typeof(initialValue) === 'number') {\n this._counter = createArray(16);\n this.setValue(initialValue);\n\n } else {\n this.setBytes(initialValue);\n }\n }\n\n Counter.prototype.setValue = function(value) {\n if (typeof(value) !== 'number' || parseInt(value) != value) {\n throw new Error('invalid counter value (must be an integer)');\n }\n\n // We cannot safely handle numbers beyond the safe range for integers\n if (value > Number.MAX_SAFE_INTEGER) {\n throw new Error('integer value out of safe range');\n }\n\n for (var index = 15; index >= 0; --index) {\n this._counter[index] = value % 256;\n value = parseInt(value / 256);\n }\n }\n\n Counter.prototype.setBytes = function(bytes) {\n bytes = coerceArray(bytes, true);\n\n if (bytes.length != 16) {\n throw new Error('invalid counter bytes size (must be 16 bytes)');\n }\n\n this._counter = bytes;\n };\n\n Counter.prototype.increment = function() {\n for (var i = 15; i >= 0; i--) {\n if (this._counter[i] === 255) {\n this._counter[i] = 0;\n } else {\n this._counter[i]++;\n break;\n }\n }\n }\n\n\n /**\n * Mode Of Operation - Counter (CTR)\n */\n var ModeOfOperationCTR = function(key, counter) {\n if (!(this instanceof ModeOfOperationCTR)) {\n throw Error('AES must be instanitated with `new`');\n }\n\n this.description = \"Counter\";\n this.name = \"ctr\";\n\n if (!(counter instanceof Counter)) {\n counter = new Counter(counter)\n }\n\n this._counter = counter;\n\n this._remainingCounter = null;\n this._remainingCounterIndex = 16;\n\n this._aes = new AES(key);\n }\n\n ModeOfOperationCTR.prototype.encrypt = function(plaintext) {\n var encrypted = coerceArray(plaintext, true);\n\n for (var i = 0; i < encrypted.length; i++) {\n if (this._remainingCounterIndex === 16) {\n this._remainingCounter = this._aes.encrypt(this._counter._counter);\n this._remainingCounterIndex = 0;\n this._counter.increment();\n }\n encrypted[i] ^= this._remainingCounter[this._remainingCounterIndex++];\n }\n\n return encrypted;\n }\n\n // Decryption is symetric\n ModeOfOperationCTR.prototype.decrypt = ModeOfOperationCTR.prototype.encrypt;\n\n\n ///////////////////////\n // Padding\n\n // See:https://tools.ietf.org/html/rfc2315\n function pkcs7pad(data) {\n data = coerceArray(data, true);\n var padder = 16 - (data.length % 16);\n var result = createArray(data.length + padder);\n copyArray(data, result);\n for (var i = data.length; i < result.length; i++) {\n result[i] = padder;\n }\n return result;\n }\n\n function pkcs7strip(data) {\n data = coerceArray(data, true);\n if (data.length < 16) { throw new Error('PKCS#7 invalid length'); }\n\n var padder = data[data.length - 1];\n if (padder > 16) { throw new Error('PKCS#7 padding byte out of range'); }\n\n var length = data.length - padder;\n for (var i = 0; i < padder; i++) {\n if (data[length + i] !== padder) {\n throw new Error('PKCS#7 invalid padding byte');\n }\n }\n\n var result = createArray(length);\n copyArray(data, result, 0, 0, length);\n return result;\n }\n\n ///////////////////////\n // Exporting\n\n\n // The block cipher\n var aesjs = {\n AES: AES,\n Counter: Counter,\n\n ModeOfOperation: {\n ecb: ModeOfOperationECB,\n cbc: ModeOfOperationCBC,\n cfb: ModeOfOperationCFB,\n ofb: ModeOfOperationOFB,\n ctr: ModeOfOperationCTR\n },\n\n utils: {\n hex: convertHex,\n utf8: convertUtf8\n },\n\n padding: {\n pkcs7: {\n pad: pkcs7pad,\n strip: pkcs7strip\n }\n },\n\n _arrayTest: {\n coerceArray: coerceArray,\n createArray: createArray,\n copyArray: copyArray,\n }\n };\n\n\n // node.js\n if (typeof exports !== 'undefined') {\n module.exports = aesjs\n\n // RequireJS/AMD\n // http://www.requirejs.org/docs/api.html\n // https://github.com/amdjs/amdjs-api/wiki/AMD\n } else if (typeof(define) === 'function' && define.amd) {\n define([], function() { return aesjs; });\n\n // Web Browsers\n } else {\n\n // If there was an existing library at \"aesjs\" make sure it's still available\n if (root.aesjs) {\n aesjs._aesjs = root.aesjs;\n }\n\n root.aesjs = aesjs;\n }\n\n\n})(this);\n", "import aesjs, { type ByteSource } from 'aes-js';\n\n// See https://github.com/ricmoo/aes-js\n// We can use keys that are 128 bits (16 bytes), 192 bits (24 bytes) or 256 bits (32 bytes).\n// To generate a random key: window.crypto.getRandomValues(new Uint8Array(16));\n\n// This default signing key is built into iD and can be used to mask/unmask sensitive values.\nconst DEFAULT_128 = [250, 157, 60, 79, 142, 134, 229, 129, 138, 126, 210, 129, 29, 71, 160, 208];\n\n\nexport function utilAesEncrypt(text: string, key?: ByteSource) {\n key = key || DEFAULT_128;\n const textBytes = aesjs.utils.utf8.toBytes(text);\n const aesCtr = new aesjs.ModeOfOperation.ctr(key);\n const encryptedBytes = aesCtr.encrypt(textBytes);\n const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes);\n return encryptedHex;\n}\n\n\nexport function utilAesDecrypt(encryptedHex: string, key?: ByteSource) {\n key = key || DEFAULT_128;\n const encryptedBytes = aesjs.utils.hex.toBytes(encryptedHex);\n const aesCtr = new aesjs.ModeOfOperation.ctr(key);\n const decryptedBytes = aesCtr.decrypt(encryptedBytes);\n const text = aesjs.utils.utf8.fromBytes(decryptedBytes);\n return text;\n}\n", "// Returns true if a and b have the same elements at the same indices.\nexport function utilArrayIdentical(a, b) {\n // an array is always identical to itself\n if (a === b) return true;\n\n var i = a.length;\n if (i !== b.length) return false;\n while (i--) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n}\n\n// http://2ality.com/2015/01/es6-set-operations.html\n\n// Difference (a \\ b): create a set that contains those elements of set a that are not in set b.\n// This operation is also sometimes called minus (-).\n// var a = [1,2,3];\n// var b = [4,3,2];\n// utilArrayDifference(a, b)\n// [1]\n// utilArrayDifference(b, a)\n// [4]\nexport function utilArrayDifference(a, b) {\n var other = new Set(b);\n return Array.from(new Set(a))\n .filter(function(v) { return !other.has(v); });\n}\n\n// Intersection (a \u2229 b): create a set that contains those elements of set a that are also in set b.\n// var a = [1,2,3];\n// var b = [4,3,2];\n// utilArrayIntersection(a, b)\n// [2,3]\nexport function utilArrayIntersection(a, b) {\n var other = new Set(b);\n return Array.from(new Set(a))\n .filter(function(v) { return other.has(v); });\n}\n\n// Union (a \u222A b): create a set that contains the elements of both set a and set b.\n// var a = [1,2,3];\n// var b = [4,3,2];\n// utilArrayUnion(a, b)\n// [1,2,3,4]\nexport function utilArrayUnion(a, b) {\n var result = new Set(a);\n b.forEach(function(v) { result.add(v); });\n return Array.from(result);\n}\n\n// Returns an Array with all the duplicates removed\n// var a = [1,1,2,3,3];\n// utilArrayUniq(a)\n// [1,2,3]\nexport function utilArrayUniq(a) {\n return Array.from(new Set(a));\n}\n\n\n// Splits array into chunks of given chunk size\n// var a = [1,2,3,4,5,6,7];\n// utilArrayChunk(a, 3);\n// [[1,2,3],[4,5,6],[7]];\nexport function utilArrayChunk(a, chunkSize) {\n if (!chunkSize || chunkSize < 0) return [a.slice()];\n\n var result = new Array(Math.ceil(a.length / chunkSize));\n return Array.from(result, function(item, i) {\n return a.slice(i * chunkSize, i * chunkSize + chunkSize);\n });\n}\n\n\n// Flattens two level array into a single level\n// var a = [[1,2,3],[4,5,6],[7]];\n// utilArrayFlatten(a);\n// [1,2,3,4,5,6,7];\nexport function utilArrayFlatten(a) {\n return a.reduce(function(acc, val) {\n return acc.concat(val);\n }, []);\n}\n\n\n// Groups the items of the Array according to the given key\n// `key` can be passed as a property or as a key function\n//\n// var pets = [\n// { type: 'Dog', name: 'Spot' },\n// { type: 'Cat', name: 'Tiger' },\n// { type: 'Dog', name: 'Rover' },\n// { type: 'Cat', name: 'Leo' }\n// ];\n//\n// utilArrayGroupBy(pets, 'type')\n// {\n// 'Dog': [{type: 'Dog', name: 'Spot'}, {type: 'Dog', name: 'Rover'}],\n// 'Cat': [{type: 'Cat', name: 'Tiger'}, {type: 'Cat', name: 'Leo'}]\n// }\n//\n// utilArrayGroupBy(pets, function(item) { return item.name.length; })\n// {\n// 3: [{type: 'Cat', name: 'Leo'}],\n// 4: [{type: 'Dog', name: 'Spot'}],\n// 5: [{type: 'Cat', name: 'Tiger'}, {type: 'Dog', name: 'Rover'}]\n// }\nexport function utilArrayGroupBy(a, key) {\n return a.reduce(function(acc, item) {\n var group = (typeof key === 'function') ? key(item) : item[key];\n (acc[group] = acc[group] || []).push(item);\n return acc;\n }, {});\n}\n\n\n/**\n * @template T\n * @param {T[]} a\n * @param {string | ((item: T) => string)} key\n * @returns {T[]}\n */\n// Returns an Array with all the duplicates removed\n// where uniqueness determined by the given key\n// `key` can be passed as a property or as a key function\n//\n// var pets = [\n// { type: 'Dog', name: 'Spot' },\n// { type: 'Cat', name: 'Tiger' },\n// { type: 'Dog', name: 'Rover' },\n// { type: 'Cat', name: 'Leo' }\n// ];\n//\n// utilArrayUniqBy(pets, 'type')\n// [\n// { type: 'Dog', name: 'Spot' },\n// { type: 'Cat', name: 'Tiger' }\n// ]\n//\n// utilArrayUniqBy(pets, function(item) { return item.name.length; })\n// [\n// { type: 'Dog', name: 'Spot' },\n// { type: 'Cat', name: 'Tiger' },\n// { type: 'Cat', name: 'Leo' }\n// }\nexport function utilArrayUniqBy(a, key) {\n var seen = new Set();\n return a.reduce(function(acc, item) {\n var val = (typeof key === 'function') ? key(item) : item[key];\n if (val && !seen.has(val)) {\n seen.add(val);\n acc.push(item);\n }\n return acc;\n }, []);\n}\n", "export {\n axisTop,\n axisRight,\n axisBottom,\n axisLeft\n} from \"./axis.js\";\n", "export default x => () => x;\n", "export default function BrushEvent(type, {\n sourceEvent,\n target,\n selection,\n mode,\n dispatch\n}) {\n Object.defineProperties(this, {\n type: {value: type, enumerable: true, configurable: true},\n sourceEvent: {value: sourceEvent, enumerable: true, configurable: true},\n target: {value: target, enumerable: true, configurable: true},\n selection: {value: selection, enumerable: true, configurable: true},\n mode: {value: mode, enumerable: true, configurable: true},\n _: {value: dispatch}\n });\n}\n", "export function nopropagation(event) {\n event.stopImmediatePropagation();\n}\n\nexport default function(event) {\n event.preventDefault();\n event.stopImmediatePropagation();\n}\n", "import {dispatch} from \"d3-dispatch\";\nimport {dragDisable, dragEnable} from \"d3-drag\";\nimport {interpolate} from \"d3-interpolate\";\nimport {pointer, select} from \"d3-selection\";\nimport {interrupt} from \"d3-transition\";\nimport constant from \"./constant.js\";\nimport BrushEvent from \"./event.js\";\nimport noevent, {nopropagation} from \"./noevent.js\";\n\nvar MODE_DRAG = {name: \"drag\"},\n MODE_SPACE = {name: \"space\"},\n MODE_HANDLE = {name: \"handle\"},\n MODE_CENTER = {name: \"center\"};\n\nconst {abs, max, min} = Math;\n\nfunction number1(e) {\n return [+e[0], +e[1]];\n}\n\nfunction number2(e) {\n return [number1(e[0]), number1(e[1])];\n}\n\nvar X = {\n name: \"x\",\n handles: [\"w\", \"e\"].map(type),\n input: function(x, e) { return x == null ? null : [[+x[0], e[0][1]], [+x[1], e[1][1]]]; },\n output: function(xy) { return xy && [xy[0][0], xy[1][0]]; }\n};\n\nvar Y = {\n name: \"y\",\n handles: [\"n\", \"s\"].map(type),\n input: function(y, e) { return y == null ? null : [[e[0][0], +y[0]], [e[1][0], +y[1]]]; },\n output: function(xy) { return xy && [xy[0][1], xy[1][1]]; }\n};\n\nvar XY = {\n name: \"xy\",\n handles: [\"n\", \"w\", \"e\", \"s\", \"nw\", \"ne\", \"sw\", \"se\"].map(type),\n input: function(xy) { return xy == null ? null : number2(xy); },\n output: function(xy) { return xy; }\n};\n\nvar cursors = {\n overlay: \"crosshair\",\n selection: \"move\",\n n: \"ns-resize\",\n e: \"ew-resize\",\n s: \"ns-resize\",\n w: \"ew-resize\",\n nw: \"nwse-resize\",\n ne: \"nesw-resize\",\n se: \"nwse-resize\",\n sw: \"nesw-resize\"\n};\n\nvar flipX = {\n e: \"w\",\n w: \"e\",\n nw: \"ne\",\n ne: \"nw\",\n se: \"sw\",\n sw: \"se\"\n};\n\nvar flipY = {\n n: \"s\",\n s: \"n\",\n nw: \"sw\",\n ne: \"se\",\n se: \"ne\",\n sw: \"nw\"\n};\n\nvar signsX = {\n overlay: +1,\n selection: +1,\n n: null,\n e: +1,\n s: null,\n w: -1,\n nw: -1,\n ne: +1,\n se: +1,\n sw: -1\n};\n\nvar signsY = {\n overlay: +1,\n selection: +1,\n n: -1,\n e: null,\n s: +1,\n w: null,\n nw: -1,\n ne: -1,\n se: +1,\n sw: +1\n};\n\nfunction type(t) {\n return {type: t};\n}\n\n// Ignore right-click, since that should open the context menu.\nfunction defaultFilter(event) {\n return !event.ctrlKey && !event.button;\n}\n\nfunction defaultExtent() {\n var svg = this.ownerSVGElement || this;\n if (svg.hasAttribute(\"viewBox\")) {\n svg = svg.viewBox.baseVal;\n return [[svg.x, svg.y], [svg.x + svg.width, svg.y + svg.height]];\n }\n return [[0, 0], [svg.width.baseVal.value, svg.height.baseVal.value]];\n}\n\nfunction defaultTouchable() {\n return navigator.maxTouchPoints || (\"ontouchstart\" in this);\n}\n\n// Like d3.local, but with the name \u201C__brush\u201D rather than auto-generated.\nfunction local(node) {\n while (!node.__brush) if (!(node = node.parentNode)) return;\n return node.__brush;\n}\n\nfunction empty(extent) {\n return extent[0][0] === extent[1][0]\n || extent[0][1] === extent[1][1];\n}\n\nexport function brushSelection(node) {\n var state = node.__brush;\n return state ? state.dim.output(state.selection) : null;\n}\n\nexport function brushX() {\n return brush(X);\n}\n\nexport function brushY() {\n return brush(Y);\n}\n\nexport default function() {\n return brush(XY);\n}\n\nfunction brush(dim) {\n var extent = defaultExtent,\n filter = defaultFilter,\n touchable = defaultTouchable,\n keys = true,\n listeners = dispatch(\"start\", \"brush\", \"end\"),\n handleSize = 6,\n touchending;\n\n function brush(group) {\n var overlay = group\n .property(\"__brush\", initialize)\n .selectAll(\".overlay\")\n .data([type(\"overlay\")]);\n\n overlay.enter().append(\"rect\")\n .attr(\"class\", \"overlay\")\n .attr(\"pointer-events\", \"all\")\n .attr(\"cursor\", cursors.overlay)\n .merge(overlay)\n .each(function() {\n var extent = local(this).extent;\n select(this)\n .attr(\"x\", extent[0][0])\n .attr(\"y\", extent[0][1])\n .attr(\"width\", extent[1][0] - extent[0][0])\n .attr(\"height\", extent[1][1] - extent[0][1]);\n });\n\n group.selectAll(\".selection\")\n .data([type(\"selection\")])\n .enter().append(\"rect\")\n .attr(\"class\", \"selection\")\n .attr(\"cursor\", cursors.selection)\n .attr(\"fill\", \"#777\")\n .attr(\"fill-opacity\", 0.3)\n .attr(\"stroke\", \"#fff\")\n .attr(\"shape-rendering\", \"crispEdges\");\n\n var handle = group.selectAll(\".handle\")\n .data(dim.handles, function(d) { return d.type; });\n\n handle.exit().remove();\n\n handle.enter().append(\"rect\")\n .attr(\"class\", function(d) { return \"handle handle--\" + d.type; })\n .attr(\"cursor\", function(d) { return cursors[d.type]; });\n\n group\n .each(redraw)\n .attr(\"fill\", \"none\")\n .attr(\"pointer-events\", \"all\")\n .on(\"mousedown.brush\", started)\n .filter(touchable)\n .on(\"touchstart.brush\", started)\n .on(\"touchmove.brush\", touchmoved)\n .on(\"touchend.brush touchcancel.brush\", touchended)\n .style(\"touch-action\", \"none\")\n .style(\"-webkit-tap-highlight-color\", \"rgba(0,0,0,0)\");\n }\n\n brush.move = function(group, selection, event) {\n if (group.tween) {\n group\n .on(\"start.brush\", function(event) { emitter(this, arguments).beforestart().start(event); })\n .on(\"interrupt.brush end.brush\", function(event) { emitter(this, arguments).end(event); })\n .tween(\"brush\", function() {\n var that = this,\n state = that.__brush,\n emit = emitter(that, arguments),\n selection0 = state.selection,\n selection1 = dim.input(typeof selection === \"function\" ? selection.apply(this, arguments) : selection, state.extent),\n i = interpolate(selection0, selection1);\n\n function tween(t) {\n state.selection = t === 1 && selection1 === null ? null : i(t);\n redraw.call(that);\n emit.brush();\n }\n\n return selection0 !== null && selection1 !== null ? tween : tween(1);\n });\n } else {\n group\n .each(function() {\n var that = this,\n args = arguments,\n state = that.__brush,\n selection1 = dim.input(typeof selection === \"function\" ? selection.apply(that, args) : selection, state.extent),\n emit = emitter(that, args).beforestart();\n\n interrupt(that);\n state.selection = selection1 === null ? null : selection1;\n redraw.call(that);\n emit.start(event).brush(event).end(event);\n });\n }\n };\n\n brush.clear = function(group, event) {\n brush.move(group, null, event);\n };\n\n function redraw() {\n var group = select(this),\n selection = local(this).selection;\n\n if (selection) {\n group.selectAll(\".selection\")\n .style(\"display\", null)\n .attr(\"x\", selection[0][0])\n .attr(\"y\", selection[0][1])\n .attr(\"width\", selection[1][0] - selection[0][0])\n .attr(\"height\", selection[1][1] - selection[0][1]);\n\n group.selectAll(\".handle\")\n .style(\"display\", null)\n .attr(\"x\", function(d) { return d.type[d.type.length - 1] === \"e\" ? selection[1][0] - handleSize / 2 : selection[0][0] - handleSize / 2; })\n .attr(\"y\", function(d) { return d.type[0] === \"s\" ? selection[1][1] - handleSize / 2 : selection[0][1] - handleSize / 2; })\n .attr(\"width\", function(d) { return d.type === \"n\" || d.type === \"s\" ? selection[1][0] - selection[0][0] + handleSize : handleSize; })\n .attr(\"height\", function(d) { return d.type === \"e\" || d.type === \"w\" ? selection[1][1] - selection[0][1] + handleSize : handleSize; });\n }\n\n else {\n group.selectAll(\".selection,.handle\")\n .style(\"display\", \"none\")\n .attr(\"x\", null)\n .attr(\"y\", null)\n .attr(\"width\", null)\n .attr(\"height\", null);\n }\n }\n\n function emitter(that, args, clean) {\n var emit = that.__brush.emitter;\n return emit && (!clean || !emit.clean) ? emit : new Emitter(that, args, clean);\n }\n\n function Emitter(that, args, clean) {\n this.that = that;\n this.args = args;\n this.state = that.__brush;\n this.active = 0;\n this.clean = clean;\n }\n\n Emitter.prototype = {\n beforestart: function() {\n if (++this.active === 1) this.state.emitter = this, this.starting = true;\n return this;\n },\n start: function(event, mode) {\n if (this.starting) this.starting = false, this.emit(\"start\", event, mode);\n else this.emit(\"brush\", event);\n return this;\n },\n brush: function(event, mode) {\n this.emit(\"brush\", event, mode);\n return this;\n },\n end: function(event, mode) {\n if (--this.active === 0) delete this.state.emitter, this.emit(\"end\", event, mode);\n return this;\n },\n emit: function(type, event, mode) {\n var d = select(this.that).datum();\n listeners.call(\n type,\n this.that,\n new BrushEvent(type, {\n sourceEvent: event,\n target: brush,\n selection: dim.output(this.state.selection),\n mode,\n dispatch: listeners\n }),\n d\n );\n }\n };\n\n function started(event) {\n if (touchending && !event.touches) return;\n if (!filter.apply(this, arguments)) return;\n\n var that = this,\n type = event.target.__data__.type,\n mode = (keys && event.metaKey ? type = \"overlay\" : type) === \"selection\" ? MODE_DRAG : (keys && event.altKey ? MODE_CENTER : MODE_HANDLE),\n signX = dim === Y ? null : signsX[type],\n signY = dim === X ? null : signsY[type],\n state = local(that),\n extent = state.extent,\n selection = state.selection,\n W = extent[0][0], w0, w1,\n N = extent[0][1], n0, n1,\n E = extent[1][0], e0, e1,\n S = extent[1][1], s0, s1,\n dx = 0,\n dy = 0,\n moving,\n shifting = signX && signY && keys && event.shiftKey,\n lockX,\n lockY,\n points = Array.from(event.touches || [event], t => {\n const i = t.identifier;\n t = pointer(t, that);\n t.point0 = t.slice();\n t.identifier = i;\n return t;\n });\n\n interrupt(that);\n var emit = emitter(that, arguments, true).beforestart();\n\n if (type === \"overlay\") {\n if (selection) moving = true;\n const pts = [points[0], points[1] || points[0]];\n state.selection = selection = [[\n w0 = dim === Y ? W : min(pts[0][0], pts[1][0]),\n n0 = dim === X ? N : min(pts[0][1], pts[1][1])\n ], [\n e0 = dim === Y ? E : max(pts[0][0], pts[1][0]),\n s0 = dim === X ? S : max(pts[0][1], pts[1][1])\n ]];\n if (points.length > 1) move(event);\n } else {\n w0 = selection[0][0];\n n0 = selection[0][1];\n e0 = selection[1][0];\n s0 = selection[1][1];\n }\n\n w1 = w0;\n n1 = n0;\n e1 = e0;\n s1 = s0;\n\n var group = select(that)\n .attr(\"pointer-events\", \"none\");\n\n var overlay = group.selectAll(\".overlay\")\n .attr(\"cursor\", cursors[type]);\n\n if (event.touches) {\n emit.moved = moved;\n emit.ended = ended;\n } else {\n var view = select(event.view)\n .on(\"mousemove.brush\", moved, true)\n .on(\"mouseup.brush\", ended, true);\n if (keys) view\n .on(\"keydown.brush\", keydowned, true)\n .on(\"keyup.brush\", keyupped, true)\n\n dragDisable(event.view);\n }\n\n redraw.call(that);\n emit.start(event, mode.name);\n\n function moved(event) {\n for (const p of event.changedTouches || [event]) {\n for (const d of points)\n if (d.identifier === p.identifier) d.cur = pointer(p, that);\n }\n if (shifting && !lockX && !lockY && points.length === 1) {\n const point = points[0];\n if (abs(point.cur[0] - point[0]) > abs(point.cur[1] - point[1]))\n lockY = true;\n else\n lockX = true;\n }\n for (const point of points)\n if (point.cur) point[0] = point.cur[0], point[1] = point.cur[1];\n moving = true;\n noevent(event);\n move(event);\n }\n\n function move(event) {\n const point = points[0], point0 = point.point0;\n var t;\n\n dx = point[0] - point0[0];\n dy = point[1] - point0[1];\n\n switch (mode) {\n case MODE_SPACE:\n case MODE_DRAG: {\n if (signX) dx = max(W - w0, min(E - e0, dx)), w1 = w0 + dx, e1 = e0 + dx;\n if (signY) dy = max(N - n0, min(S - s0, dy)), n1 = n0 + dy, s1 = s0 + dy;\n break;\n }\n case MODE_HANDLE: {\n if (points[1]) {\n if (signX) w1 = max(W, min(E, points[0][0])), e1 = max(W, min(E, points[1][0])), signX = 1;\n if (signY) n1 = max(N, min(S, points[0][1])), s1 = max(N, min(S, points[1][1])), signY = 1;\n } else {\n if (signX < 0) dx = max(W - w0, min(E - w0, dx)), w1 = w0 + dx, e1 = e0;\n else if (signX > 0) dx = max(W - e0, min(E - e0, dx)), w1 = w0, e1 = e0 + dx;\n if (signY < 0) dy = max(N - n0, min(S - n0, dy)), n1 = n0 + dy, s1 = s0;\n else if (signY > 0) dy = max(N - s0, min(S - s0, dy)), n1 = n0, s1 = s0 + dy;\n }\n break;\n }\n case MODE_CENTER: {\n if (signX) w1 = max(W, min(E, w0 - dx * signX)), e1 = max(W, min(E, e0 + dx * signX));\n if (signY) n1 = max(N, min(S, n0 - dy * signY)), s1 = max(N, min(S, s0 + dy * signY));\n break;\n }\n }\n\n if (e1 < w1) {\n signX *= -1;\n t = w0, w0 = e0, e0 = t;\n t = w1, w1 = e1, e1 = t;\n if (type in flipX) overlay.attr(\"cursor\", cursors[type = flipX[type]]);\n }\n\n if (s1 < n1) {\n signY *= -1;\n t = n0, n0 = s0, s0 = t;\n t = n1, n1 = s1, s1 = t;\n if (type in flipY) overlay.attr(\"cursor\", cursors[type = flipY[type]]);\n }\n\n if (state.selection) selection = state.selection; // May be set by brush.move!\n if (lockX) w1 = selection[0][0], e1 = selection[1][0];\n if (lockY) n1 = selection[0][1], s1 = selection[1][1];\n\n if (selection[0][0] !== w1\n || selection[0][1] !== n1\n || selection[1][0] !== e1\n || selection[1][1] !== s1) {\n state.selection = [[w1, n1], [e1, s1]];\n redraw.call(that);\n emit.brush(event, mode.name);\n }\n }\n\n function ended(event) {\n nopropagation(event);\n if (event.touches) {\n if (event.touches.length) return;\n if (touchending) clearTimeout(touchending);\n touchending = setTimeout(function() { touchending = null; }, 500); // Ghost clicks are delayed!\n } else {\n dragEnable(event.view, moving);\n view.on(\"keydown.brush keyup.brush mousemove.brush mouseup.brush\", null);\n }\n group.attr(\"pointer-events\", \"all\");\n overlay.attr(\"cursor\", cursors.overlay);\n if (state.selection) selection = state.selection; // May be set by brush.move (on start)!\n if (empty(selection)) state.selection = null, redraw.call(that);\n emit.end(event, mode.name);\n }\n\n function keydowned(event) {\n switch (event.keyCode) {\n case 16: { // SHIFT\n shifting = signX && signY;\n break;\n }\n case 18: { // ALT\n if (mode === MODE_HANDLE) {\n if (signX) e0 = e1 - dx * signX, w0 = w1 + dx * signX;\n if (signY) s0 = s1 - dy * signY, n0 = n1 + dy * signY;\n mode = MODE_CENTER;\n move(event);\n }\n break;\n }\n case 32: { // SPACE; takes priority over ALT\n if (mode === MODE_HANDLE || mode === MODE_CENTER) {\n if (signX < 0) e0 = e1 - dx; else if (signX > 0) w0 = w1 - dx;\n if (signY < 0) s0 = s1 - dy; else if (signY > 0) n0 = n1 - dy;\n mode = MODE_SPACE;\n overlay.attr(\"cursor\", cursors.selection);\n move(event);\n }\n break;\n }\n default: return;\n }\n noevent(event);\n }\n\n function keyupped(event) {\n switch (event.keyCode) {\n case 16: { // SHIFT\n if (shifting) {\n lockX = lockY = shifting = false;\n move(event);\n }\n break;\n }\n case 18: { // ALT\n if (mode === MODE_CENTER) {\n if (signX < 0) e0 = e1; else if (signX > 0) w0 = w1;\n if (signY < 0) s0 = s1; else if (signY > 0) n0 = n1;\n mode = MODE_HANDLE;\n move(event);\n }\n break;\n }\n case 32: { // SPACE\n if (mode === MODE_SPACE) {\n if (event.altKey) {\n if (signX) e0 = e1 - dx * signX, w0 = w1 + dx * signX;\n if (signY) s0 = s1 - dy * signY, n0 = n1 + dy * signY;\n mode = MODE_CENTER;\n } else {\n if (signX < 0) e0 = e1; else if (signX > 0) w0 = w1;\n if (signY < 0) s0 = s1; else if (signY > 0) n0 = n1;\n mode = MODE_HANDLE;\n }\n overlay.attr(\"cursor\", cursors[type]);\n move(event);\n }\n break;\n }\n default: return;\n }\n noevent(event);\n }\n }\n\n function touchmoved(event) {\n emitter(this, arguments).moved(event);\n }\n\n function touchended(event) {\n emitter(this, arguments).ended(event);\n }\n\n function initialize() {\n var state = this.__brush || {selection: null};\n state.extent = number2(extent.apply(this, arguments));\n state.dim = dim;\n return state;\n }\n\n brush.extent = function(_) {\n return arguments.length ? (extent = typeof _ === \"function\" ? _ : constant(number2(_)), brush) : extent;\n };\n\n brush.filter = function(_) {\n return arguments.length ? (filter = typeof _ === \"function\" ? _ : constant(!!_), brush) : filter;\n };\n\n brush.touchable = function(_) {\n return arguments.length ? (touchable = typeof _ === \"function\" ? _ : constant(!!_), brush) : touchable;\n };\n\n brush.handleSize = function(_) {\n return arguments.length ? (handleSize = +_, brush) : handleSize;\n };\n\n brush.keyModifiers = function(_) {\n return arguments.length ? (keys = !!_, brush) : keys;\n };\n\n brush.on = function() {\n var value = listeners.on.apply(listeners, arguments);\n return value === listeners ? brush : value;\n };\n\n return brush;\n}\n", "export {\n default as brush,\n brushX,\n brushY,\n brushSelection\n} from \"./brush.js\";\n", "export {Path, path, pathRound} from \"./path.js\";\n", "export {default as chord, chordTranspose, chordDirected} from \"./chord.js\";\nexport {default as ribbon, ribbonArrow} from \"./ribbon.js\";\n", "export {default as contours} from \"./contours.js\";\nexport {default as contourDensity} from \"./density.js\";\n", "export {default as Delaunay} from \"./delaunay.js\";\nexport {default as Voronoi} from \"./voronoi.js\";\n", "var EOL = {},\n EOF = {},\n QUOTE = 34,\n NEWLINE = 10,\n RETURN = 13;\n\nfunction objectConverter(columns) {\n return new Function(\"d\", \"return {\" + columns.map(function(name, i) {\n return JSON.stringify(name) + \": d[\" + i + \"] || \\\"\\\"\";\n }).join(\",\") + \"}\");\n}\n\nfunction customConverter(columns, f) {\n var object = objectConverter(columns);\n return function(row, i) {\n return f(object(row), i, columns);\n };\n}\n\n// Compute unique columns in order of discovery.\nfunction inferColumns(rows) {\n var columnSet = Object.create(null),\n columns = [];\n\n rows.forEach(function(row) {\n for (var column in row) {\n if (!(column in columnSet)) {\n columns.push(columnSet[column] = column);\n }\n }\n });\n\n return columns;\n}\n\nfunction pad(value, width) {\n var s = value + \"\", length = s.length;\n return length < width ? new Array(width - length + 1).join(0) + s : s;\n}\n\nfunction formatYear(year) {\n return year < 0 ? \"-\" + pad(-year, 6)\n : year > 9999 ? \"+\" + pad(year, 6)\n : pad(year, 4);\n}\n\nfunction formatDate(date) {\n var hours = date.getUTCHours(),\n minutes = date.getUTCMinutes(),\n seconds = date.getUTCSeconds(),\n milliseconds = date.getUTCMilliseconds();\n return isNaN(date) ? \"Invalid Date\"\n : formatYear(date.getUTCFullYear(), 4) + \"-\" + pad(date.getUTCMonth() + 1, 2) + \"-\" + pad(date.getUTCDate(), 2)\n + (milliseconds ? \"T\" + pad(hours, 2) + \":\" + pad(minutes, 2) + \":\" + pad(seconds, 2) + \".\" + pad(milliseconds, 3) + \"Z\"\n : seconds ? \"T\" + pad(hours, 2) + \":\" + pad(minutes, 2) + \":\" + pad(seconds, 2) + \"Z\"\n : minutes || hours ? \"T\" + pad(hours, 2) + \":\" + pad(minutes, 2) + \"Z\"\n : \"\");\n}\n\nexport default function(delimiter) {\n var reFormat = new RegExp(\"[\\\"\" + delimiter + \"\\n\\r]\"),\n DELIMITER = delimiter.charCodeAt(0);\n\n function parse(text, f) {\n var convert, columns, rows = parseRows(text, function(row, i) {\n if (convert) return convert(row, i - 1);\n columns = row, convert = f ? customConverter(row, f) : objectConverter(row);\n });\n rows.columns = columns || [];\n return rows;\n }\n\n function parseRows(text, f) {\n var rows = [], // output rows\n N = text.length,\n I = 0, // current character index\n n = 0, // current line number\n t, // current token\n eof = N <= 0, // current token followed by EOF?\n eol = false; // current token followed by EOL?\n\n // Strip the trailing newline.\n if (text.charCodeAt(N - 1) === NEWLINE) --N;\n if (text.charCodeAt(N - 1) === RETURN) --N;\n\n function token() {\n if (eof) return EOF;\n if (eol) return eol = false, EOL;\n\n // Unescape quotes.\n var i, j = I, c;\n if (text.charCodeAt(j) === QUOTE) {\n while (I++ < N && text.charCodeAt(I) !== QUOTE || text.charCodeAt(++I) === QUOTE);\n if ((i = I) >= N) eof = true;\n else if ((c = text.charCodeAt(I++)) === NEWLINE) eol = true;\n else if (c === RETURN) { eol = true; if (text.charCodeAt(I) === NEWLINE) ++I; }\n return text.slice(j + 1, i - 1).replace(/\"\"/g, \"\\\"\");\n }\n\n // Find next delimiter or newline.\n while (I < N) {\n if ((c = text.charCodeAt(i = I++)) === NEWLINE) eol = true;\n else if (c === RETURN) { eol = true; if (text.charCodeAt(I) === NEWLINE) ++I; }\n else if (c !== DELIMITER) continue;\n return text.slice(j, i);\n }\n\n // Return last token before EOF.\n return eof = true, text.slice(j, N);\n }\n\n while ((t = token()) !== EOF) {\n var row = [];\n while (t !== EOL && t !== EOF) row.push(t), t = token();\n if (f && (row = f(row, n++)) == null) continue;\n rows.push(row);\n }\n\n return rows;\n }\n\n function preformatBody(rows, columns) {\n return rows.map(function(row) {\n return columns.map(function(column) {\n return formatValue(row[column]);\n }).join(delimiter);\n });\n }\n\n function format(rows, columns) {\n if (columns == null) columns = inferColumns(rows);\n return [columns.map(formatValue).join(delimiter)].concat(preformatBody(rows, columns)).join(\"\\n\");\n }\n\n function formatBody(rows, columns) {\n if (columns == null) columns = inferColumns(rows);\n return preformatBody(rows, columns).join(\"\\n\");\n }\n\n function formatRows(rows) {\n return rows.map(formatRow).join(\"\\n\");\n }\n\n function formatRow(row) {\n return row.map(formatValue).join(delimiter);\n }\n\n function formatValue(value) {\n return value == null ? \"\"\n : value instanceof Date ? formatDate(value)\n : reFormat.test(value += \"\") ? \"\\\"\" + value.replace(/\"/g, \"\\\"\\\"\") + \"\\\"\"\n : value;\n }\n\n return {\n parse: parse,\n parseRows: parseRows,\n format: format,\n formatBody: formatBody,\n formatRows: formatRows,\n formatRow: formatRow,\n formatValue: formatValue\n };\n}\n", "import dsv from \"./dsv.js\";\n\nvar csv = dsv(\",\");\n\nexport var csvParse = csv.parse;\nexport var csvParseRows = csv.parseRows;\nexport var csvFormat = csv.format;\nexport var csvFormatBody = csv.formatBody;\nexport var csvFormatRows = csv.formatRows;\nexport var csvFormatRow = csv.formatRow;\nexport var csvFormatValue = csv.formatValue;\n", "import dsv from \"./dsv.js\";\n\nvar tsv = dsv(\"\\t\");\n\nexport var tsvParse = tsv.parse;\nexport var tsvParseRows = tsv.parseRows;\nexport var tsvFormat = tsv.format;\nexport var tsvFormatBody = tsv.formatBody;\nexport var tsvFormatRows = tsv.formatRows;\nexport var tsvFormatRow = tsv.formatRow;\nexport var tsvFormatValue = tsv.formatValue;\n", "export default function autoType(object) {\n for (var key in object) {\n var value = object[key].trim(), number, m;\n if (!value) value = null;\n else if (value === \"true\") value = true;\n else if (value === \"false\") value = false;\n else if (value === \"NaN\") value = NaN;\n else if (!isNaN(number = +value)) value = number;\n else if (m = value.match(/^([-+]\\d{2})?\\d{4}(-\\d{2}(-\\d{2})?)?(T\\d{2}:\\d{2}(:\\d{2}(\\.\\d{3})?)?(Z|[-+]\\d{2}:\\d{2})?)?$/)) {\n if (fixtz && !!m[4] && !m[7]) value = value.replace(/-/g, \"/\").replace(/T/, \" \");\n value = new Date(value);\n }\n else continue;\n object[key] = value;\n }\n return object;\n}\n\n// https://github.com/d3/d3-dsv/issues/45\nconst fixtz = new Date(\"2019-01-01T00:00\").getHours() || new Date(\"2019-07-01T00:00\").getHours();", "export {default as dsvFormat} from \"./dsv.js\";\nexport {csvParse, csvParseRows, csvFormat, csvFormatBody, csvFormatRows, csvFormatRow, csvFormatValue} from \"./csv.js\";\nexport {tsvParse, tsvParseRows, tsvFormat, tsvFormatBody, tsvFormatRows, tsvFormatRow, tsvFormatValue} from \"./tsv.js\";\nexport {default as autoType} from \"./autoType.js\";\n", "function responseBlob(response) {\n if (!response.ok) throw new Error(response.status + \" \" + response.statusText);\n return response.blob();\n}\n\nexport default function(input, init) {\n return fetch(input, init).then(responseBlob);\n}\n", "function responseArrayBuffer(response) {\n if (!response.ok) throw new Error(response.status + \" \" + response.statusText);\n return response.arrayBuffer();\n}\n\nexport default function(input, init) {\n return fetch(input, init).then(responseArrayBuffer);\n}\n", "function responseText(response) {\n if (!response.ok) throw new Error(response.status + \" \" + response.statusText);\n return response.text();\n}\n\nexport default function(input, init) {\n return fetch(input, init).then(responseText);\n}\n", "import {csvParse, dsvFormat, tsvParse} from \"d3-dsv\";\nimport text from \"./text.js\";\n\nfunction dsvParse(parse) {\n return function(input, init, row) {\n if (arguments.length === 2 && typeof init === \"function\") row = init, init = undefined;\n return text(input, init).then(function(response) {\n return parse(response, row);\n });\n };\n}\n\nexport default function dsv(delimiter, input, init, row) {\n if (arguments.length === 3 && typeof init === \"function\") row = init, init = undefined;\n var format = dsvFormat(delimiter);\n return text(input, init).then(function(response) {\n return format.parse(response, row);\n });\n}\n\nexport var csv = dsvParse(csvParse);\nexport var tsv = dsvParse(tsvParse);\n", "export default function(input, init) {\n return new Promise(function(resolve, reject) {\n var image = new Image;\n for (var key in init) image[key] = init[key];\n image.onerror = reject;\n image.onload = function() { resolve(image); };\n image.src = input;\n });\n}\n", "function responseJson(response) {\n if (!response.ok) throw new Error(response.status + \" \" + response.statusText);\n if (response.status === 204 || response.status === 205) return;\n return response.json();\n}\n\nexport default function(input, init) {\n return fetch(input, init).then(responseJson);\n}\n", "import text from \"./text.js\";\n\nfunction parser(type) {\n return (input, init) => text(input, init)\n .then(text => (new DOMParser).parseFromString(text, type));\n}\n\nexport default parser(\"application/xml\");\n\nexport var html = parser(\"text/html\");\n\nexport var svg = parser(\"image/svg+xml\");\n", "export {default as blob} from \"./blob.js\";\nexport {default as buffer} from \"./buffer.js\";\nexport {default as dsv, csv, tsv} from \"./dsv.js\";\nexport {default as image} from \"./image.js\";\nexport {default as json} from \"./json.js\";\nexport {default as text} from \"./text.js\";\nexport {default as xml, html, svg} from \"./xml.js\";\n", "export {default as quadtree} from \"./quadtree.js\";\n", "export {default as forceCenter} from \"./center.js\";\nexport {default as forceCollide} from \"./collide.js\";\nexport {default as forceLink} from \"./link.js\";\nexport {default as forceManyBody} from \"./manyBody.js\";\nexport {default as forceRadial} from \"./radial.js\";\nexport {default as forceSimulation} from \"./simulation.js\";\nexport {default as forceX} from \"./x.js\";\nexport {default as forceY} from \"./y.js\";\n", "export default function(x) {\n return Math.abs(x = Math.round(x)) >= 1e21\n ? x.toLocaleString(\"en\").replace(/,/g, \"\")\n : x.toString(10);\n}\n\n// Computes the decimal coefficient and exponent of the specified number x with\n// significant digits p, where x is positive and p is in [1, 21] or undefined.\n// For example, formatDecimalParts(1.23) returns [\"123\", 0].\nexport function formatDecimalParts(x, p) {\n if ((i = (x = p ? x.toExponential(p - 1) : x.toExponential()).indexOf(\"e\")) < 0) return null; // NaN, \u00B1Infinity\n var i, coefficient = x.slice(0, i);\n\n // The string returned by toExponential either has the form \\d\\.\\d+e[-+]\\d+\n // (e.g., 1.2e+3) or the form \\de[-+]\\d+ (e.g., 1e+3).\n return [\n coefficient.length > 1 ? coefficient[0] + coefficient.slice(2) : coefficient,\n +x.slice(i + 1)\n ];\n}\n", "import {formatDecimalParts} from \"./formatDecimal.js\";\n\nexport default function(x) {\n return x = formatDecimalParts(Math.abs(x)), x ? x[1] : NaN;\n}\n", "export default function(grouping, thousands) {\n return function(value, width) {\n var i = value.length,\n t = [],\n j = 0,\n g = grouping[0],\n length = 0;\n\n while (i > 0 && g > 0) {\n if (length + g + 1 > width) g = Math.max(1, width - length);\n t.push(value.substring(i -= g, i + g));\n if ((length += g + 1) > width) break;\n g = grouping[j = (j + 1) % grouping.length];\n }\n\n return t.reverse().join(thousands);\n };\n}\n", "export default function(numerals) {\n return function(value) {\n return value.replace(/[0-9]/g, function(i) {\n return numerals[+i];\n });\n };\n}\n", "// [[fill]align][sign][symbol][0][width][,][.precision][~][type]\nvar re = /^(?:(.)?([<>=^]))?([+\\-( ])?([$#])?(0)?(\\d+)?(,)?(\\.\\d+)?(~)?([a-z%])?$/i;\n\nexport default function formatSpecifier(specifier) {\n if (!(match = re.exec(specifier))) throw new Error(\"invalid format: \" + specifier);\n var match;\n return new FormatSpecifier({\n fill: match[1],\n align: match[2],\n sign: match[3],\n symbol: match[4],\n zero: match[5],\n width: match[6],\n comma: match[7],\n precision: match[8] && match[8].slice(1),\n trim: match[9],\n type: match[10]\n });\n}\n\nformatSpecifier.prototype = FormatSpecifier.prototype; // instanceof\n\nexport function FormatSpecifier(specifier) {\n this.fill = specifier.fill === undefined ? \" \" : specifier.fill + \"\";\n this.align = specifier.align === undefined ? \">\" : specifier.align + \"\";\n this.sign = specifier.sign === undefined ? \"-\" : specifier.sign + \"\";\n this.symbol = specifier.symbol === undefined ? \"\" : specifier.symbol + \"\";\n this.zero = !!specifier.zero;\n this.width = specifier.width === undefined ? undefined : +specifier.width;\n this.comma = !!specifier.comma;\n this.precision = specifier.precision === undefined ? undefined : +specifier.precision;\n this.trim = !!specifier.trim;\n this.type = specifier.type === undefined ? \"\" : specifier.type + \"\";\n}\n\nFormatSpecifier.prototype.toString = function() {\n return this.fill\n + this.align\n + this.sign\n + this.symbol\n + (this.zero ? \"0\" : \"\")\n + (this.width === undefined ? \"\" : Math.max(1, this.width | 0))\n + (this.comma ? \",\" : \"\")\n + (this.precision === undefined ? \"\" : \".\" + Math.max(0, this.precision | 0))\n + (this.trim ? \"~\" : \"\")\n + this.type;\n};\n", "// Trims insignificant zeros, e.g., replaces 1.2000k with 1.2k.\nexport default function(s) {\n out: for (var n = s.length, i = 1, i0 = -1, i1; i < n; ++i) {\n switch (s[i]) {\n case \".\": i0 = i1 = i; break;\n case \"0\": if (i0 === 0) i0 = i; i1 = i; break;\n default: if (!+s[i]) break out; if (i0 > 0) i0 = 0; break;\n }\n }\n return i0 > 0 ? s.slice(0, i0) + s.slice(i1 + 1) : s;\n}\n", "import {formatDecimalParts} from \"./formatDecimal.js\";\n\nexport var prefixExponent;\n\nexport default function(x, p) {\n var d = formatDecimalParts(x, p);\n if (!d) return x + \"\";\n var coefficient = d[0],\n exponent = d[1],\n i = exponent - (prefixExponent = Math.max(-8, Math.min(8, Math.floor(exponent / 3))) * 3) + 1,\n n = coefficient.length;\n return i === n ? coefficient\n : i > n ? coefficient + new Array(i - n + 1).join(\"0\")\n : i > 0 ? coefficient.slice(0, i) + \".\" + coefficient.slice(i)\n : \"0.\" + new Array(1 - i).join(\"0\") + formatDecimalParts(x, Math.max(0, p + i - 1))[0]; // less than 1y!\n}\n", "import {formatDecimalParts} from \"./formatDecimal.js\";\n\nexport default function(x, p) {\n var d = formatDecimalParts(x, p);\n if (!d) return x + \"\";\n var coefficient = d[0],\n exponent = d[1];\n return exponent < 0 ? \"0.\" + new Array(-exponent).join(\"0\") + coefficient\n : coefficient.length > exponent + 1 ? coefficient.slice(0, exponent + 1) + \".\" + coefficient.slice(exponent + 1)\n : coefficient + new Array(exponent - coefficient.length + 2).join(\"0\");\n}\n", "import formatDecimal from \"./formatDecimal.js\";\nimport formatPrefixAuto from \"./formatPrefixAuto.js\";\nimport formatRounded from \"./formatRounded.js\";\n\nexport default {\n \"%\": (x, p) => (x * 100).toFixed(p),\n \"b\": (x) => Math.round(x).toString(2),\n \"c\": (x) => x + \"\",\n \"d\": formatDecimal,\n \"e\": (x, p) => x.toExponential(p),\n \"f\": (x, p) => x.toFixed(p),\n \"g\": (x, p) => x.toPrecision(p),\n \"o\": (x) => Math.round(x).toString(8),\n \"p\": (x, p) => formatRounded(x * 100, p),\n \"r\": formatRounded,\n \"s\": formatPrefixAuto,\n \"X\": (x) => Math.round(x).toString(16).toUpperCase(),\n \"x\": (x) => Math.round(x).toString(16)\n};\n", "export default function(x) {\n return x;\n}\n", "import exponent from \"./exponent.js\";\nimport formatGroup from \"./formatGroup.js\";\nimport formatNumerals from \"./formatNumerals.js\";\nimport formatSpecifier from \"./formatSpecifier.js\";\nimport formatTrim from \"./formatTrim.js\";\nimport formatTypes from \"./formatTypes.js\";\nimport {prefixExponent} from \"./formatPrefixAuto.js\";\nimport identity from \"./identity.js\";\n\nvar map = Array.prototype.map,\n prefixes = [\"y\",\"z\",\"a\",\"f\",\"p\",\"n\",\"\u00B5\",\"m\",\"\",\"k\",\"M\",\"G\",\"T\",\"P\",\"E\",\"Z\",\"Y\"];\n\nexport default function(locale) {\n var group = locale.grouping === undefined || locale.thousands === undefined ? identity : formatGroup(map.call(locale.grouping, Number), locale.thousands + \"\"),\n currencyPrefix = locale.currency === undefined ? \"\" : locale.currency[0] + \"\",\n currencySuffix = locale.currency === undefined ? \"\" : locale.currency[1] + \"\",\n decimal = locale.decimal === undefined ? \".\" : locale.decimal + \"\",\n numerals = locale.numerals === undefined ? identity : formatNumerals(map.call(locale.numerals, String)),\n percent = locale.percent === undefined ? \"%\" : locale.percent + \"\",\n minus = locale.minus === undefined ? \"\u2212\" : locale.minus + \"\",\n nan = locale.nan === undefined ? \"NaN\" : locale.nan + \"\";\n\n function newFormat(specifier) {\n specifier = formatSpecifier(specifier);\n\n var fill = specifier.fill,\n align = specifier.align,\n sign = specifier.sign,\n symbol = specifier.symbol,\n zero = specifier.zero,\n width = specifier.width,\n comma = specifier.comma,\n precision = specifier.precision,\n trim = specifier.trim,\n type = specifier.type;\n\n // The \"n\" type is an alias for \",g\".\n if (type === \"n\") comma = true, type = \"g\";\n\n // The \"\" type, and any invalid type, is an alias for \".12~g\".\n else if (!formatTypes[type]) precision === undefined && (precision = 12), trim = true, type = \"g\";\n\n // If zero fill is specified, padding goes after sign and before digits.\n if (zero || (fill === \"0\" && align === \"=\")) zero = true, fill = \"0\", align = \"=\";\n\n // Compute the prefix and suffix.\n // For SI-prefix, the suffix is lazily computed.\n var prefix = symbol === \"$\" ? currencyPrefix : symbol === \"#\" && /[boxX]/.test(type) ? \"0\" + type.toLowerCase() : \"\",\n suffix = symbol === \"$\" ? currencySuffix : /[%p]/.test(type) ? percent : \"\";\n\n // What format function should we use?\n // Is this an integer type?\n // Can this type generate exponential notation?\n var formatType = formatTypes[type],\n maybeSuffix = /[defgprs%]/.test(type);\n\n // Set the default precision if not specified,\n // or clamp the specified precision to the supported range.\n // For significant precision, it must be in [1, 21].\n // For fixed precision, it must be in [0, 20].\n precision = precision === undefined ? 6\n : /[gprs]/.test(type) ? Math.max(1, Math.min(21, precision))\n : Math.max(0, Math.min(20, precision));\n\n function format(value) {\n var valuePrefix = prefix,\n valueSuffix = suffix,\n i, n, c;\n\n if (type === \"c\") {\n valueSuffix = formatType(value) + valueSuffix;\n value = \"\";\n } else {\n value = +value;\n\n // Determine the sign. -0 is not less than 0, but 1 / -0 is!\n var valueNegative = value < 0 || 1 / value < 0;\n\n // Perform the initial formatting.\n value = isNaN(value) ? nan : formatType(Math.abs(value), precision);\n\n // Trim insignificant zeros.\n if (trim) value = formatTrim(value);\n\n // If a negative value rounds to zero after formatting, and no explicit positive sign is requested, hide the sign.\n if (valueNegative && +value === 0 && sign !== \"+\") valueNegative = false;\n\n // Compute the prefix and suffix.\n valuePrefix = (valueNegative ? (sign === \"(\" ? sign : minus) : sign === \"-\" || sign === \"(\" ? \"\" : sign) + valuePrefix;\n valueSuffix = (type === \"s\" ? prefixes[8 + prefixExponent / 3] : \"\") + valueSuffix + (valueNegative && sign === \"(\" ? \")\" : \"\");\n\n // Break the formatted value into the integer \u201Cvalue\u201D part that can be\n // grouped, and fractional or exponential \u201Csuffix\u201D part that is not.\n if (maybeSuffix) {\n i = -1, n = value.length;\n while (++i < n) {\n if (c = value.charCodeAt(i), 48 > c || c > 57) {\n valueSuffix = (c === 46 ? decimal + value.slice(i + 1) : value.slice(i)) + valueSuffix;\n value = value.slice(0, i);\n break;\n }\n }\n }\n }\n\n // If the fill character is not \"0\", grouping is applied before padding.\n if (comma && !zero) value = group(value, Infinity);\n\n // Compute the padding.\n var length = valuePrefix.length + value.length + valueSuffix.length,\n padding = length < width ? new Array(width - length + 1).join(fill) : \"\";\n\n // If the fill character is \"0\", grouping is applied after padding.\n if (comma && zero) value = group(padding + value, padding.length ? width - valueSuffix.length : Infinity), padding = \"\";\n\n // Reconstruct the final output based on the desired alignment.\n switch (align) {\n case \"<\": value = valuePrefix + value + valueSuffix + padding; break;\n case \"=\": value = valuePrefix + padding + value + valueSuffix; break;\n case \"^\": value = padding.slice(0, length = padding.length >> 1) + valuePrefix + value + valueSuffix + padding.slice(length); break;\n default: value = padding + valuePrefix + value + valueSuffix; break;\n }\n\n return numerals(value);\n }\n\n format.toString = function() {\n return specifier + \"\";\n };\n\n return format;\n }\n\n function formatPrefix(specifier, value) {\n var f = newFormat((specifier = formatSpecifier(specifier), specifier.type = \"f\", specifier)),\n e = Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3,\n k = Math.pow(10, -e),\n prefix = prefixes[8 + e / 3];\n return function(value) {\n return f(k * value) + prefix;\n };\n }\n\n return {\n format: newFormat,\n formatPrefix: formatPrefix\n };\n}\n", "import formatLocale from \"./locale.js\";\n\nvar locale;\nexport var format;\nexport var formatPrefix;\n\ndefaultLocale({\n thousands: \",\",\n grouping: [3],\n currency: [\"$\", \"\"]\n});\n\nexport default function defaultLocale(definition) {\n locale = formatLocale(definition);\n format = locale.format;\n formatPrefix = locale.formatPrefix;\n return locale;\n}\n", "import exponent from \"./exponent.js\";\n\nexport default function(step) {\n return Math.max(0, -exponent(Math.abs(step)));\n}\n", "import exponent from \"./exponent.js\";\n\nexport default function(step, value) {\n return Math.max(0, Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3 - exponent(Math.abs(step)));\n}\n", "import exponent from \"./exponent.js\";\n\nexport default function(step, max) {\n step = Math.abs(step), max = Math.abs(max) - step;\n return Math.max(0, exponent(max) - exponent(step)) + 1;\n}\n", "export {default as formatDefaultLocale, format, formatPrefix} from \"./defaultLocale.js\";\nexport {default as formatLocale} from \"./locale.js\";\nexport {default as formatSpecifier, FormatSpecifier} from \"./formatSpecifier.js\";\nexport {default as precisionFixed} from \"./precisionFixed.js\";\nexport {default as precisionPrefix} from \"./precisionPrefix.js\";\nexport {default as precisionRound} from \"./precisionRound.js\";\n", "export {default as cluster} from \"./cluster.js\";\nexport {default as hierarchy, Node} from \"./hierarchy/index.js\";\nexport {default as pack} from \"./pack/index.js\";\nexport {default as packSiblings} from \"./pack/siblings.js\";\nexport {default as packEnclose} from \"./pack/enclose.js\";\nexport {default as partition} from \"./partition.js\";\nexport {default as stratify} from \"./stratify.js\";\nexport {default as tree} from \"./tree.js\";\nexport {default as treemap} from \"./treemap/index.js\";\nexport {default as treemapBinary} from \"./treemap/binary.js\";\nexport {default as treemapDice} from \"./treemap/dice.js\";\nexport {default as treemapSlice} from \"./treemap/slice.js\";\nexport {default as treemapSliceDice} from \"./treemap/sliceDice.js\";\nexport {default as treemapSquarify} from \"./treemap/squarify.js\";\nexport {default as treemapResquarify} from \"./treemap/resquarify.js\";\n", "export {default as randomUniform} from \"./uniform.js\";\nexport {default as randomInt} from \"./int.js\";\nexport {default as randomNormal} from \"./normal.js\";\nexport {default as randomLogNormal} from \"./logNormal.js\";\nexport {default as randomBates} from \"./bates.js\";\nexport {default as randomIrwinHall} from \"./irwinHall.js\";\nexport {default as randomExponential} from \"./exponential.js\";\nexport {default as randomPareto} from \"./pareto.js\";\nexport {default as randomBernoulli} from \"./bernoulli.js\";\nexport {default as randomGeometric} from \"./geometric.js\";\nexport {default as randomBinomial} from \"./binomial.js\";\nexport {default as randomGamma} from \"./gamma.js\";\nexport {default as randomBeta} from \"./beta.js\";\nexport {default as randomWeibull} from \"./weibull.js\";\nexport {default as randomCauchy} from \"./cauchy.js\";\nexport {default as randomLogistic} from \"./logistic.js\";\nexport {default as randomPoisson} from \"./poisson.js\";\nexport {default as randomLcg} from \"./lcg.js\";\n", "export function initRange(domain, range) {\n switch (arguments.length) {\n case 0: break;\n case 1: this.range(domain); break;\n default: this.range(range).domain(domain); break;\n }\n return this;\n}\n\nexport function initInterpolator(domain, interpolator) {\n switch (arguments.length) {\n case 0: break;\n case 1: {\n if (typeof domain === \"function\") this.interpolator(domain);\n else this.range(domain);\n break;\n }\n default: {\n this.domain(domain);\n if (typeof interpolator === \"function\") this.interpolator(interpolator);\n else this.range(interpolator);\n break;\n }\n }\n return this;\n}\n", "import {InternMap} from \"d3-array\";\nimport {initRange} from \"./init.js\";\n\nexport const implicit = Symbol(\"implicit\");\n\nexport default function ordinal() {\n var index = new InternMap(),\n domain = [],\n range = [],\n unknown = implicit;\n\n function scale(d) {\n let i = index.get(d);\n if (i === undefined) {\n if (unknown !== implicit) return unknown;\n index.set(d, i = domain.push(d) - 1);\n }\n return range[i % range.length];\n }\n\n scale.domain = function(_) {\n if (!arguments.length) return domain.slice();\n domain = [], index = new InternMap();\n for (const value of _) {\n if (index.has(value)) continue;\n index.set(value, domain.push(value) - 1);\n }\n return scale;\n };\n\n scale.range = function(_) {\n return arguments.length ? (range = Array.from(_), scale) : range.slice();\n };\n\n scale.unknown = function(_) {\n return arguments.length ? (unknown = _, scale) : unknown;\n };\n\n scale.copy = function() {\n return ordinal(domain, range).unknown(unknown);\n };\n\n initRange.apply(scale, arguments);\n\n return scale;\n}\n", "import {range as sequence} from \"d3-array\";\nimport {initRange} from \"./init.js\";\nimport ordinal from \"./ordinal.js\";\n\nexport default function band() {\n var scale = ordinal().unknown(undefined),\n domain = scale.domain,\n ordinalRange = scale.range,\n r0 = 0,\n r1 = 1,\n step,\n bandwidth,\n round = false,\n paddingInner = 0,\n paddingOuter = 0,\n align = 0.5;\n\n delete scale.unknown;\n\n function rescale() {\n var n = domain().length,\n reverse = r1 < r0,\n start = reverse ? r1 : r0,\n stop = reverse ? r0 : r1;\n step = (stop - start) / Math.max(1, n - paddingInner + paddingOuter * 2);\n if (round) step = Math.floor(step);\n start += (stop - start - step * (n - paddingInner)) * align;\n bandwidth = step * (1 - paddingInner);\n if (round) start = Math.round(start), bandwidth = Math.round(bandwidth);\n var values = sequence(n).map(function(i) { return start + step * i; });\n return ordinalRange(reverse ? values.reverse() : values);\n }\n\n scale.domain = function(_) {\n return arguments.length ? (domain(_), rescale()) : domain();\n };\n\n scale.range = function(_) {\n return arguments.length ? ([r0, r1] = _, r0 = +r0, r1 = +r1, rescale()) : [r0, r1];\n };\n\n scale.rangeRound = function(_) {\n return [r0, r1] = _, r0 = +r0, r1 = +r1, round = true, rescale();\n };\n\n scale.bandwidth = function() {\n return bandwidth;\n };\n\n scale.step = function() {\n return step;\n };\n\n scale.round = function(_) {\n return arguments.length ? (round = !!_, rescale()) : round;\n };\n\n scale.padding = function(_) {\n return arguments.length ? (paddingInner = Math.min(1, paddingOuter = +_), rescale()) : paddingInner;\n };\n\n scale.paddingInner = function(_) {\n return arguments.length ? (paddingInner = Math.min(1, _), rescale()) : paddingInner;\n };\n\n scale.paddingOuter = function(_) {\n return arguments.length ? (paddingOuter = +_, rescale()) : paddingOuter;\n };\n\n scale.align = function(_) {\n return arguments.length ? (align = Math.max(0, Math.min(1, _)), rescale()) : align;\n };\n\n scale.copy = function() {\n return band(domain(), [r0, r1])\n .round(round)\n .paddingInner(paddingInner)\n .paddingOuter(paddingOuter)\n .align(align);\n };\n\n return initRange.apply(rescale(), arguments);\n}\n\nfunction pointish(scale) {\n var copy = scale.copy;\n\n scale.padding = scale.paddingOuter;\n delete scale.paddingInner;\n delete scale.paddingOuter;\n\n scale.copy = function() {\n return pointish(copy());\n };\n\n return scale;\n}\n\nexport function point() {\n return pointish(band.apply(null, arguments).paddingInner(1));\n}\n", "export default function constants(x) {\n return function() {\n return x;\n };\n}\n", "export default function number(x) {\n return +x;\n}\n", "import {bisect} from \"d3-array\";\nimport {interpolate as interpolateValue, interpolateNumber, interpolateRound} from \"d3-interpolate\";\nimport constant from \"./constant.js\";\nimport number from \"./number.js\";\n\nvar unit = [0, 1];\n\nexport function identity(x) {\n return x;\n}\n\nfunction normalize(a, b) {\n return (b -= (a = +a))\n ? function(x) { return (x - a) / b; }\n : constant(isNaN(b) ? NaN : 0.5);\n}\n\nfunction clamper(a, b) {\n var t;\n if (a > b) t = a, a = b, b = t;\n return function(x) { return Math.max(a, Math.min(b, x)); };\n}\n\n// normalize(a, b)(x) takes a domain value x in [a,b] and returns the corresponding parameter t in [0,1].\n// interpolate(a, b)(t) takes a parameter t in [0,1] and returns the corresponding range value x in [a,b].\nfunction bimap(domain, range, interpolate) {\n var d0 = domain[0], d1 = domain[1], r0 = range[0], r1 = range[1];\n if (d1 < d0) d0 = normalize(d1, d0), r0 = interpolate(r1, r0);\n else d0 = normalize(d0, d1), r0 = interpolate(r0, r1);\n return function(x) { return r0(d0(x)); };\n}\n\nfunction polymap(domain, range, interpolate) {\n var j = Math.min(domain.length, range.length) - 1,\n d = new Array(j),\n r = new Array(j),\n i = -1;\n\n // Reverse descending domains.\n if (domain[j] < domain[0]) {\n domain = domain.slice().reverse();\n range = range.slice().reverse();\n }\n\n while (++i < j) {\n d[i] = normalize(domain[i], domain[i + 1]);\n r[i] = interpolate(range[i], range[i + 1]);\n }\n\n return function(x) {\n var i = bisect(domain, x, 1, j) - 1;\n return r[i](d[i](x));\n };\n}\n\nexport function copy(source, target) {\n return target\n .domain(source.domain())\n .range(source.range())\n .interpolate(source.interpolate())\n .clamp(source.clamp())\n .unknown(source.unknown());\n}\n\nexport function transformer() {\n var domain = unit,\n range = unit,\n interpolate = interpolateValue,\n transform,\n untransform,\n unknown,\n clamp = identity,\n piecewise,\n output,\n input;\n\n function rescale() {\n var n = Math.min(domain.length, range.length);\n if (clamp !== identity) clamp = clamper(domain[0], domain[n - 1]);\n piecewise = n > 2 ? polymap : bimap;\n output = input = null;\n return scale;\n }\n\n function scale(x) {\n return x == null || isNaN(x = +x) ? unknown : (output || (output = piecewise(domain.map(transform), range, interpolate)))(transform(clamp(x)));\n }\n\n scale.invert = function(y) {\n return clamp(untransform((input || (input = piecewise(range, domain.map(transform), interpolateNumber)))(y)));\n };\n\n scale.domain = function(_) {\n return arguments.length ? (domain = Array.from(_, number), rescale()) : domain.slice();\n };\n\n scale.range = function(_) {\n return arguments.length ? (range = Array.from(_), rescale()) : range.slice();\n };\n\n scale.rangeRound = function(_) {\n return range = Array.from(_), interpolate = interpolateRound, rescale();\n };\n\n scale.clamp = function(_) {\n return arguments.length ? (clamp = _ ? true : identity, rescale()) : clamp !== identity;\n };\n\n scale.interpolate = function(_) {\n return arguments.length ? (interpolate = _, rescale()) : interpolate;\n };\n\n scale.unknown = function(_) {\n return arguments.length ? (unknown = _, scale) : unknown;\n };\n\n return function(t, u) {\n transform = t, untransform = u;\n return rescale();\n };\n}\n\nexport default function continuous() {\n return transformer()(identity, identity);\n}\n", "import {tickStep} from \"d3-array\";\nimport {format, formatPrefix, formatSpecifier, precisionFixed, precisionPrefix, precisionRound} from \"d3-format\";\n\nexport default function tickFormat(start, stop, count, specifier) {\n var step = tickStep(start, stop, count),\n precision;\n specifier = formatSpecifier(specifier == null ? \",f\" : specifier);\n switch (specifier.type) {\n case \"s\": {\n var value = Math.max(Math.abs(start), Math.abs(stop));\n if (specifier.precision == null && !isNaN(precision = precisionPrefix(step, value))) specifier.precision = precision;\n return formatPrefix(specifier, value);\n }\n case \"\":\n case \"e\":\n case \"g\":\n case \"p\":\n case \"r\": {\n if (specifier.precision == null && !isNaN(precision = precisionRound(step, Math.max(Math.abs(start), Math.abs(stop))))) specifier.precision = precision - (specifier.type === \"e\");\n break;\n }\n case \"f\":\n case \"%\": {\n if (specifier.precision == null && !isNaN(precision = precisionFixed(step))) specifier.precision = precision - (specifier.type === \"%\") * 2;\n break;\n }\n }\n return format(specifier);\n}\n", "import {ticks, tickIncrement} from \"d3-array\";\nimport continuous, {copy} from \"./continuous.js\";\nimport {initRange} from \"./init.js\";\nimport tickFormat from \"./tickFormat.js\";\n\nexport function linearish(scale) {\n var domain = scale.domain;\n\n scale.ticks = function(count) {\n var d = domain();\n return ticks(d[0], d[d.length - 1], count == null ? 10 : count);\n };\n\n scale.tickFormat = function(count, specifier) {\n var d = domain();\n return tickFormat(d[0], d[d.length - 1], count == null ? 10 : count, specifier);\n };\n\n scale.nice = function(count) {\n if (count == null) count = 10;\n\n var d = domain();\n var i0 = 0;\n var i1 = d.length - 1;\n var start = d[i0];\n var stop = d[i1];\n var prestep;\n var step;\n var maxIter = 10;\n\n if (stop < start) {\n step = start, start = stop, stop = step;\n step = i0, i0 = i1, i1 = step;\n }\n \n while (maxIter-- > 0) {\n step = tickIncrement(start, stop, count);\n if (step === prestep) {\n d[i0] = start\n d[i1] = stop\n return domain(d);\n } else if (step > 0) {\n start = Math.floor(start / step) * step;\n stop = Math.ceil(stop / step) * step;\n } else if (step < 0) {\n start = Math.ceil(start * step) / step;\n stop = Math.floor(stop * step) / step;\n } else {\n break;\n }\n prestep = step;\n }\n\n return scale;\n };\n\n return scale;\n}\n\nexport default function linear() {\n var scale = continuous();\n\n scale.copy = function() {\n return copy(scale, linear());\n };\n\n initRange.apply(scale, arguments);\n\n return linearish(scale);\n}\n", "import {linearish} from \"./linear.js\";\nimport number from \"./number.js\";\n\nexport default function identity(domain) {\n var unknown;\n\n function scale(x) {\n return x == null || isNaN(x = +x) ? unknown : x;\n }\n\n scale.invert = scale;\n\n scale.domain = scale.range = function(_) {\n return arguments.length ? (domain = Array.from(_, number), scale) : domain.slice();\n };\n\n scale.unknown = function(_) {\n return arguments.length ? (unknown = _, scale) : unknown;\n };\n\n scale.copy = function() {\n return identity(domain).unknown(unknown);\n };\n\n domain = arguments.length ? Array.from(domain, number) : [0, 1];\n\n return linearish(scale);\n}\n", "import {ticks} from \"d3-array\";\nimport {format, formatSpecifier} from \"d3-format\";\nimport nice from \"./nice.js\";\nimport {copy, transformer} from \"./continuous.js\";\nimport {initRange} from \"./init.js\";\n\nfunction transformLog(x) {\n return Math.log(x);\n}\n\nfunction transformExp(x) {\n return Math.exp(x);\n}\n\nfunction transformLogn(x) {\n return -Math.log(-x);\n}\n\nfunction transformExpn(x) {\n return -Math.exp(-x);\n}\n\nfunction pow10(x) {\n return isFinite(x) ? +(\"1e\" + x) : x < 0 ? 0 : x;\n}\n\nfunction powp(base) {\n return base === 10 ? pow10\n : base === Math.E ? Math.exp\n : x => Math.pow(base, x);\n}\n\nfunction logp(base) {\n return base === Math.E ? Math.log\n : base === 10 && Math.log10\n || base === 2 && Math.log2\n || (base = Math.log(base), x => Math.log(x) / base);\n}\n\nfunction reflect(f) {\n return (x, k) => -f(-x, k);\n}\n\nexport function loggish(transform) {\n const scale = transform(transformLog, transformExp);\n const domain = scale.domain;\n let base = 10;\n let logs;\n let pows;\n\n function rescale() {\n logs = logp(base), pows = powp(base);\n if (domain()[0] < 0) {\n logs = reflect(logs), pows = reflect(pows);\n transform(transformLogn, transformExpn);\n } else {\n transform(transformLog, transformExp);\n }\n return scale;\n }\n\n scale.base = function(_) {\n return arguments.length ? (base = +_, rescale()) : base;\n };\n\n scale.domain = function(_) {\n return arguments.length ? (domain(_), rescale()) : domain();\n };\n\n scale.ticks = count => {\n const d = domain();\n let u = d[0];\n let v = d[d.length - 1];\n const r = v < u;\n\n if (r) ([u, v] = [v, u]);\n\n let i = logs(u);\n let j = logs(v);\n let k;\n let t;\n const n = count == null ? 10 : +count;\n let z = [];\n\n if (!(base % 1) && j - i < n) {\n i = Math.floor(i), j = Math.ceil(j);\n if (u > 0) for (; i <= j; ++i) {\n for (k = 1; k < base; ++k) {\n t = i < 0 ? k / pows(-i) : k * pows(i);\n if (t < u) continue;\n if (t > v) break;\n z.push(t);\n }\n } else for (; i <= j; ++i) {\n for (k = base - 1; k >= 1; --k) {\n t = i > 0 ? k / pows(-i) : k * pows(i);\n if (t < u) continue;\n if (t > v) break;\n z.push(t);\n }\n }\n if (z.length * 2 < n) z = ticks(u, v, n);\n } else {\n z = ticks(i, j, Math.min(j - i, n)).map(pows);\n }\n return r ? z.reverse() : z;\n };\n\n scale.tickFormat = (count, specifier) => {\n if (count == null) count = 10;\n if (specifier == null) specifier = base === 10 ? \"s\" : \",\";\n if (typeof specifier !== \"function\") {\n if (!(base % 1) && (specifier = formatSpecifier(specifier)).precision == null) specifier.trim = true;\n specifier = format(specifier);\n }\n if (count === Infinity) return specifier;\n const k = Math.max(1, base * count / scale.ticks().length); // TODO fast estimate?\n return d => {\n let i = d / pows(Math.round(logs(d)));\n if (i * base < base - 0.5) i *= base;\n return i <= k ? specifier(d) : \"\";\n };\n };\n\n scale.nice = () => {\n return domain(nice(domain(), {\n floor: x => pows(Math.floor(logs(x))),\n ceil: x => pows(Math.ceil(logs(x)))\n }));\n };\n\n return scale;\n}\n\nexport default function log() {\n const scale = loggish(transformer()).domain([1, 10]);\n scale.copy = () => copy(scale, log()).base(scale.base());\n initRange.apply(scale, arguments);\n return scale;\n}\n", "import {linearish} from \"./linear.js\";\nimport {copy, transformer} from \"./continuous.js\";\nimport {initRange} from \"./init.js\";\n\nfunction transformSymlog(c) {\n return function(x) {\n return Math.sign(x) * Math.log1p(Math.abs(x / c));\n };\n}\n\nfunction transformSymexp(c) {\n return function(x) {\n return Math.sign(x) * Math.expm1(Math.abs(x)) * c;\n };\n}\n\nexport function symlogish(transform) {\n var c = 1, scale = transform(transformSymlog(c), transformSymexp(c));\n\n scale.constant = function(_) {\n return arguments.length ? transform(transformSymlog(c = +_), transformSymexp(c)) : c;\n };\n\n return linearish(scale);\n}\n\nexport default function symlog() {\n var scale = symlogish(transformer());\n\n scale.copy = function() {\n return copy(scale, symlog()).constant(scale.constant());\n };\n\n return initRange.apply(scale, arguments);\n}\n", "import {linearish} from \"./linear.js\";\nimport {copy, identity, transformer} from \"./continuous.js\";\nimport {initRange} from \"./init.js\";\n\nfunction transformPow(exponent) {\n return function(x) {\n return x < 0 ? -Math.pow(-x, exponent) : Math.pow(x, exponent);\n };\n}\n\nfunction transformSqrt(x) {\n return x < 0 ? -Math.sqrt(-x) : Math.sqrt(x);\n}\n\nfunction transformSquare(x) {\n return x < 0 ? -x * x : x * x;\n}\n\nexport function powish(transform) {\n var scale = transform(identity, identity),\n exponent = 1;\n\n function rescale() {\n return exponent === 1 ? transform(identity, identity)\n : exponent === 0.5 ? transform(transformSqrt, transformSquare)\n : transform(transformPow(exponent), transformPow(1 / exponent));\n }\n\n scale.exponent = function(_) {\n return arguments.length ? (exponent = +_, rescale()) : exponent;\n };\n\n return linearish(scale);\n}\n\nexport default function pow() {\n var scale = powish(transformer());\n\n scale.copy = function() {\n return copy(scale, pow()).exponent(scale.exponent());\n };\n\n initRange.apply(scale, arguments);\n\n return scale;\n}\n\nexport function sqrt() {\n return pow.apply(null, arguments).exponent(0.5);\n}\n", "import continuous from \"./continuous.js\";\nimport {initRange} from \"./init.js\";\nimport {linearish} from \"./linear.js\";\nimport number from \"./number.js\";\n\nfunction square(x) {\n return Math.sign(x) * x * x;\n}\n\nfunction unsquare(x) {\n return Math.sign(x) * Math.sqrt(Math.abs(x));\n}\n\nexport default function radial() {\n var squared = continuous(),\n range = [0, 1],\n round = false,\n unknown;\n\n function scale(x) {\n var y = unsquare(squared(x));\n return isNaN(y) ? unknown : round ? Math.round(y) : y;\n }\n\n scale.invert = function(y) {\n return squared.invert(square(y));\n };\n\n scale.domain = function(_) {\n return arguments.length ? (squared.domain(_), scale) : squared.domain();\n };\n\n scale.range = function(_) {\n return arguments.length ? (squared.range((range = Array.from(_, number)).map(square)), scale) : range.slice();\n };\n\n scale.rangeRound = function(_) {\n return scale.range(_).round(true);\n };\n\n scale.round = function(_) {\n return arguments.length ? (round = !!_, scale) : round;\n };\n\n scale.clamp = function(_) {\n return arguments.length ? (squared.clamp(_), scale) : squared.clamp();\n };\n\n scale.unknown = function(_) {\n return arguments.length ? (unknown = _, scale) : unknown;\n };\n\n scale.copy = function() {\n return radial(squared.domain(), range)\n .round(round)\n .clamp(squared.clamp())\n .unknown(unknown);\n };\n\n initRange.apply(scale, arguments);\n\n return linearish(scale);\n}\n", "import {ascending, bisect, quantileSorted as threshold} from \"d3-array\";\nimport {initRange} from \"./init.js\";\n\nexport default function quantile() {\n var domain = [],\n range = [],\n thresholds = [],\n unknown;\n\n function rescale() {\n var i = 0, n = Math.max(1, range.length);\n thresholds = new Array(n - 1);\n while (++i < n) thresholds[i - 1] = threshold(domain, i / n);\n return scale;\n }\n\n function scale(x) {\n return x == null || isNaN(x = +x) ? unknown : range[bisect(thresholds, x)];\n }\n\n scale.invertExtent = function(y) {\n var i = range.indexOf(y);\n return i < 0 ? [NaN, NaN] : [\n i > 0 ? thresholds[i - 1] : domain[0],\n i < thresholds.length ? thresholds[i] : domain[domain.length - 1]\n ];\n };\n\n scale.domain = function(_) {\n if (!arguments.length) return domain.slice();\n domain = [];\n for (let d of _) if (d != null && !isNaN(d = +d)) domain.push(d);\n domain.sort(ascending);\n return rescale();\n };\n\n scale.range = function(_) {\n return arguments.length ? (range = Array.from(_), rescale()) : range.slice();\n };\n\n scale.unknown = function(_) {\n return arguments.length ? (unknown = _, scale) : unknown;\n };\n\n scale.quantiles = function() {\n return thresholds.slice();\n };\n\n scale.copy = function() {\n return quantile()\n .domain(domain)\n .range(range)\n .unknown(unknown);\n };\n\n return initRange.apply(scale, arguments);\n}\n", "import {bisect} from \"d3-array\";\nimport {linearish} from \"./linear.js\";\nimport {initRange} from \"./init.js\";\n\nexport default function quantize() {\n var x0 = 0,\n x1 = 1,\n n = 1,\n domain = [0.5],\n range = [0, 1],\n unknown;\n\n function scale(x) {\n return x != null && x <= x ? range[bisect(domain, x, 0, n)] : unknown;\n }\n\n function rescale() {\n var i = -1;\n domain = new Array(n);\n while (++i < n) domain[i] = ((i + 1) * x1 - (i - n) * x0) / (n + 1);\n return scale;\n }\n\n scale.domain = function(_) {\n return arguments.length ? ([x0, x1] = _, x0 = +x0, x1 = +x1, rescale()) : [x0, x1];\n };\n\n scale.range = function(_) {\n return arguments.length ? (n = (range = Array.from(_)).length - 1, rescale()) : range.slice();\n };\n\n scale.invertExtent = function(y) {\n var i = range.indexOf(y);\n return i < 0 ? [NaN, NaN]\n : i < 1 ? [x0, domain[0]]\n : i >= n ? [domain[n - 1], x1]\n : [domain[i - 1], domain[i]];\n };\n\n scale.unknown = function(_) {\n return arguments.length ? (unknown = _, scale) : scale;\n };\n\n scale.thresholds = function() {\n return domain.slice();\n };\n\n scale.copy = function() {\n return quantize()\n .domain([x0, x1])\n .range(range)\n .unknown(unknown);\n };\n\n return initRange.apply(linearish(scale), arguments);\n}\n", "import {bisect} from \"d3-array\";\nimport {initRange} from \"./init.js\";\n\nexport default function threshold() {\n var domain = [0.5],\n range = [0, 1],\n unknown,\n n = 1;\n\n function scale(x) {\n return x != null && x <= x ? range[bisect(domain, x, 0, n)] : unknown;\n }\n\n scale.domain = function(_) {\n return arguments.length ? (domain = Array.from(_), n = Math.min(domain.length, range.length - 1), scale) : domain.slice();\n };\n\n scale.range = function(_) {\n return arguments.length ? (range = Array.from(_), n = Math.min(domain.length, range.length - 1), scale) : range.slice();\n };\n\n scale.invertExtent = function(y) {\n var i = range.indexOf(y);\n return [domain[i - 1], domain[i]];\n };\n\n scale.unknown = function(_) {\n return arguments.length ? (unknown = _, scale) : unknown;\n };\n\n scale.copy = function() {\n return threshold()\n .domain(domain)\n .range(range)\n .unknown(unknown);\n };\n\n return initRange.apply(scale, arguments);\n}\n", "const t0 = new Date, t1 = new Date;\n\nexport function timeInterval(floori, offseti, count, field) {\n\n function interval(date) {\n return floori(date = arguments.length === 0 ? new Date : new Date(+date)), date;\n }\n\n interval.floor = (date) => {\n return floori(date = new Date(+date)), date;\n };\n\n interval.ceil = (date) => {\n return floori(date = new Date(date - 1)), offseti(date, 1), floori(date), date;\n };\n\n interval.round = (date) => {\n const d0 = interval(date), d1 = interval.ceil(date);\n return date - d0 < d1 - date ? d0 : d1;\n };\n\n interval.offset = (date, step) => {\n return offseti(date = new Date(+date), step == null ? 1 : Math.floor(step)), date;\n };\n\n interval.range = (start, stop, step) => {\n const range = [];\n start = interval.ceil(start);\n step = step == null ? 1 : Math.floor(step);\n if (!(start < stop) || !(step > 0)) return range; // also handles Invalid Date\n let previous;\n do range.push(previous = new Date(+start)), offseti(start, step), floori(start);\n while (previous < start && start < stop);\n return range;\n };\n\n interval.filter = (test) => {\n return timeInterval((date) => {\n if (date >= date) while (floori(date), !test(date)) date.setTime(date - 1);\n }, (date, step) => {\n if (date >= date) {\n if (step < 0) while (++step <= 0) {\n while (offseti(date, -1), !test(date)) {} // eslint-disable-line no-empty\n } else while (--step >= 0) {\n while (offseti(date, +1), !test(date)) {} // eslint-disable-line no-empty\n }\n }\n });\n };\n\n if (count) {\n interval.count = (start, end) => {\n t0.setTime(+start), t1.setTime(+end);\n floori(t0), floori(t1);\n return Math.floor(count(t0, t1));\n };\n\n interval.every = (step) => {\n step = Math.floor(step);\n return !isFinite(step) || !(step > 0) ? null\n : !(step > 1) ? interval\n : interval.filter(field\n ? (d) => field(d) % step === 0\n : (d) => interval.count(0, d) % step === 0);\n };\n }\n\n return interval;\n}\n", "import {timeInterval} from \"./interval.js\";\n\nexport const millisecond = timeInterval(() => {\n // noop\n}, (date, step) => {\n date.setTime(+date + step);\n}, (start, end) => {\n return end - start;\n});\n\n// An optimized implementation for this simple case.\nmillisecond.every = (k) => {\n k = Math.floor(k);\n if (!isFinite(k) || !(k > 0)) return null;\n if (!(k > 1)) return millisecond;\n return timeInterval((date) => {\n date.setTime(Math.floor(date / k) * k);\n }, (date, step) => {\n date.setTime(+date + step * k);\n }, (start, end) => {\n return (end - start) / k;\n });\n};\n\nexport const milliseconds = millisecond.range;\n", "export const durationSecond = 1000;\nexport const durationMinute = durationSecond * 60;\nexport const durationHour = durationMinute * 60;\nexport const durationDay = durationHour * 24;\nexport const durationWeek = durationDay * 7;\nexport const durationMonth = durationDay * 30;\nexport const durationYear = durationDay * 365;\n", "import {timeInterval} from \"./interval.js\";\nimport {durationSecond} from \"./duration.js\";\n\nexport const second = timeInterval((date) => {\n date.setTime(date - date.getMilliseconds());\n}, (date, step) => {\n date.setTime(+date + step * durationSecond);\n}, (start, end) => {\n return (end - start) / durationSecond;\n}, (date) => {\n return date.getUTCSeconds();\n});\n\nexport const seconds = second.range;\n", "import {timeInterval} from \"./interval.js\";\nimport {durationMinute, durationSecond} from \"./duration.js\";\n\nexport const timeMinute = timeInterval((date) => {\n date.setTime(date - date.getMilliseconds() - date.getSeconds() * durationSecond);\n}, (date, step) => {\n date.setTime(+date + step * durationMinute);\n}, (start, end) => {\n return (end - start) / durationMinute;\n}, (date) => {\n return date.getMinutes();\n});\n\nexport const timeMinutes = timeMinute.range;\n\nexport const utcMinute = timeInterval((date) => {\n date.setUTCSeconds(0, 0);\n}, (date, step) => {\n date.setTime(+date + step * durationMinute);\n}, (start, end) => {\n return (end - start) / durationMinute;\n}, (date) => {\n return date.getUTCMinutes();\n});\n\nexport const utcMinutes = utcMinute.range;\n", "import {timeInterval} from \"./interval.js\";\nimport {durationHour, durationMinute, durationSecond} from \"./duration.js\";\n\nexport const timeHour = timeInterval((date) => {\n date.setTime(date - date.getMilliseconds() - date.getSeconds() * durationSecond - date.getMinutes() * durationMinute);\n}, (date, step) => {\n date.setTime(+date + step * durationHour);\n}, (start, end) => {\n return (end - start) / durationHour;\n}, (date) => {\n return date.getHours();\n});\n\nexport const timeHours = timeHour.range;\n\nexport const utcHour = timeInterval((date) => {\n date.setUTCMinutes(0, 0, 0);\n}, (date, step) => {\n date.setTime(+date + step * durationHour);\n}, (start, end) => {\n return (end - start) / durationHour;\n}, (date) => {\n return date.getUTCHours();\n});\n\nexport const utcHours = utcHour.range;\n", "import {timeInterval} from \"./interval.js\";\nimport {durationDay, durationMinute} from \"./duration.js\";\n\nexport const timeDay = timeInterval(\n date => date.setHours(0, 0, 0, 0),\n (date, step) => date.setDate(date.getDate() + step),\n (start, end) => (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute) / durationDay,\n date => date.getDate() - 1\n);\n\nexport const timeDays = timeDay.range;\n\nexport const utcDay = timeInterval((date) => {\n date.setUTCHours(0, 0, 0, 0);\n}, (date, step) => {\n date.setUTCDate(date.getUTCDate() + step);\n}, (start, end) => {\n return (end - start) / durationDay;\n}, (date) => {\n return date.getUTCDate() - 1;\n});\n\nexport const utcDays = utcDay.range;\n\nexport const unixDay = timeInterval((date) => {\n date.setUTCHours(0, 0, 0, 0);\n}, (date, step) => {\n date.setUTCDate(date.getUTCDate() + step);\n}, (start, end) => {\n return (end - start) / durationDay;\n}, (date) => {\n return Math.floor(date / durationDay);\n});\n\nexport const unixDays = unixDay.range;\n", "import {timeInterval} from \"./interval.js\";\nimport {durationMinute, durationWeek} from \"./duration.js\";\n\nfunction timeWeekday(i) {\n return timeInterval((date) => {\n date.setDate(date.getDate() - (date.getDay() + 7 - i) % 7);\n date.setHours(0, 0, 0, 0);\n }, (date, step) => {\n date.setDate(date.getDate() + step * 7);\n }, (start, end) => {\n return (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute) / durationWeek;\n });\n}\n\nexport const timeSunday = timeWeekday(0);\nexport const timeMonday = timeWeekday(1);\nexport const timeTuesday = timeWeekday(2);\nexport const timeWednesday = timeWeekday(3);\nexport const timeThursday = timeWeekday(4);\nexport const timeFriday = timeWeekday(5);\nexport const timeSaturday = timeWeekday(6);\n\nexport const timeSundays = timeSunday.range;\nexport const timeMondays = timeMonday.range;\nexport const timeTuesdays = timeTuesday.range;\nexport const timeWednesdays = timeWednesday.range;\nexport const timeThursdays = timeThursday.range;\nexport const timeFridays = timeFriday.range;\nexport const timeSaturdays = timeSaturday.range;\n\nfunction utcWeekday(i) {\n return timeInterval((date) => {\n date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 7 - i) % 7);\n date.setUTCHours(0, 0, 0, 0);\n }, (date, step) => {\n date.setUTCDate(date.getUTCDate() + step * 7);\n }, (start, end) => {\n return (end - start) / durationWeek;\n });\n}\n\nexport const utcSunday = utcWeekday(0);\nexport const utcMonday = utcWeekday(1);\nexport const utcTuesday = utcWeekday(2);\nexport const utcWednesday = utcWeekday(3);\nexport const utcThursday = utcWeekday(4);\nexport const utcFriday = utcWeekday(5);\nexport const utcSaturday = utcWeekday(6);\n\nexport const utcSundays = utcSunday.range;\nexport const utcMondays = utcMonday.range;\nexport const utcTuesdays = utcTuesday.range;\nexport const utcWednesdays = utcWednesday.range;\nexport const utcThursdays = utcThursday.range;\nexport const utcFridays = utcFriday.range;\nexport const utcSaturdays = utcSaturday.range;\n", "import {timeInterval} from \"./interval.js\";\n\nexport const timeMonth = timeInterval((date) => {\n date.setDate(1);\n date.setHours(0, 0, 0, 0);\n}, (date, step) => {\n date.setMonth(date.getMonth() + step);\n}, (start, end) => {\n return end.getMonth() - start.getMonth() + (end.getFullYear() - start.getFullYear()) * 12;\n}, (date) => {\n return date.getMonth();\n});\n\nexport const timeMonths = timeMonth.range;\n\nexport const utcMonth = timeInterval((date) => {\n date.setUTCDate(1);\n date.setUTCHours(0, 0, 0, 0);\n}, (date, step) => {\n date.setUTCMonth(date.getUTCMonth() + step);\n}, (start, end) => {\n return end.getUTCMonth() - start.getUTCMonth() + (end.getUTCFullYear() - start.getUTCFullYear()) * 12;\n}, (date) => {\n return date.getUTCMonth();\n});\n\nexport const utcMonths = utcMonth.range;\n", "import {timeInterval} from \"./interval.js\";\n\nexport const timeYear = timeInterval((date) => {\n date.setMonth(0, 1);\n date.setHours(0, 0, 0, 0);\n}, (date, step) => {\n date.setFullYear(date.getFullYear() + step);\n}, (start, end) => {\n return end.getFullYear() - start.getFullYear();\n}, (date) => {\n return date.getFullYear();\n});\n\n// An optimized implementation for this simple case.\ntimeYear.every = (k) => {\n return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : timeInterval((date) => {\n date.setFullYear(Math.floor(date.getFullYear() / k) * k);\n date.setMonth(0, 1);\n date.setHours(0, 0, 0, 0);\n }, (date, step) => {\n date.setFullYear(date.getFullYear() + step * k);\n });\n};\n\nexport const timeYears = timeYear.range;\n\nexport const utcYear = timeInterval((date) => {\n date.setUTCMonth(0, 1);\n date.setUTCHours(0, 0, 0, 0);\n}, (date, step) => {\n date.setUTCFullYear(date.getUTCFullYear() + step);\n}, (start, end) => {\n return end.getUTCFullYear() - start.getUTCFullYear();\n}, (date) => {\n return date.getUTCFullYear();\n});\n\n// An optimized implementation for this simple case.\nutcYear.every = (k) => {\n return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : timeInterval((date) => {\n date.setUTCFullYear(Math.floor(date.getUTCFullYear() / k) * k);\n date.setUTCMonth(0, 1);\n date.setUTCHours(0, 0, 0, 0);\n }, (date, step) => {\n date.setUTCFullYear(date.getUTCFullYear() + step * k);\n });\n};\n\nexport const utcYears = utcYear.range;\n", "import {bisector, tickStep} from \"d3-array\";\nimport {durationDay, durationHour, durationMinute, durationMonth, durationSecond, durationWeek, durationYear} from \"./duration.js\";\nimport {millisecond} from \"./millisecond.js\";\nimport {second} from \"./second.js\";\nimport {timeMinute, utcMinute} from \"./minute.js\";\nimport {timeHour, utcHour} from \"./hour.js\";\nimport {timeDay, unixDay} from \"./day.js\";\nimport {timeSunday, utcSunday} from \"./week.js\";\nimport {timeMonth, utcMonth} from \"./month.js\";\nimport {timeYear, utcYear} from \"./year.js\";\n\nfunction ticker(year, month, week, day, hour, minute) {\n\n const tickIntervals = [\n [second, 1, durationSecond],\n [second, 5, 5 * durationSecond],\n [second, 15, 15 * durationSecond],\n [second, 30, 30 * durationSecond],\n [minute, 1, durationMinute],\n [minute, 5, 5 * durationMinute],\n [minute, 15, 15 * durationMinute],\n [minute, 30, 30 * durationMinute],\n [ hour, 1, durationHour ],\n [ hour, 3, 3 * durationHour ],\n [ hour, 6, 6 * durationHour ],\n [ hour, 12, 12 * durationHour ],\n [ day, 1, durationDay ],\n [ day, 2, 2 * durationDay ],\n [ week, 1, durationWeek ],\n [ month, 1, durationMonth ],\n [ month, 3, 3 * durationMonth ],\n [ year, 1, durationYear ]\n ];\n\n function ticks(start, stop, count) {\n const reverse = stop < start;\n if (reverse) [start, stop] = [stop, start];\n const interval = count && typeof count.range === \"function\" ? count : tickInterval(start, stop, count);\n const ticks = interval ? interval.range(start, +stop + 1) : []; // inclusive stop\n return reverse ? ticks.reverse() : ticks;\n }\n\n function tickInterval(start, stop, count) {\n const target = Math.abs(stop - start) / count;\n const i = bisector(([,, step]) => step).right(tickIntervals, target);\n if (i === tickIntervals.length) return year.every(tickStep(start / durationYear, stop / durationYear, count));\n if (i === 0) return millisecond.every(Math.max(tickStep(start, stop, count), 1));\n const [t, step] = tickIntervals[target / tickIntervals[i - 1][2] < tickIntervals[i][2] / target ? i - 1 : i];\n return t.every(step);\n }\n\n return [ticks, tickInterval];\n}\n\nconst [utcTicks, utcTickInterval] = ticker(utcYear, utcMonth, utcSunday, unixDay, utcHour, utcMinute);\nconst [timeTicks, timeTickInterval] = ticker(timeYear, timeMonth, timeSunday, timeDay, timeHour, timeMinute);\n\nexport {utcTicks, utcTickInterval, timeTicks, timeTickInterval};\n", "export {\n timeInterval\n} from \"./interval.js\";\n\nexport {\n millisecond as utcMillisecond,\n milliseconds as utcMilliseconds,\n millisecond as timeMillisecond,\n milliseconds as timeMilliseconds\n} from \"./millisecond.js\";\n\nexport {\n second as utcSecond,\n seconds as utcSeconds,\n second as timeSecond,\n seconds as timeSeconds\n} from \"./second.js\";\n\nexport {\n timeMinute,\n timeMinutes,\n utcMinute,\n utcMinutes\n} from \"./minute.js\";\n\nexport {\n timeHour,\n timeHours,\n utcHour,\n utcHours\n} from \"./hour.js\";\n\nexport {\n timeDay,\n timeDays,\n utcDay,\n utcDays,\n unixDay,\n unixDays\n} from \"./day.js\";\n\nexport {\n timeSunday as timeWeek,\n timeSundays as timeWeeks,\n timeSunday,\n timeSundays,\n timeMonday,\n timeMondays,\n timeTuesday,\n timeTuesdays,\n timeWednesday,\n timeWednesdays,\n timeThursday,\n timeThursdays,\n timeFriday,\n timeFridays,\n timeSaturday,\n timeSaturdays,\n utcSunday as utcWeek,\n utcSundays as utcWeeks,\n utcSunday,\n utcSundays,\n utcMonday,\n utcMondays,\n utcTuesday,\n utcTuesdays,\n utcWednesday,\n utcWednesdays,\n utcThursday,\n utcThursdays,\n utcFriday,\n utcFridays,\n utcSaturday,\n utcSaturdays\n} from \"./week.js\";\n\nexport {\n timeMonth,\n timeMonths,\n utcMonth,\n utcMonths\n} from \"./month.js\";\n\nexport {\n timeYear,\n timeYears,\n utcYear,\n utcYears\n} from \"./year.js\";\n\nexport {\n utcTicks,\n utcTickInterval,\n timeTicks,\n timeTickInterval\n} from \"./ticks.js\";\n", "import {\n timeDay,\n timeSunday,\n timeMonday,\n timeThursday,\n timeYear,\n utcDay,\n utcSunday,\n utcMonday,\n utcThursday,\n utcYear\n} from \"d3-time\";\n\nfunction localDate(d) {\n if (0 <= d.y && d.y < 100) {\n var date = new Date(-1, d.m, d.d, d.H, d.M, d.S, d.L);\n date.setFullYear(d.y);\n return date;\n }\n return new Date(d.y, d.m, d.d, d.H, d.M, d.S, d.L);\n}\n\nfunction utcDate(d) {\n if (0 <= d.y && d.y < 100) {\n var date = new Date(Date.UTC(-1, d.m, d.d, d.H, d.M, d.S, d.L));\n date.setUTCFullYear(d.y);\n return date;\n }\n return new Date(Date.UTC(d.y, d.m, d.d, d.H, d.M, d.S, d.L));\n}\n\nfunction newDate(y, m, d) {\n return {y: y, m: m, d: d, H: 0, M: 0, S: 0, L: 0};\n}\n\nexport default function formatLocale(locale) {\n var locale_dateTime = locale.dateTime,\n locale_date = locale.date,\n locale_time = locale.time,\n locale_periods = locale.periods,\n locale_weekdays = locale.days,\n locale_shortWeekdays = locale.shortDays,\n locale_months = locale.months,\n locale_shortMonths = locale.shortMonths;\n\n var periodRe = formatRe(locale_periods),\n periodLookup = formatLookup(locale_periods),\n weekdayRe = formatRe(locale_weekdays),\n weekdayLookup = formatLookup(locale_weekdays),\n shortWeekdayRe = formatRe(locale_shortWeekdays),\n shortWeekdayLookup = formatLookup(locale_shortWeekdays),\n monthRe = formatRe(locale_months),\n monthLookup = formatLookup(locale_months),\n shortMonthRe = formatRe(locale_shortMonths),\n shortMonthLookup = formatLookup(locale_shortMonths);\n\n var formats = {\n \"a\": formatShortWeekday,\n \"A\": formatWeekday,\n \"b\": formatShortMonth,\n \"B\": formatMonth,\n \"c\": null,\n \"d\": formatDayOfMonth,\n \"e\": formatDayOfMonth,\n \"f\": formatMicroseconds,\n \"g\": formatYearISO,\n \"G\": formatFullYearISO,\n \"H\": formatHour24,\n \"I\": formatHour12,\n \"j\": formatDayOfYear,\n \"L\": formatMilliseconds,\n \"m\": formatMonthNumber,\n \"M\": formatMinutes,\n \"p\": formatPeriod,\n \"q\": formatQuarter,\n \"Q\": formatUnixTimestamp,\n \"s\": formatUnixTimestampSeconds,\n \"S\": formatSeconds,\n \"u\": formatWeekdayNumberMonday,\n \"U\": formatWeekNumberSunday,\n \"V\": formatWeekNumberISO,\n \"w\": formatWeekdayNumberSunday,\n \"W\": formatWeekNumberMonday,\n \"x\": null,\n \"X\": null,\n \"y\": formatYear,\n \"Y\": formatFullYear,\n \"Z\": formatZone,\n \"%\": formatLiteralPercent\n };\n\n var utcFormats = {\n \"a\": formatUTCShortWeekday,\n \"A\": formatUTCWeekday,\n \"b\": formatUTCShortMonth,\n \"B\": formatUTCMonth,\n \"c\": null,\n \"d\": formatUTCDayOfMonth,\n \"e\": formatUTCDayOfMonth,\n \"f\": formatUTCMicroseconds,\n \"g\": formatUTCYearISO,\n \"G\": formatUTCFullYearISO,\n \"H\": formatUTCHour24,\n \"I\": formatUTCHour12,\n \"j\": formatUTCDayOfYear,\n \"L\": formatUTCMilliseconds,\n \"m\": formatUTCMonthNumber,\n \"M\": formatUTCMinutes,\n \"p\": formatUTCPeriod,\n \"q\": formatUTCQuarter,\n \"Q\": formatUnixTimestamp,\n \"s\": formatUnixTimestampSeconds,\n \"S\": formatUTCSeconds,\n \"u\": formatUTCWeekdayNumberMonday,\n \"U\": formatUTCWeekNumberSunday,\n \"V\": formatUTCWeekNumberISO,\n \"w\": formatUTCWeekdayNumberSunday,\n \"W\": formatUTCWeekNumberMonday,\n \"x\": null,\n \"X\": null,\n \"y\": formatUTCYear,\n \"Y\": formatUTCFullYear,\n \"Z\": formatUTCZone,\n \"%\": formatLiteralPercent\n };\n\n var parses = {\n \"a\": parseShortWeekday,\n \"A\": parseWeekday,\n \"b\": parseShortMonth,\n \"B\": parseMonth,\n \"c\": parseLocaleDateTime,\n \"d\": parseDayOfMonth,\n \"e\": parseDayOfMonth,\n \"f\": parseMicroseconds,\n \"g\": parseYear,\n \"G\": parseFullYear,\n \"H\": parseHour24,\n \"I\": parseHour24,\n \"j\": parseDayOfYear,\n \"L\": parseMilliseconds,\n \"m\": parseMonthNumber,\n \"M\": parseMinutes,\n \"p\": parsePeriod,\n \"q\": parseQuarter,\n \"Q\": parseUnixTimestamp,\n \"s\": parseUnixTimestampSeconds,\n \"S\": parseSeconds,\n \"u\": parseWeekdayNumberMonday,\n \"U\": parseWeekNumberSunday,\n \"V\": parseWeekNumberISO,\n \"w\": parseWeekdayNumberSunday,\n \"W\": parseWeekNumberMonday,\n \"x\": parseLocaleDate,\n \"X\": parseLocaleTime,\n \"y\": parseYear,\n \"Y\": parseFullYear,\n \"Z\": parseZone,\n \"%\": parseLiteralPercent\n };\n\n // These recursive directive definitions must be deferred.\n formats.x = newFormat(locale_date, formats);\n formats.X = newFormat(locale_time, formats);\n formats.c = newFormat(locale_dateTime, formats);\n utcFormats.x = newFormat(locale_date, utcFormats);\n utcFormats.X = newFormat(locale_time, utcFormats);\n utcFormats.c = newFormat(locale_dateTime, utcFormats);\n\n function newFormat(specifier, formats) {\n return function(date) {\n var string = [],\n i = -1,\n j = 0,\n n = specifier.length,\n c,\n pad,\n format;\n\n if (!(date instanceof Date)) date = new Date(+date);\n\n while (++i < n) {\n if (specifier.charCodeAt(i) === 37) {\n string.push(specifier.slice(j, i));\n if ((pad = pads[c = specifier.charAt(++i)]) != null) c = specifier.charAt(++i);\n else pad = c === \"e\" ? \" \" : \"0\";\n if (format = formats[c]) c = format(date, pad);\n string.push(c);\n j = i + 1;\n }\n }\n\n string.push(specifier.slice(j, i));\n return string.join(\"\");\n };\n }\n\n function newParse(specifier, Z) {\n return function(string) {\n var d = newDate(1900, undefined, 1),\n i = parseSpecifier(d, specifier, string += \"\", 0),\n week, day;\n if (i != string.length) return null;\n\n // If a UNIX timestamp is specified, return it.\n if (\"Q\" in d) return new Date(d.Q);\n if (\"s\" in d) return new Date(d.s * 1000 + (\"L\" in d ? d.L : 0));\n\n // If this is utcParse, never use the local timezone.\n if (Z && !(\"Z\" in d)) d.Z = 0;\n\n // The am-pm flag is 0 for AM, and 1 for PM.\n if (\"p\" in d) d.H = d.H % 12 + d.p * 12;\n\n // If the month was not specified, inherit from the quarter.\n if (d.m === undefined) d.m = \"q\" in d ? d.q : 0;\n\n // Convert day-of-week and week-of-year to day-of-year.\n if (\"V\" in d) {\n if (d.V < 1 || d.V > 53) return null;\n if (!(\"w\" in d)) d.w = 1;\n if (\"Z\" in d) {\n week = utcDate(newDate(d.y, 0, 1)), day = week.getUTCDay();\n week = day > 4 || day === 0 ? utcMonday.ceil(week) : utcMonday(week);\n week = utcDay.offset(week, (d.V - 1) * 7);\n d.y = week.getUTCFullYear();\n d.m = week.getUTCMonth();\n d.d = week.getUTCDate() + (d.w + 6) % 7;\n } else {\n week = localDate(newDate(d.y, 0, 1)), day = week.getDay();\n week = day > 4 || day === 0 ? timeMonday.ceil(week) : timeMonday(week);\n week = timeDay.offset(week, (d.V - 1) * 7);\n d.y = week.getFullYear();\n d.m = week.getMonth();\n d.d = week.getDate() + (d.w + 6) % 7;\n }\n } else if (\"W\" in d || \"U\" in d) {\n if (!(\"w\" in d)) d.w = \"u\" in d ? d.u % 7 : \"W\" in d ? 1 : 0;\n day = \"Z\" in d ? utcDate(newDate(d.y, 0, 1)).getUTCDay() : localDate(newDate(d.y, 0, 1)).getDay();\n d.m = 0;\n d.d = \"W\" in d ? (d.w + 6) % 7 + d.W * 7 - (day + 5) % 7 : d.w + d.U * 7 - (day + 6) % 7;\n }\n\n // If a time zone is specified, all fields are interpreted as UTC and then\n // offset according to the specified time zone.\n if (\"Z\" in d) {\n d.H += d.Z / 100 | 0;\n d.M += d.Z % 100;\n return utcDate(d);\n }\n\n // Otherwise, all fields are in local time.\n return localDate(d);\n };\n }\n\n function parseSpecifier(d, specifier, string, j) {\n var i = 0,\n n = specifier.length,\n m = string.length,\n c,\n parse;\n\n while (i < n) {\n if (j >= m) return -1;\n c = specifier.charCodeAt(i++);\n if (c === 37) {\n c = specifier.charAt(i++);\n parse = parses[c in pads ? specifier.charAt(i++) : c];\n if (!parse || ((j = parse(d, string, j)) < 0)) return -1;\n } else if (c != string.charCodeAt(j++)) {\n return -1;\n }\n }\n\n return j;\n }\n\n function parsePeriod(d, string, i) {\n var n = periodRe.exec(string.slice(i));\n return n ? (d.p = periodLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;\n }\n\n function parseShortWeekday(d, string, i) {\n var n = shortWeekdayRe.exec(string.slice(i));\n return n ? (d.w = shortWeekdayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;\n }\n\n function parseWeekday(d, string, i) {\n var n = weekdayRe.exec(string.slice(i));\n return n ? (d.w = weekdayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;\n }\n\n function parseShortMonth(d, string, i) {\n var n = shortMonthRe.exec(string.slice(i));\n return n ? (d.m = shortMonthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;\n }\n\n function parseMonth(d, string, i) {\n var n = monthRe.exec(string.slice(i));\n return n ? (d.m = monthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1;\n }\n\n function parseLocaleDateTime(d, string, i) {\n return parseSpecifier(d, locale_dateTime, string, i);\n }\n\n function parseLocaleDate(d, string, i) {\n return parseSpecifier(d, locale_date, string, i);\n }\n\n function parseLocaleTime(d, string, i) {\n return parseSpecifier(d, locale_time, string, i);\n }\n\n function formatShortWeekday(d) {\n return locale_shortWeekdays[d.getDay()];\n }\n\n function formatWeekday(d) {\n return locale_weekdays[d.getDay()];\n }\n\n function formatShortMonth(d) {\n return locale_shortMonths[d.getMonth()];\n }\n\n function formatMonth(d) {\n return locale_months[d.getMonth()];\n }\n\n function formatPeriod(d) {\n return locale_periods[+(d.getHours() >= 12)];\n }\n\n function formatQuarter(d) {\n return 1 + ~~(d.getMonth() / 3);\n }\n\n function formatUTCShortWeekday(d) {\n return locale_shortWeekdays[d.getUTCDay()];\n }\n\n function formatUTCWeekday(d) {\n return locale_weekdays[d.getUTCDay()];\n }\n\n function formatUTCShortMonth(d) {\n return locale_shortMonths[d.getUTCMonth()];\n }\n\n function formatUTCMonth(d) {\n return locale_months[d.getUTCMonth()];\n }\n\n function formatUTCPeriod(d) {\n return locale_periods[+(d.getUTCHours() >= 12)];\n }\n\n function formatUTCQuarter(d) {\n return 1 + ~~(d.getUTCMonth() / 3);\n }\n\n return {\n format: function(specifier) {\n var f = newFormat(specifier += \"\", formats);\n f.toString = function() { return specifier; };\n return f;\n },\n parse: function(specifier) {\n var p = newParse(specifier += \"\", false);\n p.toString = function() { return specifier; };\n return p;\n },\n utcFormat: function(specifier) {\n var f = newFormat(specifier += \"\", utcFormats);\n f.toString = function() { return specifier; };\n return f;\n },\n utcParse: function(specifier) {\n var p = newParse(specifier += \"\", true);\n p.toString = function() { return specifier; };\n return p;\n }\n };\n}\n\nvar pads = {\"-\": \"\", \"_\": \" \", \"0\": \"0\"},\n numberRe = /^\\s*\\d+/, // note: ignores next directive\n percentRe = /^%/,\n requoteRe = /[\\\\^$*+?|[\\]().{}]/g;\n\nfunction pad(value, fill, width) {\n var sign = value < 0 ? \"-\" : \"\",\n string = (sign ? -value : value) + \"\",\n length = string.length;\n return sign + (length < width ? new Array(width - length + 1).join(fill) + string : string);\n}\n\nfunction requote(s) {\n return s.replace(requoteRe, \"\\\\$&\");\n}\n\nfunction formatRe(names) {\n return new RegExp(\"^(?:\" + names.map(requote).join(\"|\") + \")\", \"i\");\n}\n\nfunction formatLookup(names) {\n return new Map(names.map((name, i) => [name.toLowerCase(), i]));\n}\n\nfunction parseWeekdayNumberSunday(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 1));\n return n ? (d.w = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseWeekdayNumberMonday(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 1));\n return n ? (d.u = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseWeekNumberSunday(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 2));\n return n ? (d.U = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseWeekNumberISO(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 2));\n return n ? (d.V = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseWeekNumberMonday(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 2));\n return n ? (d.W = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseFullYear(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 4));\n return n ? (d.y = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseYear(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 2));\n return n ? (d.y = +n[0] + (+n[0] > 68 ? 1900 : 2000), i + n[0].length) : -1;\n}\n\nfunction parseZone(d, string, i) {\n var n = /^(Z)|([+-]\\d\\d)(?::?(\\d\\d))?/.exec(string.slice(i, i + 6));\n return n ? (d.Z = n[1] ? 0 : -(n[2] + (n[3] || \"00\")), i + n[0].length) : -1;\n}\n\nfunction parseQuarter(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 1));\n return n ? (d.q = n[0] * 3 - 3, i + n[0].length) : -1;\n}\n\nfunction parseMonthNumber(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 2));\n return n ? (d.m = n[0] - 1, i + n[0].length) : -1;\n}\n\nfunction parseDayOfMonth(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 2));\n return n ? (d.d = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseDayOfYear(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 3));\n return n ? (d.m = 0, d.d = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseHour24(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 2));\n return n ? (d.H = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseMinutes(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 2));\n return n ? (d.M = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseSeconds(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 2));\n return n ? (d.S = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseMilliseconds(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 3));\n return n ? (d.L = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseMicroseconds(d, string, i) {\n var n = numberRe.exec(string.slice(i, i + 6));\n return n ? (d.L = Math.floor(n[0] / 1000), i + n[0].length) : -1;\n}\n\nfunction parseLiteralPercent(d, string, i) {\n var n = percentRe.exec(string.slice(i, i + 1));\n return n ? i + n[0].length : -1;\n}\n\nfunction parseUnixTimestamp(d, string, i) {\n var n = numberRe.exec(string.slice(i));\n return n ? (d.Q = +n[0], i + n[0].length) : -1;\n}\n\nfunction parseUnixTimestampSeconds(d, string, i) {\n var n = numberRe.exec(string.slice(i));\n return n ? (d.s = +n[0], i + n[0].length) : -1;\n}\n\nfunction formatDayOfMonth(d, p) {\n return pad(d.getDate(), p, 2);\n}\n\nfunction formatHour24(d, p) {\n return pad(d.getHours(), p, 2);\n}\n\nfunction formatHour12(d, p) {\n return pad(d.getHours() % 12 || 12, p, 2);\n}\n\nfunction formatDayOfYear(d, p) {\n return pad(1 + timeDay.count(timeYear(d), d), p, 3);\n}\n\nfunction formatMilliseconds(d, p) {\n return pad(d.getMilliseconds(), p, 3);\n}\n\nfunction formatMicroseconds(d, p) {\n return formatMilliseconds(d, p) + \"000\";\n}\n\nfunction formatMonthNumber(d, p) {\n return pad(d.getMonth() + 1, p, 2);\n}\n\nfunction formatMinutes(d, p) {\n return pad(d.getMinutes(), p, 2);\n}\n\nfunction formatSeconds(d, p) {\n return pad(d.getSeconds(), p, 2);\n}\n\nfunction formatWeekdayNumberMonday(d) {\n var day = d.getDay();\n return day === 0 ? 7 : day;\n}\n\nfunction formatWeekNumberSunday(d, p) {\n return pad(timeSunday.count(timeYear(d) - 1, d), p, 2);\n}\n\nfunction dISO(d) {\n var day = d.getDay();\n return (day >= 4 || day === 0) ? timeThursday(d) : timeThursday.ceil(d);\n}\n\nfunction formatWeekNumberISO(d, p) {\n d = dISO(d);\n return pad(timeThursday.count(timeYear(d), d) + (timeYear(d).getDay() === 4), p, 2);\n}\n\nfunction formatWeekdayNumberSunday(d) {\n return d.getDay();\n}\n\nfunction formatWeekNumberMonday(d, p) {\n return pad(timeMonday.count(timeYear(d) - 1, d), p, 2);\n}\n\nfunction formatYear(d, p) {\n return pad(d.getFullYear() % 100, p, 2);\n}\n\nfunction formatYearISO(d, p) {\n d = dISO(d);\n return pad(d.getFullYear() % 100, p, 2);\n}\n\nfunction formatFullYear(d, p) {\n return pad(d.getFullYear() % 10000, p, 4);\n}\n\nfunction formatFullYearISO(d, p) {\n var day = d.getDay();\n d = (day >= 4 || day === 0) ? timeThursday(d) : timeThursday.ceil(d);\n return pad(d.getFullYear() % 10000, p, 4);\n}\n\nfunction formatZone(d) {\n var z = d.getTimezoneOffset();\n return (z > 0 ? \"-\" : (z *= -1, \"+\"))\n + pad(z / 60 | 0, \"0\", 2)\n + pad(z % 60, \"0\", 2);\n}\n\nfunction formatUTCDayOfMonth(d, p) {\n return pad(d.getUTCDate(), p, 2);\n}\n\nfunction formatUTCHour24(d, p) {\n return pad(d.getUTCHours(), p, 2);\n}\n\nfunction formatUTCHour12(d, p) {\n return pad(d.getUTCHours() % 12 || 12, p, 2);\n}\n\nfunction formatUTCDayOfYear(d, p) {\n return pad(1 + utcDay.count(utcYear(d), d), p, 3);\n}\n\nfunction formatUTCMilliseconds(d, p) {\n return pad(d.getUTCMilliseconds(), p, 3);\n}\n\nfunction formatUTCMicroseconds(d, p) {\n return formatUTCMilliseconds(d, p) + \"000\";\n}\n\nfunction formatUTCMonthNumber(d, p) {\n return pad(d.getUTCMonth() + 1, p, 2);\n}\n\nfunction formatUTCMinutes(d, p) {\n return pad(d.getUTCMinutes(), p, 2);\n}\n\nfunction formatUTCSeconds(d, p) {\n return pad(d.getUTCSeconds(), p, 2);\n}\n\nfunction formatUTCWeekdayNumberMonday(d) {\n var dow = d.getUTCDay();\n return dow === 0 ? 7 : dow;\n}\n\nfunction formatUTCWeekNumberSunday(d, p) {\n return pad(utcSunday.count(utcYear(d) - 1, d), p, 2);\n}\n\nfunction UTCdISO(d) {\n var day = d.getUTCDay();\n return (day >= 4 || day === 0) ? utcThursday(d) : utcThursday.ceil(d);\n}\n\nfunction formatUTCWeekNumberISO(d, p) {\n d = UTCdISO(d);\n return pad(utcThursday.count(utcYear(d), d) + (utcYear(d).getUTCDay() === 4), p, 2);\n}\n\nfunction formatUTCWeekdayNumberSunday(d) {\n return d.getUTCDay();\n}\n\nfunction formatUTCWeekNumberMonday(d, p) {\n return pad(utcMonday.count(utcYear(d) - 1, d), p, 2);\n}\n\nfunction formatUTCYear(d, p) {\n return pad(d.getUTCFullYear() % 100, p, 2);\n}\n\nfunction formatUTCYearISO(d, p) {\n d = UTCdISO(d);\n return pad(d.getUTCFullYear() % 100, p, 2);\n}\n\nfunction formatUTCFullYear(d, p) {\n return pad(d.getUTCFullYear() % 10000, p, 4);\n}\n\nfunction formatUTCFullYearISO(d, p) {\n var day = d.getUTCDay();\n d = (day >= 4 || day === 0) ? utcThursday(d) : utcThursday.ceil(d);\n return pad(d.getUTCFullYear() % 10000, p, 4);\n}\n\nfunction formatUTCZone() {\n return \"+0000\";\n}\n\nfunction formatLiteralPercent() {\n return \"%\";\n}\n\nfunction formatUnixTimestamp(d) {\n return +d;\n}\n\nfunction formatUnixTimestampSeconds(d) {\n return Math.floor(+d / 1000);\n}\n", "import formatLocale from \"./locale.js\";\n\nvar locale;\nexport var timeFormat;\nexport var timeParse;\nexport var utcFormat;\nexport var utcParse;\n\ndefaultLocale({\n dateTime: \"%x, %X\",\n date: \"%-m/%-d/%Y\",\n time: \"%-I:%M:%S %p\",\n periods: [\"AM\", \"PM\"],\n days: [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"],\n shortDays: [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"],\n months: [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"],\n shortMonths: [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n});\n\nexport default function defaultLocale(definition) {\n locale = formatLocale(definition);\n timeFormat = locale.format;\n timeParse = locale.parse;\n utcFormat = locale.utcFormat;\n utcParse = locale.utcParse;\n return locale;\n}\n", "import {utcFormat} from \"./defaultLocale.js\";\n\nexport var isoSpecifier = \"%Y-%m-%dT%H:%M:%S.%LZ\";\n\nfunction formatIsoNative(date) {\n return date.toISOString();\n}\n\nvar formatIso = Date.prototype.toISOString\n ? formatIsoNative\n : utcFormat(isoSpecifier);\n\nexport default formatIso;\n", "import {isoSpecifier} from \"./isoFormat.js\";\nimport {utcParse} from \"./defaultLocale.js\";\n\nfunction parseIsoNative(string) {\n var date = new Date(string);\n return isNaN(date) ? null : date;\n}\n\nvar parseIso = +new Date(\"2000-01-01T00:00:00.000Z\")\n ? parseIsoNative\n : utcParse(isoSpecifier);\n\nexport default parseIso;\n", "export {default as timeFormatDefaultLocale, timeFormat, timeParse, utcFormat, utcParse} from \"./defaultLocale.js\";\nexport {default as timeFormatLocale} from \"./locale.js\";\nexport {default as isoFormat} from \"./isoFormat.js\";\nexport {default as isoParse} from \"./isoParse.js\";\n", "import {timeYear, timeMonth, timeWeek, timeDay, timeHour, timeMinute, timeSecond, timeTicks, timeTickInterval} from \"d3-time\";\nimport {timeFormat} from \"d3-time-format\";\nimport continuous, {copy} from \"./continuous.js\";\nimport {initRange} from \"./init.js\";\nimport nice from \"./nice.js\";\n\nfunction date(t) {\n return new Date(t);\n}\n\nfunction number(t) {\n return t instanceof Date ? +t : +new Date(+t);\n}\n\nexport function calendar(ticks, tickInterval, year, month, week, day, hour, minute, second, format) {\n var scale = continuous(),\n invert = scale.invert,\n domain = scale.domain;\n\n var formatMillisecond = format(\".%L\"),\n formatSecond = format(\":%S\"),\n formatMinute = format(\"%I:%M\"),\n formatHour = format(\"%I %p\"),\n formatDay = format(\"%a %d\"),\n formatWeek = format(\"%b %d\"),\n formatMonth = format(\"%B\"),\n formatYear = format(\"%Y\");\n\n function tickFormat(date) {\n return (second(date) < date ? formatMillisecond\n : minute(date) < date ? formatSecond\n : hour(date) < date ? formatMinute\n : day(date) < date ? formatHour\n : month(date) < date ? (week(date) < date ? formatDay : formatWeek)\n : year(date) < date ? formatMonth\n : formatYear)(date);\n }\n\n scale.invert = function(y) {\n return new Date(invert(y));\n };\n\n scale.domain = function(_) {\n return arguments.length ? domain(Array.from(_, number)) : domain().map(date);\n };\n\n scale.ticks = function(interval) {\n var d = domain();\n return ticks(d[0], d[d.length - 1], interval == null ? 10 : interval);\n };\n\n scale.tickFormat = function(count, specifier) {\n return specifier == null ? tickFormat : format(specifier);\n };\n\n scale.nice = function(interval) {\n var d = domain();\n if (!interval || typeof interval.range !== \"function\") interval = tickInterval(d[0], d[d.length - 1], interval == null ? 10 : interval);\n return interval ? domain(nice(d, interval)) : scale;\n };\n\n scale.copy = function() {\n return copy(scale, calendar(ticks, tickInterval, year, month, week, day, hour, minute, second, format));\n };\n\n return scale;\n}\n\nexport default function time() {\n return initRange.apply(calendar(timeTicks, timeTickInterval, timeYear, timeMonth, timeWeek, timeDay, timeHour, timeMinute, timeSecond, timeFormat).domain([new Date(2000, 0, 1), new Date(2000, 0, 2)]), arguments);\n}\n", "import {utcYear, utcMonth, utcWeek, utcDay, utcHour, utcMinute, utcSecond, utcTicks, utcTickInterval} from \"d3-time\";\nimport {utcFormat} from \"d3-time-format\";\nimport {calendar} from \"./time.js\";\nimport {initRange} from \"./init.js\";\n\nexport default function utcTime() {\n return initRange.apply(calendar(utcTicks, utcTickInterval, utcYear, utcMonth, utcWeek, utcDay, utcHour, utcMinute, utcSecond, utcFormat).domain([Date.UTC(2000, 0, 1), Date.UTC(2000, 0, 2)]), arguments);\n}\n", "import {interpolate, interpolateRound} from \"d3-interpolate\";\nimport {identity} from \"./continuous.js\";\nimport {initInterpolator} from \"./init.js\";\nimport {linearish} from \"./linear.js\";\nimport {loggish} from \"./log.js\";\nimport {symlogish} from \"./symlog.js\";\nimport {powish} from \"./pow.js\";\n\nfunction transformer() {\n var x0 = 0,\n x1 = 1,\n t0,\n t1,\n k10,\n transform,\n interpolator = identity,\n clamp = false,\n unknown;\n\n function scale(x) {\n return x == null || isNaN(x = +x) ? unknown : interpolator(k10 === 0 ? 0.5 : (x = (transform(x) - t0) * k10, clamp ? Math.max(0, Math.min(1, x)) : x));\n }\n\n scale.domain = function(_) {\n return arguments.length ? ([x0, x1] = _, t0 = transform(x0 = +x0), t1 = transform(x1 = +x1), k10 = t0 === t1 ? 0 : 1 / (t1 - t0), scale) : [x0, x1];\n };\n\n scale.clamp = function(_) {\n return arguments.length ? (clamp = !!_, scale) : clamp;\n };\n\n scale.interpolator = function(_) {\n return arguments.length ? (interpolator = _, scale) : interpolator;\n };\n\n function range(interpolate) {\n return function(_) {\n var r0, r1;\n return arguments.length ? ([r0, r1] = _, interpolator = interpolate(r0, r1), scale) : [interpolator(0), interpolator(1)];\n };\n }\n\n scale.range = range(interpolate);\n\n scale.rangeRound = range(interpolateRound);\n\n scale.unknown = function(_) {\n return arguments.length ? (unknown = _, scale) : unknown;\n };\n\n return function(t) {\n transform = t, t0 = t(x0), t1 = t(x1), k10 = t0 === t1 ? 0 : 1 / (t1 - t0);\n return scale;\n };\n}\n\nexport function copy(source, target) {\n return target\n .domain(source.domain())\n .interpolator(source.interpolator())\n .clamp(source.clamp())\n .unknown(source.unknown());\n}\n\nexport default function sequential() {\n var scale = linearish(transformer()(identity));\n\n scale.copy = function() {\n return copy(scale, sequential());\n };\n\n return initInterpolator.apply(scale, arguments);\n}\n\nexport function sequentialLog() {\n var scale = loggish(transformer()).domain([1, 10]);\n\n scale.copy = function() {\n return copy(scale, sequentialLog()).base(scale.base());\n };\n\n return initInterpolator.apply(scale, arguments);\n}\n\nexport function sequentialSymlog() {\n var scale = symlogish(transformer());\n\n scale.copy = function() {\n return copy(scale, sequentialSymlog()).constant(scale.constant());\n };\n\n return initInterpolator.apply(scale, arguments);\n}\n\nexport function sequentialPow() {\n var scale = powish(transformer());\n\n scale.copy = function() {\n return copy(scale, sequentialPow()).exponent(scale.exponent());\n };\n\n return initInterpolator.apply(scale, arguments);\n}\n\nexport function sequentialSqrt() {\n return sequentialPow.apply(null, arguments).exponent(0.5);\n}\n", "import {ascending, bisect, quantile} from \"d3-array\";\nimport {identity} from \"./continuous.js\";\nimport {initInterpolator} from \"./init.js\";\n\nexport default function sequentialQuantile() {\n var domain = [],\n interpolator = identity;\n\n function scale(x) {\n if (x != null && !isNaN(x = +x)) return interpolator((bisect(domain, x, 1) - 1) / (domain.length - 1));\n }\n\n scale.domain = function(_) {\n if (!arguments.length) return domain.slice();\n domain = [];\n for (let d of _) if (d != null && !isNaN(d = +d)) domain.push(d);\n domain.sort(ascending);\n return scale;\n };\n\n scale.interpolator = function(_) {\n return arguments.length ? (interpolator = _, scale) : interpolator;\n };\n\n scale.range = function() {\n return domain.map((d, i) => interpolator(i / (domain.length - 1)));\n };\n\n scale.quantiles = function(n) {\n return Array.from({length: n + 1}, (_, i) => quantile(domain, i / n));\n };\n\n scale.copy = function() {\n return sequentialQuantile(interpolator).domain(domain);\n };\n\n return initInterpolator.apply(scale, arguments);\n}\n", "import {interpolate, interpolateRound, piecewise} from \"d3-interpolate\";\nimport {identity} from \"./continuous.js\";\nimport {initInterpolator} from \"./init.js\";\nimport {linearish} from \"./linear.js\";\nimport {loggish} from \"./log.js\";\nimport {copy} from \"./sequential.js\";\nimport {symlogish} from \"./symlog.js\";\nimport {powish} from \"./pow.js\";\n\nfunction transformer() {\n var x0 = 0,\n x1 = 0.5,\n x2 = 1,\n s = 1,\n t0,\n t1,\n t2,\n k10,\n k21,\n interpolator = identity,\n transform,\n clamp = false,\n unknown;\n\n function scale(x) {\n return isNaN(x = +x) ? unknown : (x = 0.5 + ((x = +transform(x)) - t1) * (s * x < s * t1 ? k10 : k21), interpolator(clamp ? Math.max(0, Math.min(1, x)) : x));\n }\n\n scale.domain = function(_) {\n return arguments.length ? ([x0, x1, x2] = _, t0 = transform(x0 = +x0), t1 = transform(x1 = +x1), t2 = transform(x2 = +x2), k10 = t0 === t1 ? 0 : 0.5 / (t1 - t0), k21 = t1 === t2 ? 0 : 0.5 / (t2 - t1), s = t1 < t0 ? -1 : 1, scale) : [x0, x1, x2];\n };\n\n scale.clamp = function(_) {\n return arguments.length ? (clamp = !!_, scale) : clamp;\n };\n\n scale.interpolator = function(_) {\n return arguments.length ? (interpolator = _, scale) : interpolator;\n };\n\n function range(interpolate) {\n return function(_) {\n var r0, r1, r2;\n return arguments.length ? ([r0, r1, r2] = _, interpolator = piecewise(interpolate, [r0, r1, r2]), scale) : [interpolator(0), interpolator(0.5), interpolator(1)];\n };\n }\n\n scale.range = range(interpolate);\n\n scale.rangeRound = range(interpolateRound);\n\n scale.unknown = function(_) {\n return arguments.length ? (unknown = _, scale) : unknown;\n };\n\n return function(t) {\n transform = t, t0 = t(x0), t1 = t(x1), t2 = t(x2), k10 = t0 === t1 ? 0 : 0.5 / (t1 - t0), k21 = t1 === t2 ? 0 : 0.5 / (t2 - t1), s = t1 < t0 ? -1 : 1;\n return scale;\n };\n}\n\nexport default function diverging() {\n var scale = linearish(transformer()(identity));\n\n scale.copy = function() {\n return copy(scale, diverging());\n };\n\n return initInterpolator.apply(scale, arguments);\n}\n\nexport function divergingLog() {\n var scale = loggish(transformer()).domain([0.1, 1, 10]);\n\n scale.copy = function() {\n return copy(scale, divergingLog()).base(scale.base());\n };\n\n return initInterpolator.apply(scale, arguments);\n}\n\nexport function divergingSymlog() {\n var scale = symlogish(transformer());\n\n scale.copy = function() {\n return copy(scale, divergingSymlog()).constant(scale.constant());\n };\n\n return initInterpolator.apply(scale, arguments);\n}\n\nexport function divergingPow() {\n var scale = powish(transformer());\n\n scale.copy = function() {\n return copy(scale, divergingPow()).exponent(scale.exponent());\n };\n\n return initInterpolator.apply(scale, arguments);\n}\n\nexport function divergingSqrt() {\n return divergingPow.apply(null, arguments).exponent(0.5);\n}\n", "export {\n default as scaleBand,\n point as scalePoint\n} from \"./band.js\";\n\nexport {\n default as scaleIdentity\n} from \"./identity.js\";\n\nexport {\n default as scaleLinear\n} from \"./linear.js\";\n\nexport {\n default as scaleLog\n} from \"./log.js\";\n\nexport {\n default as scaleSymlog\n} from \"./symlog.js\";\n\nexport {\n default as scaleOrdinal,\n implicit as scaleImplicit\n} from \"./ordinal.js\";\n\nexport {\n default as scalePow,\n sqrt as scaleSqrt\n} from \"./pow.js\";\n\nexport {\n default as scaleRadial\n} from \"./radial.js\";\n\nexport {\n default as scaleQuantile\n} from \"./quantile.js\";\n\nexport {\n default as scaleQuantize\n} from \"./quantize.js\";\n\nexport {\n default as scaleThreshold\n} from \"./threshold.js\";\n\nexport {\n default as scaleTime\n} from \"./time.js\";\n\nexport {\n default as scaleUtc\n} from \"./utcTime.js\";\n\nexport {\n default as scaleSequential,\n sequentialLog as scaleSequentialLog,\n sequentialPow as scaleSequentialPow,\n sequentialSqrt as scaleSequentialSqrt,\n sequentialSymlog as scaleSequentialSymlog\n} from \"./sequential.js\";\n\nexport {\n default as scaleSequentialQuantile\n} from \"./sequentialQuantile.js\";\n\nexport {\n default as scaleDiverging,\n divergingLog as scaleDivergingLog,\n divergingPow as scaleDivergingPow,\n divergingSqrt as scaleDivergingSqrt,\n divergingSymlog as scaleDivergingSymlog\n} from \"./diverging.js\";\n\nexport {\n default as tickFormat\n} from \"./tickFormat.js\";\n", "export {default as schemeCategory10} from \"./categorical/category10.js\";\nexport {default as schemeAccent} from \"./categorical/Accent.js\";\nexport {default as schemeDark2} from \"./categorical/Dark2.js\";\nexport {default as schemeObservable10} from \"./categorical/observable10.js\";\nexport {default as schemePaired} from \"./categorical/Paired.js\";\nexport {default as schemePastel1} from \"./categorical/Pastel1.js\";\nexport {default as schemePastel2} from \"./categorical/Pastel2.js\";\nexport {default as schemeSet1} from \"./categorical/Set1.js\";\nexport {default as schemeSet2} from \"./categorical/Set2.js\";\nexport {default as schemeSet3} from \"./categorical/Set3.js\";\nexport {default as schemeTableau10} from \"./categorical/Tableau10.js\";\nexport {default as interpolateBrBG, scheme as schemeBrBG} from \"./diverging/BrBG.js\";\nexport {default as interpolatePRGn, scheme as schemePRGn} from \"./diverging/PRGn.js\";\nexport {default as interpolatePiYG, scheme as schemePiYG} from \"./diverging/PiYG.js\";\nexport {default as interpolatePuOr, scheme as schemePuOr} from \"./diverging/PuOr.js\";\nexport {default as interpolateRdBu, scheme as schemeRdBu} from \"./diverging/RdBu.js\";\nexport {default as interpolateRdGy, scheme as schemeRdGy} from \"./diverging/RdGy.js\";\nexport {default as interpolateRdYlBu, scheme as schemeRdYlBu} from \"./diverging/RdYlBu.js\";\nexport {default as interpolateRdYlGn, scheme as schemeRdYlGn} from \"./diverging/RdYlGn.js\";\nexport {default as interpolateSpectral, scheme as schemeSpectral} from \"./diverging/Spectral.js\";\nexport {default as interpolateBuGn, scheme as schemeBuGn} from \"./sequential-multi/BuGn.js\";\nexport {default as interpolateBuPu, scheme as schemeBuPu} from \"./sequential-multi/BuPu.js\";\nexport {default as interpolateGnBu, scheme as schemeGnBu} from \"./sequential-multi/GnBu.js\";\nexport {default as interpolateOrRd, scheme as schemeOrRd} from \"./sequential-multi/OrRd.js\";\nexport {default as interpolatePuBuGn, scheme as schemePuBuGn} from \"./sequential-multi/PuBuGn.js\";\nexport {default as interpolatePuBu, scheme as schemePuBu} from \"./sequential-multi/PuBu.js\";\nexport {default as interpolatePuRd, scheme as schemePuRd} from \"./sequential-multi/PuRd.js\";\nexport {default as interpolateRdPu, scheme as schemeRdPu} from \"./sequential-multi/RdPu.js\";\nexport {default as interpolateYlGnBu, scheme as schemeYlGnBu} from \"./sequential-multi/YlGnBu.js\";\nexport {default as interpolateYlGn, scheme as schemeYlGn} from \"./sequential-multi/YlGn.js\";\nexport {default as interpolateYlOrBr, scheme as schemeYlOrBr} from \"./sequential-multi/YlOrBr.js\";\nexport {default as interpolateYlOrRd, scheme as schemeYlOrRd} from \"./sequential-multi/YlOrRd.js\";\nexport {default as interpolateBlues, scheme as schemeBlues} from \"./sequential-single/Blues.js\";\nexport {default as interpolateGreens, scheme as schemeGreens} from \"./sequential-single/Greens.js\";\nexport {default as interpolateGreys, scheme as schemeGreys} from \"./sequential-single/Greys.js\";\nexport {default as interpolatePurples, scheme as schemePurples} from \"./sequential-single/Purples.js\";\nexport {default as interpolateReds, scheme as schemeReds} from \"./sequential-single/Reds.js\";\nexport {default as interpolateOranges, scheme as schemeOranges} from \"./sequential-single/Oranges.js\";\nexport {default as interpolateCividis} from \"./sequential-multi/cividis.js\";\nexport {default as interpolateCubehelixDefault} from \"./sequential-multi/cubehelix.js\";\nexport {default as interpolateRainbow, warm as interpolateWarm, cool as interpolateCool} from \"./sequential-multi/rainbow.js\";\nexport {default as interpolateSinebow} from \"./sequential-multi/sinebow.js\";\nexport {default as interpolateTurbo} from \"./sequential-multi/turbo.js\";\nexport {default as interpolateViridis, magma as interpolateMagma, inferno as interpolateInferno, plasma as interpolatePlasma} from \"./sequential-multi/viridis.js\";\n", "export {default as arc} from \"./arc.js\";\nexport {default as area} from \"./area.js\";\nexport {default as line} from \"./line.js\";\nexport {default as pie} from \"./pie.js\";\nexport {default as areaRadial, default as radialArea} from \"./areaRadial.js\"; // Note: radialArea is deprecated!\nexport {default as lineRadial, default as radialLine} from \"./lineRadial.js\"; // Note: radialLine is deprecated!\nexport {default as pointRadial} from \"./pointRadial.js\";\nexport {link, linkHorizontal, linkVertical, linkRadial} from \"./link.js\";\n\nexport {default as symbol, symbolsStroke, symbolsFill, symbolsFill as symbols} from \"./symbol.js\";\nexport {default as symbolAsterisk} from \"./symbol/asterisk.js\";\nexport {default as symbolCircle} from \"./symbol/circle.js\";\nexport {default as symbolCross} from \"./symbol/cross.js\";\nexport {default as symbolDiamond} from \"./symbol/diamond.js\";\nexport {default as symbolDiamond2} from \"./symbol/diamond2.js\";\nexport {default as symbolPlus} from \"./symbol/plus.js\";\nexport {default as symbolSquare} from \"./symbol/square.js\";\nexport {default as symbolSquare2} from \"./symbol/square2.js\";\nexport {default as symbolStar} from \"./symbol/star.js\";\nexport {default as symbolTriangle} from \"./symbol/triangle.js\";\nexport {default as symbolTriangle2} from \"./symbol/triangle2.js\";\nexport {default as symbolWye} from \"./symbol/wye.js\";\nexport {default as symbolTimes, default as symbolX} from \"./symbol/times.js\";\n\nexport {default as curveBasisClosed} from \"./curve/basisClosed.js\";\nexport {default as curveBasisOpen} from \"./curve/basisOpen.js\";\nexport {default as curveBasis} from \"./curve/basis.js\";\nexport {bumpX as curveBumpX, bumpY as curveBumpY} from \"./curve/bump.js\";\nexport {default as curveBundle} from \"./curve/bundle.js\";\nexport {default as curveCardinalClosed} from \"./curve/cardinalClosed.js\";\nexport {default as curveCardinalOpen} from \"./curve/cardinalOpen.js\";\nexport {default as curveCardinal} from \"./curve/cardinal.js\";\nexport {default as curveCatmullRomClosed} from \"./curve/catmullRomClosed.js\";\nexport {default as curveCatmullRomOpen} from \"./curve/catmullRomOpen.js\";\nexport {default as curveCatmullRom} from \"./curve/catmullRom.js\";\nexport {default as curveLinearClosed} from \"./curve/linearClosed.js\";\nexport {default as curveLinear} from \"./curve/linear.js\";\nexport {monotoneX as curveMonotoneX, monotoneY as curveMonotoneY} from \"./curve/monotone.js\";\nexport {default as curveNatural} from \"./curve/natural.js\";\nexport {default as curveStep, stepAfter as curveStepAfter, stepBefore as curveStepBefore} from \"./curve/step.js\";\n\nexport {default as stack} from \"./stack.js\";\nexport {default as stackOffsetExpand} from \"./offset/expand.js\";\nexport {default as stackOffsetDiverging} from \"./offset/diverging.js\";\nexport {default as stackOffsetNone} from \"./offset/none.js\";\nexport {default as stackOffsetSilhouette} from \"./offset/silhouette.js\";\nexport {default as stackOffsetWiggle} from \"./offset/wiggle.js\";\nexport {default as stackOrderAppearance} from \"./order/appearance.js\";\nexport {default as stackOrderAscending} from \"./order/ascending.js\";\nexport {default as stackOrderDescending} from \"./order/descending.js\";\nexport {default as stackOrderInsideOut} from \"./order/insideOut.js\";\nexport {default as stackOrderNone} from \"./order/none.js\";\nexport {default as stackOrderReverse} from \"./order/reverse.js\";\n", "export * from \"d3-array\";\nexport * from \"d3-axis\";\nexport * from \"d3-brush\";\nexport * from \"d3-chord\";\nexport * from \"d3-color\";\nexport * from \"d3-contour\";\nexport * from \"d3-delaunay\";\nexport * from \"d3-dispatch\";\nexport * from \"d3-drag\";\nexport * from \"d3-dsv\";\nexport * from \"d3-ease\";\nexport * from \"d3-fetch\";\nexport * from \"d3-force\";\nexport * from \"d3-format\";\nexport * from \"d3-geo\";\nexport * from \"d3-hierarchy\";\nexport * from \"d3-interpolate\";\nexport * from \"d3-path\";\nexport * from \"d3-polygon\";\nexport * from \"d3-quadtree\";\nexport * from \"d3-random\";\nexport * from \"d3-scale\";\nexport * from \"d3-scale-chromatic\";\nexport * from \"d3-selection\";\nexport * from \"d3-shape\";\nexport * from \"d3-time\";\nexport * from \"d3-time-format\";\nexport * from \"d3-timer\";\nexport * from \"d3-transition\";\nexport * from \"d3-zoom\";\n", "exports.remove = removeDiacritics;\n\nvar replacementList = [\n {\n base: ' ',\n chars: \"\\u00A0\",\n }, {\n base: '0',\n chars: \"\\u07C0\",\n }, {\n base: 'A',\n chars: \"\\u24B6\\uFF21\\u00C0\\u00C1\\u00C2\\u1EA6\\u1EA4\\u1EAA\\u1EA8\\u00C3\\u0100\\u0102\\u1EB0\\u1EAE\\u1EB4\\u1EB2\\u0226\\u01E0\\u00C4\\u01DE\\u1EA2\\u00C5\\u01FA\\u01CD\\u0200\\u0202\\u1EA0\\u1EAC\\u1EB6\\u1E00\\u0104\\u023A\\u2C6F\",\n }, {\n base: 'AA',\n chars: \"\\uA732\",\n }, {\n base: 'AE',\n chars: \"\\u00C6\\u01FC\\u01E2\",\n }, {\n base: 'AO',\n chars: \"\\uA734\",\n }, {\n base: 'AU',\n chars: \"\\uA736\",\n }, {\n base: 'AV',\n chars: \"\\uA738\\uA73A\",\n }, {\n base: 'AY',\n chars: \"\\uA73C\",\n }, {\n base: 'B',\n chars: \"\\u24B7\\uFF22\\u1E02\\u1E04\\u1E06\\u0243\\u0181\",\n }, {\n base: 'C',\n chars: \"\\u24b8\\uff23\\uA73E\\u1E08\\u0106\\u0043\\u0108\\u010A\\u010C\\u00C7\\u0187\\u023B\",\n }, {\n base: 'D',\n chars: \"\\u24B9\\uFF24\\u1E0A\\u010E\\u1E0C\\u1E10\\u1E12\\u1E0E\\u0110\\u018A\\u0189\\u1D05\\uA779\",\n }, {\n base: 'Dh',\n chars: \"\\u00D0\",\n }, {\n base: 'DZ',\n chars: \"\\u01F1\\u01C4\",\n }, {\n base: 'Dz',\n chars: \"\\u01F2\\u01C5\",\n }, {\n base: 'E',\n chars: \"\\u025B\\u24BA\\uFF25\\u00C8\\u00C9\\u00CA\\u1EC0\\u1EBE\\u1EC4\\u1EC2\\u1EBC\\u0112\\u1E14\\u1E16\\u0114\\u0116\\u00CB\\u1EBA\\u011A\\u0204\\u0206\\u1EB8\\u1EC6\\u0228\\u1E1C\\u0118\\u1E18\\u1E1A\\u0190\\u018E\\u1D07\",\n }, {\n base: 'F',\n chars: \"\\uA77C\\u24BB\\uFF26\\u1E1E\\u0191\\uA77B\",\n }, {\n base: 'G',\n chars: \"\\u24BC\\uFF27\\u01F4\\u011C\\u1E20\\u011E\\u0120\\u01E6\\u0122\\u01E4\\u0193\\uA7A0\\uA77D\\uA77E\\u0262\",\n }, {\n base: 'H',\n chars: \"\\u24BD\\uFF28\\u0124\\u1E22\\u1E26\\u021E\\u1E24\\u1E28\\u1E2A\\u0126\\u2C67\\u2C75\\uA78D\",\n }, {\n base: 'I',\n chars: \"\\u24BE\\uFF29\\xCC\\xCD\\xCE\\u0128\\u012A\\u012C\\u0130\\xCF\\u1E2E\\u1EC8\\u01CF\\u0208\\u020A\\u1ECA\\u012E\\u1E2C\\u0197\",\n }, {\n base: 'J',\n chars: \"\\u24BF\\uFF2A\\u0134\\u0248\\u0237\",\n }, {\n base: 'K',\n chars: \"\\u24C0\\uFF2B\\u1E30\\u01E8\\u1E32\\u0136\\u1E34\\u0198\\u2C69\\uA740\\uA742\\uA744\\uA7A2\",\n }, {\n base: 'L',\n chars: \"\\u24C1\\uFF2C\\u013F\\u0139\\u013D\\u1E36\\u1E38\\u013B\\u1E3C\\u1E3A\\u0141\\u023D\\u2C62\\u2C60\\uA748\\uA746\\uA780\",\n }, {\n base: 'LJ',\n chars: \"\\u01C7\",\n }, {\n base: 'Lj',\n chars: \"\\u01C8\",\n }, {\n base: 'M',\n chars: \"\\u24C2\\uFF2D\\u1E3E\\u1E40\\u1E42\\u2C6E\\u019C\\u03FB\",\n }, {\n base: 'N',\n chars: \"\\uA7A4\\u0220\\u24C3\\uFF2E\\u01F8\\u0143\\xD1\\u1E44\\u0147\\u1E46\\u0145\\u1E4A\\u1E48\\u019D\\uA790\\u1D0E\",\n }, {\n base: 'NJ',\n chars: \"\\u01CA\",\n }, {\n base: 'Nj',\n chars: \"\\u01CB\",\n }, {\n base: 'O',\n chars: \"\\u24C4\\uFF2F\\xD2\\xD3\\xD4\\u1ED2\\u1ED0\\u1ED6\\u1ED4\\xD5\\u1E4C\\u022C\\u1E4E\\u014C\\u1E50\\u1E52\\u014E\\u022E\\u0230\\xD6\\u022A\\u1ECE\\u0150\\u01D1\\u020C\\u020E\\u01A0\\u1EDC\\u1EDA\\u1EE0\\u1EDE\\u1EE2\\u1ECC\\u1ED8\\u01EA\\u01EC\\xD8\\u01FE\\u0186\\u019F\\uA74A\\uA74C\",\n }, {\n base: 'OE',\n chars: \"\\u0152\",\n }, {\n base: 'OI',\n chars: \"\\u01A2\",\n }, {\n base: 'OO',\n chars: \"\\uA74E\",\n }, {\n base: 'OU',\n chars: \"\\u0222\",\n }, {\n base: 'P',\n chars: \"\\u24C5\\uFF30\\u1E54\\u1E56\\u01A4\\u2C63\\uA750\\uA752\\uA754\",\n }, {\n base: 'Q',\n chars: \"\\u24C6\\uFF31\\uA756\\uA758\\u024A\",\n }, {\n base: 'R',\n chars: \"\\u24C7\\uFF32\\u0154\\u1E58\\u0158\\u0210\\u0212\\u1E5A\\u1E5C\\u0156\\u1E5E\\u024C\\u2C64\\uA75A\\uA7A6\\uA782\",\n }, {\n base: 'S',\n chars: \"\\u24C8\\uFF33\\u1E9E\\u015A\\u1E64\\u015C\\u1E60\\u0160\\u1E66\\u1E62\\u1E68\\u0218\\u015E\\u2C7E\\uA7A8\\uA784\",\n }, {\n base: 'T',\n chars: \"\\u24C9\\uFF34\\u1E6A\\u0164\\u1E6C\\u021A\\u0162\\u1E70\\u1E6E\\u0166\\u01AC\\u01AE\\u023E\\uA786\",\n }, {\n base: 'Th',\n chars: \"\\u00DE\",\n }, {\n base: 'TZ',\n chars: \"\\uA728\",\n }, {\n base: 'U',\n chars: \"\\u24CA\\uFF35\\xD9\\xDA\\xDB\\u0168\\u1E78\\u016A\\u1E7A\\u016C\\xDC\\u01DB\\u01D7\\u01D5\\u01D9\\u1EE6\\u016E\\u0170\\u01D3\\u0214\\u0216\\u01AF\\u1EEA\\u1EE8\\u1EEE\\u1EEC\\u1EF0\\u1EE4\\u1E72\\u0172\\u1E76\\u1E74\\u0244\",\n }, {\n base: 'V',\n chars: \"\\u24CB\\uFF36\\u1E7C\\u1E7E\\u01B2\\uA75E\\u0245\",\n }, {\n base: 'VY',\n chars: \"\\uA760\",\n }, {\n base: 'W',\n chars: \"\\u24CC\\uFF37\\u1E80\\u1E82\\u0174\\u1E86\\u1E84\\u1E88\\u2C72\",\n }, {\n base: 'X',\n chars: \"\\u24CD\\uFF38\\u1E8A\\u1E8C\",\n }, {\n base: 'Y',\n chars: \"\\u24CE\\uFF39\\u1EF2\\xDD\\u0176\\u1EF8\\u0232\\u1E8E\\u0178\\u1EF6\\u1EF4\\u01B3\\u024E\\u1EFE\",\n }, {\n base: 'Z',\n chars: \"\\u24CF\\uFF3A\\u0179\\u1E90\\u017B\\u017D\\u1E92\\u1E94\\u01B5\\u0224\\u2C7F\\u2C6B\\uA762\",\n }, {\n base: 'a',\n chars: \"\\u24D0\\uFF41\\u1E9A\\u00E0\\u00E1\\u00E2\\u1EA7\\u1EA5\\u1EAB\\u1EA9\\u00E3\\u0101\\u0103\\u1EB1\\u1EAF\\u1EB5\\u1EB3\\u0227\\u01E1\\u00E4\\u01DF\\u1EA3\\u00E5\\u01FB\\u01CE\\u0201\\u0203\\u1EA1\\u1EAD\\u1EB7\\u1E01\\u0105\\u2C65\\u0250\\u0251\",\n }, {\n base: 'aa',\n chars: \"\\uA733\",\n }, {\n base: 'ae',\n chars: \"\\u00E6\\u01FD\\u01E3\",\n }, {\n base: 'ao',\n chars: \"\\uA735\",\n }, {\n base: 'au',\n chars: \"\\uA737\",\n }, {\n base: 'av',\n chars: \"\\uA739\\uA73B\",\n }, {\n base: 'ay',\n chars: \"\\uA73D\",\n }, {\n base: 'b',\n chars: \"\\u24D1\\uFF42\\u1E03\\u1E05\\u1E07\\u0180\\u0183\\u0253\\u0182\",\n }, {\n base: 'c',\n chars: \"\\uFF43\\u24D2\\u0107\\u0109\\u010B\\u010D\\u00E7\\u1E09\\u0188\\u023C\\uA73F\\u2184\",\n }, {\n base: 'd',\n chars: \"\\u24D3\\uFF44\\u1E0B\\u010F\\u1E0D\\u1E11\\u1E13\\u1E0F\\u0111\\u018C\\u0256\\u0257\\u018B\\u13E7\\u0501\\uA7AA\",\n }, {\n base: 'dh',\n chars: \"\\u00F0\",\n }, {\n base: 'dz',\n chars: \"\\u01F3\\u01C6\",\n }, {\n base: 'e',\n chars: \"\\u24D4\\uFF45\\u00E8\\u00E9\\u00EA\\u1EC1\\u1EBF\\u1EC5\\u1EC3\\u1EBD\\u0113\\u1E15\\u1E17\\u0115\\u0117\\u00EB\\u1EBB\\u011B\\u0205\\u0207\\u1EB9\\u1EC7\\u0229\\u1E1D\\u0119\\u1E19\\u1E1B\\u0247\\u01DD\",\n }, {\n base: 'f',\n chars: \"\\u24D5\\uFF46\\u1E1F\\u0192\",\n }, {\n base: 'ff',\n chars: \"\\uFB00\",\n }, {\n base: 'fi',\n chars: \"\\uFB01\",\n }, {\n base: 'fl',\n chars: \"\\uFB02\",\n }, {\n base: 'ffi',\n chars: \"\\uFB03\",\n }, {\n base: 'ffl',\n chars: \"\\uFB04\",\n }, {\n base: 'g',\n chars: \"\\u24D6\\uFF47\\u01F5\\u011D\\u1E21\\u011F\\u0121\\u01E7\\u0123\\u01E5\\u0260\\uA7A1\\uA77F\\u1D79\",\n }, {\n base: 'h',\n chars: \"\\u24D7\\uFF48\\u0125\\u1E23\\u1E27\\u021F\\u1E25\\u1E29\\u1E2B\\u1E96\\u0127\\u2C68\\u2C76\\u0265\",\n }, {\n base: 'hv',\n chars: \"\\u0195\",\n }, {\n base: 'i',\n chars: \"\\u24D8\\uFF49\\xEC\\xED\\xEE\\u0129\\u012B\\u012D\\xEF\\u1E2F\\u1EC9\\u01D0\\u0209\\u020B\\u1ECB\\u012F\\u1E2D\\u0268\\u0131\",\n }, {\n base: 'j',\n chars: \"\\u24D9\\uFF4A\\u0135\\u01F0\\u0249\",\n }, {\n base: 'k',\n chars: \"\\u24DA\\uFF4B\\u1E31\\u01E9\\u1E33\\u0137\\u1E35\\u0199\\u2C6A\\uA741\\uA743\\uA745\\uA7A3\",\n }, {\n base: 'l',\n chars: \"\\u24DB\\uFF4C\\u0140\\u013A\\u013E\\u1E37\\u1E39\\u013C\\u1E3D\\u1E3B\\u017F\\u0142\\u019A\\u026B\\u2C61\\uA749\\uA781\\uA747\\u026D\",\n }, {\n base: 'lj',\n chars: \"\\u01C9\",\n }, {\n base: 'm',\n chars: \"\\u24DC\\uFF4D\\u1E3F\\u1E41\\u1E43\\u0271\\u026F\",\n }, {\n base: 'n',\n chars: \"\\u24DD\\uFF4E\\u01F9\\u0144\\xF1\\u1E45\\u0148\\u1E47\\u0146\\u1E4B\\u1E49\\u019E\\u0272\\u0149\\uA791\\uA7A5\\u043B\\u0509\",\n }, {\n base: 'nj',\n chars: \"\\u01CC\",\n }, {\n base: 'o',\n chars: \"\\u24DE\\uFF4F\\xF2\\xF3\\xF4\\u1ED3\\u1ED1\\u1ED7\\u1ED5\\xF5\\u1E4D\\u022D\\u1E4F\\u014D\\u1E51\\u1E53\\u014F\\u022F\\u0231\\xF6\\u022B\\u1ECF\\u0151\\u01D2\\u020D\\u020F\\u01A1\\u1EDD\\u1EDB\\u1EE1\\u1EDF\\u1EE3\\u1ECD\\u1ED9\\u01EB\\u01ED\\xF8\\u01FF\\uA74B\\uA74D\\u0275\\u0254\\u1D11\",\n }, {\n base: 'oe',\n chars: \"\\u0153\",\n }, {\n base: 'oi',\n chars: \"\\u01A3\",\n }, {\n base: 'oo',\n chars: \"\\uA74F\",\n }, {\n base: 'ou',\n chars: \"\\u0223\",\n }, {\n base: 'p',\n chars: \"\\u24DF\\uFF50\\u1E55\\u1E57\\u01A5\\u1D7D\\uA751\\uA753\\uA755\\u03C1\",\n }, {\n base: 'q',\n chars: \"\\u24E0\\uFF51\\u024B\\uA757\\uA759\",\n }, {\n base: 'r',\n chars: \"\\u24E1\\uFF52\\u0155\\u1E59\\u0159\\u0211\\u0213\\u1E5B\\u1E5D\\u0157\\u1E5F\\u024D\\u027D\\uA75B\\uA7A7\\uA783\",\n }, {\n base: 's',\n chars: \"\\u24E2\\uFF53\\u015B\\u1E65\\u015D\\u1E61\\u0161\\u1E67\\u1E63\\u1E69\\u0219\\u015F\\u023F\\uA7A9\\uA785\\u1E9B\\u0282\",\n }, {\n base: 'ss',\n chars: \"\\xDF\",\n }, {\n base: 't',\n chars: \"\\u24E3\\uFF54\\u1E6B\\u1E97\\u0165\\u1E6D\\u021B\\u0163\\u1E71\\u1E6F\\u0167\\u01AD\\u0288\\u2C66\\uA787\",\n }, {\n base: 'th',\n chars: \"\\u00FE\",\n }, {\n base: 'tz',\n chars: \"\\uA729\",\n }, {\n base: 'u',\n chars: \"\\u24E4\\uFF55\\xF9\\xFA\\xFB\\u0169\\u1E79\\u016B\\u1E7B\\u016D\\xFC\\u01DC\\u01D8\\u01D6\\u01DA\\u1EE7\\u016F\\u0171\\u01D4\\u0215\\u0217\\u01B0\\u1EEB\\u1EE9\\u1EEF\\u1EED\\u1EF1\\u1EE5\\u1E73\\u0173\\u1E77\\u1E75\\u0289\",\n }, {\n base: 'v',\n chars: \"\\u24E5\\uFF56\\u1E7D\\u1E7F\\u028B\\uA75F\\u028C\",\n }, {\n base: 'vy',\n chars: \"\\uA761\",\n }, {\n base: 'w',\n chars: \"\\u24E6\\uFF57\\u1E81\\u1E83\\u0175\\u1E87\\u1E85\\u1E98\\u1E89\\u2C73\",\n }, {\n base: 'x',\n chars: \"\\u24E7\\uFF58\\u1E8B\\u1E8D\",\n }, {\n base: 'y',\n chars: \"\\u24E8\\uFF59\\u1EF3\\xFD\\u0177\\u1EF9\\u0233\\u1E8F\\xFF\\u1EF7\\u1E99\\u1EF5\\u01B4\\u024F\\u1EFF\",\n }, {\n base: 'z',\n chars: \"\\u24E9\\uFF5A\\u017A\\u1E91\\u017C\\u017E\\u1E93\\u1E95\\u01B6\\u0225\\u0240\\u2C6C\\uA763\",\n }\n];\n\nvar diacriticsMap = {};\nfor (var i = 0; i < replacementList.length; i += 1) {\n var chars = replacementList[i].chars;\n for (var j = 0; j < chars.length; j += 1) {\n diacriticsMap[chars[j]] = replacementList[i].base;\n }\n}\n\nfunction removeDiacritics(str) {\n return str.replace(/[^\\u0000-\\u007e]/g, function(c) {\n return diacriticsMap[c] || c;\n });\n}\n\nexports.replacementList = replacementList;\nexports.diacriticsMap = diacriticsMap;\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.isArabic = isArabic;\nexports.isMath = isMath;\nconst arabicBlocks = [\n [0x0600, 0x06FF], // Arabic https://www.unicode.org/charts/PDF/U0600.pdf\n [0x0750, 0x077F], // supplement https://www.unicode.org/charts/PDF/U0750.pdf\n [0x08A0, 0x08FF], // Extended-A https://www.unicode.org/charts/PDF/U08A0.pdf\n [0xFB50, 0xFDFF], // Presentation Forms-A https://www.unicode.org/charts/PDF/UFB50.pdf\n [0xFE70, 0xFEFF], // Presentation Forms-B https://www.unicode.org/charts/PDF/UFE70.pdf\n [0x10E60, 0x10E7F], // Rumi numerals https://www.unicode.org/charts/PDF/U10E60.pdf\n [0x1EC70, 0x1ECBF], // Indic Siyaq numerals https://www.unicode.org/charts/PDF/U1EC70.pdf\n [0x1EE00, 0x1EEFF] // Mathematical Alphabetic symbols https://www.unicode.org/charts/PDF/U1EE00.pdf\n];\nfunction isArabic(char) {\n if (char.length > 1) {\n // allow the newer chars?\n throw new Error('isArabic works on only one-character strings');\n }\n let code = char.charCodeAt(0);\n for (let i = 0; i < arabicBlocks.length; i++) {\n let block = arabicBlocks[i];\n if (code >= block[0] && code <= block[1]) {\n return true;\n }\n }\n return false;\n}\nfunction isMath(char) {\n if (char.length > 2) {\n // allow the newer chars?\n throw new Error('isMath works on only one-character strings');\n }\n let code = char.charCodeAt(0);\n return ((code >= 0x660 && code <= 0x66C) || (code >= 0x6F0 && code <= 0x6F9));\n}\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst arabicReference = {\n \"alef\": {\n \"normal\": [\n \"\\u0627\"\n ],\n \"madda_above\": {\n \"normal\": [\n \"\\u0627\\u0653\",\n \"\\u0622\"\n ],\n \"isolated\": \"\\uFE81\",\n \"final\": \"\\uFE82\"\n },\n \"hamza_above\": {\n \"normal\": [\n \"\\u0627\\u0654\",\n \"\\u0623\"\n ],\n \"isolated\": \"\\uFE83\",\n \"final\": \"\\uFE84\"\n },\n \"hamza_below\": {\n \"normal\": [\n \"\\u0627\\u0655\",\n \"\\u0625\"\n ],\n \"isolated\": \"\\uFE87\",\n \"final\": \"\\uFE88\"\n },\n \"wasla\": {\n \"normal\": \"\\u0671\",\n \"isolated\": \"\\uFB50\",\n \"final\": \"\\uFB51\"\n },\n \"wavy_hamza_above\": [\n \"\\u0672\"\n ],\n \"wavy_hamza_below\": [\n \"\\u0627\\u065F\",\n \"\\u0673\"\n ],\n \"high_hamza\": [\n \"\\u0675\",\n \"\\u0627\\u0674\"\n ],\n \"indic_two_above\": [\n \"\\u0773\"\n ],\n \"indic_three_above\": [\n \"\\u0774\"\n ],\n \"fathatan\": {\n \"normal\": [\n \"\\u0627\\u064B\"\n ],\n \"final\": \"\\uFD3C\",\n \"isolated\": \"\\uFD3D\"\n },\n \"isolated\": \"\\uFE8D\",\n \"final\": \"\\uFE8E\"\n },\n \"beh\": {\n \"normal\": [\n \"\\u0628\"\n ],\n \"dotless\": [\n \"\\u066E\"\n ],\n \"three_dots_horizontally_below\": [\n \"\\u0750\"\n ],\n \"dot_below_three_dots_above\": [\n \"\\u0751\"\n ],\n \"three_dots_pointing_upwards_below\": [\n \"\\u0752\"\n ],\n \"three_dots_pointing_upwards_below_two_dots_above\": [\n \"\\u0753\"\n ],\n \"two_dots_below_dot_above\": [\n \"\\u0754\"\n ],\n \"inverted_small_v_below\": [\n \"\\u0755\"\n ],\n \"small_v\": [\n \"\\u0756\"\n ],\n \"small_v_below\": [\n \"\\u08A0\"\n ],\n \"hamza_above\": [\n \"\\u08A1\"\n ],\n \"small_meem_above\": [\n \"\\u08B6\"\n ],\n \"isolated\": \"\\uFE8F\",\n \"final\": \"\\uFE90\",\n \"initial\": \"\\uFE91\",\n \"medial\": \"\\uFE92\"\n },\n \"teh marbuta\": {\n \"normal\": [\n \"\\u0629\"\n ],\n \"isolated\": \"\\uFE93\",\n \"final\": \"\\uFE94\"\n },\n \"teh\": {\n \"normal\": [\n \"\\u062A\"\n ],\n \"ring\": [\n \"\\u067C\"\n ],\n \"three_dots_above_downwards\": [\n \"\\u067D\"\n ],\n \"small_teh_above\": [\n \"\\u08B8\"\n ],\n \"isolated\": \"\\uFE95\",\n \"final\": \"\\uFE96\",\n \"initial\": \"\\uFE97\",\n \"medial\": \"\\uFE98\"\n },\n \"theh\": {\n \"normal\": [\n \"\\u062B\"\n ],\n \"isolated\": \"\\uFE99\",\n \"final\": \"\\uFE9A\",\n \"initial\": \"\\uFE9B\",\n \"medial\": \"\\uFE9C\"\n },\n \"jeem\": {\n \"normal\": [\n \"\\u062C\"\n ],\n \"two_dots_above\": [\n \"\\u08A2\"\n ],\n \"isolated\": \"\\uFE9D\",\n \"final\": \"\\uFE9E\",\n \"initial\": \"\\uFE9F\",\n \"medial\": \"\\uFEA0\"\n },\n \"hah\": {\n \"normal\": [\n \"\\u062D\"\n ],\n \"hamza_above\": [\n \"\\u0681\"\n ],\n \"two_dots_vertical_above\": [\n \"\\u0682\"\n ],\n \"three_dots_above\": [\n \"\\u0685\"\n ],\n \"two_dots_above\": [\n \"\\u0757\"\n ],\n \"three_dots_pointing_upwards_below\": [\n \"\\u0758\"\n ],\n \"small_tah_below\": [\n \"\\u076E\"\n ],\n \"small_tah_two_dots\": [\n \"\\u076F\"\n ],\n \"small_tah_above\": [\n \"\\u0772\"\n ],\n \"indic_four_below\": [\n \"\\u077C\"\n ],\n \"isolated\": \"\\uFEA1\",\n \"final\": \"\\uFEA2\",\n \"initial\": \"\\uFEA3\",\n \"medial\": \"\\uFEA4\"\n },\n \"khah\": {\n \"normal\": [\n \"\\u062E\"\n ],\n \"isolated\": \"\\uFEA5\",\n \"final\": \"\\uFEA6\",\n \"initial\": \"\\uFEA7\",\n \"medial\": \"\\uFEA8\"\n },\n \"dal\": {\n \"normal\": [\n \"\\u062F\"\n ],\n \"ring\": [\n \"\\u0689\"\n ],\n \"dot_below\": [\n \"\\u068A\"\n ],\n \"dot_below_small_tah\": [\n \"\\u068B\"\n ],\n \"three_dots_above_downwards\": [\n \"\\u068F\"\n ],\n \"four_dots_above\": [\n \"\\u0690\"\n ],\n \"inverted_v\": [\n \"\\u06EE\"\n ],\n \"two_dots_vertically_below_small_tah\": [\n \"\\u0759\"\n ],\n \"inverted_small_v_below\": [\n \"\\u075A\"\n ],\n \"three_dots_below\": [\n \"\\u08AE\"\n ],\n \"isolated\": \"\\uFEA9\",\n \"final\": \"\\uFEAA\"\n },\n \"thal\": {\n \"normal\": [\n \"\\u0630\"\n ],\n \"isolated\": \"\\uFEAB\",\n \"final\": \"\\uFEAC\"\n },\n \"reh\": {\n \"normal\": [\n \"\\u0631\"\n ],\n \"small_v\": [\n \"\\u0692\"\n ],\n \"ring\": [\n \"\\u0693\"\n ],\n \"dot_below\": [\n \"\\u0694\"\n ],\n \"small_v_below\": [\n \"\\u0695\"\n ],\n \"dot_below_dot_above\": [\n \"\\u0696\"\n ],\n \"two_dots_above\": [\n \"\\u0697\"\n ],\n \"four_dots_above\": [\n \"\\u0699\"\n ],\n \"inverted_v\": [\n \"\\u06EF\"\n ],\n \"stroke\": [\n \"\\u075B\"\n ],\n \"two_dots_vertically_above\": [\n \"\\u076B\"\n ],\n \"hamza_above\": [\n \"\\u076C\"\n ],\n \"small_tah_two_dots\": [\n \"\\u0771\"\n ],\n \"loop\": [\n \"\\u08AA\"\n ],\n \"small_noon_above\": [\n \"\\u08B9\"\n ],\n \"isolated\": \"\\uFEAD\",\n \"final\": \"\\uFEAE\"\n },\n \"zain\": {\n \"normal\": [\n \"\\u0632\"\n ],\n \"inverted_v_above\": [\n \"\\u08B2\"\n ],\n \"isolated\": \"\\uFEAF\",\n \"final\": \"\\uFEB0\"\n },\n \"seen\": {\n \"normal\": [\n \"\\u0633\"\n ],\n \"dot_below_dot_above\": [\n \"\\u069A\"\n ],\n \"three_dots_below\": [\n \"\\u069B\"\n ],\n \"three_dots_below_three_dots_above\": [\n \"\\u069C\"\n ],\n \"four_dots_above\": [\n \"\\u075C\"\n ],\n \"two_dots_vertically_above\": [\n \"\\u076D\"\n ],\n \"small_tah_two_dots\": [\n \"\\u0770\"\n ],\n \"indic_four_above\": [\n \"\\u077D\"\n ],\n \"inverted_v\": [\n \"\\u077E\"\n ],\n \"isolated\": \"\\uFEB1\",\n \"final\": \"\\uFEB2\",\n \"initial\": \"\\uFEB3\",\n \"medial\": \"\\uFEB4\"\n },\n \"sheen\": {\n \"normal\": [\n \"\\u0634\"\n ],\n \"dot_below\": [\n \"\\u06FA\"\n ],\n \"isolated\": \"\\uFEB5\",\n \"final\": \"\\uFEB6\",\n \"initial\": \"\\uFEB7\",\n \"medial\": \"\\uFEB8\"\n },\n \"sad\": {\n \"normal\": [\n \"\\u0635\"\n ],\n \"two_dots_below\": [\n \"\\u069D\"\n ],\n \"three_dots_above\": [\n \"\\u069E\"\n ],\n \"three_dots_below\": [\n \"\\u08AF\"\n ],\n \"isolated\": \"\\uFEB9\",\n \"final\": \"\\uFEBA\",\n \"initial\": \"\\uFEBB\",\n \"medial\": \"\\uFEBC\"\n },\n \"dad\": {\n \"normal\": [\n \"\\u0636\"\n ],\n \"dot_below\": [\n \"\\u06FB\"\n ],\n \"isolated\": \"\\uFEBD\",\n \"final\": \"\\uFEBE\",\n \"initial\": \"\\uFEBF\",\n \"medial\": \"\\uFEC0\"\n },\n \"tah\": {\n \"normal\": [\n \"\\u0637\"\n ],\n \"three_dots_above\": [\n \"\\u069F\"\n ],\n \"two_dots_above\": [\n \"\\u08A3\"\n ],\n \"isolated\": \"\\uFEC1\",\n \"final\": \"\\uFEC2\",\n \"initial\": \"\\uFEC3\",\n \"medial\": \"\\uFEC4\"\n },\n \"zah\": {\n \"normal\": [\n \"\\u0638\"\n ],\n \"isolated\": \"\\uFEC5\",\n \"final\": \"\\uFEC6\",\n \"initial\": \"\\uFEC7\",\n \"medial\": \"\\uFEC8\"\n },\n \"ain\": {\n \"normal\": [\n \"\\u0639\"\n ],\n \"three_dots_above\": [\n \"\\u06A0\"\n ],\n \"two_dots_above\": [\n \"\\u075D\"\n ],\n \"three_dots_pointing_downwards_above\": [\n \"\\u075E\"\n ],\n \"two_dots_vertically_above\": [\n \"\\u075F\"\n ],\n \"three_dots_below\": [\n \"\\u08B3\"\n ],\n \"isolated\": \"\\uFEC9\",\n \"final\": \"\\uFECA\",\n \"initial\": \"\\uFECB\",\n \"medial\": \"\\uFECC\"\n },\n \"ghain\": {\n \"normal\": [\n \"\\u063A\"\n ],\n \"dot_below\": [\n \"\\u06FC\"\n ],\n \"isolated\": \"\\uFECD\",\n \"final\": \"\\uFECE\",\n \"initial\": \"\\uFECF\",\n \"medial\": \"\\uFED0\"\n },\n \"feh\": {\n \"normal\": [\n \"\\u0641\"\n ],\n \"dotless\": [\n \"\\u06A1\"\n ],\n \"dot_moved_below\": [\n \"\\u06A2\"\n ],\n \"dot_below\": [\n \"\\u06A3\"\n ],\n \"three_dots_below\": [\n \"\\u06A5\"\n ],\n \"two_dots_below\": [\n \"\\u0760\"\n ],\n \"three_dots_pointing_upwards_below\": [\n \"\\u0761\"\n ],\n \"dot_below_three_dots_above\": [\n \"\\u08A4\"\n ],\n \"isolated\": \"\\uFED1\",\n \"final\": \"\\uFED2\",\n \"initial\": \"\\uFED3\",\n \"medial\": \"\\uFED4\"\n },\n \"qaf\": {\n \"normal\": [\n \"\\u0642\"\n ],\n \"dotless\": [\n \"\\u066F\"\n ],\n \"dot_above\": [\n \"\\u06A7\"\n ],\n \"three_dots_above\": [\n \"\\u06A8\"\n ],\n \"dot_below\": [\n \"\\u08A5\"\n ],\n \"isolated\": \"\\uFED5\",\n \"final\": \"\\uFED6\",\n \"initial\": \"\\uFED7\",\n \"medial\": \"\\uFED8\"\n },\n \"kaf\": {\n \"normal\": [\n \"\\u0643\"\n ],\n \"swash\": [\n \"\\u06AA\"\n ],\n \"ring\": [\n \"\\u06AB\"\n ],\n \"dot_above\": [\n \"\\u06AC\"\n ],\n \"three_dots_below\": [\n \"\\u06AE\"\n ],\n \"two_dots_above\": [\n \"\\u077F\"\n ],\n \"dot_below\": [\n \"\\u08B4\"\n ],\n \"isolated\": \"\\uFED9\",\n \"final\": \"\\uFEDA\",\n \"initial\": \"\\uFEDB\",\n \"medial\": \"\\uFEDC\"\n },\n \"lam\": {\n \"normal\": [\n \"\\u0644\"\n ],\n \"small_v\": [\n \"\\u06B5\"\n ],\n \"dot_above\": [\n \"\\u06B6\"\n ],\n \"three_dots_above\": [\n \"\\u06B7\"\n ],\n \"three_dots_below\": [\n \"\\u06B8\"\n ],\n \"bar\": [\n \"\\u076A\"\n ],\n \"double_bar\": [\n \"\\u08A6\"\n ],\n \"isolated\": \"\\uFEDD\",\n \"final\": \"\\uFEDE\",\n \"initial\": \"\\uFEDF\",\n \"medial\": \"\\uFEE0\"\n },\n \"meem\": {\n \"normal\": [\n \"\\u0645\"\n ],\n \"dot_above\": [\n \"\\u0765\"\n ],\n \"dot_below\": [\n \"\\u0766\"\n ],\n \"three_dots_above\": [\n \"\\u08A7\"\n ],\n \"isolated\": \"\\uFEE1\",\n \"final\": \"\\uFEE2\",\n \"initial\": \"\\uFEE3\",\n \"medial\": \"\\uFEE4\"\n },\n \"noon\": {\n \"normal\": [\n \"\\u0646\"\n ],\n \"dot_below\": [\n \"\\u06B9\"\n ],\n \"ring\": [\n \"\\u06BC\"\n ],\n \"three_dots_above\": [\n \"\\u06BD\"\n ],\n \"two_dots_below\": [\n \"\\u0767\"\n ],\n \"small_tah\": [\n \"\\u0768\"\n ],\n \"small_v\": [\n \"\\u0769\"\n ],\n \"isolated\": \"\\uFEE5\",\n \"final\": \"\\uFEE6\",\n \"initial\": \"\\uFEE7\",\n \"medial\": \"\\uFEE8\"\n },\n \"heh\": {\n \"normal\": [\n \"\\u0647\"\n ],\n \"isolated\": \"\\uFEE9\",\n \"final\": \"\\uFEEA\",\n \"initial\": \"\\uFEEB\",\n \"medial\": \"\\uFEEC\"\n },\n \"waw\": {\n \"normal\": [\n \"\\u0648\"\n ],\n \"hamza_above\": {\n \"normal\": [\n \"\\u0624\",\n \"\\u0648\\u0654\"\n ],\n \"isolated\": \"\\uFE85\",\n \"final\": \"\\uFE86\"\n },\n \"high_hamza\": [\n \"\\u0676\",\n \"\\u0648\\u0674\"\n ],\n \"ring\": [\n \"\\u06C4\"\n ],\n \"two_dots_above\": [\n \"\\u06CA\"\n ],\n \"dot_above\": [\n \"\\u06CF\"\n ],\n \"indic_two_above\": [\n \"\\u0778\"\n ],\n \"indic_three_above\": [\n \"\\u0779\"\n ],\n \"dot_within\": [\n \"\\u08AB\"\n ],\n \"isolated\": \"\\uFEED\",\n \"final\": \"\\uFEEE\"\n },\n \"alef_maksura\": {\n \"normal\": [\n \"\\u0649\"\n ],\n \"hamza_above\": [\n \"\\u0626\",\n \"\\u064A\\u0654\"\n ],\n \"initial\": \"\\uFBE8\",\n \"medial\": \"\\uFBE9\",\n \"isolated\": \"\\uFEEF\",\n \"final\": \"\\uFEF0\"\n },\n \"yeh\": {\n \"normal\": [\n \"\\u064A\"\n ],\n \"hamza_above\": {\n \"normal\": [\n \"\\u0626\",\n \"\\u0649\\u0654\"\n ],\n \"isolated\": \"\\uFE89\",\n \"final\": \"\\uFE8A\",\n \"initial\": \"\\uFE8B\",\n \"medial\": \"\\uFE8C\"\n },\n \"two_dots_below_hamza_above\": [\n \"\\u08A8\"\n ],\n \"high_hamza\": [\n \"\\u0678\",\n \"\\u064A\\u0674\"\n ],\n \"tail\": [\n \"\\u06CD\"\n ],\n \"small_v\": [\n \"\\u06CE\"\n ],\n \"three_dots_below\": [\n \"\\u06D1\"\n ],\n \"two_dots_below_dot_above\": [\n \"\\u08A9\"\n ],\n \"two_dots_below_small_noon_above\": [\n \"\\u08BA\"\n ],\n \"isolated\": \"\\uFEF1\",\n \"final\": \"\\uFEF2\",\n \"initial\": \"\\uFEF3\",\n \"medial\": \"\\uFEF4\"\n },\n \"tteh\": {\n \"normal\": [\n \"\\u0679\"\n ],\n \"isolated\": \"\\uFB66\",\n \"final\": \"\\uFB67\",\n \"initial\": \"\\uFB68\",\n \"medial\": \"\\uFB69\"\n },\n \"tteheh\": {\n \"normal\": [\n \"\\u067A\"\n ],\n \"isolated\": \"\\uFB5E\",\n \"final\": \"\\uFB5F\",\n \"initial\": \"\\uFB60\",\n \"medial\": \"\\uFB61\"\n },\n \"beeh\": {\n \"normal\": [\n \"\\u067B\"\n ],\n \"isolated\": \"\\uFB52\",\n \"final\": \"\\uFB53\",\n \"initial\": \"\\uFB54\",\n \"medial\": \"\\uFB55\"\n },\n \"peh\": {\n \"normal\": [\n \"\\u067E\"\n ],\n \"small_meem_above\": [\n \"\\u08B7\"\n ],\n \"isolated\": \"\\uFB56\",\n \"final\": \"\\uFB57\",\n \"initial\": \"\\uFB58\",\n \"medial\": \"\\uFB59\"\n },\n \"teheh\": {\n \"normal\": [\n \"\\u067F\"\n ],\n \"isolated\": \"\\uFB62\",\n \"final\": \"\\uFB63\",\n \"initial\": \"\\uFB64\",\n \"medial\": \"\\uFB65\"\n },\n \"beheh\": {\n \"normal\": [\n \"\\u0680\"\n ],\n \"isolated\": \"\\uFB5A\",\n \"final\": \"\\uFB5B\",\n \"initial\": \"\\uFB5C\",\n \"medial\": \"\\uFB5D\"\n },\n \"nyeh\": {\n \"normal\": [\n \"\\u0683\"\n ],\n \"isolated\": \"\\uFB76\",\n \"final\": \"\\uFB77\",\n \"initial\": \"\\uFB78\",\n \"medial\": \"\\uFB79\"\n },\n \"dyeh\": {\n \"normal\": [\n \"\\u0684\"\n ],\n \"isolated\": \"\\uFB72\",\n \"final\": \"\\uFB73\",\n \"initial\": \"\\uFB74\",\n \"medial\": \"\\uFB75\"\n },\n \"tcheh\": {\n \"normal\": [\n \"\\u0686\"\n ],\n \"dot_above\": [\n \"\\u06BF\"\n ],\n \"isolated\": \"\\uFB7A\",\n \"final\": \"\\uFB7B\",\n \"initial\": \"\\uFB7C\",\n \"medial\": \"\\uFB7D\"\n },\n \"tcheheh\": {\n \"normal\": [\n \"\\u0687\"\n ],\n \"isolated\": \"\\uFB7E\",\n \"final\": \"\\uFB7F\",\n \"initial\": \"\\uFB80\",\n \"medial\": \"\\uFB81\"\n },\n \"ddal\": {\n \"normal\": [\n \"\\u0688\"\n ],\n \"isolated\": \"\\uFB88\",\n \"final\": \"\\uFB89\"\n },\n \"dahal\": {\n \"normal\": [\n \"\\u068C\"\n ],\n \"isolated\": \"\\uFB84\",\n \"final\": \"\\uFB85\"\n },\n \"ddahal\": {\n \"normal\": [\n \"\\u068D\"\n ],\n \"isolated\": \"\\uFB82\",\n \"final\": \"\\uFB83\"\n },\n \"dul\": {\n \"normal\": [\n \"\\u068F\",\n \"\\u068E\"\n ],\n \"isolated\": \"\\uFB86\",\n \"final\": \"\\uFB87\"\n },\n \"rreh\": {\n \"normal\": [\n \"\\u0691\"\n ],\n \"isolated\": \"\\uFB8C\",\n \"final\": \"\\uFB8D\"\n },\n \"jeh\": {\n \"normal\": [\n \"\\u0698\"\n ],\n \"isolated\": \"\\uFB8A\",\n \"final\": \"\\uFB8B\"\n },\n \"veh\": {\n \"normal\": [\n \"\\u06A4\"\n ],\n \"isolated\": \"\\uFB6A\",\n \"final\": \"\\uFB6B\",\n \"initial\": \"\\uFB6C\",\n \"medial\": \"\\uFB6D\"\n },\n \"peheh\": {\n \"normal\": [\n \"\\u06A6\"\n ],\n \"isolated\": \"\\uFB6E\",\n \"final\": \"\\uFB6F\",\n \"initial\": \"\\uFB70\",\n \"medial\": \"\\uFB71\"\n },\n \"keheh\": {\n \"normal\": [\n \"\\u06A9\"\n ],\n \"dot_above\": [\n \"\\u0762\"\n ],\n \"three_dots_above\": [\n \"\\u0763\"\n ],\n \"three_dots_pointing_upwards_below\": [\n \"\\u0764\"\n ],\n \"isolated\": \"\\uFB8E\",\n \"final\": \"\\uFB8F\",\n \"initial\": \"\\uFB90\",\n \"medial\": \"\\uFB91\"\n },\n \"ng\": {\n \"normal\": [\n \"\\u06AD\"\n ],\n \"isolated\": \"\\uFBD3\",\n \"final\": \"\\uFBD4\",\n \"initial\": \"\\uFBD5\",\n \"medial\": \"\\uFBD6\"\n },\n \"gaf\": {\n \"normal\": [\n \"\\u06AF\"\n ],\n \"ring\": [\n \"\\u06B0\"\n ],\n \"two_dots_below\": [\n \"\\u06B2\"\n ],\n \"three_dots_above\": [\n \"\\u06B4\"\n ],\n \"inverted_stroke\": [\n \"\\u08B0\"\n ],\n \"isolated\": \"\\uFB92\",\n \"final\": \"\\uFB93\",\n \"initial\": \"\\uFB94\",\n \"medial\": \"\\uFB95\"\n },\n \"ngoeh\": {\n \"normal\": [\n \"\\u06B1\"\n ],\n \"isolated\": \"\\uFB9A\",\n \"final\": \"\\uFB9B\",\n \"initial\": \"\\uFB9C\",\n \"medial\": \"\\uFB9D\"\n },\n \"gueh\": {\n \"normal\": [\n \"\\u06B3\"\n ],\n \"isolated\": \"\\uFB96\",\n \"final\": \"\\uFB97\",\n \"initial\": \"\\uFB98\",\n \"medial\": \"\\uFB99\"\n },\n \"noon ghunna\": {\n \"normal\": [\n \"\\u06BA\"\n ],\n \"isolated\": \"\\uFB9E\",\n \"final\": \"\\uFB9F\"\n },\n \"rnoon\": {\n \"normal\": [\n \"\\u06BB\"\n ],\n \"isolated\": \"\\uFBA0\",\n \"final\": \"\\uFBA1\",\n \"initial\": \"\\uFBA2\",\n \"medial\": \"\\uFBA3\"\n },\n \"heh doachashmee\": {\n \"normal\": [\n \"\\u06BE\"\n ],\n \"isolated\": \"\\uFBAA\",\n \"final\": \"\\uFBAB\",\n \"initial\": \"\\uFBAC\",\n \"medial\": \"\\uFBAD\"\n },\n \"heh goal\": {\n \"normal\": [\n \"\\u06C1\"\n ],\n \"hamza_above\": [\n \"\\u06C1\\u0654\",\n \"\\u06C2\"\n ],\n \"isolated\": \"\\uFBA6\",\n \"final\": \"\\uFBA7\",\n \"initial\": \"\\uFBA8\",\n \"medial\": \"\\uFBA9\"\n },\n \"teh marbuta goal\": {\n \"normal\": [\n \"\\u06C3\"\n ]\n },\n \"kirghiz oe\": {\n \"normal\": [\n \"\\u06C5\"\n ],\n \"isolated\": \"\\uFBE0\",\n \"final\": \"\\uFBE1\"\n },\n \"oe\": {\n \"normal\": [\n \"\\u06C6\"\n ],\n \"isolated\": \"\\uFBD9\",\n \"final\": \"\\uFBDA\"\n },\n \"u\": {\n \"normal\": [\n \"\\u06C7\"\n ],\n \"hamza_above\": {\n \"normal\": [\n \"\\u0677\",\n \"\\u06C7\\u0674\"\n ],\n \"isolated\": \"\\uFBDD\"\n },\n \"isolated\": \"\\uFBD7\",\n \"final\": \"\\uFBD8\"\n },\n \"yu\": {\n \"normal\": [\n \"\\u06C8\"\n ],\n \"isolated\": \"\\uFBDB\",\n \"final\": \"\\uFBDC\"\n },\n \"kirghiz yu\": {\n \"normal\": [\n \"\\u06C9\"\n ],\n \"isolated\": \"\\uFBE2\",\n \"final\": \"\\uFBE3\"\n },\n \"ve\": {\n \"normal\": [\n \"\\u06CB\"\n ],\n \"isolated\": \"\\uFBDE\",\n \"final\": \"\\uFBDF\"\n },\n \"farsi yeh\": {\n \"normal\": [\n \"\\u06CC\"\n ],\n \"indic_two_above\": [\n \"\\u0775\"\n ],\n \"indic_three_above\": [\n \"\\u0776\"\n ],\n \"indic_four_above\": [\n \"\\u0777\"\n ],\n \"isolated\": \"\\uFBFC\",\n \"final\": \"\\uFBFD\",\n \"initial\": \"\\uFBFE\",\n \"medial\": \"\\uFBFF\"\n },\n \"e\": {\n \"normal\": [\n \"\\u06D0\"\n ],\n \"isolated\": \"\\uFBE4\",\n \"final\": \"\\uFBE5\",\n \"initial\": \"\\uFBE6\",\n \"medial\": \"\\uFBE7\"\n },\n \"yeh barree\": {\n \"normal\": [\n \"\\u06D2\"\n ],\n \"hamza_above\": {\n \"normal\": [\n \"\\u06D2\\u0654\",\n \"\\u06D3\"\n ],\n \"isolated\": \"\\uFBB0\",\n \"final\": \"\\uFBB1\"\n },\n \"indic_two_above\": [\n \"\\u077A\"\n ],\n \"indic_three_above\": [\n \"\\u077B\"\n ],\n \"isolated\": \"\\uFBAE\",\n \"final\": \"\\uFBAF\"\n },\n \"ae\": {\n \"normal\": [\n \"\\u06D5\"\n ],\n \"isolated\": \"\\u06D5\",\n \"final\": \"\\uFEEA\",\n \"yeh_above\": {\n \"normal\": [\n \"\\u06C0\",\n \"\\u06D5\\u0654\"\n ],\n \"isolated\": \"\\uFBA4\",\n \"final\": \"\\uFBA5\"\n }\n },\n \"rohingya yeh\": {\n \"normal\": [\n \"\\u08AC\"\n ]\n },\n \"low alef\": {\n \"normal\": [\n \"\\u08AD\"\n ]\n },\n \"straight waw\": {\n \"normal\": [\n \"\\u08B1\"\n ]\n },\n \"african feh\": {\n \"normal\": [\n \"\\u08BB\"\n ]\n },\n \"african qaf\": {\n \"normal\": [\n \"\\u08BC\"\n ]\n },\n \"african noon\": {\n \"normal\": [\n \"\\u08BD\"\n ]\n }\n};\nexports.default = arabicReference;\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst ligatureReference = {\n \"\\u0626\\u0627\": {\n \"isolated\": \"\\uFBEA\",\n \"final\": \"\\uFBEB\"\n },\n \"\\u0626\\u06D5\": {\n \"isolated\": \"\\uFBEC\",\n \"final\": \"\\uFBED\"\n },\n \"\\u0626\\u0648\": {\n \"isolated\": \"\\uFBEE\",\n \"final\": \"\\uFBEF\"\n },\n \"\\u0626\\u06C7\": {\n \"isolated\": \"\\uFBF0\",\n \"final\": \"\\uFBF1\"\n },\n \"\\u0626\\u06C6\": {\n \"isolated\": \"\\uFBF2\",\n \"final\": \"\\uFBF3\"\n },\n \"\\u0626\\u06C8\": {\n \"isolated\": \"\\uFBF4\",\n \"final\": \"\\uFBF5\"\n },\n \"\\u0626\\u06D0\": {\n \"isolated\": \"\\uFBF6\",\n \"final\": \"\\uFBF7\",\n \"initial\": \"\\uFBF8\"\n },\n \"\\u0626\\u0649\": {\n \"uighur_kirghiz\": {\n \"isolated\": \"\\uFBF9\",\n \"final\": \"\\uFBFA\",\n \"initial\": \"\\uFBFB\"\n },\n \"isolated\": \"\\uFC03\",\n \"final\": \"\\uFC68\"\n },\n \"\\u0626\\u062C\": {\n \"isolated\": \"\\uFC00\",\n \"initial\": \"\\uFC97\"\n },\n \"\\u0626\\u062D\": {\n \"isolated\": \"\\uFC01\",\n \"initial\": \"\\uFC98\"\n },\n \"\\u0626\\u0645\": {\n \"isolated\": \"\\uFC02\",\n \"final\": \"\\uFC66\",\n \"initial\": \"\\uFC9A\",\n \"medial\": \"\\uFCDF\"\n },\n \"\\u0626\\u064A\": {\n \"isolated\": \"\\uFC04\",\n \"final\": \"\\uFC69\"\n },\n \"\\u0628\\u062C\": {\n \"isolated\": \"\\uFC05\",\n \"initial\": \"\\uFC9C\"\n },\n \"\\u0628\\u062D\": {\n \"isolated\": \"\\uFC06\",\n \"initial\": \"\\uFC9D\"\n },\n \"\\u0628\\u062E\": {\n \"isolated\": \"\\uFC07\",\n \"initial\": \"\\uFC9E\"\n },\n \"\\u0628\\u0645\": {\n \"isolated\": \"\\uFC08\",\n \"final\": \"\\uFC6C\",\n \"initial\": \"\\uFC9F\",\n \"medial\": \"\\uFCE1\"\n },\n \"\\u0628\\u0649\": {\n \"isolated\": \"\\uFC09\",\n \"final\": \"\\uFC6E\"\n },\n \"\\u0628\\u064A\": {\n \"isolated\": \"\\uFC0A\",\n \"final\": \"\\uFC6F\"\n },\n \"\\u062A\\u062C\": {\n \"isolated\": \"\\uFC0B\",\n \"initial\": \"\\uFCA1\"\n },\n \"\\u062A\\u062D\": {\n \"isolated\": \"\\uFC0C\",\n \"initial\": \"\\uFCA2\"\n },\n \"\\u062A\\u062E\": {\n \"isolated\": \"\\uFC0D\",\n \"initial\": \"\\uFCA3\"\n },\n \"\\u062A\\u0645\": {\n \"isolated\": \"\\uFC0E\",\n \"final\": \"\\uFC72\",\n \"initial\": \"\\uFCA4\",\n \"medial\": \"\\uFCE3\"\n },\n \"\\u062A\\u0649\": {\n \"isolated\": \"\\uFC0F\",\n \"final\": \"\\uFC74\"\n },\n \"\\u062A\\u064A\": {\n \"isolated\": \"\\uFC10\",\n \"final\": \"\\uFC75\"\n },\n \"\\u062B\\u062C\": {\n \"isolated\": \"\\uFC11\"\n },\n \"\\u062B\\u0645\": {\n \"isolated\": \"\\uFC12\",\n \"final\": \"\\uFC78\",\n \"initial\": \"\\uFCA6\",\n \"medial\": \"\\uFCE5\"\n },\n \"\\u062B\\u0649\": {\n \"isolated\": \"\\uFC13\",\n \"final\": \"\\uFC7A\"\n },\n \"\\u062B\\u0648\": {\n \"isolated\": \"\\uFC14\"\n },\n \"\\u062C\\u062D\": {\n \"isolated\": \"\\uFC15\",\n \"initial\": \"\\uFCA7\"\n },\n \"\\u062C\\u0645\": {\n \"isolated\": \"\\uFC16\",\n \"initial\": \"\\uFCA8\"\n },\n \"\\u062D\\u062C\": {\n \"isolated\": \"\\uFC17\",\n \"initial\": \"\\uFCA9\"\n },\n \"\\u062D\\u0645\": {\n \"isolated\": \"\\uFC18\",\n \"initial\": \"\\uFCAA\"\n },\n \"\\u062E\\u062C\": {\n \"isolated\": \"\\uFC19\",\n \"initial\": \"\\uFCAB\"\n },\n \"\\u062E\\u062D\": {\n \"isolated\": \"\\uFC1A\"\n },\n \"\\u062E\\u0645\": {\n \"isolated\": \"\\uFC1B\",\n \"initial\": \"\\uFCAC\"\n },\n \"\\u0633\\u062C\": {\n \"isolated\": \"\\uFC1C\",\n \"initial\": \"\\uFCAD\",\n \"medial\": \"\\uFD34\"\n },\n \"\\u0633\\u062D\": {\n \"isolated\": \"\\uFC1D\",\n \"initial\": \"\\uFCAE\",\n \"medial\": \"\\uFD35\"\n },\n \"\\u0633\\u062E\": {\n \"isolated\": \"\\uFC1E\",\n \"initial\": \"\\uFCAF\",\n \"medial\": \"\\uFD36\"\n },\n \"\\u0633\\u0645\": {\n \"isolated\": \"\\uFC1F\",\n \"initial\": \"\\uFCB0\",\n \"medial\": \"\\uFCE7\"\n },\n \"\\u0635\\u062D\": {\n \"isolated\": \"\\uFC20\",\n \"initial\": \"\\uFCB1\"\n },\n \"\\u0635\\u0645\": {\n \"isolated\": \"\\uFC21\",\n \"initial\": \"\\uFCB3\"\n },\n \"\\u0636\\u062C\": {\n \"isolated\": \"\\uFC22\",\n \"initial\": \"\\uFCB4\"\n },\n \"\\u0636\\u062D\": {\n \"isolated\": \"\\uFC23\",\n \"initial\": \"\\uFCB5\"\n },\n \"\\u0636\\u062E\": {\n \"isolated\": \"\\uFC24\",\n \"initial\": \"\\uFCB6\"\n },\n \"\\u0636\\u0645\": {\n \"isolated\": \"\\uFC25\",\n \"initial\": \"\\uFCB7\"\n },\n \"\\u0637\\u062D\": {\n \"isolated\": \"\\uFC26\",\n \"initial\": \"\\uFCB8\"\n },\n \"\\u0637\\u0645\": {\n \"isolated\": \"\\uFC27\",\n \"initial\": \"\\uFD33\",\n \"medial\": \"\\uFD3A\"\n },\n \"\\u0638\\u0645\": {\n \"isolated\": \"\\uFC28\",\n \"initial\": \"\\uFCB9\",\n \"medial\": \"\\uFD3B\"\n },\n \"\\u0639\\u062C\": {\n \"isolated\": \"\\uFC29\",\n \"initial\": \"\\uFCBA\"\n },\n \"\\u0639\\u0645\": {\n \"isolated\": \"\\uFC2A\",\n \"initial\": \"\\uFCBB\"\n },\n \"\\u063A\\u062C\": {\n \"isolated\": \"\\uFC2B\",\n \"initial\": \"\\uFCBC\"\n },\n \"\\u063A\\u0645\": {\n \"isolated\": \"\\uFC2C\",\n \"initial\": \"\\uFCBD\"\n },\n \"\\u0641\\u062C\": {\n \"isolated\": \"\\uFC2D\",\n \"initial\": \"\\uFCBE\"\n },\n \"\\u0641\\u062D\": {\n \"isolated\": \"\\uFC2E\",\n \"initial\": \"\\uFCBF\"\n },\n \"\\u0641\\u062E\": {\n \"isolated\": \"\\uFC2F\",\n \"initial\": \"\\uFCC0\"\n },\n \"\\u0641\\u0645\": {\n \"isolated\": \"\\uFC30\",\n \"initial\": \"\\uFCC1\"\n },\n \"\\u0641\\u0649\": {\n \"isolated\": \"\\uFC31\",\n \"final\": \"\\uFC7C\"\n },\n \"\\u0641\\u064A\": {\n \"isolated\": \"\\uFC32\",\n \"final\": \"\\uFC7D\"\n },\n \"\\u0642\\u062D\": {\n \"isolated\": \"\\uFC33\",\n \"initial\": \"\\uFCC2\"\n },\n \"\\u0642\\u0645\": {\n \"isolated\": \"\\uFC34\",\n \"initial\": \"\\uFCC3\"\n },\n \"\\u0642\\u0649\": {\n \"isolated\": \"\\uFC35\",\n \"final\": \"\\uFC7E\"\n },\n \"\\u0642\\u064A\": {\n \"isolated\": \"\\uFC36\",\n \"final\": \"\\uFC7F\"\n },\n \"\\u0643\\u0627\": {\n \"isolated\": \"\\uFC37\",\n \"final\": \"\\uFC80\"\n },\n \"\\u0643\\u062C\": {\n \"isolated\": \"\\uFC38\",\n \"initial\": \"\\uFCC4\"\n },\n \"\\u0643\\u062D\": {\n \"isolated\": \"\\uFC39\",\n \"initial\": \"\\uFCC5\"\n },\n \"\\u0643\\u062E\": {\n \"isolated\": \"\\uFC3A\",\n \"initial\": \"\\uFCC6\"\n },\n \"\\u0643\\u0644\": {\n \"isolated\": \"\\uFC3B\",\n \"final\": \"\\uFC81\",\n \"initial\": \"\\uFCC7\",\n \"medial\": \"\\uFCEB\"\n },\n \"\\u0643\\u0645\": {\n \"isolated\": \"\\uFC3C\",\n \"final\": \"\\uFC82\",\n \"initial\": \"\\uFCC8\",\n \"medial\": \"\\uFCEC\"\n },\n \"\\u0643\\u0649\": {\n \"isolated\": \"\\uFC3D\",\n \"final\": \"\\uFC83\"\n },\n \"\\u0643\\u064A\": {\n \"isolated\": \"\\uFC3E\",\n \"final\": \"\\uFC84\"\n },\n \"\\u0644\\u062C\": {\n \"isolated\": \"\\uFC3F\",\n \"initial\": \"\\uFCC9\"\n },\n \"\\u0644\\u062D\": {\n \"isolated\": \"\\uFC40\",\n \"initial\": \"\\uFCCA\"\n },\n \"\\u0644\\u062E\": {\n \"isolated\": \"\\uFC41\",\n \"initial\": \"\\uFCCB\"\n },\n \"\\u0644\\u0645\": {\n \"isolated\": \"\\uFC42\",\n \"final\": \"\\uFC85\",\n \"initial\": \"\\uFCCC\",\n \"medial\": \"\\uFCED\"\n },\n \"\\u0644\\u0649\": {\n \"isolated\": \"\\uFC43\",\n \"final\": \"\\uFC86\"\n },\n \"\\u0644\\u064A\": {\n \"isolated\": \"\\uFC44\",\n \"final\": \"\\uFC87\"\n },\n \"\\u0645\\u062C\": {\n \"isolated\": \"\\uFC45\",\n \"initial\": \"\\uFCCE\"\n },\n \"\\u0645\\u062D\": {\n \"isolated\": \"\\uFC46\",\n \"initial\": \"\\uFCCF\"\n },\n \"\\u0645\\u062E\": {\n \"isolated\": \"\\uFC47\",\n \"initial\": \"\\uFCD0\"\n },\n \"\\u0645\\u0645\": {\n \"isolated\": \"\\uFC48\",\n \"final\": \"\\uFC89\",\n \"initial\": \"\\uFCD1\"\n },\n \"\\u0645\\u0649\": {\n \"isolated\": \"\\uFC49\"\n },\n \"\\u0645\\u064A\": {\n \"isolated\": \"\\uFC4A\"\n },\n \"\\u0646\\u062C\": {\n \"isolated\": \"\\uFC4B\",\n \"initial\": \"\\uFCD2\"\n },\n \"\\u0646\\u062D\": {\n \"isolated\": \"\\uFC4C\",\n \"initial\": \"\\uFCD3\"\n },\n \"\\u0646\\u062E\": {\n \"isolated\": \"\\uFC4D\",\n \"initial\": \"\\uFCD4\"\n },\n \"\\u0646\\u0645\": {\n \"isolated\": \"\\uFC4E\",\n \"final\": \"\\uFC8C\",\n \"initial\": \"\\uFCD5\",\n \"medial\": \"\\uFCEE\"\n },\n \"\\u0646\\u0649\": {\n \"isolated\": \"\\uFC4F\",\n \"final\": \"\\uFC8E\"\n },\n \"\\u0646\\u064A\": {\n \"isolated\": \"\\uFC50\",\n \"final\": \"\\uFC8F\"\n },\n \"\\u0647\\u062C\": {\n \"isolated\": \"\\uFC51\",\n \"initial\": \"\\uFCD7\"\n },\n \"\\u0647\\u0645\": {\n \"isolated\": \"\\uFC52\",\n \"initial\": \"\\uFCD8\"\n },\n \"\\u0647\\u0649\": {\n \"isolated\": \"\\uFC53\"\n },\n \"\\u0647\\u064A\": {\n \"isolated\": \"\\uFC54\"\n },\n \"\\u064A\\u062C\": {\n \"isolated\": \"\\uFC55\",\n \"initial\": \"\\uFCDA\"\n },\n \"\\u064A\\u062D\": {\n \"isolated\": \"\\uFC56\",\n \"initial\": \"\\uFCDB\"\n },\n \"\\u064A\\u062E\": {\n \"isolated\": \"\\uFC57\",\n \"initial\": \"\\uFCDC\"\n },\n \"\\u064A\\u0645\": {\n \"isolated\": \"\\uFC58\",\n \"final\": \"\\uFC93\",\n \"initial\": \"\\uFCDD\",\n \"medial\": \"\\uFCF0\"\n },\n \"\\u064A\\u0649\": {\n \"isolated\": \"\\uFC59\",\n \"final\": \"\\uFC95\"\n },\n \"\\u064A\\u064A\": {\n \"isolated\": \"\\uFC5A\",\n \"final\": \"\\uFC96\"\n },\n \"\\u0630\\u0670\": {\n \"isolated\": \"\\uFC5B\"\n },\n \"\\u0631\\u0670\": {\n \"isolated\": \"\\uFC5C\"\n },\n \"\\u0649\\u0670\": {\n \"isolated\": \"\\uFC5D\",\n \"final\": \"\\uFC90\"\n },\n \"\\u064C\\u0651\": {\n \"isolated\": \"\\uFC5E\"\n },\n \"\\u064D\\u0651\": {\n \"isolated\": \"\\uFC5F\"\n },\n \"\\u064E\\u0651\": {\n \"isolated\": \"\\uFC60\"\n },\n \"\\u064F\\u0651\": {\n \"isolated\": \"\\uFC61\"\n },\n \"\\u0650\\u0651\": {\n \"isolated\": \"\\uFC62\"\n },\n \"\\u0651\\u0670\": {\n \"isolated\": \"\\uFC63\"\n },\n \"\\u0626\\u0631\": {\n \"final\": \"\\uFC64\"\n },\n \"\\u0626\\u0632\": {\n \"final\": \"\\uFC65\"\n },\n \"\\u0626\\u0646\": {\n \"final\": \"\\uFC67\"\n },\n \"\\u0628\\u0631\": {\n \"final\": \"\\uFC6A\"\n },\n \"\\u0628\\u0632\": {\n \"final\": \"\\uFC6B\"\n },\n \"\\u0628\\u0646\": {\n \"final\": \"\\uFC6D\"\n },\n \"\\u062A\\u0631\": {\n \"final\": \"\\uFC70\"\n },\n \"\\u062A\\u0632\": {\n \"final\": \"\\uFC71\"\n },\n \"\\u062A\\u0646\": {\n \"final\": \"\\uFC73\"\n },\n \"\\u062B\\u0631\": {\n \"final\": \"\\uFC76\"\n },\n \"\\u062B\\u0632\": {\n \"final\": \"\\uFC77\"\n },\n \"\\u062B\\u0646\": {\n \"final\": \"\\uFC79\"\n },\n \"\\u062B\\u064A\": {\n \"final\": \"\\uFC7B\"\n },\n \"\\u0645\\u0627\": {\n \"final\": \"\\uFC88\"\n },\n \"\\u0646\\u0631\": {\n \"final\": \"\\uFC8A\"\n },\n \"\\u0646\\u0632\": {\n \"final\": \"\\uFC8B\"\n },\n \"\\u0646\\u0646\": {\n \"final\": \"\\uFC8D\"\n },\n \"\\u064A\\u0631\": {\n \"final\": \"\\uFC91\"\n },\n \"\\u064A\\u0632\": {\n \"final\": \"\\uFC92\"\n },\n \"\\u064A\\u0646\": {\n \"final\": \"\\uFC94\"\n },\n \"\\u0626\\u062E\": {\n \"initial\": \"\\uFC99\"\n },\n \"\\u0626\\u0647\": {\n \"initial\": \"\\uFC9B\",\n \"medial\": \"\\uFCE0\"\n },\n \"\\u0628\\u0647\": {\n \"initial\": \"\\uFCA0\",\n \"medial\": \"\\uFCE2\"\n },\n \"\\u062A\\u0647\": {\n \"initial\": \"\\uFCA5\",\n \"medial\": \"\\uFCE4\"\n },\n \"\\u0635\\u062E\": {\n \"initial\": \"\\uFCB2\"\n },\n \"\\u0644\\u0647\": {\n \"initial\": \"\\uFCCD\"\n },\n \"\\u0646\\u0647\": {\n \"initial\": \"\\uFCD6\",\n \"medial\": \"\\uFCEF\"\n },\n \"\\u0647\\u0670\": {\n \"initial\": \"\\uFCD9\"\n },\n \"\\u064A\\u0647\": {\n \"initial\": \"\\uFCDE\",\n \"medial\": \"\\uFCF1\"\n },\n \"\\u062B\\u0647\": {\n \"medial\": \"\\uFCE6\"\n },\n \"\\u0633\\u0647\": {\n \"medial\": \"\\uFCE8\",\n \"initial\": \"\\uFD31\"\n },\n \"\\u0634\\u0645\": {\n \"medial\": \"\\uFCE9\",\n \"isolated\": \"\\uFD0C\",\n \"final\": \"\\uFD28\",\n \"initial\": \"\\uFD30\"\n },\n \"\\u0634\\u0647\": {\n \"medial\": \"\\uFCEA\",\n \"initial\": \"\\uFD32\"\n },\n \"\\u0640\\u064E\\u0651\": {\n \"medial\": \"\\uFCF2\"\n },\n \"\\u0640\\u064F\\u0651\": {\n \"medial\": \"\\uFCF3\"\n },\n \"\\u0640\\u0650\\u0651\": {\n \"medial\": \"\\uFCF4\"\n },\n \"\\u0637\\u0649\": {\n \"isolated\": \"\\uFCF5\",\n \"final\": \"\\uFD11\"\n },\n \"\\u0637\\u064A\": {\n \"isolated\": \"\\uFCF6\",\n \"final\": \"\\uFD12\"\n },\n \"\\u0639\\u0649\": {\n \"isolated\": \"\\uFCF7\",\n \"final\": \"\\uFD13\"\n },\n \"\\u0639\\u064A\": {\n \"isolated\": \"\\uFCF8\",\n \"final\": \"\\uFD14\"\n },\n \"\\u063A\\u0649\": {\n \"isolated\": \"\\uFCF9\",\n \"final\": \"\\uFD15\"\n },\n \"\\u063A\\u064A\": {\n \"isolated\": \"\\uFCFA\",\n \"final\": \"\\uFD16\"\n },\n \"\\u0633\\u0649\": {\n \"isolated\": \"\\uFCFB\"\n },\n \"\\u0633\\u064A\": {\n \"isolated\": \"\\uFCFC\",\n \"final\": \"\\uFD18\"\n },\n \"\\u0634\\u0649\": {\n \"isolated\": \"\\uFCFD\",\n \"final\": \"\\uFD19\"\n },\n \"\\u0634\\u064A\": {\n \"isolated\": \"\\uFCFE\",\n \"final\": \"\\uFD1A\"\n },\n \"\\u062D\\u0649\": {\n \"isolated\": \"\\uFCFF\",\n \"final\": \"\\uFD1B\"\n },\n \"\\u062D\\u064A\": {\n \"isolated\": \"\\uFD00\",\n \"final\": \"\\uFD1C\"\n },\n \"\\u062C\\u0649\": {\n \"isolated\": \"\\uFD01\",\n \"final\": \"\\uFD1D\"\n },\n \"\\u062C\\u064A\": {\n \"isolated\": \"\\uFD02\",\n \"final\": \"\\uFD1E\"\n },\n \"\\u062E\\u0649\": {\n \"isolated\": \"\\uFD03\",\n \"final\": \"\\uFD1F\"\n },\n \"\\u062E\\u064A\": {\n \"isolated\": \"\\uFD04\",\n \"final\": \"\\uFD20\"\n },\n \"\\u0635\\u0649\": {\n \"isolated\": \"\\uFD05\",\n \"final\": \"\\uFD21\"\n },\n \"\\u0635\\u064A\": {\n \"isolated\": \"\\uFD06\",\n \"final\": \"\\uFD22\"\n },\n \"\\u0636\\u0649\": {\n \"isolated\": \"\\uFD07\",\n \"final\": \"\\uFD23\"\n },\n \"\\u0636\\u064A\": {\n \"isolated\": \"\\uFD08\",\n \"final\": \"\\uFD24\"\n },\n \"\\u0634\\u062C\": {\n \"isolated\": \"\\uFD09\",\n \"final\": \"\\uFD25\",\n \"initial\": \"\\uFD2D\",\n \"medial\": \"\\uFD37\"\n },\n \"\\u0634\\u062D\": {\n \"isolated\": \"\\uFD0A\",\n \"final\": \"\\uFD26\",\n \"initial\": \"\\uFD2E\",\n \"medial\": \"\\uFD38\"\n },\n \"\\u0634\\u062E\": {\n \"isolated\": \"\\uFD0B\",\n \"final\": \"\\uFD27\",\n \"initial\": \"\\uFD2F\",\n \"medial\": \"\\uFD39\"\n },\n \"\\u0634\\u0631\": {\n \"isolated\": \"\\uFD0D\",\n \"final\": \"\\uFD29\"\n },\n \"\\u0633\\u0631\": {\n \"isolated\": \"\\uFD0E\",\n \"final\": \"\\uFD2A\"\n },\n \"\\u0635\\u0631\": {\n \"isolated\": \"\\uFD0F\",\n \"final\": \"\\uFD2B\"\n },\n \"\\u0636\\u0631\": {\n \"isolated\": \"\\uFD10\",\n \"final\": \"\\uFD2C\"\n },\n \"\\u0633\\u0639\": {\n \"final\": \"\\uFD17\"\n },\n \"\\u062A\\u062C\\u0645\": {\n \"initial\": \"\\uFD50\"\n },\n \"\\u062A\\u062D\\u062C\": {\n \"final\": \"\\uFD51\",\n \"initial\": \"\\uFD52\"\n },\n \"\\u062A\\u062D\\u0645\": {\n \"initial\": \"\\uFD53\"\n },\n \"\\u062A\\u062E\\u0645\": {\n \"initial\": \"\\uFD54\"\n },\n \"\\u062A\\u0645\\u062C\": {\n \"initial\": \"\\uFD55\"\n },\n \"\\u062A\\u0645\\u062D\": {\n \"initial\": \"\\uFD56\"\n },\n \"\\u062A\\u0645\\u062E\": {\n \"initial\": \"\\uFD57\"\n },\n \"\\u062C\\u0645\\u062D\": {\n \"final\": \"\\uFD58\",\n \"initial\": \"\\uFD59\"\n },\n \"\\u062D\\u0645\\u064A\": {\n \"final\": \"\\uFD5A\"\n },\n \"\\u062D\\u0645\\u0649\": {\n \"final\": \"\\uFD5B\"\n },\n \"\\u0633\\u062D\\u062C\": {\n \"initial\": \"\\uFD5C\"\n },\n \"\\u0633\\u062C\\u062D\": {\n \"initial\": \"\\uFD5D\"\n },\n \"\\u0633\\u062C\\u0649\": {\n \"final\": \"\\uFD5E\"\n },\n \"\\u0633\\u0645\\u062D\": {\n \"final\": \"\\uFD5F\",\n \"initial\": \"\\uFD60\"\n },\n \"\\u0633\\u0645\\u062C\": {\n \"initial\": \"\\uFD61\"\n },\n \"\\u0633\\u0645\\u0645\": {\n \"final\": \"\\uFD62\",\n \"initial\": \"\\uFD63\"\n },\n \"\\u0635\\u062D\\u062D\": {\n \"final\": \"\\uFD64\",\n \"initial\": \"\\uFD65\"\n },\n \"\\u0635\\u0645\\u0645\": {\n \"final\": \"\\uFD66\",\n \"initial\": \"\\uFDC5\"\n },\n \"\\u0634\\u062D\\u0645\": {\n \"final\": \"\\uFD67\",\n \"initial\": \"\\uFD68\"\n },\n \"\\u0634\\u062C\\u064A\": {\n \"final\": \"\\uFD69\"\n },\n \"\\u0634\\u0645\\u062E\": {\n \"final\": \"\\uFD6A\",\n \"initial\": \"\\uFD6B\"\n },\n \"\\u0634\\u0645\\u0645\": {\n \"final\": \"\\uFD6C\",\n \"initial\": \"\\uFD6D\"\n },\n \"\\u0636\\u062D\\u0649\": {\n \"final\": \"\\uFD6E\"\n },\n \"\\u0636\\u062E\\u0645\": {\n \"final\": \"\\uFD6F\",\n \"initial\": \"\\uFD70\"\n },\n \"\\u0636\\u0645\\u062D\": {\n \"final\": \"\\uFD71\"\n },\n \"\\u0637\\u0645\\u062D\": {\n \"initial\": \"\\uFD72\"\n },\n \"\\u0637\\u0645\\u0645\": {\n \"initial\": \"\\uFD73\"\n },\n \"\\u0637\\u0645\\u064A\": {\n \"final\": \"\\uFD74\"\n },\n \"\\u0639\\u062C\\u0645\": {\n \"final\": \"\\uFD75\",\n \"initial\": \"\\uFDC4\"\n },\n \"\\u0639\\u0645\\u0645\": {\n \"final\": \"\\uFD76\",\n \"initial\": \"\\uFD77\"\n },\n \"\\u0639\\u0645\\u0649\": {\n \"final\": \"\\uFD78\"\n },\n \"\\u063A\\u0645\\u0645\": {\n \"final\": \"\\uFD79\"\n },\n \"\\u063A\\u0645\\u064A\": {\n \"final\": \"\\uFD7A\"\n },\n \"\\u063A\\u0645\\u0649\": {\n \"final\": \"\\uFD7B\"\n },\n \"\\u0641\\u062E\\u0645\": {\n \"final\": \"\\uFD7C\",\n \"initial\": \"\\uFD7D\"\n },\n \"\\u0642\\u0645\\u062D\": {\n \"final\": \"\\uFD7E\",\n \"initial\": \"\\uFDB4\"\n },\n \"\\u0642\\u0645\\u0645\": {\n \"final\": \"\\uFD7F\"\n },\n \"\\u0644\\u062D\\u0645\": {\n \"final\": \"\\uFD80\",\n \"initial\": \"\\uFDB5\"\n },\n \"\\u0644\\u062D\\u064A\": {\n \"final\": \"\\uFD81\"\n },\n \"\\u0644\\u062D\\u0649\": {\n \"final\": \"\\uFD82\"\n },\n \"\\u0644\\u062C\\u062C\": {\n \"initial\": \"\\uFD83\",\n \"final\": \"\\uFD84\"\n },\n \"\\u0644\\u062E\\u0645\": {\n \"final\": \"\\uFD85\",\n \"initial\": \"\\uFD86\"\n },\n \"\\u0644\\u0645\\u062D\": {\n \"final\": \"\\uFD87\",\n \"initial\": \"\\uFD88\"\n },\n \"\\u0645\\u062D\\u062C\": {\n \"initial\": \"\\uFD89\"\n },\n \"\\u0645\\u062D\\u0645\": {\n \"initial\": \"\\uFD8A\"\n },\n \"\\u0645\\u062D\\u064A\": {\n \"final\": \"\\uFD8B\"\n },\n \"\\u0645\\u062C\\u062D\": {\n \"initial\": \"\\uFD8C\"\n },\n \"\\u0645\\u062C\\u0645\": {\n \"initial\": \"\\uFD8D\"\n },\n \"\\u0645\\u062E\\u062C\": {\n \"initial\": \"\\uFD8E\"\n },\n \"\\u0645\\u062E\\u0645\": {\n \"initial\": \"\\uFD8F\"\n },\n \"\\u0645\\u062C\\u062E\": {\n \"initial\": \"\\uFD92\"\n },\n \"\\u0647\\u0645\\u062C\": {\n \"initial\": \"\\uFD93\"\n },\n \"\\u0647\\u0645\\u0645\": {\n \"initial\": \"\\uFD94\"\n },\n \"\\u0646\\u062D\\u0645\": {\n \"initial\": \"\\uFD95\"\n },\n \"\\u0646\\u062D\\u0649\": {\n \"final\": \"\\uFD96\"\n },\n \"\\u0646\\u062C\\u0645\": {\n \"final\": \"\\uFD97\",\n \"initial\": \"\\uFD98\"\n },\n \"\\u0646\\u062C\\u0649\": {\n \"final\": \"\\uFD99\"\n },\n \"\\u0646\\u0645\\u064A\": {\n \"final\": \"\\uFD9A\"\n },\n \"\\u0646\\u0645\\u0649\": {\n \"final\": \"\\uFD9B\"\n },\n \"\\u064A\\u0645\\u0645\": {\n \"final\": \"\\uFD9C\",\n \"initial\": \"\\uFD9D\"\n },\n \"\\u0628\\u062E\\u064A\": {\n \"final\": \"\\uFD9E\"\n },\n \"\\u062A\\u062C\\u064A\": {\n \"final\": \"\\uFD9F\"\n },\n \"\\u062A\\u062C\\u0649\": {\n \"final\": \"\\uFDA0\"\n },\n \"\\u062A\\u062E\\u064A\": {\n \"final\": \"\\uFDA1\"\n },\n \"\\u062A\\u062E\\u0649\": {\n \"final\": \"\\uFDA2\"\n },\n \"\\u062A\\u0645\\u064A\": {\n \"final\": \"\\uFDA3\"\n },\n \"\\u062A\\u0645\\u0649\": {\n \"final\": \"\\uFDA4\"\n },\n \"\\u062C\\u0645\\u064A\": {\n \"final\": \"\\uFDA5\"\n },\n \"\\u062C\\u062D\\u0649\": {\n \"final\": \"\\uFDA6\"\n },\n \"\\u062C\\u0645\\u0649\": {\n \"final\": \"\\uFDA7\"\n },\n \"\\u0633\\u062E\\u0649\": {\n \"final\": \"\\uFDA8\"\n },\n \"\\u0635\\u062D\\u064A\": {\n \"final\": \"\\uFDA9\"\n },\n \"\\u0634\\u062D\\u064A\": {\n \"final\": \"\\uFDAA\"\n },\n \"\\u0636\\u062D\\u064A\": {\n \"final\": \"\\uFDAB\"\n },\n \"\\u0644\\u062C\\u064A\": {\n \"final\": \"\\uFDAC\"\n },\n \"\\u0644\\u0645\\u064A\": {\n \"final\": \"\\uFDAD\"\n },\n \"\\u064A\\u062D\\u064A\": {\n \"final\": \"\\uFDAE\"\n },\n \"\\u064A\\u062C\\u064A\": {\n \"final\": \"\\uFDAF\"\n },\n \"\\u064A\\u0645\\u064A\": {\n \"final\": \"\\uFDB0\"\n },\n \"\\u0645\\u0645\\u064A\": {\n \"final\": \"\\uFDB1\"\n },\n \"\\u0642\\u0645\\u064A\": {\n \"final\": \"\\uFDB2\"\n },\n \"\\u0646\\u062D\\u064A\": {\n \"final\": \"\\uFDB3\"\n },\n \"\\u0639\\u0645\\u064A\": {\n \"final\": \"\\uFDB6\"\n },\n \"\\u0643\\u0645\\u064A\": {\n \"final\": \"\\uFDB7\"\n },\n \"\\u0646\\u062C\\u062D\": {\n \"initial\": \"\\uFDB8\",\n \"final\": \"\\uFDBD\"\n },\n \"\\u0645\\u062E\\u064A\": {\n \"final\": \"\\uFDB9\"\n },\n \"\\u0644\\u062C\\u0645\": {\n \"initial\": \"\\uFDBA\",\n \"final\": \"\\uFDBC\"\n },\n \"\\u0643\\u0645\\u0645\": {\n \"final\": \"\\uFDBB\",\n \"initial\": \"\\uFDC3\"\n },\n \"\\u062C\\u062D\\u064A\": {\n \"final\": \"\\uFDBE\"\n },\n \"\\u062D\\u062C\\u064A\": {\n \"final\": \"\\uFDBF\"\n },\n \"\\u0645\\u062C\\u064A\": {\n \"final\": \"\\uFDC0\"\n },\n \"\\u0641\\u0645\\u064A\": {\n \"final\": \"\\uFDC1\"\n },\n \"\\u0628\\u062D\\u064A\": {\n \"final\": \"\\uFDC2\"\n },\n \"\\u0633\\u062E\\u064A\": {\n \"final\": \"\\uFDC6\"\n },\n \"\\u0646\\u062C\\u064A\": {\n \"final\": \"\\uFDC7\"\n },\n \"\\u0644\\u0622\": {\n \"isolated\": \"\\uFEF5\",\n \"final\": \"\\uFEF6\"\n },\n \"\\u0644\\u0623\": {\n \"isolated\": \"\\uFEF7\",\n \"final\": \"\\uFEF8\"\n },\n \"\\u0644\\u0625\": {\n \"isolated\": \"\\uFEF9\",\n \"final\": \"\\uFEFA\"\n },\n \"\\u0644\\u0627\": {\n \"isolated\": \"\\uFEFB\",\n \"final\": \"\\uFEFC\"\n },\n \"words\": {\n \"\\u0635\\u0644\\u06D2\": \"\\uFDF0\",\n \"\\u0642\\u0644\\u06D2\": \"\\uFDF1\",\n \"\\u0627\\u0644\\u0644\\u0647\": \"\\uFDF2\",\n \"\\u0627\\u0643\\u0628\\u0631\": \"\\uFDF3\",\n \"\\u0645\\u062D\\u0645\\u062F\": \"\\uFDF4\",\n \"\\u0635\\u0644\\u0639\\u0645\": \"\\uFDF5\",\n \"\\u0631\\u0633\\u0648\\u0644\": \"\\uFDF6\",\n \"\\u0639\\u0644\\u064A\\u0647\": \"\\uFDF7\",\n \"\\u0648\\u0633\\u0644\\u0645\": \"\\uFDF8\",\n \"\\u0635\\u0644\\u0649\": \"\\uFDF9\",\n \"\\u0635\\u0644\\u0649\\u0627\\u0644\\u0644\\u0647\\u0639\\u0644\\u064A\\u0647\\u0648\\u0633\\u0644\\u0645\": \"\\uFDFA\",\n \"\\u062C\\u0644\\u062C\\u0644\\u0627\\u0644\\u0647\": \"\\uFDFB\",\n \"\\u0631\\u06CC\\u0627\\u0644\": \"\\uFDFC\"\n }\n};\nexports.default = ligatureReference;\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.ligatureWordList = exports.ligatureList = exports.letterList = exports.alefs = exports.lams = exports.lineBreakers = exports.tashkeel = void 0;\nconst unicode_arabic_1 = require(\"./unicode-arabic\");\nconst unicode_ligatures_1 = require(\"./unicode-ligatures\");\nconst letterList = Object.keys(unicode_arabic_1.default);\nexports.letterList = letterList;\nconst ligatureList = Object.keys(unicode_ligatures_1.default);\nexports.ligatureList = ligatureList;\nconst ligatureWordList = Object.keys(unicode_ligatures_1.default.words);\nexports.ligatureWordList = ligatureWordList;\nconst lams = '\\u0644\\u06B5\\u06B6\\u06B7\\u06B8';\nexports.lams = lams;\nconst alefs = '\\u0627\\u0622\\u0623\\u0625\\u0671\\u0672\\u0673\\u0675\\u0773\\u0774';\nexports.alefs = alefs;\n// for (var l = 1; l < lams.length; l++) {\n// console.log('-');\n// for (var a = 0; a < alefs.length; a++) {\n// console.log(a + ': ' + lams[l] + alefs[a]);\n// }\n// }\nlet tashkeel = '\\u0605\\u0640\\u0670\\u0674\\u06DF\\u06E7\\u06E8';\nexports.tashkeel = tashkeel;\nfunction addToTashkeel(start, finish) {\n for (var i = start; i <= finish; i++) {\n exports.tashkeel = tashkeel += String.fromCharCode(i);\n }\n}\naddToTashkeel(0x0610, 0x061A);\naddToTashkeel(0x064B, 0x065F);\naddToTashkeel(0x06D6, 0x06DC);\naddToTashkeel(0x06E0, 0x06E4);\naddToTashkeel(0x06EA, 0x06ED);\naddToTashkeel(0x08D3, 0x08E1);\naddToTashkeel(0x08E3, 0x08FF);\naddToTashkeel(0xFE70, 0xFE7F);\nlet lineBreakers = '\\u0627\\u0629\\u0648\\u06C0\\u06CF\\u06FD\\u06FE\\u076B\\u076C\\u0771\\u0773\\u0774\\u0778\\u0779\\u08E2\\u08B1\\u08B2\\u08B9';\nexports.lineBreakers = lineBreakers;\nfunction addToLineBreakers(start, finish) {\n for (var i = start; i <= finish; i++) {\n exports.lineBreakers = lineBreakers += String.fromCharCode(i);\n }\n}\naddToLineBreakers(0x0600, 0x061F); // it's OK to include tashkeel in this range as it is ignored\naddToLineBreakers(0x0621, 0x0625);\naddToLineBreakers(0x062F, 0x0632);\naddToLineBreakers(0x0660, 0x066D); // numerals, math\naddToLineBreakers(0x0671, 0x0677);\naddToLineBreakers(0x0688, 0x0699);\naddToLineBreakers(0x06C3, 0x06CB);\naddToLineBreakers(0x06D2, 0x06F9);\naddToLineBreakers(0x0759, 0x075B);\naddToLineBreakers(0x08AA, 0x08AE);\naddToLineBreakers(0xFB50, 0xFDFD); // presentation forms look like they could connect, but never do\n// Presentation Forms A includes diacritics but they are meant to stand alone\naddToLineBreakers(0xFE80, 0xFEFC); // presentation forms look like they could connect, but never do\n// numerals, math\naddToLineBreakers(0x10E60, 0x10E7F);\naddToLineBreakers(0x1EC70, 0x1ECBF);\naddToLineBreakers(0x1EE00, 0x1EEFF);\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.GlyphSplitter = GlyphSplitter;\nconst isArabic_1 = require(\"./isArabic\");\nconst reference_1 = require(\"./reference\");\nfunction GlyphSplitter(word) {\n let letters = [];\n let lastLetter = '';\n word.split('').forEach((letter) => {\n if ((0, isArabic_1.isArabic)(letter)) {\n if (reference_1.tashkeel.indexOf(letter) > -1) {\n letters[letters.length - 1] += letter;\n }\n else if (lastLetter.length && ((reference_1.lams.indexOf(lastLetter) === 0 && reference_1.alefs.indexOf(letter) > -1) || (reference_1.lams.indexOf(lastLetter) > 0 && reference_1.alefs.indexOf(letter) === 0))) {\n // valid LA forms\n letters[letters.length - 1] += letter;\n }\n else {\n letters.push(letter);\n }\n }\n else {\n letters.push(letter);\n }\n if (reference_1.tashkeel.indexOf(letter) === -1) {\n lastLetter = letter;\n }\n });\n return letters;\n}\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.BaselineSplitter = BaselineSplitter;\nconst isArabic_1 = require(\"./isArabic\");\nconst reference_1 = require(\"./reference\");\nfunction BaselineSplitter(word) {\n let letters = [];\n let lastLetter = '';\n word.split('').forEach((letter) => {\n if ((0, isArabic_1.isArabic)(letter) && (0, isArabic_1.isArabic)(lastLetter)) {\n if (lastLetter.length && reference_1.tashkeel.indexOf(letter) > -1) {\n letters[letters.length - 1] += letter;\n }\n else if (reference_1.lineBreakers.indexOf(lastLetter) > -1) {\n letters.push(letter);\n }\n else {\n letters[letters.length - 1] += letter;\n }\n }\n else {\n letters.push(letter);\n }\n if (reference_1.tashkeel.indexOf(letter) === -1) {\n // don't allow tashkeel to hide line break\n lastLetter = letter;\n }\n });\n return letters;\n}\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.Normal = Normal;\nconst unicode_arabic_1 = require(\"./unicode-arabic\");\nconst unicode_ligatures_1 = require(\"./unicode-ligatures\");\nconst isArabic_1 = require(\"./isArabic\");\nconst reference_1 = require(\"./reference\");\nfunction Normal(word, breakPresentationForm) {\n // default is to turn initial/isolated/medial/final presentation form to generic\n if (typeof breakPresentationForm === 'undefined') {\n breakPresentationForm = true;\n }\n let returnable = '';\n word.split('').forEach((letter) => {\n if (!(0, isArabic_1.isArabic)(letter)) {\n returnable += letter;\n return;\n }\n for (let w = 0; w < reference_1.letterList.length; w++) {\n // ok so we are checking this potential lettertron\n let letterForms = unicode_arabic_1.default[reference_1.letterList[w]];\n let versions = Object.keys(letterForms);\n for (let v = 0; v < versions.length; v++) {\n let localVersion = letterForms[versions[v]];\n if (typeof localVersion === 'object' && typeof localVersion.indexOf === 'undefined') {\n // look at this embedded object\n let embeddedForms = Object.keys(localVersion);\n for (let ef = 0; ef < embeddedForms.length; ef++) {\n let form = localVersion[embeddedForms[ef]];\n if (form === letter || (typeof form === 'object' && form.indexOf && form.indexOf(letter) > -1)) {\n // match\n // console.log('embedded match');\n if (form === letter) {\n // match exact\n if (breakPresentationForm && localVersion['normal'] && ['isolated', 'initial', 'medial', 'final'].indexOf(embeddedForms[ef]) > -1) {\n // replace presentation form\n // console.log('keeping normal form of the letter');\n if (typeof localVersion['normal'] === 'object') {\n returnable += localVersion['normal'][0];\n }\n else {\n returnable += localVersion['normal'];\n }\n return;\n }\n // console.log('keeping this letter');\n returnable += letter;\n return;\n }\n else if (typeof form === 'object' && form.indexOf && form.indexOf(letter) > -1) {\n // match\n returnable += form[0];\n // console.log('added the first letter from the same array');\n return;\n }\n }\n }\n }\n else if (localVersion === letter) {\n // match exact\n if (breakPresentationForm && letterForms['normal'] && ['isolated', 'initial', 'medial', 'final'].indexOf(versions[v]) > -1) {\n // replace presentation form\n // console.log('keeping normal form of the letter');\n if (typeof letterForms['normal'] === 'object') {\n returnable += letterForms['normal'][0];\n }\n else {\n returnable += letterForms['normal'];\n }\n return;\n }\n // console.log('keeping this letter');\n returnable += letter;\n return;\n }\n else if (typeof localVersion === 'object' && localVersion.indexOf && localVersion.indexOf(letter) > -1) {\n // match\n returnable += localVersion[0];\n // console.log('added the first letter from the same array');\n return;\n }\n }\n }\n // try ligatures\n for (let v2 = 0; v2 < reference_1.ligatureList.length; v2++) {\n let normalForm = reference_1.ligatureList[v2];\n if (normalForm !== 'words') {\n let ligForms = Object.keys(unicode_ligatures_1.default[normalForm]);\n for (let f = 0; f < ligForms.length; f++) {\n if (unicode_ligatures_1.default[normalForm][ligForms[f]] === letter) {\n returnable += normalForm;\n return;\n }\n }\n }\n }\n // try words ligatures\n for (let v3 = 0; v3 < reference_1.ligatureWordList.length; v3++) {\n let normalForm = reference_1.ligatureWordList[v3];\n if (unicode_ligatures_1.default.words[normalForm] === letter) {\n returnable += normalForm;\n return;\n }\n }\n returnable += letter;\n // console.log('kept the letter')\n });\n return returnable;\n}\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.CharShaper = CharShaper;\nconst unicode_arabic_1 = require(\"./unicode-arabic\");\nconst isArabic_1 = require(\"./isArabic\");\nconst reference_1 = require(\"./reference\");\nfunction CharShaper(letter, form) {\n if (!(0, isArabic_1.isArabic)(letter)) {\n // fail not Arabic\n throw new Error('Not Arabic');\n }\n if (letter === \"\\u0621\") {\n // hamza alone\n return \"\\u0621\";\n }\n for (let w = 0; w < reference_1.letterList.length; w++) {\n // ok so we are checking this potential lettertron\n let letterForms = unicode_arabic_1.default[reference_1.letterList[w]];\n let versions = Object.keys(letterForms);\n for (let v = 0; v < versions.length; v++) {\n let localVersion = letterForms[versions[v]];\n if ((localVersion === letter) ||\n (typeof localVersion === 'object' && localVersion.indexOf && localVersion.indexOf(letter) > -1)) {\n if (versions.indexOf(form) > -1) {\n return letterForms[form];\n }\n }\n else if (typeof localVersion === 'object' && typeof localVersion.indexOf === 'undefined') {\n // check embedded\n let embeddedVersions = Object.keys(localVersion);\n for (let ev = 0; ev < embeddedVersions.length; ev++) {\n if ((localVersion[embeddedVersions[ev]] === letter) ||\n (typeof localVersion[embeddedVersions[ev]] === 'object' && localVersion[embeddedVersions[ev]].indexOf && localVersion[embeddedVersions[ev]].indexOf(letter) > -1)) {\n if (embeddedVersions.indexOf(form) > -1) {\n return localVersion[form];\n }\n }\n }\n }\n }\n }\n}\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.WordShaper = WordShaper;\nconst isArabic_1 = require(\"./isArabic\");\nconst reference_1 = require(\"./reference\");\nconst CharShaper_1 = require(\"./CharShaper\");\nconst unicode_ligatures_1 = require(\"./unicode-ligatures\");\nfunction WordShaper(word) {\n let state = 'initial';\n let output = '';\n for (let w = 0; w < word.length; w++) {\n let nextLetter = ' ';\n for (let nxw = w + 1; nxw < word.length; nxw++) {\n if (!(0, isArabic_1.isArabic)(word[nxw])) {\n break;\n }\n if (reference_1.tashkeel.indexOf(word[nxw]) === -1) {\n nextLetter = word[nxw];\n break;\n }\n }\n if (!(0, isArabic_1.isArabic)(word[w]) || (0, isArabic_1.isMath)(word[w])) {\n // space or other non-Arabic\n output += word[w];\n state = 'initial';\n }\n else if (reference_1.tashkeel.indexOf(word[w]) > -1) {\n // tashkeel - add without changing state\n output += word[w];\n }\n else if ((nextLetter === ' ') // last Arabic letter in this word\n || (reference_1.lineBreakers.indexOf(word[w]) > -1)) { // the current letter is known to break lines\n output += (0, CharShaper_1.CharShaper)(word[w], state === 'initial' ? 'isolated' : 'final');\n state = 'initial';\n }\n else if (reference_1.lams.indexOf(word[w]) > -1 && reference_1.alefs.indexOf(nextLetter) > -1) {\n // LA letters - advance an additional letter after this\n output += unicode_ligatures_1.default[word[w] + nextLetter][(state === 'initial' ? 'isolated' : 'final')];\n while (word[w] !== nextLetter) {\n w++;\n }\n state = 'initial';\n }\n else {\n output += (0, CharShaper_1.CharShaper)(word[w], state);\n state = 'medial';\n }\n }\n return output;\n}\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.ParentLetter = ParentLetter;\nexports.GrandparentLetter = GrandparentLetter;\nconst unicode_arabic_1 = require(\"./unicode-arabic\");\nconst isArabic_1 = require(\"./isArabic\");\nconst reference_1 = require(\"./reference\");\nfunction ParentLetter(letter) {\n if (!(0, isArabic_1.isArabic)(letter)) {\n throw new Error('Not an Arabic letter');\n }\n for (let w = 0; w < reference_1.letterList.length; w++) {\n // ok so we are checking this potential lettertron\n let letterForms = unicode_arabic_1.default[reference_1.letterList[w]];\n let versions = Object.keys(letterForms);\n for (let v = 0; v < versions.length; v++) {\n let localVersion = letterForms[versions[v]];\n if (typeof localVersion === 'object' && typeof localVersion.indexOf === 'undefined') {\n // look at this embedded object\n let embeddedForms = Object.keys(localVersion);\n for (let ef = 0; ef < embeddedForms.length; ef++) {\n let form = localVersion[embeddedForms[ef]];\n if (form === letter || (typeof form === 'object' && form.indexOf && form.indexOf(letter) > -1)) {\n // match\n return localVersion;\n }\n }\n }\n else if (localVersion === letter || (typeof localVersion === 'object' && localVersion.indexOf && localVersion.indexOf(letter) > -1)) {\n // match\n return letterForms;\n }\n }\n return null;\n }\n}\nfunction GrandparentLetter(letter) {\n if (!(0, isArabic_1.isArabic)(letter)) {\n throw new Error('Not an Arabic letter');\n }\n for (let w = 0; w < reference_1.letterList.length; w++) {\n // ok so we are checking this potential lettertron\n let letterForms = unicode_arabic_1.default[reference_1.letterList[w]];\n let versions = Object.keys(letterForms);\n for (let v = 0; v < versions.length; v++) {\n let localVersion = letterForms[versions[v]];\n if (typeof localVersion === 'object' && typeof localVersion.indexOf === 'undefined') {\n // look at this embedded object\n let embeddedForms = Object.keys(localVersion);\n for (let ef = 0; ef < embeddedForms.length; ef++) {\n let form = localVersion[embeddedForms[ef]];\n if (form === letter || (typeof form === 'object' && form.indexOf && form.indexOf(letter) > -1)) {\n // match\n return letterForms;\n }\n }\n }\n else if (localVersion === letter || (typeof localVersion === 'object' && localVersion.indexOf && localVersion.indexOf(letter) > -1)) {\n // match\n return letterForms;\n }\n }\n return null;\n }\n}\n", "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.GrandparentLetter = exports.ParentLetter = exports.WordShaper = exports.CharShaper = exports.Normal = exports.BaselineSplitter = exports.GlyphSplitter = exports.isArabic = void 0;\nvar isArabic_1 = require(\"./isArabic\");\nObject.defineProperty(exports, \"isArabic\", { enumerable: true, get: function () { return isArabic_1.isArabic; } });\nvar GlyphSplitter_1 = require(\"./GlyphSplitter\");\nObject.defineProperty(exports, \"GlyphSplitter\", { enumerable: true, get: function () { return GlyphSplitter_1.GlyphSplitter; } });\nvar BaselineSplitter_1 = require(\"./BaselineSplitter\");\nObject.defineProperty(exports, \"BaselineSplitter\", { enumerable: true, get: function () { return BaselineSplitter_1.BaselineSplitter; } });\nvar Normalization_1 = require(\"./Normalization\");\nObject.defineProperty(exports, \"Normal\", { enumerable: true, get: function () { return Normalization_1.Normal; } });\nvar CharShaper_1 = require(\"./CharShaper\");\nObject.defineProperty(exports, \"CharShaper\", { enumerable: true, get: function () { return CharShaper_1.CharShaper; } });\nvar WordShaper_1 = require(\"./WordShaper\");\nObject.defineProperty(exports, \"WordShaper\", { enumerable: true, get: function () { return WordShaper_1.WordShaper; } });\nvar ParentLetter_1 = require(\"./ParentLetter\");\nObject.defineProperty(exports, \"ParentLetter\", { enumerable: true, get: function () { return ParentLetter_1.ParentLetter; } });\nObject.defineProperty(exports, \"GrandparentLetter\", { enumerable: true, get: function () { return ParentLetter_1.GrandparentLetter; } });\n", "// see https://github.com/openstreetmap/iD/pull/3707\n// https://gist.github.com/mapmeld/556b09ddec07a2044c76e1ef45f01c60\n// fixed in Chromium 96.0 https://bugs.chromium.org/p/chromium/issues/detail?id=374526\n\nimport { WordShaper } from 'alif-toolkit';\n\nexport var rtlRegex = /[\\u0590-\\u05FF\\u0600-\\u06FF\\u0750-\\u07BF\\u08A0\u2013\\u08BF]/;\n\nexport function fixRTLTextForSvg(inputText: string) {\n var ret = '', rtlBuffer: string[] = [];\n var arabicRegex = /[\\u0600-\\u06FF]/g;\n var arabicDiacritics = /[\\u0610-\\u061A\\u064B-\\u065F\\u0670\\u06D6-\\u06ED]/g;\n var arabicMath = /[\\u0660-\\u066C\\u06F0-\\u06F9]+/g;\n var thaanaVowel = /[\\u07A6-\\u07B0]/;\n var hebrewSign = /[\\u0591-\\u05bd\\u05bf\\u05c1-\\u05c5\\u05c7]/;\n\n // Arabic word shaping\n if (arabicRegex.test(inputText)) {\n inputText = WordShaper(inputText);\n }\n\n for (var n = 0; n < inputText.length; n++) {\n var c = inputText[n];\n if (arabicMath.test(c)) {\n // Arabic numbers go LTR\n ret += rtlBuffer.reverse().join('');\n rtlBuffer = [c];\n } else {\n if (rtlBuffer.length && arabicMath.test(rtlBuffer[rtlBuffer.length - 1])) {\n ret += rtlBuffer.reverse().join('');\n rtlBuffer = [];\n }\n if ((thaanaVowel.test(c) || hebrewSign.test(c) || arabicDiacritics.test(c)) && rtlBuffer.length) {\n rtlBuffer[rtlBuffer.length - 1] += c;\n } else if (rtlRegex.test(c)\n // include Arabic presentation forms\n || (c.charCodeAt(0) >= 64336 && c.charCodeAt(0) <= 65023)\n || (c.charCodeAt(0) >= 65136 && c.charCodeAt(0) <= 65279)) {\n rtlBuffer.push(c);\n } else if (c === ' ' && rtlBuffer.length) {\n // whitespace within RTL text\n rtlBuffer = [rtlBuffer.reverse().join('') + ' '];\n } else {\n // non-RTL character\n ret += rtlBuffer.reverse().join('') + c;\n rtlBuffer = [];\n }\n }\n }\n ret += rtlBuffer.reverse().join('');\n return ret;\n}\n", "function castArray(value) {\n if (arguments.length === 0) {\n return [];\n }\n return Array.isArray(value) ? value : [value];\n}\n\nexport { castArray };\n", "function chunk(arr, size) {\n if (!Number.isInteger(size) || size <= 0) {\n throw new Error('Size must be an integer greater than zero.');\n }\n const chunkLength = Math.ceil(arr.length / size);\n const result = Array(chunkLength);\n for (let index = 0; index < chunkLength; index++) {\n const start = index * size;\n const end = start + size;\n result[index] = arr.slice(start, end);\n }\n return result;\n}\n\nexport { chunk };\n", "function toArray(value) {\n return Array.isArray(value) ? value : Array.from(value);\n}\n\nexport { toArray };\n", "function isLength(value) {\n return Number.isSafeInteger(value) && value >= 0;\n}\n\nexport { isLength };\n", "import { isLength } from '../../predicate/isLength.mjs';\n\nfunction isArrayLike(value) {\n return value != null && typeof value !== 'function' && isLength(value.length);\n}\n\nexport { isArrayLike };\n", "import { chunk as chunk$1 } from '../../array/chunk.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction chunk(arr, size = 1) {\n size = Math.max(Math.floor(size), 0);\n if (size === 0 || !isArrayLike(arr)) {\n return [];\n }\n return chunk$1(toArray(arr), size);\n}\n\nexport { chunk };\n", "function compact(arr) {\n const result = [];\n for (let i = 0; i < arr.length; i++) {\n const item = arr[i];\n if (item) {\n result.push(item);\n }\n }\n return result;\n}\n\nexport { compact };\n", "import { compact as compact$1 } from '../../array/compact.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction compact(arr) {\n if (!isArrayLike(arr)) {\n return [];\n }\n return compact$1(Array.from(arr));\n}\n\nexport { compact };\n", "function flatten(arr, depth = 1) {\n const result = [];\n const flooredDepth = Math.floor(depth);\n const recursive = (arr, currentDepth) => {\n for (let i = 0; i < arr.length; i++) {\n const item = arr[i];\n if (Array.isArray(item) && currentDepth < flooredDepth) {\n recursive(item, currentDepth + 1);\n }\n else {\n result.push(item);\n }\n }\n };\n recursive(arr, 0);\n return result;\n}\n\nexport { flatten };\n", "import { flatten } from '../../array/flatten.mjs';\n\nfunction concat(...values) {\n return flatten(values);\n}\n\nexport { concat };\n", "function identity(x) {\n return x;\n}\n\nexport { identity };\n", "function isUnsafeProperty(key) {\n return key === '__proto__';\n}\n\nexport { isUnsafeProperty };\n", "function isDeepKey(key) {\n switch (typeof key) {\n case 'number':\n case 'symbol': {\n return false;\n }\n case 'string': {\n return key.includes('.') || key.includes('[') || key.includes(']');\n }\n }\n}\n\nexport { isDeepKey };\n", "function toKey(value) {\n if (typeof value === 'string' || typeof value === 'symbol') {\n return value;\n }\n if (Object.is(value?.valueOf?.(), -0)) {\n return '-0';\n }\n return String(value);\n}\n\nexport { toKey };\n", "function toString(value) {\n if (value == null) {\n return '';\n }\n if (typeof value === 'string') {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map(toString).join(',');\n }\n const result = String(value);\n if (result === '0' && Object.is(Number(value), -0)) {\n return '-0';\n }\n return result;\n}\n\nexport { toString };\n", "import { toString } from './toString.mjs';\nimport { toKey } from '../_internal/toKey.mjs';\n\nfunction toPath(deepKey) {\n if (Array.isArray(deepKey)) {\n return deepKey.map(toKey);\n }\n if (typeof deepKey === 'symbol') {\n return [deepKey];\n }\n deepKey = toString(deepKey);\n const result = [];\n const length = deepKey.length;\n if (length === 0) {\n return result;\n }\n let index = 0;\n let key = '';\n let quoteChar = '';\n let bracket = false;\n if (deepKey.charCodeAt(0) === 46) {\n result.push('');\n index++;\n }\n while (index < length) {\n const char = deepKey[index];\n if (quoteChar) {\n if (char === '\\\\' && index + 1 < length) {\n index++;\n key += deepKey[index];\n }\n else if (char === quoteChar) {\n quoteChar = '';\n }\n else {\n key += char;\n }\n }\n else if (bracket) {\n if (char === '\"' || char === \"'\") {\n quoteChar = char;\n }\n else if (char === ']') {\n bracket = false;\n result.push(key);\n key = '';\n }\n else {\n key += char;\n }\n }\n else {\n if (char === '[') {\n bracket = true;\n if (key) {\n result.push(key);\n key = '';\n }\n }\n else if (char === '.') {\n if (key) {\n result.push(key);\n key = '';\n }\n }\n else {\n key += char;\n }\n }\n index++;\n }\n if (key) {\n result.push(key);\n }\n return result;\n}\n\nexport { toPath };\n", "import { isUnsafeProperty } from '../../_internal/isUnsafeProperty.mjs';\nimport { isDeepKey } from '../_internal/isDeepKey.mjs';\nimport { toKey } from '../_internal/toKey.mjs';\nimport { toPath } from '../util/toPath.mjs';\n\nfunction get(object, path, defaultValue) {\n if (object == null) {\n return defaultValue;\n }\n switch (typeof path) {\n case 'string': {\n if (isUnsafeProperty(path)) {\n return defaultValue;\n }\n const result = object[path];\n if (result === undefined) {\n if (isDeepKey(path)) {\n return get(object, toPath(path), defaultValue);\n }\n else {\n return defaultValue;\n }\n }\n return result;\n }\n case 'number':\n case 'symbol': {\n if (typeof path === 'number') {\n path = toKey(path);\n }\n const result = object[path];\n if (result === undefined) {\n return defaultValue;\n }\n return result;\n }\n default: {\n if (Array.isArray(path)) {\n return getWithPath(object, path, defaultValue);\n }\n if (Object.is(path?.valueOf(), -0)) {\n path = '-0';\n }\n else {\n path = String(path);\n }\n if (isUnsafeProperty(path)) {\n return defaultValue;\n }\n const result = object[path];\n if (result === undefined) {\n return defaultValue;\n }\n return result;\n }\n }\n}\nfunction getWithPath(object, path, defaultValue) {\n if (path.length === 0) {\n return defaultValue;\n }\n let current = object;\n for (let index = 0; index < path.length; index++) {\n if (current == null) {\n return defaultValue;\n }\n if (isUnsafeProperty(path[index])) {\n return defaultValue;\n }\n current = current[path[index]];\n }\n if (current === undefined) {\n return defaultValue;\n }\n return current;\n}\n\nexport { get };\n", "import { get } from './get.mjs';\n\nfunction property(path) {\n return function (object) {\n return get(object, path);\n };\n}\n\nexport { property };\n", "function isObject(value) {\n return value !== null && (typeof value === 'object' || typeof value === 'function');\n}\n\nexport { isObject };\n", "function isPrimitive(value) {\n return value == null || (typeof value !== 'object' && typeof value !== 'function');\n}\n\nexport { isPrimitive };\n", "function isEqualsSameValueZero(value, other) {\n return value === other || (Number.isNaN(value) && Number.isNaN(other));\n}\n\nexport { isEqualsSameValueZero };\n", "import { isObject } from './isObject.mjs';\nimport { isPrimitive } from '../../predicate/isPrimitive.mjs';\nimport { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\n\nfunction isMatchWith(target, source, compare) {\n if (typeof compare !== 'function') {\n return isMatchWith(target, source, () => undefined);\n }\n return isMatchWithInternal(target, source, function doesMatch(objValue, srcValue, key, object, source, stack) {\n const isEqual = compare(objValue, srcValue, key, object, source, stack);\n if (isEqual !== undefined) {\n return Boolean(isEqual);\n }\n return isMatchWithInternal(objValue, srcValue, doesMatch, stack);\n }, new Map());\n}\nfunction isMatchWithInternal(target, source, compare, stack) {\n if (source === target) {\n return true;\n }\n switch (typeof source) {\n case 'object': {\n return isObjectMatch(target, source, compare, stack);\n }\n case 'function': {\n const sourceKeys = Object.keys(source);\n if (sourceKeys.length > 0) {\n return isMatchWithInternal(target, { ...source }, compare, stack);\n }\n return isEqualsSameValueZero(target, source);\n }\n default: {\n if (!isObject(target)) {\n return isEqualsSameValueZero(target, source);\n }\n if (typeof source === 'string') {\n return source === '';\n }\n return true;\n }\n }\n}\nfunction isObjectMatch(target, source, compare, stack) {\n if (source == null) {\n return true;\n }\n if (Array.isArray(source)) {\n return isArrayMatch(target, source, compare, stack);\n }\n if (source instanceof Map) {\n return isMapMatch(target, source, compare, stack);\n }\n if (source instanceof Set) {\n return isSetMatch(target, source, compare, stack);\n }\n const keys = Object.keys(source);\n if (target == null || isPrimitive(target)) {\n return keys.length === 0;\n }\n if (keys.length === 0) {\n return true;\n }\n if (stack?.has(source)) {\n return stack.get(source) === target;\n }\n stack?.set(source, target);\n try {\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n if (!isPrimitive(target) && !(key in target)) {\n return false;\n }\n if (source[key] === undefined && target[key] !== undefined) {\n return false;\n }\n if (source[key] === null && target[key] !== null) {\n return false;\n }\n const isEqual = compare(target[key], source[key], key, target, source, stack);\n if (!isEqual) {\n return false;\n }\n }\n return true;\n }\n finally {\n stack?.delete(source);\n }\n}\nfunction isMapMatch(target, source, compare, stack) {\n if (source.size === 0) {\n return true;\n }\n if (!(target instanceof Map)) {\n return false;\n }\n for (const [key, sourceValue] of source.entries()) {\n const targetValue = target.get(key);\n const isEqual = compare(targetValue, sourceValue, key, target, source, stack);\n if (isEqual === false) {\n return false;\n }\n }\n return true;\n}\nfunction isArrayMatch(target, source, compare, stack) {\n if (source.length === 0) {\n return true;\n }\n if (!Array.isArray(target)) {\n return false;\n }\n const countedIndex = new Set();\n for (let i = 0; i < source.length; i++) {\n const sourceItem = source[i];\n let found = false;\n for (let j = 0; j < target.length; j++) {\n if (countedIndex.has(j)) {\n continue;\n }\n const targetItem = target[j];\n let matches = false;\n const isEqual = compare(targetItem, sourceItem, i, target, source, stack);\n if (isEqual) {\n matches = true;\n }\n if (matches) {\n countedIndex.add(j);\n found = true;\n break;\n }\n }\n if (!found) {\n return false;\n }\n }\n return true;\n}\nfunction isSetMatch(target, source, compare, stack) {\n if (source.size === 0) {\n return true;\n }\n if (!(target instanceof Set)) {\n return false;\n }\n return isArrayMatch([...target], [...source], compare, stack);\n}\n\nexport { isMatchWith, isSetMatch };\n", "import { isMatchWith } from './isMatchWith.mjs';\n\nfunction isMatch(target, source) {\n return isMatchWith(target, source, () => undefined);\n}\n\nexport { isMatch };\n", "function getSymbols(object) {\n return Object.getOwnPropertySymbols(object).filter(symbol => Object.prototype.propertyIsEnumerable.call(object, symbol));\n}\n\nexport { getSymbols };\n", "function getTag(value) {\n if (value == null) {\n return value === undefined ? '[object Undefined]' : '[object Null]';\n }\n return Object.prototype.toString.call(value);\n}\n\nexport { getTag };\n", "const regexpTag = '[object RegExp]';\nconst stringTag = '[object String]';\nconst numberTag = '[object Number]';\nconst booleanTag = '[object Boolean]';\nconst argumentsTag = '[object Arguments]';\nconst symbolTag = '[object Symbol]';\nconst dateTag = '[object Date]';\nconst mapTag = '[object Map]';\nconst setTag = '[object Set]';\nconst arrayTag = '[object Array]';\nconst functionTag = '[object Function]';\nconst arrayBufferTag = '[object ArrayBuffer]';\nconst objectTag = '[object Object]';\nconst errorTag = '[object Error]';\nconst dataViewTag = '[object DataView]';\nconst uint8ArrayTag = '[object Uint8Array]';\nconst uint8ClampedArrayTag = '[object Uint8ClampedArray]';\nconst uint16ArrayTag = '[object Uint16Array]';\nconst uint32ArrayTag = '[object Uint32Array]';\nconst bigUint64ArrayTag = '[object BigUint64Array]';\nconst int8ArrayTag = '[object Int8Array]';\nconst int16ArrayTag = '[object Int16Array]';\nconst int32ArrayTag = '[object Int32Array]';\nconst bigInt64ArrayTag = '[object BigInt64Array]';\nconst float32ArrayTag = '[object Float32Array]';\nconst float64ArrayTag = '[object Float64Array]';\n\nexport { argumentsTag, arrayBufferTag, arrayTag, bigInt64ArrayTag, bigUint64ArrayTag, booleanTag, dataViewTag, dateTag, errorTag, float32ArrayTag, float64ArrayTag, functionTag, int16ArrayTag, int32ArrayTag, int8ArrayTag, mapTag, numberTag, objectTag, regexpTag, setTag, stringTag, symbolTag, uint16ArrayTag, uint32ArrayTag, uint8ArrayTag, uint8ClampedArrayTag };\n", "function isTypedArray(x) {\n return ArrayBuffer.isView(x) && !(x instanceof DataView);\n}\n\nexport { isTypedArray };\n", "import { getSymbols } from '../compat/_internal/getSymbols.mjs';\nimport { getTag } from '../compat/_internal/getTag.mjs';\nimport { uint32ArrayTag, uint16ArrayTag, uint8ClampedArrayTag, uint8ArrayTag, symbolTag, stringTag, setTag, regexpTag, objectTag, numberTag, mapTag, int32ArrayTag, int16ArrayTag, int8ArrayTag, float64ArrayTag, float32ArrayTag, dateTag, booleanTag, dataViewTag, arrayBufferTag, arrayTag, argumentsTag } from '../compat/_internal/tags.mjs';\nimport { isPrimitive } from '../predicate/isPrimitive.mjs';\nimport { isTypedArray } from '../predicate/isTypedArray.mjs';\n\nfunction cloneDeepWith(obj, cloneValue) {\n return cloneDeepWithImpl(obj, undefined, obj, new Map(), cloneValue);\n}\nfunction cloneDeepWithImpl(valueToClone, keyToClone, objectToClone, stack = new Map(), cloneValue = undefined) {\n const cloned = cloneValue?.(valueToClone, keyToClone, objectToClone, stack);\n if (cloned !== undefined) {\n return cloned;\n }\n if (isPrimitive(valueToClone)) {\n return valueToClone;\n }\n if (stack.has(valueToClone)) {\n return stack.get(valueToClone);\n }\n if (Array.isArray(valueToClone)) {\n const result = new Array(valueToClone.length);\n stack.set(valueToClone, result);\n for (let i = 0; i < valueToClone.length; i++) {\n result[i] = cloneDeepWithImpl(valueToClone[i], i, objectToClone, stack, cloneValue);\n }\n if (Object.hasOwn(valueToClone, 'index')) {\n result.index = valueToClone.index;\n }\n if (Object.hasOwn(valueToClone, 'input')) {\n result.input = valueToClone.input;\n }\n return result;\n }\n if (valueToClone instanceof Date) {\n return new Date(valueToClone.getTime());\n }\n if (valueToClone instanceof RegExp) {\n const result = new RegExp(valueToClone.source, valueToClone.flags);\n result.lastIndex = valueToClone.lastIndex;\n return result;\n }\n if (valueToClone instanceof Map) {\n const result = new Map();\n stack.set(valueToClone, result);\n for (const [key, value] of valueToClone) {\n result.set(key, cloneDeepWithImpl(value, key, objectToClone, stack, cloneValue));\n }\n return result;\n }\n if (valueToClone instanceof Set) {\n const result = new Set();\n stack.set(valueToClone, result);\n for (const value of valueToClone) {\n result.add(cloneDeepWithImpl(value, undefined, objectToClone, stack, cloneValue));\n }\n return result;\n }\n if (typeof Buffer !== 'undefined' && Buffer.isBuffer(valueToClone)) {\n return valueToClone.subarray();\n }\n if (isTypedArray(valueToClone)) {\n const result = new (Object.getPrototypeOf(valueToClone).constructor)(valueToClone.length);\n stack.set(valueToClone, result);\n for (let i = 0; i < valueToClone.length; i++) {\n result[i] = cloneDeepWithImpl(valueToClone[i], i, objectToClone, stack, cloneValue);\n }\n return result;\n }\n if (valueToClone instanceof ArrayBuffer ||\n (typeof SharedArrayBuffer !== 'undefined' && valueToClone instanceof SharedArrayBuffer)) {\n return valueToClone.slice(0);\n }\n if (valueToClone instanceof DataView) {\n const result = new DataView(valueToClone.buffer.slice(0), valueToClone.byteOffset, valueToClone.byteLength);\n stack.set(valueToClone, result);\n copyProperties(result, valueToClone, objectToClone, stack, cloneValue);\n return result;\n }\n if (typeof File !== 'undefined' && valueToClone instanceof File) {\n const result = new File([valueToClone], valueToClone.name, {\n type: valueToClone.type,\n });\n stack.set(valueToClone, result);\n copyProperties(result, valueToClone, objectToClone, stack, cloneValue);\n return result;\n }\n if (typeof Blob !== 'undefined' && valueToClone instanceof Blob) {\n const result = new Blob([valueToClone], { type: valueToClone.type });\n stack.set(valueToClone, result);\n copyProperties(result, valueToClone, objectToClone, stack, cloneValue);\n return result;\n }\n if (valueToClone instanceof Error) {\n const result = structuredClone(valueToClone);\n stack.set(valueToClone, result);\n result.message = valueToClone.message;\n result.name = valueToClone.name;\n result.stack = valueToClone.stack;\n result.cause = valueToClone.cause;\n result.constructor = valueToClone.constructor;\n copyProperties(result, valueToClone, objectToClone, stack, cloneValue);\n return result;\n }\n if (valueToClone instanceof Boolean) {\n const result = new Boolean(valueToClone.valueOf());\n stack.set(valueToClone, result);\n copyProperties(result, valueToClone, objectToClone, stack, cloneValue);\n return result;\n }\n if (valueToClone instanceof Number) {\n const result = new Number(valueToClone.valueOf());\n stack.set(valueToClone, result);\n copyProperties(result, valueToClone, objectToClone, stack, cloneValue);\n return result;\n }\n if (valueToClone instanceof String) {\n const result = new String(valueToClone.valueOf());\n stack.set(valueToClone, result);\n copyProperties(result, valueToClone, objectToClone, stack, cloneValue);\n return result;\n }\n if (typeof valueToClone === 'object' && isCloneableObject(valueToClone)) {\n const result = Object.create(Object.getPrototypeOf(valueToClone));\n stack.set(valueToClone, result);\n copyProperties(result, valueToClone, objectToClone, stack, cloneValue);\n return result;\n }\n return valueToClone;\n}\nfunction copyProperties(target, source, objectToClone = target, stack, cloneValue) {\n const keys = [...Object.keys(source), ...getSymbols(source)];\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const descriptor = Object.getOwnPropertyDescriptor(target, key);\n if (descriptor == null || descriptor.writable) {\n target[key] = cloneDeepWithImpl(source[key], key, objectToClone, stack, cloneValue);\n }\n }\n}\nfunction isCloneableObject(object) {\n switch (getTag(object)) {\n case argumentsTag:\n case arrayTag:\n case arrayBufferTag:\n case dataViewTag:\n case booleanTag:\n case dateTag:\n case float32ArrayTag:\n case float64ArrayTag:\n case int8ArrayTag:\n case int16ArrayTag:\n case int32ArrayTag:\n case mapTag:\n case numberTag:\n case objectTag:\n case regexpTag:\n case setTag:\n case stringTag:\n case symbolTag:\n case uint8ArrayTag:\n case uint8ClampedArrayTag:\n case uint16ArrayTag:\n case uint32ArrayTag: {\n return true;\n }\n default: {\n return false;\n }\n }\n}\n\nexport { cloneDeepWith, cloneDeepWithImpl, copyProperties };\n", "import { cloneDeepWithImpl } from './cloneDeepWith.mjs';\n\nfunction cloneDeep(obj) {\n return cloneDeepWithImpl(obj, undefined, obj, new Map(), undefined);\n}\n\nexport { cloneDeep };\n", "import { isMatch } from './isMatch.mjs';\nimport { cloneDeep } from '../../object/cloneDeep.mjs';\n\nfunction matches(source) {\n source = cloneDeep(source);\n return (target) => {\n return isMatch(target, source);\n };\n}\n\nexport { matches };\n", "import { cloneDeepWith as cloneDeepWith$1, copyProperties } from '../../object/cloneDeepWith.mjs';\nimport { getTag } from '../_internal/getTag.mjs';\nimport { objectTag, argumentsTag, booleanTag, stringTag, numberTag } from '../_internal/tags.mjs';\n\nfunction cloneDeepWith(obj, customizer) {\n return cloneDeepWith$1(obj, (value, key, object, stack) => {\n const cloned = customizer?.(value, key, object, stack);\n if (cloned !== undefined) {\n return cloned;\n }\n if (typeof obj !== 'object') {\n return undefined;\n }\n if (getTag(obj) === objectTag && typeof obj.constructor !== 'function') {\n const result = {};\n stack.set(obj, result);\n copyProperties(result, obj, object, stack);\n return result;\n }\n switch (Object.prototype.toString.call(obj)) {\n case numberTag:\n case stringTag:\n case booleanTag: {\n const result = new obj.constructor(obj?.valueOf());\n copyProperties(result, obj);\n return result;\n }\n case argumentsTag: {\n const result = {};\n copyProperties(result, obj);\n result.length = obj.length;\n result[Symbol.iterator] = obj[Symbol.iterator];\n return result;\n }\n default: {\n return undefined;\n }\n }\n });\n}\n\nexport { cloneDeepWith };\n", "import { cloneDeepWith } from './cloneDeepWith.mjs';\n\nfunction cloneDeep(obj) {\n return cloneDeepWith(obj);\n}\n\nexport { cloneDeep };\n", "const IS_UNSIGNED_INTEGER = /^(?:0|[1-9]\\d*)$/;\nfunction isIndex(value, length = Number.MAX_SAFE_INTEGER) {\n switch (typeof value) {\n case 'number': {\n return Number.isInteger(value) && value >= 0 && value < length;\n }\n case 'symbol': {\n return false;\n }\n case 'string': {\n return IS_UNSIGNED_INTEGER.test(value);\n }\n }\n}\n\nexport { isIndex };\n", "import { getTag } from '../_internal/getTag.mjs';\n\nfunction isArguments(value) {\n return value !== null && typeof value === 'object' && getTag(value) === '[object Arguments]';\n}\n\nexport { isArguments };\n", "import { isDeepKey } from '../_internal/isDeepKey.mjs';\nimport { isIndex } from '../_internal/isIndex.mjs';\nimport { isArguments } from '../predicate/isArguments.mjs';\nimport { toPath } from '../util/toPath.mjs';\n\nfunction has(object, path) {\n let resolvedPath;\n if (Array.isArray(path)) {\n resolvedPath = path;\n }\n else if (typeof path === 'string' && isDeepKey(path) && object?.[path] == null) {\n resolvedPath = toPath(path);\n }\n else {\n resolvedPath = [path];\n }\n if (resolvedPath.length === 0) {\n return false;\n }\n let current = object;\n for (let i = 0; i < resolvedPath.length; i++) {\n const key = resolvedPath[i];\n if (current == null || !Object.hasOwn(current, key)) {\n const isSparseIndex = (Array.isArray(current) || isArguments(current)) && isIndex(key) && key < current.length;\n if (!isSparseIndex) {\n return false;\n }\n }\n current = current[key];\n }\n return true;\n}\n\nexport { has };\n", "import { isMatch } from './isMatch.mjs';\nimport { toKey } from '../_internal/toKey.mjs';\nimport { cloneDeep } from '../object/cloneDeep.mjs';\nimport { get } from '../object/get.mjs';\nimport { has } from '../object/has.mjs';\n\nfunction matchesProperty(property, source) {\n switch (typeof property) {\n case 'object': {\n if (Object.is(property?.valueOf(), -0)) {\n property = '-0';\n }\n break;\n }\n case 'number': {\n property = toKey(property);\n break;\n }\n }\n source = cloneDeep(source);\n return function (target) {\n const result = get(target, property);\n if (result === undefined) {\n return has(target, property);\n }\n if (source === undefined) {\n return result === undefined;\n }\n return isMatch(result, source);\n };\n}\n\nexport { matchesProperty };\n", "import { identity } from '../../function/identity.mjs';\nimport { property } from '../object/property.mjs';\nimport { matches } from '../predicate/matches.mjs';\nimport { matchesProperty } from '../predicate/matchesProperty.mjs';\n\nfunction iteratee(value) {\n if (value == null) {\n return identity;\n }\n switch (typeof value) {\n case 'function': {\n return value;\n }\n case 'object': {\n if (Array.isArray(value) && value.length === 2) {\n return matchesProperty(value[0], value[1]);\n }\n return matches(value);\n }\n case 'string':\n case 'symbol':\n case 'number': {\n return property(value);\n }\n }\n}\n\nexport { iteratee };\n", "import { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction countBy(collection, iteratee$1) {\n if (collection == null) {\n return {};\n }\n const array = isArrayLike(collection) ? Array.from(collection) : Object.values(collection);\n const mapper = iteratee(iteratee$1 ?? undefined);\n const result = Object.create(null);\n for (let i = 0; i < array.length; i++) {\n const item = array[i];\n const key = mapper(item);\n result[key] = (result[key] ?? 0) + 1;\n }\n return result;\n}\n\nexport { countBy };\n", "function difference(firstArr, secondArr) {\n const secondSet = new Set(secondArr);\n return firstArr.filter(item => !secondSet.has(item));\n}\n\nexport { difference };\n", "function isObjectLike(value) {\n return typeof value === 'object' && value !== null;\n}\n\nexport { isObjectLike };\n", "import { isArrayLike } from './isArrayLike.mjs';\nimport { isObjectLike } from './isObjectLike.mjs';\n\nfunction isArrayLikeObject(value) {\n return isObjectLike(value) && isArrayLike(value);\n}\n\nexport { isArrayLikeObject };\n", "import { difference as difference$1 } from '../../array/difference.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\n\nfunction difference(arr, ...values) {\n if (!isArrayLikeObject(arr)) {\n return [];\n }\n const arr1 = toArray(arr);\n const arr2 = [];\n for (let i = 0; i < values.length; i++) {\n const value = values[i];\n if (isArrayLikeObject(value)) {\n arr2.push(...Array.from(value));\n }\n }\n return difference$1(arr1, arr2);\n}\n\nexport { difference };\n", "function last(arr) {\n return arr[arr.length - 1];\n}\n\nexport { last };\n", "import { last as last$1 } from '../../array/last.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction last(array) {\n if (!isArrayLike(array)) {\n return undefined;\n }\n return last$1(toArray(array));\n}\n\nexport { last };\n", "function differenceBy(firstArr, secondArr, mapper) {\n const mappedSecondSet = new Set(secondArr.map(item => mapper(item)));\n return firstArr.filter(item => {\n return !mappedSecondSet.has(mapper(item));\n });\n}\n\nexport { differenceBy };\n", "import { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\n\nfunction flattenArrayLike(values) {\n const result = [];\n for (let i = 0; i < values.length; i++) {\n const arrayLike = values[i];\n if (!isArrayLikeObject(arrayLike)) {\n continue;\n }\n for (let j = 0; j < arrayLike.length; j++) {\n result.push(arrayLike[j]);\n }\n }\n return result;\n}\n\nexport { flattenArrayLike };\n", "import { last } from './last.mjs';\nimport { difference } from '../../array/difference.mjs';\nimport { differenceBy as differenceBy$1 } from '../../array/differenceBy.mjs';\nimport { flattenArrayLike } from '../_internal/flattenArrayLike.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction differenceBy(arr, ..._values) {\n if (!isArrayLikeObject(arr)) {\n return [];\n }\n const iteratee$1 = last(_values);\n const values = flattenArrayLike(_values);\n if (isArrayLikeObject(iteratee$1)) {\n return difference(Array.from(arr), values);\n }\n return differenceBy$1(Array.from(arr), values, iteratee(iteratee$1));\n}\n\nexport { differenceBy };\n", "function differenceWith(firstArr, secondArr, areItemsEqual) {\n return firstArr.filter(firstItem => {\n return secondArr.every(secondItem => {\n return !areItemsEqual(firstItem, secondItem);\n });\n });\n}\n\nexport { differenceWith };\n", "import { last } from './last.mjs';\nimport { difference } from '../../array/difference.mjs';\nimport { differenceWith as differenceWith$1 } from '../../array/differenceWith.mjs';\nimport { flattenArrayLike } from '../_internal/flattenArrayLike.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\n\nfunction differenceWith(array, ...values) {\n if (!isArrayLikeObject(array)) {\n return [];\n }\n const comparator = last(values);\n const flattenedValues = flattenArrayLike(values);\n if (typeof comparator === 'function') {\n return differenceWith$1(Array.from(array), flattenedValues, comparator);\n }\n return difference(Array.from(array), flattenedValues);\n}\n\nexport { differenceWith };\n", "function drop(arr, itemsCount) {\n itemsCount = Math.max(itemsCount, 0);\n return arr.slice(itemsCount);\n}\n\nexport { drop };\n", "function isSymbol(value) {\n return typeof value === 'symbol' || value instanceof Symbol;\n}\n\nexport { isSymbol };\n", "import { isSymbol } from '../predicate/isSymbol.mjs';\n\nfunction toNumber(value) {\n if (isSymbol(value)) {\n return NaN;\n }\n return Number(value);\n}\n\nexport { toNumber };\n", "import { toNumber } from './toNumber.mjs';\n\nfunction toFinite(value) {\n if (!value) {\n return value === 0 ? value : 0;\n }\n value = toNumber(value);\n if (value === Infinity || value === -Infinity) {\n const sign = value < 0 ? -1 : 1;\n return sign * Number.MAX_VALUE;\n }\n return value === value ? value : 0;\n}\n\nexport { toFinite };\n", "import { toFinite } from './toFinite.mjs';\n\nfunction toInteger(value) {\n const finite = toFinite(value);\n const remainder = finite % 1;\n return remainder ? finite - remainder : finite;\n}\n\nexport { toInteger };\n", "import { drop as drop$1 } from '../../array/drop.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { toInteger } from '../util/toInteger.mjs';\n\nfunction drop(collection, itemsCount = 1, guard) {\n if (!isArrayLike(collection)) {\n return [];\n }\n itemsCount = guard ? 1 : toInteger(itemsCount);\n return drop$1(toArray(collection), itemsCount);\n}\n\nexport { drop };\n", "function dropRight(arr, itemsCount) {\n itemsCount = Math.min(-itemsCount, 0);\n if (itemsCount === 0) {\n return arr.slice();\n }\n return arr.slice(0, itemsCount);\n}\n\nexport { dropRight };\n", "import { dropRight as dropRight$1 } from '../../array/dropRight.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { toInteger } from '../util/toInteger.mjs';\n\nfunction dropRight(collection, itemsCount = 1, guard) {\n if (!isArrayLike(collection)) {\n return [];\n }\n itemsCount = guard ? 1 : toInteger(itemsCount);\n return dropRight$1(toArray(collection), itemsCount);\n}\n\nexport { dropRight };\n", "function dropRightWhile(arr, canContinueDropping) {\n for (let i = arr.length - 1; i >= 0; i--) {\n if (!canContinueDropping(arr[i], i, arr)) {\n return arr.slice(0, i + 1);\n }\n }\n return [];\n}\n\nexport { dropRightWhile };\n", "import { dropRightWhile as dropRightWhile$1 } from '../../array/dropRightWhile.mjs';\nimport { identity } from '../../function/identity.mjs';\nimport { property } from '../object/property.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { matches } from '../predicate/matches.mjs';\nimport { matchesProperty } from '../predicate/matchesProperty.mjs';\n\nfunction dropRightWhile(arr, predicate = identity) {\n if (!isArrayLike(arr)) {\n return [];\n }\n return dropRightWhileImpl(Array.from(arr), predicate);\n}\nfunction dropRightWhileImpl(arr, predicate) {\n switch (typeof predicate) {\n case 'function': {\n return dropRightWhile$1(arr, (item, index, arr) => Boolean(predicate(item, index, arr)));\n }\n case 'object': {\n if (Array.isArray(predicate) && predicate.length === 2) {\n const key = predicate[0];\n const value = predicate[1];\n return dropRightWhile$1(arr, matchesProperty(key, value));\n }\n else {\n return dropRightWhile$1(arr, matches(predicate));\n }\n }\n case 'symbol':\n case 'number':\n case 'string': {\n return dropRightWhile$1(arr, property(predicate));\n }\n }\n}\n\nexport { dropRightWhile };\n", "function dropWhile(arr, canContinueDropping) {\n const dropEndIndex = arr.findIndex((item, index, arr) => !canContinueDropping(item, index, arr));\n if (dropEndIndex === -1) {\n return [];\n }\n return arr.slice(dropEndIndex);\n}\n\nexport { dropWhile };\n", "import { dropWhile as dropWhile$1 } from '../../array/dropWhile.mjs';\nimport { identity } from '../../function/identity.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { property } from '../object/property.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { matches } from '../predicate/matches.mjs';\nimport { matchesProperty } from '../predicate/matchesProperty.mjs';\n\nfunction dropWhile(arr, predicate = identity) {\n if (!isArrayLike(arr)) {\n return [];\n }\n return dropWhileImpl(toArray(arr), predicate);\n}\nfunction dropWhileImpl(arr, predicate) {\n switch (typeof predicate) {\n case 'function': {\n return dropWhile$1(arr, (item, index, arr) => Boolean(predicate(item, index, arr)));\n }\n case 'object': {\n if (Array.isArray(predicate) && predicate.length === 2) {\n const key = predicate[0];\n const value = predicate[1];\n return dropWhile$1(arr, matchesProperty(key, value));\n }\n else {\n return dropWhile$1(arr, matches(predicate));\n }\n }\n case 'number':\n case 'symbol':\n case 'string': {\n return dropWhile$1(arr, property(predicate));\n }\n }\n}\n\nexport { dropWhile };\n", "function range(start, end, step = 1) {\n if (end == null) {\n end = start;\n start = 0;\n }\n if (!Number.isInteger(step) || step === 0) {\n throw new Error(`The step value must be a non-zero integer.`);\n }\n const length = Math.max(Math.ceil((end - start) / step), 0);\n const result = new Array(length);\n for (let i = 0; i < length; i++) {\n result[i] = start + i * step;\n }\n return result;\n}\n\nexport { range };\n", "import { identity } from '../../function/identity.mjs';\nimport { range } from '../../math/range.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction forEach(collection, callback = identity) {\n if (!collection) {\n return collection;\n }\n const keys = isArrayLike(collection) || Array.isArray(collection) ? range(0, collection.length) : Object.keys(collection);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const value = collection[key];\n const result = callback(value, key, collection);\n if (result === false) {\n break;\n }\n }\n return collection;\n}\n\nexport { forEach };\n", "import { identity } from '../../function/identity.mjs';\nimport { range } from '../../math/range.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction forEachRight(collection, callback = identity) {\n if (!collection) {\n return collection;\n }\n const keys = isArrayLike(collection) ? range(0, collection.length) : Object.keys(collection);\n for (let i = keys.length - 1; i >= 0; i--) {\n const key = keys[i];\n const value = collection[key];\n const result = callback(value, key, collection);\n if (result === false) {\n break;\n }\n }\n return collection;\n}\n\nexport { forEachRight };\n", "import { isIndex } from './isIndex.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { isObject } from '../predicate/isObject.mjs';\nimport { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\n\nfunction isIterateeCall(value, index, object) {\n if (!isObject(object)) {\n return false;\n }\n if ((typeof index === 'number' && isArrayLike(object) && isIndex(index) && index < object.length) ||\n (typeof index === 'string' && index in object)) {\n return isEqualsSameValueZero(object[index], value);\n }\n return false;\n}\n\nexport { isIterateeCall };\n", "import { identity } from '../../function/identity.mjs';\nimport { isIterateeCall } from '../_internal/isIterateeCall.mjs';\nimport { property } from '../object/property.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { matches } from '../predicate/matches.mjs';\nimport { matchesProperty } from '../predicate/matchesProperty.mjs';\n\nfunction every(source, doesMatch, guard) {\n if (!source) {\n return true;\n }\n if (guard && isIterateeCall(source, doesMatch, guard)) {\n doesMatch = undefined;\n }\n if (!doesMatch) {\n doesMatch = identity;\n }\n let predicate;\n switch (typeof doesMatch) {\n case 'function': {\n predicate = doesMatch;\n break;\n }\n case 'object': {\n if (Array.isArray(doesMatch) && doesMatch.length === 2) {\n const key = doesMatch[0];\n const value = doesMatch[1];\n predicate = matchesProperty(key, value);\n }\n else {\n predicate = matches(doesMatch);\n }\n break;\n }\n case 'symbol':\n case 'number':\n case 'string': {\n predicate = property(doesMatch);\n }\n }\n if (!isArrayLike(source)) {\n const keys = Object.keys(source);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const value = source[key];\n if (!predicate(value, key, source)) {\n return false;\n }\n }\n return true;\n }\n for (let i = 0; i < source.length; i++) {\n if (!predicate(source[i], i, source)) {\n return false;\n }\n }\n return true;\n}\n\nexport { every };\n", "function fill(array, value, start = 0, end = array.length) {\n const length = array.length;\n const finalStart = Math.max(start >= 0 ? start : length + start, 0);\n const finalEnd = Math.min(end >= 0 ? end : length + end, length);\n for (let i = finalStart; i < finalEnd; i++) {\n array[i] = value;\n }\n return array;\n}\n\nexport { fill };\n", "function isString(value) {\n return typeof value === 'string' || value instanceof String;\n}\n\nexport { isString };\n", "import { fill as fill$1 } from '../../array/fill.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { isString } from '../predicate/isString.mjs';\n\nfunction fill(array, value, start = 0, end = array ? array.length : 0) {\n if (!isArrayLike(array)) {\n return [];\n }\n if (isString(array)) {\n return array;\n }\n start = Math.floor(start);\n end = Math.floor(end);\n if (!start) {\n start = 0;\n }\n if (!end) {\n end = 0;\n }\n return fill$1(array, value, start, end);\n}\n\nexport { fill };\n", "import { identity } from '../../function/identity.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction filter(source, predicate = identity) {\n if (!source) {\n return [];\n }\n predicate = iteratee(predicate);\n if (!Array.isArray(source)) {\n const result = [];\n const keys = Object.keys(source);\n const length = isArrayLike(source) ? source.length : keys.length;\n for (let i = 0; i < length; i++) {\n const key = keys[i];\n const value = source[key];\n if (predicate(value, key, source)) {\n result.push(value);\n }\n }\n return result;\n }\n const result = [];\n const length = source.length;\n for (let i = 0; i < length; i++) {\n const value = source[i];\n if (predicate(value, i, source)) {\n result.push(value);\n }\n }\n return result;\n}\n\nexport { filter };\n", "import { identity } from '../../function/identity.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction find(source, _doesMatch = identity, fromIndex = 0) {\n if (!source) {\n return undefined;\n }\n if (fromIndex < 0) {\n fromIndex = Math.max(source.length + fromIndex, 0);\n }\n const doesMatch = iteratee(_doesMatch);\n if (!Array.isArray(source)) {\n const keys = Object.keys(source);\n for (let i = fromIndex; i < keys.length; i++) {\n const key = keys[i];\n const value = source[key];\n if (doesMatch(value, key, source)) {\n return value;\n }\n }\n return undefined;\n }\n return source.slice(fromIndex).find(doesMatch);\n}\n\nexport { find };\n", "function identity(x) {\n return x;\n}\n\nexport { identity };\n", "import { identity } from '../function/identity.mjs';\nimport { property } from '../object/property.mjs';\nimport { matches } from '../predicate/matches.mjs';\nimport { matchesProperty } from '../predicate/matchesProperty.mjs';\n\nfunction findIndex(arr, doesMatch = identity, fromIndex = 0) {\n if (!arr) {\n return -1;\n }\n if (fromIndex < 0) {\n fromIndex = Math.max(arr.length + fromIndex, 0);\n }\n const subArray = Array.from(arr).slice(fromIndex);\n let index = -1;\n switch (typeof doesMatch) {\n case 'function': {\n index = subArray.findIndex(doesMatch);\n break;\n }\n case 'object': {\n if (Array.isArray(doesMatch) && doesMatch.length === 2) {\n const key = doesMatch[0];\n const value = doesMatch[1];\n index = subArray.findIndex(matchesProperty(key, value));\n }\n else {\n index = subArray.findIndex(matches(doesMatch));\n }\n break;\n }\n case 'number':\n case 'symbol':\n case 'string': {\n index = subArray.findIndex(property(doesMatch));\n }\n }\n return index === -1 ? -1 : index + fromIndex;\n}\n\nexport { findIndex };\n", "import { identity } from '../../function/identity.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\nimport { toInteger } from '../util/toInteger.mjs';\n\nfunction findLast(source, _doesMatch = identity, fromIndex) {\n if (!source) {\n return undefined;\n }\n const length = Array.isArray(source) ? source.length : Object.keys(source).length;\n fromIndex = toInteger(fromIndex ?? length - 1);\n if (fromIndex < 0) {\n fromIndex = Math.max(length + fromIndex, 0);\n }\n else {\n fromIndex = Math.min(fromIndex, length - 1);\n }\n const doesMatch = iteratee(_doesMatch);\n if (!Array.isArray(source)) {\n const keys = Object.keys(source);\n for (let i = fromIndex; i >= 0; i--) {\n const key = keys[i];\n const value = source[key];\n if (doesMatch(value, key, source)) {\n return value;\n }\n }\n return undefined;\n }\n return source.slice(0, fromIndex + 1).findLast(doesMatch);\n}\n\nexport { findLast };\n", "import { identity } from '../../function/identity.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { property } from '../object/property.mjs';\nimport { matches } from '../predicate/matches.mjs';\nimport { matchesProperty } from '../predicate/matchesProperty.mjs';\n\nfunction findLastIndex(arr, doesMatch = identity, fromIndex = arr ? arr.length - 1 : 0) {\n if (!arr) {\n return -1;\n }\n if (fromIndex < 0) {\n fromIndex = Math.max(arr.length + fromIndex, 0);\n }\n else {\n fromIndex = Math.min(fromIndex, arr.length - 1);\n }\n const subArray = toArray(arr).slice(0, fromIndex + 1);\n switch (typeof doesMatch) {\n case 'function': {\n return subArray.findLastIndex(doesMatch);\n }\n case 'object': {\n if (Array.isArray(doesMatch) && doesMatch.length === 2) {\n const key = doesMatch[0];\n const value = doesMatch[1];\n return subArray.findLastIndex(matchesProperty(key, value));\n }\n else {\n return subArray.findLastIndex(matches(doesMatch));\n }\n }\n case 'number':\n case 'symbol':\n case 'string': {\n return subArray.findLastIndex(property(doesMatch));\n }\n }\n}\n\nexport { findLastIndex };\n", "function head(arr) {\n return arr[0];\n}\n\nexport { head };\n", "import { head as head$1 } from '../../array/head.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction head(arr) {\n if (!isArrayLike(arr)) {\n return undefined;\n }\n return head$1(toArray(arr));\n}\n\nexport { head };\n", "import { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction flatten(value, depth = 1) {\n const result = [];\n const flooredDepth = Math.floor(depth);\n if (!isArrayLike(value)) {\n return result;\n }\n const recursive = (arr, currentDepth) => {\n for (let i = 0; i < arr.length; i++) {\n const item = arr[i];\n if (currentDepth < flooredDepth &&\n (Array.isArray(item) ||\n Boolean(item?.[Symbol.isConcatSpreadable]) ||\n (item !== null && typeof item === 'object' && Object.prototype.toString.call(item) === '[object Arguments]'))) {\n if (Array.isArray(item)) {\n recursive(item, currentDepth + 1);\n }\n else {\n recursive(Array.from(item), currentDepth + 1);\n }\n }\n else {\n result.push(item);\n }\n }\n };\n recursive(Array.from(value), 0);\n return result;\n}\n\nexport { flatten };\n", "import { flatten } from './flatten.mjs';\n\nfunction flattenDepth(array, depth = 1) {\n return flatten(array, depth);\n}\n\nexport { flattenDepth };\n", "import { identity } from '../../function/identity.mjs';\nimport { range } from '../../math/range.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction map(collection, _iteratee) {\n if (!collection) {\n return [];\n }\n const keys = isArrayLike(collection) || Array.isArray(collection) ? range(0, collection.length) : Object.keys(collection);\n const iteratee$1 = iteratee(_iteratee ?? identity);\n const result = new Array(keys.length);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const value = collection[key];\n result[i] = iteratee$1(value, key, collection);\n }\n return result;\n}\n\nexport { map };\n", "function isNil(x) {\n return x == null;\n}\n\nexport { isNil };\n", "import { flattenDepth } from './flattenDepth.mjs';\nimport { map } from './map.mjs';\nimport { isNil } from '../../predicate/isNil.mjs';\n\nfunction flatMap(collection, iteratee) {\n if (isNil(collection)) {\n return [];\n }\n const mapped = isNil(iteratee) ? map(collection) : map(collection, iteratee);\n return flattenDepth(mapped, 1);\n}\n\nexport { flatMap };\n", "import { flatten } from './flatten.mjs';\nimport { map } from './map.mjs';\nimport { identity } from '../../function/identity.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction flatMapDepth(collection, iteratee$1 = identity, depth = 1) {\n if (collection == null) {\n return [];\n }\n const iterateeFn = iteratee(iteratee$1);\n const mapped = map(collection, iterateeFn);\n return flatten(mapped, depth);\n}\n\nexport { flatMapDepth };\n", "import { flatMapDepth } from './flatMapDepth.mjs';\n\nfunction flatMapDeep(collection, iteratee) {\n return flatMapDepth(collection, iteratee, Infinity);\n}\n\nexport { flatMapDeep };\n", "import { flattenDepth } from './flattenDepth.mjs';\n\nfunction flattenDeep(value) {\n return flattenDepth(value, Infinity);\n}\n\nexport { flattenDeep };\n", "function groupBy(arr, getKeyFromItem) {\n const result = {};\n for (let i = 0; i < arr.length; i++) {\n const item = arr[i];\n const key = getKeyFromItem(item, i, arr);\n if (!Object.hasOwn(result, key)) {\n result[key] = [];\n }\n result[key].push(item);\n }\n return result;\n}\n\nexport { groupBy };\n", "import { groupBy as groupBy$1 } from '../../array/groupBy.mjs';\nimport { identity } from '../../function/identity.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction groupBy(source, _getKeyFromItem) {\n if (source == null) {\n return {};\n }\n const items = isArrayLike(source) ? Array.from(source) : Object.values(source);\n const getKeyFromItem = iteratee(_getKeyFromItem ?? identity);\n return groupBy$1(items, getKeyFromItem);\n}\n\nexport { groupBy };\n", "import { isString } from '../predicate/isString.mjs';\nimport { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\nimport { toInteger } from '../util/toInteger.mjs';\n\nfunction includes(source, target, fromIndex, guard) {\n if (source == null) {\n return false;\n }\n if (guard || !fromIndex) {\n fromIndex = 0;\n }\n else {\n fromIndex = toInteger(fromIndex);\n }\n if (isString(source)) {\n if (fromIndex > source.length || target instanceof RegExp) {\n return false;\n }\n if (fromIndex < 0) {\n fromIndex = Math.max(0, source.length + fromIndex);\n }\n return source.includes(target, fromIndex);\n }\n if (Array.isArray(source)) {\n return source.includes(target, fromIndex);\n }\n const keys = Object.keys(source);\n if (fromIndex < 0) {\n fromIndex = Math.max(0, keys.length + fromIndex);\n }\n for (let i = fromIndex; i < keys.length; i++) {\n const value = Reflect.get(source, keys[i]);\n if (isEqualsSameValueZero(value, target)) {\n return true;\n }\n }\n return false;\n}\n\nexport { includes };\n", "import { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction indexOf(array, searchElement, fromIndex) {\n if (!isArrayLike(array)) {\n return -1;\n }\n if (Number.isNaN(searchElement)) {\n fromIndex = fromIndex ?? 0;\n if (fromIndex < 0) {\n fromIndex = Math.max(0, array.length + fromIndex);\n }\n for (let i = fromIndex; i < array.length; i++) {\n if (Number.isNaN(array[i])) {\n return i;\n }\n }\n return -1;\n }\n return Array.from(array).indexOf(searchElement, fromIndex);\n}\n\nexport { indexOf };\n", "function initial(arr) {\n return arr.slice(0, -1);\n}\n\nexport { initial };\n", "import { initial as initial$1 } from '../../array/initial.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction initial(arr) {\n if (!isArrayLike(arr)) {\n return [];\n }\n return initial$1(Array.from(arr));\n}\n\nexport { initial };\n", "function intersection(firstArr, secondArr) {\n const secondSet = new Set(secondArr);\n return firstArr.filter(item => secondSet.has(item));\n}\n\nexport { intersection };\n", "function uniq(arr) {\n return [...new Set(arr)];\n}\n\nexport { uniq };\n", "import { intersection as intersection$1 } from '../../array/intersection.mjs';\nimport { uniq } from '../../array/uniq.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\n\nfunction intersection(...arrays) {\n if (arrays.length === 0) {\n return [];\n }\n if (!isArrayLikeObject(arrays[0])) {\n return [];\n }\n let result = uniq(Array.from(arrays[0]));\n for (let i = 1; i < arrays.length; i++) {\n const array = arrays[i];\n if (!isArrayLikeObject(array)) {\n return [];\n }\n result = intersection$1(result, Array.from(array));\n }\n return result;\n}\n\nexport { intersection };\n", "function intersectionBy(firstArr, secondArr, mapper) {\n const result = [];\n const mappedSecondSet = new Set(secondArr.map(mapper));\n for (let i = 0; i < firstArr.length; i++) {\n const item = firstArr[i];\n const mappedItem = mapper(item);\n if (mappedSecondSet.has(mappedItem)) {\n result.push(item);\n mappedSecondSet.delete(mappedItem);\n }\n }\n return result;\n}\n\nexport { intersectionBy };\n", "import { intersectionBy as intersectionBy$1 } from '../../array/intersectionBy.mjs';\nimport { last } from '../../array/last.mjs';\nimport { uniq } from '../../array/uniq.mjs';\nimport { identity } from '../../function/identity.mjs';\nimport { property } from '../object/property.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\n\nfunction intersectionBy(array, ...values) {\n if (!isArrayLikeObject(array)) {\n return [];\n }\n const lastValue = last(values);\n if (lastValue === undefined) {\n return Array.from(array);\n }\n let result = uniq(Array.from(array));\n const count = isArrayLikeObject(lastValue) ? values.length : values.length - 1;\n for (let i = 0; i < count; ++i) {\n const value = values[i];\n if (!isArrayLikeObject(value)) {\n return [];\n }\n if (isArrayLikeObject(lastValue)) {\n result = intersectionBy$1(result, Array.from(value), identity);\n }\n else if (typeof lastValue === 'function') {\n result = intersectionBy$1(result, Array.from(value), value => lastValue(value));\n }\n else if (typeof lastValue === 'string') {\n result = intersectionBy$1(result, Array.from(value), property(lastValue));\n }\n }\n return result;\n}\n\nexport { intersectionBy };\n", "function intersectionWith(firstArr, secondArr, areItemsEqual) {\n return firstArr.filter(firstItem => {\n return secondArr.some(secondItem => {\n return areItemsEqual(firstItem, secondItem);\n });\n });\n}\n\nexport { intersectionWith };\n", "import { uniq as uniq$1 } from '../../array/uniq.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction uniq(arr) {\n if (!isArrayLike(arr)) {\n return [];\n }\n return uniq$1(Array.from(arr));\n}\n\nexport { uniq };\n", "import { last } from './last.mjs';\nimport { intersectionWith as intersectionWith$1 } from '../../array/intersectionWith.mjs';\nimport { uniq } from './uniq.mjs';\nimport { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\n\nfunction intersectionWith(firstArr, ...otherArrs) {\n if (firstArr == null) {\n return [];\n }\n const _comparator = last(otherArrs);\n let comparator = isEqualsSameValueZero;\n let uniq$1 = uniq;\n if (typeof _comparator === 'function') {\n comparator = _comparator;\n uniq$1 = uniqPreserve0;\n otherArrs.pop();\n }\n let result = uniq$1(Array.from(firstArr));\n for (let i = 0; i < otherArrs.length; ++i) {\n const otherArr = otherArrs[i];\n if (otherArr == null) {\n return [];\n }\n result = intersectionWith$1(result, Array.from(otherArr), comparator);\n }\n return result;\n}\nfunction uniqPreserve0(arr) {\n const result = [];\n const added = new Set();\n for (let i = 0; i < arr.length; i++) {\n const item = arr[i];\n if (added.has(item)) {\n continue;\n }\n result.push(item);\n added.add(item);\n }\n return result;\n}\n\nexport { intersectionWith };\n", "function isFunction(value) {\n return typeof value === 'function';\n}\n\nexport { isFunction };\n", "import { isFunction } from '../../predicate/isFunction.mjs';\nimport { isNil } from '../../predicate/isNil.mjs';\nimport { get } from '../object/get.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction invokeMap(collection, path, ...args) {\n if (isNil(collection)) {\n return [];\n }\n const values = isArrayLike(collection) ? Array.from(collection) : Object.values(collection);\n const result = [];\n for (let i = 0; i < values.length; i++) {\n const value = values[i];\n if (isFunction(path)) {\n result.push(path.apply(value, args));\n continue;\n }\n const method = get(value, path);\n let thisContext = value;\n if (Array.isArray(path)) {\n const pathExceptLast = path.slice(0, -1);\n if (pathExceptLast.length > 0) {\n thisContext = get(value, pathExceptLast);\n }\n }\n else if (typeof path === 'string' && path.includes('.')) {\n const parts = path.split('.');\n const pathExceptLast = parts.slice(0, -1).join('.');\n thisContext = get(value, pathExceptLast);\n }\n result.push(method == null ? undefined : method.apply(thisContext, args));\n }\n return result;\n}\n\nexport { invokeMap };\n", "import { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction join(array, separator) {\n if (!isArrayLike(array)) {\n return '';\n }\n return Array.from(array).join(separator);\n}\n\nexport { join };\n", "import { identity } from '../../function/identity.mjs';\nimport { range } from '../../math/range.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction reduce(collection, iteratee = identity, accumulator) {\n if (!collection) {\n return accumulator;\n }\n let keys;\n let startIndex = 0;\n if (isArrayLike(collection)) {\n keys = range(0, collection.length);\n if (accumulator == null && collection.length > 0) {\n accumulator = collection[0];\n startIndex += 1;\n }\n }\n else {\n keys = Object.keys(collection);\n if (accumulator == null) {\n accumulator = collection[keys[0]];\n startIndex += 1;\n }\n }\n for (let i = startIndex; i < keys.length; i++) {\n const key = keys[i];\n const value = collection[key];\n accumulator = iteratee(accumulator, value, key, collection);\n }\n return accumulator;\n}\n\nexport { reduce };\n", "import { reduce } from './reduce.mjs';\nimport { identity } from '../../function/identity.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { isObjectLike } from '../predicate/isObjectLike.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction keyBy(collection, iteratee$1) {\n if (!isArrayLike(collection) && !isObjectLike(collection)) {\n return {};\n }\n const keyFn = iteratee(iteratee$1 ?? identity);\n return reduce(collection, (result, value) => {\n const key = keyFn(value);\n result[key] = value;\n return result;\n }, {});\n}\n\nexport { keyBy };\n", "import { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction lastIndexOf(array, searchElement, fromIndex) {\n if (!isArrayLike(array) || array.length === 0) {\n return -1;\n }\n const length = array.length;\n let index = fromIndex ?? length - 1;\n if (fromIndex != null) {\n index = index < 0 ? Math.max(length + index, 0) : Math.min(index, length - 1);\n }\n if (Number.isNaN(searchElement)) {\n for (let i = index; i >= 0; i--) {\n if (Number.isNaN(array[i])) {\n return i;\n }\n }\n }\n return Array.from(array).lastIndexOf(searchElement, index);\n}\n\nexport { lastIndexOf };\n", "import { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\nimport { toInteger } from '../util/toInteger.mjs';\n\nfunction nth(array, n = 0) {\n if (!isArrayLikeObject(array) || array.length === 0) {\n return undefined;\n }\n n = toInteger(n);\n if (n < 0) {\n n += array.length;\n }\n return array[n];\n}\n\nexport { nth };\n", "function getPriority(a) {\n if (typeof a === 'symbol') {\n return 1;\n }\n if (a === null) {\n return 2;\n }\n if (a === undefined) {\n return 3;\n }\n if (a !== a) {\n return 4;\n }\n return 0;\n}\nconst compareValues = (a, b, order) => {\n if (a !== b) {\n const aPriority = getPriority(a);\n const bPriority = getPriority(b);\n if (aPriority === bPriority && aPriority === 0) {\n if (a < b) {\n return order === 'desc' ? 1 : -1;\n }\n if (a > b) {\n return order === 'desc' ? -1 : 1;\n }\n }\n return order === 'desc' ? bPriority - aPriority : aPriority - bPriority;\n }\n return 0;\n};\n\nexport { compareValues };\n", "import { isSymbol } from '../predicate/isSymbol.mjs';\n\nconst regexIsDeepProp = /\\.|\\[(?:[^[\\]]*|([\"'])(?:(?!\\1)[^\\\\]|\\\\.)*?\\1)\\]/;\nconst regexIsPlainProp = /^\\w*$/;\nfunction isKey(value, object) {\n if (Array.isArray(value)) {\n return false;\n }\n if (typeof value === 'number' || typeof value === 'boolean' || value == null || isSymbol(value)) {\n return true;\n }\n return ((typeof value === 'string' && (regexIsPlainProp.test(value) || !regexIsDeepProp.test(value))) ||\n (object != null && Object.hasOwn(object, value)));\n}\n\nexport { isKey };\n", "import { compareValues } from '../_internal/compareValues.mjs';\nimport { isKey } from '../_internal/isKey.mjs';\nimport { toPath } from '../util/toPath.mjs';\n\nfunction orderBy(collection, criteria, orders, guard) {\n if (collection == null) {\n return [];\n }\n orders = guard ? undefined : orders;\n if (!Array.isArray(collection)) {\n collection = Object.values(collection);\n }\n if (!Array.isArray(criteria)) {\n criteria = criteria == null ? [null] : [criteria];\n }\n if (criteria.length === 0) {\n criteria = [null];\n }\n if (!Array.isArray(orders)) {\n orders = orders == null ? [] : [orders];\n }\n orders = orders.map(order => String(order));\n const getValueByNestedPath = (object, path) => {\n let target = object;\n for (let i = 0; i < path.length && target != null; ++i) {\n target = target[path[i]];\n }\n return target;\n };\n const getValueByCriterion = (criterion, object) => {\n if (object == null || criterion == null) {\n return object;\n }\n if (typeof criterion === 'object' && 'key' in criterion) {\n if (Object.hasOwn(object, criterion.key)) {\n return object[criterion.key];\n }\n return getValueByNestedPath(object, criterion.path);\n }\n if (typeof criterion === 'function') {\n return criterion(object);\n }\n if (Array.isArray(criterion)) {\n return getValueByNestedPath(object, criterion);\n }\n if (typeof object === 'object') {\n return object[criterion];\n }\n return object;\n };\n const preparedCriteria = criteria.map((criterion) => {\n if (Array.isArray(criterion) && criterion.length === 1) {\n criterion = criterion[0];\n }\n if (criterion == null || typeof criterion === 'function' || Array.isArray(criterion) || isKey(criterion)) {\n return criterion;\n }\n return { key: criterion, path: toPath(criterion) };\n });\n const preparedCollection = collection.map(item => ({\n original: item,\n criteria: preparedCriteria.map((criterion) => getValueByCriterion(criterion, item)),\n }));\n return preparedCollection\n .slice()\n .sort((a, b) => {\n for (let i = 0; i < preparedCriteria.length; i++) {\n const comparedResult = compareValues(a.criteria[i], b.criteria[i], orders[i]);\n if (comparedResult !== 0) {\n return comparedResult;\n }\n }\n return 0;\n })\n .map(item => item.original);\n}\n\nexport { orderBy };\n", "import { identity } from '../../function/identity.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction partition(source, predicate = identity) {\n if (!source) {\n return [[], []];\n }\n const collection = isArrayLike(source) ? source : Object.values(source);\n predicate = iteratee(predicate);\n const matched = [];\n const unmatched = [];\n for (let i = 0; i < collection.length; i++) {\n const value = collection[i];\n if (predicate(value)) {\n matched.push(value);\n }\n else {\n unmatched.push(value);\n }\n }\n return [matched, unmatched];\n}\n\nexport { partition };\n", "function pull(arr, valuesToRemove) {\n const valuesSet = new Set(valuesToRemove);\n let resultIndex = 0;\n for (let i = 0; i < arr.length; i++) {\n if (valuesSet.has(arr[i])) {\n continue;\n }\n if (!Object.hasOwn(arr, i)) {\n delete arr[resultIndex++];\n continue;\n }\n arr[resultIndex++] = arr[i];\n }\n arr.length = resultIndex;\n return arr;\n}\n\nexport { pull };\n", "import { pull as pull$1 } from '../../array/pull.mjs';\n\nfunction pull(arr, ...valuesToRemove) {\n return pull$1(arr, valuesToRemove);\n}\n\nexport { pull };\n", "import { pull } from '../../array/pull.mjs';\n\nfunction pullAll(arr, valuesToRemove = []) {\n return pull(arr, Array.from(valuesToRemove));\n}\n\nexport { pullAll };\n", "import { iteratee } from '../util/iteratee.mjs';\n\nfunction pullAllBy(arr, valuesToRemove, _getValue) {\n const getValue = iteratee(_getValue);\n const valuesSet = new Set(Array.from(valuesToRemove).map(x => getValue(x)));\n let resultIndex = 0;\n for (let i = 0; i < arr.length; i++) {\n const value = getValue(arr[i]);\n if (valuesSet.has(value)) {\n continue;\n }\n if (!Object.hasOwn(arr, i)) {\n delete arr[resultIndex++];\n continue;\n }\n arr[resultIndex++] = arr[i];\n }\n arr.length = resultIndex;\n return arr;\n}\n\nexport { pullAllBy };\n", "function copyArray(source, array) {\n const length = source.length;\n if (array == null) {\n array = Array(length);\n }\n for (let i = 0; i < length; i++) {\n array[i] = source[i];\n }\n return array;\n}\n\nexport { copyArray as default };\n", "import copyArray from '../_internal/copyArray.mjs';\nimport { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\n\nfunction pullAllWith(array, values, comparator) {\n if (array?.length == null || values?.length == null) {\n return array;\n }\n if (array === values) {\n values = copyArray(values);\n }\n let resultLength = 0;\n if (comparator == null) {\n comparator = (a, b) => isEqualsSameValueZero(a, b);\n }\n const valuesArray = Array.isArray(values) ? values : Array.from(values);\n const hasUndefined = valuesArray.includes(undefined);\n for (let i = 0; i < array.length; i++) {\n if (i in array) {\n const shouldRemove = valuesArray.some(value => comparator(array[i], value));\n if (!shouldRemove) {\n array[resultLength++] = array[i];\n }\n continue;\n }\n if (!hasUndefined) {\n delete array[resultLength++];\n }\n }\n array.length = resultLength;\n return array;\n}\n\nexport { pullAllWith };\n", "import { get } from './get.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { isString } from '../predicate/isString.mjs';\n\nfunction at(object, ...paths) {\n if (paths.length === 0) {\n return [];\n }\n const allPaths = [];\n for (let i = 0; i < paths.length; i++) {\n const path = paths[i];\n if (!isArrayLike(path) || isString(path)) {\n allPaths.push(path);\n continue;\n }\n for (let j = 0; j < path.length; j++) {\n allPaths.push(path[j]);\n }\n }\n const result = [];\n for (let i = 0; i < allPaths.length; i++) {\n result.push(get(object, allPaths[i]));\n }\n return result;\n}\n\nexport { at };\n", "import { get } from './get.mjs';\nimport { isUnsafeProperty } from '../../_internal/isUnsafeProperty.mjs';\nimport { isDeepKey } from '../_internal/isDeepKey.mjs';\nimport { toKey } from '../_internal/toKey.mjs';\nimport { toPath } from '../util/toPath.mjs';\n\nfunction unset(obj, path) {\n if (obj == null) {\n return true;\n }\n switch (typeof path) {\n case 'symbol':\n case 'number':\n case 'object': {\n if (Array.isArray(path)) {\n return unsetWithPath(obj, path);\n }\n if (typeof path === 'number') {\n path = toKey(path);\n }\n else if (typeof path === 'object') {\n if (Object.is(path?.valueOf(), -0)) {\n path = '-0';\n }\n else {\n path = String(path);\n }\n }\n if (isUnsafeProperty(path)) {\n return false;\n }\n if (obj?.[path] === undefined) {\n return true;\n }\n try {\n delete obj[path];\n return true;\n }\n catch {\n return false;\n }\n }\n case 'string': {\n if (obj?.[path] === undefined && isDeepKey(path)) {\n return unsetWithPath(obj, toPath(path));\n }\n if (isUnsafeProperty(path)) {\n return false;\n }\n try {\n delete obj[path];\n return true;\n }\n catch {\n return false;\n }\n }\n }\n}\nfunction unsetWithPath(obj, path) {\n const parent = path.length === 1 ? obj : get(obj, path.slice(0, -1));\n const lastKey = path[path.length - 1];\n if (parent?.[lastKey] === undefined) {\n return true;\n }\n if (isUnsafeProperty(lastKey)) {\n return false;\n }\n try {\n delete parent[lastKey];\n return true;\n }\n catch {\n return false;\n }\n}\n\nexport { unset };\n", "function isArray(value) {\n return Array.isArray(value);\n}\n\nexport { isArray };\n", "import { flattenDepth } from './flattenDepth.mjs';\nimport { isIndex } from '../_internal/isIndex.mjs';\nimport { isKey } from '../_internal/isKey.mjs';\nimport { toKey } from '../_internal/toKey.mjs';\nimport { at } from '../object/at.mjs';\nimport { unset } from '../object/unset.mjs';\nimport { isArray } from '../predicate/isArray.mjs';\nimport { toPath } from '../util/toPath.mjs';\n\nfunction pullAt(array, ..._indices) {\n const indices = flattenDepth(_indices, 1);\n if (!array) {\n return Array(indices.length);\n }\n const result = at(array, indices);\n const indicesToPull = indices\n .map(index => (isIndex(index, array.length) ? Number(index) : index))\n .sort((a, b) => b - a);\n for (const index of new Set(indicesToPull)) {\n if (isIndex(index, array.length)) {\n Array.prototype.splice.call(array, index, 1);\n continue;\n }\n if (isKey(index, array)) {\n delete array[toKey(index)];\n continue;\n }\n const path = isArray(index) ? index : toPath(index);\n unset(array, path);\n }\n return result;\n}\n\nexport { pullAt };\n", "import { identity } from '../../function/identity.mjs';\nimport { range } from '../../math/range.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction reduceRight(collection, iteratee = identity, accumulator) {\n if (!collection) {\n return accumulator;\n }\n let keys;\n let startIndex;\n if (isArrayLike(collection)) {\n keys = range(0, collection.length).reverse();\n if (accumulator == null && collection.length > 0) {\n accumulator = collection[collection.length - 1];\n startIndex = 1;\n }\n else {\n startIndex = 0;\n }\n }\n else {\n keys = Object.keys(collection).reverse();\n if (accumulator == null) {\n accumulator = collection[keys[0]];\n startIndex = 1;\n }\n else {\n startIndex = 0;\n }\n }\n for (let i = startIndex; i < keys.length; i++) {\n const key = keys[i];\n const value = collection[key];\n accumulator = iteratee(accumulator, value, key, collection);\n }\n return accumulator;\n}\n\nexport { reduceRight };\n", "function negate(func) {\n if (typeof func !== 'function') {\n throw new TypeError('Expected a function');\n }\n return function (...args) {\n return !func.apply(this, args);\n };\n}\n\nexport { negate };\n", "import { filter } from './filter.mjs';\nimport { identity } from '../../function/identity.mjs';\nimport { negate } from '../function/negate.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction reject(source, predicate = identity) {\n return filter(source, negate(iteratee(predicate)));\n}\n\nexport { reject };\n", "function remove(arr, shouldRemoveElement) {\n const originalArr = arr.slice();\n const removed = [];\n let resultIndex = 0;\n for (let i = 0; i < arr.length; i++) {\n if (shouldRemoveElement(arr[i], i, originalArr)) {\n removed.push(arr[i]);\n continue;\n }\n if (!Object.hasOwn(arr, i)) {\n delete arr[resultIndex++];\n continue;\n }\n arr[resultIndex++] = arr[i];\n }\n arr.length = resultIndex;\n return removed;\n}\n\nexport { remove };\n", "import { remove as remove$1 } from '../../array/remove.mjs';\nimport { identity } from '../../function/identity.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction remove(arr, shouldRemoveElement = identity) {\n return remove$1(arr, iteratee(shouldRemoveElement));\n}\n\nexport { remove };\n", "function reverse(array) {\n if (array == null) {\n return array;\n }\n return array.reverse();\n}\n\nexport { reverse };\n", "function sample(arr) {\n const randomIndex = Math.floor(Math.random() * arr.length);\n return arr[randomIndex];\n}\n\nexport { sample };\n", "import { sample as sample$1 } from '../../array/sample.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction sample(collection) {\n if (collection == null) {\n return undefined;\n }\n if (isArrayLike(collection)) {\n return sample$1(toArray(collection));\n }\n return sample$1(Object.values(collection));\n}\n\nexport { sample };\n", "function random(minimum, maximum) {\n if (maximum == null) {\n maximum = minimum;\n minimum = 0;\n }\n if (minimum >= maximum) {\n throw new Error('Invalid input: The maximum value must be greater than the minimum value.');\n }\n return Math.random() * (maximum - minimum) + minimum;\n}\n\nexport { random };\n", "import { random } from './random.mjs';\n\nfunction randomInt(minimum, maximum) {\n return Math.floor(random(minimum, maximum));\n}\n\nexport { randomInt };\n", "import { randomInt } from '../math/randomInt.mjs';\n\nfunction sampleSize(array, size) {\n if (size > array.length) {\n throw new Error('Size must be less than or equal to the length of array.');\n }\n const result = new Array(size);\n const selected = new Set();\n for (let step = array.length - size, resultIndex = 0; step < array.length; step++, resultIndex++) {\n let index = randomInt(0, step + 1);\n if (selected.has(index)) {\n index = step;\n }\n selected.add(index);\n result[resultIndex] = array[index];\n }\n return result;\n}\n\nexport { sampleSize };\n", "import { toNumber } from '../util/toNumber.mjs';\n\nfunction clamp(value, bound1, bound2) {\n if (bound2 === undefined) {\n bound2 = bound1;\n bound1 = undefined;\n }\n if (bound2 !== undefined) {\n bound2 = toNumber(bound2);\n value = Math.min(value, Number.isNaN(bound2) ? 0 : bound2);\n }\n if (bound1 !== undefined) {\n bound1 = toNumber(bound1);\n value = Math.max(value, Number.isNaN(bound1) ? 0 : bound1);\n }\n return value;\n}\n\nexport { clamp };\n", "function isMap(value) {\n return value instanceof Map;\n}\n\nexport { isMap };\n", "import { isMap as isMap$1 } from '../../predicate/isMap.mjs';\n\nfunction isMap(value) {\n return isMap$1(value);\n}\n\nexport { isMap };\n", "import { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { isMap } from '../predicate/isMap.mjs';\n\nfunction toArray(value) {\n if (value == null) {\n return [];\n }\n if (isArrayLike(value) || isMap(value)) {\n return Array.from(value);\n }\n if (typeof value === 'object') {\n return Object.values(value);\n }\n return [];\n}\n\nexport { toArray };\n", "import { sampleSize as sampleSize$1 } from '../../array/sampleSize.mjs';\nimport { isIterateeCall } from '../_internal/isIterateeCall.mjs';\nimport { clamp } from '../math/clamp.mjs';\nimport { toArray } from '../util/toArray.mjs';\nimport { toInteger } from '../util/toInteger.mjs';\n\nfunction sampleSize(collection, size, guard) {\n const arrayCollection = toArray(collection);\n if (guard ? isIterateeCall(collection, size, guard) : size === undefined) {\n size = 1;\n }\n else {\n size = clamp(toInteger(size), 0, arrayCollection.length);\n }\n return sampleSize$1(arrayCollection, size);\n}\n\nexport { sampleSize };\n", "function shuffle(arr) {\n const result = arr.slice();\n for (let i = result.length - 1; i >= 1; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [result[i], result[j]] = [result[j], result[i]];\n }\n return result;\n}\n\nexport { shuffle };\n", "function values(object) {\n if (object == null) {\n return [];\n }\n return Object.values(object);\n}\n\nexport { values };\n", "function isNil(x) {\n return x == null;\n}\n\nexport { isNil };\n", "import { shuffle as shuffle$1 } from '../../array/shuffle.mjs';\nimport { values } from '../object/values.mjs';\nimport { isArray } from '../predicate/isArray.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { isNil } from '../predicate/isNil.mjs';\nimport { isObjectLike } from '../predicate/isObjectLike.mjs';\n\nfunction shuffle(collection) {\n if (isNil(collection)) {\n return [];\n }\n if (isArray(collection)) {\n return shuffle$1(collection);\n }\n if (isArrayLike(collection)) {\n return shuffle$1(Array.from(collection));\n }\n if (isObjectLike(collection)) {\n return shuffle$1(values(collection));\n }\n return [];\n}\n\nexport { shuffle };\n", "import { isNil } from '../../predicate/isNil.mjs';\n\nfunction size(target) {\n if (isNil(target)) {\n return 0;\n }\n if (target instanceof Map || target instanceof Set) {\n return target.size;\n }\n return Object.keys(target).length;\n}\n\nexport { size };\n", "import { isIterateeCall } from '../_internal/isIterateeCall.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { toInteger } from '../util/toInteger.mjs';\n\nfunction slice(array, start, end) {\n if (!isArrayLike(array)) {\n return [];\n }\n const length = array.length;\n if (end === undefined) {\n end = length;\n }\n else if (typeof end !== 'number' && isIterateeCall(array, start, end)) {\n start = 0;\n end = length;\n }\n start = toInteger(start);\n end = toInteger(end);\n if (start < 0) {\n start = Math.max(length + start, 0);\n }\n else {\n start = Math.min(start, length);\n }\n if (end < 0) {\n end = Math.max(length + end, 0);\n }\n else {\n end = Math.min(end, length);\n }\n const resultLength = Math.max(end - start, 0);\n const result = new Array(resultLength);\n for (let i = 0; i < resultLength; ++i) {\n result[i] = array[start + i];\n }\n return result;\n}\n\nexport { slice };\n", "import { identity } from '../../function/identity.mjs';\nimport { property } from '../object/property.mjs';\nimport { matches } from '../predicate/matches.mjs';\nimport { matchesProperty } from '../predicate/matchesProperty.mjs';\n\nfunction some(source, predicate, guard) {\n if (!source) {\n return false;\n }\n if (guard != null) {\n predicate = undefined;\n }\n if (predicate == null) {\n predicate = identity;\n }\n const values = Array.isArray(source) ? source : Object.values(source);\n switch (typeof predicate) {\n case 'function': {\n if (!Array.isArray(source)) {\n const keys = Object.keys(source);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const value = source[key];\n if (predicate(value, key, source)) {\n return true;\n }\n }\n return false;\n }\n for (let i = 0; i < source.length; i++) {\n if (predicate(source[i], i, source)) {\n return true;\n }\n }\n return false;\n }\n case 'object': {\n if (Array.isArray(predicate) && predicate.length === 2) {\n const key = predicate[0];\n const value = predicate[1];\n const matchFunc = matchesProperty(key, value);\n if (Array.isArray(source)) {\n for (let i = 0; i < source.length; i++) {\n if (matchFunc(source[i])) {\n return true;\n }\n }\n return false;\n }\n return values.some(matchFunc);\n }\n else {\n const matchFunc = matches(predicate);\n if (Array.isArray(source)) {\n for (let i = 0; i < source.length; i++) {\n if (matchFunc(source[i])) {\n return true;\n }\n }\n return false;\n }\n return values.some(matchFunc);\n }\n }\n case 'number':\n case 'symbol':\n case 'string': {\n const propFunc = property(predicate);\n if (Array.isArray(source)) {\n for (let i = 0; i < source.length; i++) {\n if (propFunc(source[i])) {\n return true;\n }\n }\n return false;\n }\n return values.some(propFunc);\n }\n }\n}\n\nexport { some };\n", "import { orderBy } from './orderBy.mjs';\nimport { flatten } from '../../array/flatten.mjs';\nimport { isIterateeCall } from '../_internal/isIterateeCall.mjs';\n\nfunction sortBy(collection, ...criteria) {\n const length = criteria.length;\n if (length > 1 && isIterateeCall(collection, criteria[0], criteria[1])) {\n criteria = [];\n }\n else if (length > 2 && isIterateeCall(criteria[0], criteria[1], criteria[2])) {\n criteria = [criteria[0]];\n }\n return orderBy(collection, flatten(criteria), ['asc']);\n}\n\nexport { sortBy };\n", "function isNull(x) {\n return x === null;\n}\n\nexport { isNull };\n", "function isUndefined(x) {\n return x === undefined;\n}\n\nexport { isUndefined };\n", "function isNaN(value) {\n return Number.isNaN(value);\n}\n\nexport { isNaN };\n", "import { isNull } from '../../predicate/isNull.mjs';\nimport { isUndefined } from '../../predicate/isUndefined.mjs';\nimport { identity } from '../function/identity.mjs';\nimport { isNaN } from '../predicate/isNaN.mjs';\nimport { isNil } from '../predicate/isNil.mjs';\nimport { isSymbol } from '../predicate/isSymbol.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nconst MAX_ARRAY_LENGTH = 4294967295;\nconst MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1;\nfunction sortedIndexBy(array, value, iteratee$1 = identity, retHighest) {\n if (isNil(array) || array.length === 0) {\n return 0;\n }\n let low = 0;\n let high = array.length;\n const iterateeFunction = iteratee(iteratee$1);\n const transformedValue = iterateeFunction(value);\n const valIsNaN = isNaN(transformedValue);\n const valIsNull = isNull(transformedValue);\n const valIsSymbol = isSymbol(transformedValue);\n const valIsUndefined = isUndefined(transformedValue);\n while (low < high) {\n let setLow;\n const mid = Math.floor((low + high) / 2);\n const computed = iterateeFunction(array[mid]);\n const othIsDefined = !isUndefined(computed);\n const othIsNull = isNull(computed);\n const othIsReflexive = !isNaN(computed);\n const othIsSymbol = isSymbol(computed);\n if (valIsNaN) {\n setLow = retHighest || othIsReflexive;\n }\n else if (valIsUndefined) {\n setLow = othIsReflexive && (retHighest || othIsDefined);\n }\n else if (valIsNull) {\n setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull);\n }\n else if (valIsSymbol) {\n setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol);\n }\n else if (othIsNull || othIsSymbol) {\n setLow = false;\n }\n else {\n setLow = retHighest ? computed <= transformedValue : computed < transformedValue;\n }\n if (setLow) {\n low = mid + 1;\n }\n else {\n high = mid;\n }\n }\n return Math.min(high, MAX_ARRAY_INDEX);\n}\n\nexport { sortedIndexBy };\n", "function isSymbol(value) {\n return typeof value === 'symbol';\n}\n\nexport { isSymbol };\n", "function isNumber(value) {\n return typeof value === 'number' || value instanceof Number;\n}\n\nexport { isNumber };\n", "import { sortedIndexBy } from './sortedIndexBy.mjs';\nimport { isNil } from '../../predicate/isNil.mjs';\nimport { isNull } from '../../predicate/isNull.mjs';\nimport { isSymbol } from '../../predicate/isSymbol.mjs';\nimport { isNumber } from '../predicate/isNumber.mjs';\n\nconst MAX_ARRAY_LENGTH = 4294967295;\nconst HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1;\nfunction sortedIndex(array, value) {\n if (isNil(array)) {\n return 0;\n }\n let low = 0;\n let high = array.length;\n if (isNumber(value) && value === value && high <= HALF_MAX_ARRAY_LENGTH) {\n while (low < high) {\n const mid = (low + high) >>> 1;\n const compute = array[mid];\n if (!isNull(compute) && !isSymbol(compute) && compute < value) {\n low = mid + 1;\n }\n else {\n high = mid;\n }\n }\n return high;\n }\n return sortedIndexBy(array, value, value => value);\n}\n\nexport { sortedIndex };\n", "import { sortedIndex } from './sortedIndex.mjs';\nimport { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\n\nfunction sortedIndexOf(array, value) {\n if (!array?.length) {\n return -1;\n }\n const index = sortedIndex(array, value);\n if (index < array.length && isEqualsSameValueZero(array[index], value)) {\n return index;\n }\n return -1;\n}\n\nexport { sortedIndexOf };\n", "import { sortedIndexBy } from './sortedIndexBy.mjs';\n\nfunction sortedLastIndexBy(array, value, iteratee) {\n return sortedIndexBy(array, value, iteratee, true);\n}\n\nexport { sortedLastIndexBy };\n", "import { sortedLastIndexBy } from './sortedLastIndexBy.mjs';\nimport { isNil } from '../../predicate/isNil.mjs';\nimport { isNull } from '../../predicate/isNull.mjs';\nimport { isSymbol } from '../../predicate/isSymbol.mjs';\nimport { isNumber } from '../predicate/isNumber.mjs';\n\nconst MAX_ARRAY_LENGTH = 4294967295;\nconst HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1;\nfunction sortedLastIndex(array, value) {\n if (isNil(array)) {\n return 0;\n }\n let high = array.length;\n if (!isNumber(value) || Number.isNaN(value) || high > HALF_MAX_ARRAY_LENGTH) {\n return sortedLastIndexBy(array, value, value => value);\n }\n let low = 0;\n while (low < high) {\n const mid = (low + high) >>> 1;\n const compute = array[mid];\n if (!isNull(compute) && !isSymbol(compute) && compute <= value) {\n low = mid + 1;\n }\n else {\n high = mid;\n }\n }\n return high;\n}\n\nexport { sortedLastIndex };\n", "import { sortedLastIndex } from './sortedLastIndex.mjs';\nimport { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\n\nfunction sortedLastIndexOf(array, value) {\n if (!array?.length) {\n return -1;\n }\n const index = sortedLastIndex(array, value) - 1;\n if (index >= 0 && isEqualsSameValueZero(array[index], value)) {\n return index;\n }\n return -1;\n}\n\nexport { sortedLastIndexOf };\n", "function tail(arr) {\n return arr.slice(1);\n}\n\nexport { tail };\n", "import { tail as tail$1 } from '../../array/tail.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction tail(arr) {\n if (!isArrayLike(arr)) {\n return [];\n }\n return tail$1(toArray(arr));\n}\n\nexport { tail };\n", "import { toInteger } from '../compat/util/toInteger.mjs';\n\nfunction take(arr, count, guard) {\n count = guard || count === undefined ? 1 : toInteger(count);\n return arr.slice(0, count);\n}\n\nexport { take };\n", "import { take as take$1 } from '../../array/take.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { toInteger } from '../util/toInteger.mjs';\n\nfunction take(arr, count = 1, guard) {\n count = guard ? 1 : toInteger(count);\n if (count < 1 || !isArrayLike(arr)) {\n return [];\n }\n return take$1(toArray(arr), count);\n}\n\nexport { take };\n", "import { toInteger } from '../compat/util/toInteger.mjs';\n\nfunction takeRight(arr, count, guard) {\n count = guard || count === undefined ? 1 : toInteger(count);\n if (count <= 0 || arr.length === 0) {\n return [];\n }\n return arr.slice(-count);\n}\n\nexport { takeRight };\n", "import { takeRight as takeRight$1 } from '../../array/takeRight.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { toInteger } from '../util/toInteger.mjs';\n\nfunction takeRight(arr, count = 1, guard) {\n count = guard ? 1 : toInteger(count);\n if (count <= 0 || !isArrayLike(arr)) {\n return [];\n }\n return takeRight$1(toArray(arr), count);\n}\n\nexport { takeRight };\n", "function negate(func) {\n return ((...args) => !func(...args));\n}\n\nexport { negate };\n", "import { identity } from '../../function/identity.mjs';\nimport { negate } from '../../function/negate.mjs';\nimport { toArray } from '../_internal/toArray.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction takeRightWhile(_array, predicate) {\n if (!isArrayLikeObject(_array)) {\n return [];\n }\n const array = toArray(_array);\n const index = array.findLastIndex(negate(iteratee(predicate ?? identity)));\n return array.slice(index + 1);\n}\n\nexport { takeRightWhile };\n", "import { toArray } from '../_internal/toArray.mjs';\nimport { identity } from '../function/identity.mjs';\nimport { negate } from '../function/negate.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction takeWhile(array, predicate) {\n if (!isArrayLikeObject(array)) {\n return [];\n }\n const _array = toArray(array);\n const index = _array.findIndex(negate(iteratee(predicate ?? identity)));\n return index === -1 ? _array : _array.slice(0, index);\n}\n\nexport { takeWhile };\n", "import { flatMapDepth } from './flatMapDepth.mjs';\nimport { uniq } from '../../array/uniq.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\n\nfunction union(...arrays) {\n const validArrays = arrays.filter(isArrayLikeObject);\n const flattened = flatMapDepth(validArrays, v => Array.from(v), 1);\n return uniq(flattened);\n}\n\nexport { union };\n", "function uniqBy(arr, mapper) {\n const map = new Map();\n for (let i = 0; i < arr.length; i++) {\n const item = arr[i];\n const key = mapper(item, i, arr);\n if (!map.has(key)) {\n map.set(key, item);\n }\n }\n return Array.from(map.values());\n}\n\nexport { uniqBy };\n", "function ary(func, n) {\n return function (...args) {\n return func.apply(this, args.slice(0, n));\n };\n}\n\nexport { ary };\n", "import { last } from '../../array/last.mjs';\nimport { uniq } from '../../array/uniq.mjs';\nimport { uniqBy } from '../../array/uniqBy.mjs';\nimport { ary } from '../../function/ary.mjs';\nimport { flattenArrayLike } from '../_internal/flattenArrayLike.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction unionBy(...values) {\n const lastValue = last(values);\n const flattened = flattenArrayLike(values);\n if (isArrayLikeObject(lastValue) || lastValue == null) {\n return uniq(flattened);\n }\n return uniqBy(flattened, ary(iteratee(lastValue), 1));\n}\n\nexport { unionBy };\n", "function uniqWith(arr, areItemsEqual) {\n const result = [];\n for (let i = 0; i < arr.length; i++) {\n const item = arr[i];\n const isUniq = result.every(v => !areItemsEqual(v, item));\n if (isUniq) {\n result.push(item);\n }\n }\n return result;\n}\n\nexport { uniqWith };\n", "import { last } from '../../array/last.mjs';\nimport { uniq } from '../../array/uniq.mjs';\nimport { uniqWith } from '../../array/uniqWith.mjs';\nimport { flattenArrayLike } from '../_internal/flattenArrayLike.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\n\nfunction unionWith(...values) {\n const lastValue = last(values);\n const flattened = flattenArrayLike(values);\n if (isArrayLikeObject(lastValue) || lastValue == null) {\n return uniq(flattened);\n }\n return uniqWith(flattened, lastValue);\n}\n\nexport { unionWith };\n", "import { uniqBy as uniqBy$1 } from '../../array/uniqBy.mjs';\nimport { ary } from '../../function/ary.mjs';\nimport { identity } from '../../function/identity.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction uniqBy(array, iteratee$1 = identity) {\n if (!isArrayLikeObject(array)) {\n return [];\n }\n return uniqBy$1(Array.from(array), ary(iteratee(iteratee$1), 1));\n}\n\nexport { uniqBy };\n", "import { uniqWith as uniqWith$1 } from '../../array/uniqWith.mjs';\nimport { uniq } from './uniq.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction uniqWith(arr, comparator) {\n if (!isArrayLike(arr)) {\n return [];\n }\n return typeof comparator === 'function' ? uniqWith$1(Array.from(arr), comparator) : uniq(Array.from(arr));\n}\n\nexport { uniqWith };\n", "function unzip(zipped) {\n let maxLen = 0;\n for (let i = 0; i < zipped.length; i++) {\n if (zipped[i].length > maxLen) {\n maxLen = zipped[i].length;\n }\n }\n const result = new Array(maxLen);\n for (let i = 0; i < maxLen; i++) {\n result[i] = new Array(zipped.length);\n for (let j = 0; j < zipped.length; j++) {\n result[i][j] = zipped[j][i];\n }\n }\n return result;\n}\n\nexport { unzip };\n", "import { unzip as unzip$1 } from '../../array/unzip.mjs';\nimport { isArray } from '../predicate/isArray.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\n\nfunction unzip(array) {\n if (!isArrayLikeObject(array) || !array.length) {\n return [];\n }\n array = isArray(array) ? array : Array.from(array);\n array = array.filter(item => isArrayLikeObject(item));\n return unzip$1(array);\n}\n\nexport { unzip };\n", "import { unzip } from '../../array/unzip.mjs';\nimport { isArray } from '../predicate/isArray.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\n\nfunction unzipWith(array, iteratee) {\n if (!isArrayLikeObject(array) || !array.length) {\n return [];\n }\n const unzipped = isArray(array) ? unzip(array) : unzip(Array.from(array, value => Array.from(value)));\n if (!iteratee) {\n return unzipped;\n }\n const result = new Array(unzipped.length);\n for (let i = 0; i < unzipped.length; i++) {\n const value = unzipped[i];\n result[i] = iteratee(...value);\n }\n return result;\n}\n\nexport { unzipWith };\n", "import { difference } from './difference.mjs';\n\nfunction without(array, ...values) {\n return difference(array, values);\n}\n\nexport { without };\n", "import { without as without$1 } from '../../array/without.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\n\nfunction without(array, ...values) {\n if (!isArrayLikeObject(array)) {\n return [];\n }\n return without$1(Array.from(array), ...values);\n}\n\nexport { without };\n", "import { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\nimport { toArray } from '../util/toArray.mjs';\n\nfunction xor(...arrays) {\n const itemCounts = new Map();\n for (let i = 0; i < arrays.length; i++) {\n const array = arrays[i];\n if (!isArrayLikeObject(array)) {\n continue;\n }\n const itemSet = new Set(toArray(array));\n for (const item of itemSet) {\n if (!itemCounts.has(item)) {\n itemCounts.set(item, 1);\n }\n else {\n itemCounts.set(item, itemCounts.get(item) + 1);\n }\n }\n }\n const result = [];\n for (const [item, count] of itemCounts) {\n if (count === 1) {\n result.push(item);\n }\n }\n return result;\n}\n\nexport { xor };\n", "function windowed(arr, size, step = 1, { partialWindows = false } = {}) {\n if (size <= 0 || !Number.isInteger(size)) {\n throw new Error('Size must be a positive integer.');\n }\n if (step <= 0 || !Number.isInteger(step)) {\n throw new Error('Step must be a positive integer.');\n }\n const result = [];\n const end = partialWindows ? arr.length : arr.length - size + 1;\n for (let i = 0; i < end; i += step) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n}\n\nexport { windowed };\n", "import { differenceBy } from './differenceBy.mjs';\nimport { intersectionBy } from './intersectionBy.mjs';\nimport { last } from './last.mjs';\nimport { unionBy } from './unionBy.mjs';\nimport { windowed } from '../../array/windowed.mjs';\nimport { identity } from '../../function/identity.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction xorBy(...values) {\n const lastValue = last(values);\n let mapper = identity;\n if (!isArrayLikeObject(lastValue) && lastValue != null) {\n mapper = iteratee(lastValue);\n values = values.slice(0, -1);\n }\n const arrays = values.filter(isArrayLikeObject);\n const union = unionBy(...arrays, mapper);\n const intersections = windowed(arrays, 2).map(([arr1, arr2]) => intersectionBy(arr1, arr2, mapper));\n return differenceBy(union, unionBy(...intersections, mapper), mapper);\n}\n\nexport { xorBy };\n", "import { differenceWith } from './differenceWith.mjs';\nimport { intersectionWith } from './intersectionWith.mjs';\nimport { last } from './last.mjs';\nimport { unionWith } from './unionWith.mjs';\nimport { windowed } from '../../array/windowed.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\n\nfunction xorWith(...values) {\n const lastValue = last(values);\n let comparator = (a, b) => a === b;\n if (typeof lastValue === 'function') {\n comparator = lastValue;\n values = values.slice(0, -1);\n }\n const arrays = values.filter(isArrayLikeObject);\n const union = unionWith(...arrays, comparator);\n const intersections = windowed(arrays, 2).map(([arr1, arr2]) => intersectionWith(arr1, arr2, comparator));\n return differenceWith(union, unionWith(...intersections, comparator), comparator);\n}\n\nexport { xorWith };\n", "function zip(...arrs) {\n let rowCount = 0;\n for (let i = 0; i < arrs.length; i++) {\n if (arrs[i].length > rowCount) {\n rowCount = arrs[i].length;\n }\n }\n const columnCount = arrs.length;\n const result = Array(rowCount);\n for (let i = 0; i < rowCount; ++i) {\n const row = Array(columnCount);\n for (let j = 0; j < columnCount; ++j) {\n row[j] = arrs[j][i];\n }\n result[i] = row;\n }\n return result;\n}\n\nexport { zip };\n", "import { zip as zip$1 } from '../../array/zip.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\n\nfunction zip(...arrays) {\n if (!arrays.length) {\n return [];\n }\n return zip$1(...arrays.filter(group => isArrayLikeObject(group)));\n}\n\nexport { zip };\n", "import { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\n\nconst assignValue = (object, key, value) => {\n const objValue = object[key];\n if (!(Object.hasOwn(object, key) && isEqualsSameValueZero(objValue, value)) || (value === undefined && !(key in object))) {\n object[key] = value;\n }\n};\n\nexport { assignValue };\n", "import { assignValue } from '../_internal/assignValue.mjs';\n\nfunction zipObject(keys = [], values = []) {\n const result = {};\n for (let i = 0; i < keys.length; i++) {\n assignValue(result, keys[i], values[i]);\n }\n return result;\n}\n\nexport { zipObject };\n", "import { get } from './get.mjs';\nimport { isUnsafeProperty } from '../../_internal/isUnsafeProperty.mjs';\nimport { assignValue } from '../_internal/assignValue.mjs';\nimport { isIndex } from '../_internal/isIndex.mjs';\nimport { isKey } from '../_internal/isKey.mjs';\nimport { toKey } from '../_internal/toKey.mjs';\nimport { isObject } from '../predicate/isObject.mjs';\nimport { toPath } from '../util/toPath.mjs';\n\nfunction updateWith(obj, path, updater, customizer) {\n if (obj == null && !isObject(obj)) {\n return obj;\n }\n let resolvedPath;\n if (isKey(path, obj)) {\n resolvedPath = [path];\n }\n else if (Array.isArray(path)) {\n resolvedPath = path;\n }\n else {\n resolvedPath = toPath(path);\n }\n const updateValue = updater(get(obj, resolvedPath));\n let current = obj;\n for (let i = 0; i < resolvedPath.length && current != null; i++) {\n const key = toKey(resolvedPath[i]);\n if (isUnsafeProperty(key)) {\n continue;\n }\n let newValue;\n if (i === resolvedPath.length - 1) {\n newValue = updateValue;\n }\n else {\n const objValue = current[key];\n const customizerResult = customizer?.(objValue, key, obj);\n newValue =\n customizerResult !== undefined\n ? customizerResult\n : isObject(objValue)\n ? objValue\n : isIndex(resolvedPath[i + 1])\n ? []\n : {};\n }\n assignValue(current, key, newValue);\n current = current[key];\n }\n return obj;\n}\n\nexport { updateWith };\n", "import { updateWith } from './updateWith.mjs';\n\nfunction set(obj, path, value) {\n return updateWith(obj, path, () => value, () => undefined);\n}\n\nexport { set };\n", "import { zip } from '../../array/zip.mjs';\nimport { set } from '../object/set.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction zipObjectDeep(keys, values) {\n const result = {};\n if (!isArrayLike(keys)) {\n return result;\n }\n if (!isArrayLike(values)) {\n values = [];\n }\n const zipped = zip(Array.from(keys), Array.from(values));\n for (let i = 0; i < zipped.length; i++) {\n const [key, value] = zipped[i];\n if (key != null) {\n set(result, key, value);\n }\n }\n return result;\n}\n\nexport { zipObjectDeep };\n", "import { unzip } from './unzip.mjs';\nimport { isFunction } from '../../predicate/isFunction.mjs';\n\nfunction zipWith(...combine) {\n let iteratee = combine.pop();\n if (!isFunction(iteratee)) {\n combine.push(iteratee);\n iteratee = undefined;\n }\n if (!combine?.length) {\n return [];\n }\n const result = unzip(combine);\n if (iteratee == null) {\n return result;\n }\n return result.map(group => iteratee(...group));\n}\n\nexport { zipWith };\n", "import { toInteger } from '../util/toInteger.mjs';\n\nfunction after(n, func) {\n if (typeof func !== 'function') {\n throw new TypeError('Expected a function');\n }\n n = toInteger(n);\n return function (...args) {\n if (--n < 1) {\n return func.apply(this, args);\n }\n };\n}\n\nexport { after };\n", "import { ary as ary$1 } from '../../function/ary.mjs';\n\nfunction ary(func, n = func.length, guard) {\n if (guard) {\n n = func.length;\n }\n if (Number.isNaN(n) || n < 0) {\n n = 0;\n }\n return ary$1(func, n);\n}\n\nexport { ary };\n", "function attempt(func, ...args) {\n try {\n return func(...args);\n }\n catch (e) {\n return e instanceof Error ? e : new Error(e);\n }\n}\n\nexport { attempt };\n", "import { toInteger } from '../util/toInteger.mjs';\n\nfunction before(n, func) {\n if (typeof func !== 'function') {\n throw new TypeError('Expected a function');\n }\n let result;\n n = toInteger(n);\n return function (...args) {\n if (--n > 0) {\n result = func.apply(this, args);\n }\n if (n <= 1 && func) {\n func = undefined;\n }\n return result;\n };\n}\n\nexport { before };\n", "function bind(func, thisObj, ...partialArgs) {\n const bound = function (...providedArgs) {\n const args = [];\n let startIndex = 0;\n for (let i = 0; i < partialArgs.length; i++) {\n const arg = partialArgs[i];\n if (arg === bind.placeholder) {\n args.push(providedArgs[startIndex++]);\n }\n else {\n args.push(arg);\n }\n }\n for (let i = startIndex; i < providedArgs.length; i++) {\n args.push(providedArgs[i]);\n }\n if (this instanceof bound) {\n return new func(...args);\n }\n return func.apply(thisObj, args);\n };\n return bound;\n}\nconst bindPlaceholder = Symbol('bind.placeholder');\nbind.placeholder = bindPlaceholder;\n\nexport { bind };\n", "function bindKey(object, key, ...partialArgs) {\n const bound = function (...providedArgs) {\n const args = [];\n let startIndex = 0;\n for (let i = 0; i < partialArgs.length; i++) {\n const arg = partialArgs[i];\n if (arg === bindKey.placeholder) {\n args.push(providedArgs[startIndex++]);\n }\n else {\n args.push(arg);\n }\n }\n for (let i = startIndex; i < providedArgs.length; i++) {\n args.push(providedArgs[i]);\n }\n if (this instanceof bound) {\n return new object[key](...args);\n }\n return object[key].apply(object, args);\n };\n return bound;\n}\nconst bindKeyPlaceholder = Symbol('bindKey.placeholder');\nbindKey.placeholder = bindKeyPlaceholder;\n\nexport { bindKey };\n", "function curry(func, arity = func.length, guard) {\n arity = guard ? func.length : arity;\n arity = Number.parseInt(arity, 10);\n if (Number.isNaN(arity) || arity < 1) {\n arity = 0;\n }\n const wrapper = function (...partialArgs) {\n const holders = partialArgs.filter(item => item === curry.placeholder);\n const length = partialArgs.length - holders.length;\n if (length < arity) {\n return makeCurry(func, arity - length, partialArgs);\n }\n if (this instanceof wrapper) {\n return new func(...partialArgs);\n }\n return func.apply(this, partialArgs);\n };\n wrapper.placeholder = curryPlaceholder;\n return wrapper;\n}\nfunction makeCurry(func, arity, partialArgs) {\n function wrapper(...providedArgs) {\n const holders = providedArgs.filter(item => item === curry.placeholder);\n const length = providedArgs.length - holders.length;\n providedArgs = composeArgs(providedArgs, partialArgs);\n if (length < arity) {\n return makeCurry(func, arity - length, providedArgs);\n }\n if (this instanceof wrapper) {\n return new func(...providedArgs);\n }\n return func.apply(this, providedArgs);\n }\n wrapper.placeholder = curryPlaceholder;\n return wrapper;\n}\nfunction composeArgs(providedArgs, partialArgs) {\n const args = [];\n let startIndex = 0;\n for (let i = 0; i < partialArgs.length; i++) {\n const arg = partialArgs[i];\n if (arg === curry.placeholder && startIndex < providedArgs.length) {\n args.push(providedArgs[startIndex++]);\n }\n else {\n args.push(arg);\n }\n }\n for (let i = startIndex; i < providedArgs.length; i++) {\n args.push(providedArgs[i]);\n }\n return args;\n}\nconst curryPlaceholder = Symbol('curry.placeholder');\ncurry.placeholder = curryPlaceholder;\n\nexport { curry };\n", "function curryRight(func, arity = func.length, guard) {\n arity = guard ? func.length : arity;\n arity = Number.parseInt(arity, 10);\n if (Number.isNaN(arity) || arity < 1) {\n arity = 0;\n }\n const wrapper = function (...partialArgs) {\n const holders = partialArgs.filter(item => item === curryRight.placeholder);\n const length = partialArgs.length - holders.length;\n if (length < arity) {\n return makeCurryRight(func, arity - length, partialArgs);\n }\n if (this instanceof wrapper) {\n return new func(...partialArgs);\n }\n return func.apply(this, partialArgs);\n };\n wrapper.placeholder = curryRightPlaceholder;\n return wrapper;\n}\nfunction makeCurryRight(func, arity, partialArgs) {\n function wrapper(...providedArgs) {\n const holders = providedArgs.filter(item => item === curryRight.placeholder);\n const length = providedArgs.length - holders.length;\n providedArgs = composeArgs(providedArgs, partialArgs);\n if (length < arity) {\n return makeCurryRight(func, arity - length, providedArgs);\n }\n if (this instanceof wrapper) {\n return new func(...providedArgs);\n }\n return func.apply(this, providedArgs);\n }\n wrapper.placeholder = curryRightPlaceholder;\n return wrapper;\n}\nfunction composeArgs(providedArgs, partialArgs) {\n const placeholderLength = partialArgs.filter(arg => arg === curryRight.placeholder).length;\n const rangeLength = Math.max(providedArgs.length - placeholderLength, 0);\n const args = [];\n let providedIndex = 0;\n for (let i = 0; i < rangeLength; i++) {\n args.push(providedArgs[providedIndex++]);\n }\n for (let i = 0; i < partialArgs.length; i++) {\n const arg = partialArgs[i];\n if (arg === curryRight.placeholder) {\n if (providedIndex < providedArgs.length) {\n args.push(providedArgs[providedIndex++]);\n }\n else {\n args.push(arg);\n }\n }\n else {\n args.push(arg);\n }\n }\n return args;\n}\nconst curryRightPlaceholder = Symbol('curryRight.placeholder');\ncurryRight.placeholder = curryRightPlaceholder;\n\nexport { curryRight };\n", "function debounce(func, debounceMs, { signal, edges } = {}) {\n let pendingThis = undefined;\n let pendingArgs = null;\n const leading = edges != null && edges.includes('leading');\n const trailing = edges == null || edges.includes('trailing');\n const invoke = () => {\n if (pendingArgs !== null) {\n func.apply(pendingThis, pendingArgs);\n pendingThis = undefined;\n pendingArgs = null;\n }\n };\n const onTimerEnd = () => {\n if (trailing) {\n invoke();\n }\n cancel();\n };\n let timeoutId = null;\n const schedule = () => {\n if (timeoutId != null) {\n clearTimeout(timeoutId);\n }\n timeoutId = setTimeout(() => {\n timeoutId = null;\n onTimerEnd();\n }, debounceMs);\n };\n const cancelTimer = () => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId);\n timeoutId = null;\n }\n };\n const cancel = () => {\n cancelTimer();\n pendingThis = undefined;\n pendingArgs = null;\n };\n const flush = () => {\n invoke();\n };\n const debounced = function (...args) {\n if (signal?.aborted) {\n return;\n }\n pendingThis = this;\n pendingArgs = args;\n const isFirstCall = timeoutId == null;\n schedule();\n if (leading && isFirstCall) {\n invoke();\n }\n };\n debounced.schedule = schedule;\n debounced.cancel = cancel;\n debounced.flush = flush;\n signal?.addEventListener('abort', cancel, { once: true });\n return debounced;\n}\n\nexport { debounce };\n", "import { debounce as debounce$1 } from '../../function/debounce.mjs';\n\nfunction debounce(func, debounceMs = 0, options = {}) {\n if (typeof options !== 'object') {\n options = {};\n }\n const { leading = false, trailing = true, maxWait } = options;\n const edges = Array(2);\n if (leading) {\n edges[0] = 'leading';\n }\n if (trailing) {\n edges[1] = 'trailing';\n }\n let result = undefined;\n let pendingAt = null;\n const _debounced = debounce$1(function (...args) {\n result = func.apply(this, args);\n pendingAt = null;\n }, debounceMs, { edges });\n const debounced = function (...args) {\n if (maxWait != null) {\n if (pendingAt === null) {\n pendingAt = Date.now();\n }\n if (Date.now() - pendingAt >= maxWait) {\n result = func.apply(this, args);\n pendingAt = Date.now();\n _debounced.cancel();\n _debounced.schedule();\n return result;\n }\n }\n _debounced.apply(this, args);\n return result;\n };\n const flush = () => {\n _debounced.flush();\n return result;\n };\n debounced.cancel = _debounced.cancel;\n debounced.flush = flush;\n return debounced;\n}\n\nexport { debounce };\n", "function defer(func, ...args) {\n if (typeof func !== 'function') {\n throw new TypeError('Expected a function');\n }\n return setTimeout(func, 1, ...args);\n}\n\nexport { defer };\n", "import { toNumber } from '../util/toNumber.mjs';\n\nfunction delay(func, wait, ...args) {\n if (typeof func !== 'function') {\n throw new TypeError('Expected a function');\n }\n return setTimeout(func, toNumber(wait) || 0, ...args);\n}\n\nexport { delay };\n", "function flip(func) {\n return function (...args) {\n return func.apply(this, args.reverse());\n };\n}\n\nexport { flip };\n", "function flow(...funcs) {\n return function (...args) {\n let result = funcs.length ? funcs[0].apply(this, args) : args[0];\n for (let i = 1; i < funcs.length; i++) {\n result = funcs[i].call(this, result);\n }\n return result;\n };\n}\n\nexport { flow };\n", "import { flatten } from '../../array/flatten.mjs';\nimport { flow as flow$1 } from '../../function/flow.mjs';\n\nfunction flow(...funcs) {\n const flattenFuncs = flatten(funcs, 1);\n if (flattenFuncs.some(func => typeof func !== 'function')) {\n throw new TypeError('Expected a function');\n }\n return flow$1(...flattenFuncs);\n}\n\nexport { flow };\n", "import { flow } from './flow.mjs';\n\nfunction flowRight(...funcs) {\n return flow(...funcs.reverse());\n}\n\nexport { flowRight };\n", "import { flatten } from '../../array/flatten.mjs';\nimport { flowRight as flowRight$1 } from '../../function/flowRight.mjs';\n\nfunction flowRight(...funcs) {\n const flattenFuncs = flatten(funcs, 1);\n if (flattenFuncs.some(func => typeof func !== 'function')) {\n throw new TypeError('Expected a function');\n }\n return flowRight$1(...flattenFuncs);\n}\n\nexport { flowRight };\n", "function memoize(func, resolver) {\n if (typeof func !== 'function' || (resolver != null && typeof resolver !== 'function')) {\n throw new TypeError('Expected a function');\n }\n const memoized = function (...args) {\n const key = resolver ? resolver.apply(this, args) : args[0];\n const cache = memoized.cache;\n if (cache.has(key)) {\n return cache.get(key);\n }\n const result = func.apply(this, args);\n memoized.cache = cache.set(key, result) || cache;\n return result;\n };\n const CacheConstructor = memoize.Cache || Map;\n memoized.cache = new CacheConstructor();\n return memoized;\n}\nmemoize.Cache = Map;\n\nexport { memoize };\n", "import { toInteger } from '../util/toInteger.mjs';\n\nfunction nthArg(n = 0) {\n return function (...args) {\n return args.at(toInteger(n));\n };\n}\n\nexport { nthArg };\n", "function once(func) {\n let called = false;\n let cache;\n return function (...args) {\n if (!called) {\n called = true;\n cache = func(...args);\n }\n return cache;\n };\n}\n\nexport { once };\n", "import { once as once$1 } from '../../function/once.mjs';\n\nfunction once(func) {\n return once$1(func);\n}\n\nexport { once };\n", "import { identity } from '../../function/identity.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction overArgs(func, ..._transforms) {\n if (typeof func !== 'function') {\n throw new TypeError('Expected a function');\n }\n const transforms = _transforms.flat();\n return function (...args) {\n const length = Math.min(args.length, transforms.length);\n const transformedArgs = [...args];\n for (let i = 0; i < length; i++) {\n const transform = iteratee(transforms[i] ?? identity);\n transformedArgs[i] = transform.call(this, args[i]);\n }\n return func.apply(this, transformedArgs);\n };\n}\n\nexport { overArgs };\n", "function partial(func, ...partialArgs) {\n return partialImpl(func, placeholderSymbol, ...partialArgs);\n}\nfunction partialImpl(func, placeholder, ...partialArgs) {\n const partialed = function (...providedArgs) {\n let providedArgsIndex = 0;\n const substitutedArgs = partialArgs\n .slice()\n .map(arg => (arg === placeholder ? providedArgs[providedArgsIndex++] : arg));\n const remainingArgs = providedArgs.slice(providedArgsIndex);\n return func.apply(this, substitutedArgs.concat(remainingArgs));\n };\n if (func.prototype) {\n partialed.prototype = Object.create(func.prototype);\n }\n return partialed;\n}\nconst placeholderSymbol = Symbol('partial.placeholder');\npartial.placeholder = placeholderSymbol;\n\nexport { partial, partialImpl };\n", "import { partialImpl } from '../../function/partial.mjs';\n\nfunction partial(func, ...partialArgs) {\n return partialImpl(func, partial.placeholder, ...partialArgs);\n}\npartial.placeholder = Symbol('compat.partial.placeholder');\n\nexport { partial };\n", "function partialRight(func, ...partialArgs) {\n return partialRightImpl(func, placeholderSymbol, ...partialArgs);\n}\nfunction partialRightImpl(func, placeholder, ...partialArgs) {\n const partialedRight = function (...providedArgs) {\n const placeholderLength = partialArgs.filter(arg => arg === placeholder).length;\n const rangeLength = Math.max(providedArgs.length - placeholderLength, 0);\n const remainingArgs = providedArgs.slice(0, rangeLength);\n let providedArgsIndex = rangeLength;\n const substitutedArgs = partialArgs\n .slice()\n .map(arg => (arg === placeholder ? providedArgs[providedArgsIndex++] : arg));\n return func.apply(this, remainingArgs.concat(substitutedArgs));\n };\n if (func.prototype) {\n partialedRight.prototype = Object.create(func.prototype);\n }\n return partialedRight;\n}\nconst placeholderSymbol = Symbol('partialRight.placeholder');\npartialRight.placeholder = placeholderSymbol;\n\nexport { partialRight, partialRightImpl };\n", "import { partialRightImpl } from '../../function/partialRight.mjs';\n\nfunction partialRight(func, ...partialArgs) {\n return partialRightImpl(func, partialRight.placeholder, ...partialArgs);\n}\npartialRight.placeholder = Symbol('compat.partialRight.placeholder');\n\nexport { partialRight };\n", "import { flatten } from '../array/flatten.mjs';\n\nfunction rearg(func, ...indices) {\n const flattenIndices = flatten(indices);\n return function (...args) {\n const reorderedArgs = flattenIndices.map(i => args[i]).slice(0, args.length);\n for (let i = reorderedArgs.length; i < args.length; i++) {\n reorderedArgs.push(args[i]);\n }\n return func.apply(this, reorderedArgs);\n };\n}\n\nexport { rearg };\n", "function rest(func, startIndex = func.length - 1) {\n return function (...args) {\n const rest = args.slice(startIndex);\n const params = args.slice(0, startIndex);\n while (params.length < startIndex) {\n params.push(undefined);\n }\n return func.apply(this, [...params, rest]);\n };\n}\n\nexport { rest };\n", "import { rest as rest$1 } from '../../function/rest.mjs';\n\nfunction rest(func, start = func.length - 1) {\n start = Number.parseInt(start, 10);\n if (Number.isNaN(start) || start < 0) {\n start = func.length - 1;\n }\n return rest$1(func, start);\n}\n\nexport { rest };\n", "function spread(func, argsIndex = 0) {\n argsIndex = Number.parseInt(argsIndex, 10);\n if (Number.isNaN(argsIndex) || argsIndex < 0) {\n argsIndex = 0;\n }\n return function (...args) {\n const array = args[argsIndex];\n const params = args.slice(0, argsIndex);\n if (array) {\n params.push(...array);\n }\n return func.apply(this, params);\n };\n}\n\nexport { spread };\n", "import { debounce } from './debounce.mjs';\n\nfunction throttle(func, throttleMs = 0, options = {}) {\n const { leading = true, trailing = true } = options;\n return debounce(func, throttleMs, {\n leading,\n maxWait: throttleMs,\n trailing,\n });\n}\n\nexport { throttle };\n", "import { ary } from './ary.mjs';\n\nfunction unary(func) {\n return ary(func, 1);\n}\n\nexport { unary };\n", "import { identity } from '../../function/identity.mjs';\nimport { isFunction } from '../../predicate/isFunction.mjs';\n\nfunction wrap(value, wrapper) {\n return function (...args) {\n const wrapFn = isFunction(wrapper) ? wrapper : identity;\n return wrapFn.apply(this, [value, ...args]);\n };\n}\n\nexport { wrap };\n", "import { toNumber } from '../util/toNumber.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction add(value, other) {\n if (value === undefined && other === undefined) {\n return 0;\n }\n if (value === undefined || other === undefined) {\n return value ?? other;\n }\n if (typeof value === 'string' || typeof other === 'string') {\n value = toString(value);\n other = toString(other);\n }\n else {\n value = toNumber(value);\n other = toNumber(other);\n }\n return value + other;\n}\n\nexport { add };\n", "function decimalAdjust(type, number, precision = 0) {\n number = Number(number);\n if (Object.is(number, -0)) {\n number = '-0';\n }\n precision = Math.min(Number.parseInt(precision, 10), 292);\n if (precision) {\n const [magnitude, exponent = 0] = number.toString().split('e');\n let adjustedValue = Math[type](Number(`${magnitude}e${Number(exponent) + precision}`));\n if (Object.is(adjustedValue, -0)) {\n adjustedValue = '-0';\n }\n const [newMagnitude, newExponent = 0] = adjustedValue.toString().split('e');\n return Number(`${newMagnitude}e${Number(newExponent) - precision}`);\n }\n return Math[type](Number(number));\n}\n\nexport { decimalAdjust };\n", "import { decimalAdjust } from '../_internal/decimalAdjust.mjs';\n\nfunction ceil(number, precision = 0) {\n return decimalAdjust('ceil', number, precision);\n}\n\nexport { ceil };\n", "import { toNumber } from '../util/toNumber.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction divide(value, other) {\n if (value === undefined && other === undefined) {\n return 1;\n }\n if (value === undefined || other === undefined) {\n return value ?? other;\n }\n if (typeof value === 'string' || typeof other === 'string') {\n value = toString(value);\n other = toString(other);\n }\n else {\n value = toNumber(value);\n other = toNumber(other);\n }\n return value / other;\n}\n\nexport { divide };\n", "import { decimalAdjust } from '../_internal/decimalAdjust.mjs';\n\nfunction floor(number, precision = 0) {\n return decimalAdjust('floor', number, precision);\n}\n\nexport { floor };\n", "function inRange(value, minimum, maximum) {\n if (maximum == null) {\n maximum = minimum;\n minimum = 0;\n }\n if (minimum >= maximum) {\n throw new Error('The maximum value must be greater than the minimum value.');\n }\n return minimum <= value && value < maximum;\n}\n\nexport { inRange };\n", "import { inRange as inRange$1 } from '../../math/inRange.mjs';\n\nfunction inRange(value, minimum, maximum) {\n if (!minimum) {\n minimum = 0;\n }\n if (maximum != null && !maximum) {\n maximum = 0;\n }\n if (minimum != null && typeof minimum !== 'number') {\n minimum = Number(minimum);\n }\n if (maximum == null && minimum === 0) {\n return false;\n }\n if (maximum != null && typeof maximum !== 'number') {\n maximum = Number(maximum);\n }\n if (maximum != null && minimum > maximum) {\n [minimum, maximum] = [maximum, minimum];\n }\n if (minimum === maximum) {\n return false;\n }\n return inRange$1(value, minimum, maximum);\n}\n\nexport { inRange };\n", "function max(items) {\n if (!items || items.length === 0) {\n return undefined;\n }\n let maxResult = undefined;\n for (let i = 0; i < items.length; i++) {\n const current = items[i];\n if (current == null || Number.isNaN(current) || typeof current === 'symbol') {\n continue;\n }\n if (maxResult === undefined || current > maxResult) {\n maxResult = current;\n }\n }\n return maxResult;\n}\n\nexport { max };\n", "function maxBy(items, getValue) {\n if (items.length === 0) {\n return undefined;\n }\n let maxElement = items[0];\n let max = getValue(maxElement, 0, items);\n for (let i = 1; i < items.length; i++) {\n const element = items[i];\n const value = getValue(element, i, items);\n if (value > max) {\n max = value;\n maxElement = element;\n }\n }\n return maxElement;\n}\n\nexport { maxBy };\n", "import { maxBy as maxBy$1 } from '../../array/maxBy.mjs';\nimport { identity } from '../../function/identity.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction maxBy(items, iteratee$1) {\n if (items == null) {\n return undefined;\n }\n return maxBy$1(Array.from(items), iteratee(iteratee$1 ?? identity));\n}\n\nexport { maxBy };\n", "import { iteratee } from '../util/iteratee.mjs';\n\nfunction sumBy(array, iteratee$1) {\n if (!array || !array.length) {\n return 0;\n }\n if (iteratee$1 != null) {\n iteratee$1 = iteratee(iteratee$1);\n }\n let result = undefined;\n for (let i = 0; i < array.length; i++) {\n const current = iteratee$1 ? iteratee$1(array[i]) : array[i];\n if (current !== undefined) {\n if (result === undefined) {\n result = current;\n }\n else {\n result += current;\n }\n }\n }\n return result;\n}\n\nexport { sumBy };\n", "import { sumBy } from './sumBy.mjs';\n\nfunction sum(array) {\n return sumBy(array);\n}\n\nexport { sum };\n", "import { sum } from './sum.mjs';\n\nfunction mean(nums) {\n const length = nums ? nums.length : 0;\n return length === 0 ? NaN : sum(nums) / length;\n}\n\nexport { mean };\n", "function sumBy(items, getValue) {\n let result = 0;\n for (let i = 0; i < items.length; i++) {\n result += getValue(items[i], i);\n }\n return result;\n}\n\nexport { sumBy };\n", "import { sumBy } from './sumBy.mjs';\n\nfunction meanBy(items, getValue) {\n return sumBy(items, item => getValue(item)) / items.length;\n}\n\nexport { meanBy };\n", "import { identity } from '../../function/identity.mjs';\nimport { meanBy as meanBy$1 } from '../../math/meanBy.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction meanBy(items, iteratee$1) {\n if (items == null) {\n return NaN;\n }\n return meanBy$1(Array.from(items), iteratee(iteratee$1 ?? identity));\n}\n\nexport { meanBy };\n", "function min(items) {\n if (!items || items.length === 0) {\n return undefined;\n }\n let minResult = undefined;\n for (let i = 0; i < items.length; i++) {\n const current = items[i];\n if (current == null || Number.isNaN(current) || typeof current === 'symbol') {\n continue;\n }\n if (minResult === undefined || current < minResult) {\n minResult = current;\n }\n }\n return minResult;\n}\n\nexport { min };\n", "function minBy(items, getValue) {\n if (items.length === 0) {\n return undefined;\n }\n let minElement = items[0];\n let min = getValue(minElement, 0, items);\n for (let i = 1; i < items.length; i++) {\n const element = items[i];\n const value = getValue(element, i, items);\n if (value < min) {\n min = value;\n minElement = element;\n }\n }\n return minElement;\n}\n\nexport { minBy };\n", "import { minBy as minBy$1 } from '../../array/minBy.mjs';\nimport { identity } from '../../function/identity.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction minBy(items, iteratee$1) {\n if (items == null) {\n return undefined;\n }\n return minBy$1(Array.from(items), iteratee(iteratee$1 ?? identity));\n}\n\nexport { minBy };\n", "import { toNumber } from '../util/toNumber.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction multiply(value, other) {\n if (value === undefined && other === undefined) {\n return 1;\n }\n if (value === undefined || other === undefined) {\n return value ?? other;\n }\n if (typeof value === 'string' || typeof other === 'string') {\n value = toString(value);\n other = toString(other);\n }\n else {\n value = toNumber(value);\n other = toNumber(other);\n }\n return value * other;\n}\n\nexport { multiply };\n", "function parseInt(string, radix = 0, guard) {\n if (guard) {\n radix = 0;\n }\n return Number.parseInt(string, radix);\n}\n\nexport { parseInt };\n", "import { clamp } from './clamp.mjs';\nimport { random as random$1 } from '../../math/random.mjs';\nimport { randomInt } from '../../math/randomInt.mjs';\n\nfunction random(...args) {\n let minimum = 0;\n let maximum = 1;\n let floating = false;\n switch (args.length) {\n case 1: {\n if (typeof args[0] === 'boolean') {\n floating = args[0];\n }\n else {\n maximum = args[0];\n }\n break;\n }\n case 2: {\n if (typeof args[1] === 'boolean') {\n maximum = args[0];\n floating = args[1];\n }\n else {\n minimum = args[0];\n maximum = args[1];\n }\n }\n case 3: {\n if (typeof args[2] === 'object' && args[2] != null && args[2][args[1]] === args[0]) {\n minimum = 0;\n maximum = args[0];\n floating = false;\n }\n else {\n minimum = args[0];\n maximum = args[1];\n floating = args[2];\n }\n }\n }\n if (typeof minimum !== 'number') {\n minimum = Number(minimum);\n }\n if (typeof maximum !== 'number') {\n minimum = Number(maximum);\n }\n if (!minimum) {\n minimum = 0;\n }\n if (!maximum) {\n maximum = 0;\n }\n if (minimum > maximum) {\n [minimum, maximum] = [maximum, minimum];\n }\n minimum = clamp(minimum, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);\n maximum = clamp(maximum, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);\n if (minimum === maximum) {\n return minimum;\n }\n if (floating) {\n return random$1(minimum, maximum + 1);\n }\n else {\n return randomInt(minimum, maximum + 1);\n }\n}\n\nexport { random };\n", "import { isIterateeCall } from '../_internal/isIterateeCall.mjs';\nimport { toFinite } from '../util/toFinite.mjs';\n\nfunction range(start, end, step) {\n if (step && typeof step !== 'number' && isIterateeCall(start, end, step)) {\n end = step = undefined;\n }\n start = toFinite(start);\n if (end === undefined) {\n end = start;\n start = 0;\n }\n else {\n end = toFinite(end);\n }\n step = step === undefined ? (start < end ? 1 : -1) : toFinite(step);\n const length = Math.max(Math.ceil((end - start) / (step || 1)), 0);\n const result = new Array(length);\n for (let index = 0; index < length; index++) {\n result[index] = start;\n start += step;\n }\n return result;\n}\n\nexport { range };\n", "import { isIterateeCall } from '../_internal/isIterateeCall.mjs';\nimport { toFinite } from '../util/toFinite.mjs';\n\nfunction rangeRight(start, end, step) {\n if (step && typeof step !== 'number' && isIterateeCall(start, end, step)) {\n end = step = undefined;\n }\n start = toFinite(start);\n if (end === undefined) {\n end = start;\n start = 0;\n }\n else {\n end = toFinite(end);\n }\n step = step === undefined ? (start < end ? 1 : -1) : toFinite(step);\n const length = Math.max(Math.ceil((end - start) / (step || 1)), 0);\n const result = new Array(length);\n for (let index = length - 1; index >= 0; index--) {\n result[index] = start;\n start += step;\n }\n return result;\n}\n\nexport { rangeRight };\n", "import { decimalAdjust } from '../_internal/decimalAdjust.mjs';\n\nfunction round(number, precision = 0) {\n return decimalAdjust('round', number, precision);\n}\n\nexport { round };\n", "import { toNumber } from '../util/toNumber.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction subtract(value, other) {\n if (value === undefined && other === undefined) {\n return 0;\n }\n if (value === undefined || other === undefined) {\n return value ?? other;\n }\n if (typeof value === 'string' || typeof other === 'string') {\n value = toString(value);\n other = toString(other);\n }\n else {\n value = toNumber(value);\n other = toNumber(other);\n }\n return value - other;\n}\n\nexport { subtract };\n", "function isPlainObject(value) {\n if (!value || typeof value !== 'object') {\n return false;\n }\n const proto = Object.getPrototypeOf(value);\n const hasObjectPrototype = proto === null ||\n proto === Object.prototype ||\n Object.getPrototypeOf(proto) === null;\n if (!hasObjectPrototype) {\n return false;\n }\n return Object.prototype.toString.call(value) === '[object Object]';\n}\n\nexport { isPlainObject };\n", "import { isPlainObject } from './isPlainObject.mjs';\nimport { getSymbols } from '../compat/_internal/getSymbols.mjs';\nimport { getTag } from '../compat/_internal/getTag.mjs';\nimport { functionTag, regexpTag, symbolTag, dateTag, booleanTag, numberTag, stringTag, objectTag, errorTag, dataViewTag, arrayBufferTag, float64ArrayTag, float32ArrayTag, bigInt64ArrayTag, int32ArrayTag, int16ArrayTag, int8ArrayTag, bigUint64ArrayTag, uint32ArrayTag, uint16ArrayTag, uint8ClampedArrayTag, uint8ArrayTag, arrayTag, setTag, mapTag, argumentsTag } from '../compat/_internal/tags.mjs';\nimport { isEqualsSameValueZero } from '../_internal/isEqualsSameValueZero.mjs';\n\nfunction isEqualWith(a, b, areValuesEqual) {\n return isEqualWithImpl(a, b, undefined, undefined, undefined, undefined, areValuesEqual);\n}\nfunction isEqualWithImpl(a, b, property, aParent, bParent, stack, areValuesEqual) {\n const result = areValuesEqual(a, b, property, aParent, bParent, stack);\n if (result !== undefined) {\n return result;\n }\n if (typeof a === typeof b) {\n switch (typeof a) {\n case 'bigint':\n case 'string':\n case 'boolean':\n case 'symbol':\n case 'undefined': {\n return a === b;\n }\n case 'number': {\n return a === b || Object.is(a, b);\n }\n case 'function': {\n return a === b;\n }\n case 'object': {\n return areObjectsEqual(a, b, stack, areValuesEqual);\n }\n }\n }\n return areObjectsEqual(a, b, stack, areValuesEqual);\n}\nfunction areObjectsEqual(a, b, stack, areValuesEqual) {\n if (Object.is(a, b)) {\n return true;\n }\n let aTag = getTag(a);\n let bTag = getTag(b);\n if (aTag === argumentsTag) {\n aTag = objectTag;\n }\n if (bTag === argumentsTag) {\n bTag = objectTag;\n }\n if (aTag !== bTag) {\n return false;\n }\n switch (aTag) {\n case stringTag:\n return a.toString() === b.toString();\n case numberTag: {\n const x = a.valueOf();\n const y = b.valueOf();\n return isEqualsSameValueZero(x, y);\n }\n case booleanTag:\n case dateTag:\n case symbolTag:\n return Object.is(a.valueOf(), b.valueOf());\n case regexpTag: {\n return a.source === b.source && a.flags === b.flags;\n }\n case functionTag: {\n return a === b;\n }\n }\n stack = stack ?? new Map();\n const aStack = stack.get(a);\n const bStack = stack.get(b);\n if (aStack != null && bStack != null) {\n return aStack === b;\n }\n stack.set(a, b);\n stack.set(b, a);\n try {\n switch (aTag) {\n case mapTag: {\n if (a.size !== b.size) {\n return false;\n }\n for (const [key, value] of a.entries()) {\n if (!b.has(key) || !isEqualWithImpl(value, b.get(key), key, a, b, stack, areValuesEqual)) {\n return false;\n }\n }\n return true;\n }\n case setTag: {\n if (a.size !== b.size) {\n return false;\n }\n const aValues = Array.from(a.values());\n const bValues = Array.from(b.values());\n for (let i = 0; i < aValues.length; i++) {\n const aValue = aValues[i];\n const index = bValues.findIndex(bValue => {\n return isEqualWithImpl(aValue, bValue, undefined, a, b, stack, areValuesEqual);\n });\n if (index === -1) {\n return false;\n }\n bValues.splice(index, 1);\n }\n return true;\n }\n case arrayTag:\n case uint8ArrayTag:\n case uint8ClampedArrayTag:\n case uint16ArrayTag:\n case uint32ArrayTag:\n case bigUint64ArrayTag:\n case int8ArrayTag:\n case int16ArrayTag:\n case int32ArrayTag:\n case bigInt64ArrayTag:\n case float32ArrayTag:\n case float64ArrayTag: {\n if (typeof Buffer !== 'undefined' && Buffer.isBuffer(a) !== Buffer.isBuffer(b)) {\n return false;\n }\n if (a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i++) {\n if (!isEqualWithImpl(a[i], b[i], i, a, b, stack, areValuesEqual)) {\n return false;\n }\n }\n return true;\n }\n case arrayBufferTag: {\n if (a.byteLength !== b.byteLength) {\n return false;\n }\n return areObjectsEqual(new Uint8Array(a), new Uint8Array(b), stack, areValuesEqual);\n }\n case dataViewTag: {\n if (a.byteLength !== b.byteLength || a.byteOffset !== b.byteOffset) {\n return false;\n }\n return areObjectsEqual(new Uint8Array(a), new Uint8Array(b), stack, areValuesEqual);\n }\n case errorTag: {\n return a.name === b.name && a.message === b.message;\n }\n case objectTag: {\n const areEqualInstances = areObjectsEqual(a.constructor, b.constructor, stack, areValuesEqual) ||\n (isPlainObject(a) && isPlainObject(b));\n if (!areEqualInstances) {\n return false;\n }\n const aKeys = [...Object.keys(a), ...getSymbols(a)];\n const bKeys = [...Object.keys(b), ...getSymbols(b)];\n if (aKeys.length !== bKeys.length) {\n return false;\n }\n for (let i = 0; i < aKeys.length; i++) {\n const propKey = aKeys[i];\n const aProp = a[propKey];\n if (!Object.hasOwn(b, propKey)) {\n return false;\n }\n const bProp = b[propKey];\n if (!isEqualWithImpl(aProp, bProp, propKey, a, b, stack, areValuesEqual)) {\n return false;\n }\n }\n return true;\n }\n default: {\n return false;\n }\n }\n }\n finally {\n stack.delete(a);\n stack.delete(b);\n }\n}\n\nexport { isEqualWith };\n", "function noop() { }\n\nexport { noop };\n", "import { isEqualWith } from './isEqualWith.mjs';\nimport { noop } from '../function/noop.mjs';\n\nfunction isEqual(a, b) {\n return isEqualWith(a, b, noop);\n}\n\nexport { isEqual };\n", "function noop(..._) { }\n\nexport { noop };\n", "function isBuffer(x) {\n return typeof Buffer !== 'undefined' && Buffer.isBuffer(x);\n}\n\nexport { isBuffer };\n", "function isPrototype(value) {\n const constructor = value?.constructor;\n const prototype = typeof constructor === 'function' ? constructor.prototype : Object.prototype;\n return value === prototype;\n}\n\nexport { isPrototype };\n", "import { isTypedArray as isTypedArray$1 } from '../../predicate/isTypedArray.mjs';\n\nfunction isTypedArray(x) {\n return isTypedArray$1(x);\n}\n\nexport { isTypedArray };\n", "import { toInteger } from './toInteger.mjs';\n\nfunction times(n, getValue) {\n n = toInteger(n);\n if (n < 1 || !Number.isSafeInteger(n)) {\n return [];\n }\n const result = new Array(n);\n for (let i = 0; i < n; i++) {\n result[i] = typeof getValue === 'function' ? getValue(i) : i;\n }\n return result;\n}\n\nexport { times };\n", "import { isBuffer } from '../../predicate/isBuffer.mjs';\nimport { isPrototype } from '../_internal/isPrototype.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { isTypedArray } from '../predicate/isTypedArray.mjs';\nimport { times } from '../util/times.mjs';\n\nfunction keys(object) {\n if (isArrayLike(object)) {\n return arrayLikeKeys(object);\n }\n const result = Object.keys(Object(object));\n if (!isPrototype(object)) {\n return result;\n }\n return result.filter(key => key !== 'constructor');\n}\nfunction arrayLikeKeys(object) {\n const indices = times(object.length, index => `${index}`);\n const filteredKeys = new Set(indices);\n if (isBuffer(object)) {\n filteredKeys.add('offset');\n filteredKeys.add('parent');\n }\n if (isTypedArray(object)) {\n filteredKeys.add('buffer');\n filteredKeys.add('byteLength');\n filteredKeys.add('byteOffset');\n }\n const inheritedKeys = Object.keys(object).filter(key => !filteredKeys.has(key));\n if (Array.isArray(object)) {\n return [...indices, ...inheritedKeys];\n }\n return [...indices.filter(index => Object.hasOwn(object, index)), ...inheritedKeys];\n}\n\nexport { keys };\n", "import { keys } from './keys.mjs';\nimport { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\n\nfunction assign(object, ...sources) {\n for (let i = 0; i < sources.length; i++) {\n assignImpl(object, sources[i]);\n }\n return object;\n}\nfunction assignImpl(object, source) {\n const keys$1 = keys(source);\n for (let i = 0; i < keys$1.length; i++) {\n const key = keys$1[i];\n if (!(key in object) || !isEqualsSameValueZero(object[key], source[key])) {\n object[key] = source[key];\n }\n }\n}\n\nexport { assign };\n", "import { isBuffer } from '../../predicate/isBuffer.mjs';\nimport { isPrototype } from '../_internal/isPrototype.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { isTypedArray } from '../predicate/isTypedArray.mjs';\nimport { times } from '../util/times.mjs';\n\nfunction keysIn(object) {\n if (object == null) {\n return [];\n }\n switch (typeof object) {\n case 'object':\n case 'function': {\n if (isArrayLike(object)) {\n return arrayLikeKeysIn(object);\n }\n if (isPrototype(object)) {\n return prototypeKeysIn(object);\n }\n return keysInImpl(object);\n }\n default: {\n return keysInImpl(Object(object));\n }\n }\n}\nfunction keysInImpl(object) {\n const result = [];\n for (const key in object) {\n result.push(key);\n }\n return result;\n}\nfunction prototypeKeysIn(object) {\n const keys = keysInImpl(object);\n return keys.filter(key => key !== 'constructor');\n}\nfunction arrayLikeKeysIn(object) {\n const indices = times(object.length, index => `${index}`);\n const filteredKeys = new Set(indices);\n if (isBuffer(object)) {\n filteredKeys.add('offset');\n filteredKeys.add('parent');\n }\n if (isTypedArray(object)) {\n filteredKeys.add('buffer');\n filteredKeys.add('byteLength');\n filteredKeys.add('byteOffset');\n }\n const inheritedKeys = keysInImpl(object).filter(key => !filteredKeys.has(key));\n if (Array.isArray(object)) {\n return [...indices, ...inheritedKeys];\n }\n return [...indices.filter(index => Object.hasOwn(object, index)), ...inheritedKeys];\n}\n\nexport { keysIn };\n", "import { keysIn } from './keysIn.mjs';\nimport { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\n\nfunction assignIn(object, ...sources) {\n for (let i = 0; i < sources.length; i++) {\n assignInImpl(object, sources[i]);\n }\n return object;\n}\nfunction assignInImpl(object, source) {\n const keys = keysIn(source);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n if (!(key in object) || !isEqualsSameValueZero(object[key], source[key])) {\n object[key] = source[key];\n }\n }\n}\n\nexport { assignIn };\n", "import { keysIn } from './keysIn.mjs';\nimport { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\n\nfunction assignInWith(object, ...sources) {\n let getValueToAssign = sources[sources.length - 1];\n if (typeof getValueToAssign === 'function') {\n sources.pop();\n }\n else {\n getValueToAssign = undefined;\n }\n for (let i = 0; i < sources.length; i++) {\n assignInWithImpl(object, sources[i], getValueToAssign);\n }\n return object;\n}\nfunction assignInWithImpl(object, source, getValueToAssign) {\n const keys = keysIn(source);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const objValue = object[key];\n const srcValue = source[key];\n const newValue = getValueToAssign?.(objValue, srcValue, key, object, source) ?? srcValue;\n if (!(key in object) || !isEqualsSameValueZero(objValue, newValue)) {\n object[key] = newValue;\n }\n }\n}\n\nexport { assignInWith };\n", "import { keys } from './keys.mjs';\nimport { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\n\nfunction assignWith(object, ...sources) {\n let getValueToAssign = sources[sources.length - 1];\n if (typeof getValueToAssign === 'function') {\n sources.pop();\n }\n else {\n getValueToAssign = undefined;\n }\n for (let i = 0; i < sources.length; i++) {\n assignWithImpl(object, sources[i], getValueToAssign);\n }\n return object;\n}\nfunction assignWithImpl(object, source, getValueToAssign) {\n const keys$1 = keys(source);\n for (let i = 0; i < keys$1.length; i++) {\n const key = keys$1[i];\n const objValue = object[key];\n const srcValue = source[key];\n const newValue = getValueToAssign?.(objValue, srcValue, key, object, source) ?? srcValue;\n if (!(key in object) || !isEqualsSameValueZero(objValue, newValue)) {\n object[key] = newValue;\n }\n }\n}\n\nexport { assignWith };\n", "import { isPrimitive } from '../../predicate/isPrimitive.mjs';\nimport { getTag } from '../_internal/getTag.mjs';\nimport { arrayBufferTag, dataViewTag, booleanTag, numberTag, stringTag, dateTag, regexpTag, symbolTag, mapTag, setTag, argumentsTag, uint32ArrayTag, uint16ArrayTag, uint8ClampedArrayTag, uint8ArrayTag, objectTag, int32ArrayTag, int16ArrayTag, int8ArrayTag, float64ArrayTag, float32ArrayTag, arrayTag } from '../_internal/tags.mjs';\nimport { isArray } from '../predicate/isArray.mjs';\nimport { isTypedArray } from '../predicate/isTypedArray.mjs';\n\nfunction clone(obj) {\n if (isPrimitive(obj)) {\n return obj;\n }\n const tag = getTag(obj);\n if (!isCloneableObject(obj)) {\n return {};\n }\n if (isArray(obj)) {\n const result = Array.from(obj);\n if (obj.length > 0 && typeof obj[0] === 'string' && Object.hasOwn(obj, 'index')) {\n result.index = obj.index;\n result.input = obj.input;\n }\n return result;\n }\n if (isTypedArray(obj)) {\n const typedArray = obj;\n const Ctor = typedArray.constructor;\n return new Ctor(typedArray.buffer, typedArray.byteOffset, typedArray.length);\n }\n if (tag === arrayBufferTag) {\n return new ArrayBuffer(obj.byteLength);\n }\n if (tag === dataViewTag) {\n const dataView = obj;\n const buffer = dataView.buffer;\n const byteOffset = dataView.byteOffset;\n const byteLength = dataView.byteLength;\n const clonedBuffer = new ArrayBuffer(byteLength);\n const srcView = new Uint8Array(buffer, byteOffset, byteLength);\n const destView = new Uint8Array(clonedBuffer);\n destView.set(srcView);\n return new DataView(clonedBuffer);\n }\n if (tag === booleanTag || tag === numberTag || tag === stringTag) {\n const Ctor = obj.constructor;\n const clone = new Ctor(obj.valueOf());\n if (tag === stringTag) {\n cloneStringObjectProperties(clone, obj);\n }\n else {\n copyOwnProperties(clone, obj);\n }\n return clone;\n }\n if (tag === dateTag) {\n return new Date(Number(obj));\n }\n if (tag === regexpTag) {\n const regExp = obj;\n const clone = new RegExp(regExp.source, regExp.flags);\n clone.lastIndex = regExp.lastIndex;\n return clone;\n }\n if (tag === symbolTag) {\n return Object(Symbol.prototype.valueOf.call(obj));\n }\n if (tag === mapTag) {\n const map = obj;\n const result = new Map();\n map.forEach((obj, key) => {\n result.set(key, obj);\n });\n return result;\n }\n if (tag === setTag) {\n const set = obj;\n const result = new Set();\n set.forEach(obj => {\n result.add(obj);\n });\n return result;\n }\n if (tag === argumentsTag) {\n const args = obj;\n const result = {};\n copyOwnProperties(result, args);\n result.length = args.length;\n result[Symbol.iterator] = args[Symbol.iterator];\n return result;\n }\n const result = {};\n copyPrototype(result, obj);\n copyOwnProperties(result, obj);\n copySymbolProperties(result, obj);\n return result;\n}\nfunction isCloneableObject(object) {\n switch (getTag(object)) {\n case argumentsTag:\n case arrayTag:\n case arrayBufferTag:\n case dataViewTag:\n case booleanTag:\n case dateTag:\n case float32ArrayTag:\n case float64ArrayTag:\n case int8ArrayTag:\n case int16ArrayTag:\n case int32ArrayTag:\n case mapTag:\n case numberTag:\n case objectTag:\n case regexpTag:\n case setTag:\n case stringTag:\n case symbolTag:\n case uint8ArrayTag:\n case uint8ClampedArrayTag:\n case uint16ArrayTag:\n case uint32ArrayTag: {\n return true;\n }\n default: {\n return false;\n }\n }\n}\nfunction copyOwnProperties(target, source) {\n for (const key in source) {\n if (Object.hasOwn(source, key)) {\n target[key] = source[key];\n }\n }\n}\nfunction copySymbolProperties(target, source) {\n const symbols = Object.getOwnPropertySymbols(source);\n for (let i = 0; i < symbols.length; i++) {\n const symbol = symbols[i];\n if (Object.prototype.propertyIsEnumerable.call(source, symbol)) {\n target[symbol] = source[symbol];\n }\n }\n}\nfunction cloneStringObjectProperties(target, source) {\n const stringLength = source.valueOf().length;\n for (const key in source) {\n if (Object.hasOwn(source, key) && (Number.isNaN(Number(key)) || Number(key) >= stringLength)) {\n target[key] = source[key];\n }\n }\n}\nfunction copyPrototype(target, source) {\n const proto = Object.getPrototypeOf(source);\n if (proto !== null) {\n const Ctor = source.constructor;\n if (typeof Ctor === 'function') {\n Object.setPrototypeOf(target, proto);\n }\n }\n}\n\nexport { clone };\n", "import { clone } from './clone.mjs';\n\nfunction cloneWith(value, customizer) {\n if (!customizer) {\n return clone(value);\n }\n const result = customizer(value);\n if (result !== undefined) {\n return result;\n }\n return clone(value);\n}\n\nexport { cloneWith };\n", "import { keys } from './keys.mjs';\nimport { assignValue } from '../_internal/assignValue.mjs';\nimport { isObject } from '../predicate/isObject.mjs';\n\nfunction create(prototype, properties) {\n const proto = isObject(prototype) ? Object.create(prototype) : {};\n if (properties != null) {\n const propsKeys = keys(properties);\n for (let i = 0; i < propsKeys.length; i++) {\n const key = propsKeys[i];\n const propsValue = properties[key];\n assignValue(proto, key, propsValue);\n }\n }\n return proto;\n}\n\nexport { create };\n", "import { isNil } from '../../predicate/isNil.mjs';\nimport { isIterateeCall } from '../_internal/isIterateeCall.mjs';\nimport { isEqualsSameValueZero } from '../../_internal/isEqualsSameValueZero.mjs';\n\nfunction defaults(object, ...sources) {\n object = Object(object);\n const objectProto = Object.prototype;\n let length = sources.length;\n const guard = length > 2 ? sources[2] : undefined;\n if (guard && isIterateeCall(sources[0], sources[1], guard)) {\n length = 1;\n }\n for (let i = 0; i < length; i++) {\n if (isNil(sources[i])) {\n continue;\n }\n const source = sources[i];\n const keys = Object.keys(source);\n for (let j = 0; j < keys.length; j++) {\n const key = keys[j];\n const value = object[key];\n if (value === undefined ||\n (!Object.hasOwn(object, key) && isEqualsSameValueZero(value, objectProto[key]))) {\n object[key] = source[key];\n }\n }\n }\n return object;\n}\n\nexport { defaults };\n", "function isPlainObject(object) {\n if (typeof object !== 'object') {\n return false;\n }\n if (object == null) {\n return false;\n }\n if (Object.getPrototypeOf(object) === null) {\n return true;\n }\n if (Object.prototype.toString.call(object) !== '[object Object]') {\n const tag = object[Symbol.toStringTag];\n if (tag == null) {\n return false;\n }\n const isTagReadonly = !Object.getOwnPropertyDescriptor(object, Symbol.toStringTag)?.writable;\n if (isTagReadonly) {\n return false;\n }\n return object.toString() === `[object ${tag}]`;\n }\n let proto = object;\n while (Object.getPrototypeOf(proto) !== null) {\n proto = Object.getPrototypeOf(proto);\n }\n return Object.getPrototypeOf(object) === proto;\n}\n\nexport { isPlainObject };\n", "import { isPlainObject } from '../predicate/isPlainObject.mjs';\n\nfunction defaultsDeep(target, ...sources) {\n target = Object(target);\n for (let i = 0; i < sources.length; i++) {\n const source = sources[i];\n if (source != null) {\n defaultsDeepRecursive(target, source, new WeakMap());\n }\n }\n return target;\n}\nfunction defaultsDeepRecursive(target, source, stack) {\n for (const key in source) {\n const sourceValue = source[key];\n const targetValue = target[key];\n if (targetValue === undefined || !Object.hasOwn(target, key)) {\n target[key] = handleMissingProperty(sourceValue, stack);\n continue;\n }\n if (stack.get(sourceValue) === targetValue) {\n continue;\n }\n handleExistingProperty(targetValue, sourceValue, stack);\n }\n}\nfunction handleMissingProperty(sourceValue, stack) {\n if (stack.has(sourceValue)) {\n return stack.get(sourceValue);\n }\n if (isPlainObject(sourceValue)) {\n const newObj = {};\n stack.set(sourceValue, newObj);\n defaultsDeepRecursive(newObj, sourceValue, stack);\n return newObj;\n }\n return sourceValue;\n}\nfunction handleExistingProperty(targetValue, sourceValue, stack) {\n if (isPlainObject(targetValue) && isPlainObject(sourceValue)) {\n stack.set(sourceValue, targetValue);\n defaultsDeepRecursive(targetValue, sourceValue, stack);\n return;\n }\n if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {\n stack.set(sourceValue, targetValue);\n mergeArrays(targetValue, sourceValue, stack);\n }\n}\nfunction mergeArrays(targetArray, sourceArray, stack) {\n const minLength = Math.min(sourceArray.length, targetArray.length);\n for (let i = 0; i < minLength; i++) {\n if (isPlainObject(targetArray[i]) && isPlainObject(sourceArray[i])) {\n defaultsDeepRecursive(targetArray[i], sourceArray[i], stack);\n }\n }\n for (let i = minLength; i < sourceArray.length; i++) {\n targetArray.push(sourceArray[i]);\n }\n}\n\nexport { defaultsDeep };\n", "function findKey(obj, predicate) {\n const keys = Object.keys(obj);\n return keys.find(key => predicate(obj[key], key, obj));\n}\n\nexport { findKey };\n", "import { findKey as findKey$1 } from '../../object/findKey.mjs';\nimport { identity } from '../function/identity.mjs';\nimport { isObject } from '../predicate/isObject.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction findKey(obj, predicate) {\n if (!isObject(obj)) {\n return undefined;\n }\n const iteratee$1 = iteratee(predicate ?? identity);\n return findKey$1(obj, iteratee$1);\n}\n\nexport { findKey };\n", "import { identity } from '../function/identity.mjs';\nimport { isObject } from '../predicate/isObject.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction findLastKey(obj, predicate) {\n if (!isObject(obj)) {\n return undefined;\n }\n const iteratee$1 = iteratee(predicate ?? identity);\n const keys = Object.keys(obj);\n return keys.findLast(key => iteratee$1(obj[key], key, obj));\n}\n\nexport { findLastKey };\n", "import { identity } from '../../function/identity.mjs';\n\nfunction forIn(object, iteratee = identity) {\n if (object == null) {\n return object;\n }\n for (const key in object) {\n const result = iteratee(object[key], key, object);\n if (result === false) {\n break;\n }\n }\n return object;\n}\n\nexport { forIn };\n", "import { identity } from '../../function/identity.mjs';\n\nfunction forInRight(object, iteratee = identity) {\n if (object == null) {\n return object;\n }\n const keys = [];\n for (const key in object) {\n keys.push(key);\n }\n for (let i = keys.length - 1; i >= 0; i--) {\n const key = keys[i];\n const result = iteratee(object[key], key, object);\n if (result === false) {\n break;\n }\n }\n return object;\n}\n\nexport { forInRight };\n", "import { keys } from './keys.mjs';\nimport { identity } from '../../function/identity.mjs';\n\nfunction forOwn(object, iteratee = identity) {\n if (object == null) {\n return object;\n }\n const iterable = Object(object);\n const keys$1 = keys(object);\n for (let i = 0; i < keys$1.length; ++i) {\n const key = keys$1[i];\n if (iteratee(iterable[key], key, iterable) === false) {\n break;\n }\n }\n return object;\n}\n\nexport { forOwn };\n", "import { keys } from './keys.mjs';\nimport { identity } from '../../function/identity.mjs';\n\nfunction forOwnRight(object, iteratee = identity) {\n if (object == null) {\n return object;\n }\n const iterable = Object(object);\n const keys$1 = keys(object);\n for (let i = keys$1.length - 1; i >= 0; --i) {\n const key = keys$1[i];\n if (iteratee(iterable[key], key, iterable) === false) {\n break;\n }\n }\n return object;\n}\n\nexport { forOwnRight };\n", "import { isArrayLike } from '../predicate/isArrayLike.mjs';\n\nfunction fromPairs(pairs) {\n if (!isArrayLike(pairs)) {\n return {};\n }\n const result = {};\n for (let i = 0; i < pairs.length; i++) {\n const [key, value] = pairs[i];\n result[key] = value;\n }\n return result;\n}\n\nexport { fromPairs };\n", "import { keys } from './keys.mjs';\n\nfunction functions(object) {\n if (object == null) {\n return [];\n }\n return keys(object).filter(key => typeof object[key] === 'function');\n}\n\nexport { functions };\n", "import { isFunction } from '../../predicate/isFunction.mjs';\n\nfunction functionsIn(object) {\n if (object == null) {\n return [];\n }\n const result = [];\n for (const key in object) {\n if (isFunction(object[key])) {\n result.push(key);\n }\n }\n return result;\n}\n\nexport { functionsIn };\n", "import { isDeepKey } from '../_internal/isDeepKey.mjs';\nimport { isIndex } from '../_internal/isIndex.mjs';\nimport { isArguments } from '../predicate/isArguments.mjs';\nimport { toPath } from '../util/toPath.mjs';\n\nfunction hasIn(object, path) {\n if (object == null) {\n return false;\n }\n let resolvedPath;\n if (Array.isArray(path)) {\n resolvedPath = path;\n }\n else if (typeof path === 'string' && isDeepKey(path) && object[path] == null) {\n resolvedPath = toPath(path);\n }\n else {\n resolvedPath = [path];\n }\n if (resolvedPath.length === 0) {\n return false;\n }\n let current = object;\n for (let i = 0; i < resolvedPath.length; i++) {\n const key = resolvedPath[i];\n if (current == null || !(key in Object(current))) {\n const isSparseIndex = (Array.isArray(current) || isArguments(current)) && isIndex(key) && key < current.length;\n if (!isSparseIndex) {\n return false;\n }\n }\n current = current[key];\n }\n return true;\n}\n\nexport { hasIn };\n", "function invert(obj) {\n const result = {};\n const keys = Object.keys(obj);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const value = obj[key];\n result[value] = key;\n }\n return result;\n}\n\nexport { invert };\n", "import { invert as invert$1 } from '../../object/invert.mjs';\n\nfunction invert(obj) {\n return invert$1(obj);\n}\n\nexport { invert };\n", "import { identity } from '../../function/identity.mjs';\nimport { isNil } from '../../predicate/isNil.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction invertBy(object, iteratee$1) {\n const result = {};\n if (isNil(object)) {\n return result;\n }\n if (iteratee$1 == null) {\n iteratee$1 = identity;\n }\n const keys = Object.keys(object);\n const getString = iteratee(iteratee$1);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const value = object[key];\n const valueStr = getString(value);\n if (Array.isArray(result[valueStr])) {\n result[valueStr].push(key);\n }\n else {\n result[valueStr] = [key];\n }\n }\n return result;\n}\n\nexport { invertBy };\n", "function mapKeys(object, getNewKey) {\n const result = {};\n const keys = Object.keys(object);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const value = object[key];\n result[getNewKey(value, key, object)] = value;\n }\n return result;\n}\n\nexport { mapKeys };\n", "import { identity } from '../../function/identity.mjs';\nimport { mapKeys as mapKeys$1 } from '../../object/mapKeys.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction mapKeys(object, getNewKey = identity) {\n if (object == null) {\n return {};\n }\n return mapKeys$1(object, iteratee(getNewKey));\n}\n\nexport { mapKeys };\n", "function mapValues(object, getNewValue) {\n const result = {};\n const keys = Object.keys(object);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const value = object[key];\n result[key] = getNewValue(value, key, object);\n }\n return result;\n}\n\nexport { mapValues };\n", "import { identity } from '../../function/identity.mjs';\nimport { mapValues as mapValues$1 } from '../../object/mapValues.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction mapValues(object, getNewValue = identity) {\n if (object == null) {\n return {};\n }\n return mapValues$1(object, iteratee(getNewValue));\n}\n\nexport { mapValues };\n", "import { isPrimitive } from '../predicate/isPrimitive.mjs';\nimport { isTypedArray } from '../predicate/isTypedArray.mjs';\n\nfunction clone(obj) {\n if (isPrimitive(obj)) {\n return obj;\n }\n if (Array.isArray(obj) ||\n isTypedArray(obj) ||\n obj instanceof ArrayBuffer ||\n (typeof SharedArrayBuffer !== 'undefined' && obj instanceof SharedArrayBuffer)) {\n return obj.slice(0);\n }\n const prototype = Object.getPrototypeOf(obj);\n if (prototype == null) {\n return Object.assign(Object.create(prototype), obj);\n }\n const Constructor = prototype.constructor;\n if (obj instanceof Date || obj instanceof Map || obj instanceof Set) {\n return new Constructor(obj);\n }\n if (obj instanceof RegExp) {\n const newRegExp = new Constructor(obj);\n newRegExp.lastIndex = obj.lastIndex;\n return newRegExp;\n }\n if (obj instanceof DataView) {\n return new Constructor(obj.buffer.slice(0));\n }\n if (obj instanceof Error) {\n let newError;\n if (obj instanceof AggregateError) {\n newError = new Constructor(obj.errors, obj.message, { cause: obj.cause });\n }\n else {\n newError = new Constructor(obj.message, { cause: obj.cause });\n }\n newError.stack = obj.stack;\n Object.assign(newError, obj);\n return newError;\n }\n if (typeof File !== 'undefined' && obj instanceof File) {\n const newFile = new Constructor([obj], obj.name, { type: obj.type, lastModified: obj.lastModified });\n return newFile;\n }\n if (typeof obj === 'object') {\n const newObject = Object.create(prototype);\n return Object.assign(newObject, obj);\n }\n return obj;\n}\n\nexport { clone };\n", "import { cloneDeep } from './cloneDeep.mjs';\nimport { isUnsafeProperty } from '../../_internal/isUnsafeProperty.mjs';\nimport { clone } from '../../object/clone.mjs';\nimport { isPrimitive } from '../../predicate/isPrimitive.mjs';\nimport { getSymbols } from '../_internal/getSymbols.mjs';\nimport { isArguments } from '../predicate/isArguments.mjs';\nimport { isArrayLikeObject } from '../predicate/isArrayLikeObject.mjs';\nimport { isObjectLike } from '../predicate/isObjectLike.mjs';\nimport { isPlainObject } from '../predicate/isPlainObject.mjs';\nimport { isTypedArray } from '../predicate/isTypedArray.mjs';\n\nfunction mergeWith(object, ...otherArgs) {\n const sources = otherArgs.slice(0, -1);\n const merge = otherArgs[otherArgs.length - 1];\n let result = object;\n for (let i = 0; i < sources.length; i++) {\n const source = sources[i];\n result = mergeWithDeep(result, source, merge, new Map());\n }\n return result;\n}\nfunction mergeWithDeep(target, source, merge, stack) {\n if (isPrimitive(target)) {\n target = Object(target);\n }\n if (source == null || typeof source !== 'object') {\n return target;\n }\n if (stack.has(source)) {\n return clone(stack.get(source));\n }\n stack.set(source, target);\n if (Array.isArray(source)) {\n source = source.slice();\n for (let i = 0; i < source.length; i++) {\n source[i] = source[i] ?? undefined;\n }\n }\n const sourceKeys = [...Object.keys(source), ...getSymbols(source)];\n for (let i = 0; i < sourceKeys.length; i++) {\n const key = sourceKeys[i];\n if (isUnsafeProperty(key)) {\n continue;\n }\n let sourceValue = source[key];\n let targetValue = target[key];\n if (isArguments(sourceValue)) {\n sourceValue = { ...sourceValue };\n }\n if (isArguments(targetValue)) {\n targetValue = { ...targetValue };\n }\n if (typeof Buffer !== 'undefined' && Buffer.isBuffer(sourceValue)) {\n sourceValue = cloneDeep(sourceValue);\n }\n if (Array.isArray(sourceValue)) {\n if (Array.isArray(targetValue)) {\n const cloned = [];\n const targetKeys = Reflect.ownKeys(targetValue);\n for (let i = 0; i < targetKeys.length; i++) {\n const targetKey = targetKeys[i];\n cloned[targetKey] = targetValue[targetKey];\n }\n targetValue = cloned;\n }\n else if (isArrayLikeObject(targetValue)) {\n const cloned = [];\n for (let i = 0; i < targetValue.length; i++) {\n cloned[i] = targetValue[i];\n }\n targetValue = cloned;\n }\n else {\n targetValue = [];\n }\n }\n const merged = merge(targetValue, sourceValue, key, target, source, stack);\n if (merged !== undefined) {\n target[key] = merged;\n }\n else if (Array.isArray(sourceValue)) {\n target[key] = mergeWithDeep(targetValue, sourceValue, merge, stack);\n }\n else if (isObjectLike(targetValue) &&\n isObjectLike(sourceValue) &&\n (isPlainObject(targetValue) ||\n isPlainObject(sourceValue) ||\n isTypedArray(targetValue) ||\n isTypedArray(sourceValue))) {\n target[key] = mergeWithDeep(targetValue, sourceValue, merge, stack);\n }\n else if (targetValue == null && isPlainObject(sourceValue)) {\n target[key] = mergeWithDeep({}, sourceValue, merge, stack);\n }\n else if (targetValue == null && isTypedArray(sourceValue)) {\n target[key] = cloneDeep(sourceValue);\n }\n else if (targetValue === undefined || sourceValue !== undefined) {\n target[key] = sourceValue;\n }\n }\n return target;\n}\n\nexport { mergeWith };\n", "import { mergeWith } from './mergeWith.mjs';\nimport { noop } from '../../function/noop.mjs';\n\nfunction merge(object, ...sources) {\n return mergeWith(object, ...sources, noop);\n}\n\nexport { merge };\n", "import { getSymbols } from './getSymbols.mjs';\n\nfunction getSymbolsIn(object) {\n const result = [];\n while (object) {\n result.push(...getSymbols(object));\n object = Object.getPrototypeOf(object);\n }\n return result;\n}\n\nexport { getSymbolsIn };\n", "import { cloneDeepWith } from './cloneDeepWith.mjs';\nimport { keysIn } from './keysIn.mjs';\nimport { unset } from './unset.mjs';\nimport { getSymbolsIn } from '../_internal/getSymbolsIn.mjs';\nimport { isDeepKey } from '../_internal/isDeepKey.mjs';\nimport { flatten } from '../array/flatten.mjs';\nimport { isPlainObject } from '../predicate/isPlainObject.mjs';\n\nfunction omit(obj, ...keysArr) {\n if (obj == null) {\n return {};\n }\n keysArr = flatten(keysArr);\n const result = cloneInOmit(obj, keysArr);\n for (let i = 0; i < keysArr.length; i++) {\n let keys = keysArr[i];\n switch (typeof keys) {\n case 'object': {\n if (!Array.isArray(keys)) {\n keys = Array.from(keys);\n }\n for (let j = 0; j < keys.length; j++) {\n const key = keys[j];\n unset(result, key);\n }\n break;\n }\n case 'string':\n case 'symbol':\n case 'number': {\n unset(result, keys);\n break;\n }\n }\n }\n return result;\n}\nfunction cloneInOmit(obj, keys) {\n const hasDeepKey = keys.some(key => Array.isArray(key) || isDeepKey(key));\n if (hasDeepKey) {\n return deepCloneInOmit(obj);\n }\n return shallowCloneInOmit(obj);\n}\nfunction shallowCloneInOmit(obj) {\n const result = {};\n const keysToCopy = [...keysIn(obj), ...getSymbolsIn(obj)];\n for (let i = 0; i < keysToCopy.length; i++) {\n const key = keysToCopy[i];\n result[key] = obj[key];\n }\n return result;\n}\nfunction deepCloneInOmit(obj) {\n const result = {};\n const keysToCopy = [...keysIn(obj), ...getSymbolsIn(obj)];\n for (let i = 0; i < keysToCopy.length; i++) {\n const key = keysToCopy[i];\n result[key] = cloneDeepWith(obj[key], valueToClone => {\n if (isPlainObject(valueToClone)) {\n return undefined;\n }\n return valueToClone;\n });\n }\n return result;\n}\n\nexport { omit };\n", "import { keysIn } from './keysIn.mjs';\nimport { range } from '../../math/range.mjs';\nimport { getSymbolsIn } from '../_internal/getSymbolsIn.mjs';\nimport { identity } from '../function/identity.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { isSymbol } from '../predicate/isSymbol.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction omitBy(object, shouldOmit) {\n if (object == null) {\n return {};\n }\n const result = {};\n const predicate = iteratee(shouldOmit ?? identity);\n const keys = isArrayLike(object)\n ? range(0, object.length)\n : [...keysIn(object), ...getSymbolsIn(object)];\n for (let i = 0; i < keys.length; i++) {\n const key = (isSymbol(keys[i]) ? keys[i] : keys[i].toString());\n const value = object[key];\n if (!predicate(value, key, object)) {\n result[key] = value;\n }\n }\n return result;\n}\n\nexport { omitBy };\n", "import { get } from './get.mjs';\nimport { has } from './has.mjs';\nimport { set } from './set.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { isNil } from '../predicate/isNil.mjs';\n\nfunction pick(obj, ...keysArr) {\n if (isNil(obj)) {\n return {};\n }\n const result = {};\n for (let i = 0; i < keysArr.length; i++) {\n let keys = keysArr[i];\n switch (typeof keys) {\n case 'object': {\n if (!Array.isArray(keys)) {\n if (isArrayLike(keys)) {\n keys = Array.from(keys);\n }\n else {\n keys = [keys];\n }\n }\n break;\n }\n case 'string':\n case 'symbol':\n case 'number': {\n keys = [keys];\n break;\n }\n }\n for (const key of keys) {\n const value = get(obj, key);\n if (value === undefined && !has(obj, key)) {\n continue;\n }\n if (typeof key === 'string' && Object.hasOwn(obj, key)) {\n result[key] = value;\n }\n else {\n set(result, key, value);\n }\n }\n }\n return result;\n}\n\nexport { pick };\n", "import { keysIn } from './keysIn.mjs';\nimport { range } from '../../math/range.mjs';\nimport { getSymbolsIn } from '../_internal/getSymbolsIn.mjs';\nimport { identity } from '../function/identity.mjs';\nimport { isArrayLike } from '../predicate/isArrayLike.mjs';\nimport { isSymbol } from '../predicate/isSymbol.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction pickBy(obj, shouldPick) {\n if (obj == null) {\n return {};\n }\n const predicate = iteratee(shouldPick ?? identity);\n const result = {};\n const keys = isArrayLike(obj) ? range(0, obj.length) : [...keysIn(obj), ...getSymbolsIn(obj)];\n for (let i = 0; i < keys.length; i++) {\n const key = (isSymbol(keys[i]) ? keys[i] : keys[i].toString());\n const value = obj[key];\n if (predicate(value, key, obj)) {\n result[key] = value;\n }\n }\n return result;\n}\n\nexport { pickBy };\n", "import { get } from './get.mjs';\n\nfunction propertyOf(object) {\n return function (path) {\n return get(object, path);\n };\n}\n\nexport { propertyOf };\n", "import { isKey } from '../_internal/isKey.mjs';\nimport { toKey } from '../_internal/toKey.mjs';\nimport { toPath } from '../util/toPath.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction result(object, path, defaultValue) {\n if (isKey(path, object)) {\n path = [path];\n }\n else if (!Array.isArray(path)) {\n path = toPath(toString(path));\n }\n const pathLength = Math.max(path.length, 1);\n for (let index = 0; index < pathLength; index++) {\n const value = object == null ? undefined : object[toKey(path[index])];\n if (value === undefined) {\n return typeof defaultValue === 'function' ? defaultValue.call(object) : defaultValue;\n }\n object = typeof value === 'function' ? value.call(object) : value;\n }\n return object;\n}\n\nexport { result };\n", "import { updateWith } from './updateWith.mjs';\n\nfunction setWith(obj, path, value, customizer) {\n let customizerFn;\n if (typeof customizer === 'function') {\n customizerFn = customizer;\n }\n else {\n customizerFn = () => undefined;\n }\n return updateWith(obj, path, () => value, customizerFn);\n}\n\nexport { setWith };\n", "import { cloneDeep } from './cloneDeep.mjs';\nimport { defaults } from './defaults.mjs';\n\nfunction toDefaulted(object, ...sources) {\n const cloned = cloneDeep(object);\n return defaults(cloned, ...sources);\n}\n\nexport { toDefaulted };\n", "function mapToEntries(map) {\n const arr = new Array(map.size);\n const keys = map.keys();\n const values = map.values();\n for (let i = 0; i < arr.length; i++) {\n arr[i] = [keys.next().value, values.next().value];\n }\n return arr;\n}\n\nexport { mapToEntries };\n", "function setToEntries(set) {\n const arr = new Array(set.size);\n const values = set.values();\n for (let i = 0; i < arr.length; i++) {\n const value = values.next().value;\n arr[i] = [value, value];\n }\n return arr;\n}\n\nexport { setToEntries };\n", "import { keys } from './keys.mjs';\nimport { mapToEntries } from '../_internal/mapToEntries.mjs';\nimport { setToEntries } from '../_internal/setToEntries.mjs';\n\nfunction toPairs(object) {\n if (object == null) {\n return [];\n }\n if (object instanceof Set) {\n return setToEntries(object);\n }\n if (object instanceof Map) {\n return mapToEntries(object);\n }\n const keys$1 = keys(object);\n const result = new Array(keys$1.length);\n for (let i = 0; i < keys$1.length; i++) {\n const key = keys$1[i];\n const value = object[key];\n result[i] = [key, value];\n }\n return result;\n}\n\nexport { toPairs };\n", "import { keysIn } from './keysIn.mjs';\nimport { mapToEntries } from '../_internal/mapToEntries.mjs';\nimport { setToEntries } from '../_internal/setToEntries.mjs';\n\nfunction toPairsIn(object) {\n if (object == null) {\n return [];\n }\n if (object instanceof Set) {\n return setToEntries(object);\n }\n if (object instanceof Map) {\n return mapToEntries(object);\n }\n const keys = keysIn(object);\n const result = new Array(keys.length);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const value = object[key];\n result[i] = [key, value];\n }\n return result;\n}\n\nexport { toPairsIn };\n", "import { isBuffer as isBuffer$1 } from '../../predicate/isBuffer.mjs';\n\nfunction isBuffer(x) {\n return isBuffer$1(x);\n}\n\nexport { isBuffer };\n", "import { identity } from '../../function/identity.mjs';\nimport { isFunction } from '../../predicate/isFunction.mjs';\nimport { forEach } from '../array/forEach.mjs';\nimport { isBuffer } from '../predicate/isBuffer.mjs';\nimport { isObject } from '../predicate/isObject.mjs';\nimport { isTypedArray } from '../predicate/isTypedArray.mjs';\nimport { iteratee } from '../util/iteratee.mjs';\n\nfunction transform(object, iteratee$1 = identity, accumulator) {\n const isArrayOrBufferOrTypedArray = Array.isArray(object) || isBuffer(object) || isTypedArray(object);\n iteratee$1 = iteratee(iteratee$1);\n if (accumulator == null) {\n if (isArrayOrBufferOrTypedArray) {\n accumulator = [];\n }\n else if (isObject(object) && isFunction(object.constructor)) {\n accumulator = Object.create(Object.getPrototypeOf(object));\n }\n else {\n accumulator = {};\n }\n }\n if (object == null) {\n return accumulator;\n }\n forEach(object, (value, key, object) => iteratee$1(accumulator, value, key, object));\n return accumulator;\n}\n\nexport { transform };\n", "import { updateWith } from './updateWith.mjs';\n\nfunction update(obj, path, updater) {\n return updateWith(obj, path, updater, () => undefined);\n}\n\nexport { update };\n", "import { keysIn } from './keysIn.mjs';\n\nfunction valuesIn(object) {\n const keys = keysIn(object);\n const result = new Array(keys.length);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n result[i] = object[key];\n }\n return result;\n}\n\nexport { valuesIn };\n", "function isFunction(value) {\n return typeof value === 'function';\n}\n\nexport { isFunction };\n", "function isLength(value) {\n return Number.isSafeInteger(value) && value >= 0;\n}\n\nexport { isLength };\n", "const functionToString = Function.prototype.toString;\nconst REGEXP_SYNTAX_CHARS = /[\\\\^$.*+?()[\\]{}|]/g;\nconst IS_NATIVE_FUNCTION_REGEXP = RegExp(`^${functionToString\n .call(Object.prototype.hasOwnProperty)\n .replace(REGEXP_SYNTAX_CHARS, '\\\\$&')\n .replace(/hasOwnProperty|(function).*?(?=\\\\\\()| for .+?(?=\\\\\\])/g, '$1.*?')}$`);\nfunction isNative(value) {\n if (typeof value !== 'function') {\n return false;\n }\n if (globalThis?.['__core-js_shared__'] != null) {\n throw new Error('Unsupported core-js use. Try https://npms.io/search?q=ponyfill.');\n }\n return IS_NATIVE_FUNCTION_REGEXP.test(functionToString.call(value));\n}\n\nexport { isNative };\n", "function isNull(value) {\n return value === null;\n}\n\nexport { isNull };\n", "import { isUndefined as isUndefined$1 } from '../../predicate/isUndefined.mjs';\n\nfunction isUndefined(x) {\n return isUndefined$1(x);\n}\n\nexport { isUndefined };\n", "function conformsTo(target, source) {\n if (source == null) {\n return true;\n }\n if (target == null) {\n return Object.keys(source).length === 0;\n }\n const keys = Object.keys(source);\n for (let i = 0; i < keys.length; i++) {\n const key = keys[i];\n const predicate = source[key];\n const value = target[key];\n if (value === undefined && !(key in target)) {\n return false;\n }\n if (typeof predicate === 'function' && !predicate(value)) {\n return false;\n }\n }\n return true;\n}\n\nexport { conformsTo };\n", "import { conformsTo } from './conformsTo.mjs';\nimport { cloneDeep } from '../../object/cloneDeep.mjs';\n\nfunction conforms(source) {\n source = cloneDeep(source);\n return function (object) {\n return conformsTo(object, source);\n };\n}\n\nexport { conforms };\n", "function isArrayBuffer(value) {\n return value instanceof ArrayBuffer;\n}\n\nexport { isArrayBuffer };\n", "import { isArrayBuffer as isArrayBuffer$1 } from '../../predicate/isArrayBuffer.mjs';\n\nfunction isArrayBuffer(value) {\n return isArrayBuffer$1(value);\n}\n\nexport { isArrayBuffer };\n", "function isBoolean(value) {\n return typeof value === 'boolean' || value instanceof Boolean;\n}\n\nexport { isBoolean };\n", "function isDate(value) {\n return value instanceof Date;\n}\n\nexport { isDate };\n", "import { isDate as isDate$1 } from '../../predicate/isDate.mjs';\n\nfunction isDate(value) {\n return isDate$1(value);\n}\n\nexport { isDate };\n", "import { isObjectLike } from './isObjectLike.mjs';\nimport { isPlainObject } from './isPlainObject.mjs';\n\nfunction isElement(value) {\n return isObjectLike(value) && value.nodeType === 1 && !isPlainObject(value);\n}\n\nexport { isElement };\n", "import { isArguments } from './isArguments.mjs';\nimport { isArrayLike } from './isArrayLike.mjs';\nimport { isTypedArray } from './isTypedArray.mjs';\nimport { isPrototype } from '../_internal/isPrototype.mjs';\n\nfunction isEmpty(value) {\n if (value == null) {\n return true;\n }\n if (isArrayLike(value)) {\n if (typeof value.splice !== 'function' &&\n typeof value !== 'string' &&\n (typeof Buffer === 'undefined' || !Buffer.isBuffer(value)) &&\n !isTypedArray(value) &&\n !isArguments(value)) {\n return false;\n }\n return value.length === 0;\n }\n if (typeof value === 'object') {\n if (value instanceof Map || value instanceof Set) {\n return value.size === 0;\n }\n const keys = Object.keys(value);\n if (isPrototype(value)) {\n return keys.filter(x => x !== 'constructor').length === 0;\n }\n return keys.length === 0;\n }\n return true;\n}\n\nexport { isEmpty };\n", "function after(n, func) {\n if (!Number.isInteger(n) || n < 0) {\n throw new Error(`n must be a non-negative integer.`);\n }\n let counter = 0;\n return (...args) => {\n if (++counter >= n) {\n return func(...args);\n }\n return undefined;\n };\n}\n\nexport { after };\n", "import { after } from '../../function/after.mjs';\nimport { isEqualWith as isEqualWith$1 } from '../../predicate/isEqualWith.mjs';\n\nfunction isEqualWith(a, b, areValuesEqual) {\n if (typeof areValuesEqual !== 'function') {\n areValuesEqual = () => undefined;\n }\n return isEqualWith$1(a, b, (...args) => {\n const result = areValuesEqual(...args);\n if (result !== undefined) {\n return Boolean(result);\n }\n if (a instanceof Map && b instanceof Map) {\n return isEqualWith(Array.from(a), Array.from(b), after(2, areValuesEqual));\n }\n if (a instanceof Set && b instanceof Set) {\n return isEqualWith(Array.from(a), Array.from(b), after(2, areValuesEqual));\n }\n });\n}\n\nexport { isEqualWith };\n", "import { getTag } from '../_internal/getTag.mjs';\n\nfunction isError(value) {\n return getTag(value) === '[object Error]';\n}\n\nexport { isError };\n", "function isFinite(value) {\n return Number.isFinite(value);\n}\n\nexport { isFinite };\n", "function isInteger(value) {\n return Number.isInteger(value);\n}\n\nexport { isInteger };\n", "function isRegExp(value) {\n return value instanceof RegExp;\n}\n\nexport { isRegExp };\n", "import { isRegExp as isRegExp$1 } from '../../predicate/isRegExp.mjs';\n\nfunction isRegExp(value) {\n return isRegExp$1(value);\n}\n\nexport { isRegExp };\n", "function isSafeInteger(value) {\n return Number.isSafeInteger(value);\n}\n\nexport { isSafeInteger };\n", "function isSet(value) {\n return value instanceof Set;\n}\n\nexport { isSet };\n", "import { isSet as isSet$1 } from '../../predicate/isSet.mjs';\n\nfunction isSet(value) {\n return isSet$1(value);\n}\n\nexport { isSet };\n", "function isWeakMap(value) {\n return value instanceof WeakMap;\n}\n\nexport { isWeakMap };\n", "import { isWeakMap as isWeakMap$1 } from '../../predicate/isWeakMap.mjs';\n\nfunction isWeakMap(value) {\n return isWeakMap$1(value);\n}\n\nexport { isWeakMap };\n", "function isWeakSet(value) {\n return value instanceof WeakSet;\n}\n\nexport { isWeakSet };\n", "import { isWeakSet as isWeakSet$1 } from '../../predicate/isWeakSet.mjs';\n\nfunction isWeakSet(value) {\n return isWeakSet$1(value);\n}\n\nexport { isWeakSet };\n", "function capitalize(str) {\n return (str.charAt(0).toUpperCase() + str.slice(1).toLowerCase());\n}\n\nexport { capitalize };\n", "import { capitalize as capitalize$1 } from '../../string/capitalize.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction capitalize(str) {\n return capitalize$1(toString(str));\n}\n\nexport { capitalize };\n", "import { isFunction } from '../../predicate/isFunction.mjs';\nimport { isArray } from '../predicate/isArray.mjs';\nimport { isObject } from '../predicate/isObject.mjs';\nimport { toString } from './toString.mjs';\n\nfunction bindAll(object, ...methodNames) {\n if (object == null) {\n return object;\n }\n if (!isObject(object)) {\n return object;\n }\n if (isArray(object) && methodNames.length === 0) {\n return object;\n }\n const methods = [];\n for (let i = 0; i < methodNames.length; i++) {\n const name = methodNames[i];\n if (isArray(name)) {\n methods.push(...name);\n }\n else if (name && typeof name === 'object' && 'length' in name) {\n methods.push(...Array.from(name));\n }\n else {\n methods.push(name);\n }\n }\n if (methods.length === 0) {\n return object;\n }\n for (let i = 0; i < methods.length; i++) {\n const key = methods[i];\n const stringKey = toString(key);\n const func = object[stringKey];\n if (isFunction(func)) {\n object[stringKey] = func.bind(object);\n }\n }\n return object;\n}\n\nexport { bindAll };\n", "const deburrMap = new Map([\n ['\u00C6', 'Ae'],\n ['\u00D0', 'D'],\n ['\u00D8', 'O'],\n ['\u00DE', 'Th'],\n ['\u00DF', 'ss'],\n ['\u00E6', 'ae'],\n ['\u00F0', 'd'],\n ['\u00F8', 'o'],\n ['\u00FE', 'th'],\n ['\u0110', 'D'],\n ['\u0111', 'd'],\n ['\u0126', 'H'],\n ['\u0127', 'h'],\n ['\u0131', 'i'],\n ['\u0132', 'IJ'],\n ['\u0133', 'ij'],\n ['\u0138', 'k'],\n ['\u013F', 'L'],\n ['\u0140', 'l'],\n ['\u0141', 'L'],\n ['\u0142', 'l'],\n ['\u0149', \"'n\"],\n ['\u014A', 'N'],\n ['\u014B', 'n'],\n ['\u0152', 'Oe'],\n ['\u0153', 'oe'],\n ['\u0166', 'T'],\n ['\u0167', 't'],\n ['\u017F', 's'],\n]);\nfunction deburr(str) {\n str = str.normalize('NFD');\n let result = '';\n for (let i = 0; i < str.length; i++) {\n const char = str[i];\n if ((char >= '\\u0300' && char <= '\\u036f') || (char >= '\\ufe20' && char <= '\\ufe23')) {\n continue;\n }\n result += deburrMap.get(char) ?? char;\n }\n return result;\n}\n\nexport { deburr };\n", "import { deburr as deburr$1 } from '../../string/deburr.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction deburr(str) {\n return deburr$1(toString(str));\n}\n\nexport { deburr };\n", "const CASE_SPLIT_PATTERN = /\\p{Lu}?\\p{Ll}+|[0-9]+|\\p{Lu}+(?!\\p{Ll})|\\p{Emoji_Presentation}|\\p{Extended_Pictographic}|\\p{L}+/gu;\nfunction words(str) {\n return Array.from(str.match(CASE_SPLIT_PATTERN) ?? []);\n}\n\nexport { CASE_SPLIT_PATTERN, words };\n", "import { capitalize } from './capitalize.mjs';\nimport { words } from './words.mjs';\n\nfunction camelCase(str) {\n const words$1 = words(str);\n if (words$1.length === 0) {\n return '';\n }\n const [first, ...rest] = words$1;\n return `${first.toLowerCase()}${rest.map(word => capitalize(word)).join('')}`;\n}\n\nexport { camelCase };\n", "import { toString } from '../util/toString.mjs';\n\nfunction normalizeForCase(str) {\n if (typeof str !== 'string') {\n str = toString(str);\n }\n return str.replace(/['\\u2019]/g, '');\n}\n\nexport { normalizeForCase };\n", "import { deburr } from './deburr.mjs';\nimport { camelCase as camelCase$1 } from '../../string/camelCase.mjs';\nimport { normalizeForCase } from '../_internal/normalizeForCase.mjs';\n\nfunction camelCase(str) {\n return camelCase$1(normalizeForCase(deburr(str)));\n}\n\nexport { camelCase };\n", "function endsWith(str, target, position) {\n if (str == null || target == null) {\n return false;\n }\n if (position == null) {\n position = str.length;\n }\n return str.endsWith(target, position);\n}\n\nexport { endsWith };\n", "const htmlEscapes = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n};\nfunction escape(str) {\n return str.replace(/[&<>\"']/g, match => htmlEscapes[match]);\n}\n\nexport { escape };\n", "import { escape as escape$1 } from '../../string/escape.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction escape(string) {\n return escape$1(toString(string));\n}\n\nexport { escape };\n", "function escapeRegExp(str) {\n return str.replace(/[\\\\^$.*+?()[\\]{}|]/g, '\\\\$&');\n}\n\nexport { escapeRegExp };\n", "import { escapeRegExp as escapeRegExp$1 } from '../../string/escapeRegExp.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction escapeRegExp(str) {\n return escapeRegExp$1(toString(str));\n}\n\nexport { escapeRegExp };\n", "import { words } from './words.mjs';\n\nfunction kebabCase(str) {\n const words$1 = words(str);\n return words$1.map(word => word.toLowerCase()).join('-');\n}\n\nexport { kebabCase };\n", "import { deburr } from './deburr.mjs';\nimport { kebabCase as kebabCase$1 } from '../../string/kebabCase.mjs';\nimport { normalizeForCase } from '../_internal/normalizeForCase.mjs';\n\nfunction kebabCase(str) {\n return kebabCase$1(normalizeForCase(deburr(str)));\n}\n\nexport { kebabCase };\n", "import { words } from './words.mjs';\n\nfunction lowerCase(str) {\n const words$1 = words(str);\n return words$1.map(word => word.toLowerCase()).join(' ');\n}\n\nexport { lowerCase };\n", "import { deburr } from './deburr.mjs';\nimport { lowerCase as lowerCase$1 } from '../../string/lowerCase.mjs';\nimport { normalizeForCase } from '../_internal/normalizeForCase.mjs';\n\nfunction lowerCase(str) {\n return lowerCase$1(normalizeForCase(deburr(str)));\n}\n\nexport { lowerCase };\n", "function lowerFirst(str) {\n return str.substring(0, 1).toLowerCase() + str.substring(1);\n}\n\nexport { lowerFirst };\n", "import { lowerFirst as lowerFirst$1 } from '../../string/lowerFirst.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction lowerFirst(str) {\n return lowerFirst$1(toString(str));\n}\n\nexport { lowerFirst };\n", "function pad(str, length, chars = ' ') {\n return str.padStart(Math.floor((length - str.length) / 2) + str.length, chars).padEnd(length, chars);\n}\n\nexport { pad };\n", "import { pad as pad$1 } from '../../string/pad.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction pad(str, length, chars) {\n return pad$1(toString(str), length, chars);\n}\n\nexport { pad };\n", "import { toString } from '../util/toString.mjs';\n\nfunction padEnd(str, length = 0, chars = ' ') {\n return toString(str).padEnd(length, chars);\n}\n\nexport { padEnd };\n", "import { toString } from '../util/toString.mjs';\n\nfunction padStart(str, length = 0, chars = ' ') {\n return toString(str).padStart(length, chars);\n}\n\nexport { padStart };\n", "const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;\n\nexport { MAX_SAFE_INTEGER };\n", "import { isIterateeCall } from '../_internal/isIterateeCall.mjs';\nimport { MAX_SAFE_INTEGER } from '../_internal/MAX_SAFE_INTEGER.mjs';\nimport { toInteger } from '../util/toInteger.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction repeat(str, n, guard) {\n if (guard ? isIterateeCall(str, n, guard) : n === undefined) {\n n = 1;\n }\n else {\n n = toInteger(n);\n }\n if (n < 1 || n > MAX_SAFE_INTEGER) {\n return '';\n }\n return toString(str).repeat(n);\n}\n\nexport { repeat };\n", "import { toString } from '../util/toString.mjs';\n\nfunction replace(target, pattern, replacement) {\n if (arguments.length < 3) {\n return toString(target);\n }\n return toString(target).replace(pattern, replacement);\n}\n\nexport { replace };\n", "import { words } from './words.mjs';\n\nfunction snakeCase(str) {\n const words$1 = words(str);\n return words$1.map(word => word.toLowerCase()).join('_');\n}\n\nexport { snakeCase };\n", "import { deburr } from './deburr.mjs';\nimport { snakeCase as snakeCase$1 } from '../../string/snakeCase.mjs';\nimport { normalizeForCase } from '../_internal/normalizeForCase.mjs';\n\nfunction snakeCase(str) {\n return snakeCase$1(normalizeForCase(deburr(str)));\n}\n\nexport { snakeCase };\n", "import { toString } from '../util/toString.mjs';\n\nfunction split(string, separator, limit) {\n return toString(string).split(separator, limit);\n}\n\nexport { split };\n", "import { deburr } from './deburr.mjs';\nimport { words } from '../../string/words.mjs';\nimport { normalizeForCase } from '../_internal/normalizeForCase.mjs';\n\nfunction startCase(str) {\n const words$1 = words(normalizeForCase(deburr(str)).trim());\n let result = '';\n for (let i = 0; i < words$1.length; i++) {\n const word = words$1[i];\n if (result) {\n result += ' ';\n }\n if (word === word.toUpperCase()) {\n result += word;\n }\n else {\n result += word[0].toUpperCase() + word.slice(1).toLowerCase();\n }\n }\n return result;\n}\n\nexport { startCase };\n", "function startsWith(str, target, position) {\n if (str == null || target == null) {\n return false;\n }\n if (position == null) {\n position = 0;\n }\n return str.startsWith(target, position);\n}\n\nexport { startsWith };\n", "import { escape } from './escape.mjs';\nimport { attempt } from '../function/attempt.mjs';\nimport { defaults } from '../object/defaults.mjs';\nimport { toString } from '../util/toString.mjs';\n\nconst esTemplateRegExp = /\\$\\{([^\\\\}]*(?:\\\\.[^\\\\}]*)*)\\}/g;\nconst unEscapedRegExp = /['\\n\\r\\u2028\\u2029\\\\]/g;\nconst noMatchExp = /($^)/;\nconst escapeMap = new Map([\n ['\\\\', '\\\\'],\n [\"'\", \"'\"],\n ['\\n', 'n'],\n ['\\r', 'r'],\n ['\\u2028', 'u2028'],\n ['\\u2029', 'u2029'],\n]);\nfunction escapeString(match) {\n return `\\\\${escapeMap.get(match)}`;\n}\nconst defaultInterpolateRegExp = /<%=([\\s\\S]+?)%>/g;\nconst templateSettings = {\n escape: /<%-([\\s\\S]+?)%>/g,\n evaluate: /<%([\\s\\S]+?)%>/g,\n interpolate: defaultInterpolateRegExp,\n variable: '',\n imports: {\n _: {\n escape,\n template,\n },\n },\n};\nfunction template(string, options, guard) {\n string = toString(string);\n if (guard) {\n options = templateSettings;\n }\n options = defaults({ ...options }, templateSettings);\n const delimitersRegExp = new RegExp([\n options.escape?.source ?? noMatchExp.source,\n options.interpolate?.source ?? noMatchExp.source,\n options.interpolate === defaultInterpolateRegExp ? esTemplateRegExp.source : noMatchExp.source,\n options.evaluate?.source ?? noMatchExp.source,\n '$',\n ].join('|'), 'g');\n let lastIndex = 0;\n let isEvaluated = false;\n let source = `__p += ''`;\n for (const match of string.matchAll(delimitersRegExp)) {\n const [fullMatch, escapeValue, interpolateValue, esTemplateValue, evaluateValue] = match;\n const { index } = match;\n source += ` + '${string.slice(lastIndex, index).replace(unEscapedRegExp, escapeString)}'`;\n if (escapeValue) {\n source += ` + _.escape(${escapeValue})`;\n }\n if (interpolateValue) {\n source += ` + ((${interpolateValue}) == null ? '' : ${interpolateValue})`;\n }\n else if (esTemplateValue) {\n source += ` + ((${esTemplateValue}) == null ? '' : ${esTemplateValue})`;\n }\n if (evaluateValue) {\n source += `;\\n${evaluateValue};\\n __p += ''`;\n isEvaluated = true;\n }\n lastIndex = index + fullMatch.length;\n }\n const imports = defaults({ ...options.imports }, templateSettings.imports);\n const importsKeys = Object.keys(imports);\n const importValues = Object.values(imports);\n const sourceURL = `//# sourceURL=${options.sourceURL ? String(options.sourceURL).replace(/[\\r\\n]/g, ' ') : `es-toolkit.templateSource[${Date.now()}]`}\\n`;\n const compiledFunction = `function(${options.variable || 'obj'}) {\n let __p = '';\n ${options.variable ? '' : 'if (obj == null) { obj = {}; }'}\n ${isEvaluated ? `function print() { __p += Array.prototype.join.call(arguments, ''); }` : ''}\n ${options.variable ? source : `with(obj) {\\n${source}\\n}`}\n return __p;\n }`;\n const result = attempt(() => new Function(...importsKeys, `${sourceURL}return ${compiledFunction}`)(...importValues));\n result.source = compiledFunction;\n if (result instanceof Error) {\n throw result;\n }\n return result;\n}\n\nexport { template, templateSettings };\n", "import { toString } from '../util/toString.mjs';\n\nfunction toLower(value) {\n return toString(value).toLowerCase();\n}\n\nexport { toLower };\n", "import { toString } from '../util/toString.mjs';\n\nfunction toUpper(value) {\n return toString(value).toUpperCase();\n}\n\nexport { toUpper };\n", "function trimEnd(str, chars) {\n if (chars === undefined) {\n return str.trimEnd();\n }\n let endIndex = str.length;\n switch (typeof chars) {\n case 'string': {\n if (chars.length !== 1) {\n throw new Error(`The 'chars' parameter should be a single character string.`);\n }\n while (endIndex > 0 && str[endIndex - 1] === chars) {\n endIndex--;\n }\n break;\n }\n case 'object': {\n while (endIndex > 0 && chars.includes(str[endIndex - 1])) {\n endIndex--;\n }\n }\n }\n return str.substring(0, endIndex);\n}\n\nexport { trimEnd };\n", "function trimStart(str, chars) {\n if (chars === undefined) {\n return str.trimStart();\n }\n let startIndex = 0;\n switch (typeof chars) {\n case 'string': {\n while (startIndex < str.length && str[startIndex] === chars) {\n startIndex++;\n }\n break;\n }\n case 'object': {\n while (startIndex < str.length && chars.includes(str[startIndex])) {\n startIndex++;\n }\n }\n }\n return str.substring(startIndex);\n}\n\nexport { trimStart };\n", "import { trimEnd } from './trimEnd.mjs';\nimport { trimStart } from './trimStart.mjs';\n\nfunction trim(str, chars) {\n if (chars === undefined) {\n return str.trim();\n }\n return trimStart(trimEnd(str, chars), chars);\n}\n\nexport { trim };\n", "import { trim as trim$1 } from '../../string/trim.mjs';\n\nfunction trim(str, chars, guard) {\n if (str == null) {\n return '';\n }\n if (guard != null || chars == null) {\n return str.toString().trim();\n }\n switch (typeof chars) {\n case 'object': {\n if (Array.isArray(chars)) {\n return trim$1(str, chars.flatMap(x => x.toString().split('')));\n }\n else {\n return trim$1(str, chars.toString().split(''));\n }\n }\n default: {\n return trim$1(str, chars.toString().split(''));\n }\n }\n}\n\nexport { trim };\n", "import { trimEnd as trimEnd$1 } from '../../string/trimEnd.mjs';\n\nfunction trimEnd(str, chars, guard) {\n if (str == null) {\n return '';\n }\n if (guard != null || chars == null) {\n return str.toString().trimEnd();\n }\n return trimEnd$1(str, chars.toString().split(''));\n}\n\nexport { trimEnd };\n", "import { trimStart as trimStart$1 } from '../../string/trimStart.mjs';\n\nfunction trimStart(str, chars, guard) {\n if (str == null) {\n return '';\n }\n if (guard != null || chars == null) {\n return str.toString().trimStart();\n }\n return trimStart$1(str, chars.toString().split(''));\n}\n\nexport { trimStart };\n", "import { isObject } from '../predicate/isObject.mjs';\n\nconst regexMultiByte = /[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]/;\nfunction truncate(string, options) {\n string = string != null ? `${string}` : '';\n let length = 30;\n let omission = '...';\n if (isObject(options)) {\n length = parseLength(options.length);\n omission = 'omission' in options ? `${options.omission}` : '...';\n }\n let i = string.length;\n const lengthOmission = Array.from(omission).length;\n const lengthBase = Math.max(length - lengthOmission, 0);\n let strArray = undefined;\n const unicode = regexMultiByte.test(string);\n if (unicode) {\n strArray = Array.from(string);\n i = strArray.length;\n }\n if (length >= i) {\n return string;\n }\n if (i <= lengthOmission) {\n return omission;\n }\n let base = strArray === undefined ? string.slice(0, lengthBase) : strArray?.slice(0, lengthBase).join('');\n const separator = options?.separator;\n if (!separator) {\n base += omission;\n return base;\n }\n const search = separator instanceof RegExp ? separator.source : separator;\n const flags = 'u' + (separator instanceof RegExp ? separator.flags.replace('u', '') : '');\n const withoutSeparator = new RegExp(`(?.*(?:(?!${search}).))(?:${search})`, flags).exec(base);\n return (!withoutSeparator?.groups ? base : withoutSeparator.groups.result) + omission;\n}\nfunction parseLength(length) {\n if (length == null) {\n return 30;\n }\n if (length <= 0) {\n return 0;\n }\n return length;\n}\n\nexport { truncate };\n", "const htmlUnescapes = {\n '&': '&',\n '<': '<',\n '>': '>',\n '"': '\"',\n ''': \"'\",\n};\nfunction unescape(str) {\n return str.replace(/&(?:amp|lt|gt|quot|#(0+)?39);/g, match => htmlUnescapes[match] || \"'\");\n}\n\nexport { unescape };\n", "import { unescape as unescape$1 } from '../../string/unescape.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction unescape(str) {\n return unescape$1(toString(str));\n}\n\nexport { unescape };\n", "import { words } from './words.mjs';\n\nfunction upperCase(str) {\n const words$1 = words(str);\n let result = '';\n for (let i = 0; i < words$1.length; i++) {\n result += words$1[i].toUpperCase();\n if (i < words$1.length - 1) {\n result += ' ';\n }\n }\n return result;\n}\n\nexport { upperCase };\n", "import { deburr } from './deburr.mjs';\nimport { upperCase as upperCase$1 } from '../../string/upperCase.mjs';\nimport { normalizeForCase } from '../_internal/normalizeForCase.mjs';\n\nfunction upperCase(str) {\n return upperCase$1(normalizeForCase(deburr(str)));\n}\n\nexport { upperCase };\n", "function upperFirst(str) {\n return str.substring(0, 1).toUpperCase() + str.substring(1);\n}\n\nexport { upperFirst };\n", "import { upperFirst as upperFirst$1 } from '../../string/upperFirst.mjs';\nimport { toString } from '../util/toString.mjs';\n\nfunction upperFirst(str) {\n return upperFirst$1(toString(str));\n}\n\nexport { upperFirst };\n", "import { toString } from '../util/toString.mjs';\n\nconst rNonCharLatin = '\\\\x00-\\\\x2f\\\\x3a-\\\\x40\\\\x5b-\\\\x60\\\\x7b-\\\\xbf\\\\xd7\\\\xf7';\nconst rUnicodeUpper = '\\\\p{Lu}';\nconst rUnicodeLower = '\\\\p{Ll}';\nconst rMisc = '(?:[\\\\p{Lm}\\\\p{Lo}]\\\\p{M}*)';\nconst rNumber = '\\\\d';\nconst rUnicodeOptContrLower = \"(?:['\\u2019](?:d|ll|m|re|s|t|ve))?\";\nconst rUnicodeOptContrUpper = \"(?:['\\u2019](?:D|LL|M|RE|S|T|VE))?\";\nconst rUnicodeBreak = `[\\\\p{Z}\\\\p{P}${rNonCharLatin}]`;\nconst rUnicodeMiscUpper = `(?:${rUnicodeUpper}|${rMisc})`;\nconst rUnicodeMiscLower = `(?:${rUnicodeLower}|${rMisc})`;\nconst rUnicodeWord = RegExp([\n `${rUnicodeUpper}?${rUnicodeLower}+${rUnicodeOptContrLower}(?=${rUnicodeBreak}|${rUnicodeUpper}|$)`,\n `${rUnicodeMiscUpper}+${rUnicodeOptContrUpper}(?=${rUnicodeBreak}|${rUnicodeUpper}${rUnicodeMiscLower}|$)`,\n `${rUnicodeUpper}?${rUnicodeMiscLower}+${rUnicodeOptContrLower}`,\n `${rUnicodeUpper}+${rUnicodeOptContrUpper}`,\n `${rNumber}*(?:1ST|2ND|3RD|(?![123])${rNumber}TH)(?=\\\\b|[a-z_])`,\n `${rNumber}*(?:1st|2nd|3rd|(?![123])${rNumber}th)(?=\\\\b|[A-Z_])`,\n `${rNumber}+`,\n '\\\\p{Emoji_Presentation}',\n '\\\\p{Extended_Pictographic}',\n].join('|'), 'gu');\nfunction words(str, pattern = rUnicodeWord, guard) {\n const input = toString(str);\n if (guard) {\n pattern = rUnicodeWord;\n }\n if (typeof pattern === 'number') {\n pattern = pattern.toString();\n }\n const words = Array.from(input.match(pattern) ?? []);\n return words.filter(x => x !== '');\n}\n\nexport { words };\n", "import { iteratee } from './iteratee.mjs';\nimport { isFunction } from '../../predicate/isFunction.mjs';\n\nfunction cond(pairs) {\n const length = pairs.length;\n const processedPairs = pairs.map(pair => {\n const predicate = pair[0];\n const func = pair[1];\n if (!isFunction(func)) {\n throw new TypeError('Expected a function');\n }\n return [iteratee(predicate), func];\n });\n return function (...args) {\n for (let i = 0; i < length; i++) {\n const pair = processedPairs[i];\n const predicate = pair[0];\n const func = pair[1];\n if (predicate.apply(this, args)) {\n return func.apply(this, args);\n }\n }\n };\n}\n\nexport { cond };\n", "function constant(value) {\n return () => value;\n}\n\nexport { constant };\n", "function defaultTo(value, defaultValue) {\n if (value == null || Number.isNaN(value)) {\n return defaultValue;\n }\n return value;\n}\n\nexport { defaultTo };\n", "import { toNumber } from './toNumber.mjs';\n\nfunction gt(value, other) {\n if (typeof value === 'string' && typeof other === 'string') {\n return value > other;\n }\n return toNumber(value) > toNumber(other);\n}\n\nexport { gt };\n", "import { toNumber } from './toNumber.mjs';\n\nfunction gte(value, other) {\n if (typeof value === 'string' && typeof other === 'string') {\n return value >= other;\n }\n return toNumber(value) >= toNumber(other);\n}\n\nexport { gte };\n", "import { toPath } from './toPath.mjs';\nimport { toKey } from '../_internal/toKey.mjs';\nimport { last } from '../array/last.mjs';\nimport { get } from '../object/get.mjs';\n\nfunction invoke(object, path, ...args) {\n args = args.flat(1);\n if (object == null) {\n return;\n }\n switch (typeof path) {\n case 'string': {\n if (typeof object === 'object' && Object.hasOwn(object, path)) {\n return invokeImpl(object, [path], args);\n }\n return invokeImpl(object, toPath(path), args);\n }\n case 'number':\n case 'symbol': {\n return invokeImpl(object, [path], args);\n }\n default: {\n if (Array.isArray(path)) {\n return invokeImpl(object, path, args);\n }\n else {\n return invokeImpl(object, [path], args);\n }\n }\n }\n}\nfunction invokeImpl(object, path, args) {\n const parent = get(object, path.slice(0, -1), object);\n if (parent == null) {\n return undefined;\n }\n let lastKey = last(path);\n const lastValue = lastKey?.valueOf();\n if (typeof lastValue === 'number') {\n lastKey = toKey(lastValue);\n }\n else {\n lastKey = String(lastKey);\n }\n const func = get(parent, lastKey);\n return func?.apply(parent, args);\n}\n\nexport { invoke };\n", "import { toNumber } from './toNumber.mjs';\n\nfunction lt(value, other) {\n if (typeof value === 'string' && typeof other === 'string') {\n return value < other;\n }\n return toNumber(value) < toNumber(other);\n}\n\nexport { lt };\n", "import { toNumber } from './toNumber.mjs';\n\nfunction lte(value, other) {\n if (typeof value === 'string' && typeof other === 'string') {\n return value <= other;\n }\n return toNumber(value) <= toNumber(other);\n}\n\nexport { lte };\n", "import { invoke } from './invoke.mjs';\n\nfunction method(path, ...args) {\n return function (object) {\n return invoke(object, path, args);\n };\n}\n\nexport { method };\n", "import { invoke } from './invoke.mjs';\n\nfunction methodOf(object, ...args) {\n return function (path) {\n return invoke(object, path, args);\n };\n}\n\nexport { methodOf };\n", "function now() {\n return Date.now();\n}\n\nexport { now };\n", "import { iteratee } from './iteratee.mjs';\n\nfunction over(...iteratees) {\n if (iteratees.length === 1 && Array.isArray(iteratees[0])) {\n iteratees = iteratees[0];\n }\n const funcs = iteratees.map(item => iteratee(item));\n return function (...args) {\n return funcs.map(func => func.apply(this, args));\n };\n}\n\nexport { over };\n", "import { iteratee } from './iteratee.mjs';\n\nfunction overEvery(...predicates) {\n return function (...values) {\n for (let i = 0; i < predicates.length; ++i) {\n const predicate = predicates[i];\n if (!Array.isArray(predicate)) {\n if (!iteratee(predicate).apply(this, values)) {\n return false;\n }\n continue;\n }\n for (let j = 0; j < predicate.length; ++j) {\n if (!iteratee(predicate[j]).apply(this, values)) {\n return false;\n }\n }\n }\n return true;\n };\n}\n\nexport { overEvery };\n", "import { iteratee } from './iteratee.mjs';\n\nfunction overSome(...predicates) {\n return function (...values) {\n for (let i = 0; i < predicates.length; ++i) {\n const predicate = predicates[i];\n if (!Array.isArray(predicate)) {\n if (iteratee(predicate).apply(this, values)) {\n return true;\n }\n continue;\n }\n for (let j = 0; j < predicate.length; ++j) {\n if (iteratee(predicate[j]).apply(this, values)) {\n return true;\n }\n }\n }\n return false;\n };\n}\n\nexport { overSome };\n", "function stubArray() {\n return [];\n}\n\nexport { stubArray };\n", "function stubFalse() {\n return false;\n}\n\nexport { stubFalse };\n", "function stubObject() {\n return {};\n}\n\nexport { stubObject };\n", "function stubString() {\n return '';\n}\n\nexport { stubString };\n", "function stubTrue() {\n return true;\n}\n\nexport { stubTrue };\n", "const MAX_ARRAY_LENGTH = 4_294_967_295;\n\nexport { MAX_ARRAY_LENGTH };\n", "import { MAX_ARRAY_LENGTH } from '../_internal/MAX_ARRAY_LENGTH.mjs';\nimport { clamp } from '../math/clamp.mjs';\n\nfunction toLength(value) {\n if (value == null) {\n return 0;\n }\n const length = Math.floor(Number(value));\n return clamp(length, 0, MAX_ARRAY_LENGTH);\n}\n\nexport { toLength };\n", "import { keysIn } from '../object/keysIn.mjs';\n\nfunction toPlainObject(value) {\n const plainObject = {};\n const valueKeys = keysIn(value);\n for (let i = 0; i < valueKeys.length; i++) {\n const key = valueKeys[i];\n const objValue = value[key];\n if (key === '__proto__') {\n Object.defineProperty(plainObject, key, {\n configurable: true,\n enumerable: true,\n value: objValue,\n writable: true,\n });\n }\n else {\n plainObject[key] = objValue;\n }\n }\n return plainObject;\n}\n\nexport { toPlainObject };\n", "import { toInteger } from './toInteger.mjs';\nimport { MAX_SAFE_INTEGER } from '../_internal/MAX_SAFE_INTEGER.mjs';\nimport { clamp } from '../math/clamp.mjs';\n\nfunction toSafeInteger(value) {\n if (value == null) {\n return 0;\n }\n return clamp(toInteger(value), -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);\n}\n\nexport { toSafeInteger };\n", "let idCounter = 0;\nfunction uniqueId(prefix = '') {\n const id = ++idCounter;\n return `${prefix}${id}`;\n}\n\nexport { uniqueId };\n", "export { castArray } from './array/castArray.mjs';\nexport { chunk } from './array/chunk.mjs';\nexport { compact } from './array/compact.mjs';\nexport { concat } from './array/concat.mjs';\nexport { countBy } from './array/countBy.mjs';\nexport { difference } from './array/difference.mjs';\nexport { differenceBy } from './array/differenceBy.mjs';\nexport { differenceWith } from './array/differenceWith.mjs';\nexport { drop } from './array/drop.mjs';\nexport { dropRight } from './array/dropRight.mjs';\nexport { dropRightWhile } from './array/dropRightWhile.mjs';\nexport { dropWhile } from './array/dropWhile.mjs';\nexport { forEach as each, forEach } from './array/forEach.mjs';\nexport { forEachRight as eachRight, forEachRight } from './array/forEachRight.mjs';\nexport { every } from './array/every.mjs';\nexport { fill } from './array/fill.mjs';\nexport { filter } from './array/filter.mjs';\nexport { find } from './array/find.mjs';\nexport { findIndex } from './array/findIndex.mjs';\nexport { findLast } from './array/findLast.mjs';\nexport { findLastIndex } from './array/findLastIndex.mjs';\nexport { head as first, head } from './array/head.mjs';\nexport { flatMap } from './array/flatMap.mjs';\nexport { flatMapDeep } from './array/flatMapDeep.mjs';\nexport { flatMapDepth } from './array/flatMapDepth.mjs';\nexport { flatten } from './array/flatten.mjs';\nexport { flattenDeep } from './array/flattenDeep.mjs';\nexport { flattenDepth } from './array/flattenDepth.mjs';\nexport { groupBy } from './array/groupBy.mjs';\nexport { includes } from './array/includes.mjs';\nexport { indexOf } from './array/indexOf.mjs';\nexport { initial } from './array/initial.mjs';\nexport { intersection } from './array/intersection.mjs';\nexport { intersectionBy } from './array/intersectionBy.mjs';\nexport { intersectionWith } from './array/intersectionWith.mjs';\nexport { invokeMap } from './array/invokeMap.mjs';\nexport { join } from './array/join.mjs';\nexport { keyBy } from './array/keyBy.mjs';\nexport { last } from './array/last.mjs';\nexport { lastIndexOf } from './array/lastIndexOf.mjs';\nexport { map } from './array/map.mjs';\nexport { nth } from './array/nth.mjs';\nexport { orderBy } from './array/orderBy.mjs';\nexport { partition } from './array/partition.mjs';\nexport { pull } from './array/pull.mjs';\nexport { pullAll } from './array/pullAll.mjs';\nexport { pullAllBy } from './array/pullAllBy.mjs';\nexport { pullAllWith } from './array/pullAllWith.mjs';\nexport { pullAt } from './array/pullAt.mjs';\nexport { reduce } from './array/reduce.mjs';\nexport { reduceRight } from './array/reduceRight.mjs';\nexport { reject } from './array/reject.mjs';\nexport { remove } from './array/remove.mjs';\nexport { reverse } from './array/reverse.mjs';\nexport { sample } from './array/sample.mjs';\nexport { sampleSize } from './array/sampleSize.mjs';\nexport { shuffle } from './array/shuffle.mjs';\nexport { size } from './array/size.mjs';\nexport { slice } from './array/slice.mjs';\nexport { some } from './array/some.mjs';\nexport { sortBy } from './array/sortBy.mjs';\nexport { sortedIndex } from './array/sortedIndex.mjs';\nexport { sortedIndexBy } from './array/sortedIndexBy.mjs';\nexport { sortedIndexOf } from './array/sortedIndexOf.mjs';\nexport { sortedLastIndex } from './array/sortedLastIndex.mjs';\nexport { sortedLastIndexBy } from './array/sortedLastIndexBy.mjs';\nexport { sortedLastIndexOf } from './array/sortedLastIndexOf.mjs';\nexport { tail } from './array/tail.mjs';\nexport { take } from './array/take.mjs';\nexport { takeRight } from './array/takeRight.mjs';\nexport { takeRightWhile } from './array/takeRightWhile.mjs';\nexport { takeWhile } from './array/takeWhile.mjs';\nexport { union } from './array/union.mjs';\nexport { unionBy } from './array/unionBy.mjs';\nexport { unionWith } from './array/unionWith.mjs';\nexport { uniq } from './array/uniq.mjs';\nexport { uniqBy } from './array/uniqBy.mjs';\nexport { uniqWith } from './array/uniqWith.mjs';\nexport { unzip } from './array/unzip.mjs';\nexport { unzipWith } from './array/unzipWith.mjs';\nexport { without } from './array/without.mjs';\nexport { xor } from './array/xor.mjs';\nexport { xorBy } from './array/xorBy.mjs';\nexport { xorWith } from './array/xorWith.mjs';\nexport { zip } from './array/zip.mjs';\nexport { zipObject } from './array/zipObject.mjs';\nexport { zipObjectDeep } from './array/zipObjectDeep.mjs';\nexport { zipWith } from './array/zipWith.mjs';\nexport { after } from './function/after.mjs';\nexport { ary } from './function/ary.mjs';\nexport { attempt } from './function/attempt.mjs';\nexport { before } from './function/before.mjs';\nexport { bind } from './function/bind.mjs';\nexport { bindKey } from './function/bindKey.mjs';\nexport { curry } from './function/curry.mjs';\nexport { curryRight } from './function/curryRight.mjs';\nexport { debounce } from './function/debounce.mjs';\nexport { defer } from './function/defer.mjs';\nexport { delay } from './function/delay.mjs';\nexport { flip } from './function/flip.mjs';\nexport { flow } from './function/flow.mjs';\nexport { flowRight } from './function/flowRight.mjs';\nexport { memoize } from './function/memoize.mjs';\nexport { negate } from './function/negate.mjs';\nexport { nthArg } from './function/nthArg.mjs';\nexport { once } from './function/once.mjs';\nexport { overArgs } from './function/overArgs.mjs';\nexport { partial } from './function/partial.mjs';\nexport { partialRight } from './function/partialRight.mjs';\nexport { rearg } from './function/rearg.mjs';\nexport { rest } from './function/rest.mjs';\nexport { spread } from './function/spread.mjs';\nexport { throttle } from './function/throttle.mjs';\nexport { unary } from './function/unary.mjs';\nexport { wrap } from './function/wrap.mjs';\nexport { add } from './math/add.mjs';\nexport { ceil } from './math/ceil.mjs';\nexport { clamp } from './math/clamp.mjs';\nexport { divide } from './math/divide.mjs';\nexport { floor } from './math/floor.mjs';\nexport { inRange } from './math/inRange.mjs';\nexport { max } from './math/max.mjs';\nexport { maxBy } from './math/maxBy.mjs';\nexport { mean } from './math/mean.mjs';\nexport { meanBy } from './math/meanBy.mjs';\nexport { min } from './math/min.mjs';\nexport { minBy } from './math/minBy.mjs';\nexport { multiply } from './math/multiply.mjs';\nexport { parseInt } from './math/parseInt.mjs';\nexport { random } from './math/random.mjs';\nexport { range } from './math/range.mjs';\nexport { rangeRight } from './math/rangeRight.mjs';\nexport { round } from './math/round.mjs';\nexport { subtract } from './math/subtract.mjs';\nexport { sum } from './math/sum.mjs';\nexport { sumBy } from './math/sumBy.mjs';\nexport { isEqual } from '../predicate/isEqual.mjs';\nexport { identity } from './function/identity.mjs';\nexport { noop } from './function/noop.mjs';\nexport { assign } from './object/assign.mjs';\nexport { assignIn, assignIn as extend } from './object/assignIn.mjs';\nexport { assignInWith, assignInWith as extendWith } from './object/assignInWith.mjs';\nexport { assignWith } from './object/assignWith.mjs';\nexport { at } from './object/at.mjs';\nexport { clone } from './object/clone.mjs';\nexport { cloneDeep } from './object/cloneDeep.mjs';\nexport { cloneDeepWith } from './object/cloneDeepWith.mjs';\nexport { cloneWith } from './object/cloneWith.mjs';\nexport { create } from './object/create.mjs';\nexport { defaults } from './object/defaults.mjs';\nexport { defaultsDeep } from './object/defaultsDeep.mjs';\nexport { findKey } from './object/findKey.mjs';\nexport { findLastKey } from './object/findLastKey.mjs';\nexport { forIn } from './object/forIn.mjs';\nexport { forInRight } from './object/forInRight.mjs';\nexport { forOwn } from './object/forOwn.mjs';\nexport { forOwnRight } from './object/forOwnRight.mjs';\nexport { fromPairs } from './object/fromPairs.mjs';\nexport { functions } from './object/functions.mjs';\nexport { functionsIn } from './object/functionsIn.mjs';\nexport { get } from './object/get.mjs';\nexport { has } from './object/has.mjs';\nexport { hasIn } from './object/hasIn.mjs';\nexport { invert } from './object/invert.mjs';\nexport { invertBy } from './object/invertBy.mjs';\nexport { keys } from './object/keys.mjs';\nexport { keysIn } from './object/keysIn.mjs';\nexport { mapKeys } from './object/mapKeys.mjs';\nexport { mapValues } from './object/mapValues.mjs';\nexport { merge } from './object/merge.mjs';\nexport { mergeWith } from './object/mergeWith.mjs';\nexport { omit } from './object/omit.mjs';\nexport { omitBy } from './object/omitBy.mjs';\nexport { pick } from './object/pick.mjs';\nexport { pickBy } from './object/pickBy.mjs';\nexport { property } from './object/property.mjs';\nexport { propertyOf } from './object/propertyOf.mjs';\nexport { result } from './object/result.mjs';\nexport { set } from './object/set.mjs';\nexport { setWith } from './object/setWith.mjs';\nexport { toDefaulted } from './object/toDefaulted.mjs';\nexport { toPairs } from './object/toPairs.mjs';\nexport { toPairsIn } from './object/toPairsIn.mjs';\nexport { transform } from './object/transform.mjs';\nexport { unset } from './object/unset.mjs';\nexport { update } from './object/update.mjs';\nexport { updateWith } from './object/updateWith.mjs';\nexport { values } from './object/values.mjs';\nexport { valuesIn } from './object/valuesIn.mjs';\nexport { isFunction } from './predicate/isFunction.mjs';\nexport { isLength } from './predicate/isLength.mjs';\nexport { isMatchWith } from './predicate/isMatchWith.mjs';\nexport { isNative } from './predicate/isNative.mjs';\nexport { isNull } from './predicate/isNull.mjs';\nexport { isUndefined } from './predicate/isUndefined.mjs';\nexport { conforms } from './predicate/conforms.mjs';\nexport { conformsTo } from './predicate/conformsTo.mjs';\nexport { isArguments } from './predicate/isArguments.mjs';\nexport { isArray } from './predicate/isArray.mjs';\nexport { isArrayBuffer } from './predicate/isArrayBuffer.mjs';\nexport { isArrayLike } from './predicate/isArrayLike.mjs';\nexport { isArrayLikeObject } from './predicate/isArrayLikeObject.mjs';\nexport { isBoolean } from './predicate/isBoolean.mjs';\nexport { isBuffer } from './predicate/isBuffer.mjs';\nexport { isDate } from './predicate/isDate.mjs';\nexport { isElement } from './predicate/isElement.mjs';\nexport { isEmpty } from './predicate/isEmpty.mjs';\nexport { isEqualWith } from './predicate/isEqualWith.mjs';\nexport { isError } from './predicate/isError.mjs';\nexport { isFinite } from './predicate/isFinite.mjs';\nexport { isInteger } from './predicate/isInteger.mjs';\nexport { isMap } from './predicate/isMap.mjs';\nexport { isMatch } from './predicate/isMatch.mjs';\nexport { isNaN } from './predicate/isNaN.mjs';\nexport { isNil } from './predicate/isNil.mjs';\nexport { isNumber } from './predicate/isNumber.mjs';\nexport { isObject } from './predicate/isObject.mjs';\nexport { isObjectLike } from './predicate/isObjectLike.mjs';\nexport { isPlainObject } from './predicate/isPlainObject.mjs';\nexport { isRegExp } from './predicate/isRegExp.mjs';\nexport { isSafeInteger } from './predicate/isSafeInteger.mjs';\nexport { isSet } from './predicate/isSet.mjs';\nexport { isString } from './predicate/isString.mjs';\nexport { isSymbol } from './predicate/isSymbol.mjs';\nexport { isTypedArray } from './predicate/isTypedArray.mjs';\nexport { isWeakMap } from './predicate/isWeakMap.mjs';\nexport { isWeakSet } from './predicate/isWeakSet.mjs';\nexport { matches } from './predicate/matches.mjs';\nexport { matchesProperty } from './predicate/matchesProperty.mjs';\nexport { capitalize } from './string/capitalize.mjs';\nexport { bindAll } from './util/bindAll.mjs';\nexport { camelCase } from './string/camelCase.mjs';\nexport { deburr } from './string/deburr.mjs';\nexport { endsWith } from './string/endsWith.mjs';\nexport { escape } from './string/escape.mjs';\nexport { escapeRegExp } from './string/escapeRegExp.mjs';\nexport { kebabCase } from './string/kebabCase.mjs';\nexport { lowerCase } from './string/lowerCase.mjs';\nexport { lowerFirst } from './string/lowerFirst.mjs';\nexport { pad } from './string/pad.mjs';\nexport { padEnd } from './string/padEnd.mjs';\nexport { padStart } from './string/padStart.mjs';\nexport { repeat } from './string/repeat.mjs';\nexport { replace } from './string/replace.mjs';\nexport { snakeCase } from './string/snakeCase.mjs';\nexport { split } from './string/split.mjs';\nexport { startCase } from './string/startCase.mjs';\nexport { startsWith } from './string/startsWith.mjs';\nexport { template, templateSettings } from './string/template.mjs';\nexport { toLower } from './string/toLower.mjs';\nexport { toUpper } from './string/toUpper.mjs';\nexport { trim } from './string/trim.mjs';\nexport { trimEnd } from './string/trimEnd.mjs';\nexport { trimStart } from './string/trimStart.mjs';\nexport { truncate } from './string/truncate.mjs';\nexport { unescape } from './string/unescape.mjs';\nexport { upperCase } from './string/upperCase.mjs';\nexport { upperFirst } from './string/upperFirst.mjs';\nexport { words } from './string/words.mjs';\nexport { cond } from './util/cond.mjs';\nexport { constant } from './util/constant.mjs';\nexport { defaultTo } from './util/defaultTo.mjs';\nexport { isEqualsSameValueZero as eq } from '../_internal/isEqualsSameValueZero.mjs';\nexport { gt } from './util/gt.mjs';\nexport { gte } from './util/gte.mjs';\nexport { invoke } from './util/invoke.mjs';\nexport { iteratee } from './util/iteratee.mjs';\nexport { lt } from './util/lt.mjs';\nexport { lte } from './util/lte.mjs';\nexport { method } from './util/method.mjs';\nexport { methodOf } from './util/methodOf.mjs';\nexport { now } from './util/now.mjs';\nexport { over } from './util/over.mjs';\nexport { overEvery } from './util/overEvery.mjs';\nexport { overSome } from './util/overSome.mjs';\nexport { stubArray } from './util/stubArray.mjs';\nexport { stubFalse } from './util/stubFalse.mjs';\nexport { stubObject } from './util/stubObject.mjs';\nexport { stubString } from './util/stubString.mjs';\nexport { stubTrue } from './util/stubTrue.mjs';\nexport { times } from './util/times.mjs';\nexport { toArray } from './util/toArray.mjs';\nexport { toFinite } from './util/toFinite.mjs';\nexport { toInteger } from './util/toInteger.mjs';\nexport { toLength } from './util/toLength.mjs';\nexport { toNumber } from './util/toNumber.mjs';\nexport { toPath } from './util/toPath.mjs';\nexport { toPlainObject } from './util/toPlainObject.mjs';\nexport { toSafeInteger } from './util/toSafeInteger.mjs';\nexport { toString } from './util/toString.mjs';\nexport { uniqueId } from './util/uniqueId.mjs';\n", "import * as compat from './compat.mjs';\n\nconst toolkit = ((value) => {\n return value;\n});\nObject.assign(toolkit, compat);\ntoolkit.partial.placeholder = toolkit;\ntoolkit.partialRight.placeholder = toolkit;\n\nexport { toolkit };\n", "export { castArray } from './array/castArray.mjs';\nexport { chunk } from './array/chunk.mjs';\nexport { compact } from './array/compact.mjs';\nexport { concat } from './array/concat.mjs';\nexport { countBy } from './array/countBy.mjs';\nexport { difference } from './array/difference.mjs';\nexport { differenceBy } from './array/differenceBy.mjs';\nexport { differenceWith } from './array/differenceWith.mjs';\nexport { drop } from './array/drop.mjs';\nexport { dropRight } from './array/dropRight.mjs';\nexport { dropRightWhile } from './array/dropRightWhile.mjs';\nexport { dropWhile } from './array/dropWhile.mjs';\nexport { forEach as each, forEach } from './array/forEach.mjs';\nexport { forEachRight as eachRight, forEachRight } from './array/forEachRight.mjs';\nexport { every } from './array/every.mjs';\nexport { fill } from './array/fill.mjs';\nexport { filter } from './array/filter.mjs';\nexport { find } from './array/find.mjs';\nexport { findIndex } from './array/findIndex.mjs';\nexport { findLast } from './array/findLast.mjs';\nexport { findLastIndex } from './array/findLastIndex.mjs';\nexport { head as first, head } from './array/head.mjs';\nexport { flatMap } from './array/flatMap.mjs';\nexport { flatMapDeep } from './array/flatMapDeep.mjs';\nexport { flatMapDepth } from './array/flatMapDepth.mjs';\nexport { flatten } from './array/flatten.mjs';\nexport { flattenDeep } from './array/flattenDeep.mjs';\nexport { flattenDepth } from './array/flattenDepth.mjs';\nexport { groupBy } from './array/groupBy.mjs';\nexport { includes } from './array/includes.mjs';\nexport { indexOf } from './array/indexOf.mjs';\nexport { initial } from './array/initial.mjs';\nexport { intersection } from './array/intersection.mjs';\nexport { intersectionBy } from './array/intersectionBy.mjs';\nexport { intersectionWith } from './array/intersectionWith.mjs';\nexport { invokeMap } from './array/invokeMap.mjs';\nexport { join } from './array/join.mjs';\nexport { keyBy } from './array/keyBy.mjs';\nexport { last } from './array/last.mjs';\nexport { lastIndexOf } from './array/lastIndexOf.mjs';\nexport { map } from './array/map.mjs';\nexport { nth } from './array/nth.mjs';\nexport { orderBy } from './array/orderBy.mjs';\nexport { partition } from './array/partition.mjs';\nexport { pull } from './array/pull.mjs';\nexport { pullAll } from './array/pullAll.mjs';\nexport { pullAllBy } from './array/pullAllBy.mjs';\nexport { pullAllWith } from './array/pullAllWith.mjs';\nexport { pullAt } from './array/pullAt.mjs';\nexport { reduce } from './array/reduce.mjs';\nexport { reduceRight } from './array/reduceRight.mjs';\nexport { reject } from './array/reject.mjs';\nexport { remove } from './array/remove.mjs';\nexport { reverse } from './array/reverse.mjs';\nexport { sample } from './array/sample.mjs';\nexport { sampleSize } from './array/sampleSize.mjs';\nexport { shuffle } from './array/shuffle.mjs';\nexport { size } from './array/size.mjs';\nexport { slice } from './array/slice.mjs';\nexport { some } from './array/some.mjs';\nexport { sortBy } from './array/sortBy.mjs';\nexport { sortedIndex } from './array/sortedIndex.mjs';\nexport { sortedIndexBy } from './array/sortedIndexBy.mjs';\nexport { sortedIndexOf } from './array/sortedIndexOf.mjs';\nexport { sortedLastIndex } from './array/sortedLastIndex.mjs';\nexport { sortedLastIndexBy } from './array/sortedLastIndexBy.mjs';\nexport { sortedLastIndexOf } from './array/sortedLastIndexOf.mjs';\nexport { tail } from './array/tail.mjs';\nexport { take } from './array/take.mjs';\nexport { takeRight } from './array/takeRight.mjs';\nexport { takeRightWhile } from './array/takeRightWhile.mjs';\nexport { takeWhile } from './array/takeWhile.mjs';\nexport { union } from './array/union.mjs';\nexport { unionBy } from './array/unionBy.mjs';\nexport { unionWith } from './array/unionWith.mjs';\nexport { uniq } from './array/uniq.mjs';\nexport { uniqBy } from './array/uniqBy.mjs';\nexport { uniqWith } from './array/uniqWith.mjs';\nexport { unzip } from './array/unzip.mjs';\nexport { unzipWith } from './array/unzipWith.mjs';\nexport { without } from './array/without.mjs';\nexport { xor } from './array/xor.mjs';\nexport { xorBy } from './array/xorBy.mjs';\nexport { xorWith } from './array/xorWith.mjs';\nexport { zip } from './array/zip.mjs';\nexport { zipObject } from './array/zipObject.mjs';\nexport { zipObjectDeep } from './array/zipObjectDeep.mjs';\nexport { zipWith } from './array/zipWith.mjs';\nexport { after } from './function/after.mjs';\nexport { ary } from './function/ary.mjs';\nexport { attempt } from './function/attempt.mjs';\nexport { before } from './function/before.mjs';\nexport { bind } from './function/bind.mjs';\nexport { bindKey } from './function/bindKey.mjs';\nexport { curry } from './function/curry.mjs';\nexport { curryRight } from './function/curryRight.mjs';\nexport { debounce } from './function/debounce.mjs';\nexport { defer } from './function/defer.mjs';\nexport { delay } from './function/delay.mjs';\nexport { flip } from './function/flip.mjs';\nexport { flow } from './function/flow.mjs';\nexport { flowRight } from './function/flowRight.mjs';\nexport { memoize } from './function/memoize.mjs';\nexport { negate } from './function/negate.mjs';\nexport { nthArg } from './function/nthArg.mjs';\nexport { once } from './function/once.mjs';\nexport { overArgs } from './function/overArgs.mjs';\nexport { partial } from './function/partial.mjs';\nexport { partialRight } from './function/partialRight.mjs';\nexport { rearg } from './function/rearg.mjs';\nexport { rest } from './function/rest.mjs';\nexport { spread } from './function/spread.mjs';\nexport { throttle } from './function/throttle.mjs';\nexport { unary } from './function/unary.mjs';\nexport { wrap } from './function/wrap.mjs';\nexport { add } from './math/add.mjs';\nexport { ceil } from './math/ceil.mjs';\nexport { clamp } from './math/clamp.mjs';\nexport { divide } from './math/divide.mjs';\nexport { floor } from './math/floor.mjs';\nexport { inRange } from './math/inRange.mjs';\nexport { max } from './math/max.mjs';\nexport { maxBy } from './math/maxBy.mjs';\nexport { mean } from './math/mean.mjs';\nexport { meanBy } from './math/meanBy.mjs';\nexport { min } from './math/min.mjs';\nexport { minBy } from './math/minBy.mjs';\nexport { multiply } from './math/multiply.mjs';\nexport { parseInt } from './math/parseInt.mjs';\nexport { random } from './math/random.mjs';\nexport { range } from './math/range.mjs';\nexport { rangeRight } from './math/rangeRight.mjs';\nexport { round } from './math/round.mjs';\nexport { subtract } from './math/subtract.mjs';\nexport { sum } from './math/sum.mjs';\nexport { sumBy } from './math/sumBy.mjs';\nexport { isEqual } from '../predicate/isEqual.mjs';\nexport { identity } from './function/identity.mjs';\nexport { noop } from './function/noop.mjs';\nexport { assign } from './object/assign.mjs';\nexport { assignIn, assignIn as extend } from './object/assignIn.mjs';\nexport { assignInWith, assignInWith as extendWith } from './object/assignInWith.mjs';\nexport { assignWith } from './object/assignWith.mjs';\nexport { at } from './object/at.mjs';\nexport { clone } from './object/clone.mjs';\nexport { cloneDeep } from './object/cloneDeep.mjs';\nexport { cloneDeepWith } from './object/cloneDeepWith.mjs';\nexport { cloneWith } from './object/cloneWith.mjs';\nexport { create } from './object/create.mjs';\nexport { defaults } from './object/defaults.mjs';\nexport { defaultsDeep } from './object/defaultsDeep.mjs';\nexport { findKey } from './object/findKey.mjs';\nexport { findLastKey } from './object/findLastKey.mjs';\nexport { forIn } from './object/forIn.mjs';\nexport { forInRight } from './object/forInRight.mjs';\nexport { forOwn } from './object/forOwn.mjs';\nexport { forOwnRight } from './object/forOwnRight.mjs';\nexport { fromPairs } from './object/fromPairs.mjs';\nexport { functions } from './object/functions.mjs';\nexport { functionsIn } from './object/functionsIn.mjs';\nexport { get } from './object/get.mjs';\nexport { has } from './object/has.mjs';\nexport { hasIn } from './object/hasIn.mjs';\nexport { invert } from './object/invert.mjs';\nexport { invertBy } from './object/invertBy.mjs';\nexport { keys } from './object/keys.mjs';\nexport { keysIn } from './object/keysIn.mjs';\nexport { mapKeys } from './object/mapKeys.mjs';\nexport { mapValues } from './object/mapValues.mjs';\nexport { merge } from './object/merge.mjs';\nexport { mergeWith } from './object/mergeWith.mjs';\nexport { omit } from './object/omit.mjs';\nexport { omitBy } from './object/omitBy.mjs';\nexport { pick } from './object/pick.mjs';\nexport { pickBy } from './object/pickBy.mjs';\nexport { property } from './object/property.mjs';\nexport { propertyOf } from './object/propertyOf.mjs';\nexport { result } from './object/result.mjs';\nexport { set } from './object/set.mjs';\nexport { setWith } from './object/setWith.mjs';\nexport { toDefaulted } from './object/toDefaulted.mjs';\nexport { toPairs } from './object/toPairs.mjs';\nexport { toPairsIn } from './object/toPairsIn.mjs';\nexport { transform } from './object/transform.mjs';\nexport { unset } from './object/unset.mjs';\nexport { update } from './object/update.mjs';\nexport { updateWith } from './object/updateWith.mjs';\nexport { values } from './object/values.mjs';\nexport { valuesIn } from './object/valuesIn.mjs';\nexport { isFunction } from './predicate/isFunction.mjs';\nexport { isLength } from './predicate/isLength.mjs';\nexport { isMatchWith } from './predicate/isMatchWith.mjs';\nexport { isNative } from './predicate/isNative.mjs';\nexport { isNull } from './predicate/isNull.mjs';\nexport { isUndefined } from './predicate/isUndefined.mjs';\nexport { conforms } from './predicate/conforms.mjs';\nexport { conformsTo } from './predicate/conformsTo.mjs';\nexport { isArguments } from './predicate/isArguments.mjs';\nexport { isArray } from './predicate/isArray.mjs';\nexport { isArrayBuffer } from './predicate/isArrayBuffer.mjs';\nexport { isArrayLike } from './predicate/isArrayLike.mjs';\nexport { isArrayLikeObject } from './predicate/isArrayLikeObject.mjs';\nexport { isBoolean } from './predicate/isBoolean.mjs';\nexport { isBuffer } from './predicate/isBuffer.mjs';\nexport { isDate } from './predicate/isDate.mjs';\nexport { isElement } from './predicate/isElement.mjs';\nexport { isEmpty } from './predicate/isEmpty.mjs';\nexport { isEqualWith } from './predicate/isEqualWith.mjs';\nexport { isError } from './predicate/isError.mjs';\nexport { isFinite } from './predicate/isFinite.mjs';\nexport { isInteger } from './predicate/isInteger.mjs';\nexport { isMap } from './predicate/isMap.mjs';\nexport { isMatch } from './predicate/isMatch.mjs';\nexport { isNaN } from './predicate/isNaN.mjs';\nexport { isNil } from './predicate/isNil.mjs';\nexport { isNumber } from './predicate/isNumber.mjs';\nexport { isObject } from './predicate/isObject.mjs';\nexport { isObjectLike } from './predicate/isObjectLike.mjs';\nexport { isPlainObject } from './predicate/isPlainObject.mjs';\nexport { isRegExp } from './predicate/isRegExp.mjs';\nexport { isSafeInteger } from './predicate/isSafeInteger.mjs';\nexport { isSet } from './predicate/isSet.mjs';\nexport { isString } from './predicate/isString.mjs';\nexport { isSymbol } from './predicate/isSymbol.mjs';\nexport { isTypedArray } from './predicate/isTypedArray.mjs';\nexport { isWeakMap } from './predicate/isWeakMap.mjs';\nexport { isWeakSet } from './predicate/isWeakSet.mjs';\nexport { matches } from './predicate/matches.mjs';\nexport { matchesProperty } from './predicate/matchesProperty.mjs';\nexport { capitalize } from './string/capitalize.mjs';\nexport { bindAll } from './util/bindAll.mjs';\nexport { camelCase } from './string/camelCase.mjs';\nexport { deburr } from './string/deburr.mjs';\nexport { endsWith } from './string/endsWith.mjs';\nexport { escape } from './string/escape.mjs';\nexport { escapeRegExp } from './string/escapeRegExp.mjs';\nexport { kebabCase } from './string/kebabCase.mjs';\nexport { lowerCase } from './string/lowerCase.mjs';\nexport { lowerFirst } from './string/lowerFirst.mjs';\nexport { pad } from './string/pad.mjs';\nexport { padEnd } from './string/padEnd.mjs';\nexport { padStart } from './string/padStart.mjs';\nexport { repeat } from './string/repeat.mjs';\nexport { replace } from './string/replace.mjs';\nexport { snakeCase } from './string/snakeCase.mjs';\nexport { split } from './string/split.mjs';\nexport { startCase } from './string/startCase.mjs';\nexport { startsWith } from './string/startsWith.mjs';\nexport { template, templateSettings } from './string/template.mjs';\nexport { toLower } from './string/toLower.mjs';\nexport { toUpper } from './string/toUpper.mjs';\nexport { trim } from './string/trim.mjs';\nexport { trimEnd } from './string/trimEnd.mjs';\nexport { trimStart } from './string/trimStart.mjs';\nexport { truncate } from './string/truncate.mjs';\nexport { unescape } from './string/unescape.mjs';\nexport { upperCase } from './string/upperCase.mjs';\nexport { upperFirst } from './string/upperFirst.mjs';\nexport { words } from './string/words.mjs';\nexport { cond } from './util/cond.mjs';\nexport { constant } from './util/constant.mjs';\nexport { defaultTo } from './util/defaultTo.mjs';\nexport { isEqualsSameValueZero as eq } from '../_internal/isEqualsSameValueZero.mjs';\nexport { gt } from './util/gt.mjs';\nexport { gte } from './util/gte.mjs';\nexport { invoke } from './util/invoke.mjs';\nexport { iteratee } from './util/iteratee.mjs';\nexport { lt } from './util/lt.mjs';\nexport { lte } from './util/lte.mjs';\nexport { method } from './util/method.mjs';\nexport { methodOf } from './util/methodOf.mjs';\nexport { now } from './util/now.mjs';\nexport { over } from './util/over.mjs';\nexport { overEvery } from './util/overEvery.mjs';\nexport { overSome } from './util/overSome.mjs';\nexport { stubArray } from './util/stubArray.mjs';\nexport { stubFalse } from './util/stubFalse.mjs';\nexport { stubObject } from './util/stubObject.mjs';\nexport { stubString } from './util/stubString.mjs';\nexport { stubTrue } from './util/stubTrue.mjs';\nexport { times } from './util/times.mjs';\nexport { toArray } from './util/toArray.mjs';\nexport { toFinite } from './util/toFinite.mjs';\nexport { toInteger } from './util/toInteger.mjs';\nexport { toLength } from './util/toLength.mjs';\nexport { toNumber } from './util/toNumber.mjs';\nexport { toPath } from './util/toPath.mjs';\nexport { toPlainObject } from './util/toPlainObject.mjs';\nexport { toSafeInteger } from './util/toSafeInteger.mjs';\nexport { toString } from './util/toString.mjs';\nexport { uniqueId } from './util/uniqueId.mjs';\nexport { toolkit as default } from './toolkit.mjs';\n", "export interface Detected {\n browser: string;\n version: string;\n opera: boolean;\n ie: boolean;\n support: boolean;\n filedrop: boolean;\n os: 'win' | 'mac' | 'linux';\n platform: 'Windows' | 'Macintosh' | 'Linux' | 'Unknown';\n isMobileWebKit: boolean;\n browserLocales: string[];\n host: string;\n}\n\nlet _detected: Detected;\n\nexport function utilDetect(refresh?: boolean) {\n if (_detected && !refresh) return _detected;\n _detected = {};\n\n const ua = navigator.userAgent;\n let m;\n\n /* Browser */\n m = ua.match(/(edge)\\/?\\s*(\\.?\\d+(\\.\\d+)*)/i); // Edge\n if (m !== null) {\n _detected.browser = m[1];\n _detected.version = m[2];\n }\n if (!_detected.browser) {\n m = ua.match(/Trident\\/.*rv:([0-9]{1,}[\\.0-9]{0,})/i); // IE11\n if (m !== null) {\n _detected.browser = 'msie';\n _detected.version = m[1];\n }\n }\n if (!_detected.browser) {\n m = ua.match(/(opr)\\/?\\s*(\\.?\\d+(\\.\\d+)*)/i); // Opera 15+\n if (m !== null) {\n _detected.browser = 'Opera';\n _detected.version = m[2];\n }\n }\n if (!_detected.browser) {\n m = ua.match(/(opera|chrome|safari|firefox|msie)\\/?\\s*(\\.?\\d+(\\.\\d+)*)/i);\n if (m !== null) {\n _detected.browser = m[1];\n _detected.version = m[2];\n m = ua.match(/version\\/([\\.\\d]+)/i);\n if (m !== null) _detected.version = m[1];\n }\n }\n if (!_detected.browser) {\n _detected.browser = navigator.appName;\n _detected.version = navigator.appVersion;\n }\n\n // keep major.minor version only..\n _detected.version = _detected.version.split(/\\W/).slice(0,2).join('.');\n\n // detect other browser capabilities\n // Legacy Opera has incomplete svg style support. See #715\n _detected.opera = (_detected.browser.toLowerCase() === 'opera' && Number(_detected.version) < 15 );\n\n if (_detected.browser.toLowerCase() === 'msie') {\n _detected.ie = true;\n _detected.browser = 'Internet Explorer';\n _detected.support = false;\n } else {\n _detected.ie = false;\n _detected.support = true;\n }\n\n _detected.filedrop = (window.FileReader && 'ondrop' in window);\n\n\n /* Platform */\n if (/Win/.test(ua)) {\n _detected.os = 'win';\n _detected.platform = 'Windows';\n } else if (/Mac/.test(ua)) {\n _detected.os = 'mac';\n _detected.platform = 'Macintosh';\n } else if (/X11/.test(ua) || /Linux/.test(ua)) {\n _detected.os = 'linux';\n _detected.platform = 'Linux';\n } else {\n _detected.os = 'win';\n _detected.platform = 'Unknown';\n }\n\n _detected.isMobileWebKit = (/\\b(iPad|iPhone|iPod)\\b/.test(ua) ||\n // HACK: iPadOS 13+ requests desktop sites by default by using a Mac user agent,\n // so assume any \"mac\" with multitouch is actually iOS\n (navigator.platform === 'MacIntel' && 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 1)) &&\n /WebKit/.test(ua) &&\n !/Edge/.test(ua) &&\n // @ts-expect-error -- this attribute is so old that there are no definitions in @types/web\n !window.MSStream;\n\n\n /* Locale */\n // An array of locales requested by the browser in priority order.\n _detected.browserLocales = Array.from(new Set( // remove duplicates\n [navigator.language]\n .concat(navigator.languages || [])\n .concat([\n // @ts-expect-error -- this attribute is so old that there are no definitions in @types/web\n // old property for backwards compatibility\n navigator.userLanguage\n ])\n // remove any undefined values\n .filter(Boolean)\n ));\n\n\n /* Host */\n let loc;\n try {\n loc = window.top!.location;\n } catch {\n loc = window.location;\n }\n\n _detected.host = loc.origin + loc.pathname;\n\n\n return _detected;\n}\n", "/* eslint-disable no-undef */\n\n// cdns for external data packages\nconst presetsCdnUrl = ENV__ID_PRESETS_CDN_URL\n || 'https://cdn.jsdelivr.net/npm/@openstreetmap/id-tagging-schema@{presets_version}/';\nconst ociCdnUrl = ENV__ID_OCI_CDN_URL\n || 'https://cdn.jsdelivr.net/npm/osm-community-index@{version}/';\nconst wmfSitematrixCdnUrl = ENV__ID_WMF_SITEMATRIX_CDN_URL\n || 'https://cdn.jsdelivr.net/npm/wmf-sitematrix@{version}/';\nconst nsiCdnUrl = ENV__ID_NSI_CDN_URL\n || 'https://cdn.jsdelivr.net/npm/name-suggestion-index@{version}/';\n\n// api urls and settings\nconst defaultOsmApiConnections = {\n live: {\n url: 'https://www.openhistoricalmap.org',\n apiUrl: 'https://api.openhistoricalmap.org',\n client_id: '0tmNTmd0Jo1dQp4AUmMBLtGiD9YpMuXzHefitcuVStc',\n client_secret: 'BTlNrNxIPitHdL4sP2clHw5KLoee9aKkA7dQbc0Bj7Q'\n },\n dev: {\n url: 'https://api06.dev.openstreetmap.org',\n client_id: 'Ee1wWJ6UlpERbF6BfTNOpwn0R8k_06mvMXdDUkeHMgw',\n client_secret: 'OnfWFC-JkZNHyYdr_viNn_h_RTZXRslKcUxllOXqf5g'\n }\n};\n/** @type {{ url: string; apiUrl: string; client_id: string; }[]} */\nconst osmApiConnections = [];\nif (ENV__ID_API_CONNECTION_URL !== null &&\n ENV__ID_API_CONNECTION_CLIENT_ID !== null &&\n ENV__ID_API_CONNECTION_CLIENT_SECRET !== null) {\n // user specified API Oauth2 connection details\n // see https://wiki.openstreetmap.org/wiki/OAuth#OAuth_2.0_2\n osmApiConnections.push({\n url: ENV__ID_API_CONNECTION_URL,\n apiUrl: ENV__ID_API_CONNECTION_API_URL,\n client_id: ENV__ID_API_CONNECTION_CLIENT_ID,\n client_secret: ENV__ID_API_CONNECTION_CLIENT_SECRET\n });\n} else if (ENV__ID_API_CONNECTION !== null &&\n defaultOsmApiConnections[ENV__ID_API_CONNECTION] !== undefined) {\n // if environment variable ID_API_CONNECTION is either \"live\" or \"dev\":\n // only allow to connect to the respective OSM server\n osmApiConnections.push(defaultOsmApiConnections[ENV__ID_API_CONNECTION]);\n} else {\n // offer both \"live\" and \"dev\" servers by default\n osmApiConnections.push(defaultOsmApiConnections.live);\n osmApiConnections.push(defaultOsmApiConnections.dev);\n}\n\n// auxiliary OSM services\nconst taginfoApiUrl = ENV__ID_TAGINFO_API_URL\n || 'https://taginfo.openstreetmap.org/api/4/';\nconst nominatimApiUrl = ENV__ID_NOMINATIM_API_URL\n || 'https://nominatim.openstreetmap.org/';\n\n// support/donation message on upload success screen\nconst showDonationMessage = ENV__ID_SHOW_DONATION_MESSAGE !== 'false';\n\nexport {\n presetsCdnUrl,\n ociCdnUrl,\n wmfSitematrixCdnUrl,\n nsiCdnUrl,\n osmApiConnections,\n taginfoApiUrl,\n nominatimApiUrl,\n showDonationMessage\n};\n", "import { select as d3_select } from 'd3-selection';\nimport { escape } from 'es-toolkit/compat';\n\nimport { fileFetcher } from './file_fetcher';\nimport { utilDetect } from '../util/detect';\nimport { utilStringQs } from '../util';\nimport { utilArrayUniq } from '../util/array';\nimport { presetsCdnUrl } from '../../config/id.js';\n\nlet _mainLocalizer = coreLocalizer(); // singleton\nlet _t = _mainLocalizer.t;\n\nexport {\n _mainLocalizer as localizer,\n // export `t` function for ease-of-use\n _t as t\n};\n\n//\n// coreLocalizer manages language and locale parameters including translated strings\n//\nexport function coreLocalizer() {\n\n let localizer = {};\n\n let _dataLanguages = {};\n\n // `_dataLocales` is an object containing all _supported_ locale codes -> language info.\n // * `rtl` - right-to-left or left-to-right text direction\n // * `pct` - the percent of strings translated; 1 = 100%, full coverage\n //\n // {\n // en: { rtl: false, pct: {\u2026} },\n // de: { rtl: false, pct: {\u2026} },\n // \u2026\n // }\n let _dataLocales = {};\n\n // `localeStrings` is an object containing all _loaded_ locale codes -> string data.\n // {\n // en: { icons: {\u2026}, toolbar: {\u2026}, modes: {\u2026}, operations: {\u2026}, \u2026 },\n // de: { icons: {\u2026}, toolbar: {\u2026}, modes: {\u2026}, operations: {\u2026}, \u2026 },\n // \u2026\n // }\n let _localeStrings = {};\n\n // the current locale\n let _localeCode = 'en-US';\n // `_localeCodes` must contain `_localeCode` first, optionally followed by fallbacks\n let _localeCodes = ['en-US', 'en'];\n let _languageCode = 'en';\n let _textDirection = 'ltr';\n let _usesMetric = false;\n let _languageNames = {};\n let _scriptNames = {};\n\n // getters for the current locale parameters\n localizer.localeCode = () => _localeCode;\n localizer.localeCodes = () => _localeCodes;\n localizer.languageCode = () => _languageCode;\n localizer.textDirection = () => _textDirection;\n localizer.usesMetric = () => _usesMetric;\n localizer.languageNames = () => _languageNames;\n localizer.scriptNames = () => _scriptNames;\n localizer.languages = () => _dataLanguages; // Expose all the languages supported\n\n\n // The client app may want to manually set the locale, regardless of the\n // settings provided by the browser\n let _preferredLocaleCodes = [];\n localizer.preferredLocaleCodes = function(codes) {\n if (!arguments.length) return _preferredLocaleCodes;\n if (typeof codes === 'string') {\n // be generous and accept delimited strings as input\n _preferredLocaleCodes = codes.split(/,|;| /gi).filter(Boolean);\n } else {\n _preferredLocaleCodes = codes;\n }\n _loadPromise = undefined;\n return localizer;\n };\n\n\n var _loadPromise;\n\n localizer.ensureLoaded = () => {\n if (_loadPromise) return _loadPromise;\n\n let filesToFetch = [\n 'languages', // load the list of languages\n 'locales' // load the list of supported locales\n ];\n\n const localeDirs = {\n general: 'locales',\n tagging: presetsCdnUrl + 'dist/translations'\n };\n\n let fileMap = fileFetcher.fileMap();\n for (let scopeId in localeDirs) {\n const key = `locales_index_${scopeId}`;\n if (!fileMap[key]) {\n fileMap[key] = localeDirs[scopeId] + '/index.min.json';\n }\n filesToFetch.push(key);\n }\n\n return _loadPromise = Promise.all(filesToFetch.map(key => fileFetcher.get(key)))\n .then(results => {\n _dataLanguages = results[0];\n _dataLocales = results[1];\n\n let indexes = results.slice(2);\n\n _localeCodes = localizer.localesToUseFrom(_dataLocales);\n _localeCode = _localeCodes[0]; // Run iD in the highest-priority locale; the rest are fallbacks\n\n let loadStringsPromises = [];\n\n indexes.forEach((index, i) => {\n // Will always return the index for `en` if nothing else\n const fullCoverageIndex = _localeCodes.findIndex(function(locale) {\n // Only English contains everything including the OpenHistoricalMap additions, because we haven\u2019t set up a translation management system yet.\n // https://github.com/OpenHistoricalMap/issues/issues/470\n return locale === 'en' && index[locale] && index[locale].pct === 1;\n });\n // We only need to load locales up until we find one with full coverage\n _localeCodes.slice(0, fullCoverageIndex + 1).forEach(function(code) {\n let scopeId = Object.keys(localeDirs)[i];\n let directory = Object.values(localeDirs)[i];\n if (index[code]) loadStringsPromises.push(localizer.loadLocale(code, scopeId, directory));\n });\n });\n\n return Promise.all(loadStringsPromises);\n })\n .then(() => {\n updateForCurrentLocale();\n })\n .catch(err => console.error(err)); // eslint-disable-line\n };\n\n // Returns the locales from `requestedLocales` supported by iD that we should use\n /** @param {{ [locale: string]: unknown }} supportedLocales */\n localizer.localesToUseFrom = (supportedLocales) => {\n const requestedLocales = [\n ...(_preferredLocaleCodes || []),\n ...utilDetect().browserLocales, // List of locales preferred by the browser in priority order.\n 'en', // fallback to English since it's the only guaranteed complete language\n ];\n\n /** @type {string[]} */\n let toUse = [];\n for (const locale of requestedLocales) {\n if (supportedLocales[locale]) toUse.push(locale);\n\n if ('Intl' in window && 'Locale' in window.Intl) {\n // Full locale ('es-ES'), add fallback to the base ('es')\n const localeObj = new Intl.Locale(locale);\n const withoutScript = `${localeObj.language}-${localeObj.region}`;\n const base = localeObj.language;\n\n if (supportedLocales[withoutScript]) toUse.push(withoutScript);\n if (supportedLocales[base]) toUse.push(base);\n } else if (locale.includes('-')) {\n // legacy logic: if Intl.Locale is not available\n let langPart = locale.split('-')[0];\n if (supportedLocales[langPart]) toUse.push(langPart);\n }\n }\n // remove duplicates\n return utilArrayUniq(toUse);\n };\n\n function updateForCurrentLocale() {\n if (!_localeCode) return;\n\n _languageCode = _localeCode.split('-')[0];\n\n const currentData = _dataLocales[_localeCode] || _dataLocales[_languageCode];\n\n const hash = utilStringQs(window.location.hash);\n\n if (hash.rtl === 'true') {\n _textDirection = 'rtl';\n } else if (hash.rtl === 'false') {\n _textDirection = 'ltr';\n } else {\n _textDirection = currentData && currentData.rtl ? 'rtl' : 'ltr';\n }\n\n let locale = _localeCode;\n if (locale.toLowerCase() === 'en-us') locale = 'en';\n\n // some locales (like fr-FR) have no languageNames or scriptNames,\n // so we need to load them from the base language (see #8673)\n _languageNames = (\n _localeStrings.general[locale].languageNames ||\n _localeStrings.general[_languageCode].languageNames\n );\n _scriptNames = (\n _localeStrings.general[locale].scriptNames ||\n _localeStrings.general[_languageCode].scriptNames\n );\n\n _usesMetric = _localeCode.slice(-3).toLowerCase() !== '-us';\n }\n\n\n /* Locales */\n // Returns a Promise to load the strings for the requested locale\n localizer.loadLocale = (locale, scopeId, directory) => {\n\n // US English is the default\n if (locale.toLowerCase() === 'en-us') locale = 'en';\n\n if (_localeStrings[scopeId] && _localeStrings[scopeId][locale]) { // already loaded\n return Promise.resolve(locale);\n }\n\n let fileMap = fileFetcher.fileMap();\n const key = `locale_${scopeId}_${locale}`;\n if (!fileMap[key]) {\n fileMap[key] = `${directory}/${locale}.min.json`;\n }\n\n return fileFetcher.get(key)\n .then(d => {\n if (!_localeStrings[scopeId]) _localeStrings[scopeId] = {};\n _localeStrings[scopeId][locale] = d[locale];\n return locale;\n });\n };\n\n localizer.pluralRule = function(number) {\n return pluralRule(number, _localeCode);\n };\n\n // Returns the plural rule for the given `number` with the given `localeCode`.\n // One of: `zero`, `one`, `two`, `few`, `many`, `other`\n function pluralRule(number, localeCode) {\n\n // modern browsers have this functionality built-in\n const rules = 'Intl' in window && Intl.PluralRules && new Intl.PluralRules(localeCode);\n if (rules) {\n return rules.select(number);\n }\n\n // fallback to basic one/other, as in English\n if (number === 1) return 'one';\n return 'other';\n }\n\n /**\n * Try to find that string in `locale` or the current `_localeCode` matching\n * the given `stringId`. If no string can be found in the requested locale,\n * we'll recurse down all the `_localeCodes` until one is found.\n *\n * @param {string} origStringId string identifier\n * @param {object?} replacements token replacements and default string\n * @param {string?} locale locale to use (defaults to currentLocale)\n * @return {{locale: string, texts: [string|function]}} a list of localized strings and replacement parts\n */\n localizer.tInfo = function(origStringId, replacements, locale) {\n let stringId = origStringId.trim();\n\n let scopeId = 'general';\n\n if (stringId[0] === '_') {\n let split = stringId.split('.');\n scopeId = split[0].slice(1);\n stringId = split.slice(1).join('.');\n }\n\n locale = locale || _localeCode;\n\n let path = stringId\n .split('.')\n .map(s => s.replace(//g, '.'))\n .reverse();\n\n let stringsKey = locale;\n // US English is the default\n if (stringsKey.toLowerCase() === 'en-us') stringsKey = 'en';\n let localeString = _localeStrings && _localeStrings[scopeId] && _localeStrings[scopeId][stringsKey];\n\n while (localeString !== undefined && path.length) {\n localeString = localeString[path.pop()];\n }\n\n if (localeString !== undefined) {\n if (replacements) {\n if (typeof localeString === 'object' && Object.keys(localeString).length) {\n // If plural forms are provided, dig one level deeper based on the\n // first numeric token replacement provided.\n const number = Object.values(replacements).find(function(value) {\n return typeof value === 'number';\n });\n if (number !== undefined) {\n const rule = pluralRule(number, locale);\n if (localeString[rule]) {\n localeString = localeString[rule];\n } else {\n // We're pretty sure this should be a plural but no string\n // could be found for the given rule. Just pick the first\n // string and hope it makes sense.\n localeString = Object.values(localeString)[0];\n }\n }\n }\n if (typeof localeString === 'string') {\n let parts = [localeString];\n for (let key in replacements) {\n const token = `{${key}}`;\n const regex = new RegExp(token, 'g');\n parts = parts.flatMap(part => {\n if (typeof part === 'object') return part;\n return part\n .split(regex)\n .flatMap(p => [{key}, p])\n .slice(1);\n });\n }\n\n const result = parts.map(part => {\n if (typeof part === 'object') {\n const value = replacements[part.key];\n\n if (typeof value === 'number') {\n if (value.toLocaleString) {\n // format numbers for the locale\n return value.toLocaleString(locale, {\n style: 'decimal',\n useGrouping: true,\n minimumFractionDigits: 0\n });\n } else {\n return value.toString();\n }\n }\n\n return value;\n }\n\n return part;\n });\n\n return {\n texts: result,\n locale\n };\n }\n }\n return {\n texts: [localeString],\n locale\n };\n }\n // no localized string found...\n\n // attempt to fallback to a lower-priority language\n let index = _localeCodes.indexOf(locale);\n if (index >= 0 && index < _localeCodes.length - 1) {\n // eventually this will be 'en' or another locale with 100% coverage\n let fallback = _localeCodes[index + 1];\n return localizer.tInfo(origStringId, replacements, fallback);\n }\n\n if (replacements && 'default' in replacements) {\n // Fallback to a default value if one is specified in `replacements`\n return {\n texts: [replacements.default],\n locale: null\n };\n }\n\n const missing = `Missing ${locale} translation: ${origStringId}`;\n if (typeof console !== 'undefined') console.error(missing); // eslint-disable-line\n\n return {\n texts: [missing],\n locale: 'en'\n };\n };\n\n localizer.hasTextForStringId = function(stringId) {\n return !!localizer.tInfo(stringId, { default: 'nothing found'}).locale;\n };\n\n localizer.coalesceStringIds = stringIds => stringIds.find(id => localizer.hasTextForStringId(id)) || stringIds[stringIds.length - 1];\n\n // Returns only the localized text, discarding the locale info\n localizer.t = function(stringId, replacements, locale) {\n return localizer.tInfo(stringId, replacements, locale).texts.join('');\n };\n\n // Returns the localized text wrapped in an HTML element encoding the locale info\n /**\n * @deprecated This method is considered deprecated. Instead, use the direct DOM manipulating\n * method `t.append`.\n */\n localizer.t.html = function(stringId, replacements, locale) {\n // replacement string might be html unsafe, so we need to escape it except if it is explicitly marked as html code\n replacements = Object.assign({}, replacements);\n for (var k in replacements) {\n if (typeof replacements[k] === 'string') {\n replacements[k] = escape(replacements[k]);\n }\n if (typeof replacements[k] === 'object' && typeof replacements[k].html === 'string') {\n replacements[k] = replacements[k].html;\n }\n }\n\n const info = localizer.tInfo(stringId, replacements, locale);\n // text may be empty or undefined if `replacements.default` is\n const text = info.texts.join('');\n if (text) {\n return `${text}`;\n } else {\n return '';\n }\n };\n\n // Adds localized text wrapped as an HTML span element with locale info to the DOM\n localizer.t.append = function(stringId, replacements, locale) {\n const ret = function(selection) {\n const info = localizer.tInfo(stringId, replacements, locale);\n const texts = [\n replacements?.prefix,\n ...info.texts,\n replacements?.suffix\n ].filter(Boolean);\n\n texts.forEach(text => {\n if (typeof text === 'string') {\n selection.append('span')\n .attr('class', 'localized-text')\n .attr('lang', info.locale || 'und')\n .text(replacements?._trim ? text.trim() : text);\n } else {\n selection.call(text);\n }\n });\n };\n ret.stringId = stringId;\n return ret;\n };\n\n // Adds or updates a localized text wrapped as an HTML span element with locale info to the DOM\n localizer.t.addOrUpdate = function(stringId, replacements, locale) {\n const ret = function(selection) {\n const info = localizer.tInfo(stringId, replacements, locale);\n const texts = [\n replacements?.prefix,\n ...info.texts,\n replacements?.suffix\n ].filter(Boolean);\n\n const span = selection.selectAll('span')\n .data(texts.map((_, i) => i), d => stringId + d);\n span.exit().remove();\n const enter = span.enter()\n .append('span');\n span.merge(enter).each(function(d) {\n const text = texts[d];\n if (typeof text === 'string') {\n d3_select(this)\n .classed('localized-text', true)\n .attr('lang', info.locale || 'und')\n .text(replacements?._trim ? text.trim() : text);\n } else {\n d3_select(this).call(text);\n }\n });\n };\n ret.stringId = stringId;\n return ret;\n };\n\n localizer.languageName = (code, options) => {\n\n if (_languageNames && _languageNames[code]) { // name in locale language\n // e.g. \"German\"\n return _languageNames[code];\n }\n\n // sometimes we only want the local name\n if (options && options.localOnly) return null;\n\n const langInfo = _dataLanguages[code];\n if (langInfo) {\n if (langInfo.nativeName) { // name in native language\n // e.g. \"Deutsch (de)\"\n return localizer.t('translate.language_and_code', { language: langInfo.nativeName, code: code });\n\n } else if (langInfo.base && langInfo.script) {\n const base = langInfo.base; // the code of the language this is based on\n\n if (_languageNames && _languageNames[base]) { // base language name in locale language\n const scriptCode = langInfo.script;\n const script = (_scriptNames && _scriptNames[scriptCode]) || scriptCode;\n // e.g. \"Serbian (Cyrillic)\"\n return localizer.t('translate.language_and_code', { language: _languageNames[base], code: script });\n\n } else if (_dataLanguages[base] && _dataLanguages[base].nativeName) {\n // e.g. \"\u0441\u0440\u043F\u0441\u043A\u0438 (sr-Cyrl)\"\n return localizer.t('translate.language_and_code', { language: _dataLanguages[base].nativeName, code: code });\n }\n }\n }\n return code; // if not found, use the code\n };\n\n /**\n * Returns a function that formats a floating-point number in the given\n * locale.\n */\n localizer.floatFormatter = (locale) => {\n if (!('Intl' in window && 'NumberFormat' in Intl &&\n 'formatToParts' in Intl.NumberFormat.prototype)) {\n return (number, fractionDigits) => {\n return fractionDigits === undefined ? number.toString() : number.toFixed(fractionDigits);\n };\n } else {\n return (number, fractionDigits) => number.toLocaleString(locale, {\n minimumFractionDigits: fractionDigits,\n maximumFractionDigits: fractionDigits === undefined ? 20 : fractionDigits,\n });\n }\n };\n\n /**\n * Returns a function that parses a number formatted according to the given\n * locale as a floating-point number.\n */\n localizer.floatParser = (locale) => {\n // https://stackoverflow.com/a/55366435/4585461\n const polyfill = (string) => +string.trim();\n if (!('Intl' in window && 'NumberFormat' in Intl)) return polyfill;\n const format = new Intl.NumberFormat(locale, { maximumFractionDigits: 20 });\n if (!('formatToParts' in format)) return polyfill;\n const parts = format.formatToParts(-12345.6);\n const numerals = Array.from({ length: 10 }).map((_, i) => format.format(i));\n const index = new Map(numerals.map((d, i) => [d, i]));\n const literalPart = parts.find(d => d.type === 'literal');\n const literal = literalPart && new RegExp(`[${literalPart.value}]`, 'g');\n const groupPart = parts.find(d => d.type === 'group');\n const group = groupPart && new RegExp(`[${groupPart.value}]`, 'g');\n const decimalPart = parts.find(d => d.type === 'decimal');\n const decimal = decimalPart && new RegExp(`[${decimalPart.value}]`);\n const numeral = new RegExp(`[${numerals.join('')}]`, 'g');\n const getIndex = d => index.get(d);\n return (string) => {\n string = string.trim();\n if (literal) string = string.replace(literal, '');\n if (group) string = string.replace(group, '');\n if (decimal) string = string.replace(decimal, '.');\n string = string.replace(numeral, getIndex);\n return string ? +string : NaN;\n };\n };\n\n /**\n * Returns a function that returns the number of decimal places in a\n * formatted number string.\n */\n localizer.decimalPlaceCounter = (locale) => {\n var literal, group, decimal;\n if ('Intl' in window && 'NumberFormat' in Intl) {\n const format = new Intl.NumberFormat(locale, { maximumFractionDigits: 20 });\n if ('formatToParts' in format) {\n const parts = format.formatToParts(-12345.6);\n const literalPart = parts.find(d => d.type === 'literal');\n literal = literalPart && new RegExp(`[${literalPart.value}]`, 'g');\n const groupPart = parts.find(d => d.type === 'group');\n group = groupPart && new RegExp(`[${groupPart.value}]`, 'g');\n const decimalPart = parts.find(d => d.type === 'decimal');\n decimal = decimalPart && new RegExp(`[${decimalPart.value}]`);\n }\n }\n return (string) => {\n string = string.trim();\n if (literal) string = string.replace(literal, '');\n if (group) string = string.replace(group, '');\n const parts = string.split(decimal || '.');\n return parts && parts[1] && parts[1].length || 0;\n };\n };\n\n return localizer;\n}\n", "export function assert(value, message) {\n return equal(!!value, true, message ||\n `expected \"${value}\" to be ok`)\n}\n\nexport function equal(actual, expected, message) {\n // eslint-disable-next-line eqeqeq\n if (actual == expected)\n return true\n\n if (Number.isNaN(actual) && Number.isNaN(expected))\n return true\n\n throw new Error(message ||\n `expected \"${actual}\" to equal \"${expected}\"`)\n}\n\nassert.equal = equal\n\nexport default assert\n", "const DAY = /^days?$/i\nconst MONTH = /^months?$/i\nconst YEAR = /^years?$/i\nconst SYMBOL = /^[xX]$/\nconst SYMBOLS = /[xX]/g\nconst PATTERN = /^[0-9xXdDmMyY]{8}$/\nconst YYYYMMDD = 'YYYYMMDD'.split('')\nconst MAXDAYS = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]\n\nconst { floor, pow, max, min } = Math\n\n\n/**\n * Bitmasks are used to set Unspecified, Uncertain and\n * Approximate flags for a Date. The bitmask for one\n * feature corresponds to a numeric value based on the\n * following pattern:\n *\n * YYYYMMDD\n * --------\n * Day 00000011\n * Month 00001100\n * Year 11110000\n *\n */\nexport class Bitmask {\n\n static test(a, b) {\n return this.convert(a) & this.convert(b)\n }\n\n static convert(value = 0) { // eslint-disable-line complexity\n value = value || 0\n\n if (value instanceof Bitmask) return value.value\n\n switch (typeof value) {\n case 'number': return value\n\n case 'boolean': return value ? Bitmask.YMD : 0\n\n case 'string':\n if (DAY.test(value)) return Bitmask.DAY\n if (MONTH.test(value)) return Bitmask.MONTH\n if (YEAR.test(value)) return Bitmask.YEAR\n if (PATTERN.test(value)) return Bitmask.compute(value)\n // fall through!\n\n default:\n throw new Error(`invalid value: ${value}`)\n }\n }\n\n static compute(value) {\n return value.split('').reduce((memo, c, idx) =>\n (memo | (SYMBOL.test(c) ? pow(2, idx) : 0)), 0)\n }\n\n static values(mask, digit = 0, normalize = true) {\n let num = Bitmask.numbers(mask, digit).split('')\n let values = [Number(num.slice(0, 4).join(''))]\n\n if (num.length > 4) values.push(Number(num.slice(4, 6).join('')))\n if (num.length > 6) values.push(Number(num.slice(6, 8).join('')))\n\n return normalize ? Bitmask.normalize(values) : values\n }\n\n static numbers(mask, digit = 0) {\n return mask.replace(SYMBOLS, digit)\n }\n\n static normalize(values) {\n if (values.length > 1)\n values[1] = min(11, max(0, values[1] - 1))\n\n if (values.length > 2)\n values[2] = min(MAXDAYS[values[1]] || NaN, max(1, values[2]))\n\n return values\n }\n\n\n constructor(value = 0) {\n this.value = Bitmask.convert(value)\n }\n\n test(value = 0) {\n return this.value & Bitmask.convert(value)\n }\n\n bit(k) {\n return this.value & pow(2, k)\n }\n\n get day() { return this.test(Bitmask.DAY) }\n\n get month() { return this.test(Bitmask.MONTH) }\n\n get year() { return this.test(Bitmask.YEAR) }\n\n\n add(value) {\n return (this.value = this.value | Bitmask.convert(value)), this\n }\n\n set(value = 0) {\n return (this.value = Bitmask.convert(value)), this\n }\n\n mask(input = YYYYMMDD, offset = 0, symbol = 'X') {\n return input.map((c, idx) => this.bit(offset + idx) ? symbol : c)\n }\n\n masks(values, symbol = 'X') {\n let offset = 0\n\n return values.map(value => {\n let mask = this.mask(value.split(''), offset, symbol)\n offset = offset + mask.length\n\n return mask.join('')\n })\n }\n\n // eslint-disable-next-line complexity\n max([year, month, day]) {\n if (!year) return []\n\n year = Number(\n (this.test(Bitmask.YEAR)) ? this.masks([year], '9')[0] : year\n )\n\n if (!month) return [year]\n\n month = Number(month) - 1\n\n switch (this.test(Bitmask.MONTH)) {\n case Bitmask.MONTH:\n month = 11\n break\n case Bitmask.MX:\n month = (month < 9) ? 8 : 11\n break\n case Bitmask.XM:\n month = (month + 1) % 10\n month = (month < 3) ? month + 9 : month - 1\n break\n }\n\n if (!day) return [year, month]\n\n day = Number(day)\n\n switch (this.test(Bitmask.DAY)) {\n case Bitmask.DAY:\n day = MAXDAYS[month]\n break\n case Bitmask.DX:\n day = min(MAXDAYS[month], day + (9 - (day % 10)))\n break\n case Bitmask.XD:\n day = day % 10\n\n if (month === 1) {\n day = (day === 9 && !leap(year)) ? day + 10 : day + 20\n\n } else {\n day = (day < 2) ? day + 30 : day + 20\n if (day > MAXDAYS[month]) day = day - 10\n }\n\n break\n }\n\n if (month === 1 && day > 28 && !leap(year)) {\n day = 28\n }\n\n return [year, month, day]\n }\n\n // eslint-disable-next-line complexity\n min([year, month, day]) {\n if (!year) return []\n\n year = Number(\n (this.test(Bitmask.YEAR)) ? this.masks([year], '0')[0] : year\n )\n\n if (month == null) return [year]\n\n month = Number(month) - 1\n\n switch (this.test(Bitmask.MONTH)) {\n case Bitmask.MONTH:\n case Bitmask.XM:\n month = 0\n break\n case Bitmask.MX:\n month = (month < 9) ? 0 : 9\n break\n }\n\n if (!day) return [year, month]\n\n day = Number(day)\n\n switch (this.test(Bitmask.DAY)) {\n case Bitmask.DAY:\n day = 1\n break\n case Bitmask.DX:\n day = max(1, floor(day / 10) * 10)\n break\n case Bitmask.XD:\n day = max(1, day % 10)\n break\n }\n\n return [year, month, day]\n }\n\n marks(values, symbol = '?') {\n return values\n .map((value, idx) => [\n this.qualified(idx * 2) ? symbol : '',\n value,\n this.qualified(idx * 2 + 1) ? symbol : ''\n ].join(''))\n }\n\n qualified(idx) { // eslint-disable-line complexity\n switch (idx) {\n case 1:\n return this.value === Bitmask.YEAR ||\n (this.value & Bitmask.YEAR) && !(this.value & Bitmask.MONTH)\n case 2:\n return this.value === Bitmask.MONTH ||\n (this.value & Bitmask.MONTH) && !(this.value & Bitmask.YEAR)\n case 3:\n return this.value === Bitmask.YM\n case 4:\n return this.value === Bitmask.DAY ||\n (this.value & Bitmask.DAY) && (this.value !== Bitmask.YMD)\n case 5:\n return this.value === Bitmask.YMD\n default:\n return false\n }\n }\n\n qualify(idx) {\n return (this.value = this.value | Bitmask.UA[idx]), this\n }\n\n toJSON() {\n return this.value\n }\n\n toString(symbol = 'X') {\n return this.masks(['YYYY', 'MM', 'DD'], symbol).join('-')\n }\n}\n\nBitmask.prototype.is = Bitmask.prototype.test\n\nfunction leap(year) {\n if (year % 4 > 0) return false\n if (year % 100 > 0) return true\n if (year % 400 > 0) return false\n return true\n}\n\nBitmask.DAY = Bitmask.D = Bitmask.compute('yyyymmxx')\nBitmask.MONTH = Bitmask.M = Bitmask.compute('yyyyxxdd')\nBitmask.YEAR = Bitmask.Y = Bitmask.compute('xxxxmmdd')\n\nBitmask.MD = Bitmask.M | Bitmask.D\nBitmask.YMD = Bitmask.Y | Bitmask.MD\nBitmask.YM = Bitmask.Y | Bitmask.M\n\nBitmask.YYXX = Bitmask.compute('yyxxmmdd')\nBitmask.YYYX = Bitmask.compute('yyyxmmdd')\nBitmask.XXXX = Bitmask.compute('xxxxmmdd')\n\nBitmask.DX = Bitmask.compute('yyyymmdx')\nBitmask.XD = Bitmask.compute('yyyymmxd')\nBitmask.MX = Bitmask.compute('yyyymxdd')\nBitmask.XM = Bitmask.compute('yyyyxmdd')\n\n/*\n * Map each UA symbol position to a mask.\n *\n * ~YYYY~-~MM~-~DD~\n * 0 1 2 3 4 5\n */\nBitmask.UA = [\n Bitmask.YEAR,\n Bitmask.YEAR, // YEAR !DAY\n Bitmask.MONTH,\n Bitmask.YM,\n Bitmask.DAY, // YEARDAY\n Bitmask.YMD\n]\n", "(function(root, factory) {\n if (typeof module === 'object' && module.exports) {\n module.exports = factory();\n } else {\n root.nearley = factory();\n }\n}(this, function() {\n\n function Rule(name, symbols, postprocess) {\n this.id = ++Rule.highestId;\n this.name = name;\n this.symbols = symbols; // a list of literal | regex class | nonterminal\n this.postprocess = postprocess;\n return this;\n }\n Rule.highestId = 0;\n\n Rule.prototype.toString = function(withCursorAt) {\n var symbolSequence = (typeof withCursorAt === \"undefined\")\n ? this.symbols.map(getSymbolShortDisplay).join(' ')\n : ( this.symbols.slice(0, withCursorAt).map(getSymbolShortDisplay).join(' ')\n + \" \u25CF \"\n + this.symbols.slice(withCursorAt).map(getSymbolShortDisplay).join(' ') );\n return this.name + \" \u2192 \" + symbolSequence;\n }\n\n\n // a State is a rule at a position from a given starting point in the input stream (reference)\n function State(rule, dot, reference, wantedBy) {\n this.rule = rule;\n this.dot = dot;\n this.reference = reference;\n this.data = [];\n this.wantedBy = wantedBy;\n this.isComplete = this.dot === rule.symbols.length;\n }\n\n State.prototype.toString = function() {\n return \"{\" + this.rule.toString(this.dot) + \"}, from: \" + (this.reference || 0);\n };\n\n State.prototype.nextState = function(child) {\n var state = new State(this.rule, this.dot + 1, this.reference, this.wantedBy);\n state.left = this;\n state.right = child;\n if (state.isComplete) {\n state.data = state.build();\n // Having right set here will prevent the right state and its children\n // form being garbage collected\n state.right = undefined;\n }\n return state;\n };\n\n State.prototype.build = function() {\n var children = [];\n var node = this;\n do {\n children.push(node.right.data);\n node = node.left;\n } while (node.left);\n children.reverse();\n return children;\n };\n\n State.prototype.finish = function() {\n if (this.rule.postprocess) {\n this.data = this.rule.postprocess(this.data, this.reference, Parser.fail);\n }\n };\n\n\n function Column(grammar, index) {\n this.grammar = grammar;\n this.index = index;\n this.states = [];\n this.wants = {}; // states indexed by the non-terminal they expect\n this.scannable = []; // list of states that expect a token\n this.completed = {}; // states that are nullable\n }\n\n\n Column.prototype.process = function(nextColumn) {\n var states = this.states;\n var wants = this.wants;\n var completed = this.completed;\n\n for (var w = 0; w < states.length; w++) { // nb. we push() during iteration\n var state = states[w];\n\n if (state.isComplete) {\n state.finish();\n if (state.data !== Parser.fail) {\n // complete\n var wantedBy = state.wantedBy;\n for (var i = wantedBy.length; i--; ) { // this line is hot\n var left = wantedBy[i];\n this.complete(left, state);\n }\n\n // special-case nullables\n if (state.reference === this.index) {\n // make sure future predictors of this rule get completed.\n var exp = state.rule.name;\n (this.completed[exp] = this.completed[exp] || []).push(state);\n }\n }\n\n } else {\n // queue scannable states\n var exp = state.rule.symbols[state.dot];\n if (typeof exp !== 'string') {\n this.scannable.push(state);\n continue;\n }\n\n // predict\n if (wants[exp]) {\n wants[exp].push(state);\n\n if (completed.hasOwnProperty(exp)) {\n var nulls = completed[exp];\n for (var i = 0; i < nulls.length; i++) {\n var right = nulls[i];\n this.complete(state, right);\n }\n }\n } else {\n wants[exp] = [state];\n this.predict(exp);\n }\n }\n }\n }\n\n Column.prototype.predict = function(exp) {\n var rules = this.grammar.byName[exp] || [];\n\n for (var i = 0; i < rules.length; i++) {\n var r = rules[i];\n var wantedBy = this.wants[exp];\n var s = new State(r, 0, this.index, wantedBy);\n this.states.push(s);\n }\n }\n\n Column.prototype.complete = function(left, right) {\n var copy = left.nextState(right);\n this.states.push(copy);\n }\n\n\n function Grammar(rules, start) {\n this.rules = rules;\n this.start = start || this.rules[0].name;\n var byName = this.byName = {};\n this.rules.forEach(function(rule) {\n if (!byName.hasOwnProperty(rule.name)) {\n byName[rule.name] = [];\n }\n byName[rule.name].push(rule);\n });\n }\n\n // So we can allow passing (rules, start) directly to Parser for backwards compatibility\n Grammar.fromCompiled = function(rules, start) {\n var lexer = rules.Lexer;\n if (rules.ParserStart) {\n start = rules.ParserStart;\n rules = rules.ParserRules;\n }\n var rules = rules.map(function (r) { return (new Rule(r.name, r.symbols, r.postprocess)); });\n var g = new Grammar(rules, start);\n g.lexer = lexer; // nb. storing lexer on Grammar is iffy, but unavoidable\n return g;\n }\n\n\n function StreamLexer() {\n this.reset(\"\");\n }\n\n StreamLexer.prototype.reset = function(data, state) {\n this.buffer = data;\n this.index = 0;\n this.line = state ? state.line : 1;\n this.lastLineBreak = state ? -state.col : 0;\n }\n\n StreamLexer.prototype.next = function() {\n if (this.index < this.buffer.length) {\n var ch = this.buffer[this.index++];\n if (ch === '\\n') {\n this.line += 1;\n this.lastLineBreak = this.index;\n }\n return {value: ch};\n }\n }\n\n StreamLexer.prototype.save = function() {\n return {\n line: this.line,\n col: this.index - this.lastLineBreak,\n }\n }\n\n StreamLexer.prototype.formatError = function(token, message) {\n // nb. this gets called after consuming the offending token,\n // so the culprit is index-1\n var buffer = this.buffer;\n if (typeof buffer === 'string') {\n var lines = buffer\n .split(\"\\n\")\n .slice(\n Math.max(0, this.line - 5), \n this.line\n );\n\n var nextLineBreak = buffer.indexOf('\\n', this.index);\n if (nextLineBreak === -1) nextLineBreak = buffer.length;\n var col = this.index - this.lastLineBreak;\n var lastLineDigits = String(this.line).length;\n message += \" at line \" + this.line + \" col \" + col + \":\\n\\n\";\n message += lines\n .map(function(line, i) {\n return pad(this.line - lines.length + i + 1, lastLineDigits) + \" \" + line;\n }, this)\n .join(\"\\n\");\n message += \"\\n\" + pad(\"\", lastLineDigits + col) + \"^\\n\";\n return message;\n } else {\n return message + \" at index \" + (this.index - 1);\n }\n\n function pad(n, length) {\n var s = String(n);\n return Array(length - s.length + 1).join(\" \") + s;\n }\n }\n\n function Parser(rules, start, options) {\n if (rules instanceof Grammar) {\n var grammar = rules;\n var options = start;\n } else {\n var grammar = Grammar.fromCompiled(rules, start);\n }\n this.grammar = grammar;\n\n // Read options\n this.options = {\n keepHistory: false,\n lexer: grammar.lexer || new StreamLexer,\n };\n for (var key in (options || {})) {\n this.options[key] = options[key];\n }\n\n // Setup lexer\n this.lexer = this.options.lexer;\n this.lexerState = undefined;\n\n // Setup a table\n var column = new Column(grammar, 0);\n var table = this.table = [column];\n\n // I could be expecting anything.\n column.wants[grammar.start] = [];\n column.predict(grammar.start);\n // TODO what if start rule is nullable?\n column.process();\n this.current = 0; // token index\n }\n\n // create a reserved token for indicating a parse fail\n Parser.fail = {};\n\n Parser.prototype.feed = function(chunk) {\n var lexer = this.lexer;\n lexer.reset(chunk, this.lexerState);\n\n var token;\n while (true) {\n try {\n token = lexer.next();\n if (!token) {\n break;\n }\n } catch (e) {\n // Create the next column so that the error reporter\n // can display the correctly predicted states.\n var nextColumn = new Column(this.grammar, this.current + 1);\n this.table.push(nextColumn);\n var err = new Error(this.reportLexerError(e));\n err.offset = this.current;\n err.token = e.token;\n throw err;\n }\n // We add new states to table[current+1]\n var column = this.table[this.current];\n\n // GC unused states\n if (!this.options.keepHistory) {\n delete this.table[this.current - 1];\n }\n\n var n = this.current + 1;\n var nextColumn = new Column(this.grammar, n);\n this.table.push(nextColumn);\n\n // Advance all tokens that expect the symbol\n var literal = token.text !== undefined ? token.text : token.value;\n var value = lexer.constructor === StreamLexer ? token.value : token;\n var scannable = column.scannable;\n for (var w = scannable.length; w--; ) {\n var state = scannable[w];\n var expect = state.rule.symbols[state.dot];\n // Try to consume the token\n // either regex or literal\n if (expect.test ? expect.test(value) :\n expect.type ? expect.type === token.type\n : expect.literal === literal) {\n // Add it\n var next = state.nextState({data: value, token: token, isToken: true, reference: n - 1});\n nextColumn.states.push(next);\n }\n }\n\n // Next, for each of the rules, we either\n // (a) complete it, and try to see if the reference row expected that\n // rule\n // (b) predict the next nonterminal it expects by adding that\n // nonterminal's start state\n // To prevent duplication, we also keep track of rules we have already\n // added\n\n nextColumn.process();\n\n // If needed, throw an error:\n if (nextColumn.states.length === 0) {\n // No states at all! This is not good.\n var err = new Error(this.reportError(token));\n err.offset = this.current;\n err.token = token;\n throw err;\n }\n\n // maybe save lexer state\n if (this.options.keepHistory) {\n column.lexerState = lexer.save()\n }\n\n this.current++;\n }\n if (column) {\n this.lexerState = lexer.save()\n }\n\n // Incrementally keep track of results\n this.results = this.finish();\n\n // Allow chaining, for whatever it's worth\n return this;\n };\n\n Parser.prototype.reportLexerError = function(lexerError) {\n var tokenDisplay, lexerMessage;\n // Planning to add a token property to moo's thrown error\n // even on erroring tokens to be used in error display below\n var token = lexerError.token;\n if (token) {\n tokenDisplay = \"input \" + JSON.stringify(token.text[0]) + \" (lexer error)\";\n lexerMessage = this.lexer.formatError(token, \"Syntax error\");\n } else {\n tokenDisplay = \"input (lexer error)\";\n lexerMessage = lexerError.message;\n }\n return this.reportErrorCommon(lexerMessage, tokenDisplay);\n };\n\n Parser.prototype.reportError = function(token) {\n var tokenDisplay = (token.type ? token.type + \" token: \" : \"\") + JSON.stringify(token.value !== undefined ? token.value : token);\n var lexerMessage = this.lexer.formatError(token, \"Syntax error\");\n return this.reportErrorCommon(lexerMessage, tokenDisplay);\n };\n\n Parser.prototype.reportErrorCommon = function(lexerMessage, tokenDisplay) {\n var lines = [];\n lines.push(lexerMessage);\n var lastColumnIndex = this.table.length - 2;\n var lastColumn = this.table[lastColumnIndex];\n var expectantStates = lastColumn.states\n .filter(function(state) {\n var nextSymbol = state.rule.symbols[state.dot];\n return nextSymbol && typeof nextSymbol !== \"string\";\n });\n\n if (expectantStates.length === 0) {\n lines.push('Unexpected ' + tokenDisplay + '. I did not expect any more input. Here is the state of my parse table:\\n');\n this.displayStateStack(lastColumn.states, lines);\n } else {\n lines.push('Unexpected ' + tokenDisplay + '. Instead, I was expecting to see one of the following:\\n');\n // Display a \"state stack\" for each expectant state\n // - which shows you how this state came to be, step by step.\n // If there is more than one derivation, we only display the first one.\n var stateStacks = expectantStates\n .map(function(state) {\n return this.buildFirstStateStack(state, []) || [state];\n }, this);\n // Display each state that is expecting a terminal symbol next.\n stateStacks.forEach(function(stateStack) {\n var state = stateStack[0];\n var nextSymbol = state.rule.symbols[state.dot];\n var symbolDisplay = this.getSymbolDisplay(nextSymbol);\n lines.push('A ' + symbolDisplay + ' based on:');\n this.displayStateStack(stateStack, lines);\n }, this);\n }\n lines.push(\"\");\n return lines.join(\"\\n\");\n }\n \n Parser.prototype.displayStateStack = function(stateStack, lines) {\n var lastDisplay;\n var sameDisplayCount = 0;\n for (var j = 0; j < stateStack.length; j++) {\n var state = stateStack[j];\n var display = state.rule.toString(state.dot);\n if (display === lastDisplay) {\n sameDisplayCount++;\n } else {\n if (sameDisplayCount > 0) {\n lines.push(' ^ ' + sameDisplayCount + ' more lines identical to this');\n }\n sameDisplayCount = 0;\n lines.push(' ' + display);\n }\n lastDisplay = display;\n }\n };\n\n Parser.prototype.getSymbolDisplay = function(symbol) {\n return getSymbolLongDisplay(symbol);\n };\n\n /*\n Builds a the first state stack. You can think of a state stack as the call stack\n of the recursive-descent parser which the Nearley parse algorithm simulates.\n A state stack is represented as an array of state objects. Within a\n state stack, the first item of the array will be the starting\n state, with each successive item in the array going further back into history.\n\n This function needs to be given a starting state and an empty array representing\n the visited states, and it returns an single state stack.\n\n */\n Parser.prototype.buildFirstStateStack = function(state, visited) {\n if (visited.indexOf(state) !== -1) {\n // Found cycle, return null\n // to eliminate this path from the results, because\n // we don't know how to display it meaningfully\n return null;\n }\n if (state.wantedBy.length === 0) {\n return [state];\n }\n var prevState = state.wantedBy[0];\n var childVisited = [state].concat(visited);\n var childResult = this.buildFirstStateStack(prevState, childVisited);\n if (childResult === null) {\n return null;\n }\n return [state].concat(childResult);\n };\n\n Parser.prototype.save = function() {\n var column = this.table[this.current];\n column.lexerState = this.lexerState;\n return column;\n };\n\n Parser.prototype.restore = function(column) {\n var index = column.index;\n this.current = index;\n this.table[index] = column;\n this.table.splice(index + 1);\n this.lexerState = column.lexerState;\n\n // Incrementally keep track of results\n this.results = this.finish();\n };\n\n // nb. deprecated: use save/restore instead!\n Parser.prototype.rewind = function(index) {\n if (!this.options.keepHistory) {\n throw new Error('set option `keepHistory` to enable rewinding')\n }\n // nb. recall column (table) indicies fall between token indicies.\n // col 0 -- token 0 -- col 1\n this.restore(this.table[index]);\n };\n\n Parser.prototype.finish = function() {\n // Return the possible parsings\n var considerations = [];\n var start = this.grammar.start;\n var column = this.table[this.table.length - 1]\n column.states.forEach(function (t) {\n if (t.rule.name === start\n && t.dot === t.rule.symbols.length\n && t.reference === 0\n && t.data !== Parser.fail) {\n considerations.push(t);\n }\n });\n return considerations.map(function(c) {return c.data; });\n };\n\n function getSymbolLongDisplay(symbol) {\n var type = typeof symbol;\n if (type === \"string\") {\n return symbol;\n } else if (type === \"object\") {\n if (symbol.literal) {\n return JSON.stringify(symbol.literal);\n } else if (symbol instanceof RegExp) {\n return 'character matching ' + symbol;\n } else if (symbol.type) {\n return symbol.type + ' token';\n } else if (symbol.test) {\n return 'token matching ' + String(symbol.test);\n } else {\n throw new Error('Unknown symbol type: ' + symbol);\n }\n }\n }\n\n function getSymbolShortDisplay(symbol) {\n var type = typeof symbol;\n if (type === \"string\") {\n return symbol;\n } else if (type === \"object\") {\n if (symbol.literal) {\n return JSON.stringify(symbol.literal);\n } else if (symbol instanceof RegExp) {\n return symbol.toString();\n } else if (symbol.type) {\n return '%' + symbol.type;\n } else if (symbol.test) {\n return '<' + String(symbol.test) + '>';\n } else {\n throw new Error('Unknown symbol type: ' + symbol);\n }\n }\n }\n\n return {\n Parser: Parser,\n Grammar: Grammar,\n Rule: Rule,\n };\n\n}));\n", "export const defaults = {\n level: 2,\n offset: true,\n types: [],\n seasonIntervals: false,\n seasonUncertainty: false\n}\n", "import { Bitmask } from './bitmask.js'\nimport { defaults } from './defaults.js'\nconst { assign } = Object\n\n\nexport function num(data) {\n return Number(Array.isArray(data) ? data.join('') : data)\n}\n\nexport function join(data) {\n return data.join('')\n}\n\nexport function zero() { return 0 }\n\nexport function nothing() { return null }\n\nexport function pick(...args) {\n return args.length === 1 ?\n data => data[args[0]] :\n data => concat(data, args)\n}\n\nexport function pluck(...args) {\n return data => args.map(i => data[i])\n}\n\nexport function concat(data, idx = data.keys()) {\n return Array.from(idx)\n .reduce((memo, i) => data[i] !== null ? memo.concat(data[i]) : memo, [])\n}\n\nexport function merge(...args) {\n if (typeof args[args.length - 1] === 'object')\n var extra = args.pop()\n\n return data => assign(args.reduce((a, i) => assign(a, data[i]), {}), extra)\n}\n\nexport function interval(level) {\n return data => ({\n values: [data[0], data[2]],\n type: 'Interval',\n level\n })\n}\n\nexport function masked(type = 'unspecified', symbol = 'X', normalize = true) {\n return (data, _, reject) => {\n data = data.join('')\n\n let negative = data.startsWith('-')\n let mask = data.replace(/-/g, '')\n\n if (mask.indexOf(symbol) === -1) return reject\n\n let values = Bitmask.values(mask, 0, normalize)\n\n if (negative) values[0] = -values[0]\n\n return {\n values, [type]: Bitmask.compute(mask)\n }\n }\n}\n\nexport function date(values, level = 0, extra = null) {\n return assign({\n type: 'Date',\n level,\n values: Bitmask.normalize(values.map(Number))\n }, extra)\n}\n\nexport function year(values, level = 1, extra = null) {\n return assign({\n type: 'Year',\n level,\n values: values.map(Number)\n }, extra)\n}\n\nexport function century(value, level = 0) {\n return {\n type: 'Century',\n level,\n values: [value]\n }\n}\n\nexport function decade(value, level = 2) {\n return {\n type: 'Decade',\n level,\n values: [value]\n }\n}\n\nexport function offsetToTimeZone(offset) {\n let sign = offset < 0 ? '-' : '+'\n let abs = Math.abs(offset)\n let h = String(Math.floor(abs / 60)).padStart(2, '0')\n let m = String(abs % 60).padStart(2, '0')\n return `${sign}${h}:${m}`\n}\n\nexport function datetime(data) {\n let offset = data[3]\n let values = Bitmask.normalize(data[0].map(Number)).concat(data[2])\n let timeZone\n\n if (offset == null && !!defaults.offset) {\n if (typeof defaults.offset === 'number') {\n offset = defaults.offset\n } else {\n offset = -1 * new Date(...values).getTimezoneOffset()\n }\n } else if (offset != null) {\n timeZone = offsetToTimeZone(offset)\n }\n\n return {\n level: 0,\n offset,\n timeZone,\n type: 'Date',\n values\n }\n}\n\nexport function season(values, level = 1) {\n return {\n type: 'Season',\n level,\n values: values.map(Number)\n }\n}\n\nexport function list(data) {\n return assign({ values: data[1], level: 2 }, data[0], data[2])\n}\n\nexport function qualified(fn, level = 2) {\n return ([parts], _, reject) => {\n let q = {\n uncertain: new Bitmask(), approximate: new Bitmask()\n }\n\n let values = parts\n .map(([lhs, part, rhs], idx) => {\n for (let ua in lhs) q[ua].qualify(idx * 2)\n for (let ua in rhs) q[ua].qualify(1 + idx * 2)\n return part\n })\n\n return (!q.uncertain.value && !q.approximate.value) ?\n reject : {\n ...fn(values, level),\n uncertain: q.uncertain.value,\n approximate: q.approximate.value\n }\n }\n}\n", "// Generated automatically by nearley, version 2.20.1\n// http://github.com/Hardmath123/nearley\nfunction id(x) { return x[0]; }\n\n import {\n num, zero, nothing, pick, pluck, join, concat, merge, century,\n interval, list, masked, date, datetime, season, qualified, year, decade\n } from './util.js'\n\n import { Bitmask } from './bitmask.js'\n\n const {\n DAY, MONTH, YEAR, YMD, YM, MD, YYXX, YYYX, XXXX\n } = Bitmask\nlet Lexer = undefined;\nlet ParserRules = [\n {\"name\": \"edtf\", \"symbols\": [\"L0\"], \"postprocess\": id},\n {\"name\": \"edtf\", \"symbols\": [\"L1\"], \"postprocess\": id},\n {\"name\": \"edtf\", \"symbols\": [\"L2\"], \"postprocess\": id},\n {\"name\": \"edtf\", \"symbols\": [\"L3\"], \"postprocess\": id},\n {\"name\": \"L0\", \"symbols\": [\"date_time\"], \"postprocess\": id},\n {\"name\": \"L0\", \"symbols\": [\"century\"], \"postprocess\": id},\n {\"name\": \"L0\", \"symbols\": [\"L0i\"], \"postprocess\": id},\n {\"name\": \"L0i\", \"symbols\": [\"date_time\", {\"literal\":\"/\"}, \"date_time\"], \"postprocess\": interval(0)},\n {\"name\": \"century\", \"symbols\": [\"positive_century\"], \"postprocess\": data => century(data[0])},\n {\"name\": \"century$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"century\", \"symbols\": [\"century$string$1\"], \"postprocess\": data => century(0)},\n {\"name\": \"century\", \"symbols\": [{\"literal\":\"-\"}, \"positive_century\"], \"postprocess\": data => century(-data[1])},\n {\"name\": \"positive_century\", \"symbols\": [\"positive_digit\", \"digit\"], \"postprocess\": num},\n {\"name\": \"positive_century\", \"symbols\": [{\"literal\":\"0\"}, \"positive_digit\"], \"postprocess\": num},\n {\"name\": \"date_time\", \"symbols\": [\"date\"], \"postprocess\": id},\n {\"name\": \"date_time\", \"symbols\": [\"datetime\"], \"postprocess\": id},\n {\"name\": \"date\", \"symbols\": [\"year\"], \"postprocess\": data => date(data)},\n {\"name\": \"date\", \"symbols\": [\"year_month\"], \"postprocess\": data => date(data[0])},\n {\"name\": \"date\", \"symbols\": [\"year_month_day\"], \"postprocess\": data => date(data[0])},\n {\"name\": \"year\", \"symbols\": [\"positive_year\"], \"postprocess\": id},\n {\"name\": \"year\", \"symbols\": [\"negative_year\"], \"postprocess\": id},\n {\"name\": \"year$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}, {\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"year\", \"symbols\": [\"year$string$1\"], \"postprocess\": join},\n {\"name\": \"positive_year\", \"symbols\": [\"positive_digit\", \"digit\", \"digit\", \"digit\"], \"postprocess\": join},\n {\"name\": \"positive_year\", \"symbols\": [{\"literal\":\"0\"}, \"positive_digit\", \"digit\", \"digit\"], \"postprocess\": join},\n {\"name\": \"positive_year$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"positive_year\", \"symbols\": [\"positive_year$string$1\", \"positive_digit\", \"digit\"], \"postprocess\": join},\n {\"name\": \"positive_year$string$2\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"positive_year\", \"symbols\": [\"positive_year$string$2\", \"positive_digit\"], \"postprocess\": join},\n {\"name\": \"negative_year\", \"symbols\": [{\"literal\":\"-\"}, \"positive_year\"], \"postprocess\": join},\n {\"name\": \"year_month\", \"symbols\": [\"year\", {\"literal\":\"-\"}, \"month\"], \"postprocess\": pick(0, 2)},\n {\"name\": \"year_month_day\", \"symbols\": [\"year\", {\"literal\":\"-\"}, \"month_day\"], \"postprocess\": pick(0, 2)},\n {\"name\": \"month\", \"symbols\": [\"d01_12\"], \"postprocess\": id},\n {\"name\": \"month_day\", \"symbols\": [\"m31\", {\"literal\":\"-\"}, \"day\"], \"postprocess\": pick(0, 2)},\n {\"name\": \"month_day\", \"symbols\": [\"m30\", {\"literal\":\"-\"}, \"d01_30\"], \"postprocess\": pick(0, 2)},\n {\"name\": \"month_day$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"2\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"month_day\", \"symbols\": [\"month_day$string$1\", {\"literal\":\"-\"}, \"d01_29\"], \"postprocess\": pick(0, 2)},\n {\"name\": \"day\", \"symbols\": [\"d01_31\"], \"postprocess\": id},\n {\"name\": \"datetime$ebnf$1$subexpression$1\", \"symbols\": [\"timezone\"], \"postprocess\": id},\n {\"name\": \"datetime$ebnf$1\", \"symbols\": [\"datetime$ebnf$1$subexpression$1\"], \"postprocess\": id},\n {\"name\": \"datetime$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"datetime\", \"symbols\": [\"year_month_day\", {\"literal\":\"T\"}, \"time\", \"datetime$ebnf$1\"], \"postprocess\": datetime},\n {\"name\": \"time\", \"symbols\": [\"hours\", {\"literal\":\":\"}, \"minutes\", {\"literal\":\":\"}, \"seconds\", \"milliseconds\"], \"postprocess\": pick(0, 2, 4, 5)},\n {\"name\": \"time\", \"symbols\": [\"hours\", {\"literal\":\":\"}, \"minutes\"], \"postprocess\": pick(0, 2)},\n {\"name\": \"time$string$1\", \"symbols\": [{\"literal\":\"2\"}, {\"literal\":\"4\"}, {\"literal\":\":\"}, {\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"time$ebnf$1$string$1\", \"symbols\": [{\"literal\":\":\"}, {\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"time$ebnf$1\", \"symbols\": [\"time$ebnf$1$string$1\"], \"postprocess\": id},\n {\"name\": \"time$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"time\", \"symbols\": [\"time$string$1\", \"time$ebnf$1\"], \"postprocess\": () => [24, 0, 0]},\n {\"name\": \"hours\", \"symbols\": [\"d00_23\"], \"postprocess\": num},\n {\"name\": \"minutes\", \"symbols\": [\"d00_59\"], \"postprocess\": num},\n {\"name\": \"seconds\", \"symbols\": [\"d00_59\"], \"postprocess\": num},\n {\"name\": \"milliseconds\", \"symbols\": []},\n {\"name\": \"milliseconds\", \"symbols\": [{\"literal\":\".\"}, \"d3s\"], \"postprocess\": data => num(data.slice(1))},\n {\"name\": \"timezone\", \"symbols\": [{\"literal\":\"Z\"}], \"postprocess\": zero},\n {\"name\": \"timezone$subexpression$1\", \"symbols\": [{\"literal\":\"-\"}]},\n {\"name\": \"timezone$subexpression$1\", \"symbols\": [{\"literal\":\"\u2212\"}]},\n {\"name\": \"timezone\", \"symbols\": [\"timezone$subexpression$1\", \"offset\"], \"postprocess\": data => -data[1]},\n {\"name\": \"timezone\", \"symbols\": [{\"literal\":\"+\"}, \"positive_offset\"], \"postprocess\": pick(1)},\n {\"name\": \"positive_offset\", \"symbols\": [\"offset\"], \"postprocess\": id},\n {\"name\": \"positive_offset$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"positive_offset$ebnf$1\", \"symbols\": [{\"literal\":\":\"}], \"postprocess\": id},\n {\"name\": \"positive_offset$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"positive_offset$string$2\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"positive_offset\", \"symbols\": [\"positive_offset$string$1\", \"positive_offset$ebnf$1\", \"positive_offset$string$2\"], \"postprocess\": zero},\n {\"name\": \"positive_offset$subexpression$1$string$1\", \"symbols\": [{\"literal\":\"1\"}, {\"literal\":\"2\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"positive_offset$subexpression$1\", \"symbols\": [\"positive_offset$subexpression$1$string$1\"]},\n {\"name\": \"positive_offset$subexpression$1$string$2\", \"symbols\": [{\"literal\":\"1\"}, {\"literal\":\"3\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"positive_offset$subexpression$1\", \"symbols\": [\"positive_offset$subexpression$1$string$2\"]},\n {\"name\": \"positive_offset$ebnf$2\", \"symbols\": [{\"literal\":\":\"}], \"postprocess\": id},\n {\"name\": \"positive_offset$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"positive_offset\", \"symbols\": [\"positive_offset$subexpression$1\", \"positive_offset$ebnf$2\", \"minutes\"], \"postprocess\": data => num(data[0]) * 60 + data[2]},\n {\"name\": \"positive_offset$string$3\", \"symbols\": [{\"literal\":\"1\"}, {\"literal\":\"4\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"positive_offset$ebnf$3\", \"symbols\": [{\"literal\":\":\"}], \"postprocess\": id},\n {\"name\": \"positive_offset$ebnf$3\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"positive_offset$string$4\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"positive_offset\", \"symbols\": [\"positive_offset$string$3\", \"positive_offset$ebnf$3\", \"positive_offset$string$4\"], \"postprocess\": () => 840},\n {\"name\": \"positive_offset\", \"symbols\": [\"d00_14\"], \"postprocess\": data => num(data[0]) * 60},\n {\"name\": \"offset$ebnf$1\", \"symbols\": [{\"literal\":\":\"}], \"postprocess\": id},\n {\"name\": \"offset$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"offset\", \"symbols\": [\"d01_11\", \"offset$ebnf$1\", \"minutes\"], \"postprocess\": data => num(data[0]) * 60 + data[2]},\n {\"name\": \"offset$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"offset$ebnf$2\", \"symbols\": [{\"literal\":\":\"}], \"postprocess\": id},\n {\"name\": \"offset$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"offset\", \"symbols\": [\"offset$string$1\", \"offset$ebnf$2\", \"d01_59\"], \"postprocess\": data => num(data[2])},\n {\"name\": \"offset$string$2\", \"symbols\": [{\"literal\":\"1\"}, {\"literal\":\"2\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"offset$ebnf$3\", \"symbols\": [{\"literal\":\":\"}], \"postprocess\": id},\n {\"name\": \"offset$ebnf$3\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"offset$string$3\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"offset\", \"symbols\": [\"offset$string$2\", \"offset$ebnf$3\", \"offset$string$3\"], \"postprocess\": () => 720},\n {\"name\": \"offset\", \"symbols\": [\"d01_12\"], \"postprocess\": data => num(data[0]) * 60},\n {\"name\": \"L1\", \"symbols\": [\"L1d\"], \"postprocess\": id},\n {\"name\": \"L1\", \"symbols\": [\"L1Y\"], \"postprocess\": id},\n {\"name\": \"L1\", \"symbols\": [\"L1S\"], \"postprocess\": id},\n {\"name\": \"L1\", \"symbols\": [\"L1i\"], \"postprocess\": id},\n {\"name\": \"L1d\", \"symbols\": [\"date_ua\"], \"postprocess\": id},\n {\"name\": \"L1d\", \"symbols\": [\"L1X\"], \"postprocess\": merge(0, { type: 'Date', level: 1 })},\n {\"name\": \"date_ua\", \"symbols\": [\"date\", \"UA\"], \"postprocess\": merge(0, 1, { level: 1 })},\n {\"name\": \"L1i\", \"symbols\": [\"L1i_date\", {\"literal\":\"/\"}, \"L1i_date\"], \"postprocess\": interval(1)},\n {\"name\": \"L1i\", \"symbols\": [\"date_time\", {\"literal\":\"/\"}, \"L1i_date\"], \"postprocess\": interval(1)},\n {\"name\": \"L1i\", \"symbols\": [\"L1i_date\", {\"literal\":\"/\"}, \"date_time\"], \"postprocess\": interval(1)},\n {\"name\": \"L1i_date\", \"symbols\": [], \"postprocess\": nothing},\n {\"name\": \"L1i_date\", \"symbols\": [\"date_ua\"], \"postprocess\": id},\n {\"name\": \"L1i_date\", \"symbols\": [\"INFINITY\"], \"postprocess\": id},\n {\"name\": \"INFINITY$string$1\", \"symbols\": [{\"literal\":\".\"}, {\"literal\":\".\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"INFINITY\", \"symbols\": [\"INFINITY$string$1\"], \"postprocess\": () => Infinity},\n {\"name\": \"L1X$string$1\", \"symbols\": [{\"literal\":\"-\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"L1X\", \"symbols\": [\"nd4\", {\"literal\":\"-\"}, \"md\", \"L1X$string$1\"], \"postprocess\": masked()},\n {\"name\": \"L1X$string$2\", \"symbols\": [{\"literal\":\"-\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}, {\"literal\":\"-\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"L1X\", \"symbols\": [\"nd4\", \"L1X$string$2\"], \"postprocess\": masked()},\n {\"name\": \"L1X$string$3\", \"symbols\": [{\"literal\":\"X\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}, {\"literal\":\"-\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}, {\"literal\":\"-\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"L1X\", \"symbols\": [\"L1X$string$3\"], \"postprocess\": masked()},\n {\"name\": \"L1X$string$4\", \"symbols\": [{\"literal\":\"-\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"L1X\", \"symbols\": [\"nd4\", \"L1X$string$4\"], \"postprocess\": masked()},\n {\"name\": \"L1X$string$5\", \"symbols\": [{\"literal\":\"X\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}, {\"literal\":\"-\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"L1X\", \"symbols\": [\"L1X$string$5\"], \"postprocess\": masked()},\n {\"name\": \"L1X$string$6\", \"symbols\": [{\"literal\":\"X\"}, {\"literal\":\"X\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"L1X\", \"symbols\": [\"nd2\", \"L1X$string$6\"], \"postprocess\": masked()},\n {\"name\": \"L1X\", \"symbols\": [\"nd3\", {\"literal\":\"X\"}], \"postprocess\": masked()},\n {\"name\": \"L1X$string$7\", \"symbols\": [{\"literal\":\"X\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}, {\"literal\":\"X\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"L1X\", \"symbols\": [\"L1X$string$7\"], \"postprocess\": masked()},\n {\"name\": \"L1Y\", \"symbols\": [{\"literal\":\"Y\"}, \"d5+\"], \"postprocess\": data => year([num(data[1])], 1)},\n {\"name\": \"L1Y$string$1\", \"symbols\": [{\"literal\":\"Y\"}, {\"literal\":\"-\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"L1Y\", \"symbols\": [\"L1Y$string$1\", \"d5+\"], \"postprocess\": data => year([-num(data[1])], 1)},\n {\"name\": \"UA\", \"symbols\": [{\"literal\":\"?\"}], \"postprocess\": () => ({ uncertain: true })},\n {\"name\": \"UA\", \"symbols\": [{\"literal\":\"~\"}], \"postprocess\": () => ({ approximate: true })},\n {\"name\": \"UA\", \"symbols\": [{\"literal\":\"%\"}], \"postprocess\": () => ({ approximate: true, uncertain: true })},\n {\"name\": \"L1S\", \"symbols\": [\"year\", {\"literal\":\"-\"}, \"d21_24\"], \"postprocess\": d => season([d[0], d[2]], 1)},\n {\"name\": \"L2\", \"symbols\": [\"L2d\"], \"postprocess\": id},\n {\"name\": \"L2\", \"symbols\": [\"L2Y\"], \"postprocess\": id},\n {\"name\": \"L2\", \"symbols\": [\"L2S\"], \"postprocess\": id},\n {\"name\": \"L2\", \"symbols\": [\"L2D\"], \"postprocess\": id},\n {\"name\": \"L2\", \"symbols\": [\"L2C\"], \"postprocess\": id},\n {\"name\": \"L2\", \"symbols\": [\"L2i\"], \"postprocess\": id},\n {\"name\": \"L2\", \"symbols\": [\"set\"], \"postprocess\": id},\n {\"name\": \"L2\", \"symbols\": [\"list\"], \"postprocess\": id},\n {\"name\": \"L2d\", \"symbols\": [\"ua_date\"], \"postprocess\": id},\n {\"name\": \"L2d\", \"symbols\": [\"L2X\"], \"postprocess\": merge(0, { type: 'Date', level: 2 })},\n {\"name\": \"L2D\", \"symbols\": [\"decade\"], \"postprocess\": id},\n {\"name\": \"L2D\", \"symbols\": [\"decade\", \"UA\"], \"postprocess\": merge(0, 1)},\n {\"name\": \"L2C\", \"symbols\": [\"century\"], \"postprocess\": id},\n {\"name\": \"L2C\", \"symbols\": [\"century\", \"UA\"], \"postprocess\": merge(0, 1, {level: 2})},\n {\"name\": \"ua_date\", \"symbols\": [\"ua_year\"], \"postprocess\": qualified(date)},\n {\"name\": \"ua_date\", \"symbols\": [\"ua_year_month\"], \"postprocess\": qualified(date)},\n {\"name\": \"ua_date\", \"symbols\": [\"ua_year_month_day\"], \"postprocess\": qualified(date)},\n {\"name\": \"ua_year\", \"symbols\": [\"UA\", \"year\"], \"postprocess\": data => [data]},\n {\"name\": \"ua_year_month$macrocall$2\", \"symbols\": [\"year\"]},\n {\"name\": \"ua_year_month$macrocall$1$ebnf$1\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_year_month$macrocall$1$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_year_month$macrocall$1$ebnf$2\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_year_month$macrocall$1$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_year_month$macrocall$1\", \"symbols\": [\"ua_year_month$macrocall$1$ebnf$1\", \"ua_year_month$macrocall$2\", \"ua_year_month$macrocall$1$ebnf$2\"]},\n {\"name\": \"ua_year_month$macrocall$4\", \"symbols\": [\"month\"]},\n {\"name\": \"ua_year_month$macrocall$3$ebnf$1\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_year_month$macrocall$3$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_year_month$macrocall$3$ebnf$2\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_year_month$macrocall$3$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_year_month$macrocall$3\", \"symbols\": [\"ua_year_month$macrocall$3$ebnf$1\", \"ua_year_month$macrocall$4\", \"ua_year_month$macrocall$3$ebnf$2\"]},\n {\"name\": \"ua_year_month\", \"symbols\": [\"ua_year_month$macrocall$1\", {\"literal\":\"-\"}, \"ua_year_month$macrocall$3\"], \"postprocess\": pluck(0, 2)},\n {\"name\": \"ua_year_month_day$macrocall$2\", \"symbols\": [\"year\"]},\n {\"name\": \"ua_year_month_day$macrocall$1$ebnf$1\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_year_month_day$macrocall$1$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_year_month_day$macrocall$1$ebnf$2\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_year_month_day$macrocall$1$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_year_month_day$macrocall$1\", \"symbols\": [\"ua_year_month_day$macrocall$1$ebnf$1\", \"ua_year_month_day$macrocall$2\", \"ua_year_month_day$macrocall$1$ebnf$2\"]},\n {\"name\": \"ua_year_month_day\", \"symbols\": [\"ua_year_month_day$macrocall$1\", {\"literal\":\"-\"}, \"ua_month_day\"], \"postprocess\": data => [data[0], ...data[2]]},\n {\"name\": \"ua_month_day$macrocall$2\", \"symbols\": [\"m31\"]},\n {\"name\": \"ua_month_day$macrocall$1$ebnf$1\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_month_day$macrocall$1$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_month_day$macrocall$1$ebnf$2\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_month_day$macrocall$1$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_month_day$macrocall$1\", \"symbols\": [\"ua_month_day$macrocall$1$ebnf$1\", \"ua_month_day$macrocall$2\", \"ua_month_day$macrocall$1$ebnf$2\"]},\n {\"name\": \"ua_month_day$macrocall$4\", \"symbols\": [\"day\"]},\n {\"name\": \"ua_month_day$macrocall$3$ebnf$1\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_month_day$macrocall$3$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_month_day$macrocall$3$ebnf$2\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_month_day$macrocall$3$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_month_day$macrocall$3\", \"symbols\": [\"ua_month_day$macrocall$3$ebnf$1\", \"ua_month_day$macrocall$4\", \"ua_month_day$macrocall$3$ebnf$2\"]},\n {\"name\": \"ua_month_day\", \"symbols\": [\"ua_month_day$macrocall$1\", {\"literal\":\"-\"}, \"ua_month_day$macrocall$3\"], \"postprocess\": pluck(0, 2)},\n {\"name\": \"ua_month_day$macrocall$6\", \"symbols\": [\"m30\"]},\n {\"name\": \"ua_month_day$macrocall$5$ebnf$1\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_month_day$macrocall$5$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_month_day$macrocall$5$ebnf$2\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_month_day$macrocall$5$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_month_day$macrocall$5\", \"symbols\": [\"ua_month_day$macrocall$5$ebnf$1\", \"ua_month_day$macrocall$6\", \"ua_month_day$macrocall$5$ebnf$2\"]},\n {\"name\": \"ua_month_day$macrocall$8\", \"symbols\": [\"d01_30\"]},\n {\"name\": \"ua_month_day$macrocall$7$ebnf$1\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_month_day$macrocall$7$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_month_day$macrocall$7$ebnf$2\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_month_day$macrocall$7$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_month_day$macrocall$7\", \"symbols\": [\"ua_month_day$macrocall$7$ebnf$1\", \"ua_month_day$macrocall$8\", \"ua_month_day$macrocall$7$ebnf$2\"]},\n {\"name\": \"ua_month_day\", \"symbols\": [\"ua_month_day$macrocall$5\", {\"literal\":\"-\"}, \"ua_month_day$macrocall$7\"], \"postprocess\": pluck(0, 2)},\n {\"name\": \"ua_month_day$macrocall$10$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"2\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"ua_month_day$macrocall$10\", \"symbols\": [\"ua_month_day$macrocall$10$string$1\"]},\n {\"name\": \"ua_month_day$macrocall$9$ebnf$1\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_month_day$macrocall$9$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_month_day$macrocall$9$ebnf$2\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_month_day$macrocall$9$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_month_day$macrocall$9\", \"symbols\": [\"ua_month_day$macrocall$9$ebnf$1\", \"ua_month_day$macrocall$10\", \"ua_month_day$macrocall$9$ebnf$2\"]},\n {\"name\": \"ua_month_day$macrocall$12\", \"symbols\": [\"d01_29\"]},\n {\"name\": \"ua_month_day$macrocall$11$ebnf$1\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_month_day$macrocall$11$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_month_day$macrocall$11$ebnf$2\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_month_day$macrocall$11$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_month_day$macrocall$11\", \"symbols\": [\"ua_month_day$macrocall$11$ebnf$1\", \"ua_month_day$macrocall$12\", \"ua_month_day$macrocall$11$ebnf$2\"]},\n {\"name\": \"ua_month_day\", \"symbols\": [\"ua_month_day$macrocall$9\", {\"literal\":\"-\"}, \"ua_month_day$macrocall$11\"], \"postprocess\": pluck(0, 2)},\n {\"name\": \"L2X\", \"symbols\": [\"dx4\"], \"postprocess\": masked()},\n {\"name\": \"L2X\", \"symbols\": [\"dx4\", {\"literal\":\"-\"}, \"mx\"], \"postprocess\": masked()},\n {\"name\": \"L2X\", \"symbols\": [\"dx4\", {\"literal\":\"-\"}, \"mdx\"], \"postprocess\": masked()},\n {\"name\": \"mdx\", \"symbols\": [\"m31x\", {\"literal\":\"-\"}, \"d31x\"], \"postprocess\": join},\n {\"name\": \"mdx\", \"symbols\": [\"m30x\", {\"literal\":\"-\"}, \"d30x\"], \"postprocess\": join},\n {\"name\": \"mdx$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"2\"}, {\"literal\":\"-\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"mdx\", \"symbols\": [\"mdx$string$1\", \"d29x\"], \"postprocess\": join},\n {\"name\": \"L2i\", \"symbols\": [\"L2i_date\", {\"literal\":\"/\"}, \"L2i_date\"], \"postprocess\": interval(2)},\n {\"name\": \"L2i\", \"symbols\": [\"date_time\", {\"literal\":\"/\"}, \"L2i_date\"], \"postprocess\": interval(2)},\n {\"name\": \"L2i\", \"symbols\": [\"L2i_date\", {\"literal\":\"/\"}, \"date_time\"], \"postprocess\": interval(2)},\n {\"name\": \"L2i_date\", \"symbols\": [], \"postprocess\": nothing},\n {\"name\": \"L2i_date\", \"symbols\": [\"ua_date\"], \"postprocess\": id},\n {\"name\": \"L2i_date\", \"symbols\": [\"L2X\"], \"postprocess\": id},\n {\"name\": \"L2i_date\", \"symbols\": [\"INFINITY\"], \"postprocess\": id},\n {\"name\": \"L2Y\", \"symbols\": [\"exp_year\"], \"postprocess\": id},\n {\"name\": \"L2Y\", \"symbols\": [\"exp_year\", \"significant_digits\"], \"postprocess\": merge(0, 1)},\n {\"name\": \"L2Y\", \"symbols\": [\"L1Y\", \"significant_digits\"], \"postprocess\": merge(0, 1, { level: 2 })},\n {\"name\": \"L2Y\", \"symbols\": [\"year\", \"significant_digits\"], \"postprocess\": data => year([data[0]], 2, data[1])},\n {\"name\": \"significant_digits\", \"symbols\": [{\"literal\":\"S\"}, \"positive_digit\"], \"postprocess\": data => ({ significant: num(data[1]) })},\n {\"name\": \"exp_year\", \"symbols\": [{\"literal\":\"Y\"}, \"exp\"], \"postprocess\": data => year([data[1]], 2)},\n {\"name\": \"exp_year$string$1\", \"symbols\": [{\"literal\":\"Y\"}, {\"literal\":\"-\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"exp_year\", \"symbols\": [\"exp_year$string$1\", \"exp\"], \"postprocess\": data => year([-data[1]], 2)},\n {\"name\": \"exp\", \"symbols\": [\"digits\", {\"literal\":\"E\"}, \"digits\"], \"postprocess\": data => num(data[0]) * Math.pow(10, num(data[2]))},\n {\"name\": \"L2S\", \"symbols\": [\"year\", {\"literal\":\"-\"}, \"d25_41\"], \"postprocess\": d => season([d[0], d[2]], 2)},\n {\"name\": \"decade\", \"symbols\": [\"positive_decade\"], \"postprocess\": data => decade(data[0])},\n {\"name\": \"decade$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"decade\", \"symbols\": [\"decade$string$1\"], \"postprocess\": () => decade(0)},\n {\"name\": \"decade\", \"symbols\": [{\"literal\":\"-\"}, \"positive_decade\"], \"postprocess\": data => decade(-data[1])},\n {\"name\": \"positive_decade\", \"symbols\": [\"positive_digit\", \"digit\", \"digit\"], \"postprocess\": num},\n {\"name\": \"positive_decade\", \"symbols\": [{\"literal\":\"0\"}, \"positive_digit\", \"digit\"], \"postprocess\": num},\n {\"name\": \"positive_decade$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"positive_decade\", \"symbols\": [\"positive_decade$string$1\", \"positive_digit\"], \"postprocess\": num},\n {\"name\": \"set\", \"symbols\": [\"LSB\", \"OL\", \"RSB\"], \"postprocess\": list},\n {\"name\": \"list\", \"symbols\": [\"LLB\", \"OL\", \"RLB\"], \"postprocess\": list},\n {\"name\": \"LSB\", \"symbols\": [{\"literal\":\"[\"}], \"postprocess\": () => ({ type: 'Set' })},\n {\"name\": \"LSB$string$1\", \"symbols\": [{\"literal\":\"[\"}, {\"literal\":\".\"}, {\"literal\":\".\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"LSB\", \"symbols\": [\"LSB$string$1\"], \"postprocess\": () => ({ type: 'Set', earlier: true })},\n {\"name\": \"LLB\", \"symbols\": [{\"literal\":\"{\"}], \"postprocess\": () => ({ type: 'List' })},\n {\"name\": \"LLB$string$1\", \"symbols\": [{\"literal\":\"{\"}, {\"literal\":\".\"}, {\"literal\":\".\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"LLB\", \"symbols\": [\"LLB$string$1\"], \"postprocess\": () => ({ type: 'List', earlier: true })},\n {\"name\": \"RSB\", \"symbols\": [{\"literal\":\"]\"}], \"postprocess\": nothing},\n {\"name\": \"RSB$string$1\", \"symbols\": [{\"literal\":\".\"}, {\"literal\":\".\"}, {\"literal\":\"]\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"RSB\", \"symbols\": [\"RSB$string$1\"], \"postprocess\": () => ({ later: true })},\n {\"name\": \"RLB\", \"symbols\": [{\"literal\":\"}\"}], \"postprocess\": nothing},\n {\"name\": \"RLB$string$1\", \"symbols\": [{\"literal\":\".\"}, {\"literal\":\".\"}, {\"literal\":\"}\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"RLB\", \"symbols\": [\"RLB$string$1\"], \"postprocess\": () => ({ later: true })},\n {\"name\": \"OL\", \"symbols\": [\"LI\"], \"postprocess\": data => [data[0]]},\n {\"name\": \"OL\", \"symbols\": [\"OL\", \"_\", {\"literal\":\",\"}, \"_\", \"LI\"], \"postprocess\": data => [...data[0], data[4]]},\n {\"name\": \"LI\", \"symbols\": [\"date\"], \"postprocess\": id},\n {\"name\": \"LI\", \"symbols\": [\"ua_date\"], \"postprocess\": id},\n {\"name\": \"LI\", \"symbols\": [\"L2X\"], \"postprocess\": id},\n {\"name\": \"LI\", \"symbols\": [\"consecutives\"], \"postprocess\": id},\n {\"name\": \"consecutives$string$1\", \"symbols\": [{\"literal\":\".\"}, {\"literal\":\".\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"consecutives\", \"symbols\": [\"year_month_day\", \"consecutives$string$1\", \"year_month_day\"], \"postprocess\": d => [date(d[0]), date(d[2])]},\n {\"name\": \"consecutives$string$2\", \"symbols\": [{\"literal\":\".\"}, {\"literal\":\".\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"consecutives\", \"symbols\": [\"year_month\", \"consecutives$string$2\", \"year_month\"], \"postprocess\": d => [date(d[0]), date(d[2])]},\n {\"name\": \"consecutives$string$3\", \"symbols\": [{\"literal\":\".\"}, {\"literal\":\".\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"consecutives\", \"symbols\": [\"year\", \"consecutives$string$3\", \"year\"], \"postprocess\": d => [date([d[0]]), date([d[2]])]},\n {\"name\": \"L3\", \"symbols\": [\"L3i\"], \"postprocess\": id},\n {\"name\": \"L3\", \"symbols\": [\"L3S\"], \"postprocess\": id},\n {\"name\": \"L3i\", \"symbols\": [\"L3s\", {\"literal\":\"/\"}, \"L3s\"], \"postprocess\": interval(3)},\n {\"name\": \"L3s\", \"symbols\": [\"L1S\"], \"postprocess\": id},\n {\"name\": \"L3s\", \"symbols\": [\"L2S\"], \"postprocess\": id},\n {\"name\": \"L3s\", \"symbols\": [\"L3S\"], \"postprocess\": id},\n {\"name\": \"L3S\", \"symbols\": [\"ua_season\"], \"postprocess\": qualified(season, 3)},\n {\"name\": \"L3S\", \"symbols\": [\"xx_season\"], \"postprocess\": merge(0, { type: 'Season', level: 3 })},\n {\"name\": \"ua_season$macrocall$2\", \"symbols\": [\"year\"]},\n {\"name\": \"ua_season$macrocall$1$ebnf$1\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_season$macrocall$1$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_season$macrocall$1$ebnf$2\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_season$macrocall$1$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_season$macrocall$1\", \"symbols\": [\"ua_season$macrocall$1$ebnf$1\", \"ua_season$macrocall$2\", \"ua_season$macrocall$1$ebnf$2\"]},\n {\"name\": \"ua_season$macrocall$4\", \"symbols\": [\"d21_41\"]},\n {\"name\": \"ua_season$macrocall$3$ebnf$1\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_season$macrocall$3$ebnf$1\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_season$macrocall$3$ebnf$2\", \"symbols\": [\"UA\"], \"postprocess\": id},\n {\"name\": \"ua_season$macrocall$3$ebnf$2\", \"symbols\": [], \"postprocess\": function(d) {return null;}},\n {\"name\": \"ua_season$macrocall$3\", \"symbols\": [\"ua_season$macrocall$3$ebnf$1\", \"ua_season$macrocall$4\", \"ua_season$macrocall$3$ebnf$2\"]},\n {\"name\": \"ua_season\", \"symbols\": [\"ua_season$macrocall$1\", {\"literal\":\"-\"}, \"ua_season$macrocall$3\"], \"postprocess\": pluck(0, 2)},\n {\"name\": \"xx_season\", \"symbols\": [\"dx4\", {\"literal\":\"-\"}, \"d21_41\"], \"postprocess\": masked('unspecified', 'X', false)},\n {\"name\": \"digit\", \"symbols\": [\"positive_digit\"], \"postprocess\": id},\n {\"name\": \"digit\", \"symbols\": [{\"literal\":\"0\"}], \"postprocess\": id},\n {\"name\": \"digits\", \"symbols\": [\"digit\"], \"postprocess\": id},\n {\"name\": \"digits\", \"symbols\": [\"digits\", \"digit\"], \"postprocess\": join},\n {\"name\": \"nd4\", \"symbols\": [\"d4\"]},\n {\"name\": \"nd4\", \"symbols\": [{\"literal\":\"-\"}, \"d4\"], \"postprocess\": join},\n {\"name\": \"nd3\", \"symbols\": [\"d3\"]},\n {\"name\": \"nd3\", \"symbols\": [{\"literal\":\"-\"}, \"d3\"], \"postprocess\": join},\n {\"name\": \"nd2\", \"symbols\": [\"d2\"]},\n {\"name\": \"nd2\", \"symbols\": [{\"literal\":\"-\"}, \"d2\"], \"postprocess\": join},\n {\"name\": \"d4\", \"symbols\": [\"d2\", \"d2\"], \"postprocess\": join},\n {\"name\": \"d3\", \"symbols\": [\"d2\", \"digit\"], \"postprocess\": join},\n {\"name\": \"d2\", \"symbols\": [\"digit\", \"digit\"], \"postprocess\": join},\n {\"name\": \"d3s\", \"symbols\": [\"digit\"], \"postprocess\": id},\n {\"name\": \"d3s\", \"symbols\": [\"d2\"], \"postprocess\": id},\n {\"name\": \"d3s\", \"symbols\": [\"d3\"], \"postprocess\": id},\n {\"name\": \"d3s\", \"symbols\": [\"d3\", \"digits\"], \"postprocess\": pick(0)},\n {\"name\": \"d5+\", \"symbols\": [\"positive_digit\", \"d3\", \"digits\"], \"postprocess\": num},\n {\"name\": \"d1x\", \"symbols\": [/[1-9X]/], \"postprocess\": id},\n {\"name\": \"dx\", \"symbols\": [\"d1x\"], \"postprocess\": id},\n {\"name\": \"dx\", \"symbols\": [{\"literal\":\"0\"}], \"postprocess\": id},\n {\"name\": \"dx2\", \"symbols\": [\"dx\", \"dx\"], \"postprocess\": join},\n {\"name\": \"dx4\", \"symbols\": [\"dx2\", \"dx2\"], \"postprocess\": join},\n {\"name\": \"dx4\", \"symbols\": [{\"literal\":\"-\"}, \"dx2\", \"dx2\"], \"postprocess\": join},\n {\"name\": \"md\", \"symbols\": [\"m31\"], \"postprocess\": id},\n {\"name\": \"md\", \"symbols\": [\"m30\"], \"postprocess\": id},\n {\"name\": \"md$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"2\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"md\", \"symbols\": [\"md$string$1\"], \"postprocess\": id},\n {\"name\": \"mx\", \"symbols\": [{\"literal\":\"0\"}, \"d1x\"], \"postprocess\": join},\n {\"name\": \"mx\", \"symbols\": [/[1X]/, /[012X]/], \"postprocess\": join},\n {\"name\": \"m31x\", \"symbols\": [/[0X]/, /[13578X]/], \"postprocess\": join},\n {\"name\": \"m31x\", \"symbols\": [/[1X]/, /[02]/], \"postprocess\": join},\n {\"name\": \"m31x$string$1\", \"symbols\": [{\"literal\":\"1\"}, {\"literal\":\"X\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m31x\", \"symbols\": [\"m31x$string$1\"], \"postprocess\": id},\n {\"name\": \"m30x\", \"symbols\": [/[0X]/, /[469]/], \"postprocess\": join},\n {\"name\": \"m30x$string$1\", \"symbols\": [{\"literal\":\"1\"}, {\"literal\":\"1\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m30x\", \"symbols\": [\"m30x$string$1\"], \"postprocess\": join},\n {\"name\": \"d29x\", \"symbols\": [{\"literal\":\"0\"}, \"d1x\"], \"postprocess\": join},\n {\"name\": \"d29x\", \"symbols\": [/[1-2X]/, \"dx\"], \"postprocess\": join},\n {\"name\": \"d30x\", \"symbols\": [\"d29x\"], \"postprocess\": join},\n {\"name\": \"d30x$string$1\", \"symbols\": [{\"literal\":\"3\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"d30x\", \"symbols\": [\"d30x$string$1\"], \"postprocess\": id},\n {\"name\": \"d31x\", \"symbols\": [\"d30x\"], \"postprocess\": id},\n {\"name\": \"d31x\", \"symbols\": [{\"literal\":\"3\"}, /[1X]/], \"postprocess\": join},\n {\"name\": \"positive_digit\", \"symbols\": [/[1-9]/], \"postprocess\": id},\n {\"name\": \"m31$subexpression$1$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"1\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m31$subexpression$1\", \"symbols\": [\"m31$subexpression$1$string$1\"]},\n {\"name\": \"m31$subexpression$1$string$2\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"3\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m31$subexpression$1\", \"symbols\": [\"m31$subexpression$1$string$2\"]},\n {\"name\": \"m31$subexpression$1$string$3\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"5\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m31$subexpression$1\", \"symbols\": [\"m31$subexpression$1$string$3\"]},\n {\"name\": \"m31$subexpression$1$string$4\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"7\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m31$subexpression$1\", \"symbols\": [\"m31$subexpression$1$string$4\"]},\n {\"name\": \"m31$subexpression$1$string$5\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"8\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m31$subexpression$1\", \"symbols\": [\"m31$subexpression$1$string$5\"]},\n {\"name\": \"m31$subexpression$1$string$6\", \"symbols\": [{\"literal\":\"1\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m31$subexpression$1\", \"symbols\": [\"m31$subexpression$1$string$6\"]},\n {\"name\": \"m31$subexpression$1$string$7\", \"symbols\": [{\"literal\":\"1\"}, {\"literal\":\"2\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m31$subexpression$1\", \"symbols\": [\"m31$subexpression$1$string$7\"]},\n {\"name\": \"m31\", \"symbols\": [\"m31$subexpression$1\"], \"postprocess\": id},\n {\"name\": \"m30$subexpression$1$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"4\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m30$subexpression$1\", \"symbols\": [\"m30$subexpression$1$string$1\"]},\n {\"name\": \"m30$subexpression$1$string$2\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"6\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m30$subexpression$1\", \"symbols\": [\"m30$subexpression$1$string$2\"]},\n {\"name\": \"m30$subexpression$1$string$3\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"9\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m30$subexpression$1\", \"symbols\": [\"m30$subexpression$1$string$3\"]},\n {\"name\": \"m30$subexpression$1$string$4\", \"symbols\": [{\"literal\":\"1\"}, {\"literal\":\"1\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"m30$subexpression$1\", \"symbols\": [\"m30$subexpression$1$string$4\"]},\n {\"name\": \"m30\", \"symbols\": [\"m30$subexpression$1\"], \"postprocess\": id},\n {\"name\": \"d01_11\", \"symbols\": [{\"literal\":\"0\"}, \"positive_digit\"], \"postprocess\": join},\n {\"name\": \"d01_11\", \"symbols\": [{\"literal\":\"1\"}, /[0-1]/], \"postprocess\": join},\n {\"name\": \"d01_12\", \"symbols\": [\"d01_11\"], \"postprocess\": id},\n {\"name\": \"d01_12$string$1\", \"symbols\": [{\"literal\":\"1\"}, {\"literal\":\"2\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"d01_12\", \"symbols\": [\"d01_12$string$1\"], \"postprocess\": id},\n {\"name\": \"d01_13\", \"symbols\": [\"d01_12\"], \"postprocess\": id},\n {\"name\": \"d01_13$string$1\", \"symbols\": [{\"literal\":\"1\"}, {\"literal\":\"3\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"d01_13\", \"symbols\": [\"d01_13$string$1\"], \"postprocess\": id},\n {\"name\": \"d00_14$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"d00_14\", \"symbols\": [\"d00_14$string$1\"], \"postprocess\": id},\n {\"name\": \"d00_14\", \"symbols\": [\"d01_13\"], \"postprocess\": id},\n {\"name\": \"d00_14$string$2\", \"symbols\": [{\"literal\":\"1\"}, {\"literal\":\"4\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"d00_14\", \"symbols\": [\"d00_14$string$2\"], \"postprocess\": id},\n {\"name\": \"d00_23$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"d00_23\", \"symbols\": [\"d00_23$string$1\"], \"postprocess\": id},\n {\"name\": \"d00_23\", \"symbols\": [\"d01_23\"], \"postprocess\": id},\n {\"name\": \"d01_23\", \"symbols\": [{\"literal\":\"0\"}, \"positive_digit\"], \"postprocess\": join},\n {\"name\": \"d01_23\", \"symbols\": [{\"literal\":\"1\"}, \"digit\"], \"postprocess\": join},\n {\"name\": \"d01_23\", \"symbols\": [{\"literal\":\"2\"}, /[0-3]/], \"postprocess\": join},\n {\"name\": \"d01_29\", \"symbols\": [{\"literal\":\"0\"}, \"positive_digit\"], \"postprocess\": join},\n {\"name\": \"d01_29\", \"symbols\": [/[1-2]/, \"digit\"], \"postprocess\": join},\n {\"name\": \"d01_30\", \"symbols\": [\"d01_29\"], \"postprocess\": id},\n {\"name\": \"d01_30$string$1\", \"symbols\": [{\"literal\":\"3\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"d01_30\", \"symbols\": [\"d01_30$string$1\"], \"postprocess\": id},\n {\"name\": \"d01_31\", \"symbols\": [\"d01_30\"], \"postprocess\": id},\n {\"name\": \"d01_31$string$1\", \"symbols\": [{\"literal\":\"3\"}, {\"literal\":\"1\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"d01_31\", \"symbols\": [\"d01_31$string$1\"], \"postprocess\": id},\n {\"name\": \"d00_59$string$1\", \"symbols\": [{\"literal\":\"0\"}, {\"literal\":\"0\"}], \"postprocess\": function joiner(d) {return d.join('');}},\n {\"name\": \"d00_59\", \"symbols\": [\"d00_59$string$1\"], \"postprocess\": id},\n {\"name\": \"d00_59\", \"symbols\": [\"d01_59\"], \"postprocess\": id},\n {\"name\": \"d01_59\", \"symbols\": [\"d01_29\"], \"postprocess\": id},\n {\"name\": \"d01_59\", \"symbols\": [/[345]/, \"digit\"], \"postprocess\": join},\n {\"name\": \"d21_24\", \"symbols\": [{\"literal\":\"2\"}, /[1-4]/], \"postprocess\": join},\n {\"name\": \"d25_41\", \"symbols\": [{\"literal\":\"2\"}, /[5-9]/], \"postprocess\": join},\n {\"name\": \"d25_41\", \"symbols\": [{\"literal\":\"3\"}, \"digit\"], \"postprocess\": join},\n {\"name\": \"d25_41\", \"symbols\": [{\"literal\":\"4\"}, /[01]/], \"postprocess\": join},\n {\"name\": \"d21_41\", \"symbols\": [\"d21_24\"], \"postprocess\": id},\n {\"name\": \"d21_41\", \"symbols\": [\"d25_41\"], \"postprocess\": id},\n {\"name\": \"_$ebnf$1\", \"symbols\": []},\n {\"name\": \"_$ebnf$1\", \"symbols\": [\"_$ebnf$1\", {\"literal\":\" \"}], \"postprocess\": function arrpush(d) {return d[0].concat([d[1]]);}},\n {\"name\": \"_\", \"symbols\": [\"_$ebnf$1\"]}\n];\nlet ParserStart = \"edtf\";\nexport default { Lexer, ParserRules, ParserStart };\n", "import nearley from 'nearley'\nimport grammar from './grammar.js'\nimport { defaults } from './defaults.js'\n\nfunction byLevel(a, b) {\n return a.level < b.level ? -1 : a.level > b.level ? 1 : 0\n}\n\nfunction limit(results, constraints = {}) {\n if (!results.length) return results\n\n let {\n level,\n types,\n seasonIntervals,\n seasonUncertainty\n } = { ...defaults, ...constraints }\n\n\n return results.filter(res => {\n if (seasonIntervals && isSeasonInterval(res))\n return true\n if (seasonUncertainty && isSeasonLevel3(res))\n return true\n\n if (res.level > level)\n return false\n if (types.length && !types.includes(res.type))\n return false\n\n return true\n })\n}\n\nfunction isSeasonInterval({ type, values }) {\n return type === 'Interval' && values[0].type === 'Season'\n}\nfunction isSeasonLevel3({ type, level }) {\n return type === 'Season' && level >= 3\n}\n\nfunction best(results) {\n if (results.length < 2) return results[0]\n\n // If there are multiple results, pick the first\n // one on the lowest level!\n return results.sort(byLevel)[0]\n}\n\nexport function parse(input, constraints = {}) {\n try {\n let nep = parser()\n let res = best(limit(nep.feed(input).results, constraints))\n\n if (!res) throw new Error('edtf: No possible parsings (@EOS)')\n\n return res\n\n } catch (error) {\n error.message += ` for \"${input}\"`\n throw error\n }\n}\n\nexport function parser() {\n return new nearley.Parser(grammar.ParserRules, grammar.ParserStart)\n}\n", "import { parse } from './parser.js'\n\nexport class ExtDateTime {\n\n static get type() {\n return this.name\n }\n\n static parse(input) {\n return parse(input, { types: [this.type] })\n }\n\n static from(input) {\n return (input instanceof this) ? input : new this(input)\n }\n\n static UTC(...args) {\n let time = Date.UTC(...args)\n\n // ECMA Date constructor converts 0-99 to 1900-1999!\n if (args[0] >= 0 && args[0] < 100)\n time = adj(new Date(time))\n\n return time\n }\n\n get type() {\n return this.constructor.type\n }\n\n get edtf() {\n return this.toEDTF()\n }\n\n get isEDTF() {\n return true\n }\n\n toJSON() {\n return this.toEDTF()\n }\n\n toString() {\n return this.toEDTF()\n }\n\n toLocaleString(...args) {\n return this.localize(...args)\n }\n\n inspect() {\n return this.toEDTF()\n }\n\n valueOf() {\n return this.min\n }\n\n [Symbol.toPrimitive](hint) {\n return (hint === 'number') ? this.valueOf() : this.toEDTF()\n }\n\n\n covers(other) {\n return (this.min <= other.min) && (this.max >= other.max)\n }\n\n compare(other) {\n if (other.min == null || other.max == null) return null\n\n let [a, x, b, y] = [this.min, this.max, other.min, other.max]\n\n if (a !== b)\n return a < b ? -1 : 1\n\n if (x !== y)\n return x < y ? -1 : 1\n\n return 0\n }\n\n includes(other) {\n let covered = this.covers(other)\n if (!covered || !this[Symbol.iterator]) return covered\n\n for (let cur of this) {\n if (cur.edtf === other.edtf) return true\n }\n\n return false\n }\n\n *until(then) {\n yield this\n if (this.compare(then)) yield* this.between(then)\n }\n\n *through(then) {\n yield* this.until(then)\n if (this.compare(then)) yield then\n }\n\n *between(then) {\n then = this.constructor.from(then)\n\n let cur = this\n let dir = this.compare(then)\n\n if (!dir) return\n\n for (;;) {\n cur = cur.next(-dir)\n if (cur.compare(then) !== dir) break\n yield cur\n }\n }\n}\n\nfunction adj(date, by = 1900) {\n date.setUTCFullYear(date.getUTCFullYear() - by)\n return date.getTime()\n}\n", "const keys = Reflect.ownKeys.bind(Reflect)\nconst descriptor = Object.getOwnPropertyDescriptor.bind(Object)\nconst define = Object.defineProperty.bind(Object)\nconst has = Object.prototype.hasOwnProperty\n\nexport function mixin(target, ...mixins) {\n for (let source of mixins) {\n inherit(target, source)\n inherit(target.prototype, source.prototype)\n }\n\n return target\n}\n\nfunction inherit(target, source) {\n for (let key of keys(source)) {\n if (!has.call(target, key)) {\n define(target, key, descriptor(source, key))\n }\n }\n}\n", "{\n \"locale\": \"en-US\",\n\n \"date\": {\n \"approximate\": {\n \"long\": \"circa %D\",\n \"medium\": \"ca. %D\",\n \"short\": \"c. %D\"\n },\n \"uncertain\": {\n \"long\": \"%D (unspecified)\",\n \"medium\": \"%D (?)\",\n \"short\": \"%D (?)\"\n }\n }\n}\n", "{\n \"locale\": \"es-ES\",\n\n \"date\": {\n \"approximate\": {\n \"long\": \"circa %D\",\n \"medium\": \"ca. %D\",\n \"short\": \"c. %D\"\n },\n \"uncertain\": {\n \"long\": \"%D (?)\",\n \"medium\": \"%D (?)\",\n \"short\": \"%D (?)\"\n }\n }\n}\n", "{\n \"locale\": \"de-DE\",\n\n \"date\": {\n \"approximate\": {\n \"long\": \"circa %D\",\n \"medium\": \"ca. %D\",\n \"short\": \"ca. %D\"\n },\n \"uncertain\": {\n \"long\": \"%D (?)\",\n \"medium\": \"%D (?)\",\n \"short\": \"%D (?)\"\n }\n }\n}\n", "{\n \"locale\": \"fr-FR\",\n\n \"date\": {\n \"approximate\": {\n \"long\": \"circa %D\",\n \"medium\": \"ca. %D\",\n \"short\": \"c. %D\"\n },\n \"uncertain\": {\n \"long\": \"%D (?)\",\n \"medium\": \"%D (?)\",\n \"short\": \"%D (?)\"\n }\n }\n}\n", "{\n \"locale\": \"it-IT\",\n\n \"date\": {\n \"approximate\": {\n \"long\": \"circa %D\",\n \"medium\": \"ca. %D\",\n \"short\": \"c. %D\"\n },\n \"uncertain\": {\n \"long\": \"%D (?)\",\n \"medium\": \"%D (?)\",\n \"short\": \"%D (?)\"\n }\n }\n}\n", "{\n \"locale\": \"ja-JA\",\n\n \"date\": {\n \"approximate\": {\n \"long\": \"%D\u9803\",\n \"medium\": \"%D\u9803\",\n \"short\": \"%D\u9803\"\n },\n \"uncertain\": {\n \"long\": \"%D\u9803\",\n \"medium\": \"%D\u9803\",\n \"short\": \"%D\u9803\"\n }\n }\n}\n", "const en = require('./en-US.json')\nconst es = require('./es-ES.json')\nconst de = require('./de-DE.json')\nconst fr = require('./fr-FR.json')\nconst it = require('./it-IT.json')\nconst ja = require('./ja-JA.json')\n\nconst alias = (lang, ...regions) => {\n for (let region of regions)\n data[`${lang}-${region}`] = data[lang]\n}\n\nconst data = { en, es, de, fr, it, ja }\n\nalias('en', 'AU', 'CA', 'GB', 'NZ', 'SA', 'US')\nalias('de', 'AT', 'CH', 'DE')\nalias('fr', 'CH', 'FR')\n\nmodule.exports = data\n", "import LC from '../locale-data/index.cjs'\n\nconst { assign } = Object\n\nconst OPTS = [\n {\n day: 'numeric',\n month: 'numeric',\n year: 'numeric',\n timeZoneName: undefined,\n hour: 'numeric',\n minute: 'numeric',\n second: 'numeric'\n },\n {\n year: 'numeric',\n timeZone: 'UTC',\n timeZoneName: undefined\n },\n {\n month: 'numeric',\n year: 'numeric',\n timeZone: 'UTC',\n timeZoneName: undefined\n },\n {\n day: 'numeric',\n month: 'numeric',\n year: 'numeric',\n timeZone: 'UTC',\n timeZoneName: undefined\n }\n]\n\nconst CONS = [\n {},\n {\n month: undefined,\n day: undefined,\n weekday: undefined,\n hour: undefined,\n minute: undefined,\n second: undefined\n },\n {\n day: undefined,\n weekday: undefined,\n hour: undefined,\n minute: undefined,\n second: undefined\n },\n {\n hour: undefined,\n minute: undefined,\n second: undefined\n }\n]\n\n\nfunction getCacheId(...args) {\n let id = []\n\n for (let arg of args) {\n if (arg && typeof arg === 'object') {\n id.push(getOrderedProps(arg))\n } else {\n id.push(arg)\n }\n }\n\n return JSON.stringify(id)\n\n}\n\nfunction getOrderedProps(obj) {\n let props = []\n let keys = Object.getOwnPropertyNames(obj)\n\n for (let key of keys.sort()) {\n props.push({ [key]: obj[key] })\n }\n\n return props\n}\n\nexport function getFormat(date, locale, options) {\n let opts = assign(\n {},\n OPTS[date.precision],\n options,\n CONS[date.precision]\n )\n\n let id = getCacheId(locale, opts)\n\n if (!format.cache.has(id)) {\n format.cache.set(id, new Intl.DateTimeFormat(locale, opts))\n }\n\n return format.cache.get(id)\n}\n\nfunction getPatternsFor(fmt) {\n const { locale, weekday, month, year } = fmt.resolvedOptions()\n const lc = LC[locale]\n\n if (lc == null) return null\n\n const variant = (weekday || month === 'long') ? 'long' :\n (!month || year === '2-digit') ? 'short' : 'medium'\n\n return {\n approximate: lc.date.approximate[variant],\n uncertain: lc.date.uncertain[variant]\n }\n}\n\nfunction isDMY(type) {\n return type === 'day' || type === 'month' || type === 'year'\n}\n\nfunction mask(date, parts) {\n let string = ''\n\n for (let { type, value } of parts) {\n string += (isDMY(type) && date.unspecified.is(type)) ?\n value.replace(/./g, 'X') :\n value\n }\n\n return string\n}\n\n// eslint-disable-next-line complexity\nexport function format(date, locale = 'en-US', options = {}) {\n if (date.timeZone && !options.timeZone) {\n options = {\n timeZoneName: 'short',\n ...options,\n timeZone: date.timeZone\n }\n }\n\n const fmt = getFormat(date, locale, options)\n const pat = getPatternsFor(fmt)\n\n if (!date.isEDTF || pat == null) {\n return fmt.format(date)\n }\n\n if (date.type === 'Interval') {\n if (date.finite) {\n return fmt.formatRange(date.lower, date.upper)\n } else {\n throw new Error('cannot format infinite intervals')\n }\n }\n\n let string = (!date.unspecified.value || !fmt.formatToParts) ?\n fmt.format(date) :\n mask(date, fmt.formatToParts(date))\n\n\n if (date.approximate.value) {\n string = pat.approximate.replace('%D', string)\n }\n\n if (date.uncertain.value) {\n string = pat.uncertain.replace('%D', string)\n }\n\n return string\n}\n\nformat.cache = new Map()\n", "import assert from './assert.js'\nimport { Bitmask } from './bitmask.js'\nimport { ExtDateTime } from './interface.js'\nimport { mixin } from './mixin.js'\nimport { format } from './format.js'\n\nconst { abs } = Math\nconst { isArray } = Array\n\nconst P = new WeakMap()\nconst U = new WeakMap()\nconst A = new WeakMap()\nconst X = new WeakMap()\nconst Z = new WeakMap()\n\nconst PM = [Bitmask.YMD, Bitmask.Y, Bitmask.YM, Bitmask.YMD]\n\nexport class Date extends globalThis.Date {\n constructor(...args) { // eslint-disable-line complexity\n let precision = 0\n let uncertain, approximate, unspecified, timeZone\n\n switch (args.length) {\n case 0:\n break\n\n case 1:\n switch (typeof args[0]) {\n case 'number':\n break\n\n case 'string':\n args = [Date.parse(args[0])]\n\n // eslint-disable-next-line no-fallthrough\n case 'object':\n if (isArray(args[0]))\n args[0] = { values: args[0] }\n\n {\n let obj = args[0]\n\n assert(obj != null)\n if (obj.type) assert.equal('Date', obj.type)\n\n if (obj.values && obj.values.length) {\n precision = obj.values.length\n args = obj.values.slice()\n\n // ECMA Date constructor needs at least two date parts!\n if (args.length < 2) args.push(0)\n\n if (obj.offset) {\n if (args.length < 3) args.push(1)\n while (args.length < 5) args.push(0)\n\n // ECMA Date constructor handles overflows so we\n // simply subtract the offset here!\n args[4] = args[4] - obj.offset\n }\n\n args = [ExtDateTime.UTC(...args)]\n }\n\n ({ uncertain, approximate, unspecified, timeZone } = obj)\n }\n break\n\n default:\n throw new RangeError('Invalid time value')\n }\n\n break\n\n default:\n precision = args.length\n }\n\n super(...args)\n\n this.precision = precision\n\n this.uncertain = uncertain\n this.approximate = approximate\n this.unspecified = unspecified\n this.timeZone = timeZone\n }\n\n set precision(value) {\n P.set(this, (value > 3) ? 0 : Number(value))\n }\n\n get precision() {\n return P.get(this)\n }\n\n set uncertain(value) {\n U.set(this, this.bits(value))\n }\n\n get uncertain() {\n return U.get(this)\n }\n\n set approximate(value) {\n A.set(this, this.bits(value))\n }\n\n get approximate() {\n return A.get(this)\n }\n\n set unspecified(value) {\n X.set(this, new Bitmask(value))\n }\n\n get unspecified() {\n return X.get(this)\n }\n\n set timeZone(value) {\n Z.set(this, value)\n }\n\n get timeZone() {\n return Z.get(this)\n }\n\n get atomic() {\n return !(\n this.precision || this.unspecified.value\n )\n }\n\n get min() {\n // TODO uncertain and approximate\n\n if (this.unspecified.value && this.year < 0) {\n let values = this.unspecified.max(this.values.map(Date.pad))\n values[0] = -values[0]\n return (new Date({ values })).getTime()\n }\n\n return this.getTime()\n }\n\n get max() {\n // TODO uncertain and approximate\n return (this.atomic) ? this.getTime() : this.next().getTime() - 1\n }\n\n get year() {\n return this.getUTCFullYear()\n }\n\n get month() {\n return this.getUTCMonth()\n }\n\n get date() {\n return this.getUTCDate()\n }\n\n get hours() {\n return this.getUTCHours()\n }\n\n get minutes() {\n return this.getUTCMinutes()\n }\n\n get seconds() {\n return this.getUTCSeconds()\n }\n\n get values() {\n switch (this.precision) {\n case 1:\n return [this.year]\n case 2:\n return [this.year, this.month]\n case 3:\n return [this.year, this.month, this.date]\n default:\n return [\n this.year, this.month, this.date, this.hours, this.minutes, this.seconds\n ]\n }\n }\n\n /**\n * Returns the next second, day, month, or year, depending on\n * the current date's precision. Uncertain, approximate and\n * unspecified masks are copied.\n */\n next(k = 1) {\n let { values, unspecified, uncertain, approximate } = this\n\n if (unspecified.value) {\n let bc = values[0] < 0\n\n values = (k < 0) ^ bc ?\n unspecified.min(values.map(Date.pad)) :\n unspecified.max(values.map(Date.pad))\n\n if (bc) values[0] = -values[0]\n }\n\n values.push(values.pop() + k)\n\n return new Date({ values, unspecified, uncertain, approximate })\n }\n\n prev(k = 1) {\n return this.next(-k)\n }\n\n *[Symbol.iterator]() {\n let cur = this\n\n while (cur <= this.max) {\n yield cur\n cur = cur.next()\n }\n }\n\n toEDTF() {\n if (!this.precision) return this.toISOString()\n\n let sign = (this.year < 0) ? '-' : ''\n let values = this.values.map(Date.pad)\n\n if (this.unspecified.value)\n return sign + this.unspecified.masks(values).join('-')\n\n if (this.uncertain.value)\n values = this.uncertain.marks(values, '?')\n\n if (this.approximate.value) {\n values = this.approximate.marks(values, '~')\n .map(value => value.replace(/(~\\?)|(\\?~)/, '%'))\n }\n\n return sign + values.join('-')\n }\n\n format(...args) {\n return format(this, ...args)\n }\n\n static pad(number, idx = 0) {\n if (!idx) { // idx 0 = year, 1 = month, ...\n let k = abs(number)\n\n if (k < 10) return `000${k}`\n if (k < 100) return `00${k}`\n if (k < 1000) return `0${k}`\n\n return `${k}`\n }\n\n if (idx === 1) number = number + 1\n\n return (number < 10) ? `0${number}` : `${number}`\n }\n\n bits(value) {\n if (value === true)\n value = PM[this.precision]\n\n return new Bitmask(value)\n }\n}\n\nmixin(Date, ExtDateTime)\n\nexport const pad = Date.pad\n", "import assert from './assert.js'\nimport { ExtDateTime } from './interface.js'\nimport { pad } from './date.js'\n\nconst { abs } = Math\n\nconst V = new WeakMap()\nconst S = new WeakMap()\n\nexport class Year extends ExtDateTime {\n constructor(input) {\n super()\n\n V.set(this, [])\n\n switch (typeof input) {\n case 'number':\n this.year = input\n break\n\n case 'string':\n input = Year.parse(input)\n\n // eslint-disable-next-line no-fallthrough\n case 'object':\n if (Array.isArray(input))\n input = { values: input }\n\n {\n assert(input !== null)\n if (input.type) assert.equal('Year', input.type)\n\n assert(input.values)\n assert(input.values.length)\n\n this.year = input.values[0]\n this.significant = input.significant\n }\n break\n\n case 'undefined':\n this.year = new Date().getUTCFullYear()\n break\n\n default:\n throw new RangeError('Invalid year value')\n }\n }\n\n get year() {\n return this.values[0]\n }\n\n set year(year) {\n this.values[0] = Number(year)\n }\n\n get significant() {\n return S.get(this)\n }\n\n set significant(digits) {\n S.set(this, Number(digits))\n }\n\n get values() {\n return V.get(this)\n }\n\n get min() {\n return ExtDateTime.UTC(this.year, 0)\n }\n\n get max() {\n return ExtDateTime.UTC(this.year + 1, 0) - 1\n }\n\n toEDTF() {\n let y = abs(this.year)\n let s = this.significant ? `S${this.significant}` : ''\n\n if (y <= 9999) return `${this.year < 0 ? '-' : ''}${pad(this.year)}${s}`\n\n // TODO exponential form for ending zeroes\n\n return `Y${this.year}${s}`\n }\n}\n", "import assert from './assert.js'\nimport { Date as ExtDate } from './date.js'\nimport { ExtDateTime } from './interface.js'\n\nconst { abs, floor } = Math\nconst V = new WeakMap()\n\n\nexport class Decade extends ExtDateTime {\n constructor(input) {\n super()\n\n V.set(this, [])\n\n this.uncertain = false\n this.approximate = false\n\n switch (typeof input) {\n case 'number':\n this.decade = input\n break\n\n case 'string':\n input = Decade.parse(input)\n\n // eslint-disable-next-line no-fallthrough\n case 'object':\n if (Array.isArray(input))\n input = { values: input }\n\n {\n assert(input !== null)\n if (input.type) assert.equal('Decade', input.type)\n\n assert(input.values)\n assert(input.values.length === 1)\n\n this.decade = input.values[0]\n this.uncertain = !!input.uncertain\n this.approximate = !!input.approximate\n }\n break\n\n case 'undefined':\n this.year = new Date().getUTCFullYear()\n break\n\n default:\n throw new RangeError('Invalid decade value')\n }\n }\n\n get decade() {\n return this.values[0]\n }\n\n set decade(decade) {\n decade = floor(Number(decade))\n assert(abs(decade) < 1000, `invalid decade: ${decade}`)\n this.values[0] = decade\n }\n\n get year() {\n return this.values[0] * 10\n }\n\n set year(year) {\n this.decade = year / 10\n }\n\n get values() {\n return V.get(this)\n }\n\n get min() {\n return ExtDate.UTC(this.year, 0)\n }\n\n get max() {\n return ExtDate.UTC(this.year + 10, 0) - 1\n }\n\n toEDTF() {\n let decade = Decade.pad(this.decade)\n\n if (this.uncertain)\n decade = decade + '?'\n\n if (this.approximate)\n decade = (decade + '~').replace(/\\?~/, '%')\n\n return decade\n }\n\n static pad(number) {\n let k = abs(number)\n let sign = (k === number) ? '' : '-'\n\n if (k < 10) return `${sign}00${k}`\n if (k < 100) return `${sign}0${k}`\n\n return `${number}`\n }\n}\n", "import assert from './assert.js'\nimport { Date as ExtDate } from './date.js'\nimport { ExtDateTime } from './interface.js'\n\nconst { abs, floor } = Math\nconst V = new WeakMap()\n\nexport class Century extends ExtDateTime {\n constructor(input) {\n super()\n\n V.set(this, [])\n\n this.uncertain = false\n this.approximate = false\n\n switch (typeof input) {\n case 'number':\n this.century = input\n break\n\n case 'string':\n input = Century.parse(input)\n\n // eslint-disable-next-line no-fallthrough\n case 'object':\n if (Array.isArray(input))\n input = { values: input }\n\n {\n assert(input !== null)\n if (input.type) assert.equal('Century', input.type)\n\n assert(input.values)\n assert(input.values.length === 1)\n\n this.century = input.values[0]\n this.uncertain = !!input.uncertain\n this.approximate = !!input.approximate\n }\n break\n\n case 'undefined':\n this.year = new Date().getUTCFullYear()\n break\n\n default:\n throw new RangeError('Invalid century value')\n }\n }\n\n get century() {\n return this.values[0]\n }\n\n set century(century) {\n century = floor(Number(century))\n assert(abs(century) < 100, `invalid century: ${century}`)\n this.values[0] = century\n }\n\n get year() {\n return this.values[0] * 100\n }\n\n set year(year) {\n this.century = year / 100\n }\n\n get values() {\n return V.get(this)\n }\n\n get min() {\n return ExtDate.UTC(this.year, 0)\n }\n\n get max() {\n return ExtDate.UTC(this.year + 100, 0) - 1\n }\n\n toEDTF() {\n let century = Century.pad(this.century)\n\n if (this.uncertain)\n century = century + '?'\n\n if (this.approximate)\n century = (century + '~').replace(/\\?~/, '%')\n\n return century\n }\n\n static pad(number) {\n let k = abs(number)\n let sign = (k === number) ? '' : '-'\n\n if (k < 10) return `${sign}0${k}`\n\n return `${number}`\n }\n}\n", "import assert from './assert.js'\nimport { Bitmask } from './bitmask.js'\nimport { ExtDateTime } from './interface.js'\nimport { pad } from './date.js'\n\nconst A = new WeakMap()\nconst U = new WeakMap()\nconst V = new WeakMap()\nconst X = new WeakMap()\n\nexport class Season extends ExtDateTime {\n constructor(input) {\n super()\n let uncertain, approximate, unspecified\n\n V.set(this, [])\n\n switch (typeof input) {\n case 'number':\n this.year = input\n this.season = arguments[1] || 21\n break\n\n case 'string':\n input = Season.parse(input)\n\n // eslint-disable-next-line no-fallthrough\n case 'object':\n if (Array.isArray(input))\n input = { values: input }\n\n {\n assert(input !== null)\n if (input.type) assert.equal('Season', input.type)\n\n assert(input.values)\n assert.equal(2, input.values.length)\n\n this.year = input.values[0]\n this.season = input.values[1]\n\n ;({ unspecified, uncertain, approximate } = input)\n\n }\n break\n\n case 'undefined':\n this.year = new Date().getUTCFullYear()\n this.season = 21\n break\n\n default:\n throw new RangeError('Invalid season value')\n }\n\n this.unspecified = unspecified\n this.uncertain = uncertain\n this.approximate = approximate\n }\n\n get year() {\n return this.values[0]\n }\n\n set year(year) {\n this.values[0] = Number(year)\n }\n\n get season() {\n return this.values[1]\n }\n\n set season(season) {\n this.values[1] = validate(Number(season))\n }\n\n get values() {\n return V.get(this)\n }\n\n set uncertain(value) {\n U.set(this, new Bitmask(value))\n }\n\n get uncertain() {\n return U.get(this)\n }\n\n set approximate(value) {\n A.set(this, new Bitmask(value))\n }\n\n get approximate() {\n return A.get(this)\n }\n\n set unspecified(value) {\n X.set(this, new Bitmask(value))\n }\n\n get unspecified() {\n return X.get(this)\n }\n\n next(k = 1) {\n let { season, year, unspecified, approximate, uncertain } = this\n\n switch (true) {\n case (season >= 21 && season <= 36):\n [year, season] = inc(year, season, k, season - (season - 21) % 4, 4)\n break\n case (season >= 37 && season <= 39):\n [year, season] = inc(year, season, k, 37, 3)\n break\n case (season >= 40 && season <= 41):\n [year, season] = inc(year, season, k, 40, 2)\n break\n default:\n throw new RangeError(`Cannot compute next/prev for season ${season}`)\n }\n\n return new Season({\n values: [year, season],\n approximate,\n uncertain,\n unspecified\n })\n }\n\n prev(k = 1) {\n return this.next(-k)\n }\n\n get min() { // eslint-disable-line complexity\n switch (this.season) {\n case 21:\n case 25:\n case 32:\n case 33:\n case 40:\n case 37:\n return ExtDateTime.UTC(this.year, 0)\n\n case 22:\n case 26:\n case 31:\n case 34:\n return ExtDateTime.UTC(this.year, 3)\n\n case 23:\n case 27:\n case 30:\n case 35:\n case 41:\n return ExtDateTime.UTC(this.year, 6)\n\n case 24:\n case 28:\n case 29:\n case 36:\n return ExtDateTime.UTC(this.year, 9)\n\n case 38:\n return ExtDateTime.UTC(this.year, 4)\n\n case 39:\n return ExtDateTime.UTC(this.year, 8)\n\n default:\n return ExtDateTime.UTC(this.year, 0)\n }\n }\n\n get max() { // eslint-disable-line complexity\n let [year] = this.unspecified.max([pad(this.year)])\n\n switch (this.season) {\n case 21:\n case 25:\n case 32:\n case 33:\n return ExtDateTime.UTC(year, 3) - 1\n\n case 22:\n case 26:\n case 31:\n case 34:\n case 40:\n return ExtDateTime.UTC(year, 6) - 1\n\n case 23:\n case 27:\n case 30:\n case 35:\n return ExtDateTime.UTC(year, 9) - 1\n\n case 24:\n case 28:\n case 29:\n case 36:\n case 41:\n case 39:\n return ExtDateTime.UTC(year + 1, 0) - 1\n\n case 37:\n return ExtDateTime.UTC(year, 5) - 1\n\n case 38:\n return ExtDateTime.UTC(year, 9) - 1\n\n default:\n return ExtDateTime.UTC(year + 1, 0) - 1\n }\n }\n\n toEDTF() {\n let sign = (this.year < 0) ? '-' : ''\n let values = [pad(this.year), String(this.season)]\n\n if (this.unspecified.value)\n return sign + this.unspecified.masks(values).join('-')\n\n if (this.uncertain.value)\n values = this.uncertain.marks(values, '?')\n\n if (this.approximate.value) {\n values = this.approximate.marks(values, '~')\n .map(value => value.replace(/(~\\?)|(\\?~)/, '%'))\n }\n\n return sign + values.join('-')\n }\n}\n\nfunction validate(season) {\n if (isNaN(season) || season < 21 || season === Infinity)\n throw new RangeError(`invalid division of year: ${season}`)\n return season\n}\n\nfunction inc(year, season, by, base, size) {\n const m = (season + by) - base\n\n return [\n year + Math.floor(m / size),\n validate(base + (m % size + size) % size)\n ]\n}\n", "import assert from './assert.js'\nimport { Date as ExtDate } from './date.js'\nimport { ExtDateTime } from './interface.js'\nimport { Season } from './season.js'\n\nconst V = new WeakMap()\n\n\nexport class Interval extends ExtDateTime {\n constructor(...args) {\n super()\n\n V.set(this, [null, null])\n\n switch (args.length) {\n case 2:\n this.lower = args[0]\n this.upper = args[1]\n break\n\n case 1:\n switch (typeof args[0]) {\n case 'string':\n args[0] = Interval.parse(args[0])\n\n // eslint-disable-next-line no-fallthrough\n case 'object':\n if (Array.isArray(args[0]))\n args[0] = { values: args[0] }\n\n {\n let [obj] = args\n\n assert(obj !== null)\n if (obj.type) assert.equal('Interval', obj.type)\n\n assert(obj.values)\n assert(obj.values.length < 3)\n\n this.lower = obj.values[0]\n this.upper = obj.values[1]\n\n this.earlier = obj.earlier\n this.later = obj.later\n }\n break\n\n default:\n this.lower = args[0]\n }\n break\n\n case 0:\n break\n\n default:\n throw new RangeError(`invalid interval value: ${args}`)\n }\n }\n\n get lower() {\n return this.values[0]\n }\n\n set lower(value) {\n if (value == null)\n return this.values[0] = null\n\n if (value === Infinity || value === -Infinity)\n return this.values[0] = Infinity\n\n value = getDateOrSeasonFrom(value)\n\n if (value >= this.upper && this.upper != null)\n throw new RangeError(`invalid lower bound: ${value}`)\n\n this.values[0] = value\n }\n\n get upper() {\n return this.values[1]\n }\n\n set upper(value) {\n if (value == null)\n return this.values[1] = null\n\n if (value === Infinity)\n return this.values[1] = Infinity\n\n value = getDateOrSeasonFrom(value)\n\n if (this.lower !== null && this.lower !== Infinity && value <= this.lower)\n throw new RangeError(`invalid upper bound: ${value}`)\n\n this.values[1] = value\n }\n\n get finite() {\n return (this.lower != null && this.lower !== Infinity) &&\n (this.upper != null && this.upper !== Infinity)\n }\n\n get precision() {\n return this.lower?.precision ?? this.upper?.precision ?? 0\n }\n\n *[Symbol.iterator]() {\n if (!this.finite) throw Error('cannot iterate infinite interval')\n yield* this.lower.through(this.upper)\n }\n\n get values() {\n return V.get(this)\n }\n\n get min() {\n let v = this.lower\n return !v ? null : (v === Infinity) ? -Infinity : v.min\n }\n\n get max() {\n let v = this.upper\n return !v ? null : (v === Infinity) ? Infinity : v.max\n }\n\n get isEDTFInterval() {\n return true\n }\n\n toEDTF() {\n return this.values\n .map(v => {\n if (v === Infinity) return '..'\n if (!v) return ''\n return v.edtf\n })\n .join('/')\n }\n}\n\nfunction getDateOrSeasonFrom(value) {\n try {\n return ExtDate.from(value)\n } catch {\n return Season.from(value)\n }\n}\n", "import assert from './assert.js'\nimport { Date } from './date.js'\nimport { ExtDateTime } from './interface.js'\n\nconst { isArray } = Array\nconst V = new WeakMap()\n\n\nexport class List extends ExtDateTime {\n constructor(...args) {\n super()\n\n V.set(this, [])\n\n if (args.length > 1) args = [args]\n\n if (args.length) {\n switch (typeof args[0]) {\n case 'string':\n args[0] = new.target.parse(args[0])\n\n // eslint-disable-next-line no-fallthrough\n case 'object':\n if (isArray(args[0]))\n args[0] = { values: args[0] }\n\n {\n let [obj] = args\n\n assert(obj !== null)\n if (obj.type) assert.equal(this.type, obj.type)\n\n assert(obj.values)\n this.concat(...obj.values)\n\n this.earlier = !!obj.earlier\n this.later = !!obj.later\n }\n break\n\n default:\n throw new RangeError(`invalid ${this.type} value: ${args}`)\n }\n }\n }\n\n get values() {\n return V.get(this)\n }\n\n get length() {\n return this.values.length\n }\n\n get empty() {\n return this.length === 0\n }\n\n get first() {\n let value = this.values[0]\n return isArray(value) ? value[0] : value\n }\n\n get last() {\n let value = this.values[this.length - 1]\n return isArray(value) ? value[0] : value\n }\n\n clear() {\n return (this.values.length = 0), this\n }\n\n concat(...args) {\n for (let value of args) this.push(value)\n return this\n }\n\n push(value) {\n if (isArray(value)) {\n assert.equal(2, value.length)\n return this.values.push(value.map(v => Date.from(v)))\n }\n\n return this.values.push(Date.from(value))\n }\n\n *[Symbol.iterator]() {\n for (let value of this.values) {\n if (isArray(value))\n yield* value[0].through(value[1])\n else\n yield value\n }\n }\n\n get min() {\n return this.earlier ? -Infinity : (this.empty ? 0 : this.first.min)\n }\n\n get max() {\n return this.later ? Infinity : (this.empty ? 0 : this.last.max)\n }\n\n content() {\n return this\n .values\n .map(v => isArray(v) ? v.map(d => d.edtf).join('..') : v.edtf)\n .join(',')\n }\n\n toEDTF() {\n return this.wrap(this.empty ?\n '' :\n `${this.earlier ? '..' : ''}${this.content()}${this.later ? '..' : ''}`\n )\n }\n\n wrap(content) {\n return `{${content}}`\n }\n}\n", "import { List } from './list.js'\nimport { parse } from './parser.js'\n\nexport class Set extends List {\n static parse(input) {\n return parse(input, { types: ['Set'] })\n }\n\n get type() {\n return 'Set'\n }\n\n wrap(content) {\n return `[${content}]`\n }\n}\n", "export { Date } from './date.js'\nexport { Year } from './year.js'\nexport { Decade } from './decade.js'\nexport { Century } from './century.js'\nexport { Season } from './season.js'\nexport { Interval } from './interval.js'\nexport { List } from './list.js'\nexport { Set } from './set.js'\n", "import * as types from './types.js'\nimport { parse } from './parser.js'\n\nconst UNIX_TIME = /^\\d{5,}$/\n\nexport function edtf(...args) {\n if (!args.length)\n return new types.Date()\n\n if (args.length === 1) {\n switch (typeof args[0]) {\n case 'object':\n return new (types[args[0].type] || types.Date)(args[0])\n case 'number':\n return new types.Date(args[0])\n case 'string':\n if ((UNIX_TIME).test(args[0]))\n return new types.Date(Number(args[0]))\n }\n }\n\n let res = parse(...args)\n return new types[res.type](res)\n}\n", "export { edtf as default } from './src/edtf.js'\nexport * from './src/types.js'\nexport { Bitmask } from './src/bitmask.js'\nexport { defaults } from './src/defaults.js'\nexport { parse } from './src/parser.js'\nexport { format } from './src/format.js'\n", "import edtf, { Interval } from 'edtf';\n\n// Returns whether two date component arrays represent the same date.\n// A date component array contains the full date, followed by year, month, day.\n// Dates are only compared to the lowest common precision.\nfunction isSameDate(date1, date2) {\n if (date1[1] !== date2[1]) return false;\n if (date1[2] && date2[2] && date1[2] !== date2[2]) return false;\n if (date1[3] && date2[3] && date1[3] !== date2[3]) return false;\n return true;\n}\n\nfunction isBefore(date1, date2) {\n if (date1[1] > date2[1]) return false;\n if (date1[2] && date2[2] && date1[2] > date2[2]) return false;\n if (date1[3] && date2[3] && date1[3] > date2[3]) return false;\n return true;\n}\n\nfunction isAfter(date1, date2) {\n if (date1[1] < date2[1]) return false;\n if (date1[2] && date2[2] && date1[2] < date2[2]) return false;\n if (date1[3] && date2[3] && date1[3] < date2[3]) return false;\n return true;\n}\n\n/**\n * Returns whether the two sets of tags overlap temporally.\n *\n * @param {object} tags1 Tags to compare.\n * @param {object} tags2 Tags to compare.\n * @param {boolean?} touchIsOverlap True to consider it an overlap if one date range ends at the same time that the other begins.\n */\nexport function utilDatesOverlap(tags1, tags2, touchIsOverlap) {\n // If the feature has a valid start date but no valid end date, assume it\n // starts at the beginning of time.\n var dateRegex = /^(-?\\d{1,4})(?:-(\\d\\d))?(?:-(\\d\\d))?$/;\n var minDate = ['-9999', '-9999'],\n maxDate = ['9999', '9999'],\n start1 = (tags1.start_date || '').match(dateRegex) || minDate,\n start2 = (tags2.start_date || '').match(dateRegex) || minDate,\n end1 = (tags1.end_date || '').match(dateRegex) || maxDate,\n end2 = (tags2.end_date || '').match(dateRegex) || maxDate;\n\n if (isSameDate(end1, start2) || isSameDate(end2, start1)) {\n return touchIsOverlap === true;\n }\n\n return ((isAfter(start1, start2) && isBefore(start1, end2)) ||\n (isAfter(start2, start1) && isBefore(start2, end1)) ||\n (isAfter(end1, start2) && isBefore(end1, end2)) ||\n (isAfter(end2, start1) && isBefore(end2, end1)));\n}\n\n// Returns an object containing the given date string normalized as an ISO 8601 date string and parsed as a Date object.\n// Date components are padded for compatibility with tagging conventions.\n// Dates such as January 0, February 31, and Duodecember 1 are wrapped to make more sense.\nexport function utilNormalizeDateString(raw) {\n if (!raw) return null;\n\n var date;\n\n // Enforce the same date formats supported by DateFunctions-plpgsql and decimaldate-python.\n var dateRegex = /^(-)?(\\d+)(?:-(\\d\\d?)(?:-(\\d\\d?))?)?$/;\n var match = raw.match(dateRegex);\n if (match !== null) {\n // Manually construct a Date.\n // Passing the string directly into the Date constructor would throw an error on negative years.\n date = new Date(0);\n date.setUTCFullYear(parseInt((match[1] || '') + match[2], 10));\n if (match[3]) date.setUTCMonth(parseInt(match[3], 10) - 1); // 0-based\n if (match[4]) date.setUTCDate(parseInt(match[4], 10));\n if (isNaN(date.getDate())) {\n return null;\n }\n } else {\n // Fall back on whatever the browser can parse into a date.\n date = new Date(raw);\n try {\n date.toISOString();\n } catch {\n return null;\n }\n }\n\n // Reconstruct the date string.\n // Avoid Date.toISOString() because it has fixed precision and excessively pads negative years.\n var normalized = '';\n if (match !== null && date.getUTCFullYear() < 0) {\n var absYear = Math.abs(date.getUTCFullYear());\n normalized += '-' + String(absYear).padStart(4, '0');\n } else {\n normalized += String(date.getUTCFullYear()).padStart(4, '0');\n }\n if (match === null || match[3]) {\n normalized += '-' + String(date.getUTCMonth() + 1).padStart(2, '0');\n }\n if (match === null || match[4]) {\n normalized += '-' + String(date.getUTCDate()).padStart(2, '0');\n }\n return {\n date: date,\n value: normalized,\n localeOptions: {\n year: 'numeric',\n era: date.getUTCFullYear() < 1 ? 'short' : undefined,\n month: (match === null || match[3]) ? 'long' : undefined,\n day: (match === null || match[4]) ? 'numeric' : undefined,\n timeZone: 'UTC'\n }\n };\n}\n\n/**\n * Converts a date in OSM approximate date format to EDTF.\n */\nexport function utilEDTFFromOSMDateString(osm) {\n // https://wiki.openstreetmap.org/wiki/Key:start_date#Approximations\n // https://wiki.openstreetmap.org/wiki/Special:Diff/510218/557626#Proposal_for_date_formatting\n // https://www.loc.gov/standards/datetime/\n let [match, start, end, bc] = osm.match(/^(.+)\\.\\.(.+)( BCE?)?$/) || [];\n if (match) {\n if (!bc) bc = '';\n let startEDTF = utilEDTFFromOSMDateString(start + bc);\n if (startEDTF) {\n let parsed = edtf(startEDTF);\n if (parsed instanceof Set) {\n startEDTF = parsed.earlier ? '' : parsed.first.edtf;\n }\n if (parsed instanceof Interval) {\n startEDTF = parsed.earlier ? '' : parsed.lower.edtf;\n }\n }\n\n let endEDTF = utilEDTFFromOSMDateString(end + bc);\n if (endEDTF) {\n let parsed = edtf(endEDTF);\n if (parsed instanceof Set) {\n endEDTF = parsed.later ? '' : parsed.last.edtf;\n }\n if (parsed instanceof Interval) {\n endEDTF = parsed.later ? '' : parsed.upper.edtf;\n }\n }\n\n if (startEDTF && endEDTF) {\n if (startEDTF.match(/[~?%]/) || endEDTF.match(/[~?%]/)) {\n // Set notation is incompatible with qualifiers.\n return `${startEDTF}/${endEDTF}`;\n } else {\n return `[${startEDTF}..${endEDTF}]`;\n }\n }\n }\n\n let year, circa, monthDay;\n [match, circa, year, monthDay, bc] = osm.match(/^(~)?(\\d+)(-\\d\\d(?:-\\d\\d)?)?( BCE?)?$/) || [];\n if (match) {\n if (!circa) circa = '';\n if (!monthDay) monthDay = '';\n if (bc) {\n year = '-' + String(parseInt(year, 10) - 1).padStart(4, '0');\n } else {\n year = year.padStart(4, '0');\n }\n return `${year}${monthDay}${circa}`;\n }\n\n let decade;\n [match, circa, decade, bc] = osm.match(/^(~)?(\\d+)0s( BCE?)?$/) || [];\n if (match) {\n if (!circa) circa = '';\n if (!bc) {\n return `${decade.padStart(3, '0')}X${circa}`;\n }\n let startYear = String(parseInt(decade, 10) * 10 + 8).padStart(4, '0');\n let endYear = String(parseInt(decade, 10) * 10 - 1).padStart(4, '0');\n return `-${startYear}${circa}/-${endYear}${circa}`;\n }\n\n let century;\n [match, circa, century, bc] = osm.match(/^(~)?C(\\d+)( BCE?)?$/) || [];\n if (match) {\n if (!circa) circa = '';\n if (!bc) {\n return `${String(parseInt(century, 10) - 1).padStart(2, '0')}XX${circa}`;\n }\n let startYear = String((parseInt(century, 10) - 1) * 100 + 98).padStart(4, '0');\n let endYear = String((parseInt(century, 10) - 1) * 100 - 1).padStart(4, '0');\n return `-${startYear}${circa}/-${endYear}${circa}`;\n }\n\n let third;\n [match, third, decade, bc] = osm.match(/^(early|mid|late) (\\d+)0s( BCE?)?$/) || [];\n if (match) {\n // https://uhlibraries-digital.github.io/bcdams-map/guidelines/date\n const offsetsByThird = {\n early: [0, 3],\n mid: [3, 7],\n late: [7, 9],\n };\n\n let startYear = decade * 10 + offsetsByThird[third][bc ? 1 : 0];\n let endYear = decade * 10 + offsetsByThird[third][bc ? 0 : 1];\n if (bc) {\n startYear = startYear + 1;\n endYear = endYear + 1;\n return `-${String(startYear).padStart(4, '0')}~/-${String(endYear).padStart(4, '0')}~`;\n } else {\n return `${String(startYear).padStart(4, '0')}~/${String(endYear).padStart(4, '0')}~`;\n }\n }\n\n [match, third, century, bc] = osm.match(/^(early|mid|late) C(\\d+)( BCE?)?$/) || [];\n if (match) {\n // https://uhlibraries-digital.github.io/bcdams-map/guidelines/date\n const offsetsByThird = {\n early: [0, 30],\n mid: [30, 70],\n late: [70, 99],\n };\n\n century = parseInt(century, 10) - 1;\n let startYear = century * 100 + offsetsByThird[third][bc ? 1 : 0];\n let endYear = century * 100 + offsetsByThird[third][bc ? 0 : 1];\n if (bc) {\n startYear = startYear + 1;\n endYear = endYear + 1;\n return `-${String(startYear).padStart(4, '0')}~/-${String(endYear).padStart(4, '0')}~`;\n } else {\n return `${String(startYear).padStart(4, '0')}~/${String(endYear).padStart(4, '0')}~`;\n }\n }\n\n [match, end] = osm.match(/^before (\\d{4}(?:-\\d\\d)?(?:-\\d\\d)?)$/) || [];\n if (match) {\n return `[..${end}]`;\n }\n\n [match, start] = osm.match(/^after (\\d{4}(?:-\\d\\d)?(?:-\\d\\d)?)$/) || [];\n if (match) {\n return `[${start}..]`;\n }\n}\n", "import { color as d3_color } from 'd3';\nimport { remove as removeDiacritics } from 'diacritics';\n\nimport { fixRTLTextForSvg, rtlRegex } from './svg_paths_rtl_fix';\n\nimport { t, localizer } from '../core/localizer';\nimport { utilArrayUnion } from './array';\nimport { utilNormalizeDateString } from './ohm_date';\nimport { utilDetect } from './detect';\nimport { geoExtent } from '../geo/extent';\n\n\nexport function utilTagText(entity) {\n var obj = (entity && entity.tags) || {};\n return Object.keys(obj)\n .map(function(k) { return k + '=' + obj[k]; })\n .join(', ');\n}\n\n\nexport function utilTotalExtent(array, graph) {\n var extent = geoExtent();\n var val, entity;\n for (var i = 0; i < array.length; i++) {\n val = array[i];\n entity = typeof val === 'string' ? graph.hasEntity(val) : val;\n if (entity) {\n extent._extend(entity.extent(graph));\n }\n }\n return extent;\n}\n\n/**\n * @typedef {{ type: '-' | '+'; key: string; oldVal: string; newVal: string; display: string; }} TagDiff\n * @param {Tags} oldTags\n * @param {Tags} newTags\n */\nexport function utilTagDiff(oldTags, newTags) {\n /** @type {TagDiff[]} */\n var tagDiff = [];\n var keys = utilArrayUnion(Object.keys(oldTags), Object.keys(newTags)).sort();\n keys.forEach(function(k) {\n var oldVal = oldTags[k];\n var newVal = newTags[k];\n\n if ((oldVal || oldVal === '') && (newVal === undefined || newVal !== oldVal)) {\n tagDiff.push({\n type: '-',\n key: k,\n oldVal: oldVal,\n newVal: newVal,\n display: '- ' + k + '=' + oldVal\n });\n }\n if ((newVal || newVal === '') && (oldVal === undefined || newVal !== oldVal)) {\n tagDiff.push({\n type: '+',\n key: k,\n oldVal: oldVal,\n newVal: newVal,\n display: '+ ' + k + '=' + newVal\n });\n }\n });\n return tagDiff;\n}\n\n\nexport function utilEntitySelector(ids) {\n return ids.length ? '.' + ids.join(',.') : 'nothing';\n}\n\n\n// returns an selector to select entity ids for:\n// - entityIDs passed in\n// - shallow descendant entityIDs for any of those entities that are relations\nexport function utilEntityOrMemberSelector(ids, graph) {\n var seen = new Set(ids);\n ids.forEach(collectShallowDescendants);\n return utilEntitySelector(Array.from(seen));\n\n function collectShallowDescendants(id) {\n var entity = graph.hasEntity(id);\n if (!entity || entity.type !== 'relation') return;\n\n entity.members\n .map(function(member) { return member.id; })\n .forEach(function(id) { seen.add(id); });\n }\n}\n\n\n// returns an selector to select entity ids for:\n// - entityIDs passed in\n// - deep descendant entityIDs for any of those entities that are relations\nexport function utilEntityOrDeepMemberSelector(ids, graph) {\n return utilEntitySelector(utilEntityAndDeepMemberIDs(ids, graph));\n}\n\n\n// returns an selector to select entity ids for:\n// - entityIDs passed in\n// - deep descendant entityIDs for any of those entities that are relations\nexport function utilEntityAndDeepMemberIDs(ids, graph) {\n var seen = new Set();\n ids.forEach(collectDeepDescendants);\n return Array.from(seen);\n\n function collectDeepDescendants(id) {\n if (seen.has(id)) return;\n seen.add(id);\n\n var entity = graph.hasEntity(id);\n if (!entity || entity.type !== 'relation') return;\n\n entity.members\n .map(function(member) { return member.id; })\n .forEach(collectDeepDescendants); // recurse\n }\n}\n\n// returns an selector to select entity ids for:\n// - deep descendant entityIDs for any of those entities that are relations\nexport function utilDeepMemberSelector(ids, graph, skipMultipolgonMembers) {\n var idsSet = new Set(ids);\n var seen = new Set();\n var returners = new Set();\n ids.forEach(collectDeepDescendants);\n return utilEntitySelector(Array.from(returners));\n\n function collectDeepDescendants(id) {\n if (seen.has(id)) return;\n seen.add(id);\n\n if (!idsSet.has(id)) {\n returners.add(id);\n }\n\n var entity = graph.hasEntity(id);\n if (!entity || entity.type !== 'relation') return;\n if (skipMultipolgonMembers && entity.isMultipolygon()) return;\n entity.members\n .map(function(member) { return member.id; })\n .forEach(collectDeepDescendants); // recurse\n }\n}\n\n\n// Adds or removes highlight styling for the specified entities\nexport function utilHighlightEntities(ids, highlighted, context) {\n context.surface()\n .selectAll(utilEntityOrDeepMemberSelector(ids, context.graph()))\n .classed('highlighted', highlighted);\n}\n\n\n// returns an Array that is the union of:\n// - nodes for any nodeIDs passed in\n// - child nodes of any wayIDs passed in\n// - descendant member and child nodes of relationIDs passed in\nexport function utilGetAllNodes(ids, graph) {\n var seen = new Set();\n var nodes = new Set();\n\n ids.forEach(collectNodes);\n return Array.from(nodes);\n\n function collectNodes(id) {\n if (seen.has(id)) return;\n seen.add(id);\n\n var entity = graph.hasEntity(id);\n if (!entity) return;\n\n if (entity.type === 'node') {\n nodes.add(entity);\n } else if (entity.type === 'way') {\n entity.nodes.forEach(collectNodes);\n } else {\n entity.members\n .map(function(member) { return member.id; })\n .forEach(collectNodes); // recurse\n }\n }\n}\n\n/**\n * @param {iD.OsmEntity} entity the entity to generate a display name for\n * @param {object} flags a set of flags to tweak the display name output:\n * - hideNetwork: If true, the `network` tag will not be used\n * in the name to prevent it being shown twice\n * (see PR #8707#discussion_r712658175)\n * - hideRef: If true, the `ref` tag will not be output.\n * - isMapLabel: If true, this name is for a label on the map.\n * If falsy, it's for a label elsewhere in the UI.\n */\nexport function utilDisplayName(entity, flags) {\n /* eslint-disable-next-line no-warning-comments */\n // FIXME: Make this localizable again once we're set up for translation.\n // https://github.com/OpenHistoricalMap/issues/issues/652\n // https://github.com/OpenHistoricalMap/issues/issues/470\n //return name && dateRange ? t('inspector.display_name.dated', {dateRange: dateRange, name: name}) : name;\n const appendDateRange = (name, dateRange) => {\n return dateRange ? `${name} [${dateRange}]`.trim() : name;\n };\n\n var localizedNameKey = 'name:' + localizer.languageCode().toLowerCase();\n var name = entity.tags[localizedNameKey] || entity.tags.name || '';\n\n let dateRange;\n if (entity.tags.start_date || entity.tags.end_date) {\n let start = entity.tags.start_date && utilNormalizeDateString(entity.tags.start_date);\n let end = entity.tags.end_date && utilNormalizeDateString(entity.tags.end_date);\n\n if (start || end) {\n // Keep the date range suffix succinct by only including the year and era.\n let options = {timeZone: 'UTC'};\n if (end) {\n options.year = end.localeOptions.year;\n options.era = end.localeOptions.era;\n }\n if (start) {\n // Override any settings from the end of the range.\n options.year = start.localeOptions.year;\n options.era = start.localeOptions.era;\n }\n\n // Get the date range format in structured form, then filter out anything untagged.\n let format = new Intl.DateTimeFormat(localizer.languageCode(), options);\n let lateDate = new Date(Date.UTC(9999));\n let parts = format.formatRangeToParts(start ? start.date : lateDate, end ? end.date : lateDate);\n if (!start) {\n parts = parts.filter(p => p.source !== 'startRange');\n }\n if (!end) {\n parts = parts.filter(p => p.source !== 'endRange');\n }\n dateRange = parts.map(p => p.value).join('');\n }\n }\n\n var tags = {\n direction: entity.tags.direction,\n from: entity.tags.from,\n name,\n network: flags?.hideNetwork ? undefined : (entity.tags.cycle_network || entity.tags.network),\n ref: flags?.hideRef ? undefined : entity.tags.ref,\n to: entity.tags.to,\n via: entity.tags.via\n };\n\n // A right or left-right arrow likely indicates a formulaic \u201Cname\u201D as specified by the Public Transport v2 schema.\n // This name format already contains enough details to disambiguate the feature; avoid duplicating these details.\n if (entity.tags.route && entity.tags.name && entity.tags.name.match(/[\u2192\u21D2\u2194\u21D4]|[-=]>/)) {\n return appendDateRange(entity.tags.name, dateRange);\n }\n\n // Non-routes tend to be labeled in many places besides the relation lists, such as the map, where brevity is important.\n if (!entity.tags.route && name) {\n return appendDateRange(name, dateRange);\n }\n\n var keyComponents = [];\n\n if (tags.network) {\n keyComponents.push('network');\n }\n if (tags.ref) {\n keyComponents.push('ref');\n }\n if (tags.name) {\n keyComponents.push('name');\n }\n\n // Routes may need more disambiguation based on direction or destination\n if (entity.tags.route) {\n if (tags.direction) {\n keyComponents.push('direction');\n } else if (tags.from && tags.to) {\n keyComponents.push('from');\n keyComponents.push('to');\n if (tags.via) {\n keyComponents.push('via');\n }\n }\n }\n\n if (keyComponents.length) {\n return appendDateRange(t('inspector.display_name.' + keyComponents.join('_'), tags), dateRange);\n }\n\n const alternativeNameKeys = [\n 'addr:housename',\n 'alt_name',\n 'official_name',\n 'loc_name',\n 'loc_ref',\n 'unsigned_ref',\n 'seamark:name',\n 'sector:name',\n 'lock_name'\n ];\n\n if (entity.tags.highway === 'milestone' || entity.tags.railway === 'milestone') {\n // distance & railway:position are only valid as names when used on a milestone\n alternativeNameKeys.push('distance', 'railway:position');\n }\n\n // if there's still no name found, try some other name-like tags\n for (const key of alternativeNameKeys) {\n if (key in entity.tags) {\n return appendDateRange(entity.tags[key], dateRange);\n }\n }\n\n // as a last resort, use the street address as a name.\n const unit = entity.tags['addr:unit'];\n const housenumber = entity.tags['addr:housenumber'];\n const streetOrPlace = entity.tags['addr:street'] || entity.tags['addr:place'];\n\n if (!flags?.isMapLabel && unit && housenumber && streetOrPlace) {\n return appendDateRange(t('inspector.display_name_addr_with_unit', {\n unit,\n housenumber,\n streetOrPlace,\n }), dateRange);\n }\n\n if (!flags?.isMapLabel && housenumber && streetOrPlace) {\n return appendDateRange(t('inspector.display_name_addr', {\n housenumber,\n streetOrPlace,\n }), dateRange);\n }\n\n // the housenumber can always be used, regardless of isMapLabel\n if (housenumber) {\n return appendDateRange(housenumber, dateRange);\n }\n\n // no match found\n return appendDateRange('', dateRange);\n}\n\n\nexport function utilDisplayNameForPath(entity) {\n var name = utilDisplayName(entity, { isMapLabel: true });\n var isFirefox = utilDetect().browser.toLowerCase().indexOf('firefox') > -1;\n var isNewChromium = Number(utilDetect().version.split('.')[0]) >= 96.0;\n\n if (!isFirefox && !isNewChromium && name && rtlRegex.test(name)) {\n name = fixRTLTextForSvg(name);\n }\n\n return name;\n}\n\n\nexport function utilDisplayType(id) {\n return {\n n: t('inspector.node'),\n w: t('inspector.way'),\n r: t('inspector.relation')\n }[id.charAt(0)];\n}\n\n\nexport function utilEntityRoot(entityType) {\n return {\n node: 'n',\n way: 'w',\n relation: 'r'\n }[entityType];\n}\n\n\n// Returns a single object containing the tags of all the given entities.\n// Example:\n// {\n// highway: 'service',\n// service: 'parking_aisle'\n// }\n// +\n// {\n// highway: 'service',\n// service: 'driveway',\n// width: '3'\n// }\n// =\n// {\n// highway: 'service',\n// service: [ 'driveway', 'parking_aisle' ],\n// width: [ '3', undefined ]\n// }\nexport function utilCombinedTags(entityIDs, graph) {\n\n var tags = {};\n var tagCounts = {};\n var allKeys = new Set();\n var allTags = [];\n\n var entities = entityIDs.map(function(entityID) {\n return graph.hasEntity(entityID);\n }).filter(Boolean);\n\n // gather the aggregate keys\n entities.forEach(function(entity) {\n var keys = Object.keys(entity.tags).filter(Boolean);\n keys.forEach(function(key) {\n allKeys.add(key);\n });\n });\n\n entities.forEach(function(entity) {\n allTags.push(entity.tags);\n\n allKeys.forEach(function(key) {\n\n var value = entity.tags[key]; // purposely allow `undefined`\n\n if (!tags.hasOwnProperty(key)) {\n // first value, set as raw\n tags[key] = value;\n } else {\n if (!Array.isArray(tags[key])) {\n if (tags[key] !== value) {\n // first alternate value, replace single value with array\n tags[key] = [tags[key], value];\n }\n } else { // type is array\n if (tags[key].indexOf(value) === -1) {\n // subsequent alternate value, add to array\n tags[key].push(value);\n }\n }\n }\n\n var tagHash = key + '=' + value;\n if (!tagCounts[tagHash]) tagCounts[tagHash] = 0;\n tagCounts[tagHash] += 1;\n });\n });\n\n for (const key in tags) {\n if (!Array.isArray(tags[key])) continue;\n\n // sort values by frequency then alphabetically\n tags[key] = tags[key].sort(function(val1, val2) {\n var count2 = tagCounts[key + '=' + val2];\n var count1 = tagCounts[key + '=' + val1];\n if (count2 !== count1) {\n return count2 - count1;\n }\n if (val2 && val1) {\n return val1.localeCompare(val2);\n }\n return val1 ? 1 : -1;\n });\n }\n\n tags = Object.defineProperty(tags, Symbol.for('allTags'), { enumerable: false, value: allTags });\n return tags;\n}\n\n\nexport function utilStringQs(str) {\n str = str.replace(/^[#?]{0,2}/, ''); // advance past any leading '?' or '#' characters\n return Object.fromEntries(new URLSearchParams(str));\n}\n\n\nexport function utilQsString(obj, softEncode) {\n let str = new URLSearchParams(obj).toString();\n if (softEncode) {\n // for better readability of URL hashes: optionally\n // leave some special characters unescaped\n // \"/\" used in map state\n // \":\", \",\", {\" and \"}\" used in background param\n str = str.replace(/(%2F|%3A|%2C|%7B|%7D)/g, decodeURIComponent);\n }\n return str;\n}\n\n\nexport function utilPrefixDOMProperty(property) {\n var prefixes = ['webkit', 'ms', 'moz', 'o'];\n var i = -1;\n var n = prefixes.length;\n var s = document.body;\n\n if (property in s) return property;\n\n property = property.slice(0, 1).toUpperCase() + property.slice(1);\n\n while (++i < n) {\n if (prefixes[i] + property in s) {\n return prefixes[i] + property;\n }\n }\n\n return false;\n}\n\n\nexport function utilPrefixCSSProperty(property) {\n var prefixes = ['webkit', 'ms', 'Moz', 'O'];\n var i = -1;\n var n = prefixes.length;\n var s = document.body.style;\n\n if (property.toLowerCase() in s) {\n return property.toLowerCase();\n }\n\n while (++i < n) {\n if (prefixes[i] + property in s) {\n return '-' + prefixes[i].toLowerCase() + property.replace(/([A-Z])/g, '-$1').toLowerCase();\n }\n }\n\n return false;\n}\n\n\nvar transformProperty;\nexport function utilSetTransform(el, x, y, scale) {\n transformProperty ||= utilPrefixCSSProperty('Transform');\n var prop = transformProperty;\n var translate = utilDetect().opera ? 'translate(' + x + 'px,' + y + 'px)'\n : 'translate3d(' + x + 'px,' + y + 'px,0)';\n return el.style(prop, translate + (scale ? ' scale(' + scale + ')' : ''));\n}\n\n\n// Calculates Levenshtein distance between two strings\n// see: https://en.wikipedia.org/wiki/Levenshtein_distance\n// first converts the strings to lowercase and replaces diacritic marks with ascii equivalents.\nexport function utilEditDistance(a, b) {\n a = removeDiacritics(a.toLowerCase());\n b = removeDiacritics(b.toLowerCase());\n if (a.length === 0) return b.length;\n if (b.length === 0) return a.length;\n var matrix = [];\n var i, j;\n for (i = 0; i <= b.length; i++) { matrix[i] = [i]; }\n for (j = 0; j <= a.length; j++) { matrix[0][j] = j; }\n for (i = 1; i <= b.length; i++) {\n for (j = 1; j <= a.length; j++) {\n if (b.charAt(i-1) === a.charAt(j-1)) {\n matrix[i][j] = matrix[i-1][j-1];\n } else {\n matrix[i][j] = Math.min(matrix[i-1][j-1] + 1, // substitution\n Math.min(matrix[i][j-1] + 1, // insertion\n matrix[i-1][j] + 1)); // deletion\n }\n }\n }\n return matrix[b.length][a.length];\n}\n\n\n// a d3.mouse-alike which\n// 1. Only works on HTML elements, not SVG\n// 2. Does not cause style recalculation\nexport function utilFastMouse(container) {\n var rect = container.getBoundingClientRect();\n var rectLeft = rect.left;\n var rectTop = rect.top;\n var clientLeft = +container.clientLeft;\n var clientTop = +container.clientTop;\n return function(e) {\n return [\n e.clientX - rectLeft - clientLeft,\n e.clientY - rectTop - clientTop\n ];\n };\n}\n\n\nexport function utilAsyncMap(inputs, func, callback) {\n var remaining = inputs.length;\n var results = [];\n var errors = [];\n\n inputs.forEach(function(d, i) {\n func(d, function done(err, data) {\n errors[i] = err;\n results[i] = data;\n remaining--;\n if (!remaining) callback(errors, results);\n });\n });\n}\n\n\n// wraps an index to an interval [0..length-1]\nexport function utilWrap(index, length) {\n if (index < 0) {\n index += Math.ceil(-index/length)*length;\n }\n return index % length;\n}\n\n\n/**\n * a replacement for functor\n *\n * @param {*} value any value\n * @returns {Function} a function that returns that value or the value if it's a function\n */\nexport function utilFunctor(value) {\n if (typeof value === 'function') return value;\n return function() {\n return value;\n };\n}\n\n\nexport function utilNoAuto(selection) {\n var isText = (selection.size() && selection.node().tagName.toLowerCase() === 'textarea');\n\n return selection\n // assign 'new-password' even for non-password fields to prevent browsers (Chrome) ignoring 'off'\n .attr('autocomplete', 'new-password')\n .attr('autocorrect', 'off')\n .attr('autocapitalize', 'off')\n .attr('data-1p-ignore', 'true') // 1Password\n .attr('data-bwignore', 'true') // Bitwarden\n .attr('data-form-type', 'other') // Dashlane\n .attr('data-lpignore', 'true') // LastPass\n .attr('spellcheck', isText ? 'true' : 'false');\n}\n\n\n// https://stackoverflow.com/questions/194846/is-there-any-kind-of-hash-code-function-in-javascript\n// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/\nexport function utilHashcode(str) {\n var hash = 0;\n if (str.length === 0) {\n return hash;\n }\n for (var i = 0; i < str.length; i++) {\n var char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash; // Convert to 32bit integer\n }\n return hash;\n}\n\n// Returns version of `str` with all runs of special characters replaced by `_`;\n// suitable for HTML ids, classes, selectors, etc.\nexport function utilSafeClassName(str) {\n return str.toLowerCase().replace(/[^a-z0-9]+/g, '_');\n}\n\n// Returns string based on `val` that is highly unlikely to collide with an id\n// used previously or that's present elsewhere in the document. Useful for preventing\n// browser-provided autofills or when embedding iD on pages with unknown elements.\nexport function utilUniqueDomId(val) {\n return 'ideditor-' + utilSafeClassName(val.toString()) + '-' + new Date().getTime().toString();\n}\n\n// Returns the length of `str` in unicode characters. This can be less than\n// `String.length()` since a single unicode character can be composed of multiple\n// JavaScript UTF-16 code units.\nexport function utilUnicodeCharsCount(str) {\n // Native ES2015 implementations of `Array.from` split strings into unicode characters\n return Array.from(str).length;\n}\n\n// Returns a new string representing `str` cut from its start to `limit` length\n// in unicode characters. Note that this runs the risk of splitting graphemes.\nexport function utilUnicodeCharsTruncated(str, limit) {\n return Array.from(str).slice(0, limit).join('');\n}\n\nfunction toNumericID(id) {\n var match = id.match(/^[cnwr](-?\\d+)$/);\n if (match) {\n return parseInt(match[1], 10);\n }\n return NaN;\n}\n\nfunction compareNumericIDs(left, right) {\n if (isNaN(left) && isNaN(right)) return -1;\n if (isNaN(left)) return 1;\n if (isNaN(right)) return -1;\n if (Math.sign(left) !== Math.sign(right)) return -Math.sign(left);\n if (Math.sign(left) < 0) return Math.sign(right - left);\n return Math.sign(left - right);\n}\n\n// Returns -1 if the first parameter ID is older than the second,\n// 1 if the second parameter is older, 0 if they are the same.\n// If both IDs are test IDs, the function returns -1.\nexport function utilCompareIDs(left, right) {\n return compareNumericIDs(toNumericID(left), toNumericID(right));\n}\n\n// Returns the chronologically oldest ID in the list.\n// Database IDs (with positive numbers) before editor ones (with negative numbers).\n// Among each category, the closest number to 0 is the oldest.\n// Test IDs (any string that does not conform to OSM's ID scheme) are taken last.\nexport function utilOldestID(ids) {\n if (ids.length === 0) {\n return undefined;\n }\n\n var oldestIDIndex = 0;\n var oldestID = toNumericID(ids[0]);\n\n for (var i = 1; i < ids.length; i++) {\n var num = toNumericID(ids[i]);\n\n if (compareNumericIDs(oldestID, num) === 1) {\n oldestIDIndex = i;\n oldestID = num;\n }\n }\n\n return ids[oldestIDIndex];\n}\n\n// returns a normalized and truncated string to `maxChars` utf-8 characters\nexport function utilCleanOsmString(val, maxChars) {\n // be lenient with input\n if (val === undefined || val === null) {\n val = '';\n } else {\n val = val.toString();\n }\n\n // remove whitespace\n val = val.trim();\n\n // use the canonical form of the string\n if (val.normalize) val = val.normalize('NFC');\n\n // trim to the number of allowed characters\n return utilUnicodeCharsTruncated(val, maxChars);\n}\n\n// https://stackoverflow.com/a/70360753/1627467\nexport function getLuma(color) {\n const {r, g, b} = d3_color(color);\n return 0.2999 * r + 0.587 * g + 0.114 * b;\n}\n\n/** @param {XMLHttpRequestBodyInit} input */\nexport function utilGzip(input) {\n // check if compression is supported natively\n if (!globalThis.CompressionStream) return undefined;\n\n try {\n const stream = new Response(input).body.pipeThrough(\n new CompressionStream('gzip')\n );\n return new Response(stream).blob();\n } catch {\n // if an error is thrown, it means the browser supports\n // CompressionStream but not the specific algorithm.\n return undefined;\n }\n}\n", "export function utilCleanTags(tags: Tags) {\n var out: Tags = {};\n for (var k in tags) {\n if (!k) continue;\n var v = tags[k];\n if (v !== undefined) {\n out[k] = cleanValue(k, v);\n }\n }\n\n return out;\n\n\n function cleanValue(k: string, v: string) {\n function keepSpaces(k: string) {\n return /_hours|_times|:conditional$/.test(k);\n }\n\n function skip(k: string) {\n return /^(description|note|fixme|inscription)(:.+)?$/.test(k);\n }\n\n if (skip(k)) return v;\n\n var cleaned = v\n .split(';')\n .map(function(s) { return s.trim(); })\n .join(keepSpaces(k) ? '; ' : ';');\n\n // The code below is not intended to validate websites and emails.\n // It is only intended to prevent obvious copy-paste errors. (#2323)\n // clean website- and email-like tags\n if (k.indexOf('website') !== -1 ||\n k.indexOf('email') !== -1 ||\n cleaned.indexOf('http') === 0) {\n cleaned = cleaned\n .replace(/[\\u200B-\\u200F\\uFEFF]/g, ''); // strip LRM and other zero width chars\n\n }\n\n return cleaned;\n }\n}\n", "// Like selection.property('value', ...), but avoids no-op value sets,\n// which can result in layout/repaint thrashing in some situations.\n/** @returns {string} */\nexport function utilGetSetValue(selection, value, shouldUpdate) {\n function setValue(value, shouldUpdate) {\n function valueNull() {\n delete this.value;\n }\n\n function valueConstant() {\n if (shouldUpdate(this.value, value)) {\n this.value = value;\n }\n }\n\n function valueFunction() {\n var x = value.apply(this, arguments);\n if (x === null) {\n return;\n } else if (x === undefined) {\n this.value = '';\n } else if (shouldUpdate(this.value, x)) {\n this.value = x;\n }\n }\n\n return (value === null || value === undefined)\n ? valueNull : (typeof value === 'function'\n ? valueFunction : valueConstant);\n }\n\n if (arguments.length === 1) {\n return selection.property('value');\n }\n\n if (shouldUpdate === undefined) {\n shouldUpdate = (a, b) => a !== b;\n }\n\n return selection.each(setValue(value, shouldUpdate));\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { utilArrayUniq } from './array';\n\n\nexport function utilKeybinding(namespace) {\n var _keybindings = {};\n\n\n function testBindings(d3_event, isCapturing) {\n var didMatch = false;\n var bindings = Object.keys(_keybindings).map(function(id) { return _keybindings[id]; });\n\n // Most key shortcuts will accept either lower or uppercase ('h' or 'H'),\n // so we don't strictly match on the shift key, but we prioritize\n // shifted keybindings first, and fallback to unshifted only if no match.\n // (This lets us differentiate between '\u2190'/'\u21E7\u2190' or '\u2318Z'/'\u2318\u21E7Z')\n\n // priority match shifted keybindings first\n for (const binding of bindings) {\n if (!binding.event.modifiers.shiftKey) continue; // no shift\n if (!!binding.capture !== isCapturing) continue;\n if (matches(d3_event, binding, true)) {\n binding.callback(d3_event);\n didMatch = true;\n\n // match a max of one binding per event\n break;\n }\n }\n\n if (didMatch) return;\n\n // then unshifted keybindings\n for (const binding of bindings) {\n if (binding.event.modifiers.shiftKey) continue; // shift\n if (!!binding.capture !== isCapturing) continue;\n if (matches(d3_event, binding, false)) {\n binding.callback(d3_event);\n break;\n }\n }\n\n\n function matches(d3_event, binding, testShift) {\n var event = d3_event;\n var isMatch = false;\n var tryKeyCode = true;\n\n // Prefer a match on `KeyboardEvent.key`\n if (event.key !== undefined) {\n tryKeyCode = (event.key.charCodeAt(0) > 127); // outside ISO-Latin-1\n isMatch = true;\n\n if (binding.event.key === undefined) {\n isMatch = false;\n } else if (Array.isArray(binding.event.key)) {\n if (binding.event.key.map(function(s) {\n return s.toLowerCase();\n }).indexOf(event.key.toLowerCase()) === -1) {\n isMatch = false;\n }\n } else {\n if (event.key.toLowerCase() !== binding.event.key.toLowerCase()) {\n isMatch = false;\n }\n }\n }\n\n // Fallback match on `KeyboardEvent.keyCode`, can happen if:\n // - `KeyboardEvent.key` is outside ASCII range (e.g. cyrillic - # )\n // - alt/option/\u2325 key is also requested (e.g. Spanish keyboard on MacOS - #8905)\n if (!isMatch && (tryKeyCode || binding.event.modifiers.altKey)) {\n isMatch = (event.keyCode === binding.event.keyCode);\n }\n\n if (!isMatch) return false;\n\n // test modifier keys\n if (!(event.ctrlKey && event.altKey)) { // if both are set, assume AltGr and skip it - #4096\n if (event.ctrlKey !== binding.event.modifiers.ctrlKey) return false;\n if (event.altKey !== binding.event.modifiers.altKey) return false;\n }\n if (event.metaKey !== binding.event.modifiers.metaKey) return false;\n if (testShift && event.shiftKey !== binding.event.modifiers.shiftKey) return false;\n\n return true;\n }\n }\n\n\n function capture(d3_event) {\n testBindings(d3_event, true);\n }\n\n\n function bubble(d3_event) {\n var tagName = d3_select(d3_event.target).node().tagName;\n if (tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA') {\n return;\n }\n testBindings(d3_event, false);\n }\n\n\n function keybinding(selection) {\n selection = selection || d3_select(document);\n selection.on('keydown.capture.' + namespace, capture, true);\n selection.on('keydown.bubble.' + namespace, bubble, false);\n return keybinding;\n }\n\n // was: keybinding.off()\n keybinding.unbind = function(selection) {\n _keybindings = [];\n selection = selection || d3_select(document);\n selection.on('keydown.capture.' + namespace, null);\n selection.on('keydown.bubble.' + namespace, null);\n return keybinding;\n };\n\n\n keybinding.clear = function() {\n _keybindings = {};\n return keybinding;\n };\n\n\n // Remove one or more keycode bindings.\n keybinding.off = function(codes, capture) {\n var arr = utilArrayUniq([].concat(codes));\n\n for (var i = 0; i < arr.length; i++) {\n var id = arr[i] + (capture ? '-capture' : '-bubble');\n delete _keybindings[id];\n }\n return keybinding;\n };\n\n\n // Add one or more keycode bindings.\n keybinding.on = function(codes, callback, capture) {\n if (typeof callback !== 'function') {\n return keybinding.off(codes, capture);\n }\n\n var arr = utilArrayUniq([].concat(codes));\n\n for (var i = 0; i < arr.length; i++) {\n var id = arr[i] + (capture ? '-capture' : '-bubble');\n var binding = {\n id: id,\n capture: capture,\n callback: callback,\n event: {\n key: undefined, // preferred\n keyCode: 0, // fallback\n modifiers: {\n shiftKey: false,\n ctrlKey: false,\n altKey: false,\n metaKey: false\n }\n }\n };\n\n if (_keybindings[id]) {\n console.warn('warning: duplicate keybinding for \"' + id + '\"'); // eslint-disable-line no-console\n }\n\n _keybindings[id] = binding;\n\n var matches = arr[i].toLowerCase().match(/(?:(?:[^+\u21E7\u2303\u2325\u2318])+|[\u21E7\u2303\u2325\u2318]|\\+\\+|^\\+$)/g);\n for (var j = 0; j < matches.length; j++) {\n // Normalise matching errors\n if (matches[j] === '++') matches[j] = '+';\n\n if (matches[j] in utilKeybinding.modifierCodes) {\n var prop = utilKeybinding.modifierProperties[utilKeybinding.modifierCodes[matches[j]]];\n binding.event.modifiers[prop] = true;\n } else {\n binding.event.key = utilKeybinding.keys[matches[j]] || matches[j];\n if (matches[j] in utilKeybinding.keyCodes) {\n binding.event.keyCode = utilKeybinding.keyCodes[matches[j]];\n }\n }\n }\n }\n\n return keybinding;\n };\n\n\n return keybinding;\n}\n\n\n/*\n * See https://github.com/keithamus/jwerty\n */\n\nutilKeybinding.modifierCodes = {\n // Shift key, \u21E7\n '\u21E7': 16, shift: 16,\n // CTRL key, on Mac: \u2303\n '\u2303': 17, ctrl: 17,\n // ALT key, on Mac: \u2325 (Alt)\n '\u2325': 18, alt: 18, option: 18,\n // META, on Mac: \u2318 (CMD), on Windows (Win), on Linux (Super)\n '\u2318': 91, meta: 91, cmd: 91, 'super': 91, win: 91\n};\n\nutilKeybinding.modifierProperties = {\n 16: 'shiftKey',\n 17: 'ctrlKey',\n 18: 'altKey',\n 91: 'metaKey'\n};\n\nutilKeybinding.plusKeys = ['plus', 'ffplus', '=', 'ffequals', '\u2260', '\u00B1'];\nutilKeybinding.minusKeys = ['_', '-', 'ffminus', 'dash', '\u2013', '\u2014'];\n\n/* eslint-disable sort-keys */\nutilKeybinding.keys = {\n // Backspace key, on Mac: \u232B (Backspace)\n '\u232B': 'Backspace', backspace: 'Backspace',\n // Tab Key, on Mac: \u21E5 (Tab), on Windows \u21E5\u21E5\n '\u21E5': 'Tab', '\u21C6': 'Tab', tab: 'Tab',\n // Return key, \u21A9\n '\u21A9': 'Enter', '\u21B5': 'Enter', '\u23CE': 'Enter', 'return': 'Enter', enter: 'Enter', '\u2305': 'Enter',\n // Pause/Break key\n 'pause': 'Pause', 'pause-break': 'Pause',\n // Caps Lock key, \u21EA\n '\u21EA': 'CapsLock', caps: 'CapsLock', 'caps-lock': 'CapsLock',\n // Escape key, on Mac: \u238B, on Windows: Esc\n '\u238B': ['Escape', 'Esc'], escape: ['Escape', 'Esc'], esc: ['Escape', 'Esc'],\n // Space key\n space: [' ', 'Spacebar'],\n // Page-Up key, or pgup, on Mac: \u2196\n '\u2196': 'PageUp', pgup: 'PageUp', 'page-up': 'PageUp',\n // Page-Down key, or pgdown, on Mac: \u2198\n '\u2198': 'PageDown', pgdown: 'PageDown', 'page-down': 'PageDown',\n // END key, on Mac: \u21DF\n '\u21DF': 'End', end: 'End',\n // HOME key, on Mac: \u21DE\n '\u21DE': 'Home', home: 'Home',\n // Insert key, or ins\n ins: 'Insert', insert: 'Insert',\n // Delete key, on Mac: \u2326 (Delete)\n '\u2326': ['Delete', 'Del'], del: ['Delete', 'Del'], 'delete': ['Delete', 'Del'],\n // Left Arrow Key, or \u2190\n '\u2190': ['ArrowLeft', 'Left'], left: ['ArrowLeft', 'Left'], 'arrow-left': ['ArrowLeft', 'Left'],\n // Up Arrow Key, or \u2191\n '\u2191': ['ArrowUp', 'Up'], up: ['ArrowUp', 'Up'], 'arrow-up': ['ArrowUp', 'Up'],\n // Right Arrow Key, or \u2192\n '\u2192': ['ArrowRight', 'Right'], right: ['ArrowRight', 'Right'], 'arrow-right': ['ArrowRight', 'Right'],\n // Up Arrow Key, or \u2193\n '\u2193': ['ArrowDown', 'Down'], down: ['ArrowDown', 'Down'], 'arrow-down': ['ArrowDown', 'Down'],\n // odities, stuff for backward compatibility (browsers and code):\n // Num-Multiply, or *\n '*': ['*', 'Multiply'], star: ['*', 'Multiply'], asterisk: ['*', 'Multiply'], multiply: ['*', 'Multiply'],\n // Num-Plus or +\n '+': ['+', 'Add'], 'plus': ['+', 'Add'],\n // Num-Subtract, or -\n '-': ['-', 'Subtract'], subtract: ['-', 'Subtract'], 'dash': ['-', 'Subtract'],\n // Semicolon\n semicolon: ';',\n // = or equals\n equals: '=',\n // Comma, or ,\n comma: ',',\n // Period, or ., or full-stop\n period: '.', 'full-stop': '.',\n // Slash, or /, or forward-slash\n slash: '/', 'forward-slash': '/',\n // Tick, or `, or back-quote\n tick: '`', 'back-quote': '`',\n // Open bracket, or [\n 'open-bracket': '[',\n // Back slash, or \\\n 'back-slash': '\\\\',\n // Close bracket, or ]\n 'close-bracket': ']',\n // Apostrophe, or Quote, or '\n quote: '\\'', apostrophe: '\\'',\n // NUMPAD 0-9\n 'num-0': '0',\n 'num-1': '1',\n 'num-2': '2',\n 'num-3': '3',\n 'num-4': '4',\n 'num-5': '5',\n 'num-6': '6',\n 'num-7': '7',\n 'num-8': '8',\n 'num-9': '9',\n // F1-F25\n f1: 'F1',\n f2: 'F2',\n f3: 'F3',\n f4: 'F4',\n f5: 'F5',\n f6: 'F6',\n f7: 'F7',\n f8: 'F8',\n f9: 'F9',\n f10: 'F10',\n f11: 'F11',\n f12: 'F12',\n f13: 'F13',\n f14: 'F14',\n f15: 'F15',\n f16: 'F16',\n f17: 'F17',\n f18: 'F18',\n f19: 'F19',\n f20: 'F20',\n f21: 'F21',\n f22: 'F22',\n f23: 'F23',\n f24: 'F24',\n f25: 'F25'\n};\n\nutilKeybinding.keyCodes = {\n // Backspace key, on Mac: \u232B (Backspace)\n '\u232B': 8, backspace: 8,\n // Tab Key, on Mac: \u21E5 (Tab), on Windows \u21E5\u21E5\n '\u21E5': 9, '\u21C6': 9, tab: 9,\n // Return key, \u21A9\n '\u21A9': 13, '\u21B5': 13, '\u23CE': 13, 'return': 13, enter: 13, '\u2305': 13,\n // Pause/Break key\n 'pause': 19, 'pause-break': 19,\n // Caps Lock key, \u21EA\n '\u21EA': 20, caps: 20, 'caps-lock': 20,\n // Escape key, on Mac: \u238B, on Windows: Esc\n '\u238B': 27, escape: 27, esc: 27,\n // Space key\n space: 32,\n // Page-Up key, or pgup, on Mac: \u2196\n '\u2196': 33, pgup: 33, 'page-up': 33,\n // Page-Down key, or pgdown, on Mac: \u2198\n '\u2198': 34, pgdown: 34, 'page-down': 34,\n // END key, on Mac: \u21DF\n '\u21DF': 35, end: 35,\n // HOME key, on Mac: \u21DE\n '\u21DE': 36, home: 36,\n // Insert key, or ins\n ins: 45, insert: 45,\n // Delete key, on Mac: \u2326 (Delete)\n '\u2326': 46, del: 46, 'delete': 46,\n // Left Arrow Key, or \u2190\n '\u2190': 37, left: 37, 'arrow-left': 37,\n // Up Arrow Key, or \u2191\n '\u2191': 38, up: 38, 'arrow-up': 38,\n // Right Arrow Key, or \u2192\n '\u2192': 39, right: 39, 'arrow-right': 39,\n // Up Arrow Key, or \u2193\n '\u2193': 40, down: 40, 'arrow-down': 40,\n // odities, printing characters that come out wrong:\n // Firefox Equals\n 'ffequals': 61,\n // Num-Multiply, or *\n '*': 106, star: 106, asterisk: 106, multiply: 106,\n // Num-Plus or +\n '+': 107, 'plus': 107,\n // Num-Subtract, or -\n '-': 109, subtract: 109,\n // Vertical Bar / Pipe\n '|': 124,\n // Firefox Plus\n 'ffplus': 171,\n // Firefox Minus\n 'ffminus': 173,\n // Semicolon\n ';': 186, semicolon: 186,\n // = or equals\n '=': 187, 'equals': 187,\n // Comma, or ,\n ',': 188, comma: 188,\n // Dash / Underscore key\n 'dash': 189,\n // Period, or ., or full-stop\n '.': 190, period: 190, 'full-stop': 190,\n // Slash, or /, or forward-slash\n '/': 191, slash: 191, 'forward-slash': 191,\n // Tick, or `, or back-quote\n '`': 192, tick: 192, 'back-quote': 192,\n // Open bracket, or [\n '[': 219, 'open-bracket': 219,\n // Back slash, or \\\n '\\\\': 220, 'back-slash': 220,\n // Close bracket, or ]\n ']': 221, 'close-bracket': 221,\n // Apostrophe, or Quote, or '\n '\\'': 222, quote: 222, apostrophe: 222\n};\n\n// NUMPAD 0-9\nvar i = 95, n = 0;\nwhile (++i < 106) {\n utilKeybinding.keyCodes['num-' + n] = i;\n ++n;\n}\n\n// 0-9\ni = 47; n = 0;\nwhile (++i < 58) {\n utilKeybinding.keyCodes[n] = i;\n ++n;\n}\n\n// F1-F25\ni = 111; n = 1;\nwhile (++i < 136) {\n utilKeybinding.keyCodes['f' + n] = i;\n ++n;\n}\n\n// a-z\ni = 64;\nwhile (++i < 91) {\n utilKeybinding.keyCodes[String.fromCharCode(i).toLowerCase()] = i;\n}\n/* eslint-enable sort-keys */\n", "export function utilObjectOmit(obj, omitKeys) {\n return Object.keys(obj).reduce(function(result, key) {\n if (omitKeys.indexOf(key) === -1) {\n result[key] = obj[key]; // keep\n }\n return result;\n }, {});\n}\n\n/**\n * @template T\n * @typedef {{ [key: string]: { [value: string]: T } }} TagDictionary\n */\n\n/**\n * searches a dictionary for a match, such as `osmOneWayForwardTags`,\n * `osmAreaKeysExceptions`, etc.\n * @template T\n * @param {Tags} tags\n * @param {TagDictionary} tagDictionary\n * @returns {T | undefined}\n */\nexport function utilCheckTagDictionary(tags, tagDictionary) {\n for (const key in tags) {\n const value = tags[key];\n if (tagDictionary[key] && value in tagDictionary[key]) {\n return tagDictionary[key][value];\n }\n }\n return undefined;\n}\n\n/**\n * converts every value in an object to a string, if\n * it's not already a string.\n * @param {Record} object\n */\nexport function stringifyProperties(object) {\n /** @type {Tags} */\n const tags = {};\n for (const key in object) {\n switch (typeof object[key]) {\n case 'undefined':\n break; // skip property\n case 'string':\n tags[key] = object[key];\n break;\n default:\n tags[key] = JSON.stringify(\n object[key],\n (_, value) => typeof value === 'bigint' ? value.toString() : value\n );\n }\n }\n return tags;\n}\n", "// Copies a variable number of methods from source to target.\n/**\n * @template T\n * @template S\n * @template {keyof S} Args\n * @param {T} target\n * @param {S} source\n * @param {...Args} args\n * @returns {T & Pick}\n */\nexport function utilRebind(target, source, ...args) {\n for (const method of args) {\n target[method] = d3_rebind(target, source, source[method]);\n }\n return target;\n}\n\n// Method is assumed to be a standard D3 getter-setter:\n// If passed with no arguments, gets the value.\n// If passed with arguments, sets the value and returns the target.\nfunction d3_rebind(target, source, method) {\n return function() {\n var value = method.apply(source, arguments);\n return value === source ? target : value;\n };\n}\n", "// A per-domain session mutex backed by a cookie and dead man's\n// switch. If the session crashes, the mutex will auto-release\n// after 5 seconds.\n\n// This accepts a string and returns an object that complies with utilSessionMutexType\nexport function utilSessionMutex(name) {\n var mutex = {};\n var intervalID;\n\n function renew() {\n // in unit tests, we need to abort if the test has already completed\n if (typeof window === 'undefined') return;\n\n var expires = new Date();\n expires.setSeconds(expires.getSeconds() + 5);\n document.cookie = name + '=1; expires=' + expires.toUTCString() + '; sameSite=strict';\n }\n\n mutex.lock = function () {\n if (intervalID) return true;\n var cookie = document.cookie.replace(new RegExp('(?:(?:^|.*;)\\\\s*' + name + '\\\\s*\\\\=\\\\s*([^;]*).*$)|^.*$'), '$1');\n if (cookie) return false;\n renew();\n intervalID = window.setInterval(renew, 4000);\n return true;\n };\n\n mutex.unlock = function () {\n if (!intervalID) return;\n document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; sameSite=strict';\n clearInterval(intervalID);\n intervalID = null;\n };\n\n mutex.locked = function () {\n return !!intervalID;\n };\n\n return mutex;\n}\n", "import { range as d3_range } from 'd3-array';\nimport { clamp } from 'es-toolkit/compat';\n\nimport { geoExtent, geoScaleToZoom } from '../geo';\n\n/** @typedef {{ id: string; xyz: import('../geo/vector').Vec3; extent: geoExtent }} Tile */\n\nexport function utilTiler() {\n var _size = [256, 256];\n var _scale = 256;\n var _tileSize = 256;\n var _zoomExtent = [0, 20];\n var _translate = [_size[0] / 2, _size[1] / 2];\n var _margin = 0;\n var _skipNullIsland = false;\n\n\n function nearNullIsland(tile) {\n var x = tile[0];\n var y = tile[1];\n var z = tile[2];\n if (z >= 7) {\n var center = Math.pow(2, z - 1);\n var width = Math.pow(2, z - 6);\n var min = center - (width / 2);\n var max = center + (width / 2) - 1;\n return x >= min && x <= max && y >= min && y <= max;\n }\n return false;\n }\n\n\n function tiler() {\n var z = geoScaleToZoom(_scale / (2 * Math.PI), _tileSize);\n var z0 = clamp(Math.round(z), _zoomExtent[0], _zoomExtent[1]);\n var tileMin = 0;\n var tileMax = Math.pow(2, z0) - 1;\n var log2ts = Math.log(_tileSize) * Math.LOG2E;\n var k = Math.pow(2, z - z0 + log2ts);\n var origin = [\n (_translate[0] - _scale / 2) / k,\n (_translate[1] - _scale / 2) / k\n ];\n\n var cols = d3_range(\n clamp(Math.floor(-origin[0]) - _margin, tileMin, tileMax + 1),\n clamp(Math.ceil(_size[0] / k - origin[0]) + _margin, tileMin, tileMax + 1)\n );\n var rows = d3_range(\n clamp(Math.floor(-origin[1]) - _margin, tileMin, tileMax + 1),\n clamp(Math.ceil(_size[1] / k - origin[1]) + _margin, tileMin, tileMax + 1)\n );\n\n var tiles = [];\n for (var i = 0; i < rows.length; i++) {\n var y = rows[i];\n for (var j = 0; j < cols.length; j++) {\n var x = cols[j];\n\n if (i >= _margin && i <= rows.length - _margin &&\n j >= _margin && j <= cols.length - _margin) {\n tiles.unshift([x, y, z0]); // tiles in view at beginning\n } else {\n tiles.push([x, y, z0]); // tiles in margin at the end\n }\n }\n }\n\n tiles.translate = origin;\n tiles.scale = k;\n\n return tiles;\n }\n\n\n /**\n * getTiles() returns an array of tiles that cover the map view\n * @returns {false | Tile[]}\n */\n tiler.getTiles = function(projection) {\n var origin = [\n projection.scale() * Math.PI - projection.translate()[0],\n projection.scale() * Math.PI - projection.translate()[1]\n ];\n\n this\n .size(projection.clipExtent()[1])\n .scale(projection.scale() * 2 * Math.PI)\n .translate(projection.translate());\n\n var tiles = tiler();\n var ts = tiles.scale;\n\n return tiles\n .map(function(tile) {\n if (_skipNullIsland && nearNullIsland(tile)) {\n return false;\n }\n var x = tile[0] * ts - origin[0];\n var y = tile[1] * ts - origin[1];\n return {\n id: tile.toString(),\n xyz: tile,\n extent: geoExtent(\n projection.invert([x, y + ts]),\n projection.invert([x + ts, y])\n )\n };\n }).filter(Boolean);\n };\n\n\n /**\n * getGeoJSON() returns a FeatureCollection for debugging tiles\n */\n tiler.getGeoJSON = function(projection) {\n var features = tiler.getTiles(projection).map(function(tile) {\n return {\n type: 'Feature',\n properties: {\n id: tile.id,\n name: tile.id\n },\n geometry: {\n type: 'Polygon',\n coordinates: [ tile.extent.polygon() ]\n }\n };\n });\n\n return {\n type: 'FeatureCollection',\n features: features\n };\n };\n\n\n tiler.tileSize = function(val) {\n if (!arguments.length) return _tileSize;\n _tileSize = val;\n return tiler;\n };\n\n\n /** @type {GetSet} */\n tiler.zoomExtent = function(val) {\n if (!arguments.length) return _zoomExtent;\n _zoomExtent = val;\n return tiler;\n };\n\n\n tiler.size = function(val) {\n if (!arguments.length) return _size;\n _size = val;\n return tiler;\n };\n\n\n tiler.scale = function(val) {\n if (!arguments.length) return _scale;\n _scale = val;\n return tiler;\n };\n\n\n tiler.translate = function(val) {\n if (!arguments.length) return _translate;\n _translate = val;\n return tiler;\n };\n\n\n // number to extend the rows/columns beyond those covering the viewport\n tiler.margin = function(val) {\n if (!arguments.length) return _margin;\n _margin = +val;\n return tiler;\n };\n\n\n /** @type {GetSet} */\n tiler.skipNullIsland = function(val) {\n if (!arguments.length) return _skipNullIsland;\n _skipNullIsland = val;\n return tiler;\n };\n\n\n return tiler;\n}\n", "export function utilTriggerEvent(target, type, eventProperties) {\n target.each(function() {\n const event = new Event(type, {\n bubbles: true,\n cancelable: true\n });\n Object.assign(event, eventProperties);\n this.dispatchEvent(event);\n });\n}\n", "import { clamp } from 'es-toolkit/compat';\n\nimport { t, localizer } from '../core/localizer';\nimport type { Vec2, Vec3 } from '../geo/vector';\n\nvar OSM_PRECISION = 7;\n\n/**\n * Returns a localized representation of the given length measurement.\n *\n * @param {Number} m area in meters\n * @param {Boolean} isImperial true for U.S. customary units; false for metric\n */\nexport function displayLength(m: number, isImperial: boolean) {\n var d = m * (isImperial ? 3.28084 : 1);\n var unit;\n\n if (isImperial) {\n if (d >= 5280) {\n d /= 5280;\n unit = 'miles';\n } else {\n unit = 'feet';\n }\n } else {\n if (d >= 1000) {\n d /= 1000;\n unit = 'kilometers';\n } else {\n unit = 'meters';\n }\n }\n\n return t('units.' + unit, {\n quantity: d.toLocaleString(localizer.localeCode(), {\n maximumSignificantDigits: 4\n })\n });\n}\n\n/**\n * Returns a localized representation of the given area measurement.\n *\n * @param {Number} m2 area in square meters\n * @param {Boolean} isImperial true for U.S. customary units; false for metric\n */\nexport function displayArea(m2: number, isImperial: boolean) {\n var locale = localizer.localeCode();\n var d = m2 * (isImperial ? 10.7639111056 : 1);\n var d1, d2, area;\n\n var unit1 = ''; // eslint-disable-line no-useless-assignment\n var unit2 = '';\n\n if (isImperial) {\n if (d >= 6969600) { // > 0.25mi\u00B2 show mi\u00B2\n d1 = d / 27878400;\n unit1 = 'square_miles';\n } else {\n d1 = d;\n unit1 = 'square_feet';\n }\n\n if (d > 4356 && d < 43560000) { // 0.1 - 1000 acres\n d2 = d / 43560;\n unit2 = 'acres';\n }\n\n } else {\n if (d >= 250000) { // > 0.25km\u00B2 show km\u00B2\n d1 = d / 1000000;\n unit1 = 'square_kilometers';\n } else {\n d1 = d;\n unit1 = 'square_meters';\n }\n\n if (d > 1000 && d < 10000000) { // 0.1 - 1000 hectares\n d2 = d / 10000;\n unit2 = 'hectares';\n }\n }\n\n area = t('units.' + unit1, {\n quantity: d1.toLocaleString(locale, {\n maximumSignificantDigits: 4\n })\n });\n\n if (d2) {\n return t('units.area_pair', {\n area1: area,\n area2: t('units.' + unit2, {\n quantity: d2.toLocaleString(locale, {\n maximumSignificantDigits: 2\n })\n })\n });\n } else {\n return area;\n }\n}\n\nfunction wrap(x: number, min: number, max: number) {\n var d = max - min;\n return ((x - min) % d + d) % d + min;\n}\n\nfunction roundToDecimal (target: number, decimalPlace: number) {\n target = Number(target);\n decimalPlace = Number(decimalPlace);\n const factor = Math.pow(10, decimalPlace);\n return Math.round(target * factor) / factor;\n}\n\nfunction displayCoordinate(deg: number, pos: string, neg: string) {\n var displayCoordinate;\n var locale = localizer.localeCode();\n\n var degreesFloor = Math.floor(Math.abs(deg));\n var min = (Math.abs(deg) - degreesFloor) * 60;\n var minFloor = Math.floor(min);\n var sec = (min - minFloor) * 60;\n\n\n // if you input 45\u00B0,90\u00B00'0.5\" , sec should be 0.5 instead 0.499999\u2026\n // in order to mitigate precision errors after calculating, round two time\n // 0.499999\u2026 => 0.5\n var fix = roundToDecimal(sec, 8);\n // 0.5 => 1\n var secRounded = roundToDecimal(fix, 0);\n\n if (secRounded === 60) {\n secRounded = 0;\n minFloor += 1;\n if (minFloor === 60) {\n minFloor = 0;\n degreesFloor += 1;\n }\n }\n displayCoordinate =\n t('units.arcdegrees', {\n quantity: degreesFloor.toLocaleString(locale)\n }) +\n (minFloor !== 0 || secRounded !== 0 ?\n t('units.arcminutes', {\n quantity: minFloor.toLocaleString(locale)\n }) : '') +\n (secRounded !== 0 ?\n t('units.arcseconds', {\n quantity: secRounded.toLocaleString(locale)\n }) : '' );\n\n if (deg === 0) {\n return displayCoordinate;\n } else {\n return t('units.coordinate', {\n coordinate: displayCoordinate,\n direction: t('units.' + (deg > 0 ? pos : neg))\n });\n }\n}\n\n/**\n * Returns given coordinate pair in degree-minute-second format.\n *\n * @param {Array} coord longitude and latitude\n */\nexport function dmsCoordinatePair(coord: Vec2) {\n return t('units.coordinate_pair', {\n latitude: displayCoordinate(clamp(coord[1], -90, 90), 'north', 'south'),\n longitude: displayCoordinate(wrap(coord[0], -180, 180), 'east', 'west')\n });\n}\n\n/**\n * Returns the given coordinate pair in decimal format.\n * note: unlocalized to avoid comma ambiguity - see #4765\n *\n * @param {Array} coord longitude and latitude\n */\nexport function decimalCoordinatePair(coord: Vec2) {\n return t('units.coordinate_pair', {\n latitude: clamp(coord[1], -90, 90).toFixed(OSM_PRECISION),\n longitude: wrap(coord[0], -180, 180).toFixed(OSM_PRECISION)\n });\n}\n\n// Return the parsed value that @mapbox/sexagesimal can't parse\n// return value format : [D, D] ex:[ 35.1861, 136.83161 ]\nexport function dmsMatcher(q: string, _localeCode = undefined) {\n const matchers: {\n condition: RegExp;\n parser(q: string): Vec2 | Vec3 | false\n }[] = [\n // D M SS , D M SS ex: 35 11 10.1 , 136 49 53.8\n {\n condition: /^\\s*(-?)\\s*(\\d+)\\s+(\\d+)\\s+(\\d+\\.?\\d*)\\s*\\,\\s*(-?)\\s*(\\d+)\\s+(\\d+)\\s+(\\d+\\.?\\d*)\\s*$/,\n parser: function(q) {\n const match = this.condition.exec(q)!;\n const lat = (+match[2]) + (+match[3]) / 60 + (+match[4]) / 3600;\n const lng = (+match[6]) + (+match[7]) / 60 + (+match[8]) / 3600;\n const isNegLat = match[1] === '-' ? -lat : lat;\n const isNegLng = match[5] === '-' ? -lng : lng;\n return [isNegLat, isNegLng];\n }\n },\n // D MM , D MM ex: 35 11.1683 , 136 49.8966\n {\n condition: /^\\s*(-?)\\s*(\\d+)\\s+(\\d+\\.?\\d*)\\s*\\,\\s*(-?)\\s*(\\d+)\\s+(\\d+\\.?\\d*)\\s*$/,\n parser: function(q) {\n const match = this.condition.exec(q)!;\n const lat = +match[2] + (+match[3]) / 60;\n const lng = +match[5] + (+match[6]) / 60;\n const isNegLat = match[1] === '-' ? -lat : lat;\n const isNegLng = match[4] === '-' ? -lng : lng;\n return [isNegLat, isNegLng];\n }\n },\n // D/D ex: 46.112785/72.921033\n {\n condition: /^\\s*(-?\\d+\\.?\\d*)\\s*\\/\\s*(-?\\d+\\.?\\d*)\\s*$/,\n parser: function(q) {\n const match = this.condition.exec(q)!;\n return [+match[1], +match[2]];\n }\n },\n // zoom/x/y ex: 2/1.23/34.44\n {\n condition: /^\\s*(\\d+\\.?\\d*)\\s*\\/\\s*(-?\\d+\\.?\\d*)\\s*\\/\\s*(-?\\d+\\.?\\d*)\\s*$/,\n parser: function(q) {\n const match = this.condition.exec(q)!;\n const lat = +match[2];\n const lng = +match[3];\n const zoom = +match[1];\n return [lat, lng, zoom];\n }\n },\n // x/y , x, y , x y where x and y are localized floats, e.g. in German locale: 49,4109399, 8,7147086\n {\n condition: { test: q => !!localizedNumberCoordsParser(q) },\n parser: localizedNumberCoordsParser\n }\n ];\n function localizedNumberCoordsParser(q: string): Vec2 | false {\n const parseLocaleFloat = localizer.floatParser(_localeCode || localizer.localeCode());\n let parts = q.split(/,?\\s+|\\s*[\\/\\\\]\\s*/);\n if (parts.length !== 2) return false;\n const lat = parseLocaleFloat(parts[0]);\n const lng = parseLocaleFloat(parts[1]);\n if (isNaN(lat) || isNaN(lng)) return false;\n return [lat, lng];\n }\n for (const matcher of matchers) {\n if (matcher.condition.test(q)){\n return matcher.parser(q);\n }\n }\n return null;\n}\n", "export { utilAesEncrypt } from './aes';\nexport { utilAesDecrypt } from './aes';\n\nexport { utilArrayChunk } from './array';\nexport { utilArrayDifference } from './array';\nexport { utilArrayFlatten } from './array';\nexport { utilArrayGroupBy } from './array';\nexport { utilArrayIdentical } from './array';\nexport { utilArrayIntersection } from './array';\nexport { utilArrayUnion } from './array';\nexport { utilArrayUniq } from './array';\nexport { utilArrayUniqBy } from './array';\n\nexport { utilAsyncMap } from './util';\nexport { utilCleanTags } from './clean_tags';\nexport { utilCombinedTags } from './util';\nexport { utilDatesOverlap } from './ohm_date';\nexport { utilDeepMemberSelector } from './util';\nexport { utilDetect } from './detect';\nexport { utilDisplayName } from './util';\nexport { utilDisplayNameForPath } from './util';\nexport { utilDisplayType } from './util';\nexport { utilEntityRoot } from './util';\nexport { utilEditDistance } from './util';\nexport { utilEDTFFromOSMDateString } from './ohm_date';\nexport { utilEntityAndDeepMemberIDs } from './util';\nexport { utilEntityOrMemberSelector } from './util';\nexport { utilEntityOrDeepMemberSelector } from './util';\nexport { utilEntitySelector } from './util';\nexport { utilFastMouse } from './util';\nexport { utilFunctor } from './util';\nexport { utilGetAllNodes } from './util';\nexport { utilGetSetValue } from './get_set_value';\nexport { utilHashcode } from './util';\nexport { utilHighlightEntities } from './util';\nexport { utilKeybinding } from './keybinding';\nexport { utilNormalizeDateString } from './ohm_date';\nexport { utilNoAuto } from './util';\nexport { utilObjectOmit, utilCheckTagDictionary, stringifyProperties } from './object';\nexport { utilCompareIDs } from './util';\nexport { utilOldestID } from './util';\nexport { utilPrefixCSSProperty } from './util';\nexport { utilPrefixDOMProperty } from './util';\nexport { utilQsString } from './util';\nexport { utilRebind } from './rebind';\nexport { utilSafeClassName } from './util';\nexport { utilSetTransform } from './util';\nexport { utilSessionMutex } from './session_mutex';\nexport { utilStringQs } from './util';\nexport { utilTagDiff } from './util';\nexport { utilTagText } from './util';\nexport { utilTiler } from './tiler';\nexport { utilTotalExtent } from './util';\nexport { utilTriggerEvent } from './trigger_event';\nexport { utilUnicodeCharsCount } from './util';\nexport { utilUnicodeCharsTruncated } from './util';\nexport { utilUniqueDomId } from './util';\nexport { utilWrap } from './util';\nexport { utilCleanOsmString } from './util';\n\nexport { dmsCoordinatePair } from './units';\nexport { dmsMatcher } from './units';\n", "import { geoEdgeEqual } from '../geo';\nimport { utilArrayIntersection } from '../util';\n\n\nexport function actionAddMidpoint(midpoint, node) {\n return function(graph) {\n graph = graph.replace(node.move(midpoint.loc));\n\n var parents = utilArrayIntersection(\n graph.parentWays(graph.entity(midpoint.edge[0])),\n graph.parentWays(graph.entity(midpoint.edge[1]))\n );\n\n parents.forEach(function(way) {\n for (var i = 0; i < way.nodes.length - 1; i++) {\n if (geoEdgeEqual([way.nodes[i], way.nodes[i + 1]], midpoint.edge)) {\n graph = graph.replace(graph.entity(way.id).addNode(node.id, i + 1));\n\n // Add only one midpoint on doubled-back segments,\n // turning them into self-intersections.\n return;\n }\n }\n });\n\n return graph;\n };\n}\n", "// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as\nexport function actionAddVertex(wayId, nodeId, index) {\n return function(graph) {\n return graph.replace(graph.entity(wayId).addNode(nodeId, index));\n };\n}\n", "export function actionChangeMember(relationId, member, memberIndex) {\n return function(graph) {\n return graph.replace(graph.entity(relationId).updateMember(member, memberIndex));\n };\n}\n", "import { utilArrayDifference, utilObjectOmit } from '../util';\n\nexport function actionChangePreset(entityID, oldPreset, newPreset, skipFieldDefaults) {\n return function action(graph) {\n var entity = graph.entity(entityID);\n var geometry = entity.geometry(graph);\n var tags = entity.tags;\n const loc = entity.extent(graph).center();\n\n // preserve tags that the new preset might care about, if any\n var preserveKeys;\n if (newPreset) {\n preserveKeys = [];\n if (newPreset.addTags) {\n preserveKeys = preserveKeys.concat(Object.keys(newPreset.addTags));\n }\n if (oldPreset && !oldPreset.id.startsWith(newPreset.id)) {\n // only if old preset is not a sub-preset of the new one:\n // preserve tags for which the new preset has a field\n // https://github.com/openstreetmap/iD/issues/9372\n newPreset.fields(loc).concat(newPreset.moreFields(loc))\n .filter(f => f.matchGeometry(geometry))\n .flatMap(f => f.allKeys())\n .filter(Boolean)\n .forEach(key => preserveKeys.push(key));\n }\n\n if (oldPreset && (oldPreset.id !== newPreset.id)) {\n // 'field-keys' are keys used by fields (different to the keys used by preset itself)\n const oldPresetFieldKeys = [\n ...oldPreset.fields(loc),\n ...oldPreset.moreFields(loc)\n ].flatMap(f => f.allKeys());\n\n // field-keys used by the old preset but not the new preset\n const fieldKeysToRemove = utilArrayDifference(oldPresetFieldKeys, preserveKeys);\n tags = utilObjectOmit(tags, fieldKeysToRemove);\n }\n }\n if (oldPreset) tags = oldPreset.unsetTags(tags, geometry, preserveKeys, false, loc);\n if (newPreset) tags = newPreset.setTags(tags, geometry, skipFieldDefaults, loc);\n\n return graph.replace(entity.update({tags: tags}));\n };\n}\n", "export function actionChangeTags(entityId, tags) {\n return function(graph) {\n var entity = graph.entity(entityId);\n return graph.replace(entity.update({tags: tags}));\n };\n}\n", "import { merge } from 'es-toolkit/compat';\nimport { getLuma } from '../util/util';\n\nconst uninterestingKeys = new Set([\n 'attribution',\n 'created_by',\n 'import_uuid',\n 'geobase:datasetName',\n 'geobase:uuid',\n 'KSJ2:curve_id',\n 'KSJ2:lat',\n 'KSJ2:long',\n 'lat',\n 'latitude',\n 'lon',\n 'longitude',\n 'source',\n 'source_ref',\n 'odbl',\n 'odbl:note'\n]);\nconst uninterestingKeyRegex = /^(source(_ref)?|tiger):/;\n\n/**\n * Returns whether the given OSM tag key is potentially \"interesting\".\n * For example, some tags are deemed not interesting because the respective tag is\n * considered \"discardable\".\n *\n * @param {string} key the key to test\n * @returns {boolean}\n */\nexport function osmIsInterestingTag(key) {\n if (uninterestingKeys.has(key)) return false;\n if (uninterestingKeyRegex.test(key)) return false;\n return true;\n}\n\nexport const osmLifecyclePrefixes = {\n // nonexistent, might be built\n proposed: true, planned: true,\n // under maintenance or between groundbreaking and opening\n construction: true,\n // existent but not functional\n disused: true,\n // dilapidated to nonexistent\n abandoned: true, was: true,\n // nonexistent, still may appear in imagery\n dismantled: true, razed: true, demolished: true, destroyed: true, removed: true, obliterated: true,\n // existent occasionally, e.g. stormwater drainage basin\n intermittent: true\n};\n\n/** @param {string} key */\nexport function osmRemoveLifecyclePrefix(key) {\n const keySegments = key.split(':');\n if (keySegments.length === 1) return key;\n\n if (keySegments[0] in osmLifecyclePrefixes) {\n return key.slice(keySegments[0].length + 1);\n }\n\n return key;\n}\n\nexport var osmAreaKeys = {};\nexport function osmSetAreaKeys(value) {\n osmAreaKeys = value;\n}\n\n// `highway` and `railway` are typically linear features, but there\n// are a few exceptions that should be treated as areas, even in the\n// absence of a proper `area=yes` or `areaKeys` tag.. see #4194\n// similarly, some tags are both used as a primary key for area features,\n// but also as an attribute tag for linear features (e.g. `emergency=yes`)\nexport var osmAreaKeysExceptions = {\n highway: {\n elevator: true,\n rest_area: true,\n services: true\n },\n public_transport: {\n platform: true\n },\n railway: {\n platform: true,\n roundhouse: true,\n station: true,\n traverser: true,\n turntable: true,\n wash: true,\n ventilation_shaft: true\n },\n waterway: {\n dam: true\n },\n amenity: {\n bicycle_parking: true\n },\n emergency: {\n yes: false,\n no: false,\n private: false,\n designated: false,\n destination: false,\n official: false\n }\n};\n\n// returns an object with the tag from `tags` that implies an area geometry, if any\nexport function osmTagSuggestingArea(tags) {\n if (tags.area === 'yes') return { area: 'yes' };\n if (tags.area === 'no') return null;\n\n var returnTags = {};\n for (var realKey in tags) {\n const key = osmRemoveLifecyclePrefix(realKey);\n if (key in osmAreaKeysExceptions && osmAreaKeysExceptions[key][tags[realKey]] === false) {\n continue;\n }\n if (key in osmAreaKeys && !(tags[realKey] in osmAreaKeys[key])) {\n returnTags[realKey] = tags[realKey];\n return returnTags;\n }\n if (key in osmAreaKeysExceptions && tags[realKey] in osmAreaKeysExceptions[key]) {\n returnTags[realKey] = tags[realKey];\n return returnTags;\n }\n }\n return null;\n}\n\nexport var osmLineTags = {};\nexport function osmSetLineTags(value) {\n osmLineTags = value;\n}\n\n// Tags that indicate a node can be a standalone point\n// e.g. { amenity: { bar: true, parking: true, ... } ... }\nexport var osmPointTags = {};\nexport function osmSetPointTags(value) {\n osmPointTags = value;\n}\n// Tags that indicate a node can be part of a way\n// e.g. { amenity: { parking: true, ... }, highway: { stop: true ... } ... }\nexport var osmVertexTags = {};\nexport function osmSetVertexTags(value) {\n osmVertexTags = value;\n}\n\nexport function osmNodeGeometriesForTags(nodeTags) {\n var geometries = {};\n for (var key in nodeTags) {\n if (osmPointTags[key] &&\n (osmPointTags[key]['*'] || osmPointTags[key][nodeTags[key]])) {\n geometries.point = true;\n }\n if (osmVertexTags[key] &&\n (osmVertexTags[key]['*'] || osmVertexTags[key][nodeTags[key]])) {\n geometries.vertex = true;\n }\n // break early if both are already supported\n if (geometries.point && geometries.vertex) break;\n }\n return geometries;\n}\n\nexport const osmOneWayForwardTags = {\n 'aerialway': {\n 'chair_lift': true,\n 'drag_lift': true,\n 'j-bar': true,\n 'magic_carpet': true,\n 'mixed_lift': true,\n 'platter': true,\n 'rope_tow': true,\n 't-bar': true,\n 'zip_line': true\n },\n 'conveying': {\n 'forward': true,\n },\n 'highway': {\n 'motorway': true\n },\n 'junction': {\n 'circular': true,\n 'roundabout': true\n },\n 'man_made': {\n 'goods_conveyor': true,\n 'piste:halfpipe': true\n },\n 'oneway': {\n 'yes': true,\n },\n 'piste:type': {\n 'downhill': true,\n 'sled': true,\n 'yes': true\n },\n 'seamark:type': {\n 'two-way_route': true,\n 'recommended_traffic_lane': true,\n 'separation_lane': true,\n 'separation_roundabout': true\n },\n 'waterway': {\n 'canal': true,\n 'ditch': true,\n 'drain': true,\n 'fish_pass': true,\n 'flowline': true,\n 'pressurised': true,\n 'river': true,\n 'spillway': true,\n 'stream': true,\n 'tidal_channel': true\n }\n};\nexport const osmOneWayBackwardTags = {\n 'conveying': {\n 'backward': true,\n },\n 'oneway': {\n '-1': true,\n },\n};\nexport const osmOneWayBiDirectionalTags = {\n 'conveying': {\n 'reversible': true,\n },\n 'oneway': {\n 'alternating': true,\n 'reversible': true,\n },\n};\nexport const osmOneWayTags = merge(\n osmOneWayForwardTags,\n osmOneWayBackwardTags,\n osmOneWayBiDirectionalTags,\n);\n\n// solid and smooth surfaces akin to the assumed default road surface in OSM\nexport var osmPavedTags = {\n 'surface': {\n 'paved': true,\n 'asphalt': true,\n 'concrete': true,\n 'chipseal': true,\n 'concrete:lanes': true,\n 'concrete:plates': true,\n 'tiles': true\n },\n 'tracktype': {\n 'grade1': true\n }\n};\n\n// solid, if somewhat uncommon surfaces with a high range of smoothness\nexport var osmSemipavedTags = {\n 'surface': {\n 'bricks': true,\n 'cobblestone': true,\n 'cobblestone:flattened': true,\n 'unhewn_cobblestone': true,\n 'sett': true,\n 'paving_stones': true,\n 'grass_paver': true,\n 'metal': true,\n 'metal_grid': true,\n 'fibre_reinforced_polymer_grate': true,\n 'wood': true\n }\n};\n\nexport var osmRightSideIsInsideTags = {\n 'natural': {\n 'cliff': true,\n 'coastline': 'coastline'\n },\n 'barrier': {\n 'retaining_wall': true,\n 'kerb': true,\n 'guard_rail': 'guard_rail',\n 'city_wall': true,\n },\n 'man_made': {\n 'embankment': true,\n 'quay': true\n },\n 'waterway': {\n 'weir': true\n }\n};\n\n// \"highway\" tag values for pedestrian or vehicle right-of-ways that make up the routable network\n// (does not include `raceway`)\nexport var osmRoutableHighwayTagValues = {\n motorway: true, trunk: true, primary: true, secondary: true, tertiary: true, residential: true,\n motorway_link: true, trunk_link: true, primary_link: true, secondary_link: true, tertiary_link: true,\n unclassified: true, road: true, service: true, track: true, living_street: true, bus_guideway: true, busway: true,\n path: true, footway: true, cycleway: true, bridleway: true, pedestrian: true, corridor: true, steps: true, ladder: true\n};\n/** aeroway tags that are treated as routable for aircraft */\nexport const osmRoutableAerowayTags = {\n runway: true, taxiway: true\n};\n// \"highway\" tag values that generally do not allow motor vehicles\nexport var osmPathHighwayTagValues = {\n path: true, footway: true, cycleway: true, bridleway: true, pedestrian: true, corridor: true, steps: true, ladder: true\n};\n\n// \"railway\" tag values representing existing railroad tracks (purposely does not include 'abandoned')\nexport var osmRailwayTrackTagValues = {\n rail: true, light_rail: true, tram: true, subway: true,\n monorail: true, funicular: true, miniature: true, narrow_gauge: true,\n disused: true, preserved: true\n};\n\n// \"waterway\" tag values for line features representing water flow\nexport var osmFlowingWaterwayTagValues = {\n canal: true, ditch: true, drain: true, fish_pass: true, flowline: true, river: true, stream: true, tidal_channel: true\n};\n\n// Tag values that represent actual land use (areas)\nexport var osmLanduseTags = {\n 'amenity': {\n 'bicycle_parking': true,\n 'college': true,\n 'grave_yard': true,\n 'hospital': true,\n 'marketplace': true,\n 'motorcycle_parking': true,\n 'parking': true,\n 'place_of_worship': true,\n 'prison': true,\n 'school': true,\n 'university': true\n },\n 'landuse': true,\n 'leisure': true,\n 'natural': true\n};\n\n// Tags which values should be considered case sensitive when offering tag suggestions\nexport const allowUpperCaseTagValues = /network|taxon|genus|species|brand|grape_variety|royal_cypher|listed_status|booth|rating|stars|:output|_hours|_times|_ref|manufacturer|country|target|brewery|cai_scale|traffic_sign/;\n\n// Returns whether a `colour` tag value looks like a valid color we can display\nexport function isColorValid(value) {\n if (!value) return false;\n if (!value.match(/^(#([0-9a-fA-F]{3}){1,2}|\\w+)$/)) {\n // OSM only supports hex or named colors\n return false;\n }\n if (!CSS.supports('color', value) || ['unset', 'inherit', 'initial', 'revert'].includes(value)) {\n // see https://stackoverflow.com/a/68217760/1627467\n return false;\n }\n return true;\n}\n\nexport function getRelationColor(tags, fallback) {\n let color, textColor;\n if (tags['ref:colour']) color = tags['ref:colour'];\n else if (tags.colour) color = tags.colour;\n const isValid = isColorValid(color);\n if (!isValid) color = fallback;\n if (tags['ref:colour_tx']) textColor = tags['ref:colour_tx'];\n if (!isColorValid(textColor)) textColor = getLuma(color) > 165 ? '#000' : '#fff';\n\n return {\n isValid,\n color,\n textColor\n };\n}\n\n// https://wiki.openstreetmap.org/wiki/Special:WhatLinksHere/Property:P44\nexport const osmMutuallyExclusiveTagPairs = [\n ['noname', 'name'],\n ['noref', 'ref'],\n ['nohousenumber', 'addr:housenumber'],\n ['noaddress', 'addr:housenumber'],\n ['noaddress', 'addr:housename'],\n ['noaddress', 'addr:unit'],\n ['addr:nostreet', 'addr:street']\n];\n\nexport const osmTimelessFeatureTagValues = {\n wood: true, wetland: true, beach: true, cave_entrance: true, peak: true, cliff: true, coastline: true, tree_row: true,\n water: true, scrub: true, grassland: true, heath: true, bare_rock: true, glacier: true, stream: true, river: true, pond: true, basin: true, lake: true\n};\n\n/**\n * @param {Tags} vertexTags @param {Tags} wayTags\n * returns true if iD should render the `direction` tag for\n * this vertex+way combination.\n */\nexport function osmShouldRenderDirection(vertexTags, wayTags) {\n if (vertexTags.highway || vertexTags.traffic_sign || vertexTags.traffic_calming || vertexTags.barrier) {\n // allowed on roads and tramways\n return !!(wayTags.highway || wayTags.railway);\n }\n if (vertexTags.railway) return !!wayTags.railway;\n if (vertexTags.waterway) return !!wayTags.waterway;\n if (vertexTags.cycleway === 'asl') return !!wayTags.highway;\n return true;\n}\n\nexport var osmSummableTags = new Set([\n 'step_count',\n 'parking:both:capacity',\n 'parking:left:capacity',\n 'parking:right:capacity'\n]);\n\n// ISO country codes keys\nexport const osmIsoCountryKeys = new Set([\n 'country',\n 'target'\n]);\n", "import { debug } from '../index';\nimport { osmIsInterestingTag } from './tags';\nimport { utilArrayUnion } from '../util/array';\nimport { utilUnicodeCharsTruncated } from '../util/util';\n\n\nexport function osmEntity(attrs) {\n // For prototypal inheritance.\n if (this instanceof osmEntity) return;\n\n // Create the appropriate subtype.\n if (attrs && attrs.type) {\n return osmEntity[attrs.type].apply(this, arguments);\n } else if (attrs && attrs.id) {\n return osmEntity[osmEntity.id.type(attrs.id)].apply(this, arguments);\n }\n\n // Initialize a generic Entity (used only in tests).\n return (new osmEntity()).initialize(arguments);\n}\n\n\nosmEntity.id = function(type) {\n return osmEntity.id.fromOSM(type, osmEntity.id.next[type]--);\n};\n\n\nosmEntity.id.next = {\n changeset: -1, node: -1, way: -1, relation: -1\n};\n\n\nosmEntity.id.fromOSM = function(type, id) {\n return type[0] + id;\n};\n\n\nosmEntity.id.toOSM = function(id) {\n var match = id.match(/^[cnwr](-?\\d+)$/);\n if (match) {\n return match[1];\n }\n return '';\n};\n\n\nosmEntity.id.type = function(id) {\n return { 'c': 'changeset', 'n': 'node', 'w': 'way', 'r': 'relation' }[id[0]];\n};\n\n\n// A function suitable for use as the second argument to d3.selection#data().\nosmEntity.key = function(entity) {\n return entity.id + 'v' + (entity.v || 0);\n};\n\n\nosmEntity.prototype = {\n\n /** @type {Tags} */\n tags: {},\n\n /** @type {String} */\n id: undefined,\n\n initialize: function(sources) {\n for (var i = 0; i < sources.length; ++i) {\n var source = sources[i];\n for (var prop in source) {\n if (Object.prototype.hasOwnProperty.call(source, prop)) {\n if (source[prop] === undefined) {\n delete this[prop];\n } else {\n this[prop] = source[prop];\n }\n }\n }\n }\n\n if (!this.id && this.type) {\n this.id = osmEntity.id(this.type);\n }\n if (!this.hasOwnProperty('visible')) {\n this.visible = true;\n }\n\n if (debug) {\n Object.freeze(this);\n Object.freeze(this.tags);\n\n if (this.loc) Object.freeze(this.loc);\n if (this.nodes) Object.freeze(this.nodes);\n if (this.members) Object.freeze(this.members);\n }\n\n return this;\n },\n\n\n copy: function(resolver, copies) {\n if (copies[this.id]) return copies[this.id];\n\n var copy = osmEntity(this, { id: undefined, user: undefined, version: undefined });\n copies[this.id] = copy;\n\n return copy;\n },\n\n\n osmId: function() {\n return osmEntity.id.toOSM(this.id);\n },\n\n\n isNew: function() {\n var osmId = osmEntity.id.toOSM(this.id);\n return osmId.length === 0 || osmId[0] === '-';\n },\n\n\n update: function(attrs) {\n return osmEntity(this, attrs, { v: 1 + (this.v || 0) });\n },\n\n\n /**\n *\n * @param {Tags} tags tags to merge into this entity's tags\n * @param {Tags} setTags (optional) a set of tags to overwrite in this entity's tags\n * @returns {iD.OsmEntity}\n */\n mergeTags: function(tags, setTags = {}) {\n const merged = Object.assign({}, this.tags); // shallow copy\n let changed = false;\n\n for (const k in tags) {\n if (setTags.hasOwnProperty(k)) continue;\n const t1 = this.tags[k];\n const t2 = tags[k];\n if (!t1) {\n changed = true;\n merged[k] = t2;\n } else if (t1 !== t2) {\n changed = true;\n merged[k] = utilUnicodeCharsTruncated(\n utilArrayUnion(t1.split(/;\\s*/), t2.split(/;\\s*/)).join(';'),\n 255 // avoid exceeding character limit; see also context.maxCharsForTagValue()\n );\n }\n }\n for (const k in setTags) {\n if (this.tags[k] !== setTags[k]) {\n changed = true;\n merged[k] = setTags[k];\n }\n }\n\n return changed ? this.update({ tags: merged }) : this;\n },\n\n\n intersects: function(extent, resolver) {\n return this.extent(resolver).intersects(extent);\n },\n\n\n hasNonGeometryTags: function() {\n return Object.keys(this.tags).some(function(k) { return k !== 'area'; });\n },\n\n hasParentRelations: function(resolver) {\n return resolver.parentRelations(this).length > 0;\n },\n\n hasInterestingTags: function() {\n return Object.keys(this.tags).some(osmIsInterestingTag);\n },\n\n isDegenerate: function() {\n return true;\n },\n};\n", "import { osmEntity } from './entity';\nimport { geoAngle, geoExtent, geoVecAdd, geoVecLength, geoVecNormalize, geoVecSubtract } from '../geo';\nimport { utilArrayUniqBy } from '../util';\nimport { osmShouldRenderDirection } from './tags';\n\nexport const cardinal = {\n north: 0, n: 0,\n northnortheast: 22, nne: 22,\n northeast: 45, ne: 45,\n eastnortheast: 67, ene: 67,\n east: 90, e: 90,\n eastsoutheast: 112, ese: 112,\n southeast: 135, se: 135,\n southsoutheast: 157, sse: 157,\n south: 180, s: 180,\n southsouthwest: 202, ssw: 202,\n southwest: 225, sw: 225,\n westsouthwest: 247, wsw: 247,\n west: 270, w: 270,\n westnorthwest: 292, wnw: 292,\n northwest: 315, nw: 315,\n northnorthwest: 337, nnw: 337\n};\n\nexport const SIDE_TAGS = [\n 'side',\n 'railway:signal:position',\n // railway:turnout_side has special handling, since it's not relative to the way direction\n];\n\nexport const SIDES = new Set(['left', 'right', 'both']);\n\nexport const SIDE_ANGLE_OFFSET = { left: 180, right: 0 };\n\n/**\n * @typedef {typeof prototype & iD.AbstractEntity} OsmNode\n * @returns {OsmNode}\n */\nexport function osmNode() {\n if (!(this instanceof osmNode)) {\n return (new osmNode()).initialize(arguments);\n } else if (arguments.length) {\n this.initialize(arguments);\n }\n}\n\nosmEntity.node = osmNode;\n\nosmNode.prototype = Object.create(osmEntity.prototype);\n\nconst prototype = {\n type: 'node',\n loc: /** @type {Vec2} */ ([9999, 9999]),\n\n extent: function() {\n return new geoExtent(this.loc);\n },\n\n\n geometry: function(graph) {\n return graph.transient(this, 'geometry', function() {\n return graph.isPoi(this) ? 'point' : 'vertex';\n });\n },\n\n\n move: function(loc) {\n return this.update({loc: loc});\n },\n\n\n isDegenerate: function() {\n return !(\n Array.isArray(this.loc) && this.loc.length === 2 &&\n this.loc[0] >= -180 && this.loc[0] <= 180 &&\n this.loc[1] >= -90 && this.loc[1] <= 90\n );\n },\n\n\n // Inspect tags and geometry to determine which direction(s) this node/vertex points\n directions: function(resolver, projection) {\n /** @type {{ type: 'side' | 'turnout_side' | 'direction'; value: string }[]} */\n const rawValues = [];\n\n // which tag to use?\n if (this.isHighwayIntersection(resolver) && (this.tags.stop || '').toLowerCase() === 'all') {\n // all-way stop tag on a highway intersection\n rawValues.push({\n type: 'direction',\n value: 'all',\n });\n } else if (this.tags['railway:turnout_side'] && resolver.parentWays(this).length > 1) {\n rawValues.push({\n type: 'turnout_side',\n value: this.tags['railway:turnout_side'].toLowerCase(),\n });\n } else {\n // generic side tag\n\n // unfortunately, the proposal for highway=cyclist_waiting_aid used\n // an ambiguous definition for `side`, which basically makes the tag\n // useless in the situation where the parent way (the cycleway) is\n // bidirectional. It's impossible for software to determine which\n // direction the mapper is referring to\u2026\n const hideSideTag = (\n this.tags.highway === 'cyclist_waiting_aid' &&\n resolver.parentWays(this).some(way => !way.isOneWayForwards())\n );\n\n const sideTag = SIDE_TAGS.map(key => this.tags[key]).find(Boolean);\n if (SIDES.has(sideTag?.toLowerCase()) && !hideSideTag) {\n rawValues.push({\n type: 'side',\n value: sideTag?.toLowerCase(),\n });\n }\n\n // generic direction tag\n let val = (this.tags.direction || '').toLowerCase();\n\n // better suffix-style direction tag\n const re = /:direction$/i;\n for (const key of Object.keys(this.tags)) {\n if (re.test(key)) {\n val = this.tags[key].toLowerCase();\n break;\n }\n }\n for (const value of val.split(';')) {\n rawValues.push({ type: 'direction', value });\n }\n }\n\n if (!rawValues.length) return [];\n\n\n /** @type {{ type: 'side' | 'direction'; angle: number }[]} */\n const results = [];\n\n const neighborNodeReducer = (neighbor, { lookBackward = true, lookForward = true } = {}) => function(collection, { nodes }) {\n nodes.forEach((_, i) => {\n if (nodes[i] !== neighbor.id) return; // not a match of current entity\n\n if (lookForward && i > 0) {\n collection[nodes[i - 1]] = true; // look back to prev node\n }\n if (lookBackward && i < nodes.length - 1) {\n collection[nodes[i + 1]] = true; // look ahead to next node\n }\n });\n return collection;\n };\n\n rawValues.forEach(({ type, value: v }) => {\n // swap cardinal for numeric directions\n if (cardinal[v] !== undefined) {\n v = cardinal[v];\n }\n\n // numeric direction - just add to results\n if (v !== '' && !isNaN(+v)) {\n results.push({ type: 'direction', angle: +v });\n return;\n }\n\n if (type === 'turnout_side') {\n if (SIDE_ANGLE_OFFSET[v] === undefined) return;\n\n const branchVectors = resolver.parentWays(this)\n .filter(way => way.tags.railway && way.geometry(resolver) === 'line')\n .reduce(neighborNodeReducer(this), {});\n\n const ids = Object.keys(branchVectors);\n // the turnout side tag is only meaningfully defined for switches with 3 branches - one incoming and two outgoing\n if (ids.length !== 3) return;\n\n ids.forEach(id => branchVectors[id] = geoVecNormalize(geoVecSubtract(projection(resolver.entity(id).loc), projection(this.loc))));\n\n const sortedIds = ids.map(id => {\n const otherVectorSum = ids\n .filter(n => n !== id)\n .map(n => branchVectors[n])\n .reduce(geoVecAdd, [0, 0]);\n return { id, alignment: geoVecLength(otherVectorSum) };\n }).sort((a, b) => b.alignment - a.alignment);\n\n // don't attempt to interpret the turnout side if the angles of the branches are too similar\n const MIN_ALIGNMENT_RATIO = 2;\n if (sortedIds[0].alignment < MIN_ALIGNMENT_RATIO * sortedIds[1].alignment || !sortedIds[0].id) return;\n\n results.push({\n type: 'side',\n angle: (geoAngle(this, resolver.entity(sortedIds[0].id), projection) * (180 / Math.PI)) + SIDE_ANGLE_OFFSET[v]\n });\n } else {\n\n const isSide = type === 'side' && SIDES.has(v);\n\n // string direction - inspect parent ways\n const lookBackward =\n (this.tags['traffic_sign:backward'] || v === (isSide ? 'left' : 'backward') || v === 'both' || v === 'all');\n const lookForward =\n (this.tags['traffic_sign:forward'] || v === (isSide ? 'right' : 'forward') || v === 'both' || v === 'all');\n\n if (!lookForward && !lookBackward) return;\n\n const nodeIds = resolver.parentWays(this)\n .filter(way => osmShouldRenderDirection(this.tags, way.tags))\n .reduce(neighborNodeReducer(this, { lookForward, lookBackward }), {});\n\n Object.keys(nodeIds).forEach(function(nodeId) {\n // +90 because geoAngle returns angle from X axis, not Y (north)\n results.push({\n type: isSide ? 'side' : 'direction',\n angle: (geoAngle(this, resolver.entity(nodeId), projection) * (180 / Math.PI)) + (isSide ? 0 : 90)\n });\n }, this);\n }\n }, this);\n\n return utilArrayUniqBy(results, item => item.type + item.angle);\n },\n\n isCrossing: function(){\n return this.tags.highway === 'crossing' ||\n this.tags.railway && this.tags.railway.indexOf('crossing') !== -1;\n },\n\n isEndpoint: function(resolver) {\n return resolver.transient(this, 'isEndpoint', function() {\n var id = this.id;\n return resolver.parentWays(this).filter(function(parent) {\n return !parent.isClosed() && !!parent.affix(id);\n }).length > 0;\n });\n },\n\n\n isConnected: function(resolver) {\n return resolver.transient(this, 'isConnected', function() {\n var parents = resolver.parentWays(this);\n\n if (parents.length > 1) {\n // vertex is connected to multiple parent ways\n for (var i in parents) {\n if (parents[i].geometry(resolver) === 'line' &&\n parents[i].hasInterestingTags()) return true;\n }\n } else if (parents.length === 1) {\n var way = parents[0];\n var nodes = way.nodes.slice();\n if (way.isClosed()) { nodes.pop(); } // ignore connecting node if closed\n\n // return true if vertex appears multiple times (way is self intersecting)\n return nodes.indexOf(this.id) !== nodes.lastIndexOf(this.id);\n }\n\n return false;\n });\n },\n\n\n parentIntersectionWays: function(resolver) {\n return resolver.transient(this, 'parentIntersectionWays', function() {\n return resolver.parentWays(this).filter(function(parent) {\n return (parent.tags.highway ||\n parent.tags.waterway ||\n parent.tags.railway ||\n parent.tags.aeroway) &&\n parent.geometry(resolver) === 'line';\n });\n });\n },\n\n\n isIntersection: function(resolver) {\n return this.parentIntersectionWays(resolver).length > 1;\n },\n\n\n isHighwayIntersection: function(resolver) {\n return resolver.transient(this, 'isHighwayIntersection', function() {\n return resolver.parentWays(this).filter(function(parent) {\n return parent.tags.highway && parent.geometry(resolver) === 'line';\n }).length > 1;\n });\n },\n\n\n isOnAddressLine: function(resolver) {\n return resolver.transient(this, 'isOnAddressLine', function() {\n return resolver.parentWays(this).filter(function(parent) {\n return parent.tags.hasOwnProperty('addr:interpolation') &&\n parent.geometry(resolver) === 'line';\n }).length > 0;\n });\n },\n\n\n asJXON: function(changeset_id) {\n var r = {\n node: {\n '@id': this.osmId(),\n '@lon': this.loc[0],\n '@lat': this.loc[1],\n '@version': (this.version || 0),\n tag: Object.keys(this.tags).map(function(k) {\n return { keyAttributes: { k: k, v: this.tags[k] } };\n }, this)\n }\n };\n if (changeset_id) r.node['@changeset'] = changeset_id;\n return r;\n },\n\n\n asGeoJSON: function() {\n return {\n type: 'Point',\n coordinates: this.loc\n };\n }\n};\nObject.assign(osmNode.prototype, prototype);\n", "import { median as d3_median } from 'd3-array';\n\nimport {\n polygonArea as d3_polygonArea,\n polygonHull as d3_polygonHull,\n polygonCentroid as d3_polygonCentroid\n} from 'd3-polygon';\n\nimport { geoVecInterp, geoVecLength } from '../geo';\nimport { osmNode } from '../osm/node';\nimport { utilArrayUniq } from '../util';\nimport { geoVecLengthSquare } from '../geo/vector';\n\n\nexport function actionCircularize(wayId, projection, maxAngle) {\n maxAngle = (maxAngle || 20) * Math.PI / 180;\n\n\n var action = function(graph, t) {\n if (t === null || !isFinite(t)) t = 1;\n t = Math.min(Math.max(+t, 0), 1);\n\n const way = graph.entity(wayId);\n const origNodes = {};\n\n for (const node of graph.childNodes(way)) {\n if (!origNodes[node.id]) {\n origNodes[node.id] = node;\n }\n }\n\n if (!way.isConvex(graph)) {\n graph = action.makeConvex(graph);\n }\n\n const nodes = utilArrayUniq(graph.childNodes(way));\n const keyNodes = nodes.filter(n => graph.parentWays(n).length > 1 || n.hasInterestingTags() );\n const points = nodes.map(n => projection(n.loc));\n const keyPoints = keyNodes.map(n => projection(n.loc));\n const centroid = (points.length === 2)\n ? geoVecInterp(points[0], points[1], 0.5)\n : d3_polygonCentroid(points);\n const radius = d3_median(points, p => geoVecLength(centroid, p));\n const sign = d3_polygonArea(points) > 0 ? 1 : -1;\n let ids, i, j, k;\n\n // we need at least two key nodes for the algorithm to work\n if (!keyNodes.length) {\n keyNodes.push(nodes[0]);\n keyPoints.push(points[0]);\n }\n\n if (keyNodes.length === 1) {\n var index = nodes.indexOf(keyNodes[0]);\n var oppositeIndex = Math.floor((index + nodes.length / 2) % nodes.length);\n\n keyNodes.push(nodes[oppositeIndex]);\n keyPoints.push(points[oppositeIndex]);\n }\n\n // key points and nodes are those connected to the ways,\n // they are projected onto the circle, in between nodes are moved\n // to constant intervals between key nodes, extra in between nodes are\n // added if necessary.\n for (i = 0; i < keyPoints.length; i++) {\n var nextKeyNodeIndex = (i + 1) % keyNodes.length;\n var startNode = keyNodes[i];\n var endNode = keyNodes[nextKeyNodeIndex];\n var startNodeIndex = nodes.indexOf(startNode);\n var endNodeIndex = nodes.indexOf(endNode);\n var numberNewPoints = -1;\n var indexRange = endNodeIndex - startNodeIndex;\n var nearNodes = {};\n var inBetweenNodes = [];\n var startAngle, endAngle, totalAngle, eachAngle;\n var angle, loc, node, origNode;\n\n if (indexRange < 0) {\n indexRange += nodes.length;\n }\n\n // position this key node\n var distance = geoVecLength(centroid, keyPoints[i]) || 1e-4;\n keyPoints[i] = [\n centroid[0] + (keyPoints[i][0] - centroid[0]) / distance * radius,\n centroid[1] + (keyPoints[i][1] - centroid[1]) / distance * radius\n ];\n loc = projection.invert(keyPoints[i]);\n node = keyNodes[i];\n origNode = origNodes[node.id];\n node = node.move(geoVecInterp(origNode.loc, loc, t));\n graph = graph.replace(node);\n\n // figure out the between delta angle we want to match to\n startAngle = Math.atan2(keyPoints[i][1] - centroid[1], keyPoints[i][0] - centroid[0]);\n endAngle = Math.atan2(keyPoints[nextKeyNodeIndex][1] - centroid[1], keyPoints[nextKeyNodeIndex][0] - centroid[0]);\n totalAngle = endAngle - startAngle;\n\n // detects looping around -pi/pi\n if (totalAngle * sign > 0) {\n totalAngle = -sign * (2 * Math.PI - Math.abs(totalAngle));\n }\n\n do {\n numberNewPoints++;\n eachAngle = totalAngle / (indexRange + numberNewPoints);\n } while (Math.abs(eachAngle) > maxAngle);\n\n\n // move existing nodes\n for (j = 1; j < indexRange; j++) {\n angle = startAngle + j * eachAngle;\n loc = projection.invert([\n centroid[0] + Math.cos(angle) * radius,\n centroid[1] + Math.sin(angle) * radius\n ]);\n\n node = nodes[(j + startNodeIndex) % nodes.length];\n origNode = origNodes[node.id];\n nearNodes[node.id] = angle;\n\n node = node.move(geoVecInterp(origNode.loc, loc, t));\n graph = graph.replace(node);\n }\n\n // add new in between nodes if necessary\n for (j = 0; j < numberNewPoints; j++) {\n angle = startAngle + (indexRange + j) * eachAngle;\n loc = projection.invert([\n centroid[0] + Math.cos(angle) * radius,\n centroid[1] + Math.sin(angle) * radius\n ]);\n\n // choose a nearnode to use as the original\n var min = Infinity;\n for (var nodeId in nearNodes) {\n var nearAngle = nearNodes[nodeId];\n var dist = Math.abs(nearAngle - angle);\n if (dist < min) {\n min = dist;\n origNode = origNodes[nodeId];\n }\n }\n\n node = osmNode({ loc: geoVecInterp(origNode.loc, loc, t) });\n graph = graph.replace(node);\n\n nodes.splice(endNodeIndex + j, 0, node);\n inBetweenNodes.push(node.id);\n }\n\n // Check for other ways that share these keyNodes..\n // If keyNodes are adjacent in both ways,\n // we can add inBetweenNodes to that shared way too..\n if (indexRange === 1 && inBetweenNodes.length) {\n var startIndex1 = way.nodes.lastIndexOf(startNode.id);\n var endIndex1 = way.nodes.lastIndexOf(endNode.id);\n var wayDirection1 = (endIndex1 - startIndex1);\n if (wayDirection1 < -1) { wayDirection1 = 1; }\n\n var parentWays = graph.parentWays(keyNodes[i]);\n for (j = 0; j < parentWays.length; j++) {\n var sharedWay = parentWays[j];\n if (sharedWay === way) continue;\n\n if (sharedWay.areAdjacent(startNode.id, endNode.id)) {\n var startIndex2 = sharedWay.nodes.lastIndexOf(startNode.id);\n var endIndex2 = sharedWay.nodes.lastIndexOf(endNode.id);\n var wayDirection2 = (endIndex2 - startIndex2);\n var insertAt = endIndex2;\n if (wayDirection2 < -1) { wayDirection2 = 1; }\n\n if (wayDirection1 !== wayDirection2) {\n inBetweenNodes.reverse();\n insertAt = startIndex2;\n }\n for (k = 0; k < inBetweenNodes.length; k++) {\n sharedWay = sharedWay.addNode(inBetweenNodes[k], insertAt + k);\n }\n graph = graph.replace(sharedWay);\n }\n }\n }\n\n }\n\n // update the way to have all the new nodes\n ids = nodes.map(function(n) { return n.id; });\n ids.push(ids[0]);\n\n graph = graph.replace(\n way.update({nodes: ids})\n );\n\n return graph;\n };\n\n\n action.makeConvex = function(graph) {\n var way = graph.entity(wayId);\n var nodes = utilArrayUniq(graph.childNodes(way));\n var points = nodes.map(function(n) { return projection(n.loc); });\n var sign = d3_polygonArea(points) > 0 ? 1 : -1;\n var hull = d3_polygonHull(points);\n var i, j;\n\n // D3 convex hulls go counterclockwise..\n if (sign === -1) {\n nodes.reverse();\n points.reverse();\n }\n\n for (i = 0; i < hull.length - 1; i++) {\n var startIndex = points.indexOf(hull[i]);\n var endIndex = points.indexOf(hull[i+1]);\n var indexRange = (endIndex - startIndex);\n\n if (indexRange < 0) {\n indexRange += nodes.length;\n }\n\n // move interior nodes to the surface of the convex hull..\n for (j = 1; j < indexRange; j++) {\n var point = geoVecInterp(hull[i], hull[i+1], j / indexRange);\n var node = nodes[(j + startIndex) % nodes.length].move(projection.invert(point));\n graph = graph.replace(node);\n }\n }\n return graph;\n };\n\n\n action.disabled = function(graph) {\n if (!graph.entity(wayId).isClosed()) {\n return 'not_closed';\n }\n\n //disable when already circular\n var way = graph.entity(wayId);\n var nodes = utilArrayUniq(graph.childNodes(way));\n var points = nodes.map(function(n) { return projection(n.loc); });\n var hull = d3_polygonHull(points);\n var epsilonAngle = Math.PI / 180;\n if (hull.length !== points.length || hull.length < 3){\n return false;\n }\n var centroid = d3_polygonCentroid(points);\n var radius = geoVecLengthSquare(centroid, points[0]);\n\n var i, actualPoint;\n\n // compare distances between centroid and points\n for (i = 0; i < hull.length; i++){\n actualPoint = hull[i];\n var actualDist = geoVecLengthSquare(actualPoint, centroid);\n var diff = Math.abs(actualDist - radius);\n //compare distances with epsilon-error (5%)\n if (diff > 0.05*radius) {\n return false;\n }\n }\n\n //check if central angles are smaller than maxAngle\n for (i = 0; i < hull.length; i++){\n actualPoint = hull[i];\n var nextPoint = hull[(i+1)%hull.length];\n var startAngle = Math.atan2(actualPoint[1] - centroid[1], actualPoint[0] - centroid[0]);\n var endAngle = Math.atan2(nextPoint[1] - centroid[1], nextPoint[0] - centroid[0]);\n var angle = endAngle - startAngle;\n if (angle < 0) {\n angle = -angle;\n }\n if (angle > Math.PI){\n angle = (2*Math.PI - angle);\n }\n\n if (angle > maxAngle + epsilonAngle) {\n return false;\n }\n }\n return 'already_circular';\n };\n\n\n action.transitionable = true;\n\n\n return action;\n}\n", "import { osmNodeGeometriesForTags } from '../osm/tags';\nimport { actionDeleteRelation } from './delete_relation';\n\n\n// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteWayAction.as\nexport function actionDeleteWay(wayID) {\n\n function canDeleteNode(node, graph) {\n // don't delete nodes still attached to ways or relations\n if (graph.parentWays(node).length ||\n graph.parentRelations(node).length) return false;\n\n var geometries = osmNodeGeometriesForTags(node.tags);\n // don't delete if this node can be a standalone point\n if (geometries.point) return false;\n // delete if this node only be a vertex\n if (geometries.vertex) return true;\n\n // iD doesn't know if this should be a point or vertex,\n // so only delete if there are no interesting tags\n return !node.hasInterestingTags();\n }\n\n\n var action = function(graph) {\n var way = graph.entity(wayID);\n\n graph.parentRelations(way).forEach(function(parent) {\n parent = parent.removeMembersWithID(wayID);\n graph = graph.replace(parent);\n\n if (parent.isDegenerate()) {\n graph = actionDeleteRelation(parent.id)(graph);\n }\n });\n\n (new Set(way.nodes)).forEach(function(nodeID) {\n graph = graph.replace(way.removeNode(nodeID));\n\n var node = graph.entity(nodeID);\n if (canDeleteNode(node, graph)) {\n graph = graph.remove(node);\n }\n });\n\n return graph.remove(way);\n };\n\n\n return action;\n}\n", "import { actionDeleteNode } from './delete_node';\nimport { actionDeleteRelation } from './delete_relation';\nimport { actionDeleteWay } from './delete_way';\n\n\nexport function actionDeleteMultiple(ids) {\n var actions = {\n way: actionDeleteWay,\n node: actionDeleteNode,\n relation: actionDeleteRelation\n };\n\n\n var action = function(graph) {\n ids.forEach(function(id) {\n if (graph.hasEntity(id)) { // It may have been deleted already.\n graph = actions[graph.entity(id).type](id)(graph);\n }\n });\n\n return graph;\n };\n\n\n return action;\n}\n", "import { actionDeleteMultiple } from './delete_multiple';\nimport { utilArrayUniq } from '../util';\n\n\n// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteRelationAction.as\nexport function actionDeleteRelation(relationID, allowUntaggedMembers) {\n\n function canDeleteEntity(entity, graph) {\n return !graph.parentWays(entity).length &&\n !graph.parentRelations(entity).length &&\n (!entity.hasInterestingTags() && !allowUntaggedMembers);\n }\n\n\n var action = function(graph) {\n var relation = graph.entity(relationID);\n\n graph.parentRelations(relation)\n .forEach(function(parent) {\n parent = parent.removeMembersWithID(relationID);\n graph = graph.replace(parent);\n\n if (parent.isDegenerate()) {\n graph = actionDeleteRelation(parent.id)(graph);\n }\n });\n\n var memberIDs = utilArrayUniq(relation.members.map(function(m) { return m.id; }));\n memberIDs.forEach(function(memberID) {\n graph = graph.replace(relation.removeMembersWithID(memberID));\n\n var entity = graph.entity(memberID);\n if (canDeleteEntity(entity, graph)) {\n graph = actionDeleteMultiple([memberID])(graph);\n }\n });\n\n return graph.remove(relation);\n };\n\n\n return action;\n}\n", "import { actionDeleteRelation } from './delete_relation';\nimport { actionDeleteWay } from './delete_way';\n\n\n// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteNodeAction.as\nexport function actionDeleteNode(nodeId) {\n var action = function(graph) {\n var node = graph.entity(nodeId);\n\n graph.parentWays(node)\n .forEach(function(parent) {\n parent = parent.removeNode(nodeId);\n graph = graph.replace(parent);\n\n if (parent.isDegenerate()) {\n graph = actionDeleteWay(parent.id)(graph);\n }\n });\n\n graph.parentRelations(node)\n .forEach(function(parent) {\n parent = parent.removeMembersWithID(nodeId);\n graph = graph.replace(parent);\n\n if (parent.isDegenerate()) {\n graph = actionDeleteRelation(parent.id)(graph);\n }\n });\n\n return graph.remove(node);\n };\n\n\n return action;\n}\n", "import { actionDeleteNode } from './delete_node';\nimport { actionDeleteWay } from './delete_way';\nimport { utilArrayUniq, utilOldestID } from '../util';\n\n\n// Connect the ways at the given nodes.\n//\n// First choose a node to be the survivor, with preference given\n// to the oldest existing (not new) and \"interesting\" node.\n//\n// Tags and relation memberships of of non-surviving nodes are merged\n// to the survivor.\n//\n// This is the inverse of `iD.actionDisconnect`.\n//\n// Reference:\n// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MergeNodesAction.as\n// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/MergeNodesAction.java\n//\nexport function actionConnect(nodeIDs) {\n var action = function(graph) {\n var survivor;\n var node;\n var parents;\n var i, j;\n\n // Select the node with the ID passed as parameter if it is in the list,\n // otherwise select the node with the oldest ID as the survivor, or the\n // last one if there are only new nodes.\n nodeIDs.reverse();\n\n var interestingIDs = [];\n for (i = 0; i < nodeIDs.length; i++) {\n node = graph.entity(nodeIDs[i]);\n if (node.hasInterestingTags()) {\n if (!node.isNew()) {\n interestingIDs.push(node.id);\n }\n }\n }\n survivor = graph.entity(utilOldestID(interestingIDs.length > 0 ? interestingIDs : nodeIDs));\n\n // Replace all non-surviving nodes with the survivor and merge tags.\n for (i = 0; i < nodeIDs.length; i++) {\n node = graph.entity(nodeIDs[i]);\n if (node.id === survivor.id) continue;\n\n parents = graph.parentWays(node);\n for (j = 0; j < parents.length; j++) {\n graph = graph.replace(parents[j].replaceNode(node.id, survivor.id));\n }\n\n parents = graph.parentRelations(node);\n for (j = 0; j < parents.length; j++) {\n graph = graph.replace(parents[j].replaceMember(node, survivor));\n }\n\n survivor = survivor.mergeTags(node.tags);\n graph = actionDeleteNode(node.id)(graph);\n }\n\n graph = graph.replace(survivor);\n\n // find and delete any degenerate ways created by connecting adjacent vertices\n parents = graph.parentWays(survivor);\n for (i = 0; i < parents.length; i++) {\n if (parents[i].isDegenerate()) {\n graph = actionDeleteWay(parents[i].id)(graph);\n }\n }\n\n return graph;\n };\n\n\n action.disabled = function(graph) {\n var seen = {};\n var restrictionIDs = [];\n var survivor;\n var node, way;\n var relations, relation, role;\n var i, j, k;\n\n // Select the node with the oldest ID as the survivor.\n survivor = graph.entity(utilOldestID(nodeIDs));\n\n // 1. disable if the nodes being connected have conflicting relation roles\n for (i = 0; i < nodeIDs.length; i++) {\n node = graph.entity(nodeIDs[i]);\n relations = graph.parentRelations(node);\n\n for (j = 0; j < relations.length; j++) {\n relation = relations[j];\n role = relation.memberById(node.id).role || '';\n\n // if this node is a via node in a restriction, remember for later\n if (relation.hasFromViaTo()) {\n restrictionIDs.push(relation.id);\n }\n\n if (seen[relation.id] !== undefined && seen[relation.id] !== role) {\n return 'relation';\n } else {\n seen[relation.id] = role;\n }\n }\n }\n\n // gather restrictions for parent ways\n for (i = 0; i < nodeIDs.length; i++) {\n node = graph.entity(nodeIDs[i]);\n\n var parents = graph.parentWays(node);\n for (j = 0; j < parents.length; j++) {\n var parent = parents[j];\n relations = graph.parentRelations(parent);\n\n for (k = 0; k < relations.length; k++) {\n relation = relations[k];\n if (relation.hasFromViaTo()) {\n restrictionIDs.push(relation.id);\n }\n }\n }\n }\n\n\n // test restrictions\n restrictionIDs = utilArrayUniq(restrictionIDs);\n for (i = 0; i < restrictionIDs.length; i++) {\n relation = graph.entity(restrictionIDs[i]);\n if (!relation.isComplete(graph)) continue;\n\n var memberWays = relation.members\n .filter(function(m) { return m.type === 'way'; })\n .map(function(m) { return graph.entity(m.id); });\n\n memberWays = utilArrayUniq(memberWays);\n var f = relation.memberByRole('from');\n var t = relation.memberByRole('to');\n var isUturn = (f.id === t.id);\n\n // 2a. disable if connection would damage a restriction\n // (a key node is a node at the junction of ways)\n var nodes = { from: [], via: [], to: [], keyfrom: [], keyto: [] };\n for (j = 0; j < relation.members.length; j++) {\n collectNodes(relation.members[j], nodes);\n }\n\n nodes.keyfrom = utilArrayUniq(nodes.keyfrom.filter(hasDuplicates));\n nodes.keyto = utilArrayUniq(nodes.keyto.filter(hasDuplicates));\n\n var filter = keyNodeFilter(nodes.keyfrom, nodes.keyto);\n nodes.from = nodes.from.filter(filter);\n nodes.via = nodes.via.filter(filter);\n nodes.to = nodes.to.filter(filter);\n\n var connectFrom = false;\n var connectVia = false;\n var connectTo = false;\n var connectKeyFrom = false;\n var connectKeyTo = false;\n\n for (j = 0; j < nodeIDs.length; j++) {\n var n = nodeIDs[j];\n if (nodes.from.indexOf(n) !== -1) { connectFrom = true; }\n if (nodes.via.indexOf(n) !== -1) { connectVia = true; }\n if (nodes.to.indexOf(n) !== -1) { connectTo = true; }\n if (nodes.keyfrom.indexOf(n) !== -1) { connectKeyFrom = true; }\n if (nodes.keyto.indexOf(n) !== -1) { connectKeyTo = true; }\n }\n if (connectFrom && connectTo && !isUturn) { return 'restriction'; }\n if (connectFrom && connectVia) { return 'restriction'; }\n if (connectTo && connectVia) { return 'restriction'; }\n\n // connecting to a key node -\n // if both nodes are on a member way (i.e. part of the turn restriction),\n // the connecting node must be adjacent to the key node.\n if (connectKeyFrom || connectKeyTo) {\n if (nodeIDs.length !== 2) { return 'restriction'; }\n\n var n0 = null;\n var n1 = null;\n for (j = 0; j < memberWays.length; j++) {\n way = memberWays[j];\n if (way.contains(nodeIDs[0])) { n0 = nodeIDs[0]; }\n if (way.contains(nodeIDs[1])) { n1 = nodeIDs[1]; }\n }\n\n if (n0 && n1) { // both nodes are part of the restriction\n var ok = false;\n for (j = 0; j < memberWays.length; j++) {\n way = memberWays[j];\n if (way.areAdjacent(n0, n1)) {\n ok = true;\n break;\n }\n }\n if (!ok) {\n return 'restriction';\n }\n }\n }\n\n // 2b. disable if nodes being connected will destroy a member way in a restriction\n // (to test, make a copy and try actually connecting the nodes)\n for (j = 0; j < memberWays.length; j++) {\n way = memberWays[j].update({}); // make copy\n for (k = 0; k < nodeIDs.length; k++) {\n if (nodeIDs[k] === survivor.id) continue;\n\n if (way.areAdjacent(nodeIDs[k], survivor.id)) {\n way = way.removeNode(nodeIDs[k]);\n } else {\n way = way.replaceNode(nodeIDs[k], survivor.id);\n }\n }\n if (way.isDegenerate()) {\n return 'restriction';\n }\n }\n }\n\n return false;\n\n\n // if a key node appears multiple times (indexOf !== lastIndexOf) it's a FROM-VIA or TO-VIA junction\n function hasDuplicates(n, i, arr) {\n return arr.indexOf(n) !== arr.lastIndexOf(n);\n }\n\n function keyNodeFilter(froms, tos) {\n return function(n) {\n return froms.indexOf(n) === -1 && tos.indexOf(n) === -1;\n };\n }\n\n function collectNodes(member, collection) {\n var entity = graph.hasEntity(member.id);\n if (!entity) return;\n\n var role = member.role || '';\n if (!collection[role]) {\n collection[role] = [];\n }\n\n if (member.type === 'node') {\n collection[role].push(member.id);\n if (role === 'via') {\n collection.keyfrom.push(member.id);\n collection.keyto.push(member.id);\n }\n\n } else if (member.type === 'way') {\n collection[role].push.apply(collection[role], entity.nodes);\n if (role === 'from' || role === 'via') {\n collection.keyfrom.push(entity.first());\n collection.keyfrom.push(entity.last());\n }\n if (role === 'to' || role === 'via') {\n collection.keyto.push(entity.first());\n collection.keyto.push(entity.last());\n }\n }\n }\n };\n\n\n return action;\n}\n", "export function actionCopyEntities(ids, fromGraph) {\n var _copies = {};\n\n\n var action = function(graph) {\n ids.forEach(function(id) {\n fromGraph.entity(id).copy(fromGraph, _copies);\n });\n\n for (var id in _copies) {\n graph = graph.replace(_copies[id]);\n }\n\n return graph;\n };\n\n\n action.copies = function() {\n return _copies;\n };\n\n\n return action;\n}\n", "import { actionDeleteRelation } from './delete_relation';\n\n\nexport function actionDeleteMember(relationId, memberIndex) {\n return function(graph) {\n var relation = graph.entity(relationId)\n .removeMember(memberIndex);\n\n graph = graph.replace(relation);\n\n if (relation.isDegenerate()) {\n graph = actionDeleteRelation(relation.id)(graph);\n }\n\n return graph;\n };\n}\n", "import { actionDeleteMember } from './delete_member';\n\n\nexport function actionDeleteMembers(relationId, memberIndexes) {\n return function(graph) {\n // Remove the members in descending order so removals won't shift what members\n // are at the remaining indexes\n memberIndexes.sort((a, b) => b - a);\n for (var i in memberIndexes) {\n graph = actionDeleteMember(relationId, memberIndexes[i])(graph);\n }\n return graph;\n };\n}\n", "export function actionDiscardTags(difference, discardTags) {\n discardTags = discardTags || {};\n\n return (graph) => {\n difference.modified().forEach(checkTags);\n difference.created().forEach(checkTags);\n return graph;\n\n function checkTags(entity) {\n const keys = Object.keys(entity.tags);\n let didDiscard = false;\n let tags = {};\n\n for (let i = 0; i < keys.length; i++) {\n const k = keys[i];\n const v = entity.tags[k];\n if (discardTags[k] === true || (typeof discardTags[k] === 'object' && discardTags[k][v]) || !entity.tags[k]) {\n didDiscard = true;\n } else {\n tags[k] = entity.tags[k];\n }\n }\n if (didDiscard) {\n graph = graph.replace(entity.update({ tags: tags }));\n }\n }\n\n };\n}\n", "import { osmNode } from '../osm/node';\n\n\n// Disconnect the ways at the given node.\n//\n// Optionally, disconnect only the given ways.\n//\n// For testing convenience, accepts an ID to assign to the (first) new node.\n// Normally, this will be undefined and the way will automatically\n// be assigned a new ID.\n//\n// This is the inverse of `iD.actionConnect`.\n//\n// Reference:\n// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/UnjoinNodeAction.as\n// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/UnGlueAction.java\n//\nexport function actionDisconnect(nodeId, newNodeId) {\n var wayIds;\n\n var disconnectableRelationTypes = {\n 'associatedStreet': true,\n 'enforcement': true,\n 'site': true,\n };\n\n var action = function(graph) {\n var node = graph.entity(nodeId);\n var connections = action.connections(graph);\n\n connections.forEach(function(connection) {\n var way = graph.entity(connection.wayID);\n var newNode = osmNode({id: newNodeId, loc: node.loc, tags: node.tags});\n\n graph = graph.replace(newNode);\n if (connection.index === 0 && way.isArea()) {\n // replace shared node with shared node..\n graph = graph.replace(way.replaceNode(way.nodes[0], newNode.id));\n } else if (way.isClosed() && connection.index === way.nodes.length - 1) {\n // replace closing node with new new node..\n graph = graph.replace(way.unclose().addNode(newNode.id));\n } else {\n // replace shared node with multiple new nodes..\n graph = graph.replace(way.updateNode(newNode.id, connection.index));\n }\n });\n\n return graph;\n };\n\n\n action.connections = function(graph) {\n var candidates = [];\n var keeping = false;\n var parentWays = graph.parentWays(graph.entity(nodeId));\n var way, waynode;\n for (var i = 0; i < parentWays.length; i++) {\n way = parentWays[i];\n if (wayIds && wayIds.indexOf(way.id) === -1) {\n keeping = true;\n continue;\n }\n if (way.isArea() && (way.nodes[0] === nodeId)) {\n candidates.push({ wayID: way.id, index: 0 });\n } else {\n for (var j = 0; j < way.nodes.length; j++) {\n waynode = way.nodes[j];\n if (waynode === nodeId) {\n if (way.isClosed() &&\n parentWays.length > 1 &&\n wayIds &&\n wayIds.indexOf(way.id) !== -1 &&\n j === way.nodes.length - 1) {\n continue;\n }\n candidates.push({ wayID: way.id, index: j });\n }\n }\n }\n }\n\n return keeping ? candidates : candidates.slice(1);\n };\n\n\n action.disabled = function(graph) {\n var connections = action.connections(graph);\n if (connections.length === 0) return 'not_connected';\n\n var parentWays = graph.parentWays(graph.entity(nodeId));\n var seenRelationIds = {};\n var sharedRelation;\n\n parentWays.forEach(function(way) {\n var relations = graph.parentRelations(way);\n relations\n .filter(relation => !disconnectableRelationTypes[relation.tags.type])\n .forEach(function(relation) {\n if (relation.id in seenRelationIds) {\n if (wayIds) {\n if (wayIds.indexOf(way.id) !== -1 ||\n wayIds.indexOf(seenRelationIds[relation.id]) !== -1) {\n sharedRelation = relation;\n }\n } else {\n sharedRelation = relation;\n }\n } else {\n seenRelationIds[relation.id] = way.id;\n }\n });\n });\n\n if (sharedRelation) return 'relation';\n };\n\n\n action.limitWays = function(val) {\n if (!arguments.length) return wayIds;\n wayIds = val;\n return action;\n };\n\n\n return action;\n}\n", "import { geoPath as d3_geoPath } from 'd3-geo';\n\nimport { osmNode } from '../osm/node';\n\nexport function actionExtract(entityID, projection) {\n\n var extractedNodeID;\n\n /** @param {boolean} shiftKeyPressed */\n var action = function(graph, shiftKeyPressed) {\n var entity = graph.entity(entityID);\n\n if (entity.type === 'node') {\n return extractFromNode(entity, graph, shiftKeyPressed);\n }\n\n return extractFromWayOrRelation(entity, graph);\n };\n\n /** @param {boolean} shiftKeyPressed */\n function extractFromNode(node, graph, shiftKeyPressed) {\n\n extractedNodeID = node.id;\n\n // Create a new node to replace the one we will detach\n var replacement = osmNode({ loc: node.loc });\n graph = graph.replace(replacement);\n\n // Process each way in turn, updating the graph as we go\n graph = graph.parentWays(node)\n .reduce(function(accGraph, parentWay) {\n return accGraph.replace(parentWay.replaceNode(entityID, replacement.id));\n }, graph);\n\n if (!shiftKeyPressed) return graph;\n\n // Process any relations too\n // but only if the user holds down the shift key while triggering the operation.\n return graph.parentRelations(node)\n .reduce(function(accGraph, parentRel) {\n return accGraph.replace(parentRel.replaceMember(node, replacement));\n }, graph);\n }\n\n function extractFromWayOrRelation(entity, graph) {\n\n var fromGeometry = entity.geometry(graph);\n\n var keysToCopyAndRetain = ['source', 'wheelchair'];\n var keysToRetain = ['area'];\n var buildingKeysToRetain = ['architect', 'building', 'height', 'layer', 'nycdoitt:bin', 'ref:GB:uprn', 'ref:linz:building_id'];\n\n var extractedLoc = d3_geoPath(projection).centroid(entity.asGeoJSON(graph));\n extractedLoc = extractedLoc && projection.invert(extractedLoc);\n if (!extractedLoc || !isFinite(extractedLoc[0]) || !isFinite(extractedLoc[1])) {\n extractedLoc = entity.extent(graph).center();\n }\n\n var indoorAreaValues = {\n area: true,\n corridor: true,\n elevator: true,\n level: true,\n room: true\n };\n\n var isBuilding = (entity.tags.building && entity.tags.building !== 'no') ||\n (entity.tags['building:part'] && entity.tags['building:part'] !== 'no');\n\n var isIndoorArea = fromGeometry === 'area' && entity.tags.indoor && indoorAreaValues[entity.tags.indoor];\n\n var entityTags = Object.assign({}, entity.tags); // shallow copy\n var pointTags = {};\n for (var key in entityTags) {\n\n if (entity.type === 'relation' &&\n key === 'type') {\n continue;\n }\n\n if (keysToRetain.indexOf(key) !== -1) {\n continue;\n }\n\n if (isBuilding) {\n // don't transfer building-related tags\n if (buildingKeysToRetain.indexOf(key) !== -1 ||\n key.match(/^building:.{1,}/) ||\n key.match(/^roof:.{1,}/)) continue;\n }\n // leave `indoor` tag on the area\n if (isIndoorArea && key === 'indoor') {\n continue;\n }\n\n // copy the tag from the entity to the point\n pointTags[key] = entityTags[key];\n\n // leave addresses and some other tags so they're on both features\n if (keysToCopyAndRetain.indexOf(key) !== -1 ||\n key.match(/^addr:.{1,}/)) {\n continue;\n } else if (isIndoorArea && key === 'level') {\n // leave `level` on both features\n continue;\n }\n\n // remove the tag from the entity\n delete entityTags[key];\n }\n\n if (!isBuilding && !isIndoorArea && fromGeometry === 'area') {\n // ensure that areas keep area geometry\n entityTags.area = 'yes';\n }\n\n var replacement = osmNode({ loc: extractedLoc, tags: pointTags });\n graph = graph.replace(replacement);\n\n extractedNodeID = replacement.id;\n\n return graph.replace(entity.update({tags: entityTags}));\n }\n\n action.getExtractedNodeID = function() {\n return extractedNodeID;\n };\n\n return action;\n}\n", "import { actionDeleteRelation } from './delete_relation';\nimport { actionDeleteWay } from './delete_way';\nimport { osmIsInterestingTag, osmSummableTags } from '../osm/tags';\nimport { osmJoinWays } from '../osm/multipolygon';\nimport { geoPathIntersections } from '../geo';\nimport { utilArrayGroupBy, utilArrayIdentical, utilArrayIntersection, utilOldestID } from '../util';\n\n\n// Join ways at the end node they share.\n//\n// This is the inverse of `iD.actionSplit`.\n//\n// Reference:\n// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MergeWaysAction.as\n// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/CombineWayAction.java\n//\nexport function actionJoin(ids) {\n\n function groupEntitiesByGeometry(graph) {\n var entities = ids.map(function(id) { return graph.entity(id); });\n return Object.assign(\n { line: [] },\n utilArrayGroupBy(entities, function(entity) { return entity.geometry(graph); })\n );\n }\n\n\n var action = function(graph) {\n var ways = ids.map(graph.entity, graph);\n\n // Prefer to keep an existing way.\n // if there are multiple existing ways, keep the oldest one\n // the oldest way is determined by the ID of the way.\n var survivorID = utilOldestID(ways.map(way => way.id));\n\n // if any of the ways are sided (e.g. coastline, cliff, kerb)\n // sort them first so they establish the overall order - #6033\n ways.sort(function(a, b) {\n var aSided = a.isSided();\n var bSided = b.isSided();\n return (aSided && !bSided) ? -1\n : (bSided && !aSided) ? 1\n : 0;\n });\n\n var sequences = osmJoinWays(ways, graph);\n var joined = sequences[0];\n\n // We might need to reverse some of these ways before joining them. #4688\n // `joined.actions` property will contain any actions we need to apply.\n graph = sequences.actions.reduce(function(g, action) { return action(g); }, graph);\n\n var survivor = graph.entity(survivorID);\n survivor = survivor.update({ nodes: joined.nodes.map(function(n) { return n.id; }) });\n graph = graph.replace(survivor);\n\n joined.forEach(function(way) {\n if (way.id === survivorID) return;\n\n graph.parentRelations(way).forEach(function(parent) {\n graph = graph.replace(parent.replaceMember(way, survivor));\n });\n\n const summedTags = {};\n for (const key in way.tags) {\n if (!canSumTags(key, way.tags, survivor.tags)) continue;\n summedTags[key] = (+way.tags[key] + +survivor.tags[key]).toString();\n }\n survivor = survivor.mergeTags(way.tags, summedTags);\n\n graph = graph.replace(survivor);\n graph = actionDeleteWay(way.id)(graph);\n });\n\n // Finds if the join created a single-member multipolygon,\n // and if so turns it into a basic area instead\n function checkForSimpleMultipolygon() {\n if (!survivor.isClosed()) return;\n\n const multipolygons = graph.parentMultipolygons(survivor).filter(multipolygon => {\n // find multipolygons where the survivor is the only member\n return multipolygon.members.length === 1;\n });\n\n // skip if this is the single member of multiple multipolygons\n if (multipolygons.length !== 1) return;\n\n const multipolygon = multipolygons[0];\n\n for (const key in survivor.tags) {\n if (multipolygon.tags[key] &&\n // don't collapse if tags cannot be cleanly merged\n multipolygon.tags[key] !== survivor.tags[key]) return;\n }\n\n survivor = survivor.mergeTags(multipolygon.tags);\n graph = graph.replace(survivor);\n for (const relation of graph.parentRelations(multipolygon)) {\n // transfer membership of collapsed single-member multipolygon\n // onto resulting basic area, #9064\n graph = graph.replace(relation.replaceMember(multipolygon, survivor));\n }\n graph = actionDeleteRelation(multipolygon.id, true /* allow untagged members */)(graph);\n\n const tags = Object.assign({}, survivor.tags);\n if (survivor.geometry(graph) !== 'area') {\n // ensure the feature persists as an area\n tags.area = 'yes';\n }\n delete tags.type; // remove type=multipolygon\n survivor = survivor.update({ tags: tags });\n graph = graph.replace(survivor);\n }\n checkForSimpleMultipolygon();\n\n return graph;\n };\n\n // Returns the number of nodes the resultant way is expected to have\n action.resultingWayNodesLength = function(graph) {\n return ids.reduce(function(count, id) {\n return count + graph.entity(id).nodes.length;\n }, 0) - ids.length - 1;\n };\n\n\n action.disabled = function(graph) {\n var geometries = groupEntitiesByGeometry(graph);\n if (ids.length < 2 || ids.length !== geometries.line.length) {\n return 'not_eligible';\n }\n\n var joined = osmJoinWays(ids.map(graph.entity, graph), graph);\n if (joined.length > 1) {\n return 'not_adjacent';\n }\n\n var i;\n\n // All joined ways must belong to the same set of (non-restriction) relations.\n // Restriction relations have different logic, below, which allows some cases\n // this prohibits, and prohibits some cases this allows.\n var sortedParentRelations = function (id) {\n return graph.parentRelations(graph.entity(id))\n .filter((rel) => !rel.isRestriction() && !rel.isConnectivity())\n .sort((a, b) => a.id.localeCompare(b.id));\n };\n var relsA = sortedParentRelations(ids[0]);\n for (i = 1; i < ids.length; i++) {\n var relsB = sortedParentRelations(ids[i]);\n if (!utilArrayIdentical(relsA, relsB)) {\n return 'conflicting_relations';\n }\n }\n\n // Loop through all combinations of path-pairs\n // to check potential intersections between all pairs\n for (i = 0; i < ids.length - 1; i++) {\n for (var j = i + 1; j < ids.length; j++) {\n var path1 = graph.childNodes(graph.entity(ids[i]))\n .map(function(e) { return e.loc; });\n var path2 = graph.childNodes(graph.entity(ids[j]))\n .map(function(e) { return e.loc; });\n var intersections = geoPathIntersections(path1, path2);\n\n // Check if intersections are just nodes lying on top of\n // each other/the line, as opposed to crossing it\n var common = utilArrayIntersection(\n joined[0].nodes.map(function(n) { return n.loc.toString(); }),\n intersections.map(function(n) { return n.toString(); })\n );\n if (common.length !== intersections.length) {\n return 'paths_intersect';\n }\n }\n }\n\n var nodeIds = joined[0].nodes.map(function(n) { return n.id; }).slice(1, -1);\n var relation;\n var tags = {};\n var conflicting = false;\n\n joined[0].forEach(function(way) {\n var parents = graph.parentRelations(way);\n parents.forEach(function(parent) {\n if ((parent.isRestriction() || parent.isConnectivity()) && parent.members.some(function(m) { return nodeIds.indexOf(m.id) >= 0; })) {\n relation = parent;\n }\n });\n\n for (var k in way.tags) {\n if (!(k in tags)) {\n tags[k] = way.tags[k];\n } else if (canSumTags(k, tags, way.tags)) {\n tags[k] = (+tags[k] + +way.tags[k]).toString();\n } else if (tags[k] && osmIsInterestingTag(k) && tags[k] !== way.tags[k]) {\n conflicting = true;\n }\n }\n });\n\n if (relation) {\n return relation.isRestriction() ? 'restriction' : 'connectivity';\n }\n\n if (conflicting) {\n return 'conflicting_tags';\n }\n };\n\n function canSumTags(key, tagsA, tagsB) {\n return osmSummableTags.has(key) &&\n isFinite(tagsA[key] &&\n isFinite(tagsB[key]));\n }\n\n\n return action;\n}\n", "import { osmTagSuggestingArea } from '../osm/tags';\nimport { utilArrayGroupBy, utilArrayUniq, utilCompareIDs } from '../util';\n\n\nexport function actionMerge(ids) {\n\n function groupEntitiesByGeometry(graph) {\n var entities = ids.map(function(id) { return graph.entity(id); });\n return Object.assign(\n { point: [], area: [], line: [], relation: [] },\n utilArrayGroupBy(entities, function(entity) { return entity.geometry(graph); })\n );\n }\n\n\n var action = function(graph) {\n var geometries = groupEntitiesByGeometry(graph);\n var target = geometries.area[0] || geometries.line[0];\n var points = geometries.point;\n\n points.forEach(function(point) {\n target = target.mergeTags(point.tags);\n graph = graph.replace(target);\n\n graph.parentRelations(point).forEach(function(parent) {\n graph = graph.replace(parent.replaceMember(point, target));\n });\n\n var nodes = utilArrayUniq(graph.childNodes(target));\n var removeNode = point;\n\n if (!point.isNew()) {\n // Try to preserve the original point if it already has\n // an ID in the database.\n\n var inserted = false;\n\n var canBeReplaced = function(node) {\n return !(graph.parentWays(node).length > 1 ||\n graph.parentRelations(node).length);\n };\n\n var replaceNode = function(node) {\n graph = graph.replace(point.update({ tags: node.tags, loc: node.loc }));\n target = target.replaceNode(node.id, point.id);\n graph = graph.replace(target);\n removeNode = node;\n inserted = true;\n };\n\n var i;\n var node;\n\n // First, try to replace a new child node on the target way.\n for (i = 0; i < nodes.length; i++) {\n node = nodes[i];\n if (canBeReplaced(node) && node.isNew()) {\n replaceNode(node);\n break;\n }\n }\n\n if (!inserted && point.hasInterestingTags()) {\n // No new child node found, try to find an existing, but\n // uninteresting child node instead.\n for (i = 0; i < nodes.length; i++) {\n node = nodes[i];\n if (canBeReplaced(node) &&\n !node.hasInterestingTags()) {\n replaceNode(node);\n break;\n }\n }\n\n if (!inserted) {\n // Still not inserted, try to find an existing, interesting,\n // but more recent child node.\n for (i = 0; i < nodes.length; i++) {\n node = nodes[i];\n if (canBeReplaced(node) &&\n utilCompareIDs(point.id, node.id) < 0) {\n replaceNode(node);\n break;\n }\n }\n }\n\n // If the point still hasn't been inserted, we give up.\n // There are more interesting or older nodes on the way.\n }\n }\n\n graph = graph.remove(removeNode);\n });\n\n if (target.tags.area === 'yes') {\n var tags = Object.assign({}, target.tags); // shallow copy\n delete tags.area;\n if (osmTagSuggestingArea(tags)) {\n // remove the `area` tag if area geometry is now implied - #3851\n target = target.update({ tags: tags });\n graph = graph.replace(target);\n }\n }\n\n return graph;\n };\n\n\n action.disabled = function(graph) {\n var geometries = groupEntitiesByGeometry(graph);\n if (geometries.point.length === 0 ||\n (geometries.area.length + geometries.line.length) !== 1 ||\n geometries.relation.length !== 0) {\n return 'not_eligible';\n }\n };\n\n\n return action;\n}\n", "import { actionConnect } from './connect';\nimport { geoVecAdd, geoVecScale } from '../geo';\n\n\n// `actionMergeNodes` is just a combination of:\n//\n// 1. move all the nodes to a common location\n// 2. `actionConnect` them\n\nexport function actionMergeNodes(nodeIDs, loc) {\n\n // If there is a single \"interesting\" node, use that as the location.\n // Otherwise return the average location of all the nodes.\n function chooseLoc(graph) {\n if (!nodeIDs.length) return null;\n var sum = [0,0];\n var interestingCount = 0;\n var interestingLoc;\n\n for (var i = 0; i < nodeIDs.length; i++) {\n var node = graph.entity(nodeIDs[i]);\n if (node.hasInterestingTags()) {\n interestingLoc = (++interestingCount === 1) ? node.loc : null;\n }\n sum = geoVecAdd(sum, node.loc);\n }\n\n return interestingLoc || geoVecScale(sum, 1 / nodeIDs.length);\n }\n\n\n var action = function(graph) {\n if (nodeIDs.length < 2) return graph;\n var toLoc = loc;\n if (!toLoc) {\n toLoc = chooseLoc(graph);\n }\n\n for (var i = 0; i < nodeIDs.length; i++) {\n var node = graph.entity(nodeIDs[i]);\n if (node.loc !== toLoc) {\n graph = graph.replace(node.move(toLoc));\n }\n }\n\n return actionConnect(nodeIDs)(graph);\n };\n\n\n action.disabled = function(graph) {\n if (nodeIDs.length < 2) return 'not_eligible';\n\n for (var i = 0; i < nodeIDs.length; i++) {\n var entity = graph.entity(nodeIDs[i]);\n if (entity.type !== 'node') return 'not_eligible';\n }\n\n return actionConnect(nodeIDs).disabled(graph);\n };\n\n return action;\n}\n", "import { osmEntity } from './entity';\nimport { geoExtent } from '../geo';\n\n\nexport function osmChangeset() {\n if (!(this instanceof osmChangeset)) {\n return (new osmChangeset()).initialize(arguments);\n } else if (arguments.length) {\n this.initialize(arguments);\n }\n}\n\n\nosmEntity.changeset = osmChangeset;\n\nosmChangeset.prototype = Object.create(osmEntity.prototype);\n\nObject.assign(osmChangeset.prototype, {\n\n type: 'changeset',\n\n\n extent: function() {\n return new geoExtent();\n },\n\n\n geometry: function() {\n return 'changeset';\n },\n\n\n asJXON: function() {\n return {\n osm: {\n changeset: {\n tag: Object.keys(this.tags).map(function(k) {\n return { '@k': k, '@v': this.tags[k] };\n }, this),\n '@version': 0.6,\n '@generator': 'iD'\n }\n }\n };\n },\n\n\n // Generate [osmChange](http://wiki.openstreetmap.org/wiki/OsmChange)\n // XML. Returns a string.\n osmChangeJXON: function(changes) {\n var changeset_id = this.id;\n\n function nest(x, order) {\n var groups = {};\n for (var i = 0; i < x.length; i++) {\n var tagName = Object.keys(x[i])[0];\n if (!groups[tagName]) groups[tagName] = [];\n groups[tagName].push(x[i][tagName]);\n }\n var ordered = {};\n order.forEach(function(o) {\n if (groups[o]) ordered[o] = groups[o];\n });\n return ordered;\n }\n\n\n // sort relations in a changeset by dependencies\n function sort(changes) {\n\n // find a referenced relation in the current changeset\n function resolve(item) {\n return relations.find(function(relation) {\n return item.keyAttributes.type === 'relation'\n && item.keyAttributes.ref === relation['@id'];\n });\n }\n\n // a new item is an item that has not been already processed\n function isNew(item) {\n return !sorted[ item['@id'] ] && !processing.find(function(proc) {\n return proc['@id'] === item['@id'];\n });\n }\n\n var processing = [];\n var sorted = {};\n var relations = changes.relation;\n\n if (!relations) return changes;\n\n for (var i = 0; i < relations.length; i++) {\n var relation = relations[i];\n\n // skip relation if already sorted\n if (!sorted[relation['@id']]) {\n processing.push(relation);\n }\n\n while (processing.length > 0) {\n var next = processing[0],\n deps = next.member.map(resolve).filter(Boolean).filter(isNew);\n if (deps.length === 0) {\n sorted[next['@id']] = next;\n processing.shift();\n } else {\n processing = deps.concat(processing);\n }\n }\n }\n\n changes.relation = Object.values(sorted);\n return changes;\n }\n\n function rep(entity) {\n return entity.asJXON(changeset_id);\n }\n\n return {\n osmChange: {\n '@version': 0.6,\n '@generator': 'iD',\n 'create': sort(nest(changes.created.map(rep), ['node', 'way', 'relation'])),\n 'modify': nest(changes.modified.map(rep), ['node', 'way', 'relation']),\n 'delete': Object.assign(nest(changes.deleted.map(rep), ['relation', 'way', 'node']), { '@if-unused': true })\n }\n };\n },\n\n\n asGeoJSON: function() {\n return {};\n }\n\n});\n", "import { geoExtent } from '../geo';\n\n\nexport function osmNote() {\n if (!(this instanceof osmNote)) {\n return (new osmNote()).initialize(arguments);\n } else if (arguments.length) {\n this.initialize(arguments);\n }\n}\n\n\nosmNote.id = function() {\n return osmNote.id.next--;\n};\n\n\nosmNote.id.next = -1;\n\n\nObject.assign(osmNote.prototype, {\n\n type: 'note',\n\n initialize: function(sources) {\n for (var i = 0; i < sources.length; ++i) {\n var source = sources[i];\n for (var prop in source) {\n if (Object.prototype.hasOwnProperty.call(source, prop)) {\n if (source[prop] === undefined) {\n delete this[prop];\n } else {\n this[prop] = source[prop];\n }\n }\n }\n }\n\n if (!this.id) {\n this.id = osmNote.id().toString();\n }\n\n return this;\n },\n\n extent: function() {\n return new geoExtent(this.loc);\n },\n\n update: function(attrs) {\n return osmNote(this, attrs); // {v: 1 + (this.v || 0)}\n },\n\n isNew: function() {\n return this.id < 0;\n },\n\n move: function(loc) {\n return this.update({ loc: loc });\n }\n\n});\n", "import { geoArea as d3_geoArea } from 'd3-geo';\n\nimport { osmEntity } from './entity';\nimport { osmJoinWays } from './multipolygon';\nimport { geoExtent, geoPolygonContainsPolygon, geoPolygonIntersectsPolygon } from '../geo';\n\n/**\n * @typedef {typeof prototype & iD.AbstractEntity} OsmRelation\n * @returns {OsmRelation}\n */\nexport function osmRelation() {\n if (!(this instanceof osmRelation)) {\n return (new osmRelation()).initialize(arguments);\n } else if (arguments.length) {\n this.initialize(arguments);\n }\n}\n\n\nosmEntity.relation = osmRelation;\n\nosmRelation.prototype = Object.create(osmEntity.prototype);\n\n\nosmRelation.creationOrder = function(a, b) {\n var aId = parseInt(osmEntity.id.toOSM(a.id), 10);\n var bId = parseInt(osmEntity.id.toOSM(b.id), 10);\n\n if (aId < 0 || bId < 0) return aId - bId;\n return bId - aId;\n};\n\n\nconst prototype = {\n type: 'relation',\n members: [],\n\n\n copy: function(resolver, copies) {\n if (copies[this.id]) return copies[this.id];\n\n var copy = osmEntity.prototype.copy.call(this, resolver, copies);\n\n var members = this.members.map(function(member) {\n return Object.assign({}, member, { id: resolver.entity(member.id).copy(resolver, copies).id });\n });\n\n copy = copy.update({members: members});\n copies[this.id] = copy;\n\n return copy;\n },\n\n\n extent: function(resolver, memo) {\n return resolver.transient(this, 'extent', function() {\n if (memo && memo[this.id]) return geoExtent();\n memo = memo || {};\n memo[this.id] = true;\n\n var extent = geoExtent();\n for (var i = 0; i < this.members.length; i++) {\n var member = resolver.hasEntity(this.members[i].id);\n if (member) {\n extent._extend(member.extent(resolver, memo));\n }\n }\n return extent;\n });\n },\n\n\n geometry: function(graph) {\n return graph.transient(this, 'geometry', function() {\n return this.isMultipolygon() ? 'area' : 'relation';\n });\n },\n\n\n isDegenerate: function() {\n return this.members.length === 0;\n },\n\n\n // Return an array of members, each extended with an 'index' property whose value\n // is the member index.\n indexedMembers: function() {\n var result = new Array(this.members.length);\n for (var i = 0; i < this.members.length; i++) {\n result[i] = Object.assign({}, this.members[i], {index: i});\n }\n return result;\n },\n\n\n // Return the first member with the given role. A copy of the member object\n // is returned, extended with an 'index' property whose value is the member index.\n memberByRole: function(role) {\n for (var i = 0; i < this.members.length; i++) {\n if (this.members[i].role === role) {\n return Object.assign({}, this.members[i], {index: i});\n }\n }\n },\n\n // Same as memberByRole, but returns all members with the given role\n membersByRole: function(role) {\n var result = [];\n for (var i = 0; i < this.members.length; i++) {\n if (this.members[i].role === role) {\n result.push(Object.assign({}, this.members[i], {index: i}));\n }\n }\n return result;\n },\n\n // Return the first member with the given id. A copy of the member object\n // is returned, extended with an 'index' property whose value is the member index.\n memberById: function(id) {\n for (var i = 0; i < this.members.length; i++) {\n if (this.members[i].id === id) {\n return Object.assign({}, this.members[i], {index: i});\n }\n }\n },\n\n\n // Return the first member with the given id and role. A copy of the member object\n // is returned, extended with an 'index' property whose value is the member index.\n memberByIdAndRole: function(id, role) {\n for (var i = 0; i < this.members.length; i++) {\n if (this.members[i].id === id && this.members[i].role === role) {\n return Object.assign({}, this.members[i], {index: i});\n }\n }\n },\n\n\n addMember: function(member, index) {\n var members = this.members.slice();\n members.splice(index === undefined ? members.length : index, 0, member);\n return this.update({members: members});\n },\n\n\n updateMember: function(member, index) {\n var members = this.members.slice();\n members.splice(index, 1, Object.assign({}, members[index], member));\n return this.update({members: members});\n },\n\n\n removeMember: function(index) {\n var members = this.members.slice();\n members.splice(index, 1);\n return this.update({members: members});\n },\n\n\n removeMembersWithID: function(id) {\n var members = this.members.filter(function(m) { return m.id !== id; });\n return this.update({members: members});\n },\n\n moveMember: function(fromIndex, toIndex) {\n var members = this.members.slice();\n members.splice(toIndex, 0, members.splice(fromIndex, 1)[0]);\n return this.update({members: members});\n },\n\n\n // Wherever a member appears with id `needle.id`, replace it with a member\n // with id `replacement.id`, type `replacement.type`, and the original role,\n // By default, adding a duplicate member (by id and role) is prevented.\n // Return an updated relation.\n replaceMember: function(needle, replacement, keepDuplicates) {\n if (!this.memberById(needle.id)) return this;\n\n var members = [];\n\n for (var i = 0; i < this.members.length; i++) {\n var member = this.members[i];\n if (member.id !== needle.id) {\n members.push(member);\n } else if (keepDuplicates || !this.memberByIdAndRole(replacement.id, member.role)) {\n members.push({ id: replacement.id, type: replacement.type, role: member.role });\n }\n }\n\n return this.update({ members: members });\n },\n\n\n asJXON: function(changeset_id) {\n var r = {\n relation: {\n '@id': this.osmId(),\n '@version': this.version || 0,\n member: this.members.map(function(member) {\n return {\n keyAttributes: {\n type: member.type,\n role: member.role,\n ref: osmEntity.id.toOSM(member.id)\n }\n };\n }, this),\n tag: Object.keys(this.tags).map(function(k) {\n return { keyAttributes: { k: k, v: this.tags[k] } };\n }, this)\n }\n };\n if (changeset_id) {\n r.relation['@changeset'] = changeset_id;\n }\n return r;\n },\n\n\n asGeoJSON: function(resolver) {\n return resolver.transient(this, 'GeoJSON', function () {\n if (this.isMultipolygon()) {\n return {\n type: 'MultiPolygon',\n coordinates: this.multipolygon(resolver)\n };\n } else {\n return {\n type: 'FeatureCollection',\n properties: this.tags,\n features: this.members.map(function (member) {\n return Object.assign({role: member.role}, resolver.entity(member.id).asGeoJSON(resolver));\n })\n };\n }\n });\n },\n\n\n area: function(resolver) {\n return resolver.transient(this, 'area', function() {\n return d3_geoArea(this.asGeoJSON(resolver));\n });\n },\n\n\n isMultipolygon: function() {\n return this.tags.type === 'multipolygon';\n },\n\n\n isComplete: function(resolver) {\n for (var i = 0; i < this.members.length; i++) {\n if (!resolver.hasEntity(this.members[i].id)) {\n return false;\n }\n }\n return true;\n },\n\n\n hasFromViaTo: function() {\n return (\n this.members.some(function(m) { return m.role === 'from'; }) &&\n this.members.some((m) =>\n m.role === 'via' ||\n (m.role === 'intersection' && this.tags.type === 'destination_sign')\n ) &&\n this.members.some(function(m) { return m.role === 'to'; })\n );\n },\n\n\n isRestriction: function() {\n return !!(this.tags.type && this.tags.type.match(/^restriction:?/));\n },\n\n\n isValidRestriction: function() {\n if (!this.isRestriction()) return false;\n\n var froms = this.members.filter(function(m) { return m.role === 'from'; });\n var vias = this.members.filter(function(m) { return m.role === 'via'; });\n var tos = this.members.filter(function(m) { return m.role === 'to'; });\n\n if (froms.length !== 1 && this.tags.restriction !== 'no_entry') return false;\n if (froms.some(function(m) { return m.type !== 'way'; })) return false;\n\n if (tos.length !== 1 && this.tags.restriction !== 'no_exit') return false;\n if (tos.some(function(m) { return m.type !== 'way'; })) return false;\n\n if (vias.length === 0) return false;\n if (vias.length > 1 && vias.some(function(m) { return m.type !== 'way'; })) return false;\n\n return true;\n },\n\n isConnectivity: function() {\n return !!(this.tags.type && this.tags.type.match(/^connectivity:?/));\n },\n\n // Returns an array [A0, ... An], each Ai being an array of node arrays [Nds0, ... Ndsm],\n // where Nds0 is an outer ring and subsequent Ndsi's (if any i > 0) being inner rings.\n //\n // This corresponds to the structure needed for rendering a multipolygon path using a\n // `evenodd` fill rule, as well as the structure of a GeoJSON MultiPolygon geometry.\n //\n // In the case of invalid geometries, this function will still return a result which\n // includes the nodes of all way members, but some Nds may be unclosed and some inner\n // rings not matched with the intended outer ring.\n //\n multipolygon: function(resolver) {\n var outers = this.members.filter(function(m) { return 'outer' === (m.role || 'outer'); });\n var inners = this.members.filter(function(m) { return 'inner' === m.role; });\n\n outers = osmJoinWays(outers, resolver);\n inners = osmJoinWays(inners, resolver);\n\n var sequenceToLineString = function(sequence) {\n if (sequence.nodes.length > 2 &&\n sequence.nodes[0] !== sequence.nodes[sequence.nodes.length - 1]) {\n // close unclosed parts to ensure correct area rendering - #2945\n sequence.nodes.push(sequence.nodes[0]);\n }\n return sequence.nodes.map(function(node) { return node.loc; });\n };\n\n outers = outers.map(sequenceToLineString);\n inners = inners.map(sequenceToLineString);\n\n var result = outers.map(function(o) {\n // Heuristic for detecting counterclockwise winding order. Assumes\n // that OpenStreetMap polygons are not hemisphere-spanning.\n return [d3_geoArea({ type: 'Polygon', coordinates: [o] }) > 2 * Math.PI ? o.reverse() : o];\n });\n\n function findOuter(inner) {\n var o, outer;\n\n for (o = 0; o < outers.length; o++) {\n outer = outers[o];\n if (geoPolygonContainsPolygon(outer, inner)) {\n return o;\n }\n }\n\n for (o = 0; o < outers.length; o++) {\n outer = outers[o];\n if (geoPolygonIntersectsPolygon(outer, inner, false)) {\n return o;\n }\n }\n }\n\n for (var i = 0; i < inners.length; i++) {\n var inner = inners[i];\n\n if (d3_geoArea({ type: 'Polygon', coordinates: [inner] }) < 2 * Math.PI) {\n inner.reverse();\n }\n\n var o = findOuter(inners[i]);\n if (o !== undefined) {\n result[o].push(inners[i]);\n } else {\n result.push([inners[i]]); // Invalid geometry\n }\n }\n\n return result;\n }\n};\nObject.assign(osmRelation.prototype, prototype);\n", "export function osmLanes(entity) {\n if (entity.type !== 'way') return null;\n if (!entity.tags.highway) return null;\n\n var tags = entity.tags;\n var isOneWay = entity.isOneWay();\n var laneCount = getLaneCount(tags, isOneWay);\n var maxspeed = parseMaxspeed(tags);\n\n var laneDirections = parseLaneDirections(tags, isOneWay, laneCount);\n var forward = laneDirections.forward;\n var backward = laneDirections.backward;\n var bothways = laneDirections.bothways;\n\n // parse the piped string 'x|y|z' format\n var turnLanes = {};\n turnLanes.unspecified = parseTurnLanes(tags['turn:lanes']);\n turnLanes.forward = parseTurnLanes(tags['turn:lanes:forward']);\n turnLanes.backward = parseTurnLanes(tags['turn:lanes:backward']);\n\n var maxspeedLanes = {};\n maxspeedLanes.unspecified = parseMaxspeedLanes(tags['maxspeed:lanes'], maxspeed);\n maxspeedLanes.forward = parseMaxspeedLanes(tags['maxspeed:lanes:forward'], maxspeed);\n maxspeedLanes.backward = parseMaxspeedLanes(tags['maxspeed:lanes:backward'], maxspeed);\n\n var psvLanes = {};\n psvLanes.unspecified = parseMiscLanes(tags['psv:lanes']);\n psvLanes.forward = parseMiscLanes(tags['psv:lanes:forward']);\n psvLanes.backward = parseMiscLanes(tags['psv:lanes:backward']);\n\n var busLanes = {};\n busLanes.unspecified = parseMiscLanes(tags['bus:lanes']);\n busLanes.forward = parseMiscLanes(tags['bus:lanes:forward']);\n busLanes.backward = parseMiscLanes(tags['bus:lanes:backward']);\n\n var taxiLanes = {};\n taxiLanes.unspecified = parseMiscLanes(tags['taxi:lanes']);\n taxiLanes.forward = parseMiscLanes(tags['taxi:lanes:forward']);\n taxiLanes.backward = parseMiscLanes(tags['taxi:lanes:backward']);\n\n var hovLanes = {};\n hovLanes.unspecified = parseMiscLanes(tags['hov:lanes']);\n hovLanes.forward = parseMiscLanes(tags['hov:lanes:forward']);\n hovLanes.backward = parseMiscLanes(tags['hov:lanes:backward']);\n\n var hgvLanes = {};\n hgvLanes.unspecified = parseMiscLanes(tags['hgv:lanes']);\n hgvLanes.forward = parseMiscLanes(tags['hgv:lanes:forward']);\n hgvLanes.backward = parseMiscLanes(tags['hgv:lanes:backward']);\n\n var bicyclewayLanes = {};\n bicyclewayLanes.unspecified = parseBicycleWay(tags['bicycleway:lanes']);\n bicyclewayLanes.forward = parseBicycleWay(tags['bicycleway:lanes:forward']);\n bicyclewayLanes.backward = parseBicycleWay(tags['bicycleway:lanes:backward']);\n\n var lanesObj = {\n forward: [],\n backward: [],\n unspecified: []\n };\n\n // map forward/backward/unspecified of each lane type to lanesObj\n mapToLanesObj(lanesObj, turnLanes, 'turnLane');\n mapToLanesObj(lanesObj, maxspeedLanes, 'maxspeed');\n mapToLanesObj(lanesObj, psvLanes, 'psv');\n mapToLanesObj(lanesObj, busLanes, 'bus');\n mapToLanesObj(lanesObj, taxiLanes, 'taxi');\n mapToLanesObj(lanesObj, hovLanes, 'hov');\n mapToLanesObj(lanesObj, hgvLanes, 'hgv');\n mapToLanesObj(lanesObj, bicyclewayLanes, 'bicycleway');\n\n return {\n metadata: {\n count: laneCount,\n oneway: isOneWay,\n forward: forward,\n backward: backward,\n bothways: bothways,\n turnLanes: turnLanes,\n maxspeed: maxspeed,\n maxspeedLanes: maxspeedLanes,\n psvLanes: psvLanes,\n busLanes: busLanes,\n taxiLanes: taxiLanes,\n hovLanes: hovLanes,\n hgvLanes: hgvLanes,\n bicyclewayLanes: bicyclewayLanes\n },\n lanes: lanesObj\n };\n}\n\n\nfunction getLaneCount(tags, isOneWay) {\n var count;\n if (tags.lanes) {\n count = parseInt(tags.lanes, 10);\n if (count > 0) {\n return count;\n }\n }\n\n\n switch (tags.highway) {\n case 'trunk':\n case 'motorway':\n count = isOneWay ? 2 : 4;\n break;\n default:\n count = isOneWay ? 1 : 2;\n break;\n }\n\n return count;\n}\n\n\nfunction parseMaxspeed(tags) {\n var maxspeed = tags.maxspeed;\n if (!maxspeed) return;\n\n var maxspeedRegex = /^([0-9][\\.0-9]+?)(?:[ ]?(?:km\\/h|kmh|kph|mph|knots))?$/;\n if (!maxspeedRegex.test(maxspeed)) return;\n\n return parseInt(maxspeed, 10);\n}\n\n\nfunction parseLaneDirections(tags, isOneWay, laneCount) {\n var forward = parseInt(tags['lanes:forward'], 10);\n var backward = parseInt(tags['lanes:backward'], 10);\n var bothways = parseInt(tags['lanes:both_ways'], 10) > 0 ? 1 : 0;\n\n if (parseInt(tags.oneway, 10) === -1) {\n forward = 0;\n bothways = 0;\n backward = laneCount;\n } else if (isOneWay) {\n forward = laneCount;\n bothways = 0;\n backward = 0;\n } else if (isNaN(forward) && isNaN(backward)) {\n backward = Math.floor((laneCount - bothways) / 2);\n forward = laneCount - bothways - backward;\n } else if (isNaN(forward)) {\n if (backward > laneCount - bothways) {\n backward = laneCount - bothways;\n }\n forward = laneCount - bothways - backward;\n } else if (isNaN(backward)) {\n if (forward > laneCount - bothways) {\n forward = laneCount - bothways;\n }\n backward = laneCount - bothways - forward;\n }\n return {\n forward: forward,\n backward: backward,\n bothways: bothways\n };\n}\n\n\nfunction parseTurnLanes(tag){\n if (!tag) return;\n\n var validValues = [\n 'left', 'slight_left', 'sharp_left', 'through', 'right', 'slight_right',\n 'sharp_right', 'reverse', 'merge_to_left', 'merge_to_right', 'none'\n ];\n\n return tag.split('|')\n .map(function (s) {\n if (s === '') s = 'none';\n return s.split(';')\n .map(function (d) {\n return validValues.indexOf(d) === -1 ? 'unknown': d;\n });\n });\n}\n\n\nfunction parseMaxspeedLanes(tag, maxspeed) {\n if (!tag) return;\n\n return tag.split('|')\n .map(function (s) {\n if (s === 'none') return s;\n var m = parseInt(s, 10);\n if (s === '' || m === maxspeed) return null;\n return isNaN(m) ? 'unknown': m;\n });\n}\n\n\nfunction parseMiscLanes(tag) {\n if (!tag) return;\n\n var validValues = [\n 'yes', 'no', 'designated'\n ];\n\n return tag.split('|')\n .map(function (s) {\n if (s === '') s = 'no';\n return validValues.indexOf(s) === -1 ? 'unknown': s;\n });\n}\n\n\nfunction parseBicycleWay(tag) {\n if (!tag) return;\n\n var validValues = [\n 'yes', 'no', 'designated', 'lane'\n ];\n\n return tag.split('|')\n .map(function (s) {\n if (s === '') s = 'no';\n return validValues.indexOf(s) === -1 ? 'unknown': s;\n });\n}\n\n\nfunction mapToLanesObj(lanesObj, data, key) {\n if (data.forward) {\n data.forward.forEach(function(l, i) {\n if (!lanesObj.forward[i]) lanesObj.forward[i] = {};\n lanesObj.forward[i][key] = l;\n });\n }\n if (data.backward) {\n data.backward.forEach(function(l, i) {\n if (!lanesObj.backward[i]) lanesObj.backward[i] = {};\n lanesObj.backward[i][key] = l;\n });\n }\n if (data.unspecified) {\n data.unspecified.forEach(function(l, i) {\n if (!lanesObj.unspecified[i]) lanesObj.unspecified[i] = {};\n lanesObj.unspecified[i][key] = l;\n });\n }\n}\n", "import { geoArea as d3_geoArea } from 'd3-geo';\n\nimport { geoExtent, geoVecCross } from '../geo';\nimport { osmEntity } from './entity';\nimport { osmLanes } from './lanes';\nimport { osmTagSuggestingArea, osmRightSideIsInsideTags, osmRemoveLifecyclePrefix, osmOneWayBiDirectionalTags, osmOneWayBackwardTags, osmOneWayForwardTags, osmOneWayTags } from './tags';\nimport { utilArrayUniq, utilCheckTagDictionary } from '../util';\n\n/**\n * @typedef {typeof prototype & iD.AbstractEntity} OsmWay\n * @returns {OsmWay}\n */\nexport function osmWay() {\n if (!(this instanceof osmWay)) {\n return (new osmWay()).initialize(arguments);\n } else if (arguments.length) {\n this.initialize(arguments);\n }\n}\n\n\nosmEntity.way = osmWay;\n\nosmWay.prototype = Object.create(osmEntity.prototype);\n\n\nconst prototype = {\n type: 'way',\n nodes: [],\n\n\n copy: function(resolver, copies) {\n if (copies[this.id]) return copies[this.id];\n\n var copy = osmEntity.prototype.copy.call(this, resolver, copies);\n\n var nodes = this.nodes.map(function(id) {\n return resolver.entity(id).copy(resolver, copies).id;\n });\n\n copy = copy.update({ nodes: nodes });\n copies[this.id] = copy;\n\n return copy;\n },\n\n\n extent: function(resolver) {\n return resolver.transient(this, 'extent', function() {\n var extent = geoExtent();\n for (var i = 0; i < this.nodes.length; i++) {\n var node = resolver.hasEntity(this.nodes[i]);\n if (node) {\n extent._extend(node.extent());\n }\n }\n return extent;\n });\n },\n\n\n first: function() {\n return this.nodes[0];\n },\n\n\n last: function() {\n return this.nodes[this.nodes.length - 1];\n },\n\n\n contains: function(node) {\n return this.nodes.indexOf(node) >= 0;\n },\n\n\n affix: function(node) {\n if (this.nodes[0] === node) return 'prefix';\n if (this.nodes[this.nodes.length - 1] === node) return 'suffix';\n },\n\n\n layer: function() {\n // explicit layer tag, clamp between -10, 10..\n if (isFinite(this.tags.layer)) {\n return Math.max(-10, Math.min(+(this.tags.layer), 10));\n }\n\n // implied layer tag..\n if (this.tags.covered === 'yes') return -1;\n if (this.tags.location === 'overground') return 1;\n if (this.tags.location === 'underground') return -1;\n if (this.tags.location === 'underwater') return -10;\n\n if (this.tags.power === 'line') return 10;\n if (this.tags.power === 'minor_line') return 10;\n if (this.tags.aerialway) return 10;\n if (this.tags.bridge) return 1;\n if (this.tags.cutting) return -1;\n if (this.tags.tunnel) return -1;\n if (this.tags.waterway) return -1;\n if (this.tags.man_made === 'pipeline') return -10;\n if (this.tags.boundary) return -10;\n return 0;\n },\n\n\n // the approximate width of the line based on its tags except its `width` tag\n impliedLineWidthMeters: function() {\n var averageWidths = {\n highway: { // width is for single lane\n motorway: 5, motorway_link: 5, trunk: 4.5, trunk_link: 4.5,\n primary: 4, secondary: 4, tertiary: 4,\n primary_link: 4, secondary_link: 4, tertiary_link: 4,\n unclassified: 4, road: 4, living_street: 4, bus_guideway: 4, busway: 4, pedestrian: 4,\n residential: 3.5, service: 3.5, track: 3, cycleway: 2.5,\n bridleway: 2, corridor: 2, steps: 2, path: 1.5, footway: 1.5, ladder: 0.5,\n },\n railway: { // width includes ties and rail bed, not just track gauge\n rail: 2.5, light_rail: 2.5, tram: 2.5, subway: 2.5,\n monorail: 2.5, funicular: 2.5, disused: 2.5, preserved: 2.5,\n miniature: 1.5, narrow_gauge: 1.5\n },\n waterway: {\n river: 50, canal: 25, stream: 5, tidal_channel: 5, fish_pass: 2.5, drain: 2.5, ditch: 1.5\n }\n };\n for (var key in averageWidths) {\n if (this.tags[key] && averageWidths[key][this.tags[key]]) {\n var width = averageWidths[key][this.tags[key]];\n if (key === 'highway') {\n var laneCount = this.tags.lanes && parseInt(this.tags.lanes, 10);\n if (!laneCount) laneCount = this.isOneWay() ? 1 : 2;\n\n return width * laneCount;\n }\n return width;\n }\n }\n return null;\n },\n\n\n /** @returns {boolean} for example, if `oneway=yes` */\n isOneWayForwards() {\n if (this.tags.oneway === 'no') return false;\n\n return !!utilCheckTagDictionary(this.tags, osmOneWayForwardTags);\n },\n\n /** @returns {boolean} for example, if `oneway=-1` */\n isOneWayBackwards() {\n if (this.tags.oneway === 'no') return false;\n\n return !!utilCheckTagDictionary(this.tags, osmOneWayBackwardTags);\n },\n\n /** @returns {boolean} for example, if `oneway=alternating` */\n isBiDirectional() {\n if (this.tags.oneway === 'no') return false;\n\n return !!utilCheckTagDictionary(this.tags, osmOneWayBiDirectionalTags);\n },\n\n /** @returns {boolean} */\n isOneWay() {\n if (this.tags.oneway === 'no') return false;\n\n return !!utilCheckTagDictionary(this.tags, osmOneWayTags);\n },\n\n // Some identifier for tag that implies that this way is \"sided\",\n // i.e. the right side is the 'inside' (e.g. the right side of a\n // natural=cliff is lower).\n sidednessIdentifier: function() {\n for (const realKey in this.tags) {\n const value = this.tags[realKey];\n const key = osmRemoveLifecyclePrefix(realKey);\n if (key in osmRightSideIsInsideTags && (value in osmRightSideIsInsideTags[key])) {\n if (osmRightSideIsInsideTags[key][value] === true) {\n return key;\n } else {\n // if the map's value is something other than a\n // literal true, we should use it so we can\n // special case some keys (e.g. natural=coastline\n // is handled differently to other naturals).\n return osmRightSideIsInsideTags[key][value];\n }\n }\n }\n\n return null;\n },\n\n isSided: function() {\n if (this.tags.two_sided === 'yes') {\n return false;\n }\n\n return this.sidednessIdentifier() !== null;\n },\n\n lanes: function() {\n return osmLanes(this);\n },\n\n\n isClosed: function() {\n return this.nodes.length > 1 && this.first() === this.last();\n },\n\n\n isConvex: function(resolver) {\n if (!this.isClosed() || this.isDegenerate()) return null;\n\n const nodes = utilArrayUniq(resolver.childNodes(this));\n const coords = nodes.map(function(n) { return n.loc; });\n let prev = 0;\n\n for (var i = 0; i < coords.length; i++) {\n var o = coords[(i+1) % coords.length];\n var a = coords[i];\n var b = coords[(i+2) % coords.length];\n var res = geoVecCross(a, b, o);\n\n const curr = (res > 0) ? 1 : (res < 0) ? -1 : 0;\n if (curr === 0) {\n continue;\n } else if (prev && curr !== prev) {\n return false;\n }\n prev = curr;\n }\n return true;\n },\n\n // returns an object with the tag that implies this is an area, if any\n tagSuggestingArea: function() {\n return osmTagSuggestingArea(this.tags);\n },\n\n isArea: function() {\n if (this.tags.area === 'yes') return true;\n if (!this.isClosed() || this.tags.area === 'no') return false;\n return this.tagSuggestingArea() !== null;\n },\n\n\n isDegenerate: function() {\n return (new Set(this.nodes).size < (this.isClosed() ? 3 : 2));\n },\n\n\n areAdjacent: function(n1, n2) {\n for (var i = 0; i < this.nodes.length; i++) {\n if (this.nodes[i] === n1) {\n if (this.nodes[i - 1] === n2) return true;\n if (this.nodes[i + 1] === n2) return true;\n }\n }\n return false;\n },\n\n\n geometry: function(graph) {\n return graph.transient(this, 'geometry', function() {\n return this.isArea() ? 'area' : 'line';\n });\n },\n\n\n // returns an array of objects representing the segments between the nodes in this way\n segments: function(graph) {\n\n function segmentExtent(graph) {\n var n1 = graph.hasEntity(this.nodes[0]);\n var n2 = graph.hasEntity(this.nodes[1]);\n return n1 && n2 && geoExtent([\n [\n Math.min(n1.loc[0], n2.loc[0]),\n Math.min(n1.loc[1], n2.loc[1])\n ],\n [\n Math.max(n1.loc[0], n2.loc[0]),\n Math.max(n1.loc[1], n2.loc[1])\n ]\n ]);\n }\n\n return graph.transient(this, 'segments', function() {\n var segments = [];\n for (var i = 0; i < this.nodes.length - 1; i++) {\n segments.push({\n id: this.id + '-' + i,\n wayId: this.id,\n index: i,\n nodes: [this.nodes[i], this.nodes[i + 1]],\n extent: segmentExtent\n });\n }\n return segments;\n });\n },\n\n\n // If this way is not closed, append the beginning node to the end of the nodelist to close it.\n close: function() {\n if (this.isClosed() || !this.nodes.length) return this;\n\n var nodes = this.nodes.slice();\n nodes = nodes.filter(noRepeatNodes);\n nodes.push(nodes[0]);\n return this.update({ nodes: nodes });\n },\n\n\n // If this way is closed, remove any connector nodes from the end of the nodelist to unclose it.\n unclose: function() {\n if (!this.isClosed()) return this;\n\n var nodes = this.nodes.slice();\n var connector = this.first();\n var i = nodes.length - 1;\n\n // remove trailing connectors..\n while (i > 0 && nodes.length > 1 && nodes[i] === connector) {\n nodes.splice(i, 1);\n i = nodes.length - 1;\n }\n\n nodes = nodes.filter(noRepeatNodes);\n return this.update({ nodes: nodes });\n },\n\n\n // Adds a node (id) in front of the node which is currently at position index.\n // If index is undefined, the node will be added to the end of the way for linear ways,\n // or just before the final connecting node for circular ways.\n // Consecutive duplicates are eliminated including existing ones.\n // Circularity is always preserved when adding a node.\n addNode: function(id, index) {\n var nodes = this.nodes.slice();\n var isClosed = this.isClosed();\n var max = isClosed ? nodes.length - 1 : nodes.length;\n\n if (index === undefined) {\n index = max;\n }\n\n if (index < 0 || index > max) {\n throw new RangeError('index ' + index + ' out of range 0..' + max);\n }\n\n // If this is a closed way, remove all connector nodes except the first one\n // (there may be duplicates) and adjust index if necessary..\n if (isClosed) {\n var connector = this.first();\n\n // leading connectors..\n var i = 1;\n while (i < nodes.length && nodes.length > 2 && nodes[i] === connector) {\n nodes.splice(i, 1);\n if (index > i) index--;\n }\n\n // trailing connectors..\n i = nodes.length - 1;\n while (i > 0 && nodes.length > 1 && nodes[i] === connector) {\n nodes.splice(i, 1);\n if (index > i) index--;\n i = nodes.length - 1;\n }\n }\n\n nodes.splice(index, 0, id);\n nodes = nodes.filter(noRepeatNodes);\n\n // If the way was closed before, append a connector node to keep it closed..\n if (isClosed && (nodes.length === 1 || nodes[0] !== nodes[nodes.length - 1])) {\n nodes.push(nodes[0]);\n }\n\n return this.update({ nodes: nodes });\n },\n\n\n // Replaces the node which is currently at position index with the given node (id).\n // Consecutive duplicates are eliminated including existing ones.\n // Circularity is preserved when updating a node.\n updateNode: function(id, index) {\n var nodes = this.nodes.slice();\n var isClosed = this.isClosed();\n var max = nodes.length - 1;\n\n if (index === undefined || index < 0 || index > max) {\n throw new RangeError('index ' + index + ' out of range 0..' + max);\n }\n\n // If this is a closed way, remove all connector nodes except the first one\n // (there may be duplicates) and adjust index if necessary..\n if (isClosed) {\n var connector = this.first();\n\n // leading connectors..\n var i = 1;\n while (i < nodes.length && nodes.length > 2 && nodes[i] === connector) {\n nodes.splice(i, 1);\n if (index > i) index--;\n }\n\n // trailing connectors..\n i = nodes.length - 1;\n while (i > 0 && nodes.length > 1 && nodes[i] === connector) {\n nodes.splice(i, 1);\n if (index === i) index = 0; // update leading connector instead\n i = nodes.length - 1;\n }\n }\n\n nodes.splice(index, 1, id);\n nodes = nodes.filter(noRepeatNodes);\n\n // If the way was closed before, append a connector node to keep it closed..\n if (isClosed && (nodes.length === 1 || nodes[0] !== nodes[nodes.length - 1])) {\n nodes.push(nodes[0]);\n }\n\n return this.update({nodes: nodes});\n },\n\n\n // Replaces each occurrence of node id needle with replacement.\n // Consecutive duplicates are eliminated including existing ones.\n // Circularity is preserved.\n replaceNode: function(needleID, replacementID) {\n var nodes = this.nodes.slice();\n var isClosed = this.isClosed();\n\n for (var i = 0; i < nodes.length; i++) {\n if (nodes[i] === needleID) {\n nodes[i] = replacementID;\n }\n }\n\n nodes = nodes.filter(noRepeatNodes);\n\n // If the way was closed before, append a connector node to keep it closed..\n if (isClosed && (nodes.length === 1 || nodes[0] !== nodes[nodes.length - 1])) {\n nodes.push(nodes[0]);\n }\n\n return this.update({nodes: nodes});\n },\n\n\n // Removes each occurrence of node id.\n // Consecutive duplicates are eliminated including existing ones.\n // Circularity is preserved.\n removeNode: function(id) {\n var nodes = this.nodes.slice();\n var isClosed = this.isClosed();\n\n nodes = nodes\n .filter(function(node) { return node !== id; })\n .filter(noRepeatNodes);\n\n // If the way was closed before, append a connector node to keep it closed..\n if (isClosed && (nodes.length === 1 || nodes[0] !== nodes[nodes.length - 1])) {\n nodes.push(nodes[0]);\n }\n\n return this.update({nodes: nodes});\n },\n\n\n asJXON: function(changeset_id) {\n var r = {\n way: {\n '@id': this.osmId(),\n '@version': this.version || 0,\n nd: this.nodes.map(function(id) {\n return { keyAttributes: { ref: osmEntity.id.toOSM(id) } };\n }, this),\n tag: Object.keys(this.tags).map(function(k) {\n return { keyAttributes: { k: k, v: this.tags[k] } };\n }, this)\n }\n };\n if (changeset_id) {\n r.way['@changeset'] = changeset_id;\n }\n return r;\n },\n\n\n asGeoJSON: function(resolver) {\n return resolver.transient(this, 'GeoJSON', function() {\n var coordinates = resolver.childNodes(this)\n .map(function(n) { return n.loc; });\n\n if (this.isArea() && this.isClosed()) {\n return {\n type: 'Polygon',\n coordinates: [coordinates]\n };\n } else {\n return {\n type: 'LineString',\n coordinates: coordinates\n };\n }\n });\n },\n\n\n area: function(resolver) {\n return resolver.transient(this, 'area', function() {\n var nodes = resolver.childNodes(this);\n\n var json = {\n type: 'Polygon',\n coordinates: [ nodes.map(function(n) { return n.loc; }) ]\n };\n\n if (!this.isClosed() && nodes.length) {\n json.coordinates[0].push(nodes[0].loc);\n }\n\n var area = d3_geoArea(json);\n\n // Heuristic for detecting counterclockwise winding order. Assumes\n // that OpenStreetMap polygons are not hemisphere-spanning.\n if (area > 2 * Math.PI) {\n json.coordinates[0].reverse();\n area = d3_geoArea(json);\n }\n\n return isNaN(area) ? 0 : area;\n });\n }\n};\nObject.assign(osmWay.prototype, prototype);\n\n\n// Filter function to eliminate consecutive duplicates.\nfunction noRepeatNodes(node, i, arr) {\n return i === 0 || node !== arr[i - 1];\n}\n", "export class QAItem {\n constructor(loc, service, itemType, id, props) {\n // Store required properties\n this.loc = loc;\n this.service = service.title;\n this.itemType = itemType;\n\n // All issues must have an ID for selection, use generic if none specified\n this.id = id ? id : `${QAItem.id()}`;\n\n this.update(props);\n\n // Some QA services have marker icons to differentiate issues\n if (service && typeof service.getIcon === 'function') {\n this.icon = service.getIcon(itemType);\n }\n }\n\n update(props) {\n // You can't override this initial information\n const { loc, service, itemType, id } = this;\n\n Object.keys(props).forEach(prop => this[prop] = props[prop]);\n\n this.loc = loc;\n this.service = service;\n this.itemType = itemType;\n this.id = id;\n\n return this;\n }\n\n // Generic handling for newly created QAItems\n static id() {\n return this.nextId--;\n }\n}\nQAItem.nextId = -1;\n", "import { geoSphericalDistance } from '../geo/geo';\nimport { osmRelation } from '../osm/relation';\nimport { osmWay } from '../osm/way';\nimport { utilArrayIntersection, utilWrap } from '../util';\nimport { osmSummableTags } from '../osm/tags';\n\n\n// Split a way at the given node.\n//\n// Optionally, split only the given ways, if multiple ways share\n// the given node.\n//\n// This is the inverse of `iD.actionJoin`.\n//\n// For testing convenience, accepts an ID to assign to the new way.\n// Normally, this will be undefined and the way will automatically\n// be assigned a new ID.\n//\n// Reference:\n// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as\n//\nexport function actionSplit(nodeIds, newWayIds) {\n // accept single ID for backwards-compatibility\n if (typeof nodeIds === 'string') nodeIds = [nodeIds];\n\n var _wayIDs;\n // the strategy for picking which way will have a new version and which way is newly created\n var _keepHistoryOn = 'longest'; // 'longest', 'first'\n\n // these closed ways have to be treated in a special way when contained in a (route) relation\n const circularJunctions = ['roundabout', 'circular'];\n\n // The IDs of the ways actually created by running this action\n var _createdWayIDs = [];\n\n function dist(graph, nA, nB) {\n var locA = graph.entity(nA).loc;\n var locB = graph.entity(nB).loc;\n var epsilon = 1e-6;\n return (locA && locB) ? geoSphericalDistance(locA, locB) : epsilon;\n }\n\n // If the way is closed, we need to search for a partner node\n // to split the way at.\n //\n // The following looks for a node that is both far away from\n // the initial node in terms of way segment length and nearby\n // in terms of beeline-distance. This assures that areas get\n // split on the most \"natural\" points (independent of the number\n // of nodes).\n // For example: bone-shaped areas get split across their waist\n // line, circles across the diameter.\n function splitArea(nodes, idxA, graph) {\n var lengths = new Array(nodes.length);\n var length;\n var i;\n var best = 0;\n var idxB;\n\n function wrap(index) {\n return utilWrap(index, nodes.length);\n }\n\n // calculate lengths\n length = 0;\n for (i = wrap(idxA + 1); i !== idxA; i = wrap(i + 1)) {\n length += dist(graph, nodes[i], nodes[wrap(i - 1)]);\n lengths[i] = length;\n }\n\n length = 0;\n for (i = wrap(idxA - 1); i !== idxA; i = wrap(i - 1)) {\n length += dist(graph, nodes[i], nodes[wrap(i + 1)]);\n if (length < lengths[i]) {\n lengths[i] = length;\n }\n }\n\n // determine best opposite node to split\n for (i = 0; i < nodes.length; i++) {\n var cost = lengths[i] / dist(graph, nodes[idxA], nodes[i]);\n if (cost > best) {\n idxB = i;\n best = cost;\n }\n }\n\n return idxB;\n }\n\n function totalLengthBetweenNodes(graph, nodes) {\n var totalLength = 0;\n for (var i = 0; i < nodes.length - 1; i++) {\n totalLength += dist(graph, nodes[i], nodes[i + 1]);\n }\n return totalLength;\n }\n\n function split(graph, nodeId, wayA, newWayId, otherNodeIds) {\n var wayB = osmWay({ id: newWayId, tags: wayA.tags }); // `wayB` is the NEW way\n var nodesA;\n var nodesB;\n var isArea = wayA.isArea();\n\n if (wayA.isClosed()) {\n var nodes = wayA.nodes.slice(0, -1);\n var idxA = nodes.indexOf(nodeId);\n var idxB = otherNodeIds.length > 0 ? nodes.indexOf(otherNodeIds[0]) : splitArea(nodes, idxA, graph);\n\n if (idxB < idxA) {\n nodesA = nodes.slice(idxA).concat(nodes.slice(0, idxB + 1));\n nodesB = nodes.slice(idxB, idxA + 1);\n } else {\n nodesA = nodes.slice(idxA, idxB + 1);\n nodesB = nodes.slice(idxB).concat(nodes.slice(0, idxA + 1));\n }\n } else {\n var idx = wayA.nodes.indexOf(nodeId, 1);\n nodesA = wayA.nodes.slice(0, idx + 1);\n nodesB = wayA.nodes.slice(idx);\n }\n\n var lengthA = totalLengthBetweenNodes(graph, nodesA);\n var lengthB = totalLengthBetweenNodes(graph, nodesB);\n\n if (_keepHistoryOn === 'longest' &&\n lengthB > lengthA) {\n // keep the history on the longer way, regardless of the node count\n wayA = wayA.update({ nodes: nodesB });\n wayB = wayB.update({ nodes: nodesA });\n\n var temp = lengthA;\n lengthA = lengthB;\n lengthB = temp;\n } else {\n wayA = wayA.update({ nodes: nodesA });\n wayB = wayB.update({ nodes: nodesB });\n }\n\n for (const key in wayA.tags) {\n if (!osmSummableTags.has(key)) continue;\n\n // divide up the the e.g. step count proportionally between the two ways\n var count = Number(wayA.tags[key]);\n if (count &&\n // ensure a number\n isFinite(count) &&\n // ensure positive\n count > 0 &&\n // ensure integer\n Math.round(count) === count) {\n var tagsA = Object.assign({}, wayA.tags);\n var tagsB = Object.assign({}, wayB.tags);\n\n var ratioA = lengthA / (lengthA + lengthB);\n var countA = Math.round(count * ratioA);\n tagsA[key] = countA.toString();\n tagsB[key] = (count - countA).toString();\n\n wayA = wayA.update({ tags: tagsA });\n wayB = wayB.update({ tags: tagsB });\n }\n }\n\n graph = graph.replace(wayA);\n graph = graph.replace(wayB);\n\n graph.parentRelations(wayA).forEach(function(relation) {\n if (relation.hasFromViaTo()) {\n // Turn restrictions - make sure:\n // 1. Splitting a FROM/TO way - only `wayA` OR `wayB` remains in relation\n // (whichever one is connected to the VIA node/ways)\n // 2. Splitting a VIA way - `wayB` remains in relation as a VIA way\n var f = relation.memberByRole('from');\n var v = [\n ...relation.membersByRole('via'),\n ...relation.membersByRole('intersection'),\n ];\n var t = relation.memberByRole('to');\n var i;\n\n if (f.id === wayA.id || t.id === wayA.id) {\n // 1. split a FROM/TO\n var keepB = false;\n if (v.length === 1 && v[0].type === 'node') { // check via node\n keepB = wayB.contains(v[0].id);\n } else { // check via way(s)\n for (i = 0; i < v.length; i++) {\n if (v[i].type === 'way') {\n var wayVia = graph.hasEntity(v[i].id);\n if (wayVia && utilArrayIntersection(wayB.nodes, wayVia.nodes).length) {\n keepB = true;\n break;\n }\n }\n }\n }\n\n if (keepB) {\n relation = relation.replaceMember(wayA, wayB);\n graph = graph.replace(relation);\n }\n\n } else {\n // 2. split a VIA\n for (i = 0; i < v.length; i++) {\n if (v[i].type === 'way' && v[i].id === wayA.id) {\n graph = splitWayMember(graph, relation.id, wayA, wayB);\n }\n }\n }\n\n } else {\n if (isArea) return; // handled below\n // All other relations (Routes, Multipolygons, etc):\n // both `wayA` and `wayB` remain in the relation,\n // but must be inserted in the correct order\n graph = splitWayMember(graph, relation.id, wayA, wayB);\n }\n });\n\n if (isArea) {\n const areaTags = {\n ...wayA.tags,\n type: 'multipolygon'\n };\n const lineTags = {};\n if (areaTags.natural === 'coastline') {\n // preserve coastline tag on a polygonal way when it is split, #9563\n delete areaTags.natural;\n lineTags.natural = 'coastline';\n }\n const multipolygon = osmRelation({\n tags: areaTags,\n members: [\n { id: wayA.id, role: 'outer', type: 'way' },\n { id: wayB.id, role: 'outer', type: 'way' }\n ]\n });\n\n graph = graph.replace(multipolygon);\n for (const relation of graph.parentRelations(wayA)) {\n if (relation.id === multipolygon.id) continue;\n // transfer memberships to the multipolygon, #12024\n graph = graph.replace(relation.replaceMember(wayA, multipolygon));\n }\n\n graph = graph.replace(wayA.update({ tags: lineTags }));\n graph = graph.replace(wayB.update({ tags: lineTags }));\n }\n\n _createdWayIDs.push(wayB.id);\n\n return graph;\n }\n\n // Handles (most kinds of) parent relations of a way that is split:\n // We need to find the correct order to insert the newly created way\n // relative to the existing way.\n //\n // This applies some heuristics to find the most likely correct order to\n // perform the operation, working under the assumption that the members\n // of the relation are already \"properly\" sorted and that the relevant\n // member entities are loaded in graph: The new way is inserted into the\n // relation before or after the existing way depending on how the old/new\n // way connect to their neighboring members.\n //\n // As this is a local operation, it means that even if these conditions\n // are not met, the order of the relation members will at most be incorrect\n // between the existing and newly created way; other relation members are\n // kept unmodified.\n function splitWayMember(graph, relationId, wayA, wayB) {\n // returns true if way1 connects to way2 at either end node, or if one\n // of the two ways is tagged as a \"roundabout\" and connects to the other\n // way at any of its nodes.\n function connects(way1, way2) {\n if (way1.nodes.length < 2 || way2.nodes.length < 2) return false;\n if (circularJunctions.includes(way1.tags.junction) && way1.isClosed()) {\n return way1.nodes.some(nodeId =>\n nodeId === way2.nodes[0] ||\n nodeId === way2.nodes[way2.nodes.length - 1]);\n } else if (circularJunctions.includes(way2.tags.junction) && way2.isClosed()) {\n return way2.nodes.some(nodeId =>\n nodeId === way1.nodes[0] ||\n nodeId === way1.nodes[way1.nodes.length - 1]);\n }\n if (way1.nodes[0] === way2.nodes[0]) return true;\n if (way1.nodes[0] === way2.nodes[way2.nodes.length - 1]) return true;\n if (way1.nodes[way1.nodes.length - 1] === way2.nodes[way2.nodes.length - 1]) return true;\n if (way1.nodes[way1.nodes.length - 1] === way2.nodes[0]) return true;\n return false;\n }\n\n let relation = graph.entity(relationId);\n // insertMembers stores the positions where the new way (wayB) is to be inserted\n // into the parent relation\n const insertMembers = [];\n const members = relation.members;\n for (let i = 0; i < members.length; i++) {\n const member = members[i];\n if (member.id === wayA.id) { // wayA is the existing way\n // determine connection matrix of wayA, wayB and their neighboring members\n let wayAconnectsPrev = false;\n let wayAconnectsNext = false;\n let wayBconnectsPrev = false;\n let wayBconnectsNext = false;\n if (i > 0 && graph.hasEntity(members[i - 1].id)) {\n const prevEntity = graph.entity(members[i - 1].id);\n if (prevEntity.type === 'way') {\n wayAconnectsPrev = connects(prevEntity, wayA);\n wayBconnectsPrev = connects(prevEntity, wayB);\n }\n }\n if (i < members.length - 1 && graph.hasEntity(members[i + 1].id)) {\n const nextEntity = graph.entity(members[i + 1].id);\n if (nextEntity.type === 'way') {\n wayAconnectsNext = connects(nextEntity, wayA);\n wayBconnectsNext = connects(nextEntity, wayB);\n }\n }\n // possible outcomes of connection matrix\n //\n // \u27CD 0 0 1 1 <- wayA connects to next member\n // \u27CD 0 1 0 1 <- wayB connects to next member\n // +---+---+---+---+\n // 0 0 | ? | \u2192 | \u2190 | * | \u2192 ... wayB should be inserted after wayA\n // +---+---+---+---+ \u2190 ... wayB should be inserted before wayA\n // 0 1 | \u2190 | x | \u2190 | \u2190 | \u21BA ... members form a loop\n // +---+---+---+---+ ? ... wayA/B do not connect to their neighbor members\n // 1 0 | \u2192 | \u2192 | x | \u2192 | x ... undefined state\n // +---+---+---+---+ * ... undefined state (any order results in a connection)\n // 1 1 | * | \u2192 | \u2190 | \u21BA |\n // +---+---+---+---+\n // ^ ^\n // | |\n // | +-- wayB connects to previous member\n // +---- wayA connects to previous member\n //\n // These boolean conditions can be simplified to the following conditions\n // (considering the outcome as arbitrary for the undefined \"*\" cases),\n // i.e. wayB should be inserted after wayA if:\n // * wayA connects the the previous member but not the next one, or\n // * wayB connects to the next member but not the previous one, and wayA's connectivity does not contradict that\n // (and vice versa)\n // the remaining cases to be handles specifically are:\n // * unconnected ways\n // * members for a loop\n // * a few invalid/undefined cases (e.g. forks with no proper solution)\n if (wayAconnectsPrev && !wayAconnectsNext ||\n !wayBconnectsPrev && wayBconnectsNext && !(!wayAconnectsPrev && wayAconnectsNext)\n ) {\n insertMembers.push({at: i + 1, role: member.role});\n continue;\n }\n if (!wayAconnectsPrev && wayAconnectsNext ||\n wayBconnectsPrev && !wayBconnectsNext && !(wayAconnectsPrev && !wayAconnectsNext)\n ) {\n insertMembers.push({at: i, role: member.role});\n continue;\n }\n // loops: try to look one further member ahead/behind to resolve the connectivity\n if (wayAconnectsPrev && wayBconnectsPrev && wayAconnectsNext && wayBconnectsNext) {\n // look one further member ahead\n if (i > 2 && graph.hasEntity(members[i - 2].id)) {\n const prev2Entity = graph.entity(members[i - 2].id);\n if (connects(prev2Entity, wayA) && !connects(prev2Entity, wayB)) {\n // prev-2 member connects only to A: insert B before A\n insertMembers.push({at: i, role: member.role});\n continue;\n }\n if (connects(prev2Entity, wayB) && !connects(prev2Entity, wayA)) {\n // prev-2 member connects only to B: insert B after A\n insertMembers.push({at: i + 1, role: member.role});\n continue;\n }\n }\n // look one further member behind\n if (i < members.length - 2 && graph.hasEntity(members[i + 2].id)) {\n const next2Entity = graph.entity(members[i + 2].id);\n if (connects(next2Entity, wayA) && !connects(next2Entity, wayB)) {\n // next+2 member connects only to A: insert B after A\n insertMembers.push({at: i + 1, role: member.role});\n continue;\n }\n if (connects(next2Entity, wayB) && !connects(next2Entity, wayA)) {\n // next+2 member connects only to B: insert B before A\n insertMembers.push({at: i, role: member.role});\n continue;\n }\n }\n }\n\n // could not determine how new member should connect (e.g. existing way was not\n // connected to other member ways): insert them in the original orientation of wayA\n if (wayA.nodes[wayA.nodes.length - 1] === wayB.nodes[0]) {\n insertMembers.push({at: i + 1, role: member.role});\n } else {\n insertMembers.push({at: i, role: member.role});\n }\n }\n }\n // insert new member(s) at the determined indices\n insertMembers.reverse().forEach(item => {\n graph = graph.replace(relation.addMember({\n id: wayB.id,\n type: 'way',\n role: item.role\n }, item.at));\n relation = graph.entity(relation.id);\n });\n return graph;\n }\n\n const action = function(graph) {\n _createdWayIDs = [];\n let newWayIndex = 0;\n for (const i in nodeIds) {\n const nodeId = nodeIds[i];\n const candidates = waysForNodes(nodeIds.slice(i), graph);\n for (const candidate of candidates) {\n graph = split(graph, nodeId, candidate, newWayIds && newWayIds[newWayIndex], nodeIds.slice(i + 1));\n newWayIndex += 1;\n }\n }\n return graph;\n };\n\n action.getCreatedWayIDs = function() {\n return _createdWayIDs;\n };\n\n function waysForNodes(nodeIds, graph) {\n const splittableWays = nodeIds\n .map(nodeId => waysForNode(nodeId, graph))\n .reduce((cur, acc) => utilArrayIntersection(cur, acc));\n\n if (!_wayIDs) {\n // If the ways to split aren't specified, only split the lines.\n // If there are no lines to split, split the areas.\n const hasLine = splittableWays.some(way => way.geometry(graph) === 'line');\n if (hasLine) {\n return splittableWays.filter(way => way.geometry(graph) === 'line');\n }\n }\n\n return splittableWays;\n }\n\n function waysForNode(nodeId, graph) {\n const node = graph.entity(nodeId);\n return graph.parentWays(node).filter(isSplittable);\n\n function isSplittable(way) {\n // If the ways to split are specified, ignore everything else.\n if (_wayIDs && _wayIDs.indexOf(way.id) === -1) return false;\n\n // We can fake splitting closed ways at their endpoints...\n if (way.isClosed()) return true;\n\n // otherwise, we can't split nodes at their endpoints.\n for (let i = 1; i < way.nodes.length - 1; i++) {\n if (way.nodes[i] === nodeId) return true;\n }\n return false;\n }\n };\n\n action.ways = function(graph) {\n return waysForNodes(nodeIds, graph);\n };\n\n\n action.disabled = function(graph) {\n const candidates = waysForNodes(nodeIds, graph);\n if (candidates.length === 0 || (_wayIDs && _wayIDs.length !== candidates.length)) {\n return 'not_eligible';\n }\n for (const way of candidates) {\n const parentRelations = graph.parentRelations(way);\n for (const parentRelation of parentRelations) {\n if (parentRelation.hasFromViaTo()) {\n // turn restrictions: via members must be loaded\n const vias = [\n ...parentRelation.membersByRole('via'),\n ...parentRelation.membersByRole('intersection'),\n ];\n if (!vias.every(via => graph.hasEntity(via.id))) {\n return 'parent_incomplete';\n }\n } else {\n // other relations (e.g. route relations): at least one members before or after way must be present\n for (let i = 0; i < parentRelation.members.length; i++) {\n if (parentRelation.members[i].id === way.id) {\n const memberBeforePresent = i > 0 && graph.hasEntity(parentRelation.members[i - 1].id);\n const memberAfterPresent = i < parentRelation.members.length - 1 && graph.hasEntity(parentRelation.members[i + 1].id);\n if (!memberBeforePresent && !memberAfterPresent && parentRelation.members.length > 1) {\n return 'parent_incomplete';\n }\n }\n }\n }\n const relTypesExceptions = ['junction', 'enforcement']; // some relation types should not prehibit a member from being split\n if (circularJunctions.includes(way.tags.junction) &&\n way.isClosed() &&\n !relTypesExceptions.includes(parentRelation.tags.type)) {\n return 'simple_roundabout';\n }\n }\n }\n };\n\n\n action.limitWays = function(val) {\n if (!arguments.length) return _wayIDs;\n _wayIDs = val;\n return action;\n };\n\n\n action.keepHistoryOn = function(val) {\n if (!arguments.length) return _keepHistoryOn;\n _keepHistoryOn = val;\n return action;\n };\n\n\n return action;\n}\n", "import { debug } from '../index';\nimport { utilArrayDifference } from '../util';\n\n\nexport function coreGraph(other, mutable) {\n if (!(this instanceof coreGraph)) return new coreGraph(other, mutable);\n\n if (other instanceof coreGraph) {\n var base = other.base();\n this.entities = Object.assign(Object.create(base.entities), other.entities);\n this._parentWays = Object.assign(Object.create(base.parentWays), other._parentWays);\n this._parentRels = Object.assign(Object.create(base.parentRels), other._parentRels);\n\n } else {\n this.entities = Object.create({});\n this._parentWays = Object.create({});\n this._parentRels = Object.create({});\n this.rebase(other || [], [this]);\n }\n\n this.transients = {};\n this._childNodes = {};\n this.frozen = !mutable;\n}\n\n\ncoreGraph.prototype = {\n\n hasEntity: function(id) {\n return this.entities[id];\n },\n\n\n entity: function(id) {\n var entity = this.entities[id];\n\n if (!entity) {\n throw new Error('entity ' + id + ' not found');\n }\n return entity;\n },\n\n\n geometry: function(id) {\n return this.entity(id).geometry(this);\n },\n\n\n transient: function(entity, key, fn) {\n var id = entity.id;\n var transients = this.transients[id] || (this.transients[id] = {});\n\n if (transients[key] !== undefined) {\n return transients[key];\n }\n\n transients[key] = fn.call(entity);\n\n return transients[key];\n },\n\n\n parentWays: function(entity) {\n var parents = this._parentWays[entity.id];\n var result = [];\n if (parents) {\n parents.forEach(function(id) {\n result.push(this.entity(id));\n }, this);\n }\n return result;\n },\n\n\n isPoi: function(entity) {\n var parents = this._parentWays[entity.id];\n return !parents || parents.size === 0;\n },\n\n\n isShared: function(entity) {\n var parents = this._parentWays[entity.id];\n return parents && parents.size > 1;\n },\n\n\n parentRelations: function(entity) {\n var parents = this._parentRels[entity.id];\n var result = [];\n if (parents) {\n parents.forEach(function(id) {\n result.push(this.entity(id));\n }, this);\n }\n return result;\n },\n\n parentMultipolygons: function(entity) {\n return this.parentRelations(entity).filter(function(relation) {\n return relation.isMultipolygon();\n });\n },\n\n\n childNodes: function(entity) {\n if (this._childNodes[entity.id]) return this._childNodes[entity.id];\n if (!entity.nodes) return [];\n\n var nodes = [];\n for (var i = 0; i < entity.nodes.length; i++) {\n nodes[i] = this.entity(entity.nodes[i]);\n }\n\n if (debug) Object.freeze(nodes);\n\n this._childNodes[entity.id] = nodes;\n return this._childNodes[entity.id];\n },\n\n\n base: function() {\n return {\n 'entities': Object.getPrototypeOf(this.entities),\n 'parentWays': Object.getPrototypeOf(this._parentWays),\n 'parentRels': Object.getPrototypeOf(this._parentRels)\n };\n },\n\n\n // Unlike other graph methods, rebase mutates in place. This is because it\n // is used only during the history operation that merges newly downloaded\n // data into each state. To external consumers, it should appear as if the\n // graph always contained the newly downloaded data.\n rebase: function(entities, stack, force) {\n var base = this.base();\n var i, j, k, id;\n\n for (i = 0; i < entities.length; i++) {\n var entity = entities[i];\n\n if (!entity.visible || (!force && base.entities[entity.id])) continue;\n\n // Merging data into the base graph\n base.entities[entity.id] = entity;\n this._updateCalculated(undefined, entity, base.parentWays, base.parentRels);\n\n // Restore provisionally-deleted nodes that are discovered to have an extant parent\n if (entity.type === 'way') {\n for (j = 0; j < entity.nodes.length; j++) {\n id = entity.nodes[j];\n for (k = 1; k < stack.length; k++) {\n var ents = stack[k].entities;\n if (ents.hasOwnProperty(id) && ents[id] === undefined) {\n delete ents[id];\n }\n }\n }\n }\n }\n\n for (i = 0; i < stack.length; i++) {\n stack[i]._updateRebased();\n }\n },\n\n\n _updateRebased: function() {\n var base = this.base();\n\n Object.keys(this._parentWays).forEach(function(child) {\n if (base.parentWays[child]) {\n base.parentWays[child].forEach(function(id) {\n if (!this.entities.hasOwnProperty(id)) {\n this._parentWays[child].add(id);\n }\n }, this);\n }\n }, this);\n\n Object.keys(this._parentRels).forEach(function(child) {\n if (base.parentRels[child]) {\n base.parentRels[child].forEach(function(id) {\n if (!this.entities.hasOwnProperty(id)) {\n this._parentRels[child].add(id);\n }\n }, this);\n }\n }, this);\n\n this.transients = {};\n\n // this._childNodes is not updated, under the assumption that\n // ways are always downloaded with their child nodes.\n },\n\n\n // Updates calculated properties (parentWays, parentRels) for the specified change\n _updateCalculated: function(oldentity, entity, parentWays, parentRels) {\n parentWays = parentWays || this._parentWays;\n parentRels = parentRels || this._parentRels;\n\n var type = entity && entity.type || oldentity && oldentity.type;\n var removed, added, i;\n\n if (type === 'way') { // Update parentWays\n if (oldentity && entity) {\n removed = utilArrayDifference(oldentity.nodes, entity.nodes);\n added = utilArrayDifference(entity.nodes, oldentity.nodes);\n } else if (oldentity) {\n removed = oldentity.nodes;\n added = [];\n } else if (entity) {\n removed = [];\n added = entity.nodes;\n }\n for (i = 0; i < removed.length; i++) {\n // make a copy of prototype property, store as own property, and update..\n parentWays[removed[i]] = new Set(parentWays[removed[i]]);\n parentWays[removed[i]].delete(oldentity.id);\n }\n for (i = 0; i < added.length; i++) {\n // make a copy of prototype property, store as own property, and update..\n parentWays[added[i]] = new Set(parentWays[added[i]]);\n parentWays[added[i]].add(entity.id);\n }\n\n } else if (type === 'relation') { // Update parentRels\n\n // diff only on the IDs since the same entity can be a member multiple times with different roles\n var oldentityMemberIDs = oldentity ? oldentity.members.map(function(m) { return m.id; }) : [];\n var entityMemberIDs = entity ? entity.members.map(function(m) { return m.id; }) : [];\n\n if (oldentity && entity) {\n removed = utilArrayDifference(oldentityMemberIDs, entityMemberIDs);\n added = utilArrayDifference(entityMemberIDs, oldentityMemberIDs);\n } else if (oldentity) {\n removed = oldentityMemberIDs;\n added = [];\n } else if (entity) {\n removed = [];\n added = entityMemberIDs;\n }\n for (i = 0; i < removed.length; i++) {\n // make a copy of prototype property, store as own property, and update..\n parentRels[removed[i]] = new Set(parentRels[removed[i]]);\n parentRels[removed[i]].delete(oldentity.id);\n }\n for (i = 0; i < added.length; i++) {\n // make a copy of prototype property, store as own property, and update..\n parentRels[added[i]] = new Set(parentRels[added[i]]);\n parentRels[added[i]].add(entity.id);\n }\n }\n },\n\n\n replace: function(entity) {\n if (this.entities[entity.id] === entity) return this;\n\n return this.update(function() {\n this._updateCalculated(this.entities[entity.id], entity);\n this.entities[entity.id] = entity;\n });\n },\n\n\n remove: function(entity) {\n return this.update(function() {\n this._updateCalculated(entity, undefined);\n this.entities[entity.id] = undefined;\n });\n },\n\n\n revert: function(id) {\n var baseEntity = this.base().entities[id];\n var headEntity = this.entities[id];\n if (headEntity === baseEntity) return this;\n\n return this.update(function() {\n this._updateCalculated(headEntity, baseEntity);\n delete this.entities[id];\n });\n },\n\n\n update: function() {\n var graph = this.frozen ? coreGraph(this, true) : this;\n for (var i = 0; i < arguments.length; i++) {\n arguments[i].call(graph, graph);\n }\n\n if (this.frozen) graph.frozen = true;\n\n return graph;\n },\n\n\n // Obliterates any existing entities\n load: function(entities) {\n var base = this.base();\n this.entities = Object.create(base.entities);\n\n for (var i in entities) {\n this.entities[i] = entities[i];\n this._updateCalculated(base.entities[i], this.entities[i]);\n }\n\n return this;\n }\n};\n", "import { actionDeleteRelation } from '../actions/delete_relation';\nimport { actionReverse } from '../actions/reverse';\nimport { actionSplit } from '../actions/split';\nimport { coreGraph } from '../core/graph';\nimport { geoAngle, geoSphericalDistance } from '../geo';\nimport { osmEntity } from './entity';\nimport { utilArrayDifference, utilArrayUniq } from '../util';\n\n\nexport function osmTurn(turn) {\n if (!(this instanceof osmTurn)) {\n return new osmTurn(turn);\n }\n Object.assign(this, turn);\n}\n\n\nexport function osmIntersection(graph, startVertexId, maxDistance) {\n maxDistance = maxDistance || 30; // in meters\n var vgraph = coreGraph(); // virtual graph\n var i, j, k;\n\n\n function memberOfRestriction(entity) {\n return graph.parentRelations(entity)\n .some(function(r) { return r.isRestriction(); });\n }\n\n function isRoad(way) {\n if (way.isArea() || way.isDegenerate()) return false;\n var roads = {\n 'motorway': true,\n 'motorway_link': true,\n 'trunk': true,\n 'trunk_link': true,\n 'primary': true,\n 'primary_link': true,\n 'secondary': true,\n 'secondary_link': true,\n 'tertiary': true,\n 'tertiary_link': true,\n 'residential': true,\n 'unclassified': true,\n 'living_street': true,\n 'service': true,\n 'busway': true,\n 'road': true,\n 'track': true\n };\n return roads[way.tags.highway];\n }\n\n\n var startNode = graph.entity(startVertexId);\n var checkVertices = [startNode];\n var checkWays;\n var vertices = [];\n var vertexIds = [];\n var vertex;\n var ways = [];\n var way;\n var node;\n var parent;\n\n // `actions` will store whatever actions must be performed to satisfy\n // preconditions for adding a turn restriction to this intersection.\n // - Remove any existing degenerate turn restrictions (missing from/to, etc)\n // - Reverse oneways so that they are drawn in the forward direction\n // - Split ways on key vertices\n var actions = [];\n\n\n // STEP 1: walk the graph outwards from starting vertex to search\n // for more key vertices and ways to include in the intersection..\n\n while (checkVertices.length) {\n vertex = checkVertices.pop();\n\n // check this vertex for parent ways that are roads\n checkWays = graph.parentWays(vertex);\n var hasWays = false;\n for (i = 0; i < checkWays.length; i++) {\n way = checkWays[i];\n if (!isRoad(way) && !memberOfRestriction(way)) continue;\n\n ways.push(way); // it's a road, or it's already in a turn restriction\n hasWays = true;\n\n // check the way's children for more key vertices\n const nodes = utilArrayUniq(graph.childNodes(way));\n for (j = 0; j < nodes.length; j++) {\n node = nodes[j];\n if (node === vertex) continue; // same thing\n if (vertices.indexOf(node) !== -1) continue; // seen it already\n if (geoSphericalDistance(node.loc, startNode.loc) > maxDistance) continue; // too far from start\n\n // a key vertex will have parents that are also roads\n var hasParents = false;\n const parents = graph.parentWays(node);\n for (k = 0; k < parents.length; k++) {\n parent = parents[k];\n if (parent === way) continue; // same thing\n if (ways.indexOf(parent) !== -1) continue; // seen it already\n if (!isRoad(parent)) continue; // not a road\n hasParents = true;\n break;\n }\n\n if (hasParents) {\n checkVertices.push(node);\n }\n }\n }\n\n if (hasWays) {\n vertices.push(vertex);\n }\n }\n\n vertices = utilArrayUniq(vertices);\n ways = utilArrayUniq(ways);\n\n\n // STEP 2: Build a virtual graph containing only the entities in the intersection..\n // Everything done after this step should act on the virtual graph\n // Any actions that must be performed later to the main graph go in `actions` array\n ways.forEach(function(way) {\n graph.childNodes(way).forEach(function(node) {\n vgraph = vgraph.replace(node);\n });\n\n vgraph = vgraph.replace(way);\n\n graph.parentRelations(way).forEach(function(relation) {\n if (relation.isRestriction()) {\n if (relation.isValidRestriction(graph)) {\n vgraph = vgraph.replace(relation);\n } else if (relation.isComplete(graph)) {\n actions.push(actionDeleteRelation(relation.id));\n }\n }\n });\n });\n\n\n // STEP 3: Force all oneways to be drawn in the forward direction\n ways.forEach(function(w) {\n var way = vgraph.entity(w.id);\n if (way.tags.oneway === '-1') {\n var action = actionReverse(way.id, { reverseOneway: true });\n actions.push(action);\n vgraph = action(vgraph);\n }\n });\n\n\n // STEP 4: Split ways on key vertices\n var origCount = osmEntity.id.next.way;\n vertices.forEach(function(v) {\n // This is an odd way to do it, but we need to find all the ways that\n // will be split here, then split them one at a time to ensure that these\n // actions can be replayed on the main graph exactly in the same order.\n // (It is unintuitive, but the order of ways returned from graph.parentWays()\n // is arbitrary, depending on how the main graph and vgraph were built)\n var splitAll = actionSplit([v.id]).keepHistoryOn('first');\n if (!splitAll.disabled(vgraph)) {\n splitAll.ways(vgraph).forEach(function(way) {\n var splitOne = actionSplit([v.id]).limitWays([way.id]).keepHistoryOn('first');\n actions.push(splitOne);\n vgraph = splitOne(vgraph);\n });\n }\n });\n\n // In here is where we should also split the intersection at nearby junction.\n // for https://github.com/mapbox/iD-internal/issues/31\n // nearbyVertices.forEach(function(v) {\n // });\n\n // Reasons why we reset the way id count here:\n // 1. Continuity with way ids created by the splits so that we can replay\n // these actions later if the user decides to create a turn restriction\n // 2. Avoids churning way ids just by hovering over a vertex\n // and displaying the turn restriction editor\n osmEntity.id.next.way = origCount;\n\n\n // STEP 5: Update arrays to point to vgraph entities\n vertexIds = vertices.map(function(v) { return v.id; });\n vertices = [];\n ways = [];\n\n vertexIds.forEach(function(id) {\n const vertex = vgraph.entity(id);\n const parents = vgraph.parentWays(vertex);\n vertices.push(vertex);\n ways = ways.concat(parents);\n });\n\n vertices = utilArrayUniq(vertices);\n ways = utilArrayUniq(ways);\n\n vertexIds = vertices.map(function(v) { return v.id; });\n const wayIds = ways.map(function(w) { return w.id; });\n\n\n // STEP 6: Update the ways with some metadata that will be useful for\n // walking the intersection graph later and rendering turn arrows.\n\n function withMetadata(way, vertexIds) {\n // bidirectional ways are two-way from an intersection's perspective\n var __oneWay = way.isOneWay() && !way.isBiDirectional();\n\n // which affixes are key vertices?\n var __first = (vertexIds.indexOf(way.first()) !== -1);\n var __last = (vertexIds.indexOf(way.last()) !== -1);\n\n // what roles is this way eligible for?\n var __via = (__first && __last);\n var __from = ((__first && !__oneWay) || __last);\n var __to = (__first || (__last && !__oneWay));\n\n return way.update({\n __first: __first,\n __last: __last,\n __from: __from,\n __via: __via,\n __to: __to,\n __oneWay: __oneWay\n });\n }\n\n ways = [];\n wayIds.forEach(function(id) {\n var way = withMetadata(vgraph.entity(id), vertexIds);\n vgraph = vgraph.replace(way);\n ways.push(way);\n });\n\n\n // STEP 7: Simplify - This is an iterative process where we:\n // 1. Find trivial vertices with only 2 parents\n // 2. trim off the leaf way from those vertices and remove from vgraph\n\n var keepGoing;\n var removeWayIds = [];\n var removeVertexIds = [];\n\n do {\n keepGoing = false;\n checkVertices = vertexIds.slice();\n\n for (i = 0; i < checkVertices.length; i++) {\n var vertexId = checkVertices[i];\n vertex = vgraph.hasEntity(vertexId);\n\n if (!vertex) {\n if (vertexIds.indexOf(vertexId) !== -1) {\n vertexIds.splice(vertexIds.indexOf(vertexId), 1); // stop checking this one\n }\n removeVertexIds.push(vertexId);\n continue;\n }\n\n let parents = vgraph.parentWays(vertex);\n if (parents.length < 3) {\n if (vertexIds.indexOf(vertexId) !== -1) {\n vertexIds.splice(vertexIds.indexOf(vertexId), 1); // stop checking this one\n }\n }\n\n if (parents.length === 2) { // vertex with 2 parents is trivial\n var a = parents[0];\n var b = parents[1];\n var aIsLeaf = a && !a.__via;\n var bIsLeaf = b && !b.__via;\n var leaf, survivor;\n\n if (aIsLeaf && !bIsLeaf) {\n leaf = a;\n survivor = b;\n } else if (!aIsLeaf && bIsLeaf) {\n leaf = b;\n survivor = a;\n }\n\n if (leaf && survivor) {\n survivor = withMetadata(survivor, vertexIds); // update survivor way\n vgraph = vgraph.replace(survivor).remove(leaf); // update graph\n removeWayIds.push(leaf.id);\n keepGoing = true;\n }\n }\n\n parents = vgraph.parentWays(vertex);\n\n if (parents.length < 2) { // vertex is no longer a key vertex\n if (vertexIds.indexOf(vertexId) !== -1) {\n vertexIds.splice(vertexIds.indexOf(vertexId), 1); // stop checking this one\n }\n removeVertexIds.push(vertexId);\n keepGoing = true;\n }\n\n if (parents.length < 1) { // vertex is no longer attached to anything\n vgraph = vgraph.remove(vertex);\n }\n\n }\n } while (keepGoing);\n\n\n vertices = vertices\n .filter(function(vertex) { return removeVertexIds.indexOf(vertex.id) === -1; })\n .map(function(vertex) { return vgraph.entity(vertex.id); });\n ways = ways\n .filter(function(way) { return removeWayIds.indexOf(way.id) === -1; })\n .map(function(way) { return vgraph.entity(way.id); });\n\n\n // OK! Here is our intersection..\n var intersection = {\n graph: vgraph,\n actions: actions,\n vertices: vertices,\n ways: ways,\n };\n\n\n\n // Get all the valid turns through this intersection given a starting way id.\n // This operates on the virtual graph for everything.\n //\n // Basically, walk through all possible paths from starting way,\n // honoring the existing turn restrictions as we go (watch out for loops!)\n //\n // For each path found, generate and return a `osmTurn` datastructure.\n //\n intersection.turns = function(fromWayId, maxViaWay) {\n if (!fromWayId) return [];\n if (!maxViaWay) maxViaWay = 0;\n\n var vgraph = intersection.graph;\n var keyVertexIds = intersection.vertices.map(function(v) { return v.id; });\n\n var start = vgraph.entity(fromWayId);\n if (!start || !(start.__from || start.__via)) return [];\n\n // maxViaWay=0 from-*-to (0 vias)\n // maxViaWay=1 from-*-via-*-to (1 via max)\n // maxViaWay=2 from-*-via-*-via-*-to (2 vias max)\n var maxPathLength = (maxViaWay * 2) + 3;\n var turns = [];\n\n step(start);\n return turns;\n\n\n // traverse the intersection graph and find all the valid paths\n function step(entity, currPath, currRestrictions, matchedRestriction) {\n currPath = (currPath || []).slice(); // shallow copy\n if (currPath.length >= maxPathLength) return;\n currPath.push(entity.id);\n currRestrictions = (currRestrictions || []).slice(); // shallow copy\n\n if (entity.type === 'node') {\n stepNode(entity, currPath, currRestrictions);\n } else { // entity.type === 'way'\n stepWay(entity, currPath, currRestrictions, matchedRestriction);\n }\n }\n\n function stepNode(entity, currPath, currRestrictions) {\n var i, j;\n var parents = vgraph.parentWays(entity);\n var nextWays = [];\n\n // which ways can we step into?\n for (i = 0; i < parents.length; i++) {\n var way = parents[i];\n\n // if next way is a oneway incoming to this vertex, skip\n if (way.__oneWay && way.nodes[0] !== entity.id) continue;\n\n // if we have seen it before (allowing for an initial u-turn), skip\n if (currPath.indexOf(way.id) !== -1 && currPath.length >= 3) continue;\n\n // Check all \"current\" restrictions (where we've already walked the `FROM`)\n var restrict = null;\n for (j = 0; j < currRestrictions.length; j++) {\n var restriction = currRestrictions[j];\n var f = restriction.memberByRole('from');\n var v = restriction.membersByRole('via');\n var t = restriction.memberByRole('to');\n var isNo = /^no_/.test(restriction.tags.restriction);\n var isOnly = /^only_/.test(restriction.tags.restriction);\n\n if (!(isNo || isOnly)) {\n continue; // skip unsupported restriction values\n }\n\n // Does the current path match this turn restriction?\n var matchesFrom = (f.id === fromWayId);\n var matchesViaTo = false;\n var isAlongOnlyPath = false;\n\n if (t.id === way.id) { // match TO\n\n if (v.length === 1 && v[0].type === 'node') { // match VIA node\n matchesViaTo = (v[0].id === entity.id && (\n (matchesFrom && currPath.length === 2) ||\n (!matchesFrom && currPath.length > 2)\n ));\n\n } else { // match all VIA ways\n var pathVias = [];\n for (k = 2; k < currPath.length; k +=2 ) { // k = 2 skips FROM\n pathVias.push(currPath[k]); // (path goes way-node-way...)\n }\n var restrictionVias = [];\n for (k = 0; k < v.length; k++) {\n if (v[k].type === 'way') {\n restrictionVias.push(v[k].id);\n }\n }\n var diff = utilArrayDifference(pathVias, restrictionVias);\n matchesViaTo = !diff.length;\n }\n\n } else if (isOnly) {\n for (k = 0; k < v.length; k++) {\n // way doesn't match TO, but is one of the via ways along the path of an \"only\"\n if (v[k].type === 'way' && v[k].id === way.id) {\n isAlongOnlyPath = true;\n break;\n }\n }\n }\n\n if (matchesViaTo) {\n if (isOnly) {\n restrict = { id: restriction.id, direct: matchesFrom, from: f.id, only: true, end: true };\n } else {\n restrict = { id: restriction.id, direct: matchesFrom, from: f.id, no: true, end: true };\n }\n } else { // indirect - caused by a different nearby restriction\n if (isAlongOnlyPath) {\n restrict = { id: restriction.id, direct: false, from: f.id, only: true, end: false };\n } else if (isOnly) {\n restrict = { id: restriction.id, direct: false, from: f.id, no: true, end: true };\n }\n }\n\n // stop looking if we find a \"direct\" restriction (matching FROM, VIA, TO)\n if (restrict && restrict.direct) break;\n }\n\n nextWays.push({ way: way, restrict: restrict });\n }\n\n nextWays.forEach(function(nextWay) {\n step(nextWay.way, currPath, currRestrictions, nextWay.restrict);\n });\n }\n\n function stepWay(entity, currPath, currRestrictions, matchedRestriction) {\n var i;\n if (currPath.length >= 3) { // this is a \"complete\" path..\n var turnPath = currPath.slice(); // shallow copy\n\n // an indirect restriction - only include the partial path (starting at FROM)\n if (matchedRestriction && matchedRestriction.direct === false) {\n for (i = 0; i < turnPath.length; i++) {\n if (turnPath[i] === matchedRestriction.from) {\n turnPath = turnPath.slice(i);\n break;\n }\n }\n }\n\n var turn = pathToTurn(turnPath);\n if (turn) {\n if (matchedRestriction) {\n turn.restrictionID = matchedRestriction.id;\n turn.no = matchedRestriction.no;\n turn.only = matchedRestriction.only;\n turn.direct = matchedRestriction.direct;\n }\n turns.push(osmTurn(turn));\n }\n\n if (currPath[0] === currPath[2]) return; // if we made a u-turn - stop here\n }\n\n if (matchedRestriction && matchedRestriction.end) return; // don't advance any further\n\n // which nodes can we step into?\n var n1 = vgraph.entity(entity.first());\n var n2 = vgraph.entity(entity.last());\n var dist = geoSphericalDistance(n1.loc, n2.loc);\n var nextNodes = [];\n\n if (currPath.length > 1) {\n if (dist > maxDistance) return; // the next node is too far\n if (!entity.__via) return; // this way is a leaf / can't be a via\n }\n\n if (!entity.__oneWay && // bidirectional..\n keyVertexIds.indexOf(n1.id) !== -1 && // key vertex..\n currPath.indexOf(n1.id) === -1) { // haven't seen it yet..\n nextNodes.push(n1); // can advance to first node\n }\n if (keyVertexIds.indexOf(n2.id) !== -1 && // key vertex..\n currPath.indexOf(n2.id) === -1) { // haven't seen it yet..\n nextNodes.push(n2); // can advance to last node\n }\n\n nextNodes.forEach(function(nextNode) {\n // gather restrictions FROM this way\n var fromRestrictions = vgraph.parentRelations(entity).filter(function(r) {\n if (!r.isRestriction()) return false;\n\n var f = r.memberByRole('from');\n if (!f || f.id !== entity.id) return false;\n\n var isOnly = /^only_/.test(r.tags.restriction);\n if (!isOnly) return true;\n\n // `only_` restrictions only matter along the direction of the VIA - #4849\n var isOnlyVia = false;\n var v = r.membersByRole('via');\n if (v.length === 1 && v[0].type === 'node') { // via node\n isOnlyVia = (v[0].id === nextNode.id);\n } else { // via way(s)\n for (var i = 0; i < v.length; i++) {\n if (v[i].type !== 'way') continue;\n var viaWay = vgraph.entity(v[i].id);\n if (viaWay.first() === nextNode.id || viaWay.last() === nextNode.id) {\n isOnlyVia = true;\n break;\n }\n }\n }\n return isOnlyVia;\n });\n\n step(nextNode, currPath, currRestrictions.concat(fromRestrictions), false);\n });\n }\n\n\n // assumes path is alternating way-node-way of odd length\n function pathToTurn(path) {\n if (path.length < 3) return;\n var fromWayId, fromNodeId, fromVertexId;\n var toWayId, toNodeId, toVertexId;\n var viaWayIds, viaNodeId, isUturn;\n\n fromWayId = path[0];\n toWayId = path[path.length - 1];\n\n if (path.length === 3 && fromWayId === toWayId) { // u turn\n var way = vgraph.entity(fromWayId);\n if (way.__oneWay) return null;\n\n isUturn = true;\n viaNodeId = path[1];\n fromVertexId = path[1];\n toVertexId = path[1];\n fromNodeId = adjacentNode(fromWayId, viaNodeId);\n toNodeId = fromNodeId;\n\n } else {\n isUturn = false;\n fromVertexId = path[1];\n fromNodeId = adjacentNode(fromWayId, fromVertexId);\n toVertexId = path[path.length - 2];\n toNodeId = adjacentNode(toWayId, toVertexId);\n\n if (path.length === 3) {\n viaNodeId = path[1];\n } else {\n viaWayIds = path.filter(function(entityId) { return entityId[0] === 'w'; });\n viaWayIds = viaWayIds.slice(1, viaWayIds.length - 1); // remove first, last\n }\n }\n\n return {\n key: path.join('_'),\n path: path,\n from: { node: fromNodeId, way: fromWayId, vertex: fromVertexId },\n via: { node: viaNodeId, ways: viaWayIds },\n to: { node: toNodeId, way: toWayId, vertex: toVertexId },\n u: isUturn\n };\n\n\n function adjacentNode(wayId, affixId) {\n var nodes = vgraph.entity(wayId).nodes;\n return affixId === nodes[0] ? nodes[1] : nodes[nodes.length - 2];\n }\n }\n\n };\n\n return intersection;\n}\n\n\nexport function osmInferRestriction(graph, turn, projection) {\n var fromWay = graph.entity(turn.from.way);\n var fromNode = graph.entity(turn.from.node);\n var fromVertex = graph.entity(turn.from.vertex);\n var toWay = graph.entity(turn.to.way);\n var toNode = graph.entity(turn.to.node);\n var toVertex = graph.entity(turn.to.vertex);\n\n var fromOneWay = (fromWay.tags.oneway === 'yes');\n var toOneWay = (toWay.tags.oneway === 'yes');\n var angle = (geoAngle(fromVertex, fromNode, projection) -\n geoAngle(toVertex, toNode, projection)) * 180 / Math.PI;\n\n while (angle < 0) {\n angle += 360;\n }\n\n if (fromNode === toNode) {\n return 'no_u_turn';\n }\n if ((angle < 23 || angle > 336) && fromOneWay && toOneWay) {\n return 'no_u_turn'; // wider tolerance for u-turn if both ways are oneway\n }\n if ((angle < 40 || angle > 319) && fromOneWay && toOneWay && turn.from.vertex !== turn.to.vertex) {\n return 'no_u_turn'; // even wider tolerance for u-turn if there is a via way (from !== to)\n }\n if (angle < 158) {\n return 'no_right_turn';\n }\n if (angle > 202) {\n return 'no_left_turn';\n }\n\n return 'no_straight_on';\n}\n", "export { osmChangeset } from './changeset';\nexport { osmEntity } from './entity';\nexport { osmNode } from './node';\nexport { osmNote } from './note';\nexport { osmRelation } from './relation';\nexport { osmWay } from './way';\nexport { QAItem } from './qa_item';\n\nexport {\n osmIntersection,\n osmTurn,\n osmInferRestriction\n} from './intersection';\n\nexport {\n osmLanes\n} from './lanes';\n\nexport {\n osmJoinWays\n} from './multipolygon';\n\nexport {\n osmAreaKeys,\n osmSetAreaKeys,\n osmTagSuggestingArea,\n osmPointTags,\n osmSetPointTags,\n osmVertexTags,\n osmSetVertexTags,\n osmNodeGeometriesForTags,\n osmPavedTags,\n osmIsInterestingTag,\n osmLifecyclePrefixes,\n osmRemoveLifecyclePrefix,\n osmRoutableHighwayTagValues,\n osmFlowingWaterwayTagValues,\n osmRailwayTrackTagValues,\n osmTimelessFeatureTagValues\n} from './tags';\n", "import { geoPolygonContainsPolygon } from '../geo';\nimport { osmJoinWays, osmRelation } from '../osm';\nimport { utilArrayGroupBy, utilArrayIntersection, utilObjectOmit, utilOldestID } from '../util';\n\n\nexport function actionMergePolygon(ids, newRelationId) {\n\n function groupEntities(graph) {\n var entities = ids.map(function (id) { return graph.entity(id); });\n var geometryGroups = utilArrayGroupBy(entities, function(entity) {\n if (entity.type === 'way' && entity.isClosed()) {\n return 'closedWay';\n } else if (entity.type === 'relation' && entity.isMultipolygon()) {\n return 'multipolygon';\n } else {\n return 'other';\n }\n });\n\n return Object.assign(\n { closedWay: [], multipolygon: [], other: [] },\n geometryGroups\n );\n }\n\n\n var action = function(graph) {\n var entities = groupEntities(graph);\n\n // An array representing all the polygons that are part of the multipolygon.\n //\n // Each element is itself an array of objects with an id property, and has a\n // locs property which is an array of the locations forming the polygon.\n var polygons = entities.multipolygon.reduce(function(polygons, m) {\n return polygons.concat(osmJoinWays(m.members, graph));\n }, []).concat(entities.closedWay.map(function(d) {\n var member = [{id: d.id}];\n member.nodes = graph.childNodes(d);\n return member;\n }));\n\n // contained is an array of arrays of boolean values,\n // where contained[j][k] is true iff the jth way is\n // contained by the kth way.\n var contained = polygons.map(function(w, i) {\n return polygons.map(function(d, n) {\n if (i === n) return null;\n return geoPolygonContainsPolygon(\n d.nodes.map(function(n) { return n.loc; }),\n w.nodes.map(function(n) { return n.loc; })\n );\n });\n });\n\n // Sort all polygons as either outer or inner ways\n var members = [];\n var outer = true;\n\n while (polygons.length) {\n extractUncontained(polygons);\n polygons = polygons.filter(isContained);\n contained = contained.filter(isContained).map(filterContained);\n }\n\n function isContained(d, i) {\n return contained[i].some(function(val) { return val; });\n }\n\n function filterContained(d) {\n return d.filter(isContained);\n }\n\n function extractUncontained(polygons) {\n polygons.forEach(function(d, i) {\n if (!isContained(d, i)) {\n d.forEach(function(member) {\n members.push({\n type: 'way',\n id: member.id,\n role: outer ? 'outer' : 'inner'\n });\n });\n }\n });\n outer = !outer;\n }\n\n // Move all tags to one relation.\n // Keep the oldest multipolygon alive if it exists.\n var relation;\n if (entities.multipolygon.length > 0) {\n var oldestID = utilOldestID(entities.multipolygon.map((entity) => entity.id));\n relation = entities.multipolygon.find((entity) => entity.id === oldestID);\n } else {\n relation = osmRelation({ id: newRelationId, tags: { type: 'multipolygon' }});\n }\n\n entities.multipolygon.forEach(function(m) {\n if (m.id !== relation.id) {\n relation = relation.mergeTags(m.tags);\n graph = graph.remove(m);\n }\n });\n\n entities.closedWay.forEach(function(way) {\n function isThisOuter(m) {\n return m.id === way.id && m.role !== 'inner';\n }\n if (members.some(isThisOuter)) {\n relation = relation.mergeTags(way.tags);\n graph = graph.replace(way.update({ tags: {} }));\n }\n });\n\n return graph.replace(relation.update({\n members: members,\n tags: utilObjectOmit(relation.tags, ['area'])\n }));\n };\n\n\n action.disabled = function(graph) {\n var entities = groupEntities(graph);\n if (entities.other.length > 0 ||\n entities.closedWay.length + entities.multipolygon.length < 2) {\n return 'not_eligible';\n }\n if (!entities.multipolygon.every(function(r) { return r.isComplete(graph); })) {\n return 'incomplete_relation';\n }\n\n if (!entities.multipolygon.length) {\n var sharedMultipolygons = [];\n entities.closedWay.forEach(function(way, i) {\n if (i === 0) {\n sharedMultipolygons = graph.parentMultipolygons(way);\n } else {\n sharedMultipolygons = utilArrayIntersection(sharedMultipolygons, graph.parentMultipolygons(way));\n }\n });\n sharedMultipolygons = sharedMultipolygons.filter(function(relation) {\n return relation.members.length === entities.closedWay.length;\n });\n if (sharedMultipolygons.length) {\n // don't create a new multipolygon if it'd be redundant\n return 'not_eligible';\n }\n } else if (entities.closedWay.some(function(way) {\n return utilArrayIntersection(graph.parentMultipolygons(way), entities.multipolygon).length;\n })) {\n // don't add a way to a multipolygon again if it's already a member\n return 'not_eligible';\n }\n };\n\n\n return action;\n}\n", "const { getOwnPropertyNames, getOwnPropertySymbols } = Object;\n// eslint-disable-next-line @typescript-eslint/unbound-method\nconst { hasOwnProperty } = Object.prototype;\n/**\n * Combine two comparators into a single comparators.\n */\nfunction combineComparators(comparatorA, comparatorB) {\n return function isEqual(a, b, state) {\n return comparatorA(a, b, state) && comparatorB(a, b, state);\n };\n}\n/**\n * Wrap the provided `areItemsEqual` method to manage the circular state, allowing\n * for circular references to be safely included in the comparison without creating\n * stack overflows.\n */\nfunction createIsCircular(areItemsEqual) {\n return function isCircular(a, b, state) {\n if (!a || !b || typeof a !== 'object' || typeof b !== 'object') {\n return areItemsEqual(a, b, state);\n }\n const { cache } = state;\n const cachedA = cache.get(a);\n const cachedB = cache.get(b);\n if (cachedA && cachedB) {\n return cachedA === b && cachedB === a;\n }\n cache.set(a, b);\n cache.set(b, a);\n const result = areItemsEqual(a, b, state);\n cache.delete(a);\n cache.delete(b);\n return result;\n };\n}\n/**\n * Get the properties to strictly examine, which include both own properties that are\n * not enumerable and symbol properties.\n */\nfunction getStrictProperties(object) {\n return getOwnPropertyNames(object).concat(getOwnPropertySymbols(object));\n}\n/**\n * Whether the object contains the property passed as an own property.\n */\nconst hasOwn = \n// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\nObject.hasOwn || ((object, property) => hasOwnProperty.call(object, property));\n\nconst PREACT_VNODE = '__v';\nconst PREACT_OWNER = '__o';\nconst REACT_OWNER = '_owner';\nconst { getOwnPropertyDescriptor, keys } = Object;\n/**\n * Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevalue) basis.\n * Simplified, this maps to if the two values are referentially equal to one another (`a === b`) or both are `NaN`.\n *\n * @note\n * When available in the environment, this is just a re-export of the global\n * [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) method.\n */\nconst sameValueEqual = \n// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\nObject.is\n || function sameValueEqual(a, b) {\n return a === b ? a !== 0 || 1 / a === 1 / b : a !== a && b !== b;\n };\n/**\n * Whether the values passed are equal based on a [SameValue](https://262.ecma-international.org/7.0/#sec-samevaluezero) basis.\n * Simplified, this maps to if the two values are referentially equal to one another (`a === b`), both are `NaN`, or both\n * are either positive or negative zero.\n */\nfunction sameValueZeroEqual(a, b) {\n return a === b || (a !== a && b !== b);\n}\n/**\n * Whether the values passed are equal based on a\n * [Strict Equality Comparison](https://262.ecma-international.org/7.0/#sec-strict-equality-comparison) basis.\n * Simplified, this maps to if the two values are referentially equal to one another (`a === b`).\n *\n * @note\n * This is mainly available as a convenience function, such as being a default when a function to determine equality between\n * two objects is used.\n */\nfunction strictEqual(a, b) {\n return a === b;\n}\n/**\n * Whether the array buffers are equal in value.\n */\nfunction areArrayBuffersEqual(a, b) {\n return a.byteLength === b.byteLength && areTypedArraysEqual(new Uint8Array(a), new Uint8Array(b));\n}\n/**\n * Whether the arrays are equal in value.\n */\nfunction areArraysEqual(a, b, state) {\n let index = a.length;\n if (b.length !== index) {\n return false;\n }\n while (index-- > 0) {\n if (!state.equals(a[index], b[index], index, index, a, b, state)) {\n return false;\n }\n }\n return true;\n}\n/**\n * Whether the dataviews are equal in value.\n */\nfunction areDataViewsEqual(a, b) {\n return (a.byteLength === b.byteLength\n && areTypedArraysEqual(new Uint8Array(a.buffer, a.byteOffset, a.byteLength), new Uint8Array(b.buffer, b.byteOffset, b.byteLength)));\n}\n/**\n * Whether the dates passed are equal in value.\n */\nfunction areDatesEqual(a, b) {\n return sameValueEqual(a.getTime(), b.getTime());\n}\n/**\n * Whether the errors passed are equal in value.\n */\nfunction areErrorsEqual(a, b) {\n return a.name === b.name && a.message === b.message && a.cause === b.cause && a.stack === b.stack;\n}\n/**\n * Whether the `Map`s are equal in value.\n */\nfunction areMapsEqual(a, b, state) {\n const size = a.size;\n if (size !== b.size) {\n return false;\n }\n if (!size) {\n return true;\n }\n const matchedIndices = new Array(size);\n const aIterable = a.entries();\n let aResult;\n let bResult;\n let index = 0;\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while ((aResult = aIterable.next())) {\n if (aResult.done) {\n break;\n }\n const bIterable = b.entries();\n let hasMatch = false;\n let matchIndex = 0;\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while ((bResult = bIterable.next())) {\n if (bResult.done) {\n break;\n }\n if (matchedIndices[matchIndex]) {\n matchIndex++;\n continue;\n }\n const aEntry = aResult.value;\n const bEntry = bResult.value;\n if (state.equals(aEntry[0], bEntry[0], index, matchIndex, a, b, state)\n && state.equals(aEntry[1], bEntry[1], aEntry[0], bEntry[0], a, b, state)) {\n hasMatch = matchedIndices[matchIndex] = true;\n break;\n }\n matchIndex++;\n }\n if (!hasMatch) {\n return false;\n }\n index++;\n }\n return true;\n}\n/**\n * Whether the objects are equal in value.\n */\nfunction areObjectsEqual(a, b, state) {\n const properties = keys(a);\n let index = properties.length;\n if (keys(b).length !== index) {\n return false;\n }\n // Decrementing `while` showed faster results than either incrementing or\n // decrementing `for` loop and than an incrementing `while` loop. Declarative\n // methods like `some` / `every` were not used to avoid incurring the garbage\n // cost of anonymous callbacks.\n while (index-- > 0) {\n if (!isPropertyEqual(a, b, state, properties[index])) {\n return false;\n }\n }\n return true;\n}\n/**\n * Whether the objects are equal in value with strict property checking.\n */\nfunction areObjectsEqualStrict(a, b, state) {\n const properties = getStrictProperties(a);\n let index = properties.length;\n if (getStrictProperties(b).length !== index) {\n return false;\n }\n let property;\n let descriptorA;\n let descriptorB;\n // Decrementing `while` showed faster results than either incrementing or\n // decrementing `for` loop and than an incrementing `while` loop. Declarative\n // methods like `some` / `every` were not used to avoid incurring the garbage\n // cost of anonymous callbacks.\n while (index-- > 0) {\n property = properties[index];\n if (!isPropertyEqual(a, b, state, property)) {\n return false;\n }\n descriptorA = getOwnPropertyDescriptor(a, property);\n descriptorB = getOwnPropertyDescriptor(b, property);\n if ((descriptorA || descriptorB)\n && (!descriptorA\n || !descriptorB\n || descriptorA.configurable !== descriptorB.configurable\n || descriptorA.enumerable !== descriptorB.enumerable\n || descriptorA.writable !== descriptorB.writable)) {\n return false;\n }\n }\n return true;\n}\n/**\n * Whether the primitive wrappers passed are equal in value.\n */\nfunction arePrimitiveWrappersEqual(a, b) {\n return sameValueEqual(a.valueOf(), b.valueOf());\n}\n/**\n * Whether the regexps passed are equal in value.\n */\nfunction areRegExpsEqual(a, b) {\n return a.source === b.source && a.flags === b.flags;\n}\n/**\n * Whether the `Set`s are equal in value.\n */\nfunction areSetsEqual(a, b, state) {\n const size = a.size;\n if (size !== b.size) {\n return false;\n }\n if (!size) {\n return true;\n }\n const matchedIndices = new Array(size);\n const aIterable = a.values();\n let aResult;\n let bResult;\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while ((aResult = aIterable.next())) {\n if (aResult.done) {\n break;\n }\n const bIterable = b.values();\n let hasMatch = false;\n let matchIndex = 0;\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while ((bResult = bIterable.next())) {\n if (bResult.done) {\n break;\n }\n if (!matchedIndices[matchIndex]\n && state.equals(aResult.value, bResult.value, aResult.value, bResult.value, a, b, state)) {\n hasMatch = matchedIndices[matchIndex] = true;\n break;\n }\n matchIndex++;\n }\n if (!hasMatch) {\n return false;\n }\n }\n return true;\n}\n/**\n * Whether the TypedArray instances are equal in value.\n */\nfunction areTypedArraysEqual(a, b) {\n let index = a.byteLength;\n if (b.byteLength !== index || a.byteOffset !== b.byteOffset) {\n return false;\n }\n while (index-- > 0) {\n if (a[index] !== b[index]) {\n return false;\n }\n }\n return true;\n}\n/**\n * Whether the URL instances are equal in value.\n */\nfunction areUrlsEqual(a, b) {\n return (a.hostname === b.hostname\n && a.pathname === b.pathname\n && a.protocol === b.protocol\n && a.port === b.port\n && a.hash === b.hash\n && a.username === b.username\n && a.password === b.password);\n}\nfunction isPropertyEqual(a, b, state, property) {\n if ((property === REACT_OWNER || property === PREACT_OWNER || property === PREACT_VNODE)\n && (a.$$typeof || b.$$typeof)) {\n return true;\n }\n return hasOwn(b, property) && state.equals(a[property], b[property], property, property, a, b, state);\n}\n\n// eslint-disable-next-line @typescript-eslint/unbound-method\nconst toString = Object.prototype.toString;\n/**\n * Create a comparator method based on the type-specific equality comparators passed.\n */\nfunction createEqualityComparator(config) {\n const supportedComparatorMap = createSupportedComparatorMap(config);\n const { areArraysEqual, areDatesEqual, areFunctionsEqual, areMapsEqual, areNumbersEqual, areObjectsEqual, areRegExpsEqual, areSetsEqual, getUnsupportedCustomComparator, } = config;\n /**\n * compare the value of the two objects and return true if they are equivalent in values\n */\n return function comparator(a, b, state) {\n // If the items are strictly equal, no need to do a value comparison.\n if (a === b) {\n return true;\n }\n // If either of the items are nullish and fail the strictly equal check\n // above, then they must be unequal.\n if (a == null || b == null) {\n return false;\n }\n const type = typeof a;\n if (type !== typeof b) {\n return false;\n }\n if (type !== 'object') {\n if (type === 'number' || type === 'bigint') {\n return areNumbersEqual(a, b, state);\n }\n if (type === 'function') {\n return areFunctionsEqual(a, b, state);\n }\n // If a primitive value that is not strictly equal, it must be unequal.\n return false;\n }\n const constructor = a.constructor;\n // Checks are listed in order of commonality of use-case:\n // 1. Common complex object types (plain object, array)\n // 2. Common data values (date, regexp)\n // 3. Less-common complex object types (map, set)\n // 4. Less-common data values (promise, primitive wrappers)\n // Inherently this is both subjective and assumptive, however\n // when reviewing comparable libraries in the wild this order\n // appears to be generally consistent.\n // Constructors should match, otherwise there is potential for false positives\n // between class and subclass or custom object and POJO.\n if (constructor !== b.constructor) {\n return false;\n }\n // Try to fast-path equality checks for other complex object types in the\n // same realm to avoid capturing the string tag. Strict equality is used\n // instead of `instanceof` because it is more performant for the common\n // use-case. If someone is creating a subclass from a native class, it will be\n // handled with the string tag comparison.\n if (constructor === Object) {\n return areObjectsEqual(a, b, state);\n }\n if (constructor === Array) {\n return areArraysEqual(a, b, state);\n }\n if (constructor === Date) {\n return areDatesEqual(a, b, state);\n }\n if (constructor === RegExp) {\n return areRegExpsEqual(a, b, state);\n }\n if (constructor === Map) {\n return areMapsEqual(a, b, state);\n }\n if (constructor === Set) {\n return areSetsEqual(a, b, state);\n }\n if (constructor === Promise) {\n // Avoid tag checks for promise values, since we know if they are not referentially equal\n // then they are not equal.\n return false;\n }\n // `isArray()` works on subclasses and is cross-realm, so we can avoid capturing\n // the string tag or doing an `instanceof` in edge cases.\n if (Array.isArray(a)) {\n return areArraysEqual(a, b, state);\n }\n // Since this is a custom object, capture the string tag to determining its type.\n // This is reasonably performant in modern environments like v8 and SpiderMonkey.\n const tag = toString.call(a);\n const supportedComparator = supportedComparatorMap[tag];\n if (supportedComparator) {\n return supportedComparator(a, b, state);\n }\n const unsupportedCustomComparator = getUnsupportedCustomComparator && getUnsupportedCustomComparator(a, b, state, tag);\n if (unsupportedCustomComparator) {\n return unsupportedCustomComparator(a, b, state);\n }\n // If not matching any tags that require a specific type of comparison, then we hard-code false because\n // the only thing remaining is strict equality, which has already been compared. This is for a few reasons:\n // - Certain types that cannot be introspected (e.g., `WeakMap`). For these types, this is the only\n // comparison that can be made.\n // - For types that can be introspected but do not have an objective definition of what\n // equality is (`Error`, etc.), the subjective decision is to be conservative and strictly compare.\n // In all cases, these decisions should be reevaluated based on changes to the language and\n // common development practices.\n return false;\n };\n}\n/**\n * Create the configuration object used for building comparators.\n */\nfunction createEqualityComparatorConfig({ circular, createCustomConfig, strict, }) {\n let config = {\n areArrayBuffersEqual,\n areArraysEqual: strict ? areObjectsEqualStrict : areArraysEqual,\n areDataViewsEqual,\n areDatesEqual: areDatesEqual,\n areErrorsEqual: areErrorsEqual,\n areFunctionsEqual: strictEqual,\n areMapsEqual: strict ? combineComparators(areMapsEqual, areObjectsEqualStrict) : areMapsEqual,\n areNumbersEqual: sameValueEqual,\n areObjectsEqual: strict ? areObjectsEqualStrict : areObjectsEqual,\n arePrimitiveWrappersEqual: arePrimitiveWrappersEqual,\n areRegExpsEqual: areRegExpsEqual,\n areSetsEqual: strict ? combineComparators(areSetsEqual, areObjectsEqualStrict) : areSetsEqual,\n areTypedArraysEqual: strict\n ? combineComparators(areTypedArraysEqual, areObjectsEqualStrict)\n : areTypedArraysEqual,\n areUrlsEqual: areUrlsEqual,\n getUnsupportedCustomComparator: undefined,\n };\n if (createCustomConfig) {\n config = Object.assign({}, config, createCustomConfig(config));\n }\n if (circular) {\n const areArraysEqual = createIsCircular(config.areArraysEqual);\n const areMapsEqual = createIsCircular(config.areMapsEqual);\n const areObjectsEqual = createIsCircular(config.areObjectsEqual);\n const areSetsEqual = createIsCircular(config.areSetsEqual);\n config = Object.assign({}, config, {\n areArraysEqual,\n areMapsEqual,\n areObjectsEqual,\n areSetsEqual,\n });\n }\n return config;\n}\n/**\n * Default equality comparator pass-through, used as the standard `isEqual` creator for\n * use inside the built comparator.\n */\nfunction createInternalEqualityComparator(compare) {\n return function (a, b, _indexOrKeyA, _indexOrKeyB, _parentA, _parentB, state) {\n return compare(a, b, state);\n };\n}\n/**\n * Create the `isEqual` function used by the consuming application.\n */\nfunction createIsEqual({ circular, comparator, createState, equals, strict }) {\n if (createState) {\n return function isEqual(a, b) {\n const { cache = circular ? new WeakMap() : undefined, meta } = createState();\n return comparator(a, b, {\n cache,\n equals,\n meta,\n strict,\n });\n };\n }\n if (circular) {\n return function isEqual(a, b) {\n return comparator(a, b, {\n cache: new WeakMap(),\n equals,\n meta: undefined,\n strict,\n });\n };\n }\n const state = {\n cache: undefined,\n equals,\n meta: undefined,\n strict,\n };\n return function isEqual(a, b) {\n return comparator(a, b, state);\n };\n}\n/**\n * Create a map of `toString()` values to their respective handlers for `tag`-based lookups.\n */\nfunction createSupportedComparatorMap({ areArrayBuffersEqual, areArraysEqual, areDataViewsEqual, areDatesEqual, areErrorsEqual, areFunctionsEqual, areMapsEqual, areNumbersEqual, areObjectsEqual, arePrimitiveWrappersEqual, areRegExpsEqual, areSetsEqual, areTypedArraysEqual, areUrlsEqual, }) {\n return {\n '[object Arguments]': areObjectsEqual,\n '[object Array]': areArraysEqual,\n '[object ArrayBuffer]': areArrayBuffersEqual,\n '[object AsyncGeneratorFunction]': areFunctionsEqual,\n '[object BigInt]': areNumbersEqual,\n '[object BigInt64Array]': areTypedArraysEqual,\n '[object BigUint64Array]': areTypedArraysEqual,\n '[object Boolean]': arePrimitiveWrappersEqual,\n '[object DataView]': areDataViewsEqual,\n '[object Date]': areDatesEqual,\n // If an error tag, it should be tested explicitly. Like RegExp, the properties are not\n // enumerable, and therefore will give false positives if tested like a standard object.\n '[object Error]': areErrorsEqual,\n '[object Float16Array]': areTypedArraysEqual,\n '[object Float32Array]': areTypedArraysEqual,\n '[object Float64Array]': areTypedArraysEqual,\n '[object Function]': areFunctionsEqual,\n '[object GeneratorFunction]': areFunctionsEqual,\n '[object Int8Array]': areTypedArraysEqual,\n '[object Int16Array]': areTypedArraysEqual,\n '[object Int32Array]': areTypedArraysEqual,\n '[object Map]': areMapsEqual,\n '[object Number]': arePrimitiveWrappersEqual,\n '[object Object]': (a, b, state) => \n // The exception for value comparison is custom `Promise`-like class instances. These should\n // be treated the same as standard `Promise` objects, which means strict equality, and if\n // it reaches this point then that strict equality comparison has already failed.\n typeof a.then !== 'function' && typeof b.then !== 'function' && areObjectsEqual(a, b, state),\n // For RegExp, the properties are not enumerable, and therefore will give false positives if\n // tested like a standard object.\n '[object RegExp]': areRegExpsEqual,\n '[object Set]': areSetsEqual,\n '[object String]': arePrimitiveWrappersEqual,\n '[object URL]': areUrlsEqual,\n '[object Uint8Array]': areTypedArraysEqual,\n '[object Uint8ClampedArray]': areTypedArraysEqual,\n '[object Uint16Array]': areTypedArraysEqual,\n '[object Uint32Array]': areTypedArraysEqual,\n };\n}\n\n/**\n * Whether the items passed are deeply-equal in value.\n */\nconst deepEqual = createCustomEqual();\n/**\n * Whether the items passed are deeply-equal in value based on strict comparison.\n */\nconst strictDeepEqual = createCustomEqual({ strict: true });\n/**\n * Whether the items passed are deeply-equal in value, including circular references.\n */\nconst circularDeepEqual = createCustomEqual({ circular: true });\n/**\n * Whether the items passed are deeply-equal in value, including circular references,\n * based on strict comparison.\n */\nconst strictCircularDeepEqual = createCustomEqual({\n circular: true,\n strict: true,\n});\n/**\n * Whether the items passed are shallowly-equal in value.\n */\nconst shallowEqual = createCustomEqual({\n createInternalComparator: () => sameValueEqual,\n});\n/**\n * Whether the items passed are shallowly-equal in value based on strict comparison\n */\nconst strictShallowEqual = createCustomEqual({\n strict: true,\n createInternalComparator: () => sameValueEqual,\n});\n/**\n * Whether the items passed are shallowly-equal in value, including circular references.\n */\nconst circularShallowEqual = createCustomEqual({\n circular: true,\n createInternalComparator: () => sameValueEqual,\n});\n/**\n * Whether the items passed are shallowly-equal in value, including circular references,\n * based on strict comparison.\n */\nconst strictCircularShallowEqual = createCustomEqual({\n circular: true,\n createInternalComparator: () => sameValueEqual,\n strict: true,\n});\n/**\n * Create a custom equality comparison method.\n *\n * This can be done to create very targeted comparisons in extreme hot-path scenarios\n * where the standard methods are not performant enough, but can also be used to provide\n * support for legacy environments that do not support expected features like\n * `RegExp.prototype.flags` out of the box.\n */\nfunction createCustomEqual(options = {}) {\n const { circular = false, createInternalComparator: createCustomInternalComparator, createState, strict = false, } = options;\n const config = createEqualityComparatorConfig(options);\n const comparator = createEqualityComparator(config);\n const equals = createCustomInternalComparator\n ? createCustomInternalComparator(comparator)\n : createInternalEqualityComparator(comparator);\n return createIsEqual({ circular, comparator, createState, equals, strict });\n}\n\nexport { circularDeepEqual, circularShallowEqual, createCustomEqual, deepEqual, sameValueEqual, sameValueZeroEqual, shallowEqual, strictCircularDeepEqual, strictCircularShallowEqual, strictDeepEqual, strictEqual, strictShallowEqual };\n", "export {\n LCS,\n diffComm,\n diffIndices,\n diffPatch,\n diff3MergeRegions,\n diff3Merge,\n mergeDiff3,\n merge,\n mergeDigIn,\n patch,\n stripPatch,\n invertPatch\n};\n\n\n// Text diff algorithm following Hunt and McIlroy 1976.\n// J. W. Hunt and M. D. McIlroy, An algorithm for differential buffer\n// comparison, Bell Telephone Laboratories CSTR #41 (1976)\n// http://www.cs.dartmouth.edu/~doug/\n// https://en.wikipedia.org/wiki/Longest_common_subsequence_problem\n//\n// Expects two arrays, finds longest common sequence\nfunction LCS(buffer1, buffer2) {\n let equivalenceClasses = {};\n for (let j = 0; j < buffer2.length; j++) {\n const item = buffer2[j];\n if (equivalenceClasses[item]) {\n equivalenceClasses[item].push(j);\n } else {\n equivalenceClasses[item] = [j];\n }\n }\n\n const NULLRESULT = { buffer1index: -1, buffer2index: -1, chain: null };\n let candidates = [NULLRESULT];\n\n for (let i = 0; i < buffer1.length; i++) {\n const item = buffer1[i];\n const buffer2indices = equivalenceClasses[item] || [];\n let r = 0;\n let c = candidates[0];\n\n for (let jx = 0; jx < buffer2indices.length; jx++) {\n const j = buffer2indices[jx];\n\n let s;\n for (s = r; s < candidates.length; s++) {\n if ((candidates[s].buffer2index < j) && ((s === candidates.length - 1) || (candidates[s + 1].buffer2index > j))) {\n break;\n }\n }\n\n if (s < candidates.length) {\n const newCandidate = { buffer1index: i, buffer2index: j, chain: candidates[s] };\n if (r === candidates.length) {\n candidates.push(c);\n } else {\n candidates[r] = c;\n }\n r = s + 1;\n c = newCandidate;\n if (r === candidates.length) {\n break; // no point in examining further (j)s\n }\n }\n }\n\n candidates[r] = c;\n }\n\n // At this point, we know the LCS: it's in the reverse of the\n // linked-list through .chain of candidates[candidates.length - 1].\n\n return candidates[candidates.length - 1];\n}\n\n\n// We apply the LCS to build a 'comm'-style picture of the\n// differences between buffer1 and buffer2.\nfunction diffComm(buffer1, buffer2) {\n const lcs = LCS(buffer1, buffer2);\n let result = [];\n let tail1 = buffer1.length;\n let tail2 = buffer2.length;\n let common = {common: []};\n\n function processCommon() {\n if (common.common.length) {\n common.common.reverse();\n result.push(common);\n common = {common: []};\n }\n }\n\n for (let candidate = lcs; candidate !== null; candidate = candidate.chain) {\n let different = {buffer1: [], buffer2: []};\n\n while (--tail1 > candidate.buffer1index) {\n different.buffer1.push(buffer1[tail1]);\n }\n\n while (--tail2 > candidate.buffer2index) {\n different.buffer2.push(buffer2[tail2]);\n }\n\n if (different.buffer1.length || different.buffer2.length) {\n processCommon();\n different.buffer1.reverse();\n different.buffer2.reverse();\n result.push(different);\n }\n\n if (tail1 >= 0) {\n common.common.push(buffer1[tail1]);\n }\n }\n\n processCommon();\n\n result.reverse();\n return result;\n}\n\n\n// We apply the LCS to give a simple representation of the\n// offsets and lengths of mismatched chunks in the input\n// buffers. This is used by diff3MergeRegions.\nfunction diffIndices(buffer1, buffer2) {\n const lcs = LCS(buffer1, buffer2);\n let result = [];\n let tail1 = buffer1.length;\n let tail2 = buffer2.length;\n\n for (let candidate = lcs; candidate !== null; candidate = candidate.chain) {\n const mismatchLength1 = tail1 - candidate.buffer1index - 1;\n const mismatchLength2 = tail2 - candidate.buffer2index - 1;\n tail1 = candidate.buffer1index;\n tail2 = candidate.buffer2index;\n\n if (mismatchLength1 || mismatchLength2) {\n result.push({\n buffer1: [tail1 + 1, mismatchLength1],\n buffer1Content: buffer1.slice(tail1 + 1, tail1 + 1 + mismatchLength1),\n buffer2: [tail2 + 1, mismatchLength2],\n buffer2Content: buffer2.slice(tail2 + 1, tail2 + 1 + mismatchLength2)\n });\n }\n }\n\n result.reverse();\n return result;\n}\n\n\n// We apply the LCS to build a JSON representation of a\n// diff(1)-style patch.\nfunction diffPatch(buffer1, buffer2) {\n const lcs = LCS(buffer1, buffer2);\n let result = [];\n let tail1 = buffer1.length;\n let tail2 = buffer2.length;\n\n function chunkDescription(buffer, offset, length) {\n let chunk = [];\n for (let i = 0; i < length; i++) {\n chunk.push(buffer[offset + i]);\n }\n return {\n offset: offset,\n length: length,\n chunk: chunk\n };\n }\n\n for (let candidate = lcs; candidate !== null; candidate = candidate.chain) {\n const mismatchLength1 = tail1 - candidate.buffer1index - 1;\n const mismatchLength2 = tail2 - candidate.buffer2index - 1;\n tail1 = candidate.buffer1index;\n tail2 = candidate.buffer2index;\n\n if (mismatchLength1 || mismatchLength2) {\n result.push({\n buffer1: chunkDescription(buffer1, candidate.buffer1index + 1, mismatchLength1),\n buffer2: chunkDescription(buffer2, candidate.buffer2index + 1, mismatchLength2)\n });\n }\n }\n\n result.reverse();\n return result;\n}\n\n\n// Given three buffers, A, O, and B, where both A and B are\n// independently derived from O, returns a fairly complicated\n// internal representation of merge decisions it's taken. The\n// interested reader may wish to consult\n//\n// Sanjeev Khanna, Keshav Kunal, and Benjamin C. Pierce.\n// 'A Formal Investigation of ' In Arvind and Prasad,\n// editors, Foundations of Software Technology and Theoretical\n// Computer Science (FSTTCS), December 2007.\n//\n// (http://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf)\n//\nfunction diff3MergeRegions(a, o, b) {\n\n // \"hunks\" are array subsets where `a` or `b` are different from `o`\n // https://www.gnu.org/software/diffutils/manual/html_node/diff3-Hunks.html\n let hunks = [];\n function addHunk(h, ab) {\n hunks.push({\n ab: ab,\n oStart: h.buffer1[0],\n oLength: h.buffer1[1], // length of o to remove\n abStart: h.buffer2[0],\n abLength: h.buffer2[1] // length of a/b to insert\n // abContent: (ab === 'a' ? a : b).slice(h.buffer2[0], h.buffer2[0] + h.buffer2[1])\n });\n }\n\n diffIndices(o, a).forEach(item => addHunk(item, 'a'));\n diffIndices(o, b).forEach(item => addHunk(item, 'b'));\n hunks.sort((x,y) => x.oStart - y.oStart);\n\n let results = [];\n let currOffset = 0;\n\n function advanceTo(endOffset) {\n if (endOffset > currOffset) {\n results.push({\n stable: true,\n buffer: 'o',\n bufferStart: currOffset,\n bufferLength: endOffset - currOffset,\n bufferContent: o.slice(currOffset, endOffset)\n });\n currOffset = endOffset;\n }\n }\n\n while (hunks.length) {\n let hunk = hunks.shift();\n let regionStart = hunk.oStart;\n let regionEnd = hunk.oStart + hunk.oLength;\n let regionHunks = [hunk];\n advanceTo(regionStart);\n\n // Try to pull next overlapping hunk into this region\n while (hunks.length) {\n const nextHunk = hunks[0];\n const nextHunkStart = nextHunk.oStart;\n if (nextHunkStart > regionEnd) break; // no overlap\n\n regionEnd = Math.max(regionEnd, nextHunkStart + nextHunk.oLength);\n regionHunks.push(hunks.shift());\n }\n\n if (regionHunks.length === 1) {\n // Only one hunk touches this region, meaning that there is no conflict here.\n // Either `a` or `b` is inserting into a region of `o` unchanged by the other.\n if (hunk.abLength > 0) {\n const buffer = (hunk.ab === 'a' ? a : b);\n results.push({\n stable: true,\n buffer: hunk.ab,\n bufferStart: hunk.abStart,\n bufferLength: hunk.abLength,\n bufferContent: buffer.slice(hunk.abStart, hunk.abStart + hunk.abLength)\n });\n }\n } else {\n // A true a/b conflict. Determine the bounds involved from `a`, `o`, and `b`.\n // Effectively merge all the `a` hunks into one giant hunk, then do the\n // same for the `b` hunks; then, correct for skew in the regions of `o`\n // that each side changed, and report appropriate spans for the three sides.\n let bounds = {\n a: [a.length, -1, o.length, -1],\n b: [b.length, -1, o.length, -1]\n };\n while (regionHunks.length) {\n hunk = regionHunks.shift();\n const oStart = hunk.oStart;\n const oEnd = oStart + hunk.oLength;\n const abStart = hunk.abStart;\n const abEnd = abStart + hunk.abLength;\n let b = bounds[hunk.ab];\n b[0] = Math.min(abStart, b[0]);\n b[1] = Math.max(abEnd, b[1]);\n b[2] = Math.min(oStart, b[2]);\n b[3] = Math.max(oEnd, b[3]);\n }\n\n const aStart = bounds.a[0] + (regionStart - bounds.a[2]);\n const aEnd = bounds.a[1] + (regionEnd - bounds.a[3]);\n const bStart = bounds.b[0] + (regionStart - bounds.b[2]);\n const bEnd = bounds.b[1] + (regionEnd - bounds.b[3]);\n\n let result = {\n stable: false,\n aStart: aStart,\n aLength: aEnd - aStart,\n aContent: a.slice(aStart, aEnd),\n oStart: regionStart,\n oLength: regionEnd - regionStart,\n oContent: o.slice(regionStart, regionEnd),\n bStart: bStart,\n bLength: bEnd - bStart,\n bContent: b.slice(bStart, bEnd)\n };\n results.push(result);\n }\n currOffset = regionEnd;\n }\n\n advanceTo(o.length);\n\n return results;\n}\n\n\n// Applies the output of diff3MergeRegions to actually\n// construct the merged buffer; the returned result alternates\n// between 'ok' and 'conflict' blocks.\n// A \"false conflict\" is where `a` and `b` both change the same from `o`\nfunction diff3Merge(a, o, b, options) {\n let defaults = {\n excludeFalseConflicts: true,\n stringSeparator: /\\s+/\n };\n options = Object.assign(defaults, options);\n\n if (typeof a === 'string') a = a.split(options.stringSeparator);\n if (typeof o === 'string') o = o.split(options.stringSeparator);\n if (typeof b === 'string') b = b.split(options.stringSeparator);\n\n let results = [];\n const regions = diff3MergeRegions(a, o, b);\n\n let okBuffer = [];\n function flushOk() {\n if (okBuffer.length) {\n results.push({ ok: okBuffer });\n }\n okBuffer = [];\n }\n\n function isFalseConflict(a, b) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n }\n\n regions.forEach(region => {\n if (region.stable) {\n okBuffer.push(...region.bufferContent);\n } else {\n if (options.excludeFalseConflicts && isFalseConflict(region.aContent, region.bContent)) {\n okBuffer.push(...region.aContent);\n } else {\n flushOk();\n results.push({\n conflict: {\n a: region.aContent,\n aIndex: region.aStart,\n o: region.oContent,\n oIndex: region.oStart,\n b: region.bContent,\n bIndex: region.bStart\n }\n });\n }\n }\n });\n\n flushOk();\n return results;\n}\n\n\nfunction mergeDiff3(a, o, b, options) {\n const defaults = {\n excludeFalseConflicts: true,\n stringSeparator: /\\s+/,\n label: {}\n };\n options = Object.assign(defaults, options);\n\n const aSection = '<<<<<<<' + (options.label.a ? ` ${options.label.a}` : '');\n const oSection = '|||||||' + (options.label.o ? ` ${options.label.o}` : '');\n const xSection = '=======';\n const bSection = '>>>>>>>' + (options.label.b ? ` ${options.label.b}` : '');\n\n const regions = diff3Merge(a, o, b, options);\n let conflict = false;\n let result = [];\n\n regions.forEach(region => {\n if (region.ok) {\n result = result.concat(region.ok);\n } else if (region.conflict) {\n conflict = true;\n result = result.concat(\n [aSection],\n region.conflict.a,\n [oSection],\n region.conflict.o,\n [xSection],\n region.conflict.b,\n [bSection]\n );\n }\n });\n\n return {\n conflict: conflict,\n result: result\n };\n}\n\n\nfunction merge(a, o, b, options) {\n const defaults = {\n excludeFalseConflicts: true,\n stringSeparator: /\\s+/,\n label: {}\n };\n options = Object.assign(defaults, options);\n\n const aSection = '<<<<<<<' + (options.label.a ? ` ${options.label.a}` : '');\n const xSection = '=======';\n const bSection = '>>>>>>>' + (options.label.b ? ` ${options.label.b}` : '');\n\n const regions = diff3Merge(a, o, b, options);\n let conflict = false;\n let result = [];\n\n regions.forEach(region => {\n if (region.ok) {\n result = result.concat(region.ok);\n } else if (region.conflict) {\n conflict = true;\n result = result.concat(\n [aSection],\n region.conflict.a,\n [xSection],\n region.conflict.b,\n [bSection]\n );\n }\n });\n\n return {\n conflict: conflict,\n result: result\n };\n}\n\n\nfunction mergeDigIn(a, o, b, options) {\n const defaults = {\n excludeFalseConflicts: true,\n stringSeparator: /\\s+/,\n label: {}\n };\n options = Object.assign(defaults, options);\n\n const aSection = '<<<<<<<' + (options.label.a ? ` ${options.label.a}` : '');\n const xSection = '=======';\n const bSection = '>>>>>>>' + (options.label.b ? ` ${options.label.b}` : '');\n\n const regions = diff3Merge(a, o, b, options);\n let conflict = false;\n let result = [];\n\n regions.forEach(region => {\n if (region.ok) {\n result = result.concat(region.ok);\n } else {\n const c = diffComm(region.conflict.a, region.conflict.b);\n for (let j = 0; j < c.length; j++) {\n let inner = c[j];\n if (inner.common) {\n result = result.concat(inner.common);\n } else {\n conflict = true;\n result = result.concat(\n [aSection],\n inner.buffer1,\n [xSection],\n inner.buffer2,\n [bSection]\n );\n }\n }\n }\n });\n\n return {\n conflict: conflict,\n result: result\n };\n}\n\n\n// Applies a patch to a buffer.\n// Given buffer1 and buffer2, `patch(buffer1, diffPatch(buffer1, buffer2))` should give buffer2.\nfunction patch(buffer, patch) {\n let result = [];\n let currOffset = 0;\n\n function advanceTo(targetOffset) {\n while (currOffset < targetOffset) {\n result.push(buffer[currOffset]);\n currOffset++;\n }\n }\n\n for (let chunkIndex = 0; chunkIndex < patch.length; chunkIndex++) {\n let chunk = patch[chunkIndex];\n advanceTo(chunk.buffer1.offset);\n for (let itemIndex = 0; itemIndex < chunk.buffer2.chunk.length; itemIndex++) {\n result.push(chunk.buffer2.chunk[itemIndex]);\n }\n currOffset += chunk.buffer1.length;\n }\n\n advanceTo(buffer.length);\n return result;\n}\n\n\n// Takes the output of diffPatch(), and removes extra information from it.\n// It can still be used by patch(), below, but can no longer be inverted.\nfunction stripPatch(patch) {\n return patch.map(chunk => ({\n buffer1: { offset: chunk.buffer1.offset, length: chunk.buffer1.length },\n buffer2: { chunk: chunk.buffer2.chunk }\n }));\n}\n\n\n// Takes the output of diffPatch(), and inverts the sense of it, so that it\n// can be applied to buffer2 to give buffer1 rather than the other way around.\nfunction invertPatch(patch) {\n return patch.map(chunk => ({\n buffer1: chunk.buffer2,\n buffer2: chunk.buffer1\n }));\n}\n", "import { deepEqual } from 'fast-equals';\nimport { diff3Merge } from 'node-diff3';\n\nimport { t } from '../core/localizer';\nimport { actionDeleteMultiple } from './delete_multiple';\nimport { osmEntity } from '../osm';\nimport { utilArrayUnion, utilArrayUniq } from '../util';\n\n\nexport function actionMergeRemoteChanges(id, localGraph, remoteGraph, discardTags, formatUser) {\n discardTags = discardTags || {};\n var _option = 'safe'; // 'safe', 'force_local', 'force_remote'\n var _conflicts = [];\n\n\n function user(user) {\n return (typeof formatUser === 'function') ? selection => selection.call(formatUser, user) : selection => selection.text(user);\n }\n\n\n function mergeLocation(remote, target) {\n function pointEqual(a, b) {\n var epsilon = 1e-6;\n return (Math.abs(a[0] - b[0]) < epsilon) && (Math.abs(a[1] - b[1]) < epsilon);\n }\n\n if (_option === 'force_local' || pointEqual(target.loc, remote.loc)) {\n return target;\n }\n if (_option === 'force_remote') {\n return target.update({loc: remote.loc});\n }\n\n _conflicts.push(t.append('merge_remote_changes.conflict.location', { user: user(remote.user) } ));\n return target;\n }\n\n\n function mergeNodes(base, remote, target) {\n if (_option === 'force_local' || deepEqual(target.nodes, remote.nodes)) {\n return target;\n }\n if (_option === 'force_remote') {\n return target.update({nodes: remote.nodes});\n }\n\n var ccount = _conflicts.length;\n var o = base.nodes || [];\n var a = target.nodes || [];\n var b = remote.nodes || [];\n var nodes = [];\n var hunks = diff3Merge(a, o, b, { excludeFalseConflicts: true });\n\n for (var i = 0; i < hunks.length; i++) {\n var hunk = hunks[i];\n if (hunk.ok) {\n nodes.push.apply(nodes, hunk.ok);\n } else {\n // for all conflicts, we can assume c.a !== c.b\n // because `diff3Merge` called with `true` option to exclude false conflicts..\n var c = hunk.conflict;\n if (deepEqual(c.o, c.a)) { // only changed remotely\n nodes.push.apply(nodes, c.b);\n } else if (deepEqual(c.o, c.b)) { // only changed locally\n nodes.push.apply(nodes, c.a);\n } else { // changed both locally and remotely\n _conflicts.push(t.append('merge_remote_changes.conflict.nodelist', { user: user(remote.user) }));\n break;\n }\n }\n }\n\n return (_conflicts.length === ccount) ? target.update({nodes: nodes}) : target;\n }\n\n\n function mergeChildren(targetWay, children, updates, graph) {\n function isUsed(node, targetWay) {\n var hasInterestingParent = graph.parentWays(node)\n .some(function(way) { return way.id !== targetWay.id; });\n\n return node.hasInterestingTags() ||\n hasInterestingParent ||\n graph.parentRelations(node).length > 0;\n }\n\n var ccount = _conflicts.length;\n\n for (var i = 0; i < children.length; i++) {\n var id = children[i];\n var node = graph.hasEntity(id);\n\n // remove unused childNodes..\n if (targetWay.nodes.indexOf(id) === -1) {\n if (node && !isUsed(node, targetWay)) {\n updates.removeIds.push(id);\n }\n continue;\n }\n\n // restore used childNodes..\n var local = localGraph.hasEntity(id);\n var remote = remoteGraph.hasEntity(id);\n var target;\n\n if (_option === 'force_remote' && remote && remote.visible) {\n updates.replacements.push(remote);\n\n } else if (_option === 'force_local' && local) {\n target = osmEntity(local);\n if (remote) {\n target = target.update({ version: remote.version });\n }\n updates.replacements.push(target);\n\n } else if (_option === 'safe' && local && remote && local.version !== remote.version) {\n target = osmEntity(local, { version: remote.version });\n if (remote.visible) {\n target = mergeLocation(remote, target);\n } else {\n _conflicts.push(t.append('merge_remote_changes.conflict.deleted', { user: user(remote.user) }));\n }\n\n if (_conflicts.length !== ccount) break;\n updates.replacements.push(target);\n }\n }\n\n return targetWay;\n }\n\n\n function updateChildren(updates, graph) {\n for (var i = 0; i < updates.replacements.length; i++) {\n graph = graph.replace(updates.replacements[i]);\n }\n if (updates.removeIds.length) {\n graph = actionDeleteMultiple(updates.removeIds)(graph);\n }\n return graph;\n }\n\n\n function mergeMembers(remote, target) {\n if (_option === 'force_local' || deepEqual(target.members, remote.members)) {\n return target;\n }\n if (_option === 'force_remote') {\n return target.update({members: remote.members});\n }\n\n _conflicts.push(t.append('merge_remote_changes.conflict.memberlist', { user: user(remote.user) }));\n return target;\n }\n\n\n function mergeTags(base, remote, target) {\n if (_option === 'force_local' || deepEqual(target.tags, remote.tags)) {\n return target;\n }\n if (_option === 'force_remote') {\n return target.update({tags: remote.tags});\n }\n\n var ccount = _conflicts.length;\n var o = base.tags || {};\n var a = target.tags || {};\n var b = remote.tags || {};\n var keys = utilArrayUnion(utilArrayUnion(Object.keys(o), Object.keys(a)), Object.keys(b))\n .filter(function(k) { return !discardTags[k]; });\n var tags = Object.assign({}, a); // shallow copy\n var changed = false;\n\n for (var i = 0; i < keys.length; i++) {\n var k = keys[i];\n\n if (o[k] !== b[k] && a[k] !== b[k]) { // changed remotely..\n if (o[k] !== a[k]) { // changed locally..\n _conflicts.push(t.append('merge_remote_changes.conflict.tags',\n { tag: k, local: a[k], remote: b[k], user: user(remote.user) }));\n } else { // unchanged locally, accept remote change..\n if (b.hasOwnProperty(k)) {\n tags[k] = b[k];\n } else {\n delete tags[k];\n }\n changed = true;\n }\n }\n }\n\n return (changed && _conflicts.length === ccount) ? target.update({tags: tags}) : target;\n }\n\n\n // `graph.base()` is the common ancestor of the two graphs.\n // `localGraph` contains user's edits up to saving\n // `remoteGraph` contains remote edits to modified nodes\n // `graph` must be a descendent of `localGraph` and may include\n // some conflict resolution actions performed on it.\n //\n // --- ... --- `localGraph` -- ... -- `graph`\n // /\n // `graph.base()` --- ... --- `remoteGraph`\n //\n var action = function(graph) {\n var updates = { replacements: [], removeIds: [] };\n var base = graph.base().entities[id];\n var local = localGraph.entity(id);\n var remote = remoteGraph.entity(id);\n var target = osmEntity(local, { version: remote.version });\n\n // delete/undelete\n if (!remote.visible) {\n if (_option === 'force_remote') {\n return actionDeleteMultiple([id])(graph);\n\n } else if (_option === 'force_local') {\n if (target.type === 'way') {\n target = mergeChildren(target, utilArrayUniq(local.nodes), updates, graph);\n graph = updateChildren(updates, graph);\n }\n return graph.replace(target);\n\n } else {\n _conflicts.push(t.append('merge_remote_changes.conflict.deleted', { user: user(remote.user) }));\n return graph; // do nothing\n }\n }\n\n // merge\n if (target.type === 'node') {\n target = mergeLocation(remote, target);\n\n } else if (target.type === 'way') {\n // pull in any child nodes that may not be present locally..\n graph.rebase(remoteGraph.childNodes(remote), [graph], false);\n target = mergeNodes(base, remote, target);\n target = mergeChildren(target, utilArrayUnion(local.nodes, remote.nodes), updates, graph);\n\n } else if (target.type === 'relation') {\n target = mergeMembers(remote, target);\n }\n\n target = mergeTags(base, remote, target);\n\n if (!_conflicts.length) {\n graph = updateChildren(updates, graph).replace(target);\n }\n\n return graph;\n };\n\n\n action.withOption = function(opt) {\n _option = opt;\n return action;\n };\n\n\n action.conflicts = function() {\n return _conflicts;\n };\n\n\n return action;\n}\n", "import {\n geoAngle, geoChooseEdge, geoPathIntersections, geoPathLength,\n geoVecAdd, geoVecEqual, geoVecInterp, geoVecSubtract\n} from '../geo';\n\nimport { osmNode } from '../osm/node';\nimport { utilArrayIntersection } from '../util';\n\n\n// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java\n// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as\nexport function actionMove(moveIDs, tryDelta, projection, cache) {\n var _delta = tryDelta;\n\n function setupCache(graph) {\n function canMove(nodeID) {\n // Allow movement of any node that is in the selectedIDs list..\n if (moveIDs.indexOf(nodeID) !== -1) return true;\n\n // Allow movement of a vertex where 2 ways meet..\n var parents = graph.parentWays(graph.entity(nodeID));\n if (parents.length < 3) return true;\n\n // Restrict movement of a vertex where >2 ways meet, unless all parentWays are moving too..\n var parentsMoving = parents.every(function(way) { return cache.moving[way.id]; });\n if (!parentsMoving) delete cache.moving[nodeID];\n\n return parentsMoving;\n }\n\n function cacheEntities(ids) {\n for (var i = 0; i < ids.length; i++) {\n var id = ids[i];\n if (cache.moving[id]) continue;\n cache.moving[id] = true;\n\n var entity = graph.hasEntity(id);\n if (!entity) continue;\n\n if (entity.type === 'node') {\n cache.nodes.push(id);\n cache.startLoc[id] = entity.loc;\n } else if (entity.type === 'way') {\n cache.ways.push(id);\n cacheEntities(entity.nodes);\n } else {\n cacheEntities(entity.members.map(function(member) {\n return member.id;\n }));\n }\n }\n }\n\n function cacheIntersections(ids) {\n function isEndpoint(way, id) {\n return !way.isClosed() && !!way.affix(id);\n }\n\n for (var i = 0; i < ids.length; i++) {\n var id = ids[i];\n\n // consider only intersections with 1 moved and 1 unmoved way.\n var childNodes = graph.childNodes(graph.entity(id));\n for (var j = 0; j < childNodes.length; j++) {\n var node = childNodes[j];\n var parents = graph.parentWays(node);\n if (parents.length !== 2) continue;\n\n var moved = graph.entity(id);\n var unmoved = null;\n for (var k = 0; k < parents.length; k++) {\n var way = parents[k];\n if (!cache.moving[way.id]) {\n unmoved = way;\n break;\n }\n }\n if (!unmoved) continue;\n\n // exclude ways that are overly connected..\n if (utilArrayIntersection(moved.nodes, unmoved.nodes).length > 2) continue;\n if (moved.isArea() || unmoved.isArea()) continue;\n\n cache.intersections.push({\n nodeId: node.id,\n movedId: moved.id,\n unmovedId: unmoved.id,\n movedIsEP: isEndpoint(moved, node.id),\n unmovedIsEP: isEndpoint(unmoved, node.id)\n });\n }\n }\n }\n\n\n if (!cache) {\n cache = {};\n }\n if (!cache.ok) {\n cache.moving = {};\n cache.intersections = [];\n cache.replacedVertex = {};\n cache.startLoc = {};\n cache.nodes = [];\n cache.ways = [];\n\n cacheEntities(moveIDs);\n cacheIntersections(cache.ways);\n cache.nodes = cache.nodes.filter(canMove);\n\n cache.ok = true;\n }\n }\n\n\n // Place a vertex where the moved vertex used to be, to preserve way shape..\n //\n // Start:\n // b ---- e\n // / \\\n // / \\\n // / \\\n // a c\n //\n // * node '*' added to preserve shape\n // / \\\n // / b ---- e way `b,e` moved here:\n // / \\\n // a c\n //\n //\n function replaceMovedVertex(nodeId, wayId, graph, delta) {\n var way = graph.entity(wayId);\n var moved = graph.entity(nodeId);\n var movedIndex = way.nodes.indexOf(nodeId);\n var len, prevIndex, nextIndex;\n\n if (way.isClosed()) {\n len = way.nodes.length - 1;\n prevIndex = (movedIndex + len - 1) % len;\n nextIndex = (movedIndex + len + 1) % len;\n } else {\n len = way.nodes.length;\n prevIndex = movedIndex - 1;\n nextIndex = movedIndex + 1;\n }\n\n var prev = graph.hasEntity(way.nodes[prevIndex]);\n var next = graph.hasEntity(way.nodes[nextIndex]);\n\n // Don't add orig vertex at endpoint..\n if (!prev || !next) return graph;\n\n var key = wayId + '_' + nodeId;\n var orig = cache.replacedVertex[key];\n if (!orig) {\n orig = osmNode();\n cache.replacedVertex[key] = orig;\n cache.startLoc[orig.id] = cache.startLoc[nodeId];\n }\n\n var start, end;\n if (delta) {\n start = projection(cache.startLoc[nodeId]);\n end = projection.invert(geoVecAdd(start, delta));\n } else {\n end = cache.startLoc[nodeId];\n }\n orig = orig.move(end);\n\n var angle = Math.abs(geoAngle(orig, prev, projection) -\n geoAngle(orig, next, projection)) * 180 / Math.PI;\n\n // Don't add orig vertex if it would just make a straight line..\n if (angle > 175 && angle < 185) return graph;\n\n // moving forward or backward along way?\n var p1 = [prev.loc, orig.loc, moved.loc, next.loc].map(projection);\n var p2 = [prev.loc, moved.loc, orig.loc, next.loc].map(projection);\n var d1 = geoPathLength(p1);\n var d2 = geoPathLength(p2);\n var insertAt = (d1 <= d2) ? movedIndex : nextIndex;\n\n // moving around closed loop?\n if (way.isClosed() && insertAt === 0) insertAt = len;\n\n way = way.addNode(orig.id, insertAt);\n return graph.replace(orig).replace(way);\n }\n\n\n // Remove duplicate vertex that might have been added by\n // replaceMovedVertex. This is done after the unzorro checks.\n function removeDuplicateVertices(wayId, graph) {\n var way = graph.entity(wayId);\n var epsilon = 1e-6;\n var prev, curr;\n\n function isInteresting(node, graph) {\n return graph.parentWays(node).length > 1 ||\n graph.parentRelations(node).length ||\n node.hasInterestingTags();\n }\n\n for (var i = 0; i < way.nodes.length; i++) {\n curr = graph.entity(way.nodes[i]);\n\n if (prev && curr && geoVecEqual(prev.loc, curr.loc, epsilon)) {\n if (!isInteresting(prev, graph)) {\n way = way.removeNode(prev.id);\n graph = graph.replace(way).remove(prev);\n } else if (!isInteresting(curr, graph)) {\n way = way.removeNode(curr.id);\n graph = graph.replace(way).remove(curr);\n }\n }\n\n prev = curr;\n }\n\n return graph;\n }\n\n\n // Reorder nodes around intersections that have moved..\n //\n // Start: way1.nodes: b,e (moving)\n // a - b - c ----- d way2.nodes: a,b,c,d (static)\n // | vertex: b\n // e isEP1: true, isEP2, false\n //\n // way1 `b,e` moved here:\n // a ----- c = b - d\n // |\n // e\n //\n // reorder nodes way1.nodes: b,e\n // a ----- c - b - d way2.nodes: a,c,b,d\n // |\n // e\n //\n function unZorroIntersection(intersection, graph) {\n var vertex = graph.entity(intersection.nodeId);\n var way1 = graph.entity(intersection.movedId);\n var way2 = graph.entity(intersection.unmovedId);\n var isEP1 = intersection.movedIsEP;\n var isEP2 = intersection.unmovedIsEP;\n\n // don't move the vertex if it is the endpoint of both ways.\n if (isEP1 && isEP2) return graph;\n\n var nodes1 = graph.childNodes(way1).filter(function(n) { return n !== vertex; });\n var nodes2 = graph.childNodes(way2).filter(function(n) { return n !== vertex; });\n\n if (way1.isClosed() && way1.first() === vertex.id) nodes1.push(nodes1[0]);\n if (way2.isClosed() && way2.first() === vertex.id) nodes2.push(nodes2[0]);\n\n var edge1 = !isEP1 && geoChooseEdge(nodes1, projection(vertex.loc), projection);\n var edge2 = !isEP2 && geoChooseEdge(nodes2, projection(vertex.loc), projection);\n var loc;\n\n // snap vertex to nearest edge (or some point between them)..\n if (!isEP1 && !isEP2) {\n var epsilon = 1e-6, maxIter = 10;\n for (var i = 0; i < maxIter; i++) {\n loc = geoVecInterp(edge1.loc, edge2.loc, 0.5);\n edge1 = geoChooseEdge(nodes1, projection(loc), projection);\n edge2 = geoChooseEdge(nodes2, projection(loc), projection);\n if (Math.abs(edge1.distance - edge2.distance) < epsilon) break;\n }\n } else if (!isEP1) {\n loc = edge1.loc;\n } else {\n loc = edge2.loc;\n }\n\n graph = graph.replace(vertex.move(loc));\n\n // if zorro happened, reorder nodes..\n if (!isEP1 && edge1.index !== way1.nodes.indexOf(vertex.id)) {\n way1 = way1.removeNode(vertex.id).addNode(vertex.id, edge1.index);\n graph = graph.replace(way1);\n }\n if (!isEP2 && edge2.index !== way2.nodes.indexOf(vertex.id)) {\n way2 = way2.removeNode(vertex.id).addNode(vertex.id, edge2.index);\n graph = graph.replace(way2);\n }\n\n return graph;\n }\n\n\n function cleanupIntersections(graph) {\n for (var i = 0; i < cache.intersections.length; i++) {\n var obj = cache.intersections[i];\n graph = replaceMovedVertex(obj.nodeId, obj.movedId, graph, _delta);\n graph = replaceMovedVertex(obj.nodeId, obj.unmovedId, graph, null);\n graph = unZorroIntersection(obj, graph);\n graph = removeDuplicateVertices(obj.movedId, graph);\n graph = removeDuplicateVertices(obj.unmovedId, graph);\n }\n\n return graph;\n }\n\n\n // check if moving way endpoint can cross an unmoved way, if so limit delta..\n function limitDelta(graph) {\n function moveNode(loc) {\n return geoVecAdd(projection(loc), _delta);\n }\n\n for (var i = 0; i < cache.intersections.length; i++) {\n var obj = cache.intersections[i];\n\n // Don't limit movement if this is vertex joins 2 endpoints..\n if (obj.movedIsEP && obj.unmovedIsEP) continue;\n // Don't limit movement if this vertex is not an endpoint anyway..\n if (!obj.movedIsEP) continue;\n\n var node = graph.entity(obj.nodeId);\n var start = projection(node.loc);\n var end = geoVecAdd(start, _delta);\n var movedNodes = graph.childNodes(graph.entity(obj.movedId));\n var movedPath = movedNodes.map(function(n) { return moveNode(n.loc); });\n var unmovedNodes = graph.childNodes(graph.entity(obj.unmovedId));\n var unmovedPath = unmovedNodes.map(function(n) { return projection(n.loc); });\n var hits = geoPathIntersections(movedPath, unmovedPath);\n\n for (var j = 0; i < hits.length; i++) {\n if (geoVecEqual(hits[j], end)) continue;\n var edge = geoChooseEdge(unmovedNodes, end, projection);\n _delta = geoVecSubtract(projection(edge.loc), start);\n }\n }\n }\n\n\n var action = function(graph) {\n if (_delta[0] === 0 && _delta[1] === 0) return graph;\n\n setupCache(graph);\n\n if (cache.intersections.length) {\n limitDelta(graph);\n }\n\n for (var i = 0; i < cache.nodes.length; i++) {\n var node = graph.entity(cache.nodes[i]);\n var start = projection(node.loc);\n var end = geoVecAdd(start, _delta);\n graph = graph.replace(node.move(projection.invert(end)));\n }\n\n if (cache.intersections.length) {\n graph = cleanupIntersections(graph);\n }\n\n return graph;\n };\n\n\n action.delta = function() {\n return _delta;\n };\n\n\n return action;\n}\n", "export function actionMoveMember(relationId, fromIndex, toIndex) {\n return function(graph) {\n return graph.replace(graph.entity(relationId).moveMember(fromIndex, toIndex));\n };\n}\n", "import { geoVecInterp } from '../geo';\n\nexport function actionMoveNode(nodeID, toLoc) {\n\n var action = function(graph, t) {\n if (t === null || !isFinite(t)) t = 1;\n t = Math.min(Math.max(+t, 0), 1);\n\n var node = graph.entity(nodeID);\n return graph.replace(\n node.move(geoVecInterp(node.loc, toLoc, t))\n );\n };\n\n action.transitionable = true;\n\n return action;\n}\n", "export function actionNoop() {\n return function(graph) {\n return graph;\n };\n}\n", "import { actionDeleteNode } from './delete_node';\nimport {\n geoVecAdd, geoVecEqual, geoVecInterp, geoVecLength, geoVecNormalize,\n geoVecProject, geoVecScale, geoVecSubtract,\n geoOrthoCalcScore, geoOrthoCanOrthogonalize, geoVecNormalizedDot\n} from '../geo';\n\n\nexport function actionOrthogonalize(wayID, projection, vertexID, degThresh, ep) {\n var epsilon = ep || 1e-4;\n var threshold = degThresh || 13; // degrees within right or straight to alter\n\n // We test normalized dot products so we can compare as cos(angle)\n var lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180);\n var upperThreshold = Math.cos(threshold * Math.PI / 180);\n\n\n var action = function(graph, t) {\n if (t === null || !isFinite(t)) t = 1;\n t = Math.min(Math.max(+t, 0), 1);\n\n let way = graph.entity(wayID);\n way = way.removeNode(''); // sanity check - remove any consecutive duplicates\n\n if (way.tags.nonsquare) {\n var tags = Object.assign({}, way.tags);\n // since we're squaring, remove indication that this is physically unsquare\n delete tags.nonsquare;\n way = way.update({tags: tags});\n }\n\n graph = graph.replace(way);\n\n var isClosed = way.isClosed();\n var nodes = graph.childNodes(way).slice(); // shallow copy\n if (isClosed) nodes.pop();\n\n if (vertexID !== undefined) {\n nodes = nodeSubset(nodes, vertexID, isClosed);\n if (nodes.length !== 3) return graph;\n }\n\n // note: all geometry functions here use the unclosed node/point/coord list\n\n var nodeCount = {};\n var points = [];\n var corner = { i: 0, dotp: 1 };\n var node, point, loc, score, motions, i, j;\n\n for (i = 0; i < nodes.length; i++) {\n node = nodes[i];\n nodeCount[node.id] = (nodeCount[node.id] || 0) + 1;\n points.push({ id: node.id, coord: projection(node.loc) });\n }\n\n\n if (points.length === 3) { // move only one vertex for right triangle\n for (i = 0; i < 1000; i++) {\n const motion = calcMotion(points[1], 1, points);\n\n points[corner.i].coord = geoVecAdd(points[corner.i].coord, motion);\n score = corner.dotp;\n if (score < epsilon) {\n break;\n }\n }\n\n node = graph.entity(nodes[corner.i].id);\n loc = projection.invert(points[corner.i].coord);\n graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t)));\n\n } else {\n const straights = [];\n const simplified = [];\n\n // Remove points from nearly straight sections..\n // This produces a simplified shape to orthogonalize\n for (i = 0; i < points.length; i++) {\n point = points[i];\n let dotp = 0;\n if (isClosed || (i > 0 && i < points.length - 1)) {\n const a = points[(i - 1 + points.length) % points.length];\n const b = points[(i + 1) % points.length];\n dotp = Math.abs(geoVecNormalizedDot(a.coord, b.coord, point.coord));\n }\n\n if (dotp > upperThreshold) {\n straights.push(point);\n } else {\n simplified.push(point);\n }\n }\n\n // Orthogonalize the simplified shape\n var bestPoints = clonePoints(simplified);\n var originalPoints = clonePoints(simplified);\n\n score = Infinity;\n for (i = 0; i < 1000; i++) {\n motions = simplified.map(calcMotion);\n\n for (j = 0; j < motions.length; j++) {\n simplified[j].coord = geoVecAdd(simplified[j].coord, motions[j]);\n }\n var newScore = geoOrthoCalcScore(simplified, isClosed, epsilon, threshold);\n if (newScore < score) {\n bestPoints = clonePoints(simplified);\n score = newScore;\n }\n if (score < epsilon) {\n break;\n }\n }\n\n var bestCoords = bestPoints.map(function(p) { return p.coord; });\n if (isClosed) bestCoords.push(bestCoords[0]);\n\n // move the nodes that should move\n for (i = 0; i < bestPoints.length; i++) {\n point = bestPoints[i];\n if (!geoVecEqual(originalPoints[i].coord, point.coord)) {\n node = graph.entity(point.id);\n loc = projection.invert(point.coord);\n graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t)));\n }\n }\n\n // move the nodes along straight segments\n for (i = 0; i < straights.length; i++) {\n point = straights[i];\n if (nodeCount[point.id] > 1) continue; // skip self-intersections\n\n node = graph.entity(point.id);\n\n if (t === 1 &&\n graph.parentWays(node).length === 1 &&\n graph.parentRelations(node).length === 0 &&\n !node.hasInterestingTags()\n ) {\n // remove uninteresting points..\n graph = actionDeleteNode(node.id)(graph);\n\n } else {\n // move interesting points to the nearest edge..\n var choice = geoVecProject(point.coord, bestCoords);\n if (choice) {\n loc = projection.invert(choice.target);\n graph = graph.replace(node.move(geoVecInterp(node.loc, loc, t)));\n }\n }\n }\n }\n\n return graph;\n\n\n function clonePoints(array) {\n return array.map(function(p) {\n return { id: p.id, coord: [p.coord[0], p.coord[1]] };\n });\n }\n\n\n function calcMotion(point, i, array) {\n // don't try to move the endpoints of a non-closed way.\n if (!isClosed && (i === 0 || i === array.length - 1)) return [0, 0];\n // don't try to move a node that appears more than once (self intersection)\n if (nodeCount[array[i].id] > 1) return [0, 0];\n\n var a = array[(i - 1 + array.length) % array.length].coord;\n var origin = point.coord;\n var b = array[(i + 1) % array.length].coord;\n var p = geoVecSubtract(a, origin);\n var q = geoVecSubtract(b, origin);\n\n var scale = 2 * Math.min(geoVecLength(p), geoVecLength(q));\n p = geoVecNormalize(p);\n q = geoVecNormalize(q);\n\n var dotp = (p[0] * q[0] + p[1] * q[1]);\n var val = Math.abs(dotp);\n\n if (val < lowerThreshold) { // nearly orthogonal\n corner.i = i;\n corner.dotp = val;\n var vec = geoVecNormalize(geoVecAdd(p, q));\n return geoVecScale(vec, 0.1 * dotp * scale);\n }\n\n return [0, 0]; // do nothing\n }\n };\n\n\n // if we are only orthogonalizing one vertex,\n // get that vertex and the previous and next\n function nodeSubset(nodes, vertexID, isClosed) {\n var first = isClosed ? 0 : 1;\n var last = isClosed ? nodes.length : nodes.length - 1;\n\n for (var i = first; i < last; i++) {\n if (nodes[i].id === vertexID) {\n return [\n nodes[(i - 1 + nodes.length) % nodes.length],\n nodes[i],\n nodes[(i + 1) % nodes.length]\n ];\n }\n }\n\n return [];\n }\n\n\n action.disabled = function(graph) {\n let way = graph.entity(wayID);\n way = way.removeNode(''); // sanity check - remove any consecutive duplicates\n graph = graph.replace(way);\n\n let isClosed = way.isClosed();\n let nodes = graph.childNodes(way).slice(); // shallow copy\n if (isClosed) nodes.pop();\n\n let allowStraightAngles = false;\n if (vertexID !== undefined) {\n allowStraightAngles = true;\n nodes = nodeSubset(nodes, vertexID, isClosed);\n if (nodes.length !== 3) return 'end_vertex';\n isClosed = false; // from now on: treat these 3 ways as a line\n }\n\n const coords = nodes.map(function(n) { return projection(n.loc); });\n const score = geoOrthoCanOrthogonalize(coords, isClosed, epsilon, threshold, allowStraightAngles);\n\n if (score === null) {\n return 'not_squarish';\n } else if (score === 0) {\n return 'square_enough';\n } else {\n return false;\n }\n };\n\n\n action.transitionable = true;\n\n return action;\n}\n", "import { geoGetSmallestSurroundingRectangle, geoVecInterp, geoVecLength } from '../geo';\nimport { utilGetAllNodes } from '../util';\n\n\n/* Reflect the given area around its axis of symmetry */\nexport function actionReflect(reflectIds, projection) {\n var _useLongAxis = true;\n\n\n var action = function(graph, t) {\n if (t === null || !isFinite(t)) t = 1;\n t = Math.min(Math.max(+t, 0), 1);\n\n const [p, q] = action.getReflectAxis(graph);\n\n // reflect c across pq\n // http://math.stackexchange.com/questions/65503/point-reflection-over-a-line\n var dx = q[0] - p[0];\n var dy = q[1] - p[1];\n var a = (dx * dx - dy * dy) / (dx * dx + dy * dy);\n var b = 2 * dx * dy / (dx * dx + dy * dy);\n\n const nodes = utilGetAllNodes(reflectIds, graph);\n for (const node of nodes) {\n const c = projection(node.loc);\n const newLoc = projection.invert([\n a * (c[0] - p[0]) + b * (c[1] - p[1]) + p[0],\n b * (c[0] - p[0]) - a * (c[1] - p[1]) + p[1]\n ]);\n graph = graph.replace(\n node.move(geoVecInterp(node.loc, newLoc, t)));\n }\n\n return graph;\n };\n\n\n action.useLongAxis = function(val) {\n if (!arguments.length) return _useLongAxis;\n _useLongAxis = val;\n return action;\n };\n\n\n action.getReflectAxis = function(graph) {\n const nodes = utilGetAllNodes(reflectIds, graph);\n const points = nodes.map(function(n) { return projection(n.loc); });\n const ssr = geoGetSmallestSurroundingRectangle(points);\n\n // Choose line pq = axis of symmetry.\n // The shape's surrounding rectangle has 2 axes of symmetry.\n // Reflect across the longer axis by default.\n const p1 = [(ssr.poly[0][0] + ssr.poly[1][0]) / 2, (ssr.poly[0][1] + ssr.poly[1][1]) / 2 ];\n const q1 = [(ssr.poly[2][0] + ssr.poly[3][0]) / 2, (ssr.poly[2][1] + ssr.poly[3][1]) / 2 ];\n const p2 = [(ssr.poly[3][0] + ssr.poly[4][0]) / 2, (ssr.poly[3][1] + ssr.poly[4][1]) / 2 ];\n const q2 = [(ssr.poly[1][0] + ssr.poly[2][0]) / 2, (ssr.poly[1][1] + ssr.poly[2][1]) / 2 ];\n\n const isLong = (geoVecLength(p1, q1) > geoVecLength(p2, q2));\n if ((_useLongAxis && isLong) || (!_useLongAxis && !isLong)) {\n return [p1, q1];\n } else {\n return [p2, q2];\n }\n };\n\n\n action.transitionable = true;\n\n\n return action;\n}\n", "import { osmRelation } from '../osm/relation';\n\n\n// `actionRestrictTurn` creates a turn restriction relation.\n//\n// `turn` must be an `osmTurn` object\n// see osm/intersection.js, pathToTurn()\n//\n// This specifies a restriction of type `restriction` when traveling from\n// `turn.from.way` toward `turn.to.way` via `turn.via.node` OR `turn.via.ways`.\n// (The action does not check that these entities form a valid intersection.)\n//\n// From, to, and via ways should be split before calling this action.\n// (old versions of the code would split the ways here, but we no longer do it)\n//\n// For testing convenience, accepts a restrictionID to assign to the new\n// relation. Normally, this will be undefined and the relation will\n// automatically be assigned a new ID.\n//\nexport function actionRestrictTurn(turn, restrictionType, restrictionID) {\n\n return function(graph) {\n var fromWay = graph.entity(turn.from.way);\n var toWay = graph.entity(turn.to.way);\n var viaNode = turn.via.node && graph.entity(turn.via.node);\n var viaWays = turn.via.ways && turn.via.ways.map(function(id) { return graph.entity(id); });\n var members = [];\n\n members.push({ id: fromWay.id, type: 'way', role: 'from' });\n\n if (viaNode) {\n members.push({ id: viaNode.id, type: 'node', role: 'via' });\n } else if (viaWays) {\n viaWays.forEach(function(viaWay) {\n members.push({ id: viaWay.id, type: 'way', role: 'via' });\n });\n }\n\n members.push({ id: toWay.id, type: 'way', role: 'to' });\n\n return graph.replace(osmRelation({\n id: restrictionID,\n tags: {\n type: 'restriction',\n restriction: restrictionType\n },\n members: members\n }));\n };\n}\n", "import { actionDeleteRelation } from './delete_relation';\nimport { actionDeleteWay } from './delete_way';\n\n\nexport function actionRevert(id) {\n var action = function(graph) {\n var entity = graph.hasEntity(id),\n base = graph.base().entities[id];\n\n if (entity && !base) { // entity will be removed..\n if (entity.type === 'node') {\n graph.parentWays(entity)\n .forEach(function(parent) {\n parent = parent.removeNode(id);\n graph = graph.replace(parent);\n\n if (parent.isDegenerate()) {\n graph = actionDeleteWay(parent.id)(graph);\n }\n });\n }\n\n graph.parentRelations(entity)\n .forEach(function(parent) {\n parent = parent.removeMembersWithID(id);\n graph = graph.replace(parent);\n\n if (parent.isDegenerate()) {\n graph = actionDeleteRelation(parent.id)(graph);\n }\n });\n }\n\n return graph.revert(id);\n };\n\n return action;\n}\n", "import { geoRotate } from '../geo';\nimport { utilGetAllNodes } from '../util';\n\n\nexport function actionRotate(rotateIds, pivot, angle, projection) {\n\n var action = function(graph) {\n return graph.update(function(graph) {\n utilGetAllNodes(rotateIds, graph).forEach(function(node) {\n var point = geoRotate([projection(node.loc)], angle, pivot)[0];\n graph = graph.replace(node.move(projection.invert(point)));\n });\n });\n };\n\n return action;\n}\n", "import { utilGetAllNodes } from '../util';\n\nexport function actionScale(ids, pivotLoc, scaleFactor, projection) {\n return function(graph) {\n return graph.update(function(graph) {\n let point, radial;\n\n utilGetAllNodes(ids, graph).forEach(function(node) {\n\n point = projection(node.loc);\n radial = [\n point[0] - pivotLoc[0],\n point[1] - pivotLoc[1]\n ];\n point = [\n pivotLoc[0] + (scaleFactor * radial[0]),\n pivotLoc[1] + (scaleFactor * radial[1])\n ];\n\n graph = graph.replace(node.move(projection.invert(point)));\n });\n });\n };\n}\n", "import { geoGetSmallestSurroundingRectangle, geoVecDot, geoVecLength, geoVecInterp } from '../geo';\n\n\n/* Align nodes along their common axis */\nexport function actionStraightenNodes(nodeIDs, projection) {\n\n function positionAlongWay(a, o, b) {\n return geoVecDot(a, b, o) / geoVecDot(b, b, o);\n }\n\n // returns the endpoints of the long axis of symmetry of the `points` bounding rect\n function getEndpoints(points) {\n var ssr = geoGetSmallestSurroundingRectangle(points);\n\n // Choose line pq = axis of symmetry.\n // The shape's surrounding rectangle has 2 axes of symmetry.\n // Snap points to the long axis\n var p1 = [(ssr.poly[0][0] + ssr.poly[1][0]) / 2, (ssr.poly[0][1] + ssr.poly[1][1]) / 2 ];\n var q1 = [(ssr.poly[2][0] + ssr.poly[3][0]) / 2, (ssr.poly[2][1] + ssr.poly[3][1]) / 2 ];\n var p2 = [(ssr.poly[3][0] + ssr.poly[4][0]) / 2, (ssr.poly[3][1] + ssr.poly[4][1]) / 2 ];\n var q2 = [(ssr.poly[1][0] + ssr.poly[2][0]) / 2, (ssr.poly[1][1] + ssr.poly[2][1]) / 2 ];\n\n var isLong = (geoVecLength(p1, q1) > geoVecLength(p2, q2));\n if (isLong) {\n return [p1, q1];\n }\n return [p2, q2];\n }\n\n\n var action = function(graph, t) {\n if (t === null || !isFinite(t)) t = 1;\n t = Math.min(Math.max(+t, 0), 1);\n\n var nodes = nodeIDs.map(function(id) { return graph.entity(id); });\n var points = nodes.map(function(n) { return projection(n.loc); });\n var endpoints = getEndpoints(points);\n var startPoint = endpoints[0];\n var endPoint = endpoints[1];\n\n // Move points onto the line connecting the endpoints\n for (var i = 0; i < points.length; i++) {\n var node = nodes[i];\n var point = points[i];\n var u = positionAlongWay(point, startPoint, endPoint);\n var point2 = geoVecInterp(startPoint, endPoint, u);\n var loc2 = projection.invert(point2);\n graph = graph.replace(node.move(geoVecInterp(node.loc, loc2, t)));\n }\n\n return graph;\n };\n\n\n action.disabled = function(graph) {\n\n var nodes = nodeIDs.map(function(id) { return graph.entity(id); });\n var points = nodes.map(function(n) { return projection(n.loc); });\n var endpoints = getEndpoints(points);\n var startPoint = endpoints[0];\n var endPoint = endpoints[1];\n\n var maxDistance = 0;\n\n for (var i = 0; i < points.length; i++) {\n var point = points[i];\n var u = positionAlongWay(point, startPoint, endPoint);\n var p = geoVecInterp(startPoint, endPoint, u);\n var dist = geoVecLength(p, point);\n\n if (!isNaN(dist) && dist > maxDistance) {\n maxDistance = dist;\n }\n }\n\n if (maxDistance < 0.0001) {\n return 'straight_enough';\n }\n };\n\n\n action.transitionable = true;\n\n\n return action;\n}\n", "import { actionDeleteNode } from './delete_node';\nimport { geoVecDot, geoVecInterp, geoVecLength } from '../geo';\nimport { utilArrayDifference } from '../util';\n\n\n/*\n * Based on https://github.com/openstreetmap/potlatch2/net/systemeD/potlatch2/tools/Straighten.as\n */\nexport function actionStraightenWay(selectedIDs, projection) {\n\n function positionAlongWay(a, o, b) {\n return geoVecDot(a, b, o) / geoVecDot(b, b, o);\n }\n\n // Return all selected ways as a continuous, ordered array of nodes\n function allNodes(graph) {\n var startNodes = [];\n var endNodes = [];\n var remainingWays = [];\n var selectedWays = selectedIDs.filter(function(w) {\n return graph.entity(w).type === 'way';\n });\n var selectedNodes = selectedIDs.filter(function(n) {\n return graph.entity(n).type === 'node';\n });\n\n for (var i = 0; i < selectedWays.length; i++) {\n var way = graph.entity(selectedWays[i]);\n const nodes = way.nodes.slice(0);\n remainingWays.push(nodes);\n startNodes.push(nodes[0]);\n endNodes.push(nodes[nodes.length-1]);\n }\n\n // Remove duplicate end/startNodes (duplicate nodes cannot be at the line end,\n // and need to be removed so currNode difference calculation below works)\n // i.e. [\"n-1\", \"n-1\", \"n-2\"] => [\"n-2\"]\n startNodes = startNodes.filter(function(n) {\n return startNodes.indexOf(n) === startNodes.lastIndexOf(n);\n });\n endNodes = endNodes.filter(function(n) {\n return endNodes.indexOf(n) === endNodes.lastIndexOf(n);\n });\n\n // Choose the initial endpoint to start from\n var currNode = utilArrayDifference(startNodes, endNodes)\n .concat(utilArrayDifference(endNodes, startNodes))[0];\n\n // Create nested function outside of loop to avoid \"function in loop\" lint error\n var getNextWay = function(currNode, remainingWays) {\n return remainingWays.filter(function(way) {\n return way[0] === currNode || way[way.length-1] === currNode;\n })[0];\n };\n\n // Add nodes to end of nodes array, until all ways are added\n let nodes = [];\n while (remainingWays.length) {\n const nextWay = getNextWay(currNode, remainingWays);\n remainingWays = utilArrayDifference(remainingWays, [nextWay]);\n\n if (nextWay[0] !== currNode) {\n nextWay.reverse();\n }\n nodes = nodes.concat(nextWay);\n currNode = nodes[nodes.length-1];\n }\n\n // If user selected 2 nodes to straighten between, then slice nodes array to those nodes\n if (selectedNodes.length === 2) {\n var startNodeIdx = nodes.indexOf(selectedNodes[0]);\n var endNodeIdx = nodes.indexOf(selectedNodes[1]);\n var sortedStartEnd = [startNodeIdx, endNodeIdx];\n\n sortedStartEnd.sort(function(a, b) { return a - b; });\n nodes = nodes.slice(sortedStartEnd[0], sortedStartEnd[1]+1);\n }\n\n return nodes.map(function(n) { return graph.entity(n); });\n }\n\n function shouldKeepNode(node, graph) {\n return graph.parentWays(node).length > 1 ||\n graph.parentRelations(node).length ||\n node.hasInterestingTags();\n }\n\n\n var action = function(graph, t) {\n if (t === null || !isFinite(t)) t = 1;\n t = Math.min(Math.max(+t, 0), 1);\n\n var nodes = allNodes(graph);\n var points = nodes.map(function(n) { return projection(n.loc); });\n var startPoint = points[0];\n var endPoint = points[points.length-1];\n var toDelete = [];\n var i;\n\n for (i = 1; i < points.length-1; i++) {\n var node = nodes[i];\n var point = points[i];\n\n if (t < 1 || shouldKeepNode(node, graph)) {\n var u = positionAlongWay(point, startPoint, endPoint);\n var p = geoVecInterp(startPoint, endPoint, u);\n var loc2 = projection.invert(p);\n graph = graph.replace(node.move(geoVecInterp(node.loc, loc2, t)));\n\n } else {\n // safe to delete\n if (toDelete.indexOf(node) === -1) {\n toDelete.push(node);\n }\n }\n }\n\n for (i = 0; i < toDelete.length; i++) {\n graph = actionDeleteNode(toDelete[i].id)(graph);\n }\n\n return graph;\n };\n\n\n action.disabled = function(graph) {\n // check way isn't too bendy\n var nodes = allNodes(graph);\n var points = nodes.map(function(n) { return projection(n.loc); });\n var startPoint = points[0];\n var endPoint = points[points.length-1];\n var threshold = 0.2 * geoVecLength(startPoint, endPoint);\n var i;\n\n if (threshold === 0) {\n return 'too_bendy';\n }\n\n var maxDistance = 0;\n\n for (i = 1; i < points.length - 1; i++) {\n var point = points[i];\n var u = positionAlongWay(point, startPoint, endPoint);\n var p = geoVecInterp(startPoint, endPoint, u);\n var dist = geoVecLength(p, point);\n\n // to bendy if point is off by 20% of total start/end distance in projected space\n if (isNaN(dist) || dist > threshold) {\n return 'too_bendy';\n } else if (dist > maxDistance) {\n maxDistance = dist;\n }\n }\n\n var keepingAllNodes = nodes.every(function(node, i) {\n return i === 0 || i === nodes.length - 1 || shouldKeepNode(node, graph);\n });\n\n if (maxDistance < 0.0001 &&\n // Allow straightening even if already straight in order to remove extraneous nodes\n keepingAllNodes) {\n return 'straight_enough';\n }\n };\n\n action.transitionable = true;\n\n\n return action;\n}\n", "import { actionDeleteRelation } from './delete_relation';\n\n\n// `actionUnrestrictTurn` deletes a turn restriction relation.\n//\n// `turn` must be an `osmTurn` object with a `restrictionID` property.\n// see osm/intersection.js, pathToTurn()\n//\nexport function actionUnrestrictTurn(turn) {\n return function(graph) {\n return actionDeleteRelation(turn.restrictionID)(graph);\n };\n}\n", "export function actionUpgradeTags(entityId, oldTags, replaceTags) {\n\n return function(graph) {\n var entity = graph.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n var transferValues = [];\n var semiIndex;\n\n for (var oldTagKey in oldTags) {\n if (!(oldTagKey in tags)) continue;\n // wildcard match\n if (oldTags[oldTagKey] === '*') {\n // note the value since we might need to transfer it\n transferValues.push(tags[oldTagKey]);\n delete tags[oldTagKey];\n // exact match\n } else if (oldTags[oldTagKey] === tags[oldTagKey]) {\n delete tags[oldTagKey];\n // match is within semicolon-delimited values\n } else {\n var vals = tags[oldTagKey].split(';').filter(Boolean);\n var oldIndex = vals.indexOf(oldTags[oldTagKey]);\n if (vals.length === 1 || oldIndex === -1) {\n delete tags[oldTagKey];\n } else {\n if (replaceTags && replaceTags[oldTagKey]) {\n // replacing a value within a semicolon-delimited value, note the index\n semiIndex = oldIndex;\n }\n vals.splice(oldIndex, 1);\n tags[oldTagKey] = vals.join(';');\n }\n }\n }\n\n if (replaceTags) {\n for (var replaceKey in replaceTags) {\n var replaceValue = replaceTags[replaceKey];\n if (replaceValue === '*') {\n if (tags[replaceKey] && tags[replaceKey] !== 'no') {\n // allow any pre-existing value except `no` (troll tag)\n continue;\n } else {\n // otherwise assume `yes` is okay\n tags[replaceKey] = 'yes';\n }\n } else if (replaceValue.startsWith('$')) {\n tags[replaceKey] = transferValues[+replaceValue.substring(1) - 1];\n } else {\n if (tags[replaceKey] && oldTags[replaceKey] && semiIndex !== undefined) {\n // don't override preexisting values\n var existingVals = tags[replaceKey].split(';').filter(Boolean);\n if (existingVals.indexOf(replaceValue) === -1) {\n existingVals.splice(semiIndex, 0, replaceValue);\n tags[replaceKey] = existingVals.join(';');\n }\n } else {\n tags[replaceKey] = replaceValue;\n }\n }\n }\n }\n\n return graph.replace(entity.update({ tags: tags }));\n };\n}\n", "export function behaviorEdit(context) {\n\n function behavior() {\n context.map()\n .minzoom(context.minEditableZoom());\n }\n\n\n behavior.off = function() {\n context.map()\n .minzoom(0);\n };\n\n return behavior;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { osmEntity, osmNote, QAItem } from '../osm';\nimport { utilKeybinding, utilRebind } from '../util';\n\n/*\n The hover behavior adds the `.hover` class on pointerover to all elements to which\n the identical datum is bound, and removes it on pointerout.\n\n The :hover pseudo-class is insufficient for iD's purposes because a datum's visual\n representation may consist of several elements scattered throughout the DOM hierarchy.\n Only one of these elements can have the :hover pseudo-class, but all of them will\n have the .hover class.\n */\nexport function behaviorHover(context) {\n var dispatch = d3_dispatch('hover');\n var _selection = d3_select(null);\n var _newNodeId = null;\n var _initialNodeID = null;\n var _altDisables;\n var _ignoreVertex;\n var _targets = [];\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function keydown(d3_event) {\n if (_altDisables && d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n _selection.selectAll('.hover')\n .classed('hover-suppressed', true)\n .classed('hover', false);\n\n _selection\n .classed('hover-disabled', true);\n\n dispatch.call('hover', this, null);\n }\n }\n\n\n function keyup(d3_event) {\n if (_altDisables && d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n _selection.selectAll('.hover-suppressed')\n .classed('hover-suppressed', false)\n .classed('hover', true);\n\n _selection\n .classed('hover-disabled', false);\n\n dispatch.call('hover', this, _targets);\n }\n }\n\n\n function behavior(selection) {\n _selection = selection;\n\n _targets = [];\n\n if (_initialNodeID) {\n _newNodeId = _initialNodeID;\n _initialNodeID = null;\n } else {\n _newNodeId = null;\n }\n\n _selection\n .on(_pointerPrefix + 'over.hover', pointerover)\n .on(_pointerPrefix + 'out.hover', pointerout)\n // treat pointerdown as pointerover for touch devices\n .on(_pointerPrefix + 'down.hover', pointerover);\n\n d3_select(window)\n .on(_pointerPrefix + 'up.hover pointercancel.hover', pointerout, true)\n .on('keydown.hover', keydown)\n .on('keyup.hover', keyup);\n\n\n function eventTarget(d3_event) {\n var datum = d3_event.target && d3_event.target.__data__;\n if (typeof datum !== 'object') return null;\n if (!(datum instanceof osmEntity) && datum.properties && (datum.properties.entity instanceof osmEntity)) {\n return datum.properties.entity;\n }\n return datum;\n }\n\n function pointerover(d3_event) {\n // ignore mouse hovers with buttons pressed unless dragging\n if (context.mode().id.indexOf('drag') === -1 &&\n (!d3_event.pointerType || d3_event.pointerType === 'mouse') &&\n d3_event.buttons) return;\n\n var target = eventTarget(d3_event);\n if (target && _targets.indexOf(target) === -1) {\n _targets.push(target);\n updateHover(d3_event, _targets);\n }\n }\n\n function pointerout(d3_event) {\n\n var target = eventTarget(d3_event);\n var index = _targets.indexOf(target);\n if (index !== -1) {\n _targets.splice(index);\n updateHover(d3_event, _targets);\n }\n }\n\n function allowsVertex(d) {\n return d.geometry(context.graph()) === 'vertex' || presetManager.allowsVertex(d, context.graph());\n }\n\n function modeAllowsHover(target) {\n var mode = context.mode();\n if (mode.id === 'add-point') {\n return mode.preset.matchGeometry('vertex') ||\n (target.type !== 'way' && target.geometry(context.graph()) !== 'vertex');\n }\n return true;\n }\n\n function updateHover(d3_event, targets) {\n\n _selection.selectAll('.hover')\n .classed('hover', false);\n _selection.selectAll('.hover-suppressed')\n .classed('hover-suppressed', false);\n\n var mode = context.mode();\n\n if (!_newNodeId && (mode.id === 'draw-line' || mode.id === 'draw-area')) {\n var node = targets.find(function(target) {\n return target instanceof osmEntity && target.type === 'node';\n });\n _newNodeId = node && node.id;\n }\n\n targets = targets.filter(function(datum) {\n if (datum instanceof osmEntity) {\n // If drawing a way, don't hover on a node that was just placed. #3974\n return datum.id !== _newNodeId &&\n (datum.type !== 'node' || !_ignoreVertex || allowsVertex(datum)) &&\n modeAllowsHover(datum);\n }\n return true;\n });\n\n var selector = '';\n\n for (var i in targets) {\n var datum = targets[i];\n\n // What are we hovering over?\n if (datum.__featurehash__) {\n // hovering custom data\n selector += ', .data' + datum.__featurehash__;\n\n } else if (datum instanceof QAItem) {\n selector += ', .' + datum.service + '.itemId-' + datum.id;\n\n } else if (datum instanceof osmNote) {\n selector += ', .note-' + datum.id;\n\n } else if (datum instanceof osmEntity) {\n selector += ', .' + datum.id;\n if (datum.type === 'relation') {\n for (var j in datum.members) {\n selector += ', .' + datum.members[j].id;\n }\n }\n }\n }\n\n var suppressed = _altDisables && d3_event && d3_event.altKey;\n\n if (selector.trim().length) {\n // remove the first comma\n selector = selector.slice(1);\n _selection.selectAll(selector)\n .classed(suppressed ? 'hover-suppressed' : 'hover', true);\n }\n\n dispatch.call('hover', this, !suppressed && targets);\n }\n }\n\n\n behavior.off = function(selection) {\n selection.selectAll('.hover')\n .classed('hover', false);\n selection.selectAll('.hover-suppressed')\n .classed('hover-suppressed', false);\n selection\n .classed('hover-disabled', false);\n\n selection\n .on(_pointerPrefix + 'over.hover', null)\n .on(_pointerPrefix + 'out.hover', null)\n .on(_pointerPrefix + 'down.hover', null);\n\n d3_select(window)\n .on(_pointerPrefix + 'up.hover pointercancel.hover', null, true)\n .on('keydown.hover', null)\n .on('keyup.hover', null);\n };\n\n\n behavior.altDisables = function(val) {\n if (!arguments.length) return _altDisables;\n _altDisables = val;\n return behavior;\n };\n\n behavior.ignoreVertex = function(val) {\n if (!arguments.length) return _ignoreVertex;\n _ignoreVertex = val;\n return behavior;\n };\n\n behavior.initialNodeID = function(nodeId) {\n _initialNodeID = nodeId;\n return behavior;\n };\n\n return utilRebind(behavior, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { behaviorEdit } from './edit';\nimport { behaviorHover } from './hover';\nimport { geoChooseEdge, geoVecLength } from '../geo';\nimport { utilFastMouse, utilKeybinding, utilRebind } from '../util';\n\nvar _disableSpace = false;\nvar _lastSpace = null;\n\n\nexport function behaviorDraw(context) {\n var dispatch = d3_dispatch(\n 'move', 'down', 'downcancel', 'click', 'clickWay', 'clickNode', 'undo', 'cancel', 'finish'\n );\n\n var keybinding = utilKeybinding('draw');\n\n var _hover = behaviorHover(context)\n .altDisables(true)\n .ignoreVertex(true)\n .on('hover', context.ui().sidebar.hover);\n var _edit = behaviorEdit(context);\n\n var _closeTolerance = 4;\n var _tolerance = 12;\n var _mouseLeave = false;\n var _lastMouse = null;\n var _lastPointerUpEvent;\n\n var _downPointer;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n // related code\n // - `mode/drag_node.js` `datum()`\n function datum(d3_event) {\n var mode = context.mode();\n var isNote = mode && (mode.id.indexOf('note') !== -1);\n if (d3_event.altKey || isNote) return {};\n\n var element;\n if (d3_event.type === 'keydown') {\n element = _lastMouse && _lastMouse.target;\n } else {\n element = d3_event.target;\n }\n\n // When drawing, snap only to touch targets..\n // (this excludes area fills and active drawing elements)\n var d = element.__data__;\n return (d && d.properties && d.properties.target) ? d : {};\n }\n\n function pointerdown(d3_event) {\n\n if (_downPointer) return;\n\n var pointerLocGetter = utilFastMouse(this);\n _downPointer = {\n id: d3_event.pointerId || 'mouse',\n pointerLocGetter: pointerLocGetter,\n downTime: +new Date(),\n downLoc: pointerLocGetter(d3_event)\n };\n\n dispatch.call('down', this, d3_event, datum(d3_event));\n }\n\n function pointerup(d3_event) {\n\n if (!_downPointer || _downPointer.id !== (d3_event.pointerId || 'mouse')) return;\n\n var downPointer = _downPointer;\n _downPointer = null;\n\n _lastPointerUpEvent = d3_event;\n\n if (downPointer.isCancelled) return;\n\n var t2 = +new Date();\n var p2 = downPointer.pointerLocGetter(d3_event);\n var dist = geoVecLength(downPointer.downLoc, p2);\n\n if (dist < _closeTolerance ||\n (dist < _tolerance && (t2 - downPointer.downTime) < 500)) {\n // Prevent a quick second click\n d3_select(window).on('click.draw-block', function() {\n d3_event.stopPropagation();\n }, true);\n\n context.map().dblclickZoomEnable(false);\n\n window.setTimeout(function() {\n context.map().dblclickZoomEnable(true);\n d3_select(window).on('click.draw-block', null);\n }, 500);\n\n click(d3_event, p2);\n }\n }\n\n function pointermove(d3_event) {\n if (_downPointer &&\n _downPointer.id === (d3_event.pointerId || 'mouse') &&\n !_downPointer.isCancelled) {\n var p2 = _downPointer.pointerLocGetter(d3_event);\n var dist = geoVecLength(_downPointer.downLoc, p2);\n if (dist >= _closeTolerance) {\n _downPointer.isCancelled = true;\n dispatch.call('downcancel', this);\n }\n }\n\n if ((d3_event.pointerType && d3_event.pointerType !== 'mouse') ||\n d3_event.buttons ||\n _downPointer) return;\n\n // HACK: Mobile Safari likes to send one or more `mouse` type pointermove\n // events immediately after non-mouse pointerup events; detect and ignore them.\n if (_lastPointerUpEvent &&\n _lastPointerUpEvent.pointerType !== 'mouse' &&\n d3_event.timeStamp - _lastPointerUpEvent.timeStamp < 100) return;\n\n _lastMouse = d3_event;\n dispatch.call('move', this, d3_event, datum(d3_event));\n }\n\n function pointercancel(d3_event) {\n if (_downPointer &&\n _downPointer.id === (d3_event.pointerId || 'mouse')) {\n\n if (!_downPointer.isCancelled) {\n dispatch.call('downcancel', this);\n }\n _downPointer = null;\n }\n }\n\n function mouseenter() {\n _mouseLeave = false;\n }\n\n function mouseleave() {\n _mouseLeave = true;\n }\n\n function allowsVertex(d) {\n return d.geometry(context.graph()) === 'vertex' || presetManager.allowsVertex(d, context.graph());\n }\n\n // related code\n // - `mode/drag_node.js` `doMove()`\n // - `behavior/draw.js` `click()`\n // - `behavior/draw_way.js` `move()`\n function click(d3_event, loc) {\n var d = datum(d3_event);\n var target = d && d.properties && d.properties.entity;\n\n var mode = context.mode();\n\n if (target && target.type === 'node' && allowsVertex(target)) { // Snap to a node\n dispatch.call('clickNode', this, target, d);\n return;\n\n } else if (target && target.type === 'way' && (mode.id !== 'add-point' || mode.preset.matchGeometry('vertex'))) { // Snap to a way\n var choice = geoChooseEdge(\n context.graph().childNodes(target), loc, context.projection, context.activeID()\n );\n if (choice) {\n var edge = [target.nodes[choice.index - 1], target.nodes[choice.index]];\n dispatch.call('clickWay', this, choice.loc, edge, d);\n return;\n }\n } else if (mode.id !== 'add-point' || mode.preset.matchGeometry('point')) {\n var locLatLng = context.projection.invert(loc);\n dispatch.call('click', this, locLatLng, d);\n }\n\n }\n\n // treat a spacebar press like a click\n function space(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n var currSpace = context.map().mouse();\n if (_disableSpace && _lastSpace) {\n var dist = geoVecLength(_lastSpace, currSpace);\n if (dist > _tolerance) {\n _disableSpace = false;\n }\n }\n\n if (_disableSpace || _mouseLeave || !_lastMouse) return;\n\n // user must move mouse or release space bar to allow another click\n _lastSpace = currSpace;\n _disableSpace = true;\n\n d3_select(window).on('keyup.space-block', function() {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n _disableSpace = false;\n d3_select(window).on('keyup.space-block', null);\n });\n\n // get the current mouse position\n var loc = context.map().mouse() ||\n // or the map center if the mouse has never entered the map\n context.projection(context.map().center());\n click(d3_event, loc);\n }\n\n\n function backspace(d3_event) {\n d3_event.preventDefault();\n dispatch.call('undo');\n }\n\n\n function del(d3_event) {\n d3_event.preventDefault();\n dispatch.call('cancel');\n }\n\n\n function ret(d3_event) {\n d3_event.preventDefault();\n dispatch.call('finish');\n }\n\n\n function behavior(selection) {\n context.install(_hover);\n context.install(_edit);\n\n _downPointer = null;\n\n keybinding\n .on('\u232B', backspace)\n .on('\u2326', del)\n .on('\u238B', ret)\n .on('\u21A9', ret)\n .on('space', space)\n .on('\u2325space', space);\n\n selection\n .on('mouseenter.draw', mouseenter)\n .on('mouseleave.draw', mouseleave)\n .on(_pointerPrefix + 'down.draw', pointerdown)\n .on(_pointerPrefix + 'move.draw', pointermove);\n\n d3_select(window)\n .on(_pointerPrefix + 'up.draw', pointerup, true)\n .on('pointercancel.draw', pointercancel, true);\n\n d3_select(document)\n .call(keybinding);\n\n return behavior;\n }\n\n\n behavior.off = function(selection) {\n context.ui().sidebar.hover.cancel();\n context.uninstall(_hover);\n context.uninstall(_edit);\n\n selection\n .on('mouseenter.draw', null)\n .on('mouseleave.draw', null)\n .on(_pointerPrefix + 'down.draw', null)\n .on(_pointerPrefix + 'move.draw', null);\n\n d3_select(window)\n .on(_pointerPrefix + 'up.draw', null)\n .on('pointercancel.draw', null);\n // note: keyup.space-block, click.draw-block should remain\n\n d3_select(document)\n .call(keybinding.unbind);\n };\n\n\n behavior.hover = function() {\n return _hover;\n };\n\n\n return utilRebind(behavior, dispatch, 'on');\n}\n", "import { deepEqual } from 'fast-equals';\n\nimport {\n interpolateNumber as d3_interpolateNumber,\n quantize as d3_quantize\n} from 'd3-interpolate';\n\nimport { select as d3_select } from 'd3-selection';\nimport { scaleQuantize as d3_scaleQuantize } from 'd3-scale';\nimport { timer as d3_timer } from 'd3-timer';\n\n\nexport function behaviorBreathe() {\n var duration = 800;\n var steps = 4;\n var selector = '.selected.shadow, .selected .shadow';\n var _selected = d3_select(null);\n var _classed = '';\n var _params = {};\n var _done = false;\n var _timer;\n\n\n function ratchetyInterpolator(a, b, steps, units) {\n a = Number(a);\n b = Number(b);\n var sample = d3_scaleQuantize()\n .domain([0, 1])\n .range(d3_quantize(d3_interpolateNumber(a, b), steps));\n\n return function(t) {\n return String(sample(t)) + (units || '');\n };\n }\n\n\n function reset(selection) {\n selection\n .style('stroke-opacity', null)\n .style('stroke-width', null)\n .style('fill-opacity', null)\n .style('r', null);\n }\n\n\n function setAnimationParams(transition, fromTo) {\n var toFrom = (fromTo === 'from' ? 'to' : 'from');\n\n transition\n .styleTween('stroke-opacity', function(d) {\n return ratchetyInterpolator(\n _params[d.id][toFrom].opacity,\n _params[d.id][fromTo].opacity,\n steps\n );\n })\n .styleTween('stroke-width', function(d) {\n return ratchetyInterpolator(\n _params[d.id][toFrom].width,\n _params[d.id][fromTo].width,\n steps,\n 'px'\n );\n })\n .styleTween('fill-opacity', function(d) {\n return ratchetyInterpolator(\n _params[d.id][toFrom].opacity,\n _params[d.id][fromTo].opacity,\n steps\n );\n })\n .styleTween('r', function(d) {\n return ratchetyInterpolator(\n _params[d.id][toFrom].width,\n _params[d.id][fromTo].width,\n steps,\n 'px'\n );\n });\n }\n\n\n function calcAnimationParams(selection) {\n selection\n .call(reset)\n .each(function(d) {\n var s = d3_select(this);\n var tag = s.node().tagName;\n var p = {'from': {}, 'to': {}};\n var opacity;\n var width;\n\n // determine base opacity and width\n if (tag === 'circle') {\n opacity = Number(s.style('fill-opacity') || 0.5);\n width = Number(s.style('r') || 15.5);\n } else {\n opacity = Number(s.style('stroke-opacity') || 0.7);\n width = Number(s.style('stroke-width') || 10);\n }\n\n // calculate from/to interpolation params..\n p.tag = tag;\n p.from.opacity = opacity * 0.6;\n p.to.opacity = opacity * 1.25;\n p.from.width = width * 0.7;\n p.to.width = width * (tag === 'circle' ? 1.5 : 1);\n _params[d.id] = p;\n });\n }\n\n\n function run(surface, fromTo) {\n var toFrom = (fromTo === 'from' ? 'to' : 'from');\n var currSelected = surface.selectAll(selector);\n var currClassed = surface.attr('class');\n\n if (_done || currSelected.empty()) {\n _selected.call(reset);\n _selected = d3_select(null);\n return;\n }\n\n if (!deepEqual(currSelected.data(), _selected.data()) || currClassed !== _classed) {\n _selected.call(reset);\n _classed = currClassed;\n _selected = currSelected.call(calcAnimationParams);\n }\n\n var didCallNextRun = false;\n\n _selected\n .transition()\n .duration(duration)\n .call(setAnimationParams, fromTo)\n .on('end', function() {\n // `end` event is called for each selected element, but we want\n // it to run only once\n if (!didCallNextRun) {\n surface.call(run, toFrom);\n didCallNextRun = true;\n }\n\n // if entity was deselected, remove breathe styling\n if (!d3_select(this).classed('selected')) {\n reset(d3_select(this));\n }\n });\n }\n\n function behavior(surface) {\n _done = false;\n _timer = d3_timer(function() {\n // wait for elements to actually become selected\n if (surface.selectAll(selector).empty()) {\n return false;\n }\n\n surface.call(run, 'from');\n _timer.stop();\n return true;\n }, 20);\n }\n\n behavior.restartIfNeeded = function(surface) {\n if (_selected.empty()) {\n surface.call(run, 'from');\n if (_timer) {\n _timer.stop();\n }\n }\n };\n\n behavior.off = function() {\n _done = true;\n if (_timer) {\n _timer.stop();\n }\n _selected\n .interrupt()\n .call(reset);\n };\n\n\n return behavior;\n}\n", "{\n \"name\": \"@openhistoricalmap/id\",\n \"version\": \"2.39.5\",\n \"description\": \"The OpenHistoricalMap fork of a friendly editor for OpenStreetMap\",\n \"main\": \"dist/iD.min.js\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/OpenHistoricalMap/iD.git\"\n },\n \"homepage\": \"https://github.com/OpenHistoricalMap/iD\",\n \"bugs\": \"https://github.com/OpenHistoricalMap/issues/issues\",\n \"keywords\": [\n \"editor\",\n \"openhistoricalmap\"\n ],\n \"license\": \"ISC\",\n \"type\": \"module\",\n \"scripts\": {\n \"all\": \"run-s clean build dist\",\n \"build\": \"run-s build:css build:data build:js\",\n \"build:css\": \"node scripts/build_css.js\",\n \"build:data\": \"shx mkdir -p dist/data && node scripts/build_data.js\",\n \"build:stats\": \"node config/esbuild.config.min.js --stats && esbuild-visualizer --metadata dist/esbuild.json --filename docs/statistics.html && shx rm dist/esbuild.json\",\n \"build:js\": \"node config/esbuild.config.js\",\n \"build:js:watch\": \"node config/esbuild.config.js --watch\",\n \"clean\": \"shx rm -f dist/esbuild.json dist/*.js dist/*.map dist/*.css dist/img/*.svg\",\n \"dist\": \"run-p dist:**\",\n \"dist:mapillary\": \"shx mkdir -p dist/mapillary-js && shx cp -R node_modules/mapillary-js/dist/* dist/mapillary-js/\",\n \"dist:pannellum\": \"shx mkdir -p dist/pannellum && shx cp -R node_modules/pannellum/build/* dist/pannellum/\",\n \"dist:min\": \"node config/esbuild.config.min.js\",\n \"dist:svg:iD\": \"svg-sprite --symbol --symbol-dest . --shape-id-generator \\\"iD-%s\\\" --symbol-sprite dist/img/iD-sprite.svg \\\"svg/iD-sprite/**/*.svg\\\"\",\n \"dist:svg:community\": \"svg-sprite --symbol --symbol-dest . --shape-id-generator \\\"community-%s\\\" --symbol-sprite dist/img/community-sprite.svg node_modules/osm-community-index/dist/img/*.svg\",\n \"dist:svg:fa\": \"svg-sprite --symbol --symbol-dest . --symbol-sprite dist/img/fa-sprite.svg svg/fontawesome/*.svg\",\n \"dist:svg:maki\": \"svg-sprite --symbol --symbol-dest . --shape-id-generator \\\"maki-%s\\\" --symbol-sprite dist/img/maki-sprite.svg node_modules/@mapbox/maki/icons/*.svg\",\n \"dist:svg:mapillary:signs\": \"svg-sprite --symbol --symbol-dest . --symbol-sprite dist/img/mapillary-sprite.svg node_modules/@rapideditor/mapillary_sprite_source/package_signs/*.svg\",\n \"dist:svg:mapillary:objects\": \"svg-sprite --symbol --symbol-dest . --symbol-sprite dist/img/mapillary-object-sprite.svg node_modules/@rapideditor/mapillary_sprite_source/package_objects/*.svg\",\n \"dist:svg:roentgen\": \"svg-sprite --shape-id-generator \\\"roentgen-%s\\\" --shape-dim-width 16 --shape-dim-height 16 --symbol --symbol-dest . --symbol-sprite dist/img/roentgen-sprite.svg node_modules/@enzet/roentgen/icons/*.svg\",\n \"dist:svg:temaki\": \"svg-sprite --symbol --symbol-dest . --shape-id-generator \\\"temaki-%s\\\" --symbol-sprite dist/img/temaki-sprite.svg node_modules/@rapideditor/temaki/icons/*.svg\",\n \"imagery\": \"node scripts/update_imagery.js\",\n \"lint\": \"eslint config scripts test/spec modules\",\n \"lint:fix\": \"eslint scripts test/spec modules --fix\",\n \"start\": \"run-s start:watch\",\n \"start:single-build\": \"run-p build:js start:server\",\n \"start:watch\": \"run-p build:js:watch start:server\",\n \"start:server\": \"node scripts/server.js\",\n \"test\": \"npm-run-all -s lint build test:typecheck test:spec\",\n \"test:typecheck\": \"tsc\",\n \"test:spec\": \"vitest --no-isolate\",\n \"test:once\": \"vitest run --no-isolate\",\n \"translations\": \"node scripts/update_locales.js\"\n },\n \"dependencies\": {\n \"@esri/wayback-core\": \"^1.0.10\",\n \"@mapbox/geojson-area\": \"^0.2.2\",\n \"@mapbox/sexagesimal\": \"1.2.0\",\n \"@mapbox/vector-tile\": \"^2.0.4\",\n \"@rapideditor/country-coder\": \"~5.6.0\",\n \"@rapideditor/location-conflation\": \"~2.0.0\",\n \"@tmcw/togeojson\": \"^7.1.2\",\n \"@turf/bbox\": \"^7.2.0\",\n \"@turf/bbox-clip\": \"^7.2.0\",\n \"abortcontroller-polyfill\": \"^1.7.8\",\n \"aes-js\": \"^3.1.2\",\n \"alif-toolkit\": \"^1.3.0\",\n \"core-js-bundle\": \"^3.46.0\",\n \"diacritics\": \"1.3.0\",\n \"edtf\": \"^4.7.1\",\n \"es-toolkit\": \"^1.45.0\",\n \"exifr\": \"^7.1.3\",\n \"fast-deep-equal\": \"~3.1.3\",\n \"fast-equals\": \"^6.0.0\",\n \"fast-json-stable-stringify\": \"2.1.0\",\n \"idb-keyval\": \"^6.2.2\",\n \"marked\": \"~17.0.0\",\n \"node-diff3\": \"~3.2.0\",\n \"osm-auth\": \"^3.1.1\",\n \"pannellum\": \"2.5.7\",\n \"pbf\": \"^4.0.1\",\n \"polygon-clipping\": \"~0.15.7\",\n \"rbush\": \"4.0.1\",\n \"whatwg-fetch\": \"^3.6.20\",\n \"which-polygon\": \"2.2.1\"\n },\n \"devDependencies\": {\n \"@actions/github-script\": \"github:actions/github-script#v8.0.0\",\n \"@enzet/roentgen\": \"^0.14.0\",\n \"@eslint/js\": \"^10.0.1\",\n \"@fortawesome/fontawesome-svg-core\": \"^7.2.0\",\n \"@fortawesome/free-brands-svg-icons\": \"^7.2.0\",\n \"@fortawesome/free-regular-svg-icons\": \"^7.2.0\",\n \"@fortawesome/free-solid-svg-icons\": \"^7.2.0\",\n \"@mapbox/maki\": \"^8.2.0\",\n \"@openstreetmap/id-tagging-schema\": \"^6.13.4\",\n \"@rapideditor/mapillary_sprite_source\": \"^1.8.0\",\n \"@rapideditor/temaki\": \"^5.11.0\",\n \"@transifex/api\": \"^7.1.5\",\n \"@types/aes-js\": \"^3.1.4\",\n \"@types/chai\": \"^5.2.3\",\n \"@types/d3\": \"^7.4.3\",\n \"@types/diacritics\": \"^1.3.3\",\n \"@types/geojson\": \"^7946.0.16\",\n \"@types/happen\": \"^0.3.0\",\n \"@types/mapbox__geojson-area\": \"^0.2.6\",\n \"@types/node\": \"^25.0.2\",\n \"@types/sinon\": \"^21.0.0\",\n \"@types/sinon-chai\": \"^4.0.0\",\n \"@types/which-polygon\": \"^2.2.5\",\n \"autoprefixer\": \"^10.4.21\",\n \"browserslist\": \"^4.27.0\",\n \"browserslist-to-esbuild\": \"^2.1.1\",\n \"chai\": \"^6.2.0\",\n \"cldr-core\": \"^48.0.0\",\n \"cldr-localenames-full\": \"^48.0.0\",\n \"concat-files\": \"^0.1.1\",\n \"d3\": \"~7.9.0\",\n \"dotenv\": \"^17.2.3\",\n \"editor-layer-index\": \"github:osmlab/editor-layer-index#gh-pages\",\n \"esbuild\": \"^0.27.0\",\n \"esbuild-visualizer\": \"^0.7.0\",\n \"eslint\": \"^10.0.0\",\n \"fake-indexeddb\": \"^6.2.5\",\n \"fetch-mock\": \"^11.1.1\",\n \"gaze\": \"^1.1.3\",\n \"globals\": \"^17.3.0\",\n \"happen\": \"^0.3.2\",\n \"js-yaml\": \"^4.1.1\",\n \"jsdom\": \"^29.0.0\",\n \"json-stringify-pretty-compact\": \"^3.0.0\",\n \"mapillary-js\": \"4.1.2\",\n \"name-suggestion-index\": \"~7.0\",\n \"netlify-cli\": \"^24.0.0\",\n \"nise\": \"^6.1.1\",\n \"npm-run-all\": \"^4.0.0\",\n \"ohm-editor-layer-index\": \"github:openhistoricalmap/ohm-editor-layer-index#dist\",\n \"osm-community-index\": \"6.0.0\",\n \"postcss\": \"^8.5.6\",\n \"postcss-prefix-selector\": \"^2.1.1\",\n \"serve-handler\": \"^6.1.6\",\n \"shelljs\": \"^0.10.0\",\n \"shx\": \"^0.4.0\",\n \"sinon\": \"^21.0.0\",\n \"sinon-chai\": \"^4.0.1\",\n \"svg-sprite\": \"2.0.4\",\n \"typescript\": \"^5.9.3\",\n \"typescript-eslint\": \"^8.56.0\",\n \"vitest\": \"^4.1.2\"\n },\n \"engines\": {\n \"node\": \">=22\"\n },\n \"browserslist\": [\n \"> 0.3%, last 6 major versions, not dead, Firefox ESR, maintained node versions\"\n ]\n}\n", "import { deepEqual } from 'fast-equals';\n\nimport { geoVecEqual } from '../geo';\nimport { utilArrayDifference, utilArrayUnion, utilArrayUniq } from '../util/array';\n\n\n/*\n iD.coreDifference represents the difference between two graphs.\n It knows how to calculate the set of entities that were\n created, modified, or deleted, and also contains the logic\n for recursively extending a difference to the complete set\n of entities that will require a redraw, taking into account\n child and parent relationships.\n */\nexport function coreDifference(base, head) {\n var _changes = {};\n var _didChange = {}; // 'addition', 'deletion', 'geometry', 'properties'\n var _diff = {};\n\n function checkEntityID(id) {\n var h = head.entities[id];\n var b = base.entities[id];\n\n if (h === b) return;\n if (_changes[id]) return;\n\n if (!h && b) {\n _changes[id] = { base: b, head: h };\n _didChange.deletion = true;\n return;\n }\n if (h && !b) {\n _changes[id] = { base: b, head: h };\n _didChange.addition = true;\n return;\n }\n\n if (h && b) {\n if (h.members && b.members && !deepEqual(h.members, b.members)) {\n _changes[id] = { base: b, head: h };\n _didChange.geometry = true;\n _didChange.properties = true;\n return;\n }\n if (h.loc && b.loc && !geoVecEqual(h.loc, b.loc)) {\n _changes[id] = { base: b, head: h };\n _didChange.geometry = true;\n }\n if (h.nodes && b.nodes && !deepEqual(h.nodes, b.nodes)) {\n _changes[id] = { base: b, head: h };\n _didChange.geometry = true;\n }\n if (h.tags && b.tags && !deepEqual(h.tags, b.tags)) {\n _changes[id] = { base: b, head: h };\n _didChange.properties = true;\n }\n }\n }\n\n function load() {\n // HOT CODE: there can be many thousands of downloaded entities, so looping\n // through them all can become a performance bottleneck. Optimize by\n // resolving duplicates and using a basic `for` loop\n var ids = utilArrayUniq(Object.keys(head.entities).concat(Object.keys(base.entities)));\n for (var i = 0; i < ids.length; i++) {\n checkEntityID(ids[i]);\n }\n }\n load();\n\n\n _diff.length = function length() {\n return Object.keys(_changes).length;\n };\n\n\n _diff.changes = function changes() {\n return _changes;\n };\n\n _diff.didChange = _didChange;\n\n\n // pass true to include affected relation members\n _diff.extantIDs = function extantIDs(includeRelMembers) {\n var result = new Set();\n Object.keys(_changes).forEach(function(id) {\n if (_changes[id].head) {\n result.add(id);\n }\n\n var h = _changes[id].head;\n var b = _changes[id].base;\n var entity = h || b;\n\n if (includeRelMembers && entity.type === 'relation') {\n var mh = h ? h.members.map(function(m) { return m.id; }) : [];\n var mb = b ? b.members.map(function(m) { return m.id; }) : [];\n utilArrayUnion(mh, mb).forEach(function(memberID) {\n if (head.hasEntity(memberID)) {\n result.add(memberID);\n }\n });\n }\n });\n\n return Array.from(result);\n };\n\n\n _diff.modified = function modified() {\n var result = [];\n Object.values(_changes).forEach(function(change) {\n if (change.base && change.head) {\n result.push(change.head);\n }\n });\n return result;\n };\n\n\n _diff.created = function created() {\n var result = [];\n Object.values(_changes).forEach(function(change) {\n if (!change.base && change.head) {\n result.push(change.head);\n }\n });\n return result;\n };\n\n\n _diff.deleted = function deleted() {\n var result = [];\n Object.values(_changes).forEach(function(change) {\n if (change.base && !change.head) {\n result.push(change.base);\n }\n });\n return result;\n };\n\n\n _diff.summary = function summary() {\n var relevant = {};\n\n var keys = Object.keys(_changes);\n for (var i = 0; i < keys.length; i++) {\n var change = _changes[keys[i]];\n\n if (change.head && change.head.geometry(head) !== 'vertex') {\n addEntity(change.head, head, change.base ? 'modified' : 'created');\n\n } else if (change.base && change.base.geometry(base) !== 'vertex') {\n addEntity(change.base, base, 'deleted');\n\n } else if (change.base && change.head) { // modified vertex\n var moved = !deepEqual(change.base.loc, change.head.loc);\n var retagged = !deepEqual(change.base.tags, change.head.tags);\n\n if (moved) {\n addParents(change.head);\n }\n\n if (retagged || (moved && change.head.hasInterestingTags())) {\n addEntity(change.head, head, 'modified');\n }\n\n } else if (change.head && change.head.hasInterestingTags()) { // created vertex\n addEntity(change.head, head, 'created');\n\n } else if (change.base && change.base.hasInterestingTags()) { // deleted vertex\n addEntity(change.base, base, 'deleted');\n }\n }\n\n return Object.values(relevant);\n\n\n function addEntity(entity, graph, changeType) {\n relevant[entity.id] = {\n entity: entity,\n graph: graph,\n changeType: changeType\n };\n }\n\n function addParents(entity) {\n var parents = head.parentWays(entity);\n for (var j = parents.length - 1; j >= 0; j--) {\n var parent = parents[j];\n if (!(parent.id in relevant)) {\n addEntity(parent, head, 'modified');\n }\n }\n }\n };\n\n\n // returns complete set of entities that require a redraw\n // (optionally within given `extent`)\n _diff.complete = function complete(extent) {\n var result = {};\n var id, change;\n\n for (id in _changes) {\n change = _changes[id];\n\n var h = change.head;\n var b = change.base;\n var entity = h || b;\n var i;\n\n if (extent &&\n (!h || !h.intersects(extent, head)) &&\n (!b || !b.intersects(extent, base))) {\n continue;\n }\n\n result[id] = h;\n\n if (entity.type === 'way') {\n var nh = h ? h.nodes : [];\n var nb = b ? b.nodes : [];\n var diff;\n\n diff = utilArrayDifference(nh, nb);\n for (i = 0; i < diff.length; i++) {\n result[diff[i]] = head.hasEntity(diff[i]);\n }\n\n diff = utilArrayDifference(nb, nh);\n for (i = 0; i < diff.length; i++) {\n result[diff[i]] = head.hasEntity(diff[i]);\n }\n }\n\n if (entity.type === 'relation' && entity.isMultipolygon()) {\n var mh = h ? h.members.map(function(m) { return m.id; }) : [];\n var mb = b ? b.members.map(function(m) { return m.id; }) : [];\n var ids = utilArrayUnion(mh, mb);\n for (i = 0; i < ids.length; i++) {\n var member = head.hasEntity(ids[i]);\n if (!member) continue; // not downloaded\n if (extent && !member.intersects(extent, head)) continue; // not visible\n result[ids[i]] = member;\n }\n }\n\n addParents(head.parentWays(entity), result);\n addParents(head.parentRelations(entity), result);\n }\n\n return result;\n\n\n function addParents(parents, result) {\n for (var i = 0; i < parents.length; i++) {\n var parent = parents[i];\n if (parent.id in result) continue;\n\n result[parent.id] = parent;\n addParents(head.parentRelations(parent), result);\n }\n }\n };\n\n\n return _diff;\n}\n", "\n/**\n * Rearranges items so that all items in the [left, k] are the smallest.\n * The k-th element will have the (k - left + 1)-th smallest value in [left, right].\n *\n * @template T\n * @param {T[]} arr the array to partially sort (in place)\n * @param {number} k middle index for partial sorting (as defined above)\n * @param {number} [left=0] left index of the range to sort\n * @param {number} [right=arr.length-1] right index\n * @param {(a: T, b: T) => number} [compare = (a, b) => a - b] compare function\n */\nexport default function quickselect(arr, k, left = 0, right = arr.length - 1, compare = defaultCompare) {\n\n while (right > left) {\n if (right - left > 600) {\n const n = right - left + 1;\n const m = k - left + 1;\n const z = Math.log(n);\n const s = 0.5 * Math.exp(2 * z / 3);\n const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);\n const newLeft = Math.max(left, Math.floor(k - m * s / n + sd));\n const newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));\n quickselect(arr, k, newLeft, newRight, compare);\n }\n\n const t = arr[k];\n let i = left;\n /** @type {number} */\n let j = right;\n\n swap(arr, left, k);\n if (compare(arr[right], t) > 0) swap(arr, left, right);\n\n while (i < j) {\n swap(arr, i, j);\n i++;\n j--;\n while (compare(arr[i], t) < 0) i++;\n while (compare(arr[j], t) > 0) j--;\n }\n\n if (compare(arr[left], t) === 0) swap(arr, left, j);\n else {\n j++;\n swap(arr, j, right);\n }\n\n if (j <= k) left = j + 1;\n if (k <= j) right = j - 1;\n }\n}\n\n/**\n * @template T\n * @param {T[]} arr\n * @param {number} i\n * @param {number} j\n */\nfunction swap(arr, i, j) {\n const tmp = arr[i];\n arr[i] = arr[j];\n arr[j] = tmp;\n}\n\n/**\n * @template T\n * @param {T} a\n * @param {T} b\n * @returns {number}\n */\nfunction defaultCompare(a, b) {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n", "import quickselect from 'quickselect';\n\nexport default class RBush {\n constructor(maxEntries = 9) {\n // max entries in a node is 9 by default; min node fill is 40% for best performance\n this._maxEntries = Math.max(4, maxEntries);\n this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));\n this.clear();\n }\n\n all() {\n return this._all(this.data, []);\n }\n\n search(bbox) {\n let node = this.data;\n const result = [];\n\n if (!intersects(bbox, node)) return result;\n\n const toBBox = this.toBBox;\n const nodesToSearch = [];\n\n while (node) {\n for (let i = 0; i < node.children.length; i++) {\n const child = node.children[i];\n const childBBox = node.leaf ? toBBox(child) : child;\n\n if (intersects(bbox, childBBox)) {\n if (node.leaf) result.push(child);\n else if (contains(bbox, childBBox)) this._all(child, result);\n else nodesToSearch.push(child);\n }\n }\n node = nodesToSearch.pop();\n }\n\n return result;\n }\n\n collides(bbox) {\n let node = this.data;\n\n if (!intersects(bbox, node)) return false;\n\n const nodesToSearch = [];\n while (node) {\n for (let i = 0; i < node.children.length; i++) {\n const child = node.children[i];\n const childBBox = node.leaf ? this.toBBox(child) : child;\n\n if (intersects(bbox, childBBox)) {\n if (node.leaf || contains(bbox, childBBox)) return true;\n nodesToSearch.push(child);\n }\n }\n node = nodesToSearch.pop();\n }\n\n return false;\n }\n\n load(data) {\n if (!(data && data.length)) return this;\n\n if (data.length < this._minEntries) {\n for (let i = 0; i < data.length; i++) {\n this.insert(data[i]);\n }\n return this;\n }\n\n // recursively build the tree with the given data from scratch using OMT algorithm\n let node = this._build(data.slice(), 0, data.length - 1, 0);\n\n if (!this.data.children.length) {\n // save as is if tree is empty\n this.data = node;\n\n } else if (this.data.height === node.height) {\n // split root if trees have the same height\n this._splitRoot(this.data, node);\n\n } else {\n if (this.data.height < node.height) {\n // swap trees if inserted one is bigger\n const tmpNode = this.data;\n this.data = node;\n node = tmpNode;\n }\n\n // insert the small tree into the large tree at appropriate level\n this._insert(node, this.data.height - node.height - 1, true);\n }\n\n return this;\n }\n\n insert(item) {\n if (item) this._insert(item, this.data.height - 1);\n return this;\n }\n\n clear() {\n this.data = createNode([]);\n return this;\n }\n\n remove(item, equalsFn) {\n if (!item) return this;\n\n let node = this.data;\n const bbox = this.toBBox(item);\n const path = [];\n const indexes = [];\n let i, parent, goingUp;\n\n // depth-first iterative tree traversal\n while (node || path.length) {\n\n if (!node) { // go up\n node = path.pop();\n parent = path[path.length - 1];\n i = indexes.pop();\n goingUp = true;\n }\n\n if (node.leaf) { // check current node\n const index = findItem(item, node.children, equalsFn);\n\n if (index !== -1) {\n // item found, remove the item and condense tree upwards\n node.children.splice(index, 1);\n path.push(node);\n this._condense(path);\n return this;\n }\n }\n\n if (!goingUp && !node.leaf && contains(node, bbox)) { // go down\n path.push(node);\n indexes.push(i);\n i = 0;\n parent = node;\n node = node.children[0];\n\n } else if (parent) { // go right\n i++;\n node = parent.children[i];\n goingUp = false;\n\n } else node = null; // nothing found\n }\n\n return this;\n }\n\n toBBox(item) { return item; }\n\n compareMinX(a, b) { return a.minX - b.minX; }\n compareMinY(a, b) { return a.minY - b.minY; }\n\n toJSON() { return this.data; }\n\n fromJSON(data) {\n this.data = data;\n return this;\n }\n\n _all(node, result) {\n const nodesToSearch = [];\n while (node) {\n if (node.leaf) result.push(...node.children);\n else nodesToSearch.push(...node.children);\n\n node = nodesToSearch.pop();\n }\n return result;\n }\n\n _build(items, left, right, height) {\n\n const N = right - left + 1;\n let M = this._maxEntries;\n let node;\n\n if (N <= M) {\n // reached leaf level; return leaf\n node = createNode(items.slice(left, right + 1));\n calcBBox(node, this.toBBox);\n return node;\n }\n\n if (!height) {\n // target height of the bulk-loaded tree\n height = Math.ceil(Math.log(N) / Math.log(M));\n\n // target number of root entries to maximize storage utilization\n M = Math.ceil(N / Math.pow(M, height - 1));\n }\n\n node = createNode([]);\n node.leaf = false;\n node.height = height;\n\n // split the items into M mostly square tiles\n\n const N2 = Math.ceil(N / M);\n const N1 = N2 * Math.ceil(Math.sqrt(M));\n\n multiSelect(items, left, right, N1, this.compareMinX);\n\n for (let i = left; i <= right; i += N1) {\n\n const right2 = Math.min(i + N1 - 1, right);\n\n multiSelect(items, i, right2, N2, this.compareMinY);\n\n for (let j = i; j <= right2; j += N2) {\n\n const right3 = Math.min(j + N2 - 1, right2);\n\n // pack each entry recursively\n node.children.push(this._build(items, j, right3, height - 1));\n }\n }\n\n calcBBox(node, this.toBBox);\n\n return node;\n }\n\n _chooseSubtree(bbox, node, level, path) {\n while (true) {\n path.push(node);\n\n if (node.leaf || path.length - 1 === level) break;\n\n let minArea = Infinity;\n let minEnlargement = Infinity;\n let targetNode;\n\n for (let i = 0; i < node.children.length; i++) {\n const child = node.children[i];\n const area = bboxArea(child);\n const enlargement = enlargedArea(bbox, child) - area;\n\n // choose entry with the least area enlargement\n if (enlargement < minEnlargement) {\n minEnlargement = enlargement;\n minArea = area < minArea ? area : minArea;\n targetNode = child;\n\n } else if (enlargement === minEnlargement) {\n // otherwise choose one with the smallest area\n if (area < minArea) {\n minArea = area;\n targetNode = child;\n }\n }\n }\n\n node = targetNode || node.children[0];\n }\n\n return node;\n }\n\n _insert(item, level, isNode) {\n const bbox = isNode ? item : this.toBBox(item);\n const insertPath = [];\n\n // find the best node for accommodating the item, saving all nodes along the path too\n const node = this._chooseSubtree(bbox, this.data, level, insertPath);\n\n // put the item into the node\n node.children.push(item);\n extend(node, bbox);\n\n // split on node overflow; propagate upwards if necessary\n while (level >= 0) {\n if (insertPath[level].children.length > this._maxEntries) {\n this._split(insertPath, level);\n level--;\n } else break;\n }\n\n // adjust bboxes along the insertion path\n this._adjustParentBBoxes(bbox, insertPath, level);\n }\n\n // split overflowed node into two\n _split(insertPath, level) {\n const node = insertPath[level];\n const M = node.children.length;\n const m = this._minEntries;\n\n this._chooseSplitAxis(node, m, M);\n\n const splitIndex = this._chooseSplitIndex(node, m, M);\n\n const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));\n newNode.height = node.height;\n newNode.leaf = node.leaf;\n\n calcBBox(node, this.toBBox);\n calcBBox(newNode, this.toBBox);\n\n if (level) insertPath[level - 1].children.push(newNode);\n else this._splitRoot(node, newNode);\n }\n\n _splitRoot(node, newNode) {\n // split root node\n this.data = createNode([node, newNode]);\n this.data.height = node.height + 1;\n this.data.leaf = false;\n calcBBox(this.data, this.toBBox);\n }\n\n _chooseSplitIndex(node, m, M) {\n let index;\n let minOverlap = Infinity;\n let minArea = Infinity;\n\n for (let i = m; i <= M - m; i++) {\n const bbox1 = distBBox(node, 0, i, this.toBBox);\n const bbox2 = distBBox(node, i, M, this.toBBox);\n\n const overlap = intersectionArea(bbox1, bbox2);\n const area = bboxArea(bbox1) + bboxArea(bbox2);\n\n // choose distribution with minimum overlap\n if (overlap < minOverlap) {\n minOverlap = overlap;\n index = i;\n\n minArea = area < minArea ? area : minArea;\n\n } else if (overlap === minOverlap) {\n // otherwise choose distribution with minimum area\n if (area < minArea) {\n minArea = area;\n index = i;\n }\n }\n }\n\n return index || M - m;\n }\n\n // sorts node children by the best axis for split\n _chooseSplitAxis(node, m, M) {\n const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;\n const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;\n const xMargin = this._allDistMargin(node, m, M, compareMinX);\n const yMargin = this._allDistMargin(node, m, M, compareMinY);\n\n // if total distributions margin value is minimal for x, sort by minX,\n // otherwise it's already sorted by minY\n if (xMargin < yMargin) node.children.sort(compareMinX);\n }\n\n // total margin of all possible split distributions where each node is at least m full\n _allDistMargin(node, m, M, compare) {\n node.children.sort(compare);\n\n const toBBox = this.toBBox;\n const leftBBox = distBBox(node, 0, m, toBBox);\n const rightBBox = distBBox(node, M - m, M, toBBox);\n let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);\n\n for (let i = m; i < M - m; i++) {\n const child = node.children[i];\n extend(leftBBox, node.leaf ? toBBox(child) : child);\n margin += bboxMargin(leftBBox);\n }\n\n for (let i = M - m - 1; i >= m; i--) {\n const child = node.children[i];\n extend(rightBBox, node.leaf ? toBBox(child) : child);\n margin += bboxMargin(rightBBox);\n }\n\n return margin;\n }\n\n _adjustParentBBoxes(bbox, path, level) {\n // adjust bboxes along the given tree path\n for (let i = level; i >= 0; i--) {\n extend(path[i], bbox);\n }\n }\n\n _condense(path) {\n // go through the path, removing empty nodes and updating bboxes\n for (let i = path.length - 1, siblings; i >= 0; i--) {\n if (path[i].children.length === 0) {\n if (i > 0) {\n siblings = path[i - 1].children;\n siblings.splice(siblings.indexOf(path[i]), 1);\n\n } else this.clear();\n\n } else calcBBox(path[i], this.toBBox);\n }\n }\n}\n\nfunction findItem(item, items, equalsFn) {\n if (!equalsFn) return items.indexOf(item);\n\n for (let i = 0; i < items.length; i++) {\n if (equalsFn(item, items[i])) return i;\n }\n return -1;\n}\n\n// calculate node's bbox from bboxes of its children\nfunction calcBBox(node, toBBox) {\n distBBox(node, 0, node.children.length, toBBox, node);\n}\n\n// min bounding rectangle of node children from k to p-1\nfunction distBBox(node, k, p, toBBox, destNode) {\n if (!destNode) destNode = createNode(null);\n destNode.minX = Infinity;\n destNode.minY = Infinity;\n destNode.maxX = -Infinity;\n destNode.maxY = -Infinity;\n\n for (let i = k; i < p; i++) {\n const child = node.children[i];\n extend(destNode, node.leaf ? toBBox(child) : child);\n }\n\n return destNode;\n}\n\nfunction extend(a, b) {\n a.minX = Math.min(a.minX, b.minX);\n a.minY = Math.min(a.minY, b.minY);\n a.maxX = Math.max(a.maxX, b.maxX);\n a.maxY = Math.max(a.maxY, b.maxY);\n return a;\n}\n\nfunction compareNodeMinX(a, b) { return a.minX - b.minX; }\nfunction compareNodeMinY(a, b) { return a.minY - b.minY; }\n\nfunction bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }\nfunction bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }\n\nfunction enlargedArea(a, b) {\n return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *\n (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));\n}\n\nfunction intersectionArea(a, b) {\n const minX = Math.max(a.minX, b.minX);\n const minY = Math.max(a.minY, b.minY);\n const maxX = Math.min(a.maxX, b.maxX);\n const maxY = Math.min(a.maxY, b.maxY);\n\n return Math.max(0, maxX - minX) *\n Math.max(0, maxY - minY);\n}\n\nfunction contains(a, b) {\n return a.minX <= b.minX &&\n a.minY <= b.minY &&\n b.maxX <= a.maxX &&\n b.maxY <= a.maxY;\n}\n\nfunction intersects(a, b) {\n return b.minX <= a.maxX &&\n b.minY <= a.maxY &&\n b.maxX >= a.minX &&\n b.maxY >= a.minY;\n}\n\nfunction createNode(children) {\n return {\n children,\n height: 1,\n leaf: true,\n minX: Infinity,\n minY: Infinity,\n maxX: -Infinity,\n maxY: -Infinity\n };\n}\n\n// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;\n// combines selection algorithm with binary divide & conquer approach\n\nfunction multiSelect(arr, left, right, n, compare) {\n const stack = [left, right];\n\n while (stack.length) {\n right = stack.pop();\n left = stack.pop();\n\n if (right - left <= n) continue;\n\n const mid = left + Math.ceil((right - left) / n / 2) * n;\n quickselect(arr, mid, left, right, compare);\n\n stack.push(left, mid, mid, right);\n }\n}\n", "import RBush from 'rbush';\n\nimport { coreDifference } from './difference';\n\n\nexport function coreTree(head) {\n // tree for entities\n var _rtree = new RBush();\n var _bboxes = {};\n\n // maintain a separate tree for granular way segments\n var _segmentsRTree = new RBush();\n var _segmentsBBoxes = {};\n var _segmentsByWayId = {};\n\n var tree = {};\n\n\n function entityBBox(entity) {\n var bbox = entity.extent(head).bbox();\n bbox.id = entity.id;\n _bboxes[entity.id] = bbox;\n return bbox;\n }\n\n\n function segmentBBox(segment) {\n var extent = segment.extent(head);\n // extent can be null if the node entities aren't in the graph for some reason\n if (!extent) return null;\n\n var bbox = extent.bbox();\n bbox.segment = segment;\n _segmentsBBoxes[segment.id] = bbox;\n return bbox;\n }\n\n\n function removeEntity(entity) {\n _rtree.remove(_bboxes[entity.id]);\n delete _bboxes[entity.id];\n\n if (_segmentsByWayId[entity.id]) {\n _segmentsByWayId[entity.id].forEach(function(segment) {\n _segmentsRTree.remove(_segmentsBBoxes[segment.id]);\n delete _segmentsBBoxes[segment.id];\n });\n delete _segmentsByWayId[entity.id];\n }\n }\n\n\n function loadEntities(entities) {\n _rtree.load(entities.map(entityBBox));\n\n var segments = [];\n entities.forEach(function(entity) {\n if (entity.segments) {\n var entitySegments = entity.segments(head);\n // cache these to make them easy to remove later\n _segmentsByWayId[entity.id] = entitySegments;\n segments = segments.concat(entitySegments);\n }\n });\n if (segments.length) _segmentsRTree.load(segments.map(segmentBBox).filter(Boolean));\n }\n\n\n function updateParents(entity, insertions, memo) {\n head.parentWays(entity).forEach(function(way) {\n if (_bboxes[way.id]) {\n removeEntity(way);\n insertions[way.id] = way;\n }\n updateParents(way, insertions, memo);\n });\n\n head.parentRelations(entity).forEach(function(relation) {\n if (memo[relation.id]) return;\n memo[relation.id] = true;\n if (_bboxes[relation.id]) {\n removeEntity(relation);\n insertions[relation.id] = relation;\n }\n updateParents(relation, insertions, memo);\n });\n }\n\n\n tree.rebase = function(entities, force) {\n var insertions = {};\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n if (!entity.visible) continue;\n\n if (head.entities.hasOwnProperty(entity.id) || _bboxes[entity.id]) {\n if (!force) {\n continue;\n } else if (_bboxes[entity.id]) {\n removeEntity(entity);\n }\n }\n\n insertions[entity.id] = entity;\n updateParents(entity, insertions, {});\n }\n\n loadEntities(Object.values(insertions));\n\n return tree;\n };\n\n\n function updateToGraph(graph) {\n if (graph === head) return;\n\n var diff = coreDifference(head, graph);\n\n head = graph;\n\n var changed = diff.didChange;\n if (!changed.addition && !changed.deletion && !changed.geometry) return;\n\n var insertions = {};\n\n if (changed.deletion) {\n diff.deleted().forEach(function(entity) {\n removeEntity(entity);\n });\n }\n\n if (changed.geometry) {\n diff.modified().forEach(function(entity) {\n removeEntity(entity);\n insertions[entity.id] = entity;\n updateParents(entity, insertions, {});\n });\n }\n\n if (changed.addition) {\n diff.created().forEach(function(entity) {\n insertions[entity.id] = entity;\n });\n }\n\n loadEntities(Object.values(insertions));\n }\n\n // returns an array of entities with bounding boxes overlapping `extent` for the given `graph`\n tree.intersects = function(extent, graph) {\n updateToGraph(graph);\n return _rtree.search(extent.bbox())\n .map(function(bbox) { return graph.entity(bbox.id); });\n };\n\n // returns an array of segment objects with bounding boxes overlapping `extent` for the given `graph`\n tree.waySegments = function(extent, graph) {\n updateToGraph(graph);\n return _segmentsRTree.search(extent.bbox())\n .map(function(bbox) { return bbox.segment; });\n };\n\n\n return tree;\n}\n", "export function svgIcon(name, svgklass, useklass) {\n return function drawIcon(selection) {\n selection.selectAll('svg.icon' + (svgklass ? '.' + svgklass.split(' ')[0] : ''))\n .data([0])\n .enter()\n .append('svg')\n .attr('class', 'icon ' + (svgklass || ''))\n .append('use')\n .attr('xlink:href', name)\n .attr('class', useklass);\n };\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from './../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { utilKeybinding } from '../util';\n\n\nexport function uiModal(selection, blocking) {\n let keybinding = utilKeybinding('modal');\n let previous = selection.select('div.modal');\n let animate = previous.empty();\n\n previous.transition()\n .duration(200)\n .style('opacity', 0)\n .remove();\n\n let shaded = selection\n .append('div')\n .attr('class', 'shaded')\n .style('opacity', 0);\n\n shaded.close = () => {\n shaded\n .transition()\n .duration(200)\n .style('opacity',0)\n .remove();\n\n modal\n .transition()\n .duration(200)\n .style('top','0px');\n\n d3_select(document)\n .call(keybinding.unbind);\n };\n\n\n let modal = shaded\n .append('div')\n .attr('class', 'modal fillL');\n\n modal\n .append('input')\n .attr('class', 'keytrap keytrap-first')\n .on('focus.keytrap', moveFocusToLast);\n\n if (!blocking) {\n shaded.on('click.remove-modal', (d3_event) => {\n if (d3_event.target === this) {\n shaded.close();\n }\n });\n\n modal\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', shaded.close)\n .call(svgIcon('#iD-icon-close'));\n\n keybinding\n .on('\u232B', shaded.close)\n .on('\u238B', shaded.close);\n\n d3_select(document)\n .call(keybinding);\n }\n\n modal\n .append('div')\n .attr('class', 'content');\n\n modal\n .append('input')\n .attr('class', 'keytrap keytrap-last')\n .on('focus.keytrap', moveFocusToFirst);\n\n if (animate) {\n shaded.transition().style('opacity', 1);\n } else {\n shaded.style('opacity', 1);\n }\n\n return shaded;\n\n\n function moveFocusToFirst() {\n let node = modal\n // there are additional rules about what's focusable, but this suits our purposes\n .select('a, button, input:not(.keytrap), select, textarea')\n .node();\n\n if (node) {\n node.focus();\n } else {\n d3_select(this).node().blur();\n }\n }\n\n function moveFocusToLast() {\n let nodes = modal\n .selectAll('a, button, input:not(.keytrap), select, textarea')\n .nodes();\n\n if (nodes.length) {\n nodes[nodes.length - 1].focus();\n } else {\n d3_select(this).node().blur();\n }\n }\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { uiModal } from './modal';\n\n\nexport function uiLoading(context) {\n let _modalSelection = d3_select(null);\n let _message = '';\n let _blocking = false;\n\n\n let loading = (selection) => {\n _modalSelection = uiModal(selection, _blocking);\n\n let loadertext = _modalSelection.select('.content')\n .classed('loading-modal', true)\n .append('div')\n .attr('class', 'modal-section fillL');\n\n loadertext\n .append('img')\n .attr('class', 'loader')\n .attr('src', context.imagePath('loader-white.gif'));\n\n if (typeof message === 'string') {\n loadertext\n .append('h3')\n .text(_message);\n } else {\n loadertext\n .append('h3')\n .call(_message);\n }\n\n _modalSelection.select('button.close')\n .attr('class', 'hide');\n\n return loading;\n };\n\n\n loading.message = function(val) {\n if (!arguments.length) return _message;\n _message = val;\n return loading;\n };\n\n\n loading.blocking = function(val) {\n if (!arguments.length) return _blocking;\n _blocking = val;\n return loading;\n };\n\n\n loading.close = () => {\n _modalSelection.remove();\n };\n\n\n loading.isShown = () => {\n return _modalSelection && !_modalSelection.empty() && _modalSelection.node().parentNode;\n };\n\n\n return loading;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { easeLinear as d3_easeLinear } from 'd3-ease';\nimport { select as d3_select } from 'd3-selection';\n\nimport { asyncPrefs, prefs } from './preferences';\nimport { coreDifference } from './difference';\nimport { coreGraph } from './graph';\nimport { coreTree } from './tree';\nimport { osmEntity } from '../osm/entity';\nimport { uiLoading } from '../ui/loading';\nimport {\n utilArrayDifference, utilArrayGroupBy, utilArrayUnion,\n utilObjectOmit, utilRebind, utilSessionMutex\n} from '../util';\n\n\nexport function coreHistory(context) {\n var dispatch = d3_dispatch('reset', 'change', 'merge', 'restore', 'undone', 'redone', 'storage_error');\n var lock = utilSessionMutex('lock');\n\n // restorable if iD not open in another window/tab and a saved history exists in localStorage\n var _hasUnresolvedRestorableChanges = lock.lock() && !!prefs('has_saved_history');\n\n var duration = 150;\n var _imageryUsed = [];\n var _photoOverlaysUsed = [];\n var _checkpoints = {};\n var _pausedGraph;\n var _stack;\n var _index;\n var _tree;\n\n\n // internal _act, accepts list of actions and eased time\n function _act(actions, t) {\n actions = Array.prototype.slice.call(actions);\n\n var annotation;\n if (typeof actions[actions.length - 1] !== 'function') {\n annotation = actions.pop();\n }\n\n var graph = _stack[_index].graph;\n for (var i = 0; i < actions.length; i++) {\n graph = actions[i](graph, t);\n }\n\n return {\n graph: graph,\n annotation: annotation,\n imageryUsed: _imageryUsed,\n photoOverlaysUsed: _photoOverlaysUsed,\n transform: context.projection.transform(),\n selectedIDs: context.selectedIDs()\n };\n }\n\n\n // internal _perform with eased time\n function _perform(args, t) {\n var previous = _stack[_index].graph;\n _stack = _stack.slice(0, _index + 1);\n var actionResult = _act(args, t);\n _stack.push(actionResult);\n _index++;\n return change(previous);\n }\n\n\n // internal _replace with eased time\n function _replace(args, t) {\n var previous = _stack[_index].graph;\n // assert(_index == _stack.length - 1)\n var actionResult = _act(args, t);\n _stack[_index] = actionResult;\n return change(previous);\n }\n\n\n // internal _overwrite with eased time\n function _overwrite(args, t) {\n var previous = _stack[_index].graph;\n if (_index > 0) {\n _index--;\n _stack.pop();\n }\n _stack = _stack.slice(0, _index + 1);\n var actionResult = _act(args, t);\n _stack.push(actionResult);\n _index++;\n return change(previous);\n }\n\n\n // determine difference and dispatch a change event\n function change(previous) {\n var difference = coreDifference(previous, history.graph());\n if (!_pausedGraph) {\n dispatch.call('change', this, difference);\n }\n return difference;\n }\n\n\n var history = {\n\n graph: function() {\n return _stack[_index].graph;\n },\n\n\n tree: function() {\n return _tree;\n },\n\n\n base: function() {\n return _stack[0].graph;\n },\n\n\n merge: function(entities/*, extent*/) {\n var stack = _stack.map(function(state) { return state.graph; });\n _stack[0].graph.rebase(entities, stack, false);\n _tree.rebase(entities, false);\n\n dispatch.call('merge', this, entities);\n },\n\n\n perform: function() {\n // complete any transition already in progress\n d3_select(document).interrupt('history.perform');\n\n var transitionable = false;\n var action0 = arguments[0];\n\n if (arguments.length === 1 ||\n (arguments.length === 2 && (typeof arguments[1] !== 'function'))) {\n transitionable = !!action0.transitionable;\n }\n\n if (transitionable) {\n var origArguments = arguments;\n return new Promise(resolve => {\n d3_select(document)\n .transition('history.perform')\n .duration(duration)\n .ease(d3_easeLinear)\n .tween('history.tween', function() {\n return function(t) {\n if (t < 1) _overwrite([action0], t);\n };\n })\n .on('start', function() {\n resolve(_perform([action0], 0));\n })\n .on('end interrupt', function() {\n resolve(_overwrite(origArguments, 1));\n });\n });\n\n } else {\n return _perform(arguments);\n }\n },\n\n\n replace: function() {\n d3_select(document).interrupt('history.perform');\n return _replace(arguments);\n },\n\n\n pop: function(n) {\n d3_select(document).interrupt('history.perform');\n\n var previous = _stack[_index].graph;\n if (isNaN(+n) || +n < 0) {\n n = 1;\n }\n while (n-- > 0 && _index > 0) {\n _index--;\n _stack.pop();\n }\n return change(previous);\n },\n\n\n // Back to the previous annotated state or _index = 0.\n undo: function() {\n d3_select(document).interrupt('history.perform');\n\n var previousStack = _stack[_index];\n var previous = previousStack.graph;\n while (_index > 0) {\n _index--;\n if (_stack[_index].annotation) break;\n }\n\n dispatch.call('undone', this, _stack[_index], previousStack);\n return change(previous);\n },\n\n\n // Forward to the next annotated state.\n redo: function() {\n d3_select(document).interrupt('history.perform');\n\n var previousStack = _stack[_index];\n var previous = previousStack.graph;\n var tryIndex = _index;\n while (tryIndex < _stack.length - 1) {\n tryIndex++;\n if (_stack[tryIndex].annotation) {\n _index = tryIndex;\n dispatch.call('redone', this, _stack[_index], previousStack);\n break;\n }\n }\n\n return change(previous);\n },\n\n\n pauseChangeDispatch: function() {\n if (!_pausedGraph) {\n _pausedGraph = _stack[_index].graph;\n }\n },\n\n\n resumeChangeDispatch: function() {\n if (_pausedGraph) {\n var previous = _pausedGraph;\n _pausedGraph = null;\n return change(previous);\n }\n },\n\n\n undoAnnotation: function() {\n var i = _index;\n while (i >= 0) {\n if (_stack[i].annotation) return _stack[i].annotation;\n i--;\n }\n },\n\n\n redoAnnotation: function() {\n var i = _index + 1;\n while (i <= _stack.length - 1) {\n if (_stack[i].annotation) return _stack[i].annotation;\n i++;\n }\n },\n\n\n // Returns the entities from the active graph with bounding boxes\n // overlapping the given `extent`.\n intersects: function(extent) {\n return _tree.intersects(extent, _stack[_index].graph);\n },\n\n\n difference: function() {\n var base = _stack[0].graph;\n var head = _stack[_index].graph;\n return coreDifference(base, head);\n },\n\n\n changes: function(action) {\n var base = _stack[0].graph;\n var head = _stack[_index].graph;\n\n if (action) {\n head = action(head);\n }\n\n var difference = coreDifference(base, head);\n\n return {\n modified: difference.modified(),\n created: difference.created(),\n deleted: difference.deleted()\n };\n },\n\n\n changesCount() {\n return Object.values(this.changes()).flat().length;\n },\n\n\n hasChanges: function() {\n return this.difference().length() > 0;\n },\n\n\n imageryUsed: function(sources) {\n if (sources) {\n _imageryUsed = sources;\n return history;\n } else {\n var s = new Set();\n _stack.slice(1, _index + 1).forEach(function(state) {\n state.imageryUsed.forEach(function(source) {\n if (source !== 'Custom') {\n s.add(source);\n }\n });\n });\n return Array.from(s);\n }\n },\n\n\n photoOverlaysUsed: function(sources) {\n if (sources) {\n _photoOverlaysUsed = sources;\n return history;\n } else {\n var s = new Set();\n _stack.slice(1, _index + 1).forEach(function(state) {\n if (state.photoOverlaysUsed && Array.isArray(state.photoOverlaysUsed)) {\n state.photoOverlaysUsed.forEach(function(photoOverlay) {\n s.add(photoOverlay);\n });\n }\n });\n return Array.from(s);\n }\n },\n\n\n // save the current history state\n checkpoint: function(key) {\n _checkpoints[key] = {\n stack: _stack,\n index: _index\n };\n return history;\n },\n\n\n // restore history state to a given checkpoint or reset completely\n reset: function(key) {\n if (key !== undefined && _checkpoints.hasOwnProperty(key)) {\n _stack = _checkpoints[key].stack;\n _index = _checkpoints[key].index;\n } else {\n _stack = [{graph: coreGraph()}];\n _index = 0;\n _tree = coreTree(_stack[0].graph);\n _checkpoints = {};\n }\n _pausedGraph = null;\n dispatch.call('reset');\n dispatch.call('change');\n return history;\n },\n\n\n // `toIntroGraph()` is used to export the intro graph used by the walkthrough.\n //\n // To use it:\n // 1. Start the walkthrough.\n // 2. Get to a \"free editing\" tutorial step\n // 3. Make your edits to the walkthrough map\n // 4. In your browser dev console run:\n // `id.history().toIntroGraph()`\n // 5. This outputs stringified JSON to the browser console\n // 6. Copy it to `data/intro_graph.json` and prettify it in your code editor\n toIntroGraph: function() {\n var nextID = { n: 0, r: 0, w: 0 };\n var permIDs = {};\n var graph = this.graph();\n var baseEntities = {};\n\n // clone base entities..\n Object.values(graph.base().entities).forEach(function(entity) {\n var copy = copyIntroEntity(entity);\n baseEntities[copy.id] = copy;\n });\n\n // replace base entities with head entities..\n Object.keys(graph.entities).forEach(function(id) {\n var entity = graph.entities[id];\n if (entity) {\n var copy = copyIntroEntity(entity);\n baseEntities[copy.id] = copy;\n } else {\n delete baseEntities[id];\n }\n });\n\n // swap temporary for permanent ids..\n Object.values(baseEntities).forEach(function(entity) {\n if (Array.isArray(entity.nodes)) {\n entity.nodes = entity.nodes.map(function(node) {\n return permIDs[node] || node;\n });\n }\n if (Array.isArray(entity.members)) {\n entity.members = entity.members.map(function(member) {\n member.id = permIDs[member.id] || member.id;\n return member;\n });\n }\n });\n\n return JSON.stringify({ dataIntroGraph: baseEntities });\n\n\n function copyIntroEntity(source) {\n var copy = utilObjectOmit(source, ['type', 'user', 'v', 'version', 'visible']);\n\n // Note: the copy is no longer an osmEntity, so it might not have `tags`\n if (copy.tags && !Object.keys(copy.tags)) {\n delete copy.tags;\n }\n\n if (Array.isArray(copy.loc)) {\n copy.loc[0] = +copy.loc[0].toFixed(6);\n copy.loc[1] = +copy.loc[1].toFixed(6);\n }\n\n var match = source.id.match(/([nrw])-\\d*/); // temporary id\n if (match !== null) {\n var nrw = match[1];\n var permID;\n do { permID = nrw + (++nextID[nrw]); }\n while (baseEntities.hasOwnProperty(permID));\n\n copy.id = permID;\n permIDs[source.id] = permID;\n }\n return copy;\n }\n },\n\n\n toJSON: function() {\n if (!this.hasChanges()) return;\n\n var allEntities = {};\n var baseEntities = {};\n var base = _stack[0];\n\n var s = _stack.map(function(i) {\n var modified = [];\n var deleted = [];\n\n Object.keys(i.graph.entities).forEach(function(id) {\n var entity = i.graph.entities[id];\n if (entity) {\n var key = osmEntity.key(entity);\n allEntities[key] = entity;\n modified.push(key);\n } else {\n deleted.push(id);\n }\n\n // make sure that the originals of changed or deleted entities get merged\n // into the base of the _stack after restoring the data from JSON.\n if (id in base.graph.entities) {\n baseEntities[id] = base.graph.entities[id];\n }\n if (entity && entity.nodes) {\n // get originals of pre-existing child nodes\n entity.nodes.forEach(function(nodeID) {\n if (nodeID in base.graph.entities) {\n baseEntities[nodeID] = base.graph.entities[nodeID];\n }\n });\n }\n // get originals of parent entities too\n var baseParents = base.graph._parentWays[id];\n if (baseParents) {\n baseParents.forEach(function(parentID) {\n if (parentID in base.graph.entities) {\n baseEntities[parentID] = base.graph.entities[parentID];\n }\n });\n }\n });\n\n var x = {};\n\n if (modified.length) x.modified = modified;\n if (deleted.length) x.deleted = deleted;\n if (i.imageryUsed) x.imageryUsed = i.imageryUsed;\n if (i.photoOverlaysUsed) x.photoOverlaysUsed = i.photoOverlaysUsed;\n if (i.annotation) x.annotation = i.annotation;\n if (i.transform) x.transform = i.transform;\n if (i.selectedIDs) x.selectedIDs = i.selectedIDs;\n\n return x;\n });\n\n return {\n version: 3,\n entities: Object.values(allEntities),\n baseEntities: Object.values(baseEntities),\n stack: s,\n nextIDs: osmEntity.id.next,\n index: _index,\n // note the time the changes were saved\n timestamp: (new Date()).getTime()\n };\n },\n\n\n fromJSON: function(h, loadChildNodes) {\n var loadComplete = true;\n\n osmEntity.id.next = h.nextIDs;\n _index = h.index;\n\n if (h.version === 2 || h.version === 3) {\n var allEntities = {};\n\n h.entities.forEach(function(entity) {\n allEntities[osmEntity.key(entity)] = osmEntity(entity);\n });\n\n if (h.version === 3) {\n // This merges originals for changed entities into the base of\n // the _stack even if the current _stack doesn't have them (for\n // example when iD has been restarted in a different region)\n var baseEntities = h.baseEntities.map(function(d) { return osmEntity(d); });\n var stack = _stack.map(function(state) { return state.graph; });\n _stack[0].graph.rebase(baseEntities, stack, true);\n _tree.rebase(baseEntities, true);\n\n // When we restore a modified way, we also need to fetch any missing\n // childnodes that would normally have been downloaded with it.. #2142\n if (loadChildNodes) {\n var osm = context.connection();\n var baseWays = baseEntities\n .filter(function(e) { return e.type === 'way'; });\n var nodeIDs = baseWays\n .reduce(function(acc, way) { return utilArrayUnion(acc, way.nodes); }, []);\n var missing = nodeIDs\n .filter(function(n) { return !_stack[0].graph.hasEntity(n); });\n\n if (missing.length && osm) {\n loadComplete = false;\n context.map().redrawEnable(false);\n\n var loading = uiLoading(context).blocking(true);\n context.container().call(loading);\n\n var childNodesLoaded = function(err, result) {\n if (!err) {\n var visibleGroups = utilArrayGroupBy(result.data, 'visible');\n var visibles = visibleGroups.true || []; // alive nodes\n var invisibles = visibleGroups.false || []; // deleted nodes\n\n if (visibles.length) {\n var visibleIDs = visibles.map(function(entity) { return entity.id; });\n var stack = _stack.map(function(state) { return state.graph; });\n missing = utilArrayDifference(missing, visibleIDs);\n _stack[0].graph.rebase(visibles, stack, true);\n _tree.rebase(visibles, true);\n }\n\n // fetch older versions of nodes that were deleted..\n invisibles.forEach(function(entity) {\n osm.loadEntityVersion(entity.id, +entity.version - 1, childNodesLoaded);\n });\n }\n\n if (err || !missing.length) {\n loading.close();\n context.map().redrawEnable(true);\n dispatch.call('change');\n dispatch.call('restore', this);\n }\n };\n\n osm.loadMultiple(missing, childNodesLoaded);\n }\n }\n }\n\n _stack = h.stack.map(function(d) {\n var entities = {}, entity;\n\n if (d.modified) {\n d.modified.forEach(function(key) {\n entity = allEntities[key];\n entities[entity.id] = entity;\n });\n }\n\n if (d.deleted) {\n d.deleted.forEach(function(id) {\n entities[id] = undefined;\n });\n }\n\n return {\n graph: coreGraph(_stack[0].graph).load(entities),\n annotation: d.annotation,\n imageryUsed: d.imageryUsed,\n photoOverlaysUsed: d.photoOverlaysUsed,\n transform: d.transform,\n selectedIDs: d.selectedIDs\n };\n });\n\n } else { // original version\n _stack = h.stack.map(function(d) {\n var entities = {};\n\n for (var i in d.entities) {\n var entity = d.entities[i];\n entities[i] = entity === 'undefined' ? undefined : osmEntity(entity);\n }\n\n d.graph = coreGraph(_stack[0].graph).load(entities);\n return d;\n });\n }\n\n var transform = _stack[_index].transform;\n if (transform) {\n context.map().transformEase(transform, 0); // 0 = immediate, no easing\n }\n\n if (loadComplete) {\n dispatch.call('change');\n dispatch.call('restore', this);\n }\n\n return history;\n },\n\n\n lock: function() {\n return lock.lock();\n },\n\n\n unlock: function() {\n lock.unlock();\n },\n\n\n save: function() {\n if (lock.locked() &&\n // don't overwrite existing, unresolved changes\n !_hasUnresolvedRestorableChanges) {\n\n const historyData = history.toJSON();\n if (!historyData) {\n asyncPrefs.del('saved_history')\n .then(() => prefs('has_saved_history', null))\n .catch(() => dispatch.call('storage_error'));\n } else {\n asyncPrefs.set('saved_history', historyData)\n .then(() => prefs('has_saved_history', true))\n .catch(() => dispatch.call('storage_error'));\n }\n }\n return history;\n },\n\n\n // delete the history version saved in IndexedDB\n clearSaved: function() {\n context.debouncedSave.cancel();\n if (lock.locked()) {\n _hasUnresolvedRestorableChanges = false;\n\n asyncPrefs.del('saved_history')\n .then(() => prefs('has_saved_history', null));\n\n // clear the changeset metadata associated with the saved history\n prefs('comment', null);\n prefs('hashtags', null);\n prefs('source', null);\n }\n return history;\n },\n\n\n hasRestorableChanges: function() {\n return _hasUnresolvedRestorableChanges;\n },\n\n\n restore: async function() {\n if (lock.locked()) {\n _hasUnresolvedRestorableChanges = false;\n var json = await asyncPrefs.get('saved_history');\n if (json) history.fromJSON(json, true);\n }\n },\n\n\n migrateHistoryData: async function() {\n const value = JSON.parse(prefs(this._getLegacyKey('saved_history')));\n\n if (value !== null) {\n await asyncPrefs.set('saved_history', value);\n prefs('has_saved_history', true);\n prefs(this._getLegacyKey('saved_history'), null);\n }\n },\n\n\n // (legacy, was used for local-storage based history)\n // iD uses namespaced keys so multiple installations do not conflict\n _getLegacyKey: n => 'iD_' + window.location.origin + '_' + n,\n };\n\n history.reset();\n\n return utilRebind(history, dispatch, 'on');\n}\n", "import { presetManager } from '../presets';\nimport { utilDisplayName, utilDisplayType } from './util';\n\n/**\n * `utilDisplayLabel` returns a string suitable for display\n *\n * By default returns something like name/ref, fallback to preset type, fallback to OSM type\n * \"Main Street\" or \"Tertiary Road\"\n *\n * If `verbose=true`, include both preset name and feature name.\n * \"Tertiary Road Main Street\"\n * @param {osmEntity} entity\n * @param {string | unknown} graphOrGeometry\n * @param {boolean} [verbose]\n * @returns {string}\n */\nexport function utilDisplayLabel(entity, graphOrGeometry, verbose) {\n var result;\n var displayName = utilDisplayName(entity);\n var preset = typeof graphOrGeometry === 'string' ?\n presetManager.matchTags(entity.tags, graphOrGeometry) :\n presetManager.match(entity, graphOrGeometry);\n var presetName = preset && (preset.suggestion ? preset.subtitle() : preset.name());\n\n if (verbose) {\n result = [presetName, displayName].filter(Boolean).join(' ');\n } else {\n result = displayName || presetName;\n }\n\n // Fallback to the OSM type (node/way/relation)\n return result || utilDisplayType(entity.id);\n}\n", "import { geoExtent } from '../../geo';\nimport { t } from '../../core/localizer';\n\nexport function validationIssue(attrs) {\n this.type = attrs.type; // required - name of rule that created the issue (e.g. 'missing_tag')\n this.subtype = attrs.subtype; // optional - category of the issue within the type (e.g. 'relation_type' under 'missing_tag')\n this.severity = attrs.severity; // required - 'suggestion' or 'warning' or 'error'\n this.message = attrs.message; // required - function returning localized string\n this.reference = attrs.reference; // optional - function(selection) to render reference information\n this.entityIds = attrs.entityIds; // optional - array of IDs of entities involved in the issue\n this.loc = attrs.loc; // optional - [lon, lat] to zoom in on to see the issue\n this.data = attrs.data; // optional - object containing extra data for the fixes\n this.dynamicFixes = attrs.dynamicFixes;// optional - function(context) returning fixes\n this.hash = attrs.hash; // optional - string to further differentiate the issue\n this.extent = attrs.extent; // optional - a method that returns the geometric extent of the issue, if absent, it will be calculated from the given entityIds\n\n this.id = generateID.apply(this); // generated - see below\n this.key = generateKey.apply(this); // generated - see below (call after generating this.id)\n\n // A unique, deterministic string hash.\n // Issues with identical id values are considered identical.\n function generateID() {\n var parts = [this.type];\n\n if (this.hash) { // subclasses can pass in their own differentiator\n parts.push(this.hash);\n }\n\n if (this.subtype) {\n parts.push(this.subtype);\n }\n\n // include the entities this issue is for\n // (sort them so the id is deterministic)\n if (this.entityIds) {\n var entityKeys = this.entityIds.slice().sort();\n parts.push.apply(parts, entityKeys);\n }\n\n return parts.join(':');\n }\n\n // An identifier suitable for use as the second argument to d3.selection#data().\n // (i.e. this should change whenever the data needs to be refreshed)\n function generateKey() {\n return this.id + ':' + Date.now().toString(); // include time of creation\n }\n\n this.extent = this.extent || function(resolver) {\n if (this.loc) {\n return geoExtent(this.loc);\n }\n if (this.entityIds && this.entityIds.length) {\n return this.entityIds.reduce(function(extent, entityId) {\n return extent.extend(resolver.entity(entityId).extent(resolver));\n }, geoExtent());\n }\n return null;\n };\n\n this.fixes = function(context) {\n var fixes = this.dynamicFixes ? this.dynamicFixes(context) : [];\n var issue = this;\n\n if (issue.severity === 'warning' || issue.severity === 'suggestion') {\n // allow ignoring any issue that's not an error\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.ignore_issue.title'),\n icon: 'iD-icon-close',\n onClick: function() {\n context.validator().ignoreIssue(this.issue.id);\n }\n }));\n }\n\n fixes.forEach(function(fix) {\n // the id doesn't matter as long as it's unique to this issue/fix\n // except cases where fix depends on the currently selected feature.\n fix.id ||= fix.title.stringId;\n // add a reference to the issue for use in actions\n fix.issue = issue;\n });\n return fixes;\n };\n\n}\n\nvalidationIssue.ICONS = {\n suggestion: '#iD-icon-info',\n warning: '#iD-icon-alert',\n error: '#iD-icon-error'\n};\n\n\nexport function validationIssueFix(attrs) {\n this.title = attrs.title; // Required\n this.id = attrs.id; // Optional\n this.onClick = attrs.onClick; // Optional - the function to run to apply the fix\n this.disabledReason = attrs.disabledReason; // Optional - a string explaining why the fix is unavailable, if any\n this.icon = attrs.icon; // Optional - shows 'iD-icon-wrench' if not set\n this.entityIds = attrs.entityIds || []; // Optional - used for hover-higlighting.\n\n this.issue = null; // Generated link - added by validationIssue\n}\n", "export { validationIssue, validationIssueFix } from './models';\n", "import type { MarkedOptions } from './MarkedOptions.ts';\n\n/**\n * Gets the original marked default options.\n */\nexport function _getDefaults(): MarkedOptions {\n return {\n async: false,\n breaks: false,\n extensions: null,\n gfm: true,\n hooks: null,\n pedantic: false,\n renderer: null,\n silent: false,\n tokenizer: null,\n walkTokens: null,\n };\n}\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport let _defaults: MarkedOptions = _getDefaults();\n\nexport function changeDefaults(newDefaults: MarkedOptions) {\n _defaults = newDefaults;\n}\n", "const noopTest = { exec: () => null } as unknown as RegExp;\n\nfunction edit(regex: string | RegExp, opt = '') {\n let source = typeof regex === 'string' ? regex : regex.source;\n const obj = {\n replace: (name: string | RegExp, val: string | RegExp) => {\n let valSource = typeof val === 'string' ? val : val.source;\n valSource = valSource.replace(other.caret, '$1');\n source = source.replace(name, valSource);\n return obj;\n },\n getRegex: () => {\n return new RegExp(source, opt);\n },\n };\n return obj;\n}\n\nconst supportsLookbehind = (() => {\ntry {\n // eslint-disable-next-line prefer-regex-literals\n return !!new RegExp('(?<=1)(?/,\n blockquoteSetextReplace: /\\n {0,3}((?:=+|-+) *)(?=\\n|$)/g,\n blockquoteSetextReplace2: /^ {0,3}>[ \\t]?/gm,\n listReplaceNesting: /^ {1,4}(?=( {4})*[^ ])/g,\n listIsTask: /^\\[[ xX]\\] +\\S/,\n listReplaceTask: /^\\[[ xX]\\] +/,\n listTaskCheckbox: /\\[[ xX]\\]/,\n anyLine: /\\n.*\\n/,\n hrefBrackets: /^<(.*)>$/,\n tableDelimiter: /[:|]/,\n tableAlignChars: /^\\||\\| *$/g,\n tableRowBlankLine: /\\n[ \\t]*$/,\n tableAlignRight: /^ *-+: *$/,\n tableAlignCenter: /^ *:-+: *$/,\n tableAlignLeft: /^ *:-+ *$/,\n startATag: /^/i,\n startPreScriptTag: /^<(pre|code|kbd|script)(\\s|>)/i,\n endPreScriptTag: /^<\\/(pre|code|kbd|script)(\\s|>)/i,\n startAngleBracket: /^$/,\n pedanticHrefTitle: /^([^'\"]*[^\\s])\\s+(['\"])(.*)\\2/,\n unicodeAlphaNumeric: /[\\p{L}\\p{N}]/u,\n escapeTest: /[&<>\"']/,\n escapeReplace: /[&<>\"']/g,\n escapeTestNoEncode: /[<>\"']|&(?!(#\\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\\w+);)/,\n escapeReplaceNoEncode: /[<>\"']|&(?!(#\\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\\w+);)/g,\n caret: /(^|[^\\[])\\^/g,\n percentDecode: /%25/g,\n findPipe: /\\|/g,\n splitPipe: / \\|/,\n slashPipe: /\\\\\\|/g,\n carriageReturn: /\\r\\n|\\r/g,\n spaceLine: /^ +$/gm,\n notSpaceStart: /^\\S*/,\n endingNewline: /\\n$/,\n listItemRegex: (bull: string) => new RegExp(`^( {0,3}${bull})((?:[\\t ][^\\\\n]*)?(?:\\\\n|$))`),\n nextBulletRegex: (indent: number) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:[*+-]|\\\\d{1,9}[.)])((?:[ \\t][^\\\\n]*)?(?:\\\\n|$))`),\n hrRegex: (indent: number) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\\\* *){3,})(?:\\\\n+|$)`),\n fencesBeginRegex: (indent: number) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}(?:\\`\\`\\`|~~~)`),\n headingBeginRegex: (indent: number) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}#`),\n htmlBeginRegex: (indent: number) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}<(?:[a-z].*>|!--)`, 'i'),\n blockquoteBeginRegex: (indent: number) => new RegExp(`^ {0,${Math.min(3, indent - 1)}}>`),\n};\n\n/**\n * Block-Level Grammar\n */\n\nconst newline = /^(?:[ \\t]*(?:\\n|$))+/;\nconst blockCode = /^((?: {4}| {0,3}\\t)[^\\n]+(?:\\n(?:[ \\t]*(?:\\n|$))*)?)+/;\nconst fences = /^ {0,3}(`{3,}(?=[^`\\n]*(?:\\n|$))|~{3,})([^\\n]*)(?:\\n|$)(?:|([\\s\\S]*?)(?:\\n|$))(?: {0,3}\\1[~`]* *(?=\\n|$)|$)/;\nconst hr = /^ {0,3}((?:-[\\t ]*){3,}|(?:_[ \\t]*){3,}|(?:\\*[ \\t]*){3,})(?:\\n+|$)/;\nconst heading = /^ {0,3}(#{1,6})(?=\\s|$)(.*)(?:\\n+|$)/;\nconst bullet = / {0,3}(?:[*+-]|\\d{1,9}[.)])/;\nconst lheadingCore = /^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\\n(?!\\s*?\\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\\n {0,3}(=+|-+) *(?:\\n+|$)/;\nconst lheading = edit(lheadingCore)\n .replace(/bull/g, bullet) // lists can interrupt\n .replace(/blockCode/g, /(?: {4}| {0,3}\\t)/) // indented code blocks can interrupt\n .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt\n .replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt\n .replace(/heading/g, / {0,3}#{1,6}/) // ATX heading can interrupt\n .replace(/html/g, / {0,3}<[^\\n>]+>\\n/) // block html can interrupt\n .replace(/\\|table/g, '') // table not in commonmark\n .getRegex();\nconst lheadingGfm = edit(lheadingCore)\n .replace(/bull/g, bullet) // lists can interrupt\n .replace(/blockCode/g, /(?: {4}| {0,3}\\t)/) // indented code blocks can interrupt\n .replace(/fences/g, / {0,3}(?:`{3,}|~{3,})/) // fenced code blocks can interrupt\n .replace(/blockquote/g, / {0,3}>/) // blockquote can interrupt\n .replace(/heading/g, / {0,3}#{1,6}/) // ATX heading can interrupt\n .replace(/html/g, / {0,3}<[^\\n>]+>\\n/) // block html can interrupt\n .replace(/table/g, / {0,3}\\|?(?:[:\\- ]*\\|)+[\\:\\- ]*\\n/) // table can interrupt\n .getRegex();\nconst _paragraph = /^([^\\n]+(?:\\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\\n)[^\\n]+)*)/;\nconst blockText = /^[^\\n]+/;\nconst _blockLabel = /(?!\\s*\\])(?:\\\\[\\s\\S]|[^\\[\\]\\\\])+/;\nconst def = edit(/^ {0,3}\\[(label)\\]: *(?:\\n[ \\t]*)?([^<\\s][^\\s]*|<.*?>)(?:(?: +(?:\\n[ \\t]*)?| *\\n[ \\t]*)(title))? *(?:\\n+|$)/)\n .replace('label', _blockLabel)\n .replace('title', /(?:\"(?:\\\\\"?|[^\"\\\\])*\"|'[^'\\n]*(?:\\n[^'\\n]+)*\\n?'|\\([^()]*\\))/)\n .getRegex();\n\nconst list = edit(/^(bull)([ \\t][^\\n]+?)?(?:\\n|$)/)\n .replace(/bull/g, bullet)\n .getRegex();\n\nconst _tag = 'address|article|aside|base|basefont|blockquote|body|caption'\n + '|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption'\n + '|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe'\n + '|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option'\n + '|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title'\n + '|tr|track|ul';\nconst _comment = /|$))/;\nconst html = edit(\n '^ {0,3}(?:' // optional indentation\n+ '<(script|pre|style|textarea)[\\\\s>][\\\\s\\\\S]*?(?:[^\\\\n]*\\\\n+|$)' // (1)\n+ '|comment[^\\\\n]*(\\\\n+|$)' // (2)\n+ '|<\\\\?[\\\\s\\\\S]*?(?:\\\\?>\\\\n*|$)' // (3)\n+ '|\\\\n*|$)' // (4)\n+ '|\\\\n*|$)' // (5)\n+ '|)[\\\\s\\\\S]*?(?:(?:\\\\n[ \\t]*)+\\\\n|$)' // (6)\n+ '|<(?!script|pre|style|textarea)([a-z][\\\\w-]*)(?:attribute)*? */?>(?=[ \\\\t]*(?:\\\\n|$))[\\\\s\\\\S]*?(?:(?:\\\\n[ \\t]*)+\\\\n|$)' // (7) open tag\n+ '|(?=[ \\\\t]*(?:\\\\n|$))[\\\\s\\\\S]*?(?:(?:\\\\n[ \\t]*)+\\\\n|$)' // (7) closing tag\n+ ')', 'i')\n .replace('comment', _comment)\n .replace('tag', _tag)\n .replace('attribute', / +[a-zA-Z:_][\\w.:-]*(?: *= *\"[^\"\\n]*\"| *= *'[^'\\n]*'| *= *[^\\s\"'=<>`]+)?/)\n .getRegex();\n\nconst paragraph = edit(_paragraph)\n .replace('hr', hr)\n .replace('heading', ' {0,3}#{1,6}(?:\\\\s|$)')\n .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs\n .replace('|table', '')\n .replace('blockquote', ' {0,3}>')\n .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\\\n]*\\\\n)|~{3,})[^\\\\n]*\\\\n')\n .replace('list', ' {0,3}(?:[*+-]|1[.)])[ \\\\t]') // only lists starting from 1 can interrupt\n .replace('html', ')|<(?:script|pre|style|textarea|!--)')\n .replace('tag', _tag) // pars can be interrupted by type (6) html blocks\n .getRegex();\n\nconst blockquote = edit(/^( {0,3}> ?(paragraph|[^\\n]*)(?:\\n|$))+/)\n .replace('paragraph', paragraph)\n .getRegex();\n\n/**\n * Normal Block Grammar\n */\n\nconst blockNormal = {\n blockquote,\n code: blockCode,\n def,\n fences,\n heading,\n hr,\n html,\n lheading,\n list,\n newline,\n paragraph,\n table: noopTest,\n text: blockText,\n};\n\ntype BlockKeys = keyof typeof blockNormal;\n\n/**\n * GFM Block Grammar\n */\n\nconst gfmTable = edit(\n '^ *([^\\\\n ].*)\\\\n' // Header\n+ ' {0,3}((?:\\\\| *)?:?-+:? *(?:\\\\| *:?-+:? *)*(?:\\\\| *)?)' // Align\n+ '(?:\\\\n((?:(?! *\\\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\\\n|$))*)\\\\n*|$)') // Cells\n .replace('hr', hr)\n .replace('heading', ' {0,3}#{1,6}(?:\\\\s|$)')\n .replace('blockquote', ' {0,3}>')\n .replace('code', '(?: {4}| {0,3}\\t)[^\\\\n]')\n .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\\\n]*\\\\n)|~{3,})[^\\\\n]*\\\\n')\n .replace('list', ' {0,3}(?:[*+-]|1[.)])[ \\\\t]') // only lists starting from 1 can interrupt\n .replace('html', ')|<(?:script|pre|style|textarea|!--)')\n .replace('tag', _tag) // tables can be interrupted by type (6) html blocks\n .getRegex();\n\nconst blockGfm: Record = {\n ...blockNormal,\n lheading: lheadingGfm,\n table: gfmTable,\n paragraph: edit(_paragraph)\n .replace('hr', hr)\n .replace('heading', ' {0,3}#{1,6}(?:\\\\s|$)')\n .replace('|lheading', '') // setext headings don't interrupt commonmark paragraphs\n .replace('table', gfmTable) // interrupt paragraphs with table\n .replace('blockquote', ' {0,3}>')\n .replace('fences', ' {0,3}(?:`{3,}(?=[^`\\\\n]*\\\\n)|~{3,})[^\\\\n]*\\\\n')\n .replace('list', ' {0,3}(?:[*+-]|1[.)])[ \\\\t]') // only lists starting from 1 can interrupt\n .replace('html', ')|<(?:script|pre|style|textarea|!--)')\n .replace('tag', _tag) // pars can be interrupted by type (6) html blocks\n .getRegex(),\n};\n\n/**\n * Pedantic grammar (original John Gruber's loose markdown specification)\n */\n\nconst blockPedantic: Record = {\n ...blockNormal,\n html: edit(\n '^ *(?:comment *(?:\\\\n|\\\\s*$)'\n + '|<(tag)[\\\\s\\\\S]+? *(?:\\\\n{2,}|\\\\s*$)' // closed tag\n + '|\\\\s]*)*?/?> *(?:\\\\n{2,}|\\\\s*$))')\n .replace('comment', _comment)\n .replace(/tag/g, '(?!(?:'\n + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub'\n + '|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)'\n + '\\\\b)\\\\w+(?!:|[^\\\\w\\\\s@]*@)\\\\b')\n .getRegex(),\n def: /^ *\\[([^\\]]+)\\]: *]+)>?(?: +([\"(][^\\n]+[\")]))? *(?:\\n+|$)/,\n heading: /^(#{1,6})(.*)(?:\\n+|$)/,\n fences: noopTest, // fences not supported\n lheading: /^(.+?)\\n {0,3}(=+|-+) *(?:\\n+|$)/,\n paragraph: edit(_paragraph)\n .replace('hr', hr)\n .replace('heading', ' *#{1,6} *[^\\n]')\n .replace('lheading', lheading)\n .replace('|table', '')\n .replace('blockquote', ' {0,3}>')\n .replace('|fences', '')\n .replace('|list', '')\n .replace('|html', '')\n .replace('|tag', '')\n .getRegex(),\n};\n\n/**\n * Inline-Level Grammar\n */\n\nconst escape = /^\\\\([!\"#$%&'()*+,\\-./:;<=>?@\\[\\]\\\\^_`{|}~])/;\nconst inlineCode = /^(`+)([^`]|[^`][\\s\\S]*?[^`])\\1(?!`)/;\nconst br = /^( {2,}|\\\\)\\n(?!\\s*$)/;\nconst inlineText = /^(`+|[^`])(?:(?= {2,}\\n)|[\\s\\S]*?(?:(?=[\\\\\nconst blockSkip = edit(/link|precode-code|html/, 'g')\n .replace('link', /\\[(?:[^\\[\\]`]|(?`+)[^`]+\\k(?!`))*?\\]\\((?:\\\\[\\s\\S]|[^\\\\\\(\\)]|\\((?:\\\\[\\s\\S]|[^\\\\\\(\\)])*\\))*\\)/)\n .replace('precode-', supportsLookbehind ? '(?`+)[^`]+\\k(?!`)/)\n .replace('html', /<(?! )[^<>]*?>/)\n .getRegex();\n\nconst emStrongLDelimCore = /^(?:\\*+(?:((?!\\*)punct)|[^\\s*]))|^_+(?:((?!_)punct)|([^\\s_]))/;\n\nconst emStrongLDelim = edit(emStrongLDelimCore, 'u')\n .replace(/punct/g, _punctuation)\n .getRegex();\n\nconst emStrongLDelimGfm = edit(emStrongLDelimCore, 'u')\n .replace(/punct/g, _punctuationGfmStrongEm)\n .getRegex();\n\nconst emStrongRDelimAstCore =\n '^[^_*]*?__[^_*]*?\\\\*[^_*]*?(?=__)' // Skip orphan inside strong\n+ '|[^*]+(?=[^*])' // Consume to delim\n+ '|(?!\\\\*)punct(\\\\*+)(?=[\\\\s]|$)' // (1) #*** can only be a Right Delimiter\n+ '|notPunctSpace(\\\\*+)(?!\\\\*)(?=punctSpace|$)' // (2) a***#, a*** can only be a Right Delimiter\n+ '|(?!\\\\*)punctSpace(\\\\*+)(?=notPunctSpace)' // (3) #***a, ***a can only be Left Delimiter\n+ '|[\\\\s](\\\\*+)(?!\\\\*)(?=punct)' // (4) ***# can only be Left Delimiter\n+ '|(?!\\\\*)punct(\\\\*+)(?!\\\\*)(?=punct)' // (5) #***# can be either Left or Right Delimiter\n+ '|notPunctSpace(\\\\*+)(?=notPunctSpace)'; // (6) a***a can be either Left or Right Delimiter\n\nconst emStrongRDelimAst = edit(emStrongRDelimAstCore, 'gu')\n .replace(/notPunctSpace/g, _notPunctuationOrSpace)\n .replace(/punctSpace/g, _punctuationOrSpace)\n .replace(/punct/g, _punctuation)\n .getRegex();\n\nconst emStrongRDelimAstGfm = edit(emStrongRDelimAstCore, 'gu')\n .replace(/notPunctSpace/g, _notPunctuationOrSpaceGfmStrongEm)\n .replace(/punctSpace/g, _punctuationOrSpaceGfmStrongEm)\n .replace(/punct/g, _punctuationGfmStrongEm)\n .getRegex();\n\n// (6) Not allowed for _\nconst emStrongRDelimUnd = edit(\n '^[^_*]*?\\\\*\\\\*[^_*]*?_[^_*]*?(?=\\\\*\\\\*)' // Skip orphan inside strong\n+ '|[^_]+(?=[^_])' // Consume to delim\n+ '|(?!_)punct(_+)(?=[\\\\s]|$)' // (1) #___ can only be a Right Delimiter\n+ '|notPunctSpace(_+)(?!_)(?=punctSpace|$)' // (2) a___#, a___ can only be a Right Delimiter\n+ '|(?!_)punctSpace(_+)(?=notPunctSpace)' // (3) #___a, ___a can only be Left Delimiter\n+ '|[\\\\s](_+)(?!_)(?=punct)' // (4) ___# can only be Left Delimiter\n+ '|(?!_)punct(_+)(?!_)(?=punct)', 'gu') // (5) #___# can be either Left or Right Delimiter\n .replace(/notPunctSpace/g, _notPunctuationOrSpace)\n .replace(/punctSpace/g, _punctuationOrSpace)\n .replace(/punct/g, _punctuation)\n .getRegex();\n\n// Tilde left delimiter for strikethrough (similar to emStrongLDelim for asterisk)\nconst delLDelim = edit(/^~~?(?:((?!~)punct)|[^\\s~])/, 'u')\n .replace(/punct/g, _punctuationGfmDel)\n .getRegex();\n\n// Tilde delimiter patterns for strikethrough (similar to asterisk)\nconst delRDelimCore =\n '^[^~]+(?=[^~])' // Consume to delim\n+ '|(?!~)punct(~~?)(?=[\\\\s]|$)' // (1) #~~ can only be a Right Delimiter\n+ '|notPunctSpace(~~?)(?!~)(?=punctSpace|$)' // (2) a~~#, a~~ can only be a Right Delimiter\n+ '|(?!~)punctSpace(~~?)(?=notPunctSpace)' // (3) #~~a, ~~a can only be Left Delimiter\n+ '|[\\\\s](~~?)(?!~)(?=punct)' // (4) ~~# can only be Left Delimiter\n+ '|(?!~)punct(~~?)(?!~)(?=punct)' // (5) #~~# can be either Left or Right Delimiter\n+ '|notPunctSpace(~~?)(?=notPunctSpace)'; // (6) a~~a can be either Left or Right Delimiter\n\nconst delRDelim = edit(delRDelimCore, 'gu')\n .replace(/notPunctSpace/g, _notPunctuationOrSpaceGfmDel)\n .replace(/punctSpace/g, _punctuationOrSpaceGfmDel)\n .replace(/punct/g, _punctuationGfmDel)\n .getRegex();\n\nconst anyPunctuation = edit(/\\\\(punct)/, 'gu')\n .replace(/punct/g, _punctuation)\n .getRegex();\n\nconst autolink = edit(/^<(scheme:[^\\s\\x00-\\x1f<>]*|email)>/)\n .replace('scheme', /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/)\n .replace('email', /[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/)\n .getRegex();\n\nconst _inlineComment = edit(_comment).replace('(?:-->|$)', '-->').getRegex();\nconst tag = edit(\n '^comment'\n + '|^' // self-closing tag\n + '|^<[a-zA-Z][\\\\w-]*(?:attribute)*?\\\\s*/?>' // open tag\n + '|^<\\\\?[\\\\s\\\\S]*?\\\\?>' // processing instruction, e.g. \n + '|^' // declaration, e.g. \n + '|^') // CDATA section\n .replace('comment', _inlineComment)\n .replace('attribute', /\\s+[a-zA-Z:_][\\w.:-]*(?:\\s*=\\s*\"[^\"]*\"|\\s*=\\s*'[^']*'|\\s*=\\s*[^\\s\"'=<>`]+)?/)\n .getRegex();\n\nconst _inlineLabel = /(?:\\[(?:\\\\[\\s\\S]|[^\\[\\]\\\\])*\\]|\\\\[\\s\\S]|`+[^`]*?`+(?!`)|[^\\[\\]\\\\`])*?/;\n\nconst link = edit(/^!?\\[(label)\\]\\(\\s*(href)(?:(?:[ \\t]+(?:\\n[ \\t]*)?|\\n[ \\t]*)(title))?\\s*\\)/)\n .replace('label', _inlineLabel)\n .replace('href', /<(?:\\\\.|[^\\n<>\\\\])+>|[^ \\t\\n\\x00-\\x1f]*/)\n .replace('title', /\"(?:\\\\\"?|[^\"\\\\])*\"|'(?:\\\\'?|[^'\\\\])*'|\\((?:\\\\\\)?|[^)\\\\])*\\)/)\n .getRegex();\n\nconst reflink = edit(/^!?\\[(label)\\]\\[(ref)\\]/)\n .replace('label', _inlineLabel)\n .replace('ref', _blockLabel)\n .getRegex();\n\nconst nolink = edit(/^!?\\[(ref)\\](?:\\[\\])?/)\n .replace('ref', _blockLabel)\n .getRegex();\n\nconst reflinkSearch = edit('reflink|nolink(?!\\\\()', 'g')\n .replace('reflink', reflink)\n .replace('nolink', nolink)\n .getRegex();\n\nconst _caseInsensitiveProtocol = /[hH][tT][tT][pP][sS]?|[fF][tT][pP]/;\n\n/**\n * Normal Inline Grammar\n */\n\nconst inlineNormal = {\n _backpedal: noopTest, // only used for GFM url\n anyPunctuation,\n autolink,\n blockSkip,\n br,\n code: inlineCode,\n del: noopTest,\n delLDelim: noopTest,\n delRDelim: noopTest,\n emStrongLDelim,\n emStrongRDelimAst,\n emStrongRDelimUnd,\n escape,\n link,\n nolink,\n punctuation,\n reflink,\n reflinkSearch,\n tag,\n text: inlineText,\n url: noopTest,\n};\n\ntype InlineKeys = keyof typeof inlineNormal;\n\n/**\n * Pedantic Inline Grammar\n */\n\nconst inlinePedantic: Record = {\n ...inlineNormal,\n link: edit(/^!?\\[(label)\\]\\((.*?)\\)/)\n .replace('label', _inlineLabel)\n .getRegex(),\n reflink: edit(/^!?\\[(label)\\]\\s*\\[([^\\]]*)\\]/)\n .replace('label', _inlineLabel)\n .getRegex(),\n};\n\n/**\n * GFM Inline Grammar\n */\n\nconst inlineGfm: Record = {\n ...inlineNormal,\n emStrongRDelimAst: emStrongRDelimAstGfm,\n emStrongLDelim: emStrongLDelimGfm,\n delLDelim,\n delRDelim,\n url: edit(/^((?:protocol):\\/\\/|www\\.)(?:[a-zA-Z0-9\\-]+\\.?)+[^\\s<]*|^email/)\n .replace('protocol', _caseInsensitiveProtocol)\n .replace('email', /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/)\n .getRegex(),\n _backpedal: /(?:[^?!.,:;*_'\"~()&]+|\\([^)]*\\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'\"~)]+(?!$))+/,\n del: /^(~~?)(?=[^\\s~])((?:\\\\[\\s\\S]|[^\\\\])*?(?:\\\\[\\s\\S]|[^\\s~\\\\]))\\1(?=[^~]|$)/,\n text: edit(/^([`~]+|[^`~])(?:(?= {2,}\\n)|(?=[a-zA-Z0-9.!#$%&'*+\\/=?_`{\\|}~-]+@)|[\\s\\S]*?(?:(?=[\\\\ = {\n ...inlineGfm,\n br: edit(br).replace('{2,}', '*').getRegex(),\n text: edit(inlineGfm.text)\n .replace('\\\\b_', '\\\\b_| {2,}\\\\n')\n .replace(/\\{2,\\}/g, '*')\n .getRegex(),\n};\n\n/**\n * exports\n */\n\nexport const block = {\n normal: blockNormal,\n gfm: blockGfm,\n pedantic: blockPedantic,\n};\n\nexport const inline = {\n normal: inlineNormal,\n gfm: inlineGfm,\n breaks: inlineBreaks,\n pedantic: inlinePedantic,\n};\n\nexport interface Rules {\n other: typeof other\n block: Record\n inline: Record\n}\n", "import { other } from './rules.ts';\n\n/**\n * Helpers\n */\nconst escapeReplacements: { [index: string]: string } = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": ''',\n};\nconst getEscapeReplacement = (ch: string) => escapeReplacements[ch];\n\nexport function escapeHtmlEntities(html: string, encode?: boolean) {\n if (encode) {\n if (other.escapeTest.test(html)) {\n return html.replace(other.escapeReplace, getEscapeReplacement);\n }\n } else {\n if (other.escapeTestNoEncode.test(html)) {\n return html.replace(other.escapeReplaceNoEncode, getEscapeReplacement);\n }\n }\n\n return html;\n}\n\nexport function cleanUrl(href: string) {\n try {\n href = encodeURI(href).replace(other.percentDecode, '%');\n } catch {\n return null;\n }\n return href;\n}\n\nexport function splitCells(tableRow: string, count?: number) {\n // ensure that every cell-delimiting pipe has a space\n // before it to distinguish it from an escaped pipe\n const row = tableRow.replace(other.findPipe, (match, offset, str) => {\n let escaped = false;\n let curr = offset;\n while (--curr >= 0 && str[curr] === '\\\\') escaped = !escaped;\n if (escaped) {\n // odd number of slashes means | is escaped\n // so we leave it alone\n return '|';\n } else {\n // add space before unescaped |\n return ' |';\n }\n }),\n cells = row.split(other.splitPipe);\n let i = 0;\n\n // First/last cell in a row cannot be empty if it has no leading/trailing pipe\n if (!cells[0].trim()) {\n cells.shift();\n }\n if (cells.length > 0 && !cells.at(-1)?.trim()) {\n cells.pop();\n }\n\n if (count) {\n if (cells.length > count) {\n cells.splice(count);\n } else {\n while (cells.length < count) cells.push('');\n }\n }\n\n for (; i < cells.length; i++) {\n // leading or trailing whitespace is ignored per the gfm spec\n cells[i] = cells[i].trim().replace(other.slashPipe, '|');\n }\n return cells;\n}\n\n/**\n * Remove trailing 'c's. Equivalent to str.replace(/c*$/, '').\n * /c*$/ is vulnerable to REDOS.\n *\n * @param str\n * @param c\n * @param invert Remove suffix of non-c chars instead. Default falsey.\n */\nexport function rtrim(str: string, c: string, invert?: boolean) {\n const l = str.length;\n if (l === 0) {\n return '';\n }\n\n // Length of suffix matching the invert condition.\n let suffLen = 0;\n\n // Step left until we fail to match the invert condition.\n while (suffLen < l) {\n const currChar = str.charAt(l - suffLen - 1);\n if (currChar === c && !invert) {\n suffLen++;\n } else if (currChar !== c && invert) {\n suffLen++;\n } else {\n break;\n }\n }\n\n return str.slice(0, l - suffLen);\n}\n\nexport function findClosingBracket(str: string, b: string) {\n if (str.indexOf(b[1]) === -1) {\n return -1;\n }\n\n let level = 0;\n for (let i = 0; i < str.length; i++) {\n if (str[i] === '\\\\') {\n i++;\n } else if (str[i] === b[0]) {\n level++;\n } else if (str[i] === b[1]) {\n level--;\n if (level < 0) {\n return i;\n }\n }\n }\n if (level > 0) {\n return -2;\n }\n\n return -1;\n}\n\nexport function expandTabs(line: string, indent = 0) {\n let col = indent;\n let expanded = '';\n for (const char of line) {\n if (char === '\\t') {\n const added = 4 - (col % 4);\n expanded += ' '.repeat(added);\n col += added;\n } else {\n expanded += char;\n col++;\n }\n }\n\n return expanded;\n}\n", "import { _defaults } from './defaults.ts';\nimport {\n rtrim,\n splitCells,\n findClosingBracket,\n expandTabs,\n} from './helpers.ts';\nimport type { Rules } from './rules.ts';\nimport type { _Lexer } from './Lexer.ts';\nimport type { Links, Tokens, Token } from './Tokens.ts';\nimport type { MarkedOptions } from './MarkedOptions.ts';\n\nfunction outputLink(cap: string[], link: Pick, raw: string, lexer: _Lexer, rules: Rules): Tokens.Link | Tokens.Image {\n const href = link.href;\n const title = link.title || null;\n const text = cap[1].replace(rules.other.outputLinkReplace, '$1');\n\n lexer.state.inLink = true;\n const token: Tokens.Link | Tokens.Image = {\n type: cap[0].charAt(0) === '!' ? 'image' : 'link',\n raw,\n href,\n title,\n text,\n tokens: lexer.inlineTokens(text),\n };\n lexer.state.inLink = false;\n return token;\n}\n\nfunction indentCodeCompensation(raw: string, text: string, rules: Rules) {\n const matchIndentToCode = raw.match(rules.other.indentCodeCompensation);\n\n if (matchIndentToCode === null) {\n return text;\n }\n\n const indentToCode = matchIndentToCode[1];\n\n return text\n .split('\\n')\n .map(node => {\n const matchIndentInNode = node.match(rules.other.beginningSpace);\n if (matchIndentInNode === null) {\n return node;\n }\n\n const [indentInNode] = matchIndentInNode;\n\n if (indentInNode.length >= indentToCode.length) {\n return node.slice(indentToCode.length);\n }\n\n return node;\n })\n .join('\\n');\n}\n\n/**\n * Tokenizer\n */\nexport class _Tokenizer {\n options: MarkedOptions;\n rules!: Rules; // set by the lexer\n lexer!: _Lexer; // set by the lexer\n\n constructor(options?: MarkedOptions) {\n this.options = options || _defaults;\n }\n\n space(src: string): Tokens.Space | undefined {\n const cap = this.rules.block.newline.exec(src);\n if (cap && cap[0].length > 0) {\n return {\n type: 'space',\n raw: cap[0],\n };\n }\n }\n\n code(src: string): Tokens.Code | undefined {\n const cap = this.rules.block.code.exec(src);\n if (cap) {\n const text = cap[0].replace(this.rules.other.codeRemoveIndent, '');\n return {\n type: 'code',\n raw: cap[0],\n codeBlockStyle: 'indented',\n text: !this.options.pedantic\n ? rtrim(text, '\\n')\n : text,\n };\n }\n }\n\n fences(src: string): Tokens.Code | undefined {\n const cap = this.rules.block.fences.exec(src);\n if (cap) {\n const raw = cap[0];\n const text = indentCodeCompensation(raw, cap[3] || '', this.rules);\n\n return {\n type: 'code',\n raw,\n lang: cap[2] ? cap[2].trim().replace(this.rules.inline.anyPunctuation, '$1') : cap[2],\n text,\n };\n }\n }\n\n heading(src: string): Tokens.Heading | undefined {\n const cap = this.rules.block.heading.exec(src);\n if (cap) {\n let text = cap[2].trim();\n\n // remove trailing #s\n if (this.rules.other.endingHash.test(text)) {\n const trimmed = rtrim(text, '#');\n if (this.options.pedantic) {\n text = trimmed.trim();\n } else if (!trimmed || this.rules.other.endingSpaceChar.test(trimmed)) {\n // CommonMark requires space before trailing #s\n text = trimmed.trim();\n }\n }\n\n return {\n type: 'heading',\n raw: cap[0],\n depth: cap[1].length,\n text,\n tokens: this.lexer.inline(text),\n };\n }\n }\n\n hr(src: string): Tokens.Hr | undefined {\n const cap = this.rules.block.hr.exec(src);\n if (cap) {\n return {\n type: 'hr',\n raw: rtrim(cap[0], '\\n'),\n };\n }\n }\n\n blockquote(src: string): Tokens.Blockquote | undefined {\n const cap = this.rules.block.blockquote.exec(src);\n if (cap) {\n let lines = rtrim(cap[0], '\\n').split('\\n');\n let raw = '';\n let text = '';\n const tokens: Token[] = [];\n\n while (lines.length > 0) {\n let inBlockquote = false;\n const currentLines = [];\n\n let i;\n for (i = 0; i < lines.length; i++) {\n // get lines up to a continuation\n if (this.rules.other.blockquoteStart.test(lines[i])) {\n currentLines.push(lines[i]);\n inBlockquote = true;\n } else if (!inBlockquote) {\n currentLines.push(lines[i]);\n } else {\n break;\n }\n }\n lines = lines.slice(i);\n\n const currentRaw = currentLines.join('\\n');\n const currentText = currentRaw\n // precede setext continuation with 4 spaces so it isn't a setext\n .replace(this.rules.other.blockquoteSetextReplace, '\\n $1')\n .replace(this.rules.other.blockquoteSetextReplace2, '');\n raw = raw ? `${raw}\\n${currentRaw}` : currentRaw;\n text = text ? `${text}\\n${currentText}` : currentText;\n\n // parse blockquote lines as top level tokens\n // merge paragraphs if this is a continuation\n const top = this.lexer.state.top;\n this.lexer.state.top = true;\n this.lexer.blockTokens(currentText, tokens, true);\n this.lexer.state.top = top;\n\n // if there is no continuation then we are done\n if (lines.length === 0) {\n break;\n }\n\n const lastToken = tokens.at(-1);\n\n if (lastToken?.type === 'code') {\n // blockquote continuation cannot be preceded by a code block\n break;\n } else if (lastToken?.type === 'blockquote') {\n // include continuation in nested blockquote\n const oldToken = lastToken as Tokens.Blockquote;\n const newText = oldToken.raw + '\\n' + lines.join('\\n');\n const newToken = this.blockquote(newText)!;\n tokens[tokens.length - 1] = newToken;\n\n raw = raw.substring(0, raw.length - oldToken.raw.length) + newToken.raw;\n text = text.substring(0, text.length - oldToken.text.length) + newToken.text;\n break;\n } else if (lastToken?.type === 'list') {\n // include continuation in nested list\n const oldToken = lastToken as Tokens.List;\n const newText = oldToken.raw + '\\n' + lines.join('\\n');\n const newToken = this.list(newText)!;\n tokens[tokens.length - 1] = newToken;\n\n raw = raw.substring(0, raw.length - lastToken.raw.length) + newToken.raw;\n text = text.substring(0, text.length - oldToken.raw.length) + newToken.raw;\n lines = newText.substring(tokens.at(-1)!.raw.length).split('\\n');\n continue;\n }\n }\n\n return {\n type: 'blockquote',\n raw,\n tokens,\n text,\n };\n }\n }\n\n list(src: string): Tokens.List | undefined {\n let cap = this.rules.block.list.exec(src);\n if (cap) {\n let bull = cap[1].trim();\n const isordered = bull.length > 1;\n\n const list: Tokens.List = {\n type: 'list',\n raw: '',\n ordered: isordered,\n start: isordered ? +bull.slice(0, -1) : '',\n loose: false,\n items: [],\n };\n\n bull = isordered ? `\\\\d{1,9}\\\\${bull.slice(-1)}` : `\\\\${bull}`;\n\n if (this.options.pedantic) {\n bull = isordered ? bull : '[*+-]';\n }\n\n // Get next list item\n const itemRegex = this.rules.other.listItemRegex(bull);\n let endsWithBlankLine = false;\n // Check if current bullet point can start a new List Item\n while (src) {\n let endEarly = false;\n let raw = '';\n let itemContents = '';\n if (!(cap = itemRegex.exec(src))) {\n break;\n }\n\n if (this.rules.block.hr.test(src)) { // End list if bullet was actually HR (possibly move into itemRegex?)\n break;\n }\n\n raw = cap[0];\n src = src.substring(raw.length);\n\n let line = expandTabs(cap[2].split('\\n', 1)[0], cap[1].length);\n let nextLine = src.split('\\n', 1)[0];\n let blankLine = !line.trim();\n\n let indent = 0;\n if (this.options.pedantic) {\n indent = 2;\n itemContents = line.trimStart();\n } else if (blankLine) {\n indent = cap[1].length + 1;\n } else {\n indent = line.search(this.rules.other.nonSpaceChar); // Find first non-space char\n indent = indent > 4 ? 1 : indent; // Treat indented code blocks (> 4 spaces) as having only 1 indent\n itemContents = line.slice(indent);\n indent += cap[1].length;\n }\n\n if (blankLine && this.rules.other.blankLine.test(nextLine)) { // Items begin with at most one blank line\n raw += nextLine + '\\n';\n src = src.substring(nextLine.length + 1);\n endEarly = true;\n }\n\n if (!endEarly) {\n const nextBulletRegex = this.rules.other.nextBulletRegex(indent);\n const hrRegex = this.rules.other.hrRegex(indent);\n const fencesBeginRegex = this.rules.other.fencesBeginRegex(indent);\n const headingBeginRegex = this.rules.other.headingBeginRegex(indent);\n const htmlBeginRegex = this.rules.other.htmlBeginRegex(indent);\n const blockquoteBeginRegex = this.rules.other.blockquoteBeginRegex(indent);\n\n // Check if following lines should be included in List Item\n while (src) {\n const rawLine = src.split('\\n', 1)[0];\n let nextLineWithoutTabs;\n nextLine = rawLine;\n\n // Re-align to follow commonmark nesting rules\n if (this.options.pedantic) {\n nextLine = nextLine.replace(this.rules.other.listReplaceNesting, ' ');\n nextLineWithoutTabs = nextLine;\n } else {\n nextLineWithoutTabs = nextLine.replace(this.rules.other.tabCharGlobal, ' ');\n }\n\n // End list item if found code fences\n if (fencesBeginRegex.test(nextLine)) {\n break;\n }\n\n // End list item if found start of new heading\n if (headingBeginRegex.test(nextLine)) {\n break;\n }\n\n // End list item if found start of html block\n if (htmlBeginRegex.test(nextLine)) {\n break;\n }\n\n // End list item if found start of blockquote\n if (blockquoteBeginRegex.test(nextLine)) {\n break;\n }\n\n // End list item if found start of new bullet\n if (nextBulletRegex.test(nextLine)) {\n break;\n }\n\n // Horizontal rule found\n if (hrRegex.test(nextLine)) {\n break;\n }\n\n if (nextLineWithoutTabs.search(this.rules.other.nonSpaceChar) >= indent || !nextLine.trim()) { // Dedent if possible\n itemContents += '\\n' + nextLineWithoutTabs.slice(indent);\n } else {\n // not enough indentation\n if (blankLine) {\n break;\n }\n\n // paragraph continuation unless last line was a different block level element\n if (line.replace(this.rules.other.tabCharGlobal, ' ').search(this.rules.other.nonSpaceChar) >= 4) { // indented code block\n break;\n }\n if (fencesBeginRegex.test(line)) {\n break;\n }\n if (headingBeginRegex.test(line)) {\n break;\n }\n if (hrRegex.test(line)) {\n break;\n }\n\n itemContents += '\\n' + nextLine;\n }\n\n blankLine = !nextLine.trim();\n\n raw += rawLine + '\\n';\n src = src.substring(rawLine.length + 1);\n line = nextLineWithoutTabs.slice(indent);\n }\n }\n\n if (!list.loose) {\n // If the previous item ended with a blank line, the list is loose\n if (endsWithBlankLine) {\n list.loose = true;\n } else if (this.rules.other.doubleBlankLine.test(raw)) {\n endsWithBlankLine = true;\n }\n }\n\n list.items.push({\n type: 'list_item',\n raw,\n task: !!this.options.gfm && this.rules.other.listIsTask.test(itemContents),\n loose: false,\n text: itemContents,\n tokens: [],\n });\n\n list.raw += raw;\n }\n\n // Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic\n const lastItem = list.items.at(-1);\n if (lastItem) {\n lastItem.raw = lastItem.raw.trimEnd();\n lastItem.text = lastItem.text.trimEnd();\n } else {\n // not a list since there were no items\n return;\n }\n list.raw = list.raw.trimEnd();\n\n // Item child tokens handled here at end because we needed to have the final item to trim it first\n for (const item of list.items) {\n this.lexer.state.top = false;\n item.tokens = this.lexer.blockTokens(item.text, []);\n if (item.task) {\n // Remove checkbox markdown from item tokens\n item.text = item.text.replace(this.rules.other.listReplaceTask, '');\n if (item.tokens[0]?.type === 'text' || item.tokens[0]?.type === 'paragraph') {\n item.tokens[0].raw = item.tokens[0].raw.replace(this.rules.other.listReplaceTask, '');\n item.tokens[0].text = item.tokens[0].text.replace(this.rules.other.listReplaceTask, '');\n for (let i = this.lexer.inlineQueue.length - 1; i >= 0; i--) {\n if (this.rules.other.listIsTask.test(this.lexer.inlineQueue[i].src)) {\n this.lexer.inlineQueue[i].src = this.lexer.inlineQueue[i].src.replace(this.rules.other.listReplaceTask, '');\n break;\n }\n }\n }\n\n const taskRaw = this.rules.other.listTaskCheckbox.exec(item.raw);\n if (taskRaw) {\n const checkboxToken: Tokens.Checkbox = {\n type: 'checkbox',\n raw: taskRaw[0] + ' ',\n checked: taskRaw[0] !== '[ ]',\n };\n item.checked = checkboxToken.checked;\n if (list.loose) {\n if (item.tokens[0] && ['paragraph', 'text'].includes(item.tokens[0].type) && 'tokens' in item.tokens[0] && item.tokens[0].tokens) {\n item.tokens[0].raw = checkboxToken.raw + item.tokens[0].raw;\n item.tokens[0].text = checkboxToken.raw + item.tokens[0].text;\n item.tokens[0].tokens.unshift(checkboxToken);\n } else {\n item.tokens.unshift({\n type: 'paragraph',\n raw: checkboxToken.raw,\n text: checkboxToken.raw,\n tokens: [checkboxToken],\n });\n }\n } else {\n item.tokens.unshift(checkboxToken);\n }\n }\n }\n\n if (!list.loose) {\n // Check if list should be loose\n const spacers = item.tokens.filter(t => t.type === 'space');\n const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => this.rules.other.anyLine.test(t.raw));\n\n list.loose = hasMultipleLineBreaks;\n }\n }\n\n // Set all items to loose if list is loose\n if (list.loose) {\n for (const item of list.items) {\n item.loose = true;\n for (const token of item.tokens) {\n if (token.type === 'text') {\n token.type = 'paragraph';\n }\n }\n }\n }\n\n return list;\n }\n }\n\n html(src: string): Tokens.HTML | undefined {\n const cap = this.rules.block.html.exec(src);\n if (cap) {\n const token: Tokens.HTML = {\n type: 'html',\n block: true,\n raw: cap[0],\n pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style',\n text: cap[0],\n };\n return token;\n }\n }\n\n def(src: string): Tokens.Def | undefined {\n const cap = this.rules.block.def.exec(src);\n if (cap) {\n const tag = cap[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal, ' ');\n const href = cap[2] ? cap[2].replace(this.rules.other.hrefBrackets, '$1').replace(this.rules.inline.anyPunctuation, '$1') : '';\n const title = cap[3] ? cap[3].substring(1, cap[3].length - 1).replace(this.rules.inline.anyPunctuation, '$1') : cap[3];\n return {\n type: 'def',\n tag,\n raw: cap[0],\n href,\n title,\n };\n }\n }\n\n table(src: string): Tokens.Table | undefined {\n const cap = this.rules.block.table.exec(src);\n if (!cap) {\n return;\n }\n\n if (!this.rules.other.tableDelimiter.test(cap[2])) {\n // delimiter row must have a pipe (|) or colon (:) otherwise it is a setext heading\n return;\n }\n\n const headers = splitCells(cap[1]);\n const aligns = cap[2].replace(this.rules.other.tableAlignChars, '').split('|');\n const rows = cap[3]?.trim() ? cap[3].replace(this.rules.other.tableRowBlankLine, '').split('\\n') : [];\n\n const item: Tokens.Table = {\n type: 'table',\n raw: cap[0],\n header: [],\n align: [],\n rows: [],\n };\n\n if (headers.length !== aligns.length) {\n // header and align columns must be equal, rows can be different.\n return;\n }\n\n for (const align of aligns) {\n if (this.rules.other.tableAlignRight.test(align)) {\n item.align.push('right');\n } else if (this.rules.other.tableAlignCenter.test(align)) {\n item.align.push('center');\n } else if (this.rules.other.tableAlignLeft.test(align)) {\n item.align.push('left');\n } else {\n item.align.push(null);\n }\n }\n\n for (let i = 0; i < headers.length; i++) {\n item.header.push({\n text: headers[i],\n tokens: this.lexer.inline(headers[i]),\n header: true,\n align: item.align[i],\n });\n }\n\n for (const row of rows) {\n item.rows.push(splitCells(row, item.header.length).map((cell, i) => {\n return {\n text: cell,\n tokens: this.lexer.inline(cell),\n header: false,\n align: item.align[i],\n };\n }));\n }\n\n return item;\n }\n\n lheading(src: string): Tokens.Heading | undefined {\n const cap = this.rules.block.lheading.exec(src);\n if (cap) {\n return {\n type: 'heading',\n raw: cap[0],\n depth: cap[2].charAt(0) === '=' ? 1 : 2,\n text: cap[1],\n tokens: this.lexer.inline(cap[1]),\n };\n }\n }\n\n paragraph(src: string): Tokens.Paragraph | undefined {\n const cap = this.rules.block.paragraph.exec(src);\n if (cap) {\n const text = cap[1].charAt(cap[1].length - 1) === '\\n'\n ? cap[1].slice(0, -1)\n : cap[1];\n return {\n type: 'paragraph',\n raw: cap[0],\n text,\n tokens: this.lexer.inline(text),\n };\n }\n }\n\n text(src: string): Tokens.Text | undefined {\n const cap = this.rules.block.text.exec(src);\n if (cap) {\n return {\n type: 'text',\n raw: cap[0],\n text: cap[0],\n tokens: this.lexer.inline(cap[0]),\n };\n }\n }\n\n escape(src: string): Tokens.Escape | undefined {\n const cap = this.rules.inline.escape.exec(src);\n if (cap) {\n return {\n type: 'escape',\n raw: cap[0],\n text: cap[1],\n };\n }\n }\n\n tag(src: string): Tokens.Tag | undefined {\n const cap = this.rules.inline.tag.exec(src);\n if (cap) {\n if (!this.lexer.state.inLink && this.rules.other.startATag.test(cap[0])) {\n this.lexer.state.inLink = true;\n } else if (this.lexer.state.inLink && this.rules.other.endATag.test(cap[0])) {\n this.lexer.state.inLink = false;\n }\n if (!this.lexer.state.inRawBlock && this.rules.other.startPreScriptTag.test(cap[0])) {\n this.lexer.state.inRawBlock = true;\n } else if (this.lexer.state.inRawBlock && this.rules.other.endPreScriptTag.test(cap[0])) {\n this.lexer.state.inRawBlock = false;\n }\n\n return {\n type: 'html',\n raw: cap[0],\n inLink: this.lexer.state.inLink,\n inRawBlock: this.lexer.state.inRawBlock,\n block: false,\n text: cap[0],\n };\n }\n }\n\n link(src: string): Tokens.Link | Tokens.Image | undefined {\n const cap = this.rules.inline.link.exec(src);\n if (cap) {\n const trimmedUrl = cap[2].trim();\n if (!this.options.pedantic && this.rules.other.startAngleBracket.test(trimmedUrl)) {\n // commonmark requires matching angle brackets\n if (!(this.rules.other.endAngleBracket.test(trimmedUrl))) {\n return;\n }\n\n // ending angle bracket cannot be escaped\n const rtrimSlash = rtrim(trimmedUrl.slice(0, -1), '\\\\');\n if ((trimmedUrl.length - rtrimSlash.length) % 2 === 0) {\n return;\n }\n } else {\n // find closing parenthesis\n const lastParenIndex = findClosingBracket(cap[2], '()');\n if (lastParenIndex === -2) {\n // more open parens than closed\n return;\n }\n\n if (lastParenIndex > -1) {\n const start = cap[0].indexOf('!') === 0 ? 5 : 4;\n const linkLen = start + cap[1].length + lastParenIndex;\n cap[2] = cap[2].substring(0, lastParenIndex);\n cap[0] = cap[0].substring(0, linkLen).trim();\n cap[3] = '';\n }\n }\n let href = cap[2];\n let title = '';\n if (this.options.pedantic) {\n // split pedantic href and title\n const link = this.rules.other.pedanticHrefTitle.exec(href);\n\n if (link) {\n href = link[1];\n title = link[3];\n }\n } else {\n title = cap[3] ? cap[3].slice(1, -1) : '';\n }\n\n href = href.trim();\n if (this.rules.other.startAngleBracket.test(href)) {\n if (this.options.pedantic && !(this.rules.other.endAngleBracket.test(trimmedUrl))) {\n // pedantic allows starting angle bracket without ending angle bracket\n href = href.slice(1);\n } else {\n href = href.slice(1, -1);\n }\n }\n return outputLink(cap, {\n href: href ? href.replace(this.rules.inline.anyPunctuation, '$1') : href,\n title: title ? title.replace(this.rules.inline.anyPunctuation, '$1') : title,\n }, cap[0], this.lexer, this.rules);\n }\n }\n\n reflink(src: string, links: Links): Tokens.Link | Tokens.Image | Tokens.Text | undefined {\n let cap;\n if ((cap = this.rules.inline.reflink.exec(src))\n || (cap = this.rules.inline.nolink.exec(src))) {\n const linkString = (cap[2] || cap[1]).replace(this.rules.other.multipleSpaceGlobal, ' ');\n const link = links[linkString.toLowerCase()];\n if (!link) {\n const text = cap[0].charAt(0);\n return {\n type: 'text',\n raw: text,\n text,\n };\n }\n return outputLink(cap, link, cap[0], this.lexer, this.rules);\n }\n }\n\n emStrong(src: string, maskedSrc: string, prevChar = ''): Tokens.Em | Tokens.Strong | undefined {\n let match = this.rules.inline.emStrongLDelim.exec(src);\n if (!match) return;\n\n // _ can't be between two alphanumerics. \\p{L}\\p{N} includes non-english alphabet/numbers as well\n if (match[3] && prevChar.match(this.rules.other.unicodeAlphaNumeric)) return;\n\n const nextChar = match[1] || match[2] || '';\n\n if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) {\n // unicode Regex counts emoji as 1 char; spread into array for proper count (used multiple times below)\n const lLength = [...match[0]].length - 1;\n let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0;\n\n const endReg = match[0][0] === '*' ? this.rules.inline.emStrongRDelimAst : this.rules.inline.emStrongRDelimUnd;\n endReg.lastIndex = 0;\n\n // Clip maskedSrc to same section of string as src (move to lexer?)\n maskedSrc = maskedSrc.slice(-1 * src.length + lLength);\n\n while ((match = endReg.exec(maskedSrc)) != null) {\n rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6];\n\n if (!rDelim) continue; // skip single * in __abc*abc__\n\n rLength = [...rDelim].length;\n\n if (match[3] || match[4]) { // found another Left Delim\n delimTotal += rLength;\n continue;\n } else if (match[5] || match[6]) { // either Left or Right Delim\n if (lLength % 3 && !((lLength + rLength) % 3)) {\n midDelimTotal += rLength;\n continue; // CommonMark Emphasis Rules 9-10\n }\n }\n\n delimTotal -= rLength;\n\n if (delimTotal > 0) continue; // Haven't found enough closing delimiters\n\n // Remove extra characters. *a*** -> *a*\n rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal);\n // char length can be >1 for unicode characters;\n const lastCharLength = [...match[0]][0].length;\n const raw = src.slice(0, lLength + match.index + lastCharLength + rLength);\n\n // Create `em` if smallest delimiter has odd char count. *a***\n if (Math.min(lLength, rLength) % 2) {\n const text = raw.slice(1, -1);\n return {\n type: 'em',\n raw,\n text,\n tokens: this.lexer.inlineTokens(text),\n };\n }\n\n // Create 'strong' if smallest delimiter has even char count. **a***\n const text = raw.slice(2, -2);\n return {\n type: 'strong',\n raw,\n text,\n tokens: this.lexer.inlineTokens(text),\n };\n }\n }\n }\n\n codespan(src: string): Tokens.Codespan | undefined {\n const cap = this.rules.inline.code.exec(src);\n if (cap) {\n let text = cap[2].replace(this.rules.other.newLineCharGlobal, ' ');\n const hasNonSpaceChars = this.rules.other.nonSpaceChar.test(text);\n const hasSpaceCharsOnBothEnds = this.rules.other.startingSpaceChar.test(text) && this.rules.other.endingSpaceChar.test(text);\n if (hasNonSpaceChars && hasSpaceCharsOnBothEnds) {\n text = text.substring(1, text.length - 1);\n }\n return {\n type: 'codespan',\n raw: cap[0],\n text,\n };\n }\n }\n\n br(src: string): Tokens.Br | undefined {\n const cap = this.rules.inline.br.exec(src);\n if (cap) {\n return {\n type: 'br',\n raw: cap[0],\n };\n }\n }\n\n del(src: string, maskedSrc: string, prevChar = ''): Tokens.Del | undefined {\n let match = this.rules.inline.delLDelim.exec(src);\n if (!match) return;\n\n const nextChar = match[1] || '';\n\n if (!nextChar || !prevChar || this.rules.inline.punctuation.exec(prevChar)) {\n // unicode Regex counts emoji as 1 char; spread into array for proper count\n const lLength = [...match[0]].length - 1;\n let rDelim, rLength, delimTotal = lLength;\n\n const endReg = this.rules.inline.delRDelim;\n endReg.lastIndex = 0;\n\n // Clip maskedSrc to same section of string as src\n maskedSrc = maskedSrc.slice(-1 * src.length + lLength);\n\n while ((match = endReg.exec(maskedSrc)) != null) {\n rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6];\n\n if (!rDelim) continue;\n\n rLength = [...rDelim].length;\n\n if (rLength !== lLength) continue;\n\n if (match[3] || match[4]) { // found another Left Delim\n delimTotal += rLength;\n continue;\n }\n\n delimTotal -= rLength;\n\n if (delimTotal > 0) continue; // Haven't found enough closing delimiters\n\n // Remove extra characters\n rLength = Math.min(rLength, rLength + delimTotal);\n // char length can be >1 for unicode characters\n const lastCharLength = [...match[0]][0].length;\n const raw = src.slice(0, lLength + match.index + lastCharLength + rLength);\n\n // Create del token - only single ~ or double ~~ supported\n const text = raw.slice(lLength, -lLength);\n return {\n type: 'del',\n raw,\n text,\n tokens: this.lexer.inlineTokens(text),\n };\n }\n }\n }\n\n autolink(src: string): Tokens.Link | undefined {\n const cap = this.rules.inline.autolink.exec(src);\n if (cap) {\n let text, href;\n if (cap[2] === '@') {\n text = cap[1];\n href = 'mailto:' + text;\n } else {\n text = cap[1];\n href = text;\n }\n\n return {\n type: 'link',\n raw: cap[0],\n text,\n href,\n tokens: [\n {\n type: 'text',\n raw: text,\n text,\n },\n ],\n };\n }\n }\n\n url(src: string): Tokens.Link | undefined {\n let cap;\n if (cap = this.rules.inline.url.exec(src)) {\n let text, href;\n if (cap[2] === '@') {\n text = cap[0];\n href = 'mailto:' + text;\n } else {\n // do extended autolink path validation\n let prevCapZero;\n do {\n prevCapZero = cap[0];\n cap[0] = this.rules.inline._backpedal.exec(cap[0])?.[0] ?? '';\n } while (prevCapZero !== cap[0]);\n text = cap[0];\n if (cap[1] === 'www.') {\n href = 'http://' + cap[0];\n } else {\n href = cap[0];\n }\n }\n return {\n type: 'link',\n raw: cap[0],\n text,\n href,\n tokens: [\n {\n type: 'text',\n raw: text,\n text,\n },\n ],\n };\n }\n }\n\n inlineText(src: string): Tokens.Text | undefined {\n const cap = this.rules.inline.text.exec(src);\n if (cap) {\n const escaped = this.lexer.state.inRawBlock;\n return {\n type: 'text',\n raw: cap[0],\n text: cap[0],\n escaped,\n };\n }\n }\n}\n", "import { _Tokenizer } from './Tokenizer.ts';\nimport { _defaults } from './defaults.ts';\nimport { other, block, inline } from './rules.ts';\nimport type { Token, TokensList, Tokens } from './Tokens.ts';\nimport type { MarkedOptions } from './MarkedOptions.ts';\n\n/**\n * Block Lexer\n */\nexport class _Lexer {\n tokens: TokensList;\n options: MarkedOptions;\n state: {\n inLink: boolean;\n inRawBlock: boolean;\n top: boolean;\n };\n\n public inlineQueue: { src: string, tokens: Token[] }[];\n\n private tokenizer: _Tokenizer;\n\n constructor(options?: MarkedOptions) {\n // TokenList cannot be created in one go\n this.tokens = [] as unknown as TokensList;\n this.tokens.links = Object.create(null);\n this.options = options || _defaults;\n this.options.tokenizer = this.options.tokenizer || new _Tokenizer();\n this.tokenizer = this.options.tokenizer;\n this.tokenizer.options = this.options;\n this.tokenizer.lexer = this;\n this.inlineQueue = [];\n this.state = {\n inLink: false,\n inRawBlock: false,\n top: true,\n };\n\n const rules = {\n other,\n block: block.normal,\n inline: inline.normal,\n };\n\n if (this.options.pedantic) {\n rules.block = block.pedantic;\n rules.inline = inline.pedantic;\n } else if (this.options.gfm) {\n rules.block = block.gfm;\n if (this.options.breaks) {\n rules.inline = inline.breaks;\n } else {\n rules.inline = inline.gfm;\n }\n }\n this.tokenizer.rules = rules;\n }\n\n /**\n * Expose Rules\n */\n static get rules() {\n return {\n block,\n inline,\n };\n }\n\n /**\n * Static Lex Method\n */\n static lex(src: string, options?: MarkedOptions) {\n const lexer = new _Lexer(options);\n return lexer.lex(src);\n }\n\n /**\n * Static Lex Inline Method\n */\n static lexInline(src: string, options?: MarkedOptions) {\n const lexer = new _Lexer(options);\n return lexer.inlineTokens(src);\n }\n\n /**\n * Preprocessing\n */\n lex(src: string) {\n src = src.replace(other.carriageReturn, '\\n');\n\n this.blockTokens(src, this.tokens);\n\n for (let i = 0; i < this.inlineQueue.length; i++) {\n const next = this.inlineQueue[i];\n this.inlineTokens(next.src, next.tokens);\n }\n this.inlineQueue = [];\n\n return this.tokens;\n }\n\n /**\n * Lexing\n */\n blockTokens(src: string, tokens?: Token[], lastParagraphClipped?: boolean): Token[];\n blockTokens(src: string, tokens?: TokensList, lastParagraphClipped?: boolean): TokensList;\n blockTokens(src: string, tokens: Token[] = [], lastParagraphClipped = false) {\n if (this.options.pedantic) {\n src = src.replace(other.tabCharGlobal, ' ').replace(other.spaceLine, '');\n }\n\n while (src) {\n let token: Tokens.Generic | undefined;\n\n if (this.options.extensions?.block?.some((extTokenizer) => {\n if (token = extTokenizer.call({ lexer: this }, src, tokens)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n return true;\n }\n return false;\n })) {\n continue;\n }\n\n // newline\n if (token = this.tokenizer.space(src)) {\n src = src.substring(token.raw.length);\n const lastToken = tokens.at(-1);\n if (token.raw.length === 1 && lastToken !== undefined) {\n // if there's a single \\n as a spacer, it's terminating the last line,\n // so move it there so that we don't get unnecessary paragraph tags\n lastToken.raw += '\\n';\n } else {\n tokens.push(token);\n }\n continue;\n }\n\n // code\n if (token = this.tokenizer.code(src)) {\n src = src.substring(token.raw.length);\n const lastToken = tokens.at(-1);\n // An indented code block cannot interrupt a paragraph.\n if (lastToken?.type === 'paragraph' || lastToken?.type === 'text') {\n lastToken.raw += (lastToken.raw.endsWith('\\n') ? '' : '\\n') + token.raw;\n lastToken.text += '\\n' + token.text;\n this.inlineQueue.at(-1)!.src = lastToken.text;\n } else {\n tokens.push(token);\n }\n continue;\n }\n\n // fences\n if (token = this.tokenizer.fences(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // heading\n if (token = this.tokenizer.heading(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // hr\n if (token = this.tokenizer.hr(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // blockquote\n if (token = this.tokenizer.blockquote(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // list\n if (token = this.tokenizer.list(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // html\n if (token = this.tokenizer.html(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // def\n if (token = this.tokenizer.def(src)) {\n src = src.substring(token.raw.length);\n const lastToken = tokens.at(-1);\n if (lastToken?.type === 'paragraph' || lastToken?.type === 'text') {\n lastToken.raw += (lastToken.raw.endsWith('\\n') ? '' : '\\n') + token.raw;\n lastToken.text += '\\n' + token.raw;\n this.inlineQueue.at(-1)!.src = lastToken.text;\n } else if (!this.tokens.links[token.tag]) {\n this.tokens.links[token.tag] = {\n href: token.href,\n title: token.title,\n };\n tokens.push(token);\n }\n continue;\n }\n\n // table (gfm)\n if (token = this.tokenizer.table(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // lheading\n if (token = this.tokenizer.lheading(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // top-level paragraph\n // prevent paragraph consuming extensions by clipping 'src' to extension start\n let cutSrc = src;\n if (this.options.extensions?.startBlock) {\n let startIndex = Infinity;\n const tempSrc = src.slice(1);\n let tempStart;\n this.options.extensions.startBlock.forEach((getStartIndex) => {\n tempStart = getStartIndex.call({ lexer: this }, tempSrc);\n if (typeof tempStart === 'number' && tempStart >= 0) {\n startIndex = Math.min(startIndex, tempStart);\n }\n });\n if (startIndex < Infinity && startIndex >= 0) {\n cutSrc = src.substring(0, startIndex + 1);\n }\n }\n if (this.state.top && (token = this.tokenizer.paragraph(cutSrc))) {\n const lastToken = tokens.at(-1);\n if (lastParagraphClipped && lastToken?.type === 'paragraph') {\n lastToken.raw += (lastToken.raw.endsWith('\\n') ? '' : '\\n') + token.raw;\n lastToken.text += '\\n' + token.text;\n this.inlineQueue.pop();\n this.inlineQueue.at(-1)!.src = lastToken.text;\n } else {\n tokens.push(token);\n }\n lastParagraphClipped = cutSrc.length !== src.length;\n src = src.substring(token.raw.length);\n continue;\n }\n\n // text\n if (token = this.tokenizer.text(src)) {\n src = src.substring(token.raw.length);\n const lastToken = tokens.at(-1);\n if (lastToken?.type === 'text') {\n lastToken.raw += (lastToken.raw.endsWith('\\n') ? '' : '\\n') + token.raw;\n lastToken.text += '\\n' + token.text;\n this.inlineQueue.pop();\n this.inlineQueue.at(-1)!.src = lastToken.text;\n } else {\n tokens.push(token);\n }\n continue;\n }\n\n if (src) {\n const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0);\n if (this.options.silent) {\n console.error(errMsg);\n break;\n } else {\n throw new Error(errMsg);\n }\n }\n }\n\n this.state.top = true;\n return tokens;\n }\n\n inline(src: string, tokens: Token[] = []) {\n this.inlineQueue.push({ src, tokens });\n return tokens;\n }\n\n /**\n * Lexing/Compiling\n */\n inlineTokens(src: string, tokens: Token[] = []): Token[] {\n // String with links masked to avoid interference with em and strong\n let maskedSrc = src;\n let match: RegExpExecArray | null = null;\n\n // Mask out reflinks\n if (this.tokens.links) {\n const links = Object.keys(this.tokens.links);\n if (links.length > 0) {\n while ((match = this.tokenizer.rules.inline.reflinkSearch.exec(maskedSrc)) != null) {\n if (links.includes(match[0].slice(match[0].lastIndexOf('[') + 1, -1))) {\n maskedSrc = maskedSrc.slice(0, match.index)\n + '[' + 'a'.repeat(match[0].length - 2) + ']'\n + maskedSrc.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex);\n }\n }\n }\n }\n\n // Mask out escaped characters\n while ((match = this.tokenizer.rules.inline.anyPunctuation.exec(maskedSrc)) != null) {\n maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);\n }\n\n // Mask out other blocks\n let offset;\n while ((match = this.tokenizer.rules.inline.blockSkip.exec(maskedSrc)) != null) {\n offset = match[2] ? match[2].length : 0;\n maskedSrc = maskedSrc.slice(0, match.index + offset) + '[' + 'a'.repeat(match[0].length - offset - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);\n }\n\n // Mask out blocks from extensions\n maskedSrc = this.options.hooks?.emStrongMask?.call({ lexer: this }, maskedSrc) ?? maskedSrc;\n\n let keepPrevChar = false;\n let prevChar = '';\n while (src) {\n if (!keepPrevChar) {\n prevChar = '';\n }\n keepPrevChar = false;\n\n let token: Tokens.Generic | undefined;\n\n // extensions\n if (this.options.extensions?.inline?.some((extTokenizer) => {\n if (token = extTokenizer.call({ lexer: this }, src, tokens)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n return true;\n }\n return false;\n })) {\n continue;\n }\n\n // escape\n if (token = this.tokenizer.escape(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // tag\n if (token = this.tokenizer.tag(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // link\n if (token = this.tokenizer.link(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // reflink, nolink\n if (token = this.tokenizer.reflink(src, this.tokens.links)) {\n src = src.substring(token.raw.length);\n const lastToken = tokens.at(-1);\n if (token.type === 'text' && lastToken?.type === 'text') {\n lastToken.raw += token.raw;\n lastToken.text += token.text;\n } else {\n tokens.push(token);\n }\n continue;\n }\n\n // em & strong\n if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // code\n if (token = this.tokenizer.codespan(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // br\n if (token = this.tokenizer.br(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // del (gfm)\n if (token = this.tokenizer.del(src, maskedSrc, prevChar)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // autolink\n if (token = this.tokenizer.autolink(src)) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // url (gfm)\n if (!this.state.inLink && (token = this.tokenizer.url(src))) {\n src = src.substring(token.raw.length);\n tokens.push(token);\n continue;\n }\n\n // text\n // prevent inlineText consuming extensions by clipping 'src' to extension start\n let cutSrc = src;\n if (this.options.extensions?.startInline) {\n let startIndex = Infinity;\n const tempSrc = src.slice(1);\n let tempStart;\n this.options.extensions.startInline.forEach((getStartIndex) => {\n tempStart = getStartIndex.call({ lexer: this }, tempSrc);\n if (typeof tempStart === 'number' && tempStart >= 0) {\n startIndex = Math.min(startIndex, tempStart);\n }\n });\n if (startIndex < Infinity && startIndex >= 0) {\n cutSrc = src.substring(0, startIndex + 1);\n }\n }\n if (token = this.tokenizer.inlineText(cutSrc)) {\n src = src.substring(token.raw.length);\n if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started\n prevChar = token.raw.slice(-1);\n }\n keepPrevChar = true;\n const lastToken = tokens.at(-1);\n if (lastToken?.type === 'text') {\n lastToken.raw += token.raw;\n lastToken.text += token.text;\n } else {\n tokens.push(token);\n }\n continue;\n }\n\n if (src) {\n const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0);\n if (this.options.silent) {\n console.error(errMsg);\n break;\n } else {\n throw new Error(errMsg);\n }\n }\n }\n\n return tokens;\n }\n}\n", "import { _defaults } from './defaults.ts';\nimport {\n cleanUrl,\n escapeHtmlEntities,\n} from './helpers.ts';\nimport { other } from './rules.ts';\nimport type { MarkedOptions } from './MarkedOptions.ts';\nimport type { Tokens } from './Tokens.ts';\nimport type { _Parser } from './Parser.ts';\n\n/**\n * Renderer\n */\nexport class _Renderer {\n options: MarkedOptions;\n parser!: _Parser; // set by the parser\n constructor(options?: MarkedOptions) {\n this.options = options || _defaults;\n }\n\n space(token: Tokens.Space): RendererOutput {\n return '' as RendererOutput;\n }\n\n code({ text, lang, escaped }: Tokens.Code): RendererOutput {\n const langString = (lang || '').match(other.notSpaceStart)?.[0];\n\n const code = text.replace(other.endingNewline, '') + '\\n';\n\n if (!langString) {\n return '
'\n        + (escaped ? code : escapeHtmlEntities(code, true))\n        + '
\\n' as RendererOutput;\n }\n\n return '
'\n      + (escaped ? code : escapeHtmlEntities(code, true))\n      + '
\\n' as RendererOutput;\n }\n\n blockquote({ tokens }: Tokens.Blockquote): RendererOutput {\n const body = this.parser.parse(tokens);\n return `
\\n${body}
\\n` as RendererOutput;\n }\n\n html({ text }: Tokens.HTML | Tokens.Tag): RendererOutput {\n return text as RendererOutput;\n }\n\n def(token: Tokens.Def): RendererOutput {\n return '' as RendererOutput;\n }\n\n heading({ tokens, depth }: Tokens.Heading): RendererOutput {\n return `${this.parser.parseInline(tokens)}\\n` as RendererOutput;\n }\n\n hr(token: Tokens.Hr): RendererOutput {\n return '
\\n' as RendererOutput;\n }\n\n list(token: Tokens.List): RendererOutput {\n const ordered = token.ordered;\n const start = token.start;\n\n let body = '';\n for (let j = 0; j < token.items.length; j++) {\n const item = token.items[j];\n body += this.listitem(item);\n }\n\n const type = ordered ? 'ol' : 'ul';\n const startAttr = (ordered && start !== 1) ? (' start=\"' + start + '\"') : '';\n return '<' + type + startAttr + '>\\n' + body + '\\n' as RendererOutput;\n }\n\n listitem(item: Tokens.ListItem): RendererOutput {\n return `
  • ${this.parser.parse(item.tokens)}
  • \\n` as RendererOutput;\n }\n\n checkbox({ checked }: Tokens.Checkbox): RendererOutput {\n return ' ' as RendererOutput;\n }\n\n paragraph({ tokens }: Tokens.Paragraph): RendererOutput {\n return `

    ${this.parser.parseInline(tokens)}

    \\n` as RendererOutput;\n }\n\n table(token: Tokens.Table): RendererOutput {\n let header = '';\n\n // header\n let cell = '';\n for (let j = 0; j < token.header.length; j++) {\n cell += this.tablecell(token.header[j]);\n }\n header += this.tablerow({ text: cell as ParserOutput });\n\n let body = '';\n for (let j = 0; j < token.rows.length; j++) {\n const row = token.rows[j];\n\n cell = '';\n for (let k = 0; k < row.length; k++) {\n cell += this.tablecell(row[k]);\n }\n\n body += this.tablerow({ text: cell as ParserOutput });\n }\n if (body) body = `${body}`;\n\n return '\\n'\n + '\\n'\n + header\n + '\\n'\n + body\n + '
    \\n' as RendererOutput;\n }\n\n tablerow({ text }: Tokens.TableRow): RendererOutput {\n return `\\n${text}\\n` as RendererOutput;\n }\n\n tablecell(token: Tokens.TableCell): RendererOutput {\n const content = this.parser.parseInline(token.tokens);\n const type = token.header ? 'th' : 'td';\n const tag = token.align\n ? `<${type} align=\"${token.align}\">`\n : `<${type}>`;\n return tag + content + `\\n` as RendererOutput;\n }\n\n /**\n * span level renderer\n */\n strong({ tokens }: Tokens.Strong): RendererOutput {\n return `${this.parser.parseInline(tokens)}` as RendererOutput;\n }\n\n em({ tokens }: Tokens.Em): RendererOutput {\n return `${this.parser.parseInline(tokens)}` as RendererOutput;\n }\n\n codespan({ text }: Tokens.Codespan): RendererOutput {\n return `${escapeHtmlEntities(text, true)}` as RendererOutput;\n }\n\n br(token: Tokens.Br): RendererOutput {\n return '
    ' as RendererOutput;\n }\n\n del({ tokens }: Tokens.Del): RendererOutput {\n return `${this.parser.parseInline(tokens)}` as RendererOutput;\n }\n\n link({ href, title, tokens }: Tokens.Link): RendererOutput {\n const text = this.parser.parseInline(tokens) as string;\n const cleanHref = cleanUrl(href);\n if (cleanHref === null) {\n return text as RendererOutput;\n }\n href = cleanHref;\n let out = '
    ';\n return out as RendererOutput;\n }\n\n image({ href, title, text, tokens }: Tokens.Image): RendererOutput {\n if (tokens) {\n text = this.parser.parseInline(tokens, this.parser.textRenderer) as string;\n }\n const cleanHref = cleanUrl(href);\n if (cleanHref === null) {\n return escapeHtmlEntities(text) as RendererOutput;\n }\n href = cleanHref;\n\n let out = `\"${escapeHtmlEntities(text)}\"`;\n {\n // no need for block level renderers\n strong({ text }: Tokens.Strong): RendererOutput {\n return text as RendererOutput;\n }\n\n em({ text }: Tokens.Em): RendererOutput {\n return text as RendererOutput;\n }\n\n codespan({ text }: Tokens.Codespan): RendererOutput {\n return text as RendererOutput;\n }\n\n del({ text }: Tokens.Del): RendererOutput {\n return text as RendererOutput;\n }\n\n html({ text }: Tokens.HTML | Tokens.Tag): RendererOutput {\n return text as RendererOutput;\n }\n\n text({ text }: Tokens.Text | Tokens.Escape | Tokens.Tag): RendererOutput {\n return text as RendererOutput;\n }\n\n link({ text }: Tokens.Link): RendererOutput {\n return '' + text as RendererOutput;\n }\n\n image({ text }: Tokens.Image): RendererOutput {\n return '' + text as RendererOutput;\n }\n\n br(): RendererOutput {\n return '' as RendererOutput;\n }\n\n checkbox({ raw }: Tokens.Checkbox): RendererOutput {\n return raw as RendererOutput;\n }\n}\n", "import { _Renderer } from './Renderer.ts';\nimport { _TextRenderer } from './TextRenderer.ts';\nimport { _defaults } from './defaults.ts';\nimport type { MarkedToken, Token, Tokens } from './Tokens.ts';\nimport type { MarkedOptions } from './MarkedOptions.ts';\n\n/**\n * Parsing & Compiling\n */\nexport class _Parser {\n options: MarkedOptions;\n renderer: _Renderer;\n textRenderer: _TextRenderer;\n constructor(options?: MarkedOptions) {\n this.options = options || _defaults;\n this.options.renderer = this.options.renderer || new _Renderer();\n this.renderer = this.options.renderer;\n this.renderer.options = this.options;\n this.renderer.parser = this;\n this.textRenderer = new _TextRenderer();\n }\n\n /**\n * Static Parse Method\n */\n static parse(tokens: Token[], options?: MarkedOptions) {\n const parser = new _Parser(options);\n return parser.parse(tokens);\n }\n\n /**\n * Static Parse Inline Method\n */\n static parseInline(tokens: Token[], options?: MarkedOptions) {\n const parser = new _Parser(options);\n return parser.parseInline(tokens);\n }\n\n /**\n * Parse Loop\n */\n parse(tokens: Token[]): ParserOutput {\n let out = '';\n\n for (let i = 0; i < tokens.length; i++) {\n const anyToken = tokens[i];\n\n // Run any renderer extensions\n if (this.options.extensions?.renderers?.[anyToken.type]) {\n const genericToken = anyToken as Tokens.Generic;\n const ret = this.options.extensions.renderers[genericToken.type].call({ parser: this }, genericToken);\n if (ret !== false || !['space', 'hr', 'heading', 'code', 'table', 'blockquote', 'list', 'html', 'def', 'paragraph', 'text'].includes(genericToken.type)) {\n out += ret || '';\n continue;\n }\n }\n\n const token = anyToken as MarkedToken;\n\n switch (token.type) {\n case 'space': {\n out += this.renderer.space(token);\n break;\n }\n case 'hr': {\n out += this.renderer.hr(token);\n break;\n }\n case 'heading': {\n out += this.renderer.heading(token);\n break;\n }\n case 'code': {\n out += this.renderer.code(token);\n break;\n }\n case 'table': {\n out += this.renderer.table(token);\n break;\n }\n case 'blockquote': {\n out += this.renderer.blockquote(token);\n break;\n }\n case 'list': {\n out += this.renderer.list(token);\n break;\n }\n case 'checkbox': {\n out += this.renderer.checkbox(token);\n break;\n }\n case 'html': {\n out += this.renderer.html(token);\n break;\n }\n case 'def': {\n out += this.renderer.def(token);\n break;\n }\n case 'paragraph': {\n out += this.renderer.paragraph(token);\n break;\n }\n case 'text': {\n out += this.renderer.text(token);\n break;\n }\n\n default: {\n const errMsg = 'Token with \"' + token.type + '\" type was not found.';\n if (this.options.silent) {\n console.error(errMsg);\n return '' as ParserOutput;\n } else {\n throw new Error(errMsg);\n }\n }\n }\n }\n\n return out as ParserOutput;\n }\n\n /**\n * Parse Inline Tokens\n */\n parseInline(tokens: Token[], renderer: _Renderer | _TextRenderer = this.renderer): ParserOutput {\n let out = '';\n\n for (let i = 0; i < tokens.length; i++) {\n const anyToken = tokens[i];\n\n // Run any renderer extensions\n if (this.options.extensions?.renderers?.[anyToken.type]) {\n const ret = this.options.extensions.renderers[anyToken.type].call({ parser: this }, anyToken);\n if (ret !== false || !['escape', 'html', 'link', 'image', 'strong', 'em', 'codespan', 'br', 'del', 'text'].includes(anyToken.type)) {\n out += ret || '';\n continue;\n }\n }\n\n const token = anyToken as MarkedToken;\n\n switch (token.type) {\n case 'escape': {\n out += renderer.text(token);\n break;\n }\n case 'html': {\n out += renderer.html(token);\n break;\n }\n case 'link': {\n out += renderer.link(token);\n break;\n }\n case 'image': {\n out += renderer.image(token);\n break;\n }\n case 'checkbox': {\n out += renderer.checkbox(token);\n break;\n }\n case 'strong': {\n out += renderer.strong(token);\n break;\n }\n case 'em': {\n out += renderer.em(token);\n break;\n }\n case 'codespan': {\n out += renderer.codespan(token);\n break;\n }\n case 'br': {\n out += renderer.br(token);\n break;\n }\n case 'del': {\n out += renderer.del(token);\n break;\n }\n case 'text': {\n out += renderer.text(token);\n break;\n }\n default: {\n const errMsg = 'Token with \"' + token.type + '\" type was not found.';\n if (this.options.silent) {\n console.error(errMsg);\n return '' as ParserOutput;\n } else {\n throw new Error(errMsg);\n }\n }\n }\n }\n return out as ParserOutput;\n }\n}\n", "import { _defaults } from './defaults.ts';\nimport { _Lexer } from './Lexer.ts';\nimport { _Parser } from './Parser.ts';\nimport type { MarkedOptions } from './MarkedOptions.ts';\nimport type { Token, TokensList } from './Tokens.ts';\n\nexport class _Hooks {\n options: MarkedOptions;\n block?: boolean;\n\n constructor(options?: MarkedOptions) {\n this.options = options || _defaults;\n }\n\n static passThroughHooks = new Set([\n 'preprocess',\n 'postprocess',\n 'processAllTokens',\n 'emStrongMask',\n ]);\n\n static passThroughHooksRespectAsync = new Set([\n 'preprocess',\n 'postprocess',\n 'processAllTokens',\n ]);\n\n /**\n * Process markdown before marked\n */\n preprocess(markdown: string) {\n return markdown;\n }\n\n /**\n * Process HTML after marked is finished\n */\n postprocess(html: ParserOutput) {\n return html;\n }\n\n /**\n * Process all tokens before walk tokens\n */\n processAllTokens(tokens: Token[] | TokensList) {\n return tokens;\n }\n\n /**\n * Mask contents that should not be interpreted as em/strong delimiters\n */\n emStrongMask(src: string) {\n return src;\n }\n\n /**\n * Provide function to tokenize markdown\n */\n provideLexer() {\n return this.block ? _Lexer.lex : _Lexer.lexInline;\n }\n\n /**\n * Provide function to parse tokens\n */\n provideParser() {\n return this.block ? _Parser.parse : _Parser.parseInline;\n }\n}\n", "import { _getDefaults } from './defaults.ts';\nimport { _Lexer } from './Lexer.ts';\nimport { _Parser } from './Parser.ts';\nimport { _Hooks } from './Hooks.ts';\nimport { _Renderer } from './Renderer.ts';\nimport { _Tokenizer } from './Tokenizer.ts';\nimport { _TextRenderer } from './TextRenderer.ts';\nimport { escapeHtmlEntities } from './helpers.ts';\nimport type { MarkedExtension, MarkedOptions } from './MarkedOptions.ts';\nimport type { Token, Tokens, TokensList } from './Tokens.ts';\n\nexport type MaybePromise = void | Promise;\n\ntype UnknownFunction = (...args: unknown[]) => unknown;\ntype GenericRendererFunction = (...args: unknown[]) => string | false;\n\nexport class Marked {\n defaults = _getDefaults();\n options = this.setOptions;\n\n parse = this.parseMarkdown(true);\n parseInline = this.parseMarkdown(false);\n\n Parser = _Parser;\n Renderer = _Renderer;\n TextRenderer = _TextRenderer;\n Lexer = _Lexer;\n Tokenizer = _Tokenizer;\n Hooks = _Hooks;\n\n constructor(...args: MarkedExtension[]) {\n this.use(...args);\n }\n\n /**\n * Run callback for every token\n */\n walkTokens(tokens: Token[] | TokensList, callback: (token: Token) => MaybePromise | MaybePromise[]) {\n let values: MaybePromise[] = [];\n for (const token of tokens) {\n values = values.concat(callback.call(this, token));\n switch (token.type) {\n case 'table': {\n const tableToken = token as Tokens.Table;\n for (const cell of tableToken.header) {\n values = values.concat(this.walkTokens(cell.tokens, callback));\n }\n for (const row of tableToken.rows) {\n for (const cell of row) {\n values = values.concat(this.walkTokens(cell.tokens, callback));\n }\n }\n break;\n }\n case 'list': {\n const listToken = token as Tokens.List;\n values = values.concat(this.walkTokens(listToken.items, callback));\n break;\n }\n default: {\n const genericToken = token as Tokens.Generic;\n if (this.defaults.extensions?.childTokens?.[genericToken.type]) {\n this.defaults.extensions.childTokens[genericToken.type].forEach((childTokens) => {\n const tokens = genericToken[childTokens].flat(Infinity) as Token[] | TokensList;\n values = values.concat(this.walkTokens(tokens, callback));\n });\n } else if (genericToken.tokens) {\n values = values.concat(this.walkTokens(genericToken.tokens, callback));\n }\n }\n }\n }\n return values;\n }\n\n use(...args: MarkedExtension[]) {\n const extensions: MarkedOptions['extensions'] = this.defaults.extensions || { renderers: {}, childTokens: {} };\n\n args.forEach((pack) => {\n // copy options to new object\n const opts = { ...pack } as MarkedOptions;\n\n // set async to true if it was set to true before\n opts.async = this.defaults.async || opts.async || false;\n\n // ==-- Parse \"addon\" extensions --== //\n if (pack.extensions) {\n pack.extensions.forEach((ext) => {\n if (!ext.name) {\n throw new Error('extension name required');\n }\n if ('renderer' in ext) { // Renderer extensions\n const prevRenderer = extensions.renderers[ext.name];\n if (prevRenderer) {\n // Replace extension with func to run new extension but fall back if false\n extensions.renderers[ext.name] = function(...args) {\n let ret = ext.renderer.apply(this, args);\n if (ret === false) {\n ret = prevRenderer.apply(this, args);\n }\n return ret;\n };\n } else {\n extensions.renderers[ext.name] = ext.renderer;\n }\n }\n if ('tokenizer' in ext) { // Tokenizer Extensions\n if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) {\n throw new Error(\"extension level must be 'block' or 'inline'\");\n }\n const extLevel = extensions[ext.level];\n if (extLevel) {\n extLevel.unshift(ext.tokenizer);\n } else {\n extensions[ext.level] = [ext.tokenizer];\n }\n if (ext.start) { // Function to check for start of token\n if (ext.level === 'block') {\n if (extensions.startBlock) {\n extensions.startBlock.push(ext.start);\n } else {\n extensions.startBlock = [ext.start];\n }\n } else if (ext.level === 'inline') {\n if (extensions.startInline) {\n extensions.startInline.push(ext.start);\n } else {\n extensions.startInline = [ext.start];\n }\n }\n }\n }\n if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens\n extensions.childTokens[ext.name] = ext.childTokens;\n }\n });\n opts.extensions = extensions;\n }\n\n // ==-- Parse \"overwrite\" extensions --== //\n if (pack.renderer) {\n const renderer = this.defaults.renderer || new _Renderer(this.defaults);\n for (const prop in pack.renderer) {\n if (!(prop in renderer)) {\n throw new Error(`renderer '${prop}' does not exist`);\n }\n if (['options', 'parser'].includes(prop)) {\n // ignore options property\n continue;\n }\n const rendererProp = prop as Exclude, 'options' | 'parser'>;\n const rendererFunc = pack.renderer[rendererProp] as GenericRendererFunction;\n const prevRenderer = renderer[rendererProp] as GenericRendererFunction;\n // Replace renderer with func to run extension, but fall back if false\n renderer[rendererProp] = (...args: unknown[]) => {\n let ret = rendererFunc.apply(renderer, args);\n if (ret === false) {\n ret = prevRenderer.apply(renderer, args);\n }\n return (ret || '') as RendererOutput;\n };\n }\n opts.renderer = renderer;\n }\n if (pack.tokenizer) {\n const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults);\n for (const prop in pack.tokenizer) {\n if (!(prop in tokenizer)) {\n throw new Error(`tokenizer '${prop}' does not exist`);\n }\n if (['options', 'rules', 'lexer'].includes(prop)) {\n // ignore options, rules, and lexer properties\n continue;\n }\n const tokenizerProp = prop as Exclude, 'options' | 'rules' | 'lexer'>;\n const tokenizerFunc = pack.tokenizer[tokenizerProp] as UnknownFunction;\n const prevTokenizer = tokenizer[tokenizerProp] as UnknownFunction;\n // Replace tokenizer with func to run extension, but fall back if false\n // @ts-expect-error cannot type tokenizer function dynamically\n tokenizer[tokenizerProp] = (...args: unknown[]) => {\n let ret = tokenizerFunc.apply(tokenizer, args);\n if (ret === false) {\n ret = prevTokenizer.apply(tokenizer, args);\n }\n return ret;\n };\n }\n opts.tokenizer = tokenizer;\n }\n\n // ==-- Parse Hooks extensions --== //\n if (pack.hooks) {\n const hooks = this.defaults.hooks || new _Hooks();\n for (const prop in pack.hooks) {\n if (!(prop in hooks)) {\n throw new Error(`hook '${prop}' does not exist`);\n }\n if (['options', 'block'].includes(prop)) {\n // ignore options and block properties\n continue;\n }\n const hooksProp = prop as Exclude, 'options' | 'block'>;\n const hooksFunc = pack.hooks[hooksProp] as UnknownFunction;\n const prevHook = hooks[hooksProp] as UnknownFunction;\n if (_Hooks.passThroughHooks.has(prop)) {\n // @ts-expect-error cannot type hook function dynamically\n hooks[hooksProp] = (arg: unknown) => {\n if (this.defaults.async && _Hooks.passThroughHooksRespectAsync.has(prop)) {\n return (async() => {\n const ret = await hooksFunc.call(hooks, arg);\n return prevHook.call(hooks, ret);\n })();\n }\n\n const ret = hooksFunc.call(hooks, arg);\n return prevHook.call(hooks, ret);\n };\n } else {\n // @ts-expect-error cannot type hook function dynamically\n hooks[hooksProp] = (...args: unknown[]) => {\n if (this.defaults.async) {\n return (async() => {\n let ret = await hooksFunc.apply(hooks, args);\n if (ret === false) {\n ret = await prevHook.apply(hooks, args);\n }\n return ret;\n })();\n }\n\n let ret = hooksFunc.apply(hooks, args);\n if (ret === false) {\n ret = prevHook.apply(hooks, args);\n }\n return ret;\n };\n }\n }\n opts.hooks = hooks;\n }\n\n // ==-- Parse WalkTokens extensions --== //\n if (pack.walkTokens) {\n const walkTokens = this.defaults.walkTokens;\n const packWalktokens = pack.walkTokens;\n opts.walkTokens = function(token) {\n let values: MaybePromise[] = [];\n values.push(packWalktokens.call(this, token));\n if (walkTokens) {\n values = values.concat(walkTokens.call(this, token));\n }\n return values;\n };\n }\n\n this.defaults = { ...this.defaults, ...opts };\n });\n\n return this;\n }\n\n setOptions(opt: MarkedOptions) {\n this.defaults = { ...this.defaults, ...opt };\n return this;\n }\n\n lexer(src: string, options?: MarkedOptions) {\n return _Lexer.lex(src, options ?? this.defaults);\n }\n\n parser(tokens: Token[], options?: MarkedOptions) {\n return _Parser.parse(tokens, options ?? this.defaults);\n }\n\n private parseMarkdown(blockType: boolean) {\n type overloadedParse = {\n (src: string, options: MarkedOptions & { async: true }): Promise;\n (src: string, options: MarkedOptions & { async: false }): ParserOutput;\n (src: string, options?: MarkedOptions | null): ParserOutput | Promise;\n };\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const parse: overloadedParse = (src: string, options?: MarkedOptions | null): any => {\n const origOpt = { ...options };\n const opt = { ...this.defaults, ...origOpt };\n\n const throwError = this.onError(!!opt.silent, !!opt.async);\n\n // throw error if an extension set async to true but parse was called with async: false\n if (this.defaults.async === true && origOpt.async === false) {\n return throwError(new Error('marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.'));\n }\n\n // throw error in case of non string input\n if (typeof src === 'undefined' || src === null) {\n return throwError(new Error('marked(): input parameter is undefined or null'));\n }\n if (typeof src !== 'string') {\n return throwError(new Error('marked(): input parameter is of type '\n + Object.prototype.toString.call(src) + ', string expected'));\n }\n\n if (opt.hooks) {\n opt.hooks.options = opt;\n opt.hooks.block = blockType;\n }\n\n if (opt.async) {\n return (async() => {\n const processedSrc = opt.hooks ? await opt.hooks.preprocess(src) : src;\n const lexer = opt.hooks ? await opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline);\n const tokens = await lexer(processedSrc, opt);\n const processedTokens = opt.hooks ? await opt.hooks.processAllTokens(tokens) : tokens;\n if (opt.walkTokens) {\n await Promise.all(this.walkTokens(processedTokens, opt.walkTokens));\n }\n const parser = opt.hooks ? await opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline);\n const html = await parser(processedTokens, opt);\n return opt.hooks ? await opt.hooks.postprocess(html) : html;\n })().catch(throwError);\n }\n\n try {\n if (opt.hooks) {\n src = opt.hooks.preprocess(src) as string;\n }\n const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline);\n let tokens = lexer(src, opt);\n if (opt.hooks) {\n tokens = opt.hooks.processAllTokens(tokens);\n }\n if (opt.walkTokens) {\n this.walkTokens(tokens, opt.walkTokens);\n }\n const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline);\n let html = parser(tokens, opt);\n if (opt.hooks) {\n html = opt.hooks.postprocess(html);\n }\n return html;\n } catch(e) {\n return throwError(e as Error);\n }\n };\n\n return parse;\n }\n\n private onError(silent: boolean, async: boolean) {\n return (e: Error): string | Promise => {\n e.message += '\\nPlease report this to https://github.com/markedjs/marked.';\n\n if (silent) {\n const msg = '

    An error occurred:

    '\n          + escapeHtmlEntities(e.message + '', true)\n          + '
    ';\n if (async) {\n return Promise.resolve(msg);\n }\n return msg;\n }\n\n if (async) {\n return Promise.reject(e);\n }\n throw e;\n };\n }\n}\n", "import { _Lexer } from './Lexer.ts';\nimport { _Parser } from './Parser.ts';\nimport { _Tokenizer } from './Tokenizer.ts';\nimport { _Renderer } from './Renderer.ts';\nimport { _TextRenderer } from './TextRenderer.ts';\nimport { _Hooks } from './Hooks.ts';\nimport { Marked } from './Instance.ts';\nimport {\n _getDefaults,\n changeDefaults,\n _defaults,\n} from './defaults.ts';\nimport type { MarkedExtension, MarkedOptions } from './MarkedOptions.ts';\nimport type { Token, TokensList } from './Tokens.ts';\nimport type { MaybePromise } from './Instance.ts';\n\nconst markedInstance = new Marked();\n\n/**\n * Compiles markdown to HTML asynchronously.\n *\n * @param src String of markdown source to be compiled\n * @param options Hash of options, having async: true\n * @return Promise of string of compiled HTML\n */\nexport function marked(src: string, options: MarkedOptions & { async: true }): Promise;\n\n/**\n * Compiles markdown to HTML.\n *\n * @param src String of markdown source to be compiled\n * @param options Optional hash of options\n * @return String of compiled HTML. Will be a Promise of string if async is set to true by any extensions.\n */\nexport function marked(src: string, options: MarkedOptions & { async: false }): string;\nexport function marked(src: string, options: MarkedOptions & { async: true }): Promise;\nexport function marked(src: string, options?: MarkedOptions | null): string | Promise;\nexport function marked(src: string, opt?: MarkedOptions | null): string | Promise {\n return markedInstance.parse(src, opt);\n}\n\n/**\n * Sets the default options.\n *\n * @param options Hash of options\n */\nmarked.options =\n marked.setOptions = function(options: MarkedOptions) {\n markedInstance.setOptions(options);\n marked.defaults = markedInstance.defaults;\n changeDefaults(marked.defaults);\n return marked;\n };\n\n/**\n * Gets the original marked default options.\n */\nmarked.getDefaults = _getDefaults;\n\nmarked.defaults = _defaults;\n\n/**\n * Use Extension\n */\n\nmarked.use = function(...args: MarkedExtension[]) {\n markedInstance.use(...args);\n marked.defaults = markedInstance.defaults;\n changeDefaults(marked.defaults);\n return marked;\n};\n\n/**\n * Run callback for every token\n */\n\nmarked.walkTokens = function(tokens: Token[] | TokensList, callback: (token: Token) => MaybePromise | MaybePromise[]) {\n return markedInstance.walkTokens(tokens, callback);\n};\n\n/**\n * Compiles markdown to HTML without enclosing `p` tag.\n *\n * @param src String of markdown source to be compiled\n * @param options Hash of options\n * @return String of compiled HTML\n */\nmarked.parseInline = markedInstance.parseInline;\n\n/**\n * Expose\n */\nmarked.Parser = _Parser;\nmarked.parser = _Parser.parse;\nmarked.Renderer = _Renderer;\nmarked.TextRenderer = _TextRenderer;\nmarked.Lexer = _Lexer;\nmarked.lexer = _Lexer.lex;\nmarked.Tokenizer = _Tokenizer;\nmarked.Hooks = _Hooks;\nmarked.parse = marked;\n\nexport const options = marked.options;\nexport const setOptions = marked.setOptions;\nexport const use = marked.use;\nexport const walkTokens = marked.walkTokens;\nexport const parseInline = marked.parseInline;\nexport const parse = marked;\nexport const parser = _Parser.parse;\nexport const lexer = _Lexer.lex;\nexport { _defaults as defaults, _getDefaults as getDefaults } from './defaults.ts';\nexport { _Lexer as Lexer } from './Lexer.ts';\nexport { _Parser as Parser } from './Parser.ts';\nexport { _Tokenizer as Tokenizer } from './Tokenizer.ts';\nexport { _Renderer as Renderer } from './Renderer.ts';\nexport { _TextRenderer as TextRenderer } from './TextRenderer.ts';\nexport { _Hooks as Hooks } from './Hooks.ts';\nexport { Marked } from './Instance.ts';\nexport type * from './MarkedOptions.ts';\nexport type * from './Tokens.ts';\n", "\nconst SHIFT_LEFT_32 = (1 << 16) * (1 << 16);\nconst SHIFT_RIGHT_32 = 1 / SHIFT_LEFT_32;\n\n// Threshold chosen based on both benchmarking and knowledge about browser string\n// data structures (which currently switch structure types at 12 bytes or more)\nconst TEXT_DECODER_MIN_LENGTH = 12;\nconst utf8TextDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8');\n\nconst PBF_VARINT = 0; // varint: int32, int64, uint32, uint64, sint32, sint64, bool, enum\nconst PBF_FIXED64 = 1; // 64-bit: double, fixed64, sfixed64\nconst PBF_BYTES = 2; // length-delimited: string, bytes, embedded messages, packed repeated fields\nconst PBF_FIXED32 = 5; // 32-bit: float, fixed32, sfixed32\n\nexport default class Pbf {\n /**\n * @param {Uint8Array | ArrayBuffer} [buf]\n */\n constructor(buf = new Uint8Array(16)) {\n this.buf = ArrayBuffer.isView(buf) ? buf : new Uint8Array(buf);\n this.dataView = new DataView(this.buf.buffer);\n this.pos = 0;\n this.type = 0;\n this.length = this.buf.length;\n }\n\n // === READING =================================================================\n\n /**\n * @template T\n * @param {(tag: number, result: T, pbf: Pbf) => void} readField\n * @param {T} result\n * @param {number} [end]\n */\n readFields(readField, result, end = this.length) {\n while (this.pos < end) {\n const val = this.readVarint(),\n tag = val >> 3,\n startPos = this.pos;\n\n this.type = val & 0x7;\n readField(tag, result, this);\n\n if (this.pos === startPos) this.skip(val);\n }\n return result;\n }\n\n /**\n * @template T\n * @param {(tag: number, result: T, pbf: Pbf) => void} readField\n * @param {T} result\n */\n readMessage(readField, result) {\n return this.readFields(readField, result, this.readVarint() + this.pos);\n }\n\n readFixed32() {\n const val = this.dataView.getUint32(this.pos, true);\n this.pos += 4;\n return val;\n }\n\n readSFixed32() {\n const val = this.dataView.getInt32(this.pos, true);\n this.pos += 4;\n return val;\n }\n\n // 64-bit int handling is based on github.com/dpw/node-buffer-more-ints (MIT-licensed)\n\n readFixed64() {\n const val = this.dataView.getUint32(this.pos, true) + this.dataView.getUint32(this.pos + 4, true) * SHIFT_LEFT_32;\n this.pos += 8;\n return val;\n }\n\n readSFixed64() {\n const val = this.dataView.getUint32(this.pos, true) + this.dataView.getInt32(this.pos + 4, true) * SHIFT_LEFT_32;\n this.pos += 8;\n return val;\n }\n\n readFloat() {\n const val = this.dataView.getFloat32(this.pos, true);\n this.pos += 4;\n return val;\n }\n\n readDouble() {\n const val = this.dataView.getFloat64(this.pos, true);\n this.pos += 8;\n return val;\n }\n\n /**\n * @param {boolean} [isSigned]\n */\n readVarint(isSigned) {\n const buf = this.buf;\n let val, b;\n\n b = buf[this.pos++]; val = b & 0x7f; if (b < 0x80) return val;\n b = buf[this.pos++]; val |= (b & 0x7f) << 7; if (b < 0x80) return val;\n b = buf[this.pos++]; val |= (b & 0x7f) << 14; if (b < 0x80) return val;\n b = buf[this.pos++]; val |= (b & 0x7f) << 21; if (b < 0x80) return val;\n b = buf[this.pos]; val |= (b & 0x0f) << 28;\n\n return readVarintRemainder(val, isSigned, this);\n }\n\n readVarint64() { // for compatibility with v2.0.1\n return this.readVarint(true);\n }\n\n readSVarint() {\n const num = this.readVarint();\n return num % 2 === 1 ? (num + 1) / -2 : num / 2; // zigzag encoding\n }\n\n readBoolean() {\n return Boolean(this.readVarint());\n }\n\n readString() {\n const end = this.readVarint() + this.pos;\n const pos = this.pos;\n this.pos = end;\n\n if (end - pos >= TEXT_DECODER_MIN_LENGTH && utf8TextDecoder) {\n // longer strings are fast with the built-in browser TextDecoder API\n return utf8TextDecoder.decode(this.buf.subarray(pos, end));\n }\n // short strings are fast with our custom implementation\n return readUtf8(this.buf, pos, end);\n }\n\n readBytes() {\n const end = this.readVarint() + this.pos,\n buffer = this.buf.subarray(this.pos, end);\n this.pos = end;\n return buffer;\n }\n\n // verbose for performance reasons; doesn't affect gzipped size\n\n /**\n * @param {number[]} [arr]\n * @param {boolean} [isSigned]\n */\n readPackedVarint(arr = [], isSigned) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readVarint(isSigned));\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedSVarint(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readSVarint());\n return arr;\n }\n /** @param {boolean[]} [arr] */\n readPackedBoolean(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readBoolean());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedFloat(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readFloat());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedDouble(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readDouble());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedFixed32(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readFixed32());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedSFixed32(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readSFixed32());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedFixed64(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readFixed64());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedSFixed64(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readSFixed64());\n return arr;\n }\n readPackedEnd() {\n return this.type === PBF_BYTES ? this.readVarint() + this.pos : this.pos + 1;\n }\n\n /** @param {number} val */\n skip(val) {\n const type = val & 0x7;\n if (type === PBF_VARINT) while (this.buf[this.pos++] > 0x7f) {}\n else if (type === PBF_BYTES) this.pos = this.readVarint() + this.pos;\n else if (type === PBF_FIXED32) this.pos += 4;\n else if (type === PBF_FIXED64) this.pos += 8;\n else throw new Error(`Unimplemented type: ${type}`);\n }\n\n // === WRITING =================================================================\n\n /**\n * @param {number} tag\n * @param {number} type\n */\n writeTag(tag, type) {\n this.writeVarint((tag << 3) | type);\n }\n\n /** @param {number} min */\n realloc(min) {\n let length = this.length || 16;\n\n while (length < this.pos + min) length *= 2;\n\n if (length !== this.length) {\n const buf = new Uint8Array(length);\n buf.set(this.buf);\n this.buf = buf;\n this.dataView = new DataView(buf.buffer);\n this.length = length;\n }\n }\n\n finish() {\n this.length = this.pos;\n this.pos = 0;\n return this.buf.subarray(0, this.length);\n }\n\n /** @param {number} val */\n writeFixed32(val) {\n this.realloc(4);\n this.dataView.setInt32(this.pos, val, true);\n this.pos += 4;\n }\n\n /** @param {number} val */\n writeSFixed32(val) {\n this.realloc(4);\n this.dataView.setInt32(this.pos, val, true);\n this.pos += 4;\n }\n\n /** @param {number} val */\n writeFixed64(val) {\n this.realloc(8);\n this.dataView.setInt32(this.pos, val & -1, true);\n this.dataView.setInt32(this.pos + 4, Math.floor(val * SHIFT_RIGHT_32), true);\n this.pos += 8;\n }\n\n /** @param {number} val */\n writeSFixed64(val) {\n this.realloc(8);\n this.dataView.setInt32(this.pos, val & -1, true);\n this.dataView.setInt32(this.pos + 4, Math.floor(val * SHIFT_RIGHT_32), true);\n this.pos += 8;\n }\n\n /** @param {number} val */\n writeVarint(val) {\n val = +val || 0;\n\n if (val > 0xfffffff || val < 0) {\n writeBigVarint(val, this);\n return;\n }\n\n this.realloc(4);\n\n this.buf[this.pos++] = val & 0x7f | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;\n this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;\n this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;\n this.buf[this.pos++] = (val >>> 7) & 0x7f;\n }\n\n /** @param {number} val */\n writeSVarint(val) {\n this.writeVarint(val < 0 ? -val * 2 - 1 : val * 2);\n }\n\n /** @param {boolean} val */\n writeBoolean(val) {\n this.writeVarint(+val);\n }\n\n /** @param {string} str */\n writeString(str) {\n str = String(str);\n this.realloc(str.length * 4);\n\n this.pos++; // reserve 1 byte for short string length\n\n const startPos = this.pos;\n // write the string directly to the buffer and see how much was written\n this.pos = writeUtf8(this.buf, str, this.pos);\n const len = this.pos - startPos;\n\n if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);\n\n // finally, write the message length in the reserved place and restore the position\n this.pos = startPos - 1;\n this.writeVarint(len);\n this.pos += len;\n }\n\n /** @param {number} val */\n writeFloat(val) {\n this.realloc(4);\n this.dataView.setFloat32(this.pos, val, true);\n this.pos += 4;\n }\n\n /** @param {number} val */\n writeDouble(val) {\n this.realloc(8);\n this.dataView.setFloat64(this.pos, val, true);\n this.pos += 8;\n }\n\n /** @param {Uint8Array} buffer */\n writeBytes(buffer) {\n const len = buffer.length;\n this.writeVarint(len);\n this.realloc(len);\n for (let i = 0; i < len; i++) this.buf[this.pos++] = buffer[i];\n }\n\n /**\n * @template T\n * @param {(obj: T, pbf: Pbf) => void} fn\n * @param {T} obj\n */\n writeRawMessage(fn, obj) {\n this.pos++; // reserve 1 byte for short message length\n\n // write the message directly to the buffer and see how much was written\n const startPos = this.pos;\n fn(obj, this);\n const len = this.pos - startPos;\n\n if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);\n\n // finally, write the message length in the reserved place and restore the position\n this.pos = startPos - 1;\n this.writeVarint(len);\n this.pos += len;\n }\n\n /**\n * @template T\n * @param {number} tag\n * @param {(obj: T, pbf: Pbf) => void} fn\n * @param {T} obj\n */\n writeMessage(tag, fn, obj) {\n this.writeTag(tag, PBF_BYTES);\n this.writeRawMessage(fn, obj);\n }\n\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedVarint(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedVarint, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedSVarint(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedSVarint, arr);\n }\n /**\n * @param {number} tag\n * @param {boolean[]} arr\n */\n writePackedBoolean(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedBoolean, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedFloat(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedFloat, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedDouble(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedDouble, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedFixed32(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedFixed32, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedSFixed32(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedSFixed32, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedFixed64(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedFixed64, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedSFixed64(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedSFixed64, arr);\n }\n\n /**\n * @param {number} tag\n * @param {Uint8Array} buffer\n */\n writeBytesField(tag, buffer) {\n this.writeTag(tag, PBF_BYTES);\n this.writeBytes(buffer);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeFixed32Field(tag, val) {\n this.writeTag(tag, PBF_FIXED32);\n this.writeFixed32(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeSFixed32Field(tag, val) {\n this.writeTag(tag, PBF_FIXED32);\n this.writeSFixed32(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeFixed64Field(tag, val) {\n this.writeTag(tag, PBF_FIXED64);\n this.writeFixed64(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeSFixed64Field(tag, val) {\n this.writeTag(tag, PBF_FIXED64);\n this.writeSFixed64(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeVarintField(tag, val) {\n this.writeTag(tag, PBF_VARINT);\n this.writeVarint(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeSVarintField(tag, val) {\n this.writeTag(tag, PBF_VARINT);\n this.writeSVarint(val);\n }\n /**\n * @param {number} tag\n * @param {string} str\n */\n writeStringField(tag, str) {\n this.writeTag(tag, PBF_BYTES);\n this.writeString(str);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeFloatField(tag, val) {\n this.writeTag(tag, PBF_FIXED32);\n this.writeFloat(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeDoubleField(tag, val) {\n this.writeTag(tag, PBF_FIXED64);\n this.writeDouble(val);\n }\n /**\n * @param {number} tag\n * @param {boolean} val\n */\n writeBooleanField(tag, val) {\n this.writeVarintField(tag, +val);\n }\n};\n\n/**\n * @param {number} l\n * @param {boolean | undefined} s\n * @param {Pbf} p\n */\nfunction readVarintRemainder(l, s, p) {\n const buf = p.buf;\n let h, b;\n\n b = buf[p.pos++]; h = (b & 0x70) >> 4; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 3; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 10; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 17; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 24; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x01) << 31; if (b < 0x80) return toNum(l, h, s);\n\n throw new Error('Expected varint not more than 10 bytes');\n}\n\n/**\n * @param {number} low\n * @param {number} high\n * @param {boolean} [isSigned]\n */\nfunction toNum(low, high, isSigned) {\n return isSigned ? high * 0x100000000 + (low >>> 0) : ((high >>> 0) * 0x100000000) + (low >>> 0);\n}\n\n/**\n * @param {number} val\n * @param {Pbf} pbf\n */\nfunction writeBigVarint(val, pbf) {\n let low, high;\n\n if (val >= 0) {\n low = (val % 0x100000000) | 0;\n high = (val / 0x100000000) | 0;\n } else {\n low = ~(-val % 0x100000000);\n high = ~(-val / 0x100000000);\n\n if (low ^ 0xffffffff) {\n low = (low + 1) | 0;\n } else {\n low = 0;\n high = (high + 1) | 0;\n }\n }\n\n if (val >= 0x10000000000000000 || val < -0x10000000000000000) {\n throw new Error('Given varint doesn\\'t fit into 10 bytes');\n }\n\n pbf.realloc(10);\n\n writeBigVarintLow(low, high, pbf);\n writeBigVarintHigh(high, pbf);\n}\n\n/**\n * @param {number} high\n * @param {number} low\n * @param {Pbf} pbf\n */\nfunction writeBigVarintLow(low, high, pbf) {\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos] = low & 0x7f;\n}\n\n/**\n * @param {number} high\n * @param {Pbf} pbf\n */\nfunction writeBigVarintHigh(high, pbf) {\n const lsb = (high & 0x07) << 4;\n\n pbf.buf[pbf.pos++] |= lsb | ((high >>>= 3) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f;\n}\n\n/**\n * @param {number} startPos\n * @param {number} len\n * @param {Pbf} pbf\n */\nfunction makeRoomForExtraLength(startPos, len, pbf) {\n const extraLen =\n len <= 0x3fff ? 1 :\n len <= 0x1fffff ? 2 :\n len <= 0xfffffff ? 3 : Math.floor(Math.log(len) / (Math.LN2 * 7));\n\n // if 1 byte isn't enough for encoding message length, shift the data to the right\n pbf.realloc(extraLen);\n for (let i = pbf.pos - 1; i >= startPos; i--) pbf.buf[i + extraLen] = pbf.buf[i];\n}\n\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedVarint(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeVarint(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedSVarint(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeSVarint(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedFloat(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeFloat(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedDouble(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeDouble(arr[i]);\n}\n/**\n * @param {boolean[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedBoolean(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeBoolean(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedFixed32(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeFixed32(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedSFixed32(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeSFixed32(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedFixed64(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeFixed64(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedSFixed64(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeSFixed64(arr[i]);\n}\n\n// Buffer code below from https://github.com/feross/buffer, MIT-licensed\n\n/**\n * @param {Uint8Array} buf\n * @param {number} pos\n * @param {number} end\n */\nfunction readUtf8(buf, pos, end) {\n let str = '';\n let i = pos;\n\n while (i < end) {\n const b0 = buf[i];\n let c = null; // codepoint\n let bytesPerSequence =\n b0 > 0xEF ? 4 :\n b0 > 0xDF ? 3 :\n b0 > 0xBF ? 2 : 1;\n\n if (i + bytesPerSequence > end) break;\n\n let b1, b2, b3;\n\n if (bytesPerSequence === 1) {\n if (b0 < 0x80) {\n c = b0;\n }\n } else if (bytesPerSequence === 2) {\n b1 = buf[i + 1];\n if ((b1 & 0xC0) === 0x80) {\n c = (b0 & 0x1F) << 0x6 | (b1 & 0x3F);\n if (c <= 0x7F) {\n c = null;\n }\n }\n } else if (bytesPerSequence === 3) {\n b1 = buf[i + 1];\n b2 = buf[i + 2];\n if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80) {\n c = (b0 & 0xF) << 0xC | (b1 & 0x3F) << 0x6 | (b2 & 0x3F);\n if (c <= 0x7FF || (c >= 0xD800 && c <= 0xDFFF)) {\n c = null;\n }\n }\n } else if (bytesPerSequence === 4) {\n b1 = buf[i + 1];\n b2 = buf[i + 2];\n b3 = buf[i + 3];\n if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80 && (b3 & 0xC0) === 0x80) {\n c = (b0 & 0xF) << 0x12 | (b1 & 0x3F) << 0xC | (b2 & 0x3F) << 0x6 | (b3 & 0x3F);\n if (c <= 0xFFFF || c >= 0x110000) {\n c = null;\n }\n }\n }\n\n if (c === null) {\n c = 0xFFFD;\n bytesPerSequence = 1;\n\n } else if (c > 0xFFFF) {\n c -= 0x10000;\n str += String.fromCharCode(c >>> 10 & 0x3FF | 0xD800);\n c = 0xDC00 | c & 0x3FF;\n }\n\n str += String.fromCharCode(c);\n i += bytesPerSequence;\n }\n\n return str;\n}\n\n/**\n * @param {Uint8Array} buf\n * @param {string} str\n * @param {number} pos\n */\nfunction writeUtf8(buf, str, pos) {\n for (let i = 0, c, lead; i < str.length; i++) {\n c = str.charCodeAt(i); // code point\n\n if (c > 0xD7FF && c < 0xE000) {\n if (lead) {\n if (c < 0xDC00) {\n buf[pos++] = 0xEF;\n buf[pos++] = 0xBF;\n buf[pos++] = 0xBD;\n lead = c;\n continue;\n } else {\n c = lead - 0xD800 << 10 | c - 0xDC00 | 0x10000;\n lead = null;\n }\n } else {\n if (c > 0xDBFF || (i + 1 === str.length)) {\n buf[pos++] = 0xEF;\n buf[pos++] = 0xBF;\n buf[pos++] = 0xBD;\n } else {\n lead = c;\n }\n continue;\n }\n } else if (lead) {\n buf[pos++] = 0xEF;\n buf[pos++] = 0xBF;\n buf[pos++] = 0xBD;\n lead = null;\n }\n\n if (c < 0x80) {\n buf[pos++] = c;\n } else {\n if (c < 0x800) {\n buf[pos++] = c >> 0x6 | 0xC0;\n } else {\n if (c < 0x10000) {\n buf[pos++] = c >> 0xC | 0xE0;\n } else {\n buf[pos++] = c >> 0x12 | 0xF0;\n buf[pos++] = c >> 0xC & 0x3F | 0x80;\n }\n buf[pos++] = c >> 0x6 & 0x3F | 0x80;\n }\n buf[pos++] = c & 0x3F | 0x80;\n }\n }\n return pos;\n}\n", "/**\n * A standalone point geometry with useful accessor, comparison, and\n * modification methods.\n *\n * @class\n * @param {number} x the x-coordinate. This could be longitude or screen pixels, or any other sort of unit.\n * @param {number} y the y-coordinate. This could be latitude or screen pixels, or any other sort of unit.\n *\n * @example\n * const point = new Point(-77, 38);\n */\nexport default function Point(x, y) {\n this.x = x;\n this.y = y;\n}\n\nPoint.prototype = {\n /**\n * Clone this point, returning a new point that can be modified\n * without affecting the old one.\n * @return {Point} the clone\n */\n clone() { return new Point(this.x, this.y); },\n\n /**\n * Add this point's x & y coordinates to another point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n add(p) { return this.clone()._add(p); },\n\n /**\n * Subtract this point's x & y coordinates to from point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n sub(p) { return this.clone()._sub(p); },\n\n /**\n * Multiply this point's x & y coordinates by point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n multByPoint(p) { return this.clone()._multByPoint(p); },\n\n /**\n * Divide this point's x & y coordinates by point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n divByPoint(p) { return this.clone()._divByPoint(p); },\n\n /**\n * Multiply this point's x & y coordinates by a factor,\n * yielding a new point.\n * @param {number} k factor\n * @return {Point} output point\n */\n mult(k) { return this.clone()._mult(k); },\n\n /**\n * Divide this point's x & y coordinates by a factor,\n * yielding a new point.\n * @param {number} k factor\n * @return {Point} output point\n */\n div(k) { return this.clone()._div(k); },\n\n /**\n * Rotate this point around the 0, 0 origin by an angle a,\n * given in radians\n * @param {number} a angle to rotate around, in radians\n * @return {Point} output point\n */\n rotate(a) { return this.clone()._rotate(a); },\n\n /**\n * Rotate this point around p point by an angle a,\n * given in radians\n * @param {number} a angle to rotate around, in radians\n * @param {Point} p Point to rotate around\n * @return {Point} output point\n */\n rotateAround(a, p) { return this.clone()._rotateAround(a, p); },\n\n /**\n * Multiply this point by a 4x1 transformation matrix\n * @param {[number, number, number, number]} m transformation matrix\n * @return {Point} output point\n */\n matMult(m) { return this.clone()._matMult(m); },\n\n /**\n * Calculate this point but as a unit vector from 0, 0, meaning\n * that the distance from the resulting point to the 0, 0\n * coordinate will be equal to 1 and the angle from the resulting\n * point to the 0, 0 coordinate will be the same as before.\n * @return {Point} unit vector point\n */\n unit() { return this.clone()._unit(); },\n\n /**\n * Compute a perpendicular point, where the new y coordinate\n * is the old x coordinate and the new x coordinate is the old y\n * coordinate multiplied by -1\n * @return {Point} perpendicular point\n */\n perp() { return this.clone()._perp(); },\n\n /**\n * Return a version of this point with the x & y coordinates\n * rounded to integers.\n * @return {Point} rounded point\n */\n round() { return this.clone()._round(); },\n\n /**\n * Return the magnitude of this point: this is the Euclidean\n * distance from the 0, 0 coordinate to this point's x and y\n * coordinates.\n * @return {number} magnitude\n */\n mag() {\n return Math.sqrt(this.x * this.x + this.y * this.y);\n },\n\n /**\n * Judge whether this point is equal to another point, returning\n * true or false.\n * @param {Point} other the other point\n * @return {boolean} whether the points are equal\n */\n equals(other) {\n return this.x === other.x &&\n this.y === other.y;\n },\n\n /**\n * Calculate the distance from this point to another point\n * @param {Point} p the other point\n * @return {number} distance\n */\n dist(p) {\n return Math.sqrt(this.distSqr(p));\n },\n\n /**\n * Calculate the distance from this point to another point,\n * without the square root step. Useful if you're comparing\n * relative distances.\n * @param {Point} p the other point\n * @return {number} distance\n */\n distSqr(p) {\n const dx = p.x - this.x,\n dy = p.y - this.y;\n return dx * dx + dy * dy;\n },\n\n /**\n * Get the angle from the 0, 0 coordinate to this point, in radians\n * coordinates.\n * @return {number} angle\n */\n angle() {\n return Math.atan2(this.y, this.x);\n },\n\n /**\n * Get the angle from this point to another point, in radians\n * @param {Point} b the other point\n * @return {number} angle\n */\n angleTo(b) {\n return Math.atan2(this.y - b.y, this.x - b.x);\n },\n\n /**\n * Get the angle between this point and another point, in radians\n * @param {Point} b the other point\n * @return {number} angle\n */\n angleWith(b) {\n return this.angleWithSep(b.x, b.y);\n },\n\n /**\n * Find the angle of the two vectors, solving the formula for\n * the cross product a x b = |a||b|sin(\u03B8) for \u03B8.\n * @param {number} x the x-coordinate\n * @param {number} y the y-coordinate\n * @return {number} the angle in radians\n */\n angleWithSep(x, y) {\n return Math.atan2(\n this.x * y - this.y * x,\n this.x * x + this.y * y);\n },\n\n /** @param {[number, number, number, number]} m */\n _matMult(m) {\n const x = m[0] * this.x + m[1] * this.y,\n y = m[2] * this.x + m[3] * this.y;\n this.x = x;\n this.y = y;\n return this;\n },\n\n /** @param {Point} p */\n _add(p) {\n this.x += p.x;\n this.y += p.y;\n return this;\n },\n\n /** @param {Point} p */\n _sub(p) {\n this.x -= p.x;\n this.y -= p.y;\n return this;\n },\n\n /** @param {number} k */\n _mult(k) {\n this.x *= k;\n this.y *= k;\n return this;\n },\n\n /** @param {number} k */\n _div(k) {\n this.x /= k;\n this.y /= k;\n return this;\n },\n\n /** @param {Point} p */\n _multByPoint(p) {\n this.x *= p.x;\n this.y *= p.y;\n return this;\n },\n\n /** @param {Point} p */\n _divByPoint(p) {\n this.x /= p.x;\n this.y /= p.y;\n return this;\n },\n\n _unit() {\n this._div(this.mag());\n return this;\n },\n\n _perp() {\n const y = this.y;\n this.y = this.x;\n this.x = -y;\n return this;\n },\n\n /** @param {number} angle */\n _rotate(angle) {\n const cos = Math.cos(angle),\n sin = Math.sin(angle),\n x = cos * this.x - sin * this.y,\n y = sin * this.x + cos * this.y;\n this.x = x;\n this.y = y;\n return this;\n },\n\n /**\n * @param {number} angle\n * @param {Point} p\n */\n _rotateAround(angle, p) {\n const cos = Math.cos(angle),\n sin = Math.sin(angle),\n x = p.x + cos * (this.x - p.x) - sin * (this.y - p.y),\n y = p.y + sin * (this.x - p.x) + cos * (this.y - p.y);\n this.x = x;\n this.y = y;\n return this;\n },\n\n _round() {\n this.x = Math.round(this.x);\n this.y = Math.round(this.y);\n return this;\n },\n\n constructor: Point\n};\n\n/**\n * Construct a point from an array if necessary, otherwise if the input\n * is already a Point, return it unchanged.\n * @param {Point | [number, number] | {x: number, y: number}} p input value\n * @return {Point} constructed point.\n * @example\n * // this\n * var point = Point.convert([0, 1]);\n * // is equivalent to\n * var point = new Point(0, 1);\n */\nPoint.convert = function (p) {\n if (p instanceof Point) {\n return /** @type {Point} */ (p);\n }\n if (Array.isArray(p)) {\n return new Point(+p[0], +p[1]);\n }\n if (p.x !== undefined && p.y !== undefined) {\n return new Point(+p.x, +p.y);\n }\n throw new Error('Expected [x, y] or {x, y} point format');\n};\n", "\nimport Point from '@mapbox/point-geometry';\n\n/** @import Pbf from 'pbf' */\n/** @import {Feature} from 'geojson' */\n\nexport class VectorTileFeature {\n /**\n * @param {Pbf} pbf\n * @param {number} end\n * @param {number} extent\n * @param {string[]} keys\n * @param {(number | string | boolean)[]} values\n */\n constructor(pbf, end, extent, keys, values) {\n // Public\n\n /** @type {Record} */\n this.properties = {};\n\n this.extent = extent;\n /** @type {0 | 1 | 2 | 3} */\n this.type = 0;\n\n /** @type {number | undefined} */\n this.id = undefined;\n\n /** @private */\n this._pbf = pbf;\n /** @private */\n this._geometry = -1;\n /** @private */\n this._keys = keys;\n /** @private */\n this._values = values;\n\n pbf.readFields(readFeature, this, end);\n }\n\n loadGeometry() {\n const pbf = this._pbf;\n pbf.pos = this._geometry;\n\n const end = pbf.readVarint() + pbf.pos;\n\n /** @type Point[][] */\n const lines = [];\n\n /** @type Point[] | undefined */\n let line;\n\n let cmd = 1;\n let length = 0;\n let x = 0;\n let y = 0;\n\n while (pbf.pos < end) {\n if (length <= 0) {\n const cmdLen = pbf.readVarint();\n cmd = cmdLen & 0x7;\n length = cmdLen >> 3;\n }\n\n length--;\n\n if (cmd === 1 || cmd === 2) {\n x += pbf.readSVarint();\n y += pbf.readSVarint();\n\n if (cmd === 1) { // moveTo\n if (line) lines.push(line);\n line = [];\n }\n\n if (line) line.push(new Point(x, y));\n\n } else if (cmd === 7) {\n\n // Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90\n if (line) {\n line.push(line[0].clone()); // closePolygon\n }\n\n } else {\n throw new Error(`unknown command ${cmd}`);\n }\n }\n\n if (line) lines.push(line);\n\n return lines;\n }\n\n bbox() {\n const pbf = this._pbf;\n pbf.pos = this._geometry;\n\n const end = pbf.readVarint() + pbf.pos;\n let cmd = 1,\n length = 0,\n x = 0,\n y = 0,\n x1 = Infinity,\n x2 = -Infinity,\n y1 = Infinity,\n y2 = -Infinity;\n\n while (pbf.pos < end) {\n if (length <= 0) {\n const cmdLen = pbf.readVarint();\n cmd = cmdLen & 0x7;\n length = cmdLen >> 3;\n }\n\n length--;\n\n if (cmd === 1 || cmd === 2) {\n x += pbf.readSVarint();\n y += pbf.readSVarint();\n if (x < x1) x1 = x;\n if (x > x2) x2 = x;\n if (y < y1) y1 = y;\n if (y > y2) y2 = y;\n\n } else if (cmd !== 7) {\n throw new Error(`unknown command ${cmd}`);\n }\n }\n\n return [x1, y1, x2, y2];\n }\n\n /**\n * @param {number} x\n * @param {number} y\n * @param {number} z\n * @return {Feature}\n */\n toGeoJSON(x, y, z) {\n const size = this.extent * Math.pow(2, z),\n x0 = this.extent * x,\n y0 = this.extent * y,\n vtCoords = this.loadGeometry();\n\n /** @param {Point} p */\n function projectPoint(p) {\n return [\n (p.x + x0) * 360 / size - 180,\n 360 / Math.PI * Math.atan(Math.exp((1 - (p.y + y0) * 2 / size) * Math.PI)) - 90\n ];\n }\n\n /** @param {Point[]} line */\n function projectLine(line) {\n return line.map(projectPoint);\n }\n\n /** @type {Feature[\"geometry\"]} */\n let geometry;\n\n if (this.type === 1) {\n const points = [];\n for (const line of vtCoords) {\n points.push(line[0]);\n }\n const coordinates = projectLine(points);\n geometry = points.length === 1 ?\n {type: 'Point', coordinates: coordinates[0]} :\n {type: 'MultiPoint', coordinates};\n\n } else if (this.type === 2) {\n\n const coordinates = vtCoords.map(projectLine);\n geometry = coordinates.length === 1 ?\n {type: 'LineString', coordinates: coordinates[0]} :\n {type: 'MultiLineString', coordinates};\n\n } else if (this.type === 3) {\n const polygons = classifyRings(vtCoords);\n const coordinates = [];\n for (const polygon of polygons) {\n coordinates.push(polygon.map(projectLine));\n }\n geometry = coordinates.length === 1 ?\n {type: 'Polygon', coordinates: coordinates[0]} :\n {type: 'MultiPolygon', coordinates};\n } else {\n\n throw new Error('unknown feature type');\n }\n\n /** @type {Feature} */\n const result = {\n type: 'Feature',\n geometry,\n properties: this.properties\n };\n\n if (this.id != null) {\n result.id = this.id;\n }\n\n return result;\n }\n}\n\n/** @type {['Unknown', 'Point', 'LineString', 'Polygon']} */\nVectorTileFeature.types = ['Unknown', 'Point', 'LineString', 'Polygon'];\n\n/**\n * @param {number} tag\n * @param {VectorTileFeature} feature\n * @param {Pbf} pbf\n */\nfunction readFeature(tag, feature, pbf) {\n if (tag === 1) feature.id = pbf.readVarint();\n else if (tag === 2) readTag(pbf, feature);\n else if (tag === 3) feature.type = /** @type {0 | 1 | 2 | 3} */ (pbf.readVarint());\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 4) feature._geometry = pbf.pos;\n}\n\n/**\n * @param {Pbf} pbf\n * @param {VectorTileFeature} feature\n */\nfunction readTag(pbf, feature) {\n const end = pbf.readVarint() + pbf.pos;\n\n while (pbf.pos < end) {\n // @ts-expect-error TS2341 deliberately accessing a private property\n const key = feature._keys[pbf.readVarint()];\n // @ts-expect-error TS2341 deliberately accessing a private property\n const value = feature._values[pbf.readVarint()];\n feature.properties[key] = value;\n }\n}\n\n/** classifies an array of rings into polygons with outer rings and holes\n * @param {Point[][]} rings\n */\nexport function classifyRings(rings) {\n const len = rings.length;\n\n if (len <= 1) return [rings];\n\n const polygons = [];\n let polygon, ccw;\n\n for (let i = 0; i < len; i++) {\n const area = signedArea(rings[i]);\n if (area === 0) continue;\n\n if (ccw === undefined) ccw = area < 0;\n\n if (ccw === area < 0) {\n if (polygon) polygons.push(polygon);\n polygon = [rings[i]];\n\n } else if (polygon) {\n polygon.push(rings[i]);\n }\n }\n if (polygon) polygons.push(polygon);\n\n return polygons;\n}\n\n/** @param {Point[]} ring */\nfunction signedArea(ring) {\n let sum = 0;\n for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {\n p1 = ring[i];\n p2 = ring[j];\n sum += (p2.x - p1.x) * (p1.y + p2.y);\n }\n return sum;\n}\n\nexport class VectorTileLayer {\n /**\n * @param {Pbf} pbf\n * @param {number} [end]\n */\n constructor(pbf, end) {\n // Public\n this.version = 1;\n this.name = '';\n this.extent = 4096;\n this.length = 0;\n\n /** @private */\n this._pbf = pbf;\n\n /** @private\n * @type {string[]} */\n this._keys = [];\n\n /** @private\n * @type {(number | string | boolean)[]} */\n this._values = [];\n\n /** @private\n * @type {number[]} */\n this._features = [];\n\n pbf.readFields(readLayer, this, end);\n\n this.length = this._features.length;\n }\n\n /** return feature `i` from this layer as a `VectorTileFeature`\n * @param {number} i\n */\n feature(i) {\n if (i < 0 || i >= this._features.length) throw new Error('feature index out of bounds');\n\n this._pbf.pos = this._features[i];\n\n const end = this._pbf.readVarint() + this._pbf.pos;\n return new VectorTileFeature(this._pbf, end, this.extent, this._keys, this._values);\n }\n}\n\n/**\n * @param {number} tag\n * @param {VectorTileLayer} layer\n * @param {Pbf} pbf\n */\nfunction readLayer(tag, layer, pbf) {\n if (tag === 15) layer.version = pbf.readVarint();\n else if (tag === 1) layer.name = pbf.readString();\n else if (tag === 5) layer.extent = pbf.readVarint();\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 2) layer._features.push(pbf.pos);\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 3) layer._keys.push(pbf.readString());\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 4) layer._values.push(readValueMessage(pbf));\n}\n\n/**\n * @param {Pbf} pbf\n */\nfunction readValueMessage(pbf) {\n let value = null;\n const end = pbf.readVarint() + pbf.pos;\n\n while (pbf.pos < end) {\n const tag = pbf.readVarint() >> 3;\n\n value = tag === 1 ? pbf.readString() :\n tag === 2 ? pbf.readFloat() :\n tag === 3 ? pbf.readDouble() :\n tag === 4 ? pbf.readVarint64() :\n tag === 5 ? pbf.readVarint() :\n tag === 6 ? pbf.readSVarint() :\n tag === 7 ? pbf.readBoolean() : null;\n }\n if (value == null) {\n throw new Error('unknown feature value');\n }\n\n return value;\n}\n\nexport class VectorTile {\n /**\n * @param {Pbf} pbf\n * @param {number} [end]\n */\n constructor(pbf, end) {\n /** @type {Record} */\n this.layers = pbf.readFields(readTile, {}, end);\n }\n}\n\n/**\n * @param {number} tag\n * @param {Record} layers\n * @param {Pbf} pbf\n */\nfunction readTile(tag, layers, pbf) {\n if (tag === 3) {\n const layer = new VectorTileLayer(pbf, pbf.readVarint() + pbf.pos);\n if (layer.length) layers[layer.name] = layer;\n }\n}\n", "import RBush from 'rbush';\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { json as d3_json } from 'd3-fetch';\n\nimport { marked } from 'marked';\nimport Protobuf from 'pbf';\nimport { VectorTile } from '@mapbox/vector-tile';\n\nimport { fileFetcher } from '../core/file_fetcher';\nimport { localizer } from '../core/localizer';\nimport { geoExtent, geoVecAdd } from '../geo';\nimport { QAItem } from '../osm';\nimport { utilRebind, utilTiler, utilQsString } from '../util';\n\nconst tiler = utilTiler();\nconst dispatch = d3_dispatch('loaded');\nconst _tileZoom = 14;\nconst _osmoseUrlRoot = 'https://osmose.openstreetmap.fr/api/0.3';\nlet _osmoseData = { icons: {}, items: [] };\n\n// This gets reassigned if reset\nlet _cache;\n\nfunction abortRequest(controller) {\n if (controller) {\n controller.abort();\n }\n}\n\nfunction abortUnwantedRequests(cache, tiles) {\n Object.keys(cache.inflightTile).forEach(k => {\n let wanted = tiles.find(tile => k === tile.id);\n if (!wanted) {\n abortRequest(cache.inflightTile[k]);\n delete cache.inflightTile[k];\n }\n });\n}\n\nfunction encodeIssueRtree(d) {\n return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };\n}\n\n// Replace or remove QAItem from rtree\nfunction updateRtree(item, replace) {\n _cache.rtree.remove(item, (a, b) => a.data.id === b.data.id);\n\n if (replace) {\n _cache.rtree.insert(item);\n }\n}\n\n// Issues shouldn't obscure each other\nfunction preventCoincident(loc) {\n let coincident = false;\n do {\n // first time, move marker up. after that, move marker right.\n let delta = coincident ? [0.00001, 0] : [0, 0.00001];\n loc = geoVecAdd(loc, delta);\n let bbox = geoExtent(loc).bbox();\n coincident = _cache.rtree.search(bbox).length;\n } while (coincident);\n\n return loc;\n}\n\nexport default {\n title: 'osmose',\n\n init() {\n fileFetcher.get('qa_data')\n .then(d => {\n _osmoseData = d.osmose;\n _osmoseData.items = Object.keys(d.osmose.icons)\n .map(s => s.split('-')[0])\n .reduce((unique, item) => unique.indexOf(item) !== -1 ? unique : [...unique, item], []);\n });\n\n if (!_cache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset() {\n let _strings = {};\n let _colors = {};\n if (_cache) {\n Object.values(_cache.inflightTile).forEach(abortRequest);\n // Strings and colors are static and should not be re-populated\n _strings = _cache.strings;\n _colors = _cache.colors;\n }\n _cache = {\n data: {},\n loadedTile: {},\n inflightTile: {},\n inflightPost: {},\n closed: {},\n rtree: new RBush(),\n strings: _strings,\n colors: _colors\n };\n },\n\n loadIssues(projection) {\n let params = {\n // Tiles return a maximum # of issues\n // So we want to filter our request for only types iD supports\n item: _osmoseData.items\n };\n\n // determine the needed tiles to cover the view\n let tiles = tiler\n .zoomExtent([_tileZoom, _tileZoom])\n .getTiles(projection);\n\n // abort inflight requests that are no longer needed\n abortUnwantedRequests(_cache, tiles);\n\n // issue new requests..\n tiles.forEach(tile => {\n if (_cache.loadedTile[tile.id] || _cache.inflightTile[tile.id]) return;\n\n let [ x, y, z ] = tile.xyz;\n let url = `${_osmoseUrlRoot}/issues/${z}/${x}/${y}.mvt?` + utilQsString(params);\n\n let controller = new AbortController();\n _cache.inflightTile[tile.id] = controller;\n\n fetch(url, { signal: controller.signal })\n .then(data => data.arrayBuffer())\n .then(data => {\n delete _cache.inflightTile[tile.id];\n _cache.loadedTile[tile.id] = true;\n\n var vectorTile = new VectorTile(new Protobuf(data));\n data = vectorTile.layers.issues;\n const features = [];\n for (let i = 0; i < data.length; i++) {\n features.push(data.feature(i).toGeoJSON(x, y, z));\n }\n\n if (features.length > 0) {\n features.forEach(issue => {\n const { item, class: cl, uuid: id } = issue.properties;\n /* Osmose issues are uniquely identified by a unique\n `item` and `class` combination (both integer values) */\n const itemType = `${item}-${cl}`;\n\n // Filter out unsupported issue types (some are too specific or advanced)\n if (itemType in _osmoseData.icons) {\n let loc = issue.geometry.coordinates; // lon, lat\n loc = preventCoincident(loc);\n\n let d = new QAItem(loc, this, itemType, id, { item });\n\n // Setting elems here prevents UI detail requests\n if (item === 8300 || item === 8360) {\n d.elems = [];\n }\n\n _cache.data[d.id] = d;\n _cache.rtree.insert(encodeIssueRtree(d));\n }\n });\n }\n\n dispatch.call('loaded');\n })\n .catch(() => {\n delete _cache.inflightTile[tile.id];\n _cache.loadedTile[tile.id] = true;\n });\n });\n },\n\n loadIssueDetail(issue) {\n // Issue details only need to be fetched once\n if (issue.elems !== undefined) {\n return Promise.resolve(issue);\n }\n\n const url = `${_osmoseUrlRoot}/issue/${issue.id}?langs=${localizer.localeCode()}`;\n const cacheDetails = data => {\n // Associated elements used for highlighting\n // Assign directly for immediate use in the callback\n issue.elems = data.elems.map(e => e.type.substring(0,1) + e.id);\n\n // Some issues have instance specific detail in a subtitle\n issue.detail = data.subtitle ? marked(data.subtitle.auto) : '';\n\n this.replaceItem(issue);\n };\n\n return d3_json(url).then(cacheDetails).then(() => issue);\n },\n\n loadStrings(locale=localizer.localeCode()) {\n const items = Object.keys(_osmoseData.icons);\n\n if (\n locale in _cache.strings\n && Object.keys(_cache.strings[locale]).length === items.length\n ) {\n return Promise.resolve(_cache.strings[locale]);\n }\n\n // May be partially populated already if some requests were successful\n if (!(locale in _cache.strings)) {\n _cache.strings[locale] = {};\n }\n\n // Only need to cache strings for supported issue types\n // Using multiple individual item + class requests to reduce fetched data size\n const allRequests = items.map(itemType => {\n // No need to request data we already have\n if (itemType in _cache.strings[locale]) return null;\n\n const cacheData = data => {\n // Bunch of nested single value arrays of objects\n const [ cat = {items:[]} ] = data.categories;\n const [ item = {class:[]} ] = cat.items;\n const [ cl = null ] = item.class;\n\n // If null default value is reached, data wasn't as expected (or was empty)\n if (!cl) {\n /* eslint-disable no-console */\n console.log(`Osmose strings request (${itemType}) had unexpected data`);\n /* eslint-enable no-console */\n return;\n }\n\n // Cache served item colors to automatically style issue markers later\n const { item: itemInt, color } = item;\n if (/^#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/.test(color)) {\n _cache.colors[itemInt] = color;\n }\n\n // Value of root key will be null if no string exists\n // If string exists, value is an object with key 'auto' for string\n const { title, detail, fix, trap } = cl;\n\n // Osmose titles shouldn't contain markdown\n let issueStrings = {};\n if (title) issueStrings.title = title.auto;\n if (detail) issueStrings.detail = marked(detail.auto);\n if (trap) issueStrings.trap = marked(trap.auto);\n if (fix) issueStrings.fix = marked(fix.auto);\n\n _cache.strings[locale][itemType] = issueStrings;\n };\n\n const [ item, cl ] = itemType.split('-');\n\n // Osmose API falls back to English strings where untranslated or if locale doesn't exist\n const url = `${_osmoseUrlRoot}/items/${item}/class/${cl}?langs=${locale}`;\n\n return d3_json(url).then(cacheData);\n }).filter(Boolean);\n\n return Promise.all(allRequests).then(() => _cache.strings[locale]);\n },\n\n getStrings(itemType, locale=localizer.localeCode()) {\n // No need to fallback to English, Osmose API handles this for us\n return (locale in _cache.strings) ? _cache.strings[locale][itemType] : {};\n },\n\n getColor(itemType) {\n return (itemType in _cache.colors) ? _cache.colors[itemType] : '#FFFFFF';\n },\n\n postUpdate(issue, callback) {\n if (_cache.inflightPost[issue.id]) {\n return callback({ message: 'Issue update already inflight', status: -2 }, issue);\n }\n\n // UI sets the status to either 'done' or 'false'\n const url = `${_osmoseUrlRoot}/issue/${issue.id}/${issue.newStatus}`;\n const controller = new AbortController();\n const after = () => {\n delete _cache.inflightPost[issue.id];\n\n this.removeItem(issue);\n if (issue.newStatus === 'done') {\n // Keep track of the number of issues closed per `item` to tag the changeset\n if (!(issue.item in _cache.closed)) {\n _cache.closed[issue.item] = 0;\n }\n _cache.closed[issue.item] += 1;\n }\n if (callback) callback(null, issue);\n };\n\n _cache.inflightPost[issue.id] = controller;\n\n fetch(url, { signal: controller.signal })\n .then(after)\n .catch(err => {\n delete _cache.inflightPost[issue.id];\n if (callback) callback(err.message);\n });\n },\n\n // Get all cached QAItems covering the viewport\n getItems(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n\n return _cache.rtree.search(bbox).map(d => d.data);\n },\n\n // Get a QAItem from cache\n // NOTE: Don't change method name until UI v3 is merged\n getError(id) {\n return _cache.data[id];\n },\n\n // get the name of the icon to display for this item\n getIcon(itemType) {\n return _osmoseData.icons[itemType];\n },\n\n // Replace a single QAItem in the cache\n replaceItem(item) {\n if (!(item instanceof QAItem) || !item.id) return;\n\n _cache.data[item.id] = item;\n updateRtree(encodeIssueRtree(item), true); // true = replace\n return item;\n },\n\n // Remove a single QAItem from the cache\n removeItem(item) {\n if (!(item instanceof QAItem) || !item.id) return;\n\n delete _cache.data[item.id];\n updateRtree(encodeIssueRtree(item), false); // false = remove\n },\n\n // Used to populate `closed:osmose:*` changeset tags\n getClosedCounts() {\n return _cache.closed;\n },\n\n itemURL(item) {\n return `https://osmose.openstreetmap.fr/en/error/${item.id}`;\n }\n};\n", "import { geoScaleToZoom } from '../geo';\nimport { utilTiler } from './tiler';\nimport type { Projection } from '../geo/raw_mercator';\nimport type RBush from 'rbush';\nimport type { BBox } from 'rbush';\n\nexport interface WithBbox extends BBox {\n data: T;\n}\n\nexport function partitionViewport(projection: Projection) {\n let z = geoScaleToZoom(projection.scale());\n let z2 = (Math.ceil(z * 2) / 2) + 2.5; // round to next 0.5 and add 2.5\n let tiler = utilTiler().zoomExtent([z2, z2]);\n\n return (tiler.getTiles(projection) || []).map(tile => tile.extent);\n}\n\n\n/** no more than `limit` results per partition */\nexport function searchLimited(limit: number | undefined, projection: Projection, rtree: RBush>): T[] {\n limit ||= 5;\n\n return partitionViewport(projection)\n .flatMap((extent) => rtree.search(extent.bbox()).slice(0, limit))\n .map(result => result.data);\n}\n", "/* global mapillary:false */\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport Protobuf from 'pbf';\nimport RBush from 'rbush';\nimport { VectorTile } from '@mapbox/vector-tile';\nimport { geoExtent } from '../geo';\nimport { utilQsString, utilRebind, utilTiler, utilStringQs } from '../util';\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\n\nconst accessToken = 'MLY|4100327730013843|5bb78b81720791946a9a7b956c57b7cf';\nconst apiUrl = 'https://graph.mapillary.com/';\nconst baseTileUrl = 'https://tiles.mapillary.com/maps/vtp';\nconst mapFeatureTileUrl = `${baseTileUrl}/mly_map_feature_point/2/{z}/{x}/{y}?access_token=${accessToken}`;\nconst tileUrl = `${baseTileUrl}/mly1_public/2/{z}/{x}/{y}?access_token=${accessToken}`;\nconst trafficSignTileUrl = `${baseTileUrl}/mly_map_feature_traffic_sign/2/{z}/{x}/{y}?access_token=${accessToken}`;\n\nconst viewercss = 'mapillary-js/mapillary.css';\nconst viewerjs = 'mapillary-js/mapillary.js';\nconst minZoom = 14;\nconst dispatch = d3_dispatch('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'imageChanged');\n\nlet _loadViewerPromise;\nlet _mlyActiveImage;\nlet _mlyCache;\nlet _mlyFallback = false;\nlet _mlyHighlightedDetection;\nlet _mlyShowFeatureDetections = false;\nlet _mlyShowSignDetections = false;\nlet _mlyViewer;\nlet _mlyViewerFilter = ['all'];\nlet _isViewerOpen = false;\n\n\n// Load all data for the specified type from Mapillary vector tiles\nfunction loadTiles(which, url, maxZoom, projection) {\n const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);\n const tiles = tiler.getTiles(projection);\n\n tiles.forEach(function(tile) {\n loadTile(which, url, tile);\n });\n}\n\n\n// Load all data for the specified type from one vector tile\nfunction loadTile(which, url, tile) {\n const cache = _mlyCache.requests;\n const tileId = `${tile.id}-${which}`;\n if (cache.loaded[tileId] || cache.inflight[tileId]) return;\n const controller = new AbortController();\n cache.inflight[tileId] = controller;\n const requestUrl = url.replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n .replace('{z}', tile.xyz[2]);\n\n fetch(requestUrl, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (!data) {\n throw new Error('No Data');\n }\n loadTileDataToCache(data, tile, which);\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n } else if (which === 'signs') {\n dispatch.call('loadedSigns');\n } else if (which === 'points') {\n dispatch.call('loadedMapFeatures');\n }\n })\n .catch(function() {\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n });\n}\n\n\n// Load the data from the vector tile into cache\nfunction loadTileDataToCache(data, tile, which) {\n const vectorTile = new VectorTile(new Protobuf(data));\n let features,\n cache,\n layer,\n i,\n feature,\n loc,\n d;\n\n if (vectorTile.layers.hasOwnProperty('image')) {\n features = [];\n cache = _mlyCache.images;\n layer = vectorTile.layers.image;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n d = {\n service: 'photo',\n loc: loc,\n captured_at: feature.properties.captured_at,\n ca: feature.properties.compass_angle,\n id: feature.properties.id,\n is_pano: feature.properties.is_pano,\n sequence_id: feature.properties.sequence_id,\n };\n cache.forImageId[d.id] = d;\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty('sequence')) {\n cache = _mlyCache.sequences;\n layer = vectorTile.layers.sequence;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n if (cache.lineString[feature.properties.id]) {\n cache.lineString[feature.properties.id].push(feature);\n } else {\n cache.lineString[feature.properties.id] = [feature];\n }\n }\n }\n\n if (vectorTile.layers.hasOwnProperty('point')) {\n features = [];\n cache = _mlyCache[which];\n layer = vectorTile.layers.point;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n\n d = {\n service: 'photo',\n loc: loc,\n id: feature.properties.id,\n first_seen_at: feature.properties.first_seen_at,\n last_seen_at: feature.properties.last_seen_at,\n value: feature.properties.value\n };\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty('traffic_sign')) {\n features = [];\n cache = _mlyCache[which];\n layer = vectorTile.layers.traffic_sign;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n\n d = {\n service: 'photo',\n loc: loc,\n id: feature.properties.id,\n first_seen_at: feature.properties.first_seen_at,\n last_seen_at: feature.properties.last_seen_at,\n value: feature.properties.value\n };\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n}\n\n\n// Get data from the API\nfunction loadData(url) {\n return fetch(url)\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n })\n .then(function(result) {\n if (!result) {\n return [];\n }\n return result.data || [];\n });\n}\n\nexport default {\n // Initialize Mapillary\n init: function() {\n if (!_mlyCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n // Reset cache and state\n reset: function() {\n if (_mlyCache) {\n Object.values(_mlyCache.requests.inflight).forEach(function(request) { request.abort(); });\n }\n\n _mlyCache = {\n images: { rtree: new RBush(), forImageId: {} },\n image_detections: { forImageId: {} },\n signs: { rtree: new RBush() },\n points: { rtree: new RBush() },\n sequences: { rtree: new RBush(), lineString: {} },\n requests: { loaded: {}, inflight: {} }\n };\n\n _mlyActiveImage = null;\n },\n\n // Get visible images\n images: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _mlyCache.images.rtree);\n },\n\n // Get visible traffic signs\n signs: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _mlyCache.signs.rtree);\n },\n\n // Get visible map (point) features\n mapFeatures: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _mlyCache.points.rtree);\n },\n\n // Get cached image by id\n cachedImage: function(imageId) {\n return _mlyCache.images.forImageId[imageId];\n },\n\n // Get visible sequences\n sequences: function(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const sequenceIds = {};\n let lineStrings = [];\n\n _mlyCache.images.rtree.search(bbox)\n .forEach(function(d) {\n if (d.data.sequence_id) {\n sequenceIds[d.data.sequence_id] = true;\n }\n });\n\n Object.keys(sequenceIds).forEach(function(sequenceId) {\n if (_mlyCache.sequences.lineString[sequenceId]) {\n lineStrings = lineStrings.concat(_mlyCache.sequences.lineString[sequenceId]);\n }\n });\n\n return lineStrings;\n },\n\n\n // Load images in the visible area\n loadImages: function(projection) {\n loadTiles('images', tileUrl, 14, projection);\n },\n\n\n // Load traffic signs in the visible area\n loadSigns: function(projection) {\n loadTiles('signs', trafficSignTileUrl, 14, projection);\n },\n\n\n // Load map (point) features in the visible area\n loadMapFeatures: function(projection) {\n loadTiles('points', mapFeatureTileUrl, 14, projection);\n },\n\n\n // Return a promise that resolves when the image viewer (Mapillary JS) library has finished loading\n ensureViewerLoaded: function(context) {\n if (_loadViewerPromise) return _loadViewerPromise;\n\n // add mly-wrapper\n const wrap = context.container().select('.photoviewer')\n .selectAll('.mly-wrapper')\n .data([0]);\n\n wrap.enter()\n .append('div')\n .attr('id', 'ideditor-mly')\n .attr('class', 'photo-wrapper mly-wrapper')\n .classed('hide', true);\n\n const that = this;\n\n _loadViewerPromise = new Promise((resolve, reject) => {\n let loadedCount = 0;\n function loaded() {\n loadedCount += 1;\n\n // wait until both files are loaded\n if (loadedCount === 2) resolve();\n }\n\n const head = d3_select('head');\n\n // load mapillary-viewercss\n head.selectAll('#ideditor-mapillary-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-mapillary-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(viewercss))\n .on('load.serviceMapillary', loaded)\n .on('error.serviceMapillary', function() {\n reject();\n });\n\n // load mapillary-viewerjs\n head.selectAll('#ideditor-mapillary-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-mapillary-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(viewerjs))\n .on('load.serviceMapillary', loaded)\n .on('error.serviceMapillary', function() {\n reject();\n });\n })\n .catch(function() {\n _loadViewerPromise = null;\n })\n .then(function() {\n that.initViewer(context);\n });\n\n return _loadViewerPromise;\n },\n\n\n // Load traffic sign image sprites\n loadSignResources: function(context) {\n context.ui().svgDefs.addSprites(['mapillary-sprite'], false /* don't override colors */ );\n return this;\n },\n\n\n // Load map (point) feature image sprites\n loadObjectResources: function(context) {\n context.ui().svgDefs.addSprites(['mapillary-object-sprite'], false /* don't override colors */ );\n return this;\n },\n\n\n // Remove previous detections in image viewer\n resetTags: function() {\n if (_mlyViewer && !_mlyFallback) {\n _mlyViewer.getComponent('tag').removeAll();\n }\n },\n\n\n // Show map feature detections in image viewer\n showFeatureDetections: function(value) {\n _mlyShowFeatureDetections = value;\n if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {\n this.resetTags();\n }\n },\n\n\n // Show traffic sign detections in image viewer\n showSignDetections: function(value) {\n _mlyShowSignDetections = value;\n if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {\n this.resetTags();\n }\n },\n\n\n // Apply filter to image viewer\n filterViewer: function(context) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n const filter = ['all'];\n\n if (!showsPano) filter.push([ '!=', 'cameraType', 'spherical' ]);\n if (!showsFlat && showsPano) filter.push(['==', 'pano', true]);\n if (fromDate) {\n filter.push(['>=', 'capturedAt', new Date(fromDate).getTime()]);\n }\n if (toDate) {\n filter.push(['>=', 'capturedAt', new Date(toDate).getTime()]);\n }\n\n if (_mlyViewer) {\n _mlyViewer.setFilter(filter);\n }\n _mlyViewerFilter = filter;\n\n return filter;\n },\n\n\n // Make the image viewer visible\n showViewer: function(context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.mly-wrapper.hide').size();\n\n if (isHidden && _mlyViewer) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.mly-wrapper')\n .classed('hide', false);\n\n _mlyViewer.resize();\n }\n\n _isViewerOpen = true;\n return this;\n },\n\n\n // Hide the image viewer and resets map markers\n hideViewer: function(context) {\n _mlyActiveImage = null;\n\n if (!_mlyFallback && _mlyViewer) {\n _mlyViewer.getComponent('sequence').stop();\n }\n\n const viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n this.updateUrlImage(null);\n\n dispatch.call('imageChanged');\n dispatch.call('loadedMapFeatures');\n dispatch.call('loadedSigns');\n\n _isViewerOpen = false;\n\n return this.setStyles(context, null);\n },\n\n\n // Get viewer status\n isViewerOpen: function() {\n return _isViewerOpen;\n },\n\n\n // Update the URL with current image id\n updateUrlImage: function(imageId) {\n const hash = utilStringQs(window.location.hash);\n if (imageId) {\n hash.photo = 'mapillary/' + imageId;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n\n // Highlight the detection in the viewer that is related to the clicked map feature\n highlightDetection: function(detection) {\n if (detection) {\n _mlyHighlightedDetection = detection.id;\n }\n\n return this;\n },\n\n\n // Initialize image viewer (Mapillar JS)\n initViewer: function(context) {\n if (!window.mapillary) return;\n\n const opts = {\n accessToken: accessToken,\n component: {\n cover: false,\n keyboard: false,\n tag: true\n },\n container: 'ideditor-mly',\n };\n\n // Disable components requiring WebGL support\n if (!mapillary.isSupported() && mapillary.isFallbackSupported()) {\n _mlyFallback = true;\n opts.component = {\n cover: false,\n direction: false,\n imagePlane: false,\n keyboard: false,\n mouse: false,\n sequence: false,\n tag: false,\n image: true, // fallback\n navigation: true // fallback\n };\n }\n\n _mlyViewer = new mapillary.Viewer(opts);\n _mlyViewer.on('image', imageChanged.bind(this));\n _mlyViewer.on('bearing', bearingChanged);\n\n if (_mlyViewerFilter) {\n _mlyViewer.setFilter(_mlyViewerFilter);\n }\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.mapillary', function() {\n if (_mlyViewer) _mlyViewer.resize();\n });\n\n // imageChanged: called after the viewer has changed images and is ready.\n function imageChanged(photo) {\n this.resetTags();\n const image = photo.image;\n this.setActiveImage(image);\n this.setStyles(context, null);\n const loc = [image.originalLngLat.lng, image.originalLngLat.lat];\n context.map().centerEase(loc);\n this.updateUrlImage(image.id);\n\n if (_mlyShowFeatureDetections || _mlyShowSignDetections) {\n this.updateDetections(image.id, `${apiUrl}/${image.id}/detections?access_token=${accessToken}&fields=id,image,geometry,value`);\n }\n dispatch.call('imageChanged');\n }\n\n\n // bearingChanged: called when the bearing changes in the image viewer.\n function bearingChanged(e) {\n dispatch.call('bearingChanged', undefined, e);\n }\n },\n\n\n // Move to an image\n selectImage: function(context, imageId) {\n if (_mlyViewer && imageId) {\n _mlyViewer.moveTo(imageId)\n .then(image => this.setActiveImage(image))\n .catch(function(e) {\n console.error('mly3', e); // eslint-disable-line no-console\n });\n }\n\n return this;\n },\n\n\n // Return the currently displayed image\n getActiveImage: function() {\n return _mlyActiveImage;\n },\n\n\n // Return a list of detection objects for the given id\n getDetections: function(id) {\n return loadData(`${apiUrl}/${id}/detections?access_token=${accessToken}&fields=id,value,image`);\n },\n\n\n // Set the currently visible image\n setActiveImage: function(image) {\n if (image) {\n _mlyActiveImage = {\n ca: image.originalCompassAngle,\n id: image.id,\n loc: [image.originalLngLat.lng, image.originalLngLat.lat],\n is_pano: image.cameraType === 'spherical',\n sequence_id: image.sequenceId\n };\n } else {\n _mlyActiveImage = null;\n }\n },\n\n\n // Update the currently highlighted sequence and selected bubble.\n setStyles: function(context, hovered) {\n const hoveredImageId = hovered && hovered.id;\n const hoveredSequenceId = hovered && hovered.sequence_id;\n const selectedSequenceId = _mlyActiveImage && _mlyActiveImage.sequence_id;\n\n context.container().selectAll('.layer-mapillary .viewfield-group')\n .classed('highlighted', function(d) { return (d.sequence_id === selectedSequenceId) || (d.id === hoveredImageId); })\n .classed('hovered', function(d) { return d.id === hoveredImageId; });\n\n context.container().selectAll('.layer-mapillary .sequence')\n .classed('highlighted', function(d) { return d.properties.id === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.id === selectedSequenceId; });\n\n return this;\n },\n\n\n // Get detections for the current image and shows them in the image viewer\n updateDetections: function(imageId, url) {\n if (!_mlyViewer || _mlyFallback) return;\n if (!imageId) return;\n const cache = _mlyCache.image_detections;\n if (cache.forImageId[imageId]) {\n showDetections(_mlyCache.image_detections.forImageId[imageId]);\n } else {\n loadData(url)\n .then(detections => {\n detections.forEach(function(detection) {\n if (!cache.forImageId[imageId]) {\n cache.forImageId[imageId] = [];\n }\n cache.forImageId[imageId].push({\n geometry: detection.geometry,\n id: detection.id,\n image_id: imageId,\n value:detection.value\n });\n });\n\n showDetections(_mlyCache.image_detections.forImageId[imageId] || []);\n });\n }\n\n\n // Create a tag for each detection and shows it in the image viewer\n function showDetections(detections) {\n const tagComponent = _mlyViewer.getComponent('tag');\n detections.forEach(function(data) {\n const tag = makeTag(data);\n if (tag) {\n tagComponent.add([tag]);\n }\n });\n }\n\n\n // Create a Mapillary JS tag object\n function makeTag(data) {\n const valueParts = data.value.split('--');\n if (!valueParts.length) return;\n\n let tag;\n let text;\n let color = 0xffffff;\n\n if (_mlyHighlightedDetection === data.id) {\n color = 0xffff00;\n text = valueParts[1];\n if (text === 'flat' || text === 'discrete' || text === 'sign') {\n text = valueParts[2];\n }\n text = text.replace(/-/g, ' ');\n text = text.charAt(0).toUpperCase() + text.slice(1);\n _mlyHighlightedDetection = null;\n }\n\n var decodedGeometry = window.atob(data.geometry);\n var uintArray = new Uint8Array(decodedGeometry.length);\n for (var i = 0; i < decodedGeometry.length; i++) {\n uintArray[i] = decodedGeometry.charCodeAt(i);\n }\n const tile = new VectorTile(new Protobuf(uintArray.buffer));\n const layer = tile.layers['mpy-or'];\n\n const geometries = layer.feature(0).loadGeometry();\n\n const polygon = geometries.map(ring =>\n ring.map(point =>\n [point.x / layer.extent, point.y / layer.extent]));\n\n tag = new mapillary.OutlineTag(\n data.id,\n new mapillary.PolygonGeometry(polygon[0]),\n {\n text: text,\n textColor: color,\n lineColor: color,\n lineWidth: 2,\n fillColor: color,\n fillOpacity: 0.3,\n }\n );\n\n return tag;\n }\n },\n\n\n // Return the current cache\n cache: function() {\n return _mlyCache;\n }\n};\n", "import { osmAreaKeys as areaKeys } from '../osm/tags';\nimport { utilArrayIntersection } from '../util';\nimport { validationIssue } from '../core/validation';\n\n\nvar buildRuleChecks = function() {\n return {\n equals: function (equals) {\n return function(tags) {\n return Object.keys(equals).every(function(k) {\n return equals[k] === tags[k];\n });\n };\n },\n notEquals: function (notEquals) {\n return function(tags) {\n return Object.keys(notEquals).some(function(k) {\n return notEquals[k] !== tags[k];\n });\n };\n },\n absence: function(absence) {\n return function(tags) {\n return Object.keys(tags).indexOf(absence) === -1;\n };\n },\n presence: function(presence) {\n return function(tags) {\n return Object.keys(tags).indexOf(presence) > -1;\n };\n },\n greaterThan: function(greaterThan) {\n var key = Object.keys(greaterThan)[0];\n var value = greaterThan[key];\n\n return function(tags) {\n return tags[key] > value;\n };\n },\n greaterThanEqual: function(greaterThanEqual) {\n var key = Object.keys(greaterThanEqual)[0];\n var value = greaterThanEqual[key];\n\n return function(tags) {\n return tags[key] >= value;\n };\n },\n lessThan: function(lessThan) {\n var key = Object.keys(lessThan)[0];\n var value = lessThan[key];\n\n return function(tags) {\n return tags[key] < value;\n };\n },\n lessThanEqual: function(lessThanEqual) {\n var key = Object.keys(lessThanEqual)[0];\n var value = lessThanEqual[key];\n\n return function(tags) {\n return tags[key] <= value;\n };\n },\n positiveRegex: function(positiveRegex) {\n var tagKey = Object.keys(positiveRegex)[0];\n var expression = positiveRegex[tagKey].join('|');\n var regex = new RegExp(expression);\n\n return function(tags) {\n return regex.test(tags[tagKey]);\n };\n },\n negativeRegex: function(negativeRegex) {\n var tagKey = Object.keys(negativeRegex)[0];\n var expression = negativeRegex[tagKey].join('|');\n var regex = new RegExp(expression);\n\n return function(tags) {\n return !regex.test(tags[tagKey]);\n };\n }\n };\n};\n\nvar buildLineKeys = function() {\n return {\n highway: {\n rest_area: true,\n services: true\n },\n railway: {\n roundhouse: true,\n station: true,\n traverser: true,\n turntable: true,\n wash: true\n }\n };\n};\n\nexport default {\n init: function() {\n this._ruleChecks = buildRuleChecks();\n this._validationRules = [];\n this._areaKeys = areaKeys;\n this._lineKeys = buildLineKeys();\n },\n\n // list of rules only relevant to tag checks...\n filterRuleChecks: function(selector) {\n var _ruleChecks = this._ruleChecks;\n return Object.keys(selector).reduce(function(rules, key) {\n if (['geometry', 'error', 'warning'].indexOf(key) === -1) {\n rules.push(_ruleChecks[key](selector[key]));\n }\n return rules;\n }, []);\n },\n\n // builds tagMap from mapcss-parse selector object...\n buildTagMap: function(selector) {\n var getRegexValues = function(regexes) {\n return regexes.map(function(regex) {\n return regex.replace(/\\$|\\^/g, '');\n });\n };\n\n var tagMap = Object.keys(selector).reduce(function (expectedTags, key) {\n var values;\n var isRegex = /regex/gi.test(key);\n var isEqual = /equals/gi.test(key);\n\n if (isRegex || isEqual) {\n Object.keys(selector[key]).forEach(function(selectorKey) {\n values = isEqual ? [selector[key][selectorKey]] : getRegexValues(selector[key][selectorKey]);\n\n if (expectedTags.hasOwnProperty(selectorKey)) {\n values = values.concat(expectedTags[selectorKey]);\n }\n\n expectedTags[selectorKey] = values;\n });\n\n } else if (/(greater|less)Than(Equal)?|presence/g.test(key)) {\n var tagKey = /presence/.test(key) ? selector[key] : Object.keys(selector[key])[0];\n\n values = [selector[key][tagKey]];\n\n if (expectedTags.hasOwnProperty(tagKey)) {\n values = values.concat(expectedTags[tagKey]);\n }\n\n expectedTags[tagKey] = values;\n }\n\n return expectedTags;\n }, {});\n\n return tagMap;\n },\n\n // inspired by osmWay#isArea()\n inferGeometry: function(tagMap) {\n var _lineKeys = this._lineKeys;\n var _areaKeys = this._areaKeys;\n\n var keyValueDoesNotImplyArea = function(key) {\n return utilArrayIntersection(tagMap[key], Object.keys(_areaKeys[key])).length > 0;\n };\n var keyValueImpliesLine = function(key) {\n return utilArrayIntersection(tagMap[key], Object.keys(_lineKeys[key])).length > 0;\n };\n\n if (tagMap.hasOwnProperty('area')) {\n if (tagMap.area.indexOf('yes') > -1) {\n return 'area';\n }\n if (tagMap.area.indexOf('no') > -1) {\n return 'line';\n }\n }\n\n for (var key in tagMap) {\n if (key in _areaKeys && !keyValueDoesNotImplyArea(key)) {\n return 'area';\n }\n if (key in _lineKeys && keyValueImpliesLine(key)) {\n return 'area';\n }\n }\n\n return 'line';\n },\n\n // adds from mapcss-parse selector check...\n addRule: function(selector) {\n var rule = {\n // checks relevant to mapcss-selector\n checks: this.filterRuleChecks(selector),\n // true if all conditions for a tag error are true..\n matches: function(entity) {\n return this.checks.every(function(check) {\n return check(entity.tags);\n });\n },\n // borrowed from Way#isArea()\n inferredGeometry: this.inferGeometry(this.buildTagMap(selector), this._areaKeys),\n geometryMatches: function(entity, graph) {\n if (entity.type === 'node' || entity.type === 'relation') {\n return selector.geometry === entity.type;\n } else if (entity.type === 'way') {\n return this.inferredGeometry === entity.geometry(graph);\n }\n },\n // when geometries match and tag matches are present, return a warning...\n findIssues: function (entity, graph, issues) {\n if (this.geometryMatches(entity, graph) && this.matches(entity)) {\n var severity = Object.keys(selector).indexOf('error') > -1\n ? 'error'\n : 'warning';\n var message = selector[severity];\n issues.push(new validationIssue({\n type: 'maprules',\n severity: severity,\n message: function() {\n return message;\n },\n entityIds: [entity.id]\n }));\n }\n }\n };\n this._validationRules.push(rule);\n },\n\n clearRules: function() { this._validationRules = []; },\n\n // returns validationRules...\n validationRules: function() { return this._validationRules; },\n\n // returns ruleChecks\n ruleChecks: function() { return this._ruleChecks; }\n};\n", "import { json as d3_json } from 'd3-fetch';\n\nimport RBush from 'rbush';\nimport { geoExtent } from '../geo';\nimport { utilQsString } from '../util';\nimport { localizer } from '../core';\n\nimport { nominatimApiUrl } from '../../config/id.js';\n\n\nvar apibase = nominatimApiUrl;\nvar _inflight = {};\nvar _nominatimCache;\n\n\nexport default {\n\n init: function() {\n _inflight = {};\n _nominatimCache = new RBush();\n },\n\n reset: function() {\n Object.values(_inflight).forEach(function(controller) { controller.abort(); });\n _inflight = {};\n _nominatimCache = new RBush();\n },\n\n\n countryCode: function (location, callback) {\n this.reverse(location, function(err, result) {\n if (err) {\n return callback(err);\n } else if (result.address) {\n return callback(null, result.address.country_code);\n } else {\n return callback('Unable to geocode', null);\n }\n });\n },\n\n\n reverse: function (loc, callback) {\n var cached = _nominatimCache.search(\n { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1] }\n );\n\n if (cached.length > 0) {\n if (callback) callback(null, cached[0].data);\n return;\n }\n\n var params = { zoom: 13, format: 'json', addressdetails: 1, lat: loc[1], lon: loc[0] };\n var url = apibase + 'reverse?' + utilQsString(params);\n\n if (_inflight[url]) return;\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, {\n signal: controller.signal,\n headers: {\n 'Accept-Language': localizer.localeCodes().join(',')\n }\n })\n .then(function(result) {\n delete _inflight[url];\n if (result && result.error) {\n throw new Error(result.error);\n }\n var extent = geoExtent(loc).padByMeters(200);\n _nominatimCache.insert(Object.assign(extent.bbox(), {data: result}));\n if (callback) callback(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (callback) callback(err.message);\n });\n },\n\n\n search: function (val, callback) {\n const params = {\n q: val,\n limit:10,\n format: 'json'\n };\n var url = apibase + 'search?' + utilQsString(params);\n\n if (_inflight[url]) return;\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, {\n signal: controller.signal,\n headers: {\n 'Accept-Language': localizer.localeCodes().join(',')\n }\n })\n .then(function(result) {\n delete _inflight[url];\n if (result && result.error) {\n throw new Error(result.error);\n }\n if (callback) callback(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (callback) callback(err.message);\n });\n }\n\n};\n", "(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n\ttypeof define === 'function' && define.amd ? define(factory) :\n\t(global.quickselect = factory());\n}(this, (function () { 'use strict';\n\nfunction quickselect(arr, k, left, right, compare) {\n quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);\n}\n\nfunction quickselectStep(arr, k, left, right, compare) {\n\n while (right > left) {\n if (right - left > 600) {\n var n = right - left + 1;\n var m = k - left + 1;\n var z = Math.log(n);\n var s = 0.5 * Math.exp(2 * z / 3);\n var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);\n var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));\n var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));\n quickselectStep(arr, k, newLeft, newRight, compare);\n }\n\n var t = arr[k];\n var i = left;\n var j = right;\n\n swap(arr, left, k);\n if (compare(arr[right], t) > 0) swap(arr, left, right);\n\n while (i < j) {\n swap(arr, i, j);\n i++;\n j--;\n while (compare(arr[i], t) < 0) i++;\n while (compare(arr[j], t) > 0) j--;\n }\n\n if (compare(arr[left], t) === 0) swap(arr, left, j);\n else {\n j++;\n swap(arr, j, right);\n }\n\n if (j <= k) left = j + 1;\n if (k <= j) right = j - 1;\n }\n}\n\nfunction swap(arr, i, j) {\n var tmp = arr[i];\n arr[i] = arr[j];\n arr[j] = tmp;\n}\n\nfunction defaultCompare(a, b) {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\nreturn quickselect;\n\n})));\n", "'use strict';\n\nmodule.exports = rbush;\nmodule.exports.default = rbush;\n\nvar quickselect = require('quickselect');\n\nfunction rbush(maxEntries, format) {\n if (!(this instanceof rbush)) return new rbush(maxEntries, format);\n\n // max entries in a node is 9 by default; min node fill is 40% for best performance\n this._maxEntries = Math.max(4, maxEntries || 9);\n this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));\n\n if (format) {\n this._initFormat(format);\n }\n\n this.clear();\n}\n\nrbush.prototype = {\n\n all: function () {\n return this._all(this.data, []);\n },\n\n search: function (bbox) {\n\n var node = this.data,\n result = [],\n toBBox = this.toBBox;\n\n if (!intersects(bbox, node)) return result;\n\n var nodesToSearch = [],\n i, len, child, childBBox;\n\n while (node) {\n for (i = 0, len = node.children.length; i < len; i++) {\n\n child = node.children[i];\n childBBox = node.leaf ? toBBox(child) : child;\n\n if (intersects(bbox, childBBox)) {\n if (node.leaf) result.push(child);\n else if (contains(bbox, childBBox)) this._all(child, result);\n else nodesToSearch.push(child);\n }\n }\n node = nodesToSearch.pop();\n }\n\n return result;\n },\n\n collides: function (bbox) {\n\n var node = this.data,\n toBBox = this.toBBox;\n\n if (!intersects(bbox, node)) return false;\n\n var nodesToSearch = [],\n i, len, child, childBBox;\n\n while (node) {\n for (i = 0, len = node.children.length; i < len; i++) {\n\n child = node.children[i];\n childBBox = node.leaf ? toBBox(child) : child;\n\n if (intersects(bbox, childBBox)) {\n if (node.leaf || contains(bbox, childBBox)) return true;\n nodesToSearch.push(child);\n }\n }\n node = nodesToSearch.pop();\n }\n\n return false;\n },\n\n load: function (data) {\n if (!(data && data.length)) return this;\n\n if (data.length < this._minEntries) {\n for (var i = 0, len = data.length; i < len; i++) {\n this.insert(data[i]);\n }\n return this;\n }\n\n // recursively build the tree with the given data from scratch using OMT algorithm\n var node = this._build(data.slice(), 0, data.length - 1, 0);\n\n if (!this.data.children.length) {\n // save as is if tree is empty\n this.data = node;\n\n } else if (this.data.height === node.height) {\n // split root if trees have the same height\n this._splitRoot(this.data, node);\n\n } else {\n if (this.data.height < node.height) {\n // swap trees if inserted one is bigger\n var tmpNode = this.data;\n this.data = node;\n node = tmpNode;\n }\n\n // insert the small tree into the large tree at appropriate level\n this._insert(node, this.data.height - node.height - 1, true);\n }\n\n return this;\n },\n\n insert: function (item) {\n if (item) this._insert(item, this.data.height - 1);\n return this;\n },\n\n clear: function () {\n this.data = createNode([]);\n return this;\n },\n\n remove: function (item, equalsFn) {\n if (!item) return this;\n\n var node = this.data,\n bbox = this.toBBox(item),\n path = [],\n indexes = [],\n i, parent, index, goingUp;\n\n // depth-first iterative tree traversal\n while (node || path.length) {\n\n if (!node) { // go up\n node = path.pop();\n parent = path[path.length - 1];\n i = indexes.pop();\n goingUp = true;\n }\n\n if (node.leaf) { // check current node\n index = findItem(item, node.children, equalsFn);\n\n if (index !== -1) {\n // item found, remove the item and condense tree upwards\n node.children.splice(index, 1);\n path.push(node);\n this._condense(path);\n return this;\n }\n }\n\n if (!goingUp && !node.leaf && contains(node, bbox)) { // go down\n path.push(node);\n indexes.push(i);\n i = 0;\n parent = node;\n node = node.children[0];\n\n } else if (parent) { // go right\n i++;\n node = parent.children[i];\n goingUp = false;\n\n } else node = null; // nothing found\n }\n\n return this;\n },\n\n toBBox: function (item) { return item; },\n\n compareMinX: compareNodeMinX,\n compareMinY: compareNodeMinY,\n\n toJSON: function () { return this.data; },\n\n fromJSON: function (data) {\n this.data = data;\n return this;\n },\n\n _all: function (node, result) {\n var nodesToSearch = [];\n while (node) {\n if (node.leaf) result.push.apply(result, node.children);\n else nodesToSearch.push.apply(nodesToSearch, node.children);\n\n node = nodesToSearch.pop();\n }\n return result;\n },\n\n _build: function (items, left, right, height) {\n\n var N = right - left + 1,\n M = this._maxEntries,\n node;\n\n if (N <= M) {\n // reached leaf level; return leaf\n node = createNode(items.slice(left, right + 1));\n calcBBox(node, this.toBBox);\n return node;\n }\n\n if (!height) {\n // target height of the bulk-loaded tree\n height = Math.ceil(Math.log(N) / Math.log(M));\n\n // target number of root entries to maximize storage utilization\n M = Math.ceil(N / Math.pow(M, height - 1));\n }\n\n node = createNode([]);\n node.leaf = false;\n node.height = height;\n\n // split the items into M mostly square tiles\n\n var N2 = Math.ceil(N / M),\n N1 = N2 * Math.ceil(Math.sqrt(M)),\n i, j, right2, right3;\n\n multiSelect(items, left, right, N1, this.compareMinX);\n\n for (i = left; i <= right; i += N1) {\n\n right2 = Math.min(i + N1 - 1, right);\n\n multiSelect(items, i, right2, N2, this.compareMinY);\n\n for (j = i; j <= right2; j += N2) {\n\n right3 = Math.min(j + N2 - 1, right2);\n\n // pack each entry recursively\n node.children.push(this._build(items, j, right3, height - 1));\n }\n }\n\n calcBBox(node, this.toBBox);\n\n return node;\n },\n\n _chooseSubtree: function (bbox, node, level, path) {\n\n var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;\n\n while (true) {\n path.push(node);\n\n if (node.leaf || path.length - 1 === level) break;\n\n minArea = minEnlargement = Infinity;\n\n for (i = 0, len = node.children.length; i < len; i++) {\n child = node.children[i];\n area = bboxArea(child);\n enlargement = enlargedArea(bbox, child) - area;\n\n // choose entry with the least area enlargement\n if (enlargement < minEnlargement) {\n minEnlargement = enlargement;\n minArea = area < minArea ? area : minArea;\n targetNode = child;\n\n } else if (enlargement === minEnlargement) {\n // otherwise choose one with the smallest area\n if (area < minArea) {\n minArea = area;\n targetNode = child;\n }\n }\n }\n\n node = targetNode || node.children[0];\n }\n\n return node;\n },\n\n _insert: function (item, level, isNode) {\n\n var toBBox = this.toBBox,\n bbox = isNode ? item : toBBox(item),\n insertPath = [];\n\n // find the best node for accommodating the item, saving all nodes along the path too\n var node = this._chooseSubtree(bbox, this.data, level, insertPath);\n\n // put the item into the node\n node.children.push(item);\n extend(node, bbox);\n\n // split on node overflow; propagate upwards if necessary\n while (level >= 0) {\n if (insertPath[level].children.length > this._maxEntries) {\n this._split(insertPath, level);\n level--;\n } else break;\n }\n\n // adjust bboxes along the insertion path\n this._adjustParentBBoxes(bbox, insertPath, level);\n },\n\n // split overflowed node into two\n _split: function (insertPath, level) {\n\n var node = insertPath[level],\n M = node.children.length,\n m = this._minEntries;\n\n this._chooseSplitAxis(node, m, M);\n\n var splitIndex = this._chooseSplitIndex(node, m, M);\n\n var newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));\n newNode.height = node.height;\n newNode.leaf = node.leaf;\n\n calcBBox(node, this.toBBox);\n calcBBox(newNode, this.toBBox);\n\n if (level) insertPath[level - 1].children.push(newNode);\n else this._splitRoot(node, newNode);\n },\n\n _splitRoot: function (node, newNode) {\n // split root node\n this.data = createNode([node, newNode]);\n this.data.height = node.height + 1;\n this.data.leaf = false;\n calcBBox(this.data, this.toBBox);\n },\n\n _chooseSplitIndex: function (node, m, M) {\n\n var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index;\n\n minOverlap = minArea = Infinity;\n\n for (i = m; i <= M - m; i++) {\n bbox1 = distBBox(node, 0, i, this.toBBox);\n bbox2 = distBBox(node, i, M, this.toBBox);\n\n overlap = intersectionArea(bbox1, bbox2);\n area = bboxArea(bbox1) + bboxArea(bbox2);\n\n // choose distribution with minimum overlap\n if (overlap < minOverlap) {\n minOverlap = overlap;\n index = i;\n\n minArea = area < minArea ? area : minArea;\n\n } else if (overlap === minOverlap) {\n // otherwise choose distribution with minimum area\n if (area < minArea) {\n minArea = area;\n index = i;\n }\n }\n }\n\n return index;\n },\n\n // sorts node children by the best axis for split\n _chooseSplitAxis: function (node, m, M) {\n\n var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX,\n compareMinY = node.leaf ? this.compareMinY : compareNodeMinY,\n xMargin = this._allDistMargin(node, m, M, compareMinX),\n yMargin = this._allDistMargin(node, m, M, compareMinY);\n\n // if total distributions margin value is minimal for x, sort by minX,\n // otherwise it's already sorted by minY\n if (xMargin < yMargin) node.children.sort(compareMinX);\n },\n\n // total margin of all possible split distributions where each node is at least m full\n _allDistMargin: function (node, m, M, compare) {\n\n node.children.sort(compare);\n\n var toBBox = this.toBBox,\n leftBBox = distBBox(node, 0, m, toBBox),\n rightBBox = distBBox(node, M - m, M, toBBox),\n margin = bboxMargin(leftBBox) + bboxMargin(rightBBox),\n i, child;\n\n for (i = m; i < M - m; i++) {\n child = node.children[i];\n extend(leftBBox, node.leaf ? toBBox(child) : child);\n margin += bboxMargin(leftBBox);\n }\n\n for (i = M - m - 1; i >= m; i--) {\n child = node.children[i];\n extend(rightBBox, node.leaf ? toBBox(child) : child);\n margin += bboxMargin(rightBBox);\n }\n\n return margin;\n },\n\n _adjustParentBBoxes: function (bbox, path, level) {\n // adjust bboxes along the given tree path\n for (var i = level; i >= 0; i--) {\n extend(path[i], bbox);\n }\n },\n\n _condense: function (path) {\n // go through the path, removing empty nodes and updating bboxes\n for (var i = path.length - 1, siblings; i >= 0; i--) {\n if (path[i].children.length === 0) {\n if (i > 0) {\n siblings = path[i - 1].children;\n siblings.splice(siblings.indexOf(path[i]), 1);\n\n } else this.clear();\n\n } else calcBBox(path[i], this.toBBox);\n }\n },\n\n _initFormat: function (format) {\n // data format (minX, minY, maxX, maxY accessors)\n\n // uses eval-type function compilation instead of just accepting a toBBox function\n // because the algorithms are very sensitive to sorting functions performance,\n // so they should be dead simple and without inner calls\n\n var compareArr = ['return a', ' - b', ';'];\n\n this.compareMinX = new Function('a', 'b', compareArr.join(format[0]));\n this.compareMinY = new Function('a', 'b', compareArr.join(format[1]));\n\n this.toBBox = new Function('a',\n 'return {minX: a' + format[0] +\n ', minY: a' + format[1] +\n ', maxX: a' + format[2] +\n ', maxY: a' + format[3] + '};');\n }\n};\n\nfunction findItem(item, items, equalsFn) {\n if (!equalsFn) return items.indexOf(item);\n\n for (var i = 0; i < items.length; i++) {\n if (equalsFn(item, items[i])) return i;\n }\n return -1;\n}\n\n// calculate node's bbox from bboxes of its children\nfunction calcBBox(node, toBBox) {\n distBBox(node, 0, node.children.length, toBBox, node);\n}\n\n// min bounding rectangle of node children from k to p-1\nfunction distBBox(node, k, p, toBBox, destNode) {\n if (!destNode) destNode = createNode(null);\n destNode.minX = Infinity;\n destNode.minY = Infinity;\n destNode.maxX = -Infinity;\n destNode.maxY = -Infinity;\n\n for (var i = k, child; i < p; i++) {\n child = node.children[i];\n extend(destNode, node.leaf ? toBBox(child) : child);\n }\n\n return destNode;\n}\n\nfunction extend(a, b) {\n a.minX = Math.min(a.minX, b.minX);\n a.minY = Math.min(a.minY, b.minY);\n a.maxX = Math.max(a.maxX, b.maxX);\n a.maxY = Math.max(a.maxY, b.maxY);\n return a;\n}\n\nfunction compareNodeMinX(a, b) { return a.minX - b.minX; }\nfunction compareNodeMinY(a, b) { return a.minY - b.minY; }\n\nfunction bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }\nfunction bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }\n\nfunction enlargedArea(a, b) {\n return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *\n (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));\n}\n\nfunction intersectionArea(a, b) {\n var minX = Math.max(a.minX, b.minX),\n minY = Math.max(a.minY, b.minY),\n maxX = Math.min(a.maxX, b.maxX),\n maxY = Math.min(a.maxY, b.maxY);\n\n return Math.max(0, maxX - minX) *\n Math.max(0, maxY - minY);\n}\n\nfunction contains(a, b) {\n return a.minX <= b.minX &&\n a.minY <= b.minY &&\n b.maxX <= a.maxX &&\n b.maxY <= a.maxY;\n}\n\nfunction intersects(a, b) {\n return b.minX <= a.maxX &&\n b.minY <= a.maxY &&\n b.maxX >= a.minX &&\n b.maxY >= a.minY;\n}\n\nfunction createNode(children) {\n return {\n children: children,\n height: 1,\n leaf: true,\n minX: Infinity,\n minY: Infinity,\n maxX: -Infinity,\n maxY: -Infinity\n };\n}\n\n// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;\n// combines selection algorithm with binary divide & conquer approach\n\nfunction multiSelect(arr, left, right, n, compare) {\n var stack = [left, right],\n mid;\n\n while (stack.length) {\n right = stack.pop();\n left = stack.pop();\n\n if (right - left <= n) continue;\n\n mid = left + Math.ceil((right - left) / n / 2) * n;\n quickselect(arr, mid, left, right, compare);\n\n stack.push(left, mid, mid, right);\n }\n}\n", "'use strict';\n\nmodule.exports = lineclip;\n\nlineclip.polyline = lineclip;\nlineclip.polygon = polygonclip;\n\n\n// Cohen-Sutherland line clippign algorithm, adapted to efficiently\n// handle polylines rather than just segments\n\nfunction lineclip(points, bbox, result) {\n\n var len = points.length,\n codeA = bitCode(points[0], bbox),\n part = [],\n i, a, b, codeB, lastCode;\n\n if (!result) result = [];\n\n for (i = 1; i < len; i++) {\n a = points[i - 1];\n b = points[i];\n codeB = lastCode = bitCode(b, bbox);\n\n while (true) {\n\n if (!(codeA | codeB)) { // accept\n part.push(a);\n\n if (codeB !== lastCode) { // segment went outside\n part.push(b);\n\n if (i < len - 1) { // start a new line\n result.push(part);\n part = [];\n }\n } else if (i === len - 1) {\n part.push(b);\n }\n break;\n\n } else if (codeA & codeB) { // trivial reject\n break;\n\n } else if (codeA) { // a outside, intersect with clip edge\n a = intersect(a, b, codeA, bbox);\n codeA = bitCode(a, bbox);\n\n } else { // b outside\n b = intersect(a, b, codeB, bbox);\n codeB = bitCode(b, bbox);\n }\n }\n\n codeA = lastCode;\n }\n\n if (part.length) result.push(part);\n\n return result;\n}\n\n// Sutherland-Hodgeman polygon clipping algorithm\n\nfunction polygonclip(points, bbox) {\n\n var result, edge, prev, prevInside, i, p, inside;\n\n // clip against each side of the clip rectangle\n for (edge = 1; edge <= 8; edge *= 2) {\n result = [];\n prev = points[points.length - 1];\n prevInside = !(bitCode(prev, bbox) & edge);\n\n for (i = 0; i < points.length; i++) {\n p = points[i];\n inside = !(bitCode(p, bbox) & edge);\n\n // if segment goes through the clip window, add an intersection\n if (inside !== prevInside) result.push(intersect(prev, p, edge, bbox));\n\n if (inside) result.push(p); // add a point if it's inside\n\n prev = p;\n prevInside = inside;\n }\n\n points = result;\n\n if (!points.length) break;\n }\n\n return result;\n}\n\n// intersect a segment against one of the 4 lines that make up the bbox\n\nfunction intersect(a, b, edge, bbox) {\n return edge & 8 ? [a[0] + (b[0] - a[0]) * (bbox[3] - a[1]) / (b[1] - a[1]), bbox[3]] : // top\n edge & 4 ? [a[0] + (b[0] - a[0]) * (bbox[1] - a[1]) / (b[1] - a[1]), bbox[1]] : // bottom\n edge & 2 ? [bbox[2], a[1] + (b[1] - a[1]) * (bbox[2] - a[0]) / (b[0] - a[0])] : // right\n edge & 1 ? [bbox[0], a[1] + (b[1] - a[1]) * (bbox[0] - a[0]) / (b[0] - a[0])] : // left\n null;\n}\n\n// bit code reflects the point position relative to the bbox:\n\n// left mid right\n// top 1001 1000 1010\n// mid 0001 0000 0010\n// bottom 0101 0100 0110\n\nfunction bitCode(p, bbox) {\n var code = 0;\n\n if (p[0] < bbox[0]) code |= 1; // left\n else if (p[0] > bbox[2]) code |= 2; // right\n\n if (p[1] < bbox[1]) code |= 4; // bottom\n else if (p[1] > bbox[3]) code |= 8; // top\n\n return code;\n}\n", "'use strict';\n\nvar rbush = require('rbush');\nvar lineclip = require('lineclip');\n\nmodule.exports = whichPolygon;\n\nfunction whichPolygon(data) {\n var bboxes = [];\n for (var i = 0; i < data.features.length; i++) {\n var feature = data.features[i];\n\n // unlocated GeoJSON features can have null `geometry`\n if (!feature.geometry) continue;\n\n var coords = feature.geometry.coordinates;\n\n if (feature.geometry.type === 'Polygon') {\n bboxes.push(treeItem(coords, feature.properties));\n\n } else if (feature.geometry.type === 'MultiPolygon') {\n for (var j = 0; j < coords.length; j++) {\n bboxes.push(treeItem(coords[j], feature.properties));\n }\n }\n }\n\n var tree = rbush().load(bboxes);\n\n function query(p, multi) {\n var output = [],\n result = tree.search({\n minX: p[0],\n minY: p[1],\n maxX: p[0],\n maxY: p[1]\n });\n for (var i = 0; i < result.length; i++) {\n if (insidePolygon(result[i].coords, p)) {\n if (multi)\n output.push(result[i].props);\n else\n return result[i].props;\n }\n }\n return multi && output.length ? output : null;\n }\n\n query.tree = tree;\n query.bbox = function queryBBox(bbox) {\n var output = [];\n var result = tree.search({\n minX: bbox[0],\n minY: bbox[1],\n maxX: bbox[2],\n maxY: bbox[3]\n });\n for (var i = 0; i < result.length; i++) {\n if (polygonIntersectsBBox(result[i].coords, bbox)) {\n output.push(result[i].props);\n }\n }\n return output;\n };\n\n return query;\n}\n\nfunction polygonIntersectsBBox(polygon, bbox) {\n var bboxCenter = [\n (bbox[0] + bbox[2]) / 2,\n (bbox[1] + bbox[3]) / 2\n ];\n if (insidePolygon(polygon, bboxCenter)) return true;\n for (var i = 0; i < polygon.length; i++) {\n if (lineclip(polygon[i], bbox).length > 0) return true;\n }\n return false;\n}\n\n// ray casting algorithm for detecting if point is in polygon\nfunction insidePolygon(rings, p) {\n var inside = false;\n for (var i = 0, len = rings.length; i < len; i++) {\n var ring = rings[i];\n for (var j = 0, len2 = ring.length, k = len2 - 1; j < len2; k = j++) {\n if (rayIntersect(p, ring[j], ring[k])) inside = !inside;\n }\n }\n return inside;\n}\n\nfunction rayIntersect(p, p1, p2) {\n return ((p1[1] > p[1]) !== (p2[1] > p[1])) && (p[0] < (p2[0] - p1[0]) * (p[1] - p1[1]) / (p2[1] - p1[1]) + p1[0]);\n}\n\nfunction treeItem(coords, props) {\n var item = {\n minX: Infinity,\n minY: Infinity,\n maxX: -Infinity,\n maxY: -Infinity,\n coords: coords,\n props: props\n };\n\n for (var i = 0; i < coords[0].length; i++) {\n var p = coords[0][i];\n item.minX = Math.min(item.minX, p[0]);\n item.minY = Math.min(item.minY, p[1]);\n item.maxX = Math.max(item.maxX, p[0]);\n item.maxY = Math.max(item.maxY, p[1]);\n }\n return item;\n}\n", "/* eslint @typescript-eslint/no-this-alias: \"warn\" */\nimport whichPolygon from 'which-polygon';\n\nimport { simplify } from './simplify.ts';\n\n// JSON\nimport matchGroupsJSON from '../config/matchGroups.json' with {type: 'json'};\nimport genericWordsJSON from '../config/genericWords.json' with {type: 'json'};\nimport treesJSON from '../config/trees.json' with {type: 'json'};\n\nconst matchGroups = matchGroupsJSON.matchGroups;\nconst trees = treesJSON.trees;\n\n\ntype Vec2 = [number, number];\ntype Vec3 = [number, number, number];\ntype Location = Vec2 | Vec3 | string | number;\n\ninterface LocationSet {\n include?: Array,\n exclude?: Array\n};\n\ninterface LocationConflation {\n validateLocation: (a: Location) => unknown;\n resolveLocation: (a: Location) => unknown;\n validateLocationSet: (a: LocationSet) => unknown;\n resolveLocationSet: (a: LocationSet) => unknown;\n};\n\ntype HitType = 'primary' | 'alternate' | 'excludeGeneric' | 'excludeNamed';\ninterface Hit {\n match: HitType;\n itemID?: string;\n area?: number;\n kv?: string;\n nsimple?: string;\n pattern?: string;\n};\n\n\nexport class Matcher {\n private matchIndex;\n private genericWords = new Map();\n private itemLocation;\n private locationSets;\n private locationIndex;\n private warnings: Array = [];\n\n\n // `constructor`\n // initialize the genericWords regexes\n constructor() {\n // The `matchIndex` is a specialized structure that allows us to quickly answer\n // _\"Given a [key/value tagpair, name, location], what canonical items (brands etc) can match it?\"_\n //\n // The index contains all valid combinations of k/v tagpairs and names\n // matchIndex:\n // {\n // 'k/v': {\n // 'primary': Map (String 'nsimple' -> Set (itemIDs…), // matches for tags like `name`, `name:xx`, etc.\n // 'alternate': Map (String 'nsimple' -> Set (itemIDs…), // matches for tags like `alt_name`, `brand`, etc.\n // 'excludeNamed': Map (String 'pattern' -> RegExp),\n // 'excludeGeneric': Map (String 'pattern' -> RegExp)\n // },\n // }\n //\n // {\n // 'amenity/bank': {\n // 'primary': {\n // 'firstbank': Set (\"firstbank-978cca\", \"firstbank-9794e6\", \"firstbank-f17495\", …),\n // …\n // },\n // 'alternate': {\n // '1stbank': Set (\"firstbank-f17495\"),\n // …\n // }\n // },\n // 'shop/supermarket': {\n // 'primary': {\n // 'coop': Set (\"coop-76454b\", \"coop-ebf2d9\", \"coop-36e991\", …),\n // 'coopfood': Set (\"coopfood-a8278b\", …),\n // …\n // },\n // 'alternate': {\n // 'coop': Set (\"coopfood-a8278b\", …),\n // 'federatedcooperatives': Set (\"coop-76454b\", …),\n // 'thecooperative': Set (\"coopfood-a8278b\", …),\n // …\n // }\n // }\n // }\n //\n this.matchIndex = undefined;\n\n // The `genericWords` structure matches the contents of genericWords.json to instantiated RegExp objects\n // Map (String 'pattern' -> RegExp),\n this.genericWords = new Map();\n (genericWordsJSON.genericWords || []).forEach(s => this.genericWords.set(s, new RegExp(s, 'i')));\n\n // The `itemLocation` structure maps itemIDs to locationSetIDs:\n // {\n // 'firstbank-f17495': '+[first_bank_western_us.geojson]',\n // 'firstbank-978cca': '+[first_bank_carolinas.geojson]',\n // 'coop-76454b': '+[Q16]',\n // 'coopfood-a8278b': '+[Q23666]',\n // …\n // }\n this.itemLocation = undefined;\n\n // The `locationSets` structure maps locationSetIDs to *resolved* locationSets:\n // {\n // '+[first_bank_western_us.geojson]': GeoJSON {…},\n // '+[first_bank_carolinas.geojson]': GeoJSON {…},\n // '+[Q16]': GeoJSON {…},\n // '+[Q23666]': GeoJSON {…},\n // …\n // }\n this.locationSets = undefined;\n\n // The `locationIndex` is an instance of which-polygon spatial index for the locationSets.\n this.locationIndex = undefined;\n\n // Array of match conflict pairs (currently unused)\n this.warnings = [];\n }\n\n\n //\n // `buildMatchIndex()`\n // Call this to prepare the matcher for use\n //\n // `data` needs to be an Object indexed on a 'tree/key/value' path.\n // (e.g. cache filled by `fileTree.read` or data found in `dist/nsi.json`)\n // {\n // 'brands/amenity/bank': { properties: {}, items: [ {}, {}, … ] },\n // 'brands/amenity/bar': { properties: {}, items: [ {}, {}, … ] },\n // …\n // }\n //\n buildMatchIndex(data: Record): void {\n const that = this;\n if (that.matchIndex) return; // it was built already\n that.matchIndex = new Map();\n\n const seenTree = new Map(); // warn if the same [k, v, nsimple] appears in multiple trees - #5625\n\n Object.keys(data).forEach(tkv => {\n const category = data[tkv];\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const t = parts[0];\n const k = parts[1];\n const v = parts[2];\n const thiskv = `${k}/${v}`;\n const tree = trees[t];\n\n let branch = that.matchIndex.get(thiskv);\n if (!branch) {\n branch = {\n primary: new Map(),\n alternate: new Map(),\n excludeGeneric: new Map(),\n excludeNamed: new Map()\n };\n that.matchIndex.set(thiskv, branch);\n }\n\n // ADD EXCLUSIONS\n const properties = category.properties || {};\n const exclude = properties.exclude || {};\n (exclude.generic || []).forEach(s => branch.excludeGeneric.set(s, new RegExp(s, 'i')));\n (exclude.named || []).forEach(s => branch.excludeNamed.set(s, new RegExp(s, 'i')));\n const excludeRegexes = [...branch.excludeGeneric.values(), ...branch.excludeNamed.values()];\n\n\n // ADD ITEMS\n const items = category.items;\n if (!Array.isArray(items) || !items.length) return;\n\n\n // Primary name patterns, match tags to take first\n // e.g. `name`, `name:ru`\n const primaryName = new RegExp(tree.nameTags.primary, 'i');\n\n // Alternate name patterns, match tags to consider after primary\n // e.g. `alt_name`, `short_name`, `brand`, `brand:ru`, etc..\n const alternateName = new RegExp(tree.nameTags.alternate, 'i');\n\n // There are a few exceptions to the name matching regexes.\n // Usually a tag suffix contains a language code like `name:en`, `name:ru`\n // but we want to exclude things like `operator:type`, `name:etymology`, etc..\n const notName = /:(colou?r|type|forward|backward|left|right|etymology|pronunciation|signed|wikipedia)$/i;\n\n // For certain categories we do not want to match generic KV pairs like `building/yes` or `amenity/yes`\n const skipGenericKV = skipGenericKVMatches(t, k, v);\n\n // We will collect the generic KV pairs anyway (for the purpose of filtering them out of matchTags)\n const genericKV = new Set([`${k}/yes`, `building/yes`]);\n\n // Collect alternate tagpairs for this kv category from matchGroups.\n // We might also pick up a few more generic KVs (like `shop/yes`)\n const matchGroupKV = new Set();\n Object.values(matchGroups).forEach(matchGroup => {\n const inGroup = matchGroup.some(otherkv => otherkv === thiskv);\n if (!inGroup) return;\n\n matchGroup.forEach(otherkv => {\n if (otherkv === thiskv) return; // skip self\n matchGroupKV.add(otherkv);\n\n const otherk = otherkv.split('/', 2)[0]; // we might pick up a `shop/yes`\n genericKV.add(`${otherk}/yes`);\n });\n });\n\n // For each item, insert all [key, value, name] combinations into the match index\n items.forEach(item => {\n if (!item.id) return;\n\n // Automatically remove redundant `matchTags` - #3417, #8137\n // (i.e. This kv is already covered by matchGroups, so it doesn't need to be in `item.matchTags`\n // or this kv is the primary kv, so it doesn't need to be duplicated in `item.matchTags`)\n if (Array.isArray(item.matchTags) && item.matchTags.length) {\n item.matchTags = item.matchTags\n .filter(matchTag => !matchGroupKV.has(matchTag) && (matchTag !== thiskv) && !genericKV.has(matchTag));\n\n if (!item.matchTags.length) delete item.matchTags;\n }\n\n // key/value tagpairs to insert into the match index..\n let kvTags = [`${thiskv}`]\n .concat(item.matchTags || []);\n\n if (!skipGenericKV) {\n kvTags = kvTags\n .concat(Array.from(genericKV)); // #3454 - match some generic tags\n }\n\n // Index all the namelike tag values\n Object.keys(item.tags).forEach(osmkey => {\n if (notName.test(osmkey)) return; // osmkey is not a namelike tag, skip\n const osmvalue = item.tags[osmkey];\n if (!osmvalue || excludeRegexes.some(regex => regex.test(osmvalue))) return; // osmvalue missing or excluded\n\n if (primaryName.test(osmkey)) {\n kvTags.forEach(kv => insertName('primary', t, kv, simplify(osmvalue), item.id));\n } else if (alternateName.test(osmkey)) {\n kvTags.forEach(kv => insertName('alternate', t, kv, simplify(osmvalue), item.id));\n }\n });\n\n // Index `matchNames` after indexing all other names..\n const keepMatchNames = new Set();\n (item.matchNames || []).forEach(matchName => {\n // If this matchname isn't already indexed, add it to the alternate index\n const nsimple = simplify(matchName);\n kvTags.forEach(kv => {\n const branch = that.matchIndex.get(kv);\n const primaryLeaf = branch && branch.primary.get(nsimple);\n const alternateLeaf = branch && branch.alternate.get(nsimple);\n const inPrimary = primaryLeaf && primaryLeaf.has(item.id);\n const inAlternate = alternateLeaf && alternateLeaf.has(item.id);\n\n if (!inPrimary && !inAlternate) {\n insertName('alternate', t, kv, nsimple, item.id);\n keepMatchNames.add(matchName);\n }\n });\n });\n\n // Automatically remove redundant `matchNames` - #3417\n // (i.e. This name got indexed some other way, so it doesn't need to be in `item.matchNames`)\n if (keepMatchNames.size) {\n item.matchNames = Array.from(keepMatchNames);\n } else {\n delete item.matchNames;\n }\n\n }); // each item\n }); // each tkv\n\n\n // Insert this item into the matchIndex\n function insertName(which: string, t: string, kv: string, nsimple: string, itemID: string) {\n if (!nsimple) {\n that.warnings.push(`Warning: skipping empty ${which} name for item ${t}/${kv}: ${itemID}`);\n return;\n }\n\n let branch = that.matchIndex.get(kv);\n if (!branch) {\n branch = {\n primary: new Map(),\n alternate: new Map(),\n excludeGeneric: new Map(),\n excludeNamed: new Map()\n };\n that.matchIndex.set(kv, branch);\n }\n\n let leaf = branch[which].get(nsimple);\n if (!leaf) {\n leaf = new Set();\n branch[which].set(nsimple, leaf);\n }\n\n leaf.add(itemID); // insert\n\n // check for duplicates - #5625\n if (!/yes$/.test(kv)) { // ignore genericKV like amenity/yes, building/yes, etc\n const kvnsimple = `${kv}/${nsimple}`;\n const existing = seenTree.get(kvnsimple);\n if (existing && existing !== t) {\n const items = Array.from(leaf);\n that.warnings.push(`Duplicate cache key \"${kvnsimple}\" in trees \"${t}\" and \"${existing}\", check items: ${items}`);\n return;\n }\n seenTree.set(kvnsimple, t);\n }\n }\n\n // For certain categories we do not want to match generic KV pairs like `building/yes` or `amenity/yes`\n function skipGenericKVMatches(t: string, k: string, v: string): boolean {\n return (\n t === 'flags' ||\n t === 'transit' ||\n k === 'landuse' ||\n v === 'atm' ||\n v === 'bicycle_parking' ||\n v === 'car_sharing' ||\n v === 'caravan_site' ||\n v === 'charging_station' ||\n v === 'dog_park' ||\n v === 'parking' ||\n v === 'phone' ||\n v === 'playground' ||\n v === 'post_box' ||\n v === 'public_bookcase' ||\n v === 'recycling' ||\n v === 'vending_machine'\n );\n }\n }\n\n\n //\n // `buildLocationIndex()`\n // Call this to prepare a which-polygon location index.\n // This *resolves* all the locationSets into GeoJSON, which takes some time.\n // You can skip this step if you don't care about matching within a location.\n //\n // `data` needs to be an Object indexed on a 'tree/key/value' path.\n // (e.g. cache filled by `fileTree.read` or data found in `dist/nsi.json`)\n // {\n // 'brands/amenity/bank': { properties: {}, items: [ {}, {}, … ] },\n // 'brands/amenity/bar': { properties: {}, items: [ {}, {}, … ] },\n // …\n // }\n //\n buildLocationIndex(data: Record, loco: LocationConflation): void {\n const that = this;\n if (that.locationIndex) return; // it was built already\n\n that.itemLocation = new Map();\n that.locationSets = new Map();\n\n Object.keys(data).forEach(tkv => {\n const items = data[tkv].items;\n if (!Array.isArray(items) || !items.length) return;\n\n items.forEach(item => {\n if (that.itemLocation.has(item.id)) return; // we've seen item id already - shouldn't be possible?\n\n let resolved;\n try {\n resolved = loco.resolveLocationSet(item.locationSet); // resolve a feature for this locationSet\n } catch (err: unknown) {\n const message = (err instanceof Error) ? err.message : err;\n console.warn(`buildLocationIndex: ${message}`); // couldn't resolve\n }\n if (!resolved || !resolved.id) return;\n\n that.itemLocation.set(item.id, resolved.id); // link it to the item\n if (that.locationSets.has(resolved.id)) return; // we've seen this locationSet feature before..\n\n // First time seeing this locationSet feature, make a copy and add to locationSet cache..\n const feature = _cloneDeep(resolved.feature);\n feature.id = resolved.id; // Important: always use the locationSet `id` (`+[Q30]`), not the feature `id` (`Q30`)\n feature.properties.id = resolved.id;\n\n if (!feature.geometry.coordinates.length || !feature.properties.area) {\n console.warn(`buildLocationIndex: locationSet ${resolved.id} for ${item.id} resolves to an empty feature:`);\n console.warn(JSON.stringify(feature));\n return;\n }\n\n that.locationSets.set(resolved.id, feature);\n });\n });\n\n that.locationIndex = whichPolygon({ type: 'FeatureCollection', features: [...that.locationSets.values()] });\n\n function _cloneDeep(obj) {\n return JSON.parse(JSON.stringify(obj));\n }\n }\n\n\n //\n // `match()`\n // Pass parts and return an Array of matches.\n // `k` - key\n // `v` - value\n // `n` - namelike\n // `loc` - optional - [lon,lat] location to search\n //\n // 1. If the [k,v,n] tuple matches a canonical item…\n // Return an Array of match results.\n // Each result will include the area in km² that the item is valid.\n //\n // Order of results:\n // Primary ordering will be on the \"match\" column:\n // \"primary\" - where the query matches the `name` tag, followed by\n // \"alternate\" - where the query matches an alternate name tag (e.g. short_name, brand, operator, etc)\n // Secondary ordering will be on the \"area\" column:\n // \"area descending\" if no location was provided, (worldwide before local)\n // \"area ascending\" if location was provided (local before worldwide)\n //\n // [\n // { match: 'primary', itemID: String, area: Number, kv: String, nsimple: String },\n // { match: 'primary', itemID: String, area: Number, kv: String, nsimple: String },\n // { match: 'alternate', itemID: String, area: Number, kv: String, nsimple: String },\n // { match: 'alternate', itemID: String, area: Number, kv: String, nsimple: String },\n // …\n // ]\n //\n // -or-\n //\n // 2. If the [k,v,n] tuple matches an exclude pattern…\n // Return an Array with a single exclude result, either\n //\n // [ { match: 'excludeGeneric', pattern: String, kv: String } ] // \"generic\" e.g. \"Food Court\"\n // or\n // [ { match: 'excludeNamed', pattern: String, kv: String } ] // \"named\", e.g. \"Kebabai\"\n //\n // About results\n // \"generic\" - a generic word that is probably not really a name.\n // For these, iD should warn the user \"Hey don't put 'food court' in the name tag\".\n // \"named\" - a real name like \"Kebabai\" that is just common, but not a brand.\n // For these, iD should just let it be. We don't include these in NSI, but we don't want to nag users about it either.\n //\n // -or-\n //\n // 3. If the [k,v,n] tuple matches nothing of any kind, return `null`\n //\n //\n match(k: string, v: string, n: string, loc?: Vec2): Array | null {\n const that = this;\n if (!that.matchIndex) {\n throw new Error('match: matchIndex not built.');\n }\n\n // If we were supplied a location, and a that.locationIndex has been set up,\n // get the locationSets that are valid there so we can filter results.\n let matchLocations;\n if (Array.isArray(loc) && that.locationIndex) {\n // which-polygon query returns an array of GeoJSON properties, pass true to return all results\n matchLocations = that.locationIndex([loc[0], loc[1], loc[0], loc[1]], true);\n }\n\n const nsimple = simplify(n);\n\n const seen = new Set();\n const results: Array = [];\n gatherResults('primary');\n gatherResults('alternate');\n if (results.length) return results;\n\n gatherResults('exclude');\n return results.length ? results : null;\n\n\n function gatherResults(which: string): void {\n // First try an exact match on k/v\n const kv = `${k}/${v}`;\n let didMatch = tryMatch(which, kv);\n if (didMatch) return;\n\n // If that didn't work, look in match groups for other pairs considered equivalent to k/v..\n for (const mg in matchGroups) {\n const matchGroup = matchGroups[mg];\n const inGroup = matchGroup.some(otherkv => otherkv === kv);\n if (!inGroup) continue;\n\n for (const otherkv of matchGroup) {\n if (otherkv === kv) continue; // skip self\n didMatch = tryMatch(which, otherkv);\n if (didMatch) return;\n }\n }\n\n // If finished 'exclude' pass and still haven't matched anything, try the global `genericWords.json` patterns\n if (which === 'exclude') {\n const regex = [...that.genericWords.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeGeneric', pattern: String(regex) }); // note no `branch`, no `kv`\n return;\n }\n }\n }\n\n function tryMatch(which: string, kv: string): boolean {\n const branch = that.matchIndex.get(kv);\n if (!branch) return false;\n\n if (which === 'exclude') { // Test name `n` against named and generic exclude patterns\n let regex = [...branch.excludeNamed.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeNamed', pattern: String(regex), kv: kv });\n return false;\n }\n regex = [...branch.excludeGeneric.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeGeneric', pattern: String(regex), kv: kv });\n return false;\n }\n return false;\n }\n\n const leaf = branch[which].get(nsimple);\n if (!leaf || !leaf.size) return false;\n if (!(which === 'primary' || which === 'alternate')) return false;\n\n // If we get here, we matched something..\n // Prepare the results, calculate areas (if location index was set up)\n let hits: Array = [];\n for (const itemID of [...leaf]) {\n let area = Infinity;\n if (that.itemLocation && that.locationSets) {\n const location = that.locationSets.get(that.itemLocation.get(itemID));\n area = (location && location.properties.area) || Infinity;\n }\n hits.push({ match: which, itemID: itemID, area: area, kv: kv, nsimple: nsimple });\n }\n\n let sortFn = byAreaDescending;\n\n // Filter the match to include only results valid in the requested `loc`..\n if (matchLocations) {\n hits = hits.filter(isValidLocation);\n sortFn = byAreaAscending;\n }\n\n if (!hits.length) return false;\n\n // push results\n hits.sort(sortFn).forEach(hit => {\n if (seen.has(hit.itemID)) return;\n seen.add(hit.itemID);\n results.push(hit);\n });\n\n return true;\n\n\n function isValidLocation(hit: Hit): boolean {\n if (!that.itemLocation) return true;\n return matchLocations.find(props => props.id === that.itemLocation.get(hit.itemID));\n }\n // Sort smaller (more local) locations first.\n function byAreaAscending(hitA: Hit, hitB: Hit): number {\n const areaA = hitA.area || 0;\n const areaB = hitB.area || 0;\n return areaA - areaB;\n }\n // Sort larger (more worldwide) locations first.\n function byAreaDescending(hitA: Hit, hitB: Hit): number {\n const areaA = hitA.area || 0;\n const areaB = hitB.area || 0;\n return areaB - areaA;\n }\n }\n }\n\n\n //\n // `getWarnings()`\n // Return any warnings discovered when buiding the index.\n // (currently this does nothing)\n //\n getWarnings(): Array {\n return this.warnings;\n }\n}\n", "// External\nimport diacritics from 'diacritics';\n\n// remove spaces, punctuation, diacritics\n// for punction see https://stackoverflow.com/a/21224179\nexport function simplify(str?: string): string {\n if (typeof str !== 'string') return '';\n\n return diacritics.remove(\n str\n .replace(/&/g, 'and')\n .replace(/(İ|i̇)/ig, 'i') // for BİM, İşbank - #5017, #8261\n .replace(/[\\s\\-=_!\"#%'*{},.\\/:;?\\(\\)\\[\\]@\\\\$\\^*+<>«»~`’\\u00a1\\u00a7\\u00b6\\u00b7\\u00bf\\u037e\\u0387\\u055a-\\u055f\\u0589\\u05c0\\u05c3\\u05c6\\u05f3\\u05f4\\u0609\\u060a\\u060c\\u060d\\u061b\\u061e\\u061f\\u066a-\\u066d\\u06d4\\u0700-\\u070d\\u07f7-\\u07f9\\u0830-\\u083e\\u085e\\u0964\\u0965\\u0970\\u0af0\\u0df4\\u0e4f\\u0e5a\\u0e5b\\u0f04-\\u0f12\\u0f14\\u0f85\\u0fd0-\\u0fd4\\u0fd9\\u0fda\\u104a-\\u104f\\u10fb\\u1360-\\u1368\\u166d\\u166e\\u16eb-\\u16ed\\u1735\\u1736\\u17d4-\\u17d6\\u17d8-\\u17da\\u1800-\\u1805\\u1807-\\u180a\\u1944\\u1945\\u1a1e\\u1a1f\\u1aa0-\\u1aa6\\u1aa8-\\u1aad\\u1b5a-\\u1b60\\u1bfc-\\u1bff\\u1c3b-\\u1c3f\\u1c7e\\u1c7f\\u1cc0-\\u1cc7\\u1cd3\\u2000-\\u206f\\u2cf9-\\u2cfc\\u2cfe\\u2cff\\u2d70\\u2e00-\\u2e7f\\u3001-\\u3003\\u303d\\u30fb\\ua4fe\\ua4ff\\ua60d-\\ua60f\\ua673\\ua67e\\ua6f2-\\ua6f7\\ua874-\\ua877\\ua8ce\\ua8cf\\ua8f8-\\ua8fa\\ua92e\\ua92f\\ua95f\\ua9c1-\\ua9cd\\ua9de\\ua9df\\uaa5c-\\uaa5f\\uaade\\uaadf\\uaaf0\\uaaf1\\uabeb\\ufe10-\\ufe16\\ufe19\\ufe30\\ufe45\\ufe46\\ufe49-\\ufe4c\\ufe50-\\ufe52\\ufe54-\\ufe57\\ufe5f-\\ufe61\\ufe68\\ufe6a\\ufe6b\\ufeff\\uff01-\\uff03\\uff05-\\uff07\\uff0a\\uff0c\\uff0e\\uff0f\\uff1a\\uff1b\\uff1f\\uff20\\uff3c\\uff61\\uff64\\uff65]+/g,'')\n .toLowerCase()\n );\n}\n", "// Internal\nimport { simplify } from './simplify.ts';\n\n// Removes noise from the name so that we can compare\n// similar names for catching duplicates.\nexport function stemmer(str?: string): string {\n if (typeof str !== 'string') return '';\n\n const noise = [\n /ban(k|c)(a|o)?/ig,\n /банк/ig,\n /coop/ig,\n /express/ig,\n /(gas|fuel)/ig,\n /wireless/ig,\n /(shop|store)/ig\n ];\n\n str = noise.reduce((acc, regex) => acc.replace(regex, ''), str);\n return simplify(str);\n}\n", "import { Matcher } from 'name-suggestion-index';\nimport { fileFetcher, locationManager } from '../core';\nimport { presetManager } from '../presets';\n\nimport { nsiCdnUrl } from '../../config/id.js';\n\n// Make very sure this resolves to iD's `package.json`\n// If you mess up the `../`s, the resolver may import another random package.json from somewhere else.\nimport packageJSON from '../../package.json';\n\n\n// This service contains all the code related to the **name-suggestion-index** (aka NSI)\n// NSI contains the most correct tagging for many commonly mapped features.\n// See https://github.com/osmlab/name-suggestion-index and https://nsi.guide\n\n\n// DATA\n\nlet _nsiStatus = 'loading'; // 'loading', 'ok', 'failed'\nlet _nsi = {};\n\n// Sometimes we can upgrade a feature tagged like `building=yes` to a better tag.\nconst buildingPreset = {\n 'building/commercial': true,\n 'building/government': true,\n 'building/hotel': true,\n 'building/retail': true,\n 'building/office': true,\n 'building/supermarket': true,\n 'building/yes': true\n};\n\n// Exceptions to the namelike regexes.\n// Usually a tag suffix contains a language code like `name:en`, `name:ru`\n// but we want to exclude things like `operator:type`, `name:etymology`, etc..\nconst notNames = /:(colou?r|type|forward|backward|left|right|etymology|pronunciation|wikipedia)$/i;\n\n// Exceptions to the branchlike regexes\nconst notBranches = /(coop|express|wireless|factory|outlet)/i;\n\n\n// PRIVATE FUNCTIONS\n\n// `setNsiSources()`\n// Adds the sources to iD's filemap so we can start downloading data.\n//\nfunction setNsiSources() {\n const nsiVersion = packageJSON.dependencies['name-suggestion-index'] || packageJSON.devDependencies['name-suggestion-index'];\n const cdn = nsiCdnUrl.replace('{version}', nsiVersion);\n const sources = {\n 'nsi_data': cdn + 'dist/json/nsi.min.json',\n 'nsi_dissolved': cdn + 'dist/wikidata/dissolved.min.json',\n 'nsi_features': cdn + 'dist/json/featureCollection.min.json',\n 'nsi_generics': cdn + 'dist/json/genericWords.min.json',\n 'nsi_presets': cdn + 'dist/presets/nsi-id-presets.min.json',\n 'nsi_replacements': cdn + 'dist/json/replacements.min.json',\n 'nsi_trees': cdn + 'dist/json/trees.min.json'\n };\n\n let fileMap = fileFetcher.fileMap();\n for (const k in sources) {\n if (!fileMap[k]) fileMap[k] = sources[k];\n }\n}\n\n\n// `loadNsiPresets()`\n// Returns a Promise fulfilled when the presets have been downloaded and merged into iD.\n//\nfunction loadNsiPresets() {\n return (\n Promise.all([\n fileFetcher.get('nsi_presets'),\n fileFetcher.get('nsi_features')\n ])\n .then(vals => {\n // Add `suggestion=true` to all the nsi presets\n // The preset json schema doesn't include it, but the iD code still uses it\n Object.values(vals[0].presets).forEach(preset => preset.suggestion = true);\n\n // nsi does not specify *:wikipedia (anymore):\n // clean up previous values to prevent that the wikidata/wikipedia information\n // is going to be out of sync, see #9103\n Object.values(vals[0].presets).forEach(preset => {\n if (preset.tags['brand:wikidata']) {\n preset.removeTags = {'brand:wikipedia': '*', ...(preset.removeTags || preset.addTags || preset.tags)};\n }\n if (preset.tags['operator:wikidata']) {\n preset.removeTags = {'operator:wikipedia': '*', ...(preset.removeTags || preset.addTags || preset.tags)};\n }\n if (preset.tags['network:wikidata']) {\n preset.removeTags = {'network:wikipedia': '*', ...(preset.removeTags || preset.addTags || preset.tags)};\n }\n });\n\n presetManager.merge({\n presets: vals[0].presets,\n featureCollection: vals[1]\n });\n })\n );\n}\n\n\n// `loadNsiData()`\n// Returns a Promise fulfilled when the other data have been downloaded and processed\n//\nfunction loadNsiData() {\n return (\n Promise.all([\n fileFetcher.get('nsi_data'),\n fileFetcher.get('nsi_dissolved'),\n fileFetcher.get('nsi_replacements'),\n fileFetcher.get('nsi_trees')\n ])\n .then(vals => {\n _nsi = {\n data: vals[0].nsi, // the raw name-suggestion-index data\n dissolved: vals[1].dissolved, // list of dissolved items\n replacements: vals[2].replacements, // trivial old->new qid replacements\n trees: vals[3].trees, // metadata about trees, main tags\n kvt: new Map(), // Map (k -> Map (v -> t) )\n qids: new Map(), // Map (wd/wp tag values -> qids)\n ids: new Map() // Map (id -> NSI item)\n };\n\n const matcher = new Matcher();\n _nsi.matcher = matcher;\n matcher.buildMatchIndex(_nsi.data);\n\n// *** BEGIN HACK ***\n\n// old - built in matcher will set up the locationindex by resolving all the locationSets one-by-one\n // matcher.buildLocationIndex(_nsi.data, locationManager.loco());\n\n// new - Use the location manager instead of redoing that work\n// It has already processed the presets at this point\n\n// We need to monkeypatch a few of the collections that the NSI matcher depends on.\n// The `itemLocation` structure maps itemIDs to locationSetIDs\nmatcher.itemLocation = new Map();\n\n// The `locationSets` structure maps locationSetIDs to GeoJSON\n// We definitely need this, but don't need full geojson, just { properties: { area: xxx }}\nmatcher.locationSets = new Map();\n\nObject.keys(_nsi.data).forEach(tkv => {\n const items = _nsi.data[tkv].items;\n if (!Array.isArray(items) || !items.length) return;\n\n items.forEach(item => {\n if (matcher.itemLocation.has(item.id)) return; // we've seen item id already - shouldn't be possible?\n\n const locationSetID = locationManager.locationSetID(item.locationSet);\n matcher.itemLocation.set(item.id, locationSetID);\n\n if (matcher.locationSets.has(locationSetID)) return; // we've seen this locationSet before..\n\n const fakeFeature = { id: locationSetID, properties: { id: locationSetID, area: 1 } };\n matcher.locationSets.set(locationSetID, fakeFeature);\n });\n});\n\n// The `locationIndex` is an instance of which-polygon spatial index for the locationSets.\n// We only really need this to _look like_ which-polygon query `_wp.locationIndex(bbox, true);`\n// i.e. it needs to return the properties of the locationsets\nmatcher.locationIndex = (bbox) => {\n const validHere = locationManager.locationSetsAt([bbox[0], bbox[1]]);\n const results = [];\n\n for (const [locationSetID, area] of Object.entries(validHere)) {\n const fakeFeature = matcher.locationSets.get(locationSetID);\n if (fakeFeature) {\n fakeFeature.properties.area = area;\n results.push(fakeFeature);\n }\n }\n return results;\n};\n\n// *** END HACK ***\n\n\n Object.keys(_nsi.data).forEach(tkv => {\n const category = _nsi.data[tkv];\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const t = parts[0];\n const k = parts[1];\n const v = parts[2];\n\n // Build a reverse index of keys -> values -> trees present in the name-suggestion-index\n // Collect primary keys (e.g. \"amenity\", \"craft\", \"shop\", \"man_made\", \"route\", etc)\n // \"amenity\": {\n // \"restaurant\": \"brands\"\n // }\n let vmap = _nsi.kvt.get(k);\n if (!vmap) {\n vmap = new Map();\n _nsi.kvt.set(k, vmap);\n }\n vmap.set(v, t);\n\n const tree = _nsi.trees[t]; // e.g. \"brands\", \"operators\"\n const mainTag = tree.mainTag; // e.g. \"brand:wikidata\", \"operator:wikidata\", etc\n\n const items = category.items || [];\n items.forEach(item => {\n // Remember some useful things for later, cache NSI id -> item\n item.tkv = tkv;\n item.mainTag = mainTag;\n _nsi.ids.set(item.id, item);\n\n // Cache Wikidata/Wikipedia values -> qid, for #6416\n const wd = item.tags[mainTag];\n const wp = item.tags[mainTag.replace('wikidata', 'wikipedia')];\n if (wd) _nsi.qids.set(wd, wd);\n if (wp && wd) _nsi.qids.set(wp, wd);\n });\n });\n })\n );\n}\n\n\n// `gatherKVs()`\n// Gather all the k/v pairs that we will run through the NSI matcher.\n// An OSM tags object can contain anything, but only a few tags will be interesting to NSI.\n//\n// This function will return the interesting tag pairs like:\n// \"amenity/restaurant\", \"man_made/flagpole\"\n// and fallbacks like\n// \"amenity/yes\"\n// excluding things like\n// \"tiger:reviewed\", \"surface\", \"ref\", etc.\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `Object` containing kv pairs to test:\n// {\n// 'primary': Set(),\n// 'alternate': Set()\n// }\n//\nfunction gatherKVs(tags) {\n let primary = new Set();\n let alternate = new Set();\n\n Object.keys(tags).forEach(osmkey => {\n const osmvalue = tags[osmkey];\n if (!osmvalue) return;\n\n // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184\n if (osmkey === 'route_master') osmkey = 'route';\n\n const vmap = _nsi.kvt.get(osmkey);\n if (!vmap) return; // not an interesting key\n\n if (vmap.get(osmvalue)) { // Matched a category in NSI\n primary.add(`${osmkey}/${osmvalue}`); // interesting key/value\n } else if (osmvalue === 'yes') {\n alternate.add(`${osmkey}/${osmvalue}`); // fallback key/yes\n }\n });\n\n // Can we try a generic building fallback match? - See #6122, #7197\n // Only try this if we do a preset match and find nothing else remarkable about that building.\n // For example, a way with `building=yes` + `name=Westfield` may be a Westfield department store.\n // But a way with `building=yes` + `name=Westfield` + `public_transport=station` is a train station for a town named \"Westfield\"\n const preset = presetManager.matchTags(tags, 'area');\n if (buildingPreset[preset.id]) {\n alternate.add('building/yes');\n }\n\n return { primary: primary, alternate: alternate };\n}\n\n\n// `identifyTree()`\n// NSI has a concept of trees: \"brands\", \"operators\", \"flags\", \"transit\".\n// The tree determines things like which tags are namelike, and which tags hold important wikidata.\n// This takes an Object of tags and tries to identify what tree to use.\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `string` the name of the tree if known\n// or 'unknown' if it could match several trees (e.g. amenity/yes)\n// or null if no match\n//\nfunction identifyTree(tags) {\n let unknown;\n let t;\n\n // Check all tags\n Object.keys(tags).forEach(osmkey => {\n if (t) return; // found already\n\n const osmvalue = tags[osmkey];\n if (!osmvalue) return;\n\n // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184\n if (osmkey === 'route_master') osmkey = 'route';\n\n const vmap = _nsi.kvt.get(osmkey);\n if (!vmap) return; // this key is not in nsi\n\n if (osmvalue === 'yes') {\n unknown = 'unknown';\n } else {\n t = vmap.get(osmvalue);\n }\n });\n\n return t || unknown || null;\n}\n\n\n// `gatherNames()`\n// Gather all the namelike values that we will run through the NSI matcher.\n// It will gather values primarily from tags `name`, `name:ru`, `flag:name`\n// and fallback to alternate tags like `brand`, `brand:ru`, `alt_name`\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `Object` containing namelike values to test:\n// {\n// 'primary': Set(),\n// 'fallbacks': Set()\n// }\n//\nfunction gatherNames(tags) {\n const empty = { primary: new Set(), alternate: new Set() };\n let primary = new Set();\n let alternate = new Set();\n let foundSemi = false;\n let testNameFragments = false;\n let patterns;\n\n // Patterns for matching OSM keys that might contain namelike values.\n // These roughly correspond to the \"trees\" concept in name-suggestion-index,\n let t = identifyTree(tags);\n if (!t) return empty;\n\n if (t === 'transit') {\n patterns = {\n primary: /^network$/i,\n alternate: /^(operator|operator:\\w+|network:\\w+|\\w+_name|\\w+_name:\\w+)$/i\n };\n } else if (t === 'flags') {\n patterns = {\n primary: /^(flag:name|flag:name:\\w+)$/i,\n alternate: /^(flag|flag:\\w+|subject|subject:\\w+)$/i // note: no `country`, we special-case it below\n };\n } else if (t === 'brands') {\n testNameFragments = true;\n patterns = {\n primary: /^(name|name:\\w+)$/i,\n alternate: /^(brand|brand:\\w+|operator|operator:\\w+|\\w+_name|\\w+_name:\\w+)/i,\n };\n } else if (t === 'operators') {\n testNameFragments = true;\n patterns = {\n primary: /^(name|name:\\w+|operator|operator:\\w+)$/i,\n alternate: /^(brand|brand:\\w+|\\w+_name|\\w+_name:\\w+)/i,\n };\n } else { // unknown/multiple\n testNameFragments = true;\n patterns = {\n primary: /^(name|name:\\w+)$/i,\n alternate: /^(brand|brand:\\w+|network|network:\\w+|operator|operator:\\w+|\\w+_name|\\w+_name:\\w+)/i,\n };\n }\n\n // Test `name` fragments, longest to shortest, to fit them into a \"Name Branch\" pattern.\n // e.g. \"TUI ReiseCenter - Neuss Innenstadt\" -> [\"TUI\", \"ReiseCenter\", \"Neuss\", \"Innenstadt\"]\n if (tags.name && testNameFragments) {\n const nameParts = tags.name.split(/[\\s\\-\\/,.]/);\n for (let split = nameParts.length; split > 0; split--) {\n const name = nameParts.slice(0, split).join(' '); // e.g. \"TUI ReiseCenter\"\n primary.add(name);\n }\n }\n\n // Check all tags\n Object.keys(tags).forEach(osmkey => {\n const osmvalue = tags[osmkey];\n if (!osmvalue) return;\n\n if (isNamelike(osmkey, 'primary')) {\n if (/;/.test(osmvalue)) {\n foundSemi = true;\n } else {\n primary.add(osmvalue);\n alternate.delete(osmvalue);\n }\n } else if (!primary.has(osmvalue) && isNamelike(osmkey, 'alternate')) {\n if (/;/.test(osmvalue)) {\n foundSemi = true;\n } else {\n alternate.add(osmvalue);\n }\n }\n });\n\n // For flags only, fallback to `country` tag only if no other namelike values were found.\n // See https://github.com/openstreetmap/iD/pull/8305#issuecomment-769174070\n if (tags.man_made === 'flagpole' && !primary.size && !alternate.size && !!tags.country) {\n const osmvalue = tags.country;\n if (/;/.test(osmvalue)) {\n foundSemi = true;\n } else {\n alternate.add(osmvalue);\n }\n }\n\n // If any namelike value contained a semicolon, return empty set and don't try matching anything.\n if (foundSemi) {\n return empty;\n } else {\n return { primary: primary, alternate: alternate };\n }\n\n function isNamelike(osmkey, which) {\n if (osmkey === 'old_name') return false;\n return patterns[which].test(osmkey) && !notNames.test(osmkey);\n }\n}\n\n\n// `gatherTuples()`\n// Generate all combinations of [key,value,name] that we want to test.\n// This prioritizes them so that the primary name and k/v pairs go first\n//\n// Arguments\n// `tryKVs`: `Object` containing primary and alternate k/v pairs to test\n// `tryNames`: `Object` containing primary and alternate names to test\n// Returns\n// `Array`: tuple objects ordered by priority\n//\nfunction gatherTuples(tryKVs, tryNames) {\n let tuples = [];\n ['primary', 'alternate'].forEach(whichName => {\n // test names longest to shortest\n const arr = Array.from(tryNames[whichName]).sort((a, b) => b.length - a.length);\n arr.forEach(n => {\n ['primary', 'alternate'].forEach(whichKV => {\n tryKVs[whichKV].forEach(kv => {\n const parts = kv.split('/', 2);\n const k = parts[0];\n const v = parts[1];\n tuples.push({ k: k, v: v, n: n });\n });\n });\n });\n });\n return tuples;\n}\n\n\n// `_upgradeTags()`\n// Try to match a feature to a canonical record in name-suggestion-index\n// and upgrade the tags to match.\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// `loc`: Location where this feature exists, as a [lon, lat]\n// Returns\n// `Object` containing the result, or `null` if no changes needed:\n// {\n// 'newTags': `Object` - The tags the the feature should have\n// 'matched': `Object` - The matched item\n// }\n//\nfunction _upgradeTags(tags, loc) {\n let newTags = Object.assign({}, tags); // shallow copy\n let changed = false;\n\n // Before anything, perform trivial Wikipedia/Wikidata replacements\n Object.keys(newTags).forEach(osmkey => {\n const matchTag = osmkey.match(/^(\\w+:)?wikidata$/);\n if (matchTag) { // Look at '*:wikidata' tags\n const prefix = (matchTag[1] || '');\n const wd = newTags[osmkey];\n const replace = _nsi.replacements[wd]; // If it matches a QID in the replacement list...\n\n if (replace && replace.wikidata !== undefined) { // replace or delete `*:wikidata` tag\n changed = true;\n if (replace.wikidata) {\n newTags[osmkey] = replace.wikidata;\n } else {\n delete newTags[osmkey];\n }\n }\n if (replace && replace.wikipedia !== undefined) { // replace or delete `*:wikipedia` tag\n changed = true;\n const wpkey = `${prefix}wikipedia`;\n if (replace.wikipedia) {\n newTags[wpkey] = replace.wikipedia;\n } else {\n delete newTags[wpkey];\n }\n }\n }\n });\n\n // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184\n const isRouteMaster = (tags.type === 'route_master');\n\n // Gather key/value tag pairs to try to match\n const tryKVs = gatherKVs(tags);\n if (!tryKVs.primary.size && !tryKVs.alternate.size) {\n return changed ? { newTags: newTags, matched: null } : null;\n }\n\n // Gather namelike tag values to try to match\n const tryNames = gatherNames(tags);\n\n // Do `wikidata=*` or `wikipedia=*` tags identify this entity as a chain? - See #6416\n // If so, these tags can be swapped to e.g. `brand:wikidata`/`brand:wikipedia`.\n const foundQID = _nsi.qids.get(tags.wikidata) || _nsi.qids.get(tags.wikipedia);\n if (foundQID) tryNames.primary.add(foundQID); // matcher will recognize the Wikidata QID as name too\n\n if (!tryNames.primary.size && !tryNames.alternate.size) {\n return changed ? { newTags: newTags, matched: null } : null;\n }\n\n // Order the [key,value,name] tuples - test primary before alternate\n const tuples = gatherTuples(tryKVs, tryNames);\n\n for (let i = 0; i < tuples.length; i++) {\n const tuple = tuples[i];\n const hits = _nsi.matcher.match(tuple.k, tuple.v, tuple.n, loc); // Attempt to match an item in NSI\n\n if (!hits || !hits.length) continue; // no match, try next tuple\n if (hits[0].match !== 'primary' && hits[0].match !== 'alternate') break; // a generic match, stop looking\n\n // A match may contain multiple results, the first one is likely the best one for this location\n // e.g. `['pfk-a54c14', 'kfc-1ff19c', 'kfc-658eea']`\n let itemID, item;\n for (let j = 0; j < hits.length; j++) {\n const hit = hits[j];\n itemID = hit.itemID;\n if (_nsi.dissolved[itemID]) continue; // Don't upgrade to a dissolved item\n\n item = _nsi.ids.get(itemID);\n if (!item) continue;\n const mainTag = item.mainTag; // e.g. `brand:wikidata`\n const itemQID = item.tags[mainTag]; // e.g. `brand:wikidata` qid\n const notQID = newTags[`not:${mainTag}`]; // e.g. `not:brand:wikidata` qid\n\n if ( // Exceptions, skip this hit\n (!itemQID || itemQID === notQID) || // No `*:wikidata` or matched a `not:*:wikidata`\n (newTags.office && !item.tags.office) // feature may be a corporate office for a brand? - #6416\n ) {\n item = null;\n continue; // continue looking\n } else {\n break; // use `item`\n }\n }\n\n // Can't use any of these hits, try next tuple..\n if (!item) continue;\n\n // At this point we have matched a canonical item and can suggest tag upgrades..\n item = JSON.parse(JSON.stringify(item)); // deep copy\n const tkv = item.tkv;\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const k = parts[1];\n const v = parts[2];\n const category = _nsi.data[tkv];\n const properties = category.properties || {};\n\n // Preserve some tags that we specifically don't want NSI to overwrite. ('^name', sometimes)\n let preserveTags = item.preserveTags || properties.preserveTags || [];\n\n // These tags can be toplevel tags -or- attributes - so we generally want to preserve existing values - #8615\n // We'll only _replace_ the tag value if this tag is the toplevel/defining tag for the matched item (`k`)\n ['building', 'emergency', 'internet_access', 'opening_hours', 'takeaway'].forEach(osmkey => {\n if (k !== osmkey) preserveTags.push(`^${osmkey}$`);\n });\n\n const regexes = preserveTags.map(s => new RegExp(s, 'i'));\n\n let keepTags = {};\n Object.keys(newTags).forEach(osmkey => {\n if (regexes.some(regex => regex.test(osmkey))) {\n keepTags[osmkey] = newTags[osmkey];\n }\n });\n\n // Remove any primary tags (\"amenity\", \"craft\", \"shop\", \"man_made\", \"route\", etc) that have a\n // value like `amenity=yes` or `shop=yes` (exceptions have already been added to `keepTags` above)\n _nsi.kvt.forEach((vmap, k) => {\n if (newTags[k] === 'yes') delete newTags[k];\n });\n\n // Replace mistagged `wikidata`/`wikipedia` with e.g. `brand:wikidata`/`brand:wikipedia`\n if (foundQID) {\n delete newTags.wikipedia;\n delete newTags.wikidata;\n }\n\n // Do the tag upgrade\n Object.assign(newTags, item.tags, keepTags);\n\n // Swap `route` back to `route_master` - name-suggestion-index#5184\n if (isRouteMaster) {\n newTags.route_master = newTags.route;\n delete newTags.route;\n }\n\n // Special `branch` splitting rules - IF..\n // - NSI is suggesting to replace `name`, AND\n // - `branch` doesn't already contain something, AND\n // - original name has not moved to an alternate name (e.g. \"Dunkin' Donuts\" -> \"Dunkin'\"), AND\n // - original name is \"some name\" + \"some stuff\", THEN\n // consider splitting `name` into `name`/`branch`..\n const origName = tags.name;\n const newName = newTags.name;\n if (newName && origName && newName !== origName && !newTags.branch) {\n const newNames = gatherNames(newTags);\n const newSet = new Set([...newNames.primary, ...newNames.alternate]);\n const isMoved = newSet.has(origName); // another tag holds the original name now\n\n if (!isMoved) {\n // Test name fragments, longest to shortest, to fit them into a \"Name Branch\" pattern.\n // e.g. \"TUI ReiseCenter - Neuss Innenstadt\" -> [\"TUI\", \"ReiseCenter\", \"Neuss\", \"Innenstadt\"]\n const nameParts = origName.split(/[\\s\\-\\/,.]/);\n for (let split = nameParts.length; split > 0; split--) {\n const name = nameParts.slice(0, split).join(' '); // e.g. \"TUI ReiseCenter\"\n const branch = nameParts.slice(split).join(' '); // e.g. \"Neuss Innenstadt\"\n const nameHits = _nsi.matcher.match(k, v, name, loc);\n if (!nameHits || !nameHits.length) continue; // no match, try next name fragment\n\n if (nameHits.some(hit => hit.itemID === itemID)) { // matched the name fragment to the same itemID above\n if (branch) {\n if (notBranches.test(branch)) { // \"branch\" was detected but is noise (\"factory outlet\", etc)\n newTags.name = origName; // Leave `name` alone, this part of the name may be significant..\n } else {\n const branchHits = _nsi.matcher.match(k, v, branch, loc);\n if (branchHits && branchHits.length) { // if \"branch\" matched something else in NSI..\n if (branchHits[0].match === 'primary' || branchHits[0].match === 'alternate') { // if another brand! (e.g. \"KFC - Taco Bell\"?)\n return null; // bail out - can't suggest tags in this case\n } // else a generic (e.g. \"gas\", \"cafe\") - ignore\n } else { // \"branch\" is not noise and not something in NSI\n newTags.branch = branch; // Stick it in the `branch` tag..\n }\n }\n }\n break;\n }\n }\n }\n }\n\n return { newTags: newTags, matched: item };\n }\n\n return changed ? { newTags: newTags, matched: null } : null;\n}\n\n\n// `_isGenericName()`\n// Is the `name` tag generic?\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `true` if it is generic, `false` if not\n//\nfunction _isGenericName(tags) {\n const n = tags.name;\n if (!n) return false;\n\n // tryNames just contains the `name` tag value and nothing else\n const tryNames = { primary: new Set([n]), alternate: new Set() };\n\n // Gather key/value tag pairs to try to match\n const tryKVs = gatherKVs(tags);\n if (!tryKVs.primary.size && !tryKVs.alternate.size) return false;\n\n // Order the [key,value,name] tuples - test primary before alternate\n const tuples = gatherTuples(tryKVs, tryNames);\n\n for (let i = 0; i < tuples.length; i++) {\n const tuple = tuples[i];\n const hits = _nsi.matcher.match(tuple.k, tuple.v, tuple.n); // Attempt to match an item in NSI\n\n // If we get a `excludeGeneric` hit, this is a generic name.\n if (hits && hits.length && hits[0].match === 'excludeGeneric') return true;\n }\n\n return false;\n}\n\n\n\n// PUBLIC INTERFACE\n\nexport default {\n\n // `init()`\n // On init, start preparing the name-suggestion-index\n //\n init: () => {\n // Note: service.init is called immediately after the presetManager has started loading its data.\n // We expect to chain onto an unfulfilled promise here.\n setNsiSources();\n presetManager.ensureLoaded()\n .then(() => loadNsiPresets())\n .then(() => loadNsiData())\n .then(() => _nsiStatus = 'ok')\n .catch(() => _nsiStatus = 'failed');\n },\n\n\n // `reset()`\n // Reset is called when user saves data to OSM (does nothing here)\n //\n reset: () => {},\n\n\n // `status()`\n // To let other code know how it's going...\n //\n // Returns\n // `String`: 'loading', 'ok', 'failed'\n //\n status: () => _nsiStatus,\n\n\n // `isGenericName()`\n // Is the `name` tag generic?\n //\n // Arguments\n // `tags`: `Object` containing the feature's OSM tags\n // Returns\n // `true` if it is generic, `false` if not\n //\n isGenericName: (tags) => _isGenericName(tags),\n\n\n // `upgradeTags()`\n // Suggest tag upgrades.\n // This function will not modify the input tags, it makes a copy.\n //\n // Arguments\n // `tags`: `Object` containing the feature's OSM tags\n // `loc`: Location where this feature exists, as a [lon, lat]\n // Returns\n // `Object` containing the result, or `null` if no changes needed:\n // {\n // 'newTags': `Object` - The tags the the feature should have\n // 'matched': `Object` - The matched item\n // }\n //\n upgradeTags: (tags, loc) => _upgradeTags(tags, loc),\n\n\n // `cache()`\n // Direct access to the NSI cache, useful for testing or breaking things\n //\n // Returns\n // `Object`: the internal NSI cache\n //\n cache: () => _nsi\n};\n", "import { localizer } from '../core/localizer';\n\nfunction timeSince(date: Date): [value: number, unit: Intl.RelativeTimeFormatUnit] {\n const seconds = Math.floor((+new Date() - +date) / 1000);\n const s = (n: number) => Math.floor(seconds / n);\n\n if (s(60 * 60 * 24 * 365) > 1) return [s(60 * 60 * 24 * 365), 'years'];\n if (s(60 * 60 * 24 * 30) > 1) return [s(60 * 60 * 24 * 30), 'months'];\n if (s(60 * 60 * 24) > 1) return [s(60 * 60 * 24), 'days'];\n if (s(60 * 60) > 1) return [s(60 * 60), 'hours'];\n if (s(60) > 1) return [s(60), 'minutes'];\n return [s(1), 'seconds'];\n}\n\n/**\n * Show the relative time if {@link Intl.RelativeTimeFormat} is supported\n * Otherwise fallback to the current date\n */\nexport function getRelativeDate(date: Date) {\n const preferredLanguage = localizer.localeCode();\n\n if (typeof Intl === 'undefined' || typeof Intl.RelativeTimeFormat === 'undefined') {\n return `on ${date.toLocaleDateString(preferredLanguage)}`;\n }\n\n const [number, units] = timeSince(date);\n if (!Number.isFinite(number)) return '-';\n\n return new Intl.RelativeTimeFormat(preferredLanguage).format(-number, units);\n}\n\nexport function localeDateString(date: Date | string) {\n if (!date) return null;\n const options: Intl.DateTimeFormatOptions = {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n };\n const d = new Date(date);\n if (Number.isNaN(d.getTime())) return null;\n return d.toLocaleDateString(localizer.localeCode(), options);\n}\n\nexport function localeTimestamp(date: Date) {\n const options: Intl.DateTimeFormatOptions = {\n day: '2-digit',\n month: '2-digit',\n year: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n second: 'numeric',\n };\n return date.toLocaleString(localizer.localeCode(), options);\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { json as d3_json } from 'd3-fetch';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport RBush from 'rbush';\n\nimport { geoExtent, geoScaleToZoom } from '../geo';\nimport { utilQsString, utilRebind, utilSetTransform, utilStringQs, utilTiler } from '../util';\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\nimport { localeDateString } from '../util/date';\n\n\nvar apibase = 'https://kartaview.org';\nvar maxResults = 1000;\nvar tileZoom = 14;\nvar tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);\nvar dispatch = d3_dispatch('loadedImages');\nvar imgZoom = d3_zoom()\n .extent([[0, 0], [320, 240]])\n .translateExtent([[0, 0], [320, 240]])\n .scaleExtent([1, 15]);\nvar _oscCache;\nvar _oscSelectedImage;\nvar _loadViewerPromise;\n\n\nfunction abortRequest(controller) {\n controller.abort();\n}\n\n\nfunction maxPageAtZoom(z) {\n if (z < 15) return 2;\n if (z === 15) return 5;\n if (z === 16) return 10;\n if (z === 17) return 20;\n if (z === 18) return 40;\n if (z > 18) return 80;\n}\n\n\nfunction loadTiles(which, url, projection) {\n var currZoom = Math.floor(geoScaleToZoom(projection.scale()));\n var tiles = tiler.getTiles(projection);\n\n // abort inflight requests that are no longer needed\n var cache = _oscCache[which];\n Object.keys(cache.inflight).forEach(function(k) {\n var wanted = tiles.find(function(tile) { return k.indexOf(tile.id + ',') === 0; });\n if (!wanted) {\n abortRequest(cache.inflight[k]);\n delete cache.inflight[k];\n }\n });\n\n tiles.forEach(function(tile) {\n loadNextTilePage(which, currZoom, url, tile);\n });\n}\n\n\nfunction loadNextTilePage(which, currZoom, url, tile) {\n var cache = _oscCache[which];\n var bbox = tile.extent.bbox();\n var maxPages = maxPageAtZoom(currZoom);\n var nextPage = cache.nextPage[tile.id] || 1;\n var params = utilQsString({\n ipp: maxResults,\n page: nextPage,\n // client_id: clientId,\n bbTopLeft: [bbox.maxY, bbox.minX].join(','),\n bbBottomRight: [bbox.minY, bbox.maxX].join(',')\n }, true);\n\n if (nextPage > maxPages) return;\n\n var id = tile.id + ',' + String(nextPage);\n if (cache.loaded[id] || cache.inflight[id]) return;\n\n var controller = new AbortController();\n cache.inflight[id] = controller;\n\n var options = {\n method: 'POST',\n signal: controller.signal,\n body: params,\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' }\n };\n\n d3_json(url, options)\n .then(function(data) {\n cache.loaded[id] = true;\n delete cache.inflight[id];\n if (!data || !data.currentPageItems || !data.currentPageItems.length) {\n throw new Error('No Data');\n }\n\n var features = data.currentPageItems.map(function(item) {\n var loc = [+item.lng, +item.lat];\n var d;\n\n if (which === 'images') {\n d = {\n service: 'photo',\n loc: loc,\n key: item.id,\n ca: +item.heading,\n captured_at: (item.shot_date || item.date_added),\n captured_by: item.username,\n imagePath: item.name,\n sequence_id: item.sequence_id,\n sequence_index: +item.sequence_index\n };\n\n // cache sequence info\n var seq = _oscCache.sequences[d.sequence_id];\n if (!seq) {\n seq = { rotation: 0, images: [] };\n _oscCache.sequences[d.sequence_id] = seq;\n }\n seq.images[d.sequence_index] = d;\n _oscCache.images.forImageKey[d.key] = d; // cache imageKey -> image\n }\n\n return {\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n };\n });\n\n cache.rtree.load(features);\n\n if (data.currentPageItems.length === maxResults) { // more pages to load\n cache.nextPage[tile.id] = nextPage + 1;\n loadNextTilePage(which, currZoom, url, tile);\n } else {\n cache.nextPage[tile.id] = Infinity; // no more pages to load\n }\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n }\n })\n .catch(function() {\n cache.loaded[id] = true;\n delete cache.inflight[id];\n });\n}\n\nexport default {\n\n init: function() {\n if (!_oscCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset: function() {\n if (_oscCache) {\n Object.values(_oscCache.images.inflight).forEach(abortRequest);\n }\n\n _oscCache = {\n images: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), forImageKey: {} },\n sequences: {}\n };\n },\n\n\n images: function(projection) {\n var limit = 5;\n return searchLimited(limit, projection, _oscCache.images.rtree);\n },\n\n\n sequences: function(projection) {\n var viewport = projection.clipExtent();\n var min = [viewport[0][0], viewport[1][1]];\n var max = [viewport[1][0], viewport[0][1]];\n var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n var sequenceKeys = {};\n\n // all sequences for images in viewport\n _oscCache.images.rtree.search(bbox)\n .forEach(function(d) { sequenceKeys[d.data.sequence_id] = true; });\n\n // make linestrings from those sequences\n var lineStrings = [];\n Object.keys(sequenceKeys)\n .forEach(function(sequenceKey) {\n var seq = _oscCache.sequences[sequenceKey];\n var images = seq && seq.images;\n\n if (images) {\n lineStrings.push({\n type: 'LineString',\n coordinates: images.map(function (d) { return d.loc; }).filter(Boolean),\n properties: {\n captured_at: images[0] ? images[0].captured_at: null,\n captured_by: images[0] ? images[0].captured_by: null,\n key: sequenceKey\n }\n });\n }\n });\n return lineStrings;\n },\n\n\n cachedImage: function(imageKey) {\n return _oscCache.images.forImageKey[imageKey];\n },\n\n\n loadImages: function(projection) {\n var url = apibase + '/1.0/list/nearby-photos/';\n loadTiles('images', url, projection);\n },\n\n\n ensureViewerLoaded: function(context) {\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n // add kartaview-wrapper\n var wrap = context.container().select('.photoviewer').selectAll('.kartaview-wrapper')\n .data([0]);\n\n var that = this;\n\n var wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper kartaview-wrapper')\n .classed('hide', true)\n .call(imgZoom.on('zoom', zoomPan))\n .on('dblclick.zoom', null);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n var controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls');\n\n controlsEnter\n .append('button')\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .on('click.rotate-ccw', rotate(-90))\n .text('\u293F');\n\n controlsEnter\n .append('button')\n .on('click.rotate-cw', rotate(90))\n .text('\u293E');\n\n controlsEnter\n .append('button')\n .on('click.forward', step(1))\n .text('\u25BA');\n\n wrapEnter\n .append('div')\n .attr('class', 'kartaview-image-wrap');\n\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.kartaview', function(dimensions) {\n imgZoom\n .extent([[0, 0], dimensions])\n .translateExtent([[0, 0], dimensions]);\n });\n\n\n function zoomPan(d3_event) {\n var t = d3_event.transform;\n context.container().select('.photoviewer .kartaview-image-wrap')\n .call(utilSetTransform, t.x, t.y, t.k);\n }\n\n\n function rotate(deg) {\n return function() {\n if (!_oscSelectedImage) return;\n var sequenceKey = _oscSelectedImage.sequence_id;\n var sequence = _oscCache.sequences[sequenceKey];\n if (!sequence) return;\n\n var r = sequence.rotation || 0;\n r += deg;\n\n if (r > 180) r -= 360;\n if (r < -180) r += 360;\n sequence.rotation = r;\n\n var wrap = context.container().select('.photoviewer .kartaview-wrapper');\n\n wrap\n .transition()\n .duration(100)\n .call(imgZoom.transform, d3_zoomIdentity);\n\n wrap.selectAll('.kartaview-image')\n .transition()\n .duration(100)\n .style('transform', 'rotate(' + r + 'deg)');\n };\n }\n\n function step(stepBy) {\n return function() {\n if (!_oscSelectedImage) return;\n var sequenceKey = _oscSelectedImage.sequence_id;\n var sequence = _oscCache.sequences[sequenceKey];\n if (!sequence) return;\n\n var nextIndex = _oscSelectedImage.sequence_index + stepBy;\n var nextImage = sequence.images[nextIndex];\n if (!nextImage) return;\n\n context.map().centerEase(nextImage.loc);\n\n that\n .selectImage(context, nextImage.key);\n };\n }\n\n // don't need any async loading so resolve immediately\n _loadViewerPromise = Promise.resolve();\n\n return _loadViewerPromise;\n },\n\n\n showViewer: function(context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.kartaview-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.kartaview-wrapper')\n .classed('hide', false);\n }\n\n return this;\n },\n\n\n hideViewer: function(context) {\n _oscSelectedImage = null;\n\n this.updateUrlImage(null);\n\n var viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n return this.setStyles(context, null, true);\n },\n\n\n selectImage: function(context, imageKey) {\n\n var d = this.cachedImage(imageKey);\n\n _oscSelectedImage = d;\n\n this.updateUrlImage(imageKey);\n\n var viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null, true);\n\n context.container().selectAll('.icon-sign')\n .classed('currentView', false);\n\n if (!d) return this;\n\n var wrap = context.container().select('.photoviewer .kartaview-wrapper');\n var imageWrap = wrap.selectAll('.kartaview-image-wrap');\n var attribution = wrap.selectAll('.photo-attribution').text('');\n\n wrap\n .transition()\n .duration(100)\n .call(imgZoom.transform, d3_zoomIdentity);\n\n imageWrap\n .selectAll('.kartaview-image')\n .remove();\n\n if (d) {\n var sequence = _oscCache.sequences[d.sequence_id];\n var r = (sequence && sequence.rotation) || 0;\n\n imageWrap\n .append('img')\n .attr('class', 'kartaview-image')\n .attr('src', (apibase + '/' + d.imagePath).replace(/^https:\\/\\/kartaview\\.org\\/storage(\\d+)\\//, 'https://storage$1.openstreetcam.org/'))\n .style('transform', 'rotate(' + r + 'deg)');\n\n if (d.captured_by) {\n attribution\n .append('a')\n .attr('class', 'captured_by')\n .attr('target', '_blank')\n .attr('href', 'https://kartaview.org/user/' + encodeURIComponent(d.captured_by))\n .text('@' + d.captured_by);\n\n attribution\n .append('span')\n .text('|');\n }\n\n if (d.captured_at) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeDateString(d.captured_at));\n\n attribution\n .append('span')\n .text('|');\n }\n\n attribution\n .append('a')\n .attr('class', 'image-link')\n .attr('target', '_blank')\n .attr('href', 'https://kartaview.org/details/' + d.sequence_id + '/' + d.sequence_index)\n .text('kartaview.org');\n }\n\n return this;\n },\n\n\n getSelectedImage: function() {\n return _oscSelectedImage;\n },\n\n\n getSequenceKeyForImage: function(d) {\n return d && d.sequence_id;\n },\n\n\n // Updates the currently highlighted sequence and selected bubble.\n // Reset is only necessary when interacting with the viewport because\n // this implicitly changes the currently selected bubble/sequence\n setStyles: function(context, hovered, reset) {\n if (reset) { // reset all layers\n context.container().selectAll('.viewfield-group')\n .classed('highlighted', false)\n .classed('hovered', false)\n .classed('currentView', false);\n\n context.container().selectAll('.sequence')\n .classed('highlighted', false)\n .classed('currentView', false);\n }\n\n var hoveredImageId = hovered && hovered.key;\n var hoveredSequenceId = this.getSequenceKeyForImage(hovered);\n\n var viewer = context.container().select('.photoviewer');\n var selected = viewer.empty() ? undefined : viewer.datum();\n var selectedImageId = selected && selected.key;\n var selectedSequenceId = this.getSequenceKeyForImage(selected);\n\n // highlight sibling viewfields on either the selected or the hovered sequences\n context.container().selectAll('.layer-kartaview .viewfield-group')\n .classed('highlighted', function(d) { return d.sequence_id === selectedSequenceId || d.id === hoveredImageId; })\n .classed('hovered', function(d) { return d.key === hoveredImageId; })\n .classed('currentView', function(d) { return d.key === selectedImageId; });\n\n context.container().selectAll('.layer-kartaview .sequence')\n .classed('highlighted', function(d) { return d.properties.key === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.key === selectedSequenceId; });\n\n // update viewfields if needed\n context.container().selectAll('.layer-kartaview .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n var d = this.parentNode.__data__;\n if (d.pano && d.key !== selectedImageId) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n return this;\n },\n\n\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'kartaview/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n\n cache: function() {\n return _oscCache;\n }\n\n};\n", "import type { Feature, FeatureCollection, Geometry, Position } from 'geojson';\nimport whichPolygon from 'which-polygon';\nimport rawBorders from './data/borders.json';\n\ninterface RegionFeatureProperties {\n // Unique identifier specific to country-coder\n id: string;\n\n // ISO 3166-1 alpha-2 code\n iso1A2: string | undefined;\n\n // ISO 3166-1 alpha-3 code\n iso1A3: string | undefined;\n\n // ISO 3166-1 numeric-3 code\n iso1N3: string | undefined;\n\n // UN M49 code\n m49: string | undefined;\n\n // Wikidata QID\n wikidata: string;\n\n // The emoji flag sequence derived from this feature's ISO 3166-1 alpha-2 code\n emojiFlag: string | undefined;\n\n // The ccTLD (country code top-level domain)\n ccTLD: string | undefined;\n\n // The common English name\n nameEn: string;\n\n // Additional identifiers which can be used to look up this feature;\n // these cannot collide with the identifiers for any other feature\n aliases: Array | undefined;\n\n // For features entirely within a country, the ISO 3166-1 alpha-2 code for that country\n country: string | undefined;\n\n // The ISO 3166-1 alpha-2, M49, or QIDs of other features this feature is entirely within, including its country\n groups: Array;\n\n // The ISO 3166-1 alpha-2, M49, or QIDs of other features this feature contains;\n // the inverse of `groups`\n members: Array | undefined;\n\n // The rough geographic type of this feature.\n // Levels do not necessarily nest cleanly within each other.\n // - `world`: all features\n\n // - `unitedNations`: United Nations\n // - `union`: European Union\n // - `subunion`: Outermost Regions of the EU, Overseas Countries and Territories of the EU\n\n // Defined by the UN\n // - `region`: Africa, Americas, Antarctica, Asia, Europe, Oceania\n // - `subregion`: Sub-Saharan Africa, North America, Micronesia, etc.\n // - `intermediateRegion`: Eastern Africa, South America, Channel Islands, etc.\n\n // - `sharedLandform`: Great Britain, Macaronesia, Mariana Islands, etc.\n // - `country`: Ethiopia, Brazil, United States, etc.\n // - `subcountryGroup`\n // - `territory`: Puerto Rico, Gurnsey, Hong Kong, etc.\n // - `subterritory`: Sark, Ascension Island, Diego Garcia, etc.\n level: string;\n\n // The status of this feature's ISO 3166-1 code(s), if any\n // - `official`: officially-assigned\n // - `excRes`: exceptionally-reserved\n // - `usrAssn`: user-assigned\n isoStatus: string | undefined;\n\n // The side of the road that traffic drives on within this feature\n // - `right`\n // - `left`\n driveSide: 'left' | 'right' | undefined;\n\n // The unit used for road traffic speeds within this feature\n // - `mph`: miles per hour\n // - `km/h`: kilometers per hour\n roadSpeedUnit: 'mph' | 'km/h' | undefined;\n\n // The unit used for road vehicle height restrictions within this feature\n // - `ft`: feet and inches\n // - `m`: meters\n roadHeightUnit: 'ft' | 'm' | undefined;\n\n // The international calling codes for this feature, sometimes including area codes\n // e.g. `1`, `1 340`\n callingCodes: Array | undefined;\n};\n\ntype RegionFeature = Feature;\ntype RegionFeatureCollection = FeatureCollection;\ntype Vec2 = [number, number]; // [lon, lat]\ntype Bbox = [number, number, number, number]; // [minLon, minLat, maxLon, maxLat]\n\ninterface PointGeometry {\n type: string;\n coordinates: Vec2\n};\n\ninterface PointFeature {\n type: string;\n geometry: PointGeometry;\n properties: any\n};\ntype Location = Vec2 | PointGeometry | PointFeature;\n\ninterface CodingOptions {\n // For overlapping features, the division level of the one to get. If no feature\n // exists at the given level, the feature at the next higher level is returned.\n // See the `level` property of `RegionFeatureProperties` for possible values.\n level?: string | undefined;\n // Only a feature at the specified level or lower will be returned.\n maxLevel?: string | undefined;\n // Only a feature with the specified property will be returned.\n withProp?: string | undefined;\n};\n\n// The base GeoJSON feature collection\nexport const borders: RegionFeatureCollection = rawBorders as RegionFeatureCollection;\n\n// The whichPolygon interface for looking up a feature by point\nlet _whichPolygon: any = {};\n// The cache for looking up a feature by identifier\nconst _featuresByCode: any = {};\n\n// discard special characters and instances of and/the/of that aren't the only characters\nconst idFilterRegex =\n /(?=(?!^(and|the|of|el|la|de)$))(\\b(and|the|of|el|la|de)\\b)|[-_ .,'()&[\\]/]/gi;\n\nfunction canonicalID(id: string | null): string {\n const s = id || '';\n if (s.charAt(0) === '.') {\n // skip replace if it leads with a '.' (e.g. a ccTLD like '.de', '.la')\n return s.toUpperCase();\n } else {\n return s.replace(idFilterRegex, '').toUpperCase();\n }\n}\n\n// Geographic levels, roughly from most to least granular\nconst levels = [\n 'subterritory',\n 'territory',\n 'subcountryGroup',\n 'country',\n 'sharedLandform',\n 'intermediateRegion',\n 'subregion',\n 'region',\n 'subunion',\n 'union',\n 'unitedNations',\n 'world'\n];\n\nloadDerivedDataAndCaches(borders);\n\n// Loads implicit feature data and the getter index caches\nfunction loadDerivedDataAndCaches(borders) {\n const identifierProps = ['iso1A2', 'iso1A3', 'm49', 'wikidata', 'emojiFlag', 'ccTLD', 'nameEn'];\n const geometryFeatures: Array = [];\n\n for (const feature of borders.features) {\n // generate a unique ID for each feature\n const props = feature.properties;\n props.id = props.iso1A2 || props.m49 || props.wikidata;\n\n loadM49(feature);\n loadTLD(feature);\n loadIsoStatus(feature);\n loadLevel(feature);\n loadGroups(feature);\n loadFlag(feature);\n // cache only after loading derived IDs\n cacheFeatureByIDs(feature);\n\n if (feature.geometry) {\n geometryFeatures.push(feature);\n }\n }\n\n // must load `members` only after fully loading `featuresByID`\n for (const feature of borders.features) {\n // ensure all groups are listed by their ID\n feature.properties.groups = feature.properties.groups.map(groupID => {\n return _featuresByCode[groupID].properties.id;\n });\n loadMembersForGroupsOf(feature);\n }\n\n // must load attributes only after loading geometry features into `members`\n for (const feature of borders.features) {\n loadRoadSpeedUnit(feature);\n loadRoadHeightUnit(feature);\n loadDriveSide(feature);\n loadCallingCodes(feature);\n loadGroupGroups(feature);\n }\n\n for (const feature of borders.features) {\n // order groups by their `level`\n feature.properties.groups.sort((groupID1, groupID2) => {\n return (\n levels.indexOf(_featuresByCode[groupID1].properties.level) -\n levels.indexOf(_featuresByCode[groupID2].properties.level)\n );\n });\n // order members by their `level` and then by order in borders\n if (feature.properties.members) {\n feature.properties.members.sort((id1, id2) => {\n const diff =\n levels.indexOf(_featuresByCode[id1].properties.level) -\n levels.indexOf(_featuresByCode[id2].properties.level);\n if (diff === 0) {\n return (\n borders.features.indexOf(_featuresByCode[id1]) -\n borders.features.indexOf(_featuresByCode[id2])\n );\n }\n return diff;\n });\n }\n }\n\n // whichPolygon doesn't support null geometry even though GeoJSON does\n const geometryOnlyCollection: RegionFeatureCollection = {\n type: 'FeatureCollection',\n features: geometryFeatures\n };\n _whichPolygon = whichPolygon(geometryOnlyCollection);\n\n function loadGroups(feature: RegionFeature) {\n const props = feature.properties;\n if (!props.groups) {\n props.groups = [];\n }\n if (feature.geometry && props.country) {\n // Add `country` to `groups`\n props.groups.push(props.country);\n }\n if (props.m49 !== '001') { // all features are in the world feature except the world itself\n props.groups.push('001');\n }\n }\n\n function loadM49(feature: RegionFeature) {\n const props = feature.properties;\n if (!props.m49 && props.iso1N3) { // M49 is a superset of ISO numerics so we only need to store one\n props.m49 = props.iso1N3;\n }\n }\n\n function loadTLD(feature: RegionFeature) {\n const props = feature.properties;\n if (props.level === 'unitedNations') return; // `.un` is not a ccTLD\n if (props.ccTLD === null) return; // e.g. Sark is an ISO code but not a ccTLD\n if (!props.ccTLD && props.iso1A2) { // ccTLD is nearly the same as iso1A2, so we only need to explicitly code any exceptions\n props.ccTLD = '.' + props.iso1A2.toLowerCase();\n }\n }\n\n function loadIsoStatus(feature: RegionFeature) {\n const props = feature.properties;\n if (!props.isoStatus && props.iso1A2) { // Features with an ISO code but no explicit status are officially-assigned\n props.isoStatus = 'official';\n }\n }\n\n function loadLevel(feature: RegionFeature) {\n const props = feature.properties;\n if (props.level) return;\n if (!props.country) { // a feature without an explicit `level` or `country` is itself a country\n props.level = 'country';\n } else if (!props.iso1A2 || props.isoStatus === 'official') {\n props.level = 'territory';\n } else {\n props.level = 'subterritory';\n }\n }\n\n function loadGroupGroups(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry || !props.members) return;\n\n const featureLevelIndex = levels.indexOf(props.level);\n let sharedGroups: Array = [];\n\n props.members.forEach((memberID, index) => {\n const member = _featuresByCode[memberID];\n const memberGroups = member.properties.groups.filter((groupID) => {\n return (\n groupID !== feature.properties.id &&\n featureLevelIndex < levels.indexOf(_featuresByCode[groupID].properties.level)\n );\n });\n if (index === 0) {\n sharedGroups = memberGroups;\n } else {\n sharedGroups = sharedGroups.filter((groupID) => memberGroups.indexOf(groupID) !== -1);\n }\n });\n\n props.groups = props.groups.concat(\n sharedGroups.filter((groupID) => props.groups.indexOf(groupID) === -1)\n );\n\n for (const groupID of sharedGroups) {\n const groupFeature = _featuresByCode[groupID];\n if (groupFeature.properties.members.indexOf(props.id) === -1) {\n groupFeature.properties.members.push(props.id);\n }\n }\n }\n\n function loadRoadSpeedUnit(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry) {\n // only `mph` regions are listed explicitly, else assume `km/h`\n if (!props.roadSpeedUnit) props.roadSpeedUnit = 'km/h';\n } else if (props.members) {\n const vals = Array.from(\n new Set(\n props.members\n .map(id => {\n const member = _featuresByCode[id];\n if (member.geometry) {\n return member.properties.roadSpeedUnit || 'km/h';\n } else {\n return null;\n }\n })\n .filter(Boolean)\n )\n );\n // if all members have the same value then that's also the value for this feature\n if (vals.length === 1) props.roadSpeedUnit = vals[0];\n }\n }\n\n function loadRoadHeightUnit(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry) {\n // only `ft` regions are listed explicitly, else assume `m`\n if (!props.roadHeightUnit) props.roadHeightUnit = 'm';\n } else if (props.members) {\n const vals = Array.from(\n new Set(\n props.members\n .map(id => {\n const member = _featuresByCode[id];\n if (member.geometry) {\n return member.properties.roadHeightUnit || 'm';\n } else {\n return null;\n }\n })\n .filter(Boolean)\n )\n );\n // if all members have the same value then that's also the value for this feature\n if (vals.length === 1) props.roadHeightUnit = vals[0];\n }\n }\n\n function loadDriveSide(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry) {\n // only `left` regions are listed explicitly, else assume `right`\n if (!props.driveSide) props.driveSide = 'right';\n } else if (props.members) {\n const vals = Array.from(\n new Set(\n props.members\n .map(id => {\n const member = _featuresByCode[id];\n if (member.geometry) {\n return member.properties.driveSide || 'right';\n } else {\n return null;\n }\n })\n .filter(Boolean)\n )\n );\n // if all members have the same value then that's also the value for this feature\n if (vals.length === 1) props.driveSide = vals[0];\n }\n }\n\n function loadCallingCodes(feature: RegionFeature) {\n const props = feature.properties;\n if (!feature.geometry && props.members) {\n props.callingCodes = Array.from(\n new Set(\n props.members.reduce((array, id) => {\n const member = _featuresByCode[id];\n if (member.geometry && member.properties.callingCodes) {\n return array.concat(member.properties.callingCodes);\n }\n return array;\n }, [])\n )\n );\n }\n }\n\n // Calculates the emoji flag (if any) and caches it\n function loadFlag(feature: RegionFeature) {\n let flag = '';\n\n // Most emoji flags can be generated from their 2 letter code.\n // Skip 'FX' (Metropolitan France), allow it to roll up to 'FR' - #25\n const country = feature.properties.iso1A2;\n if (country && country !== 'FX') {\n flag = _toEmojiCountryFlag(country);\n }\n\n // Support a few regional flags - #157\n const regionStrings = {\n Q21: 'gbeng', // GB-ENG (England)\n Q22: 'gbsct', // GB-SCT (Scotland)\n Q25: 'gbwls' // GB-WLS (Wales)\n };\n const region = regionStrings[feature.properties.wikidata];\n if (region) {\n flag = _toEmojiRegionFlag(region);\n }\n\n if (flag) {\n feature.properties.emojiFlag = flag;\n }\n\n // Normally, take isoA2 chars and jump up into \"Enclosed Alphanumeric Supplement\" block\n // see https://en.wikipedia.org/wiki/Regional_indicator_symbol\n // see https://en.wikipedia.org/wiki/Enclosed_Alphanumeric_Supplement\n function _toEmojiCountryFlag(s: string): string {\n return s.replace(/./g, c => String.fromCodePoint(c.charCodeAt(0) + 0x1F1A5));\n }\n\n // Regional flags are encoded as U+1F3F4 (black flag) + the region string (jump up to \"Tags\" block) + U+E007F (end)\n // see https://en.wikipedia.org/wiki/Tags_(Unicode_block)\n function _toEmojiRegionFlag(s: string) {\n const codepoints = [0x1F3F4];\n for (const c of [...s]) {\n codepoints.push(c.codePointAt(0) as number + 0xE0000);\n }\n codepoints.push(0xE007F);\n return String.fromCodePoint.apply(null, codepoints);\n }\n }\n\n // Populate `members` as the inverse relationship of `groups`\n function loadMembersForGroupsOf(feature: RegionFeature) {\n for (const groupID of feature.properties.groups) {\n const groupFeature = _featuresByCode[groupID];\n\n if (!groupFeature.properties.members) {\n groupFeature.properties.members = [];\n }\n groupFeature.properties.members.push(feature.properties.id);\n }\n }\n\n // Caches features by their identifying strings for rapid lookup\n function cacheFeatureByIDs(feature: RegionFeature) {\n const ids: Array = [];\n\n for (const prop of identifierProps) {\n const id = feature.properties[prop];\n if (id) {\n ids.push(id);\n }\n }\n\n for (const alias of (feature.properties.aliases || [])) {\n ids.push(alias);\n }\n\n for (const id of ids) {\n const cid = canonicalID(id);\n _featuresByCode[cid] = feature;\n }\n }\n}\n\n// Returns the [longitude, latitude] for the location argument\nfunction locArray(loc: Location): Vec2 {\n if (Array.isArray(loc)) {\n return loc as Vec2;\n } else if ((loc as PointGeometry).coordinates) {\n return (loc as PointGeometry).coordinates;\n } else {\n return (loc as PointFeature).geometry.coordinates;\n }\n}\n\n// Returns the smallest feature of any kind containing `loc`, if any\nfunction smallestFeature(loc: Location): RegionFeature | null {\n const query = locArray(loc);\n const featureProperties: RegionFeatureProperties = _whichPolygon(query);\n if (!featureProperties) return null;\n return _featuresByCode[featureProperties.id];\n}\n\n// Returns the country feature containing `loc`, if any\nfunction countryFeature(loc: Location): RegionFeature | null {\n const feature = smallestFeature(loc);\n if (!feature) return null;\n\n // a feature without `country` but with geometry is itself a country\n const countryCode = feature.properties.country || feature.properties.iso1A2;\n return _featuresByCode[countryCode as string] || null;\n}\n\nconst defaultOpts = {\n level: undefined,\n maxLevel: undefined,\n withProp: undefined\n};\n\n// Returns the feature containing `loc` for the `opts`, if any\nfunction featureForLoc(loc: Location, opts: CodingOptions): RegionFeature | null {\n const targetLevel = opts.level || 'country';\n const maxLevel = opts.maxLevel || 'world';\n const withProp = opts.withProp;\n\n const targetLevelIndex = levels.indexOf(targetLevel);\n if (targetLevelIndex === -1) return null;\n\n const maxLevelIndex = levels.indexOf(maxLevel);\n if (maxLevelIndex === -1) return null;\n if (maxLevelIndex < targetLevelIndex) return null;\n\n if (targetLevel === 'country') {\n // attempt fast path for country-level coding\n const fastFeature = countryFeature(loc);\n if (fastFeature) {\n if (!withProp || fastFeature.properties[withProp]) {\n return fastFeature;\n }\n }\n }\n\n const features = featuresContaining(loc);\n\n const match = features.find((feature) => {\n const levelIndex = levels.indexOf(feature.properties.level);\n if (\n feature.properties.level === targetLevel ||\n // if no feature exists at the target level, return the first feature at the next level up\n (levelIndex > targetLevelIndex && levelIndex <= maxLevelIndex)\n ) {\n if (!withProp || feature.properties[withProp]) {\n return feature;\n }\n }\n return false;\n });\n\n return match || null;\n}\n\n// Returns the feature with an identifying property matching `id`, if any\nfunction featureForID(id: string | number): RegionFeature | null {\n let stringID: string;\n\n if (typeof id === 'number') {\n stringID = id.toString();\n if (stringID.length === 1) {\n stringID = '00' + stringID;\n } else if (stringID.length === 2) {\n stringID = '0' + stringID;\n }\n } else {\n stringID = canonicalID(id);\n }\n return _featuresByCode[stringID] || null;\n}\n\nfunction smallestFeaturesForBbox(bbox: Bbox): [RegionFeature] {\n return _whichPolygon.bbox(bbox).map(props => _featuresByCode[props.id]);\n}\n\nfunction smallestOrMatchingFeature(query: Location | string | number): RegionFeature | null {\n if (typeof query === 'object') {\n return smallestFeature(query as Location);\n }\n return featureForID(query);\n}\n\n// Returns the feature matching the given arguments, if any\nexport function feature(query: Location | string | number, opts: CodingOptions = defaultOpts): RegionFeature | null {\n if (typeof query === 'object') {\n return featureForLoc(query as Location, opts);\n }\n return featureForID(query);\n}\n\n// Returns the ISO 3166-1 alpha-2 code for the feature matching the arguments, if any\nexport function iso1A2Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'iso1A2';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.iso1A2 || null;\n}\n\n// Returns the ISO 3166-1 alpha-3 code for the feature matching the arguments, if any\nexport function iso1A3Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'iso1A3';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.iso1A3 || null;\n}\n\n// Returns the ISO 3166-1 numeric-3 code for the feature matching the arguments, if any\nexport function iso1N3Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'iso1N3';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.iso1N3 || null;\n}\n\n// Returns the UN M49 code for the feature matching the arguments, if any\nexport function m49Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'm49';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.m49 || null;\n}\n\n// Returns the Wikidata QID code for the feature matching the arguments, if any\nexport function wikidataQID(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'wikidata';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.wikidata;\n}\n\n// Returns the emoji emojiFlag sequence for the feature matching the arguments, if any\nexport function emojiFlag(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'emojiFlag';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.emojiFlag || null;\n}\n\n// Returns the ccTLD (country code top-level domain) for the feature matching the arguments, if any\nexport function ccTLD(\n query: Location | string | number,\n opts: CodingOptions = defaultOpts\n): string | null {\n opts.withProp = 'ccTLD';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.ccTLD || null;\n}\n\nfunction propertiesForQuery(query: Location | Bbox, property: string): Array {\n const features = featuresContaining(query, false);\n return features.map(feature => feature.properties[property]).filter(Boolean);\n}\n\n// Returns all the ISO 3166-1 alpha-2 codes of features at the location\nexport function iso1A2Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'iso1A2');\n}\n\n// Returns all the ISO 3166-1 alpha-3 codes of features at the location\nexport function iso1A3Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'iso1A3');\n}\n\n// Returns all the ISO 3166-1 numeric-3 codes of features at the location\nexport function iso1N3Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'iso1N3');\n}\n\n// Returns all the UN M49 codes of features at the location\nexport function m49Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'm49');\n}\n\n// Returns all the Wikidata QIDs of features at the location\nexport function wikidataQIDs(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'wikidata');\n}\n\n// Returns all the emoji flag sequences of features at the location\nexport function emojiFlags(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'emojiFlag');\n}\n\n// Returns all the ccTLD (country code top-level domain) sequences of features at the location\nexport function ccTLDs(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'ccTLD');\n}\n\n// Returns the feature matching `query` and all features containing it, if any.\n// If passing `true` for `strict`, an exact match will not be included\nexport function featuresContaining(query: Location | Bbox | string | number, strict?: boolean): Array {\n let matchingFeatures: Array;\n\n if (Array.isArray(query) && query.length === 4) { // check if bounding box\n matchingFeatures = smallestFeaturesForBbox(query as Bbox);\n } else {\n const smallestOrMatching = smallestOrMatchingFeature(query as Location | string | number);\n matchingFeatures = smallestOrMatching ? [smallestOrMatching] : [];\n }\n\n if (!matchingFeatures.length) return [];\n\n let returnFeatures: Array;\n if (!strict || typeof query === 'object') {\n returnFeatures = matchingFeatures.slice();\n } else {\n returnFeatures = [];\n }\n\n for (const feature of matchingFeatures) {\n const properties = feature.properties;\n for (const groupID of properties.groups) {\n const groupFeature = _featuresByCode[groupID];\n if (returnFeatures.indexOf(groupFeature) === -1) {\n returnFeatures.push(groupFeature);\n }\n }\n }\n\n return returnFeatures;\n}\n\n// Returns the feature matching `id` and all features it contains, if any.\n// If passing `true` for `strict`, an exact match will not be included\nexport function featuresIn(id: string | number, strict?: boolean): Array {\n const feature = featureForID(id);\n if (!feature) return [];\n\n const features: Array = [];\n if (!strict) {\n features.push(feature);\n }\n\n const properties = feature.properties;\n for (const memberID of (properties.members || [])) {\n features.push(_featuresByCode[memberID]);\n }\n\n return features;\n}\n\n// Returns a new feature with the properties of the feature matching `id`\n// and the combined geometry of all its component features\nexport function aggregateFeature(id: string | number): RegionFeature | null {\n const features = featuresIn(id, false);\n if (features.length === 0) return null;\n\n let aggregateCoordinates: Position[][][] = [];\n for (const feature of features) {\n if (feature.geometry?.type === 'MultiPolygon' && feature.geometry.coordinates) {\n aggregateCoordinates = aggregateCoordinates.concat(feature.geometry.coordinates);\n }\n }\n\n return {\n type: 'Feature',\n properties: features[0].properties,\n geometry: {\n type: 'MultiPolygon',\n coordinates: aggregateCoordinates\n }\n };\n}\n\n// Returns true if the feature matching `query` is, or is a part of, the feature matching `bounds`\nexport function isIn(query: Location | string | number, bounds: string | number): boolean | null {\n const queryFeature = smallestOrMatchingFeature(query);\n const boundsFeature = featureForID(bounds);\n\n if (!queryFeature || !boundsFeature) return null;\n\n if (queryFeature.properties.id === boundsFeature.properties.id) return true;\n return queryFeature.properties.groups.indexOf(boundsFeature.properties.id) !== -1;\n}\n\n// Returns true if the feature matching `query` is within EU jurisdiction\nexport function isInEuropeanUnion(query: Location | string | number): boolean | null {\n return isIn(query, 'EU');\n}\n\n// Returns true if the feature matching `query` is, or is within, a United Nations member state\nexport function isInUnitedNations(query: Location | string | number): boolean | null {\n return isIn(query, 'UN');\n}\n\n// Returns the side traffic drives on in the feature matching `query` as a string (`right` or `left`)\nexport function driveSide(query: Location | string | number): string | null {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.driveSide) || null;\n}\n\n// Returns the road speed unit for the feature matching `query` as a string (`mph` or `km/h`)\nexport function roadSpeedUnit(query: Location | string | number): string | null {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.roadSpeedUnit) || null;\n}\n\n// Returns the road vehicle height restriction unit for the feature matching `query` as a string (`ft` or `m`)\nexport function roadHeightUnit(query: Location | string | number): string | null {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.roadHeightUnit) || null;\n}\n\n// Returns the full international calling codes for phone numbers in the feature matching `query`, if any\nexport function callingCodes(query: Location | string | number): Array {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.callingCodes) || [];\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { utilRebind } from '../util';\n\n\nconst pannellumViewerCSS = 'pannellum/pannellum.css';\nconst pannellumViewerJS = 'pannellum/pannellum.js';\n\nexport async function pannellumPhotoFrame(context, selection) {\n const dispatch = d3_dispatch('viewerChanged');\n\n const module = {};\n module.event = utilRebind(module, dispatch, 'on');\n module.loadPannellum = function(context) {\n const head = d3_select('head');\n\n return Promise.all([\n new Promise((resolve, reject) => {\n // load pannellum viewer css\n head\n .selectAll('#ideditor-pannellum-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-pannellum-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(pannellumViewerCSS))\n .on('load.pannellum', resolve)\n .on('error.pannellum', reject);\n }),\n new Promise((resolve, reject) => {\n // load pannellum viewer js\n head\n .selectAll('#ideditor-pannellum-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-pannellum-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(pannellumViewerJS))\n .on('load.pannellum', resolve)\n .on('error.pannellum', reject);\n })\n ]);\n };\n\n let _currScenes = [];\n let _pannellumViewer;\n let _activeSceneKey;\n\n selection\n .append('div')\n .attr('class', 'photo-frame pannellum-frame')\n .attr('id', 'ideditor-pannellum-viewer')\n .classed('hide', true)\n .on('mousedown', function(e) { e.stopPropagation(); });\n\n if (!window.pannellum) {\n await module.loadPannellum(context);\n }\n\n const options = {\n 'default': { firstScene: '' },\n scenes: {},\n minHfov: 20,\n disableKeyboardCtrl: true,\n sceneFadeDuration: 0\n };\n\n _pannellumViewer = window.pannellum.viewer('ideditor-pannellum-viewer', options);\n\n _pannellumViewer\n .on('mousedown', () => d3_select(window)\n .on('pointermove.pannellum mousemove.pannellum', () => dispatch.call('viewerChanged')))\n .on('mouseup', () => d3_select(window)\n .on('pointermove.pannellum mousemove.pannellum', null))\n .on('animatefinished', () => dispatch.call('viewerChanged'));\n\n context.ui().photoviewer.on('resize.pannellum', () => {\n _pannellumViewer.resize();\n });\n\n /**\n * Shows the photo frame if hidden\n * @param {*} context the HTML wrap of the frame\n */\n module.showPhotoFrame = function(context) {\n const isHidden = context.selectAll('.photo-frame.pannellum-frame.hide').size();\n\n if (isHidden) {\n context\n .selectAll('.photo-frame:not(.pannellum-frame)')\n .classed('hide', true);\n\n context\n .selectAll('.photo-frame.pannellum-frame')\n .classed('hide', false);\n }\n\n return module;\n };\n\n /**\n * Hides the photo frame if shown\n * @param {*} context the HTML wrap of the frame\n */\n module.hidePhotoFrame = function(viewerContext) {\n viewerContext\n .select('photo-frame.pannellum-frame')\n .classed('hide', false);\n\n return module;\n };\n\n /**\n * Renders an image inside the frame\n * @param {*} data the image data, it should contain an image_path attribute, a link to the actual image.\n * @param {boolean} keepOrientation if true, HFOV, pitch and yaw will be kept between images\n */\n module.selectPhoto = function(data, keepOrientation) {\n const key = data.image_path;\n _activeSceneKey = key;\n if (!_currScenes.includes(key)) {\n let newSceneOptions = {\n showFullscreenCtrl: false,\n autoLoad: false,\n compass: false,\n yaw: 0,\n type: 'equirectangular',\n preview: data.preview_path,\n panorama: data.image_path,\n northOffset: data.ca\n };\n\n _currScenes.push(key);\n _pannellumViewer.addScene(key, newSceneOptions);\n }\n\n let yaw = 0;\n let pitch = 0;\n let hfov = 0;\n\n if (keepOrientation) {\n yaw = module.getYaw();\n pitch = module.getPitch();\n hfov = module.getHfov();\n }\n if (_pannellumViewer.isLoaded() !== false) {\n _pannellumViewer.loadScene(key, pitch, yaw, hfov);\n dispatch.call('viewerChanged');\n } else {\n // pannellum is currently loading another scene: wait for it to finish\n // loading the previous panorama first\n const retry = setInterval(() => {\n if (_pannellumViewer.isLoaded() === false) {\n // still not done: wait a bit longer\n return;\n }\n if (_activeSceneKey === key) {\n // only load scene if no other photo has been selected in the meantime\n _pannellumViewer.loadScene(key, pitch, yaw, hfov);\n dispatch.call('viewerChanged');\n }\n clearInterval(retry);\n }, 100);\n }\n\n if (_currScenes.length > 3) {\n const old_key = _currScenes.shift();\n _pannellumViewer.removeScene(old_key);\n }\n\n _pannellumViewer.resize();\n\n return module;\n };\n\n module.getYaw = function() {\n return _pannellumViewer.getYaw();\n };\n\n module.getPitch = function() {\n return _pannellumViewer.getPitch();\n };\n\n module.getHfov = function() {\n return _pannellumViewer.getHfov();\n };\n\n return module;\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\nimport { utilSetTransform, utilRebind } from '../util';\n\n\nexport async function planePhotoFrame(context, selection) {\n const dispatch = d3_dispatch('viewerChanged');\n\n const module = {};\n module.event = utilRebind(module, dispatch, 'on');\n\n let _photo;\n let _imageWrapper;\n let _planeWrapper;\n let _viewerDimensions = [];\n let _photoDimensions = [];\n const _imgZoom = d3_zoom()\n .on('zoom', zoomPan)\n .on('start', () => _imageWrapper.classed('grabbing', true))\n .on('end', () => _imageWrapper.classed('grabbing', false));\n\n function zoomPan(d3_event) {\n let t = d3_event.transform;\n _imageWrapper.call(utilSetTransform, t.x, t.y, t.k);\n }\n\n function loadImage(selection, path) {\n return new Promise((resolve) => {\n selection.attr('src', path);\n selection.on('load', () => {\n resolve(selection);\n });\n });\n }\n\n function updateTransform() {\n const xScale = _viewerDimensions[0] / _photoDimensions[0];\n const yScale = _viewerDimensions[1] / _photoDimensions[1];\n const fitScale = Math.max(xScale, yScale);\n const minScale = Math.min(xScale, yScale);\n _imgZoom\n .extent([[0, 0], _viewerDimensions])\n .translateExtent([[0, 0], _photoDimensions])\n .scaleExtent([minScale, 4]);\n const centerOffset = [0, 0];\n if (xScale < yScale) {\n centerOffset[0] = (_viewerDimensions[0] / fitScale - _photoDimensions[0]) / 2;\n } else {\n centerOffset[1] = (_viewerDimensions[1] / fitScale - _photoDimensions[1]) / 2;\n }\n const transform = d3_zoomIdentity.scale(fitScale).translate(centerOffset[0], centerOffset[1]);\n _planeWrapper.call(_imgZoom.transform, transform);\n }\n\n _planeWrapper = selection.append('div')\n .classed('plane-frame-wrapper', true);\n _planeWrapper.call(_imgZoom);\n\n _imageWrapper = _planeWrapper\n .append('div')\n .classed('photo-frame', true)\n .classed('plane-frame', true)\n .classed('hide', true);\n\n _photo = _imageWrapper\n .append('img')\n .attr('class', 'plane-photo');\n\n context.ui().photoviewer.on('resize.plane', function(dimensions) {\n _viewerDimensions = dimensions;\n updateTransform();\n });\n\n await Promise.resolve();\n\n /**\n * Shows the photo frame if hidden\n * @param {*} selection the HTML wrap of the frame\n */\n module.showPhotoFrame = function(selection) {\n const isHidden = selection.selectAll('.photo-frame.plane-frame.hide').size();\n\n if (isHidden) {\n selection\n .selectAll('.photo-frame:not(.plane-frame)')\n .classed('hide', true);\n\n selection\n .selectAll('.photo-frame.plane-frame')\n .classed('hide', false);\n }\n\n // set initial viewer size\n _viewerDimensions = context.ui().photoviewer.viewerSize();\n updateTransform();\n\n return module;\n };\n\n /**\n * Hides the photo frame if shown\n * @param {*} context the HTML wrap of the frame\n */\n module.hidePhotoFrame = function(context) {\n context\n .select('photo-frame.plane-frame')\n .classed('hide', false);\n\n return module;\n };\n\n /**\n * Renders an image inside the frame\n * @param {*} data the image data, it should contain an image_path attribute, a link to the actual image.\n */\n module.selectPhoto = function(data) {\n dispatch.call('viewerChanged');\n\n loadImage(_photo, '');\n _planeWrapper.classed('show-loader', true);\n loadImage(_photo, data.image_path)\n .then(selection => {\n _planeWrapper.classed('show-loader', false);\n const { naturalWidth, naturalHeight } = selection.node();\n _photoDimensions = [naturalWidth, naturalHeight];\n updateTransform();\n });\n return module;\n };\n\n module.getYaw = function() {\n return 0;\n };\n\n return module;\n};\n", "import { json as d3_json, xml as d3_xml} from 'd3-fetch';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { pairs as d3_pairs } from 'd3-array';\nimport RBush from 'rbush';\nimport { iso1A2Codes } from '@rapideditor/country-coder';\nimport { t } from '../core/localizer';\nimport { utilQsString, utilTiler, utilRebind, utilArrayUnion, utilStringQs } from '../util';\nimport { searchLimited } from '../util/partition';\nimport { localeTimestamp } from '../util/date';\nimport { geoExtent, geoVecAngle, geoVecEqual } from '../geo';\nimport { pannellumPhotoFrame } from './pannellum_photo';\nimport { planePhotoFrame } from './plane_photo';\nimport { services } from './';\n\n\nconst owsEndpoint = 'https://www.vegvesen.no/kart/ogc/vegbilder_1_0/ows?';\nconst tileZoom = 14;\nconst tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);\nconst dispatch = d3_dispatch('loadedImages', 'viewerChanged');\nconst directionEnum = Object.freeze({\n forward: Symbol(0),\n backward: Symbol(1)\n});\n\nlet _planeFrame;\nlet _pannellumFrame;\nlet _currentFrame;\nlet _loadViewerPromise;\nlet _vegbilderCache;\n\nasync function fetchAvailableLayers() {\n const params = {\n service: 'WFS',\n request: 'GetCapabilities',\n version: '2.0.0',\n };\n\n const urlForRequest = owsEndpoint + utilQsString(params);\n const response = await d3_xml(urlForRequest);\n const regexMatcher = /^vegbilder_1_0:Vegbilder(?_360)?_(?\\d{4})$/;\n const availableLayers = [];\n for (const node of response.querySelectorAll('FeatureType > Name')) {\n const match = node.textContent?.match(regexMatcher);\n if (match) {\n availableLayers.push({\n name: match[0],\n is_sphere: !!match.groups?.image_type,\n year: parseInt(match.groups?.year, 10)\n });\n }\n }\n return availableLayers;\n}\n\nfunction filterAvailableLayers(photoContex) {\n const fromDateString = photoContex.fromDate();\n const toDateString = photoContex.toDate();\n const fromYear = fromDateString ? new Date(fromDateString).getFullYear() : 2016;\n const toYear = toDateString ? new Date(toDateString).getFullYear() : null;\n const showsFlat = photoContex.showsFlat();\n const showsPano = photoContex.showsPanoramic();\n return Array.from(_vegbilderCache.wfslayers.values()).filter(({layerInfo}) => (\n (layerInfo.year >= fromYear) &&\n (!toYear || (layerInfo.year <= toYear)) &&\n ((!layerInfo.is_sphere && showsFlat) || (layerInfo.is_sphere && showsPano))\n ));\n}\n\nfunction loadWFSLayers(projection, margin, wfslayers) {\n const tiles = tiler.margin(margin).getTiles(projection);\n for (const cache of wfslayers) {\n loadWFSLayer(projection, cache, tiles);\n }\n}\n\nfunction loadWFSLayer(projection, cache, tiles) {\n // abort inflight requests that are no longer needed\n for (const [key, controller] of cache.inflight.entries()) {\n const wanted = tiles.some(tile => key === tile.id);\n if (!wanted) {\n controller.abort();\n cache.inflight.delete(key);\n }\n }\n\n Promise.all(tiles.map(\n tile => loadTile(cache, cache.layerInfo.name, tile)\n )).then(() => orderSequences(projection, cache));\n}\n\n/**\n* loadNextTilePage() load data for the next tile page in line.\n*/\nasync function loadTile(cache, typename, tile) {\n const bbox = tile.extent.bbox();\n const tileid = tile.id;\n if ((cache.loaded.get(tileid) === true) || cache.inflight.has(tileid)) return;\n\n const params = {\n service: 'WFS',\n request: 'GetFeature',\n version: '2.0.0',\n typenames: typename,\n bbox: [bbox.minY, bbox.minX, bbox.maxY, bbox.maxX].join(','),\n outputFormat: 'json'\n };\n\n const controller = new AbortController();\n cache.inflight.set(tileid, controller);\n\n const options = {\n method: 'GET',\n signal: controller.signal,\n };\n\n const urlForRequest = owsEndpoint + utilQsString(params);\n\n let featureCollection;\n try {\n featureCollection = await d3_json(urlForRequest, options);\n } catch {\n cache.loaded.set(tileid, false);\n return;\n } finally {\n cache.inflight.delete(tileid);\n }\n\n cache.loaded.set(tileid, true);\n\n if (featureCollection.features.length === 0) { return; }\n\n const features = featureCollection.features.map(feature => {\n const loc = feature.geometry.coordinates;\n const key = feature.id;\n const properties = feature.properties;\n const {\n RETNING: ca,\n TIDSPUNKT: captured_at,\n URL: image_path,\n URLPREVIEW : preview_path,\n BILDETYPE: image_type,\n METER: metering,\n FELTKODE: lane_code\n } = properties;\n const lane_number = parseInt((lane_code.match(/^[0-9]+/) || [])[0], 10);\n const direction = lane_number % 2 === 0 ? directionEnum.backward : directionEnum.forward;\n const data = {\n service: 'photo',\n loc,\n key,\n ca,\n image_path,\n preview_path,\n road_reference: roadReference(properties),\n metering,\n lane_code,\n direction,\n captured_at: new Date(captured_at),\n is_sphere: image_type === '360'\n };\n\n cache.points.set(key, data);\n\n return {\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data\n };\n });\n\n _vegbilderCache.rtree.load(features);\n dispatch.call('loadedImages');\n}\n\nfunction orderSequences(projection, cache) {\n const {points} = cache;\n\n const grouped = Array.from(points.values()).reduce((grouped, image) => {\n const key = image.road_reference;\n if (grouped.has(key)) {\n grouped.get(key).push(image);\n } else {\n grouped.set(key, [image]);\n }\n return grouped;\n }, new Map());\n\n const imageSequences = Array.from(grouped.values()).reduce((imageSequences, imageGroup) => {\n imageGroup.sort((a, b) => {\n if (a.captured_at.valueOf() > b.captured_at.valueOf()) {\n return 1;\n } else if (a.captured_at.valueOf() < b.captured_at.valueOf()) {\n return -1;\n } else {\n const {direction} = a;\n if (direction === directionEnum.forward) {\n return a.metering - b.metering;\n } else {\n return b.metering - a.metering;\n }\n }\n });\n let imageSequence = [imageGroup[0]];\n let angle = null;\n for (const [lastImage, image] of d3_pairs(imageGroup)) {\n if (lastImage.ca === null) {\n const b = projection(lastImage.loc);\n const a = projection(image.loc);\n if (!geoVecEqual(a, b)) {\n angle = geoVecAngle(a, b);\n angle *= (180 / Math.PI);\n angle -= 90;\n angle = angle >= 0 ? angle : angle + 360;\n }\n lastImage.ca = angle;\n }\n if (\n image.direction === lastImage.direction &&\n image.captured_at.valueOf() - lastImage.captured_at.valueOf() <= 20000\n ) {\n imageSequence.push(image);\n } else {\n imageSequences.push(imageSequence);\n imageSequence = [image];\n }\n }\n imageSequences.push(imageSequence);\n return imageSequences;\n }, []);\n\n cache.sequences = imageSequences.map(images => {\n const sequence = {\n images,\n key: images[0].key,\n geometry : {\n type : 'LineString',\n coordinates : images.map(image => image.loc)\n }\n };\n for (const image of images) {\n _vegbilderCache.image2sequence_map.set(image.key, sequence);\n }\n return sequence;\n });\n}\n\nfunction roadReference(properties) {\n const {\n FYLKENUMMER: county_number,\n VEGKATEGORI: road_class,\n VEGSTATUS: road_status,\n VEGNUMMER: road_number,\n STREKNING: section,\n DELSTREKNING: subsection,\n HP: parcel,\n KRYSSDEL: junction_part,\n SIDEANLEGGSDEL: services_part,\n ANKERPUNKT: anker_point,\n AAR: year,\n } = properties;\n\n let reference;\n\n if (year >= 2020) {\n reference = `${road_class}${road_status}${road_number} S${section}D${subsection}`;\n if (junction_part) {\n reference = `${reference} M${anker_point} KD${junction_part}`;\n } else if (services_part) {\n reference = `${reference} M${anker_point} SD${services_part}`;\n }\n } else {\n reference = `${county_number}${road_class}${road_status}${road_number} HP${parcel}`;\n }\n\n return reference;\n}\n\nexport default {\n\n init: function () {\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset: async function () {\n if (_vegbilderCache) {\n for (const layer of _vegbilderCache.wfslayers.values()) {\n for (const controller of layer.inflight.values()) {\n controller.abort();\n }\n }\n }\n\n _vegbilderCache = {\n wfslayers: new Map(),\n rtree: new RBush(),\n image2sequence_map: new Map()\n };\n\n const availableLayers = await fetchAvailableLayers();\n const {wfslayers} = _vegbilderCache;\n\n for (const layerInfo of availableLayers) {\n const cache = {\n layerInfo,\n loaded: new Map(),\n inflight: new Map(),\n points: new Map(),\n sequences: []\n };\n wfslayers.set(layerInfo.name, cache);\n }\n },\n\n images: function (projection) {\n const limit = 5;\n return searchLimited(limit, projection, _vegbilderCache.rtree);\n },\n\n\n sequences: function (projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const seen = new Set();\n const line_strings = [];\n\n for (const {data} of _vegbilderCache.rtree.search(bbox)) {\n const sequence = _vegbilderCache.image2sequence_map.get(data.key);\n if (!sequence) continue;\n const {key, geometry, images} = sequence;\n if (seen.has(key)) continue;\n seen.add(key);\n const line = {\n type: 'LineString',\n coordinates: geometry.coordinates,\n key,\n images\n };\n line_strings.push(line);\n }\n return line_strings;\n },\n\n cachedImage: function (key) {\n for (const {points} of _vegbilderCache.wfslayers.values()) {\n if (points.has(key)) return points.get(key);\n }\n },\n\n getSequenceForImage: function (image) {\n return _vegbilderCache?.image2sequence_map.get(image?.key);\n },\n\n loadImages: async function (context, margin) {\n if (!_vegbilderCache) {\n await this.reset();\n }\n margin ??= 1;\n const wfslayers = filterAvailableLayers(context.photos());\n loadWFSLayers(context.projection, margin, wfslayers);\n },\n\n photoFrame: function() {\n return _currentFrame;\n },\n\n ensureViewerLoaded: function(context) {\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n const step = (stepBy) => () => {\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n if (!selected) return;\n\n const sequence = this.getSequenceForImage(selected);\n const nextIndex = sequence.images.indexOf(selected) + stepBy;\n const nextImage = sequence.images[nextIndex];\n\n if (!nextImage) return;\n\n context.map().centerEase(nextImage.loc);\n this.selectImage(context, nextImage.key, true);\n };\n\n const wrap = context.container().select('.photoviewer')\n .selectAll('.vegbilder-wrapper')\n .data([0]);\n\n const wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper vegbilder-wrapper')\n .classed('hide', true);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n const controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls');\n\n controlsEnter\n .append('button')\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .on('click.forward', step(1))\n .text('\u25BA');\n\n _loadViewerPromise = Promise.all([\n pannellumPhotoFrame(context, wrapEnter),\n planePhotoFrame(context, wrapEnter)\n ]).then(([pannellumPhotoFrame, planePhotoFrame]) => {\n _pannellumFrame = pannellumPhotoFrame;\n _pannellumFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n _planeFrame = planePhotoFrame;\n _planeFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n });\n\n return _loadViewerPromise;\n },\n\n selectImage: function(context, key, keepOrientation) {\n const d = this.cachedImage(key);\n this.updateUrlImage(key);\n\n const viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) { viewer.datum(d); }\n\n this.setStyles(context, null, true);\n\n if (!d) return this;\n\n const wrap = context.container().select('.photoviewer .vegbilder-wrapper');\n const attribution = wrap.selectAll('.photo-attribution').text('');\n\n if (d.captured_at) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeTimestamp(d.captured_at));\n }\n\n attribution\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://vegvesen.no')\n .call(t.append('vegbilder.publisher'));\n\n attribution\n .append('a')\n .attr('target', '_blank')\n .attr('href', `https://vegbilder.atlas.vegvesen.no/?year=${d.captured_at.getFullYear()}&lat=${d.loc[1]}&lng=${d.loc[0]}&view=image&imageId=${d.key}`)\n .call(t.append('vegbilder.view_on'));\n\n _currentFrame = d.is_sphere? _pannellumFrame : _planeFrame;\n\n _currentFrame\n .showPhotoFrame(wrap)\n .selectPhoto(d, keepOrientation);\n\n return this;\n },\n\n showViewer: function (context) {\n const viewer = context.container().select('.photoviewer');\n const isHidden = viewer.selectAll('.photo-wrapper.vegbilder-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n viewer\n .classed('hide', false)\n .selectAll('.photo-wrapper.vegbilder-wrapper')\n .classed('hide', false);\n }\n return this;\n },\n\n hideViewer: function(context) {\n this.updateUrlImage(null);\n\n const viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence')\n .classed('currentView', false);\n\n return this.setStyles(context, null, true);\n },\n\n\n // Updates the currently highlighted sequence and selected bubble.\n // Reset is only necessary when interacting with the viewport because\n // this implicitly changes the currently selected bubble/sequence\n setStyles: function (context, hovered, reset) {\n if (reset) { // reset all layers\n context.container().selectAll('.viewfield-group')\n .classed('highlighted', false)\n .classed('hovered', false)\n .classed('currentView', false);\n\n context.container().selectAll('.sequence')\n .classed('highlighted', false)\n .classed('currentView', false);\n }\n\n const hoveredImageKey = hovered?.key;\n const hoveredSequence = this.getSequenceForImage(hovered);\n const hoveredSequenceKey = hoveredSequence?.key;\n const hoveredImageKeys = hoveredSequence?.images.map(d => d.key) ?? [];\n\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n const selectedImageKey = selected?.key;\n const selectedSequence = this.getSequenceForImage(selected);\n const selectedSequenceKey = selectedSequence?.key;\n const selectedImageKeys = selectedSequence?.images.map(d => d.key) ?? [];\n\n // highlight sibling viewfields on either the selected or the hovered sequences\n const highlightedImageKeys = utilArrayUnion(hoveredImageKeys, selectedImageKeys);\n\n context.container().selectAll('.layer-vegbilder .viewfield-group')\n .classed('highlighted', d => highlightedImageKeys.indexOf(d.key) !== -1)\n .classed('hovered', d => d.key === hoveredImageKey)\n .classed('currentView', d => d.key === selectedImageKey);\n\n context.container().selectAll('.layer-vegbilder .sequence')\n .classed('highlighted', d => d.key === hoveredSequenceKey)\n .classed('currentView', d => d.key === selectedSequenceKey);\n\n // update viewfields if needed\n context.container().selectAll('.layer-vegbilder .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n const d = this.parentNode.__data__;\n if (d.is_sphere && d.key !== selectedImageKey) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n return this;\n },\n\n updateUrlImage: function (key) {\n const hash = utilStringQs(window.location.hash);\n if (key) {\n hash.photo = 'vegbilder/' + key;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n validHere: function(extent) {\n const bbox = Object.values(extent.bbox());\n return iso1A2Codes(bbox).includes('NO');\n },\n\n\n cache: function () {\n return _vegbilderCache;\n }\n\n};\n", "\n/**\n * osmAuth\n * Easy authentication with OpenStreetMap over OAuth 2.0.\n * @module\n *\n * @param o `Object` containing options:\n * @param o.scope OAuth2 scopes requested (e.g. \"read_prefs write_api\")\n * @param o.client_id OAuth2 client ID\n * @param o.redirect_uri OAuth2 redirect URI (e.g. \"http://127.0.0.1:8080/land.html\")\n * @param o.access_token Can pre-authorize with an OAuth2 bearer token if you have one\n * @param o.apiUrl A base url for the OSM API (default: \"https://api.openstreetmap.org\")\n * @param o.url A base url for the OAuth2 handshake (default: \"https://www.openstreetmap.org\")\n * @param o.auto If `true`, attempt to authenticate automatically when calling `.xhr()` or `.fetch()` (default: `false`)\n * @param o.singlepage If `true`, use page redirection instead of a popup (default: `false`)\n * @param o.loading Function called when auth-related xhr calls start\n * @param o.done Function called when auth-related xhr calls end\n * @param o.locale The locale to use on the OAuth2 authentication page. Optional.\n * @return `self`\n */\nexport function osmAuth(o) {\n var oauth = {};\n\n var CHANNEL_ID = 'osm-api-auth-complete';\n\n // Mock localStorage if needed.\n // Note that accessing localStorage may throw a `SecurityError`, so wrap in a try/catch.\n var _store = null;\n try {\n if (!('localStorage' in globalThis)) {\n throw new Error('No localStorage');\n }\n _store = globalThis.localStorage;\n\n } catch (e) {\n var _mock = new Map();\n _store = {\n isMocked: true,\n hasItem: (k) => _mock.has(k),\n getItem: (k) => _mock.get(k),\n setItem: (k, v) => _mock.set(k, v),\n removeItem: (k) => _mock.delete(k),\n clear: () => _mock.clear()\n };\n }\n\n /**\n * token\n * Get/Set tokens. These are prefixed with the base URL so that `osm-auth`\n * can be used with multiple APIs and the keys in `localStorage` will not clash\n * @param {string} k key\n * @param {string?} v value\n * @return {string?} If getting, returns the stored value or `null`. If setting, returns `undefined`.\n */\n function token(k, v) {\n var key = o.url + k;\n if (arguments.length === 1) {\n var val = _store.getItem(key) || '';\n // Note: legacy tokens might be wrapped in double quotes - remove them, see #129\n return val.replace(/\"/g, '');\n\n } else if (arguments.length === 2) {\n if (v) {\n return _store.setItem(key, v);\n } else {\n return _store.removeItem(key);\n }\n }\n }\n\n\n /**\n * authenticated\n * Test whether the user is currently authenticated\n *\n * @return {boolean} `true` if authenticated, `false` if not\n */\n oauth.authenticated = function() {\n return !!token('oauth2_access_token');\n };\n\n\n /**\n * logout\n * Removes any stored authentication tokens (legacy OAuth1 tokens too)\n *\n * @return `self`\n */\n oauth.logout = function () {\n token('oauth2_access_token', ''); // OAuth2\n token('oauth_token', ''); // OAuth1\n token('oauth_token_secret', ''); // OAuth1\n token('oauth_request_token_secret', ''); // OAuth1\n return oauth;\n };\n\n\n /**\n * authenticate\n * First logs out, then runs the authentiation flow, finally calls the callback.\n * TODO: detect lack of click event (probably can settimeout it)\n *\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @param {LoginOptions} [options] Other options\n * @return none\n */\n oauth.authenticate = function(callback, options) {\n if (oauth.authenticated()) {\n callback(null, oauth);\n return;\n }\n\n oauth.logout();\n\n _preopenPopup(function(error, popup) {\n if (error) {\n callback(error);\n } else {\n _generatePkceChallenge(function(pkce) {\n _authenticate(pkce, options, popup, callback);\n });\n }\n });\n };\n\n\n /**\n * authenticateAsync\n * Promisified version of `authenticate`\n * @param {LoginOptions} [options]\n * @return {Promise} Promise settled with whatever `_authenticate` did\n */\n oauth.authenticateAsync = function(options) {\n if (oauth.authenticated()) {\n return Promise.resolve(oauth);\n }\n\n oauth.logout();\n\n return new Promise((resolve, reject) => {\n var errback = (err, result) => {\n if (err) {\n reject(err);\n } else {\n resolve(result);\n }\n };\n\n _preopenPopup((error, popup) => {\n if (error) {\n errback(error);\n } else {\n _generatePkceChallenge(pkce => _authenticate(pkce, options, popup, errback));\n }\n });\n });\n };\n\n\n /**\n * opens an empty popup to be later used for the authentication page\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @return none\n */\n function _preopenPopup(callback) {\n if (o.singlepage) {\n callback(null, undefined);\n return;\n }\n\n // Create a 550x610 popup window in the center of the screen\n var w = 550;\n var h = 610;\n var settings = [\n ['width', w],\n ['height', h],\n ['left', window.screen.width / 2 - w / 2],\n ['top', window.screen.height / 2 - h / 2],\n ]\n .map(function (x) { return x.join('='); })\n .join(',');\n var popup = window.open('about:blank', 'oauth_window', settings);\n if (popup) {\n callback(null, popup);\n } else {\n var error = new Error('Popup was blocked');\n error.status = 'popup-blocked';\n callback(error);\n }\n }\n\n\n /**\n * _authenticate\n * internal authenticate\n *\n * @typedef {{ switchUser?: boolean }} LoginOptions\n *\n * @param {Object} pkce Object containing PKCE code challenge properties\n * @param {LoginOptions=} options Other options\n * @param {Window} popup Popup Window to use for the authentication page, should be undefined when using singlepage mode\n * @param {function} callback Errback-style callback that accepts `(err, result)`\n */\n function _authenticate(pkce, options, popup, callback) {\n var state = generateState();\n\n // ## Request authorization to access resources from the user\n // and receive authorization code\n var path =\n '/oauth2/authorize?' +\n utilQsString({\n client_id: o.client_id,\n redirect_uri: o.redirect_uri,\n response_type: 'code',\n scope: o.scope,\n state: state,\n code_challenge: pkce.code_challenge,\n code_challenge_method: pkce.code_challenge_method,\n locale: o.locale || '',\n });\n\n var url = options?.switchUser\n ? `${o.url}/logout?referer=${encodeURIComponent(`/login?referer=${encodeURIComponent(path)}`)}`\n : o.url + path;\n\n if (o.singlepage) {\n if (_store.isMocked) {\n // in singlepage mode, PKCE requires working non-volatile storage\n var error = new Error('localStorage unavailable, but required in singlepage mode');\n error.status = 'pkce-localstorage-unavailable';\n callback(error);\n return;\n }\n var params = utilStringQs(window.location.search.slice(1));\n if (params.code) {\n oauth.bootstrapToken(params.code, callback);\n } else {\n // save OAuth2 state and PKCE challenge in local storage, for later use\n // in the `/oauth/token` request\n token('oauth2_state', state);\n token('oauth2_pkce_code_verifier', pkce.code_verifier);\n window.location = url;\n }\n } else {\n oauth.popupWindow = popup;\n popup.location = url;\n }\n\n // Called by a function in the redirect URL page, in the popup window. The\n // window closes itself.\n var bc = new BroadcastChannel(CHANNEL_ID);\n bc.addEventListener('message', (event) => {\n var url = event.data;\n var params = utilStringQs(url.split('?')[1]);\n if (params.state !== state) {\n var error = new Error('Invalid state');\n error.status = 'invalid-state';\n callback(error);\n return;\n } else if (params.error !== undefined) {\n var err = new Error(params.error_description.replace(/\\+/g, ' '));\n err.status = params.error;\n callback(err);\n return;\n }\n _getAccessToken(params.code, pkce.code_verifier, accessTokenDone);\n bc.close();\n });\n\n function accessTokenDone(err, xhr) {\n o.done();\n if (err) {\n callback(err);\n return;\n }\n var access_token = JSON.parse(xhr.response);\n token('oauth2_access_token', access_token.access_token);\n callback(null, oauth);\n }\n }\n\n\n /**\n * _getAccessToken\n * The client requests an access token by authenticating with the\n * authorization server and presenting the `auth_code`, brought\n * in from a function call on a landing page popup.\n * @param {string} auth_code\n * @param {string} code_verifier\n * @param {function} accessTokenDone Errback-style callback `(err, result)`, called when complete\n */\n function _getAccessToken(auth_code, code_verifier, accessTokenDone) {\n var url =\n o.url +\n '/oauth2/token?' +\n utilQsString({\n client_id: o.client_id,\n redirect_uri: o.redirect_uri,\n grant_type: 'authorization_code',\n code: auth_code,\n code_verifier: code_verifier\n });\n\n // The authorization server authenticates the client and validates\n // the authorization grant, and if valid, issues an access token.\n oauth.rawxhr('POST', url, null, null, null, accessTokenDone);\n o.loading();\n }\n\n\n /**\n * bringPopupWindowToFront\n * Tries to bring an existing authentication popup to the front.\n *\n * @return {boolean} `true` if it succeeded, `false` if not\n */\n oauth.bringPopupWindowToFront = function() {\n var broughtPopupToFront = false;\n try {\n // This may cause a cross-origin error:\n // `DOMException: Blocked a frame with origin \"...\" from accessing a cross-origin frame.`\n if (oauth.popupWindow && !oauth.popupWindow.closed) {\n oauth.popupWindow.focus();\n broughtPopupToFront = true;\n }\n } catch (err) {\n // Bringing popup window to front failed (probably because of the cross-origin error mentioned above)\n }\n return broughtPopupToFront;\n };\n\n\n /**\n * bootstrapToken\n * The authorization code is a temporary code that a client can exchange for an access token.\n * If using this library in single-page mode, you'll need to call this once your application\n * has an `auth_code` and wants to get an access_token.\n *\n * @param {string} auth_code The OAuth2 `auth_code`\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @return none\n */\n oauth.bootstrapToken = function(auth_code, callback) {\n var state = token('oauth2_state');\n token('oauth2_state', '');\n var params = utilStringQs(window.location.search.slice(1));\n if (params.state !== state) {\n var error = new Error('Invalid state');\n error.status = 'invalid-state';\n callback(error);\n return;\n }\n var code_verifier = token('oauth2_pkce_code_verifier');\n token('oauth2_pkce_code_verifier', '');\n _getAccessToken(auth_code, code_verifier, accessTokenDone);\n\n function accessTokenDone(err, xhr) {\n o.done();\n if (err) {\n callback(err);\n return;\n }\n var access_token = JSON.parse(xhr.response);\n token('oauth2_access_token', access_token.access_token);\n callback(null, oauth);\n }\n };\n\n\n /**\n * fetch\n * A `fetch` wrapper that includes the Authorization header if the user is authenticated.\n * https://developer.mozilla.org/en-US/docs/Web/API/fetch\n *\n * @param {string} resource Resource passed to `fetch`\n * @param {Object} options Options passed to `fetch`\n * @return {Promise} Promise that wraps `authenticateAsync` then `fetch`\n */\n oauth.fetch = function(resource, options) {\n if (oauth.authenticated()) {\n return _doFetch();\n } else {\n if (o.auto) {\n return oauth.authenticateAsync().then(_doFetch);\n } else {\n return Promise.reject(new Error('not authenticated'));\n }\n }\n\n function _doFetch() {\n options = options || {};\n if (!options.headers) {\n options.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };\n }\n options.headers.Authorization = 'Bearer ' + token('oauth2_access_token');\n return fetch(resource, options);\n }\n };\n\n\n /**\n * xhr\n * A `XMLHttpRequest` wrapper that does authenticated calls if the user has logged in.\n * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest\n *\n * @param {Object} options\n * @param options.method Passed to `xhr.open` (e.g. 'GET', 'POST')\n * @param options.prefix If `true` path contains a path, if `false` path contains the full url\n * @param options.path The URL path (e.g. \"/api/0.6/user/details\") (or full url, if `prefix`=`false`)\n * @param options.content Passed to `xhr.send`\n * @param options.headers `Object` containing request headers\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @return {XMLHttpRequest} `XMLHttpRequest` if authenticated, otherwise `null`\n */\n oauth.xhr = function (options, callback) {\n if (oauth.authenticated()) {\n return _doXHR();\n } else {\n if (o.auto) {\n oauth.authenticate(_doXHR);\n return;\n } else {\n callback('not authenticated', null);\n return;\n }\n }\n\n function _doXHR() {\n var url = options.prefix !== false ? (o.apiUrl + options.path) : options.path;\n return oauth.rawxhr(\n options.method,\n url,\n token('oauth2_access_token'),\n options.content,\n options.headers,\n done\n );\n }\n\n function done(err, xhr) {\n if (err) {\n callback(err);\n } else if (xhr.responseXML) {\n callback(err, xhr.responseXML);\n } else {\n callback(err, xhr.response);\n }\n }\n };\n\n\n /**\n * rawxhr\n * Creates the XMLHttpRequest set up with a header and response handling\n * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest\n *\n * @param method Passed to `xhr.open` (e.g. 'GET', 'POST')\n * @param url Passed to `xhr.open`\n * @param access_token OAuth2 bearer token\n * @param data Passed to `xhr.send`\n * @param headers `Object` containing request headers\n * @param callback An \"errback\"-style callback (`err`, `result`), called when complete\n * @return `XMLHttpRequest`\n */\n oauth.rawxhr = function(method, url, access_token, data, headers, callback) {\n headers = headers || { 'Content-Type': 'application/x-www-form-urlencoded' };\n\n if (access_token) {\n headers.Authorization = 'Bearer ' + access_token;\n }\n\n var xhr = new XMLHttpRequest();\n\n xhr.onreadystatechange = function () {\n if (4 === xhr.readyState && 0 !== xhr.status) {\n if (/^20\\d$/.test(xhr.status)) { // a 20x status code - OK\n callback(null, xhr);\n } else {\n callback(xhr, null);\n }\n }\n };\n xhr.onerror = function (e) {\n callback(e, null);\n };\n\n xhr.open(method, url, true);\n for (var h in headers) xhr.setRequestHeader(h, headers[h]);\n\n xhr.send(data);\n return xhr;\n };\n\n\n /**\n * preauth\n * Pre-authorize this object, if we already have access token from the start\n *\n * @param {Object} val Object containing `access_token` property\n * @return `self`\n */\n oauth.preauth = function(val) {\n if (val && val.access_token) {\n token('oauth2_access_token', val.access_token);\n }\n return oauth;\n };\n\n\n /**\n * options (getter / setter)\n * If passed with no arguments, just return the options\n * If passed an Object, set the options then attempt to pre-authorize\n *\n * @param val? Object containing options\n * @return current `options` (if getting), or `self` (if setting)\n */\n oauth.options = function(val) {\n if (!arguments.length) return o;\n\n o = val;\n o.apiUrl = o.apiUrl || 'https://api.openstreetmap.org';\n o.url = o.url || 'https://www.openstreetmap.org';\n o.auto = o.auto || false;\n o.singlepage = o.singlepage || false;\n\n // Optional loading and loading-done functions for nice UI feedback.\n // by default, no-ops\n o.loading = o.loading || function () {};\n o.done = o.done || function () {};\n return oauth.preauth(o);\n };\n\n\n // Everything below here is initialization/setup code\n // Handle options and attempt to pre-authorize\n oauth.options(o);\n\n return oauth;\n}\n\n\n/**\n * utilQsString\n * Transforms object of `key=value` pairs into query string\n * @param {Object} Object of `key=value` pairs\n * @returns {string} query string\n */\nfunction utilQsString(obj) {\n return Object.keys(obj)\n .filter(function(key) {\n return obj[key] !== undefined;\n })\n .sort()\n .map(function(key) {\n return (encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));\n })\n .join('&');\n}\n\n/**\n * utilStringQs\n * Transforms query string into object of `key=value` pairs\n * @param {string} query string\n * @returns {Object} Object of `key=value` pairs\n */\nfunction utilStringQs(str) {\n var i = 0; // advance past any leading '?' or '#' characters\n while (i < str.length && (str[i] === '?' || str[i] === '#')) i++;\n str = str.slice(i);\n\n return str.split('&').reduce(function(obj, pair) {\n var parts = pair.split('=');\n if (parts.length === 2) {\n obj[parts[0]] = decodeURIComponent(parts[1]);\n }\n return obj;\n }, {});\n}\n\n\n/**\n * Generates a challenge/verifier pair for PKCE.\n * If the browser does not support the WebCryptoAPI, the \"plain\" method is\n * used as a fallback instead of a SHA-256 hash.\n * @param {callback} callback called with the result of the generated PKCE challenge\n */\nfunction _generatePkceChallenge(callback) {\n var code_verifier;\n // generate a random code_verifier\n // https://datatracker.ietf.org/doc/html/rfc7636#section-7.1\n var random = globalThis.crypto.getRandomValues(new Uint8Array(32));\n code_verifier = base64(random.buffer);\n var verifier = Uint8Array.from(Array.from(code_verifier).map(function(char) {\n return char.charCodeAt(0);\n }));\n\n // generate challenge for code verifier\n globalThis.crypto.subtle.digest('SHA-256', verifier).then(function(hash) {\n var code_challenge = base64(hash);\n\n callback({\n code_challenge: code_challenge,\n code_verifier: code_verifier,\n code_challenge_method: 'S256'\n });\n });\n}\n\n\n/**\n * Returns a random state to be used as the \"state\" of the OAuth2 authentication\n * See https://datatracker.ietf.org/doc/html/rfc6749#section-10.12\n */\nfunction generateState() {\n var state;\n var random = globalThis.crypto.getRandomValues(new Uint8Array(32));\n state = base64(random.buffer);\n\n return state;\n}\n\n\n/**\n * base64\n * Converts binary buffer to base64 encoded string, as used in rfc7636\n * @param {ArrayBuffer} buffer\n * @returns {string} base64 encoded\n */\nfunction base64(buffer) {\n return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))\n .replace(/\\//g, '_')\n .replace(/\\+/g, '-')\n .replace(/[=]/g, '');\n}\n", "export var JXON = new (function () {\n var\n sValueProp = 'keyValue', sAttributesProp = 'keyAttributes', sAttrPref = '@', /* you can customize these values */\n aCache = [], rIsNull = /^\\s*$/, rIsBool = /^(?:true|false)$/i;\n\n function parseText (sValue) {\n if (rIsNull.test(sValue)) { return null; }\n if (rIsBool.test(sValue)) { return sValue.toLowerCase() === 'true'; }\n if (isFinite(sValue)) { return parseFloat(sValue); }\n if (isFinite(Date.parse(sValue))) { return new Date(sValue); }\n return sValue;\n }\n\n function EmptyTree () { }\n EmptyTree.prototype.toString = function () { return 'null'; };\n EmptyTree.prototype.valueOf = function () { return null; };\n\n function objectify (vValue) {\n return vValue === null ? new EmptyTree() : vValue instanceof Object ? vValue : new vValue.constructor(vValue);\n }\n\n function createObjTree (oParentNode, nVerb, bFreeze, bNesteAttr) {\n var\n nLevelStart = aCache.length, bChildren = oParentNode.hasChildNodes(),\n bAttributes = oParentNode.hasAttributes(), bHighVerb = Boolean(nVerb & 2);\n\n var\n sProp, vContent, nLength = 0, sCollectedTxt = '',\n vResult = bHighVerb ? {} : /* put here the default value for empty nodes: */ true;\n\n if (bChildren) {\n for (var oNode, nItem = 0; nItem < oParentNode.childNodes.length; nItem++) {\n oNode = oParentNode.childNodes.item(nItem);\n if (oNode.nodeType === 4) {\n /* nodeType is 'CDATASection' (4) */\n sCollectedTxt += oNode.nodeValue;\n } else if (oNode.nodeType === 3) {\n /* nodeType is 'Text' (3) */\n sCollectedTxt += oNode.nodeValue.trim();\n } else if (oNode.nodeType === 1 && !oNode.prefix) {\n /* nodeType is 'Element' (1) */\n aCache.push(oNode);\n }\n }\n }\n\n var nLevelEnd = aCache.length, vBuiltVal = parseText(sCollectedTxt);\n\n if (!bHighVerb && (bChildren || bAttributes)) { vResult = nVerb === 0 ? objectify(vBuiltVal) : {}; }\n\n for (var nElId = nLevelStart; nElId < nLevelEnd; nElId++) {\n sProp = aCache[nElId].nodeName.toLowerCase();\n vContent = createObjTree(aCache[nElId], nVerb, bFreeze, bNesteAttr);\n if (vResult.hasOwnProperty(sProp)) {\n if (vResult[sProp].constructor !== Array) { vResult[sProp] = [vResult[sProp]]; }\n vResult[sProp].push(vContent);\n } else {\n vResult[sProp] = vContent;\n nLength++;\n }\n }\n\n if (bAttributes) {\n var\n nAttrLen = oParentNode.attributes.length,\n sAPrefix = bNesteAttr ? '' : sAttrPref, oAttrParent = bNesteAttr ? {} : vResult;\n\n for (var oAttrib, nAttrib = 0; nAttrib < nAttrLen; nLength++, nAttrib++) {\n oAttrib = oParentNode.attributes.item(nAttrib);\n oAttrParent[sAPrefix + oAttrib.name.toLowerCase()] = parseText(oAttrib.value.trim());\n }\n\n if (bNesteAttr) {\n if (bFreeze) { Object.freeze(oAttrParent); }\n vResult[sAttributesProp] = oAttrParent;\n nLength -= nAttrLen - 1;\n }\n }\n\n if (nVerb === 3 || (nVerb === 2 || nVerb === 1 && nLength > 0) && sCollectedTxt) {\n vResult[sValueProp] = vBuiltVal;\n } else if (!bHighVerb && nLength === 0 && sCollectedTxt) {\n vResult = vBuiltVal;\n }\n\n if (bFreeze && (bHighVerb || nLength > 0)) { Object.freeze(vResult); }\n\n aCache.length = nLevelStart;\n\n return vResult;\n }\n\n function loadObjTree (oXMLDoc, oParentEl, oParentObj) {\n var vValue, oChild;\n\n if (oParentObj instanceof String || oParentObj instanceof Number || oParentObj instanceof Boolean) {\n oParentEl.appendChild(oXMLDoc.createTextNode(oParentObj.toString())); /* verbosity level is 0 */\n } else if (oParentObj.constructor === Date) {\n oParentEl.appendChild(oXMLDoc.createTextNode(oParentObj.toGMTString()));\n }\n\n for (var sName in oParentObj) {\n vValue = oParentObj[sName];\n if (isFinite(sName) || vValue instanceof Function) { continue; } /* verbosity level is 0 */\n if (sName === sValueProp) {\n if (vValue !== null && vValue !== true) { oParentEl.appendChild(oXMLDoc.createTextNode(vValue.constructor === Date ? vValue.toGMTString() : String(vValue))); }\n } else if (sName === sAttributesProp) { /* verbosity level is 3 */\n for (var sAttrib in vValue) { oParentEl.setAttribute(sAttrib, vValue[sAttrib]); }\n } else if (sName.charAt(0) === sAttrPref) {\n oParentEl.setAttribute(sName.slice(1), vValue);\n } else if (vValue.constructor === Array) {\n for (var nItem = 0; nItem < vValue.length; nItem++) {\n oChild = oXMLDoc.createElementNS(null, sName);\n loadObjTree(oXMLDoc, oChild, vValue[nItem]);\n oParentEl.appendChild(oChild);\n }\n } else {\n oChild = oXMLDoc.createElementNS(null, sName);\n if (vValue instanceof Object) {\n loadObjTree(oXMLDoc, oChild, vValue);\n } else if (vValue !== null && vValue !== true) {\n oChild.appendChild(oXMLDoc.createTextNode(vValue.toString()));\n }\n oParentEl.appendChild(oChild);\n }\n }\n }\n\n this.build = function (oXMLParent, nVerbosity /* optional */, bFreeze /* optional */, bNesteAttributes /* optional */) {\n var _nVerb = arguments.length > 1 && typeof nVerbosity === 'number' ? nVerbosity & 3 : /* put here the default verbosity level: */ 1;\n return createObjTree(oXMLParent, _nVerb, bFreeze || false, arguments.length > 3 ? bNesteAttributes : _nVerb === 3);\n };\n\n this.unbuild = function (oObjTree) {\n var oNewDoc = document.implementation.createDocument('', '', null);\n loadObjTree(oNewDoc, oNewDoc, oObjTree);\n return oNewDoc;\n };\n\n this.stringify = function (oObjTree) {\n return (new XMLSerializer()).serializeToString(JXON.unbuild(oObjTree));\n };\n})();\n\n// var myObject = JXON.build(doc);\n// we got our javascript object! try: alert(JSON.stringify(myObject));\n\n// var newDoc = JXON.unbuild(myObject);\n// we got our Document instance! try: alert((new XMLSerializer()).serializeToString(newDoc));\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { json as d3_json } from 'd3-fetch';\nimport { osmAuth } from 'osm-auth';\nimport RBush from 'rbush';\n\nimport { JXON } from '../util/jxon';\nimport { geoExtent, geoRawMercator, geoVecAdd, geoZoomToScale } from '../geo';\nimport { osmEntity, osmNode, osmNote, osmRelation, osmWay } from '../osm';\nimport { utilArrayChunk, utilArrayGroupBy, utilArrayUniq, utilObjectOmit, utilRebind, utilTiler, utilQsString } from '../util';\nimport { localizer } from '../core/localizer.js';\nimport { utilGzip } from '../util/util';\nimport { osmApiConnections } from '../../config/id.js';\n\n\nvar tiler = utilTiler();\nvar dispatch = d3_dispatch('apiStatusChange', 'authLoading', 'authDone', 'change', 'loading', 'loaded', 'loadedNotes');\n\nvar urlroot = osmApiConnections[0].url;\nvar apiUrlroot = osmApiConnections[0].apiUrl || urlroot;\nvar redirectPath = window.location.origin + window.location.pathname;\nvar oauth = new osmAuth({\n url: urlroot,\n apiUrl: apiUrlroot,\n client_id: osmApiConnections[0].client_id,\n scope: 'read_prefs write_prefs write_api read_gpx write_notes',\n redirect_uri: redirectPath + 'land.html',\n loading: authLoading,\n done: authDone\n});\nvar _apiConnections = osmApiConnections;\n\n// hardcode default block of Google Maps\nvar _imageryBlocklists = [/.*\\.google(apis)?\\..*\\/(vt|kh)[\\?\\/].*([xyz]=.*){3}.*/];\nvar _tileCache = { toLoad: {}, loaded: {}, inflight: {}, seen: {}, rtree: new RBush() };\nvar _noteCache = { toLoad: {}, loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: new RBush() };\nvar _userCache = { toLoad: {}, user: {} };\nvar _cachedApiStatus;\nvar _changeset = {};\n\nvar _deferred = new Set();\nvar _connectionID = 1;\nvar _tileZoom = 16;\nvar _noteZoom = 12;\nvar _rateLimitError;\nvar _userChangesets;\nvar _userDetails;\nvar _off;\n\n// set a default but also load this from the API status\nvar _maxWayNodes = 2000;\nlet _maxChangesetElements = 10_000;\n\n\nfunction authLoading() {\n dispatch.call('authLoading');\n}\n\n\nfunction authDone() {\n dispatch.call('authDone');\n}\n\n\nfunction abortRequest(controllerOrXHR) {\n if (controllerOrXHR) {\n controllerOrXHR.abort();\n }\n}\n\n\nfunction hasInflightRequests(cache) {\n return Object.keys(cache.inflight).length;\n}\n\n\nfunction abortUnwantedRequests(cache, visibleTiles) {\n Object.keys(cache.inflight).forEach(function(k) {\n if (cache.toLoad[k]) return;\n if (visibleTiles.find(function(tile) { return k === tile.id; })) return;\n\n abortRequest(cache.inflight[k]);\n delete cache.inflight[k];\n });\n}\n\nfunction getNodesJSON(obj) {\n var elems = obj.nodes;\n var nodes = new Array(elems.length);\n for (var i = 0, l = elems.length; i < l; i++) {\n nodes[i] = 'n' + elems[i];\n }\n return nodes;\n}\n\nfunction getMembersJSON(obj) {\n var elems = obj.members;\n var members = new Array(elems.length);\n for (var i = 0, l = elems.length; i < l; i++) {\n var attrs = elems[i];\n members[i] = {\n id: attrs.type[0] + attrs.ref,\n type: attrs.type,\n role: attrs.role\n };\n }\n return members;\n}\n\nfunction encodeNoteRtree(note) {\n return {\n minX: note.loc[0],\n minY: note.loc[1],\n maxX: note.loc[0],\n maxY: note.loc[1],\n data: note\n };\n}\n\n\nvar jsonparsers = {\n\n node: function nodeData(obj, uid) {\n return new osmNode({\n id: uid,\n visible: typeof obj.visible === 'boolean' ? obj.visible : true,\n version: obj.version && obj.version.toString(),\n changeset: obj.changeset && obj.changeset.toString(),\n timestamp: obj.timestamp,\n user: obj.user,\n uid: obj.uid && obj.uid.toString(),\n loc: [Number(obj.lon), Number(obj.lat)],\n tags: obj.tags\n });\n },\n\n way: function wayData(obj, uid) {\n return new osmWay({\n id: uid,\n visible: typeof obj.visible === 'boolean' ? obj.visible : true,\n version: obj.version && obj.version.toString(),\n changeset: obj.changeset && obj.changeset.toString(),\n timestamp: obj.timestamp,\n user: obj.user,\n uid: obj.uid && obj.uid.toString(),\n tags: obj.tags,\n nodes: getNodesJSON(obj)\n });\n },\n\n relation: function relationData(obj, uid) {\n return new osmRelation({\n id: uid,\n visible: typeof obj.visible === 'boolean' ? obj.visible : true,\n version: obj.version && obj.version.toString(),\n changeset: obj.changeset && obj.changeset.toString(),\n timestamp: obj.timestamp,\n user: obj.user,\n uid: obj.uid && obj.uid.toString(),\n tags: obj.tags,\n members: getMembersJSON(obj)\n });\n },\n\n user: function parseUser(obj, uid) {\n return {\n id: uid,\n display_name: obj.display_name,\n account_created: obj.account_created,\n image_url: obj.img && obj.img.href,\n changesets_count: obj.changesets && obj.changesets.count && obj.changesets.count.toString() || '0',\n active_blocks: obj.blocks && obj.blocks.received && obj.blocks.received.active && obj.blocks.received.active.toString() || '0'\n };\n }\n};\n\nfunction parseJSON(payload, callback, options) {\n options = Object.assign({ skipSeen: true }, options);\n if (!payload) {\n return callback({ message: 'No JSON', status: -1 });\n }\n\n var json = payload;\n if (typeof json !== 'object') json = JSON.parse(payload);\n\n if (!json.elements) return callback({ message: 'No JSON', status: -1 });\n\n var children = json.elements;\n\n var handle = window.requestIdleCallback(function() {\n _deferred.delete(handle);\n var results = [];\n var result;\n for (var i = 0; i < children.length; i++) {\n result = parseChild(children[i]);\n if (result) results.push(result);\n }\n callback(null, results);\n });\n _deferred.add(handle);\n\n function parseChild(child) {\n var parser = jsonparsers[child.type];\n if (!parser) return null;\n\n var uid;\n\n uid = osmEntity.id.fromOSM(child.type, child.id);\n if (options.skipSeen) {\n if (_tileCache.seen[uid]) return null; // avoid reparsing a \"seen\" entity\n _tileCache.seen[uid] = true;\n }\n\n return parser(child, uid);\n }\n}\n\nfunction parseUserJSON(payload, callback, options) {\n options = Object.assign({ skipSeen: true }, options);\n if (!payload) {\n return callback({ message: 'No JSON', status: -1 });\n }\n\n var json = payload;\n if (typeof json !== 'object') json = JSON.parse(payload);\n\n if (!json.users && !json.user) return callback({ message: 'No JSON', status: -1 });\n\n var objs = json.users || [json];\n\n var handle = window.requestIdleCallback(function() {\n _deferred.delete(handle);\n var results = [];\n var result;\n for (var i = 0; i < objs.length; i++) {\n result = parseObj(objs[i]);\n if (result) results.push(result);\n }\n callback(null, results);\n });\n _deferred.add(handle);\n\n function parseObj(obj) {\n var uid = obj.user.id && obj.user.id.toString();\n if (options.skipSeen && _userCache.user[uid]) {\n delete _userCache.toLoad[uid];\n return null;\n }\n var user = jsonparsers.user(obj.user, uid);\n _userCache.user[uid] = user;\n delete _userCache.toLoad[uid];\n return user;\n }\n}\n\nfunction parseNoteJSON(payload, callback, _options) {\n const options = { skipSeen: true, ..._options };\n if (!payload) {\n return callback({ message: 'No JSON', status: -1 });\n }\n\n const features = payload.type === 'FeatureCollection' ? payload.features : [payload];\n\n const notes = features.map(feature => {\n const uid = feature.properties.id;\n if (options.skipSeen) {\n if (_tileCache.seen[uid]) return null; // avoid reparsing a \"seen\" entity\n _tileCache.seen[uid] = true;\n }\n\n const props = {\n ...feature.properties,\n loc: feature.geometry.coordinates,\n };\n\n // if notes are coincident, move them apart slightly\n if (!_noteCache.note[uid]) {\n let coincident = false;\n const epsilon = 0.00001;\n do {\n if (coincident) {\n props.loc = geoVecAdd(props.loc, [epsilon, epsilon]);\n }\n const bbox = geoExtent(props.loc).bbox();\n coincident = _noteCache.rtree.search(bbox).length;\n } while (coincident);\n } else {\n // we already saw this note: don't change its location again\n props.loc = _noteCache.note[uid].loc;\n }\n\n var note = new osmNote(props);\n var item = encodeNoteRtree(note);\n _noteCache.note[note.id] = note;\n updateRtree(item, true);\n\n return note;\n });\n callback(undefined, notes);\n}\n\n// replace or remove note from rtree\nfunction updateRtree(item, replace) {\n _noteCache.rtree.remove(item, function isEql(a, b) { return a.data.id === b.data.id; });\n\n if (replace) {\n _noteCache.rtree.insert(item);\n }\n}\n\n\nfunction wrapcb(thisArg, callback, cid) {\n return function(err, result) {\n if (err) {\n return callback.call(thisArg, err);\n\n } else if (thisArg.getConnectionId() !== cid) {\n return callback.call(thisArg, { message: 'Connection Switched', status: -1 });\n\n } else {\n return callback.call(thisArg, err, result);\n }\n };\n}\n\n\nexport default {\n\n init: function() {\n utilRebind(this, dispatch, 'on');\n },\n\n\n reset: function() {\n Array.from(_deferred).forEach(function(handle) {\n window.cancelIdleCallback(handle);\n _deferred.delete(handle);\n });\n\n _connectionID++;\n _userChangesets = undefined;\n _userDetails = undefined;\n _rateLimitError = undefined;\n\n Object.values(_tileCache.inflight).forEach(abortRequest);\n Object.values(_noteCache.inflight).forEach(abortRequest);\n Object.values(_noteCache.inflightPost).forEach(abortRequest);\n if (_changeset.inflight) abortRequest(_changeset.inflight);\n\n _tileCache = { toLoad: {}, loaded: {}, inflight: {}, seen: {}, rtree: new RBush() };\n _noteCache = { toLoad: {}, loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: new RBush() };\n _userCache = { toLoad: {}, user: {} };\n _cachedApiStatus = undefined;\n _changeset = {};\n\n return this;\n },\n\n\n getConnectionId: function() {\n return _connectionID;\n },\n\n\n getUrlRoot: function() {\n return urlroot;\n },\n\n\n getApiUrlRoot: function() {\n return apiUrlroot;\n },\n\n\n changesetURL: function(changesetID) {\n return urlroot + '/changeset/' + changesetID;\n },\n\n\n changesetsURL: function(center, zoom) {\n var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));\n return urlroot + '/history#map=' +\n Math.floor(zoom) + '/' +\n center[1].toFixed(precision) + '/' +\n center[0].toFixed(precision);\n },\n\n\n entityURL: function(entity) {\n return urlroot + '/' + entity.type + '/' + entity.osmId();\n },\n\n\n historyURL: function(entity) {\n return urlroot + '/' + entity.type + '/' + entity.osmId() + '/history';\n },\n\n\n userURL: function(username) {\n return urlroot + '/user/' + encodeURIComponent(username);\n },\n\n\n noteURL: function(note) {\n return urlroot + '/note/' + note.id;\n },\n\n\n noteReportURL: function(note) {\n return urlroot + '/reports/new?reportable_type=Note&reportable_id=' + note.id;\n },\n\n\n // Generic method to load data from the OSM API\n // Can handle either auth or unauth calls.\n loadFromAPI: function(path, callback, options) {\n options = Object.assign({ skipSeen: true }, options);\n var that = this;\n var cid = _connectionID;\n\n function done(err, payloadString) {\n if (that.getConnectionId() !== cid) {\n if (callback) callback({ message: 'Connection Switched', status: -1 });\n return;\n }\n\n if ((err && _cachedApiStatus === 'online') ||\n (!err && _cachedApiStatus !== 'online')) {\n // If the response's error state doesn't match the status,\n // it's likely we lost or gained the connection so reload the status\n that.reloadApiStatus();\n }\n\n if (callback) {\n if (err) {\n // eslint-disable-next-line no-console\n console.error('API error:', err);\n return callback(err);\n } else {\n const payload = typeof payloadString === 'string' ? JSON.parse(payloadString) : payloadString;\n\n if (payload.type === 'FeatureCollection' || payload.type === 'Feature') {\n return parseNoteJSON(payload, callback, options);\n } else {\n return parseJSON(payload, callback, options);\n }\n }\n }\n }\n\n if (this.authenticated()) {\n return oauth.xhr({\n method: 'GET',\n path\n }, done);\n } else {\n var url = apiUrlroot + path;\n var controller = new AbortController();\n\n d3_json(url, { signal: controller.signal })\n .then(function(data) {\n done(null, data);\n })\n .catch(function(err) {\n if (err.name === 'AbortError') return;\n // d3-fetch includes status in the error message,\n // but we can't access the response itself\n // https://github.com/d3/d3-fetch/issues/27\n var match = err.message.match(/^\\d{3}/);\n if (match) {\n done({ status: +match[0], statusText: err.message });\n } else {\n done(err.message);\n }\n });\n return controller;\n }\n },\n\n\n // Load a single entity by id (ways and relations use the `/full` call to include\n // nodes and members). Parent relations are not included, see `loadEntityRelations`.\n // GET /api/0.6/node/#id\n // GET /api/0.6/[way|relation]/#id/full\n loadEntity: function(id, callback) {\n var type = osmEntity.id.type(id);\n var osmID = osmEntity.id.toOSM(id);\n var options = { skipSeen: false };\n\n this.loadFromAPI(\n '/api/0.6/' + type + '/' + osmID + (type !== 'node' ? '/full' : '') + '.json',\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n // Load a single note by id , XML format\n // GET /api/0.6/notes/#id\n loadEntityNote: function(id, callback) {\n var options = { skipSeen: false };\n this.loadFromAPI(\n `/api/0.6/notes/${id}.json`,\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n\n // Load a single entity with a specific version\n // GET /api/0.6/[node|way|relation]/#id/#version\n loadEntityVersion: function(id, version, callback) {\n var type = osmEntity.id.type(id);\n var osmID = osmEntity.id.toOSM(id);\n var options = { skipSeen: false };\n\n this.loadFromAPI(\n '/api/0.6/' + type + '/' + osmID + '/' + version + '.json',\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n\n // Load the relations of a single entity with the given.\n // GET /api/0.6/[node|way|relation]/#id/relations\n loadEntityRelations: function(id, callback) {\n var type = osmEntity.id.type(id);\n var osmID = osmEntity.id.toOSM(id);\n var options = { skipSeen: false };\n\n this.loadFromAPI(\n '/api/0.6/' + type + '/' + osmID + '/relations.json',\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n\n // Load multiple entities in chunks\n // (note: callback may be called multiple times)\n // Unlike `loadEntity`, child nodes and members are not fetched\n // GET /api/0.6/[nodes|ways|relations]?#parameters\n loadMultiple: function(ids, callback) {\n var that = this;\n var groups = utilArrayGroupBy(utilArrayUniq(ids), osmEntity.id.type);\n\n Object.keys(groups).forEach(function(k) {\n var type = k + 's'; // nodes, ways, relations\n var osmIDs = groups[k].map(function(id) { return osmEntity.id.toOSM(id); });\n var options = { skipSeen: false };\n\n utilArrayChunk(osmIDs, 150).forEach(function(arr) {\n that.loadFromAPI(\n '/api/0.6/' + type + '.json?' + type + '=' + arr.join(),\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n });\n });\n },\n\n\n // Create, upload, and close a changeset\n // PUT /api/0.6/changeset/create\n // POST /api/0.6/changeset/#id/upload\n // PUT /api/0.6/changeset/#id/close\n putChangeset: function(changeset, changes, callback) {\n var cid = _connectionID;\n\n if (_changeset.inflight) {\n return callback({ message: 'Changeset already inflight', status: -2 }, changeset);\n\n } else if (_changeset.open) { // reuse existing open changeset..\n return createdChangeset.call(this, null, _changeset.open);\n\n } else { // Open a new changeset..\n var options = {\n method: 'PUT',\n path: '/api/0.6/changeset/create',\n headers: { 'Content-Type': 'text/xml' },\n content: JXON.stringify(changeset.asJXON())\n };\n _changeset.inflight = oauth.xhr(\n options,\n wrapcb(this, createdChangeset, cid)\n );\n }\n\n\n async function createdChangeset(err, changesetID) {\n _changeset.inflight = null;\n if (err) { return callback(err, changeset); }\n\n _changeset.open = changesetID;\n changeset = changeset.update({ id: changesetID });\n\n // Upload the changeset..\n const xml = JXON.stringify(changeset.osmChangeJXON(changes));\n const compressed = await utilGzip(xml);\n\n const headers = { 'Content-Type': 'text/xml' };\n if (compressed) headers['Content-Encoding'] = 'gzip';\n\n var options = {\n method: 'POST',\n path: '/api/0.6/changeset/' + changesetID + '/upload',\n headers,\n content: compressed || xml,\n };\n _changeset.inflight = oauth.xhr(\n options,\n wrapcb(this, uploadedChangeset, cid)\n );\n }\n\n\n function uploadedChangeset(err) {\n _changeset.inflight = null;\n if (err) return callback(err, changeset);\n\n // Upload was successful, safe to call the callback.\n // Add delay to allow for postgres replication #1646 #2678\n window.setTimeout(function() { callback(null, changeset); }, 2500);\n _changeset.open = null;\n\n // At this point, we don't really care if the connection was switched..\n // Only try to close the changeset if we're still talking to the same server.\n if (this.getConnectionId() === cid) {\n // Still attempt to close changeset, but ignore response because #2667\n oauth.xhr({\n method: 'PUT',\n path: '/api/0.6/changeset/' + changeset.id + '/close',\n headers: { 'Content-Type': 'text/xml' }\n }, function() { return true; });\n }\n }\n },\n\n /** updates the tags on an existing unclosed changeset */\n // PUT /api/0.6/changeset/#id\n updateChangesetTags: (changeset) => {\n return oauth.fetch(`${oauth.options().apiUrl}/api/0.6/changeset/${changeset.id}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'text/xml' },\n body: JXON.stringify(changeset.asJXON())\n });\n },\n\n\n // Load multiple users in chunks\n // (note: callback may be called multiple times)\n // GET /api/0.6/users?users=#id1,#id2,...,#idn\n loadUsers: function(uids, callback) {\n var toLoad = [];\n var cached = [];\n\n utilArrayUniq(uids).forEach(function(uid) {\n if (_userCache.user[uid]) {\n delete _userCache.toLoad[uid];\n cached.push(_userCache.user[uid]);\n } else {\n toLoad.push(uid);\n }\n });\n\n if (cached.length || !this.authenticated()) {\n callback(undefined, cached);\n if (!this.authenticated()) return; // require auth\n }\n\n utilArrayChunk(toLoad, 150).forEach(function(arr) {\n oauth.xhr({\n method: 'GET',\n path: '/api/0.6/users.json?users=' + arr.join()\n }, wrapcb(this, done, _connectionID));\n }.bind(this));\n\n function done(err, payload) {\n if (err) return callback(err);\n\n var options = { skipSeen: true };\n return parseUserJSON(payload, function(err, results) {\n if (err) return callback(err);\n return callback(undefined, results);\n }, options);\n }\n },\n\n\n // Load a given user by id\n // GET /api/0.6/user/#id\n loadUser: function(uid, callback) {\n if (_userCache.user[uid] || !this.authenticated()) { // require auth\n delete _userCache.toLoad[uid];\n return callback(undefined, _userCache.user[uid]);\n }\n\n oauth.xhr({\n method: 'GET',\n path: '/api/0.6/user/' + uid + '.json'\n }, wrapcb(this, done, _connectionID));\n\n function done(err, payload) {\n if (err) return callback(err);\n\n var options = { skipSeen: true };\n return parseUserJSON(payload, function(err, results) {\n if (err) return callback(err);\n return callback(undefined, results[0]);\n }, options);\n }\n },\n\n\n // Load the details of the logged-in user\n // GET /api/0.6/user/details\n userDetails: function(callback) {\n if (_userDetails) { // retrieve cached\n return callback(undefined, _userDetails);\n }\n\n oauth.xhr({\n method: 'GET',\n path: '/api/0.6/user/details.json'\n }, wrapcb(this, done, _connectionID));\n\n function done(err, payload) {\n if (err) return callback(err);\n\n var options = { skipSeen: false };\n return parseUserJSON(payload, function(err, results) {\n if (err) return callback(err);\n _userDetails = results[0];\n return callback(undefined, _userDetails);\n }, options);\n }\n },\n\n\n // Load previous changesets for the logged in user\n // GET /api/0.6/changesets?user=#id\n userChangesets: function(callback) {\n if (_userChangesets) { // retrieve cached\n return callback(undefined, _userChangesets);\n }\n\n this.userDetails(\n wrapcb(this, gotDetails, _connectionID)\n );\n\n\n function gotDetails(err, user) {\n if (err) { return callback(err); }\n\n oauth.xhr({\n method: 'GET',\n path: `/api/0.6/changesets.json?user=${user.id}`\n }, wrapcb(this, done, _connectionID));\n }\n\n function done(err, payloadString) {\n if (err) { return callback(err); }\n\n const payload = JSON.parse(payloadString);\n _userChangesets = payload.changesets.filter(tags => tags.tags.comment);\n\n return callback(undefined, _userChangesets);\n }\n },\n\n\n // Fetch the status of the OSM API\n // GET /api/capabilities\n status: function(callback) {\n const url = `${apiUrlroot}/api/capabilities.json`;\n var errback = wrapcb(this, done, _connectionID);\n d3_json(url)\n .then(function(data) { errback(null, data); })\n .catch(function(err) { errback(err.message); });\n\n function done(err, payload) {\n if (err) {\n // the status is null if no response could be retrieved\n return callback(err, null);\n }\n\n if (_rateLimitError) {\n return callback(_rateLimitError, 'rateLimited');\n } else {\n _maxWayNodes = payload.api.waynodes.maximum;\n\n _imageryBlocklists = payload.policy.imagery.blacklist.map(item => new RegExp(item.regex, 'i'));\n\n const maxChangesetElements = payload.api.changesets.maximum_elements;\n if (!Number.isNaN(maxChangesetElements)) _maxChangesetElements = maxChangesetElements;\n\n return callback(undefined, payload.api.status.api);\n }\n }\n },\n\n // Calls `status` and dispatches an `apiStatusChange` event if the returned\n // status differs from the cached status.\n reloadApiStatus: function() {\n // throttle to avoid unnecessary API calls\n if (!this.throttledReloadApiStatus) {\n var that = this;\n this.throttledReloadApiStatus = throttle(function() {\n that.status(function(err, status) {\n if (status !== _cachedApiStatus) {\n _cachedApiStatus = status;\n dispatch.call('apiStatusChange', that, err, status);\n }\n });\n }, 500);\n }\n this.throttledReloadApiStatus();\n },\n\n\n // Returns the maximum number of nodes a single way can have\n maxWayNodes: function() {\n return _maxWayNodes;\n },\n\n\n maxChangesetElements: () => _maxChangesetElements,\n\n\n // Load data (entities) from the API in tiles\n // GET /api/0.6/map?bbox=\n loadTiles: function(projection, callback) {\n if (_off) return;\n\n // determine the needed tiles to cover the view\n var tiles = tiler.zoomExtent([_tileZoom, _tileZoom]).getTiles(projection);\n\n // abort inflight requests that are no longer needed\n var hadRequests = hasInflightRequests(_tileCache);\n abortUnwantedRequests(_tileCache, tiles);\n if (hadRequests && !hasInflightRequests(_tileCache)) {\n if (_rateLimitError) {\n // was rate limited, but has settled\n _rateLimitError = undefined;\n dispatch.call('change');\n this.reloadApiStatus();\n }\n dispatch.call('loaded'); // stop the spinner\n }\n\n // issue new requests..\n tiles.forEach(function(tile) {\n this.loadTile(tile, callback);\n }, this);\n },\n\n\n // Load a single data tile\n // GET /api/0.6/map?bbox=\n loadTile: function(tile, callback) {\n if (_off) return;\n if (_tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return;\n\n if (!hasInflightRequests(_tileCache)) {\n dispatch.call('loading'); // start the spinner\n }\n\n var path = '/api/0.6/map.json?bbox=';\n var options = { skipSeen: true };\n\n _tileCache.inflight[tile.id] = this.loadFromAPI(\n path + tile.extent.toParam(),\n tileCallback.bind(this),\n options\n );\n\n function tileCallback(err, parsed) {\n if (!err) {\n delete _tileCache.inflight[tile.id];\n delete _tileCache.toLoad[tile.id];\n _tileCache.loaded[tile.id] = true;\n var bbox = tile.extent.bbox();\n bbox.id = tile.id;\n _tileCache.rtree.insert(bbox);\n } else {\n // map tile loading error: e.g. network connection error,\n // 509 Bandwidth Limit Exceeded, 429 Too Many Requests\n if (!_rateLimitError && err.status === 509 || err.status === 429) {\n // show \"API rate limiting\" warning\n _rateLimitError = err;\n dispatch.call('change');\n this.reloadApiStatus();\n }\n setTimeout(() => {\n // retry loading the tiles\n delete _tileCache.inflight[tile.id];\n this.loadTile(tile, callback);\n }, 8000);\n }\n if (callback) {\n callback(err, Object.assign({ data: parsed }, tile));\n }\n if (!hasInflightRequests(_tileCache)) {\n if (_rateLimitError) {\n // was rate limited, but has settled\n _rateLimitError = undefined;\n dispatch.call('change');\n this.reloadApiStatus();\n }\n dispatch.call('loaded'); // stop the spinner\n }\n }\n },\n\n\n isDataLoaded: function(loc) {\n var bbox = { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1] };\n return _tileCache.rtree.collides(bbox);\n },\n\n\n // load the tile that covers the given `loc`\n loadTileAtLoc: function(loc, callback) {\n // Back off if the toLoad queue is filling up.. re #6417\n // (Currently `loadTileAtLoc` requests are considered low priority - used by operations to\n // let users safely edit geometries which extend to unloaded tiles. We can drop some.)\n if (Object.keys(_tileCache.toLoad).length > 50) return;\n\n var k = geoZoomToScale(_tileZoom + 1);\n var offset = geoRawMercator().scale(k)(loc);\n var projection = geoRawMercator().transform({ k: k, x: -offset[0], y: -offset[1] });\n var tiles = tiler.zoomExtent([_tileZoom, _tileZoom]).getTiles(projection);\n\n tiles.forEach(function(tile) {\n if (_tileCache.toLoad[tile.id] || _tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return;\n\n _tileCache.toLoad[tile.id] = true;\n this.loadTile(tile, callback);\n }, this);\n },\n\n\n // Load notes from the API in tiles\n // GET /api/0.6/notes?bbox=\n loadNotes: function(projection, noteOptions) {\n noteOptions = Object.assign({ limit: 10000, closed: 7 }, noteOptions);\n if (_off) return;\n\n var that = this;\n var path = `/api/0.6/notes.json?limit=${noteOptions.limit}&closed=${noteOptions.closed}&bbox=`;\n var throttleLoadUsers = throttle(function() {\n var uids = Object.keys(_userCache.toLoad);\n if (!uids.length) return;\n that.loadUsers(uids, function() {}); // eagerly load user details\n }, 750);\n\n // determine the needed tiles to cover the view\n var tiles = tiler.zoomExtent([_noteZoom, _noteZoom]).getTiles(projection);\n\n // abort inflight requests that are no longer needed\n abortUnwantedRequests(_noteCache, tiles);\n\n // issue new requests..\n tiles.forEach(function(tile) {\n if (_noteCache.loaded[tile.id] || _noteCache.inflight[tile.id]) return;\n\n var options = { skipSeen: false };\n _noteCache.inflight[tile.id] = that.loadFromAPI(\n path + tile.extent.toParam(),\n function(err) {\n delete _noteCache.inflight[tile.id];\n if (!err) {\n _noteCache.loaded[tile.id] = true;\n }\n throttleLoadUsers();\n dispatch.call('loadedNotes');\n },\n options\n );\n });\n },\n\n\n // Create a note\n // POST /api/0.6/notes?params\n postNoteCreate: function(note, callback) {\n if (!this.authenticated()) {\n return callback({ message: 'Not Authenticated', status: -3 }, note);\n }\n if (_noteCache.inflightPost[note.id]) {\n return callback({ message: 'Note update already inflight', status: -2 }, note);\n }\n\n if (!note.loc[0] || !note.loc[1] || !note.newComment) return; // location & description required\n\n var comment = note.newComment;\n if (note.newCategory && note.newCategory !== 'None') { comment += ' #' + note.newCategory; }\n\n var path = '/api/0.6/notes.json?' + utilQsString({ lon: note.loc[0], lat: note.loc[1], text: comment });\n\n _noteCache.inflightPost[note.id] = oauth.xhr({\n method: 'POST',\n path: path\n }, wrapcb(this, done, _connectionID));\n\n\n function done(err, payload) {\n delete _noteCache.inflightPost[note.id];\n if (err) { return callback(err); }\n\n // we get the updated note back, remove from caches and reparse..\n this.removeNote(note);\n\n var options = { skipSeen: false };\n return parseNoteJSON(JSON.parse(payload), function(err, results) {\n if (err) {\n return callback(err);\n } else {\n return callback(undefined, results[0]);\n }\n }, options);\n }\n },\n\n\n // Update a note\n // POST /api/0.6/notes/#id/comment?text=comment\n // POST /api/0.6/notes/#id/close?text=comment\n // POST /api/0.6/notes/#id/reopen?text=comment\n postNoteUpdate: function(note, newStatus, callback) {\n if (!this.authenticated()) {\n return callback({ message: 'Not Authenticated', status: -3 }, note);\n }\n if (_noteCache.inflightPost[note.id]) {\n return callback({ message: 'Note update already inflight', status: -2 }, note);\n }\n\n var action;\n if (note.status !== 'closed' && newStatus === 'closed') {\n action = 'close';\n } else if (note.status !== 'open' && newStatus === 'open') {\n action = 'reopen';\n } else {\n action = 'comment';\n if (!note.newComment) return; // when commenting, comment required\n }\n\n var path = `/api/0.6/notes/${note.id}/${action}.json`;\n if (note.newComment) {\n path += '?' + utilQsString({ text: note.newComment });\n }\n\n _noteCache.inflightPost[note.id] = oauth.xhr({\n method: 'POST',\n path: path\n }, wrapcb(this, done, _connectionID));\n\n\n function done(err, payload) {\n delete _noteCache.inflightPost[note.id];\n if (err) { return callback(err); }\n\n // we get the updated note back, remove from caches and reparse..\n this.removeNote(note);\n\n // update closed note cache - used to populate `closed:note` changeset tag\n if (action === 'close') {\n _noteCache.closed[note.id] = true;\n } else if (action === 'reopen') {\n delete _noteCache.closed[note.id];\n }\n\n var options = { skipSeen: false };\n return parseNoteJSON(JSON.parse(payload), function(err, results) {\n if (err) {\n return callback(err);\n } else {\n return callback(undefined, results[0]);\n }\n }, options);\n }\n },\n\n\n /* connection options for source switcher (optional) */\n apiConnections: function(val) {\n if (!arguments.length) return _apiConnections;\n _apiConnections = val;\n return this;\n },\n\n\n switch: function(newOptions) {\n urlroot = newOptions.url;\n apiUrlroot = newOptions.apiUrl || urlroot;\n if (newOptions.url && !newOptions.apiUrl) {\n newOptions = {\n ...newOptions,\n apiUrl: newOptions.url\n };\n }\n\n // Copy the existing options, but omit 'access_token'.\n // (if we did preauth, access_token won't work on a different server)\n const oldOptions = utilObjectOmit(oauth.options(), 'access_token');\n oauth.options({...oldOptions, ...newOptions});\n\n this.reset();\n this.userChangesets(function() {}); // eagerly load user details/changesets\n dispatch.call('change');\n return this;\n },\n\n\n toggle: function(val) {\n _off = !val;\n return this;\n },\n\n\n isChangesetInflight: function() {\n return !!_changeset.inflight;\n },\n\n\n // get/set cached data\n // This is used to save/restore the state when entering/exiting the walkthrough\n // Also used for testing purposes.\n caches: function(obj) {\n function cloneCache(source) {\n var target = {};\n Object.keys(source).forEach(function(k) {\n if (k === 'rtree') {\n target.rtree = new RBush().fromJSON(source.rtree.toJSON()); // clone rbush\n } else if (k === 'note') {\n target.note = {};\n Object.keys(source.note).forEach(function(id) {\n target.note[id] = osmNote(source.note[id]); // copy notes\n });\n } else {\n target[k] = JSON.parse(JSON.stringify(source[k])); // clone deep\n }\n });\n return target;\n }\n\n if (!arguments.length) {\n return {\n tile: cloneCache(_tileCache),\n note: cloneCache(_noteCache),\n user: cloneCache(_userCache)\n };\n }\n\n // access caches directly for testing (e.g., loading notes rtree)\n if (obj === 'get') {\n return {\n tile: _tileCache,\n note: _noteCache,\n user: _userCache\n };\n }\n\n if (obj.tile) {\n _tileCache = obj.tile;\n _tileCache.inflight = {};\n }\n if (obj.note) {\n _noteCache = obj.note;\n _noteCache.inflight = {};\n _noteCache.inflightPost = {};\n }\n if (obj.user) {\n _userCache = obj.user;\n }\n\n return this;\n },\n\n\n logout: function() {\n _userChangesets = undefined;\n _userDetails = undefined;\n oauth.logout();\n dispatch.call('change');\n return this;\n },\n\n\n authenticated: function() {\n return oauth.authenticated();\n },\n\n\n /** @param {import('osm-auth').LoginOptions} options */\n authenticate: function(callback, options) {\n var that = this;\n var cid = _connectionID;\n _userChangesets = undefined;\n _userDetails = undefined;\n\n function done(err, res) {\n if (err) {\n if (callback) callback(err);\n return;\n }\n if (that.getConnectionId() !== cid) {\n if (callback) callback({ message: 'Connection Switched', status: -1 });\n return;\n }\n _rateLimitError = undefined;\n dispatch.call('change');\n if (callback) callback(err, res);\n that.userChangesets(function() {}); // eagerly load user details/changesets\n }\n\n // ensure the locale is correctly set before opening the popup\n oauth.options({\n ...oauth.options(),\n locale: localizer.localeCode(),\n });\n\n oauth.authenticate(done, options);\n },\n\n\n imageryBlocklists: function() {\n return _imageryBlocklists;\n },\n\n\n tileZoom: function(val) {\n if (!arguments.length) return _tileZoom;\n _tileZoom = val;\n return this;\n },\n\n\n // get all cached notes covering the viewport\n notes: function(projection) {\n var viewport = projection.clipExtent();\n var min = [viewport[0][0], viewport[1][1]];\n var max = [viewport[1][0], viewport[0][1]];\n var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n\n return _noteCache.rtree.search(bbox)\n .map(function(d) { return d.data; });\n },\n\n\n // get a single note from the cache\n getNote: function(id) {\n return _noteCache.note[id];\n },\n\n\n // remove a single note from the cache\n removeNote: function(note) {\n if (!(note instanceof osmNote) || !note.id) return;\n\n delete _noteCache.note[note.id];\n updateRtree(encodeNoteRtree(note), false); // false = remove\n },\n\n\n // replace a single note in the cache\n replaceNote: function(note) {\n if (!(note instanceof osmNote) || !note.id) return;\n\n _noteCache.note[note.id] = note;\n updateRtree(encodeNoteRtree(note), true); // true = replace\n return note;\n },\n\n\n // Get an array of note IDs closed during this session.\n // Used to populate `closed:note` changeset tag\n getClosedIDs: function() {\n return Object.keys(_noteCache.closed).sort();\n }\n\n};\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { json as d3_json } from 'd3-fetch';\n\nimport { localizer } from '../core/localizer';\nimport { utilQsString } from '../util';\n\n\nvar apibase = 'https://wiki.openstreetmap.org/w/api.php';\nvar _inflight = {};\nvar _wikibaseCache = {};\nvar _localeIDs = { en: false };\n\n\nvar debouncedRequest = debounce(request, 500, { leading: false });\n\nfunction request(url, callback) {\n if (_inflight[url]) return;\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, { signal: controller.signal })\n .then(function(result) {\n delete _inflight[url];\n if (callback) callback(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (callback) callback(err.message);\n });\n}\n\n\nexport default {\n\n init: function() {\n _inflight = {};\n _wikibaseCache = {};\n _localeIDs = {};\n },\n\n\n reset: function() {\n Object.values(_inflight).forEach(function(controller) { controller.abort(); });\n _inflight = {};\n },\n\n\n /**\n * Get the best value for the property, or undefined if not found\n * @param entity object from wikibase\n * @param property string e.g. 'P4' for image\n * @param langCode string e.g. 'fr' for French\n */\n claimToValue: function(entity, property, langCode) {\n if (!entity.claims[property]) return undefined;\n var locale = _localeIDs[langCode];\n var preferredPick, localePick;\n\n entity.claims[property].forEach(function(stmt) {\n // If exists, use value limited to the needed language (has a qualifier P26 = locale)\n // Or if not found, use the first value with the \"preferred\" rank\n if (!preferredPick && stmt.rank === 'preferred') {\n preferredPick = stmt;\n }\n if (locale && stmt.qualifiers && stmt.qualifiers.P26 &&\n stmt.qualifiers.P26[0].datavalue.value.id === locale\n ) {\n localePick = stmt;\n }\n });\n\n var result = localePick || preferredPick;\n if (result) {\n var datavalue = result.mainsnak.datavalue;\n return datavalue.type === 'wikibase-entityid' ? datavalue.value.id : datavalue.value;\n } else {\n return undefined;\n }\n },\n\n\n /**\n * Convert monolingual property into a key-value object (language -> value)\n * @param entity object from wikibase\n * @param property string e.g. 'P31' for monolingual wiki page title\n */\n monolingualClaimToValueObj: function(entity, property) {\n if (!entity || !entity.claims[property]) return undefined;\n\n return entity.claims[property].reduce(function(acc, obj) {\n var value = obj.mainsnak.datavalue.value;\n acc[value.language] = value.text;\n return acc;\n }, {});\n },\n\n\n toSitelink: function(key, value, isHistorical) {\n var type = value ? 'Tag' : 'Key';\n var prefix = '';\n if (isHistorical) {\n prefix = `OpenHistoricalMap/Tags/${type}/`;\n } else {\n prefix = type + ':';\n }\n return (prefix + (value ? `${key}=${value}` : key).replace(/_/g, ' ')).trim();\n },\n\n /**\n * Converts text like `tag:...=...` into clickable links\n *\n * @param {string} unsafeText - unsanitized text\n */\n linkifyWikiText(unsafeText) {\n /** @param {import('d3').Selection} selection */\n return (selection) => {\n const segments = unsafeText.split(/(key|tag):([\\w-]+)(=([\\w-]+))?/g);\n\n for (let i = 0; i < segments.length; i += 5) {\n const [plainText, , key, , value] = segments.slice(i);\n\n if (plainText) {\n selection\n .append('span')\n .text(plainText);\n }\n\n if (key) {\n selection\n .append('a')\n .attr('href', `https://wiki.openstreetmap.org/wiki/${this.toSitelink(key, value)}`)\n .attr('target', '_blank')\n .attr('rel', 'noreferrer')\n .append('code')\n .text(`${key}=${value || '*'}`);\n }\n }\n };\n },\n\n\n //\n // Pass params object of the form:\n // {\n // key: 'string',\n // value: 'string',\n // langCode: 'string'\n // }\n //\n getEntity: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n var that = this;\n var titles = [];\n var result = {};\n var rtypeSitelink = (params.key === 'type' && params.value) ? ('Relation:' + params.value).replace(/_/g, ' ').trim() : false;\n var rtypeSitelinkHistorical = (params.key === 'type' && params.value) ? ('OpenHistoricalMap/Tags/Relation/' + params.value.replace(/_/g, ' ').trim()) : false;\n var keySitelink = params.key ? this.toSitelink(params.key) : false;\n var keySitelinkHistorical = params.key ? this.toSitelink(params.key, null, true) : false;\n var tagSitelink = (params.key && params.value) ? this.toSitelink(params.key, params.value) : false;\n var tagSitelinkHistorical = (params.key && params.value) ? this.toSitelink(params.key, params.value, true) : false;\n\n if (params.langCodes) {\n params.langCodes.forEach(function(langCode) {\n if (_localeIDs[langCode] === undefined) {\n // If this is the first time we are asking about this locale,\n // fetch corresponding entity (if it exists), and cache it.\n // If there is no such entry, cache `false` value to avoid re-requesting it.\n let localeSitelink = ('Locale:' + langCode).replace(/_/g, ' ').trim();\n titles.push(localeSitelink);\n\n // initialize with false, such that if locale ID is not found in first request,\n // it will not be retried in further queries\n that.addLocale(langCode, false);\n }\n });\n }\n\n if (rtypeSitelink) {\n if (_wikibaseCache[rtypeSitelinkHistorical]) {\n result.rtype = _wikibaseCache[rtypeSitelinkHistorical];\n } else if (_wikibaseCache[rtypeSitelink]) {\n result.rtype = _wikibaseCache[rtypeSitelink];\n } else {\n titles.push(rtypeSitelink, rtypeSitelinkHistorical);\n }\n }\n\n if (keySitelink) {\n if (_wikibaseCache[keySitelinkHistorical]) {\n result.key = _wikibaseCache[keySitelinkHistorical];\n } else if (_wikibaseCache[keySitelink]) {\n result.key = _wikibaseCache[keySitelink];\n } else {\n titles.push(keySitelink, keySitelinkHistorical);\n }\n }\n\n if (tagSitelink) {\n if (_wikibaseCache[tagSitelinkHistorical]) {\n result.tag = _wikibaseCache[tagSitelinkHistorical];\n } else if (_wikibaseCache[tagSitelink]) {\n result.tag = _wikibaseCache[tagSitelink];\n } else {\n titles.push(tagSitelink, tagSitelinkHistorical);\n }\n }\n\n if (!titles.length) {\n // Nothing to do, we already had everything in the cache\n return callback(null, result);\n }\n\n // Requesting just the user language code\n // If backend recognizes the code, it will perform proper fallbacks,\n // and the result will contain the requested code. If not, all values are returned:\n // {\"zh-tw\":{\"value\":\"...\",\"language\":\"zh-tw\",\"source-language\":\"zh-hant\"}\n // {\"pt-br\":{\"value\":\"...\",\"language\":\"pt\",\"for-language\":\"pt-br\"}}\n var obj = {\n action: 'wbgetentities',\n sites: 'wiki',\n titles: titles.join('|'),\n languages: params.langCodes.join('|'),\n languagefallback: 1,\n origin: '*',\n format: 'json',\n // There is an MW Wikibase API bug https://phabricator.wikimedia.org/T212069\n // We shouldn't use v1 until it gets fixed, but should switch to it afterwards\n // formatversion: 2,\n };\n\n var url = apibase + '?' + utilQsString(obj);\n doRequest(url, function(err, d) {\n if (err) {\n callback(err);\n } else if (!d.success || d.error) {\n callback(d.error.messages.map(function(v) { return v.html['*']; }).join('
    '));\n } else {\n Object.values(d.entities).forEach(function(res) {\n if (res.missing !== '') {\n\n var title = res.sitelinks.wiki.title;\n if (title === rtypeSitelinkHistorical) {\n _wikibaseCache[rtypeSitelinkHistorical] = res;\n result.rtype = res;\n } else if (title === rtypeSitelink) {\n _wikibaseCache[rtypeSitelink] = res;\n result.rtype = res;\n } else if (title === keySitelinkHistorical) {\n _wikibaseCache[keySitelinkHistorical] = res;\n result.key = res;\n } else if (title === keySitelink) {\n _wikibaseCache[keySitelink] = res;\n result.key = res;\n } else if (title === tagSitelinkHistorical) {\n _wikibaseCache[tagSitelinkHistorical] = res;\n result.tag = res;\n } else if (title === tagSitelink) {\n _wikibaseCache[tagSitelink] = res;\n result.tag = res;\n } else if (title.startsWith('Locale:')) {\n const langCode = title.replace(/ /g, '_').replace(/^Locale:/, '');\n that.addLocale(langCode, res.id);\n } else {\n console.log('Unexpected title ' + title); // eslint-disable-line no-console\n }\n }\n });\n\n callback(null, result);\n }\n });\n },\n\n\n //\n // Pass params object of the form:\n // {\n // key: 'string', // required\n // value: 'string' // optional\n // }\n //\n // Get an result object used to display tag documentation\n // {\n // title: 'string',\n // description: 'string',\n // editURL: 'string',\n // imageURL: 'string',\n // wiki: { title: 'string', text: 'string', url: 'string' }\n // }\n //\n getDocs: function(params, callback) {\n var that = this;\n var langCodes = localizer.localeCodes().map(function(code) {\n return code.toLowerCase();\n });\n params.langCodes = langCodes;\n\n this.getEntity(params, function(err, data) {\n if (err) {\n callback(err);\n return;\n }\n\n var entity = data.rtype || data.tag || data.key;\n if (!entity) {\n callback('No entity');\n return;\n }\n\n var i;\n var description;\n for (i in langCodes) {\n let code = langCodes[i];\n if (entity.descriptions[code] && entity.descriptions[code].language === code) {\n description = entity.descriptions[code];\n break;\n }\n }\n if (!description && Object.values(entity.descriptions).length) description = Object.values(entity.descriptions)[0];\n\n // prepare result\n var result = {\n title: entity.title,\n description: that.linkifyWikiText(description?.value || ''),\n descriptionLocaleCode: description ? description.language : '',\n editURL: 'https://wiki.openstreetmap.org/wiki/' + entity.title\n };\n\n // add image\n if (entity.claims) {\n var imageroot;\n var image = that.claimToValue(entity, 'P4', langCodes[0]);\n if (image) {\n imageroot = 'https://commons.wikimedia.org/w/index.php';\n } else {\n image = that.claimToValue(entity, 'P28', langCodes[0]);\n if (image) {\n imageroot = 'https://wiki.openstreetmap.org/w/index.php';\n }\n }\n if (imageroot && image) {\n result.imageURL = imageroot + '?' + utilQsString({\n title: 'Special:Redirect/file/' + image,\n width: 400\n });\n }\n }\n\n // Try to get a wiki page from tag data item first, followed by the corresponding key data item.\n // If neither tag nor key data item contain a wiki page in the needed language nor English,\n // get the first found wiki page from either the tag or the key item.\n var rtypeWiki = that.monolingualClaimToValueObj(data.rtype, 'P31');\n var tagWiki = that.monolingualClaimToValueObj(data.tag, 'P31');\n var keyWiki = that.monolingualClaimToValueObj(data.key, 'P31');\n\n var wikis = [rtypeWiki, tagWiki, keyWiki];\n for (i in wikis) {\n var wiki = wikis[i];\n for (var j in langCodes) {\n var code = langCodes[j];\n var referenceId = (langCodes[0].split('-')[0] !== 'en' && code.split('-')[0] === 'en') ? 'inspector.wiki_en_reference' : 'inspector.wiki_reference';\n var info = getWikiInfo(wiki, code, referenceId);\n if (info) {\n result.wiki = info;\n break;\n }\n }\n if (result.wiki) break;\n }\n\n callback(null, result);\n\n\n // Helper method to get wiki info if a given language exists\n function getWikiInfo(wiki, langCode, tKey) {\n if (wiki && wiki[langCode]) {\n return {\n title: wiki[langCode],\n text: tKey,\n url: 'https://wiki.openstreetmap.org/wiki/' + wiki[langCode]\n };\n }\n }\n });\n },\n\n getLocaleIDs: () => _localeIDs,\n\n\n addLocale: function(langCode, qid) {\n // Makes it easier to unit test\n _localeIDs[langCode] = qid;\n },\n\n\n apibase: function(val) {\n if (!arguments.length) return apibase;\n apibase = val;\n return this;\n }\n\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { timer as d3_timer } from 'd3-timer';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport RBush from 'rbush';\nimport { t } from '../core/localizer';\n\nimport {\n geoExtent, geoMetersToLat, geoMetersToLon, geoPointInPolygon,\n geoRotate, geoVecLength\n} from '../geo';\n\nimport { utilAesDecrypt, utilArrayUnion, utilQsString, utilRebind, utilStringQs, utilTiler, utilUniqueDomId } from '../util';\n\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\nimport { localeTimestamp } from '../util/date';\n\n\nconst streetsideApi = 'https://dev.virtualearth.net/REST/v1/Imagery/MetaData/Streetside?mapArea={bbox}&key={key}&count={count}&uriScheme=https';\nconst maxResults = 500;\nconst bubbleAppKey = utilAesDecrypt('5c875730b09c6b422433e807e1ff060b6536c791dbfffcffc4c6b18a1bdba1f14593d151adb50e19e1be1ab19aef813bf135d0f103475e5c724dec94389e45d0');\nconst pannellumViewerCSS = 'pannellum/pannellum.css';\nconst pannellumViewerJS = 'pannellum/pannellum.js';\nconst tileZoom = 16.5;\nconst tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);\nconst dispatch = d3_dispatch('loadedImages', 'viewerChanged');\nconst minHfov = 10; // zoom in degrees: 20, 10, 5\nconst maxHfov = 90; // zoom out degrees\nconst defaultHfov = 45;\n\nlet _hires = false;\nlet _resolution = 512; // higher numbers are slower - 512, 1024, 2048, 4096\nlet _currScene = 0;\nlet _ssCache;\nlet _pannellumViewer;\nlet _sceneOptions = {\n showFullscreenCtrl: false,\n autoLoad: true,\n compass: true,\n yaw: 0,\n minHfov: minHfov,\n maxHfov: maxHfov,\n hfov: defaultHfov,\n type: 'cubemap',\n cubeMap: []\n};\nlet _loadViewerPromise;\n\n\n/**\n * abortRequest().\n */\nfunction abortRequest(i) {\n i.abort();\n}\n\n/**\n * loadTiles() wraps the process of generating tiles and then fetching image points for each tile.\n */\nfunction loadTiles(which, url, projection, margin) {\n const tiles = tiler.margin(margin).getTiles(projection);\n\n // abort inflight requests that are no longer needed\n const cache = _ssCache[which];\n Object.keys(cache.inflight).forEach(k => {\n const wanted = tiles.find(tile => k.indexOf(tile.id + ',') === 0);\n if (!wanted) {\n abortRequest(cache.inflight[k]);\n delete cache.inflight[k];\n }\n });\n\n tiles.forEach(tile => loadNextTilePage(which, url, tile));\n}\n\n\n/**\n * loadNextTilePage() load data for the next tile page in line.\n */\nfunction loadNextTilePage(which, url, tile) {\n const cache = _ssCache[which];\n const nextPage = cache.nextPage[tile.id] || 0;\n const id = tile.id + ',' + String(nextPage);\n if (cache.loaded[id] || cache.inflight[id]) return;\n\n cache.inflight[id] = getBubbles(url, tile, response => {\n cache.loaded[id] = true;\n delete cache.inflight[id];\n if (!response) return;\n\n if (response.resourceSets[0].resources.length === maxResults) {\n // there are more bubbles than the response can fit: re-fetch using tile split into 4\n const split = tile.extent.split();\n loadNextTilePage(which, url, { id: tile.id + ',a', extent: split[0] });\n loadNextTilePage(which, url, { id: tile.id + ',b', extent: split[1] });\n loadNextTilePage(which, url, { id: tile.id + ',c', extent: split[2] });\n loadNextTilePage(which, url, { id: tile.id + ',d', extent: split[3] });\n }\n\n const features = response.resourceSets[0].resources.map(bubble => {\n const bubbleId = bubble.imageUrl;\n if (cache.points[bubbleId]) return null; // skip duplicates\n\n // workaround for https://github.com/openstreetmap/iD/issues/10341#issuecomment-2275724738\n const loc = [\n bubble.lon || bubble.longitude,\n bubble.lat || bubble.latitude\n ];\n const d = {\n service: 'photo',\n loc: loc,\n key: bubbleId,\n imageUrl: bubble.imageUrl\n .replace('{subdomain}', bubble.imageUrlSubdomains[0]),\n ca: bubble.he || bubble.heading,\n captured_at: bubble.vintageEnd,\n captured_by: 'microsoft',\n pano: true,\n sequenceKey: null\n };\n\n cache.points[bubbleId] = d;\n\n return {\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n };\n\n }).filter(Boolean);\n\n cache.rtree.load(features);\n\n if (which === 'bubbles') {\n dispatch.call('loadedImages');\n }\n });\n}\n\n\n/**\n * getBubbles() handles the request to the server for a tile extent of 'bubbles' (streetside image locations).\n */\nfunction getBubbles(url, tile, callback) {\n let rect = tile.extent.rectangle();\n let urlForRequest = url\n .replace('{key}', bubbleAppKey)\n .replace('{bbox}', [rect[1], rect[0], rect[3], rect[2]].join(','))\n .replace('{count}', maxResults);\n\n const controller = new AbortController();\n fetch(urlForRequest, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n }).then(function(result) {\n if (!result) {\n callback(null);\n }\n return callback(result || []);\n }).catch(function(err) {\n if (err.name === 'AbortError') {\n // ignore aborted requests, e.g. from duplicate requests while zooming/panning the map\n } else {\n throw new Error(err);\n }\n });\n return controller;\n }\n\n\n/**\n * loadImage()\n */\nfunction loadImage(imgInfo) {\n return new Promise(resolve => {\n let img = new Image();\n img.onload = () => {\n let canvas = document.getElementById('ideditor-canvas' + imgInfo.face);\n let ctx = canvas.getContext('2d');\n ctx.drawImage(img, imgInfo.x, imgInfo.y);\n resolve({ imgInfo: imgInfo, status: 'ok' });\n };\n img.onerror = () => {\n resolve({ data: imgInfo, status: 'error' });\n };\n img.setAttribute('crossorigin', '');\n img.src = imgInfo.url;\n });\n}\n\n\n/**\n * loadCanvas()\n */\nfunction loadCanvas(imageGroup) {\n return Promise.all(imageGroup.map(loadImage))\n .then((data) => {\n let canvas = document.getElementById('ideditor-canvas' + data[0].imgInfo.face);\n const which = { '01': 0, '02': 1, '03': 2, '10': 3, '11': 4, '12': 5 };\n let face = data[0].imgInfo.face;\n _sceneOptions.cubeMap[which[face]] = canvas.toDataURL('image/jpeg', 1.0);\n return { status: 'loadCanvas for face ' + data[0].imgInfo.face + 'ok'};\n });\n}\n\n\n/**\n * loadFaces()\n */\nfunction loadFaces(faceGroup) {\n return Promise.all(faceGroup.map(loadCanvas))\n .then(() => { return { status: 'loadFaces done' }; });\n}\n\n\nfunction setupCanvas(selection, reset) {\n if (reset) {\n selection.selectAll('#ideditor-stitcher-canvases')\n .remove();\n }\n\n // Add the Streetside working canvases. These are used for 'stitching', or combining,\n // multiple images for each of the six faces, before passing to the Pannellum control as DataUrls\n selection.selectAll('#ideditor-stitcher-canvases')\n .data([0])\n .enter()\n .append('div')\n .attr('id', 'ideditor-stitcher-canvases')\n .attr('display', 'none')\n .selectAll('canvas')\n .data(['canvas01', 'canvas02', 'canvas03', 'canvas10', 'canvas11', 'canvas12'])\n .enter()\n .append('canvas')\n .attr('id', d => 'ideditor-' + d)\n .attr('width', _resolution)\n .attr('height', _resolution);\n}\n\n\nfunction qkToXY(qk) {\n let x = 0;\n let y = 0;\n let scale = 256;\n for (let i = qk.length; i > 0; i--) {\n const key = qk[i-1];\n x += (+(key === '1' || key === '3')) * scale;\n y += (+(key === '2' || key === '3')) * scale;\n scale *= 2;\n }\n return [x, y];\n}\n\n\nfunction getQuadKeys() {\n let dim = _resolution / 256;\n let quadKeys;\n\n if (dim === 16) {\n quadKeys = [\n '0000','0001','0010','0011','0100','0101','0110','0111', '1000','1001','1010','1011','1100','1101','1110','1111',\n '0002','0003','0012','0013','0102','0103','0112','0113', '1002','1003','1012','1013','1102','1103','1112','1113',\n '0020','0021','0030','0031','0120','0121','0130','0131', '1020','1021','1030','1031','1120','1121','1130','1131',\n '0022','0023','0032','0033','0122','0123','0132','0133', '1022','1023','1032','1033','1122','1123','1132','1133',\n '0200','0201','0210','0211','0300','0301','0310','0311', '1200','1201','1210','1211','1300','1301','1310','1311',\n '0202','0203','0212','0213','0302','0303','0312','0313', '1202','1203','1212','1213','1302','1303','1312','1313',\n '0220','0221','0230','0231','0320','0321','0330','0331', '1220','1221','1230','1231','1320','1321','1330','1331',\n '0222','0223','0232','0233','0322','0323','0332','0333', '1222','1223','1232','1233','1322','1323','1332','1333',\n\n '2000','2001','2010','2011','2100','2101','2110','2111', '3000','3001','3010','3011','3100','3101','3110','3111',\n '2002','2003','2012','2013','2102','2103','2112','2113', '3002','3003','3012','3013','3102','3103','3112','3113',\n '2020','2021','2030','2031','2120','2121','2130','2131', '3020','3021','3030','3031','3120','3121','3130','3131',\n '2022','2023','2032','2033','2122','2123','2132','2133', '3022','3023','3032','3033','3122','3123','3132','3133',\n '2200','2201','2210','2211','2300','2301','2310','2311', '3200','3201','3210','3211','3300','3301','3310','3311',\n '2202','2203','2212','2213','2302','2303','2312','2313', '3202','3203','3212','3213','3302','3303','3312','3313',\n '2220','2221','2230','2231','2320','2321','2330','2331', '3220','3221','3230','3231','3320','3321','3330','3331',\n '2222','2223','2232','2233','2322','2323','2332','2333', '3222','3223','3232','3233','3322','3323','3332','3333'\n ];\n\n } else if (dim === 8) {\n quadKeys = [\n '000','001','010','011', '100','101','110','111',\n '002','003','012','013', '102','103','112','113',\n '020','021','030','031', '120','121','130','131',\n '022','023','032','033', '122','123','132','133',\n\n '200','201','210','211', '300','301','310','311',\n '202','203','212','213', '302','303','312','313',\n '220','221','230','231', '320','321','330','331',\n '222','223','232','233', '322','323','332','333'\n ];\n\n } else if (dim === 4) {\n quadKeys = [\n '00','01', '10','11',\n '02','03', '12','13',\n\n '20','21', '30','31',\n '22','23', '32','33'\n ];\n\n } else { // dim === 2\n quadKeys = [\n '0', '1',\n '2', '3'\n ];\n }\n\n return quadKeys;\n}\n\n\n\nexport default {\n /**\n * init() initialize streetside.\n */\n init: function() {\n if (!_ssCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n /**\n * reset() reset the cache.\n */\n reset: function() {\n if (_ssCache) {\n Object.values(_ssCache.bubbles.inflight).forEach(abortRequest);\n }\n\n _ssCache = {\n bubbles: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), points: {} },\n sequences: {}\n };\n },\n\n /**\n * bubbles()\n */\n bubbles: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _ssCache.bubbles.rtree);\n },\n\n\n cachedImage: function(imageKey) {\n return _ssCache.bubbles.points[imageKey];\n },\n\n\n sequences: function(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n let seen = {};\n let results = [];\n\n // all sequences for bubbles in viewport\n _ssCache.bubbles.rtree.search(bbox)\n .forEach(d => {\n const key = d.data.sequenceKey;\n if (key && !seen[key]) {\n seen[key] = true;\n results.push(_ssCache.sequences[key].geojson);\n }\n });\n\n return results;\n },\n\n\n /**\n * loadBubbles()\n */\n loadBubbles: function(projection, margin) {\n // by default: request 2 nearby tiles so we can connect sequences.\n if (margin === undefined) margin = 2;\n\n loadTiles('bubbles', streetsideApi, projection, margin);\n },\n\n\n viewer: function() {\n return _pannellumViewer;\n },\n\n\n initViewer: function () {\n if (!window.pannellum) return;\n if (_pannellumViewer) return;\n\n _currScene += 1;\n const sceneID = _currScene.toString();\n const options = {\n 'default': { firstScene: sceneID },\n scenes: {}\n };\n options.scenes[sceneID] = _sceneOptions;\n\n _pannellumViewer = window.pannellum.viewer('ideditor-viewer-streetside', options);\n },\n\n\n ensureViewerLoaded: function(context) {\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n // create ms-wrapper, a photo wrapper class\n let wrap = context.container().select('.photoviewer').selectAll('.ms-wrapper')\n .data([0]);\n\n // inject ms-wrapper into the photoviewer div\n // (used by all to house each custom photo viewer)\n let wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper ms-wrapper')\n .classed('hide', true);\n\n let that = this;\n\n let pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n // inject div to support streetside viewer (pannellum) and attribution line\n wrapEnter\n .append('div')\n .attr('id', 'ideditor-viewer-streetside')\n .on(pointerPrefix + 'down.streetside', () => {\n d3_select(window)\n .on(pointerPrefix + 'move.streetside', () => {\n dispatch.call('viewerChanged');\n }, true);\n })\n .on(pointerPrefix + 'up.streetside pointercancel.streetside', () => {\n d3_select(window)\n .on(pointerPrefix + 'move.streetside', null);\n\n // continue dispatching events for a few seconds, in case viewer has inertia.\n let t = d3_timer(elapsed => {\n dispatch.call('viewerChanged');\n if (elapsed > 2000) {\n t.stop();\n }\n });\n })\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n let controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls');\n\n controlsEnter\n .append('button')\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .on('click.forward', step(1))\n .text('\u25BA');\n\n\n // create working canvas for stitching together images\n wrap\n .merge(wrapEnter)\n .call(setupCanvas, true);\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.streetside', () => {\n if (_pannellumViewer) {\n _pannellumViewer.resize();\n }\n });\n\n _loadViewerPromise = new Promise((resolve, reject) => {\n\n let loadedCount = 0;\n function loaded() {\n loadedCount += 1;\n // wait until both files are loaded\n if (loadedCount === 2) resolve();\n }\n\n const head = d3_select('head');\n\n // load streetside pannellum viewer css\n head.selectAll('#ideditor-streetside-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-streetside-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(pannellumViewerCSS))\n .on('load.serviceStreetside', loaded)\n .on('error.serviceStreetside', function() {\n reject();\n });\n\n // load streetside pannellum viewer js\n head.selectAll('#ideditor-streetside-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-streetside-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(pannellumViewerJS))\n .on('load.serviceStreetside', loaded)\n .on('error.serviceStreetside', function() {\n reject();\n });\n })\n .catch(function() {\n _loadViewerPromise = null;\n });\n\n return _loadViewerPromise;\n\n function step(stepBy) {\n return () => {\n let viewer = context.container().select('.photoviewer');\n let selected = viewer.empty() ? undefined : viewer.datum();\n if (!selected) return;\n\n let nextID = (stepBy === 1 ? selected.ne : selected.pr);\n let yaw = _pannellumViewer.getYaw();\n let ca = selected.ca + yaw;\n let origin = selected.loc;\n\n // construct a search trapezoid pointing out from current bubble\n const meters = 35;\n let p1 = [\n origin[0] + geoMetersToLon(meters / 5, origin[1]),\n origin[1]\n ];\n let p2 = [\n origin[0] + geoMetersToLon(meters / 2, origin[1]),\n origin[1] + geoMetersToLat(meters)\n ];\n let p3 = [\n origin[0] - geoMetersToLon(meters / 2, origin[1]),\n origin[1] + geoMetersToLat(meters)\n ];\n let p4 = [\n origin[0] - geoMetersToLon(meters / 5, origin[1]),\n origin[1]\n ];\n\n let poly = [p1, p2, p3, p4, p1];\n\n // rotate it to face forward/backward\n let angle = (stepBy === 1 ? ca : ca + 180) * (Math.PI / 180);\n poly = geoRotate(poly, -angle, origin);\n\n let extent = poly.reduce((extent, point) => {\n return extent.extend(geoExtent(point));\n }, geoExtent());\n\n // find nearest other bubble in the search polygon\n let minDist = Infinity;\n _ssCache.bubbles.rtree.search(extent.bbox())\n .forEach(d => {\n if (d.data.key === selected.key) return;\n if (!geoPointInPolygon(d.data.loc, poly)) return;\n\n let dist = geoVecLength(d.data.loc, selected.loc);\n let theta = selected.ca - d.data.ca;\n let minTheta = Math.min(Math.abs(theta), 360 - Math.abs(theta));\n if (minTheta > 20) {\n dist += 5; // penalize distance if camera angles don't match\n }\n\n if (dist < minDist) {\n nextID = d.data.key;\n minDist = dist;\n }\n });\n\n let nextBubble = nextID && that.cachedImage(nextID);\n if (!nextBubble) return;\n\n context.map().centerEase(nextBubble.loc);\n\n that.selectImage(context, nextBubble.key)\n .yaw(yaw)\n .showViewer(context);\n };\n }\n },\n\n\n yaw: function(yaw) {\n if (typeof yaw !== 'number') return yaw;\n _sceneOptions.yaw = yaw;\n return this;\n },\n\n /**\n * showViewer()\n */\n showViewer: function(context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.ms-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap\n .classed('hide', false)\n .selectAll('.photo-wrapper.ms-wrapper')\n .classed('hide', false);\n }\n\n return this;\n },\n\n\n /**\n * hideViewer()\n */\n hideViewer: function (context) {\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n this.updateUrlImage(null);\n\n return this.setStyles(context, null, true);\n },\n\n\n /**\n * selectImage().\n */\n selectImage: function (context, key) {\n let that = this;\n\n let d = this.cachedImage(key);\n\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null, true);\n\n let wrap = context.container().select('.photoviewer .ms-wrapper');\n let attribution = wrap.selectAll('.photo-attribution').html('');\n\n wrap.selectAll('.pnlm-load-box') // display \"loading..\"\n .style('display', 'block');\n\n if (!d) return this;\n\n this.updateUrlImage(key);\n\n _sceneOptions.northOffset = d.ca;\n\n let line1 = attribution\n .append('div')\n .attr('class', 'attribution-row');\n\n const hiresDomId = utilUniqueDomId('streetside-hires');\n\n // Add hires checkbox\n let label = line1\n .append('label')\n .attr('for', hiresDomId)\n .attr('class', 'streetside-hires');\n\n label\n .append('input')\n .attr('type', 'checkbox')\n .attr('id', hiresDomId)\n .property('checked', _hires)\n .on('click', (d3_event) => {\n d3_event.stopPropagation();\n\n _hires = !_hires;\n _resolution = _hires ? 1024 : 512;\n wrap.call(setupCanvas, true);\n\n let viewstate = {\n yaw: _pannellumViewer.getYaw(),\n pitch: _pannellumViewer.getPitch(),\n hfov: _pannellumViewer.getHfov()\n };\n\n _sceneOptions = Object.assign(_sceneOptions, viewstate);\n that.selectImage(context, d.key)\n .showViewer(context);\n });\n\n label\n .append('span')\n .call(t.append('streetside.hires'));\n\n\n let captureInfo = line1\n .append('div')\n .attr('class', 'attribution-capture-info');\n\n // Add capture date\n if (d.captured_by) {\n const yyyy = (new Date()).getFullYear();\n\n captureInfo\n .append('a')\n .attr('class', 'captured_by')\n .attr('target', '_blank')\n .attr('href', 'https://www.microsoft.com/en-us/maps/streetside')\n .text('\u00A9' + yyyy + ' Microsoft');\n\n captureInfo\n .append('span')\n .text('|');\n }\n\n if (d.captured_at) {\n captureInfo\n .append('span')\n .attr('class', 'captured_at')\n .text(localeTimestamp(d.captured_at));\n }\n\n // Add image links\n let line2 = attribution\n .append('div')\n .attr('class', 'attribution-row');\n\n line2\n .append('a')\n .attr('class', 'image-view-link')\n .attr('target', '_blank')\n .attr('href', 'https://www.bing.com/maps?cp=' + d.loc[1] + '~' + d.loc[0] +\n '&lvl=17&dir=' + d.ca + '&style=x&v=2&sV=1')\n .call(t.append('streetside.view_on_bing'));\n\n line2\n .append('a')\n .attr('class', 'image-report-link')\n .attr('target', '_blank')\n .attr('href', 'https://www.bing.com/maps/privacyreport/streetsideprivacyreport?bubbleid=' +\n encodeURIComponent(d.key) + '&focus=photo&lat=' + d.loc[1] + '&lng=' + d.loc[0] + '&z=17')\n .call(t.append('streetside.report'));\n\n // Cubemap face code order matters here: front=01, right=02, back=03, left=10, up=11, down=12\n const faceKeys = ['01','02','03','10','11','12'];\n\n // Map images to cube faces\n let quadKeys = getQuadKeys();\n let faces = faceKeys.map((faceKey) => {\n return quadKeys.map((quadKey) => {\n const xy = qkToXY(quadKey);\n return {\n face: faceKey,\n url: d.imageUrl\n .replace('{faceId}', faceKey)\n .replace('{tileId}', quadKey),\n x: xy[0],\n y: xy[1]\n };\n });\n });\n\n loadFaces(faces)\n .then(function() {\n\n if (!_pannellumViewer) {\n that.initViewer();\n } else {\n // make a new scene\n _currScene += 1;\n let sceneID = _currScene.toString();\n _pannellumViewer\n .addScene(sceneID, _sceneOptions)\n .loadScene(sceneID);\n\n // remove previous scene\n if (_currScene > 2) {\n sceneID = (_currScene - 1).toString();\n _pannellumViewer\n .removeScene(sceneID);\n }\n }\n });\n\n return this;\n },\n\n\n getSequenceKeyForBubble: function(d) {\n return d && d.sequenceKey;\n },\n\n\n // Updates the currently highlighted sequence and selected bubble.\n // Reset is only necessary when interacting with the viewport because\n // this implicitly changes the currently selected bubble/sequence\n setStyles: function (context, hovered, reset) {\n if (reset) { // reset all layers\n context.container().selectAll('.viewfield-group')\n .classed('highlighted', false)\n .classed('hovered', false)\n .classed('currentView', false);\n\n context.container().selectAll('.sequence')\n .classed('highlighted', false)\n .classed('currentView', false);\n }\n\n let hoveredBubbleKey = hovered && hovered.key;\n let hoveredSequenceKey = this.getSequenceKeyForBubble(hovered);\n let hoveredSequence = hoveredSequenceKey && _ssCache.sequences[hoveredSequenceKey];\n let hoveredBubbleKeys = (hoveredSequence && hoveredSequence.bubbles.map(d => d.key)) || [];\n\n let viewer = context.container().select('.photoviewer');\n let selected = viewer.empty() ? undefined : viewer.datum();\n let selectedBubbleKey = selected && selected.key;\n let selectedSequenceKey = this.getSequenceKeyForBubble(selected);\n let selectedSequence = selectedSequenceKey && _ssCache.sequences[selectedSequenceKey];\n let selectedBubbleKeys = (selectedSequence && selectedSequence.bubbles.map(d => d.key)) || [];\n\n // highlight sibling viewfields on either the selected or the hovered sequences\n let highlightedBubbleKeys = utilArrayUnion(hoveredBubbleKeys, selectedBubbleKeys);\n\n context.container().selectAll('.layer-streetside-images .viewfield-group')\n .classed('highlighted', d => highlightedBubbleKeys.indexOf(d.key) !== -1)\n .classed('hovered', d => d.key === hoveredBubbleKey)\n .classed('currentView', d => d.key === selectedBubbleKey);\n\n context.container().selectAll('.layer-streetside-images .sequence')\n .classed('highlighted', d => d.properties.key === hoveredSequenceKey)\n .classed('currentView', d => d.properties.key === selectedSequenceKey);\n\n // update viewfields if needed\n context.container().selectAll('.layer-streetside-images .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n let d = this.parentNode.__data__;\n if (d.pano && d.key !== selectedBubbleKey) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n return this;\n },\n\n\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'streetside/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n\n /**\n * cache().\n */\n cache: function () {\n return _ssCache;\n }\n};\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { json as d3_json } from 'd3-fetch';\n\nimport { utilObjectOmit, utilQsString } from '../util';\nimport { localizer } from '../core/localizer';\nimport { allowUpperCaseTagValues } from '../osm/tags';\n\nimport { taginfoApiUrl } from '../../config/id.js';\n\nvar apibase = taginfoApiUrl;\nvar _inflight = {};\nvar _popularKeys = {};\n// manually exclude some additional keys \u2013 #5377, #7485, #10287, #11733\n// these will be returned by keys(), but taginfo will not be queried for values() requests\nvar _extraExcludedKeys = /^(addr:.+|postal_code|via|((int_|loc_|nat_|official_|old_|ref_|reg_|short_|full_|sorting_|alt_|artist_|long_|bridge:|tunnel:)?name(:left|:right)?(:[a-z]+)?))$/;\n\nvar _extraExcludedKeyNames = /^(hashtags?|created_by)$/;\n\nvar _taginfoCache = {};\n\nvar tag_sorts = {\n point: 'count_nodes',\n vertex: 'count_nodes',\n area: 'count_ways',\n line: 'count_ways'\n};\nvar tag_sort_members = {\n point: 'count_node_members',\n vertex: 'count_node_members',\n area: 'count_way_members',\n line: 'count_way_members',\n relation: 'count_relation_members'\n};\nvar tag_filters = {\n point: 'nodes',\n vertex: 'nodes',\n area: 'ways',\n line: 'ways'\n};\nvar tag_members_fractions = {\n point: 'count_node_members_fraction',\n vertex: 'count_node_members_fraction',\n area: 'count_way_members_fraction',\n line: 'count_way_members_fraction',\n relation: 'count_relation_members_fraction'\n};\n\n\nfunction sets(params, n, o) {\n if (params.geometry && o[params.geometry]) {\n params[n] = o[params.geometry];\n }\n return params;\n}\n\n\nfunction setFilter(params) {\n return sets(params, 'filter', tag_filters);\n}\n\n\nfunction setSort(params) {\n return sets(params, 'sortname', tag_sorts);\n}\n\n\nfunction setSortMembers(params) {\n return sets(params, 'sortname', tag_sort_members);\n}\n\n\nfunction clean(params) {\n return utilObjectOmit(params, ['geometry', 'debounce']);\n}\n\n\nfunction filterKeys(type) {\n var count_type = type ? 'count_' + type : 'count_all';\n return function(d) {\n return Number(d[count_type]) > 2500 || d.in_wiki;\n };\n}\n\n\nfunction filterMultikeys(prefix) {\n return function(d) {\n // d.key begins with prefix, and d.key contains no additional ':'s\n var re = new RegExp('^' + prefix + '(.*)$', 'i');\n var matches = d.key.match(re) || [];\n return (matches.length === 2 && matches[1].indexOf(':') === -1);\n };\n}\n\n\nfunction filterValues(allowUpperCase, key) {\n return function(d) {\n if (d.value.match(/[;,]/) !== null) return false; // exclude some punctuation\n if (!allowUpperCase &&\n !(key === 'type' && d.value === 'associatedStreet') &&\n d.value.match(/[A-Z*]/) !== null) return false; // exclude uppercase letters\n return d.count > 100; // exclude rare tags\n };\n}\n\n\nfunction filterRoles(geometry) {\n return function(d) {\n if (d.role === '') return false; // exclude empty role\n if (d.role.match(/[A-Z*;,]/) !== null) return false; // exclude uppercase letters and some punctuation\n return Number(d[tag_members_fractions[geometry]]) > 0.0;\n };\n}\n\n\nfunction valKey(d) {\n return {\n value: d.key,\n title: d.key\n };\n}\n\n\nfunction valKeyDescription(d) {\n var obj = {\n value: d.value,\n title: d.description || d.value\n };\n return obj;\n}\n\n\nfunction roleKey(d) {\n return {\n value: d.role,\n title: d.role\n };\n}\n\n\n// sort keys with ':' lower than keys without ':'\nfunction sortKeys(a, b) {\n return (a.key.indexOf(':') === -1 && b.key.indexOf(':') !== -1) ? -1\n : (a.key.indexOf(':') !== -1 && b.key.indexOf(':') === -1) ? 1\n : 0;\n}\n\n\nvar debouncedRequest = debounce(request, 300, { leading: false });\n\nfunction request(url, params, exactMatch, callback, loaded) {\n if (_inflight[url]) return;\n\n if (checkCache(url, params, exactMatch, callback)) return;\n\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, { signal: controller.signal })\n .then(function(result) {\n delete _inflight[url];\n if (loaded) loaded(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (loaded) loaded(err.message);\n });\n}\n\n\nfunction checkCache(url, params, exactMatch, callback) {\n var rp = params.rp || 25;\n var testQuery = params.query || '';\n var testUrl = url;\n\n do {\n var hit = _taginfoCache[testUrl];\n\n // exact match, or shorter match yielding fewer than max results (rp)\n if (hit && (url === testUrl || hit.length < rp)) {\n callback(null, hit);\n return true;\n }\n\n // don't try to shorten the query\n if (exactMatch || !testQuery.length) return false;\n\n // do shorten the query to see if we already have a cached result\n // that has returned fewer than max results (rp)\n testQuery = testQuery.slice(0, -1);\n testUrl = url.replace(/&query=(.*?)&/, '&query=' + testQuery + '&');\n } while (testQuery.length >= 0);\n\n return false;\n}\n\n\nexport default {\n\n init: function() {\n _inflight = {};\n _taginfoCache = {};\n _popularKeys = [];\n\n // Fetch popular keys. We'll exclude these from `values`\n // lookups because they stress taginfo, and they aren't likely\n // to yield meaningful autocomplete results.. see #3955\n var params = {\n rp: 100,\n sortname: 'values_all',\n sortorder: 'desc',\n page: 1,\n debounce: false,\n lang: localizer.languageCode()\n };\n this.keys(params, function(err, data) {\n if (err) return;\n data.forEach(function(d) {\n if (d.value === 'opening_hours') return; // exception\n _popularKeys[d.value] = true;\n });\n });\n },\n\n\n reset: function() {\n Object.values(_inflight).forEach(function(controller) { controller.abort(); });\n _inflight = {};\n },\n\n\n keys: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(params));\n params = Object.assign({\n rp: 10,\n sortname: 'count_all',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var url = apibase + 'keys/all?' + utilQsString(params);\n doRequest(url, params, false, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n var f = filterKeys(params.filter);\n var result = d.data.filter(f).filter(d => !_extraExcludedKeyNames.test(d.key)).sort(sortKeys).map(valKey);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n multikeys: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(params));\n params = Object.assign({\n rp: 25,\n sortname: 'count_all',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var prefix = params.query;\n var url = apibase + 'keys/all?' + utilQsString(params);\n doRequest(url, params, true, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n var f = filterMultikeys(prefix);\n var result = d.data.filter(f).map(valKey);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n values: function(params, callback) {\n // Exclude popular keys from values lookups.. see #3955\n var key = params.key;\n if (key && _popularKeys[key] === true || _extraExcludedKeys.test(key)) {\n callback(null, []);\n return;\n }\n\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(setFilter(params)));\n params = Object.assign({\n rp: 25,\n sortname: 'count_all',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var url = apibase + 'key/values?' + utilQsString(params);\n doRequest(url, params, false, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n // In most cases we prefer taginfo value results with lowercase letters.\n // A few OSM keys expect values to contain uppercase values (see #3377).\n // This is not an exhaustive list (e.g. `name` also has uppercase values)\n // but these are the fields where taginfo value lookup is most useful.\n var allowUpperCase = allowUpperCaseTagValues.test(params.key);\n var f = filterValues(allowUpperCase, params.key);\n\n var result = d.data.filter(f).map(valKeyDescription);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n roles: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n var geometry = params.geometry;\n params = clean(setSortMembers(params));\n params = Object.assign({\n rp: 25,\n sortname: 'count_all_members',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var url = apibase + 'relation/roles?' + utilQsString(params);\n doRequest(url, params, true, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n var f = filterRoles(geometry);\n var result = d.data.filter(f).map(roleKey);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n docs: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(params));\n\n var path = 'key/wiki_pages?';\n if (params.value) {\n path = 'tag/wiki_pages?';\n } else if (params.rtype) {\n path = 'relation/wiki_pages?';\n }\n\n var url = apibase + path + utilQsString(params);\n doRequest(url, params, true, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n _taginfoCache[url] = d.data;\n callback(null, d.data);\n }\n });\n },\n\n\n apibase: function(_) {\n if (!arguments.length) return apibase;\n apibase = _;\n return this;\n }\n\n};\n", "import {\n BBox,\n Feature,\n FeatureCollection,\n Geometry,\n GeometryCollection,\n GeometryObject,\n LineString,\n MultiLineString,\n MultiPoint,\n MultiPolygon,\n Point,\n Polygon,\n Position,\n GeoJsonProperties,\n} from \"geojson\";\n\nimport { Id } from \"./lib/geojson.js\";\nexport * from \"./lib/geojson.js\";\n\n/**\n * @module helpers\n */\n\n// TurfJS Combined Types\nexport type Coord = Feature | Point | Position;\n\n/**\n * Linear measurement units.\n *\n * ⚠️ Warning. Be aware of the implications of using radian or degree units to\n * measure distance. The distance represented by a degree of longitude *varies*\n * depending on latitude.\n *\n * See https://www.thoughtco.com/degree-of-latitude-and-longitude-distance-4070616\n * for an illustration of this behaviour.\n *\n * @typedef\n */\nexport type Units =\n | \"meters\"\n | \"metres\"\n | \"millimeters\"\n | \"millimetres\"\n | \"centimeters\"\n | \"centimetres\"\n | \"kilometers\"\n | \"kilometres\"\n | \"miles\"\n | \"nauticalmiles\"\n | \"inches\"\n | \"yards\"\n | \"feet\"\n | \"radians\"\n | \"degrees\";\n\n/**\n * Area measurement units.\n *\n * @typedef\n */\nexport type AreaUnits =\n | Exclude\n | \"acres\"\n | \"hectares\";\n\n/**\n * Grid types.\n *\n * @typedef\n */\nexport type Grid = \"point\" | \"square\" | \"hex\" | \"triangle\";\n\n/**\n * Shorthand corner identifiers.\n *\n * @typedef\n */\nexport type Corners = \"sw\" | \"se\" | \"nw\" | \"ne\" | \"center\" | \"centroid\";\n\n/**\n * Geometries made up of lines i.e. lines and polygons.\n *\n * @typedef\n */\nexport type Lines = LineString | MultiLineString | Polygon | MultiPolygon;\n\n/**\n * Convenience type for all possible GeoJSON.\n *\n * @typedef\n */\nexport type AllGeoJSON =\n | Feature\n | FeatureCollection\n | Geometry\n | GeometryCollection;\n\n/**\n * The Earth radius in meters. Used by Turf modules that model the Earth as a sphere. The {@link https://en.wikipedia.org/wiki/Earth_radius#Arithmetic_mean_radius mean radius} was selected because it is {@link https://rosettacode.org/wiki/Haversine_formula#:~:text=This%20value%20is%20recommended recommended } by the Haversine formula (used by turf/distance) to reduce error.\n *\n * @constant\n */\nexport const earthRadius = 6371008.8;\n\n/**\n * Unit of measurement factors based on earthRadius.\n *\n * Keys are the name of the unit, values are the number of that unit in a single radian\n *\n * @constant\n */\nexport const factors: Record = {\n centimeters: earthRadius * 100,\n centimetres: earthRadius * 100,\n degrees: 360 / (2 * Math.PI),\n feet: earthRadius * 3.28084,\n inches: earthRadius * 39.37,\n kilometers: earthRadius / 1000,\n kilometres: earthRadius / 1000,\n meters: earthRadius,\n metres: earthRadius,\n miles: earthRadius / 1609.344,\n millimeters: earthRadius * 1000,\n millimetres: earthRadius * 1000,\n nauticalmiles: earthRadius / 1852,\n radians: 1,\n yards: earthRadius * 1.0936,\n};\n\n/**\n\n * Area of measurement factors based on 1 square meter.\n *\n * @constant\n */\nexport const areaFactors: Record = {\n acres: 0.000247105,\n centimeters: 10000,\n centimetres: 10000,\n feet: 10.763910417,\n hectares: 0.0001,\n inches: 1550.003100006,\n kilometers: 0.000001,\n kilometres: 0.000001,\n meters: 1,\n metres: 1,\n miles: 3.86e-7,\n nauticalmiles: 2.9155334959812285e-7,\n millimeters: 1000000,\n millimetres: 1000000,\n yards: 1.195990046,\n};\n\n/**\n * Wraps a GeoJSON {@link Geometry} in a GeoJSON {@link Feature}.\n *\n * @function\n * @param {GeometryObject} geometry input geometry\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a GeoJSON Feature\n * @example\n * var geometry = {\n * \"type\": \"Point\",\n * \"coordinates\": [110, 50]\n * };\n *\n * var feature = turf.feature(geometry);\n *\n * //=feature\n */\nexport function feature<\n G extends GeometryObject = Geometry,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geom: G | null,\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const feat: any = { type: \"Feature\" };\n if (options.id === 0 || options.id) {\n feat.id = options.id;\n }\n if (options.bbox) {\n feat.bbox = options.bbox;\n }\n feat.properties = properties || {};\n feat.geometry = geom;\n return feat;\n}\n\n/**\n * Creates a GeoJSON {@link Geometry} from a Geometry string type & coordinates.\n * For GeometryCollection type use `helpers.geometryCollection`\n *\n * @function\n * @param {(\"Point\" | \"LineString\" | \"Polygon\" | \"MultiPoint\" | \"MultiLineString\" | \"MultiPolygon\")} type Geometry Type\n * @param {Array} coordinates Coordinates\n * @param {Object} [options={}] Optional Parameters\n * @returns {Geometry} a GeoJSON Geometry\n * @example\n * var type = \"Point\";\n * var coordinates = [110, 50];\n * var geometry = turf.geometry(type, coordinates);\n * // => geometry\n */\nexport function geometry<\n T extends\n | \"Point\"\n | \"LineString\"\n | \"Polygon\"\n | \"MultiPoint\"\n | \"MultiLineString\"\n | \"MultiPolygon\",\n>(\n type: T,\n coordinates: any[],\n _options: Record = {}\n): Extract {\n switch (type) {\n case \"Point\":\n return point(coordinates).geometry as Extract;\n case \"LineString\":\n return lineString(coordinates).geometry as Extract;\n case \"Polygon\":\n return polygon(coordinates).geometry as Extract;\n case \"MultiPoint\":\n return multiPoint(coordinates).geometry as Extract;\n case \"MultiLineString\":\n return multiLineString(coordinates).geometry as Extract<\n Geometry,\n { type: T }\n >;\n case \"MultiPolygon\":\n return multiPolygon(coordinates).geometry as Extract<\n Geometry,\n { type: T }\n >;\n default:\n throw new Error(type + \" is invalid\");\n }\n}\n\n/**\n * Creates a {@link Point} {@link Feature} from a Position.\n *\n * @function\n * @param {Position} coordinates longitude, latitude position (each in decimal degrees)\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a Point feature\n * @example\n * var point = turf.point([-75.343, 39.984]);\n *\n * //=point\n */\nexport function point

    (\n coordinates: Position,\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n if (!coordinates) {\n throw new Error(\"coordinates is required\");\n }\n if (!Array.isArray(coordinates)) {\n throw new Error(\"coordinates must be an Array\");\n }\n if (coordinates.length < 2) {\n throw new Error(\"coordinates must be at least 2 numbers long\");\n }\n if (!isNumber(coordinates[0]) || !isNumber(coordinates[1])) {\n throw new Error(\"coordinates must contain numbers\");\n }\n\n const geom: Point = {\n type: \"Point\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Point} {@link FeatureCollection} from an Array of Point coordinates.\n *\n * @function\n * @param {Position[]} coordinates an array of Points\n * @param {GeoJsonProperties} [properties={}] Translate these properties to each Feature\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north]\n * associated with the FeatureCollection\n * @param {Id} [options.id] Identifier associated with the FeatureCollection\n * @returns {FeatureCollection} Point Feature\n * @example\n * var points = turf.points([\n * [-75, 39],\n * [-80, 45],\n * [-78, 50]\n * ]);\n *\n * //=points\n */\nexport function points

    (\n coordinates: Position[],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n return featureCollection(\n coordinates.map((coords) => {\n return point(coords, properties);\n }),\n options\n );\n}\n\n/**\n * Creates a {@link Polygon} {@link Feature} from an Array of LinearRings.\n *\n * @function\n * @param {Position[][]} coordinates an array of LinearRings\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} Polygon Feature\n * @example\n * var polygon = turf.polygon([[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]], { name: 'poly1' });\n *\n * //=polygon\n */\nexport function polygon

    (\n coordinates: Position[][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n for (const ring of coordinates) {\n if (ring.length < 4) {\n throw new Error(\n \"Each LinearRing of a Polygon must have 4 or more Positions.\"\n );\n }\n\n if (ring[ring.length - 1].length !== ring[0].length) {\n throw new Error(\"First and last Position are not equivalent.\");\n }\n\n for (let j = 0; j < ring[ring.length - 1].length; j++) {\n // Check if first point of Polygon contains two numbers\n if (ring[ring.length - 1][j] !== ring[0][j]) {\n throw new Error(\"First and last Position are not equivalent.\");\n }\n }\n }\n const geom: Polygon = {\n type: \"Polygon\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Polygon} {@link FeatureCollection} from an Array of Polygon coordinates.\n *\n * @function\n * @param {Position[][][]} coordinates an array of Polygon coordinates\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the FeatureCollection\n * @returns {FeatureCollection} Polygon FeatureCollection\n * @example\n * var polygons = turf.polygons([\n * [[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]],\n * [[[-15, 42], [-14, 46], [-12, 41], [-17, 44], [-15, 42]]],\n * ]);\n *\n * //=polygons\n */\nexport function polygons

    (\n coordinates: Position[][][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n return featureCollection(\n coordinates.map((coords) => {\n return polygon(coords, properties);\n }),\n options\n );\n}\n\n/**\n * Creates a {@link LineString} {@link Feature} from an Array of Positions.\n *\n * @function\n * @param {Position[]} coordinates an array of Positions\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} LineString Feature\n * @example\n * var linestring1 = turf.lineString([[-24, 63], [-23, 60], [-25, 65], [-20, 69]], {name: 'line 1'});\n * var linestring2 = turf.lineString([[-14, 43], [-13, 40], [-15, 45], [-10, 49]], {name: 'line 2'});\n *\n * //=linestring1\n * //=linestring2\n */\nexport function lineString

    (\n coordinates: Position[],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n if (coordinates.length < 2) {\n throw new Error(\"coordinates must be an array of two or more positions\");\n }\n const geom: LineString = {\n type: \"LineString\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link LineString} {@link FeatureCollection} from an Array of LineString coordinates.\n *\n * @function\n * @param {Position[][]} coordinates an array of LinearRings\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north]\n * associated with the FeatureCollection\n * @param {Id} [options.id] Identifier associated with the FeatureCollection\n * @returns {FeatureCollection} LineString FeatureCollection\n * @example\n * var linestrings = turf.lineStrings([\n * [[-24, 63], [-23, 60], [-25, 65], [-20, 69]],\n * [[-14, 43], [-13, 40], [-15, 45], [-10, 49]]\n * ]);\n *\n * //=linestrings\n */\nexport function lineStrings

    (\n coordinates: Position[][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n return featureCollection(\n coordinates.map((coords) => {\n return lineString(coords, properties);\n }),\n options\n );\n}\n\n/**\n * Takes one or more {@link Feature|Features} and creates a {@link FeatureCollection}.\n *\n * @function\n * @param {Array>} features input features\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {FeatureCollection} FeatureCollection of Features\n * @example\n * var locationA = turf.point([-75.343, 39.984], {name: 'Location A'});\n * var locationB = turf.point([-75.833, 39.284], {name: 'Location B'});\n * var locationC = turf.point([-75.534, 39.123], {name: 'Location C'});\n *\n * var collection = turf.featureCollection([\n * locationA,\n * locationB,\n * locationC\n * ]);\n *\n * //=collection\n */\nexport function featureCollection<\n G extends GeometryObject = Geometry,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n features: Array>,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n const fc: any = { type: \"FeatureCollection\" };\n if (options.id) {\n fc.id = options.id;\n }\n if (options.bbox) {\n fc.bbox = options.bbox;\n }\n fc.features = features;\n return fc;\n}\n\n/**\n * Creates a {@link Feature}<{@link MultiLineString}> based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Position[][]} coordinates an array of LineStrings\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a MultiLineString feature\n * @throws {Error} if no coordinates are passed\n * @example\n * var multiLine = turf.multiLineString([[[0,0],[10,10]]]);\n *\n * //=multiLine\n */\nexport function multiLineString<\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n coordinates: Position[][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const geom: MultiLineString = {\n type: \"MultiLineString\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Feature}<{@link MultiPoint}> based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Position[]} coordinates an array of Positions\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a MultiPoint feature\n * @throws {Error} if no coordinates are passed\n * @example\n * var multiPt = turf.multiPoint([[0,0],[10,10]]);\n *\n * //=multiPt\n */\nexport function multiPoint

    (\n coordinates: Position[],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const geom: MultiPoint = {\n type: \"MultiPoint\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Feature}<{@link MultiPolygon}> based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Position[][][]} coordinates an array of Polygons\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a multipolygon feature\n * @throws {Error} if no coordinates are passed\n * @example\n * var multiPoly = turf.multiPolygon([[[[0,0],[0,10],[10,10],[10,0],[0,0]]]]);\n *\n * //=multiPoly\n *\n */\nexport function multiPolygon

    (\n coordinates: Position[][][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const geom: MultiPolygon = {\n type: \"MultiPolygon\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a Feature based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Array} geometries an array of GeoJSON Geometries\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a GeoJSON GeometryCollection Feature\n * @example\n * var pt = turf.geometry(\"Point\", [100, 0]);\n * var line = turf.geometry(\"LineString\", [[101, 0], [102, 1]]);\n * var collection = turf.geometryCollection([pt, line]);\n *\n * // => collection\n */\nexport function geometryCollection<\n G extends\n | Point\n | LineString\n | Polygon\n | MultiPoint\n | MultiLineString\n | MultiPolygon,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geometries: Array,\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature, P> {\n const geom: GeometryCollection = {\n type: \"GeometryCollection\",\n geometries,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Round number to precision\n *\n * @function\n * @param {number} num Number\n * @param {number} [precision=0] Precision\n * @returns {number} rounded number\n * @example\n * turf.round(120.4321)\n * //=120\n *\n * turf.round(120.4321, 2)\n * //=120.43\n */\nexport function round(num: number, precision = 0): number {\n if (precision && !(precision >= 0)) {\n throw new Error(\"precision must be a positive number\");\n }\n const multiplier = Math.pow(10, precision || 0);\n return Math.round(num * multiplier) / multiplier;\n}\n\n/**\n * Convert a distance measurement (assuming a spherical Earth) from radians to a more friendly unit.\n * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet\n *\n * @function\n * @param {number} radians in radians across the sphere\n * @param {Units} [units=\"kilometers\"] can be degrees, radians, miles, inches, yards, metres,\n * meters, kilometres, kilometers.\n * @returns {number} distance\n */\nexport function radiansToLength(\n radians: number,\n units: Units = \"kilometers\"\n): number {\n const factor = factors[units];\n if (!factor) {\n throw new Error(units + \" units is invalid\");\n }\n return radians * factor;\n}\n\n/**\n * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into radians\n * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet\n *\n * @function\n * @param {number} distance in real units\n * @param {Units} [units=\"kilometers\"] can be degrees, radians, miles, inches, yards, metres,\n * meters, kilometres, kilometers.\n * @returns {number} radians\n */\nexport function lengthToRadians(\n distance: number,\n units: Units = \"kilometers\"\n): number {\n const factor = factors[units];\n if (!factor) {\n throw new Error(units + \" units is invalid\");\n }\n return distance / factor;\n}\n\n/**\n * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into degrees\n * Valid units: miles, nauticalmiles, inches, yards, meters, metres, centimeters, kilometres, feet\n *\n * @function\n * @param {number} distance in real units\n * @param {Units} [units=\"kilometers\"] can be degrees, radians, miles, inches, yards, metres,\n * meters, kilometres, kilometers.\n * @returns {number} degrees\n */\nexport function lengthToDegrees(distance: number, units?: Units): number {\n return radiansToDegrees(lengthToRadians(distance, units));\n}\n\n/**\n * Converts any bearing angle from the north line direction (positive clockwise)\n * and returns an angle between 0-360 degrees (positive clockwise), 0 being the north line\n *\n * @function\n * @param {number} bearing angle, between -180 and +180 degrees\n * @returns {number} angle between 0 and 360 degrees\n */\nexport function bearingToAzimuth(bearing: number): number {\n let angle = bearing % 360;\n if (angle < 0) {\n angle += 360;\n }\n return angle;\n}\n\n/**\n * Converts any azimuth angle from the north line direction (positive clockwise)\n * and returns an angle between -180 and +180 degrees (positive clockwise), 0 being the north line\n *\n * @function\n * @param {number} angle between 0 and 360 degrees\n * @returns {number} bearing between -180 and +180 degrees\n */\nexport function azimuthToBearing(angle: number): number {\n // Ignore full revolutions (multiples of 360)\n angle = angle % 360;\n\n if (angle > 180) {\n return angle - 360;\n } else if (angle < -180) {\n return angle + 360;\n }\n\n return angle;\n}\n\n/**\n * Converts an angle in radians to degrees\n *\n * @function\n * @param {number} radians angle in radians\n * @returns {number} degrees between 0 and 360 degrees\n */\nexport function radiansToDegrees(radians: number): number {\n // % (2 * Math.PI) radians in case someone passes value > 2π\n const normalisedRadians = radians % (2 * Math.PI);\n return (normalisedRadians * 180) / Math.PI;\n}\n\n/**\n * Converts an angle in degrees to radians\n *\n * @function\n * @param {number} degrees angle between 0 and 360 degrees\n * @returns {number} angle in radians\n */\nexport function degreesToRadians(degrees: number): number {\n // % 360 degrees in case someone passes value > 360\n const normalisedDegrees = degrees % 360;\n return (normalisedDegrees * Math.PI) / 180;\n}\n\n/**\n * Converts a length from one unit to another.\n *\n * @function\n * @param {number} length Length to be converted\n * @param {Units} [originalUnit=\"kilometers\"] Input length unit\n * @param {Units} [finalUnit=\"kilometers\"] Returned length unit\n * @returns {number} The converted length\n */\nexport function convertLength(\n length: number,\n originalUnit: Units = \"kilometers\",\n finalUnit: Units = \"kilometers\"\n): number {\n if (!(length >= 0)) {\n throw new Error(\"length must be a positive number\");\n }\n return radiansToLength(lengthToRadians(length, originalUnit), finalUnit);\n}\n\n/**\n * Converts an area from one unit to another.\n *\n * @function\n * @param {number} area Area to be converted\n * @param {AreaUnits} [originalUnit=\"meters\"] Input area unit\n * @param {AreaUnits} [finalUnit=\"kilometers\"] Returned area unit\n * @returns {number} The converted length\n */\nexport function convertArea(\n area: number,\n originalUnit: AreaUnits = \"meters\",\n finalUnit: AreaUnits = \"kilometers\"\n): number {\n if (!(area >= 0)) {\n throw new Error(\"area must be a positive number\");\n }\n\n const startFactor = areaFactors[originalUnit];\n if (!startFactor) {\n throw new Error(\"invalid original units\");\n }\n\n const finalFactor = areaFactors[finalUnit];\n if (!finalFactor) {\n throw new Error(\"invalid final units\");\n }\n\n return (area / startFactor) * finalFactor;\n}\n\n/**\n * isNumber\n *\n * @function\n * @param {any} num Number to validate\n * @returns {boolean} true/false\n * @example\n * turf.isNumber(123)\n * //=true\n * turf.isNumber('foo')\n * //=false\n */\nexport function isNumber(num: any): boolean {\n return !isNaN(num) && num !== null && !Array.isArray(num);\n}\n\n/**\n * isObject\n *\n * @function\n * @param {any} input variable to validate\n * @returns {boolean} true/false, including false for Arrays and Functions\n * @example\n * turf.isObject({elevation: 10})\n * //=true\n * turf.isObject('foo')\n * //=false\n */\nexport function isObject(input: any): boolean {\n return input !== null && typeof input === \"object\" && !Array.isArray(input);\n}\n\n/**\n * Validate BBox\n *\n * @private\n * @param {any} bbox BBox to validate\n * @returns {void}\n * @throws {Error} if BBox is not valid\n * @example\n * validateBBox([-180, -40, 110, 50])\n * //=OK\n * validateBBox([-180, -40])\n * //=Error\n * validateBBox('Foo')\n * //=Error\n * validateBBox(5)\n * //=Error\n * validateBBox(null)\n * //=Error\n * validateBBox(undefined)\n * //=Error\n */\nexport function validateBBox(bbox: any): void {\n if (!bbox) {\n throw new Error(\"bbox is required\");\n }\n if (!Array.isArray(bbox)) {\n throw new Error(\"bbox must be an Array\");\n }\n if (bbox.length !== 4 && bbox.length !== 6) {\n throw new Error(\"bbox must be an Array of 4 or 6 numbers\");\n }\n bbox.forEach((num) => {\n if (!isNumber(num)) {\n throw new Error(\"bbox must only contain numbers\");\n }\n });\n}\n\n/**\n * Validate Id\n *\n * @private\n * @param {any} id Id to validate\n * @returns {void}\n * @throws {Error} if Id is not valid\n * @example\n * validateId([-180, -40, 110, 50])\n * //=Error\n * validateId([-180, -40])\n * //=Error\n * validateId('Foo')\n * //=OK\n * validateId(5)\n * //=OK\n * validateId(null)\n * //=Error\n * validateId(undefined)\n * //=Error\n */\nexport function validateId(id: any): void {\n if (!id) {\n throw new Error(\"id is required\");\n }\n if ([\"string\", \"number\"].indexOf(typeof id) === -1) {\n throw new Error(\"id must be a number or a string\");\n }\n}\n", "import {\n Feature,\n FeatureCollection,\n Geometry,\n LineString,\n MultiPoint,\n MultiLineString,\n MultiPolygon,\n Point,\n Polygon,\n} from \"geojson\";\nimport { isNumber } from \"@turf/helpers\";\n\n/**\n * Unwrap a coordinate from a Point Feature, Geometry or a single coordinate.\n *\n * @function\n * @param {Array|Geometry|Feature} coord GeoJSON Point or an Array of numbers\n * @returns {Array} coordinates\n * @example\n * var pt = turf.point([10, 10]);\n *\n * var coord = turf.getCoord(pt);\n * //= [10, 10]\n */\nfunction getCoord(coord: Feature | Point | number[]): number[] {\n if (!coord) {\n throw new Error(\"coord is required\");\n }\n\n if (!Array.isArray(coord)) {\n if (\n coord.type === \"Feature\" &&\n coord.geometry !== null &&\n coord.geometry.type === \"Point\"\n ) {\n return [...coord.geometry.coordinates];\n }\n if (coord.type === \"Point\") {\n return [...coord.coordinates];\n }\n }\n if (\n Array.isArray(coord) &&\n coord.length >= 2 &&\n !Array.isArray(coord[0]) &&\n !Array.isArray(coord[1])\n ) {\n return [...coord];\n }\n\n throw new Error(\"coord must be GeoJSON Point or an Array of numbers\");\n}\n\n/**\n * Unwrap coordinates from a Feature, Geometry Object or an Array\n *\n * @function\n * @param {Array|Geometry|Feature} coords Feature, Geometry Object or an Array\n * @returns {Array} coordinates\n * @example\n * var poly = turf.polygon([[[119.32, -8.7], [119.55, -8.69], [119.51, -8.54], [119.32, -8.7]]]);\n *\n * var coords = turf.getCoords(poly);\n * //= [[[119.32, -8.7], [119.55, -8.69], [119.51, -8.54], [119.32, -8.7]]]\n */\nfunction getCoords<\n G extends\n | Point\n | LineString\n | Polygon\n | MultiPoint\n | MultiLineString\n | MultiPolygon,\n>(coords: any[] | Feature | G): any[] {\n if (Array.isArray(coords)) {\n return coords;\n }\n\n // Feature\n if (coords.type === \"Feature\") {\n if (coords.geometry !== null) {\n return coords.geometry.coordinates;\n }\n } else {\n // Geometry\n if (coords.coordinates) {\n return coords.coordinates;\n }\n }\n\n throw new Error(\n \"coords must be GeoJSON Feature, Geometry Object or an Array\"\n );\n}\n\n/**\n * Checks if coordinates contains a number\n *\n * @function\n * @param {Array} coordinates GeoJSON Coordinates\n * @returns {boolean} true if Array contains a number\n */\nfunction containsNumber(coordinates: any[]): boolean {\n if (\n coordinates.length > 1 &&\n isNumber(coordinates[0]) &&\n isNumber(coordinates[1])\n ) {\n return true;\n }\n\n if (Array.isArray(coordinates[0]) && coordinates[0].length) {\n return containsNumber(coordinates[0]);\n }\n throw new Error(\"coordinates must only contain numbers\");\n}\n\n/**\n * Enforce expectations about types of GeoJSON objects for Turf.\n *\n * @function\n * @param {GeoJSON} value any GeoJSON object\n * @param {string} type expected GeoJSON type\n * @param {string} name name of calling function\n * @throws {Error} if value is not the expected type.\n */\nfunction geojsonType(value: any, type: string, name: string): void {\n if (!type || !name) {\n throw new Error(\"type and name required\");\n }\n\n if (!value || value.type !== type) {\n throw new Error(\n \"Invalid input to \" +\n name +\n \": must be a \" +\n type +\n \", given \" +\n value.type\n );\n }\n}\n\n/**\n * Enforce expectations about types of {@link Feature} inputs for Turf.\n * Internally this uses {@link geojsonType} to judge geometry types.\n *\n * @function\n * @param {Feature} feature a feature with an expected geometry type\n * @param {string} type expected GeoJSON type\n * @param {string} name name of calling function\n * @throws {Error} error if value is not the expected type.\n */\nfunction featureOf(feature: Feature, type: string, name: string): void {\n if (!feature) {\n throw new Error(\"No feature passed\");\n }\n if (!name) {\n throw new Error(\".featureOf() requires a name\");\n }\n if (!feature || feature.type !== \"Feature\" || !feature.geometry) {\n throw new Error(\n \"Invalid input to \" + name + \", Feature with geometry required\"\n );\n }\n if (!feature.geometry || feature.geometry.type !== type) {\n throw new Error(\n \"Invalid input to \" +\n name +\n \": must be a \" +\n type +\n \", given \" +\n feature.geometry.type\n );\n }\n}\n\n/**\n * Enforce expectations about types of {@link FeatureCollection} inputs for Turf.\n * Internally this uses {@link geojsonType} to judge geometry types.\n *\n * @function\n * @param {FeatureCollection} featureCollection a FeatureCollection for which features will be judged\n * @param {string} type expected GeoJSON type\n * @param {string} name name of calling function\n * @throws {Error} if value is not the expected type.\n */\nfunction collectionOf(\n featureCollection: FeatureCollection,\n type: string,\n name: string\n) {\n if (!featureCollection) {\n throw new Error(\"No featureCollection passed\");\n }\n if (!name) {\n throw new Error(\".collectionOf() requires a name\");\n }\n if (!featureCollection || featureCollection.type !== \"FeatureCollection\") {\n throw new Error(\n \"Invalid input to \" + name + \", FeatureCollection required\"\n );\n }\n for (const feature of featureCollection.features) {\n if (!feature || feature.type !== \"Feature\" || !feature.geometry) {\n throw new Error(\n \"Invalid input to \" + name + \", Feature with geometry required\"\n );\n }\n if (!feature.geometry || feature.geometry.type !== type) {\n throw new Error(\n \"Invalid input to \" +\n name +\n \": must be a \" +\n type +\n \", given \" +\n feature.geometry.type\n );\n }\n }\n}\n\n/**\n * Get Geometry from Feature or Geometry Object\n *\n * @param {Feature|Geometry} geojson GeoJSON Feature or Geometry Object\n * @returns {Geometry|null} GeoJSON Geometry Object\n * @throws {Error} if geojson is not a Feature or Geometry Object\n * @example\n * var point = {\n * \"type\": \"Feature\",\n * \"properties\": {},\n * \"geometry\": {\n * \"type\": \"Point\",\n * \"coordinates\": [110, 40]\n * }\n * }\n * var geom = turf.getGeom(point)\n * //={\"type\": \"Point\", \"coordinates\": [110, 40]}\n */\nfunction getGeom(geojson: Feature | G): G {\n if (geojson.type === \"Feature\") {\n return geojson.geometry;\n }\n return geojson;\n}\n\n/**\n * Get GeoJSON object's type, Geometry type is prioritize.\n *\n * @param {GeoJSON} geojson GeoJSON object\n * @param {string} [name=\"geojson\"] name of the variable to display in error message (unused)\n * @returns {string} GeoJSON type\n * @example\n * var point = {\n * \"type\": \"Feature\",\n * \"properties\": {},\n * \"geometry\": {\n * \"type\": \"Point\",\n * \"coordinates\": [110, 40]\n * }\n * }\n * var geom = turf.getType(point)\n * //=\"Point\"\n */\nfunction getType(\n geojson: Feature | FeatureCollection | Geometry,\n _name?: string\n): string {\n if (geojson.type === \"FeatureCollection\") {\n return \"FeatureCollection\";\n }\n if (geojson.type === \"GeometryCollection\") {\n return \"GeometryCollection\";\n }\n if (geojson.type === \"Feature\" && geojson.geometry !== null) {\n return geojson.geometry.type;\n }\n return geojson.type;\n}\n\nexport {\n getCoord,\n getCoords,\n containsNumber,\n geojsonType,\n featureOf,\n collectionOf,\n getGeom,\n getType,\n};\n// No default export!\n", "import {\n BBox,\n Feature,\n LineString,\n MultiLineString,\n MultiPolygon,\n GeoJsonProperties,\n Polygon,\n} from \"geojson\";\n\nimport {\n lineString,\n multiLineString,\n multiPolygon,\n polygon,\n} from \"@turf/helpers\";\nimport { getGeom } from \"@turf/invariant\";\nimport { lineclip, polygonclip } from \"./lib/lineclip.js\";\n\n/**\n * Takes a {@link Feature} and a bbox and clips the feature to the bbox using\n * [lineclip](https://github.com/mapbox/lineclip).\n * May result in degenerate edges when clipping Polygons.\n *\n * @function\n * @param {Feature} feature feature to clip to the bbox\n * @param {BBox} bbox extent in [minX, minY, maxX, maxY] order\n * @returns {Feature} clipped Feature\n * @example\n * var bbox = [0, 0, 10, 10];\n * var poly = turf.polygon([[[2, 2], [8, 4], [12, 8], [3, 7], [2, 2]]]);\n *\n * var clipped = turf.bboxClip(poly, bbox);\n *\n * //addToMap\n * var addToMap = [bbox, poly, clipped]\n */\nfunction bboxClip<\n G extends Polygon | MultiPolygon | LineString | MultiLineString,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(feature: Feature | G, bbox: BBox) {\n const geom = getGeom(feature);\n const type = geom.type;\n const properties = feature.type === \"Feature\" ? feature.properties : {};\n let coords: any[] = geom.coordinates;\n\n switch (type) {\n case \"LineString\":\n case \"MultiLineString\": {\n const lines: any[] = [];\n if (type === \"LineString\") {\n coords = [coords];\n }\n coords.forEach((line) => {\n lineclip(line, bbox, lines);\n });\n if (lines.length === 1) {\n return lineString(lines[0], properties);\n }\n return multiLineString(lines, properties);\n }\n case \"Polygon\":\n return polygon(clipPolygon(coords, bbox), properties);\n case \"MultiPolygon\":\n return multiPolygon(\n coords.map((poly) => {\n return clipPolygon(poly, bbox);\n }),\n properties\n );\n default:\n throw new Error(\"geometry \" + type + \" not supported\");\n }\n}\n\nfunction clipPolygon(rings: number[][][], bbox: BBox) {\n const outRings = [];\n for (const ring of rings) {\n const clipped = polygonclip(ring, bbox);\n if (clipped.length > 0) {\n if (\n clipped[0][0] !== clipped[clipped.length - 1][0] ||\n clipped[0][1] !== clipped[clipped.length - 1][1]\n ) {\n clipped.push(clipped[0]);\n }\n if (clipped.length >= 4) {\n outRings.push(clipped);\n }\n }\n }\n return outRings;\n}\n\nexport { bboxClip };\nexport default bboxClip;\n", "// Cohen-Sutherland line clipping algorithm, adapted to efficiently\n// handle polylines rather than just segments\nimport { BBox } from \"geojson\";\n\nexport function lineclip(\n points: number[][],\n bbox: BBox,\n result?: number[][][]\n): number[][][] {\n var len = points.length,\n codeA = bitCode(points[0], bbox),\n part = [] as number[][],\n i,\n codeB,\n lastCode;\n let a: number[];\n let b: number[];\n\n if (!result) result = [];\n\n for (i = 1; i < len; i++) {\n a = points[i - 1];\n b = points[i];\n codeB = lastCode = bitCode(b, bbox);\n\n while (true) {\n if (!(codeA | codeB)) {\n // accept\n part.push(a);\n\n if (codeB !== lastCode) {\n // segment went outside\n part.push(b);\n\n if (i < len - 1) {\n // start a new line\n result.push(part);\n part = [];\n }\n } else if (i === len - 1) {\n part.push(b);\n }\n break;\n } else if (codeA & codeB) {\n // trivial reject\n break;\n } else if (codeA) {\n // a outside, intersect with clip edge\n a = intersect(a, b, codeA, bbox)!;\n codeA = bitCode(a, bbox);\n } else {\n // b outside\n b = intersect(a, b, codeB, bbox)!;\n codeB = bitCode(b, bbox);\n }\n }\n\n codeA = lastCode;\n }\n\n if (part.length) result.push(part);\n\n return result;\n}\n\n// Sutherland-Hodgeman polygon clipping algorithm\n\nexport function polygonclip(points: number[][], bbox: BBox): number[][] {\n var result: number[][], edge, prev, prevInside, i, p, inside;\n\n // clip against each side of the clip rectangle\n for (edge = 1; edge <= 8; edge *= 2) {\n result = [];\n prev = points[points.length - 1];\n prevInside = !(bitCode(prev, bbox) & edge);\n\n for (i = 0; i < points.length; i++) {\n p = points[i];\n inside = !(bitCode(p, bbox) & edge);\n\n // if segment goes through the clip window, add an intersection\n if (inside !== prevInside) result.push(intersect(prev, p, edge, bbox)!);\n\n if (inside) result.push(p); // add a point if it's inside\n\n prev = p;\n prevInside = inside;\n }\n\n points = result;\n\n if (!points.length) break;\n }\n\n return result!;\n}\n\n// intersect a segment against one of the 4 lines that make up the bbox\n\nfunction intersect(\n a: number[],\n b: number[],\n edge: number,\n bbox: BBox\n): number[] | null {\n return edge & 8\n ? [a[0] + ((b[0] - a[0]) * (bbox[3] - a[1])) / (b[1] - a[1]), bbox[3]] // top\n : edge & 4\n ? [a[0] + ((b[0] - a[0]) * (bbox[1] - a[1])) / (b[1] - a[1]), bbox[1]] // bottom\n : edge & 2\n ? [bbox[2], a[1] + ((b[1] - a[1]) * (bbox[2] - a[0])) / (b[0] - a[0])] // right\n : edge & 1\n ? [bbox[0], a[1] + ((b[1] - a[1]) * (bbox[0] - a[0])) / (b[0] - a[0])] // left\n : null;\n}\n\n// bit code reflects the point position relative to the bbox:\n\n// left mid right\n// top 1001 1000 1010\n// mid 0001 0000 0010\n// bottom 0101 0100 0110\n\nfunction bitCode(p: number[], bbox: BBox) {\n var code = 0;\n\n if (p[0] < bbox[0]) code |= 1;\n // left\n else if (p[0] > bbox[2]) code |= 2; // right\n\n if (p[1] < bbox[1]) code |= 4;\n // bottom\n else if (p[1] > bbox[3]) code |= 8; // top\n\n return code;\n}\n", "'use strict';\n\nmodule.exports = function (data, opts) {\n if (!opts) opts = {};\n if (typeof opts === 'function') opts = { cmp: opts };\n var cycles = (typeof opts.cycles === 'boolean') ? opts.cycles : false;\n\n var cmp = opts.cmp && (function (f) {\n return function (node) {\n return function (a, b) {\n var aobj = { key: a, value: node[a] };\n var bobj = { key: b, value: node[b] };\n return f(aobj, bobj);\n };\n };\n })(opts.cmp);\n\n var seen = [];\n return (function stringify (node) {\n if (node && node.toJSON && typeof node.toJSON === 'function') {\n node = node.toJSON();\n }\n\n if (node === undefined) return;\n if (typeof node == 'number') return isFinite(node) ? '' + node : 'null';\n if (typeof node !== 'object') return JSON.stringify(node);\n\n var i, out;\n if (Array.isArray(node)) {\n out = '[';\n for (i = 0; i < node.length; i++) {\n if (i) out += ',';\n out += stringify(node[i]) || 'null';\n }\n return out + ']';\n }\n\n if (node === null) return 'null';\n\n if (seen.indexOf(node) !== -1) {\n if (cycles) return JSON.stringify('__cycle__');\n throw new TypeError('Converting circular structure to JSON');\n }\n\n var seenIndex = seen.push(node) - 1;\n var keys = Object.keys(node).sort(cmp && cmp(node));\n out = '';\n for (i = 0; i < keys.length; i++) {\n var key = keys[i];\n var value = stringify(node[key]);\n\n if (!value) continue;\n if (out) out += ',';\n out += JSON.stringify(key) + ':' + value;\n }\n seen.splice(seenIndex, 1);\n return '{' + out + '}';\n })(data);\n};\n", "(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.polygonClipping = factory());\n})(this, (function () { 'use strict';\n\n /**\n * splaytree v3.1.2\n * Fast Splay tree for Node and browser\n *\n * @author Alexander Milevski \n * @license MIT\n * @preserve\n */\n\n /*! *****************************************************************************\r\n Copyright (c) Microsoft Corporation. All rights reserved.\r\n Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use\r\n this file except in compliance with the License. You may obtain a copy of the\r\n License at http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r\n KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED\r\n WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,\r\n MERCHANTABLITY OR NON-INFRINGEMENT.\r\n\r\n See the Apache Version 2.0 License for specific language governing permissions\r\n and limitations under the License.\r\n ***************************************************************************** */\n\n function __generator(thisArg, body) {\n var _ = {\n label: 0,\n sent: function () {\n if (t[0] & 1) throw t[1];\n return t[1];\n },\n trys: [],\n ops: []\n },\n f,\n y,\n t,\n g;\n return g = {\n next: verb(0),\n \"throw\": verb(1),\n \"return\": verb(2)\n }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function () {\n return this;\n }), g;\n function verb(n) {\n return function (v) {\n return step([n, v]);\n };\n }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (_) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0:\n case 1:\n t = op;\n break;\n case 4:\n _.label++;\n return {\n value: op[1],\n done: false\n };\n case 5:\n _.label++;\n y = op[1];\n op = [0];\n continue;\n case 7:\n op = _.ops.pop();\n _.trys.pop();\n continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {\n _ = 0;\n continue;\n }\n if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {\n _.label = op[1];\n break;\n }\n if (op[0] === 6 && _.label < t[1]) {\n _.label = t[1];\n t = op;\n break;\n }\n if (t && _.label < t[2]) {\n _.label = t[2];\n _.ops.push(op);\n break;\n }\n if (t[2]) _.ops.pop();\n _.trys.pop();\n continue;\n }\n op = body.call(thisArg, _);\n } catch (e) {\n op = [6, e];\n y = 0;\n } finally {\n f = t = 0;\n }\n if (op[0] & 5) throw op[1];\n return {\n value: op[0] ? op[1] : void 0,\n done: true\n };\n }\n }\n var Node = /** @class */function () {\n function Node(key, data) {\n this.next = null;\n this.key = key;\n this.data = data;\n this.left = null;\n this.right = null;\n }\n return Node;\n }();\n\n /* follows \"An implementation of top-down splaying\"\r\n * by D. Sleator March 1992\r\n */\n function DEFAULT_COMPARE(a, b) {\n return a > b ? 1 : a < b ? -1 : 0;\n }\n /**\r\n * Simple top down splay, not requiring i to be in the tree t.\r\n */\n function splay(i, t, comparator) {\n var N = new Node(null, null);\n var l = N;\n var r = N;\n while (true) {\n var cmp = comparator(i, t.key);\n //if (i < t.key) {\n if (cmp < 0) {\n if (t.left === null) break;\n //if (i < t.left.key) {\n if (comparator(i, t.left.key) < 0) {\n var y = t.left; /* rotate right */\n t.left = y.right;\n y.right = t;\n t = y;\n if (t.left === null) break;\n }\n r.left = t; /* link right */\n r = t;\n t = t.left;\n //} else if (i > t.key) {\n } else if (cmp > 0) {\n if (t.right === null) break;\n //if (i > t.right.key) {\n if (comparator(i, t.right.key) > 0) {\n var y = t.right; /* rotate left */\n t.right = y.left;\n y.left = t;\n t = y;\n if (t.right === null) break;\n }\n l.right = t; /* link left */\n l = t;\n t = t.right;\n } else break;\n }\n /* assemble */\n l.right = t.left;\n r.left = t.right;\n t.left = N.right;\n t.right = N.left;\n return t;\n }\n function insert(i, data, t, comparator) {\n var node = new Node(i, data);\n if (t === null) {\n node.left = node.right = null;\n return node;\n }\n t = splay(i, t, comparator);\n var cmp = comparator(i, t.key);\n if (cmp < 0) {\n node.left = t.left;\n node.right = t;\n t.left = null;\n } else if (cmp >= 0) {\n node.right = t.right;\n node.left = t;\n t.right = null;\n }\n return node;\n }\n function split(key, v, comparator) {\n var left = null;\n var right = null;\n if (v) {\n v = splay(key, v, comparator);\n var cmp = comparator(v.key, key);\n if (cmp === 0) {\n left = v.left;\n right = v.right;\n } else if (cmp < 0) {\n right = v.right;\n v.right = null;\n left = v;\n } else {\n left = v.left;\n v.left = null;\n right = v;\n }\n }\n return {\n left: left,\n right: right\n };\n }\n function merge(left, right, comparator) {\n if (right === null) return left;\n if (left === null) return right;\n right = splay(left.key, right, comparator);\n right.left = left;\n return right;\n }\n /**\r\n * Prints level of the tree\r\n */\n function printRow(root, prefix, isTail, out, printNode) {\n if (root) {\n out(\"\" + prefix + (isTail ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ') + printNode(root) + \"\\n\");\n var indent = prefix + (isTail ? ' ' : '\u2502 ');\n if (root.left) printRow(root.left, indent, false, out, printNode);\n if (root.right) printRow(root.right, indent, true, out, printNode);\n }\n }\n var Tree = /** @class */function () {\n function Tree(comparator) {\n if (comparator === void 0) {\n comparator = DEFAULT_COMPARE;\n }\n this._root = null;\n this._size = 0;\n this._comparator = comparator;\n }\n /**\r\n * Inserts a key, allows duplicates\r\n */\n Tree.prototype.insert = function (key, data) {\n this._size++;\n return this._root = insert(key, data, this._root, this._comparator);\n };\n /**\r\n * Adds a key, if it is not present in the tree\r\n */\n Tree.prototype.add = function (key, data) {\n var node = new Node(key, data);\n if (this._root === null) {\n node.left = node.right = null;\n this._size++;\n this._root = node;\n }\n var comparator = this._comparator;\n var t = splay(key, this._root, comparator);\n var cmp = comparator(key, t.key);\n if (cmp === 0) this._root = t;else {\n if (cmp < 0) {\n node.left = t.left;\n node.right = t;\n t.left = null;\n } else if (cmp > 0) {\n node.right = t.right;\n node.left = t;\n t.right = null;\n }\n this._size++;\n this._root = node;\n }\n return this._root;\n };\n /**\r\n * @param {Key} key\r\n * @return {Node|null}\r\n */\n Tree.prototype.remove = function (key) {\n this._root = this._remove(key, this._root, this._comparator);\n };\n /**\r\n * Deletes i from the tree if it's there\r\n */\n Tree.prototype._remove = function (i, t, comparator) {\n var x;\n if (t === null) return null;\n t = splay(i, t, comparator);\n var cmp = comparator(i, t.key);\n if (cmp === 0) {\n /* found it */\n if (t.left === null) {\n x = t.right;\n } else {\n x = splay(i, t.left, comparator);\n x.right = t.right;\n }\n this._size--;\n return x;\n }\n return t; /* It wasn't there */\n };\n /**\r\n * Removes and returns the node with smallest key\r\n */\n Tree.prototype.pop = function () {\n var node = this._root;\n if (node) {\n while (node.left) node = node.left;\n this._root = splay(node.key, this._root, this._comparator);\n this._root = this._remove(node.key, this._root, this._comparator);\n return {\n key: node.key,\n data: node.data\n };\n }\n return null;\n };\n /**\r\n * Find without splaying\r\n */\n Tree.prototype.findStatic = function (key) {\n var current = this._root;\n var compare = this._comparator;\n while (current) {\n var cmp = compare(key, current.key);\n if (cmp === 0) return current;else if (cmp < 0) current = current.left;else current = current.right;\n }\n return null;\n };\n Tree.prototype.find = function (key) {\n if (this._root) {\n this._root = splay(key, this._root, this._comparator);\n if (this._comparator(key, this._root.key) !== 0) return null;\n }\n return this._root;\n };\n Tree.prototype.contains = function (key) {\n var current = this._root;\n var compare = this._comparator;\n while (current) {\n var cmp = compare(key, current.key);\n if (cmp === 0) return true;else if (cmp < 0) current = current.left;else current = current.right;\n }\n return false;\n };\n Tree.prototype.forEach = function (visitor, ctx) {\n var current = this._root;\n var Q = []; /* Initialize stack s */\n var done = false;\n while (!done) {\n if (current !== null) {\n Q.push(current);\n current = current.left;\n } else {\n if (Q.length !== 0) {\n current = Q.pop();\n visitor.call(ctx, current);\n current = current.right;\n } else done = true;\n }\n }\n return this;\n };\n /**\r\n * Walk key range from `low` to `high`. Stops if `fn` returns a value.\r\n */\n Tree.prototype.range = function (low, high, fn, ctx) {\n var Q = [];\n var compare = this._comparator;\n var node = this._root;\n var cmp;\n while (Q.length !== 0 || node) {\n if (node) {\n Q.push(node);\n node = node.left;\n } else {\n node = Q.pop();\n cmp = compare(node.key, high);\n if (cmp > 0) {\n break;\n } else if (compare(node.key, low) >= 0) {\n if (fn.call(ctx, node)) return this; // stop if smth is returned\n }\n node = node.right;\n }\n }\n return this;\n };\n /**\r\n * Returns array of keys\r\n */\n Tree.prototype.keys = function () {\n var keys = [];\n this.forEach(function (_a) {\n var key = _a.key;\n return keys.push(key);\n });\n return keys;\n };\n /**\r\n * Returns array of all the data in the nodes\r\n */\n Tree.prototype.values = function () {\n var values = [];\n this.forEach(function (_a) {\n var data = _a.data;\n return values.push(data);\n });\n return values;\n };\n Tree.prototype.min = function () {\n if (this._root) return this.minNode(this._root).key;\n return null;\n };\n Tree.prototype.max = function () {\n if (this._root) return this.maxNode(this._root).key;\n return null;\n };\n Tree.prototype.minNode = function (t) {\n if (t === void 0) {\n t = this._root;\n }\n if (t) while (t.left) t = t.left;\n return t;\n };\n Tree.prototype.maxNode = function (t) {\n if (t === void 0) {\n t = this._root;\n }\n if (t) while (t.right) t = t.right;\n return t;\n };\n /**\r\n * Returns node at given index\r\n */\n Tree.prototype.at = function (index) {\n var current = this._root;\n var done = false;\n var i = 0;\n var Q = [];\n while (!done) {\n if (current) {\n Q.push(current);\n current = current.left;\n } else {\n if (Q.length > 0) {\n current = Q.pop();\n if (i === index) return current;\n i++;\n current = current.right;\n } else done = true;\n }\n }\n return null;\n };\n Tree.prototype.next = function (d) {\n var root = this._root;\n var successor = null;\n if (d.right) {\n successor = d.right;\n while (successor.left) successor = successor.left;\n return successor;\n }\n var comparator = this._comparator;\n while (root) {\n var cmp = comparator(d.key, root.key);\n if (cmp === 0) break;else if (cmp < 0) {\n successor = root;\n root = root.left;\n } else root = root.right;\n }\n return successor;\n };\n Tree.prototype.prev = function (d) {\n var root = this._root;\n var predecessor = null;\n if (d.left !== null) {\n predecessor = d.left;\n while (predecessor.right) predecessor = predecessor.right;\n return predecessor;\n }\n var comparator = this._comparator;\n while (root) {\n var cmp = comparator(d.key, root.key);\n if (cmp === 0) break;else if (cmp < 0) root = root.left;else {\n predecessor = root;\n root = root.right;\n }\n }\n return predecessor;\n };\n Tree.prototype.clear = function () {\n this._root = null;\n this._size = 0;\n return this;\n };\n Tree.prototype.toList = function () {\n return toList(this._root);\n };\n /**\r\n * Bulk-load items. Both array have to be same size\r\n */\n Tree.prototype.load = function (keys, values, presort) {\n if (values === void 0) {\n values = [];\n }\n if (presort === void 0) {\n presort = false;\n }\n var size = keys.length;\n var comparator = this._comparator;\n // sort if needed\n if (presort) sort(keys, values, 0, size - 1, comparator);\n if (this._root === null) {\n // empty tree\n this._root = loadRecursive(keys, values, 0, size);\n this._size = size;\n } else {\n // that re-builds the whole tree from two in-order traversals\n var mergedList = mergeLists(this.toList(), createList(keys, values), comparator);\n size = this._size + size;\n this._root = sortedListToBST({\n head: mergedList\n }, 0, size);\n }\n return this;\n };\n Tree.prototype.isEmpty = function () {\n return this._root === null;\n };\n Object.defineProperty(Tree.prototype, \"size\", {\n get: function () {\n return this._size;\n },\n enumerable: true,\n configurable: true\n });\n Object.defineProperty(Tree.prototype, \"root\", {\n get: function () {\n return this._root;\n },\n enumerable: true,\n configurable: true\n });\n Tree.prototype.toString = function (printNode) {\n if (printNode === void 0) {\n printNode = function (n) {\n return String(n.key);\n };\n }\n var out = [];\n printRow(this._root, '', true, function (v) {\n return out.push(v);\n }, printNode);\n return out.join('');\n };\n Tree.prototype.update = function (key, newKey, newData) {\n var comparator = this._comparator;\n var _a = split(key, this._root, comparator),\n left = _a.left,\n right = _a.right;\n if (comparator(key, newKey) < 0) {\n right = insert(newKey, newData, right, comparator);\n } else {\n left = insert(newKey, newData, left, comparator);\n }\n this._root = merge(left, right, comparator);\n };\n Tree.prototype.split = function (key) {\n return split(key, this._root, this._comparator);\n };\n Tree.prototype[Symbol.iterator] = function () {\n var current, Q, done;\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0:\n current = this._root;\n Q = [];\n done = false;\n _a.label = 1;\n case 1:\n if (!!done) return [3 /*break*/, 6];\n if (!(current !== null)) return [3 /*break*/, 2];\n Q.push(current);\n current = current.left;\n return [3 /*break*/, 5];\n case 2:\n if (!(Q.length !== 0)) return [3 /*break*/, 4];\n current = Q.pop();\n return [4 /*yield*/, current];\n case 3:\n _a.sent();\n current = current.right;\n return [3 /*break*/, 5];\n case 4:\n done = true;\n _a.label = 5;\n case 5:\n return [3 /*break*/, 1];\n case 6:\n return [2 /*return*/];\n }\n });\n };\n return Tree;\n }();\n function loadRecursive(keys, values, start, end) {\n var size = end - start;\n if (size > 0) {\n var middle = start + Math.floor(size / 2);\n var key = keys[middle];\n var data = values[middle];\n var node = new Node(key, data);\n node.left = loadRecursive(keys, values, start, middle);\n node.right = loadRecursive(keys, values, middle + 1, end);\n return node;\n }\n return null;\n }\n function createList(keys, values) {\n var head = new Node(null, null);\n var p = head;\n for (var i = 0; i < keys.length; i++) {\n p = p.next = new Node(keys[i], values[i]);\n }\n p.next = null;\n return head.next;\n }\n function toList(root) {\n var current = root;\n var Q = [];\n var done = false;\n var head = new Node(null, null);\n var p = head;\n while (!done) {\n if (current) {\n Q.push(current);\n current = current.left;\n } else {\n if (Q.length > 0) {\n current = p = p.next = Q.pop();\n current = current.right;\n } else done = true;\n }\n }\n p.next = null; // that'll work even if the tree was empty\n return head.next;\n }\n function sortedListToBST(list, start, end) {\n var size = end - start;\n if (size > 0) {\n var middle = start + Math.floor(size / 2);\n var left = sortedListToBST(list, start, middle);\n var root = list.head;\n root.left = left;\n list.head = list.head.next;\n root.right = sortedListToBST(list, middle + 1, end);\n return root;\n }\n return null;\n }\n function mergeLists(l1, l2, compare) {\n var head = new Node(null, null); // dummy\n var p = head;\n var p1 = l1;\n var p2 = l2;\n while (p1 !== null && p2 !== null) {\n if (compare(p1.key, p2.key) < 0) {\n p.next = p1;\n p1 = p1.next;\n } else {\n p.next = p2;\n p2 = p2.next;\n }\n p = p.next;\n }\n if (p1 !== null) {\n p.next = p1;\n } else if (p2 !== null) {\n p.next = p2;\n }\n return head.next;\n }\n function sort(keys, values, left, right, compare) {\n if (left >= right) return;\n var pivot = keys[left + right >> 1];\n var i = left - 1;\n var j = right + 1;\n while (true) {\n do i++; while (compare(keys[i], pivot) < 0);\n do j--; while (compare(keys[j], pivot) > 0);\n if (i >= j) break;\n var tmp = keys[i];\n keys[i] = keys[j];\n keys[j] = tmp;\n tmp = values[i];\n values[i] = values[j];\n values[j] = tmp;\n }\n sort(keys, values, left, j, compare);\n sort(keys, values, j + 1, right, compare);\n }\n\n /**\n * A bounding box has the format:\n *\n * { ll: { x: xmin, y: ymin }, ur: { x: xmax, y: ymax } }\n *\n */\n\n const isInBbox = (bbox, point) => {\n return bbox.ll.x <= point.x && point.x <= bbox.ur.x && bbox.ll.y <= point.y && point.y <= bbox.ur.y;\n };\n\n /* Returns either null, or a bbox (aka an ordered pair of points)\n * If there is only one point of overlap, a bbox with identical points\n * will be returned */\n const getBboxOverlap = (b1, b2) => {\n // check if the bboxes overlap at all\n if (b2.ur.x < b1.ll.x || b1.ur.x < b2.ll.x || b2.ur.y < b1.ll.y || b1.ur.y < b2.ll.y) return null;\n\n // find the middle two X values\n const lowerX = b1.ll.x < b2.ll.x ? b2.ll.x : b1.ll.x;\n const upperX = b1.ur.x < b2.ur.x ? b1.ur.x : b2.ur.x;\n\n // find the middle two Y values\n const lowerY = b1.ll.y < b2.ll.y ? b2.ll.y : b1.ll.y;\n const upperY = b1.ur.y < b2.ur.y ? b1.ur.y : b2.ur.y;\n\n // put those middle values together to get the overlap\n return {\n ll: {\n x: lowerX,\n y: lowerY\n },\n ur: {\n x: upperX,\n y: upperY\n }\n };\n };\n\n /* Javascript doesn't do integer math. Everything is\n * floating point with percision Number.EPSILON.\n *\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON\n */\n\n let epsilon$1 = Number.EPSILON;\n\n // IE Polyfill\n if (epsilon$1 === undefined) epsilon$1 = Math.pow(2, -52);\n const EPSILON_SQ = epsilon$1 * epsilon$1;\n\n /* FLP comparator */\n const cmp = (a, b) => {\n // check if they're both 0\n if (-epsilon$1 < a && a < epsilon$1) {\n if (-epsilon$1 < b && b < epsilon$1) {\n return 0;\n }\n }\n\n // check if they're flp equal\n const ab = a - b;\n if (ab * ab < EPSILON_SQ * a * b) {\n return 0;\n }\n\n // normal comparison\n return a < b ? -1 : 1;\n };\n\n /**\n * This class rounds incoming values sufficiently so that\n * floating points problems are, for the most part, avoided.\n *\n * Incoming points are have their x & y values tested against\n * all previously seen x & y values. If either is 'too close'\n * to a previously seen value, it's value is 'snapped' to the\n * previously seen value.\n *\n * All points should be rounded by this class before being\n * stored in any data structures in the rest of this algorithm.\n */\n\n class PtRounder {\n constructor() {\n this.reset();\n }\n reset() {\n this.xRounder = new CoordRounder();\n this.yRounder = new CoordRounder();\n }\n round(x, y) {\n return {\n x: this.xRounder.round(x),\n y: this.yRounder.round(y)\n };\n }\n }\n class CoordRounder {\n constructor() {\n this.tree = new Tree();\n // preseed with 0 so we don't end up with values < Number.EPSILON\n this.round(0);\n }\n\n // Note: this can rounds input values backwards or forwards.\n // You might ask, why not restrict this to just rounding\n // forwards? Wouldn't that allow left endpoints to always\n // remain left endpoints during splitting (never change to\n // right). No - it wouldn't, because we snap intersections\n // to endpoints (to establish independence from the segment\n // angle for t-intersections).\n round(coord) {\n const node = this.tree.add(coord);\n const prevNode = this.tree.prev(node);\n if (prevNode !== null && cmp(node.key, prevNode.key) === 0) {\n this.tree.remove(coord);\n return prevNode.key;\n }\n const nextNode = this.tree.next(node);\n if (nextNode !== null && cmp(node.key, nextNode.key) === 0) {\n this.tree.remove(coord);\n return nextNode.key;\n }\n return coord;\n }\n }\n\n // singleton available by import\n const rounder = new PtRounder();\n\n const epsilon = 1.1102230246251565e-16;\n const splitter = 134217729;\n const resulterrbound = (3 + 8 * epsilon) * epsilon;\n\n // fast_expansion_sum_zeroelim routine from oritinal code\n function sum(elen, e, flen, f, h) {\n let Q, Qnew, hh, bvirt;\n let enow = e[0];\n let fnow = f[0];\n let eindex = 0;\n let findex = 0;\n if (fnow > enow === fnow > -enow) {\n Q = enow;\n enow = e[++eindex];\n } else {\n Q = fnow;\n fnow = f[++findex];\n }\n let hindex = 0;\n if (eindex < elen && findex < flen) {\n if (fnow > enow === fnow > -enow) {\n Qnew = enow + Q;\n hh = Q - (Qnew - enow);\n enow = e[++eindex];\n } else {\n Qnew = fnow + Q;\n hh = Q - (Qnew - fnow);\n fnow = f[++findex];\n }\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n while (eindex < elen && findex < flen) {\n if (fnow > enow === fnow > -enow) {\n Qnew = Q + enow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (enow - bvirt);\n enow = e[++eindex];\n } else {\n Qnew = Q + fnow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (fnow - bvirt);\n fnow = f[++findex];\n }\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n }\n }\n while (eindex < elen) {\n Qnew = Q + enow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (enow - bvirt);\n enow = e[++eindex];\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n }\n while (findex < flen) {\n Qnew = Q + fnow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (fnow - bvirt);\n fnow = f[++findex];\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n }\n if (Q !== 0 || hindex === 0) {\n h[hindex++] = Q;\n }\n return hindex;\n }\n function estimate(elen, e) {\n let Q = e[0];\n for (let i = 1; i < elen; i++) Q += e[i];\n return Q;\n }\n function vec(n) {\n return new Float64Array(n);\n }\n\n const ccwerrboundA = (3 + 16 * epsilon) * epsilon;\n const ccwerrboundB = (2 + 12 * epsilon) * epsilon;\n const ccwerrboundC = (9 + 64 * epsilon) * epsilon * epsilon;\n const B = vec(4);\n const C1 = vec(8);\n const C2 = vec(12);\n const D = vec(16);\n const u = vec(4);\n function orient2dadapt(ax, ay, bx, by, cx, cy, detsum) {\n let acxtail, acytail, bcxtail, bcytail;\n let bvirt, c, ahi, alo, bhi, blo, _i, _j, _0, s1, s0, t1, t0, u3;\n const acx = ax - cx;\n const bcx = bx - cx;\n const acy = ay - cy;\n const bcy = by - cy;\n s1 = acx * bcy;\n c = splitter * acx;\n ahi = c - (c - acx);\n alo = acx - ahi;\n c = splitter * bcy;\n bhi = c - (c - bcy);\n blo = bcy - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acy * bcx;\n c = splitter * acy;\n ahi = c - (c - acy);\n alo = acy - ahi;\n c = splitter * bcx;\n bhi = c - (c - bcx);\n blo = bcx - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n B[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n B[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n B[2] = _j - (u3 - bvirt) + (_i - bvirt);\n B[3] = u3;\n let det = estimate(4, B);\n let errbound = ccwerrboundB * detsum;\n if (det >= errbound || -det >= errbound) {\n return det;\n }\n bvirt = ax - acx;\n acxtail = ax - (acx + bvirt) + (bvirt - cx);\n bvirt = bx - bcx;\n bcxtail = bx - (bcx + bvirt) + (bvirt - cx);\n bvirt = ay - acy;\n acytail = ay - (acy + bvirt) + (bvirt - cy);\n bvirt = by - bcy;\n bcytail = by - (bcy + bvirt) + (bvirt - cy);\n if (acxtail === 0 && acytail === 0 && bcxtail === 0 && bcytail === 0) {\n return det;\n }\n errbound = ccwerrboundC * detsum + resulterrbound * Math.abs(det);\n det += acx * bcytail + bcy * acxtail - (acy * bcxtail + bcx * acytail);\n if (det >= errbound || -det >= errbound) return det;\n s1 = acxtail * bcy;\n c = splitter * acxtail;\n ahi = c - (c - acxtail);\n alo = acxtail - ahi;\n c = splitter * bcy;\n bhi = c - (c - bcy);\n blo = bcy - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acytail * bcx;\n c = splitter * acytail;\n ahi = c - (c - acytail);\n alo = acytail - ahi;\n c = splitter * bcx;\n bhi = c - (c - bcx);\n blo = bcx - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n u[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n u[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n u[2] = _j - (u3 - bvirt) + (_i - bvirt);\n u[3] = u3;\n const C1len = sum(4, B, 4, u, C1);\n s1 = acx * bcytail;\n c = splitter * acx;\n ahi = c - (c - acx);\n alo = acx - ahi;\n c = splitter * bcytail;\n bhi = c - (c - bcytail);\n blo = bcytail - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acy * bcxtail;\n c = splitter * acy;\n ahi = c - (c - acy);\n alo = acy - ahi;\n c = splitter * bcxtail;\n bhi = c - (c - bcxtail);\n blo = bcxtail - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n u[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n u[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n u[2] = _j - (u3 - bvirt) + (_i - bvirt);\n u[3] = u3;\n const C2len = sum(C1len, C1, 4, u, C2);\n s1 = acxtail * bcytail;\n c = splitter * acxtail;\n ahi = c - (c - acxtail);\n alo = acxtail - ahi;\n c = splitter * bcytail;\n bhi = c - (c - bcytail);\n blo = bcytail - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acytail * bcxtail;\n c = splitter * acytail;\n ahi = c - (c - acytail);\n alo = acytail - ahi;\n c = splitter * bcxtail;\n bhi = c - (c - bcxtail);\n blo = bcxtail - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n u[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n u[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n u[2] = _j - (u3 - bvirt) + (_i - bvirt);\n u[3] = u3;\n const Dlen = sum(C2len, C2, 4, u, D);\n return D[Dlen - 1];\n }\n function orient2d(ax, ay, bx, by, cx, cy) {\n const detleft = (ay - cy) * (bx - cx);\n const detright = (ax - cx) * (by - cy);\n const det = detleft - detright;\n const detsum = Math.abs(detleft + detright);\n if (Math.abs(det) >= ccwerrboundA * detsum) return det;\n return -orient2dadapt(ax, ay, bx, by, cx, cy, detsum);\n }\n\n /* Cross Product of two vectors with first point at origin */\n const crossProduct = (a, b) => a.x * b.y - a.y * b.x;\n\n /* Dot Product of two vectors with first point at origin */\n const dotProduct = (a, b) => a.x * b.x + a.y * b.y;\n\n /* Comparator for two vectors with same starting point */\n const compareVectorAngles = (basePt, endPt1, endPt2) => {\n const res = orient2d(basePt.x, basePt.y, endPt1.x, endPt1.y, endPt2.x, endPt2.y);\n if (res > 0) return -1;\n if (res < 0) return 1;\n return 0;\n };\n const length = v => Math.sqrt(dotProduct(v, v));\n\n /* Get the sine of the angle from pShared -> pAngle to pShaed -> pBase */\n const sineOfAngle = (pShared, pBase, pAngle) => {\n const vBase = {\n x: pBase.x - pShared.x,\n y: pBase.y - pShared.y\n };\n const vAngle = {\n x: pAngle.x - pShared.x,\n y: pAngle.y - pShared.y\n };\n return crossProduct(vAngle, vBase) / length(vAngle) / length(vBase);\n };\n\n /* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */\n const cosineOfAngle = (pShared, pBase, pAngle) => {\n const vBase = {\n x: pBase.x - pShared.x,\n y: pBase.y - pShared.y\n };\n const vAngle = {\n x: pAngle.x - pShared.x,\n y: pAngle.y - pShared.y\n };\n return dotProduct(vAngle, vBase) / length(vAngle) / length(vBase);\n };\n\n /* Get the x coordinate where the given line (defined by a point and vector)\n * crosses the horizontal line with the given y coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\n const horizontalIntersection = (pt, v, y) => {\n if (v.y === 0) return null;\n return {\n x: pt.x + v.x / v.y * (y - pt.y),\n y: y\n };\n };\n\n /* Get the y coordinate where the given line (defined by a point and vector)\n * crosses the vertical line with the given x coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\n const verticalIntersection = (pt, v, x) => {\n if (v.x === 0) return null;\n return {\n x: x,\n y: pt.y + v.y / v.x * (x - pt.x)\n };\n };\n\n /* Get the intersection of two lines, each defined by a base point and a vector.\n * In the case of parrallel lines (including overlapping ones) returns null. */\n const intersection$1 = (pt1, v1, pt2, v2) => {\n // take some shortcuts for vertical and horizontal lines\n // this also ensures we don't calculate an intersection and then discover\n // it's actually outside the bounding box of the line\n if (v1.x === 0) return verticalIntersection(pt2, v2, pt1.x);\n if (v2.x === 0) return verticalIntersection(pt1, v1, pt2.x);\n if (v1.y === 0) return horizontalIntersection(pt2, v2, pt1.y);\n if (v2.y === 0) return horizontalIntersection(pt1, v1, pt2.y);\n\n // General case for non-overlapping segments.\n // This algorithm is based on Schneider and Eberly.\n // http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf - pg 244\n\n const kross = crossProduct(v1, v2);\n if (kross == 0) return null;\n const ve = {\n x: pt2.x - pt1.x,\n y: pt2.y - pt1.y\n };\n const d1 = crossProduct(ve, v1) / kross;\n const d2 = crossProduct(ve, v2) / kross;\n\n // take the average of the two calculations to minimize rounding error\n const x1 = pt1.x + d2 * v1.x,\n x2 = pt2.x + d1 * v2.x;\n const y1 = pt1.y + d2 * v1.y,\n y2 = pt2.y + d1 * v2.y;\n const x = (x1 + x2) / 2;\n const y = (y1 + y2) / 2;\n return {\n x: x,\n y: y\n };\n };\n\n class SweepEvent {\n // for ordering sweep events in the sweep event queue\n static compare(a, b) {\n // favor event with a point that the sweep line hits first\n const ptCmp = SweepEvent.comparePoints(a.point, b.point);\n if (ptCmp !== 0) return ptCmp;\n\n // the points are the same, so link them if needed\n if (a.point !== b.point) a.link(b);\n\n // favor right events over left\n if (a.isLeft !== b.isLeft) return a.isLeft ? 1 : -1;\n\n // we have two matching left or right endpoints\n // ordering of this case is the same as for their segments\n return Segment.compare(a.segment, b.segment);\n }\n\n // for ordering points in sweep line order\n static comparePoints(aPt, bPt) {\n if (aPt.x < bPt.x) return -1;\n if (aPt.x > bPt.x) return 1;\n if (aPt.y < bPt.y) return -1;\n if (aPt.y > bPt.y) return 1;\n return 0;\n }\n\n // Warning: 'point' input will be modified and re-used (for performance)\n constructor(point, isLeft) {\n if (point.events === undefined) point.events = [this];else point.events.push(this);\n this.point = point;\n this.isLeft = isLeft;\n // this.segment, this.otherSE set by factory\n }\n link(other) {\n if (other.point === this.point) {\n throw new Error(\"Tried to link already linked events\");\n }\n const otherEvents = other.point.events;\n for (let i = 0, iMax = otherEvents.length; i < iMax; i++) {\n const evt = otherEvents[i];\n this.point.events.push(evt);\n evt.point = this.point;\n }\n this.checkForConsuming();\n }\n\n /* Do a pass over our linked events and check to see if any pair\n * of segments match, and should be consumed. */\n checkForConsuming() {\n // FIXME: The loops in this method run O(n^2) => no good.\n // Maintain little ordered sweep event trees?\n // Can we maintaining an ordering that avoids the need\n // for the re-sorting with getLeftmostComparator in geom-out?\n\n // Compare each pair of events to see if other events also match\n const numEvents = this.point.events.length;\n for (let i = 0; i < numEvents; i++) {\n const evt1 = this.point.events[i];\n if (evt1.segment.consumedBy !== undefined) continue;\n for (let j = i + 1; j < numEvents; j++) {\n const evt2 = this.point.events[j];\n if (evt2.consumedBy !== undefined) continue;\n if (evt1.otherSE.point.events !== evt2.otherSE.point.events) continue;\n evt1.segment.consume(evt2.segment);\n }\n }\n }\n getAvailableLinkedEvents() {\n // point.events is always of length 2 or greater\n const events = [];\n for (let i = 0, iMax = this.point.events.length; i < iMax; i++) {\n const evt = this.point.events[i];\n if (evt !== this && !evt.segment.ringOut && evt.segment.isInResult()) {\n events.push(evt);\n }\n }\n return events;\n }\n\n /**\n * Returns a comparator function for sorting linked events that will\n * favor the event that will give us the smallest left-side angle.\n * All ring construction starts as low as possible heading to the right,\n * so by always turning left as sharp as possible we'll get polygons\n * without uncessary loops & holes.\n *\n * The comparator function has a compute cache such that it avoids\n * re-computing already-computed values.\n */\n getLeftmostComparator(baseEvent) {\n const cache = new Map();\n const fillCache = linkedEvent => {\n const nextEvent = linkedEvent.otherSE;\n cache.set(linkedEvent, {\n sine: sineOfAngle(this.point, baseEvent.point, nextEvent.point),\n cosine: cosineOfAngle(this.point, baseEvent.point, nextEvent.point)\n });\n };\n return (a, b) => {\n if (!cache.has(a)) fillCache(a);\n if (!cache.has(b)) fillCache(b);\n const {\n sine: asine,\n cosine: acosine\n } = cache.get(a);\n const {\n sine: bsine,\n cosine: bcosine\n } = cache.get(b);\n\n // both on or above x-axis\n if (asine >= 0 && bsine >= 0) {\n if (acosine < bcosine) return 1;\n if (acosine > bcosine) return -1;\n return 0;\n }\n\n // both below x-axis\n if (asine < 0 && bsine < 0) {\n if (acosine < bcosine) return -1;\n if (acosine > bcosine) return 1;\n return 0;\n }\n\n // one above x-axis, one below\n if (bsine < asine) return -1;\n if (bsine > asine) return 1;\n return 0;\n };\n }\n }\n\n // Give segments unique ID's to get consistent sorting of\n // segments and sweep events when all else is identical\n let segmentId = 0;\n class Segment {\n /* This compare() function is for ordering segments in the sweep\n * line tree, and does so according to the following criteria:\n *\n * Consider the vertical line that lies an infinestimal step to the\n * right of the right-more of the two left endpoints of the input\n * segments. Imagine slowly moving a point up from negative infinity\n * in the increasing y direction. Which of the two segments will that\n * point intersect first? That segment comes 'before' the other one.\n *\n * If neither segment would be intersected by such a line, (if one\n * or more of the segments are vertical) then the line to be considered\n * is directly on the right-more of the two left inputs.\n */\n static compare(a, b) {\n const alx = a.leftSE.point.x;\n const blx = b.leftSE.point.x;\n const arx = a.rightSE.point.x;\n const brx = b.rightSE.point.x;\n\n // check if they're even in the same vertical plane\n if (brx < alx) return 1;\n if (arx < blx) return -1;\n const aly = a.leftSE.point.y;\n const bly = b.leftSE.point.y;\n const ary = a.rightSE.point.y;\n const bry = b.rightSE.point.y;\n\n // is left endpoint of segment B the right-more?\n if (alx < blx) {\n // are the two segments in the same horizontal plane?\n if (bly < aly && bly < ary) return 1;\n if (bly > aly && bly > ary) return -1;\n\n // is the B left endpoint colinear to segment A?\n const aCmpBLeft = a.comparePoint(b.leftSE.point);\n if (aCmpBLeft < 0) return 1;\n if (aCmpBLeft > 0) return -1;\n\n // is the A right endpoint colinear to segment B ?\n const bCmpARight = b.comparePoint(a.rightSE.point);\n if (bCmpARight !== 0) return bCmpARight;\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return -1;\n }\n\n // is left endpoint of segment A the right-more?\n if (alx > blx) {\n if (aly < bly && aly < bry) return -1;\n if (aly > bly && aly > bry) return 1;\n\n // is the A left endpoint colinear to segment B?\n const bCmpALeft = b.comparePoint(a.leftSE.point);\n if (bCmpALeft !== 0) return bCmpALeft;\n\n // is the B right endpoint colinear to segment A?\n const aCmpBRight = a.comparePoint(b.rightSE.point);\n if (aCmpBRight < 0) return 1;\n if (aCmpBRight > 0) return -1;\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return 1;\n }\n\n // if we get here, the two left endpoints are in the same\n // vertical plane, ie alx === blx\n\n // consider the lower left-endpoint to come first\n if (aly < bly) return -1;\n if (aly > bly) return 1;\n\n // left endpoints are identical\n // check for colinearity by using the left-more right endpoint\n\n // is the A right endpoint more left-more?\n if (arx < brx) {\n const bCmpARight = b.comparePoint(a.rightSE.point);\n if (bCmpARight !== 0) return bCmpARight;\n }\n\n // is the B right endpoint more left-more?\n if (arx > brx) {\n const aCmpBRight = a.comparePoint(b.rightSE.point);\n if (aCmpBRight < 0) return 1;\n if (aCmpBRight > 0) return -1;\n }\n if (arx !== brx) {\n // are these two [almost] vertical segments with opposite orientation?\n // if so, the one with the lower right endpoint comes first\n const ay = ary - aly;\n const ax = arx - alx;\n const by = bry - bly;\n const bx = brx - blx;\n if (ay > ax && by < bx) return 1;\n if (ay < ax && by > bx) return -1;\n }\n\n // we have colinear segments with matching orientation\n // consider the one with more left-more right endpoint to be first\n if (arx > brx) return 1;\n if (arx < brx) return -1;\n\n // if we get here, two two right endpoints are in the same\n // vertical plane, ie arx === brx\n\n // consider the lower right-endpoint to come first\n if (ary < bry) return -1;\n if (ary > bry) return 1;\n\n // right endpoints identical as well, so the segments are idential\n // fall back on creation order as consistent tie-breaker\n if (a.id < b.id) return -1;\n if (a.id > b.id) return 1;\n\n // identical segment, ie a === b\n return 0;\n }\n\n /* Warning: a reference to ringWindings input will be stored,\n * and possibly will be later modified */\n constructor(leftSE, rightSE, rings, windings) {\n this.id = ++segmentId;\n this.leftSE = leftSE;\n leftSE.segment = this;\n leftSE.otherSE = rightSE;\n this.rightSE = rightSE;\n rightSE.segment = this;\n rightSE.otherSE = leftSE;\n this.rings = rings;\n this.windings = windings;\n // left unset for performance, set later in algorithm\n // this.ringOut, this.consumedBy, this.prev\n }\n static fromRing(pt1, pt2, ring) {\n let leftPt, rightPt, winding;\n\n // ordering the two points according to sweep line ordering\n const cmpPts = SweepEvent.comparePoints(pt1, pt2);\n if (cmpPts < 0) {\n leftPt = pt1;\n rightPt = pt2;\n winding = 1;\n } else if (cmpPts > 0) {\n leftPt = pt2;\n rightPt = pt1;\n winding = -1;\n } else throw new Error(`Tried to create degenerate segment at [${pt1.x}, ${pt1.y}]`);\n const leftSE = new SweepEvent(leftPt, true);\n const rightSE = new SweepEvent(rightPt, false);\n return new Segment(leftSE, rightSE, [ring], [winding]);\n }\n\n /* When a segment is split, the rightSE is replaced with a new sweep event */\n replaceRightSE(newRightSE) {\n this.rightSE = newRightSE;\n this.rightSE.segment = this;\n this.rightSE.otherSE = this.leftSE;\n this.leftSE.otherSE = this.rightSE;\n }\n bbox() {\n const y1 = this.leftSE.point.y;\n const y2 = this.rightSE.point.y;\n return {\n ll: {\n x: this.leftSE.point.x,\n y: y1 < y2 ? y1 : y2\n },\n ur: {\n x: this.rightSE.point.x,\n y: y1 > y2 ? y1 : y2\n }\n };\n }\n\n /* A vector from the left point to the right */\n vector() {\n return {\n x: this.rightSE.point.x - this.leftSE.point.x,\n y: this.rightSE.point.y - this.leftSE.point.y\n };\n }\n isAnEndpoint(pt) {\n return pt.x === this.leftSE.point.x && pt.y === this.leftSE.point.y || pt.x === this.rightSE.point.x && pt.y === this.rightSE.point.y;\n }\n\n /* Compare this segment with a point.\n *\n * A point P is considered to be colinear to a segment if there\n * exists a distance D such that if we travel along the segment\n * from one * endpoint towards the other a distance D, we find\n * ourselves at point P.\n *\n * Return value indicates:\n *\n * 1: point lies above the segment (to the left of vertical)\n * 0: point is colinear to segment\n * -1: point lies below the segment (to the right of vertical)\n */\n comparePoint(point) {\n if (this.isAnEndpoint(point)) return 0;\n const lPt = this.leftSE.point;\n const rPt = this.rightSE.point;\n const v = this.vector();\n\n // Exactly vertical segments.\n if (lPt.x === rPt.x) {\n if (point.x === lPt.x) return 0;\n return point.x < lPt.x ? 1 : -1;\n }\n\n // Nearly vertical segments with an intersection.\n // Check to see where a point on the line with matching Y coordinate is.\n const yDist = (point.y - lPt.y) / v.y;\n const xFromYDist = lPt.x + yDist * v.x;\n if (point.x === xFromYDist) return 0;\n\n // General case.\n // Check to see where a point on the line with matching X coordinate is.\n const xDist = (point.x - lPt.x) / v.x;\n const yFromXDist = lPt.y + xDist * v.y;\n if (point.y === yFromXDist) return 0;\n return point.y < yFromXDist ? -1 : 1;\n }\n\n /**\n * Given another segment, returns the first non-trivial intersection\n * between the two segments (in terms of sweep line ordering), if it exists.\n *\n * A 'non-trivial' intersection is one that will cause one or both of the\n * segments to be split(). As such, 'trivial' vs. 'non-trivial' intersection:\n *\n * * endpoint of segA with endpoint of segB --> trivial\n * * endpoint of segA with point along segB --> non-trivial\n * * endpoint of segB with point along segA --> non-trivial\n * * point along segA with point along segB --> non-trivial\n *\n * If no non-trivial intersection exists, return null\n * Else, return null.\n */\n getIntersection(other) {\n // If bboxes don't overlap, there can't be any intersections\n const tBbox = this.bbox();\n const oBbox = other.bbox();\n const bboxOverlap = getBboxOverlap(tBbox, oBbox);\n if (bboxOverlap === null) return null;\n\n // We first check to see if the endpoints can be considered intersections.\n // This will 'snap' intersections to endpoints if possible, and will\n // handle cases of colinearity.\n\n const tlp = this.leftSE.point;\n const trp = this.rightSE.point;\n const olp = other.leftSE.point;\n const orp = other.rightSE.point;\n\n // does each endpoint touch the other segment?\n // note that we restrict the 'touching' definition to only allow segments\n // to touch endpoints that lie forward from where we are in the sweep line pass\n const touchesOtherLSE = isInBbox(tBbox, olp) && this.comparePoint(olp) === 0;\n const touchesThisLSE = isInBbox(oBbox, tlp) && other.comparePoint(tlp) === 0;\n const touchesOtherRSE = isInBbox(tBbox, orp) && this.comparePoint(orp) === 0;\n const touchesThisRSE = isInBbox(oBbox, trp) && other.comparePoint(trp) === 0;\n\n // do left endpoints match?\n if (touchesThisLSE && touchesOtherLSE) {\n // these two cases are for colinear segments with matching left\n // endpoints, and one segment being longer than the other\n if (touchesThisRSE && !touchesOtherRSE) return trp;\n if (!touchesThisRSE && touchesOtherRSE) return orp;\n // either the two segments match exactly (two trival intersections)\n // or just on their left endpoint (one trivial intersection\n return null;\n }\n\n // does this left endpoint matches (other doesn't)\n if (touchesThisLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesOtherRSE) {\n if (tlp.x === orp.x && tlp.y === orp.y) return null;\n }\n // t-intersection on left endpoint\n return tlp;\n }\n\n // does other left endpoint matches (this doesn't)\n if (touchesOtherLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesThisRSE) {\n if (trp.x === olp.x && trp.y === olp.y) return null;\n }\n // t-intersection on left endpoint\n return olp;\n }\n\n // trivial intersection on right endpoints\n if (touchesThisRSE && touchesOtherRSE) return null;\n\n // t-intersections on just one right endpoint\n if (touchesThisRSE) return trp;\n if (touchesOtherRSE) return orp;\n\n // None of our endpoints intersect. Look for a general intersection between\n // infinite lines laid over the segments\n const pt = intersection$1(tlp, this.vector(), olp, other.vector());\n\n // are the segments parrallel? Note that if they were colinear with overlap,\n // they would have an endpoint intersection and that case was already handled above\n if (pt === null) return null;\n\n // is the intersection found between the lines not on the segments?\n if (!isInBbox(bboxOverlap, pt)) return null;\n\n // round the the computed point if needed\n return rounder.round(pt.x, pt.y);\n }\n\n /**\n * Split the given segment into multiple segments on the given points.\n * * Each existing segment will retain its leftSE and a new rightSE will be\n * generated for it.\n * * A new segment will be generated which will adopt the original segment's\n * rightSE, and a new leftSE will be generated for it.\n * * If there are more than two points given to split on, new segments\n * in the middle will be generated with new leftSE and rightSE's.\n * * An array of the newly generated SweepEvents will be returned.\n *\n * Warning: input array of points is modified\n */\n split(point) {\n const newEvents = [];\n const alreadyLinked = point.events !== undefined;\n const newLeftSE = new SweepEvent(point, true);\n const newRightSE = new SweepEvent(point, false);\n const oldRightSE = this.rightSE;\n this.replaceRightSE(newRightSE);\n newEvents.push(newRightSE);\n newEvents.push(newLeftSE);\n const newSeg = new Segment(newLeftSE, oldRightSE, this.rings.slice(), this.windings.slice());\n\n // when splitting a nearly vertical downward-facing segment,\n // sometimes one of the resulting new segments is vertical, in which\n // case its left and right events may need to be swapped\n if (SweepEvent.comparePoints(newSeg.leftSE.point, newSeg.rightSE.point) > 0) {\n newSeg.swapEvents();\n }\n if (SweepEvent.comparePoints(this.leftSE.point, this.rightSE.point) > 0) {\n this.swapEvents();\n }\n\n // in the point we just used to create new sweep events with was already\n // linked to other events, we need to check if either of the affected\n // segments should be consumed\n if (alreadyLinked) {\n newLeftSE.checkForConsuming();\n newRightSE.checkForConsuming();\n }\n return newEvents;\n }\n\n /* Swap which event is left and right */\n swapEvents() {\n const tmpEvt = this.rightSE;\n this.rightSE = this.leftSE;\n this.leftSE = tmpEvt;\n this.leftSE.isLeft = true;\n this.rightSE.isLeft = false;\n for (let i = 0, iMax = this.windings.length; i < iMax; i++) {\n this.windings[i] *= -1;\n }\n }\n\n /* Consume another segment. We take their rings under our wing\n * and mark them as consumed. Use for perfectly overlapping segments */\n consume(other) {\n let consumer = this;\n let consumee = other;\n while (consumer.consumedBy) consumer = consumer.consumedBy;\n while (consumee.consumedBy) consumee = consumee.consumedBy;\n const cmp = Segment.compare(consumer, consumee);\n if (cmp === 0) return; // already consumed\n // the winner of the consumption is the earlier segment\n // according to sweep line ordering\n if (cmp > 0) {\n const tmp = consumer;\n consumer = consumee;\n consumee = tmp;\n }\n\n // make sure a segment doesn't consume it's prev\n if (consumer.prev === consumee) {\n const tmp = consumer;\n consumer = consumee;\n consumee = tmp;\n }\n for (let i = 0, iMax = consumee.rings.length; i < iMax; i++) {\n const ring = consumee.rings[i];\n const winding = consumee.windings[i];\n const index = consumer.rings.indexOf(ring);\n if (index === -1) {\n consumer.rings.push(ring);\n consumer.windings.push(winding);\n } else consumer.windings[index] += winding;\n }\n consumee.rings = null;\n consumee.windings = null;\n consumee.consumedBy = consumer;\n\n // mark sweep events consumed as to maintain ordering in sweep event queue\n consumee.leftSE.consumedBy = consumer.leftSE;\n consumee.rightSE.consumedBy = consumer.rightSE;\n }\n\n /* The first segment previous segment chain that is in the result */\n prevInResult() {\n if (this._prevInResult !== undefined) return this._prevInResult;\n if (!this.prev) this._prevInResult = null;else if (this.prev.isInResult()) this._prevInResult = this.prev;else this._prevInResult = this.prev.prevInResult();\n return this._prevInResult;\n }\n beforeState() {\n if (this._beforeState !== undefined) return this._beforeState;\n if (!this.prev) this._beforeState = {\n rings: [],\n windings: [],\n multiPolys: []\n };else {\n const seg = this.prev.consumedBy || this.prev;\n this._beforeState = seg.afterState();\n }\n return this._beforeState;\n }\n afterState() {\n if (this._afterState !== undefined) return this._afterState;\n const beforeState = this.beforeState();\n this._afterState = {\n rings: beforeState.rings.slice(0),\n windings: beforeState.windings.slice(0),\n multiPolys: []\n };\n const ringsAfter = this._afterState.rings;\n const windingsAfter = this._afterState.windings;\n const mpsAfter = this._afterState.multiPolys;\n\n // calculate ringsAfter, windingsAfter\n for (let i = 0, iMax = this.rings.length; i < iMax; i++) {\n const ring = this.rings[i];\n const winding = this.windings[i];\n const index = ringsAfter.indexOf(ring);\n if (index === -1) {\n ringsAfter.push(ring);\n windingsAfter.push(winding);\n } else windingsAfter[index] += winding;\n }\n\n // calcualte polysAfter\n const polysAfter = [];\n const polysExclude = [];\n for (let i = 0, iMax = ringsAfter.length; i < iMax; i++) {\n if (windingsAfter[i] === 0) continue; // non-zero rule\n const ring = ringsAfter[i];\n const poly = ring.poly;\n if (polysExclude.indexOf(poly) !== -1) continue;\n if (ring.isExterior) polysAfter.push(poly);else {\n if (polysExclude.indexOf(poly) === -1) polysExclude.push(poly);\n const index = polysAfter.indexOf(ring.poly);\n if (index !== -1) polysAfter.splice(index, 1);\n }\n }\n\n // calculate multiPolysAfter\n for (let i = 0, iMax = polysAfter.length; i < iMax; i++) {\n const mp = polysAfter[i].multiPoly;\n if (mpsAfter.indexOf(mp) === -1) mpsAfter.push(mp);\n }\n return this._afterState;\n }\n\n /* Is this segment part of the final result? */\n isInResult() {\n // if we've been consumed, we're not in the result\n if (this.consumedBy) return false;\n if (this._isInResult !== undefined) return this._isInResult;\n const mpsBefore = this.beforeState().multiPolys;\n const mpsAfter = this.afterState().multiPolys;\n switch (operation.type) {\n case \"union\":\n {\n // UNION - included iff:\n // * On one side of us there is 0 poly interiors AND\n // * On the other side there is 1 or more.\n const noBefores = mpsBefore.length === 0;\n const noAfters = mpsAfter.length === 0;\n this._isInResult = noBefores !== noAfters;\n break;\n }\n case \"intersection\":\n {\n // INTERSECTION - included iff:\n // * on one side of us all multipolys are rep. with poly interiors AND\n // * on the other side of us, not all multipolys are repsented\n // with poly interiors\n let least;\n let most;\n if (mpsBefore.length < mpsAfter.length) {\n least = mpsBefore.length;\n most = mpsAfter.length;\n } else {\n least = mpsAfter.length;\n most = mpsBefore.length;\n }\n this._isInResult = most === operation.numMultiPolys && least < most;\n break;\n }\n case \"xor\":\n {\n // XOR - included iff:\n // * the difference between the number of multipolys represented\n // with poly interiors on our two sides is an odd number\n const diff = Math.abs(mpsBefore.length - mpsAfter.length);\n this._isInResult = diff % 2 === 1;\n break;\n }\n case \"difference\":\n {\n // DIFFERENCE included iff:\n // * on exactly one side, we have just the subject\n const isJustSubject = mps => mps.length === 1 && mps[0].isSubject;\n this._isInResult = isJustSubject(mpsBefore) !== isJustSubject(mpsAfter);\n break;\n }\n default:\n throw new Error(`Unrecognized operation type found ${operation.type}`);\n }\n return this._isInResult;\n }\n }\n\n class RingIn {\n constructor(geomRing, poly, isExterior) {\n if (!Array.isArray(geomRing) || geomRing.length === 0) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n this.poly = poly;\n this.isExterior = isExterior;\n this.segments = [];\n if (typeof geomRing[0][0] !== \"number\" || typeof geomRing[0][1] !== \"number\") {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n const firstPoint = rounder.round(geomRing[0][0], geomRing[0][1]);\n this.bbox = {\n ll: {\n x: firstPoint.x,\n y: firstPoint.y\n },\n ur: {\n x: firstPoint.x,\n y: firstPoint.y\n }\n };\n let prevPoint = firstPoint;\n for (let i = 1, iMax = geomRing.length; i < iMax; i++) {\n if (typeof geomRing[i][0] !== \"number\" || typeof geomRing[i][1] !== \"number\") {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n let point = rounder.round(geomRing[i][0], geomRing[i][1]);\n // skip repeated points\n if (point.x === prevPoint.x && point.y === prevPoint.y) continue;\n this.segments.push(Segment.fromRing(prevPoint, point, this));\n if (point.x < this.bbox.ll.x) this.bbox.ll.x = point.x;\n if (point.y < this.bbox.ll.y) this.bbox.ll.y = point.y;\n if (point.x > this.bbox.ur.x) this.bbox.ur.x = point.x;\n if (point.y > this.bbox.ur.y) this.bbox.ur.y = point.y;\n prevPoint = point;\n }\n // add segment from last to first if last is not the same as first\n if (firstPoint.x !== prevPoint.x || firstPoint.y !== prevPoint.y) {\n this.segments.push(Segment.fromRing(prevPoint, firstPoint, this));\n }\n }\n getSweepEvents() {\n const sweepEvents = [];\n for (let i = 0, iMax = this.segments.length; i < iMax; i++) {\n const segment = this.segments[i];\n sweepEvents.push(segment.leftSE);\n sweepEvents.push(segment.rightSE);\n }\n return sweepEvents;\n }\n }\n class PolyIn {\n constructor(geomPoly, multiPoly) {\n if (!Array.isArray(geomPoly)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n this.exteriorRing = new RingIn(geomPoly[0], this, true);\n // copy by value\n this.bbox = {\n ll: {\n x: this.exteriorRing.bbox.ll.x,\n y: this.exteriorRing.bbox.ll.y\n },\n ur: {\n x: this.exteriorRing.bbox.ur.x,\n y: this.exteriorRing.bbox.ur.y\n }\n };\n this.interiorRings = [];\n for (let i = 1, iMax = geomPoly.length; i < iMax; i++) {\n const ring = new RingIn(geomPoly[i], this, false);\n if (ring.bbox.ll.x < this.bbox.ll.x) this.bbox.ll.x = ring.bbox.ll.x;\n if (ring.bbox.ll.y < this.bbox.ll.y) this.bbox.ll.y = ring.bbox.ll.y;\n if (ring.bbox.ur.x > this.bbox.ur.x) this.bbox.ur.x = ring.bbox.ur.x;\n if (ring.bbox.ur.y > this.bbox.ur.y) this.bbox.ur.y = ring.bbox.ur.y;\n this.interiorRings.push(ring);\n }\n this.multiPoly = multiPoly;\n }\n getSweepEvents() {\n const sweepEvents = this.exteriorRing.getSweepEvents();\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringSweepEvents = this.interiorRings[i].getSweepEvents();\n for (let j = 0, jMax = ringSweepEvents.length; j < jMax; j++) {\n sweepEvents.push(ringSweepEvents[j]);\n }\n }\n return sweepEvents;\n }\n }\n class MultiPolyIn {\n constructor(geom, isSubject) {\n if (!Array.isArray(geom)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n try {\n // if the input looks like a polygon, convert it to a multipolygon\n if (typeof geom[0][0][0] === \"number\") geom = [geom];\n } catch (ex) {\n // The input is either malformed or has empty arrays.\n // In either case, it will be handled later on.\n }\n this.polys = [];\n this.bbox = {\n ll: {\n x: Number.POSITIVE_INFINITY,\n y: Number.POSITIVE_INFINITY\n },\n ur: {\n x: Number.NEGATIVE_INFINITY,\n y: Number.NEGATIVE_INFINITY\n }\n };\n for (let i = 0, iMax = geom.length; i < iMax; i++) {\n const poly = new PolyIn(geom[i], this);\n if (poly.bbox.ll.x < this.bbox.ll.x) this.bbox.ll.x = poly.bbox.ll.x;\n if (poly.bbox.ll.y < this.bbox.ll.y) this.bbox.ll.y = poly.bbox.ll.y;\n if (poly.bbox.ur.x > this.bbox.ur.x) this.bbox.ur.x = poly.bbox.ur.x;\n if (poly.bbox.ur.y > this.bbox.ur.y) this.bbox.ur.y = poly.bbox.ur.y;\n this.polys.push(poly);\n }\n this.isSubject = isSubject;\n }\n getSweepEvents() {\n const sweepEvents = [];\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polySweepEvents = this.polys[i].getSweepEvents();\n for (let j = 0, jMax = polySweepEvents.length; j < jMax; j++) {\n sweepEvents.push(polySweepEvents[j]);\n }\n }\n return sweepEvents;\n }\n }\n\n class RingOut {\n /* Given the segments from the sweep line pass, compute & return a series\n * of closed rings from all the segments marked to be part of the result */\n static factory(allSegments) {\n const ringsOut = [];\n for (let i = 0, iMax = allSegments.length; i < iMax; i++) {\n const segment = allSegments[i];\n if (!segment.isInResult() || segment.ringOut) continue;\n let prevEvent = null;\n let event = segment.leftSE;\n let nextEvent = segment.rightSE;\n const events = [event];\n const startingPoint = event.point;\n const intersectionLEs = [];\n\n /* Walk the chain of linked events to form a closed ring */\n while (true) {\n prevEvent = event;\n event = nextEvent;\n events.push(event);\n\n /* Is the ring complete? */\n if (event.point === startingPoint) break;\n while (true) {\n const availableLEs = event.getAvailableLinkedEvents();\n\n /* Did we hit a dead end? This shouldn't happen.\n * Indicates some earlier part of the algorithm malfunctioned. */\n if (availableLEs.length === 0) {\n const firstPt = events[0].point;\n const lastPt = events[events.length - 1].point;\n throw new Error(`Unable to complete output ring starting at [${firstPt.x},` + ` ${firstPt.y}]. Last matching segment found ends at` + ` [${lastPt.x}, ${lastPt.y}].`);\n }\n\n /* Only one way to go, so cotinue on the path */\n if (availableLEs.length === 1) {\n nextEvent = availableLEs[0].otherSE;\n break;\n }\n\n /* We must have an intersection. Check for a completed loop */\n let indexLE = null;\n for (let j = 0, jMax = intersectionLEs.length; j < jMax; j++) {\n if (intersectionLEs[j].point === event.point) {\n indexLE = j;\n break;\n }\n }\n /* Found a completed loop. Cut that off and make a ring */\n if (indexLE !== null) {\n const intersectionLE = intersectionLEs.splice(indexLE)[0];\n const ringEvents = events.splice(intersectionLE.index);\n ringEvents.unshift(ringEvents[0].otherSE);\n ringsOut.push(new RingOut(ringEvents.reverse()));\n continue;\n }\n /* register the intersection */\n intersectionLEs.push({\n index: events.length,\n point: event.point\n });\n /* Choose the left-most option to continue the walk */\n const comparator = event.getLeftmostComparator(prevEvent);\n nextEvent = availableLEs.sort(comparator)[0].otherSE;\n break;\n }\n }\n ringsOut.push(new RingOut(events));\n }\n return ringsOut;\n }\n constructor(events) {\n this.events = events;\n for (let i = 0, iMax = events.length; i < iMax; i++) {\n events[i].segment.ringOut = this;\n }\n this.poly = null;\n }\n getGeom() {\n // Remove superfluous points (ie extra points along a straight line),\n let prevPt = this.events[0].point;\n const points = [prevPt];\n for (let i = 1, iMax = this.events.length - 1; i < iMax; i++) {\n const pt = this.events[i].point;\n const nextPt = this.events[i + 1].point;\n if (compareVectorAngles(pt, prevPt, nextPt) === 0) continue;\n points.push(pt);\n prevPt = pt;\n }\n\n // ring was all (within rounding error of angle calc) colinear points\n if (points.length === 1) return null;\n\n // check if the starting point is necessary\n const pt = points[0];\n const nextPt = points[1];\n if (compareVectorAngles(pt, prevPt, nextPt) === 0) points.shift();\n points.push(points[0]);\n const step = this.isExteriorRing() ? 1 : -1;\n const iStart = this.isExteriorRing() ? 0 : points.length - 1;\n const iEnd = this.isExteriorRing() ? points.length : -1;\n const orderedPoints = [];\n for (let i = iStart; i != iEnd; i += step) orderedPoints.push([points[i].x, points[i].y]);\n return orderedPoints;\n }\n isExteriorRing() {\n if (this._isExteriorRing === undefined) {\n const enclosing = this.enclosingRing();\n this._isExteriorRing = enclosing ? !enclosing.isExteriorRing() : true;\n }\n return this._isExteriorRing;\n }\n enclosingRing() {\n if (this._enclosingRing === undefined) {\n this._enclosingRing = this._calcEnclosingRing();\n }\n return this._enclosingRing;\n }\n\n /* Returns the ring that encloses this one, if any */\n _calcEnclosingRing() {\n // start with the ealier sweep line event so that the prevSeg\n // chain doesn't lead us inside of a loop of ours\n let leftMostEvt = this.events[0];\n for (let i = 1, iMax = this.events.length; i < iMax; i++) {\n const evt = this.events[i];\n if (SweepEvent.compare(leftMostEvt, evt) > 0) leftMostEvt = evt;\n }\n let prevSeg = leftMostEvt.segment.prevInResult();\n let prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null;\n while (true) {\n // no segment found, thus no ring can enclose us\n if (!prevSeg) return null;\n\n // no segments below prev segment found, thus the ring of the prev\n // segment must loop back around and enclose us\n if (!prevPrevSeg) return prevSeg.ringOut;\n\n // if the two segments are of different rings, the ring of the prev\n // segment must either loop around us or the ring of the prev prev\n // seg, which would make us and the ring of the prev peers\n if (prevPrevSeg.ringOut !== prevSeg.ringOut) {\n if (prevPrevSeg.ringOut.enclosingRing() !== prevSeg.ringOut) {\n return prevSeg.ringOut;\n } else return prevSeg.ringOut.enclosingRing();\n }\n\n // two segments are from the same ring, so this was a penisula\n // of that ring. iterate downward, keep searching\n prevSeg = prevPrevSeg.prevInResult();\n prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null;\n }\n }\n }\n class PolyOut {\n constructor(exteriorRing) {\n this.exteriorRing = exteriorRing;\n exteriorRing.poly = this;\n this.interiorRings = [];\n }\n addInterior(ring) {\n this.interiorRings.push(ring);\n ring.poly = this;\n }\n getGeom() {\n const geom = [this.exteriorRing.getGeom()];\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (geom[0] === null) return null;\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringGeom = this.interiorRings[i].getGeom();\n // interior ring was all (within rounding error of angle calc) colinear points\n if (ringGeom === null) continue;\n geom.push(ringGeom);\n }\n return geom;\n }\n }\n class MultiPolyOut {\n constructor(rings) {\n this.rings = rings;\n this.polys = this._composePolys(rings);\n }\n getGeom() {\n const geom = [];\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polyGeom = this.polys[i].getGeom();\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (polyGeom === null) continue;\n geom.push(polyGeom);\n }\n return geom;\n }\n _composePolys(rings) {\n const polys = [];\n for (let i = 0, iMax = rings.length; i < iMax; i++) {\n const ring = rings[i];\n if (ring.poly) continue;\n if (ring.isExteriorRing()) polys.push(new PolyOut(ring));else {\n const enclosingRing = ring.enclosingRing();\n if (!enclosingRing.poly) polys.push(new PolyOut(enclosingRing));\n enclosingRing.poly.addInterior(ring);\n }\n }\n return polys;\n }\n }\n\n /**\n * NOTE: We must be careful not to change any segments while\n * they are in the SplayTree. AFAIK, there's no way to tell\n * the tree to rebalance itself - thus before splitting\n * a segment that's in the tree, we remove it from the tree,\n * do the split, then re-insert it. (Even though splitting a\n * segment *shouldn't* change its correct position in the\n * sweep line tree, the reality is because of rounding errors,\n * it sometimes does.)\n */\n\n class SweepLine {\n constructor(queue) {\n let comparator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Segment.compare;\n this.queue = queue;\n this.tree = new Tree(comparator);\n this.segments = [];\n }\n process(event) {\n const segment = event.segment;\n const newEvents = [];\n\n // if we've already been consumed by another segment,\n // clean up our body parts and get out\n if (event.consumedBy) {\n if (event.isLeft) this.queue.remove(event.otherSE);else this.tree.remove(segment);\n return newEvents;\n }\n const node = event.isLeft ? this.tree.add(segment) : this.tree.find(segment);\n if (!node) throw new Error(`Unable to find segment #${segment.id} ` + `[${segment.leftSE.point.x}, ${segment.leftSE.point.y}] -> ` + `[${segment.rightSE.point.x}, ${segment.rightSE.point.y}] ` + \"in SweepLine tree.\");\n let prevNode = node;\n let nextNode = node;\n let prevSeg = undefined;\n let nextSeg = undefined;\n\n // skip consumed segments still in tree\n while (prevSeg === undefined) {\n prevNode = this.tree.prev(prevNode);\n if (prevNode === null) prevSeg = null;else if (prevNode.key.consumedBy === undefined) prevSeg = prevNode.key;\n }\n\n // skip consumed segments still in tree\n while (nextSeg === undefined) {\n nextNode = this.tree.next(nextNode);\n if (nextNode === null) nextSeg = null;else if (nextNode.key.consumedBy === undefined) nextSeg = nextNode.key;\n }\n if (event.isLeft) {\n // Check for intersections against the previous segment in the sweep line\n let prevMySplitter = null;\n if (prevSeg) {\n const prevInter = prevSeg.getIntersection(segment);\n if (prevInter !== null) {\n if (!segment.isAnEndpoint(prevInter)) prevMySplitter = prevInter;\n if (!prevSeg.isAnEndpoint(prevInter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, prevInter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n }\n }\n\n // Check for intersections against the next segment in the sweep line\n let nextMySplitter = null;\n if (nextSeg) {\n const nextInter = nextSeg.getIntersection(segment);\n if (nextInter !== null) {\n if (!segment.isAnEndpoint(nextInter)) nextMySplitter = nextInter;\n if (!nextSeg.isAnEndpoint(nextInter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, nextInter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n }\n }\n\n // For simplicity, even if we find more than one intersection we only\n // spilt on the 'earliest' (sweep-line style) of the intersections.\n // The other intersection will be handled in a future process().\n if (prevMySplitter !== null || nextMySplitter !== null) {\n let mySplitter = null;\n if (prevMySplitter === null) mySplitter = nextMySplitter;else if (nextMySplitter === null) mySplitter = prevMySplitter;else {\n const cmpSplitters = SweepEvent.comparePoints(prevMySplitter, nextMySplitter);\n mySplitter = cmpSplitters <= 0 ? prevMySplitter : nextMySplitter;\n }\n\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n this.queue.remove(segment.rightSE);\n newEvents.push(segment.rightSE);\n const newEventsFromSplit = segment.split(mySplitter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n if (newEvents.length > 0) {\n // We found some intersections, so re-do the current event to\n // make sure sweep line ordering is totally consistent for later\n // use with the segment 'prev' pointers\n this.tree.remove(segment);\n newEvents.push(event);\n } else {\n // done with left event\n this.segments.push(segment);\n segment.prev = prevSeg;\n }\n } else {\n // event.isRight\n\n // since we're about to be removed from the sweep line, check for\n // intersections between our previous and next segments\n if (prevSeg && nextSeg) {\n const inter = prevSeg.getIntersection(nextSeg);\n if (inter !== null) {\n if (!prevSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, inter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n if (!nextSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, inter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n }\n }\n this.tree.remove(segment);\n }\n return newEvents;\n }\n\n /* Safely split a segment that is currently in the datastructures\n * IE - a segment other than the one that is currently being processed. */\n _splitSafely(seg, pt) {\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n // removeNode() doesn't work, so have re-find the seg\n // https://github.com/w8r/splay-tree/pull/5\n this.tree.remove(seg);\n const rightSE = seg.rightSE;\n this.queue.remove(rightSE);\n const newEvents = seg.split(pt);\n newEvents.push(rightSE);\n // splitting can trigger consumption\n if (seg.consumedBy === undefined) this.tree.add(seg);\n return newEvents;\n }\n }\n\n // Limits on iterative processes to prevent infinite loops - usually caused by floating-point math round-off errors.\n const POLYGON_CLIPPING_MAX_QUEUE_SIZE = typeof process !== \"undefined\" && process.env.POLYGON_CLIPPING_MAX_QUEUE_SIZE || 1000000;\n const POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS = typeof process !== \"undefined\" && process.env.POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS || 1000000;\n class Operation {\n run(type, geom, moreGeoms) {\n operation.type = type;\n rounder.reset();\n\n /* Convert inputs to MultiPoly objects */\n const multipolys = [new MultiPolyIn(geom, true)];\n for (let i = 0, iMax = moreGeoms.length; i < iMax; i++) {\n multipolys.push(new MultiPolyIn(moreGeoms[i], false));\n }\n operation.numMultiPolys = multipolys.length;\n\n /* BBox optimization for difference operation\n * If the bbox of a multipolygon that's part of the clipping doesn't\n * intersect the bbox of the subject at all, we can just drop that\n * multiploygon. */\n if (operation.type === \"difference\") {\n // in place removal\n const subject = multipolys[0];\n let i = 1;\n while (i < multipolys.length) {\n if (getBboxOverlap(multipolys[i].bbox, subject.bbox) !== null) i++;else multipolys.splice(i, 1);\n }\n }\n\n /* BBox optimization for intersection operation\n * If we can find any pair of multipolygons whose bbox does not overlap,\n * then the result will be empty. */\n if (operation.type === \"intersection\") {\n // TODO: this is O(n^2) in number of polygons. By sorting the bboxes,\n // it could be optimized to O(n * ln(n))\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const mpA = multipolys[i];\n for (let j = i + 1, jMax = multipolys.length; j < jMax; j++) {\n if (getBboxOverlap(mpA.bbox, multipolys[j].bbox) === null) return [];\n }\n }\n }\n\n /* Put segment endpoints in a priority queue */\n const queue = new Tree(SweepEvent.compare);\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const sweepEvents = multipolys[i].getSweepEvents();\n for (let j = 0, jMax = sweepEvents.length; j < jMax; j++) {\n queue.insert(sweepEvents[j]);\n if (queue.size > POLYGON_CLIPPING_MAX_QUEUE_SIZE) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n throw new Error(\"Infinite loop when putting segment endpoints in a priority queue \" + \"(queue size too big).\");\n }\n }\n }\n\n /* Pass the sweep line over those endpoints */\n const sweepLine = new SweepLine(queue);\n let prevQueueSize = queue.size;\n let node = queue.pop();\n while (node) {\n const evt = node.key;\n if (queue.size === prevQueueSize) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n const seg = evt.segment;\n throw new Error(`Unable to pop() ${evt.isLeft ? \"left\" : \"right\"} SweepEvent ` + `[${evt.point.x}, ${evt.point.y}] from segment #${seg.id} ` + `[${seg.leftSE.point.x}, ${seg.leftSE.point.y}] -> ` + `[${seg.rightSE.point.x}, ${seg.rightSE.point.y}] from queue.`);\n }\n if (queue.size > POLYGON_CLIPPING_MAX_QUEUE_SIZE) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n throw new Error(\"Infinite loop when passing sweep line over endpoints \" + \"(queue size too big).\");\n }\n if (sweepLine.segments.length > POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n throw new Error(\"Infinite loop when passing sweep line over endpoints \" + \"(too many sweep line segments).\");\n }\n const newEvents = sweepLine.process(evt);\n for (let i = 0, iMax = newEvents.length; i < iMax; i++) {\n const evt = newEvents[i];\n if (evt.consumedBy === undefined) queue.insert(evt);\n }\n prevQueueSize = queue.size;\n node = queue.pop();\n }\n\n // free some memory we don't need anymore\n rounder.reset();\n\n /* Collect and compile segments we're keeping into a multipolygon */\n const ringsOut = RingOut.factory(sweepLine.segments);\n const result = new MultiPolyOut(ringsOut);\n return result.getGeom();\n }\n }\n\n // singleton available by import\n const operation = new Operation();\n\n const union = function (geom) {\n for (var _len = arguments.length, moreGeoms = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n moreGeoms[_key - 1] = arguments[_key];\n }\n return operation.run(\"union\", geom, moreGeoms);\n };\n const intersection = function (geom) {\n for (var _len2 = arguments.length, moreGeoms = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n moreGeoms[_key2 - 1] = arguments[_key2];\n }\n return operation.run(\"intersection\", geom, moreGeoms);\n };\n const xor = function (geom) {\n for (var _len3 = arguments.length, moreGeoms = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {\n moreGeoms[_key3 - 1] = arguments[_key3];\n }\n return operation.run(\"xor\", geom, moreGeoms);\n };\n const difference = function (subjectGeom) {\n for (var _len4 = arguments.length, clippingGeoms = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {\n clippingGeoms[_key4 - 1] = arguments[_key4];\n }\n return operation.run(\"difference\", subjectGeom, clippingGeoms);\n };\n var index = {\n union: union,\n intersection: intersection,\n xor: xor,\n difference: difference\n };\n\n return index;\n\n}));\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { deepEqual } from 'fast-equals';\nimport turf_bboxClip from '@turf/bbox-clip';\nimport stringify from 'fast-json-stable-stringify';\nimport polygonClipping from 'polygon-clipping';\n\nimport Protobuf from 'pbf';\nimport { VectorTile } from '@mapbox/vector-tile';\n\nimport { utilHashcode, utilRebind, utilTiler } from '../util';\n\n\nvar tiler = utilTiler().tileSize(512).margin(1);\nvar dispatch = d3_dispatch('loadedData');\nvar _vtCache;\n\n\nfunction abortRequest(controller) {\n controller.abort();\n}\n\n\nfunction vtToGeoJSON(data, tile, mergeCache) {\n var vectorTile = new VectorTile(new Protobuf(data));\n var layers = Object.keys(vectorTile.layers);\n if (!Array.isArray(layers)) { layers = [layers]; }\n\n var features = [];\n layers.forEach(function(layerID) {\n var layer = vectorTile.layers[layerID];\n if (layer) {\n for (var i = 0; i < layer.length; i++) {\n var feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n var geometry = feature.geometry;\n\n // Treat all Polygons as MultiPolygons\n if (geometry.type === 'Polygon') {\n geometry.type = 'MultiPolygon';\n geometry.coordinates = [geometry.coordinates];\n }\n\n var isClipped = false;\n\n // Clip to tile bounds\n if (geometry.type === 'MultiPolygon') {\n var featureClip = turf_bboxClip(feature, tile.extent.rectangle());\n if (!deepEqual(feature.geometry, featureClip.geometry)) {\n // feature = featureClip;\n isClipped = true;\n }\n if (!feature.geometry.coordinates.length) continue; // not actually on this tile\n if (!feature.geometry.coordinates[0].length) continue; // not actually on this tile\n }\n\n // Generate some unique IDs and add some metadata\n var featurehash = utilHashcode(stringify(feature));\n var propertyhash = utilHashcode(stringify(feature.properties || {}));\n feature.__layerID__ = layerID.replace(/[^_a-zA-Z0-9\\-]/g, '_');\n feature.__featurehash__ = featurehash;\n feature.__propertyhash__ = propertyhash;\n features.push(feature);\n\n // Clipped Polygons at same zoom with identical properties can get merged\n if (isClipped && geometry.type === 'MultiPolygon') {\n var merged = mergeCache[propertyhash];\n if (merged && merged.length) {\n var other = merged[0];\n var coords = polygonClipping.union(\n feature.geometry.coordinates,\n other.geometry.coordinates\n );\n\n if (!coords || !coords.length) {\n continue; // something failed in polygon union\n }\n\n merged.push(feature);\n for (var j = 0; j < merged.length; j++) { // all these features get...\n merged[j].geometry.coordinates = coords; // same coords\n merged[j].__featurehash__ = featurehash; // same hash, so deduplication works\n }\n } else {\n mergeCache[propertyhash] = [feature];\n }\n }\n }\n }\n });\n\n return features;\n}\n\n\nfunction loadTile(source, tile) {\n if (source.loaded[tile.id] || source.inflight[tile.id]) return;\n\n var url = source.template\n .replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n // TMS-flipped y coordinate\n .replace(/\\{[t-]y\\}/, Math.pow(2, tile.xyz[2]) - tile.xyz[1] - 1)\n .replace(/\\{z(oom)?\\}/, tile.xyz[2])\n .replace(/\\{switch:([^}]+)\\}/, function(s, r) {\n var subdomains = r.split(',');\n return subdomains[(tile.xyz[0] + tile.xyz[1]) % subdomains.length];\n });\n\n\n var controller = new AbortController();\n source.inflight[tile.id] = controller;\n\n fetch(url, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n source.loaded[tile.id] = [];\n delete source.inflight[tile.id];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (!data) {\n throw new Error('No Data');\n }\n\n var z = tile.xyz[2];\n if (!source.canMerge[z]) {\n source.canMerge[z] = {}; // initialize mergeCache\n }\n\n source.loaded[tile.id] = vtToGeoJSON(data, tile, source.canMerge[z]);\n dispatch.call('loadedData');\n })\n .catch(function() {\n source.loaded[tile.id] = [];\n delete source.inflight[tile.id];\n });\n}\n\n\nexport default {\n\n init: function() {\n if (!_vtCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n\n reset: function() {\n for (var sourceID in _vtCache) {\n var source = _vtCache[sourceID];\n if (source && source.inflight) {\n Object.values(source.inflight).forEach(abortRequest);\n }\n }\n\n _vtCache = {};\n },\n\n\n addSource: function(sourceID, template) {\n _vtCache[sourceID] = { template: template, inflight: {}, loaded: {}, canMerge: {} };\n return _vtCache[sourceID];\n },\n\n\n data: function(sourceID, projection) {\n var source = _vtCache[sourceID];\n if (!source) return [];\n\n var tiles = tiler.getTiles(projection);\n var seen = {};\n var results = [];\n\n for (var i = 0; i < tiles.length; i++) {\n var features = source.loaded[tiles[i].id];\n if (!features || !features.length) continue;\n\n for (var j = 0; j < features.length; j++) {\n var feature = features[j];\n var hash = feature.__featurehash__;\n if (seen[hash]) continue;\n seen[hash] = true;\n\n // return a shallow copy, because the hash may change\n // later if this feature gets merged with another\n results.push(Object.assign({}, feature)); // shallow copy\n }\n }\n\n return results;\n },\n\n\n loadTiles: function(sourceID, template, projection) {\n var source = _vtCache[sourceID];\n if (!source) {\n source = this.addSource(sourceID, template);\n }\n\n var tiles = tiler.getTiles(projection);\n\n // abort inflight requests that are no longer needed\n Object.keys(source.inflight).forEach(function(k) {\n var wanted = tiles.find(function(tile) { return k === tile.id; });\n if (!wanted) {\n abortRequest(source.inflight[k]);\n delete source.inflight[k];\n }\n });\n\n tiles.forEach(function(tile) {\n loadTile(source, tile);\n });\n },\n\n\n cache: function() {\n return _vtCache;\n }\n\n};\n", "import { json as d3_json } from 'd3-fetch';\n\nimport { utilQsString } from '../util';\nimport { localizer } from '../core/localizer';\n\nvar apibase = 'https://www.wikidata.org/w/api.php?';\nvar _wikidataCache = {};\n\n\nexport default {\n\n init: function() {},\n\n reset: function() {\n _wikidataCache = {};\n },\n\n\n // Search for Wikidata items matching the query\n itemsForSearchQuery: function(query, callback, language) {\n if (!query) {\n if (callback) callback('No query', {});\n return;\n }\n\n var lang = this.languagesToQuery()[0];\n\n var url = apibase + utilQsString({\n action: 'wbsearchentities',\n format: 'json',\n formatversion: 2,\n search: query,\n type: 'item',\n // the language to search\n language: language || lang,\n // the language for the label and description in the result\n uselang: lang,\n limit: 10,\n origin: '*'\n });\n\n d3_json(url)\n .then(result => {\n if (result && result.error) {\n if (result.error.code === 'badvalue' &&\n result.error.info.includes(lang) &&\n !language && lang.includes('-')) {\n // retry without \"country suffix\" region subtag\n this.itemsForSearchQuery(query, callback, lang.split('-')[0]);\n return;\n } else {\n throw new Error(result.error);\n }\n }\n if (callback) callback(null, result.search || {});\n })\n .catch(function(err) {\n if (callback) callback(err.message, {});\n });\n },\n\n\n // Given a Wikipedia language and article title,\n // return an array of corresponding Wikidata entities.\n itemsByTitle: function(lang, title, callback) {\n if (!title) {\n if (callback) callback('No title', {});\n return;\n }\n\n lang = lang || 'en';\n var url = apibase + utilQsString({\n action: 'wbgetentities',\n format: 'json',\n formatversion: 2,\n sites: lang.replace(/-/g, '_') + 'wiki',\n titles: title,\n languages: 'en', // shrink response by filtering to one language\n origin: '*'\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n }\n if (callback) callback(null, result.entities || {});\n })\n .catch(function(err) {\n if (callback) callback(err.message, {});\n });\n },\n\n\n languagesToQuery: function() {\n return localizer.localeCodes().map(function(code) {\n return code.toLowerCase();\n }).filter(function(code) {\n // HACK: en-us isn't a wikidata language. We should really be filtering by\n // the languages known to be supported by wikidata.\n return code !== 'en-us';\n });\n },\n\n\n entityByQID: function(qid, callback) {\n if (!qid) {\n callback('No qid', {});\n return;\n }\n if (_wikidataCache[qid]) {\n if (callback) callback(null, _wikidataCache[qid]);\n return;\n }\n\n var langs = this.languagesToQuery();\n var url = apibase + utilQsString({\n action: 'wbgetentities',\n format: 'json',\n formatversion: 2,\n ids: qid,\n props: 'labels|descriptions|claims|sitelinks',\n sitefilter: langs.map(function(d) { return d + 'wiki'; }).join('|'),\n languages: langs.join('|'),\n languagefallback: 1,\n origin: '*'\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n }\n if (callback) callback(null, result.entities[qid] || {});\n })\n .catch(function(err) {\n if (callback) callback(err.message, {});\n });\n },\n\n\n // Pass `params` object of the form:\n // {\n // qid: 'string' // brand wikidata (e.g. 'Q37158')\n // }\n //\n // Get an result object used to display tag documentation\n // {\n // title: 'string',\n // description: 'string',\n // editURL: 'string',\n // imageURL: 'string',\n // wiki: { title: 'string', text: 'string', url: 'string' }\n // }\n //\n getDocs: function(params, callback) {\n var langs = this.languagesToQuery();\n this.entityByQID(params.qid, function(err, entity) {\n if (err || !entity) {\n callback(err || 'No entity');\n return;\n }\n\n var i;\n var description;\n for (i in langs) {\n let code = langs[i];\n if (entity.descriptions[code] && entity.descriptions[code].language === code) {\n description = entity.descriptions[code];\n break;\n }\n }\n if (!description && Object.values(entity.descriptions).length) description = Object.values(entity.descriptions)[0];\n\n // prepare result\n var result = {\n title: entity.id,\n description: selection => selection.text(description ? description.value : ''),\n descriptionLocaleCode: description ? description.language : '',\n editURL: 'https://www.wikidata.org/wiki/' + entity.id\n };\n\n // add image\n if (entity.claims) {\n var imageroot = 'https://commons.wikimedia.org/w/index.php';\n var props = ['P154','P18']; // logo image, image\n var prop, image;\n for (i = 0; i < props.length; i++) {\n prop = entity.claims[props[i]];\n if (prop && Object.keys(prop).length > 0) {\n image = prop[Object.keys(prop)[0]].mainsnak.datavalue.value;\n if (image) {\n result.imageURL = imageroot + '?' + utilQsString({\n title: 'Special:Redirect/file/' + image,\n width: 400\n });\n break;\n }\n }\n }\n }\n\n if (entity.sitelinks) {\n var englishLocale = localizer.languageCode().toLowerCase() === 'en';\n\n // must be one of these that we requested..\n for (i = 0; i < langs.length; i++) { // check each, in order of preference\n var w = langs[i] + 'wiki';\n if (entity.sitelinks[w]) {\n var title = entity.sitelinks[w].title;\n var tKey = 'inspector.wiki_reference';\n if (!englishLocale && langs[i] === 'en') { // user's locale isn't English but\n tKey = 'inspector.wiki_en_reference'; // we are sending them to enwiki anyway..\n }\n\n result.wiki = {\n title: title,\n text: tKey,\n url: 'https://' + langs[i] + '.wikipedia.org/wiki/' + title.replace(/ /g, '_')\n };\n break;\n }\n }\n }\n\n callback(null, result);\n });\n }\n\n};\n", "import { json as d3_json } from 'd3-fetch';\n\nimport { utilQsString } from '../util';\n\n\nvar endpoint = 'https://en.wikipedia.org/w/api.php?';\n\nexport default {\n\n init: function() {},\n reset: function() {},\n\n\n search: function(lang, query, callback) {\n if (!query) {\n if (callback) callback('No Query', []);\n return;\n }\n\n lang = lang || 'en';\n var url = endpoint.replace('en', lang) +\n utilQsString({\n action: 'query',\n list: 'search',\n srlimit: '10',\n srinfo: 'suggestion',\n format: 'json',\n origin: '*',\n srsearch: query\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n } else if (!result || !result.query || !result.query.search) {\n throw new Error('No Results');\n }\n if (callback) {\n var titles = result.query.search.map(function(d) { return d.title; });\n callback(null, titles);\n }\n })\n .catch(function(err) {\n if (callback) callback(err, []);\n });\n },\n\n\n suggestions: function(lang, query, callback) {\n if (!query) {\n if (callback) callback('', []);\n return;\n }\n\n lang = lang || 'en';\n var url = endpoint.replace('en', lang) +\n utilQsString({\n action: 'opensearch',\n namespace: 0,\n suggest: '',\n format: 'json',\n origin: '*',\n search: query\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n } else if (!result || result.length < 2) {\n throw new Error('No Results');\n }\n if (callback) callback(null, result[1] || []);\n })\n .catch(function(err) {\n if (callback) callback(err.message, []);\n });\n },\n\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport { deepEqual } from 'fast-equals';\nimport Protobuf from 'pbf';\nimport RBush from 'rbush';\nimport { VectorTile } from '@mapbox/vector-tile';\n\nimport { utilRebind, utilTiler, utilQsString, utilStringQs, utilSetTransform } from '../util';\nimport { geoExtent } from '../geo';\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\nimport { localeDateString } from '../util/date';\n\nconst apiUrl = 'https://end.mapilio.com';\nconst imageBaseUrl = 'https://cdn.mapilio.com/im';\nconst baseTileUrl = 'https://geo.mapilio.com/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=mapilio:';\nconst pointLayer = 'map_points';\nconst lineLayer = 'map_roads_line';\nconst tileStyle = '&STYLE=&TILEMATRIX=EPSG:900913:{z}&TILEMATRIXSET=EPSG:900913&FORMAT=application/vnd.mapbox-vector-tile&TILECOL={x}&TILEROW={y}';\n\nconst minZoom = 14;\nconst dispatch = d3_dispatch('loadedImages', 'loadedLines');\nconst imgZoom = d3_zoom()\n .extent([[0, 0], [320, 240]])\n .translateExtent([[0, 0], [320, 240]])\n .scaleExtent([1, 15]);\nconst pannellumViewerCSS = 'pannellum/pannellum.css';\nconst pannellumViewerJS = 'pannellum/pannellum.js';\nconst resolution = 1080;\n\nlet _activeImage;\nlet _cache;\nlet _loadViewerPromise;\nlet _pannellumViewer;\nlet _sceneOptions = {\n showFullscreenCtrl: false,\n autoLoad: true,\n yaw: 0,\n minHfov: 10,\n maxHfov: 90,\n hfov: 60,\n};\nlet _currScene = 0;\n\n// Load all data for the specified type from Mapilio vector tiles\nfunction loadTiles(which, url, maxZoom, projection) {\n const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);\n const tiles = tiler.getTiles(projection);\n\n tiles.forEach(function(tile) {\n loadTile(which, url, tile);\n });\n}\n\n\n// Load all data for the specified type from one vector tile\nfunction loadTile(which, url, tile) {\n const cache = _cache.requests;\n const tileId = `${tile.id}-${which}`;\n if (cache.loaded[tileId] || cache.inflight[tileId]) return;\n const controller = new AbortController();\n cache.inflight[tileId] = controller;\n const requestUrl = url.replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n .replace('{z}', tile.xyz[2]);\n\n fetch(requestUrl, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (data.byteLength === 0) {\n throw new Error('No Data');\n }\n\n loadTileDataToCache(data, tile, which);\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n } else {\n dispatch.call('loadedLines');\n }\n })\n .catch(function (e) {\n if (e.message === 'No Data') {\n cache.loaded[tileId] = true;\n } else {\n console.error(e); // eslint-disable-line no-console\n }\n });\n}\n\n\n// Load the data from the vector tile into cache\nfunction loadTileDataToCache(data, tile) {\n const vectorTile = new VectorTile(new Protobuf(data));\n if (vectorTile.layers.hasOwnProperty(pointLayer)) {\n const features = [];\n const cache = _cache.images;\n const layer = vectorTile.layers[pointLayer];\n\n for (let i = 0; i < layer.length; i++) {\n const feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n const loc = feature.geometry.coordinates;\n\n let resolutionArr = feature.properties.resolution.split('x');\n let sourceWidth = Math.max(resolutionArr[0], resolutionArr[1]);\n let sourceHeight = Math.min(resolutionArr[0] ,resolutionArr[1]);\n let isPano = sourceWidth % sourceHeight === 0;\n\n const d = {\n service: 'photo',\n loc: loc,\n capture_time: feature.properties.capture_time,\n created_by_id: feature.properties.created_by_id,\n id: feature.properties.id,\n sequence_id: feature.properties.sequence_uuid,\n heading: feature.properties.heading,\n resolution: feature.properties.resolution,\n isPano: isPano\n };\n cache.forImageId[d.id] = d;\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty(lineLayer)) {\n const cache = _cache.sequences;\n const layer = vectorTile.layers[lineLayer];\n\n for (let i = 0; i < layer.length; i++) {\n const feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n if (cache.lineString[feature.properties.sequence_uuid]) {\n const cacheEntry = cache.lineString[feature.properties.sequence_uuid];\n if (cacheEntry.some(f => {\n // for some reason, mapilio sometimes returns a large amount of duplicate\n // sequence lines, causing very poor performance. this de-duplicates them,\n // see https://github.com/openstreetmap/iD/issues/10532\n const cachedCoords = f.geometry.coordinates;\n const featureCoords = feature.geometry.coordinates;\n return deepEqual(cachedCoords, featureCoords);\n })) continue;\n cacheEntry.push(feature);\n } else {\n cache.lineString[feature.properties.sequence_uuid] = [feature];\n }\n }\n }\n\n}\n\nfunction getImageData(imageId, sequenceId) {\n\n return fetch(apiUrl + `/api/sequence-detail?sequence_uuid=${sequenceId}`, {method: 'GET'})\n .then(function (response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n })\n .then(function (data) {\n let index = data.data.findIndex((feature) => feature.id === imageId);\n const {filename, uploaded_hash} = data.data[index];\n _sceneOptions.panorama = imageBaseUrl + '/' + uploaded_hash + '/' + filename + '/' + resolution;\n });\n}\n\nfunction getUserData(userId) {\n return fetch(apiUrl + `/api/search-user?options[parameters][id]=${userId}`, {method: 'GET'})\n .then(function (response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n })\n .then(function (data) {\n return data.data[0].username;\n });\n}\n\n\nexport default {\n // Initialize Mapilio\n init: function() {\n if (!_cache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n // Reset cache and state\n reset: function() {\n if (_cache) {\n Object.values(_cache.requests.inflight).forEach(function(request) { request.abort(); });\n }\n\n _cache = {\n images: { rtree: new RBush(), forImageId: {} },\n sequences: { rtree: new RBush(), lineString: {} },\n requests: { loaded: {}, inflight: {} }\n };\n },\n\n // Get visible images\n images: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _cache.images.rtree);\n },\n\n cachedImage: function(imageKey) {\n return _cache.images.forImageId[imageKey];\n },\n\n\n // Load images in the visible area\n loadImages: function(projection) {\n let url = baseTileUrl + pointLayer + tileStyle;\n loadTiles('images', url, 14, projection);\n },\n\n // Load line in the visible area\n loadLines: function(projection) {\n let url = baseTileUrl + lineLayer + tileStyle;\n loadTiles('line', url, 14, projection);\n },\n\n // Get visible sequences\n sequences: function(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const sequenceIds = {};\n let lineStrings = [];\n\n _cache.images.rtree.search(bbox)\n .forEach(function(d) {\n if (d.data.sequence_id) {\n sequenceIds[d.data.sequence_id] = true;\n }\n });\n\n Object.keys(sequenceIds).forEach(function(sequenceId) {\n if (_cache.sequences.lineString[sequenceId]) {\n lineStrings = lineStrings.concat(_cache.sequences.lineString[sequenceId]);\n }\n });\n\n return lineStrings;\n },\n\n // Set the currently visible image\n setActiveImage: function(image) {\n if (image) {\n _activeImage = {\n id: image.id,\n sequence_id: image.sequence_id\n };\n } else {\n _activeImage = null;\n }\n },\n\n\n // Update the currently highlighted sequence and selected bubble.\n setStyles: function(context, hovered) {\n const hoveredImageId = hovered && hovered.id;\n const hoveredSequenceId = hovered && hovered.sequence_id;\n const selectedSequenceId = _activeImage && _activeImage.sequence_id;\n const selectedImageId = _activeImage && _activeImage.id;\n\n const markers = context.container().selectAll('.layer-mapilio .viewfield-group');\n const sequences = context.container().selectAll('.layer-mapilio .sequence');\n\n markers.classed('highlighted', function(d) { return d.id === hoveredImageId; })\n .classed('hovered', function(d) { return d.id === hoveredImageId; })\n .classed('currentView', function(d) { return d.id === selectedImageId; });\n\n sequences.classed('highlighted', function(d) { return d.properties.sequence_uuid === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.sequence_uuid === selectedSequenceId; });\n\n return this;\n },\n\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'mapilio/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n initViewer: function () {\n if (!window.pannellum) return;\n if (_pannellumViewer) return;\n\n _currScene += 1;\n const sceneID = _currScene.toString();\n const options = {\n 'default': { firstScene: sceneID },\n scenes: {}\n };\n options.scenes[sceneID] = _sceneOptions;\n\n _pannellumViewer = window.pannellum.viewer('ideditor-viewer-mapilio-pnlm', options);\n },\n\n selectImage: function (context, id) {\n\n let that = this;\n\n let d = this.cachedImage(id);\n\n this.setActiveImage(d);\n\n this.updateUrlImage(d.id);\n\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null);\n\n if (!d) return this;\n\n let wrap = context.container().select('.photoviewer .mapilio-wrapper');\n let attribution = wrap.selectAll('.photo-attribution').text('\\u00A0');\n\n getUserData(d.created_by_id).then((username) => {\n if (username) {\n attribution\n .append('span')\n .attr('class', 'captured_by')\n .text('@' + username);\n attribution\n .append('span')\n .text('|');\n }\n }).finally(() => {\n if (d.capture_time) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeDateString(d.capture_time));\n attribution\n .append('span')\n .text('|');\n }\n attribution\n .append('a')\n .attr('class', 'image-link')\n .attr('target', '_blank')\n .attr('href', `https://mapilio.com/app?lat=${d.loc[1]}&lng=${d.loc[0]}&zoom=17&pId=${d.id}`)\n .text('mapilio.com');\n });\n\n wrap\n .transition()\n .duration(100)\n .call(imgZoom.transform, d3_zoomIdentity);\n\n wrap\n .selectAll('img')\n .remove();\n\n wrap\n .selectAll('button.back')\n .classed('hide', !_cache.images.forImageId.hasOwnProperty(+id - 1));\n wrap\n .selectAll('button.forward')\n .classed('hide', !_cache.images.forImageId.hasOwnProperty(+id + 1));\n\n\n getImageData(d.id,d.sequence_id).then(function () {\n\n if (d.isPano) {\n if (!_pannellumViewer) {\n that.initViewer();\n } else {\n // make a new scene\n _currScene += 1;\n let sceneID = _currScene.toString();\n _pannellumViewer\n .addScene(sceneID, _sceneOptions)\n .loadScene(sceneID);\n\n // remove previous scene\n if (_currScene > 2) {\n sceneID = (_currScene - 1).toString();\n _pannellumViewer\n .removeScene(sceneID);\n }\n }\n } else {\n // make non-panoramic photo viewer\n that.initOnlyPhoto(context);\n }\n });\n\n return this;\n },\n\n initOnlyPhoto: function (context) {\n\n if (_pannellumViewer) {\n _pannellumViewer.destroy();\n _pannellumViewer = null;\n }\n\n let wrap = context.container().select('#ideditor-viewer-mapilio-simple');\n\n let imgWrap = wrap.select('img');\n\n if (!imgWrap.empty()) {\n imgWrap.attr('src',_sceneOptions.panorama);\n } else {\n wrap.append('img')\n .attr('src',_sceneOptions.panorama);\n }\n\n },\n\n ensureViewerLoaded: function(context) {\n\n let that = this;\n\n let imgWrap = context.container().select('#ideditor-viewer-mapilio-simple > img');\n\n if (!imgWrap.empty()) {\n imgWrap.remove();\n }\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n let wrap = context.container().select('.photoviewer').selectAll('.mapilio-wrapper')\n .data([0]);\n\n let wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper mapilio-wrapper')\n .classed('hide', true)\n .on('dblclick.zoom', null);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n const controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls-mapilio');\n\n controlsEnter\n .append('button')\n .classed('back', true)\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .classed('forward', true)\n .on('click.forward', step(1))\n .text('\u25BA');\n\n wrapEnter\n .append('div')\n .attr('id', 'ideditor-viewer-mapilio-pnlm');\n\n wrapEnter\n .append('div')\n .attr('id', 'ideditor-viewer-mapilio-simple-wrap')\n .call(imgZoom.on('zoom', zoomPan))\n .append('div')\n .attr('id', 'ideditor-viewer-mapilio-simple');\n\n\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.mapilio', () => {\n if (_pannellumViewer) {\n _pannellumViewer.resize();\n }\n });\n\n _loadViewerPromise = new Promise((resolve, reject) => {\n let loadedCount = 0;\n function loaded() {\n loadedCount += 1;\n\n // wait until both files are loaded\n if (loadedCount === 2) resolve();\n }\n\n const head = d3_select('head');\n\n // load pannellum-viewercss\n head.selectAll('#ideditor-mapilio-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-mapilio-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(pannellumViewerCSS))\n .on('load.serviceMapilio', loaded)\n .on('error.serviceMapilio', function() {\n reject();\n });\n\n // load pannellum-viewerjs\n head.selectAll('#ideditor-mapilio-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-mapilio-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(pannellumViewerJS))\n .on('load.serviceMapilio', loaded)\n .on('error.serviceMapilio', function() {\n reject();\n });\n })\n .catch(function() {\n _loadViewerPromise = null;\n });\n\n function step(stepBy) {\n return function () {\n if (!_activeImage) return;\n const imageId = _activeImage.id;\n\n const nextIndex = imageId + stepBy;\n if (!nextIndex) return;\n\n const nextImage = _cache.images.forImageId[nextIndex];\n\n context.map().centerEase(nextImage.loc);\n\n that.selectImage(context, nextImage.id);\n };\n }\n\n function zoomPan(d3_event) {\n var t = d3_event.transform;\n context.container().select('.photoviewer #ideditor-viewer-mapilio-simple')\n .call(utilSetTransform, t.x, t.y, t.k);\n }\n\n return _loadViewerPromise;\n },\n\n showViewer:function (context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.mapilio-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.mapilio-wrapper')\n .classed('hide', false);\n }\n\n return this;\n },\n\n /**\n * hideViewer()\n */\n hideViewer: function (context) {\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n this.updateUrlImage(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n this.setActiveImage();\n return this.setStyles(context, null);\n },\n\n // Return the current cache\n cache: function() {\n return _cache;\n }\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport Protobuf from 'pbf';\nimport RBush from 'rbush';\nimport { VectorTile } from '@mapbox/vector-tile';\nimport { utilRebind, utilTiler, utilQsString, utilStringQs, utilUniqueDomId } from '../util';\nimport { geoExtent } from '../geo';\nimport { t } from '../core/localizer';\nimport { pannellumPhotoFrame } from './pannellum_photo';\nimport { planePhotoFrame } from './plane_photo';\nimport { services } from './';\nimport { partitionViewport } from '../util/partition';\nimport { localeDateString } from '../util/date';\n\n\nconst apiUrl = 'https://api.panoramax.xyz/';\nconst tileUrl = apiUrl + 'api/map/{z}/{x}/{y}.mvt';\nconst imageDataUrl = apiUrl + 'api/collections/{collectionId}/items/{itemId}';\nconst sequenceDataUrl = apiUrl + 'api/collections/{collectionId}/items?limit=1000';\nconst userIdUrl = apiUrl + 'api/users/search?q={username}';\nconst usernameURL = apiUrl + 'api/users/{userId}';\nconst viewerUrl = apiUrl;\n\nconst highDefinition = 'hd';\nconst standardDefinition = 'sd';\n\nconst pictureLayer = 'pictures';\nconst sequenceLayer = 'sequences';\n\nconst minZoom = 10;\nconst imageMinZoom = 15;\nconst lineMinZoom = 10;\nconst dispatch = d3_dispatch('loadedImages', 'loadedLines', 'viewerChanged');\n\nlet _cache;\nlet _loadViewerPromise;\nlet _definition = standardDefinition;\nlet _isHD = false;\n\nlet _planeFrame;\nlet _pannellumFrame;\nlet _currentFrame;\n\nlet _currentScene = {\n currentImage : null,\n nextImage : null,\n prevImage : null\n};\n\nlet _activeImage;\nlet _isViewerOpen = false;\n\n\n/**\n * Return no more than `limit` results per partition.\n * @param {number} limit Number of maximum objects to return\n * @param {*} projection Current projection\n * @param {*} rtree The cache\n * @returns Data found\n */\nfunction searchLimited(limit, projection, rtree) {\n limit = limit || 5;\n\n return partitionViewport(projection)\n .reduce(function(result, extent) {\n let found = rtree.search(extent.bbox());\n const spacing = Math.max(1, Math.floor(found.length / limit));\n found = found\n .filter((d, idx) => idx % spacing === 0 ||\n d.data.id === _activeImage?.id)\n .sort((a, b) => {\n if (a.data.id === _activeImage?.id) return -1;\n if (b.data.id === _activeImage?.id) return 1;\n return 0;\n })\n .slice(0, limit)\n .map(d => d.data);\n\n return (found.length ? result.concat(found) : result);\n }, []);\n}\n\n/**\n * Load all data for the specified type from Panoramax vector tiles\n * @param {string} which Either 'images' or 'lines'\n * @param {string} url Tile endpoint\n * @param {number} maxZoom Maximum zoom out\n * @param {*} projection Current projection\n * @param {number} zoom current zoom\n */\nfunction loadTiles(which, url, maxZoom, projection, zoom) {\n const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);\n const tiles = tiler.getTiles(projection);\n\n tiles.forEach(function(tile) {\n loadTile(which, url, tile, zoom);\n });\n}\n\n/**\n * Load all data for the specified type from one vector tile\n * @param {*} which Either 'images' or 'lines'\n * @param {*} url Tile endpoint\n * @param {*} tile Current tile\n * @param {*} zoom Current zoom\n */\nfunction loadTile(which, url, tile, zoom) {\n const cache = _cache.requests;\n const tileId = `${tile.id}-${which}`;\n if (cache.loaded[tileId] || cache.inflight[tileId]) return;\n const controller = new AbortController();\n cache.inflight[tileId] = controller;\n const requestUrl = url.replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n .replace('{z}', tile.xyz[2]);\n\n fetch(requestUrl, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (data.byteLength === 0) {\n throw new Error('No Data');\n }\n\n loadTileDataToCache(data, tile, zoom);\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n } else {\n dispatch.call('loadedLines');\n }\n })\n .catch(function (e) {\n if (e.message === 'No Data') {\n cache.loaded[tileId] = true;\n } else {\n console.error(e); // eslint-disable-line no-console\n }\n });\n}\n\n/**\n * Fetches all data for the specified tile and adds them to cache\n * @param {*} data Tile data\n * @param {*} tile Current tile\n * @param {*} zoom Current zoom\n */\nfunction loadTileDataToCache(data, tile, zoom) {\n const vectorTile = new VectorTile(new Protobuf(data));\n\n let features,\n cache,\n layer,\n i,\n feature,\n loc,\n d;\n\n if (vectorTile.layers.hasOwnProperty(pictureLayer)) {\n features = [];\n cache = _cache.images;\n layer = vectorTile.layers[pictureLayer];\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n\n d = {\n service: 'photo',\n loc: loc,\n capture_time: feature.properties.ts,\n capture_time_parsed: new Date(feature.properties.ts),\n id: feature.properties.id,\n account_id: feature.properties.account_id,\n sequence_id: feature.properties.first_sequence,\n heading: parseInt(feature.properties.heading, 10),\n image_path: '',\n isPano: feature.properties.type === 'equirectangular',\n model: feature.properties.model,\n };\n cache.forImageId[d.id] = d;\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty(sequenceLayer)) {\n\n cache = _cache.sequences;\n\n if (zoom >= lineMinZoom && zoom < imageMinZoom) cache = _cache.mockSequences;\n\n layer = vectorTile.layers[sequenceLayer];\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n if (cache.lineString[feature.properties.id]) {\n cache.lineString[feature.properties.id].push(feature);\n } else {\n cache.lineString[feature.properties.id] = [feature];\n }\n }\n }\n}\n\n/**\n * Fetches the username from Panoramax\n * @param {string} userId\n * @returns the username\n */\nasync function getUsername(userId) {\n const cache = _cache.users;\n if (cache[userId]) return cache[userId].name;\n\n const requestUrl = usernameURL.replace('{userId}', userId);\n\n const response = await fetch(requestUrl, { method: 'GET' });\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n const data = await response.json();\n cache[userId] = data;\n\n return data.name;\n}\n\nexport default {\n init: function() {\n if (!_cache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset: function() {\n if (_cache) {\n Object.values(_cache.requests.inflight).forEach(function(request) { request.abort(); });\n }\n\n _cache = {\n images: { rtree: new RBush(), forImageId: {} },\n sequences: { rtree: new RBush(), lineString: {}, items: {} },\n users: {},\n mockSequences: { rtree: new RBush(), lineString: {} },\n requests: { loaded: {}, inflight: {} }\n };\n },\n\n /**\n * Get visible images from cache\n * @param {*} projection Current Projection\n * @returns images data for the current projection\n */\n images: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _cache.images.rtree);\n },\n\n /**\n * Get a specific image from cache\n * @param {*} imageKey the image id\n * @returns\n */\n cachedImage: function(imageKey) {\n return _cache.images.forImageId[imageKey];\n },\n\n /**\n * Fetches images data for the visible area\n * @param {*} projection Current Projection\n */\n loadImages: function(projection) {\n loadTiles('images', tileUrl, imageMinZoom, projection);\n },\n\n /**\n * Fetches sequences data for the visible area\n * @param {*} projection Current Projection\n */\n loadLines: function(projection, zoom) {\n loadTiles('line', tileUrl, lineMinZoom, projection, zoom);\n },\n\n /**\n * Fetches all possible userIDs from Panoramax\n * @param {string} usernames one or multiple usernames\n * @returns userIDs\n */\n getUserIds: async function(usernames) {\n const requestUrls = usernames.map(username =>\n userIdUrl.replace('{username}', username));\n\n const responses = await Promise.all(requestUrls.map(requestUrl =>\n fetch(requestUrl, { method: 'GET' })));\n if (responses.some(response => !response.ok)) {\n const response = responses.find(response => !response.ok);\n throw new Error(response.status + ' ' + response.statusText);\n }\n const data = await Promise.all(responses.map(response => response.json()));\n // in panoramax, a username can have multiple ids, when the same name is\n // used on different servers\n return data.flatMap((d, i) => d.features.filter(f => f.name === usernames[i]).map(f => f.id));\n },\n\n /**\n * Get visible sequences from cache\n * @param {*} projection Current Projection\n * @param {number} zoom Current zoom (if zoom < `lineMinZoom` less accurate lines will be drawn)\n * @returns sequences data for the current projection\n */\n sequences: function(projection, zoom) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const sequenceIds = {};\n let lineStrings = [];\n\n if (zoom >= imageMinZoom){\n _cache.images.rtree.search(bbox).forEach(function(d) {\n if (d.data.sequence_id) {\n sequenceIds[d.data.sequence_id] = true;\n }\n });\n Object.keys(sequenceIds).forEach(function(sequenceId) {\n if (_cache.sequences.lineString[sequenceId]) {\n lineStrings = lineStrings.concat(_cache.sequences.lineString[sequenceId]);\n }\n });\n return lineStrings;\n }\n if (zoom >= lineMinZoom){\n Object.keys(_cache.mockSequences.lineString).forEach(function(sequenceId) {\n lineStrings = lineStrings.concat(_cache.mockSequences.lineString[sequenceId]);\n });\n }\n return lineStrings;\n },\n\n /**\n * Updates the data for the currently visible image\n * @param {*} image Image data\n */\n setActiveImage: function(image) {\n if (image && image.id && image.sequence_id) {\n _activeImage = {\n id: image.id,\n sequence_id: image.sequence_id,\n loc: image.loc\n };\n } else {\n _activeImage = null;\n }\n },\n\n getActiveImage: function(){\n return _activeImage;\n },\n\n /**\n * Update the currently highlighted sequence and selected bubble\n * @param {*} context Current HTML context\n * @param {*} [hovered] The hovered bubble image\n */\n setStyles: function(context, hovered) {\n const hoveredImageId = hovered && hovered.id;\n const hoveredSequenceId = hovered && hovered.sequence_id;\n const selectedSequenceId = _activeImage && _activeImage.sequence_id;\n const selectedImageId = _activeImage && _activeImage.id;\n\n const markers = context.container().selectAll('.layer-panoramax .viewfield-group');\n const sequences = context.container().selectAll('.layer-panoramax .sequence');\n\n markers\n .classed('highlighted', function(d) { return d.sequence_id === selectedSequenceId || d.id === hoveredImageId; })\n .classed('hovered', function(d) { return d.id === hoveredImageId; })\n .classed('currentView', function(d) { return d.id === selectedImageId; });\n\n sequences\n .classed('highlighted', function(d) { return d.properties.id === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.id === selectedSequenceId; });\n\n // update viewfields if needed\n context.container().selectAll('.layer-panoramax .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n let d = this.parentNode.__data__;\n if (d.isPano && d.id !== selectedImageId) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n return this;\n },\n\n // Get viewer status\n isViewerOpen: function() {\n return _isViewerOpen;\n },\n\n /**\n * Updates the URL to save the current shown image\n * @param {*} imageKey\n */\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'panoramax/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n /**\n * Loads the selected image in the frame\n * @param {*} context Current HTML context\n * @param {*} id of the selected image\n * @returns\n */\n selectImage: function (context, id) {\n let that = this;\n\n let d = that.cachedImage(id);\n that.setActiveImage(d);\n that.updateUrlImage(d.id);\n\n const viewerLink = `${viewerUrl}#pic=${d.id}&focus=pic`;\n\n let viewer = context.container()\n .select('.photoviewer');\n\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null);\n\n if (!d) return this;\n\n let wrap = context.container()\n .select('.photoviewer .panoramax-wrapper');\n\n let attribution = wrap.selectAll('.photo-attribution').text('');\n\n let line1 = attribution\n .append('div')\n .attr('class', 'attribution-row');\n\n const hdDomId = utilUniqueDomId('panoramax-hd');\n\n let label = line1\n .append('label')\n .attr('for', hdDomId)\n .attr('class', 'panoramax-hd');\n\n label\n .append('input')\n .attr('type', 'checkbox')\n .attr('id', hdDomId)\n .property('checked', _isHD)\n .on('click', (d3_event) => {\n d3_event.stopPropagation();\n _isHD = !_isHD;\n _definition = _isHD ? highDefinition : standardDefinition;\n that.selectImage(context, d.id)\n .showViewer(context);\n });\n\n label\n .append('span')\n .call(t.append('panoramax.hd'));\n\n if (d.capture_time) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeDateString(d.capture_time));\n\n attribution\n .append('span')\n .text('|');\n }\n\n attribution\n .append('a')\n .attr('class', 'report-photo')\n .attr('href', 'mailto:signalement.ign@panoramax.fr')\n .call(t.append('panoramax.report'));\n\n attribution\n .append('span')\n .text('|');\n\n attribution\n .append('a')\n .attr('class', 'image-link')\n .attr('target', '_blank')\n .attr('href', viewerLink)\n .text('panoramax.xyz');\n\n this.getImageData(d.sequence_id, d.id).then(function(data) {\n _currentScene = {\n currentImage: null,\n nextImage: null,\n prevImage: null\n };\n _currentScene.currentImage = data.assets[_definition];\n const nextIndex = data.links.findIndex(x => x.rel === 'next');\n const prevIndex = data.links.findIndex(x => x.rel === 'prev');\n\n if (nextIndex !== -1){\n _currentScene.nextImage = data.links[nextIndex];\n }\n if (prevIndex !== -1){\n _currentScene.prevImage = data.links[prevIndex];\n }\n\n d.image_path = _currentScene.currentImage.href;\n\n wrap\n .selectAll('button.back')\n .classed('hide', _currentScene.prevImage === null);\n wrap\n .selectAll('button.forward')\n .classed('hide', _currentScene.nextImage === null);\n\n _currentFrame = d.isPano ? _pannellumFrame : _planeFrame;\n\n _currentFrame\n .showPhotoFrame(wrap)\n .selectPhoto(d, true);\n });\n\n if (d.account_id) {\n attribution\n .append('span')\n .text('|');\n\n let line2 = attribution\n .append('span')\n .attr('class', 'attribution-row');\n\n getUsername(d.account_id).then(function(username){\n line2\n .append('span')\n .attr('class', 'captured_by')\n .text('@' + username);\n });\n }\n\n return this;\n },\n\n photoFrame: function() {\n return _currentFrame;\n },\n\n /**\n * Fetches the data for a specific image\n * @param {*} collectionId\n * @param {*} imageId\n * @returns The fetched image data\n */\n getImageData: async function(collectionId, imageId) {\n const cache = _cache.sequences.items;\n if (cache[collectionId]) {\n const cached = cache[collectionId]\n .find(d => d.id === imageId);\n if (cached) return cached;\n } else {\n // prime the cache with data from sequence\n const response = await fetch(sequenceDataUrl\n .replace('{collectionId}', collectionId),\n { method: 'GET' });\n\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n const data = (await response.json()).features;\n cache[collectionId] = data;\n }\n\n const result = cache[collectionId]\n .find(d => d.id === imageId);\n if (result) return result;\n\n // not found in sequence: retry to load single item data\n // ideally, we'd use the `withPicture` parameter, but it is buggy:\n // https://gitlab.com/panoramax/server/api/-/issues/268\n const itemResponse = await fetch(imageDataUrl\n .replace('{collectionId}', collectionId)\n .replace('{itemId}', imageId),\n { method: 'GET' });\n\n if (!itemResponse.ok) {\n throw new Error(itemResponse.status + ' ' + itemResponse.statusText);\n }\n const itemData = await itemResponse.json();\n cache[collectionId].push(itemData);\n return itemData;\n },\n\n ensureViewerLoaded: function(context) {\n\n let that = this;\n\n let imgWrap = context.container()\n .select('#ideditor-viewer-panoramax-simple > img');\n\n if (!imgWrap.empty()) {\n imgWrap.remove();\n }\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n let wrap = context.container()\n .select('.photoviewer')\n .selectAll('.panoramax-wrapper')\n .data([0]);\n\n let wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper panoramax-wrapper')\n .classed('hide', true)\n .on('dblclick.zoom', null);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n const controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls-panoramax');\n\n controlsEnter\n .append('button')\n .classed('back', true)\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .classed('forward', true)\n .on('click.forward', step(1))\n .text('\u25BA');\n\n // Register viewer resize handler\n _loadViewerPromise = Promise.all([\n pannellumPhotoFrame(context, wrapEnter),\n planePhotoFrame(context, wrapEnter)\n ]).then(([pannellumPhotoFrame, planePhotoFrame]) => {\n _pannellumFrame = pannellumPhotoFrame;\n _pannellumFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n _planeFrame = planePhotoFrame;\n _planeFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n });\n\n /**\n * Loads the next image in the sequence\n * @param {number} stepBy '-1' if backwards or '1' if forward\n * @returns\n */\n function step(stepBy) {\n return function () {\n if (!_currentScene.currentImage) return;\n\n let nextId;\n if (stepBy === 1) nextId = _currentScene.nextImage.id;\n else nextId = _currentScene.prevImage.id;\n\n if (!nextId) return;\n\n const nextImage = _cache.images.forImageId[nextId];\n\n if (nextImage){\n context.map().centerEase(nextImage.loc);\n that.selectImage(context, nextImage.id);\n }\n };\n }\n\n return _loadViewerPromise;\n },\n\n /**\n * Shows the current viewer if hidden\n * @param {*} context\n */\n showViewer: function (context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.panoramax-wrapper.hide').size();\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.panoramax-wrapper')\n .classed('hide', false);\n }\n\n _isViewerOpen = true;\n return this;\n },\n\n /**\n * Hides the current viewer if shown, resets the active image and sequence\n * @param {*} context\n */\n hideViewer: function (context) {\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n this.updateUrlImage(null);\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n this.setActiveImage(null);\n _isViewerOpen = false;\n\n return this.setStyles(context, null);\n },\n\n cache: function() {\n return _cache;\n }\n};\n", "import serviceOsmose from './osmose';\nimport serviceMapillary from './mapillary';\nimport serviceMapRules from './maprules';\nimport serviceNominatim from './nominatim';\nimport serviceNsi from './nsi';\nimport serviceKartaview from './kartaview';\nimport serviceVegbilder from './vegbilder';\nimport serviceOsm from './osm';\nimport serviceOsmWikibase from './osm_wikibase';\nimport serviceStreetside from './streetside';\nimport serviceTaginfo from './taginfo';\nimport serviceVectorTile from './vector_tile';\nimport serviceWikidata from './wikidata';\nimport serviceWikipedia from './wikipedia';\nimport serviceMapilio from './mapilio';\nimport servicePanoramax from './panoramax';\n\n\nexport let services = {\n geocoder: serviceNominatim,\n osmose: serviceOsmose,\n mapillary: serviceMapillary,\n nsi: serviceNsi,\n kartaview: serviceKartaview,\n vegbilder: serviceVegbilder,\n osm: serviceOsm,\n osmWikibase: serviceOsmWikibase,\n maprules: serviceMapRules,\n streetside: serviceStreetside,\n taginfo: serviceTaginfo,\n vectorTile: serviceVectorTile,\n wikidata: serviceWikidata,\n wikipedia: serviceWikipedia,\n mapilio: serviceMapilio,\n panoramax: servicePanoramax\n};\n\nexport {\n serviceOsmose,\n serviceMapillary,\n serviceMapRules,\n serviceNominatim,\n serviceNsi,\n serviceKartaview,\n serviceVegbilder,\n serviceOsm,\n serviceOsmWikibase,\n serviceStreetside,\n serviceTaginfo,\n serviceVectorTile,\n serviceWikidata,\n serviceWikipedia,\n serviceMapilio,\n servicePanoramax\n};\n", "import {\n geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon,\n geoSphericalDistance, geoVecInterp, geoHasSelfIntersections,\n geoSphericalClosestNode, geoAngle\n} from '../geo';\n\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionMergeNodes } from '../actions/merge_nodes';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilDatesOverlap } from '../util';\nimport { osmRoutableHighwayTagValues } from '../osm/tags';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\n\n/**\n * Look for roads that can be connected to other roads with a short extension\n */\nexport function validationAlmostJunction(context) {\n const type = 'almost_junction';\n const EXTEND_TH_METERS = 5;\n const WELD_TH_METERS = 0.75;\n // Comes from considering bounding case of parallel ways\n const CLOSE_NODE_TH = EXTEND_TH_METERS - WELD_TH_METERS;\n // Comes from considering bounding case of perpendicular ways\n const SIG_ANGLE_TH = Math.atan(WELD_TH_METERS / EXTEND_TH_METERS);\n\n function isHighway(entity) {\n return entity.type === 'way'\n && osmRoutableHighwayTagValues[entity.tags.highway];\n }\n\n function isTaggedAsNotContinuing(node) {\n return node.tags.noexit === 'yes'\n || node.tags.amenity === 'parking_entrance'\n || (node.tags.entrance && node.tags.entrance !== 'no');\n }\n\n\n const validation = function checkAlmostJunction(entity, graph) {\n if (!isHighway(entity)) return [];\n if (entity.isDegenerate()) return [];\n\n const tree = context.history().tree();\n const extendableNodeInfos = findConnectableEndNodesByExtension(entity);\n\n let issues = [];\n\n extendableNodeInfos.forEach(extendableNodeInfo => {\n issues.push(new validationIssue({\n type,\n subtype: 'highway-highway',\n severity: 'warning',\n message: function(context) {\n const entity1 = context.hasEntity(this.entityIds[0]);\n if (this.entityIds[0] === this.entityIds[2]) {\n return entity1 ? t.append('issues.almost_junction.self.message', {\n feature: utilDisplayLabel(entity1, context.graph())\n }) : '';\n } else {\n const entity2 = context.hasEntity(this.entityIds[2]);\n return (entity1 && entity2) ? t.append('issues.almost_junction.message', {\n feature: utilDisplayLabel(entity1, context.graph()),\n feature2: utilDisplayLabel(entity2, context.graph())\n }) : '';\n }\n },\n reference: showReference,\n entityIds: [\n entity.id,\n extendableNodeInfo.node.id,\n extendableNodeInfo.wid,\n ],\n loc: extendableNodeInfo.node.loc,\n hash: JSON.stringify(extendableNodeInfo.node.loc),\n data: {\n midId: extendableNodeInfo.mid.id,\n edge: extendableNodeInfo.edge,\n cross_loc: extendableNodeInfo.cross_loc\n },\n dynamicFixes: makeFixes\n }));\n });\n\n return issues;\n\n function makeFixes(context) {\n let fixes = [new validationIssueFix({\n icon: 'iD-icon-abutment',\n title: t.append('issues.fix.connect_features.title'),\n onClick: function(context) {\n const annotation = t('issues.fix.connect_almost_junction.annotation');\n const [, endNodeId, crossWayId] = this.issue.entityIds;\n const midNode = context.entity(this.issue.data.midId);\n const endNode = context.entity(endNodeId);\n const crossWay = context.entity(crossWayId);\n\n // When endpoints are close, just join if resulting small change in angle (#7201)\n const nearEndNodes = findNearbyEndNodes(endNode, crossWay);\n if (nearEndNodes.length > 0) {\n const collinear = findSmallJoinAngle(midNode, endNode, nearEndNodes);\n if (collinear) {\n context.perform(\n actionMergeNodes([collinear.id, endNode.id], collinear.loc),\n annotation\n );\n return;\n }\n }\n\n const targetEdge = this.issue.data.edge;\n const crossLoc = this.issue.data.cross_loc;\n const edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])];\n const closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc);\n\n // already a point nearby, just connect to that\n if (closestNodeInfo.distance < WELD_TH_METERS) {\n context.perform(\n actionMergeNodes([closestNodeInfo.node.id, endNode.id], closestNodeInfo.node.loc),\n annotation\n );\n // else add the end node to the edge way\n } else {\n context.perform(\n actionAddMidpoint({loc: crossLoc, edge: targetEdge}, endNode),\n annotation\n );\n }\n }\n })];\n\n const node = context.hasEntity(this.entityIds[1]);\n if (node && !node.hasInterestingTags()) {\n // node has no descriptive tags, suggest noexit fix\n fixes.push(new validationIssueFix({\n icon: 'maki-barrier',\n title: t.append('issues.fix.tag_as_disconnected.title'),\n onClick: function(context) {\n const nodeID = this.issue.entityIds[1];\n const tags = Object.assign({}, context.entity(nodeID).tags);\n tags.noexit = 'yes';\n context.perform(\n actionChangeTags(nodeID, tags),\n t('issues.fix.tag_as_disconnected.annotation')\n );\n }\n }));\n }\n\n // changing the date range is always an option\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-change-date-range',\n title: t.append('issues.fix.change_date_range.title')\n }));\n\n return fixes;\n }\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.almost_junction.highway-highway.reference'));\n }\n\n function isExtendableCandidate(node, way) {\n // can not accurately test vertices on tiles not downloaded from osm - #5938\n const osm = services.osm;\n if (osm && !osm.isDataLoaded(node.loc)) {\n return false;\n }\n if (isTaggedAsNotContinuing(node) || graph.parentWays(node).length !== 1) {\n return false;\n }\n\n let occurrences = 0;\n for (const index in way.nodes) {\n if (way.nodes[index] === node.id) {\n occurrences += 1;\n if (occurrences > 1) {\n return false;\n }\n }\n }\n return true;\n }\n\n function findConnectableEndNodesByExtension(way) {\n let results = [];\n if (way.isClosed()) return results;\n\n let testNodes;\n const indices = [0, way.nodes.length - 1];\n indices.forEach(nodeIndex => {\n const nodeID = way.nodes[nodeIndex];\n const node = graph.entity(nodeID);\n\n if (!isExtendableCandidate(node, way)) return;\n\n const connectionInfo = canConnectByExtend(way, nodeIndex);\n if (!connectionInfo) return;\n\n testNodes = graph.childNodes(way).slice(); // shallow copy\n testNodes[nodeIndex] = testNodes[nodeIndex].move(connectionInfo.cross_loc);\n\n // don't flag issue if connecting the ways would cause self-intersection\n if (geoHasSelfIntersections(testNodes, nodeID)) return;\n\n results.push(connectionInfo);\n });\n\n return results;\n }\n\n function findNearbyEndNodes(node, way) {\n return [\n way.nodes[0],\n way.nodes[way.nodes.length - 1]\n ].map(d => graph.entity(d))\n .filter(d => {\n // Node cannot be near to itself, but other endnode of same way could be\n return d.id !== node.id\n && geoSphericalDistance(node.loc, d.loc) <= CLOSE_NODE_TH;\n });\n }\n\n function findSmallJoinAngle(midNode, tipNode, endNodes) {\n // Both nodes could be close, so want to join whichever is closest to collinear\n let joinTo;\n let minAngle = Infinity;\n\n // Checks midNode -> tipNode -> endNode for collinearity\n endNodes.forEach(endNode => {\n const a1 = geoAngle(midNode, tipNode, context.projection) + Math.PI;\n const a2 = geoAngle(midNode, endNode, context.projection) + Math.PI;\n const diff = Math.max(a1, a2) - Math.min(a1, a2);\n\n if (diff < minAngle) {\n joinTo = endNode;\n minAngle = diff;\n }\n });\n\n /* Threshold set by considering right angle triangle\n based on node joining threshold and extension distance */\n if (minAngle <= SIG_ANGLE_TH) return joinTo;\n\n return null;\n }\n\n function hasTag(tags, key) {\n return tags[key] !== undefined && tags[key] !== 'no';\n }\n\n function canConnectWays(way, way2) {\n\n // allow self-connections\n if (way.id === way2.id) return true;\n\n // if one is bridge or tunnel, both must be bridge or tunnel\n if ((hasTag(way.tags, 'bridge') || hasTag(way2.tags, 'bridge')) &&\n !(hasTag(way.tags, 'bridge') && hasTag(way2.tags, 'bridge'))) return false;\n if ((hasTag(way.tags, 'tunnel') || hasTag(way2.tags, 'tunnel')) &&\n !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false;\n\n // must have equivalent layers and levels\n const layer1 = way.tags.layer || '0',\n layer2 = way2.tags.layer || '0';\n if (layer1 !== layer2) return false;\n\n const level1 = way.tags.level || '0',\n level2 = way2.tags.level || '0';\n if (level1 !== level2) return false;\n\n // must have overlapping date ranges\n if ((way.tags.start_date || way.tags.end_date) && (way2.tags.start_date || way2.tags.end_date)) {\n if (!utilDatesOverlap(way.tags, way2.tags)) return false;\n }\n\n return true;\n }\n\n function canConnectByExtend(way, endNodeIdx) {\n const tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point\n const midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge\n const tipNode = graph.entity(tipNid);\n const midNode = graph.entity(midNid);\n const lon = tipNode.loc[0];\n const lat = tipNode.loc[1];\n const lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2;\n const lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2;\n const queryExtent = geoExtent([\n [lon - lon_range, lat - lat_range],\n [lon + lon_range, lat + lat_range]\n ]);\n\n // first, extend the edge of [midNode -> tipNode] by EXTEND_TH_METERS and find the \"extended tip\" location\n const edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc);\n const t = EXTEND_TH_METERS / edgeLen + 1.0;\n const extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t);\n\n // then, check if the extension part [tipNode.loc -> extTipLoc] intersects any other ways\n const segmentInfos = tree.waySegments(queryExtent, graph);\n for (let i = 0; i < segmentInfos.length; i++) {\n let segmentInfo = segmentInfos[i];\n\n let way2 = graph.entity(segmentInfo.wayId);\n\n if (!isHighway(way2)) continue;\n\n if (!canConnectWays(way, way2)) continue;\n\n let nAid = segmentInfo.nodes[0],\n nBid = segmentInfo.nodes[1];\n\n if (nAid === tipNid || nBid === tipNid) continue;\n\n let nA = graph.entity(nAid),\n nB = graph.entity(nBid);\n let crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]);\n if (crossLoc) {\n return {\n mid: midNode,\n node: tipNode,\n wid: way2.id,\n edge: [nA.id, nB.id],\n cross_loc: crossLoc\n };\n }\n }\n return null;\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionMergeNodes } from '../actions/merge_nodes';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilDatesOverlap } from '../util';\nimport { t } from '../core/localizer';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { osmPathHighwayTagValues } from '../osm/tags';\nimport { geoMetersToLat, geoMetersToLon, geoSphericalDistance } from '../geo/geo';\nimport { geoExtent } from '../geo/extent';\n\nexport function validationCloseNodes(context) {\n var type = 'close_nodes';\n\n var pointThresholdMeters = 0.2;\n\n var validation = function(entity, graph) {\n if (entity.type === 'node') {\n return getIssuesForNode(entity);\n } else if (entity.type === 'way') {\n return getIssuesForWay(entity);\n }\n return [];\n\n function getIssuesForNode(node) {\n var parentWays = graph.parentWays(node);\n if (parentWays.length) {\n return getIssuesForVertex(node, parentWays);\n } else {\n return getIssuesForDetachedPoint(node);\n }\n }\n\n function wayTypeFor(way) {\n\n if (way.tags.boundary && way.tags.boundary !== 'no') return 'boundary';\n if (way.tags.indoor && way.tags.indoor !== 'no') return 'indoor';\n if ((way.tags.building && way.tags.building !== 'no') ||\n (way.tags['building:part'] && way.tags['building:part'] !== 'no')) return 'building';\n if (osmPathHighwayTagValues[way.tags.highway]) return 'path';\n\n var parentRelations = graph.parentRelations(way);\n for (var i in parentRelations) {\n var relation = parentRelations[i];\n\n if (relation.tags.type === 'boundary') return 'boundary';\n\n if (relation.isMultipolygon()) {\n if (relation.tags.indoor && relation.tags.indoor !== 'no') return 'indoor';\n if ((relation.tags.building && relation.tags.building !== 'no') ||\n (relation.tags['building:part'] && relation.tags['building:part'] !== 'no')) return 'building';\n }\n }\n\n return 'other';\n }\n\n function shouldCheckWay(way) {\n\n // don't flag issues where merging would create degenerate ways\n if (way.nodes.length <= 2 ||\n (way.isClosed() && way.nodes.length <= 4)) return false;\n\n var bbox = way.extent(graph).bbox();\n var hypotenuseMeters = geoSphericalDistance([bbox.minX, bbox.minY], [bbox.maxX, bbox.maxY]);\n // don't flag close nodes in very small ways\n if (hypotenuseMeters < 1.5) return false;\n\n return true;\n }\n\n function getIssuesForWay(way) {\n if (!shouldCheckWay(way)) return [];\n\n var issues = [],\n nodes = graph.childNodes(way);\n for (var i = 0; i < nodes.length - 1; i++) {\n var node1 = nodes[i];\n var node2 = nodes[i+1];\n\n var issue = getWayIssueIfAny(node1, node2, way);\n if (issue) issues.push(issue);\n }\n return issues;\n }\n\n function getIssuesForVertex(node, parentWays) {\n var issues = [];\n\n function checkForCloseness(node1, node2, way) {\n var issue = getWayIssueIfAny(node1, node2, way);\n if (issue) issues.push(issue);\n }\n\n for (var i = 0; i < parentWays.length; i++) {\n var parentWay = parentWays[i];\n\n if (!shouldCheckWay(parentWay)) continue;\n\n var lastIndex = parentWay.nodes.length - 1;\n for (var j = 0; j < parentWay.nodes.length; j++) {\n if (j !== 0) {\n if (parentWay.nodes[j-1] === node.id) {\n checkForCloseness(node, graph.entity(parentWay.nodes[j]), parentWay);\n }\n }\n if (j !== lastIndex) {\n if (parentWay.nodes[j+1] === node.id) {\n checkForCloseness(graph.entity(parentWay.nodes[j]), node, parentWay);\n }\n }\n }\n }\n return issues;\n }\n\n function thresholdMetersForWay(way) {\n if (!shouldCheckWay(way)) return 0;\n\n var wayType = wayTypeFor(way);\n\n // don't flag boundaries since they might be highly detailed and can't be easily verified\n if (wayType === 'boundary') return 0;\n // expect some features to be mapped with higher levels of detail\n if (wayType === 'indoor') return 0.01;\n if (wayType === 'building') return 0.05;\n if (wayType === 'path') return 0.1;\n return 0.2;\n }\n\n function getIssuesForDetachedPoint(node) {\n\n var issues = [];\n\n var lon = node.loc[0];\n var lat = node.loc[1];\n var lon_range = geoMetersToLon(pointThresholdMeters, lat) / 2;\n var lat_range = geoMetersToLat(pointThresholdMeters) / 2;\n var queryExtent = geoExtent([\n [lon - lon_range, lat - lat_range],\n [lon + lon_range, lat + lat_range]\n ]);\n\n var intersected = context.history().tree().intersects(queryExtent, graph);\n for (var j = 0; j < intersected.length; j++) {\n var nearby = intersected[j];\n\n if (nearby.id === node.id) continue;\n if (nearby.type !== 'node' || nearby.geometry(graph) !== 'point') continue;\n\n if (nearby.loc === node.loc ||\n geoSphericalDistance(node.loc, nearby.loc) < pointThresholdMeters) {\n\n // ignore stolperstein (https://wiki.openstreetmap.org/wiki/DE:Stolpersteine)\n if ('memorial:type' in node.tags && 'memorial:type' in nearby.tags && node.tags['memorial:type']==='stolperstein' && nearby.tags['memorial:type']==='stolperstein') continue;\n if ('memorial' in node.tags && 'memorial' in nearby.tags && node.tags.memorial==='stolperstein' && nearby.tags.memorial === 'stolperstein') continue;\n\n // allow very close points if tags indicate the z-axis might vary\n var zAxisKeys = { layer: true, level: true, 'addr:housenumber': true, 'addr:unit': true };\n var zAxisDifferentiates = false;\n for (var key in zAxisKeys) {\n var nodeValue = node.tags[key] || '0';\n var nearbyValue = nearby.tags[key] || '0';\n if (nodeValue !== nearbyValue) {\n zAxisDifferentiates = true;\n break;\n }\n }\n if (zAxisDifferentiates) continue;\n\n // allow features to overlap spatially if they don't overlap temporally\n if ((node.tags.start_date || node.tags.end_date) && (nearby.tags.start_date || nearby.tags.end_date)) {\n if (!utilDatesOverlap(node.tags, nearby.tags)) continue;\n }\n\n issues.push(new validationIssue({\n type: type,\n subtype: 'detached',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]),\n entity2 = context.hasEntity(this.entityIds[1]);\n return (entity && entity2) ? t.append('issues.close_nodes.detached.message', {\n feature: utilDisplayLabel(entity, context.graph()),\n feature2: utilDisplayLabel(entity2, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [node.id, nearby.id],\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-disconnect',\n title: t.append('issues.fix.move_points_apart.title')\n }),\n new validationIssueFix({\n icon: 'iD-icon-layers',\n title: t.append('issues.fix.use_different_layers_or_levels.title')\n }),\n new validationIssueFix({\n icon: 'iD-operation-change-date-range',\n title: t.append('issues.fix.change_date_range.title')\n })\n ];\n }\n }));\n }\n }\n\n return issues;\n\n function showReference(selection) {\n var referenceText = t('issues.close_nodes.detached.reference');\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .html(referenceText);\n }\n }\n\n function getWayIssueIfAny(node1, node2, way) {\n if (node1.id === node2.id ||\n (node1.hasInterestingTags() && node2.hasInterestingTags())) {\n return null;\n }\n\n if (node1.loc !== node2.loc) {\n var parentWays1 = graph.parentWays(node1);\n var parentWays2 = new Set(graph.parentWays(node2));\n\n var sharedWays = parentWays1.filter(function(parentWay) {\n return parentWays2.has(parentWay);\n });\n\n var thresholds = sharedWays.map(function(parentWay) {\n return thresholdMetersForWay(parentWay);\n });\n\n var threshold = Math.min(...thresholds);\n var distance = geoSphericalDistance(node1.loc, node2.loc);\n if (distance > threshold) return null;\n }\n\n return new validationIssue({\n type: type,\n subtype: 'vertices',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.close_nodes.message', { way: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: showReference,\n entityIds: [way.id, node1.id, node2.id],\n loc: node1.loc,\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-icon-plus',\n title: t.append('issues.fix.merge_points.title'),\n onClick: function(context) {\n var entityIds = this.issue.entityIds;\n var action = actionMergeNodes([entityIds[1], entityIds[2]]);\n context.perform(action, t('issues.fix.merge_close_vertices.annotation'));\n }\n }),\n new validationIssueFix({\n icon: 'iD-operation-disconnect',\n title: t.append('issues.fix.move_points_apart.title')\n })\n ];\n }\n });\n\n function showReference(selection) {\n var referenceText = t('issues.close_nodes.reference');\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .html(referenceText);\n }\n }\n\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { deepEqual } from 'fast-equals';\n\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionMergeNodes } from '../actions/merge_nodes';\nimport { actionSplit } from '../actions/split';\nimport { modeSelect } from '../modes/select';\nimport { geoAngle, geoExtent, geoLatToMeters, geoLonToMeters, geoLineIntersection,\n geoSphericalClosestNode, geoSphericalDistance, geoVecAngle, geoVecLength, geoMetersToLat, geoMetersToLon } from '../geo';\nimport { osmNode } from '../osm/node';\nimport { osmFlowingWaterwayTagValues, osmPathHighwayTagValues, osmRailwayTrackTagValues, osmRoutableAerowayTags, osmRoutableHighwayTagValues } from '../osm/tags';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilDatesOverlap } from '../util';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationCrossingWays(context) {\n var type = 'crossing_ways';\n\n // returns the way or its parent relation, whichever has a useful feature type\n function getFeatureWithFeatureTypeTagsForWay(way, graph) {\n if (getFeatureType(way, graph) === null) {\n // if the way doesn't match a feature type, check its parent relations\n var parentRels = graph.parentRelations(way);\n for (var i = 0; i < parentRels.length; i++) {\n var rel = parentRels[i];\n if (getFeatureType(rel, graph) !== null) {\n return rel;\n }\n }\n }\n return way;\n }\n\n\n function hasTag(tags, key) {\n return tags[key] !== undefined && tags[key] !== 'no';\n }\n\n function taggedAsIndoor(tags) {\n return hasTag(tags, 'indoor') ||\n hasTag(tags, 'level') ||\n tags.highway === 'corridor';\n }\n\n function allowsBridge(featureType) {\n return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway' || featureType === 'aeroway';\n }\n function allowsTunnel(featureType) {\n return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway';\n }\n\n // discard\n var ignoredBuildings = {\n demolished: true, dismantled: true, proposed: true, razed: true\n };\n\n\n function getFeatureType(entity, graph) {\n\n var geometry = entity.geometry(graph);\n if (geometry !== 'line' && geometry !== 'area') return null;\n\n var tags = entity.tags;\n\n if (tags.aeroway in osmRoutableAerowayTags) return 'aeroway';\n\n if (hasTag(tags, 'building') && !ignoredBuildings[tags.building]) return 'building';\n if (hasTag(tags, 'highway') && osmRoutableHighwayTagValues[tags.highway]) return 'highway';\n\n // don't check railway or waterway areas\n if (geometry !== 'line') return null;\n\n if (hasTag(tags, 'railway') && osmRailwayTrackTagValues[tags.railway]) return 'railway';\n if (hasTag(tags, 'waterway') && osmFlowingWaterwayTagValues[tags.waterway]) return 'waterway';\n\n return null;\n }\n\n\n function isLegitCrossing(tags1, featureType1, tags2, featureType2) {\n\n // assume 0 by default\n var level1 = tags1.level || '0';\n var level2 = tags2.level || '0';\n\n if (taggedAsIndoor(tags1) && taggedAsIndoor(tags2) && level1 !== level2) {\n // assume features don't interact if they're indoor on different levels\n return true;\n }\n\n // don't flag crossing waterways and pier/highways\n if (featureType1 === 'waterway' && featureType2 === 'highway' && tags2.man_made === 'pier') return true;\n if (featureType2 === 'waterway' && featureType1 === 'highway' && tags1.man_made === 'pier') return true;\n\n // allow features to overlap spatially if they don't overlap temporally\n if ((tags1.start_date || tags1.end_date) && (tags2.start_date || tags2.end_date)) {\n if (!utilDatesOverlap(tags1, tags2)) return true;\n }\n\n if (tags1.layer !== undefined && tags1.layer === tags2.layer) return false; // Warn if both have the same defined layer\n\n const isElement1Bridge = allowsBridge(featureType1) && hasTag(tags1, 'bridge');\n const isElement2Bridge = allowsBridge(featureType2) && hasTag(tags2, 'bridge');\n if (isElement1Bridge !== isElement2Bridge) return true; // Either one is bridge, the other is not\n\n const isElement1Tunnel = allowsTunnel(featureType1) && hasTag(tags1, 'tunnel');\n const isElement2Tunnel = allowsTunnel(featureType2) && hasTag(tags2, 'tunnel');\n if (isElement1Tunnel !== isElement2Tunnel ) return true; // Either one is tunnel, the other is not\n\n return (tags1.layer || '0') !== (tags2.layer || '0');\n }\n\n\n // highway values for which we shouldn't recommend connecting to waterways\n var highwaysDisallowingFords = {\n motorway: true, motorway_link: true, trunk: true, trunk_link: true,\n primary: true, primary_link: true, secondary: true, secondary_link: true\n };\n\n /**\n * @returns {object | null} the tags for the connecting node, or null if the entities should not be joined\n */\n function tagsForConnectionNodeIfAllowed(entity1, entity2, graph, lessLikelyTags) {\n var featureType1 = getFeatureType(entity1, graph);\n var featureType2 = getFeatureType(entity2, graph);\n\n var geometry1 = entity1.geometry(graph);\n var geometry2 = entity2.geometry(graph);\n var bothLines = geometry1 === 'line' && geometry2 === 'line';\n\n /**\n * @typedef {NonNullable>} FeatureType\n * @type {`${FeatureType}-${FeatureType}`}\n */\n const featureTypes = [featureType1, featureType2].sort().join('-');\n\n if (featureTypes === 'aeroway-aeroway') return {};\n\n if (featureTypes === 'aeroway-highway') {\n const isServiceRoad = entity1.tags.highway === 'service' || entity2.tags.highway === 'service';\n const isPath = entity1.tags.highway in osmPathHighwayTagValues || entity2.tags.highway in osmPathHighwayTagValues;\n // only significant roads get the aeroway=aircraft_crossing tag\n return isServiceRoad || isPath ? {} : { aeroway: 'aircraft_crossing' };\n }\n\n if (featureTypes === 'aeroway-railway') {\n return { aeroway: 'aircraft_crossing', railway: 'level_crossing' };\n }\n\n if (featureTypes === 'aeroway-waterway') return null;\n\n if (featureType1 === featureType2) {\n if (featureType1 === 'highway') {\n var entity1IsPath = osmPathHighwayTagValues[entity1.tags.highway];\n var entity2IsPath = osmPathHighwayTagValues[entity2.tags.highway];\n if ((entity1IsPath || entity2IsPath) && entity1IsPath !== entity2IsPath) {\n // one feature is a path but not both\n\n if (!bothLines) return {};\n\n var roadFeature = entity1IsPath ? entity2 : entity1;\n var pathFeature = entity1IsPath ? entity1 : entity2;\n // don't mark path connections with tracks as crossings\n if (roadFeature.tags.highway === 'track') {\n return {};\n }\n // a sidewalk crossing a driveway is unremarkable and unlikely to be interrupted by the driveway\n // a sidewalk crossing another kind of service road may be similarly unremarkable\n if (!lessLikelyTags &&\n roadFeature.tags.highway === 'service' &&\n pathFeature.tags.highway === 'footway' && pathFeature.tags.footway === 'sidewalk') {\n return {};\n }\n if (['marked', 'unmarked', 'traffic_signals', 'uncontrolled'].indexOf(pathFeature.tags.crossing) !== -1) {\n // if the path is a crossing, match the crossing type and markings\n var tags = { highway: 'crossing', crossing: pathFeature.tags.crossing };\n if ('crossing:markings' in pathFeature.tags) {\n tags['crossing:markings'] = pathFeature.tags['crossing:markings'];\n }\n return tags;\n }\n // don't add a `crossing` subtag to ambiguous crossings\n return { highway: 'crossing' };\n }\n return {};\n }\n if (featureType1 === 'waterway') return {};\n if (featureType1 === 'railway') {\n return { railway: 'railway_crossing' };\n }\n\n } else {\n if (featureTypes.indexOf('highway') !== -1) {\n if (featureTypes.indexOf('railway') !== -1) {\n if (!bothLines) return {};\n\n var isTram = entity1.tags.railway === 'tram' || entity2.tags.railway === 'tram';\n\n if (osmPathHighwayTagValues[entity1.tags.highway] ||\n osmPathHighwayTagValues[entity2.tags.highway]) {\n\n // path-tram connections use this tag\n if (isTram) return { railway: 'tram_crossing' };\n\n // other path-rail connections use this tag\n return { railway: 'crossing' };\n } else {\n // path-tram connections use this tag\n if (isTram) return { railway: 'tram_level_crossing' };\n\n // other road-rail connections use this tag\n return { railway: 'level_crossing' };\n }\n }\n\n if (featureTypes.indexOf('waterway') !== -1) {\n // do not allow fords on structures\n if (hasTag(entity1.tags, 'tunnel') && hasTag(entity2.tags, 'tunnel')) return null;\n if (hasTag(entity1.tags, 'bridge') && hasTag(entity2.tags, 'bridge')) return null;\n\n if (highwaysDisallowingFords[entity1.tags.highway] ||\n highwaysDisallowingFords[entity2.tags.highway]) {\n // do not allow fords on major highways\n return null;\n }\n return bothLines ? { ford: 'yes' } : {};\n }\n }\n }\n return null;\n }\n\n\n function findCrossingsByWay(way1, graph, tree) {\n var edgeCrossInfos = [];\n if (way1.type !== 'way') return edgeCrossInfos;\n\n var taggedFeature1 = getFeatureWithFeatureTypeTagsForWay(way1, graph);\n var way1FeatureType = getFeatureType(taggedFeature1, graph);\n if (way1FeatureType === null) return edgeCrossInfos;\n\n var checkedSingleCrossingWays = {};\n\n // declare vars ahead of time to reduce garbage collection\n var i, j;\n var extent;\n var n1, n2, nA, nB, nAId, nBId;\n var segment1, segment2;\n var oneOnly;\n var segmentInfos, segment2Info, way2, taggedFeature2, way2FeatureType;\n var way1Nodes = graph.childNodes(way1);\n var comparedWays = {};\n for (i = 0; i < way1Nodes.length - 1; i++) {\n n1 = way1Nodes[i];\n n2 = way1Nodes[i + 1];\n extent = geoExtent([\n [\n Math.min(n1.loc[0], n2.loc[0]),\n Math.min(n1.loc[1], n2.loc[1])\n ],\n [\n Math.max(n1.loc[0], n2.loc[0]),\n Math.max(n1.loc[1], n2.loc[1])\n ]\n ]);\n\n // Optimize by only checking overlapping segments, not every segment\n // of overlapping ways\n segmentInfos = tree.waySegments(extent, graph);\n\n for (j = 0; j < segmentInfos.length; j++) {\n segment2Info = segmentInfos[j];\n\n // don't check for self-intersection in this validation\n if (segment2Info.wayId === way1.id) continue;\n\n // skip if this way was already checked and only one issue is needed\n if (checkedSingleCrossingWays[segment2Info.wayId]) continue;\n\n // mark this way as checked even if there are no crossings\n comparedWays[segment2Info.wayId] = true;\n\n way2 = graph.hasEntity(segment2Info.wayId);\n if (!way2) continue;\n taggedFeature2 = getFeatureWithFeatureTypeTagsForWay(way2, graph);\n // only check crossing highway, waterway, building, and railway\n way2FeatureType = getFeatureType(taggedFeature2, graph);\n\n if (way2FeatureType === null ||\n isLegitCrossing(taggedFeature1.tags, way1FeatureType, taggedFeature2.tags, way2FeatureType)) {\n continue;\n }\n\n // create only one issue for building crossings\n oneOnly = way1FeatureType === 'building' || way2FeatureType === 'building';\n\n nAId = segment2Info.nodes[0];\n nBId = segment2Info.nodes[1];\n if (nAId === n1.id || nAId === n2.id ||\n nBId === n1.id || nBId === n2.id) {\n // n1 or n2 is a connection node; skip\n continue;\n }\n nA = graph.hasEntity(nAId);\n if (!nA) continue;\n nB = graph.hasEntity(nBId);\n if (!nB) continue;\n\n segment1 = [n1.loc, n2.loc];\n segment2 = [nA.loc, nB.loc];\n var point = geoLineIntersection(segment1, segment2);\n if (point) {\n edgeCrossInfos.push({\n wayInfos: [\n {\n way: way1,\n featureType: way1FeatureType,\n edge: [n1.id, n2.id]\n },\n {\n way: way2,\n featureType: way2FeatureType,\n edge: [nA.id, nB.id]\n }\n ],\n crossPoint: point\n });\n if (oneOnly) {\n checkedSingleCrossingWays[way2.id] = true;\n break;\n }\n }\n }\n }\n return edgeCrossInfos;\n }\n\n\n function waysToCheck(entity, graph) {\n var featureType = getFeatureType(entity, graph);\n if (!featureType) return [];\n\n if (entity.type === 'way') {\n return [entity];\n } else if (entity.type === 'relation') {\n return entity.members.reduce(function(array, member) {\n if (member.type === 'way' &&\n // only look at geometry ways\n (!member.role || member.role === 'outer' || member.role === 'inner')) {\n var entity = graph.hasEntity(member.id);\n // don't add duplicates\n if (entity && array.indexOf(entity) === -1) {\n array.push(entity);\n }\n }\n return array;\n }, []);\n }\n return [];\n }\n\n\n var validation = function checkCrossingWays(entity, graph) {\n\n var tree = context.history().tree();\n\n var ways = waysToCheck(entity, graph);\n\n var issues = [];\n // declare these here to reduce garbage collection\n var wayIndex, crossingIndex, crossings;\n for (wayIndex in ways) {\n crossings = findCrossingsByWay(ways[wayIndex], graph, tree);\n for (crossingIndex in crossings) {\n issues.push(createIssue(crossings[crossingIndex], graph));\n }\n }\n return issues;\n };\n\n\n function createIssue(crossing, graph) {\n\n // use the entities with the tags that define the feature type\n crossing.wayInfos.sort(function(way1Info, way2Info) {\n var type1 = way1Info.featureType;\n var type2 = way2Info.featureType;\n if (type1 === type2) {\n return utilDisplayLabel(way1Info.way, graph) > utilDisplayLabel(way2Info.way, graph);\n } else if (type1 === 'waterway') {\n return true;\n } else if (type2 === 'waterway') {\n return false;\n }\n return type1 < type2;\n });\n var entities = crossing.wayInfos.map(function(wayInfo) {\n return getFeatureWithFeatureTypeTagsForWay(wayInfo.way, graph);\n });\n var edges = [crossing.wayInfos[0].edge, crossing.wayInfos[1].edge];\n var featureTypes = [crossing.wayInfos[0].featureType, crossing.wayInfos[1].featureType];\n\n var connectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1], graph);\n\n var featureType1 = crossing.wayInfos[0].featureType;\n var featureType2 = crossing.wayInfos[1].featureType;\n\n var isCrossingIndoors = taggedAsIndoor(entities[0].tags) && taggedAsIndoor(entities[1].tags);\n var isCrossingTunnels = allowsTunnel(featureType1) && hasTag(entities[0].tags, 'tunnel') &&\n allowsTunnel(featureType2) && hasTag(entities[1].tags, 'tunnel');\n var isCrossingBridges = allowsBridge(featureType1) && hasTag(entities[0].tags, 'bridge') &&\n allowsBridge(featureType2) && hasTag(entities[1].tags, 'bridge');\n\n var subtype = [featureType1, featureType2].sort().join('-');\n\n var crossingTypeID = subtype;\n\n if (isCrossingIndoors) {\n crossingTypeID = 'indoor-indoor';\n } else if (isCrossingTunnels) {\n crossingTypeID = 'tunnel-tunnel';\n } else if (isCrossingBridges) {\n crossingTypeID = 'bridge-bridge';\n }\n if (connectionTags && (isCrossingIndoors || isCrossingTunnels || isCrossingBridges)) {\n crossingTypeID += '_connectable';\n }\n\n // Differentiate based on the loc rounded to 4 digits, since two ways can cross multiple times.\n var uniqueID = crossing.crossPoint[0].toFixed(4) + ',' + crossing.crossPoint[1].toFixed(4);\n\n return new validationIssue({\n type: type,\n subtype: subtype,\n severity: 'warning',\n message: function(context) {\n var graph = context.graph();\n var entity1 = graph.hasEntity(this.entityIds[0]),\n entity2 = graph.hasEntity(this.entityIds[1]);\n return (entity1 && entity2) ? t.append('issues.crossing_ways.message', {\n feature: utilDisplayLabel(entity1, graph, featureType1 === 'building'),\n feature2: utilDisplayLabel(entity2, graph, featureType2 === 'building')\n }) : '';\n },\n reference: showReference,\n entityIds: entities.map(function(entity) {\n return entity.id;\n }),\n data: {\n edges: edges,\n featureTypes: featureTypes,\n connectionTags: connectionTags\n },\n hash: uniqueID,\n loc: crossing.crossPoint,\n dynamicFixes: function(context) {\n var mode = context.mode();\n if (!mode || mode.id !== 'select' || mode.selectedIDs().length !== 1) return [];\n\n var selectedIndex = this.entityIds[0] === mode.selectedIDs()[0] ? 0 : 1;\n var selectedFeatureType = this.data.featureTypes[selectedIndex];\n var otherFeatureType = this.data.featureTypes[selectedIndex === 0 ? 1 : 0];\n\n var fixes = [];\n\n if (connectionTags) {\n fixes.push(makeConnectWaysFix(this.data.connectionTags));\n let lessLikelyConnectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1], graph, true);\n if (lessLikelyConnectionTags && !deepEqual(connectionTags, lessLikelyConnectionTags)) {\n fixes.push(makeConnectWaysFix(lessLikelyConnectionTags));\n }\n }\n\n if (isCrossingIndoors) {\n fixes.push(new validationIssueFix({\n icon: 'iD-icon-layers',\n title: t.append('issues.fix.use_different_levels.title')\n }));\n } else if (isCrossingTunnels ||\n isCrossingBridges ||\n featureType1 === 'building' ||\n featureType2 === 'building') {\n\n fixes.push(makeChangeLayerFix('higher'));\n fixes.push(makeChangeLayerFix('lower'));\n\n // can only add bridge/tunnel if both features are lines\n } else if (context.graph().geometry(this.entityIds[0]) === 'line' &&\n context.graph().geometry(this.entityIds[1]) === 'line') {\n\n // don't recommend adding bridges to waterways since they're uncommon\n if (allowsBridge(selectedFeatureType) && selectedFeatureType !== 'waterway') {\n fixes.push(makeAddBridgeOrTunnelFix('add_a_bridge', 'temaki-bridge', 'bridge'));\n }\n\n // don't recommend adding tunnels under waterways since they're uncommon\n var skipTunnelFix = otherFeatureType === 'waterway' && selectedFeatureType !== 'waterway';\n if (allowsTunnel(selectedFeatureType) && !skipTunnelFix) {\n if (selectedFeatureType === 'waterway') {\n // naming piped waterway \"tunnel\" is a confusing osmism, culvert should be more clear\n fixes.push(makeAddBridgeOrTunnelFix('add_a_culvert', 'temaki-waste', 'tunnel'));\n } else {\n fixes.push(makeAddBridgeOrTunnelFix('add_a_tunnel', 'temaki-tunnel', 'tunnel'));\n }\n }\n }\n\n // changing the date range is always an option\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-change-date-range',\n title: t.append('issues.fix.change_date_range.title')\n }));\n\n // repositioning the features is always an option\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-move',\n title: t.append('issues.fix.reposition_features.title')\n }));\n\n if (featureType1 === 'building' || featureType2 === 'building') {\n // if the validation is about overlapping buildings:\n // show \"reposition features\" suggestion first, as that is most often\n // most sensible fix for those errors, see #11329\n fixes.unshift(fixes.pop());\n }\n\n return fixes;\n }\n });\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.crossing_ways.' + crossingTypeID + '.reference'));\n }\n }\n\n function makeAddBridgeOrTunnelFix(fixTitleID, iconName, bridgeOrTunnel){\n return new validationIssueFix({\n icon: iconName,\n title: t.append('issues.fix.' + fixTitleID + '.title'),\n onClick: function(context) {\n var mode = context.mode();\n if (!mode || mode.id !== 'select') return;\n\n var selectedIDs = mode.selectedIDs();\n if (selectedIDs.length !== 1) return;\n\n var selectedWayID = selectedIDs[0];\n if (!context.hasEntity(selectedWayID)) return;\n\n var resultWayIDs = [selectedWayID];\n\n var edge, crossedEdge, crossedWayID;\n if (this.issue.entityIds[0] === selectedWayID) {\n edge = this.issue.data.edges[0];\n crossedEdge = this.issue.data.edges[1];\n crossedWayID = this.issue.entityIds[1];\n } else {\n edge = this.issue.data.edges[1];\n crossedEdge = this.issue.data.edges[0];\n crossedWayID = this.issue.entityIds[0];\n }\n\n var crossingLoc = this.issue.loc;\n\n var projection = context.projection;\n\n var action = function actionAddStructure(graph) {\n\n var edgeNodes = [graph.entity(edge[0]), graph.entity(edge[1])];\n\n var crossedWay = graph.hasEntity(crossedWayID);\n // use the explicit width of the crossed feature as the structure length, if available\n var structLengthMeters = crossedWay && isFinite(crossedWay.tags.width) && Number(crossedWay.tags.width);\n if (!structLengthMeters) {\n // if no explicit width is set, approximate the width based on the tags\n structLengthMeters = crossedWay && crossedWay.impliedLineWidthMeters();\n }\n if (structLengthMeters) {\n if (getFeatureType(crossedWay, graph) === 'railway') {\n // bridges over railways are generally much longer than the rail bed itself, compensate\n structLengthMeters *= 2;\n }\n } else {\n // should ideally never land here since all rail/water/road tags should have an implied width\n structLengthMeters = 8;\n }\n\n var a1 = geoAngle(edgeNodes[0], edgeNodes[1], projection) + Math.PI;\n var a2 = geoAngle(graph.entity(crossedEdge[0]), graph.entity(crossedEdge[1]), projection) + Math.PI;\n var crossingAngle = Math.max(a1, a2) - Math.min(a1, a2);\n if (crossingAngle > Math.PI) crossingAngle -= Math.PI;\n // lengthen the structure to account for the angle of the crossing\n structLengthMeters = ((structLengthMeters / 2) / Math.sin(crossingAngle)) * 2;\n\n // add padding since the structure must extend past the edges of the crossed feature\n structLengthMeters += 4;\n\n // clamp the length to a reasonable range\n structLengthMeters = Math.min(Math.max(structLengthMeters, 4), 50);\n\n function geomToProj(geoPoint) {\n return [\n geoLonToMeters(geoPoint[0], geoPoint[1]),\n geoLatToMeters(geoPoint[1])\n ];\n }\n function projToGeom(projPoint) {\n var lat = geoMetersToLat(projPoint[1]);\n return [\n geoMetersToLon(projPoint[0], lat),\n lat\n ];\n }\n\n var projEdgeNode1 = geomToProj(edgeNodes[0].loc);\n var projEdgeNode2 = geomToProj(edgeNodes[1].loc);\n\n var projectedAngle = geoVecAngle(projEdgeNode1, projEdgeNode2);\n\n var projectedCrossingLoc = geomToProj(crossingLoc);\n var linearToSphericalMetersRatio = geoVecLength(projEdgeNode1, projEdgeNode2) /\n geoSphericalDistance(edgeNodes[0].loc, edgeNodes[1].loc);\n\n function locSphericalDistanceFromCrossingLoc(angle, distanceMeters) {\n var lengthSphericalMeters = distanceMeters * linearToSphericalMetersRatio;\n return projToGeom([\n projectedCrossingLoc[0] + Math.cos(angle) * lengthSphericalMeters,\n projectedCrossingLoc[1] + Math.sin(angle) * lengthSphericalMeters\n ]);\n }\n\n var endpointLocGetter1 = function(lengthMeters) {\n return locSphericalDistanceFromCrossingLoc(projectedAngle, lengthMeters);\n };\n var endpointLocGetter2 = function(lengthMeters) {\n return locSphericalDistanceFromCrossingLoc(projectedAngle + Math.PI, lengthMeters);\n };\n\n // avoid creating very short edges from splitting too close to another node\n var minEdgeLengthMeters = 0.55;\n\n // decide where to bound the structure along the way, splitting as necessary\n function determineEndpoint(edge, endNode, locGetter) {\n var newNode;\n\n var idealLengthMeters = structLengthMeters / 2;\n\n // distance between the crossing location and the end of the edge,\n // the maximum length of this side of the structure\n var crossingToEdgeEndDistance = geoSphericalDistance(crossingLoc, endNode.loc);\n\n if (crossingToEdgeEndDistance - idealLengthMeters > minEdgeLengthMeters) {\n // the edge is long enough to insert a new node\n\n // the loc that would result in the full expected length\n var idealNodeLoc = locGetter(idealLengthMeters);\n\n newNode = osmNode();\n graph = actionAddMidpoint({ loc: idealNodeLoc, edge: edge }, newNode)(graph);\n\n } else {\n var edgeCount = 0;\n endNode.parentIntersectionWays(graph).forEach(function(way) {\n way.nodes.forEach(function(nodeID) {\n if (nodeID === endNode.id) {\n if ((endNode.id === way.first() && endNode.id !== way.last()) ||\n (endNode.id === way.last() && endNode.id !== way.first())) {\n edgeCount += 1;\n } else {\n edgeCount += 2;\n }\n }\n });\n });\n\n if (edgeCount >= 3) {\n // the end node is a junction, try to leave a segment\n // between it and the structure - #7202\n\n var insetLength = crossingToEdgeEndDistance - minEdgeLengthMeters;\n if (insetLength > minEdgeLengthMeters) {\n var insetNodeLoc = locGetter(insetLength);\n newNode = osmNode();\n graph = actionAddMidpoint({ loc: insetNodeLoc, edge: edge }, newNode)(graph);\n }\n }\n }\n\n // if the edge is too short to subdivide as desired, then\n // just bound the structure at the existing end node\n if (!newNode) newNode = endNode;\n\n var splitAction = actionSplit([newNode.id])\n .limitWays(resultWayIDs); // only split selected or created ways\n\n // do the split\n graph = splitAction(graph);\n if (splitAction.getCreatedWayIDs().length) {\n resultWayIDs.push(splitAction.getCreatedWayIDs()[0]);\n }\n\n return newNode;\n }\n\n var structEndNode1 = determineEndpoint(edge, edgeNodes[1], endpointLocGetter1);\n var structEndNode2 = determineEndpoint([edgeNodes[0].id, structEndNode1.id], edgeNodes[0], endpointLocGetter2);\n\n var structureWay = resultWayIDs.map(function(id) {\n return graph.entity(id);\n }).find(function(way) {\n return way.nodes.indexOf(structEndNode1.id) !== -1 &&\n way.nodes.indexOf(structEndNode2.id) !== -1;\n });\n\n var tags = Object.assign({}, structureWay.tags); // copy tags\n if (bridgeOrTunnel === 'bridge'){\n tags.bridge = 'yes';\n tags.layer = '1';\n } else {\n var tunnelValue = 'yes';\n if (getFeatureType(structureWay, graph) === 'waterway') {\n // use `tunnel=culvert` for waterways by default\n tunnelValue = 'culvert';\n }\n tags.tunnel = tunnelValue;\n tags.layer = '-1';\n }\n // apply the structure tags to the way\n graph = actionChangeTags(structureWay.id, tags)(graph);\n return graph;\n };\n\n context.perform(action, t('issues.fix.' + fixTitleID + '.annotation'));\n context.enter(modeSelect(context, resultWayIDs));\n }\n });\n }\n\n function makeConnectWaysFix(connectionTags) {\n\n var fixTitleID = 'connect_features';\n var fixIcon = 'iD-icon-crossing';\n if (connectionTags.highway === 'crossing') {\n fixTitleID = 'connect_using_crossing';\n fixIcon = 'temaki-pedestrian';\n }\n if (connectionTags.ford) {\n fixTitleID = 'connect_using_ford';\n fixIcon = 'roentgen-ford';\n }\n\n const fix = new validationIssueFix({\n icon: fixIcon,\n title: t.append('issues.fix.' + fixTitleID + '.title'),\n onClick: function(context) {\n var loc = this.issue.loc;\n var edges = this.issue.data.edges;\n\n context.perform(\n function actionConnectCrossingWays(graph) {\n // create the new node for the points\n var node = osmNode({ loc: loc, tags: connectionTags });\n graph = graph.replace(node);\n\n var nodesToMerge = [node.id];\n var mergeThresholdInMeters = 0.75;\n\n edges.forEach(function(edge) {\n var edgeNodes = [graph.entity(edge[0]), graph.entity(edge[1])];\n var nearby = geoSphericalClosestNode(edgeNodes, loc);\n // if there is already a suitable node nearby, use that\n // use the node if node has no interesting tags or if it is a crossing node #8326\n if ((!nearby.node.hasInterestingTags() || nearby.node.isCrossing()) && nearby.distance < mergeThresholdInMeters) {\n nodesToMerge.push(nearby.node.id);\n // else add the new node to the way\n } else {\n graph = actionAddMidpoint({loc: loc, edge: edge}, node)(graph);\n }\n });\n\n if (nodesToMerge.length > 1) {\n // if we're using nearby nodes, merge them with the new node\n graph = actionMergeNodes(nodesToMerge, loc)(graph);\n }\n\n return graph;\n },\n t('issues.fix.connect_crossing_features.annotation')\n );\n }\n });\n fix._connectionTags = connectionTags;\n return fix;\n }\n\n /** @returns {osmEntity | undefined} */\n function getSelectedFeature() {\n const mode = context.mode();\n if (!mode || mode.id !== 'select') return undefined;\n\n const selectedIDs = mode.selectedIDs();\n if (selectedIDs.length !== 1) return undefined;\n\n const selectedID = selectedIDs[0];\n\n const entity = context.hasEntity(selectedID);\n return entity;\n }\n\n /**\n * @param {\"higher\" | \"lower\"} higherOrLower\n * @returns {validationIssueFix | undefined}\n */\n function makeChangeLayerFix(higherOrLower) {\n const selectedFeature = getSelectedFeature();\n return new validationIssueFix({\n id: selectedFeature.id,\n icon: 'iD-icon-' + (higherOrLower === 'higher' ? 'up' : 'down'),\n title: selectedFeature\n ? t.append('issues.fix.tag_this_as_' + higherOrLower + '.informative_title', {\n feature: utilDisplayLabel(selectedFeature, context.graph())\n })\n // in this context, there is no selected feature so we\n // have to show a generic name\n : t.append('issues.fix.tag_this_as_' + higherOrLower + '.title'),\n\n onClick: function(context) {\n const entity = getSelectedFeature();\n const selectedID = entity.id;\n if (!entity) return;\n\n\n if (!this.issue.entityIds.some(function(entityId) {\n return entityId === selectedID;\n })) return;\n\n var tags = Object.assign({}, entity.tags); // shallow copy\n var layer = tags.layer && Number(tags.layer);\n if (layer && !isNaN(layer)) {\n if (higherOrLower === 'higher') {\n layer += 1;\n } else {\n layer -= 1;\n }\n } else {\n if (higherOrLower === 'higher') {\n layer = 1;\n } else {\n layer = -1;\n }\n }\n tags.layer = layer.toString();\n context.perform(\n actionChangeTags(entity.id, tags),\n t('operations.change_tags.annotation')\n );\n }\n });\n }\n\n validation.type = type;\n\n return validation;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { t } from '../core/localizer';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionMoveNode } from '../actions/move_node';\nimport { actionNoop } from '../actions/noop';\nimport { behaviorDraw } from './draw';\nimport { geoChooseEdge, geoHasSelfIntersections } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect } from '../modes/select';\nimport { osmNode } from '../osm/node';\nimport { utilRebind } from '../util/rebind';\nimport { utilKeybinding } from '../util';\n\nexport function behaviorDrawWay(context, wayID, mode, startGraph) {\n const keybinding = utilKeybinding('drawWay');\n\n var dispatch = d3_dispatch('rejectedSelfIntersection');\n\n var behavior = behaviorDraw(context);\n\n // Must be set by `drawWay.nodeIndex` before each install of this behavior.\n var _nodeIndex;\n\n var _origWay;\n var _wayGeometry;\n var _headNodeID;\n var _annotation;\n\n var _pointerHasMoved = false;\n\n // The osmNode to be placed.\n // This is temporary and just follows the mouse cursor until an \"add\" event occurs.\n var _drawNode;\n\n var _didResolveTempEdit = false;\n\n function createDrawNode(loc) {\n // don't make the draw node until we actually need it\n _drawNode = osmNode({ loc: loc });\n\n context.pauseChangeDispatch();\n context.replace(function actionAddDrawNode(graph) {\n // add the draw node to the graph and insert it into the way\n var way = graph.entity(wayID);\n return graph\n .replace(_drawNode)\n .replace(way.addNode(_drawNode.id, _nodeIndex));\n }, _annotation);\n context.resumeChangeDispatch();\n\n setActiveElements();\n }\n\n function removeDrawNode() {\n\n context.pauseChangeDispatch();\n context.replace(\n function actionDeleteDrawNode(graph) {\n var way = graph.entity(wayID);\n return graph\n .replace(way.removeNode(_drawNode.id))\n .remove(_drawNode);\n },\n _annotation\n );\n _drawNode = undefined;\n context.resumeChangeDispatch();\n }\n\n\n function keydown(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope')) {\n context.surface()\n .classed('nope-suppressed', true);\n }\n context.surface()\n .classed('nope', false)\n .classed('nope-disabled', true);\n }\n }\n\n\n function keyup(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope-suppressed')) {\n context.surface()\n .classed('nope', true);\n }\n context.surface()\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false);\n }\n }\n\n\n function allowsVertex(d) {\n return d.geometry(context.graph()) === 'vertex' || presetManager.allowsVertex(d, context.graph());\n }\n\n\n // related code\n // - `mode/drag_node.js` `doMove()`\n // - `behavior/draw.js` `click()`\n // - `behavior/draw_way.js` `move()`\n function move(d3_event, datum) {\n\n var loc = context.map().mouseCoordinates();\n\n if (!_drawNode) createDrawNode(loc);\n\n context.surface().classed('nope-disabled', d3_event.altKey);\n\n var targetLoc = datum && datum.properties && datum.properties.entity &&\n allowsVertex(datum.properties.entity) && datum.properties.entity.loc;\n var targetNodes = datum && datum.properties && datum.properties.nodes;\n\n if (targetLoc) { // snap to node/vertex - a point target with `.loc`\n loc = targetLoc;\n\n } else if (targetNodes) { // snap to way - a line target with `.nodes`\n var choice = geoChooseEdge(targetNodes, context.map().mouse(), context.projection, _drawNode.id);\n if (choice) {\n loc = choice.loc;\n }\n }\n\n context.replace(actionMoveNode(_drawNode.id, loc), _annotation);\n _drawNode = context.entity(_drawNode.id);\n checkGeometry(true /* includeDrawNode */);\n }\n\n\n // Check whether this edit causes the geometry to break.\n // If so, class the surface with a nope cursor.\n // `includeDrawNode` - Only check the relevant line segments if finishing drawing\n function checkGeometry(includeDrawNode) {\n var nopeDisabled = context.surface().classed('nope-disabled');\n var isInvalid = isInvalidGeometry(includeDrawNode);\n\n if (nopeDisabled) {\n context.surface()\n .classed('nope', false)\n .classed('nope-suppressed', isInvalid);\n } else {\n context.surface()\n .classed('nope', isInvalid)\n .classed('nope-suppressed', false);\n }\n }\n\n\n function isInvalidGeometry(includeDrawNode) {\n\n var testNode = _drawNode;\n\n // we only need to test the single way we're drawing\n var parentWay = context.graph().entity(wayID);\n var nodes = context.graph().childNodes(parentWay).slice(); // shallow copy\n\n if (includeDrawNode) {\n if (parentWay.isClosed()) {\n // don't test the last segment for closed ways - #4655\n // (still test the first segment)\n nodes.pop();\n }\n } else { // discount the draw node\n\n if (parentWay.isClosed()) {\n if (nodes.length < 3) return false;\n if (_drawNode) nodes.splice(-2, 1);\n testNode = nodes[nodes.length - 2];\n } else {\n // there's nothing we need to test if we ignore the draw node on open ways\n return false;\n }\n }\n\n return testNode && geoHasSelfIntersections(nodes, testNode.id);\n }\n\n\n function undone() {\n\n // undoing removed the temp edit\n _didResolveTempEdit = true;\n\n context.pauseChangeDispatch();\n\n var nextMode;\n\n if (context.graph() === startGraph) {\n // We've undone back to the initial state before we started drawing.\n // Just exit the draw mode without undoing whatever we did before\n // we entered the draw mode.\n nextMode = modeSelect(context, [wayID]);\n } else {\n // The `undo` only removed the temporary edit, so here we have to\n // manually undo to actually remove the last node we added. We can't\n // use the `undo` function since the initial \"add\" graph doesn't have\n // an annotation and so cannot be undone to.\n context.pop(1);\n\n // continue drawing\n nextMode = mode;\n }\n\n // clear the redo stack by adding and removing a blank edit\n context.perform(actionNoop());\n context.pop(1);\n\n context.resumeChangeDispatch();\n context.enter(nextMode);\n }\n\n\n function setActiveElements() {\n if (!_drawNode) return;\n\n context.surface().selectAll('.' + _drawNode.id)\n .classed('active', true);\n }\n\n\n function resetToStartGraph() {\n while (context.graph() !== startGraph) {\n context.pop();\n }\n }\n\n\n var drawWay = function(surface) {\n _drawNode = undefined;\n _didResolveTempEdit = false;\n _origWay = context.entity(wayID);\n\n if (typeof _nodeIndex === 'number') {\n _headNodeID = _origWay.nodes[_nodeIndex];\n } else if (_origWay.isClosed()) {\n _headNodeID = _origWay.nodes[_origWay.nodes.length - 2];\n } else {\n _headNodeID = _origWay.nodes[_origWay.nodes.length - 1];\n }\n\n _wayGeometry = _origWay.geometry(context.graph());\n _annotation = t((_origWay.nodes.length === (_origWay.isClosed() ? 2 : 1) ?\n 'operations.start.annotation.' :\n 'operations.continue.annotation.') + _wayGeometry\n );\n _pointerHasMoved = false;\n\n // Push an annotated state for undo to return back to.\n // We must make sure to replace or remove it later.\n context.pauseChangeDispatch();\n context.perform(actionNoop(), _annotation);\n context.resumeChangeDispatch();\n\n behavior.hover()\n .initialNodeID(_headNodeID);\n\n behavior\n .on('move', function() {\n _pointerHasMoved = true;\n move.apply(this, arguments);\n })\n .on('down', function() {\n move.apply(this, arguments);\n })\n .on('downcancel', function() {\n if (_drawNode) removeDrawNode();\n })\n .on('click', drawWay.add)\n .on('clickWay', drawWay.addWay)\n .on('clickNode', drawWay.addNode)\n .on('undo', context.undo)\n .on('cancel', drawWay.cancel)\n .on('finish', drawWay.finish);\n\n d3_select(window)\n .on('keydown.drawWay', keydown)\n .on('keyup.drawWay', keyup);\n\n context.map()\n .dblclickZoomEnable(false)\n .on('drawn.draw', setActiveElements);\n\n setActiveElements();\n\n surface.call(behavior);\n\n context.history()\n .on('undone.draw', undone);\n };\n\n\n drawWay.off = function(surface) {\n\n if (!_didResolveTempEdit) {\n // Drawing was interrupted unexpectedly.\n // This can happen if the user changes modes,\n // clicks geolocate button, a hashchange event occurs, etc.\n\n context.pauseChangeDispatch();\n resetToStartGraph();\n context.resumeChangeDispatch();\n }\n\n _drawNode = undefined;\n _nodeIndex = undefined;\n\n context.map()\n .on('drawn.draw', null);\n\n surface.call(behavior.off)\n .selectAll('.active')\n .classed('active', false);\n\n surface\n .classed('nope', false)\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false);\n\n d3_select(window)\n .on('keydown.drawWay', null)\n .on('keyup.drawWay', null);\n\n context.history()\n .on('undone.draw', null);\n };\n\n\n function attemptAdd(d, loc, doAdd) {\n\n if (_drawNode) {\n // move the node to the final loc in case move wasn't called\n // consistently (e.g. on touch devices)\n context.replace(actionMoveNode(_drawNode.id, loc), _annotation);\n _drawNode = context.entity(_drawNode.id);\n } else {\n createDrawNode(loc);\n }\n\n checkGeometry(true /* includeDrawNode */);\n if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {\n if (!_pointerHasMoved) {\n // prevent the temporary draw node from appearing on touch devices\n removeDrawNode();\n }\n dispatch.call('rejectedSelfIntersection', this);\n return; // can't click here\n }\n\n context.pauseChangeDispatch();\n doAdd();\n // we just replaced the temporary edit with the real one\n _didResolveTempEdit = true;\n context.resumeChangeDispatch();\n\n context.enter(mode);\n }\n\n\n // Accept the current position of the drawing node\n drawWay.add = function(loc, d) {\n attemptAdd(d, loc, function() {\n // don't need to do anything extra\n });\n };\n\n\n // Connect the way to an existing way\n drawWay.addWay = function(loc, edge, d) {\n attemptAdd(d, loc, function() {\n context.replace(\n actionAddMidpoint({ loc: loc, edge: edge }, _drawNode),\n _annotation\n );\n });\n };\n\n\n // Connect the way to an existing node\n drawWay.addNode = function(node, d) {\n\n // finish drawing if the mapper targets the prior node\n if (node.id === _headNodeID ||\n // or the first node when drawing an area\n (_origWay.isClosed() && node.id === _origWay.first())) {\n drawWay.finish();\n return;\n }\n\n attemptAdd(d, node.loc, function() {\n context.replace(\n function actionReplaceDrawNode(graph) {\n // remove the temporary draw node and insert the existing node\n // at the same index\n\n graph = graph\n .replace(graph.entity(wayID).removeNode(_drawNode.id))\n .remove(_drawNode);\n return graph\n .replace(graph.entity(wayID).addNode(node.id, _nodeIndex));\n },\n _annotation\n );\n });\n };\n\n /**\n * @param {(typeof osmWay)[]} ways\n * @returns {\"line\" | \"area\" | \"generic\"}\n */\n function getFeatureType(ways) {\n if (ways.every(way => way.isClosed())) return 'area';\n if (ways.every(way => !way.isClosed())) return 'line';\n return 'generic';\n }\n\n /** see PR #8671 */\n function followMode() {\n if (_didResolveTempEdit) return;\n\n try {\n\n // get the last 2 added nodes.\n // check if they are both part of only oneway (the same one)\n // check if the ways that they're part of are the same way\n // find index of the last two nodes, to determine the direction to travel around the existing way\n // add the next node to the way we are drawing\n\n // if we're drawing an area, the first node = last node.\n const isDrawingArea = _origWay.nodes[0] === _origWay.nodes.slice(-1)[0];\n\n const [secondLastNodeId, lastNodeId] = _origWay.nodes.slice(isDrawingArea ? -3 : -2);\n\n // Unlike startGraph, the full history graph may contain unsaved vertices to follow.\n // https://github.com/openstreetmap/iD/issues/8749\n const historyGraph = context.history().graph();\n if (!lastNodeId || !secondLastNodeId || !historyGraph.hasEntity(lastNodeId) || !historyGraph.hasEntity(secondLastNodeId)) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('operations.follow.error.needs_more_initial_nodes'))();\n return;\n }\n\n // If the way has looped over itself, follow some other way.\n const lastNodesParents = historyGraph.parentWays(historyGraph.entity(lastNodeId)).filter(w => w.id !== wayID);\n const secondLastNodesParents = historyGraph.parentWays(historyGraph.entity(secondLastNodeId)).filter(w => w.id !== wayID);\n\n const featureType = getFeatureType(lastNodesParents);\n\n if (lastNodesParents.length !== 1 || secondLastNodesParents.length === 0) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append(`operations.follow.error.intersection_of_multiple_ways.${featureType}`))();\n return;\n }\n\n // Check if the last node's parent is also the parent of the second last node.\n // The last node must only have one parent, but the second last node can have\n // multiple parents.\n if (!secondLastNodesParents.some(n => n.id === lastNodesParents[0].id)) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append(`operations.follow.error.intersection_of_different_ways.${featureType}`))();\n return;\n }\n\n const way = lastNodesParents[0];\n\n const indexOfLast = way.nodes.indexOf(lastNodeId);\n const indexOfSecondLast = way.nodes.indexOf(secondLastNodeId);\n\n // for a closed way, the first/last node is the same so it appears twice in the array,\n // but indexOf always finds the first occurrence. This is only an issue when following a way\n // in descending order\n const isDescendingPastZero = indexOfLast === way.nodes.length - 2 && indexOfSecondLast === 0;\n\n let nextNodeIndex = indexOfLast + (indexOfLast > indexOfSecondLast && !isDescendingPastZero ? 1 : -1);\n // if we're following a closed way and we pass the first/last node, the next index will be -1\n if (nextNodeIndex === -1) nextNodeIndex = indexOfSecondLast === 1 ? way.nodes.length - 2 : 1;\n\n const nextNode = historyGraph.entity(way.nodes[nextNodeIndex]);\n\n drawWay.addNode(nextNode, {\n geometry: { type: 'Point', coordinates: nextNode.loc },\n id: nextNode.id,\n properties: { target: true, entity: nextNode },\n });\n } catch {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('operations.follow.error.unknown'))();\n }\n }\n\n keybinding.on(t('operations.follow.key'), followMode);\n d3_select(document).call(keybinding);\n\n // Finish the draw operation, removing the temporary edit.\n // If the way has enough nodes to be valid, it's selected.\n // Otherwise, delete everything and return to browse mode.\n drawWay.finish = function() {\n checkGeometry(false /* includeDrawNode */);\n if (context.surface().classed('nope')) {\n dispatch.call('rejectedSelfIntersection', this);\n return; // can't click here\n }\n\n context.pauseChangeDispatch();\n // remove the temporary edit\n context.pop(1);\n _didResolveTempEdit = true;\n context.resumeChangeDispatch();\n\n var way = context.hasEntity(wayID);\n if (!way || way.isDegenerate()) {\n drawWay.cancel();\n return;\n }\n\n window.setTimeout(function() {\n context.map().dblclickZoomEnable(true);\n }, 1000);\n\n var isNewFeature = !mode.isContinuing;\n context.enter(modeSelect(context, [wayID]).newFeature(isNewFeature));\n };\n\n\n // Cancel the draw operation, delete everything, and return to browse mode.\n drawWay.cancel = function() {\n context.pauseChangeDispatch();\n resetToStartGraph();\n context.resumeChangeDispatch();\n\n window.setTimeout(function() {\n context.map().dblclickZoomEnable(true);\n }, 1000);\n\n context.surface()\n .classed('nope', false)\n .classed('nope-disabled', false)\n .classed('nope-suppressed', false);\n\n context.enter(modeBrowse(context));\n };\n\n\n drawWay.nodeIndex = function(val) {\n if (!arguments.length) return _nodeIndex;\n _nodeIndex = val;\n return drawWay;\n };\n\n\n drawWay.activeID = function() {\n if (!arguments.length) return _drawNode && _drawNode.id;\n // no assign\n return drawWay;\n };\n\n\n return utilRebind(drawWay, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDrawWay } from '../behavior/draw_way';\n\n\nexport function modeDrawLine(context, wayID, startGraph, button, affix, continuing) {\n var mode = {\n button: button,\n id: 'draw-line'\n };\n\n var behavior = behaviorDrawWay(context, wayID, mode, startGraph)\n .on('rejectedSelfIntersection.modeDrawLine', function() {\n context.ui().flash\n .iconName('#iD-icon-no')\n .label(t.append('self_intersection.error.lines'))();\n });\n\n mode.wayID = wayID;\n\n mode.isContinuing = continuing;\n\n mode.enter = function() {\n behavior\n .nodeIndex(affix === 'prefix' ? 0 : undefined);\n\n context.install(behavior);\n };\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n mode.selectedIDs = function() {\n return [wayID];\n };\n\n mode.activeID = function() {\n return (behavior && behavior.activeID()) || [];\n };\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { utilDetect } from '../util/detect';\n\n\n// Translate a MacOS key command into the appropriate Windows/Linux equivalent.\n// For example, \u2318Z -> Ctrl+Z\nexport var uiCmd = function (code) {\n var detected = utilDetect();\n\n if (detected.os === 'mac') {\n return code;\n }\n\n if (detected.os === 'win') {\n if (code === '\u2318\u21E7Z') return 'Ctrl+Y';\n }\n\n var result = '',\n replacements = {\n '\u2318': 'Ctrl',\n '\u21E7': 'Shift',\n '\u2325': 'Alt',\n '\u232B': 'Backspace',\n '\u2326': 'Delete'\n };\n\n for (var i = 0; i < code.length; i++) {\n if (code[i] in replacements) {\n result += replacements[code[i]] + (i < code.length - 1 ? '+' : '');\n } else {\n result += code[i];\n }\n }\n\n return result;\n};\n\n\n// return a display-focused string for a given keyboard code\nuiCmd.display = function(code) {\n if (code.length !== 1) return code;\n\n var detected = utilDetect();\n var mac = (detected.os === 'mac');\n var replacements = {\n '\u2318': mac ? '\u2318 ' + t('shortcuts.key.cmd') : t('shortcuts.key.ctrl'),\n '\u21E7': mac ? '\u21E7 ' + t('shortcuts.key.shift') : t('shortcuts.key.shift'),\n '\u2325': mac ? '\u2325 ' + t('shortcuts.key.option') : t('shortcuts.key.alt'),\n '\u2303': mac ? '\u2303 ' + t('shortcuts.key.ctrl') : t('shortcuts.key.ctrl'),\n '\u232B': mac ? '\u232B ' + t('shortcuts.key.delete') : t('shortcuts.key.backspace'),\n '\u2326': mac ? '\u2326 ' + t('shortcuts.key.del') : t('shortcuts.key.del'),\n '\u2196': mac ? '\u2196 ' + t('shortcuts.key.pgup') : t('shortcuts.key.pgup'),\n '\u2198': mac ? '\u2198 ' + t('shortcuts.key.pgdn') : t('shortcuts.key.pgdn'),\n '\u21DE': mac ? '\u21DE ' + t('shortcuts.key.home') : t('shortcuts.key.home'),\n '\u21DF': mac ? '\u21DF ' + t('shortcuts.key.end') : t('shortcuts.key.end'),\n '\u21B5': mac ? '\u23CE ' + t('shortcuts.key.return') : t('shortcuts.key.enter'),\n '\u238B': mac ? '\u238B ' + t('shortcuts.key.esc') : t('shortcuts.key.esc'),\n '\u2630': mac ? '\u2630 ' + t('shortcuts.key.menu') : t('shortcuts.key.menu'),\n };\n\n return replacements[code] || code;\n};\n", "import { t } from '../core/localizer';\nimport { actionDeleteMultiple } from '../actions/delete_multiple';\nimport { behaviorOperation } from '../behavior/operation';\nimport { geoSphericalDistance } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect } from '../modes/select';\nimport { uiCmd } from '../ui/cmd';\nimport { utilGetAllNodes, utilTotalExtent } from '../util';\n\n\nexport function operationDelete(context, selectedIDs) {\n var multi = (selectedIDs.length === 1 ? 'single' : 'multiple');\n var action = actionDeleteMultiple(selectedIDs);\n var nodes = utilGetAllNodes(selectedIDs, context.graph());\n var coords = nodes.map(function(n) { return n.loc; });\n var extent = utilTotalExtent(selectedIDs, context.graph());\n\n\n var operation = function() {\n var nextSelectedID;\n var nextSelectedLoc;\n\n if (selectedIDs.length === 1) {\n var id = selectedIDs[0];\n var entity = context.entity(id);\n var geometry = entity.geometry(context.graph());\n var parents = context.graph().parentWays(entity);\n var parent = parents[0];\n\n // Select the next closest node in the way.\n if (geometry === 'vertex') {\n var nodes = parent.nodes;\n var i = nodes.indexOf(id);\n\n if (i === 0) {\n i++;\n } else if (i === nodes.length - 1) {\n i--;\n } else {\n var a = geoSphericalDistance(entity.loc, context.entity(nodes[i - 1]).loc);\n var b = geoSphericalDistance(entity.loc, context.entity(nodes[i + 1]).loc);\n i = a < b ? i - 1 : i + 1;\n }\n\n nextSelectedID = nodes[i];\n nextSelectedLoc = context.entity(nextSelectedID).loc;\n }\n }\n\n context.perform(action, operation.annotation());\n context.validator().validate();\n\n if (nextSelectedID && nextSelectedLoc) {\n if (context.hasEntity(nextSelectedID)) {\n context.enter(modeSelect(context, [nextSelectedID]).follow(true));\n } else {\n context.map().centerEase(nextSelectedLoc);\n context.enter(modeBrowse(context));\n }\n } else {\n context.enter(modeBrowse(context));\n }\n\n };\n\n\n operation.available = function() {\n return true;\n };\n\n\n operation.disabled = function() {\n if (extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n } else if (selectedIDs.some(protectedMember)) {\n return 'part_of_relation';\n } else if (selectedIDs.some(incompleteRelation)) {\n return 'incomplete_relation';\n } else if (selectedIDs.some(hasWikidataTag)) {\n return 'has_wikidata_tag';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n\n function hasWikidataTag(id) {\n var entity = context.entity(id);\n return entity.tags.wikidata && entity.tags.wikidata.trim().length > 0;\n }\n\n function incompleteRelation(id) {\n var entity = context.entity(id);\n return entity.type === 'relation' && !entity.isComplete(context.graph());\n }\n\n function protectedMember(id) {\n var entity = context.entity(id);\n if (entity.type !== 'way') return false;\n\n var parents = context.graph().parentRelations(entity);\n for (var i = 0; i < parents.length; i++) {\n var parent = parents[i];\n var type = parent.tags.type;\n var role = parent.memberById(id).role || 'outer';\n if (type === 'route' || type === 'boundary' || (type === 'multipolygon' && role === 'outer')) {\n return true;\n }\n }\n return false;\n }\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.delete.' + disable + '.' + multi) :\n t.append('operations.delete.description.' + multi);\n };\n\n\n operation.annotation = function() {\n return selectedIDs.length === 1 ?\n t('operations.delete.annotation.' + context.graph().geometry(selectedIDs[0])) :\n t('operations.delete.annotation.feature', { n: selectedIDs.length });\n };\n\n\n operation.id = 'delete';\n operation.keys = [uiCmd('\u2318\u232B'), uiCmd('\u2318\u2326'), uiCmd('\u2326')];\n operation.title = t.append('operations.delete.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import { t, localizer } from '../core/localizer';\nimport { modeDrawLine } from '../modes/draw_line';\nimport { operationDelete } from '../operations/delete';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { osmRoutableHighwayTagValues } from '../osm/tags';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\nexport function validationDisconnectedWay() {\n var type = 'disconnected_way';\n\n function isTaggedAsHighway(entity) {\n return osmRoutableHighwayTagValues[entity.tags.highway];\n }\n\n var validation = function checkDisconnectedWay(entity, graph) {\n\n var routingIslandWays = routingIslandForEntity(entity);\n if (!routingIslandWays) return [];\n\n return [new validationIssue({\n type: type,\n subtype: 'highway',\n severity: 'warning',\n message: function(context) {\n var entity = this.entityIds.length && context.hasEntity(this.entityIds[0]);\n var label = entity && utilDisplayLabel(entity, context.graph());\n return t.append('issues.disconnected_way.routable.message', { count: this.entityIds.length, highway: label });\n },\n reference: showReference,\n entityIds: Array.from(routingIslandWays).map(function(way) { return way.id; }),\n dynamicFixes: makeFixes\n })];\n\n\n function makeFixes(context) {\n\n var fixes = [];\n\n var singleEntity = this.entityIds.length === 1 && context.hasEntity(this.entityIds[0]);\n\n if (singleEntity) {\n\n if (singleEntity.type === 'way' && !singleEntity.isClosed()) {\n\n var textDirection = localizer.textDirection();\n\n var startFix = makeContinueDrawingFixIfAllowed(textDirection, singleEntity.first(), 'start');\n if (startFix) fixes.push(startFix);\n\n var endFix = makeContinueDrawingFixIfAllowed(textDirection, singleEntity.last(), 'end');\n if (endFix) fixes.push(endFix);\n }\n if (!fixes.length) {\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.connect_feature.title')\n }));\n }\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.delete_feature.title'),\n entityIds: [singleEntity.id],\n onClick: function(context) {\n var id = this.issue.entityIds[0];\n var operation = operationDelete(context, [id]);\n if (!operation.disabled()) {\n operation();\n }\n }\n }));\n } else {\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.connect_features.title')\n }));\n }\n\n return fixes;\n }\n\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.disconnected_way.routable.reference'));\n }\n\n function routingIslandForEntity(entity) {\n\n var routingIsland = new Set(); // the interconnected routable features\n var waysToCheck = []; // the queue of remaining routable ways to traverse\n\n function queueParentWays(node) {\n graph.parentWays(node).forEach(function(parentWay) {\n if (!routingIsland.has(parentWay) && // only check each feature once\n isRoutableWay(parentWay, false)) { // only check routable features\n routingIsland.add(parentWay);\n waysToCheck.push(parentWay);\n }\n });\n }\n\n if (entity.type === 'way' && isRoutableWay(entity, true)) {\n\n routingIsland.add(entity);\n waysToCheck.push(entity);\n\n } else if (entity.type === 'node' && isRoutableNode(entity)) {\n\n routingIsland.add(entity);\n queueParentWays(entity);\n\n } else {\n // this feature isn't routable, cannot be a routing island\n return null;\n }\n\n while (waysToCheck.length) {\n var wayToCheck = waysToCheck.pop();\n var childNodes = graph.childNodes(wayToCheck);\n for (var i in childNodes) {\n var vertex = childNodes[i];\n\n if (isConnectedVertex(vertex)) {\n // found a link to the wider network, not a routing island\n return null;\n }\n\n if (isRoutableNode(vertex)) {\n routingIsland.add(vertex);\n }\n\n queueParentWays(vertex);\n }\n }\n\n // no network link found, this is a routing island, return its members\n return routingIsland;\n }\n\n function isConnectedVertex(vertex) {\n // assume ways overlapping unloaded tiles are connected to the wider road network - #5938\n var osm = services.osm;\n if (osm && !osm.isDataLoaded(vertex.loc)) return true;\n\n // entrances are considered connected\n if (vertex.tags.entrance &&\n vertex.tags.entrance !== 'no') return true;\n if (vertex.tags.amenity === 'parking_entrance') return true;\n\n return false;\n }\n\n function isRoutableNode(node) {\n // treat elevators as distinct features in the highway network\n if (node.tags.highway === 'elevator') return true;\n return false;\n }\n\n function isRoutableWay(way, ignoreInnerWays) {\n if (way.tags.golf === 'path' || way.tags.golf === 'cartpath') {\n // skip golf paths #11863\n return false;\n }\n\n if (isTaggedAsHighway(way) || way.tags.route === 'ferry') return true;\n\n return graph.parentRelations(way).some(function(parentRelation) {\n if (parentRelation.tags.type === 'route' &&\n parentRelation.tags.route === 'ferry') return true;\n\n if (parentRelation.isMultipolygon() &&\n isTaggedAsHighway(parentRelation) &&\n (!ignoreInnerWays || parentRelation.memberById(way.id).role !== 'inner')) return true;\n\n return false;\n });\n }\n\n function makeContinueDrawingFixIfAllowed(textDirection, vertexID, whichEnd) {\n var vertex = graph.hasEntity(vertexID);\n if (!vertex || vertex.tags.noexit === 'yes') return null;\n\n var useLeftContinue = (whichEnd === 'start' && textDirection === 'ltr') ||\n (whichEnd === 'end' && textDirection === 'rtl');\n\n return new validationIssueFix({\n icon: 'iD-operation-continue' + (useLeftContinue ? '-left' : ''),\n title: t.append('issues.fix.continue_from_' + whichEnd + '.title'),\n entityIds: [vertexID],\n onClick: function(context) {\n var wayId = this.issue.entityIds[0];\n var way = context.hasEntity(wayId);\n var vertexId = this.entityIds[0];\n var vertex = context.hasEntity(vertexId);\n\n if (!way || !vertex) return;\n\n // make sure the vertex is actually visible and editable\n var map = context.map();\n if (!context.editable() || !map.trimmedExtent().contains(vertex.loc)) {\n map.zoomToEase(vertex);\n }\n\n context.enter(\n modeDrawLine(context, wayId, context.graph(), 'line', way.affix(vertexId), true)\n );\n }\n });\n }\n\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { osmTimelessFeatureTagValues } from '../osm/tags';\n\nexport function validationMissingStartDate(context) {\n const type = 'missing_start_date';\n\n const validation = function checkMissingStartDate(entity, graph) {\n // If start_date is not empty, return nothing\n if (entity.tags && (entity.tags.start_date || entity.tags['start_date:edtf'])) return [];\n // If entity has no tags, return nothing\n if (Object.keys(entity.tags).length === 0) return [];\n // Rule should be ignored for natural entities and waterways\n if (entity.tags && (\n (entity.tags.natural && osmTimelessFeatureTagValues[entity.tags.natural]) ||\n (entity.tags.waterway && osmTimelessFeatureTagValues[entity.tags.waterway]) ||\n (entity.tags.water && osmTimelessFeatureTagValues[entity.tags.water]))) return [];\n\n // If entity is a vertex node\n var osm = context.connection();\n var isUnloadedNode = entity.type === 'node' && osm && !osm.isDataLoaded(entity.loc);\n\n // Should skip this validation if node is unloaded, is a vertex or has parent relations\n if (isUnloadedNode ||\n // allow untagged nodes that are part of ways\n entity.geometry(graph) === 'vertex' ||\n // allow untagged entities that are part of relations\n entity.hasParentRelations(graph)) return [];\n\n const entityID = entity.id;\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.missing_start_date.reference'));\n }\n\n return [new validationIssue({\n type: type,\n severity: 'warning',\n message: (context) => {\n const entity = context.hasEntity(entityID);\n return entity ? t.append('issues.missing_start_date.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [entityID],\n dynamicFixes: () => {\n return [\n new validationIssueFix({ title: t.append('issues.fix.add_start_date.title')})\n ];\n }\n })];\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { localizer, t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilNormalizeDateString, utilEDTFFromOSMDateString } from '../util';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { actionChangeTags } from '../actions/change_tags';\n\nimport * as edtf from 'edtf';\n\nexport function validationFormatting() {\n var type = 'invalid_format';\n\n var validation = function(entity) {\n var issues = [];\n\n function showReferenceDate(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.invalid_format.date.reference'));\n }\n\n function validateDate(key, msgKey) {\n if (!entity.tags[key]) return;\n var normalized = utilNormalizeDateString(entity.tags[key]);\n if (normalized !== null && entity.tags[key] === normalized.value) return;\n issues.push(new validationIssue({\n type: type,\n subtype: 'date',\n severity: 'error',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.date.message_' + msgKey,\n { feature: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: showReferenceDate,\n entityIds: [entity.id],\n hash: key + entity.tags[key],\n dynamicFixes: function() {\n var fixes = [];\n\n let alternatives = [];\n if (normalized !== null) {\n let label = normalized.date.toLocaleDateString(localizer.localeCodes(), normalized.localeOptions);\n alternatives.push({\n date: normalized.value,\n label: label || normalized.value,\n });\n }\n let edtfFromOSM = utilEDTFFromOSMDateString(entity.tags[key]);\n if (edtfFromOSM) {\n let label;\n try {\n label = edtf.default(edtfFromOSM).format(localizer.localeCode());\n } catch {\n label = edtfFromOSM;\n }\n alternatives.push({\n edtf: edtfFromOSM,\n label: label,\n });\n }\n\n fixes.push(...alternatives.map(alt => new validationIssueFix({\n title: t.append('issues.fix.reformat_date.title', { date: alt.label }),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n if (alt.date) {\n newTags[key] = alt.date;\n } else {\n delete newTags[key];\n }\n newTags[key + ':edtf'] = alt.edtf;\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.reformat_date.annotation'));\n }\n })));\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n delete newTags[key];\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.remove_tag.annotation'));\n }\n }));\n\n return fixes;\n }\n }));\n }\n validateDate('start_date', 'start');\n validateDate('end_date', 'end');\n\n function showReferenceEDTF(selection, parserError) {\n let message;\n if (typeof parserError.offset === 'number' && parserError.token) {\n message = t.append('issues.invalid_format.edtf.reference', {\n token: parserError.token.value,\n position: (parserError.offset + 1).toLocaleString(localizer.localeCodes()),\n });\n } else if (parserError.message) {\n message = selection => selection.append('span')\n .attr('class', 'localized-text')\n .attr('lang', 'en')\n .text(parserError.message.replace(/^edtf: /, ''));\n }\n if (!message) {\n return;\n }\n\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(message);\n }\n\n function validateEDTF(key, msgKey) {\n key += ':edtf';\n if (!entity.tags[key]) return;\n let parserError;\n try {\n edtf.parse(entity.tags[key]);\n return;\n } catch (e) {\n parserError = e;\n }\n issues.push(new validationIssue({\n type: type,\n subtype: 'date',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.edtf.message_' + msgKey,\n { feature: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: selection => showReferenceEDTF(selection, parserError),\n entityIds: [entity.id],\n hash: key + entity.tags[key],\n dynamicFixes: function() {\n var fixes = [];\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n delete newTags[key];\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.remove_tag.annotation'));\n }\n }));\n return fixes;\n }\n }));\n }\n validateEDTF('start_date', 'start');\n validateEDTF('end_date', 'end');\n\n function isValidEmail(email) {\n // Emails in OSM are going to be official so they should be pretty simple\n // Using negated lists to better support all possible unicode characters (#6494)\n var valid_email = /^[^\\(\\)\\\\,\":;<>@\\[\\]]+@[^\\(\\)\\\\,\":;<>@\\[\\]\\.]+(?:\\.[a-z0-9-]+)*$/i;\n\n // An empty value is also acceptable\n return (!email || valid_email.test(email));\n }\n\n function showReferenceEmail(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.invalid_format.email.reference'));\n }\n\n function isValidURL(url, strict = false) {\n try {\n // First try strict WHATWG parsing\n const link = new URL(url);\n return link.href.includes(url);\n } catch {\n if (strict) return false;\n // Fallback: accept if it looks like a valid scheme://something, even if semicolons are present\n return /^https?:\\/\\/\\S+$/i.test(url);\n }\n }\n\n function cleanWikimediaCommonsReference(value) {\n if (!value) return null;\n for (const prefix of ['file', 'datei', 'fichier', 'plik']) {\n if (!value.toLowerCase().startsWith(prefix + ':')) continue;\n return 'File' + decodeURIComponent(value.slice(prefix.length));\n }\n if (value.startsWith('Category:')) return decodeURIComponent(value);\n return null;\n }\n\n function showReferenceWebsite(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.invalid_format.website.reference'));\n }\n\n const websiteValidationIssueBase = {\n type: type,\n subtype: 'website',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.website.message' + (this.data?.count > 1 ? '_multi' : ''),\n { feature: utilDisplayLabel(entity, context.graph()), site: this.data?.value }) : '';\n },\n dynamicFixes: function(context) {\n const wikimedia_commons_reference = cleanWikimediaCommonsReference(this.data?.value);\n const fixes = [{ protocol: 'https', icon: 'temaki-lock' }, { protocol: 'http' }]\n .filter(fix => isValidURL(fix.protocol + '://' + this.data?.value, true))\n .map(fix => new validationIssueFix({\n icon: fix.icon,\n title: t.append('issues.fix.add_protocol_'+ fix.protocol +'.title'),\n onClick: function() {\n const entityID = this.issue.entityIds[0];\n const entity = context.entity(entityID);\n if (!entity) return;\n const key = this.issue.data.key;\n const tags = Object.assign({}, entity.tags);\n tags[key] = entity.tags[key]\n .split(';')\n .map(s => s.trim())\n .map(s => isValidURL(s) ? s : fix.protocol + '://' + s)\n .join(';');\n\n context.perform(\n actionChangeTags(entityID, tags),\n t('issues.fix.add_protocol_'+ fix.protocol +'.annotation')\n );\n }\n }));\n if (this.data?.key === 'image' && !entity.tags.wikimedia_commons && wikimedia_commons_reference) {\n fixes.push(new validationIssueFix({\n icon: 'iD-icon-out-link',\n title: t.append('issues.fix.move_value_to_wikimedia_commons.title'),\n onClick: function() {\n const entityID = this.issue.entityIds[0];\n const entity = context.entity(entityID);\n if (!entity) return;\n const key = this.issue.data.key;\n const tags = Object.assign({}, entity.tags);\n tags.wikimedia_commons = wikimedia_commons_reference;\n delete tags[key];\n\n context.perform(\n actionChangeTags(entityID, tags),\n t('issues.fix.move_value_to_wikimedia_commons.annotation')\n );\n }\n }));\n }\n return fixes;\n },\n reference: showReferenceWebsite,\n entityIds: [entity.id]\n };\n\n Object.entries(entity.tags).map(function([key, tag]) {\n if (!/\\b(website|url)\\b|^image$/i.test(key)) return null;\n if (!tag) return null;\n const value = tag.trim();\n if (!value) return null;\n if (!value.includes(';')) {\n // No semicolon, validate whole value\n if (isValidURL(value)) return null;\n return {\n ...websiteValidationIssueBase,\n data: { key, value },\n hash: key + '=' + value\n };\n }\n const invalidParts = value.split(';').map(s => s.trim()).filter(x => !isValidURL(x));\n if (!invalidParts.length) {\n if (isValidURL(value)) return null;\n // All split parts valid, but whole value still invalid\n return {\n ...websiteValidationIssueBase,\n data: { key, value },\n hash: key + '=' + value\n };\n }\n return {\n ...websiteValidationIssueBase,\n data: { key, value: invalidParts.join(', '), count: invalidParts.length },\n hash: key + '=' + invalidParts.join()\n };\n }).filter(issue => issue !== null).forEach(issueData => issues.push(new validationIssue(issueData)));\n\n if (entity.tags.email) {\n // Multiple emails are possible\n var emails = entity.tags.email\n .split(';')\n .map(function(s) { return s.trim(); })\n .filter(function(x) { return !isValidEmail(x); });\n\n if (emails.length) {\n issues.push(new validationIssue({\n type: type,\n subtype: 'email',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.email.message' + this.data,\n { feature: utilDisplayLabel(entity, context.graph()), email: emails.join(', ') }) : '';\n },\n reference: showReferenceEmail,\n entityIds: [entity.id],\n hash: emails.join(),\n data: (emails.length > 1) ? '_multi' : ''\n }));\n }\n }\n\n return issues;\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationHelpRequest(context) {\n var type = 'help_request';\n\n var validation = function checkFixmeTag(entity) {\n\n if (!entity.tags.fixme) return [];\n\n // don't flag fixmes on features added by the user\n if (entity.version === undefined) return [];\n\n if (entity.v !== undefined) {\n var baseEntity = context.history().base().hasEntity(entity.id);\n // don't flag fixmes added by the user on existing features\n if (!baseEntity || !baseEntity.tags.fixme) return [];\n }\n\n return [new validationIssue({\n type: type,\n subtype: 'fixme_tag',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.fixme_tag.message', {\n feature: utilDisplayLabel(entity, context.graph(), true /* verbose */)\n }) : '';\n },\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n title: t.append('issues.fix.address_the_concern.title')\n })\n ];\n },\n reference: showReference,\n entityIds: [entity.id]\n })];\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.fixme_tag.reference'));\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t, localizer } from '../core/localizer';\nimport { modeDrawLine } from '../modes/draw_line';\nimport { actionReverse } from '../actions/reverse';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { osmFlowingWaterwayTagValues, osmRoutableHighwayTagValues } from '../osm/tags';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\nexport function validationImpossibleOneway() {\n const type = 'impossible_oneway';\n\n const validation = function checkImpossibleOneway(entity, graph) {\n if (entity.type !== 'way' || entity.geometry(graph) !== 'line') return [];\n if (entity.isClosed()) return [];\n if (!typeForWay(entity)) return [];\n if (!entity.isOneWay()) return [];\n\n return [\n ...issuesForNode(entity, entity.first()),\n ...issuesForNode(entity, entity.last())\n ];\n\n function typeForWay(way) {\n if (way.geometry(graph) !== 'line') return null;\n\n if (osmRoutableHighwayTagValues[way.tags.highway]) return 'highway';\n if (osmFlowingWaterwayTagValues[way.tags.waterway]) return 'waterway';\n return null;\n }\n\n function nodeOccursMoreThanOnce(way, nodeID) {\n let occurrences = 0;\n for (const index in way.nodes) {\n if (way.nodes[index] === nodeID) {\n occurrences++;\n if (occurrences > 1) return true;\n }\n }\n return false;\n }\n\n function isConnectedViaOtherTypes(way, node) {\n\n var wayType = typeForWay(way);\n\n if (wayType === 'highway') {\n // entrances are considered connected\n if (node.tags.entrance && node.tags.entrance !== 'no') return true;\n if (node.tags.amenity === 'parking_entrance') return true;\n } else if (wayType === 'waterway') {\n if (node.id === way.first()) {\n // multiple waterways may start at the same spring\n if (node.tags.natural === 'spring') return true;\n } else {\n // multiple waterways may end at the same drain\n if (node.tags.manhole === 'drain') return true;\n }\n }\n\n return graph.parentWays(node).some(function(parentWay) {\n if (parentWay.id === way.id) return false;\n\n if (wayType === 'highway') {\n\n // allow connections to highway areas\n if (parentWay.geometry(graph) === 'area' &&\n osmRoutableHighwayTagValues[parentWay.tags.highway]) return true;\n\n // count connections to ferry routes as connected\n if (parentWay.tags.route === 'ferry') return true;\n\n return graph.parentRelations(parentWay).some(function(parentRelation) {\n if (parentRelation.tags.type === 'route' &&\n parentRelation.tags.route === 'ferry') return true;\n\n // allow connections to highway multipolygons\n return parentRelation.isMultipolygon() && osmRoutableHighwayTagValues[parentRelation.tags.highway];\n });\n } else if (wayType === 'waterway') {\n // multiple waterways may start or end at a water body at the same node\n if (parentWay.tags.natural === 'water' ||\n parentWay.tags.natural === 'coastline') return true;\n }\n return false;\n });\n }\n\n function issuesForNode(way, nodeID) {\n const isFirst = (nodeID === way.first()) ^ way.isOneWayBackwards();\n const wayType = typeForWay(way);\n\n // ignore if this way is self-connected at this node\n if (nodeOccursMoreThanOnce(way, nodeID)) return [];\n\n const osm = services.osm;\n if (!osm) return [];\n const node = graph.hasEntity(nodeID);\n // ignore if this node or its tile are unloaded\n if (!node || !osm.isDataLoaded(node.loc)) return [];\n\n if (isConnectedViaOtherTypes(way, node)) return [];\n\n const attachedWaysOfSameType = graph.parentWays(node).filter(parentWay => {\n if (parentWay.id === way.id) return false;\n return typeForWay(parentWay) === wayType;\n });\n\n // assume it's okay for waterways to start or end disconnected for now\n if (wayType === 'waterway' && attachedWaysOfSameType.length === 0) return [];\n\n const attachedOneways = attachedWaysOfSameType\n .filter(attachedWay => attachedWay.isOneWay());\n\n // ignore if the way is connected to some non-oneway features\n if (attachedOneways.length < attachedWaysOfSameType.length) return [];\n\n if (attachedOneways.length) {\n const connectedEndpointsOkay = attachedOneways.some(attachedOneway => {\n const isAttachedBackwards = attachedOneway.isOneWayBackwards();\n if ((isFirst ^ isAttachedBackwards\n ? attachedOneway.first()\n : attachedOneway.last()\n ) !== nodeID) {\n return true;\n }\n if (nodeOccursMoreThanOnce(attachedOneway, nodeID)) return true;\n return false;\n });\n if (connectedEndpointsOkay) return [];\n }\n\n const placement = isFirst ? 'start' : 'end';\n let messageID = wayType + '.';\n let referenceID = wayType + '.';\n\n if (wayType === 'waterway') {\n messageID += 'connected.' + placement;\n referenceID += 'connected';\n } else {\n messageID += placement;\n referenceID += placement;\n }\n\n return [new validationIssue({\n type: type,\n subtype: wayType,\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.impossible_oneway.' + messageID + '.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: getReference(referenceID),\n entityIds: [way.id, node.id],\n dynamicFixes: function() {\n\n var fixes = [];\n\n if (attachedOneways.length) {\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-reverse',\n title: t.append('issues.fix.reverse_feature.title'),\n entityIds: [way.id],\n onClick: function(context) {\n var id = this.issue.entityIds[0];\n context.perform(actionReverse(id), t('operations.reverse.annotation.line', { n: 1 }));\n }\n }));\n }\n if (node.tags.noexit !== 'yes') {\n var textDirection = localizer.textDirection();\n var useLeftContinue = (isFirst && textDirection === 'ltr') ||\n (!isFirst && textDirection === 'rtl');\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-continue' + (useLeftContinue ? '-left' : ''),\n title: t.append('issues.fix.continue_from_' + (isFirst ? 'start' : 'end') + '.title'),\n onClick: function(context) {\n var entityID = this.issue.entityIds[0];\n var vertexID = this.issue.entityIds[1];\n var way = context.entity(entityID);\n var vertex = context.entity(vertexID);\n continueDrawing(way, vertex, context);\n }\n }));\n }\n\n return fixes;\n },\n loc: node.loc\n })];\n\n function getReference(referenceID) {\n return function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.impossible_oneway.' + referenceID + '.reference'));\n };\n }\n }\n };\n\n function continueDrawing(way, vertex, context) {\n // make sure the vertex is actually visible and editable\n var map = context.map();\n if (!context.editable() || !map.trimmedExtent().contains(vertex.loc)) {\n map.zoomToEase(vertex);\n }\n\n context.enter(\n modeDrawLine(context, way.id, context.graph(), 'line', way.affix(vertex.id), true)\n );\n }\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\nconst incompatibleRules = [\n {\n id: 'amap',\n regex: /(^amap$|^amap\\.com|autonavi|mapabc|\u9AD8\u5FB7)/i\n },\n {\n id: 'baidu',\n regex: /(baidu|mapbar|\u767E\u5EA6)/i\n },\n {\n id: 'google',\n regex: /(google)/i,\n exceptRegex: /((books|drive)\\.google|google\\s?(books|drive|plus))|(esri\\/Google_(Africa|Open)_Buildings)|(:\\/\\/\\S+\\/\\S+(google)\\S+)/i\n }\n];\n\n/**\n * @param {string} str String (e.g. tag value) to check for incompatible sources\n * @returns {{id:string, regex: RegExp, exceptRegex?: RegExp}[]}\n */\nexport function getIncompatibleSources(str) {\n return incompatibleRules\n .filter(rule =>\n rule.regex.test(str) &&\n !rule.exceptRegex?.test(str)\n );\n}\n\nexport function validationIncompatibleSource() {\n const type = 'incompatible_source';\n\n const validation = function checkIncompatibleSource(entity) {\n const entitySources = entity.tags && entity.tags.source && entity.tags.source.split(';');\n if (!entitySources) return [];\n\n const entityID = entity.id;\n\n return entitySources\n .flatMap(source => getIncompatibleSources(source)\n .map(matchRule => new validationIssue({\n type: type,\n severity: 'warning',\n message: (context) => {\n const entity = context.hasEntity(entityID);\n return entity ? t.append('issues.incompatible_source.feature.message', {\n feature: utilDisplayLabel(entity, context.graph(), true /* verbose */),\n value: source\n }) : '';\n },\n reference: getReference(matchRule.id),\n entityIds: [entityID],\n hash: source,\n dynamicFixes: () => {\n return [\n new validationIssueFix({ title: t.append('issues.fix.remove_proprietary_data.title') })\n ];\n }\n }))\n );\n\n function getReference(id) {\n return function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append(`issues.incompatible_source.reference.${id}`));\n };\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { services } from '../services';\n\n\nexport function validationMaprules() {\n var type = 'maprules';\n\n var validation = function checkMaprules(entity, graph) {\n if (!services.maprules) return [];\n\n var rules = services.maprules.validationRules();\n var issues = [];\n\n for (var i = 0; i < rules.length; i++) {\n var rule = rules[i];\n rule.findIssues(entity, graph, issues);\n }\n\n return issues;\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { localizer, t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilNormalizeDateString } from '../util/ohm_date';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\nimport * as edtf from 'edtf';\n\nexport function validationMismatchedDates() {\n let type = 'mismatched_dates';\n\n function parseEDTF(value) {\n try {\n let parsed = edtf.default(value);\n\n // According to edtf.js, an extended interval with an unknown start or end covers no date.\n // This isn't useful for the purpose of testing whether the basic date matches, so treat it as an unspecified start or end.\n if (parsed.lower === null) {\n parsed.lower = Infinity;\n }\n if (parsed.upper === null) {\n parsed.upper = Infinity;\n }\n\n return parsed;\n } catch {\n // Already handled by invalid_format rule.\n return;\n }\n }\n\n function getReplacementDates(parsed) {\n let likelyDates = new Set();\n\n let valueFromDate = (date, precision) => {\n date.precision = precision;\n return date.edtf.split('T')[0];\n };\n\n if (Number.isFinite(parsed.min)) {\n let min = edtf.default(parsed.min);\n let precision = (parsed.lower || parsed.first || parsed).precision;\n likelyDates.add(valueFromDate(min, precision));\n }\n\n if (Number.isFinite(parsed.max)) {\n let max = edtf.default(parsed.max);\n let precision = (parsed.upper || parsed.last || parsed).precision;\n likelyDates.add(valueFromDate(max, precision));\n }\n\n let sortedDates = [...likelyDates];\n sortedDates.sort();\n return sortedDates;\n }\n\n let validation = function(entity) {\n let issues = [];\n\n function showReferenceEDTF(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.mismatched_dates.edtf.reference'));\n }\n\n function getDynamicFixes(key, parsed) {\n let fixes = [];\n\n let replacementDates = getReplacementDates(parsed);\n fixes.push(...replacementDates.map(value => {\n let normalized = utilNormalizeDateString(value);\n let localeDateString = normalized.date.toLocaleDateString(localizer.languageCode(), normalized.localeOptions);\n return new validationIssueFix({\n title: t.append('issues.fix.reformat_date.title', { date: localeDateString }),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n newTags[key] = normalized.value;\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.reformat_date.annotation'));\n }\n });\n }));\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n delete newTags[key];\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.remove_tag.annotation'));\n }\n }));\n\n return fixes;\n }\n\n function validateEDTF(key, msgKey) {\n if (!entity.tags[key] || !entity.tags[key + ':edtf']) return;\n let basic = entity.tags[key];\n let basicAsEDTF = parseEDTF(basic);\n let parsed = parseEDTF(entity.tags[key + ':edtf']);\n if (!basicAsEDTF || !parsed || parsed.covers(basicAsEDTF) || basicAsEDTF.covers(parsed)) return;\n\n // start_date and end_date disallow time precision. Transform the basic date into a daylong range in EDTF to allow a comparison to an EDTF date with time precision.\n // https://github.com/OpenHistoricalMap/issues/issues/764\n if (basic.match('^-?[0-9]+-[0-9]{2}-[0-9]{2}$')) {\n let basicTime = parseEDTF(`${basic}T00:00:00/${basic}T24:00:00`);\n if (basicTime && (parsed.covers(basicTime) || basicTime.covers(parsed))) return;\n }\n\n issues.push(new validationIssue({\n type: type,\n subtype: 'date',\n severity: 'warning',\n message: function(context) {\n let entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.mismatched_dates.edtf.message_' + msgKey,\n { feature: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: showReferenceEDTF,\n entityIds: [entity.id],\n hash: key + entity.tags[key + ':edtf'],\n dynamicFixes: () => getDynamicFixes(key, parsed),\n }));\n }\n validateEDTF('start_date', 'start');\n validateEDTF('end_date', 'end');\n\n return issues;\n };\n\n validation.type = type;\n validation.parseEDTF = parseEDTF;\n validation.getReplacementDates = getReplacementDates;\n\n return validation;\n}\n", "import { deepEqual } from 'fast-equals';\n\nimport { actionAddVertex } from '../actions/add_vertex';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionMergeNodes } from '../actions/merge_nodes';\nimport { actionExtract } from '../actions/extract';\nimport { modeSelect } from '../modes/select';\nimport { osmJoinWays } from '../osm/multipolygon';\nimport { osmNodeGeometriesForTags, osmTagSuggestingArea } from '../osm/tags';\nimport { presetManager } from '../presets';\nimport { geoHasSelfIntersections, geoSphericalDistance } from '../geo';\nimport { t } from '../core/localizer';\nimport { utilTagText } from '../util';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationMismatchedGeometry() {\n var type = 'mismatched_geometry';\n\n function tagSuggestingLineIsArea(entity) {\n if (entity.type !== 'way' || entity.isClosed()) return null;\n\n var tagSuggestingArea = entity.tagSuggestingArea();\n\n if (!tagSuggestingArea) {\n return null;\n }\n\n var asLine = presetManager.matchTags(tagSuggestingArea, 'line');\n var asArea = presetManager.matchTags(tagSuggestingArea, 'area');\n if (asLine && asArea && deepEqual(asLine.tags, asArea.tags)) {\n // this tag also allows lines and making this an area wouldn't matter\n return null;\n }\n\n if (asLine.isFallback() && asArea.isFallback() && !deepEqual(tagSuggestingArea, { area: 'yes' })) {\n // if the entity matches the fallback preset, regardless of the\n // geometry, then changing the geometry will not help.\n return null;\n }\n\n return tagSuggestingArea;\n }\n\n\n function makeConnectEndpointsFixOnClick(way, graph) {\n // must have at least three nodes to close this automatically\n if (way.nodes.length < 3) return null;\n\n var nodes = graph.childNodes(way), testNodes;\n var firstToLastDistanceMeters = geoSphericalDistance(nodes[0].loc, nodes[nodes.length-1].loc);\n\n // if the distance is very small, attempt to merge the endpoints\n if (firstToLastDistanceMeters < 0.75) {\n testNodes = nodes.slice(); // shallow copy\n testNodes.pop();\n testNodes.push(testNodes[0]);\n // make sure this will not create a self-intersection\n if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {\n return function(context) {\n var way = context.entity(this.issue.entityIds[0]);\n context.perform(\n actionMergeNodes([way.nodes[0], way.nodes[way.nodes.length-1]], nodes[0].loc),\n t('issues.fix.connect_endpoints.annotation')\n );\n };\n }\n }\n\n // if the points were not merged, attempt to close the way\n testNodes = nodes.slice(); // shallow copy\n testNodes.push(testNodes[0]);\n // make sure this will not create a self-intersection\n if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {\n return function(context) {\n var wayId = this.issue.entityIds[0];\n var way = context.entity(wayId);\n var nodeId = way.nodes[0];\n var index = way.nodes.length;\n context.perform(\n actionAddVertex(wayId, nodeId, index),\n t('issues.fix.connect_endpoints.annotation')\n );\n };\n }\n }\n\n function lineTaggedAsAreaIssue(entity) {\n\n var tagSuggestingArea = tagSuggestingLineIsArea(entity);\n if (!tagSuggestingArea) return null;\n\n var validAsLine = false;\n var presetAsLine = presetManager.matchTags(entity.tags, 'line');\n if (presetAsLine) {\n validAsLine = true;\n var key = Object.keys(tagSuggestingArea)[0];\n if (presetAsLine.tags[key] && presetAsLine.tags[key] === '*') {\n // only matches a fallback preset of the tag which is suggesting to be an area\n validAsLine = false;\n }\n if (Object.keys(presetAsLine.tags).length === 0) {\n // only matches the fallback \"line\" preset\n validAsLine = false;\n }\n }\n\n return new validationIssue({\n type: type,\n subtype: 'area_as_line',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.tag_suggests_area.message', {\n feature: utilDisplayLabel(entity, 'area', true /* verbose */),\n tag: utilTagText({ tags: tagSuggestingArea })\n }) : '';\n },\n reference: showReference,\n entityIds: [entity.id],\n hash: JSON.stringify(tagSuggestingArea),\n dynamicFixes: function(context) {\n\n var fixes = [];\n\n var entity = context.entity(this.entityIds[0]);\n var connectEndsOnClick = makeConnectEndpointsFixOnClick(entity, context.graph());\n\n if (!validAsLine) {\n // only suggest to \"connect the ends\" if the feature is not also valid as a line\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.connect_endpoints.title'),\n onClick: connectEndsOnClick\n }));\n }\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n var entityId = this.issue.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n for (var key in tagSuggestingArea) {\n delete tags[key];\n }\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.remove_tag.annotation')\n );\n }\n }));\n\n return fixes;\n }\n });\n\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.tag_suggests_area.reference'));\n }\n }\n\n function vertexPointIssue(entity, graph) {\n // we only care about nodes\n if (entity.type !== 'node') return null;\n\n // ignore tagless points\n if (Object.keys(entity.tags).length === 0) return null;\n\n // address lines are special so just ignore them\n if (entity.isOnAddressLine(graph)) return null;\n\n var geometry = entity.geometry(graph);\n var allowedGeometries = osmNodeGeometriesForTags(entity.tags);\n\n if (geometry === 'point' && !allowedGeometries.point && allowedGeometries.vertex) {\n\n return new validationIssue({\n type: type,\n subtype: 'vertex_as_point',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.vertex_as_point.message', {\n feature: utilDisplayLabel(entity, 'vertex', true /* verbose */)\n }) : '';\n },\n reference: function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.vertex_as_point.reference'));\n },\n entityIds: [entity.id],\n dynamicFixes: () => [new validationIssueFix({\n icon: 'iD-operation-move',\n title: t.append('issues.fix.reposition_point_to_vertex.title')\n })]\n });\n\n } else if (geometry === 'vertex' && !allowedGeometries.vertex && allowedGeometries.point) {\n\n return new validationIssue({\n type: type,\n subtype: 'point_as_vertex',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.point_as_vertex.message', {\n feature: utilDisplayLabel(entity, 'point', true /* verbose */)\n }) : '';\n },\n reference: function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.point_as_vertex.reference'));\n },\n entityIds: [entity.id],\n dynamicFixes: extractPointDynamicFixes\n });\n }\n\n return null;\n }\n\n\n function otherMismatchIssue(entity, graph) {\n // ignore boring features\n if (!entity.hasInterestingTags()) return null;\n\n if (entity.type !== 'node' && entity.type !== 'way') return null;\n\n // address lines are special so just ignore them\n if (entity.type === 'node' && entity.isOnAddressLine(graph)) return null;\n\n var sourceGeom = entity.geometry(graph);\n\n var targetGeoms = entity.type === 'way' ? ['point', 'vertex'] : ['line', 'area'];\n\n if (sourceGeom === 'area') targetGeoms.unshift('line');\n\n var asSource = presetManager.match(entity, graph);\n\n const originalTargetGeom = targetGeoms.find(nodeGeom => {\n const asTarget = presetManager.matchTags(\n entity.tags,\n nodeGeom,\n entity.extent(graph).center(),\n );\n if (!asSource || !asTarget ||\n asSource === asTarget ||\n // sometimes there are two presets with the same tags for different geometries\n deepEqual(asSource.tags, asTarget.tags)) return false;\n\n if (asTarget.isFallback()) return false;\n\n var primaryKey = Object.keys(asTarget.tags)[0];\n\n // special case: buildings-as-points are discouraged by iD, but common in OSM, so ignore them\n if (primaryKey === 'building') return false;\n\n if (asTarget.tags[primaryKey] === '*') return false;\n\n return asSource.isFallback() || asSource.tags[primaryKey] === '*';\n });\n\n let targetGeom = originalTargetGeom;\n\n if (!targetGeom) return null;\n\n var subtype = targetGeom + '_as_' + sourceGeom;\n\n if (targetGeom === 'vertex') targetGeom = 'point';\n if (sourceGeom === 'vertex') sourceGeom = 'point';\n\n var referenceId = targetGeom + '_as_' + sourceGeom;\n\n var dynamicFixes;\n if (targetGeom === 'point') {\n dynamicFixes = extractPointDynamicFixes;\n\n } else if (sourceGeom === 'area' && targetGeom === 'line') {\n dynamicFixes = lineToAreaDynamicFixes;\n }\n\n return new validationIssue({\n type: type,\n subtype: subtype,\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.' + referenceId + '.message', {\n feature: utilDisplayLabel(entity, originalTargetGeom, true /* verbose */)\n }) : '';\n },\n reference: function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.mismatched_geometry.reference'));\n },\n entityIds: [entity.id],\n dynamicFixes: dynamicFixes\n });\n }\n\n function lineToAreaDynamicFixes(context) {\n\n var convertOnClick;\n\n var entityId = this.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n delete tags.area;\n if (!osmTagSuggestingArea(tags)) {\n // if removing the area tag would make this a line, offer that as a quick fix\n convertOnClick = function(context) {\n var entityId = this.issue.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n if (tags.area) {\n delete tags.area;\n }\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.convert_to_line.annotation')\n );\n };\n }\n\n return [\n new validationIssueFix({\n icon: 'iD-icon-line',\n title: t.append('issues.fix.convert_to_line.title'),\n onClick: convertOnClick\n })\n ];\n }\n\n function extractPointDynamicFixes(context) {\n\n var entityId = this.entityIds[0];\n\n var extractOnClick = null;\n if (!context.hasHiddenConnections(entityId)) {\n\n extractOnClick = function(context) {\n var entityId = this.issue.entityIds[0];\n var action = actionExtract(entityId, context.projection);\n context.perform(\n action,\n t('operations.extract.annotation', { n: 1 })\n );\n // re-enter mode to trigger updates\n context.enter(modeSelect(context, [action.getExtractedNodeID()]));\n };\n }\n\n return [\n new validationIssueFix({\n icon: 'iD-operation-extract',\n title: t.append('issues.fix.extract_point.title'),\n onClick: extractOnClick\n })\n ];\n }\n\n function unclosedMultipolygonPartIssues(entity, graph) {\n\n if (entity.type !== 'relation' ||\n !entity.isMultipolygon() ||\n entity.isDegenerate() ||\n // cannot determine issues for incompletely-downloaded relations\n !entity.isComplete(graph)) return [];\n\n var sequences = osmJoinWays(entity.members, graph);\n\n var issues = [];\n\n for (var i in sequences) {\n var sequence = sequences[i];\n\n if (!sequence.nodes) continue;\n\n var firstNode = sequence.nodes[0];\n var lastNode = sequence.nodes[sequence.nodes.length - 1];\n\n // part is closed if the first and last nodes are the same\n if (firstNode === lastNode) continue;\n\n var issue = new validationIssue({\n type: type,\n subtype: 'unclosed_multipolygon_part',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.unclosed_multipolygon_part.message', {\n feature: utilDisplayLabel(entity, context.graph(), true /* verbose */)\n }) : '';\n },\n reference: showReference,\n loc: sequence.nodes[0].loc,\n entityIds: [entity.id],\n hash: sequence.map(function(way) {\n return way.id;\n }).join()\n });\n issues.push(issue);\n }\n\n return issues;\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.unclosed_multipolygon_part.reference'));\n }\n }\n\n var validation = function checkMismatchedGeometry(entity, graph) {\n var vertexPoint = vertexPointIssue(entity, graph);\n if (vertexPoint) return [vertexPoint];\n\n var lineAsArea = lineTaggedAsAreaIssue(entity);\n if (lineAsArea) return [lineAsArea];\n\n var mismatch = otherMismatchIssue(entity, graph);\n if (mismatch) return [mismatch];\n\n return unclosedMultipolygonPartIssues(entity, graph);\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeMember } from '../actions/change_member';\nimport { actionDeleteMember } from '../actions/delete_member';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationMissingRole() {\n var type = 'missing_role';\n\n var validation = function checkMissingRole(entity, graph) {\n var issues = [];\n if (entity.type === 'way') {\n graph.parentRelations(entity).forEach(function(relation) {\n if (!relation.isMultipolygon()) return;\n\n var member = relation.memberById(entity.id);\n if (member && isMissingRole(member)) {\n issues.push(makeIssue(entity, relation, member));\n }\n });\n } else if (entity.type === 'relation' && entity.isMultipolygon()) {\n entity.indexedMembers().forEach(function(member) {\n var way = graph.hasEntity(member.id);\n if (way && isMissingRole(member)) {\n issues.push(makeIssue(way, entity, member));\n }\n });\n }\n\n return issues;\n };\n\n\n function isMissingRole(member) {\n return !member.role || !member.role.trim().length;\n }\n\n\n function makeIssue(way, relation, member) {\n return new validationIssue({\n type: type,\n severity: 'warning',\n message: function(context) {\n const member = context.hasEntity(this.entityIds[0]),\n relation = context.hasEntity(this.entityIds[1]);\n return (member && relation) ? t.append('issues.missing_role.message', {\n member: utilDisplayLabel(member, context.graph()),\n relation: utilDisplayLabel(relation, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [way.id, relation.id],\n extent: function(graph) {\n return graph.entity(this.entityIds[0]).extent(graph);\n },\n data: {\n member: member\n },\n hash: member.index.toString(),\n dynamicFixes: function() {\n return [\n makeAddRoleFix('inner'),\n makeAddRoleFix('outer'),\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_from_relation.title'),\n onClick: function(context) {\n context.perform(\n actionDeleteMember(this.issue.entityIds[1], this.issue.data.member.index),\n t('operations.delete_member.annotation', {\n n: 1\n })\n );\n }\n })\n ];\n }\n });\n\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.missing_role.multipolygon.reference'));\n }\n }\n\n\n function makeAddRoleFix(role) {\n return new validationIssueFix({\n title: t.append('issues.fix.set_as_' + role + '.title'),\n onClick: function(context) {\n var oldMember = this.issue.data.member;\n var member = { id: this.issue.entityIds[0], type: oldMember.type, role: role };\n context.perform(\n actionChangeMember(this.issue.entityIds[1], member, oldMember.index),\n t('operations.change_role.annotation', {\n n: 1\n })\n );\n }\n });\n }\n\n validation.type = type;\n\n return validation;\n}\n", "import { operationDelete } from '../operations/delete';\nimport { osmIsInterestingTag } from '../osm/tags';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationMissingTag(context) {\n var type = 'missing_tag';\n\n function hasDescriptiveTags(entity) {\n var onlyAttributeKeys = ['description', 'name', 'start_date', 'oneway'];\n var entityDescriptiveKeys = Object.keys(entity.tags)\n .filter(function(k) {\n if (k === 'area' || !osmIsInterestingTag(k)) return false;\n\n return !onlyAttributeKeys.some(function(attributeKey) {\n return k === attributeKey || k.indexOf(attributeKey + ':') === 0;\n });\n });\n\n if (entity.type === 'relation' &&\n entityDescriptiveKeys.length === 1 &&\n entity.tags.type === 'multipolygon') {\n // this relation's only interesting tag just says its a multipolygon,\n // which is not descriptive enough\n return false;\n }\n\n return entityDescriptiveKeys.length > 0;\n }\n\n function isUnknownRoad(entity) {\n return entity.type === 'way' && entity.tags.highway === 'road';\n }\n\n function isUntypedRelation(entity) {\n return entity.type === 'relation' && !entity.tags.type;\n }\n\n var validation = function checkMissingTag(entity, graph) {\n\n var subtype;\n\n var osm = context.connection();\n var isUnloadedNode = entity.type === 'node' && osm && !osm.isDataLoaded(entity.loc);\n\n // we can't know if the node is a vertex if the tile is undownloaded\n if (!isUnloadedNode &&\n // allow untagged nodes that are part of ways\n entity.geometry(graph) !== 'vertex' &&\n // allow untagged entities that are part of relations\n !entity.hasParentRelations(graph)) {\n\n if (Object.keys(entity.tags).length === 0) {\n subtype = 'any';\n } else if (!hasDescriptiveTags(entity)) {\n subtype = 'descriptive';\n } else if (isUntypedRelation(entity)) {\n subtype = 'relation_type';\n }\n }\n\n // flag an unknown road even if it's a member of a relation\n if (!subtype && isUnknownRoad(entity)) {\n subtype = 'highway_classification';\n }\n\n if (!subtype) return [];\n\n var messageID = subtype === 'highway_classification' ? 'unknown_road' : 'missing_tag.' + subtype;\n var referenceID = subtype === 'highway_classification' ? 'unknown_road' : 'missing_tag';\n\n // can always delete if the user created it in the first place..\n var canDelete = (entity.version === undefined || entity.v !== undefined);\n var severity = (canDelete && subtype !== 'highway_classification') ? 'error' : 'warning';\n\n return [new validationIssue({\n type: type,\n subtype: subtype,\n severity: severity,\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.' + messageID + '.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [entity.id],\n dynamicFixes: function(context) {\n\n var fixes = [];\n\n var selectFixType = subtype === 'highway_classification' ? 'select_road_type' : 'select_preset';\n\n fixes.push(new validationIssueFix({\n icon: 'iD-icon-search',\n title: t.append('issues.fix.' + selectFixType + '.title'),\n onClick: function(context) {\n context.ui().sidebar.showPresetList();\n }\n }));\n\n var deleteOnClick;\n\n var id = this.entityIds[0];\n var operation = operationDelete(context, [id]);\n var disabledReasonID = operation.disabled();\n if (!disabledReasonID) {\n deleteOnClick = function(context) {\n var id = this.issue.entityIds[0];\n var operation = operationDelete(context, [id]);\n if (!operation.disabled()) {\n operation();\n }\n };\n }\n\n fixes.push(\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.delete_feature.title'),\n disabledReason: disabledReasonID ? t('operations.delete.' + disabledReasonID + '.single') : undefined,\n onClick: deleteOnClick\n })\n );\n\n return fixes;\n }\n })];\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.' + referenceID + '.reference'));\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { osmMutuallyExclusiveTagPairs } from '../osm/tags';\n\nexport function validationMutuallyExclusiveTags(/* context */) {\n const type = 'mutually_exclusive_tags';\n\n // https://wiki.openstreetmap.org/wiki/Special:WhatLinksHere/Property:P44\n const tagKeyPairs = osmMutuallyExclusiveTagPairs;\n\n const validation = function checkMutuallyExclusiveTags(entity /*, graph */) {\n\n let pairsFounds = tagKeyPairs.filter((pair) => {\n return (pair[0] in entity.tags && pair[1] in entity.tags);\n }).filter((pair) => {\n // noname=no is double-negation, thus positive and not conflicting. We'll ignore those\n return !((pair[0].match(/^(addr:)?no[a-z]/) && entity.tags[pair[0]] === 'no') ||\n (pair[1].match(/^(addr:)?no[a-z]/) && entity.tags[pair[1]] === 'no'));\n });\n\n // Additional:\n // Check if name and not:name (and similar) are set and both have the same value\n // not:name can actually have multiple values, separate by ;\n // https://taginfo.openstreetmap.org/search?q=not%3A#keys\n Object.keys(entity.tags).forEach((key) => {\n let negative_key = 'not:' + key;\n if (negative_key in entity.tags && entity.tags[negative_key].split(';').includes(entity.tags[key])) {\n pairsFounds.push([negative_key, key, 'same_value']);\n }\n // For name:xx we also compare against the not:name tag\n if (key.match(/^name:[a-z]+/)) {\n negative_key = 'not:name';\n if (negative_key in entity.tags && entity.tags[negative_key].split(';').includes(entity.tags[key])) {\n pairsFounds.push([negative_key, key, 'same_value']);\n }\n }\n });\n\n let issues = pairsFounds.map((pair) => {\n const subtype = pair[2] || 'default';\n return new validationIssue({\n type: type,\n subtype: subtype,\n severity: 'warning',\n message: function(context) {\n let entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append(`issues.${type}.${subtype}.message`, {\n feature: utilDisplayLabel(entity, context.graph()),\n tag1: pair[0],\n tag2: pair[1]\n }) : '';\n },\n reference: (selection) => showReference(selection, pair, subtype),\n entityIds: [entity.id],\n dynamicFixes: () => pair.slice(0,2).map((tagToRemove) => createIssueFix(tagToRemove))\n });\n });\n\n function createIssueFix(tagToRemove) {\n return new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_named_tag.title', { tag: tagToRemove }),\n onClick: function(context) {\n const entityId = this.issue.entityIds[0];\n const entity = context.entity(entityId);\n let tags = Object.assign({}, entity.tags); // shallow copy\n delete tags[tagToRemove];\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.remove_named_tag.annotation', { tag: tagToRemove })\n );\n }\n });\n }\n\n function showReference(selection, pair, subtype) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append(`issues.${type}.${subtype}.reference`, { tag1: pair[0], tag2: pair[1] }));\n }\n\n return issues;\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { actionSplit } from '../actions/split';\nimport { behaviorOperation } from '../behavior/operation';\nimport { modeSelect } from '../modes/select';\n\n\nexport function operationSplit(context, selectedIDs) {\n var _vertexIds = selectedIDs.filter(function(id) {\n return context.graph().geometry(id) === 'vertex';\n });\n var _selectedWayIds = selectedIDs.filter(function(id) {\n var entity = context.graph().hasEntity(id);\n return entity && entity.type === 'way';\n });\n var _isAvailable = _vertexIds.length > 0 &&\n _vertexIds.length + _selectedWayIds.length === selectedIDs.length;\n var _action = actionSplit(_vertexIds);\n var _ways = [];\n var _geometry = 'feature';\n var _waysAmount = 'single';\n var _nodesAmount = _vertexIds.length === 1 ? 'single' : 'multiple';\n\n if (_isAvailable) {\n if (_selectedWayIds.length) _action.limitWays(_selectedWayIds);\n _ways = _action.ways(context.graph());\n var geometries = {};\n _ways.forEach(function(way) {\n geometries[way.geometry(context.graph())] = true;\n });\n if (Object.keys(geometries).length === 1) {\n _geometry = Object.keys(geometries)[0];\n }\n _waysAmount = _ways.length === 1 ? 'single' : 'multiple';\n }\n\n\n var operation = function() {\n var difference = context.perform(_action, operation.annotation());\n // select both the nodes and the ways so the mapper can immediately disconnect them if desired\n var idsToSelect = _vertexIds.concat(difference.extantIDs().filter(function(id) {\n // filter out relations that may have had member additions\n return context.entity(id).type === 'way';\n }));\n context.enter(modeSelect(context, idsToSelect));\n };\n\n\n operation.relatedEntityIds = function() {\n return _selectedWayIds.length ? [] : _ways.map(way => way.id);\n };\n\n\n operation.available = function() {\n return _isAvailable;\n };\n\n\n operation.disabled = function() {\n var reason = _action.disabled(context.graph());\n if (reason) {\n return reason;\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n }\n return false;\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.split.' + disable) :\n t.append('operations.split.description.' + _geometry + '.' + _waysAmount + '.' + _nodesAmount + '_node');\n };\n\n\n operation.annotation = function() {\n return t('operations.split.annotation.' + _geometry, { n: _ways.length });\n };\n\n\n operation.icon = function() {\n if (_waysAmount === 'multiple') {\n return '#iD-operation-split-multiple';\n } else {\n return '#iD-operation-split';\n }\n };\n\n\n operation.id = 'split';\n operation.keys = [t('operations.split.key')];\n operation.title = t.append('operations.split.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import { t } from '../core/localizer';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { operationSplit } from '../operations/split';\n\nexport function validationOsmApiLimits(context) {\n const type = 'osm_api_limits';\n\n const validation = function checkOsmApiLimits(entity) {\n const issues = [];\n const osm = context.connection();\n if (!osm) return issues; // cannot check if there is no connection to the osm api, e.g. during unit tests\n const maxWayNodes = osm.maxWayNodes();\n\n if (entity.type === 'way') {\n if (entity.nodes.length > maxWayNodes) {\n issues.push(new validationIssue({\n type: type,\n subtype: 'exceededMaxWayNodes',\n severity: 'error',\n message: function() {\n return t.append('issues.osm_api_limits.max_way_nodes.message');\n },\n reference: function(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.osm_api_limits.max_way_nodes.reference', { maxWayNodes }));\n },\n entityIds: [entity.id],\n dynamicFixes: splitWayIntoSmallChunks\n }));\n }\n }\n\n return issues;\n };\n\n function splitWayIntoSmallChunks() {\n const fix = new validationIssueFix({\n icon: 'iD-operation-split',\n title: t.append('issues.fix.split_way.title'),\n entityIds: this.entityIds,\n onClick: function(context) {\n const maxWayNodes = context.connection().maxWayNodes();\n const g = context.graph();\n\n const entityId = this.entityIds[0];\n const entity = context.graph().entities[entityId];\n const numberOfParts = Math.ceil(entity.nodes.length / maxWayNodes);\n let splitVertices;\n\n if (numberOfParts === 2) {\n // simple case: try to split at the an intersection vertex\n const splitIntersections = entity.nodes\n .map(nid => g.entity(nid))\n .filter(n => g.parentWays(n).length > 1)\n .map(n => n.id)\n .filter(nid => {\n const splitIndex = entity.nodes.indexOf(nid);\n return splitIndex < maxWayNodes &&\n entity.nodes.length - splitIndex < maxWayNodes;\n });\n if (splitIntersections.length > 0) {\n splitVertices = [\n splitIntersections[Math.floor(splitIntersections.length / 2)]\n ];\n }\n }\n\n if (splitVertices === undefined) {\n // general case: either more than one split is needed or no possible\n // intersection split point was found -> just split at regular intervals\n splitVertices = [...Array(numberOfParts - 1)].map((_, i) =>\n entity.nodes[Math.floor(entity.nodes.length * (i + 1) / numberOfParts)]);\n }\n\n if (entity.isClosed()) {\n // add extra split for closed ways at start of way\n splitVertices.push(entity.nodes[0]);\n }\n\n const operation = operationSplit(context, splitVertices.concat(entityId));\n if (!operation.disabled()) {\n operation();\n }\n }\n });\n\n return [fix];\n }\n\n\n validation.type = type;\n\n return validation;\n}\n", "/** @typedef {{ old: Tags; replace?: Tags }[]} DataDeprecated */\n\n/** @param {Tags} tags @param {DataDeprecated} dataDeprecated */\nexport function getDeprecatedTags(tags, dataDeprecated) {\n // if there are no tags, none can be deprecated\n if (Object.keys(tags).length === 0) return [];\n\n /** @type {DataDeprecated} */\n var deprecated = [];\n dataDeprecated.forEach((d) => {\n const oldKeys = Object.keys(d.old);\n const transferKeys = oldKeys.filter(key => d.old[key] === '*');\n if (d.replace) {\n var hasExistingValues = Object.keys(d.replace).some((replaceKey) => {\n if (!tags[replaceKey] || d.old[replaceKey]) return false;\n var replaceValue = d.replace[replaceKey];\n if (replaceValue === '*') return false;\n if (replaceValue.startsWith('$1') && tags[replaceKey] === tags[transferKeys[+replaceValue.substring(1) - 1]]) return false;\n if (replaceValue === tags[replaceKey]) return false;\n return true;\n });\n // don't flag deprecated tags if the upgrade path would overwrite existing data - #7843\n if (hasExistingValues) return;\n }\n\n var matchesDeprecatedTags = oldKeys.every((oldKey) => {\n if (!tags[oldKey]) return false;\n if (d.old[oldKey] === '*') return true;\n if (d.old[oldKey] === tags[oldKey]) return true;\n\n var vals = tags[oldKey].split(';').filter(Boolean);\n if (vals.length === 0) {\n return false;\n } else if (vals.length > 1) {\n return vals.indexOf(d.old[oldKey]) !== -1;\n } else {\n if (tags[oldKey] === d.old[oldKey]) {\n if (d.replace && d.old[oldKey] === d.replace[oldKey]) {\n var replaceKeys = Object.keys(d.replace);\n return !replaceKeys.every((replaceKey) => {\n return tags[replaceKey] === d.replace[replaceKey];\n });\n } else {\n return true;\n }\n }\n }\n\n return false;\n });\n\n if (matchesDeprecatedTags) {\n deprecated.push(d);\n }\n });\n\n return deprecated;\n}\n\n/** @type {{ [key: string]: string[] }} */\nvar _deprecatedTagValuesByKey;\n\n/** @param {DataDeprecated} dataDeprecated */\nexport function deprecatedTagValuesByKey(dataDeprecated) {\n if (!_deprecatedTagValuesByKey) {\n _deprecatedTagValuesByKey = {};\n dataDeprecated.forEach((d) => {\n var oldKeys = Object.keys(d.old);\n if (oldKeys.length === 1) {\n var oldKey = oldKeys[0];\n var oldValue = d.old[oldKey];\n if (oldValue !== '*') {\n if (!_deprecatedTagValuesByKey[oldKey]) {\n _deprecatedTagValuesByKey[oldKey] = [oldValue];\n } else {\n _deprecatedTagValuesByKey[oldKey].push(oldValue);\n }\n }\n }\n });\n }\n return _deprecatedTagValuesByKey;\n};\n", "import { t } from '../core/localizer';\n\nimport { actionChangePreset } from '../actions/change_preset';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionUpgradeTags } from '../actions/upgrade_tags';\nimport { fileFetcher } from '../core';\nimport { presetManager } from '../presets';\nimport { services } from '../services';\nimport { utilHashcode, utilTagDiff } from '../util';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { getDeprecatedTags } from '../osm/deprecated';\n\n/** @import { TagDiff } from '../util/util'. */\n\n\nexport function validationOutdatedTags() {\n const type = 'outdated_tags';\n let _waitingForDeprecated = true;\n let _dataDeprecated;\n\n // fetch deprecated tags\n fileFetcher.get('deprecated')\n .then(d => _dataDeprecated = d)\n .catch(() => { /* ignore */ })\n .finally(() => _waitingForDeprecated = false);\n\n\n function oldTagIssues(entity, graph) {\n if (!entity.hasInterestingTags()) return [];\n\n let preset = presetManager.match(entity, graph);\n if (!preset) return [];\n\n const oldTags = Object.assign({}, entity.tags); // shallow copy\n\n // Upgrade preset, if a replacement is available..\n if (preset.replacement) {\n const newPreset = presetManager.item(preset.replacement);\n graph = actionChangePreset(entity.id, preset, newPreset, true /* skip field defaults */)(graph);\n entity = graph.entity(entity.id);\n preset = newPreset;\n }\n\n // Attempt to match a canonical record in the name-suggestion-index.\n const nsi = services.nsi;\n let waitingForNsi = false;\n let nsiResult;\n if (nsi) {\n waitingForNsi = (nsi.status() === 'loading');\n if (!waitingForNsi) {\n const loc = entity.extent(graph).center();\n nsiResult = nsi.upgradeTags(oldTags, loc);\n }\n }\n const nsiDiff = nsiResult ? utilTagDiff(oldTags, nsiResult.newTags) : [];\n\n // Upgrade deprecated tags\n let deprecatedTags;\n if (_dataDeprecated) {\n deprecatedTags = getDeprecatedTags(entity.tags, _dataDeprecated);\n if (entity.type === 'way' && entity.isClosed() &&\n entity.tags.traffic_calming === 'island' && !entity.tags.highway) {\n // https://github.com/openstreetmap/id-tagging-schema/issues/1162#issuecomment-2000356902\n deprecatedTags.push({\n old: {traffic_calming: 'island'},\n replace: {'area:highway': 'traffic_island'}\n });\n }\n if (deprecatedTags.length) {\n deprecatedTags.forEach(tag => {\n graph = actionUpgradeTags(entity.id, tag.old, tag.replace)(graph);\n });\n entity = graph.entity(entity.id);\n }\n }\n\n // Add missing addTags from the detected preset\n let newTags = Object.assign({}, entity.tags); // shallow copy\n if (preset.tags !== preset.addTags) {\n Object.keys(preset.addTags).filter(k => {\n // if nsi suggestion already includes this tag: don't repeat it in \"incomplete tags\"\n return !nsiResult?.newTags[k];\n }).forEach(k => {\n if (!newTags[k]) {\n if (preset.addTags[k] === '*') {\n newTags[k] = 'yes';\n } else if (preset.addTags[k]) {\n newTags[k] = preset.addTags[k];\n }\n }\n });\n }\n const deprecationDiff = utilTagDiff(oldTags, newTags);\n const deprecationDiffContext = Object.keys(oldTags)\n .filter(key => deprecatedTags?.some(deprecated => deprecated.replace?.[key] !== undefined))\n .filter(key => newTags[key] === oldTags[key])\n .map(key => ({\n type: '~',\n key,\n oldVal: oldTags[key],\n newVal: newTags[key],\n display: '  ' + key + '=' + oldTags[key]\n }));\n\n let issues = [];\n issues.provisional = (_waitingForDeprecated || waitingForNsi);\n\n if (deprecationDiff.length) {\n const isOnlyAddingTags = !deprecationDiff.some(d => d.type === '-');\n const prefix = isOnlyAddingTags ? 'incomplete.' : '';\n\n issues.push(new validationIssue({\n type: type,\n subtype: isOnlyAddingTags ? 'incomplete_tags' : 'deprecated_tags',\n severity: 'warning',\n message: (context) => {\n const currEntity = context.hasEntity(entity.id);\n if (!currEntity) return '';\n\n const feature = utilDisplayLabel(currEntity, context.graph(), /* verbose */ true);\n\n return t.append(`issues.outdated_tags.${prefix}message`, { feature });\n },\n reference: selection => showReference(\n selection,\n t.append(`issues.outdated_tags.${prefix}reference`),\n [...deprecationDiff, ...deprecationDiffContext]\n ),\n entityIds: [entity.id],\n hash: utilHashcode(JSON.stringify(deprecationDiff)),\n dynamicFixes: () => {\n let fixes = [\n new validationIssueFix({\n title: t.append('issues.fix.upgrade_tags.title'),\n onClick: (context) => {\n context.perform(graph => doUpgrade(graph, deprecationDiff), t('issues.fix.upgrade_tags.annotation'));\n }\n })\n ];\n return fixes;\n }\n }));\n }\n\n if (nsiDiff.length) {\n const isOnlyAddingTags = nsiDiff.every(d => d.type === '+');\n\n issues.push(new validationIssue({\n type: type,\n subtype: 'noncanonical_brand',\n severity: 'warning',\n message: (context) => {\n const currEntity = context.hasEntity(entity.id);\n if (!currEntity) return '';\n\n const feature = utilDisplayLabel(currEntity, context.graph(), /* verbose */ true);\n\n return isOnlyAddingTags\n ? t.append('issues.outdated_tags.noncanonical_brand.message_incomplete', { feature })\n : t.append('issues.outdated_tags.noncanonical_brand.message', { feature });\n },\n reference: selection => showReference(\n selection,\n t.append('issues.outdated_tags.noncanonical_brand.reference'),\n nsiDiff\n ),\n entityIds: [entity.id],\n hash: utilHashcode(JSON.stringify(nsiDiff)),\n dynamicFixes: () => {\n let fixes = [\n new validationIssueFix({\n title: t.append('issues.fix.upgrade_tags.title'),\n onClick: (context) => {\n context.perform(graph => doUpgrade(graph, nsiDiff), t('issues.fix.upgrade_tags.annotation'));\n }\n }),\n new validationIssueFix({\n title: t.append('issues.fix.tag_as_not.title', { name: nsiResult.matched.displayName }),\n onClick: (context) => {\n context.perform(addNotTag, t('issues.fix.tag_as_not.annotation'));\n }\n })\n ];\n return fixes;\n }\n }));\n }\n\n return issues;\n\n\n /** @param {iD.Graph} graph @param {TagDiff[]} diff */\n function doUpgrade(graph, diff) {\n const currEntity = graph.hasEntity(entity.id);\n if (!currEntity) return graph;\n\n let newTags = Object.assign({}, currEntity.tags); // shallow copy\n diff.forEach(diff => {\n if (diff.type === '-') {\n delete newTags[diff.key];\n } else if (diff.type === '+') {\n newTags[diff.key] = diff.newVal;\n }\n });\n\n return actionChangeTags(currEntity.id, newTags)(graph);\n }\n\n\n function addNotTag(graph) {\n const currEntity = graph.hasEntity(entity.id);\n if (!currEntity) return graph;\n\n const item = nsiResult && nsiResult.matched;\n if (!item) return graph;\n\n let newTags = Object.assign({}, currEntity.tags); // shallow copy\n const wd = item.mainTag; // e.g. `brand:wikidata`\n const notwd = `not:${wd}`; // e.g. `not:brand:wikidata`\n const qid = item.tags[wd];\n newTags[notwd] = qid;\n\n if (newTags[wd] === qid) { // if `brand:wikidata` was set to that qid\n const wp = item.mainTag.replace('wikidata', 'wikipedia');\n delete newTags[wd]; // remove `brand:wikidata`\n delete newTags[wp]; // remove `brand:wikipedia`\n }\n\n return actionChangeTags(currEntity.id, newTags)(graph);\n }\n\n\n function showReference(selection, reference, tagDiff) {\n let enter = selection.selectAll('.issue-reference')\n .data([0])\n .enter();\n\n enter\n .append('div')\n .attr('class', 'issue-reference')\n .call(reference);\n\n enter\n .append('strong')\n .call(t.append('issues.suggested'));\n\n enter\n .append('table')\n .attr('class', 'tagDiff-table')\n .selectAll('.tagDiff-row')\n .data(tagDiff)\n .enter()\n .append('tr')\n .attr('class', 'tagDiff-row')\n .append('td')\n .attr('class', d => {\n const klass = 'tagDiff-cell';\n switch (d.type) {\n case '+':\n return `${klass} tagDiff-cell-add`;\n case '-':\n return `${klass} tagDiff-cell-remove`;\n default:\n return `${klass} tagDiff-cell-unchanged`;\n }\n })\n .html(d => d.display);\n }\n }\n\n\n let validation = oldTagIssues;\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { t } from '../core/localizer';\nimport { utilTagDiff } from '../util';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationPrivateData() {\n var type = 'private_data';\n\n // assume that some buildings are private\n var privateBuildingValues = {\n detached: true,\n farm: true,\n house: true,\n houseboat: true,\n residential: true,\n semidetached_house: true,\n static_caravan: true\n };\n\n // but they might be public if they have one of these other tags\n var publicKeys = {\n amenity: true,\n craft: true,\n historic: true,\n leisure: true,\n office: true,\n shop: true,\n tourism: true\n };\n\n // these tags may contain personally identifying info\n var personalTags = {\n 'contact:email': true,\n 'contact:fax': true,\n 'contact:phone': true,\n email: true,\n fax: true,\n phone: true\n };\n\n\n var validation = function checkPrivateData(entity) {\n var tags = entity.tags;\n if (!tags.building || !privateBuildingValues[tags.building]) return [];\n\n var keepTags = {};\n for (var k in tags) {\n if (publicKeys[k]) return []; // probably a public feature\n if (!personalTags[k]) {\n keepTags[k] = tags[k];\n }\n }\n\n var tagDiff = utilTagDiff(tags, keepTags);\n if (!tagDiff.length) return [];\n\n var fixID = tagDiff.length === 1 ? 'remove_tag' : 'remove_tags';\n\n return [new validationIssue({\n type: type,\n severity: 'warning',\n message: showMessage,\n reference: showReference,\n entityIds: [entity.id],\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.' + fixID + '.title'),\n onClick: function(context) {\n context.perform(doUpgrade, t('issues.fix.remove_tag.annotation'));\n }\n })\n ];\n }\n })];\n\n\n function doUpgrade(graph) {\n var currEntity = graph.hasEntity(entity.id);\n if (!currEntity) return graph;\n\n var newTags = Object.assign({}, currEntity.tags); // shallow copy\n tagDiff.forEach(function(diff) {\n if (diff.type === '-') {\n delete newTags[diff.key];\n } else if (diff.type === '+') {\n newTags[diff.key] = diff.newVal;\n }\n });\n\n return actionChangeTags(currEntity.id, newTags)(graph);\n }\n\n\n function showMessage(context) {\n var currEntity = context.hasEntity(this.entityIds[0]);\n if (!currEntity) return '';\n\n return t.append('issues.private_data.contact.message',\n { feature: utilDisplayLabel(currEntity, context.graph()) }\n );\n }\n\n\n function showReference(selection) {\n var enter = selection.selectAll('.issue-reference')\n .data([0])\n .enter();\n\n enter\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.private_data.reference'));\n\n enter\n .append('strong')\n .call(t.append('issues.suggested'));\n\n enter\n .append('table')\n .attr('class', 'tagDiff-table')\n .selectAll('.tagDiff-row')\n .data(tagDiff)\n .enter()\n .append('tr')\n .attr('class', 'tagDiff-row')\n .append('td')\n .attr('class', function(d) {\n var klass = d.type === '+' ? 'add' : 'remove';\n return 'tagDiff-cell tagDiff-cell-' + klass;\n })\n .html(function(d) { return d.display; });\n }\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { presetManager } from '../presets';\nimport { services } from '../services';\nimport { t, localizer } from '../core/localizer';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationSuspiciousName(context) {\n const type = 'suspicious_name';\n const keysToTestForGenericValues = [\n 'aerialway', 'aeroway', 'amenity', 'building', 'craft', 'highway',\n 'leisure', 'railway', 'man_made', 'office', 'shop', 'tourism', 'waterway'\n ];\n const ignoredPresets = new Set([\n 'amenity/place_of_worship/christian/jehovahs_witness',\n '__test__ignored_preset' // for unit tests\n ]);\n let _waitingForNsi = false;\n\n\n // Attempt to match a generic record in the name-suggestion-index.\n function isGenericMatchInNsi(tags) {\n const nsi = services.nsi;\n if (nsi) {\n _waitingForNsi = (nsi.status() === 'loading');\n if (!_waitingForNsi) {\n return nsi.isGenericName(tags);\n }\n }\n return false;\n }\n\n\n // Test if the name is just the key or tag value (e.g. \"park\")\n function nameMatchesRawTag(lowercaseName, tags) {\n for (let i = 0; i < keysToTestForGenericValues.length; i++) {\n let key = keysToTestForGenericValues[i];\n let val = tags[key];\n if (val) {\n val = val.toLowerCase();\n if (key === lowercaseName ||\n val === lowercaseName ||\n key.replace(/\\_/g, ' ') === lowercaseName ||\n val.replace(/\\_/g, ' ') === lowercaseName) {\n return true;\n }\n }\n }\n return false;\n }\n\n /** @param {string} name */\n function nameMatchesPresetName(name, preset) {\n if (!preset) return false;\n if (ignoredPresets.has(preset.id)) return false;\n\n name = name.toLowerCase();\n return name === preset.name().toLowerCase() || preset.aliases().some(alias => name === alias.toLowerCase());\n }\n\n /** @param {string} name */\n function isGenericName(name, tags, preset) {\n name = name.toLowerCase();\n return nameMatchesRawTag(name, tags) || nameMatchesPresetName(name, preset) || isGenericMatchInNsi(tags);\n }\n\n function makeGenericNameIssue(entityId, nameKey, genericName, langCode) {\n return new validationIssue({\n type: type,\n subtype: 'generic_name',\n severity: 'warning',\n message: function(context) {\n let entity = context.hasEntity(this.entityIds[0]);\n if (!entity) return '';\n let preset = presetManager.match(entity, context.graph());\n let langName = langCode && localizer.languageName(langCode);\n return t.append('issues.generic_name.message' + (langName ? '_language' : ''),\n { feature: preset.name(), name: genericName, language: langName }\n );\n },\n reference: showReference,\n entityIds: [entityId],\n hash: `${nameKey}=${genericName}`,\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_the_name.title'),\n onClick: function(context) {\n let entityId = this.issue.entityIds[0];\n let entity = context.entity(entityId);\n let tags = Object.assign({}, entity.tags); // shallow copy\n delete tags[nameKey];\n context.perform(\n actionChangeTags(entityId, tags), t('issues.fix.remove_generic_name.annotation')\n );\n }\n })\n ];\n }\n });\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.generic_name.reference'));\n }\n }\n\n let validation = function checkGenericName(entity) {\n const tags = entity.tags;\n\n // a generic name is allowed if it's a known brand or entity\n const hasWikidata = (!!tags.wikidata || !!tags['brand:wikidata'] || !!tags['operator:wikidata']);\n if (hasWikidata) return [];\n\n let issues = [];\n\n const preset = presetManager.match(entity, context.graph());\n\n for (let key in tags) {\n const m = key.match(/^name(?:(?::)([a-zA-Z_-]+))?$/);\n if (!m) continue;\n\n const langCode = m.length >= 2 ? m[1] : null;\n const value = tags[key];\n\n if (isGenericName(value, tags, preset)) {\n issues.provisional = _waitingForNsi; // retry later if we are waiting on NSI to finish loading\n issues.push(makeGenericNameIssue(entity.id, key, value, langCode));\n }\n }\n\n return issues;\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { prefs } from '../core/preferences';\nimport { t } from '../core/localizer';\n//import { actionChangeTags } from '../actions/change_tags';\nimport { actionOrthogonalize } from '../actions/orthogonalize';\nimport { geoOrthoCanOrthogonalize } from '../geo/ortho';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\nexport function validationUnsquareWay(context) {\n var type = 'unsquare_way';\n var DEFAULT_DEG_THRESHOLD = 5; // see also issues.js\n\n // use looser epsilon for detection to reduce warnings of buildings that are essentially square already\n var epsilon = 0.05;\n var nodeThreshold = 10;\n\n function isBuilding(entity, graph) {\n if (entity.type !== 'way' || entity.geometry(graph) !== 'area') return false;\n return entity.tags.building && entity.tags.building !== 'no';\n }\n\n\n var validation = function checkUnsquareWay(entity, graph) {\n\n if (!isBuilding(entity, graph)) return [];\n\n // don't flag ways marked as physically unsquare\n if (entity.tags.nonsquare === 'yes') return [];\n\n var isClosed = entity.isClosed();\n if (!isClosed) return []; // this building has bigger problems\n\n // don't flag ways with lots of nodes since they are likely detail-mapped\n var nodes = graph.childNodes(entity).slice(); // shallow copy\n if (nodes.length > nodeThreshold + 1) return []; // +1 because closing node appears twice\n\n // ignore if not all nodes are fully downloaded\n var osm = services.osm;\n if (!osm || nodes.some(function(node) { return !osm.isDataLoaded(node.loc); })) return [];\n\n // don't flag connected ways to avoid unresolvable unsquare loops\n var hasConnectedSquarableWays = nodes.some(function(node) {\n return graph.parentWays(node).some(function(way) {\n if (way.id === entity.id) return false;\n if (isBuilding(way, graph)) return true;\n return graph.parentRelations(way).some(function(parentRelation) {\n return parentRelation.isMultipolygon() &&\n parentRelation.tags.building &&\n parentRelation.tags.building !== 'no';\n });\n });\n });\n if (hasConnectedSquarableWays) return [];\n\n\n // user-configurable square threshold\n var storedDegreeThreshold = prefs('validate-square-degrees');\n var degreeThreshold = isFinite(storedDegreeThreshold) ? Number(storedDegreeThreshold) : DEFAULT_DEG_THRESHOLD;\n\n var points = nodes.map(function(node) { return context.projection(node.loc); });\n if (!geoOrthoCanOrthogonalize(points, isClosed, epsilon, degreeThreshold, true)) return [];\n\n return [new validationIssue({\n type: type,\n subtype: 'building',\n severity: 'suggestion',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.unsquare_way.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [entity.id],\n hash: degreeThreshold,\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-orthogonalize',\n title: t.append('issues.fix.square_feature.title'),\n onClick: function(context, completionHandler) {\n var entityId = this.issue.entityIds[0];\n // use same degree threshold as for detection\n context.perform(\n actionOrthogonalize(entityId, context.projection, undefined, degreeThreshold),\n t('operations.orthogonalize.annotation.feature', { n: 1 })\n );\n // run after the squaring transition (currently 150ms)\n window.setTimeout(function() { completionHandler(); }, 175);\n }\n }),\n /*\n new validationIssueFix({\n title: t.append('issues.fix.tag_as_unsquare.title'),\n onClick: function(context) {\n var entityId = this.issue.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n tags.nonsquare = 'yes';\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.tag_as_unsquare.annotation')\n );\n }\n })\n */\n ];\n }\n })];\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.unsquare_way.buildings.reference'));\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "export { validationAlmostJunction } from './almost_junction';\nexport { validationCloseNodes } from './close_nodes';\nexport { validationCrossingWays } from './crossing_ways';\nexport { validationDisconnectedWay } from './disconnected_way';\nexport { validationMissingStartDate } from './missing_start_date';\nexport { validationFormatting } from './invalid_format';\nexport { validationHelpRequest } from './help_request';\nexport { validationImpossibleOneway } from './impossible_oneway';\nexport { validationIncompatibleSource } from './incompatible_source';\nexport { validationMaprules } from './maprules';\nexport { validationMismatchedDates } from './mismatched_dates';\nexport { validationMismatchedGeometry } from './mismatched_geometry';\nexport { validationMissingRole } from './missing_role';\nexport { validationMissingTag } from './missing_tag';\nexport { validationMutuallyExclusiveTags } from './mutually_exclusive_tags';\nexport { validationOsmApiLimits } from './osm_api_limits';\nexport { validationOutdatedTags } from './outdated_tags';\nexport { validationPrivateData } from './private_data';\nexport { validationSuspiciousName } from './suspicious_name';\nexport { validationUnsquareWay } from './unsquare_way';\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from './preferences';\nimport { coreDifference } from './difference';\nimport { geoExtent } from '../geo/extent';\nimport { modeSelect } from '../modes/select';\nimport { utilArrayChunk, utilArrayDifference, utilArrayGroupBy, utilArrayIntersection, utilArrayUnion, utilEntityAndDeepMemberIDs, utilRebind } from '../util';\nimport * as Validations from '../validations/index';\n\n\nexport function coreValidator(context) {\n let dispatch = d3_dispatch('validated', 'focusedIssue');\n const validator = {};\n\n let _rules = {};\n let _disabledRules = {};\n\n let _ignoredIssueIDs = new Set();\n let _resolvedIssueIDs = new Set();\n let _baseCache = validationCache('base'); // issues before any user edits\n let _headCache = validationCache('head'); // issues after all user edits\n let _completeDiff = {}; // complete diff base -> head of what the user changed\n let _headIsCurrent = false;\n\n let _deferredRIC = {}; // Object( RequestIdleCallback handle : rejectPromise method )\n let _deferredST = new Set(); // Set( SetTimeout handles )\n let _headPromise; // Promise fulfilled when validation is performed up to headGraph snapshot\n\n const RETRY = 5000; // wait 5sec before revalidating provisional entities\n\n\n // Allow validation severity to be overridden by url queryparams...\n // See: https://github.com/openstreetmap/iD/pull/8243\n //\n // Each param should contain a urlencoded comma separated list of\n // `type/subtype` rules. `*` may be used as a wildcard..\n // Examples:\n // `validationError=disconnected_way/*`\n // `validationError=disconnected_way/highway`\n // `validationError=crossing_ways/bridge*`\n // `validationError=crossing_ways/bridge*,crossing_ways/tunnel*`\n\n const _errorOverrides = parseHashParam(context.initialHashParams.validationError);\n const _warningOverrides = parseHashParam(context.initialHashParams.validationWarning);\n const _suggestionOverrides = parseHashParam(context.initialHashParams.validationSuggestion);\n const _disableOverrides = parseHashParam(context.initialHashParams.validationDisable);\n\n // `parseHashParam()` (private)\n // Checks hash parameters for severity overrides\n // Arguments\n // `param` - a url hash parameter (`validationError`, `validationWarning`, `validationSuggestion`, or `validationDisable`)\n // Returns\n // Array of Objects like { type: RegExp, subtype: RegExp }\n //\n function parseHashParam(param) {\n let result = [];\n let rules = (param || '').split(',');\n rules.forEach(rule => {\n rule = rule.trim();\n const parts = rule.split('/', 2); // \"type/subtype\"\n const type = parts[0];\n const subtype = parts[1] || '*';\n if (!type || !subtype) return;\n result.push({ type: makeRegExp(type), subtype: makeRegExp(subtype) });\n });\n return result;\n\n function makeRegExp(str) {\n const escaped = str\n .replace(/[-\\/\\\\^$+?.()|[\\]{}]/g, '\\\\$&') // escape all reserved chars except for the '*'\n .replace(/\\*/g, '.*'); // treat a '*' like '.*'\n return new RegExp('^' + escaped + '$');\n }\n }\n\n\n // `init()`\n // Initialize the validator, called once on iD startup\n //\n validator.init = () => {\n Object.values(Validations).forEach(validation => {\n if (typeof validation !== 'function') return;\n const fn = validation(context);\n const key = fn.type;\n _rules[key] = fn;\n });\n\n let disabledRules = prefs('validate-disabledRules');\n if (disabledRules) {\n disabledRules.split(',').forEach(k => _disabledRules[k] = true);\n }\n };\n\n\n // `reset()` (private)\n // Cancels deferred work and resets all caches\n //\n // Arguments\n // `resetIgnored` - `true` to clear the list of user-ignored issues\n //\n function reset(resetIgnored) {\n // empty queues\n _baseCache.queue = [];\n _headCache.queue = [];\n\n // cancel deferred work and reject any pending promise\n Object.keys(_deferredRIC).forEach(key => {\n window.cancelIdleCallback(key);\n _deferredRIC[key]();\n });\n _deferredRIC = {};\n _deferredST.forEach(window.clearTimeout);\n _deferredST.clear();\n\n // clear caches\n if (resetIgnored) _ignoredIssueIDs.clear();\n _resolvedIssueIDs.clear();\n _baseCache = validationCache('base');\n _headCache = validationCache('head');\n _completeDiff = {};\n _headIsCurrent = false;\n }\n\n\n // `reset()`\n // clear caches, called whenever iD resets after a save or switches sources\n // (clears out the _ignoredIssueIDs set also)\n //\n validator.reset = () => {\n reset(true);\n };\n\n\n // `resetIgnoredIssues()`\n // clears out the _ignoredIssueIDs Set\n //\n validator.resetIgnoredIssues = () => {\n _ignoredIssueIDs.clear();\n dispatch.call('validated'); // redraw UI\n };\n\n\n // `revalidateUnsquare()`\n // Called whenever the user changes the unsquare threshold\n // It reruns just the \"unsquare_way\" validation on all buildings.\n //\n validator.revalidateUnsquare = () => {\n revalidateUnsquare(_headCache);\n revalidateUnsquare(_baseCache);\n dispatch.call('validated');\n };\n\n function revalidateUnsquare(cache) {\n const checkUnsquareWay = _rules.unsquare_way;\n if (!cache.graph || typeof checkUnsquareWay !== 'function') return;\n\n // uncache existing\n cache.uncacheIssuesOfType('unsquare_way');\n\n const buildings = context.history().tree().intersects(geoExtent([-180,-90],[180, 90]), cache.graph) // everywhere\n .filter(entity => (entity.type === 'way' && entity.tags.building && entity.tags.building !== 'no'));\n\n // rerun for all buildings\n buildings.forEach(entity => {\n const detected = checkUnsquareWay(entity, cache.graph);\n if (!detected.length) return;\n cache.cacheIssues(detected);\n });\n }\n\n\n // `getIssues()`\n // Gets all issues that match the given options\n // This is called by many other places\n //\n // Arguments\n // `options` Object like:\n // {\n // what: 'all', // 'all' or 'edited'\n // where: 'all', // 'all' or 'visible'\n // includeIgnored: false, // true, false, or 'only'\n // includeDisabledRules: false // true, false, or 'only'\n // }\n //\n // Returns\n // An Array containing the issues\n //\n validator.getIssues = (options) => {\n const opts = Object.assign({ what: 'all', where: 'all', includeIgnored: false, includeDisabledRules: false }, options);\n const view = context.map().extent();\n let seen = new Set();\n let results = [];\n\n // collect head issues - present in the user edits\n if (_headCache.graph && _headCache.graph !== _baseCache.graph) {\n Object.values(_headCache.issuesByIssueID).forEach(issue => {\n // In the head cache, only count features that the user is responsible for - #8632\n // For example, a user can undo some work and an issue will still present in the\n // head graph, but we don't want to credit the user for causing that issue.\n const userModified = (issue.entityIds || []).some(id => _completeDiff.hasOwnProperty(id));\n if (opts.what === 'edited' && !userModified) return; // present in head but user didn't touch it\n\n if (!filter(issue)) return;\n seen.add(issue.id);\n results.push(issue);\n });\n }\n\n // collect base issues - present before user edits\n if (opts.what === 'all') {\n Object.values(_baseCache.issuesByIssueID).forEach(issue => {\n if (!filter(issue)) return;\n seen.add(issue.id);\n results.push(issue);\n });\n }\n\n return results;\n\n\n // Filter the issue set to include only what the calling code wants to see.\n // Note that we use `context.graph()`/`context.hasEntity()` here, not `cache.graph`,\n // because that is the graph that the calling code will be using.\n function filter(issue) {\n if (!issue) return false;\n if (seen.has(issue.id)) return false;\n if (_resolvedIssueIDs.has(issue.id)) return false;\n if (opts.includeDisabledRules === 'only' && !_disabledRules[issue.type]) return false;\n if (!opts.includeDisabledRules && _disabledRules[issue.type]) return false;\n\n if (opts.includeIgnored === 'only' && !_ignoredIssueIDs.has(issue.id)) return false;\n if (!opts.includeIgnored && _ignoredIssueIDs.has(issue.id)) return false;\n\n // This issue may involve an entity that doesn't exist in context.graph()\n // This can happen because validation is async and rendering the issue lists is async.\n if ((issue.entityIds || []).some(id => !context.hasEntity(id))) return false;\n\n if (opts.where === 'visible') {\n const extent = issue.extent(context.graph());\n if (!view.intersects(extent)) return false;\n }\n\n return true;\n }\n };\n\n\n // `getResolvedIssues()`\n // Gets the issues that have been fixed by the user.\n //\n // Resolved issues are tracked in the `_resolvedIssueIDs` Set,\n // and they should all be issues that exist in the _baseCache.\n //\n // Returns\n // An Array containing the issues\n //\n validator.getResolvedIssues = () => {\n return Array.from(_resolvedIssueIDs)\n .map(issueID => _baseCache.issuesByIssueID[issueID])\n .filter(Boolean);\n };\n\n\n // `focusIssue()`\n // Adjusts the map to focus on the given issue.\n // (requires the issue to have a reasonable extent defined)\n //\n // Arguments\n // `issue` - the issue to focus on\n //\n validator.focusIssue = (issue) => {\n // Note that we use `context.graph()`/`context.hasEntity()` here, not `cache.graph`,\n // because that is the graph that the calling code will be using.\n const graph = context.graph();\n let selectID;\n\n // Try to focus the map at the center of the issue..\n let issueExtent = issue.extent(graph);\n\n // Try to select the first entity in the issue..\n if (issue.entityIds && issue.entityIds.length) {\n selectID = issue.entityIds[0];\n\n // If a relation, focus on one of its members instead.\n // Otherwise we might be focusing on a part of map where the relation is not visible.\n if (selectID && selectID.charAt(0) === 'r') { // relation\n const ids = utilEntityAndDeepMemberIDs([selectID], graph);\n let nodeID = ids.find(id => id.charAt(0) === 'n' && graph.hasEntity(id));\n\n if (!nodeID) { // relation has no downloaded nodes to focus on\n const wayID = ids.find(id => id.charAt(0) === 'w' && graph.hasEntity(id));\n if (wayID) {\n nodeID = graph.entity(wayID).first(); // focus on the first node of this way\n }\n }\n\n if (nodeID) {\n issueExtent = graph.entity(nodeID).extent(graph);\n }\n }\n }\n\n // Adjust the view\n context.map().zoomToEase(issueExtent);\n\n if (selectID) { // Enter select mode\n window.setTimeout(() => {\n context.enter(modeSelect(context, [selectID]));\n dispatch.call('focusedIssue', this, issue);\n }, 250); // after ease\n }\n };\n\n\n // `getIssuesBySeverity()`\n // Gets the issues then groups them by error/warning/suggestion\n // (This just calls getIssues, then puts issues in groups)\n //\n // Arguments\n // `options` - (see `getIssues`)\n // Returns\n // Object result like:\n // {\n // error: Array of errors,\n // warning: Array of warnings,\n // suggestion: Array of suggestions,\n // }\n //\n validator.getIssuesBySeverity = (options) => {\n let groups = utilArrayGroupBy(validator.getIssues(options), 'severity');\n groups.error = groups.error || [];\n groups.warning = groups.warning || [];\n groups.suggestion = groups.suggestion || [];\n return groups;\n };\n\n\n // `getEntityIssues()`\n // Gets the issues that the given entity IDs have in common, matching the given options\n // (This just calls getIssues, then filters for the given entity IDs)\n // The issues are sorted for relevance\n //\n // Arguments\n // `entityIDs` - Array or Set of entityIDs to get issues for\n // `options` - (see `getIssues`)\n // Returns\n // An Array containing the issues\n //\n validator.getSharedEntityIssues = (entityIDs, options) => {\n const orderedIssueTypes = [ // Show some issue types in a particular order:\n 'missing_tag', 'missing_role', // - missing data first\n 'outdated_tags', 'mismatched_geometry', // - identity issues\n 'crossing_ways', 'almost_junction', // - geometry issues where fixing them might solve connectivity issues\n 'disconnected_way', 'impossible_oneway' // - finally connectivity issues\n ];\n\n const allIssues = validator.getIssues(options);\n const forEntityIDs = new Set(entityIDs);\n\n return allIssues\n .filter(issue => (issue.entityIds || []).some(entityID => forEntityIDs.has(entityID)))\n .sort((issue1, issue2) => {\n if (issue1.type === issue2.type) { // issues of the same type, sort deterministically\n return issue1.id < issue2.id ? -1 : 1;\n }\n const index1 = orderedIssueTypes.indexOf(issue1.type);\n const index2 = orderedIssueTypes.indexOf(issue2.type);\n if (index1 !== -1 && index2 !== -1) { // both issue types have explicit sort orders\n return index1 - index2;\n } else if (index1 === -1 && index2 === -1) { // neither issue type has an explicit sort order, sort by type\n return issue1.type < issue2.type ? -1 : 1;\n } else { // order explicit types before everything else\n return index1 !== -1 ? -1 : 1;\n }\n });\n };\n\n\n // `getEntityIssues()`\n // Get an array of detected issues for the given entityID.\n // (This just calls getSharedEntityIssues for a single entity)\n //\n // Arguments\n // `entityID` - the entity ID to get the issues for\n // `options` - (see `getIssues`)\n // Returns\n // An Array containing the issues\n //\n validator.getEntityIssues = (entityID, options) => {\n return validator.getSharedEntityIssues([entityID], options);\n };\n\n\n // `getRuleKeys()`\n //\n // Returns\n // An Array containing the rule keys\n //\n validator.getRuleKeys = () => {\n return Object.keys(_rules);\n };\n\n\n // `isRuleEnabled()`\n //\n // Arguments\n // `key` - the rule to check (e.g. 'crossing_ways')\n // Returns\n // `true`/`false`\n //\n validator.isRuleEnabled = (key) => {\n return !_disabledRules[key];\n };\n\n\n // `toggleRule()`\n // Toggles a single validation rule,\n // then reruns the validation so that the user sees something happen in the UI\n //\n // Arguments\n // `key` - the rule to toggle (e.g. 'crossing_ways')\n //\n validator.toggleRule = (key) => {\n if (_disabledRules[key]) {\n delete _disabledRules[key];\n } else {\n _disabledRules[key] = true;\n }\n\n prefs('validate-disabledRules', Object.keys(_disabledRules).join(','));\n validator.validate();\n };\n\n\n // `disableRules()`\n // Disables given validation rules,\n // then reruns the validation so that the user sees something happen in the UI\n //\n // Arguments\n // `keys` - Array or Set containing rule keys to disable\n //\n validator.disableRules = (keys) => {\n _disabledRules = {};\n keys.forEach(k => _disabledRules[k] = true);\n\n prefs('validate-disabledRules', Object.keys(_disabledRules).join(','));\n validator.validate();\n };\n\n\n // `ignoreIssue()`\n // Don't show the given issue in lists\n //\n // Arguments\n // `issueID` - the issueID\n //\n validator.ignoreIssue = (issueID) => {\n _ignoredIssueIDs.add(issueID);\n };\n\n\n // `validate()`\n // Validates anything that has changed in the head graph since the last time it was run.\n // (head graph contains user's edits)\n //\n // Returns\n // A Promise fulfilled when the validation has completed and then dispatches a `validated` event.\n // This may take time but happen in the background during browser idle time.\n //\n validator.validate = () => {\n // Make sure the caches have graphs assigned to them.\n // (we don't do this in `reset` because context is still resetting things and `history.base()` is unstable then)\n const baseGraph = context.history().base();\n if (!_headCache.graph) _headCache.graph = baseGraph;\n if (!_baseCache.graph) _baseCache.graph = baseGraph;\n\n const prevGraph = _headCache.graph;\n const currGraph = context.graph();\n\n if (currGraph === prevGraph) { // _headCache.graph is current - we are caught up\n _headIsCurrent = true;\n dispatch.call('validated');\n return Promise.resolve();\n }\n\n if (_headPromise) { // Validation already in process, but we aren't caught up to current\n _headIsCurrent = false; // We will need to catch up after the validation promise fulfills\n return _headPromise;\n }\n\n // If we get here, its time to start validating stuff.\n _headCache.graph = currGraph; // take snapshot\n _completeDiff = context.history().difference().complete();\n const incrementalDiff = coreDifference(prevGraph, currGraph);\n const diff = Object.keys(incrementalDiff.complete());\n const entityIDs = _headCache.withAllRelatedEntities(diff); // expand set\n\n if (!entityIDs.size) {\n dispatch.call('validated');\n return Promise.resolve();\n }\n\n // revalidate also connected (or previously connected) entities to the current way\n // https://github.com/openstreetmap/iD/issues/8758\n const addConnectedWays = graph => diff\n .filter(entityID => graph.hasEntity(entityID))\n .map(entityID => graph.entity(entityID))\n .flatMap(entity => graph.childNodes(entity))\n .flatMap(vertex => graph.parentWays(vertex))\n .forEach(way => entityIDs.add(way.id));\n addConnectedWays(currGraph);\n addConnectedWays(prevGraph);\n\n // revalidate entities with changed relation memberships\n // https://github.com/openstreetmap/iD/issues/10786\n Object.values({...incrementalDiff.created(), ...incrementalDiff.deleted()})\n .filter(e => e.type === 'relation')\n .flatMap(r => r.members)\n .forEach(m => entityIDs.add(m.id));\n Object.values(incrementalDiff.modified())\n .filter(e => e.type === 'relation')\n .map(r => ({ baseEntity: prevGraph.entity(r.id), headEntity: r }))\n .forEach(({ baseEntity, headEntity }) => {\n const bm = baseEntity.members.map(m => m.id);\n const hm = headEntity.members.map(m => m.id);\n const symDiff = utilArrayDifference(utilArrayUnion(bm, hm), utilArrayIntersection(bm, hm));\n symDiff.forEach(id => entityIDs.add(id));\n });\n\n _headPromise = validateEntitiesAsync(entityIDs, _headCache)\n .then(() => updateResolvedIssues(entityIDs))\n .then(() => dispatch.call('validated'))\n .catch(() => { /* ignore */ })\n .then(() => {\n _headPromise = null;\n if (!_headIsCurrent) {\n validator.validate(); // run it again to catch up to current graph\n }\n });\n\n return _headPromise;\n };\n\n\n // register event handlers:\n\n // WHEN TO RUN VALIDATION:\n // When history changes:\n context.history()\n .on('restore.validator', validator.validate) // on restore saved history\n .on('undone.validator', validator.validate) // on undo\n .on('redone.validator', validator.validate) // on redo\n .on('reset.validator', () => { // on history reset - happens after save, or enter/exit walkthrough\n reset(false); // cached issues aren't valid any longer if the history has been reset\n validator.validate();\n });\n // but not on 'change' (e.g. while drawing)\n\n // When user changes editing modes (to catch recent changes e.g. drawing)\n context\n .on('exit.validator', validator.validate);\n\n // When merging fetched data, validate base graph:\n context.history()\n .on('merge.validator', entities => {\n if (!entities) return;\n\n // Make sure the caches have graphs assigned to them.\n // (we don't do this in `reset` because context is still resetting things and `history.base()` is unstable then)\n const baseGraph = context.history().base();\n if (!_headCache.graph) _headCache.graph = baseGraph;\n if (!_baseCache.graph) _baseCache.graph = baseGraph;\n\n let entityIDs = entities.map(entity => entity.id);\n entityIDs = _baseCache.withAllRelatedEntities(entityIDs); // expand set\n validateEntitiesAsync(entityIDs, _baseCache);\n });\n\n\n\n // `validateEntity()` (private)\n // Runs all validation rules on a single entity.\n // Some things to note:\n // - Graph is passed in from whenever the validation was started. Validators shouldn't use\n // `context.graph()` because this all happens async, and the graph might have changed\n // (for example, nodes getting deleted before the validation can run)\n // - Validator functions may still be waiting on something and return a \"provisional\" result.\n // In this situation, we will schedule to revalidate the entity sometime later.\n //\n // Arguments\n // `entity` - The entity\n // `graph` - graph containing the entity\n //\n // Returns\n // Object result like:\n // {\n // issues: Array of detected issues\n // provisional: `true` if provisional result, `false` if final result\n // }\n //\n function validateEntity(entity, graph) {\n let result = { issues: [], provisional: false };\n Object.keys(_rules).forEach(runValidation); // run all rules\n return result;\n\n\n // runs validation and appends resulting issues\n function runValidation(key) {\n const fn = _rules[key];\n if (typeof fn !== 'function') {\n console.error('no such validation rule = ' + key); // eslint-disable-line no-console\n return;\n }\n\n let detected = fn(entity, graph);\n if (detected.provisional) { // this validation should be run again later\n result.provisional = true;\n }\n detected = detected.filter(applySeverityOverrides);\n result.issues = result.issues.concat(detected);\n\n\n // If there are any override rules that match the issue type/subtype,\n // adjust severity (or disable it) and keep/discard as quickly as possible.\n function applySeverityOverrides(issue) {\n const type = issue.type;\n const subtype = issue.subtype || '';\n let i;\n\n for (i = 0; i < _errorOverrides.length; i++) {\n if (_errorOverrides[i].type.test(type) && _errorOverrides[i].subtype.test(subtype)) {\n issue.severity = 'error';\n return true;\n }\n }\n for (i = 0; i < _warningOverrides.length; i++) {\n if (_warningOverrides[i].type.test(type) && _warningOverrides[i].subtype.test(subtype)) {\n issue.severity = 'warning';\n return true;\n }\n }\n for (i = 0; i < _suggestionOverrides.length; i++) {\n if (_suggestionOverrides[i].type.test(type) && _suggestionOverrides[i].subtype.test(subtype)) {\n issue.severity = 'suggestion';\n return true;\n }\n }\n for (i = 0; i < _disableOverrides.length; i++) {\n if (_disableOverrides[i].type.test(type) && _disableOverrides[i].subtype.test(subtype)) {\n return false;\n }\n }\n return true;\n }\n }\n }\n\n\n // `updateResolvedIssues()` (private)\n // Determine if any issues were resolved for the given entities.\n // This is called by `validate()` after validation of the head graph\n //\n // Give the user credit for fixing an issue if:\n // - the issue is in the base cache\n // - the issue is not in the head cache\n // - the user did something to one of the entities involved in the issue\n //\n // Arguments\n // `entityIDs` - Array or Set containing entity IDs.\n //\n function updateResolvedIssues(entityIDs) {\n entityIDs.forEach(entityID => {\n const baseIssues = _baseCache.issuesByEntityID[entityID];\n if (!baseIssues) return;\n\n baseIssues.forEach(issueID => {\n // Check if the user did something to one of the entities involved in this issue.\n // (This issue could involve multiple entities, e.g. disconnected routable features)\n const issue = _baseCache.issuesByIssueID[issueID];\n const userModified = (issue.entityIds || []).some(id => _completeDiff.hasOwnProperty(id));\n\n if (userModified && !_headCache.issuesByIssueID[issueID]) { // issue seems fixed\n _resolvedIssueIDs.add(issueID);\n } else { // issue still not resolved\n _resolvedIssueIDs.delete(issueID); // (did undo, or possibly fixed and then re-caused the issue)\n }\n });\n });\n }\n\n\n // `validateEntitiesAsync()` (private)\n // Schedule validation for many entities.\n //\n // Arguments\n // `entityIDs` - Array or Set containing entityIDs.\n // `graph` - the graph to validate that contains those entities\n // `cache` - the cache to store results in (_headCache or _baseCache)\n //\n // Returns\n // A Promise fulfilled when the validation has completed.\n // This may take time but happen in the background during browser idle time.\n //\n function validateEntitiesAsync(entityIDs, cache) {\n // Enqueue the work\n const jobs = Array.from(entityIDs).map(entityID => {\n if (cache.queuedEntityIDs.has(entityID)) return null; // queued already\n cache.queuedEntityIDs.add(entityID);\n\n // Clear caches for existing issues related to this entity\n cache.uncacheEntityID(entityID);\n\n return () => {\n cache.queuedEntityIDs.delete(entityID);\n\n const graph = cache.graph;\n if (!graph) return; // was reset?\n\n const entity = graph.hasEntity(entityID); // Sanity check: don't validate deleted entities\n if (!entity) return;\n\n // detect new issues and update caches\n const result = validateEntity(entity, graph);\n if (result.provisional) { // provisional result\n cache.provisionalEntityIDs.add(entityID); // we'll need to revalidate this entity again later\n }\n\n cache.cacheIssues(result.issues); // update cache\n };\n\n }).filter(Boolean);\n\n\n // Perform the work in chunks.\n // Because this will happen during idle callbacks, we want to choose a chunk size\n // that won't make the browser stutter too badly.\n cache.queue = cache.queue.concat(utilArrayChunk(jobs, 100));\n\n // Perform the work\n if (cache.queuePromise) return cache.queuePromise;\n\n cache.queuePromise = processQueue(cache)\n .then(() => revalidateProvisionalEntities(cache))\n .catch(() => { /* ignore */ })\n .finally(() => cache.queuePromise = null);\n\n return cache.queuePromise;\n }\n\n\n // `revalidateProvisionalEntities()` (private)\n // Sometimes a validator will return a \"provisional\" result.\n // In this situation, we'll need to revalidate the entity later.\n // This function waits a delay, then places them back into the validation queue.\n //\n // Arguments\n // `cache` - The cache (_headCache or _baseCache)\n //\n function revalidateProvisionalEntities(cache) {\n if (!cache.provisionalEntityIDs.size) return; // nothing to do\n\n const handle = window.setTimeout(() => {\n _deferredST.delete(handle);\n if (!cache.provisionalEntityIDs.size) return; // nothing to do\n validateEntitiesAsync(Array.from(cache.provisionalEntityIDs), cache);\n }, RETRY);\n\n _deferredST.add(handle);\n }\n\n\n // `processQueue(queue)` (private)\n // Process the next chunk of deferred validation work\n //\n // Arguments\n // `cache` - The cache (_headCache or _baseCache)\n //\n // Returns\n // A Promise fulfilled when the validation has completed.\n // This may take time but happen in the background during browser idle time.\n //\n function processQueue(cache) {\n // console.log(`${cache.which} queue length ${cache.queue.length}`);\n\n if (!cache.queue.length) return Promise.resolve(); // we're done\n const chunk = cache.queue.pop();\n\n return new Promise((resolvePromise, rejectPromise) => {\n const handle = window.requestIdleCallback(() => {\n delete (_deferredRIC[handle]);\n // const t0 = performance.now();\n chunk.forEach(job => job());\n // const t1 = performance.now();\n // console.log('chunk processed in ' + (t1 - t0) + ' ms');\n resolvePromise();\n });\n _deferredRIC[handle] = rejectPromise;\n })\n .then(() => { // dispatch an event sometimes to redraw various UI things\n if (cache.queue.length % 25 === 0) dispatch.call('validated');\n })\n .then(() => processQueue(cache));\n }\n\n\n return utilRebind(validator, dispatch, 'on');\n}\n\n\n// `validationCache()` (private)\n// Creates a cache to store validation state\n// We create 2 of these:\n// `_baseCache` for validation on the base graph (unedited)\n// `_headCache` for validation on the head graph (user edits applied)\n//\n// Arguments\n// `which` - just a String 'base' or 'head' to keep track of it\n//\nfunction validationCache(which) {\n let cache = {\n which: which,\n graph: null,\n queue: [],\n queuePromise: null,\n queuedEntityIDs: new Set(),\n provisionalEntityIDs: new Set(),\n issuesByIssueID: {}, // issue.id -> issue\n issuesByEntityID: {} // entity.id -> Set(issue.id)\n };\n\n\n cache.cacheIssue = (issue) => {\n (issue.entityIds || []).forEach(entityID => {\n if (!cache.issuesByEntityID[entityID]) {\n cache.issuesByEntityID[entityID] = new Set();\n }\n cache.issuesByEntityID[entityID].add(issue.id);\n });\n cache.issuesByIssueID[issue.id] = issue;\n };\n\n\n cache.uncacheIssue = (issue) => {\n (issue.entityIds || []).forEach(entityID => {\n if (cache.issuesByEntityID[entityID]) {\n cache.issuesByEntityID[entityID].delete(issue.id);\n }\n });\n delete cache.issuesByIssueID[issue.id];\n };\n\n\n cache.cacheIssues = (issues) => {\n issues.forEach(cache.cacheIssue);\n };\n\n\n cache.uncacheIssues = (issues) => {\n issues.forEach(cache.uncacheIssue);\n };\n\n\n cache.uncacheIssuesOfType = (type) => {\n const issuesOfType = Object.values(cache.issuesByIssueID)\n .filter(issue => issue.type === type);\n cache.uncacheIssues(issuesOfType);\n };\n\n\n // Remove a single entity and all its related issues from the caches\n cache.uncacheEntityID = (entityID) => {\n const entityIssueIDs = cache.issuesByEntityID[entityID];\n if (entityIssueIDs) {\n entityIssueIDs.forEach(issueID => {\n const issue = cache.issuesByIssueID[issueID];\n if (issue) {\n cache.uncacheIssue(issue);\n } else { // shouldn't happen, clean up\n delete cache.issuesByIssueID[issueID];\n }\n });\n }\n\n delete cache.issuesByEntityID[entityID];\n cache.provisionalEntityIDs.delete(entityID);\n };\n\n\n // Return the expandeded set of entityIDs related to issues for the given entityIDs\n //\n // Arguments\n // `entityIDs` - Array or Set containing entityIDs.\n //\n cache.withAllRelatedEntities = (entityIDs) => {\n let result = new Set();\n (entityIDs || []).forEach(entityID => {\n result.add(entityID); // include self\n\n const entityIssueIDs = cache.issuesByEntityID[entityID];\n if (entityIssueIDs) {\n entityIssueIDs.forEach(issueID => {\n const issue = cache.issuesByIssueID[issueID];\n if (issue) {\n (issue.entityIds || []).forEach(relatedID => result.add(relatedID));\n } else { // shouldn't happen, clean up\n delete cache.issuesByIssueID[issueID];\n }\n });\n }\n });\n\n return result;\n };\n\n\n return cache;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { fileFetcher } from './file_fetcher';\nimport { actionDiscardTags } from '../actions/discard_tags';\nimport { actionMergeRemoteChanges } from '../actions/merge_remote_changes';\nimport { actionNoop } from '../actions/noop';\nimport { actionRevert } from '../actions/revert';\nimport { coreGraph } from '../core/graph';\nimport { t } from '../core/localizer';\nimport { utilArrayUnion, utilArrayUniq, utilDisplayName, utilDisplayType, utilRebind } from '../util';\n\n\n/** @param {iD.Context} context */\nexport function coreUploader(context) {\n\n var dispatch = d3_dispatch(\n // Start and end events are dispatched exactly once each per legitimate outside call to `save`\n 'saveStarted', // dispatched as soon as a call to `save` has been deemed legitimate\n 'saveEnded', // dispatched after the result event has been dispatched\n\n 'willAttemptUpload', // dispatched before the actual upload call occurs, if it will\n 'progressChanged',\n\n // Each save results in one of these outcomes:\n 'resultNoChanges', // upload wasn't attempted since there were no edits\n 'resultErrors', // upload failed due to errors\n 'resultConflicts', // upload failed due to data conflicts\n 'resultSuccess' // upload completed without errors\n );\n\n var _isSaving = false;\n\n let _anyConflictsAutomaticallyResolved = false;\n var _conflicts = [];\n var _errors = [];\n var _origChanges;\n\n var _discardTags = {};\n fileFetcher.get('discarded')\n .then(function(d) { _discardTags = d; })\n .catch(function() { /* ignore */ });\n\n const uploader = {};\n\n uploader.isSaving = function() {\n return _isSaving;\n };\n\n uploader.save = function(changeset, tryAgain, checkConflicts) {\n // Guard against accidentally entering save code twice - #4641\n if (_isSaving && !tryAgain) {\n return;\n }\n\n var osm = context.connection();\n if (!osm) return;\n\n // If user somehow got logged out mid-save, try to reauthenticate..\n // This can happen if they were logged in from before, but the tokens are no longer valid.\n if (!osm.authenticated()) {\n osm.authenticate(function(err) {\n if (!err) {\n uploader.save(changeset, tryAgain, checkConflicts); // continue where we left off..\n }\n });\n return;\n }\n\n if (!_isSaving) {\n _isSaving = true;\n dispatch.call('saveStarted', this);\n }\n\n var history = context.history();\n\n _anyConflictsAutomaticallyResolved = false;\n _conflicts = [];\n _errors = [];\n\n // Store original changes, in case user wants to download them as an .osc file\n _origChanges = history.changes(actionDiscardTags(history.difference(), _discardTags));\n\n // First time, `history.perform` a no-op action.\n // Any conflict resolutions will be done as `history.replace`\n // Remember to pop this later if needed\n if (!tryAgain) {\n history.perform(actionNoop());\n }\n\n // Attempt a fast upload.. If there are conflicts, re-enter with `checkConflicts = true`\n if (!checkConflicts) {\n upload(changeset);\n\n // Do the full (slow) conflict check..\n } else {\n performFullConflictCheck(changeset);\n }\n\n };\n\n\n function performFullConflictCheck(changeset) {\n\n var osm = context.connection();\n if (!osm) return;\n\n var history = context.history();\n\n var localGraph = context.graph();\n var remoteGraph = coreGraph(history.base(), true);\n\n var summary = history.difference().summary();\n var _toCheck = [];\n for (var i = 0; i < summary.length; i++) {\n var item = summary[i];\n if (item.changeType === 'modified') {\n _toCheck.push(item.entity.id);\n }\n }\n\n var _toLoad = withChildNodes(_toCheck, localGraph);\n var _loaded = {};\n var _toLoadCount = 0;\n var _toLoadTotal = _toLoad.length;\n\n if (_toCheck.length) {\n dispatch.call('progressChanged', this, _toLoadCount, _toLoadTotal);\n _toLoad.forEach(function(id) { _loaded[id] = false; });\n osm.loadMultiple(_toLoad, loaded);\n } else {\n upload(changeset);\n }\n\n return;\n\n function withChildNodes(ids, graph) {\n var s = new Set(ids);\n ids.forEach(function(id) {\n var entity = graph.entity(id);\n if (entity.type !== 'way') return;\n\n graph.childNodes(entity).forEach(function(child) {\n if (child.version !== undefined) {\n s.add(child.id);\n }\n });\n });\n\n return Array.from(s);\n }\n\n\n // Reload modified entities into an alternate graph and check for conflicts..\n function loaded(err, result) {\n if (_errors.length) return;\n\n if (err) {\n _errors.push({\n msg: err.message || err.responseText,\n details: [ t('save.status_code', { code: err.status }) ]\n });\n didResultInErrors();\n\n } else {\n var loadMore = [];\n\n result.data.forEach(function(entity) {\n remoteGraph.replace(entity);\n _loaded[entity.id] = true;\n _toLoad = _toLoad.filter(function(val) { return val !== entity.id; });\n\n if (!entity.visible) return;\n\n // Because loadMultiple doesn't download /full like loadEntity,\n // need to also load children that aren't already being checked..\n var i, id;\n if (entity.type === 'way') {\n for (i = 0; i < entity.nodes.length; i++) {\n id = entity.nodes[i];\n if (_loaded[id] === undefined) {\n _loaded[id] = false;\n loadMore.push(id);\n }\n }\n } else if (entity.type === 'relation' && entity.isMultipolygon()) {\n for (i = 0; i < entity.members.length; i++) {\n id = entity.members[i].id;\n if (_loaded[id] === undefined) {\n _loaded[id] = false;\n loadMore.push(id);\n }\n }\n }\n });\n\n _toLoadCount += result.data.length;\n _toLoadTotal += loadMore.length;\n dispatch.call('progressChanged', this, _toLoadCount, _toLoadTotal);\n\n if (loadMore.length) {\n _toLoad.push.apply(_toLoad, loadMore);\n osm.loadMultiple(loadMore, loaded);\n }\n\n if (!_toLoad.length) {\n detectConflicts();\n upload(changeset);\n }\n }\n }\n\n\n function detectConflicts() {\n function choice(id, text, action) {\n return {\n id: id,\n text: text,\n action: function() {\n history.replace(action);\n }\n };\n }\n function formatUser(selection, d) {\n selection\n .append('a')\n .attr('href', osm.userURL(d))\n .attr('target', '_blank')\n .text(d);\n }\n function entityName(entity) {\n return utilDisplayName(entity) || (utilDisplayType(entity.id) + ' ' + entity.id);\n }\n\n function sameVersions(local, remote) {\n if (local.version !== remote.version) return false;\n\n if (local.type === 'way') {\n var children = utilArrayUnion(local.nodes, remote.nodes);\n for (var i = 0; i < children.length; i++) {\n var a = localGraph.hasEntity(children[i]);\n var b = remoteGraph.hasEntity(children[i]);\n if (a && b && a.version !== b.version) return false;\n }\n }\n\n return true;\n }\n\n _toCheck.forEach(function(id) {\n var local = localGraph.entity(id);\n var remote = remoteGraph.entity(id);\n\n if (sameVersions(local, remote)) return;\n\n var merge = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags, formatUser);\n\n history.replace(merge);\n\n var mergeConflicts = merge.conflicts();\n if (!mergeConflicts.length) {\n _anyConflictsAutomaticallyResolved = true;\n return; // merged safely\n }\n\n var forceLocal = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags).withOption('force_local');\n var forceRemote = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags).withOption('force_remote');\n var keepMine = t('save.conflict.' + (remote.visible ? 'keep_local' : 'restore'));\n var keepTheirs = t('save.conflict.' + (remote.visible ? 'keep_remote' : 'delete'));\n\n _conflicts.push({\n id: id,\n name: entityName(local),\n details: mergeConflicts,\n chosen: 1,\n choices: [\n choice(id, keepMine, forceLocal),\n choice(id, keepTheirs, forceRemote)\n ]\n });\n });\n }\n }\n\n\n async function upload(changeset) {\n var osm = context.connection();\n if (!osm) {\n _errors.push({ msg: 'No OSM Service' });\n }\n\n if (_conflicts.length) {\n didResultInConflicts(changeset);\n\n } else if (_errors.length) {\n didResultInErrors();\n\n } else {\n if (_anyConflictsAutomaticallyResolved) {\n // add a changeset tag to aid reviewers\n changeset.tags.merge_conflict_resolved = 'automatically';\n await osm.updateChangesetTags(changeset);\n }\n var history = context.history();\n var changes = history.changes(actionDiscardTags(history.difference(), _discardTags));\n if (changes.modified.length || changes.created.length || changes.deleted.length) {\n\n dispatch.call('willAttemptUpload', this);\n\n osm.putChangeset(changeset, changes, uploadCallback);\n\n } else {\n // changes were insignificant or reverted by user\n didResultInNoChanges();\n }\n }\n }\n\n\n function uploadCallback(err, changeset) {\n if (err) {\n if (err.status === 409) { // 409 Conflict\n uploader.save(changeset, true, true); // tryAgain = true, checkConflicts = true\n } else {\n _errors.push({\n msg: err.message || err.responseText,\n details: [ t('save.status_code', { code: err.status }) ]\n });\n didResultInErrors();\n }\n\n } else {\n didResultInSuccess(changeset);\n }\n }\n\n function didResultInNoChanges() {\n\n dispatch.call('resultNoChanges', this);\n\n endSave();\n\n context.flush(); // reset iD\n }\n\n function didResultInErrors() {\n\n context.history().pop();\n\n dispatch.call('resultErrors', this, _errors);\n\n endSave();\n }\n\n\n function didResultInConflicts(changeset) {\n // add a changeset tag to aid reviewers\n changeset.tags.merge_conflict_resolved = 'manually';\n context.connection().updateChangesetTags(changeset);\n\n _conflicts.sort(function(a, b) { return b.id.localeCompare(a.id); });\n\n dispatch.call('resultConflicts', this, changeset, _conflicts, _origChanges);\n\n endSave();\n }\n\n\n function didResultInSuccess(changeset) {\n\n // delete the edit stack cached to local storage\n context.history().clearSaved();\n\n dispatch.call('resultSuccess', this, changeset);\n\n // Add delay to allow for postgres replication #1646 #2678\n window.setTimeout(function() {\n\n endSave();\n\n context.flush(); // reset iD\n }, 2500);\n }\n\n\n function endSave() {\n _isSaving = false;\n\n dispatch.call('saveEnded', this);\n }\n\n\n uploader.cancelConflictResolution = function() {\n context.history().pop();\n };\n\n\n uploader.processResolvedConflicts = function(changeset) {\n var history = context.history();\n\n for (var i = 0; i < _conflicts.length; i++) {\n if (_conflicts[i].chosen === 1) { // user chose \"use theirs\"\n var entity = context.hasEntity(_conflicts[i].id);\n if (entity && entity.type === 'way') {\n var children = utilArrayUniq(entity.nodes);\n for (var j = 0; j < children.length; j++) {\n history.replace(actionRevert(children[j]));\n }\n }\n history.replace(actionRevert(_conflicts[i].id));\n }\n }\n\n uploader.save(changeset, true, false); // tryAgain = true, checkConflicts = false\n };\n\n\n uploader.reset = function() {\n\n };\n\n\n return utilRebind(uploader, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDrawWay } from '../behavior/draw_way';\n\n\nexport function modeDrawArea(context, wayID, startGraph, button) {\n var mode = {\n button: button,\n id: 'draw-area'\n };\n\n var behavior = behaviorDrawWay(context, wayID, mode, startGraph)\n .on('rejectedSelfIntersection.modeDrawArea', function() {\n context.ui().flash\n .iconName('#iD-icon-no')\n .label(t.append('self_intersection.error.areas'))();\n });\n\n mode.wayID = wayID;\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n mode.selectedIDs = function() {\n return [wayID];\n };\n\n mode.activeID = function() {\n return (behavior && behavior.activeID()) || [];\n };\n\n return mode;\n}\n", "import { actionAddEntity } from '../actions/add_entity';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionAddVertex } from '../actions/add_vertex';\n\nimport { behaviorAddWay } from '../behavior/add_way';\nimport { modeDrawArea } from './draw_area';\nimport { osmNode, osmWay } from '../osm';\n\n\nexport function modeAddArea(context, mode) {\n mode.id = 'add-area';\n\n var behavior = behaviorAddWay(context)\n .on('start', start)\n .on('startFromWay', startFromWay)\n .on('startFromNode', startFromNode);\n\n function defaultTags(loc) {\n var defaultTags = { area: 'yes' };\n if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'area', false, loc);\n return defaultTags;\n }\n\n function actionClose(wayId) {\n return function (graph) {\n return graph.replace(graph.entity(wayId).close());\n };\n }\n\n\n function start(loc) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionClose(way.id)\n );\n\n context.enter(modeDrawArea(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromWay(loc, edge) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionClose(way.id),\n actionAddMidpoint({ loc: loc, edge: edge }, node)\n );\n\n context.enter(modeDrawArea(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromNode(node) {\n var startGraph = context.graph();\n var way = osmWay({ tags: defaultTags(node.loc) });\n\n context.perform(\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionClose(way.id)\n );\n\n context.enter(modeDrawArea(context, way.id, startGraph, mode.button));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n\n return mode;\n}\n", "import { actionAddEntity } from '../actions/add_entity';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionAddVertex } from '../actions/add_vertex';\n\nimport { behaviorAddWay } from '../behavior/add_way';\nimport { modeDrawLine } from './draw_line';\nimport { osmNode, osmWay } from '../osm';\n\n\nexport function modeAddLine(context, mode) {\n mode.id = 'add-line';\n\n var behavior = behaviorAddWay(context)\n .on('start', start)\n .on('startFromWay', startFromWay)\n .on('startFromNode', startFromNode);\n\n function defaultTags(loc) {\n var defaultTags = {};\n if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'line', false, loc);\n return defaultTags;\n }\n\n\n function start(loc) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id)\n );\n\n context.enter(modeDrawLine(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromWay(loc, edge) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionAddMidpoint({ loc: loc, edge: edge }, node)\n );\n\n context.enter(modeDrawLine(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromNode(node) {\n var startGraph = context.graph();\n var way = osmWay({ tags: defaultTags(node.loc) });\n\n context.perform(\n actionAddEntity(way),\n actionAddVertex(way.id, node.id)\n );\n\n context.enter(modeDrawLine(context, way.id, startGraph, mode.button));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDraw } from '../behavior/draw';\nimport { modeBrowse } from './browse';\nimport { modeSelect } from './select';\nimport { osmNode } from '../osm/node';\nimport { actionAddEntity } from '../actions/add_entity';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\n\n\nexport function modeAddPoint(context, mode) {\n\n mode.id = 'add-point';\n\n var behavior = behaviorDraw(context)\n .on('click', add)\n .on('clickWay', addWay)\n .on('clickNode', addNode)\n .on('cancel', cancel)\n .on('finish', cancel);\n\n function defaultTags(loc) {\n var defaultTags = {};\n if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'point', false, loc);\n return defaultTags;\n }\n\n\n function add(loc) {\n var node = osmNode({ loc: loc, tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n t('operations.add.annotation.point')\n );\n\n enterSelectMode(node);\n }\n\n\n function addWay(loc, edge) {\n var node = osmNode({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddMidpoint({loc: loc, edge: edge}, node),\n t('operations.add.annotation.vertex')\n );\n\n enterSelectMode(node);\n }\n\n function enterSelectMode(node) {\n context.enter(\n modeSelect(context, [node.id]).newFeature(true)\n );\n }\n\n\n function addNode(node) {\n const _defaultTags = defaultTags(node.loc);\n if (Object.keys(_defaultTags).length === 0) {\n enterSelectMode(node);\n return;\n }\n\n var tags = Object.assign({}, node.tags); // shallow copy\n for (var key in _defaultTags) {\n tags[key] = _defaultTags[key];\n }\n\n context.perform(\n actionChangeTags(node.id, tags),\n t('operations.add.annotation.point')\n );\n\n enterSelectMode(node);\n }\n\n\n function cancel() {\n context.enter(modeBrowse(context));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n\n return mode;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select,\n selection as d3_selection\n} from 'd3-selection';\n\nimport { geoVecLength } from '../geo';\nimport { osmNote } from '../osm';\nimport { utilRebind } from '../util/rebind';\nimport { utilFastMouse, utilPrefixCSSProperty, utilPrefixDOMProperty } from '../util';\n\n\n/*\n `behaviorDrag` is like `d3_behavior.drag`, with the following differences:\n\n * The `origin` function is expected to return an [x, y] tuple rather than an\n {x, y} object.\n * The events are `start`, `move`, and `end`.\n (https://github.com/mbostock/d3/issues/563)\n * The `start` event is not dispatched until the first cursor movement occurs.\n (https://github.com/mbostock/d3/pull/368)\n * The `move` event has a `point` and `delta` [x, y] tuple properties rather\n than `x`, `y`, `dx`, and `dy` properties.\n * The `end` event is not dispatched if no movement occurs.\n * An `off` function is available that unbinds the drag's internal event handlers.\n */\n\nexport function behaviorDrag() {\n var dispatch = d3_dispatch('start', 'move', 'end');\n\n // see also behaviorSelect\n var _tolerancePx = 1; // keep this low to facilitate pixel-perfect micromapping\n var _penTolerancePx = 4; // styluses can be touchy so require greater movement - #1981\n\n var _origin = null;\n var _selector = '';\n var _targetNode;\n var _targetEntity;\n var _surface;\n var _pointerId;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n var d3_event_userSelectProperty = utilPrefixCSSProperty('UserSelect');\n var d3_event_userSelectSuppress = function() {\n var selection = d3_selection();\n var select = selection.style(d3_event_userSelectProperty);\n selection.style(d3_event_userSelectProperty, 'none');\n return function() {\n selection.style(d3_event_userSelectProperty, select);\n };\n };\n\n\n function pointerdown(d3_event) {\n\n if (_pointerId) return;\n\n _pointerId = d3_event.pointerId || 'mouse';\n\n _targetNode = this;\n\n // only force reflow once per drag\n var pointerLocGetter = utilFastMouse(_surface || _targetNode.parentNode);\n\n var offset;\n var startOrigin = pointerLocGetter(d3_event);\n var started = false;\n var selectEnable = d3_event_userSelectSuppress();\n\n d3_select(window)\n .on(_pointerPrefix + 'move.drag', pointermove)\n .on(_pointerPrefix + 'up.drag pointercancel.drag', pointerup, true);\n\n if (_origin) {\n offset = _origin.call(_targetNode, _targetEntity);\n offset = [offset[0] - startOrigin[0], offset[1] - startOrigin[1]];\n } else {\n offset = [0, 0];\n }\n\n d3_event.stopPropagation();\n\n\n function pointermove(d3_event) {\n if (_pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n var p = pointerLocGetter(d3_event);\n\n if (!started) {\n var dist = geoVecLength(startOrigin, p);\n var tolerance = d3_event.pointerType === 'pen' ? _penTolerancePx : _tolerancePx;\n // don't start until the drag has actually moved somewhat\n if (dist < tolerance) return;\n\n started = true;\n dispatch.call('start', this, d3_event, _targetEntity);\n\n // Don't send a `move` event in the same cycle as `start` since dragging\n // a midpoint will convert the target to a node.\n } else {\n\n startOrigin = p;\n d3_event.stopPropagation();\n d3_event.preventDefault();\n\n var dx = p[0] - startOrigin[0];\n var dy = p[1] - startOrigin[1];\n dispatch.call('move', this, d3_event, _targetEntity, [p[0] + offset[0], p[1] + offset[1]], [dx, dy]);\n }\n }\n\n\n function pointerup(d3_event) {\n if (_pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n _pointerId = null;\n\n if (started) {\n dispatch.call('end', this, d3_event, _targetEntity);\n\n d3_event.preventDefault();\n }\n\n d3_select(window)\n .on(_pointerPrefix + 'move.drag', null)\n .on(_pointerPrefix + 'up.drag pointercancel.drag', null);\n\n selectEnable();\n }\n }\n\n\n function behavior(selection) {\n var matchesSelector = utilPrefixDOMProperty('matchesSelector');\n var delegate = pointerdown;\n\n if (_selector) {\n delegate = function(d3_event) {\n var root = this;\n var target = d3_event.target;\n for (; target && target !== root; target = target.parentNode) {\n var datum = target.__data__;\n\n _targetEntity = datum instanceof osmNote ? datum\n : datum && datum.properties && datum.properties.entity;\n\n if (_targetEntity && target[matchesSelector](_selector)) {\n return pointerdown.call(target, d3_event);\n }\n }\n };\n }\n\n selection\n .on(_pointerPrefix + 'down.drag' + _selector, delegate);\n }\n\n\n behavior.off = function(selection) {\n selection\n .on(_pointerPrefix + 'down.drag' + _selector, null);\n };\n\n\n behavior.selector = function(_) {\n if (!arguments.length) return _selector;\n _selector = _;\n return behavior;\n };\n\n\n behavior.origin = function(_) {\n if (!arguments.length) return _origin;\n _origin = _;\n return behavior;\n };\n\n\n behavior.cancel = function() {\n d3_select(window)\n .on(_pointerPrefix + 'move.drag', null)\n .on(_pointerPrefix + 'up.drag pointercancel.drag', null);\n return behavior;\n };\n\n\n behavior.targetNode = function(_) {\n if (!arguments.length) return _targetNode;\n _targetNode = _;\n return behavior;\n };\n\n\n behavior.targetEntity = function(_) {\n if (!arguments.length) return _targetEntity;\n _targetEntity = _;\n return behavior;\n };\n\n\n behavior.surface = function(_) {\n if (!arguments.length) return _surface;\n _surface = _;\n return behavior;\n };\n\n\n return utilRebind(behavior, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { t } from '../core/localizer';\n\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionConnect } from '../actions/connect';\nimport { actionMoveNode } from '../actions/move_node';\nimport { actionNoop } from '../actions/noop';\n\nimport { behaviorDrag } from '../behavior/drag';\nimport { behaviorEdit } from '../behavior/edit';\nimport { behaviorHover } from '../behavior/hover';\n\nimport {\n geoChooseEdge,\n geoHasLineIntersections,\n geoHasSelfIntersections,\n geoVecSubtract,\n geoViewportEdge\n} from '../geo';\n\nimport { modeBrowse } from './browse';\nimport { modeSelect } from './select';\nimport { osmJoinWays, osmNode } from '../osm';\nimport { utilArrayIntersection, utilKeybinding } from '../util';\n\n\n\nexport function modeDragNode(context) {\n var mode = {\n id: 'drag-node',\n button: 'browse'\n };\n var hover = behaviorHover(context).altDisables(true)\n .on('hover', context.ui().sidebar.hover);\n var edit = behaviorEdit(context);\n\n var _nudgeInterval;\n var _restoreSelectedIDs = [];\n var _wasMidpoint = false;\n var _isCancelled = false;\n var _activeEntity;\n var _startLoc;\n var _lastLoc;\n\n\n function startNudge(d3_event, entity, nudge) {\n if (_nudgeInterval) window.clearInterval(_nudgeInterval);\n _nudgeInterval = window.setInterval(function() {\n context.map().pan(nudge);\n doMove(d3_event, entity, nudge);\n }, 50);\n }\n\n\n function stopNudge() {\n if (_nudgeInterval) {\n window.clearInterval(_nudgeInterval);\n _nudgeInterval = null;\n }\n }\n\n\n function moveAnnotation(entity) {\n return t('operations.move.annotation.' + entity.geometry(context.graph()));\n }\n\n\n function connectAnnotation(nodeEntity, targetEntity) {\n var nodeGeometry = nodeEntity.geometry(context.graph());\n var targetGeometry = targetEntity.geometry(context.graph());\n if (nodeGeometry === 'vertex' && targetGeometry === 'vertex') {\n var nodeParentWayIDs = context.graph().parentWays(nodeEntity);\n var targetParentWayIDs = context.graph().parentWays(targetEntity);\n var sharedParentWays = utilArrayIntersection(nodeParentWayIDs, targetParentWayIDs);\n // if both vertices are part of the same way\n if (sharedParentWays.length !== 0) {\n // if the nodes are next to each other, they are merged\n if (sharedParentWays[0].areAdjacent(nodeEntity.id, targetEntity.id)) {\n return t('operations.connect.annotation.from_vertex.to_adjacent_vertex');\n }\n return t('operations.connect.annotation.from_vertex.to_sibling_vertex');\n }\n }\n return t('operations.connect.annotation.from_' + nodeGeometry + '.to_' + targetGeometry);\n }\n\n\n function shouldSnapToNode(target) {\n if (!_activeEntity) return false;\n return _activeEntity.geometry(context.graph()) !== 'vertex' ||\n (target.geometry(context.graph()) === 'vertex' || presetManager.allowsVertex(target, context.graph()));\n }\n\n\n function origin(entity) {\n return context.projection(entity.loc);\n }\n\n\n function keydown(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope')) {\n context.surface()\n .classed('nope-suppressed', true);\n }\n context.surface()\n .classed('nope', false)\n .classed('nope-disabled', true);\n }\n }\n\n\n function keyup(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope-suppressed')) {\n context.surface()\n .classed('nope', true);\n }\n context.surface()\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false);\n }\n }\n\n\n function start(d3_event, entity) {\n _wasMidpoint = entity.type === 'midpoint';\n var hasHidden = context.features().hasHiddenConnections(entity, context.graph());\n _isCancelled = !context.editable() || d3_event.shiftKey || hasHidden;\n\n\n if (_isCancelled) {\n if (hasHidden) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('modes.drag_node.connected_to_hidden'))();\n }\n return drag.cancel();\n }\n\n if (_wasMidpoint) {\n var midpoint = entity;\n entity = osmNode();\n context.perform(actionAddMidpoint(midpoint, entity));\n entity = context.entity(entity.id); // get post-action entity\n\n var vertex = context.surface().selectAll('.' + entity.id);\n drag.targetNode(vertex.node())\n .targetEntity(entity);\n\n } else {\n context.perform(actionNoop());\n }\n\n _activeEntity = entity;\n _startLoc = entity.loc;\n\n hover.ignoreVertex(entity.geometry(context.graph()) === 'vertex');\n\n context.surface().selectAll('.' + _activeEntity.id)\n .classed('active', true);\n\n context.enter(mode);\n }\n\n\n // related code\n // - `behavior/draw.js` `datum()`\n function datum(d3_event) {\n if (!d3_event || d3_event.altKey) {\n return {};\n } else {\n // When dragging, snap only to touch targets..\n // (this excludes area fills and active drawing elements)\n var d = d3_event.target.__data__;\n return (d && d.properties && d.properties.target) ? d : {};\n }\n }\n\n\n function doMove(d3_event, entity, nudge) {\n nudge = nudge || [0, 0];\n\n var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc);\n var currMouse = geoVecSubtract(currPoint, nudge);\n var loc = context.projection.invert(currMouse);\n\n var target, edge;\n\n if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap..\n // related code\n // - `mode/drag_node.js` `doMove()`\n // - `behavior/draw.js` `click()`\n // - `behavior/draw_way.js` `move()`\n var d = datum(d3_event);\n target = d && d.properties && d.properties.entity;\n var targetLoc = target && target.loc;\n var targetNodes = d && d.properties && d.properties.nodes;\n\n if (targetLoc) { // snap to node/vertex - a point target with `.loc`\n if (shouldSnapToNode(target)) {\n loc = targetLoc;\n }\n\n } else if (targetNodes) { // snap to way - a line target with `.nodes`\n edge = geoChooseEdge(targetNodes, context.map().mouse(), context.projection, end.id);\n if (edge) {\n loc = edge.loc;\n }\n }\n }\n\n context.replace(\n actionMoveNode(entity.id, loc)\n );\n\n // Below here: validations\n var isInvalid = false;\n\n // Check if this connection to `target` could cause relations to break..\n if (target) {\n isInvalid = hasRelationConflict(entity, target, edge, context.graph());\n }\n\n // Check if this drag causes the geometry to break..\n if (!isInvalid) {\n isInvalid = hasInvalidGeometry(entity, context.graph());\n }\n\n\n var nope = context.surface().classed('nope');\n if (isInvalid === 'relation' || isInvalid === 'restriction') {\n if (!nope) { // about to nope - show hint\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('operations.connect.' + isInvalid,\n { relation: presetManager.item('type/restriction').name() }\n ))();\n }\n } else if (isInvalid) {\n var errorID = isInvalid === 'line' ? 'lines' : 'areas';\n context.ui().flash\n .duration(3000)\n .iconName('#iD-icon-no')\n .label(t.append('self_intersection.error.' + errorID))();\n } else {\n if (nope) { // about to un-nope, remove hint\n context.ui().flash\n .duration(1)\n .label('')();\n }\n }\n\n\n var nopeDisabled = context.surface().classed('nope-disabled');\n if (nopeDisabled) {\n context.surface()\n .classed('nope', false)\n .classed('nope-suppressed', isInvalid);\n } else {\n context.surface()\n .classed('nope', isInvalid)\n .classed('nope-suppressed', false);\n }\n\n _lastLoc = loc;\n }\n\n\n // Uses `actionConnect.disabled()` to know whether this connection is ok..\n function hasRelationConflict(entity, target, edge, graph) {\n var testGraph = graph.update(); // copy\n\n // if snapping to way - add midpoint there and consider that the target..\n if (edge) {\n var midpoint = osmNode();\n var action = actionAddMidpoint({\n loc: edge.loc,\n edge: [target.nodes[edge.index - 1], target.nodes[edge.index]]\n }, midpoint);\n\n testGraph = action(testGraph);\n target = midpoint;\n }\n\n // can we connect to it?\n var ids = [entity.id, target.id];\n return actionConnect(ids).disabled(testGraph);\n }\n\n\n function hasInvalidGeometry(entity, graph) {\n var parents = graph.parentWays(entity);\n var i, j, k;\n\n for (i = 0; i < parents.length; i++) {\n var parent = parents[i];\n var activeIndex = null; // which multipolygon ring contains node being dragged\n\n // test any parent multipolygons for valid geometry\n var relations = graph.parentRelations(parent);\n for (j = 0; j < relations.length; j++) {\n if (!relations[j].isMultipolygon()) continue;\n\n var rings = osmJoinWays(relations[j].members, graph);\n\n // find active ring and test it for self intersections\n for (k = 0; k < rings.length; k++) {\n const nodes = rings[k].nodes;\n if (nodes.find(function(n) { return n.id === entity.id; })) {\n activeIndex = k;\n if (geoHasSelfIntersections(nodes, entity.id)) {\n return 'multipolygonMember';\n }\n }\n rings[k].coords = nodes.map(function(n) { return n.loc; });\n }\n\n // test active ring for intersections with other rings in the multipolygon\n for (k = 0; k < rings.length; k++) {\n if (k === activeIndex) continue;\n\n // make sure active ring doesn't cross passive rings\n if (geoHasLineIntersections(rings[activeIndex].nodes, rings[k].nodes, entity.id)) {\n return 'multipolygonRing';\n }\n }\n }\n\n\n // If we still haven't tested this node's parent way for self-intersections.\n // (because it's not a member of a multipolygon), test it now.\n if (activeIndex === null) {\n const nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); });\n if (nodes.length && geoHasSelfIntersections(nodes, entity.id)) {\n return parent.geometry(graph);\n }\n }\n\n }\n\n return false;\n }\n\n\n function move(d3_event, entity, point) {\n if (_isCancelled) return;\n d3_event.stopPropagation();\n\n context.surface().classed('nope-disabled', d3_event.altKey);\n\n _lastLoc = context.projection.invert(point);\n\n doMove(d3_event, entity);\n var nudge = geoViewportEdge(point, context.map().dimensions());\n if (nudge) {\n startNudge(d3_event, entity, nudge);\n } else {\n stopNudge();\n }\n }\n\n function end(d3_event, entity) {\n if (_isCancelled) return;\n\n var wasPoint = entity.geometry(context.graph()) === 'point';\n\n var d = datum(d3_event);\n var nope = (d && d.properties && d.properties.nope) || context.surface().classed('nope');\n var target = d && d.properties && d.properties.entity; // entity to snap to\n\n if (nope) { // bounce back\n context.perform(\n _actionBounceBack(entity.id, _startLoc)\n );\n\n } else if (target && target.type === 'way') {\n var choice = geoChooseEdge(context.graph().childNodes(target), context.map().mouse(), context.projection, entity.id);\n context.replace(\n actionAddMidpoint({\n loc: choice.loc,\n edge: [target.nodes[choice.index - 1], target.nodes[choice.index]]\n }, entity),\n connectAnnotation(entity, target)\n );\n\n } else if (target && target.type === 'node' && shouldSnapToNode(target)) {\n context.replace(\n actionConnect([target.id, entity.id]),\n connectAnnotation(entity, target)\n );\n\n } else if (_wasMidpoint) {\n context.replace(\n actionNoop(),\n t('operations.add.annotation.vertex')\n );\n\n } else {\n context.replace(\n actionNoop(),\n moveAnnotation(entity)\n );\n }\n\n if (wasPoint) {\n context.enter(modeSelect(context, [entity.id]));\n\n } else {\n var reselection = _restoreSelectedIDs.filter(function(id) {\n return context.graph().hasEntity(id);\n });\n\n if (reselection.length) {\n context.enter(modeSelect(context, reselection));\n } else {\n context.enter(modeBrowse(context));\n }\n }\n }\n\n\n function _actionBounceBack(nodeID, toLoc) {\n var moveNode = actionMoveNode(nodeID, toLoc);\n var action = function(graph, t) {\n // last time through, pop off the bounceback perform.\n // it will then overwrite the initial perform with a moveNode that does nothing\n if (t === 1) context.pop();\n return moveNode(graph, t);\n };\n action.transitionable = true;\n return action;\n }\n\n\n function cancel() {\n drag.cancel();\n context.enter(modeBrowse(context));\n }\n\n\n var drag = behaviorDrag()\n .selector('.layer-touch.points .target')\n .surface(context.container().select('.main-map').node())\n .origin(origin)\n .on('start', start)\n .on('move', move)\n .on('end', end);\n\n\n mode.enter = function() {\n context.install(hover);\n context.install(edit);\n\n d3_select(window)\n .on('keydown.dragNode', keydown)\n .on('keyup.dragNode', keyup);\n\n context.history()\n .on('undone.drag-node', cancel);\n };\n\n\n mode.exit = function() {\n context.ui().sidebar.hover.cancel();\n context.uninstall(hover);\n context.uninstall(edit);\n\n d3_select(window)\n .on('keydown.dragNode', null)\n .on('keyup.dragNode', null);\n\n context.history()\n .on('undone.drag-node', null);\n\n _activeEntity = null;\n\n context.surface()\n .classed('nope', false)\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false)\n .selectAll('.active')\n .classed('active', false);\n\n stopNudge();\n };\n\n\n mode.selectedIDs = function() {\n if (!arguments.length) return _activeEntity ? [_activeEntity.id] : [];\n // no assign\n return mode;\n };\n\n\n mode.activeID = function() {\n if (!arguments.length) return _activeEntity && _activeEntity.id;\n // no assign\n return mode;\n };\n\n\n mode.restoreSelectedIDs = function(_) {\n if (!arguments.length) return _restoreSelectedIDs;\n _restoreSelectedIDs = _;\n return mode;\n };\n\n\n mode.behavior = drag;\n\n\n return mode;\n}\n", "import { services } from '../services';\nimport { actionNoop } from '../actions/noop';\nimport { behaviorDrag } from '../behavior/drag';\nimport { behaviorEdit } from '../behavior/edit';\nimport { geoVecSubtract, geoViewportEdge } from '../geo';\nimport { modeSelectNote } from './select_note';\n\n\nexport function modeDragNote(context) {\n var mode = {\n id: 'drag-note',\n button: 'browse'\n };\n\n var edit = behaviorEdit(context);\n\n var _nudgeInterval;\n var _lastLoc;\n var _note; // most current note.. dragged note may have stale datum.\n\n\n function startNudge(d3_event, nudge) {\n if (_nudgeInterval) window.clearInterval(_nudgeInterval);\n _nudgeInterval = window.setInterval(function() {\n context.map().pan(nudge);\n doMove(d3_event, nudge);\n }, 50);\n }\n\n\n function stopNudge() {\n if (_nudgeInterval) {\n window.clearInterval(_nudgeInterval);\n _nudgeInterval = null;\n }\n }\n\n\n function origin(note) {\n return context.projection(note.loc);\n }\n\n\n function start(d3_event, note) {\n _note = note;\n var osm = services.osm;\n if (osm) {\n // Get latest note from cache.. The marker may have a stale datum bound to it\n // and dragging it around can sometimes delete the users note comment.\n _note = osm.getNote(_note.id);\n }\n\n context.surface().selectAll('.note-' + _note.id)\n .classed('active', true);\n\n context.perform(actionNoop());\n context.enter(mode);\n context.selectedNoteID(_note.id);\n }\n\n\n function move(d3_event, entity, point) {\n d3_event.stopPropagation();\n _lastLoc = context.projection.invert(point);\n\n doMove(d3_event);\n var nudge = geoViewportEdge(point, context.map().dimensions());\n if (nudge) {\n startNudge(d3_event, nudge);\n } else {\n stopNudge();\n }\n }\n\n\n function doMove(d3_event, nudge) {\n nudge = nudge || [0, 0];\n\n var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc);\n var currMouse = geoVecSubtract(currPoint, nudge);\n var loc = context.projection.invert(currMouse);\n\n _note = _note.move(loc);\n\n var osm = services.osm;\n if (osm) {\n osm.replaceNote(_note); // update note cache\n }\n\n context.replace(actionNoop()); // trigger redraw\n }\n\n\n function end() {\n context.replace(actionNoop()); // trigger redraw\n\n context\n .selectedNoteID(_note.id)\n .enter(modeSelectNote(context, _note.id));\n }\n\n\n var drag = behaviorDrag()\n .selector('.layer-touch.markers .target.note.new')\n .surface(context.container().select('.main-map').node())\n .origin(origin)\n .on('start', start)\n .on('move', move)\n .on('end', end);\n\n\n mode.enter = function() {\n context.install(edit);\n };\n\n\n mode.exit = function() {\n context.ui().sidebar.hover.cancel();\n context.uninstall(edit);\n\n context.surface()\n .selectAll('.active')\n .classed('active', false);\n\n stopNudge();\n };\n\n mode.behavior = drag;\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiDataHeader() {\n var _datum;\n\n\n function dataHeader(selection) {\n var header = selection.selectAll('.data-header')\n .data(\n (_datum ? [_datum] : []),\n function(d) { return d.__featurehash__; }\n );\n\n header.exit()\n .remove();\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'data-header');\n\n var iconEnter = headerEnter\n .append('div')\n .attr('class', 'data-header-icon');\n\n iconEnter\n .append('div')\n .attr('class', 'preset-icon-28')\n .call(svgIcon('#iD-icon-data', 'note-fill'));\n\n headerEnter\n .append('div')\n .attr('class', 'data-header-label')\n .call(t.append('map_data.layers.custom.title'));\n }\n\n\n dataHeader.datum = function(val) {\n if (!arguments.length) return _datum;\n _datum = val;\n return this;\n };\n\n\n return dataHeader;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { utilGetSetValue, utilRebind, utilTriggerEvent } from '../util';\n\n\n// This code assumes that the combobox values will not have duplicate entries.\n// It is keyed on the `value` of the entry. Data should be an array of objects like:\n// [{\n// value: 'string value', // required\n// display: 'label function' // optional, if present will be called with d3 selection\n// to modify/append, see localizer's t.append\n// title: 'hover text' // optional\n// terms: ['search terms'] // optional\n// }, ...]\n\nvar _comboHideTimerID;\n\nexport function uiCombobox(context, klass) {\n var dispatch = d3_dispatch('accept', 'cancel', 'update');\n var container = context.container();\n\n var _suggestions = [];\n var _data = [];\n var _fetched = {};\n var _selected = null;\n var _canAutocomplete = true;\n var _caseSensitive = false;\n var _cancelFetch = false;\n var _minItems = 2;\n var _tDown = 0;\n var _mouseEnterHandler, _mouseLeaveHandler;\n\n var _fetcher = function(val, cb) {\n cb(_data.filter(function(d) {\n var terms = d.terms || [];\n terms.push(d.value);\n if (d.key) {\n terms.push(d.key);\n }\n return terms.some(function(term) {\n return term\n .toString()\n .toLowerCase()\n .indexOf(val.toLowerCase()) !== -1;\n });\n }));\n };\n\n var combobox = function(input, attachTo) {\n if (!input || input.empty()) return;\n\n input\n .classed('combobox-input', true)\n .on('focus.combo-input', focus)\n .on('blur.combo-input', blur)\n .on('keydown.combo-input', keydown)\n .on('keyup.combo-input', keyup)\n .on('input.combo-input', change)\n .on('mousedown.combo-input', mousedown)\n .each(function() {\n var parent = this.parentNode;\n var sibling = this.nextSibling;\n\n d3_select(parent).selectAll('.combobox-caret')\n .filter(function(d) { return d === input.node(); })\n .data([input.node()])\n .enter()\n .insert('div', function() { return sibling; })\n .attr('class', 'combobox-caret')\n .on('mousedown.combo-caret', function(d3_event) {\n d3_event.preventDefault(); // don't steal focus from input\n input.node().focus(); // focus the input as if it was clicked\n mousedown(d3_event);\n })\n .on('mouseup.combo-caret', function(d3_event) {\n d3_event.preventDefault(); // don't steal focus from input\n mouseup(d3_event);\n });\n });\n\n\n function mousedown(d3_event) {\n if (d3_event.button !== 0) return; // left click only\n if (input.classed('disabled')) return;\n _tDown = +new Date();\n\n // mousedown should never bubble up (see #10481)\n d3_event.stopPropagation();\n\n // clear selection\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n if (start !== end) {\n var val = utilGetSetValue(input);\n input.node().setSelectionRange(val.length, val.length);\n return;\n }\n\n input.on('mouseup.combo-input', mouseup);\n }\n\n\n function mouseup(d3_event) {\n input.on('mouseup.combo-input', null);\n if (d3_event.button !== 0) return; // left click only\n if (input.classed('disabled')) return;\n if (input.node() !== document.activeElement) return; // exit if this input is not focused\n\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n if (start !== end) return; // exit if user is selecting\n\n // not showing or showing for a different field - try to show it.\n var combo = container.selectAll('.combobox');\n if (combo.empty() || combo.datum() !== input.node()) {\n var tOrig = _tDown;\n window.setTimeout(function() {\n if (tOrig !== _tDown) return; // exit if user double clicked\n fetchComboData('', function() {\n show();\n render();\n });\n }, 250);\n\n } else {\n hide();\n }\n }\n\n\n function focus() {\n fetchComboData(''); // prefetch values (may warm taginfo cache)\n }\n\n\n function blur() {\n _comboHideTimerID = window.setTimeout(hide, 75);\n }\n\n\n function show() {\n hide(); // remove any existing\n\n container\n .insert('div', ':first-child')\n .datum(input.node())\n .attr('class', 'combobox' + (klass ? ' combobox-' + klass : ''))\n .style('position', 'absolute')\n .style('display', 'block')\n .style('left', '0px')\n .on('mousedown.combo-container', function (d3_event) {\n // prevent moving focus out of the input field\n d3_event.preventDefault();\n });\n\n container\n .on('scroll.combo-scroll', render, true);\n }\n\n function hide() {\n _hide(container);\n }\n\n\n function keydown(d3_event) {\n var shown = !container.selectAll('.combobox').empty();\n var tagName = input.node() ? input.node().tagName.toLowerCase() : '';\n\n switch (d3_event.keyCode) {\n case 8: // \u232B Backspace\n case 46: // \u2326 Delete\n d3_event.stopPropagation();\n _selected = null;\n render();\n input.on('input.combo-input', function() {\n var start = input.property('selectionStart');\n input.node().setSelectionRange(start, start);\n input.on('input.combo-input', change); // reset event handler\n change(false);\n });\n break;\n\n case 9: // \u21E5 Tab\n accept(d3_event);\n break;\n\n case 13: // \u21A9 Return\n d3_event.preventDefault();\n d3_event.stopPropagation();\n accept(d3_event);\n break;\n\n case 38: // \u2191 Up arrow\n if (tagName === 'textarea' && !shown) return;\n d3_event.preventDefault();\n if (tagName === 'input' && !shown) {\n show();\n }\n nav(-1);\n break;\n\n case 40: // \u2193 Down arrow\n if (tagName === 'textarea' && !shown) return;\n d3_event.preventDefault();\n if (tagName === 'input' && !shown) {\n show();\n }\n nav(+1);\n break;\n }\n }\n\n\n function keyup(d3_event) {\n switch (d3_event.keyCode) {\n case 27: // \u238B Escape\n cancel();\n break;\n }\n }\n\n\n // Called whenever the input value is changed (e.g. on typing)\n function change(doAutoComplete) {\n if (doAutoComplete === undefined) doAutoComplete = true;\n fetchComboData(value(), function(skipAutosuggest) {\n _selected = null;\n var val = input.property('value');\n\n if (_suggestions.length) {\n if (doAutoComplete && !skipAutosuggest && input.property('selectionEnd') === val.length) {\n _selected = tryAutocomplete();\n }\n\n if (!_selected) {\n _selected = val;\n }\n }\n\n if (val.length) {\n var combo = container.selectAll('.combobox');\n if (combo.empty()) {\n show();\n }\n } else {\n hide();\n }\n\n render();\n });\n }\n\n\n // Called when the user presses up/down arrows to navigate the list\n function nav(dir) {\n if (_suggestions.length) {\n // try to determine previously selected index..\n var index = -1;\n for (var i = 0; i < _suggestions.length; i++) {\n if (_selected && _suggestions[i].value === _selected) {\n index = i;\n break;\n }\n }\n\n // pick new _selected\n index = Math.max(Math.min(index + dir, _suggestions.length - 1), 0);\n _selected = _suggestions[index].value;\n utilGetSetValue(input, _selected);\n dispatch.call('update');\n }\n\n render();\n ensureVisible();\n }\n\n\n function ensureVisible() {\n var combo = container.selectAll('.combobox');\n if (combo.empty()) return;\n\n var containerRect = container.node().getBoundingClientRect();\n var comboRect = combo.node().getBoundingClientRect();\n\n if (comboRect.bottom > containerRect.bottom) {\n var node = attachTo ? attachTo.node() : input.node();\n node.scrollIntoView({ behavior: 'instant', block: 'center' });\n render();\n }\n\n // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move\n var selected = combo.selectAll('.combobox-option.selected').node();\n if (selected) {\n selected.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' });\n }\n }\n\n\n function value() {\n var value = input.property('value');\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n\n if (start && end) {\n value = value.substring(0, start);\n }\n\n return value;\n }\n\n\n function fetchComboData(v, cb) {\n _cancelFetch = false;\n\n _fetcher.call(input, v, function(results, skipAutosuggest) {\n // already chose a value, don't overwrite or autocomplete it\n if (_cancelFetch) return;\n\n _suggestions = results;\n results.forEach(function(d) { _fetched[d.value] = d; });\n\n if (cb) {\n cb(skipAutosuggest);\n }\n });\n }\n\n\n function tryAutocomplete() {\n if (!_canAutocomplete) return;\n\n var val = _caseSensitive ? value() : value().toLowerCase();\n if (!val) return;\n\n // Don't autocomplete if user is typing a number - #4935\n if (isFinite(val)) return;\n\n const suggestionValues = [];\n _suggestions.forEach(s => {\n suggestionValues.push(s.value);\n if (s.key && s.key !== s.value) {\n suggestionValues.push(s.key);\n }\n });\n\n var bestIndex = -1;\n for (var i = 0; i < suggestionValues.length; i++) {\n var suggestion = suggestionValues[i];\n var compare = _caseSensitive ? suggestion : suggestion.toLowerCase();\n\n // if search string matches suggestion exactly, pick it..\n if (compare === val) {\n bestIndex = i;\n break;\n\n // otherwise lock in the first result that starts with the search string..\n } else if (bestIndex === -1 && compare.indexOf(val) === 0) {\n bestIndex = i;\n }\n }\n\n if (bestIndex !== -1) {\n var bestVal = suggestionValues[bestIndex];\n input.property('value', bestVal);\n input.node().setSelectionRange(val.length, bestVal.length);\n dispatch.call('update');\n return bestVal;\n }\n }\n\n\n function render() {\n if (_suggestions.length < _minItems || document.activeElement !== input.node()) {\n hide();\n return;\n }\n\n var shown = !container.selectAll('.combobox').empty();\n if (!shown) return;\n\n var combo = container.selectAll('.combobox');\n var options = combo.selectAll('.combobox-option')\n .data(_suggestions, function(d) { return d.value; });\n\n options.exit()\n .remove();\n\n // enter/update\n const enter = options.enter()\n .append('a')\n .attr('class', function(d) {\n return 'combobox-option ' + (d.klass || '') + (d.description ? ' has-description' : '');\n })\n .attr('title', function(d) { return d.title; });\n\n enter.each(function(d) {\n const sel = d3_select(this);\n const labelSpan = sel.append('span')\n .attr('class', 'combobox-option-label');\n if (d.display) {\n d.display(labelSpan);\n } else {\n labelSpan.text(d.value);\n }\n if (d.description) {\n sel.append('span')\n .attr('class', 'combobox-option-description')\n .text(d.description);\n }\n });\n\n enter\n .on('mouseenter', _mouseEnterHandler)\n .on('mouseleave', _mouseLeaveHandler)\n .merge(options)\n .classed('selected', function(d) { return d.value === _selected || d.key === _selected; })\n .on('click.combo-option', accept)\n .order();\n\n var node = attachTo ? attachTo.node() : input.node();\n var containerRect = container.node().getBoundingClientRect();\n var rect = node.getBoundingClientRect();\n\n combo\n .style('left', (rect.left + 5 - containerRect.left) + 'px')\n .style('width', (rect.width - 10) + 'px')\n .style('top', (rect.height + rect.top - containerRect.top) + 'px');\n }\n\n\n // Dispatches an 'accept' event\n // Then hides the combobox.\n function accept(d3_event, d) {\n _cancelFetch = true;\n var thiz = input.node();\n\n if (d) { // user clicked on a suggestion\n utilGetSetValue(input, d.value); // replace field contents\n utilTriggerEvent(input, 'change');\n }\n\n // clear (and keep) selection\n var val = utilGetSetValue(input);\n thiz.setSelectionRange(val.length, val.length);\n\n if (!d) {\n d = _fetched[val];\n }\n dispatch.call('accept', thiz, d, val);\n hide();\n }\n\n\n // Dispatches an 'cancel' event\n // Then hides the combobox.\n function cancel() {\n _cancelFetch = true;\n var thiz = input.node();\n\n // clear (and remove) selection, and replace field contents\n var val = utilGetSetValue(input);\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n val = val.slice(0, start) + val.slice(end);\n utilGetSetValue(input, val);\n thiz.setSelectionRange(val.length, val.length);\n\n dispatch.call('cancel', thiz);\n\n hide();\n }\n\n };\n\n\n combobox.canAutocomplete = function(val) {\n if (!arguments.length) return _canAutocomplete;\n _canAutocomplete = val;\n return combobox;\n };\n\n combobox.caseSensitive = function(val) {\n if (!arguments.length) return _caseSensitive;\n _caseSensitive = val;\n return combobox;\n };\n\n combobox.data = function(val) {\n if (!arguments.length) return _data;\n _data = val;\n return combobox;\n };\n\n combobox.fetcher = function(val) {\n if (!arguments.length) return _fetcher;\n _fetcher = val;\n return combobox;\n };\n\n combobox.minItems = function(val) {\n if (!arguments.length) return _minItems;\n _minItems = val;\n return combobox;\n };\n\n combobox.itemsMouseEnter = function(val) {\n if (!arguments.length) return _mouseEnterHandler;\n _mouseEnterHandler = val;\n return combobox;\n };\n\n combobox.itemsMouseLeave = function(val) {\n if (!arguments.length) return _mouseLeaveHandler;\n _mouseLeaveHandler = val;\n return combobox;\n };\n\n return utilRebind(combobox, dispatch, 'on');\n}\n\n\nfunction _hide(container) {\n if (_comboHideTimerID) {\n window.clearTimeout(_comboHideTimerID);\n _comboHideTimerID = undefined;\n }\n\n container.selectAll('.combobox')\n .remove();\n\n container\n .on('scroll.combo-scroll', null);\n}\n\n\nuiCombobox.off = function(input, context) {\n _hide(context.container());\n input\n .on('focus.combo-input', null)\n .on('blur.combo-input', null)\n .on('keydown.combo-input', null)\n .on('keyup.combo-input', null)\n .on('input.combo-input', null)\n .on('mousedown.combo-input', null)\n .on('mouseup.combo-input', null);\n\n\n context.container()\n .on('scroll.combo-scroll', null);\n};\n", "import { select as d3_select } from 'd3-selection';\n\n\n// toggles the visibility of ui elements, using a combination of the\n// hide class, which sets display=none, and a d3 transition for opacity.\n// this will cause blinking when called repeatedly, so check that the\n// value actually changes between calls.\n//\n// When the selection is a direct child of a

    element, the\n// parent's `open` property is used instead of the `hide` class.\nexport function uiToggle(show, callback) {\n return function(selection) {\n const parent = selection.node().parentNode;\n const isDetails = parent && parent.tagName === 'DETAILS';\n\n // ensure content is visible before animating\n if (isDetails) {\n if (show) parent.open = true;\n } else {\n selection.classed('hide', false);\n }\n\n selection\n .style('opacity', show ? 0 : 1)\n .transition()\n .style('opacity', show ? 1 : 0)\n .on('end', function() {\n d3_select(this).style('opacity', null);\n // hide content after fade-out completes\n if (isDetails) {\n if (!show) parent.open = false;\n } else {\n d3_select(this).classed('hide', !show);\n }\n if (callback) callback.apply(this);\n });\n };\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from '../core/preferences';\nimport { svgIcon } from '../svg/icon';\nimport { utilFunctor } from '../util';\nimport { utilRebind } from '../util/rebind';\nimport { uiToggle } from './toggle';\nimport { t, localizer } from '../core/localizer';\n\n\nexport function uiDisclosure(context, key, expandedDefault) {\n const dispatch = d3_dispatch('toggled');\n let _expanded;\n let _label = utilFunctor('');\n let _updatePreference = true;\n let _content = function () {};\n\n\n const disclosure = function(selection) {\n\n if (_expanded === undefined || _expanded === null) {\n // loading _expanded here allows it to be reset by calling `disclosure.expanded(null)`\n\n const preference = prefs('disclosure.' + key + '.expanded');\n _expanded = preference === null ? !!expandedDefault : (preference === 'true');\n }\n\n let details = selection.selectAll('.disclosure-wrap-' + key)\n .data([0]);\n\n // enter\n const detailsEnter = details.enter()\n .append('details')\n .attr('class', 'disclosure-wrap disclosure-wrap-' + key);\n\n const summaryEnter = detailsEnter\n .append('summary')\n .attr('class', 'hide-toggle hide-toggle-' + key)\n .call(svgIcon('', 'pre-text', 'hide-toggle-icon'));\n\n summaryEnter\n .append('span')\n .attr('class', 'hide-toggle-text');\n\n detailsEnter\n .append('div')\n .attr('class', 'disclosure-content');\n\n // update\n details = detailsEnter\n .merge(details);\n\n details\n .property('open', _expanded);\n\n const summary = details.selectAll('summary.hide-toggle');\n\n summary\n .on('click', toggle);\n\n updateSummary();\n\n const label = _label();\n const labelSelection = summary.selectAll('.hide-toggle-text');\n if (typeof label !== 'function') {\n labelSelection.text(label);\n } else {\n labelSelection.text('').call(label);\n }\n\n const contentWrap = details.selectAll('.disclosure-content');\n\n if (_expanded) {\n contentWrap\n .call(_content);\n }\n\n\n function updateSummary() {\n summary\n .classed('expanded', _expanded)\n .attr('title', t(`icons.${_expanded ? 'collapse' : 'expand'}`));\n\n summary.selectAll('.hide-toggle-icon')\n .attr('xlink:href', _expanded ? '#iD-icon-down'\n : (localizer.textDirection() === 'rtl') ? '#iD-icon-backward' : '#iD-icon-forward'\n );\n }\n\n\n function toggle(d3_event) {\n d3_event.preventDefault();\n\n _expanded = !_expanded;\n\n if (_updatePreference) {\n prefs('disclosure.' + key + '.expanded', _expanded);\n }\n\n updateSummary();\n\n contentWrap.call(uiToggle(_expanded));\n\n if (_expanded) {\n contentWrap.call(_content);\n }\n\n dispatch.call('toggled', this, _expanded);\n }\n };\n\n\n disclosure.label = function(val) {\n if (!arguments.length) return _label;\n _label = utilFunctor(val);\n return disclosure;\n };\n\n\n disclosure.expanded = function(val) {\n if (!arguments.length) return _expanded;\n _expanded = val;\n return disclosure;\n };\n\n\n disclosure.updatePreference = function(val) {\n if (!arguments.length) return _updatePreference;\n _updatePreference = val;\n return disclosure;\n };\n\n\n disclosure.content = function(val) {\n if (!arguments.length) return _content;\n _content = val;\n return disclosure;\n };\n\n\n return utilRebind(disclosure, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { uiDisclosure } from './disclosure';\nimport { utilFunctor } from '../util';\n\n// A unit of controls or info to be used in a layout, such as within a pane.\n// Can be labeled and collapsible.\nexport function uiSection(id, context) {\n\n var _classes = utilFunctor('');\n var _shouldDisplay;\n var _content;\n\n var _disclosure;\n var _label;\n var _expandedByDefault = utilFunctor(true);\n var _disclosureContent;\n var _disclosureExpanded;\n\n var _containerSelection = d3_select(null);\n\n var section = {\n id: id\n };\n\n section.classes = function(val) {\n if (!arguments.length) return _classes;\n _classes = utilFunctor(val);\n return section;\n };\n\n section.label = function(val) {\n if (!arguments.length) return _label;\n _label = utilFunctor(val);\n return section;\n };\n\n section.expandedByDefault = function(val) {\n if (!arguments.length) return _expandedByDefault;\n _expandedByDefault = utilFunctor(val);\n return section;\n };\n\n section.shouldDisplay = function(val) {\n if (!arguments.length) return _shouldDisplay;\n _shouldDisplay = utilFunctor(val);\n return section;\n };\n\n section.content = function(val) {\n if (!arguments.length) return _content;\n _content = val;\n return section;\n };\n\n section.disclosureContent = function(val) {\n if (!arguments.length) return _disclosureContent;\n _disclosureContent = val;\n return section;\n };\n\n section.disclosureExpanded = function(val) {\n if (!arguments.length) return _disclosureExpanded;\n _disclosureExpanded = val;\n return section;\n };\n\n // may be called multiple times\n section.render = function(selection) {\n\n _containerSelection = selection\n .selectAll('.section-' + id)\n .data([0]);\n\n var sectionEnter = _containerSelection\n .enter()\n .append('div')\n .attr('class', 'section section-' + id + ' ' + (_classes && _classes() || ''));\n\n _containerSelection = sectionEnter\n .merge(_containerSelection);\n\n _containerSelection\n .call(renderContent);\n };\n\n section.reRender = function() {\n _containerSelection\n .call(renderContent);\n };\n\n section.selection = function() {\n return _containerSelection;\n };\n\n section.disclosure = function() {\n return _disclosure;\n };\n\n // may be called multiple times\n function renderContent(selection) {\n if (_shouldDisplay) {\n var shouldDisplay = _shouldDisplay();\n selection.classed('hide', !shouldDisplay);\n if (!shouldDisplay) {\n selection.html('');\n return;\n }\n }\n\n if (_disclosureContent) {\n if (!_disclosure) {\n _disclosure = uiDisclosure(context, id.replace(/-/g, '_'), _expandedByDefault())\n .label(_label || '')\n /*.on('toggled', function(expanded) {\n if (expanded) { selection.node().parentNode.scrollTop += 200; }\n })*/\n .content(_disclosureContent);\n }\n if (_disclosureExpanded !== undefined) {\n _disclosure.expanded(_disclosureExpanded);\n _disclosureExpanded = undefined;\n }\n selection\n .call(_disclosure);\n\n return;\n }\n\n if (_content) {\n selection\n .call(_content);\n }\n }\n\n return section;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\n\n\n// Pass `what` object of the form:\n// {\n// key: 'string', // required\n// value: 'string' // optional\n// }\n// -or-\n// {\n// qid: 'string' // brand wikidata (e.g. 'Q37158')\n// }\n//\nexport function uiTagReference(what) {\n var wikibase = what.qid ? services.wikidata : services.osmWikibase;\n var tagReference = {};\n\n var _button = d3_select(null);\n var _body = d3_select(null);\n var _loaded;\n var _showing;\n\n\n function load() {\n if (!wikibase) return;\n\n _button\n .classed('tag-reference-loading', true);\n\n wikibase.getDocs(what, gotDocs);\n }\n\n\n function gotDocs(err, docs) {\n _body.html('');\n\n if (!docs || !docs.title) {\n _body\n .append('p')\n .attr('class', 'tag-reference-description')\n .call(t.append('inspector.no_documentation_key'));\n done();\n return;\n }\n\n if (docs.imageURL) {\n _body\n .append('img')\n .attr('class', 'tag-reference-wiki-image')\n .attr('alt', docs.title)\n .attr('src', docs.imageURL)\n .on('load', function() { done(); })\n .on('error', function() { d3_select(this).remove(); done(); });\n } else {\n done();\n }\n\n var tagReferenceDescription = _body\n .append('p')\n .attr('class', 'tag-reference-description')\n .append('span');\n if (docs.description) {\n tagReferenceDescription = tagReferenceDescription\n .attr('class', 'localized-text')\n .attr('lang', docs.descriptionLocaleCode || 'und')\n .call(docs.description);\n } else {\n tagReferenceDescription = tagReferenceDescription\n .call(t.append('inspector.no_documentation_key'));\n }\n tagReferenceDescription\n .append('a')\n .attr('class', 'tag-reference-edit')\n .attr('target', '_blank')\n .attr('title', t('inspector.edit_reference'))\n .attr('href', docs.editURL)\n .call(svgIcon('#iD-icon-edit', 'inline'));\n\n if (docs.wiki) {\n _body\n .append('a')\n .attr('class', 'tag-reference-link')\n .attr('target', '_blank')\n .attr('href', docs.wiki.url)\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append(docs.wiki.text));\n }\n\n // Add link to info about \"good changeset comments\" - #2923\n if (what.key === 'comment') {\n _body\n .append('a')\n .attr('class', 'tag-reference-comment-link')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', t('commit.about_changeset_comments_link'))\n .append('span')\n .call(t.append('commit.about_changeset_comments'));\n }\n }\n\n\n function done() {\n _loaded = true;\n\n _button\n .classed('tag-reference-loading', false);\n\n _body\n .classed('expanded', true)\n .transition()\n .duration(200)\n .style('max-height', '200px')\n .style('opacity', '1');\n\n _showing = true;\n\n _button.selectAll('svg.icon use').each(function() {\n var iconUse = d3_select(this);\n if (iconUse.attr('href') === '#iD-icon-info') {\n iconUse.attr('href', '#iD-icon-info-filled');\n }\n });\n }\n\n\n function hide() {\n _body\n .transition()\n .duration(200)\n .style('max-height', '0px')\n .style('opacity', '0')\n .on('end', function () {\n _body.classed('expanded', false);\n });\n\n _showing = false;\n\n _button.selectAll('svg.icon use').each(function() {\n var iconUse = d3_select(this);\n if (iconUse.attr('href') === '#iD-icon-info-filled') {\n iconUse.attr('href', '#iD-icon-info');\n }\n });\n\n }\n\n\n tagReference.button = function(selection, klass, iconName) {\n _button = selection.selectAll('.tag-reference-button')\n .data([0]);\n\n _button = _button.enter()\n .append('button')\n .attr('class', 'tag-reference-button ' + (klass || ''))\n .attr('title', t('icons.information'))\n .call(svgIcon('#iD-icon-' + (iconName || 'inspect')))\n .merge(_button);\n\n _button\n .on('click', function (d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n this.blur(); // avoid keeping focus on the button - #4641\n if (_showing) {\n hide();\n } else if (_loaded) {\n done();\n } else {\n load();\n }\n });\n };\n\n\n tagReference.body = function(selection) {\n var itemID = what.qid || (what.key + '-' + (what.value || ''));\n _body = selection.selectAll('.tag-reference-body')\n .data([itemID], function(d) { return d; });\n\n _body.exit()\n .remove();\n\n _body = _body.enter()\n .append('div')\n .attr('class', 'tag-reference-body')\n .style('max-height', '0')\n .style('opacity', '0')\n .merge(_body);\n\n if (_showing === false) {\n hide();\n }\n };\n\n\n tagReference.showing = function(val) {\n if (!arguments.length) return _showing;\n _showing = val;\n return tagReference;\n };\n\n\n return tagReference;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { isEmpty } from 'es-toolkit/compat';\n\nimport { services } from '../../services';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCombobox } from '../combobox';\nimport { uiSection } from '../section';\nimport { uiTagReference } from '../tag_reference';\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { utilArrayDifference, utilArrayIdentical } from '../../util/array';\nimport { utilGetSetValue, utilNoAuto, utilRebind, utilTagDiff } from '../../util';\nimport { allowUpperCaseTagValues } from '../../osm/tags';\nimport { fileFetcher } from '../../core';\n\n\nexport function uiSectionRawTagEditor(id, context) {\n\n var section = uiSection(id, context)\n .classes('raw-tag-editor')\n .label(function() {\n var count = Object.keys(_tags).filter(function(d) { return d; }).length;\n return t.append('inspector.title_count', { title: t.append('inspector.tags'), count: count });\n })\n .expandedByDefault(false)\n .disclosureContent(renderDisclosureContent);\n\n var taginfo = services.taginfo;\n var dispatch = d3_dispatch('change');\n var availableViews = [\n { id: 'list', icon: '#fas-th-list' },\n { id: 'text', icon: '#fas-i-cursor' }\n ];\n\n let _discardTags = {};\n fileFetcher.get('discarded')\n .then((d) => { _discardTags = d; })\n .catch(() => { /* ignore */ });\n\n var _tagView = (prefs('raw-tag-editor-view') || 'list'); // 'list, 'text'\n var _readOnlyTags = [];\n // the keys in the order we want them to display\n var _orderedKeys = [];\n var _pendingChange = null;\n var _state;\n var _presets;\n var _tags;\n var _entityIDs;\n var _didInteract = false;\n\n function interacted() {\n _didInteract = true;\n }\n\n function renderDisclosureContent(wrap) {\n\n // remove deleted keys\n _orderedKeys = _orderedKeys.filter(function(key) {\n return _tags[key] !== undefined;\n });\n\n // When switching to a different entity or changing the state (hover/select)\n // reorder the keys alphabetically.\n // We trigger this by emptying the `_orderedKeys` array, then it will be rebuilt here.\n // Otherwise leave their order alone - #5857, #5927\n var all = Object.keys(_tags).sort();\n var missingKeys = utilArrayDifference(all, _orderedKeys);\n for (var i in missingKeys) {\n _orderedKeys.push(missingKeys[i]);\n }\n\n // assemble row data\n var rowData = _orderedKeys.map(function(key, i) {\n return { index: i, key: key, value: _tags[key] };\n });\n\n // append blank row last\n rowData.push({ index: rowData.length, key: '', value: '' });\n\n\n // View Options\n var options = wrap.selectAll('.raw-tag-options')\n .data([0]);\n\n options.exit()\n .remove();\n\n var optionsEnter = options.enter()\n .insert('div', ':first-child')\n .attr('class', 'raw-tag-options')\n .attr('role', 'tablist');\n\n var optionEnter = optionsEnter.selectAll('.raw-tag-option')\n .data(availableViews, function(d) { return d.id; })\n .enter();\n\n optionEnter\n .append('button')\n .attr('class', function(d) {\n return 'raw-tag-option raw-tag-option-' + d.id + (_tagView === d.id ? ' selected' : '');\n })\n .attr('aria-selected', function(d) { return _tagView === d.id; })\n .attr('role', 'tab')\n .attr('title', function(d) { return t('icons.' + d.id); })\n .on('click', function(d3_event, d) {\n _tagView = d.id;\n prefs('raw-tag-editor-view', d.id);\n\n wrap.selectAll('.raw-tag-option')\n .classed('selected', function(datum) { return datum === d; })\n .attr('aria-selected', function(datum) { return datum === d; });\n\n wrap.selectAll('.tag-text')\n .classed('hide', (d.id !== 'text'))\n .each(setTextareaHeight);\n\n wrap.selectAll('.tag-list, .add-row')\n .classed('hide', (d.id !== 'list'));\n })\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(d.icon));\n });\n\n\n // View as Text\n var textData = rowsToText(rowData);\n var textarea = wrap.selectAll('.tag-text')\n .data([0]);\n\n textarea = textarea.enter()\n .append('textarea')\n .attr('class', 'tag-text' + (_tagView !== 'text' ? ' hide' : ''))\n .call(utilNoAuto)\n .attr('placeholder', t('inspector.key_value'))\n .attr('spellcheck', 'false')\n .style('direction', 'ltr')\n .merge(textarea);\n\n textarea\n .call(utilGetSetValue, textData)\n .each(setTextareaHeight)\n .on('input', setTextareaHeight)\n .on('focus', interacted)\n .on('blur', textChanged)\n .on('change', textChanged);\n\n\n // View as List\n var list = wrap.selectAll('.tag-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'tag-list' + (_tagView !== 'list' ? ' hide' : ''))\n .merge(list);\n\n\n // Tag list items\n var items = list.selectAll('.tag-row')\n .data(rowData, d => d.key);\n\n items.exit()\n .each(unbind)\n .remove();\n\n\n // Enter\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', 'tag-row')\n .classed('readonly', isReadOnly);\n\n var innerWrap = itemsEnter.append('div')\n .attr('class', 'inner-wrap');\n\n innerWrap\n .append('div')\n .attr('class', 'key-wrap')\n .append('input')\n .property('type', 'text')\n .attr('class', 'key')\n .call(utilNoAuto)\n .on('focus', interacted)\n .on('blur', keyChange)\n .on('change', keyChange);\n\n innerWrap\n .append('div')\n .attr('class', 'value-wrap')\n .append('input')\n .property('type', 'text')\n .attr('dir', 'auto')\n .attr('class', 'value')\n .call(utilNoAuto)\n .on('focus', interacted)\n .on('blur', valueChange)\n .on('change', valueChange);\n\n innerWrap\n .append('button')\n .attr('tabindex', -1)\n .attr('class', 'form-field-button remove')\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'));\n\n\n // Update\n items = items\n .merge(itemsEnter)\n .sort(function(a, b) { return a.index - b.index; });\n\n items\n .classed('add-tag', d => d.key === '')\n .each(function(d) {\n var row = d3_select(this);\n var key = row.select('input.key'); // propagate bound data\n var value = row.select('input.value'); // propagate bound data\n\n if (_entityIDs && taginfo && _state !== 'hover') {\n bindTypeahead(key, value);\n }\n\n var referenceOptions = { key: d.key };\n if (typeof d.value === 'string') {\n referenceOptions.value = d.value;\n }\n var reference = uiTagReference(referenceOptions, context);\n\n if (_state === 'hover') {\n reference.showing(false);\n }\n\n row.select('.inner-wrap') // propagate bound data\n .call(reference.button)\n .select('.tag-reference-button')\n .attr('tabindex', -1)\n .classed('disabled', d => d.key === '')\n .attr('disabled', d => d.key === '' ? 'disabled' : null);\n\n row.call(reference.body);\n\n row.select('button.remove'); // propagate bound data\n });\n\n items.selectAll('input.key')\n .attr('title', function(d) { return d.key; })\n .attr('placeholder', function(d) {\n return d.key === '' ? t('inspector.add_tag') : null;\n })\n .attr('readonly', function(d) {\n return isReadOnly(d) || null;\n })\n .call(utilGetSetValue,\n d => d.key,\n (_, newKey) => _pendingChange === null || isEmpty(_pendingChange) || _pendingChange[newKey] // if there are pending changes: skip untouched tags\n );\n\n items.selectAll('input.value')\n .attr('title', function(d) {\n return Array.isArray(d.value) ? d.value.filter(Boolean).join('\\n') : d.value;\n })\n .classed('mixed', function(d) {\n return Array.isArray(d.value);\n })\n .attr('placeholder', function(d) {\n return Array.isArray(d.value) ? t('inspector.multiple_values') : null;\n })\n .attr('readonly', function(d) {\n return isReadOnly(d) || null;\n })\n .call(utilGetSetValue, d => {\n if (_pendingChange !== null && !isEmpty(_pendingChange) && !_pendingChange[d.key]) {\n // if there are pending changes: skip untouched tags\n return null;\n }\n return Array.isArray(d.value) ? '' : d.value;\n });\n\n items.selectAll('button.remove')\n .classed('disabled', d => d.key === '')\n .on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'down', // 'click' fires too late - #5878\n (d3_event, d) => {\n if (d3_event.button !== 0) return;\n removeTag(d3_event, d);\n });\n\n }\n\n function isReadOnly(d) {\n for (var i = 0; i < _readOnlyTags.length; i++) {\n if (d.key.match(_readOnlyTags[i]) !== null) {\n return true;\n }\n }\n return false;\n }\n\n function setTextareaHeight() {\n if (_tagView !== 'text') return;\n\n var selection = d3_select(this);\n var matches = selection.node().value.match(/\\n/g);\n var lineCount = 2 + Number(matches && matches.length);\n var lineHeight = 20;\n\n selection.style('height', lineCount * lineHeight + 'px');\n }\n\n function stringify(s) {\n const stringified = JSON.stringify(s).slice(1, -1); // without leading/trailing \"\n if (stringified !== s) {\n return `\"${stringified}\"`;\n } else {\n return s;\n }\n }\n\n function unstringify(s) {\n const isQuoted = s.length > 1 && s.charAt(0) === '\"' && s.charAt(s.length - 1) === '\"';\n if (isQuoted) {\n try {\n return JSON.parse(s);\n } catch {\n return s;\n }\n } else {\n return s;\n }\n }\n\n function rowsToText(rows) {\n var str = rows\n .filter(function(row) { return row.key && row.key.trim() !== ''; })\n .map(function(row) {\n var rawVal = row.value;\n if (Array.isArray(rawVal)) rawVal = '*';\n var val = rawVal ? stringify(rawVal) : '';\n return stringify(row.key) + '=' + val;\n })\n .join('\\n');\n\n if (_state !== 'hover' && str.length) {\n return str + '\\n';\n }\n return str;\n }\n\n function textChanged() {\n var newText = this.value.trim();\n var newTags = {};\n newText.split('\\n').forEach(function(row) {\n var m = row.match(/^\\s*([^=]+)=(.*)$/);\n if (m !== null) {\n var k = context.cleanTagKey(unstringify(m[1].trim()));\n var v = context.cleanTagValue(unstringify(m[2].trim()));\n newTags[k] = v;\n }\n });\n\n var tagDiff = utilTagDiff(_tags, newTags);\n\n _pendingChange = _pendingChange || {};\n\n tagDiff.forEach(function(change) {\n if (isReadOnly({ key: change.key })) return;\n\n // skip unchanged multiselection placeholders\n if (change.newVal === '*' && Array.isArray(change.oldVal)) return;\n\n if (change.type === '-') {\n _pendingChange[change.key] = undefined;\n } else if (change.type === '+') {\n _pendingChange[change.key] = change.newVal || '';\n }\n });\n\n if (isEmpty(_pendingChange)) {\n _pendingChange = null;\n section.reRender();\n return;\n }\n\n scheduleChange();\n }\n\n function bindTypeahead(key, value) {\n if (isReadOnly(key.datum())) return;\n\n if (Array.isArray(value.datum().value)) {\n value.call(uiCombobox(context, 'tag-value')\n .minItems(1)\n .fetcher(function(value, callback) {\n var keyString = utilGetSetValue(key);\n if (!_tags[keyString]) return;\n var data = _tags[keyString].map(function(tagValue) {\n if (!tagValue) {\n return {\n value: ' ',\n title: t('inspector.empty'),\n display: selection => selection.text('')\n .classed('virtual-option', true)\n .call(t.append('inspector.empty'))\n };\n }\n return {\n value: tagValue,\n title: tagValue\n };\n });\n callback(data);\n }));\n return;\n }\n\n var geometry = context.graph().geometry(_entityIDs[0]);\n\n key.call(uiCombobox(context, 'tag-key')\n .fetcher(function(value, callback) {\n taginfo.keys({\n debounce: true,\n geometry: geometry,\n query: value\n }, function(err, data) {\n if (!err) {\n const filtered = data\n .filter(d => _tags[d.value] === undefined) // already used tag\n .filter(d => !(d.value in _discardTags)) // do not suggest discardable tags (see #9817)\n .filter(d => !/_\\d$/.test(d.value)) // tag like name_1 (see #9422)\n .filter(d => d.value.toLowerCase().includes(value.toLowerCase())); // tag does not match user input\n callback(sort(value, filtered));\n }\n });\n }));\n\n value.call(uiCombobox(context, 'tag-value')\n .fetcher(function(value, callback) {\n taginfo.values({\n debounce: true,\n key: utilGetSetValue(key),\n geometry: geometry,\n query: value\n }, function(err, data) {\n if (!err) {\n const filtered = data.filter(d => d.value.toLowerCase().includes(value.toLowerCase()));\n callback(sort(value, filtered));\n }\n });\n })\n .caseSensitive(allowUpperCaseTagValues.test(utilGetSetValue(key))));\n\n\n function sort(value, data) {\n var sameletter = [];\n var other = [];\n for (var i = 0; i < data.length; i++) {\n if (data[i].value.substring(0, value.length) === value) {\n sameletter.push(data[i]);\n } else {\n other.push(data[i]);\n }\n }\n return sameletter.concat(other);\n }\n }\n\n function unbind() {\n var row = d3_select(this);\n\n row.selectAll('input.key')\n .call(uiCombobox.off, context);\n\n row.selectAll('input.value')\n .call(uiCombobox.off, context);\n }\n\n function keyChange(d3_event, d) {\n const input = d3_select(this);\n if (input.attr('readonly')) return;\n\n var kOld = d.key;\n\n // exit if we are currently about to delete this row anyway - #6366\n if (_pendingChange && _pendingChange.hasOwnProperty(kOld) && _pendingChange[kOld] === undefined) return;\n\n var kNew = context.cleanTagKey(this.value.trim());\n\n // allow no change if the key should be readonly\n if (isReadOnly({ key: kNew })) {\n this.value = kOld;\n return;\n }\n\n if (kNew !== this.value) {\n utilGetSetValue(input, kNew);\n }\n\n if (kNew &&\n kNew !== kOld &&\n _tags[kNew] !== undefined) {\n // new key is already in use, switch focus to the existing row\n\n this.value = kOld; // reset the key\n section.selection().selectAll('.tag-list input.value')\n .each(function(d) {\n if (d.key === kNew) { // send focus to that other value combo instead\n var input = d3_select(this).node();\n input.focus();\n input.select();\n }\n });\n return;\n }\n\n\n _pendingChange = _pendingChange || {};\n\n if (kOld) {\n if (kOld === kNew) return;\n // a tag key was renamed\n _pendingChange[kNew] = _pendingChange[kOld] || { oldKey: kOld };\n _pendingChange[kOld] = undefined;\n } else {\n // a new tag was added\n let row = this.parentNode.parentNode;\n let inputVal = d3_select(row).selectAll('input.value');\n let vNew = context.cleanTagValue(utilGetSetValue(inputVal));\n _pendingChange[kNew] = vNew;\n utilGetSetValue(inputVal, vNew);\n }\n\n // update the ordered key index so this row doesn't change position\n var existingKeyIndex = _orderedKeys.indexOf(kOld);\n if (existingKeyIndex !== -1) _orderedKeys[existingKeyIndex] = kNew;\n\n d.key = kNew; // update datum to avoid exit/enter on tag update\n\n this.value = kNew;\n scheduleChange();\n }\n\n function valueChange(d3_event, d) {\n if (isReadOnly(d)) return;\n\n // exit if this is a multiselection and no value was entered\n if (Array.isArray(d.value) && !this.value) return;\n\n // exit if we are currently about to delete this row anyway - #6366\n if (_pendingChange && _pendingChange.hasOwnProperty(d.key) && _pendingChange[d.key] === undefined) return;\n\n _pendingChange = _pendingChange || {};\n\n const vNew = context.cleanTagValue(this.value);\n if (vNew !== this.value) {\n utilGetSetValue(d3_select(this), vNew);\n }\n\n _pendingChange[d.key] = vNew;\n scheduleChange();\n }\n\n function removeTag(d3_event, d) {\n if (isReadOnly(d)) return;\n\n // remove the key from the ordered key index\n _orderedKeys = _orderedKeys.filter(function(key) { return key !== d.key; });\n\n _pendingChange = _pendingChange || {};\n _pendingChange[d.key] = undefined;\n scheduleChange();\n }\n\n function scheduleChange() {\n if (!_pendingChange) return;\n\n for (const key in _pendingChange) {\n _tags[key] = _pendingChange[key];\n }\n dispatch.call('change', this, _entityIDs, _pendingChange);\n _pendingChange = null;\n }\n\n\n section.state = function(val) {\n if (!arguments.length) return _state;\n if (_state !== val) {\n _orderedKeys = [];\n _state = val;\n }\n return section;\n };\n\n\n section.presets = function(val) {\n if (!arguments.length) return _presets;\n _presets = val;\n if (_presets && _presets.length && _presets[0].isFallback()) {\n section.disclosureExpanded(true);\n\n // don't collapse the disclosure if the mapper used the raw tag editor - #1881\n } else if (!_didInteract) {\n section.disclosureExpanded(null);\n }\n return section;\n };\n\n\n section.tags = function(val) {\n if (!arguments.length) return _tags;\n _tags = val;\n return section;\n };\n\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n if (!_entityIDs || !val || !utilArrayIdentical(_entityIDs, val)) {\n _entityIDs = val;\n _orderedKeys = [];\n }\n return section;\n };\n\n\n // pass an array of regular expressions to test against the tag key\n section.readOnlyTags = function(val) {\n if (!arguments.length) return _readOnlyTags;\n _readOnlyTags = val;\n return section;\n };\n\n\n return utilRebind(section, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\nimport { stringifyProperties } from '../util/object';\nimport { uiDataHeader } from './data_header';\nimport { uiSectionRawTagEditor } from './sections/raw_tag_editor';\n\n\nexport function uiDataEditor(context) {\n var dataHeader = uiDataHeader();\n var rawTagEditor = uiSectionRawTagEditor('custom-data-tag-editor', context)\n .expandedByDefault(true)\n .readOnlyTags([/.*/]);\n var _datum;\n\n\n function dataEditor(selection) {\n\n var header = selection.selectAll('.header')\n .data([0]);\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function() {\n context.enter(modeBrowse(context));\n })\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('map_data.title'));\n\n\n var body = selection.selectAll('.body')\n .data([0]);\n\n body = body.enter()\n .append('div')\n .attr('class', 'body')\n .merge(body);\n\n var editor = body.selectAll('.data-editor')\n .data([0]);\n\n // enter/update\n editor.enter()\n .append('div')\n .attr('class', 'modal-section data-editor')\n .merge(editor)\n .call(dataHeader.datum(_datum));\n\n var rte = body.selectAll('.raw-tag-editor')\n .data([0]);\n\n // enter/update\n rte.enter()\n .append('div')\n .attr('class', 'raw-tag-editor data-editor')\n .merge(rte)\n .call(rawTagEditor\n .tags(stringifyProperties(_datum?.properties || {}))\n .state('hover')\n .render\n )\n .selectAll('textarea.tag-text')\n .attr('readonly', true)\n .classed('readonly', true);\n }\n\n\n dataEditor.datum = function(val) {\n if (!arguments.length) return _datum;\n _datum = val;\n return this;\n };\n\n\n return dataEditor;\n}\n", "import { geoBounds as d3_geoBounds } from 'd3-geo';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { behaviorBreathe } from '../behavior/breathe';\nimport { behaviorHover } from '../behavior/hover';\nimport { behaviorLasso } from '../behavior/lasso';\nimport { behaviorSelect } from '../behavior/select';\n\nimport { t } from '../core/localizer';\n\nimport { geoExtent } from '../geo';\nimport { modeBrowse } from './browse';\nimport { modeDragNode } from './drag_node';\nimport { modeDragNote } from './drag_note';\nimport { uiDataEditor } from '../ui/data_editor';\nimport { utilKeybinding } from '../util';\n\n\nexport function modeSelectData(context, selectedDatum) {\n var mode = {\n id: 'select-data',\n button: 'browse'\n };\n\n var keybinding = utilKeybinding('select-data');\n var dataEditor = uiDataEditor(context);\n\n var behaviors = [\n behaviorBreathe(context),\n behaviorHover(context),\n behaviorSelect(context),\n behaviorLasso(context),\n modeDragNode(context).behavior,\n modeDragNote(context).behavior\n ];\n\n\n // class the data as selected, or return to browse mode if the data is gone\n function selectData(d3_event, drawn) {\n var selection = context.surface().selectAll('.layer-mapdata .data' + selectedDatum.__featurehash__);\n\n if (selection.empty()) {\n // Return to browse mode if selected DOM elements have\n // disappeared because the user moved them out of view..\n var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;\n if (drawn && source && (source.type === 'pointermove' || source.type === 'mousemove' || source.type === 'touchmove')) {\n context.enter(modeBrowse(context));\n }\n } else {\n selection.classed('selected', true);\n }\n }\n\n\n function esc() {\n if (context.container().select('.combobox').size()) return;\n context.enter(modeBrowse(context));\n }\n\n\n mode.zoomToSelected = function() {\n var extent = geoExtent(d3_geoBounds(selectedDatum));\n context.map().centerZoomEase(extent.center(), context.map().trimmedExtentZoom(extent));\n };\n\n\n mode.enter = function() {\n behaviors.forEach(context.install);\n\n keybinding\n .on(t('inspector.zoom_to.key'), mode.zoomToSelected)\n .on('\u238B', esc, true);\n\n d3_select(document)\n .call(keybinding);\n\n selectData();\n\n var sidebar = context.ui().sidebar;\n sidebar.show(dataEditor.datum(selectedDatum));\n\n // expand the sidebar, avoid obscuring the data if needed\n var extent = geoExtent(d3_geoBounds(selectedDatum));\n sidebar.expand(sidebar.intersects(extent));\n\n context.map()\n .on('drawn.select-data', selectData);\n };\n\n\n mode.exit = function() {\n behaviors.forEach(context.uninstall);\n\n d3_select(document)\n .call(keybinding.unbind);\n\n context.surface()\n .selectAll('.layer-mapdata .selected')\n .classed('selected hover', false);\n\n context.map()\n .on('drawn.select-data', null);\n\n context.ui().sidebar\n .hide();\n };\n\n\n return mode;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { modeSelect } from '../modes/select';\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { utilDisplayName, utilHighlightEntities } from '../util';\n\n\nexport function uiOsmoseDetails(context) {\n let _qaItem;\n\n function issueString(d, type) {\n if (!d) return '';\n\n // Issue strings are cached from Osmose API\n const s = services.osmose.getStrings(d.itemType);\n return (type in s) ? s[type] : '';\n }\n\n\n function osmoseDetails(selection) {\n const details = selection.selectAll('.error-details')\n .data(\n _qaItem ? [_qaItem] : [],\n d => `${d.id}-${d.status || 0}`\n );\n\n details.exit()\n .remove();\n\n const detailsEnter = details.enter()\n .append('div')\n .attr('class', 'error-details qa-details-container');\n\n\n // Description\n if (issueString(_qaItem, 'detail')) {\n const div = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n div\n .append('h4')\n .call(t.append('QA.keepRight.detail_description'));\n\n div\n .append('p')\n .attr('class', 'qa-details-description-text')\n .html(d => issueString(d, 'detail'))\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Elements (populated later as data is requested)\n const detailsDiv = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n const elemsDiv = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n // Suggested Fix (mustn't exist for every issue type)\n if (issueString(_qaItem, 'fix')) {\n const div = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n div\n .append('h4')\n .call(t.append('QA.osmose.fix_title'));\n\n div\n .append('p')\n .html(d => issueString(d, 'fix'))\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Common Pitfalls (mustn't exist for every issue type)\n if (issueString(_qaItem, 'trap')) {\n const div = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n div\n .append('h4')\n .call(t.append('QA.osmose.trap_title'));\n\n div\n .append('p')\n .html(d => issueString(d, 'trap'))\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Save current item to check if UI changed by time request resolves\n const thisItem = _qaItem;\n services.osmose.loadIssueDetail(_qaItem)\n .then(d => {\n // No details to add if there are no associated issue elements\n if (!d.elems || d.elems.length === 0) return;\n\n // Do nothing if UI has moved on by the time this resolves\n if (\n context.selectedErrorID() !== thisItem.id\n && context.container().selectAll(`.qaItem.osmose.hover.itemId-${thisItem.id}`).empty()\n ) return;\n\n // Things like keys and values are dynamically added to a subtitle string\n if (d.detail) {\n detailsDiv\n .append('h4')\n .call(t.append('QA.osmose.detail_title'));\n\n detailsDiv\n .append('p')\n .html(d => d.detail)\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Create list of linked issue elements\n elemsDiv\n .append('h4')\n .call(t.append('QA.osmose.elems_title'));\n\n elemsDiv\n .append('ul').selectAll('li')\n .data(d.elems)\n .enter()\n .append('li')\n .append('a')\n .attr('href', '#')\n .attr('class', 'error_entity_link')\n .text(d => d)\n .each(function() {\n const link = d3_select(this);\n const entityID = this.textContent;\n const entity = context.hasEntity(entityID);\n\n // Add click handler\n link\n .on('mouseenter', () => {\n utilHighlightEntities([entityID], true, context);\n })\n .on('mouseleave', () => {\n utilHighlightEntities([entityID], false, context);\n })\n .on('click', (d3_event) => {\n d3_event.preventDefault();\n\n utilHighlightEntities([entityID], false, context);\n\n const osmlayer = context.layers().layer('osm');\n if (!osmlayer.enabled()) {\n osmlayer.enabled(true);\n }\n\n context.map().centerZoom(d.loc, 20);\n\n if (entity) {\n context.enter(modeSelect(context, [entityID]));\n } else {\n context.loadEntity(entityID, (err, result) => {\n if (err) return;\n const entity = result.data.find(e => e.id === entityID);\n if (entity) context.enter(modeSelect(context, [entityID]));\n });\n }\n });\n\n // Replace with friendly name if possible\n // (The entity may not yet be loaded into the graph)\n if (entity) {\n let name = utilDisplayName(entity); // try to use common name\n\n if (!name) {\n const preset = presetManager.match(entity, context.graph());\n name = preset && !preset.isFallback() && preset.name(); // fallback to preset name\n }\n\n if (name) {\n this.innerText = name;\n }\n }\n });\n\n // Don't hide entities related to this issue - #5880\n context.features().forceVisible(d.elems);\n context.map().pan([0,0]); // trigger a redraw\n })\n .catch(err => {\n console.log(err); // eslint-disable-line no-console\n });\n }\n\n\n osmoseDetails.issue = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return osmoseDetails;\n };\n\n\n return osmoseDetails;\n}\n", "import { services } from '../services';\nimport { t } from '../core/localizer';\n\n\nexport function uiOsmoseHeader() {\n let _qaItem;\n\n function issueTitle(d) {\n const unknown = t('inspector.unknown');\n\n if (!d) return unknown;\n\n // Issue titles supplied by Osmose\n const s = services.osmose.getStrings(d.itemType);\n return ('title' in s) ? s.title : unknown;\n }\n\n function osmoseHeader(selection) {\n const header = selection.selectAll('.qa-header')\n .data(\n (_qaItem ? [_qaItem] : []),\n d => `${d.id}-${d.status || 0}`\n );\n\n header.exit()\n .remove();\n\n const headerEnter = header.enter()\n .append('div')\n .attr('class', 'qa-header');\n\n const svgEnter = headerEnter\n .append('div')\n .attr('class', 'qa-header-icon')\n .classed('new', d => d.id < 0)\n .append('svg')\n .attr('width', '20px')\n .attr('height', '30px')\n .attr('viewbox', '0 0 20 30')\n .attr('class', d => `preset-icon-28 qaItem ${d.service} itemId-${d.id} itemType-${d.itemType}`);\n\n svgEnter\n .append('polygon')\n .attr('fill', d => services.osmose.getColor(d.item))\n .attr('class', 'qaItem-fill')\n .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');\n\n svgEnter\n .append('use')\n .attr('class', 'icon-annotation')\n .attr('width', '12px')\n .attr('height', '12px')\n .attr('transform', 'translate(4, 5.5)')\n .attr('xlink:href', d => d.icon ? '#' + d.icon : '');\n\n headerEnter\n .append('div')\n .attr('class', 'qa-header-label')\n .text(issueTitle);\n }\n\n osmoseHeader.issue = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return osmoseHeader;\n };\n\n return osmoseHeader;\n}\n", "import { t } from '../core/localizer';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\nimport { QAItem } from '../osm';\n\nexport function uiViewOnOsmose() {\n let _qaItem;\n\n function viewOnOsmose(selection) {\n let url;\n if (services.osmose && (_qaItem instanceof QAItem)) {\n url = services.osmose.itemURL(_qaItem);\n }\n\n const link = selection.selectAll('.view-on-osmose')\n .data(url ? [url] : []);\n\n // exit\n link.exit()\n .remove();\n\n // enter\n const linkEnter = link.enter()\n .append('a')\n .attr('class', 'view-on-osmose')\n .attr('target', '_blank')\n .attr('rel', 'noopener') // security measure\n .attr('href', d => d)\n .call(svgIcon('#iD-icon-out-link', 'inline'));\n\n linkEnter\n .append('span')\n .call(t.append('inspector.view_on_osmose'));\n }\n\n viewOnOsmose.what = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return viewOnOsmose;\n };\n\n return viewOnOsmose;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\n\nimport { uiOsmoseDetails } from './osmose_details';\nimport { uiOsmoseHeader } from './osmose_header';\nimport { uiViewOnOsmose } from './view_on_osmose';\n\nimport { utilRebind } from '../util';\n\nexport function uiOsmoseEditor(context) {\n const dispatch = d3_dispatch('change');\n const qaDetails = uiOsmoseDetails(context);\n const qaHeader = uiOsmoseHeader(context);\n\n let _qaItem;\n\n function osmoseEditor(selection) {\n\n const header = selection.selectAll('.header')\n .data([0]);\n\n const headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', () => context.enter(modeBrowse(context)))\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('QA.osmose.title'));\n\n let body = selection.selectAll('.body')\n .data([0]);\n\n body = body.enter()\n .append('div')\n .attr('class', 'body')\n .merge(body);\n\n let editor = body.selectAll('.qa-editor')\n .data([0]);\n\n editor.enter()\n .append('div')\n .attr('class', 'modal-section qa-editor')\n .merge(editor)\n .call(qaHeader.issue(_qaItem))\n .call(qaDetails.issue(_qaItem))\n .call(osmoseSaveSection);\n\n const footer = selection.selectAll('.footer')\n .data([0]);\n\n footer.enter()\n .append('div')\n .attr('class', 'footer')\n .merge(footer)\n .call(uiViewOnOsmose(context).what(_qaItem));\n }\n\n function osmoseSaveSection(selection) {\n const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());\n const isShown = (_qaItem && isSelected);\n let saveSection = selection.selectAll('.qa-save')\n .data(\n (isShown ? [_qaItem] : []),\n d => `${d.id}-${d.status || 0}`\n );\n\n // exit\n saveSection.exit()\n .remove();\n\n // enter\n const saveSectionEnter = saveSection.enter()\n .append('div')\n .attr('class', 'qa-save save-section cf');\n\n // update\n saveSectionEnter\n .merge(saveSection)\n .call(qaSaveButtons);\n }\n\n function qaSaveButtons(selection) {\n const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());\n let buttonSection = selection.selectAll('.buttons')\n .data((isSelected ? [_qaItem] : []), d => d.status + d.id);\n\n // exit\n buttonSection.exit()\n .remove();\n\n // enter\n const buttonEnter = buttonSection.enter()\n .append('div')\n .attr('class', 'buttons');\n\n buttonEnter\n .append('button')\n .attr('class', 'button close-button action');\n\n buttonEnter\n .append('button')\n .attr('class', 'button ignore-button action');\n\n // update\n buttonSection = buttonSection\n .merge(buttonEnter);\n\n buttonSection.select('.close-button')\n .call(t.append('QA.keepRight.close'))\n .on('click.close', function(d3_event, d) {\n this.blur(); // avoid keeping focus on the button - #4641\n const qaService = services.osmose;\n if (qaService) {\n d.newStatus = 'done';\n qaService.postUpdate(d, (err, item) => dispatch.call('change', item));\n }\n });\n\n buttonSection.select('.ignore-button')\n .call(t.append('QA.keepRight.ignore'))\n .on('click.ignore', function(d3_event, d) {\n this.blur(); // avoid keeping focus on the button - #4641\n const qaService = services.osmose;\n if (qaService) {\n d.newStatus = 'false';\n qaService.postUpdate(d, (err, item) => dispatch.call('change', item));\n }\n });\n }\n\n // NOTE: Don't change method name until UI v3 is merged\n osmoseEditor.error = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return osmoseEditor;\n };\n\n return utilRebind(osmoseEditor, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { behaviorBreathe } from '../behavior/breathe';\nimport { behaviorHover } from '../behavior/hover';\nimport { behaviorLasso } from '../behavior/lasso';\nimport { behaviorSelect } from '../behavior/select';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { modeBrowse } from './browse';\nimport { modeDragNode } from './drag_node';\nimport { modeDragNote } from './drag_note';\nimport { uiOsmoseEditor } from '../ui/osmose_editor';\nimport { utilKeybinding } from '../util';\n\n// NOTE: Don't change name of this until UI v3 is merged\nexport function modeSelectError(context, selectedErrorID, selectedErrorService) {\n var mode = {\n id: 'select-error',\n button: 'browse'\n };\n\n var keybinding = utilKeybinding('select-error');\n\n var errorService = services[selectedErrorService];\n var errorEditor;\n switch (selectedErrorService) {\n case 'osmose':\n errorEditor = uiOsmoseEditor(context)\n .on('change', function() {\n context.map().pan([0,0]); // trigger a redraw\n var error = checkSelectedID();\n if (!error) return;\n context.ui().sidebar\n .show(errorEditor.error(error));\n });\n break;\n }\n\n\n var behaviors = [\n behaviorBreathe(context),\n behaviorHover(context),\n behaviorSelect(context),\n behaviorLasso(context),\n modeDragNode(context).behavior,\n modeDragNote(context).behavior\n ];\n\n\n function checkSelectedID() {\n if (!errorService) return;\n var error = errorService.getError(selectedErrorID);\n if (!error) {\n context.enter(modeBrowse(context));\n }\n return error;\n }\n\n\n mode.zoomToSelected = function() {\n if (!errorService) return;\n var error = errorService.getError(selectedErrorID);\n if (error) {\n context.map().centerZoomEase(error.loc, 20);\n }\n };\n\n\n mode.enter = function() {\n var error = checkSelectedID();\n if (!error) return;\n\n behaviors.forEach(context.install);\n keybinding\n .on(t('inspector.zoom_to.key'), mode.zoomToSelected)\n .on('\u238B', esc, true);\n\n d3_select(document)\n .call(keybinding);\n\n selectError();\n\n var sidebar = context.ui().sidebar;\n sidebar.show(errorEditor.error(error));\n\n context.map()\n .on('drawn.select-error', selectError);\n\n\n // class the error as selected, or return to browse mode if the error is gone\n function selectError(d3_event, drawn) {\n if (!checkSelectedID()) return;\n\n var selection = context.surface()\n .selectAll('.itemId-' + selectedErrorID + '.' + selectedErrorService);\n\n if (selection.empty()) {\n // Return to browse mode if selected DOM elements have\n // disappeared because the user moved them out of view..\n var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;\n if (drawn && source && (source.type === 'pointermove' || source.type === 'mousemove' || source.type === 'touchmove')) {\n context.enter(modeBrowse(context));\n }\n\n } else {\n selection\n .classed('selected', true);\n\n context.selectedErrorID(selectedErrorID);\n }\n }\n\n function esc() {\n if (context.container().select('.combobox').size()) return;\n context.enter(modeBrowse(context));\n }\n };\n\n\n mode.exit = function() {\n behaviors.forEach(context.uninstall);\n\n d3_select(document)\n .call(keybinding.unbind);\n\n context.surface()\n .selectAll('.qaItem.selected')\n .classed('selected hover', false);\n\n context.map()\n .on('drawn.select-error', null);\n\n context.ui().sidebar\n .hide();\n\n context.selectedErrorID(null);\n context.features().forceVisible([]);\n };\n\n\n return mode;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { geoVecLength } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect } from '../modes/select';\nimport { modeSelectData } from '../modes/select_data';\nimport { modeSelectNote } from '../modes/select_note';\nimport { modeSelectError } from '../modes/select_error';\nimport { osmEntity, osmNote, QAItem } from '../osm';\nimport { utilFastMouse } from '../util/util';\n\n\nexport function behaviorSelect(context) {\n var _tolerancePx = 4; // see also behaviorDrag\n var _lastMouseEvent = null;\n var _showMenu = false;\n var _downPointers = {};\n var _longPressTimeout = null;\n var _lastInteractionType = null;\n // the id of the down pointer that's enabling multiselection while down\n var _multiselectionPointerId = null;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function keydown(d3_event) {\n\n if (d3_event.keyCode === 32) {\n // don't react to spacebar events during text input\n var activeNode = document.activeElement;\n if (activeNode && new Set(['INPUT', 'TEXTAREA']).has(activeNode.nodeName)) return;\n }\n\n if (d3_event.keyCode === 93 || // context menu key\n d3_event.keyCode === 32) { // spacebar\n d3_event.preventDefault();\n }\n\n if (d3_event.repeat) return; // ignore repeated events for held keys\n\n // if any key is pressed the user is probably doing something other than long-pressing\n cancelLongPress();\n\n if (d3_event.shiftKey) {\n context.surface()\n .classed('behavior-multiselect', true);\n }\n\n if (d3_event.keyCode === 32) { // spacebar\n if (!_downPointers.spacebar && _lastMouseEvent) {\n cancelLongPress();\n _longPressTimeout = window.setTimeout(didLongPress, 500, 'spacebar', 'spacebar');\n\n _downPointers.spacebar = {\n firstEvent: _lastMouseEvent,\n lastEvent: _lastMouseEvent\n };\n }\n }\n }\n\n\n function keyup(d3_event) {\n cancelLongPress();\n\n if (!d3_event.shiftKey) {\n context.surface()\n .classed('behavior-multiselect', false);\n }\n\n if (d3_event.keyCode === 93) { // context menu key\n d3_event.preventDefault();\n _lastInteractionType = 'menukey';\n contextmenu(d3_event);\n } else if (d3_event.keyCode === 32) { // spacebar\n var pointer = _downPointers.spacebar;\n if (pointer) {\n delete _downPointers.spacebar;\n\n if (pointer.done) return;\n\n d3_event.preventDefault();\n _lastInteractionType = 'spacebar';\n click(pointer.firstEvent, pointer.lastEvent, 'spacebar');\n }\n }\n }\n\n\n function pointerdown(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n\n cancelLongPress();\n\n if (d3_event.buttons && d3_event.buttons !== 1) return;\n\n context.ui().closeEditMenu();\n\n if (d3_event.pointerType !== 'mouse') {\n _longPressTimeout = window.setTimeout(didLongPress, 500, id, 'longdown-' + (d3_event.pointerType || 'mouse'));\n }\n\n _downPointers[id] = {\n firstEvent: d3_event,\n lastEvent: d3_event\n };\n }\n\n\n function didLongPress(id, interactionType) {\n var pointer = _downPointers[id];\n if (!pointer) return;\n\n for (var i in _downPointers) {\n // don't allow this or any currently down pointer to trigger another click\n _downPointers[i].done = true;\n }\n\n // treat long presses like right-clicks\n _longPressTimeout = null;\n _lastInteractionType = interactionType;\n _showMenu = true;\n\n click(pointer.firstEvent, pointer.lastEvent, id);\n }\n\n\n function pointermove(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n if (_downPointers[id]) {\n _downPointers[id].lastEvent = d3_event;\n }\n if (!d3_event.pointerType || d3_event.pointerType === 'mouse') {\n _lastMouseEvent = d3_event;\n if (_downPointers.spacebar) {\n _downPointers.spacebar.lastEvent = d3_event;\n }\n }\n }\n\n\n function pointerup(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n var pointer = _downPointers[id];\n if (!pointer) return;\n\n delete _downPointers[id];\n\n if (_multiselectionPointerId === id) {\n _multiselectionPointerId = null;\n }\n\n if (pointer.done) return;\n\n click(pointer.firstEvent, d3_event, id);\n }\n\n\n function pointercancel(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n if (!_downPointers[id]) return;\n\n delete _downPointers[id];\n\n if (_multiselectionPointerId === id) {\n _multiselectionPointerId = null;\n }\n }\n\n\n function contextmenu(d3_event) {\n d3_event.preventDefault();\n\n if (!+d3_event.clientX && !+d3_event.clientY) {\n if (_lastMouseEvent) {\n d3_event = _lastMouseEvent;\n } else {\n return;\n }\n } else {\n _lastMouseEvent = d3_event;\n if (d3_event.pointerType === 'touch' || d3_event.pointerType === 'pen' ||\n d3_event.mozInputSource && ( // firefox doesn't give a pointerType on contextmenu events\n d3_event.mozInputSource === MouseEvent.MOZ_SOURCE_TOUCH ||\n d3_event.mozInputSource === MouseEvent.MOZ_SOURCE_PEN)) {\n _lastInteractionType = 'touch';\n } else {\n _lastInteractionType = 'rightclick';\n }\n }\n\n _showMenu = true;\n click(d3_event, d3_event);\n }\n\n\n function click(firstEvent, lastEvent, pointerId) {\n cancelLongPress();\n\n var mapNode = context.container().select('.main-map').node();\n\n // Use the `main-map` coordinate system since the surface and supersurface\n // are transformed when drag-panning.\n var pointGetter = utilFastMouse(mapNode);\n var p1 = pointGetter(firstEvent);\n var p2 = pointGetter(lastEvent);\n var dist = geoVecLength(p1, p2);\n\n if (dist > _tolerancePx ||\n !mapContains(lastEvent)) {\n\n resetProperties();\n return;\n }\n\n var targetDatum = lastEvent.target.__data__;\n if (targetDatum === 0 && lastEvent.target.parentNode.__data__) {\n // some targets (like markers of the street level photo\n // layers) have the data bound to the parent node\n targetDatum = lastEvent.target.parentNode.__data__;\n }\n\n var multiselectEntityId;\n\n if (!_multiselectionPointerId) {\n // If a different pointer than the one triggering this click is down on a\n // feature, treat this and all future clicks as multiselection until that\n // pointer is raised.\n var selectPointerInfo = pointerDownOnSelection(pointerId);\n if (selectPointerInfo) {\n _multiselectionPointerId = selectPointerInfo.pointerId;\n // if the other feature isn't selected yet, make sure we select it\n multiselectEntityId = !selectPointerInfo.selected && selectPointerInfo.entityId;\n _downPointers[selectPointerInfo.pointerId].done = true;\n }\n }\n\n // support multiselect if data is already selected\n var isMultiselect = context.mode().id === 'select' && (\n // and shift key is down\n (lastEvent && lastEvent.shiftKey) ||\n // or we're lasso-selecting\n context.surface().select('.lasso').node() ||\n // or a pointer is down over a selected feature\n (_multiselectionPointerId && !multiselectEntityId)\n );\n\n processClick(targetDatum, isMultiselect, p2, multiselectEntityId);\n\n function mapContains(event) {\n var rect = mapNode.getBoundingClientRect();\n return event.clientX >= rect.left &&\n event.clientX <= rect.right &&\n event.clientY >= rect.top &&\n event.clientY <= rect.bottom;\n }\n\n function pointerDownOnSelection(skipPointerId) {\n var mode = context.mode();\n var selectedIDs = mode.id === 'select' ? mode.selectedIDs() : [];\n for (var pointerId in _downPointers) {\n if (pointerId === 'spacebar' || pointerId === skipPointerId) continue;\n\n var pointerInfo = _downPointers[pointerId];\n\n var p1 = pointGetter(pointerInfo.firstEvent);\n var p2 = pointGetter(pointerInfo.lastEvent);\n if (geoVecLength(p1, p2) > _tolerancePx) continue;\n\n var datum = pointerInfo.firstEvent.target.__data__;\n var entity = (datum && datum.properties && datum.properties.entity) || datum;\n if (context.graph().hasEntity(entity.id)) {\n return {\n pointerId: pointerId,\n entityId: entity.id,\n selected: selectedIDs.indexOf(entity.id) !== -1\n };\n }\n }\n return null;\n }\n }\n\n\n function processClick(datum, isMultiselect, point, alsoSelectId) {\n var mode = context.mode();\n var showMenu = _showMenu;\n var interactionType = _lastInteractionType;\n\n var entity = datum && datum.properties && datum.properties.entity;\n if (entity) datum = entity;\n\n if (datum && datum.type === 'midpoint') {\n // treat targeting midpoints as if targeting the parent way\n datum = datum.parents[0];\n }\n\n var newMode;\n\n if (datum instanceof osmEntity) {\n // targeting an entity\n var selectedIDs = context.selectedIDs();\n context.selectedNoteID(null);\n context.selectedErrorID(null);\n\n if (!isMultiselect) {\n // don't change the selection if we're toggling the menu atop a multiselection\n if (!showMenu ||\n selectedIDs.length <= 1 ||\n selectedIDs.indexOf(datum.id) === -1) {\n\n if (alsoSelectId === datum.id) alsoSelectId = null;\n\n selectedIDs = (alsoSelectId ? [alsoSelectId] : []).concat([datum.id]);\n // always enter modeSelect even if the entity is already\n // selected since listeners may expect `context.enter` events,\n // e.g. in the walkthrough\n newMode = mode.id === 'select' ? mode.selectedIDs(selectedIDs) : modeSelect(context, selectedIDs).selectBehavior(behavior);\n context.enter(newMode);\n }\n\n } else {\n if (selectedIDs.indexOf(datum.id) !== -1) {\n // clicked entity is already in the selectedIDs list..\n if (!showMenu) {\n // deselect clicked entity, then reenter select mode or return to browse mode..\n selectedIDs = selectedIDs.filter(function(id) { return id !== datum.id; });\n newMode = selectedIDs.length ? mode.selectedIDs(selectedIDs) : modeBrowse(context).selectBehavior(behavior);\n context.enter(newMode);\n }\n } else {\n // clicked entity is not in the selected list, add it..\n selectedIDs = selectedIDs.concat([datum.id]);\n newMode = mode.selectedIDs(selectedIDs);\n context.enter(newMode);\n }\n }\n\n } else if (datum && datum.__featurehash__ && !isMultiselect) {\n // targeting custom data\n context\n .selectedNoteID(null)\n .enter(modeSelectData(context, datum));\n\n } else if (datum instanceof osmNote && !isMultiselect) {\n // targeting a note\n context\n .selectedNoteID(datum.id)\n .enter(modeSelectNote(context, datum.id));\n\n } else if (datum instanceof QAItem && !isMultiselect) {\n // targeting an external QA issue\n context\n .selectedErrorID(datum.id)\n .enter(modeSelectError(context, datum.id, datum.service));\n\n } else if (datum.service === 'photo') {\n // street level photo was selected:\n // don't change mode and selection\n } else {\n // targeting nothing\n context.selectedNoteID(null);\n context.selectedErrorID(null);\n if (!isMultiselect && mode.id !== 'browse') {\n context.enter(modeBrowse(context));\n }\n }\n\n context.ui().closeEditMenu();\n\n // always request to show the edit menu in case the mode needs it\n if (showMenu) context.ui().showEditMenu(point, interactionType);\n\n resetProperties();\n }\n\n\n function cancelLongPress() {\n if (_longPressTimeout) window.clearTimeout(_longPressTimeout);\n _longPressTimeout = null;\n }\n\n\n function resetProperties() {\n cancelLongPress();\n _showMenu = false;\n _lastInteractionType = null;\n // don't reset _lastMouseEvent since it might still be useful\n }\n\n\n function behavior(selection) {\n resetProperties();\n _lastMouseEvent = context.map().lastPointerEvent();\n\n d3_select(window)\n .on('keydown.select', keydown)\n .on('keyup.select', keyup)\n .on(_pointerPrefix + 'move.select', pointermove, true)\n .on(_pointerPrefix + 'up.select', pointerup, true)\n .on('pointercancel.select', pointercancel, true)\n .on('contextmenu.select-window', function(d3_event) {\n // Edge and IE really like to show the contextmenu on the\n // menubar when user presses a keyboard menu button\n // even after we've already preventdefaulted the key event.\n var e = d3_event;\n if (+e.clientX === 0 && +e.clientY === 0) {\n d3_event.preventDefault();\n }\n });\n\n selection\n .on(_pointerPrefix + 'down.select', pointerdown)\n .on('contextmenu.select', contextmenu);\n\n /*if (d3_event && d3_event.shiftKey) {\n context.surface()\n .classed('behavior-multiselect', true);\n }*/\n }\n\n\n behavior.off = function(selection) {\n cancelLongPress();\n\n d3_select(window)\n .on('keydown.select', null)\n .on('keyup.select', null)\n .on('contextmenu.select-window', null)\n .on(_pointerPrefix + 'move.select', null, true)\n .on(_pointerPrefix + 'up.select', null, true)\n .on('pointercancel.select', null, true);\n\n selection\n .on(_pointerPrefix + 'down.select', null)\n .on('contextmenu.select', null);\n\n context.surface()\n .classed('behavior-multiselect', false);\n };\n\n\n return behavior;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../core/preferences';\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { services } from '../services';\nimport { localeDateString } from '../util/date';\n\n\nexport function uiNoteComments() {\n var _note;\n\n\n function noteComments(selection) {\n if (_note.isNew()) return; // don't draw .comments-container\n\n var comments = selection.selectAll('.comments-container')\n .data([0]);\n\n comments = comments.enter()\n .append('div')\n .attr('class', 'comments-container')\n .merge(comments);\n\n var commentEnter = comments.selectAll('.comment')\n .data(_note.comments)\n .enter()\n .append('div')\n .attr('class', 'comment');\n\n commentEnter\n .append('div')\n .attr('class', function(d) { return 'comment-avatar user-' + d.uid; })\n .call(svgIcon('#iD-icon-avatar', 'comment-avatar-icon'));\n\n var mainEnter = commentEnter\n .append('div')\n .attr('class', 'comment-main');\n\n var metadataEnter = mainEnter\n .append('div')\n .attr('class', 'comment-metadata');\n\n metadataEnter\n .append('div')\n .attr('class', 'comment-author')\n .each(function(d) {\n var selection = d3_select(this);\n var osm = services.osm;\n if (osm && d.user) {\n selection = selection\n .append('a')\n .attr('class', 'comment-author-link')\n .attr('href', osm.userURL(d.user))\n .attr('target', '_blank');\n }\n if (d.user) {\n selection.text(d.user);\n } else {\n selection.call(t.append('note.anonymous'));\n }\n });\n\n metadataEnter\n .append('div')\n .attr('class', 'comment-date')\n .each(function(d) {\n d3_select(this).call(\n t.addOrUpdate('note.status.' + d.action, {\n when: localeDateString(d.date.replace(' UTC', 'Z').replace(' ', 'T')),\n }));\n });\n\n mainEnter\n .append('div')\n .attr('class', 'comment-text')\n .html(function(d) { return d.html; })\n .selectAll('a')\n .attr('rel', 'noopener nofollow')\n .attr('target', '_blank');\n\n comments\n .call(replaceAvatars);\n }\n\n\n function replaceAvatars(selection) {\n var showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n var osm = services.osm;\n if (showThirdPartyIcons !== 'true' || !osm) return;\n\n var uids = {}; // gather uids in the comment thread\n _note.comments.forEach(function(d) {\n if (d.uid) uids[d.uid] = true;\n });\n\n Object.keys(uids).forEach(function(uid) {\n osm.loadUser(uid, function(err, user) {\n if (!user || !user.image_url) return;\n\n selection.selectAll('.comment-avatar.user-' + uid)\n .html('')\n .append('img')\n .attr('class', 'icon comment-avatar-icon')\n .attr('src', user.image_url)\n .attr('alt', user.display_name);\n });\n });\n }\n\n\n noteComments.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteComments;\n };\n\n\n return noteComments;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiNoteHeader() {\n var _note;\n\n\n function noteHeader(selection) {\n var header = selection.selectAll('.note-header')\n .data(\n (_note ? [_note] : []),\n function(d) { return d.status + d.id; }\n );\n\n header.exit()\n .remove();\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'note-header');\n\n var iconEnter = headerEnter\n .append('div')\n .attr('class', function(d) { return 'note-header-icon ' + d.status; })\n .classed('new', function(d) { return d.id < 0; });\n\n iconEnter\n .append('div')\n .attr('class', 'preset-icon-28')\n .call(svgIcon('#iD-icon-note', 'note-fill'));\n\n iconEnter.each(function(d) {\n var statusIcon;\n if (d.id < 0) {\n statusIcon = '#iD-icon-plus';\n } else if (d.status === 'open') {\n statusIcon = '#iD-icon-close';\n } else {\n statusIcon = '#iD-icon-apply';\n }\n iconEnter\n .append('div')\n .attr('class', 'note-icon-annotation')\n .attr('title', t('icons.close'))\n .call(svgIcon(statusIcon, 'icon-annotation'));\n });\n\n headerEnter\n .append('div')\n .attr('class', 'note-header-label')\n .each(function(d) {\n const selection = d3_select(this);\n selection.text('');\n if (_note.isNew()) {\n selection.call(t.append('note.new'));\n } else {\n selection.call(t.append('note.note'));\n selection\n .append('span')\n .text(` ${d.id} `);\n if (d.status === 'closed') {\n selection.call(t.append('note.closed'));\n }\n }\n });\n }\n\n\n noteHeader.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteHeader;\n };\n\n\n return noteHeader;\n}\n", "import { t } from '../core/localizer';\nimport { osmNote } from '../osm';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiNoteReport() {\n var _note;\n\n function noteReport(selection) {\n var url;\n if (services.osm && (_note instanceof osmNote) && (!_note.isNew())) {\n url = services.osm.noteReportURL(_note);\n }\n\n var link = selection.selectAll('.note-report')\n .data(url ? [url] : []);\n\n // exit\n link.exit()\n .remove();\n\n // enter\n var linkEnter = link.enter()\n .append('a')\n .attr('class', 'note-report')\n .attr('target', '_blank')\n .attr('href', function(d) { return d; })\n .call(svgIcon('#iD-icon-out-link', 'inline'));\n\n linkEnter\n .append('span')\n .call(t.append('note.report'));\n }\n\n\n noteReport.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteReport;\n };\n\n return noteReport;\n}\n", "import { t } from '../core/localizer';\nimport { osmEntity, osmNote, osmRelation, osmWay } from '../osm';\nimport { svgIcon } from '../svg/icon';\nimport { getRelativeDate } from '../util/date';\n\n\nexport function uiViewOnOSM(context) {\n var _what; // an osmEntity or osmNote\n\n\n function viewOnOSM(selection) {\n var url;\n if (_what instanceof osmEntity) {\n url = context.connection().historyURL(_what);\n } else if (_what instanceof osmNote) {\n url = context.connection().noteURL(_what);\n }\n\n var data = ((!_what || _what.isNew()) ? [] : [_what]);\n var link = selection.selectAll('.view-on-osm')\n .data(data, function(d) { return d.id; });\n\n // exit\n link.exit()\n .remove();\n\n // enter\n var linkEnter = link.enter()\n .append('a')\n .attr('class', 'view-on-osm')\n .attr('target', '_blank')\n .attr('href', url)\n .call(svgIcon('#iD-icon-out-link', 'inline'));\n\n\n if (_what && !(_what instanceof osmNote)) {\n // node/way/relation\n const { user, timestamp } = uiViewOnOSM.findLastModifiedChild(context.history().base(), _what);\n\n linkEnter\n .call(t.append('inspector.last_touched', {\n timeago: getRelativeDate(new Date(timestamp)),\n user\n }))\n .attr('title', t('inspector.view_on_osm'));\n } else {\n linkEnter\n .append('span')\n .call(t.append('inspector.view_on_osm'));\n }\n }\n\n\n viewOnOSM.what = function(_) {\n if (!arguments.length) return _what;\n _what = _;\n return viewOnOSM;\n };\n\n return viewOnOSM;\n}\n\n\n/**\n * @param {iD.Graph} graph\n * @param {iD.OsmEntity} feature\n */\nuiViewOnOSM.findLastModifiedChild = (graph, feature) => {\n let latest = feature;\n\n /** @param {iD.OsmEntity} obj */\n function recurseChilds(obj) {\n if (obj.timestamp > latest.timestamp) {\n latest = obj;\n }\n if (obj instanceof osmWay) {\n obj.nodes\n .map(id => graph.hasEntity(id))\n .filter(Boolean)\n .forEach(recurseChilds);\n } else if (obj instanceof osmRelation) {\n obj.members\n .map(m => graph.hasEntity(m.id))\n .filter(e => e instanceof osmWay || e instanceof osmRelation)\n .forEach(recurseChilds);\n }\n }\n\n recurseChilds(feature);\n return latest;\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\n\n// import { uiField } from './field';\n// import { uiFormFields } from './form_fields';\n\nimport { uiNoteComments } from './note_comments';\nimport { uiNoteHeader } from './note_header';\nimport { uiNoteReport } from './note_report';\nimport { uiViewOnOSM } from './view_on_osm';\n\nimport {\n utilNoAuto,\n utilRebind\n} from '../util';\nimport { osmNote } from '../osm';\n\n\nexport function uiNoteEditor(context) {\n var dispatch = d3_dispatch('change');\n var noteComments = uiNoteComments(context);\n var noteHeader = uiNoteHeader();\n\n // var formFields = uiFormFields(context);\n\n var _note;\n var _newNote;\n // var _fieldsArr;\n\n\n function noteEditor(selection) {\n\n var header = selection.selectAll('.header')\n .data([0]);\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function() {\n context.enter(modeBrowse(context));\n })\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('note.title'));\n\n\n var body = selection.selectAll('.body')\n .data([0]);\n\n body = body.enter()\n .append('div')\n .attr('class', 'body')\n .merge(body);\n\n var editor = body.selectAll('.note-editor')\n .data([0]);\n\n editor.enter()\n .append('div')\n .attr('class', 'modal-section note-editor')\n .merge(editor)\n .call(noteHeader.note(_note))\n .call(noteComments.note(_note))\n .call(noteSaveSection);\n\n var footer = selection.selectAll('.footer')\n .data([0]);\n\n footer.enter()\n .append('div')\n .attr('class', 'footer')\n .merge(footer)\n .call(uiViewOnOSM(context).what(_note))\n .call(uiNoteReport(context).note(_note));\n\n\n // rerender the note editor on any auth change\n var osm = services.osm;\n if (osm) {\n osm.on('change.note-save', function() {\n selection.call(noteEditor);\n });\n }\n }\n\n\n function noteSaveSection(selection) {\n var isSelected = (_note && _note.id === context.selectedNoteID());\n var noteSave = selection.selectAll('.note-save')\n .data((isSelected ? [_note].filter(d => d.status !== 'hidden') : []), d => d.status + d.id);\n\n // exit\n noteSave.exit()\n .remove();\n\n // enter\n var noteSaveEnter = noteSave.enter()\n .append('div')\n .attr('class', 'note-save save-section cf');\n\n // // if new note, show categories to pick from\n // if (_note.isNew()) {\n // var presets = presetManager;\n\n // // NOTE: this key isn't a age and therefore there is no documentation (yet)\n // _fieldsArr = [\n // uiField(context, presets.field('category'), null, { show: true, revert: false }),\n // ];\n\n // _fieldsArr.forEach(function(field) {\n // field\n // .on('change', changeCategory);\n // });\n\n // noteSaveEnter\n // .append('div')\n // .attr('class', 'note-category')\n // .call(formFields.fieldsArr(_fieldsArr));\n // }\n\n // function changeCategory() {\n // // NOTE: perhaps there is a better way to get value\n // var val = context.container().select('input[name=\\'category\\']:checked').property('__data__') || undefined;\n\n // // store the unsaved category with the note itself\n // _note = _note.update({ newCategory: val });\n // var osm = services.osm;\n // if (osm) {\n // osm.replaceNote(_note); // update note cache\n // }\n // noteSave\n // .call(noteSaveButtons);\n // }\n\n noteSaveEnter\n .append('h4')\n .attr('class', '.note-save-header')\n .text('')\n .each(function() {\n if (_note.isNew()) {\n t.append('note.newDescription')(d3_select(this));\n } else {\n t.append('note.newComment')(d3_select(this));\n }\n });\n\n var commentTextarea = noteSaveEnter\n .append('textarea')\n .attr('class', 'new-comment-input')\n .attr('placeholder', t('note.inputPlaceholder'))\n .attr('maxlength', 1000)\n .property('value', function(d) { return d.newComment; })\n .call(utilNoAuto)\n .on('keydown.note-input', keydown)\n .on('input.note-input', changeInput)\n .on('blur.note-input', changeInput);\n\n if (!commentTextarea.empty() && _newNote) {\n // autofocus the comment field for new notes\n commentTextarea.node().focus();\n }\n\n // update\n noteSave = noteSaveEnter\n .merge(noteSave)\n .call(userDetails)\n .call(noteSaveButtons);\n\n\n // fast submit if user presses cmd+enter\n function keydown(d3_event) {\n if (!(d3_event.keyCode === 13 && // \u21A9 Return\n d3_event.metaKey)) return;\n\n var osm = services.osm;\n if (!osm) return;\n\n var hasAuth = osm.authenticated();\n if (!hasAuth) return;\n\n if (!_note.newComment) return;\n\n d3_event.preventDefault();\n\n d3_select(this)\n .on('keydown.note-input', null);\n\n // focus on button and submit\n window.setTimeout(function() {\n if (_note.isNew()) {\n noteSave.selectAll('.save-button').node().focus();\n clickSave(_note);\n } else {\n noteSave.selectAll('.comment-button').node().focus();\n postCommentAndStatus(_note);\n }\n }, 10);\n }\n\n\n function changeInput() {\n var input = d3_select(this);\n var val = input.property('value').trim() || undefined;\n\n // store the unsaved comment with the note itself\n _note = _note.update({ newComment: val });\n\n var osm = services.osm;\n if (osm) {\n osm.replaceNote(_note); // update note cache\n }\n\n noteSave\n .call(noteSaveButtons);\n }\n }\n\n\n function userDetails(selection) {\n var detailSection = selection.selectAll('.detail-section')\n .data([0]);\n\n detailSection = detailSection.enter()\n .append('div')\n .attr('class', 'detail-section')\n .merge(detailSection);\n\n var osm = services.osm;\n if (!osm) return;\n\n // Add warning if user is not logged in\n var hasAuth = osm.authenticated();\n var authWarning = detailSection.selectAll('.auth-warning')\n .data(hasAuth ? [] : [0]);\n\n authWarning.exit()\n .transition()\n .duration(200)\n .style('opacity', 0)\n .remove();\n\n var authEnter = authWarning.enter()\n .insert('div', '.tag-reference-body')\n .attr('class', 'field-warning auth-warning')\n .style('opacity', 0);\n\n authEnter\n .call(svgIcon('#iD-icon-alert', 'inline'));\n\n authEnter\n .append('span')\n .call(t.append('note.login'));\n\n authEnter\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append('login'))\n .on('click.note-login', function(d3_event) {\n d3_event.preventDefault();\n osm.authenticate();\n });\n\n authEnter\n .transition()\n .duration(200)\n .style('opacity', 1);\n\n\n var prose = detailSection.selectAll('.note-save-prose')\n .data(hasAuth ? [0] : []);\n\n prose.exit()\n .remove();\n\n prose = prose.enter()\n .append('p')\n .attr('class', 'note-save-prose')\n .call(t.append('note.upload_explanation'))\n .merge(prose);\n\n osm.userDetails(function(err, user) {\n if (err) return;\n\n const userLink = selection => {\n if (user.image_url) {\n selection\n .append('img')\n .attr('src', user.image_url)\n .attr('class', 'icon pre-text user-icon');\n }\n\n selection\n .append('a')\n .attr('class', 'user-info')\n .text(user.display_name)\n .attr('href', osm.userURL(user.display_name))\n .attr('target', '_blank');\n };\n\n prose\n .call(t.addOrUpdate('note.upload_explanation_with_user', { user: userLink }));\n });\n }\n\n\n function noteSaveButtons(selection) {\n var osm = services.osm;\n var hasAuth = osm && osm.authenticated();\n\n var isSelected = (_note && _note.id === context.selectedNoteID());\n var buttonSection = selection.selectAll('.buttons')\n .data((isSelected ? [_note] : []), d => d.status + d.id);\n\n // exit\n buttonSection.exit()\n .remove();\n\n // enter\n var buttonEnter = buttonSection.enter()\n .append('div')\n .attr('class', 'buttons');\n\n if (_note.isNew()) {\n buttonEnter\n .append('button')\n .attr('class', 'button cancel-button secondary-action')\n .call(t.append('confirm.cancel'));\n\n buttonEnter\n .append('button')\n .attr('class', 'button save-button action')\n .call(t.append('note.save'));\n\n } else {\n buttonEnter\n .append('button')\n .attr('class', 'button status-button action');\n\n buttonEnter\n .append('button')\n .attr('class', 'button comment-button action')\n .call(t.append('note.comment'));\n }\n\n\n // update\n buttonSection = buttonSection\n .merge(buttonEnter);\n\n buttonSection.select('.cancel-button') // select and propagate data\n .on('click.cancel', clickCancel);\n\n buttonSection.select('.save-button') // select and propagate data\n .attr('disabled', isSaveDisabled)\n .on('click.save', clickSave);\n\n buttonSection.select('.status-button') // select and propagate data\n .attr('disabled', (hasAuth ? null : true))\n .each(function(d) {\n var action = (d.status === 'open' ? 'close' : 'open');\n var andComment = (d.newComment ? '_comment' : '');\n t.addOrUpdate('note.' + action + andComment)(d3_select(this));\n })\n .on('click.status', function(d3_event, note) {\n const setStatus = (note.status === 'open' ? 'closed' : 'open');\n postCommentAndStatus.bind(this)(d3_event, note, setStatus);\n });\n\n buttonSection.select('.comment-button') // select and propagate data\n .attr('disabled', isSaveDisabled)\n .on('click.comment', postCommentAndStatus);\n\n\n function isSaveDisabled(d) {\n return (hasAuth && d.status === 'open' && d.newComment) ? null : true;\n }\n }\n\n\n\n function clickCancel(d3_event, d) {\n this.blur(); // avoid keeping focus on the button - #4641\n var osm = services.osm;\n if (osm) {\n osm.removeNote(d);\n }\n context.enter(modeBrowse(context));\n dispatch.call('change', d);\n }\n\n\n function clickSave(d3_event, note) {\n this.blur(); // avoid keeping focus on the button - #4641\n var osm = services.osm;\n if (osm) {\n osm.postNoteCreate(note, (err, d) => dispatch.call('change', d));\n }\n }\n\n\n function postCommentAndStatus(d3_event, note, newStatus) {\n this.blur(); // avoid keeping focus on the button - #4641\n var osm = services.osm;\n if (osm) {\n osm.postNoteUpdate(note, newStatus || note.status, function(err, d) {\n if (!err) {\n dispatch.call('change', d);\n } else if (err.status === 409) {\n // note was probably closed in the meantime: reload it - #8464\n osm.loadEntityNote(note.id, (err, d) => {\n dispatch.call('change', osmNote({ ...d.data[0], newComment: note.newComment }));\n });\n } else if (err.status === 410) {\n // note was deleted/hidden by a moderator\n osm.removeNote(note);\n dispatch.call('change', osmNote({ id: note.id, status: 'hidden', comments: [...note.comments, { action: 'hidden' }] }));\n }\n });\n }\n }\n\n\n noteEditor.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteEditor;\n };\n\n noteEditor.newNote = function(val) {\n if (!arguments.length) return _newNote;\n _newNote = val;\n return noteEditor;\n };\n\n\n return utilRebind(noteEditor, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { behaviorBreathe } from '../behavior/breathe';\nimport { behaviorHover } from '../behavior/hover';\nimport { behaviorLasso } from '../behavior/lasso';\nimport { behaviorSelect } from '../behavior/select';\n\nimport { t } from '../core/localizer';\n\nimport { modeBrowse } from './browse';\nimport { modeDragNode } from './drag_node';\nimport { modeDragNote } from './drag_note';\nimport { services } from '../services';\nimport { uiNoteEditor } from '../ui/note_editor';\nimport { utilKeybinding } from '../util';\n\n\nexport function modeSelectNote(context, selectedNoteID) {\n var mode = {\n id: 'select-note',\n button: 'browse'\n };\n\n var _keybinding = utilKeybinding('select-note');\n var _noteEditor = uiNoteEditor(context)\n .on('change', function() {\n context.map().pan([0,0]); // trigger a redraw\n var note = this || checkSelectedID();\n if (!note) return;\n context.ui().sidebar\n .show(_noteEditor.note(note));\n });\n\n var _behaviors = [\n behaviorBreathe(context),\n behaviorHover(context),\n behaviorSelect(context),\n behaviorLasso(context),\n modeDragNode(context).behavior,\n modeDragNote(context).behavior\n ];\n\n var _newFeature = false;\n\n\n function checkSelectedID() {\n if (!services.osm) return;\n var note = services.osm.getNote(selectedNoteID);\n if (!note) {\n context.enter(modeBrowse(context));\n }\n return note;\n }\n\n\n // class the note as selected, or return to browse mode if the note is gone\n function selectNote(d3_event, drawn) {\n if (!checkSelectedID()) return;\n\n var selection = context.surface().selectAll('.layer-notes .note-' + selectedNoteID);\n\n if (selection.empty()) {\n // Return to browse mode if selected DOM elements have\n // disappeared because the user moved them out of view..\n var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;\n if (drawn && source && (source.type === 'pointermove' || source.type === 'mousemove' || source.type === 'touchmove')) {\n context.enter(modeBrowse(context));\n }\n\n } else {\n selection\n .classed('selected', true);\n\n context.selectedNoteID(selectedNoteID);\n }\n }\n\n\n function esc() {\n if (context.container().select('.combobox').size()) return;\n context.enter(modeBrowse(context));\n }\n\n\n mode.zoomToSelected = function() {\n if (!services.osm) return;\n var note = services.osm.getNote(selectedNoteID);\n if (note) {\n context.map().centerZoomEase(note.loc, 20);\n }\n };\n\n\n mode.newFeature = function(val) {\n if (!arguments.length) return _newFeature;\n _newFeature = val;\n return mode;\n };\n\n\n mode.enter = function() {\n var note = checkSelectedID();\n if (!note) return;\n\n _behaviors.forEach(context.install);\n\n _keybinding\n .on(t('inspector.zoom_to.key'), mode.zoomToSelected)\n .on('\u238B', esc, true);\n\n d3_select(document)\n .call(_keybinding);\n\n selectNote();\n\n var sidebar = context.ui().sidebar;\n sidebar.show(_noteEditor.note(note).newNote(_newFeature));\n\n // expand the sidebar, avoid obscuring the note if needed\n sidebar.expand(sidebar.intersects(note.extent()));\n\n context.map()\n .on('drawn.select', selectNote);\n };\n\n\n mode.exit = function() {\n _behaviors.forEach(context.uninstall);\n\n d3_select(document)\n .call(_keybinding.unbind);\n\n context.surface()\n .selectAll('.layer-notes .selected')\n .classed('selected hover', false);\n\n context.map()\n .on('drawn.select', null);\n\n context.ui().sidebar\n .hide();\n\n context.selectedNoteID(null);\n };\n\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDraw } from '../behavior/draw';\nimport { modeBrowse } from './browse';\nimport { modeSelectNote } from './select_note';\nimport { osmNote } from '../osm';\nimport { services } from '../services';\n\n\nexport function modeAddNote(context) {\n var mode = {\n id: 'add-note',\n button: 'note',\n description: t.append('modes.add_note.description'),\n key: t('modes.add_note.key')\n };\n\n var behavior = behaviorDraw(context)\n .on('click', add)\n .on('cancel', cancel)\n .on('finish', cancel);\n\n\n function add(loc) {\n var osm = services.osm;\n if (!osm) return;\n\n var note = osmNote({ loc: loc, status: 'open', comments: [] });\n osm.replaceNote(note);\n\n // force a reraw (there is no history change that would otherwise do this)\n context.map().pan([0,0]);\n\n context\n .selectedNoteID(note.id)\n .enter(modeSelectNote(context, note.id).newFeature(true));\n }\n\n\n function cancel() {\n context.enter(modeBrowse(context));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { behaviorOperation } from '../behavior/operation';\nimport { modeMove } from '../modes/move';\nimport { utilGetAllNodes, utilTotalExtent } from '../util/util';\n\n\nexport function operationMove(context, selectedIDs) {\n var multi = (selectedIDs.length === 1 ? 'single' : 'multiple');\n var nodes = utilGetAllNodes(selectedIDs, context.graph());\n var coords = nodes.map(function(n) { return n.loc; });\n var extent = utilTotalExtent(selectedIDs, context.graph());\n\n\n var operation = function() {\n context.enter(modeMove(context, selectedIDs));\n };\n\n\n operation.available = function() {\n return selectedIDs.length > 0;\n };\n\n\n operation.disabled = function() {\n if (extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n } else if (selectedIDs.some(incompleteRelation)) {\n return 'incomplete_relation';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n\n function incompleteRelation(id) {\n var entity = context.entity(id);\n return entity.type === 'relation' && !entity.isComplete(context.graph());\n }\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.move.' + disable + '.' + multi) :\n t.append('operations.move.description.' + multi);\n };\n\n\n operation.annotation = function() {\n return selectedIDs.length === 1 ?\n t('operations.move.annotation.' + context.graph().geometry(selectedIDs[0])) :\n t('operations.move.annotation.feature', { n: selectedIDs.length });\n };\n\n\n operation.id = 'move';\n operation.keys = [t('operations.move.key')];\n operation.title = t.append('operations.move.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n operation.mouseOnly = true;\n\n return operation;\n}\n", "import {\n geoIdentity as d3_geoIdentity,\n geoPath as d3_geoPath,\n geoStream as d3_geoStream\n} from 'd3-geo';\n\nimport { geoVecAdd, geoVecAngle, geoVecLength } from '../geo';\n\n\n// Touch targets control which other vertices we can drag a vertex onto.\n//\n// - the activeID - nope\n// - 1 away (adjacent) to the activeID - yes (vertices will be merged)\n// - 2 away from the activeID - nope (would create a self intersecting segment)\n// - all others on a linear way - yes\n// - all others on a closed way - nope (would create a self intersecting polygon)\n//\n// returns\n// 0 = active vertex - no touch/connect\n// 1 = passive vertex - yes touch/connect\n// 2 = adjacent vertex - yes but pay attention segmenting a line here\n//\nexport function svgPassiveVertex(node, graph, activeID) {\n if (!activeID) return 1;\n if (activeID === node.id) return 0;\n\n var parents = graph.parentWays(node);\n\n var i, j, nodes, isClosed, ix1, ix2, ix3, ix4, max;\n\n for (i = 0; i < parents.length; i++) {\n nodes = parents[i].nodes;\n isClosed = parents[i].isClosed();\n for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby\n if (nodes[j] === node.id) {\n ix1 = j - 2;\n ix2 = j - 1;\n ix3 = j + 1;\n ix4 = j + 2;\n\n if (isClosed) { // wraparound if needed\n max = nodes.length - 1;\n if (ix1 < 0) ix1 = max + ix1;\n if (ix2 < 0) ix2 = max + ix2;\n if (ix3 > max) ix3 = ix3 - max;\n if (ix4 > max) ix4 = ix4 - max;\n }\n\n if (nodes[ix1] === activeID) return 0; // no - prevent self intersect\n else if (nodes[ix2] === activeID) return 2; // ok - adjacent\n else if (nodes[ix3] === activeID) return 2; // ok - adjacent\n else if (nodes[ix4] === activeID) return 0; // no - prevent self intersect\n else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // no - prevent self intersect\n }\n }\n }\n\n return 1; // ok\n}\n\n\n/**\n *\n * @param {iD.Projection} projection\n * @param {iD.Graph} graph\n * @param {Number} dt spacing between segments\n * @param {Function} [shouldReverse]\n * @param {Function} [bothDirections]\n */\nexport function svgMarkerSegments(projection, graph, dt, shouldReverse = () => false, bothDirections = () => false) {\n /**\n * @param {iD.OsmWay} entity\n * @returns {[{id: String, d: String}]} list of svg path segments corres\n */\n return function(entity) {\n let i = 0;\n let offset = dt / 2;\n const segments = [];\n\n const clip = paddedClipExtent(projection);\n\n const coordinates = graph.childNodes(entity).map(function(n) { return n.loc; });\n let a, b;\n\n const _shouldReverse = shouldReverse(entity);\n const _bothDirections = bothDirections(entity);\n\n d3_geoStream({\n type: 'LineString',\n coordinates: coordinates\n }, projection.stream(clip({\n lineStart: function() {},\n lineEnd: function() { a = null; },\n point: function(x, y) {\n b = [x, y];\n\n if (a) {\n let span = geoVecLength(a, b) - offset;\n\n if (span >= 0) {\n const heading = geoVecAngle(a, b);\n const dx = dt * Math.cos(heading);\n const dy = dt * Math.sin(heading);\n let p = [\n a[0] + offset * Math.cos(heading),\n a[1] + offset * Math.sin(heading)\n ];\n\n // gather coordinates\n const coord = [a, p];\n for (span -= dt; span >= 0; span -= dt) {\n p = geoVecAdd(p, [dx, dy]);\n coord.push(p);\n }\n coord.push(b);\n\n // generate svg paths\n let segment = '';\n\n if (!_shouldReverse || _bothDirections) {\n for (let j = 0; j < coord.length; j++) {\n segment += (j === 0 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];\n }\n segments.push({ id: entity.id, index: i++, d: segment });\n }\n\n if (_shouldReverse || _bothDirections) {\n segment = '';\n for (let j = coord.length - 1; j >= 0; j--) {\n segment += (j === coord.length - 1 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];\n }\n segments.push({ id: entity.id, index: i++, d: segment });\n }\n }\n\n offset = -span;\n }\n\n a = b;\n }\n })));\n\n return segments;\n };\n}\n\n\n/**\n * @param {iD.Projection} projection\n * @param {iD.Graph} graph\n * @param {Boolean} isArea\n */\nexport function svgPath(projection, graph, isArea) {\n const cache = {};\n const project = projection.stream;\n const clip = paddedClipExtent(projection, isArea);\n const path = d3_geoPath()\n .projection({stream: function(output) { return project(clip(output)); }});\n\n const svgpath = function(entity) {\n if (entity.id in cache) {\n return cache[entity.id];\n } else {\n return cache[entity.id] = path(entity.asGeoJSON(graph));\n }\n };\n\n svgpath.geojson = function(d) {\n if (d.__featurehash__ !== undefined) {\n if (d.__featurehash__ in cache) {\n return cache[d.__featurehash__];\n } else {\n return cache[d.__featurehash__] = path(d);\n }\n } else {\n return path(d);\n }\n };\n\n return svgpath;\n}\n\n\nexport function svgPointTransform(projection) {\n var svgpoint = function(entity) {\n // http://jsperf.com/short-array-join\n var pt = projection(entity.loc);\n return 'translate(' + pt[0] + ',' + pt[1] + ')';\n };\n\n svgpoint.geojson = function(d) {\n return svgpoint(d.properties.entity);\n };\n\n return svgpoint;\n}\n\n\nexport function svgRelationMemberTags(graph) {\n return function(entity) {\n var tags = entity.tags;\n var shouldCopyMultipolygonTags = !entity.hasInterestingTags();\n graph.parentRelations(entity).forEach(function(relation) {\n var type = relation.tags.type;\n if ((type === 'multipolygon' && shouldCopyMultipolygonTags) || type === 'boundary') {\n tags = Object.assign({}, relation.tags, tags);\n }\n });\n return tags;\n };\n}\n\n\nexport function svgSegmentWay(way, graph, activeID) {\n // When there is no activeID, we can memoize this expensive computation\n if (activeID === undefined) {\n return graph.transient(way, 'waySegments', getWaySegments);\n } else {\n return getWaySegments();\n }\n\n function getWaySegments() {\n const isActiveWay = (way.nodes.indexOf(activeID) !== -1);\n const features = { passive: [], active: [] };\n let start = {};\n\n for (var i = 0; i < way.nodes.length; i++) {\n const node = graph.entity(way.nodes[i]);\n const type = svgPassiveVertex(node, graph, activeID);\n const end = { node: node, type: type };\n\n if (start.type !== undefined) {\n if (start.node.id === activeID || end.node.id === activeID) {\n // push nothing\n } else if (isActiveWay && (start.type === 2 || end.type === 2)) { // one adjacent vertex\n pushActive(start, end, i);\n } else if (start.type === 0 && end.type === 0) { // both active vertices\n pushActive(start, end, i);\n } else {\n pushPassive(start, end, i);\n }\n }\n\n start = end;\n }\n\n return features;\n\n function pushActive(start, end, index) {\n features.active.push({\n type: 'Feature',\n id: way.id + '-' + index + '-nope',\n properties: {\n nope: true,\n target: true,\n entity: way,\n nodes: [start.node, end.node],\n index: index\n },\n geometry: {\n type: 'LineString',\n coordinates: [start.node.loc, end.node.loc]\n }\n });\n }\n\n function pushPassive(start, end, index) {\n features.passive.push({\n type: 'Feature',\n id: way.id + '-' + index,\n properties: {\n target: true,\n entity: way,\n nodes: [start.node, end.node],\n index: index\n },\n geometry: {\n type: 'LineString',\n coordinates: [start.node.loc, end.node.loc]\n }\n });\n }\n }\n}\n\n\n/**\n * Returns a d3 projection stream that clips the given geometries to an\n * extent that is slightly padded.\n *\n * Explanation of magic numbers:\n * \"padding\" here allows space for strokes to extend beyond the viewport,\n * so that the stroke isn't drawn along the edge of the viewport when\n * the shape is clipped.\n * When drawing lines, pad viewport by 5px.\n * When drawing areas, pad viewport by 65px in each direction to allow\n * for 60px area fill stroke (see \".fill-partial path.fill\" css rule)\n *\n * @param {import('../geo/raw_mercator').Projection} projection\n * @param {Boolean} isArea\n */\nfunction paddedClipExtent(projection, isArea = false) {\n var padding = isArea ? 65 : 5;\n var viewport = projection.clipExtent();\n var paddedExtent = [\n [viewport[0][0] - padding, viewport[0][1] - padding],\n [viewport[1][0] + padding, viewport[1][1] + padding]\n ];\n return d3_geoIdentity().clipExtent(paddedExtent).stream;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { osmPathHighwayTagValues, osmPavedTags, osmSemipavedTags, osmLifecyclePrefixes } from '../osm/tags';\n\n\nexport function svgTagClasses() {\n var primaries = [\n 'building', 'highway', 'railway', 'waterway', 'aeroway', 'aerialway',\n 'piste:type', 'boundary', 'power', 'amenity', 'natural', 'landuse',\n 'leisure', 'military', 'place', 'man_made', 'route', 'attraction',\n 'roller_coaster', 'building:part', 'indoor', 'climbing'\n ];\n var statuses = Object.keys(osmLifecyclePrefixes);\n var secondaries = [\n 'oneway', 'bridge', 'tunnel', 'embankment', 'cutting', 'barrier',\n 'surface', 'tracktype', 'footway', 'crossing', 'service', 'sport',\n 'public_transport', 'location', 'parking', 'golf', 'type', 'leisure',\n 'man_made', 'indoor', 'construction', 'proposed'\n ];\n var _tags = function(entity) { return entity.tags; };\n\n\n var tagClasses = function(selection) {\n selection.each(function tagClassesEach(entity) {\n var value = this.className;\n\n if (value.baseVal !== undefined) {\n value = value.baseVal;\n }\n\n var t = _tags(entity);\n\n var computed = tagClasses.getClassesString(t, value);\n\n if (computed !== value) {\n d3_select(this).attr('class', computed);\n }\n });\n };\n\n\n tagClasses.getClassesString = function(t, value) {\n var primary, status;\n var i, j, k, v;\n\n // in some situations we want to render perimeter strokes a certain way\n var overrideGeometry;\n if (/\\bstroke\\b/.test(value)) {\n if (!!t.barrier && t.barrier !== 'no') {\n overrideGeometry = 'line';\n }\n }\n\n // preserve base classes (nothing with `tag-`)\n var classes = value.trim().split(/\\s+/)\n .filter(function(klass) {\n return klass.length && !/^tag-/.test(klass);\n })\n .map(function(klass) { // special overrides for some perimeter strokes\n return (klass === 'line' || klass === 'area') ? (overrideGeometry || klass) : klass;\n });\n\n // pick at most one primary classification tag..\n for (i = 0; i < primaries.length; i++) {\n k = primaries[i];\n v = t[k];\n if (!v || v === 'no') continue;\n\n if (k === 'piste:type') { // avoid a ':' in the class name\n k = 'piste';\n } else if (k === 'building:part') { // avoid a ':' in the class name\n k = 'building_part';\n }\n\n primary = k;\n if (statuses.indexOf(v) !== -1) { // e.g. `railway=abandoned`\n status = v;\n classes.push('tag-' + k);\n } else {\n classes.push('tag-' + k);\n classes.push('tag-' + k + '-' + v);\n }\n\n break;\n }\n\n if (!primary) {\n for (i = 0; i < statuses.length; i++) {\n for (j = 0; j < primaries.length; j++) {\n k = statuses[i] + ':' + primaries[j]; // e.g. `demolished:building=yes`\n v = t[k];\n if (!v || v === 'no') continue;\n\n status = statuses[i];\n break;\n }\n }\n }\n\n // add at most one status tag, only if relates to primary tag..\n if (!status) {\n for (i = 0; i < statuses.length; i++) {\n k = statuses[i];\n v = t[k];\n if (!v || v === 'no') continue;\n\n if (v === 'yes') { // e.g. `railway=rail + abandoned=yes`\n status = k;\n } else if (primary && primary === v) { // e.g. `railway=rail + abandoned=railway`\n status = k;\n } else if (!primary && primaries.indexOf(v) !== -1) { // e.g. `abandoned=railway`\n status = k;\n primary = v;\n classes.push('tag-' + v);\n } // else ignore e.g. `highway=path + abandoned=railway`\n\n if (status) break;\n }\n }\n\n if (status) {\n classes.push('tag-status');\n classes.push('tag-status-' + status);\n }\n\n // add any secondary tags\n for (i = 0; i < secondaries.length; i++) {\n k = secondaries[i];\n v = t[k];\n if (!v || v === 'no' || k === primary) continue;\n classes.push('tag-' + k);\n classes.push('tag-' + k + '-' + v);\n }\n\n // For highways, look for surface tagging..\n if ((primary === 'highway' && !osmPathHighwayTagValues[t.highway]) || primary === 'aeroway') {\n var surface = t.highway === 'track' ? 'unpaved' : 'paved';\n for (k in t) {\n v = t[k];\n if (k in osmPavedTags) {\n surface = osmPavedTags[k][v] ? 'paved' : 'unpaved';\n }\n if (k in osmSemipavedTags && !!osmSemipavedTags[k][v]) {\n surface = 'semipaved';\n }\n }\n classes.push('tag-' + surface);\n }\n\n // If this is a wikidata-tagged item, add a class for that..\n var qid = (\n t.wikidata ||\n t['flag:wikidata'] ||\n t['brand:wikidata'] ||\n t['network:wikidata'] ||\n t['operator:wikidata']\n );\n\n if (qid) {\n classes.push('tag-wikidata');\n }\n\n // ensure that classes for tags keys/values with special characters like spaces\n // are not added to the DOM, because it can cause bizarre issues (#9448)\n return classes\n .filter(klass => /^[-_a-z0-9]+$/.test(klass))\n .join(' ')\n .trim();\n };\n\n\n tagClasses.tags = function(val) {\n if (!arguments.length) return _tags;\n _tags = val;\n return tagClasses;\n };\n\n return tagClasses;\n}\n", "// Patterns only work in Firefox when set directly on element.\n// (This is not a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=750632)\nvar patterns = {\n // tag - pattern name\n // -or-\n // tag - value - pattern name\n // -or-\n // tag - value - rules (optional tag-values, pattern name)\n // (matches earlier rules first, so fallback should be last entry)\n amenity: {\n grave_yard: 'cemetery',\n fountain: 'water_standing'\n },\n landuse: {\n cemetery: [\n { religion: 'christian', pattern: 'cemetery_christian' },\n { religion: 'buddhist', pattern: 'cemetery_buddhist' },\n { religion: 'muslim', pattern: 'cemetery_muslim' },\n { religion: 'jewish', pattern: 'cemetery_jewish' },\n { pattern: 'cemetery' }\n ],\n construction: 'construction',\n farmland: 'farmland',\n farmyard: 'farmyard',\n forest: [\n { leaf_type: 'broadleaved', pattern: 'forest_broadleaved' },\n { leaf_type: 'needleleaved', pattern: 'forest_needleleaved' },\n { leaf_type: 'leafless', pattern: 'forest_leafless' },\n { pattern: 'forest' } // same as 'leaf_type:mixed'\n ],\n grave_yard: 'cemetery',\n grass: 'grass',\n landfill: 'landfill',\n meadow: 'meadow',\n military: 'construction',\n orchard: 'orchard',\n quarry: 'quarry',\n vineyard: 'vineyard'\n },\n leisure: {\n horse_riding: 'farmyard'\n },\n natural: {\n beach: 'beach',\n grassland: 'grass',\n sand: 'beach',\n scrub: 'scrub',\n water: [\n { water: 'pond', pattern: 'pond' },\n { water: 'reservoir', pattern: 'water_standing' },\n { pattern: 'waves' }\n ],\n wetland: [\n { wetland: 'marsh', pattern: 'wetland_marsh' },\n { wetland: 'swamp', pattern: 'wetland_swamp' },\n { wetland: 'bog', pattern: 'wetland_bog' },\n { wetland: 'reedbed', pattern: 'wetland_reedbed' },\n { pattern: 'wetland' }\n ],\n wood: [\n { leaf_type: 'broadleaved', pattern: 'forest_broadleaved' },\n { leaf_type: 'needleleaved', pattern: 'forest_needleleaved' },\n { leaf_type: 'leafless', pattern: 'forest_leafless' },\n { pattern: 'forest' } // same as 'leaf_type:mixed'\n ]\n },\n golf: {\n green: 'golf_green',\n tee: 'grass',\n fairway: 'grass',\n rough: 'scrub'\n },\n surface: {\n grass: 'grass',\n sand: 'beach'\n }\n};\n\nexport function svgTagPattern(tags) {\n // Skip pattern filling if this is a building (buildings don't get patterns applied)\n if (tags.building && tags.building !== 'no') {\n return null;\n }\n\n for (var tag in patterns) {\n var entityValue = tags[tag];\n if (!entityValue) continue;\n\n if (typeof patterns[tag] === 'string') { // extra short syntax (just tag) - pattern name\n return 'pattern-' + patterns[tag];\n } else {\n var values = patterns[tag];\n for (var value in values) {\n if (entityValue !== value) continue;\n\n var rules = values[value];\n if (typeof rules === 'string') { // short syntax - pattern name\n return 'pattern-' + rules;\n }\n\n // long syntax - rule array\n for (var ruleKey in rules) {\n var rule = rules[ruleKey];\n\n var pass = true;\n for (var criterion in rule) {\n if (criterion !== 'pattern') { // reserved for pattern name\n // The only rule is a required tag-value pair\n var v = tags[criterion];\n if (!v || v !== rule[criterion]) {\n pass = false;\n break;\n }\n }\n }\n\n if (pass) {\n return 'pattern-' + rule.pattern;\n }\n }\n }\n }\n }\n\n return null;\n}\n", "import { deepEqual } from 'fast-equals';\nimport { bisector as d3_bisector } from 'd3-array';\n\nimport { osmEntity } from '../osm';\nimport { svgPath, svgSegmentWay } from './helpers';\nimport { svgTagClasses } from './tag_classes';\nimport { svgTagPattern } from './tag_pattern';\n\nexport function svgAreas(projection, context) {\n\n\n function getPatternStyle(tags) {\n var imageID = svgTagPattern(tags);\n if (imageID) {\n return 'url(\"#ideditor-' + imageID + '\")';\n }\n return '';\n }\n\n\n function drawTargets(selection, graph, entities, filter) {\n var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';\n var getPath = svgPath(projection).geojson;\n var activeID = context.activeID();\n var base = context.history().base();\n\n // The targets and nopes will be MultiLineString sub-segments of the ways\n var data = { targets: [], nopes: [] };\n\n entities.forEach(function(way) {\n var features = svgSegmentWay(way, graph, activeID);\n data.targets.push.apply(data.targets, features.passive);\n data.nopes.push.apply(data.nopes, features.active);\n });\n\n\n // Targets allow hover and vertex snapping\n var targetData = data.targets.filter(getPath);\n var targets = selection.selectAll('.area.target-allowed')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(targetData, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n var segmentWasEdited = function(d) {\n var wayID = d.properties.entity.id;\n // if the whole line was edited, don't draw segment changes\n if (!base.entities[wayID] ||\n !deepEqual(graph.entities[wayID].nodes, base.entities[wayID].nodes)) {\n return false;\n }\n return d.properties.nodes.some(function(n) {\n return !base.entities[n.id] ||\n !deepEqual(graph.entities[n.id].loc, base.entities[n.id].loc);\n });\n };\n\n // enter/update\n targets.enter()\n .append('path')\n .merge(targets)\n .attr('d', getPath)\n .attr('class', function(d) { return 'way area target target-allowed ' + targetClass + d.id; })\n .classed('segment-edited', segmentWasEdited);\n\n\n // NOPE\n var nopeData = data.nopes.filter(getPath);\n var nopes = selection.selectAll('.area.target-nope')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(nopeData, function key(d) { return d.id; });\n\n // exit\n nopes.exit()\n .remove();\n\n // enter/update\n nopes.enter()\n .append('path')\n .merge(nopes)\n .attr('d', getPath)\n .attr('class', function(d) { return 'way area target target-nope ' + nopeClass + d.id; })\n .classed('segment-edited', segmentWasEdited);\n }\n\n\n function drawAreas(selection, graph, entities, filter) {\n var path = svgPath(projection, graph, true);\n var areas = {};\n var base = context.history().base();\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n if (entity.geometry(graph) !== 'area') continue;\n if (!areas[entity.id]) {\n areas[entity.id] = {\n entity: entity,\n area: Math.abs(entity.area(graph))\n };\n }\n }\n\n var fills = Object.values(areas).filter(function hasPath(a) { return path(a.entity); });\n fills.sort(function areaSort(a, b) { return b.area - a.area; });\n fills = fills.map(function(a) { return a.entity; });\n\n var strokes = fills.filter(function(area) { return area.type === 'way'; });\n\n var data = {\n clip: fills,\n shadow: strokes,\n stroke: strokes,\n fill: fills\n };\n\n var clipPaths = context.surface().selectAll('defs').selectAll('.clipPath-osm')\n .filter(filter)\n .data(data.clip, osmEntity.key);\n\n clipPaths.exit()\n .remove();\n\n var clipPathsEnter = clipPaths.enter()\n .append('clipPath')\n .attr('class', 'clipPath-osm')\n .attr('id', function(entity) { return 'ideditor-' + entity.id + '-clippath'; });\n\n clipPathsEnter\n .append('path');\n\n clipPaths.merge(clipPathsEnter)\n .selectAll('path')\n .attr('d', path);\n\n\n var drawLayer = selection.selectAll('.layer-osm.areas');\n var touchLayer = selection.selectAll('.layer-touch.areas');\n\n // Draw areas..\n var areagroup = drawLayer\n .selectAll('g.areagroup')\n .data(['fill', 'shadow', 'stroke']);\n\n areagroup = areagroup.enter()\n .append('g')\n .attr('class', function(d) { return 'areagroup area-' + d; })\n .merge(areagroup);\n\n var paths = areagroup\n .selectAll('path')\n .filter(filter)\n .data(function(layer) { return data[layer]; }, osmEntity.key);\n\n paths.exit()\n .remove();\n\n\n var fillpaths = selection.selectAll('.area-fill path.area').nodes();\n var bisect = d3_bisector(function(node) { return -node.__data__.area(graph); }).left;\n\n function sortedByArea(entity) {\n if (this._parent.__data__ === 'fill') {\n return fillpaths[bisect(fillpaths, -entity.area(graph))];\n }\n }\n\n paths.enter()\n .insert('path', sortedByArea)\n .merge(paths)\n .each(function(entity) {\n var layer = this.parentNode.__data__;\n this.setAttribute('class', entity.type + ' area ' + layer + ' ' + entity.id);\n\n if (layer === 'fill') {\n this.setAttribute('clip-path', 'url(#ideditor-' + entity.id + '-clippath)');\n this.style.fill = getPatternStyle(entity.tags);\n this.style.stroke = this.style.fill;\n }\n })\n .classed('added', function(d) {\n return !base.entities[d.id];\n })\n .classed('geometry-edited', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].nodes, base.entities[d.id].nodes);\n })\n .classed('retagged', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses())\n .attr('d', path);\n\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, data.stroke, filter);\n }\n\n return drawAreas;\n}\n", "import type { Feature, Geometry } from \"geojson\";\n\nexport function $(element: Element | Document, tagName: string): Element[] {\n return Array.from(element.getElementsByTagName(tagName));\n}\n\nexport type P = NonNullable;\nexport type F = Feature;\n\nexport type StyleMap = { [key: string]: P };\n\nexport type NS = [string, string][];\n\nexport function normalizeId(id: string) {\n return id[0] === \"#\" ? id : `#${id}`;\n}\n\nexport function $ns(\n element: Element | Document,\n tagName: string,\n ns: string\n): Element[] {\n return Array.from(element.getElementsByTagNameNS(ns, tagName));\n}\n\n/**\n * get the content of a text node, if any\n */\nexport function nodeVal(node: Element | null) {\n node?.normalize();\n return node?.textContent || \"\";\n}\n\n/**\n * Get one Y child of X, if any, otherwise null\n */\nexport function get1(\n node: Element,\n tagName: string,\n callback?: (elem: Element) => unknown\n) {\n const n = node.getElementsByTagName(tagName);\n const result = n.length ? n[0] : null;\n if (result && callback) callback(result);\n return result;\n}\n\nexport function get(\n node: Element | null,\n tagName: string,\n callback?: (elem: Element, properties: P) => P\n) {\n const properties: Feature[\"properties\"] = {};\n if (!node) return properties;\n const n = node.getElementsByTagName(tagName);\n const result = n.length ? n[0] : null;\n if (result && callback) {\n return callback(result, properties);\n }\n return properties;\n}\n\nexport function val1(\n node: Element,\n tagName: string,\n callback: (val: string) => P | undefined | void\n): P {\n const val = nodeVal(get1(node, tagName));\n if (val && callback) return callback(val) || {};\n return {};\n}\n\nexport function $num(\n node: Element,\n tagName: string,\n callback: (val: number) => Feature[\"properties\"]\n) {\n const val = Number.parseFloat(nodeVal(get1(node, tagName)));\n if (Number.isNaN(val)) return undefined;\n if (val && callback) return callback(val) || {};\n return {};\n}\n\nexport function num1(\n node: Element,\n tagName: string,\n callback?: (val: number) => unknown\n) {\n const val = Number.parseFloat(nodeVal(get1(node, tagName)));\n if (Number.isNaN(val)) return undefined;\n if (callback) callback(val);\n return val;\n}\n\nexport function getMulti(node: Element, propertyNames: string[]): P {\n const properties: P = {};\n for (const property of propertyNames) {\n val1(node, property, (val) => {\n properties[property] = val;\n });\n }\n return properties;\n}\n\nexport function isElement(node: Node | null): node is Element {\n return node?.nodeType === 1;\n}\n", "import { isElement, nodeVal } from \"../shared\";\n\nexport type ExtendedValues = [string, string | number][];\n\nexport function getExtensions(node: Element | null): ExtendedValues {\n let values: [string, string | number][] = [];\n if (node === null) return values;\n for (const child of Array.from(node.childNodes)) {\n if (!isElement(child)) continue;\n const name = abbreviateName(child.nodeName);\n if (name === \"gpxtpx:TrackPointExtension\") {\n // loop again for nested garmin extensions (eg. \"gpxtpx:hr\")\n values = values.concat(getExtensions(child));\n } else {\n // push custom extension (eg. \"power\")\n const val = nodeVal(child);\n values.push([name, parseNumeric(val)]);\n }\n }\n return values;\n}\n\nfunction abbreviateName(name: string) {\n return [\"heart\", \"gpxtpx:hr\", \"hr\"].includes(name) ? \"heart\" : name;\n}\n\nfunction parseNumeric(val: string) {\n const num = Number.parseFloat(val);\n return Number.isNaN(num) ? val : num;\n}\n", "import type { Position } from \"geojson\";\nimport { get1, nodeVal, num1 } from \"../shared\";\nimport { type ExtendedValues, getExtensions } from \"./extensions\";\n\ninterface CoordPair {\n coordinates: Position;\n time: string | null;\n extendedValues: ExtendedValues;\n}\n\nexport function coordPair(node: Element): CoordPair | null {\n const ll = [\n Number.parseFloat(node.getAttribute(\"lon\") || \"\"),\n Number.parseFloat(node.getAttribute(\"lat\") || \"\"),\n ];\n\n if (Number.isNaN(ll[0]) || Number.isNaN(ll[1])) {\n return null;\n }\n\n num1(node, \"ele\", (val) => {\n ll.push(val);\n });\n\n const time = get1(node, \"time\");\n return {\n coordinates: ll,\n time: time ? nodeVal(time) : null,\n extendedValues: getExtensions(get1(node, \"extensions\")),\n };\n}\n", "import { $num, type P, get, val1 } from \"../shared\";\n\nexport function getLineStyle(node: Element | null) {\n return get(node, \"line\", (lineStyle) => {\n const val: P = Object.assign(\n {},\n val1(lineStyle, \"color\", (color) => {\n return { stroke: `#${color}` };\n }),\n $num(lineStyle, \"opacity\", (opacity) => {\n return { \"stroke-opacity\": opacity };\n }),\n $num(lineStyle, \"width\", (width) => {\n // GPX width is in mm, convert to px with 96 px per inch\n return { \"stroke-width\": (width * 96) / 25.4 };\n })\n );\n return val;\n });\n}\n", "import { $, type NS, getMulti, nodeVal } from \"../shared\";\n\nexport function extractProperties(ns: NS, node: Element) {\n const properties = getMulti(node, [\n \"name\",\n \"cmt\",\n \"desc\",\n \"type\",\n \"time\",\n \"keywords\",\n ]);\n\n for (const [n, url] of ns) {\n for (const child of Array.from(node.getElementsByTagNameNS(url, \"*\"))) {\n properties[child.tagName.replace(\":\", \"_\")] = nodeVal(child)?.trim();\n }\n }\n\n const links = $(node, \"link\");\n if (links.length) {\n properties.links = links.map((link) =>\n Object.assign(\n { href: link.getAttribute(\"href\") },\n getMulti(link, [\"text\", \"type\"])\n )\n );\n }\n\n return properties;\n}\n", "import type { Document as XDocument } from \"@xmldom/xmldom\";\nimport type {\n Feature,\n FeatureCollection,\n LineString,\n MultiLineString,\n Point,\n Position,\n} from \"geojson\";\nimport { coordPair } from \"./gpx/coord_pair\";\nimport { getLineStyle } from \"./gpx/line\";\nimport { extractProperties } from \"./gpx/properties\";\nimport { $, type NS, type P, get1, getMulti } from \"./shared\";\n\n/**\n * Extract points from a trkseg or rte element.\n */\nfunction getPoints(node: Element, pointname: \"trkpt\" | \"rtept\") {\n const pts = $(node, pointname);\n const line: Position[] = [];\n const times = [];\n const extendedValues: P = {};\n\n for (let i = 0; i < pts.length; i++) {\n const c = coordPair(pts[i]);\n if (!c) {\n continue;\n }\n line.push(c.coordinates);\n if (c.time) times.push(c.time);\n for (const [name, val] of c.extendedValues) {\n const plural =\n name === \"heart\" ? name : `${name.replace(\"gpxtpx:\", \"\")}s`;\n if (!extendedValues[plural]) {\n extendedValues[plural] = Array(pts.length).fill(null);\n }\n extendedValues[plural][i] = val;\n }\n }\n\n if (line.length < 2) return; // Invalid line in GeoJSON\n\n return {\n line: line,\n times: times,\n extendedValues: extendedValues,\n };\n}\n\n/**\n * Extract a LineString geometry from a rte\n * element.\n */\nfunction getRoute(ns: NS, node: Element): Feature | undefined {\n const line = getPoints(node, \"rtept\");\n if (!line) return;\n return {\n type: \"Feature\",\n properties: Object.assign(\n { _gpxType: \"rte\" },\n extractProperties(ns, node),\n getLineStyle(get1(node, \"extensions\"))\n ),\n geometry: {\n type: \"LineString\",\n coordinates: line.line,\n },\n };\n}\n\nfunction getTrack(\n ns: NS,\n node: Element\n): Feature | null {\n const segments = $(node, \"trkseg\");\n const track = [];\n const times = [];\n const extractedLines = [];\n\n for (const segment of segments) {\n const line = getPoints(segment, \"trkpt\");\n if (line) {\n extractedLines.push(line);\n if (line.times?.length) times.push(line.times);\n }\n }\n\n if (extractedLines.length === 0) return null;\n\n const multi = extractedLines.length > 1;\n\n const properties: Feature[\"properties\"] = Object.assign(\n { _gpxType: \"trk\" },\n extractProperties(ns, node),\n getLineStyle(get1(node, \"extensions\")),\n times.length\n ? {\n coordinateProperties: {\n times: multi ? times : times[0],\n },\n }\n : {}\n );\n\n for (let i = 0; i < extractedLines.length; i++) {\n const line = extractedLines[i];\n track.push(line.line);\n if (!properties.coordinateProperties) {\n properties.coordinateProperties = {};\n }\n const props = properties.coordinateProperties;\n // Generally extendedValues will be things like heart\n // rate, and this is an array like { heart: [100, 101...] }\n for (const [name, val] of Object.entries(line.extendedValues)) {\n if (multi) {\n if (!props[name]) {\n props[name] = extractedLines.map((line) =>\n new Array(line.line.length).fill(null)\n );\n }\n props[name][i] = val;\n } else {\n props[name] = val;\n }\n }\n }\n\n return {\n type: \"Feature\",\n properties: properties,\n geometry: multi\n ? {\n type: \"MultiLineString\",\n coordinates: track,\n }\n : {\n type: \"LineString\",\n coordinates: track[0],\n },\n };\n}\n\n/**\n * Extract a point, if possible, from a given node,\n * which is usually a wpt or trkpt\n */\nfunction getPoint(ns: NS, node: Element): Feature | null {\n const properties: Feature[\"properties\"] = Object.assign(\n extractProperties(ns, node),\n getMulti(node, [\"sym\"])\n );\n const pair = coordPair(node);\n if (!pair) return null;\n return {\n type: \"Feature\",\n properties,\n geometry: {\n type: \"Point\",\n coordinates: pair.coordinates,\n },\n };\n}\n\n/**\n * Convert GPX to GeoJSON incrementally, returning\n * a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)\n * that yields output feature by feature.\n */\nexport function* gpxGen(node: Document | XDocument): Generator {\n const n = node as Document;\n const GPXX = \"gpxx\";\n const GPXX_URI = \"http://www.garmin.com/xmlschemas/GpxExtensions/v3\";\n // Namespaces\n const ns: NS = [[GPXX, GPXX_URI]];\n const attrs = n.getElementsByTagName(\"gpx\")[0]?.attributes;\n if (attrs) {\n for (const attr of Array.from(attrs)) {\n if (attr.name?.startsWith(\"xmlns:\") && attr.value !== GPXX_URI) {\n ns.push([attr.name, attr.value]);\n }\n }\n }\n\n for (const track of $(n, \"trk\")) {\n const feature = getTrack(ns, track);\n if (feature) yield feature;\n }\n\n for (const route of $(n, \"rte\")) {\n const feature = getRoute(ns, route);\n if (feature) yield feature;\n }\n\n for (const waypoint of $(n, \"wpt\")) {\n const point = getPoint(ns, waypoint);\n if (point) yield point;\n }\n}\n\n/**\n *\n * Convert a GPX document to GeoJSON. The first argument, `doc`, must be a GPX\n * document as an XML DOM - not as a string. You can get this using jQuery's default\n * `.ajax` function or using a bare XMLHttpRequest with the `.response` property\n * holding an XML DOM.\n *\n * The output is a JavaScript object of GeoJSON data, same as `.kml` outputs, with the\n * addition of a `_gpxType` property on each `LineString` feature that indicates whether\n * the feature was encoded as a route (`rte`) or track (`trk`) in the GPX document.\n */\nexport function gpx(node: Document | XDocument): FeatureCollection {\n return {\n type: \"FeatureCollection\",\n features: Array.from(gpxGen(node)),\n };\n}\n", "import type { Document as XDocument } from \"@xmldom/xmldom\";\nimport type { Feature, FeatureCollection, Position } from \"geojson\";\nimport { $, type P, get, get1, nodeVal, num1 } from \"./shared\";\n\ntype PropertyMapping = readonly [string, string][];\n\nconst EXTENSIONS_NS = \"http://www.garmin.com/xmlschemas/ActivityExtension/v2\";\n\nconst TRACKPOINT_ATTRIBUTES: PropertyMapping = [\n [\"heartRate\", \"heartRates\"],\n [\"Cadence\", \"cadences\"],\n // Extended Trackpoint attributes\n [\"Speed\", \"speeds\"],\n [\"Watts\", \"watts\"],\n];\n\nconst LAP_ATTRIBUTES: PropertyMapping = [\n [\"TotalTimeSeconds\", \"totalTimeSeconds\"],\n [\"DistanceMeters\", \"distanceMeters\"],\n [\"MaximumSpeed\", \"maxSpeed\"],\n [\"AverageHeartRateBpm\", \"avgHeartRate\"],\n [\"MaximumHeartRateBpm\", \"maxHeartRate\"],\n\n // Extended Lap attributes\n [\"AvgSpeed\", \"avgSpeed\"],\n [\"AvgWatts\", \"avgWatts\"],\n [\"MaxWatts\", \"maxWatts\"],\n];\n\nfunction getProperties(node: Element, attributeNames: PropertyMapping) {\n const properties = [];\n\n for (const [tag, alias] of attributeNames) {\n let elem = get1(node, tag);\n if (!elem) {\n const elements = node.getElementsByTagNameNS(EXTENSIONS_NS, tag);\n if (elements.length) {\n elem = elements[0];\n }\n }\n const val = Number.parseFloat(nodeVal(elem));\n if (!Number.isNaN(val)) {\n properties.push([alias, val]);\n }\n }\n\n return properties;\n}\n\nfunction coordPair(node: Element) {\n const ll = [num1(node, \"LongitudeDegrees\"), num1(node, \"LatitudeDegrees\")];\n if (\n ll[0] === undefined ||\n Number.isNaN(ll[0]) ||\n ll[1] === undefined ||\n Number.isNaN(ll[1])\n ) {\n return null;\n }\n const heartRate = get1(node, \"HeartRateBpm\");\n const time = nodeVal(get1(node, \"Time\"));\n get1(node, \"AltitudeMeters\", (alt) => {\n const a = Number.parseFloat(nodeVal(alt));\n if (!Number.isNaN(a)) {\n ll.push(a);\n }\n });\n return {\n coordinates: ll as number[],\n time: time || null,\n heartRate: heartRate ? Number.parseFloat(nodeVal(heartRate)) : null,\n extensions: getProperties(node, TRACKPOINT_ATTRIBUTES),\n };\n}\n\nfunction getPoints(node: Element) {\n const pts = $(node, \"Trackpoint\");\n const line: Position[] = [];\n const times = [];\n const heartRates = [];\n if (pts.length < 2) return null; // Invalid line in GeoJSON\n const extendedProperties: P = {};\n const result = { extendedProperties };\n for (let i = 0; i < pts.length; i++) {\n const c = coordPair(pts[i]);\n if (c === null) continue;\n line.push(c.coordinates);\n const { time, heartRate, extensions } = c;\n if (time) times.push(time);\n if (heartRate) heartRates.push(heartRate);\n for (const [alias, value] of extensions) {\n if (!extendedProperties[alias]) {\n extendedProperties[alias] = Array(pts.length).fill(null);\n }\n extendedProperties[alias][i] = value;\n }\n }\n if (line.length < 2) return null;\n return Object.assign(result, {\n line: line,\n times: times,\n heartRates: heartRates,\n });\n}\n\nfunction getLap(node: Element): Feature | null {\n const segments = $(node, \"Track\");\n const track = [];\n const times = [];\n const heartRates = [];\n const allExtendedProperties = [];\n let line: any;\n const properties: P = Object.assign(\n Object.fromEntries(getProperties(node, LAP_ATTRIBUTES)),\n get(node, \"Name\", (nameElement) => {\n return { name: nodeVal(nameElement) };\n })\n );\n\n for (const segment of segments) {\n line = getPoints(segment);\n if (line) {\n track.push(line.line);\n if (line.times.length) times.push(line.times);\n if (line.heartRates.length) heartRates.push(line.heartRates);\n allExtendedProperties.push(line.extendedProperties);\n }\n }\n for (let i = 0; i < allExtendedProperties.length; i++) {\n const extendedProperties = allExtendedProperties[i];\n for (const property in extendedProperties) {\n if (segments.length === 1) {\n if (line) {\n properties[property] = line.extendedProperties[property];\n }\n } else {\n if (!properties[property]) {\n properties[property] = track.map((track) =>\n Array(track.length).fill(null)\n );\n }\n properties[property][i] = extendedProperties[property];\n }\n }\n }\n\n if (track.length === 0) return null;\n\n if (times.length || heartRates.length) {\n properties.coordinateProperties = Object.assign(\n times.length\n ? {\n times: track.length === 1 ? times[0] : times,\n }\n : {},\n heartRates.length\n ? {\n heart: track.length === 1 ? heartRates[0] : heartRates,\n }\n : {}\n );\n }\n\n return {\n type: \"Feature\",\n properties: properties,\n geometry:\n track.length === 1\n ? {\n type: \"LineString\",\n coordinates: track[0],\n }\n : {\n type: \"MultiLineString\",\n coordinates: track,\n },\n };\n}\n\n/**\n * Incrementally convert a TCX document to GeoJSON. The\n * first argument, `doc`, must be a TCX\n * document as an XML DOM - not as a string.\n */\nexport function* tcxGen(node: Document | XDocument): Generator {\n for (const lap of $(node as Document, \"Lap\")) {\n const feature = getLap(lap);\n if (feature) yield feature;\n }\n\n for (const course of $(node as Document, \"Courses\")) {\n const feature = getLap(course);\n if (feature) yield feature;\n }\n}\n\n/**\n * Convert a TCX document to GeoJSON. The first argument, `doc`, must be a TCX\n * document as an XML DOM - not as a string.\n */\nexport function tcx(node: Document | XDocument): FeatureCollection {\n return {\n type: \"FeatureCollection\",\n features: Array.from(tcxGen(node)),\n };\n}\n", "import type { P } from \"../shared\";\n\nexport function fixColor(v: string, prefix: string): P {\n const properties: P = {};\n const colorProp =\n prefix === \"stroke\" || prefix === \"fill\" ? prefix : `${prefix}-color`;\n if (v[0] === \"#\") {\n v = v.substring(1);\n }\n if (v.length === 6 || v.length === 3) {\n properties[colorProp] = `#${v}`;\n } else if (v.length === 8) {\n properties[`${prefix}-opacity`] =\n Number.parseInt(v.substring(0, 2), 16) / 255;\n properties[colorProp] =\n `#${v.substring(6, 8)}${v.substring(4, 6)}${v.substring(2, 4)}`;\n }\n return properties;\n}\n", "import { type P, get, nodeVal, num1, val1 } from \"../shared\";\nimport { fixColor } from \"./fixColor\";\n\nfunction numericProperty(node: Element, source: string, target: string): P {\n const properties: P = {};\n num1(node, source, (val) => {\n properties[target] = val;\n });\n return properties;\n}\n\nfunction getColor(node: Element, output: string): P {\n return get(node, \"color\", (elem) => fixColor(nodeVal(elem), output));\n}\n\nexport function extractIconHref(node: Element) {\n return get(node, \"Icon\", (icon, properties) => {\n val1(icon, \"href\", (href) => {\n properties.icon = href;\n });\n return properties;\n });\n}\n\nexport function extractIcon(node: Element) {\n return get(node, \"IconStyle\", (iconStyle) => {\n return Object.assign(\n getColor(iconStyle, \"icon\"),\n numericProperty(iconStyle, \"scale\", \"icon-scale\"),\n numericProperty(iconStyle, \"heading\", \"icon-heading\"),\n get(iconStyle, \"hotSpot\", (hotspot) => {\n const left = Number.parseFloat(hotspot.getAttribute(\"x\") || \"\");\n const top = Number.parseFloat(hotspot.getAttribute(\"y\") || \"\");\n const xunits = hotspot.getAttribute(\"xunits\") || \"\";\n const yunits = hotspot.getAttribute(\"yunits\") || \"\";\n if (!Number.isNaN(left) && !Number.isNaN(top))\n return {\n \"icon-offset\": [left, top],\n \"icon-offset-units\": [xunits, yunits],\n };\n return {};\n }),\n extractIconHref(iconStyle)\n );\n });\n}\n\nexport function extractLabel(node: Element) {\n return get(node, \"LabelStyle\", (labelStyle) => {\n return Object.assign(\n getColor(labelStyle, \"label\"),\n numericProperty(labelStyle, \"scale\", \"label-scale\")\n );\n });\n}\n\nexport function extractLine(node: Element) {\n return get(node, \"LineStyle\", (lineStyle) => {\n return Object.assign(\n getColor(lineStyle, \"stroke\"),\n numericProperty(lineStyle, \"width\", \"stroke-width\")\n );\n });\n}\n\nexport function extractPoly(node: Element) {\n return get(node, \"PolyStyle\", (polyStyle, properties) => {\n return Object.assign(\n properties,\n get(polyStyle, \"color\", (elem) => fixColor(nodeVal(elem), \"fill\")),\n val1(polyStyle, \"fill\", (fill) => {\n if (fill === \"0\") return { \"fill-opacity\": 0 };\n }),\n val1(polyStyle, \"outline\", (outline) => {\n if (outline === \"0\") return { \"stroke-opacity\": 0 };\n })\n );\n });\n}\n\nexport function extractStyle(node: Element) {\n return Object.assign(\n {},\n extractPoly(node),\n extractLine(node),\n extractLabel(node),\n extractIcon(node)\n );\n}\n", "import type { Geometry, LineString, Point, Position } from \"geojson\";\nimport { $, $ns, get1, isElement, nodeVal } from \"../shared\";\n\nconst removeSpace = /\\s*/g;\nconst trimSpace = /^\\s*|\\s*$/g;\nconst splitSpace = /\\s+/;\n\n/**\n * Get one coordinate from a coordinate array, if any\n */\nexport function coord1(value: string): Position {\n return value\n .replace(removeSpace, \"\")\n .split(\",\")\n .map(Number.parseFloat)\n .filter((num) => !Number.isNaN(num))\n .slice(0, 3);\n}\n\n/**\n * Get all coordinates from a coordinate array as [[],[]]\n */\nexport function coord(value: string): Position[] {\n return value\n .replace(trimSpace, \"\")\n .split(splitSpace)\n .map(coord1)\n .filter((coord) => {\n return coord.length >= 2;\n });\n}\n\nfunction gxCoords(\n node: Element\n): { geometry: Point | LineString; times: string[] } | null {\n let elems = $(node, \"coord\");\n if (elems.length === 0) {\n elems = $ns(node, \"coord\", \"*\");\n }\n\n const coordinates = elems.map((elem) => {\n return nodeVal(elem).split(\" \").map(Number.parseFloat);\n });\n\n if (coordinates.length === 0) {\n return null;\n }\n\n return {\n geometry:\n coordinates.length > 2\n ? {\n type: \"LineString\",\n coordinates,\n }\n : {\n type: \"Point\",\n coordinates: coordinates[0],\n },\n times: $(node, \"when\").map((elem) => nodeVal(elem)),\n };\n}\n\nexport function fixRing(ring: Position[]) {\n if (ring.length === 0) return ring;\n const first = ring[0];\n const last = ring[ring.length - 1];\n let equal = true;\n for (let i = 0; i < Math.max(first.length, last.length); i++) {\n if (first[i] !== last[i]) {\n equal = false;\n break;\n }\n }\n if (!equal) {\n return ring.concat([ring[0]]);\n }\n return ring;\n}\n\nexport function getCoordinates(node: Element) {\n return nodeVal(get1(node, \"coordinates\"));\n}\n\ninterface GeometriesAndTimes {\n geometries: Geometry[];\n coordTimes: string[][];\n}\n\nexport function getGeometry(node: Element): GeometriesAndTimes {\n let geometries: Geometry[] = [];\n let coordTimes: string[][] = [];\n\n for (let i = 0; i < node.childNodes.length; i++) {\n const child = node.childNodes.item(i);\n if (isElement(child)) {\n switch (child.tagName) {\n case \"MultiGeometry\":\n case \"MultiTrack\":\n case \"gx:MultiTrack\": {\n const childGeometries = getGeometry(child);\n geometries = geometries.concat(childGeometries.geometries);\n coordTimes = coordTimes.concat(childGeometries.coordTimes);\n break;\n }\n\n case \"Point\": {\n const coordinates = coord1(getCoordinates(child));\n if (coordinates.length >= 2) {\n geometries.push({\n type: \"Point\",\n coordinates,\n });\n }\n break;\n }\n case \"LinearRing\":\n case \"LineString\": {\n const coordinates = coord(getCoordinates(child));\n if (coordinates.length >= 2) {\n geometries.push({\n type: \"LineString\",\n coordinates,\n });\n }\n break;\n }\n case \"Polygon\": {\n const coords = [];\n for (const linearRing of $(child, \"LinearRing\")) {\n const ring = fixRing(coord(getCoordinates(linearRing)));\n if (ring.length >= 4) {\n coords.push(ring);\n }\n }\n if (coords.length) {\n geometries.push({\n type: \"Polygon\",\n coordinates: coords,\n });\n }\n break;\n }\n case \"Track\":\n case \"gx:Track\": {\n const gx = gxCoords(child);\n if (!gx) break;\n const { times, geometry } = gx;\n geometries.push(geometry);\n if (times.length) coordTimes.push(times);\n break;\n }\n }\n }\n }\n\n return {\n geometries,\n coordTimes,\n };\n}\n", "import {\n $,\n type P,\n type StyleMap,\n get,\n get1,\n nodeVal,\n normalizeId,\n val1,\n} from \"../shared\";\n\nexport type TypeConverter = (x: string) => unknown;\nexport type Schema = { [key: string]: TypeConverter };\n\nconst toNumber: TypeConverter = (x) => Number(x);\nexport const typeConverters: Record = {\n string: (x) => x,\n int: toNumber,\n uint: toNumber,\n short: toNumber,\n ushort: toNumber,\n float: toNumber,\n double: toNumber,\n bool: (x) => Boolean(x),\n};\n\nexport function extractExtendedData(node: Element, schema: Schema) {\n return get(node, \"ExtendedData\", (extendedData, properties) => {\n for (const data of $(extendedData, \"Data\")) {\n properties[data.getAttribute(\"name\") || \"\"] = nodeVal(\n get1(data, \"value\")\n );\n }\n for (const simpleData of $(extendedData, \"SimpleData\")) {\n const name = simpleData.getAttribute(\"name\") || \"\";\n const typeConverter = schema[name] || typeConverters.string;\n properties[name] = typeConverter(nodeVal(simpleData));\n }\n return properties;\n });\n}\n\nexport function getMaybeHTMLDescription(node: Element) {\n const descriptionNode = get1(node, \"description\");\n for (const c of Array.from(descriptionNode?.childNodes || [])) {\n if (c.nodeType === 4) {\n return {\n description: {\n \"@type\": \"html\",\n value: nodeVal(c as Element),\n },\n };\n }\n }\n return {};\n}\n\nexport function extractTimeSpan(node: Element): P {\n return get(node, \"TimeSpan\", (timeSpan) => {\n return {\n timespan: {\n begin: nodeVal(get1(timeSpan, \"begin\")),\n end: nodeVal(get1(timeSpan, \"end\")),\n },\n };\n });\n}\n\nexport function extractTimeStamp(node: Element): P {\n return get(node, \"TimeStamp\", (timeStamp) => {\n return { timestamp: nodeVal(get1(timeStamp, \"when\")) };\n });\n}\n\nexport function extractCascadedStyle(node: Element, styleMap: StyleMap): P {\n return val1(node, \"styleUrl\", (styleUrl) => {\n styleUrl = normalizeId(styleUrl);\n if (styleMap[styleUrl]) {\n return Object.assign({ styleUrl }, styleMap[styleUrl]);\n }\n // For backward-compatibility. Should we still include\n // styleUrl even if it's not resolved?\n return { styleUrl };\n });\n}\n\nexport enum AltitudeMode {\n ABSOLUTE = \"absolute\",\n RELATIVE_TO_GROUND = \"relativeToGround\",\n CLAMP_TO_GROUND = \"clampToGround\",\n CLAMP_TO_SEAFLOOR = \"clampToSeaFloor\",\n RELATIVE_TO_SEAFLOOR = \"relativeToSeaFloor\",\n}\n\nexport function processAltitudeMode(mode: Element | null): AltitudeMode | null {\n switch (mode?.textContent) {\n case AltitudeMode.ABSOLUTE:\n return AltitudeMode.ABSOLUTE;\n case AltitudeMode.CLAMP_TO_GROUND:\n return AltitudeMode.CLAMP_TO_GROUND;\n case AltitudeMode.CLAMP_TO_SEAFLOOR:\n return AltitudeMode.CLAMP_TO_SEAFLOOR;\n case AltitudeMode.RELATIVE_TO_GROUND:\n return AltitudeMode.RELATIVE_TO_GROUND;\n case AltitudeMode.RELATIVE_TO_SEAFLOOR:\n return AltitudeMode.RELATIVE_TO_SEAFLOOR;\n default:\n break;\n }\n return null;\n}\n\nexport type BBox = [number, number, number, number];\n", "import type { Feature, Polygon } from \"geojson\";\nimport type { KMLOptions } from \"lib/kml\";\nimport { type StyleMap, get1, getMulti, num1 } from \"../shared\";\nimport { extractIconHref, extractStyle } from \"./extractStyle\";\nimport { coord, fixRing, getCoordinates } from \"./geometry\";\nimport {\n type BBox,\n type Schema,\n extractCascadedStyle,\n extractExtendedData,\n extractTimeSpan,\n extractTimeStamp,\n getMaybeHTMLDescription,\n} from \"./shared\";\n\ninterface BoxGeometry {\n bbox?: BBox;\n geometry: Polygon;\n}\n\nfunction getGroundOverlayBox(node: Element): BoxGeometry | null {\n const latLonQuad = get1(node, \"gx:LatLonQuad\");\n\n if (latLonQuad) {\n const ring = fixRing(coord(getCoordinates(node)));\n return {\n geometry: {\n type: \"Polygon\",\n coordinates: [ring],\n },\n };\n }\n\n return getLatLonBox(node);\n}\n\nconst DEGREES_TO_RADIANS = Math.PI / 180;\n\nfunction rotateBox(\n bbox: BBox,\n coordinates: Polygon[\"coordinates\"],\n rotation: number\n): Polygon[\"coordinates\"] {\n const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];\n\n return [\n coordinates[0].map((coordinate) => {\n const dy = coordinate[1] - center[1];\n const dx = coordinate[0] - center[0];\n const distance = Math.sqrt(dy ** 2 + dx ** 2);\n const angle = Math.atan2(dy, dx) + rotation * DEGREES_TO_RADIANS;\n\n return [\n center[0] + Math.cos(angle) * distance,\n center[1] + Math.sin(angle) * distance,\n ];\n }),\n ];\n}\n\nfunction getLatLonBox(node: Element): BoxGeometry | null {\n const latLonBox = get1(node, \"LatLonBox\");\n\n if (latLonBox) {\n const north = num1(latLonBox, \"north\");\n const west = num1(latLonBox, \"west\");\n const east = num1(latLonBox, \"east\");\n const south = num1(latLonBox, \"south\");\n const rotation = num1(latLonBox, \"rotation\");\n\n if (\n typeof north === \"number\" &&\n typeof south === \"number\" &&\n typeof west === \"number\" &&\n typeof east === \"number\"\n ) {\n const bbox: BBox = [west, south, east, north];\n let coordinates = [\n [\n [west, north], // top left\n [east, north], // top right\n [east, south], // top right\n [west, south], // bottom left\n [west, north], // top left (again)\n ],\n ];\n if (typeof rotation === \"number\") {\n coordinates = rotateBox(bbox, coordinates, rotation);\n }\n return {\n bbox,\n geometry: {\n type: \"Polygon\",\n coordinates,\n },\n };\n }\n }\n\n return null;\n}\n\nexport function getGroundOverlay(\n node: Element,\n styleMap: StyleMap,\n schema: Schema,\n options: KMLOptions\n): Feature | null {\n const box = getGroundOverlayBox(node);\n\n const geometry = box?.geometry || null;\n\n if (!geometry && options.skipNullGeometry) {\n return null;\n }\n\n const feature: Feature = {\n type: \"Feature\",\n geometry,\n properties: Object.assign(\n /**\n * Related to\n * https://gist.github.com/tmcw/037a1cb6660d74a392e9da7446540f46\n */\n { \"@geometry-type\": \"groundoverlay\" },\n getMulti(node, [\n \"name\",\n \"address\",\n \"visibility\",\n \"open\",\n \"phoneNumber\",\n \"description\",\n ]),\n getMaybeHTMLDescription(node),\n extractCascadedStyle(node, styleMap),\n extractStyle(node),\n extractIconHref(node),\n extractExtendedData(node, schema),\n extractTimeSpan(node),\n extractTimeStamp(node)\n ),\n };\n\n if (box?.bbox) {\n feature.bbox = box.bbox;\n }\n\n if (feature.properties?.visibility !== undefined) {\n feature.properties.visibility = feature.properties.visibility !== \"0\";\n }\n\n const id = node.getAttribute(\"id\");\n if (id !== null && id !== \"\") feature.id = id;\n return feature;\n}\n", "import type { Feature, Polygon } from \"geojson\";\nimport type { KMLOptions } from \"lib/kml\";\nimport { type StyleMap, get1, getMulti, num1 } from \"../shared\";\nimport { extractIconHref, extractStyle } from \"./extractStyle\";\nimport {\n AltitudeMode,\n type BBox,\n type Schema,\n extractCascadedStyle,\n extractExtendedData,\n extractTimeSpan,\n extractTimeStamp,\n getMaybeHTMLDescription,\n processAltitudeMode,\n} from \"./shared\";\n\ninterface BoxGeometry {\n bbox?: BBox;\n geometry: Polygon;\n}\n\ntype LOD = [number, number | null, number | null, number | null];\ninterface IRegion {\n coordinateBox: BoxGeometry | null;\n lod: LOD | null;\n}\n\nfunction getNetworkLinkRegion(node: Element): IRegion | null {\n const region = get1(node, \"Region\");\n\n if (region) {\n return {\n coordinateBox: getLatLonAltBox(region),\n lod: getLod(node),\n };\n }\n return null;\n}\n\nfunction getLod(node: Element): LOD | null {\n const lod = get1(node, \"Lod\");\n\n if (lod) {\n return [\n num1(lod, \"minLodPixels\") ?? -1,\n num1(lod, \"maxLodPixels\") ?? -1,\n num1(lod, \"minFadeExtent\") ?? null,\n num1(lod, \"maxFadeExtent\") ?? null,\n ];\n }\n\n return null;\n}\n\nfunction getLatLonAltBox(node: Element): BoxGeometry | null {\n const latLonAltBox = get1(node, \"LatLonAltBox\");\n\n if (latLonAltBox) {\n const north = num1(latLonAltBox, \"north\");\n const west = num1(latLonAltBox, \"west\");\n const east = num1(latLonAltBox, \"east\");\n const south = num1(latLonAltBox, \"south\");\n const altitudeMode = processAltitudeMode(\n get1(latLonAltBox, \"altitudeMode\") ||\n get1(latLonAltBox, \"gx:altitudeMode\")\n );\n\n if (altitudeMode) {\n console.debug(\n \"Encountered an unsupported feature of KML for togeojson: please contact developers for support of altitude mode.\"\n );\n }\n if (\n typeof north === \"number\" &&\n typeof south === \"number\" &&\n typeof west === \"number\" &&\n typeof east === \"number\"\n ) {\n const bbox: BBox = [west, south, east, north];\n const coordinates = [\n [\n [west, north], // top left\n [east, north], // top right\n [east, south], // top right\n [west, south], // bottom left\n [west, north], // top left (again)\n ],\n ];\n return {\n bbox,\n geometry: {\n type: \"Polygon\",\n coordinates,\n },\n };\n }\n }\n\n return null;\n}\n\nfunction getLinkObject(node: Element) {\n /*\n \n \n ... \n onChange\n \n 4 \n never\n \n 4 \n 1 \n BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]\n \n ... \n \n */\n const linkObj = get1(node, \"Link\");\n\n if (linkObj) {\n return getMulti(linkObj, [\n \"href\",\n \"refreshMode\",\n \"refreshInterval\",\n \"viewRefreshMode\",\n \"viewRefreshTime\",\n \"viewBoundScale\",\n \"viewFormat\",\n \"httpQuery\",\n ]);\n }\n\n return {};\n}\n\nexport function getNetworkLink(\n node: Element,\n styleMap: StyleMap,\n schema: Schema,\n options: KMLOptions\n): Feature | null {\n const box = getNetworkLinkRegion(node);\n\n const geometry = box?.coordinateBox?.geometry || null;\n\n if (!geometry && options.skipNullGeometry) {\n return null;\n }\n\n const feature: Feature = {\n type: \"Feature\",\n geometry,\n properties: Object.assign(\n /**\n * Related to\n * https://gist.github.com/tmcw/037a1cb6660d74a392e9da7446540f46\n */\n { \"@geometry-type\": \"networklink\" },\n getMulti(node, [\n \"name\",\n \"address\",\n \"visibility\",\n \"open\",\n \"phoneNumber\",\n \"styleUrl\",\n \"refreshVisibility\",\n \"flyToView\",\n \"description\",\n ]),\n getMaybeHTMLDescription(node),\n extractCascadedStyle(node, styleMap),\n extractStyle(node),\n extractIconHref(node),\n extractExtendedData(node, schema),\n extractTimeSpan(node),\n extractTimeStamp(node),\n getLinkObject(node),\n box?.lod ? { lod: box.lod } : {}\n ),\n };\n\n if (box?.coordinateBox?.bbox) {\n feature.bbox = box.coordinateBox.bbox;\n }\n\n if (feature.properties?.visibility !== undefined) {\n feature.properties.visibility = feature.properties.visibility !== \"0\";\n }\n\n const id = node.getAttribute(\"id\");\n if (id !== null && id !== \"\") feature.id = id;\n return feature;\n}\n", "import type { Feature, Geometry } from \"geojson\";\nimport type { KMLOptions } from \"lib/kml\";\nimport { type StyleMap, getMulti } from \"../shared\";\nimport { extractStyle } from \"./extractStyle\";\nimport { getGeometry } from \"./geometry\";\nimport {\n type Schema,\n extractCascadedStyle,\n extractExtendedData,\n extractTimeSpan,\n extractTimeStamp,\n getMaybeHTMLDescription,\n} from \"./shared\";\n\nfunction geometryListToGeometry(geometries: Geometry[]): Geometry | null {\n return geometries.length === 0\n ? null\n : geometries.length === 1\n ? geometries[0]\n : {\n type: \"GeometryCollection\",\n geometries,\n };\n}\n\nexport function getPlacemark(\n node: Element,\n styleMap: StyleMap,\n schema: Schema,\n options: KMLOptions\n): Feature | null {\n const { coordTimes, geometries } = getGeometry(node);\n\n const geometry = geometryListToGeometry(geometries);\n\n if (!geometry && options.skipNullGeometry) {\n return null;\n }\n\n const feature: Feature = {\n type: \"Feature\",\n geometry,\n properties: Object.assign(\n getMulti(node, [\n \"name\",\n \"address\",\n \"visibility\",\n \"open\",\n \"phoneNumber\",\n \"description\",\n ]),\n getMaybeHTMLDescription(node),\n extractCascadedStyle(node, styleMap),\n extractStyle(node),\n extractExtendedData(node, schema),\n extractTimeSpan(node),\n extractTimeStamp(node),\n coordTimes.length\n ? {\n coordinateProperties: {\n times: coordTimes.length === 1 ? coordTimes[0] : coordTimes,\n },\n }\n : {}\n ),\n };\n\n if (feature.properties?.visibility !== undefined) {\n feature.properties.visibility = feature.properties.visibility !== \"0\";\n }\n\n const id = node.getAttribute(\"id\");\n if (id !== null && id !== \"\") feature.id = id;\n return feature;\n}\n", "import type { Document as XDocument } from \"@xmldom/xmldom\";\nimport type { FeatureCollection, Geometry } from \"geojson\";\nimport { extractStyle } from \"./kml/extractStyle\";\nimport { getGroundOverlay } from \"./kml/ground_overlay\";\nimport { getNetworkLink } from \"./kml/networklink\";\nimport { getPlacemark } from \"./kml/placemark\";\nimport { type Schema, typeConverters } from \"./kml/shared\";\nimport {\n $,\n type F,\n type P,\n type StyleMap,\n isElement,\n nodeVal,\n normalizeId,\n val1,\n} from \"./shared\";\n\n/**\n * Options to customize KML output.\n *\n * The only option currently\n * is `skipNullGeometry`. Both the KML and GeoJSON formats support\n * the idea of features that don't have geometries: in KML,\n * this is a Placemark without a Point, etc element, and in GeoJSON\n * it's a geometry member with a value of `null`.\n *\n * toGeoJSON, by default, translates null geometries in KML to\n * null geometries in GeoJSON. For systems that use GeoJSON but\n * don't support null geometries, you can specify `skipNullGeometry`\n * to omit these features entirely and only include\n * features that have a geometry defined.\n */\nexport interface KMLOptions {\n skipNullGeometry?: boolean;\n}\n\n/**\n * A folder including metadata. Folders\n * may contain other folders or features,\n * or nothing at all.\n */\nexport interface Folder {\n type: \"folder\";\n /**\n * Standard values:\n *\n * * \"name\",\n * * \"visibility\",\n * * \"open\",\n * * \"address\",\n * * \"description\",\n * * \"phoneNumber\",\n * * \"visibility\",\n */\n meta: {\n [key: string]: unknown;\n };\n children: Array;\n}\n\n/**\n * A nested folder structure, represented\n * as a tree with folders and features.\n */\nexport interface Root {\n type: \"root\";\n children: Array;\n}\n\ntype TreeContainer = Root | Folder;\n\nfunction getStyleId(style: Element) {\n let id = style.getAttribute(\"id\");\n const parentNode = style.parentNode;\n if (\n !id &&\n isElement(parentNode) &&\n parentNode.localName === \"CascadingStyle\"\n ) {\n id = parentNode.getAttribute(\"kml:id\") || parentNode.getAttribute(\"id\");\n }\n return normalizeId(id || \"\");\n}\n\nfunction buildStyleMap(node: Document): StyleMap {\n const styleMap: StyleMap = {};\n for (const style of $(node, \"Style\")) {\n styleMap[getStyleId(style)] = extractStyle(style);\n }\n for (const map of $(node, \"StyleMap\")) {\n const id = normalizeId(map.getAttribute(\"id\") || \"\");\n val1(map, \"styleUrl\", (styleUrl) => {\n styleUrl = normalizeId(styleUrl);\n if (styleMap[styleUrl]) {\n styleMap[id] = styleMap[styleUrl];\n }\n });\n }\n return styleMap;\n}\n\nfunction buildSchema(node: Document): Schema {\n const schema: Schema = {};\n for (const field of $(node, \"SimpleField\")) {\n schema[field.getAttribute(\"name\") || \"\"] =\n typeConverters[field.getAttribute(\"type\") || \"\"] || typeConverters.string;\n }\n return schema;\n}\n\nconst FOLDER_PROPS = [\n \"name\",\n \"visibility\",\n \"open\",\n \"address\",\n \"description\",\n \"phoneNumber\",\n \"visibility\",\n] as const;\n\nfunction getFolder(node: Element): Folder {\n const meta: P = {};\n\n for (const child of Array.from(node.childNodes)) {\n if (isElement(child) && FOLDER_PROPS.includes(child.tagName as any)) {\n meta[child.tagName] = nodeVal(child);\n }\n }\n\n return {\n type: \"folder\",\n meta,\n children: [],\n };\n}\n\n/**\n * Yield a nested tree with KML folder structure\n *\n * This generates a tree with the given structure:\n *\n * ```js\n * {\n * \"type\": \"root\",\n * \"children\": [\n * {\n * \"type\": \"folder\",\n * \"meta\": {\n * \"name\": \"Test\"\n * },\n * \"children\": [\n * // ...features and folders\n * ]\n * }\n * // ...features\n * ]\n * }\n * ```\n *\n * ### GroundOverlay\n *\n * GroundOverlay elements are converted into\n * `Feature` objects with `Polygon` geometries,\n * a property like:\n *\n * ```json\n * {\n * \"@geometry-type\": \"groundoverlay\"\n * }\n * ```\n *\n * And the ground overlay's image URL in the `href`\n * property. Ground overlays will need to be displayed\n * with a separate method to other features, depending\n * on which map framework you're using.\n */\nexport function kmlWithFolders(\n node: Document | XDocument,\n options: KMLOptions = {\n skipNullGeometry: false,\n }\n): Root {\n const n = node as Document;\n const styleMap = buildStyleMap(n);\n const schema = buildSchema(n);\n\n // atomic geospatial types supported by KML - MultiGeometry is\n // handled separately\n // all root placemarks in the file\n const placemarks = [];\n const networkLinks = [];\n const tree: Root = { type: \"root\", children: [] };\n\n function traverse(\n node: Document | ChildNode | Element,\n pointer: TreeContainer,\n options: KMLOptions\n ) {\n if (isElement(node)) {\n switch (node.tagName) {\n case \"GroundOverlay\": {\n placemarks.push(node);\n const placemark = getGroundOverlay(node, styleMap, schema, options);\n if (placemark) {\n pointer.children.push(placemark);\n }\n break;\n }\n case \"Placemark\": {\n placemarks.push(node);\n const placemark = getPlacemark(node, styleMap, schema, options);\n if (placemark) {\n pointer.children.push(placemark);\n }\n break;\n }\n case \"Folder\": {\n const folder = getFolder(node);\n pointer.children.push(folder);\n pointer = folder;\n break;\n }\n case \"NetworkLink\": {\n networkLinks.push(node);\n const networkLink = getNetworkLink(node, styleMap, schema, options);\n if (networkLink) {\n pointer.children.push(networkLink);\n }\n break;\n }\n }\n }\n\n if (node.childNodes) {\n for (let i = 0; i < node.childNodes.length; i++) {\n traverse(node.childNodes[i], pointer, options);\n }\n }\n }\n\n traverse(n, tree, options);\n\n return tree;\n}\n\n/**\n * Convert KML to GeoJSON incrementally, returning\n * a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)\n * that yields output feature by feature.\n */\nexport function* kmlGen(\n node: Document | XDocument,\n options: KMLOptions = {\n skipNullGeometry: false,\n }\n): Generator {\n const n = node as Document;\n const styleMap = buildStyleMap(n);\n const schema = buildSchema(n);\n for (const placemark of $(n, \"Placemark\")) {\n const feature = getPlacemark(placemark, styleMap, schema, options);\n if (feature) yield feature;\n }\n for (const groundOverlay of $(n, \"GroundOverlay\")) {\n const feature = getGroundOverlay(groundOverlay, styleMap, schema, options);\n if (feature) yield feature;\n }\n for (const networkLink of $(n, \"NetworkLink\")) {\n const feature = getNetworkLink(networkLink, styleMap, schema, options);\n if (feature) yield feature;\n }\n}\n\n/**\n * Convert a KML document to GeoJSON. The first argument, `doc`, must be a KML\n * document as an XML DOM - not as a string. You can get this using jQuery's default\n * `.ajax` function or using a bare XMLHttpRequest with the `.response` property\n * holding an XML DOM.\n *\n * The output is a JavaScript object of GeoJSON data. You can convert it to a string\n * with [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)\n * or use it directly in libraries.\n */\nexport function kml(\n node: Document | XDocument,\n options: KMLOptions = {\n skipNullGeometry: false,\n }\n): FeatureCollection {\n return {\n type: \"FeatureCollection\",\n features: Array.from(kmlGen(node as Document, options)),\n };\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { geoBounds as d3_geoBounds, geoPath as d3_geoPath } from 'd3-geo';\nimport { text as d3_text } from 'd3-fetch';\nimport { select as d3_select } from 'd3-selection';\n\nimport stringify from 'fast-json-stable-stringify';\nimport { gpx, kml } from '@tmcw/togeojson';\n\nimport { geoExtent, geoPolygonIntersectsPolygon } from '../geo';\nimport { services } from '../services';\nimport { svgPath } from './helpers';\nimport { utilDetect } from '../util/detect';\nimport { utilArrayFlatten, utilArrayUnion, utilHashcode } from '../util';\n\n\nvar _initialized = false;\nvar _enabled = false;\nvar _geojson;\n\n\nexport function svgData(projection, context, dispatch) {\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var _showLabels = true;\n var detected = utilDetect();\n var layer = d3_select(null);\n var _vtService;\n var _fileList;\n var _template;\n var _src;\n\n const supportedFormats = [\n '.gpx',\n '.kml',\n '.geojson',\n '.json'\n ];\n\n\n function init() {\n if (_initialized) return; // run once\n\n _geojson = {};\n _enabled = true;\n\n function over(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n d3_event.dataTransfer.dropEffect = 'copy';\n }\n\n context.container()\n .attr('dropzone', 'copy')\n .on('drop.svgData', function(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n if (!detected.filedrop) return;\n var f = d3_event.dataTransfer.files[0];\n var extension = getExtension(f.name);\n if (!supportedFormats.includes(extension)) return;\n drawData.fileList(d3_event.dataTransfer.files);\n })\n .on('dragenter.svgData', over)\n .on('dragexit.svgData', over)\n .on('dragover.svgData', over);\n\n _initialized = true;\n }\n\n\n function getService() {\n if (services.vectorTile && !_vtService) {\n _vtService = services.vectorTile;\n _vtService.event.on('loadedData', throttledRedraw);\n } else if (!services.vectorTile && _vtService) {\n _vtService = null;\n }\n\n return _vtService;\n }\n\n\n function showLayer() {\n layerOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', layerOff);\n }\n\n\n function layerOn() {\n layer.style('display', 'block');\n }\n\n\n function layerOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n // ensure that all geojson features in a collection have IDs\n function ensureIDs(gj) {\n if (!gj) return null;\n\n if (gj.type === 'FeatureCollection') {\n for (var i = 0; i < gj.features.length; i++) {\n ensureFeatureID(gj.features[i]);\n }\n } else {\n ensureFeatureID(gj);\n }\n return gj;\n }\n\n\n // ensure that each single Feature object has a unique ID\n function ensureFeatureID(feature) {\n if (!feature) return;\n feature.__featurehash__ = utilHashcode(stringify(feature));\n return feature;\n }\n\n\n // Prefer an array of Features instead of a FeatureCollection\n function getFeatures(gj) {\n if (!gj) return [];\n\n if (gj.type === 'FeatureCollection') {\n return gj.features;\n } else {\n return [gj];\n }\n }\n\n\n function featureKey(d) {\n return d.__featurehash__;\n }\n\n\n function isPolygon(d) {\n return d.geometry.type === 'Polygon' || d.geometry.type === 'MultiPolygon';\n }\n\n\n function clipPathID(d) {\n return 'ideditor-data-' + d.__featurehash__ + '-clippath';\n }\n\n\n function featureClasses(d) {\n return [\n 'data' + d.__featurehash__,\n d.geometry.type,\n isPolygon(d) ? 'area' : '',\n d.__layerID__ || ''\n ].filter(Boolean).join(' ');\n }\n\n\n function drawData(selection) {\n var vtService = getService();\n var getPath = svgPath(projection).geojson;\n var getAreaPath = svgPath(projection, null, true).geojson;\n var hasData = drawData.hasData();\n\n layer = selection.selectAll('.layer-mapdata')\n .data(_enabled && hasData ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-mapdata')\n .merge(layer);\n\n var surface = context.surface();\n if (!surface || surface.empty()) return; // not ready to draw yet, starting up\n\n\n // Gather data\n var geoData, polygonData;\n if (_template && vtService) { // fetch data from vector tile service\n var sourceID = _template;\n vtService.loadTiles(sourceID, _template, projection);\n geoData = vtService.data(sourceID, projection);\n } else {\n geoData = getFeatures(_geojson);\n }\n geoData = geoData.filter(getPath);\n polygonData = geoData.filter(isPolygon);\n\n\n // Draw clip paths for polygons\n var clipPaths = surface.selectAll('defs').selectAll('.clipPath-data')\n .data(polygonData, featureKey);\n\n clipPaths.exit()\n .remove();\n\n var clipPathsEnter = clipPaths.enter()\n .append('clipPath')\n .attr('class', 'clipPath-data')\n .attr('id', clipPathID);\n\n clipPathsEnter\n .append('path');\n\n clipPaths.merge(clipPathsEnter)\n .selectAll('path')\n .attr('d', getAreaPath);\n\n\n // Draw fill, shadow, stroke layers\n var datagroups = layer\n .selectAll('g.datagroup')\n .data(['fill', 'shadow', 'stroke']);\n\n datagroups = datagroups.enter()\n .append('g')\n .attr('class', function(d) { return 'datagroup datagroup-' + d; })\n .merge(datagroups);\n\n\n // Draw paths\n var pathData = {\n fill: polygonData,\n shadow: geoData,\n stroke: geoData\n };\n\n var paths = datagroups\n .selectAll('path')\n .data(function(layer) { return pathData[layer]; }, featureKey);\n\n // exit\n paths.exit()\n .remove();\n\n // enter/update\n paths.enter()\n .append('path')\n .attr('class', function(d) {\n var datagroup = this.parentNode.__data__;\n return 'pathdata ' + datagroup + ' ' + featureClasses(d);\n })\n .attr('clip-path', function(d) {\n var datagroup = this.parentNode.__data__;\n return datagroup === 'fill' ? ('url(#' + clipPathID(d) + ')') : null;\n })\n .merge(paths)\n .attr('d', function(d) {\n var datagroup = this.parentNode.__data__;\n return datagroup === 'fill' ? getAreaPath(d) : getPath(d);\n });\n\n\n // Draw labels\n layer\n .call(drawLabels, 'label-halo', geoData)\n .call(drawLabels, 'label', geoData);\n\n\n function drawLabels(selection, textClass, data) {\n var labelPath = d3_geoPath(projection);\n var labelData = data.filter(function(d) {\n return _showLabels && d.properties && (d.properties.desc || d.properties.name);\n });\n\n var labels = selection.selectAll('text.' + textClass)\n .data(labelData, featureKey);\n\n // exit\n labels.exit()\n .remove();\n\n // enter/update\n labels.enter()\n .append('text')\n .attr('class', function(d) { return textClass + ' ' + featureClasses(d); })\n .merge(labels)\n .text(function(d) {\n return d.properties.desc || d.properties.name;\n })\n .attr('x', function(d) {\n var centroid = labelPath.centroid(d);\n return centroid[0] + 11;\n })\n .attr('y', function(d) {\n var centroid = labelPath.centroid(d);\n return centroid[1];\n });\n }\n }\n\n\n function getExtension(fileName) {\n if (!fileName) return;\n\n var re = /\\.(gpx|kml|(geo)?json|png)$/i;\n var match = fileName.toLowerCase().match(re);\n return match && match.length && match[0];\n }\n\n\n function xmlToDom(textdata) {\n return (new DOMParser()).parseFromString(textdata, 'text/xml');\n }\n\n\n function stringifyGeojsonProperties(feature) {\n const properties = feature.properties;\n for (const key in properties) {\n const property = properties[key];\n if (typeof property === 'number' || typeof property === 'boolean' || Array.isArray(property)) {\n properties[key] = property.toString();\n } else if (property === null) {\n properties[key] = 'null';\n } else if (typeof property === 'object') {\n properties[key] = JSON.stringify(property);\n }\n }\n }\n\n\n drawData.setFile = function(extension, data) {\n _template = null;\n _fileList = null;\n _geojson = null;\n _src = null;\n\n var gj;\n switch (extension) {\n case '.gpx':\n gj = gpx(xmlToDom(data));\n break;\n case '.kml':\n gj = kml(xmlToDom(data));\n break;\n case '.geojson':\n case '.json':\n gj = JSON.parse(data);\n if (gj.type === 'FeatureCollection') {\n gj.features.forEach(stringifyGeojsonProperties);\n } else if (gj.type === 'Feature') {\n stringifyGeojsonProperties(gj);\n }\n break;\n }\n\n gj = gj || {};\n if (Object.keys(gj).length) {\n _geojson = ensureIDs(gj);\n _src = extension + ' data file';\n this.fitZoom();\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.showLabels = function(val) {\n if (!arguments.length) return _showLabels;\n\n _showLabels = val;\n return this;\n };\n\n\n drawData.enabled = function(val) {\n if (!arguments.length) return _enabled;\n\n _enabled = val;\n if (_enabled) {\n showLayer();\n } else {\n hideLayer();\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.hasData = function() {\n var gj = _geojson || {};\n return !!(_template || Object.keys(gj).length);\n };\n\n\n drawData.template = function(val, src) {\n if (!arguments.length) return _template;\n\n // test source against OSM imagery blocklists..\n var osm = context.connection();\n if (osm) {\n for (const regex of osm.imageryBlocklists()) {\n if (regex.test(val)) {\n // matches a blocked sources -> do not set template\n return;\n };\n }\n }\n\n _template = val;\n _fileList = null;\n _geojson = null;\n\n // strip off the querystring/hash from the template,\n // it often includes the access token\n _src = src || ('vectortile:' + val.split(/[?#]/)[0]);\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.geojson = function(gj, src) {\n if (!arguments.length) return _geojson;\n\n _template = null;\n _fileList = null;\n _geojson = null;\n _src = null;\n\n gj = gj || {};\n if (Object.keys(gj).length) {\n _geojson = ensureIDs(gj);\n _src = src || 'unknown.geojson';\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.fileList = function(fileList) {\n if (!arguments.length) return _fileList;\n\n _template = null;\n _geojson = null;\n _src = null;\n _fileList = fileList;\n\n if (!fileList || !fileList.length) return this;\n var f = fileList[0];\n var extension = getExtension(f.name);\n var reader = new FileReader();\n reader.onload = (function() {\n return function(e) {\n drawData.setFile(extension, e.target.result);\n };\n })(f);\n\n reader.readAsText(f);\n\n return this;\n };\n\n\n drawData.url = function(url, defaultExtension) {\n _template = null;\n _fileList = null;\n _geojson = null;\n _src = null;\n\n // strip off any querystring/hash from the url before checking extension\n var testUrl = url.split(/[?#]/)[0];\n var extension = getExtension(testUrl) || defaultExtension;\n if (extension) {\n _template = null;\n d3_text(url)\n .then(function(data) {\n drawData.setFile(extension, data);\n })\n .catch(function() {\n /* ignore */\n });\n\n } else {\n drawData.template(url);\n }\n\n return this;\n };\n\n\n drawData.getSrc = function() {\n return _src || '';\n };\n\n\n drawData.fitZoom = function() {\n var features = getFeatures(_geojson);\n if (!features.length) return;\n\n var map = context.map();\n var viewport = map.trimmedExtent().polygon();\n var coords = features.reduce(function(coords, feature) {\n var geom = feature.geometry;\n if (!geom) return coords;\n\n var c = geom.coordinates;\n\n /* eslint-disable no-fallthrough */\n switch (geom.type) {\n case 'Point':\n c = [c];\n case 'MultiPoint':\n case 'LineString':\n break;\n\n case 'MultiPolygon':\n c = utilArrayFlatten(c);\n case 'Polygon':\n case 'MultiLineString':\n c = utilArrayFlatten(c);\n break;\n }\n /* eslint-enable no-fallthrough */\n\n return utilArrayUnion(coords, c);\n }, []);\n\n if (!geoPolygonIntersectsPolygon(viewport, coords, true)) {\n var extent = geoExtent(d3_geoBounds({ type: 'LineString', coordinates: coords }));\n map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));\n }\n\n return this;\n };\n\n\n init();\n return drawData;\n}\n", "import { fileFetcher } from '../core/file_fetcher';\nimport { svgPath } from './helpers';\n\n\nexport function svgDebug(projection, context) {\n\n function drawDebug(selection) {\n const showTile = context.getDebug('tile');\n const showCollision = context.getDebug('collision');\n const showImagery = context.getDebug('imagery');\n const showTouchTargets = context.getDebug('target');\n const showDownloaded = context.getDebug('downloaded');\n\n let debugData = [];\n if (showTile) {\n debugData.push({ class: 'red', label: 'tile' });\n }\n if (showCollision) {\n debugData.push({ class: 'yellow', label: 'collision' });\n }\n if (showImagery) {\n debugData.push({ class: 'orange', label: 'imagery' });\n }\n if (showTouchTargets) {\n debugData.push({ class: 'pink', label: 'touchTargets' });\n }\n if (showDownloaded) {\n debugData.push({ class: 'purple', label: 'downloaded' });\n }\n\n\n let legend = context.container().select('.main-content')\n .selectAll('.debug-legend')\n .data(debugData.length ? [0] : []);\n\n legend.exit()\n .remove();\n\n legend = legend.enter()\n .append('div')\n .attr('class', 'fillD debug-legend')\n .merge(legend);\n\n\n let legendItems = legend.selectAll('.debug-legend-item')\n .data(debugData, d => d.label);\n\n legendItems.exit()\n .remove();\n\n legendItems.enter()\n .append('span')\n .attr('class', d => `debug-legend-item ${d.class}`)\n .text(d => d.label);\n\n\n let layer = selection.selectAll('.layer-debug')\n .data(showImagery || showDownloaded ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-debug')\n .merge(layer);\n\n\n // imagery\n const extent = context.map().extent();\n fileFetcher.get('imagery')\n .then(d => {\n const hits = (showImagery && d.query.bbox(extent.rectangle(), true)) || [];\n const features = hits.map(d => d.features[d.id]);\n\n let imagery = layer.selectAll('path.debug-imagery')\n .data(features);\n\n imagery.exit()\n .remove();\n\n imagery.enter()\n .append('path')\n .attr('class', 'debug-imagery debug orange');\n })\n .catch(() => { /* ignore */ });\n\n // downloaded\n const osm = context.connection();\n let dataDownloaded = [];\n if (osm && showDownloaded) {\n const rtree = osm.caches('get').tile.rtree;\n dataDownloaded = rtree.all().map(bbox => {\n return {\n type: 'Feature',\n properties: { id: bbox.id },\n geometry: {\n type: 'Polygon',\n coordinates: [[\n [ bbox.minX, bbox.minY ],\n [ bbox.minX, bbox.maxY ],\n [ bbox.maxX, bbox.maxY ],\n [ bbox.maxX, bbox.minY ],\n [ bbox.minX, bbox.minY ]\n ]]\n }\n };\n });\n }\n\n let downloaded = layer\n .selectAll('path.debug-downloaded')\n .data(showDownloaded ? dataDownloaded : []);\n\n downloaded.exit()\n .remove();\n\n downloaded.enter()\n .append('path')\n .attr('class', 'debug-downloaded debug purple');\n\n // update\n layer.selectAll('path')\n .attr('d', svgPath(projection).geojson);\n }\n\n\n // This looks strange because `enabled` methods on other layers are\n // chainable getter/setters, and this one is just a getter.\n drawDebug.enabled = function() {\n if (!arguments.length) {\n return context.getDebug('tile') ||\n context.getDebug('collision') ||\n context.getDebug('imagery') ||\n context.getDebug('target') ||\n context.getDebug('downloaded');\n } else {\n return this;\n }\n };\n\n\n return drawDebug;\n}\n", "import { svg as d3_svg } from 'd3-fetch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { utilArrayUniq } from '../util';\n\n\n/*\n A standalone SVG element that contains only a `defs` sub-element. To be\n used once globally, since defs IDs must be unique within a document.\n*/\nexport function svgDefs(context) {\n\n var _defsSelection = d3_select(null);\n\n var _spritesheetIds = [\n 'iD-sprite', 'maki-sprite', 'temaki-sprite', 'fa-sprite', 'roentgen-sprite', 'community-sprite'\n ];\n\n function drawDefs(selection) {\n _defsSelection = selection.append('defs');\n\n // add markers\n\n // SVG markers have to be given a colour where they're defined\n // (they can't inherit it from the line they're attached to),\n // so we need to manually define markers for each color of tag\n // (also, it's slightly nicer if we can control the\n // positioning for different tags)\n\n /** @param {string} name @param {string} colour */\n function addOnewayMarker(name, colour) {\n _defsSelection\n .append('marker')\n .attr('id', `ideditor-oneway-marker-${name}`)\n .attr('viewBox', '0 0 10 5')\n .attr('refX', 4)\n .attr('refY', 2.5)\n .attr('markerWidth', 2)\n .attr('markerHeight', 2)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'oneway-marker-path')\n .attr('d', 'M 6,3 L 0,3 L 0,2 L 6,2 L 5,0 L 10,2.5 L 5,5 z')\n .attr('stroke', 'none')\n .attr('fill', colour)\n .attr('opacity', '1');\n }\n addOnewayMarker('black', '#333'); // default\n addOnewayMarker('white', '#fff'); // for dark lines (bridges under construction, railways, etc.)\n addOnewayMarker('gray', '#eee'); // for railway lines\n\n\n function addSidedMarker(name, color, offset, style) {\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-sided-marker-' + name)\n .attr('viewBox', '0 0 2 2')\n .attr('refX', 1)\n .attr('refY', -offset)\n .attr('markerWidth', 1.5)\n .attr('markerHeight', 1.5)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'sided-marker-path sided-marker-' + name + '-path')\n .attr('d', style === 'circle'\n ? 'M 0,0.5 a 0.5,0.5 0 1,0 1,0 a 0.5,0.5 0 1,0 -1,0'\n : 'M 0,0 L 1,1 L 2,0 z')\n .attr('stroke', 'none')\n .attr('fill', color);\n }\n addSidedMarker('natural', 'rgb(170, 170, 170)', 0);\n // for a coastline, the arrows are (somewhat unintuitively) on\n // the water side, so let's color them blue (with a gap) to\n // give a stronger indication\n addSidedMarker('coastline', '#77dede', 1);\n addSidedMarker('waterway', '#77dede', 1);\n // barriers have a dashed line, and separating the triangle\n // from the line visually suits that\n addSidedMarker('barrier', '#ddd', 1);\n // dedicated style for guard rails (#9594):\n // marker on opposite side, circles instead of triangles\n addSidedMarker('guard_rail', '#ddd', -1.5, 'circle');\n addSidedMarker('man_made', '#fff', 0);\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z')\n .attr('fill', '#333')\n .attr('fill-opacity', '0.75')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker-wireframe')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z')\n .attr('fill', 'none')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker-side')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 3 14 C 8 13 8 13 13 14 L 8 5 Z')\n .attr('fill', '#333')\n .attr('fill-opacity', '0.75')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker-side-wireframe')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 3 14 C 8 13 8 13 13 14 L 8 5 Z')\n .attr('fill', 'none')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n\n // add patterns\n var patterns = _defsSelection.selectAll('pattern')\n .data([\n // pattern name, pattern image name\n ['beach', 'dots'],\n ['construction', 'construction'],\n ['cemetery', 'cemetery'],\n ['cemetery_christian', 'cemetery_christian'],\n ['cemetery_buddhist', 'cemetery_buddhist'],\n ['cemetery_muslim', 'cemetery_muslim'],\n ['cemetery_jewish', 'cemetery_jewish'],\n ['farmland', 'farmland'],\n ['farmyard', 'farmyard'],\n ['forest', 'forest'],\n ['forest_broadleaved', 'forest_broadleaved'],\n ['forest_needleleaved', 'forest_needleleaved'],\n ['forest_leafless', 'forest_leafless'],\n ['golf_green', 'grass'],\n ['grass', 'grass'],\n ['landfill', 'landfill'],\n ['meadow', 'grass'],\n ['orchard', 'orchard'],\n ['pond', 'pond'],\n ['quarry', 'quarry'],\n ['scrub', 'bushes'],\n ['vineyard', 'vineyard'],\n ['water_standing', 'lines'],\n ['waves', 'waves'],\n ['wetland', 'wetland'],\n ['wetland_marsh', 'wetland_marsh'],\n ['wetland_swamp', 'wetland_swamp'],\n ['wetland_bog', 'wetland_bog'],\n ['wetland_reedbed', 'wetland_reedbed']\n ])\n .enter()\n .append('pattern')\n .attr('id', function (d) { return 'ideditor-pattern-' + d[0]; })\n .attr('width', 32)\n .attr('height', 32)\n .attr('patternUnits', 'userSpaceOnUse');\n\n patterns\n .append('rect')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', 32)\n .attr('height', 32)\n .attr('class', function (d) { return 'pattern-color-' + d[0]; });\n\n patterns\n .append('image')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', 32)\n .attr('height', 32)\n .attr('xlink:href', function (d) {\n return context.imagePath('pattern/' + d[1] + '.png');\n });\n\n // add clip paths\n _defsSelection.selectAll('clipPath')\n .data([12, 18, 20, 32, 45])\n .enter()\n .append('clipPath')\n .attr('id', function (d) { return 'ideditor-clip-square-' + d; })\n .append('rect')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', function (d) { return d; })\n .attr('height', function (d) { return d; });\n\n // add svg filters\n const filters = _defsSelection.selectAll('filter')\n .data(['alpha-slope5'])\n .enter()\n .append('filter')\n .attr('id', d => d);\n // Alters the alpha channel such that everything but\n // (almost) transparent pixels are rendered fully opaque:\n // This is used in a workaround for how chrome is rendering\n // the edges of `img` elements when the page zoom is not a\n // \"round value\": the semi-transparent pixels of neighboring\n // tiles cannot \"add up\" to a fully opaque background layer.\n // See https://github.com/openstreetmap/iD/issues/10747\n // and https://github.com/openstreetmap/iD/pull/10594\n const alphaSlope5 = filters.filter('#alpha-slope5')\n .append('feComponentTransfer');\n alphaSlope5.append('feFuncR').attr('type', 'identity');\n alphaSlope5.append('feFuncG').attr('type', 'identity');\n alphaSlope5.append('feFuncB').attr('type', 'identity');\n alphaSlope5.append('feFuncA')\n .attr('type', 'linear')\n .attr('slope', 5);\n\n // add symbol spritesheets\n addSprites(_spritesheetIds, true);\n }\n\n function addSprites(ids, overrideColors) {\n _spritesheetIds = utilArrayUniq(_spritesheetIds.concat(ids));\n\n var spritesheets = _defsSelection\n .selectAll('.spritesheet')\n .data(_spritesheetIds);\n\n spritesheets\n .enter()\n .append('g')\n .attr('class', function(d) { return 'spritesheet spritesheet-' + d; })\n .each(function(d) {\n var url = context.imagePath(d + '.svg');\n var node = d3_select(this).node();\n\n d3_svg(url)\n .then(function(svg) {\n node.appendChild(\n d3_select(svg.documentElement).attr('id', 'ideditor-' + d).node()\n );\n if (overrideColors && d !== 'iD-sprite') { // allow icon colors to be overridden..\n d3_select(node).selectAll('path')\n .attr('fill', 'currentColor');\n }\n })\n .catch(function() {\n /* ignore */\n });\n });\n\n spritesheets\n .exit()\n .remove();\n }\n\n drawDefs.addSprites = addSprites;\n\n return drawDefs;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { svgPointTransform } from './helpers';\nimport { geoMetersToLat } from '../geo';\n\n\nexport function svgGeolocate(projection) {\n var layer = d3_select(null);\n var _position;\n\n\n function init() {\n if (svgGeolocate.initialized) return; // run once\n svgGeolocate.enabled = false;\n svgGeolocate.initialized = true;\n }\n\n function showLayer() {\n layer.style('display', 'block');\n }\n\n\n function hideLayer() {\n layer\n .transition()\n .duration(250)\n .style('opacity', 0);\n }\n\n function layerOn() {\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1);\n\n }\n\n function layerOff() {\n layer.style('display', 'none');\n }\n\n function transform(d) {\n return svgPointTransform(projection)(d);\n }\n\n function accuracy(accuracy, loc) { // converts accuracy to pixels...\n var degreesRadius = geoMetersToLat(accuracy),\n tangentLoc = [loc[0], loc[1] + degreesRadius],\n projectedTangent = projection(tangentLoc),\n projectedLoc = projection([loc[0], loc[1]]);\n\n // southern most point will have higher pixel value...\n return Math.round(projectedLoc[1] - projectedTangent[1]).toString();\n }\n\n function update() {\n var geolocation = { loc: [_position.coords.longitude, _position.coords.latitude] };\n\n var groups = layer.selectAll('.geolocations').selectAll('.geolocation')\n .data([geolocation]);\n\n groups.exit()\n .remove();\n\n var pointsEnter = groups.enter()\n .append('g')\n .attr('class', 'geolocation');\n\n pointsEnter\n .append('circle')\n .attr('class', 'geolocate-radius')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('fill', 'rgb(15,128,225)')\n .attr('fill-opacity', '0.3')\n .attr('r', '0');\n\n pointsEnter\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('fill', 'rgb(15,128,225)')\n .attr('stroke', 'white')\n .attr('stroke-width', '1.5')\n .attr('r', '6');\n\n groups.merge(pointsEnter)\n .attr('transform', transform);\n\n layer.select('.geolocate-radius').attr('r', accuracy(_position.coords.accuracy, geolocation.loc));\n }\n\n function drawLocation(selection) {\n var enabled = svgGeolocate.enabled;\n\n layer = selection.selectAll('.layer-geolocate')\n .data([0]);\n\n layer.exit()\n .remove();\n\n var layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-geolocate')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'geolocations');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n update();\n } else {\n layerOff();\n }\n }\n\n drawLocation.enabled = function (position, enabled) {\n if (!arguments.length) return svgGeolocate.enabled;\n _position = position;\n svgGeolocate.enabled = enabled;\n if (svgGeolocate.enabled) {\n showLayer();\n layerOn();\n } else {\n hideLayer();\n }\n return this;\n };\n\n init();\n return drawLocation;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { geoPath as d3_geoPath } from 'd3-geo';\nimport RBush from 'rbush';\nimport { localizer } from '../core/localizer';\n\nimport {\n geoExtent, geoPolygonIntersectsPolygon, geoPathLength,\n geoScaleToZoom, geoVecInterp, geoVecLength\n} from '../geo';\nimport { presetManager } from '../presets';\nimport { osmEntity, osmIsInterestingTag } from '../osm';\nimport { utilDetect } from '../util/detect';\nimport { utilArrayDifference, utilArrayUniq, utilDisplayName, utilDisplayNameForPath, utilEntitySelector } from '../util';\n\n\n\nexport function svgLabels(projection, context) {\n var path = d3_geoPath(projection);\n var detected = utilDetect();\n var baselineHack = detected.browser.toLowerCase() === 'safari';\n\n var _rdrawn = new RBush();\n var _rskipped = new RBush();\n var _entitybboxes = {};\n\n // Listed from highest to lowest priority\n const labelStack = [\n ['line', 'aeroway', '*', 12],\n ['line', 'highway', 'motorway', 12],\n ['line', 'highway', 'trunk', 12],\n ['line', 'highway', 'primary', 12],\n ['line', 'highway', 'secondary', 12],\n ['line', 'highway', 'tertiary', 12],\n ['line', 'highway', '*', 12],\n ['line', 'railway', '*', 12],\n ['line', 'waterway', '*', 12],\n ['area', 'aeroway', '*', 12],\n ['area', 'amenity', '*', 12],\n ['area', 'building', '*', 12],\n ['area', 'historic', '*', 12],\n ['area', 'leisure', '*', 12],\n ['area', 'man_made', '*', 12],\n ['area', 'natural', '*', 12],\n ['area', 'shop', '*', 12],\n ['area', 'craft', '*', 12],\n ['area', 'tourism', '*', 12],\n ['area', 'camp_site', '*', 12],\n ['point', 'aeroway', '*', 10],\n ['point', 'amenity', '*', 10],\n ['point', 'building', '*', 10],\n ['point', 'historic', '*', 10],\n ['point', 'leisure', '*', 10],\n ['point', 'man_made', '*', 10],\n ['point', 'natural', '*', 10],\n ['point', 'shop', '*', 10],\n ['point', 'tourism', '*', 10],\n ['point', 'camp_site', '*', 10],\n ['*', 'alt_name', '*', 12],\n ['*', 'official_name', '*', 12],\n ['*', 'loc_name', '*', 12],\n ['*', 'loc_ref', '*', 12],\n ['*', 'unsigned_ref', '*', 12],\n ['*', 'seamark:name', '*', 12],\n ['*', 'sector:name', '*', 12],\n ['*', 'lock_name', '*', 12],\n ['*', 'distance', '*', 12],\n ['*', 'railway:position', '*', 12],\n ['line', 'ref', '*', 12],\n ['area', 'ref', '*', 12],\n ['point', 'ref', '*', 10],\n ['line', 'name', '*', 12],\n ['area', 'name', '*', 12],\n ['point', 'name', '*', 10],\n ['point', 'addr:housenumber', '*', 10],\n ['point', 'addr:housename', '*', 10]\n ];\n\n\n function shouldSkipIcon(preset) {\n var noIcons = ['building', 'landuse', 'natural'];\n return noIcons.some(function(s) {\n return preset.id.indexOf(s) >= 0;\n });\n }\n\n\n function drawLinePaths(selection, labels, filter, classes) {\n var paths = selection.selectAll('path:not(.debug)')\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n paths.exit()\n .remove();\n\n // enter/update\n paths.enter()\n .append('path')\n .style('stroke-width', d => d.position['font-size'])\n .attr('id', d => 'ideditor-labelpath-' + d.entity.id)\n .attr('class', classes)\n .merge(paths)\n .attr('d', d => d.position.lineString);\n }\n\n\n function drawLineLabels(selection, labels, filter, classes) {\n var texts = selection.selectAll('text.' + classes)\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n texts.exit()\n .remove();\n\n // enter\n texts.enter()\n .append('text')\n .attr('class', d => classes + ' ' + d.position.classes + ' ' + d.entity.id)\n .attr('dy', baselineHack ? '0.35em' : null)\n .append('textPath')\n .attr('class', 'textpath');\n\n // update\n selection.selectAll('text.' + classes).selectAll('.textpath')\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity))\n .attr('startOffset', '50%')\n .attr('xlink:href', function(d) { return '#ideditor-labelpath-' + d.entity.id; })\n .text(d => d.name);\n }\n\n\n function drawPointLabels(selection, labels, filter, classes) {\n if (classes.includes('pointlabel-halo')) {\n labels = labels.filter(d => !d.position.isAddr);\n }\n var texts = selection.selectAll('text.' + classes)\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n texts.exit()\n .remove();\n\n // enter/update\n texts.enter()\n .append('text')\n .attr('class', d => classes + ' ' + d.position.classes + ' ' + d.entity.id)\n .style('text-anchor', d => d.position.textAnchor)\n .text(d => d.name)\n .merge(texts)\n .attr('x', d => d.position.x)\n .attr('y', d => d.position.y);\n }\n\n\n function drawAreaLabels(selection, labels, filter, classes) {\n labels = labels.filter(hasText);\n drawPointLabels(selection, labels, filter, classes);\n\n function hasText(d) {\n return d.position.hasOwnProperty('x') && d.position.hasOwnProperty('y');\n }\n }\n\n\n function drawAreaIcons(selection, labels, filter, classes) {\n var icons = selection.selectAll('use.' + classes)\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n icons.exit()\n .remove();\n\n // enter/update\n icons.enter()\n .append('use')\n .attr('class', 'icon ' + classes)\n .attr('width', '17px')\n .attr('height', '17px')\n .merge(icons)\n .attr('transform', d => d.position.transform)\n .attr('xlink:href', function(d) {\n var preset = presetManager.match(d.entity, context.graph());\n var picon = preset && preset.icon;\n return picon ? '#' + picon : '';\n });\n }\n\n\n function drawCollisionBoxes(selection, rtree, which) {\n var classes = 'debug ' + which + ' ' + (which === 'debug-skipped' ? 'orange' : 'yellow');\n\n var gj = [];\n if (context.getDebug('collision')) {\n gj = rtree.all().map(function(d) {\n return { type: 'Polygon', coordinates: [[\n [d.minX, d.minY],\n [d.maxX, d.minY],\n [d.maxX, d.maxY],\n [d.minX, d.maxY],\n [d.minX, d.minY]\n ]]};\n });\n }\n\n var boxes = selection.selectAll('.' + which)\n .data(gj);\n\n // exit\n boxes.exit()\n .remove();\n\n // enter/update\n boxes.enter()\n .append('path')\n .attr('class', classes)\n .merge(boxes)\n .attr('d', d3_geoPath());\n }\n\n\n function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) {\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n\n var labelable = [];\n var renderNodeAs = {};\n var i, j, k, entity, geometry;\n\n for (i = 0; i < labelStack.length; i++) {\n labelable.push([]);\n }\n\n if (fullRedraw) {\n _rdrawn.clear();\n _rskipped.clear();\n _entitybboxes = {};\n\n } else {\n for (i = 0; i < entities.length; i++) {\n entity = entities[i];\n var toRemove = []\n .concat(_entitybboxes[entity.id] || [])\n .concat(_entitybboxes[entity.id + 'I'] || []);\n\n for (j = 0; j < toRemove.length; j++) {\n _rdrawn.remove(toRemove[j]);\n _rskipped.remove(toRemove[j]);\n }\n }\n }\n\n // Loop through all the entities to do some preprocessing\n for (i = 0; i < entities.length; i++) {\n entity = entities[i];\n geometry = entity.geometry(graph);\n\n // Insert collision boxes around interesting points/vertices\n if (geometry === 'point' || (geometry === 'vertex' && isInterestingVertex(entity))) {\n const isAddr = isAddressPoint(entity.tags);\n var hasDirections = entity.directions(graph, projection).length;\n var markerPadding = 0;\n\n if (wireframe) {\n renderNodeAs[entity.id] = { geometry: 'vertex', isAddr };\n } else if (geometry === 'vertex') {\n renderNodeAs[entity.id] = { geometry: 'vertex', isAddr };\n } else if (zoom >= 18 && hasDirections) {\n renderNodeAs[entity.id] = { geometry: 'vertex', isAddr };\n } else {\n renderNodeAs[entity.id] = { geometry: 'point', isAddr };\n markerPadding = 20; // extra y for marker height\n }\n\n if (isAddr) {\n undoInsert(entity.id + 'P');\n } else {\n var coord = projection(entity.loc);\n var nodePadding = 10;\n var bbox = {\n minX: coord[0] - nodePadding,\n minY: coord[1] - nodePadding - markerPadding,\n maxX: coord[0] + nodePadding,\n maxY: coord[1] + nodePadding\n };\n doInsert(bbox, entity.id + 'P');\n }\n }\n\n // From here on, treat vertices like points\n if (geometry === 'vertex') {\n geometry = 'point';\n }\n\n // Determine which entities are label-able\n var preset = geometry === 'area' && presetManager.match(entity, graph);\n var icon = preset && !shouldSkipIcon(preset) && preset.icon;\n\n if (!icon && !utilDisplayName(entity, { isMapLabel: true })) continue;\n\n for (k = 0; k < labelStack.length; k++) {\n var matchGeom = labelStack[k][0];\n var matchKey = labelStack[k][1];\n var matchVal = labelStack[k][2];\n var hasVal = entity.tags[matchKey];\n\n if ((matchGeom === '*' || geometry === matchGeom) && hasVal && (matchVal === '*' || matchVal === hasVal)) {\n labelable[k].push(entity);\n break;\n }\n }\n }\n\n var labelled = {\n point: [],\n line: [],\n area: []\n };\n\n // Try and find a valid label for labellable entities\n for (k = 0; k < labelable.length; k++) {\n var fontSize = labelStack[k][3];\n\n for (i = 0; i < labelable[k].length; i++) {\n entity = labelable[k][i];\n geometry = entity.geometry(graph);\n\n let name = geometry === 'line'\n ? utilDisplayNameForPath(entity)\n : utilDisplayName(entity, { isMapLabel: true });\n var width = name && textWidth(name, fontSize, selection.select('g.layer-osm.labels').node());\n var p = null;\n\n if (geometry === 'point' || geometry === 'vertex') {\n // no point or vertex labels in wireframe mode\n // no vertex labels at low zooms (vertices have no icons)\n if (wireframe) continue;\n var renderAs = renderNodeAs[entity.id];\n if (renderAs.geometry === 'vertex' && zoom < 17) continue;\n while (renderAs.isAddr && width > 36) {\n name = `${name.substring(0, name.replace(/\u2026$/, '').length - 1)}\u2026`;\n width = textWidth(name, fontSize, selection.select('g.layer-osm.labels').node());\n }\n\n p = getPointLabel(entity, width, fontSize, renderAs);\n } else if (geometry === 'line') {\n p = getLineLabel(entity, width, fontSize);\n\n } else if (geometry === 'area') {\n p = getAreaLabel(entity, width, fontSize);\n }\n\n if (p) {\n if (geometry === 'vertex') { geometry = 'point'; } // treat vertex like point\n p.classes = geometry + ' tag-' + labelStack[k][1];\n labelled[geometry].push({\n entity,\n name,\n position: p\n });\n }\n }\n }\n\n\n function isInterestingVertex(entity) {\n var selectedIDs = context.selectedIDs();\n\n return entity.hasInterestingTags() ||\n entity.isEndpoint(graph) ||\n entity.isConnected(graph) ||\n selectedIDs.indexOf(entity.id) !== -1 ||\n graph.parentWays(entity).some(function(parent) {\n return selectedIDs.indexOf(parent.id) !== -1;\n });\n }\n\n\n function getPointLabel(entity, width, height, style) {\n var y = (style.geometry === 'point' ? -12 : 0);\n var pointOffsets = {\n ltr: [15, y, 'start'],\n rtl: [-15, y, 'end']\n };\n const isAddrMarker = style.isAddr && style.geometry !== 'vertex';\n\n var textDirection = localizer.textDirection();\n\n var coord = projection(entity.loc);\n var textPadding = 2;\n var offset = pointOffsets[textDirection];\n if (isAddrMarker) offset = [0, 1, 'middle'];\n var p = {\n height: height,\n width: width,\n x: coord[0] + offset[0],\n y: coord[1] + offset[1],\n textAnchor: offset[2]\n };\n\n // insert a collision box for the text label..\n let bbox;\n if (isAddrMarker) {\n bbox = {\n minX: p.x - (width / 2) - textPadding,\n minY: p.y - (height / 2) - textPadding,\n maxX: p.x + (width / 2) + textPadding,\n maxY: p.y + (height / 2) + textPadding\n };\n } else if (textDirection === 'rtl') {\n bbox = {\n minX: p.x - width - textPadding,\n minY: p.y - (height / 2) - textPadding,\n maxX: p.x + textPadding,\n maxY: p.y + (height / 2) + textPadding\n };\n } else {\n bbox = {\n minX: p.x - textPadding,\n minY: p.y - (height / 2) - textPadding,\n maxX: p.x + width + textPadding,\n maxY: p.y + (height / 2) + textPadding\n };\n }\n\n if (tryInsert([bbox], entity.id, true)) {\n return p;\n }\n }\n\n\n function getLineLabel(entity, width, height) {\n var viewport = geoExtent(context.projection.clipExtent()).polygon();\n var points = graph.childNodes(entity)\n .map(function(node) { return projection(node.loc); });\n var length = geoPathLength(points);\n\n if (length < width + 20) return;\n\n // % along the line to attempt to place the label\n var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70,\n 25, 75, 20, 80, 15, 95, 10, 90, 5, 95];\n var padding = 3;\n\n for (var i = 0; i < lineOffsets.length; i++) {\n var offset = lineOffsets[i];\n var middle = offset / 100 * length;\n var start = middle - width / 2;\n\n if (start < 0 || start + width > length) continue;\n\n // generate subpath and ignore paths that are invalid or don't cross viewport.\n var sub = subpath(points, start, start + width);\n if (!sub || !geoPolygonIntersectsPolygon(viewport, sub, true)) {\n continue;\n }\n\n var isReverse = reverse(sub);\n if (isReverse) {\n sub = sub.reverse();\n }\n\n var bboxes = [];\n var boxsize = (height + 2) / 2;\n\n for (var j = 0; j < sub.length - 1; j++) {\n var a = sub[j];\n var b = sub[j + 1];\n\n // split up the text into small collision boxes\n var num = Math.max(1, Math.floor(geoVecLength(a, b) / boxsize / 2));\n\n for (var box = 0; box < num; box++) {\n var p = geoVecInterp(a, b, box / num);\n var x0 = p[0] - boxsize - padding;\n var y0 = p[1] - boxsize - padding;\n var x1 = p[0] + boxsize + padding;\n var y1 = p[1] + boxsize + padding;\n\n bboxes.push({\n minX: Math.min(x0, x1),\n minY: Math.min(y0, y1),\n maxX: Math.max(x0, x1),\n maxY: Math.max(y0, y1)\n });\n }\n }\n\n if (tryInsert(bboxes, entity.id, false)) { // accept this one\n return {\n 'font-size': height + 2,\n lineString: lineString(sub),\n startOffset: offset + '%'\n };\n }\n }\n\n function reverse(p) {\n var angle = Math.atan2(p[1][1] - p[0][1], p[1][0] - p[0][0]);\n return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2);\n }\n\n function lineString(points) {\n return 'M' + points.join('L');\n }\n\n function subpath(points, from, to) {\n var sofar = 0;\n var start, end, i0, i1;\n\n for (var i = 0; i < points.length - 1; i++) {\n var a = points[i];\n var b = points[i + 1];\n var current = geoVecLength(a, b);\n var portion;\n if (!start && sofar + current >= from) {\n portion = (from - sofar) / current;\n start = [\n a[0] + portion * (b[0] - a[0]),\n a[1] + portion * (b[1] - a[1])\n ];\n i0 = i + 1;\n }\n if (!end && sofar + current >= to) {\n portion = (to - sofar) / current;\n end = [\n a[0] + portion * (b[0] - a[0]),\n a[1] + portion * (b[1] - a[1])\n ];\n i1 = i + 1;\n }\n sofar += current;\n }\n\n var result = points.slice(i0, i1);\n result.unshift(start);\n result.push(end);\n return result;\n }\n }\n\n\n function getAreaLabel(entity, width, height) {\n var centroid = path.centroid(entity.asGeoJSON(graph));\n var extent = entity.extent(graph);\n var areaWidth = projection(extent[1])[0] - projection(extent[0])[0];\n\n if (isNaN(centroid[0]) || areaWidth < 20) return;\n\n var preset = presetManager.match(entity, context.graph());\n var picon = preset && preset.icon;\n var iconSize = 17;\n var padding = 2;\n var p = {};\n\n if (picon && !shouldSkipIcon(preset)) { // icon and label..\n if (addIcon()) {\n addLabel(iconSize + padding);\n return p;\n }\n } else { // label only..\n if (addLabel(0)) {\n return p;\n }\n }\n\n\n function addIcon() {\n var iconX = centroid[0] - (iconSize / 2);\n var iconY = centroid[1] - (iconSize / 2);\n var bbox = {\n minX: iconX,\n minY: iconY,\n maxX: iconX + iconSize,\n maxY: iconY + iconSize\n };\n\n if (tryInsert([bbox], entity.id + 'I', true)) {\n p.transform = 'translate(' + iconX + ',' + iconY + ')';\n return true;\n }\n return false;\n }\n\n function addLabel(yOffset) {\n if (width && areaWidth >= width + 20) {\n var labelX = centroid[0];\n var labelY = centroid[1] + yOffset;\n var bbox = {\n minX: labelX - (width / 2) - padding,\n minY: labelY - (height / 2) - padding,\n maxX: labelX + (width / 2) + padding,\n maxY: labelY + (height / 2) + padding\n };\n\n if (tryInsert([bbox], entity.id, true)) {\n p.x = labelX;\n p.y = labelY;\n p.textAnchor = 'middle';\n p.height = height;\n return true;\n }\n }\n return false;\n }\n }\n\n\n // force insert a singular bounding box\n // singular box only, no array, id better be unique\n function doInsert(bbox, id) {\n bbox.id = id;\n\n var oldbox = _entitybboxes[id];\n if (oldbox) {\n _rdrawn.remove(oldbox);\n }\n _entitybboxes[id] = bbox;\n _rdrawn.insert(bbox);\n }\n\n function undoInsert(id) {\n var oldbox = _entitybboxes[id];\n if (oldbox) {\n _rdrawn.remove(oldbox);\n }\n delete _entitybboxes[id];\n }\n\n function tryInsert(bboxes, id, saveSkipped) {\n var skipped = false;\n\n for (var i = 0; i < bboxes.length; i++) {\n var bbox = bboxes[i];\n bbox.id = id;\n\n // Check that label is visible\n if (bbox.minX < 0 || bbox.minY < 0 || bbox.maxX > dimensions[0] || bbox.maxY > dimensions[1]) {\n skipped = true;\n break;\n }\n if (_rdrawn.collides(bbox)) {\n skipped = true;\n break;\n }\n }\n\n _entitybboxes[id] = bboxes;\n\n if (skipped) {\n if (saveSkipped) {\n _rskipped.load(bboxes);\n }\n } else {\n _rdrawn.load(bboxes);\n }\n\n return !skipped;\n }\n\n\n var layer = selection.selectAll('.layer-osm.labels');\n layer.selectAll('.labels-group')\n .data(['halo', 'label', 'debug'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'labels-group ' + d; });\n\n var halo = layer.selectAll('.labels-group.halo');\n var label = layer.selectAll('.labels-group.label');\n var debug = layer.selectAll('.labels-group.debug');\n\n // points\n drawPointLabels(label, labelled.point, filter, 'pointlabel');\n drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo');\n\n // lines\n drawLinePaths(layer, labelled.line, filter, '');\n drawLineLabels(label, labelled.line, filter, 'linelabel');\n drawLineLabels(halo, labelled.line, filter, 'linelabel-halo');\n\n // areas\n drawAreaLabels(label, labelled.area, filter, 'arealabel');\n drawAreaLabels(halo, labelled.area, filter, 'arealabel-halo');\n drawAreaIcons(label, labelled.area, filter, 'areaicon');\n drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo');\n\n // debug\n drawCollisionBoxes(debug, _rskipped, 'debug-skipped');\n drawCollisionBoxes(debug, _rdrawn, 'debug-drawn');\n\n layer.call(filterLabels);\n }\n\n\n function filterLabels(selection) {\n var drawLayer = selection.selectAll('.layer-osm.labels');\n var layers = drawLayer.selectAll('.labels-group.halo, .labels-group.label');\n\n layers.selectAll('.nolabel')\n .classed('nolabel', false);\n\n const graph = context.graph();\n const mouse = context.map().mouse();\n let bbox;\n let hideIds = [];\n\n // hide labels near the mouse\n if (mouse && context.mode().id !== 'browse') {\n const pad = 20;\n bbox = { minX: mouse[0] - pad, minY: mouse[1] - pad, maxX: mouse[0] + pad, maxY: mouse[1] + pad };\n const nearMouse = _rdrawn.search(bbox)\n .map(entity => entity.id)\n .filter(id =>\n context.mode().id !== 'select' ||\n // in select mode: hide labels of currently selected line(s)\n // to still allow accessing midpoints\n // https://github.com/openstreetmap/iD/issues/11220\n context.mode().selectedIDs().includes(id) && graph.hasEntity(id)?.geometry(graph) === 'line');\n hideIds.push.apply(hideIds, nearMouse);\n hideIds = utilArrayUniq(hideIds);\n }\n\n // don't hide label of currently selected entity while in e.g. drag mode\n const selected = (context.mode()?.selectedIDs?.() || [])\n .filter(id => graph.hasEntity(id)?.geometry(graph) !== 'line');\n hideIds = utilArrayDifference(hideIds, selected);\n\n layers.selectAll(utilEntitySelector(hideIds))\n .classed('nolabel', true);\n\n\n // draw the mouse bbox if debugging is on..\n var debug = selection.selectAll('.labels-group.debug');\n var gj = [];\n if (context.getDebug('collision')) {\n gj = bbox ? [{\n type: 'Polygon',\n coordinates: [[\n [bbox.minX, bbox.minY],\n [bbox.maxX, bbox.minY],\n [bbox.maxX, bbox.maxY],\n [bbox.minX, bbox.maxY],\n [bbox.minX, bbox.minY]\n ]]\n }] : [];\n }\n\n var box = debug.selectAll('.debug-mouse')\n .data(gj);\n\n // exit\n box.exit()\n .remove();\n\n // enter/update\n box.enter()\n .append('path')\n .attr('class', 'debug debug-mouse yellow')\n .merge(box)\n .attr('d', d3_geoPath());\n }\n\n\n var throttleFilterLabels = throttle(filterLabels, 100);\n\n\n drawLabels.observe = function(selection) {\n var listener = function() { throttleFilterLabels(selection); };\n selection.on('mousemove.hidelabels', listener);\n context.on('enter.hidelabels', listener);\n };\n\n\n drawLabels.off = function(selection) {\n throttleFilterLabels.cancel();\n selection.on('mousemove.hidelabels', null);\n context.on('enter.hidelabels', null);\n };\n\n\n return drawLabels;\n}\n\n\nconst _textWidthCache = {};\nexport function textWidth(text, size, container) {\n _textWidthCache[size] ||= {};\n let c = _textWidthCache[size];\n\n if (c[text]) {\n return c[text];\n }\n const elem = document.createElementNS('http://www.w3.org/2000/svg', 'text');\n elem.style.fontSize = `${size}px`;\n elem.style.fontWeight = 'bold';\n elem.textContent = text;\n container.appendChild(elem);\n c[text] = elem.getComputedTextLength();\n elem.remove();\n return c[text];\n}\n\n\nconst nonPrimaryKeys = new Set([\n 'building:flats',\n 'check_date',\n 'fixme',\n 'layer',\n 'level',\n 'level:ref',\n 'note'\n]);\nconst nonPrimaryKeysRegex = /^(ref|survey|note|([^:]+:|old_|alt_)addr):/;\nexport function isAddressPoint(tags) {\n const keys = Object.keys(tags).filter(key =>\n osmIsInterestingTag(key) &&\n !nonPrimaryKeys.has(key) &&\n !nonPrimaryKeysRegex.test(key)\n );\n return keys.length > 0 && keys.every(key =>\n key.startsWith('addr:')\n );\n}\n", "var e=\"undefined\"!=typeof self?self:global;const t=\"undefined\"!=typeof navigator,i=t&&\"undefined\"==typeof HTMLImageElement,n=!(\"undefined\"==typeof global||\"undefined\"==typeof process||!process.versions||!process.versions.node),s=e.Buffer,r=e.BigInt,a=!!s,o=e=>e;function l(e,t=o){if(n)try{return\"function\"==typeof require?Promise.resolve(t(require(e))):import(/* webpackIgnore: true */ e).then(t)}catch(t){console.warn(`Couldn't load ${e}`)}}let h=e.fetch;const u=e=>h=e;if(!e.fetch){const e=l(\"http\",(e=>e)),t=l(\"https\",(e=>e)),i=(n,{headers:s}={})=>new Promise((async(r,a)=>{let{port:o,hostname:l,pathname:h,protocol:u,search:c}=new URL(n);const f={method:\"GET\",hostname:l,path:encodeURI(h)+c,headers:s};\"\"!==o&&(f.port=Number(o));const d=(\"https:\"===u?await t:await e).request(f,(e=>{if(301===e.statusCode||302===e.statusCode){let t=new URL(e.headers.location,n).toString();return i(t,{headers:s}).then(r).catch(a)}r({status:e.statusCode,arrayBuffer:()=>new Promise((t=>{let i=[];e.on(\"data\",(e=>i.push(e))),e.on(\"end\",(()=>t(Buffer.concat(i))))}))})}));d.on(\"error\",a),d.end()}));u(i)}function c(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}const f=e=>p(e)?void 0:e,d=e=>void 0!==e;function p(e){return void 0===e||(e instanceof Map?0===e.size:0===Object.values(e).filter(d).length)}function g(e){let t=new Error(e);throw delete t.stack,t}function m(e){return\"\"===(e=function(e){for(;e.endsWith(\"\\0\");)e=e.slice(0,-1);return e}(e).trim())?void 0:e}function S(e){let t=function(e){let t=0;return e.ifd0.enabled&&(t+=1024),e.exif.enabled&&(t+=2048),e.makerNote&&(t+=2048),e.userComment&&(t+=1024),e.gps.enabled&&(t+=512),e.interop.enabled&&(t+=100),e.ifd1.enabled&&(t+=1024),t+2048}(e);return e.jfif.enabled&&(t+=50),e.xmp.enabled&&(t+=2e4),e.iptc.enabled&&(t+=14e3),e.icc.enabled&&(t+=6e3),t}const C=e=>String.fromCharCode.apply(null,e),y=\"undefined\"!=typeof TextDecoder?new TextDecoder(\"utf-8\"):void 0;function b(e){return y?y.decode(e):a?Buffer.from(e).toString(\"utf8\"):decodeURIComponent(escape(C(e)))}class I{static from(e,t){return e instanceof this&&e.le===t?e:new I(e,void 0,void 0,t)}constructor(e,t=0,i,n){if(\"boolean\"==typeof n&&(this.le=n),Array.isArray(e)&&(e=new Uint8Array(e)),0===e)this.byteOffset=0,this.byteLength=0;else if(e instanceof ArrayBuffer){void 0===i&&(i=e.byteLength-t);let n=new DataView(e,t,i);this._swapDataView(n)}else if(e instanceof Uint8Array||e instanceof DataView||e instanceof I){void 0===i&&(i=e.byteLength-t),(t+=e.byteOffset)+i>e.byteOffset+e.byteLength&&g(\"Creating view outside of available memory in ArrayBuffer\");let n=new DataView(e.buffer,t,i);this._swapDataView(n)}else if(\"number\"==typeof e){let t=new DataView(new ArrayBuffer(e));this._swapDataView(t)}else g(\"Invalid input argument for BufferView: \"+e)}_swapArrayBuffer(e){this._swapDataView(new DataView(e))}_swapBuffer(e){this._swapDataView(new DataView(e.buffer,e.byteOffset,e.byteLength))}_swapDataView(e){this.dataView=e,this.buffer=e.buffer,this.byteOffset=e.byteOffset,this.byteLength=e.byteLength}_lengthToEnd(e){return this.byteLength-e}set(e,t,i=I){return e instanceof DataView||e instanceof I?e=new Uint8Array(e.buffer,e.byteOffset,e.byteLength):e instanceof ArrayBuffer&&(e=new Uint8Array(e)),e instanceof Uint8Array||g(\"BufferView.set(): Invalid data argument.\"),this.toUint8().set(e,t),new i(this,t,e.byteLength)}subarray(e,t){return t=t||this._lengthToEnd(e),new I(this,e,t)}toUint8(){return new Uint8Array(this.buffer,this.byteOffset,this.byteLength)}getUint8Array(e,t){return new Uint8Array(this.buffer,this.byteOffset+e,t)}getString(e=0,t=this.byteLength){return b(this.getUint8Array(e,t))}getLatin1String(e=0,t=this.byteLength){let i=this.getUint8Array(e,t);return C(i)}getUnicodeString(e=0,t=this.byteLength){const i=[];for(let n=0;n1e4?v(e,i,\"base64\"):n&&e.includes(\"://\")?x(e,i,\"url\",M):n?v(e,i,\"fs\"):t?x(e,i,\"url\",M):void g(\"Invalid input argument\");var s}async function x(e,t,i,n){return A.has(i)?v(e,t,i):n?async function(e,t){let i=await t(e);return new I(i)}(e,n):void g(`Parser ${i} is not loaded`)}async function v(e,t,i){let n=new(A.get(i))(e,t);return await n.read(),n}const M=e=>h(e).then((e=>e.arrayBuffer())),R=e=>new Promise(((t,i)=>{let n=new FileReader;n.onloadend=()=>t(n.result||new ArrayBuffer),n.onerror=i,n.readAsArrayBuffer(e)}));class L extends Map{get tagKeys(){return this.allKeys||(this.allKeys=Array.from(this.keys())),this.allKeys}get tagValues(){return this.allValues||(this.allValues=Array.from(this.values())),this.allValues}}function U(e,t,i){let n=new L;for(let[e,t]of i)n.set(e,t);if(Array.isArray(t))for(let i of t)e.set(i,n);else e.set(t,n);return n}function F(e,t,i){let n,s=e.get(t);for(n of i)s.set(n[0],n[1])}const E=new Map,B=new Map,N=new Map,G=[\"chunked\",\"firstChunkSize\",\"firstChunkSizeNode\",\"firstChunkSizeBrowser\",\"chunkSize\",\"chunkLimit\"],V=[\"jfif\",\"xmp\",\"icc\",\"iptc\",\"ihdr\"],z=[\"tiff\",...V],H=[\"ifd0\",\"ifd1\",\"exif\",\"gps\",\"interop\"],j=[...z,...H],W=[\"makerNote\",\"userComment\"],K=[\"translateKeys\",\"translateValues\",\"reviveValues\",\"multiSegment\"],X=[...K,\"sanitize\",\"mergeOutput\",\"silentErrors\"];class _{get translate(){return this.translateKeys||this.translateValues||this.reviveValues}}class Y extends _{get needed(){return this.enabled||this.deps.size>0}constructor(e,t,i,n){if(super(),c(this,\"enabled\",!1),c(this,\"skip\",new Set),c(this,\"pick\",new Set),c(this,\"deps\",new Set),c(this,\"translateKeys\",!1),c(this,\"translateValues\",!1),c(this,\"reviveValues\",!1),this.key=e,this.enabled=t,this.parse=this.enabled,this.applyInheritables(n),this.canBeFiltered=H.includes(e),this.canBeFiltered&&(this.dict=E.get(e)),void 0!==i)if(Array.isArray(i))this.parse=this.enabled=!0,this.canBeFiltered&&i.length>0&&this.translateTagSet(i,this.pick);else if(\"object\"==typeof i){if(this.enabled=!0,this.parse=!1!==i.parse,this.canBeFiltered){let{pick:e,skip:t}=i;e&&e.length>0&&this.translateTagSet(e,this.pick),t&&t.length>0&&this.translateTagSet(t,this.skip)}this.applyInheritables(i)}else!0===i||!1===i?this.parse=this.enabled=i:g(`Invalid options argument: ${i}`)}applyInheritables(e){let t,i;for(t of K)i=e[t],void 0!==i&&(this[t]=i)}translateTagSet(e,t){if(this.dict){let i,n,{tagKeys:s,tagValues:r}=this.dict;for(i of e)\"string\"==typeof i?(n=r.indexOf(i),-1===n&&(n=s.indexOf(Number(i))),-1!==n&&t.add(Number(s[n]))):t.add(i)}else for(let i of e)t.add(i)}finalizeFilters(){!this.enabled&&this.deps.size>0?(this.enabled=!0,ee(this.pick,this.deps)):this.enabled&&this.pick.size>0&&ee(this.pick,this.deps)}}var $={jfif:!1,tiff:!0,xmp:!1,icc:!1,iptc:!1,ifd0:!0,ifd1:!1,exif:!0,gps:!0,interop:!1,ihdr:void 0,makerNote:!1,userComment:!1,multiSegment:!1,skip:[],pick:[],translateKeys:!0,translateValues:!0,reviveValues:!0,sanitize:!0,mergeOutput:!0,silentErrors:!0,chunked:!0,firstChunkSize:void 0,firstChunkSizeNode:512,firstChunkSizeBrowser:65536,chunkSize:65536,chunkLimit:5},J=new Map;class q extends _{static useCached(e){let t=J.get(e);return void 0!==t||(t=new this(e),J.set(e,t)),t}constructor(e){super(),!0===e?this.setupFromTrue():void 0===e?this.setupFromUndefined():Array.isArray(e)?this.setupFromArray(e):\"object\"==typeof e?this.setupFromObject(e):g(`Invalid options argument ${e}`),void 0===this.firstChunkSize&&(this.firstChunkSize=t?this.firstChunkSizeBrowser:this.firstChunkSizeNode),this.mergeOutput&&(this.ifd1.enabled=!1),this.filterNestedSegmentTags(),this.traverseTiffDependencyTree(),this.checkLoadedPlugins()}setupFromUndefined(){let e;for(e of G)this[e]=$[e];for(e of X)this[e]=$[e];for(e of W)this[e]=$[e];for(e of j)this[e]=new Y(e,$[e],void 0,this)}setupFromTrue(){let e;for(e of G)this[e]=$[e];for(e of X)this[e]=$[e];for(e of W)this[e]=!0;for(e of j)this[e]=new Y(e,!0,void 0,this)}setupFromArray(e){let t;for(t of G)this[t]=$[t];for(t of X)this[t]=$[t];for(t of W)this[t]=$[t];for(t of j)this[t]=new Y(t,!1,void 0,this);this.setupGlobalFilters(e,void 0,H)}setupFromObject(e){let t;for(t of(H.ifd0=H.ifd0||H.image,H.ifd1=H.ifd1||H.thumbnail,Object.assign(this,e),G))this[t]=Z(e[t],$[t]);for(t of X)this[t]=Z(e[t],$[t]);for(t of W)this[t]=Z(e[t],$[t]);for(t of z)this[t]=new Y(t,$[t],e[t],this);for(t of H)this[t]=new Y(t,$[t],e[t],this.tiff);this.setupGlobalFilters(e.pick,e.skip,H,j),!0===e.tiff?this.batchEnableWithBool(H,!0):!1===e.tiff?this.batchEnableWithUserValue(H,e):Array.isArray(e.tiff)?this.setupGlobalFilters(e.tiff,void 0,H):\"object\"==typeof e.tiff&&this.setupGlobalFilters(e.tiff.pick,e.tiff.skip,H)}batchEnableWithBool(e,t){for(let i of e)this[i].enabled=t}batchEnableWithUserValue(e,t){for(let i of e){let e=t[i];this[i].enabled=!1!==e&&void 0!==e}}setupGlobalFilters(e,t,i,n=i){if(e&&e.length){for(let e of n)this[e].enabled=!1;let t=Q(e,i);for(let[e,i]of t)ee(this[e].pick,i),this[e].enabled=!0}else if(t&&t.length){let e=Q(t,i);for(let[t,i]of e)ee(this[t].skip,i)}}filterNestedSegmentTags(){let{ifd0:e,exif:t,xmp:i,iptc:n,icc:s}=this;this.makerNote?t.deps.add(37500):t.skip.add(37500),this.userComment?t.deps.add(37510):t.skip.add(37510),i.enabled||e.skip.add(700),n.enabled||e.skip.add(33723),s.enabled||e.skip.add(34675)}traverseTiffDependencyTree(){let{ifd0:e,exif:t,gps:i,interop:n}=this;n.needed&&(t.deps.add(40965),e.deps.add(40965)),t.needed&&e.deps.add(34665),i.needed&&e.deps.add(34853),this.tiff.enabled=H.some((e=>!0===this[e].enabled))||this.makerNote||this.userComment;for(let e of H)this[e].finalizeFilters()}get onlyTiff(){return!V.map((e=>this[e].enabled)).some((e=>!0===e))&&this.tiff.enabled}checkLoadedPlugins(){for(let e of z)this[e].enabled&&!T.has(e)&&P(\"segment parser\",e)}}function Q(e,t){let i,n,s,r,a=[];for(s of t){for(r of(i=E.get(s),n=[],i))(e.includes(r[0])||e.includes(r[1]))&&n.push(r[0]);n.length&&a.push([s,n])}return a}function Z(e,t){return void 0!==e?e:void 0!==t?t:void 0}function ee(e,t){for(let i of t)e.add(i)}c(q,\"default\",$);class te{constructor(e){c(this,\"parsers\",{}),c(this,\"output\",{}),c(this,\"errors\",[]),c(this,\"pushToErrors\",(e=>this.errors.push(e))),this.options=q.useCached(e)}async read(e){this.file=await D(e,this.options)}setup(){if(this.fileParser)return;let{file:e}=this,t=e.getUint16(0);for(let[i,n]of w)if(n.canHandle(e,t))return this.fileParser=new n(this.options,this.file,this.parsers),e[i]=!0;this.file.close&&this.file.close(),g(\"Unknown file format\")}async parse(){let{output:e,errors:t}=this;return this.setup(),this.options.silentErrors?(await this.executeParsers().catch(this.pushToErrors),t.push(...this.fileParser.errors)):await this.executeParsers(),this.file.close&&this.file.close(),this.options.silentErrors&&t.length>0&&(e.errors=t),f(e)}async executeParsers(){let{output:e}=this;await this.fileParser.parse();let t=Object.values(this.parsers).map((async t=>{let i=await t.parse();t.assignToOutput(e,i)}));this.options.silentErrors&&(t=t.map((e=>e.catch(this.pushToErrors)))),await Promise.all(t)}async extractThumbnail(){this.setup();let{options:e,file:t}=this,i=T.get(\"tiff\",e);var n;if(t.tiff?n={start:0,type:\"tiff\"}:t.jpeg&&(n=await this.fileParser.getOrFindSegment(\"tiff\")),void 0===n)return;let s=await this.fileParser.ensureSegmentChunk(n),r=this.parsers.tiff=new i(s,e,t),a=await r.extractThumbnail();return t.close&&t.close(),a}}async function ie(e,t){let i=new te(t);return await i.read(e),i.parse()}var ne=Object.freeze({__proto__:null,parse:ie,Exifr:te,fileParsers:w,segmentParsers:T,fileReaders:A,tagKeys:E,tagValues:B,tagRevivers:N,createDictionary:U,extendDictionary:F,fetchUrlAsArrayBuffer:M,readBlobAsArrayBuffer:R,chunkedProps:G,otherSegments:V,segments:z,tiffBlocks:H,segmentsAndBlocks:j,tiffExtractables:W,inheritables:K,allFormatters:X,Options:q});class se{constructor(e,t,i){c(this,\"errors\",[]),c(this,\"ensureSegmentChunk\",(async e=>{let t=e.start,i=e.size||65536;if(this.file.chunked)if(this.file.available(t,i))e.chunk=this.file.subarray(t,i);else try{e.chunk=await this.file.readChunk(t,i)}catch(t){g(`Couldn't read segment: ${JSON.stringify(e)}. ${t.message}`)}else this.file.byteLength>t+i?e.chunk=this.file.subarray(t,i):void 0===e.size?e.chunk=this.file.subarray(t):g(\"Segment unreachable: \"+JSON.stringify(e));return e.chunk})),this.extendOptions&&this.extendOptions(e),this.options=e,this.file=t,this.parsers=i}injectSegment(e,t){this.options[e].enabled&&this.createParser(e,t)}createParser(e,t){let i=new(T.get(e))(t,this.options,this.file);return this.parsers[e]=i}createParsers(e){for(let t of e){let{type:e,chunk:i}=t,n=this.options[e];if(n&&n.enabled){let t=this.parsers[e];t&&t.append||t||this.createParser(e,i)}}}async readSegments(e){let t=e.map(this.ensureSegmentChunk);await Promise.all(t)}}class re{static findPosition(e,t){let i=e.getUint16(t+2)+2,n=\"function\"==typeof this.headerLength?this.headerLength(e,t,i):this.headerLength,s=t+n,r=i-n;return{offset:t,length:i,headerLength:n,start:s,size:r,end:s+r}}static parse(e,t={}){return new this(e,new q({[this.type]:t}),e).parse()}normalizeInput(e){return e instanceof I?e:new I(e)}constructor(e,t={},i){c(this,\"errors\",[]),c(this,\"raw\",new Map),c(this,\"handleError\",(e=>{if(!this.options.silentErrors)throw e;this.errors.push(e.message)})),this.chunk=this.normalizeInput(e),this.file=i,this.type=this.constructor.type,this.globalOptions=this.options=t,this.localOptions=t[this.type],this.canTranslate=this.localOptions&&this.localOptions.translate}translate(){this.canTranslate&&(this.translated=this.translateBlock(this.raw,this.type))}get output(){return this.translated?this.translated:this.raw?Object.fromEntries(this.raw):void 0}translateBlock(e,t){let i=N.get(t),n=B.get(t),s=E.get(t),r=this.options[t],a=r.reviveValues&&!!i,o=r.translateValues&&!!n,l=r.translateKeys&&!!s,h={};for(let[t,r]of e)a&&i.has(t)?r=i.get(t)(r):o&&n.has(t)&&(r=this.translateValue(r,n.get(t))),l&&s.has(t)&&(t=s.get(t)||t),h[t]=r;return h}translateValue(e,t){return t[e]||t.DEFAULT||e}assignToOutput(e,t){this.assignObjectToOutput(e,this.constructor.type,t)}assignObjectToOutput(e,t,i){if(this.globalOptions.mergeOutput)return Object.assign(e,i);e[t]?Object.assign(e[t],i):e[t]=i}}c(re,\"headerLength\",4),c(re,\"type\",void 0),c(re,\"multiSegment\",!1),c(re,\"canHandle\",(()=>!1));function ae(e){return 192===e||194===e||196===e||219===e||221===e||218===e||254===e}function oe(e){return e>=224&&e<=239}function le(e,t,i){for(let[n,s]of T)if(s.canHandle(e,t,i))return n}class he extends se{constructor(...e){super(...e),c(this,\"appSegments\",[]),c(this,\"jpegSegments\",[]),c(this,\"unknownSegments\",[])}static canHandle(e,t){return 65496===t}async parse(){await this.findAppSegments(),await this.readSegments(this.appSegments),this.mergeMultiSegments(),this.createParsers(this.mergedAppSegments||this.appSegments)}setupSegmentFinderArgs(e){!0===e?(this.findAll=!0,this.wanted=new Set(T.keyList())):(e=void 0===e?T.keyList().filter((e=>this.options[e].enabled)):e.filter((e=>this.options[e].enabled&&T.has(e))),this.findAll=!1,this.remaining=new Set(e),this.wanted=new Set(e)),this.unfinishedMultiSegment=!1}async findAppSegments(e=0,t){this.setupSegmentFinderArgs(t);let{file:i,findAll:n,wanted:s,remaining:r}=this;if(!n&&this.file.chunked&&(n=Array.from(s).some((e=>{let t=T.get(e),i=this.options[e];return t.multiSegment&&i.multiSegment})),n&&await this.file.readWhole()),e=this.findAppSegmentsInRange(e,i.byteLength),!this.options.onlyTiff&&i.chunked){let t=!1;for(;r.size>0&&!t&&(i.canReadNextChunk||this.unfinishedMultiSegment);){let{nextChunkOffset:n}=i,s=this.appSegments.some((e=>!this.file.available(e.offset||e.start,e.length||e.size)));if(t=e>n&&!s?!await i.readNextChunk(e):!await i.readNextChunk(n),void 0===(e=this.findAppSegmentsInRange(e,i.byteLength)))return}}}findAppSegmentsInRange(e,t){t-=2;let i,n,s,r,a,o,{file:l,findAll:h,wanted:u,remaining:c,options:f}=this;for(;ee.multiSegment)))return;let e=function(e,t){let i,n,s,r=new Map;for(let a=0;a{let i=T.get(e,this.options);if(i.handleMultiSegments){return{type:e,chunk:i.handleMultiSegments(t)}}return t[0]}))}getSegment(e){return this.appSegments.find((t=>t.type===e))}async getOrFindSegment(e){let t=this.getSegment(e);return void 0===t&&(await this.findAppSegments(0,[e]),t=this.getSegment(e)),t}}c(he,\"type\",\"jpeg\"),w.set(\"jpeg\",he);const ue=[void 0,1,1,2,4,8,1,1,2,4,8,4,8,4];class ce extends re{parseHeader(){var e=this.chunk.getUint16();18761===e?this.le=!0:19789===e&&(this.le=!1),this.chunk.le=this.le,this.headerParsed=!0}parseTags(e,t,i=new Map){let{pick:n,skip:s}=this.options[t];n=new Set(n);let r=n.size>0,a=0===s.size,o=this.chunk.getUint16(e);e+=2;for(let l=0;l13)&&g(`Invalid TIFF value type. block: ${i.toUpperCase()}, tag: ${t.toString(16)}, type: ${s}, offset ${e}`),e>n.byteLength&&g(`Invalid TIFF value offset. block: ${i.toUpperCase()}, tag: ${t.toString(16)}, type: ${s}, offset ${e} is outside of chunk size ${n.byteLength}`),1===s)return n.getUint8Array(e,r);if(2===s)return m(n.getString(e,r));if(7===s)return n.getUint8Array(e,r);if(1===r)return this.parseTagValue(s,e);{let t=new(function(e){switch(e){case 1:return Uint8Array;case 3:return Uint16Array;case 4:return Uint32Array;case 5:return Array;case 6:return Int8Array;case 8:return Int16Array;case 9:return Int32Array;case 10:return Array;case 11:return Float32Array;case 12:return Float64Array;default:return Array}}(s))(r),i=a;for(let n=0;ne.byteLength&&g(`IFD0 offset points to outside of file.\\nthis.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e.byteLength}`),e.tiff&&await e.ensureChunk(this.ifd0Offset,S(this.options));let t=this.parseBlock(this.ifd0Offset,\"ifd0\");return 0!==t.size?(this.exifOffset=t.get(34665),this.interopOffset=t.get(40965),this.gpsOffset=t.get(34853),this.xmp=t.get(700),this.iptc=t.get(33723),this.icc=t.get(34675),this.options.sanitize&&(t.delete(34665),t.delete(40965),t.delete(34853),t.delete(700),t.delete(33723),t.delete(34675)),t):void 0}async parseExifBlock(){if(this.exif)return;if(this.ifd0||await this.parseIfd0Block(),void 0===this.exifOffset)return;this.file.tiff&&await this.file.ensureChunk(this.exifOffset,S(this.options));let e=this.parseBlock(this.exifOffset,\"exif\");return this.interopOffset||(this.interopOffset=e.get(40965)),this.makerNote=e.get(37500),this.userComment=e.get(37510),this.options.sanitize&&(e.delete(40965),e.delete(37500),e.delete(37510)),this.unpack(e,41728),this.unpack(e,41729),e}unpack(e,t){let i=e.get(t);i&&1===i.length&&e.set(t,i[0])}async parseGpsBlock(){if(this.gps)return;if(this.ifd0||await this.parseIfd0Block(),void 0===this.gpsOffset)return;let e=this.parseBlock(this.gpsOffset,\"gps\");return e&&e.has(2)&&e.has(4)&&(e.set(\"latitude\",de(...e.get(2),e.get(1))),e.set(\"longitude\",de(...e.get(4),e.get(3)))),e}async parseInteropBlock(){if(!this.interop&&(this.ifd0||await this.parseIfd0Block(),void 0!==this.interopOffset||this.exif||await this.parseExifBlock(),void 0!==this.interopOffset))return this.parseBlock(this.interopOffset,\"interop\")}async parseThumbnailBlock(e=!1){if(!this.ifd1&&!this.ifd1Parsed&&(!this.options.mergeOutput||e))return this.findIfd1Offset(),this.ifd1Offset>0&&(this.parseBlock(this.ifd1Offset,\"ifd1\"),this.ifd1Parsed=!0),this.ifd1}async extractThumbnail(){if(this.headerParsed||this.parseHeader(),this.ifd1Parsed||await this.parseThumbnailBlock(!0),void 0===this.ifd1)return;let e=this.ifd1.get(513),t=this.ifd1.get(514);return this.chunk.getUint8Array(e,t)}get image(){return this.ifd0}get thumbnail(){return this.ifd1}createOutput(){let e,t,i,n={};for(t of H)if(e=this[t],!p(e))if(i=this.canTranslate?this.translateBlock(e,t):Object.fromEntries(e),this.options.mergeOutput){if(\"ifd1\"===t)continue;Object.assign(n,i)}else n[t]=i;return this.makerNote&&(n.makerNote=this.makerNote),this.userComment&&(n.userComment=this.userComment),n}assignToOutput(e,t){if(this.globalOptions.mergeOutput)Object.assign(e,t);else for(let[i,n]of Object.entries(t))this.assignObjectToOutput(e,i,n)}}function de(e,t,i,n){var s=e+t/60+i/3600;return\"S\"!==n&&\"W\"!==n||(s*=-1),s}c(fe,\"type\",\"tiff\"),c(fe,\"headerLength\",10),T.set(\"tiff\",fe);var pe=Object.freeze({__proto__:null,default:ne,Exifr:te,fileParsers:w,segmentParsers:T,fileReaders:A,tagKeys:E,tagValues:B,tagRevivers:N,createDictionary:U,extendDictionary:F,fetchUrlAsArrayBuffer:M,readBlobAsArrayBuffer:R,chunkedProps:G,otherSegments:V,segments:z,tiffBlocks:H,segmentsAndBlocks:j,tiffExtractables:W,inheritables:K,allFormatters:X,Options:q,parse:ie});const ge={ifd0:!1,ifd1:!1,exif:!1,gps:!1,interop:!1,sanitize:!1,reviveValues:!0,translateKeys:!1,translateValues:!1,mergeOutput:!1},me=Object.assign({},ge,{firstChunkSize:4e4,gps:[1,2,3,4]});async function Se(e){let t=new te(me);await t.read(e);let i=await t.parse();if(i&&i.gps){let{latitude:e,longitude:t}=i.gps;return{latitude:e,longitude:t}}}const Ce=Object.assign({},ge,{tiff:!1,ifd1:!0,mergeOutput:!1});async function ye(e){let t=new te(Ce);await t.read(e);let i=await t.extractThumbnail();return i&&a?s.from(i):i}async function be(e){let t=await this.thumbnail(e);if(void 0!==t){let e=new Blob([t]);return URL.createObjectURL(e)}}const Ie=Object.assign({},ge,{firstChunkSize:4e4,ifd0:[274]});async function Pe(e){let t=new te(Ie);await t.read(e);let i=await t.parse();if(i&&i.ifd0)return i.ifd0[274]}const ke=Object.freeze({1:{dimensionSwapped:!1,scaleX:1,scaleY:1,deg:0,rad:0},2:{dimensionSwapped:!1,scaleX:-1,scaleY:1,deg:0,rad:0},3:{dimensionSwapped:!1,scaleX:1,scaleY:1,deg:180,rad:180*Math.PI/180},4:{dimensionSwapped:!1,scaleX:-1,scaleY:1,deg:180,rad:180*Math.PI/180},5:{dimensionSwapped:!0,scaleX:1,scaleY:-1,deg:90,rad:90*Math.PI/180},6:{dimensionSwapped:!0,scaleX:1,scaleY:1,deg:90,rad:90*Math.PI/180},7:{dimensionSwapped:!0,scaleX:1,scaleY:-1,deg:270,rad:270*Math.PI/180},8:{dimensionSwapped:!0,scaleX:1,scaleY:1,deg:270,rad:270*Math.PI/180}});let we=!0,Te=!0;if(\"object\"==typeof navigator){let e=navigator.userAgent;if(e.includes(\"iPad\")||e.includes(\"iPhone\")){let t=e.match(/OS (\\d+)_(\\d+)/);if(t){let[,e,i]=t,n=Number(e)+.1*Number(i);we=n<13.4,Te=!1}}else if(e.includes(\"OS X 10\")){let[,t]=e.match(/OS X 10[_.](\\d+)/);we=Te=Number(t)<15}if(e.includes(\"Chrome/\")){let[,t]=e.match(/Chrome\\/(\\d+)/);we=Te=Number(t)<81}else if(e.includes(\"Firefox/\")){let[,t]=e.match(/Firefox\\/(\\d+)/);we=Te=Number(t)<77}}async function Ae(e){let t=await Pe(e);return Object.assign({canvas:we,css:Te},ke[t])}class De extends I{constructor(...e){super(...e),c(this,\"ranges\",new Oe),0!==this.byteLength&&this.ranges.add(0,this.byteLength)}_tryExtend(e,t,i){if(0===e&&0===this.byteLength&&i){let e=new DataView(i.buffer||i,i.byteOffset,i.byteLength);this._swapDataView(e)}else{let i=e+t;if(i>this.byteLength){let{dataView:e}=this._extend(i);this._swapDataView(e)}}}_extend(e){let t;t=a?s.allocUnsafe(e):new Uint8Array(e);let i=new DataView(t.buffer,t.byteOffset,t.byteLength);return t.set(new Uint8Array(this.buffer,this.byteOffset,this.byteLength),0),{uintView:t,dataView:i}}subarray(e,t,i=!1){return t=t||this._lengthToEnd(e),i&&this._tryExtend(e,t),this.ranges.add(e,t),super.subarray(e,t)}set(e,t,i=!1){i&&this._tryExtend(t,e.byteLength,e);let n=super.set(e,t);return this.ranges.add(t,n.byteLength),n}async ensureChunk(e,t){this.chunked&&(this.ranges.available(e,t)||await this.readChunk(e,t))}available(e,t){return this.ranges.available(e,t)}}class Oe{constructor(){c(this,\"list\",[])}get length(){return this.list.length}add(e,t,i=0){let n=e+t,s=this.list.filter((t=>xe(e,t.offset,n)||xe(e,t.end,n)));if(s.length>0){e=Math.min(e,...s.map((e=>e.offset))),n=Math.max(n,...s.map((e=>e.end))),t=n-e;let i=s.shift();i.offset=e,i.length=t,i.end=n,this.list=this.list.filter((e=>!s.includes(e)))}else this.list.push({offset:e,length:t,end:n})}available(e,t){let i=e+t;return this.list.some((t=>t.offset<=e&&i<=t.end))}}function xe(e,t,i){return e<=t&&t<=i}class ve extends De{constructor(e,t){super(0),c(this,\"chunksRead\",0),this.input=e,this.options=t}async readWhole(){this.chunked=!1,await this.readChunk(this.nextChunkOffset)}async readChunked(){this.chunked=!0,await this.readChunk(0,this.options.firstChunkSize)}async readNextChunk(e=this.nextChunkOffset){if(this.fullyRead)return this.chunksRead++,!1;let t=this.options.chunkSize,i=await this.readChunk(e,t);return!!i&&i.byteLength===t}async readChunk(e,t){if(this.chunksRead++,0!==(t=this.safeWrapAddress(e,t)))return this._readChunk(e,t)}safeWrapAddress(e,t){return void 0!==this.size&&e+t>this.size?Math.max(0,this.size-e):t}get nextChunkOffset(){if(0!==this.ranges.list.length)return this.ranges.list[0].length}get canReadNextChunk(){return this.chunksReade.kind===t))}parseBoxHead(e){let t=this.file.getUint32(e),i=this.file.getString(e+4,4),n=e+8;return 1===t&&(t=this.file.getUint64(e+8),n+=8),{offset:e,length:t,kind:i,start:n}}parseBoxFullHead(e){if(void 0!==e.version)return;let t=this.file.getUint32(e.start);e.version=t>>24,e.start+=4}}class Le extends Re{static canHandle(e,t){if(0!==t)return!1;let i=e.getUint16(2);if(i>50)return!1;let n=16,s=[];for(;n=2&&(n=3===t.version?4:2,s=this.file.getString(i+n+2,4),\"Exif\"===s))return this.file.getUintBytes(i,n);r+=t.length}}get8bits(e){let t=this.file.getUint8(e);return[t>>4,15&t]}findExtentInIloc(e,t){this.parseBoxFullHead(e);let i=e.start,[n,s]=this.get8bits(i++),[r,a]=this.get8bits(i++),o=2===e.version?4:2,l=1===e.version||2===e.version?2:0,h=a+n+s,u=2===e.version?4:2,c=this.file.getUintBytes(i,u);for(i+=u;c--;){let e=this.file.getUintBytes(i,o);i+=o+l+2+r;let u=this.file.getUint16(i);if(i+=2,e===t)return u>1&&console.warn(\"ILOC box has more than one extent but we're only processing one\\nPlease create an issue at https://github.com/MikeKovarik/exifr with this file\"),[this.file.getUintBytes(i+a,n),this.file.getUintBytes(i+a+n,s)];i+=u*h}}}class Ue extends Le{}c(Ue,\"type\",\"heic\");class Fe extends Le{}c(Fe,\"type\",\"avif\"),w.set(\"heic\",Ue),w.set(\"avif\",Fe),U(E,[\"ifd0\",\"ifd1\"],[[256,\"ImageWidth\"],[257,\"ImageHeight\"],[258,\"BitsPerSample\"],[259,\"Compression\"],[262,\"PhotometricInterpretation\"],[270,\"ImageDescription\"],[271,\"Make\"],[272,\"Model\"],[273,\"StripOffsets\"],[274,\"Orientation\"],[277,\"SamplesPerPixel\"],[278,\"RowsPerStrip\"],[279,\"StripByteCounts\"],[282,\"XResolution\"],[283,\"YResolution\"],[284,\"PlanarConfiguration\"],[296,\"ResolutionUnit\"],[301,\"TransferFunction\"],[305,\"Software\"],[306,\"ModifyDate\"],[315,\"Artist\"],[316,\"HostComputer\"],[317,\"Predictor\"],[318,\"WhitePoint\"],[319,\"PrimaryChromaticities\"],[513,\"ThumbnailOffset\"],[514,\"ThumbnailLength\"],[529,\"YCbCrCoefficients\"],[530,\"YCbCrSubSampling\"],[531,\"YCbCrPositioning\"],[532,\"ReferenceBlackWhite\"],[700,\"ApplicationNotes\"],[33432,\"Copyright\"],[33723,\"IPTC\"],[34665,\"ExifIFD\"],[34675,\"ICC\"],[34853,\"GpsIFD\"],[330,\"SubIFD\"],[40965,\"InteropIFD\"],[40091,\"XPTitle\"],[40092,\"XPComment\"],[40093,\"XPAuthor\"],[40094,\"XPKeywords\"],[40095,\"XPSubject\"]]),U(E,\"exif\",[[33434,\"ExposureTime\"],[33437,\"FNumber\"],[34850,\"ExposureProgram\"],[34852,\"SpectralSensitivity\"],[34855,\"ISO\"],[34858,\"TimeZoneOffset\"],[34859,\"SelfTimerMode\"],[34864,\"SensitivityType\"],[34865,\"StandardOutputSensitivity\"],[34866,\"RecommendedExposureIndex\"],[34867,\"ISOSpeed\"],[34868,\"ISOSpeedLatitudeyyy\"],[34869,\"ISOSpeedLatitudezzz\"],[36864,\"ExifVersion\"],[36867,\"DateTimeOriginal\"],[36868,\"CreateDate\"],[36873,\"GooglePlusUploadCode\"],[36880,\"OffsetTime\"],[36881,\"OffsetTimeOriginal\"],[36882,\"OffsetTimeDigitized\"],[37121,\"ComponentsConfiguration\"],[37122,\"CompressedBitsPerPixel\"],[37377,\"ShutterSpeedValue\"],[37378,\"ApertureValue\"],[37379,\"BrightnessValue\"],[37380,\"ExposureCompensation\"],[37381,\"MaxApertureValue\"],[37382,\"SubjectDistance\"],[37383,\"MeteringMode\"],[37384,\"LightSource\"],[37385,\"Flash\"],[37386,\"FocalLength\"],[37393,\"ImageNumber\"],[37394,\"SecurityClassification\"],[37395,\"ImageHistory\"],[37396,\"SubjectArea\"],[37500,\"MakerNote\"],[37510,\"UserComment\"],[37520,\"SubSecTime\"],[37521,\"SubSecTimeOriginal\"],[37522,\"SubSecTimeDigitized\"],[37888,\"AmbientTemperature\"],[37889,\"Humidity\"],[37890,\"Pressure\"],[37891,\"WaterDepth\"],[37892,\"Acceleration\"],[37893,\"CameraElevationAngle\"],[40960,\"FlashpixVersion\"],[40961,\"ColorSpace\"],[40962,\"ExifImageWidth\"],[40963,\"ExifImageHeight\"],[40964,\"RelatedSoundFile\"],[41483,\"FlashEnergy\"],[41486,\"FocalPlaneXResolution\"],[41487,\"FocalPlaneYResolution\"],[41488,\"FocalPlaneResolutionUnit\"],[41492,\"SubjectLocation\"],[41493,\"ExposureIndex\"],[41495,\"SensingMethod\"],[41728,\"FileSource\"],[41729,\"SceneType\"],[41730,\"CFAPattern\"],[41985,\"CustomRendered\"],[41986,\"ExposureMode\"],[41987,\"WhiteBalance\"],[41988,\"DigitalZoomRatio\"],[41989,\"FocalLengthIn35mmFormat\"],[41990,\"SceneCaptureType\"],[41991,\"GainControl\"],[41992,\"Contrast\"],[41993,\"Saturation\"],[41994,\"Sharpness\"],[41996,\"SubjectDistanceRange\"],[42016,\"ImageUniqueID\"],[42032,\"OwnerName\"],[42033,\"SerialNumber\"],[42034,\"LensInfo\"],[42035,\"LensMake\"],[42036,\"LensModel\"],[42037,\"LensSerialNumber\"],[42080,\"CompositeImage\"],[42081,\"CompositeImageCount\"],[42082,\"CompositeImageExposureTimes\"],[42240,\"Gamma\"],[59932,\"Padding\"],[59933,\"OffsetSchema\"],[65e3,\"OwnerName\"],[65001,\"SerialNumber\"],[65002,\"Lens\"],[65100,\"RawFile\"],[65101,\"Converter\"],[65102,\"WhiteBalance\"],[65105,\"Exposure\"],[65106,\"Shadows\"],[65107,\"Brightness\"],[65108,\"Contrast\"],[65109,\"Saturation\"],[65110,\"Sharpness\"],[65111,\"Smoothness\"],[65112,\"MoireFilter\"],[40965,\"InteropIFD\"]]),U(E,\"gps\",[[0,\"GPSVersionID\"],[1,\"GPSLatitudeRef\"],[2,\"GPSLatitude\"],[3,\"GPSLongitudeRef\"],[4,\"GPSLongitude\"],[5,\"GPSAltitudeRef\"],[6,\"GPSAltitude\"],[7,\"GPSTimeStamp\"],[8,\"GPSSatellites\"],[9,\"GPSStatus\"],[10,\"GPSMeasureMode\"],[11,\"GPSDOP\"],[12,\"GPSSpeedRef\"],[13,\"GPSSpeed\"],[14,\"GPSTrackRef\"],[15,\"GPSTrack\"],[16,\"GPSImgDirectionRef\"],[17,\"GPSImgDirection\"],[18,\"GPSMapDatum\"],[19,\"GPSDestLatitudeRef\"],[20,\"GPSDestLatitude\"],[21,\"GPSDestLongitudeRef\"],[22,\"GPSDestLongitude\"],[23,\"GPSDestBearingRef\"],[24,\"GPSDestBearing\"],[25,\"GPSDestDistanceRef\"],[26,\"GPSDestDistance\"],[27,\"GPSProcessingMethod\"],[28,\"GPSAreaInformation\"],[29,\"GPSDateStamp\"],[30,\"GPSDifferential\"],[31,\"GPSHPositioningError\"]]),U(B,[\"ifd0\",\"ifd1\"],[[274,{1:\"Horizontal (normal)\",2:\"Mirror horizontal\",3:\"Rotate 180\",4:\"Mirror vertical\",5:\"Mirror horizontal and rotate 270 CW\",6:\"Rotate 90 CW\",7:\"Mirror horizontal and rotate 90 CW\",8:\"Rotate 270 CW\"}],[296,{1:\"None\",2:\"inches\",3:\"cm\"}]]);let Ee=U(B,\"exif\",[[34850,{0:\"Not defined\",1:\"Manual\",2:\"Normal program\",3:\"Aperture priority\",4:\"Shutter priority\",5:\"Creative program\",6:\"Action program\",7:\"Portrait mode\",8:\"Landscape mode\"}],[37121,{0:\"-\",1:\"Y\",2:\"Cb\",3:\"Cr\",4:\"R\",5:\"G\",6:\"B\"}],[37383,{0:\"Unknown\",1:\"Average\",2:\"CenterWeightedAverage\",3:\"Spot\",4:\"MultiSpot\",5:\"Pattern\",6:\"Partial\",255:\"Other\"}],[37384,{0:\"Unknown\",1:\"Daylight\",2:\"Fluorescent\",3:\"Tungsten (incandescent light)\",4:\"Flash\",9:\"Fine weather\",10:\"Cloudy weather\",11:\"Shade\",12:\"Daylight fluorescent (D 5700 - 7100K)\",13:\"Day white fluorescent (N 4600 - 5400K)\",14:\"Cool white fluorescent (W 3900 - 4500K)\",15:\"White fluorescent (WW 3200 - 3700K)\",17:\"Standard light A\",18:\"Standard light B\",19:\"Standard light C\",20:\"D55\",21:\"D65\",22:\"D75\",23:\"D50\",24:\"ISO studio tungsten\",255:\"Other\"}],[37385,{0:\"Flash did not fire\",1:\"Flash fired\",5:\"Strobe return light not detected\",7:\"Strobe return light detected\",9:\"Flash fired, compulsory flash mode\",13:\"Flash fired, compulsory flash mode, return light not detected\",15:\"Flash fired, compulsory flash mode, return light detected\",16:\"Flash did not fire, compulsory flash mode\",24:\"Flash did not fire, auto mode\",25:\"Flash fired, auto mode\",29:\"Flash fired, auto mode, return light not detected\",31:\"Flash fired, auto mode, return light detected\",32:\"No flash function\",65:\"Flash fired, red-eye reduction mode\",69:\"Flash fired, red-eye reduction mode, return light not detected\",71:\"Flash fired, red-eye reduction mode, return light detected\",73:\"Flash fired, compulsory flash mode, red-eye reduction mode\",77:\"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected\",79:\"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected\",89:\"Flash fired, auto mode, red-eye reduction mode\",93:\"Flash fired, auto mode, return light not detected, red-eye reduction mode\",95:\"Flash fired, auto mode, return light detected, red-eye reduction mode\"}],[41495,{1:\"Not defined\",2:\"One-chip color area sensor\",3:\"Two-chip color area sensor\",4:\"Three-chip color area sensor\",5:\"Color sequential area sensor\",7:\"Trilinear sensor\",8:\"Color sequential linear sensor\"}],[41728,{1:\"Film Scanner\",2:\"Reflection Print Scanner\",3:\"Digital Camera\"}],[41729,{1:\"Directly photographed\"}],[41985,{0:\"Normal\",1:\"Custom\",2:\"HDR (no original saved)\",3:\"HDR (original saved)\",4:\"Original (for HDR)\",6:\"Panorama\",7:\"Portrait HDR\",8:\"Portrait\"}],[41986,{0:\"Auto\",1:\"Manual\",2:\"Auto bracket\"}],[41987,{0:\"Auto\",1:\"Manual\"}],[41990,{0:\"Standard\",1:\"Landscape\",2:\"Portrait\",3:\"Night\",4:\"Other\"}],[41991,{0:\"None\",1:\"Low gain up\",2:\"High gain up\",3:\"Low gain down\",4:\"High gain down\"}],[41996,{0:\"Unknown\",1:\"Macro\",2:\"Close\",3:\"Distant\"}],[42080,{0:\"Unknown\",1:\"Not a Composite Image\",2:\"General Composite Image\",3:\"Composite Image Captured While Shooting\"}]]);const Be={1:\"No absolute unit of measurement\",2:\"Inch\",3:\"Centimeter\"};Ee.set(37392,Be),Ee.set(41488,Be);const Ne={0:\"Normal\",1:\"Low\",2:\"High\"};function Ge(e){return\"object\"==typeof e&&void 0!==e.length?e[0]:e}function Ve(e){let t=Array.from(e).slice(1);return t[1]>15&&(t=t.map((e=>String.fromCharCode(e)))),\"0\"!==t[2]&&0!==t[2]||t.pop(),t.join(\".\")}function ze(e){if(\"string\"==typeof e){var[t,i,n,s,r,a]=e.trim().split(/[-: ]/g).map(Number),o=new Date(t,i-1,n);return Number.isNaN(s)||Number.isNaN(r)||Number.isNaN(a)||(o.setHours(s),o.setMinutes(r),o.setSeconds(a)),Number.isNaN(+o)?e:o}}function He(e){if(\"string\"==typeof e)return e;let t=[];if(0===e[1]&&0===e[e.length-1])for(let i=0;iArray.from(e).join(\".\")],[7,e=>Array.from(e).join(\":\")]]);class We extends re{static canHandle(e,t){return 225===e.getUint8(t+1)&&1752462448===e.getUint32(t+4)&&\"http://ns.adobe.com/\"===e.getString(t+4,\"http://ns.adobe.com/\".length)}static headerLength(e,t){return\"http://ns.adobe.com/xmp/extension/\"===e.getString(t+4,\"http://ns.adobe.com/xmp/extension/\".length)?79:4+\"http://ns.adobe.com/xap/1.0/\".length+1}static findPosition(e,t){let i=super.findPosition(e,t);return i.multiSegment=i.extended=79===i.headerLength,i.multiSegment?(i.chunkCount=e.getUint8(t+72),i.chunkNumber=e.getUint8(t+76),0!==e.getUint8(t+77)&&i.chunkNumber++):(i.chunkCount=1/0,i.chunkNumber=-1),i}static handleMultiSegments(e){return e.map((e=>e.chunk.getString())).join(\"\")}normalizeInput(e){return\"string\"==typeof e?e:I.from(e).getString()}parse(e=this.chunk){if(!this.localOptions.parse)return e;e=function(e){let t={},i={};for(let e of Ze)t[e]=[],i[e]=0;return e.replace(et,((e,n,s)=>{if(\"<\"===n){let n=++i[s];return t[s].push(n),`${e}#${n}`}return`${e}#${t[s].pop()}`}))}(e);let t=Xe.findAll(e,\"rdf\",\"Description\");0===t.length&&t.push(new Xe(\"rdf\",\"Description\",void 0,e));let i,n={};for(let e of t)for(let t of e.properties)i=Je(t.ns,n),_e(t,i);return function(e){let t;for(let i in e)t=e[i]=f(e[i]),void 0===t&&delete e[i];return f(e)}(n)}assignToOutput(e,t){if(this.localOptions.parse)for(let[i,n]of Object.entries(t))switch(i){case\"tiff\":this.assignObjectToOutput(e,\"ifd0\",n);break;case\"exif\":this.assignObjectToOutput(e,\"exif\",n);break;case\"xmlns\":break;default:this.assignObjectToOutput(e,i,n)}else e.xmp=t}}c(We,\"type\",\"xmp\"),c(We,\"multiSegment\",!0),T.set(\"xmp\",We);class Ke{static findAll(e){return qe(e,/([a-zA-Z0-9-]+):([a-zA-Z0-9-]+)=(\"[^\"]*\"|'[^']*')/gm).map(Ke.unpackMatch)}static unpackMatch(e){let t=e[1],i=e[2],n=e[3].slice(1,-1);return n=Qe(n),new Ke(t,i,n)}constructor(e,t,i){this.ns=e,this.name=t,this.value=i}serialize(){return this.value}}class Xe{static findAll(e,t,i){if(void 0!==t||void 0!==i){t=t||\"[\\\\w\\\\d-]+\",i=i||\"[\\\\w\\\\d-]+\";var n=new RegExp(`<(${t}):(${i})(#\\\\d+)?((\\\\s+?[\\\\w\\\\d-:]+=(\"[^\"]*\"|'[^']*'))*\\\\s*)(\\\\/>|>([\\\\s\\\\S]*?)<\\\\/\\\\1:\\\\2\\\\3>)`,\"gm\")}else n=/<([\\w\\d-]+):([\\w\\d-]+)(#\\d+)?((\\s+?[\\w\\d-:]+=(\"[^\"]*\"|'[^']*'))*\\s*)(\\/>|>([\\s\\S]*?)<\\/\\1:\\2\\3>)/gm;return qe(e,n).map(Xe.unpackMatch)}static unpackMatch(e){let t=e[1],i=e[2],n=e[4],s=e[8];return new Xe(t,i,n,s)}constructor(e,t,i,n){this.ns=e,this.name=t,this.attrString=i,this.innerXml=n,this.attrs=Ke.findAll(i),this.children=Xe.findAll(n),this.value=0===this.children.length?Qe(n):void 0,this.properties=[...this.attrs,...this.children]}get isPrimitive(){return void 0!==this.value&&0===this.attrs.length&&0===this.children.length}get isListContainer(){return 1===this.children.length&&this.children[0].isList}get isList(){let{ns:e,name:t}=this;return\"rdf\"===e&&(\"Seq\"===t||\"Bag\"===t||\"Alt\"===t)}get isListItem(){return\"rdf\"===this.ns&&\"li\"===this.name}serialize(){if(0===this.properties.length&&void 0===this.value)return;if(this.isPrimitive)return this.value;if(this.isListContainer)return this.children[0].serialize();if(this.isList)return $e(this.children.map(Ye));if(this.isListItem&&1===this.children.length&&0===this.attrs.length)return this.children[0].serialize();let e={};for(let t of this.properties)_e(t,e);return void 0!==this.value&&(e.value=this.value),f(e)}}function _e(e,t){let i=e.serialize();void 0!==i&&(t[e.name]=i)}var Ye=e=>e.serialize(),$e=e=>1===e.length?e[0]:e,Je=(e,t)=>t[e]?t[e]:t[e]={};function qe(e,t){let i,n=[];if(!e)return n;for(;null!==(i=t.exec(e));)n.push(i);return n}function Qe(e){if(function(e){return null==e||\"null\"===e||\"undefined\"===e||\"\"===e||\"\"===e.trim()}(e))return;let t=Number(e);if(!Number.isNaN(t))return t;let i=e.toLowerCase();return\"true\"===i||\"false\"!==i&&e.trim()}const Ze=[\"rdf:li\",\"rdf:Seq\",\"rdf:Bag\",\"rdf:Alt\",\"rdf:Description\"],et=new RegExp(`(<|\\\\/)(${Ze.join(\"|\")})`,\"g\");var tt=Object.freeze({__proto__:null,default:Me,Exifr:te,fileParsers:w,segmentParsers:T,fileReaders:A,tagKeys:E,tagValues:B,tagRevivers:N,createDictionary:U,extendDictionary:F,fetchUrlAsArrayBuffer:M,readBlobAsArrayBuffer:R,chunkedProps:G,otherSegments:V,segments:z,tiffBlocks:H,segmentsAndBlocks:j,tiffExtractables:W,inheritables:K,allFormatters:X,Options:q,parse:ie,gpsOnlyOptions:me,gps:Se,thumbnailOnlyOptions:Ce,thumbnail:ye,thumbnailUrl:be,orientationOnlyOptions:Ie,orientation:Pe,rotations:ke,get rotateCanvas(){return we},get rotateCss(){return Te},rotation:Ae});const it=[\"xmp\",\"icc\",\"iptc\",\"tiff\"],nt=()=>{};async function st(e,t,i){let n=new q(t);n.chunked=!1,void 0===i&&\"string\"==typeof e&&(i=function(e){let t=e.toLowerCase().split(\".\").pop();if(function(e){return\"exif\"===e||\"tiff\"===e||\"tif\"===e}(t))return\"tiff\";if(it.includes(t))return t}(e));let s=await D(e,n);if(i){if(it.includes(i))return rt(i,s,n);g(\"Invalid segment type\")}else{if(function(e){let t=e.getString(0,50).trim();return t.includes(\"e.promises));A.set(\"fs\",class extends ve{async readWhole(){this.chunked=!1,this.fs=await at;let e=await this.fs.readFile(this.input);this._swapBuffer(e)}async readChunked(){this.chunked=!0,this.fs=await at,await this.open(),await this.readChunk(0,this.options.firstChunkSize)}async open(){void 0===this.fh&&(this.fh=await this.fs.open(this.input,\"r\"),this.size=(await this.fh.stat(this.input)).size)}async _readChunk(e,t){void 0===this.fh&&await this.open(),e+t>this.size&&(t=this.size-e);var i=this.subarray(e,t,!0);return await this.fh.read(i.dataView,0,t,e),i}async close(){if(this.fh){let e=this.fh;this.fh=void 0,await e.close()}}});A.set(\"base64\",class extends ve{constructor(...e){super(...e),this.input=this.input.replace(/^data:([^;]+);base64,/gim,\"\"),this.size=this.input.length/4*3,this.input.endsWith(\"==\")?this.size-=2:this.input.endsWith(\"=\")&&(this.size-=1)}async _readChunk(e,t){let i,n,r=this.input;void 0===e?(e=0,i=0,n=0):(i=4*Math.floor(e/3),n=e-i/4*3),void 0===t&&(t=this.size);let o=e+t,l=i+4*Math.ceil(o/3);r=r.slice(i,l);let h=Math.min(t,this.size-e);if(a){let t=s.from(r,\"base64\").slice(n,n+h);return this.set(t,e,!0)}{let t=this.subarray(e,h,!0),i=atob(r),s=t.toUint8();for(let e=0;ethis.errors.push(e))),c(this,\"metaChunks\",[]),c(this,\"unknownChunks\",[])}static canHandle(e,t){return 35152===t&&2303741511===e.getUint32(0)&&218765834===e.getUint32(4)}async parse(){let{file:e}=this;await this.findPngChunksInRange(\"\u0089PNG\\r\\n\u001A\\n\".length,e.byteLength),await this.readSegments(this.metaChunks),this.findIhdr(),this.parseTextChunks(),await this.findExif().catch(this.catchError),await this.findXmp().catch(this.catchError),await this.findIcc().catch(this.catchError)}async findPngChunksInRange(e,t){let{file:i}=this;for(;e\"text\"===e.type));for(let t of e){let[e,i]=this.file.getString(t.start,t.size).split(\"\\0\");this.injectKeyValToIhdr(e,i)}}injectKeyValToIhdr(e,t){let i=this.parsers.ihdr;i&&i.raw.set(e,t)}findIhdr(){let e=this.metaChunks.find((e=>\"ihdr\"===e.type));e&&!1!==this.options.ihdr.enabled&&this.createParser(\"ihdr\",e.chunk)}async findExif(){let e=this.metaChunks.find((e=>\"exif\"===e.type));e&&this.injectSegment(\"tiff\",e.chunk)}async findXmp(){let e=this.metaChunks.filter((e=>\"itxt\"===e.type));for(let t of e){\"XML:com.adobe.xmp\"===t.chunk.getString(0,\"XML:com.adobe.xmp\".length)&&this.injectSegment(\"xmp\",t.chunk)}}async findIcc(){let e=this.metaChunks.find((e=>\"iccp\"===e.type));if(!e)return;let{chunk:t}=e,i=t.getUint8Array(0,81),s=0;for(;s<80&&0!==i[s];)s++;let r=s+2,a=t.getString(0,s);if(this.injectKeyValToIhdr(\"ProfileName\",a),n){let e=await lt,i=t.getUint8Array(r);i=e.inflateSync(i),this.injectSegment(\"icc\",i)}}}c(ut,\"type\",\"png\"),w.set(\"png\",ut),U(E,\"interop\",[[1,\"InteropIndex\"],[2,\"InteropVersion\"],[4096,\"RelatedImageFileFormat\"],[4097,\"RelatedImageWidth\"],[4098,\"RelatedImageHeight\"]]),F(E,\"ifd0\",[[11,\"ProcessingSoftware\"],[254,\"SubfileType\"],[255,\"OldSubfileType\"],[263,\"Thresholding\"],[264,\"CellWidth\"],[265,\"CellLength\"],[266,\"FillOrder\"],[269,\"DocumentName\"],[280,\"MinSampleValue\"],[281,\"MaxSampleValue\"],[285,\"PageName\"],[286,\"XPosition\"],[287,\"YPosition\"],[290,\"GrayResponseUnit\"],[297,\"PageNumber\"],[321,\"HalftoneHints\"],[322,\"TileWidth\"],[323,\"TileLength\"],[332,\"InkSet\"],[337,\"TargetPrinter\"],[18246,\"Rating\"],[18249,\"RatingPercent\"],[33550,\"PixelScale\"],[34264,\"ModelTransform\"],[34377,\"PhotoshopSettings\"],[50706,\"DNGVersion\"],[50707,\"DNGBackwardVersion\"],[50708,\"UniqueCameraModel\"],[50709,\"LocalizedCameraModel\"],[50736,\"DNGLensInfo\"],[50739,\"ShadowScale\"],[50740,\"DNGPrivateData\"],[33920,\"IntergraphMatrix\"],[33922,\"ModelTiePoint\"],[34118,\"SEMInfo\"],[34735,\"GeoTiffDirectory\"],[34736,\"GeoTiffDoubleParams\"],[34737,\"GeoTiffAsciiParams\"],[50341,\"PrintIM\"],[50721,\"ColorMatrix1\"],[50722,\"ColorMatrix2\"],[50723,\"CameraCalibration1\"],[50724,\"CameraCalibration2\"],[50725,\"ReductionMatrix1\"],[50726,\"ReductionMatrix2\"],[50727,\"AnalogBalance\"],[50728,\"AsShotNeutral\"],[50729,\"AsShotWhiteXY\"],[50730,\"BaselineExposure\"],[50731,\"BaselineNoise\"],[50732,\"BaselineSharpness\"],[50734,\"LinearResponseLimit\"],[50735,\"CameraSerialNumber\"],[50741,\"MakerNoteSafety\"],[50778,\"CalibrationIlluminant1\"],[50779,\"CalibrationIlluminant2\"],[50781,\"RawDataUniqueID\"],[50827,\"OriginalRawFileName\"],[50828,\"OriginalRawFileData\"],[50831,\"AsShotICCProfile\"],[50832,\"AsShotPreProfileMatrix\"],[50833,\"CurrentICCProfile\"],[50834,\"CurrentPreProfileMatrix\"],[50879,\"ColorimetricReference\"],[50885,\"SRawType\"],[50898,\"PanasonicTitle\"],[50899,\"PanasonicTitle2\"],[50931,\"CameraCalibrationSig\"],[50932,\"ProfileCalibrationSig\"],[50933,\"ProfileIFD\"],[50934,\"AsShotProfileName\"],[50936,\"ProfileName\"],[50937,\"ProfileHueSatMapDims\"],[50938,\"ProfileHueSatMapData1\"],[50939,\"ProfileHueSatMapData2\"],[50940,\"ProfileToneCurve\"],[50941,\"ProfileEmbedPolicy\"],[50942,\"ProfileCopyright\"],[50964,\"ForwardMatrix1\"],[50965,\"ForwardMatrix2\"],[50966,\"PreviewApplicationName\"],[50967,\"PreviewApplicationVersion\"],[50968,\"PreviewSettingsName\"],[50969,\"PreviewSettingsDigest\"],[50970,\"PreviewColorSpace\"],[50971,\"PreviewDateTime\"],[50972,\"RawImageDigest\"],[50973,\"OriginalRawFileDigest\"],[50981,\"ProfileLookTableDims\"],[50982,\"ProfileLookTableData\"],[51043,\"TimeCodes\"],[51044,\"FrameRate\"],[51058,\"TStop\"],[51081,\"ReelName\"],[51089,\"OriginalDefaultFinalSize\"],[51090,\"OriginalBestQualitySize\"],[51091,\"OriginalDefaultCropSize\"],[51105,\"CameraLabel\"],[51107,\"ProfileHueSatMapEncoding\"],[51108,\"ProfileLookTableEncoding\"],[51109,\"BaselineExposureOffset\"],[51110,\"DefaultBlackRender\"],[51111,\"NewRawImageDigest\"],[51112,\"RawToPreviewGain\"]]);let ct=[[273,\"StripOffsets\"],[279,\"StripByteCounts\"],[288,\"FreeOffsets\"],[289,\"FreeByteCounts\"],[291,\"GrayResponseCurve\"],[292,\"T4Options\"],[293,\"T6Options\"],[300,\"ColorResponseUnit\"],[320,\"ColorMap\"],[324,\"TileOffsets\"],[325,\"TileByteCounts\"],[326,\"BadFaxLines\"],[327,\"CleanFaxData\"],[328,\"ConsecutiveBadFaxLines\"],[330,\"SubIFD\"],[333,\"InkNames\"],[334,\"NumberofInks\"],[336,\"DotRange\"],[338,\"ExtraSamples\"],[339,\"SampleFormat\"],[340,\"SMinSampleValue\"],[341,\"SMaxSampleValue\"],[342,\"TransferRange\"],[343,\"ClipPath\"],[344,\"XClipPathUnits\"],[345,\"YClipPathUnits\"],[346,\"Indexed\"],[347,\"JPEGTables\"],[351,\"OPIProxy\"],[400,\"GlobalParametersIFD\"],[401,\"ProfileType\"],[402,\"FaxProfile\"],[403,\"CodingMethods\"],[404,\"VersionYear\"],[405,\"ModeNumber\"],[433,\"Decode\"],[434,\"DefaultImageColor\"],[435,\"T82Options\"],[437,\"JPEGTables\"],[512,\"JPEGProc\"],[515,\"JPEGRestartInterval\"],[517,\"JPEGLosslessPredictors\"],[518,\"JPEGPointTransforms\"],[519,\"JPEGQTables\"],[520,\"JPEGDCTables\"],[521,\"JPEGACTables\"],[559,\"StripRowCounts\"],[999,\"USPTOMiscellaneous\"],[18247,\"XP_DIP_XML\"],[18248,\"StitchInfo\"],[28672,\"SonyRawFileType\"],[28688,\"SonyToneCurve\"],[28721,\"VignettingCorrection\"],[28722,\"VignettingCorrParams\"],[28724,\"ChromaticAberrationCorrection\"],[28725,\"ChromaticAberrationCorrParams\"],[28726,\"DistortionCorrection\"],[28727,\"DistortionCorrParams\"],[29895,\"SonyCropTopLeft\"],[29896,\"SonyCropSize\"],[32781,\"ImageID\"],[32931,\"WangTag1\"],[32932,\"WangAnnotation\"],[32933,\"WangTag3\"],[32934,\"WangTag4\"],[32953,\"ImageReferencePoints\"],[32954,\"RegionXformTackPoint\"],[32955,\"WarpQuadrilateral\"],[32956,\"AffineTransformMat\"],[32995,\"Matteing\"],[32996,\"DataType\"],[32997,\"ImageDepth\"],[32998,\"TileDepth\"],[33300,\"ImageFullWidth\"],[33301,\"ImageFullHeight\"],[33302,\"TextureFormat\"],[33303,\"WrapModes\"],[33304,\"FovCot\"],[33305,\"MatrixWorldToScreen\"],[33306,\"MatrixWorldToCamera\"],[33405,\"Model2\"],[33421,\"CFARepeatPatternDim\"],[33422,\"CFAPattern2\"],[33423,\"BatteryLevel\"],[33424,\"KodakIFD\"],[33445,\"MDFileTag\"],[33446,\"MDScalePixel\"],[33447,\"MDColorTable\"],[33448,\"MDLabName\"],[33449,\"MDSampleInfo\"],[33450,\"MDPrepDate\"],[33451,\"MDPrepTime\"],[33452,\"MDFileUnits\"],[33589,\"AdventScale\"],[33590,\"AdventRevision\"],[33628,\"UIC1Tag\"],[33629,\"UIC2Tag\"],[33630,\"UIC3Tag\"],[33631,\"UIC4Tag\"],[33918,\"IntergraphPacketData\"],[33919,\"IntergraphFlagRegisters\"],[33921,\"INGRReserved\"],[34016,\"Site\"],[34017,\"ColorSequence\"],[34018,\"IT8Header\"],[34019,\"RasterPadding\"],[34020,\"BitsPerRunLength\"],[34021,\"BitsPerExtendedRunLength\"],[34022,\"ColorTable\"],[34023,\"ImageColorIndicator\"],[34024,\"BackgroundColorIndicator\"],[34025,\"ImageColorValue\"],[34026,\"BackgroundColorValue\"],[34027,\"PixelIntensityRange\"],[34028,\"TransparencyIndicator\"],[34029,\"ColorCharacterization\"],[34030,\"HCUsage\"],[34031,\"TrapIndicator\"],[34032,\"CMYKEquivalent\"],[34152,\"AFCP_IPTC\"],[34232,\"PixelMagicJBIGOptions\"],[34263,\"JPLCartoIFD\"],[34306,\"WB_GRGBLevels\"],[34310,\"LeafData\"],[34687,\"TIFF_FXExtensions\"],[34688,\"MultiProfiles\"],[34689,\"SharedData\"],[34690,\"T88Options\"],[34732,\"ImageLayer\"],[34750,\"JBIGOptions\"],[34856,\"Opto-ElectricConvFactor\"],[34857,\"Interlace\"],[34908,\"FaxRecvParams\"],[34909,\"FaxSubAddress\"],[34910,\"FaxRecvTime\"],[34929,\"FedexEDR\"],[34954,\"LeafSubIFD\"],[37387,\"FlashEnergy\"],[37388,\"SpatialFrequencyResponse\"],[37389,\"Noise\"],[37390,\"FocalPlaneXResolution\"],[37391,\"FocalPlaneYResolution\"],[37392,\"FocalPlaneResolutionUnit\"],[37397,\"ExposureIndex\"],[37398,\"TIFF-EPStandardID\"],[37399,\"SensingMethod\"],[37434,\"CIP3DataFile\"],[37435,\"CIP3Sheet\"],[37436,\"CIP3Side\"],[37439,\"StoNits\"],[37679,\"MSDocumentText\"],[37680,\"MSPropertySetStorage\"],[37681,\"MSDocumentTextPosition\"],[37724,\"ImageSourceData\"],[40965,\"InteropIFD\"],[40976,\"SamsungRawPointersOffset\"],[40977,\"SamsungRawPointersLength\"],[41217,\"SamsungRawByteOrder\"],[41218,\"SamsungRawUnknown\"],[41484,\"SpatialFrequencyResponse\"],[41485,\"Noise\"],[41489,\"ImageNumber\"],[41490,\"SecurityClassification\"],[41491,\"ImageHistory\"],[41494,\"TIFF-EPStandardID\"],[41995,\"DeviceSettingDescription\"],[42112,\"GDALMetadata\"],[42113,\"GDALNoData\"],[44992,\"ExpandSoftware\"],[44993,\"ExpandLens\"],[44994,\"ExpandFilm\"],[44995,\"ExpandFilterLens\"],[44996,\"ExpandScanner\"],[44997,\"ExpandFlashLamp\"],[46275,\"HasselbladRawImage\"],[48129,\"PixelFormat\"],[48130,\"Transformation\"],[48131,\"Uncompressed\"],[48132,\"ImageType\"],[48256,\"ImageWidth\"],[48257,\"ImageHeight\"],[48258,\"WidthResolution\"],[48259,\"HeightResolution\"],[48320,\"ImageOffset\"],[48321,\"ImageByteCount\"],[48322,\"AlphaOffset\"],[48323,\"AlphaByteCount\"],[48324,\"ImageDataDiscard\"],[48325,\"AlphaDataDiscard\"],[50215,\"OceScanjobDesc\"],[50216,\"OceApplicationSelector\"],[50217,\"OceIDNumber\"],[50218,\"OceImageLogic\"],[50255,\"Annotations\"],[50459,\"HasselbladExif\"],[50547,\"OriginalFileName\"],[50560,\"USPTOOriginalContentType\"],[50656,\"CR2CFAPattern\"],[50710,\"CFAPlaneColor\"],[50711,\"CFALayout\"],[50712,\"LinearizationTable\"],[50713,\"BlackLevelRepeatDim\"],[50714,\"BlackLevel\"],[50715,\"BlackLevelDeltaH\"],[50716,\"BlackLevelDeltaV\"],[50717,\"WhiteLevel\"],[50718,\"DefaultScale\"],[50719,\"DefaultCropOrigin\"],[50720,\"DefaultCropSize\"],[50733,\"BayerGreenSplit\"],[50737,\"ChromaBlurRadius\"],[50738,\"AntiAliasStrength\"],[50752,\"RawImageSegmentation\"],[50780,\"BestQualityScale\"],[50784,\"AliasLayerMetadata\"],[50829,\"ActiveArea\"],[50830,\"MaskedAreas\"],[50935,\"NoiseReductionApplied\"],[50974,\"SubTileBlockSize\"],[50975,\"RowInterleaveFactor\"],[51008,\"OpcodeList1\"],[51009,\"OpcodeList2\"],[51022,\"OpcodeList3\"],[51041,\"NoiseProfile\"],[51114,\"CacheVersion\"],[51125,\"DefaultUserCrop\"],[51157,\"NikonNEFInfo\"],[65024,\"KdcIFD\"]];F(E,\"ifd0\",ct),F(E,\"exif\",ct),U(B,\"gps\",[[23,{M:\"Magnetic North\",T:\"True North\"}],[25,{K:\"Kilometers\",M:\"Miles\",N:\"Nautical Miles\"}]]);class ft extends re{static canHandle(e,t){return 224===e.getUint8(t+1)&&1246120262===e.getUint32(t+4)&&0===e.getUint8(t+8)}parse(){return this.parseTags(),this.translate(),this.output}parseTags(){this.raw=new Map([[0,this.chunk.getUint16(0)],[2,this.chunk.getUint8(2)],[3,this.chunk.getUint16(3)],[5,this.chunk.getUint16(5)],[7,this.chunk.getUint8(7)],[8,this.chunk.getUint8(8)]])}}c(ft,\"type\",\"jfif\"),c(ft,\"headerLength\",9),T.set(\"jfif\",ft),U(E,\"jfif\",[[0,\"JFIFVersion\"],[2,\"ResolutionUnit\"],[3,\"XResolution\"],[5,\"YResolution\"],[7,\"ThumbnailWidth\"],[8,\"ThumbnailHeight\"]]);class dt extends re{parse(){return this.parseTags(),this.translate(),this.output}parseTags(){this.raw=new Map([[0,this.chunk.getUint32(0)],[4,this.chunk.getUint32(4)],[8,this.chunk.getUint8(8)],[9,this.chunk.getUint8(9)],[10,this.chunk.getUint8(10)],[11,this.chunk.getUint8(11)],[12,this.chunk.getUint8(12)],...Array.from(this.raw)])}}c(dt,\"type\",\"ihdr\"),T.set(\"ihdr\",dt),U(E,\"ihdr\",[[0,\"ImageWidth\"],[4,\"ImageHeight\"],[8,\"BitDepth\"],[9,\"ColorType\"],[10,\"Compression\"],[11,\"Filter\"],[12,\"Interlace\"]]),U(B,\"ihdr\",[[9,{0:\"Grayscale\",2:\"RGB\",3:\"Palette\",4:\"Grayscale with Alpha\",6:\"RGB with Alpha\",DEFAULT:\"Unknown\"}],[10,{0:\"Deflate/Inflate\",DEFAULT:\"Unknown\"}],[11,{0:\"Adaptive\",DEFAULT:\"Unknown\"}],[12,{0:\"Noninterlaced\",1:\"Adam7 Interlace\",DEFAULT:\"Unknown\"}]]);class pt extends re{static canHandle(e,t){return 226===e.getUint8(t+1)&&1229144927===e.getUint32(t+4)}static findPosition(e,t){let i=super.findPosition(e,t);return i.chunkNumber=e.getUint8(t+16),i.chunkCount=e.getUint8(t+17),i.multiSegment=i.chunkCount>1,i}static handleMultiSegments(e){return function(e){let t=function(e){let t=e[0].constructor,i=0;for(let t of e)i+=t.length;let n=new t(i),s=0;for(let t of e)n.set(t,s),s+=t.length;return n}(e.map((e=>e.chunk.toUint8())));return new I(t)}(e)}parse(){return this.raw=new Map,this.parseHeader(),this.parseTags(),this.translate(),this.output}parseHeader(){let{raw:e}=this;this.chunk.byteLength<84&&g(\"ICC header is too short\");for(let[t,i]of Object.entries(gt)){t=parseInt(t,10);let n=i(this.chunk,t);\"\\0\\0\\0\\0\"!==n&&e.set(t,n)}}parseTags(){let e,t,i,n,s,{raw:r}=this,a=this.chunk.getUint32(128),o=132,l=this.chunk.byteLength;for(;a--;){if(e=this.chunk.getString(o,4),t=this.chunk.getUint32(o+4),i=this.chunk.getUint32(o+8),n=this.chunk.getString(t,4),t+i>l)return void console.warn(\"reached the end of the first ICC chunk. Enable options.tiff.multiSegment to read all ICC segments.\");s=this.parseTag(n,t,i),void 0!==s&&\"\\0\\0\\0\\0\"!==s&&r.set(e,s),o+=12}}parseTag(e,t,i){switch(e){case\"desc\":return this.parseDesc(t);case\"mluc\":return this.parseMluc(t);case\"text\":return this.parseText(t,i);case\"sig \":return this.parseSig(t)}if(!(t+i>this.chunk.byteLength))return this.chunk.getUint8Array(t,i)}parseDesc(e){let t=this.chunk.getUint32(e+8)-1;return m(this.chunk.getString(e+12,t))}parseText(e,t){return m(this.chunk.getString(e+8,t-8))}parseSig(e){return m(this.chunk.getString(e+8,4))}parseMluc(e){let{chunk:t}=this,i=t.getUint32(e+8),n=t.getUint32(e+12),s=e+16,r=[];for(let a=0;a>4,e.getUint8(t+1)%16].map((e=>e.toString(10))).join(\".\")},12:mt,16:mt,20:mt,24:function(e,t){const i=e.getUint16(t),n=e.getUint16(t+2)-1,s=e.getUint16(t+4),r=e.getUint16(t+6),a=e.getUint16(t+8),o=e.getUint16(t+10);return new Date(Date.UTC(i,n,s,r,a,o))},36:mt,40:mt,48:mt,52:mt,64:(e,t)=>e.getUint32(t),80:mt};function mt(e,t){return m(e.getString(t,4))}T.set(\"icc\",pt),U(E,\"icc\",[[4,\"ProfileCMMType\"],[8,\"ProfileVersion\"],[12,\"ProfileClass\"],[16,\"ColorSpaceData\"],[20,\"ProfileConnectionSpace\"],[24,\"ProfileDateTime\"],[36,\"ProfileFileSignature\"],[40,\"PrimaryPlatform\"],[44,\"CMMFlags\"],[48,\"DeviceManufacturer\"],[52,\"DeviceModel\"],[56,\"DeviceAttributes\"],[64,\"RenderingIntent\"],[68,\"ConnectionSpaceIlluminant\"],[80,\"ProfileCreator\"],[84,\"ProfileID\"],[\"Header\",\"ProfileHeader\"],[\"MS00\",\"WCSProfiles\"],[\"bTRC\",\"BlueTRC\"],[\"bXYZ\",\"BlueMatrixColumn\"],[\"bfd\",\"UCRBG\"],[\"bkpt\",\"MediaBlackPoint\"],[\"calt\",\"CalibrationDateTime\"],[\"chad\",\"ChromaticAdaptation\"],[\"chrm\",\"Chromaticity\"],[\"ciis\",\"ColorimetricIntentImageState\"],[\"clot\",\"ColorantTableOut\"],[\"clro\",\"ColorantOrder\"],[\"clrt\",\"ColorantTable\"],[\"cprt\",\"ProfileCopyright\"],[\"crdi\",\"CRDInfo\"],[\"desc\",\"ProfileDescription\"],[\"devs\",\"DeviceSettings\"],[\"dmdd\",\"DeviceModelDesc\"],[\"dmnd\",\"DeviceMfgDesc\"],[\"dscm\",\"ProfileDescriptionML\"],[\"fpce\",\"FocalPlaneColorimetryEstimates\"],[\"gTRC\",\"GreenTRC\"],[\"gXYZ\",\"GreenMatrixColumn\"],[\"gamt\",\"Gamut\"],[\"kTRC\",\"GrayTRC\"],[\"lumi\",\"Luminance\"],[\"meas\",\"Measurement\"],[\"meta\",\"Metadata\"],[\"mmod\",\"MakeAndModel\"],[\"ncl2\",\"NamedColor2\"],[\"ncol\",\"NamedColor\"],[\"ndin\",\"NativeDisplayInfo\"],[\"pre0\",\"Preview0\"],[\"pre1\",\"Preview1\"],[\"pre2\",\"Preview2\"],[\"ps2i\",\"PS2RenderingIntent\"],[\"ps2s\",\"PostScript2CSA\"],[\"psd0\",\"PostScript2CRD0\"],[\"psd1\",\"PostScript2CRD1\"],[\"psd2\",\"PostScript2CRD2\"],[\"psd3\",\"PostScript2CRD3\"],[\"pseq\",\"ProfileSequenceDesc\"],[\"psid\",\"ProfileSequenceIdentifier\"],[\"psvm\",\"PS2CRDVMSize\"],[\"rTRC\",\"RedTRC\"],[\"rXYZ\",\"RedMatrixColumn\"],[\"resp\",\"OutputResponse\"],[\"rhoc\",\"ReflectionHardcopyOrigColorimetry\"],[\"rig0\",\"PerceptualRenderingIntentGamut\"],[\"rig2\",\"SaturationRenderingIntentGamut\"],[\"rpoc\",\"ReflectionPrintOutputColorimetry\"],[\"sape\",\"SceneAppearanceEstimates\"],[\"scoe\",\"SceneColorimetryEstimates\"],[\"scrd\",\"ScreeningDesc\"],[\"scrn\",\"Screening\"],[\"targ\",\"CharTarget\"],[\"tech\",\"Technology\"],[\"vcgt\",\"VideoCardGamma\"],[\"view\",\"ViewingConditions\"],[\"vued\",\"ViewingCondDesc\"],[\"wtpt\",\"MediaWhitePoint\"]]);const St={\"4d2p\":\"Erdt Systems\",AAMA:\"Aamazing Technologies\",ACER:\"Acer\",ACLT:\"Acolyte Color Research\",ACTI:\"Actix Sytems\",ADAR:\"Adara Technology\",ADBE:\"Adobe\",ADI:\"ADI Systems\",AGFA:\"Agfa Graphics\",ALMD:\"Alps Electric\",ALPS:\"Alps Electric\",ALWN:\"Alwan Color Expertise\",AMTI:\"Amiable Technologies\",AOC:\"AOC International\",APAG:\"Apago\",APPL:\"Apple Computer\",AST:\"AST\",\"AT&T\":\"AT&T\",BAEL:\"BARBIERI electronic\",BRCO:\"Barco NV\",BRKP:\"Breakpoint\",BROT:\"Brother\",BULL:\"Bull\",BUS:\"Bus Computer Systems\",\"C-IT\":\"C-Itoh\",CAMR:\"Intel\",CANO:\"Canon\",CARR:\"Carroll Touch\",CASI:\"Casio\",CBUS:\"Colorbus PL\",CEL:\"Crossfield\",CELx:\"Crossfield\",CGS:\"CGS Publishing Technologies International\",CHM:\"Rochester Robotics\",CIGL:\"Colour Imaging Group, London\",CITI:\"Citizen\",CL00:\"Candela\",CLIQ:\"Color IQ\",CMCO:\"Chromaco\",CMiX:\"CHROMiX\",COLO:\"Colorgraphic Communications\",COMP:\"Compaq\",COMp:\"Compeq/Focus Technology\",CONR:\"Conrac Display Products\",CORD:\"Cordata Technologies\",CPQ:\"Compaq\",CPRO:\"ColorPro\",CRN:\"Cornerstone\",CTX:\"CTX International\",CVIS:\"ColorVision\",CWC:\"Fujitsu Laboratories\",DARI:\"Darius Technology\",DATA:\"Dataproducts\",DCP:\"Dry Creek Photo\",DCRC:\"Digital Contents Resource Center, Chung-Ang University\",DELL:\"Dell Computer\",DIC:\"Dainippon Ink and Chemicals\",DICO:\"Diconix\",DIGI:\"Digital\",\"DL&C\":\"Digital Light & Color\",DPLG:\"Doppelganger\",DS:\"Dainippon Screen\",DSOL:\"DOOSOL\",DUPN:\"DuPont\",EPSO:\"Epson\",ESKO:\"Esko-Graphics\",ETRI:\"Electronics and Telecommunications Research Institute\",EVER:\"Everex Systems\",EXAC:\"ExactCODE\",Eizo:\"Eizo\",FALC:\"Falco Data Products\",FF:\"Fuji Photo Film\",FFEI:\"FujiFilm Electronic Imaging\",FNRD:\"Fnord Software\",FORA:\"Fora\",FORE:\"Forefront Technology\",FP:\"Fujitsu\",FPA:\"WayTech Development\",FUJI:\"Fujitsu\",FX:\"Fuji Xerox\",GCC:\"GCC Technologies\",GGSL:\"Global Graphics Software\",GMB:\"Gretagmacbeth\",GMG:\"GMG\",GOLD:\"GoldStar Technology\",GOOG:\"Google\",GPRT:\"Giantprint\",GTMB:\"Gretagmacbeth\",GVC:\"WayTech Development\",GW2K:\"Sony\",HCI:\"HCI\",HDM:\"Heidelberger Druckmaschinen\",HERM:\"Hermes\",HITA:\"Hitachi America\",HP:\"Hewlett-Packard\",HTC:\"Hitachi\",HiTi:\"HiTi Digital\",IBM:\"IBM\",IDNT:\"Scitex\",IEC:\"Hewlett-Packard\",IIYA:\"Iiyama North America\",IKEG:\"Ikegami Electronics\",IMAG:\"Image Systems\",IMI:\"Ingram Micro\",INTC:\"Intel\",INTL:\"N/A (INTL)\",INTR:\"Intra Electronics\",IOCO:\"Iocomm International Technology\",IPS:\"InfoPrint Solutions Company\",IRIS:\"Scitex\",ISL:\"Ichikawa Soft Laboratory\",ITNL:\"N/A (ITNL)\",IVM:\"IVM\",IWAT:\"Iwatsu Electric\",Idnt:\"Scitex\",Inca:\"Inca Digital Printers\",Iris:\"Scitex\",JPEG:\"Joint Photographic Experts Group\",JSFT:\"Jetsoft Development\",JVC:\"JVC Information Products\",KART:\"Scitex\",KFC:\"KFC Computek Components\",KLH:\"KLH Computers\",KMHD:\"Konica Minolta\",KNCA:\"Konica\",KODA:\"Kodak\",KYOC:\"Kyocera\",Kart:\"Scitex\",LCAG:\"Leica\",LCCD:\"Leeds Colour\",LDAK:\"Left Dakota\",LEAD:\"Leading Technology\",LEXM:\"Lexmark International\",LINK:\"Link Computer\",LINO:\"Linotronic\",LITE:\"Lite-On\",Leaf:\"Leaf\",Lino:\"Linotronic\",MAGC:\"Mag Computronic\",MAGI:\"MAG Innovision\",MANN:\"Mannesmann\",MICN:\"Micron Technology\",MICR:\"Microtek\",MICV:\"Microvitec\",MINO:\"Minolta\",MITS:\"Mitsubishi Electronics America\",MITs:\"Mitsuba\",MNLT:\"Minolta\",MODG:\"Modgraph\",MONI:\"Monitronix\",MONS:\"Monaco Systems\",MORS:\"Morse Technology\",MOTI:\"Motive Systems\",MSFT:\"Microsoft\",MUTO:\"MUTOH INDUSTRIES\",Mits:\"Mitsubishi Electric\",NANA:\"NANAO\",NEC:\"NEC\",NEXP:\"NexPress Solutions\",NISS:\"Nissei Sangyo America\",NKON:\"Nikon\",NONE:\"none\",OCE:\"Oce Technologies\",OCEC:\"OceColor\",OKI:\"Oki\",OKID:\"Okidata\",OKIP:\"Okidata\",OLIV:\"Olivetti\",OLYM:\"Olympus\",ONYX:\"Onyx Graphics\",OPTI:\"Optiquest\",PACK:\"Packard Bell\",PANA:\"Matsushita Electric Industrial\",PANT:\"Pantone\",PBN:\"Packard Bell\",PFU:\"PFU\",PHIL:\"Philips Consumer Electronics\",PNTX:\"HOYA\",POne:\"Phase One A/S\",PREM:\"Premier Computer Innovations\",PRIN:\"Princeton Graphic Systems\",PRIP:\"Princeton Publishing Labs\",QLUX:\"Hong Kong\",QMS:\"QMS\",QPCD:\"QPcard AB\",QUAD:\"QuadLaser\",QUME:\"Qume\",RADI:\"Radius\",RDDx:\"Integrated Color Solutions\",RDG:\"Roland DG\",REDM:\"REDMS Group\",RELI:\"Relisys\",RGMS:\"Rolf Gierling Multitools\",RICO:\"Ricoh\",RNLD:\"Edmund Ronald\",ROYA:\"Royal\",RPC:\"Ricoh Printing Systems\",RTL:\"Royal Information Electronics\",SAMP:\"Sampo\",SAMS:\"Samsung\",SANT:\"Jaime Santana Pomares\",SCIT:\"Scitex\",SCRN:\"Dainippon Screen\",SDP:\"Scitex\",SEC:\"Samsung\",SEIK:\"Seiko Instruments\",SEIk:\"Seikosha\",SGUY:\"ScanGuy.com\",SHAR:\"Sharp Laboratories\",SICC:\"International Color Consortium\",SONY:\"Sony\",SPCL:\"SpectraCal\",STAR:\"Star\",STC:\"Sampo Technology\",Scit:\"Scitex\",Sdp:\"Scitex\",Sony:\"Sony\",TALO:\"Talon Technology\",TAND:\"Tandy\",TATU:\"Tatung\",TAXA:\"TAXAN America\",TDS:\"Tokyo Denshi Sekei\",TECO:\"TECO Information Systems\",TEGR:\"Tegra\",TEKT:\"Tektronix\",TI:\"Texas Instruments\",TMKR:\"TypeMaker\",TOSB:\"Toshiba\",TOSH:\"Toshiba\",TOTK:\"TOTOKU ELECTRIC\",TRIU:\"Triumph\",TSBT:\"Toshiba\",TTX:\"TTX Computer Products\",TVM:\"TVM Professional Monitor\",TW:\"TW Casper\",ULSX:\"Ulead Systems\",UNIS:\"Unisys\",UTZF:\"Utz Fehlau & Sohn\",VARI:\"Varityper\",VIEW:\"Viewsonic\",VISL:\"Visual communication\",VIVO:\"Vivo Mobile Communication\",WANG:\"Wang\",WLBR:\"Wilbur Imaging\",WTG2:\"Ware To Go\",WYSE:\"WYSE Technology\",XERX:\"Xerox\",XRIT:\"X-Rite\",ZRAN:\"Zoran\",Zebr:\"Zebra Technologies\",appl:\"Apple Computer\",bICC:\"basICColor\",berg:\"bergdesign\",ceyd:\"Integrated Color Solutions\",clsp:\"MacDermid ColorSpan\",ds:\"Dainippon Screen\",dupn:\"DuPont\",ffei:\"FujiFilm Electronic Imaging\",flux:\"FluxData\",iris:\"Scitex\",kart:\"Scitex\",lcms:\"Little CMS\",lino:\"Linotronic\",none:\"none\",ob4d:\"Erdt Systems\",obic:\"Medigraph\",quby:\"Qubyx Sarl\",scit:\"Scitex\",scrn:\"Dainippon Screen\",sdp:\"Scitex\",siwi:\"SIWI GRAFIKA\",yxym:\"YxyMaster\"},Ct={scnr:\"Scanner\",mntr:\"Monitor\",prtr:\"Printer\",link:\"Device Link\",abst:\"Abstract\",spac:\"Color Space Conversion Profile\",nmcl:\"Named Color\",cenc:\"ColorEncodingSpace profile\",mid:\"MultiplexIdentification profile\",mlnk:\"MultiplexLink profile\",mvis:\"MultiplexVisualization profile\",nkpf:\"Nikon Input Device Profile (NON-STANDARD!)\"};U(B,\"icc\",[[4,St],[12,Ct],[40,Object.assign({},St,Ct)],[48,St],[80,St],[64,{0:\"Perceptual\",1:\"Relative Colorimetric\",2:\"Saturation\",3:\"Absolute Colorimetric\"}],[\"tech\",{amd:\"Active Matrix Display\",crt:\"Cathode Ray Tube Display\",kpcd:\"Photo CD\",pmd:\"Passive Matrix Display\",dcam:\"Digital Camera\",dcpj:\"Digital Cinema Projector\",dmpc:\"Digital Motion Picture Camera\",dsub:\"Dye Sublimation Printer\",epho:\"Electrophotographic Printer\",esta:\"Electrostatic Printer\",flex:\"Flexography\",fprn:\"Film Writer\",fscn:\"Film Scanner\",grav:\"Gravure\",ijet:\"Ink Jet Printer\",imgs:\"Photo Image Setter\",mpfr:\"Motion Picture Film Recorder\",mpfs:\"Motion Picture Film Scanner\",offs:\"Offset Lithography\",pjtv:\"Projection Television\",rpho:\"Photographic Paper Printer\",rscn:\"Reflective Scanner\",silk:\"Silkscreen\",twax:\"Thermal Wax Printer\",vidc:\"Video Camera\",vidm:\"Video Monitor\"}]]);class yt extends re{static canHandle(e,t,i){return 237===e.getUint8(t+1)&&\"Photoshop\"===e.getString(t+4,9)&&void 0!==this.containsIptc8bim(e,t,i)}static headerLength(e,t,i){let n,s=this.containsIptc8bim(e,t,i);if(void 0!==s)return n=e.getUint8(t+s+7),n%2!=0&&(n+=1),0===n&&(n=4),s+8+n}static containsIptc8bim(e,t,i){for(let n=0;n {\n if (loaded.length > 0) {\n drawPhotos.fitZoom(false);\n }\n });\n })\n .on('dragenter.svgLocalPhotos', over)\n .on('dragexit.svgLocalPhotos', over)\n .on('dragover.svgLocalPhotos', over);\n\n _initialized = true;\n }\n\n function ensureViewerLoaded(context) {\n if (_photoFrame) {\n return Promise.resolve(_photoFrame);\n }\n\n const viewer = context.container().select('.photoviewer')\n .selectAll('.local-photos-wrapper')\n .data([0]);\n\n const viewerEnter = viewer.enter()\n .append('div')\n .attr('class', 'photo-wrapper local-photos-wrapper')\n .classed('hide', true);\n\n viewerEnter\n .append('div')\n .attr('class', 'photo-attribution photo-attribution-dual fillD');\n\n const controlsEnter = viewerEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls-local');\n\n controlsEnter\n .append('button')\n .classed('back', true)\n .on('click.back', () => stepPhotos(-1))\n .text('\u25C0');\n\n controlsEnter\n .append('button')\n .classed('forward', true)\n .on('click.forward', () => stepPhotos(1))\n .text('\u25B6');\n\n return planePhotoFrame(context, viewerEnter)\n .then(planePhotoFrame => _photoFrame = planePhotoFrame);\n }\n\n function stepPhotos(stepBy){\n if (!_photos || _photos.length === 0) return;\n if (_activePhotoIdx === undefined) _activePhotoIdx = 0;\n\n const newIndex = _activePhotoIdx + stepBy;\n _activePhotoIdx = Math.max(0, Math.min(_photos.length - 1, newIndex));\n\n click(null, _photos[_activePhotoIdx], false);\n }\n\n // opens the image at bottom left\n function click(d3_event, image, zoomTo) {\n _activePhotoIdx = _photos.indexOf(image);\n ensureViewerLoaded(context).then(() => {\n const viewer = context.container().select('.photoviewer')\n .datum(image);\n\n const viewerWrap = viewer.select('.local-photos-wrapper');\n const isHidden = viewerWrap.classed('hide');\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n viewer.classed('hide', false);\n viewerWrap.classed('hide', false);\n }\n\n const controlsWrap = viewerWrap.select('.photo-controls-wrap');\n\n controlsWrap.select('.back')\n .attr('disabled', _activePhotoIdx <= 0 ? true: null);\n controlsWrap.select('.forward')\n .attr('disabled', _activePhotoIdx >= _photos.length - 1 ? true: null);\n\n const attribution = viewerWrap.selectAll('.photo-attribution').text('');\n\n if (image.date) {\n attribution\n .append('span')\n .text(image.date.toLocaleString(localizer.localeCode()));\n }\n if (image.name) {\n attribution\n .append('span')\n .classed('filename', true)\n .text(image.name);\n }\n\n _photoFrame.selectPhoto({ image_path: '' });\n image.getSrc().then(src => {\n _photoFrame\n .selectPhoto({ image_path: src })\n .showPhotoFrame(viewerWrap);\n\n setStyles();\n });\n });\n\n // centers the map with image location\n if (zoomTo) {\n context.map().centerEase(image.loc);\n }\n }\n\n\n function transform(d) {\n // projection expects [long, lat]\n var svgpoint = projection(d.loc);\n return 'translate(' + svgpoint[0] + ',' + svgpoint[1] + ')';\n }\n\n function setStyles(hovered) {\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n\n context.container().selectAll('.layer-local-photos .viewfield-group')\n .classed('hovered', d => d.id === hovered?.id)\n .classed('highlighted', d => d.id === hovered?.id || d.id === selected?.id)\n .classed('currentView', d => d.id === selected?.id);\n }\n\n // puts the image markers on the map\n function display_markers(imageList) {\n imageList = imageList.filter(image => isArray(image.loc) && isNumber(image.loc[0]) && isNumber(image.loc[1]));\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(imageList, function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', (d3_event, d) => setStyles(d))\n .on('mouseleave', () => setStyles(null))\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const showViewfields = context.map().zoom() >= minViewfieldZoom;\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n // viewfields may or may not be drawn...\n // but if they are, draw below the circles\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', function() {\n const d = this.parentNode.__data__;\n return `rotate(${Math.round(d.direction ?? 0)},0,0),scale(1.5,1.5),translate(-8,-13)`;\n })\n .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z')\n .style('visibility', function() {\n const d = this.parentNode.__data__;\n return isNumber(d.direction) ? 'visible' : 'hidden';\n });\n }\n\n function drawPhotos(selection) {\n layer = selection.selectAll('.layer-local-photos')\n .data(_photos ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-local-photos');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (_photos) {\n display_markers(_photos);\n }\n }\n\n\n function readFileAsDataURL(file) {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result);\n reader.onerror = error => reject(error);\n reader.readAsDataURL(file);\n });\n }\n /**\n * Reads and parses files\n * @param {Array} files - Holds array of file - [file_1, file_2, ...]\n */\n async function readmultifiles(files, callback) {\n const loaded = [];\n\n for (const file of files) {\n try {\n const exifData = await exifr.parse(file); // eslint-disable-line no-await-in-loop\n const photo = {\n service: 'photo',\n id: _idAutoinc++,\n name: file.name,\n getSrc: () => readFileAsDataURL(file),\n file: file,\n loc: [exifData.longitude, exifData.latitude],\n direction: exifData.GPSImgDirection,\n date: exifData.CreateDate || exifData.DateTimeOriginal || exifData.ModifyDate,\n };\n loaded.push(photo);\n const sameName = _photos.filter(i => i.name === photo.name);\n if (sameName.length === 0) {\n _photos.push(photo);\n } else {\n const thisContent = await photo.getSrc(); // eslint-disable-line no-await-in-loop\n const sameNameContent = await Promise.allSettled(sameName.map(i => i.getSrc())); // eslint-disable-line no-await-in-loop\n if (!sameNameContent.some(i => i.value === thisContent)) {\n _photos.push(photo);\n }\n }\n } catch {\n // skip files which are not a supported image file\n }\n }\n\n if (typeof callback === 'function') callback(loaded);\n dispatch.call('change');\n }\n\n drawPhotos.setFiles = function(fileList, callback) {\n // read and parse asynchronously\n readmultifiles(Array.from(fileList), callback);\n return this;\n };\n\n // Step 1: entry point\n /**\n * Sets the fileList\n * @param {Object} fileList - The uploaded files. fileList is an object, not an array object\n * @param {Object} fileList.0 - A File - {name: \"Das.png\", lastModified: 1625064498536, lastModifiedDate: Wed Jun 30 2021 20:18:18 GMT+0530 (India Standard Time), webkitRelativePath: \"\", size: 859658, \u2026}\n * @param {Function} callback - A callback to be called after the photos have been loaded and parsed\n */\n drawPhotos.fileList = function(fileList, callback) {\n if (!arguments.length) return _fileList;\n\n _fileList = fileList;\n\n if (!fileList || !fileList.length) return this;\n\n drawPhotos.setFiles(_fileList, callback);\n\n return this;\n };\n\n drawPhotos.getPhotos = function() {\n return _photos;\n };\n\n drawPhotos.removePhoto = function(id) {\n _photos = _photos.filter(i => i.id !== id);\n dispatch.call('change');\n return _photos;\n };\n\n drawPhotos.openPhoto = click;\n\n drawPhotos.fitZoom = function(force) {\n const coords = _photos\n .map(image => image.loc)\n .filter(l => isArray(l) && isNumber(l[0]) && isNumber(l[1]));\n if (coords.length === 0) return;\n const extent = coords\n .map(l => geoExtent(l, l))\n .reduce((a, b) => a.extend(b));\n\n const map = context.map();\n var viewport = map.trimmedExtent().polygon();\n\n if (force !== false || !geoPolygonIntersectsPolygon(viewport, coords, true)) {\n map.centerZoom(extent.center(), Math.min(18, map.trimmedExtentZoom(extent)));\n }\n };\n\n function showLayer() {\n layer.style('display', 'block');\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', () => {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n });\n }\n\n drawPhotos.enabled = function(val) {\n if (!arguments.length) return _enabled;\n\n _enabled = val;\n if (_enabled) {\n showLayer();\n } else {\n hideLayer();\n }\n\n dispatch.call('change');\n return this;\n };\n\n drawPhotos.hasData = function() {\n return isArray(_photos) && _photos.length > 0;\n };\n\n\n init();\n return drawPhotos;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\n\nimport { modeBrowse } from '../modes/browse';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\n\nlet _layerEnabled = false;\nlet _qaService;\n\nexport function svgOsmose(projection, context, dispatch) {\n const throttledRedraw = throttle(() => dispatch.call('change'), 1000);\n const minZoom = 12;\n\n let touchLayer = d3_select(null);\n let drawLayer = d3_select(null);\n let layerVisible = false;\n\n function markerPath(selection, klass) {\n selection\n .attr('class', klass)\n .attr('transform', 'translate(-10, -28)')\n .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');\n }\n\n // Loosely-coupled osmose service for fetching issues\n function getService() {\n if (services.osmose && !_qaService) {\n _qaService = services.osmose;\n _qaService.on('loaded', throttledRedraw);\n } else if (!services.osmose && _qaService) {\n _qaService = null;\n }\n\n return _qaService;\n }\n\n // Show the markers\n function editOn() {\n if (!layerVisible) {\n layerVisible = true;\n drawLayer\n .style('display', 'block');\n }\n }\n\n // Immediately remove the markers and their touch targets\n function editOff() {\n if (layerVisible) {\n layerVisible = false;\n drawLayer\n .style('display', 'none');\n drawLayer.selectAll('.qaItem.osmose')\n .remove();\n touchLayer.selectAll('.qaItem.osmose')\n .remove();\n }\n }\n\n // Enable the layer. This shows the markers and transitions them to visible.\n function layerOn() {\n editOn();\n\n drawLayer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end interrupt', () => dispatch.call('change'));\n }\n\n // Disable the layer. This transitions the layer invisible and then hides the markers.\n function layerOff() {\n throttledRedraw.cancel();\n drawLayer.interrupt();\n touchLayer.selectAll('.qaItem.osmose')\n .remove();\n\n drawLayer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end interrupt', () => {\n editOff();\n dispatch.call('change');\n });\n }\n\n // Update the issue markers\n function updateMarkers() {\n if (!layerVisible || !_layerEnabled) return;\n\n const service = getService();\n const selectedID = context.selectedErrorID();\n const data = (service ? service.getItems(projection) : []);\n const getTransform = svgPointTransform(projection);\n\n // Draw markers..\n const markers = drawLayer.selectAll('.qaItem.osmose')\n .data(data, d => d.id);\n\n // exit\n markers.exit()\n .remove();\n\n // enter\n const markersEnter = markers.enter()\n .append('g')\n .attr('class', d => `qaItem ${d.service} itemId-${d.id} itemType-${d.itemType}`);\n\n markersEnter\n .append('polygon')\n .call(markerPath, 'shadow');\n\n markersEnter\n .append('ellipse')\n .attr('cx', 0)\n .attr('cy', 0)\n .attr('rx', 4.5)\n .attr('ry', 2)\n .attr('class', 'stroke');\n\n markersEnter\n .append('polygon')\n .attr('fill', d => service.getColor(d.item))\n .call(markerPath, 'qaItem-fill');\n\n markersEnter\n .append('use')\n .attr('class', 'icon-annotation')\n .attr('transform', 'translate(-6, -22)')\n .attr('width', '12px')\n .attr('height', '12px')\n .attr('xlink:href', d => d.icon ? '#' + d.icon : '');\n\n // update\n markers\n .merge(markersEnter)\n .sort(sortY)\n .classed('selected', d => d.id === selectedID)\n .attr('transform', getTransform);\n\n // Draw targets..\n if (touchLayer.empty()) return;\n const fillClass = context.getDebug('target') ? 'pink' : 'nocolor';\n\n const targets = touchLayer.selectAll('.qaItem.osmose')\n .data(data, d => d.id);\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('rect')\n .attr('width', '20px')\n .attr('height', '30px')\n .attr('x', '-10px')\n .attr('y', '-28px')\n .merge(targets)\n .sort(sortY)\n .attr('class', d => `qaItem ${d.service} target ${fillClass} itemId-${d.id}`)\n .attr('transform', getTransform);\n\n function sortY(a, b) {\n return (a.id === selectedID) ? 1\n : (b.id === selectedID) ? -1\n : b.loc[1] - a.loc[1];\n }\n }\n\n // Draw the Osmose layer and schedule loading issues and updating markers.\n function drawOsmose(selection) {\n const service = getService();\n\n const surface = context.surface();\n if (surface && !surface.empty()) {\n touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');\n }\n\n drawLayer = selection.selectAll('.layer-osmose')\n .data(service ? [0] : []);\n\n drawLayer.exit()\n .remove();\n\n drawLayer = drawLayer.enter()\n .append('g')\n .attr('class', 'layer-osmose')\n .style('display', _layerEnabled ? 'block' : 'none')\n .merge(drawLayer);\n\n if (_layerEnabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n service.loadIssues(projection);\n updateMarkers();\n } else {\n editOff();\n }\n }\n }\n\n // Toggles the layer on and off\n drawOsmose.enabled = function(val) {\n if (!arguments.length) return _layerEnabled;\n\n _layerEnabled = val;\n if (_layerEnabled) {\n // Strings supplied by Osmose fetched before showing layer for first time\n // NOTE: Currently no way to change locale in iD at runtime, would need to re-call this method if that's ever implemented\n // Also, If layer is toggled quickly multiple requests are sent\n getService().loadStrings()\n .then(layerOn)\n .catch(err => {\n console.log(err); // eslint-disable-line no-console\n });\n } else {\n layerOff();\n if (context.selectedErrorID()) {\n context.enter(modeBrowse(context));\n }\n }\n\n dispatch.call('change');\n return this;\n };\n\n drawOsmose.supported = () => !!getService();\n\n return drawOsmose;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgStreetside(projection, context, dispatch) {\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var minZoom = 14;\n var minMarkerZoom = 16;\n var minViewfieldZoom = 18;\n var layer = d3_select(null);\n var _viewerYaw = 0;\n var _selectedSequence = null;\n var _streetside;\n\n /**\n * init().\n */\n function init() {\n if (svgStreetside.initialized) return; // run once\n svgStreetside.enabled = false;\n svgStreetside.initialized = true;\n }\n\n /**\n * getService().\n */\n function getService() {\n if (services.streetside && !_streetside) {\n _streetside = services.streetside;\n _streetside.event\n .on('viewerChanged.svgStreetside', viewerChanged)\n .on('loadedImages.svgStreetside', throttledRedraw);\n } else if (!services.streetside && _streetside) {\n _streetside = null;\n }\n\n return _streetside;\n }\n\n /**\n * showLayer().\n */\n function showLayer() {\n var service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n /**\n * hideLayer().\n */\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n /**\n * editOn().\n */\n function editOn() {\n layer.style('display', 'block');\n }\n\n /**\n * editOff().\n */\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n /**\n * click() Handles 'bubble' point click event.\n */\n function click(d3_event, d) {\n var service = getService();\n if (!service) return;\n\n // try to preserve the viewer rotation when staying on the same sequence\n if (d.sequenceKey !== _selectedSequence) {\n _viewerYaw = 0; // reset\n }\n _selectedSequence = d.sequenceKey;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, d.key)\n .yaw(_viewerYaw)\n .showViewer(context);\n });\n\n context.map().centerEase(d.loc);\n }\n\n /**\n * mouseover().\n */\n function mouseover(d3_event, d) {\n var service = getService();\n if (service) service.setStyles(context, d);\n }\n\n /**\n * mouseout().\n */\n function mouseout() {\n var service = getService();\n if (service) service.setStyles(context, null);\n }\n\n /**\n * transform().\n */\n function transform(d) {\n var t = svgPointTransform(projection)(d);\n var rot = d.ca + _viewerYaw;\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n\n function viewerChanged() {\n var service = getService();\n if (!service) return;\n\n var viewer = service.viewer();\n if (!viewer) return;\n\n // update viewfield rotation\n _viewerYaw = viewer.getYaw();\n\n // avoid updating if the map is currently transformed\n // e.g. during drags or easing.\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .attr('transform', transform);\n }\n\n\n function filterBubbles(bubbles, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n bubbles = bubbles.filter(function(bubble) {\n return new Date(bubble.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n bubbles = bubbles.filter(function(bubble) {\n return new Date(bubble.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n bubbles = bubbles.filter(function(bubble) {\n return usernames.indexOf(bubble.captured_by) !== -1;\n });\n }\n\n return bubbles;\n }\n\n function filterSequences(sequences, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n sequences = sequences.filter(function(sequences) {\n return new Date(sequences.properties.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n sequences = sequences.filter(function(sequences) {\n return new Date(sequences.properties.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n sequences = sequences.filter(function(sequences) {\n return usernames.indexOf(sequences.properties.captured_by) !== -1;\n });\n }\n\n return sequences;\n }\n\n /**\n * update().\n */\n function update() {\n var viewer = context.container().select('.photoviewer');\n var selected = viewer.empty() ? undefined : viewer.datum();\n var z = ~~context.map().zoom();\n var showMarkers = (z >= minMarkerZoom);\n var showViewfields = (z >= minViewfieldZoom);\n var service = getService();\n\n var sequences = [];\n var bubbles = [];\n\n if (context.photos().showsPanoramic()) {\n sequences = (service ? service.sequences(projection) : []);\n bubbles = (service && showMarkers ? service.bubbles(projection) : []);\n sequences = filterSequences(sequences);\n bubbles = filterBubbles(bubbles);\n }\n\n var traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.key; });\n\n dispatch.call('photoDatesChanged', this, 'streetside', [\n ...filterBubbles(bubbles, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(t => t.properties.vintageStart)]);\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n var groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(bubbles, function(d) {\n // force reenter once bubbles are attached to a sequence\n return d.key + (d.sequenceKey ? 'v1' : 'v0');\n });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n var markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n return (a === selected) ? 1\n : (b === selected) ? -1\n : b.loc[1] - a.loc[1];\n })\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n var viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n // viewfields may or may not be drawn...\n // but if they are, draw below the circles\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n var d = this.parentNode.__data__;\n if (d.pano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n }\n\n /**\n * drawImages()\n * drawImages is the method that is returned (and that runs) every time 'svgStreetside()' is called.\n * 'svgStreetside()' is called from index.js\n */\n function drawImages(selection) {\n var enabled = svgStreetside.enabled;\n var service = getService();\n\n layer = selection.selectAll('.layer-streetside-images')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n var layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-streetside-images')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadBubbles(projection);\n } else {\n dispatch.call('photoDatesChanged', this, 'streetside', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'streetside', []);\n }\n }\n\n\n /**\n * drawImages.enabled().\n */\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgStreetside.enabled;\n svgStreetside.enabled = _;\n if (svgStreetside.enabled) {\n showLayer();\n context.photos().on('change.streetside', update);\n } else {\n hideLayer();\n context.photos().on('change.streetside', null);\n }\n dispatch.call('change');\n return this;\n };\n\n /**\n * drawImages.supported().\n */\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n init();\n\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgVegbilder(projection, context, dispatch) {\n const throttledRedraw = throttle(() => dispatch.call('change'), 1000);\n const minZoom = 14;\n const minMarkerZoom = 16;\n const minViewfieldZoom = 18;\n let layer = d3_select(null);\n let _viewerYaw = 0;\n let _vegbilder;\n\n /**\n * init().\n */\n function init() {\n if (svgVegbilder.initialized) return; // run once\n svgVegbilder.enabled = false;\n svgVegbilder.initialized = true;\n }\n\n /**\n * getService().\n */\n function getService() {\n if (services.vegbilder && !_vegbilder) {\n _vegbilder = services.vegbilder;\n _vegbilder.event\n .on('viewerChanged.svgVegbilder', viewerChanged)\n .on('loadedImages.svgVegbilder', throttledRedraw);\n } else if (!services.vegbilder && _vegbilder) {\n _vegbilder = null;\n }\n\n return _vegbilder;\n }\n\n /**\n * showLayer().\n */\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', () => dispatch.call('change'));\n }\n\n /**\n * hideLayer().\n */\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n /**\n * editOn().\n */\n function editOn() {\n layer.style('display', 'block');\n }\n\n /**\n * editOff().\n */\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n function click(d3_event, d) {\n const service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(() => {\n service\n .selectImage(context, d.key)\n .showViewer(context);\n });\n\n context.map().centerEase(d.loc);\n }\n\n /**\n * mouseover().\n */\n function mouseover(d3_event, d) {\n const service = getService();\n if (service) service.setStyles(context, d);\n }\n\n /**\n * mouseout().\n */\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n /**\n * transform().\n */\n function transform(d, selected) {\n let t = svgPointTransform(projection)(d);\n let rot = d.ca;\n if (d === selected) {\n rot += _viewerYaw;\n }\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n\n function viewerChanged() {\n const service = getService();\n if (!service) return;\n\n const frame = service.photoFrame();\n if (!frame) return;\n\n // update viewfield rotation\n _viewerYaw = frame.getYaw();\n\n // avoid updating if the map is currently transformed\n // e.g. during drags or easing.\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .attr('transform', (d) => transform(d, d));\n }\n\n function filterImages(images, skipDateFilter) {\n const photoContext = context.photos();\n const fromDateString = photoContext.fromDate();\n const toDateString = photoContext.toDate();\n const showsFlat = photoContext.showsFlat();\n const showsPano = photoContext.showsPanoramic();\n\n if (fromDateString && !skipDateFilter) {\n const fromDate = new Date(fromDateString);\n images = images.filter(image => image.captured_at.getTime() >= fromDate.getTime());\n }\n\n if (toDateString && !skipDateFilter) {\n const toDate = new Date(toDateString);\n images = images.filter(image => image.captured_at.getTime() <= toDate.getTime());\n }\n\n if (!showsPano) {\n images = images.filter(image => !image.is_sphere);\n }\n\n if (!showsFlat) {\n images = images.filter(image => image.is_sphere);\n }\n\n return images;\n }\n\n function filterSequences(sequences, skipDateFilter) {\n const photoContext = context.photos();\n const fromDateString = photoContext.fromDate();\n const toDateString = photoContext.toDate();\n const showsFlat = photoContext.showsFlat();\n const showsPano = photoContext.showsPanoramic();\n\n if (fromDateString && !skipDateFilter) {\n const fromDate = new Date(fromDateString);\n sequences = sequences.filter(({images}) => images[0].captured_at.getTime() >= fromDate.getTime());\n }\n\n if (toDateString && !skipDateFilter) {\n const toDate = new Date(toDateString);\n sequences = sequences.filter(({images}) => images[images.length - 1].captured_at.getTime() <= toDate.getTime());\n }\n\n if (!showsPano) {\n sequences = sequences.filter(({images}) => !images[0].is_sphere);\n }\n\n if (!showsFlat) {\n sequences = sequences.filter(({images}) => images[0].is_sphere);\n }\n\n return sequences;\n }\n\n /**\n * update().\n */\n function update() {\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n const z = ~~context.map().zoom();\n const showMarkers = (z >= minMarkerZoom);\n const showViewfields = (z >= minViewfieldZoom);\n const service = getService();\n let sequences = [];\n let images = [];\n\n if (service) {\n // The WFS-layer for that year or image type may not be loaded after a filter is changed\n service.loadImages(context);\n\n sequences = service.sequences(projection);\n images = showMarkers ? service.images(projection) : [];\n\n dispatch.call('photoDatesChanged', this, 'vegbilder', [\n ...filterImages(images, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(s => s.images[0].captured_at)]);\n\n images = filterImages(images);\n sequences = filterSequences(sequences);\n }\n\n let traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, d => d.key);\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, (d) => d.key);\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort((a, b) => {\n return (a === selected) ? 1\n : (b === selected) ? -1\n : b.loc[1] - a.loc[1];\n })\n .attr('transform', (d) => transform(d, selected))\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n // viewfields may or may not be drawn...\n // but if they are, draw below the circles\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n const d = this.parentNode.__data__;\n if (d.is_sphere) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n }\n\n /**\n * drawImages()\n * drawImages is the method that is returned (and that runs) every time 'svgStreetside()' is called.\n * 'svgStreetside()' is called from index.js\n */\n function drawImages(selection) {\n const enabled = svgVegbilder.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-vegbilder')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-vegbilder')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadImages(context);\n } else {\n dispatch.call('photoDatesChanged', this, 'vegbilder', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'vegbilder', []);\n }\n }\n\n\n /**\n * drawImages.enabled().\n */\n drawImages.enabled = function (_) {\n if (!arguments.length) return svgVegbilder.enabled;\n svgVegbilder.enabled = _;\n if (svgVegbilder.enabled) {\n showLayer();\n context.photos().on('change.vegbilder', update);\n } else {\n hideLayer();\n context.photos().on('change.vegbilder', null);\n }\n dispatch.call('change');\n return this;\n };\n\n /**\n * drawImages.supported().\n */\n drawImages.supported = function () {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n drawImages.validHere = function(extent, zoom) {\n return zoom >= (minZoom - 2)\n && getService().validHere(extent);\n };\n\n init();\n\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgMapillaryImages(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const minZoom = 12;\n const minMarkerZoom = 16;\n const minViewfieldZoom = 18;\n let layer = d3_select(null);\n let _mapillary;\n\n\n function init() {\n if (svgMapillaryImages.initialized) return; // run once\n svgMapillaryImages.enabled = false;\n svgMapillaryImages.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('loadedImages', throttledRedraw);\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n\n return _mapillary;\n }\n\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, image) {\n const service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, image.id)\n .showViewer(context);\n });\n\n context.map().centerEase(image.loc);\n }\n\n\n function mouseover(d3_event, image) {\n const service = getService();\n\n if (service) service.setStyles(context, image);\n }\n\n\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n\n function transform(d) {\n let t = svgPointTransform(projection)(d);\n if (d.ca) {\n t += ' rotate(' + Math.floor(d.ca) + ',0,0)';\n }\n return t;\n }\n\n\n function filterImages(images, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n\n if (!showsPano || !showsFlat) {\n images = images.filter(function(image) {\n if (image.is_pano) return showsPano;\n return showsFlat;\n });\n }\n if (fromDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.captured_at).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.captured_at).getTime() <= new Date(toDate).getTime();\n });\n }\n\n return images;\n }\n\n function filterSequences(sequences, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n\n if (!showsPano || !showsFlat) {\n sequences = sequences.filter(function(sequence) {\n if (sequence.properties.hasOwnProperty('is_pano')) {\n if (sequence.properties.is_pano) return showsPano;\n return showsFlat;\n }\n return false;\n });\n }\n if (fromDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() >= new Date(fromDate).getTime().toString();\n });\n }\n if (toDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() <= new Date(toDate).getTime().toString();\n });\n }\n\n return sequences;\n }\n\n function update() {\n const z = ~~context.map().zoom();\n const showMarkers = (z >= minMarkerZoom);\n const showViewfields = (z >= minViewfieldZoom);\n\n const service = getService();\n let sequences = (service ? service.sequences(projection) : []);\n let images = (service && showMarkers ? service.images(projection) : []);\n // images[0]\n // {\n // \"loc\":[13.235349655151367,52.50694232952122],\n // \"captured_at\":1619457514500,\n // \"ca\":0,\n // \"id\":505488307476058,\n // \"is_pano\":false,\n // \"sequence_id\":\"zcyumxorbza3dq3twjybam\"\n // }\n dispatch.call('photoDatesChanged', this, 'mapillary', [\n ...filterImages(images, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(s => s.properties.captured_at)]);\n\n images = filterImages(images);\n sequences = filterSequences(sequences);\n\n service.filterViewer(context);\n\n let traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.id; });\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n return b.loc[1] - a.loc[1]; // sort Y\n })\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter() // viewfields may or may not be drawn...\n .insert('path', 'circle') // but if they are, draw below the circles\n .attr('class', 'viewfield')\n .classed('pano', function() { return this.parentNode.__data__.is_pano; })\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n if (this.parentNode.__data__.is_pano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n }\n\n\n function drawImages(selection) {\n const enabled = svgMapillaryImages.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadImages(projection);\n } else {\n dispatch.call('photoDatesChanged', this, 'mapillary', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'mapillary', []);\n }\n }\n\n\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgMapillaryImages.enabled;\n svgMapillaryImages.enabled = _;\n if (svgMapillaryImages.enabled) {\n showLayer();\n context.photos().on('change.mapillary_images', update);\n } else {\n hideLayer();\n context.photos().on('change.mapillary_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgMapillaryPosition(projection, context) {\n const throttledRedraw = throttle(function () { update(); }, 1000);\n const minZoom = 12;\n const minViewfieldZoom = 18;\n let layer = d3_select(null);\n let _mapillary;\n let viewerCompassAngle;\n\n\n function init() {\n if (svgMapillaryPosition.initialized) return; // run once\n svgMapillaryPosition.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('imageChanged', throttledRedraw);\n _mapillary.event.on('bearingChanged', function(e) {\n viewerCompassAngle = e.bearing;\n\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .filter(function(d) {\n return d.is_pano;\n })\n .attr('transform', transform);\n });\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n\n return _mapillary;\n }\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n function transform(d) {\n let t = svgPointTransform(projection)(d);\n if (d.is_pano && viewerCompassAngle !== null && isFinite(viewerCompassAngle)) {\n t += ' rotate(' + Math.floor(viewerCompassAngle) + ',0,0)';\n } else if (d.ca) {\n t += ' rotate(' + Math.floor(d.ca) + ',0,0)';\n }\n return t;\n }\n\n function update() {\n\n const z = ~~context.map().zoom();\n const showViewfields = (z >= minViewfieldZoom);\n\n const service = getService();\n const image = service && service.getActiveImage();\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(image ? [image] : [], function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group currentView highlighted');\n\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z');\n }\n\n\n function drawImages(selection) {\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary-position')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary-position');\n\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n } else {\n editOff();\n }\n }\n\n\n drawImages.enabled = function() {\n update();\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgMapillarySigns(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const minZoom = 12;\n let layer = d3_select(null);\n let _mapillary;\n\n\n function init() {\n if (svgMapillarySigns.initialized) return; // run once\n svgMapillarySigns.enabled = false;\n svgMapillarySigns.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('loadedSigns', throttledRedraw);\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n return _mapillary;\n }\n\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n service.loadSignResources(context);\n editOn();\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n editOff();\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.icon-sign').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, d) {\n const service = getService();\n if (!service) return;\n\n context.map().centerEase(d.loc);\n\n const selectedImageId = service.getActiveImage() && service.getActiveImage().id;\n\n service.getDetections(d.id).then(detections => {\n if (detections.length) {\n const imageId = detections[0].image.id;\n if (imageId === selectedImageId) {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId);\n } else {\n service.ensureViewerLoaded(context)\n .then(function() {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId)\n .showViewer(context);\n });\n }\n }\n });\n }\n\n\n function filterData(detectedFeatures) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n\n if (fromDate) {\n var fromTimestamp = new Date(fromDate).getTime();\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.last_seen_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate) {\n var toTimestamp = new Date(toDate).getTime();\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.first_seen_at).getTime() <= toTimestamp;\n });\n }\n\n return detectedFeatures;\n }\n\n\n function update() {\n const service = getService();\n let data = (service ? service.signs(projection) : []);\n data = filterData(data);\n\n const transform = svgPointTransform(projection);\n\n const signs = layer.selectAll('.icon-sign')\n .data(data, function(d) { return d.id; });\n\n // exit\n signs.exit()\n .remove();\n\n // enter\n const enter = signs.enter()\n .append('g')\n .attr('class', 'icon-sign icon-detected')\n .on('click', click);\n\n enter\n .append('use')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px')\n .attr('xlink:href', function(d) { return '#' + d.value; });\n\n enter\n .append('rect')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px');\n\n // update\n signs\n .merge(enter)\n .attr('transform', transform);\n }\n\n\n function drawSigns(selection) {\n const enabled = svgMapillarySigns.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary-signs')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary-signs layer-mapillary-detections')\n .style('display', enabled ? 'block' : 'none')\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadSigns(projection);\n service.showSignDetections(true);\n } else {\n editOff();\n }\n } else if (service) {\n service.showSignDetections(false);\n }\n }\n\n\n drawSigns.enabled = function(_) {\n if (!arguments.length) return svgMapillarySigns.enabled;\n svgMapillarySigns.enabled = _;\n if (svgMapillarySigns.enabled) {\n showLayer();\n context.photos().on('change.mapillary_signs', update);\n } else {\n hideLayer();\n context.photos().on('change.mapillary_signs', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawSigns.supported = function() {\n return !!getService();\n };\n\n drawSigns.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawSigns;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\nimport { t } from '../core/localizer';\n\nexport function svgMapillaryMapFeatures(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const minZoom = 12;\n let layer = d3_select(null);\n let _mapillary;\n\n\n function init() {\n if (svgMapillaryMapFeatures.initialized) return; // run once\n svgMapillaryMapFeatures.enabled = false;\n svgMapillaryMapFeatures.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('loadedMapFeatures', throttledRedraw);\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n return _mapillary;\n }\n\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n service.loadObjectResources(context);\n editOn();\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n editOff();\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.icon-map-feature').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, d) {\n const service = getService();\n if (!service) return;\n\n context.map().centerEase(d.loc);\n\n const selectedImageId = service.getActiveImage() && service.getActiveImage().id;\n\n service.getDetections(d.id).then(detections => {\n if (detections.length) {\n const imageId = detections[0].image.id;\n if (imageId === selectedImageId) {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId);\n } else {\n service.ensureViewerLoaded(context)\n .then(function() {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId)\n .showViewer(context);\n });\n }\n }\n });\n }\n\n\n function filterData(detectedFeatures) {\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n\n if (fromDate) {\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.last_seen_at).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate) {\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.first_seen_at).getTime() <= new Date(toDate).getTime();\n });\n }\n\n return detectedFeatures;\n }\n\n\n function update() {\n const service = getService();\n let data = (service ? service.mapFeatures(projection) : []);\n data = filterData(data);\n\n const transform = svgPointTransform(projection);\n\n const mapFeatures = layer.selectAll('.icon-map-feature')\n .data(data, function(d) { return d.id; });\n\n // exit\n mapFeatures.exit()\n .remove();\n\n // enter\n const enter = mapFeatures.enter()\n .append('g')\n .attr('class', 'icon-map-feature icon-detected')\n .on('click', click);\n\n enter\n .append('title')\n .text(function(d) {\n var id = d.value.replace(/--/g, '.').replace(/-/g, '_');\n return t('mapillary_map_features.' + id);\n });\n\n enter\n .append('use')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px')\n .attr('xlink:href', function(d) {\n if (d.value === 'object--billboard') {\n // no billboard icon right now, so use the advertisement icon\n return '#object--sign--advertisement';\n }\n return '#' + d.value;\n });\n\n enter\n .append('rect')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px');\n\n // update\n mapFeatures\n .merge(enter)\n .attr('transform', transform);\n }\n\n\n function drawMapFeatures(selection) {\n const enabled = svgMapillaryMapFeatures.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary-map-features')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary-map-features layer-mapillary-detections')\n .style('display', enabled ? 'block' : 'none')\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadMapFeatures(projection);\n service.showFeatureDetections(true);\n } else {\n editOff();\n }\n } else if (service) {\n service.showFeatureDetections(false);\n }\n }\n\n\n drawMapFeatures.enabled = function(_) {\n if (!arguments.length) return svgMapillaryMapFeatures.enabled;\n svgMapillaryMapFeatures.enabled = _;\n if (svgMapillaryMapFeatures.enabled) {\n showLayer();\n context.photos().on('change.mapillary_map_features', update);\n } else {\n hideLayer();\n context.photos().on('change.mapillary_map_features', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawMapFeatures.supported = function() {\n return !!getService();\n };\n\n drawMapFeatures.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawMapFeatures;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgKartaviewImages(projection, context, dispatch) {\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var minZoom = 12;\n var minMarkerZoom = 16;\n var minViewfieldZoom = 18;\n var layer = d3_select(null);\n var _kartaview;\n\n\n function init() {\n if (svgKartaviewImages.initialized) return; // run once\n svgKartaviewImages.enabled = false;\n svgKartaviewImages.initialized = true;\n }\n\n\n function getService() {\n if (services.kartaview && !_kartaview) {\n _kartaview = services.kartaview;\n _kartaview.event.on('loadedImages', throttledRedraw);\n } else if (!services.kartaview && _kartaview) {\n _kartaview = null;\n }\n\n return _kartaview;\n }\n\n\n function showLayer() {\n var service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, d) {\n var service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service.selectImage(context, d.key)\n .showViewer(context);\n });\n\n context.map().centerEase(d.loc);\n }\n\n\n function mouseover(d3_event, d) {\n var service = getService();\n if (service) service.setStyles(context, d);\n }\n\n\n function mouseout() {\n var service = getService();\n if (service) service.setStyles(context, null);\n }\n\n\n function transform(d) {\n var t = svgPointTransform(projection)(d);\n if (d.ca) {\n t += ' rotate(' + Math.floor(d.ca) + ',0,0)';\n }\n return t;\n }\n\n\n function filterImages(images, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n images = images.filter(function(item) {\n return new Date(item.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n images = images.filter(function(item) {\n return new Date(item.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n images = images.filter(function(item) {\n return usernames.indexOf(item.captured_by) !== -1;\n });\n }\n\n return images;\n }\n\n function filterSequences(sequences, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n sequences = sequences.filter(function(sequence) {\n return usernames.indexOf(sequence.properties.captured_by) !== -1;\n });\n }\n\n return sequences;\n }\n\n function update() {\n var viewer = context.container().select('.photoviewer');\n var selected = viewer.empty() ? undefined : viewer.datum();\n\n var z = ~~context.map().zoom();\n var showMarkers = (z >= minMarkerZoom);\n var showViewfields = (z >= minViewfieldZoom);\n\n const service = getService();\n\n let sequences = (service ? service.sequences(projection) : []);\n let images = (service && showMarkers ? service.images(projection) : []);\n dispatch.call('photoDatesChanged', this, 'kartaview', [\n ...filterImages(images, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(s => s.properties.captured_at)]);\n sequences = filterSequences(sequences);\n images = filterImages(images);\n\n var traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.key; });\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n var groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, function(d) { return d.key; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n var markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n return (a === selected) ? 1\n : (b === selected) ? -1\n : b.loc[1] - a.loc[1]; // sort Y\n })\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n var viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter() // viewfields may or may not be drawn...\n .insert('path', 'circle') // but if they are, draw below the circles\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z');\n }\n\n\n function drawImages(selection) {\n var enabled = svgKartaviewImages.enabled,\n service = getService();\n\n layer = selection.selectAll('.layer-kartaview')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n var layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-kartaview')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadImages(projection);\n } else {\n dispatch.call('photoDatesChanged', this, 'kartaview', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'kartaview', []);\n }\n }\n\n\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgKartaviewImages.enabled;\n svgKartaviewImages.enabled = _;\n if (svgKartaviewImages.enabled) {\n showLayer();\n context.photos().on('change.kartaview_images', update);\n } else {\n hideLayer();\n context.photos().on('change.kartaview_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { services } from '../services';\nimport {svgPath, svgPointTransform} from './helpers';\n\n\nexport function svgMapilioImages(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const imageMinZoom = 16;\n const lineMinZoom = 10;\n const viewFieldZoomLevel = 18;\n let layer = d3_select(null);\n let _mapilio;\n let _viewerYaw = 0;\n\n function init() {\n if (svgMapilioImages.initialized) return;\n svgMapilioImages.enabled = false;\n svgMapilioImages.initialized = true;\n }\n\n function getService() {\n if (services.mapilio && !_mapilio) {\n _mapilio = services.mapilio;\n _mapilio.event\n .on('loadedImages', throttledRedraw)\n .on('loadedLines', throttledRedraw);\n } else if (!services.mapilio && _mapilio) {\n _mapilio = null;\n }\n\n return _mapilio;\n }\n\n /**\n * Filters images\n * @param {*} images\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered images\n */\n function filterImages(images, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n\n if (fromDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() <= new Date(toDate).getTime();\n });\n }\n\n return images;\n }\n\n /**\n * Filters sequences\n * @param {*} sequences\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered sequences\n */\n function filterSequences(sequences, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n\n if (fromDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.capture_time).getTime() >= new Date(fromDate).getTime().toString();\n });\n }\n if (toDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.capture_time).getTime() <= new Date(toDate).getTime().toString();\n });\n }\n\n return sequences;\n }\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n function transform(d, selectedImageId) {\n let t = svgPointTransform(projection)(d);\n let rot = d.heading || 0;\n\n if (d.id === selectedImageId) {\n rot += _viewerYaw;\n }\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n function click(d3_event, image) {\n const service = getService();\n if (!service) return;\n\n service.ensureViewerLoaded(context, image.id)\n .then(() => {\n service.selectImage(context, image.id)\n .showViewer(context);\n });\n\n context.map().centerEase(image.loc);\n }\n\n function mouseover(d3_event, image) {\n const service = getService();\n if (service) service.setStyles(context, image);\n }\n\n\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n async function update() {\n const zoom = ~~context.map().zoom();\n const showViewfields = (zoom >= viewFieldZoomLevel);\n const service = getService();\n\n let sequences = (service ? service.sequences(projection, zoom) : []);\n let images = (service && zoom >= imageMinZoom ? service.images(projection) : []);\n\n dispatch.call('photoDatesChanged', this, 'mapilio', [\n ...filterImages(images, true).map(p => p.capture_time),\n ...filterSequences(sequences, true).map(s => s.properties.capture_time)\n ]);\n\n images = await filterImages(images);\n sequences = await filterSequences(sequences);\n\n const activeImage = service.getActiveImage?.();\n const activeImageId = activeImage ? activeImage.id : null;\n\n let traces = layer\n .selectAll('.sequences')\n .selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.id; });\n\n // exit\n traces.exit().remove();\n\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n const groups = layer\n .selectAll('.markers')\n .selectAll('.viewfield-group')\n .data(images, function(d) { return d.id; });\n\n // exit\n groups.exit().remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n if (a.id === activeImageId) return 1;\n if (b.id === activeImageId) return -1;\n return a.capture_time_parsed - b.capture_time_parsed;\n })\n .attr('transform', d => transform(d, activeImageId))\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit().remove();\n\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n service.setStyles(context, null);\n\n function viewfieldPath() {\n if (this.parentNode.__data__.isPano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n }\n\n function drawImages(selection) {\n const enabled = svgMapilioImages.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapilio')\n .data(service ? [0] : []);\n\n layer.exit().remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-mapilio')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter.merge(layer);\n\n if (enabled) {\n let zoom = ~~context.map().zoom();\n if (service) {\n if (zoom >= imageMinZoom) {\n editOn();\n update();\n service.loadImages(projection);\n service.loadLines(projection, zoom);\n } else if (zoom >= lineMinZoom) {\n editOn();\n update();\n service.loadImages(projection);\n service.loadLines(projection, zoom);\n } else {\n editOff();\n dispatch.call('photoDatesChanged', this, 'mapilio', []);\n service.selectImage(context, null);\n }\n } else {\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'mapilio', []);\n }\n }\n\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgMapilioImages.enabled;\n svgMapilioImages.enabled = _;\n if (svgMapilioImages.enabled) {\n showLayer();\n context.photos().on('change.mapilio_images', update);\n } else {\n hideLayer();\n context.photos().on('change.mapilio_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= lineMinZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { services } from '../services';\nimport {svgPath, svgPointTransform} from './helpers';\n\n\nexport function svgPanoramaxImages(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const imageMinZoom = 15;\n const lineMinZoom = 10;\n const viewFieldZoomLevel = 18;\n let layer = d3_select(null);\n let _panoramax;\n let _viewerYaw = 0;\n let _activeUsernameFilter;\n let _activeIds;\n\n function init() {\n if (svgPanoramaxImages.initialized) return;\n svgPanoramaxImages.enabled = false;\n svgPanoramaxImages.initialized = true;\n }\n\n function getService() {\n if (services.panoramax && !_panoramax) {\n _panoramax = services.panoramax;\n _panoramax.event\n .on('viewerChanged', viewerChanged)\n .on('loadedLines', throttledRedraw)\n .on('loadedImages', throttledRedraw);\n } else if (!services.panoramax && _panoramax) {\n _panoramax = null;\n }\n\n return _panoramax;\n }\n\n /**\n * Filters the images given the filters on the right panel\n * @param {*} images\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered images\n */\n async function filterImages(images, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n const username = context.photos().usernames();\n\n const service = getService();\n\n if (!showsPano || !showsFlat) {\n images = images.filter(function(image) {\n if (image.isPano) return showsPano;\n return showsFlat;\n });\n }\n if (fromDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() <= new Date(toDate).getTime();\n });\n }\n if (username && service) {\n if (_activeUsernameFilter !== username) {\n _activeUsernameFilter = username;\n\n const tempIds = await service.getUserIds(username);\n\n _activeIds = {};\n tempIds.forEach(id => {\n _activeIds[id] = true;\n });\n }\n\n images = images.filter(function(image) {\n return _activeIds[image.account_id];\n });\n }\n\n return images;\n }\n\n /**\n * Filters the sequences given the filters on the right panel\n * @param {*} sequences\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered sequences\n */\n async function filterSequences(sequences, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n const username = context.photos().usernames();\n\n const service = getService();\n\n if (!showsPano || !showsFlat) {\n sequences = sequences.filter(function(sequence) {\n if (sequence.properties.type === 'equirectangular') return showsPano;\n return showsFlat;\n });\n }\n if (fromDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.date).getTime() >= new Date(fromDate).getTime().toString();\n });\n }\n if (toDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.date).getTime() <= new Date(toDate).getTime().toString();\n });\n }\n if (username && service) {\n if (_activeUsernameFilter !== username) {\n _activeUsernameFilter = username;\n\n const tempIds = await service.getUserIds(username);\n\n _activeIds = {};\n tempIds.forEach(id => {\n _activeIds[id] = true;\n });\n }\n\n sequences = sequences.filter(function(sequence) {\n return _activeIds[sequence.properties.account_id];\n });\n }\n\n return sequences;\n }\n\n /**\n * Shows the selected layer\n */\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n /**\n * Hides the selected layer\n */\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n /**\n * Updates the viewfinder for the selected image bubble based on the frame's yaw\n * @param {*} d Current Active image Data\n * @param {*} selectedImageId The selected bubble image ID\n */\n function transform(d, selectedImageId) {\n let t = svgPointTransform(projection)(d);\n let rot = d.heading;\n if (d.id === selectedImageId) {\n rot += _viewerYaw;\n }\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n /**\n * Updates the current selected image\n * @param {*} image The selected image bubble data\n */\n function click(d3_event, image) {\n const service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, image.id)\n .showViewer(context);\n });\n\n context.map().centerEase(image.loc);\n }\n\n function mouseover(d3_event, image) {\n const service = getService();\n if (service) service.setStyles(context, image);\n }\n\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n /**\n * Updates the current view, rearranging lines and bubbles.\n */\n async function update() {\n const zoom = ~~context.map().zoom();\n const showViewfields = (zoom >= viewFieldZoomLevel);\n\n const service = getService();\n let sequences = (service ? service.sequences(projection, zoom) : []);\n let images = (service && zoom >= imageMinZoom ? service.images(projection) : []);\n dispatch.call('photoDatesChanged', this, 'panoramax', [\n ...(await filterImages(images, true)).map(p => p.capture_time),\n ...(await filterSequences(sequences, true)).map(s => s.properties.date)]);\n\n images = await filterImages(images);\n sequences = await filterSequences(sequences);\n\n let traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.id; });\n\n // exit\n traces.exit()\n .remove();\n\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n const activeImageId = service.getActiveImage()?.id;\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n // active image on top\n if (a.id === activeImageId) return 1;\n if (b.id === activeImageId) return -1;\n // else: sort by capture time (newest on top)\n return a.capture_time_parsed - b.capture_time_parsed;\n })\n .attr('transform', d => transform(d, activeImageId))\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n service.setStyles(context, null);\n\n function viewfieldPath() {\n if (this.parentNode.__data__.isPano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n }\n\n function viewerChanged() {\n const service = getService();\n if (!service) return;\n\n const frame = service.photoFrame();\n if (!frame) return;\n\n // update viewfield rotation\n _viewerYaw = frame.getYaw();\n\n // avoid updating if the map is currently transformed\n // e.g. during drags or easing.\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .attr('transform', d => transform(d, d.id));\n }\n\n\n /**\n * Draws bubbles and lines on the current view\n * @param {*} selection Current HTML Selection\n */\n function drawImages(selection) {\n\n const enabled = svgPanoramaxImages.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-panoramax')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-panoramax')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n let zoom = ~~context.map().zoom();\n if (service){\n if (zoom >= imageMinZoom) {\n editOn();\n update();\n service.loadImages(projection);\n } else if (zoom >= lineMinZoom) {\n editOn();\n update();\n service.loadLines(projection, zoom);\n } else {\n editOff();\n dispatch.call('photoDatesChanged', this, 'panoramax', []);\n }\n } else {\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'panoramax', []);\n }\n }\n\n /**\n * @returns if layer is active\n */\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgPanoramaxImages.enabled;\n svgPanoramaxImages.enabled = _;\n if (svgPanoramaxImages.enabled) {\n showLayer();\n context.photos().on('change.panoramax_images', update);\n } else {\n hideLayer();\n context.photos().on('change.panoramax_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n /**\n * @returns if layer is drawn\n */\n drawImages.rendered = function(zoom) {\n return zoom >= lineMinZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "export function svgOsm(projection, context, dispatch) {\n var enabled = true;\n\n\n function drawOsm(selection) {\n selection.selectAll('.layer-osm')\n .data(['covered', 'areas', 'lines', 'points', 'auxiliary', 'labels'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'layer-osm ' + d; });\n\n selection.selectAll('.layer-osm.points').selectAll('.points-group')\n .data(['vertices', 'midpoints', 'points', 'turns'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'points-group ' + d; });\n }\n\n\n function showLayer() {\n var layer = context.surface().selectAll('.data-layer.osm');\n layer.interrupt();\n\n layer\n .classed('disabled', false)\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end interrupt', function () {\n dispatch.call('change');\n });\n }\n\n\n function hideLayer() {\n var layer = context.surface().selectAll('.data-layer.osm');\n layer.interrupt();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end interrupt', function () {\n layer.classed('disabled', true);\n dispatch.call('change');\n });\n }\n\n\n drawOsm.enabled = function(val) {\n if (!arguments.length) return enabled;\n enabled = val;\n\n if (enabled) {\n showLayer();\n } else {\n hideLayer();\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n return drawOsm;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { modeBrowse } from '../modes/browse';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\nimport { utilStringQs } from '../util';\n\nvar hash = utilStringQs(window.location.hash);\n\nvar _notesEnabled = !!hash.notes;\nvar _osmService;\n\n\nexport function svgNotes(projection, context, dispatch) {\n if (!dispatch) { dispatch = d3_dispatch('change'); }\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var minZoom = 12;\n var touchLayer = d3_select(null);\n var drawLayer = d3_select(null);\n var _notesVisible = false;\n\n\n function markerPath(selection, klass) {\n selection\n .attr('class', klass)\n .attr('transform', 'translate(-8, -22)')\n .attr('d', 'm17.5,0l-15,0c-1.37,0 -2.5,1.12 -2.5,2.5l0,11.25c0,1.37 1.12,2.5 2.5,2.5l3.75,0l0,3.28c0,0.38 0.43,0.6 0.75,0.37l4.87,-3.65l5.62,0c1.37,0 2.5,-1.12 2.5,-2.5l0,-11.25c0,-1.37 -1.12,-2.5 -2.5,-2.5z');\n }\n\n\n // Loosely-coupled osm service for fetching notes.\n function getService() {\n if (services.osm && !_osmService) {\n _osmService = services.osm;\n _osmService.on('loadedNotes', throttledRedraw);\n } else if (!services.osm && _osmService) {\n _osmService = null;\n }\n\n return _osmService;\n }\n\n\n // Show the notes\n function editOn() {\n if (!_notesVisible) {\n _notesVisible = true;\n drawLayer\n .style('display', 'block');\n }\n }\n\n\n // Immediately remove the notes and their touch targets\n function editOff() {\n if (_notesVisible) {\n _notesVisible = false;\n drawLayer\n .style('display', 'none');\n drawLayer.selectAll('.note')\n .remove();\n touchLayer.selectAll('.note')\n .remove();\n }\n }\n\n\n // Enable the layer. This shows the notes and transitions them to visible.\n function layerOn() {\n editOn();\n\n drawLayer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end interrupt', function () {\n dispatch.call('change');\n });\n }\n\n\n // Disable the layer. This transitions the layer invisible and then hides the notes.\n function layerOff() {\n throttledRedraw.cancel();\n drawLayer.interrupt();\n touchLayer.selectAll('.note')\n .remove();\n\n drawLayer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end interrupt', function () {\n editOff();\n dispatch.call('change');\n });\n }\n\n\n // Update the note markers\n function updateMarkers() {\n if (!_notesVisible || !_notesEnabled) return;\n\n var service = getService();\n var selectedID = context.selectedNoteID();\n var data = (service ? service.notes(projection) : []);\n var getTransform = svgPointTransform(projection);\n\n // Draw markers..\n var notes = drawLayer.selectAll('.note')\n .data(data, function(d) { return d.status + d.id; });\n\n // exit\n notes.exit()\n .remove();\n\n // enter\n var notesEnter = notes.enter()\n .append('g')\n .attr('class', function(d) { return 'note note-' + d.id + ' ' + d.status; })\n .classed('new', function(d) { return d.id < 0; });\n\n notesEnter\n .append('ellipse')\n .attr('cx', 0.5)\n .attr('cy', 1)\n .attr('rx', 6.5)\n .attr('ry', 3)\n .attr('class', 'stroke');\n\n notesEnter\n .append('path')\n .call(markerPath, 'shadow');\n\n notesEnter\n .append('use')\n .attr('class', 'note-fill')\n .attr('width', '20px')\n .attr('height', '20px')\n .attr('x', '-8px')\n .attr('y', '-22px')\n .attr('xlink:href', '#iD-icon-note');\n\n notesEnter.selectAll('.icon-annotation')\n .data(function(d) { return [d]; })\n .enter()\n .append('use')\n .attr('class', 'icon-annotation')\n .attr('width', '10px')\n .attr('height', '10px')\n .attr('x', '-3px')\n .attr('y', '-19px')\n .attr('xlink:href', function(d) {\n if (d.id < 0) return '#iD-icon-plus';\n if (d.status === 'open') return '#iD-icon-close';\n return '#iD-icon-apply';\n });\n\n // update\n notes\n .merge(notesEnter)\n .sort(sortY)\n .classed('selected', function(d) {\n var mode = context.mode();\n var isMoving = mode && mode.id === 'drag-note'; // no shadows when dragging\n return !isMoving && d.id === selectedID;\n })\n .attr('transform', getTransform);\n\n\n // Draw targets..\n if (touchLayer.empty()) return;\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n\n var targets = touchLayer.selectAll('.note')\n .data(data, function(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('rect')\n .attr('width', '20px')\n .attr('height', '20px')\n .attr('x', '-8px')\n .attr('y', '-22px')\n .merge(targets)\n .sort(sortY)\n .attr('class', function(d) {\n var newClass = (d.id < 0 ? 'new' : '');\n return 'note target note-' + d.id + ' ' + fillClass + newClass;\n })\n .attr('transform', getTransform);\n\n\n function sortY(a, b) {\n if (a.id === selectedID) return 1;\n if (b.id === selectedID) return -1;\n return b.loc[1] - a.loc[1];\n }\n }\n\n\n // Draw the notes layer and schedule loading notes and updating markers.\n function drawNotes(selection) {\n var service = getService();\n\n var surface = context.surface();\n if (surface && !surface.empty()) {\n touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');\n }\n\n drawLayer = selection.selectAll('.layer-notes')\n .data(service ? [0] : []);\n\n drawLayer.exit()\n .remove();\n\n drawLayer = drawLayer.enter()\n .append('g')\n .attr('class', 'layer-notes')\n .style('display', _notesEnabled ? 'block' : 'none')\n .merge(drawLayer);\n\n if (_notesEnabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n service.loadNotes(projection);\n updateMarkers();\n } else {\n editOff();\n }\n }\n }\n\n\n // Toggles the layer on and off\n drawNotes.enabled = function(val) {\n if (!arguments.length) return _notesEnabled;\n\n _notesEnabled = val;\n if (_notesEnabled) {\n layerOn();\n } else {\n layerOff();\n if (context.selectedNoteID()) {\n context.enter(modeBrowse(context));\n }\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n return drawNotes;\n}\n", "export function svgTouch() {\n\n function drawTouch(selection) {\n selection.selectAll('.layer-touch')\n .data(['areas', 'lines', 'points', 'turns', 'markers'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'layer-touch ' + d; });\n }\n\n return drawTouch;\n}\n", "function refresh(selection, node) {\n var cr = node.getBoundingClientRect();\n var prop = [cr.width, cr.height];\n selection.property('__dimensions__', prop);\n return prop;\n}\n\nexport function utilGetDimensions(selection, force) {\n if (!selection || selection.empty()) {\n return [0, 0];\n }\n var node = selection.node(),\n cached = selection.property('__dimensions__');\n return (!cached || force) ? refresh(selection, node) : cached;\n}\n\n\nexport function utilSetDimensions(selection, dimensions) {\n if (!selection || selection.empty()) {\n return selection;\n }\n var node = selection.node();\n if (dimensions === null) {\n refresh(selection, node);\n return selection;\n }\n return selection\n .property('__dimensions__', [dimensions[0], dimensions[1]])\n .attr('width', dimensions[0])\n .attr('height', dimensions[1]);\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { svgData } from './data';\nimport { svgLocalPhotos} from './local_photos';\nimport { svgDebug } from './debug';\nimport { svgGeolocate } from './geolocate';\nimport { svgOsmose } from './osmose';\nimport { svgStreetside } from './streetside';\nimport { svgVegbilder} from './vegbilder';\nimport { svgMapillaryImages } from './mapillary_images';\nimport { svgMapillaryPosition } from './mapillary_position';\nimport { svgMapillarySigns } from './mapillary_signs';\nimport { svgMapillaryMapFeatures } from './mapillary_map_features';\nimport { svgKartaviewImages } from './kartaview_images';\nimport { svgMapilioImages } from './mapilio_images';\nimport { svgPanoramaxImages } from './panoramax_images';\nimport { svgOsm } from './osm';\nimport { svgNotes } from './notes';\nimport { svgTouch } from './touch';\nimport { utilArrayDifference, utilRebind } from '../util';\nimport { utilGetDimensions, utilSetDimensions } from '../util/dimensions';\n\n\nexport function svgLayers(projection, context) {\n var dispatch = d3_dispatch('change', 'photoDatesChanged');\n var svg = d3_select(null);\n var _layers = [\n { id: 'osm', layer: svgOsm(projection, context, dispatch) },\n { id: 'notes', layer: svgNotes(projection, context, dispatch) },\n { id: 'data', layer: svgData(projection, context, dispatch) },\n { id: 'osmose', layer: svgOsmose(projection, context, dispatch) },\n { id: 'streetside', layer: svgStreetside(projection, context, dispatch)},\n { id: 'mapillary', layer: svgMapillaryImages(projection, context, dispatch) },\n { id: 'mapillary-position', layer: svgMapillaryPosition(projection, context, dispatch) },\n { id: 'mapillary-map-features', layer: svgMapillaryMapFeatures(projection, context, dispatch) },\n { id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) },\n { id: 'kartaview', layer: svgKartaviewImages(projection, context, dispatch) },\n { id: 'mapilio', layer: svgMapilioImages(projection, context, dispatch) },\n { id: 'vegbilder', layer: svgVegbilder(projection, context, dispatch) },\n { id: 'panoramax', layer: svgPanoramaxImages(projection, context, dispatch) },\n { id: 'local-photos', layer: svgLocalPhotos(projection, context, dispatch) },\n { id: 'debug', layer: svgDebug(projection, context, dispatch) },\n { id: 'geolocate', layer: svgGeolocate(projection, context, dispatch) },\n { id: 'touch', layer: svgTouch(projection, context, dispatch) },\n ];\n\n\n function drawLayers(selection) {\n svg = selection.selectAll('.surface')\n .data([0]);\n\n svg = svg.enter()\n .append('svg')\n .attr('class', 'surface')\n .merge(svg);\n\n var defs = svg.selectAll('.surface-defs')\n .data([0]);\n\n defs.enter()\n .append('defs')\n .attr('class', 'surface-defs');\n\n var groups = svg.selectAll('.data-layer')\n .data(_layers);\n\n groups.exit()\n .remove();\n\n groups.enter()\n .append('g')\n .attr('class', function(d) { return 'data-layer ' + d.id; })\n .merge(groups)\n .each(function(d) { d3_select(this).call(d.layer); });\n }\n\n\n drawLayers.all = function() {\n return _layers;\n };\n\n\n drawLayers.layer = function(id) {\n var obj = _layers.find(function(o) { return o.id === id; });\n return obj && obj.layer;\n };\n\n\n drawLayers.only = function(what) {\n var arr = [].concat(what);\n var all = _layers.map(function(layer) { return layer.id; });\n return drawLayers.remove(utilArrayDifference(all, arr));\n };\n\n\n drawLayers.remove = function(what) {\n var arr = [].concat(what);\n arr.forEach(function(id) {\n _layers = _layers.filter(function(o) { return o.id !== id; });\n });\n dispatch.call('change');\n return this;\n };\n\n\n drawLayers.add = function(what) {\n var arr = [].concat(what);\n arr.forEach(function(obj) {\n if ('id' in obj && 'layer' in obj) {\n _layers.push(obj);\n }\n });\n dispatch.call('change');\n return this;\n };\n\n\n drawLayers.dimensions = function(val) {\n if (!arguments.length) return utilGetDimensions(svg);\n utilSetDimensions(svg, val);\n return this;\n };\n\n\n return utilRebind(drawLayers, dispatch, 'on');\n}\n", "import { deepEqual } from 'fast-equals';\nimport { range as d3_range } from 'd3-array';\n\nimport {\n svgMarkerSegments, svgPath, svgRelationMemberTags, svgSegmentWay\n} from './helpers';\nimport { svgTagClasses } from './tag_classes';\n\nimport { osmEntity } from '../osm';\nimport { utilArrayFlatten, utilArrayGroupBy } from '../util';\nimport { utilDetect } from '../util/detect';\n\n/** @param {{ [key: string ]: string }} tags */\nfunction onewayArrowColour(tags) {\n // the return value must be defined in ./defs.js\n if (tags.highway === 'construction' && tags.bridge) return 'white';\n if (tags.highway === 'pedestrian') return 'gray';\n if (tags.railway && !tags.highway) return 'gray';\n if (tags.aeroway === 'runway') return 'white';\n\n return 'black';\n}\n\nexport function svgLines(projection, context) {\n var detected = utilDetect();\n\n var highway_stack = {\n motorway: 0,\n motorway_link: 1,\n trunk: 2,\n trunk_link: 3,\n primary: 4,\n primary_link: 5,\n secondary: 6,\n tertiary: 7,\n unclassified: 8,\n residential: 9,\n service: 10,\n busway: 11,\n footway: 12\n };\n\n\n function drawTargets(selection, graph, entities, filter) {\n var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';\n var getPath = svgPath(projection).geojson;\n var activeID = context.activeID();\n var base = context.history().base();\n\n // The targets and nopes will be MultiLineString sub-segments of the ways\n var data = { targets: [], nopes: [] };\n\n entities.forEach(function(way) {\n var features = svgSegmentWay(way, graph, activeID);\n data.targets.push.apply(data.targets, features.passive);\n data.nopes.push.apply(data.nopes, features.active);\n });\n\n\n // Targets allow hover and vertex snapping\n var targetData = data.targets.filter(getPath);\n var targets = selection.selectAll('.line.target-allowed')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(targetData, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n var segmentWasEdited = function(d) {\n var wayID = d.properties.entity.id;\n // if the whole line was edited, don't draw segment changes\n if (!base.entities[wayID] ||\n !deepEqual(graph.entities[wayID].nodes, base.entities[wayID].nodes)) {\n return false;\n }\n return d.properties.nodes.some(function(n) {\n return !base.entities[n.id] ||\n !deepEqual(graph.entities[n.id].loc, base.entities[n.id].loc);\n });\n };\n\n // enter/update\n targets.enter()\n .append('path')\n .merge(targets)\n .attr('d', getPath)\n .attr('class', function(d) {\n return 'way line target target-allowed ' + targetClass + d.id;\n })\n .classed('segment-edited', segmentWasEdited);\n\n // NOPE\n var nopeData = data.nopes.filter(getPath);\n var nopes = selection.selectAll('.line.target-nope')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(nopeData, function key(d) { return d.id; });\n\n // exit\n nopes.exit()\n .remove();\n\n // enter/update\n nopes.enter()\n .append('path')\n .merge(nopes)\n .attr('d', getPath)\n .attr('class', function(d) {\n return 'way line target target-nope ' + nopeClass + d.id;\n })\n .classed('segment-edited', segmentWasEdited);\n }\n\n\n function drawLines(selection, graph, entities, filter) {\n var base = context.history().base();\n\n function waystack(a, b) {\n var selected = context.selectedIDs();\n var scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0;\n var scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0;\n\n if (a.tags.highway) { scoreA -= highway_stack[a.tags.highway]; }\n if (b.tags.highway) { scoreB -= highway_stack[b.tags.highway]; }\n return scoreA - scoreB;\n }\n\n\n function drawLineGroup(selection, klass, isSelected) {\n // Note: Don't add `.selected` class in draw modes\n var mode = context.mode();\n var isDrawing = mode && /^draw/.test(mode.id);\n var selectedClass = (!isDrawing && isSelected) ? 'selected ' : '';\n\n var lines = selection\n .selectAll('path')\n .filter(filter)\n .data(getPathData(isSelected), osmEntity.key);\n\n lines.exit()\n .remove();\n\n // Optimization: Call expensive TagClasses only on enter selection. This\n // works because osmEntity.key is defined to include the entity v attribute.\n lines.enter()\n .append('path')\n .attr('class', function(d) {\n\n var prefix = 'way line';\n\n // if this line isn't styled by its own tags\n if (!d.hasInterestingTags()) {\n\n var parentRelations = graph.parentRelations(d);\n var parentMultipolygons = parentRelations.filter(function(relation) {\n return relation.isMultipolygon();\n });\n\n // and if it's a member of at least one multipolygon relation\n if (parentMultipolygons.length > 0 &&\n // and only multipolygon relations\n parentRelations.length === parentMultipolygons.length) {\n // then fudge the classes to style this as an area edge\n prefix = 'relation area';\n }\n }\n\n var oldMPClass = oldMultiPolygonOuters[d.id] ? 'old-multipolygon ' : '';\n return prefix + ' ' + klass + ' ' + selectedClass + oldMPClass + d.id;\n })\n .classed('added', function(d) {\n return !base.entities[d.id];\n })\n .classed('geometry-edited', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].nodes, base.entities[d.id].nodes);\n })\n .classed('retagged', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses())\n .merge(lines)\n .sort(waystack)\n .attr('d', getPath)\n .call(svgTagClasses().tags(svgRelationMemberTags(graph)));\n\n return selection;\n }\n\n\n function getPathData(isSelected) {\n return function() {\n var layer = this.parentNode.__data__;\n var data = pathdata[layer] || [];\n return data.filter(function(d) {\n if (isSelected) {\n return context.selectedIDs().indexOf(d.id) !== -1;\n } else {\n return context.selectedIDs().indexOf(d.id) === -1;\n }\n });\n };\n }\n\n function addMarkers(layergroup, pathclass, groupclass, groupdata, marker) {\n var markergroup = layergroup\n .selectAll('g.' + groupclass)\n .data([pathclass]);\n\n markergroup = markergroup.enter()\n .append('g')\n .attr('class', groupclass)\n .merge(markergroup);\n\n var markers = markergroup\n .selectAll('path')\n .filter(filter)\n .data(\n function data() { return groupdata[this.parentNode.__data__] || []; },\n function key(d) { return [d.id, d.index]; }\n );\n\n markers.exit()\n .remove();\n\n markers = markers.enter()\n .append('path')\n .attr('class', pathclass)\n .merge(markers)\n .attr('marker-mid', marker)\n .attr('d', function(d) { return d.d; });\n\n if (detected.ie) {\n markers.each(function() { this.parentNode.insertBefore(this, this); });\n }\n }\n\n\n var getPath = svgPath(projection, graph);\n var ways = [];\n var onewaydata = {};\n var sideddata = {};\n var oldMultiPolygonOuters = {};\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n if (entity.geometry(graph) === 'line'\n // to render side-markers for coastlines (see\n // https://github.com/openstreetmap/iD/issues/9293)\n || entity.geometry(graph) === 'area' && entity.sidednessIdentifier\n && entity.sidednessIdentifier() === 'coastline') {\n ways.push(entity);\n }\n }\n\n ways = ways.filter(getPath);\n const pathdata = utilArrayGroupBy(ways, (way) => Math.trunc(way.layer()));\n\n Object.keys(pathdata).forEach(function(k) {\n var v = pathdata[k];\n var onewayArr = v.filter(function(d) { return d.isOneWay(); });\n var onewaySegments = svgMarkerSegments(\n projection, graph, 36,\n entity => entity.isOneWayBackwards(),\n entity => entity.isBiDirectional(),\n );\n onewaydata[k] = utilArrayFlatten(onewayArr.map(onewaySegments));\n\n var sidedArr = v.filter(function(d) { return d.isSided(); });\n var sidedSegments = svgMarkerSegments(\n projection, graph, 30\n );\n sideddata[k] = utilArrayFlatten(sidedArr.map(sidedSegments));\n });\n\n\n var covered = selection.selectAll('.layer-osm.covered'); // under areas\n var uncovered = selection.selectAll('.layer-osm.lines'); // over areas\n var touchLayer = selection.selectAll('.layer-touch.lines');\n\n // Draw lines..\n [covered, uncovered].forEach(function(selection) {\n var range = (selection === covered ? d3_range(-10,0) : d3_range(0,11));\n var layergroup = selection\n .selectAll('g.layergroup')\n .data(range);\n\n layergroup = layergroup.enter()\n .append('g')\n .attr('class', function(d) { return 'layergroup layer' + String(d); })\n .merge(layergroup);\n\n layergroup\n .selectAll('g.linegroup')\n .data(['shadow', 'casing', 'stroke', 'shadow-highlighted', 'casing-highlighted', 'stroke-highlighted'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'linegroup line-' + d; });\n\n layergroup.selectAll('g.line-shadow')\n .call(drawLineGroup, 'shadow', false);\n layergroup.selectAll('g.line-casing')\n .call(drawLineGroup, 'casing', false);\n layergroup.selectAll('g.line-stroke')\n .call(drawLineGroup, 'stroke', false);\n\n layergroup.selectAll('g.line-shadow-highlighted')\n .call(drawLineGroup, 'shadow', true);\n layergroup.selectAll('g.line-casing-highlighted')\n .call(drawLineGroup, 'casing', true);\n layergroup.selectAll('g.line-stroke-highlighted')\n .call(drawLineGroup, 'stroke', true);\n\n addMarkers(layergroup, 'oneway', 'onewaygroup', onewaydata, (d) => {\n const category = onewayArrowColour(graph.entity(d.id).tags);\n return `url(#ideditor-oneway-marker-${category})`;\n });\n addMarkers(layergroup, 'sided', 'sidedgroup', sideddata,\n function marker(d) {\n var category = graph.entity(d.id).sidednessIdentifier();\n return 'url(#ideditor-sided-marker-' + category + ')';\n }\n );\n });\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, ways, filter);\n }\n\n\n return drawLines;\n}\n", "import { svgPointTransform } from './helpers';\nimport { svgTagClasses } from './tag_classes';\nimport { geoAngle, geoLineIntersection, geoVecInterp, geoVecLength } from '../geo';\n\n\nexport function svgMidpoints(projection, context) {\n var targetRadius = 8;\n\n function drawTargets(selection, graph, entities, filter) {\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var getTransform = svgPointTransform(projection).geojson;\n\n var data = entities.map(function(midpoint) {\n return {\n type: 'Feature',\n id: midpoint.id,\n properties: {\n target: true,\n entity: midpoint\n },\n geometry: {\n type: 'Point',\n coordinates: midpoint.loc\n }\n };\n });\n\n var targets = selection.selectAll('.midpoint.target')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(data, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('circle')\n .attr('r', targetRadius)\n .merge(targets)\n .attr('class', function(d) { return 'node midpoint target ' + fillClass + d.id; })\n .attr('transform', getTransform);\n }\n\n\n function drawMidpoints(selection, graph, entities, filter, extent) {\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.midpoints');\n var touchLayer = selection.selectAll('.layer-touch.points');\n\n var mode = context.mode();\n if ((mode && mode.id !== 'select') || !context.map().withinEditableZoom()) {\n drawLayer.selectAll('.midpoint').remove();\n touchLayer.selectAll('.midpoint.target').remove();\n return;\n }\n\n var poly = extent.polygon();\n var midpoints = {};\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n\n if (entity.type !== 'way') continue;\n if (!filter(entity)) continue;\n if (context.selectedIDs().indexOf(entity.id) < 0) continue;\n\n var nodes = graph.childNodes(entity);\n for (var j = 0; j < nodes.length - 1; j++) {\n var a = nodes[j];\n var b = nodes[j + 1];\n var id = [a.id, b.id].sort().join('-');\n\n if (midpoints[id]) {\n midpoints[id].parents.push(entity);\n } else if (geoVecLength(projection(a.loc), projection(b.loc)) > 40) {\n var point = geoVecInterp(a.loc, b.loc, 0.5);\n var loc = null;\n\n if (extent.intersects(point)) {\n loc = point;\n } else {\n for (var k = 0; k < 4; k++) {\n point = geoLineIntersection([a.loc, b.loc], [poly[k], poly[k + 1]]);\n if (point &&\n geoVecLength(projection(a.loc), projection(point)) > 20 &&\n geoVecLength(projection(b.loc), projection(point)) > 20) {\n loc = point;\n break;\n }\n }\n }\n\n if (loc) {\n midpoints[id] = {\n type: 'midpoint',\n id: id,\n loc: loc,\n edge: [a.id, b.id],\n parents: [entity]\n };\n }\n }\n }\n }\n\n\n function midpointFilter(d) {\n if (midpoints[d.id]) return true;\n\n for (var i = 0; i < d.parents.length; i++) {\n if (filter(d.parents[i])) {\n return true;\n }\n }\n\n return false;\n }\n\n\n var groups = drawLayer.selectAll('.midpoint')\n .filter(midpointFilter)\n .data(Object.values(midpoints), function(d) { return d.id; });\n\n groups.exit()\n .remove();\n\n var enter = groups.enter()\n .insert('g', ':first-child')\n .attr('class', 'midpoint');\n\n enter\n .append('polygon')\n .attr('points', '-6,8 10,0 -6,-8')\n .attr('class', 'shadow');\n\n enter\n .append('polygon')\n .attr('points', '-3,4 5,0 -3,-4')\n .attr('class', 'fill');\n\n groups = groups\n .merge(enter)\n .attr('transform', function(d) {\n var translate = svgPointTransform(projection);\n var a = graph.entity(d.edge[0]);\n var b = graph.entity(d.edge[1]);\n var angle = geoAngle(a, b, projection) * (180 / Math.PI);\n return translate(d) + ' rotate(' + angle + ')';\n })\n .call(svgTagClasses().tags(\n function(d) { return d.parents[0].tags; }\n ));\n\n // Propagate data bindings.\n groups.select('polygon.shadow');\n groups.select('polygon.fill');\n\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, Object.values(midpoints), midpointFilter);\n }\n\n return drawMidpoints;\n}\n", "import { deepEqual } from 'fast-equals';\nimport { clamp } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3';\n\nimport { geoScaleToZoom } from '../geo';\nimport { osmEntity } from '../osm';\nimport { svgPointTransform } from './helpers';\nimport { svgTagClasses } from './tag_classes';\nimport { presetManager } from '../presets';\nimport { textWidth, isAddressPoint } from './labels';\n\nexport function svgPoints(projection, context) {\n\n function markerPath(selection, klass) {\n selection\n .attr('class', klass)\n .attr('transform', d => isAddressPoint(d.tags)\n ? `translate(-${addressShieldWidth(d, selection)/2}, -8)`\n : 'translate(-8, -23)')\n .attr('d', d => {\n if (!isAddressPoint(d.tags)) {\n return 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z';\n }\n const w = addressShieldWidth(d, selection);\n return `M ${w-3.5} 0 a 3.5 3.5 0 0 0 3.5 3.5 l 0 9 a 3.5 3.5 0 0 0 -3.5 3.5 l -${w-7} 0 a 3.5 3.5 0 0 0 -3.5 -3.5 l 0 -9 a 3.5 3.5 0 0 0 3.5 -3.5 z`;\n });\n }\n\n function sortY(a, b) {\n return b.loc[1] - a.loc[1];\n }\n\n function addressShieldWidth(d, selection) {\n const width = textWidth(d.tags['addr:housenumber'] || d.tags['addr:housename'] || '', 10, selection.node().parentElement);\n return clamp(width, 10, 34) + 8;\n };\n\n // Avoid exit/enter if we're just moving stuff around.\n // The node will get a new version but we only need to run the update selection.\n function fastEntityKey(d) {\n const mode = context.mode();\n const isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);\n return isMoving ? d.id : osmEntity.key(d);\n }\n\n\n function drawTargets(selection, graph, entities, filter) {\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var getTransform = svgPointTransform(projection).geojson;\n var activeID = context.activeID();\n var data = [];\n\n entities.forEach(function(node) {\n if (activeID === node.id) return; // draw no target on the activeID\n\n data.push({\n type: 'Feature',\n id: node.id,\n properties: {\n target: true,\n entity: node,\n isAddr: isAddressPoint(node.tags)\n },\n geometry: node.asGeoJSON()\n });\n });\n\n var targets = selection.selectAll('.point.target')\n .filter(d => filter(d.properties.entity))\n .data(data, d => fastEntityKey(d.properties.entity));\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('rect')\n .attr('x', d => d.properties.isAddr ? -addressShieldWidth(d.properties.entity, selection) / 2 : -10)\n .attr('y', d => d.properties.isAddr ? -8 : -26)\n .attr('width', d => d.properties.isAddr ? addressShieldWidth(d.properties.entity, selection) : 20)\n .attr('height', d => d.properties.isAddr ? 16 : 30)\n .attr('class', function(d) { return 'node point target ' + fillClass + d.id; })\n .merge(targets)\n .attr('transform', getTransform);\n }\n\n\n function drawPoints(selection, graph, entities, filter) {\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n var base = context.history().base();\n\n // Points with a direction will render as vertices at higher zooms..\n function renderAsPoint(entity) {\n return entity.geometry(graph) === 'point' &&\n !(zoom >= 18 && entity.directions(graph, projection).length);\n }\n\n // All points will render as vertices in wireframe mode too..\n var points = wireframe ? [] : entities.filter(renderAsPoint);\n points.sort(sortY);\n\n\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.points');\n var touchLayer = selection.selectAll('.layer-touch.points');\n\n // Draw points..\n var groups = drawLayer.selectAll('g.point')\n .filter(filter)\n .data(points, fastEntityKey);\n\n groups.exit()\n .remove();\n\n var enter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'node point ' + d.id; })\n .order();\n\n enter\n .append('path')\n .call(markerPath, 'shadow');\n\n enter.each(function(d) {\n if (isAddressPoint(d.tags)) return;\n d3_select(this)\n .append('ellipse')\n .attr('cx', 0.5)\n .attr('cy', 1)\n .attr('rx', 6.5)\n .attr('ry', 3)\n .attr('class', 'stroke');\n });\n\n enter\n .append('path')\n .call(markerPath, 'stroke');\n\n enter\n .append('use')\n .attr('transform', 'translate(-5.5, -20)')\n .attr('class', 'icon')\n .attr('width', '12px')\n .attr('height', '12px');\n\n groups = groups\n .merge(enter)\n .attr('transform', svgPointTransform(projection))\n .classed('added', function(d) {\n return !base.entities[d.id]; // if it doesn't exist in the base graph, it's new\n })\n .classed('moved', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].loc, base.entities[d.id].loc);\n })\n .classed('retagged', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses());\n\n groups.select('.shadow'); // propagate bound data\n groups.select('.stroke'); // propagate bound data\n groups.select('.icon') // propagate bound data\n .attr('xlink:href', function(entity) {\n var preset = presetManager.match(entity, graph);\n var picon = preset && preset.icon;\n return picon ? '#' + picon : '';\n });\n\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, points, filter);\n }\n\n\n return drawPoints;\n}\n", "import { geoAngle, geoPathLength } from '../geo';\n\n\nexport function svgTurns(projection, context) {\n\n function icon(turn) {\n var u = turn.u ? '-u' : '';\n if (turn.no) return '#iD-turn-no' + u;\n if (turn.only) return '#iD-turn-only' + u;\n return '#iD-turn-yes' + u;\n }\n\n function drawTurns(selection, graph, turns) {\n\n function turnTransform(d) {\n var pxRadius = 50;\n var toWay = graph.entity(d.to.way);\n var toPoints = graph.childNodes(toWay)\n .map(function (n) { return n.loc; })\n .map(projection);\n var toLength = geoPathLength(toPoints);\n var mid = toLength / 2; // midpoint of destination way\n\n var toNode = graph.entity(d.to.node);\n var toVertex = graph.entity(d.to.vertex);\n var a = geoAngle(toVertex, toNode, projection);\n var o = projection(toVertex.loc);\n var r = d.u ? 0 // u-turn: no radius\n : !toWay.__via ? pxRadius // leaf way: put marker at pxRadius\n : Math.min(mid, pxRadius); // via way: prefer pxRadius, fallback to mid for very short ways\n\n return 'translate(' + (r * Math.cos(a) + o[0]) + ',' + (r * Math.sin(a) + o[1]) + ') ' +\n 'rotate(' + a * 180 / Math.PI + ')';\n }\n\n\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.turns');\n var touchLayer = selection.selectAll('.layer-touch.turns');\n\n // Draw turns..\n var groups = drawLayer.selectAll('g.turn')\n .data(turns, function(d) { return d.key; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var groupsEnter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'turn ' + d.key; });\n\n var turnsEnter = groupsEnter\n .filter(function(d) { return !d.u; });\n\n turnsEnter.append('rect')\n .attr('transform', 'translate(-22, -12)')\n .attr('width', '44')\n .attr('height', '24');\n\n turnsEnter.append('use')\n .attr('transform', 'translate(-22, -12)')\n .attr('width', '44')\n .attr('height', '24');\n\n var uEnter = groupsEnter\n .filter(function(d) { return d.u; });\n\n uEnter.append('circle')\n .attr('r', '16');\n\n uEnter.append('use')\n .attr('transform', 'translate(-16, -16)')\n .attr('width', '32')\n .attr('height', '32');\n\n // update\n groups = groups\n .merge(groupsEnter)\n .attr('opacity', function(d) { return d.direct === false ? '0.7' : null; })\n .attr('transform', turnTransform);\n\n groups.select('use')\n .attr('xlink:href', icon);\n\n groups.select('rect'); // propagate bound data\n groups.select('circle'); // propagate bound data\n\n\n // Draw touch targets..\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n groups = touchLayer.selectAll('g.turn')\n .data(turns, function(d) { return d.key; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n groupsEnter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'turn ' + d.key; });\n\n turnsEnter = groupsEnter\n .filter(function(d) { return !d.u; });\n\n turnsEnter.append('rect')\n .attr('class', 'target ' + fillClass)\n .attr('transform', 'translate(-22, -12)')\n .attr('width', '44')\n .attr('height', '24');\n\n uEnter = groupsEnter\n .filter(function(d) { return d.u; });\n\n uEnter.append('circle')\n .attr('class', 'target ' + fillClass)\n .attr('r', '16');\n\n // update\n groups = groups\n .merge(groupsEnter)\n .attr('transform', turnTransform);\n\n groups.select('rect'); // propagate bound data\n groups.select('circle'); // propagate bound data\n\n\n return this;\n }\n\n return drawTurns;\n}\n", "import { deepEqual } from 'fast-equals';\nimport { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { geoScaleToZoom } from '../geo';\nimport { osmEntity } from '../osm';\nimport { svgPassiveVertex, svgPointTransform } from './helpers';\nimport { svgTagClasses } from './tag_classes';\n\nexport function svgVertices(projection, context) {\n var radiuses = {\n // z16-, z17, z18+, w/icon\n shadow: [6, 7.5, 7.5, 12],\n stroke: [2.5, 3.5, 3.5, 8],\n fill: [1, 1.5, 1.5, 1.5]\n };\n\n var _currHoverTarget;\n var _currPersistent = {};\n var _currHover = {};\n var _prevHover = {};\n var _currSelected = {};\n var _prevSelected = {};\n var _radii = {};\n\n\n function sortY(a, b) {\n return b.loc[1] - a.loc[1];\n }\n\n // Avoid exit/enter if we're just moving stuff around.\n // The node will get a new version but we only need to run the update selection.\n function fastEntityKey(d) {\n var mode = context.mode();\n var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);\n return isMoving ? d.id : osmEntity.key(d);\n }\n\n\n function draw(selection, graph, vertices, sets, filter) {\n sets = sets || { selected: {}, important: {}, hovered: {} };\n\n var icons = {};\n var directions = {};\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2);\n var activeID = context.activeID();\n var base = context.history().base();\n\n\n function getIcon(d) {\n // always check latest entity, as fastEntityKey avoids enter/exit now\n var entity = graph.entity(d.id);\n if (entity.id in icons) return icons[entity.id];\n\n icons[entity.id] =\n entity.hasInterestingTags() &&\n presetManager.match(entity, graph).icon;\n\n return icons[entity.id];\n }\n\n\n // memoize directions results, return false for empty arrays (for use in filter)\n function getDirections(entity) {\n if (entity.id in directions) return directions[entity.id];\n\n var angles = entity.directions(graph, projection);\n directions[entity.id] = angles.length ? angles : false;\n return angles;\n }\n\n\n function updateAttributes(selection) {\n ['shadow', 'stroke', 'fill'].forEach(function(klass) {\n var rads = radiuses[klass];\n selection.selectAll('.' + klass)\n .each(function(entity) {\n var i = z && getIcon(entity);\n var r = rads[i ? 3 : z];\n\n // slightly increase the size of unconnected endpoints #3775\n if (entity.id !== activeID && entity.isEndpoint(graph) && !entity.isConnected(graph)) {\n r += 1.5;\n }\n\n if (klass === 'shadow') { // remember this value, so we don't need to\n _radii[entity.id] = r; // recompute it when we draw the touch targets\n }\n\n d3_select(this)\n .attr('r', r)\n .attr('visibility', (i && klass === 'fill') ? 'hidden' : null);\n });\n });\n }\n\n vertices.sort(sortY);\n\n var groups = selection.selectAll('g.vertex')\n .filter(filter)\n .data(vertices, fastEntityKey);\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var enter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'node vertex ' + d.id; })\n .order();\n\n enter\n .append('circle')\n .attr('class', 'shadow');\n\n enter\n .append('circle')\n .attr('class', 'stroke');\n\n // Vertices with tags get a fill.\n enter.filter(function(d) { return d.hasInterestingTags(); })\n .append('circle')\n .attr('class', 'fill');\n\n // update\n groups = groups\n .merge(enter)\n .attr('transform', svgPointTransform(projection))\n .classed('sibling', function(d) { return d.id in sets.selected; })\n .classed('shared', function(d) { return graph.isShared(d); })\n .classed('endpoint', function(d) { return d.isEndpoint(graph); })\n .classed('added', function(d) {\n return !base.entities[d.id]; // if it doesn't exist in the base graph, it's new\n })\n .classed('moved', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].loc, base.entities[d.id].loc);\n })\n .classed('retagged', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses())\n .call(updateAttributes);\n\n // Vertices with icons get a `use`.\n var iconUse = groups\n .selectAll('.icon')\n .data(function data(d) { return zoom >= 17 && getIcon(d) ? [d] : []; }, fastEntityKey);\n\n // exit\n iconUse.exit()\n .remove();\n\n // enter\n iconUse.enter()\n .append('use')\n .attr('class', 'icon')\n .attr('width', '12px')\n .attr('height', '12px')\n .attr('transform', 'translate(-6, -6)')\n .attr('xlink:href', function(d) {\n var picon = getIcon(d);\n return picon ? '#' + picon : '';\n });\n\n\n // Vertices with directions get viewfields\n var dgroups = groups\n .selectAll('.viewfieldgroup')\n .data(function data(d) { return zoom >= 18 && getDirections(d) ? [d] : []; }, fastEntityKey);\n\n // exit\n dgroups.exit()\n .remove();\n\n // enter/update\n dgroups = dgroups.enter()\n .insert('g', '.shadow')\n .attr('class', 'viewfieldgroup')\n .merge(dgroups);\n\n var viewfields = dgroups.selectAll('.viewfield')\n .data(getDirections, function key(d) { return osmEntity.key(d); });\n\n // exit\n viewfields.exit()\n .remove();\n\n // enter/update\n viewfields.enter()\n .append('path')\n .attr('class', 'viewfield')\n .attr('d', 'M0,0H0')\n .merge(viewfields)\n .attr('marker-start', d => 'url(#ideditor-viewfield-marker' + (d.type === 'side' ? '-side' : '') + (wireframe ? '-wireframe' : '') + ')')\n .attr('transform', d => `rotate(${d.angle})`);\n }\n\n\n function drawTargets(selection, graph, entities, filter) {\n var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';\n var getTransform = svgPointTransform(projection).geojson;\n var activeID = context.activeID();\n var data = { targets: [], nopes: [] };\n\n entities.forEach(function(node) {\n if (activeID === node.id) return; // draw no target on the activeID\n\n var vertexType = svgPassiveVertex(node, graph, activeID);\n if (vertexType !== 0) { // passive or adjacent - allow to connect\n data.targets.push({\n type: 'Feature',\n id: node.id,\n properties: {\n target: true,\n entity: node\n },\n geometry: node.asGeoJSON()\n });\n } else {\n data.nopes.push({\n type: 'Feature',\n id: node.id + '-nope',\n properties: {\n nope: true,\n target: true,\n entity: node\n },\n geometry: node.asGeoJSON()\n });\n }\n });\n\n // Targets allow hover and vertex snapping\n var targets = selection.selectAll('.vertex.target-allowed')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(data.targets, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('circle')\n .attr('r', function(d) {\n return _radii[d.id]\n || radiuses.shadow[3];\n })\n .merge(targets)\n .attr('class', function(d) {\n return 'node vertex target target-allowed '\n + targetClass + d.id;\n })\n .attr('transform', getTransform);\n\n\n // NOPE\n var nopes = selection.selectAll('.vertex.target-nope')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(data.nopes, function key(d) { return d.id; });\n\n // exit\n nopes.exit()\n .remove();\n\n // enter/update\n nopes.enter()\n .append('circle')\n .attr('r', function(d) { return (_radii[d.properties.entity.id] || radiuses.shadow[3]); })\n .merge(nopes)\n .attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; })\n .attr('transform', getTransform);\n }\n\n\n // Points can also render as vertices:\n // 1. in wireframe mode or\n // 2. at higher zooms if they have a direction\n function renderAsVertex(entity, graph, wireframe, zoom) {\n var geometry = entity.geometry(graph);\n return geometry === 'vertex' || (geometry === 'point' && (\n wireframe || (zoom >= 18 && entity.directions(graph, projection).length)\n ));\n }\n\n\n function isEditedNode(node, base, head) {\n var baseNode = base.entities[node.id];\n var headNode = head.entities[node.id];\n return !headNode ||\n !baseNode ||\n !deepEqual(headNode.tags, baseNode.tags) ||\n !deepEqual(headNode.loc, baseNode.loc);\n }\n\n\n function getSiblingAndChildVertices(ids, graph, wireframe, zoom) {\n var results = {};\n\n var seenIds = {};\n\n function addChildVertices(entity) {\n\n // avoid redundant work and infinite recursion of circular relations\n if (seenIds[entity.id]) return;\n seenIds[entity.id] = true;\n\n var geometry = entity.geometry(graph);\n if (!context.features().isHiddenFeature(entity, graph, geometry)) {\n var i;\n if (entity.type === 'way') {\n for (i = 0; i < entity.nodes.length; i++) {\n var child = graph.hasEntity(entity.nodes[i]);\n if (child) {\n addChildVertices(child);\n }\n }\n } else if (entity.type === 'relation') {\n for (i = 0; i < entity.members.length; i++) {\n var member = graph.hasEntity(entity.members[i].id);\n if (member) {\n addChildVertices(member);\n }\n }\n } else if (renderAsVertex(entity, graph, wireframe, zoom)) {\n results[entity.id] = entity;\n }\n }\n }\n\n ids.forEach(function(id) {\n var entity = graph.hasEntity(id);\n if (!entity) return;\n\n if (entity.type === 'node') {\n if (renderAsVertex(entity, graph, wireframe, zoom)) {\n results[entity.id] = entity;\n graph.parentWays(entity).forEach(function(entity) {\n addChildVertices(entity);\n });\n }\n } else { // way, relation\n addChildVertices(entity);\n }\n });\n\n return results;\n }\n\n\n function drawVertices(selection, graph, entities, filter, extent, fullRedraw) {\n var wireframe = context.surface().classed('fill-wireframe');\n var visualDiff = context.surface().classed('highlight-edited');\n var zoom = geoScaleToZoom(projection.scale());\n var mode = context.mode();\n var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);\n var base = context.history().base();\n\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.vertices');\n var touchLayer = selection.selectAll('.layer-touch.points');\n\n if (fullRedraw) {\n _currPersistent = {};\n _radii = {};\n }\n\n // Collect important vertices from the `entities` list..\n // (during a partial redraw, it will not contain everything)\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n var geometry = entity.geometry(graph);\n var keep = false;\n\n // a point that looks like a vertex..\n if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) {\n _currPersistent[entity.id] = entity;\n keep = true;\n\n // a vertex of some importance..\n } else if (geometry === 'vertex' &&\n (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)\n || (visualDiff && isEditedNode(entity, base, graph)))) {\n _currPersistent[entity.id] = entity;\n keep = true;\n }\n\n // whatever this is, it's not a persistent vertex..\n if (!keep && !fullRedraw) {\n delete _currPersistent[entity.id];\n }\n }\n\n // 3 sets of vertices to consider:\n var sets = {\n persistent: _currPersistent, // persistent = important vertices (render always)\n selected: _currSelected, // selected + siblings of selected (render always)\n hovered: _currHover // hovered + siblings of hovered (render only in draw modes)\n };\n\n var all = Object.assign({}, (isMoving ? _currHover : {}), _currSelected, _currPersistent);\n\n // Draw the vertices..\n // The filter function controls the scope of what objects d3 will touch (exit/enter/update)\n // Adjust the filter function to expand the scope beyond whatever entities were passed in.\n var filterRendered = function(d) {\n return d.id in _currPersistent || d.id in _currSelected || d.id in _currHover || filter(d);\n };\n drawLayer\n .call(draw, graph, currentVisible(all), sets, filterRendered);\n\n // Draw touch targets..\n // When drawing, render all targets (not just those affected by a partial redraw)\n var filterTouch = function(d) {\n return isMoving ? true : filterRendered(d);\n };\n touchLayer\n .call(drawTargets, graph, currentVisible(all), filterTouch);\n\n\n function currentVisible(which) {\n return Object.keys(which)\n .map(graph.hasEntity, graph) // the current version of this entity\n .filter(function (entity) { return entity && entity.intersects(extent, graph); });\n }\n }\n\n\n // partial redraw - only update the selected items..\n drawVertices.drawSelected = function(selection, graph, extent) {\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n\n _prevSelected = _currSelected || {};\n if (context.map().isInWideSelection()) {\n _currSelected = {};\n context.selectedIDs().forEach(function(id) {\n var entity = graph.hasEntity(id);\n if (!entity) return;\n\n if (entity.type === 'node') {\n if (renderAsVertex(entity, graph, wireframe, zoom)) {\n _currSelected[entity.id] = entity;\n }\n }\n });\n\n } else {\n _currSelected = getSiblingAndChildVertices(context.selectedIDs(), graph, wireframe, zoom);\n }\n\n // note that drawVertices will add `_currSelected` automatically if needed..\n var filter = function(d) { return d.id in _prevSelected; };\n drawVertices(selection, graph, Object.values(_prevSelected), filter, extent, false);\n };\n\n\n // partial redraw - only update the hovered items..\n drawVertices.drawHover = function(selection, graph, target, extent) {\n if (target === _currHoverTarget) return; // continue only if something changed\n\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n\n _prevHover = _currHover || {};\n _currHoverTarget = target;\n var entity = target && target.properties && target.properties.entity;\n\n if (entity) {\n _currHover = getSiblingAndChildVertices([entity.id], graph, wireframe, zoom);\n } else {\n _currHover = {};\n }\n\n // note that drawVertices will add `_currHover` automatically if needed..\n var filter = function(d) { return d.id in _prevHover; };\n drawVertices(selection, graph, Object.values(_prevHover), filter, extent, false);\n };\n\n return drawVertices;\n}\n", "export { svgAreas } from './areas.js';\nexport { svgData } from './data.js';\nexport { svgDebug } from './debug.js';\nexport { svgDefs } from './defs.js';\nexport { svgIcon } from './icon.js';\nexport { svgGeolocate } from './geolocate';\nexport { svgLabels } from './labels.js';\nexport { svgLayers } from './layers.js';\nexport { svgLines } from './lines.js';\nexport { svgMapillaryImages } from './mapillary_images.js';\nexport { svgMapillarySigns } from './mapillary_signs.js';\nexport { svgMidpoints } from './midpoints.js';\nexport { svgNotes } from './notes.js';\nexport { svgMarkerSegments } from './helpers.js';\nexport { svgKartaviewImages } from './kartaview_images.js';\nexport { svgOsm } from './osm.js';\nexport { svgPassiveVertex } from './helpers.js';\nexport { svgPath } from './helpers.js';\nexport { svgPointTransform } from './helpers.js';\nexport { svgPoints } from './points.js';\nexport { svgRelationMemberTags } from './helpers.js';\nexport { svgSegmentWay } from './helpers.js';\nexport { svgStreetside } from './streetside.js';\nexport { svgVegbilder } from './vegbilder';\nexport { svgTagClasses } from './tag_classes.js';\nexport { svgTagPattern } from './tag_pattern.js';\nexport { svgTouch } from './touch.js';\nexport { svgTurns } from './turns.js';\nexport { svgVertices } from './vertices.js';\nexport { svgMapilioImages } from './mapilio_images.js';\nexport { svgPanoramaxImages } from './panoramax_images.js';\n", "import { t } from '../core/localizer';\nimport { actionOrthogonalize } from '../actions/orthogonalize';\nimport { behaviorOperation } from '../behavior/operation';\nimport { utilGetAllNodes } from '../util';\nimport { svgPath } from '../svg';\n\n\nexport function operationOrthogonalize(context, selectedIDs) {\n var _extent;\n var _type;\n var _actions = selectedIDs.map(chooseAction).filter(Boolean);\n var _amount = _actions.length === 1 ? 'single' : 'multiple';\n var _coords = utilGetAllNodes(selectedIDs, context.graph())\n .map(function(n) { return n.loc; });\n\n\n function chooseAction(entityID) {\n\n var entity = context.entity(entityID);\n var geometry = entity.geometry(context.graph());\n\n if (!_extent) {\n _extent = entity.extent(context.graph());\n } else {\n _extent = _extent.extend(entity.extent(context.graph()));\n }\n\n // square a line/area\n if (entity.type === 'way' && new Set(entity.nodes).size > 2 ) {\n if (_type && _type !== 'feature') return null;\n _type = 'feature';\n return actionOrthogonalize(entityID, context.projection);\n\n // square a single vertex\n } else if (geometry === 'vertex') {\n if (_type && _type !== 'corner') return null;\n _type = 'corner';\n var graph = context.graph();\n var parents = graph.parentWays(entity);\n if (parents.length === 1) {\n var way = parents[0];\n if (way.nodes.indexOf(entityID) !== -1) {\n return actionOrthogonalize(way.id, context.projection, entityID);\n }\n }\n }\n\n return null;\n }\n\n\n var operation = function() {\n if (!_actions.length) return;\n\n var combinedAction = function(graph, t) {\n _actions.forEach(function(action) {\n if (!action.disabled(graph)) {\n graph = action(graph, t);\n }\n });\n return graph;\n };\n combinedAction.transitionable = true;\n\n context.perform(combinedAction, operation.annotation());\n\n window.setTimeout(function() {\n context.validator().validate();\n }, 300); // after any transition\n };\n\n\n operation.available = function() {\n return _actions.length && selectedIDs.length === _actions.length;\n };\n\n\n // don't cache this because the visible extent could change\n operation.disabled = function() {\n if (!_actions.length) return '';\n\n var actionDisableds = _actions.map(function(action) {\n return action.disabled(context.graph());\n }).filter(Boolean);\n\n if (actionDisableds.length === _actions.length) {\n // none of the features can be squared\n\n if (new Set(actionDisableds).size > 1) {\n return 'multiple_blockers';\n }\n return actionDisableds[0];\n } else if (_extent &&\n _extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = _coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n };\n\n\n operation.getAuxiliaryGeometry = function() {\n const graph = context.graph();\n return _actions.map((action, idx) => {\n if (!action.disabled(graph)) {\n const previewGraph = action(graph, t);\n const way = previewGraph.hasEntity(selectedIDs[idx]);\n const getPath = svgPath(context.projection, previewGraph, false);\n return {\n id: way.id,\n path: getPath(way),\n klass: 'preview'\n };\n } else {\n return false;\n }\n }).filter(Boolean);\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.orthogonalize.' + disable + '.' + _amount) :\n t.append('operations.orthogonalize.description.' + _type + '.' + _amount);\n };\n\n\n operation.annotation = function() {\n return t('operations.orthogonalize.annotation.' + _type, { n: _actions.length });\n };\n\n\n operation.id = 'orthogonalize';\n operation.keys = [t('operations.orthogonalize.key')];\n operation.title = t.append('operations.orthogonalize.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import { t } from '../core/localizer';\nimport { actionReflect } from '../actions/reflect';\nimport { behaviorOperation } from '../behavior/operation';\nimport { utilGetAllNodes, utilTotalExtent } from '../util/util';\nimport { svgPath } from '../svg';\n\n\nexport function operationReflectShort(context, selectedIDs) {\n return operationReflect(context, selectedIDs, 'short');\n}\n\n\nexport function operationReflectLong(context, selectedIDs) {\n return operationReflect(context, selectedIDs, 'long');\n}\n\n\nexport function operationReflect(context, selectedIDs, axis) {\n axis = axis || 'long';\n var multi = (selectedIDs.length === 1 ? 'single' : 'multiple');\n var nodes = utilGetAllNodes(selectedIDs, context.graph());\n var coords = nodes.map(function(n) { return n.loc; });\n var extent = utilTotalExtent(selectedIDs, context.graph());\n\n\n var _action = actionReflect(selectedIDs, context.projection)\n .useLongAxis(Boolean(axis === 'long'));\n\n var operation = function() {\n context.perform(_action, operation.annotation());\n\n window.setTimeout(function() {\n context.validator().validate();\n }, 300); // after any transition\n };\n\n\n operation.available = function() {\n return nodes.length >= 3;\n };\n\n\n // don't cache this because the visible extent could change\n operation.disabled = function() {\n if (extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n } else if (selectedIDs.some(incompleteRelation)) {\n return 'incomplete_relation';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n\n function incompleteRelation(id) {\n var entity = context.entity(id);\n return entity.type === 'relation' && !entity.isComplete(context.graph());\n }\n };\n\n\n operation.getAuxiliaryGeometry = function() {\n const graph = context.graph();\n const [p, q] = _action.getReflectAxis(graph);\n const previewGraph = _action(graph);\n const getPath = svgPath(context.projection, previewGraph, false);\n return [{\n id: 'axis',\n path: `M ${p[0]} ${p[1]} L ${q[0]} ${q[1]}`,\n klass: 'reflect-axis'\n }, ...selectedIDs.map(entityId => {\n const entity = previewGraph.hasEntity(entityId);\n return {\n id: entity.id,\n path: getPath(entity),\n klass: 'preview'\n };\n })];\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.reflect.' + disable + '.' + multi) :\n t.append('operations.reflect.description.' + axis + '.' + multi);\n };\n\n\n operation.annotation = function() {\n return t('operations.reflect.annotation.' + axis + '.feature', { n: selectedIDs.length });\n };\n\n\n operation.id = 'reflect-' + axis;\n operation.keys = [t('operations.reflect.key.' + axis)];\n operation.title = t.append('operations.reflect.title.' + axis);\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport {\n polygonHull as d3_polygonHull,\n polygonCentroid as d3_polygonCentroid\n} from 'd3-polygon';\n\nimport { t } from '../core/localizer';\nimport { actionRotate } from '../actions/rotate';\nimport { actionNoop } from '../actions/noop';\nimport { behaviorEdit } from '../behavior/edit';\nimport { geoVecInterp, geoVecLength } from '../geo/vector';\nimport { modeBrowse } from './browse';\nimport { modeSelect } from './select';\n\nimport { operationCircularize } from '../operations/circularize';\nimport { operationDelete } from '../operations/delete';\nimport { operationMove } from '../operations/move';\nimport { operationOrthogonalize } from '../operations/orthogonalize';\nimport { operationReflectLong, operationReflectShort } from '../operations/reflect';\n\nimport { utilKeybinding } from '../util/keybinding';\nimport { utilFastMouse, utilGetAllNodes } from '../util/util';\n\n\nexport function modeRotate(context, entityIDs) {\n\n var _tolerancePx = 4; // see also behaviorDrag, behaviorSelect, modeMove\n\n var mode = {\n id: 'rotate',\n button: 'browse'\n };\n\n var keybinding = utilKeybinding('rotate');\n var behaviors = [\n behaviorEdit(context),\n operationCircularize(context, entityIDs).behavior,\n operationDelete(context, entityIDs).behavior,\n operationMove(context, entityIDs).behavior,\n operationOrthogonalize(context, entityIDs).behavior,\n operationReflectLong(context, entityIDs).behavior,\n operationReflectShort(context, entityIDs).behavior\n ];\n var annotation = entityIDs.length === 1 ?\n t('operations.rotate.annotation.' + context.graph().geometry(entityIDs[0])) :\n t('operations.rotate.annotation.feature', { n: entityIDs.length });\n\n var _prevGraph;\n var _prevAngle;\n var _prevTransform;\n var _pivot;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function doRotate(d3_event) {\n var fn;\n if (context.graph() !== _prevGraph) {\n fn = context.perform;\n } else {\n fn = context.replace;\n }\n\n // projection changed, recalculate _pivot\n var projection = context.projection;\n var currTransform = projection.transform();\n if (!_prevTransform ||\n currTransform.k !== _prevTransform.k ||\n currTransform.x !== _prevTransform.x ||\n currTransform.y !== _prevTransform.y) {\n\n var nodes = utilGetAllNodes(entityIDs, context.graph());\n var points = nodes.map(function(n) { return projection(n.loc); });\n _pivot = getPivot(points);\n _prevAngle = undefined;\n }\n\n\n var currMouse = context.map().mouse(d3_event);\n var currAngle = Math.atan2(currMouse[1] - _pivot[1], currMouse[0] - _pivot[0]);\n\n if (typeof _prevAngle === 'undefined') _prevAngle = currAngle;\n var delta = currAngle - _prevAngle;\n\n fn(actionRotate(entityIDs, _pivot, delta, projection));\n\n _prevTransform = currTransform;\n _prevAngle = currAngle;\n _prevGraph = context.graph();\n }\n\n function getPivot(points) {\n var _pivot;\n if (points.length === 1) {\n _pivot = points[0];\n } else if (points.length === 2) {\n _pivot = geoVecInterp(points[0], points[1], 0.5);\n } else {\n var polygonHull = d3_polygonHull(points);\n if (polygonHull.length === 2) {\n _pivot = geoVecInterp(points[0], points[1], 0.5);\n } else {\n _pivot = d3_polygonCentroid(d3_polygonHull(points));\n }\n }\n return _pivot;\n }\n\n\n function finish(d3_event) {\n d3_event.stopPropagation();\n context.replace(actionNoop(), annotation);\n context.enter(modeSelect(context, entityIDs));\n }\n\n\n function cancel() {\n if (_prevGraph) context.pop(); // remove the rotate\n context.enter(modeSelect(context, entityIDs));\n }\n\n\n function undone() {\n context.enter(modeBrowse(context));\n }\n\n\n mode.enter = function() {\n _prevGraph = null;\n context.features().forceVisible(entityIDs);\n\n behaviors.forEach(context.install);\n\n var downEvent;\n\n context.surface()\n .on(_pointerPrefix + 'down.modeRotate', function(d3_event) {\n downEvent = d3_event;\n });\n\n d3_select(window)\n .on(_pointerPrefix + 'move.modeRotate', doRotate, true)\n .on(_pointerPrefix + 'up.modeRotate', function(d3_event) {\n if (!downEvent) return;\n var mapNode = context.container().select('.main-map').node();\n var pointGetter = utilFastMouse(mapNode);\n var p1 = pointGetter(downEvent);\n var p2 = pointGetter(d3_event);\n var dist = geoVecLength(p1, p2);\n\n if (dist <= _tolerancePx) finish(d3_event);\n downEvent = null;\n }, true);\n\n context.history()\n .on('undone.modeRotate', undone);\n\n keybinding\n .on('\u238B', cancel)\n .on('\u21A9', finish);\n\n d3_select(document)\n .call(keybinding);\n };\n\n\n mode.exit = function() {\n behaviors.forEach(context.uninstall);\n\n context.surface()\n .on(_pointerPrefix + 'down.modeRotate', null);\n\n d3_select(window)\n .on(_pointerPrefix + 'move.modeRotate', null, true)\n .on(_pointerPrefix + 'up.modeRotate', null, true);\n\n context.history()\n .on('undone.modeRotate', null);\n\n d3_select(document)\n .call(keybinding.unbind);\n\n context.features().forceVisible([]);\n };\n\n\n mode.selectedIDs = function() {\n if (!arguments.length) return entityIDs;\n // no assign\n return mode;\n };\n\n\n return mode;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { t } from '../core/localizer';\nimport { JXON } from '../util/jxon';\nimport { geoExtent } from '../geo';\nimport { osmChangeset } from '../osm';\nimport { svgIcon } from '../svg/icon';\n\nimport {\n utilEntityOrMemberSelector,\n utilKeybinding,\n utilRebind,\n utilWrap\n} from '../util';\n\n\nexport function uiConflicts(context) {\n var dispatch = d3_dispatch('cancel', 'save');\n var keybinding = utilKeybinding('conflicts');\n var _origChanges;\n var _conflictList;\n var _shownConflictIndex;\n\n\n function keybindingOn() {\n d3_select(document)\n .call(keybinding.on('\u238B', cancel, true));\n }\n\n function keybindingOff() {\n d3_select(document)\n .call(keybinding.unbind);\n }\n\n function tryAgain() {\n keybindingOff();\n dispatch.call('save');\n }\n\n function cancel() {\n keybindingOff();\n dispatch.call('cancel');\n }\n\n\n function conflicts(selection) {\n keybindingOn();\n\n var headerEnter = selection.selectAll('.header')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'fr')\n .attr('title', t('icons.close'))\n .on('click', cancel)\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('save.conflict.header'));\n\n var bodyEnter = selection.selectAll('.body')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'body fillL');\n\n var conflictsHelpEnter = bodyEnter\n .append('div')\n .attr('class', 'conflicts-help')\n .call(t.append('save.conflict.help'));\n\n\n // Download changes link\n var changeset = new osmChangeset();\n\n delete changeset.id; // Export without changeset_id\n\n var data = JXON.stringify(changeset.osmChangeJXON(_origChanges));\n var blob = new Blob([data], { type: 'text/xml;charset=utf-8;' });\n var fileName = 'changes.osc';\n\n var linkEnter = conflictsHelpEnter.selectAll('.download-changes')\n .append('a')\n .attr('class', 'download-changes');\n\n // download the data as a file\n linkEnter\n .attr('href', window.URL.createObjectURL(blob))\n .attr('download', fileName);\n\n linkEnter\n .call(svgIcon('#iD-icon-load', 'inline'))\n .append('span')\n .call(t.append('save.conflict.download_changes'));\n\n\n bodyEnter\n .append('div')\n .attr('class', 'conflict-container fillL3')\n .call(showConflict, 0);\n\n bodyEnter\n .append('div')\n .attr('class', 'conflicts-done')\n .attr('opacity', 0)\n .style('display', 'none')\n .call(t.append('save.conflict.done'));\n\n var buttonsEnter = bodyEnter\n .append('div')\n .attr('class','buttons col12 joined conflicts-buttons');\n\n buttonsEnter\n .append('button')\n .attr('disabled', _conflictList.length > 1)\n .attr('class', 'action conflicts-button col6')\n .call(t.append('save.title'))\n .on('click.try_again', tryAgain);\n\n buttonsEnter\n .append('button')\n .attr('class', 'secondary-action conflicts-button col6')\n .call(t.append('confirm.cancel'))\n .on('click.cancel', cancel);\n }\n\n\n function showConflict(selection, index) {\n index = utilWrap(index, _conflictList.length);\n _shownConflictIndex = index;\n\n var parent = d3_select(selection.node().parentNode);\n\n // enable save button if this is the last conflict being reviewed..\n if (index === _conflictList.length - 1) {\n window.setTimeout(function() {\n parent.select('.conflicts-button')\n .attr('disabled', null);\n\n parent.select('.conflicts-done')\n .transition()\n .attr('opacity', 1)\n .style('display', 'block');\n }, 250);\n }\n\n var conflict = selection\n .selectAll('.conflict')\n .data([_conflictList[index]]);\n\n conflict.exit()\n .remove();\n\n var conflictEnter = conflict.enter()\n .append('div')\n .attr('class', 'conflict');\n\n conflictEnter\n .append('h4')\n .attr('class', 'conflict-count')\n .call(t.append('save.conflict.count', { num: index + 1, total: _conflictList.length }));\n\n conflictEnter\n .append('a')\n .attr('class', 'conflict-description')\n .attr('href', '#')\n .text(function(d) { return d.name; })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n zoomToEntity(d.id);\n });\n\n var details = conflictEnter\n .append('div')\n .attr('class', 'conflict-detail-container');\n\n details\n .append('ul')\n .attr('class', 'conflict-detail-list')\n .selectAll('li')\n .data(function(d) { return d.details || []; })\n .enter()\n .append('li')\n .attr('class', 'conflict-detail-item')\n .each(function(d) {\n d3_select(this).call(d);\n });\n\n details\n .append('div')\n .attr('class', 'conflict-choices')\n .call(addChoices);\n\n details\n .append('div')\n .attr('class', 'conflict-nav-buttons joined cf')\n .selectAll('button')\n .data(['previous', 'next'])\n .enter()\n .append('button')\n .attr('class', 'conflict-nav-button action col6')\n .attr('disabled', function(d, i) {\n return (i === 0 && index === 0) ||\n (i === 1 && index === _conflictList.length - 1) || null;\n })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n\n var container = parent.selectAll('.conflict-container');\n var sign = (d === 'previous' ? -1 : 1);\n\n container\n .selectAll('.conflict')\n .remove();\n\n container\n .call(showConflict, index + sign);\n })\n .each(function(d) { t.append('save.conflict.' + d)(d3_select(this)); });\n\n }\n\n\n function addChoices(selection) {\n var choices = selection\n .append('ul')\n .attr('class', 'layer-list')\n .selectAll('li')\n .data(function(d) { return d.choices || []; });\n\n // enter\n var choicesEnter = choices.enter()\n .append('li')\n .attr('class', 'layer');\n\n var labelEnter = choicesEnter\n .append('label');\n\n labelEnter\n .append('input')\n .attr('type', 'radio')\n .attr('name', function(d) { return d.id; })\n .on('change', function(d3_event, d) {\n var ul = this.parentNode.parentNode.parentNode;\n ul.__data__.chosen = d.id;\n choose(d3_event, ul, d);\n });\n\n labelEnter\n .append('span')\n .text(function(d) { return d.text; });\n\n // update\n choicesEnter\n .merge(choices)\n .each(function(d) {\n var ul = this.parentNode;\n if (ul.__data__.chosen === d.id) {\n choose(null, ul, d);\n }\n });\n }\n\n\n function choose(d3_event, ul, datum) {\n if (d3_event) d3_event.preventDefault();\n\n d3_select(ul)\n .selectAll('li')\n .classed('active', function(d) { return d === datum; })\n .selectAll('input')\n .property('checked', function(d) { return d === datum; });\n\n var extent = geoExtent();\n var entity;\n\n entity = context.graph().hasEntity(datum.id);\n if (entity) extent._extend(entity.extent(context.graph()));\n\n datum.action();\n\n entity = context.graph().hasEntity(datum.id);\n if (entity) extent._extend(entity.extent(context.graph()));\n\n zoomToEntity(datum.id, extent);\n }\n\n\n function zoomToEntity(id, extent) {\n context.surface().selectAll('.hover')\n .classed('hover', false);\n\n var entity = context.graph().hasEntity(id);\n if (entity) {\n if (extent) {\n context.map().trimmedExtent(extent);\n } else {\n context.map().zoomToEase(entity);\n }\n context.surface().selectAll(utilEntityOrMemberSelector([entity.id], context.graph()))\n .classed('hover', true);\n }\n }\n\n\n // The conflict list should be an array of objects like:\n // {\n // id: id,\n // name: entityName(local),\n // details: merge.conflicts(),\n // chosen: 1,\n // choices: [\n // choice(id, keepMine, forceLocal),\n // choice(id, keepTheirs, forceRemote)\n // ]\n // }\n conflicts.conflictList = function(_) {\n if (!arguments.length) return _conflictList;\n _conflictList = _;\n return conflicts;\n };\n\n\n conflicts.origChanges = function(_) {\n if (!arguments.length) return _origChanges;\n _origChanges = _;\n return conflicts;\n };\n\n\n conflicts.shownEntityIds = function() {\n if (_conflictList && typeof _shownConflictIndex === 'number') {\n return [_conflictList[_shownConflictIndex].id];\n }\n return [];\n };\n\n\n return utilRebind(conflicts, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { uiModal } from './modal';\n\n\nexport function uiConfirm(selection) {\n var modalSelection = uiModal(selection);\n\n modalSelection.select('.modal')\n .classed('modal-alert', true);\n\n var section = modalSelection.select('.content');\n\n section.append('div')\n .attr('class', 'modal-section header');\n\n section.append('div')\n .attr('class', 'modal-section message-text');\n\n var buttons = section.append('div')\n .attr('class', 'modal-section buttons cf');\n\n\n modalSelection.okButton = function() {\n buttons\n .append('button')\n .attr('class', 'button ok-button action')\n .on('click.confirm', function() {\n modalSelection.remove();\n })\n .call(t.append('confirm.okay'))\n .node()\n .focus();\n\n return modalSelection;\n };\n\n\n return modalSelection;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { utilFunctor } from '../util/util';\n\nvar _popoverID = 0;\n\nexport function uiPopover(klass) {\n var _id = _popoverID++;\n var _anchorSelection = d3_select(null);\n var popover = function(selection) {\n _anchorSelection = selection;\n selection.each(setup);\n };\n var _animation = utilFunctor(false);\n var _placement = utilFunctor('top'); // top, bottom, left, right\n var _alignment = utilFunctor('center'); // leading, center, trailing\n var _scrollContainer = utilFunctor(d3_select(null));\n var _content;\n var _displayType = utilFunctor('');\n var _hasArrow = utilFunctor(true);\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n popover.displayType = function(val) {\n if (arguments.length) {\n _displayType = utilFunctor(val);\n return popover;\n } else {\n return _displayType;\n }\n };\n\n popover.hasArrow = function(val) {\n if (arguments.length) {\n _hasArrow = utilFunctor(val);\n return popover;\n } else {\n return _hasArrow;\n }\n };\n\n popover.placement = function(val) {\n if (arguments.length) {\n _placement = utilFunctor(val);\n return popover;\n } else {\n return _placement;\n }\n };\n\n popover.alignment = function(val) {\n if (arguments.length) {\n _alignment = utilFunctor(val);\n return popover;\n } else {\n return _alignment;\n }\n };\n\n popover.scrollContainer = function(val) {\n if (arguments.length) {\n _scrollContainer = utilFunctor(val);\n return popover;\n } else {\n return _scrollContainer;\n }\n };\n\n popover.content = function(val) {\n if (arguments.length) {\n _content = val;\n return popover;\n } else {\n return _content;\n }\n };\n\n popover.isShown = function() {\n var popoverSelection = _anchorSelection.select('.popover-' + _id);\n return !popoverSelection.empty() && popoverSelection.classed('in');\n };\n\n popover.show = function() {\n _anchorSelection.each(show);\n };\n\n popover.updateContent = function() {\n _anchorSelection.each(updateContent);\n };\n\n popover.hide = function() {\n _anchorSelection.each(hide);\n };\n\n popover.toggle = function() {\n _anchorSelection.each(toggle);\n };\n\n popover.destroy = function(selection, selector) {\n // by default, just destroy the current popover\n selector = selector || '.popover-' + _id;\n\n selection\n .on(_pointerPrefix + 'enter.popover', null)\n .on(_pointerPrefix + 'leave.popover', null)\n .on(_pointerPrefix + 'up.popover', null)\n .on(_pointerPrefix + 'down.popover', null)\n .on('focus.popover', null)\n .on('blur.popover', null)\n .on('click.popover', null)\n .attr('title', function() {\n return this.getAttribute('data-original-title') || this.getAttribute('title');\n })\n .attr('data-original-title', null)\n .selectAll(selector)\n .remove();\n };\n\n\n popover.destroyAny = function(selection) {\n selection.call(popover.destroy, '.popover');\n };\n\n function setup() {\n var anchor = d3_select(this);\n var animate = _animation.apply(this, arguments);\n var popoverSelection = anchor.selectAll('.popover-' + _id)\n .data([0]);\n\n\n var enter = popoverSelection.enter()\n .append('div')\n .attr('class', 'popover popover-' + _id + ' ' + (klass ? klass : ''))\n .classed('arrowed', _hasArrow.apply(this, arguments));\n\n enter\n .append('div')\n .attr('class', 'popover-arrow');\n\n enter\n .append('div')\n .attr('class', 'popover-inner');\n\n popoverSelection = enter\n .merge(popoverSelection);\n\n if (animate) {\n popoverSelection.classed('fade', true);\n }\n\n var display = _displayType.apply(this, arguments);\n\n if (display === 'hover') {\n var _lastNonMouseEnterTime;\n anchor.on(_pointerPrefix + 'enter.popover', function(d3_event) {\n\n if (d3_event.pointerType) {\n if (d3_event.pointerType !== 'mouse') {\n _lastNonMouseEnterTime = d3_event.timeStamp;\n // only allow hover behavior for mouse input\n return;\n } else if (_lastNonMouseEnterTime &&\n d3_event.timeStamp - _lastNonMouseEnterTime < 1500) {\n // HACK: iOS 13.4 sends an erroneous `mouse` type pointerenter\n // event for non-mouse interactions right after sending\n // the correct type pointerenter event. Workaround by discarding\n // any mouse event that occurs immediately after a non-mouse event.\n return;\n }\n }\n\n // don't show if buttons are pressed, e.g. during click and drag of map\n if (d3_event.buttons !== 0) return;\n\n show.apply(this, arguments);\n })\n .on(_pointerPrefix + 'leave.popover', function() {\n hide.apply(this, arguments);\n })\n // show on focus too for better keyboard navigation support\n .on('focus.popover', function() {\n show.apply(this, arguments);\n })\n .on('blur.popover', function() {\n hide.apply(this, arguments);\n });\n\n } else if (display === 'clickFocus') {\n anchor\n .on(_pointerPrefix + 'down.popover', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n })\n .on(_pointerPrefix + 'up.popover', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n })\n .on('click.popover', toggle);\n\n popoverSelection\n // This attribute lets the popover take focus\n .attr('tabindex', 0)\n .on('blur.popover', function() {\n anchor.each(function() {\n hide.apply(this, arguments);\n });\n });\n }\n }\n\n\n function show() {\n var anchor = d3_select(this);\n var popoverSelection = anchor.selectAll('.popover-' + _id);\n\n if (popoverSelection.empty()) {\n // popover was removed somehow, put it back\n anchor.call(popover.destroy);\n anchor.each(setup);\n popoverSelection = anchor.selectAll('.popover-' + _id);\n }\n\n popoverSelection.classed('in', true);\n\n var displayType = _displayType.apply(this, arguments);\n if (displayType === 'clickFocus') {\n anchor.classed('active', true);\n popoverSelection.node().focus();\n }\n\n anchor.each(updateContent);\n }\n\n function updateContent() {\n var anchor = d3_select(this);\n\n if (_content) {\n anchor.selectAll('.popover-' + _id + ' > .popover-inner')\n .call(_content.apply(this, arguments));\n }\n\n updatePosition.apply(this, arguments);\n // hack: update multiple times to fix instances where the absolute offset is\n // set before the dynamic popover size is calculated by the browser\n updatePosition.apply(this, arguments);\n updatePosition.apply(this, arguments);\n }\n\n\n function updatePosition() {\n\n var anchor = d3_select(this);\n var popoverSelection = anchor.selectAll('.popover-' + _id);\n\n var scrollContainer = _scrollContainer && _scrollContainer.apply(this, arguments);\n var scrollNode = scrollContainer && !scrollContainer.empty() && scrollContainer.node();\n var scrollLeft = scrollNode ? scrollNode.scrollLeft : 0;\n var scrollTop = scrollNode ? scrollNode.scrollTop : 0;\n\n var placement = _placement.apply(this, arguments);\n popoverSelection\n .classed('left', false)\n .classed('right', false)\n .classed('top', false)\n .classed('bottom', false)\n .classed(placement, true);\n\n var alignment = _alignment.apply(this, arguments);\n var alignFactor = 0.5;\n if (alignment === 'leading') {\n alignFactor = 0;\n } else if (alignment === 'trailing') {\n alignFactor = 1;\n }\n var anchorFrame = getFrame(anchor.node());\n var popoverFrame = getFrame(popoverSelection.node());\n var position;\n\n switch (placement) {\n case 'top':\n position = {\n x: anchorFrame.x + (anchorFrame.w - popoverFrame.w) * alignFactor,\n y: anchorFrame.y - popoverFrame.h\n };\n break;\n case 'bottom':\n position = {\n x: anchorFrame.x + (anchorFrame.w - popoverFrame.w) * alignFactor,\n y: anchorFrame.y + anchorFrame.h\n };\n break;\n case 'left':\n position = {\n x: anchorFrame.x - popoverFrame.w,\n y: anchorFrame.y + (anchorFrame.h - popoverFrame.h) * alignFactor\n };\n break;\n case 'right':\n position = {\n x: anchorFrame.x + anchorFrame.w,\n y: anchorFrame.y + (anchorFrame.h - popoverFrame.h) * alignFactor\n };\n break;\n }\n\n if (position) {\n if (scrollNode) {\n const MIN_MARGIN = 10;\n const popoverRect = popoverSelection.node().getBoundingClientRect();\n const scrollNodeRect = scrollNode.getBoundingClientRect();\n const arrow = anchor.selectAll('.popover-' + _id + ' > .popover-arrow');\n\n if (placement === 'top' || placement === 'bottom') {\n const initialPosX = position.x;\n if (popoverRect.right > scrollNodeRect.right - MIN_MARGIN) {\n position.x -= popoverRect.right - (scrollNodeRect.right - MIN_MARGIN);\n } else if (popoverRect.left < scrollNodeRect.left) {\n position.x += (scrollNodeRect.left + MIN_MARGIN) - popoverRect.left;\n }\n // keep the arrow centered on the button, or as close as possible\n const arrowPosX = Math.min(Math.max(popoverFrame.w / 2 - (position.x - initialPosX), MIN_MARGIN), popoverFrame.w - MIN_MARGIN);\n arrow.style('left', ~~arrowPosX + 'px');\n\n } else if (placement === 'left' || placement === 'right') {\n const initialPosY = position.y;\n if (popoverRect.bottom > scrollNodeRect.bottom - MIN_MARGIN) {\n position.y -= popoverRect.bottom - (scrollNodeRect.bottom - MIN_MARGIN);\n } else if (popoverRect.top < scrollNodeRect.top + MIN_MARGIN) {\n position.y += (scrollNodeRect.top + MIN_MARGIN) - popoverRect.top;\n }\n // keep the arrow centered on the button, or as close as possible\n const arrowPosY = Math.min(Math.max(popoverFrame.h / 2 - (position.y - initialPosY), MIN_MARGIN), popoverFrame.h - MIN_MARGIN);\n arrow.style('top', ~~arrowPosY + 'px');\n }\n }\n\n popoverSelection\n .style('left', ~~position.x + 'px')\n .style('top', ~~position.y + 'px');\n } else {\n popoverSelection\n .style('left', null)\n .style('top', null);\n }\n\n function getFrame(node) {\n var positionStyle = d3_select(node).style('position');\n if (positionStyle === 'absolute' || positionStyle === 'static') {\n return {\n x: node.offsetLeft - scrollLeft,\n y: node.offsetTop - scrollTop,\n w: node.offsetWidth,\n h: node.offsetHeight\n };\n } else {\n return {\n x: 0,\n y: 0,\n w: node.offsetWidth,\n h: node.offsetHeight\n };\n }\n }\n }\n\n\n function hide() {\n var anchor = d3_select(this);\n if (_displayType.apply(this, arguments) === 'clickFocus') {\n anchor.classed('active', false);\n }\n anchor.selectAll('.popover-' + _id).classed('in', false);\n }\n\n\n function toggle() {\n if (d3_select(this).select('.popover-' + _id).classed('in')) {\n hide.apply(this, arguments);\n } else {\n show.apply(this, arguments);\n }\n }\n\n\n return popover;\n}\n", "import { utilFunctor } from '../util/util';\nimport { t } from '../core/localizer';\nimport { uiPopover } from './popover';\n\nexport function uiTooltip(klass) {\n\n var tooltip = uiPopover((klass || '') + ' tooltip')\n .displayType('hover');\n\n var _title = function() {\n var title = this.getAttribute('data-original-title');\n if (title) {\n return title;\n } else {\n title = this.getAttribute('title');\n this.removeAttribute('title');\n this.setAttribute('data-original-title', title);\n }\n return title;\n };\n\n var _heading = utilFunctor(null);\n var _keys = utilFunctor(null);\n\n tooltip.title = function(val) {\n if (!arguments.length) return _title;\n _title = utilFunctor(val);\n return tooltip;\n };\n\n tooltip.heading = function(val) {\n if (!arguments.length) return _heading;\n _heading = utilFunctor(val);\n return tooltip;\n };\n\n tooltip.keys = function(val) {\n if (!arguments.length) return _keys;\n _keys = utilFunctor(val);\n return tooltip;\n };\n\n tooltip.content(function() {\n var heading = _heading.apply(this, arguments);\n var text = _title.apply(this, arguments);\n var keys = _keys.apply(this, arguments);\n\n var headingCallback = typeof heading === 'function' ? heading : s => s.text(heading);\n var textCallback = typeof text === 'function' ? text : s => s.text(text);\n\n return function(selection) {\n\n var headingSelect = selection\n .selectAll('.tooltip-heading')\n .data(heading ? [heading] :[]);\n\n headingSelect.exit()\n .remove();\n\n headingSelect.enter()\n .append('div')\n .attr('class', 'tooltip-heading')\n .merge(headingSelect)\n .text('')\n .call(headingCallback);\n\n var textSelect = selection\n .selectAll('.tooltip-text')\n .data(text ? [text] :[]);\n\n textSelect.exit()\n .remove();\n\n textSelect.enter()\n .append('div')\n .attr('class', 'tooltip-text')\n .merge(textSelect)\n .text('')\n .call(textCallback);\n\n var keyhintWrap = selection\n .selectAll('.keyhint-wrap')\n .data(keys && keys.length ? [0] : []);\n\n keyhintWrap.exit()\n .remove();\n\n var keyhintWrapEnter = keyhintWrap.enter()\n .append('div')\n .attr('class', 'keyhint-wrap');\n\n keyhintWrapEnter\n .append('span')\n .call(t.append('tooltip_keyhint'));\n\n keyhintWrap = keyhintWrapEnter.merge(keyhintWrap);\n\n keyhintWrap.selectAll('kbd.shortcut')\n .data(keys && keys.length ? keys : [])\n .enter()\n .append('kbd')\n .attr('class', 'shortcut')\n .text(function(d) {\n return d;\n });\n };\n });\n\n return tooltip;\n}\n", "/*\r\n * bignumber.js v9.3.1\r\n * A JavaScript library for arbitrary-precision arithmetic.\r\n * https://github.com/MikeMcl/bignumber.js\r\n * Copyright (c) 2025 Michael Mclaughlin \r\n * MIT Licensed.\r\n *\r\n * BigNumber.prototype methods | BigNumber methods\r\n * |\r\n * absoluteValue abs | clone\r\n * comparedTo | config set\r\n * decimalPlaces dp | DECIMAL_PLACES\r\n * dividedBy div | ROUNDING_MODE\r\n * dividedToIntegerBy idiv | EXPONENTIAL_AT\r\n * exponentiatedBy pow | RANGE\r\n * integerValue | CRYPTO\r\n * isEqualTo eq | MODULO_MODE\r\n * isFinite | POW_PRECISION\r\n * isGreaterThan gt | FORMAT\r\n * isGreaterThanOrEqualTo gte | ALPHABET\r\n * isInteger | isBigNumber\r\n * isLessThan lt | maximum max\r\n * isLessThanOrEqualTo lte | minimum min\r\n * isNaN | random\r\n * isNegative | sum\r\n * isPositive |\r\n * isZero |\r\n * minus |\r\n * modulo mod |\r\n * multipliedBy times |\r\n * negated |\r\n * plus |\r\n * precision sd |\r\n * shiftedBy |\r\n * squareRoot sqrt |\r\n * toExponential |\r\n * toFixed |\r\n * toFormat |\r\n * toFraction |\r\n * toJSON |\r\n * toNumber |\r\n * toPrecision |\r\n * toString |\r\n * valueOf |\r\n *\r\n */\r\n\r\n\r\nvar\r\n isNumeric = /^-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?$/i,\r\n mathceil = Math.ceil,\r\n mathfloor = Math.floor,\r\n\r\n bignumberError = '[BigNumber Error] ',\r\n tooManyDigits = bignumberError + 'Number primitive has more than 15 significant digits: ',\r\n\r\n BASE = 1e14,\r\n LOG_BASE = 14,\r\n MAX_SAFE_INTEGER = 0x1fffffffffffff, // 2^53 - 1\r\n // MAX_INT32 = 0x7fffffff, // 2^31 - 1\r\n POWS_TEN = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13],\r\n SQRT_BASE = 1e7,\r\n\r\n // EDITABLE\r\n // The limit on the value of DECIMAL_PLACES, TO_EXP_NEG, TO_EXP_POS, MIN_EXP, MAX_EXP, and\r\n // the arguments to toExponential, toFixed, toFormat, and toPrecision.\r\n MAX = 1E9; // 0 to MAX_INT32\r\n\r\n\r\n/*\r\n * Create and return a BigNumber constructor.\r\n */\r\nfunction clone(configObject) {\r\n var div, convertBase, parseNumeric,\r\n P = BigNumber.prototype = { constructor: BigNumber, toString: null, valueOf: null },\r\n ONE = new BigNumber(1),\r\n\r\n\r\n //----------------------------- EDITABLE CONFIG DEFAULTS -------------------------------\r\n\r\n\r\n // The default values below must be integers within the inclusive ranges stated.\r\n // The values can also be changed at run-time using BigNumber.set.\r\n\r\n // The maximum number of decimal places for operations involving division.\r\n DECIMAL_PLACES = 20, // 0 to MAX\r\n\r\n // The rounding mode used when rounding to the above decimal places, and when using\r\n // toExponential, toFixed, toFormat and toPrecision, and round (default value).\r\n // UP 0 Away from zero.\r\n // DOWN 1 Towards zero.\r\n // CEIL 2 Towards +Infinity.\r\n // FLOOR 3 Towards -Infinity.\r\n // HALF_UP 4 Towards nearest neighbour. If equidistant, up.\r\n // HALF_DOWN 5 Towards nearest neighbour. If equidistant, down.\r\n // HALF_EVEN 6 Towards nearest neighbour. If equidistant, towards even neighbour.\r\n // HALF_CEIL 7 Towards nearest neighbour. If equidistant, towards +Infinity.\r\n // HALF_FLOOR 8 Towards nearest neighbour. If equidistant, towards -Infinity.\r\n ROUNDING_MODE = 4, // 0 to 8\r\n\r\n // EXPONENTIAL_AT : [TO_EXP_NEG , TO_EXP_POS]\r\n\r\n // The exponent value at and beneath which toString returns exponential notation.\r\n // Number type: -7\r\n TO_EXP_NEG = -7, // 0 to -MAX\r\n\r\n // The exponent value at and above which toString returns exponential notation.\r\n // Number type: 21\r\n TO_EXP_POS = 21, // 0 to MAX\r\n\r\n // RANGE : [MIN_EXP, MAX_EXP]\r\n\r\n // The minimum exponent value, beneath which underflow to zero occurs.\r\n // Number type: -324 (5e-324)\r\n MIN_EXP = -1e7, // -1 to -MAX\r\n\r\n // The maximum exponent value, above which overflow to Infinity occurs.\r\n // Number type: 308 (1.7976931348623157e+308)\r\n // For MAX_EXP > 1e7, e.g. new BigNumber('1e100000000').plus(1) may be slow.\r\n MAX_EXP = 1e7, // 1 to MAX\r\n\r\n // Whether to use cryptographically-secure random number generation, if available.\r\n CRYPTO = false, // true or false\r\n\r\n // The modulo mode used when calculating the modulus: a mod n.\r\n // The quotient (q = a / n) is calculated according to the corresponding rounding mode.\r\n // The remainder (r) is calculated as: r = a - n * q.\r\n //\r\n // UP 0 The remainder is positive if the dividend is negative, else is negative.\r\n // DOWN 1 The remainder has the same sign as the dividend.\r\n // This modulo mode is commonly known as 'truncated division' and is\r\n // equivalent to (a % n) in JavaScript.\r\n // FLOOR 3 The remainder has the same sign as the divisor (Python %).\r\n // HALF_EVEN 6 This modulo mode implements the IEEE 754 remainder function.\r\n // EUCLID 9 Euclidian division. q = sign(n) * floor(a / abs(n)).\r\n // The remainder is always positive.\r\n //\r\n // The truncated division, floored division, Euclidian division and IEEE 754 remainder\r\n // modes are commonly used for the modulus operation.\r\n // Although the other rounding modes can also be used, they may not give useful results.\r\n MODULO_MODE = 1, // 0 to 9\r\n\r\n // The maximum number of significant digits of the result of the exponentiatedBy operation.\r\n // If POW_PRECISION is 0, there will be unlimited significant digits.\r\n POW_PRECISION = 0, // 0 to MAX\r\n\r\n // The format specification used by the BigNumber.prototype.toFormat method.\r\n FORMAT = {\r\n prefix: '',\r\n groupSize: 3,\r\n secondaryGroupSize: 0,\r\n groupSeparator: ',',\r\n decimalSeparator: '.',\r\n fractionGroupSize: 0,\r\n fractionGroupSeparator: '\\xA0', // non-breaking space\r\n suffix: ''\r\n },\r\n\r\n // The alphabet used for base conversion. It must be at least 2 characters long, with no '+',\r\n // '-', '.', whitespace, or repeated character.\r\n // '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_'\r\n ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz',\r\n alphabetHasNormalDecimalDigits = true;\r\n\r\n\r\n //------------------------------------------------------------------------------------------\r\n\r\n\r\n // CONSTRUCTOR\r\n\r\n\r\n /*\r\n * The BigNumber constructor and exported function.\r\n * Create and return a new instance of a BigNumber object.\r\n *\r\n * v {number|string|BigNumber} A numeric value.\r\n * [b] {number} The base of v. Integer, 2 to ALPHABET.length inclusive.\r\n */\r\n function BigNumber(v, b) {\r\n var alphabet, c, caseChanged, e, i, isNum, len, str,\r\n x = this;\r\n\r\n // Enable constructor call without `new`.\r\n if (!(x instanceof BigNumber)) return new BigNumber(v, b);\r\n\r\n if (b == null) {\r\n\r\n if (v && v._isBigNumber === true) {\r\n x.s = v.s;\r\n\r\n if (!v.c || v.e > MAX_EXP) {\r\n x.c = x.e = null;\r\n } else if (v.e < MIN_EXP) {\r\n x.c = [x.e = 0];\r\n } else {\r\n x.e = v.e;\r\n x.c = v.c.slice();\r\n }\r\n\r\n return;\r\n }\r\n\r\n if ((isNum = typeof v == 'number') && v * 0 == 0) {\r\n\r\n // Use `1 / n` to handle minus zero also.\r\n x.s = 1 / v < 0 ? (v = -v, -1) : 1;\r\n\r\n // Fast path for integers, where n < 2147483648 (2**31).\r\n if (v === ~~v) {\r\n for (e = 0, i = v; i >= 10; i /= 10, e++);\r\n\r\n if (e > MAX_EXP) {\r\n x.c = x.e = null;\r\n } else {\r\n x.e = e;\r\n x.c = [v];\r\n }\r\n\r\n return;\r\n }\r\n\r\n str = String(v);\r\n } else {\r\n\r\n if (!isNumeric.test(str = String(v))) return parseNumeric(x, str, isNum);\r\n\r\n x.s = str.charCodeAt(0) == 45 ? (str = str.slice(1), -1) : 1;\r\n }\r\n\r\n // Decimal point?\r\n if ((e = str.indexOf('.')) > -1) str = str.replace('.', '');\r\n\r\n // Exponential form?\r\n if ((i = str.search(/e/i)) > 0) {\r\n\r\n // Determine exponent.\r\n if (e < 0) e = i;\r\n e += +str.slice(i + 1);\r\n str = str.substring(0, i);\r\n } else if (e < 0) {\r\n\r\n // Integer.\r\n e = str.length;\r\n }\r\n\r\n } else {\r\n\r\n // '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}'\r\n intCheck(b, 2, ALPHABET.length, 'Base');\r\n\r\n // Allow exponential notation to be used with base 10 argument, while\r\n // also rounding to DECIMAL_PLACES as with other bases.\r\n if (b == 10 && alphabetHasNormalDecimalDigits) {\r\n x = new BigNumber(v);\r\n return round(x, DECIMAL_PLACES + x.e + 1, ROUNDING_MODE);\r\n }\r\n\r\n str = String(v);\r\n\r\n if (isNum = typeof v == 'number') {\r\n\r\n // Avoid potential interpretation of Infinity and NaN as base 44+ values.\r\n if (v * 0 != 0) return parseNumeric(x, str, isNum, b);\r\n\r\n x.s = 1 / v < 0 ? (str = str.slice(1), -1) : 1;\r\n\r\n // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}'\r\n if (BigNumber.DEBUG && str.replace(/^0\\.0*|\\./, '').length > 15) {\r\n throw Error\r\n (tooManyDigits + v);\r\n }\r\n } else {\r\n x.s = str.charCodeAt(0) === 45 ? (str = str.slice(1), -1) : 1;\r\n }\r\n\r\n alphabet = ALPHABET.slice(0, b);\r\n e = i = 0;\r\n\r\n // Check that str is a valid base b number.\r\n // Don't use RegExp, so alphabet can contain special characters.\r\n for (len = str.length; i < len; i++) {\r\n if (alphabet.indexOf(c = str.charAt(i)) < 0) {\r\n if (c == '.') {\r\n\r\n // If '.' is not the first character and it has not be found before.\r\n if (i > e) {\r\n e = len;\r\n continue;\r\n }\r\n } else if (!caseChanged) {\r\n\r\n // Allow e.g. hexadecimal 'FF' as well as 'ff'.\r\n if (str == str.toUpperCase() && (str = str.toLowerCase()) ||\r\n str == str.toLowerCase() && (str = str.toUpperCase())) {\r\n caseChanged = true;\r\n i = -1;\r\n e = 0;\r\n continue;\r\n }\r\n }\r\n\r\n return parseNumeric(x, String(v), isNum, b);\r\n }\r\n }\r\n\r\n // Prevent later check for length on converted number.\r\n isNum = false;\r\n str = convertBase(str, b, 10, x.s);\r\n\r\n // Decimal point?\r\n if ((e = str.indexOf('.')) > -1) str = str.replace('.', '');\r\n else e = str.length;\r\n }\r\n\r\n // Determine leading zeros.\r\n for (i = 0; str.charCodeAt(i) === 48; i++);\r\n\r\n // Determine trailing zeros.\r\n for (len = str.length; str.charCodeAt(--len) === 48;);\r\n\r\n if (str = str.slice(i, ++len)) {\r\n len -= i;\r\n\r\n // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}'\r\n if (isNum && BigNumber.DEBUG &&\r\n len > 15 && (v > MAX_SAFE_INTEGER || v !== mathfloor(v))) {\r\n throw Error\r\n (tooManyDigits + (x.s * v));\r\n }\r\n\r\n // Overflow?\r\n if ((e = e - i - 1) > MAX_EXP) {\r\n\r\n // Infinity.\r\n x.c = x.e = null;\r\n\r\n // Underflow?\r\n } else if (e < MIN_EXP) {\r\n\r\n // Zero.\r\n x.c = [x.e = 0];\r\n } else {\r\n x.e = e;\r\n x.c = [];\r\n\r\n // Transform base\r\n\r\n // e is the base 10 exponent.\r\n // i is where to slice str to get the first element of the coefficient array.\r\n i = (e + 1) % LOG_BASE;\r\n if (e < 0) i += LOG_BASE; // i < 1\r\n\r\n if (i < len) {\r\n if (i) x.c.push(+str.slice(0, i));\r\n\r\n for (len -= LOG_BASE; i < len;) {\r\n x.c.push(+str.slice(i, i += LOG_BASE));\r\n }\r\n\r\n i = LOG_BASE - (str = str.slice(i)).length;\r\n } else {\r\n i -= len;\r\n }\r\n\r\n for (; i--; str += '0');\r\n x.c.push(+str);\r\n }\r\n } else {\r\n\r\n // Zero.\r\n x.c = [x.e = 0];\r\n }\r\n }\r\n\r\n\r\n // CONSTRUCTOR PROPERTIES\r\n\r\n\r\n BigNumber.clone = clone;\r\n\r\n BigNumber.ROUND_UP = 0;\r\n BigNumber.ROUND_DOWN = 1;\r\n BigNumber.ROUND_CEIL = 2;\r\n BigNumber.ROUND_FLOOR = 3;\r\n BigNumber.ROUND_HALF_UP = 4;\r\n BigNumber.ROUND_HALF_DOWN = 5;\r\n BigNumber.ROUND_HALF_EVEN = 6;\r\n BigNumber.ROUND_HALF_CEIL = 7;\r\n BigNumber.ROUND_HALF_FLOOR = 8;\r\n BigNumber.EUCLID = 9;\r\n\r\n\r\n /*\r\n * Configure infrequently-changing library-wide settings.\r\n *\r\n * Accept an object with the following optional properties (if the value of a property is\r\n * a number, it must be an integer within the inclusive range stated):\r\n *\r\n * DECIMAL_PLACES {number} 0 to MAX\r\n * ROUNDING_MODE {number} 0 to 8\r\n * EXPONENTIAL_AT {number|number[]} -MAX to MAX or [-MAX to 0, 0 to MAX]\r\n * RANGE {number|number[]} -MAX to MAX (not zero) or [-MAX to -1, 1 to MAX]\r\n * CRYPTO {boolean} true or false\r\n * MODULO_MODE {number} 0 to 9\r\n * POW_PRECISION {number} 0 to MAX\r\n * ALPHABET {string} A string of two or more unique characters which does\r\n * not contain '.'.\r\n * FORMAT {object} An object with some of the following properties:\r\n * prefix {string}\r\n * groupSize {number}\r\n * secondaryGroupSize {number}\r\n * groupSeparator {string}\r\n * decimalSeparator {string}\r\n * fractionGroupSize {number}\r\n * fractionGroupSeparator {string}\r\n * suffix {string}\r\n *\r\n * (The values assigned to the above FORMAT object properties are not checked for validity.)\r\n *\r\n * E.g.\r\n * BigNumber.config({ DECIMAL_PLACES : 20, ROUNDING_MODE : 4 })\r\n *\r\n * Ignore properties/parameters set to null or undefined, except for ALPHABET.\r\n *\r\n * Return an object with the properties current values.\r\n */\r\n BigNumber.config = BigNumber.set = function (obj) {\r\n var p, v;\r\n\r\n if (obj != null) {\r\n\r\n if (typeof obj == 'object') {\r\n\r\n // DECIMAL_PLACES {number} Integer, 0 to MAX inclusive.\r\n // '[BigNumber Error] DECIMAL_PLACES {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'DECIMAL_PLACES')) {\r\n v = obj[p];\r\n intCheck(v, 0, MAX, p);\r\n DECIMAL_PLACES = v;\r\n }\r\n\r\n // ROUNDING_MODE {number} Integer, 0 to 8 inclusive.\r\n // '[BigNumber Error] ROUNDING_MODE {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'ROUNDING_MODE')) {\r\n v = obj[p];\r\n intCheck(v, 0, 8, p);\r\n ROUNDING_MODE = v;\r\n }\r\n\r\n // EXPONENTIAL_AT {number|number[]}\r\n // Integer, -MAX to MAX inclusive or\r\n // [integer -MAX to 0 inclusive, 0 to MAX inclusive].\r\n // '[BigNumber Error] EXPONENTIAL_AT {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'EXPONENTIAL_AT')) {\r\n v = obj[p];\r\n if (v && v.pop) {\r\n intCheck(v[0], -MAX, 0, p);\r\n intCheck(v[1], 0, MAX, p);\r\n TO_EXP_NEG = v[0];\r\n TO_EXP_POS = v[1];\r\n } else {\r\n intCheck(v, -MAX, MAX, p);\r\n TO_EXP_NEG = -(TO_EXP_POS = v < 0 ? -v : v);\r\n }\r\n }\r\n\r\n // RANGE {number|number[]} Non-zero integer, -MAX to MAX inclusive or\r\n // [integer -MAX to -1 inclusive, integer 1 to MAX inclusive].\r\n // '[BigNumber Error] RANGE {not a primitive number|not an integer|out of range|cannot be zero}: {v}'\r\n if (obj.hasOwnProperty(p = 'RANGE')) {\r\n v = obj[p];\r\n if (v && v.pop) {\r\n intCheck(v[0], -MAX, -1, p);\r\n intCheck(v[1], 1, MAX, p);\r\n MIN_EXP = v[0];\r\n MAX_EXP = v[1];\r\n } else {\r\n intCheck(v, -MAX, MAX, p);\r\n if (v) {\r\n MIN_EXP = -(MAX_EXP = v < 0 ? -v : v);\r\n } else {\r\n throw Error\r\n (bignumberError + p + ' cannot be zero: ' + v);\r\n }\r\n }\r\n }\r\n\r\n // CRYPTO {boolean} true or false.\r\n // '[BigNumber Error] CRYPTO not true or false: {v}'\r\n // '[BigNumber Error] crypto unavailable'\r\n if (obj.hasOwnProperty(p = 'CRYPTO')) {\r\n v = obj[p];\r\n if (v === !!v) {\r\n if (v) {\r\n if (typeof crypto != 'undefined' && crypto &&\r\n (crypto.getRandomValues || crypto.randomBytes)) {\r\n CRYPTO = v;\r\n } else {\r\n CRYPTO = !v;\r\n throw Error\r\n (bignumberError + 'crypto unavailable');\r\n }\r\n } else {\r\n CRYPTO = v;\r\n }\r\n } else {\r\n throw Error\r\n (bignumberError + p + ' not true or false: ' + v);\r\n }\r\n }\r\n\r\n // MODULO_MODE {number} Integer, 0 to 9 inclusive.\r\n // '[BigNumber Error] MODULO_MODE {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'MODULO_MODE')) {\r\n v = obj[p];\r\n intCheck(v, 0, 9, p);\r\n MODULO_MODE = v;\r\n }\r\n\r\n // POW_PRECISION {number} Integer, 0 to MAX inclusive.\r\n // '[BigNumber Error] POW_PRECISION {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'POW_PRECISION')) {\r\n v = obj[p];\r\n intCheck(v, 0, MAX, p);\r\n POW_PRECISION = v;\r\n }\r\n\r\n // FORMAT {object}\r\n // '[BigNumber Error] FORMAT not an object: {v}'\r\n if (obj.hasOwnProperty(p = 'FORMAT')) {\r\n v = obj[p];\r\n if (typeof v == 'object') FORMAT = v;\r\n else throw Error\r\n (bignumberError + p + ' not an object: ' + v);\r\n }\r\n\r\n // ALPHABET {string}\r\n // '[BigNumber Error] ALPHABET invalid: {v}'\r\n if (obj.hasOwnProperty(p = 'ALPHABET')) {\r\n v = obj[p];\r\n\r\n // Disallow if less than two characters,\r\n // or if it contains '+', '-', '.', whitespace, or a repeated character.\r\n if (typeof v == 'string' && !/^.?$|[+\\-.\\s]|(.).*\\1/.test(v)) {\r\n alphabetHasNormalDecimalDigits = v.slice(0, 10) == '0123456789';\r\n ALPHABET = v;\r\n } else {\r\n throw Error\r\n (bignumberError + p + ' invalid: ' + v);\r\n }\r\n }\r\n\r\n } else {\r\n\r\n // '[BigNumber Error] Object expected: {v}'\r\n throw Error\r\n (bignumberError + 'Object expected: ' + obj);\r\n }\r\n }\r\n\r\n return {\r\n DECIMAL_PLACES: DECIMAL_PLACES,\r\n ROUNDING_MODE: ROUNDING_MODE,\r\n EXPONENTIAL_AT: [TO_EXP_NEG, TO_EXP_POS],\r\n RANGE: [MIN_EXP, MAX_EXP],\r\n CRYPTO: CRYPTO,\r\n MODULO_MODE: MODULO_MODE,\r\n POW_PRECISION: POW_PRECISION,\r\n FORMAT: FORMAT,\r\n ALPHABET: ALPHABET\r\n };\r\n };\r\n\r\n\r\n /*\r\n * Return true if v is a BigNumber instance, otherwise return false.\r\n *\r\n * If BigNumber.DEBUG is true, throw if a BigNumber instance is not well-formed.\r\n *\r\n * v {any}\r\n *\r\n * '[BigNumber Error] Invalid BigNumber: {v}'\r\n */\r\n BigNumber.isBigNumber = function (v) {\r\n if (!v || v._isBigNumber !== true) return false;\r\n if (!BigNumber.DEBUG) return true;\r\n\r\n var i, n,\r\n c = v.c,\r\n e = v.e,\r\n s = v.s;\r\n\r\n out: if ({}.toString.call(c) == '[object Array]') {\r\n\r\n if ((s === 1 || s === -1) && e >= -MAX && e <= MAX && e === mathfloor(e)) {\r\n\r\n // If the first element is zero, the BigNumber value must be zero.\r\n if (c[0] === 0) {\r\n if (e === 0 && c.length === 1) return true;\r\n break out;\r\n }\r\n\r\n // Calculate number of digits that c[0] should have, based on the exponent.\r\n i = (e + 1) % LOG_BASE;\r\n if (i < 1) i += LOG_BASE;\r\n\r\n // Calculate number of digits of c[0].\r\n //if (Math.ceil(Math.log(c[0] + 1) / Math.LN10) == i) {\r\n if (String(c[0]).length == i) {\r\n\r\n for (i = 0; i < c.length; i++) {\r\n n = c[i];\r\n if (n < 0 || n >= BASE || n !== mathfloor(n)) break out;\r\n }\r\n\r\n // Last element cannot be zero, unless it is the only element.\r\n if (n !== 0) return true;\r\n }\r\n }\r\n\r\n // Infinity/NaN\r\n } else if (c === null && e === null && (s === null || s === 1 || s === -1)) {\r\n return true;\r\n }\r\n\r\n throw Error\r\n (bignumberError + 'Invalid BigNumber: ' + v);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the maximum of the arguments.\r\n *\r\n * arguments {number|string|BigNumber}\r\n */\r\n BigNumber.maximum = BigNumber.max = function () {\r\n return maxOrMin(arguments, -1);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the minimum of the arguments.\r\n *\r\n * arguments {number|string|BigNumber}\r\n */\r\n BigNumber.minimum = BigNumber.min = function () {\r\n return maxOrMin(arguments, 1);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber with a random value equal to or greater than 0 and less than 1,\r\n * and with dp, or DECIMAL_PLACES if dp is omitted, decimal places (or less if trailing\r\n * zeros are produced).\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp}'\r\n * '[BigNumber Error] crypto unavailable'\r\n */\r\n BigNumber.random = (function () {\r\n var pow2_53 = 0x20000000000000;\r\n\r\n // Return a 53 bit integer n, where 0 <= n < 9007199254740992.\r\n // Check if Math.random() produces more than 32 bits of randomness.\r\n // If it does, assume at least 53 bits are produced, otherwise assume at least 30 bits.\r\n // 0x40000000 is 2^30, 0x800000 is 2^23, 0x1fffff is 2^21 - 1.\r\n var random53bitInt = (Math.random() * pow2_53) & 0x1fffff\r\n ? function () { return mathfloor(Math.random() * pow2_53); }\r\n : function () { return ((Math.random() * 0x40000000 | 0) * 0x800000) +\r\n (Math.random() * 0x800000 | 0); };\r\n\r\n return function (dp) {\r\n var a, b, e, k, v,\r\n i = 0,\r\n c = [],\r\n rand = new BigNumber(ONE);\r\n\r\n if (dp == null) dp = DECIMAL_PLACES;\r\n else intCheck(dp, 0, MAX);\r\n\r\n k = mathceil(dp / LOG_BASE);\r\n\r\n if (CRYPTO) {\r\n\r\n // Browsers supporting crypto.getRandomValues.\r\n if (crypto.getRandomValues) {\r\n\r\n a = crypto.getRandomValues(new Uint32Array(k *= 2));\r\n\r\n for (; i < k;) {\r\n\r\n // 53 bits:\r\n // ((Math.pow(2, 32) - 1) * Math.pow(2, 21)).toString(2)\r\n // 11111 11111111 11111111 11111111 11100000 00000000 00000000\r\n // ((Math.pow(2, 32) - 1) >>> 11).toString(2)\r\n // 11111 11111111 11111111\r\n // 0x20000 is 2^21.\r\n v = a[i] * 0x20000 + (a[i + 1] >>> 11);\r\n\r\n // Rejection sampling:\r\n // 0 <= v < 9007199254740992\r\n // Probability that v >= 9e15, is\r\n // 7199254740992 / 9007199254740992 ~= 0.0008, i.e. 1 in 1251\r\n if (v >= 9e15) {\r\n b = crypto.getRandomValues(new Uint32Array(2));\r\n a[i] = b[0];\r\n a[i + 1] = b[1];\r\n } else {\r\n\r\n // 0 <= v <= 8999999999999999\r\n // 0 <= (v % 1e14) <= 99999999999999\r\n c.push(v % 1e14);\r\n i += 2;\r\n }\r\n }\r\n i = k / 2;\r\n\r\n // Node.js supporting crypto.randomBytes.\r\n } else if (crypto.randomBytes) {\r\n\r\n // buffer\r\n a = crypto.randomBytes(k *= 7);\r\n\r\n for (; i < k;) {\r\n\r\n // 0x1000000000000 is 2^48, 0x10000000000 is 2^40\r\n // 0x100000000 is 2^32, 0x1000000 is 2^24\r\n // 11111 11111111 11111111 11111111 11111111 11111111 11111111\r\n // 0 <= v < 9007199254740992\r\n v = ((a[i] & 31) * 0x1000000000000) + (a[i + 1] * 0x10000000000) +\r\n (a[i + 2] * 0x100000000) + (a[i + 3] * 0x1000000) +\r\n (a[i + 4] << 16) + (a[i + 5] << 8) + a[i + 6];\r\n\r\n if (v >= 9e15) {\r\n crypto.randomBytes(7).copy(a, i);\r\n } else {\r\n\r\n // 0 <= (v % 1e14) <= 99999999999999\r\n c.push(v % 1e14);\r\n i += 7;\r\n }\r\n }\r\n i = k / 7;\r\n } else {\r\n CRYPTO = false;\r\n throw Error\r\n (bignumberError + 'crypto unavailable');\r\n }\r\n }\r\n\r\n // Use Math.random.\r\n if (!CRYPTO) {\r\n\r\n for (; i < k;) {\r\n v = random53bitInt();\r\n if (v < 9e15) c[i++] = v % 1e14;\r\n }\r\n }\r\n\r\n k = c[--i];\r\n dp %= LOG_BASE;\r\n\r\n // Convert trailing digits to zeros according to dp.\r\n if (k && dp) {\r\n v = POWS_TEN[LOG_BASE - dp];\r\n c[i] = mathfloor(k / v) * v;\r\n }\r\n\r\n // Remove trailing elements which are zero.\r\n for (; c[i] === 0; c.pop(), i--);\r\n\r\n // Zero?\r\n if (i < 0) {\r\n c = [e = 0];\r\n } else {\r\n\r\n // Remove leading elements which are zero and adjust exponent accordingly.\r\n for (e = -1 ; c[0] === 0; c.splice(0, 1), e -= LOG_BASE);\r\n\r\n // Count the digits of the first element of c to determine leading zeros, and...\r\n for (i = 1, v = c[0]; v >= 10; v /= 10, i++);\r\n\r\n // adjust the exponent accordingly.\r\n if (i < LOG_BASE) e -= LOG_BASE - i;\r\n }\r\n\r\n rand.e = e;\r\n rand.c = c;\r\n return rand;\r\n };\r\n })();\r\n\r\n\r\n /*\r\n * Return a BigNumber whose value is the sum of the arguments.\r\n *\r\n * arguments {number|string|BigNumber}\r\n */\r\n BigNumber.sum = function () {\r\n var i = 1,\r\n args = arguments,\r\n sum = new BigNumber(args[0]);\r\n for (; i < args.length;) sum = sum.plus(args[i++]);\r\n return sum;\r\n };\r\n\r\n\r\n // PRIVATE FUNCTIONS\r\n\r\n\r\n // Called by BigNumber and BigNumber.prototype.toString.\r\n convertBase = (function () {\r\n var decimal = '0123456789';\r\n\r\n /*\r\n * Convert string of baseIn to an array of numbers of baseOut.\r\n * Eg. toBaseOut('255', 10, 16) returns [15, 15].\r\n * Eg. toBaseOut('ff', 16, 10) returns [2, 5, 5].\r\n */\r\n function toBaseOut(str, baseIn, baseOut, alphabet) {\r\n var j,\r\n arr = [0],\r\n arrL,\r\n i = 0,\r\n len = str.length;\r\n\r\n for (; i < len;) {\r\n for (arrL = arr.length; arrL--; arr[arrL] *= baseIn);\r\n\r\n arr[0] += alphabet.indexOf(str.charAt(i++));\r\n\r\n for (j = 0; j < arr.length; j++) {\r\n\r\n if (arr[j] > baseOut - 1) {\r\n if (arr[j + 1] == null) arr[j + 1] = 0;\r\n arr[j + 1] += arr[j] / baseOut | 0;\r\n arr[j] %= baseOut;\r\n }\r\n }\r\n }\r\n\r\n return arr.reverse();\r\n }\r\n\r\n // Convert a numeric string of baseIn to a numeric string of baseOut.\r\n // If the caller is toString, we are converting from base 10 to baseOut.\r\n // If the caller is BigNumber, we are converting from baseIn to base 10.\r\n return function (str, baseIn, baseOut, sign, callerIsToString) {\r\n var alphabet, d, e, k, r, x, xc, y,\r\n i = str.indexOf('.'),\r\n dp = DECIMAL_PLACES,\r\n rm = ROUNDING_MODE;\r\n\r\n // Non-integer.\r\n if (i >= 0) {\r\n k = POW_PRECISION;\r\n\r\n // Unlimited precision.\r\n POW_PRECISION = 0;\r\n str = str.replace('.', '');\r\n y = new BigNumber(baseIn);\r\n x = y.pow(str.length - i);\r\n POW_PRECISION = k;\r\n\r\n // Convert str as if an integer, then restore the fraction part by dividing the\r\n // result by its base raised to a power.\r\n\r\n y.c = toBaseOut(toFixedPoint(coeffToString(x.c), x.e, '0'),\r\n 10, baseOut, decimal);\r\n y.e = y.c.length;\r\n }\r\n\r\n // Convert the number as integer.\r\n\r\n xc = toBaseOut(str, baseIn, baseOut, callerIsToString\r\n ? (alphabet = ALPHABET, decimal)\r\n : (alphabet = decimal, ALPHABET));\r\n\r\n // xc now represents str as an integer and converted to baseOut. e is the exponent.\r\n e = k = xc.length;\r\n\r\n // Remove trailing zeros.\r\n for (; xc[--k] == 0; xc.pop());\r\n\r\n // Zero?\r\n if (!xc[0]) return alphabet.charAt(0);\r\n\r\n // Does str represent an integer? If so, no need for the division.\r\n if (i < 0) {\r\n --e;\r\n } else {\r\n x.c = xc;\r\n x.e = e;\r\n\r\n // The sign is needed for correct rounding.\r\n x.s = sign;\r\n x = div(x, y, dp, rm, baseOut);\r\n xc = x.c;\r\n r = x.r;\r\n e = x.e;\r\n }\r\n\r\n // xc now represents str converted to baseOut.\r\n\r\n // The index of the rounding digit.\r\n d = e + dp + 1;\r\n\r\n // The rounding digit: the digit to the right of the digit that may be rounded up.\r\n i = xc[d];\r\n\r\n // Look at the rounding digits and mode to determine whether to round up.\r\n\r\n k = baseOut / 2;\r\n r = r || d < 0 || xc[d + 1] != null;\r\n\r\n r = rm < 4 ? (i != null || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2))\r\n : i > k || i == k &&(rm == 4 || r || rm == 6 && xc[d - 1] & 1 ||\r\n rm == (x.s < 0 ? 8 : 7));\r\n\r\n // If the index of the rounding digit is not greater than zero, or xc represents\r\n // zero, then the result of the base conversion is zero or, if rounding up, a value\r\n // such as 0.00001.\r\n if (d < 1 || !xc[0]) {\r\n\r\n // 1^-dp or 0\r\n str = r ? toFixedPoint(alphabet.charAt(1), -dp, alphabet.charAt(0)) : alphabet.charAt(0);\r\n } else {\r\n\r\n // Truncate xc to the required number of decimal places.\r\n xc.length = d;\r\n\r\n // Round up?\r\n if (r) {\r\n\r\n // Rounding up may mean the previous digit has to be rounded up and so on.\r\n for (--baseOut; ++xc[--d] > baseOut;) {\r\n xc[d] = 0;\r\n\r\n if (!d) {\r\n ++e;\r\n xc = [1].concat(xc);\r\n }\r\n }\r\n }\r\n\r\n // Determine trailing zeros.\r\n for (k = xc.length; !xc[--k];);\r\n\r\n // E.g. [4, 11, 15] becomes 4bf.\r\n for (i = 0, str = ''; i <= k; str += alphabet.charAt(xc[i++]));\r\n\r\n // Add leading zeros, decimal point and trailing zeros as required.\r\n str = toFixedPoint(str, e, alphabet.charAt(0));\r\n }\r\n\r\n // The caller will add the sign.\r\n return str;\r\n };\r\n })();\r\n\r\n\r\n // Perform division in the specified base. Called by div and convertBase.\r\n div = (function () {\r\n\r\n // Assume non-zero x and k.\r\n function multiply(x, k, base) {\r\n var m, temp, xlo, xhi,\r\n carry = 0,\r\n i = x.length,\r\n klo = k % SQRT_BASE,\r\n khi = k / SQRT_BASE | 0;\r\n\r\n for (x = x.slice(); i--;) {\r\n xlo = x[i] % SQRT_BASE;\r\n xhi = x[i] / SQRT_BASE | 0;\r\n m = khi * xlo + xhi * klo;\r\n temp = klo * xlo + ((m % SQRT_BASE) * SQRT_BASE) + carry;\r\n carry = (temp / base | 0) + (m / SQRT_BASE | 0) + khi * xhi;\r\n x[i] = temp % base;\r\n }\r\n\r\n if (carry) x = [carry].concat(x);\r\n\r\n return x;\r\n }\r\n\r\n function compare(a, b, aL, bL) {\r\n var i, cmp;\r\n\r\n if (aL != bL) {\r\n cmp = aL > bL ? 1 : -1;\r\n } else {\r\n\r\n for (i = cmp = 0; i < aL; i++) {\r\n\r\n if (a[i] != b[i]) {\r\n cmp = a[i] > b[i] ? 1 : -1;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n return cmp;\r\n }\r\n\r\n function subtract(a, b, aL, base) {\r\n var i = 0;\r\n\r\n // Subtract b from a.\r\n for (; aL--;) {\r\n a[aL] -= i;\r\n i = a[aL] < b[aL] ? 1 : 0;\r\n a[aL] = i * base + a[aL] - b[aL];\r\n }\r\n\r\n // Remove leading zeros.\r\n for (; !a[0] && a.length > 1; a.splice(0, 1));\r\n }\r\n\r\n // x: dividend, y: divisor.\r\n return function (x, y, dp, rm, base) {\r\n var cmp, e, i, more, n, prod, prodL, q, qc, rem, remL, rem0, xi, xL, yc0,\r\n yL, yz,\r\n s = x.s == y.s ? 1 : -1,\r\n xc = x.c,\r\n yc = y.c;\r\n\r\n // Either NaN, Infinity or 0?\r\n if (!xc || !xc[0] || !yc || !yc[0]) {\r\n\r\n return new BigNumber(\r\n\r\n // Return NaN if either NaN, or both Infinity or 0.\r\n !x.s || !y.s || (xc ? yc && xc[0] == yc[0] : !yc) ? NaN :\r\n\r\n // Return \u00B10 if x is \u00B10 or y is \u00B1Infinity, or return \u00B1Infinity as y is \u00B10.\r\n xc && xc[0] == 0 || !yc ? s * 0 : s / 0\r\n );\r\n }\r\n\r\n q = new BigNumber(s);\r\n qc = q.c = [];\r\n e = x.e - y.e;\r\n s = dp + e + 1;\r\n\r\n if (!base) {\r\n base = BASE;\r\n e = bitFloor(x.e / LOG_BASE) - bitFloor(y.e / LOG_BASE);\r\n s = s / LOG_BASE | 0;\r\n }\r\n\r\n // Result exponent may be one less then the current value of e.\r\n // The coefficients of the BigNumbers from convertBase may have trailing zeros.\r\n for (i = 0; yc[i] == (xc[i] || 0); i++);\r\n\r\n if (yc[i] > (xc[i] || 0)) e--;\r\n\r\n if (s < 0) {\r\n qc.push(1);\r\n more = true;\r\n } else {\r\n xL = xc.length;\r\n yL = yc.length;\r\n i = 0;\r\n s += 2;\r\n\r\n // Normalise xc and yc so highest order digit of yc is >= base / 2.\r\n\r\n n = mathfloor(base / (yc[0] + 1));\r\n\r\n // Not necessary, but to handle odd bases where yc[0] == (base / 2) - 1.\r\n // if (n > 1 || n++ == 1 && yc[0] < base / 2) {\r\n if (n > 1) {\r\n yc = multiply(yc, n, base);\r\n xc = multiply(xc, n, base);\r\n yL = yc.length;\r\n xL = xc.length;\r\n }\r\n\r\n xi = yL;\r\n rem = xc.slice(0, yL);\r\n remL = rem.length;\r\n\r\n // Add zeros to make remainder as long as divisor.\r\n for (; remL < yL; rem[remL++] = 0);\r\n yz = yc.slice();\r\n yz = [0].concat(yz);\r\n yc0 = yc[0];\r\n if (yc[1] >= base / 2) yc0++;\r\n // Not necessary, but to prevent trial digit n > base, when using base 3.\r\n // else if (base == 3 && yc0 == 1) yc0 = 1 + 1e-15;\r\n\r\n do {\r\n n = 0;\r\n\r\n // Compare divisor and remainder.\r\n cmp = compare(yc, rem, yL, remL);\r\n\r\n // If divisor < remainder.\r\n if (cmp < 0) {\r\n\r\n // Calculate trial digit, n.\r\n\r\n rem0 = rem[0];\r\n if (yL != remL) rem0 = rem0 * base + (rem[1] || 0);\r\n\r\n // n is how many times the divisor goes into the current remainder.\r\n n = mathfloor(rem0 / yc0);\r\n\r\n // Algorithm:\r\n // product = divisor multiplied by trial digit (n).\r\n // Compare product and remainder.\r\n // If product is greater than remainder:\r\n // Subtract divisor from product, decrement trial digit.\r\n // Subtract product from remainder.\r\n // If product was less than remainder at the last compare:\r\n // Compare new remainder and divisor.\r\n // If remainder is greater than divisor:\r\n // Subtract divisor from remainder, increment trial digit.\r\n\r\n if (n > 1) {\r\n\r\n // n may be > base only when base is 3.\r\n if (n >= base) n = base - 1;\r\n\r\n // product = divisor * trial digit.\r\n prod = multiply(yc, n, base);\r\n prodL = prod.length;\r\n remL = rem.length;\r\n\r\n // Compare product and remainder.\r\n // If product > remainder then trial digit n too high.\r\n // n is 1 too high about 5% of the time, and is not known to have\r\n // ever been more than 1 too high.\r\n while (compare(prod, rem, prodL, remL) == 1) {\r\n n--;\r\n\r\n // Subtract divisor from product.\r\n subtract(prod, yL < prodL ? yz : yc, prodL, base);\r\n prodL = prod.length;\r\n cmp = 1;\r\n }\r\n } else {\r\n\r\n // n is 0 or 1, cmp is -1.\r\n // If n is 0, there is no need to compare yc and rem again below,\r\n // so change cmp to 1 to avoid it.\r\n // If n is 1, leave cmp as -1, so yc and rem are compared again.\r\n if (n == 0) {\r\n\r\n // divisor < remainder, so n must be at least 1.\r\n cmp = n = 1;\r\n }\r\n\r\n // product = divisor\r\n prod = yc.slice();\r\n prodL = prod.length;\r\n }\r\n\r\n if (prodL < remL) prod = [0].concat(prod);\r\n\r\n // Subtract product from remainder.\r\n subtract(rem, prod, remL, base);\r\n remL = rem.length;\r\n\r\n // If product was < remainder.\r\n if (cmp == -1) {\r\n\r\n // Compare divisor and new remainder.\r\n // If divisor < new remainder, subtract divisor from remainder.\r\n // Trial digit n too low.\r\n // n is 1 too low about 5% of the time, and very rarely 2 too low.\r\n while (compare(yc, rem, yL, remL) < 1) {\r\n n++;\r\n\r\n // Subtract divisor from remainder.\r\n subtract(rem, yL < remL ? yz : yc, remL, base);\r\n remL = rem.length;\r\n }\r\n }\r\n } else if (cmp === 0) {\r\n n++;\r\n rem = [0];\r\n } // else cmp === 1 and n will be 0\r\n\r\n // Add the next digit, n, to the result array.\r\n qc[i++] = n;\r\n\r\n // Update the remainder.\r\n if (rem[0]) {\r\n rem[remL++] = xc[xi] || 0;\r\n } else {\r\n rem = [xc[xi]];\r\n remL = 1;\r\n }\r\n } while ((xi++ < xL || rem[0] != null) && s--);\r\n\r\n more = rem[0] != null;\r\n\r\n // Leading zero?\r\n if (!qc[0]) qc.splice(0, 1);\r\n }\r\n\r\n if (base == BASE) {\r\n\r\n // To calculate q.e, first get the number of digits of qc[0].\r\n for (i = 1, s = qc[0]; s >= 10; s /= 10, i++);\r\n\r\n round(q, dp + (q.e = i + e * LOG_BASE - 1) + 1, rm, more);\r\n\r\n // Caller is convertBase.\r\n } else {\r\n q.e = e;\r\n q.r = +more;\r\n }\r\n\r\n return q;\r\n };\r\n })();\r\n\r\n\r\n /*\r\n * Return a string representing the value of BigNumber n in fixed-point or exponential\r\n * notation rounded to the specified decimal places or significant digits.\r\n *\r\n * n: a BigNumber.\r\n * i: the index of the last digit required (i.e. the digit that may be rounded up).\r\n * rm: the rounding mode.\r\n * id: 1 (toExponential) or 2 (toPrecision).\r\n */\r\n function format(n, i, rm, id) {\r\n var c0, e, ne, len, str;\r\n\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n\r\n if (!n.c) return n.toString();\r\n\r\n c0 = n.c[0];\r\n ne = n.e;\r\n\r\n if (i == null) {\r\n str = coeffToString(n.c);\r\n str = id == 1 || id == 2 && (ne <= TO_EXP_NEG || ne >= TO_EXP_POS)\r\n ? toExponential(str, ne)\r\n : toFixedPoint(str, ne, '0');\r\n } else {\r\n n = round(new BigNumber(n), i, rm);\r\n\r\n // n.e may have changed if the value was rounded up.\r\n e = n.e;\r\n\r\n str = coeffToString(n.c);\r\n len = str.length;\r\n\r\n // toPrecision returns exponential notation if the number of significant digits\r\n // specified is less than the number of digits necessary to represent the integer\r\n // part of the value in fixed-point notation.\r\n\r\n // Exponential notation.\r\n if (id == 1 || id == 2 && (i <= e || e <= TO_EXP_NEG)) {\r\n\r\n // Append zeros?\r\n for (; len < i; str += '0', len++);\r\n str = toExponential(str, e);\r\n\r\n // Fixed-point notation.\r\n } else {\r\n i -= ne + (id === 2 && e > ne);\r\n str = toFixedPoint(str, e, '0');\r\n\r\n // Append zeros?\r\n if (e + 1 > len) {\r\n if (--i > 0) for (str += '.'; i--; str += '0');\r\n } else {\r\n i += e - len;\r\n if (i > 0) {\r\n if (e + 1 == len) str += '.';\r\n for (; i--; str += '0');\r\n }\r\n }\r\n }\r\n }\r\n\r\n return n.s < 0 && c0 ? '-' + str : str;\r\n }\r\n\r\n\r\n // Handle BigNumber.max and BigNumber.min.\r\n // If any number is NaN, return NaN.\r\n function maxOrMin(args, n) {\r\n var k, y,\r\n i = 1,\r\n x = new BigNumber(args[0]);\r\n\r\n for (; i < args.length; i++) {\r\n y = new BigNumber(args[i]);\r\n if (!y.s || (k = compare(x, y)) === n || k === 0 && x.s === n) {\r\n x = y;\r\n }\r\n }\r\n\r\n return x;\r\n }\r\n\r\n\r\n /*\r\n * Strip trailing zeros, calculate base 10 exponent and check against MIN_EXP and MAX_EXP.\r\n * Called by minus, plus and times.\r\n */\r\n function normalise(n, c, e) {\r\n var i = 1,\r\n j = c.length;\r\n\r\n // Remove trailing zeros.\r\n for (; !c[--j]; c.pop());\r\n\r\n // Calculate the base 10 exponent. First get the number of digits of c[0].\r\n for (j = c[0]; j >= 10; j /= 10, i++);\r\n\r\n // Overflow?\r\n if ((e = i + e * LOG_BASE - 1) > MAX_EXP) {\r\n\r\n // Infinity.\r\n n.c = n.e = null;\r\n\r\n // Underflow?\r\n } else if (e < MIN_EXP) {\r\n\r\n // Zero.\r\n n.c = [n.e = 0];\r\n } else {\r\n n.e = e;\r\n n.c = c;\r\n }\r\n\r\n return n;\r\n }\r\n\r\n\r\n // Handle values that fail the validity test in BigNumber.\r\n parseNumeric = (function () {\r\n var basePrefix = /^(-?)0([xbo])(?=\\w[\\w.]*$)/i,\r\n dotAfter = /^([^.]+)\\.$/,\r\n dotBefore = /^\\.([^.]+)$/,\r\n isInfinityOrNaN = /^-?(Infinity|NaN)$/,\r\n whitespaceOrPlus = /^\\s*\\+(?=[\\w.])|^\\s+|\\s+$/g;\r\n\r\n return function (x, str, isNum, b) {\r\n var base,\r\n s = isNum ? str : str.replace(whitespaceOrPlus, '');\r\n\r\n // No exception on \u00B1Infinity or NaN.\r\n if (isInfinityOrNaN.test(s)) {\r\n x.s = isNaN(s) ? null : s < 0 ? -1 : 1;\r\n } else {\r\n if (!isNum) {\r\n\r\n // basePrefix = /^(-?)0([xbo])(?=\\w[\\w.]*$)/i\r\n s = s.replace(basePrefix, function (m, p1, p2) {\r\n base = (p2 = p2.toLowerCase()) == 'x' ? 16 : p2 == 'b' ? 2 : 8;\r\n return !b || b == base ? p1 : m;\r\n });\r\n\r\n if (b) {\r\n base = b;\r\n\r\n // E.g. '1.' to '1', '.1' to '0.1'\r\n s = s.replace(dotAfter, '$1').replace(dotBefore, '0.$1');\r\n }\r\n\r\n if (str != s) return new BigNumber(s, base);\r\n }\r\n\r\n // '[BigNumber Error] Not a number: {n}'\r\n // '[BigNumber Error] Not a base {b} number: {n}'\r\n if (BigNumber.DEBUG) {\r\n throw Error\r\n (bignumberError + 'Not a' + (b ? ' base ' + b : '') + ' number: ' + str);\r\n }\r\n\r\n // NaN\r\n x.s = null;\r\n }\r\n\r\n x.c = x.e = null;\r\n }\r\n })();\r\n\r\n\r\n /*\r\n * Round x to sd significant digits using rounding mode rm. Check for over/under-flow.\r\n * If r is truthy, it is known that there are more digits after the rounding digit.\r\n */\r\n function round(x, sd, rm, r) {\r\n var d, i, j, k, n, ni, rd,\r\n xc = x.c,\r\n pows10 = POWS_TEN;\r\n\r\n // if x is not Infinity or NaN...\r\n if (xc) {\r\n\r\n // rd is the rounding digit, i.e. the digit after the digit that may be rounded up.\r\n // n is a base 1e14 number, the value of the element of array x.c containing rd.\r\n // ni is the index of n within x.c.\r\n // d is the number of digits of n.\r\n // i is the index of rd within n including leading zeros.\r\n // j is the actual index of rd within n (if < 0, rd is a leading zero).\r\n out: {\r\n\r\n // Get the number of digits of the first element of xc.\r\n for (d = 1, k = xc[0]; k >= 10; k /= 10, d++);\r\n i = sd - d;\r\n\r\n // If the rounding digit is in the first element of xc...\r\n if (i < 0) {\r\n i += LOG_BASE;\r\n j = sd;\r\n n = xc[ni = 0];\r\n\r\n // Get the rounding digit at index j of n.\r\n rd = mathfloor(n / pows10[d - j - 1] % 10);\r\n } else {\r\n ni = mathceil((i + 1) / LOG_BASE);\r\n\r\n if (ni >= xc.length) {\r\n\r\n if (r) {\r\n\r\n // Needed by sqrt.\r\n for (; xc.length <= ni; xc.push(0));\r\n n = rd = 0;\r\n d = 1;\r\n i %= LOG_BASE;\r\n j = i - LOG_BASE + 1;\r\n } else {\r\n break out;\r\n }\r\n } else {\r\n n = k = xc[ni];\r\n\r\n // Get the number of digits of n.\r\n for (d = 1; k >= 10; k /= 10, d++);\r\n\r\n // Get the index of rd within n.\r\n i %= LOG_BASE;\r\n\r\n // Get the index of rd within n, adjusted for leading zeros.\r\n // The number of leading zeros of n is given by LOG_BASE - d.\r\n j = i - LOG_BASE + d;\r\n\r\n // Get the rounding digit at index j of n.\r\n rd = j < 0 ? 0 : mathfloor(n / pows10[d - j - 1] % 10);\r\n }\r\n }\r\n\r\n r = r || sd < 0 ||\r\n\r\n // Are there any non-zero digits after the rounding digit?\r\n // The expression n % pows10[d - j - 1] returns all digits of n to the right\r\n // of the digit at j, e.g. if n is 908714 and j is 2, the expression gives 714.\r\n xc[ni + 1] != null || (j < 0 ? n : n % pows10[d - j - 1]);\r\n\r\n r = rm < 4\r\n ? (rd || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2))\r\n : rd > 5 || rd == 5 && (rm == 4 || r || rm == 6 &&\r\n\r\n // Check whether the digit to the left of the rounding digit is odd.\r\n ((i > 0 ? j > 0 ? n / pows10[d - j] : 0 : xc[ni - 1]) % 10) & 1 ||\r\n rm == (x.s < 0 ? 8 : 7));\r\n\r\n if (sd < 1 || !xc[0]) {\r\n xc.length = 0;\r\n\r\n if (r) {\r\n\r\n // Convert sd to decimal places.\r\n sd -= x.e + 1;\r\n\r\n // 1, 0.1, 0.01, 0.001, 0.0001 etc.\r\n xc[0] = pows10[(LOG_BASE - sd % LOG_BASE) % LOG_BASE];\r\n x.e = -sd || 0;\r\n } else {\r\n\r\n // Zero.\r\n xc[0] = x.e = 0;\r\n }\r\n\r\n return x;\r\n }\r\n\r\n // Remove excess digits.\r\n if (i == 0) {\r\n xc.length = ni;\r\n k = 1;\r\n ni--;\r\n } else {\r\n xc.length = ni + 1;\r\n k = pows10[LOG_BASE - i];\r\n\r\n // E.g. 56700 becomes 56000 if 7 is the rounding digit.\r\n // j > 0 means i > number of leading zeros of n.\r\n xc[ni] = j > 0 ? mathfloor(n / pows10[d - j] % pows10[j]) * k : 0;\r\n }\r\n\r\n // Round up?\r\n if (r) {\r\n\r\n for (; ;) {\r\n\r\n // If the digit to be rounded up is in the first element of xc...\r\n if (ni == 0) {\r\n\r\n // i will be the length of xc[0] before k is added.\r\n for (i = 1, j = xc[0]; j >= 10; j /= 10, i++);\r\n j = xc[0] += k;\r\n for (k = 1; j >= 10; j /= 10, k++);\r\n\r\n // if i != k the length has increased.\r\n if (i != k) {\r\n x.e++;\r\n if (xc[0] == BASE) xc[0] = 1;\r\n }\r\n\r\n break;\r\n } else {\r\n xc[ni] += k;\r\n if (xc[ni] != BASE) break;\r\n xc[ni--] = 0;\r\n k = 1;\r\n }\r\n }\r\n }\r\n\r\n // Remove trailing zeros.\r\n for (i = xc.length; xc[--i] === 0; xc.pop());\r\n }\r\n\r\n // Overflow? Infinity.\r\n if (x.e > MAX_EXP) {\r\n x.c = x.e = null;\r\n\r\n // Underflow? Zero.\r\n } else if (x.e < MIN_EXP) {\r\n x.c = [x.e = 0];\r\n }\r\n }\r\n\r\n return x;\r\n }\r\n\r\n\r\n function valueOf(n) {\r\n var str,\r\n e = n.e;\r\n\r\n if (e === null) return n.toString();\r\n\r\n str = coeffToString(n.c);\r\n\r\n str = e <= TO_EXP_NEG || e >= TO_EXP_POS\r\n ? toExponential(str, e)\r\n : toFixedPoint(str, e, '0');\r\n\r\n return n.s < 0 ? '-' + str : str;\r\n }\r\n\r\n\r\n // PROTOTYPE/INSTANCE METHODS\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the absolute value of this BigNumber.\r\n */\r\n P.absoluteValue = P.abs = function () {\r\n var x = new BigNumber(this);\r\n if (x.s < 0) x.s = 1;\r\n return x;\r\n };\r\n\r\n\r\n /*\r\n * Return\r\n * 1 if the value of this BigNumber is greater than the value of BigNumber(y, b),\r\n * -1 if the value of this BigNumber is less than the value of BigNumber(y, b),\r\n * 0 if they have the same value,\r\n * or null if the value of either is NaN.\r\n */\r\n P.comparedTo = function (y, b) {\r\n return compare(this, new BigNumber(y, b));\r\n };\r\n\r\n\r\n /*\r\n * If dp is undefined or null or true or false, return the number of decimal places of the\r\n * value of this BigNumber, or null if the value of this BigNumber is \u00B1Infinity or NaN.\r\n *\r\n * Otherwise, if dp is a number, return a new BigNumber whose value is the value of this\r\n * BigNumber rounded to a maximum of dp decimal places using rounding mode rm, or\r\n * ROUNDING_MODE if rm is omitted.\r\n *\r\n * [dp] {number} Decimal places: integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n */\r\n P.decimalPlaces = P.dp = function (dp, rm) {\r\n var c, n, v,\r\n x = this;\r\n\r\n if (dp != null) {\r\n intCheck(dp, 0, MAX);\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n\r\n return round(new BigNumber(x), dp + x.e + 1, rm);\r\n }\r\n\r\n if (!(c = x.c)) return null;\r\n n = ((v = c.length - 1) - bitFloor(this.e / LOG_BASE)) * LOG_BASE;\r\n\r\n // Subtract the number of trailing zeros of the last number.\r\n if (v = c[v]) for (; v % 10 == 0; v /= 10, n--);\r\n if (n < 0) n = 0;\r\n\r\n return n;\r\n };\r\n\r\n\r\n /*\r\n * n / 0 = I\r\n * n / N = N\r\n * n / I = 0\r\n * 0 / n = 0\r\n * 0 / 0 = N\r\n * 0 / N = N\r\n * 0 / I = 0\r\n * N / n = N\r\n * N / 0 = N\r\n * N / N = N\r\n * N / I = N\r\n * I / n = I\r\n * I / 0 = I\r\n * I / N = N\r\n * I / I = N\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber divided by the value of\r\n * BigNumber(y, b), rounded according to DECIMAL_PLACES and ROUNDING_MODE.\r\n */\r\n P.dividedBy = P.div = function (y, b) {\r\n return div(this, new BigNumber(y, b), DECIMAL_PLACES, ROUNDING_MODE);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the integer part of dividing the value of this\r\n * BigNumber by the value of BigNumber(y, b).\r\n */\r\n P.dividedToIntegerBy = P.idiv = function (y, b) {\r\n return div(this, new BigNumber(y, b), 0, 1);\r\n };\r\n\r\n\r\n /*\r\n * Return a BigNumber whose value is the value of this BigNumber exponentiated by n.\r\n *\r\n * If m is present, return the result modulo m.\r\n * If n is negative round according to DECIMAL_PLACES and ROUNDING_MODE.\r\n * If POW_PRECISION is non-zero and m is not present, round to POW_PRECISION using ROUNDING_MODE.\r\n *\r\n * The modular power operation works efficiently when x, n, and m are integers, otherwise it\r\n * is equivalent to calculating x.exponentiatedBy(n).modulo(m) with a POW_PRECISION of 0.\r\n *\r\n * n {number|string|BigNumber} The exponent. An integer.\r\n * [m] {number|string|BigNumber} The modulus.\r\n *\r\n * '[BigNumber Error] Exponent not an integer: {n}'\r\n */\r\n P.exponentiatedBy = P.pow = function (n, m) {\r\n var half, isModExp, i, k, more, nIsBig, nIsNeg, nIsOdd, y,\r\n x = this;\r\n\r\n n = new BigNumber(n);\r\n\r\n // Allow NaN and \u00B1Infinity, but not other non-integers.\r\n if (n.c && !n.isInteger()) {\r\n throw Error\r\n (bignumberError + 'Exponent not an integer: ' + valueOf(n));\r\n }\r\n\r\n if (m != null) m = new BigNumber(m);\r\n\r\n // Exponent of MAX_SAFE_INTEGER is 15.\r\n nIsBig = n.e > 14;\r\n\r\n // If x is NaN, \u00B1Infinity, \u00B10 or \u00B11, or n is \u00B1Infinity, NaN or \u00B10.\r\n if (!x.c || !x.c[0] || x.c[0] == 1 && !x.e && x.c.length == 1 || !n.c || !n.c[0]) {\r\n\r\n // The sign of the result of pow when x is negative depends on the evenness of n.\r\n // If +n overflows to \u00B1Infinity, the evenness of n would be not be known.\r\n y = new BigNumber(Math.pow(+valueOf(x), nIsBig ? n.s * (2 - isOdd(n)) : +valueOf(n)));\r\n return m ? y.mod(m) : y;\r\n }\r\n\r\n nIsNeg = n.s < 0;\r\n\r\n if (m) {\r\n\r\n // x % m returns NaN if abs(m) is zero, or m is NaN.\r\n if (m.c ? !m.c[0] : !m.s) return new BigNumber(NaN);\r\n\r\n isModExp = !nIsNeg && x.isInteger() && m.isInteger();\r\n\r\n if (isModExp) x = x.mod(m);\r\n\r\n // Overflow to \u00B1Infinity: >=2**1e10 or >=1.0000024**1e15.\r\n // Underflow to \u00B10: <=0.79**1e10 or <=0.9999975**1e15.\r\n } else if (n.e > 9 && (x.e > 0 || x.e < -1 || (x.e == 0\r\n // [1, 240000000]\r\n ? x.c[0] > 1 || nIsBig && x.c[1] >= 24e7\r\n // [80000000000000] [99999750000000]\r\n : x.c[0] < 8e13 || nIsBig && x.c[0] <= 9999975e7))) {\r\n\r\n // If x is negative and n is odd, k = -0, else k = 0.\r\n k = x.s < 0 && isOdd(n) ? -0 : 0;\r\n\r\n // If x >= 1, k = \u00B1Infinity.\r\n if (x.e > -1) k = 1 / k;\r\n\r\n // If n is negative return \u00B10, else return \u00B1Infinity.\r\n return new BigNumber(nIsNeg ? 1 / k : k);\r\n\r\n } else if (POW_PRECISION) {\r\n\r\n // Truncating each coefficient array to a length of k after each multiplication\r\n // equates to truncating significant digits to POW_PRECISION + [28, 41],\r\n // i.e. there will be a minimum of 28 guard digits retained.\r\n k = mathceil(POW_PRECISION / LOG_BASE + 2);\r\n }\r\n\r\n if (nIsBig) {\r\n half = new BigNumber(0.5);\r\n if (nIsNeg) n.s = 1;\r\n nIsOdd = isOdd(n);\r\n } else {\r\n i = Math.abs(+valueOf(n));\r\n nIsOdd = i % 2;\r\n }\r\n\r\n y = new BigNumber(ONE);\r\n\r\n // Performs 54 loop iterations for n of 9007199254740991.\r\n for (; ;) {\r\n\r\n if (nIsOdd) {\r\n y = y.times(x);\r\n if (!y.c) break;\r\n\r\n if (k) {\r\n if (y.c.length > k) y.c.length = k;\r\n } else if (isModExp) {\r\n y = y.mod(m); //y = y.minus(div(y, m, 0, MODULO_MODE).times(m));\r\n }\r\n }\r\n\r\n if (i) {\r\n i = mathfloor(i / 2);\r\n if (i === 0) break;\r\n nIsOdd = i % 2;\r\n } else {\r\n n = n.times(half);\r\n round(n, n.e + 1, 1);\r\n\r\n if (n.e > 14) {\r\n nIsOdd = isOdd(n);\r\n } else {\r\n i = +valueOf(n);\r\n if (i === 0) break;\r\n nIsOdd = i % 2;\r\n }\r\n }\r\n\r\n x = x.times(x);\r\n\r\n if (k) {\r\n if (x.c && x.c.length > k) x.c.length = k;\r\n } else if (isModExp) {\r\n x = x.mod(m); //x = x.minus(div(x, m, 0, MODULO_MODE).times(m));\r\n }\r\n }\r\n\r\n if (isModExp) return y;\r\n if (nIsNeg) y = ONE.div(y);\r\n\r\n return m ? y.mod(m) : k ? round(y, POW_PRECISION, ROUNDING_MODE, more) : y;\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the value of this BigNumber rounded to an integer\r\n * using rounding mode rm, or ROUNDING_MODE if rm is omitted.\r\n *\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {rm}'\r\n */\r\n P.integerValue = function (rm) {\r\n var n = new BigNumber(this);\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n return round(n, n.e + 1, rm);\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is equal to the value of BigNumber(y, b),\r\n * otherwise return false.\r\n */\r\n P.isEqualTo = P.eq = function (y, b) {\r\n return compare(this, new BigNumber(y, b)) === 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is a finite number, otherwise return false.\r\n */\r\n P.isFinite = function () {\r\n return !!this.c;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is greater than the value of BigNumber(y, b),\r\n * otherwise return false.\r\n */\r\n P.isGreaterThan = P.gt = function (y, b) {\r\n return compare(this, new BigNumber(y, b)) > 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is greater than or equal to the value of\r\n * BigNumber(y, b), otherwise return false.\r\n */\r\n P.isGreaterThanOrEqualTo = P.gte = function (y, b) {\r\n return (b = compare(this, new BigNumber(y, b))) === 1 || b === 0;\r\n\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is an integer, otherwise return false.\r\n */\r\n P.isInteger = function () {\r\n return !!this.c && bitFloor(this.e / LOG_BASE) > this.c.length - 2;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is less than the value of BigNumber(y, b),\r\n * otherwise return false.\r\n */\r\n P.isLessThan = P.lt = function (y, b) {\r\n return compare(this, new BigNumber(y, b)) < 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is less than or equal to the value of\r\n * BigNumber(y, b), otherwise return false.\r\n */\r\n P.isLessThanOrEqualTo = P.lte = function (y, b) {\r\n return (b = compare(this, new BigNumber(y, b))) === -1 || b === 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is NaN, otherwise return false.\r\n */\r\n P.isNaN = function () {\r\n return !this.s;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is negative, otherwise return false.\r\n */\r\n P.isNegative = function () {\r\n return this.s < 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is positive, otherwise return false.\r\n */\r\n P.isPositive = function () {\r\n return this.s > 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is 0 or -0, otherwise return false.\r\n */\r\n P.isZero = function () {\r\n return !!this.c && this.c[0] == 0;\r\n };\r\n\r\n\r\n /*\r\n * n - 0 = n\r\n * n - N = N\r\n * n - I = -I\r\n * 0 - n = -n\r\n * 0 - 0 = 0\r\n * 0 - N = N\r\n * 0 - I = -I\r\n * N - n = N\r\n * N - 0 = N\r\n * N - N = N\r\n * N - I = N\r\n * I - n = I\r\n * I - 0 = I\r\n * I - N = N\r\n * I - I = N\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber minus the value of\r\n * BigNumber(y, b).\r\n */\r\n P.minus = function (y, b) {\r\n var i, j, t, xLTy,\r\n x = this,\r\n a = x.s;\r\n\r\n y = new BigNumber(y, b);\r\n b = y.s;\r\n\r\n // Either NaN?\r\n if (!a || !b) return new BigNumber(NaN);\r\n\r\n // Signs differ?\r\n if (a != b) {\r\n y.s = -b;\r\n return x.plus(y);\r\n }\r\n\r\n var xe = x.e / LOG_BASE,\r\n ye = y.e / LOG_BASE,\r\n xc = x.c,\r\n yc = y.c;\r\n\r\n if (!xe || !ye) {\r\n\r\n // Either Infinity?\r\n if (!xc || !yc) return xc ? (y.s = -b, y) : new BigNumber(yc ? x : NaN);\r\n\r\n // Either zero?\r\n if (!xc[0] || !yc[0]) {\r\n\r\n // Return y if y is non-zero, x if x is non-zero, or zero if both are zero.\r\n return yc[0] ? (y.s = -b, y) : new BigNumber(xc[0] ? x :\r\n\r\n // IEEE 754 (2008) 6.3: n - n = -0 when rounding to -Infinity\r\n ROUNDING_MODE == 3 ? -0 : 0);\r\n }\r\n }\r\n\r\n xe = bitFloor(xe);\r\n ye = bitFloor(ye);\r\n xc = xc.slice();\r\n\r\n // Determine which is the bigger number.\r\n if (a = xe - ye) {\r\n\r\n if (xLTy = a < 0) {\r\n a = -a;\r\n t = xc;\r\n } else {\r\n ye = xe;\r\n t = yc;\r\n }\r\n\r\n t.reverse();\r\n\r\n // Prepend zeros to equalise exponents.\r\n for (b = a; b--; t.push(0));\r\n t.reverse();\r\n } else {\r\n\r\n // Exponents equal. Check digit by digit.\r\n j = (xLTy = (a = xc.length) < (b = yc.length)) ? a : b;\r\n\r\n for (a = b = 0; b < j; b++) {\r\n\r\n if (xc[b] != yc[b]) {\r\n xLTy = xc[b] < yc[b];\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // x < y? Point xc to the array of the bigger number.\r\n if (xLTy) {\r\n t = xc;\r\n xc = yc;\r\n yc = t;\r\n y.s = -y.s;\r\n }\r\n\r\n b = (j = yc.length) - (i = xc.length);\r\n\r\n // Append zeros to xc if shorter.\r\n // No need to add zeros to yc if shorter as subtract only needs to start at yc.length.\r\n if (b > 0) for (; b--; xc[i++] = 0);\r\n b = BASE - 1;\r\n\r\n // Subtract yc from xc.\r\n for (; j > a;) {\r\n\r\n if (xc[--j] < yc[j]) {\r\n for (i = j; i && !xc[--i]; xc[i] = b);\r\n --xc[i];\r\n xc[j] += BASE;\r\n }\r\n\r\n xc[j] -= yc[j];\r\n }\r\n\r\n // Remove leading zeros and adjust exponent accordingly.\r\n for (; xc[0] == 0; xc.splice(0, 1), --ye);\r\n\r\n // Zero?\r\n if (!xc[0]) {\r\n\r\n // Following IEEE 754 (2008) 6.3,\r\n // n - n = +0 but n - n = -0 when rounding towards -Infinity.\r\n y.s = ROUNDING_MODE == 3 ? -1 : 1;\r\n y.c = [y.e = 0];\r\n return y;\r\n }\r\n\r\n // No need to check for Infinity as +x - +y != Infinity && -x - -y != Infinity\r\n // for finite x and y.\r\n return normalise(y, xc, ye);\r\n };\r\n\r\n\r\n /*\r\n * n % 0 = N\r\n * n % N = N\r\n * n % I = n\r\n * 0 % n = 0\r\n * -0 % n = -0\r\n * 0 % 0 = N\r\n * 0 % N = N\r\n * 0 % I = 0\r\n * N % n = N\r\n * N % 0 = N\r\n * N % N = N\r\n * N % I = N\r\n * I % n = N\r\n * I % 0 = N\r\n * I % N = N\r\n * I % I = N\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber modulo the value of\r\n * BigNumber(y, b). The result depends on the value of MODULO_MODE.\r\n */\r\n P.modulo = P.mod = function (y, b) {\r\n var q, s,\r\n x = this;\r\n\r\n y = new BigNumber(y, b);\r\n\r\n // Return NaN if x is Infinity or NaN, or y is NaN or zero.\r\n if (!x.c || !y.s || y.c && !y.c[0]) {\r\n return new BigNumber(NaN);\r\n\r\n // Return x if y is Infinity or x is zero.\r\n } else if (!y.c || x.c && !x.c[0]) {\r\n return new BigNumber(x);\r\n }\r\n\r\n if (MODULO_MODE == 9) {\r\n\r\n // Euclidian division: q = sign(y) * floor(x / abs(y))\r\n // r = x - qy where 0 <= r < abs(y)\r\n s = y.s;\r\n y.s = 1;\r\n q = div(x, y, 0, 3);\r\n y.s = s;\r\n q.s *= s;\r\n } else {\r\n q = div(x, y, 0, MODULO_MODE);\r\n }\r\n\r\n y = x.minus(q.times(y));\r\n\r\n // To match JavaScript %, ensure sign of zero is sign of dividend.\r\n if (!y.c[0] && MODULO_MODE == 1) y.s = x.s;\r\n\r\n return y;\r\n };\r\n\r\n\r\n /*\r\n * n * 0 = 0\r\n * n * N = N\r\n * n * I = I\r\n * 0 * n = 0\r\n * 0 * 0 = 0\r\n * 0 * N = N\r\n * 0 * I = N\r\n * N * n = N\r\n * N * 0 = N\r\n * N * N = N\r\n * N * I = N\r\n * I * n = I\r\n * I * 0 = N\r\n * I * N = N\r\n * I * I = I\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber multiplied by the value\r\n * of BigNumber(y, b).\r\n */\r\n P.multipliedBy = P.times = function (y, b) {\r\n var c, e, i, j, k, m, xcL, xlo, xhi, ycL, ylo, yhi, zc,\r\n base, sqrtBase,\r\n x = this,\r\n xc = x.c,\r\n yc = (y = new BigNumber(y, b)).c;\r\n\r\n // Either NaN, \u00B1Infinity or \u00B10?\r\n if (!xc || !yc || !xc[0] || !yc[0]) {\r\n\r\n // Return NaN if either is NaN, or one is 0 and the other is Infinity.\r\n if (!x.s || !y.s || xc && !xc[0] && !yc || yc && !yc[0] && !xc) {\r\n y.c = y.e = y.s = null;\r\n } else {\r\n y.s *= x.s;\r\n\r\n // Return \u00B1Infinity if either is \u00B1Infinity.\r\n if (!xc || !yc) {\r\n y.c = y.e = null;\r\n\r\n // Return \u00B10 if either is \u00B10.\r\n } else {\r\n y.c = [0];\r\n y.e = 0;\r\n }\r\n }\r\n\r\n return y;\r\n }\r\n\r\n e = bitFloor(x.e / LOG_BASE) + bitFloor(y.e / LOG_BASE);\r\n y.s *= x.s;\r\n xcL = xc.length;\r\n ycL = yc.length;\r\n\r\n // Ensure xc points to longer array and xcL to its length.\r\n if (xcL < ycL) {\r\n zc = xc;\r\n xc = yc;\r\n yc = zc;\r\n i = xcL;\r\n xcL = ycL;\r\n ycL = i;\r\n }\r\n\r\n // Initialise the result array with zeros.\r\n for (i = xcL + ycL, zc = []; i--; zc.push(0));\r\n\r\n base = BASE;\r\n sqrtBase = SQRT_BASE;\r\n\r\n for (i = ycL; --i >= 0;) {\r\n c = 0;\r\n ylo = yc[i] % sqrtBase;\r\n yhi = yc[i] / sqrtBase | 0;\r\n\r\n for (k = xcL, j = i + k; j > i;) {\r\n xlo = xc[--k] % sqrtBase;\r\n xhi = xc[k] / sqrtBase | 0;\r\n m = yhi * xlo + xhi * ylo;\r\n xlo = ylo * xlo + ((m % sqrtBase) * sqrtBase) + zc[j] + c;\r\n c = (xlo / base | 0) + (m / sqrtBase | 0) + yhi * xhi;\r\n zc[j--] = xlo % base;\r\n }\r\n\r\n zc[j] = c;\r\n }\r\n\r\n if (c) {\r\n ++e;\r\n } else {\r\n zc.splice(0, 1);\r\n }\r\n\r\n return normalise(y, zc, e);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the value of this BigNumber negated,\r\n * i.e. multiplied by -1.\r\n */\r\n P.negated = function () {\r\n var x = new BigNumber(this);\r\n x.s = -x.s || null;\r\n return x;\r\n };\r\n\r\n\r\n /*\r\n * n + 0 = n\r\n * n + N = N\r\n * n + I = I\r\n * 0 + n = n\r\n * 0 + 0 = 0\r\n * 0 + N = N\r\n * 0 + I = I\r\n * N + n = N\r\n * N + 0 = N\r\n * N + N = N\r\n * N + I = N\r\n * I + n = I\r\n * I + 0 = I\r\n * I + N = N\r\n * I + I = I\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber plus the value of\r\n * BigNumber(y, b).\r\n */\r\n P.plus = function (y, b) {\r\n var t,\r\n x = this,\r\n a = x.s;\r\n\r\n y = new BigNumber(y, b);\r\n b = y.s;\r\n\r\n // Either NaN?\r\n if (!a || !b) return new BigNumber(NaN);\r\n\r\n // Signs differ?\r\n if (a != b) {\r\n y.s = -b;\r\n return x.minus(y);\r\n }\r\n\r\n var xe = x.e / LOG_BASE,\r\n ye = y.e / LOG_BASE,\r\n xc = x.c,\r\n yc = y.c;\r\n\r\n if (!xe || !ye) {\r\n\r\n // Return \u00B1Infinity if either \u00B1Infinity.\r\n if (!xc || !yc) return new BigNumber(a / 0);\r\n\r\n // Either zero?\r\n // Return y if y is non-zero, x if x is non-zero, or zero if both are zero.\r\n if (!xc[0] || !yc[0]) return yc[0] ? y : new BigNumber(xc[0] ? x : a * 0);\r\n }\r\n\r\n xe = bitFloor(xe);\r\n ye = bitFloor(ye);\r\n xc = xc.slice();\r\n\r\n // Prepend zeros to equalise exponents. Faster to use reverse then do unshifts.\r\n if (a = xe - ye) {\r\n if (a > 0) {\r\n ye = xe;\r\n t = yc;\r\n } else {\r\n a = -a;\r\n t = xc;\r\n }\r\n\r\n t.reverse();\r\n for (; a--; t.push(0));\r\n t.reverse();\r\n }\r\n\r\n a = xc.length;\r\n b = yc.length;\r\n\r\n // Point xc to the longer array, and b to the shorter length.\r\n if (a - b < 0) {\r\n t = yc;\r\n yc = xc;\r\n xc = t;\r\n b = a;\r\n }\r\n\r\n // Only start adding at yc.length - 1 as the further digits of xc can be ignored.\r\n for (a = 0; b;) {\r\n a = (xc[--b] = xc[b] + yc[b] + a) / BASE | 0;\r\n xc[b] = BASE === xc[b] ? 0 : xc[b] % BASE;\r\n }\r\n\r\n if (a) {\r\n xc = [a].concat(xc);\r\n ++ye;\r\n }\r\n\r\n // No need to check for zero, as +x + +y != 0 && -x + -y != 0\r\n // ye = MAX_EXP + 1 possible\r\n return normalise(y, xc, ye);\r\n };\r\n\r\n\r\n /*\r\n * If sd is undefined or null or true or false, return the number of significant digits of\r\n * the value of this BigNumber, or null if the value of this BigNumber is \u00B1Infinity or NaN.\r\n * If sd is true include integer-part trailing zeros in the count.\r\n *\r\n * Otherwise, if sd is a number, return a new BigNumber whose value is the value of this\r\n * BigNumber rounded to a maximum of sd significant digits using rounding mode rm, or\r\n * ROUNDING_MODE if rm is omitted.\r\n *\r\n * sd {number|boolean} number: significant digits: integer, 1 to MAX inclusive.\r\n * boolean: whether to count integer-part trailing zeros: true or false.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}'\r\n */\r\n P.precision = P.sd = function (sd, rm) {\r\n var c, n, v,\r\n x = this;\r\n\r\n if (sd != null && sd !== !!sd) {\r\n intCheck(sd, 1, MAX);\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n\r\n return round(new BigNumber(x), sd, rm);\r\n }\r\n\r\n if (!(c = x.c)) return null;\r\n v = c.length - 1;\r\n n = v * LOG_BASE + 1;\r\n\r\n if (v = c[v]) {\r\n\r\n // Subtract the number of trailing zeros of the last element.\r\n for (; v % 10 == 0; v /= 10, n--);\r\n\r\n // Add the number of digits of the first element.\r\n for (v = c[0]; v >= 10; v /= 10, n++);\r\n }\r\n\r\n if (sd && x.e + 1 > n) n = x.e + 1;\r\n\r\n return n;\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the value of this BigNumber shifted by k places\r\n * (powers of 10). Shift to the right if n > 0, and to the left if n < 0.\r\n *\r\n * k {number} Integer, -MAX_SAFE_INTEGER to MAX_SAFE_INTEGER inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {k}'\r\n */\r\n P.shiftedBy = function (k) {\r\n intCheck(k, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);\r\n return this.times('1e' + k);\r\n };\r\n\r\n\r\n /*\r\n * sqrt(-n) = N\r\n * sqrt(N) = N\r\n * sqrt(-I) = N\r\n * sqrt(I) = I\r\n * sqrt(0) = 0\r\n * sqrt(-0) = -0\r\n *\r\n * Return a new BigNumber whose value is the square root of the value of this BigNumber,\r\n * rounded according to DECIMAL_PLACES and ROUNDING_MODE.\r\n */\r\n P.squareRoot = P.sqrt = function () {\r\n var m, n, r, rep, t,\r\n x = this,\r\n c = x.c,\r\n s = x.s,\r\n e = x.e,\r\n dp = DECIMAL_PLACES + 4,\r\n half = new BigNumber('0.5');\r\n\r\n // Negative/NaN/Infinity/zero?\r\n if (s !== 1 || !c || !c[0]) {\r\n return new BigNumber(!s || s < 0 && (!c || c[0]) ? NaN : c ? x : 1 / 0);\r\n }\r\n\r\n // Initial estimate.\r\n s = Math.sqrt(+valueOf(x));\r\n\r\n // Math.sqrt underflow/overflow?\r\n // Pass x to Math.sqrt as integer, then adjust the exponent of the result.\r\n if (s == 0 || s == 1 / 0) {\r\n n = coeffToString(c);\r\n if ((n.length + e) % 2 == 0) n += '0';\r\n s = Math.sqrt(+n);\r\n e = bitFloor((e + 1) / 2) - (e < 0 || e % 2);\r\n\r\n if (s == 1 / 0) {\r\n n = '5e' + e;\r\n } else {\r\n n = s.toExponential();\r\n n = n.slice(0, n.indexOf('e') + 1) + e;\r\n }\r\n\r\n r = new BigNumber(n);\r\n } else {\r\n r = new BigNumber(s + '');\r\n }\r\n\r\n // Check for zero.\r\n // r could be zero if MIN_EXP is changed after the this value was created.\r\n // This would cause a division by zero (x/t) and hence Infinity below, which would cause\r\n // coeffToString to throw.\r\n if (r.c[0]) {\r\n e = r.e;\r\n s = e + dp;\r\n if (s < 3) s = 0;\r\n\r\n // Newton-Raphson iteration.\r\n for (; ;) {\r\n t = r;\r\n r = half.times(t.plus(div(x, t, dp, 1)));\r\n\r\n if (coeffToString(t.c).slice(0, s) === (n = coeffToString(r.c)).slice(0, s)) {\r\n\r\n // The exponent of r may here be one less than the final result exponent,\r\n // e.g 0.0009999 (e-4) --> 0.001 (e-3), so adjust s so the rounding digits\r\n // are indexed correctly.\r\n if (r.e < e) --s;\r\n n = n.slice(s - 3, s + 1);\r\n\r\n // The 4th rounding digit may be in error by -1 so if the 4 rounding digits\r\n // are 9999 or 4999 (i.e. approaching a rounding boundary) continue the\r\n // iteration.\r\n if (n == '9999' || !rep && n == '4999') {\r\n\r\n // On the first iteration only, check to see if rounding up gives the\r\n // exact result as the nines may infinitely repeat.\r\n if (!rep) {\r\n round(t, t.e + DECIMAL_PLACES + 2, 0);\r\n\r\n if (t.times(t).eq(x)) {\r\n r = t;\r\n break;\r\n }\r\n }\r\n\r\n dp += 4;\r\n s += 4;\r\n rep = 1;\r\n } else {\r\n\r\n // If rounding digits are null, 0{0,4} or 50{0,3}, check for exact\r\n // result. If not, then there are further digits and m will be truthy.\r\n if (!+n || !+n.slice(1) && n.charAt(0) == '5') {\r\n\r\n // Truncate to the first rounding digit.\r\n round(r, r.e + DECIMAL_PLACES + 2, 1);\r\n m = !r.times(r).eq(x);\r\n }\r\n\r\n break;\r\n }\r\n }\r\n }\r\n }\r\n\r\n return round(r, r.e + DECIMAL_PLACES + 1, ROUNDING_MODE, m);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in exponential notation and\r\n * rounded using ROUNDING_MODE to dp fixed decimal places.\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n */\r\n P.toExponential = function (dp, rm) {\r\n if (dp != null) {\r\n intCheck(dp, 0, MAX);\r\n dp++;\r\n }\r\n return format(this, dp, rm, 1);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in fixed-point notation rounding\r\n * to dp fixed decimal places using rounding mode rm, or ROUNDING_MODE if rm is omitted.\r\n *\r\n * Note: as with JavaScript's number type, (-0).toFixed(0) is '0',\r\n * but e.g. (-0.00001).toFixed(0) is '-0'.\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n */\r\n P.toFixed = function (dp, rm) {\r\n if (dp != null) {\r\n intCheck(dp, 0, MAX);\r\n dp = dp + this.e + 1;\r\n }\r\n return format(this, dp, rm);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in fixed-point notation rounded\r\n * using rm or ROUNDING_MODE to dp decimal places, and formatted according to the properties\r\n * of the format or FORMAT object (see BigNumber.set).\r\n *\r\n * The formatting object may contain some or all of the properties shown below.\r\n *\r\n * FORMAT = {\r\n * prefix: '',\r\n * groupSize: 3,\r\n * secondaryGroupSize: 0,\r\n * groupSeparator: ',',\r\n * decimalSeparator: '.',\r\n * fractionGroupSize: 0,\r\n * fractionGroupSeparator: '\\xA0', // non-breaking space\r\n * suffix: ''\r\n * };\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n * [format] {object} Formatting options. See FORMAT pbject above.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n * '[BigNumber Error] Argument not an object: {format}'\r\n */\r\n P.toFormat = function (dp, rm, format) {\r\n var str,\r\n x = this;\r\n\r\n if (format == null) {\r\n if (dp != null && rm && typeof rm == 'object') {\r\n format = rm;\r\n rm = null;\r\n } else if (dp && typeof dp == 'object') {\r\n format = dp;\r\n dp = rm = null;\r\n } else {\r\n format = FORMAT;\r\n }\r\n } else if (typeof format != 'object') {\r\n throw Error\r\n (bignumberError + 'Argument not an object: ' + format);\r\n }\r\n\r\n str = x.toFixed(dp, rm);\r\n\r\n if (x.c) {\r\n var i,\r\n arr = str.split('.'),\r\n g1 = +format.groupSize,\r\n g2 = +format.secondaryGroupSize,\r\n groupSeparator = format.groupSeparator || '',\r\n intPart = arr[0],\r\n fractionPart = arr[1],\r\n isNeg = x.s < 0,\r\n intDigits = isNeg ? intPart.slice(1) : intPart,\r\n len = intDigits.length;\r\n\r\n if (g2) {\r\n i = g1;\r\n g1 = g2;\r\n g2 = i;\r\n len -= i;\r\n }\r\n\r\n if (g1 > 0 && len > 0) {\r\n i = len % g1 || g1;\r\n intPart = intDigits.substr(0, i);\r\n for (; i < len; i += g1) intPart += groupSeparator + intDigits.substr(i, g1);\r\n if (g2 > 0) intPart += groupSeparator + intDigits.slice(i);\r\n if (isNeg) intPart = '-' + intPart;\r\n }\r\n\r\n str = fractionPart\r\n ? intPart + (format.decimalSeparator || '') + ((g2 = +format.fractionGroupSize)\r\n ? fractionPart.replace(new RegExp('\\\\d{' + g2 + '}\\\\B', 'g'),\r\n '$&' + (format.fractionGroupSeparator || ''))\r\n : fractionPart)\r\n : intPart;\r\n }\r\n\r\n return (format.prefix || '') + str + (format.suffix || '');\r\n };\r\n\r\n\r\n /*\r\n * Return an array of two BigNumbers representing the value of this BigNumber as a simple\r\n * fraction with an integer numerator and an integer denominator.\r\n * The denominator will be a positive non-zero value less than or equal to the specified\r\n * maximum denominator. If a maximum denominator is not specified, the denominator will be\r\n * the lowest value necessary to represent the number exactly.\r\n *\r\n * [md] {number|string|BigNumber} Integer >= 1, or Infinity. The maximum denominator.\r\n *\r\n * '[BigNumber Error] Argument {not an integer|out of range} : {md}'\r\n */\r\n P.toFraction = function (md) {\r\n var d, d0, d1, d2, e, exp, n, n0, n1, q, r, s,\r\n x = this,\r\n xc = x.c;\r\n\r\n if (md != null) {\r\n n = new BigNumber(md);\r\n\r\n // Throw if md is less than one or is not an integer, unless it is Infinity.\r\n if (!n.isInteger() && (n.c || n.s !== 1) || n.lt(ONE)) {\r\n throw Error\r\n (bignumberError + 'Argument ' +\r\n (n.isInteger() ? 'out of range: ' : 'not an integer: ') + valueOf(n));\r\n }\r\n }\r\n\r\n if (!xc) return new BigNumber(x);\r\n\r\n d = new BigNumber(ONE);\r\n n1 = d0 = new BigNumber(ONE);\r\n d1 = n0 = new BigNumber(ONE);\r\n s = coeffToString(xc);\r\n\r\n // Determine initial denominator.\r\n // d is a power of 10 and the minimum max denominator that specifies the value exactly.\r\n e = d.e = s.length - x.e - 1;\r\n d.c[0] = POWS_TEN[(exp = e % LOG_BASE) < 0 ? LOG_BASE + exp : exp];\r\n md = !md || n.comparedTo(d) > 0 ? (e > 0 ? d : n1) : n;\r\n\r\n exp = MAX_EXP;\r\n MAX_EXP = 1 / 0;\r\n n = new BigNumber(s);\r\n\r\n // n0 = d1 = 0\r\n n0.c[0] = 0;\r\n\r\n for (; ;) {\r\n q = div(n, d, 0, 1);\r\n d2 = d0.plus(q.times(d1));\r\n if (d2.comparedTo(md) == 1) break;\r\n d0 = d1;\r\n d1 = d2;\r\n n1 = n0.plus(q.times(d2 = n1));\r\n n0 = d2;\r\n d = n.minus(q.times(d2 = d));\r\n n = d2;\r\n }\r\n\r\n d2 = div(md.minus(d0), d1, 0, 1);\r\n n0 = n0.plus(d2.times(n1));\r\n d0 = d0.plus(d2.times(d1));\r\n n0.s = n1.s = x.s;\r\n e = e * 2;\r\n\r\n // Determine which fraction is closer to x, n0/d0 or n1/d1\r\n r = div(n1, d1, e, ROUNDING_MODE).minus(x).abs().comparedTo(\r\n div(n0, d0, e, ROUNDING_MODE).minus(x).abs()) < 1 ? [n1, d1] : [n0, d0];\r\n\r\n MAX_EXP = exp;\r\n\r\n return r;\r\n };\r\n\r\n\r\n /*\r\n * Return the value of this BigNumber converted to a number primitive.\r\n */\r\n P.toNumber = function () {\r\n return +valueOf(this);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber rounded to sd significant digits\r\n * using rounding mode rm or ROUNDING_MODE. If sd is less than the number of digits\r\n * necessary to represent the integer part of the value in fixed-point notation, then use\r\n * exponential notation.\r\n *\r\n * [sd] {number} Significant digits. Integer, 1 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}'\r\n */\r\n P.toPrecision = function (sd, rm) {\r\n if (sd != null) intCheck(sd, 1, MAX);\r\n return format(this, sd, rm, 2);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in base b, or base 10 if b is\r\n * omitted. If a base is specified, including base 10, round according to DECIMAL_PLACES and\r\n * ROUNDING_MODE. If a base is not specified, and this BigNumber has a positive exponent\r\n * that is equal to or greater than TO_EXP_POS, or a negative exponent equal to or less than\r\n * TO_EXP_NEG, return exponential notation.\r\n *\r\n * [b] {number} Integer, 2 to ALPHABET.length inclusive.\r\n *\r\n * '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}'\r\n */\r\n P.toString = function (b) {\r\n var str,\r\n n = this,\r\n s = n.s,\r\n e = n.e;\r\n\r\n // Infinity or NaN?\r\n if (e === null) {\r\n if (s) {\r\n str = 'Infinity';\r\n if (s < 0) str = '-' + str;\r\n } else {\r\n str = 'NaN';\r\n }\r\n } else {\r\n if (b == null) {\r\n str = e <= TO_EXP_NEG || e >= TO_EXP_POS\r\n ? toExponential(coeffToString(n.c), e)\r\n : toFixedPoint(coeffToString(n.c), e, '0');\r\n } else if (b === 10 && alphabetHasNormalDecimalDigits) {\r\n n = round(new BigNumber(n), DECIMAL_PLACES + e + 1, ROUNDING_MODE);\r\n str = toFixedPoint(coeffToString(n.c), n.e, '0');\r\n } else {\r\n intCheck(b, 2, ALPHABET.length, 'Base');\r\n str = convertBase(toFixedPoint(coeffToString(n.c), e, '0'), 10, b, s, true);\r\n }\r\n\r\n if (s < 0 && n.c[0]) str = '-' + str;\r\n }\r\n\r\n return str;\r\n };\r\n\r\n\r\n /*\r\n * Return as toString, but do not accept a base argument, and include the minus sign for\r\n * negative zero.\r\n */\r\n P.valueOf = P.toJSON = function () {\r\n return valueOf(this);\r\n };\r\n\r\n\r\n P._isBigNumber = true;\r\n\r\n P[Symbol.toStringTag] = 'BigNumber';\r\n\r\n // Node.js v10.12.0+\r\n P[Symbol.for('nodejs.util.inspect.custom')] = P.valueOf;\r\n\r\n if (configObject != null) BigNumber.set(configObject);\r\n\r\n return BigNumber;\r\n}\r\n\r\n\r\n// PRIVATE HELPER FUNCTIONS\r\n\r\n// These functions don't need access to variables,\r\n// e.g. DECIMAL_PLACES, in the scope of the `clone` function above.\r\n\r\n\r\nfunction bitFloor(n) {\r\n var i = n | 0;\r\n return n > 0 || n === i ? i : i - 1;\r\n}\r\n\r\n\r\n// Return a coefficient array as a string of base 10 digits.\r\nfunction coeffToString(a) {\r\n var s, z,\r\n i = 1,\r\n j = a.length,\r\n r = a[0] + '';\r\n\r\n for (; i < j;) {\r\n s = a[i++] + '';\r\n z = LOG_BASE - s.length;\r\n for (; z--; s = '0' + s);\r\n r += s;\r\n }\r\n\r\n // Determine trailing zeros.\r\n for (j = r.length; r.charCodeAt(--j) === 48;);\r\n\r\n return r.slice(0, j + 1 || 1);\r\n}\r\n\r\n\r\n// Compare the value of BigNumbers x and y.\r\nfunction compare(x, y) {\r\n var a, b,\r\n xc = x.c,\r\n yc = y.c,\r\n i = x.s,\r\n j = y.s,\r\n k = x.e,\r\n l = y.e;\r\n\r\n // Either NaN?\r\n if (!i || !j) return null;\r\n\r\n a = xc && !xc[0];\r\n b = yc && !yc[0];\r\n\r\n // Either zero?\r\n if (a || b) return a ? b ? 0 : -j : i;\r\n\r\n // Signs differ?\r\n if (i != j) return i;\r\n\r\n a = i < 0;\r\n b = k == l;\r\n\r\n // Either Infinity?\r\n if (!xc || !yc) return b ? 0 : !xc ^ a ? 1 : -1;\r\n\r\n // Compare exponents.\r\n if (!b) return k > l ^ a ? 1 : -1;\r\n\r\n j = (k = xc.length) < (l = yc.length) ? k : l;\r\n\r\n // Compare digit by digit.\r\n for (i = 0; i < j; i++) if (xc[i] != yc[i]) return xc[i] > yc[i] ^ a ? 1 : -1;\r\n\r\n // Compare lengths.\r\n return k == l ? 0 : k > l ^ a ? 1 : -1;\r\n}\r\n\r\n\r\n/*\r\n * Check that n is a primitive number, an integer, and in range, otherwise throw.\r\n */\r\nfunction intCheck(n, min, max, name) {\r\n if (n < min || n > max || n !== mathfloor(n)) {\r\n throw Error\r\n (bignumberError + (name || 'Argument') + (typeof n == 'number'\r\n ? n < min || n > max ? ' out of range: ' : ' not an integer: '\r\n : ' not a primitive number: ') + String(n));\r\n }\r\n}\r\n\r\n\r\n// Assumes finite n.\r\nfunction isOdd(n) {\r\n var k = n.c.length - 1;\r\n return bitFloor(n.e / LOG_BASE) == k && n.c[k] % 2 != 0;\r\n}\r\n\r\n\r\nfunction toExponential(str, e) {\r\n return (str.length > 1 ? str.charAt(0) + '.' + str.slice(1) : str) +\r\n (e < 0 ? 'e' : 'e+') + e;\r\n}\r\n\r\n\r\nfunction toFixedPoint(str, e, z) {\r\n var len, zs;\r\n\r\n // Negative exponent?\r\n if (e < 0) {\r\n\r\n // Prepend zeros.\r\n for (zs = z + '.'; ++e; zs += z);\r\n str = zs + str;\r\n\r\n // Positive exponent\r\n } else {\r\n len = str.length;\r\n\r\n // Append zeros.\r\n if (++e > len) {\r\n for (zs = z, e -= len; --e; zs += z);\r\n str += zs;\r\n } else if (e < len) {\r\n str = str.slice(0, e) + '.' + str.slice(e);\r\n }\r\n }\r\n\r\n return str;\r\n}\r\n\r\n\r\n// EXPORT\r\n\r\n\r\nexport var BigNumber = clone();\r\n\r\nexport default BigNumber;\r\n", "type Comparator = (a: T, b: T) => number;\ntype Predicate = (value: T) => boolean;\n\nclass SplayTreeNode> {\n readonly key: K;\n\n left: Node | null = null;\n right: Node | null = null;\n\n constructor(key: K) {\n this.key = key;\n }\n}\n\nclass SplayTreeSetNode extends SplayTreeNode> {\n constructor(key: K) {\n super(key);\n }\n}\n\nclass SplayTreeMapNode extends SplayTreeNode> {\n readonly value: V;\n\n constructor(key: K, value: V) {\n super(key);\n this.value = value;\n }\n\n replaceValue(value: V) {\n const node = new SplayTreeMapNode(this.key, value);\n node.left = this.left;\n node.right = this.right;\n return node;\n }\n}\n\nabstract class SplayTree> {\n protected abstract root: Node | null;\n\n public size = 0;\n\n protected modificationCount = 0;\n\n protected splayCount = 0;\n\n protected abstract compare: Comparator;\n\n protected abstract validKey: Predicate;\n\n protected splay(key: K) {\n const root = this.root;\n if (root == null) {\n this.compare(key, key);\n return -1;\n }\n\n let right: Node | null = null;\n let newTreeRight: Node | null = null;\n let left: Node | null = null;\n let newTreeLeft: Node | null = null;\n let current = root;\n const compare = this.compare;\n let comp: number;\n while (true) {\n comp = compare(current.key, key);\n if (comp > 0) {\n let currentLeft = current.left;\n if (currentLeft == null) break;\n comp = compare(currentLeft.key, key);\n if (comp > 0) {\n current.left = currentLeft.right;\n currentLeft.right = current;\n current = currentLeft;\n currentLeft = current.left;\n if (currentLeft == null) break;\n }\n if (right == null) {\n newTreeRight = current;\n } else {\n right.left = current;\n }\n right = current;\n current = currentLeft;\n } else if (comp < 0) {\n let currentRight = current.right;\n if (currentRight == null) break;\n comp = compare(currentRight.key, key);\n if (comp < 0) {\n current.right = currentRight.left;\n currentRight.left = current;\n current = currentRight;\n currentRight = current.right;\n if (currentRight == null) break;\n }\n if (left == null) {\n newTreeLeft = current;\n } else {\n left.right = current;\n }\n left = current;\n current = currentRight;\n } else {\n break;\n }\n }\n if (left != null) {\n left.right = current.left;\n current.left = newTreeLeft;\n }\n if (right != null) {\n right.left = current.right;\n current.right = newTreeRight;\n }\n if (this.root !== current) {\n this.root = current;\n this.splayCount++;\n }\n return comp;\n }\n\n protected splayMin(node: Node) {\n let current = node;\n let nextLeft = current.left;\n while (nextLeft != null) {\n const left = nextLeft;\n current.left = left.right;\n left.right = current;\n current = left;\n nextLeft = current.left;\n }\n return current;\n }\n\n protected splayMax(node: Node) {\n let current = node;\n let nextRight = current.right;\n while (nextRight != null) {\n const right = nextRight;\n current.right = right.left;\n right.left = current;\n current = right;\n nextRight = current.right;\n }\n return current;\n }\n\n protected _delete(key: K) {\n if (this.root == null) return null;\n const comp = this.splay(key);\n if (comp != 0) return null;\n let root = this.root;\n const result = root;\n const left = root.left;\n this.size--;\n if (left == null) {\n this.root = root.right;\n } else {\n const right = root.right;\n root = this.splayMax(left);\n\n root.right = right;\n this.root = root;\n }\n this.modificationCount++;\n return result;\n }\n\n protected addNewRoot(node: Node, comp: number) {\n this.size++;\n this.modificationCount++;\n const root = this.root;\n if (root == null) {\n this.root = node;\n return;\n }\n if (comp < 0) {\n node.left = root;\n node.right = root.right;\n root.right = null;\n } else {\n node.right = root;\n node.left = root.left;\n root.left = null;\n }\n this.root = node;\n }\n\n protected _first() {\n const root = this.root;\n if (root == null) return null;\n this.root = this.splayMin(root);\n return this.root;\n }\n\n protected _last() {\n const root = this.root;\n if (root == null) return null;\n this.root = this.splayMax(root);\n return this.root;\n }\n\n public clear() {\n this.root = null;\n this.size = 0;\n this.modificationCount++;\n }\n\n public has(key: unknown) {\n return this.validKey(key) && this.splay(key as K) == 0;\n }\n\n protected defaultCompare(): Comparator {\n return (a: K, b: K) => a < b ? -1 : a > b ? 1 : 0;\n }\n\n protected wrap(): SplayTreeWrapper {\n return {\n getRoot: () => { return this.root },\n setRoot: (root) => { this.root = root },\n getSize: () => { return this.size },\n getModificationCount: () => { return this.modificationCount },\n getSplayCount: () => { return this.splayCount },\n setSplayCount: (count) => { this.splayCount = count },\n splay: (key) => { return this.splay(key) },\n has: (key) => { return this.has(key) },\n };\n }\n}\n\nexport class SplayTreeMap extends SplayTree> implements Iterable<[K, V]>, Map {\n protected root: SplayTreeMapNode | null = null;\n\n protected compare: Comparator;\n protected validKey: Predicate;\n\n constructor(compare?: Comparator, isValidKey?: Predicate) {\n super();\n this.compare = compare ?? this.defaultCompare();\n this.validKey = isValidKey ?? ((a: unknown) => a != null && a != undefined);\n }\n\n delete(key: unknown) {\n if (!this.validKey(key)) return false;\n return this._delete(key as K) != null;\n }\n\n forEach(f: (value: V, key: K, map: Map) => void) {\n const nodes: Iterator<[K, V]> = new SplayTreeMapEntryIterableIterator(this.wrap());\n let result: IteratorResult<[K, V]>;\n while (result = nodes.next(), !result.done) {\n f(result.value[1], result.value[0], this);\n }\n }\n\n get(key: unknown): V | undefined {\n if (!this.validKey(key)) return undefined;\n if (this.root != null) {\n const comp = this.splay(key as K);\n if (comp == 0) {\n return this.root!.value;\n }\n }\n return undefined;\n }\n\n hasValue(value: unknown) {\n const initialSplayCount = this.splayCount;\n const visit = (node: SplayTreeMapNode | null) => {\n while (node != null) {\n if (node.value == value) return true;\n if (initialSplayCount != this.splayCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (node.right != null && visit(node.right)) {\n return true;\n }\n node = node.left;\n }\n return false;\n }\n\n return visit(this.root);\n }\n\n set(key: K, value: V) {\n const comp = this.splay(key);\n if (comp == 0) {\n this.root = this.root!.replaceValue(value);\n this.splayCount += 1;\n return this;\n }\n this.addNewRoot(new SplayTreeMapNode(key, value), comp);\n return this;\n }\n\n setAll(other: Map) {\n other.forEach((value: V, key: K) => {\n this.set(key, value);\n });\n }\n\n setIfAbsent(key: K, ifAbsent: () => V) {\n let comp = this.splay(key);\n if (comp == 0) {\n return this.root!.value;\n }\n const modificationCount = this.modificationCount;\n const splayCount = this.splayCount;\n const value = ifAbsent();\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (splayCount != this.splayCount) {\n comp = this.splay(key);\n }\n this.addNewRoot(new SplayTreeMapNode(key, value), comp);\n return value;\n }\n\n isEmpty() {\n return this.root == null;\n }\n\n isNotEmpty() {\n return !this.isEmpty();\n }\n\n firstKey() {\n if (this.root == null) return null;\n return this._first()!.key;\n }\n\n lastKey() {\n if (this.root == null) return null;\n return this._last()!.key;\n }\n\n lastKeyBefore(key: K) {\n if (key == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(key);\n if (comp < 0) return this.root!.key;\n let node: SplayTreeMapNode | null = this.root!.left;\n if (node == null) return null;\n let nodeRight = node.right;\n while (nodeRight != null) {\n node = nodeRight;\n nodeRight = node.right;\n }\n return node!.key;\n }\n\n firstKeyAfter(key: K) {\n if (key == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(key);\n if (comp > 0) return this.root!.key;\n let node: SplayTreeMapNode | null = this.root!.right;\n if (node == null) return null;\n let nodeLeft = node.left;\n while (nodeLeft != null) {\n node = nodeLeft;\n nodeLeft = node.left;\n }\n return node!.key;\n }\n\n update(key: K, update: (value: V) => V, ifAbsent?: () => V) {\n let comp = this.splay(key);\n if (comp == 0) {\n const modificationCount = this.modificationCount;\n const splayCount = this.splayCount;\n const newValue = update(this.root!.value);\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (splayCount != this.splayCount) {\n this.splay(key);\n }\n this.root = this.root!.replaceValue(newValue);\n this.splayCount += 1;\n return newValue;\n }\n if (ifAbsent != null) {\n const modificationCount = this.modificationCount;\n const splayCount = this.splayCount;\n const newValue = ifAbsent();\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (splayCount != this.splayCount) {\n comp = this.splay(key);\n }\n this.addNewRoot(new SplayTreeMapNode(key, newValue), comp);\n return newValue;\n }\n throw \"Invalid argument (key): Key not in map.\"\n }\n\n updateAll(update: (key: K, value: V) => V) {\n const root = this.root;\n if (root == null) return;\n const iterator = new SplayTreeMapEntryIterableIterator(this.wrap());\n let node: IteratorResult<[K, V]>;\n while (node = iterator.next(), !node.done) {\n const newValue = update(...node.value);\n iterator.replaceValue(newValue);\n }\n }\n\n keys(): IterableIterator {\n return new SplayTreeKeyIterableIterator>(this.wrap());\n }\n\n values(): IterableIterator {\n return new SplayTreeValueIterableIterator(this.wrap());\n }\n\n entries(): IterableIterator<[K, V]> {\n return this[Symbol.iterator]();\n }\n\n [Symbol.iterator](): IterableIterator<[K, V]> {\n return new SplayTreeMapEntryIterableIterator(this.wrap());\n }\n\n [Symbol.toStringTag] = '[object Map]'\n}\n\nexport class SplayTreeSet extends SplayTree> implements Iterable, Set {\n protected root: SplayTreeSetNode | null = null;\n\n protected compare: Comparator;\n protected validKey: Predicate;\n\n constructor(compare?: Comparator, isValidKey?: Predicate) {\n super();\n this.compare = compare ?? this.defaultCompare();\n this.validKey = isValidKey ?? ((v: unknown) => v != null && v != undefined );\n }\n\n delete(element: unknown) {\n if (!this.validKey(element)) return false;\n return this._delete(element as E) != null;\n }\n\n deleteAll(elements: Iterable) {\n for (const element of elements) {\n this.delete(element);\n }\n }\n\n forEach(f: (element: E, element2: E, set: Set) => void) {\n const nodes: Iterator = this[Symbol.iterator]();\n let result: IteratorResult;\n while (result = nodes.next(), !result.done) {\n f(result.value, result.value, this);\n }\n }\n\n add(element: E) {\n const compare = this.splay(element);\n if (compare != 0) this.addNewRoot(new SplayTreeSetNode(element), compare);\n return this;\n }\n\n addAndReturn(element: E) {\n const compare = this.splay(element);\n if (compare != 0) this.addNewRoot(new SplayTreeSetNode(element), compare);\n return this.root!.key;\n }\n\n addAll(elements: Iterable) {\n for (const element of elements) {\n this.add(element);\n }\n }\n\n isEmpty() {\n return this.root == null;\n }\n\n isNotEmpty() {\n return this.root != null;\n }\n\n single() {\n if (this.size == 0) throw \"Bad state: No element\";\n if (this.size > 1) throw \"Bad state: Too many element\";\n return this.root!.key;\n }\n\n first() {\n if (this.size == 0) throw \"Bad state: No element\";\n return this._first()!.key;\n }\n\n last() {\n if (this.size == 0) throw \"Bad state: No element\";\n return this._last()!.key;\n }\n\n lastBefore(element: E) {\n if (element == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(element);\n if (comp < 0) return this.root!.key;\n let node: SplayTreeSetNode | null = this.root!.left;\n if (node == null) return null;\n let nodeRight = node.right;\n while (nodeRight != null) {\n node = nodeRight;\n nodeRight = node.right;\n }\n return node!.key;\n }\n\n firstAfter(element: E) {\n if (element == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(element);\n if (comp > 0) return this.root!.key;\n let node: SplayTreeSetNode | null = this.root!.right;\n if (node == null) return null;\n let nodeLeft = node.left;\n while (nodeLeft != null) {\n node = nodeLeft;\n nodeLeft = node.left;\n }\n return node!.key;\n }\n\n retainAll(elements: Iterable) {\n const retainSet = new SplayTreeSet(this.compare, this.validKey);\n const modificationCount = this.modificationCount;\n for (const object of elements) {\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (this.validKey(object) && this.splay(object as E) == 0) {\n retainSet.add(this.root!.key);\n }\n }\n if (retainSet.size != this.size) {\n this.root = retainSet.root;\n this.size = retainSet.size;\n this.modificationCount++;\n }\n }\n\n lookup(object: unknown): E | null {\n if (!this.validKey(object)) return null;\n const comp = this.splay(object as E);\n if (comp != 0) return null;\n return this.root!.key;\n }\n\n intersection(other: Set): Set {\n const result = new SplayTreeSet(this.compare, this.validKey);\n for (const element of this) {\n if (other.has(element)) result.add(element);\n }\n return result;\n }\n\n difference(other: Set): Set {\n const result = new SplayTreeSet(this.compare, this.validKey);\n for (const element of this) {\n if (!other.has(element)) result.add(element);\n }\n return result;\n }\n\n union(other: Set): Set {\n const u = this.clone();\n u.addAll(other);\n return u;\n }\n\n protected clone() {\n const set = new SplayTreeSet(this.compare, this.validKey);\n set.size = this.size;\n set.root = this.copyNode>(this.root);\n return set;\n }\n\n protected copyNode>(node: Node | null) {\n if (node == null) return null;\n function copyChildren(node: Node, dest: SplayTreeSetNode) {\n let left: Node | null;\n let right: Node | null;\n do {\n left = node.left;\n right = node.right;\n if (left != null) {\n const newLeft = new SplayTreeSetNode(left.key);\n dest.left = newLeft;\n copyChildren(left, newLeft);\n }\n if (right != null) {\n const newRight = new SplayTreeSetNode(right.key);\n dest.right = newRight;\n node = right;\n dest = newRight;\n }\n } while (right != null);\n }\n\n const result = new SplayTreeSetNode(node.key);\n copyChildren(node, result);\n return result;\n }\n\n toSet(): Set {\n return this.clone();\n }\n\n entries(): IterableIterator<[E, E]> {\n return new SplayTreeSetEntryIterableIterator>(this.wrap());\n }\n\n keys(): IterableIterator {\n return this[Symbol.iterator]();\n }\n \n values(): IterableIterator {\n return this[Symbol.iterator]();\n }\n\n [Symbol.iterator](): IterableIterator {\n return new SplayTreeKeyIterableIterator>(this.wrap());\n }\n\n [Symbol.toStringTag] = '[object Set]'\n}\n\ninterface SplayTreeWrapper> {\n getRoot: () => Node | null;\n setRoot: (root: Node | null) => void;\n getSize: () => number;\n getModificationCount: () => number;\n getSplayCount: () => number;\n setSplayCount: (count: number) => void;\n splay: (key: K) => number;\n has: (key: unknown) => boolean;\n}\n\ntype SplayTreeMapWrapper = SplayTreeWrapper>;\n\nabstract class SplayTreeIterableIterator, T> implements IterableIterator {\n protected readonly tree: SplayTreeWrapper;\n\n protected readonly path = new Array();\n\n protected modificationCount: number | null = null;\n\n protected splayCount: number;\n\n constructor(tree: SplayTreeWrapper) {\n this.tree = tree;\n this.splayCount = tree.getSplayCount();\n }\n\n [Symbol.iterator](): IterableIterator {\n return this;\n }\n\n next(): IteratorResult {\n if (this.moveNext()) return { done: false, value: this.current()! }\n return { done: true, value: null }\n }\n\n protected current() {\n if (!this.path.length) return null;\n const node = this.path[this.path.length - 1];\n return this.getValue(node);\n }\n\n protected rebuildPath(key: K) {\n this.path.splice(0, this.path.length)\n this.tree.splay(key);\n this.path.push(this.tree.getRoot()!);\n this.splayCount = this.tree.getSplayCount();\n }\n\n protected findLeftMostDescendent(node: Node | null) {\n while (node != null) {\n this.path.push(node);\n node = node.left;\n }\n }\n\n protected moveNext() {\n if (this.modificationCount != this.tree.getModificationCount()) {\n if (this.modificationCount == null) {\n this.modificationCount = this.tree.getModificationCount();\n let node = this.tree.getRoot();\n while (node != null) {\n this.path.push(node);\n node = node.left;\n }\n return this.path.length > 0;\n }\n throw \"Concurrent modification during iteration.\";\n }\n if (!this.path.length) return false;\n if (this.splayCount != this.tree.getSplayCount()) {\n this.rebuildPath(this.path[this.path.length - 1].key);\n }\n let node = this.path[this.path.length - 1];\n let next = node.right;\n if (next != null) {\n while (next != null) {\n this.path.push(next);\n next = next.left;\n }\n return true;\n }\n this.path.pop();\n while (this.path.length && this.path[this.path.length - 1].right === node) {\n node = this.path.pop()!;\n }\n return this.path.length > 0;\n }\n\n protected abstract getValue(node: Node): T\n}\n\nclass SplayTreeKeyIterableIterator> extends SplayTreeIterableIterator {\n\n protected getValue(node: Node) {\n return node.key;\n }\n}\n\nclass SplayTreeSetEntryIterableIterator> extends SplayTreeIterableIterator {\n\n protected getValue(node: Node): [K, K] {\n return [node.key, node.key];\n }\n}\n\nclass SplayTreeValueIterableIterator extends SplayTreeIterableIterator, V> {\n\n constructor(map: SplayTreeMapWrapper) {\n super(map);\n }\n\n protected getValue(node: SplayTreeMapNode) {\n return node.value;\n }\n}\n\nclass SplayTreeMapEntryIterableIterator extends SplayTreeIterableIterator, [K, V]> {\n\n constructor(map: SplayTreeMapWrapper) {\n super(map);\n }\n\n protected getValue(node: SplayTreeMapNode): [K, V] {\n return [node.key, node.value];\n }\n\n replaceValue(value: V) {\n if (this.modificationCount != this.tree.getModificationCount()) {\n throw \"Concurrent modification during iteration.\";\n }\n if (this.splayCount != this.tree.getSplayCount()) {\n this.rebuildPath(this.path[this.path.length - 1].key);\n }\n const last = this.path.pop()!;\n const newLast = last.replaceValue(value);\n if (!this.path.length) {\n this.tree.setRoot(newLast);\n } else {\n const parent = this.path[this.path.length - 1];\n if (last === parent.left) {\n parent.left = newLast;\n } else {\n parent.right = newLast;\n }\n }\n this.path.push(newLast);\n const count = this.tree.getSplayCount() + 1;\n this.tree.setSplayCount(count);\n this.splayCount = count;\n }\n}", "import BigNumber from \"bignumber.js\";\nimport { Bbox } from \"./bbox.js\";\nimport { precision } from \"./precision.js\";\nimport Segment from \"./segment.js\";\nimport { Point } from \"./sweep-event.js\";\n\nexport type Ring = [number, number][]\nexport type Poly = Ring[]\nexport type MultiPoly = Poly[]\nexport type Geom = Poly | MultiPoly\n\nexport class RingIn {\n poly: PolyIn\n isExterior: boolean\n segments: Segment[]\n bbox: Bbox\n\n constructor(geomRing: Ring, poly: PolyIn, isExterior: boolean) {\n if (!Array.isArray(geomRing) || geomRing.length === 0) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n\n this.poly = poly\n this.isExterior = isExterior\n this.segments = []\n\n if (\n typeof geomRing[0][0] !== \"number\" ||\n typeof geomRing[0][1] !== \"number\"\n ) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n\n const firstPoint = precision.snap({ x: new BigNumber(geomRing[0][0]), y: new BigNumber(geomRing[0][1]) }) as Point\n this.bbox = {\n ll: { x: firstPoint.x, y: firstPoint.y },\n ur: { x: firstPoint.x, y: firstPoint.y },\n }\n\n let prevPoint = firstPoint\n for (let i = 1, iMax = geomRing.length; i < iMax; i++) {\n if (\n typeof geomRing[i][0] !== \"number\" ||\n typeof geomRing[i][1] !== \"number\"\n ) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n const point = precision.snap({ x: new BigNumber(geomRing[i][0]), y: new BigNumber(geomRing[i][1]) }) as Point\n // skip repeated points\n if (point.x.eq(prevPoint.x) && point.y.eq(prevPoint.y)) continue\n this.segments.push(Segment.fromRing(prevPoint, point, this))\n if (point.x.isLessThan(this.bbox.ll.x)) this.bbox.ll.x = point.x\n if (point.y.isLessThan(this.bbox.ll.y)) this.bbox.ll.y = point.y\n if (point.x.isGreaterThan(this.bbox.ur.x)) this.bbox.ur.x = point.x\n if (point.y.isGreaterThan(this.bbox.ur.y)) this.bbox.ur.y = point.y\n prevPoint = point\n }\n // add segment from last to first if last is not the same as first\n if (!firstPoint.x.eq(prevPoint.x) || !firstPoint.y.eq(prevPoint.y)) {\n this.segments.push(Segment.fromRing(prevPoint, firstPoint, this))\n }\n }\n\n getSweepEvents() {\n const sweepEvents = []\n for (let i = 0, iMax = this.segments.length; i < iMax; i++) {\n const segment = this.segments[i]\n sweepEvents.push(segment.leftSE)\n sweepEvents.push(segment.rightSE)\n }\n return sweepEvents\n }\n}\n\nexport class PolyIn {\n multiPoly: MultiPolyIn\n exteriorRing: RingIn\n interiorRings: RingIn[]\n bbox: Bbox\n\n constructor(geomPoly: Poly, multiPoly: MultiPolyIn) {\n if (!Array.isArray(geomPoly)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n this.exteriorRing = new RingIn(geomPoly[0], this, true)\n // copy by value\n this.bbox = {\n ll: { x: this.exteriorRing.bbox.ll.x, y: this.exteriorRing.bbox.ll.y },\n ur: { x: this.exteriorRing.bbox.ur.x, y: this.exteriorRing.bbox.ur.y },\n }\n this.interiorRings = []\n for (let i = 1, iMax = geomPoly.length; i < iMax; i++) {\n const ring = new RingIn(geomPoly[i], this, false)\n if (ring.bbox.ll.x.isLessThan(this.bbox.ll.x)) this.bbox.ll.x = ring.bbox.ll.x\n if (ring.bbox.ll.y.isLessThan(this.bbox.ll.y)) this.bbox.ll.y = ring.bbox.ll.y\n if (ring.bbox.ur.x.isGreaterThan(this.bbox.ur.x)) this.bbox.ur.x = ring.bbox.ur.x\n if (ring.bbox.ur.y.isGreaterThan(this.bbox.ur.y)) this.bbox.ur.y = ring.bbox.ur.y\n this.interiorRings.push(ring)\n }\n this.multiPoly = multiPoly\n }\n\n getSweepEvents() {\n const sweepEvents = this.exteriorRing.getSweepEvents()\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringSweepEvents = this.interiorRings[i].getSweepEvents()\n for (let j = 0, jMax = ringSweepEvents.length; j < jMax; j++) {\n sweepEvents.push(ringSweepEvents[j])\n }\n }\n return sweepEvents\n }\n}\n\nexport class MultiPolyIn {\n isSubject: boolean\n polys: PolyIn[]\n bbox: Bbox\n\n constructor(geom: Geom, isSubject: boolean) {\n if (!Array.isArray(geom)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n\n try {\n // if the input looks like a polygon, convert it to a multipolygon\n if (typeof geom[0][0][0] === \"number\") geom = [geom as Poly]\n } catch (ex) {\n // The input is either malformed or has empty arrays.\n // In either case, it will be handled later on.\n }\n\n this.polys = []\n this.bbox = {\n ll: { x: new BigNumber(Number.POSITIVE_INFINITY), y: new BigNumber(Number.POSITIVE_INFINITY) },\n ur: { x: new BigNumber(Number.NEGATIVE_INFINITY), y: new BigNumber(Number.NEGATIVE_INFINITY) },\n }\n for (let i = 0, iMax = geom.length; i < iMax; i++) {\n const poly = new PolyIn(geom[i] as Poly, this)\n if (poly.bbox.ll.x.isLessThan(this.bbox.ll.x)) this.bbox.ll.x = poly.bbox.ll.x\n if (poly.bbox.ll.y.isLessThan(this.bbox.ll.y)) this.bbox.ll.y = poly.bbox.ll.y\n if (poly.bbox.ur.x.isGreaterThan(this.bbox.ur.x)) this.bbox.ur.x = poly.bbox.ur.x\n if (poly.bbox.ur.y.isGreaterThan(this.bbox.ur.y)) this.bbox.ur.y = poly.bbox.ur.y\n this.polys.push(poly)\n }\n this.isSubject = isSubject\n }\n\n getSweepEvents() {\n const sweepEvents = []\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polySweepEvents = this.polys[i].getSweepEvents()\n for (let j = 0, jMax = polySweepEvents.length; j < jMax; j++) {\n sweepEvents.push(polySweepEvents[j])\n }\n }\n return sweepEvents\n }\n}\n", "export default (x: T) => {\n return () => {\n return x\n }\n}", "import BigNumber from \"bignumber.js\"\nimport constant from \"./constant.js\"\n\nexport default (eps?: number) => {\n const almostEqual = eps ? (a: BigNumber, b: BigNumber) =>\n b.minus(a).abs().isLessThanOrEqualTo(eps)\n : constant(false)\n\n return (a: BigNumber, b: BigNumber) => {\n if (almostEqual(a, b)) return 0\n\n return a.comparedTo(b)\n }\n}", "import BigNumber from \"bignumber.js\";\nimport constant from \"./constant.js\";\nimport { Vector } from \"./vector.js\";\n\nexport default function (eps?: number) {\n const almostCollinear = eps ? (area2: BigNumber, ax: BigNumber, ay: BigNumber, cx: BigNumber, cy: BigNumber) =>\n area2.exponentiatedBy(2).isLessThanOrEqualTo(\n cx.minus(ax).exponentiatedBy(2).plus(cy.minus(ay).exponentiatedBy(2))\n .times(eps))\n : constant(false)\n\n return (a: Vector, b: Vector, c: Vector) => {\n const ax = a.x, ay = a.y, cx = c.x, cy = c.y\n\n const area2 = ay.minus(cy).times(b.x.minus(cx)).minus(ax.minus(cx).times(b.y.minus(cy)))\n\n if (almostCollinear(area2, ax, ay, cx, cy)) return 0\n\n return area2.comparedTo(0)\n }\n}", "import BigNumber from \"bignumber.js\";\nimport { SplayTreeSet } from \"splaytree-ts\"\nimport compare from \"./compare.js\";\nimport identity from \"./identity.js\";\nimport { Vector } from \"./vector.js\";\n\nexport default (eps?: number) => {\n if (eps) {\n\n const xTree = new SplayTreeSet(compare(eps))\n const yTree = new SplayTreeSet(compare(eps))\n\n const snapCoord = (coord: BigNumber, tree: SplayTreeSet) => {\n return tree.addAndReturn(coord)\n }\n\n const snap = (v: Vector) => {\n return {\n x: snapCoord(v.x, xTree),\n y: snapCoord(v.y, yTree),\n } as Vector\n }\n\n snap({ x: new BigNumber(0), y: new BigNumber(0)})\n\n return snap\n }\n\n return identity\n}", "export default (x: T) => {\n return x;\n}", "import compare from \"./compare.js\";\nimport orient from \"./orient.js\";\nimport snap from \"./snap.js\";\n\nconst set = (eps?: number) => {\n return {\n set: (eps?: number) => { precision = set(eps) },\n reset: () => set(eps),\n compare: compare(eps),\n snap: snap(eps),\n orient: orient(eps)\n }\n}\n\nexport let precision: ReturnType = set()", "import { Vector } from \"./vector.js\";\n\nexport interface Bbox {\n ll: Vector;\n ur: Vector;\n}\n\n/**\n * A bounding box has the format:\n *\n * { ll: { x: xmin, y: ymin }, ur: { x: xmax, y: ymax } }\n *\n */\n\nexport const isInBbox = (bbox: Bbox, point: Vector) => {\n return (\n bbox.ll.x.isLessThanOrEqualTo(point.x) &&\n point.x.isLessThanOrEqualTo(bbox.ur.x) &&\n bbox.ll.y.isLessThanOrEqualTo(point.y) &&\n point.y.isLessThanOrEqualTo(bbox.ur.y) \n )\n}\n\n/* Returns either null, or a bbox (aka an ordered pair of points)\n * If there is only one point of overlap, a bbox with identical points\n * will be returned */\nexport const getBboxOverlap = (b1: Bbox, b2: Bbox) => {\n // check if the bboxes overlap at all\n if (\n b2.ur.x.isLessThan(b1.ll.x) ||\n b1.ur.x.isLessThan(b2.ll.x) ||\n b2.ur.y.isLessThan(b1.ll.y) ||\n b1.ur.y.isLessThan(b2.ll.y) \n )\n return null\n\n // find the middle two X values\n const lowerX = b1.ll.x.isLessThan(b2.ll.x) ? b2.ll.x : b1.ll.x\n const upperX = b1.ur.x.isLessThan(b2.ur.x) ? b1.ur.x : b2.ur.x\n\n // find the middle two Y values\n const lowerY = b1.ll.y.isLessThan(b2.ll.y) ? b2.ll.y : b1.ll.y\n const upperY = b1.ur.y.isLessThan(b2.ur.y) ? b1.ur.y : b2.ur.y\n\n // put those middle values together to get the overlap\n return { ll: { x: lowerX, y: lowerY }, ur: { x: upperX, y: upperY } } as Bbox\n}\n", "import { SplayTreeSet } from \"splaytree-ts\"\nimport { getBboxOverlap } from \"./bbox.js\"\nimport * as geomIn from \"./geom-in.js\"\nimport { Geom } from \"./geom-in.js\"\nimport * as geomOut from \"./geom-out.js\"\nimport { precision } from \"./precision.js\"\nimport SweepEvent from \"./sweep-event.js\"\nimport SweepLine from \"./sweep-line.js\"\n\nexport class Operation {\n type!: string\n numMultiPolys!: number\n\n run(type: string, geom: Geom, moreGeoms: Geom[]) {\n operation.type = type\n\n /* Convert inputs to MultiPoly objects */\n const multipolys = [new geomIn.MultiPolyIn(geom, true)]\n for (let i = 0, iMax = moreGeoms.length; i < iMax; i++) {\n multipolys.push(new geomIn.MultiPolyIn(moreGeoms[i], false))\n }\n operation.numMultiPolys = multipolys.length\n\n /* BBox optimization for difference operation\n * If the bbox of a multipolygon that's part of the clipping doesn't\n * intersect the bbox of the subject at all, we can just drop that\n * multiploygon. */\n if (operation.type === \"difference\") {\n // in place removal\n const subject = multipolys[0]\n let i = 1\n while (i < multipolys.length) {\n if (getBboxOverlap(multipolys[i].bbox, subject.bbox) !== null) i++\n else multipolys.splice(i, 1)\n }\n }\n\n /* BBox optimization for intersection operation\n * If we can find any pair of multipolygons whose bbox does not overlap,\n * then the result will be empty. */\n if (operation.type === \"intersection\") {\n // TODO: this is O(n^2) in number of polygons. By sorting the bboxes,\n // it could be optimized to O(n * ln(n))\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const mpA = multipolys[i]\n for (let j = i + 1, jMax = multipolys.length; j < jMax; j++) {\n if (getBboxOverlap(mpA.bbox, multipolys[j].bbox) === null) return []\n }\n }\n }\n\n /* Put segment endpoints in a priority queue */\n const queue = new SplayTreeSet(SweepEvent.compare)\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const sweepEvents = multipolys[i].getSweepEvents()\n for (let j = 0, jMax = sweepEvents.length; j < jMax; j++) {\n queue.add(sweepEvents[j])\n }\n }\n\n /* Pass the sweep line over those endpoints */\n const sweepLine = new SweepLine(queue)\n let evt = null\n if (queue.size != 0) {\n evt = queue.first()\n queue.delete(evt)\n }\n while (evt) {\n const newEvents = sweepLine.process(evt)\n for (let i = 0, iMax = newEvents.length; i < iMax; i++) {\n const evt = newEvents[i]\n if (evt.consumedBy === undefined) queue.add(evt)\n }\n if (queue.size != 0) {\n evt = queue.first()\n queue.delete(evt)\n } else {\n evt = null;\n }\n }\n\n // free some memory we don't need anymore\n precision.reset()\n\n /* Collect and compile segments we're keeping into a multipolygon */\n const ringsOut = geomOut.RingOut.factory(sweepLine.segments)\n const result = new geomOut.MultiPolyOut(ringsOut)\n return result.getGeom()\n }\n}\n\n// singleton available by import\nconst operation = new Operation()\n\nexport default operation\n", "import * as bn from \"bignumber.js\";\n\nexport interface Vector {\n x: bn.BigNumber;\n y: bn.BigNumber;\n}\n\n/* Cross Product of two vectors with first point at origin */\nexport const crossProduct = (a: Vector, b: Vector) => a.x.times(b.y).minus(a.y.times(b.x))\n\n/* Dot Product of two vectors with first point at origin */\nexport const dotProduct = (a: Vector, b: Vector) => a.x.times(b.x).plus(a.y.times(b.y))\n\nexport const length = (v: Vector) => dotProduct(v, v).sqrt()\n\n/* Get the sine of the angle from pShared -> pAngle to pShaed -> pBase */\nexport const sineOfAngle = (pShared: Vector, pBase: Vector, pAngle: Vector) => {\n const vBase = { x: pBase.x.minus(pShared.x), y: pBase.y.minus(pShared.y) }\n const vAngle = { x: pAngle.x.minus(pShared.x), y: pAngle.y.minus(pShared.y) }\n return crossProduct(vAngle, vBase).div(length(vAngle)).div(length(vBase))\n}\n\n/* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */\nexport const cosineOfAngle = (pShared: Vector, pBase: Vector, pAngle: Vector) => {\n const vBase = { x: pBase.x.minus(pShared.x), y: pBase.y.minus(pShared.y) }\n const vAngle = { x: pAngle.x.minus(pShared.x), y: pAngle.y.minus(pShared.y) }\n return dotProduct(vAngle, vBase).div(length(vAngle)).div(length(vBase))\n}\n\n/* Get the x coordinate where the given line (defined by a point and vector)\n * crosses the horizontal line with the given y coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\nexport const horizontalIntersection = (pt: Vector, v: Vector, y: bn.BigNumber) => {\n if (v.y.isZero()) return null\n return { x: pt.x.plus((v.x.div(v.y)).times(y.minus(pt.y))), y: y }\n}\n\n/* Get the y coordinate where the given line (defined by a point and vector)\n * crosses the vertical line with the given x coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\nexport const verticalIntersection = (pt: Vector, v: Vector, x: bn.BigNumber) => {\n if (v.x.isZero()) return null\n return { x: x, y: pt.y.plus((v.y.div(v.x)).times(x.minus(pt.x))) }\n}\n\n/* Get the intersection of two lines, each defined by a base point and a vector.\n * In the case of parrallel lines (including overlapping ones) returns null. */\nexport const intersection = (pt1: Vector, v1: Vector, pt2: Vector, v2: Vector) => {\n // take some shortcuts for vertical and horizontal lines\n // this also ensures we don't calculate an intersection and then discover\n // it's actually outside the bounding box of the line\n if (v1.x.isZero()) return verticalIntersection(pt2, v2, pt1.x)\n if (v2.x.isZero()) return verticalIntersection(pt1, v1, pt2.x)\n if (v1.y.isZero()) return horizontalIntersection(pt2, v2, pt1.y)\n if (v2.y.isZero()) return horizontalIntersection(pt1, v1, pt2.y)\n\n // General case for non-overlapping segments.\n // This algorithm is based on Schneider and Eberly.\n // http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf - pg 244\n\n const kross = crossProduct(v1, v2)\n if (kross.isZero()) return null\n\n const ve = { x: pt2.x.minus(pt1.x), y: pt2.y.minus(pt1.y) }\n const d1 = crossProduct(ve, v1).div(kross)\n const d2 = crossProduct(ve, v2).div(kross)\n\n // take the average of the two calculations to minimize rounding error\n const x1 = pt1.x.plus(d2.times(v1.x)),\n x2 = pt2.x.plus(d1.times(v2.x))\n const y1 = pt1.y.plus(d2.times(v1.y)),\n y2 = pt2.y.plus(d1.times(v2.y))\n const x = x1.plus(x2).div(2)\n const y = y1.plus(y2).div(2)\n return { x: x, y: y } as Vector\n}\n\n/* Given a vector, return one that is perpendicular */\nexport const perpendicular = (v: Vector) => {\n return { x: v.y.negated(), y: v.x }\n}", "import BigNumber from \"bignumber.js\";\nimport Segment from \"./segment.js\"\nimport { cosineOfAngle, sineOfAngle, Vector } from \"./vector.js\"\n\nexport interface Point extends Vector {\n events: SweepEvent[];\n}\n\nexport default class SweepEvent {\n point: Point;\n isLeft: boolean;\n segment!: Segment;\n otherSE!: SweepEvent;\n consumedBy: SweepEvent | undefined;\n\n // for ordering sweep events in the sweep event queue\n static compare(a: SweepEvent, b: SweepEvent) {\n // favor event with a point that the sweep line hits first\n const ptCmp = SweepEvent.comparePoints(a.point, b.point)\n if (ptCmp !== 0) return ptCmp\n\n // the points are the same, so link them if needed\n if (a.point !== b.point) a.link(b)\n\n // favor right events over left\n if (a.isLeft !== b.isLeft) return a.isLeft ? 1 : -1\n\n // we have two matching left or right endpoints\n // ordering of this case is the same as for their segments\n return Segment.compare(a.segment, b.segment)\n }\n\n // for ordering points in sweep line order\n static comparePoints(aPt: Point, bPt: Point) {\n if (aPt.x.isLessThan(bPt.x)) return -1\n if (aPt.x.isGreaterThan(bPt.x)) return 1\n\n if (aPt.y.isLessThan(bPt.y)) return -1\n if (aPt.y.isGreaterThan(bPt.y)) return 1\n\n return 0\n }\n\n // Warning: 'point' input will be modified and re-used (for performance)\n constructor(point: Point, isLeft: boolean) {\n if (point.events === undefined) point.events = [this]\n else point.events.push(this)\n this.point = point\n this.isLeft = isLeft\n // this.segment, this.otherSE set by factory\n }\n\n link(other: SweepEvent) {\n if (other.point === this.point) {\n throw new Error(\"Tried to link already linked events\")\n }\n const otherEvents = other.point.events\n for (let i = 0, iMax = otherEvents.length; i < iMax; i++) {\n const evt = otherEvents[i]\n this.point.events.push(evt)\n evt.point = this.point\n }\n this.checkForConsuming()\n }\n\n /* Do a pass over our linked events and check to see if any pair\n * of segments match, and should be consumed. */\n checkForConsuming() {\n // FIXME: The loops in this method run O(n^2) => no good.\n // Maintain little ordered sweep event trees?\n // Can we maintaining an ordering that avoids the need\n // for the re-sorting with getLeftmostComparator in geom-out?\n\n // Compare each pair of events to see if other events also match\n const numEvents = this.point.events.length\n for (let i = 0; i < numEvents; i++) {\n const evt1 = this.point.events[i]\n if (evt1.segment.consumedBy !== undefined) continue\n for (let j = i + 1; j < numEvents; j++) {\n const evt2 = this.point.events[j]\n if (evt2.consumedBy !== undefined) continue\n if (evt1.otherSE.point.events !== evt2.otherSE.point.events) continue\n evt1.segment.consume(evt2.segment)\n }\n }\n }\n\n getAvailableLinkedEvents() {\n // point.events is always of length 2 or greater\n const events = []\n for (let i = 0, iMax = this.point.events.length; i < iMax; i++) {\n const evt = this.point.events[i]\n if (evt !== this && !evt.segment.ringOut && evt.segment.isInResult()) {\n events.push(evt)\n }\n }\n return events\n }\n\n /**\n * Returns a comparator function for sorting linked events that will\n * favor the event that will give us the smallest left-side angle.\n * All ring construction starts as low as possible heading to the right,\n * so by always turning left as sharp as possible we'll get polygons\n * without uncessary loops & holes.\n *\n * The comparator function has a compute cache such that it avoids\n * re-computing already-computed values.\n */\n getLeftmostComparator(baseEvent: SweepEvent) {\n const cache = new Map()\n\n const fillCache = (linkedEvent: SweepEvent) => {\n const nextEvent = linkedEvent.otherSE\n cache.set(linkedEvent, {\n sine: sineOfAngle(this.point, baseEvent.point, nextEvent.point),\n cosine: cosineOfAngle(this.point, baseEvent.point, nextEvent.point),\n })\n }\n\n return (a: SweepEvent, b: SweepEvent) => {\n if (!cache.has(a)) fillCache(a)\n if (!cache.has(b)) fillCache(b)\n\n const { sine: asine, cosine: acosine } = cache.get(a)!\n const { sine: bsine, cosine: bcosine } = cache.get(b)!\n\n // both on or above x-axis\n if (asine.isGreaterThanOrEqualTo(0) && bsine.isGreaterThanOrEqualTo(0)) {\n if (acosine.isLessThan(bcosine)) return 1\n if (acosine.isGreaterThan(bcosine)) return -1\n return 0\n }\n\n // both below x-axis\n if (asine.isLessThan(0) && bsine.isLessThan(0)) {\n if (acosine.isLessThan(bcosine)) return -1\n if (acosine.isGreaterThan(bcosine)) return 1\n return 0\n }\n\n // one above x-axis, one below\n if (bsine.isLessThan(asine)) return -1\n if (bsine.isGreaterThan(asine)) return 1\n return 0\n }\n }\n}\n", "import { MultiPoly, Poly, Ring } from \"./geom-in.js\"\nimport { precision } from \"./precision.js\"\nimport Segment from \"./segment.js\"\nimport SweepEvent from \"./sweep-event.js\"\n\nexport class RingOut {\n events: SweepEvent[]\n poly: PolyOut | null\n _isExteriorRing: boolean | undefined\n _enclosingRing: RingOut | null | undefined\n \n /* Given the segments from the sweep line pass, compute & return a series\n * of closed rings from all the segments marked to be part of the result */\n static factory(allSegments: Segment[]) {\n const ringsOut = []\n\n for (let i = 0, iMax = allSegments.length; i < iMax; i++) {\n const segment = allSegments[i]\n if (!segment.isInResult() || segment.ringOut) continue\n\n let prevEvent = null\n let event = segment.leftSE\n let nextEvent = segment.rightSE\n const events = [event]\n\n const startingPoint = event.point\n const intersectionLEs = []\n\n /* Walk the chain of linked events to form a closed ring */\n while (true) {\n prevEvent = event\n event = nextEvent\n events.push(event)\n\n /* Is the ring complete? */\n if (event.point === startingPoint) break\n\n while (true) {\n const availableLEs = event.getAvailableLinkedEvents()\n\n /* Did we hit a dead end? This shouldn't happen. Indicates some earlier\n * part of the algorithm malfunctioned... please file a bug report. */\n if (availableLEs.length === 0) {\n const firstPt = events[0].point\n const lastPt = events[events.length - 1].point\n throw new Error(\n `Unable to complete output ring starting at [${firstPt.x},` +\n ` ${firstPt.y}]. Last matching segment found ends at` +\n ` [${lastPt.x}, ${lastPt.y}].`,\n )\n }\n\n /* Only one way to go, so cotinue on the path */\n if (availableLEs.length === 1) {\n nextEvent = availableLEs[0].otherSE\n break\n }\n\n /* We must have an intersection. Check for a completed loop */\n let indexLE = null\n for (let j = 0, jMax = intersectionLEs.length; j < jMax; j++) {\n if (intersectionLEs[j].point === event.point) {\n indexLE = j\n break\n }\n }\n /* Found a completed loop. Cut that off and make a ring */\n if (indexLE !== null) {\n const intersectionLE = intersectionLEs.splice(indexLE)[0]\n const ringEvents = events.splice(intersectionLE.index)\n ringEvents.unshift(ringEvents[0].otherSE)\n ringsOut.push(new RingOut(ringEvents.reverse()))\n continue\n }\n /* register the intersection */\n intersectionLEs.push({\n index: events.length,\n point: event.point,\n })\n /* Choose the left-most option to continue the walk */\n const comparator = event.getLeftmostComparator(prevEvent)\n nextEvent = availableLEs.sort(comparator)[0].otherSE\n break\n }\n }\n\n ringsOut.push(new RingOut(events))\n }\n return ringsOut\n }\n\n constructor(events: SweepEvent[]) {\n this.events = events\n for (let i = 0, iMax = events.length; i < iMax; i++) {\n events[i].segment.ringOut = this\n }\n this.poly = null\n }\n\n getGeom() {\n // Remove superfluous points (ie extra points along a straight line),\n let prevPt = this.events[0].point\n const points = [prevPt]\n for (let i = 1, iMax = this.events.length - 1; i < iMax; i++) {\n const pt = this.events[i].point\n const nextPt = this.events[i + 1].point\n if (precision.orient(pt, prevPt, nextPt) === 0) continue\n points.push(pt)\n prevPt = pt\n }\n\n // ring was all (within rounding error of angle calc) colinear points\n if (points.length === 1) return null\n\n // check if the starting point is necessary\n const pt = points[0]\n const nextPt = points[1]\n if (precision.orient(pt, prevPt, nextPt) === 0) points.shift()\n\n points.push(points[0])\n const step = this.isExteriorRing() ? 1 : -1\n const iStart = this.isExteriorRing() ? 0 : points.length - 1\n const iEnd = this.isExteriorRing() ? points.length : -1\n const orderedPoints: Ring = []\n for (let i = iStart; i != iEnd; i += step)\n orderedPoints.push([points[i].x.toNumber(), points[i].y.toNumber()])\n return orderedPoints\n }\n\n isExteriorRing(): boolean {\n if (this._isExteriorRing === undefined) {\n const enclosing = this.enclosingRing()\n this._isExteriorRing = enclosing ? !enclosing.isExteriorRing() : true\n }\n return this._isExteriorRing\n }\n\n enclosingRing() {\n if (this._enclosingRing === undefined) {\n this._enclosingRing = this._calcEnclosingRing()\n }\n return this._enclosingRing\n }\n\n /* Returns the ring that encloses this one, if any */\n _calcEnclosingRing(): RingOut | null | undefined {\n // start with the ealier sweep line event so that the prevSeg\n // chain doesn't lead us inside of a loop of ours\n let leftMostEvt = this.events[0]\n for (let i = 1, iMax = this.events.length; i < iMax; i++) {\n const evt = this.events[i]\n if (SweepEvent.compare(leftMostEvt, evt) > 0) leftMostEvt = evt\n }\n\n let prevSeg: Segment | null | undefined = leftMostEvt.segment.prevInResult()\n let prevPrevSeg: Segment | null | undefined = prevSeg ? prevSeg.prevInResult() : null\n\n while (true) {\n // no segment found, thus no ring can enclose us\n if (!prevSeg) return null\n\n // no segments below prev segment found, thus the ring of the prev\n // segment must loop back around and enclose us\n if (!prevPrevSeg) return prevSeg.ringOut\n\n // if the two segments are of different rings, the ring of the prev\n // segment must either loop around us or the ring of the prev prev\n // seg, which would make us and the ring of the prev peers\n if (prevPrevSeg.ringOut !== prevSeg.ringOut) {\n if (prevPrevSeg.ringOut?.enclosingRing() !== prevSeg.ringOut) {\n return prevSeg.ringOut\n } else return prevSeg.ringOut?.enclosingRing()\n }\n\n // two segments are from the same ring, so this was a penisula\n // of that ring. iterate downward, keep searching\n prevSeg = prevPrevSeg.prevInResult()\n prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null\n }\n }\n}\n\nexport class PolyOut {\n exteriorRing: RingOut;\n interiorRings: RingOut[];\n\n constructor(exteriorRing: RingOut) {\n this.exteriorRing = exteriorRing\n exteriorRing.poly = this\n this.interiorRings = []\n }\n\n addInterior(ring: RingOut) {\n this.interiorRings.push(ring)\n ring.poly = this\n }\n\n getGeom() {\n const geom0 = this.exteriorRing.getGeom()\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (geom0 === null) return null\n const geom: Poly = [geom0];\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringGeom = this.interiorRings[i].getGeom()\n // interior ring was all (within rounding error of angle calc) colinear points\n if (ringGeom === null) continue\n geom.push(ringGeom)\n }\n return geom\n }\n}\n\nexport class MultiPolyOut {\n rings: RingOut[];\n polys: PolyOut[];\n\n constructor(rings: RingOut[]) {\n this.rings = rings\n this.polys = this._composePolys(rings)\n }\n\n getGeom() {\n const geom: MultiPoly = []\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polyGeom = this.polys[i].getGeom()\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (polyGeom === null) continue\n geom.push(polyGeom)\n }\n return geom\n }\n\n _composePolys(rings: RingOut[]) {\n const polys = []\n for (let i = 0, iMax = rings.length; i < iMax; i++) {\n const ring = rings[i]\n if (ring.poly) continue\n if (ring.isExteriorRing()) polys.push(new PolyOut(ring))\n else {\n const enclosingRing = ring.enclosingRing()\n if (!enclosingRing?.poly) polys.push(new PolyOut(enclosingRing!))\n enclosingRing?.poly?.addInterior(ring)\n }\n }\n return polys\n }\n}\n", "import { SplayTreeSet } from \"splaytree-ts\"\nimport Segment from \"./segment.js\"\nimport SweepEvent, { Point } from \"./sweep-event.js\"\n\n/**\n * NOTE: We must be careful not to change any segments while\n * they are in the SplayTree. AFAIK, there's no way to tell\n * the tree to rebalance itself - thus before splitting\n * a segment that's in the tree, we remove it from the tree,\n * do the split, then re-insert it. (Even though splitting a\n * segment *shouldn't* change its correct position in the\n * sweep line tree, the reality is because of rounding errors,\n * it sometimes does.)\n */\n\nexport default class SweepLine {\n private queue: SplayTreeSet\n private tree: SplayTreeSet\n segments: Segment[]\n\n constructor(queue: SplayTreeSet, comparator = Segment.compare) {\n this.queue = queue\n this.tree = new SplayTreeSet(comparator)\n this.segments = []\n }\n\n process(event: SweepEvent) {\n const segment = event.segment\n const newEvents: SweepEvent[] = []\n\n // if we've already been consumed by another segment,\n // clean up our body parts and get out\n if (event.consumedBy) {\n if (event.isLeft) this.queue.delete(event.otherSE)\n else this.tree.delete(segment)\n return newEvents\n }\n\n if (event.isLeft) this.tree.add(segment);\n\n let prevSeg: Segment | null = segment\n let nextSeg: Segment | null = segment\n\n // skip consumed segments still in tree\n do {\n prevSeg = this.tree.lastBefore(prevSeg)\n } while (prevSeg != null && prevSeg.consumedBy != undefined)\n\n // skip consumed segments still in tree\n do {\n nextSeg = this.tree.firstAfter(nextSeg)\n } while (nextSeg != null && nextSeg.consumedBy != undefined)\n\n if (event.isLeft) {\n // Check for intersections against the previous segment in the sweep line\n let prevMySplitter = null\n if (prevSeg) {\n const prevInter = prevSeg.getIntersection(segment)\n if (prevInter !== null) {\n if (!segment.isAnEndpoint(prevInter)) prevMySplitter = prevInter\n if (!prevSeg.isAnEndpoint(prevInter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, prevInter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n }\n }\n\n // Check for intersections against the next segment in the sweep line\n let nextMySplitter = null\n if (nextSeg) {\n const nextInter = nextSeg.getIntersection(segment)\n if (nextInter !== null) {\n if (!segment.isAnEndpoint(nextInter)) nextMySplitter = nextInter\n if (!nextSeg.isAnEndpoint(nextInter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, nextInter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n }\n }\n\n // For simplicity, even if we find more than one intersection we only\n // spilt on the 'earliest' (sweep-line style) of the intersections.\n // The other intersection will be handled in a future process().\n if (prevMySplitter !== null || nextMySplitter !== null) {\n let mySplitter = null\n if (prevMySplitter === null) mySplitter = nextMySplitter\n else if (nextMySplitter === null) mySplitter = prevMySplitter\n else {\n const cmpSplitters = SweepEvent.comparePoints(\n prevMySplitter,\n nextMySplitter,\n )\n mySplitter = cmpSplitters <= 0 ? prevMySplitter : nextMySplitter\n }\n\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n this.queue.delete(segment.rightSE)\n newEvents.push(segment.rightSE)\n\n const newEventsFromSplit = segment.split(mySplitter!)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n\n if (newEvents.length > 0) {\n // We found some intersections, so re-do the current event to\n // make sure sweep line ordering is totally consistent for later\n // use with the segment 'prev' pointers\n this.tree.delete(segment)\n newEvents.push(event)\n } else {\n // done with left event\n this.segments.push(segment)\n segment.prev = prevSeg\n }\n } else {\n // event.isRight\n\n // since we're about to be removed from the sweep line, check for\n // intersections between our previous and next segments\n if (prevSeg && nextSeg) {\n const inter = prevSeg.getIntersection(nextSeg)\n if (inter !== null) {\n if (!prevSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, inter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n if (!nextSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, inter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n }\n }\n\n this.tree.delete(segment)\n }\n\n return newEvents\n }\n\n /* Safely split a segment that is currently in the datastructures\n * IE - a segment other than the one that is currently being processed. */\n _splitSafely(seg: Segment, pt: Point) {\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n // removeNode() doesn't work, so have re-find the seg\n // https://github.com/w8r/splay-tree/pull/5\n this.tree.delete(seg)\n const rightSE = seg.rightSE\n this.queue.delete(rightSE)\n const newEvents = seg.split(pt)\n newEvents.push(rightSE)\n // splitting can trigger consumption\n if (seg.consumedBy === undefined) this.tree.add(seg)\n return newEvents\n }\n}\n", "import { getBboxOverlap, isInBbox } from \"./bbox.js\"\nimport { MultiPolyIn, RingIn } from \"./geom-in.js\"\nimport { RingOut } from \"./geom-out.js\"\nimport operation from \"./operation.js\"\nimport { precision } from \"./precision.js\"\nimport SweepEvent, { Point } from \"./sweep-event.js\"\nimport { intersection } from \"./vector.js\"\n\ninterface State {\n rings: RingIn[],\n windings: number[],\n multiPolys: MultiPolyIn[]\n}\n\n// Give segments unique ID's to get consistent sorting of\n// segments and sweep events when all else is identical\nlet segmentId = 0\n\nexport default class Segment {\n id: number\n leftSE: SweepEvent\n rightSE: SweepEvent\n rings: RingIn[] | null\n windings: number[] | null\n ringOut: RingOut | undefined\n consumedBy: Segment | undefined\n prev: Segment | null | undefined\n _prevInResult: Segment | null | undefined\n _beforeState: State | undefined\n _afterState: State | undefined\n _isInResult: boolean | undefined\n\n /* This compare() function is for ordering segments in the sweep\n * line tree, and does so according to the following criteria:\n *\n * Consider the vertical line that lies an infinestimal step to the\n * right of the right-more of the two left endpoints of the input\n * segments. Imagine slowly moving a point up from negative infinity\n * in the increasing y direction. Which of the two segments will that\n * point intersect first? That segment comes 'before' the other one.\n *\n * If neither segment would be intersected by such a line, (if one\n * or more of the segments are vertical) then the line to be considered\n * is directly on the right-more of the two left inputs.\n */\n static compare(a: Segment, b: Segment) {\n const alx = a.leftSE.point.x\n const blx = b.leftSE.point.x\n const arx = a.rightSE.point.x\n const brx = b.rightSE.point.x\n\n // check if they're even in the same vertical plane\n if (brx.isLessThan(alx)) return 1\n if (arx.isLessThan(blx)) return -1\n\n const aly = a.leftSE.point.y\n const bly = b.leftSE.point.y\n const ary = a.rightSE.point.y\n const bry = b.rightSE.point.y\n\n // is left endpoint of segment B the right-more?\n if (alx.isLessThan(blx)) {\n // are the two segments in the same horizontal plane?\n if (bly.isLessThan(aly) && bly.isLessThan(ary)) return 1\n if (bly.isGreaterThan(aly) && bly.isGreaterThan(ary)) return -1\n\n // is the B left endpoint colinear to segment A?\n const aCmpBLeft = a.comparePoint(b.leftSE.point)\n if (aCmpBLeft < 0) return 1\n if (aCmpBLeft > 0) return -1\n\n // is the A right endpoint colinear to segment B ?\n const bCmpARight = b.comparePoint(a.rightSE.point)\n if (bCmpARight !== 0) return bCmpARight\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return -1\n }\n\n // is left endpoint of segment A the right-more?\n if (alx.isGreaterThan(blx)) {\n if (aly.isLessThan(bly) && aly.isLessThan(bry)) return -1\n if (aly.isGreaterThan(bly) && aly.isGreaterThan(bry)) return 1\n\n // is the A left endpoint colinear to segment B?\n const bCmpALeft = b.comparePoint(a.leftSE.point)\n if (bCmpALeft !== 0) return bCmpALeft\n\n // is the B right endpoint colinear to segment A?\n const aCmpBRight = a.comparePoint(b.rightSE.point)\n if (aCmpBRight < 0) return 1\n if (aCmpBRight > 0) return -1\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return 1\n }\n\n // if we get here, the two left endpoints are in the same\n // vertical plane, ie alx === blx\n\n // consider the lower left-endpoint to come first\n if (aly.isLessThan(bly)) return -1\n if (aly.isGreaterThan(bly)) return 1\n\n // left endpoints are identical\n // check for colinearity by using the left-more right endpoint\n\n // is the A right endpoint more left-more?\n if (arx.isLessThan(brx)) {\n const bCmpARight = b.comparePoint(a.rightSE.point)\n if (bCmpARight !== 0) return bCmpARight\n }\n\n // is the B right endpoint more left-more?\n if (arx.isGreaterThan(brx)) {\n const aCmpBRight = a.comparePoint(b.rightSE.point)\n if (aCmpBRight < 0) return 1\n if (aCmpBRight > 0) return -1\n }\n\n if (!arx.eq(brx)) {\n // are these two [almost] vertical segments with opposite orientation?\n // if so, the one with the lower right endpoint comes first\n const ay = ary.minus(aly)\n const ax = arx.minus(alx)\n const by = bry.minus(bly)\n const bx = brx.minus(blx)\n if (ay.isGreaterThan(ax) && by.isLessThan(bx)) return 1\n if (ay.isLessThan(ax) && by.isGreaterThan(bx)) return -1\n }\n\n // we have colinear segments with matching orientation\n // consider the one with more left-more right endpoint to be first\n if (arx.isGreaterThan(brx)) return 1\n if (arx.isLessThan(brx)) return -1\n\n // if we get here, two two right endpoints are in the same\n // vertical plane, ie arx === brx\n\n // consider the lower right-endpoint to come first\n if (ary.isLessThan(bry)) return -1\n if (ary.isGreaterThan(bry)) return 1\n\n // right endpoints identical as well, so the segments are idential\n // fall back on creation order as consistent tie-breaker\n if (a.id < b.id) return -1\n if (a.id > b.id) return 1\n\n // identical segment, ie a === b\n return 0\n }\n\n /* Warning: a reference to ringWindings input will be stored,\n * and possibly will be later modified */\n constructor(leftSE: SweepEvent, rightSE: SweepEvent, rings: RingIn[], windings: number[]) {\n this.id = ++segmentId\n this.leftSE = leftSE\n leftSE.segment = this\n leftSE.otherSE = rightSE\n this.rightSE = rightSE\n rightSE.segment = this\n rightSE.otherSE = leftSE\n this.rings = rings\n this.windings = windings\n // left unset for performance, set later in algorithm\n // this.ringOut, this.consumedBy, this.prev\n }\n\n static fromRing(pt1: Point, pt2: Point, ring: RingIn) {\n let leftPt: Point, rightPt: Point, winding: number\n\n // ordering the two points according to sweep line ordering\n const cmpPts = SweepEvent.comparePoints(pt1, pt2)\n if (cmpPts < 0) {\n leftPt = pt1\n rightPt = pt2\n winding = 1\n } else if (cmpPts > 0) {\n leftPt = pt2\n rightPt = pt1\n winding = -1\n } else\n throw new Error(\n `Tried to create degenerate segment at [${pt1.x}, ${pt1.y}]`,\n )\n\n const leftSE = new SweepEvent(leftPt, true)\n const rightSE = new SweepEvent(rightPt, false)\n return new Segment(leftSE, rightSE, [ring], [winding])\n }\n\n /* When a segment is split, the rightSE is replaced with a new sweep event */\n replaceRightSE(newRightSE: SweepEvent) {\n this.rightSE = newRightSE\n this.rightSE.segment = this\n this.rightSE.otherSE = this.leftSE\n this.leftSE.otherSE = this.rightSE\n }\n\n bbox() {\n const y1 = this.leftSE.point.y\n const y2 = this.rightSE.point.y\n return {\n ll: { x: this.leftSE.point.x, y: y1.isLessThan(y2) ? y1 : y2 },\n ur: { x: this.rightSE.point.x, y: y1.isGreaterThan(y2) ? y1 : y2 },\n }\n }\n\n /* A vector from the left point to the right */\n vector() {\n return {\n x: this.rightSE.point.x.minus(this.leftSE.point.x),\n y: this.rightSE.point.y.minus(this.leftSE.point.y),\n }\n }\n\n isAnEndpoint(pt: Point) {\n return (\n (pt.x.eq(this.leftSE.point.x) && pt.y.eq(this.leftSE.point.y)) ||\n (pt.x.eq(this.rightSE.point.x) && pt.y.eq(this.rightSE.point.y))\n )\n }\n\n /* Compare this segment with a point.\n *\n * A point P is considered to be colinear to a segment if there\n * exists a distance D such that if we travel along the segment\n * from one * endpoint towards the other a distance D, we find\n * ourselves at point P.\n *\n * Return value indicates:\n *\n * 1: point lies above the segment (to the left of vertical)\n * 0: point is colinear to segment\n * -1: point lies below the segment (to the right of vertical)\n */\n comparePoint(point: Point) {\n return precision.orient(this.leftSE.point, point, this.rightSE.point)\n }\n\n /**\n * Given another segment, returns the first non-trivial intersection\n * between the two segments (in terms of sweep line ordering), if it exists.\n *\n * A 'non-trivial' intersection is one that will cause one or both of the\n * segments to be split(). As such, 'trivial' vs. 'non-trivial' intersection:\n *\n * * endpoint of segA with endpoint of segB --> trivial\n * * endpoint of segA with point along segB --> non-trivial\n * * endpoint of segB with point along segA --> non-trivial\n * * point along segA with point along segB --> non-trivial\n *\n * If no non-trivial intersection exists, return null\n * Else, return null.\n */\n getIntersection(other: Segment) {\n // If bboxes don't overlap, there can't be any intersections\n const tBbox = this.bbox()\n const oBbox = other.bbox()\n const bboxOverlap = getBboxOverlap(tBbox, oBbox)\n if (bboxOverlap === null) return null\n\n // We first check to see if the endpoints can be considered intersections.\n // This will 'snap' intersections to endpoints if possible, and will\n // handle cases of colinearity.\n\n const tlp = this.leftSE.point\n const trp = this.rightSE.point\n const olp = other.leftSE.point\n const orp = other.rightSE.point\n\n // does each endpoint touch the other segment?\n // note that we restrict the 'touching' definition to only allow segments\n // to touch endpoints that lie forward from where we are in the sweep line pass\n const touchesOtherLSE = isInBbox(tBbox, olp) && this.comparePoint(olp) === 0\n const touchesThisLSE = isInBbox(oBbox, tlp) && other.comparePoint(tlp) === 0\n const touchesOtherRSE = isInBbox(tBbox, orp) && this.comparePoint(orp) === 0\n const touchesThisRSE = isInBbox(oBbox, trp) && other.comparePoint(trp) === 0\n\n // do left endpoints match?\n if (touchesThisLSE && touchesOtherLSE) {\n // these two cases are for colinear segments with matching left\n // endpoints, and one segment being longer than the other\n if (touchesThisRSE && !touchesOtherRSE) return trp\n if (!touchesThisRSE && touchesOtherRSE) return orp\n // either the two segments match exactly (two trival intersections)\n // or just on their left endpoint (one trivial intersection\n return null\n }\n\n // does this left endpoint matches (other doesn't)\n if (touchesThisLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesOtherRSE) {\n if (tlp.x.eq(orp.x) && tlp.y.eq(orp.y)) return null\n }\n // t-intersection on left endpoint\n return tlp\n }\n\n // does other left endpoint matches (this doesn't)\n if (touchesOtherLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesThisRSE) {\n if (trp.x.eq(olp.x) && trp.y.eq(olp.y)) return null\n }\n // t-intersection on left endpoint\n return olp\n }\n\n // trivial intersection on right endpoints\n if (touchesThisRSE && touchesOtherRSE) return null\n\n // t-intersections on just one right endpoint\n if (touchesThisRSE) return trp\n if (touchesOtherRSE) return orp\n\n // None of our endpoints intersect. Look for a general intersection between\n // infinite lines laid over the segments\n const pt = intersection(tlp, this.vector(), olp, other.vector())\n\n // are the segments parrallel? Note that if they were colinear with overlap,\n // they would have an endpoint intersection and that case was already handled above\n if (pt === null) return null\n\n // is the intersection found between the lines not on the segments?\n if (!isInBbox(bboxOverlap, pt)) return null\n\n // round the the computed point if needed\n return precision.snap(pt) as Point\n }\n\n /**\n * Split the given segment into multiple segments on the given points.\n * * Each existing segment will retain its leftSE and a new rightSE will be\n * generated for it.\n * * A new segment will be generated which will adopt the original segment's\n * rightSE, and a new leftSE will be generated for it.\n * * If there are more than two points given to split on, new segments\n * in the middle will be generated with new leftSE and rightSE's.\n * * An array of the newly generated SweepEvents will be returned.\n *\n * Warning: input array of points is modified\n */\n split(point: Point) {\n const newEvents = []\n const alreadyLinked = point.events !== undefined\n\n const newLeftSE = new SweepEvent(point, true)\n const newRightSE = new SweepEvent(point, false)\n const oldRightSE = this.rightSE\n this.replaceRightSE(newRightSE)\n newEvents.push(newRightSE)\n newEvents.push(newLeftSE)\n const newSeg = new Segment(\n newLeftSE,\n oldRightSE,\n this.rings!.slice(),\n this.windings!.slice(),\n )\n\n // when splitting a nearly vertical downward-facing segment,\n // sometimes one of the resulting new segments is vertical, in which\n // case its left and right events may need to be swapped\n if (\n SweepEvent.comparePoints(newSeg.leftSE.point, newSeg.rightSE.point) > 0\n ) {\n newSeg.swapEvents()\n }\n if (SweepEvent.comparePoints(this.leftSE.point, this.rightSE.point) > 0) {\n this.swapEvents()\n }\n\n // in the point we just used to create new sweep events with was already\n // linked to other events, we need to check if either of the affected\n // segments should be consumed\n if (alreadyLinked) {\n newLeftSE.checkForConsuming()\n newRightSE.checkForConsuming()\n }\n\n return newEvents\n }\n\n /* Swap which event is left and right */\n swapEvents() {\n const tmpEvt = this.rightSE\n this.rightSE = this.leftSE\n this.leftSE = tmpEvt\n this.leftSE.isLeft = true\n this.rightSE.isLeft = false\n for (let i = 0, iMax = this.windings!.length; i < iMax; i++) {\n this.windings![i] *= -1\n }\n }\n\n /* Consume another segment. We take their rings under our wing\n * and mark them as consumed. Use for perfectly overlapping segments */\n consume(other: Segment) {\n let consumer = this as Segment\n let consumee = other\n while (consumer.consumedBy) consumer = consumer.consumedBy\n while (consumee.consumedBy) consumee = consumee.consumedBy\n\n const cmp = Segment.compare(consumer, consumee)\n if (cmp === 0) return // already consumed\n // the winner of the consumption is the earlier segment\n // according to sweep line ordering\n if (cmp > 0) {\n const tmp = consumer\n consumer = consumee\n consumee = tmp\n }\n\n // make sure a segment doesn't consume it's prev\n if (consumer.prev === consumee) {\n const tmp = consumer\n consumer = consumee\n consumee = tmp\n }\n\n for (let i = 0, iMax = consumee.rings!.length; i < iMax; i++) {\n const ring = consumee.rings![i]\n const winding = consumee.windings![i]\n const index = consumer.rings!.indexOf(ring)\n if (index === -1) {\n consumer.rings!.push(ring)\n consumer.windings!.push(winding)\n } else consumer.windings![index] += winding\n }\n consumee.rings = null\n consumee.windings = null\n consumee.consumedBy = consumer\n\n // mark sweep events consumed as to maintain ordering in sweep event queue\n consumee.leftSE.consumedBy = consumer.leftSE\n consumee.rightSE.consumedBy = consumer.rightSE\n }\n\n /* The first segment previous segment chain that is in the result */\n prevInResult(): Segment | null | undefined {\n if (this._prevInResult !== undefined) return this._prevInResult\n if (!this.prev) this._prevInResult = null\n else if (this.prev.isInResult()) this._prevInResult = this.prev\n else this._prevInResult = this.prev.prevInResult()\n return this._prevInResult\n }\n\n beforeState(): State {\n if (this._beforeState !== undefined) return this._beforeState\n if (!this.prev)\n this._beforeState = {\n rings: [],\n windings: [],\n multiPolys: [],\n }\n else {\n const seg = this.prev.consumedBy || this.prev\n this._beforeState = seg.afterState()\n }\n return this._beforeState\n }\n\n afterState() {\n if (this._afterState !== undefined) return this._afterState\n\n const beforeState = this.beforeState()\n this._afterState = {\n rings: beforeState.rings.slice(0),\n windings: beforeState.windings.slice(0),\n multiPolys: [],\n }\n const ringsAfter = this._afterState.rings\n const windingsAfter = this._afterState.windings\n const mpsAfter = this._afterState.multiPolys\n\n // calculate ringsAfter, windingsAfter\n for (let i = 0, iMax = this.rings!.length; i < iMax; i++) {\n const ring = this.rings![i]\n const winding = this.windings![i]\n const index = ringsAfter.indexOf(ring)\n if (index === -1) {\n ringsAfter.push(ring)\n windingsAfter.push(winding)\n } else windingsAfter[index] += winding\n }\n\n // calcualte polysAfter\n const polysAfter = []\n const polysExclude = []\n for (let i = 0, iMax = ringsAfter.length; i < iMax; i++) {\n if (windingsAfter[i] === 0) continue // non-zero rule\n const ring = ringsAfter[i]\n const poly = ring.poly\n if (polysExclude.indexOf(poly) !== -1) continue\n if (ring.isExterior) polysAfter.push(poly)\n else {\n if (polysExclude.indexOf(poly) === -1) polysExclude.push(poly)\n const index = polysAfter.indexOf(ring.poly)\n if (index !== -1) polysAfter.splice(index, 1)\n }\n }\n\n // calculate multiPolysAfter\n for (let i = 0, iMax = polysAfter.length; i < iMax; i++) {\n const mp = polysAfter[i].multiPoly\n if (mpsAfter.indexOf(mp) === -1) mpsAfter.push(mp)\n }\n\n return this._afterState\n }\n\n /* Is this segment part of the final result? */\n isInResult() {\n // if we've been consumed, we're not in the result\n if (this.consumedBy) return false\n\n if (this._isInResult !== undefined) return this._isInResult\n\n const mpsBefore = this.beforeState().multiPolys\n const mpsAfter = this.afterState().multiPolys\n\n switch (operation.type) {\n case \"union\": {\n // UNION - included iff:\n // * On one side of us there is 0 poly interiors AND\n // * On the other side there is 1 or more.\n const noBefores = mpsBefore.length === 0\n const noAfters = mpsAfter.length === 0\n this._isInResult = noBefores !== noAfters\n break\n }\n\n case \"intersection\": {\n // INTERSECTION - included iff:\n // * on one side of us all multipolys are rep. with poly interiors AND\n // * on the other side of us, not all multipolys are repsented\n // with poly interiors\n let least\n let most\n if (mpsBefore.length < mpsAfter.length) {\n least = mpsBefore.length\n most = mpsAfter.length\n } else {\n least = mpsAfter.length\n most = mpsBefore.length\n }\n this._isInResult = most === operation.numMultiPolys && least < most\n break\n }\n\n case \"xor\": {\n // XOR - included iff:\n // * the difference between the number of multipolys represented\n // with poly interiors on our two sides is an odd number\n const diff = Math.abs(mpsBefore.length - mpsAfter.length)\n this._isInResult = diff % 2 === 1\n break\n }\n\n case \"difference\": {\n // DIFFERENCE included iff:\n // * on exactly one side, we have just the subject\n const isJustSubject = (mps: MultiPolyIn[]) => mps.length === 1 && mps[0].isSubject\n this._isInResult = isJustSubject(mpsBefore) !== isJustSubject(mpsAfter)\n break\n }\n }\n\n return this._isInResult\n }\n}\n", "import { Geom } from \"./geom-in.js\"\nimport { precision } from \"./precision.js\"\nimport operation from \"./operation.js\"\n\nexport { Geom }\n\nexport const union = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"union\", geom, moreGeoms)\n\nexport const intersection = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"intersection\", geom, moreGeoms)\n\nexport const xor = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"xor\", geom, moreGeoms)\n\nexport const difference = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"difference\", geom, moreGeoms)\n\nexport const setPrecision = precision.set", "module.exports.RADIUS = 6378137;\nmodule.exports.FLATTENING = 1/298.257223563;\nmodule.exports.POLAR_RADIUS = 6356752.3142;\n", "var wgs84 = require('wgs84');\n\nmodule.exports.geometry = geometry;\nmodule.exports.ring = ringArea;\n\nfunction geometry(_) {\n var area = 0, i;\n switch (_.type) {\n case 'Polygon':\n return polygonArea(_.coordinates);\n case 'MultiPolygon':\n for (i = 0; i < _.coordinates.length; i++) {\n area += polygonArea(_.coordinates[i]);\n }\n return area;\n case 'Point':\n case 'MultiPoint':\n case 'LineString':\n case 'MultiLineString':\n return 0;\n case 'GeometryCollection':\n for (i = 0; i < _.geometries.length; i++) {\n area += geometry(_.geometries[i]);\n }\n return area;\n }\n}\n\nfunction polygonArea(coords) {\n var area = 0;\n if (coords && coords.length > 0) {\n area += Math.abs(ringArea(coords[0]));\n for (var i = 1; i < coords.length; i++) {\n area -= Math.abs(ringArea(coords[i]));\n }\n }\n return area;\n}\n\n/**\n * Calculate the approximate area of the polygon were it projected onto\n * the earth. Note that this area will be positive if ring is oriented\n * clockwise, otherwise it will be negative.\n *\n * Reference:\n * Robert. G. Chamberlain and William H. Duquette, \"Some Algorithms for\n * Polygons on a Sphere\", JPL Publication 07-03, Jet Propulsion\n * Laboratory, Pasadena, CA, June 2007 http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409\n *\n * Returns:\n * {float} The approximate signed geodesic area of the polygon in square\n * meters.\n */\n\nfunction ringArea(coords) {\n var p1, p2, p3, lowerIndex, middleIndex, upperIndex, i,\n area = 0,\n coordsLength = coords.length;\n\n if (coordsLength > 2) {\n for (i = 0; i < coordsLength; i++) {\n if (i === coordsLength - 2) {// i = N-2\n lowerIndex = coordsLength - 2;\n middleIndex = coordsLength -1;\n upperIndex = 0;\n } else if (i === coordsLength - 1) {// i = N-1\n lowerIndex = coordsLength - 1;\n middleIndex = 0;\n upperIndex = 1;\n } else { // i = 0 to N-3\n lowerIndex = i;\n middleIndex = i+1;\n upperIndex = i+2;\n }\n p1 = coords[lowerIndex];\n p2 = coords[middleIndex];\n p3 = coords[upperIndex];\n area += ( rad(p3[0]) - rad(p1[0]) ) * Math.sin( rad(p2[1]));\n }\n\n area = area * wgs84.RADIUS * wgs84.RADIUS / 2;\n }\n\n return area;\n}\n\nfunction rad(_) {\n return _ * Math.PI / 180;\n}", "exports.validateCenter = function validateCenter(center) {\n var validCenterLengths = [2, 3];\n if (!Array.isArray(center) || !validCenterLengths.includes(center.length)) {\n throw new Error(\"ERROR! Center has to be an array of length two or three\");\n }\n\n var [lng, lat] = center;\n if (typeof lng !== \"number\" || typeof lat !== \"number\") {\n throw new Error(\n `ERROR! Longitude and Latitude has to be numbers but where ${typeof lng} and ${typeof lat}`\n );\n }\n if (lng > 180 || lng < -180) {\n throw new Error(`ERROR! Longitude has to be between -180 and 180 but was ${lng}`);\n }\n\n if (lat > 90 || lat < -90) {\n throw new Error(`ERROR! Latitude has to be between -90 and 90 but was ${lat}`);\n }\n};\n", "exports.validateRadius = function validateRadius(radius) {\n if (typeof radius !== \"number\") {\n throw new Error(`ERROR! Radius has to be a positive number but was: ${typeof radius}`);\n }\n\n if (radius <= 0) {\n throw new Error(`ERROR! Radius has to be a positive number but was: ${radius}`);\n }\n};\n", "exports.validateNumberOfEdges = function validateNumberOfEdges(numberOfEdges) {\n if (typeof numberOfEdges !== \"number\") {\n const ARGUMENT_TYPE = Array.isArray(numberOfEdges) ? \"array\" : typeof numberOfEdges;\n throw new Error(`ERROR! Number of edges has to be a number but was: ${ARGUMENT_TYPE}`);\n }\n\n if (numberOfEdges < 3) {\n throw new Error(`ERROR! Number of edges has to be at least 3 but was: ${numberOfEdges}`);\n }\n};\n", "exports.validateEarthRadius = function validateEarthRadius(earthRadius) {\n if (typeof earthRadius !== \"number\") {\n const ARGUMENT_TYPE = Array.isArray(earthRadius) ? \"array\" : typeof earthRadius;\n throw new Error(`ERROR! Earth radius has to be a number but was: ${ARGUMENT_TYPE}`);\n }\n\n if (earthRadius <= 0) {\n throw new Error(`ERROR! Earth radius has to be a positive number but was: ${earthRadius}`);\n }\n};\n", "exports.validateBearing = function validateBearing(bearing) {\n if (typeof bearing !== \"number\") {\n const ARGUMENT_TYPE = Array.isArray(bearing) ? \"array\" : typeof bearing;\n throw new Error(`ERROR! Bearing has to be a number but was: ${ARGUMENT_TYPE}`);\n }\n};\n", "var validateCenter = require(\"./validateCenter\").validateCenter;\nvar validateRadius = require(\"./validateRadius\").validateRadius;\nvar validateNumberOfEdges = require(\"./validateNumberOfEdges\").validateNumberOfEdges;\nvar validateEarthRadius = require(\"./validateEarthRadius\").validateEarthRadius;\nvar validateBearing = require(\"./validateBearing\").validateBearing;\n\nfunction validateInput({ center, radius, numberOfEdges, earthRadius, bearing }) {\n validateCenter(center);\n validateRadius(radius);\n validateNumberOfEdges(numberOfEdges);\n validateEarthRadius(earthRadius);\n validateBearing(bearing);\n}\n\nexports.validateCenter = validateCenter;\nexports.validateRadius = validateRadius;\nexports.validateNumberOfEdges = validateNumberOfEdges;\nexports.validateEarthRadius = validateEarthRadius;\nexports.validateBearing = validateBearing;\nexports.validateInput = validateInput;\n", "\"use strict\";\nvar { validateInput } = require(\"./input-validation\");\n\nconst defaultEarthRadius = 6378137; // equatorial Earth radius\n\nfunction toRadians(angleInDegrees) {\n return (angleInDegrees * Math.PI) / 180;\n}\n\nfunction toDegrees(angleInRadians) {\n return (angleInRadians * 180) / Math.PI;\n}\n\nfunction offset(c1, distance, earthRadius, bearing) {\n var lat1 = toRadians(c1[1]);\n var lon1 = toRadians(c1[0]);\n var dByR = distance / earthRadius;\n var lat = Math.asin(\n Math.sin(lat1) * Math.cos(dByR) + Math.cos(lat1) * Math.sin(dByR) * Math.cos(bearing)\n );\n var lon =\n lon1 +\n Math.atan2(\n Math.sin(bearing) * Math.sin(dByR) * Math.cos(lat1),\n Math.cos(dByR) - Math.sin(lat1) * Math.sin(lat)\n );\n return [toDegrees(lon), toDegrees(lat)];\n}\n\nmodule.exports = function circleToPolygon(center, radius, options) {\n var n = getNumberOfEdges(options);\n var earthRadius = getEarthRadius(options);\n var bearing = getBearing(options);\n var direction = getDirection(options);\n\n // validateInput() throws error on invalid input and do nothing on valid input\n validateInput({ center, radius, numberOfEdges: n, earthRadius, bearing });\n\n var start = toRadians(bearing);\n var coordinates = [];\n for (var i = 0; i < n; ++i) {\n coordinates.push(\n offset(\n center, radius, earthRadius, start + (direction * 2 * Math.PI * -i) / n\n )\n );\n }\n coordinates.push(coordinates[0]);\n\n return {\n type: \"Polygon\",\n coordinates: [coordinates]\n };\n};\n\nfunction getNumberOfEdges(options) {\n if (isUndefinedOrNull(options)) {\n return 32;\n } else if (isObjectNotArray(options)) {\n var numberOfEdges = options.numberOfEdges;\n return numberOfEdges === undefined ? 32 : numberOfEdges;\n }\n return options;\n}\n\nfunction getEarthRadius(options) {\n if (isUndefinedOrNull(options)) {\n return defaultEarthRadius;\n } else if (isObjectNotArray(options)) {\n var earthRadius = options.earthRadius;\n return earthRadius === undefined ? defaultEarthRadius : earthRadius;\n }\n return defaultEarthRadius;\n}\n\nfunction getDirection(options){\n if (isObjectNotArray(options) && options.rightHandRule){\n return -1;\n }\n return 1;\n}\n\nfunction getBearing(options) {\n if (isUndefinedOrNull(options)) {\n return 0;\n } else if (isObjectNotArray(options)) {\n var bearing = options.bearing;\n return bearing === undefined ? 0 : bearing;\n }\n return 0;\n}\n\nfunction isObjectNotArray(argument) {\n return argument !== null && typeof argument === \"object\" && !Array.isArray(argument);\n}\n\nfunction isUndefinedOrNull(argument) {\n return argument === null || argument === undefined;\n}\n", "(function() {\n\n function parse(t, coordinatePrecision, extrasPrecision) {\n\n function point(p) {\n return p.map(function(e, index) {\n if (index < 2) {\n return 1 * e.toFixed(coordinatePrecision);\n } else {\n return 1 * e.toFixed(extrasPrecision);\n }\n });\n }\n\n function multi(l) {\n return l.map(point);\n }\n\n function poly(p) {\n return p.map(multi);\n }\n\n function multiPoly(m) {\n return m.map(poly);\n }\n\n function geometry(obj) {\n if (!obj) {\n return {};\n }\n \n switch (obj.type) {\n case \"Point\":\n obj.coordinates = point(obj.coordinates);\n return obj;\n case \"LineString\":\n case \"MultiPoint\":\n obj.coordinates = multi(obj.coordinates);\n return obj;\n case \"Polygon\":\n case \"MultiLineString\":\n obj.coordinates = poly(obj.coordinates);\n return obj;\n case \"MultiPolygon\":\n obj.coordinates = multiPoly(obj.coordinates);\n return obj;\n case \"GeometryCollection\":\n obj.geometries = obj.geometries.map(geometry);\n return obj;\n default :\n return {};\n }\n }\n\n function feature(obj) {\n obj.geometry = geometry(obj.geometry);\n return obj\n }\n\n function featureCollection(f) {\n f.features = f.features.map(feature);\n return f;\n }\n\n function geometryCollection(g) {\n g.geometries = g.geometries.map(geometry);\n return g;\n }\n\n if (!t) {\n return t;\n }\n\n switch (t.type) {\n case \"Feature\":\n return feature(t);\n case \"GeometryCollection\" :\n return geometryCollection(t);\n case \"FeatureCollection\" :\n return featureCollection(t);\n case \"Point\":\n case \"LineString\":\n case \"Polygon\":\n case \"MultiPoint\":\n case \"MultiPolygon\":\n case \"MultiLineString\":\n return geometry(t);\n default :\n return t;\n }\n \n }\n\n module.exports = parse;\n module.exports.parse = parse;\n\n}());\n \n", "// Note: This regex matches even invalid JSON strings, but since we\u2019re\n// working on the output of `JSON.stringify` we know that only valid strings\n// are present (unless the user supplied a weird `options.indent` but in\n// that case we don\u2019t care since the output would be invalid anyway).\nconst stringOrChar = /(\"(?:[^\\\\\"]|\\\\.)*\")|[:,]/g;\n\nexport default function stringify(passedObj, options = {}) {\n const indent = JSON.stringify(\n [1],\n undefined,\n options.indent === undefined ? 2 : options.indent\n ).slice(2, -3);\n\n const maxLength =\n indent === \"\"\n ? Infinity\n : options.maxLength === undefined\n ? 80\n : options.maxLength;\n\n let { replacer } = options;\n\n return (function _stringify(obj, currentIndent, reserved) {\n if (obj && typeof obj.toJSON === \"function\") {\n obj = obj.toJSON();\n }\n\n const string = JSON.stringify(obj, replacer);\n\n if (string === undefined) {\n return string;\n }\n\n const length = maxLength - currentIndent.length - reserved;\n\n if (string.length <= length) {\n const prettified = string.replace(\n stringOrChar,\n (match, stringLiteral) => {\n return stringLiteral || `${match} `;\n }\n );\n if (prettified.length <= length) {\n return prettified;\n }\n }\n\n if (replacer != null) {\n obj = JSON.parse(string);\n replacer = undefined;\n }\n\n if (typeof obj === \"object\" && obj !== null) {\n const nextIndent = currentIndent + indent;\n const items = [];\n let index = 0;\n let start;\n let end;\n\n if (Array.isArray(obj)) {\n start = \"[\";\n end = \"]\";\n const { length } = obj;\n for (; index < length; index++) {\n items.push(\n _stringify(obj[index], nextIndent, index === length - 1 ? 0 : 1) ||\n \"null\"\n );\n }\n } else {\n start = \"{\";\n end = \"}\";\n const keys = Object.keys(obj);\n const { length } = keys;\n for (; index < length; index++) {\n const key = keys[index];\n const keyPart = `${JSON.stringify(key)}: `;\n const value = _stringify(\n obj[key],\n nextIndent,\n keyPart.length + (index === length - 1 ? 0 : 1)\n );\n if (value !== undefined) {\n items.push(keyPart + value);\n }\n }\n }\n\n if (items.length > 0) {\n return [start, indent + items.join(`,\\n${nextIndent}`), end].join(\n `\\n${currentIndent}`\n );\n }\n }\n\n return string;\n })(passedObj, \"\", 0);\n}\n", "import * as CountryCoder from '@rapideditor/country-coder';\nimport * as Polyclip from 'polyclip-ts';\nimport calcArea from '@mapbox/geojson-area';\nimport circleToPolygon from 'circle-to-polygon';\nimport precision from 'geojson-precision';\nimport prettyStringify from 'json-stringify-pretty-compact';\nimport type { Polygon, MultiPolygon } from 'geojson';\n\n// Type definitions\nexport type Vec2 = [number, number];\nexport type Vec3 = [number, number, number];\nexport type Location = Vec2 | Vec3 | string | number;\n\nexport interface FeatureProperties {\n id: string;\n area: number;\n members?: string[];\n [key: string]: unknown;\n}\n\nexport type GeoJSONGeometry = Polygon | MultiPolygon;\n\nexport interface GeoJSONFeature {\n type: 'Feature';\n id: string;\n properties: FeatureProperties;\n geometry: GeoJSONGeometry;\n}\n\nexport interface FeatureCollection {\n type: 'FeatureCollection';\n features: GeoJSONFeature[];\n}\n\nexport interface LocationSet {\n include?: Location[];\n exclude?: Location[];\n}\n\nexport interface ValidatedLocation {\n type: 'point' | 'geojson' | 'countrycoder';\n location: Location;\n id: string;\n}\n\nexport interface ResolvedLocation extends ValidatedLocation {\n feature: GeoJSONFeature;\n}\n\nexport interface ValidatedLocationSet {\n type: 'locationset';\n locationSet: LocationSet;\n id: string;\n}\n\nexport interface ResolvedLocationSet extends ValidatedLocationSet {\n feature: GeoJSONFeature;\n}\n\nexport type ClipOperation = 'UNION' | 'DIFFERENCE';\n\nexport type StringifyOptions = Parameters[1];\n\nexport class LocationConflation {\n public _cache: Map;\n public strict: boolean;\n\n /**\n * Creates a new LocationConflation instance\n * @param fc - Optional FeatureCollection of known features with filename-like IDs (e.g., \"something.geojson\")\n */\n constructor(fc?: FeatureCollection) {\n // The _cache retains resolved features, so if you ask for the same thing multiple times\n // we don't repeat the expensive resolving/clipping operations.\n //\n // Each feature has a stable identifier that is used as the cache key.\n // The identifiers look like:\n // - for point locations, the stringified point: e.g. '[8.67039,49.41882]'\n // - for geojson locations, the geojson id: e.g. 'de-hamburg.geojson'\n // - for countrycoder locations, feature.id property: e.g. 'Q2' (countrycoder uses Wikidata identifiers)\n // - for aggregated locationSets, +[include]-[exclude]: e.g '+[Q2]-[Q18,Q27611]'\n this._cache = new Map();\n\n // When strict mode = true, throw on invalid locations or locationSets.\n // When strict mode = false, return `null` for invalid locations or locationSets.\n this.strict = true;\n\n // process input FeatureCollection\n if (fc?.type === 'FeatureCollection' && Array.isArray(fc.features)) {\n for (const feature of fc.features) {\n feature.properties = feature.properties || ({} as FeatureProperties);\n const props = feature.properties;\n\n // Get `id` from either `id` or `properties`\n let id = feature.id || props.id;\n if (!id || !/^\\S+\\.geojson$/i.test(id)) continue;\n\n // Ensure `id` exists and is lowercase\n id = id.toLowerCase();\n feature.id = id;\n props.id = id;\n\n // Ensure `area` property exists\n if (!props.area) {\n const area = calcArea.geometry(feature.geometry) / 1e6; // m² to km²\n props.area = Number(area.toFixed(2));\n }\n\n this._cache.set(id, feature);\n }\n }\n\n // Replace CountryCoder world geometry to be a polygon covering the world.\n const worldFeature = CountryCoder.feature('Q2');\n const world = cloneDeep(worldFeature) as unknown as GeoJSONFeature;\n world.geometry = {\n type: 'Polygon',\n coordinates: [[[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]]],\n };\n world.id = 'Q2';\n world.properties.id = 'Q2';\n world.properties.area = calcArea.geometry(world.geometry) / 1e6; // m² to km²\n this._cache.set('Q2', world);\n }\n\n /**\n * Validates a location and returns its type and stable identifier\n * @param location - Location to validate (point, geojson filename, or country code)\n * @returns Validated location object or null if invalid\n *\n * @example\n * ```typescript\n * // Point location with default 25km radius\n * loco.validateLocation([8.67039, 49.41882]);\n * // => { type: 'point', location: [8.67039, 49.41882], id: '[8.67039,49.41882]' }\n *\n * // Point location with custom radius\n * loco.validateLocation([-77.0369, 38.9072, 10]);\n * // => { type: 'point', location: [-77.0369, 38.9072, 10], id: '[-77.0369,38.9072,10]' }\n *\n * // Country code\n * loco.validateLocation('de');\n * // => { type: 'countrycoder', location: 'de', id: 'Q183' }\n *\n * // GeoJSON file\n * loco.validateLocation('philly_metro.geojson');\n * // => { type: 'geojson', location: 'philly_metro.geojson', id: 'philly_metro.geojson' }\n * ```\n */\n validateLocation(location: Location): ValidatedLocation | null {\n // [lon, lat] or [lon, lat, radius] point?\n if (Array.isArray(location) && (location.length === 2 || location.length === 3)) {\n const lon = location[0];\n const lat = location[1];\n const radius = location[2];\n\n if (\n Number.isFinite(lon) &&\n lon >= -180 &&\n lon <= 180 &&\n Number.isFinite(lat) &&\n lat >= -90 &&\n lat <= 90 &&\n (location.length === 2 || (radius !== undefined && Number.isFinite(radius) && radius > 0))\n ) {\n const id = '[' + location.toString() + ']';\n return { type: 'point', location, id };\n }\n } else if (typeof location === 'string' && /^\\S+\\.geojson$/i.test(location)) {\n // a .geojson filename?\n const id = location.toLowerCase();\n if (this._cache.has(id)) {\n return { type: 'geojson', location, id };\n }\n } else if (typeof location === 'string' || typeof location === 'number') {\n // a country-coder value?\n const feature = CountryCoder.feature(location);\n if (feature) {\n // Use wikidata QID as the identifier, since that seems to be the one\n // property that everything in CountryCoder is guaranteed to have.\n const id = feature.properties.wikidata;\n return { type: 'countrycoder', location, id };\n }\n }\n\n if (this.strict) {\n throw new Error(`validateLocation: Invalid location: \"${location}\".`);\n }\n return null;\n }\n\n /**\n * Resolves a location to a GeoJSON feature\n * @param location - Location to resolve\n * @returns Resolved location with GeoJSON feature or null if invalid\n *\n * @example\n * ```typescript\n * // Resolve a point location to a circular polygon\n * const result = loco.resolveLocation([8.67039, 49.41882]);\n * // result.feature is a GeoJSON Feature with a circular Polygon geometry\n *\n * // Resolve a country code\n * const germany = loco.resolveLocation('de');\n * // germany.feature is a GeoJSON Feature with Germany's boundary\n *\n * // Resolve a custom GeoJSON file\n * const metro = loco.resolveLocation('philly_metro.geojson');\n * // metro.feature is the pre-loaded GeoJSON Feature\n * ```\n */\n resolveLocation(location: Location): ResolvedLocation | null {\n const valid = this.validateLocation(location);\n if (!valid) return null;\n\n const id = valid.id;\n\n // Return a result from cache if we can\n if (this._cache.has(id)) {\n return { ...valid, feature: this._cache.get(id)! };\n }\n\n // A [lon,lat] coordinate pair?\n if (valid.type === 'point' && Array.isArray(location)) {\n const lon = location[0];\n const lat = location[1];\n const radius = location[2] || 25; // km\n const EDGES = 10;\n const PRECISION = 3;\n const area = Math.PI * radius * radius;\n const feature = precision(\n {\n type: 'Feature',\n id,\n properties: { id, area: Number(area.toFixed(2)) },\n geometry: circleToPolygon([lon, lat], radius * 1000, EDGES), // km to m\n },\n PRECISION\n ) as GeoJSONFeature;\n this._cache.set(id, feature);\n return { ...valid, feature };\n }\n\n // A .geojson filename?\n if (valid.type === 'geojson') {\n // nothing to do here - these are all in _cache and would have returned already\n }\n\n // A country-coder identifier?\n if (valid.type === 'countrycoder') {\n const ccFeature = CountryCoder.feature(id);\n const feature = cloneDeep(ccFeature) as unknown as GeoJSONFeature;\n const props = feature.properties;\n\n // -> This block of code is weird and requires some explanation. <-\n // CountryCoder includes higher level features which are made up of members.\n // These features don't have their own geometry, but CountryCoder provides an\n // `aggregateFeature` method to combine these members into a MultiPolygon.\n // In the past, Turf/JSTS/martinez could not handle the aggregated features,\n // so we'd iteratively union them all together. (this was slow)\n // But now mfogel/polygon-clipping handles these MultiPolygons like a boss.\n // This approach also has the benefit of removing all the internal borders and\n // simplifying the regional polygons a lot.\n if (Array.isArray(props.members)) {\n const aggregate = CountryCoder.aggregateFeature(id);\n if (aggregate) {\n const clipped = clip([aggregate as unknown as GeoJSONFeature], 'UNION');\n if (clipped) {\n feature.geometry = clipped.geometry;\n }\n }\n }\n\n // Ensure `area` property exists\n if (!props.area) {\n const area = calcArea.geometry(feature.geometry) / 1e6; // m² to km²\n props.area = Number(area.toFixed(2));\n }\n\n // Ensure `id` property exists\n feature.id = id;\n props.id = id;\n\n this._cache.set(id, feature);\n return { ...valid, feature };\n }\n\n if (this.strict) {\n throw new Error(`resolveLocation: Couldn't resolve location \"${location}\".`);\n }\n return null;\n }\n\n /**\n * Validates a locationSet and returns its stable identifier\n * @param locationSet - LocationSet with include/exclude arrays\n * @returns Validated locationSet object or null if invalid\n *\n * @example\n * ```typescript\n * // Include multiple countries\n * loco.validateLocationSet({ include: ['de', 'fr', 'it'] });\n * // => { type: 'locationset', locationSet: {...}, id: '+[Q183,Q142,Q38]' }\n *\n * // Include with exclusions\n * loco.validateLocationSet({\n * include: ['de'],\n * exclude: ['de-berlin.geojson']\n * });\n * // => { type: 'locationset', locationSet: {...}, id: '+[Q183]-[de-berlin.geojson]' }\n *\n * // Mix different location types\n * loco.validateLocationSet({\n * include: ['us', [8.67039, 49.41882], 'philly_metro.geojson']\n * });\n * ```\n */\n validateLocationSet(locationSet?: LocationSet): ValidatedLocationSet {\n locationSet = locationSet || {};\n const validator = this.validateLocation.bind(this);\n let include = (locationSet.include || []).map(validator).filter(Boolean) as ValidatedLocation[];\n const exclude = (locationSet.exclude || []).map(validator).filter(Boolean) as ValidatedLocation[];\n\n if (!include.length) {\n if (this.strict) {\n throw new Error('validateLocationSet: LocationSet includes nothing.');\n } else {\n // non-strict mode, replace an empty locationSet with one that includes \"the world\"\n locationSet.include = ['Q2'];\n include = [{ type: 'countrycoder', location: 'Q2', id: 'Q2' }];\n }\n }\n\n // Generate stable identifier\n include.sort(sortLocations);\n let id = '+[' + include.map((d) => d.id).join(',') + ']';\n if (exclude.length) {\n exclude.sort(sortLocations);\n id += '-[' + exclude.map((d) => d.id).join(',') + ']';\n }\n\n return { type: 'locationset', locationSet, id };\n }\n\n /**\n * Resolves a locationSet to a GeoJSON feature by combining included/excluded regions\n * @param locationSet - LocationSet with include/exclude arrays\n * @returns Resolved locationSet with GeoJSON feature or null if invalid\n *\n * @example\n * ```typescript\n * // Combine multiple countries into one feature\n * const benelux = loco.resolveLocationSet({\n * include: ['be', 'nl', 'lu']\n * });\n * // benelux.feature is a GeoJSON Feature with combined boundaries\n *\n * // Germany excluding Berlin\n * const germanyNoCapital = loco.resolveLocationSet({\n * include: ['de'],\n * exclude: ['de-berlin.geojson']\n * });\n * // Result is Germany with Berlin cut out\n *\n * // Complex region definition\n * const customRegion = loco.resolveLocationSet({\n * include: ['us-ca', 'us-or', 'us-wa'],\n * exclude: [[8.67039, 49.41882, 50]]\n * });\n * // West coast states minus a 50km circle\n * ```\n */\n resolveLocationSet(locationSet?: LocationSet): ResolvedLocationSet | null {\n locationSet = locationSet || {};\n const valid = this.validateLocationSet(locationSet);\n if (!valid) return null;\n\n const id = valid.id;\n\n // Return a result from cache if we can\n if (this._cache.has(id)) {\n return { ...valid, feature: this._cache.get(id)! };\n }\n\n const resolver = this.resolveLocation.bind(this);\n const includes = (locationSet.include || []).map(resolver).filter(Boolean) as ResolvedLocation[];\n const excludes = (locationSet.exclude || []).map(resolver).filter(Boolean) as ResolvedLocation[];\n\n // Return quickly if it's a single included location..\n if (includes.length === 1 && excludes.length === 0) {\n return { ...valid, feature: includes[0].feature };\n }\n\n // Calculate unions\n const includeGeoJSON = clip(includes.map((d) => d.feature), 'UNION')!;\n const excludeGeoJSON = clip(excludes.map((d) => d.feature), 'UNION');\n\n // Calculate difference, update `area` and return result\n const resultGeoJSON = excludeGeoJSON ? clip([includeGeoJSON, excludeGeoJSON], 'DIFFERENCE')! : includeGeoJSON;\n const area = calcArea.geometry(resultGeoJSON.geometry) / 1e6; // m² to km²\n resultGeoJSON.id = id;\n resultGeoJSON.properties = { id, area: Number(area.toFixed(2)) };\n\n this._cache.set(id, resultGeoJSON);\n return { ...valid, feature: resultGeoJSON };\n }\n\n /**\n * Convenience method to pretty-stringify an object\n * @param obj - Object to stringify\n * @param options - Stringify options\n * @returns Pretty-formatted JSON string\n *\n * @example\n * ```typescript\n * const result = loco.resolveLocation('de');\n * console.log(loco.stringify(result.feature));\n * // Outputs a compact, readable JSON representation of the feature\n *\n * // Custom formatting options\n * console.log(loco.stringify(result.feature, { indent: 2, maxLength: 80 }));\n * ```\n */\n stringify(obj: unknown, options?: StringifyOptions): string {\n return prettyStringify(obj, options);\n }\n}\n\n/**\n * Wraps the polyclip-ts library and returns a GeoJSON feature\n * @param features - Array of features to clip\n * @param which - Operation type (UNION or DIFFERENCE)\n * @returns Clipped GeoJSON feature or null if features array is empty\n */\nfunction clip(features: GeoJSONFeature[], which: ClipOperation): GeoJSONFeature | null {\n if (!Array.isArray(features) || !features.length) return null;\n\n const fn = { UNION: Polyclip.union, DIFFERENCE: Polyclip.difference }[which];\n const args = features.map((feature) => feature.geometry.coordinates);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const coords = (fn as any)(...args);\n\n return {\n type: 'Feature',\n properties: {} as FeatureProperties,\n geometry: {\n type: whichType(coords),\n coordinates: coords,\n } as GeoJSONGeometry,\n id: '',\n };\n\n // is this a Polygon or a MultiPolygon?\n function whichType(coords: unknown): 'Polygon' | 'MultiPolygon' {\n const a = Array.isArray(coords);\n const b = a && Array.isArray(coords[0]);\n const c = b && Array.isArray(coords[0][0]);\n const d = c && Array.isArray(coords[0][0][0]);\n return d ? 'MultiPolygon' : 'Polygon';\n }\n}\n\n/**\n * Deep clones an object using JSON serialization\n * @param obj - Object to clone\n * @returns Cloned object\n */\nfunction cloneDeep(obj: T): T {\n return JSON.parse(JSON.stringify(obj));\n}\n\n/**\n * Sorting function for locations to generate deterministic IDs\n * Sorting the location lists is ok because they end up unioned together.\n * @param a - First location\n * @param b - Second location\n * @returns Sort order\n */\nfunction sortLocations(a: ValidatedLocation, b: ValidatedLocation): number {\n const rank = { countrycoder: 1, geojson: 2, point: 3 };\n const aRank = rank[a.type];\n const bRank = rank[b.type];\n\n return aRank > bRank ? 1 : aRank < bRank ? -1 : a.id.localeCompare(b.id);\n}\n\nexport default LocationConflation;\n", "import { LocationConflation, type LocationSet, type GeoJSONFeature, type FeatureProperties, type FeatureCollection } from '@rapideditor/location-conflation';\nimport whichPolygon from 'which-polygon';\nimport calcArea from '@mapbox/geojson-area';\nimport type { Vec2 } from '../geo/vector';\n\nexport interface ObjectWithLocationSet {\n locationSetID?: string;\n locationSet: LocationSet;\n}\n\nconst _loco = new LocationConflation(); // instance of a location-conflation resolver\n\n\n/**\n * `LocationManager` maintains an internal index of all the boundaries/geofences used by iD.\n * It's used by presets, community index, background imagery, to know where in the world these things are valid.\n * These geofences should be defined by `locationSet` objects:\n *\n * let locationSet = {\n * include: [ Array of locations ],\n * exclude: [ Array of locations ]\n * };\n *\n * For more info see the location-conflation and country-coder projects, see:\n * https://github.com/rapideditor/location-conflation\n * https://github.com/rapideditor/country-coder\n */\nexport class LocationManager {\n /** A which-polygon index */\n _wp!: whichPolygon.Query;\n /** Map (id -> GeoJSON feature) */\n _resolved = new Map();\n /** Map (locationSetID -> Number area) */\n _knownLocationSets = new Map();\n /** Map (locationID -> Set(locationSetID) ) */\n _locationIncludedIn = new Map>();\n /** Map (locationID -> Set(locationSetID) ) */\n _locationExcludedIn = new Map>();\n\n /**\n * @constructor\n */\n constructor() {\n // pre-resolve the worldwide locationSet\n const world = { locationSet: { include: ['Q2'] } };\n this._resolveLocationSet(world);\n this._rebuildIndex();\n }\n\n\n /**\n * _validateLocationSet\n * Pass an Object with a `locationSet` property.\n * Validates the `locationSet` and sets a `locationSetID` property on the object.\n * To avoid so much computation we only resolve the include and exclude regions, but not the locationSet itself.\n *\n * Use `_resolveLocationSet()` instead if you need to resolve geojson of locationSet, for example to render it.\n * Note: You need to call `_rebuildIndex()` after you're all finished validating the locationSets.\n *\n * @param `obj` Object to check, it should have `locationSet` property\n */\n _validateLocationSet(obj: ObjectWithLocationSet) {\n if (obj.locationSetID) return; // work was done already\n\n try {\n let locationSet = obj.locationSet;\n if (!locationSet) {\n throw new Error('object missing locationSet property');\n }\n if (!locationSet.include) { // missing `include`, default to worldwide include\n locationSet.include = ['Q2']; // https://github.com/openstreetmap/iD/pull/8305#discussion_r662344647\n }\n\n // Validate the locationSet only\n // Resolve the include/excludes\n const locationSetID = _loco.validateLocationSet(locationSet).id;\n obj.locationSetID = locationSetID;\n if (this._knownLocationSets.has(locationSetID)) return; // seen one like this before\n\n let area = 0;\n\n // Resolve and index the 'includes'\n (locationSet.include || []).forEach(location => {\n const locationID = _loco.validateLocation(location)!.id;\n let geojson = this._resolved.get(locationID);\n\n if (!geojson) { // first time seeing a location like this\n geojson = _loco.resolveLocation(location)!.feature; // resolve to GeoJSON\n this._resolved.set(locationID, geojson);\n }\n area += geojson.properties.area;\n\n let s = this._locationIncludedIn.get(locationID);\n if (!s) {\n s = new Set();\n this._locationIncludedIn.set(locationID, s);\n }\n s.add(locationSetID);\n });\n\n // Resolve and index the 'excludes'\n (locationSet.exclude || []).forEach(location => {\n const locationID = _loco.validateLocation(location)!.id;\n let geojson = this._resolved.get(locationID);\n\n if (!geojson) { // first time seeing a location like this\n geojson = _loco.resolveLocation(location)!.feature; // resolve to GeoJSON\n this._resolved.set(locationID, geojson);\n }\n area -= geojson.properties.area;\n\n let s = this._locationExcludedIn.get(locationID);\n if (!s) {\n s = new Set();\n this._locationExcludedIn.set(locationID, s);\n }\n s.add(locationSetID);\n });\n\n this._knownLocationSets.set(locationSetID, area);\n\n } catch {\n obj.locationSet = { include: ['Q2'] }; // default worldwide\n obj.locationSetID = '+[Q2]';\n }\n }\n\n\n /**\n * _resolveLocationSet\n * Does everything that `_validateLocationSet()` does, but then \"resolves\" the locationSet into GeoJSON.\n * This step is a bit more computationally expensive, so really only needed if you intend to render the shape.\n *\n * Note: You need to call `_rebuildIndex()` after you're all finished validating the locationSets.\n *\n * @param `obj` Object to check, it should have `locationSet` property\n */\n _resolveLocationSet(obj: ObjectWithLocationSet) {\n this._validateLocationSet(obj);\n\n if (this._resolved.has(obj.locationSetID)) return; // work was done already\n\n try {\n const result = _loco.resolveLocationSet(obj.locationSet)!;\n const locationSetID = result.id;\n obj.locationSetID = locationSetID;\n\n if (!result.feature.geometry.coordinates.length || !result.feature.properties.area) {\n throw new Error(`locationSet ${locationSetID} resolves to an empty feature.`);\n }\n\n let geojson = JSON.parse(JSON.stringify(result.feature)); // deep clone\n geojson.id = locationSetID; // Important: always use the locationSet `id` (`+[Q30]`), not the feature `id` (`Q30`)\n geojson.properties.id = locationSetID;\n this._resolved.set(locationSetID, geojson);\n\n } catch {\n obj.locationSet = { include: ['Q2'] }; // default worldwide\n obj.locationSetID = '+[Q2]';\n }\n }\n\n\n /**\n * _rebuildIndex\n * Rebuilds the whichPolygon index with whatever features have been resolved into GeoJSON.\n */\n _rebuildIndex() {\n this._wp = whichPolygon({\n type: 'FeatureCollection',\n features: [...this._resolved.values()],\n });\n }\n\n\n /**\n * mergeCustomGeoJSON\n * Accepts a FeatureCollection-like object containing custom locations\n * Each feature must have a filename-like `id`, for example: `something.geojson`\n * {\n * \"type\": \"FeatureCollection\"\n * \"features\": [\n * {\n * \"type\": \"Feature\",\n * \"id\": \"philly_metro.geojson\",\n * \"properties\": { \u2026 },\n * \"geometry\": { \u2026 }\n * }\n * ]\n * }\n *\n * @param `fc` FeatureCollection-like Object containing custom locations\n */\n mergeCustomGeoJSON(fc: FeatureCollection) {\n if (!fc || fc.type !== 'FeatureCollection' || !Array.isArray(fc.features)) return;\n\n fc.features.forEach(feature => {\n feature.properties = feature.properties || {};\n let props = feature.properties;\n\n // Get `id` from either `id` or `properties`\n let id = feature.id || props.id;\n if (!id || !/^\\S+\\.geojson$/i.test(id)) return;\n\n // Ensure `id` exists and is lowercase\n id = id.toLowerCase();\n feature.id = id;\n props.id = id;\n\n // Ensure `area` property exists\n if (!props.area) {\n const area = calcArea.geometry(feature.geometry) / 1e6; // m\u00B2 to km\u00B2\n props.area = Number(area.toFixed(2));\n }\n\n _loco._cache.set(id, feature); // insert directly into LocationConflations internal cache\n });\n }\n\n\n /**\n * mergeLocationSets\n * Accepts an Array of Objects containing `locationSet` properties:\n * [\n * { id: 'preset1', locationSet: {\u2026} },\n * { id: 'preset2', locationSet: {\u2026} },\n * \u2026\n * ]\n * After validating, the Objects will be decorated with a `locationSetID` property:\n * [\n * { id: 'preset1', locationSet: {\u2026}, locationSetID: '+[Q2]' },\n * { id: 'preset2', locationSet: {\u2026}, locationSetID: '+[Q30]' },\n * \u2026\n * ]\n *\n * @param `objects` Objects to check - they should have `locationSet` property\n * @return Promise resolved true (this function used to be slow/async, now it's faster and sync)\n */\n mergeLocationSets(objects: ObjectWithLocationSet[]) {\n if (!Array.isArray(objects)) return Promise.reject('nothing to do');\n\n objects.forEach(obj => this._validateLocationSet(obj));\n this._rebuildIndex();\n return Promise.resolve(objects);\n }\n\n\n /**\n * locationSetID\n * Returns a locationSetID for a given locationSet (fallback to `+[Q2]`, world)\n * (The locationSet doesn't necessarily need to be resolved to compute its `id`)\n *\n * @param `locationSet` A locationSet Object, e.g. `{ include: ['us'] }`\n * @return String locationSetID, e.g. `+[Q30]`\n */\n locationSetID(locationSet: LocationSet) {\n let locationSetID;\n try {\n locationSetID = _loco.validateLocationSet(locationSet).id;\n } catch {\n locationSetID = '+[Q2]'; // the world\n }\n return locationSetID;\n }\n\n\n /**\n * feature\n * Returns the resolved GeoJSON feature for a given locationSetID (fallback to 'world')\n * A GeoJSON feature:\n * {\n * type: 'Feature',\n * id: '+[Q30]',\n * properties: { id: '+[Q30]', area: 21817019.17, \u2026 },\n * geometry: { \u2026 }\n * }\n *\n * @param `locationSetID` String identifier, e.g. `+[Q30]`\n * @return GeoJSON object (fallback to world)\n */\n feature(locationSetID = '+[Q2]') {\n const feature = this._resolved.get(locationSetID);\n return feature || this._resolved.get('+[Q2]');\n }\n\n\n /**\n * locationSetsAt\n * Find all the locationSets valid at the given location.\n * Results include the area (in km\u00B2) to facilitate sorting.\n *\n * Object of locationSetIDs to areas (in km\u00B2)\n * {\n * \"+[Q2]\": 511207893.3958111,\n * \"+[Q30]\": 21817019.17,\n * \"+[new_jersey.geojson]\": 22390.77,\n * \u2026\n * }\n *\n * @param `loc` `[lon,lat]` location to query, e.g. `[-74.4813, 40.7967]`\n * @return Object of locationSetIDs valid at given location\n */\n locationSetsAt(loc: Vec2) {\n const result: { [locationSetID: string]: number } = {};\n\n const hits = this._wp(loc, true) || [];\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n const thiz = this;\n\n // locationSets\n hits.forEach(prop => {\n if (prop.id[0] !== '+') return; // skip - it's a location\n const locationSetID = prop.id;\n const area = thiz._knownLocationSets.get(locationSetID);\n if (area) {\n result[locationSetID] = area;\n }\n });\n\n // locations included\n hits.forEach(prop => {\n if (prop.id[0] === '+') return; // skip - it's a locationset\n const locationID = prop.id;\n const included = thiz._locationIncludedIn.get(locationID);\n (included || []).forEach(locationSetID => {\n const area = thiz._knownLocationSets.get(locationSetID);\n if (area) {\n result[locationSetID] = area;\n }\n });\n });\n\n // locations excluded\n hits.forEach(prop => {\n if (prop.id[0] === '+') return; // skip - it's a locationset\n const locationID = prop.id;\n const excluded = thiz._locationExcludedIn.get(locationID);\n (excluded || []).forEach(locationSetID => {\n delete result[locationSetID];\n });\n });\n\n return result;\n }\n\n\n // Direct access to the location-conflation resolver\n loco() {\n return _loco;\n }\n}\n\n\nconst _sharedLocationManager = new LocationManager();\nexport { _sharedLocationManager as locationManager };\n\n", "import { t, localizer } from '../../core/localizer';\nimport { geoSphericalDistance, geoVecNormalizedDot } from '../../geo';\nimport { uiCmd } from '../cmd';\n\nexport function pointBox(loc, context) {\n var rect = context.surfaceRect();\n var point = context.curtainProjection(loc);\n return {\n left: point[0] + rect.left - 40,\n top: point[1] + rect.top - 60,\n width: 80,\n height: 90\n };\n}\n\n\nexport function pad(locOrBox, padding, context) {\n var box;\n if (locOrBox instanceof Array) {\n var rect = context.surfaceRect();\n var point = context.curtainProjection(locOrBox);\n box = {\n left: point[0] + rect.left,\n top: point[1] + rect.top\n };\n } else {\n box = locOrBox;\n }\n\n return {\n left: box.left - padding,\n top: box.top - padding,\n width: (box.width || 0) + 2 * padding,\n height: (box.width || 0) + 2 * padding\n };\n}\n\n\nexport function icon(name, svgklass, useklass) {\n return '' +\n '';\n}\n\nvar helpStringReplacements;\n\n// Returns the localized HTML element for `id` with a standardized set of icon, key, and\n// label replacements suitable for tutorials and documentation. Optionally supplemented\n// with custom `replacements`\nexport function helpHtml(id, replacements) {\n // only load these the first time\n if (!helpStringReplacements) {\n /* eslint-disable sort-keys */\n helpStringReplacements = {\n // insert icons corresponding to various UI elements\n point_icon: icon('#iD-icon-point', 'inline'),\n line_icon: icon('#iD-icon-line', 'inline'),\n area_icon: icon('#iD-icon-area', 'inline'),\n note_icon: icon('#iD-icon-note', 'inline add-note'),\n plus: icon('#iD-icon-plus', 'inline'),\n minus: icon('#iD-icon-minus', 'inline'),\n layers_icon: icon('#iD-icon-layers', 'inline'),\n data_icon: icon('#iD-icon-data', 'inline'),\n inspect: icon('#iD-icon-inspect', 'inline'),\n help_icon: icon('#iD-icon-help', 'inline'),\n undo_icon: icon(localizer.textDirection() === 'rtl' ? '#iD-icon-redo' : '#iD-icon-undo', 'inline'),\n redo_icon: icon(localizer.textDirection() === 'rtl' ? '#iD-icon-undo' : '#iD-icon-redo', 'inline'),\n save_icon: icon('#iD-icon-save', 'inline'),\n\n // operation icons\n circularize_icon: icon('#iD-operation-circularize', 'inline operation'),\n continue_icon: icon('#iD-operation-continue', 'inline operation'),\n copy_icon: icon('#iD-operation-copy', 'inline operation'),\n delete_icon: icon('#iD-operation-delete', 'inline operation'),\n disconnect_icon: icon('#iD-operation-disconnect', 'inline operation'),\n downgrade_icon: icon('#iD-operation-downgrade', 'inline operation'),\n extract_icon: icon('#iD-operation-extract', 'inline operation'),\n merge_icon: icon('#iD-operation-merge', 'inline operation'),\n move_icon: icon('#iD-operation-move', 'inline operation'),\n orthogonalize_icon: icon('#iD-operation-orthogonalize', 'inline operation'),\n paste_icon: icon('#iD-operation-paste', 'inline operation'),\n reflect_long_icon: icon('#iD-operation-reflect-long', 'inline operation'),\n reflect_short_icon: icon('#iD-operation-reflect-short', 'inline operation'),\n reverse_icon: icon('#iD-operation-reverse', 'inline operation'),\n rotate_icon: icon('#iD-operation-rotate', 'inline operation'),\n split_icon: icon('#iD-operation-split', 'inline operation'),\n straighten_icon: icon('#iD-operation-straighten', 'inline operation'),\n\n // interaction icons\n leftclick: icon('#iD-walkthrough-mouse-left', 'inline operation'),\n rightclick: icon('#iD-walkthrough-mouse-right', 'inline operation'),\n mousewheel_icon: icon('#iD-walkthrough-mousewheel', 'inline operation'),\n tap_icon: icon('#iD-walkthrough-tap', 'inline operation'),\n doubletap_icon: icon('#iD-walkthrough-doubletap', 'inline operation'),\n longpress_icon: icon('#iD-walkthrough-longpress', 'inline operation'),\n touchdrag_icon: icon('#iD-walkthrough-touchdrag', 'inline operation'),\n pinch_icon: icon('#iD-walkthrough-pinch-apart', 'inline operation'),\n\n // insert keys; may be localized and platform-dependent\n shift: uiCmd.display('\u21E7'),\n alt: uiCmd.display('\u2325'),\n return: uiCmd.display('\u21B5'),\n esc: t.html('shortcuts.key.esc'),\n space: t.html('shortcuts.key.space'),\n add_note_key: t.html('modes.add_note.key'),\n help_key: t.html('help.key'),\n shortcuts_key: t.html('shortcuts.toggle.key'),\n\n // reference localized UI labels directly so that they'll always match\n save: t.html('save.title'),\n undo: t.html('undo.title'),\n redo: t.html('redo.title'),\n upload: t.html('commit.save'),\n point: t.html('modes.add_point.title'),\n line: t.html('modes.add_line.title'),\n area: t.html('modes.add_area.title'),\n note: t.html('modes.add_note.label'),\n\n circularize: t.html('operations.circularize.title'),\n continue: t.html('operations.continue.title'),\n copy: t.html('operations.copy.title'),\n delete: t.html('operations.delete.title'),\n disconnect: t.html('operations.disconnect.title'),\n downgrade: t.html('operations.downgrade.title'),\n extract: t.html('operations.extract.title'),\n merge: t.html('operations.merge.title'),\n move: t.html('operations.move.title'),\n orthogonalize: t.html('operations.orthogonalize.title'),\n paste: t.html('operations.paste.title'),\n reflect_long: t.html('operations.reflect.title.long'),\n reflect_short: t.html('operations.reflect.title.short'),\n reverse: t.html('operations.reverse.title'),\n rotate: t.html('operations.rotate.title'),\n split: t.html('operations.split.title'),\n straighten: t.html('operations.straighten.title'),\n\n map_data: t.html('map_data.title'),\n osm_notes: t.html('map_data.layers.notes.title'),\n fields: t.html('inspector.fields'),\n tags: t.html('inspector.tags'),\n relations: t.html('inspector.relations'),\n new_relation: t.html('inspector.new_relation'),\n turn_restrictions: t.html('_tagging.presets.fields.restrictions.label'),\n background_settings: t.html('background.description'),\n imagery_offset: t.html('background.fix_misalignment'),\n start_the_walkthrough: t.html('splash.walkthrough'),\n help: t.html('help.title'),\n ok: t.html('intro.ok')\n };\n /* eslint-enable sort-keys */\n for (var key in helpStringReplacements) {\n helpStringReplacements[key] = { html: helpStringReplacements[key] };\n }\n }\n\n var reps;\n if (replacements) {\n reps = Object.assign(replacements, helpStringReplacements);\n } else {\n reps = helpStringReplacements;\n }\n\n return t.html(id, reps)\n // use keyboard key styling for shortcuts\n .replace(/\\`(.*?)\\`/g, '$1');\n}\n\n\nfunction slugify(text) {\n return text.toString().toLowerCase()\n .replace(/\\s+/g, '-') // Replace spaces with -\n .replace(/[^\\w\\-]+/g, '') // Remove all non-word chars\n .replace(/\\-\\-+/g, '-') // Replace multiple - with single -\n .replace(/^-+/, '') // Trim - from start of text\n .replace(/-+$/, ''); // Trim - from end of text\n}\n\n\n// console warning for missing walkthrough names\nexport var missingStrings = {};\nfunction checkKey(key, text) {\n if (t(key, { default: undefined}) === undefined) {\n if (missingStrings.hasOwnProperty(key)) return; // warn once\n missingStrings[key] = text;\n var missing = key + ': ' + text;\n if (typeof console !== 'undefined') console.log(missing); // eslint-disable-line\n }\n}\n\n\nexport function localize(obj) {\n var key;\n\n // Assign name if entity has one..\n var name = obj.tags && obj.tags.name;\n if (name) {\n key = 'intro.graph.name.' + slugify(name);\n obj.tags.name = t(key, { default: name });\n checkKey(key, name);\n }\n\n // Assign street name if entity has one..\n var street = obj.tags && obj.tags['addr:street'];\n if (street) {\n key = 'intro.graph.name.' + slugify(street);\n obj.tags['addr:street'] = t(key, { default: street });\n checkKey(key, street);\n\n // Add address details common across walkthrough..\n var addrTags = [\n 'block_number', 'city', 'county', 'district', 'hamlet', 'neighbourhood',\n 'postcode', 'province', 'quarter', 'state', 'subdistrict', 'suburb'\n ];\n addrTags.forEach(function(k) {\n var key = 'intro.graph.' + k;\n var tag = 'addr:' + k;\n var val = obj.tags && obj.tags[tag];\n var str = t(key, { default: val });\n\n if (str) {\n if (str.match(/^<.*>$/) !== null) {\n delete obj.tags[tag];\n } else {\n obj.tags[tag] = str;\n }\n }\n });\n }\n\n return obj;\n}\n\n\n// Used to detect squareness.. some duplicataion of code from actionOrthogonalize.\nexport function isMostlySquare(points) {\n // note: uses 15 here instead of the 12 from actionOrthogonalize because\n // actionOrthogonalize can actually straighten some larger angles as it iterates\n var threshold = 15; // degrees within right or straight\n var lowerBound = Math.cos((90 - threshold) * Math.PI / 180); // near right\n var upperBound = Math.cos(threshold * Math.PI / 180); // near straight\n\n for (var i = 0; i < points.length; i++) {\n var a = points[(i - 1 + points.length) % points.length];\n var origin = points[i];\n var b = points[(i + 1) % points.length];\n\n var dotp = geoVecNormalizedDot(a, b, origin);\n var mag = Math.abs(dotp);\n if (mag > lowerBound && mag < upperBound) {\n return false;\n }\n }\n\n return true;\n}\n\n\nexport function selectMenuItem(context, operation) {\n return context.container().select('.edit-menu .edit-menu-item-' + operation);\n}\n\n\nexport function transitionTime(point1, point2) {\n var distance = geoSphericalDistance(point1, point2);\n if (distance === 0) {\n return 0;\n } else if (distance < 80) {\n return 500;\n } else {\n return 1000;\n }\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { marked } from 'marked';\nimport { t, localizer } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { icon } from './intro/helper';\n\n\n// This currently only works with the 'restrictions' field\n// It borrows some code from uiHelp\n\nexport function uiFieldHelp(context, fieldName) {\n var fieldHelp = {};\n var _inspector = d3_select(null);\n var _wrap = d3_select(null);\n var _body = d3_select(null);\n\n var fieldHelpKeys = {\n restrictions: [\n ['about',[\n 'about',\n 'from_via_to',\n 'maxdist',\n 'maxvia'\n ]],\n ['inspecting',[\n 'about',\n 'from_shadow',\n 'allow_shadow',\n 'restrict_shadow',\n 'only_shadow',\n 'restricted',\n 'only'\n ]],\n ['modifying',[\n 'about',\n 'indicators',\n 'allow_turn',\n 'restrict_turn',\n 'only_turn'\n ]],\n ['tips',[\n 'simple',\n 'simple_example',\n 'indirect',\n 'indirect_example',\n 'indirect_noedit'\n ]]\n ]\n };\n\n var fieldHelpHeadings = {};\n\n var replacements = {\n distField: { html: t.html('restriction.controls.distance') },\n viaField: { html: t.html('restriction.controls.via') },\n fromShadow: { html: icon('#iD-turn-shadow', 'inline shadow from') },\n allowShadow: { html: icon('#iD-turn-shadow', 'inline shadow allow') },\n restrictShadow: { html: icon('#iD-turn-shadow', 'inline shadow restrict') },\n onlyShadow: { html: icon('#iD-turn-shadow', 'inline shadow only') },\n allowTurn: { html: icon('#iD-turn-yes', 'inline turn') },\n restrictTurn: { html: icon('#iD-turn-no', 'inline turn') },\n onlyTurn: { html: icon('#iD-turn-only', 'inline turn') }\n };\n\n\n // For each section, squash all the texts into a single markdown document\n var docs = fieldHelpKeys[fieldName].map(function(key) {\n var helpkey = 'help.field.' + fieldName + '.' + key[0];\n var text = key[1].reduce(function(all, part) {\n var subkey = helpkey + '.' + part;\n var depth = fieldHelpHeadings[subkey]; // is this subkey a heading?\n var hhh = depth ? Array(depth + 1).join('#') + ' ' : ''; // if so, prepend with some ##'s\n return all + hhh + t.html(subkey, replacements) + '\\n\\n';\n }, '');\n\n return {\n key: helpkey,\n title: t.html(helpkey + '.title'),\n html: marked(text.trim())\n };\n });\n\n\n function show() {\n updatePosition();\n\n _body\n .classed('hide', false)\n .style('opacity', '0')\n .transition()\n .duration(200)\n .style('opacity', '1');\n }\n\n\n function hide() {\n _body\n .classed('hide', true)\n .transition()\n .duration(200)\n .style('opacity', '0')\n .on('end', function () {\n _body.classed('hide', true);\n });\n }\n\n\n function clickHelp(index) {\n var d = docs[index];\n var tkeys = fieldHelpKeys[fieldName][index][1];\n\n _body.selectAll('.field-help-nav-item')\n .classed('active', function(d, i) { return i === index; });\n\n var content = _body.selectAll('.field-help-content')\n .html(d.html);\n\n // class the paragraphs so we can find and style them\n content.selectAll('p')\n .attr('class', function(d, i) { return tkeys[i]; });\n\n // insert special content for certain help sections\n if (d.key === 'help.field.restrictions.inspecting') {\n content\n .insert('img', 'p.from_shadow')\n .attr('class', 'field-help-image cf')\n .attr('src', context.imagePath('tr_inspect.gif'));\n\n } else if (d.key === 'help.field.restrictions.modifying') {\n content\n .insert('img', 'p.allow_turn')\n .attr('class', 'field-help-image cf')\n .attr('src', context.imagePath('tr_modify.gif'));\n }\n }\n\n\n fieldHelp.button = function(selection) {\n if (_body.empty()) return;\n\n var button = selection.selectAll('.field-help-button')\n .data([0]);\n\n // enter/update\n button.enter()\n .append('button')\n .attr('class', 'field-help-button')\n .call(svgIcon('#iD-icon-help'))\n .merge(button)\n .on('click', function (d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n if (_body.classed('hide')) {\n show();\n } else {\n hide();\n }\n });\n };\n\n\n function updatePosition() {\n var wrap = _wrap.node();\n var inspector = _inspector.node();\n var wRect = wrap.getBoundingClientRect();\n var iRect = inspector.getBoundingClientRect();\n\n _body\n .style('top', wRect.top + inspector.scrollTop - iRect.top + 'px');\n }\n\n\n fieldHelp.body = function(selection) {\n // This control expects the field to have a form-field-input-wrap div\n _wrap = selection.selectAll('.form-field-input-wrap');\n if (_wrap.empty()) return;\n\n // absolute position relative to the inspector, so it \"floats\" above the fields\n _inspector = context.container().select('.sidebar .entity-editor-pane .inspector-body');\n if (_inspector.empty()) return;\n\n _body = _inspector.selectAll('.field-help-body')\n .data([0]);\n\n var enter = _body.enter()\n .append('div')\n .attr('class', 'field-help-body hide'); // initially hidden\n\n var titleEnter = enter\n .append('div')\n .attr('class', 'field-help-title cf');\n\n titleEnter\n .append('h2')\n .attr('class', ((localizer.textDirection() === 'rtl') ? 'fr' : 'fl'))\n .call(t.append('help.field.' + fieldName + '.title'));\n\n titleEnter\n .append('button')\n .attr('class', 'fr close')\n .attr('title', t('icons.close'))\n .on('click', function(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n hide();\n })\n .call(svgIcon('#iD-icon-close'));\n\n var navEnter = enter\n .append('div')\n .attr('class', 'field-help-nav cf');\n\n var titles = docs.map(function(d) { return d.title; });\n navEnter.selectAll('.field-help-nav-item')\n .data(titles)\n .enter()\n .append('div')\n .attr('class', 'field-help-nav-item')\n .html(function(d) { return d; })\n .on('click', function(d3_event, d) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n clickHelp(titles.indexOf(d));\n });\n\n enter\n .append('div')\n .attr('class', 'field-help-content');\n\n _body = _body\n .merge(enter);\n\n clickHelp(0);\n };\n\n\n return fieldHelp;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport {\n select as d3_select\n} from 'd3-selection';\nimport { omit } from 'es-toolkit/compat';\n\nimport { utilRebind } from '../../util/rebind';\nimport { t } from '../../core/localizer';\nimport { actionReverse } from '../../actions/reverse';\nimport { svgIcon } from '../../svg/icon';\nimport { utilCheckTagDictionary } from '../../util';\nimport { osmOneWayTags } from '../../osm/tags';\n\nexport { uiFieldCheck as uiFieldDefaultCheck };\nexport { uiFieldCheck as uiFieldOnewayCheck };\n\n\nexport function uiFieldCheck(field, context) {\n var dispatch = d3_dispatch('change');\n var options = field.options;\n var values = [];\n var texts = [];\n\n var _tags;\n\n var input = d3_select(null);\n var text = d3_select(null);\n var label = d3_select(null);\n var reverser = d3_select(null);\n\n var _impliedYes;\n var _entityIDs = [];\n var _value;\n\n\n var stringsField = field.resolveReference('stringsCrossReference');\n if (!options && stringsField.options) {\n options = stringsField.options;\n }\n\n if (options) {\n for (var i in options) {\n var v = options[i];\n values.push(v === 'undefined' ? undefined : v);\n texts.push(stringsField.t.append('options.' + v, { 'default': v }));\n }\n } else {\n values = [undefined, 'yes'];\n texts = [t.append('inspector.unknown'), t.append('inspector.check.yes')];\n if (field.type !== 'defaultCheck') {\n values.push('no');\n texts.push(t.append('inspector.check.no'));\n }\n }\n\n\n // Checks tags to see whether an undefined value is \"Assumed to be Yes\"\n function checkImpliedYes() {\n _impliedYes = (field.id === 'oneway_yes');\n\n // hack: pretend `oneway` field is a `oneway_yes` field\n // where implied oneway tag exists (e.g. `junction=roundabout`) #2220, #1841\n if (field.id === 'oneway') {\n var entity = context.entity(_entityIDs[0]);\n if (entity.type === 'way' && !!utilCheckTagDictionary(entity.tags, omit(osmOneWayTags, 'oneway'))) {\n _impliedYes = true;\n texts[0] = t.append('_tagging.presets.fields.oneway_yes.options.undefined');\n }\n }\n }\n\n\n function reverserHidden() {\n if (!context.container().select('div.inspector-hover').empty()) return true;\n return !(_value === 'yes' || (_impliedYes && !_value));\n }\n\n\n function reverserSetText(selection) {\n var entity = _entityIDs.length && context.hasEntity(_entityIDs[0]);\n if (reverserHidden() || !entity) return selection;\n\n var first = entity.first();\n var last = entity.isClosed() ? entity.nodes[entity.nodes.length - 2] : entity.last();\n var pseudoDirection = first < last;\n var icon = pseudoDirection ? '#iD-icon-forward' : '#iD-icon-backward';\n\n selection.selectAll('.reverser-span')\n .text('')\n .call(t.append('inspector.check.reverser'))\n .call(svgIcon(icon, 'inline'));\n\n return selection;\n }\n\n\n var check = function(selection) {\n checkImpliedYes();\n\n label = selection.selectAll('.form-field-input-wrap')\n .data([0]);\n\n var enter = label.enter()\n .append('label')\n .attr('class', 'form-field-input-wrap form-field-input-check');\n\n enter\n .append('input')\n .property('indeterminate', field.type !== 'defaultCheck')\n .attr('type', 'checkbox')\n .attr('id', field.domId);\n\n enter\n .append('span')\n .call(texts[0])\n .attr('class', 'value');\n\n if (field.type === 'onewayCheck') {\n enter\n .append('button')\n .attr('class', 'reverser' + (reverserHidden() ? ' hide' : ''))\n .append('span')\n .attr('class', 'reverser-span');\n }\n\n label = label.merge(enter);\n input = label.selectAll('input');\n text = label.selectAll('span.value');\n\n input\n .on('click', function(d3_event) {\n d3_event.stopPropagation();\n var t = {};\n\n if (Array.isArray(_tags[field.key])) {\n if (values.indexOf('yes') !== -1) {\n t[field.key] = 'yes';\n } else {\n t[field.key] = values[0];\n }\n } else {\n t[field.key] = values[(values.indexOf(_value) + 1) % values.length];\n }\n\n // Don't cycle through `alternating` or `reversible` states - #4970\n // (They are supported as translated strings, but should not toggle with clicks)\n if (t[field.key] === 'reversible' || t[field.key] === 'alternating') {\n t[field.key] = values[0];\n }\n\n dispatch.call('change', this, t);\n });\n\n if (field.type === 'onewayCheck') {\n reverser = label.selectAll('.reverser');\n\n reverser\n .call(reverserSetText)\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n context.perform(\n function(graph) {\n for (var i in _entityIDs) {\n graph = actionReverse(_entityIDs[i])(graph);\n }\n return graph;\n },\n t('operations.reverse.annotation.line', { n: 1 })\n );\n\n // must manually revalidate since no 'change' event was called\n context.validator().validate();\n\n d3_select(this)\n .call(reverserSetText);\n });\n }\n };\n\n\n check.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return check;\n };\n\n\n check.tags = function(tags) {\n\n _tags = tags;\n\n function isChecked(val) {\n return val !== 'no' && val !== '' && val !== undefined && val !== null;\n }\n\n function textFor(val) {\n if (val === '') val = undefined;\n var index = values.indexOf(val);\n return (index !== -1 ? texts[index] : ('\"' + val + '\"'));\n }\n\n checkImpliedYes();\n\n var isMixed = Array.isArray(tags[field.key]);\n\n _value = !isMixed && tags[field.key] && tags[field.key].toLowerCase();\n\n if (field.type === 'onewayCheck' && (_value === '1' || _value === '-1')) {\n _value = 'yes';\n }\n\n input\n .property('indeterminate', isMixed || (field.type !== 'defaultCheck' && !_value))\n .property('checked', isChecked(_value));\n\n const textForValue = textFor(_value);\n text\n .text('')\n .call(isMixed\n ? t.append('inspector.multiple_values')\n : typeof textForValue === 'string' ? selection => selection.text(textForValue) : textForValue)\n .classed('mixed', isMixed);\n\n label\n .classed('set', !!_value);\n\n if (field.type === 'onewayCheck') {\n reverser\n .classed('hide', reverserHidden())\n .call(reverserSetText);\n }\n };\n\n\n check.focus = function() {\n input.node().focus();\n };\n\n return utilRebind(check, dispatch, 'on');\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg';\nimport {\n utilUnicodeCharsCount,\n utilCleanOsmString\n} from '../util';\nimport { uiPopover } from './popover';\n\n\nexport function uiLengthIndicator(maxChars) {\n var _wrap = d3_select(null);\n var _tooltip = uiPopover('tooltip max-length-warning')\n .placement('bottom')\n .hasArrow(true)\n .content(() => selection => {\n selection.text('');\n selection.call(svgIcon('#iD-icon-alert', 'inline'));\n selection.call(t.append('inspector.max_length_reached', { maxChars }));\n });\n var _silent = false;\n\n var lengthIndicator = function(selection) {\n _wrap = selection.selectAll('span.length-indicator-wrap').data([0]);\n _wrap = _wrap.enter()\n .append('span')\n .merge(_wrap)\n .classed('length-indicator-wrap', true);\n selection.call(_tooltip);\n };\n\n lengthIndicator.update = function(val) {\n const strLen = utilUnicodeCharsCount(utilCleanOsmString(val, Number.POSITIVE_INFINITY));\n\n let indicator = _wrap.selectAll('span.length-indicator')\n .data([strLen]);\n\n indicator.enter()\n .append('span')\n .merge(indicator)\n .classed('length-indicator', true)\n .classed('limit-reached', d => d > maxChars)\n .style('border-right-width', d => `${Math.abs(maxChars - d) * 2}px`)\n .style('margin-right', d => d > maxChars\n ? `${(maxChars - d) * 2}px`\n : 0)\n .style('opacity', d => d > maxChars * 0.8\n ? Math.min(1, (d / maxChars - 0.8) / (1 - 0.8))\n : 0)\n .style('pointer-events', d => d > maxChars * 0.8 ? null: 'none');\n\n if (_silent) return;\n if (strLen > maxChars) {\n _tooltip.show();\n } else {\n _tooltip.hide();\n }\n };\n\n lengthIndicator.silent = function(val) {\n if (!arguments.length) return _silent;\n _silent = val;\n return lengthIndicator;\n };\n\n return lengthIndicator;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { drag as d3_drag } from 'd3-drag';\nimport * as countryCoder from '@rapideditor/country-coder';\n\nimport { fileFetcher } from '../../core/file_fetcher';\nimport { localizer, t } from '../../core/localizer';\nimport { services } from '../../services';\nimport { uiCombobox } from '../combobox';\nimport { svgIcon } from '../../svg/icon';\n\nimport { utilKeybinding } from '../../util/keybinding';\nimport { utilArrayUniq, utilDetect, utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent, utilUnicodeCharsCount } from '../../util';\nimport { uiLengthIndicator } from '../length_indicator';\nimport { deprecatedTagValuesByKey } from '../../osm/deprecated';\nimport { osmIsoCountryKeys } from '../../osm/tags';\n\nexport {\n uiFieldCombo as uiFieldManyCombo,\n uiFieldCombo as uiFieldMultiCombo,\n uiFieldCombo as uiFieldNetworkCombo,\n uiFieldCombo as uiFieldSemiCombo,\n uiFieldCombo as uiFieldTypeCombo\n};\n\nexport function uiFieldCombo(field, context) {\n var dispatch = d3_dispatch('change');\n var _isMulti = (field.type === 'multiCombo' || field.type === 'manyCombo');\n var _isNetwork = (field.type === 'networkCombo');\n var _isSemi = (field.type === 'semiCombo');\n var _showTagInfoSuggestions = field.type !== 'manyCombo' && field.autoSuggestions !== false;\n var _allowCustomValues = field.type !== 'manyCombo' && field.customValues !== false;\n var _snake_case = (field.snake_case || (field.snake_case === undefined));\n var _combobox = uiCombobox(context, 'combo-' + field.safeid)\n .caseSensitive(field.caseSensitive)\n .minItems(1);\n var _container = d3_select(null);\n var _inputWrap = d3_select(null);\n var _input = d3_select(null);\n var _lengthIndicator = uiLengthIndicator(context.maxCharsForTagValue());\n var _comboData = [];\n var _multiData = [];\n var _entityIDs = [];\n var _tags;\n var _countryCode;\n var _staticPlaceholder;\n var _customOptions = [];\n\n // initialize deprecated tags array\n var _dataDeprecated = [];\n fileFetcher.get('deprecated')\n .then(function(d) { _dataDeprecated = d; })\n .catch(function() { /* ignore */ });\n\n\n // ensure multiCombo field.key ends with a ':'\n if (_isMulti && field.key && /[^:]$/.test(field.key)) {\n field.key += ':';\n }\n\n\n function snake(s) {\n return s.replace(/\\s+/g, '_');\n }\n\n function clean(s) {\n return s.split(';')\n .map(function(s) { return s.trim(); })\n .join(';');\n }\n\n // windows does not support emoji flags\n const showEmojiFlags = utilDetect().os !== 'win';\n\n // adds emoji flags to country dropdown and input\n function addFlagIcon(selection, name, flag) {\n if (showEmojiFlags && flag) {\n const icon = selection.insert('span', ':first-child')\n .attr('class', 'tag-value-icon');\n\n icon.append('span')\n .attr('class', 'emoji')\n .text(flag);\n }\n selection.insert('span')\n .attr('class', 'tag-value')\n .text(name);\n }\n\n // cache for language and region maps\n let languageByCodes = null;\n let regionByCodes = null;\n\n // Helper to get regionByCodes\n const buildCountry = () => {\n if (regionByCodes) return regionByCodes;\n const localeCode = localizer.localeCode();\n\n const regionNames = new Intl.DisplayNames(localeCode, { type: 'region' });\n const features = countryCoder.borders.features;\n\n regionByCodes = {};\n\n for (const feature of features) {\n const code = feature.properties.iso1A2;\n let flag = feature.properties.emojiFlag;\n // if the flag is not present like for 'FX' code, we will look for the corresponding country flag for that code\n if (!flag && features?.properties?.country) {\n flag = countryCoder.feature(feature.properties.country).properties.emojiFlag;\n }\n if (!code) continue;\n\n try {\n const name = regionNames.of(code);\n if (!name) continue;\n regionByCodes[code] = {name, flag};\n } catch {\n continue;\n }\n }\n\n return regionByCodes;\n };\n\n // Helper to get languageByCodes\n const buildLanguages = () => {\n if (languageByCodes) return languageByCodes;\n\n languageByCodes = {};\n let codes = Object.keys(localizer.languages());\n\n for (const code of codes) {\n const name = localizer.languageName(code);\n if (!name || name === code) continue;\n languageByCodes[code] = name;\n }\n\n return languageByCodes;\n };\n\n // returns the tag value for a display value\n // (for multiCombo, dval should be the key suffix, not the entire key)\n function tagValue(dval) {\n dval = clean(dval || '');\n\n var found = getOptions(true).find(function(o) {\n return o.key && clean(o.value) === dval;\n });\n if (found) return found.key;\n\n if (field.type === 'typeCombo' && !dval) {\n return 'yes';\n }\n\n return restrictTagValueSpelling(dval) || undefined;\n }\n\n function restrictTagValueSpelling(dval) {\n if (_snake_case) {\n dval = snake(dval);\n }\n\n if (!field.caseSensitive) {\n if (!(field.key === 'type' && dval === 'associatedStreet')) {\n // don't lowercase \"type=associatedStreet\" tag\n // https://github.com/openstreetmap/iD/issues/9639\n dval = dval.toLowerCase();\n }\n }\n\n return dval;\n }\n\n\n function getLabelId(field, v) {\n return field.hasTextForStringId(`options.${v}.title`)\n ? `options.${v}.title`\n : `options.${v}`;\n }\n\n\n // returns the display value for a tag value\n // (for multiCombo, tval should be the key suffix, not the entire key)\n function displayValue(tval) {\n tval = tval || '';\n // Issue #11652\n // Ignore language: key and when tval is not others\n if (field.key === 'language:' && tval !== 'others'){\n let langName = buildLanguages()[tval];\n if (langName) return langName;\n }\n\n if (osmIsoCountryKeys.has(field.key) && tval) {\n const data = buildCountry()[tval];\n if (data) return data.name;\n return tval;\n }\n\n var stringsField = field.resolveReference('stringsCrossReference');\n const labelId = getLabelId(stringsField, tval);\n if (stringsField.hasTextForStringId(labelId)) {\n return stringsField.t(labelId, { default: tval });\n }\n\n if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {\n return '';\n }\n\n return tval;\n }\n\n\n // returns function which renders the display value for a tag value\n // (for multiCombo, tval should be the key suffix, not the entire key)\n function renderValue(tval) {\n tval = tval || '';\n\n // Issue #11652\n // Ignore language: key and when tval is not others\n if (field.key === 'language:' && tval !== 'others') {\n let langName = buildLanguages()[tval];\n if (langName) return selection => selection.text(langName);\n }\n\n if (osmIsoCountryKeys.has(field.key) && tval) {\n const data = buildCountry()[tval];\n if (data) return selection => addFlagIcon(selection, data.name, data.flag);\n return selection => selection.text(tval);\n }\n\n var stringsField = field.resolveReference('stringsCrossReference');\n const labelId = getLabelId(stringsField, tval);\n if (stringsField.hasTextForStringId(labelId)) {\n return stringsField.t.append(labelId, { default: tval });\n }\n\n if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {\n tval = '';\n }\n\n return selection => selection.text(tval);\n }\n\n\n // Compute the difference between arrays of objects by `value` property\n //\n // objectDifference([{value:1}, {value:2}, {value:3}], [{value:2}])\n // > [{value:1}, {value:3}]\n //\n function objectDifference(a, b) {\n return a.filter(function(d1) {\n return !b.some(function(d2) {\n return d1.value === d2.value;\n });\n });\n }\n\n\n function initCombo(selection, attachTo) {\n if (!_allowCustomValues) {\n selection.attr('readonly', 'readonly');\n }\n\n if (_showTagInfoSuggestions && services.taginfo) {\n selection.call(_combobox.fetcher(setTaginfoValues), attachTo);\n setTaginfoValues('', setPlaceholder);\n } else {\n selection.call(_combobox, attachTo);\n setTimeout(() => setStaticValues(setPlaceholder), 0);\n }\n }\n\n function getOptions(allOptions) {\n var stringsField = field.resolveReference('stringsCrossReference');\n const localeCode = localizer.localeCode();\n // Get dropdown list for language: key via localizer instead of taginfo\n if (field.key === 'language:') {\n const langMap = buildLanguages();\n\n let options = Object.entries(langMap).map(([code, name]) => {\n return {\n key: code,\n value: name,\n title: code, // the tooltip should show the raw-tag value\n display: selection => selection.text(name)\n };\n });\n\n const localeCode = localizer.localeCode();\n\n options.sort((a, b) => {\n return a.value.localeCompare(b.value, localeCode);\n });\n\n const v = 'others';\n const labelId = getLabelId(stringsField, v);\n\n // inserting others because it does not come via _dataLanguages\n options.push({\n key: v,\n value: stringsField.t(labelId, { default: v }),\n title: stringsField.t(`options.${v}.description`, { default: v }),\n description: stringsField.hasTextForStringId(`options.${v}.description`)\n ? stringsField.t(`options.${v}.description`) : undefined,\n display: addComboboxIcons(stringsField.t.append(labelId, { default: v }), v),\n klass: stringsField.hasTextForStringId(labelId) ? '' : 'raw-option'\n });\n\n\n return options;\n }\n\n // Get dropdown list for country key\n if (osmIsoCountryKeys.has(field.key)) {\n const countryMap = buildCountry();\n\n let options = Object.entries(countryMap).map(([c, {name, flag}]) => {\n return {\n key: c,\n value: name,\n title: c, // the tooltip should show the raw-tag value\n display: selection => addFlagIcon(selection, name, flag),\n sortname: name, // store just the name without emojis to sort the names\n klass: 'has-icon' // to specifically target the emoji css\n };\n });\n\n options.sort((a, b) => {\n return a.sortname.localeCompare(b.sortname, localeCode);\n });\n\n return options;\n }\n\n if (!(field.options || stringsField.options)) return [];\n\n let options;\n if (allOptions !== true) {\n options = field.options || stringsField.options;\n } else {\n options = [].concat(field.options, stringsField.options).filter(Boolean);\n }\n const result = options.map(function(v) {\n const labelId = getLabelId(stringsField, v);\n return {\n key: v,\n value: stringsField.t(labelId, { default: v }),\n title: stringsField.t(`options.${v}.description`, { default: v }),\n description: stringsField.hasTextForStringId(`options.${v}.description`)\n ? stringsField.t(`options.${v}.description`) : undefined,\n display: addComboboxIcons(stringsField.t.append(labelId, { default: v }), v),\n klass: stringsField.hasTextForStringId(labelId) ? '' : 'raw-option'\n };\n });\n return [...result, ..._customOptions];\n }\n\n\n function hasStaticValues() {\n return getOptions().length > 0;\n }\n\n\n function setStaticValues(callback, filter) {\n _comboData = getOptions();\n\n if (filter !== undefined) {\n _comboData = _comboData.filter(filter);\n }\n\n if (!field.allowDuplicates) {\n _comboData = objectDifference(_comboData, _multiData);\n }\n _combobox.data(_comboData);\n\n // hide the caret if there are no suggestions\n _container.classed('empty-combobox', _comboData.length === 0);\n\n if (callback) callback(_comboData);\n }\n\n\n function setTaginfoValues(q, callback) {\n var queryFilter = d => d.value.toLowerCase().includes(q.toLowerCase()) || d.key.toLowerCase().includes(q.toLowerCase());\n if (hasStaticValues()) {\n setStaticValues(callback, queryFilter);\n\n // If it is language field or a country field, we don't need to request for values, we get it from getOptions\n if (field.key === 'language:') return;\n if (osmIsoCountryKeys.has(field.key)) return;\n }\n\n var stringsField = field.resolveReference('stringsCrossReference');\n var fn = _isMulti ? 'multikeys' : 'values';\n var query = (_isMulti ? field.key : '') + q;\n var hasCountryPrefix = _isNetwork && _countryCode && _countryCode.indexOf(q.toLowerCase()) === 0;\n if (hasCountryPrefix) {\n query = _countryCode + ':';\n }\n\n var params = {\n debounce: (q !== ''),\n key: field.key,\n query: query\n };\n\n if (_entityIDs.length) {\n params.geometry = context.graph().geometry(_entityIDs[0]);\n }\n\n services.taginfo[fn](params, function(err, data) {\n if (err) return;\n\n // don't show the fallback value\n data = data.filter(d =>\n field.type !== 'typeCombo' || d.value !== 'yes');\n\n // don't show misspelled values\n data = data.filter(d => {\n var value = d.value;\n if (_isMulti) {\n value = value.slice(field.key.length);\n }\n return value === restrictTagValueSpelling(value);\n });\n\n var deprecatedValues = deprecatedTagValuesByKey(_dataDeprecated)[field.key];\n if (deprecatedValues) {\n // don't suggest deprecated tag values\n data = data.filter(d =>\n !deprecatedValues.includes(d.value));\n }\n\n if (hasCountryPrefix) {\n data = data.filter(d =>\n d.value.toLowerCase().indexOf(_countryCode + ':') === 0);\n }\n\n const additionalOptions = (field.options || stringsField.options || [])\n .filter(v => !data.some(dv => dv.value === (_isMulti ? field.key + v : v)))\n .map(v => ({ value: v }));\n\n // hide the caret if there are no suggestions\n _container.classed('empty-combobox', data.length === 0);\n\n _comboData = data.concat(additionalOptions).map(function(d) {\n var v = d.value;\n if (_isMulti) v = v.replace(field.key, '');\n const labelId = getLabelId(stringsField, v);\n var isLocalizable = stringsField.hasTextForStringId(labelId);\n var label = stringsField.t(labelId, { default: v });\n return {\n key: v,\n value: label,\n title: stringsField.t(`options.${v}.description`, { default:\n isLocalizable ? label : (d.title !== label ? d.title : '') }),\n description: stringsField.hasTextForStringId(`options.${v}.description`)\n ? stringsField.t(`options.${v}.description`) : undefined,\n display: addComboboxIcons(stringsField.t.append(labelId, { default: label }), v),\n klass: isLocalizable ? '' : 'raw-option'\n };\n });\n\n _comboData = _comboData.filter(queryFilter);\n\n if (!field.allowDuplicates) {\n _comboData = objectDifference(_comboData, _multiData);\n }\n if (callback) callback(_comboData, hasStaticValues());\n });\n }\n\n // adds icons to tag values which have one\n function addComboboxIcons(disp, value) {\n const iconsField = field.resolveReference('iconsCrossReference');\n if (iconsField.icons) {\n return function(selection) {\n var span = selection\n .insert('span', ':first-child')\n .attr('class', 'tag-value-icon');\n if (iconsField.icons[value]) {\n span.call(svgIcon(`#${iconsField.icons[value]}`));\n }\n disp.call(this, selection);\n };\n }\n return disp;\n }\n\n\n function setPlaceholder(values) {\n\n if (_isMulti || _isSemi) {\n _staticPlaceholder = field.placeholder() || t('inspector.add');\n } else {\n var vals = values\n .map(function(d) { return d.value; })\n .filter(function(s) { return s.length < 20; });\n\n var placeholders = vals.length > 1 ? vals : values.map(function(d) { return d.key; });\n _staticPlaceholder = field.placeholder() || placeholders.slice(0, 3).join(', ');\n }\n\n if (!/(\u2026|\\.\\.\\.)$/.test(_staticPlaceholder)) {\n _staticPlaceholder += '\u2026';\n }\n\n var ph;\n if (!_isMulti && !_isSemi && _tags && Array.isArray(_tags[field.key])) {\n ph = t('inspector.multiple_values');\n } else {\n ph = _staticPlaceholder;\n }\n\n _container.selectAll('input')\n .attr('placeholder', ph);\n\n // Hide 'Add' button if this field uses fixed set of\n // options and they're all currently used\n var hideAdd = (!_allowCustomValues && !values.length);\n _container.selectAll('.chiplist .input-wrap')\n .style('display', hideAdd ? 'none' : null);\n }\n\n\n function change() {\n var t = {};\n var val;\n\n if (_isMulti || _isSemi) {\n var vals;\n if (_isMulti) {\n vals = [tagValue(utilGetSetValue(_input))];\n } else if (_isSemi) {\n val = tagValue(utilGetSetValue(_input)) || '';\n val = val.replace(/,/g, ';');\n vals = val.split(';');\n }\n vals = vals.filter(Boolean);\n\n if (!vals.length) return;\n\n _container.classed('active', false);\n utilGetSetValue(_input, '');\n\n if (_isMulti) {\n utilArrayUniq(vals).forEach(function(v) {\n var key = (field.key || '') + v;\n if (_tags) {\n // don't set a multicombo value to 'yes' if it already has a non-'no' value\n // e.g. `language:de=main`\n var old = _tags[key];\n if (typeof old === 'string' && old.toLowerCase() !== 'no') return;\n }\n key = context.cleanTagKey(key);\n field.keys.push(key);\n t[key] = 'yes';\n });\n\n } else if (_isSemi) {\n var arr = _multiData.map(function(d) { return d.key; });\n arr = arr.concat(vals);\n if (!field.allowDuplicates) {\n arr = utilArrayUniq(arr);\n }\n t[field.key] = context.cleanTagValue(arr.filter(Boolean).join(';'));\n }\n\n window.setTimeout(function() { _input.node().focus(); }, 10);\n\n } else {\n var rawValue = utilGetSetValue(_input);\n\n // don't override multiple values with blank string\n if (!rawValue && Array.isArray(_tags[field.key])) return;\n\n val = context.cleanTagValue(tagValue(rawValue));\n t[field.key] = val || undefined;\n }\n\n dispatch.call('change', this, t);\n }\n\n\n function removeMultikey(d3_event, d) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n var t = {};\n if (_isMulti) {\n t[d.key] = undefined;\n } else if (_isSemi) {\n let arr = _multiData.map(item => item.key);\n\n // delete the value using the index, since a value\n // may exist multiple times in the array.\n arr.splice(d.index, 1);\n\n if (!field.allowDuplicates) {\n arr = utilArrayUniq(arr);\n }\n t[field.key] = arr.length ? arr.join(';') : undefined;\n\n _lengthIndicator.update(t[field.key]);\n }\n dispatch.call('change', this, t);\n }\n\n\n function invertMultikey(d3_event, d) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n var t = {};\n if (_isMulti) {\n t[d.key] = _tags[d.key] === 'yes' ? 'no' : 'yes';\n }\n dispatch.call('change', this, t);\n }\n\n\n function combo(selection) {\n _container = selection.selectAll('.form-field-input-wrap')\n .data([0]);\n\n var type = (_isMulti || _isSemi) ? 'multicombo': 'combo';\n _container = _container.enter()\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-' + type)\n .merge(_container);\n\n if (_isMulti || _isSemi) {\n _container = _container.selectAll('.chiplist')\n .data([0]);\n\n var listClass = 'chiplist';\n\n // Use a separate line for each value in the Destinations and Via fields\n // to mimic highway exit signs\n if (field.key === 'destination' || field.key === 'via') {\n listClass += ' full-line-chips';\n }\n\n _container = _container.enter()\n .append('ul')\n .attr('class', listClass)\n .on('click', function() {\n window.setTimeout(function() { _input.node().focus(); }, 10);\n })\n .merge(_container);\n\n\n _inputWrap = _container.selectAll('.input-wrap')\n .data([0]);\n\n _inputWrap = _inputWrap.enter()\n .append('li')\n .attr('class', 'input-wrap')\n .merge(_inputWrap);\n\n // Hide 'Add' button if this field uses fixed set of\n // options and they're all currently used\n var hideAdd = (!_allowCustomValues && !_comboData.length);\n _inputWrap.style('display', hideAdd ? 'none' : null);\n\n _input = _inputWrap.selectAll('input')\n .data([0]);\n } else {\n _input = _container.selectAll('input')\n .data([0]);\n }\n\n _input = _input.enter()\n .append('input')\n .attr('type', 'text')\n .attr('dir', 'auto')\n .attr('id', field.domId)\n .call(utilNoAuto)\n .call(initCombo, _container)\n .merge(_input);\n\n if (_isSemi) {\n _inputWrap.call(_lengthIndicator);\n } else if (!_isMulti) {\n _container.call(_lengthIndicator);\n }\n\n if (_isNetwork) {\n var extent = combinedEntityExtent();\n var countryCode = extent && countryCoder.iso1A2Code(extent.center());\n _countryCode = countryCode && countryCode.toLowerCase();\n }\n\n _input\n .on('change', change)\n .on('blur', change)\n .on('input', function() {\n let val = utilGetSetValue(_input);\n updateIcon(val);\n if (_isSemi && _tags[field.key]) {\n // when adding a new value to existing ones\n val += ';' + _tags[field.key];\n }\n _lengthIndicator.update(val);\n });\n\n _input\n .on('keydown.field', function(d3_event) {\n switch (d3_event.keyCode) {\n case 13: // \u21A9 Return\n _input.node().blur(); // blurring also enters the value\n d3_event.stopPropagation();\n break;\n }\n });\n\n if (_isMulti || _isSemi) {\n _combobox\n .on('accept', function() {\n _input.node().blur();\n _input.node().focus();\n });\n\n _input\n .on('focus', function() { _container.classed('active', true); });\n }\n\n _combobox\n .on('cancel', function() {\n _input.node().blur();\n })\n .on('update', function() {\n updateIcon(utilGetSetValue(_input));\n });\n }\n\n function updateIcon(value) {\n value = tagValue(value);\n let container = _container;\n if (field.type === 'multiCombo' || field.type === 'semiCombo') {\n container = _container.select('.input-wrap');\n }\n\n // For the country emoji flags\n container.selectAll('.tag-value-icon').remove();\n if (osmIsoCountryKeys.has(field.key) && value) {\n const data = buildCountry()[value];\n\n if (data && data.flag && showEmojiFlags) {\n container.selectAll('.tag-value-icon')\n .data([value])\n .enter()\n .insert('div', 'input')\n .attr('class', 'tag-value-icon')\n .append('span')\n .attr('class', 'emoji')\n .text(data.flag);\n return;\n }\n };\n\n const iconsField = field.resolveReference('iconsCrossReference');\n if (iconsField.icons) {\n container.selectAll('.tag-value-icon').remove();\n if (iconsField.icons[value]) {\n container.selectAll('.tag-value-icon')\n .data([value])\n .enter()\n .insert('div', 'input')\n .attr('class', 'tag-value-icon')\n .call(svgIcon(`#${iconsField.icons[value]}`));\n }\n }\n }\n\n combo.tags = function(tags) {\n _tags = tags;\n var stringsField = field.resolveReference('stringsCrossReference');\n\n var isMixed = Array.isArray(tags[field.key]);\n var showsValue = value => !isMixed && value && !(field.type === 'typeCombo' && value === 'yes');\n var isRawValue = value => showsValue(value)\n && !stringsField.hasTextForStringId(`options.${value}`)\n && !stringsField.hasTextForStringId(`options.${value}.title`)\n && !(osmIsoCountryKeys.has(field.key) && value in buildCountry());\n var isKnownValue = value => showsValue(value) && !isRawValue(value);\n var isReadOnly = !_allowCustomValues;\n\n if (_isMulti || _isSemi) {\n _multiData = [];\n\n var maxLength;\n\n if (_isMulti) {\n // Build _multiData array containing keys already set..\n for (var k in tags) {\n if (field.key && k.indexOf(field.key) !== 0) continue;\n if (!field.key && field.keys.indexOf(k) === -1) continue;\n\n var v = tags[k];\n\n var suffix = field.key ? k.slice(field.key.length) : k;\n _multiData.push({\n key: k,\n value: displayValue(suffix),\n display: addComboboxIcons(renderValue(suffix), suffix),\n state: typeof v === 'string' ? v.toLowerCase() : '',\n isMixed: Array.isArray(v)\n });\n }\n\n if (field.key) {\n // Set keys for form-field modified (needed for undo and reset buttons)..\n field.keys = _multiData.map(function(d) { return d.key; });\n\n // limit the input length so it fits after prepending the key prefix\n maxLength = context.maxCharsForTagKey() - utilUnicodeCharsCount(field.key);\n } else {\n maxLength = context.maxCharsForTagKey();\n }\n\n } else if (_isSemi) {\n\n var allValues = [];\n var commonValues;\n if (Array.isArray(tags[field.key])) {\n\n tags[field.key].forEach(function(tagVal) {\n var thisVals = (tagVal || '').split(';').filter(Boolean);\n allValues = allValues.concat(thisVals);\n if (!commonValues) {\n commonValues = thisVals;\n } else {\n commonValues = commonValues.filter(value => thisVals.includes(value));\n }\n });\n allValues = allValues.filter(Boolean);\n\n } else {\n allValues = (tags[field.key] || '').split(';').filter(Boolean);\n commonValues = allValues;\n }\n\n if (!field.allowDuplicates) {\n commonValues = utilArrayUniq(commonValues);\n allValues = utilArrayUniq(allValues);\n }\n\n _multiData = allValues.map(function(v) {\n return {\n key: v,\n value: displayValue(v),\n display: addComboboxIcons(renderValue(v), v),\n isMixed: !commonValues.includes(v)\n };\n });\n\n var currLength = utilUnicodeCharsCount(commonValues.join(';'));\n\n // limit the input length to the remaining available characters\n maxLength = context.maxCharsForTagValue() - currLength;\n\n if (currLength > 0) {\n // account for the separator if a new value will be appended to existing\n maxLength -= 1;\n }\n }\n // a negative maxlength doesn't make sense\n maxLength = Math.max(0, maxLength);\n\n // Hide 'Add' button if this field is already at its character limit\n var hideAdd = maxLength <= 0 || (!_allowCustomValues && !_comboData.length);\n _container.selectAll('.chiplist .input-wrap')\n .style('display', hideAdd ? 'none' : null);\n\n var allowDragAndDrop = _isSemi // only semiCombo values are ordered\n && !Array.isArray(tags[field.key]);\n\n // Render chips\n var chips = _container.selectAll('.chip')\n .data(_multiData.map((item, index) => ({ ...item, index })));\n\n chips.exit()\n .remove();\n\n var enter = chips.enter()\n .insert('li', '.input-wrap')\n .attr('class', 'chip');\n\n enter.append('span');\n const field_buttons = enter\n .append('div')\n .attr('class', 'field_buttons');\n field_buttons\n .append('a')\n .attr('class', 'remove');\n\n chips = chips.merge(enter)\n .order()\n .classed('raw-value', function(d) {\n var k = d.key;\n if (_isMulti) k = k.replace(field.key, '');\n // Ignore the raw-value class for key language:\n if (field.key === 'language:' && localizer.languageName(k) !== k) return false;\n return !stringsField.hasTextForStringId('options.' + k);\n })\n .classed('draggable', allowDragAndDrop)\n .classed('mixed', function(d) {\n return d.isMixed;\n })\n .attr('title', function(d) {\n if (d.isMixed) {\n return t('inspector.unshared_value_tooltip');\n }\n if (!['yes', 'no'].includes(d.state)) {\n return d.state;\n }\n return null;\n })\n .classed('negated', d => d.state === 'no');\n\n if (!_isSemi) {\n chips.selectAll('input[type=checkbox]').remove();\n chips.insert('input', 'span')\n .attr('type', 'checkbox')\n .property('checked', d => d.state === 'yes')\n .property('indeterminate', d => d.isMixed || !['yes', 'no'].includes(d.state))\n .on('click', invertMultikey);\n }\n\n if (allowDragAndDrop) {\n registerDragAndDrop(chips);\n }\n\n chips.each(function(d) {\n const selection = d3_select(this);\n const text_span = selection.select('span');\n const field_buttons = selection.select('.field_buttons');\n const clean_value = d.value.trim();\n text_span.text('');\n if (!field_buttons.select('button').empty()) {\n field_buttons.select('button').remove();\n }\n if (clean_value.startsWith('https://')) {\n // create a button to open the link in a new tab\n text_span.text(clean_value);\n field_buttons.append('button')\n .call(svgIcon('#iD-icon-out-link'))\n .attr('class', 'form-field-button foreign-id-permalink')\n .attr('title', () => t('icons.visit_website'))\n .attr('aria-label', () => t('icons.visit_website'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n window.open(clean_value, '_blank');\n });\n return;\n }\n if (d.display) {\n d.display(text_span);\n return;\n }\n text_span.text(d.value);\n });\n\n chips.select('a.remove')\n .attr('href', '#')\n .on('click', removeMultikey)\n .attr('class', 'remove')\n .text('\u00D7');\n\n updateIcon('');\n } else {\n var mixedValues = isMixed && tags[field.key].map(function(val) {\n return displayValue(val);\n }).filter(Boolean);\n\n utilGetSetValue(_input, !isMixed ? displayValue(tags[field.key]) : '')\n .data([tags[field.key]])\n .classed('raw-value', isRawValue)\n .classed('known-value', isKnownValue)\n .attr('readonly', isReadOnly ? 'readonly' : undefined)\n .attr('title', isMixed ? mixedValues.join('\\n') : undefined)\n .attr('placeholder', isMixed ? t('inspector.multiple_values') : _staticPlaceholder || '')\n .classed('mixed', isMixed)\n .on('keydown.deleteCapture', function(d3_event) {\n if (isReadOnly &&\n isKnownValue(tags[field.key]) &&\n (d3_event.keyCode === utilKeybinding.keyCodes['\u232B'] ||\n d3_event.keyCode === utilKeybinding.keyCodes['\u2326'])) {\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n var t = {};\n t[field.key] = undefined;\n dispatch.call('change', this, t);\n }\n });\n\n if (!Array.isArray(tags[field.key])) {\n updateIcon(tags[field.key]);\n }\n\n if (!isMixed) {\n _lengthIndicator.update(tags[field.key]);\n }\n }\n\n const refreshStyles = () => {\n _input\n .data([tagValue(utilGetSetValue(_input))])\n .classed('raw-value', isRawValue)\n .classed('known-value', isKnownValue);\n };\n _input.on('input.refreshStyles', refreshStyles);\n _combobox.on('update.refreshStyles', refreshStyles);\n refreshStyles();\n };\n\n function registerDragAndDrop(selection) {\n\n // allow drag and drop re-ordering of chips\n var dragOrigin, targetIndex;\n selection.call(d3_drag()\n .on('start', function(d3_event) {\n dragOrigin = {\n x: d3_event.x,\n y: d3_event.y\n };\n targetIndex = null;\n })\n .on('drag', function(d3_event) {\n var x = d3_event.x - dragOrigin.x,\n y = d3_event.y - dragOrigin.y;\n\n if (!d3_select(this).classed('dragging') &&\n // don't display drag until dragging beyond a distance threshold\n Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) <= 5) return;\n\n var index = selection.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', true);\n\n targetIndex = null;\n var targetIndexOffsetTop = null;\n var draggedTagWidth = d3_select(this).node().offsetWidth;\n\n if (field.key === 'destination' || field.key === 'via') { // meaning tags are full width\n _container.selectAll('.chip')\n .style('transform', function(d2, index2) {\n var node = d3_select(this).node();\n\n if (index === index2) {\n return 'translate(' + x + 'px, ' + y + 'px)';\n // move the dragged tag up the order\n } else if (index2 > index && d3_event.y > node.offsetTop) {\n if (targetIndex === null || index2 > targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(-100%)';\n // move the dragged tag down the order\n } else if (index2 < index && d3_event.y < node.offsetTop + node.offsetHeight) {\n if (targetIndex === null || index2 < targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(100%)';\n }\n return null;\n });\n } else {\n _container.selectAll('.chip')\n .each(function(d2, index2) {\n var node = d3_select(this).node();\n\n // check the cursor is in the bounding box\n if (\n index !== index2 &&\n d3_event.x < node.offsetLeft + node.offsetWidth + 5 &&\n d3_event.x > node.offsetLeft &&\n d3_event.y < node.offsetTop + node.offsetHeight &&\n d3_event.y > node.offsetTop\n ) {\n targetIndex = index2;\n targetIndexOffsetTop = node.offsetTop;\n }\n })\n .style('transform', function(d2, index2) {\n var node = d3_select(this).node();\n\n if (index === index2) {\n return 'translate(' + x + 'px, ' + y + 'px)';\n }\n\n // only translate tags in the same row\n if (node.offsetTop === targetIndexOffsetTop) {\n if (index2 < index && index2 >= targetIndex) {\n return 'translateX(' + draggedTagWidth + 'px)';\n } else if (index2 > index && index2 <= targetIndex) {\n return 'translateX(-' + draggedTagWidth + 'px)';\n }\n }\n return null;\n });\n }\n })\n .on('end', function() {\n if (!d3_select(this).classed('dragging')) {\n return;\n }\n var index = selection.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', false);\n\n _container.selectAll('.chip')\n .style('transform', null);\n\n if (typeof targetIndex === 'number') {\n var element = _multiData[index];\n _multiData.splice(index, 1);\n _multiData.splice(targetIndex, 0, element);\n\n var t = {};\n\n if (_multiData.length) {\n t[field.key] = _multiData.map(function(element) {\n return element.key;\n }).join(';');\n } else {\n t[field.key] = undefined;\n }\n\n dispatch.call('change', this, t);\n }\n dragOrigin = undefined;\n targetIndex = undefined;\n })\n );\n }\n\n combo.setCustomOptions = (newValue) => {\n _customOptions = newValue;\n };\n\n\n combo.focus = function() {\n _input.node().focus();\n };\n\n\n combo.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return combo;\n };\n\n\n function combinedEntityExtent() {\n return _entityIDs && _entityIDs.length && utilTotalExtent(_entityIDs, context.graph());\n }\n\n\n return utilRebind(combo, dispatch, 'on');\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport { geoSphericalDistance } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect, modeSelectNote } from '../modes';\nimport { utilObjectOmit, utilQsString, utilStringQs } from '../util';\nimport { utilArrayIdentical } from '../util/array';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { localizer, t } from '../core/localizer';\nimport { prefs } from '../core/preferences';\n\n\nexport function behaviorHash(context) {\n\n // cached window.location.hash\n var _cachedHash = null;\n // allowable latitude range\n var _latitudeLimit = 90 - 1e-8;\n\n function computedHashParameters() {\n var map = context.map();\n var center = map.center();\n var zoom = map.zoom();\n var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));\n var oldParams = utilObjectOmit(utilStringQs(window.location.hash),\n ['comment', 'source', 'hashtags', 'walkthrough']\n );\n var newParams = {};\n\n delete oldParams.id;\n var selected = context.selectedIDs().filter(function(id) {\n return context.hasEntity(id);\n });\n if (selected.length) {\n newParams.id = selected.join(',');\n } else if (context.selectedNoteID()) {\n newParams.id = `note/${context.selectedNoteID()}`;\n }\n\n newParams.map = zoom.toFixed(2) +\n '/' + center[1].toFixed(precision) +\n '/' + center[0].toFixed(precision);\n\n return Object.assign(oldParams, newParams);\n }\n\n function computedHash() {\n return '#' + utilQsString(computedHashParameters(), true);\n }\n\n function computedTitle(includeChangeCount) {\n\n var baseTitle = context.documentTitleBase() || 'iD';\n var contextual;\n var changeCount;\n var titleID;\n\n var selected = context.selectedIDs().filter(function(id) {\n return context.hasEntity(id);\n });\n if (selected.length) {\n var firstLabel = utilDisplayLabel(context.entity(selected[0]), context.graph());\n if (selected.length > 1) {\n contextual = t('title.labeled_and_more', {\n labeled: firstLabel,\n count: selected.length - 1\n });\n } else {\n contextual = firstLabel;\n }\n titleID = 'context';\n }\n\n if (includeChangeCount) {\n changeCount = context.history().difference().summary().length;\n if (changeCount > 0) {\n titleID = contextual ? 'changes_context' : 'changes';\n }\n }\n\n if (titleID) {\n return t('title.format.' + titleID, {\n changes: changeCount,\n base: baseTitle,\n context: contextual\n });\n }\n\n return baseTitle;\n }\n\n function updateTitle(includeChangeCount) {\n if (!context.setsDocumentTitle()) return;\n\n var newTitle = computedTitle(includeChangeCount);\n if (document.title !== newTitle) {\n document.title = newTitle;\n }\n }\n\n function updateHashIfNeeded() {\n if (context.inIntro()) return;\n\n var latestHash = computedHash();\n if (_cachedHash !== latestHash) {\n _cachedHash = latestHash;\n\n // Update the URL hash without affecting the browser navigation stack,\n // though unavoidably creating a browser history entry\n window.history.replaceState(null, '', latestHash);\n\n // set the title we want displayed for the browser tab/window\n updateTitle(true /* includeChangeCount */);\n\n // save last used map location for future\n const q = utilStringQs(latestHash);\n if (q.map) {\n prefs('map-location', q.map);\n }\n }\n }\n\n var _throttledUpdate = throttle(updateHashIfNeeded, 500);\n var _throttledUpdateTitle = throttle(function() {\n updateTitle(true /* includeChangeCount */);\n }, 500);\n\n function hashchange() {\n // ignore spurious hashchange events\n if (window.location.hash === _cachedHash) return;\n\n _cachedHash = window.location.hash;\n\n var q = utilStringQs(_cachedHash);\n\n if (q.theme) {\n context.theme(q.theme);\n }\n\n if (q.locale && q.locale !== localizer.preferredLocaleCodes().join(',')) {\n localizer.preferredLocaleCodes(q.locale);\n context.ui().restart();\n }\n\n var mapArgs = (q.map || '').split('/').map(Number);\n if (mapArgs.length < 3 || mapArgs.some(isNaN)) {\n // replace bogus hash\n updateHashIfNeeded();\n\n } else {\n // don't update if the new hash already reflects the state of iD\n if (_cachedHash === computedHash()) return;\n\n var mode = context.mode();\n\n context.map().centerZoom([mapArgs[2], Math.min(_latitudeLimit, Math.max(-_latitudeLimit, mapArgs[1]))], mapArgs[0]);\n\n if (q.id && mode) {\n var ids = q.id.split(',').filter(function(id) {\n return context.hasEntity(id) || id.startsWith('note/');\n });\n if (ids.length && ['browse', 'select-note', 'select'].includes(mode.id)) {\n if (ids.length === 1 && ids[0].startsWith('note/')) {\n context.enter(modeSelectNote(context, ids[0]));\n } else if (!utilArrayIdentical(mode.selectedIDs(), ids)) {\n context.enter(modeSelect(context, ids));\n }\n return;\n }\n }\n\n var center = context.map().center();\n var dist = geoSphericalDistance(center, [mapArgs[2], mapArgs[1]]);\n var maxdist = 500;\n\n // Don't allow the hash location to change too much while drawing\n // This can happen if the user accidentally hit the back button. #3996\n if (mode && mode.id.match(/^draw/) !== null && dist > maxdist) {\n context.enter(modeBrowse(context));\n return;\n }\n }\n }\n\n function behavior() {\n context.map()\n .on('move.behaviorHash', _throttledUpdate);\n\n context.history()\n .on('change.behaviorHash', _throttledUpdateTitle);\n\n context\n .on('enter.behaviorHash', _throttledUpdate);\n\n d3_select(window)\n .on('hashchange.behaviorHash', hashchange);\n\n var q = utilStringQs(window.location.hash);\n\n if (q.id) {\n // targeting specific features: download, select, and zoom to them\n const selectIds = q.id.split(',');\n if (selectIds.length === 1 && selectIds[0].startsWith('note/')) {\n const noteId = selectIds[0].split('/')[1];\n context.moveToNote(noteId, !q.map);\n } else {\n context.zoomToEntities(\n // convert ids to short form id: node/123 -> n123\n selectIds.map(id => id.replace(/([nwr])[^/]*\\//, '$1')),\n !q.map);\n }\n }\n\n if (q.walkthrough === 'true') {\n behavior.startWalkthrough = true;\n }\n\n if (q.map) {\n behavior.hadLocation = true;\n } else if (!q.id && prefs('map-location')) {\n // center map at last visited map location\n const mapArgs = prefs('map-location').split('/').map(Number);\n context.map().centerZoom([mapArgs[2], Math.min(_latitudeLimit, Math.max(-_latitudeLimit, mapArgs[1]))], mapArgs[0]);\n\n updateHashIfNeeded();\n\n behavior.hadLocation = true;\n }\n\n hashchange();\n\n updateTitle(false);\n }\n\n behavior.off = function() {\n _throttledUpdate.cancel();\n _throttledUpdateTitle.cancel();\n\n context.map()\n .on('move.behaviorHash', null);\n\n context\n .on('enter.behaviorHash', null);\n\n d3_select(window)\n .on('hashchange.behaviorHash', null);\n\n window.location.hash = '';\n };\n\n return behavior;\n}\n", "export { behaviorAddWay } from './add_way';\nexport { behaviorBreathe } from './breathe';\nexport { behaviorDrag } from './drag';\nexport { behaviorDrawWay } from './draw_way';\nexport { behaviorDraw } from './draw';\nexport { behaviorEdit } from './edit';\nexport { behaviorHash } from './hash';\nexport { behaviorHover } from './hover';\nexport { behaviorLasso } from './lasso';\nexport { behaviorOperation } from './operation';\nexport { behaviorPaste } from './paste';\nexport { behaviorSelect } from './select';\n", "import { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiAccount(context) {\n const osm = context.connection();\n\n\n function updateUserDetails(selection) {\n if (!osm) return;\n\n if (!osm.authenticated()) { // logged out\n render(selection, null);\n } else {\n osm.userDetails((err, user) => {\n if (err && err.status === 401) {\n // 401 Unauthorized\n // cannot load own user data: there must be something wrong (e.g. API token was revoked)\n // -> log out to allow user to reauthenticate\n osm.logout();\n }\n render(selection, user);\n });\n }\n }\n\n\n function render(selection, user) {\n let userInfo = selection.select('.userInfo');\n let loginLogout = selection.select('.loginLogout');\n\n if (user) {\n userInfo\n .html('')\n .classed('hide', false);\n\n let userLink = userInfo\n .append('a')\n .attr('href', osm.userURL(user.display_name))\n .attr('target', '_blank');\n\n // Add user's image or placeholder\n if (user.image_url) {\n userLink.append('img')\n .attr('class', 'icon pre-text user-icon')\n .attr('src', user.image_url);\n } else {\n userLink\n .call(svgIcon('#iD-icon-avatar', 'pre-text light'));\n }\n\n // Add user name\n userLink.append('span')\n .attr('class', 'label')\n .text(user.display_name);\n\n // show \"Log Out\"\n loginLogout\n .classed('hide', false)\n .select('a')\n .text(t('logout'))\n .on('click', e => {\n e.preventDefault();\n osm.logout();\n // OAuth2's idea of \"logout\" is just to get rid of the bearer token.\n // If we try to \"login\" again, it will just grab the token again.\n // What a user probably _really_ expects is to logout of OSM so that they can switch users.\n // So, we open a popup with a \"Logout\" button. After logging out, they can login again using\n // the same popup window.\n osm.authenticate(undefined, { switchUser: true });\n });\n\n } else { // no user\n userInfo\n .html('')\n .classed('hide', true);\n\n // show \"Log In\"\n loginLogout\n .classed('hide', false)\n .select('a')\n .text(t('login'))\n .on('click', e => {\n e.preventDefault();\n osm.authenticate();\n });\n }\n }\n\n\n return function(selection) {\n if (!osm) return;\n\n selection.append('li')\n .attr('class', 'userInfo')\n .classed('hide', true);\n\n selection.append('li')\n .attr('class', 'loginLogout')\n .classed('hide', true)\n .append('a')\n .attr('href', '#');\n\n osm.on('change.account', () => updateUserDetails(selection));\n updateUserDetails(selection);\n };\n\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { t } from '../core/localizer';\n\n\nexport function uiAttribution(context) {\n let _selection = d3_select(null);\n\n\n function render(selection, data, klass) {\n let div = selection.selectAll(`.${klass}`)\n .data([0]);\n\n div = div.enter()\n .append('div')\n .attr('class', klass)\n .merge(div);\n\n\n let attributions = div.selectAll('.attribution')\n .data(data, d => d.id);\n\n attributions.exit()\n .remove();\n\n attributions = attributions.enter()\n .append('span')\n .attr('class', 'attribution')\n .each((d, i, nodes) => {\n let attribution = d3_select(nodes[i]);\n\n if (d.terms_html) {\n attribution.html(d.terms_html);\n return;\n }\n\n if (d.terms_url) {\n attribution = attribution\n .append('a')\n .attr('href', d.terms_url)\n .attr('target', '_blank');\n }\n\n const sourceID = d.id.replace(/\\./g, '');\n const terms_text = t(`imagery.${sourceID}.attribution.text`,\n { default: d.terms_text || d.id || d.name() }\n );\n\n if (d.icon && !d.overlay) {\n attribution\n .append('img')\n .attr('class', 'source-image')\n .attr('src', d.icon);\n }\n\n attribution\n .append('span')\n .attr('class', 'attribution-text')\n .text(terms_text);\n })\n .merge(attributions);\n\n\n let copyright = attributions.selectAll('.copyright-notice')\n .data(d => {\n let notice = d.copyrightNotices(context.map().zoom(), context.map().extent());\n return notice ? [notice] : [];\n });\n\n copyright.exit()\n .remove();\n\n copyright = copyright.enter()\n .append('span')\n .attr('class', 'copyright-notice')\n .merge(copyright);\n\n copyright\n .text(String);\n }\n\n\n function update() {\n let baselayer = context.background().baseLayerSource();\n _selection\n .call(render, (baselayer ? [baselayer] : []), 'base-layer-attribution');\n\n const z = context.map().zoom();\n let overlays = context.background().overlayLayerSources() || [];\n _selection\n .call(render, overlays.filter(s => s.validZoom(z)), 'overlay-layer-attribution');\n }\n\n\n return function(selection) {\n _selection = selection;\n\n context.background()\n .on('change.attribution', update);\n\n context.map()\n .on('move.attribution', throttle(update, 400, { leading: false }));\n\n update();\n };\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/index';\n\n\nexport function uiContributors(context) {\n var osm = context.connection(),\n debouncedUpdate = debounce(function() { update(); }, 1000),\n limit = 4,\n hidden = false,\n wrap = d3_select(null);\n\n\n function update() {\n if (!osm) return;\n\n var users = {},\n entities = context.history().intersects(context.map().extent());\n\n entities.forEach(function(entity) {\n if (entity && entity.user) users[entity.user] = true;\n });\n\n var u = Object.keys(users),\n subset = u.slice(0, u.length > limit ? limit - 1 : limit);\n\n wrap.html('')\n .call(svgIcon('#iD-icon-nearby', 'pre-text light'));\n\n const userList = selection => selection.selectAll()\n .data(subset)\n .enter()\n .append('a')\n .attr('class', 'user-link')\n .attr('href', d => osm.userURL(d))\n .attr('target', '_blank')\n .text(String);\n\n if (u.length > limit) {\n var othersNum = u.length - limit + 1;\n\n const count = selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', () => osm.changesetsURL(context.map().center(), context.map().zoom()))\n .text(othersNum);\n\n wrap.append('span')\n .call(t.append('contributors.truncated_list', { n: othersNum, users: userList, count }));\n\n } else {\n wrap.append('span')\n .call(t.append('contributors.list', { users: userList }));\n }\n\n if (!u.length) {\n hidden = true;\n wrap\n .transition()\n .style('opacity', 0);\n\n } else if (hidden) {\n wrap\n .transition()\n .style('opacity', 1);\n }\n }\n\n\n return function(selection) {\n if (!osm) return;\n wrap = selection;\n update();\n\n osm.on('loaded.contributors', debouncedUpdate);\n context.map().on('move.contributors', debouncedUpdate);\n };\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { geoVecAdd } from '../geo';\nimport { localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\nimport { utilRebind } from '../util/rebind';\nimport { utilHighlightEntities } from '../util/util';\nimport { utilGetDimensions } from '../util/dimensions';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiEditMenu(context) {\n var dispatch = d3_dispatch('toggled');\n\n var _menu = d3_select(null);\n var _operations = [];\n // the position the menu should be displayed relative to\n var _anchorLoc = [0, 0];\n var _anchorLocLonLat = [0, 0];\n // a string indicating how the menu was opened\n var _triggerType = '';\n\n var _vpTopMargin = 85; // viewport top margin\n var _vpBottomMargin = 45; // viewport bottom margin\n var _vpSideMargin = 35; // viewport side margin\n\n var _menuTop = false;\n var _menuHeight;\n var _menuWidth;\n\n // hardcode these values to make menu positioning easier\n var _verticalPadding = 4;\n\n // see also `.edit-menu .tooltip` CSS; include margin\n var _tooltipWidth = 210;\n\n // offset the menu slightly from the target location\n var _menuSideMargin = 10;\n\n var _tooltips = [];\n\n var editMenu = function(selection) {\n\n var isTouchMenu = _triggerType.includes('touch') || _triggerType.includes('pen');\n\n var ops = _operations.filter(function(op) {\n return !isTouchMenu || !op.mouseOnly;\n });\n\n if (!ops.length) return;\n\n _tooltips = [];\n\n // Position the menu above the anchor for stylus and finger input\n // since the mapper's hand likely obscures the screen below the anchor\n _menuTop = isTouchMenu;\n\n // Show labels for touch input since there aren't hover tooltips\n var showLabels = isTouchMenu;\n\n var buttonHeight = showLabels ? 32 : 34;\n if (showLabels) {\n // Get a general idea of the width based on the length of the label\n _menuWidth = 52 + Math.min(120, 6 * Math.max.apply(Math, ops.map(function(op) {\n return op.title.length;\n })));\n } else {\n _menuWidth = 44;\n }\n\n _menuHeight = _verticalPadding * 2 + ops.length * buttonHeight;\n\n _menu = selection\n .append('div')\n .attr('class', 'edit-menu')\n .classed('touch-menu', isTouchMenu)\n .style('padding', _verticalPadding + 'px 0');\n\n var buttons = _menu.selectAll('.edit-menu-item')\n .data(ops);\n\n // enter\n var buttonsEnter = buttons.enter()\n .append('button')\n .attr('class', function (d) { return 'edit-menu-item edit-menu-item-' + d.id; })\n .style('height', buttonHeight + 'px')\n .on('click', click)\n // don't listen for `mouseup` because we only care about non-mouse pointer types\n .on('pointerup', pointerup)\n .on('pointerdown mousedown', function pointerdown(d3_event) {\n // don't let button presses also act as map input - #1869\n d3_event.stopPropagation();\n })\n .on('mouseenter.highlight', function(d3_event, d) {\n if (d3_select(this).classed('disabled')) return;\n\n if (d.relatedEntityIds) {\n utilHighlightEntities(d.relatedEntityIds(), true, context);\n }\n\n if (d.getAuxiliaryGeometry) {\n drawAuxiliaryGeometry(context, d.getAuxiliaryGeometry());\n }\n })\n .on('mouseleave.highlight', function(d3_event, d) {\n if (d.relatedEntityIds) {\n utilHighlightEntities(d.relatedEntityIds(), false, context);\n }\n\n if (d.getAuxiliaryGeometry) {\n drawAuxiliaryGeometry(context, []);\n }\n });\n\n buttonsEnter.each(function(d) {\n var tooltip = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .heading(() => d.title)\n .title(d.tooltip)\n .keys([d.keys[0]]);\n\n _tooltips.push(tooltip);\n\n d3_select(this)\n .call(tooltip)\n .append('div')\n .attr('class', 'icon-wrap')\n .call(svgIcon(d.icon && d.icon() || '#iD-operation-' + d.id, 'operation'));\n });\n\n if (showLabels) {\n buttonsEnter.append('span')\n .attr('class', 'label')\n .each(function(d) {\n d3_select(this).call(d.title);\n });\n }\n\n // update\n buttonsEnter\n .merge(buttons)\n .classed('disabled', function(d) { return d.disabled(); });\n\n updatePosition();\n\n var initialScale = context.projection.scale();\n context.map()\n .on('move.edit-menu', function() {\n if (initialScale !== context.projection.scale()) {\n editMenu.close();\n }\n })\n .on('drawn.edit-menu', function(info) {\n if (info.full) updatePosition();\n });\n\n var lastPointerUpType;\n // `pointerup` is always called before `click`\n function pointerup(d3_event) {\n lastPointerUpType = d3_event.pointerType;\n }\n\n function click(d3_event, operation) {\n d3_event.stopPropagation();\n\n if (operation.relatedEntityIds) {\n utilHighlightEntities(operation.relatedEntityIds(), false, context);\n }\n\n if (operation.disabled()) {\n if (lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen') {\n // there are no tooltips for touch interactions so flash feedback instead\n context.ui().flash\n .duration(4000)\n .iconName('#iD-operation-' + operation.id)\n .iconClass('operation disabled')\n .label(operation.tooltip())();\n }\n } else {\n if (lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen') {\n context.ui().flash\n .duration(2000)\n .iconName('#iD-operation-' + operation.id)\n .iconClass('operation')\n .label(operation.annotation() || operation.title)();\n }\n\n operation();\n editMenu.close();\n }\n lastPointerUpType = null;\n }\n\n dispatch.call('toggled', this, true);\n };\n\n function updatePosition() {\n\n if (!_menu || _menu.empty()) return;\n\n var anchorLoc = context.projection(_anchorLocLonLat);\n\n var viewport = context.surfaceRect();\n\n if (anchorLoc[0] < 0 ||\n anchorLoc[0] > viewport.width ||\n anchorLoc[1] < 0 ||\n anchorLoc[1] > viewport.height) {\n // close the menu if it's gone offscreen\n\n editMenu.close();\n return;\n }\n\n var menuLeft = displayOnLeft(viewport);\n\n var offset = [0, 0];\n\n offset[0] = menuLeft ? -1 * (_menuSideMargin + _menuWidth) : _menuSideMargin;\n\n if (_menuTop) {\n if (anchorLoc[1] - _menuHeight < _vpTopMargin) {\n // menu is near top viewport edge, shift downward\n offset[1] = -anchorLoc[1] + _vpTopMargin;\n } else {\n offset[1] = -_menuHeight;\n }\n } else {\n if (anchorLoc[1] + _menuHeight > (viewport.height - _vpBottomMargin)) {\n // menu is near bottom viewport edge, shift upwards\n offset[1] = -anchorLoc[1] - _menuHeight + viewport.height - _vpBottomMargin;\n } else {\n offset[1] = 0;\n }\n }\n\n var origin = geoVecAdd(anchorLoc, offset);\n // repositioning the menu to account for the top menu height\n var _verticalOffset = parseFloat(utilGetDimensions(d3_select('.top-toolbar-wrap'))[1]);\n origin[1] -= _verticalOffset;\n\n _menu\n .style('left', origin[0] + 'px')\n .style('top', origin[1] + 'px');\n\n var tooltipSide = tooltipPosition(viewport, menuLeft);\n _tooltips.forEach(function(tooltip) {\n tooltip.placement(tooltipSide);\n });\n\n function displayOnLeft(viewport) {\n if (localizer.textDirection() === 'ltr') {\n if ((anchorLoc[0] + _menuSideMargin + _menuWidth) > (viewport.width - _vpSideMargin)) {\n // right menu would be too close to the right viewport edge, go left\n return true;\n }\n // prefer right menu\n return false;\n\n } else { // rtl\n if ((anchorLoc[0] - _menuSideMargin - _menuWidth) < _vpSideMargin) {\n // left menu would be too close to the left viewport edge, go right\n return false;\n }\n // prefer left menu\n return true;\n }\n }\n\n function tooltipPosition(viewport, menuLeft) {\n if (localizer.textDirection() === 'ltr') {\n if (menuLeft) {\n // if there's not room for a right-side menu then there definitely\n // isn't room for right-side tooltips\n return 'left';\n }\n if ((anchorLoc[0] + _menuSideMargin + _menuWidth + _tooltipWidth) > (viewport.width - _vpSideMargin)) {\n // right tooltips would be too close to the right viewport edge, go left\n return 'left';\n }\n // prefer right tooltips\n return 'right';\n\n } else { // rtl\n if (!menuLeft) {\n return 'right';\n }\n if ((anchorLoc[0] - _menuSideMargin - _menuWidth - _tooltipWidth) < _vpSideMargin) {\n // left tooltips would be too close to the left viewport edge, go right\n return 'right';\n }\n // prefer left tooltips\n return 'left';\n }\n }\n }\n\n editMenu.close = function () {\n\n context.map()\n .on('move.edit-menu', null)\n .on('drawn.edit-menu', null);\n\n _menu.remove();\n _tooltips = [];\n\n // Clean up any auxiliary overlays\n drawAuxiliaryGeometry(context, []);\n\n dispatch.call('toggled', this, false);\n };\n\n editMenu.anchorLoc = function(val) {\n if (!arguments.length) return _anchorLoc;\n _anchorLoc = val;\n _anchorLocLonLat = context.projection.invert(_anchorLoc);\n return editMenu;\n };\n\n editMenu.triggerType = function(val) {\n if (!arguments.length) return _triggerType;\n _triggerType = val;\n return editMenu;\n };\n\n editMenu.operations = function(val) {\n if (!arguments.length) return _operations;\n _operations = val;\n return editMenu;\n };\n\n return utilRebind(editMenu, dispatch, 'on');\n}\n\n\n// Helper function to draw/remove reflect axis overlay\nfunction drawAuxiliaryGeometry(context, d) {\n const surface = context.surface();\n // Append to the OSM data layer to be in the same coordinate space as map features\n const container = surface.selectAll('.data-layer.osm .auxiliary');\n const paths = container.selectAll('path')\n .data(d, d => d.id);\n\n paths.exit().remove();\n const enter = paths.enter()\n .append('path');\n\n enter.merge(paths)\n .attr('class', d => d.klass)\n .attr('d', d => d.path);\n}\n", "import { t } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\n\nexport function uiFeatureInfo(context) {\n function update(selection) {\n var features = context.features();\n var stats = features.stats();\n var dateMatchCount = features.dateMatchCount();\n var count = 0;\n var hiddenList = features.hidden().map(function(k) {\n if (stats[k]) {\n count += stats[k];\n return t.append('inspector.title_count', {\n title: t('feature.' + k + '.description'),\n count: stats[k]\n });\n }\n return null;\n }).filter(Boolean);\n count += dateMatchCount;\n\n selection.text('');\n\n if (hiddenList.length || dateMatchCount > 0) {\n var tooltipBehavior = uiTooltip()\n .placement('top')\n .title(function() {\n return selection => {\n hiddenList.forEach(hiddenFeature => {\n selection.append('div').call(hiddenFeature);\n });\n };\n });\n\n selection.append('a')\n .attr('class', 'chip')\n .attr('href', '#')\n .call(t.append('feature_info.hidden_warning', { count: count }))\n .call(tooltipBehavior)\n .on('click', function(d3_event) {\n tooltipBehavior.hide();\n d3_event.preventDefault();\n // open the Map Data pane\n context.ui().togglePanes(context.container().select('.map-panes .map-data-pane'));\n });\n }\n\n selection\n .classed('hide', !hiddenList.length && !dateMatchCount);\n }\n\n\n return function(selection) {\n update(selection);\n\n context.features().on('change.feature_info', function() {\n update(selection);\n });\n };\n}\n", "import { timeout as d3_timeout } from 'd3-timer';\n\nexport function uiFlash(context) {\n var _flashTimer;\n\n var _duration = 2000;\n var _iconName = '#iD-icon-no';\n var _iconClass = 'disabled';\n var _label = s => s.text('');\n\n function flash() {\n if (_flashTimer) {\n _flashTimer.stop();\n }\n\n context.container().select('.main-footer-wrap')\n .classed('footer-hide', true)\n .classed('footer-show', false);\n context.container().select('.flash-wrap')\n .classed('footer-hide', false)\n .classed('footer-show', true);\n\n var content = context.container().select('.flash-wrap').selectAll('.flash-content')\n .data([0]);\n\n // Enter\n var contentEnter = content.enter()\n .append('div')\n .attr('class', 'flash-content');\n\n var iconEnter = contentEnter\n .append('svg')\n .attr('class', 'flash-icon icon')\n .append('g')\n .attr('transform', 'translate(10,10)');\n\n iconEnter\n .append('circle')\n .attr('r', 9);\n\n iconEnter\n .append('use')\n .attr('transform', 'translate(-7,-7)')\n .attr('width', '14')\n .attr('height', '14');\n\n contentEnter\n .append('div')\n .attr('class', 'flash-text');\n\n\n // Update\n content = content\n .merge(contentEnter);\n\n content\n .selectAll('.flash-icon')\n .attr('class', 'icon flash-icon ' + (_iconClass || ''));\n\n content\n .selectAll('.flash-icon use')\n .attr('xlink:href', _iconName);\n\n content\n .selectAll('.flash-text')\n .attr('class', 'flash-text')\n .call(_label);\n\n\n _flashTimer = d3_timeout(function() {\n _flashTimer = null;\n context.container().select('.main-footer-wrap')\n .classed('footer-hide', false)\n .classed('footer-show', true);\n context.container().select('.flash-wrap')\n .classed('footer-hide', true)\n .classed('footer-show', false);\n }, _duration);\n\n return content;\n }\n\n\n flash.duration = function(_) {\n if (!arguments.length) return _duration;\n _duration = _;\n return flash;\n };\n\n flash.label = function(_) {\n if (!arguments.length) return _label;\n if (typeof _ !== 'function') {\n _label = selection => selection.text(_);\n } else {\n _label = selection => selection.text('').call(_);\n }\n return flash;\n };\n\n flash.iconName = function(_) {\n if (!arguments.length) return _iconName;\n _iconName = _;\n return flash;\n };\n\n flash.iconClass = function(_) {\n if (!arguments.length) return _iconClass;\n _iconClass = _;\n return flash;\n };\n\n return flash;\n}\n", "import { uiCmd } from './cmd';\nimport { utilDetect } from '../util/detect';\n\nexport function uiFullScreen(context) {\n var element = context.container().node();\n // var button = d3_select(null);\n\n\n function getFullScreenFn() {\n if (element.requestFullscreen) {\n return element.requestFullscreen;\n } else if (element.msRequestFullscreen) {\n return element.msRequestFullscreen;\n } else if (element.mozRequestFullScreen) {\n return element.mozRequestFullScreen;\n } else if (element.webkitRequestFullscreen) {\n return element.webkitRequestFullscreen;\n }\n }\n\n\n function getExitFullScreenFn() {\n if (document.exitFullscreen) {\n return document.exitFullscreen;\n } else if (document.msExitFullscreen) {\n return document.msExitFullscreen;\n } else if (document.mozCancelFullScreen) {\n return document.mozCancelFullScreen;\n } else if (document.webkitExitFullscreen) {\n return document.webkitExitFullscreen;\n }\n }\n\n\n function isFullScreen() {\n return document.fullscreenElement ||\n document.mozFullScreenElement ||\n document.webkitFullscreenElement ||\n document.msFullscreenElement;\n }\n\n\n function isSupported() {\n return !!getFullScreenFn();\n }\n\n\n function fullScreen(d3_event) {\n d3_event.preventDefault();\n if (!isFullScreen()) {\n // button.classed('active', true);\n getFullScreenFn().apply(element);\n } else {\n // button.classed('active', false);\n getExitFullScreenFn().apply(document);\n }\n }\n\n\n return function() { // selection) {\n if (!isSupported()) return;\n\n // button = selection.append('button')\n // .attr('title', t('full_screen'))\n // .on('click', fullScreen)\n // .call(tooltip);\n\n // button.append('span')\n // .attr('class', 'icon full-screen');\n\n var detected = utilDetect();\n var keys = (detected.os === 'mac' ? [uiCmd('\u2303\u2318F'), 'f11'] : ['f11']);\n context.keybinding().on(keys, fullScreen);\n };\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t, localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\nimport { geoExtent } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\nimport { uiLoading } from './loading';\n\nexport function uiGeolocate(context) {\n var _geolocationOptions = {\n // prioritize speed and power usage over precision\n enableHighAccuracy: false,\n // don't hang indefinitely getting the location\n timeout: 6000 // 6sec\n };\n var _locating = uiLoading(context).message(t.addOrUpdate('geolocate.locating')).blocking(true);\n var _layer = context.layers().layer('geolocate');\n var _position;\n var _extent;\n var _timeoutID;\n var _button = d3_select(null);\n\n function click() {\n if (context.inIntro()) return;\n if (!_layer.enabled() && !_locating.isShown()) {\n\n // This timeout ensures that we still call finish() even if\n // the user declines to share their location in Firefox\n _timeoutID = setTimeout(error, 10000 /* 10sec */ );\n\n context.container().call(_locating);\n // get the latest position even if we already have one\n navigator.geolocation.getCurrentPosition(success, error, _geolocationOptions);\n } else {\n _locating.close();\n _layer.enabled(null, false);\n updateButtonState();\n }\n }\n\n function zoomTo() {\n context.enter(modeBrowse(context));\n\n var map = context.map();\n _layer.enabled(_position, true);\n updateButtonState();\n map.centerZoomEase(_extent.center(), Math.min(20, map.extentZoom(_extent)));\n }\n\n function success(geolocation) {\n _position = geolocation;\n var coords = _position.coords;\n _extent = geoExtent([coords.longitude, coords.latitude]).padByMeters(coords.accuracy);\n zoomTo();\n finish();\n }\n\n function error() {\n if (_position) {\n // use the position from a previous call if we have one\n zoomTo();\n } else {\n context.ui().flash\n .label(t.append('geolocate.location_unavailable'))\n .iconName('#iD-icon-geolocate')();\n }\n\n finish();\n }\n\n function finish() {\n _locating.close(); // unblock ui\n if (_timeoutID) { clearTimeout(_timeoutID); }\n _timeoutID = undefined;\n }\n\n function updateButtonState() {\n _button.classed('active', _layer.enabled());\n _button.attr('aria-pressed', _layer.enabled());\n }\n\n return function(selection) {\n if (!navigator.geolocation || !navigator.geolocation.getCurrentPosition) return;\n\n _button = selection\n .append('button')\n .on('click', click)\n .attr('aria-pressed', false)\n .call(svgIcon('#iD-icon-geolocate', 'light'))\n .call(uiTooltip()\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(() => t.append('geolocate.title'))\n );\n };\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { t } from '../../core/localizer';\n\n\nexport function uiPanelBackground(context) {\n const background = context.background();\n let _currSource = null;\n let _metadata = {};\n const _metadataKeys = [\n 'zoom', 'vintage', 'source', 'description', 'resolution', 'accuracy'\n ];\n\n var debouncedRedraw = debounce(redraw, 250);\n\n function redraw(selection) {\n var source = background.baseLayerSource();\n if (!source) return;\n\n if (_currSource?.id !== source.id) {\n _currSource = source;\n _metadata = {};\n }\n\n selection.text('');\n\n var list = selection\n .append('ul')\n .attr('class', 'background-info');\n\n list\n .append('li')\n .call(_currSource.label());\n\n _metadataKeys.forEach(function(k) {\n list\n .append('li')\n .attr('class', 'background-info-list-' + k)\n .classed('hide', !_metadata[k])\n .call(t.append('info_panels.background.' + k, { suffix: ':' }))\n .append('span')\n .attr('class', 'background-info-span-' + k)\n .text(_metadata[k]);\n });\n\n debouncedGetMetadata(selection);\n\n var toggleTiles = context.getDebug('tile') ? 'hide_tiles' : 'show_tiles';\n\n selection\n .append('a')\n .call(t.append('info_panels.background.' + toggleTiles))\n .attr('href', '#')\n .attr('class', 'button button-toggle-tiles')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.setDebug('tile', !context.getDebug('tile'));\n selection.call(redraw);\n });\n }\n\n\n var debouncedGetMetadata = debounce(getMetadata, 250);\n\n function getMetadata(selection) {\n var tile = context.container().select('.layer-background img.tile-center'); // tile near viewport center\n if (tile.empty()) return;\n\n var sourceId = _currSource.id;\n var d = tile.datum();\n var zoom = (d && d.length >= 3 && d[2]) || Math.floor(context.map().zoom());\n var center = context.map().center();\n\n // update zoom\n _metadata.zoom = String(zoom);\n selection.selectAll('.background-info-list-zoom')\n .classed('hide', false)\n .selectAll('.background-info-span-zoom')\n .text(_metadata.zoom);\n\n if (!d || !d.length >= 3) return;\n\n background.baseLayerSource().getMetadata(center, d, function(err, result) {\n if (err || _currSource.id !== sourceId) return;\n\n // update vintage\n var vintage = result.vintage;\n _metadata.vintage = (vintage && vintage.range) || t('info_panels.background.unknown');\n selection.selectAll('.background-info-list-vintage')\n .classed('hide', false)\n .selectAll('.background-info-span-vintage')\n .text(_metadata.vintage);\n\n // update other _metadata\n _metadataKeys.forEach(function(k) {\n if (k === 'zoom' || k === 'vintage') return; // done already\n var val = result[k];\n _metadata[k] = val;\n selection.selectAll('.background-info-list-' + k)\n .classed('hide', !val)\n .selectAll('.background-info-span-' + k)\n .text(val);\n });\n });\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.map()\n .on('drawn.info-background', function() {\n selection.call(debouncedRedraw);\n })\n .on('move.info-background', function() {\n selection.call(debouncedGetMetadata);\n });\n\n };\n\n panel.off = function() {\n context.map()\n .on('drawn.info-background', null)\n .on('move.info-background', null);\n };\n\n panel.id = 'background';\n panel.label = t.append('info_panels.background.title');\n panel.key = t('info_panels.background.key');\n\n\n return panel;\n}\n", "import { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\n\n\nexport function uiPanelHistory(context) {\n var osm;\n\n function displayTimestamp(timestamp) {\n if (!timestamp) return t('info_panels.history.unknown');\n var options = {\n day: 'numeric', month: 'short', year: 'numeric',\n hour: 'numeric', minute: 'numeric', second: 'numeric'\n };\n var d = new Date(timestamp);\n if (isNaN(d.getTime())) return t('info_panels.history.unknown');\n return d.toLocaleString(localizer.localeCode(), options);\n }\n\n\n function displayUser(selection, userName) {\n if (!userName) {\n selection\n .append('span')\n .call(t.append('info_panels.history.unknown'));\n return;\n }\n\n selection\n .append('span')\n .attr('class', 'user-name')\n .text(userName);\n\n var links = selection\n .append('div')\n .attr('class', 'links');\n\n if (osm) {\n links\n .append('a')\n .attr('class', 'user-osm-link')\n .attr('href', osm.userURL(userName))\n .attr('target', '_blank')\n .call(t.append('info_panels.history.profile_link'));\n }\n }\n\n\n function displayChangeset(selection, changeset) {\n if (!changeset) {\n selection\n .append('span')\n .call(t.append('info_panels.history.unknown'));\n return;\n }\n\n selection\n .append('span')\n .attr('class', 'changeset-id')\n .text(changeset);\n\n var links = selection\n .append('div')\n .attr('class', 'links');\n\n if (osm) {\n links\n .append('a')\n .attr('class', 'changeset-osm-link')\n .attr('href', osm.changesetURL(changeset))\n .attr('target', '_blank')\n .call(t.append('info_panels.history.changeset_link'));\n }\n\n links\n .append('a')\n .attr('class', 'changeset-osmcha-link')\n .attr('href', 'https://osmcha.openhistoricalmap.org/changesets/' + changeset)\n .attr('target', '_blank')\n .text('OSMCha');\n }\n\n\n function redraw(selection) {\n var selectedNoteID = context.selectedNoteID();\n osm = context.connection();\n var selected, note, entity;\n if (selectedNoteID && osm) { // selected 1 note\n selected = [ t.append('note.note', { suffix: ' ' + selectedNoteID }) ];\n note = osm.getNote(selectedNoteID);\n } else { // selected 1..n entities\n selected = context.selectedIDs()\n .filter(function(e) { return context.hasEntity(e); });\n if (selected.length) {\n entity = context.entity(selected[0]);\n }\n }\n\n var singular = selected.length === 1 ? selected[0] : null;\n\n selection.text('');\n\n const heading = selection\n .append('h4')\n .attr('class', 'history-heading');\n\n if (singular) {\n if (typeof singular === 'function') {\n heading.call(singular);\n } else {\n heading.text(singular);\n }\n } else {\n heading.call(t.append('info_panels.selected', { n: selected.length }));\n }\n\n if (!singular) return;\n\n if (entity) {\n selection.call(redrawEntity, entity);\n } else if (note) {\n selection.call(redrawNote, note);\n }\n }\n\n\n function redrawNote(selection, note) {\n if (!note || note.isNew()) {\n selection\n .append('div')\n .call(t.append('info_panels.history.note_no_history'));\n return;\n }\n\n var list = selection\n .append('ul');\n\n list\n .append('li')\n .call(t.append('info_panels.history.note_comments', { suffix: ':' }))\n .append('span')\n .text(note.comments.length);\n\n if (note.comments.length) {\n list\n .append('li')\n .call(t.append('info_panels.history.note_created_date', { suffix: ':' }))\n .append('span')\n .text(displayTimestamp(note.comments[0].date));\n\n list\n .append('li')\n .call(t.append('info_panels.history.note_created_user', { suffix: ':' }))\n .call(displayUser, note.comments[0].user);\n }\n\n if (osm) {\n selection\n .append('a')\n .attr('class', 'view-history-on-osm')\n .attr('target', '_blank')\n .attr('href', osm.noteURL(note))\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append('info_panels.history.note_link_text'));\n }\n }\n\n\n function redrawEntity(selection, entity) {\n if (!entity || entity.isNew()) {\n selection\n .append('div')\n .call(t.append('info_panels.history.no_history'));\n return;\n }\n\n var links = selection\n .append('div')\n .attr('class', 'links');\n\n if (osm) {\n links\n .append('a')\n .attr('class', 'view-history-on-osm')\n .attr('href', osm.historyURL(entity))\n .attr('target', '_blank')\n .call(t.append('info_panels.history.history_link'));\n }\n\n var list = selection\n .append('ul');\n\n list\n .append('li')\n .call(t.append('info_panels.history.version', { suffix: ':' }))\n .append('span')\n .text(entity.version);\n\n list\n .append('li')\n .call(t.append('info_panels.history.last_edit', { suffix: ':' }))\n .append('span')\n .text(displayTimestamp(entity.timestamp));\n\n list\n .append('li')\n .call(t.append('info_panels.history.edited_by', { suffix: ':' }))\n .call(displayUser, entity.user);\n\n list\n .append('li')\n .call(t.append('info_panels.history.changeset', { suffix: ':' }))\n .call(displayChangeset, entity.changeset);\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.map()\n .on('drawn.info-history', function() {\n selection.call(redraw);\n });\n\n context\n .on('enter.info-history', function() {\n selection.call(redraw);\n });\n };\n\n panel.off = function() {\n context.map().on('drawn.info-history', null);\n context.on('enter.info-history', null);\n };\n\n panel.id = 'history';\n panel.label = t.append('info_panels.history.title');\n panel.key = t('info_panels.history.key');\n\n\n return panel;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { decimalCoordinatePair, dmsCoordinatePair } from '../../util/units';\nimport { t } from '../../core/localizer';\nimport { services } from '../../services';\n\n\nexport function uiPanelLocation(context) {\n var currLocation = '';\n\n\n function redraw(selection) {\n selection.html('');\n\n var list = selection\n .append('ul');\n\n // Mouse coordinates\n var coord = context.map().mouseCoordinates();\n if (coord.some(isNaN)) {\n coord = context.map().center();\n }\n\n list\n .append('li')\n .text(dmsCoordinatePair(coord))\n .append('li')\n .text(decimalCoordinatePair(coord));\n\n // Location Info\n selection\n .append('div')\n .attr('class', 'location-info')\n .text(currLocation || ' ');\n\n debouncedGetLocation(selection, coord);\n }\n\n\n var debouncedGetLocation = debounce(getLocation, 250);\n function getLocation(selection, coord) {\n if (!services.geocoder) {\n currLocation = t('info_panels.location.unknown_location');\n selection.selectAll('.location-info')\n .text(currLocation);\n } else {\n services.geocoder.reverse(coord, function(err, result) {\n currLocation = result ? result.display_name : t('info_panels.location.unknown_location');\n selection.selectAll('.location-info')\n .text(currLocation);\n });\n }\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.surface()\n .on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'move.info-location', function() {\n selection.call(redraw);\n });\n };\n\n panel.off = function() {\n context.surface()\n .on('.info-location', null);\n };\n\n panel.id = 'location';\n panel.label = t.append('info_panels.location.title');\n panel.key = t('info_panels.location.key');\n\n\n return panel;\n}\n", "import {\n geoLength as d3_geoLength,\n geoPath as d3_geoPath\n} from 'd3-geo';\n\nimport { t, localizer } from '../../core/localizer';\nimport { displayArea, displayLength, decimalCoordinatePair, dmsCoordinatePair } from '../../util/units';\nimport { geoExtent, geoSphericalDistance } from '../../geo';\nimport { services } from '../../services';\nimport { utilGetAllNodes } from '../../util';\n\nexport function uiPanelMeasurement(context) {\n\n function radiansToMeters(r) {\n // using WGS84 authalic radius (6371007.1809 m)\n return r * 6371007.1809;\n }\n\n function steradiansToSqmeters(r) {\n // http://gis.stackexchange.com/a/124857/40446\n return r / (4 * Math.PI) * 510065621724000;\n }\n\n\n function toLineString(feature) {\n if (feature.type === 'LineString') return feature;\n\n var result = { type: 'LineString', coordinates: [] };\n if (feature.type === 'Polygon') {\n result.coordinates = feature.coordinates[0];\n } else if (feature.type === 'MultiPolygon') {\n result.coordinates = feature.coordinates[0][0];\n }\n\n return result;\n }\n\n var _isImperial = !localizer.usesMetric();\n\n function redraw(selection) {\n var graph = context.graph();\n var selectedNoteID = context.selectedNoteID();\n var osm = services.osm;\n\n var localeCode = localizer.localeCode();\n\n var heading;\n var center, location, centroid;\n var closed, geometry;\n var totalNodeCount, length = 0, area = 0, distance;\n\n if (selectedNoteID && osm) { // selected 1 note\n var note = osm.getNote(selectedNoteID);\n heading = t.append('note.note', { suffix: ' ' + selectedNoteID });\n location = note.loc;\n geometry = 'note';\n\n } else { // selected 1..n entities\n var selectedIDs = context.selectedIDs().filter(function(id) {\n return context.hasEntity(id);\n });\n var selected = selectedIDs.map(function(id) {\n return context.entity(id);\n });\n\n heading = selected.length === 1 ? selected[0].id :\n t.append('info_panels.selected', { n: selected.length });\n\n if (selected.length) {\n var extent = geoExtent();\n for (var i in selected) {\n var entity = selected[i];\n extent._extend(entity.extent(graph));\n\n geometry = entity.geometry(graph);\n if (geometry === 'line' || geometry === 'area') {\n closed = (entity.type === 'relation') || (entity.isClosed() && !entity.isDegenerate());\n var feature = entity.asGeoJSON(graph);\n length += radiansToMeters(d3_geoLength(toLineString(feature)));\n centroid = d3_geoPath(context.projection).centroid(entity.asGeoJSON(graph));\n centroid = centroid && context.projection.invert(centroid);\n if (!centroid || !isFinite(centroid[0]) || !isFinite(centroid[1])) {\n centroid = entity.extent(graph).center();\n }\n if (closed) {\n area += steradiansToSqmeters(entity.area(graph));\n }\n }\n }\n\n if (selected.length > 1) {\n geometry = null;\n closed = null;\n centroid = null;\n }\n\n if (selected.length === 2 &&\n selected[0].type === 'node' &&\n selected[1].type === 'node') {\n distance = geoSphericalDistance(selected[0].loc, selected[1].loc);\n }\n\n if (selected.length === 1 && selected[0].type === 'node') {\n location = selected[0].loc;\n } else {\n totalNodeCount = utilGetAllNodes(selectedIDs, context.graph()).length;\n }\n\n if (!location && !centroid) {\n center = extent.center();\n }\n }\n }\n\n selection.html('');\n\n if (heading && typeof heading === 'function') {\n selection\n .append('h4')\n .attr('class', 'measurement-heading')\n .call(heading);\n } else {\n selection\n .append('h4')\n .attr('class', 'measurement-heading')\n .text(heading);\n }\n\n var list = selection\n .append('ul');\n var coordItem;\n\n if (geometry) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.geometry', { suffix: ':' }))\n .append('span')\n .call(\n closed ? t.append('info_panels.measurement.closed_' + geometry) : t.append('geometry.' + geometry)\n );\n }\n\n if (totalNodeCount) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.node_count', { suffix: ':' }))\n .append('span')\n .text(totalNodeCount.toLocaleString(localeCode));\n }\n\n if (area) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.area', { suffix: ':' }))\n .append('span')\n .text(displayArea(area, _isImperial));\n }\n\n if (length) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.' + (closed ? 'perimeter' : 'length'), { suffix: ':' }))\n .append('span')\n .text(displayLength(length, _isImperial));\n }\n\n if (typeof distance === 'number') {\n list\n .append('li')\n .call(t.append('info_panels.measurement.distance', { suffix: ':' }))\n .append('span')\n .text(displayLength(distance, _isImperial));\n }\n\n if (location) {\n coordItem = list\n .append('li')\n .call(t.append('info_panels.measurement.location', { suffix: ':' }));\n coordItem.append('span')\n .text(dmsCoordinatePair(location));\n coordItem.append('span')\n .text(decimalCoordinatePair(location));\n }\n\n if (centroid) {\n coordItem = list\n .append('li')\n .call(t.append('info_panels.measurement.centroid', { suffix: ':' }));\n coordItem.append('span')\n .text(dmsCoordinatePair(centroid));\n coordItem.append('span')\n .text(decimalCoordinatePair(centroid));\n }\n\n if (center) {\n coordItem = list\n .append('li')\n .call(t.append('info_panels.measurement.center', { suffix: ':' }));\n coordItem.append('span')\n .text(dmsCoordinatePair(center));\n coordItem.append('span')\n .text(decimalCoordinatePair(center));\n }\n\n if (length || area || typeof distance === 'number') {\n var toggle = _isImperial ? 'imperial' : 'metric';\n selection\n .append('a')\n .call(t.append('info_panels.measurement.' + toggle))\n .attr('href', '#')\n .attr('class', 'button button-toggle-units')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n _isImperial = !_isImperial;\n selection.call(redraw);\n });\n }\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.map()\n .on('drawn.info-measurement', function() {\n selection.call(redraw);\n });\n\n context\n .on('enter.info-measurement', function() {\n selection.call(redraw);\n });\n };\n\n panel.off = function() {\n context.map().on('drawn.info-measurement', null);\n context.on('enter.info-measurement', null);\n };\n\n panel.id = 'measurement';\n panel.label = t.append('info_panels.measurement.title');\n panel.key = t('info_panels.measurement.key');\n\n\n return panel;\n}\n", "export * from './background';\nexport * from './history';\nexport * from './location';\nexport * from './measurement';\n\nimport { uiPanelBackground } from './background';\nimport { uiPanelHistory } from './history';\nimport { uiPanelLocation } from './location';\nimport { uiPanelMeasurement } from './measurement';\n\nexport var uiInfoPanels = {\n background: uiPanelBackground,\n history: uiPanelHistory,\n location: uiPanelLocation,\n measurement: uiPanelMeasurement,\n};\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\nimport { uiInfoPanels } from './panels';\n\n\nexport function uiInfo(context) {\n var ids = Object.keys(uiInfoPanels);\n var wasActive = ['measurement'];\n var panels = {};\n var active = {};\n\n // create panels\n ids.forEach(function(k) {\n if (!panels[k]) {\n panels[k] = uiInfoPanels[k](context);\n active[k] = false;\n }\n });\n\n\n function info(selection) {\n\n function redraw() {\n var activeids = ids.filter(function(k) { return active[k]; }).sort();\n\n var containers = infoPanels.selectAll('.panel-container')\n .data(activeids, function(k) { return k; });\n\n containers.exit()\n .style('opacity', 1)\n .transition()\n .duration(200)\n .style('opacity', 0)\n .on('end', function(d) {\n d3_select(this)\n .call(panels[d].off)\n .remove();\n });\n\n var enter = containers.enter()\n .append('div')\n .attr('class', function(d) { return 'fillD2 panel-container panel-container-' + d; });\n\n enter\n .style('opacity', 0)\n .transition()\n .duration(200)\n .style('opacity', 1);\n\n var title = enter\n .append('div')\n .attr('class', 'panel-title fillD2');\n\n title\n .append('h3')\n .each(function(d) { return panels[d].label(d3_select(this)); });\n\n title\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function(d3_event, d) {\n d3_event.stopImmediatePropagation();\n d3_event.preventDefault();\n info.toggle(d);\n })\n .call(svgIcon('#iD-icon-close'));\n\n enter\n .append('div')\n .attr('class', function(d) { return 'panel-content panel-content-' + d; });\n\n\n // redraw the panels\n infoPanels.selectAll('.panel-content')\n .each(function(d) {\n d3_select(this).call(panels[d]);\n });\n }\n\n\n info.toggle = function(which) {\n var activeids = ids.filter(function(k) { return active[k]; });\n\n if (which) { // toggle one\n active[which] = !active[which];\n if (activeids.length === 1 && activeids[0] === which) { // none active anymore\n wasActive = [which];\n }\n\n context.container().select('.' + which + '-panel-toggle-item')\n .classed('active', active[which])\n .select('input')\n .property('checked', active[which]);\n\n } else { // toggle all\n if (activeids.length) {\n wasActive = activeids;\n activeids.forEach(function(k) { active[k] = false; });\n } else {\n wasActive.forEach(function(k) { active[k] = true; });\n }\n }\n\n redraw();\n };\n\n\n var infoPanels = selection.selectAll('.info-panels')\n .data([0]);\n\n infoPanels = infoPanels.enter()\n .append('div')\n .attr('class', 'info-panels')\n .merge(infoPanels);\n\n redraw();\n\n context.keybinding()\n .on(uiCmd('\u2318' + t('info_panels.key')), function(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.stopImmediatePropagation();\n d3_event.preventDefault();\n info.toggle();\n });\n\n ids.forEach(function(k) {\n var key = t('info_panels.' + k + '.key', { default: null });\n if (!key) return;\n context.keybinding()\n .on(uiCmd('\u2318\u21E7' + key), function(d3_event) {\n d3_event.stopImmediatePropagation();\n d3_event.preventDefault();\n info.toggle(k);\n });\n });\n }\n\n return info;\n}\n", "import { easeLinear as d3_easeLinear } from 'd3-ease';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { localizer } from '../core/localizer';\nimport { uiToggle } from './toggle';\n\n\n// Tooltips and svg mask used to highlight certain features\nexport function uiCurtain(containerNode) {\n\n var surface = d3_select(null),\n tooltip = d3_select(null),\n darkness = d3_select(null);\n\n function curtain(selection) {\n surface = selection\n .append('svg')\n .attr('class', 'curtain')\n .style('top', 0)\n .style('left', 0);\n\n darkness = surface.append('path')\n .attr('x', 0)\n .attr('y', 0)\n .attr('class', 'curtain-darkness');\n\n d3_select(window).on('resize.curtain', resize);\n\n tooltip = selection.append('div')\n .attr('class', 'tooltip');\n\n tooltip\n .append('div')\n .attr('class', 'popover-arrow');\n\n tooltip\n .append('div')\n .attr('class', 'popover-inner');\n\n resize();\n\n\n function resize() {\n surface\n .attr('width', containerNode.clientWidth)\n .attr('height', containerNode.clientHeight);\n curtain.cut(darkness.datum());\n }\n }\n\n\n /**\n * Reveal cuts the curtain to highlight the given box,\n * and shows a tooltip with instructions next to the box.\n *\n * @param {String|ClientRect|HTMLElement} [box] box used to cut the curtain\n * @param {String} [text] text for a tooltip\n * @param {Object} [options]\n * @param {string} [options.tooltipClass] optional class to add to the tooltip\n * @param {integer} [options.duration] transition time in milliseconds\n * @param {string} [options.buttonText] if set, create a button with this text label\n * @param {function} [options.buttonCallback] if set, the callback for the button\n * @param {function} [options.padding] extra margin in px to put around bbox\n * @param {String|ClientRect} [options.tooltipBox] box for tooltip position, if different from box for the curtain\n */\n curtain.reveal = function(box, html, options) {\n options = options || {};\n\n if (typeof box === 'string') {\n box = d3_select(box).node();\n }\n if (box && box.getBoundingClientRect) {\n box = copyBox(box.getBoundingClientRect());\n }\n if (box) {\n var containerRect = containerNode.getBoundingClientRect();\n box.top -= containerRect.top;\n box.left -= containerRect.left;\n }\n if (box && options.padding) {\n box.top -= options.padding;\n box.left -= options.padding;\n box.bottom += options.padding;\n box.right += options.padding;\n box.height += options.padding * 2;\n box.width += options.padding * 2;\n }\n\n var tooltipBox;\n if (options.tooltipBox) {\n tooltipBox = options.tooltipBox;\n if (typeof tooltipBox === 'string') {\n tooltipBox = d3_select(tooltipBox).node();\n }\n if (tooltipBox && tooltipBox.getBoundingClientRect) {\n tooltipBox = copyBox(tooltipBox.getBoundingClientRect());\n }\n } else {\n tooltipBox = box;\n }\n\n if (tooltipBox && html) {\n\n if (html.indexOf('**') !== -1) {\n if (html.indexOf(')(.+?)(\\*\\*)/, '$1$2$3');\n } else {\n html = html.replace(/^(.+?)(\\*\\*)/, '$1$2');\n }\n // pseudo markdown bold text for the instruction section..\n html = html.replace(/\\*\\*(.*?)\\*\\*/g, '$1');\n }\n\n html = html.replace(/\\*(.*?)\\*/g, '$1'); // emphasis\n html = html.replace(/\\{br\\}/g, '

    '); // linebreak\n\n if (options.buttonText && options.buttonCallback) {\n html += '
    ' +\n '
    ';\n }\n\n var classes = 'curtain-tooltip popover tooltip arrowed in ' + (options.tooltipClass || '');\n tooltip\n .classed(classes, true)\n .selectAll('.popover-inner')\n .html(html);\n\n if (options.buttonText && options.buttonCallback) {\n var button = tooltip.selectAll('.button-section .button.action');\n button\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n options.buttonCallback();\n });\n }\n\n var tip = copyBox(tooltip.node().getBoundingClientRect()),\n w = containerNode.clientWidth,\n h = containerNode.clientHeight,\n tooltipWidth = 200,\n tooltipArrow = 5,\n side, pos;\n\n\n // hack: this will have bottom placement,\n // so need to reserve extra space for the tooltip illustration.\n if (options.tooltipClass === 'intro-mouse') {\n tip.height += 80;\n }\n\n // trim box dimensions to just the portion that fits in the container..\n if (tooltipBox.top + tooltipBox.height > h) {\n tooltipBox.height -= (tooltipBox.top + tooltipBox.height - h);\n }\n if (tooltipBox.left + tooltipBox.width > w) {\n tooltipBox.width -= (tooltipBox.left + tooltipBox.width - w);\n }\n\n // determine tooltip placement..\n const onLeftOrRightEdge = tooltipBox.left + tooltipBox.width / 2 > w - 100 || tooltipBox.left + tooltipBox.width / 2 < 100;\n if (tooltipBox.top + tooltipBox.height < 100 && !onLeftOrRightEdge) {\n // tooltip below box..\n side = 'bottom';\n pos = [\n tooltipBox.left + tooltipBox.width / 2 - tip.width / 2,\n tooltipBox.top + tooltipBox.height\n ];\n\n } else if (tooltipBox.top > h - 140 && !onLeftOrRightEdge) {\n // tooltip above box..\n side = 'top';\n pos = [\n tooltipBox.left + tooltipBox.width / 2 - tip.width / 2,\n tooltipBox.top - tip.height\n ];\n\n } else {\n // tooltip to the side of the tooltipBox..\n var tipY = tooltipBox.top + tooltipBox.height / 2 - tip.height / 2;\n\n if (localizer.textDirection() === 'rtl') {\n if (tooltipBox.left - tooltipWidth - tooltipArrow < 70) {\n side = 'right';\n pos = [tooltipBox.left + tooltipBox.width + tooltipArrow, tipY];\n\n } else {\n side = 'left';\n pos = [tooltipBox.left - tooltipWidth - tooltipArrow, tipY];\n }\n\n } else {\n if (tooltipBox.left + tooltipBox.width + tooltipArrow + tooltipWidth > w - 70) {\n side = 'left';\n pos = [tooltipBox.left - tooltipWidth - tooltipArrow, tipY];\n } else {\n side = 'right';\n pos = [tooltipBox.left + tooltipBox.width + tooltipArrow, tipY];\n }\n }\n }\n\n if (options.duration !== 0 || !tooltip.classed(side)) {\n tooltip.call(uiToggle(true));\n }\n\n tooltip\n .style('top', pos[1] + 'px')\n .style('left', pos[0] + 'px')\n .attr('class', classes + ' ' + side);\n\n\n // shift popover-inner if it is very close to the top or bottom edge\n // (doesn't affect the placement of the popover-arrow)\n var shiftY = 0;\n if (side === 'left' || side === 'right') {\n if (pos[1] < 60) {\n shiftY = 60 - pos[1];\n } else if (pos[1] + tip.height > h - 100) {\n shiftY = h - pos[1] - tip.height - 100;\n }\n }\n tooltip.selectAll('.popover-inner')\n .style('top', shiftY + 'px');\n\n } else {\n tooltip\n .classed('in', false)\n .call(uiToggle(false));\n }\n\n curtain.cut(box, options.duration);\n\n return tooltip;\n };\n\n\n curtain.cut = function(datum, duration) {\n darkness.datum(datum)\n .interrupt();\n\n var selection;\n if (duration === 0) {\n selection = darkness;\n } else {\n selection = darkness\n .transition()\n .duration(duration || 600)\n .ease(d3_easeLinear);\n }\n\n selection\n .attr('d', function(d) {\n var containerWidth = containerNode.clientWidth;\n var containerHeight = containerNode.clientHeight;\n var string = 'M 0,0 L 0,' + containerHeight + ' L ' +\n containerWidth + ',' + containerHeight + 'L' +\n containerWidth + ',0 Z';\n\n if (!d) return string;\n return string + 'M' +\n d.left + ',' + d.top + 'L' +\n d.left + ',' + (d.top + d.height) + 'L' +\n (d.left + d.width) + ',' + (d.top + d.height) + 'L' +\n (d.left + d.width) + ',' + (d.top) + 'Z';\n\n });\n };\n\n\n curtain.remove = function() {\n surface.remove();\n tooltip.remove();\n d3_select(window).on('resize.curtain', null);\n };\n\n\n // ClientRects are immutable, so copy them to an object,\n // in case we need to trim the height/width.\n function copyBox(src) {\n return {\n top: src.top,\n right: src.right,\n bottom: src.bottom,\n left: src.left,\n width: src.width,\n height: src.height\n };\n }\n\n\n return curtain;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { helpHtml } from './helper';\nimport { t } from '../../core/localizer';\nimport { utilRebind } from '../../util/rebind';\n\n\nexport function uiIntroWelcome(context, reveal) {\n var dispatch = d3_dispatch('done');\n\n var chapter = {\n title: 'intro.welcome.title'\n };\n\n\n function welcome() {\n context.map().centerZoom([-85.63591, 41.94285], 19);\n reveal('.intro-nav-wrap .chapter-welcome',\n helpHtml('intro.welcome.welcome'),\n { buttonText: t.html('intro.ok'), buttonCallback: practice }\n );\n }\n\n function practice() {\n reveal('.intro-nav-wrap .chapter-welcome',\n helpHtml('intro.welcome.practice'),\n { buttonText: t.html('intro.ok'), buttonCallback: words }\n );\n }\n\n function words() {\n reveal('.intro-nav-wrap .chapter-welcome',\n helpHtml('intro.welcome.words'),\n { buttonText: t.html('intro.ok'), buttonCallback: chapters }\n );\n }\n\n\n function chapters() {\n dispatch.call('done');\n reveal('.intro-nav-wrap .chapter-navigation',\n helpHtml('intro.welcome.chapters', { next: t('intro.navigation.title') })\n );\n }\n\n\n chapter.enter = function() {\n welcome();\n };\n\n\n chapter.exit = function() {\n context.container().select('.curtain-tooltip.intro-mouse')\n .selectAll('.counter')\n .remove();\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pointBox, transitionTime } from './helper';\n\n\nexport function uiIntroNavigation(context, reveal) {\n var dispatch = d3_dispatch('done');\n var timeouts = [];\n var hallId = 'n2061';\n var townHall = [-85.63591, 41.94285];\n var springStreetId = 'w397';\n var springStreetEndId = 'n1834';\n var springStreet = [-85.63582, 41.94255];\n var onewayField = presetManager.field('oneway');\n var maxspeedField = presetManager.field('maxspeed');\n\n\n var chapter = {\n title: 'intro.navigation.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function isTownHallSelected() {\n var ids = context.selectedIDs();\n return ids.length === 1 && ids[0] === hallId;\n }\n\n\n function dragMap() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var msec = transitionTime(townHall, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(townHall, 19, msec);\n\n timeout(function() {\n var centerStart = context.map().center();\n\n var textId = context.lastPointerType() === 'mouse' ? 'drag' : 'drag_touch';\n var dragString = helpHtml('intro.navigation.map_info') + '{br}' + helpHtml('intro.navigation.' + textId);\n reveal('.main-map .surface', dragString);\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', dragString, { duration: 0 });\n });\n\n context.map().on('move.intro', function() {\n var centerNow = context.map().center();\n if (centerStart[0] !== centerNow[0] || centerStart[1] !== centerNow[1]) {\n context.map().on('move.intro', null);\n timeout(function() { continueTo(zoomMap); }, 3000);\n }\n });\n\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function zoomMap() {\n var zoomStart = context.map().zoom();\n\n var textId = context.lastPointerType() === 'mouse' ? 'zoom' : 'zoom_touch';\n var zoomString = helpHtml('intro.navigation.' + textId);\n\n reveal('.main-map .surface', zoomString);\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', zoomString, { duration: 0 });\n });\n\n context.map().on('move.intro', function() {\n if (context.map().zoom() !== zoomStart) {\n context.map().on('move.intro', null);\n timeout(function() { continueTo(features); }, 3000);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function features() {\n var onClick = function() { continueTo(pointsLinesAreas); };\n\n reveal('.main-map .surface', helpHtml('intro.navigation.features'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', helpHtml('intro.navigation.features'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('drawn.intro', null);\n nextStep();\n }\n }\n\n function pointsLinesAreas() {\n var onClick = function() { continueTo(nodesWays); };\n\n reveal('.main-map .surface', helpHtml('intro.navigation.points_lines_areas'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', helpHtml('intro.navigation.points_lines_areas'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('drawn.intro', null);\n nextStep();\n }\n }\n\n function nodesWays() {\n var onClick = function() { continueTo(clickTownHall); };\n\n reveal('.main-map .surface', helpHtml('intro.navigation.nodes_ways'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', helpHtml('intro.navigation.nodes_ways'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('drawn.intro', null);\n nextStep();\n }\n }\n\n function clickTownHall() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n reveal(null, null, { duration: 0 });\n context.map().centerZoomEase(entity.loc, 19, 500);\n\n timeout(function() {\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n var textId = context.lastPointerType() === 'mouse' ? 'click_townhall' : 'tap_townhall';\n reveal(box, helpHtml('intro.navigation.' + textId));\n\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.navigation.' + textId), { duration: 0 });\n });\n\n context.on('enter.intro', function() {\n if (isTownHallSelected()) continueTo(selectedTownHall);\n });\n\n }, 550); // after centerZoomEase\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function selectedTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n var entity = context.hasEntity(hallId);\n if (!entity) return clickTownHall();\n\n var box = pointBox(entity.loc, context);\n var onClick = function() { continueTo(editorTownHall); };\n\n reveal(box, helpHtml('intro.navigation.selected_townhall'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.navigation.selected_townhall'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function editorTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n var onClick = function() { continueTo(presetTownHall); };\n\n reveal('.entity-editor-pane',\n helpHtml('intro.navigation.editor_townhall'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.on('exit.intro', function() {\n continueTo(clickTownHall);\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n nextStep();\n }\n }\n\n\n function presetTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n // preset match, in case the user happened to change it.\n var entity = context.entity(context.selectedIDs()[0]);\n var preset = presetManager.match(entity, context.graph());\n\n var onClick = function() { continueTo(fieldsTownHall); };\n\n reveal('.entity-editor-pane .section-feature-type',\n helpHtml('intro.navigation.preset_townhall', { preset: preset.name() }),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.on('exit.intro', function() {\n continueTo(clickTownHall);\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n nextStep();\n }\n }\n\n\n function fieldsTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n var onClick = function() { continueTo(closeTownHall); };\n\n reveal('.entity-editor-pane .section-preset-fields',\n helpHtml('intro.navigation.fields_townhall'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.on('exit.intro', function() {\n continueTo(clickTownHall);\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n nextStep();\n }\n }\n\n\n function closeTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane',\n helpHtml('intro.navigation.close_townhall', { button: { html: icon(href, 'inline') } })\n );\n\n context.on('exit.intro', function() {\n continueTo(searchStreet);\n });\n\n context.history().on('change.intro', function() {\n // update the close icon in the tooltip if the user edits something.\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane',\n helpHtml('intro.navigation.close_townhall', { button: { html: icon(href, 'inline') } }),\n { duration: 0 }\n );\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function searchStreet() {\n context.enter(modeBrowse(context));\n context.history().reset('initial'); // ensure spring street exists\n\n var msec = transitionTime(springStreet, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(springStreet, 19, msec); // ..and user can see it\n\n timeout(function() {\n reveal('.search-header input',\n helpHtml('intro.navigation.search_street', { name: t('intro.graph.name.spring-street') })\n );\n\n context.container().select('.search-header input')\n .on('keyup.intro', checkSearchResult);\n }, msec + 100);\n }\n\n\n function checkSearchResult() {\n var first = context.container().select('.feature-list-item:nth-child(0n+2)'); // skip \"No Results\" item\n var firstName = first.select('.entity-name');\n var name = t('intro.graph.name.spring-street');\n\n if (!firstName.empty() && firstName.html() === name) {\n reveal(first.node(),\n helpHtml('intro.navigation.choose_street', { name: name }),\n { duration: 300 }\n );\n\n context.on('exit.intro', function() {\n continueTo(selectedStreet);\n });\n\n context.container().select('.search-header input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n }\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.container().select('.search-header input')\n .on('keydown.intro', null)\n .on('keyup.intro', null);\n nextStep();\n }\n }\n\n\n function selectedStreet() {\n if (!context.hasEntity(springStreetEndId) || !context.hasEntity(springStreetId)) {\n return searchStreet();\n }\n\n var onClick = function() { continueTo(editorStreet); };\n var entity = context.entity(springStreetEndId);\n var box = pointBox(entity.loc, context);\n box.height = 500;\n\n reveal(box,\n helpHtml('intro.navigation.selected_street', { name: t('intro.graph.name.spring-street') }),\n { duration: 600, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(springStreetEndId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n box.height = 500;\n reveal(box,\n helpHtml('intro.navigation.selected_street', { name: t('intro.graph.name.spring-street') }),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n }, 600); // after reveal.\n\n context.on('enter.intro', function(mode) {\n if (!context.hasEntity(springStreetId)) {\n return continueTo(searchStreet);\n }\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== springStreetId) {\n // keep Spring Street selected..\n context.enter(modeSelect(context, [springStreetId]));\n }\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(springStreetEndId) || !context.hasEntity(springStreetId)) {\n timeout(function() {\n continueTo(searchStreet);\n }, 300); // after any transition (e.g. if user deleted intersection)\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function editorStreet() {\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane', helpHtml('intro.navigation.street_different_fields') + '{br}' +\n helpHtml('intro.navigation.editor_street', {\n button: { html: icon(href, 'inline') },\n field1: onewayField.title(),\n field2: maxspeedField.title()\n }));\n\n context.on('exit.intro', function() {\n continueTo(play);\n });\n\n context.history().on('change.intro', function() {\n // update the close icon in the tooltip if the user edits something.\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane', helpHtml('intro.navigation.street_different_fields') + '{br}' +\n helpHtml('intro.navigation.editor_street', {\n button: { html: icon(href, 'inline') },\n field1: onewayField.title(),\n field2: maxspeedField.title()\n }), { duration: 0 }\n );\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.navigation.play', { next: t('intro.points.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-point',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n dragMap();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.search-header input').on('keydown.intro keyup.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { actionChangePreset } from '../../actions/change_preset';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pointBox, pad, selectMenuItem, transitionTime } from './helper';\n\n\nexport function uiIntroPoint(context, reveal) {\n var dispatch = d3_dispatch('done');\n var timeouts = [];\n var intersection = [-85.63279, 41.94394];\n var building = [-85.632422, 41.944045];\n var cafePreset = presetManager.item('amenity/cafe');\n var _pointID = null;\n\n\n var chapter = {\n title: 'intro.points.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function addPoint() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var msec = transitionTime(intersection, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(intersection, 19, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-point',\n helpHtml('intro.points.points_info') + '{br}' + helpHtml('intro.points.add_point'));\n\n _pointID = null;\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-points');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-point') return;\n continueTo(placePoint);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function placePoint() {\n if (context.mode().id !== 'add-point') {\n return chapter.restart();\n }\n\n var pointBox = pad(building, 150, context);\n var textId = context.lastPointerType() === 'mouse' ? 'place_point' : 'place_point_touch';\n reveal(pointBox, helpHtml('intro.points.' + textId));\n\n context.map().on('move.intro drawn.intro', function() {\n pointBox = pad(building, 150, context);\n reveal(pointBox, helpHtml('intro.points.' + textId), { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return chapter.restart();\n _pointID = context.mode().selectedIDs()[0];\n\n if (context.graph().geometry(_pointID) === 'vertex'){\n\n //disallow all\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n\n reveal(pointBox, helpHtml('intro.points.place_point_error'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { return chapter.restart(); }\n });\n } else {\n continueTo(searchPreset);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function searchPreset() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return addPoint();\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.points.search_cafe', { preset: cafePreset.name() })\n );\n\n context.on('enter.intro', function(mode) {\n if (!_pointID || !context.hasEntity(_pointID)) {\n return continueTo(addPoint);\n }\n\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _pointID) {\n // keep the user's point selected..\n context.enter(modeSelect(context, [_pointID]));\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.points.search_cafe', { preset: cafePreset.name() })\n );\n\n context.history().on('change.intro', null);\n }\n });\n\n\n function checkPresetSearch() {\n var first = context.container().select('.preset-list-item:first-child');\n\n if (first.classed('preset-amenity-cafe')) {\n context.container().select('.preset-search-input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n\n reveal(first.select('.preset-list-button').node(),\n helpHtml('intro.points.choose_cafe', { preset: cafePreset.name() }),\n { duration: 300 }\n );\n\n context.history().on('change.intro', function() {\n continueTo(aboutFeatureEditor);\n });\n }\n }\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n nextStep();\n }\n }\n\n\n function aboutFeatureEditor() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return addPoint();\n }\n\n timeout(function() {\n reveal('.entity-editor-pane', helpHtml('intro.points.feature_editor'), {\n tooltipClass: 'intro-points-describe',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(addName); }\n });\n }, 400);\n\n context.on('exit.intro', function() {\n // if user leaves select mode here, just continue with the tutorial.\n continueTo(reselectPoint);\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function addName() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return addPoint();\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n var addNameString = helpHtml('intro.points.fields_info') + '{br}' + helpHtml('intro.points.add_name') + '{br}' + helpHtml('intro.points.add_reminder');\n\n timeout(function() {\n // It's possible for the user to add a name in a previous step..\n // If so, don't tell them to add the name in this step.\n // Give them an OK button instead.\n var entity = context.entity(_pointID);\n if (entity.tags.name) {\n var tooltip = reveal('.entity-editor-pane', addNameString, {\n tooltipClass: 'intro-points-describe',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(addCloseEditor); }\n });\n tooltip.select('.instruction').style('display', 'none');\n\n } else {\n reveal('.entity-editor-pane', addNameString,\n { tooltipClass: 'intro-points-describe' }\n );\n }\n }, 400);\n\n context.history().on('change.intro', function() {\n continueTo(addCloseEditor);\n });\n\n context.on('exit.intro', function() {\n // if user leaves select mode here, just continue with the tutorial.\n continueTo(reselectPoint);\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function addCloseEditor() {\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n context.on('exit.intro', function() {\n continueTo(reselectPoint);\n });\n\n reveal('.entity-editor-pane',\n helpHtml('intro.points.add_close', { button: { html: icon(href, 'inline') } })\n );\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function reselectPoint() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n\n // make sure it's still a cafe, in case user somehow changed it..\n var oldPreset = presetManager.match(entity, context.graph());\n context.replace(actionChangePreset(_pointID, oldPreset, cafePreset));\n\n context.enter(modeBrowse(context));\n\n var msec = transitionTime(entity.loc, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerEase(entity.loc, msec);\n\n timeout(function() {\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.points.reselect'), { duration: 600 });\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.points.reselect'), { duration: 0 });\n });\n }, 600); // after reveal..\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n continueTo(updatePoint);\n });\n\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function updatePoint() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return continueTo(reselectPoint);\n }\n\n // reset pane, in case user happened to untag the point..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n context.on('exit.intro', function() {\n continueTo(reselectPoint);\n });\n\n context.history().on('change.intro', function() {\n continueTo(updateCloseEditor);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane', helpHtml('intro.points.update'),\n { tooltipClass: 'intro-points-describe' }\n );\n }, 400);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function updateCloseEditor() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return continueTo(reselectPoint);\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n context.on('exit.intro', function() {\n continueTo(rightClickPoint);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.points.update_close', { button: { html: icon('#iD-icon-close', 'inline') } })\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickPoint() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n\n context.enter(modeBrowse(context));\n\n var box = pointBox(entity.loc, context);\n var textId = context.lastPointerType() === 'mouse' ? 'rightclick' : 'edit_menu_touch';\n reveal(box, helpHtml('intro.points.' + textId), { duration: 600 });\n\n timeout(function() {\n context.map().on('move.intro', function() {\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.points.' + textId), { duration: 0 });\n });\n }, 600); // after reveal\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== _pointID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'delete').node();\n if (!node) return;\n continueTo(enterDelete);\n }, 50); // after menu visible\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro', null);\n nextStep();\n }\n }\n\n\n function enterDelete() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n\n var node = selectMenuItem(context, 'delete').node();\n if (!node) { return continueTo(rightClickPoint); }\n\n reveal('.edit-menu',\n helpHtml('intro.points.delete'),\n { padding: 50 }\n );\n\n timeout(function() {\n context.map().on('move.intro', function() {\n if (selectMenuItem(context, 'delete').empty()) {\n return continueTo(rightClickPoint);\n }\n reveal('.edit-menu',\n helpHtml('intro.points.delete'),\n { duration: 0, padding: 50 }\n );\n });\n }, 300); // after menu visible\n\n context.on('exit.intro', function() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (entity) return continueTo(rightClickPoint); // point still exists\n });\n\n context.history().on('change.intro', function(changed) {\n if (changed.deleted().length) {\n continueTo(undo);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro', null);\n context.history().on('change.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function undo() {\n context.history().on('change.intro', function() {\n continueTo(play);\n });\n\n reveal('.top-toolbar button.undo-button',\n helpHtml('intro.points.undo')\n );\n\n function continueTo(nextStep) {\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.points.play', { next: t('intro.areas.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-area',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addPoint();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n interpolateNumber as d3_interpolateNumber\n} from 'd3-interpolate';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pad, transitionTime } from './helper';\n\n\nexport function uiIntroArea(context, reveal) {\n var dispatch = d3_dispatch('done');\n var playground = [-85.63552, 41.94159];\n var playgroundPreset = presetManager.item('leisure/playground');\n var nameField = presetManager.field('name');\n var descriptionField = presetManager.field('description');\n var timeouts = [];\n var _areaID;\n\n\n var chapter = {\n title: 'intro.areas.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function revealPlayground(center, text, options) {\n var padding = 180 * Math.pow(2, context.map().zoom() - 19.5);\n var box = pad(center, padding, context);\n reveal(box, text, options);\n }\n\n\n function addArea() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n _areaID = null;\n\n var msec = transitionTime(playground, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(playground, 19, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-area',\n helpHtml('intro.areas.add_playground'));\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-areas');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-area') return;\n continueTo(startPlayground);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startPlayground() {\n if (context.mode().id !== 'add-area') {\n return chapter.restart();\n }\n\n _areaID = null;\n context.map().zoomEase(19.5, 500);\n\n timeout(function() {\n var textId = context.lastPointerType() === 'mouse' ? 'starting_node_click' : 'starting_node_tap';\n var startDrawString = helpHtml('intro.areas.start_playground') + helpHtml('intro.areas.' + textId);\n revealPlayground(playground,\n startDrawString, { duration: 250 }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n revealPlayground(playground,\n startDrawString, { duration: 0 }\n );\n });\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-area') return chapter.restart();\n continueTo(continuePlayground);\n });\n }, 250); // after reveal\n\n }, 550); // after easing\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continuePlayground() {\n if (context.mode().id !== 'draw-area') {\n return chapter.restart();\n }\n\n _areaID = null;\n revealPlayground(playground,\n helpHtml('intro.areas.continue_playground'),\n { duration: 250 }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n revealPlayground(playground,\n helpHtml('intro.areas.continue_playground'),\n { duration: 0 }\n );\n });\n }, 250); // after reveal\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n var entity = context.hasEntity(context.selectedIDs()[0]);\n if (entity && entity.nodes.length >= 6) {\n return continueTo(finishPlayground);\n } else {\n return;\n }\n } else if (mode.id === 'select') {\n _areaID = context.selectedIDs()[0];\n return continueTo(searchPresets);\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function finishPlayground() {\n if (context.mode().id !== 'draw-area') {\n return chapter.restart();\n }\n\n _areaID = null;\n\n var finishString = helpHtml('intro.areas.finish_area_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.areas.finish_playground');\n revealPlayground(playground,\n finishString, { duration: 250 }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n revealPlayground(playground,\n finishString, { duration: 0 }\n );\n });\n }, 250); // after reveal\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n return;\n } else if (mode.id === 'select') {\n _areaID = context.selectedIDs()[0];\n return continueTo(searchPresets);\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function searchPresets() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n context.enter(modeSelect(context, [_areaID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.areas.search_playground', { preset: playgroundPreset.name() })\n );\n }, 400); // after preset list pane visible..\n\n context.on('enter.intro', function(mode) {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return continueTo(addArea);\n }\n\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _areaID) {\n // keep the user's area selected..\n context.enter(modeSelect(context, [_areaID]));\n\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.areas.search_playground', { preset: playgroundPreset.name() })\n );\n\n context.history().on('change.intro', null);\n }\n });\n\n function checkPresetSearch() {\n var first = context.container().select('.preset-list-item:first-child');\n\n if (first.classed('preset-leisure-playground')) {\n reveal(first.select('.preset-list-button').node(),\n helpHtml('intro.areas.choose_playground', { preset: playgroundPreset.name() }),\n { duration: 300 }\n );\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n\n context.history().on('change.intro', function() {\n continueTo(clickAddField);\n });\n }\n }\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n nextStep();\n }\n }\n\n\n function clickAddField() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n if (!context.container().select('.form-field-description').empty()) {\n return continueTo(describePlayground);\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n // It's possible for the user to add a description in a previous step..\n // If they did this already, just continue to next step.\n var entity = context.entity(_areaID);\n if (entity.tags.description) {\n return continueTo(play);\n }\n\n // scroll \"Add field\" into view\n var box = context.container().select('.more-fields').node().getBoundingClientRect();\n if (box.top > 300) {\n var pane = context.container().select('.entity-editor-pane .inspector-body');\n var start = pane.node().scrollTop;\n var end = start + (box.top - 300);\n\n pane\n .transition()\n .duration(250)\n .tween('scroll.inspector', function() {\n var node = this;\n var i = d3_interpolateNumber(start, end);\n return function(t) {\n node.scrollTop = i(t);\n };\n });\n }\n\n timeout(function() {\n reveal('.more-fields .combobox-input',\n helpHtml('intro.areas.add_field', {\n name: nameField.title(),\n description: descriptionField.title()\n }),\n { duration: 300 }\n );\n\n context.container().select('.more-fields .combobox-input')\n .on('click.intro', function() {\n // Watch for the combobox to appear...\n var watcher;\n watcher = window.setInterval(function() {\n if (!context.container().select('div.combobox').empty()) {\n window.clearInterval(watcher);\n continueTo(chooseDescriptionField);\n }\n }, 300);\n });\n }, 300); // after \"Add Field\" visible\n\n }, 400); // after editor pane visible\n\n context.on('exit.intro', function() {\n return continueTo(searchPresets);\n });\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.more-fields .combobox-input').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function chooseDescriptionField() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n if (!context.container().select('.form-field-description').empty()) {\n return continueTo(describePlayground);\n }\n\n // Make sure combobox is ready..\n if (context.container().select('div.combobox').empty()) {\n return continueTo(clickAddField);\n }\n // Watch for the combobox to go away..\n var watcher;\n watcher = window.setInterval(function() {\n if (context.container().select('div.combobox').empty()) {\n window.clearInterval(watcher);\n timeout(function() {\n if (context.container().select('.form-field-description').empty()) {\n continueTo(retryChooseDescription);\n } else {\n continueTo(describePlayground);\n }\n }, 300); // after description field added.\n }\n }, 300);\n\n reveal('div.combobox',\n helpHtml('intro.areas.choose_field', { field: descriptionField.title() }),\n { duration: 300 }\n );\n\n context.on('exit.intro', function() {\n return continueTo(searchPresets);\n });\n\n function continueTo(nextStep) {\n if (watcher) window.clearInterval(watcher);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function describePlayground() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n if (context.container().select('.form-field-description').empty()) {\n return continueTo(retryChooseDescription);\n }\n\n context.on('exit.intro', function() {\n continueTo(play);\n });\n\n reveal('.entity-editor-pane',\n helpHtml('intro.areas.describe_playground', { button: { html: icon('#iD-icon-close', 'inline') } }),\n { duration: 300 }\n );\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function retryChooseDescription() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n reveal('.entity-editor-pane',\n helpHtml('intro.areas.retry_add_field', { field: descriptionField.title() }), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(clickAddField); }\n });\n\n context.on('exit.intro', function() {\n return continueTo(searchPresets);\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.areas.play', { next: t('intro.lines.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-line',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addArea();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n context.container().select('.more-fields .combobox-input').on('click.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { geoSphericalDistance } from '../../geo';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pad, selectMenuItem, transitionTime } from './helper';\n\n\nexport function uiIntroLine(context, reveal) {\n var dispatch = d3_dispatch('done');\n var timeouts = [];\n var _tulipRoadID = null;\n var flowerRoadID = 'w646';\n var tulipRoadStart = [-85.6297754121684, 41.95805253325314];\n var tulipRoadMidpoint = [-85.62975395449628, 41.95787501510204];\n var tulipRoadIntersection = [-85.62974496187628, 41.95742515554585];\n var roadCategory = presetManager.item('category-road_minor');\n var residentialPreset = presetManager.item('highway/residential');\n var woodRoadID = 'w525';\n var woodRoadEndID = 'n2862';\n var woodRoadAddNode = [-85.62390110349587, 41.95397111462291];\n var woodRoadDragEndpoint = [-85.623867390213, 41.95466987786487];\n var woodRoadDragMidpoint = [-85.62386254803509, 41.95430395953872];\n var washingtonStreetID = 'w522';\n var twelfthAvenueID = 'w1';\n var eleventhAvenueEndID = 'n3550';\n var twelfthAvenueEndID = 'n5';\n var _washingtonSegmentID = null;\n var eleventhAvenueEnd = context.entity(eleventhAvenueEndID).loc;\n var twelfthAvenueEnd = context.entity(twelfthAvenueEndID).loc;\n var deleteLinesLoc = [-85.6219395542764, 41.95228033922477];\n var twelfthAvenue = [-85.62219310052491, 41.952505413152956];\n\n\n var chapter = {\n title: 'intro.lines.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function addLine() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var msec = transitionTime(tulipRoadStart, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(tulipRoadStart, 18.5, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-line',\n helpHtml('intro.lines.add_line'));\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-lines');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-line') return;\n continueTo(startLine);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startLine() {\n if (context.mode().id !== 'add-line') return chapter.restart();\n\n _tulipRoadID = null;\n\n var padding = 70 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(tulipRoadStart, padding, context);\n box.height = box.height + 100;\n\n var textId = context.lastPointerType() === 'mouse' ? 'start_line' : 'start_line_tap';\n var startLineString = helpHtml('intro.lines.missing_road') + '{br}' +\n helpHtml('intro.lines.line_draw_info') +\n helpHtml('intro.lines.' + textId);\n reveal(box, startLineString);\n\n context.map().on('move.intro drawn.intro', function() {\n padding = 70 * Math.pow(2, context.map().zoom() - 18);\n box = pad(tulipRoadStart, padding, context);\n box.height = box.height + 100;\n reveal(box, startLineString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-line') return chapter.restart();\n continueTo(drawLine);\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function drawLine() {\n if (context.mode().id !== 'draw-line') return chapter.restart();\n\n _tulipRoadID = context.mode().selectedIDs()[0];\n context.map().centerEase(tulipRoadMidpoint, 500);\n\n timeout(function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18.5);\n var box = pad(tulipRoadMidpoint, padding, context);\n box.height = box.height * 2;\n reveal(box,\n helpHtml('intro.lines.intersect', { name: t('intro.graph.name.flower-street') })\n );\n\n context.map().on('move.intro drawn.intro', function() {\n padding = 200 * Math.pow(2, context.map().zoom() - 18.5);\n box = pad(tulipRoadMidpoint, padding, context);\n box.height = box.height * 2;\n reveal(box,\n helpHtml('intro.lines.intersect', { name: t('intro.graph.name.flower-street') }),\n { duration: 0 }\n );\n });\n }, 550); // after easing..\n\n context.history().on('change.intro', function() {\n if (isLineConnected()) {\n continueTo(continueLine);\n }\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-line') {\n return;\n } else if (mode.id === 'select') {\n continueTo(retryIntersect);\n return;\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function isLineConnected() {\n var entity = _tulipRoadID && context.hasEntity(_tulipRoadID);\n if (!entity) return false;\n\n var drawNodes = context.graph().childNodes(entity);\n return drawNodes.some(function(node) {\n return context.graph().parentWays(node).some(function(parent) {\n return parent.id === flowerRoadID;\n });\n });\n }\n\n\n function retryIntersect() {\n d3_select(window).on('pointerdown.intro mousedown.intro', eventCancel, true);\n\n var box = pad(tulipRoadIntersection, 80, context);\n reveal(box,\n helpHtml('intro.lines.retry_intersect', { name: t('intro.graph.name.flower-street') })\n );\n\n timeout(chapter.restart, 3000);\n }\n\n\n function continueLine() {\n if (context.mode().id !== 'draw-line') return chapter.restart();\n var entity = _tulipRoadID && context.hasEntity(_tulipRoadID);\n if (!entity) return chapter.restart();\n\n context.map().centerEase(tulipRoadIntersection, 500);\n\n var continueLineText = helpHtml('intro.lines.continue_line') + '{br}' +\n helpHtml('intro.lines.finish_line_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.lines.finish_road');\n\n reveal('.main-map .surface', continueLineText);\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-line') {\n return;\n } else if (mode.id === 'select') {\n return continueTo(chooseCategoryRoad);\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function chooseCategoryRoad() {\n if (context.mode().id !== 'select') return chapter.restart();\n\n context.on('exit.intro', function() {\n return chapter.restart();\n });\n\n var button = context.container().select('.preset-category-road_minor .preset-list-button');\n if (button.empty()) return chapter.restart();\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n reveal(button.node(),\n helpHtml('intro.lines.choose_category_road', { category: roadCategory.name() })\n );\n\n button.on('click.intro', function() {\n continueTo(choosePresetResidential);\n });\n\n }, 400); // after editor pane visible\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function choosePresetResidential() {\n if (context.mode().id !== 'select') return chapter.restart();\n\n context.on('exit.intro', function() {\n return chapter.restart();\n });\n\n var subgrid = context.container().select('.preset-category-road_minor .subgrid');\n if (subgrid.empty()) return chapter.restart();\n\n subgrid.selectAll(':not(.preset-highway-residential) .preset-list-button')\n .on('click.intro', function() {\n continueTo(retryPresetResidential);\n });\n\n subgrid.selectAll('.preset-highway-residential .preset-list-button')\n .on('click.intro', function() {\n continueTo(nameRoad);\n });\n\n timeout(function() {\n reveal(subgrid.node(),\n helpHtml('intro.lines.choose_preset_residential', { preset: residentialPreset.name() }),\n { tooltipBox: '.preset-highway-residential .preset-list-button', duration: 300 }\n );\n }, 300);\n\n function continueTo(nextStep) {\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n // selected wrong road type\n function retryPresetResidential() {\n if (context.mode().id !== 'select') return chapter.restart();\n\n context.on('exit.intro', function() {\n return chapter.restart();\n });\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n var button = context.container().select('.entity-editor-pane .preset-list-button');\n\n reveal(button.node(),\n helpHtml('intro.lines.retry_preset_residential', { preset: residentialPreset.name() })\n );\n\n button.on('click.intro', function() {\n continueTo(chooseCategoryRoad);\n });\n\n }, 500);\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function nameRoad() {\n context.on('exit.intro', function() {\n continueTo(didNameRoad);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.lines.name_road', { button: { html: icon('#iD-icon-close', 'inline') } }),\n { tooltipClass: 'intro-lines-name_road' }\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function didNameRoad() {\n context.history().checkpoint('doneAddLine');\n\n timeout(function() {\n reveal('.main-map .surface', helpHtml('intro.lines.did_name_road'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(updateLine); }\n });\n }, 500);\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function updateLine() {\n context.history().reset('doneAddLine');\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return chapter.restart();\n }\n\n var msec = transitionTime(woodRoadDragMidpoint, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(woodRoadDragMidpoint, 19, msec);\n\n timeout(function() {\n var padding = 250 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n var advance = function() { continueTo(addNode); };\n\n reveal(box, helpHtml('intro.lines.update_line'),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 250 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n reveal(box, helpHtml('intro.lines.update_line'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function addNode() {\n context.history().reset('doneAddLine');\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return chapter.restart();\n }\n\n var padding = 40 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadAddNode, padding, context);\n var addNodeString = helpHtml('intro.lines.add_node' + (context.lastPointerType() === 'mouse' ? '' : '_touch'));\n reveal(box, addNodeString);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 40 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadAddNode, padding, context);\n reveal(box, addNodeString, { duration: 0 });\n });\n\n context.history().on('change.intro', function(changed) {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n if (changed.created().length === 1) {\n timeout(function() { continueTo(startDragEndpoint); }, 500);\n }\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') {\n continueTo(updateLine);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startDragEndpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n var startDragString = helpHtml('intro.lines.start_drag_endpoint' + (context.lastPointerType() === 'mouse' ? '' : '_touch')) +\n helpHtml('intro.lines.drag_to_intersection');\n reveal(box, startDragString);\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n reveal(box, startDragString, { duration: 0 });\n\n var entity = context.entity(woodRoadEndID);\n if (geoSphericalDistance(entity.loc, woodRoadDragEndpoint) <= 4) {\n continueTo(finishDragEndpoint);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function finishDragEndpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n var finishDragString = helpHtml('intro.lines.spot_looks_good') +\n helpHtml('intro.lines.finish_drag_endpoint' + (context.lastPointerType() === 'mouse' ? '' : '_touch'));\n reveal(box, finishDragString);\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n reveal(box, finishDragString, { duration: 0 });\n\n var entity = context.entity(woodRoadEndID);\n if (geoSphericalDistance(entity.loc, woodRoadDragEndpoint) > 4) {\n continueTo(startDragEndpoint);\n }\n });\n\n context.on('enter.intro', function() {\n continueTo(startDragMidpoint);\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startDragMidpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n if (context.selectedIDs().indexOf(woodRoadID) === -1) {\n context.enter(modeSelect(context, [woodRoadID]));\n }\n\n var padding = 80 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n reveal(box, helpHtml('intro.lines.start_drag_midpoint'));\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 80 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n reveal(box, helpHtml('intro.lines.start_drag_midpoint'), { duration: 0 });\n });\n\n context.history().on('change.intro', function(changed) {\n if (changed.created().length === 1) {\n continueTo(continueDragMidpoint);\n }\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') {\n // keep Wood Road selected so midpoint triangles are drawn..\n context.enter(modeSelect(context, [woodRoadID]));\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continueDragMidpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n box.height += 400;\n\n var advance = function() {\n context.history().checkpoint('doneUpdateLine');\n continueTo(deleteLines);\n };\n\n reveal(box, helpHtml('intro.lines.continue_drag_midpoint'),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n box.height += 400;\n reveal(box, helpHtml('intro.lines.continue_drag_midpoint'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function deleteLines() {\n context.history().reset('doneUpdateLine');\n context.enter(modeBrowse(context));\n\n if (!context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return chapter.restart();\n }\n\n var msec = transitionTime(deleteLinesLoc, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(deleteLinesLoc, 18, msec);\n\n timeout(function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(deleteLinesLoc, padding, context);\n box.top -= 200;\n box.height += 400;\n var advance = function() { continueTo(rightClickIntersection); };\n\n reveal(box, helpHtml('intro.lines.delete_lines', { street: t('intro.graph.name.12th-avenue') }),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(deleteLinesLoc, padding, context);\n box.top -= 200;\n box.height += 400;\n reveal(box, helpHtml('intro.lines.delete_lines', { street: t('intro.graph.name.12th-avenue') }),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n\n context.history().on('change.intro', function() {\n timeout(function() {\n continueTo(deleteLines);\n }, 500); // after any transition (e.g. if user deleted intersection)\n });\n\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickIntersection() {\n context.history().reset('doneUpdateLine');\n context.enter(modeBrowse(context));\n\n context.map().centerZoomEase(eleventhAvenueEnd, 18, 500);\n\n var rightClickString = helpHtml('intro.lines.split_street', {\n street1: t('intro.graph.name.11th-avenue'),\n street2: t('intro.graph.name.washington-street')\n }) +\n helpHtml('intro.lines.' + (context.lastPointerType() === 'mouse' ? 'rightclick_intersection' : 'edit_menu_intersection_touch'));\n\n timeout(function() {\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, rightClickString);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, rightClickString,\n { duration: 0 }\n );\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== eleventhAvenueEndID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'split').node();\n if (!node) return;\n continueTo(splitIntersection);\n }, 50); // after menu visible\n });\n\n context.history().on('change.intro', function() {\n timeout(function() {\n continueTo(deleteLines);\n }, 300); // after any transition (e.g. if user deleted intersection)\n });\n\n }, 600);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function splitIntersection() {\n if (!context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(deleteLines);\n }\n\n var node = selectMenuItem(context, 'split').node();\n if (!node) { return continueTo(rightClickIntersection); }\n\n var wasChanged = false;\n _washingtonSegmentID = null;\n\n reveal('.edit-menu', helpHtml('intro.lines.split_intersection',\n { street: t('intro.graph.name.washington-street') }),\n { padding: 50 }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var node = selectMenuItem(context, 'split').node();\n if (!wasChanged && !node) { return continueTo(rightClickIntersection); }\n\n reveal('.edit-menu', helpHtml('intro.lines.split_intersection',\n { street: t('intro.graph.name.washington-street') }),\n { duration: 0, padding: 50 }\n );\n });\n\n context.history().on('change.intro', function(changed) {\n wasChanged = true;\n timeout(function() {\n if (context.history().undoAnnotation() === t('operations.split.annotation.line', { n: 1 })) {\n _washingtonSegmentID = changed.created()[0].id;\n continueTo(didSplit);\n } else {\n _washingtonSegmentID = null;\n continueTo(retrySplit);\n }\n }, 300); // after any transition (e.g. if user deleted intersection)\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retrySplit() {\n context.enter(modeBrowse(context));\n context.map().centerZoomEase(eleventhAvenueEnd, 18, 500);\n var advance = function() { continueTo(rightClickIntersection); };\n\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, helpHtml('intro.lines.retry_split'),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, helpHtml('intro.lines.retry_split'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function didSplit() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var ids = context.selectedIDs();\n var string = 'intro.lines.did_split_' + (ids.length > 1 ? 'multi' : 'single');\n var street = t('intro.graph.name.washington-street');\n\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n box.width = box.width / 2;\n reveal(box, helpHtml(string, { street1: street, street2: street }),\n { duration: 500 }\n );\n\n timeout(function() {\n context.map().centerZoomEase(twelfthAvenue, 18, 500);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n box.width = box.width / 2;\n reveal(box, helpHtml(string, { street1: street, street2: street }),\n { duration: 0 }\n );\n });\n }, 600); // after initial reveal and curtain cut\n\n context.on('enter.intro', function() {\n var ids = context.selectedIDs();\n if (ids.length === 1 && ids[0] === _washingtonSegmentID) {\n continueTo(multiSelect);\n }\n });\n\n context.history().on('change.intro', function() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function multiSelect() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var ids = context.selectedIDs();\n var hasWashington = ids.indexOf(_washingtonSegmentID) !== -1;\n var hasTwelfth = ids.indexOf(twelfthAvenueID) !== -1;\n\n if (hasWashington && hasTwelfth) {\n return continueTo(multiRightClick);\n } else if (!hasWashington && !hasTwelfth) {\n return continueTo(didSplit);\n }\n\n context.map().centerZoomEase(twelfthAvenue, 18, 500);\n\n timeout(function() {\n var selected, other, padding, box;\n if (hasWashington) {\n selected = t('intro.graph.name.washington-street');\n other = t('intro.graph.name.12th-avenue');\n padding = 60 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenueEnd, padding, context);\n box.width *= 3;\n } else {\n selected = t('intro.graph.name.12th-avenue');\n other = t('intro.graph.name.washington-street');\n padding = 200 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenue, padding, context);\n box.width /= 2;\n }\n\n reveal(box,\n helpHtml('intro.lines.multi_select',\n { selected: selected, other1: other }) + ' ' +\n helpHtml('intro.lines.add_to_selection_' + (context.lastPointerType() === 'mouse' ? 'click' : 'touch'),\n { selected: selected, other2: other })\n );\n\n context.map().on('move.intro drawn.intro', function() {\n if (hasWashington) {\n selected = t('intro.graph.name.washington-street');\n other = t('intro.graph.name.12th-avenue');\n padding = 60 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenueEnd, padding, context);\n box.width *= 3;\n } else {\n selected = t('intro.graph.name.12th-avenue');\n other = t('intro.graph.name.washington-street');\n padding = 200 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenue, padding, context);\n box.width /= 2;\n }\n\n reveal(box,\n helpHtml('intro.lines.multi_select',\n { selected: selected, other1: other }) + ' ' +\n helpHtml('intro.lines.add_to_selection_' + (context.lastPointerType() === 'mouse' ? 'click' : 'touch'),\n { selected: selected, other2: other }),\n { duration: 0 }\n );\n });\n\n context.on('enter.intro', function() {\n continueTo(multiSelect);\n });\n\n context.history().on('change.intro', function() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n });\n }, 600);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function multiRightClick() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n\n var rightClickString = helpHtml('intro.lines.multi_select_success') +\n helpHtml('intro.lines.multi_' + (context.lastPointerType() === 'mouse' ? 'rightclick' : 'edit_menu_touch'));\n reveal(box, rightClickString);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n reveal(box, rightClickString, { duration: 0 });\n });\n\n context.ui().editMenu().on('toggled.intro', function(open) {\n if (!open) return;\n\n timeout(function() {\n var ids = context.selectedIDs();\n if (ids.length === 2 &&\n ids.indexOf(twelfthAvenueID) !== -1 &&\n ids.indexOf(_washingtonSegmentID) !== -1) {\n var node = selectMenuItem(context, 'delete').node();\n if (!node) return;\n continueTo(multiDelete);\n } else if (ids.length === 1 &&\n ids.indexOf(_washingtonSegmentID) !== -1) {\n return continueTo(multiSelect);\n } else {\n return continueTo(didSplit);\n }\n }, 300); // after edit menu visible\n });\n\n context.history().on('change.intro', function() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.ui().editMenu().on('toggled.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function multiDelete() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var node = selectMenuItem(context, 'delete').node();\n if (!node) return continueTo(multiRightClick);\n\n reveal('.edit-menu',\n helpHtml('intro.lines.multi_delete'),\n { padding: 50 }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n reveal('.edit-menu',\n helpHtml('intro.lines.multi_delete'),\n { duration: 0, padding: 50 }\n );\n });\n\n context.on('exit.intro', function() {\n if (context.hasEntity(_washingtonSegmentID) || context.hasEntity(twelfthAvenueID)) {\n return continueTo(multiSelect); // left select mode but roads still exist\n }\n });\n\n context.history().on('change.intro', function() {\n if (context.hasEntity(_washingtonSegmentID) || context.hasEntity(twelfthAvenueID)) {\n continueTo(retryDelete); // changed something but roads still exist\n } else {\n continueTo(play);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retryDelete() {\n context.enter(modeBrowse(context));\n\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n reveal(box, helpHtml('intro.lines.retry_delete'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(multiSelect); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.lines.play', { next: t('intro.buildings.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-building',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addLine();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n d3_select(window).on('pointerdown.intro mousedown.intro', null, true);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilArrayUniq, utilRebind } from '../../util';\nimport { helpHtml, icon, pad, isMostlySquare, selectMenuItem, transitionTime } from './helper';\n\n\nexport function uiIntroBuilding(context, reveal) {\n var dispatch = d3_dispatch('done');\n var house = [-85.62815, 41.95638];\n var tank = [-85.62732, 41.95347];\n var buildingCatetory = presetManager.item('category-building');\n var housePreset = presetManager.item('building/house');\n var tankPreset = presetManager.item('man_made/storage_tank');\n var timeouts = [];\n var _houseID = null;\n var _tankID = null;\n\n\n var chapter = {\n title: 'intro.buildings.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function revealHouse(center, text, options) {\n var padding = 160 * Math.pow(2, context.map().zoom() - 20);\n var box = pad(center, padding, context);\n reveal(box, text, options);\n }\n\n\n function revealTank(center, text, options) {\n var padding = 190 * Math.pow(2, context.map().zoom() - 19.5);\n var box = pad(center, padding, context);\n reveal(box, text, options);\n }\n\n\n function addHouse() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n _houseID = null;\n\n var msec = transitionTime(house, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(house, 19, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-area',\n helpHtml('intro.buildings.add_building'));\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-buildings');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-area') return;\n continueTo(startHouse);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startHouse() {\n if (context.mode().id !== 'add-area') {\n return continueTo(addHouse);\n }\n\n _houseID = null;\n context.map().zoomEase(20, 500);\n\n timeout(function() {\n var startString = helpHtml('intro.buildings.start_building') +\n helpHtml('intro.buildings.building_corner_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap'));\n revealHouse(house, startString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealHouse(house, startString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-area') return chapter.restart();\n continueTo(continueHouse);\n });\n\n }, 550); // after easing\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continueHouse() {\n if (context.mode().id !== 'draw-area') {\n return continueTo(addHouse);\n }\n\n _houseID = null;\n\n var continueString = helpHtml('intro.buildings.continue_building') + '{br}' +\n helpHtml('intro.areas.finish_area_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.buildings.finish_building');\n\n revealHouse(house, continueString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealHouse(house, continueString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n return;\n } else if (mode.id === 'select') {\n var graph = context.graph();\n var way = context.entity(context.selectedIDs()[0]);\n var nodes = graph.childNodes(way);\n var points = utilArrayUniq(nodes)\n .map(function(n) { return context.projection(n.loc); });\n\n if (isMostlySquare(points)) {\n _houseID = way.id;\n return continueTo(chooseCategoryBuilding);\n } else {\n return continueTo(retryHouse);\n }\n\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function retryHouse() {\n var onClick = function() { continueTo(addHouse); };\n\n revealHouse(house, helpHtml('intro.buildings.retry_building'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n revealHouse(house, helpHtml('intro.buildings.retry_building'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function chooseCategoryBuilding() {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return addHouse();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {\n context.enter(modeSelect(context, [_houseID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n var button = context.container().select('.preset-category-building .preset-list-button');\n\n reveal(button.node(),\n helpHtml('intro.buildings.choose_category_building', { category: buildingCatetory.name() })\n );\n\n button.on('click.intro', function() {\n button.on('click.intro', null);\n continueTo(choosePresetHouse);\n });\n\n }, 400); // after preset list pane visible..\n\n\n context.on('enter.intro', function(mode) {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return continueTo(addHouse);\n }\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _houseID) {\n return continueTo(chooseCategoryBuilding);\n }\n });\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function choosePresetHouse() {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return addHouse();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {\n context.enter(modeSelect(context, [_houseID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n var button = context.container().select('.preset-building-house .preset-list-button');\n\n reveal(button.node(),\n helpHtml('intro.buildings.choose_preset_house', { preset: housePreset.name() }),\n { duration: 300 }\n );\n\n button.on('click.intro', function() {\n button.on('click.intro', null);\n continueTo(closeEditorHouse);\n });\n\n }, 400); // after preset list pane visible..\n\n context.on('enter.intro', function(mode) {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return continueTo(addHouse);\n }\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _houseID) {\n return continueTo(chooseCategoryBuilding);\n }\n });\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function closeEditorHouse() {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return addHouse();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {\n context.enter(modeSelect(context, [_houseID]));\n }\n\n context.history().checkpoint('hasHouse');\n\n context.on('exit.intro', function() {\n continueTo(rightClickHouse);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.buildings.close', { button: { html: icon('#iD-icon-close', 'inline') } })\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickHouse() {\n if (!_houseID) return chapter.restart();\n\n context.enter(modeBrowse(context));\n context.history().reset('hasHouse');\n var zoom = context.map().zoom();\n if (zoom < 20) {\n zoom = 20;\n }\n context.map().centerZoomEase(house, zoom, 500);\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== _houseID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'orthogonalize').node();\n if (!node) return;\n continueTo(clickSquare);\n }, 50); // after menu visible\n });\n\n context.map().on('move.intro drawn.intro', function() {\n var rightclickString = helpHtml('intro.buildings.' + (context.lastPointerType() === 'mouse' ? 'rightclick_building' : 'edit_menu_building_touch'));\n revealHouse(house, rightclickString, { duration: 0 });\n });\n\n context.history().on('change.intro', function() {\n continueTo(rightClickHouse);\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function clickSquare() {\n if (!_houseID) return chapter.restart();\n var entity = context.hasEntity(_houseID);\n if (!entity) return continueTo(rightClickHouse);\n\n var node = selectMenuItem(context, 'orthogonalize').node();\n if (!node) { return continueTo(rightClickHouse); }\n\n var wasChanged = false;\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.square_building'),\n { padding: 50 }\n );\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'browse') {\n continueTo(rightClickHouse);\n } else if (mode.id === 'move' || mode.id === 'rotate') {\n continueTo(retryClickSquare);\n }\n });\n\n context.map().on('move.intro', function() {\n var node = selectMenuItem(context, 'orthogonalize').node();\n if (!wasChanged && !node) { return continueTo(rightClickHouse); }\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.square_building'),\n { duration: 0, padding: 50 }\n );\n });\n\n context.history().on('change.intro', function() {\n wasChanged = true;\n context.history().on('change.intro', null);\n\n // Something changed. Wait for transition to complete and check undo annotation.\n timeout(function() {\n if (context.history().undoAnnotation() === t('operations.orthogonalize.annotation.feature', { n: 1 })) {\n continueTo(doneSquare);\n } else {\n continueTo(retryClickSquare);\n }\n }, 500); // after transitioned actions\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retryClickSquare() {\n context.enter(modeBrowse(context));\n\n revealHouse(house, helpHtml('intro.buildings.retry_square'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(rightClickHouse); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function doneSquare() {\n context.history().checkpoint('doneSquare');\n\n revealHouse(house, helpHtml('intro.buildings.done_square'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(addTank); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function addTank() {\n context.enter(modeBrowse(context));\n context.history().reset('doneSquare');\n _tankID = null;\n\n var msec = transitionTime(tank, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(tank, 19.5, msec);\n\n timeout(function() {\n reveal('button.add-area',\n helpHtml('intro.buildings.add_tank')\n );\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-area') return;\n continueTo(startTank);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startTank() {\n if (context.mode().id !== 'add-area') {\n return continueTo(addTank);\n }\n\n _tankID = null;\n\n timeout(function() {\n var startString = helpHtml('intro.buildings.start_tank') +\n helpHtml('intro.buildings.tank_edge_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap'));\n revealTank(tank, startString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealTank(tank, startString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-area') return chapter.restart();\n continueTo(continueTank);\n });\n\n }, 550); // after easing\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continueTank() {\n if (context.mode().id !== 'draw-area') {\n return continueTo(addTank);\n }\n\n _tankID = null;\n\n var continueString = helpHtml('intro.buildings.continue_tank') + '{br}' +\n helpHtml('intro.areas.finish_area_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.buildings.finish_tank');\n\n revealTank(tank, continueString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealTank(tank, continueString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n return;\n } else if (mode.id === 'select') {\n _tankID = context.selectedIDs()[0];\n return continueTo(searchPresetTank);\n } else {\n return continueTo(addTank);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function searchPresetTank() {\n if (!_tankID || !context.hasEntity(_tankID)) {\n return addTank();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _tankID) {\n context.enter(modeSelect(context, [_tankID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.buildings.search_tank', { preset: tankPreset.name() })\n );\n }, 400); // after preset list pane visible..\n\n context.on('enter.intro', function(mode) {\n if (!_tankID || !context.hasEntity(_tankID)) {\n return continueTo(addTank);\n }\n\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _tankID) {\n // keep the user's area selected..\n context.enter(modeSelect(context, [_tankID]));\n\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.buildings.search_tank', { preset: tankPreset.name() })\n );\n\n context.history().on('change.intro', null);\n }\n });\n\n function checkPresetSearch() {\n var first = context.container().select('.preset-list-item:first-child');\n\n if (first.classed('preset-man_made-storage_tank')) {\n reveal(first.select('.preset-list-button').node(),\n helpHtml('intro.buildings.choose_tank', { preset: tankPreset.name() }),\n { duration: 300 }\n );\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n\n context.history().on('change.intro', function() {\n continueTo(closeEditorTank);\n });\n }\n }\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n nextStep();\n }\n }\n\n\n function closeEditorTank() {\n if (!_tankID || !context.hasEntity(_tankID)) {\n return addTank();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _tankID) {\n context.enter(modeSelect(context, [_tankID]));\n }\n\n context.history().checkpoint('hasTank');\n\n context.on('exit.intro', function() {\n continueTo(rightClickTank);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.buildings.close', { button: { html: icon('#iD-icon-close', 'inline') } })\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickTank() {\n if (!_tankID) return continueTo(addTank);\n\n context.enter(modeBrowse(context));\n context.history().reset('hasTank');\n context.map().centerEase(tank, 500);\n\n timeout(function() {\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== _tankID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'circularize').node();\n if (!node) return;\n continueTo(clickCircle);\n }, 50); // after menu visible\n });\n\n var rightclickString = helpHtml('intro.buildings.' + (context.lastPointerType() === 'mouse' ? 'rightclick_tank' : 'edit_menu_tank_touch'));\n\n revealTank(tank, rightclickString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealTank(tank, rightclickString, { duration: 0 });\n });\n\n context.history().on('change.intro', function() {\n continueTo(rightClickTank);\n });\n\n }, 600);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function clickCircle() {\n if (!_tankID) return chapter.restart();\n var entity = context.hasEntity(_tankID);\n if (!entity) return continueTo(rightClickTank);\n\n var node = selectMenuItem(context, 'circularize').node();\n if (!node) { return continueTo(rightClickTank); }\n\n var wasChanged = false;\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.circle_tank'),\n { padding: 50 }\n );\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'browse') {\n continueTo(rightClickTank);\n } else if (mode.id === 'move' || mode.id === 'rotate') {\n continueTo(retryClickCircle);\n }\n });\n\n context.map().on('move.intro', function() {\n var node = selectMenuItem(context, 'circularize').node();\n if (!wasChanged && !node) { return continueTo(rightClickTank); }\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.circle_tank'),\n { duration: 0, padding: 50 }\n );\n });\n\n context.history().on('change.intro', function() {\n wasChanged = true;\n context.history().on('change.intro', null);\n\n // Something changed. Wait for transition to complete and check undo annotation.\n timeout(function() {\n if (context.history().undoAnnotation() === t('operations.circularize.annotation.feature', { n: 1 })) {\n continueTo(play);\n } else {\n continueTo(retryClickCircle);\n }\n }, 500); // after transitioned actions\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retryClickCircle() {\n context.enter(modeBrowse(context));\n\n revealTank(tank, helpHtml('intro.buildings.retry_circle'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(rightClickTank); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.buildings.play', { next: t('intro.startediting.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-startEditing',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addHouse();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n context.container().select('.more-fields .combobox-input').on('click.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { helpHtml } from './helper';\nimport { uiModal } from '../modal';\nimport { utilRebind } from '../../util/rebind';\n\n\nexport function uiIntroStartEditing(context, reveal) {\n var dispatch = d3_dispatch('done', 'startEditing');\n var modalSelection = d3_select(null);\n\n\n var chapter = {\n title: 'intro.startediting.title'\n };\n\n function showHelp() {\n reveal('.map-control.help-control',\n helpHtml('intro.startediting.help'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { shortcuts(); }\n }\n );\n }\n\n function shortcuts() {\n reveal('.map-control.help-control',\n helpHtml('intro.startediting.shortcuts'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { showSave(); }\n }\n );\n }\n\n function showSave() {\n context.container().selectAll('.shaded').remove(); // in case user opened keyboard shortcuts\n reveal('.top-toolbar button.save',\n helpHtml('intro.startediting.save'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { showStart(); }\n }\n );\n }\n\n function showStart() {\n context.container().selectAll('.shaded').remove(); // in case user opened keyboard shortcuts\n\n modalSelection = uiModal(context.container());\n\n modalSelection.select('.modal')\n .attr('class', 'modal-splash modal');\n\n modalSelection.selectAll('.close').remove();\n\n var startbutton = modalSelection.select('.content')\n .attr('class', 'fillL')\n .append('button')\n .attr('class', 'modal-section huge-modal-button')\n .on('click', function() {\n modalSelection.remove();\n });\n\n startbutton\n .append('svg')\n .attr('class', 'illustration')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n startbutton\n .append('h2')\n .call(t.append('intro.startediting.start'));\n\n dispatch.call('startEditing');\n }\n\n\n chapter.enter = function() {\n showHelp();\n };\n\n\n chapter.exit = function() {\n modalSelection.remove();\n context.container().selectAll('.shaded').remove(); // in case user opened keyboard shortcuts\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { t, localizer } from '../../core/localizer';\nimport { localize } from './helper';\n\nimport { prefs } from '../../core/preferences';\nimport { fileFetcher } from '../../core/file_fetcher';\nimport { coreGraph } from '../../core/graph';\nimport { modeBrowse } from '../../modes/browse';\nimport { osmEntity } from '../../osm/entity';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCurtain } from '../curtain';\nimport { utilArrayDifference, utilArrayUniq } from '../../util';\n\nimport { uiIntroWelcome } from './welcome';\nimport { uiIntroNavigation } from './navigation';\nimport { uiIntroPoint } from './point';\nimport { uiIntroArea } from './area';\nimport { uiIntroLine } from './line';\nimport { uiIntroBuilding } from './building';\nimport { uiIntroStartEditing } from './start_editing';\n\n\nconst chapterUi = {\n welcome: uiIntroWelcome,\n navigation: uiIntroNavigation,\n point: uiIntroPoint,\n area: uiIntroArea,\n line: uiIntroLine,\n building: uiIntroBuilding,\n startEditing: uiIntroStartEditing\n};\n\nconst chapterFlow = [\n 'welcome',\n 'navigation',\n 'point',\n 'area',\n 'line',\n 'building',\n 'startEditing'\n];\n\n\nexport function uiIntro(context) {\n const INTRO_IMAGERY = 'Bing';\n let _introGraph = {};\n let _currChapter;\n\n\n function intro(selection) {\n fileFetcher.get('intro_graph')\n .then(dataIntroGraph => {\n // create entities for intro graph and localize names\n for (let id in dataIntroGraph) {\n if (!_introGraph[id]) {\n _introGraph[id] = osmEntity(localize(dataIntroGraph[id]));\n }\n }\n selection.call(startIntro);\n })\n .catch(function() { /* ignore */ });\n }\n\n\n function startIntro(selection) {\n context.enter(modeBrowse(context));\n\n // Save current map state\n let osm = context.connection();\n let history = context.history().toJSON();\n let hash = window.location.hash;\n let center = context.map().center();\n let zoom = context.map().zoom();\n let background = context.background().baseLayerSource();\n let overlays = context.background().overlayLayerSources();\n let opacity = context.container().selectAll('.main-map .layer-background').style('opacity');\n let caches = osm && osm.caches();\n let baseEntities = context.history().graph().base().entities;\n\n // Show sidebar and disable the sidebar resizing button\n // (this needs to be before `context.inIntro(true)`)\n context.ui().sidebar.expand();\n context.container().selectAll('button.sidebar-toggle').classed('disabled', true);\n\n // Block saving\n context.inIntro(true);\n\n // Load semi-real data used in intro\n if (osm) { osm.toggle(false).reset(); }\n context.history().reset();\n context.history().merge(Object.values(coreGraph().load(_introGraph).entities));\n context.history().checkpoint('initial');\n\n // Setup imagery\n let imagery = context.background().findSource(INTRO_IMAGERY);\n if (imagery) {\n context.background().baseLayerSource(imagery);\n } else {\n context.background().bing();\n }\n overlays.forEach(d => context.background().toggleOverlayLayer(d));\n\n // Setup data layers (only OSM)\n let layers = context.layers();\n layers.all().forEach(item => {\n // if the layer has the function `enabled`\n if (typeof item.layer.enabled === 'function') {\n item.layer.enabled(item.id === 'osm');\n }\n });\n\n\n context.container().selectAll('.main-map .layer-background').style('opacity', 1);\n\n let curtain = uiCurtain(context.container().node());\n selection.call(curtain);\n\n // Store that the user started the walkthrough..\n prefs('walkthrough_started', 'yes');\n\n // Restore previous walkthrough progress..\n let storedProgress = prefs('walkthrough_progress') || '';\n let progress = storedProgress.split(';').filter(Boolean);\n\n let chapters = chapterFlow.map((chapter, i) => {\n let s = chapterUi[chapter](context, curtain.reveal)\n .on('done', () => {\n\n buttons\n .filter(d => d.title === s.title)\n .classed('finished', true);\n\n if (i < chapterFlow.length - 1) {\n const next = chapterFlow[i + 1];\n context.container().select(`button.chapter-${next}`)\n .classed('next', true);\n }\n\n // Store walkthrough progress..\n progress.push(chapter);\n prefs('walkthrough_progress', utilArrayUniq(progress).join(';'));\n });\n return s;\n });\n\n chapters[chapters.length - 1].on('startEditing', () => {\n // Store walkthrough progress..\n progress.push('startEditing');\n prefs('walkthrough_progress', utilArrayUniq(progress).join(';'));\n\n // Store if walkthrough is completed..\n let incomplete = utilArrayDifference(chapterFlow, progress);\n if (!incomplete.length) {\n prefs('walkthrough_completed', 'yes');\n }\n\n curtain.remove();\n navwrap.remove();\n context.container().selectAll('.main-map .layer-background').style('opacity', opacity);\n context.container().selectAll('button.sidebar-toggle').classed('disabled', false);\n if (osm) { osm.toggle(true).reset().caches(caches); }\n context.history().reset().merge(Object.values(baseEntities));\n context.background().baseLayerSource(background);\n overlays.forEach(d => context.background().toggleOverlayLayer(d));\n if (history) { context.history().fromJSON(history, false); }\n context.map().centerZoom(center, zoom);\n window.history.replaceState(null, '', hash);\n context.inIntro(false);\n });\n\n let navwrap = selection\n .append('div')\n .attr('class', 'intro-nav-wrap fillD');\n\n navwrap\n .append('svg')\n .attr('class', 'intro-nav-wrap-logo')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n let buttonwrap = navwrap\n .append('div')\n .attr('class', 'joined')\n .selectAll('button.chapter');\n\n let buttons = buttonwrap\n .data(chapters)\n .enter()\n .append('button')\n .attr('class', (d, i) => `chapter chapter-${chapterFlow[i]}`)\n .on('click', enterChapter);\n\n buttons\n .append('span')\n .html(d => t.html(d.title));\n\n buttons\n .append('span')\n .attr('class', 'status')\n .call(svgIcon((localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'));\n\n enterChapter(null, chapters[0]);\n\n\n function enterChapter(d3_event, newChapter) {\n if (_currChapter) { _currChapter.exit(); }\n context.enter(modeBrowse(context));\n\n _currChapter = newChapter;\n _currChapter.enter();\n\n buttons\n .classed('next', false)\n .classed('active', d => d.title === _currChapter.title);\n }\n }\n\n\n return intro;\n}\n", "export { uiIntro } from './intro';\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../core/preferences';\nimport { svgIcon } from '../svg/icon';\nimport { t } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\n\n\nexport function uiIssuesInfo(context) {\n\n var warningsItem = {\n id: 'warnings',\n count: 0,\n iconID: 'iD-icon-alert',\n descriptionID: 'issues.warnings_and_errors'\n };\n\n var resolvedItem = {\n id: 'resolved',\n count: 0,\n iconID: 'iD-icon-apply',\n descriptionID: 'issues.user_resolved_issues'\n };\n\n function update(selection) {\n\n var shownItems = [];\n\n var liveIssues = context.validator().getIssues({\n what: prefs('validate-what') || 'edited',\n where: prefs('validate-where') || 'all'\n }).filter(issue => issue.severity !== 'suggestion');\n\n if (liveIssues.length) {\n warningsItem.count = liveIssues.length;\n shownItems.push(warningsItem);\n }\n\n if (prefs('validate-what') === 'all') {\n var resolvedIssues = context.validator().getResolvedIssues();\n if (resolvedIssues.length) {\n resolvedItem.count = resolvedIssues.length;\n shownItems.push(resolvedItem);\n }\n }\n\n var chips = selection.selectAll('.chip')\n .data(shownItems, function(d) {\n return d.id;\n });\n\n chips.exit().remove();\n\n var enter = chips.enter()\n .append('a')\n .attr('class', function(d) {\n return 'chip ' + d.id + '-count';\n })\n .attr('href', '#')\n .each(function(d) {\n\n var chipSelection = d3_select(this);\n\n var tooltipBehavior = uiTooltip()\n .placement('top')\n .title(() => t.append(d.descriptionID));\n\n chipSelection\n .call(tooltipBehavior)\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n\n tooltipBehavior.hide(d3_select(this));\n // open the Issues pane\n context.ui().togglePanes(context.container().select('.map-panes .issues-pane'));\n });\n\n chipSelection.call(svgIcon('#' + d.iconID));\n\n });\n\n enter.append('span')\n .attr('class', 'count');\n\n enter.merge(chips)\n .selectAll('span.count')\n .text(function(d) {\n return d.count.toString();\n });\n }\n\n\n return function(selection) {\n update(selection);\n\n context.validator().on('validated.infobox', function() {\n update(selection);\n });\n };\n}\n", "/**\n * IntervalTasksQueue\n * Enabled task execution under interval limit\n */\nexport class IntervalTasksQueue {\n readonly intervalInMs: number;\n pendingHandles: ReturnType[];\n time: number;\n\n /**\n * Interval in milliseconds inside which only 1 task can execute.\n * e.g. if interval is 200ms, and 5 async tasks are unqueued,\n * they will complete in ~1s if not cleared\n * @param {number} intervalInMs\n */\n constructor(intervalInMs: number) {\n this.intervalInMs = intervalInMs;\n this.pendingHandles = [];\n this.time = 0;\n }\n\n enqueue(task: () => void) {\n let taskTimeout = this.time;\n this.time += this.intervalInMs;\n this.pendingHandles.push(setTimeout(() => {\n this.time -= this.intervalInMs;\n task();\n }, taskTimeout));\n }\n\n clear() {\n this.pendingHandles.forEach((timeoutHandle) => {\n clearTimeout(timeoutHandle);\n });\n this.pendingHandles = [];\n this.time = 0;\n }\n}\n", "import { geoArea as d3_geoArea, geoMercatorRaw as d3_geoMercatorRaw } from 'd3-geo';\nimport { json as d3_json } from 'd3-fetch';\n\nimport { t, localizer } from '../core/localizer';\nimport { geoExtent, geoSphericalDistance } from '../geo';\nimport { utilQsString, utilStringQs } from '../util';\nimport { utilAesDecrypt } from '../util/aes';\nimport { IntervalTasksQueue } from '../util/IntervalTasksQueue';\nimport { localeDateString } from '../util/date';\n\nvar isRetina = window.devicePixelRatio && window.devicePixelRatio >= 2;\n\n// listen for DPI change, e.g. when dragging a browser window from a retina to non-retina screen\nwindow.matchMedia?.(`\n (-webkit-min-device-pixel-ratio: 2), /* Safari */\n (min-resolution: 2dppx), /* standard */\n (min-resolution: 192dpi) /* fallback */\n `).addListener(function() {\n\n isRetina = window.devicePixelRatio && window.devicePixelRatio >= 2;\n});\n\n\nfunction vintageRange(vintage) {\n var s;\n if (vintage.start || vintage.end) {\n s = (vintage.start || '?');\n if (vintage.start !== vintage.end) {\n s += ' - ' + (vintage.end || '?');\n }\n }\n return s;\n}\n\n\nexport function rendererBackgroundSource(data) {\n var source = Object.assign({}, data); // shallow copy\n var _offset = [0, 0];\n var _name = source.name;\n var _description = source.description;\n var _best = !!source.best;\n var _template = source.encrypted ? utilAesDecrypt(source.template) : source.template;\n\n source.tileSize = data.tileSize || 256;\n source.zoomExtent = data.zoomExtent || [0, 22];\n source.overzoom = data.overzoom !== false;\n\n source.offset = function(val) {\n if (!arguments.length) return _offset;\n _offset = val;\n return source;\n };\n\n\n source.nudge = function(val, zoomlevel) {\n _offset[0] += val[0] / Math.pow(2, zoomlevel);\n _offset[1] += val[1] / Math.pow(2, zoomlevel);\n return source;\n };\n\n\n source.name = function() {\n var id_safe = source.id.replace(/\\./g, '');\n return t('imagery.' + id_safe + '.name', { default: _name });\n };\n\n\n source.label = function() {\n var id_safe = source.id.replace(/\\./g, '');\n return t.append('imagery.' + id_safe + '.name', { default: _name });\n };\n\n\n source.hasDescription = function() {\n var id_safe = source.id.replace(/\\./g, '');\n var descriptionText = localizer.tInfo('imagery.' + id_safe + '.description', { default: escape(_description) }).texts.join('');\n return !!descriptionText;\n };\n\n\n source.description = function() {\n var id_safe = source.id.replace(/\\./g, '');\n return t.append('imagery.' + id_safe + '.description', { default: _description });\n };\n\n\n source.best = function() {\n return _best;\n };\n\n\n source.area = function() {\n if (!data.polygon) return Number.MAX_VALUE; // worldwide\n var area = d3_geoArea({ type: 'MultiPolygon', coordinates: [ data.polygon ] });\n return isNaN(area) ? 0 : area;\n };\n\n\n source.imageryUsed = function() {\n return _name || source.id;\n };\n\n\n source.template = function(val) {\n if (!arguments.length) return _template;\n if (source.id === 'custom' || source.id === 'Bing') {\n _template = val;\n }\n return source;\n };\n\n\n source.url = function(coord) {\n var result = _template.replace(/#[\\s\\S]*/u, ''); // strip hash part of URL\n if (result === '') return result; // source 'none'\n\n\n // Guess a type based on the tokens present in the template\n // (This is for 'custom' source, where we don't know)\n if (!source.type || source.id === 'custom') {\n if (/SERVICE=WMS|\\{(proj|wkid|bbox)\\}/.test(result)) {\n source.type = 'wms';\n source.projection = 'EPSG:3857'; // guess\n } else if (/\\{(x|y)\\}/.test(result)) {\n source.type = 'tms';\n } else if (/\\{u\\}/.test(result)) {\n source.type = 'bing';\n }\n }\n\n\n if (source.type === 'wms') {\n var tileToProjectedCoords = (function(x, y, z) {\n var zoomSize = Math.pow(2, z);\n var lon = x / zoomSize * Math.PI * 2 - Math.PI;\n var lat = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / zoomSize)));\n\n switch (source.projection) {\n case 'EPSG:4326':\n return {\n x: lon * 180 / Math.PI,\n y: lat * 180 / Math.PI\n };\n default: // EPSG:3857 and synonyms\n var mercCoords = d3_geoMercatorRaw(lon, lat);\n return {\n x: 20037508.34 / Math.PI * mercCoords[0],\n y: 20037508.34 / Math.PI * mercCoords[1]\n };\n }\n });\n\n var tileSize = source.tileSize;\n var projection = source.projection;\n var minXmaxY = tileToProjectedCoords(coord[0], coord[1], coord[2]);\n var maxXminY = tileToProjectedCoords(coord[0]+1, coord[1]+1, coord[2]);\n\n result = result.replace(/\\{(\\w+)\\}/g, function (token, key) {\n switch (key) {\n case 'width':\n case 'height':\n return tileSize;\n case 'proj':\n return projection;\n case 'wkid':\n return projection.replace(/^EPSG:/, '');\n case 'bbox':\n // WMS 1.3 flips x/y for some coordinate systems including EPSG:4326 - #7557\n if (projection === 'EPSG:4326' &&\n // The CRS parameter implies version 1.3 (prior versions use SRS)\n /VERSION=1.3|CRS={proj}/.test(source.template().toUpperCase())) {\n return maxXminY.y + ',' + minXmaxY.x + ',' + minXmaxY.y + ',' + maxXminY.x;\n } else {\n return minXmaxY.x + ',' + maxXminY.y + ',' + maxXminY.x + ',' + minXmaxY.y;\n }\n case 'w':\n return minXmaxY.x;\n case 's':\n return maxXminY.y;\n case 'n':\n return maxXminY.x;\n case 'e':\n return minXmaxY.y;\n default:\n return token;\n }\n });\n\n } else if (source.type === 'tms') {\n result = result\n .replace('{x}', coord[0])\n .replace('{y}', coord[1])\n // TMS-flipped y coordinate\n .replace(/\\{[t-]y\\}/, Math.pow(2, coord[2]) - coord[1] - 1)\n .replace(/\\{z(oom)?\\}/, coord[2])\n // only fetch retina tiles for retina screens\n .replace(/\\{@2x\\}|\\{r\\}/, isRetina ? '@2x' : '');\n\n } else if (source.type === 'bing') {\n result = result\n .replace('{u}', function() {\n var u = '';\n for (var zoom = coord[2]; zoom > 0; zoom--) {\n var b = 0;\n var mask = 1 << (zoom - 1);\n if ((coord[0] & mask) !== 0) b++;\n if ((coord[1] & mask) !== 0) b += 2;\n u += b.toString();\n }\n return u;\n });\n }\n\n // these apply to any type..\n result = result.replace(/\\{switch:([^}]+)\\}/, function(s, r) {\n var subdomains = r.split(',');\n return subdomains[(coord[0] + coord[1]) % subdomains.length];\n });\n\n\n return result;\n };\n\n\n source.validZoom = function(z, underzoom) {\n if (underzoom === undefined) underzoom = 0;\n return source.zoomExtent[0] - underzoom <= z &&\n (source.overzoom || source.zoomExtent[1] > z);\n };\n\n\n source.isLocatorOverlay = function() {\n return source.id === 'mapbox_locator_overlay';\n };\n\n\n /* hides a source from the list, but leaves it available for use */\n source.isHidden = function() {\n return false; // currently there are no hidden layers\n };\n\n\n source.copyrightNotices = function() {};\n\n\n source.getMetadata = function(center, tileCoord, callback) {\n var vintage = {\n start: localeDateString(source.startDate),\n end: localeDateString(source.endDate)\n };\n vintage.range = vintageRange(vintage);\n\n var metadata = { vintage: vintage };\n callback(null, metadata);\n };\n\n\n return source;\n}\n\n\nrendererBackgroundSource.Bing = function(data, dispatch) {\n // https://docs.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata\n // https://docs.microsoft.com/en-us/bingmaps/rest-services/directly-accessing-the-bing-maps-tiles\n\n //fallback url template\n data.template = 'https://ecn.t{switch:0,1,2,3}.tiles.virtualearth.net/tiles/a{u}.jpeg?g=1&pr=odbl&n=z';\n\n var bing = rendererBackgroundSource(data);\n var key = utilAesDecrypt('5c875730b09c6b422433e807e1ff060b6536c791dbfffcffc4c6b18a1bdba1f14593d151adb50e19e1be1ab19aef813bf135d0f103475e5c724dec94389e45d0');\n /*\n missing tile image strictness param (n=)\n \u2022\tn=f -> (Fail) returns a 404\n \u2022\tn=z -> (Empty) returns a 200 with 0 bytes (no content)\n \u2022\tn=t -> (Transparent) returns a 200 with a transparent (png) tile\n */\n const strictParam = 'n';\n\n var url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/AerialOSM?include=ImageryProviders&uriScheme=https&key=' + key;\n var cache = {};\n var inflight = {};\n var providers = [];\n var taskQueue = new IntervalTasksQueue(250);\n var metadataLastZoom = -1;\n\n d3_json(url)\n .then(function(json) {\n let imageryResource = json.resourceSets[0].resources[0];\n\n //retrieve and prepare up to date imagery template\n let template = imageryResource.imageUrl; //https://ecn.{subdomain}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=10339\n let subDomains = imageryResource.imageUrlSubdomains; //[\"t0, t1, t2, t3\"]\n let subDomainNumbers = subDomains.map((subDomain) => {\n return subDomain.substring(1);\n } ).join(',');\n\n template = template.replace('{subdomain}', `t{switch:${subDomainNumbers}}`).replace('{quadkey}', '{u}');\n if (!new URLSearchParams(template).has(strictParam)){\n template += `&${strictParam}=z`;\n }\n bing.template(template);\n\n providers = imageryResource.imageryProviders.map(function(provider) {\n return {\n attribution: provider.attribution,\n areas: provider.coverageAreas.map(function(area) {\n return {\n zoom: [area.zoomMin, area.zoomMax],\n extent: geoExtent([area.bbox[1], area.bbox[0]], [area.bbox[3], area.bbox[2]])\n };\n })\n };\n });\n dispatch.call('change');\n })\n .catch(function() {\n /* ignore */\n });\n\n\n bing.copyrightNotices = function(zoom, extent) {\n zoom = Math.min(zoom, 21);\n return providers.filter(function(provider) {\n return provider.areas.some(function(area) {\n return extent.intersects(area.extent) &&\n area.zoom[0] <= zoom &&\n area.zoom[1] >= zoom;\n });\n }).map(function(provider) {\n return provider.attribution;\n }).join(', ');\n };\n\n\n bing.getMetadata = function(center, tileCoord, callback) {\n var tileID = tileCoord.slice(0, 3).join('/');\n var zoom = Math.min(tileCoord[2], 21);\n var centerPoint = center[1] + ',' + center[0]; // lat,lng\n var url = 'https://dev.virtualearth.net/REST/v1/Imagery/BasicMetadata/AerialOSM/' + centerPoint +\n '?zl=' + zoom + '&key=' + key;\n\n if (inflight[tileID]) return;\n\n if (!cache[tileID]) {\n cache[tileID] = {};\n }\n if (cache[tileID] && cache[tileID].metadata) {\n return callback(null, cache[tileID].metadata);\n }\n\n inflight[tileID] = true;\n\n if (metadataLastZoom !== tileCoord[2]){\n metadataLastZoom = tileCoord[2];\n taskQueue.clear();\n }\n\n taskQueue.enqueue(() => {\n d3_json(url)\n .then(function (result) {\n delete inflight[tileID];\n if (!result) {\n throw new Error('Unknown Error');\n }\n var vintage = {\n start: localeDateString(result.resourceSets[0].resources[0].vintageStart),\n end: localeDateString(result.resourceSets[0].resources[0].vintageEnd)\n };\n vintage.range = vintageRange(vintage);\n\n var metadata = { vintage: vintage };\n cache[tileID].metadata = metadata;\n if (callback) callback(null, metadata);\n })\n .catch(function (err) {\n delete inflight[tileID];\n if (callback) callback(err.message);\n });\n });\n };\n\n\n bing.terms_url = 'https://blog.openstreetmap.org/2010/11/30/microsoft-imagery-details';\n\n\n return bing;\n};\n\n\n\nrendererBackgroundSource.Esri = function(data) {\n // in addition to using the tilemap at zoom level 20, overzoom real tiles - #4327 (deprecated technique, but it works)\n if (data.template.match(/blankTile/) === null) {\n data.template = data.template + '?blankTile=false';\n }\n\n var esri = rendererBackgroundSource(data);\n var cache = {};\n var inflight = {};\n var _prevCenter;\n\n // use a tilemap service to set maximum zoom for esri tiles dynamically\n // https://developers.arcgis.com/documentation/tiled-elevation-service/\n esri.fetchTilemap = function(center) {\n // skip if we have already fetched a tilemap within 5km\n if (_prevCenter && geoSphericalDistance(center, _prevCenter) < 5000) return;\n _prevCenter = center;\n\n // tiles are available globally to zoom level 19, afterward they may or may not be present\n var z = 20;\n\n // first generate a random url using the template\n var dummyUrl = esri.url([1,2,3]);\n\n // calculate url z/y/x from the lat/long of the center of the map\n var x = (Math.floor((center[0] + 180) / 360 * Math.pow(2, z)));\n var y = (Math.floor((1 - Math.log(Math.tan(center[1] * Math.PI / 180) + 1 / Math.cos(center[1] * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, z)));\n\n // fetch an 8x8 grid to leverage cache\n var tilemapUrl = dummyUrl.replace(/tile\\/[0-9]+\\/[0-9]+\\/[0-9]+\\?blankTile=false/, 'tilemap') + '/' + z + '/' + y + '/' + x + '/8/8';\n\n // make the request and introspect the response from the tilemap server\n d3_json(tilemapUrl)\n .then(function(tilemap) {\n if (!tilemap) {\n throw new Error('Unknown Error');\n }\n var hasTiles = true;\n for (var i = 0; i < tilemap.data.length; i++) {\n // 0 means an individual tile in the grid doesn't exist\n if (!tilemap.data[i]) {\n hasTiles = false;\n break;\n }\n }\n\n // if any tiles are missing at level 20 we restrict maxZoom to 19\n esri.zoomExtent[1] = (hasTiles ? 22 : 19);\n })\n .catch(function() {\n /* ignore */\n });\n };\n\n\n esri.getMetadata = function(center, tileCoord, callback) {\n let mapServerUrl = esri.metadata;\n if (esri.id === 'EsriWorldImagery') {\n mapServerUrl = 'https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer';\n }\n if (!mapServerUrl) {\n // rest endpoint is not available for ESRI's \"clarity\" imagery\n return callback(null, {});\n }\n var tileID = tileCoord.slice(0, 3).join('/');\n var zoom = Math.min(tileCoord[2], esri.zoomExtent[1]);\n var centerPoint = center[0] + ',' + center[1]; // long, lat (as it should be)\n var unknown = t('info_panels.background.unknown');\n var vintage = {};\n var metadata = {};\n\n if (inflight[tileID]) return;\n\n // build up query using the layer appropriate to the current zoom\n var url = mapServerUrl + '/4/query';\n url += '?returnGeometry=false&geometry=' + centerPoint + '&inSR=4326&geometryType=esriGeometryPoint&outFields=*&f=json';\n\n if (!cache[tileID]) {\n cache[tileID] = {};\n }\n if (cache[tileID] && cache[tileID].metadata) {\n return callback(null, cache[tileID].metadata);\n }\n\n inflight[tileID] = true;\n d3_json(url)\n .then(function(result) {\n delete inflight[tileID];\n\n result = result.features.map(f => f.attributes)\n .filter(a => a.MinMapLevel <= zoom && a.MaxMapLevel >= zoom)[0];\n\n if (!result) {\n throw new Error('Unknown Error');\n } else if (result.features && result.features.length < 1) {\n throw new Error('No Results');\n } else if (result.error && result.error.message) {\n throw new Error(result.error.message);\n }\n\n // pass through the discrete capture date from metadata\n var captureDate = localeDateString(result.SRC_DATE2);\n vintage = {\n start: captureDate,\n end: captureDate,\n range: captureDate\n };\n metadata = {\n vintage: vintage,\n source: clean(result.NICE_NAME),\n description: clean(result.NICE_DESC),\n resolution: clean(+Number(result.SRC_RES).toFixed(4)),\n accuracy: clean(+Number(result.SRC_ACC).toFixed(4))\n };\n\n // append units - meters\n if (isFinite(metadata.resolution)) {\n metadata.resolution += ' m';\n }\n if (isFinite(metadata.accuracy)) {\n metadata.accuracy += ' m';\n }\n\n cache[tileID].metadata = metadata;\n if (callback) callback(null, metadata);\n })\n .catch(function(err) {\n delete inflight[tileID];\n if (callback) callback(err.message);\n });\n\n\n function clean(val) {\n return String(val).trim() || unknown;\n }\n };\n\n return esri;\n};\n\n\nrendererBackgroundSource.None = function() {\n var source = rendererBackgroundSource({ id: 'none', template: '' });\n\n\n source.name = function() {\n return t('background.none');\n };\n\n\n source.label = function() {\n return t.append('background.none');\n };\n\n\n source.imageryUsed = function() {\n return null;\n };\n\n\n source.area = function() {\n return -1; // sources in background pane are sorted by area\n };\n\n\n return source;\n};\n\n\nrendererBackgroundSource.Custom = function(template) {\n var source = rendererBackgroundSource({ id: 'custom', template: template });\n\n\n source.name = function() {\n return t('background.custom');\n };\n\n source.label = function() {\n return t.append('background.custom');\n };\n\n\n source.imageryUsed = function() {\n // sanitize personal connection tokens - #6801\n var cleaned = source.template();\n\n // from query string parameters\n if (cleaned.indexOf('?') !== -1) {\n var parts = cleaned.split('?', 2);\n var qs = utilStringQs(parts[1]);\n\n ['access_token', 'connectId', 'token', 'Signature'].forEach(function(param) {\n if (qs[param]) {\n qs[param] = '{apikey}';\n }\n });\n cleaned = parts[0] + '?' + utilQsString(qs, true); // true = soft encode\n }\n\n // from wms/wmts api path parameters\n cleaned = cleaned\n .replace(/token\\/(\\w+)/, 'token/{apikey}')\n .replace(/key=(\\w+)/, 'key={apikey}');\n return 'Custom (' + cleaned + ' )';\n };\n\n\n source.area = function() {\n return -2; // sources in background pane are sorted by area\n };\n\n\n return source;\n};\n", "import { feature, point, lineString, isObject } from \"@turf/helpers\";\nimport {\n Point,\n LineString,\n Polygon,\n MultiLineString,\n MultiPolygon,\n FeatureCollection,\n Feature,\n Geometry,\n GeometryObject,\n GeometryCollection,\n GeoJsonProperties,\n BBox,\n GeoJsonTypes,\n} from \"geojson\";\nimport { AllGeoJSON, Lines, Id } from \"@turf/helpers\";\n\n/**\n * Callback for coordEach\n *\n * @callback coordEachCallback\n * @param {number[]} currentCoord The current coordinate being processed.\n * @param {number} coordIndex The current index of the coordinate being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over coordinates in any GeoJSON object, similar to Array.forEach()\n *\n * @function\n * @param {AllGeoJSON} geojson any GeoJSON object\n * @param {coordEachCallback} callback a method that takes (currentCoord, coordIndex, featureIndex, multiFeatureIndex)\n * @param {boolean} [excludeWrapCoord=false] whether or not to include the final coordinate of LinearRings that wraps the ring in its iteration.\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {\"foo\": \"bar\"}),\n * turf.point([36, 53], {\"hello\": \"world\"})\n * ]);\n *\n * turf.coordEach(features, function (currentCoord, coordIndex, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=currentCoord\n * //=coordIndex\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * });\n */\nfunction coordEach(\n geojson: AllGeoJSON,\n callback: (\n currentCoord: number[],\n coordIndex: number,\n featureIndex: number,\n multiFeatureIndex: number,\n geometryIndex: number\n ) => void,\n excludeWrapCoord?: boolean\n): void {\n // Handles null Geometry -- Skips this GeoJSON\n if (geojson === null) return;\n var j,\n k,\n l,\n geometry,\n stopG,\n coords,\n geometryMaybeCollection,\n wrapShrink = 0,\n coordIndex = 0,\n isGeometryCollection,\n type = geojson.type,\n isFeatureCollection = type === \"FeatureCollection\",\n isFeature = type === \"Feature\",\n // @ts-expect-error: Known type conflict\n stop = isFeatureCollection ? geojson.features.length : 1;\n\n // This logic may look a little weird. The reason why it is that way\n // is because it's trying to be fast. GeoJSON supports multiple kinds\n // of objects at its root: FeatureCollection, Features, Geometries.\n // This function has the responsibility of handling all of them, and that\n // means that some of the `for` loops you see below actually just don't apply\n // to certain inputs. For instance, if you give this just a\n // Point geometry, then both loops are short-circuited and all we do\n // is gradually rename the input until it's called 'geometry'.\n //\n // This also aims to allocate as few resources as possible: just a\n // few numbers and booleans, rather than any temporary arrays as would\n // be required with the normalization approach.\n for (var featureIndex = 0; featureIndex < stop; featureIndex++) {\n geometryMaybeCollection = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[featureIndex].geometry\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.geometry\n : geojson;\n isGeometryCollection = geometryMaybeCollection\n ? geometryMaybeCollection.type === \"GeometryCollection\"\n : false;\n stopG = isGeometryCollection\n ? geometryMaybeCollection.geometries.length\n : 1;\n\n for (var geomIndex = 0; geomIndex < stopG; geomIndex++) {\n var multiFeatureIndex = 0;\n var geometryIndex = 0;\n geometry = isGeometryCollection\n ? geometryMaybeCollection.geometries[geomIndex]\n : geometryMaybeCollection;\n\n // Handles null Geometry -- Skips this geometry\n if (geometry === null) continue;\n coords = geometry.coordinates;\n var geomType = geometry.type;\n\n wrapShrink =\n excludeWrapCoord &&\n (geomType === \"Polygon\" || geomType === \"MultiPolygon\")\n ? 1\n : 0;\n\n switch (geomType) {\n case null:\n break;\n case \"Point\":\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords,\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n multiFeatureIndex++;\n break;\n case \"LineString\":\n case \"MultiPoint\":\n for (j = 0; j < coords.length; j++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords[j],\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n if (geomType === \"MultiPoint\") multiFeatureIndex++;\n }\n if (geomType === \"LineString\") multiFeatureIndex++;\n break;\n case \"Polygon\":\n case \"MultiLineString\":\n for (j = 0; j < coords.length; j++) {\n for (k = 0; k < coords[j].length - wrapShrink; k++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords[j][k],\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n }\n if (geomType === \"MultiLineString\") multiFeatureIndex++;\n if (geomType === \"Polygon\") geometryIndex++;\n }\n if (geomType === \"Polygon\") multiFeatureIndex++;\n break;\n case \"MultiPolygon\":\n for (j = 0; j < coords.length; j++) {\n geometryIndex = 0;\n for (k = 0; k < coords[j].length; k++) {\n for (l = 0; l < coords[j][k].length - wrapShrink; l++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords[j][k][l],\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n }\n geometryIndex++;\n }\n multiFeatureIndex++;\n }\n break;\n case \"GeometryCollection\":\n for (j = 0; j < geometry.geometries.length; j++)\n if (\n // @ts-expect-error: Known type conflict\n coordEach(geometry.geometries[j], callback, excludeWrapCoord) ===\n false\n )\n // @ts-expect-error: Known type conflict\n return false;\n break;\n default:\n throw new Error(\"Unknown Geometry Type\");\n }\n }\n }\n}\n\n/**\n * Callback for coordReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback coordReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {number[]} currentCoord The current coordinate being processed.\n * @param {number} coordIndex The current index of the coordinate being processed.\n * Starts at index 0, if an initialValue is provided, and at index 1 otherwise.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce coordinates in any GeoJSON object, similar to Array.reduce()\n *\n * @function\n * @param {AllGeoJSON} geojson any GeoJSON object\n * @param {coordReduceCallback} callback a method that takes (previousValue, currentCoord, coordIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @param {boolean} [excludeWrapCoord=false] whether or not to include the final coordinate of LinearRings that wraps the ring in its iteration.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {\"foo\": \"bar\"}),\n * turf.point([36, 53], {\"hello\": \"world\"})\n * ]);\n *\n * turf.coordReduce(features, function (previousValue, currentCoord, coordIndex, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=previousValue\n * //=currentCoord\n * //=coordIndex\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * return currentCoord;\n * });\n */\nfunction coordReduce(\n geojson: AllGeoJSON,\n callback: (\n previousValue: Reducer,\n currentCoord: number[],\n coordIndex: number,\n featureIndex: number,\n multiFeatureIndex: number,\n geometryIndex: number\n ) => Reducer,\n initialValue?: Reducer,\n excludeWrapCoord?: boolean\n): Reducer {\n var previousValue = initialValue;\n coordEach(\n geojson,\n function (\n currentCoord,\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) {\n if (coordIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentCoord;\n else\n previousValue = callback(\n // @ts-expect-error: Known type conflict\n previousValue,\n currentCoord,\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n );\n },\n excludeWrapCoord\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for propEach\n *\n * @callback propEachCallback\n * @param {GeoJsonProperties} currentProperties The current Properties being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over properties in any GeoJSON object, similar to Array.forEach()\n *\n * @function\n * @param {FeatureCollection|Feature} geojson any GeoJSON object\n * @param {propEachCallback} callback a method that takes (currentProperties, featureIndex)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.propEach(features, function (currentProperties, featureIndex) {\n * //=currentProperties\n * //=featureIndex\n * });\n */\nfunction propEach(\n geojson: Feature | FeatureCollection | Feature,\n callback: (currentProperties: Props, featureIndex: number) => void\n): void {\n var i;\n switch (geojson.type) {\n case \"FeatureCollection\":\n for (i = 0; i < geojson.features.length; i++) {\n // @ts-expect-error: Known type conflict\n if (callback(geojson.features[i].properties, i) === false) break;\n }\n break;\n case \"Feature\":\n // @ts-expect-error: Known type conflict\n callback(geojson.properties, 0);\n break;\n }\n}\n\n/**\n * Callback for propReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback propReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {GeoJsonProperties} currentProperties The current Properties being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce properties in any GeoJSON object into a single value,\n * similar to how Array.reduce works. However, in this case we lazily run\n * the reduction, so an array of all properties is unnecessary.\n *\n * @function\n * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON object\n * @param {propReduceCallback} callback a method that takes (previousValue, currentProperties, featureIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.propReduce(features, function (previousValue, currentProperties, featureIndex) {\n * //=previousValue\n * //=currentProperties\n * //=featureIndex\n * return currentProperties\n * });\n */\nfunction propReduce(\n geojson: Feature | FeatureCollection | Geometry,\n callback: (\n previousValue: Reducer,\n currentProperties: P,\n featureIndex: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n // @ts-expect-error: Known type conflict\n propEach(geojson, function (currentProperties, featureIndex) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentProperties;\n else\n // @ts-expect-error: Known type conflict\n previousValue = callback(previousValue, currentProperties, featureIndex);\n });\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for featureEach\n *\n * @callback featureEachCallback\n * @param {Feature} currentFeature The current Feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over features in any GeoJSON object, similar to\n * Array.forEach.\n *\n * @function\n * @param {FeatureCollection|Feature|Feature} geojson any GeoJSON object\n * @param {featureEachCallback} callback a method that takes (currentFeature, featureIndex)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.featureEach(features, function (currentFeature, featureIndex) {\n * //=currentFeature\n * //=featureIndex\n * });\n */\nfunction featureEach<\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | Feature,\n callback: (currentFeature: Feature, featureIndex: number) => void\n): void {\n if (geojson.type === \"Feature\") {\n // @ts-expect-error: Known type conflict\n callback(geojson, 0);\n } else if (geojson.type === \"FeatureCollection\") {\n for (var i = 0; i < geojson.features.length; i++) {\n // @ts-expect-error: Known type conflict\n if (callback(geojson.features[i], i) === false) break;\n }\n }\n}\n\n/**\n * Callback for featureReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback featureReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentFeature The current Feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce features in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|Feature} geojson any GeoJSON object\n * @param {featureReduceCallback} callback a method that takes (previousValue, currentFeature, featureIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {\"foo\": \"bar\"}),\n * turf.point([36, 53], {\"hello\": \"world\"})\n * ]);\n *\n * turf.featureReduce(features, function (previousValue, currentFeature, featureIndex) {\n * //=previousValue\n * //=currentFeature\n * //=featureIndex\n * return currentFeature\n * });\n */\nfunction featureReduce<\n Reducer,\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | Feature,\n callback: (\n previousValue: Reducer,\n currentFeature: Feature,\n featureIndex: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n featureEach(geojson, function (currentFeature, featureIndex) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentFeature;\n // @ts-expect-error: Known type conflict\n else previousValue = callback(previousValue, currentFeature, featureIndex);\n });\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Get all coordinates from any GeoJSON object.\n *\n * @function\n * @param {AllGeoJSON} geojson any GeoJSON object\n * @returns {Array>} coordinate position array\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * var coords = turf.coordAll(features);\n * //= [[26, 37], [36, 53]]\n */\nfunction coordAll(geojson: AllGeoJSON): number[][] {\n // @ts-expect-error: Known type conflict\n var coords = [];\n coordEach(geojson, function (coord) {\n coords.push(coord);\n });\n // @ts-expect-error: Known type conflict\n return coords;\n}\n\n/**\n * Callback for geomEach\n *\n * @callback geomEachCallback\n * @param {GeometryObject} currentGeometry The current Geometry being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {GeoJsonProperties} featureProperties The current Feature Properties being processed.\n * @param {BBox} featureBBox The current Feature BBox being processed.\n * @param {Id} featureId The current Feature Id being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over each geometry in any GeoJSON object, similar to Array.forEach()\n *\n * @function\n * @param {FeatureCollection|Feature|Geometry|GeometryObject|Feature} geojson any GeoJSON object\n * @param {geomEachCallback} callback a method that takes (currentGeometry, featureIndex, featureProperties, featureBBox, featureId)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.geomEach(features, function (currentGeometry, featureIndex, featureProperties, featureBBox, featureId) {\n * //=currentGeometry\n * //=featureIndex\n * //=featureProperties\n * //=featureBBox\n * //=featureId\n * });\n */\nfunction geomEach<\n G extends GeometryObject | null,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n currentGeometry: G,\n featureIndex: number,\n featureProperties: P,\n featureBBox: BBox,\n featureId: Id\n ) => void\n): void {\n var i,\n j,\n g,\n geometry,\n stopG,\n geometryMaybeCollection,\n isGeometryCollection,\n featureProperties,\n featureBBox,\n featureId,\n featureIndex = 0,\n // @ts-expect-error: Known type conflict\n isFeatureCollection = geojson.type === \"FeatureCollection\",\n // @ts-expect-error: Known type conflict\n isFeature = geojson.type === \"Feature\",\n // @ts-expect-error: Known type conflict\n stop = isFeatureCollection ? geojson.features.length : 1;\n\n // This logic may look a little weird. The reason why it is that way\n // is because it's trying to be fast. GeoJSON supports multiple kinds\n // of objects at its root: FeatureCollection, Features, Geometries.\n // This function has the responsibility of handling all of them, and that\n // means that some of the `for` loops you see below actually just don't apply\n // to certain inputs. For instance, if you give this just a\n // Point geometry, then both loops are short-circuited and all we do\n // is gradually rename the input until it's called 'geometry'.\n //\n // This also aims to allocate as few resources as possible: just a\n // few numbers and booleans, rather than any temporary arrays as would\n // be required with the normalization approach.\n for (i = 0; i < stop; i++) {\n geometryMaybeCollection = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].geometry\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.geometry\n : geojson;\n featureProperties = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].properties\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.properties\n : {};\n featureBBox = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].bbox\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.bbox\n : undefined;\n featureId = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].id\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.id\n : undefined;\n isGeometryCollection = geometryMaybeCollection\n ? geometryMaybeCollection.type === \"GeometryCollection\"\n : false;\n stopG = isGeometryCollection\n ? geometryMaybeCollection.geometries.length\n : 1;\n\n for (g = 0; g < stopG; g++) {\n geometry = isGeometryCollection\n ? geometryMaybeCollection.geometries[g]\n : geometryMaybeCollection;\n\n // Handle null Geometry\n if (geometry === null) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n // @ts-expect-error: Known type conflict\n null,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n continue;\n }\n switch (geometry.type) {\n case \"Point\":\n case \"LineString\":\n case \"MultiPoint\":\n case \"Polygon\":\n case \"MultiLineString\":\n case \"MultiPolygon\": {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n geometry,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n break;\n }\n case \"GeometryCollection\": {\n for (j = 0; j < geometry.geometries.length; j++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n geometry.geometries[j],\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n }\n break;\n }\n default:\n throw new Error(\"Unknown Geometry Type\");\n }\n }\n // Only increase `featureIndex` per each feature\n featureIndex++;\n }\n}\n\n/**\n * Callback for geomReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback geomReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {GeometryObject} currentGeometry The current Geometry being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {GeoJsonProperties} featureProperties The current Feature Properties being processed.\n * @param {BBox} featureBBox The current Feature BBox being processed.\n * @param {Id} featureId The current Feature Id being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce geometry in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|GeometryObject|GeometryCollection|Feature} geojson any GeoJSON object\n * @param {geomReduceCallback} callback a method that takes (previousValue, currentGeometry, featureIndex, featureProperties, featureBBox, featureId)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.geomReduce(features, function (previousValue, currentGeometry, featureIndex, featureProperties, featureBBox, featureId) {\n * //=previousValue\n * //=currentGeometry\n * //=featureIndex\n * //=featureProperties\n * //=featureBBox\n * //=featureId\n * return currentGeometry\n * });\n */\nfunction geomReduce<\n Reducer,\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n previousValue: Reducer,\n currentGeometry: G,\n featureIndex: number,\n featureProperties: P,\n featureBBox: BBox,\n featureId: Id\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n geomEach(\n geojson,\n function (\n currentGeometry,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentGeometry;\n else\n previousValue = callback(\n // @ts-expect-error: Known type conflict\n previousValue,\n currentGeometry,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n );\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for flattenEach\n *\n * @callback flattenEachCallback\n * @param {Feature} currentFeature The current flattened feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over flattened features in any GeoJSON object, similar to\n * Array.forEach.\n *\n * @function\n * @param {FeatureCollection|Feature|GeometryObject|GeometryCollection|Feature} geojson any GeoJSON object\n * @param {flattenEachCallback} callback a method that takes (currentFeature, featureIndex, multiFeatureIndex)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.multiPoint([[40, 30], [36, 53]], {hello: 'world'})\n * ]);\n *\n * turf.flattenEach(features, function (currentFeature, featureIndex, multiFeatureIndex) {\n * //=currentFeature\n * //=featureIndex\n * //=multiFeatureIndex\n * });\n */\nfunction flattenEach<\n G extends GeometryObject = GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n currentFeature: Feature,\n featureIndex: number,\n multiFeatureIndex: number\n ) => void\n): void {\n geomEach(geojson, function (geometry, featureIndex, properties, bbox, id) {\n // Callback for single geometry\n var type = geometry === null ? null : geometry.type;\n switch (type) {\n case null:\n case \"Point\":\n case \"LineString\":\n case \"Polygon\":\n if (\n // @ts-expect-error: Known type conflict\n callback(\n feature(geometry, properties, { bbox: bbox, id: id }),\n featureIndex,\n 0\n ) === false\n )\n return false;\n return;\n }\n\n var geomType;\n\n // Callback for multi-geometry\n switch (type) {\n case \"MultiPoint\":\n geomType = \"Point\";\n break;\n case \"MultiLineString\":\n geomType = \"LineString\";\n break;\n case \"MultiPolygon\":\n geomType = \"Polygon\";\n break;\n }\n\n for (\n var multiFeatureIndex = 0;\n // @ts-expect-error: Known type conflict\n multiFeatureIndex < geometry.coordinates.length;\n multiFeatureIndex++\n ) {\n // @ts-expect-error: Known type conflict\n var coordinate = geometry.coordinates[multiFeatureIndex];\n var geom = {\n type: geomType,\n coordinates: coordinate,\n };\n if (\n // @ts-expect-error: Known type conflict\n callback(feature(geom, properties), featureIndex, multiFeatureIndex) ===\n false\n )\n return false;\n }\n });\n}\n\n/**\n * Callback for flattenReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback flattenReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentFeature The current Feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce flattened features in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|GeometryObject|GeometryCollection|Feature} geojson any GeoJSON object\n * @param {flattenReduceCallback} callback a method that takes (previousValue, currentFeature, featureIndex, multiFeatureIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.multiPoint([[40, 30], [36, 53]], {hello: 'world'})\n * ]);\n *\n * turf.flattenReduce(features, function (previousValue, currentFeature, featureIndex, multiFeatureIndex) {\n * //=previousValue\n * //=currentFeature\n * //=featureIndex\n * //=multiFeatureIndex\n * return currentFeature\n * });\n */\nfunction flattenReduce<\n Reducer,\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n previousValue: Reducer,\n currentFeature: Feature,\n featureIndex: number,\n multiFeatureIndex: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n flattenEach(\n geojson,\n function (currentFeature, featureIndex, multiFeatureIndex) {\n if (\n featureIndex === 0 &&\n multiFeatureIndex === 0 &&\n initialValue === undefined\n )\n // @ts-expect-error: Known type conflict\n previousValue = currentFeature;\n else\n previousValue = callback(\n // @ts-expect-error: Known type conflict\n previousValue,\n currentFeature,\n featureIndex,\n multiFeatureIndex\n );\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for segmentEach\n *\n * @callback segmentEachCallback\n * @param {Feature} currentSegment The current Segment being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @param {number} segmentIndex The current index of the Segment being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over 2-vertex line segment in any GeoJSON object, similar to Array.forEach()\n * (Multi)Point geometries do not contain segments therefore they are ignored during this operation.\n *\n * @param {AllGeoJSON} geojson any GeoJSON\n * @param {segmentEachCallback} callback a method that takes (currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex)\n * @returns {void}\n * @example\n * var polygon = turf.polygon([[[-50, 5], [-40, -10], [-50, -10], [-40, 5], [-50, 5]]]);\n *\n * // Iterate over GeoJSON by 2-vertex segments\n * turf.segmentEach(polygon, function (currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex) {\n * //=currentSegment\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * //=segmentIndex\n * });\n *\n * // Calculate the total number of segments\n * var total = 0;\n * turf.segmentEach(polygon, function () {\n * total++;\n * });\n */\nfunction segmentEach

    (\n geojson: AllGeoJSON,\n callback: (\n currentSegment?: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n segmentIndex?: number,\n geometryIndex?: number\n ) => void\n): void {\n flattenEach(geojson, function (feature, featureIndex, multiFeatureIndex) {\n var segmentIndex = 0;\n\n // Exclude null Geometries\n if (!feature.geometry) return;\n // (Multi)Point geometries do not contain segments therefore they are ignored during this operation.\n var type = feature.geometry.type;\n if (type === \"Point\" || type === \"MultiPoint\") return;\n\n // Generate 2-vertex line segments\n // @ts-expect-error: Known type conflict\n var previousCoords;\n var previousFeatureIndex = 0;\n var previousMultiIndex = 0;\n var prevGeomIndex = 0;\n if (\n // @ts-expect-error: Known type conflict\n coordEach(\n feature,\n function (\n currentCoord,\n coordIndex,\n featureIndexCoord,\n multiPartIndexCoord,\n geometryIndex\n ) {\n // Simulating a meta.coordReduce() since `reduce` operations cannot be stopped by returning `false`\n if (\n // @ts-expect-error: Known type conflict\n previousCoords === undefined ||\n featureIndex > previousFeatureIndex ||\n multiPartIndexCoord > previousMultiIndex ||\n geometryIndex > prevGeomIndex\n ) {\n previousCoords = currentCoord;\n previousFeatureIndex = featureIndex;\n previousMultiIndex = multiPartIndexCoord;\n prevGeomIndex = geometryIndex;\n segmentIndex = 0;\n return;\n }\n var currentSegment = lineString(\n // @ts-expect-error: Known type conflict\n [previousCoords, currentCoord],\n feature.properties\n );\n if (\n // @ts-expect-error: Known type conflict\n callback(\n // @ts-expect-error: Known type conflict\n currentSegment,\n featureIndex,\n multiFeatureIndex,\n geometryIndex,\n segmentIndex\n ) === false\n )\n return false;\n segmentIndex++;\n previousCoords = currentCoord;\n }\n ) === false\n )\n return false;\n });\n}\n\n/**\n * Callback for segmentReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback segmentReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentSegment The current Segment being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @param {number} segmentIndex The current index of the Segment being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce 2-vertex line segment in any GeoJSON object, similar to Array.reduce()\n * (Multi)Point geometries do not contain segments therefore they are ignored during this operation.\n *\n * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON\n * @param {segmentReduceCallback} callback a method that takes (previousValue, currentSegment, currentIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer}\n * @example\n * var polygon = turf.polygon([[[-50, 5], [-40, -10], [-50, -10], [-40, 5], [-50, 5]]]);\n *\n * // Iterate over GeoJSON by 2-vertex segments\n * turf.segmentReduce(polygon, function (previousSegment, currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex) {\n * //= previousSegment\n * //= currentSegment\n * //= featureIndex\n * //= multiFeatureIndex\n * //= geometryIndex\n * //= segmentIndex\n * return currentSegment\n * });\n *\n * // Calculate the total number of segments\n * var initialValue = 0\n * var total = turf.segmentReduce(polygon, function (previousValue) {\n * previousValue++;\n * return previousValue;\n * }, initialValue);\n */\nfunction segmentReduce<\n Reducer,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | FeatureCollection\n | Feature\n | Lines\n | Feature\n | GeometryCollection,\n callback: (\n previousValue?: Reducer,\n currentSegment?: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n segmentIndex?: number,\n geometryIndex?: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n var started = false;\n segmentEach(\n geojson,\n function (\n currentSegment,\n featureIndex,\n multiFeatureIndex,\n geometryIndex,\n segmentIndex\n ) {\n if (started === false && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentSegment;\n else\n previousValue = callback(\n previousValue,\n // @ts-expect-error: Known type conflict\n currentSegment,\n featureIndex,\n multiFeatureIndex,\n geometryIndex,\n segmentIndex\n );\n started = true;\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for lineEach\n *\n * @callback lineEachCallback\n * @param {Feature} currentLine The current LineString|LinearRing being processed\n * @param {number} featureIndex The current index of the Feature being processed\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed\n * @param {number} geometryIndex The current index of the Geometry being processed\n * @returns {void}\n */\n\n/**\n * Iterate over line or ring coordinates in LineString, Polygon, MultiLineString, MultiPolygon Features or Geometries,\n * similar to Array.forEach.\n *\n * @function\n * @param {FeatureCollection|Feature|Lines|Feature|GeometryCollection} geojson object\n * @param {lineEachCallback} callback a method that takes (currentLine, featureIndex, multiFeatureIndex, geometryIndex)\n * @returns {void}\n * @example\n * var multiLine = turf.multiLineString([\n * [[26, 37], [35, 45]],\n * [[36, 53], [38, 50], [41, 55]]\n * ]);\n *\n * turf.lineEach(multiLine, function (currentLine, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=currentLine\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * });\n */\nfunction lineEach

    (\n geojson:\n | FeatureCollection\n | Feature\n | Lines\n | Feature\n | GeometryCollection,\n callback: (\n currentLine: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n geometryIndex?: number\n ) => void\n): void {\n // validation\n if (!geojson) throw new Error(\"geojson is required\");\n\n flattenEach(geojson, function (feature, featureIndex, multiFeatureIndex) {\n if (feature.geometry === null) return;\n var type = feature.geometry.type;\n var coords = feature.geometry.coordinates;\n switch (type) {\n case \"LineString\":\n // @ts-expect-error: Known type conflict\n if (callback(feature, featureIndex, multiFeatureIndex, 0, 0) === false)\n return false;\n break;\n case \"Polygon\":\n for (\n var geometryIndex = 0;\n geometryIndex < coords.length;\n geometryIndex++\n ) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n // @ts-expect-error: Known type conflict\n lineString(coords[geometryIndex], feature.properties),\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n return false;\n }\n break;\n }\n });\n}\n\n/**\n * Callback for lineReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback lineReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentLine The current LineString|LinearRing being processed.\n * @param {number} featureIndex The current index of the Feature being processed\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed\n * @param {number} geometryIndex The current index of the Geometry being processed\n * @returns {Reducer}\n */\n\n/**\n * Reduce features in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|Lines|Feature|GeometryCollection} geojson object\n * @param {Function} callback a method that takes (previousValue, currentLine, featureIndex, multiFeatureIndex, geometryIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var multiPoly = turf.multiPolygon([\n * turf.polygon([[[12,48],[2,41],[24,38],[12,48]], [[9,44],[13,41],[13,45],[9,44]]]),\n * turf.polygon([[[5, 5], [0, 0], [2, 2], [4, 4], [5, 5]]])\n * ]);\n *\n * turf.lineReduce(multiPoly, function (previousValue, currentLine, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=previousValue\n * //=currentLine\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * return currentLine\n * });\n */\nfunction lineReduce(\n geojson:\n | FeatureCollection\n | Feature\n | Lines\n | Feature\n | GeometryCollection,\n callback: (\n previousValue?: Reducer,\n currentLine?: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n geometryIndex?: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n lineEach(\n geojson,\n function (currentLine, featureIndex, multiFeatureIndex, geometryIndex) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentLine;\n else\n previousValue = callback(\n previousValue,\n currentLine,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n );\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Finds a particular 2-vertex LineString Segment from a GeoJSON using `@turf/meta` indexes.\n *\n * Negative indexes are permitted.\n * Point & MultiPoint will always return null.\n *\n * @param {FeatureCollection|Feature|Geometry} geojson Any GeoJSON Feature or Geometry\n * @param {Object} [options={}] Optional parameters\n * @param {number} [options.featureIndex=0] Feature Index\n * @param {number} [options.multiFeatureIndex=0] Multi-Feature Index\n * @param {number} [options.geometryIndex=0] Geometry Index\n * @param {number} [options.segmentIndex=0] Segment Index\n * @param {Object} [options.properties={}] Translate Properties to output LineString\n * @param {BBox} [options.bbox={}] Translate BBox to output LineString\n * @param {number|string} [options.id={}] Translate Id to output LineString\n * @returns {Feature} 2-vertex GeoJSON Feature LineString\n * @example\n * var multiLine = turf.multiLineString([\n * [[10, 10], [50, 30], [30, 40]],\n * [[-10, -10], [-50, -30], [-30, -40]]\n * ]);\n *\n * // First Segment (defaults are 0)\n * turf.findSegment(multiLine);\n * // => Feature>\n *\n * // First Segment of 2nd Multi Feature\n * turf.findSegment(multiLine, {multiFeatureIndex: 1});\n * // => Feature>\n *\n * // Last Segment of Last Multi Feature\n * turf.findSegment(multiLine, {multiFeatureIndex: -1, segmentIndex: -1});\n * // => Feature>\n */\nfunction findSegment<\n G extends LineString | MultiLineString | Polygon | MultiPolygon,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson: Feature | FeatureCollection | G,\n options?: {\n featureIndex?: number;\n multiFeatureIndex?: number;\n geometryIndex?: number;\n segmentIndex?: number;\n properties?: P;\n bbox?: BBox;\n id?: Id;\n }\n): Feature {\n // Optional Parameters\n options = options || {};\n if (!isObject(options)) throw new Error(\"options is invalid\");\n var featureIndex = options.featureIndex || 0;\n var multiFeatureIndex = options.multiFeatureIndex || 0;\n var geometryIndex = options.geometryIndex || 0;\n var segmentIndex = options.segmentIndex || 0;\n\n // Find FeatureIndex\n var properties = options.properties;\n var geometry;\n\n switch (geojson.type) {\n case \"FeatureCollection\":\n if (featureIndex < 0)\n featureIndex = geojson.features.length + featureIndex;\n properties = properties || geojson.features[featureIndex].properties;\n geometry = geojson.features[featureIndex].geometry;\n break;\n case \"Feature\":\n properties = properties || geojson.properties;\n geometry = geojson.geometry;\n break;\n case \"Point\" as GeoJsonTypes:\n case \"MultiPoint\" as GeoJsonTypes:\n // @ts-expect-error: Known type conflict\n return null;\n case \"LineString\":\n case \"Polygon\":\n case \"MultiLineString\":\n case \"MultiPolygon\":\n geometry = geojson;\n break;\n default:\n throw new Error(\"geojson is invalid\");\n }\n\n // Find SegmentIndex\n // @ts-expect-error: Known type conflict\n if (geometry === null) return null;\n var coords = geometry.coordinates;\n switch (geometry.type) {\n case \"Point\" as GeoJsonTypes:\n case \"MultiPoint\" as GeoJsonTypes:\n // @ts-expect-error: Known type conflict\n return null;\n case \"LineString\":\n if (segmentIndex < 0) segmentIndex = coords.length + segmentIndex - 1;\n return lineString(\n // @ts-expect-error: Known type conflict\n [coords[segmentIndex], coords[segmentIndex + 1]],\n properties,\n options\n );\n case \"Polygon\":\n if (geometryIndex < 0) geometryIndex = coords.length + geometryIndex;\n if (segmentIndex < 0)\n segmentIndex = coords[geometryIndex].length + segmentIndex - 1;\n return lineString(\n [\n // @ts-expect-error: Known type conflict\n coords[geometryIndex][segmentIndex],\n // @ts-expect-error: Known type conflict\n coords[geometryIndex][segmentIndex + 1],\n ],\n properties,\n options\n );\n case \"MultiLineString\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (segmentIndex < 0)\n segmentIndex = coords[multiFeatureIndex].length + segmentIndex - 1;\n return lineString(\n [\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][segmentIndex],\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][segmentIndex + 1],\n ],\n properties,\n options\n );\n case \"MultiPolygon\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (geometryIndex < 0)\n geometryIndex = coords[multiFeatureIndex].length + geometryIndex;\n if (segmentIndex < 0)\n segmentIndex =\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][geometryIndex].length - segmentIndex - 1;\n return lineString(\n [\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][geometryIndex][segmentIndex],\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][geometryIndex][segmentIndex + 1],\n ],\n properties,\n options\n );\n }\n throw new Error(\"geojson is invalid\");\n}\n\n/**\n * Finds a particular Point from a GeoJSON using `@turf/meta` indexes.\n *\n * Negative indexes are permitted.\n *\n * @param {FeatureCollection|Feature|Geometry} geojson Any GeoJSON Feature or Geometry\n * @param {Object} [options={}] Optional parameters\n * @param {number} [options.featureIndex=0] Feature Index\n * @param {number} [options.multiFeatureIndex=0] Multi-Feature Index\n * @param {number} [options.geometryIndex=0] Geometry Index\n * @param {number} [options.coordIndex=0] Coord Index\n * @param {Object} [options.properties={}] Translate Properties to output Point\n * @param {BBox} [options.bbox={}] Translate BBox to output Point\n * @param {number|string} [options.id={}] Translate Id to output Point\n * @returns {Feature} 2-vertex GeoJSON Feature Point\n * @example\n * var multiLine = turf.multiLineString([\n * [[10, 10], [50, 30], [30, 40]],\n * [[-10, -10], [-50, -30], [-30, -40]]\n * ]);\n *\n * // First Segment (defaults are 0)\n * turf.findPoint(multiLine);\n * // => Feature>\n *\n * // First Segment of the 2nd Multi-Feature\n * turf.findPoint(multiLine, {multiFeatureIndex: 1});\n * // => Feature>\n *\n * // Last Segment of last Multi-Feature\n * turf.findPoint(multiLine, {multiFeatureIndex: -1, coordIndex: -1});\n * // => Feature>\n */\nfunction findPoint<\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson: Feature | FeatureCollection | G,\n options?: {\n featureIndex?: number;\n multiFeatureIndex?: number;\n geometryIndex?: number;\n coordIndex?: number;\n properties?: P;\n bbox?: BBox;\n id?: Id;\n }\n): Feature {\n // Optional Parameters\n options = options || {};\n if (!isObject(options)) throw new Error(\"options is invalid\");\n var featureIndex = options.featureIndex || 0;\n var multiFeatureIndex = options.multiFeatureIndex || 0;\n var geometryIndex = options.geometryIndex || 0;\n var coordIndex = options.coordIndex || 0;\n\n // Find FeatureIndex\n var properties = options.properties;\n var geometry;\n\n switch (geojson.type) {\n case \"FeatureCollection\":\n if (featureIndex < 0)\n featureIndex = geojson.features.length + featureIndex;\n properties = properties || geojson.features[featureIndex].properties;\n geometry = geojson.features[featureIndex].geometry;\n break;\n case \"Feature\":\n properties = properties || geojson.properties;\n geometry = geojson.geometry;\n break;\n case \"Point\":\n case \"MultiPoint\":\n // @ts-expect-error: Known type conflict\n return null;\n case \"LineString\":\n case \"Polygon\":\n case \"MultiLineString\":\n case \"MultiPolygon\":\n geometry = geojson;\n break;\n default:\n throw new Error(\"geojson is invalid\");\n }\n\n // Find Coord Index\n // @ts-expect-error: Known type conflict\n if (geometry === null) return null;\n // @ts-expect-error: Known type conflict\n var coords = geometry.coordinates;\n switch (geometry.type) {\n case \"Point\":\n return point(coords, properties, options);\n case \"MultiPoint\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n return point(coords[multiFeatureIndex], properties, options);\n case \"LineString\":\n if (coordIndex < 0) coordIndex = coords.length + coordIndex;\n return point(coords[coordIndex], properties, options);\n case \"Polygon\":\n if (geometryIndex < 0) geometryIndex = coords.length + geometryIndex;\n if (coordIndex < 0)\n coordIndex = coords[geometryIndex].length + coordIndex;\n return point(coords[geometryIndex][coordIndex], properties, options);\n case \"MultiLineString\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (coordIndex < 0)\n coordIndex = coords[multiFeatureIndex].length + coordIndex;\n return point(coords[multiFeatureIndex][coordIndex], properties, options);\n case \"MultiPolygon\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (geometryIndex < 0)\n geometryIndex = coords[multiFeatureIndex].length + geometryIndex;\n if (coordIndex < 0)\n coordIndex =\n coords[multiFeatureIndex][geometryIndex].length - coordIndex;\n return point(\n coords[multiFeatureIndex][geometryIndex][coordIndex],\n properties,\n options\n );\n }\n throw new Error(\"geojson is invalid\");\n}\n\nexport {\n coordReduce,\n coordEach,\n propEach,\n propReduce,\n featureReduce,\n featureEach,\n coordAll,\n geomReduce,\n geomEach,\n flattenReduce,\n flattenEach,\n segmentReduce,\n segmentEach,\n lineReduce,\n lineEach,\n findSegment,\n findPoint,\n};\n", "import { BBox } from \"geojson\";\nimport { AllGeoJSON } from \"@turf/helpers\";\nimport { coordEach } from \"@turf/meta\";\n\n/**\n * Calculates the bounding box for any GeoJSON object, including FeatureCollection.\n * Uses geojson.bbox if available and options.recompute is not set.\n *\n * @function\n * @param {GeoJSON} geojson any GeoJSON object\n * @param {Object} [options={}] Optional parameters\n * @param {boolean} [options.recompute] Whether to ignore an existing bbox property on geojson\n * @returns {BBox} bbox extent in [minX, minY, maxX, maxY] order\n * @example\n * var line = turf.lineString([[-74, 40], [-78, 42], [-82, 35]]);\n * var bbox = turf.bbox(line);\n * var bboxPolygon = turf.bboxPolygon(bbox);\n *\n * //addToMap\n * var addToMap = [line, bboxPolygon]\n */\nfunction bbox(\n geojson: AllGeoJSON,\n options: {\n recompute?: boolean;\n } = {}\n): BBox {\n if (geojson.bbox != null && true !== options.recompute) {\n return geojson.bbox;\n }\n const result: BBox = [Infinity, Infinity, -Infinity, -Infinity];\n coordEach(geojson, (coord) => {\n if (result[0] > coord[0]) {\n result[0] = coord[0];\n }\n if (result[1] > coord[1]) {\n result[1] = coord[1];\n }\n if (result[2] < coord[0]) {\n result[2] = coord[0];\n }\n if (result[3] < coord[1]) {\n result[3] = coord[1];\n }\n });\n return result;\n}\n\nexport { bbox };\nexport default bbox;\n", "import { select as d3_select } from 'd3-selection';\nimport { t } from '../core/localizer';\n\nimport { geoScaleToZoom, geoVecLength } from '../geo';\nimport { utilPrefixCSSProperty, utilTiler } from '../util';\n\n\nexport function rendererTileLayer(context) {\n var transformProp = utilPrefixCSSProperty('Transform');\n var tiler = utilTiler();\n\n var _tileSize = 256;\n var _projection;\n var _cache = {};\n var _tileOrigin;\n var _zoom;\n var _source;\n var _underzoom = 0;\n\n function tileSizeAtZoom(d, z) {\n return (d.tileSize * Math.pow(2, z - d[2])) / d.tileSize;\n }\n\n\n function atZoom(t, distance) {\n var power = Math.pow(2, distance);\n return [\n Math.floor(t[0] * power),\n Math.floor(t[1] * power),\n t[2] + distance\n ];\n }\n\n\n function lookUp(d) {\n for (var up = -1; up > -d[2]; up--) {\n var tile = atZoom(d, up);\n if (_cache[_source.url(tile)] !== false) {\n return tile;\n }\n }\n }\n\n\n function uniqueBy(a, n) {\n var o = [];\n var seen = {};\n for (var i = 0; i < a.length; i++) {\n if (seen[a[i][n]] === undefined) {\n o.push(a[i]);\n seen[a[i][n]] = true;\n }\n }\n return o;\n }\n\n\n function addSource(d) {\n d.url = _source.url(d);\n d.tileSize = _tileSize;\n d.source = _source;\n return d;\n }\n\n\n // Update tiles based on current state of `projection`.\n function background(selection) {\n _zoom = geoScaleToZoom(_projection.scale(), _tileSize);\n\n var pixelOffset;\n if (_source) {\n pixelOffset = [\n _source.offset()[0] * Math.pow(2, _zoom),\n _source.offset()[1] * Math.pow(2, _zoom)\n ];\n } else {\n pixelOffset = [0, 0];\n }\n\n\n tiler\n .scale(_projection.scale() * 2 * Math.PI)\n .translate([\n _projection.translate()[0] + pixelOffset[0],\n _projection.translate()[1] + pixelOffset[1]\n ]);\n\n _tileOrigin = [\n _projection.scale() * Math.PI - _projection.translate()[0],\n _projection.scale() * Math.PI - _projection.translate()[1]\n ];\n\n render(selection);\n }\n\n\n // Derive the tiles onscreen, remove those offscreen and position them.\n // Important that this part not depend on `_projection` because it's\n // rendered when tiles load/error (see #644).\n function render(selection) {\n if (!_source) return;\n var requests = [];\n var showDebug = context.getDebug('tile') && !_source.overlay;\n\n if (_source.validZoom(_zoom, _underzoom)) {\n tiler.skipNullIsland(!!_source.overlay);\n\n tiler().forEach(function(d) {\n addSource(d);\n if (d.url === '') return;\n if (typeof d.url !== 'string') return; // Workaround for #2295\n requests.push(d);\n if (_cache[d.url] === false && lookUp(d)) {\n requests.push(addSource(lookUp(d)));\n }\n });\n\n requests = uniqueBy(requests, 'url').filter(function(r) {\n // don't re-request tiles which have failed in the past\n return _cache[r.url] !== false;\n });\n }\n\n function load(d3_event, d) {\n _cache[d.url] = true;\n d3_select(this)\n .on('error', null)\n .on('load', null);\n render(selection);\n }\n\n function error(d3_event, d) {\n _cache[d.url] = false;\n d3_select(this)\n .on('error', null)\n .on('load', null)\n .remove();\n render(selection);\n }\n\n function imageTransform(d) {\n var ts = d.tileSize * Math.pow(2, _zoom - d[2]);\n var scale = tileSizeAtZoom(d, _zoom);\n return 'translate(' +\n ((d[0] * ts + d.source.offset()[0] * Math.pow(2, _zoom)) * _tileSize / d.tileSize - _tileOrigin[0]\n ) + 'px,' +\n ((d[1] * ts + d.source.offset()[1] * Math.pow(2, _zoom)) * _tileSize / d.tileSize - _tileOrigin[1]\n ) + 'px) ' +\n 'scale(' + scale * _tileSize / d.tileSize + ',' + scale * _tileSize / d.tileSize + ')';\n }\n\n function tileCenter(d) {\n var ts = d.tileSize * Math.pow(2, _zoom - d[2]);\n return [\n ((d[0] * ts) - _tileOrigin[0] + (ts / 2)),\n ((d[1] * ts) - _tileOrigin[1] + (ts / 2))\n ];\n }\n\n function debugTransform(d) {\n var coord = tileCenter(d);\n return 'translate(' + coord[0] + 'px,' + coord[1] + 'px)';\n }\n\n\n // Pick a representative tile near the center of the viewport\n // (This is useful for sampling the imagery vintage)\n var dims = tiler.size();\n var mapCenter = [dims[0] / 2, dims[1] / 2];\n var minDist = Math.max(dims[0], dims[1]);\n var nearCenter;\n\n requests.forEach(function(d) {\n var c = tileCenter(d);\n var dist = geoVecLength(c, mapCenter);\n if (dist < minDist) {\n minDist = dist;\n nearCenter = d;\n }\n });\n\n\n var image = selection.selectAll('img')\n .data(requests, function(d) { return d.url; });\n\n image.exit()\n .style(transformProp, imageTransform)\n .classed('tile-removing', true)\n .classed('tile-center', false)\n .on('transitionend', function() {\n const tile = d3_select(this);\n if (tile.classed('tile-removing')) {\n tile.remove();\n }\n });\n\n image.enter()\n .append('img')\n .attr('class', 'tile')\n .attr('alt', '')\n .attr('draggable', 'false')\n .style('width', _tileSize + 'px')\n .style('height', _tileSize + 'px')\n .attr('src', function(d) { return d.url; })\n .on('error', error)\n .on('load', load)\n .merge(image)\n .style(transformProp, imageTransform)\n .classed('tile-debug', showDebug)\n .classed('tile-removing', false)\n .classed('tile-center', function(d) { return d === nearCenter; })\n .sort((a, b) => a[2] - b[2]);\n\n\n\n var debug = selection.selectAll('.tile-label-debug')\n .data(showDebug ? requests : [], function(d) { return d.url; });\n\n debug.exit()\n .remove();\n\n if (showDebug) {\n var debugEnter = debug.enter()\n .append('div')\n .attr('class', 'tile-label-debug');\n\n debugEnter\n .append('div')\n .attr('class', 'tile-label-debug-coord');\n\n debugEnter\n .append('div')\n .attr('class', 'tile-label-debug-vintage');\n\n debug = debug.merge(debugEnter);\n\n debug\n .style(transformProp, debugTransform);\n\n debug\n .selectAll('.tile-label-debug-coord')\n .text(function(d) { return d[2] + ' / ' + d[0] + ' / ' + d[1]; });\n\n debug\n .selectAll('.tile-label-debug-vintage')\n .each(function(d) {\n var span = d3_select(this);\n var center = context.projection.invert(tileCenter(d));\n _source.getMetadata(center, d, function(err, result) {\n if (result && result.vintage && result.vintage.range) {\n span.text(result.vintage.range);\n } else {\n span.text('');\n span.call(t.append('info_panels.background.vintage'));\n span.append('span').text(': ');\n span.call(t.append('info_panels.background.unknown'));\n }\n });\n });\n }\n\n }\n\n\n background.projection = function(val) {\n if (!arguments.length) return _projection;\n _projection = val;\n return background;\n };\n\n\n background.dimensions = function(val) {\n if (!arguments.length) return tiler.size();\n tiler.size(val);\n return background;\n };\n\n\n background.source = function(val) {\n if (!arguments.length) return _source;\n _source = val;\n _tileSize = _source.tileSize;\n _cache = {};\n tiler.tileSize(_source.tileSize).zoomExtent(_source.zoomExtent);\n return background;\n };\n\n\n background.underzoom = function(amount) {\n if (!arguments.length) return _underzoom;\n _underzoom = amount;\n return background;\n };\n\n\n return background;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate';\nimport { select as d3_select } from 'd3-selection';\nimport turf_bboxClip from '@turf/bbox-clip';\nimport turf_bbox from '@turf/bbox';\n\nimport whichPolygon from 'which-polygon';\n\nimport { prefs } from '../core/preferences';\nimport { fileFetcher } from '../core/file_fetcher';\nimport { geoMetersToOffset, geoOffsetToMeters, geoExtent } from '../geo';\nimport { rendererBackgroundSource } from './background_source';\nimport { rendererTileLayer } from './tile_layer';\nimport { utilQsString, utilStringQs } from '../util';\nimport { utilRebind } from '../util/rebind';\n\n\nlet _imageryIndex = null;\nlet _waybackIndex = null;\n\nexport function rendererBackground(context) {\n const dispatch = d3_dispatch('change');\n const baseLayer = rendererTileLayer(context).projection(context.projection);\n let _checkedBlocklists = [];\n let _isValid = true;\n let _overlayLayers = [];\n let _brightness = 1;\n let _contrast = 1;\n let _saturation = 1;\n let _sharpness = 1;\n\n\n function ensureImageryIndex() {\n return fileFetcher.get('wayback')\n .then(groups => {\n // eslint-disable-next-line no-warning-comments\n // TODO: Follow pagination via nextStart property.\n // Extracts the layer's date from the title.\n let extractDateFromTitle = title => {\n const dateComponents = title.match(/\\(Wayback (\\d{4})-(\\d\\d)-(\\d\\d)\\)/);\n if (!dateComponents) return;\n return new Date(Date.UTC(parseInt(dateComponents[1], 10),\n parseInt(dateComponents[2], 10) - 1,\n parseInt(dateComponents[3], 10)));\n };\n\n if (!_waybackIndex) {\n // Index the metadata MapServer URLs by the date of the World Imagery map.\n let metadataMapServersByDate = Object.fromEntries(groups.items\n .filter(item => item.type === 'Map Service')\n .map(item => {\n // Extract the layer's date from the title to avoid having to hit each MapServer right away.\n const date = extractDateFromTitle(item.title) || new Date(item.created);\n const dateString = date.toISOString().split('T')[0];\n return [dateString, item.url];\n }));\n\n _waybackIndex = groups.items\n .filter(item => item.type === 'WMTS')\n .map(item => {\n const date = extractDateFromTitle(item.title) || new Date(item.created);\n const dateString = date && date.toISOString().split('T')[0];\n\n // Convert the bounding box to a polygon.\n const bbox = {\n min_lon: item.extent[0][0],\n min_lat: item.extent[0][1],\n max_lon: item.extent[1][0],\n max_lat: item.extent[1][1],\n };\n const polygon = [[\n [bbox.min_lon, bbox.min_lat],\n [bbox.min_lon, bbox.max_lat],\n [bbox.max_lon, bbox.max_lat],\n [bbox.max_lon, bbox.min_lat],\n [bbox.min_lon, bbox.min_lat],\n ]];\n\n // Convert placeholder tokens in the URL template from Esri's format to OSM's.\n const template = item.url\n .replaceAll('{level}', '{zoom}')\n .replaceAll('{row}', '{y}')\n .replaceAll('{col}', '{x}');\n\n return {\n id: 'EsriWorldImagery_' + dateString,\n name: item.title,\n type: 'tms',\n template: template,\n metadata: metadataMapServersByDate[dateString],\n startDate: date.toISOString(),\n endDate: date.toISOString(),\n polygon: polygon,\n terms_text: item.accessInformation,\n description: item.snippet,\n // Match Esri World Imagery layer\n 'default': true,\n zoomExtent: [0, 22],\n terms_url: 'https://wiki.openstreetmap.org/wiki/Esri',\n icon: 'https://osmlab.github.io/editor-layer-index/sources/world/EsriImageryClarity.png',\n };\n });\n }\n return fileFetcher.get('imagery');\n })\n .catch(() => {\n return fileFetcher.get('imagery');\n })\n .then(sources => {\n if (_imageryIndex) return _imageryIndex;\n\n // Append Esri World Imagery Wayback sources.\n if (_waybackIndex) {\n sources.push(..._waybackIndex);\n }\n\n _imageryIndex = {\n imagery: sources,\n features: {}\n };\n\n // use which-polygon to support efficient index and querying for imagery\n const features = sources.map(source => {\n if (!source.polygon) return null;\n // workaround for editor-layer-index weirdness..\n // Add an extra array nest to each element in `source.polygon`\n // so the rings are not treated as a bunch of holes:\n // what we have: [ [[outer],[hole],[hole]] ]\n // what we want: [ [[outer]],[[outer]],[[outer]] ]\n const rings = source.polygon.map(ring => [ring]);\n\n const feature = {\n type: 'Feature',\n properties: { id: source.id },\n geometry: { type: 'MultiPolygon', coordinates: rings }\n };\n\n _imageryIndex.features[source.id] = feature;\n return feature;\n\n }).filter(Boolean);\n\n _imageryIndex.query = whichPolygon({ type: 'FeatureCollection', features: features });\n\n\n // Instantiate `rendererBackgroundSource` objects for each source\n _imageryIndex.backgrounds = sources.map(source => {\n if (source.type === 'bing') {\n return rendererBackgroundSource.Bing(source, dispatch);\n } else if (/^EsriWorldImagery/.test(source.id)) {\n return rendererBackgroundSource.Esri(source);\n } else {\n return rendererBackgroundSource(source);\n }\n });\n\n // Add 'None'\n _imageryIndex.backgrounds.unshift(rendererBackgroundSource.None());\n\n // Add 'Custom'\n let template = prefs('background-custom-template') || '';\n const custom = rendererBackgroundSource.Custom(template);\n _imageryIndex.backgrounds.unshift(custom);\n\n return _imageryIndex;\n });\n }\n\n\n function background(selection) {\n const currSource = baseLayer.source();\n\n // If we are displaying an Esri basemap at high zoom,\n // check its tilemap to see how high the zoom can go\n if (context.map().zoom() > 18) {\n if (currSource && /^EsriWorldImagery/.test(currSource.id)) {\n const center = context.map().center();\n currSource.fetchTilemap(center);\n }\n }\n\n // Is the imagery valid here? - #4827\n const sources = background.sources(context.map().extent());\n const wasValid = _isValid;\n _isValid = !!sources.filter(d => d === currSource).length;\n\n if (wasValid !== _isValid) { // change in valid status\n background.updateImagery();\n }\n\n\n let baseFilter = '';\n if (_brightness !== 1) {\n baseFilter += ` brightness(${_brightness})`;\n }\n if (_contrast !== 1) {\n baseFilter += ` contrast(${_contrast})`;\n }\n if (_saturation !== 1) {\n baseFilter += ` saturate(${_saturation})`;\n }\n if (_sharpness < 1) { // gaussian blur\n const blur = d3_interpolateNumber(0.5, 5)(1 - _sharpness);\n baseFilter += ` blur(${blur}px)`;\n }\n\n let base = selection.selectAll('.layer-background')\n .data([0]);\n\n base = base.enter()\n .insert('div', '.layer-data')\n .attr('class', 'layer layer-background')\n .merge(base);\n\n base.style('filter', baseFilter || null);\n\n\n let imagery = base.selectAll('.layer-imagery')\n .data([0]);\n\n imagery.enter()\n .append('div')\n .attr('class', 'layer layer-imagery')\n .merge(imagery)\n .call(baseLayer);\n\n\n let maskFilter = '';\n let mixBlendMode = '';\n if (_sharpness > 1) { // apply unsharp mask\n mixBlendMode = 'overlay';\n maskFilter = 'saturate(0) blur(3px) invert(1)';\n\n let contrast = _sharpness - 1;\n maskFilter += ` contrast(${contrast})`;\n\n let brightness = d3_interpolateNumber(1, 0.85)(_sharpness - 1);\n maskFilter += ` brightness(${brightness})`;\n }\n\n let mask = base.selectAll('.layer-unsharp-mask')\n .data(_sharpness > 1 ? [0] : []);\n\n mask.exit()\n .remove();\n\n mask.enter()\n .append('div')\n .attr('class', 'layer layer-mask layer-unsharp-mask')\n .merge(mask)\n .call(baseLayer)\n .style('filter', maskFilter || null)\n .style('mix-blend-mode', mixBlendMode || null);\n\n\n let overlays = selection.selectAll('.layer-overlay')\n .data(_overlayLayers, d => d.source().name());\n\n overlays.exit()\n .remove();\n\n overlays.enter()\n .insert('div', '.layer-data')\n .attr('class', 'layer layer-overlay')\n .merge(overlays)\n .each((layer, i, nodes) => d3_select(nodes[i]).call(layer));\n }\n\n\n background.updateImagery = function() {\n let currSource = baseLayer.source();\n if (context.inIntro() || !currSource) return;\n\n let o = _overlayLayers\n .filter(d => !d.source().isLocatorOverlay() && !d.source().isHidden())\n .map(d => d.source().id)\n .join(',');\n\n const meters = geoOffsetToMeters(currSource.offset());\n const EPSILON = 0.01;\n const x = +meters[0].toFixed(2);\n const y = +meters[1].toFixed(2);\n let hash = utilStringQs(window.location.hash);\n\n let id = currSource.id;\n if (id === 'custom') {\n id = `custom:${currSource.template()}`;\n }\n\n if (id) {\n hash.background = id;\n } else {\n delete hash.background;\n }\n\n if (o) {\n hash.overlays = o;\n } else {\n delete hash.overlays;\n }\n\n if (Math.abs(x) > EPSILON || Math.abs(y) > EPSILON) {\n hash.offset = `${x},${y}`;\n } else {\n delete hash.offset;\n }\n\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n\n let imageryUsed = [];\n let photoOverlaysUsed = [];\n\n const currUsed = currSource.imageryUsed();\n if (currUsed && _isValid) {\n imageryUsed.push(currUsed);\n }\n\n _overlayLayers\n .filter(d => !d.source().isLocatorOverlay() && !d.source().isHidden())\n .forEach(d => imageryUsed.push(d.source().imageryUsed()));\n\n const dataLayer = context.layers().layer('data');\n if (dataLayer && dataLayer.enabled() && dataLayer.hasData()) {\n imageryUsed.push(dataLayer.getSrc());\n }\n\n const photoOverlayLayers = {\n streetside: 'Bing Streetside',\n mapillary: 'Mapillary Images',\n 'mapillary-map-features': 'Mapillary Map Features',\n 'mapillary-signs': 'Mapillary Signs',\n kartaview: 'KartaView Images',\n vegbilder: 'Norwegian Road Administration Images',\n mapilio: 'Mapilio Images',\n panoramax: 'Panoramax Images'\n };\n\n for (let layerID in photoOverlayLayers) {\n const layer = context.layers().layer(layerID);\n if (layer && layer.enabled()) {\n photoOverlaysUsed.push(layerID);\n imageryUsed.push(photoOverlayLayers[layerID]);\n }\n }\n\n context.history().imageryUsed(imageryUsed);\n context.history().photoOverlaysUsed(photoOverlaysUsed);\n };\n\n\n background.sources = (extent, zoom, includeCurrent) => {\n if (!_imageryIndex) return []; // called before init()?\n\n let visible = {};\n (_imageryIndex.query.bbox(extent.rectangle(), true) || [])\n .forEach(d => visible[d.id] = true);\n\n const currSource = baseLayer.source();\n\n // Recheck blocked sources only if we detect new blocklists pulled from the OSM API.\n const osm = context.connection();\n const blocklists = (osm && osm.imageryBlocklists()) || [];\n const blocklistChanged = (blocklists.length !== _checkedBlocklists.length) ||\n blocklists.some((regex, index) => String(regex) !== _checkedBlocklists[index]);\n\n if (blocklistChanged) {\n _imageryIndex.backgrounds.forEach(source => {\n source.isBlocked = blocklists.some(regex => regex.test(source.template()));\n });\n _checkedBlocklists = blocklists.map(regex => String(regex));\n }\n\n return _imageryIndex.backgrounds.filter(source => {\n if (includeCurrent && currSource === source) return true; // optionally always include the current imagery\n if (source.isBlocked) return false; // even bundled sources may be blocked - #7905\n if (!source.polygon) return true; // always include imagery with worldwide coverage\n if (zoom && zoom < 6) return false; // optionally exclude local imagery at low zooms\n return visible[source.id]; // include imagery visible in given extent\n });\n };\n\n\n background.dimensions = (val) => {\n if (!val) return;\n baseLayer.dimensions(val);\n _overlayLayers.forEach(layer => layer.dimensions(val));\n };\n\n\n background.baseLayerSource = function(d) {\n if (!arguments.length) return baseLayer.source();\n\n // test source against OSM imagery blocklists..\n const osm = context.connection();\n if (!osm) return background;\n\n const blocklists = osm.imageryBlocklists();\n const template = d.template();\n let fail = false;\n let tested = 0;\n let regex;\n\n for (let i = 0; i < blocklists.length; i++) {\n regex = blocklists[i];\n fail = regex.test(template);\n tested++;\n if (fail) break;\n }\n\n // ensure at least one test was run.\n if (!tested) {\n regex = /.*\\.google(apis)?\\..*\\/(vt|kh)[\\?\\/].*([xyz]=.*){3}.*/;\n fail = regex.test(template);\n }\n\n baseLayer.source(!fail ? d : background.findSource('none'));\n dispatch.call('change');\n background.updateImagery();\n return background;\n };\n\n\n background.findSource = (id) => {\n if (!id || !_imageryIndex) return null; // called before init()?\n return _imageryIndex.backgrounds.find(d => d.id && d.id === id);\n };\n\n\n background.bing = () => {\n background.baseLayerSource(background.findSource('Bing'));\n };\n\n\n background.showsLayer = (d) => {\n const currSource = baseLayer.source();\n if (!d || !currSource) return false;\n return d.id === currSource.id || _overlayLayers.some(layer => d.id === layer.source().id);\n };\n\n\n background.overlayLayerSources = () => {\n return _overlayLayers.map(layer => layer.source());\n };\n\n\n background.toggleOverlayLayer = (d) => {\n let layer;\n for (let i = 0; i < _overlayLayers.length; i++) {\n layer = _overlayLayers[i];\n if (layer.source() === d) {\n _overlayLayers.splice(i, 1);\n dispatch.call('change');\n background.updateImagery();\n return;\n }\n }\n\n layer = rendererTileLayer(context)\n .source(d)\n .projection(context.projection)\n .dimensions(baseLayer.dimensions()\n );\n\n _overlayLayers.push(layer);\n dispatch.call('change');\n background.updateImagery();\n };\n\n\n background.nudge = (d, zoom) => {\n const currSource = baseLayer.source();\n if (currSource) {\n currSource.nudge(d, zoom);\n dispatch.call('change');\n background.updateImagery();\n }\n return background;\n };\n\n\n background.offset = function(d) {\n const currSource = baseLayer.source();\n if (!arguments.length) {\n return (currSource && currSource.offset()) || [0, 0];\n }\n if (currSource) {\n currSource.offset(d);\n dispatch.call('change');\n background.updateImagery();\n }\n return background;\n };\n\n\n background.brightness = function(d) {\n if (!arguments.length) return _brightness;\n _brightness = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n\n background.contrast = function(d) {\n if (!arguments.length) return _contrast;\n _contrast = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n\n background.saturation = function(d) {\n if (!arguments.length) return _saturation;\n _saturation = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n\n background.sharpness = function(d) {\n if (!arguments.length) return _sharpness;\n _sharpness = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n let _loadPromise;\n\n background.ensureLoaded = () => {\n if (_loadPromise) return _loadPromise;\n\n return _loadPromise = ensureImageryIndex();\n };\n\n background.init = () => {\n const loadPromise = background.ensureLoaded();\n\n const hash = utilStringQs(window.location.hash);\n const requestedBackground = hash.background || hash.layer;\n const lastUsedBackground = prefs('background-last-used');\n\n return loadPromise.then(imageryIndex => {\n const extent = context.map().extent();\n const validBackgrounds = background.sources(extent).filter(d => d.id !== 'none' && d.id !== 'custom');\n const first = validBackgrounds.length && validBackgrounds[0];\n const isLastUsedValid = !!validBackgrounds.find(d => d.id && d.id === lastUsedBackground);\n\n let best;\n if (!requestedBackground && extent) {\n const viewArea = extent.area();\n best = validBackgrounds.find(s => {\n if (!s.best() || s.overlay) return false;\n let bbox = turf_bbox(turf_bboxClip(\n { type: 'MultiPolygon', coordinates: [ s.polygon || [extent.polygon()] ] },\n extent.rectangle()));\n let area = geoExtent(bbox.slice(0,2), bbox.slice(2,4)).area();\n return area / viewArea > 0.5; // min visible size: 50% of viewport area\n });\n }\n\n // Decide which background layer to display\n if (requestedBackground && requestedBackground.indexOf('custom:') === 0) {\n const template = requestedBackground.replace(/^custom:/, '');\n const custom = background.findSource('custom');\n background.baseLayerSource(custom.template(template));\n prefs('background-custom-template', template);\n } else {\n background.baseLayerSource(\n background.findSource(requestedBackground) ||\n best ||\n isLastUsedValid && background.findSource(lastUsedBackground) ||\n background.findSource('Bing') ||\n first ||\n background.findSource('none')\n );\n }\n\n const locator = imageryIndex.backgrounds.find(d => d.overlay && d.default);\n if (locator) {\n background.toggleOverlayLayer(locator);\n }\n\n const overlays = (hash.overlays || '').split(',');\n overlays.forEach(overlay => {\n overlay = background.findSource(overlay);\n if (overlay) {\n background.toggleOverlayLayer(overlay);\n }\n });\n\n if (hash.gpx) {\n const gpx = context.layers().layer('data');\n if (gpx) {\n gpx.url(hash.gpx, '.gpx');\n }\n }\n\n if (hash.offset) {\n const offset = hash.offset\n .replace(/;/g, ',')\n .split(',')\n .map(n => !isNaN(n) && n);\n\n if (offset.length === 2) {\n background.offset(geoMetersToOffset(offset));\n }\n }\n })\n .catch(err => {\n /* eslint-disable no-console */\n console.error(err);\n /* eslint-enable no-console */\n });\n };\n\n\n return utilRebind(background, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from '../core/preferences';\nimport { osmEntity } from '../osm';\nimport { osmLanduseTags, osmLifecyclePrefixes } from '../osm/tags.js';\nimport { utilRebind } from '../util/rebind';\nimport { utilArrayGroupBy, utilArrayUnion, utilQsString, utilStringQs, utilDatesOverlap } from '../util';\nimport { isAddressPoint } from '../svg/labels';\n\n\nexport function rendererFeatures(context) {\n var dispatch = d3_dispatch('change', 'redraw');\n const features = {};\n var _deferred = new Set();\n\n var traffic_roads = {\n 'motorway': true,\n 'motorway_link': true,\n 'trunk': true,\n 'trunk_link': true,\n 'primary': true,\n 'primary_link': true,\n 'secondary': true,\n 'secondary_link': true,\n 'tertiary': true,\n 'tertiary_link': true,\n 'residential': true,\n 'unclassified': true,\n 'living_street': true,\n 'busway': true\n };\n\n var service_roads = {\n 'service': true,\n 'road': true,\n 'track': true\n };\n\n var paths = {\n 'path': true,\n 'footway': true,\n 'cycleway': true,\n 'bridleway': true,\n 'steps': true,\n 'ladder': true,\n 'pedestrian': true\n };\n\n var _cullFactor = 1;\n var _cache = {};\n var _rules = {};\n var _dateMatchCount = 0;\n var _stats = {};\n var _keys = [];\n var _hidden = [];\n var _forceVisible = {};\n\n\n function update() {\n const hash = utilStringQs(window.location.hash);\n const disabled = features.disabled();\n if (disabled.length) {\n hash.disable_features = disabled.join(',');\n } else {\n delete hash.disable_features;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n prefs('disabled-features', disabled.join(','));\n _hidden = features.hidden();\n dispatch.call('change');\n dispatch.call('redraw');\n }\n\n\n /**\n * @callback FilterFunction\n * @param {Record} tags\n * @param {string} [geometry]\n * @returns {boolean}\n */\n\n /**\n * @param {string} k\n * @param {FilterFunction} filter\n * @param {number} [max]\n */\n function defineRule(k, filter, max) {\n var isEnabled = true;\n\n _keys.push(k);\n _rules[k] = {\n filter: filter,\n enabled: isEnabled, // whether the user wants it enabled..\n count: 0,\n currentMax: (max || Infinity),\n defaultMax: (max || Infinity),\n enable: function() { this.enabled = true; this.currentMax = this.defaultMax; },\n disable: function() { this.enabled = false; this.currentMax = 0; },\n hidden: function() {\n return (this.count === 0 && !this.enabled) ||\n this.count > this.currentMax * _cullFactor;\n },\n autoHidden: function() { return this.hidden() && this.currentMax > 0; }\n };\n }\n\n defineRule('address_points', (tags, geometry) =>\n geometry === 'point' && isAddressPoint(tags),\n 100);\n\n defineRule('points', (tags, geometry) =>\n geometry === 'point' && !isAddressPoint(tags, geometry),\n 200);\n\n defineRule('traffic_roads', function isTrafficRoad(tags) {\n return traffic_roads[tags.highway];\n });\n\n defineRule('service_roads', function isServiceRoad(tags) {\n return service_roads[tags.highway];\n });\n\n defineRule('paths', function isPath(tags) {\n return paths[tags.highway];\n });\n\n defineRule('buildings', function isBuilding(tags) {\n return (\n (!!tags.building && tags.building !== 'no') ||\n tags.parking === 'multi-storey' ||\n tags.parking === 'sheds' ||\n tags.parking === 'carports' ||\n tags.parking === 'garage_boxes'\n );\n }, 250);\n\n defineRule('building_parts', function isBuildingPart(tags) {\n return !!tags['building:part'];\n });\n\n defineRule('indoor', function isIndoor(tags) {\n return (\n (!!tags.indoor && tags.indoor !== 'no') ||\n (!!tags.indoormark && tags.indoormark !== 'no')\n );\n });\n\n defineRule('landuse', function isLanduse(tags, geometry) {\n if (geometry !== 'area') return false;\n let hasLanduseTag = false;\n for (const key in osmLanduseTags) {\n if (osmLanduseTags[key] === true && tags[key] ||\n osmLanduseTags[key][tags[key]] === true) {\n hasLanduseTag = true;\n }\n }\n return hasLanduseTag &&\n !_rules.buildings.filter(tags) &&\n !_rules.building_parts.filter(tags) &&\n !_rules.indoor.filter(tags) &&\n !_rules.water.filter(tags) &&\n !_rules.pistes.filter(tags);\n });\n\n defineRule('boundaries', function isBoundary(tags, geometry) {\n // This rule applies if the object has no interesting tags, and if either:\n // (a) is a way having a `boundary=*` tag, or\n // (b) is a relation of `type=boundary`.\n return (\n (geometry === 'line' && !!tags.boundary) ||\n (geometry === 'relation' && tags.type === 'boundary')\n ) && !(\n traffic_roads[tags.highway] ||\n service_roads[tags.highway] ||\n paths[tags.highway] ||\n tags.waterway ||\n tags.railway ||\n tags.landuse ||\n tags.natural ||\n tags.building ||\n tags.power\n );\n });\n\n defineRule('water', function isWater(tags) {\n return (\n !!tags.waterway ||\n tags.natural === 'water' ||\n tags.natural === 'coastline' ||\n tags.natural === 'bay' ||\n tags.landuse === 'pond' ||\n tags.landuse === 'basin' ||\n tags.landuse === 'reservoir' ||\n tags.landuse === 'salt_pond'\n );\n });\n\n defineRule('rail', function isRail(tags) {\n return (\n !!tags.railway ||\n tags.landuse === 'railway'\n ) && !(\n traffic_roads[tags.highway] ||\n service_roads[tags.highway] ||\n paths[tags.highway]\n );\n });\n\n defineRule('pistes', function isPiste(tags) {\n return tags['piste:type'];\n });\n\n defineRule('aerialways', function isAerialways(tags) {\n return !!tags?.aerialway &&\n tags.aerialway !== 'yes' &&\n tags.aerialway !== 'station';\n });\n\n defineRule('power', function isPower(tags) {\n return !!tags.power;\n });\n\n // contains a past/future tag, but not in active use as a road/path/cycleway/etc..\n defineRule('past_future', function isPastFuture(tags) {\n if (\n traffic_roads[tags.highway] ||\n service_roads[tags.highway] ||\n paths[tags.highway]\n ) { return false; }\n\n const keys = Object.keys(tags);\n\n for (const key of keys) {\n if (osmLifecyclePrefixes[tags[key]]) return true; // legacy tagging, e.g. `highway=construction`\n const parts = key.split(':');\n if (parts.length === 1) continue;\n const prefix = parts[0];\n if (osmLifecyclePrefixes[prefix]) return true; // lifecycle tagging, e.g. `demolished:building=yes`\n }\n return false;\n });\n\n // Lines or areas that don't match another feature filter.\n // IMPORTANT: The 'others' feature must be the last one defined,\n // so that code in getMatches can skip this test if `hasMatch = true`\n defineRule('others', function isOther(tags, geometry) {\n return (geometry === 'line' || geometry === 'area');\n });\n\n\n\n features.features = function() {\n return _rules;\n };\n\n\n features.keys = function() {\n return _keys;\n };\n\n\n features.enabled = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return _rules[k].enabled; });\n }\n return _rules[k] && _rules[k].enabled;\n };\n\n\n features.disabled = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return !_rules[k].enabled; });\n }\n return _rules[k] && !_rules[k].enabled;\n };\n\n\n features.hidden = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return _rules[k].hidden(); });\n }\n return _rules[k]?.hidden();\n };\n\n\n features.autoHidden = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return _rules[k].autoHidden(); });\n }\n return _rules[k] && _rules[k].autoHidden();\n };\n\n\n features.enable = function(k) {\n if (_rules[k] && !_rules[k].enabled) {\n _rules[k].enable();\n update();\n }\n };\n\n features.enableAll = function() {\n var didEnable = false;\n for (var k in _rules) {\n if (!_rules[k].enabled) {\n didEnable = true;\n _rules[k].enable();\n }\n }\n if (didEnable) update();\n };\n\n\n features.disable = function(k) {\n if (_rules[k] && _rules[k].enabled) {\n _rules[k].disable();\n update();\n }\n };\n\n features.disableAll = function() {\n var didDisable = false;\n for (var k in _rules) {\n if (_rules[k].enabled) {\n didDisable = true;\n _rules[k].disable();\n }\n }\n if (didDisable) update();\n };\n\n\n features.toggle = function(k) {\n if (_rules[k]) {\n (function(f) { return f.enabled ? f.disable() : f.enable(); }(_rules[k]));\n update();\n }\n };\n\n\n features.redraw = function() {\n update();\n };\n\n\n features.resetStats = function() {\n for (var i = 0; i < _keys.length; i++) {\n _rules[_keys[i]].count = 0;\n }\n _dateMatchCount = 0;\n dispatch.call('change');\n };\n\n\n features.gatherStats = function(d, resolver, dimensions) {\n var needsRedraw = false;\n var types = utilArrayGroupBy(d, 'type');\n var entities = [].concat(types.relation || [], types.way || [], types.node || []);\n var currHidden, geometry, matches, i, j;\n\n for (i = 0; i < _keys.length; i++) {\n _rules[_keys[i]].count = 0;\n }\n _dateMatchCount = 0;\n\n // adjust the threshold for point/building culling based on viewport size..\n // a _cullFactor of 1 corresponds to a 1000x1000px viewport..\n _cullFactor = dimensions[0] * dimensions[1] / 1000000;\n\n for (i = 0; i < entities.length; i++) {\n geometry = entities[i].geometry(resolver);\n matches = Object.keys(features.getMatches(entities[i], resolver, geometry));\n for (j = 0; j < matches.length; j++) {\n _rules[matches[j]].count++;\n }\n if (!features.featureFitsDateRange(entities[i])) _dateMatchCount++;\n }\n\n currHidden = features.hidden();\n if (currHidden !== _hidden) {\n _hidden = currHidden;\n needsRedraw = true;\n dispatch.call('change');\n }\n\n return needsRedraw;\n };\n\n\n features.stats = function() {\n for (var i = 0; i < _keys.length; i++) {\n _stats[_keys[i]] = _rules[_keys[i]].count;\n }\n\n return _stats;\n };\n\n\n features.dateMatchCount = () => _dateMatchCount;\n\n\n features.clear = function(d) {\n for (var i = 0; i < d.length; i++) {\n features.clearEntity(d[i]);\n }\n };\n\n\n features.clearEntity = function(entity) {\n delete _cache[osmEntity.key(entity)];\n for (const key in _cache) {\n if (_cache[key].parents) {\n for (const parent of _cache[key].parents) {\n if (parent.id === entity.id) {\n delete _cache[key];\n break;\n }\n }\n }\n }\n };\n\n\n features.reset = function() {\n Array.from(_deferred).forEach(function(handle) {\n window.cancelIdleCallback(handle);\n _deferred.delete(handle);\n });\n\n _cache = {};\n };\n\n // only certain relations are worth checking\n function relationShouldBeChecked(relation) {\n // multipolygon features have `area` geometry and aren't checked here\n return relation.tags.type === 'boundary';\n }\n\n features.getMatches = function(entity, resolver, geometry) {\n if (geometry === 'vertex' ||\n (geometry === 'relation' && !relationShouldBeChecked(entity))) return {};\n\n var ent = osmEntity.key(entity);\n if (!_cache[ent]) {\n _cache[ent] = {};\n }\n\n if (!_cache[ent].matches) {\n var matches = {};\n var hasMatch = false;\n\n for (var i = 0; i < _keys.length; i++) {\n if (_keys[i] === 'others') {\n if (hasMatch) continue;\n\n // If an entity...\n // 1. is a way that hasn't matched other 'interesting' feature rules,\n if (entity.type === 'way') {\n var parents = features.getParents(entity, resolver, geometry);\n\n // 2a. belongs only to a single multipolygon relation\n if ((parents.length === 1 && parents[0].isMultipolygon()) ||\n // 2b. or belongs only to boundary relations\n (parents.length > 0 && parents.every(function(parent) { return parent.tags.type === 'boundary'; }))) {\n\n // ...then match whatever feature rules the parent relation has matched.\n // see #2548, #2887\n //\n // IMPORTANT:\n // For this to work, getMatches must be called on relations before ways.\n //\n var pkey = osmEntity.key(parents[0]);\n if (_cache[pkey] && _cache[pkey].matches) {\n matches = Object.assign({}, _cache[pkey].matches); // shallow copy\n continue;\n }\n }\n }\n }\n\n if (_rules[_keys[i]].filter(entity.tags, geometry)) {\n matches[_keys[i]] = true;\n hasMatch = true;\n }\n }\n _cache[ent].matches = matches;\n }\n\n return _cache[ent].matches;\n };\n\n\n features.getParents = function(entity, resolver, geometry) {\n if (geometry === 'point') return [];\n\n const ent = osmEntity.key(entity);\n if (!_cache[ent]) {\n _cache[ent] = {};\n }\n\n if (!_cache[ent].parents) {\n let parents;\n if (geometry === 'vertex') {\n parents = resolver.parentWays(entity);\n } else { // 'line', 'area', 'relation'\n parents = resolver.parentRelations(entity);\n }\n _cache[ent].parents = parents;\n }\n\n return _cache[ent].parents;\n };\n\n\n features.isHiddenPreset = function(preset, geometry) {\n // if (!_hidden.length) return false;\n if (!preset.tags) return false;\n\n var test = preset.setTags({...preset.tags}, geometry);\n for (var key in _rules) {\n if (_rules[key].filter(test, geometry)) {\n if (_hidden.indexOf(key) !== -1) {\n return key;\n }\n return false;\n }\n }\n return false;\n };\n\n\n features.isHiddenFeature = function(entity, resolver, geometry) {\n // if (!_hidden.length) return false;\n if (!entity.version) return false;\n if (_forceVisible[entity.id]) return false;\n if (!features.featureFitsDateRange(entity)) return true;\n // if (!_hidden.length) return false;\n\n var matches = Object.keys(features.getMatches(entity, resolver, geometry));\n return matches.length && matches.every(function(k) { return features.hidden(k); });\n };\n\n\n features.isHiddenChild = function(entity, resolver, geometry) {\n // if (!_hidden.length) return false;\n if (!entity.version || geometry === 'point') return false;\n if (_forceVisible[entity.id]) return false;\n if (!features.featureFitsDateRange(entity)) return true;\n\n var parents = features.getParents(entity, resolver, geometry);\n if (!parents.length) return false;\n\n for (var i = 0; i < parents.length; i++) {\n if (!features.isHidden(parents[i], resolver, parents[i].geometry(resolver))) {\n return false;\n }\n }\n return true;\n };\n\n\n features.hasHiddenConnections = function(entity, resolver) {\n // if (!_hidden.length) return false;\n\n var childNodes, connections;\n if (entity.type === 'midpoint') {\n childNodes = [resolver.entity(entity.edge[0]), resolver.entity(entity.edge[1])];\n connections = [];\n } else {\n childNodes = entity.nodes ? resolver.childNodes(entity) : [];\n connections = features.getParents(entity, resolver, entity.geometry(resolver));\n }\n\n // gather ways connected to child nodes..\n connections = childNodes.reduce(function(result, e) {\n return resolver.isShared(e) ? utilArrayUnion(result, resolver.parentWays(e)) : result;\n }, connections);\n\n return connections.some(function(e) {\n return features.isHidden(e, resolver, e.geometry(resolver));\n });\n };\n\n\n features.isHidden = function(entity, resolver, geometry) {\n // if (!_hidden.length) return false;\n if (!entity.version) return false;\n\n var fn = (geometry === 'vertex' ? features.isHiddenChild : features.isHiddenFeature);\n return fn(entity, resolver, geometry);\n };\n\n\n features.featureFitsDateRange = function (entity) {\n if (!features.dateRange) return true; // no Date Range e.g. unit tests\n\n // entity's start & end date + the Date Range from the on-screen controls\n // utilDatesOverlap() treats malformed start_date/end_date as 9999/-9999\n // uiSectionDateRange already standardizes the dateRange inputs\n // so we don't need much validation here\n const entityRange = {\n 'start_date': entity.tags.start_date,\n 'end_date': entity.tags.end_date\n };\n const selectedRange = {\n 'start_date': features.dateRange[0],\n 'end_date': features.dateRange[1]\n };\n\n // out of range = feature started after range ends, or feature ends before range starts\n const withinrange = utilDatesOverlap(selectedRange, entityRange, true);\n return withinrange;\n };\n\n\n features.filter = function(d, resolver) {\n // if (!_hidden.length) return d;\n\n var result = [];\n for (var i = 0; i < d.length; i++) {\n var entity = d[i];\n if (!features.isHidden(entity, resolver, entity.geometry(resolver))) {\n result.push(entity);\n }\n }\n return result;\n };\n\n\n features.forceVisible = function(entityIDs) {\n if (!arguments.length) return Object.keys(_forceVisible);\n\n _forceVisible = {};\n for (var i = 0; i < entityIDs.length; i++) {\n _forceVisible[entityIDs[i]] = true;\n var entity = context.hasEntity(entityIDs[i]);\n if (entity && entity.type === 'relation') {\n // also show relation members (one level deep)\n for (var j in entity.members) {\n _forceVisible[entity.members[j].id] = true;\n }\n }\n }\n return features;\n };\n\n\n features.init = function() {\n var storage = prefs('disabled-features');\n if (storage) {\n var storageDisabled = storage.replace(/;/g, ',').split(',');\n storageDisabled.forEach(features.disable);\n }\n\n var hash = utilStringQs(window.location.hash);\n if (hash.disable_features) {\n var hashDisabled = hash.disable_features.replace(/;/g, ',').split(',');\n hashDisabled.forEach(features.disable);\n }\n };\n\n\n // warm up the feature matching cache upon merging fetched data\n context.history().on('merge.features', function(newEntities) {\n if (!newEntities) return;\n var handle = window.requestIdleCallback(function() {\n var graph = context.graph();\n var types = utilArrayGroupBy(newEntities, 'type');\n // ensure that getMatches is called on relations before ways\n var entities = [].concat(types.relation || [], types.way || [], types.node || []);\n for (var i = 0; i < entities.length; i++) {\n var geometry = entities[i].geometry(graph);\n features.getMatches(entities[i], graph, geometry);\n }\n });\n _deferred.add(handle);\n });\n\n\n return utilRebind(features, dispatch, 'on');\n}\n", "export function utilBindOnce(target, type, listener, capture) {\n var typeOnce = type + '.once';\n function one() {\n target.on(typeOnce, null);\n listener.apply(this, arguments);\n }\n target.on(typeOnce, one, capture);\n return this;\n}\n", "// Adapted from d3-zoom to handle pointer events.\n// https://github.com/d3/d3-zoom/blob/523ccff340187a3e3c044eaa4d4a7391ea97272b/src/zoom.js\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { interpolateZoom } from 'd3-interpolate';\nimport { select as d3_select } from 'd3-selection';\nimport { interrupt as d3_interrupt } from 'd3-transition';\nimport { zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\nimport { Transform } from '../../node_modules/d3-zoom/src/transform.js';\n\nimport { utilFastMouse, utilFunctor } from './util';\nimport { utilRebind } from './rebind';\n\n// Ignore right-click, since that should open the context menu.\nfunction defaultFilter(d3_event) {\n return !d3_event.ctrlKey && !d3_event.button;\n}\n\nfunction defaultExtent() {\n var e = this;\n if (e instanceof SVGElement) {\n e = e.ownerSVGElement || e;\n if (e.hasAttribute('viewBox')) {\n e = e.viewBox.baseVal;\n return [[e.x, e.y], [e.x + e.width, e.y + e.height]];\n }\n return [[0, 0], [e.width.baseVal.value, e.height.baseVal.value]];\n }\n return [[0, 0], [e.clientWidth, e.clientHeight]];\n}\n\nfunction defaultWheelDelta(d3_event) {\n return -d3_event.deltaY * (d3_event.deltaMode === 1 ? 0.05 : d3_event.deltaMode ? 1 : 0.002);\n}\n\nfunction defaultConstrain(transform, extent, translateExtent) {\n var dx0 = transform.invertX(extent[0][0]) - translateExtent[0][0],\n dx1 = transform.invertX(extent[1][0]) - translateExtent[1][0],\n dy0 = transform.invertY(extent[0][1]) - translateExtent[0][1],\n dy1 = transform.invertY(extent[1][1]) - translateExtent[1][1];\n return transform.translate(\n dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),\n dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)\n );\n}\n\nexport function utilZoomPan() {\n var filter = defaultFilter,\n extent = defaultExtent,\n constrain = defaultConstrain,\n wheelDelta = defaultWheelDelta,\n scaleExtent = [0, Infinity],\n translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]],\n interpolate = interpolateZoom,\n dispatch = d3_dispatch('start', 'zoom', 'end'),\n _wheelDelay = 150,\n _transform = d3_zoomIdentity,\n _activeGesture;\n\n function zoom(selection) {\n selection\n .on('pointerdown.zoom', pointerdown)\n .on('wheel.zoom', wheeled)\n .style('touch-action', 'none')\n .style('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');\n\n d3_select(window)\n .on('pointermove.zoompan', pointermove)\n .on('pointerup.zoompan pointercancel.zoompan', pointerup);\n }\n\n zoom.transform = function(collection, transform, point) {\n var selection = collection.selection ? collection.selection() : collection;\n if (collection !== selection) {\n schedule(collection, transform, point);\n } else {\n selection.interrupt().each(function() {\n gesture(this, arguments)\n .start(null)\n .zoom(null, null, typeof transform === 'function' ? transform.apply(this, arguments) : transform)\n .end(null);\n });\n }\n };\n\n zoom.scaleBy = function(selection, k, p) {\n zoom.scaleTo(selection, function() {\n var k0 = _transform.k,\n k1 = typeof k === 'function' ? k.apply(this, arguments) : k;\n return k0 * k1;\n }, p);\n };\n\n zoom.scaleTo = function(selection, k, p) {\n zoom.transform(selection, function() {\n var e = extent.apply(this, arguments),\n t0 = _transform,\n p0 = !p ? centroid(e) : typeof p === 'function' ? p.apply(this, arguments) : p,\n p1 = t0.invert(p0),\n k1 = typeof k === 'function' ? k.apply(this, arguments) : k;\n return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent);\n }, p);\n };\n\n zoom.translateBy = function(selection, x, y) {\n zoom.transform(selection, function() {\n return constrain(_transform.translate(\n typeof x === 'function' ? x.apply(this, arguments) : x,\n typeof y === 'function' ? y.apply(this, arguments) : y\n ), extent.apply(this, arguments), translateExtent);\n });\n };\n\n zoom.translateTo = function(selection, x, y, p) {\n zoom.transform(selection, function() {\n var e = extent.apply(this, arguments),\n t = _transform,\n p0 = !p ? centroid(e) : typeof p === 'function' ? p.apply(this, arguments) : p;\n return constrain(d3_zoomIdentity.translate(p0[0], p0[1]).scale(t.k).translate(\n typeof x === 'function' ? -x.apply(this, arguments) : -x,\n typeof y === 'function' ? -y.apply(this, arguments) : -y\n ), e, translateExtent);\n }, p);\n };\n\n function scale(transform, k) {\n k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], k));\n return k === transform.k ? transform : new Transform(k, transform.x, transform.y);\n }\n\n function translate(transform, p0, p1) {\n var x = p0[0] - p1[0] * transform.k, y = p0[1] - p1[1] * transform.k;\n return x === transform.x && y === transform.y ? transform : new Transform(transform.k, x, y);\n }\n\n function centroid(extent) {\n return [(+extent[0][0] + +extent[1][0]) / 2, (+extent[0][1] + +extent[1][1]) / 2];\n }\n\n function schedule(transition, transform, point) {\n transition\n .on('start.zoom', function() { gesture(this, arguments).start(null); })\n .on('interrupt.zoom end.zoom', function() { gesture(this, arguments).end(null); })\n .tween('zoom', function() {\n var that = this,\n args = arguments,\n g = gesture(that, args),\n e = extent.apply(that, args),\n p = !point ? centroid(e) : typeof point === 'function' ? point.apply(that, args) : point,\n w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]),\n a = _transform,\n b = typeof transform === 'function' ? transform.apply(that, args) : transform,\n i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k));\n return function(t) {\n if (t === 1) {\n // Avoid rounding error on end.\n t = b;\n } else {\n var l = i(t);\n var k = w / l[2];\n t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k);\n }\n g.zoom(null, null, t);\n };\n });\n }\n\n function gesture(that, args, clean) {\n return (!clean && _activeGesture) || new Gesture(that, args);\n }\n\n function Gesture(that, args) {\n this.that = that;\n this.args = args;\n this.active = 0;\n this.extent = extent.apply(that, args);\n }\n\n Gesture.prototype = {\n start: function(d3_event) {\n if (++this.active === 1) {\n _activeGesture = this;\n dispatch.call('start', this, d3_event);\n }\n return this;\n },\n zoom: function(d3_event, key, transform) {\n if (this.mouse && key !== 'mouse') this.mouse[1] = transform.invert(this.mouse[0]);\n if (this.pointer0 && key !== 'touch') this.pointer0[1] = transform.invert(this.pointer0[0]);\n if (this.pointer1 && key !== 'touch') this.pointer1[1] = transform.invert(this.pointer1[0]);\n _transform = transform;\n dispatch.call('zoom', this, d3_event, key, transform);\n return this;\n },\n end: function(d3_event) {\n if (--this.active === 0) {\n _activeGesture = null;\n dispatch.call('end', this, d3_event);\n }\n return this;\n }\n };\n\n function wheeled(d3_event) {\n if (!filter.apply(this, arguments)) return;\n var g = gesture(this, arguments),\n t = _transform,\n k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k * Math.pow(2, wheelDelta.apply(this, arguments)))),\n p = utilFastMouse(this)(d3_event);\n\n // If the mouse is in the same location as before, reuse it.\n // If there were recent wheel events, reset the wheel idle timeout.\n if (g.wheel) {\n if (g.mouse[0][0] !== p[0] || g.mouse[0][1] !== p[1]) {\n g.mouse[1] = t.invert(g.mouse[0] = p);\n }\n clearTimeout(g.wheel);\n\n // Otherwise, capture the mouse point and location at the start.\n } else {\n g.mouse = [p, t.invert(p)];\n d3_interrupt(this);\n g.start(d3_event);\n }\n\n d3_event.preventDefault();\n d3_event.stopImmediatePropagation();\n g.wheel = setTimeout(wheelidled, _wheelDelay);\n g.zoom(d3_event, 'mouse', constrain(translate(scale(t, k), g.mouse[0], g.mouse[1]), g.extent, translateExtent));\n\n function wheelidled() {\n g.wheel = null;\n g.end(d3_event);\n }\n }\n\n var _downPointerIDs = new Set();\n var _pointerLocGetter;\n\n function pointerdown(d3_event) {\n _downPointerIDs.add(d3_event.pointerId);\n\n if (!filter.apply(this, arguments)) return;\n\n var g = gesture(this, arguments, _downPointerIDs.size === 1);\n var started;\n\n d3_event.stopImmediatePropagation();\n _pointerLocGetter = utilFastMouse(this);\n var loc = _pointerLocGetter(d3_event);\n var p = [loc, _transform.invert(loc), d3_event.pointerId];\n if (!g.pointer0) {\n g.pointer0 = p;\n started = true;\n\n } else if (!g.pointer1 && g.pointer0[2] !== p[2]) {\n g.pointer1 = p;\n }\n\n if (started) {\n d3_interrupt(this);\n g.start(d3_event);\n }\n }\n\n function pointermove(d3_event) {\n if (!_downPointerIDs.has(d3_event.pointerId)) return;\n\n if (!_activeGesture || !_pointerLocGetter) return;\n\n var g = gesture(this, arguments);\n\n var isPointer0 = g.pointer0 && g.pointer0[2] === d3_event.pointerId;\n var isPointer1 = !isPointer0 && g.pointer1 && g.pointer1[2] === d3_event.pointerId;\n\n if ((isPointer0 || isPointer1) && 'buttons' in d3_event && !d3_event.buttons) {\n // The pointer went up without ending the gesture somehow, e.g.\n // a down mouse was moved off the map and released. End it here.\n if (g.pointer0) _downPointerIDs.delete(g.pointer0[2]);\n if (g.pointer1) _downPointerIDs.delete(g.pointer1[2]);\n g.end(d3_event);\n return;\n }\n\n d3_event.preventDefault();\n d3_event.stopImmediatePropagation();\n\n var loc = _pointerLocGetter(d3_event);\n var t, p, l;\n\n if (isPointer0) g.pointer0[0] = loc;\n else if (isPointer1) g.pointer1[0] = loc;\n\n t = _transform;\n if (g.pointer1) {\n var p0 = g.pointer0[0], l0 = g.pointer0[1],\n p1 = g.pointer1[0], l1 = g.pointer1[1],\n dp = (dp = p1[0] - p0[0]) * dp + (dp = p1[1] - p0[1]) * dp,\n dl = (dl = l1[0] - l0[0]) * dl + (dl = l1[1] - l0[1]) * dl;\n t = scale(t, Math.sqrt(dp / dl));\n p = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];\n l = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2];\n } else if (g.pointer0) {\n p = g.pointer0[0];\n l = g.pointer0[1];\n } else {\n return;\n }\n g.zoom(d3_event, 'touch', constrain(translate(t, p, l), g.extent, translateExtent));\n }\n\n function pointerup(d3_event) {\n if (!_downPointerIDs.has(d3_event.pointerId)) return;\n\n _downPointerIDs.delete(d3_event.pointerId);\n\n if (!_activeGesture) return;\n\n var g = gesture(this, arguments);\n\n d3_event.stopImmediatePropagation();\n\n if (g.pointer0 && g.pointer0[2] === d3_event.pointerId) delete g.pointer0;\n else if (g.pointer1 && g.pointer1[2] === d3_event.pointerId) delete g.pointer1;\n\n if (g.pointer1 && !g.pointer0) {\n g.pointer0 = g.pointer1;\n delete g.pointer1;\n }\n if (g.pointer0) {\n g.pointer0[1] = _transform.invert(g.pointer0[0]);\n } else {\n g.end(d3_event);\n }\n }\n\n zoom.wheelDelta = function(_) {\n return arguments.length ? (wheelDelta = utilFunctor(+_), zoom) : wheelDelta;\n };\n\n zoom.filter = function(_) {\n return arguments.length ? (filter = utilFunctor(!!_), zoom) : filter;\n };\n\n zoom.extent = function(_) {\n return arguments.length ? (extent = utilFunctor([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;\n };\n\n zoom.scaleExtent = function(_) {\n return arguments.length ? (scaleExtent[0] = +_[0], scaleExtent[1] = +_[1], zoom) : [scaleExtent[0], scaleExtent[1]];\n };\n\n zoom.translateExtent = function(_) {\n return arguments.length ? (translateExtent[0][0] = +_[0][0], translateExtent[1][0] = +_[1][0], translateExtent[0][1] = +_[0][1], translateExtent[1][1] = +_[1][1], zoom) : [[translateExtent[0][0], translateExtent[0][1]], [translateExtent[1][0], translateExtent[1][1]]];\n };\n\n zoom.constrain = function(_) {\n return arguments.length ? (constrain = _, zoom) : constrain;\n };\n\n zoom.interpolate = function(_) {\n return arguments.length ? (interpolate = _, zoom) : interpolate;\n };\n\n zoom._transform = function(_) {\n return arguments.length ? (_transform = _, zoom) : _transform;\n };\n\n return utilRebind(zoom, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { utilFastMouse } from './util';\nimport { utilRebind } from './rebind';\nimport { geoVecLength } from '../geo/vector';\n\n// A custom double-click / double-tap event detector that works on touch devices\n// if pointer events are supported. Falls back to default `dblclick` event.\nexport function utilDoubleUp() {\n\n var dispatch = d3_dispatch('doubleUp');\n\n var _maxTimespan = 500; // milliseconds\n var _maxDistance = 20; // web pixels; be somewhat generous to account for touch devices\n var _pointer; // object representing the pointer that could trigger double up\n\n function pointerIsValidFor(loc) {\n // second pointerup must occur within a small timeframe after the first pointerdown\n return new Date().getTime() - _pointer.startTime <= _maxTimespan &&\n // all pointer events must occur within a small distance of the first pointerdown\n geoVecLength(_pointer.startLoc, loc) <= _maxDistance;\n }\n\n function pointerdown(d3_event) {\n\n // ignore right-click\n if (d3_event.ctrlKey || d3_event.button === 2) return;\n\n var loc = [d3_event.clientX, d3_event.clientY];\n\n // Don't rely on pointerId here since it can change between pointerdown\n // events on touch devices\n if (_pointer && !pointerIsValidFor(loc)) {\n // if this pointer is no longer valid, clear it so another can be started\n _pointer = undefined;\n }\n\n if (!_pointer) {\n _pointer = {\n startLoc: loc,\n startTime: new Date().getTime(),\n upCount: 0,\n pointerId: d3_event.pointerId\n };\n } else { // double down\n _pointer.pointerId = d3_event.pointerId;\n }\n }\n\n function pointerup(d3_event) {\n\n // ignore right-click\n if (d3_event.ctrlKey || d3_event.button === 2) return;\n\n if (!_pointer || _pointer.pointerId !== d3_event.pointerId) return;\n\n _pointer.upCount += 1;\n\n if (_pointer.upCount === 2) { // double up!\n var loc = [d3_event.clientX, d3_event.clientY];\n if (pointerIsValidFor(loc)) {\n var locInThis = utilFastMouse(this)(d3_event);\n dispatch.call('doubleUp', this, d3_event, locInThis);\n }\n // clear the pointer info in any case\n _pointer = undefined;\n }\n }\n\n function doubleUp(selection) {\n if ('PointerEvent' in window) {\n // dblclick isn't well supported on touch devices so manually use\n // pointer events if they're available\n selection\n .on('pointerdown.doubleUp', pointerdown)\n .on('pointerup.doubleUp', pointerup);\n } else {\n // fallback to dblclick\n selection\n .on('dblclick.doubleUp', function(d3_event) {\n dispatch.call('doubleUp', this, d3_event, utilFastMouse(this)(d3_event));\n });\n }\n }\n\n doubleUp.off = function(selection) {\n selection\n .on('pointerdown.doubleUp', null)\n .on('pointerup.doubleUp', null)\n .on('dblclick.doubleUp', null);\n };\n\n return utilRebind(doubleUp, dispatch, 'on');\n}\n", "import { throttle, isArray, clamp } from 'es-toolkit/compat';\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { interpolate as d3_interpolate } from 'd3-interpolate';\nimport { scaleLinear as d3_scaleLinear } from 'd3-scale';\nimport { select as d3_select } from 'd3-selection';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport { prefs } from '../core/preferences';\nimport { geoExtent, geoRawMercator, geoScaleToZoom, geoZoomToScale } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { svgAreas, svgLabels, svgLayers, svgLines, svgMidpoints, svgPoints, svgVertices } from '../svg';\nimport { utilFastMouse, utilFunctor, utilSetTransform, utilEntityAndDeepMemberIDs } from '../util/util';\nimport { utilBindOnce } from '../util/bind_once';\nimport { utilDetect } from '../util/detect';\nimport { utilGetDimensions } from '../util/dimensions';\nimport { utilRebind } from '../util/rebind';\nimport { utilZoomPan } from '../util/zoom_pan';\nimport { utilDoubleUp } from '../util/double_up';\n\n// constants\nvar TILESIZE = 256;\nvar minZoom = 2;\nvar maxZoom = 24;\nvar kMin = geoZoomToScale(minZoom, TILESIZE);\nvar kMax = geoZoomToScale(maxZoom, TILESIZE);\n\n\nexport function rendererMap(context) {\n var dispatch = d3_dispatch(\n 'move', 'drawn',\n 'crossEditableZoom', 'hitMinZoom',\n 'changeHighlighting', 'changeAreaFill'\n );\n var projection = context.projection;\n var curtainProjection = context.curtainProjection;\n var drawLayers;\n var drawPoints;\n var drawVertices;\n var drawLines;\n var drawAreas;\n var drawMidpoints;\n var drawLabels;\n\n var _selection = d3_select(null);\n var supersurface = d3_select(null);\n var wrapper = d3_select(null);\n var surface = d3_select(null);\n\n var _dimensions = [1, 1];\n var _dblClickZoomEnabled = true;\n var _redrawEnabled = true;\n var _gestureTransformStart;\n var _transformStart = projection.transform();\n var _transformLast;\n var _isTransformed = false;\n var _minzoom = 0;\n var _getMouseCoords;\n var _lastPointerEvent;\n var _lastWithinEditableZoom;\n\n // whether a pointerdown event started the zoom\n var _pointerDown = false;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n // use pointer event interaction if supported; fallback to touch/mouse events in d3-zoom\n var _zoomerPannerFunction = 'PointerEvent' in window ? utilZoomPan : d3_zoom;\n\n var _zoomerPanner = _zoomerPannerFunction()\n .scaleExtent([kMin, kMax])\n .interpolate(d3_interpolate)\n .filter(zoomEventFilter)\n .on('zoom.map', zoomPan)\n .on('start.map', function(d3_event) {\n _pointerDown = d3_event && (d3_event.type === 'pointerdown' ||\n (d3_event.sourceEvent && d3_event.sourceEvent.type === 'pointerdown'));\n })\n .on('end.map', function() {\n _pointerDown = false;\n });\n var _doubleUpHandler = utilDoubleUp();\n\n var scheduleRedraw = throttle(redraw, 750);\n // var isRedrawScheduled = false;\n // var pendingRedrawCall;\n // function scheduleRedraw() {\n // // Only schedule the redraw if one has not already been set.\n // if (isRedrawScheduled) return;\n // isRedrawScheduled = true;\n // var that = this;\n // var args = arguments;\n // pendingRedrawCall = window.requestIdleCallback(function () {\n // // Reset the boolean so future redraws can be set.\n // isRedrawScheduled = false;\n // redraw.apply(that, args);\n // }, { timeout: 1400 });\n // }\n\n function cancelPendingRedraw() {\n scheduleRedraw.cancel();\n // isRedrawScheduled = false;\n // window.cancelIdleCallback(pendingRedrawCall);\n }\n\n\n function map(selection) {\n _selection = selection;\n\n context\n .on('change.map', immediateRedraw);\n\n var osm = context.connection();\n if (osm) {\n osm.on('change.map', immediateRedraw);\n }\n\n function didUndoOrRedo(targetTransform) {\n var mode = context.mode().id;\n if (mode !== 'browse' && mode !== 'select') return;\n if (targetTransform) {\n map.transformEase(targetTransform);\n }\n }\n\n context.history()\n .on('merge.map', function() { scheduleRedraw(); })\n .on('change.map', immediateRedraw)\n .on('undone.map', function(stack, fromStack) {\n didUndoOrRedo(fromStack.transform);\n })\n .on('redone.map', function(stack) {\n didUndoOrRedo(stack.transform);\n });\n\n context.background()\n .on('change.map', immediateRedraw);\n\n context.features()\n .on('redraw.map', immediateRedraw);\n\n drawLayers\n .on('change.map', function() {\n context.background().updateImagery();\n immediateRedraw();\n });\n\n selection\n .on('wheel.map mousewheel.map', function(d3_event) {\n // disable swipe-to-navigate browser pages on trackpad/magic mouse \u2013 #5552\n d3_event.preventDefault();\n })\n .call(_zoomerPanner)\n .call(_zoomerPanner.transform, projection.transform())\n .on('dblclick.zoom', null); // override d3-zoom dblclick handling\n\n map.supersurface = selection.append('div')\n .attr('class', 'supersurface')\n .call(utilSetTransform, 0, 0);\n supersurface = map.supersurface;\n\n // Need a wrapper div because Opera can't cope with an absolutely positioned\n // SVG element: http://bl.ocks.org/jfirebaugh/6fbfbd922552bf776c16\n wrapper = supersurface\n .append('div')\n .attr('class', 'layer layer-data');\n\n map.surface = wrapper\n .call(drawLayers)\n .selectAll('.surface');\n surface = map.surface;\n\n surface\n .call(drawLabels.observe)\n .call(_doubleUpHandler)\n .on(_pointerPrefix + 'down.zoom', function(d3_event) {\n _lastPointerEvent = d3_event;\n if (d3_event.button === 2) {\n d3_event.stopPropagation();\n }\n }, true)\n .on(_pointerPrefix + 'up.zoom', function(d3_event) {\n _lastPointerEvent = d3_event;\n if (resetTransform()) {\n immediateRedraw();\n }\n })\n .on(_pointerPrefix + 'move.map', function(d3_event) {\n _lastPointerEvent = d3_event;\n })\n .on(_pointerPrefix + 'over.vertices', function(d3_event) {\n if (map.editableDataEnabled() && !_isTransformed) {\n var hover = d3_event.target.__data__;\n surface.call(drawVertices.drawHover, context.graph(), hover, map.extent());\n dispatch.call('drawn', this, { full: false });\n }\n })\n .on(_pointerPrefix + 'out.vertices', function(d3_event) {\n if (map.editableDataEnabled() && !_isTransformed) {\n var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__;\n surface.call(drawVertices.drawHover, context.graph(), hover, map.extent());\n dispatch.call('drawn', this, { full: false });\n }\n });\n\n var detected = utilDetect();\n\n // only WebKit supports gesture events\n if ('GestureEvent' in window &&\n // Listening for gesture events on iOS 13.4+ breaks double-tapping,\n // but we only need to do this on desktop Safari anyway. \u2013 #7694\n !detected.isMobileWebKit) {\n\n // Desktop Safari sends gesture events for multitouch trackpad pinches.\n // We can listen for these and translate them into map zooms.\n surface\n .on('gesturestart.surface', function(d3_event) {\n d3_event.preventDefault();\n _gestureTransformStart = projection.transform();\n })\n .on('gesturechange.surface', gestureChange);\n }\n\n // must call after surface init\n updateAreaFill();\n\n _doubleUpHandler.on('doubleUp.map', function(d3_event, p0) {\n if (!_dblClickZoomEnabled) return;\n\n // don't zoom if targeting something other than the map itself\n if (typeof d3_event.target.__data__ === 'object' &&\n // or area fills\n !d3_select(d3_event.target).classed('fill')) return;\n\n var zoomOut = d3_event.shiftKey;\n\n var t = projection.transform();\n\n var p1 = t.invert(p0);\n\n t = t.scale(zoomOut ? 0.5 : 2);\n\n t.x = p0[0] - p1[0] * t.k;\n t.y = p0[1] - p1[1] * t.k;\n\n map.transformEase(t);\n });\n\n context.on('enter.map', function() {\n if (!map.editableDataEnabled(true /* skip zoom check */)) return;\n if (_isTransformed) return;\n\n // redraw immediately any objects affected by a change in selectedIDs.\n var graph = context.graph();\n var selectedAndParents = {};\n context.selectedIDs().forEach(function(id) {\n var entity = graph.hasEntity(id);\n if (entity) {\n selectedAndParents[entity.id] = entity;\n if (entity.type === 'node') {\n graph.parentWays(entity).forEach(function(parent) {\n selectedAndParents[parent.id] = parent;\n });\n }\n }\n });\n var data = Object.values(selectedAndParents);\n var filter = function(d) { return d.id in selectedAndParents; };\n\n data = context.features().filter(data, graph);\n\n surface\n .call(drawVertices.drawSelected, graph, map.extent())\n .call(drawLines, graph, data, filter)\n .call(drawAreas, graph, data, filter)\n .call(drawMidpoints, graph, data, filter, map.trimmedExtent());\n\n dispatch.call('drawn', this, { full: false });\n\n // redraw everything else later\n scheduleRedraw();\n });\n\n map.dimensions(utilGetDimensions(selection));\n }\n\n\n function zoomEventFilter(d3_event) {\n // Fix for #2151, (see also d3/d3-zoom#60, d3/d3-brush#18)\n // Intercept `mousedown` and check if there is an orphaned zoom gesture.\n // This can happen if a previous `mousedown` occurred without a `mouseup`.\n // If we detect this, dispatch `mouseup` to complete the orphaned gesture,\n // so that d3-zoom won't stop propagation of new `mousedown` events.\n if (d3_event.type === 'mousedown') {\n var hasOrphan = false;\n var listeners = window.__on;\n for (var i = 0; i < listeners.length; i++) {\n var listener = listeners[i];\n if (listener.name === 'zoom' && listener.type === 'mouseup') {\n hasOrphan = true;\n break;\n }\n }\n if (hasOrphan) {\n const event = new Event('mouseup');\n // Event needs to be dispatched with an event.view property.\n event.view = window;\n window.dispatchEvent(event);\n }\n }\n\n return d3_event.button !== 2; // ignore right clicks\n }\n\n\n function pxCenter() {\n return [_dimensions[0] / 2, _dimensions[1] / 2];\n }\n\n\n function drawEditable(difference, extent) {\n var mode = context.mode();\n var graph = context.graph();\n var features = context.features();\n var all = context.history().intersects(map.extent());\n var fullRedraw = false;\n var data;\n var set;\n var filter;\n var applyFeatureLayerFilters = true;\n\n if (map.isInWideSelection()) {\n data = [];\n utilEntityAndDeepMemberIDs(mode.selectedIDs(), context.graph()).forEach(function(id) {\n var entity = context.hasEntity(id);\n if (entity) data.push(entity);\n });\n fullRedraw = true;\n filter = utilFunctor(true);\n // selected features should always be visible, so we can skip filtering\n applyFeatureLayerFilters = false;\n\n } else if (difference) {\n var complete = difference.complete(map.extent());\n data = Object.values(complete).filter(Boolean);\n set = new Set(Object.keys(complete));\n filter = function(d) { return set.has(d.id); };\n features.clear(data);\n\n } else {\n // force a full redraw if gatherStats detects that a feature\n // should be auto-hidden (e.g. points or buildings)..\n if (features.gatherStats(all, graph, _dimensions)) {\n extent = undefined;\n }\n\n if (extent) {\n data = context.history().intersects(map.extent().intersection(extent));\n set = new Set(data.map(function(entity) { return entity.id; }));\n filter = function(d) { return set.has(d.id); };\n\n } else {\n data = all;\n fullRedraw = true;\n filter = utilFunctor(true);\n }\n }\n\n if (applyFeatureLayerFilters) {\n data = features.filter(data, graph);\n } else {\n context.features().resetStats();\n }\n\n if (mode && mode.id === 'select') {\n // update selected vertices - the user might have just double-clicked a way,\n // creating a new vertex, triggering a partial redraw without a mode change\n surface.call(drawVertices.drawSelected, graph, map.extent());\n }\n\n surface\n .call(drawVertices, graph, data, filter, map.extent(), fullRedraw)\n .call(drawLines, graph, data, filter)\n .call(drawAreas, graph, data, filter)\n .call(drawMidpoints, graph, data, filter, map.trimmedExtent())\n .call(drawPoints, graph, data, filter)\n .call(drawLabels, graph, data, filter, _dimensions, fullRedraw);\n\n dispatch.call('drawn', this, {full: true});\n }\n\n map.init = function() {\n drawLayers = svgLayers(projection, context);\n drawPoints = svgPoints(projection, context);\n drawVertices = svgVertices(projection, context);\n drawLines = svgLines(projection, context);\n drawAreas = svgAreas(projection, context);\n drawMidpoints = svgMidpoints(projection, context);\n drawLabels = svgLabels(projection, context);\n };\n\n function editOff() {\n context.features().resetStats();\n surface.selectAll('.layer-osm *').remove();\n surface.selectAll('.layer-touch:not(.markers) *').remove();\n\n var allowed = {\n 'browse': true,\n 'save': true,\n 'select-note': true,\n 'select-data': true,\n 'select-error': true\n };\n\n var mode = context.mode();\n if (mode && !allowed[mode.id]) {\n context.enter(modeBrowse(context));\n }\n\n dispatch.call('drawn', this, {full: true});\n }\n\n\n\n\n\n function gestureChange(d3_event) {\n // Remap Safari gesture events to wheel events - #5492\n // We want these disabled most places, but enabled for zoom/unzoom on map surface\n // https://developer.mozilla.org/en-US/docs/Web/API/GestureEvent\n var e = d3_event;\n e.preventDefault();\n\n var props = {\n deltaMode: 0, // dummy values to ignore in zoomPan\n deltaY: 1, // dummy values to ignore in zoomPan\n clientX: e.clientX,\n clientY: e.clientY,\n screenX: e.screenX,\n screenY: e.screenY,\n x: e.x,\n y: e.y\n };\n\n var e2 = new WheelEvent('wheel', props);\n e2._scale = e.scale; // preserve the original scale\n e2._rotation = e.rotation; // preserve the original rotation\n\n _selection.node().dispatchEvent(e2);\n }\n\n\n function zoomPan(event, key, transform) {\n var source = event && event.sourceEvent || event;\n var eventTransform = transform || (event && event.transform);\n var x = eventTransform.x;\n var y = eventTransform.y;\n var k = eventTransform.k;\n\n // Special handling of 'wheel' events:\n // They might be triggered by the user scrolling the mouse wheel,\n // or 2-finger pinch/zoom gestures, the transform may need adjustment.\n if (source && source.type === 'wheel') {\n\n // assume that the gesture is already handled by pointer events\n if (_pointerDown) return;\n\n var detected = utilDetect();\n var dX = source.deltaX;\n var dY = source.deltaY;\n var x2 = x;\n var y2 = y;\n var k2 = k;\n var t0, p0, p1;\n\n // Normalize mousewheel scroll speed (Firefox) - #3029\n // If wheel delta is provided in LINE units, recalculate it in PIXEL units\n // We are essentially redoing the calculations that occur here:\n // https://github.com/d3/d3-zoom/blob/78563a8348aa4133b07cac92e2595c2227ca7cd7/src/zoom.js#L203\n // See this for more info:\n // https://github.com/basilfx/normalize-wheel/blob/master/src/normalizeWheel.js\n if (source.deltaMode === 1 /* LINE */) {\n // Convert from lines to pixels, more if the user is scrolling fast.\n // (I made up the exp function to roughly match Firefox to what Chrome does)\n // These numbers should be floats, because integers are treated as pan gesture below.\n var lines = Math.abs(source.deltaY);\n var sign = (source.deltaY > 0) ? 1 : -1;\n dY = sign * clamp(\n lines * 18.001,\n 4.000244140625, // min\n 350.000244140625 // max\n );\n\n // recalculate x2,y2,k2\n t0 = _isTransformed ? _transformLast : _transformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * Math.pow(2, -dY / 500);\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // 2 finger map pinch zooming (Safari) - #5492\n // These are fake `wheel` events we made from Safari `gesturechange` events..\n } else if (source._scale) {\n // recalculate x2,y2,k2\n t0 = _gestureTransformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * source._scale;\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // 2 finger map pinch zooming (all browsers except Safari) - #5492\n // Pinch zooming via the `wheel` event will always have:\n // - `ctrlKey = true`\n // - `deltaY` is not round integer pixels (ignore `deltaX`)\n } else if (source.ctrlKey && !isInteger(dY)) {\n dY *= 6; // slightly scale up whatever the browser gave us\n\n // recalculate x2,y2,k2\n t0 = _isTransformed ? _transformLast : _transformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * Math.pow(2, -dY / 500);\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // Trackpad scroll zooming with shift or alt/option key down\n } else if ((source.altKey || source.shiftKey) && isInteger(dY)) {\n // recalculate x2,y2,k2\n t0 = _isTransformed ? _transformLast : _transformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * Math.pow(2, -dY / 500);\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // 2 finger map panning (Mac only, all browsers except Firefox #8595) - #5492, #5512\n // Panning via the `wheel` event will always have:\n // - `ctrlKey = false`\n // - `deltaX`,`deltaY` are round integer pixels\n } else if (detected.os === 'mac' && detected.browser !== 'Firefox' && !source.ctrlKey && isInteger(dX) && isInteger(dY)) {\n p1 = projection.translate();\n x2 = p1[0] - dX;\n y2 = p1[1] - dY;\n k2 = projection.scale();\n k2 = clamp(k2, kMin, kMax);\n }\n\n // something changed - replace the event transform\n if (x2 !== x || y2 !== y || k2 !== k) {\n x = x2;\n y = y2;\n k = k2;\n eventTransform = d3_zoomIdentity.translate(x2, y2).scale(k2);\n if (_zoomerPanner._transform) {\n // utilZoomPan interface\n _zoomerPanner._transform(eventTransform);\n } else {\n // d3_zoom interface\n _selection.node().__zoom = eventTransform;\n }\n }\n\n }\n\n if (_transformStart.x === x &&\n _transformStart.y === y &&\n _transformStart.k === k) {\n return; // no change\n }\n\n if (geoScaleToZoom(k, TILESIZE) < _minzoom) {\n surface.interrupt();\n dispatch.call('hitMinZoom', this, map);\n setCenterZoom(map.center(), context.minEditableZoom(), 0, true);\n scheduleRedraw();\n dispatch.call('move', this, map);\n return;\n }\n\n projection.transform(eventTransform);\n\n var withinEditableZoom = map.withinEditableZoom();\n if (_lastWithinEditableZoom !== withinEditableZoom) {\n if (_lastWithinEditableZoom !== undefined) {\n // notify that the map zoomed in or out over the editable zoom threshold\n dispatch.call('crossEditableZoom', this, withinEditableZoom);\n }\n _lastWithinEditableZoom = withinEditableZoom;\n }\n\n var scale = k / _transformStart.k;\n var tX = (x / scale - _transformStart.x) * scale;\n var tY = (y / scale - _transformStart.y) * scale;\n\n if (context.inIntro()) {\n curtainProjection.transform({\n x: x - tX,\n y: y - tY,\n k: k\n });\n }\n\n if (source) {\n _lastPointerEvent = event;\n }\n _isTransformed = true;\n _transformLast = eventTransform;\n utilSetTransform(supersurface, tX, tY, scale);\n scheduleRedraw();\n\n dispatch.call('move', this, map);\n\n\n function isInteger(val) {\n return typeof val === 'number' && isFinite(val) && Math.floor(val) === val;\n }\n }\n\n\n function resetTransform() {\n if (!_isTransformed) return false;\n\n utilSetTransform(supersurface, 0, 0);\n _isTransformed = false;\n if (context.inIntro()) {\n curtainProjection.transform(projection.transform());\n }\n return true;\n }\n\n\n function redraw(difference, extent) {\n // in unit tests, we need to abort if the test has already completed\n if (typeof window === 'undefined') return;\n\n if (surface.empty() || !_redrawEnabled) return;\n\n // If we are in the middle of a zoom/pan, we can't do differenced redraws.\n // It would result in artifacts where differenced entities are redrawn with\n // one transform and unchanged entities with another.\n if (resetTransform()) {\n difference = undefined;\n extent = undefined;\n }\n\n var zoom = map.zoom();\n var z = String(~~zoom);\n\n if (surface.attr('data-zoom') !== z) {\n surface.attr('data-zoom', z);\n }\n\n // class surface as `lowzoom` around z17-z18.5 (based on latitude)\n var lat = map.center()[1];\n var lowzoom = d3_scaleLinear()\n .domain([-60, 0, 60])\n .range([17, 18.5, 17])\n .clamp(true);\n\n surface\n .classed('low-zoom', zoom <= lowzoom(lat));\n\n\n if (!difference) {\n supersurface.call(context.background());\n wrapper.call(drawLayers);\n }\n\n // OSM\n if (map.editableDataEnabled() || map.isInWideSelection()) {\n context.loadTiles(projection);\n drawEditable(difference, extent);\n } else {\n editOff();\n }\n\n _transformStart = projection.transform();\n\n return map;\n }\n\n\n\n var immediateRedraw = function(difference, extent) {\n if (!difference && !extent) cancelPendingRedraw();\n redraw(difference, extent);\n };\n\n\n map.lastPointerEvent = function() {\n return _lastPointerEvent;\n };\n\n\n map.mouse = function(d3_event) {\n var event = d3_event || _lastPointerEvent;\n if (event) {\n var s;\n while ((s = event.sourceEvent)) { event = s; }\n return _getMouseCoords(event);\n }\n return null;\n };\n\n\n // returns Lng/Lat\n map.mouseCoordinates = function() {\n var coord = map.mouse() || pxCenter();\n return projection.invert(coord);\n };\n\n\n map.dblclickZoomEnable = function(val) {\n if (!arguments.length) return _dblClickZoomEnabled;\n _dblClickZoomEnabled = val;\n return map;\n };\n\n\n map.redrawEnable = function(val) {\n if (!arguments.length) return _redrawEnabled;\n _redrawEnabled = val;\n return map;\n };\n\n\n map.isTransformed = function() {\n return _isTransformed;\n };\n\n\n function setTransform(t2, duration, force) {\n var t = projection.transform();\n if (!force && t2.k === t.k && t2.x === t.x && t2.y === t.y) return false;\n\n if (duration) {\n _selection\n .transition()\n .duration(duration)\n .on('start', function() { map.startEase(); })\n .call(_zoomerPanner.transform, d3_zoomIdentity.translate(t2.x, t2.y).scale(t2.k));\n } else {\n projection.transform(t2);\n _transformStart = t2;\n _selection.call(_zoomerPanner.transform, _transformStart);\n }\n\n return true;\n }\n\n\n function setCenterZoom(loc2, z2, duration, force) {\n var c = map.center();\n var z = map.zoom();\n if (loc2[0] === c[0] && loc2[1] === c[1] && z2 === z && !force) return false;\n\n var proj = geoRawMercator().transform(projection.transform()); // copy projection\n\n var k2 = clamp(geoZoomToScale(z2, TILESIZE), kMin, kMax);\n proj.scale(k2);\n\n var t = proj.translate();\n var point = proj(loc2);\n\n var center = pxCenter();\n t[0] += center[0] - point[0];\n t[1] += center[1] - point[1];\n\n return setTransform(d3_zoomIdentity.translate(t[0], t[1]).scale(k2), duration, force);\n }\n\n\n map.pan = function(delta, duration) {\n var t = projection.translate();\n var k = projection.scale();\n\n t[0] += delta[0];\n t[1] += delta[1];\n\n if (duration) {\n _selection\n .transition()\n .duration(duration)\n .on('start', function() { map.startEase(); })\n .call(_zoomerPanner.transform, d3_zoomIdentity.translate(t[0], t[1]).scale(k));\n } else {\n projection.translate(t);\n _transformStart = projection.transform();\n _selection.call(_zoomerPanner.transform, _transformStart);\n dispatch.call('move', this, map);\n immediateRedraw();\n }\n\n return map;\n };\n\n\n map.dimensions = function(val) {\n if (!arguments.length) return _dimensions;\n\n _dimensions = val;\n drawLayers.dimensions(_dimensions);\n context.background().dimensions(_dimensions);\n projection.clipExtent([[0, 0], _dimensions]);\n _getMouseCoords = utilFastMouse(supersurface.node());\n\n scheduleRedraw();\n return map;\n };\n\n\n function zoomIn(delta) {\n setCenterZoom(map.center(), Math.trunc(map.zoom() + 0.45) + delta, 150, true);\n }\n\n function zoomOut(delta) {\n setCenterZoom(map.center(), Math.ceil(map.zoom() - 0.45) - delta, 150, true);\n }\n\n map.zoomIn = function() { zoomIn(1); };\n map.zoomInFurther = function() { zoomIn(4); };\n map.canZoomIn = function() { return map.zoom() < maxZoom; };\n\n map.zoomOut = function() { zoomOut(1); };\n map.zoomOutFurther = function() { zoomOut(4); };\n map.canZoomOut = function() { return map.zoom() > minZoom; };\n\n map.center = function(loc2) {\n if (!arguments.length) {\n return projection.invert(pxCenter());\n }\n\n if (setCenterZoom(loc2, map.zoom())) {\n dispatch.call('move', this, map);\n }\n\n scheduleRedraw();\n return map;\n };\n\n function trimmedCenter(loc, zoom) {\n var offset = [paneWidth() / 2, (footerHeight() - toolbarHeight()) / 2];\n\n var proj = geoRawMercator().transform(projection.transform()); // copy projection\n // use the target zoom to calculate the offset center\n proj.scale(geoZoomToScale(zoom, TILESIZE));\n\n var locPx = proj(loc);\n var offsetLocPx = [locPx[0] + offset[0], locPx[1] + offset[1]];\n var offsetLoc = proj.invert(offsetLocPx);\n\n return offsetLoc;\n };\n\n function paneWidth() {\n const openPane = context.container().select('.map-panes .map-pane.shown');\n if (!openPane.empty()) {\n return openPane.node().offsetWidth;\n }\n return 0;\n };\n\n function toolbarHeight() {\n const toolbar = context.container().select('.top-toolbar');\n return toolbar.node().offsetHeight;\n };\n\n function footerHeight() {\n const footer = context.container().select('.map-footer-bar');\n return footer.node().offsetHeight;\n }\n\n map.zoom = function(z2) {\n if (!arguments.length) {\n return Math.max(geoScaleToZoom(projection.scale(), TILESIZE), 0);\n }\n\n if (z2 < _minzoom) {\n surface.interrupt();\n dispatch.call('hitMinZoom', this, map);\n z2 = context.minEditableZoom();\n }\n\n if (setCenterZoom(map.center(), z2)) {\n dispatch.call('move', this, map);\n }\n\n scheduleRedraw();\n return map;\n };\n\n\n map.centerZoom = function(loc2, z2) {\n if (setCenterZoom(loc2, z2)) {\n dispatch.call('move', this, map);\n }\n\n scheduleRedraw();\n return map;\n };\n\n\n map.zoomTo = function(what) {\n return map.zoomToEase(what, 0);\n };\n\n\n map.centerEase = function(loc2, duration) {\n duration = duration || 250;\n setCenterZoom(loc2, map.zoom(), duration);\n return map;\n };\n\n\n map.zoomEase = function(z2, duration) {\n duration = duration || 250;\n setCenterZoom(map.center(), z2, duration, false);\n return map;\n };\n\n\n map.centerZoomEase = function(loc2, z2, duration) {\n duration = duration || 250;\n setCenterZoom(loc2, z2, duration, false);\n return map;\n };\n\n\n map.transformEase = function(t2, duration) {\n duration = duration || 250;\n setTransform(t2, duration, false /* don't force */);\n return map;\n };\n\n\n map.zoomToEase = function(what, duration) {\n let extent;\n if (what instanceof geoExtent) {\n // we've directly been given an extent\n extent = what;\n } else {\n // we're given one or more entities to zoom to\n if (!isArray(what)) what = [what];\n extent = what\n .map(entity => entity.extent(context.graph()))\n .reduce((a, b) => a.extend(b));\n }\n\n if (!isFinite(extent.area())) return map;\n\n var z = clamp(map.trimmedExtentZoom(extent), 0, 20);\n const loc = trimmedCenter(extent.center(), z);\n\n if (duration === 0) {\n return map.centerZoom(loc, z);\n } else {\n return map.centerZoomEase(loc, z, duration);\n }\n };\n\n\n map.startEase = function() {\n utilBindOnce(surface, _pointerPrefix + 'down.ease', function() {\n map.cancelEase();\n });\n return map;\n };\n\n\n map.cancelEase = function() {\n _selection.interrupt();\n return map;\n };\n\n\n map.extent = function(val) {\n if (!arguments.length) {\n return new geoExtent(\n projection.invert([0, _dimensions[1]]),\n projection.invert([_dimensions[0], 0])\n );\n } else {\n var extent = geoExtent(val);\n map.centerZoom(extent.center(), map.extentZoom(extent));\n }\n };\n\n\n map.trimmedExtent = function(val) {\n if (!arguments.length) {\n var headerY = 71;\n var footerY = 30;\n var pad = 10;\n return new geoExtent(\n projection.invert([pad, _dimensions[1] - footerY - pad]),\n projection.invert([_dimensions[0] - pad, headerY + pad])\n );\n } else {\n var extent = geoExtent(val);\n map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));\n }\n };\n\n\n function calcExtentZoom(extent, dim) {\n var tl = projection([extent[0][0], extent[1][1]]);\n var br = projection([extent[1][0], extent[0][1]]);\n\n // Calculate maximum zoom that fits extent\n var hFactor = (br[0] - tl[0]) / dim[0];\n var vFactor = (br[1] - tl[1]) / dim[1];\n var hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2;\n var vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2;\n var newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff);\n\n return newZoom;\n }\n\n\n map.extentZoom = function(val) {\n return calcExtentZoom(geoExtent(val), _dimensions);\n };\n\n\n map.trimmedExtentZoom = function(val) {\n const trim = 40;\n const trimmed = [\n _dimensions[0] - trim - paneWidth(),\n _dimensions[1] - trim - toolbarHeight() - footerHeight()\n ];\n return calcExtentZoom(geoExtent(val), trimmed);\n };\n\n\n map.withinEditableZoom = function() {\n return map.zoom() >= context.minEditableZoom();\n };\n\n\n map.isInWideSelection = function() {\n return !map.withinEditableZoom() && context.selectedIDs().length;\n };\n\n\n map.editableDataEnabled = function(skipZoomCheck) {\n\n var layer = context.layers().layer('osm');\n if (!layer || !layer.enabled()) return false;\n\n return skipZoomCheck || map.withinEditableZoom();\n };\n\n\n map.notesEditable = function() {\n var layer = context.layers().layer('notes');\n if (!layer || !layer.enabled()) return false;\n\n return map.withinEditableZoom();\n };\n\n\n map.minzoom = function(val) {\n if (!arguments.length) return _minzoom;\n _minzoom = val;\n return map;\n };\n\n\n map.toggleHighlightEdited = function() {\n surface.classed('highlight-edited', !surface.classed('highlight-edited'));\n map.pan([0,0]); // trigger a redraw\n dispatch.call('changeHighlighting', this);\n };\n\n\n map.areaFillOptions = ['wireframe', 'partial', 'full'];\n\n map.activeAreaFill = function(val) {\n if (!arguments.length) return prefs('area-fill') || 'partial';\n\n prefs('area-fill', val);\n if (val !== 'wireframe') {\n prefs('area-fill-toggle', val);\n }\n updateAreaFill();\n map.pan([0,0]); // trigger a redraw\n dispatch.call('changeAreaFill', this);\n return map;\n };\n\n map.toggleWireframe = function() {\n\n var activeFill = map.activeAreaFill();\n\n if (activeFill === 'wireframe') {\n activeFill = prefs('area-fill-toggle') || 'partial';\n } else {\n activeFill = 'wireframe';\n }\n\n map.activeAreaFill(activeFill);\n };\n\n function updateAreaFill() {\n var activeFill = map.activeAreaFill();\n map.areaFillOptions.forEach(function(opt) {\n surface.classed('fill-' + opt, Boolean(opt === activeFill));\n });\n }\n\n\n map.layers = () => drawLayers;\n\n\n map.doubleUpHandler = function() {\n return _doubleUpHandler;\n };\n\n\n return utilRebind(map, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { services } from '../services';\nimport { utilRebind } from '../util/rebind';\nimport { utilQsString, utilStringQs } from '../util';\n\n\nexport function rendererPhotos(context) {\n var dispatch = d3_dispatch('change');\n var _layerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'kartaview', 'mapilio', 'vegbilder', 'panoramax'];\n var _allPhotoTypes = ['flat', 'panoramic'];\n var _shownPhotoTypes = _allPhotoTypes.slice(); // shallow copy\n var _dateFilters = ['fromDate', 'toDate'];\n var _fromDate;\n var _toDate;\n var _usernames;\n\n function photos() {}\n\n function updateStorage() {\n var hash = utilStringQs(window.location.hash);\n var enabled = context.layers().all().filter(function(d) {\n return _layerIDs.indexOf(d.id) !== -1 && d.layer && d.layer.supported() && d.layer.enabled();\n }).map(function(d) {\n return d.id;\n });\n if (enabled.length) {\n hash.photo_overlay = enabled.join(',');\n } else {\n delete hash.photo_overlay;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n }\n\n /**\n * @returns The layer ID\n */\n photos.overlayLayerIDs = function() {\n return _layerIDs;\n };\n\n /**\n * @returns All the photo types\n */\n photos.allPhotoTypes = function() {\n return _allPhotoTypes;\n };\n\n /**\n * @returns The date filters value\n */\n photos.dateFilters = function() {\n return _dateFilters;\n };\n\n photos.dateFilterValue = function(val) {\n return val === _dateFilters[0] ? _fromDate : _toDate;\n };\n\n /**\n * Sets the date filter (min/max date)\n * @param {*} type Either 'fromDate' or 'toDate'\n * @param {*} val The actual Date\n * @param {boolean} updateUrl Whether the URL should update or not\n */\n photos.setDateFilter = function(type, val, updateUrl) {\n // validate the date\n var date = val && new Date(val);\n if (date && !isNaN(date)) {\n val = date.toISOString().slice(0, 10);\n } else {\n val = null;\n }\n if (type === _dateFilters[0]) {\n _fromDate = val;\n if (_fromDate && _toDate && new Date(_toDate) < new Date(_fromDate)) {\n _toDate = _fromDate;\n }\n }\n if (type === _dateFilters[1]) {\n _toDate = val;\n if (_fromDate && _toDate && new Date(_toDate) < new Date(_fromDate)) {\n _fromDate = _toDate;\n }\n }\n dispatch.call('change', this);\n if (updateUrl) {\n var rangeString;\n if (_fromDate || _toDate) {\n rangeString = (_fromDate || '') + '_' + (_toDate || '');\n }\n setUrlFilterValue('photo_dates', rangeString);\n }\n };\n\n /**\n * Sets the username filter\n * @param {string} val The username\n * @param {boolean} updateUrl Whether the URL should update or not\n */\n photos.setUsernameFilter = function(val, updateUrl) {\n if (val && typeof val === 'string') val = val.replace(/;/g, ',').split(',');\n if (val) {\n val = val.map(d => d.trim()).filter(Boolean);\n if (!val.length) {\n val = null;\n }\n }\n _usernames = val;\n dispatch.call('change', this);\n if (updateUrl) {\n var hashString;\n if (_usernames) {\n hashString = _usernames.join(',');\n }\n setUrlFilterValue('photo_username', hashString);\n }\n };\n\n /**\n * Util function to set the slider date filter\n * @param {*} val Either 'panoramic' or 'flat'\n * @param {boolean} updateUrl Whether the URL should update or not\n */\n photos.togglePhotoType = function(val, updateUrl) {\n var index = _shownPhotoTypes.indexOf(val);\n if (index !== -1) {\n _shownPhotoTypes.splice(index, 1);\n } else {\n _shownPhotoTypes.push(val);\n }\n\n if (updateUrl) {\n var hashString;\n if (_shownPhotoTypes) {\n hashString = _shownPhotoTypes.join(',');\n }\n setUrlFilterValue('photo_type', hashString);\n }\n\n dispatch.call('change', this);\n return photos;\n };\n\n /**\n * Updates the URL with new values\n * @param {*} val value to save\n * @param {string} property Name of the value\n */\n function setUrlFilterValue(property, val) {\n const hash = utilStringQs(window.location.hash);\n if (val) {\n if (hash[property] === val) return;\n hash[property] = val;\n } else {\n if (!(property in hash)) return;\n delete hash[property];\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n }\n\n function showsLayer(id) {\n var layer = context.layers().layer(id);\n return layer && layer.supported() && layer.enabled();\n }\n\n /**\n * @returns If the Date Slider filter should be drawn\n */\n photos.shouldFilterDateBySlider = function(){\n return showsLayer('mapillary') || showsLayer('kartaview') || showsLayer('mapilio')\n || showsLayer('streetside') || showsLayer('vegbilder') || showsLayer('panoramax');\n };\n\n /**\n * @returns If the Photo Type filter should be drawn\n */\n photos.shouldFilterByPhotoType = function() {\n return showsLayer('mapillary') ||\n (showsLayer('streetside') && showsLayer('kartaview')) || showsLayer('vegbilder') || showsLayer('panoramax');\n };\n\n /**\n * @returns If the Username filter should be drawn\n */\n photos.shouldFilterByUsername = function() {\n return !showsLayer('mapillary') && showsLayer('kartaview') && !showsLayer('streetside') || showsLayer('panoramax');\n };\n\n photos.showsPhotoType = function(val) {\n if (!photos.shouldFilterByPhotoType()) return true;\n\n return _shownPhotoTypes.indexOf(val) !== -1;\n };\n\n photos.showsFlat = function() {\n return photos.showsPhotoType('flat');\n };\n\n photos.showsPanoramic = function() {\n return photos.showsPhotoType('panoramic');\n };\n\n photos.fromDate = function() {\n return _fromDate;\n };\n\n photos.toDate = function() {\n return _toDate;\n };\n\n photos.usernames = function() {\n return _usernames;\n };\n\n /**\n * Inits the streetlevel layer given the saved values in the URL\n */\n photos.init = function() {\n var hash = utilStringQs(window.location.hash);\n var parts;\n if (hash.photo_dates) {\n // expect format like `photo_dates=2019-01-01_2020-12-31`, but allow a couple different separators\n parts = /^(.*)[\u2013_](.*)$/g.exec(hash.photo_dates.trim());\n this.setDateFilter('fromDate', parts && parts.length >= 2 && parts[1], false);\n this.setDateFilter('toDate', parts && parts.length >= 3 && parts[2], false);\n }\n if (hash.photo_username) {\n this.setUsernameFilter(hash.photo_username, false);\n }\n if (hash.photo_type) {\n parts = hash.photo_type.replace(/;/g, ',').split(',');\n _allPhotoTypes.forEach(d => {\n if (!parts.includes(d)) this.togglePhotoType(d, false);\n });\n }\n if (hash.photo_overlay) {\n // support enabling photo layers by default via a URL parameter, e.g. `photo_overlay=kartaview;mapillary;streetside`\n var hashOverlayIDs = hash.photo_overlay.replace(/;/g, ',').split(',');\n hashOverlayIDs.forEach(function(id) {\n if (id === 'openstreetcam') id = 'kartaview'; // legacy alias\n var layer = _layerIDs.indexOf(id) !== -1 && context.layers().layer(id);\n if (layer && !layer.enabled()) layer.enabled(true);\n });\n }\n if (hash.photo) {\n // support opening a photo via a URL parameter, e.g. `photo=mapillary-fztgSDtLpa08ohPZFZjeRQ`\n var photoIds = hash.photo.replace(/;/g, ',').split(',');\n var photoId = photoIds.length && photoIds[0].trim();\n var results = /(.*)\\/(.*)/g.exec(photoId);\n if (results && results.length >= 3) {\n var serviceId = results[1];\n if (serviceId === 'openstreetcam') serviceId = 'kartaview'; // legacy alias\n var photoKey = results[2];\n var service = services[serviceId];\n if (service && service.ensureViewerLoaded) {\n\n // if we're showing a photo then make sure its layer is enabled too\n var layer = _layerIDs.indexOf(serviceId) !== -1 && context.layers().layer(serviceId);\n if (layer && !layer.enabled()) layer.enabled(true);\n\n var baselineTime = Date.now();\n\n service.on('loadedImages.rendererPhotos', function() {\n // don't open the viewer if too much time has elapsed\n if (Date.now() - baselineTime > 45000) {\n service.on('loadedImages.rendererPhotos', null);\n return;\n }\n\n if (!service.cachedImage(photoKey)) return;\n\n service.on('loadedImages.rendererPhotos', null);\n service.ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, photoKey)\n .showViewer(context);\n });\n });\n }\n }\n }\n\n context.layers().on('change.rendererPhotos', updateStorage);\n };\n\n return utilRebind(photos, dispatch, 'on');\n}\n", "export { rendererBackgroundSource } from './background_source';\nexport { rendererBackground } from './background';\nexport { rendererFeatures } from './features';\nexport { rendererMap } from './map';\nexport { rendererPhotos } from './photos';\nexport { rendererTileLayer } from './tile_layer';\n", "import { geoPath as d3_geoPath } from 'd3-geo';\nimport { select as d3_select } from 'd3-selection';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport { t } from '../core/localizer';\nimport { geoRawMercator, geoScaleToZoom, geoVecSubtract, geoVecScale, geoZoomToScale } from '../geo';\nimport { rendererTileLayer } from '../renderer';\nimport { svgDebug, svgData } from '../svg';\nimport { utilSetTransform } from '../util';\n// import { utilGetDimensions } from '../util/dimensions';\n\n\nexport function uiMapInMap(context) {\n\n function mapInMap(selection) {\n var backgroundLayer = rendererTileLayer(context)\n .underzoom(2);\n var overlayLayers = {};\n var projection = geoRawMercator();\n var dataLayer = svgData(projection, context).showLabels(false);\n var debugLayer = svgDebug(projection, context);\n var zoom = d3_zoom()\n .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(24)])\n .on('start', zoomStarted)\n .on('zoom', zoomed)\n .on('end', zoomEnded);\n\n var wrap = d3_select(null);\n var tiles = d3_select(null);\n var viewport = d3_select(null);\n\n var _isTransformed = false;\n var _isHidden = true;\n var _skipEvents = false;\n var _gesture = null;\n var _zDiff = 6; // by default, minimap renders at (main zoom - 6)\n var _dMini; // dimensions of minimap\n var _cMini; // center pixel of minimap\n var _tStart; // transform at start of gesture\n var _tCurr; // transform at most recent event\n var _timeoutID;\n\n\n function zoomStarted() {\n if (_skipEvents) return;\n _tCurr = projection.transform();\n _tStart = _tCurr;\n _gesture = null;\n }\n\n\n function zoomed(d3_event) {\n if (_skipEvents) return;\n\n var x = d3_event.transform.x;\n var y = d3_event.transform.y;\n var k = d3_event.transform.k;\n var isZooming = (k !== _tStart.k);\n var isPanning = (x !== _tStart.x || y !== _tStart.y);\n\n if (!isZooming && !isPanning) {\n return; // no change\n }\n\n // lock in either zooming or panning, don't allow both in minimap.\n if (!_gesture) {\n _gesture = isZooming ? 'zoom' : 'pan';\n }\n\n var tMini = projection.transform();\n var tX, tY, scale;\n\n if (_gesture === 'zoom') {\n scale = k / tMini.k;\n tX = (_cMini[0] / scale - _cMini[0]) * scale;\n tY = (_cMini[1] / scale - _cMini[1]) * scale;\n } else {\n k = tMini.k;\n scale = 1;\n tX = x - tMini.x;\n tY = y - tMini.y;\n }\n\n utilSetTransform(tiles, tX, tY, scale);\n utilSetTransform(viewport, 0, 0, scale);\n _isTransformed = true;\n _tCurr = d3_zoomIdentity.translate(x, y).scale(k);\n\n var zMain = geoScaleToZoom(context.projection.scale());\n var zMini = geoScaleToZoom(k);\n\n _zDiff = zMain - zMini;\n\n queueRedraw();\n }\n\n\n function zoomEnded() {\n if (_skipEvents) return;\n if (_gesture !== 'pan') return;\n\n updateProjection();\n _gesture = null;\n context.map().center(projection.invert(_cMini)); // recenter main map..\n }\n\n\n function updateProjection() {\n var loc = context.map().center();\n var tMain = context.projection.transform();\n var zMain = geoScaleToZoom(tMain.k);\n var zMini = Math.max(zMain - _zDiff, 0.5);\n var kMini = geoZoomToScale(zMini);\n\n projection\n .translate([tMain.x, tMain.y])\n .scale(kMini);\n\n var point = projection(loc);\n var mouse = (_gesture === 'pan') ? geoVecSubtract([_tCurr.x, _tCurr.y], [_tStart.x, _tStart.y]) : [0, 0];\n var xMini = _cMini[0] - point[0] + tMain.x + mouse[0];\n var yMini = _cMini[1] - point[1] + tMain.y + mouse[1];\n\n projection\n .translate([xMini, yMini])\n .clipExtent([[0, 0], _dMini]);\n\n _tCurr = projection.transform();\n\n if (_isTransformed) {\n utilSetTransform(tiles, 0, 0);\n utilSetTransform(viewport, 0, 0);\n _isTransformed = false;\n }\n\n zoom\n .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(zMain - 3)]);\n\n _skipEvents = true;\n wrap.call(zoom.transform, _tCurr);\n _skipEvents = false;\n }\n\n\n function redraw() {\n clearTimeout(_timeoutID);\n if (_isHidden) return;\n\n updateProjection();\n var zMini = geoScaleToZoom(projection.scale());\n\n // setup tile container\n tiles = wrap\n .selectAll('.map-in-map-tiles')\n .data([0]);\n\n tiles = tiles.enter()\n .append('div')\n .attr('class', 'map-in-map-tiles')\n .merge(tiles);\n\n // redraw background\n backgroundLayer\n .source(context.background().baseLayerSource())\n .projection(projection)\n .dimensions(_dMini);\n\n var background = tiles\n .selectAll('.map-in-map-background')\n .data([0]);\n\n background.enter()\n .append('div')\n .attr('class', 'map-in-map-background')\n .merge(background)\n .call(backgroundLayer);\n\n\n // redraw overlay\n var overlaySources = context.background().overlayLayerSources();\n var activeOverlayLayers = [];\n for (var i = 0; i < overlaySources.length; i++) {\n if (overlaySources[i].validZoom(zMini)) {\n if (!overlayLayers[i]) overlayLayers[i] = rendererTileLayer(context);\n activeOverlayLayers.push(overlayLayers[i]\n .source(overlaySources[i])\n .projection(projection)\n .dimensions(_dMini));\n }\n }\n\n var overlay = tiles\n .selectAll('.map-in-map-overlay')\n .data([0]);\n\n overlay = overlay.enter()\n .append('div')\n .attr('class', 'map-in-map-overlay')\n .merge(overlay);\n\n\n var overlays = overlay\n .selectAll('div')\n .data(activeOverlayLayers, function(d) { return d.source().name(); });\n\n overlays.exit()\n .remove();\n\n overlays.enter()\n .append('div')\n .merge(overlays)\n .each(function(layer) { d3_select(this).call(layer); });\n\n\n var dataLayers = tiles\n .selectAll('.map-in-map-data')\n .data([0]);\n\n dataLayers.exit()\n .remove();\n\n dataLayers.enter()\n .append('svg')\n .attr('class', 'map-in-map-data')\n .merge(dataLayers)\n .call(dataLayer)\n .call(debugLayer);\n\n\n // redraw viewport bounding box\n if (_gesture !== 'pan') {\n var getPath = d3_geoPath(projection);\n var bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] };\n\n viewport = wrap.selectAll('.map-in-map-viewport')\n .data([0]);\n\n viewport = viewport.enter()\n .append('svg')\n .attr('class', 'map-in-map-viewport')\n .merge(viewport);\n\n\n var path = viewport.selectAll('.map-in-map-bbox')\n .data([bbox]);\n\n path.enter()\n .append('path')\n .attr('class', 'map-in-map-bbox')\n .merge(path)\n .attr('d', getPath)\n .classed('thick', function(d) { return getPath.area(d) < 30; });\n }\n }\n\n\n function queueRedraw() {\n clearTimeout(_timeoutID);\n _timeoutID = setTimeout(function() { redraw(); }, 750);\n }\n\n\n function toggle(d3_event) {\n if (d3_event) d3_event.preventDefault();\n\n _isHidden = !_isHidden;\n\n context.container().select('.minimap-toggle-item')\n .classed('active', !_isHidden)\n .select('input')\n .property('checked', !_isHidden);\n\n if (_isHidden) {\n wrap\n .style('display', 'block')\n .style('opacity', '1')\n .transition()\n .duration(200)\n .style('opacity', '0')\n .on('end', function() {\n selection.selectAll('.map-in-map')\n .style('display', 'none');\n });\n } else {\n wrap\n .style('display', 'block')\n .style('opacity', '0')\n .transition()\n .duration(200)\n .style('opacity', '1')\n .on('end', function() {\n redraw();\n });\n }\n }\n\n\n uiMapInMap.toggle = toggle;\n\n wrap = selection.selectAll('.map-in-map')\n .data([0]);\n\n wrap = wrap.enter()\n .append('div')\n .attr('class', 'map-in-map')\n .style('display', (_isHidden ? 'none' : 'block'))\n .call(zoom)\n .on('dblclick.zoom', null)\n .merge(wrap);\n\n // reflow warning: Hardcode dimensions - currently can't resize it anyway..\n _dMini = [200,150]; //utilGetDimensions(wrap);\n _cMini = geoVecScale(_dMini, 0.5);\n\n context.map()\n .on('drawn.map-in-map', function(drawn) {\n if (drawn.full === true) {\n redraw();\n }\n });\n\n redraw();\n\n context.keybinding()\n .on(t('background.minimap.key'), toggle);\n }\n\n return mapInMap;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/index';\n\n\nexport function uiNotice(context) {\n\n return function(selection) {\n var div = selection\n .append('div')\n .attr('class', 'notice');\n\n var button = div\n .append('button')\n .attr('class', 'zoom-to notice fillD')\n .on('click', function() {\n context.map().zoomEase(context.minEditableZoom());\n })\n .on('wheel', function(d3_event) { // let wheel events pass through #4482\n var e2 = new WheelEvent(d3_event.type, d3_event);\n context.surface().node().dispatchEvent(e2);\n });\n\n button\n .call(svgIcon('#iD-icon-plus', 'pre-text'))\n .append('span')\n .attr('class', 'label')\n .call(t.append('zoom_in_edit'));\n\n\n function disableTooHigh() {\n var canEdit = context.map().zoom() >= context.minEditableZoom();\n div.style('display', canEdit ? 'none' : 'block');\n }\n\n context.map()\n .on('move.notice', debounce(disableTooHigh, 500));\n\n disableTooHigh();\n };\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\nimport { clamp } from 'es-toolkit/compat';\n\nimport { t } from '../core/localizer';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { svgIcon } from '../svg/icon';\nimport { utilGetDimensions } from '../util/dimensions';\nimport { utilRebind } from '../util';\nimport { services } from '../services';\nimport { uiTooltip } from './tooltip';\nimport { actionChangeTags } from '../actions';\nimport { geoSphericalDistance } from '../geo';\n\nexport function uiPhotoviewer(context) {\n\n var dispatch = d3_dispatch('resize');\n\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n const addPhotoIdButton = new Set(['mapillary', 'panoramax']);\n\n function photoviewer(selection) {\n selection\n .append('button')\n .attr('class', 'thumb-hide')\n .attr('title', t('icons.close'))\n .on('click', function () {\n for (const service of Object.values(services)) {\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n })\n .append('div')\n .call(svgIcon('#iD-icon-close'));\n\n function preventDefault(d3_event) {\n d3_event.preventDefault();\n }\n\n selection\n .append('button')\n .attr('class', 'resize-handle-xy')\n .on('touchstart touchdown touchend', preventDefault)\n .on(\n _pointerPrefix + 'down',\n buildResizeListener(selection, 'resize', dispatch, { resizeOnX: true, resizeOnY: true })\n );\n\n selection\n .append('button')\n .attr('class', 'resize-handle-x')\n .on('touchstart touchdown touchend', preventDefault)\n .on(\n _pointerPrefix + 'down',\n buildResizeListener(selection, 'resize', dispatch, { resizeOnX: true })\n );\n\n selection\n .append('button')\n .attr('class', 'resize-handle-y')\n .on('touchstart touchdown touchend', preventDefault)\n .on(\n _pointerPrefix + 'down',\n buildResizeListener(selection, 'resize', dispatch, { resizeOnY: true })\n );\n\n // update sett_photo_from_viewer button on selection change and when tags change\n context.features().on('change.setPhotoFromViewer', function() {\n setPhotoTagButton();\n });\n context.history().on('change.setPhotoFromViewer', function() {\n setPhotoTagButton();\n });\n\n\n function setPhotoTagButton() {\n const service = getServiceId();\n const isActiveForService = addPhotoIdButton.has(service) &&\n services[service].isViewerOpen() &&\n layerEnabled(service) &&\n context.mode().id === 'select';\n\n renderAddPhotoIdButton(service, isActiveForService);\n\n function layerEnabled(which) {\n const layers = context.layers();\n const layer = layers.layer(which);\n return layer.enabled();\n }\n\n function getServiceId() {\n for (const serviceId in services) {\n const service = services[serviceId];\n if (typeof service.isViewerOpen === 'function') {\n if (service.isViewerOpen()) {\n return serviceId;\n }\n }\n }\n return false;\n }\n\n function renderAddPhotoIdButton(service, shouldDisplay) {\n const button = selection.selectAll('.set-photo-from-viewer')\n .data(shouldDisplay ? [0] : []);\n\n button.exit()\n .remove();\n\n const buttonEnter = button.enter()\n .append('button')\n .attr('class', 'set-photo-from-viewer')\n .call(svgIcon('#fas-eye-dropper'))\n .call(uiTooltip()\n .title(() => t.append('inspector.set_photo_from_viewer.enable'))\n .placement('right')\n );\n\n buttonEnter\n .select('.tooltip')\n .classed('dark', true)\n .style('width', '300px');\n\n buttonEnter\n .merge(button)\n .on('click', function (e) {\n e.preventDefault();\n e.stopPropagation();\n const activeServiceId = getServiceId();\n const image = services[activeServiceId].getActiveImage();\n\n const action = graph =>\n context.selectedIDs().reduce((graph, entityID) => {\n const tags = graph.entity(entityID).tags;\n const action = actionChangeTags(entityID, {...tags, [activeServiceId]: image.id});\n return action(graph);\n }, graph);\n\n const annotation = t('operations.change_tags.annotation');\n context.perform(action, annotation);\n buttonDisable('already_set');\n });\n\n if (service === 'panoramax') {\n const panoramaxControls = selection.select('.panoramax-wrapper .pnlm-zoom-controls.pnlm-controls');\n\n panoramaxControls\n .style('margin-top', shouldDisplay ? '36px' : '6px');\n }\n\n if (!shouldDisplay) return;\n\n const activeImage = services[service].getActiveImage();\n\n const graph = context.graph();\n const entities = context.selectedIDs()\n .map(id => graph.hasEntity(id))\n .filter(Boolean);\n\n if (entities.map(entity => entity.tags[service])\n .every(value => value === activeImage?.id)) {\n buttonDisable('already_set');\n } else if (activeImage && entities\n .map(entity => entity.extent(context.graph()).center())\n .every(loc => geoSphericalDistance(loc, activeImage.loc) > 100)) {\n buttonDisable('too_far');\n } else {\n buttonDisable(false);\n }\n }\n\n function buttonDisable(reason) {\n const disabled = reason !== false;\n const button = selection.selectAll('.set-photo-from-viewer').data([0]);\n button.attr('disabled', disabled ? 'true' : null);\n button.classed('disabled', disabled);\n button.call(uiTooltip().destroyAny);\n if (disabled) {\n button.call(uiTooltip()\n .title(() => t.append(`inspector.set_photo_from_viewer.disable.${reason}`))\n .placement('right')\n );\n } else {\n button.call(uiTooltip()\n .title(() => t.append('inspector.set_photo_from_viewer.enable'))\n .placement('right')\n );\n }\n\n button.select('.tooltip')\n .classed('dark', true)\n .style('width', '300px');\n }\n }\n\n function buildResizeListener(target, eventName, dispatch, options) {\n\n var resizeOnX = !!options.resizeOnX;\n var resizeOnY = !!options.resizeOnY;\n var minHeight = options.minHeight || 240;\n var minWidth = options.minWidth || 320;\n var pointerId;\n var startX;\n var startY;\n var startWidth;\n var startHeight;\n\n function startResize(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n var mapSize = context.map().dimensions();\n\n if (resizeOnX) {\n var mapWidth = mapSize[0];\n const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-left'), 10);\n var newWidth = clamp((startWidth + d3_event.clientX - startX), minWidth, mapWidth - viewerMargin * 2);\n target.style('width', newWidth + 'px');\n }\n\n if (resizeOnY) {\n const menuHeight = utilGetDimensions(d3_select('.top-toolbar'))[1] +\n utilGetDimensions(d3_select('.map-footer'))[1];\n const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-bottom'), 10);\n var maxHeight = mapSize[1] - menuHeight - viewerMargin * 2; // preserve space at top/bottom of map\n var newHeight = clamp((startHeight + startY - d3_event.clientY), minHeight, maxHeight);\n target.style('height', newHeight + 'px');\n }\n\n dispatch.call(eventName, target, subtractPadding(utilGetDimensions(target, true), target));\n }\n\n function stopResize(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n // remove all the listeners we added\n d3_select(window)\n .on('.' + eventName, null);\n }\n\n return function initResize(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n pointerId = d3_event.pointerId || 'mouse';\n\n startX = d3_event.clientX;\n startY = d3_event.clientY;\n var targetRect = target.node().getBoundingClientRect();\n startWidth = targetRect.width;\n startHeight = targetRect.height;\n\n d3_select(window)\n .on(_pointerPrefix + 'move.' + eventName, startResize, false)\n .on(_pointerPrefix + 'up.' + eventName, stopResize, false);\n\n if (_pointerPrefix === 'pointer') {\n d3_select(window)\n .on('pointercancel.' + eventName, stopResize, false);\n }\n };\n }\n }\n\n photoviewer.onMapResize = function() {\n var photoviewer = context.container().select('.photoviewer');\n var content = context.container().select('.main-content');\n var mapDimensions = utilGetDimensions(content, true);\n const menuHeight = utilGetDimensions(d3_select('.top-toolbar'))[1] +\n utilGetDimensions(d3_select('.map-footer'))[1];\n const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-bottom'), 10);\n // shrink photo viewer if it is too big (preserves space at top and bottom of map used by menus)\n var photoDimensions = utilGetDimensions(photoviewer, true);\n if (photoDimensions[0] > mapDimensions[0] || photoDimensions[1] > (mapDimensions[1] - menuHeight - viewerMargin * 2)) {\n var setPhotoDimensions = [\n Math.min(photoDimensions[0], mapDimensions[0]),\n Math.min(photoDimensions[1], mapDimensions[1] - menuHeight - viewerMargin * 2),\n ];\n\n photoviewer\n .style('width', setPhotoDimensions[0] + 'px')\n .style('height', setPhotoDimensions[1] + 'px');\n\n dispatch.call('resize', photoviewer, subtractPadding(setPhotoDimensions, photoviewer));\n } else {\n dispatch.call('resize', photoviewer, subtractPadding(photoDimensions, photoviewer));\n }\n };\n\n function subtractPadding(dimensions, selection) {\n return [\n dimensions[0] - parseFloat(selection.style('padding-left')) - parseFloat(selection.style('padding-right')),\n dimensions[1] - parseFloat(selection.style('padding-top')) - parseFloat(selection.style('padding-bottom'))\n ];\n }\n\n photoviewer.viewerSize = function() {\n const photoviewer = context.container().select('.photoviewer');\n return subtractPadding(utilGetDimensions(photoviewer, true), photoviewer);\n };\n\n return utilRebind(photoviewer, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { uiModal } from './modal';\n\n\nexport function uiRestore(context) {\n return function(selection) {\n if (!context.history().hasRestorableChanges()) return;\n\n let modalSelection = uiModal(selection, true);\n\n modalSelection.select('.modal')\n .attr('class', 'modal fillL');\n\n let introModal = modalSelection.select('.content');\n\n introModal\n .append('div')\n .attr('class', 'modal-section')\n .append('h3')\n .call(t.append('restore.heading'));\n\n introModal\n .append('div')\n .attr('class','modal-section')\n .append('p')\n .call(t.append('restore.description'));\n\n let buttonWrap = introModal\n .append('div')\n .attr('class', 'modal-actions');\n\n let restore = buttonWrap\n .append('button')\n .attr('class', 'restore')\n .on('click', () => {\n context.history().restore();\n modalSelection.remove();\n });\n\n restore\n .append('svg')\n .attr('class', 'logo logo-restore')\n .append('use')\n .attr('xlink:href', '#iD-logo-restore');\n\n restore\n .append('div')\n .call(t.append('restore.restore'));\n\n let reset = buttonWrap\n .append('button')\n .attr('class', 'reset')\n .on('click', () => {\n context.history().clearSaved();\n modalSelection.remove();\n });\n\n reset\n .append('svg')\n .attr('class', 'logo logo-reset')\n .append('use')\n .attr('xlink:href', '#iD-logo-reset');\n\n reset\n .append('div')\n .call(t.append('restore.reset'));\n\n restore.node().focus();\n };\n}\n", "import { displayLength } from '../util/units';\nimport { geoLonToMeters, geoMetersToLon } from '../geo';\nimport { localizer } from '../core/localizer';\n\n\nexport function uiScale(context) {\n var projection = context.projection,\n isImperial = !localizer.usesMetric(),\n maxLength = 180,\n tickHeight = 8;\n\n\n function scaleDefs(loc1, loc2) {\n var lat = (loc2[1] + loc1[1]) / 2,\n conversion = (isImperial ? 3.28084 : 1),\n dist = geoLonToMeters(loc2[0] - loc1[0], lat) * conversion,\n scale = { dist: 0, px: 0, text: '' },\n buckets, i, val, dLon;\n\n if (isImperial) {\n buckets = [5280000, 528000, 52800, 5280, 500, 50, 5, 1];\n } else {\n buckets = [5000000, 500000, 50000, 5000, 500, 50, 5, 1];\n }\n\n // determine a user-friendly endpoint for the scale\n for (i = 0; i < buckets.length; i++) {\n val = buckets[i];\n if (dist >= val) {\n scale.dist = Math.floor(dist / val) * val;\n break;\n } else {\n scale.dist = +dist.toFixed(2);\n }\n }\n\n dLon = geoMetersToLon(scale.dist / conversion, lat);\n scale.px = Math.round(projection([loc1[0] + dLon, loc1[1]])[0]);\n\n scale.text = displayLength(scale.dist / conversion, isImperial);\n\n return scale;\n }\n\n\n function update(selection) {\n // choose loc1, loc2 along bottom of viewport (near where the scale will be drawn)\n var dims = context.map().dimensions(),\n loc1 = projection.invert([0, dims[1]]),\n loc2 = projection.invert([maxLength, dims[1]]),\n scale = scaleDefs(loc1, loc2);\n\n selection.select('.scale-path')\n .attr('d', 'M0.5,0.5v' + tickHeight + 'h' + scale.px + 'v-' + tickHeight);\n\n selection.select('.scale-text')\n .style(localizer.textDirection() === 'ltr' ? 'left' : 'right', (scale.px + 16) + 'px')\n .text(scale.text);\n }\n\n\n return function(selection) {\n function switchUnits() {\n isImperial = !isImperial;\n selection.call(update);\n }\n\n var scalegroup = selection.append('svg')\n .attr('class', 'scale')\n .on('click', switchUnits)\n .append('g')\n .attr('transform', 'translate(10,11)');\n\n scalegroup\n .append('path')\n .attr('class', 'scale-path');\n\n selection\n .append('div')\n .attr('class', 'scale-text');\n\n selection.call(update);\n\n context.map().on('move.scale', function() {\n update(selection);\n });\n };\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { fileFetcher } from '../core/file_fetcher';\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\nimport { uiModal } from './modal';\nimport { utilArrayUniq } from '../util';\nimport { utilDetect } from '../util/detect';\n\n\nexport function uiShortcuts(context) {\n var detected = utilDetect();\n var _activeTab = 0;\n var _modalSelection;\n var _selection = d3_select(null);\n var _dataShortcuts;\n\n\n function shortcutsModal(_modalSelection) {\n _modalSelection.select('.modal')\n .classed('modal-shortcuts', true);\n\n var content = _modalSelection.select('.content');\n\n content\n .append('div')\n .attr('class', 'modal-section header')\n .append('h2')\n .call(t.append('shortcuts.title'));\n\n fileFetcher.get('shortcuts')\n .then(function(data) {\n _dataShortcuts = data;\n content.call(render);\n })\n .catch(function() { /* ignore */ });\n }\n\n\n function render(selection) {\n if (!_dataShortcuts) return;\n\n var wrapper = selection\n .selectAll('.wrapper')\n .data([0]);\n\n var wrapperEnter = wrapper\n .enter()\n .append('div')\n .attr('class', 'wrapper modal-section');\n\n var tabsBar = wrapperEnter\n .append('div')\n .attr('class', 'tabs-bar');\n\n var shortcutsList = wrapperEnter\n .append('div')\n .attr('class', 'shortcuts-list');\n\n wrapper = wrapper.merge(wrapperEnter);\n\n var tabs = tabsBar\n .selectAll('.tab')\n .data(_dataShortcuts);\n\n var tabsEnter = tabs\n .enter()\n .append('a')\n .attr('class', 'tab')\n .attr('href', '#')\n .on('click', function (d3_event, d) {\n d3_event.preventDefault();\n var i = _dataShortcuts.indexOf(d);\n _activeTab = i;\n render(selection);\n });\n\n tabsEnter\n .append('span')\n .each(function (d) {\n d3_select(this).call(t.addOrUpdate(d.text));\n });\n\n // Update\n wrapper.selectAll('.tab')\n .classed('active', function (d, i) {\n return i === _activeTab;\n });\n\n\n var shortcuts = shortcutsList\n .selectAll('.shortcut-tab')\n .data(_dataShortcuts);\n\n var shortcutsEnter = shortcuts\n .enter()\n .append('div')\n .attr('class', function(d) { return 'shortcut-tab shortcut-tab-' + d.tab; });\n\n var columnsEnter = shortcutsEnter\n .selectAll('.shortcut-column')\n .data(function (d) { return d.columns; })\n .enter()\n .append('table')\n .attr('class', 'shortcut-column');\n\n var rowsEnter = columnsEnter\n .selectAll('.shortcut-row')\n .data(function (d) { return d.rows; })\n .enter()\n .append('tr')\n .attr('class', 'shortcut-row');\n\n\n var sectionRows = rowsEnter\n .filter(function (d) { return !d.shortcuts; });\n\n sectionRows\n .append('td');\n\n sectionRows\n .append('td')\n .attr('class', 'shortcut-section')\n .append('h3')\n .each(function (d) {\n d3_select(this).call(t.addOrUpdate(d.text));\n });\n\n\n var shortcutRows = rowsEnter\n .filter(function (d) { return d.shortcuts; });\n\n var shortcutKeys = shortcutRows\n .append('td')\n .attr('class', 'shortcut-keys');\n\n var modifierKeys = shortcutKeys\n .filter(function (d) { return d.modifiers; });\n\n modifierKeys\n .selectAll('kbd.modifier')\n .data(function (d) {\n if (detected.os === 'win' && d.text === 'shortcuts.editing.commands.redo') {\n return ['\u2318'];\n } else if (detected.os !== 'mac' && d.text === 'shortcuts.browsing.display_options.fullscreen') {\n return [];\n } else {\n return d.modifiers;\n }\n })\n .enter()\n .each(function () {\n var selection = d3_select(this);\n\n selection\n .append('kbd')\n .attr('class', 'modifier')\n .text(function (d) { return uiCmd.display(d); });\n\n selection\n .append('span')\n .text('+');\n });\n\n\n shortcutKeys\n .selectAll('kbd.shortcut')\n .data(function (d) {\n var arr = d.shortcuts;\n if (detected.os === 'win' && d.text === 'shortcuts.editing.commands.redo') {\n arr = ['Y'];\n } else if (detected.os !== 'mac' && d.text === 'shortcuts.browsing.display_options.fullscreen') {\n arr = ['F11'];\n }\n\n // replace translations\n arr = arr.map(function(s) {\n return uiCmd.display(s.indexOf('.') !== -1 ? t(s) : s);\n });\n\n return utilArrayUniq(arr).map(function(s) {\n return {\n shortcut: s,\n separator: d.separator,\n suffix: d.suffix\n };\n });\n })\n .enter()\n .each(function (d, i, nodes) {\n var selection = d3_select(this);\n var click = d.shortcut.toLowerCase().match(/(.*).click/);\n\n if (click && click[1]) { // replace \"left_click\", \"right_click\" with mouse icon\n selection\n .call(svgIcon('#iD-walkthrough-mouse-' + click[1], 'operation'));\n } else if (d.shortcut.toLowerCase() === 'long-press') {\n selection\n .call(svgIcon('#iD-walkthrough-longpress', 'longpress operation'));\n } else if (d.shortcut.toLowerCase() === 'tap') {\n selection\n .call(svgIcon('#iD-walkthrough-tap', 'tap operation'));\n } else {\n selection\n .append('kbd')\n .attr('class', 'shortcut')\n .text(function (d) { return d.shortcut; });\n }\n\n if (i < nodes.length - 1) {\n if (d.separator) {\n selection\n .append('span')\n .text(d.separator);\n } else {\n selection.append('span').text('\\u00a0');\n selection.append('span').call(t.append('shortcuts.or'));\n selection.append('span').text('\\u00a0');\n }\n } else if (i === nodes.length - 1 && d.suffix) {\n selection\n .append('span')\n .text(d.suffix);\n }\n });\n\n\n shortcutKeys\n .filter(function(d) { return d.gesture; })\n .each(function () {\n var selection = d3_select(this);\n\n selection\n .append('span')\n .text('+');\n\n selection\n .append('span')\n .attr('class', 'gesture')\n .each(function (d) {\n d3_select(this).call(t.addOrUpdate(d.gesture));\n });\n });\n\n\n shortcutRows\n .append('td')\n .attr('class', 'shortcut-desc')\n .each(function (d) {\n if (d.text) {\n d3_select(this).call(t.addOrUpdate(d.text));\n } else {\n d3_select(this).text('\\u00a0');\n }\n });\n\n\n // Update\n wrapper.selectAll('.shortcut-tab')\n .style('display', function (d, i) {\n return i === _activeTab ? 'flex' : 'none';\n });\n }\n\n\n return function(selection, show) {\n _selection = selection;\n if (show) {\n _modalSelection = uiModal(selection);\n _modalSelection.call(shortcutsModal);\n } else {\n context.keybinding()\n .on([t('shortcuts.toggle.key'), '?'], function () {\n if (context.container().selectAll('.modal-shortcuts').size()) { // already showing\n if (_modalSelection) {\n _modalSelection.close();\n _modalSelection = null;\n }\n } else {\n _modalSelection = uiModal(_selection);\n _modalSelection.call(shortcutsModal);\n }\n });\n }\n };\n}\n", "module.exports = element;\nmodule.exports.pair = pair;\nmodule.exports.format = format;\nmodule.exports.formatPair = formatPair;\nmodule.exports.coordToDMS = coordToDMS;\n\n\nfunction element(input, dims) {\n var result = search(input, dims);\n return (result === null) ? null : result.val;\n}\n\n\nfunction formatPair(input) {\n return format(input.lat, 'lat') + ' ' + format(input.lon, 'lon');\n}\n\n\n// Is 0 North or South?\nfunction format(input, dim) {\n var dms = coordToDMS(input, dim);\n return dms.whole + '\u00B0 ' +\n (dms.minutes ? dms.minutes + '\\' ' : '') +\n (dms.seconds ? dms.seconds + '\" ' : '') + dms.dir;\n}\n\n\nfunction coordToDMS(input, dim) {\n var dirs = { lat: ['N', 'S'], lon: ['E', 'W'] }[dim] || '';\n var dir = dirs[input >= 0 ? 0 : 1];\n var abs = Math.abs(input);\n var whole = Math.floor(abs);\n var fraction = abs - whole;\n var fractionMinutes = fraction * 60;\n var minutes = Math.floor(fractionMinutes);\n var seconds = Math.floor((fractionMinutes - minutes) * 60);\n\n return {\n whole: whole,\n minutes: minutes,\n seconds: seconds,\n dir: dir\n };\n}\n\n\nfunction search(input, dims) {\n if (!dims) dims = 'NSEW';\n if (typeof input !== 'string') return null;\n\n input = input.toUpperCase();\n var regex = /^[\\s\\,]*([NSEW])?\\s*([\\-|\\\u2014|\\\u2015]?[0-9.]+)[\u00B0\u00BA\u02DA]?\\s*(?:([0-9.]+)['\u2019\u2032\u2018]\\s*)?(?:([0-9.]+)(?:''|\"|\u201D|\u2033)\\s*)?([NSEW])?/;\n\n var m = input.match(regex);\n if (!m) return null; // no match\n\n var matched = m[0];\n\n // extract dimension.. m[1] = leading, m[5] = trailing\n var dim;\n if (m[1] && m[5]) { // if matched both..\n dim = m[1]; // keep leading\n matched = matched.slice(0, -1); // remove trailing dimension from match\n } else {\n dim = m[1] || m[5];\n }\n\n // if unrecognized dimension\n if (dim && dims.indexOf(dim) === -1) return null;\n\n // extract DMS\n var deg = m[2] ? parseFloat(m[2]) : 0;\n var min = m[3] ? parseFloat(m[3]) / 60 : 0;\n var sec = m[4] ? parseFloat(m[4]) / 3600 : 0;\n var sign = (deg < 0) ? -1 : 1;\n if (dim === 'S' || dim === 'W') sign *= -1;\n\n return {\n val: (Math.abs(deg) + min + sec) * sign,\n dim: dim,\n matched: matched,\n remain: input.slice(matched.length)\n };\n}\n\n\nfunction pair(input, dims) {\n input = input.trim();\n var one = search(input, dims);\n if (!one) return null;\n\n input = one.remain.trim();\n var two = search(input, dims);\n if (!two || two.remain) return null;\n\n if (one.dim) {\n return swapdim(one.val, two.val, one.dim);\n } else {\n return [one.val, two.val];\n }\n}\n\n\nfunction swapdim(a, b, dim) {\n if (dim === 'N' || dim === 'S') return [a, b];\n if (dim === 'W' || dim === 'E') return [b, a];\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\nimport * as sexagesimal from '@mapbox/sexagesimal';\n\nimport { presetManager } from '../presets';\nimport { t } from '../core/localizer';\nimport { dmsCoordinatePair, dmsMatcher } from '../util/units';\nimport { coreGraph } from '../core/graph';\nimport { geoSphericalDistance } from '../geo/geo';\nimport { geoExtent } from '../geo';\nimport { modeSelect } from '../modes/select';\nimport { osmEntity } from '../osm/entity';\nimport { getRelationColor } from '../osm/tags';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\n\nimport {\n utilDisplayName,\n utilDisplayType,\n utilHighlightEntities,\n utilNoAuto\n} from '../util';\n\n\nexport const idMatch = q => {\n const idMatchRegex = /(?:^|\\W)(node|way|relation|note|[nwr])\\W{0,2}0*([1-9]\\d*)(?:\\W|$)/i;\n const idMatch = q.match(idMatchRegex);\n if (!idMatch) return false;\n\n return {\n type: idMatch[1] === 'note' ? idMatch[1] : idMatch[1].charAt(0),\n id: idMatch[2]\n };\n};\n\nexport function uiFeatureList(context) {\n var _geocodeResults;\n\n\n function featureList(selection) {\n var header = selection\n .append('div')\n .attr('class', 'header fillL');\n\n header\n .append('h2')\n .call(t.append('inspector.feature_list'));\n\n var searchWrap = selection\n .append('div')\n .attr('class', 'search-header');\n\n searchWrap\n .call(svgIcon('#iD-icon-search', 'pre-text'));\n\n var search = searchWrap\n .append('input')\n .attr('placeholder', t('inspector.search'))\n .attr('type', 'search')\n .call(utilNoAuto)\n .on('keypress', keypress)\n .on('keydown', keydown)\n .on('input', inputevent);\n\n var listWrap = selection\n .append('div')\n .attr('class', 'inspector-body');\n\n var list = listWrap\n .append('div')\n .attr('class', 'feature-list');\n\n context\n .on('exit.feature-list', clearSearch);\n context.map()\n .on('drawn.feature-list', mapDrawn);\n\n context.keybinding()\n .on(uiCmd('\u2318F'), focusSearch);\n\n\n function focusSearch(d3_event) {\n var mode = context.mode() && context.mode().id;\n if (mode !== 'browse') return;\n\n d3_event.preventDefault();\n search.node().focus();\n }\n\n\n function keydown(d3_event) {\n if (d3_event.keyCode === 27) { // escape\n search.node().blur();\n }\n }\n\n\n function keypress(d3_event) {\n var q = search.property('value'),\n items = list.selectAll('.feature-list-item');\n if (d3_event.keyCode === 13 && // \u21A9 Return\n q.length &&\n items.size()) {\n click(d3_event, items.datum());\n }\n }\n\n\n function inputevent() {\n _geocodeResults = undefined;\n drawList();\n }\n\n\n function clearSearch() {\n search.property('value', '');\n drawList();\n }\n\n\n function mapDrawn(e) {\n if (e.full) {\n drawList();\n }\n }\n\n\n function features() {\n var graph = context.graph();\n var visibleCenter = context.map().extent().center();\n var q = search.property('value').toLowerCase().trim();\n\n if (!q) return [];\n\n const locationMatch = sexagesimal.pair(q.toUpperCase()) || dmsMatcher(q);\n\n const coordResult = [];\n if (locationMatch) {\n const latLon = [Number(locationMatch[0]), Number(locationMatch[1])];\n const lonLat = [latLon[1], latLon[0]]; // also try swapped order\n\n const isLatLonValid = latLon[0] >= -90 && latLon[0] <= 90 && latLon[1] >= -180 && latLon[1] <= 180;\n let isLonLatValid = lonLat[0] >= -90 && lonLat[0] <= 90 && lonLat[1] >= -180 && lonLat[1] <= 180;\n isLonLatValid &&= !q.match(/[NSEW]/i); // don't flip coords with explicit cardinal directions\n isLonLatValid &&= !locationMatch[2]; // don't flip zoom/x/y coords\n isLonLatValid &&= lonLat[0] !== lonLat[1]; // don't flip when lat=lon\n\n if (isLatLonValid) {\n coordResult.push({\n id: latLon[0] + '/' + latLon[1],\n geometry: 'point',\n type: t('inspector.location'),\n name: dmsCoordinatePair([latLon[1], latLon[0]]),\n location: latLon,\n zoom: locationMatch[2]\n });\n }\n if (isLonLatValid) {\n coordResult.push({\n id: lonLat[0] + '/' + lonLat[1],\n geometry: 'point',\n type: t('inspector.location'),\n name: dmsCoordinatePair([lonLat[1], lonLat[0]]),\n location: lonLat\n });\n }\n }\n\n // A location search takes priority over an ID search\n const idMatchResult = !locationMatch && idMatch(q);\n const idResult = [];\n if (idMatchResult) {\n const elemType = idMatchResult.type;\n const elemId = idMatchResult.id;\n idResult.push({\n id: elemType + elemId,\n geometry: elemType === 'n' ? 'point' : elemType === 'w' ? 'line' : elemType === 'note' ? 'note' : 'relation',\n type: elemType === 'n' ? t('inspector.node') : elemType === 'w' ? t('inspector.way') : elemType === 'note' ? t('note.note') : t('inspector.relation'),\n name: elemId\n });\n }\n\n var allEntities = graph.entities;\n const localResults = [];\n for (var id in allEntities) {\n var entity = allEntities[id];\n if (!entity) continue;\n\n var matched = presetManager.match(entity, graph);\n var name = utilDisplayName(entity, { hideNetwork: matched.suggestion }) || '';\n if (name.toLowerCase().indexOf(q) < 0) continue;\n var type = (matched && matched.name()) || utilDisplayType(entity.id);\n var extent = entity.extent(graph);\n var distance = extent ? geoSphericalDistance(visibleCenter, extent.center()) : 0;\n\n localResults.push({\n id: entity.id,\n entity: entity,\n geometry: entity.geometry(graph),\n type: type,\n name: name,\n distance: distance\n });\n\n if (localResults.length > 100) break;\n }\n localResults.sort((a, b) => a.distance - b.distance);\n\n const geocodeResults = [];\n (_geocodeResults || []).forEach(function(d) {\n if (d.osm_type && d.osm_id) { // some results may be missing these - #1890\n\n // Make a temporary osmEntity so we can preset match\n // and better localize the search result - #4725\n var id = osmEntity.id.fromOSM(d.osm_type, d.osm_id);\n var tags = {};\n tags[d.class] = d.type;\n\n var attrs = { id: id, type: d.osm_type, tags: tags };\n if (d.osm_type === 'way') { // for ways, add some fake closed nodes\n attrs.nodes = ['a','a']; // so that geometry area is possible\n }\n\n var tempEntity = osmEntity(attrs);\n var tempGraph = coreGraph([tempEntity]);\n var matched = presetManager.match(tempEntity, tempGraph);\n var type = (matched && matched.name()) || utilDisplayType(id);\n\n geocodeResults.push({\n id: tempEntity.id,\n geometry: tempEntity.geometry(tempGraph),\n type: type,\n name: d.display_name,\n extent: new geoExtent(\n [Number(d.boundingbox[3]), Number(d.boundingbox[0])],\n [Number(d.boundingbox[2]), Number(d.boundingbox[1])])\n });\n }\n });\n\n const extraResults = [];\n if (q.match(/^[0-9]+$/)) {\n // if query is just a number, possibly an OSM ID without a prefix\n extraResults.push({\n id: 'n' + q,\n geometry: 'point',\n type: t('inspector.node'),\n name: q\n });\n extraResults.push({\n id: 'w' + q,\n geometry: 'line',\n type: t('inspector.way'),\n name: q\n });\n extraResults.push({\n id: 'r' + q,\n geometry: 'relation',\n type: t('inspector.relation'),\n name: q\n });\n extraResults.push({\n id: 'note' + q,\n geometry: 'note',\n type: t('note.note'),\n name: q\n });\n }\n\n return [...idResult, ...localResults, ...coordResult, ...geocodeResults, ...extraResults];\n }\n\n\n function drawList() {\n var value = search.property('value');\n var results = features();\n\n list.classed('filtered', value.length);\n\n var resultsIndicator = list.selectAll('.no-results-item')\n .data([0])\n .enter()\n .append('button')\n .property('disabled', true)\n .attr('class', 'no-results-item')\n .call(svgIcon('#iD-icon-alert', 'pre-text'));\n\n resultsIndicator.append('span')\n .attr('class', 'entity-name');\n\n list.selectAll('.no-results-item .entity-name')\n .html('')\n .call(t.append('geocoder.no_results_worldwide'));\n\n if (services.geocoder) {\n list.selectAll('.geocode-item')\n .data([0])\n .enter()\n .append('button')\n .attr('class', 'geocode-item secondary-action')\n .on('click', geocoderSearch)\n .append('div')\n .attr('class', 'label')\n .append('span')\n .attr('class', 'entity-name')\n .call(t.append('geocoder.search'));\n }\n\n list.selectAll('.no-results-item')\n .style('display', (value.length && !results.length) ? 'block' : 'none');\n\n list.selectAll('.geocode-item')\n .style('display', (value && _geocodeResults === undefined) ? 'block' : 'none');\n\n var items = list.selectAll('.feature-list-item')\n .data(results, function(d) { return d.id; });\n\n var enter = items.enter()\n .insert('button', '.geocode-item')\n .attr('class', 'feature-list-item')\n .on('pointerenter', mouseover)\n .on('pointerleave', mouseout)\n .on('focus', mouseover)\n .on('blur', mouseout)\n .on('click', click);\n\n var label = enter\n .append('div')\n .attr('class', 'label')\n .attr('title', d => d.name);\n\n label\n .each(function(d) {\n d3_select(this)\n .call(svgIcon('#iD-icon-' + d.geometry, 'pre-text'));\n });\n\n label\n .append('span')\n .attr('class', 'entity-type')\n .text(function(d) { return d.type; });\n\n label.each(function(d) {\n if (d.entity?.type !== 'relation') return;\n\n const hasRef = d.entity.tags.ref;\n const relColors = getRelationColor(d.entity.tags, '#555');\n if (relColors.isValid || hasRef) {\n const refs = (d.entity.tags.ref || '').split(';');\n for (const ref of refs) {\n d3_select(this)\n .append('span')\n .classed('member-entity-ref-color', true)\n .style('border-color', relColors.color)\n .style('background-color', relColors.color)\n .style('color', relColors.textColor)\n .text(ref);\n }\n }\n });\n\n label\n .append('span')\n .attr('class', 'entity-name')\n .text(d => d.name);\n\n enter\n .style('opacity', 0)\n .transition()\n .style('opacity', 1);\n\n items.exit()\n .each(d => mouseout(undefined, d))\n .remove();\n\n items.merge(enter)\n .order();\n }\n\n\n function mouseover(d3_event, d) {\n if (d.location !== undefined) return;\n\n utilHighlightEntities([d.id], true, context);\n }\n\n\n function mouseout(d3_event, d) {\n if (d.location !== undefined) return;\n\n utilHighlightEntities([d.id], false, context);\n }\n\n\n function click(d3_event, d) {\n d3_event.preventDefault();\n\n if (d.location) {\n context.map().centerZoomEase([d.location[1], d.location[0]], d.zoom || 19);\n\n } else if (d.entity) {\n utilHighlightEntities([d.id], false, context);\n\n context.enter(modeSelect(context, [d.entity.id]));\n context.map().zoomToEase(d.entity);\n\n } else if (d.geometry === 'note') {\n // note\n // get number part 'note12345'\n const noteId = d.id.replace(/\\D/g, '');\n\n // load note\n context.moveToNote(noteId);\n } else {\n // download, zoom to, and select the entity with the given ID\n context.zoomToEntity(d.id);\n }\n }\n\n\n function geocoderSearch() {\n services.geocoder.search(search.property('value'), function (err, resp) {\n _geocodeResults = resp || [];\n drawList();\n });\n }\n }\n\n\n return featureList;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { svgIcon } from '../../svg/icon';\nimport { utilArrayIdentical } from '../../util/array';\nimport { t } from '../../core/localizer';\nimport { utilHighlightEntities } from '../../util';\nimport { uiSection } from '../section';\nimport { validationIssue } from '../../core/validation';\n\n\nexport function uiSectionEntityIssues(context) {\n // Does the user prefer to expand the active issue? Useful for viewing tag diff.\n // Expand by default so first timers see it - #6408, #8143\n var preference = prefs('entity-issues.reference.expanded');\n var _expanded = preference === null ? true : (preference === 'true');\n\n var _entityIDs = [];\n var _issues = [];\n var _activeIssueID;\n\n\n var section = uiSection('entity-issues', context)\n .shouldDisplay(function() {\n return _issues.length > 0;\n })\n .label(function() {\n return t.append('inspector.title_count', { title: t.append('issues.list_title'), count: _issues.length });\n })\n .disclosureContent(renderDisclosureContent);\n\n context.validator()\n .on('validated.entity_issues', function() {\n // Refresh on validated events\n reloadIssues();\n section.reRender();\n })\n .on('focusedIssue.entity_issues', function(issue) {\n makeActiveIssue(issue.id);\n });\n\n function reloadIssues() {\n _issues = context.validator().getSharedEntityIssues(_entityIDs, { includeDisabledRules: true });\n }\n\n function makeActiveIssue(issueID) {\n _activeIssueID = issueID;\n section.selection().selectAll('.issue-container')\n .classed('active', function(d) { return d.id === _activeIssueID; });\n }\n\n function renderDisclosureContent(selection) {\n\n selection.classed('grouped-items-area', true);\n\n _activeIssueID = _issues.length > 0 ? _issues[0].id : null;\n\n var containers = selection.selectAll('.issue-container')\n .data(_issues, function(d) { return d.key; });\n\n // Exit\n containers.exit()\n .remove();\n\n // Enter\n var containersEnter = containers.enter()\n .append('div')\n .attr('class', 'issue-container');\n\n\n var itemsEnter = containersEnter\n .append('div')\n .attr('class', function(d) { return 'issue severity-' + d.severity; })\n .on('mouseover.highlight', function(d3_event, d) {\n // don't hover-highlight the selected entity\n var ids = d.entityIds\n .filter(function(e) { return _entityIDs.indexOf(e) === -1; });\n\n utilHighlightEntities(ids, true, context);\n })\n .on('mouseout.highlight', function(d3_event, d) {\n var ids = d.entityIds\n .filter(function(e) { return _entityIDs.indexOf(e) === -1; });\n\n utilHighlightEntities(ids, false, context);\n });\n\n var labelsEnter = itemsEnter\n .append('div')\n .attr('class', 'issue-label');\n\n var textEnter = labelsEnter\n .append('button')\n .attr('class', 'issue-text')\n .on('click', function(d3_event, d) {\n makeActiveIssue(d.id); // expand only the clicked item\n\n const extent = d.extent(context.graph());\n if (extent) {\n context.map().zoomToEase(extent);\n }\n });\n\n textEnter\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(validationIssue.ICONS[d.severity], 'issue-icon'));\n });\n\n textEnter\n .append('span')\n .attr('class', 'issue-message');\n\n\n var infoButton = labelsEnter\n .append('button')\n .attr('class', 'issue-info-button')\n .attr('title', t('icons.information'))\n .call(svgIcon('#iD-icon-inspect'));\n\n infoButton\n .on('click', function (d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n this.blur(); // avoid keeping focus on the button - #4641\n\n var container = d3_select(this.parentNode.parentNode.parentNode);\n var info = container.selectAll('.issue-info');\n var isExpanded = info.classed('expanded');\n _expanded = !isExpanded;\n prefs('entity-issues.reference.expanded', _expanded); // update preference\n\n if (isExpanded) {\n info\n .transition()\n .duration(200)\n .style('max-height', '0px')\n .style('opacity', '0')\n .on('end', function () {\n info.classed('expanded', false);\n });\n } else {\n info\n .classed('expanded', true)\n .transition()\n .duration(200)\n .style('max-height', '200px')\n .style('opacity', '1')\n .on('end', function () {\n info.style('max-height', null);\n });\n }\n });\n\n itemsEnter\n .append('ul')\n .attr('class', 'issue-fix-list');\n\n containersEnter\n .append('div')\n .attr('class', 'issue-info' + (_expanded ? ' expanded' : ''))\n .style('max-height', (_expanded ? null : '0'))\n .style('opacity', (_expanded ? '1' : '0'))\n .each(function(d) {\n if (typeof d.reference === 'function') {\n d3_select(this)\n .call(d.reference);\n } else {\n d3_select(this)\n .call(t.append('inspector.no_documentation_key'));\n }\n });\n\n\n // Update\n containers = containers\n .merge(containersEnter)\n .classed('active', function(d) { return d.id === _activeIssueID; });\n\n containers.selectAll('.issue-message')\n .text('')\n .each(function(d) {\n return d.message(context)(d3_select(this));\n });\n\n // fixes\n var fixLists = containers.selectAll('.issue-fix-list');\n\n var fixes = fixLists.selectAll('.issue-fix-item')\n .data(function(d) { return d.fixes ? d.fixes(context) : []; }, function(fix) { return fix.id; });\n\n fixes.exit()\n .remove();\n\n var fixesEnter = fixes.enter()\n .append('li')\n .attr('class', 'issue-fix-item');\n\n var buttons = fixesEnter\n .append('button')\n .on('click', function(d3_event, d) {\n // not all fixes are actionable\n if (d3_select(this).attr('disabled') || !d.onClick) return;\n\n // Don't run another fix for this issue within a second of running one\n // (Necessary for \"Select a feature type\" fix. Most fixes should only ever run once)\n if (d.issue.dateLastRanFix && new Date() - d.issue.dateLastRanFix < 1000) return;\n d.issue.dateLastRanFix = new Date();\n\n // remove hover-highlighting\n utilHighlightEntities(d.issue.entityIds.concat(d.entityIds), false, context);\n\n new Promise(function(resolve, reject) {\n d.onClick(context, resolve, reject);\n if (d.onClick.length <= 1) {\n // if the fix doesn't take any completion parameters then consider it resolved\n resolve();\n }\n })\n .then(function() {\n // revalidate whenever the fix has finished running successfully\n context.validator().validate();\n });\n })\n .on('mouseover.highlight', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, true, context);\n })\n .on('mouseout.highlight', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, false, context);\n });\n\n buttons\n .each(function(d) {\n var iconName = d.icon || 'iD-icon-wrench';\n if (iconName.startsWith('maki')) {\n iconName += '-15';\n }\n d3_select(this).call(svgIcon('#' + iconName, 'fix-icon'));\n });\n\n buttons\n .append('span')\n .attr('class', 'fix-message')\n .each(function(d) { return d.title(d3_select(this)); });\n\n fixesEnter.merge(fixes)\n .selectAll('button')\n .classed('actionable', function(d) {\n return d.onClick;\n })\n .attr('disabled', function(d) {\n return d.onClick ? null : 'true';\n })\n .attr('title', function(d) {\n if (d.disabledReason) {\n return d.disabledReason;\n }\n return null;\n });\n }\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n if (!_entityIDs || !val || !utilArrayIdentical(_entityIDs, val)) {\n _entityIDs = val;\n _activeIssueID = null;\n reloadIssues();\n }\n return section;\n };\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { prefs } from '../core/preferences';\nimport { svgIcon, svgTagClasses } from '../svg';\nimport { utilFunctor } from '../util';\n\n\nexport function uiPresetIcon() {\n let _preset;\n let _geometry;\n\n\n function presetIcon(selection) {\n selection.each(render);\n }\n\n\n function getIcon(p, geom) {\n if (p.isFallback && p.isFallback()) return geom === 'vertex' ? '' : 'iD-icon-' + p.id;\n if (p.icon) return p.icon;\n if (geom === 'line') return 'iD-other-line';\n if (geom === 'vertex') return 'temaki-vertex';\n return 'maki-marker-stroked';\n }\n\n\n function renderPointBorder(container, drawPoint) {\n const pointBorder = container.selectAll('.preset-icon-point-border')\n .data(drawPoint ? [0] : []);\n\n pointBorder.exit()\n .remove();\n\n const pointBorderEnter = pointBorder.enter();\n\n const w = 40;\n const h = 40;\n\n pointBorderEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-point-border')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`)\n .append('path')\n .attr('transform', 'translate(11.5, 8)')\n .attr('d', 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z');\n }\n\n\n function renderCategoryBorder(container, category) {\n const categoryBorder = container.selectAll('.preset-icon-category-border')\n .data(category ? [0] : []);\n\n categoryBorder.exit()\n .remove();\n\n const categoryBorderEnter = categoryBorder.enter();\n\n const d = 60;\n\n let svgEnter = categoryBorderEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-category-border')\n .attr('width', d)\n .attr('height', d)\n .attr('viewBox', `0 0 ${d} ${d}`);\n\n svgEnter\n .append('path')\n .attr('class', 'area')\n .attr('d', 'M9.5,7.5 L25.5,7.5 L28.5,12.5 L49.5,12.5 C51.709139,12.5 53.5,14.290861 53.5,16.5 L53.5,43.5 C53.5,45.709139 51.709139,47.5 49.5,47.5 L10.5,47.5 C8.290861,47.5 6.5,45.709139 6.5,43.5 L6.5,12.5 L9.5,7.5 Z');\n\n if (category) {\n categoryBorderEnter.merge(categoryBorder)\n .selectAll('path')\n .attr('class', `area ${category.id}`);\n }\n }\n\n\n function renderCircleFill(container, drawVertex) {\n const vertexFill = container.selectAll('.preset-icon-fill-vertex')\n .data(drawVertex ? [0] : []);\n\n vertexFill.exit()\n .remove();\n\n const vertexFillEnter = vertexFill.enter();\n\n const w = 60;\n const h = 60;\n const d = 40;\n\n vertexFillEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-fill-vertex')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`)\n .append('circle')\n .attr('cx', w / 2)\n .attr('cy', h / 2)\n .attr('r', d / 2);\n }\n\n\n function renderSquareFill(container, drawArea, tagClasses) {\n\n let fill = container.selectAll('.preset-icon-fill-area')\n .data(drawArea ? [0] : []);\n\n fill.exit()\n .remove();\n\n let fillEnter = fill.enter();\n\n const d = 60;\n const w = d;\n const h = d;\n const l = d * 2/3;\n const c1 = (w-l) / 2;\n const c2 = c1 + l;\n\n fillEnter = fillEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-fill-area')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`);\n\n ['fill', 'stroke'].forEach(klass => {\n fillEnter\n .append('path')\n .attr('d', `M${c1} ${c1} L${c1} ${c2} L${c2} ${c2} L${c2} ${c1} Z`)\n .attr('class', `area ${klass}`);\n });\n\n const rVertex = 2.5;\n [[c1, c1], [c1, c2], [c2, c2], [c2, c1]].forEach(point => {\n fillEnter\n .append('circle')\n .attr('class', 'vertex')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', rVertex);\n });\n\n const rMidpoint = 1.25;\n [[c1, w/2], [c2, w/2], [h/2, c1], [h/2, c2]].forEach(point => {\n fillEnter\n .append('circle')\n .attr('class', 'midpoint')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', rMidpoint);\n });\n\n fill = fillEnter.merge(fill);\n\n fill.selectAll('path.stroke')\n .attr('class', `area stroke ${tagClasses}`);\n fill.selectAll('path.fill')\n .attr('class', `area fill ${tagClasses}`);\n }\n\n\n function renderLine(container, drawLine, tagClasses) {\n\n let line = container.selectAll('.preset-icon-line')\n .data(drawLine ? [0] : []);\n\n line.exit()\n .remove();\n\n let lineEnter = line.enter();\n\n const d = 60;\n // draw the line parametrically\n const w = d;\n const h = d;\n const y = Math.round(d * 0.72);\n const l = Math.round(d * 0.6);\n const r = 2.5;\n const x1 = (w - l) / 2;\n const x2 = x1 + l;\n\n lineEnter = lineEnter\n .append('svg')\n .attr('class', 'preset-icon-line')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`);\n\n ['casing', 'stroke'].forEach(klass => {\n lineEnter\n .append('path')\n .attr('d', `M${x1} ${y} L${x2} ${y}`)\n .attr('class', `line ${klass}`);\n });\n\n [[x1-1, y], [x2+1, y]].forEach(point => {\n lineEnter\n .append('circle')\n .attr('class', 'vertex')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', r);\n });\n\n line = lineEnter.merge(line);\n\n line.selectAll('path.stroke')\n .attr('class', `line stroke ${tagClasses}`);\n line.selectAll('path.casing')\n .attr('class', `line casing ${tagClasses}`);\n }\n\n\n function renderRoute(container, drawRoute, p) {\n\n let route = container.selectAll('.preset-icon-route')\n .data(drawRoute ? [0] : []);\n\n route.exit()\n .remove();\n\n let routeEnter = route.enter();\n\n const d = 60;\n // draw the route parametrically\n const w = d;\n const h = d;\n const y1 = Math.round(d * 0.80);\n const y2 = Math.round(d * 0.68);\n const l = Math.round(d * 0.6);\n const r = 2;\n const x1 = (w - l) / 2;\n const x2 = x1 + l / 3;\n const x3 = x2 + l / 3;\n const x4 = x3 + l / 3;\n\n routeEnter = routeEnter\n .append('svg')\n .attr('class', 'preset-icon-route')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`);\n\n ['casing', 'stroke'].forEach(klass => {\n routeEnter\n .append('path')\n .attr('d', `M${x1} ${y1} L${x2} ${y2}`)\n .attr('class', `segment0 line ${klass}`);\n routeEnter\n .append('path')\n .attr('d', `M${x2} ${y2} L${x3} ${y1}`)\n .attr('class', `segment1 line ${klass}`);\n routeEnter\n .append('path')\n .attr('d', `M${x3} ${y1} L${x4} ${y2}`)\n .attr('class', `segment2 line ${klass}`);\n });\n\n [[x1, y1], [x2, y2], [x3, y1], [x4, y2]].forEach(point => {\n routeEnter\n .append('circle')\n .attr('class', 'vertex')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', r);\n });\n\n route = routeEnter.merge(route);\n\n if (drawRoute) {\n let routeType = p.tags.type === 'waterway' ? 'waterway' : p.tags.route;\n const segmentPresetIDs = routeSegments[routeType];\n for (let i in segmentPresetIDs) {\n const segmentPreset = presetManager.item(segmentPresetIDs[i]);\n const segmentTagClasses = svgTagClasses().getClassesString(segmentPreset.tags, '');\n route.selectAll(`path.stroke.segment${i}`)\n .attr('class', `segment${i} line stroke ${segmentTagClasses}`);\n route.selectAll(`path.casing.segment${i}`)\n .attr('class', `segment${i} line casing ${segmentTagClasses}`);\n }\n }\n }\n\n function renderSvgIcon(container, picon, geom, isFramed, category, tagClasses) {\n const isMaki = picon && /^maki-/.test(picon);\n const isTemaki = picon && /^temaki-/.test(picon);\n const isFa = picon && /^fa[srb]-/.test(picon);\n const isR\u00F6ntgen = picon && /^roentgen-/.test(picon);\n const isiDIcon = picon && !(isMaki || isTemaki || isFa || isR\u00F6ntgen);\n\n let icon = container.selectAll('.preset-icon')\n .data(picon ? [0] : []);\n\n icon.exit()\n .remove();\n\n icon = icon.enter()\n .append('div')\n .attr('class', 'preset-icon')\n .call(svgIcon(''))\n .merge(icon);\n\n icon\n .attr('class', 'preset-icon ' + (geom ? geom + '-geom' : ''))\n .classed('category', category)\n .classed('framed', isFramed)\n .classed('preset-icon-iD', isiDIcon);\n\n icon.selectAll('svg')\n .attr('class', 'icon ' + picon + ' ' + (!isiDIcon && geom !== 'line' ? '' : tagClasses));\n\n icon.selectAll('use')\n .attr('href', '#' + picon);\n }\n\n\n function renderImageIcon(container, imageURL) {\n let imageIcon = container.selectAll('img.image-icon')\n .data(imageURL ? [0] : []);\n\n imageIcon.exit()\n .remove();\n\n imageIcon = imageIcon.enter()\n .append('img')\n .attr('class', 'image-icon')\n .on('load', () => container.classed('showing-img', true) )\n .on('error', () => container.classed('showing-img', false) )\n .merge(imageIcon);\n\n imageIcon\n .attr('src', imageURL);\n }\n\n // Route icons are drawn with a zigzag annotation underneath:\n // o o\n // / \\ /\n // o o\n // This dataset defines the styles that are used to draw the zigzag segments.\n const routeSegments = {\n bicycle: ['highway/cycleway', 'highway/cycleway', 'highway/cycleway'],\n bus: ['highway/unclassified', 'highway/secondary', 'highway/primary'],\n climbing: ['climbing/route', 'climbing/route', 'climbing/route'],\n trolleybus: ['highway/unclassified', 'highway/secondary', 'highway/primary'],\n detour: ['highway/tertiary', 'highway/residential', 'highway/unclassified'],\n ferry: ['route/ferry', 'route/ferry', 'route/ferry'],\n foot: ['highway/footway', 'highway/footway', 'highway/footway'],\n hiking: ['highway/path', 'highway/path', 'highway/path'],\n horse: ['highway/bridleway', 'highway/bridleway', 'highway/bridleway'],\n light_rail: ['railway/light_rail', 'railway/light_rail', 'railway/light_rail'],\n monorail: ['railway/monorail', 'railway/monorail', 'railway/monorail'],\n mtb: ['highway/path', 'highway/track', 'highway/bridleway'],\n pipeline: ['man_made/pipeline', 'man_made/pipeline', 'man_made/pipeline'],\n piste: ['piste/downhill', 'piste/hike', 'piste/nordic'],\n power: ['power/line', 'power/line', 'power/line'],\n road: ['highway/secondary', 'highway/primary', 'highway/trunk'],\n subway: ['railway/subway', 'railway/subway', 'railway/subway'],\n train: ['railway/rail', 'railway/rail', 'railway/rail'],\n tram: ['railway/tram', 'railway/tram', 'railway/tram'],\n railway: ['railway/rail', 'railway/rail', 'railway/rail'],\n waterway: ['waterway/stream', 'waterway/stream', 'waterway/stream']\n };\n\n\n function render() {\n let p = _preset.apply(this, arguments);\n let geom = _geometry ? _geometry.apply(this, arguments) : null;\n if (geom === 'relation' &&\n p.tags &&\n ((p.tags.type === 'route' && p.tags.route && routeSegments[p.tags.route]) || p.tags.type === 'waterway')) {\n geom = 'route';\n }\n\n const showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n const isFallback = p.isFallback && p.isFallback();\n const imageURL = (showThirdPartyIcons === 'true') && p.imageURL;\n const picon = getIcon(p, geom);\n const isCategory = !p.setTags;\n const drawPoint = false;\n const drawVertex = picon !== null && geom === 'vertex';\n const drawLine = picon && geom === 'line' && !isFallback && !isCategory;\n const drawArea = picon && geom === 'area' && !isFallback && !isCategory;\n const drawRoute = picon && geom === 'route';\n const isFramed = drawVertex || drawArea || drawLine || drawRoute || isCategory;\n\n let tags = !isCategory ? p.setTags({}, geom) : {};\n for (let k in tags) {\n if (tags[k] === '*') {\n tags[k] = 'yes';\n }\n }\n\n let tagClasses = svgTagClasses().getClassesString(tags, '');\n let selection = d3_select(this);\n\n let container = selection.selectAll('.preset-icon-container')\n .data([0]);\n\n container = container.enter()\n .append('div')\n .attr('class', 'preset-icon-container')\n .merge(container);\n\n container\n .classed('showing-img', !!imageURL)\n .classed('fallback', isFallback);\n\n renderCategoryBorder(container, isCategory && p);\n renderPointBorder(container, drawPoint);\n renderCircleFill(container, drawVertex);\n renderSquareFill(container, drawArea, tagClasses);\n renderLine(container, drawLine, tagClasses);\n renderRoute(container, drawRoute, p);\n renderSvgIcon(container, picon, geom, isFramed, isCategory, tagClasses);\n renderImageIcon(container, imageURL);\n }\n\n\n presetIcon.preset = function(val) {\n if (!arguments.length) return _preset;\n _preset = utilFunctor(val);\n return presetIcon;\n };\n\n\n presetIcon.geometry = function(val) {\n if (!arguments.length) return _geometry;\n _geometry = utilFunctor(val);\n return presetIcon;\n };\n\n return presetIcon;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { utilArrayIdentical } from '../../util/array';\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { utilRebind } from '../../util';\nimport { uiPresetIcon } from '../preset_icon';\nimport { uiSection } from '../section';\nimport { uiTagReference } from '../tag_reference';\n\n\nexport function uiSectionFeatureType(context) {\n\n var dispatch = d3_dispatch('choose');\n\n var _entityIDs = [];\n var _presets = [];\n\n var _tagReference;\n\n var section = uiSection('feature-type', context)\n .label(() => t.append('inspector.feature_type'))\n .disclosureContent(renderDisclosureContent);\n\n function renderDisclosureContent(selection) {\n\n selection.classed('preset-list-item', true);\n selection.classed('mixed-types', _presets.length > 1);\n\n var presetButtonWrap = selection\n .selectAll('.preset-list-button-wrap')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'preset-list-button-wrap');\n\n var presetButton = presetButtonWrap\n .append('button')\n .attr('class', 'preset-list-button preset-reset')\n .call(uiTooltip()\n .title(() => t.append('inspector.back_tooltip'))\n .placement('bottom')\n );\n\n presetButton.append('div')\n .attr('class', 'preset-icon-container');\n\n presetButton\n .append('div')\n .attr('class', 'label')\n .append('div')\n .attr('class', 'label-inner');\n\n presetButtonWrap.append('div')\n .attr('class', 'accessory-buttons');\n\n var tagReferenceBodyWrap = selection\n .selectAll('.tag-reference-body-wrap')\n .data([0]);\n\n tagReferenceBodyWrap = tagReferenceBodyWrap\n .enter()\n .append('div')\n .attr('class', 'tag-reference-body-wrap')\n .merge(tagReferenceBodyWrap);\n\n // update header\n if (_tagReference) {\n selection.selectAll('.preset-list-button-wrap .accessory-buttons')\n .style('display', _presets.length === 1 ? null : 'none')\n .call(_tagReference.button);\n\n tagReferenceBodyWrap\n .style('display', _presets.length === 1 ? null : 'none')\n .call(_tagReference.body);\n }\n\n selection.selectAll('.preset-reset')\n .on('click', function() {\n dispatch.call('choose', this, _presets);\n })\n .on('pointerdown pointerup mousedown mouseup', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n });\n\n var geometries = entityGeometries();\n selection.select('.preset-list-item button')\n .call(uiPresetIcon()\n .geometry(_presets.length === 1 ? (geometries.length === 1 && geometries[0]) : null)\n .preset(_presets.length === 1 ? _presets[0] : presetManager.item('point'))\n );\n\n var names = _presets.length === 1 ? [\n _presets[0].nameLabel(),\n _presets[0].subtitleLabel()\n ].filter(Boolean) : [ t.append('inspector.multiple_types') ];\n\n var label = selection.select('.label-inner');\n var nameparts = label.selectAll('.namepart')\n .data(names, d => d.stringId);\n\n nameparts.exit()\n .remove();\n\n nameparts\n .enter()\n .append('div')\n .attr('class', 'namepart')\n .text('')\n .each(function(d) { d(d3_select(this)); });\n }\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return section;\n };\n\n section.presets = function(val) {\n if (!arguments.length) return _presets;\n\n // don't reload the same preset\n if (!utilArrayIdentical(val, _presets)) {\n _presets = val;\n\n if (_presets.length === 1) {\n _tagReference = uiTagReference(_presets[0].reference(), context)\n .showing(false);\n }\n }\n\n return section;\n };\n\n function entityGeometries() {\n\n var counts = {};\n\n for (var i in _entityIDs) {\n var geometry = context.graph().geometry(_entityIDs[i]);\n if (!counts[geometry]) counts[geometry] = 0;\n counts[geometry] += 1;\n }\n\n return Object.keys(counts).sort(function(geom1, geom2) {\n return counts[geom2] - counts[geom1];\n });\n }\n\n return utilRebind(section, dispatch, 'on');\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { uiCombobox } from './combobox';\nimport { utilGetSetValue, utilNoAuto } from '../util';\n\n\nexport function uiFormFields(context) {\n var moreCombo = uiCombobox(context, 'more-fields').minItems(1);\n var _fieldsArr = [];\n var _lastPlaceholder = '';\n var _state = '';\n var _klass = '';\n\n\n function formFields(selection) {\n var allowedFields = _fieldsArr.filter(function(field) { return field.isAllowed(); });\n var shown = allowedFields.filter(function(field) { return field.isShown(); });\n var notShown = allowedFields.filter(function(field) { return !field.isShown(); })\n .sort(function(a, b) { return (a.universal === b.universal ? 0 : a.universal ? 1 : -1); });\n\n var container = selection.selectAll('.form-fields-container')\n .data([0]);\n\n container = container.enter()\n .append('div')\n .attr('class', 'form-fields-container ' + (_klass || ''))\n .merge(container);\n\n\n var fields = container.selectAll('.wrap-form-field')\n .data(shown, function(d) { return d.id + (d.entityIDs ? d.entityIDs.join() : ''); });\n\n fields.exit()\n .remove();\n\n // Enter\n var enter = fields.enter()\n .append('div')\n .attr('class', function(d) { return 'wrap-form-field wrap-form-field-' + d.safeid; });\n\n // Update\n fields = fields\n .merge(enter);\n\n fields\n .order()\n .each(function(d) {\n d3_select(this)\n .call(d.render);\n });\n\n\n var titles = [];\n var moreFields = notShown.map(function(field) {\n var title = field.title();\n titles.push(title);\n\n var terms = field.terms();\n if (field.key) terms.push(field.key);\n if (field.keys) terms = terms.concat(field.keys);\n\n return {\n display: field.label(),\n value: title,\n title: title,\n field: field,\n terms: terms\n };\n });\n\n var placeholder = titles.slice(0,3).join(', ') + ((titles.length > 3) ? '\u2026' : '');\n\n\n var more = selection.selectAll('.more-fields')\n .data((_state === 'hover' || moreFields.length === 0) ? [] : [0]);\n\n more.exit()\n .remove();\n\n var moreEnter = more.enter()\n .append('div')\n .attr('class', 'more-fields')\n .append('label');\n\n moreEnter\n .append('span')\n .call(t.append('inspector.add_fields'));\n\n more = moreEnter\n .merge(more);\n\n\n var input = more.selectAll('.value')\n .data([0]);\n\n input.exit()\n .remove();\n\n input = input.enter()\n .append('input')\n .attr('class', 'value')\n .attr('type', 'text')\n .attr('placeholder', placeholder)\n .call(utilNoAuto)\n .merge(input);\n\n input\n .call(utilGetSetValue, '')\n .call(moreCombo\n .data(moreFields)\n .on('accept', function (d) {\n if (!d) return; // user entered something that was not matched\n var field = d.field;\n field.show();\n selection.call(formFields); // rerender\n field.focus();\n })\n );\n\n // avoid updating placeholder excessively (triggers style recalc)\n if (_lastPlaceholder !== placeholder) {\n input.attr('placeholder', placeholder);\n _lastPlaceholder = placeholder;\n }\n }\n\n\n formFields.fieldsArr = function(val) {\n if (!arguments.length) return _fieldsArr;\n _fieldsArr = val || [];\n return formFields;\n };\n\n formFields.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n return formFields;\n };\n\n formFields.klass = function(val) {\n if (!arguments.length) return _klass;\n _klass = val;\n return formFields;\n };\n\n\n return formFields;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { presetManager } from '../../presets';\nimport { t, localizer } from '../../core/localizer';\nimport { utilArrayIdentical } from '../../util/array';\nimport { utilArrayUnion, utilRebind } from '../../util';\nimport { geoExtent } from '../../geo/extent';\nimport { uiField } from '../field';\nimport { uiFormFields } from '../form_fields';\nimport { uiSection } from '../section';\n\nexport function uiSectionPresetFields(context) {\n\n var section = uiSection('preset-fields', context)\n .label(() => t.append('inspector.fields'))\n .disclosureContent(renderDisclosureContent);\n\n var dispatch = d3_dispatch('change', 'revert');\n var formFields = uiFormFields(context);\n var _state;\n var _fieldsArr;\n var _presets = [];\n var _tags;\n var _entityIDs;\n\n function renderDisclosureContent(selection) {\n if (!_fieldsArr) {\n\n var graph = context.graph();\n\n var geometries = Object.keys(_entityIDs.reduce(function(geoms, entityID) {\n geoms[graph.entity(entityID).geometry(graph)] = true;\n return geoms;\n }, {}));\n\n const loc = _entityIDs.reduce(function(extent, entityID) {\n var entity = context.graph().entity(entityID);\n return extent.extend(entity.extent(context.graph()));\n }, geoExtent()).center();\n\n var presetsManager = presetManager;\n\n var allFields = [];\n var allMoreFields = [];\n var sharedTotalFields;\n\n _presets.forEach(function(preset) {\n var fields = preset.fields(loc);\n var moreFields = preset.moreFields(loc);\n\n allFields = utilArrayUnion(allFields, fields);\n allMoreFields = utilArrayUnion(allMoreFields, moreFields);\n\n if (!sharedTotalFields) {\n sharedTotalFields = utilArrayUnion(fields, moreFields);\n } else {\n sharedTotalFields = sharedTotalFields.filter(function(field) {\n return fields.indexOf(field) !== -1 || moreFields.indexOf(field) !== -1;\n });\n }\n });\n\n var sharedFields = allFields.filter(function(field) {\n return sharedTotalFields.indexOf(field) !== -1;\n });\n var sharedMoreFields = allMoreFields.filter(function(field) {\n return sharedTotalFields.indexOf(field) !== -1;\n });\n\n _fieldsArr = [];\n\n // Ideally, everything in OpenHistoricalMap is dated and sourced.\n let coreKeys = ['start_date', 'end_date', 'source_preset'];\n coreKeys.forEach(key => {\n let field = presetsManager.field(key);\n if (field) {\n _fieldsArr.push(uiField(context, field, _entityIDs));\n }\n });\n\n let optionalCoreKeys = ['source_preset:1', 'source_preset:2', 'source_preset:3'];\n optionalCoreKeys.forEach(key => {\n let field = presetsManager.field(key);\n if (field && !_fieldsArr.includes(field)) {\n _fieldsArr.push(uiField(context, field, _entityIDs, { show: false }));\n }\n });\n\n\n sharedFields.forEach(function(field) {\n if (!coreKeys.includes(field.id) && !optionalCoreKeys.includes(field.id) && field.matchAllGeometry(geometries)) {\n _fieldsArr.push(\n uiField(context, field, _entityIDs)\n );\n }\n });\n\n var singularEntity = _entityIDs.length === 1 && graph.hasEntity(_entityIDs[0]);\n if (singularEntity && singularEntity.type === 'node' && singularEntity.isHighwayIntersection(graph) && presetsManager.field('restrictions')) {\n _fieldsArr.push(\n uiField(context, presetsManager.field('restrictions'), _entityIDs)\n );\n }\n\n var additionalFields = utilArrayUnion(sharedMoreFields, presetsManager.universal());\n additionalFields.sort(function(field1, field2) {\n return field1.title().localeCompare(field2.title(), localizer.localeCode());\n });\n\n additionalFields.forEach(function(field) {\n if (sharedFields.indexOf(field) === -1 &&\n !coreKeys.includes(field.id) &&\n !optionalCoreKeys.includes(field.id) &&\n field.matchAllGeometry(geometries)) {\n _fieldsArr.push(\n uiField(context, field, _entityIDs, { show: false })\n );\n }\n });\n\n _fieldsArr.forEach(function(field) {\n field\n .on('change', function(t, onInput) {\n dispatch.call('change', field, _entityIDs, t, onInput);\n })\n .on('revert', function(keys) {\n dispatch.call('revert', field, keys);\n });\n });\n }\n\n _fieldsArr.forEach(function(field) {\n field\n .state(_state)\n .tags(_tags);\n });\n\n\n selection\n .call(formFields\n .fieldsArr(_fieldsArr)\n .state(_state)\n .klass('grouped-items-area')\n );\n }\n\n section.presets = function(val) {\n if (!arguments.length) return _presets;\n if (!_presets || !val || !utilArrayIdentical(_presets, val)) {\n _presets = val;\n _fieldsArr = null;\n }\n return section;\n };\n\n section.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n return section;\n };\n\n section.tags = function(val) {\n if (!arguments.length) return _tags;\n _tags = val;\n // Don't reset _fieldsArr here.\n return section;\n };\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n if (!val || !_entityIDs || !utilArrayIdentical(_entityIDs, val)) {\n _entityIDs = val;\n _fieldsArr = null;\n }\n return section;\n };\n\n return utilRebind(section, dispatch, 'on');\n}\n", "import { drag as d3_drag } from 'd3-drag';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { actionChangeMember } from '../../actions/change_member';\nimport { actionDeleteMember } from '../../actions/delete_member';\nimport { actionMoveMember } from '../../actions/move_member';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { osmEntity } from '../../osm';\nimport { getRelationColor } from '../../osm/tags';\nimport { svgIcon } from '../../svg/icon';\nimport { services } from '../../services';\nimport { uiCombobox } from '../combobox';\nimport { uiSection } from '../section';\nimport { utilDisplayName, utilDisplayType, utilHighlightEntities, utilNoAuto, utilUniqueDomId } from '../../util';\nimport { prefs } from '../../core';\n\n\nexport function uiSectionRawMemberEditor(context) {\n\n var section = uiSection('raw-member-editor', context)\n .shouldDisplay(function() {\n if (!_entityIDs || _entityIDs.length !== 1) return false;\n\n var entity = context.hasEntity(_entityIDs[0]);\n return entity && entity.type === 'relation';\n })\n .label(function() {\n var entity = context.hasEntity(_entityIDs[0]);\n if (!entity) return '';\n\n var gt = entity.members.length > _maxMembers ? '>' : '';\n var count = gt + entity.members.slice(0, _maxMembers).length;\n return t.append('inspector.title_count', { title: t.append('inspector.members'), count: count });\n })\n .disclosureContent(renderDisclosureContent);\n\n var taginfo = services.taginfo;\n var _entityIDs;\n var _maxMembers = 1000;\n\n function downloadMember(d3_event, d) {\n d3_event.preventDefault();\n\n // display the loading indicator\n d3_select(this).classed('loading', true);\n context.loadEntity(d.id, function() {\n section.reRender();\n });\n }\n\n function zoomToMember(d3_event, d) {\n d3_event.preventDefault();\n\n var entity = context.entity(d.id);\n context.map().zoomToEase(entity);\n\n // highlight the feature in case it wasn't previously on-screen\n utilHighlightEntities([d.id], true, context);\n }\n\n\n function selectMember(d3_event, d) {\n d3_event.preventDefault();\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.id], false, context);\n\n var entity = context.entity(d.id);\n var mapExtent = context.map().extent();\n if (!entity.intersects(mapExtent, context.graph())) {\n // zoom to the entity if its extent is not visible now\n context.map().zoomToEase(entity);\n }\n\n context.enter(modeSelect(context, [d.id]));\n }\n\n\n function changeRole(d3_event, d) {\n var oldRole = d.role;\n var newRole = context.cleanRelationRole(d3_select(this).property('value'));\n\n if (oldRole !== newRole) {\n var member = { id: d.id, type: d.type, role: newRole };\n context.perform(\n actionChangeMember(d.relation.id, member, d.index),\n t('operations.change_role.annotation', {\n n: 1\n })\n );\n context.validator().validate();\n }\n }\n\n\n function deleteMember(d3_event, d) {\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.id], false, context);\n\n context.perform(\n actionDeleteMember(d.relation.id, d.index),\n t('operations.delete_member.annotation', {\n n: 1\n })\n );\n\n if (!context.hasEntity(d.relation.id)) {\n // Removing the last member will also delete the relation.\n // If this happens we need to exit the selection mode\n context.enter(modeBrowse(context));\n } else {\n // Changing the mode also runs `validate`, but otherwise we need to\n // rerun it manually\n context.validator().validate();\n }\n }\n\n function renderDisclosureContent(selection) {\n\n var entityID = _entityIDs[0];\n\n var memberships = [];\n var entity = context.entity(entityID);\n entity.members.slice(0, _maxMembers).forEach(function(member, index) {\n memberships.push({\n index: index,\n id: member.id,\n type: member.type,\n role: member.role,\n relation: entity,\n member: context.hasEntity(member.id),\n domId: utilUniqueDomId(entityID + '-member-' + index)\n });\n });\n\n var list = selection.selectAll('.member-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'member-list')\n .merge(list);\n\n\n var items = list.selectAll('li')\n .data(memberships, function(d) {\n return osmEntity.key(d.relation) + ',' + d.index + ',' +\n (d.member ? osmEntity.key(d.member) : 'incomplete');\n });\n\n items.exit()\n .each(unbind)\n .remove();\n\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', 'member-row form-field')\n .classed('member-incomplete', function(d) { return !d.member; });\n\n itemsEnter\n .each(function(d) {\n var item = d3_select(this);\n\n var label = item\n .append('label')\n .attr('class', 'field-label')\n .attr('for', d.domId);\n\n if (d.member) {\n // highlight the member feature in the map while hovering on the list item\n item\n .on('mouseover', function() {\n utilHighlightEntities([d.id], true, context);\n })\n .on('mouseout', function() {\n utilHighlightEntities([d.id], false, context);\n });\n\n var labelLink = label\n .append('span')\n .attr('class', 'label-text')\n .append('a')\n .attr('href', '#')\n .on('click', selectMember);\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-type')\n .text(function(d) {\n var matched = presetManager.match(d.member, context.graph());\n while (matched?.suggestion) {\n // if is NSI preset: look for a parent preset\n matched = matched.getParentPreset();\n }\n return (matched && matched.name()) || utilDisplayType(d.member.id);\n });\n\n const showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n labelLink.each(function(d) {\n if (!showThirdPartyIcons) return;\n const matched = presetManager.match(d, context.graph());\n if (matched.suggestion) {\n // if matching an NSI preset: append icon\n d3_select(this)\n .append('img')\n .classed('member-entity-icon', true)\n .attr('src', matched.imageURL);\n }\n });\n\n labelLink.each(function(d) {\n if (d.type !== 'relation') return;\n const relColors = getRelationColor(d.member.tags, '#555');\n const hasRef = d.member.tags.ref;\n if (relColors.isValid || hasRef) {\n const refs = (d.member.tags.ref || '').split(';');\n for (const ref of refs) {\n d3_select(this)\n .append('span')\n .classed('member-entity-ref-color', true)\n .style('border-color', relColors.color)\n .style('background-color', relColors.color)\n .style('color', relColors.textColor)\n .text(ref);\n }\n }\n });\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-name')\n .text(function(d) { return utilDisplayName(d.member, { hideRef: true }); });\n\n label\n .append('button')\n .attr('title', t('icons.remove'))\n .attr('class', 'remove member-delete')\n .call(svgIcon('#iD-operation-delete'));\n\n label\n .append('button')\n .attr('class', 'member-zoom')\n .attr('title', t('icons.zoom_to'))\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'))\n .on('click', zoomToMember);\n\n } else {\n var labelText = label\n .append('span')\n .attr('class', 'label-text');\n\n labelText\n .append('span')\n .attr('class', 'member-entity-type')\n .call(t.append('inspector.' + d.type, { id: d.id }));\n\n labelText\n .append('span')\n .attr('class', 'member-entity-name')\n .call(t.append('inspector.incomplete', { id: d.id }));\n\n label\n .append('button')\n .attr('class', 'member-download')\n .attr('title', t('icons.download'))\n .call(svgIcon('#iD-icon-load'))\n .on('click', downloadMember);\n }\n });\n\n var wrapEnter = itemsEnter\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-member');\n\n wrapEnter\n .append('input')\n .attr('class', 'member-role')\n .attr('id', function(d) {\n return d.domId;\n })\n .property('type', 'text')\n .attr('placeholder', t('inspector.role'))\n .call(utilNoAuto);\n\n if (taginfo) {\n wrapEnter.each(bindTypeahead);\n }\n\n // update\n items = items\n .merge(itemsEnter)\n .order();\n\n items.select('input.member-role')\n .property('value', function(d) { return d.role; })\n .on('blur', changeRole)\n .on('change', changeRole);\n\n items.select('button.member-delete')\n .on('click', deleteMember);\n\n var dragOrigin, targetIndex;\n\n items.call(d3_drag()\n .on('start', function(d3_event) {\n dragOrigin = {\n x: d3_event.x,\n y: d3_event.y\n };\n targetIndex = null;\n })\n .on('drag', function(d3_event) {\n var x = d3_event.x - dragOrigin.x,\n y = d3_event.y - dragOrigin.y;\n\n if (!d3_select(this).classed('dragging') &&\n // don't display drag until dragging beyond a distance threshold\n Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) <= 5) return;\n\n var index = items.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', true);\n\n targetIndex = null;\n\n selection.selectAll('li.member-row')\n .style('transform', function(d2, index2) {\n var node = d3_select(this).node();\n if (index === index2) {\n return 'translate(' + x + 'px, ' + y + 'px)';\n } else if (index2 > index && d3_event.y > node.offsetTop) {\n if (targetIndex === null || index2 > targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(-100%)';\n } else if (index2 < index && d3_event.y < node.offsetTop + node.offsetHeight) {\n if (targetIndex === null || index2 < targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(100%)';\n }\n return null;\n });\n })\n .on('end', function(d3_event, d) {\n\n if (!d3_select(this).classed('dragging')) return;\n\n var index = items.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', false);\n\n selection.selectAll('li.member-row')\n .style('transform', null);\n\n if (targetIndex !== null) {\n // dragged to a new position, reorder\n context.perform(\n actionMoveMember(d.relation.id, index, targetIndex),\n t('operations.reorder_members.annotation')\n );\n context.validator().validate();\n }\n })\n );\n\n\n\n function bindTypeahead(d) {\n var row = d3_select(this);\n var role = row.selectAll('input.member-role');\n var origValue = role.property('value');\n\n function sort(value, data) {\n var sameletter = [];\n var other = [];\n for (var i = 0; i < data.length; i++) {\n if (data[i].value.substring(0, value.length) === value) {\n sameletter.push(data[i]);\n } else {\n other.push(data[i]);\n }\n }\n return sameletter.concat(other);\n }\n\n role.call(uiCombobox(context, 'member-role')\n .fetcher(function(role, callback) {\n // The `geometry` param is used in the `taginfo.js` interface for\n // filtering results, as a key into the `tag_members_fractions`\n // object. If we don't know the geometry because the member is\n // not yet downloaded, it's ok to guess based on type.\n var geometry;\n if (d.member) {\n geometry = context.graph().geometry(d.member.id);\n } else if (d.type === 'relation') {\n geometry = 'relation';\n } else if (d.type === 'way') {\n geometry = 'line';\n } else {\n geometry = 'point';\n }\n\n var rtype = entity.tags.type;\n taginfo.roles({\n debounce: true,\n rtype: rtype || '',\n geometry: geometry,\n query: role\n }, function(err, data) {\n if (!err) callback(sort(role, data));\n });\n })\n .on('cancel', function() {\n role.property('value', origValue);\n })\n );\n }\n\n\n function unbind() {\n var row = d3_select(this);\n\n row.selectAll('input.member-role')\n .call(uiCombobox.off, context);\n }\n }\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return section;\n };\n\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t, localizer } from '../../core/localizer';\n\nimport { actionAddEntity } from '../../actions/add_entity';\nimport { actionAddMember } from '../../actions/add_member';\nimport { actionChangeMember } from '../../actions/change_member';\nimport { actionDeleteMembers } from '../../actions/delete_members';\n\nimport { modeSelect } from '../../modes/select';\nimport { osmEntity, osmRelation } from '../../osm';\nimport { getRelationColor, isColorValid } from '../../osm/tags';\nimport { services } from '../../services';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCombobox } from '../combobox';\nimport { uiSection } from '../section';\nimport { uiTooltip } from '../tooltip';\nimport { utilArrayGroupBy, utilArrayIntersection } from '../../util/array';\nimport { utilDisplayName, utilNoAuto, utilHighlightEntities, utilUniqueDomId } from '../../util';\nimport { prefs } from '../../core';\nimport { idMatch } from '../feature_list';\n\n\nexport function uiSectionRawMembershipEditor(context) {\n\n var section = uiSection('raw-membership-editor', context)\n .shouldDisplay(function() {\n return _entityIDs && _entityIDs.length;\n })\n .label(function() {\n var parents = getSharedParentRelations();\n var gt = parents.length > _maxMemberships ? '>' : '';\n var count = gt + parents.slice(0, _maxMemberships).length;\n return t.append('inspector.title_count', { title: t.append('inspector.relations'), count: count });\n })\n .disclosureContent(renderDisclosureContent);\n\n var taginfo = services.taginfo;\n var nearbyCombo = uiCombobox(context, 'parent-relation')\n .minItems(1)\n .fetcher(fetchNearbyRelations)\n .itemsMouseEnter(function(d3_event, d) {\n if (d.relation) utilHighlightEntities([d.relation.id], true, context);\n })\n .itemsMouseLeave(function(d3_event, d) {\n if (d.relation) utilHighlightEntities([d.relation.id], false, context);\n });\n var _inChange = false;\n var _entityIDs = [];\n var _showBlank;\n var _maxMemberships = 1000;\n /** @type {Set} relations that were added after this panel was opened */\n const recentlyAdded = new Set();\n\n function getSharedParentRelations() {\n var parents = [];\n for (var i = 0; i < _entityIDs.length; i++) {\n var entity = context.graph().hasEntity(_entityIDs[i]);\n if (!entity) continue;\n\n if (i === 0) {\n parents = context.graph().parentRelations(entity);\n } else {\n parents = utilArrayIntersection(parents, context.graph().parentRelations(entity));\n }\n if (!parents.length) break;\n }\n return parents;\n }\n\n function getMemberships() {\n\n var memberships = [];\n var relations = getSharedParentRelations().slice(0, _maxMemberships);\n\n var isMultiselect = _entityIDs.length > 1;\n\n var i, relation, membership, index, member, indexedMember;\n for (i = 0; i < relations.length; i++) {\n relation = relations[i];\n membership = {\n relation: relation,\n members: [],\n hash: osmEntity.key(relation)\n };\n for (index = 0; index < relation.members.length; index++) {\n member = relation.members[index];\n if (_entityIDs.indexOf(member.id) !== -1) {\n indexedMember = Object.assign({}, member, { index: index });\n membership.members.push(indexedMember);\n membership.hash += ',' + index.toString();\n\n if (!isMultiselect) {\n // For single selections, list one entry per membership per relation.\n // For multiselections, list one entry per relation.\n\n memberships.push(membership);\n membership = {\n relation: relation,\n members: [],\n hash: osmEntity.key(relation)\n };\n }\n }\n }\n if (membership.members.length) memberships.push(membership);\n }\n\n memberships.forEach(function(membership) {\n membership.domId = utilUniqueDomId('membership-' + membership.relation.id);\n var roles = [];\n membership.members.forEach(function(member) {\n if (roles.indexOf(member.role) === -1) roles.push(member.role);\n });\n membership.role = roles.length === 1 ? roles[0] : roles;\n });\n\n const existingRelations = memberships\n .filter(membership => !recentlyAdded.has(membership.relation.id))\n .map(membership => ({\n ...membership,\n // We only sort relations that were not added just now.\n // Sorting uses the same label as shown in the UI.\n // If the label is not unique, the relation ID ensures\n // that the sort order is still stable.\n _sortKey: [\n baseDisplayValue(membership.relation),\n membership.relation.id,\n ].join('-'),\n }))\n .sort((a, b) => {\n return a._sortKey.localeCompare(\n b._sortKey,\n localizer.localeCodes(),\n { numeric: true },\n );\n });\n\n\n const newlyAddedRelations = memberships\n .filter(membership => recentlyAdded.has(membership.relation.id));\n\n return [\n // the sorted relations come first\n ...existingRelations,\n // then the ones that were just added from this panel\n ...newlyAddedRelations,\n ];\n }\n\n function selectRelation(d3_event, d) {\n d3_event.preventDefault();\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.relation.id], false, context);\n\n context.enter(modeSelect(context, [d.relation.id]));\n }\n\n function zoomToRelation(d3_event, d) {\n d3_event.preventDefault();\n\n var entity = context.entity(d.relation.id);\n context.map().zoomToEase(entity);\n\n // highlight the relation in case it wasn't previously on-screen\n utilHighlightEntities([d.relation.id], true, context);\n }\n\n\n function changeRole(d3_event, d) {\n if (d === 0) return; // called on newrow (shouldn't happen)\n if (_inChange) return; // avoid accidental recursive call #5731\n\n var newRole = context.cleanRelationRole(d3_select(this).property('value'));\n\n if (!newRole.trim() && typeof d.role !== 'string') return;\n\n var membersToUpdate = d.members.filter(function(member) {\n return member.role !== newRole;\n });\n\n if (membersToUpdate.length) {\n _inChange = true;\n context.perform(\n function actionChangeMemberRoles(graph) {\n membersToUpdate.forEach(function(member) {\n var newMember = Object.assign({}, member, { role: newRole });\n delete newMember.index;\n graph = actionChangeMember(d.relation.id, newMember, member.index)(graph);\n });\n return graph;\n },\n t('operations.change_role.annotation', {\n n: membersToUpdate.length\n })\n );\n context.validator().validate();\n }\n _inChange = false;\n }\n\n\n function addMembership(d, role) {\n _showBlank = false;\n\n function actionAddMembers(relationId, ids, role) {\n return function(graph) {\n for (var i in ids) {\n var member = { id: ids[i], type: graph.entity(ids[i]).type, role: role };\n graph = actionAddMember(relationId, member)(graph);\n }\n return graph;\n };\n }\n\n if (d.relation) {\n recentlyAdded.add(d.relation.id);\n context.perform(\n actionAddMembers(d.relation.id, _entityIDs, role),\n t('operations.add_member.annotation', {\n n: _entityIDs.length\n })\n );\n context.validator().validate();\n\n } else {\n var relation = osmRelation();\n context.perform(\n actionAddEntity(relation),\n actionAddMembers(relation.id, _entityIDs, role),\n t('operations.add.annotation.relation')\n );\n // changing the mode also runs `validate`\n context.enter(modeSelect(context, [relation.id]).newFeature(true));\n }\n }\n\n\n function downloadMembers(d3_event, d) {\n d3_event.preventDefault();\n const button = d3_select(this);\n\n // display the loading indicator\n button.classed('loading', true);\n context.loadEntity(d.relation.id, function() {\n section.reRender();\n });\n }\n\n\n function deleteMembership(d3_event, d) {\n this.blur(); // avoid keeping focus on the button\n if (d === 0) return; // called on newrow (shouldn't happen)\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.relation.id], false, context);\n\n var indexes = d.members.map(function(member) {\n return member.index;\n });\n\n context.perform(\n actionDeleteMembers(d.relation.id, indexes),\n t('operations.delete_member.annotation', {\n n: _entityIDs.length\n })\n );\n context.validator().validate();\n }\n\n\n function fetchNearbyRelations(q, callback) {\n var newRelation = {\n relation: null,\n value: t('inspector.new_relation'),\n display: t.append('inspector.new_relation')\n };\n\n var entityID = _entityIDs[0];\n\n var result = [];\n\n var graph = context.graph();\n\n function baseDisplayLabel(entity) {\n var matched = presetManager.match(entity, graph);\n var presetName = (matched && matched.name()) || t('inspector.relation');\n var entityName = utilDisplayName(entity) || '';\n\n return selection => {\n selection\n .append('b')\n .text(presetName + ' ');\n selection\n .append('span')\n .classed('has-colour', entity.tags.colour && isColorValid(entity.tags.colour))\n .style('border-color', entity.tags.colour)\n .text(entityName);\n };\n }\n\n\n // A location search takes priority over an ID search\n const idMatchResult = q && idMatch(q);\n var explicitRelation = context.hasEntity(`r${idMatchResult?.id || q}`);\n if (explicitRelation && explicitRelation.type === 'relation' && explicitRelation.id !== entityID) {\n // loaded relation is specified explicitly, only show that\n\n result.push({\n relation: explicitRelation,\n value: baseDisplayValue(explicitRelation) + ' ' + explicitRelation.id,\n display: baseDisplayLabel(explicitRelation)\n });\n } else {\n\n context.history().intersects(context.map().extent()).forEach(function(entity) {\n if (entity.type !== 'relation' || entity.id === entityID) return;\n\n var value = baseDisplayValue(entity);\n if (q && (value + ' ' + entity.id).toLowerCase().indexOf(q.toLowerCase()) === -1) return;\n\n result.push({\n relation: entity,\n value,\n display: baseDisplayLabel(entity)\n });\n });\n\n result.sort(function(a, b) {\n return osmRelation.creationOrder(a.relation, b.relation);\n });\n\n // Dedupe identical names by appending relation id - see #2891\n Object.values(utilArrayGroupBy(result, 'value'))\n .filter(v => v.length > 1)\n .flat()\n .forEach(obj => obj.value += ' ' + obj.relation.id);\n }\n\n result.forEach(function(obj) {\n obj.title = obj.value;\n });\n\n result.unshift(newRelation);\n callback(result);\n }\n\n function baseDisplayValue(entity) {\n const graph = context.graph();\n var matched = presetManager.match(entity, graph);\n var presetName = (matched && matched.name()) || t('inspector.relation');\n var entityName = utilDisplayName(entity) || '';\n\n return presetName + ' ' + entityName;\n }\n\n function renderDisclosureContent(selection) {\n\n var memberships = getMemberships();\n\n var list = selection.selectAll('.member-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'member-list')\n .merge(list);\n\n\n var items = list.selectAll('li.member-row-normal')\n .data(memberships, function(d) {\n return d.hash;\n });\n\n items.exit()\n .each(unbind)\n .remove();\n\n // Enter\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', 'member-row member-row-normal form-field');\n\n // highlight the relation in the map while hovering on the list item\n itemsEnter.on('mouseover', function(d3_event, d) {\n utilHighlightEntities([d.relation.id], true, context);\n })\n .on('mouseout', function(d3_event, d) {\n utilHighlightEntities([d.relation.id], false, context);\n });\n\n var labelEnter = itemsEnter\n .append('label')\n .attr('class', 'field-label')\n .attr('for', function(d) {\n return d.domId;\n });\n\n var labelLink = labelEnter\n .append('span')\n .attr('class', 'label-text')\n .append('a')\n .attr('href', '#')\n .on('click', selectRelation);\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-type')\n .text(d => {\n let matched = presetManager.match(d.relation, context.graph());\n while (matched?.suggestion) {\n // if is NSI preset: look for a parent preset\n matched = matched.getParentPreset();\n }\n return (matched && matched.name()) || t('inspector.relation');\n });\n\n const showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n labelLink.each(function(d) {\n if (!showThirdPartyIcons) return;\n const matched = presetManager.match(d.relation, context.graph());\n if (matched.suggestion) {\n // if matching an NSI preset: append icon\n d3_select(this)\n .append('img')\n .classed('member-entity-icon', true)\n .attr('src', matched.imageURL);\n }\n });\n\n labelLink.each(function(d) {\n const relColors = getRelationColor(d.relation.tags, '#555');\n const hasRef = d.relation.tags.ref;\n if (relColors.isValid || hasRef) {\n const refs = (d.relation.tags.ref || '').split(';');\n for (const ref of refs) {\n d3_select(this)\n .append('span')\n .classed('member-entity-ref-color', true)\n .style('border-color', relColors.color)\n .style('background-color', relColors.color)\n .style('color', relColors.textColor)\n .text(ref);\n }\n }\n });\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-name')\n .text(d => utilDisplayName(d.relation, { hideRef: true }));\n\n labelEnter\n .append('button')\n .attr('class', 'members-download')\n .attr('title', t('icons.download'))\n .call(svgIcon('#iD-icon-load'))\n .on('click', downloadMembers);\n\n labelEnter\n .append('button')\n .attr('class', 'remove member-delete')\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'))\n .on('click', deleteMembership);\n\n labelEnter\n .append('button')\n .attr('class', 'member-zoom')\n .attr('title', t('icons.zoom_to'))\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'))\n .on('click', zoomToRelation);\n\n items = items.merge(itemsEnter);\n items.selectAll('button.members-download')\n .classed('hide', d => {\n const graph = context.graph();\n return d.relation.members.every(m => graph.hasEntity(m.id));\n });\n\n const dupeLabels = new WeakSet(Object.values(\n utilArrayGroupBy(items.selectAll('.label-text').nodes(), 'textContent'))\n .filter(v => v.length > 1)\n .flat());\n\n items.select('.label-text').each(function() {\n const label = d3_select(this);\n const entityName = label.select('.member-entity-name');\n if (dupeLabels.has(this)) {\n // Dedupe identical names in hover text by appending relation id - see #2891, #10184\n label.attr('title', d => `${entityName.text()} ${d.relation.id}`);\n } else {\n // set full label also as hover text: useful if a (long) label is cut off with an \u2026 ellipsis\n label.attr('title', () => entityName.text());\n }\n });\n\n var wrapEnter = itemsEnter\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-member');\n\n wrapEnter\n .append('input')\n .attr('class', 'member-role')\n .attr('id', function(d) {\n return d.domId;\n })\n .property('type', 'text')\n .property('value', function(d) {\n return typeof d.role === 'string' ? d.role : '';\n })\n .attr('title', function(d) {\n return Array.isArray(d.role) ? d.role.filter(Boolean).join('\\n') : d.role;\n })\n .attr('placeholder', function(d) {\n return Array.isArray(d.role) ? t('inspector.multiple_roles') : t('inspector.role');\n })\n .classed('mixed', function(d) {\n return Array.isArray(d.role);\n })\n .call(utilNoAuto)\n .on('blur', changeRole)\n .on('change', changeRole);\n\n if (taginfo) {\n wrapEnter.each(bindTypeahead);\n }\n\n var newMembership = list.selectAll('.member-row-new')\n .data(_showBlank ? [0] : []);\n\n // Exit\n newMembership.exit()\n .remove();\n\n // Enter\n var newMembershipEnter = newMembership.enter()\n .append('li')\n .attr('class', 'member-row member-row-new form-field');\n\n var newLabelEnter = newMembershipEnter\n .append('label')\n .attr('class', 'field-label');\n\n newLabelEnter\n .append('input')\n .attr('placeholder', t('inspector.choose_relation'))\n .attr('type', 'text')\n .attr('class', 'member-entity-input')\n .call(utilNoAuto);\n\n newLabelEnter\n .append('button')\n .attr('class', 'remove member-delete')\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'))\n .on('click', function() {\n list.selectAll('.member-row-new')\n .remove();\n });\n\n var newWrapEnter = newMembershipEnter\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-member');\n\n newWrapEnter\n .append('input')\n .attr('class', 'member-role')\n .property('type', 'text')\n .attr('placeholder', t('inspector.role'))\n .call(utilNoAuto);\n\n // Update\n newMembership = newMembership\n .merge(newMembershipEnter);\n\n newMembership.selectAll('.member-entity-input')\n .on('blur', cancelEntity) // if it wasn't accepted normally, cancel it\n .call(nearbyCombo\n .on('accept', function(d) {\n this.blur(); // always blurs the triggering element\n acceptEntity.call(this, d);\n })\n .on('cancel', cancelEntity)\n );\n\n\n // Container for the Add button\n var addRow = selection.selectAll('.add-row')\n .data([0]);\n\n // enter\n var addRowEnter = addRow.enter()\n .append('div')\n .attr('class', 'add-row');\n\n var addRelationButton = addRowEnter\n .append('button')\n .attr('class', 'add-relation')\n .attr('aria-label', t('inspector.add_to_relation'));\n\n addRelationButton\n .call(svgIcon('#iD-icon-plus', 'light'));\n addRelationButton\n .call(uiTooltip()\n .title(() => t.append('inspector.add_to_relation'))\n .placement(localizer.textDirection() === 'ltr' ? 'right' : 'left'));\n\n addRowEnter\n .append('div')\n .attr('class', 'space-value'); // preserve space\n\n addRowEnter\n .append('div')\n .attr('class', 'space-buttons'); // preserve space\n\n // update\n addRow = addRow\n .merge(addRowEnter);\n\n addRow.select('.add-relation')\n .on('click', function() {\n _showBlank = true;\n section.reRender();\n list.selectAll('.member-entity-input').node().focus();\n });\n\n\n function acceptEntity(d) {\n if (!d) {\n cancelEntity();\n return;\n }\n // remove hover-higlighting\n if (d.relation) utilHighlightEntities([d.relation.id], false, context);\n\n var role = context.cleanRelationRole(list.selectAll('.member-row-new .member-role').property('value'));\n addMembership(d, role);\n }\n\n\n function cancelEntity() {\n var input = newMembership.selectAll('.member-entity-input');\n input.property('value', '');\n\n // remove hover-higlighting\n context.surface().selectAll('.highlighted')\n .classed('highlighted', false);\n }\n\n\n function bindTypeahead(d) {\n var row = d3_select(this);\n var role = row.selectAll('input.member-role');\n var origValue = role.property('value');\n\n function sort(value, data) {\n var sameletter = [];\n var other = [];\n for (var i = 0; i < data.length; i++) {\n if (data[i].value.substring(0, value.length) === value) {\n sameletter.push(data[i]);\n } else {\n other.push(data[i]);\n }\n }\n return sameletter.concat(other);\n }\n\n role.call(uiCombobox(context, 'member-role')\n .fetcher(function(role, callback) {\n var rtype = d.relation.tags.type;\n taginfo.roles({\n debounce: true,\n rtype: rtype || '',\n geometry: context.graph().geometry(_entityIDs[0]),\n query: role\n }, function(err, data) {\n if (!err) callback(sort(role, data));\n });\n })\n .on('cancel', function() {\n role.property('value', origValue);\n })\n );\n }\n\n\n function unbind() {\n var row = d3_select(this);\n\n row.selectAll('input.member-role')\n .call(uiCombobox.off, context);\n }\n }\n\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n const didChange = _entityIDs.join(',') !== val.join(',');\n _entityIDs = val;\n _showBlank = false;\n if (didChange) {\n recentlyAdded.clear(); // reset when the selected feature changes\n }\n return section;\n };\n\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { modeSelect } from '../../modes/select';\nimport { osmEntity } from '../../osm';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\nimport { t } from '../../core/localizer';\nimport { utilDisplayName, utilHighlightEntities } from '../../util';\n\nexport function uiSectionSelectionList(context) {\n\n var _selectedIDs = [];\n\n var section = uiSection('selected-features', context)\n .shouldDisplay(function() {\n return _selectedIDs.length > 1;\n })\n .label(function() {\n return t.append('inspector.title_count', { title: t.append('inspector.features'), count: _selectedIDs.length });\n })\n .disclosureContent(renderDisclosureContent);\n\n context.history()\n .on('change.selectionList', function(difference) {\n if (difference) {\n section.reRender();\n }\n });\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _selectedIDs;\n _selectedIDs = val;\n return section;\n };\n\n function selectEntity(d3_event, entity) {\n context.enter(modeSelect(context, [entity.id]));\n }\n\n function deselectEntity(d3_event, entity) {\n var selectedIDs = _selectedIDs.slice();\n var index = selectedIDs.indexOf(entity.id);\n if (index > -1) {\n selectedIDs.splice(index, 1);\n context.enter(modeSelect(context, selectedIDs));\n }\n }\n\n function renderDisclosureContent(selection) {\n\n var list = selection.selectAll('.feature-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'feature-list')\n .merge(list);\n\n var entities = _selectedIDs\n .map(function(id) { return context.hasEntity(id); })\n .filter(Boolean);\n\n var items = list.selectAll('.feature-list-item')\n .data(entities, osmEntity.key);\n\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li')\n .attr('class', 'feature-list-item')\n .each(function(d) {\n d3_select(this)\n .on('mouseover', function() {\n utilHighlightEntities([d.id], true, context);\n })\n .on('mouseout', function() {\n utilHighlightEntities([d.id], false, context);\n });\n });\n\n var label = enter\n .append('button')\n .attr('class', 'label')\n .on('click', selectEntity);\n\n label\n .append('span')\n .attr('class', 'entity-geom-icon')\n .call(svgIcon('', 'pre-text'));\n\n label\n .append('span')\n .attr('class', 'entity-type');\n\n label\n .append('span')\n .attr('class', 'entity-name');\n\n enter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.deselect'))\n .on('click', deselectEntity)\n .call(svgIcon('#iD-icon-close'));\n\n // Update\n items = items.merge(enter);\n\n items.selectAll('.entity-geom-icon use')\n .attr('href', function() {\n var entity = this.parentNode.parentNode.__data__;\n return '#iD-icon-' + entity.geometry(context.graph());\n });\n\n items.selectAll('.entity-type')\n .text(function(entity) { return presetManager.match(entity, context.graph()).name(); });\n\n items.selectAll('.entity-name')\n .text(function(d) {\n // fetch latest entity\n var entity = context.entity(d.id);\n return utilDisplayName(entity);\n });\n }\n\n return section;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { deepEqual } from 'fast-equals';\n\nimport { presetManager } from '../presets';\nimport { t, localizer } from '../core/localizer';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\nimport { utilArrayIdentical } from '../util/array';\nimport { utilCleanTags, utilCombinedTags, utilRebind } from '../util';\n\nimport { uiSectionEntityIssues } from './sections/entity_issues';\nimport { uiSectionFeatureType } from './sections/feature_type';\nimport { uiSectionPresetFields } from './sections/preset_fields';\nimport { uiSectionRawMemberEditor } from './sections/raw_member_editor';\nimport { uiSectionRawMembershipEditor } from './sections/raw_membership_editor';\nimport { uiSectionRawTagEditor } from './sections/raw_tag_editor';\nimport { uiSectionSelectionList } from './sections/selection_list';\n\nexport function uiEntityEditor(context) {\n var dispatch = d3_dispatch('choose');\n var _state = 'select';\n var _coalesceChanges = false;\n var _modified = false;\n var _base;\n var _entityIDs;\n var _activePresets = [];\n var _newFeature;\n\n var _sections;\n\n function entityEditor(selection) {\n\n var combinedTags = utilCombinedTags(_entityIDs, context.graph());\n\n // Header\n var header = selection.selectAll('.header')\n .data([0]);\n\n // Enter\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n var direction = (localizer.textDirection() === 'rtl') ? 'forward' : 'backward';\n\n headerEnter\n .append('button')\n .attr('class', 'preset-reset preset-choose')\n .attr('title', t('inspector.back_tooltip'))\n .call(svgIcon(`#iD-icon-${direction}`));\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function() { context.enter(modeBrowse(context)); })\n .call(svgIcon(_modified ? '#iD-icon-apply' : '#iD-icon-close'));\n\n headerEnter\n .append('h2');\n\n // Update\n header = header\n .merge(headerEnter);\n\n header.selectAll('h2')\n .text('')\n .call(_entityIDs.length === 1 ? t.append('inspector.edit') : t.append('inspector.edit_features'));\n\n header.selectAll('.preset-reset')\n .on('click', function() {\n dispatch.call('choose', this, _activePresets);\n });\n\n // Body\n var body = selection.selectAll('.inspector-body')\n .data([0]);\n\n // Enter\n var bodyEnter = body.enter()\n .append('div')\n .attr('class', 'entity-editor inspector-body sep-top');\n\n // Update\n body = body\n .merge(bodyEnter);\n\n if (!_sections) {\n _sections = [\n uiSectionSelectionList(context),\n uiSectionFeatureType(context).on('choose', function(presets) {\n dispatch.call('choose', this, presets);\n }),\n uiSectionEntityIssues(context),\n uiSectionPresetFields(context).on('change', changeTags).on('revert', revertTags),\n uiSectionRawTagEditor('raw-tag-editor', context).on('change', changeTags),\n uiSectionRawMemberEditor(context),\n uiSectionRawMembershipEditor(context)\n ];\n }\n\n _sections.forEach(function(section) {\n if (section.entityIDs) {\n section.entityIDs(_entityIDs);\n }\n if (section.presets) {\n section.presets(_activePresets);\n }\n if (section.tags) {\n section.tags(combinedTags);\n }\n if (section.state) {\n section.state(_state);\n }\n body.call(section.render);\n });\n\n context.history()\n .on('change.entity-editor', historyChanged);\n\n function historyChanged(difference) {\n if (selection.selectAll('.entity-editor').empty()) return;\n if (_state === 'hide') return;\n var significant = !difference ||\n difference.didChange.properties ||\n difference.didChange.addition ||\n difference.didChange.deletion;\n if (!significant) return;\n\n _entityIDs = _entityIDs.filter(context.hasEntity);\n if (!_entityIDs.length) return;\n\n var priorActivePreset = _activePresets.length === 1 && _activePresets[0];\n\n loadActivePresets();\n\n var graph = context.graph();\n entityEditor.modified(_base !== graph);\n entityEditor(selection);\n\n if (priorActivePreset && _activePresets.length === 1 && priorActivePreset !== _activePresets[0]) {\n // flash the button to indicate the preset changed\n context.container().selectAll('.entity-editor button.preset-reset .label')\n .classed('flash-bg', true)\n .on('animationend', function() {\n d3_select(this).classed('flash-bg', false);\n });\n }\n }\n }\n\n\n // Tag changes that fire on input can all get coalesced into a single\n // history operation when the user leaves the field. #2342\n // Use explicit entityIDs in case the selection changes before the event is fired.\n function changeTags(entityIDs, changed, onInput) {\n\n var actions = [];\n for (var i in entityIDs) {\n var entityID = entityIDs[i];\n var entity = context.entity(entityID);\n\n var tags = Object.assign({}, entity.tags); // shallow copy\n\n if (typeof changed === 'function') {\n // a complex callback tag change\n tags = changed(tags);\n } else {\n for (var k in changed) {\n if (!k) continue;\n var v = changed[k];\n if (typeof v === 'object') {\n // a \"key only\" tag change\n tags[k] = tags[v.oldKey];\n } else if (v !== undefined || tags.hasOwnProperty(k)) {\n tags[k] = v;\n }\n }\n }\n\n if (!onInput) {\n tags = utilCleanTags(tags);\n }\n\n if (!deepEqual(entity.tags, tags)) {\n actions.push(actionChangeTags(entityID, tags));\n }\n }\n\n if (actions.length) {\n var combinedAction = function(graph) {\n actions.forEach(function(action) {\n graph = action(graph);\n });\n return graph;\n };\n\n var annotation = t('operations.change_tags.annotation');\n\n if (_coalesceChanges) {\n context.replace(combinedAction, annotation);\n } else {\n context.perform(combinedAction, annotation);\n }\n _coalesceChanges = !!onInput;\n }\n\n // if leaving field (blur event), rerun validation\n if (!onInput) {\n context.validator().validate();\n }\n }\n\n function revertTags(keys) {\n\n var actions = [];\n for (var i in _entityIDs) {\n var entityID = _entityIDs[i];\n\n var original = context.graph().base().entities[entityID];\n var changed = {};\n for (var j in keys) {\n var key = keys[j];\n changed[key] = original ? original.tags[key] : undefined;\n }\n\n var entity = context.entity(entityID);\n var tags = Object.assign({}, entity.tags); // shallow copy\n\n for (var k in changed) {\n if (!k) continue;\n var v = changed[k];\n if (v !== undefined || tags.hasOwnProperty(k)) {\n tags[k] = v;\n }\n }\n\n\n tags = utilCleanTags(tags);\n\n if (!deepEqual(entity.tags, tags)) {\n actions.push(actionChangeTags(entityID, tags));\n }\n\n }\n\n if (actions.length) {\n var combinedAction = function(graph) {\n actions.forEach(function(action) {\n graph = action(graph);\n });\n return graph;\n };\n\n var annotation = t('operations.change_tags.annotation');\n\n if (_coalesceChanges) {\n context.replace(combinedAction, annotation);\n } else {\n context.perform(combinedAction, annotation);\n }\n }\n\n context.validator().validate();\n }\n\n\n entityEditor.modified = function(val) {\n if (!arguments.length) return _modified;\n _modified = val;\n return entityEditor;\n };\n\n\n entityEditor.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n return entityEditor;\n };\n\n\n entityEditor.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n\n // always reload these even if the entityIDs are unchanged, since we\n // could be reselecting after something like dragging a node\n _base = context.graph();\n _coalesceChanges = false;\n\n if (val && _entityIDs && utilArrayIdentical(_entityIDs, val)) return entityEditor; // exit early if no change\n\n _entityIDs = val;\n\n loadActivePresets(true);\n\n return entityEditor\n .modified(false);\n };\n\n\n entityEditor.newFeature = function(val) {\n if (!arguments.length) return _newFeature;\n _newFeature = val;\n return entityEditor;\n };\n\n\n function loadActivePresets(isForNewSelection) {\n\n var graph = context.graph();\n\n var counts = {};\n\n for (var i in _entityIDs) {\n var entity = graph.hasEntity(_entityIDs[i]);\n if (!entity) return;\n\n var match = presetManager.match(entity, graph);\n\n if (!counts[match.id]) counts[match.id] = 0;\n counts[match.id] += 1;\n }\n\n var matches = Object.keys(counts).sort(function(p1, p2) {\n return counts[p2] - counts[p1];\n }).map(function(pID) {\n return presetManager.item(pID);\n });\n\n if (!isForNewSelection) {\n // A \"weak\" preset doesn't set any tags. (e.g. \"Address\")\n var weakPreset = _activePresets.length === 1 &&\n !_activePresets[0].isFallback() &&\n Object.keys(_activePresets[0].addTags || {}).length === 0;\n // Don't replace a weak preset with a fallback preset (e.g. \"Point\")\n if (weakPreset && matches.length === 1 && matches[0].isFallback()) return;\n }\n\n entityEditor.presets(matches);\n }\n\n entityEditor.presets = function(val) {\n if (!arguments.length) return _activePresets;\n\n // don't reload the same preset\n if (!utilArrayIdentical(val, _activePresets)) {\n _activePresets = val;\n }\n return entityEditor;\n };\n\n return utilRebind(entityEditor, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { debounce } from 'es-toolkit/compat';\n\nimport { presetManager } from '../presets';\nimport { t, localizer } from '../core/localizer';\nimport { actionChangePreset } from '../actions/change_preset';\nimport { operationDelete } from '../operations/delete';\nimport { svgIcon } from '../svg/index';\nimport { uiTooltip } from './tooltip';\nimport { geoExtent } from '../geo/extent';\nimport { uiPresetIcon } from './preset_icon';\nimport { uiTagReference } from './tag_reference';\nimport { utilKeybinding, utilNoAuto, utilRebind } from '../util';\n\n\nexport function uiPresetList(context) {\n var dispatch = d3_dispatch('cancel', 'choose');\n var _entityIDs;\n var _currLoc;\n var _currentPresets;\n var _autofocus = false;\n\n\n function presetList(selection) {\n if (!_entityIDs) return;\n\n var presets = presetManager.matchAllGeometry(entityGeometries());\n\n selection.html('');\n\n var messagewrap = selection\n .append('div')\n .attr('class', 'header fillL');\n\n var message = messagewrap\n .append('h2')\n .call(t.addOrUpdate('inspector.choose'));\n\n messagewrap\n .append('button')\n .attr('class', 'preset-choose')\n .attr('title', _entityIDs.length === 1 ? t('inspector.edit') : t('inspector.edit_features'))\n .on('click', function() { dispatch.call('cancel', this); })\n .call(svgIcon('#iD-icon-close'));\n\n function initialKeydown(d3_event) {\n // hack to let delete shortcut work when search is autofocused\n if (search.property('value').length === 0 &&\n (d3_event.keyCode === utilKeybinding.keyCodes['\u232B'] ||\n d3_event.keyCode === utilKeybinding.keyCodes['\u2326'])) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n operationDelete(context, _entityIDs)();\n\n // hack to let undo work when search is autofocused\n } else if (search.property('value').length === 0 &&\n (d3_event.ctrlKey || d3_event.metaKey) &&\n d3_event.keyCode === utilKeybinding.keyCodes.z) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n context.undo();\n } else if (!d3_event.ctrlKey && !d3_event.metaKey) {\n // don't check for delete/undo hack on future keydown events\n d3_select(this).on('keydown', keydown);\n keydown.call(this, d3_event);\n }\n }\n\n function keydown(d3_event) {\n // down arrow\n if (d3_event.keyCode === utilKeybinding.keyCodes['\u2193'] &&\n // if insertion point is at the end of the string\n search.node().selectionStart === search.property('value').length) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // move focus to the first item in the preset list\n var buttons = list.selectAll('.preset-list-button');\n if (!buttons.empty()) buttons.nodes()[0].focus();\n }\n }\n\n function keypress(d3_event) {\n // enter\n var value = search.property('value');\n if (d3_event.keyCode === 13 && // \u21A9 Return\n value.length) {\n list.selectAll('.preset-list-item:first-child')\n .each(function(d) { d.choose.call(this); });\n }\n }\n\n function inputevent() {\n var value = search.property('value');\n list.classed('filtered', value.length);\n\n var results, messageText;\n if (value.length) {\n results = presets.search(value, entityGeometries()[0], _currLoc);\n messageText = t.addOrUpdate('inspector.results', {\n n: results.collection.length,\n search: value\n });\n } else {\n var entityPresets = _entityIDs.map(entityID =>\n presetManager.match(context.graph().entity(entityID), context.graph()));\n results = presetManager.defaults(entityGeometries()[0], 36, !context.inIntro(), _currLoc, entityPresets);\n messageText = t.addOrUpdate('inspector.choose');\n }\n list.call(drawList, results);\n message.call(messageText);\n }\n\n var searchWrap = selection\n .append('div')\n .attr('class', 'search-header');\n\n searchWrap\n .call(svgIcon('#iD-icon-search', 'pre-text'));\n\n var search = searchWrap\n .append('input')\n .attr('class', 'preset-search-input')\n .attr('placeholder', t('inspector.search_feature_type'))\n .attr('type', 'search')\n .call(utilNoAuto)\n .on('keydown', initialKeydown)\n .on('keypress', keypress)\n .on('input', debounce(inputevent));\n\n if (_autofocus) {\n search.node().focus();\n\n // Safari 14 doesn't always like to focus immediately,\n // so try again on the next pass\n setTimeout(function() {\n search.node().focus();\n }, 0);\n }\n\n var listWrap = selection\n .append('div')\n .attr('class', 'inspector-body');\n\n var entityPresets = _entityIDs.map(entityID =>\n presetManager.match(context.graph().entity(entityID), context.graph()));\n var list = listWrap\n .append('div')\n .attr('class', 'preset-list')\n .call(drawList, presetManager.defaults(entityGeometries()[0], 36, !context.inIntro(), _currLoc, entityPresets));\n\n context.features().on('change.preset-list', updateForFeatureHiddenState);\n }\n\n\n function drawList(list, presets) {\n presets = presets.matchAllGeometry(entityGeometries());\n var collection = presets.collection.reduce(function(collection, preset) {\n if (!preset) return collection;\n\n if (preset.members) {\n if (preset.members.collection.filter(function(preset) {\n return preset.addable();\n }).length > 1) {\n collection.push(CategoryItem(preset));\n }\n } else if (preset.addable()) {\n collection.push(PresetItem(preset));\n }\n return collection;\n }, []);\n\n var items = list.selectChildren('.preset-list-item')\n .data(collection, function(d) { return d.preset.id; });\n\n items.order();\n\n items.exit()\n .remove();\n\n items.enter()\n .append('div')\n .attr('class', function(item) { return 'preset-list-item preset-' + item.preset.id.replaceAll('/', '-'); })\n .classed('current', function(item) { return _currentPresets.indexOf(item.preset) !== -1; })\n .each(function(item) { d3_select(this).call(item); })\n .style('opacity', 0)\n .transition()\n .style('opacity', 1);\n\n updateForFeatureHiddenState();\n }\n\n function itemKeydown(d3_event) {\n // the actively focused item\n var item = d3_select(this.closest('.preset-list-item'));\n var parentItem = d3_select(item.node().parentNode.closest('.preset-list-item'));\n\n // arrow down, move focus to the next, lower item\n if (d3_event.keyCode === utilKeybinding.keyCodes['\u2193']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // the next item in the list at the same level\n var nextItem = d3_select(item.node().nextElementSibling);\n // if there is no next item in this list\n if (nextItem.empty()) {\n // if there is a parent item\n if (!parentItem.empty()) {\n // the item is the last item of a sublist,\n // select the next item at the parent level\n nextItem = d3_select(parentItem.node().nextElementSibling);\n }\n // if the focused item is expanded\n } else if (d3_select(this).classed('expanded')) {\n // select the first subitem instead\n nextItem = item.select('.subgrid .preset-list-item:first-child');\n }\n if (!nextItem.empty()) {\n // focus on the next item\n nextItem.select('.preset-list-button').node().focus();\n }\n\n // arrow up, move focus to the previous, higher item\n } else if (d3_event.keyCode === utilKeybinding.keyCodes['\u2191']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // the previous item in the list at the same level\n var previousItem = d3_select(item.node().previousElementSibling);\n\n // if there is no previous item in this list\n if (previousItem.empty()) {\n // if there is a parent item\n if (!parentItem.empty()) {\n // the item is the first subitem of a sublist select the parent item\n previousItem = parentItem;\n }\n // if the previous item is expanded\n } else if (previousItem.select('.preset-list-button').classed('expanded')) {\n // select the last subitem of the sublist of the previous item\n previousItem = previousItem.select('.subgrid .preset-list-item:last-child');\n }\n\n if (!previousItem.empty()) {\n // focus on the previous item\n previousItem.select('.preset-list-button').node().focus();\n } else {\n // the focus is at the top of the list, move focus back to the search field\n var search = d3_select(this.closest('.preset-list-pane')).select('.preset-search-input');\n search.node().focus();\n }\n\n // arrow left, move focus to the parent item if there is one\n } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2192' : '\u2190']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // if there is a parent item, focus on the parent item\n if (!parentItem.empty()) {\n parentItem.select('.preset-list-button').node().focus();\n }\n\n // arrow right, choose this item\n } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2190' : '\u2192']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n item.datum().choose.call(d3_select(this).node());\n }\n }\n\n\n function CategoryItem(preset) {\n var box, sublist, shown = false;\n\n function item(selection) {\n var wrap = selection.append('div')\n .attr('class', 'preset-list-button-wrap category');\n\n function click() {\n var isExpanded = d3_select(this).classed('expanded');\n var iconName = isExpanded ?\n (localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward') : '#iD-icon-down';\n d3_select(this)\n .classed('expanded', !isExpanded)\n .attr('title', !isExpanded ? t('icons.collapse') : t('icons.expand'));\n d3_select(this).selectAll('div.label-inner svg.icon use')\n .attr('href', iconName);\n item.choose();\n }\n\n var geometries = entityGeometries();\n\n var button = wrap\n .append('button')\n .attr('class', 'preset-list-button')\n .attr('title', t('icons.expand'))\n .classed('expanded', false)\n .call(uiPresetIcon()\n .geometry(geometries.length === 1 && geometries[0])\n .preset(preset))\n .on('click', click)\n .on('keydown', function(d3_event) {\n // right arrow, expand the focused item\n if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2190' : '\u2192']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // if the item isn't expanded\n if (!d3_select(this).classed('expanded')) {\n // toggle expansion (expand the item)\n click.call(this, d3_event);\n }\n // left arrow, collapse the focused item\n } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2192' : '\u2190']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // if the item is expanded\n if (d3_select(this).classed('expanded')) {\n // toggle expansion (collapse the item)\n click.call(this, d3_event);\n }\n } else {\n itemKeydown.call(this, d3_event);\n }\n });\n\n var label = button\n .append('div')\n .attr('class', 'label')\n .append('div')\n .attr('class', 'label-inner');\n\n label\n .append('div')\n .attr('class', 'namepart')\n .call(svgIcon((localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'))\n .append('span')\n .call(preset.nameLabel())\n .append('span').text('\u2026');\n\n box = selection.append('div')\n .attr('class', 'subgrid')\n .style('max-height', '0px')\n .style('opacity', 0);\n\n box.append('div')\n .attr('class', 'arrow');\n\n sublist = box.append('div')\n .attr('class', 'preset-list fillL3');\n }\n\n\n item.choose = function() {\n if (!box || !sublist) return;\n\n if (shown) {\n shown = false;\n box.transition()\n .duration(200)\n .style('opacity', '0')\n .style('max-height', '0px')\n .style('padding-bottom', '0px');\n } else {\n shown = true;\n var members = preset.members.matchAllGeometry(entityGeometries());\n sublist.call(drawList, members);\n box.transition()\n .duration(200)\n .style('opacity', '1')\n .style('max-height', 200 + members.collection.length * 190 + 'px')\n .style('padding-bottom', '10px');\n }\n };\n\n item.preset = preset;\n return item;\n }\n\n\n function PresetItem(preset) {\n function item(selection) {\n var wrap = selection.append('div')\n .attr('class', 'preset-list-button-wrap');\n\n var geometries = entityGeometries();\n\n var button = wrap.append('button')\n .attr('class', 'preset-list-button')\n .call(uiPresetIcon()\n .geometry(geometries.length === 1 && geometries[0])\n .preset(preset))\n .on('click', item.choose)\n .on('keydown', itemKeydown);\n\n var label = button\n .append('div')\n .attr('class', 'label')\n .append('div')\n .attr('class', 'label-inner');\n\n var nameparts = [\n preset.nameLabel(),\n preset.subtitleLabel()\n ].filter(Boolean);\n\n label.selectAll('.namepart')\n .data(nameparts, d => d.stringId)\n .enter()\n .append('div')\n .attr('class', 'namepart')\n .text('')\n .each(function(d) { d(d3_select(this)); });\n\n wrap.call(item.reference.button);\n selection.call(item.reference.body);\n }\n\n item.choose = function() {\n if (d3_select(this).classed('disabled')) return;\n if (!context.inIntro()) {\n presetManager.setMostRecent(preset, entityGeometries()[0]);\n }\n context.perform(\n function(graph) {\n for (var i in _entityIDs) {\n var entityID = _entityIDs[i];\n var oldPreset = presetManager.match(graph.entity(entityID), graph);\n graph = actionChangePreset(entityID, oldPreset, preset)(graph);\n }\n return graph;\n },\n t('operations.change_tags.annotation')\n );\n\n context.validator().validate(); // rerun validation\n dispatch.call('choose', this, preset);\n };\n\n item.help = function(d3_event) {\n d3_event.stopPropagation();\n item.reference.toggle();\n };\n\n item.preset = preset;\n item.reference = uiTagReference(preset.reference(), context);\n\n return item;\n }\n\n\n function updateForFeatureHiddenState() {\n if (!_entityIDs.every(context.hasEntity)) return;\n\n var geometries = entityGeometries();\n var button = context.container().selectAll('.preset-list .preset-list-button');\n\n // remove existing tooltips\n button.call(uiTooltip().destroyAny);\n\n button.each(function(item, index) {\n var hiddenPresetFeaturesId;\n for (var i in geometries) {\n hiddenPresetFeaturesId = context.features().isHiddenPreset(item.preset, geometries[i]);\n if (hiddenPresetFeaturesId) break;\n }\n var isHiddenPreset = !context.inIntro() &&\n !!hiddenPresetFeaturesId &&\n (_currentPresets.length !== 1 || item.preset !== _currentPresets[0]);\n\n d3_select(this)\n .classed('disabled', isHiddenPreset);\n\n if (isHiddenPreset) {\n var isAutoHidden = context.features().autoHidden(hiddenPresetFeaturesId);\n d3_select(this).call(uiTooltip()\n .title(() => t.append('inspector.hidden_preset.' + (isAutoHidden ? 'zoom' : 'manual'), {\n features: t('feature.' + hiddenPresetFeaturesId + '.description')\n }))\n .placement(index < 2 ? 'bottom' : 'top')\n );\n }\n });\n }\n\n presetList.autofocus = function(val) {\n if (!arguments.length) return _autofocus;\n _autofocus = val;\n return presetList;\n };\n\n presetList.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n\n _entityIDs = val;\n _currLoc = null;\n\n if (_entityIDs && _entityIDs.length) {\n // calculate current location\n const extent = _entityIDs.reduce(function(extent, entityID) {\n var entity = context.graph().entity(entityID);\n return extent.extend(entity.extent(context.graph()));\n }, geoExtent());\n _currLoc = extent.center();\n\n // match presets\n var presets = _entityIDs.map(function(entityID) {\n return presetManager.match(context.entity(entityID), context.graph());\n });\n presetList.presets(presets);\n }\n\n return presetList;\n };\n\n presetList.presets = function(val) {\n if (!arguments.length) return _currentPresets;\n _currentPresets = val;\n return presetList;\n };\n\n function entityGeometries() {\n\n var counts = {};\n\n for (var i in _entityIDs) {\n var entityID = _entityIDs[i];\n var entity = context.entity(entityID);\n var geometry = entity.geometry(context.graph());\n\n // Treat entities on addr:interpolation lines as points, not vertices (#3241)\n if (geometry === 'vertex' && entity.isOnAddressLine(context.graph())) {\n geometry = 'point';\n }\n\n if (!counts[geometry]) counts[geometry] = 0;\n counts[geometry] += 1;\n }\n\n return Object.keys(counts).sort(function(geom1, geom2) {\n return counts[geom2] - counts[geom1];\n });\n }\n\n return utilRebind(presetList, dispatch, 'on');\n}\n", "import { interpolate as d3_interpolate } from 'd3-interpolate';\nimport { select as d3_select } from 'd3-selection';\n\nimport { uiEntityEditor } from './entity_editor';\nimport { uiPresetList } from './preset_list';\nimport { uiViewOnOSM } from './view_on_osm';\n\n\nexport function uiInspector(context) {\n var presetList = uiPresetList(context);\n var entityEditor = uiEntityEditor(context);\n var wrap = d3_select(null),\n presetPane = d3_select(null),\n editorPane = d3_select(null);\n var _state = 'select';\n var _entityIDs;\n var _newFeature = false;\n\n\n function inspector(selection) {\n presetList\n .entityIDs(_entityIDs)\n .autofocus(_newFeature)\n .on('choose', inspector.setPreset)\n .on('cancel', function() {\n inspector.setPreset();\n });\n\n entityEditor\n .state(_state)\n .entityIDs(_entityIDs)\n .on('choose', inspector.showList);\n\n wrap = selection.selectAll('.panewrap')\n .data([0]);\n\n var enter = wrap.enter()\n .append('div')\n .attr('class', 'panewrap');\n\n enter\n .append('div')\n .attr('class', 'preset-list-pane pane');\n\n enter\n .append('div')\n .attr('class', 'entity-editor-pane pane');\n\n wrap = wrap.merge(enter);\n presetPane = wrap.selectAll('.preset-list-pane');\n editorPane = wrap.selectAll('.entity-editor-pane');\n\n function shouldDefaultToPresetList() {\n // always show the inspector on hover\n if (_state !== 'select') return false;\n\n // can only change preset on single selection\n if (_entityIDs.length !== 1) return false;\n\n var entityID = _entityIDs[0];\n var entity = context.hasEntity(entityID);\n if (!entity) return false;\n\n // default to inspector if there are already tags\n if (entity.hasNonGeometryTags()) return false;\n\n // prompt to select preset if feature is new and untagged\n if (_newFeature) return true;\n\n // all existing features except vertices should default to inspector\n if (entity.geometry(context.graph()) !== 'vertex') return false;\n\n // show vertex relations if any\n if (context.graph().parentRelations(entity).length) return false;\n\n // show vertex issues if there are any\n if (context.validator().getEntityIssues(entityID).length) return false;\n\n // show turn restriction editor for junction vertices\n if (entity.type === 'node' && entity.isHighwayIntersection(context.graph())) return false;\n\n // otherwise show preset list for uninteresting vertices\n return true;\n }\n\n if (shouldDefaultToPresetList()) {\n wrap.style('right', '-100%');\n editorPane.classed('hide', true);\n presetPane.classed('hide', false)\n .call(presetList);\n } else {\n wrap.style('right', '0%');\n presetPane.classed('hide', true);\n editorPane.classed('hide', false)\n .call(entityEditor);\n }\n\n var footer = selection.selectAll('.footer')\n .data([0]);\n\n footer = footer.enter()\n .append('div')\n .attr('class', 'footer')\n .merge(footer);\n\n footer\n .call(uiViewOnOSM(context)\n .what(context.hasEntity(_entityIDs.length === 1 && _entityIDs[0]))\n );\n }\n\n inspector.showList = function(presets) {\n\n presetPane.classed('hide', false);\n\n wrap.transition()\n .styleTween('right', function() {\n return d3_interpolate('0%', '-100%');\n })\n .on('end', function () {\n editorPane.classed('hide', true);\n });\n\n if (presets) {\n presetList.presets(presets);\n }\n\n presetPane\n .call(presetList.autofocus(true));\n };\n\n inspector.setPreset = function(preset) {\n\n // upon setting multipolygon, go to the area preset list instead of the editor\n if (preset && preset.id === 'type/multipolygon') {\n presetPane\n .call(presetList.autofocus(true));\n\n } else {\n editorPane.classed('hide', false);\n wrap.transition()\n .styleTween('right', function() {\n return d3_interpolate('-100%', '0%');\n })\n .on('end', function () {\n presetPane.classed('hide', true);\n });\n\n if (preset) {\n entityEditor.presets([preset]);\n }\n editorPane\n .call(entityEditor);\n }\n\n };\n\n inspector.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n entityEditor.state(_state);\n\n // remove any old field help overlay that might have gotten attached to the inspector\n context.container().selectAll('.field-help-body').remove();\n\n return inspector;\n };\n\n\n inspector.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return inspector;\n };\n\n\n inspector.newFeature = function(val) {\n if (!arguments.length) return _newFeature;\n _newFeature = val;\n return inspector;\n };\n\n\n return inspector;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { utilArrayIdentical } from '../util/array';\nimport { utilFastMouse } from '../util';\nimport { osmEntity, osmNote, QAItem } from '../osm';\nimport { services } from '../services';\nimport { uiDataEditor } from './data_editor';\nimport { uiFeatureList } from './feature_list';\nimport { uiInspector } from './inspector';\nimport { uiOsmoseEditor } from './osmose_editor';\nimport { uiNoteEditor } from './note_editor';\nimport { localizer } from '../core/localizer';\n\n\nexport function uiSidebar(context) {\n var inspector = uiInspector(context);\n var dataEditor = uiDataEditor(context);\n var noteEditor = uiNoteEditor(context);\n var osmoseEditor = uiOsmoseEditor(context);\n var _current;\n var _wasData = false;\n var _wasNote = false;\n var _wasQaItem = false;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function sidebar(selection) {\n var container = context.container();\n var minWidth = 240;\n var sidebarWidth;\n var containerWidth;\n var dragOffset;\n\n // Set the initial width constraints\n selection\n .style('min-width', minWidth + 'px')\n .style('max-width', '400px')\n .style('width', '33.3333%');\n\n var resizer = selection\n .append('div')\n .attr('class', 'sidebar-resizer')\n .on(_pointerPrefix + 'down.sidebar-resizer', pointerdown);\n\n var downPointerId, lastClientX, containerLocGetter;\n\n function pointerdown(d3_event) {\n if (downPointerId) return;\n\n if ('button' in d3_event && d3_event.button !== 0) return;\n\n downPointerId = d3_event.pointerId || 'mouse';\n\n lastClientX = d3_event.clientX;\n\n containerLocGetter = utilFastMouse(container.node());\n\n // offset from edge of sidebar-resizer\n dragOffset = utilFastMouse(resizer.node())(d3_event)[0] - 1;\n\n sidebarWidth = selection.node().getBoundingClientRect().width;\n containerWidth = container.node().getBoundingClientRect().width;\n var widthPct = (sidebarWidth / containerWidth) * 100;\n selection\n .style('width', widthPct + '%') // lock in current width\n .style('max-width', '85%'); // but allow larger widths\n\n resizer.classed('dragging', true);\n\n d3_select(window)\n .on('touchmove.sidebar-resizer', function(d3_event) {\n // disable page scrolling while resizing on touch input\n d3_event.preventDefault();\n }, { passive: false })\n .on(_pointerPrefix + 'move.sidebar-resizer', pointermove)\n .on(_pointerPrefix + 'up.sidebar-resizer pointercancel.sidebar-resizer', pointerup);\n }\n\n function pointermove(d3_event) {\n\n if (downPointerId !== (d3_event.pointerId || 'mouse')) return;\n\n d3_event.preventDefault();\n\n var dx = d3_event.clientX - lastClientX;\n\n lastClientX = d3_event.clientX;\n\n var isRTL = (localizer.textDirection() === 'rtl');\n var scaleX = isRTL ? 0 : 1;\n var xMarginProperty = isRTL ? 'margin-right' : 'margin-left';\n\n var x = containerLocGetter(d3_event)[0] - dragOffset;\n sidebarWidth = isRTL ? containerWidth - x : x;\n\n var isCollapsed = selection.classed('collapsed');\n var shouldCollapse = sidebarWidth < minWidth;\n\n selection.classed('collapsed', shouldCollapse);\n\n if (shouldCollapse) {\n if (!isCollapsed) {\n selection\n .style(xMarginProperty, '-400px')\n .style('width', '400px');\n\n context.ui().onResize([(sidebarWidth - dx) * scaleX, 0]);\n }\n\n } else {\n var widthPct = (sidebarWidth / containerWidth) * 100;\n selection\n .style(xMarginProperty, null)\n .style('width', widthPct + '%');\n\n if (isCollapsed) {\n context.ui().onResize([-sidebarWidth * scaleX, 0]);\n } else {\n context.ui().onResize([-dx * scaleX, 0]);\n }\n }\n }\n\n function pointerup(d3_event) {\n if (downPointerId !== (d3_event.pointerId || 'mouse')) return;\n\n downPointerId = null;\n\n resizer.classed('dragging', false);\n\n d3_select(window)\n .on('touchmove.sidebar-resizer', null)\n .on(_pointerPrefix + 'move.sidebar-resizer', null)\n .on(_pointerPrefix + 'up.sidebar-resizer pointercancel.sidebar-resizer', null);\n }\n\n var featureListWrap = selection\n .append('div')\n .attr('class', 'feature-list-pane')\n .call(uiFeatureList(context));\n\n var inspectorWrap = selection\n .append('div')\n .attr('class', 'inspector-hidden inspector-wrap');\n\n var hoverModeSelect = function(targets) {\n context.container().selectAll('.feature-list-item button').classed('hover', false);\n\n if (context.selectedIDs().length > 1 &&\n targets && targets.length) {\n\n var elements = context.container().selectAll('.feature-list-item button')\n .filter(function (node) {\n return targets.indexOf(node) !== -1;\n });\n\n if (!elements.empty()) {\n elements.classed('hover', true);\n }\n }\n };\n\n sidebar.hoverModeSelect = throttle(hoverModeSelect, 200);\n\n function hover(targets) {\n var datum = targets && targets.length && targets[0];\n if (datum && datum.__featurehash__) { // hovering on data\n _wasData = true;\n sidebar\n .show(dataEditor.datum(datum));\n\n selection.selectAll('.sidebar-component')\n .classed('inspector-hover', true);\n\n } else if (datum instanceof osmNote) {\n if (context.mode().id === 'drag-note') return;\n _wasNote = true;\n\n var osm = services.osm;\n if (osm) {\n datum = osm.getNote(datum.id); // marker may contain stale data - get latest\n }\n\n sidebar\n .show(noteEditor.note(datum));\n\n selection.selectAll('.sidebar-component')\n .classed('inspector-hover', true);\n\n } else if (datum instanceof QAItem) {\n _wasQaItem = true;\n\n var errService = services[datum.service];\n if (errService) {\n // marker may contain stale data - get latest\n datum = errService.getError(datum.id);\n }\n\n // Currently only one possible service\n var errEditor;\n if (datum.service === 'osmose') {\n errEditor = osmoseEditor;\n }\n\n context.container().selectAll('.qaItem.' + datum.service)\n .classed('hover', function(d) { return d.id === datum.id; });\n\n sidebar\n .show(errEditor.error(datum));\n\n selection.selectAll('.sidebar-component')\n .classed('inspector-hover', true);\n\n } else if (!_current && (datum instanceof osmEntity)) {\n featureListWrap\n .classed('inspector-hidden', true);\n\n inspectorWrap\n .classed('inspector-hidden', false)\n .classed('inspector-hover', true);\n\n if (!inspector.entityIDs() || !utilArrayIdentical(inspector.entityIDs(), [datum.id]) || inspector.state() !== 'hover') {\n inspector\n .state('hover')\n .entityIDs([datum.id])\n .newFeature(false);\n\n inspectorWrap\n .call(inspector);\n }\n\n } else if (!_current) {\n featureListWrap\n .classed('inspector-hidden', false);\n inspectorWrap\n .classed('inspector-hidden', true);\n inspector\n .state('hide');\n\n } else if (_wasData || _wasNote || _wasQaItem) {\n _wasNote = false;\n _wasData = false;\n _wasQaItem = false;\n context.container().selectAll('.note').classed('hover', false);\n context.container().selectAll('.qaItem').classed('hover', false);\n sidebar.hide();\n }\n }\n\n sidebar.hover = throttle(hover, 200);\n\n\n sidebar.intersects = function(extent) {\n var rect = selection.node().getBoundingClientRect();\n return extent.intersects([\n context.projection.invert([0, rect.height]),\n context.projection.invert([rect.width, 0])\n ]);\n };\n\n\n sidebar.select = function(ids, newFeature) {\n sidebar.hide();\n\n if (ids && ids.length) {\n\n var entity = ids.length === 1 && context.entity(ids[0]);\n if (entity && newFeature && selection.classed('collapsed')) {\n // uncollapse the sidebar\n var extent = entity.extent(context.graph());\n sidebar.expand(sidebar.intersects(extent));\n }\n\n featureListWrap\n .classed('inspector-hidden', true);\n\n inspectorWrap\n .classed('inspector-hidden', false)\n .classed('inspector-hover', false);\n\n // reload the UI even if the ids are the same since the entities\n // themselves may have changed\n inspector\n .state('select')\n .entityIDs(ids)\n .newFeature(newFeature);\n\n inspectorWrap\n .call(inspector);\n\n } else {\n inspector\n .state('hide');\n }\n };\n\n\n sidebar.showPresetList = function() {\n inspector.showList();\n };\n\n\n sidebar.show = function(component, element) {\n featureListWrap\n .classed('inspector-hidden', true);\n inspectorWrap\n .classed('inspector-hidden', true);\n\n if (_current) _current.remove();\n _current = selection\n .append('div')\n .attr('class', 'sidebar-component')\n .call(component, element);\n };\n\n\n sidebar.hide = function() {\n featureListWrap\n .classed('inspector-hidden', false);\n inspectorWrap\n .classed('inspector-hidden', true);\n\n if (_current) _current.remove();\n _current = null;\n };\n\n\n sidebar.expand = function(moveMap) {\n if (selection.classed('collapsed')) {\n sidebar.toggle(moveMap);\n }\n };\n\n\n sidebar.collapse = function(moveMap) {\n if (!selection.classed('collapsed')) {\n sidebar.toggle(moveMap);\n }\n };\n\n\n sidebar.toggle = function(moveMap) {\n\n // Don't allow sidebar to toggle when the user is in the walkthrough.\n if (context.inIntro()) return;\n\n var isCollapsed = selection.classed('collapsed');\n var isCollapsing = !isCollapsed;\n var isRTL = (localizer.textDirection() === 'rtl');\n var scaleX = isRTL ? 0 : 1;\n var xMarginProperty = isRTL ? 'margin-right' : 'margin-left';\n\n sidebarWidth = selection.node().getBoundingClientRect().width;\n\n // switch from % to px\n selection.style('width', sidebarWidth + 'px');\n\n var startMargin, endMargin;\n if (isCollapsing) {\n startMargin = 0;\n endMargin = -sidebarWidth;\n } else {\n startMargin = -sidebarWidth;\n endMargin = 0;\n }\n let lastMargin = startMargin;\n\n if (!isCollapsing) {\n // unhide the sidebar's content before it transitions onscreen\n selection.classed('collapsed', isCollapsing);\n }\n\n selection\n .transition()\n .style(xMarginProperty, endMargin + 'px')\n .tween('panner', function() {\n var i = d3_interpolateNumber(startMargin, endMargin);\n return function(t) {\n var dx = lastMargin - Math.round(i(t));\n lastMargin = lastMargin - dx;\n context.ui().onResize(moveMap ? undefined : [dx * scaleX, 0]);\n };\n })\n .on('end', function() {\n if (isCollapsing) {\n // hide the sidebar's content after it transitions offscreen\n selection.classed('collapsed', isCollapsing);\n }\n\n // switch back from px to %\n if (!isCollapsing) {\n var containerWidth = container.node().getBoundingClientRect().width;\n var widthPct = (sidebarWidth / containerWidth) * 100;\n selection\n .style(xMarginProperty, null)\n .style('width', widthPct + '%');\n }\n });\n };\n\n // toggle the sidebar collapse when double-clicking the resizer\n resizer.on('dblclick', function(d3_event) {\n d3_event.preventDefault();\n if (d3_event.sourceEvent) {\n d3_event.sourceEvent.preventDefault();\n }\n sidebar.toggle();\n });\n\n // ensure hover sidebar is closed when zooming out beyond editable zoom\n context.map().on('crossEditableZoom.sidebar', function(within) {\n if (!within && !selection.select('.inspector-hover').empty()) {\n hover([]);\n }\n });\n }\n\n sidebar.showPresetList = function() {};\n sidebar.hover = function() {};\n sidebar.hover.cancel = function() {};\n sidebar.intersects = function() {};\n sidebar.select = function() {};\n sidebar.show = function() {};\n sidebar.hide = function() {};\n sidebar.expand = function() {};\n sidebar.collapse = function() {};\n sidebar.toggle = function() {};\n\n return sidebar;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { modeBrowse } from '../modes/browse';\n\n\nexport function uiSourceSwitch(context) {\n var keys;\n\n\n function click(d3_event) {\n d3_event.preventDefault();\n\n var osm = context.connection();\n if (!osm) return;\n\n if (context.inIntro()) return;\n\n if (context.history().hasChanges() &&\n !window.confirm(t('source_switch.lose_changes'))) return;\n\n var isLive = d3_select(this)\n .classed('live');\n\n isLive = !isLive;\n context.enter(modeBrowse(context));\n context.history().clearSaved(); // remove saved history\n context.flush(); // remove stored data\n\n d3_select(this)\n .classed('live', isLive)\n .classed('chip', isLive)\n .text('')\n .call(isLive ? t.append('source_switch.live') : t.append('source_switch.dev'));\n\n osm.switch(isLive ? keys[0] : keys[1]); // switch connection (warning: dispatches 'change' event)\n }\n\n var sourceSwitch = function(selection) {\n selection\n .append('a')\n .attr('href', '#')\n .call(t.append('source_switch.live'))\n .attr('class', 'live chip')\n .on('click', click);\n };\n\n\n sourceSwitch.keys = function(_) {\n if (!arguments.length) return keys;\n keys = _;\n return sourceSwitch;\n };\n\n\n return sourceSwitch;\n}\n", "export function uiSpinner(context) {\n var osm = context.connection();\n\n\n return function(selection) {\n var img = selection\n .append('img')\n .attr('src', context.imagePath('loader-black.gif'))\n .style('opacity', 0);\n\n if (osm) {\n osm\n .on('loading.spinner', function() {\n img.transition()\n .style('opacity', 1);\n })\n .on('loaded.spinner', function() {\n img.transition()\n .style('opacity', 0);\n });\n }\n };\n}\n", "import { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\n\nexport function uiSectionPrivacy(context) {\n let section = uiSection('preferences-third-party', context)\n .label(() => t.append('preferences.privacy.title'))\n .disclosureContent(renderDisclosureContent);\n\n function renderDisclosureContent(selection) {\n // enter\n selection.selectAll('.privacy-options-list')\n .data([0])\n .enter()\n .append('ul')\n .attr('class', 'layer-list privacy-options-list');\n\n let thirdPartyIconsEnter = selection.select('.privacy-options-list')\n .selectAll('.privacy-third-party-icons-item')\n .data([prefs('preferences.privacy.thirdpartyicons') || 'true'])\n .enter()\n .append('li')\n .attr('class', 'privacy-third-party-icons-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('preferences.privacy.third_party_icons.tooltip'))\n .placement('bottom')\n );\n\n thirdPartyIconsEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', (d3_event, d) => {\n d3_event.preventDefault();\n prefs('preferences.privacy.thirdpartyicons', d === 'true' ? 'false' : 'true');\n });\n\n thirdPartyIconsEnter\n .append('span')\n .call(t.append('preferences.privacy.third_party_icons.description'));\n\n // update\n selection.selectAll('.privacy-third-party-icons-item')\n .classed('active', d => d === 'true')\n .select('input')\n .property('checked', d => d === 'true');\n\n // Privacy Policy link\n selection.selectAll('.privacy-link')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'privacy-link')\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', 'https://github.com/openstreetmap/iD/blob/release/PRIVACY.md')\n .append('span')\n .call(t.append('preferences.privacy.privacy_link'));\n\n }\n\n prefs.onChange('preferences.privacy.thirdpartyicons', section.reRender);\n\n return section;\n}\n", "import { prefs } from '../core/preferences';\nimport { fileFetcher } from '../core/file_fetcher';\nimport { t } from '../core/localizer';\nimport { uiIntro } from './intro';\nimport { uiModal } from './modal';\nimport { uiSectionPrivacy } from './sections/privacy';\n\n\nexport function uiSplash(context) {\n return (selection) => {\n // Exception - if there are restorable changes, skip this splash screen.\n // This is because we currently only support one `uiModal` at a time\n // and we need to show them `uiRestore`` instead of this one.\n if (context.history().hasRestorableChanges()) return;\n\n // If user has not seen this version of the privacy policy, show the splash again.\n let updateMessage = '';\n const sawPrivacyVersion = prefs('sawPrivacyVersion');\n let showSplash = !prefs('sawSplash');\n if (sawPrivacyVersion && sawPrivacyVersion !== context.privacyVersion) {\n updateMessage = t('splash.privacy_update');\n showSplash = true;\n }\n\n if (!showSplash) return;\n\n prefs('sawSplash', true);\n prefs('sawPrivacyVersion', context.privacyVersion);\n\n // fetch intro graph data now, while user is looking at the splash screen\n fileFetcher.get('intro_graph');\n\n let modalSelection = uiModal(selection);\n\n modalSelection.select('.modal')\n .attr('class', 'modal-splash modal');\n\n let introModal = modalSelection.select('.content')\n .append('div')\n .attr('class', 'fillL');\n\n introModal\n .append('div')\n .attr('class','modal-section')\n .append('h3')\n .call(t.append('splash.welcome'));\n\n let modalSection = introModal\n .append('div')\n .attr('class','modal-section');\n\n modalSection\n .append('p')\n .call(t.addOrUpdate('splash.text', {\n version: context.version,\n website: selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/openstreetmap/iD/blob/develop/CHANGELOG.md#whats-new')\n .call(t.addOrUpdate('splash.changelog')),\n github: selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/openstreetmap/iD/issues')\n .text('github.com')\n }));\n\n modalSection\n .append('p')\n .call(t.addOrUpdate('splash.privacy', {\n updateMessage: updateMessage,\n privacyLink: selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/openstreetmap/iD/blob/release/PRIVACY.md')\n .call(t.addOrUpdate('splash.privacy_policy'))\n }));\n\n uiSectionPrivacy(context)\n .label(() => t.append('splash.privacy_settings'))\n .render(modalSection);\n\n let buttonWrap = introModal\n .append('div')\n .attr('class', 'modal-actions');\n\n let walkthrough = buttonWrap\n .append('button')\n .attr('class', 'walkthrough')\n .on('click', () => {\n context.container().call(uiIntro(context));\n modalSelection.close();\n });\n\n walkthrough\n .append('svg')\n .attr('class', 'logo logo-walkthrough')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n walkthrough\n .append('div')\n .call(t.append('splash.walkthrough'));\n\n let startEditing = buttonWrap\n .append('button')\n .attr('class', 'start-editing')\n .on('click', modalSelection.close);\n\n startEditing\n .append('svg')\n .attr('class', 'logo logo-features')\n .append('use')\n .attr('xlink:href', '#iD-logo-features');\n\n startEditing\n .append('div')\n .call(t.append('splash.start'));\n\n modalSelection.select('button.close')\n .attr('class','hide');\n };\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiStatus(context) {\n var osm = context.connection();\n\n\n return function(selection) {\n if (!osm) return;\n\n function update(err, apiStatus) {\n selection.html('');\n\n if (err) {\n if (apiStatus === 'connectionSwitched') {\n // if the connection was just switched, we can't rely on\n // the status (we're getting the status of the previous api)\n return;\n\n } else if (apiStatus === 'rateLimited') {\n if (!osm.authenticated()) {\n selection\n .call(t.append('osm_api_status.message.rateLimit'))\n .append('a')\n .attr('href', '#')\n .attr('class', 'api-status-login')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append('login'))\n .on('click.login', function(d3_event) {\n d3_event.preventDefault();\n osm.authenticate();\n });\n } else {\n selection.call(t.append('osm_api_status.message.rateLimited'));\n }\n } else {\n\n // don't allow retrying too rapidly\n var throttledRetry = throttle(function() {\n // try loading the visible tiles\n context.loadTiles(context.projection);\n // manually reload the status too in case all visible tiles were already loaded\n osm.reloadApiStatus();\n }, 2000);\n\n // eslint-disable-next-line no-warning-comments\n // TODO: nice messages for different error types\n selection\n .call(t.append('osm_api_status.message.error', { suffix: ' ' }))\n .append('a')\n .attr('href', '#')\n // let the user manually retry their connection directly\n .call(t.append('osm_api_status.retry'))\n .on('click.retry', function(d3_event) {\n d3_event.preventDefault();\n throttledRetry();\n });\n }\n\n } else if (apiStatus === 'readonly') {\n selection.call(t.append('osm_api_status.message.readonly'));\n } else if (apiStatus === 'offline') {\n selection.call(t.append('osm_api_status.message.offline'));\n }\n\n selection.attr('class', 'api-status ' + (err ? 'error' : apiStatus));\n }\n\n osm.on('apiStatusChange.uiStatus', update);\n\n context.history().on('storage_error', () => {\n selection.selectAll('span.local-storage-full').remove();\n selection\n .append('span')\n .attr('class', 'local-storage-full')\n .call(t.append('osm_api_status.message.local_storage_full'));\n selection.classed('error', true);\n });\n\n // reload the status periodically regardless of other factors\n window.setInterval(function() {\n osm.reloadApiStatus();\n }, 90000);\n\n // load the initial status in case no OSM data was loaded yet\n osm.reloadApiStatus();\n };\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport {\n modeAddArea,\n modeAddLine,\n modeAddPoint,\n modeBrowse\n} from '../../modes';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiTooltip } from '../tooltip';\n\nexport function uiToolDrawModes(context) {\n\n var tool = {\n id: 'old_modes',\n label: t.append('toolbar.add_feature')\n };\n\n var modes = [\n modeAddPoint(context, {\n title: t.append('modes.add_point.title'),\n button: 'point',\n description: t.append('modes.add_point.description'),\n preset: presetManager.item('point'),\n key: '1'\n }),\n modeAddLine(context, {\n title: t.append('modes.add_line.title'),\n button: 'line',\n description: t.append('modes.add_line.description'),\n preset: presetManager.item('line'),\n key: '2'\n }),\n modeAddArea(context, {\n title: t.append('modes.add_area.title'),\n button: 'area',\n description: t.append('modes.add_area.description'),\n preset: presetManager.item('area'),\n key: '3'\n })\n ];\n\n\n function enabled(\n // eslint-disable-next-line no-unused-vars\n _mode // parameter is currently not used, but might be at some point\n ) {\n return osmEditable();\n }\n\n function osmEditable() {\n return context.editable();\n }\n\n modes.forEach(function(mode) {\n context.keybinding().on(mode.key, function() {\n if (!enabled(mode)) return;\n\n if (mode.id === context.mode().id) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(mode);\n }\n });\n });\n\n tool.render = function(selection) {\n\n var wrap = selection\n .append('div')\n .attr('class', 'joined')\n .style('display', 'flex');\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n\n context.map()\n .on('move.modes', debouncedUpdate)\n .on('drawn.modes', debouncedUpdate);\n\n context\n .on('enter.modes', update);\n\n update();\n\n\n function update() {\n\n var buttons = wrap.selectAll('button.add-button')\n .data(modes, function(d) { return d.id; });\n\n // exit\n buttons.exit()\n .remove();\n\n // enter\n var buttonsEnter = buttons.enter()\n .append('button')\n .attr('class', function(d) { return d.id + ' add-button bar-button'; })\n .on('click.mode-buttons', function(d3_event, d) {\n if (!enabled(d)) return;\n\n // When drawing, ignore accidental clicks on mode buttons - #4042\n var currMode = context.mode().id;\n if (/^draw/.test(currMode)) return;\n\n if (d.id === currMode) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(d);\n }\n })\n .call(uiTooltip()\n .placement('bottom')\n .title(function(d) { return d.description; })\n .keys(function(d) { return [d.key]; })\n .scrollContainer(context.container().select('.top-toolbar'))\n );\n\n buttonsEnter\n .each(function(d) {\n d3_select(this)\n .call(svgIcon('#iD-icon-' + d.button));\n });\n\n buttonsEnter\n .append('span')\n .attr('class', 'label')\n .text('')\n .each(function(mode) { mode.title(d3_select(this)); });\n\n // if we are adding/removing the buttons, check if toolbar has overflowed\n if (buttons.enter().size() || buttons.exit().size()) {\n context.ui().checkOverflow('.top-toolbar', true);\n }\n\n // update\n buttons\n .merge(buttonsEnter)\n .attr('aria-disabled', function(d) { return !enabled(d); })\n .classed('disabled', function(d) { return !enabled(d); })\n .attr('aria-pressed', function(d) { return context.mode() && context.mode().button === d.button; })\n .classed('active', function(d) { return context.mode() && context.mode().button === d.button; });\n }\n };\n\n return tool;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport {\n modeAddNote,\n modeBrowse\n} from '../../modes';\n\nimport { t } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiTooltip } from '../tooltip';\n\nexport function uiToolNotes(context) {\n\n var tool = {\n id: 'notes',\n label: t.append('modes.add_note.label')\n };\n\n var mode = modeAddNote(context);\n\n function enabled() {\n return notesEnabled() && notesEditable();\n }\n\n function notesEnabled() {\n var noteLayer = context.layers().layer('notes');\n return noteLayer && noteLayer.enabled();\n }\n\n function notesEditable() {\n var mode = context.mode();\n return context.map().notesEditable() && mode && mode.id !== 'save';\n }\n\n context.keybinding().on(mode.key, function() {\n if (!enabled()) return;\n\n if (mode.id === context.mode().id) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(mode);\n }\n });\n\n tool.render = function(selection) {\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n\n context.map()\n .on('move.notes', debouncedUpdate)\n .on('drawn.notes', debouncedUpdate);\n\n context\n .on('enter.notes', update);\n\n update();\n\n\n function update() {\n var showNotes = notesEnabled();\n var data = showNotes ? [mode] : [];\n\n var buttons = selection.selectAll('button.add-button')\n .data(data, function(d) { return d.id; });\n\n // exit\n buttons.exit()\n .remove();\n\n // enter\n var buttonsEnter = buttons.enter()\n .append('button')\n .attr('class', function(d) { return d.id + ' add-button bar-button'; })\n .on('click.notes', function(d3_event, d) {\n if (!enabled()) return;\n\n // When drawing, ignore accidental clicks on mode buttons - #4042\n var currMode = context.mode().id;\n if (/^draw/.test(currMode)) return;\n\n if (d.id === currMode) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(d);\n }\n })\n .call(uiTooltip()\n .placement('bottom')\n .title(function(d) { return d.description; })\n .keys(function(d) { return [d.key]; })\n .scrollContainer(context.container().select('.top-toolbar'))\n );\n\n buttonsEnter\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(d.icon || '#iD-icon-' + d.button));\n });\n\n // if we are adding/removing the buttons, check if toolbar has overflowed\n if (buttons.enter().size() || buttons.exit().size()) {\n context.ui().checkOverflow('.top-toolbar', true);\n }\n\n // update\n buttons\n .merge(buttonsEnter)\n .classed('disabled', function() { return !enabled(); })\n .attr('aria-disabled', function() { return !enabled(); })\n .classed('active', function(d) { return context.mode() && context.mode().button === d.button; })\n .attr('aria-pressed', function(d) { return context.mode() && context.mode().button === d.button; });\n }\n };\n\n tool.uninstall = function() {\n context\n .on('enter.editor.notes', null)\n .on('exit.editor.notes', null)\n .on('enter.notes', null);\n\n context.map()\n .on('move.notes', null)\n .on('drawn.notes', null);\n };\n\n return tool;\n}\n", "import { interpolateRgb as d3_interpolateRgb } from 'd3-interpolate';\n\nimport { t } from '../../core/localizer';\nimport { modeSave } from '../../modes';\nimport { svgIcon } from '../../svg';\nimport { uiCmd } from '../cmd';\nimport { uiTooltip } from '../tooltip';\n\n\nexport function uiToolSave(context) {\n\n var tool = {\n id: 'save',\n label: t.append('save.title')\n };\n\n var button = null;\n var tooltipBehavior = null;\n var history = context.history();\n var key = uiCmd('\u2318S');\n var _numChanges = 0;\n\n function isSaving() {\n var mode = context.mode();\n return mode && mode.id === 'save';\n }\n\n function isDisabled() {\n return _numChanges === 0 || isSaving();\n }\n\n function save(d3_event) {\n d3_event.preventDefault();\n if (!context.inIntro() && !isSaving() && history.hasChanges()) {\n context.enter(modeSave(context));\n }\n }\n\n function bgColor(numChanges) {\n var step;\n if (numChanges === 0) {\n return null;\n } else if (numChanges <= 50) {\n step = numChanges / 50;\n return d3_interpolateRgb('#fff0', '#ff08')(step); // transparent -> yellow\n } else {\n step = Math.min((numChanges - 50) / 50, 1.0);\n return d3_interpolateRgb('#ff08', '#f008')(step); // yellow -> red\n }\n }\n\n function updateCount() {\n var val = history.difference().summary().length;\n if (val === _numChanges) return;\n\n _numChanges = val;\n\n if (tooltipBehavior) {\n tooltipBehavior\n .title(() => t.append(_numChanges > 0 ? 'save.help' : 'save.no_changes'))\n .keys([key]);\n }\n\n if (button) {\n button\n .classed('disabled', isDisabled())\n .style('--accent-color', bgColor(_numChanges));\n\n button.select('span.count')\n .text(_numChanges);\n }\n }\n\n\n tool.render = function(selection) {\n tooltipBehavior = uiTooltip()\n .placement('bottom')\n .title(() => t.append('save.no_changes'))\n .keys([key])\n .scrollContainer(context.container().select('.top-toolbar'));\n\n var lastPointerUpType;\n\n button = selection\n .append('button')\n .attr('class', 'save disabled bar-button')\n .on('pointerup', function(d3_event) {\n lastPointerUpType = d3_event.pointerType;\n })\n .on('click', function(d3_event) {\n save(d3_event);\n\n if (_numChanges === 0 && (\n lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen')\n ) {\n // there are no tooltips for touch interactions so flash feedback instead\n context.ui().flash\n .duration(2000)\n .iconName('#iD-icon-save')\n .iconClass('disabled')\n .label(t.append('save.no_changes'))();\n }\n lastPointerUpType = null;\n })\n .call(tooltipBehavior);\n\n button\n .call(svgIcon('#iD-icon-save'));\n\n button\n .append('span')\n .attr('class', 'count')\n .attr('aria-hidden', 'true')\n .text('0');\n\n updateCount();\n\n\n context.keybinding()\n .on(key, save, true);\n\n\n context.history()\n .on('change.save', updateCount);\n\n context\n .on('enter.save', function() {\n if (button) {\n button\n .classed('disabled', isDisabled());\n\n if (isSaving()) {\n button.call(tooltipBehavior.hide);\n }\n }\n });\n };\n\n\n tool.uninstall = function() {\n context.keybinding()\n .off(key, true);\n\n context.history()\n .on('change.save', null);\n\n context\n .on('enter.save', null);\n\n button = null;\n tooltipBehavior = null;\n };\n\n return tool;\n}\n", "import { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiTooltip } from '../tooltip';\n\nexport function uiToolSidebarToggle(context) {\n\n var tool = {\n id: 'sidebar_toggle',\n label: t.append('toolbar.inspect')\n };\n\n tool.render = function(selection) {\n selection\n .append('button')\n .attr('class', 'bar-button')\n .attr('aria-label', t('sidebar.tooltip'))\n .on('click', function() {\n context.ui().sidebar.toggle();\n })\n .call(uiTooltip()\n .placement('bottom')\n .title(() => t.append('sidebar.tooltip'))\n .keys([t('sidebar.key')])\n .scrollContainer(context.container().select('.top-toolbar'))\n )\n .call(svgIcon('#iD-icon-sidebar-' + (localizer.textDirection() === 'rtl' ? 'right' : 'left')));\n };\n\n return tool;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiCmd } from '../cmd';\nimport { uiTooltip } from '../tooltip';\n\n\nexport function uiToolUndoRedo(context) {\n\n var tool = {\n id: 'undo_redo',\n label: t.append('toolbar.undo_redo')\n };\n\n var commands = [{\n id: 'undo',\n cmd: uiCmd('\u2318Z'),\n action: function() {\n context.undo();\n },\n annotation: function() {\n return context.history().undoAnnotation();\n },\n icon: 'iD-icon-' + (localizer.textDirection() === 'rtl' ? 'redo' : 'undo')\n }, {\n id: 'redo',\n cmd: uiCmd('\u2318\u21E7Z'),\n action: function() {\n context.redo();\n },\n annotation: function() {\n return context.history().redoAnnotation();\n },\n icon: 'iD-icon-' + (localizer.textDirection() === 'rtl' ? 'undo' : 'redo')\n }];\n\n\n function editable() {\n return context.mode() && context.mode().id !== 'save' && context.map().editableDataEnabled(true /* ignore min zoom */);\n }\n\n\n tool.render = function(selection) {\n var tooltipBehavior = uiTooltip()\n .placement('bottom')\n .title(function (d) {\n return d.annotation() ?\n t.append(d.id + '.tooltip', { action: d.annotation() }) :\n t.append(d.id + '.nothing');\n })\n .keys(function(d) {\n return [d.cmd];\n })\n .scrollContainer(context.container().select('.top-toolbar'));\n\n var lastPointerUpType;\n\n var buttons = selection.selectAll('button')\n .data(commands)\n .enter()\n .append('button')\n .attr('class', function(d) { return 'disabled ' + d.id + '-button bar-button'; })\n .on('pointerup', function(d3_event) {\n // `pointerup` is always called before `click`\n lastPointerUpType = d3_event.pointerType;\n })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n\n var annotation = d.annotation();\n\n if (editable() && annotation) {\n d.action();\n }\n\n if (editable() && (\n lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen')\n ) {\n // there are no tooltips for touch interactions so flash feedback instead\n\n var label = annotation ?\n t.append(d.id + '.tooltip', { action: annotation }) :\n t.append(d.id + '.nothing');\n context.ui().flash\n .duration(2000)\n .iconName('#' + d.icon)\n .iconClass(annotation ? '' : 'disabled')\n .label(label)();\n }\n lastPointerUpType = null;\n })\n .call(tooltipBehavior);\n\n buttons.each(function(d) {\n d3_select(this)\n .call(svgIcon('#' + d.icon));\n });\n\n context.keybinding()\n .on(commands[0].cmd, function(d3_event) {\n d3_event.preventDefault();\n if (editable()) commands[0].action();\n })\n .on(commands[1].cmd, function(d3_event) {\n d3_event.preventDefault();\n if (editable()) commands[1].action();\n });\n\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n\n context.map()\n .on('move.undo_redo', debouncedUpdate)\n .on('drawn.undo_redo', debouncedUpdate);\n\n context.history()\n .on('change.undo_redo', function(difference) {\n if (difference) update();\n });\n\n context\n .on('enter.undo_redo', update);\n\n\n function update() {\n buttons\n .classed('disabled', function(d) {\n return !editable() || !d.annotation();\n })\n .each(function() {\n var selection = d3_select(this);\n if (!selection.select('.tooltip.in').empty()) {\n selection.call(tooltipBehavior.updateContent);\n }\n });\n }\n };\n\n tool.uninstall = function() {\n context.keybinding()\n .off(commands[0].cmd)\n .off(commands[1].cmd);\n\n context.map()\n .on('move.undo_redo', null)\n .on('drawn.undo_redo', null);\n\n context.history()\n .on('change.undo_redo', null);\n\n context\n .on('enter.undo_redo', null);\n };\n\n return tool;\n}\n", "export * from './modes';\nexport * from './notes';\nexport * from './save';\nexport * from './sidebar_toggle';\nexport * from './undo_redo';\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { debounce } from 'es-toolkit/compat';\nimport { uiToolDrawModes, uiToolNotes, uiToolSave, uiToolSidebarToggle, uiToolUndoRedo } from './tools';\n\n\nexport function uiTopToolbar(context) {\n\n var sidebarToggle = uiToolSidebarToggle(context),\n modes = uiToolDrawModes(context),\n notes = uiToolNotes(context),\n undoRedo = uiToolUndoRedo(context),\n save = uiToolSave(context);\n\n function notesEnabled() {\n var noteLayer = context.layers().layer('notes');\n return noteLayer && noteLayer.enabled();\n }\n\n function topToolbar(bar) {\n\n bar.on('wheel.topToolbar', function(d3_event) {\n if (!d3_event.deltaX) {\n // translate vertical scrolling into horizontal scrolling in case\n // the user doesn't have an input device that can scroll horizontally\n bar.node().scrollLeft += d3_event.deltaY;\n }\n });\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n context.layers()\n .on('change.topToolbar', debouncedUpdate);\n\n update();\n\n function update() {\n\n var tools = [\n sidebarToggle,\n 'spacer',\n modes\n ];\n\n tools.push('spacer');\n\n if (notesEnabled()) {\n tools = tools.concat([notes, 'spacer']);\n }\n\n tools = tools.concat([undoRedo, save]);\n\n var toolbarItems = bar.selectAll('.toolbar-item')\n .data(tools, function(d) {\n return d.id || d;\n });\n\n toolbarItems.exit()\n .each(function(d) {\n if (d.uninstall) {\n d.uninstall();\n }\n })\n .remove();\n\n var itemsEnter = toolbarItems\n .enter()\n .append('div')\n .attr('class', function(d) {\n var classes = 'toolbar-item ' + (d.id || d).replaceAll('_', '-');\n if (d.klass) classes += ' ' + d.klass;\n return classes;\n });\n\n var actionableItems = itemsEnter.filter(function(d) { return d !== 'spacer'; });\n\n actionableItems\n .append('div')\n .attr('class', 'item-content')\n .each(function(d) {\n d3_select(this).call(d.render, bar);\n });\n\n actionableItems\n .append('div')\n .attr('class', 'item-label')\n .each(function(d) { d.label(d3_select(this)); });\n }\n\n }\n\n return topToolbar;\n}\n", "import { prefs } from '../core/preferences';\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiTooltip } from './tooltip';\n\n\n// these are module variables so they are preserved through a ui.restart()\nvar sawVersion = null;\nvar isNewVersion = false;\nvar isNewUser = false;\n\n\nexport function uiVersion(context) {\n\n var currVersion = context.version;\n var matchedVersion = currVersion.match(/\\d+\\.\\d+\\.\\d+.*/);\n\n if (sawVersion === null && matchedVersion !== null) {\n if (prefs('sawVersion')) {\n isNewUser = false;\n isNewVersion = prefs('sawVersion') !== currVersion && currVersion.indexOf('-') === -1;\n } else {\n isNewUser = true;\n isNewVersion = true;\n }\n prefs('sawVersion', currVersion);\n sawVersion = currVersion;\n }\n\n return function(selection) {\n selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/OpenHistoricalMap/iD')\n .text(currVersion);\n\n // only show new version indicator to users that have used iD before\n if (isNewVersion && !isNewUser) {\n selection\n .append('a')\n .attr('class', 'badge')\n .attr('target', '_blank')\n .attr('href', `https://github.com/OpenHistoricalMap/iD/releases/tag/v${currVersion}`)\n .call(svgIcon('#maki-gift'))\n .call(uiTooltip()\n .title(() => t.append('version.whats_new', { version: currVersion }))\n .placement('top')\n .scrollContainer(context.container().select('.main-footer-wrap'))\n );\n }\n };\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t, localizer } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\nimport { uiTooltip } from './tooltip';\nimport { utilKeybinding } from '../util/keybinding';\n\n\nexport function uiZoom(context) {\n\n var zooms = [{\n id: 'zoom-in',\n icon: 'iD-icon-plus',\n title: t.append('zoom.in'),\n action: zoomIn,\n disabled: function() {\n return !context.map().canZoomIn();\n },\n disabledTitle: t.append('zoom.disabled.in'),\n key: '+'\n }, {\n id: 'zoom-out',\n icon: 'iD-icon-minus',\n title: t.append('zoom.out'),\n action: zoomOut,\n disabled: function() {\n return !context.map().canZoomOut();\n },\n disabledTitle: t.append('zoom.disabled.out'),\n key: '-'\n }];\n\n function zoomIn(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomIn();\n }\n\n function zoomOut(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomOut();\n }\n\n function zoomInFurther(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomInFurther();\n }\n\n function zoomOutFurther(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomOutFurther();\n }\n\n return function(selection) {\n var tooltipBehavior = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(function(d) {\n if (d.disabled()) {\n return d.disabledTitle;\n }\n return d.title;\n })\n .keys(function(d) {\n return [d.key];\n });\n\n var lastPointerUpType;\n\n var buttons = selection.selectAll('button')\n .data(zooms)\n .enter()\n .append('button')\n .attr('class', function(d) { return d.id; })\n .on('pointerup.editor', function(d3_event) {\n lastPointerUpType = d3_event.pointerType;\n })\n .on('click.editor', function(d3_event, d) {\n if (!d.disabled()) {\n d.action(d3_event);\n } else if (lastPointerUpType === 'touch' || lastPointerUpType === 'pen') {\n context.ui().flash\n .duration(2000)\n .iconName('#' + d.icon)\n .iconClass('disabled')\n .label(d.disabledTitle)();\n }\n lastPointerUpType = null;\n })\n .call(tooltipBehavior);\n\n buttons.each(function(d) {\n d3_select(this)\n .call(svgIcon('#' + d.icon, 'light'));\n });\n\n utilKeybinding.plusKeys.forEach(function(key) {\n context.keybinding().on([key], zoomIn);\n context.keybinding().on([uiCmd('\u2325' + key)], zoomInFurther);\n });\n\n utilKeybinding.minusKeys.forEach(function(key) {\n context.keybinding().on([key], zoomOut);\n context.keybinding().on([uiCmd('\u2325' + key)], zoomOutFurther);\n });\n\n function updateButtonStates() {\n buttons\n .classed('disabled', function(d) {\n return d.disabled();\n })\n .each(function() {\n var selection = d3_select(this);\n if (!selection.select('.tooltip.in').empty()) {\n selection.call(tooltipBehavior.updateContent);\n }\n });\n }\n\n updateButtonStates();\n\n context.map().on('move.uiZoom', updateButtonStates);\n };\n}\n", "import { t, localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\nimport { svgIcon } from '../svg/icon';\n\nexport function uiZoomToSelection(context) {\n\n function isDisabled() {\n var mode = context.mode();\n return !mode || !mode.zoomToSelected;\n }\n\n var _lastPointerUpType;\n\n function pointerup(d3_event) {\n _lastPointerUpType = d3_event.pointerType;\n }\n\n function click(d3_event) {\n d3_event.preventDefault();\n\n if (isDisabled()) {\n if (_lastPointerUpType === 'touch' || _lastPointerUpType === 'pen') {\n context.ui().flash\n .duration(2000)\n .iconName('#iD-icon-framed-dot')\n .iconClass('disabled')\n .label(t.append('inspector.zoom_to.no_selection'))();\n }\n } else {\n var mode = context.mode();\n if (mode && mode.zoomToSelected) {\n mode.zoomToSelected();\n }\n }\n\n _lastPointerUpType = null;\n }\n\n return function(selection) {\n\n var tooltipBehavior = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(function() {\n if (isDisabled()) {\n return t.append('inspector.zoom_to.no_selection');\n }\n return t.append('inspector.zoom_to.title');\n })\n .keys([t('inspector.zoom_to.key')]);\n\n var button = selection\n .append('button')\n .on('pointerup', pointerup)\n .on('click', click)\n .call(svgIcon('#iD-icon-framed-dot', 'light'))\n .call(tooltipBehavior);\n\n function setEnabledState() {\n button.classed('disabled', isDisabled());\n if (!button.select('.tooltip.in').empty()) {\n button.call(tooltipBehavior.updateContent);\n }\n }\n\n context.on('enter.uiZoomToSelection', setEnabledState);\n\n setEnabledState();\n };\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { svgIcon } from '../svg/icon';\nimport { t, localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\n\n\nexport function uiPane(id, context) {\n\n var _key;\n var _label = '';\n var _description = '';\n var _iconName = '';\n var _sections; // array of uiSection objects\n\n var _paneSelection = d3_select(null);\n\n var _paneTooltip;\n\n var pane = {\n id: id\n };\n\n pane.label = function(val) {\n if (!arguments.length) return _label;\n _label = val;\n return pane;\n };\n\n pane.key = function(val) {\n if (!arguments.length) return _key;\n _key = val;\n return pane;\n };\n\n pane.description = function(val) {\n if (!arguments.length) return _description;\n _description = val;\n return pane;\n };\n\n pane.iconName = function(val) {\n if (!arguments.length) return _iconName;\n _iconName = val;\n return pane;\n };\n\n pane.sections = function(val) {\n if (!arguments.length) return _sections;\n _sections = val;\n return pane;\n };\n\n pane.selection = function() {\n return _paneSelection;\n };\n\n function hidePane() {\n context.ui().togglePanes();\n }\n\n pane.togglePane = function(d3_event) {\n if (d3_event) d3_event.preventDefault();\n _paneTooltip.hide();\n context.ui().togglePanes(!_paneSelection.classed('shown') ? _paneSelection : undefined);\n };\n\n pane.renderToggleButton = function(selection) {\n\n if (!_paneTooltip) {\n _paneTooltip = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(() => _description)\n .keys([_key]);\n }\n\n selection\n .append('button')\n .on('click', pane.togglePane)\n .call(svgIcon('#' + _iconName, 'light'))\n .call(_paneTooltip);\n };\n\n pane.renderContent = function(selection) {\n // override to fully customize content\n\n if (_sections) {\n _sections.forEach(function(section) {\n selection.call(section.render);\n });\n }\n };\n\n pane.renderPane = function(selection) {\n\n _paneSelection = selection\n .append('div')\n .attr('class', 'fillL map-pane hide ' + id + '-pane')\n .attr('pane', id);\n\n var heading = _paneSelection\n .append('div')\n .attr('class', 'pane-heading');\n\n heading\n .append('h2')\n .text('')\n .call(_label);\n\n heading\n .append('button')\n .attr('title', t('icons.close'))\n .on('click', hidePane)\n .call(svgIcon('#iD-icon-close'));\n\n\n _paneSelection\n .append('div')\n .attr('class', 'pane-content')\n .call(pane.renderContent);\n\n if (_key) {\n context.keybinding()\n .on(_key, pane.togglePane);\n }\n };\n\n return pane;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\nimport { clamp } from 'es-toolkit/compat';\n\nimport { prefs } from '../../core/preferences';\nimport { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\n\n\nexport function uiSectionBackgroundDisplayOptions(context) {\n\n var section = uiSection('background-display-options', context)\n .label(() => t.append('background.display_options'))\n .disclosureContent(renderDisclosureContent);\n\n var _storedOpacity = prefs('background-opacity');\n var _minVal = 0;\n var _maxVal = 3;\n\n var _sliders = ['brightness', 'contrast', 'saturation', 'sharpness'];\n\n var _options = {\n brightness: (_storedOpacity !== null ? (+_storedOpacity) : 1),\n contrast: 1,\n saturation: 1,\n sharpness: 1\n };\n\n function updateValue(d, val) {\n val = clamp(val, _minVal, _maxVal);\n\n _options[d] = val;\n context.background()[d](val);\n\n if (d === 'brightness') {\n prefs('background-opacity', val);\n }\n\n section.reRender();\n }\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.display-options-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'display-options-container controls-list');\n\n // add slider controls\n var slidersEnter = containerEnter.selectAll('.display-control')\n .data(_sliders)\n .enter()\n .append('label')\n .attr('class', function(d) { return 'display-control display-control-' + d; });\n\n slidersEnter\n .each(function(d) {\n d3_select(this).call(t.append('background.' + d));\n })\n .append('span')\n .attr('class', function(d) { return 'display-option-value display-option-value-' + d; });\n\n var sildersControlEnter = slidersEnter\n .append('div')\n .attr('class', 'control-wrap');\n\n sildersControlEnter\n .append('input')\n .attr('class', function(d) { return 'display-option-input display-option-input-' + d; })\n .attr('type', 'range')\n .attr('min', _minVal)\n .attr('max', _maxVal)\n .attr('step', '0.01')\n .on('input', function(d3_event, d) {\n var val = d3_select(this).property('value');\n if (!val && d3_event && d3_event.target) {\n val = d3_event.target.value;\n }\n updateValue(d, val);\n });\n\n sildersControlEnter\n .append('button')\n .attr('title', function(d) { return `${t('background.reset')} ${t('background.' + d)}`; })\n .attr('class', function(d) { return 'display-option-reset display-option-reset-' + d; })\n .on('click', function(d3_event, d) {\n if (d3_event.button !== 0) return;\n updateValue(d, 1);\n })\n .call(svgIcon('#iD-icon-' + (localizer.textDirection() === 'rtl' ? 'redo' : 'undo')));\n\n // reset all button\n containerEnter\n .append('a')\n .attr('class', 'display-option-resetlink')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('background.reset_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n for (var i = 0; i < _sliders.length; i++) {\n updateValue(_sliders[i], 1);\n }\n });\n\n // update\n container = containerEnter\n .merge(container);\n\n container.selectAll('.display-option-input')\n .property('value', function(d) { return _options[d]; });\n\n container.selectAll('.display-option-value')\n .text(function(d) { return Math.floor(_options[d] * 100) + '%'; });\n\n container.selectAll('.display-option-reset')\n .classed('disabled', function(d) { return _options[d] === 1; });\n\n // first time only, set brightness if needed\n if (containerEnter.size() && _options.brightness !== 1) {\n context.background().brightness(_options.brightness);\n }\n }\n\n return section;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { marked } from 'marked';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiConfirm } from '../confirm';\nimport { utilNoAuto, utilRebind } from '../../util';\n\n\nexport function uiSettingsCustomBackground() {\n var dispatch = d3_dispatch('change');\n\n function render(selection) {\n // keep separate copies of original and current settings\n var _origSettings = {\n template: prefs('background-custom-template')\n };\n var _currSettings = {\n template: prefs('background-custom-template')\n };\n\n var example = 'https://tile.openstreetmap.org/{zoom}/{x}/{y}.png';\n var modal = uiConfirm(selection).okButton();\n\n modal\n .classed('settings-modal settings-custom-background', true);\n\n modal.select('.modal-section.header')\n .append('h3')\n .call(t.append('settings.custom_background.header'));\n\n\n var textSection = modal.select('.modal-section.message-text');\n\n var instructions =\n `${t.html('settings.custom_background.instructions.info')}\\n` +\n '\\n' +\n `#### ${t.html('settings.custom_background.instructions.wms.tokens_label')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.proj')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.wkid')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.dimensions')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.bbox')}\\n` +\n '\\n' +\n `#### ${t.html('settings.custom_background.instructions.tms.tokens_label')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.xyz')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.flipped_y')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.switch')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.quadtile')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.scale_factor')}\\n` +\n '\\n' +\n `#### ${t.html('settings.custom_background.instructions.example')}\\n` +\n `\\`${example}\\``;\n\n textSection\n .append('div')\n .attr('class', 'instructions-template')\n .html(marked(instructions));\n\n textSection\n .append('textarea')\n .attr('class', 'field-template')\n .attr('placeholder', t('settings.custom_background.template.placeholder'))\n .call(utilNoAuto)\n .property('value', _currSettings.template);\n\n\n // insert a cancel button\n var buttonSection = modal.select('.modal-section.buttons');\n\n buttonSection\n .insert('button', '.ok-button')\n .attr('class', 'button cancel-button secondary-action')\n .call(t.append('confirm.cancel'));\n\n\n buttonSection.select('.cancel-button')\n .on('click.cancel', clickCancel);\n\n buttonSection.select('.ok-button')\n .attr('disabled', isSaveDisabled)\n .on('click.save', clickSave);\n\n\n function isSaveDisabled() {\n return null;\n }\n\n\n // restore the original template\n function clickCancel() {\n textSection.select('.field-template').property('value', _origSettings.template);\n prefs('background-custom-template', _origSettings.template);\n this.blur();\n modal.close();\n }\n\n // accept the current template\n function clickSave() {\n _currSettings.template = textSection.select('.field-template').property('value');\n prefs('background-custom-template', _currSettings.template);\n this.blur();\n modal.close();\n dispatch.call('change', this, _currSettings);\n }\n }\n\n return utilRebind(render, dispatch, 'on');\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport { descending as d3_descending, ascending as d3_ascending } from 'd3-array';\nimport { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t, localizer } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCmd } from '../cmd';\nimport { uiSettingsCustomBackground } from '../settings/custom_background';\nimport { uiMapInMap } from '../map_in_map';\nimport { uiSection } from '../section';\n\nexport function uiSectionBackgroundList(context) {\n\n var _backgroundList = d3_select(null);\n\n var _settingsCustomBackground = uiSettingsCustomBackground(context)\n .on('change', customChanged);\n\n var section = uiSection('background-list', context)\n .label(() => t.append('background.backgrounds'))\n .disclosureContent(renderDisclosureContent);\n\n function previousBackgroundID() {\n return prefs('background-last-used-toggle');\n }\n\n function renderDisclosureContent(selection) {\n\n // the background list\n var container = selection.selectAll('.layer-background-list')\n .data([0]);\n\n _backgroundList = container.enter()\n .append('ul')\n .attr('class', 'layer-list layer-background-list')\n .attr('dir', 'auto')\n .merge(container);\n\n\n // add minimap toggle below list\n var bgExtrasListEnter = selection.selectAll('.bg-extras-list')\n .data([0])\n .enter()\n .append('ul')\n .attr('class', 'layer-list bg-extras-list');\n\n var minimapLabelEnter = bgExtrasListEnter\n .append('li')\n .attr('class', 'minimap-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('background.minimap.tooltip'))\n .keys([t('background.minimap.key')])\n .placement('top')\n );\n\n minimapLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n uiMapInMap.toggle();\n });\n\n minimapLabelEnter\n .append('span')\n .call(t.append('background.minimap.description'));\n\n\n var panelLabelEnter = bgExtrasListEnter\n .append('li')\n .attr('class', 'background-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('background.panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.background.key'))])\n .placement('top')\n );\n\n panelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('background');\n });\n\n panelLabelEnter\n .append('span')\n .call(t.append('background.panel.description'));\n\n var locPanelLabelEnter = bgExtrasListEnter\n .append('li')\n .attr('class', 'location-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('background.location_panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.location.key'))])\n .placement('top')\n );\n\n locPanelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('location');\n });\n\n locPanelLabelEnter\n .append('span')\n .call(t.append('background.location_panel.description'));\n\n\n // \"Info / Report a Problem\" link\n selection.selectAll('.imagery-faq')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'imagery-faq')\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', 'https://github.com/openstreetmap/iD/blob/develop/FAQ.md#how-can-i-report-an-issue-with-background-imagery')\n .append('span')\n .call(t.append('background.imagery_problem_faq'));\n\n _backgroundList\n .call(drawListItems, 'radio', function(d3_event, d) {\n chooseBackground(d);\n }, function(d) {\n return !d.isHidden() && !d.overlay;\n });\n }\n\n function setTooltips(selection) {\n selection.each(function(d, i, nodes) {\n var item = d3_select(this).select('label');\n var span = item.select('span');\n var placement = (i < nodes.length / 2) ? 'bottom' : 'top';\n var hasDescription = d.hasDescription();\n var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth'));\n\n item.call(uiTooltip().destroyAny);\n\n if (d.id === previousBackgroundID()) {\n item.call(uiTooltip()\n .placement(placement)\n .title(() => t.append('background.switch'))\n .keys([uiCmd('\u2318' + t('background.key'))])\n );\n } else if (hasDescription || isOverflowing) {\n item.call(uiTooltip()\n .placement(placement)\n .title(() => hasDescription ? d.description() : d.label())\n );\n }\n });\n }\n\n function drawListItems(layerList, type, change, filter) {\n var sources = context.background()\n .sources(context.map().extent(), context.map().zoom(), true)\n .filter(filter)\n .sort(function(a, b) {\n return a.best() && !b.best() ? -1\n : b.best() && !a.best() ? 1\n : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0;\n });\n\n var layerLinks = layerList.selectAll('li')\n // We have to be a bit inefficient about reordering the list since\n // arrow key navigation of radio values likes to work in the order\n // they were added, not the display document order.\n .data(sources, function(d, i) { return d.id + '---' + i; });\n\n layerLinks.exit()\n .remove();\n\n var enter = layerLinks.enter()\n .append('li')\n .classed('layer-custom', function(d) { return d.id === 'custom'; })\n .classed('best', function(d) { return d.best(); });\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', 'background-layer')\n .attr('value', function(d) {\n return d.id;\n })\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) { d.label()(d3_select(this)); });\n\n enter.filter(function(d) { return d.id === 'custom'; })\n .append('button')\n .attr('class', 'layer-browse')\n .call(uiTooltip()\n .title(() => t.append('settings.custom_background.tooltip'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n editCustom();\n })\n .call(svgIcon('#iD-icon-more'));\n\n enter.filter(function(d) { return d.best(); })\n .append('div')\n .attr('class', 'best')\n .call(uiTooltip()\n .title(() => t.append('background.best_imagery'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .append('span')\n .text('\u2605');\n\n layerList\n .call(updateLayerSelections);\n }\n\n function updateLayerSelections(selection) {\n function active(d) {\n return context.background().showsLayer(d);\n }\n\n selection.selectAll('li')\n .classed('active', active)\n .classed('switch', function(d) { return d.id === previousBackgroundID(); })\n .call(setTooltips)\n .selectAll('input')\n .property('checked', active);\n }\n\n\n function chooseBackground(d) {\n if (d.id === 'custom' && !d.template()) {\n return editCustom();\n }\n\n var previousBackground = context.background().baseLayerSource();\n prefs('background-last-used-toggle', previousBackground.id);\n prefs('background-last-used', d.id);\n context.background().baseLayerSource(d);\n }\n\n\n function customChanged(d) {\n var background = context.background();\n var customSource = background.findSource('custom');\n if (!customSource) return;\n\n if (d && d.template) {\n customSource.template(d.template);\n chooseBackground(customSource);\n } else {\n customSource.template('');\n var noneSource = background.findSource('none');\n if (noneSource) {\n chooseBackground(noneSource);\n }\n }\n }\n\n\n function editCustom() {\n context.container()\n .call(_settingsCustomBackground);\n }\n\n\n context.background()\n .on('change.background_list', function() {\n _backgroundList.call(updateLayerSelections);\n });\n\n context.map()\n .on('move.background_list',\n debounce(function() {\n // layers in-view may have changed due to map move\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t, localizer } from '../../core/localizer';\nimport { geoMetersToOffset, geoOffsetToMeters } from '../../geo';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\n\n\nexport function uiSectionBackgroundOffset(context) {\n\n var section = uiSection('background-offset', context)\n .label(() => t.append('background.fix_misalignment'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n var _directions = [\n ['top', [0, -0.5]],\n ['left', [-0.5, 0]],\n ['right', [0.5, 0]],\n ['bottom', [0, 0.5]]\n ];\n\n\n function updateValue() {\n var meters = geoOffsetToMeters(context.background().offset());\n var x = +meters[0].toFixed(2);\n var y = +meters[1].toFixed(2);\n\n context.container().selectAll('.nudge-inner-rect')\n .select('input')\n .classed('error', false)\n .property('value', x + ', ' + y);\n\n context.container().selectAll('.nudge-reset')\n .classed('disabled', function() {\n return (x === 0 && y === 0);\n });\n }\n\n\n function resetOffset() {\n context.background().offset([0, 0]);\n updateValue();\n }\n\n\n function nudge(d) {\n context.background().nudge(d, context.map().zoom());\n updateValue();\n }\n\n\n function inputOffset() {\n var input = d3_select(this);\n var d = input.node().value;\n\n if (d === '') return resetOffset();\n\n d = d.replace(/;/g, ',').split(',').map(function(n) {\n // if n is NaN, it will always get mapped to false.\n return !isNaN(n) && n;\n });\n\n if (d.length !== 2 || !d[0] || !d[1]) {\n input.classed('error', true);\n return;\n }\n\n context.background().offset(geoMetersToOffset(d));\n updateValue();\n }\n\n\n function dragOffset(d3_event) {\n if (d3_event.button !== 0) return;\n\n var origin = [d3_event.clientX, d3_event.clientY];\n\n var pointerId = d3_event.pointerId || 'mouse';\n\n context.container()\n .append('div')\n .attr('class', 'nudge-surface');\n\n d3_select(window)\n .on(_pointerPrefix + 'move.drag-bg-offset', pointermove)\n .on(_pointerPrefix + 'up.drag-bg-offset', pointerup);\n\n if (_pointerPrefix === 'pointer') {\n d3_select(window)\n .on('pointercancel.drag-bg-offset', pointerup);\n }\n\n function pointermove(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n var latest = [d3_event.clientX, d3_event.clientY];\n var d = [\n -(origin[0] - latest[0]) / 4,\n -(origin[1] - latest[1]) / 4\n ];\n\n origin = latest;\n nudge(d);\n }\n\n function pointerup(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n if (d3_event.button !== 0) return;\n\n context.container().selectAll('.nudge-surface')\n .remove();\n\n d3_select(window)\n .on('.drag-bg-offset', null);\n }\n }\n\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.nudge-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'nudge-container');\n\n containerEnter\n .append('div')\n .attr('class', 'nudge-instructions')\n .call(t.append('background.offset'));\n\n var nudgeWrapEnter = containerEnter\n .append('div')\n .attr('class', 'nudge-controls-wrap');\n\n var nudgeEnter = nudgeWrapEnter\n .append('div')\n .attr('class', 'nudge-outer-rect')\n .on(_pointerPrefix + 'down', dragOffset);\n\n nudgeEnter\n .append('div')\n .attr('class', 'nudge-inner-rect')\n .append('input')\n .attr('type', 'text')\n .attr('aria-label', t('background.offset_label'))\n .on('change', inputOffset);\n\n nudgeWrapEnter\n .append('div')\n .selectAll('button')\n .data(_directions).enter()\n .append('button')\n .attr('title', function(d) { return t(`background.nudge.${d[0]}`); })\n .attr('class', function(d) { return d[0] + ' nudge'; })\n .on('click', function(d3_event, d) {\n nudge(d[1]);\n });\n\n nudgeWrapEnter\n .append('button')\n .attr('title', t('background.reset'))\n .attr('class', 'nudge-reset disabled')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n resetOffset();\n })\n .call(svgIcon('#iD-icon-' + (localizer.textDirection() === 'rtl' ? 'redo' : 'undo')));\n\n updateValue();\n }\n\n context.background()\n .on('change.backgroundOffset-update', updateValue);\n\n return section;\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport { descending as d3_descending, ascending as d3_ascending } from 'd3-array';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionOverlayList(context) {\n\n var section = uiSection('overlay-list', context)\n .label(() => t.append('background.overlays'))\n .disclosureContent(renderDisclosureContent);\n\n var _overlayList = d3_select(null);\n\n function setTooltips(selection) {\n selection.each(function(d, i, nodes) {\n var item = d3_select(this).select('label');\n var span = item.select('span');\n var placement = (i < nodes.length / 2) ? 'bottom' : 'top';\n var description = d.description();\n var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth'));\n\n item.call(uiTooltip().destroyAny);\n\n if (description || isOverflowing) {\n item.call(uiTooltip()\n .placement(placement)\n .title(() => description || d.name())\n );\n }\n });\n }\n\n function updateLayerSelections(selection) {\n function active(d) {\n return context.background().showsLayer(d);\n }\n\n selection.selectAll('li')\n .classed('active', active)\n .call(setTooltips)\n .selectAll('input')\n .property('checked', active);\n }\n\n\n function chooseOverlay(d3_event, d) {\n d3_event.preventDefault();\n context.background().toggleOverlayLayer(d);\n _overlayList.call(updateLayerSelections);\n document.activeElement.blur();\n }\n\n function drawListItems(layerList, type, change, filter) {\n var sources = context.background()\n .sources(context.map().extent(), context.map().zoom(), true)\n .filter(filter);\n\n var layerLinks = layerList.selectAll('li')\n .data(sources, function(d) { return d.name(); });\n\n layerLinks.exit()\n .remove();\n\n var enter = layerLinks.enter()\n .append('li');\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', 'layers')\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) { d.label()(d3_select(this)); });\n\n\n layerList.selectAll('li')\n .sort(sortSources);\n\n layerList\n .call(updateLayerSelections);\n\n\n function sortSources(a, b) {\n return a.best() && !b.best() ? -1\n : b.best() && !a.best() ? 1\n : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0;\n }\n }\n\n function renderDisclosureContent(selection) {\n\n var container = selection.selectAll('.layer-overlay-list')\n .data([0]);\n\n _overlayList = container.enter()\n .append('ul')\n .attr('class', 'layer-list layer-overlay-list')\n .attr('dir', 'auto')\n .merge(container);\n\n _overlayList\n .call(drawListItems, 'checkbox', chooseOverlay, function(d) { return !d.isHidden() && d.overlay; });\n }\n\n context.map()\n .on('move.overlay_list',\n debounce(function() {\n // layers in-view may have changed due to map move\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\n\nimport { uiSectionBackgroundDisplayOptions } from '../sections/background_display_options';\nimport { uiSectionBackgroundList } from '../sections/background_list';\nimport { uiSectionBackgroundOffset } from '../sections/background_offset';\nimport { uiSectionOverlayList } from '../sections/overlay_list';\n\nexport function uiPaneBackground(context) {\n\n var backgroundPane = uiPane('background', context)\n .key(t('background.key'))\n .label(t.append('background.title'))\n .description(t.append('background.description'))\n .iconName('iD-icon-layers')\n .sections([\n uiSectionBackgroundList(context),\n uiSectionOverlayList(context),\n uiSectionBackgroundDisplayOptions(context),\n uiSectionBackgroundOffset(context)\n ]);\n\n return backgroundPane;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { marked } from 'marked';\n\nimport { svgIcon } from '../../svg/icon';\nimport { uiIntro } from '../intro/intro';\nimport { uiPane } from '../pane';\n\nimport { t, localizer } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { helpHtml } from '../intro/helper';\n\nexport function uiPaneHelp(context) {\n\n var docKeys = [\n ['help', [\n 'welcome',\n 'open_data_h',\n 'open_data',\n 'before_start_h',\n 'before_start',\n 'open_source_h',\n 'open_source',\n 'open_source_attribution',\n 'open_source_help'\n ]],\n ['overview', [\n 'navigation_h',\n 'navigation_drag',\n 'navigation_zoom',\n 'features_h',\n 'features',\n 'nodes_ways'\n ]],\n ['editing', [\n 'select_h',\n 'select_left_click',\n 'select_right_click',\n 'select_space',\n 'multiselect_h',\n 'multiselect',\n 'multiselect_shift_click',\n 'multiselect_lasso',\n 'undo_redo_h',\n 'undo_redo',\n 'save_h',\n 'save',\n 'save_validation',\n 'upload_h',\n 'upload',\n 'backups_h',\n 'backups',\n 'keyboard_h',\n 'keyboard'\n ]],\n ['feature_editor', [\n 'intro',\n 'definitions',\n 'type_h',\n 'type',\n 'type_picker',\n 'fields_h',\n 'fields_all_fields',\n 'fields_example',\n 'fields_add_field',\n 'tags_h',\n 'tags_all_tags',\n 'tags_resources'\n ]],\n ['points', [\n 'intro',\n 'add_point_h',\n 'add_point',\n 'add_point_finish',\n 'move_point_h',\n 'move_point',\n 'delete_point_h',\n 'delete_point',\n 'delete_point_command'\n ]],\n ['lines', [\n 'intro',\n 'add_line_h',\n 'add_line',\n 'add_line_draw',\n 'add_line_continue',\n 'add_line_finish',\n 'modify_line_h',\n 'modify_line_dragnode',\n 'modify_line_addnode',\n 'connect_line_h',\n 'connect_line',\n 'connect_line_display',\n 'connect_line_drag',\n 'connect_line_tag',\n 'disconnect_line_h',\n 'disconnect_line_command',\n 'move_line_h',\n 'move_line_command',\n 'move_line_connected',\n 'delete_line_h',\n 'delete_line',\n 'delete_line_command'\n ]],\n ['areas', [\n 'intro',\n 'point_or_area_h',\n 'point_or_area',\n 'add_area_h',\n 'add_area_command',\n 'add_area_draw',\n 'add_area_continue',\n 'add_area_finish',\n 'square_area_h',\n 'square_area_command',\n 'modify_area_h',\n 'modify_area_dragnode',\n 'modify_area_addnode',\n 'delete_area_h',\n 'delete_area',\n 'delete_area_command'\n ]],\n ['relations', [\n 'intro',\n 'edit_relation_h',\n 'edit_relation',\n 'edit_relation_add',\n 'edit_relation_delete',\n 'maintain_relation_h',\n 'maintain_relation',\n 'relation_types_h',\n 'multipolygon_h',\n 'multipolygon',\n 'multipolygon_create',\n 'multipolygon_merge',\n 'turn_restriction_h',\n 'turn_restriction',\n 'turn_restriction_field',\n 'turn_restriction_editing',\n 'route_h',\n 'route',\n 'route_add',\n 'boundary_h',\n 'boundary',\n 'boundary_add'\n ]],\n ['operations', [\n 'intro',\n 'intro_2',\n 'straighten',\n 'orthogonalize',\n 'circularize',\n 'move',\n 'rotate',\n 'reflect',\n 'continue',\n 'reverse',\n 'disconnect',\n 'split',\n 'extract',\n 'merge',\n 'delete',\n 'downgrade',\n 'copy_paste'\n ]],\n ['notes', [\n 'intro',\n 'add_note_h',\n 'add_note',\n 'place_note',\n 'move_note',\n 'update_note_h',\n 'update_note',\n 'save_note_h',\n 'save_note'\n ]],\n ['imagery', [\n 'intro',\n 'sources_h',\n 'choosing',\n 'sources',\n 'offsets_h',\n 'offset',\n 'offset_change'\n ]],\n ['streetlevel', [\n 'intro',\n 'using_h',\n 'using',\n 'photos',\n 'viewer'\n ]],\n ['gps', [\n 'intro',\n 'survey',\n 'using_h',\n 'using',\n 'tracing',\n 'upload'\n ]],\n ['qa', [\n 'intro',\n 'tools_h',\n 'tools',\n 'issues_h',\n 'issues'\n ]]\n ];\n\n var headings = {\n 'help.areas.add_area_h': 3,\n 'help.areas.delete_area_h': 3,\n 'help.areas.modify_area_h': 3,\n 'help.areas.point_or_area_h': 3,\n 'help.areas.square_area_h': 3,\n 'help.editing.backups_h': 3,\n 'help.editing.keyboard_h': 3,\n 'help.editing.multiselect_h': 3,\n 'help.editing.save_h': 3,\n 'help.editing.select_h': 3,\n 'help.editing.undo_redo_h': 3,\n 'help.editing.upload_h': 3,\n 'help.feature_editor.fields_h': 3,\n 'help.feature_editor.tags_h': 3,\n 'help.feature_editor.type_h': 3,\n 'help.gps.using_h': 3,\n 'help.help.before_start_h': 3,\n 'help.help.open_data_h': 3,\n 'help.help.open_source_h': 3,\n 'help.imagery.offsets_h': 3,\n 'help.imagery.sources_h': 3,\n 'help.lines.add_line_h': 3,\n 'help.lines.connect_line_h': 3,\n 'help.lines.delete_line_h': 3,\n 'help.lines.disconnect_line_h': 3,\n 'help.lines.modify_line_h': 3,\n 'help.lines.move_line_h': 3,\n 'help.notes.add_note_h': 3,\n 'help.notes.save_note_h': 3,\n 'help.notes.update_note_h': 3,\n 'help.overview.features_h': 3,\n 'help.overview.navigation_h': 3,\n 'help.points.add_point_h': 3,\n 'help.points.delete_point_h': 3,\n 'help.points.move_point_h': 3,\n 'help.qa.issues_h': 3,\n 'help.qa.tools_h': 3,\n 'help.relations.boundary_h': 3,\n 'help.relations.edit_relation_h': 3,\n 'help.relations.maintain_relation_h': 3,\n 'help.relations.multipolygon_h': 3,\n 'help.relations.relation_types_h': 2,\n 'help.relations.route_h': 3,\n 'help.relations.turn_restriction_h': 3,\n 'help.streetlevel.using_h': 3\n };\n\n // For each section, squash all the texts into a single markdown document\n var docs = docKeys.map(function(key) {\n var helpkey = 'help.' + key[0];\n var helpPaneReplacements = { version: context.version };\n var text = key[1].reduce(function(all, part) {\n var subkey = helpkey + '.' + part;\n var depth = headings[subkey]; // is this subkey a heading?\n var hhh = depth ? Array(depth + 1).join('#') + ' ' : ''; // if so, prepend with some ##'s\n return all + hhh + helpHtml(subkey, helpPaneReplacements) + '\\n\\n';\n }, '');\n\n return {\n title: t.addOrUpdate(helpkey + '.title'),\n _title: t(helpkey + '.title'),\n content: marked(text.trim())\n // use keyboard key styling for shortcuts\n .replace(//g, '')\n .replace(/<\\/code>/g, '<\\/kbd>')\n };\n });\n\n var helpPane = uiPane('help', context)\n .key(t('help.key'))\n .label(t.append('help.title'))\n .description(t.append('help.title'))\n .iconName('iD-icon-help');\n\n helpPane.renderContent = function(content) {\n\n function clickHelp(d, i) {\n\n var rtl = (localizer.textDirection() === 'rtl');\n content.property('scrollTop', 0);\n helpPane.selection().select('.pane-heading h2').call(d.title);\n\n body.html(d.content);\n body.selectAll('a')\n .attr('target', '_blank');\n menuItems.classed('selected', function(m) {\n return m._title === d._title;\n });\n\n nav.text('');\n if (rtl) {\n nav.call(drawNext).call(drawPrevious);\n } else {\n nav.call(drawPrevious).call(drawNext);\n }\n\n\n function drawNext(selection) {\n if (i < docs.length - 1) {\n var nextLink = selection\n .append('a')\n .attr('href', '#')\n .attr('class', 'next')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n clickHelp(docs[i + 1], i + 1);\n });\n\n nextLink\n .append('span')\n .call(docs[i + 1].title)\n .call(svgIcon((rtl ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'));\n }\n }\n\n\n function drawPrevious(selection) {\n if (i > 0) {\n var prevLink = selection\n .append('a')\n .attr('href', '#')\n .attr('class', 'previous')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n clickHelp(docs[i - 1], i - 1);\n });\n\n prevLink\n .call(svgIcon((rtl ? '#iD-icon-forward' : '#iD-icon-backward'), 'inline'))\n .append('span')\n .call(docs[i - 1].title);\n }\n }\n }\n\n\n function clickWalkthrough(d3_event) {\n d3_event.preventDefault();\n if (context.inIntro()) return;\n context.container().call(uiIntro(context));\n context.ui().togglePanes();\n }\n\n\n function clickShortcuts(d3_event) {\n d3_event.preventDefault();\n context.container().call(context.ui().shortcuts, true);\n }\n\n var toc = content\n .append('ul')\n .attr('class', 'toc');\n\n var menuItems = toc.selectAll('li')\n .data(docs)\n .enter()\n .append('li')\n .append('a')\n .attr('role', 'button')\n .attr('href', '#')\n .each(function(d) {\n d3_select(this).call(d.title);\n })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n clickHelp(d, docs.indexOf(d));\n });\n\n var shortcuts = toc\n .append('li')\n .attr('class', 'shortcuts')\n .call(uiTooltip()\n .title(() => t.append('shortcuts.tooltip'))\n .keys(['?'])\n .placement('top')\n )\n .append('a')\n .attr('href', '#')\n .on('click', clickShortcuts);\n\n shortcuts\n .append('div')\n .call(t.append('shortcuts.title'));\n\n var walkthrough = toc\n .append('li')\n .attr('class', 'walkthrough')\n .append('a')\n .attr('href', '#')\n .on('click', clickWalkthrough);\n\n walkthrough\n .append('svg')\n .attr('class', 'logo logo-walkthrough')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n walkthrough\n .append('div')\n .call(t.append('splash.walkthrough'));\n\n\n var helpContent = content\n .append('div')\n .attr('class', 'left-content');\n\n var body = helpContent\n .append('div')\n .attr('class', 'body');\n\n var nav = helpContent\n .append('div')\n .attr('class', 'nav');\n\n clickHelp(docs[0], 0);\n };\n\n return helpPane;\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport {\n select as d3_select\n} from 'd3-selection';\n\n//import { actionNoop } from '../actions/noop';\nimport { geoSphericalDistance } from '../../geo';\nimport { svgIcon } from '../../svg/icon';\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { utilHighlightEntities } from '../../util';\nimport { uiSection } from '../section';\nimport { validationIssue } from '../../core/validation';\n\nexport function uiSectionValidationIssues(id, severity, context) {\n\n var _issues = [];\n\n var section = uiSection(id, context)\n .label(function() {\n if (!_issues) return '';\n var issueCountText = _issues.length > 1000 ? '1000+' : String(_issues.length);\n return t.append('inspector.title_count', { title: t.append('issues.' + severity + 's.list_title'), count: issueCountText });\n })\n .disclosureContent(renderDisclosureContent)\n .shouldDisplay(function() {\n return _issues && _issues.length;\n });\n\n function getOptions() {\n return {\n what: prefs('validate-what') || 'edited',\n where: prefs('validate-where') || 'all'\n };\n }\n\n // get and cache the issues to display, unordered\n function reloadIssues() {\n _issues = context.validator().getIssuesBySeverity(getOptions())[severity];\n }\n\n function renderDisclosureContent(selection) {\n\n var center = context.map().center();\n var graph = context.graph();\n\n // sort issues by distance away from the center of the map\n var issues = _issues.map(function withDistance(issue) {\n var extent = issue.extent(graph);\n var dist = extent ? geoSphericalDistance(center, extent.center()) : 0;\n return Object.assign(issue, { dist: dist });\n })\n .sort(function byDistance(a, b) {\n return a.dist - b.dist;\n });\n\n // cut off at 1000\n issues = issues.slice(0, 1000);\n\n //renderIgnoredIssuesReset(_warningsSelection);\n\n selection\n .call(drawIssuesList, issues);\n }\n\n function drawIssuesList(selection, issues) {\n var list = selection.selectAll('.issues-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'layer-list issues-list ' + severity + 's-list')\n .merge(list);\n\n\n var items = list.selectAll('li')\n .data(issues, function(d) { return d.key; });\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', function (d) { return 'issue severity-' + d.severity; });\n\n var labelsEnter = itemsEnter\n .append('button')\n .attr('class', 'issue-label')\n .on('click', function(d3_event, d) {\n context.validator().focusIssue(d);\n })\n .on('mouseover', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, true, context);\n })\n .on('mouseout', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, false, context);\n });\n\n var textEnter = labelsEnter\n .append('span')\n .attr('class', 'issue-text');\n\n textEnter\n .append('span')\n .attr('class', 'issue-icon')\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(validationIssue.ICONS[d.severity]));\n });\n\n textEnter\n .append('span')\n .attr('class', 'issue-message');\n\n // Update\n items = items\n .merge(itemsEnter)\n .order();\n\n items.selectAll('.issue-message')\n .text('')\n .each(function(d) {\n return d.message(context)(d3_select(this));\n });\n }\n\n context.validator().on('validated.uiSectionValidationIssues' + id, function() {\n window.requestIdleCallback(function() {\n reloadIssues();\n section.reRender();\n });\n });\n\n context.map().on('move.uiSectionValidationIssues' + id,\n debounce(function() {\n window.requestIdleCallback(function() {\n if (getOptions().where === 'visible') {\n // must refetch issues if they are viewport-dependent\n reloadIssues();\n }\n // always reload list to re-sort-by-distance\n section.reRender();\n });\n }, 1000)\n );\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiSection } from '../section';\n\nexport function uiSectionValidationOptions(context) {\n\n var section = uiSection('issues-options', context)\n .content(renderContent);\n\n function renderContent(selection) {\n\n var container = selection.selectAll('.issues-options-container')\n .data([0]);\n\n container = container.enter()\n .append('div')\n .attr('class', 'issues-options-container')\n .merge(container);\n\n var data = [\n { key: 'what', values: ['edited', 'all'] },\n { key: 'where', values: ['visible', 'all'] }\n ];\n\n var options = container.selectAll('.issues-option')\n .data(data, function(d) { return d.key; });\n\n var optionsEnter = options.enter()\n .append('div')\n .attr('class', function(d) { return 'issues-option issues-option-' + d.key; });\n\n optionsEnter\n .append('div')\n .attr('class', 'issues-option-title')\n .each(function(d) {\n d3_select(this).call(t.append('issues.options.' + d.key + '.title'));\n });\n\n var valuesEnter = optionsEnter.selectAll('label')\n .data(function(d) {\n return d.values.map(function(val) { return { value: val, key: d.key }; });\n })\n .enter()\n .append('label');\n\n valuesEnter\n .append('input')\n .attr('type', 'radio')\n .attr('name', function(d) { return 'issues-option-' + d.key; })\n .attr('value', function(d) { return d.value; })\n .property('checked', function(d) { return getOptions()[d.key] === d.value; })\n .on('change', function(d3_event, d) { updateOptionValue(d3_event, d.key, d.value); });\n\n valuesEnter\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append('issues.options.' + d.key + '.' + d.value));\n });\n }\n\n function getOptions() {\n return {\n what: prefs('validate-what') || 'edited', // 'all', 'edited'\n where: prefs('validate-where') || 'all' // 'all', 'visible'\n };\n }\n\n function updateOptionValue(d3_event, d, val) {\n if (!val && d3_event && d3_event.target) {\n val = d3_event.target.value;\n }\n\n prefs('validate-' + d, val);\n context.validator().validate();\n }\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { utilGetSetValue, utilNoAuto } from '../../util';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionValidationRules(context) {\n\n var MINSQUARE = 0;\n var MAXSQUARE = 20;\n var DEFAULTSQUARE = 5; // see also unsquare_way.js\n\n var section = uiSection('issues-rules', context)\n .disclosureContent(renderDisclosureContent)\n .label(() => t.append('issues.rules.title'));\n\n var _ruleKeys = context.validator().getRuleKeys()\n .filter(function(key) { return key !== 'maprules'; })\n .sort(function(key1, key2) {\n // alphabetize by localized title\n return t('issues.' + key1 + '.title') < t('issues.' + key2 + '.title') ? -1 : 1;\n });\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.issues-rulelist-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'issues-rulelist-container');\n\n containerEnter\n .append('ul')\n .attr('class', 'layer-list issue-rules-list');\n\n var ruleLinks = containerEnter\n .append('div')\n .attr('class', 'issue-rules-links section-footer');\n\n ruleLinks\n .append('a')\n .attr('class', 'issue-rules-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.disable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.validator().disableRules(_ruleKeys);\n });\n\n ruleLinks\n .append('a')\n .attr('class', 'issue-rules-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.enable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.validator().disableRules([]);\n });\n\n\n // Update\n container = container\n .merge(containerEnter);\n\n container.selectAll('.issue-rules-list')\n .call(drawListItems, _ruleKeys, 'checkbox', 'rule', toggleRule, isRuleEnabled);\n }\n\n function drawListItems(selection, data, type, name, change, active) {\n var items = selection.selectAll('li')\n .data(data);\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li');\n\n if (name === 'rule') {\n enter\n .call(uiTooltip()\n .title(function(d) { return t.append('issues.' + d + '.tip'); })\n .placement('top')\n );\n }\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', name)\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) {\n var params = {};\n if (d === 'unsquare_way') {\n params.val = selection => selection\n .append('span')\n .classed('square-degrees', true);\n }\n d3_select(this).call(t.append('issues.' + d + '.title', params));\n });\n\n // Update\n items = items\n .merge(enter);\n\n items\n .classed('active', active)\n .selectAll('input')\n .property('checked', active)\n .property('indeterminate', false);\n\n\n // user-configurable square threshold\n var degStr = prefs('validate-square-degrees');\n if (degStr === null) {\n degStr = DEFAULTSQUARE.toString();\n }\n\n var span = items.selectAll('.square-degrees');\n var input = span.selectAll('.square-degrees-input')\n .data([0]);\n\n // enter / update\n input.enter()\n .append('input')\n .attr('type', 'number')\n .attr('min', MINSQUARE.toString())\n .attr('max', MAXSQUARE.toString())\n .attr('step', '0.5')\n .attr('class', 'square-degrees-input')\n .call(utilNoAuto)\n .on('click', function (d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n this.select();\n })\n .on('keyup', function (d3_event) {\n if (d3_event.keyCode === 13) { // \u21A9 Return\n this.blur();\n this.select();\n }\n })\n .on('blur', changeSquare)\n .merge(input)\n .property('value', degStr);\n }\n\n function changeSquare() {\n var input = d3_select(this);\n var degStr = utilGetSetValue(input).trim();\n var degNum = Number(degStr);\n\n if (!isFinite(degNum)) {\n degNum = DEFAULTSQUARE;\n } else if (degNum > MAXSQUARE) {\n degNum = MAXSQUARE;\n } else if (degNum < MINSQUARE) {\n degNum = MINSQUARE;\n }\n\n degNum = Math.round(degNum * 10 ) / 10; // round to 1 decimal\n degStr = degNum.toString();\n\n input\n .property('value', degStr);\n\n prefs('validate-square-degrees', degStr);\n context.validator().revalidateUnsquare();\n }\n\n function isRuleEnabled(d) {\n return context.validator().isRuleEnabled(d);\n }\n\n function toggleRule(d3_event, d) {\n context.validator().toggleRule(d);\n }\n\n context.validator().on('validated.uiSectionValidationRules', function() {\n window.requestIdleCallback(section.reRender);\n });\n\n return section;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { svgIcon } from '../../svg/icon';\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiSection } from '../section';\n\nexport function uiSectionValidationStatus(context) {\n\n var section = uiSection('issues-status', context)\n .content(renderContent)\n .shouldDisplay(function() {\n var issues = context.validator().getIssues(getOptions());\n return issues.length === 0;\n });\n\n function getOptions() {\n return {\n what: prefs('validate-what') || 'edited',\n where: prefs('validate-where') || 'all'\n };\n }\n\n function renderContent(selection) {\n\n var box = selection.selectAll('.box')\n .data([0]);\n\n var boxEnter = box.enter()\n .append('div')\n .attr('class', 'box');\n\n boxEnter\n .append('div')\n .call(svgIcon('#iD-icon-apply', 'pre-text'));\n\n var noIssuesMessage = boxEnter\n .append('span');\n\n noIssuesMessage\n .append('strong')\n .attr('class', 'message');\n\n noIssuesMessage\n .append('br');\n\n noIssuesMessage\n .append('span')\n .attr('class', 'details');\n\n renderIgnoredIssuesReset(selection);\n setNoIssuesText(selection);\n }\n\n function renderIgnoredIssuesReset(selection) {\n\n var ignoredIssues = context.validator()\n .getIssues({ what: 'all', where: 'all', includeDisabledRules: true, includeIgnored: 'only' });\n\n var resetIgnored = selection.selectAll('.reset-ignored')\n .data(ignoredIssues.length ? [0] : []);\n\n // exit\n resetIgnored.exit()\n .remove();\n\n // enter\n var resetIgnoredEnter = resetIgnored.enter()\n .append('div')\n .attr('class', 'reset-ignored section-footer');\n\n resetIgnoredEnter\n .append('a')\n .attr('href', '#');\n\n // update\n resetIgnored = resetIgnored\n .merge(resetIgnoredEnter);\n\n resetIgnored.select('a')\n .call(t.addOrUpdate('inspector.title_count', { title: t.append('issues.reset_ignored'), count: ignoredIssues.length }));\n\n resetIgnored.on('click', function(d3_event) {\n d3_event.preventDefault();\n context.validator().resetIgnoredIssues();\n });\n }\n\n function setNoIssuesText(selection) {\n\n var opts = getOptions();\n\n function checkForHiddenIssues(cases) {\n for (var type in cases) {\n var hiddenOpts = cases[type];\n var hiddenIssues = context.validator().getIssues(hiddenOpts);\n if (hiddenIssues.length) {\n selection.select('.box .details')\n .html('')\n .call(t.append(\n 'issues.no_issues.hidden_issues.' + type,\n { count: hiddenIssues.length.toString() }\n ));\n return;\n }\n }\n selection.select('.box .details')\n .html('')\n .call(t.append('issues.no_issues.hidden_issues.none'));\n }\n\n var messageType;\n\n if (opts.what === 'edited' && opts.where === 'visible') {\n\n messageType = 'edits_in_view';\n\n checkForHiddenIssues({\n elsewhere: { what: 'edited', where: 'all' },\n everything_else: { what: 'all', where: 'visible' },\n disabled_rules: { what: 'edited', where: 'visible', includeDisabledRules: 'only' },\n everything_else_elsewhere: { what: 'all', where: 'all' },\n disabled_rules_elsewhere: { what: 'edited', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'edited', where: 'visible', includeIgnored: 'only' },\n ignored_issues_elsewhere: { what: 'edited', where: 'all', includeIgnored: 'only' }\n });\n\n } else if (opts.what === 'edited' && opts.where === 'all') {\n\n messageType = 'edits';\n\n checkForHiddenIssues({\n everything_else: { what: 'all', where: 'all' },\n disabled_rules: { what: 'edited', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'edited', where: 'all', includeIgnored: 'only' }\n });\n\n } else if (opts.what === 'all' && opts.where === 'visible') {\n\n messageType = 'everything_in_view';\n\n checkForHiddenIssues({\n elsewhere: { what: 'all', where: 'all' },\n disabled_rules: { what: 'all', where: 'visible', includeDisabledRules: 'only' },\n disabled_rules_elsewhere: { what: 'all', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'all', where: 'visible', includeIgnored: 'only' },\n ignored_issues_elsewhere: { what: 'all', where: 'all', includeIgnored: 'only' }\n });\n } else if (opts.what === 'all' && opts.where === 'all') {\n\n messageType = 'everything';\n\n checkForHiddenIssues({\n disabled_rules: { what: 'all', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'all', where: 'all', includeIgnored: 'only' }\n });\n }\n\n if (opts.what === 'edited' && context.history().difference().summary().length === 0) {\n messageType = 'no_edits';\n }\n\n selection.select('.box .message')\n .html('')\n .call(t.append('issues.no_issues.message.' + messageType));\n\n }\n\n context.validator().on('validated.uiSectionValidationStatus', function() {\n window.requestIdleCallback(section.reRender);\n });\n\n context.map().on('move.uiSectionValidationStatus',\n debounce(function() {\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\n\nimport { uiSectionValidationIssues } from '../sections/validation_issues';\nimport { uiSectionValidationOptions } from '../sections/validation_options';\nimport { uiSectionValidationRules } from '../sections/validation_rules';\nimport { uiSectionValidationStatus } from '../sections/validation_status';\n\nexport function uiPaneIssues(context) {\n\n var issuesPane = uiPane('issues', context)\n .key(t('issues.key'))\n .label(t.append('issues.title'))\n .description(t.append('issues.title'))\n .iconName('iD-icon-alert')\n .sections([\n uiSectionValidationOptions(context),\n uiSectionValidationStatus(context),\n uiSectionValidationIssues('issues-errors', 'error', context),\n uiSectionValidationIssues('issues-warnings', 'warning', context),\n uiSectionValidationIssues('issues-suggestions', 'suggestion', context),\n uiSectionValidationRules(context)\n ]);\n\n return issuesPane;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiConfirm } from '../confirm';\nimport { utilNoAuto, utilRebind } from '../../util';\n\n\nexport function uiSettingsCustomData(context) {\n var dispatch = d3_dispatch('change');\n\n function render(selection) {\n var dataLayer = context.layers().layer('data');\n\n // keep separate copies of original and current settings\n var _origSettings = {\n fileList: (dataLayer && dataLayer.fileList()) || null,\n url: prefs('settings-custom-data-url')\n };\n var _currSettings = {\n fileList: (dataLayer && dataLayer.fileList()) || null,\n // url: prefs('settings-custom-data-url')\n };\n\n // var example = 'https://tile.openstreetmap.org/{zoom}/{x}/{y}.png';\n var modal = uiConfirm(selection).okButton();\n\n modal\n .classed('settings-modal settings-custom-data', true);\n\n modal.select('.modal-section.header')\n .append('h3')\n .call(t.append('settings.custom_data.header'));\n\n\n var textSection = modal.select('.modal-section.message-text');\n\n textSection\n .append('pre')\n .attr('class', 'instructions-file')\n .call(t.append('settings.custom_data.file.instructions'));\n\n textSection\n .append('input')\n .attr('class', 'field-file')\n .attr('type', 'file')\n .attr('accept', '.gpx,.kml,.geojson,.json,application/gpx+xml,application/vnd.google-earth.kml+xml,application/geo+json,application/json')\n .property('files', _currSettings.fileList)\n .on('change', function(d3_event) {\n var files = d3_event.target.files;\n if (files && files.length) {\n _currSettings.url = '';\n textSection.select('.field-url').property('value', '');\n _currSettings.fileList = files;\n } else {\n _currSettings.fileList = null;\n }\n });\n\n textSection\n .append('h4')\n .call(t.append('settings.custom_data.or'));\n\n textSection\n .append('pre')\n .attr('class', 'instructions-url')\n .call(t.append('settings.custom_data.url.instructions'));\n\n textSection\n .append('textarea')\n .attr('class', 'field-url')\n .attr('placeholder', t('settings.custom_data.url.placeholder'))\n .call(utilNoAuto)\n .property('value', _currSettings.url);\n\n\n // insert a cancel button\n var buttonSection = modal.select('.modal-section.buttons');\n\n buttonSection\n .insert('button', '.ok-button')\n .attr('class', 'button cancel-button secondary-action')\n .call(t.append('confirm.cancel'));\n\n\n buttonSection.select('.cancel-button')\n .on('click.cancel', clickCancel);\n\n buttonSection.select('.ok-button')\n .attr('disabled', isSaveDisabled)\n .on('click.save', clickSave);\n\n\n function isSaveDisabled() {\n return null;\n }\n\n\n // restore the original url\n function clickCancel() {\n textSection.select('.field-url').property('value', _origSettings.url);\n prefs('settings-custom-data-url', _origSettings.url);\n this.blur();\n modal.close();\n }\n\n // accept the current url\n function clickSave() {\n _currSettings.url = textSection.select('.field-url').property('value').trim();\n\n // one or the other but not both\n if (_currSettings.url) { _currSettings.fileList = null; }\n if (_currSettings.fileList) { _currSettings.url = ''; }\n\n prefs('settings-custom-data-url', _currSettings.url);\n this.blur();\n modal.close();\n dispatch.call('change', this, _currSettings);\n }\n }\n\n return utilRebind(render, dispatch, 'on');\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t, localizer } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg/icon';\nimport { geoExtent } from '../../geo';\nimport { modeBrowse } from '../../modes/browse';\nimport { uiCmd } from '../cmd';\nimport { uiSection } from '../section';\nimport { uiSettingsCustomData } from '../settings/custom_data';\n\nexport function uiSectionDataLayers(context) {\n\n var settingsCustomData = uiSettingsCustomData(context)\n .on('change', customChanged);\n\n // refers to `modules/svg/layers.js` -> function drawLayers(selection) {...}\n var layers = context.layers();\n\n var section = uiSection('data-layers', context)\n .label(() => t.append('map_data.data_layers'))\n .disclosureContent(renderDisclosureContent);\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.data-layer-container')\n .data([0]);\n\n container.enter()\n .append('div')\n .attr('class', 'data-layer-container')\n .merge(container)\n .call(drawOsmItems)\n .call(drawQAItems)\n .call(drawCustomDataItems)\n .call(drawVectorItems) // Beta - Detroit mapping challenge\n .call(drawPanelItems);\n }\n\n function showsLayer(which) {\n var layer = layers.layer(which);\n if (layer) {\n return layer.enabled();\n }\n return false;\n }\n\n function setLayer(which, enabled) {\n // Don't allow layer changes while drawing - #6584\n var mode = context.mode();\n if (mode && /^draw/.test(mode.id)) return;\n\n var layer = layers.layer(which);\n if (layer) {\n layer.enabled(enabled);\n\n if (!enabled && (which === 'osm' || which === 'notes')) {\n context.enter(modeBrowse(context));\n }\n }\n }\n\n function toggleLayer(which) {\n setLayer(which, !showsLayer(which));\n }\n\n function drawOsmItems(selection) {\n var osmKeys = ['osm', 'notes'];\n var osmLayers = layers.all().filter(function(obj) { return osmKeys.indexOf(obj.id) !== -1; });\n\n var ul = selection\n .selectAll('.layer-list-osm')\n .data([0]);\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-osm')\n .merge(ul);\n\n var li = ul.selectAll('.list-item')\n .data(osmLayers);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) { return 'list-item list-item-' + d.id; });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n if (d.id === 'osm') {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.' + d.id + '.tooltip'))\n .keys([uiCmd('\u2325' + t('area_fill.wireframe.key'))])\n .placement('bottom')\n );\n } else {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.' + d.id + '.tooltip'))\n .placement('bottom')\n );\n }\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) { toggleLayer(d.id); });\n\n labelEnter\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append('map_data.layers.' + d.id + '.title'));\n });\n\n\n // Update\n li\n .merge(liEnter)\n .classed('active', function (d) { return d.layer.enabled(); })\n .selectAll('input')\n .property('checked', function (d) { return d.layer.enabled(); });\n }\n\n function drawQAItems(selection) {\n var qaKeys = ['osmose'];\n var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; });\n\n var ul = selection\n .selectAll('.layer-list-qa')\n .data([0]);\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-qa')\n .merge(ul);\n\n var li = ul.selectAll('.list-item')\n .data(qaLayers);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) { return 'list-item list-item-' + d.id; });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.' + d.id + '.tooltip'))\n .placement('bottom')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) { toggleLayer(d.id); });\n\n labelEnter\n .append('span')\n .each(function(d) { t.append('map_data.layers.' + d.id + '.title')(d3_select(this)); });\n\n\n // Update\n li\n .merge(liEnter)\n .classed('active', function (d) { return d.layer.enabled(); })\n .selectAll('input')\n .property('checked', function (d) { return d.layer.enabled(); });\n }\n\n // Beta feature - sample vector layers to support Detroit Mapping Challenge\n // https://github.com/osmus/detroit-mapping-challenge\n function drawVectorItems(selection) {\n var dataLayer = layers.layer('data');\n var vtData = [\n {\n name: 'Detroit Neighborhoods/Parks',\n src: 'neighborhoods-parks',\n tooltip: 'Neighborhood boundaries and parks as compiled by City of Detroit in concert with community groups.',\n template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmur6x34562qp9iv1u3ksf-54hev,jonahadkins.cjksmqxdx33jj2wp90xd9x2md-4e5y2/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'\n }, {\n name: 'Detroit Composite POIs',\n src: 'composite-poi',\n tooltip: 'Fire Inspections, Business Licenses, and other public location data collated from the City of Detroit.',\n template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmm6a02sli31myxhsr7zf3-2sw8h/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'\n }, {\n name: 'Detroit All-The-Places POIs',\n src: 'alltheplaces-poi',\n tooltip: 'Public domain business location data created by web scrapers.',\n template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmswgk340g2vo06p1w9w0j-8fjjc/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'\n }\n ];\n\n // Only show this if the map is around Detroit..\n var detroit = geoExtent([-83.5, 42.1], [-82.8, 42.5]);\n var showVectorItems = (context.map().zoom() > 9 && detroit.contains(context.map().center()));\n\n var container = selection.selectAll('.vectortile-container')\n .data(showVectorItems ? [0] : []);\n\n container.exit()\n .remove();\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'vectortile-container');\n\n containerEnter\n .append('h4')\n .attr('class', 'vectortile-header')\n .text('Detroit Vector Tiles (Beta)');\n\n containerEnter\n .append('ul')\n .attr('class', 'layer-list layer-list-vectortile');\n\n containerEnter\n .append('div')\n .attr('class', 'vectortile-footer')\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', 'https://github.com/osmus/detroit-mapping-challenge')\n .append('span')\n .text('About these layers');\n\n container = container\n .merge(containerEnter);\n\n\n var ul = container.selectAll('.layer-list-vectortile');\n\n var li = ul.selectAll('.list-item')\n .data(vtData);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) { return 'list-item list-item-' + d.src; });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n d3_select(this).call(\n uiTooltip().title(d.tooltip).placement('top')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'radio')\n .attr('name', 'vectortile')\n .on('change', selectVTLayer);\n\n labelEnter\n .append('span')\n .text(function(d) { return d.name; });\n\n // Update\n li\n .merge(liEnter)\n .classed('active', isVTLayerSelected)\n .selectAll('input')\n .property('checked', isVTLayerSelected);\n\n\n function isVTLayerSelected(d) {\n return dataLayer && dataLayer.template() === d.template;\n }\n\n function selectVTLayer(d3_event, d) {\n prefs('settings-custom-data-url', d.template);\n if (dataLayer) {\n dataLayer.template(d.template, d.src);\n dataLayer.enabled(true);\n }\n }\n }\n\n function drawCustomDataItems(selection) {\n var dataLayer = layers.layer('data');\n var hasData = dataLayer && dataLayer.hasData();\n var showsData = hasData && dataLayer.enabled();\n\n var ul = selection\n .selectAll('.layer-list-data')\n .data(dataLayer ? [0] : []);\n\n // Exit\n ul.exit()\n .remove();\n\n // Enter\n var ulEnter = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-data');\n\n var liEnter = ulEnter\n .append('li')\n .attr('class', 'list-item-data');\n\n var labelEnter = liEnter\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.custom.tooltip'))\n .placement('top')\n );\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function() { toggleLayer('data'); });\n\n labelEnter\n .append('span')\n .call(t.append('map_data.layers.custom.title'));\n\n liEnter\n .append('button')\n .attr('class', 'open-data-options')\n .call(uiTooltip()\n .title(() => t.append('settings.custom_data.tooltip'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n editCustom();\n })\n .call(svgIcon('#iD-icon-more'));\n\n liEnter\n .append('button')\n .attr('class', 'zoom-to-data')\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.custom.zoom'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n if (d3_select(this).classed('disabled')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n dataLayer.fitZoom();\n })\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'));\n\n // Update\n ul = ul\n .merge(ulEnter);\n\n ul.selectAll('.list-item-data')\n .classed('active', showsData)\n .selectAll('label')\n .classed('deemphasize', !hasData)\n .selectAll('input')\n .property('disabled', !hasData)\n .property('checked', showsData);\n\n ul.selectAll('button.zoom-to-data')\n .classed('disabled', !hasData);\n }\n\n function editCustom() {\n context.container()\n .call(settingsCustomData);\n }\n\n function customChanged(d) {\n var dataLayer = layers.layer('data');\n\n if (d && d.url) {\n dataLayer.url(d.url);\n } else if (d && d.fileList) {\n dataLayer.fileList(d.fileList);\n }\n }\n\n function drawPanelItems(selection) {\n\n var panelsListEnter = selection.selectAll('.md-extras-list')\n .data([0])\n .enter()\n .append('ul')\n .attr('class', 'layer-list md-extras-list');\n\n var historyPanelLabelEnter = panelsListEnter\n .append('li')\n .attr('class', 'history-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('map_data.history_panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.history.key'))])\n .placement('top')\n );\n\n historyPanelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('history');\n });\n\n historyPanelLabelEnter\n .append('span')\n .call(t.append('map_data.history_panel.title'));\n\n var measurementPanelLabelEnter = panelsListEnter\n .append('li')\n .attr('class', 'measurement-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('map_data.measurement_panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.measurement.key'))])\n .placement('top')\n );\n\n measurementPanelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('measurement');\n });\n\n measurementPanelLabelEnter\n .append('span')\n .call(t.append('map_data.measurement_panel.title'));\n }\n\n context.layers().on('change.uiSectionDataLayers', section.reRender);\n\n context.map()\n .on('move.uiSectionDataLayers',\n debounce(function() {\n // Detroit layers may have moved in or out of view\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionMapFeatures(context) {\n\n var _features = context.features().keys();\n\n var section = uiSection('map-features', context)\n .label(() => t.append('map_data.map_features'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n function renderDisclosureContent(selection) {\n\n var container = selection.selectAll('.layer-feature-list-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'layer-feature-list-container');\n\n containerEnter\n .append('ul')\n .attr('class', 'layer-list layer-feature-list');\n\n var footer = containerEnter\n .append('div')\n .attr('class', 'feature-list-links section-footer');\n\n footer\n .append('a')\n .attr('class', 'feature-list-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.disable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.features().disableAll();\n });\n\n footer\n .append('a')\n .attr('class', 'feature-list-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.enable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.features().enableAll();\n });\n\n // Update\n container = container\n .merge(containerEnter);\n\n container.selectAll('.layer-feature-list')\n .call(drawListItems, _features, 'checkbox', 'feature', clickFeature, showsFeature);\n }\n\n function drawListItems(selection, data, type, name, change, active) {\n var items = selection.selectAll('li')\n .data(data);\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li')\n .call(uiTooltip()\n .title(function(d) {\n var tip = t.append(name + '.' + d + '.tooltip');\n if (autoHiddenFeature(d)) {\n var msg = showsLayer('osm') ? t.append('map_data.autohidden') : t.append('map_data.osmhidden');\n return selection => {\n selection.call(tip);\n selection.append('div').call(msg);\n };\n }\n return tip;\n })\n .placement('top')\n );\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', name)\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append(name + '.' + d + '.description'));\n });\n\n // Update\n items = items\n .merge(enter);\n\n items\n .classed('active', active)\n .selectAll('input')\n .property('checked', active)\n .property('indeterminate', autoHiddenFeature);\n }\n\n function autoHiddenFeature(d) {\n return context.features().autoHidden(d);\n }\n\n function showsFeature(d) {\n return context.features().enabled(d);\n }\n\n function clickFeature(d3_event, d) {\n context.features().toggle(d);\n }\n\n function showsLayer(id) {\n var layer = context.layers().layer(id);\n return layer && layer.enabled();\n }\n\n // add listeners\n context.features()\n .on('change.map_features', section.reRender);\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionMapStyleOptions(context) {\n\n var section = uiSection('fill-area', context)\n .label(() => t.append('map_data.style_options'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.layer-fill-list')\n .data([0]);\n\n container.enter()\n .append('ul')\n .attr('class', 'layer-list layer-fill-list')\n .merge(container)\n .call(drawListItems, context.map().areaFillOptions, 'radio', 'area_fill', setFill, isActiveFill);\n\n var container2 = selection.selectAll('.layer-visual-diff-list')\n .data([0]);\n\n container2.enter()\n .append('ul')\n .attr('class', 'layer-list layer-visual-diff-list')\n .merge(container2)\n .call(drawListItems, ['highlight_edits'], 'checkbox', 'visual_diff', toggleHighlightEdited, function() {\n return context.surface().classed('highlight-edited');\n });\n }\n\n function drawListItems(selection, data, type, name, change, active) {\n var items = selection.selectAll('li')\n .data(data);\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li')\n .call(uiTooltip()\n .title(function(d) {\n return t.append(name + '.' + d + '.tooltip');\n })\n .keys(function(d) {\n var key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null);\n if (d === 'highlight_edits') key = t('map_data.highlight_edits.key');\n return key ? [key] : null;\n })\n .placement('top')\n );\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', name)\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append(name + '.' + d + '.description'));\n });\n\n // Update\n items = items\n .merge(enter);\n\n items\n .classed('active', active)\n .selectAll('input')\n .property('checked', active)\n .property('indeterminate', false);\n }\n\n function isActiveFill(d) {\n return context.map().activeAreaFill() === d;\n }\n\n function toggleHighlightEdited(d3_event) {\n d3_event.preventDefault();\n context.map().toggleHighlightEdited();\n }\n\n function setFill(d3_event, d) {\n context.map().activeAreaFill(d);\n }\n\n context.map()\n .on('changeHighlighting.ui_style, changeAreaFill.ui_style', section.reRender);\n\n return section;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { isArray, isNumber } from 'es-toolkit/compat';\n\nimport { t } from '../../core/localizer';\nimport { uiConfirm } from '../confirm';\nimport { utilRebind } from '../../util';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg';\n\n\nexport function uiSettingsLocalPhotos(context) {\n var dispatch = d3_dispatch('change');\n var photoLayer = context.layers().layer('local-photos');\n var modal;\n\n function render(selection) {\n\n modal = uiConfirm(selection).okButton();\n\n modal\n .classed('settings-modal settings-local-photos', true);\n\n modal.select('.modal-section.header')\n .append('h3')\n .call(t.append('local_photos.header'));\n\n modal.select('.modal-section.message-text')\n .append('div')\n .classed('local-photos', true);\n\n var instructionsSection = modal.select('.modal-section.message-text .local-photos')\n .append('div')\n .classed('instructions', true);\n\n instructionsSection\n .append('p')\n .classed('instructions-local-photos', true)\n .call(t.append('local_photos.file.instructions'));\n\n instructionsSection\n .append('input')\n .classed('field-file', true)\n .attr('type', 'file')\n .attr('multiple', 'multiple')\n .attr('accept', '.jpg,.jpeg,.png,image/png,image/jpeg')\n .style('visibility', 'hidden')\n .attr('id', 'local-photo-files')\n .on('change', function(d3_event) {\n var files = d3_event.target.files;\n if (files && files.length) {\n photoList\n .select('ul')\n .append('li')\n .classed('placeholder', true)\n .append('div');\n dispatch.call('change', this, files);\n }\n d3_event.target.value = null;\n });\n instructionsSection\n .append('label')\n .attr('for', 'local-photo-files')\n .classed('button', true)\n .call(t.append('local_photos.file.label'));\n\n const photoList = modal.select('.modal-section.message-text .local-photos')\n .append('div')\n .append('div')\n .classed('list-local-photos', true);\n\n photoList\n .append('ul');\n\n updatePhotoList(photoList.select('ul'));\n\n context.layers().on('change', () => updatePhotoList(photoList.select('ul')));\n }\n\n function updatePhotoList(container) {\n function locationUnavailable(d) {\n return !(isArray(d.loc) && isNumber(d.loc[0]) && isNumber(d.loc[1]));\n }\n\n container.selectAll('li.placeholder').remove();\n\n let selection = container.selectAll('li')\n .data(photoLayer.getPhotos() ?? [], d => d.id);\n selection.exit()\n .remove();\n\n const selectionEnter = selection.enter()\n .append('li');\n\n selectionEnter\n .append('span')\n .classed('filename', true);\n selectionEnter\n .append('button')\n .classed('form-field-button zoom-to-data', true)\n .attr('title', t('local_photos.zoom_single'))\n .call(svgIcon('#iD-icon-framed-dot'));\n selectionEnter\n .append('button')\n .classed('form-field-button no-geolocation', true)\n .call(svgIcon('#iD-icon-alert'))\n .call(uiTooltip()\n .title(() => t.append('local_photos.no_geolocation.tooltip'))\n .placement('left')\n );\n selectionEnter\n .append('button')\n .classed('form-field-button remove', true)\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'));\n\n selection = selection.merge(selectionEnter);\n\n selection\n .classed('invalid', locationUnavailable);\n selection.select('span.filename')\n .text(d => d.name)\n .attr('title', d => d.name);\n selection.select('span.filename')\n .on('click', (d3_event, d) => {\n photoLayer.openPhoto(d3_event, d, false);\n });\n selection.select('button.zoom-to-data')\n .on('click', (d3_event, d) => {\n photoLayer.openPhoto(d3_event, d, true);\n });\n selection.select('button.remove')\n .on('click', (d3_event, d) => {\n photoLayer.removePhoto(d.id);\n updatePhotoList(container);\n });\n }\n\n return utilRebind(render, dispatch, 'on');\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\n\nimport { localizer, t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\nimport { utilNoAuto } from '../../util';\nimport { uiSettingsLocalPhotos } from '../settings/local_photos';\nimport { svgIcon } from '../../svg';\n\nexport function uiSectionPhotoOverlays(context) {\n\n let _savedLayers = [];\n let _layersHidden = false;\n\n var settingsLocalPhotos = uiSettingsLocalPhotos(context)\n .on('change', localPhotosChanged);\n\n var layers = context.layers();\n\n var section = uiSection('photo-overlays', context)\n .label(() => t.append('photo_overlays.title'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n const photoDates = {};\n const now = +new Date();\n\n /**\n * Calls all draw function\n * @param {*} selection Current HTML selection\n */\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.photo-overlay-container')\n .data([0]);\n\n container.enter()\n .append('div')\n .attr('class', 'photo-overlay-container')\n .merge(container)\n .call(drawPhotoItems)\n .call(drawPhotoTypeItems)\n .call(drawDateSlider)\n .call(drawUsernameFilter)\n .call(drawLocalPhotos);\n }\n\n /**\n * Draws the streetlevels in the right panel\n */\n function drawPhotoItems(selection) {\n var photoKeys = context.photos().overlayLayerIDs();\n var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; });\n var data = photoLayers.filter(function(obj) {\n if (!obj.layer.supported()) return false;\n if (layerEnabled(obj)) return true;\n if (typeof obj.layer.validHere === 'function') {\n return obj.layer.validHere(context.map().extent(), context.map().zoom());\n }\n return true;\n });\n\n function layerSupported(d) {\n return d.layer && d.layer.supported();\n }\n function layerEnabled(d) {\n return layerSupported(d) && (d.layer.enabled() || _savedLayers.includes(d.id));\n }\n function layerRendered(d) {\n return d.layer.rendered?.(context.map().zoom()) ?? true;\n }\n\n var ul = selection\n .selectAll('.layer-list-photos')\n .data([0]);\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-photos')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-photos')\n .data(data, d => d.id);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) {\n var classes = 'list-item-photos list-item-' + d.id;\n if (d.id === 'mapillary-signs' || d.id === 'mapillary-map-features') {\n classes += ' indented';\n }\n return classes;\n });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n var titleID;\n if (d.id === 'mapillary-signs') titleID = 'mapillary.signs.tooltip';\n else if (d.id === 'mapillary') titleID = 'mapillary_images.tooltip';\n else if (d.id === 'kartaview') titleID = 'kartaview_images.tooltip';\n else titleID = d.id.replace(/-/g, '_') + '.tooltip';\n d3_select(this)\n .call(uiTooltip()\n .title(() => {\n if (!layerRendered(d)) {\n return t.append('street_side.minzoom_tooltip');\n } else {\n return t.append(titleID);\n }\n })\n .placement('top')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) { toggleLayer(d.id); });\n\n labelEnter\n .append('span')\n .each(function(d) {\n var id = d.id;\n if (id === 'mapillary-signs') id = 'photo_overlays.traffic_signs';\n d3_select(this).call(t.append(id.replace(/-/g, '_') + '.title'));\n });\n\n // Update\n li\n .merge(liEnter)\n .classed('active', layerEnabled)\n .selectAll('input')\n .property('disabled', d => !layerRendered(d))\n .property('checked', layerEnabled);\n }\n\n /**\n * Draws the photo type filter in the right panel\n */\n function drawPhotoTypeItems(selection) {\n var data = context.photos().allPhotoTypes();\n\n function typeEnabled(d) {\n return context.photos().showsPhotoType(d);\n }\n\n var ul = selection\n .selectAll('.layer-list-photo-types')\n .data([0]);\n\n ul.exit()\n .remove();\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-photo-types')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-photo-types')\n .data(context.photos().shouldFilterByPhotoType() ? data : []);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) {\n return 'list-item-photo-types list-item-' + d;\n });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('photo_overlays.photo_type.' + d + '.tooltip'))\n .placement('top')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) {\n context.photos().togglePhotoType(d, true);\n });\n\n labelEnter\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append('photo_overlays.photo_type.' + d + '.title'));\n });\n\n\n // Update\n li\n .merge(liEnter)\n .classed('active', typeEnabled)\n .selectAll('input')\n .property('checked', typeEnabled);\n }\n\n /**\n * Draws the date slider filter in the right panel\n */\n function drawDateSlider(selection){\n\n var ul = selection\n .selectAll('.layer-list-date-slider')\n .data([0]);\n\n ul.exit()\n .remove();\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-date-slider')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-date-slider')\n .data(context.photos().shouldFilterDateBySlider() ? ['date-slider'] : []);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', 'list-item-date-slider');\n\n var labelEnter = liEnter\n .append('label')\n .each(function() {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('photo_overlays.age_slider_filter.tooltip'))\n .placement('top')\n );\n });\n\n labelEnter\n .append('span')\n .attr('class', 'dateSliderSpan')\n .call(t.append('photo_overlays.age_slider_filter.title'));\n\n let sliderWrap = labelEnter\n .append('div')\n .attr('class','slider-wrap');\n\n sliderWrap\n .append('input')\n .attr('type', 'range')\n .attr('min', 0)\n .attr('max', 1)\n .attr('step', 0.001)\n .attr('list', 'photo-overlay-date-range')\n .attr('value', () => dateSliderValue('from'))\n .classed('list-option-date-slider', true)\n .classed('from-date', true)\n .style('direction', localizer.textDirection() === 'rtl' ? 'ltr' : 'rtl')\n .call(utilNoAuto)\n .on('change', function() {\n let value = d3_select(this).property('value');\n setYearFilter(value, true, 'from');\n });\n selection.select('input.from-date').each(function() { this.value = dateSliderValue('from'); });\n\n sliderWrap.append('div')\n .attr('class', 'date-slider-label');\n\n sliderWrap\n .append('input')\n .attr('type', 'range')\n .attr('min', 0)\n .attr('max', 1)\n .attr('step', 0.001)\n .attr('list', 'photo-overlay-date-range-inverted')\n .attr('value', () => 1 - dateSliderValue('to'))\n .classed('list-option-date-slider', true)\n .classed('to-date', true)\n // OHM variant: https://github.com/OpenHistoricalMap/issues/issues/1030 .style('display', () => dateSliderValue('to') === 0 ? 'none' : null)\n .style('direction', localizer.textDirection())\n .call(utilNoAuto)\n .on('change', function() {\n let value = d3_select(this).property('value');\n setYearFilter(1-value, true, 'to');\n });\n selection.select('input.to-date').each(function() { this.value = 1 - dateSliderValue('to'); });\n\n selection.select('.date-slider-label')\n .call(dateSliderValue('from') === 1\n ? t.addOrUpdate('photo_overlays.age_slider_filter.label_all')\n : t.addOrUpdate('photo_overlays.age_slider_filter.label_date', {\n date: new Date(now - Math.pow(dateSliderValue('from'), 1.45) * 10 * 365.25 * 86400 * 1000).toLocaleDateString(localizer.localeCode()) }));\n\n sliderWrap.append('datalist')\n .attr('class', 'date-slider-values')\n .attr('id', 'photo-overlay-date-range');\n sliderWrap.append('datalist')\n .attr('class', 'date-slider-values')\n .attr('id', 'photo-overlay-date-range-inverted');\n\n const dateTicks = new Set();\n for (const dates of Object.values(photoDates)) {\n dates.forEach(date => {\n dateTicks.add(Math.round(1000 * Math.pow((now - date) / (10 * 365.25 * 86400 * 1000), 1/1.45)) / 1000);\n });\n }\n const ticks = selection.select('datalist#photo-overlay-date-range').selectAll('option')\n .data([...dateTicks].concat([1, 0]));\n ticks.exit()\n .remove();\n ticks.enter()\n .append('option')\n .merge(ticks)\n .attr('value', d => d);\n const ticksInverted = selection.select('datalist#photo-overlay-date-range-inverted').selectAll('option')\n .data([...dateTicks].concat([1, 0]));\n ticksInverted.exit()\n .remove();\n ticksInverted.enter()\n .append('option')\n .merge(ticksInverted)\n .attr('value', d => 1 - d);\n\n\n li\n .merge(liEnter)\n .classed('active', filterEnabled);\n\n function filterEnabled() {\n return !!context.photos().fromDate();\n }\n }\n\n function dateSliderValue(which) {\n const val = which === 'from' ? context.photos().fromDate() : context.photos().toDate();\n if (val) {\n const date = +new Date(val);\n return Math.pow((now - date) / (10 * 365.25 * 86400 * 1000), 1/1.45);\n } else return which === 'from' ? 1 : 0;\n }\n\n /**\n * Util function to set the slider date filter\n * @param {Number} value The slider value\n * @param {Boolean} updateUrl whether the URL should update or not\n * @param {string} which to set either the 'from' or 'to' date\n */\n function setYearFilter(value, updateUrl, which){\n value = +value + (which === 'from' ? 0.001 : -0.001);\n\n if (value < 1 && value > 0) {\n const date = new Date(now - Math.pow(value, 1.45) * 10 * 365.25 * 86400 * 1000)\n .toISOString().substring(0,10);\n context.photos().setDateFilter(`${which}Date`, date, updateUrl);\n } else {\n context.photos().setDateFilter(`${which}Date`, null, updateUrl);\n }\n };\n\n /**\n * Draws the username filter in the right panel\n */\n function drawUsernameFilter(selection) {\n function filterEnabled() {\n return context.photos().usernames();\n }\n var ul = selection\n .selectAll('.layer-list-username-filter')\n .data([0]);\n\n ul.exit()\n .remove();\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-username-filter')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-username-filter')\n .data(context.photos().shouldFilterByUsername() ? ['username-filter'] : []);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', 'list-item-username-filter');\n\n var labelEnter = liEnter\n .append('label')\n .each(function() {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('photo_overlays.username_filter.tooltip'))\n .placement('top')\n );\n });\n\n labelEnter\n .append('span')\n .call(t.append('photo_overlays.username_filter.title'));\n\n labelEnter\n .append('input')\n .attr('type', 'text')\n .attr('class', 'list-item-input')\n .call(utilNoAuto)\n .property('value', usernameValue)\n .on('change', function() {\n var value = d3_select(this).property('value');\n context.photos().setUsernameFilter(value, true);\n d3_select(this).property('value', usernameValue);\n });\n\n li\n .merge(liEnter)\n .classed('active', filterEnabled);\n\n function usernameValue() {\n var usernames = context.photos().usernames();\n if (usernames) return usernames.join('; ');\n return usernames;\n }\n }\n\n /**\n * Toggle on/off the selected layer\n * @param {*} which Id of the selected layer\n */\n function toggleLayer(which) {\n setLayer(which, !showsLayer(which));\n }\n\n /**\n * @param {*} which Id of the selected layer\n * @returns whether the layer is enabled\n */\n function showsLayer(which) {\n var layer = layers.layer(which);\n if (layer) {\n return layer.enabled();\n }\n return false;\n }\n\n /**\n * Set the selected layer\n * @param {string} which Id of the selected layer\n * @param {boolean} enabled\n */\n function setLayer(which, enabled) {\n var layer = layers.layer(which);\n if (layer) {\n layer.enabled(enabled);\n }\n }\n\n function drawLocalPhotos(selection) {\n var photoLayer = layers.layer('local-photos');\n var hasData = photoLayer && photoLayer.hasData();\n var showsData = hasData && photoLayer.enabled();\n\n var ul = selection\n .selectAll('.layer-list-local-photos')\n .data(photoLayer ? [0] : []);\n\n // Exit\n ul.exit()\n .remove();\n\n // Enter\n var ulEnter = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-local-photos');\n\n var localPhotosEnter = ulEnter\n .append('li')\n .attr('class', 'list-item-local-photos');\n\n var localPhotosLabelEnter = localPhotosEnter\n .append('label')\n .call(uiTooltip().title(() => t.append('local_photos.tooltip')));\n\n localPhotosLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function() { toggleLayer('local-photos'); });\n\n localPhotosLabelEnter\n .call(t.append('local_photos.header'));\n\n localPhotosEnter\n .append('button')\n .attr('class', 'open-data-options')\n .call(uiTooltip()\n .title(() => t.append('local_photos.tooltip_edit'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n editLocalPhotos();\n })\n .call(svgIcon('#iD-icon-more'));\n\n localPhotosEnter\n .append('button')\n .attr('class', 'zoom-to-data')\n .call(uiTooltip()\n .title(() => t.append('local_photos.zoom'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n if (d3_select(this).classed('disabled')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n photoLayer.fitZoom();\n })\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'));\n\n // Update\n ul = ul\n .merge(ulEnter);\n\n ul.selectAll('.list-item-local-photos')\n .classed('active', showsData)\n .selectAll('label')\n .classed('deemphasize', !hasData)\n .selectAll('input')\n .property('disabled', !hasData)\n .property('checked', showsData);\n\n ul.selectAll('button.zoom-to-data')\n .classed('disabled', !hasData);\n }\n\n function editLocalPhotos() {\n context.container()\n .call(settingsLocalPhotos);\n }\n\n function localPhotosChanged(d) {\n var localPhotosLayer = layers.layer('local-photos');\n\n localPhotosLayer.fileList(d);\n }\n\n /**\n * Toggles StreetView on/off\n */\n function toggleStreetSide(){\n let layerContainer = d3_select('.photo-overlay-container');\n if (!_layersHidden){\n const streetLayerIDs = context.photos().overlayLayerIDs();\n layers.all().forEach(d => {\n if (streetLayerIDs.includes(d.id)) {\n if (showsLayer(d.id)) _savedLayers.push(d.id);\n setLayer(d.id, false);\n }\n });\n layerContainer.classed('disabled-panel', true);\n } else {\n _savedLayers.forEach(d => {\n setLayer(d, true);\n });\n _savedLayers = [];\n layerContainer.classed('disabled-panel', false);\n }\n _layersHidden = !_layersHidden;\n };\n\n context.layers().on('change.uiSectionPhotoOverlays', section.reRender);\n context.photos().on('change.uiSectionPhotoOverlays', section.reRender);\n context.layers().on('photoDatesChanged.uiSectionPhotoOverlays', function(service, dates) {\n photoDates[service] = dates.map(date => +new Date(date));\n section.reRender();\n });\n context.keybinding().on('\u21E7P', toggleStreetSide);\n\n context.map()\n .on('move.photo_overlays',\n debounce(function() {\n // layers in-view may have changed due to map move\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiSection } from '../section';\nimport { utilQsString, utilStringQs, utilNormalizeDateString } from '../../util';\n\nconst DEFAULT_MIN_DATE = '-4000-01-01';\nconst DEFAULT_MAX_DATE = (new Date()).getFullYear() + '-12-31';\n\nconst INPUT_STYLES = [\n { name: 'width', value: '125px' },\n { name: 'text-align', value: 'center' },\n];\nconst LABEL_STYLES = [\n { name: 'font-weight', value: 'bold' },\n { name: 'display', value: 'inline-block' },\n { name: 'width', value: '75px' },\n];\n\nexport function uiSectionDateRange(context) {\n // despite appearing as a separate panel, Map Features does the real filtering\n // see applyDateRange() in this panel, where the dateRange value is set\n // see modules/renderer/features.js checkDateFilter() which applies the filters\n // see modules/renderer/features.js update() which updates URL params\n\n const section = uiSection('date_ranges', context)\n .label(() => t.append('date_ranges.title'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n function renderDisclosureContent(selection) {\n const container = selection.selectAll('.date_ranges-container').data([0]);\n\n // for some reason this one uiSection keeps adding content every time it's expanded,\n // so there are 2 inputs, then 4, then 6, ...\n const alreadyhasinputs = container.enter().selectAll('input').size();\n if (alreadyhasinputs) return;\n\n // start date label & input\n const mindate_label = container.enter()\n .append('label')\n .call(t.append('date_ranges.start_date.description'))\n .merge(container);\n const mindate_input = container.enter()\n .append('input')\n .attr('type', 'text')\n .attr('value', DEFAULT_MIN_DATE)\n .attr('title', () => t('date_ranges.start_date.tooltip'))\n .attr('placeholder', () => t('date_ranges.start_date.placeholder'))\n .merge(container);\n\n // line break\n container.enter()\n .append('br')\n .merge(container);\n\n // end date label & input\n const maxdate_label = container.enter()\n .append('label')\n .call(t.append('date_ranges.end_date.description'))\n .merge(container);\n const maxdate_input = container.enter() // we will refer to this widget by its name attribute to fetch our date range\n .append('input')\n .attr('type', 'text')\n .attr('value', DEFAULT_MAX_DATE)\n .attr('title', () => t('date_ranges.end_date.tooltip'))\n .attr('placeholder', () => t('date_ranges.end_date.placeholder'))\n .merge(container);\n\n // apply styles\n INPUT_STYLES.forEach(function (style) {\n mindate_input.style(style.name, style.value);\n maxdate_input.style(style.name, style.value);\n });\n LABEL_STYLES.forEach(function (style) {\n mindate_label.style(style.name, style.value);\n maxdate_label.style(style.name, style.value);\n });\n\n // event handler for change event\n // intercept invalid & blank and correct them to our hardcoded in/max\n // then cause a re-filter/redraw\n function applyDateRange() {\n const mindate = mindate_input.property('value');\n const maxdate = maxdate_input.property('value');\n\n context.features().dateRange = [mindate, maxdate];\n context.features().redraw();\n\n updateUrlParam();\n }\n\n function ensureValidInputs() {\n // if utilNormalizeDateString() can make sense of it, so can utilDatesOverlap()\n // replace with cleaned-up value for visual feedback e.g. 5/10/2022 visibly changes to 2022-10-05\n // if not, then reset to the starting value\n const mindate = mindate_input.property('value');\n const maxdate = maxdate_input.property('value');\n\n const mindate_clean = utilNormalizeDateString(mindate);\n const maxdate_clean = utilNormalizeDateString(maxdate);\n mindate_input.property('value', mindate_clean ? mindate_clean.value : DEFAULT_MIN_DATE);\n maxdate_input.property('value', maxdate_clean ? maxdate_clean.value : DEFAULT_MAX_DATE);\n }\n\n function updateUrlParam() {\n if (!window.mocha) {\n const hash = utilStringQs(window.location.hash);\n\n const daterange = context.features().dateRange;\n if (daterange) {\n hash.daterange = daterange.join(',');\n } else {\n delete hash.daterange;\n }\n\n window.location.replace('#' + utilQsString(hash, true));\n }\n }\n\n mindate_input.on('change', function () {\n ensureValidInputs();\n applyDateRange();\n });\n maxdate_input.on('change', function () {\n ensureValidInputs();\n applyDateRange();\n });\n\n // startup\n // load the start/end date from URL params\n // then apply it so we have context().dateRange defined as early as possible\n let startingdaterange = utilStringQs(window.location.hash).daterange;\n if (startingdaterange) {\n startingdaterange = startingdaterange.split(',');\n const isvalid =startingdaterange[0].match(/^\\-?[\\d\\-]+/) && startingdaterange[1].match(/^\\-?[\\d\\-]+/);\n if (isvalid) {\n mindate_input.property('value', startingdaterange[0]);\n maxdate_input.property('value', startingdaterange[1]);\n }\n }\n applyDateRange();\n }\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\n\nimport { uiSectionDataLayers } from '../sections/data_layers';\nimport { uiSectionMapFeatures } from '../sections/map_features';\nimport { uiSectionMapStyleOptions } from '../sections/map_style_options';\nimport { uiSectionPhotoOverlays } from '../sections/photo_overlays';\nimport { uiSectionDateRange } from '../sections/map_daterange';\n\nexport function uiPaneMapData(context) {\n\n var mapDataPane = uiPane('map-data', context)\n .key(t('map_data.key'))\n .label(t.append('map_data.title'))\n .description(t.append('map_data.description'))\n .iconName('iD-icon-data')\n .sections([\n uiSectionDataLayers(context),\n uiSectionDateRange(context),\n uiSectionPhotoOverlays(context),\n uiSectionMapStyleOptions(context),\n uiSectionMapFeatures(context)\n ]);\n\n return mapDataPane;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\nimport { uiSectionPrivacy } from '../sections/privacy';\n\nexport function uiPanePreferences(context) {\n\n let preferencesPane = uiPane('preferences', context)\n .key(t('preferences.key'))\n .label(t.append('preferences.title'))\n .description(t.append('preferences.description'))\n .iconName('fas-user-cog')\n .sections([\n uiSectionPrivacy(context)\n ]);\n\n return preferencesPane;\n}\n", "import { marked } from 'marked';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { prefs } from '../core/preferences';\nimport { t, localizer } from '../core/localizer';\nimport { presetManager } from '../presets';\nimport { behaviorHash } from '../behavior';\nimport { modeBrowse } from '../modes/browse';\nimport { svgDefs, svgIcon } from '../svg';\nimport { utilDetect } from '../util/detect';\nimport { utilGetDimensions } from '../util/dimensions';\n\nimport { uiAccount } from './account';\nimport { uiAttribution } from './attribution';\nimport { uiContributors } from './contributors';\nimport { uiEditMenu } from './edit_menu';\nimport { uiFeatureInfo } from './feature_info';\nimport { uiFlash } from './flash';\nimport { uiFullScreen } from './full_screen';\nimport { uiGeolocate } from './geolocate';\nimport { uiInfo } from './info';\nimport { uiIntro } from './intro';\nimport { uiIssuesInfo } from './issues_info';\nimport { uiLoading } from './loading';\nimport { uiMapInMap } from './map_in_map';\nimport { uiNotice } from './notice';\nimport { uiPhotoviewer } from './photoviewer';\nimport { uiRestore } from './restore';\nimport { uiScale } from './scale';\nimport { uiShortcuts } from './shortcuts';\nimport { uiSidebar } from './sidebar';\nimport { uiSourceSwitch } from './source_switch';\nimport { uiSpinner } from './spinner';\nimport { uiSplash } from './splash';\nimport { uiStatus } from './status';\nimport { uiTooltip } from './tooltip';\nimport { uiTopToolbar } from './top_toolbar';\nimport { uiVersion } from './version';\nimport { uiZoom } from './zoom';\nimport { uiZoomToSelection } from './zoom_to_selection';\nimport { uiCmd } from './cmd';\n\nimport { uiPaneBackground } from './panes/background';\nimport { uiPaneHelp } from './panes/help';\nimport { uiPaneIssues } from './panes/issues';\nimport { uiPaneMapData } from './panes/map_data';\nimport { uiPanePreferences } from './panes/preferences';\n\nexport function uiInit(context) {\n var _initCounter = 0;\n var _needWidth = {};\n\n var _lastPointerType;\n\n var overMap;\n\n function render(container) {\n\n container\n .on('click.ui', function(d3_event) {\n // we're only concerned with the primary mouse button\n if (d3_event.button !== 0) return;\n\n if (!d3_event.composedPath) return;\n\n // some targets have default click events we don't want to override\n var isOkayTarget = d3_event.composedPath().some(function(node) {\n // we only care about element nodes\n return node.nodeType === 1 &&\n // clicking focuses it and/or changes a value\n (node.nodeName === 'INPUT' ||\n // clicking

    '\n        + (escaped ? code : escapeHtmlEntities(code, true))\n        + '
    \\n' as RendererOutput;\n }\n\n return '
    '\n      + (escaped ? code : escapeHtmlEntities(code, true))\n      + '
    \\n' as RendererOutput;\n }\n\n blockquote({ tokens }: Tokens.Blockquote): RendererOutput {\n const body = this.parser.parse(tokens);\n return `
    \\n${body}
    \\n` as RendererOutput;\n }\n\n html({ text }: Tokens.HTML | Tokens.Tag): RendererOutput {\n return text as RendererOutput;\n }\n\n def(token: Tokens.Def): RendererOutput {\n return '' as RendererOutput;\n }\n\n heading({ tokens, depth }: Tokens.Heading): RendererOutput {\n return `${this.parser.parseInline(tokens)}\\n` as RendererOutput;\n }\n\n hr(token: Tokens.Hr): RendererOutput {\n return '
    \\n' as RendererOutput;\n }\n\n list(token: Tokens.List): RendererOutput {\n const ordered = token.ordered;\n const start = token.start;\n\n let body = '';\n for (let j = 0; j < token.items.length; j++) {\n const item = token.items[j];\n body += this.listitem(item);\n }\n\n const type = ordered ? 'ol' : 'ul';\n const startAttr = (ordered && start !== 1) ? (' start=\"' + start + '\"') : '';\n return '<' + type + startAttr + '>\\n' + body + '\\n' as RendererOutput;\n }\n\n listitem(item: Tokens.ListItem): RendererOutput {\n return `
  • ${this.parser.parse(item.tokens)}
  • \\n` as RendererOutput;\n }\n\n checkbox({ checked }: Tokens.Checkbox): RendererOutput {\n return ' ' as RendererOutput;\n }\n\n paragraph({ tokens }: Tokens.Paragraph): RendererOutput {\n return `

    ${this.parser.parseInline(tokens)}

    \\n` as RendererOutput;\n }\n\n table(token: Tokens.Table): RendererOutput {\n let header = '';\n\n // header\n let cell = '';\n for (let j = 0; j < token.header.length; j++) {\n cell += this.tablecell(token.header[j]);\n }\n header += this.tablerow({ text: cell as ParserOutput });\n\n let body = '';\n for (let j = 0; j < token.rows.length; j++) {\n const row = token.rows[j];\n\n cell = '';\n for (let k = 0; k < row.length; k++) {\n cell += this.tablecell(row[k]);\n }\n\n body += this.tablerow({ text: cell as ParserOutput });\n }\n if (body) body = `${body}`;\n\n return '\\n'\n + '\\n'\n + header\n + '\\n'\n + body\n + '
    \\n' as RendererOutput;\n }\n\n tablerow({ text }: Tokens.TableRow): RendererOutput {\n return `\\n${text}\\n` as RendererOutput;\n }\n\n tablecell(token: Tokens.TableCell): RendererOutput {\n const content = this.parser.parseInline(token.tokens);\n const type = token.header ? 'th' : 'td';\n const tag = token.align\n ? `<${type} align=\"${token.align}\">`\n : `<${type}>`;\n return tag + content + `\\n` as RendererOutput;\n }\n\n /**\n * span level renderer\n */\n strong({ tokens }: Tokens.Strong): RendererOutput {\n return `${this.parser.parseInline(tokens)}` as RendererOutput;\n }\n\n em({ tokens }: Tokens.Em): RendererOutput {\n return `${this.parser.parseInline(tokens)}` as RendererOutput;\n }\n\n codespan({ text }: Tokens.Codespan): RendererOutput {\n return `${escapeHtmlEntities(text, true)}` as RendererOutput;\n }\n\n br(token: Tokens.Br): RendererOutput {\n return '
    ' as RendererOutput;\n }\n\n del({ tokens }: Tokens.Del): RendererOutput {\n return `${this.parser.parseInline(tokens)}` as RendererOutput;\n }\n\n link({ href, title, tokens }: Tokens.Link): RendererOutput {\n const text = this.parser.parseInline(tokens) as string;\n const cleanHref = cleanUrl(href);\n if (cleanHref === null) {\n return text as RendererOutput;\n }\n href = cleanHref;\n let out = '
    ';\n return out as RendererOutput;\n }\n\n image({ href, title, text, tokens }: Tokens.Image): RendererOutput {\n if (tokens) {\n text = this.parser.parseInline(tokens, this.parser.textRenderer) as string;\n }\n const cleanHref = cleanUrl(href);\n if (cleanHref === null) {\n return escapeHtmlEntities(text) as RendererOutput;\n }\n href = cleanHref;\n\n let out = `\"${escapeHtmlEntities(text)}\"`;\n {\n // no need for block level renderers\n strong({ text }: Tokens.Strong): RendererOutput {\n return text as RendererOutput;\n }\n\n em({ text }: Tokens.Em): RendererOutput {\n return text as RendererOutput;\n }\n\n codespan({ text }: Tokens.Codespan): RendererOutput {\n return text as RendererOutput;\n }\n\n del({ text }: Tokens.Del): RendererOutput {\n return text as RendererOutput;\n }\n\n html({ text }: Tokens.HTML | Tokens.Tag): RendererOutput {\n return text as RendererOutput;\n }\n\n text({ text }: Tokens.Text | Tokens.Escape | Tokens.Tag): RendererOutput {\n return text as RendererOutput;\n }\n\n link({ text }: Tokens.Link): RendererOutput {\n return '' + text as RendererOutput;\n }\n\n image({ text }: Tokens.Image): RendererOutput {\n return '' + text as RendererOutput;\n }\n\n br(): RendererOutput {\n return '' as RendererOutput;\n }\n\n checkbox({ raw }: Tokens.Checkbox): RendererOutput {\n return raw as RendererOutput;\n }\n}\n", "import { _Renderer } from './Renderer.ts';\nimport { _TextRenderer } from './TextRenderer.ts';\nimport { _defaults } from './defaults.ts';\nimport type { MarkedToken, Token, Tokens } from './Tokens.ts';\nimport type { MarkedOptions } from './MarkedOptions.ts';\n\n/**\n * Parsing & Compiling\n */\nexport class _Parser {\n options: MarkedOptions;\n renderer: _Renderer;\n textRenderer: _TextRenderer;\n constructor(options?: MarkedOptions) {\n this.options = options || _defaults;\n this.options.renderer = this.options.renderer || new _Renderer();\n this.renderer = this.options.renderer;\n this.renderer.options = this.options;\n this.renderer.parser = this;\n this.textRenderer = new _TextRenderer();\n }\n\n /**\n * Static Parse Method\n */\n static parse(tokens: Token[], options?: MarkedOptions) {\n const parser = new _Parser(options);\n return parser.parse(tokens);\n }\n\n /**\n * Static Parse Inline Method\n */\n static parseInline(tokens: Token[], options?: MarkedOptions) {\n const parser = new _Parser(options);\n return parser.parseInline(tokens);\n }\n\n /**\n * Parse Loop\n */\n parse(tokens: Token[]): ParserOutput {\n let out = '';\n\n for (let i = 0; i < tokens.length; i++) {\n const anyToken = tokens[i];\n\n // Run any renderer extensions\n if (this.options.extensions?.renderers?.[anyToken.type]) {\n const genericToken = anyToken as Tokens.Generic;\n const ret = this.options.extensions.renderers[genericToken.type].call({ parser: this }, genericToken);\n if (ret !== false || !['space', 'hr', 'heading', 'code', 'table', 'blockquote', 'list', 'html', 'def', 'paragraph', 'text'].includes(genericToken.type)) {\n out += ret || '';\n continue;\n }\n }\n\n const token = anyToken as MarkedToken;\n\n switch (token.type) {\n case 'space': {\n out += this.renderer.space(token);\n break;\n }\n case 'hr': {\n out += this.renderer.hr(token);\n break;\n }\n case 'heading': {\n out += this.renderer.heading(token);\n break;\n }\n case 'code': {\n out += this.renderer.code(token);\n break;\n }\n case 'table': {\n out += this.renderer.table(token);\n break;\n }\n case 'blockquote': {\n out += this.renderer.blockquote(token);\n break;\n }\n case 'list': {\n out += this.renderer.list(token);\n break;\n }\n case 'checkbox': {\n out += this.renderer.checkbox(token);\n break;\n }\n case 'html': {\n out += this.renderer.html(token);\n break;\n }\n case 'def': {\n out += this.renderer.def(token);\n break;\n }\n case 'paragraph': {\n out += this.renderer.paragraph(token);\n break;\n }\n case 'text': {\n out += this.renderer.text(token);\n break;\n }\n\n default: {\n const errMsg = 'Token with \"' + token.type + '\" type was not found.';\n if (this.options.silent) {\n console.error(errMsg);\n return '' as ParserOutput;\n } else {\n throw new Error(errMsg);\n }\n }\n }\n }\n\n return out as ParserOutput;\n }\n\n /**\n * Parse Inline Tokens\n */\n parseInline(tokens: Token[], renderer: _Renderer | _TextRenderer = this.renderer): ParserOutput {\n let out = '';\n\n for (let i = 0; i < tokens.length; i++) {\n const anyToken = tokens[i];\n\n // Run any renderer extensions\n if (this.options.extensions?.renderers?.[anyToken.type]) {\n const ret = this.options.extensions.renderers[anyToken.type].call({ parser: this }, anyToken);\n if (ret !== false || !['escape', 'html', 'link', 'image', 'strong', 'em', 'codespan', 'br', 'del', 'text'].includes(anyToken.type)) {\n out += ret || '';\n continue;\n }\n }\n\n const token = anyToken as MarkedToken;\n\n switch (token.type) {\n case 'escape': {\n out += renderer.text(token);\n break;\n }\n case 'html': {\n out += renderer.html(token);\n break;\n }\n case 'link': {\n out += renderer.link(token);\n break;\n }\n case 'image': {\n out += renderer.image(token);\n break;\n }\n case 'checkbox': {\n out += renderer.checkbox(token);\n break;\n }\n case 'strong': {\n out += renderer.strong(token);\n break;\n }\n case 'em': {\n out += renderer.em(token);\n break;\n }\n case 'codespan': {\n out += renderer.codespan(token);\n break;\n }\n case 'br': {\n out += renderer.br(token);\n break;\n }\n case 'del': {\n out += renderer.del(token);\n break;\n }\n case 'text': {\n out += renderer.text(token);\n break;\n }\n default: {\n const errMsg = 'Token with \"' + token.type + '\" type was not found.';\n if (this.options.silent) {\n console.error(errMsg);\n return '' as ParserOutput;\n } else {\n throw new Error(errMsg);\n }\n }\n }\n }\n return out as ParserOutput;\n }\n}\n", "import { _defaults } from './defaults.ts';\nimport { _Lexer } from './Lexer.ts';\nimport { _Parser } from './Parser.ts';\nimport type { MarkedOptions } from './MarkedOptions.ts';\nimport type { Token, TokensList } from './Tokens.ts';\n\nexport class _Hooks {\n options: MarkedOptions;\n block?: boolean;\n\n constructor(options?: MarkedOptions) {\n this.options = options || _defaults;\n }\n\n static passThroughHooks = new Set([\n 'preprocess',\n 'postprocess',\n 'processAllTokens',\n 'emStrongMask',\n ]);\n\n static passThroughHooksRespectAsync = new Set([\n 'preprocess',\n 'postprocess',\n 'processAllTokens',\n ]);\n\n /**\n * Process markdown before marked\n */\n preprocess(markdown: string) {\n return markdown;\n }\n\n /**\n * Process HTML after marked is finished\n */\n postprocess(html: ParserOutput) {\n return html;\n }\n\n /**\n * Process all tokens before walk tokens\n */\n processAllTokens(tokens: Token[] | TokensList) {\n return tokens;\n }\n\n /**\n * Mask contents that should not be interpreted as em/strong delimiters\n */\n emStrongMask(src: string) {\n return src;\n }\n\n /**\n * Provide function to tokenize markdown\n */\n provideLexer() {\n return this.block ? _Lexer.lex : _Lexer.lexInline;\n }\n\n /**\n * Provide function to parse tokens\n */\n provideParser() {\n return this.block ? _Parser.parse : _Parser.parseInline;\n }\n}\n", "import { _getDefaults } from './defaults.ts';\nimport { _Lexer } from './Lexer.ts';\nimport { _Parser } from './Parser.ts';\nimport { _Hooks } from './Hooks.ts';\nimport { _Renderer } from './Renderer.ts';\nimport { _Tokenizer } from './Tokenizer.ts';\nimport { _TextRenderer } from './TextRenderer.ts';\nimport { escapeHtmlEntities } from './helpers.ts';\nimport type { MarkedExtension, MarkedOptions } from './MarkedOptions.ts';\nimport type { Token, Tokens, TokensList } from './Tokens.ts';\n\nexport type MaybePromise = void | Promise;\n\ntype UnknownFunction = (...args: unknown[]) => unknown;\ntype GenericRendererFunction = (...args: unknown[]) => string | false;\n\nexport class Marked {\n defaults = _getDefaults();\n options = this.setOptions;\n\n parse = this.parseMarkdown(true);\n parseInline = this.parseMarkdown(false);\n\n Parser = _Parser;\n Renderer = _Renderer;\n TextRenderer = _TextRenderer;\n Lexer = _Lexer;\n Tokenizer = _Tokenizer;\n Hooks = _Hooks;\n\n constructor(...args: MarkedExtension[]) {\n this.use(...args);\n }\n\n /**\n * Run callback for every token\n */\n walkTokens(tokens: Token[] | TokensList, callback: (token: Token) => MaybePromise | MaybePromise[]) {\n let values: MaybePromise[] = [];\n for (const token of tokens) {\n values = values.concat(callback.call(this, token));\n switch (token.type) {\n case 'table': {\n const tableToken = token as Tokens.Table;\n for (const cell of tableToken.header) {\n values = values.concat(this.walkTokens(cell.tokens, callback));\n }\n for (const row of tableToken.rows) {\n for (const cell of row) {\n values = values.concat(this.walkTokens(cell.tokens, callback));\n }\n }\n break;\n }\n case 'list': {\n const listToken = token as Tokens.List;\n values = values.concat(this.walkTokens(listToken.items, callback));\n break;\n }\n default: {\n const genericToken = token as Tokens.Generic;\n if (this.defaults.extensions?.childTokens?.[genericToken.type]) {\n this.defaults.extensions.childTokens[genericToken.type].forEach((childTokens) => {\n const tokens = genericToken[childTokens].flat(Infinity) as Token[] | TokensList;\n values = values.concat(this.walkTokens(tokens, callback));\n });\n } else if (genericToken.tokens) {\n values = values.concat(this.walkTokens(genericToken.tokens, callback));\n }\n }\n }\n }\n return values;\n }\n\n use(...args: MarkedExtension[]) {\n const extensions: MarkedOptions['extensions'] = this.defaults.extensions || { renderers: {}, childTokens: {} };\n\n args.forEach((pack) => {\n // copy options to new object\n const opts = { ...pack } as MarkedOptions;\n\n // set async to true if it was set to true before\n opts.async = this.defaults.async || opts.async || false;\n\n // ==-- Parse \"addon\" extensions --== //\n if (pack.extensions) {\n pack.extensions.forEach((ext) => {\n if (!ext.name) {\n throw new Error('extension name required');\n }\n if ('renderer' in ext) { // Renderer extensions\n const prevRenderer = extensions.renderers[ext.name];\n if (prevRenderer) {\n // Replace extension with func to run new extension but fall back if false\n extensions.renderers[ext.name] = function(...args) {\n let ret = ext.renderer.apply(this, args);\n if (ret === false) {\n ret = prevRenderer.apply(this, args);\n }\n return ret;\n };\n } else {\n extensions.renderers[ext.name] = ext.renderer;\n }\n }\n if ('tokenizer' in ext) { // Tokenizer Extensions\n if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) {\n throw new Error(\"extension level must be 'block' or 'inline'\");\n }\n const extLevel = extensions[ext.level];\n if (extLevel) {\n extLevel.unshift(ext.tokenizer);\n } else {\n extensions[ext.level] = [ext.tokenizer];\n }\n if (ext.start) { // Function to check for start of token\n if (ext.level === 'block') {\n if (extensions.startBlock) {\n extensions.startBlock.push(ext.start);\n } else {\n extensions.startBlock = [ext.start];\n }\n } else if (ext.level === 'inline') {\n if (extensions.startInline) {\n extensions.startInline.push(ext.start);\n } else {\n extensions.startInline = [ext.start];\n }\n }\n }\n }\n if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens\n extensions.childTokens[ext.name] = ext.childTokens;\n }\n });\n opts.extensions = extensions;\n }\n\n // ==-- Parse \"overwrite\" extensions --== //\n if (pack.renderer) {\n const renderer = this.defaults.renderer || new _Renderer(this.defaults);\n for (const prop in pack.renderer) {\n if (!(prop in renderer)) {\n throw new Error(`renderer '${prop}' does not exist`);\n }\n if (['options', 'parser'].includes(prop)) {\n // ignore options property\n continue;\n }\n const rendererProp = prop as Exclude, 'options' | 'parser'>;\n const rendererFunc = pack.renderer[rendererProp] as GenericRendererFunction;\n const prevRenderer = renderer[rendererProp] as GenericRendererFunction;\n // Replace renderer with func to run extension, but fall back if false\n renderer[rendererProp] = (...args: unknown[]) => {\n let ret = rendererFunc.apply(renderer, args);\n if (ret === false) {\n ret = prevRenderer.apply(renderer, args);\n }\n return (ret || '') as RendererOutput;\n };\n }\n opts.renderer = renderer;\n }\n if (pack.tokenizer) {\n const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults);\n for (const prop in pack.tokenizer) {\n if (!(prop in tokenizer)) {\n throw new Error(`tokenizer '${prop}' does not exist`);\n }\n if (['options', 'rules', 'lexer'].includes(prop)) {\n // ignore options, rules, and lexer properties\n continue;\n }\n const tokenizerProp = prop as Exclude, 'options' | 'rules' | 'lexer'>;\n const tokenizerFunc = pack.tokenizer[tokenizerProp] as UnknownFunction;\n const prevTokenizer = tokenizer[tokenizerProp] as UnknownFunction;\n // Replace tokenizer with func to run extension, but fall back if false\n // @ts-expect-error cannot type tokenizer function dynamically\n tokenizer[tokenizerProp] = (...args: unknown[]) => {\n let ret = tokenizerFunc.apply(tokenizer, args);\n if (ret === false) {\n ret = prevTokenizer.apply(tokenizer, args);\n }\n return ret;\n };\n }\n opts.tokenizer = tokenizer;\n }\n\n // ==-- Parse Hooks extensions --== //\n if (pack.hooks) {\n const hooks = this.defaults.hooks || new _Hooks();\n for (const prop in pack.hooks) {\n if (!(prop in hooks)) {\n throw new Error(`hook '${prop}' does not exist`);\n }\n if (['options', 'block'].includes(prop)) {\n // ignore options and block properties\n continue;\n }\n const hooksProp = prop as Exclude, 'options' | 'block'>;\n const hooksFunc = pack.hooks[hooksProp] as UnknownFunction;\n const prevHook = hooks[hooksProp] as UnknownFunction;\n if (_Hooks.passThroughHooks.has(prop)) {\n // @ts-expect-error cannot type hook function dynamically\n hooks[hooksProp] = (arg: unknown) => {\n if (this.defaults.async && _Hooks.passThroughHooksRespectAsync.has(prop)) {\n return (async() => {\n const ret = await hooksFunc.call(hooks, arg);\n return prevHook.call(hooks, ret);\n })();\n }\n\n const ret = hooksFunc.call(hooks, arg);\n return prevHook.call(hooks, ret);\n };\n } else {\n // @ts-expect-error cannot type hook function dynamically\n hooks[hooksProp] = (...args: unknown[]) => {\n if (this.defaults.async) {\n return (async() => {\n let ret = await hooksFunc.apply(hooks, args);\n if (ret === false) {\n ret = await prevHook.apply(hooks, args);\n }\n return ret;\n })();\n }\n\n let ret = hooksFunc.apply(hooks, args);\n if (ret === false) {\n ret = prevHook.apply(hooks, args);\n }\n return ret;\n };\n }\n }\n opts.hooks = hooks;\n }\n\n // ==-- Parse WalkTokens extensions --== //\n if (pack.walkTokens) {\n const walkTokens = this.defaults.walkTokens;\n const packWalktokens = pack.walkTokens;\n opts.walkTokens = function(token) {\n let values: MaybePromise[] = [];\n values.push(packWalktokens.call(this, token));\n if (walkTokens) {\n values = values.concat(walkTokens.call(this, token));\n }\n return values;\n };\n }\n\n this.defaults = { ...this.defaults, ...opts };\n });\n\n return this;\n }\n\n setOptions(opt: MarkedOptions) {\n this.defaults = { ...this.defaults, ...opt };\n return this;\n }\n\n lexer(src: string, options?: MarkedOptions) {\n return _Lexer.lex(src, options ?? this.defaults);\n }\n\n parser(tokens: Token[], options?: MarkedOptions) {\n return _Parser.parse(tokens, options ?? this.defaults);\n }\n\n private parseMarkdown(blockType: boolean) {\n type overloadedParse = {\n (src: string, options: MarkedOptions & { async: true }): Promise;\n (src: string, options: MarkedOptions & { async: false }): ParserOutput;\n (src: string, options?: MarkedOptions | null): ParserOutput | Promise;\n };\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const parse: overloadedParse = (src: string, options?: MarkedOptions | null): any => {\n const origOpt = { ...options };\n const opt = { ...this.defaults, ...origOpt };\n\n const throwError = this.onError(!!opt.silent, !!opt.async);\n\n // throw error if an extension set async to true but parse was called with async: false\n if (this.defaults.async === true && origOpt.async === false) {\n return throwError(new Error('marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.'));\n }\n\n // throw error in case of non string input\n if (typeof src === 'undefined' || src === null) {\n return throwError(new Error('marked(): input parameter is undefined or null'));\n }\n if (typeof src !== 'string') {\n return throwError(new Error('marked(): input parameter is of type '\n + Object.prototype.toString.call(src) + ', string expected'));\n }\n\n if (opt.hooks) {\n opt.hooks.options = opt;\n opt.hooks.block = blockType;\n }\n\n if (opt.async) {\n return (async() => {\n const processedSrc = opt.hooks ? await opt.hooks.preprocess(src) : src;\n const lexer = opt.hooks ? await opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline);\n const tokens = await lexer(processedSrc, opt);\n const processedTokens = opt.hooks ? await opt.hooks.processAllTokens(tokens) : tokens;\n if (opt.walkTokens) {\n await Promise.all(this.walkTokens(processedTokens, opt.walkTokens));\n }\n const parser = opt.hooks ? await opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline);\n const html = await parser(processedTokens, opt);\n return opt.hooks ? await opt.hooks.postprocess(html) : html;\n })().catch(throwError);\n }\n\n try {\n if (opt.hooks) {\n src = opt.hooks.preprocess(src) as string;\n }\n const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline);\n let tokens = lexer(src, opt);\n if (opt.hooks) {\n tokens = opt.hooks.processAllTokens(tokens);\n }\n if (opt.walkTokens) {\n this.walkTokens(tokens, opt.walkTokens);\n }\n const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline);\n let html = parser(tokens, opt);\n if (opt.hooks) {\n html = opt.hooks.postprocess(html);\n }\n return html;\n } catch(e) {\n return throwError(e as Error);\n }\n };\n\n return parse;\n }\n\n private onError(silent: boolean, async: boolean) {\n return (e: Error): string | Promise => {\n e.message += '\\nPlease report this to https://github.com/markedjs/marked.';\n\n if (silent) {\n const msg = '

    An error occurred:

    '\n          + escapeHtmlEntities(e.message + '', true)\n          + '
    ';\n if (async) {\n return Promise.resolve(msg);\n }\n return msg;\n }\n\n if (async) {\n return Promise.reject(e);\n }\n throw e;\n };\n }\n}\n", "import { _Lexer } from './Lexer.ts';\nimport { _Parser } from './Parser.ts';\nimport { _Tokenizer } from './Tokenizer.ts';\nimport { _Renderer } from './Renderer.ts';\nimport { _TextRenderer } from './TextRenderer.ts';\nimport { _Hooks } from './Hooks.ts';\nimport { Marked } from './Instance.ts';\nimport {\n _getDefaults,\n changeDefaults,\n _defaults,\n} from './defaults.ts';\nimport type { MarkedExtension, MarkedOptions } from './MarkedOptions.ts';\nimport type { Token, TokensList } from './Tokens.ts';\nimport type { MaybePromise } from './Instance.ts';\n\nconst markedInstance = new Marked();\n\n/**\n * Compiles markdown to HTML asynchronously.\n *\n * @param src String of markdown source to be compiled\n * @param options Hash of options, having async: true\n * @return Promise of string of compiled HTML\n */\nexport function marked(src: string, options: MarkedOptions & { async: true }): Promise;\n\n/**\n * Compiles markdown to HTML.\n *\n * @param src String of markdown source to be compiled\n * @param options Optional hash of options\n * @return String of compiled HTML. Will be a Promise of string if async is set to true by any extensions.\n */\nexport function marked(src: string, options: MarkedOptions & { async: false }): string;\nexport function marked(src: string, options: MarkedOptions & { async: true }): Promise;\nexport function marked(src: string, options?: MarkedOptions | null): string | Promise;\nexport function marked(src: string, opt?: MarkedOptions | null): string | Promise {\n return markedInstance.parse(src, opt);\n}\n\n/**\n * Sets the default options.\n *\n * @param options Hash of options\n */\nmarked.options =\n marked.setOptions = function(options: MarkedOptions) {\n markedInstance.setOptions(options);\n marked.defaults = markedInstance.defaults;\n changeDefaults(marked.defaults);\n return marked;\n };\n\n/**\n * Gets the original marked default options.\n */\nmarked.getDefaults = _getDefaults;\n\nmarked.defaults = _defaults;\n\n/**\n * Use Extension\n */\n\nmarked.use = function(...args: MarkedExtension[]) {\n markedInstance.use(...args);\n marked.defaults = markedInstance.defaults;\n changeDefaults(marked.defaults);\n return marked;\n};\n\n/**\n * Run callback for every token\n */\n\nmarked.walkTokens = function(tokens: Token[] | TokensList, callback: (token: Token) => MaybePromise | MaybePromise[]) {\n return markedInstance.walkTokens(tokens, callback);\n};\n\n/**\n * Compiles markdown to HTML without enclosing `p` tag.\n *\n * @param src String of markdown source to be compiled\n * @param options Hash of options\n * @return String of compiled HTML\n */\nmarked.parseInline = markedInstance.parseInline;\n\n/**\n * Expose\n */\nmarked.Parser = _Parser;\nmarked.parser = _Parser.parse;\nmarked.Renderer = _Renderer;\nmarked.TextRenderer = _TextRenderer;\nmarked.Lexer = _Lexer;\nmarked.lexer = _Lexer.lex;\nmarked.Tokenizer = _Tokenizer;\nmarked.Hooks = _Hooks;\nmarked.parse = marked;\n\nexport const options = marked.options;\nexport const setOptions = marked.setOptions;\nexport const use = marked.use;\nexport const walkTokens = marked.walkTokens;\nexport const parseInline = marked.parseInline;\nexport const parse = marked;\nexport const parser = _Parser.parse;\nexport const lexer = _Lexer.lex;\nexport { _defaults as defaults, _getDefaults as getDefaults } from './defaults.ts';\nexport { _Lexer as Lexer } from './Lexer.ts';\nexport { _Parser as Parser } from './Parser.ts';\nexport { _Tokenizer as Tokenizer } from './Tokenizer.ts';\nexport { _Renderer as Renderer } from './Renderer.ts';\nexport { _TextRenderer as TextRenderer } from './TextRenderer.ts';\nexport { _Hooks as Hooks } from './Hooks.ts';\nexport { Marked } from './Instance.ts';\nexport type * from './MarkedOptions.ts';\nexport type * from './Tokens.ts';\n", "\nconst SHIFT_LEFT_32 = (1 << 16) * (1 << 16);\nconst SHIFT_RIGHT_32 = 1 / SHIFT_LEFT_32;\n\n// Threshold chosen based on both benchmarking and knowledge about browser string\n// data structures (which currently switch structure types at 12 bytes or more)\nconst TEXT_DECODER_MIN_LENGTH = 12;\nconst utf8TextDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8');\n\nconst PBF_VARINT = 0; // varint: int32, int64, uint32, uint64, sint32, sint64, bool, enum\nconst PBF_FIXED64 = 1; // 64-bit: double, fixed64, sfixed64\nconst PBF_BYTES = 2; // length-delimited: string, bytes, embedded messages, packed repeated fields\nconst PBF_FIXED32 = 5; // 32-bit: float, fixed32, sfixed32\n\nexport default class Pbf {\n /**\n * @param {Uint8Array | ArrayBuffer} [buf]\n */\n constructor(buf = new Uint8Array(16)) {\n this.buf = ArrayBuffer.isView(buf) ? buf : new Uint8Array(buf);\n this.dataView = new DataView(this.buf.buffer);\n this.pos = 0;\n this.type = 0;\n this.length = this.buf.length;\n }\n\n // === READING =================================================================\n\n /**\n * @template T\n * @param {(tag: number, result: T, pbf: Pbf) => void} readField\n * @param {T} result\n * @param {number} [end]\n */\n readFields(readField, result, end = this.length) {\n while (this.pos < end) {\n const val = this.readVarint(),\n tag = val >> 3,\n startPos = this.pos;\n\n this.type = val & 0x7;\n readField(tag, result, this);\n\n if (this.pos === startPos) this.skip(val);\n }\n return result;\n }\n\n /**\n * @template T\n * @param {(tag: number, result: T, pbf: Pbf) => void} readField\n * @param {T} result\n */\n readMessage(readField, result) {\n return this.readFields(readField, result, this.readVarint() + this.pos);\n }\n\n readFixed32() {\n const val = this.dataView.getUint32(this.pos, true);\n this.pos += 4;\n return val;\n }\n\n readSFixed32() {\n const val = this.dataView.getInt32(this.pos, true);\n this.pos += 4;\n return val;\n }\n\n // 64-bit int handling is based on github.com/dpw/node-buffer-more-ints (MIT-licensed)\n\n readFixed64() {\n const val = this.dataView.getUint32(this.pos, true) + this.dataView.getUint32(this.pos + 4, true) * SHIFT_LEFT_32;\n this.pos += 8;\n return val;\n }\n\n readSFixed64() {\n const val = this.dataView.getUint32(this.pos, true) + this.dataView.getInt32(this.pos + 4, true) * SHIFT_LEFT_32;\n this.pos += 8;\n return val;\n }\n\n readFloat() {\n const val = this.dataView.getFloat32(this.pos, true);\n this.pos += 4;\n return val;\n }\n\n readDouble() {\n const val = this.dataView.getFloat64(this.pos, true);\n this.pos += 8;\n return val;\n }\n\n /**\n * @param {boolean} [isSigned]\n */\n readVarint(isSigned) {\n const buf = this.buf;\n let val, b;\n\n b = buf[this.pos++]; val = b & 0x7f; if (b < 0x80) return val;\n b = buf[this.pos++]; val |= (b & 0x7f) << 7; if (b < 0x80) return val;\n b = buf[this.pos++]; val |= (b & 0x7f) << 14; if (b < 0x80) return val;\n b = buf[this.pos++]; val |= (b & 0x7f) << 21; if (b < 0x80) return val;\n b = buf[this.pos]; val |= (b & 0x0f) << 28;\n\n return readVarintRemainder(val, isSigned, this);\n }\n\n readVarint64() { // for compatibility with v2.0.1\n return this.readVarint(true);\n }\n\n readSVarint() {\n const num = this.readVarint();\n return num % 2 === 1 ? (num + 1) / -2 : num / 2; // zigzag encoding\n }\n\n readBoolean() {\n return Boolean(this.readVarint());\n }\n\n readString() {\n const end = this.readVarint() + this.pos;\n const pos = this.pos;\n this.pos = end;\n\n if (end - pos >= TEXT_DECODER_MIN_LENGTH && utf8TextDecoder) {\n // longer strings are fast with the built-in browser TextDecoder API\n return utf8TextDecoder.decode(this.buf.subarray(pos, end));\n }\n // short strings are fast with our custom implementation\n return readUtf8(this.buf, pos, end);\n }\n\n readBytes() {\n const end = this.readVarint() + this.pos,\n buffer = this.buf.subarray(this.pos, end);\n this.pos = end;\n return buffer;\n }\n\n // verbose for performance reasons; doesn't affect gzipped size\n\n /**\n * @param {number[]} [arr]\n * @param {boolean} [isSigned]\n */\n readPackedVarint(arr = [], isSigned) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readVarint(isSigned));\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedSVarint(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readSVarint());\n return arr;\n }\n /** @param {boolean[]} [arr] */\n readPackedBoolean(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readBoolean());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedFloat(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readFloat());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedDouble(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readDouble());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedFixed32(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readFixed32());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedSFixed32(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readSFixed32());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedFixed64(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readFixed64());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedSFixed64(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readSFixed64());\n return arr;\n }\n readPackedEnd() {\n return this.type === PBF_BYTES ? this.readVarint() + this.pos : this.pos + 1;\n }\n\n /** @param {number} val */\n skip(val) {\n const type = val & 0x7;\n if (type === PBF_VARINT) while (this.buf[this.pos++] > 0x7f) {}\n else if (type === PBF_BYTES) this.pos = this.readVarint() + this.pos;\n else if (type === PBF_FIXED32) this.pos += 4;\n else if (type === PBF_FIXED64) this.pos += 8;\n else throw new Error(`Unimplemented type: ${type}`);\n }\n\n // === WRITING =================================================================\n\n /**\n * @param {number} tag\n * @param {number} type\n */\n writeTag(tag, type) {\n this.writeVarint((tag << 3) | type);\n }\n\n /** @param {number} min */\n realloc(min) {\n let length = this.length || 16;\n\n while (length < this.pos + min) length *= 2;\n\n if (length !== this.length) {\n const buf = new Uint8Array(length);\n buf.set(this.buf);\n this.buf = buf;\n this.dataView = new DataView(buf.buffer);\n this.length = length;\n }\n }\n\n finish() {\n this.length = this.pos;\n this.pos = 0;\n return this.buf.subarray(0, this.length);\n }\n\n /** @param {number} val */\n writeFixed32(val) {\n this.realloc(4);\n this.dataView.setInt32(this.pos, val, true);\n this.pos += 4;\n }\n\n /** @param {number} val */\n writeSFixed32(val) {\n this.realloc(4);\n this.dataView.setInt32(this.pos, val, true);\n this.pos += 4;\n }\n\n /** @param {number} val */\n writeFixed64(val) {\n this.realloc(8);\n this.dataView.setInt32(this.pos, val & -1, true);\n this.dataView.setInt32(this.pos + 4, Math.floor(val * SHIFT_RIGHT_32), true);\n this.pos += 8;\n }\n\n /** @param {number} val */\n writeSFixed64(val) {\n this.realloc(8);\n this.dataView.setInt32(this.pos, val & -1, true);\n this.dataView.setInt32(this.pos + 4, Math.floor(val * SHIFT_RIGHT_32), true);\n this.pos += 8;\n }\n\n /** @param {number} val */\n writeVarint(val) {\n val = +val || 0;\n\n if (val > 0xfffffff || val < 0) {\n writeBigVarint(val, this);\n return;\n }\n\n this.realloc(4);\n\n this.buf[this.pos++] = val & 0x7f | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;\n this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;\n this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;\n this.buf[this.pos++] = (val >>> 7) & 0x7f;\n }\n\n /** @param {number} val */\n writeSVarint(val) {\n this.writeVarint(val < 0 ? -val * 2 - 1 : val * 2);\n }\n\n /** @param {boolean} val */\n writeBoolean(val) {\n this.writeVarint(+val);\n }\n\n /** @param {string} str */\n writeString(str) {\n str = String(str);\n this.realloc(str.length * 4);\n\n this.pos++; // reserve 1 byte for short string length\n\n const startPos = this.pos;\n // write the string directly to the buffer and see how much was written\n this.pos = writeUtf8(this.buf, str, this.pos);\n const len = this.pos - startPos;\n\n if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);\n\n // finally, write the message length in the reserved place and restore the position\n this.pos = startPos - 1;\n this.writeVarint(len);\n this.pos += len;\n }\n\n /** @param {number} val */\n writeFloat(val) {\n this.realloc(4);\n this.dataView.setFloat32(this.pos, val, true);\n this.pos += 4;\n }\n\n /** @param {number} val */\n writeDouble(val) {\n this.realloc(8);\n this.dataView.setFloat64(this.pos, val, true);\n this.pos += 8;\n }\n\n /** @param {Uint8Array} buffer */\n writeBytes(buffer) {\n const len = buffer.length;\n this.writeVarint(len);\n this.realloc(len);\n for (let i = 0; i < len; i++) this.buf[this.pos++] = buffer[i];\n }\n\n /**\n * @template T\n * @param {(obj: T, pbf: Pbf) => void} fn\n * @param {T} obj\n */\n writeRawMessage(fn, obj) {\n this.pos++; // reserve 1 byte for short message length\n\n // write the message directly to the buffer and see how much was written\n const startPos = this.pos;\n fn(obj, this);\n const len = this.pos - startPos;\n\n if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);\n\n // finally, write the message length in the reserved place and restore the position\n this.pos = startPos - 1;\n this.writeVarint(len);\n this.pos += len;\n }\n\n /**\n * @template T\n * @param {number} tag\n * @param {(obj: T, pbf: Pbf) => void} fn\n * @param {T} obj\n */\n writeMessage(tag, fn, obj) {\n this.writeTag(tag, PBF_BYTES);\n this.writeRawMessage(fn, obj);\n }\n\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedVarint(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedVarint, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedSVarint(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedSVarint, arr);\n }\n /**\n * @param {number} tag\n * @param {boolean[]} arr\n */\n writePackedBoolean(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedBoolean, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedFloat(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedFloat, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedDouble(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedDouble, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedFixed32(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedFixed32, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedSFixed32(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedSFixed32, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedFixed64(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedFixed64, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedSFixed64(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedSFixed64, arr);\n }\n\n /**\n * @param {number} tag\n * @param {Uint8Array} buffer\n */\n writeBytesField(tag, buffer) {\n this.writeTag(tag, PBF_BYTES);\n this.writeBytes(buffer);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeFixed32Field(tag, val) {\n this.writeTag(tag, PBF_FIXED32);\n this.writeFixed32(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeSFixed32Field(tag, val) {\n this.writeTag(tag, PBF_FIXED32);\n this.writeSFixed32(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeFixed64Field(tag, val) {\n this.writeTag(tag, PBF_FIXED64);\n this.writeFixed64(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeSFixed64Field(tag, val) {\n this.writeTag(tag, PBF_FIXED64);\n this.writeSFixed64(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeVarintField(tag, val) {\n this.writeTag(tag, PBF_VARINT);\n this.writeVarint(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeSVarintField(tag, val) {\n this.writeTag(tag, PBF_VARINT);\n this.writeSVarint(val);\n }\n /**\n * @param {number} tag\n * @param {string} str\n */\n writeStringField(tag, str) {\n this.writeTag(tag, PBF_BYTES);\n this.writeString(str);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeFloatField(tag, val) {\n this.writeTag(tag, PBF_FIXED32);\n this.writeFloat(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeDoubleField(tag, val) {\n this.writeTag(tag, PBF_FIXED64);\n this.writeDouble(val);\n }\n /**\n * @param {number} tag\n * @param {boolean} val\n */\n writeBooleanField(tag, val) {\n this.writeVarintField(tag, +val);\n }\n};\n\n/**\n * @param {number} l\n * @param {boolean | undefined} s\n * @param {Pbf} p\n */\nfunction readVarintRemainder(l, s, p) {\n const buf = p.buf;\n let h, b;\n\n b = buf[p.pos++]; h = (b & 0x70) >> 4; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 3; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 10; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 17; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 24; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x01) << 31; if (b < 0x80) return toNum(l, h, s);\n\n throw new Error('Expected varint not more than 10 bytes');\n}\n\n/**\n * @param {number} low\n * @param {number} high\n * @param {boolean} [isSigned]\n */\nfunction toNum(low, high, isSigned) {\n return isSigned ? high * 0x100000000 + (low >>> 0) : ((high >>> 0) * 0x100000000) + (low >>> 0);\n}\n\n/**\n * @param {number} val\n * @param {Pbf} pbf\n */\nfunction writeBigVarint(val, pbf) {\n let low, high;\n\n if (val >= 0) {\n low = (val % 0x100000000) | 0;\n high = (val / 0x100000000) | 0;\n } else {\n low = ~(-val % 0x100000000);\n high = ~(-val / 0x100000000);\n\n if (low ^ 0xffffffff) {\n low = (low + 1) | 0;\n } else {\n low = 0;\n high = (high + 1) | 0;\n }\n }\n\n if (val >= 0x10000000000000000 || val < -0x10000000000000000) {\n throw new Error('Given varint doesn\\'t fit into 10 bytes');\n }\n\n pbf.realloc(10);\n\n writeBigVarintLow(low, high, pbf);\n writeBigVarintHigh(high, pbf);\n}\n\n/**\n * @param {number} high\n * @param {number} low\n * @param {Pbf} pbf\n */\nfunction writeBigVarintLow(low, high, pbf) {\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos] = low & 0x7f;\n}\n\n/**\n * @param {number} high\n * @param {Pbf} pbf\n */\nfunction writeBigVarintHigh(high, pbf) {\n const lsb = (high & 0x07) << 4;\n\n pbf.buf[pbf.pos++] |= lsb | ((high >>>= 3) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f;\n}\n\n/**\n * @param {number} startPos\n * @param {number} len\n * @param {Pbf} pbf\n */\nfunction makeRoomForExtraLength(startPos, len, pbf) {\n const extraLen =\n len <= 0x3fff ? 1 :\n len <= 0x1fffff ? 2 :\n len <= 0xfffffff ? 3 : Math.floor(Math.log(len) / (Math.LN2 * 7));\n\n // if 1 byte isn't enough for encoding message length, shift the data to the right\n pbf.realloc(extraLen);\n for (let i = pbf.pos - 1; i >= startPos; i--) pbf.buf[i + extraLen] = pbf.buf[i];\n}\n\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedVarint(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeVarint(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedSVarint(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeSVarint(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedFloat(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeFloat(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedDouble(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeDouble(arr[i]);\n}\n/**\n * @param {boolean[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedBoolean(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeBoolean(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedFixed32(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeFixed32(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedSFixed32(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeSFixed32(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedFixed64(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeFixed64(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedSFixed64(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeSFixed64(arr[i]);\n}\n\n// Buffer code below from https://github.com/feross/buffer, MIT-licensed\n\n/**\n * @param {Uint8Array} buf\n * @param {number} pos\n * @param {number} end\n */\nfunction readUtf8(buf, pos, end) {\n let str = '';\n let i = pos;\n\n while (i < end) {\n const b0 = buf[i];\n let c = null; // codepoint\n let bytesPerSequence =\n b0 > 0xEF ? 4 :\n b0 > 0xDF ? 3 :\n b0 > 0xBF ? 2 : 1;\n\n if (i + bytesPerSequence > end) break;\n\n let b1, b2, b3;\n\n if (bytesPerSequence === 1) {\n if (b0 < 0x80) {\n c = b0;\n }\n } else if (bytesPerSequence === 2) {\n b1 = buf[i + 1];\n if ((b1 & 0xC0) === 0x80) {\n c = (b0 & 0x1F) << 0x6 | (b1 & 0x3F);\n if (c <= 0x7F) {\n c = null;\n }\n }\n } else if (bytesPerSequence === 3) {\n b1 = buf[i + 1];\n b2 = buf[i + 2];\n if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80) {\n c = (b0 & 0xF) << 0xC | (b1 & 0x3F) << 0x6 | (b2 & 0x3F);\n if (c <= 0x7FF || (c >= 0xD800 && c <= 0xDFFF)) {\n c = null;\n }\n }\n } else if (bytesPerSequence === 4) {\n b1 = buf[i + 1];\n b2 = buf[i + 2];\n b3 = buf[i + 3];\n if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80 && (b3 & 0xC0) === 0x80) {\n c = (b0 & 0xF) << 0x12 | (b1 & 0x3F) << 0xC | (b2 & 0x3F) << 0x6 | (b3 & 0x3F);\n if (c <= 0xFFFF || c >= 0x110000) {\n c = null;\n }\n }\n }\n\n if (c === null) {\n c = 0xFFFD;\n bytesPerSequence = 1;\n\n } else if (c > 0xFFFF) {\n c -= 0x10000;\n str += String.fromCharCode(c >>> 10 & 0x3FF | 0xD800);\n c = 0xDC00 | c & 0x3FF;\n }\n\n str += String.fromCharCode(c);\n i += bytesPerSequence;\n }\n\n return str;\n}\n\n/**\n * @param {Uint8Array} buf\n * @param {string} str\n * @param {number} pos\n */\nfunction writeUtf8(buf, str, pos) {\n for (let i = 0, c, lead; i < str.length; i++) {\n c = str.charCodeAt(i); // code point\n\n if (c > 0xD7FF && c < 0xE000) {\n if (lead) {\n if (c < 0xDC00) {\n buf[pos++] = 0xEF;\n buf[pos++] = 0xBF;\n buf[pos++] = 0xBD;\n lead = c;\n continue;\n } else {\n c = lead - 0xD800 << 10 | c - 0xDC00 | 0x10000;\n lead = null;\n }\n } else {\n if (c > 0xDBFF || (i + 1 === str.length)) {\n buf[pos++] = 0xEF;\n buf[pos++] = 0xBF;\n buf[pos++] = 0xBD;\n } else {\n lead = c;\n }\n continue;\n }\n } else if (lead) {\n buf[pos++] = 0xEF;\n buf[pos++] = 0xBF;\n buf[pos++] = 0xBD;\n lead = null;\n }\n\n if (c < 0x80) {\n buf[pos++] = c;\n } else {\n if (c < 0x800) {\n buf[pos++] = c >> 0x6 | 0xC0;\n } else {\n if (c < 0x10000) {\n buf[pos++] = c >> 0xC | 0xE0;\n } else {\n buf[pos++] = c >> 0x12 | 0xF0;\n buf[pos++] = c >> 0xC & 0x3F | 0x80;\n }\n buf[pos++] = c >> 0x6 & 0x3F | 0x80;\n }\n buf[pos++] = c & 0x3F | 0x80;\n }\n }\n return pos;\n}\n", "/**\n * A standalone point geometry with useful accessor, comparison, and\n * modification methods.\n *\n * @class\n * @param {number} x the x-coordinate. This could be longitude or screen pixels, or any other sort of unit.\n * @param {number} y the y-coordinate. This could be latitude or screen pixels, or any other sort of unit.\n *\n * @example\n * const point = new Point(-77, 38);\n */\nexport default function Point(x, y) {\n this.x = x;\n this.y = y;\n}\n\nPoint.prototype = {\n /**\n * Clone this point, returning a new point that can be modified\n * without affecting the old one.\n * @return {Point} the clone\n */\n clone() { return new Point(this.x, this.y); },\n\n /**\n * Add this point's x & y coordinates to another point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n add(p) { return this.clone()._add(p); },\n\n /**\n * Subtract this point's x & y coordinates to from point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n sub(p) { return this.clone()._sub(p); },\n\n /**\n * Multiply this point's x & y coordinates by point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n multByPoint(p) { return this.clone()._multByPoint(p); },\n\n /**\n * Divide this point's x & y coordinates by point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n divByPoint(p) { return this.clone()._divByPoint(p); },\n\n /**\n * Multiply this point's x & y coordinates by a factor,\n * yielding a new point.\n * @param {number} k factor\n * @return {Point} output point\n */\n mult(k) { return this.clone()._mult(k); },\n\n /**\n * Divide this point's x & y coordinates by a factor,\n * yielding a new point.\n * @param {number} k factor\n * @return {Point} output point\n */\n div(k) { return this.clone()._div(k); },\n\n /**\n * Rotate this point around the 0, 0 origin by an angle a,\n * given in radians\n * @param {number} a angle to rotate around, in radians\n * @return {Point} output point\n */\n rotate(a) { return this.clone()._rotate(a); },\n\n /**\n * Rotate this point around p point by an angle a,\n * given in radians\n * @param {number} a angle to rotate around, in radians\n * @param {Point} p Point to rotate around\n * @return {Point} output point\n */\n rotateAround(a, p) { return this.clone()._rotateAround(a, p); },\n\n /**\n * Multiply this point by a 4x1 transformation matrix\n * @param {[number, number, number, number]} m transformation matrix\n * @return {Point} output point\n */\n matMult(m) { return this.clone()._matMult(m); },\n\n /**\n * Calculate this point but as a unit vector from 0, 0, meaning\n * that the distance from the resulting point to the 0, 0\n * coordinate will be equal to 1 and the angle from the resulting\n * point to the 0, 0 coordinate will be the same as before.\n * @return {Point} unit vector point\n */\n unit() { return this.clone()._unit(); },\n\n /**\n * Compute a perpendicular point, where the new y coordinate\n * is the old x coordinate and the new x coordinate is the old y\n * coordinate multiplied by -1\n * @return {Point} perpendicular point\n */\n perp() { return this.clone()._perp(); },\n\n /**\n * Return a version of this point with the x & y coordinates\n * rounded to integers.\n * @return {Point} rounded point\n */\n round() { return this.clone()._round(); },\n\n /**\n * Return the magnitude of this point: this is the Euclidean\n * distance from the 0, 0 coordinate to this point's x and y\n * coordinates.\n * @return {number} magnitude\n */\n mag() {\n return Math.sqrt(this.x * this.x + this.y * this.y);\n },\n\n /**\n * Judge whether this point is equal to another point, returning\n * true or false.\n * @param {Point} other the other point\n * @return {boolean} whether the points are equal\n */\n equals(other) {\n return this.x === other.x &&\n this.y === other.y;\n },\n\n /**\n * Calculate the distance from this point to another point\n * @param {Point} p the other point\n * @return {number} distance\n */\n dist(p) {\n return Math.sqrt(this.distSqr(p));\n },\n\n /**\n * Calculate the distance from this point to another point,\n * without the square root step. Useful if you're comparing\n * relative distances.\n * @param {Point} p the other point\n * @return {number} distance\n */\n distSqr(p) {\n const dx = p.x - this.x,\n dy = p.y - this.y;\n return dx * dx + dy * dy;\n },\n\n /**\n * Get the angle from the 0, 0 coordinate to this point, in radians\n * coordinates.\n * @return {number} angle\n */\n angle() {\n return Math.atan2(this.y, this.x);\n },\n\n /**\n * Get the angle from this point to another point, in radians\n * @param {Point} b the other point\n * @return {number} angle\n */\n angleTo(b) {\n return Math.atan2(this.y - b.y, this.x - b.x);\n },\n\n /**\n * Get the angle between this point and another point, in radians\n * @param {Point} b the other point\n * @return {number} angle\n */\n angleWith(b) {\n return this.angleWithSep(b.x, b.y);\n },\n\n /**\n * Find the angle of the two vectors, solving the formula for\n * the cross product a x b = |a||b|sin(\u03B8) for \u03B8.\n * @param {number} x the x-coordinate\n * @param {number} y the y-coordinate\n * @return {number} the angle in radians\n */\n angleWithSep(x, y) {\n return Math.atan2(\n this.x * y - this.y * x,\n this.x * x + this.y * y);\n },\n\n /** @param {[number, number, number, number]} m */\n _matMult(m) {\n const x = m[0] * this.x + m[1] * this.y,\n y = m[2] * this.x + m[3] * this.y;\n this.x = x;\n this.y = y;\n return this;\n },\n\n /** @param {Point} p */\n _add(p) {\n this.x += p.x;\n this.y += p.y;\n return this;\n },\n\n /** @param {Point} p */\n _sub(p) {\n this.x -= p.x;\n this.y -= p.y;\n return this;\n },\n\n /** @param {number} k */\n _mult(k) {\n this.x *= k;\n this.y *= k;\n return this;\n },\n\n /** @param {number} k */\n _div(k) {\n this.x /= k;\n this.y /= k;\n return this;\n },\n\n /** @param {Point} p */\n _multByPoint(p) {\n this.x *= p.x;\n this.y *= p.y;\n return this;\n },\n\n /** @param {Point} p */\n _divByPoint(p) {\n this.x /= p.x;\n this.y /= p.y;\n return this;\n },\n\n _unit() {\n this._div(this.mag());\n return this;\n },\n\n _perp() {\n const y = this.y;\n this.y = this.x;\n this.x = -y;\n return this;\n },\n\n /** @param {number} angle */\n _rotate(angle) {\n const cos = Math.cos(angle),\n sin = Math.sin(angle),\n x = cos * this.x - sin * this.y,\n y = sin * this.x + cos * this.y;\n this.x = x;\n this.y = y;\n return this;\n },\n\n /**\n * @param {number} angle\n * @param {Point} p\n */\n _rotateAround(angle, p) {\n const cos = Math.cos(angle),\n sin = Math.sin(angle),\n x = p.x + cos * (this.x - p.x) - sin * (this.y - p.y),\n y = p.y + sin * (this.x - p.x) + cos * (this.y - p.y);\n this.x = x;\n this.y = y;\n return this;\n },\n\n _round() {\n this.x = Math.round(this.x);\n this.y = Math.round(this.y);\n return this;\n },\n\n constructor: Point\n};\n\n/**\n * Construct a point from an array if necessary, otherwise if the input\n * is already a Point, return it unchanged.\n * @param {Point | [number, number] | {x: number, y: number}} p input value\n * @return {Point} constructed point.\n * @example\n * // this\n * var point = Point.convert([0, 1]);\n * // is equivalent to\n * var point = new Point(0, 1);\n */\nPoint.convert = function (p) {\n if (p instanceof Point) {\n return /** @type {Point} */ (p);\n }\n if (Array.isArray(p)) {\n return new Point(+p[0], +p[1]);\n }\n if (p.x !== undefined && p.y !== undefined) {\n return new Point(+p.x, +p.y);\n }\n throw new Error('Expected [x, y] or {x, y} point format');\n};\n", "\nimport Point from '@mapbox/point-geometry';\n\n/** @import Pbf from 'pbf' */\n/** @import {Feature} from 'geojson' */\n\nexport class VectorTileFeature {\n /**\n * @param {Pbf} pbf\n * @param {number} end\n * @param {number} extent\n * @param {string[]} keys\n * @param {(number | string | boolean)[]} values\n */\n constructor(pbf, end, extent, keys, values) {\n // Public\n\n /** @type {Record} */\n this.properties = {};\n\n this.extent = extent;\n /** @type {0 | 1 | 2 | 3} */\n this.type = 0;\n\n /** @type {number | undefined} */\n this.id = undefined;\n\n /** @private */\n this._pbf = pbf;\n /** @private */\n this._geometry = -1;\n /** @private */\n this._keys = keys;\n /** @private */\n this._values = values;\n\n pbf.readFields(readFeature, this, end);\n }\n\n loadGeometry() {\n const pbf = this._pbf;\n pbf.pos = this._geometry;\n\n const end = pbf.readVarint() + pbf.pos;\n\n /** @type Point[][] */\n const lines = [];\n\n /** @type Point[] | undefined */\n let line;\n\n let cmd = 1;\n let length = 0;\n let x = 0;\n let y = 0;\n\n while (pbf.pos < end) {\n if (length <= 0) {\n const cmdLen = pbf.readVarint();\n cmd = cmdLen & 0x7;\n length = cmdLen >> 3;\n }\n\n length--;\n\n if (cmd === 1 || cmd === 2) {\n x += pbf.readSVarint();\n y += pbf.readSVarint();\n\n if (cmd === 1) { // moveTo\n if (line) lines.push(line);\n line = [];\n }\n\n if (line) line.push(new Point(x, y));\n\n } else if (cmd === 7) {\n\n // Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90\n if (line) {\n line.push(line[0].clone()); // closePolygon\n }\n\n } else {\n throw new Error(`unknown command ${cmd}`);\n }\n }\n\n if (line) lines.push(line);\n\n return lines;\n }\n\n bbox() {\n const pbf = this._pbf;\n pbf.pos = this._geometry;\n\n const end = pbf.readVarint() + pbf.pos;\n let cmd = 1,\n length = 0,\n x = 0,\n y = 0,\n x1 = Infinity,\n x2 = -Infinity,\n y1 = Infinity,\n y2 = -Infinity;\n\n while (pbf.pos < end) {\n if (length <= 0) {\n const cmdLen = pbf.readVarint();\n cmd = cmdLen & 0x7;\n length = cmdLen >> 3;\n }\n\n length--;\n\n if (cmd === 1 || cmd === 2) {\n x += pbf.readSVarint();\n y += pbf.readSVarint();\n if (x < x1) x1 = x;\n if (x > x2) x2 = x;\n if (y < y1) y1 = y;\n if (y > y2) y2 = y;\n\n } else if (cmd !== 7) {\n throw new Error(`unknown command ${cmd}`);\n }\n }\n\n return [x1, y1, x2, y2];\n }\n\n /**\n * @param {number} x\n * @param {number} y\n * @param {number} z\n * @return {Feature}\n */\n toGeoJSON(x, y, z) {\n const size = this.extent * Math.pow(2, z),\n x0 = this.extent * x,\n y0 = this.extent * y,\n vtCoords = this.loadGeometry();\n\n /** @param {Point} p */\n function projectPoint(p) {\n return [\n (p.x + x0) * 360 / size - 180,\n 360 / Math.PI * Math.atan(Math.exp((1 - (p.y + y0) * 2 / size) * Math.PI)) - 90\n ];\n }\n\n /** @param {Point[]} line */\n function projectLine(line) {\n return line.map(projectPoint);\n }\n\n /** @type {Feature[\"geometry\"]} */\n let geometry;\n\n if (this.type === 1) {\n const points = [];\n for (const line of vtCoords) {\n points.push(line[0]);\n }\n const coordinates = projectLine(points);\n geometry = points.length === 1 ?\n {type: 'Point', coordinates: coordinates[0]} :\n {type: 'MultiPoint', coordinates};\n\n } else if (this.type === 2) {\n\n const coordinates = vtCoords.map(projectLine);\n geometry = coordinates.length === 1 ?\n {type: 'LineString', coordinates: coordinates[0]} :\n {type: 'MultiLineString', coordinates};\n\n } else if (this.type === 3) {\n const polygons = classifyRings(vtCoords);\n const coordinates = [];\n for (const polygon of polygons) {\n coordinates.push(polygon.map(projectLine));\n }\n geometry = coordinates.length === 1 ?\n {type: 'Polygon', coordinates: coordinates[0]} :\n {type: 'MultiPolygon', coordinates};\n } else {\n\n throw new Error('unknown feature type');\n }\n\n /** @type {Feature} */\n const result = {\n type: 'Feature',\n geometry,\n properties: this.properties\n };\n\n if (this.id != null) {\n result.id = this.id;\n }\n\n return result;\n }\n}\n\n/** @type {['Unknown', 'Point', 'LineString', 'Polygon']} */\nVectorTileFeature.types = ['Unknown', 'Point', 'LineString', 'Polygon'];\n\n/**\n * @param {number} tag\n * @param {VectorTileFeature} feature\n * @param {Pbf} pbf\n */\nfunction readFeature(tag, feature, pbf) {\n if (tag === 1) feature.id = pbf.readVarint();\n else if (tag === 2) readTag(pbf, feature);\n else if (tag === 3) feature.type = /** @type {0 | 1 | 2 | 3} */ (pbf.readVarint());\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 4) feature._geometry = pbf.pos;\n}\n\n/**\n * @param {Pbf} pbf\n * @param {VectorTileFeature} feature\n */\nfunction readTag(pbf, feature) {\n const end = pbf.readVarint() + pbf.pos;\n\n while (pbf.pos < end) {\n // @ts-expect-error TS2341 deliberately accessing a private property\n const key = feature._keys[pbf.readVarint()];\n // @ts-expect-error TS2341 deliberately accessing a private property\n const value = feature._values[pbf.readVarint()];\n feature.properties[key] = value;\n }\n}\n\n/** classifies an array of rings into polygons with outer rings and holes\n * @param {Point[][]} rings\n */\nexport function classifyRings(rings) {\n const len = rings.length;\n\n if (len <= 1) return [rings];\n\n const polygons = [];\n let polygon, ccw;\n\n for (let i = 0; i < len; i++) {\n const area = signedArea(rings[i]);\n if (area === 0) continue;\n\n if (ccw === undefined) ccw = area < 0;\n\n if (ccw === area < 0) {\n if (polygon) polygons.push(polygon);\n polygon = [rings[i]];\n\n } else if (polygon) {\n polygon.push(rings[i]);\n }\n }\n if (polygon) polygons.push(polygon);\n\n return polygons;\n}\n\n/** @param {Point[]} ring */\nfunction signedArea(ring) {\n let sum = 0;\n for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {\n p1 = ring[i];\n p2 = ring[j];\n sum += (p2.x - p1.x) * (p1.y + p2.y);\n }\n return sum;\n}\n\nexport class VectorTileLayer {\n /**\n * @param {Pbf} pbf\n * @param {number} [end]\n */\n constructor(pbf, end) {\n // Public\n this.version = 1;\n this.name = '';\n this.extent = 4096;\n this.length = 0;\n\n /** @private */\n this._pbf = pbf;\n\n /** @private\n * @type {string[]} */\n this._keys = [];\n\n /** @private\n * @type {(number | string | boolean)[]} */\n this._values = [];\n\n /** @private\n * @type {number[]} */\n this._features = [];\n\n pbf.readFields(readLayer, this, end);\n\n this.length = this._features.length;\n }\n\n /** return feature `i` from this layer as a `VectorTileFeature`\n * @param {number} i\n */\n feature(i) {\n if (i < 0 || i >= this._features.length) throw new Error('feature index out of bounds');\n\n this._pbf.pos = this._features[i];\n\n const end = this._pbf.readVarint() + this._pbf.pos;\n return new VectorTileFeature(this._pbf, end, this.extent, this._keys, this._values);\n }\n}\n\n/**\n * @param {number} tag\n * @param {VectorTileLayer} layer\n * @param {Pbf} pbf\n */\nfunction readLayer(tag, layer, pbf) {\n if (tag === 15) layer.version = pbf.readVarint();\n else if (tag === 1) layer.name = pbf.readString();\n else if (tag === 5) layer.extent = pbf.readVarint();\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 2) layer._features.push(pbf.pos);\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 3) layer._keys.push(pbf.readString());\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 4) layer._values.push(readValueMessage(pbf));\n}\n\n/**\n * @param {Pbf} pbf\n */\nfunction readValueMessage(pbf) {\n let value = null;\n const end = pbf.readVarint() + pbf.pos;\n\n while (pbf.pos < end) {\n const tag = pbf.readVarint() >> 3;\n\n value = tag === 1 ? pbf.readString() :\n tag === 2 ? pbf.readFloat() :\n tag === 3 ? pbf.readDouble() :\n tag === 4 ? pbf.readVarint64() :\n tag === 5 ? pbf.readVarint() :\n tag === 6 ? pbf.readSVarint() :\n tag === 7 ? pbf.readBoolean() : null;\n }\n if (value == null) {\n throw new Error('unknown feature value');\n }\n\n return value;\n}\n\nexport class VectorTile {\n /**\n * @param {Pbf} pbf\n * @param {number} [end]\n */\n constructor(pbf, end) {\n /** @type {Record} */\n this.layers = pbf.readFields(readTile, {}, end);\n }\n}\n\n/**\n * @param {number} tag\n * @param {Record} layers\n * @param {Pbf} pbf\n */\nfunction readTile(tag, layers, pbf) {\n if (tag === 3) {\n const layer = new VectorTileLayer(pbf, pbf.readVarint() + pbf.pos);\n if (layer.length) layers[layer.name] = layer;\n }\n}\n", "import RBush from 'rbush';\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { json as d3_json } from 'd3-fetch';\n\nimport { marked } from 'marked';\nimport Protobuf from 'pbf';\nimport { VectorTile } from '@mapbox/vector-tile';\n\nimport { fileFetcher } from '../core/file_fetcher';\nimport { localizer } from '../core/localizer';\nimport { geoExtent, geoVecAdd } from '../geo';\nimport { QAItem } from '../osm';\nimport { utilRebind, utilTiler, utilQsString } from '../util';\n\nconst tiler = utilTiler();\nconst dispatch = d3_dispatch('loaded');\nconst _tileZoom = 14;\nconst _osmoseUrlRoot = 'https://osmose.openstreetmap.fr/api/0.3';\nlet _osmoseData = { icons: {}, items: [] };\n\n// This gets reassigned if reset\nlet _cache;\n\nfunction abortRequest(controller) {\n if (controller) {\n controller.abort();\n }\n}\n\nfunction abortUnwantedRequests(cache, tiles) {\n Object.keys(cache.inflightTile).forEach(k => {\n let wanted = tiles.find(tile => k === tile.id);\n if (!wanted) {\n abortRequest(cache.inflightTile[k]);\n delete cache.inflightTile[k];\n }\n });\n}\n\nfunction encodeIssueRtree(d) {\n return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };\n}\n\n// Replace or remove QAItem from rtree\nfunction updateRtree(item, replace) {\n _cache.rtree.remove(item, (a, b) => a.data.id === b.data.id);\n\n if (replace) {\n _cache.rtree.insert(item);\n }\n}\n\n// Issues shouldn't obscure each other\nfunction preventCoincident(loc) {\n let coincident = false;\n do {\n // first time, move marker up. after that, move marker right.\n let delta = coincident ? [0.00001, 0] : [0, 0.00001];\n loc = geoVecAdd(loc, delta);\n let bbox = geoExtent(loc).bbox();\n coincident = _cache.rtree.search(bbox).length;\n } while (coincident);\n\n return loc;\n}\n\nexport default {\n title: 'osmose',\n\n init() {\n fileFetcher.get('qa_data')\n .then(d => {\n _osmoseData = d.osmose;\n _osmoseData.items = Object.keys(d.osmose.icons)\n .map(s => s.split('-')[0])\n .reduce((unique, item) => unique.indexOf(item) !== -1 ? unique : [...unique, item], []);\n });\n\n if (!_cache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset() {\n let _strings = {};\n let _colors = {};\n if (_cache) {\n Object.values(_cache.inflightTile).forEach(abortRequest);\n // Strings and colors are static and should not be re-populated\n _strings = _cache.strings;\n _colors = _cache.colors;\n }\n _cache = {\n data: {},\n loadedTile: {},\n inflightTile: {},\n inflightPost: {},\n closed: {},\n rtree: new RBush(),\n strings: _strings,\n colors: _colors\n };\n },\n\n loadIssues(projection) {\n let params = {\n // Tiles return a maximum # of issues\n // So we want to filter our request for only types iD supports\n item: _osmoseData.items\n };\n\n // determine the needed tiles to cover the view\n let tiles = tiler\n .zoomExtent([_tileZoom, _tileZoom])\n .getTiles(projection);\n\n // abort inflight requests that are no longer needed\n abortUnwantedRequests(_cache, tiles);\n\n // issue new requests..\n tiles.forEach(tile => {\n if (_cache.loadedTile[tile.id] || _cache.inflightTile[tile.id]) return;\n\n let [ x, y, z ] = tile.xyz;\n let url = `${_osmoseUrlRoot}/issues/${z}/${x}/${y}.mvt?` + utilQsString(params);\n\n let controller = new AbortController();\n _cache.inflightTile[tile.id] = controller;\n\n fetch(url, { signal: controller.signal })\n .then(data => data.arrayBuffer())\n .then(data => {\n delete _cache.inflightTile[tile.id];\n _cache.loadedTile[tile.id] = true;\n\n var vectorTile = new VectorTile(new Protobuf(data));\n data = vectorTile.layers.issues;\n const features = [];\n for (let i = 0; i < data.length; i++) {\n features.push(data.feature(i).toGeoJSON(x, y, z));\n }\n\n if (features.length > 0) {\n features.forEach(issue => {\n const { item, class: cl, uuid: id } = issue.properties;\n /* Osmose issues are uniquely identified by a unique\n `item` and `class` combination (both integer values) */\n const itemType = `${item}-${cl}`;\n\n // Filter out unsupported issue types (some are too specific or advanced)\n if (itemType in _osmoseData.icons) {\n let loc = issue.geometry.coordinates; // lon, lat\n loc = preventCoincident(loc);\n\n let d = new QAItem(loc, this, itemType, id, { item });\n\n // Setting elems here prevents UI detail requests\n if (item === 8300 || item === 8360) {\n d.elems = [];\n }\n\n _cache.data[d.id] = d;\n _cache.rtree.insert(encodeIssueRtree(d));\n }\n });\n }\n\n dispatch.call('loaded');\n })\n .catch(() => {\n delete _cache.inflightTile[tile.id];\n _cache.loadedTile[tile.id] = true;\n });\n });\n },\n\n loadIssueDetail(issue) {\n // Issue details only need to be fetched once\n if (issue.elems !== undefined) {\n return Promise.resolve(issue);\n }\n\n const url = `${_osmoseUrlRoot}/issue/${issue.id}?langs=${localizer.localeCode()}`;\n const cacheDetails = data => {\n // Associated elements used for highlighting\n // Assign directly for immediate use in the callback\n issue.elems = data.elems.map(e => e.type.substring(0,1) + e.id);\n\n // Some issues have instance specific detail in a subtitle\n issue.detail = data.subtitle ? marked(data.subtitle.auto) : '';\n\n this.replaceItem(issue);\n };\n\n return d3_json(url).then(cacheDetails).then(() => issue);\n },\n\n loadStrings(locale=localizer.localeCode()) {\n const items = Object.keys(_osmoseData.icons);\n\n if (\n locale in _cache.strings\n && Object.keys(_cache.strings[locale]).length === items.length\n ) {\n return Promise.resolve(_cache.strings[locale]);\n }\n\n // May be partially populated already if some requests were successful\n if (!(locale in _cache.strings)) {\n _cache.strings[locale] = {};\n }\n\n // Only need to cache strings for supported issue types\n // Using multiple individual item + class requests to reduce fetched data size\n const allRequests = items.map(itemType => {\n // No need to request data we already have\n if (itemType in _cache.strings[locale]) return null;\n\n const cacheData = data => {\n // Bunch of nested single value arrays of objects\n const [ cat = {items:[]} ] = data.categories;\n const [ item = {class:[]} ] = cat.items;\n const [ cl = null ] = item.class;\n\n // If null default value is reached, data wasn't as expected (or was empty)\n if (!cl) {\n /* eslint-disable no-console */\n console.log(`Osmose strings request (${itemType}) had unexpected data`);\n /* eslint-enable no-console */\n return;\n }\n\n // Cache served item colors to automatically style issue markers later\n const { item: itemInt, color } = item;\n if (/^#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/.test(color)) {\n _cache.colors[itemInt] = color;\n }\n\n // Value of root key will be null if no string exists\n // If string exists, value is an object with key 'auto' for string\n const { title, detail, fix, trap } = cl;\n\n // Osmose titles shouldn't contain markdown\n let issueStrings = {};\n if (title) issueStrings.title = title.auto;\n if (detail) issueStrings.detail = marked(detail.auto);\n if (trap) issueStrings.trap = marked(trap.auto);\n if (fix) issueStrings.fix = marked(fix.auto);\n\n _cache.strings[locale][itemType] = issueStrings;\n };\n\n const [ item, cl ] = itemType.split('-');\n\n // Osmose API falls back to English strings where untranslated or if locale doesn't exist\n const url = `${_osmoseUrlRoot}/items/${item}/class/${cl}?langs=${locale}`;\n\n return d3_json(url).then(cacheData);\n }).filter(Boolean);\n\n return Promise.all(allRequests).then(() => _cache.strings[locale]);\n },\n\n getStrings(itemType, locale=localizer.localeCode()) {\n // No need to fallback to English, Osmose API handles this for us\n return (locale in _cache.strings) ? _cache.strings[locale][itemType] : {};\n },\n\n getColor(itemType) {\n return (itemType in _cache.colors) ? _cache.colors[itemType] : '#FFFFFF';\n },\n\n postUpdate(issue, callback) {\n if (_cache.inflightPost[issue.id]) {\n return callback({ message: 'Issue update already inflight', status: -2 }, issue);\n }\n\n // UI sets the status to either 'done' or 'false'\n const url = `${_osmoseUrlRoot}/issue/${issue.id}/${issue.newStatus}`;\n const controller = new AbortController();\n const after = () => {\n delete _cache.inflightPost[issue.id];\n\n this.removeItem(issue);\n if (issue.newStatus === 'done') {\n // Keep track of the number of issues closed per `item` to tag the changeset\n if (!(issue.item in _cache.closed)) {\n _cache.closed[issue.item] = 0;\n }\n _cache.closed[issue.item] += 1;\n }\n if (callback) callback(null, issue);\n };\n\n _cache.inflightPost[issue.id] = controller;\n\n fetch(url, { signal: controller.signal })\n .then(after)\n .catch(err => {\n delete _cache.inflightPost[issue.id];\n if (callback) callback(err.message);\n });\n },\n\n // Get all cached QAItems covering the viewport\n getItems(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n\n return _cache.rtree.search(bbox).map(d => d.data);\n },\n\n // Get a QAItem from cache\n // NOTE: Don't change method name until UI v3 is merged\n getError(id) {\n return _cache.data[id];\n },\n\n // get the name of the icon to display for this item\n getIcon(itemType) {\n return _osmoseData.icons[itemType];\n },\n\n // Replace a single QAItem in the cache\n replaceItem(item) {\n if (!(item instanceof QAItem) || !item.id) return;\n\n _cache.data[item.id] = item;\n updateRtree(encodeIssueRtree(item), true); // true = replace\n return item;\n },\n\n // Remove a single QAItem from the cache\n removeItem(item) {\n if (!(item instanceof QAItem) || !item.id) return;\n\n delete _cache.data[item.id];\n updateRtree(encodeIssueRtree(item), false); // false = remove\n },\n\n // Used to populate `closed:osmose:*` changeset tags\n getClosedCounts() {\n return _cache.closed;\n },\n\n itemURL(item) {\n return `https://osmose.openstreetmap.fr/en/error/${item.id}`;\n }\n};\n", "import { geoScaleToZoom } from '../geo';\nimport { utilTiler } from './tiler';\nimport type { Projection } from '../geo/raw_mercator';\nimport type RBush from 'rbush';\nimport type { BBox } from 'rbush';\n\nexport interface WithBbox extends BBox {\n data: T;\n}\n\nexport function partitionViewport(projection: Projection) {\n let z = geoScaleToZoom(projection.scale());\n let z2 = (Math.ceil(z * 2) / 2) + 2.5; // round to next 0.5 and add 2.5\n let tiler = utilTiler().zoomExtent([z2, z2]);\n\n return (tiler.getTiles(projection) || []).map(tile => tile.extent);\n}\n\n\n/** no more than `limit` results per partition */\nexport function searchLimited(limit: number | undefined, projection: Projection, rtree: RBush>): T[] {\n limit ||= 5;\n\n return partitionViewport(projection)\n .flatMap((extent) => rtree.search(extent.bbox()).slice(0, limit))\n .map(result => result.data);\n}\n", "/* global mapillary:false */\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport Protobuf from 'pbf';\nimport RBush from 'rbush';\nimport { VectorTile } from '@mapbox/vector-tile';\nimport { geoExtent } from '../geo';\nimport { utilQsString, utilRebind, utilTiler, utilStringQs } from '../util';\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\n\nconst accessToken = 'MLY|4100327730013843|5bb78b81720791946a9a7b956c57b7cf';\nconst apiUrl = 'https://graph.mapillary.com/';\nconst baseTileUrl = 'https://tiles.mapillary.com/maps/vtp';\nconst mapFeatureTileUrl = `${baseTileUrl}/mly_map_feature_point/2/{z}/{x}/{y}?access_token=${accessToken}`;\nconst tileUrl = `${baseTileUrl}/mly1_public/2/{z}/{x}/{y}?access_token=${accessToken}`;\nconst trafficSignTileUrl = `${baseTileUrl}/mly_map_feature_traffic_sign/2/{z}/{x}/{y}?access_token=${accessToken}`;\n\nconst viewercss = 'mapillary-js/mapillary.css';\nconst viewerjs = 'mapillary-js/mapillary.js';\nconst minZoom = 14;\nconst dispatch = d3_dispatch('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'imageChanged');\n\nlet _loadViewerPromise;\nlet _mlyActiveImage;\nlet _mlyCache;\nlet _mlyFallback = false;\nlet _mlyHighlightedDetection;\nlet _mlyShowFeatureDetections = false;\nlet _mlyShowSignDetections = false;\nlet _mlyViewer;\nlet _mlyViewerFilter = ['all'];\nlet _isViewerOpen = false;\n\n\n// Load all data for the specified type from Mapillary vector tiles\nfunction loadTiles(which, url, maxZoom, projection) {\n const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);\n const tiles = tiler.getTiles(projection);\n\n tiles.forEach(function(tile) {\n loadTile(which, url, tile);\n });\n}\n\n\n// Load all data for the specified type from one vector tile\nfunction loadTile(which, url, tile) {\n const cache = _mlyCache.requests;\n const tileId = `${tile.id}-${which}`;\n if (cache.loaded[tileId] || cache.inflight[tileId]) return;\n const controller = new AbortController();\n cache.inflight[tileId] = controller;\n const requestUrl = url.replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n .replace('{z}', tile.xyz[2]);\n\n fetch(requestUrl, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (!data) {\n throw new Error('No Data');\n }\n loadTileDataToCache(data, tile, which);\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n } else if (which === 'signs') {\n dispatch.call('loadedSigns');\n } else if (which === 'points') {\n dispatch.call('loadedMapFeatures');\n }\n })\n .catch(function() {\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n });\n}\n\n\n// Load the data from the vector tile into cache\nfunction loadTileDataToCache(data, tile, which) {\n const vectorTile = new VectorTile(new Protobuf(data));\n let features,\n cache,\n layer,\n i,\n feature,\n loc,\n d;\n\n if (vectorTile.layers.hasOwnProperty('image')) {\n features = [];\n cache = _mlyCache.images;\n layer = vectorTile.layers.image;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n d = {\n service: 'photo',\n loc: loc,\n captured_at: feature.properties.captured_at,\n ca: feature.properties.compass_angle,\n id: feature.properties.id,\n is_pano: feature.properties.is_pano,\n sequence_id: feature.properties.sequence_id,\n };\n cache.forImageId[d.id] = d;\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty('sequence')) {\n cache = _mlyCache.sequences;\n layer = vectorTile.layers.sequence;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n if (cache.lineString[feature.properties.id]) {\n cache.lineString[feature.properties.id].push(feature);\n } else {\n cache.lineString[feature.properties.id] = [feature];\n }\n }\n }\n\n if (vectorTile.layers.hasOwnProperty('point')) {\n features = [];\n cache = _mlyCache[which];\n layer = vectorTile.layers.point;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n\n d = {\n service: 'photo',\n loc: loc,\n id: feature.properties.id,\n first_seen_at: feature.properties.first_seen_at,\n last_seen_at: feature.properties.last_seen_at,\n value: feature.properties.value\n };\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty('traffic_sign')) {\n features = [];\n cache = _mlyCache[which];\n layer = vectorTile.layers.traffic_sign;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n\n d = {\n service: 'photo',\n loc: loc,\n id: feature.properties.id,\n first_seen_at: feature.properties.first_seen_at,\n last_seen_at: feature.properties.last_seen_at,\n value: feature.properties.value\n };\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n}\n\n\n// Get data from the API\nfunction loadData(url) {\n return fetch(url)\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n })\n .then(function(result) {\n if (!result) {\n return [];\n }\n return result.data || [];\n });\n}\n\nexport default {\n // Initialize Mapillary\n init: function() {\n if (!_mlyCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n // Reset cache and state\n reset: function() {\n if (_mlyCache) {\n Object.values(_mlyCache.requests.inflight).forEach(function(request) { request.abort(); });\n }\n\n _mlyCache = {\n images: { rtree: new RBush(), forImageId: {} },\n image_detections: { forImageId: {} },\n signs: { rtree: new RBush() },\n points: { rtree: new RBush() },\n sequences: { rtree: new RBush(), lineString: {} },\n requests: { loaded: {}, inflight: {} }\n };\n\n _mlyActiveImage = null;\n },\n\n // Get visible images\n images: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _mlyCache.images.rtree);\n },\n\n // Get visible traffic signs\n signs: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _mlyCache.signs.rtree);\n },\n\n // Get visible map (point) features\n mapFeatures: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _mlyCache.points.rtree);\n },\n\n // Get cached image by id\n cachedImage: function(imageId) {\n return _mlyCache.images.forImageId[imageId];\n },\n\n // Get visible sequences\n sequences: function(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const sequenceIds = {};\n let lineStrings = [];\n\n _mlyCache.images.rtree.search(bbox)\n .forEach(function(d) {\n if (d.data.sequence_id) {\n sequenceIds[d.data.sequence_id] = true;\n }\n });\n\n Object.keys(sequenceIds).forEach(function(sequenceId) {\n if (_mlyCache.sequences.lineString[sequenceId]) {\n lineStrings = lineStrings.concat(_mlyCache.sequences.lineString[sequenceId]);\n }\n });\n\n return lineStrings;\n },\n\n\n // Load images in the visible area\n loadImages: function(projection) {\n loadTiles('images', tileUrl, 14, projection);\n },\n\n\n // Load traffic signs in the visible area\n loadSigns: function(projection) {\n loadTiles('signs', trafficSignTileUrl, 14, projection);\n },\n\n\n // Load map (point) features in the visible area\n loadMapFeatures: function(projection) {\n loadTiles('points', mapFeatureTileUrl, 14, projection);\n },\n\n\n // Return a promise that resolves when the image viewer (Mapillary JS) library has finished loading\n ensureViewerLoaded: function(context) {\n if (_loadViewerPromise) return _loadViewerPromise;\n\n // add mly-wrapper\n const wrap = context.container().select('.photoviewer')\n .selectAll('.mly-wrapper')\n .data([0]);\n\n wrap.enter()\n .append('div')\n .attr('id', 'ideditor-mly')\n .attr('class', 'photo-wrapper mly-wrapper')\n .classed('hide', true);\n\n const that = this;\n\n _loadViewerPromise = new Promise((resolve, reject) => {\n let loadedCount = 0;\n function loaded() {\n loadedCount += 1;\n\n // wait until both files are loaded\n if (loadedCount === 2) resolve();\n }\n\n const head = d3_select('head');\n\n // load mapillary-viewercss\n head.selectAll('#ideditor-mapillary-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-mapillary-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(viewercss))\n .on('load.serviceMapillary', loaded)\n .on('error.serviceMapillary', function() {\n reject();\n });\n\n // load mapillary-viewerjs\n head.selectAll('#ideditor-mapillary-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-mapillary-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(viewerjs))\n .on('load.serviceMapillary', loaded)\n .on('error.serviceMapillary', function() {\n reject();\n });\n })\n .catch(function() {\n _loadViewerPromise = null;\n })\n .then(function() {\n that.initViewer(context);\n });\n\n return _loadViewerPromise;\n },\n\n\n // Load traffic sign image sprites\n loadSignResources: function(context) {\n context.ui().svgDefs.addSprites(['mapillary-sprite'], false /* don't override colors */ );\n return this;\n },\n\n\n // Load map (point) feature image sprites\n loadObjectResources: function(context) {\n context.ui().svgDefs.addSprites(['mapillary-object-sprite'], false /* don't override colors */ );\n return this;\n },\n\n\n // Remove previous detections in image viewer\n resetTags: function() {\n if (_mlyViewer && !_mlyFallback) {\n _mlyViewer.getComponent('tag').removeAll();\n }\n },\n\n\n // Show map feature detections in image viewer\n showFeatureDetections: function(value) {\n _mlyShowFeatureDetections = value;\n if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {\n this.resetTags();\n }\n },\n\n\n // Show traffic sign detections in image viewer\n showSignDetections: function(value) {\n _mlyShowSignDetections = value;\n if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {\n this.resetTags();\n }\n },\n\n\n // Apply filter to image viewer\n filterViewer: function(context) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n const filter = ['all'];\n\n if (!showsPano) filter.push([ '!=', 'cameraType', 'spherical' ]);\n if (!showsFlat && showsPano) filter.push(['==', 'pano', true]);\n if (fromDate) {\n filter.push(['>=', 'capturedAt', new Date(fromDate).getTime()]);\n }\n if (toDate) {\n filter.push(['>=', 'capturedAt', new Date(toDate).getTime()]);\n }\n\n if (_mlyViewer) {\n _mlyViewer.setFilter(filter);\n }\n _mlyViewerFilter = filter;\n\n return filter;\n },\n\n\n // Make the image viewer visible\n showViewer: function(context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.mly-wrapper.hide').size();\n\n if (isHidden && _mlyViewer) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.mly-wrapper')\n .classed('hide', false);\n\n _mlyViewer.resize();\n }\n\n _isViewerOpen = true;\n return this;\n },\n\n\n // Hide the image viewer and resets map markers\n hideViewer: function(context) {\n _mlyActiveImage = null;\n\n if (!_mlyFallback && _mlyViewer) {\n _mlyViewer.getComponent('sequence').stop();\n }\n\n const viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n this.updateUrlImage(null);\n\n dispatch.call('imageChanged');\n dispatch.call('loadedMapFeatures');\n dispatch.call('loadedSigns');\n\n _isViewerOpen = false;\n\n return this.setStyles(context, null);\n },\n\n\n // Get viewer status\n isViewerOpen: function() {\n return _isViewerOpen;\n },\n\n\n // Update the URL with current image id\n updateUrlImage: function(imageId) {\n const hash = utilStringQs(window.location.hash);\n if (imageId) {\n hash.photo = 'mapillary/' + imageId;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n\n // Highlight the detection in the viewer that is related to the clicked map feature\n highlightDetection: function(detection) {\n if (detection) {\n _mlyHighlightedDetection = detection.id;\n }\n\n return this;\n },\n\n\n // Initialize image viewer (Mapillar JS)\n initViewer: function(context) {\n if (!window.mapillary) return;\n\n const opts = {\n accessToken: accessToken,\n component: {\n cover: false,\n keyboard: false,\n tag: true\n },\n container: 'ideditor-mly',\n };\n\n // Disable components requiring WebGL support\n if (!mapillary.isSupported() && mapillary.isFallbackSupported()) {\n _mlyFallback = true;\n opts.component = {\n cover: false,\n direction: false,\n imagePlane: false,\n keyboard: false,\n mouse: false,\n sequence: false,\n tag: false,\n image: true, // fallback\n navigation: true // fallback\n };\n }\n\n _mlyViewer = new mapillary.Viewer(opts);\n _mlyViewer.on('image', imageChanged.bind(this));\n _mlyViewer.on('bearing', bearingChanged);\n\n if (_mlyViewerFilter) {\n _mlyViewer.setFilter(_mlyViewerFilter);\n }\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.mapillary', function() {\n if (_mlyViewer) _mlyViewer.resize();\n });\n\n // imageChanged: called after the viewer has changed images and is ready.\n function imageChanged(photo) {\n this.resetTags();\n const image = photo.image;\n this.setActiveImage(image);\n this.setStyles(context, null);\n const loc = [image.originalLngLat.lng, image.originalLngLat.lat];\n context.map().centerEase(loc);\n this.updateUrlImage(image.id);\n\n if (_mlyShowFeatureDetections || _mlyShowSignDetections) {\n this.updateDetections(image.id, `${apiUrl}/${image.id}/detections?access_token=${accessToken}&fields=id,image,geometry,value`);\n }\n dispatch.call('imageChanged');\n }\n\n\n // bearingChanged: called when the bearing changes in the image viewer.\n function bearingChanged(e) {\n dispatch.call('bearingChanged', undefined, e);\n }\n },\n\n\n // Move to an image\n selectImage: function(context, imageId) {\n if (_mlyViewer && imageId) {\n _mlyViewer.moveTo(imageId)\n .then(image => this.setActiveImage(image))\n .catch(function(e) {\n console.error('mly3', e); // eslint-disable-line no-console\n });\n }\n\n return this;\n },\n\n\n // Return the currently displayed image\n getActiveImage: function() {\n return _mlyActiveImage;\n },\n\n\n // Return a list of detection objects for the given id\n getDetections: function(id) {\n return loadData(`${apiUrl}/${id}/detections?access_token=${accessToken}&fields=id,value,image`);\n },\n\n\n // Set the currently visible image\n setActiveImage: function(image) {\n if (image) {\n _mlyActiveImage = {\n ca: image.originalCompassAngle,\n id: image.id,\n loc: [image.originalLngLat.lng, image.originalLngLat.lat],\n is_pano: image.cameraType === 'spherical',\n sequence_id: image.sequenceId\n };\n } else {\n _mlyActiveImage = null;\n }\n },\n\n\n // Update the currently highlighted sequence and selected bubble.\n setStyles: function(context, hovered) {\n const hoveredImageId = hovered && hovered.id;\n const hoveredSequenceId = hovered && hovered.sequence_id;\n const selectedSequenceId = _mlyActiveImage && _mlyActiveImage.sequence_id;\n\n context.container().selectAll('.layer-mapillary .viewfield-group')\n .classed('highlighted', function(d) { return (d.sequence_id === selectedSequenceId) || (d.id === hoveredImageId); })\n .classed('hovered', function(d) { return d.id === hoveredImageId; });\n\n context.container().selectAll('.layer-mapillary .sequence')\n .classed('highlighted', function(d) { return d.properties.id === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.id === selectedSequenceId; });\n\n return this;\n },\n\n\n // Get detections for the current image and shows them in the image viewer\n updateDetections: function(imageId, url) {\n if (!_mlyViewer || _mlyFallback) return;\n if (!imageId) return;\n const cache = _mlyCache.image_detections;\n if (cache.forImageId[imageId]) {\n showDetections(_mlyCache.image_detections.forImageId[imageId]);\n } else {\n loadData(url)\n .then(detections => {\n detections.forEach(function(detection) {\n if (!cache.forImageId[imageId]) {\n cache.forImageId[imageId] = [];\n }\n cache.forImageId[imageId].push({\n geometry: detection.geometry,\n id: detection.id,\n image_id: imageId,\n value:detection.value\n });\n });\n\n showDetections(_mlyCache.image_detections.forImageId[imageId] || []);\n });\n }\n\n\n // Create a tag for each detection and shows it in the image viewer\n function showDetections(detections) {\n const tagComponent = _mlyViewer.getComponent('tag');\n detections.forEach(function(data) {\n const tag = makeTag(data);\n if (tag) {\n tagComponent.add([tag]);\n }\n });\n }\n\n\n // Create a Mapillary JS tag object\n function makeTag(data) {\n const valueParts = data.value.split('--');\n if (!valueParts.length) return;\n\n let tag;\n let text;\n let color = 0xffffff;\n\n if (_mlyHighlightedDetection === data.id) {\n color = 0xffff00;\n text = valueParts[1];\n if (text === 'flat' || text === 'discrete' || text === 'sign') {\n text = valueParts[2];\n }\n text = text.replace(/-/g, ' ');\n text = text.charAt(0).toUpperCase() + text.slice(1);\n _mlyHighlightedDetection = null;\n }\n\n var decodedGeometry = window.atob(data.geometry);\n var uintArray = new Uint8Array(decodedGeometry.length);\n for (var i = 0; i < decodedGeometry.length; i++) {\n uintArray[i] = decodedGeometry.charCodeAt(i);\n }\n const tile = new VectorTile(new Protobuf(uintArray.buffer));\n const layer = tile.layers['mpy-or'];\n\n const geometries = layer.feature(0).loadGeometry();\n\n const polygon = geometries.map(ring =>\n ring.map(point =>\n [point.x / layer.extent, point.y / layer.extent]));\n\n tag = new mapillary.OutlineTag(\n data.id,\n new mapillary.PolygonGeometry(polygon[0]),\n {\n text: text,\n textColor: color,\n lineColor: color,\n lineWidth: 2,\n fillColor: color,\n fillOpacity: 0.3,\n }\n );\n\n return tag;\n }\n },\n\n\n // Return the current cache\n cache: function() {\n return _mlyCache;\n }\n};\n", "import { osmAreaKeys as areaKeys } from '../osm/tags';\nimport { utilArrayIntersection } from '../util';\nimport { validationIssue } from '../core/validation';\n\n\nvar buildRuleChecks = function() {\n return {\n equals: function (equals) {\n return function(tags) {\n return Object.keys(equals).every(function(k) {\n return equals[k] === tags[k];\n });\n };\n },\n notEquals: function (notEquals) {\n return function(tags) {\n return Object.keys(notEquals).some(function(k) {\n return notEquals[k] !== tags[k];\n });\n };\n },\n absence: function(absence) {\n return function(tags) {\n return Object.keys(tags).indexOf(absence) === -1;\n };\n },\n presence: function(presence) {\n return function(tags) {\n return Object.keys(tags).indexOf(presence) > -1;\n };\n },\n greaterThan: function(greaterThan) {\n var key = Object.keys(greaterThan)[0];\n var value = greaterThan[key];\n\n return function(tags) {\n return tags[key] > value;\n };\n },\n greaterThanEqual: function(greaterThanEqual) {\n var key = Object.keys(greaterThanEqual)[0];\n var value = greaterThanEqual[key];\n\n return function(tags) {\n return tags[key] >= value;\n };\n },\n lessThan: function(lessThan) {\n var key = Object.keys(lessThan)[0];\n var value = lessThan[key];\n\n return function(tags) {\n return tags[key] < value;\n };\n },\n lessThanEqual: function(lessThanEqual) {\n var key = Object.keys(lessThanEqual)[0];\n var value = lessThanEqual[key];\n\n return function(tags) {\n return tags[key] <= value;\n };\n },\n positiveRegex: function(positiveRegex) {\n var tagKey = Object.keys(positiveRegex)[0];\n var expression = positiveRegex[tagKey].join('|');\n var regex = new RegExp(expression);\n\n return function(tags) {\n return regex.test(tags[tagKey]);\n };\n },\n negativeRegex: function(negativeRegex) {\n var tagKey = Object.keys(negativeRegex)[0];\n var expression = negativeRegex[tagKey].join('|');\n var regex = new RegExp(expression);\n\n return function(tags) {\n return !regex.test(tags[tagKey]);\n };\n }\n };\n};\n\nvar buildLineKeys = function() {\n return {\n highway: {\n rest_area: true,\n services: true\n },\n railway: {\n roundhouse: true,\n station: true,\n traverser: true,\n turntable: true,\n wash: true\n }\n };\n};\n\nexport default {\n init: function() {\n this._ruleChecks = buildRuleChecks();\n this._validationRules = [];\n this._areaKeys = areaKeys;\n this._lineKeys = buildLineKeys();\n },\n\n // list of rules only relevant to tag checks...\n filterRuleChecks: function(selector) {\n var _ruleChecks = this._ruleChecks;\n return Object.keys(selector).reduce(function(rules, key) {\n if (['geometry', 'error', 'warning'].indexOf(key) === -1) {\n rules.push(_ruleChecks[key](selector[key]));\n }\n return rules;\n }, []);\n },\n\n // builds tagMap from mapcss-parse selector object...\n buildTagMap: function(selector) {\n var getRegexValues = function(regexes) {\n return regexes.map(function(regex) {\n return regex.replace(/\\$|\\^/g, '');\n });\n };\n\n var tagMap = Object.keys(selector).reduce(function (expectedTags, key) {\n var values;\n var isRegex = /regex/gi.test(key);\n var isEqual = /equals/gi.test(key);\n\n if (isRegex || isEqual) {\n Object.keys(selector[key]).forEach(function(selectorKey) {\n values = isEqual ? [selector[key][selectorKey]] : getRegexValues(selector[key][selectorKey]);\n\n if (expectedTags.hasOwnProperty(selectorKey)) {\n values = values.concat(expectedTags[selectorKey]);\n }\n\n expectedTags[selectorKey] = values;\n });\n\n } else if (/(greater|less)Than(Equal)?|presence/g.test(key)) {\n var tagKey = /presence/.test(key) ? selector[key] : Object.keys(selector[key])[0];\n\n values = [selector[key][tagKey]];\n\n if (expectedTags.hasOwnProperty(tagKey)) {\n values = values.concat(expectedTags[tagKey]);\n }\n\n expectedTags[tagKey] = values;\n }\n\n return expectedTags;\n }, {});\n\n return tagMap;\n },\n\n // inspired by osmWay#isArea()\n inferGeometry: function(tagMap) {\n var _lineKeys = this._lineKeys;\n var _areaKeys = this._areaKeys;\n\n var keyValueDoesNotImplyArea = function(key) {\n return utilArrayIntersection(tagMap[key], Object.keys(_areaKeys[key])).length > 0;\n };\n var keyValueImpliesLine = function(key) {\n return utilArrayIntersection(tagMap[key], Object.keys(_lineKeys[key])).length > 0;\n };\n\n if (tagMap.hasOwnProperty('area')) {\n if (tagMap.area.indexOf('yes') > -1) {\n return 'area';\n }\n if (tagMap.area.indexOf('no') > -1) {\n return 'line';\n }\n }\n\n for (var key in tagMap) {\n if (key in _areaKeys && !keyValueDoesNotImplyArea(key)) {\n return 'area';\n }\n if (key in _lineKeys && keyValueImpliesLine(key)) {\n return 'area';\n }\n }\n\n return 'line';\n },\n\n // adds from mapcss-parse selector check...\n addRule: function(selector) {\n var rule = {\n // checks relevant to mapcss-selector\n checks: this.filterRuleChecks(selector),\n // true if all conditions for a tag error are true..\n matches: function(entity) {\n return this.checks.every(function(check) {\n return check(entity.tags);\n });\n },\n // borrowed from Way#isArea()\n inferredGeometry: this.inferGeometry(this.buildTagMap(selector), this._areaKeys),\n geometryMatches: function(entity, graph) {\n if (entity.type === 'node' || entity.type === 'relation') {\n return selector.geometry === entity.type;\n } else if (entity.type === 'way') {\n return this.inferredGeometry === entity.geometry(graph);\n }\n },\n // when geometries match and tag matches are present, return a warning...\n findIssues: function (entity, graph, issues) {\n if (this.geometryMatches(entity, graph) && this.matches(entity)) {\n var severity = Object.keys(selector).indexOf('error') > -1\n ? 'error'\n : 'warning';\n var message = selector[severity];\n issues.push(new validationIssue({\n type: 'maprules',\n severity: severity,\n message: function() {\n return message;\n },\n entityIds: [entity.id]\n }));\n }\n }\n };\n this._validationRules.push(rule);\n },\n\n clearRules: function() { this._validationRules = []; },\n\n // returns validationRules...\n validationRules: function() { return this._validationRules; },\n\n // returns ruleChecks\n ruleChecks: function() { return this._ruleChecks; }\n};\n", "import { json as d3_json } from 'd3-fetch';\n\nimport RBush from 'rbush';\nimport { geoExtent } from '../geo';\nimport { utilQsString } from '../util';\nimport { localizer } from '../core';\n\nimport { nominatimApiUrl } from '../../config/id.js';\n\n\nvar apibase = nominatimApiUrl;\nvar _inflight = {};\nvar _nominatimCache;\n\n\nexport default {\n\n init: function() {\n _inflight = {};\n _nominatimCache = new RBush();\n },\n\n reset: function() {\n Object.values(_inflight).forEach(function(controller) { controller.abort(); });\n _inflight = {};\n _nominatimCache = new RBush();\n },\n\n\n countryCode: function (location, callback) {\n this.reverse(location, function(err, result) {\n if (err) {\n return callback(err);\n } else if (result.address) {\n return callback(null, result.address.country_code);\n } else {\n return callback('Unable to geocode', null);\n }\n });\n },\n\n\n reverse: function (loc, callback) {\n var cached = _nominatimCache.search(\n { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1] }\n );\n\n if (cached.length > 0) {\n if (callback) callback(null, cached[0].data);\n return;\n }\n\n var params = { zoom: 13, format: 'json', addressdetails: 1, lat: loc[1], lon: loc[0] };\n var url = apibase + 'reverse?' + utilQsString(params);\n\n if (_inflight[url]) return;\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, {\n signal: controller.signal,\n headers: {\n 'Accept-Language': localizer.localeCodes().join(',')\n }\n })\n .then(function(result) {\n delete _inflight[url];\n if (result && result.error) {\n throw new Error(result.error);\n }\n var extent = geoExtent(loc).padByMeters(200);\n _nominatimCache.insert(Object.assign(extent.bbox(), {data: result}));\n if (callback) callback(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (callback) callback(err.message);\n });\n },\n\n\n search: function (val, callback) {\n const params = {\n q: val,\n limit:10,\n format: 'json'\n };\n var url = apibase + 'search?' + utilQsString(params);\n\n if (_inflight[url]) return;\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, {\n signal: controller.signal,\n headers: {\n 'Accept-Language': localizer.localeCodes().join(',')\n }\n })\n .then(function(result) {\n delete _inflight[url];\n if (result && result.error) {\n throw new Error(result.error);\n }\n if (callback) callback(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (callback) callback(err.message);\n });\n }\n\n};\n", "(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n\ttypeof define === 'function' && define.amd ? define(factory) :\n\t(global.quickselect = factory());\n}(this, (function () { 'use strict';\n\nfunction quickselect(arr, k, left, right, compare) {\n quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);\n}\n\nfunction quickselectStep(arr, k, left, right, compare) {\n\n while (right > left) {\n if (right - left > 600) {\n var n = right - left + 1;\n var m = k - left + 1;\n var z = Math.log(n);\n var s = 0.5 * Math.exp(2 * z / 3);\n var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);\n var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));\n var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));\n quickselectStep(arr, k, newLeft, newRight, compare);\n }\n\n var t = arr[k];\n var i = left;\n var j = right;\n\n swap(arr, left, k);\n if (compare(arr[right], t) > 0) swap(arr, left, right);\n\n while (i < j) {\n swap(arr, i, j);\n i++;\n j--;\n while (compare(arr[i], t) < 0) i++;\n while (compare(arr[j], t) > 0) j--;\n }\n\n if (compare(arr[left], t) === 0) swap(arr, left, j);\n else {\n j++;\n swap(arr, j, right);\n }\n\n if (j <= k) left = j + 1;\n if (k <= j) right = j - 1;\n }\n}\n\nfunction swap(arr, i, j) {\n var tmp = arr[i];\n arr[i] = arr[j];\n arr[j] = tmp;\n}\n\nfunction defaultCompare(a, b) {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\nreturn quickselect;\n\n})));\n", "'use strict';\n\nmodule.exports = rbush;\nmodule.exports.default = rbush;\n\nvar quickselect = require('quickselect');\n\nfunction rbush(maxEntries, format) {\n if (!(this instanceof rbush)) return new rbush(maxEntries, format);\n\n // max entries in a node is 9 by default; min node fill is 40% for best performance\n this._maxEntries = Math.max(4, maxEntries || 9);\n this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));\n\n if (format) {\n this._initFormat(format);\n }\n\n this.clear();\n}\n\nrbush.prototype = {\n\n all: function () {\n return this._all(this.data, []);\n },\n\n search: function (bbox) {\n\n var node = this.data,\n result = [],\n toBBox = this.toBBox;\n\n if (!intersects(bbox, node)) return result;\n\n var nodesToSearch = [],\n i, len, child, childBBox;\n\n while (node) {\n for (i = 0, len = node.children.length; i < len; i++) {\n\n child = node.children[i];\n childBBox = node.leaf ? toBBox(child) : child;\n\n if (intersects(bbox, childBBox)) {\n if (node.leaf) result.push(child);\n else if (contains(bbox, childBBox)) this._all(child, result);\n else nodesToSearch.push(child);\n }\n }\n node = nodesToSearch.pop();\n }\n\n return result;\n },\n\n collides: function (bbox) {\n\n var node = this.data,\n toBBox = this.toBBox;\n\n if (!intersects(bbox, node)) return false;\n\n var nodesToSearch = [],\n i, len, child, childBBox;\n\n while (node) {\n for (i = 0, len = node.children.length; i < len; i++) {\n\n child = node.children[i];\n childBBox = node.leaf ? toBBox(child) : child;\n\n if (intersects(bbox, childBBox)) {\n if (node.leaf || contains(bbox, childBBox)) return true;\n nodesToSearch.push(child);\n }\n }\n node = nodesToSearch.pop();\n }\n\n return false;\n },\n\n load: function (data) {\n if (!(data && data.length)) return this;\n\n if (data.length < this._minEntries) {\n for (var i = 0, len = data.length; i < len; i++) {\n this.insert(data[i]);\n }\n return this;\n }\n\n // recursively build the tree with the given data from scratch using OMT algorithm\n var node = this._build(data.slice(), 0, data.length - 1, 0);\n\n if (!this.data.children.length) {\n // save as is if tree is empty\n this.data = node;\n\n } else if (this.data.height === node.height) {\n // split root if trees have the same height\n this._splitRoot(this.data, node);\n\n } else {\n if (this.data.height < node.height) {\n // swap trees if inserted one is bigger\n var tmpNode = this.data;\n this.data = node;\n node = tmpNode;\n }\n\n // insert the small tree into the large tree at appropriate level\n this._insert(node, this.data.height - node.height - 1, true);\n }\n\n return this;\n },\n\n insert: function (item) {\n if (item) this._insert(item, this.data.height - 1);\n return this;\n },\n\n clear: function () {\n this.data = createNode([]);\n return this;\n },\n\n remove: function (item, equalsFn) {\n if (!item) return this;\n\n var node = this.data,\n bbox = this.toBBox(item),\n path = [],\n indexes = [],\n i, parent, index, goingUp;\n\n // depth-first iterative tree traversal\n while (node || path.length) {\n\n if (!node) { // go up\n node = path.pop();\n parent = path[path.length - 1];\n i = indexes.pop();\n goingUp = true;\n }\n\n if (node.leaf) { // check current node\n index = findItem(item, node.children, equalsFn);\n\n if (index !== -1) {\n // item found, remove the item and condense tree upwards\n node.children.splice(index, 1);\n path.push(node);\n this._condense(path);\n return this;\n }\n }\n\n if (!goingUp && !node.leaf && contains(node, bbox)) { // go down\n path.push(node);\n indexes.push(i);\n i = 0;\n parent = node;\n node = node.children[0];\n\n } else if (parent) { // go right\n i++;\n node = parent.children[i];\n goingUp = false;\n\n } else node = null; // nothing found\n }\n\n return this;\n },\n\n toBBox: function (item) { return item; },\n\n compareMinX: compareNodeMinX,\n compareMinY: compareNodeMinY,\n\n toJSON: function () { return this.data; },\n\n fromJSON: function (data) {\n this.data = data;\n return this;\n },\n\n _all: function (node, result) {\n var nodesToSearch = [];\n while (node) {\n if (node.leaf) result.push.apply(result, node.children);\n else nodesToSearch.push.apply(nodesToSearch, node.children);\n\n node = nodesToSearch.pop();\n }\n return result;\n },\n\n _build: function (items, left, right, height) {\n\n var N = right - left + 1,\n M = this._maxEntries,\n node;\n\n if (N <= M) {\n // reached leaf level; return leaf\n node = createNode(items.slice(left, right + 1));\n calcBBox(node, this.toBBox);\n return node;\n }\n\n if (!height) {\n // target height of the bulk-loaded tree\n height = Math.ceil(Math.log(N) / Math.log(M));\n\n // target number of root entries to maximize storage utilization\n M = Math.ceil(N / Math.pow(M, height - 1));\n }\n\n node = createNode([]);\n node.leaf = false;\n node.height = height;\n\n // split the items into M mostly square tiles\n\n var N2 = Math.ceil(N / M),\n N1 = N2 * Math.ceil(Math.sqrt(M)),\n i, j, right2, right3;\n\n multiSelect(items, left, right, N1, this.compareMinX);\n\n for (i = left; i <= right; i += N1) {\n\n right2 = Math.min(i + N1 - 1, right);\n\n multiSelect(items, i, right2, N2, this.compareMinY);\n\n for (j = i; j <= right2; j += N2) {\n\n right3 = Math.min(j + N2 - 1, right2);\n\n // pack each entry recursively\n node.children.push(this._build(items, j, right3, height - 1));\n }\n }\n\n calcBBox(node, this.toBBox);\n\n return node;\n },\n\n _chooseSubtree: function (bbox, node, level, path) {\n\n var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;\n\n while (true) {\n path.push(node);\n\n if (node.leaf || path.length - 1 === level) break;\n\n minArea = minEnlargement = Infinity;\n\n for (i = 0, len = node.children.length; i < len; i++) {\n child = node.children[i];\n area = bboxArea(child);\n enlargement = enlargedArea(bbox, child) - area;\n\n // choose entry with the least area enlargement\n if (enlargement < minEnlargement) {\n minEnlargement = enlargement;\n minArea = area < minArea ? area : minArea;\n targetNode = child;\n\n } else if (enlargement === minEnlargement) {\n // otherwise choose one with the smallest area\n if (area < minArea) {\n minArea = area;\n targetNode = child;\n }\n }\n }\n\n node = targetNode || node.children[0];\n }\n\n return node;\n },\n\n _insert: function (item, level, isNode) {\n\n var toBBox = this.toBBox,\n bbox = isNode ? item : toBBox(item),\n insertPath = [];\n\n // find the best node for accommodating the item, saving all nodes along the path too\n var node = this._chooseSubtree(bbox, this.data, level, insertPath);\n\n // put the item into the node\n node.children.push(item);\n extend(node, bbox);\n\n // split on node overflow; propagate upwards if necessary\n while (level >= 0) {\n if (insertPath[level].children.length > this._maxEntries) {\n this._split(insertPath, level);\n level--;\n } else break;\n }\n\n // adjust bboxes along the insertion path\n this._adjustParentBBoxes(bbox, insertPath, level);\n },\n\n // split overflowed node into two\n _split: function (insertPath, level) {\n\n var node = insertPath[level],\n M = node.children.length,\n m = this._minEntries;\n\n this._chooseSplitAxis(node, m, M);\n\n var splitIndex = this._chooseSplitIndex(node, m, M);\n\n var newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));\n newNode.height = node.height;\n newNode.leaf = node.leaf;\n\n calcBBox(node, this.toBBox);\n calcBBox(newNode, this.toBBox);\n\n if (level) insertPath[level - 1].children.push(newNode);\n else this._splitRoot(node, newNode);\n },\n\n _splitRoot: function (node, newNode) {\n // split root node\n this.data = createNode([node, newNode]);\n this.data.height = node.height + 1;\n this.data.leaf = false;\n calcBBox(this.data, this.toBBox);\n },\n\n _chooseSplitIndex: function (node, m, M) {\n\n var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index;\n\n minOverlap = minArea = Infinity;\n\n for (i = m; i <= M - m; i++) {\n bbox1 = distBBox(node, 0, i, this.toBBox);\n bbox2 = distBBox(node, i, M, this.toBBox);\n\n overlap = intersectionArea(bbox1, bbox2);\n area = bboxArea(bbox1) + bboxArea(bbox2);\n\n // choose distribution with minimum overlap\n if (overlap < minOverlap) {\n minOverlap = overlap;\n index = i;\n\n minArea = area < minArea ? area : minArea;\n\n } else if (overlap === minOverlap) {\n // otherwise choose distribution with minimum area\n if (area < minArea) {\n minArea = area;\n index = i;\n }\n }\n }\n\n return index;\n },\n\n // sorts node children by the best axis for split\n _chooseSplitAxis: function (node, m, M) {\n\n var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX,\n compareMinY = node.leaf ? this.compareMinY : compareNodeMinY,\n xMargin = this._allDistMargin(node, m, M, compareMinX),\n yMargin = this._allDistMargin(node, m, M, compareMinY);\n\n // if total distributions margin value is minimal for x, sort by minX,\n // otherwise it's already sorted by minY\n if (xMargin < yMargin) node.children.sort(compareMinX);\n },\n\n // total margin of all possible split distributions where each node is at least m full\n _allDistMargin: function (node, m, M, compare) {\n\n node.children.sort(compare);\n\n var toBBox = this.toBBox,\n leftBBox = distBBox(node, 0, m, toBBox),\n rightBBox = distBBox(node, M - m, M, toBBox),\n margin = bboxMargin(leftBBox) + bboxMargin(rightBBox),\n i, child;\n\n for (i = m; i < M - m; i++) {\n child = node.children[i];\n extend(leftBBox, node.leaf ? toBBox(child) : child);\n margin += bboxMargin(leftBBox);\n }\n\n for (i = M - m - 1; i >= m; i--) {\n child = node.children[i];\n extend(rightBBox, node.leaf ? toBBox(child) : child);\n margin += bboxMargin(rightBBox);\n }\n\n return margin;\n },\n\n _adjustParentBBoxes: function (bbox, path, level) {\n // adjust bboxes along the given tree path\n for (var i = level; i >= 0; i--) {\n extend(path[i], bbox);\n }\n },\n\n _condense: function (path) {\n // go through the path, removing empty nodes and updating bboxes\n for (var i = path.length - 1, siblings; i >= 0; i--) {\n if (path[i].children.length === 0) {\n if (i > 0) {\n siblings = path[i - 1].children;\n siblings.splice(siblings.indexOf(path[i]), 1);\n\n } else this.clear();\n\n } else calcBBox(path[i], this.toBBox);\n }\n },\n\n _initFormat: function (format) {\n // data format (minX, minY, maxX, maxY accessors)\n\n // uses eval-type function compilation instead of just accepting a toBBox function\n // because the algorithms are very sensitive to sorting functions performance,\n // so they should be dead simple and without inner calls\n\n var compareArr = ['return a', ' - b', ';'];\n\n this.compareMinX = new Function('a', 'b', compareArr.join(format[0]));\n this.compareMinY = new Function('a', 'b', compareArr.join(format[1]));\n\n this.toBBox = new Function('a',\n 'return {minX: a' + format[0] +\n ', minY: a' + format[1] +\n ', maxX: a' + format[2] +\n ', maxY: a' + format[3] + '};');\n }\n};\n\nfunction findItem(item, items, equalsFn) {\n if (!equalsFn) return items.indexOf(item);\n\n for (var i = 0; i < items.length; i++) {\n if (equalsFn(item, items[i])) return i;\n }\n return -1;\n}\n\n// calculate node's bbox from bboxes of its children\nfunction calcBBox(node, toBBox) {\n distBBox(node, 0, node.children.length, toBBox, node);\n}\n\n// min bounding rectangle of node children from k to p-1\nfunction distBBox(node, k, p, toBBox, destNode) {\n if (!destNode) destNode = createNode(null);\n destNode.minX = Infinity;\n destNode.minY = Infinity;\n destNode.maxX = -Infinity;\n destNode.maxY = -Infinity;\n\n for (var i = k, child; i < p; i++) {\n child = node.children[i];\n extend(destNode, node.leaf ? toBBox(child) : child);\n }\n\n return destNode;\n}\n\nfunction extend(a, b) {\n a.minX = Math.min(a.minX, b.minX);\n a.minY = Math.min(a.minY, b.minY);\n a.maxX = Math.max(a.maxX, b.maxX);\n a.maxY = Math.max(a.maxY, b.maxY);\n return a;\n}\n\nfunction compareNodeMinX(a, b) { return a.minX - b.minX; }\nfunction compareNodeMinY(a, b) { return a.minY - b.minY; }\n\nfunction bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }\nfunction bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }\n\nfunction enlargedArea(a, b) {\n return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *\n (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));\n}\n\nfunction intersectionArea(a, b) {\n var minX = Math.max(a.minX, b.minX),\n minY = Math.max(a.minY, b.minY),\n maxX = Math.min(a.maxX, b.maxX),\n maxY = Math.min(a.maxY, b.maxY);\n\n return Math.max(0, maxX - minX) *\n Math.max(0, maxY - minY);\n}\n\nfunction contains(a, b) {\n return a.minX <= b.minX &&\n a.minY <= b.minY &&\n b.maxX <= a.maxX &&\n b.maxY <= a.maxY;\n}\n\nfunction intersects(a, b) {\n return b.minX <= a.maxX &&\n b.minY <= a.maxY &&\n b.maxX >= a.minX &&\n b.maxY >= a.minY;\n}\n\nfunction createNode(children) {\n return {\n children: children,\n height: 1,\n leaf: true,\n minX: Infinity,\n minY: Infinity,\n maxX: -Infinity,\n maxY: -Infinity\n };\n}\n\n// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;\n// combines selection algorithm with binary divide & conquer approach\n\nfunction multiSelect(arr, left, right, n, compare) {\n var stack = [left, right],\n mid;\n\n while (stack.length) {\n right = stack.pop();\n left = stack.pop();\n\n if (right - left <= n) continue;\n\n mid = left + Math.ceil((right - left) / n / 2) * n;\n quickselect(arr, mid, left, right, compare);\n\n stack.push(left, mid, mid, right);\n }\n}\n", "'use strict';\n\nmodule.exports = lineclip;\n\nlineclip.polyline = lineclip;\nlineclip.polygon = polygonclip;\n\n\n// Cohen-Sutherland line clippign algorithm, adapted to efficiently\n// handle polylines rather than just segments\n\nfunction lineclip(points, bbox, result) {\n\n var len = points.length,\n codeA = bitCode(points[0], bbox),\n part = [],\n i, a, b, codeB, lastCode;\n\n if (!result) result = [];\n\n for (i = 1; i < len; i++) {\n a = points[i - 1];\n b = points[i];\n codeB = lastCode = bitCode(b, bbox);\n\n while (true) {\n\n if (!(codeA | codeB)) { // accept\n part.push(a);\n\n if (codeB !== lastCode) { // segment went outside\n part.push(b);\n\n if (i < len - 1) { // start a new line\n result.push(part);\n part = [];\n }\n } else if (i === len - 1) {\n part.push(b);\n }\n break;\n\n } else if (codeA & codeB) { // trivial reject\n break;\n\n } else if (codeA) { // a outside, intersect with clip edge\n a = intersect(a, b, codeA, bbox);\n codeA = bitCode(a, bbox);\n\n } else { // b outside\n b = intersect(a, b, codeB, bbox);\n codeB = bitCode(b, bbox);\n }\n }\n\n codeA = lastCode;\n }\n\n if (part.length) result.push(part);\n\n return result;\n}\n\n// Sutherland-Hodgeman polygon clipping algorithm\n\nfunction polygonclip(points, bbox) {\n\n var result, edge, prev, prevInside, i, p, inside;\n\n // clip against each side of the clip rectangle\n for (edge = 1; edge <= 8; edge *= 2) {\n result = [];\n prev = points[points.length - 1];\n prevInside = !(bitCode(prev, bbox) & edge);\n\n for (i = 0; i < points.length; i++) {\n p = points[i];\n inside = !(bitCode(p, bbox) & edge);\n\n // if segment goes through the clip window, add an intersection\n if (inside !== prevInside) result.push(intersect(prev, p, edge, bbox));\n\n if (inside) result.push(p); // add a point if it's inside\n\n prev = p;\n prevInside = inside;\n }\n\n points = result;\n\n if (!points.length) break;\n }\n\n return result;\n}\n\n// intersect a segment against one of the 4 lines that make up the bbox\n\nfunction intersect(a, b, edge, bbox) {\n return edge & 8 ? [a[0] + (b[0] - a[0]) * (bbox[3] - a[1]) / (b[1] - a[1]), bbox[3]] : // top\n edge & 4 ? [a[0] + (b[0] - a[0]) * (bbox[1] - a[1]) / (b[1] - a[1]), bbox[1]] : // bottom\n edge & 2 ? [bbox[2], a[1] + (b[1] - a[1]) * (bbox[2] - a[0]) / (b[0] - a[0])] : // right\n edge & 1 ? [bbox[0], a[1] + (b[1] - a[1]) * (bbox[0] - a[0]) / (b[0] - a[0])] : // left\n null;\n}\n\n// bit code reflects the point position relative to the bbox:\n\n// left mid right\n// top 1001 1000 1010\n// mid 0001 0000 0010\n// bottom 0101 0100 0110\n\nfunction bitCode(p, bbox) {\n var code = 0;\n\n if (p[0] < bbox[0]) code |= 1; // left\n else if (p[0] > bbox[2]) code |= 2; // right\n\n if (p[1] < bbox[1]) code |= 4; // bottom\n else if (p[1] > bbox[3]) code |= 8; // top\n\n return code;\n}\n", "'use strict';\n\nvar rbush = require('rbush');\nvar lineclip = require('lineclip');\n\nmodule.exports = whichPolygon;\n\nfunction whichPolygon(data) {\n var bboxes = [];\n for (var i = 0; i < data.features.length; i++) {\n var feature = data.features[i];\n\n // unlocated GeoJSON features can have null `geometry`\n if (!feature.geometry) continue;\n\n var coords = feature.geometry.coordinates;\n\n if (feature.geometry.type === 'Polygon') {\n bboxes.push(treeItem(coords, feature.properties));\n\n } else if (feature.geometry.type === 'MultiPolygon') {\n for (var j = 0; j < coords.length; j++) {\n bboxes.push(treeItem(coords[j], feature.properties));\n }\n }\n }\n\n var tree = rbush().load(bboxes);\n\n function query(p, multi) {\n var output = [],\n result = tree.search({\n minX: p[0],\n minY: p[1],\n maxX: p[0],\n maxY: p[1]\n });\n for (var i = 0; i < result.length; i++) {\n if (insidePolygon(result[i].coords, p)) {\n if (multi)\n output.push(result[i].props);\n else\n return result[i].props;\n }\n }\n return multi && output.length ? output : null;\n }\n\n query.tree = tree;\n query.bbox = function queryBBox(bbox) {\n var output = [];\n var result = tree.search({\n minX: bbox[0],\n minY: bbox[1],\n maxX: bbox[2],\n maxY: bbox[3]\n });\n for (var i = 0; i < result.length; i++) {\n if (polygonIntersectsBBox(result[i].coords, bbox)) {\n output.push(result[i].props);\n }\n }\n return output;\n };\n\n return query;\n}\n\nfunction polygonIntersectsBBox(polygon, bbox) {\n var bboxCenter = [\n (bbox[0] + bbox[2]) / 2,\n (bbox[1] + bbox[3]) / 2\n ];\n if (insidePolygon(polygon, bboxCenter)) return true;\n for (var i = 0; i < polygon.length; i++) {\n if (lineclip(polygon[i], bbox).length > 0) return true;\n }\n return false;\n}\n\n// ray casting algorithm for detecting if point is in polygon\nfunction insidePolygon(rings, p) {\n var inside = false;\n for (var i = 0, len = rings.length; i < len; i++) {\n var ring = rings[i];\n for (var j = 0, len2 = ring.length, k = len2 - 1; j < len2; k = j++) {\n if (rayIntersect(p, ring[j], ring[k])) inside = !inside;\n }\n }\n return inside;\n}\n\nfunction rayIntersect(p, p1, p2) {\n return ((p1[1] > p[1]) !== (p2[1] > p[1])) && (p[0] < (p2[0] - p1[0]) * (p[1] - p1[1]) / (p2[1] - p1[1]) + p1[0]);\n}\n\nfunction treeItem(coords, props) {\n var item = {\n minX: Infinity,\n minY: Infinity,\n maxX: -Infinity,\n maxY: -Infinity,\n coords: coords,\n props: props\n };\n\n for (var i = 0; i < coords[0].length; i++) {\n var p = coords[0][i];\n item.minX = Math.min(item.minX, p[0]);\n item.minY = Math.min(item.minY, p[1]);\n item.maxX = Math.max(item.maxX, p[0]);\n item.maxY = Math.max(item.maxY, p[1]);\n }\n return item;\n}\n", "/* eslint @typescript-eslint/no-this-alias: \"warn\" */\nimport whichPolygon from 'which-polygon';\n\nimport { simplify } from './simplify.ts';\n\n// JSON\nimport matchGroupsJSON from '../config/matchGroups.json' with {type: 'json'};\nimport genericWordsJSON from '../config/genericWords.json' with {type: 'json'};\nimport treesJSON from '../config/trees.json' with {type: 'json'};\n\nconst matchGroups = matchGroupsJSON.matchGroups;\nconst trees = treesJSON.trees;\n\n\ntype Vec2 = [number, number];\ntype Vec3 = [number, number, number];\ntype Location = Vec2 | Vec3 | string | number;\n\ninterface LocationSet {\n include?: Array,\n exclude?: Array\n};\n\ninterface LocationConflation {\n validateLocation: (a: Location) => unknown;\n resolveLocation: (a: Location) => unknown;\n validateLocationSet: (a: LocationSet) => unknown;\n resolveLocationSet: (a: LocationSet) => unknown;\n};\n\ntype HitType = 'primary' | 'alternate' | 'excludeGeneric' | 'excludeNamed';\ninterface Hit {\n match: HitType;\n itemID?: string;\n area?: number;\n kv?: string;\n nsimple?: string;\n pattern?: string;\n};\n\n\nexport class Matcher {\n private matchIndex;\n private genericWords = new Map();\n private itemLocation;\n private locationSets;\n private locationIndex;\n private warnings: Array = [];\n\n\n // `constructor`\n // initialize the genericWords regexes\n constructor() {\n // The `matchIndex` is a specialized structure that allows us to quickly answer\n // _\"Given a [key/value tagpair, name, location], what canonical items (brands etc) can match it?\"_\n //\n // The index contains all valid combinations of k/v tagpairs and names\n // matchIndex:\n // {\n // 'k/v': {\n // 'primary': Map (String 'nsimple' -> Set (itemIDs…), // matches for tags like `name`, `name:xx`, etc.\n // 'alternate': Map (String 'nsimple' -> Set (itemIDs…), // matches for tags like `alt_name`, `brand`, etc.\n // 'excludeNamed': Map (String 'pattern' -> RegExp),\n // 'excludeGeneric': Map (String 'pattern' -> RegExp)\n // },\n // }\n //\n // {\n // 'amenity/bank': {\n // 'primary': {\n // 'firstbank': Set (\"firstbank-978cca\", \"firstbank-9794e6\", \"firstbank-f17495\", …),\n // …\n // },\n // 'alternate': {\n // '1stbank': Set (\"firstbank-f17495\"),\n // …\n // }\n // },\n // 'shop/supermarket': {\n // 'primary': {\n // 'coop': Set (\"coop-76454b\", \"coop-ebf2d9\", \"coop-36e991\", …),\n // 'coopfood': Set (\"coopfood-a8278b\", …),\n // …\n // },\n // 'alternate': {\n // 'coop': Set (\"coopfood-a8278b\", …),\n // 'federatedcooperatives': Set (\"coop-76454b\", …),\n // 'thecooperative': Set (\"coopfood-a8278b\", …),\n // …\n // }\n // }\n // }\n //\n this.matchIndex = undefined;\n\n // The `genericWords` structure matches the contents of genericWords.json to instantiated RegExp objects\n // Map (String 'pattern' -> RegExp),\n this.genericWords = new Map();\n (genericWordsJSON.genericWords || []).forEach(s => this.genericWords.set(s, new RegExp(s, 'i')));\n\n // The `itemLocation` structure maps itemIDs to locationSetIDs:\n // {\n // 'firstbank-f17495': '+[first_bank_western_us.geojson]',\n // 'firstbank-978cca': '+[first_bank_carolinas.geojson]',\n // 'coop-76454b': '+[Q16]',\n // 'coopfood-a8278b': '+[Q23666]',\n // …\n // }\n this.itemLocation = undefined;\n\n // The `locationSets` structure maps locationSetIDs to *resolved* locationSets:\n // {\n // '+[first_bank_western_us.geojson]': GeoJSON {…},\n // '+[first_bank_carolinas.geojson]': GeoJSON {…},\n // '+[Q16]': GeoJSON {…},\n // '+[Q23666]': GeoJSON {…},\n // …\n // }\n this.locationSets = undefined;\n\n // The `locationIndex` is an instance of which-polygon spatial index for the locationSets.\n this.locationIndex = undefined;\n\n // Array of match conflict pairs (currently unused)\n this.warnings = [];\n }\n\n\n //\n // `buildMatchIndex()`\n // Call this to prepare the matcher for use\n //\n // `data` needs to be an Object indexed on a 'tree/key/value' path.\n // (e.g. cache filled by `fileTree.read` or data found in `dist/nsi.json`)\n // {\n // 'brands/amenity/bank': { properties: {}, items: [ {}, {}, … ] },\n // 'brands/amenity/bar': { properties: {}, items: [ {}, {}, … ] },\n // …\n // }\n //\n buildMatchIndex(data: Record): void {\n const that = this;\n if (that.matchIndex) return; // it was built already\n that.matchIndex = new Map();\n\n const seenTree = new Map(); // warn if the same [k, v, nsimple] appears in multiple trees - #5625\n\n Object.keys(data).forEach(tkv => {\n const category = data[tkv];\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const t = parts[0];\n const k = parts[1];\n const v = parts[2];\n const thiskv = `${k}/${v}`;\n const tree = trees[t];\n\n let branch = that.matchIndex.get(thiskv);\n if (!branch) {\n branch = {\n primary: new Map(),\n alternate: new Map(),\n excludeGeneric: new Map(),\n excludeNamed: new Map()\n };\n that.matchIndex.set(thiskv, branch);\n }\n\n // ADD EXCLUSIONS\n const properties = category.properties || {};\n const exclude = properties.exclude || {};\n (exclude.generic || []).forEach(s => branch.excludeGeneric.set(s, new RegExp(s, 'i')));\n (exclude.named || []).forEach(s => branch.excludeNamed.set(s, new RegExp(s, 'i')));\n const excludeRegexes = [...branch.excludeGeneric.values(), ...branch.excludeNamed.values()];\n\n\n // ADD ITEMS\n const items = category.items;\n if (!Array.isArray(items) || !items.length) return;\n\n\n // Primary name patterns, match tags to take first\n // e.g. `name`, `name:ru`\n const primaryName = new RegExp(tree.nameTags.primary, 'i');\n\n // Alternate name patterns, match tags to consider after primary\n // e.g. `alt_name`, `short_name`, `brand`, `brand:ru`, etc..\n const alternateName = new RegExp(tree.nameTags.alternate, 'i');\n\n // There are a few exceptions to the name matching regexes.\n // Usually a tag suffix contains a language code like `name:en`, `name:ru`\n // but we want to exclude things like `operator:type`, `name:etymology`, etc..\n const notName = /:(colou?r|type|forward|backward|left|right|etymology|pronunciation|signed|wikipedia)$/i;\n\n // For certain categories we do not want to match generic KV pairs like `building/yes` or `amenity/yes`\n const skipGenericKV = skipGenericKVMatches(t, k, v);\n\n // We will collect the generic KV pairs anyway (for the purpose of filtering them out of matchTags)\n const genericKV = new Set([`${k}/yes`, `building/yes`]);\n\n // Collect alternate tagpairs for this kv category from matchGroups.\n // We might also pick up a few more generic KVs (like `shop/yes`)\n const matchGroupKV = new Set();\n Object.values(matchGroups).forEach(matchGroup => {\n const inGroup = matchGroup.some(otherkv => otherkv === thiskv);\n if (!inGroup) return;\n\n matchGroup.forEach(otherkv => {\n if (otherkv === thiskv) return; // skip self\n matchGroupKV.add(otherkv);\n\n const otherk = otherkv.split('/', 2)[0]; // we might pick up a `shop/yes`\n genericKV.add(`${otherk}/yes`);\n });\n });\n\n // For each item, insert all [key, value, name] combinations into the match index\n items.forEach(item => {\n if (!item.id) return;\n\n // Automatically remove redundant `matchTags` - #3417, #8137\n // (i.e. This kv is already covered by matchGroups, so it doesn't need to be in `item.matchTags`\n // or this kv is the primary kv, so it doesn't need to be duplicated in `item.matchTags`)\n if (Array.isArray(item.matchTags) && item.matchTags.length) {\n item.matchTags = item.matchTags\n .filter(matchTag => !matchGroupKV.has(matchTag) && (matchTag !== thiskv) && !genericKV.has(matchTag));\n\n if (!item.matchTags.length) delete item.matchTags;\n }\n\n // key/value tagpairs to insert into the match index..\n let kvTags = [`${thiskv}`]\n .concat(item.matchTags || []);\n\n if (!skipGenericKV) {\n kvTags = kvTags\n .concat(Array.from(genericKV)); // #3454 - match some generic tags\n }\n\n // Index all the namelike tag values\n Object.keys(item.tags).forEach(osmkey => {\n if (notName.test(osmkey)) return; // osmkey is not a namelike tag, skip\n const osmvalue = item.tags[osmkey];\n if (!osmvalue || excludeRegexes.some(regex => regex.test(osmvalue))) return; // osmvalue missing or excluded\n\n if (primaryName.test(osmkey)) {\n kvTags.forEach(kv => insertName('primary', t, kv, simplify(osmvalue), item.id));\n } else if (alternateName.test(osmkey)) {\n kvTags.forEach(kv => insertName('alternate', t, kv, simplify(osmvalue), item.id));\n }\n });\n\n // Index `matchNames` after indexing all other names..\n const keepMatchNames = new Set();\n (item.matchNames || []).forEach(matchName => {\n // If this matchname isn't already indexed, add it to the alternate index\n const nsimple = simplify(matchName);\n kvTags.forEach(kv => {\n const branch = that.matchIndex.get(kv);\n const primaryLeaf = branch && branch.primary.get(nsimple);\n const alternateLeaf = branch && branch.alternate.get(nsimple);\n const inPrimary = primaryLeaf && primaryLeaf.has(item.id);\n const inAlternate = alternateLeaf && alternateLeaf.has(item.id);\n\n if (!inPrimary && !inAlternate) {\n insertName('alternate', t, kv, nsimple, item.id);\n keepMatchNames.add(matchName);\n }\n });\n });\n\n // Automatically remove redundant `matchNames` - #3417\n // (i.e. This name got indexed some other way, so it doesn't need to be in `item.matchNames`)\n if (keepMatchNames.size) {\n item.matchNames = Array.from(keepMatchNames);\n } else {\n delete item.matchNames;\n }\n\n }); // each item\n }); // each tkv\n\n\n // Insert this item into the matchIndex\n function insertName(which: string, t: string, kv: string, nsimple: string, itemID: string) {\n if (!nsimple) {\n that.warnings.push(`Warning: skipping empty ${which} name for item ${t}/${kv}: ${itemID}`);\n return;\n }\n\n let branch = that.matchIndex.get(kv);\n if (!branch) {\n branch = {\n primary: new Map(),\n alternate: new Map(),\n excludeGeneric: new Map(),\n excludeNamed: new Map()\n };\n that.matchIndex.set(kv, branch);\n }\n\n let leaf = branch[which].get(nsimple);\n if (!leaf) {\n leaf = new Set();\n branch[which].set(nsimple, leaf);\n }\n\n leaf.add(itemID); // insert\n\n // check for duplicates - #5625\n if (!/yes$/.test(kv)) { // ignore genericKV like amenity/yes, building/yes, etc\n const kvnsimple = `${kv}/${nsimple}`;\n const existing = seenTree.get(kvnsimple);\n if (existing && existing !== t) {\n const items = Array.from(leaf);\n that.warnings.push(`Duplicate cache key \"${kvnsimple}\" in trees \"${t}\" and \"${existing}\", check items: ${items}`);\n return;\n }\n seenTree.set(kvnsimple, t);\n }\n }\n\n // For certain categories we do not want to match generic KV pairs like `building/yes` or `amenity/yes`\n function skipGenericKVMatches(t: string, k: string, v: string): boolean {\n return (\n t === 'flags' ||\n t === 'transit' ||\n k === 'landuse' ||\n v === 'atm' ||\n v === 'bicycle_parking' ||\n v === 'car_sharing' ||\n v === 'caravan_site' ||\n v === 'charging_station' ||\n v === 'dog_park' ||\n v === 'parking' ||\n v === 'phone' ||\n v === 'playground' ||\n v === 'post_box' ||\n v === 'public_bookcase' ||\n v === 'recycling' ||\n v === 'vending_machine'\n );\n }\n }\n\n\n //\n // `buildLocationIndex()`\n // Call this to prepare a which-polygon location index.\n // This *resolves* all the locationSets into GeoJSON, which takes some time.\n // You can skip this step if you don't care about matching within a location.\n //\n // `data` needs to be an Object indexed on a 'tree/key/value' path.\n // (e.g. cache filled by `fileTree.read` or data found in `dist/nsi.json`)\n // {\n // 'brands/amenity/bank': { properties: {}, items: [ {}, {}, … ] },\n // 'brands/amenity/bar': { properties: {}, items: [ {}, {}, … ] },\n // …\n // }\n //\n buildLocationIndex(data: Record, loco: LocationConflation): void {\n const that = this;\n if (that.locationIndex) return; // it was built already\n\n that.itemLocation = new Map();\n that.locationSets = new Map();\n\n Object.keys(data).forEach(tkv => {\n const items = data[tkv].items;\n if (!Array.isArray(items) || !items.length) return;\n\n items.forEach(item => {\n if (that.itemLocation.has(item.id)) return; // we've seen item id already - shouldn't be possible?\n\n let resolved;\n try {\n resolved = loco.resolveLocationSet(item.locationSet); // resolve a feature for this locationSet\n } catch (err: unknown) {\n const message = (err instanceof Error) ? err.message : err;\n console.warn(`buildLocationIndex: ${message}`); // couldn't resolve\n }\n if (!resolved || !resolved.id) return;\n\n that.itemLocation.set(item.id, resolved.id); // link it to the item\n if (that.locationSets.has(resolved.id)) return; // we've seen this locationSet feature before..\n\n // First time seeing this locationSet feature, make a copy and add to locationSet cache..\n const feature = _cloneDeep(resolved.feature);\n feature.id = resolved.id; // Important: always use the locationSet `id` (`+[Q30]`), not the feature `id` (`Q30`)\n feature.properties.id = resolved.id;\n\n if (!feature.geometry.coordinates.length || !feature.properties.area) {\n console.warn(`buildLocationIndex: locationSet ${resolved.id} for ${item.id} resolves to an empty feature:`);\n console.warn(JSON.stringify(feature));\n return;\n }\n\n that.locationSets.set(resolved.id, feature);\n });\n });\n\n that.locationIndex = whichPolygon({ type: 'FeatureCollection', features: [...that.locationSets.values()] });\n\n function _cloneDeep(obj) {\n return JSON.parse(JSON.stringify(obj));\n }\n }\n\n\n //\n // `match()`\n // Pass parts and return an Array of matches.\n // `k` - key\n // `v` - value\n // `n` - namelike\n // `loc` - optional - [lon,lat] location to search\n //\n // 1. If the [k,v,n] tuple matches a canonical item…\n // Return an Array of match results.\n // Each result will include the area in km² that the item is valid.\n //\n // Order of results:\n // Primary ordering will be on the \"match\" column:\n // \"primary\" - where the query matches the `name` tag, followed by\n // \"alternate\" - where the query matches an alternate name tag (e.g. short_name, brand, operator, etc)\n // Secondary ordering will be on the \"area\" column:\n // \"area descending\" if no location was provided, (worldwide before local)\n // \"area ascending\" if location was provided (local before worldwide)\n //\n // [\n // { match: 'primary', itemID: String, area: Number, kv: String, nsimple: String },\n // { match: 'primary', itemID: String, area: Number, kv: String, nsimple: String },\n // { match: 'alternate', itemID: String, area: Number, kv: String, nsimple: String },\n // { match: 'alternate', itemID: String, area: Number, kv: String, nsimple: String },\n // …\n // ]\n //\n // -or-\n //\n // 2. If the [k,v,n] tuple matches an exclude pattern…\n // Return an Array with a single exclude result, either\n //\n // [ { match: 'excludeGeneric', pattern: String, kv: String } ] // \"generic\" e.g. \"Food Court\"\n // or\n // [ { match: 'excludeNamed', pattern: String, kv: String } ] // \"named\", e.g. \"Kebabai\"\n //\n // About results\n // \"generic\" - a generic word that is probably not really a name.\n // For these, iD should warn the user \"Hey don't put 'food court' in the name tag\".\n // \"named\" - a real name like \"Kebabai\" that is just common, but not a brand.\n // For these, iD should just let it be. We don't include these in NSI, but we don't want to nag users about it either.\n //\n // -or-\n //\n // 3. If the [k,v,n] tuple matches nothing of any kind, return `null`\n //\n //\n match(k: string, v: string, n: string, loc?: Vec2): Array | null {\n const that = this;\n if (!that.matchIndex) {\n throw new Error('match: matchIndex not built.');\n }\n\n // If we were supplied a location, and a that.locationIndex has been set up,\n // get the locationSets that are valid there so we can filter results.\n let matchLocations;\n if (Array.isArray(loc) && that.locationIndex) {\n // which-polygon query returns an array of GeoJSON properties, pass true to return all results\n matchLocations = that.locationIndex([loc[0], loc[1], loc[0], loc[1]], true);\n }\n\n const nsimple = simplify(n);\n\n const seen = new Set();\n const results: Array = [];\n gatherResults('primary');\n gatherResults('alternate');\n if (results.length) return results;\n\n gatherResults('exclude');\n return results.length ? results : null;\n\n\n function gatherResults(which: string): void {\n // First try an exact match on k/v\n const kv = `${k}/${v}`;\n let didMatch = tryMatch(which, kv);\n if (didMatch) return;\n\n // If that didn't work, look in match groups for other pairs considered equivalent to k/v..\n for (const mg in matchGroups) {\n const matchGroup = matchGroups[mg];\n const inGroup = matchGroup.some(otherkv => otherkv === kv);\n if (!inGroup) continue;\n\n for (const otherkv of matchGroup) {\n if (otherkv === kv) continue; // skip self\n didMatch = tryMatch(which, otherkv);\n if (didMatch) return;\n }\n }\n\n // If finished 'exclude' pass and still haven't matched anything, try the global `genericWords.json` patterns\n if (which === 'exclude') {\n const regex = [...that.genericWords.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeGeneric', pattern: String(regex) }); // note no `branch`, no `kv`\n return;\n }\n }\n }\n\n function tryMatch(which: string, kv: string): boolean {\n const branch = that.matchIndex.get(kv);\n if (!branch) return false;\n\n if (which === 'exclude') { // Test name `n` against named and generic exclude patterns\n let regex = [...branch.excludeNamed.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeNamed', pattern: String(regex), kv: kv });\n return false;\n }\n regex = [...branch.excludeGeneric.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeGeneric', pattern: String(regex), kv: kv });\n return false;\n }\n return false;\n }\n\n const leaf = branch[which].get(nsimple);\n if (!leaf || !leaf.size) return false;\n if (!(which === 'primary' || which === 'alternate')) return false;\n\n // If we get here, we matched something..\n // Prepare the results, calculate areas (if location index was set up)\n let hits: Array = [];\n for (const itemID of [...leaf]) {\n let area = Infinity;\n if (that.itemLocation && that.locationSets) {\n const location = that.locationSets.get(that.itemLocation.get(itemID));\n area = (location && location.properties.area) || Infinity;\n }\n hits.push({ match: which, itemID: itemID, area: area, kv: kv, nsimple: nsimple });\n }\n\n let sortFn = byAreaDescending;\n\n // Filter the match to include only results valid in the requested `loc`..\n if (matchLocations) {\n hits = hits.filter(isValidLocation);\n sortFn = byAreaAscending;\n }\n\n if (!hits.length) return false;\n\n // push results\n hits.sort(sortFn).forEach(hit => {\n if (seen.has(hit.itemID)) return;\n seen.add(hit.itemID);\n results.push(hit);\n });\n\n return true;\n\n\n function isValidLocation(hit: Hit): boolean {\n if (!that.itemLocation) return true;\n return matchLocations.find(props => props.id === that.itemLocation.get(hit.itemID));\n }\n // Sort smaller (more local) locations first.\n function byAreaAscending(hitA: Hit, hitB: Hit): number {\n const areaA = hitA.area || 0;\n const areaB = hitB.area || 0;\n return areaA - areaB;\n }\n // Sort larger (more worldwide) locations first.\n function byAreaDescending(hitA: Hit, hitB: Hit): number {\n const areaA = hitA.area || 0;\n const areaB = hitB.area || 0;\n return areaB - areaA;\n }\n }\n }\n\n\n //\n // `getWarnings()`\n // Return any warnings discovered when buiding the index.\n // (currently this does nothing)\n //\n getWarnings(): Array {\n return this.warnings;\n }\n}\n", "// External\nimport diacritics from 'diacritics';\n\n// remove spaces, punctuation, diacritics\n// for punction see https://stackoverflow.com/a/21224179\nexport function simplify(str?: string): string {\n if (typeof str !== 'string') return '';\n\n return diacritics.remove(\n str\n .replace(/&/g, 'and')\n .replace(/(İ|i̇)/ig, 'i') // for BİM, İşbank - #5017, #8261\n .replace(/[\\s\\-=_!\"#%'*{},.\\/:;?\\(\\)\\[\\]@\\\\$\\^*+<>«»~`’\\u00a1\\u00a7\\u00b6\\u00b7\\u00bf\\u037e\\u0387\\u055a-\\u055f\\u0589\\u05c0\\u05c3\\u05c6\\u05f3\\u05f4\\u0609\\u060a\\u060c\\u060d\\u061b\\u061e\\u061f\\u066a-\\u066d\\u06d4\\u0700-\\u070d\\u07f7-\\u07f9\\u0830-\\u083e\\u085e\\u0964\\u0965\\u0970\\u0af0\\u0df4\\u0e4f\\u0e5a\\u0e5b\\u0f04-\\u0f12\\u0f14\\u0f85\\u0fd0-\\u0fd4\\u0fd9\\u0fda\\u104a-\\u104f\\u10fb\\u1360-\\u1368\\u166d\\u166e\\u16eb-\\u16ed\\u1735\\u1736\\u17d4-\\u17d6\\u17d8-\\u17da\\u1800-\\u1805\\u1807-\\u180a\\u1944\\u1945\\u1a1e\\u1a1f\\u1aa0-\\u1aa6\\u1aa8-\\u1aad\\u1b5a-\\u1b60\\u1bfc-\\u1bff\\u1c3b-\\u1c3f\\u1c7e\\u1c7f\\u1cc0-\\u1cc7\\u1cd3\\u2000-\\u206f\\u2cf9-\\u2cfc\\u2cfe\\u2cff\\u2d70\\u2e00-\\u2e7f\\u3001-\\u3003\\u303d\\u30fb\\ua4fe\\ua4ff\\ua60d-\\ua60f\\ua673\\ua67e\\ua6f2-\\ua6f7\\ua874-\\ua877\\ua8ce\\ua8cf\\ua8f8-\\ua8fa\\ua92e\\ua92f\\ua95f\\ua9c1-\\ua9cd\\ua9de\\ua9df\\uaa5c-\\uaa5f\\uaade\\uaadf\\uaaf0\\uaaf1\\uabeb\\ufe10-\\ufe16\\ufe19\\ufe30\\ufe45\\ufe46\\ufe49-\\ufe4c\\ufe50-\\ufe52\\ufe54-\\ufe57\\ufe5f-\\ufe61\\ufe68\\ufe6a\\ufe6b\\ufeff\\uff01-\\uff03\\uff05-\\uff07\\uff0a\\uff0c\\uff0e\\uff0f\\uff1a\\uff1b\\uff1f\\uff20\\uff3c\\uff61\\uff64\\uff65]+/g,'')\n .toLowerCase()\n );\n}\n", "// Internal\nimport { simplify } from './simplify.ts';\n\n// Removes noise from the name so that we can compare\n// similar names for catching duplicates.\nexport function stemmer(str?: string): string {\n if (typeof str !== 'string') return '';\n\n const noise = [\n /ban(k|c)(a|o)?/ig,\n /банк/ig,\n /coop/ig,\n /express/ig,\n /(gas|fuel)/ig,\n /wireless/ig,\n /(shop|store)/ig\n ];\n\n str = noise.reduce((acc, regex) => acc.replace(regex, ''), str);\n return simplify(str);\n}\n", "import { Matcher } from 'name-suggestion-index';\nimport { fileFetcher, locationManager } from '../core';\nimport { presetManager } from '../presets';\n\nimport { nsiCdnUrl } from '../../config/id.js';\n\n// Make very sure this resolves to iD's `package.json`\n// If you mess up the `../`s, the resolver may import another random package.json from somewhere else.\nimport packageJSON from '../../package.json';\n\n\n// This service contains all the code related to the **name-suggestion-index** (aka NSI)\n// NSI contains the most correct tagging for many commonly mapped features.\n// See https://github.com/osmlab/name-suggestion-index and https://nsi.guide\n\n\n// DATA\n\nlet _nsiStatus = 'loading'; // 'loading', 'ok', 'failed'\nlet _nsi = {};\n\n// Sometimes we can upgrade a feature tagged like `building=yes` to a better tag.\nconst buildingPreset = {\n 'building/commercial': true,\n 'building/government': true,\n 'building/hotel': true,\n 'building/retail': true,\n 'building/office': true,\n 'building/supermarket': true,\n 'building/yes': true\n};\n\n// Exceptions to the namelike regexes.\n// Usually a tag suffix contains a language code like `name:en`, `name:ru`\n// but we want to exclude things like `operator:type`, `name:etymology`, etc..\nconst notNames = /:(colou?r|type|forward|backward|left|right|etymology|pronunciation|wikipedia)$/i;\n\n// Exceptions to the branchlike regexes\nconst notBranches = /(coop|express|wireless|factory|outlet)/i;\n\n\n// PRIVATE FUNCTIONS\n\n// `setNsiSources()`\n// Adds the sources to iD's filemap so we can start downloading data.\n//\nfunction setNsiSources() {\n const nsiVersion = packageJSON.dependencies['name-suggestion-index'] || packageJSON.devDependencies['name-suggestion-index'];\n const cdn = nsiCdnUrl.replace('{version}', nsiVersion);\n const sources = {\n 'nsi_data': cdn + 'dist/json/nsi.min.json',\n 'nsi_dissolved': cdn + 'dist/wikidata/dissolved.min.json',\n 'nsi_features': cdn + 'dist/json/featureCollection.min.json',\n 'nsi_generics': cdn + 'dist/json/genericWords.min.json',\n 'nsi_presets': cdn + 'dist/presets/nsi-id-presets.min.json',\n 'nsi_replacements': cdn + 'dist/json/replacements.min.json',\n 'nsi_trees': cdn + 'dist/json/trees.min.json'\n };\n\n let fileMap = fileFetcher.fileMap();\n for (const k in sources) {\n if (!fileMap[k]) fileMap[k] = sources[k];\n }\n}\n\n\n// `loadNsiPresets()`\n// Returns a Promise fulfilled when the presets have been downloaded and merged into iD.\n//\nfunction loadNsiPresets() {\n return (\n Promise.all([\n fileFetcher.get('nsi_presets'),\n fileFetcher.get('nsi_features')\n ])\n .then(vals => {\n // Add `suggestion=true` to all the nsi presets\n // The preset json schema doesn't include it, but the iD code still uses it\n Object.values(vals[0].presets).forEach(preset => preset.suggestion = true);\n\n // nsi does not specify *:wikipedia (anymore):\n // clean up previous values to prevent that the wikidata/wikipedia information\n // is going to be out of sync, see #9103\n Object.values(vals[0].presets).forEach(preset => {\n if (preset.tags['brand:wikidata']) {\n preset.removeTags = {'brand:wikipedia': '*', ...(preset.removeTags || preset.addTags || preset.tags)};\n }\n if (preset.tags['operator:wikidata']) {\n preset.removeTags = {'operator:wikipedia': '*', ...(preset.removeTags || preset.addTags || preset.tags)};\n }\n if (preset.tags['network:wikidata']) {\n preset.removeTags = {'network:wikipedia': '*', ...(preset.removeTags || preset.addTags || preset.tags)};\n }\n });\n\n presetManager.merge({\n presets: vals[0].presets,\n featureCollection: vals[1]\n });\n })\n );\n}\n\n\n// `loadNsiData()`\n// Returns a Promise fulfilled when the other data have been downloaded and processed\n//\nfunction loadNsiData() {\n return (\n Promise.all([\n fileFetcher.get('nsi_data'),\n fileFetcher.get('nsi_dissolved'),\n fileFetcher.get('nsi_replacements'),\n fileFetcher.get('nsi_trees')\n ])\n .then(vals => {\n _nsi = {\n data: vals[0].nsi, // the raw name-suggestion-index data\n dissolved: vals[1].dissolved, // list of dissolved items\n replacements: vals[2].replacements, // trivial old->new qid replacements\n trees: vals[3].trees, // metadata about trees, main tags\n kvt: new Map(), // Map (k -> Map (v -> t) )\n qids: new Map(), // Map (wd/wp tag values -> qids)\n ids: new Map() // Map (id -> NSI item)\n };\n\n const matcher = new Matcher();\n _nsi.matcher = matcher;\n matcher.buildMatchIndex(_nsi.data);\n\n// *** BEGIN HACK ***\n\n// old - built in matcher will set up the locationindex by resolving all the locationSets one-by-one\n // matcher.buildLocationIndex(_nsi.data, locationManager.loco());\n\n// new - Use the location manager instead of redoing that work\n// It has already processed the presets at this point\n\n// We need to monkeypatch a few of the collections that the NSI matcher depends on.\n// The `itemLocation` structure maps itemIDs to locationSetIDs\nmatcher.itemLocation = new Map();\n\n// The `locationSets` structure maps locationSetIDs to GeoJSON\n// We definitely need this, but don't need full geojson, just { properties: { area: xxx }}\nmatcher.locationSets = new Map();\n\nObject.keys(_nsi.data).forEach(tkv => {\n const items = _nsi.data[tkv].items;\n if (!Array.isArray(items) || !items.length) return;\n\n items.forEach(item => {\n if (matcher.itemLocation.has(item.id)) return; // we've seen item id already - shouldn't be possible?\n\n const locationSetID = locationManager.locationSetID(item.locationSet);\n matcher.itemLocation.set(item.id, locationSetID);\n\n if (matcher.locationSets.has(locationSetID)) return; // we've seen this locationSet before..\n\n const fakeFeature = { id: locationSetID, properties: { id: locationSetID, area: 1 } };\n matcher.locationSets.set(locationSetID, fakeFeature);\n });\n});\n\n// The `locationIndex` is an instance of which-polygon spatial index for the locationSets.\n// We only really need this to _look like_ which-polygon query `_wp.locationIndex(bbox, true);`\n// i.e. it needs to return the properties of the locationsets\nmatcher.locationIndex = (bbox) => {\n const validHere = locationManager.locationSetsAt([bbox[0], bbox[1]]);\n const results = [];\n\n for (const [locationSetID, area] of Object.entries(validHere)) {\n const fakeFeature = matcher.locationSets.get(locationSetID);\n if (fakeFeature) {\n fakeFeature.properties.area = area;\n results.push(fakeFeature);\n }\n }\n return results;\n};\n\n// *** END HACK ***\n\n\n Object.keys(_nsi.data).forEach(tkv => {\n const category = _nsi.data[tkv];\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const t = parts[0];\n const k = parts[1];\n const v = parts[2];\n\n // Build a reverse index of keys -> values -> trees present in the name-suggestion-index\n // Collect primary keys (e.g. \"amenity\", \"craft\", \"shop\", \"man_made\", \"route\", etc)\n // \"amenity\": {\n // \"restaurant\": \"brands\"\n // }\n let vmap = _nsi.kvt.get(k);\n if (!vmap) {\n vmap = new Map();\n _nsi.kvt.set(k, vmap);\n }\n vmap.set(v, t);\n\n const tree = _nsi.trees[t]; // e.g. \"brands\", \"operators\"\n const mainTag = tree.mainTag; // e.g. \"brand:wikidata\", \"operator:wikidata\", etc\n\n const items = category.items || [];\n items.forEach(item => {\n // Remember some useful things for later, cache NSI id -> item\n item.tkv = tkv;\n item.mainTag = mainTag;\n _nsi.ids.set(item.id, item);\n\n // Cache Wikidata/Wikipedia values -> qid, for #6416\n const wd = item.tags[mainTag];\n const wp = item.tags[mainTag.replace('wikidata', 'wikipedia')];\n if (wd) _nsi.qids.set(wd, wd);\n if (wp && wd) _nsi.qids.set(wp, wd);\n });\n });\n })\n );\n}\n\n\n// `gatherKVs()`\n// Gather all the k/v pairs that we will run through the NSI matcher.\n// An OSM tags object can contain anything, but only a few tags will be interesting to NSI.\n//\n// This function will return the interesting tag pairs like:\n// \"amenity/restaurant\", \"man_made/flagpole\"\n// and fallbacks like\n// \"amenity/yes\"\n// excluding things like\n// \"tiger:reviewed\", \"surface\", \"ref\", etc.\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `Object` containing kv pairs to test:\n// {\n// 'primary': Set(),\n// 'alternate': Set()\n// }\n//\nfunction gatherKVs(tags) {\n let primary = new Set();\n let alternate = new Set();\n\n Object.keys(tags).forEach(osmkey => {\n const osmvalue = tags[osmkey];\n if (!osmvalue) return;\n\n // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184\n if (osmkey === 'route_master') osmkey = 'route';\n\n const vmap = _nsi.kvt.get(osmkey);\n if (!vmap) return; // not an interesting key\n\n if (vmap.get(osmvalue)) { // Matched a category in NSI\n primary.add(`${osmkey}/${osmvalue}`); // interesting key/value\n } else if (osmvalue === 'yes') {\n alternate.add(`${osmkey}/${osmvalue}`); // fallback key/yes\n }\n });\n\n // Can we try a generic building fallback match? - See #6122, #7197\n // Only try this if we do a preset match and find nothing else remarkable about that building.\n // For example, a way with `building=yes` + `name=Westfield` may be a Westfield department store.\n // But a way with `building=yes` + `name=Westfield` + `public_transport=station` is a train station for a town named \"Westfield\"\n const preset = presetManager.matchTags(tags, 'area');\n if (buildingPreset[preset.id]) {\n alternate.add('building/yes');\n }\n\n return { primary: primary, alternate: alternate };\n}\n\n\n// `identifyTree()`\n// NSI has a concept of trees: \"brands\", \"operators\", \"flags\", \"transit\".\n// The tree determines things like which tags are namelike, and which tags hold important wikidata.\n// This takes an Object of tags and tries to identify what tree to use.\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `string` the name of the tree if known\n// or 'unknown' if it could match several trees (e.g. amenity/yes)\n// or null if no match\n//\nfunction identifyTree(tags) {\n let unknown;\n let t;\n\n // Check all tags\n Object.keys(tags).forEach(osmkey => {\n if (t) return; // found already\n\n const osmvalue = tags[osmkey];\n if (!osmvalue) return;\n\n // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184\n if (osmkey === 'route_master') osmkey = 'route';\n\n const vmap = _nsi.kvt.get(osmkey);\n if (!vmap) return; // this key is not in nsi\n\n if (osmvalue === 'yes') {\n unknown = 'unknown';\n } else {\n t = vmap.get(osmvalue);\n }\n });\n\n return t || unknown || null;\n}\n\n\n// `gatherNames()`\n// Gather all the namelike values that we will run through the NSI matcher.\n// It will gather values primarily from tags `name`, `name:ru`, `flag:name`\n// and fallback to alternate tags like `brand`, `brand:ru`, `alt_name`\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `Object` containing namelike values to test:\n// {\n// 'primary': Set(),\n// 'fallbacks': Set()\n// }\n//\nfunction gatherNames(tags) {\n const empty = { primary: new Set(), alternate: new Set() };\n let primary = new Set();\n let alternate = new Set();\n let foundSemi = false;\n let testNameFragments = false;\n let patterns;\n\n // Patterns for matching OSM keys that might contain namelike values.\n // These roughly correspond to the \"trees\" concept in name-suggestion-index,\n let t = identifyTree(tags);\n if (!t) return empty;\n\n if (t === 'transit') {\n patterns = {\n primary: /^network$/i,\n alternate: /^(operator|operator:\\w+|network:\\w+|\\w+_name|\\w+_name:\\w+)$/i\n };\n } else if (t === 'flags') {\n patterns = {\n primary: /^(flag:name|flag:name:\\w+)$/i,\n alternate: /^(flag|flag:\\w+|subject|subject:\\w+)$/i // note: no `country`, we special-case it below\n };\n } else if (t === 'brands') {\n testNameFragments = true;\n patterns = {\n primary: /^(name|name:\\w+)$/i,\n alternate: /^(brand|brand:\\w+|operator|operator:\\w+|\\w+_name|\\w+_name:\\w+)/i,\n };\n } else if (t === 'operators') {\n testNameFragments = true;\n patterns = {\n primary: /^(name|name:\\w+|operator|operator:\\w+)$/i,\n alternate: /^(brand|brand:\\w+|\\w+_name|\\w+_name:\\w+)/i,\n };\n } else { // unknown/multiple\n testNameFragments = true;\n patterns = {\n primary: /^(name|name:\\w+)$/i,\n alternate: /^(brand|brand:\\w+|network|network:\\w+|operator|operator:\\w+|\\w+_name|\\w+_name:\\w+)/i,\n };\n }\n\n // Test `name` fragments, longest to shortest, to fit them into a \"Name Branch\" pattern.\n // e.g. \"TUI ReiseCenter - Neuss Innenstadt\" -> [\"TUI\", \"ReiseCenter\", \"Neuss\", \"Innenstadt\"]\n if (tags.name && testNameFragments) {\n const nameParts = tags.name.split(/[\\s\\-\\/,.]/);\n for (let split = nameParts.length; split > 0; split--) {\n const name = nameParts.slice(0, split).join(' '); // e.g. \"TUI ReiseCenter\"\n primary.add(name);\n }\n }\n\n // Check all tags\n Object.keys(tags).forEach(osmkey => {\n const osmvalue = tags[osmkey];\n if (!osmvalue) return;\n\n if (isNamelike(osmkey, 'primary')) {\n if (/;/.test(osmvalue)) {\n foundSemi = true;\n } else {\n primary.add(osmvalue);\n alternate.delete(osmvalue);\n }\n } else if (!primary.has(osmvalue) && isNamelike(osmkey, 'alternate')) {\n if (/;/.test(osmvalue)) {\n foundSemi = true;\n } else {\n alternate.add(osmvalue);\n }\n }\n });\n\n // For flags only, fallback to `country` tag only if no other namelike values were found.\n // See https://github.com/openstreetmap/iD/pull/8305#issuecomment-769174070\n if (tags.man_made === 'flagpole' && !primary.size && !alternate.size && !!tags.country) {\n const osmvalue = tags.country;\n if (/;/.test(osmvalue)) {\n foundSemi = true;\n } else {\n alternate.add(osmvalue);\n }\n }\n\n // If any namelike value contained a semicolon, return empty set and don't try matching anything.\n if (foundSemi) {\n return empty;\n } else {\n return { primary: primary, alternate: alternate };\n }\n\n function isNamelike(osmkey, which) {\n if (osmkey === 'old_name') return false;\n return patterns[which].test(osmkey) && !notNames.test(osmkey);\n }\n}\n\n\n// `gatherTuples()`\n// Generate all combinations of [key,value,name] that we want to test.\n// This prioritizes them so that the primary name and k/v pairs go first\n//\n// Arguments\n// `tryKVs`: `Object` containing primary and alternate k/v pairs to test\n// `tryNames`: `Object` containing primary and alternate names to test\n// Returns\n// `Array`: tuple objects ordered by priority\n//\nfunction gatherTuples(tryKVs, tryNames) {\n let tuples = [];\n ['primary', 'alternate'].forEach(whichName => {\n // test names longest to shortest\n const arr = Array.from(tryNames[whichName]).sort((a, b) => b.length - a.length);\n arr.forEach(n => {\n ['primary', 'alternate'].forEach(whichKV => {\n tryKVs[whichKV].forEach(kv => {\n const parts = kv.split('/', 2);\n const k = parts[0];\n const v = parts[1];\n tuples.push({ k: k, v: v, n: n });\n });\n });\n });\n });\n return tuples;\n}\n\n\n// `_upgradeTags()`\n// Try to match a feature to a canonical record in name-suggestion-index\n// and upgrade the tags to match.\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// `loc`: Location where this feature exists, as a [lon, lat]\n// Returns\n// `Object` containing the result, or `null` if no changes needed:\n// {\n// 'newTags': `Object` - The tags the the feature should have\n// 'matched': `Object` - The matched item\n// }\n//\nfunction _upgradeTags(tags, loc) {\n let newTags = Object.assign({}, tags); // shallow copy\n let changed = false;\n\n // Before anything, perform trivial Wikipedia/Wikidata replacements\n Object.keys(newTags).forEach(osmkey => {\n const matchTag = osmkey.match(/^(\\w+:)?wikidata$/);\n if (matchTag) { // Look at '*:wikidata' tags\n const prefix = (matchTag[1] || '');\n const wd = newTags[osmkey];\n const replace = _nsi.replacements[wd]; // If it matches a QID in the replacement list...\n\n if (replace && replace.wikidata !== undefined) { // replace or delete `*:wikidata` tag\n changed = true;\n if (replace.wikidata) {\n newTags[osmkey] = replace.wikidata;\n } else {\n delete newTags[osmkey];\n }\n }\n if (replace && replace.wikipedia !== undefined) { // replace or delete `*:wikipedia` tag\n changed = true;\n const wpkey = `${prefix}wikipedia`;\n if (replace.wikipedia) {\n newTags[wpkey] = replace.wikipedia;\n } else {\n delete newTags[wpkey];\n }\n }\n }\n });\n\n // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184\n const isRouteMaster = (tags.type === 'route_master');\n\n // Gather key/value tag pairs to try to match\n const tryKVs = gatherKVs(tags);\n if (!tryKVs.primary.size && !tryKVs.alternate.size) {\n return changed ? { newTags: newTags, matched: null } : null;\n }\n\n // Gather namelike tag values to try to match\n const tryNames = gatherNames(tags);\n\n // Do `wikidata=*` or `wikipedia=*` tags identify this entity as a chain? - See #6416\n // If so, these tags can be swapped to e.g. `brand:wikidata`/`brand:wikipedia`.\n const foundQID = _nsi.qids.get(tags.wikidata) || _nsi.qids.get(tags.wikipedia);\n if (foundQID) tryNames.primary.add(foundQID); // matcher will recognize the Wikidata QID as name too\n\n if (!tryNames.primary.size && !tryNames.alternate.size) {\n return changed ? { newTags: newTags, matched: null } : null;\n }\n\n // Order the [key,value,name] tuples - test primary before alternate\n const tuples = gatherTuples(tryKVs, tryNames);\n\n for (let i = 0; i < tuples.length; i++) {\n const tuple = tuples[i];\n const hits = _nsi.matcher.match(tuple.k, tuple.v, tuple.n, loc); // Attempt to match an item in NSI\n\n if (!hits || !hits.length) continue; // no match, try next tuple\n if (hits[0].match !== 'primary' && hits[0].match !== 'alternate') break; // a generic match, stop looking\n\n // A match may contain multiple results, the first one is likely the best one for this location\n // e.g. `['pfk-a54c14', 'kfc-1ff19c', 'kfc-658eea']`\n let itemID, item;\n for (let j = 0; j < hits.length; j++) {\n const hit = hits[j];\n itemID = hit.itemID;\n if (_nsi.dissolved[itemID]) continue; // Don't upgrade to a dissolved item\n\n item = _nsi.ids.get(itemID);\n if (!item) continue;\n const mainTag = item.mainTag; // e.g. `brand:wikidata`\n const itemQID = item.tags[mainTag]; // e.g. `brand:wikidata` qid\n const notQID = newTags[`not:${mainTag}`]; // e.g. `not:brand:wikidata` qid\n\n if ( // Exceptions, skip this hit\n (!itemQID || itemQID === notQID) || // No `*:wikidata` or matched a `not:*:wikidata`\n (newTags.office && !item.tags.office) // feature may be a corporate office for a brand? - #6416\n ) {\n item = null;\n continue; // continue looking\n } else {\n break; // use `item`\n }\n }\n\n // Can't use any of these hits, try next tuple..\n if (!item) continue;\n\n // At this point we have matched a canonical item and can suggest tag upgrades..\n item = JSON.parse(JSON.stringify(item)); // deep copy\n const tkv = item.tkv;\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const k = parts[1];\n const v = parts[2];\n const category = _nsi.data[tkv];\n const properties = category.properties || {};\n\n // Preserve some tags that we specifically don't want NSI to overwrite. ('^name', sometimes)\n let preserveTags = item.preserveTags || properties.preserveTags || [];\n\n // These tags can be toplevel tags -or- attributes - so we generally want to preserve existing values - #8615\n // We'll only _replace_ the tag value if this tag is the toplevel/defining tag for the matched item (`k`)\n ['building', 'emergency', 'internet_access', 'opening_hours', 'takeaway'].forEach(osmkey => {\n if (k !== osmkey) preserveTags.push(`^${osmkey}$`);\n });\n\n const regexes = preserveTags.map(s => new RegExp(s, 'i'));\n\n let keepTags = {};\n Object.keys(newTags).forEach(osmkey => {\n if (regexes.some(regex => regex.test(osmkey))) {\n keepTags[osmkey] = newTags[osmkey];\n }\n });\n\n // Remove any primary tags (\"amenity\", \"craft\", \"shop\", \"man_made\", \"route\", etc) that have a\n // value like `amenity=yes` or `shop=yes` (exceptions have already been added to `keepTags` above)\n _nsi.kvt.forEach((vmap, k) => {\n if (newTags[k] === 'yes') delete newTags[k];\n });\n\n // Replace mistagged `wikidata`/`wikipedia` with e.g. `brand:wikidata`/`brand:wikipedia`\n if (foundQID) {\n delete newTags.wikipedia;\n delete newTags.wikidata;\n }\n\n // Do the tag upgrade\n Object.assign(newTags, item.tags, keepTags);\n\n // Swap `route` back to `route_master` - name-suggestion-index#5184\n if (isRouteMaster) {\n newTags.route_master = newTags.route;\n delete newTags.route;\n }\n\n // Special `branch` splitting rules - IF..\n // - NSI is suggesting to replace `name`, AND\n // - `branch` doesn't already contain something, AND\n // - original name has not moved to an alternate name (e.g. \"Dunkin' Donuts\" -> \"Dunkin'\"), AND\n // - original name is \"some name\" + \"some stuff\", THEN\n // consider splitting `name` into `name`/`branch`..\n const origName = tags.name;\n const newName = newTags.name;\n if (newName && origName && newName !== origName && !newTags.branch) {\n const newNames = gatherNames(newTags);\n const newSet = new Set([...newNames.primary, ...newNames.alternate]);\n const isMoved = newSet.has(origName); // another tag holds the original name now\n\n if (!isMoved) {\n // Test name fragments, longest to shortest, to fit them into a \"Name Branch\" pattern.\n // e.g. \"TUI ReiseCenter - Neuss Innenstadt\" -> [\"TUI\", \"ReiseCenter\", \"Neuss\", \"Innenstadt\"]\n const nameParts = origName.split(/[\\s\\-\\/,.]/);\n for (let split = nameParts.length; split > 0; split--) {\n const name = nameParts.slice(0, split).join(' '); // e.g. \"TUI ReiseCenter\"\n const branch = nameParts.slice(split).join(' '); // e.g. \"Neuss Innenstadt\"\n const nameHits = _nsi.matcher.match(k, v, name, loc);\n if (!nameHits || !nameHits.length) continue; // no match, try next name fragment\n\n if (nameHits.some(hit => hit.itemID === itemID)) { // matched the name fragment to the same itemID above\n if (branch) {\n if (notBranches.test(branch)) { // \"branch\" was detected but is noise (\"factory outlet\", etc)\n newTags.name = origName; // Leave `name` alone, this part of the name may be significant..\n } else {\n const branchHits = _nsi.matcher.match(k, v, branch, loc);\n if (branchHits && branchHits.length) { // if \"branch\" matched something else in NSI..\n if (branchHits[0].match === 'primary' || branchHits[0].match === 'alternate') { // if another brand! (e.g. \"KFC - Taco Bell\"?)\n return null; // bail out - can't suggest tags in this case\n } // else a generic (e.g. \"gas\", \"cafe\") - ignore\n } else { // \"branch\" is not noise and not something in NSI\n newTags.branch = branch; // Stick it in the `branch` tag..\n }\n }\n }\n break;\n }\n }\n }\n }\n\n return { newTags: newTags, matched: item };\n }\n\n return changed ? { newTags: newTags, matched: null } : null;\n}\n\n\n// `_isGenericName()`\n// Is the `name` tag generic?\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `true` if it is generic, `false` if not\n//\nfunction _isGenericName(tags) {\n const n = tags.name;\n if (!n) return false;\n\n // tryNames just contains the `name` tag value and nothing else\n const tryNames = { primary: new Set([n]), alternate: new Set() };\n\n // Gather key/value tag pairs to try to match\n const tryKVs = gatherKVs(tags);\n if (!tryKVs.primary.size && !tryKVs.alternate.size) return false;\n\n // Order the [key,value,name] tuples - test primary before alternate\n const tuples = gatherTuples(tryKVs, tryNames);\n\n for (let i = 0; i < tuples.length; i++) {\n const tuple = tuples[i];\n const hits = _nsi.matcher.match(tuple.k, tuple.v, tuple.n); // Attempt to match an item in NSI\n\n // If we get a `excludeGeneric` hit, this is a generic name.\n if (hits && hits.length && hits[0].match === 'excludeGeneric') return true;\n }\n\n return false;\n}\n\n\n\n// PUBLIC INTERFACE\n\nexport default {\n\n // `init()`\n // On init, start preparing the name-suggestion-index\n //\n init: () => {\n // Note: service.init is called immediately after the presetManager has started loading its data.\n // We expect to chain onto an unfulfilled promise here.\n setNsiSources();\n presetManager.ensureLoaded()\n .then(() => loadNsiPresets())\n .then(() => loadNsiData())\n .then(() => _nsiStatus = 'ok')\n .catch(() => _nsiStatus = 'failed');\n },\n\n\n // `reset()`\n // Reset is called when user saves data to OSM (does nothing here)\n //\n reset: () => {},\n\n\n // `status()`\n // To let other code know how it's going...\n //\n // Returns\n // `String`: 'loading', 'ok', 'failed'\n //\n status: () => _nsiStatus,\n\n\n // `isGenericName()`\n // Is the `name` tag generic?\n //\n // Arguments\n // `tags`: `Object` containing the feature's OSM tags\n // Returns\n // `true` if it is generic, `false` if not\n //\n isGenericName: (tags) => _isGenericName(tags),\n\n\n // `upgradeTags()`\n // Suggest tag upgrades.\n // This function will not modify the input tags, it makes a copy.\n //\n // Arguments\n // `tags`: `Object` containing the feature's OSM tags\n // `loc`: Location where this feature exists, as a [lon, lat]\n // Returns\n // `Object` containing the result, or `null` if no changes needed:\n // {\n // 'newTags': `Object` - The tags the the feature should have\n // 'matched': `Object` - The matched item\n // }\n //\n upgradeTags: (tags, loc) => _upgradeTags(tags, loc),\n\n\n // `cache()`\n // Direct access to the NSI cache, useful for testing or breaking things\n //\n // Returns\n // `Object`: the internal NSI cache\n //\n cache: () => _nsi\n};\n", "import { localizer } from '../core/localizer';\n\nfunction timeSince(date: Date): [value: number, unit: Intl.RelativeTimeFormatUnit] {\n const seconds = Math.floor((+new Date() - +date) / 1000);\n const s = (n: number) => Math.floor(seconds / n);\n\n if (s(60 * 60 * 24 * 365) > 1) return [s(60 * 60 * 24 * 365), 'years'];\n if (s(60 * 60 * 24 * 30) > 1) return [s(60 * 60 * 24 * 30), 'months'];\n if (s(60 * 60 * 24) > 1) return [s(60 * 60 * 24), 'days'];\n if (s(60 * 60) > 1) return [s(60 * 60), 'hours'];\n if (s(60) > 1) return [s(60), 'minutes'];\n return [s(1), 'seconds'];\n}\n\n/**\n * Show the relative time if {@link Intl.RelativeTimeFormat} is supported\n * Otherwise fallback to the current date\n */\nexport function getRelativeDate(date: Date) {\n const preferredLanguage = localizer.localeCode();\n\n if (typeof Intl === 'undefined' || typeof Intl.RelativeTimeFormat === 'undefined') {\n return `on ${date.toLocaleDateString(preferredLanguage)}`;\n }\n\n const [number, units] = timeSince(date);\n if (!Number.isFinite(number)) return '-';\n\n return new Intl.RelativeTimeFormat(preferredLanguage).format(-number, units);\n}\n\nexport function localeDateString(date: Date | string) {\n if (!date) return null;\n const options: Intl.DateTimeFormatOptions = {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n };\n const d = new Date(date);\n if (Number.isNaN(d.getTime())) return null;\n return d.toLocaleDateString(localizer.localeCode(), options);\n}\n\nexport function localeTimestamp(date: Date) {\n const options: Intl.DateTimeFormatOptions = {\n day: '2-digit',\n month: '2-digit',\n year: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n second: 'numeric',\n };\n return date.toLocaleString(localizer.localeCode(), options);\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { json as d3_json } from 'd3-fetch';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport RBush from 'rbush';\n\nimport { geoExtent, geoScaleToZoom } from '../geo';\nimport { utilQsString, utilRebind, utilSetTransform, utilStringQs, utilTiler } from '../util';\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\nimport { localeDateString } from '../util/date';\n\n\nvar apibase = 'https://kartaview.org';\nvar maxResults = 1000;\nvar tileZoom = 14;\nvar tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);\nvar dispatch = d3_dispatch('loadedImages');\nvar imgZoom = d3_zoom()\n .extent([[0, 0], [320, 240]])\n .translateExtent([[0, 0], [320, 240]])\n .scaleExtent([1, 15]);\nvar _oscCache;\nvar _oscSelectedImage;\nvar _loadViewerPromise;\n\n\nfunction abortRequest(controller) {\n controller.abort();\n}\n\n\nfunction maxPageAtZoom(z) {\n if (z < 15) return 2;\n if (z === 15) return 5;\n if (z === 16) return 10;\n if (z === 17) return 20;\n if (z === 18) return 40;\n if (z > 18) return 80;\n}\n\n\nfunction loadTiles(which, url, projection) {\n var currZoom = Math.floor(geoScaleToZoom(projection.scale()));\n var tiles = tiler.getTiles(projection);\n\n // abort inflight requests that are no longer needed\n var cache = _oscCache[which];\n Object.keys(cache.inflight).forEach(function(k) {\n var wanted = tiles.find(function(tile) { return k.indexOf(tile.id + ',') === 0; });\n if (!wanted) {\n abortRequest(cache.inflight[k]);\n delete cache.inflight[k];\n }\n });\n\n tiles.forEach(function(tile) {\n loadNextTilePage(which, currZoom, url, tile);\n });\n}\n\n\nfunction loadNextTilePage(which, currZoom, url, tile) {\n var cache = _oscCache[which];\n var bbox = tile.extent.bbox();\n var maxPages = maxPageAtZoom(currZoom);\n var nextPage = cache.nextPage[tile.id] || 1;\n var params = utilQsString({\n ipp: maxResults,\n page: nextPage,\n // client_id: clientId,\n bbTopLeft: [bbox.maxY, bbox.minX].join(','),\n bbBottomRight: [bbox.minY, bbox.maxX].join(',')\n }, true);\n\n if (nextPage > maxPages) return;\n\n var id = tile.id + ',' + String(nextPage);\n if (cache.loaded[id] || cache.inflight[id]) return;\n\n var controller = new AbortController();\n cache.inflight[id] = controller;\n\n var options = {\n method: 'POST',\n signal: controller.signal,\n body: params,\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' }\n };\n\n d3_json(url, options)\n .then(function(data) {\n cache.loaded[id] = true;\n delete cache.inflight[id];\n if (!data || !data.currentPageItems || !data.currentPageItems.length) {\n throw new Error('No Data');\n }\n\n var features = data.currentPageItems.map(function(item) {\n var loc = [+item.lng, +item.lat];\n var d;\n\n if (which === 'images') {\n d = {\n service: 'photo',\n loc: loc,\n key: item.id,\n ca: +item.heading,\n captured_at: (item.shot_date || item.date_added),\n captured_by: item.username,\n imagePath: item.name,\n sequence_id: item.sequence_id,\n sequence_index: +item.sequence_index\n };\n\n // cache sequence info\n var seq = _oscCache.sequences[d.sequence_id];\n if (!seq) {\n seq = { rotation: 0, images: [] };\n _oscCache.sequences[d.sequence_id] = seq;\n }\n seq.images[d.sequence_index] = d;\n _oscCache.images.forImageKey[d.key] = d; // cache imageKey -> image\n }\n\n return {\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n };\n });\n\n cache.rtree.load(features);\n\n if (data.currentPageItems.length === maxResults) { // more pages to load\n cache.nextPage[tile.id] = nextPage + 1;\n loadNextTilePage(which, currZoom, url, tile);\n } else {\n cache.nextPage[tile.id] = Infinity; // no more pages to load\n }\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n }\n })\n .catch(function() {\n cache.loaded[id] = true;\n delete cache.inflight[id];\n });\n}\n\nexport default {\n\n init: function() {\n if (!_oscCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset: function() {\n if (_oscCache) {\n Object.values(_oscCache.images.inflight).forEach(abortRequest);\n }\n\n _oscCache = {\n images: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), forImageKey: {} },\n sequences: {}\n };\n },\n\n\n images: function(projection) {\n var limit = 5;\n return searchLimited(limit, projection, _oscCache.images.rtree);\n },\n\n\n sequences: function(projection) {\n var viewport = projection.clipExtent();\n var min = [viewport[0][0], viewport[1][1]];\n var max = [viewport[1][0], viewport[0][1]];\n var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n var sequenceKeys = {};\n\n // all sequences for images in viewport\n _oscCache.images.rtree.search(bbox)\n .forEach(function(d) { sequenceKeys[d.data.sequence_id] = true; });\n\n // make linestrings from those sequences\n var lineStrings = [];\n Object.keys(sequenceKeys)\n .forEach(function(sequenceKey) {\n var seq = _oscCache.sequences[sequenceKey];\n var images = seq && seq.images;\n\n if (images) {\n lineStrings.push({\n type: 'LineString',\n coordinates: images.map(function (d) { return d.loc; }).filter(Boolean),\n properties: {\n captured_at: images[0] ? images[0].captured_at: null,\n captured_by: images[0] ? images[0].captured_by: null,\n key: sequenceKey\n }\n });\n }\n });\n return lineStrings;\n },\n\n\n cachedImage: function(imageKey) {\n return _oscCache.images.forImageKey[imageKey];\n },\n\n\n loadImages: function(projection) {\n var url = apibase + '/1.0/list/nearby-photos/';\n loadTiles('images', url, projection);\n },\n\n\n ensureViewerLoaded: function(context) {\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n // add kartaview-wrapper\n var wrap = context.container().select('.photoviewer').selectAll('.kartaview-wrapper')\n .data([0]);\n\n var that = this;\n\n var wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper kartaview-wrapper')\n .classed('hide', true)\n .call(imgZoom.on('zoom', zoomPan))\n .on('dblclick.zoom', null);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n var controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls');\n\n controlsEnter\n .append('button')\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .on('click.rotate-ccw', rotate(-90))\n .text('\u293F');\n\n controlsEnter\n .append('button')\n .on('click.rotate-cw', rotate(90))\n .text('\u293E');\n\n controlsEnter\n .append('button')\n .on('click.forward', step(1))\n .text('\u25BA');\n\n wrapEnter\n .append('div')\n .attr('class', 'kartaview-image-wrap');\n\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.kartaview', function(dimensions) {\n imgZoom\n .extent([[0, 0], dimensions])\n .translateExtent([[0, 0], dimensions]);\n });\n\n\n function zoomPan(d3_event) {\n var t = d3_event.transform;\n context.container().select('.photoviewer .kartaview-image-wrap')\n .call(utilSetTransform, t.x, t.y, t.k);\n }\n\n\n function rotate(deg) {\n return function() {\n if (!_oscSelectedImage) return;\n var sequenceKey = _oscSelectedImage.sequence_id;\n var sequence = _oscCache.sequences[sequenceKey];\n if (!sequence) return;\n\n var r = sequence.rotation || 0;\n r += deg;\n\n if (r > 180) r -= 360;\n if (r < -180) r += 360;\n sequence.rotation = r;\n\n var wrap = context.container().select('.photoviewer .kartaview-wrapper');\n\n wrap\n .transition()\n .duration(100)\n .call(imgZoom.transform, d3_zoomIdentity);\n\n wrap.selectAll('.kartaview-image')\n .transition()\n .duration(100)\n .style('transform', 'rotate(' + r + 'deg)');\n };\n }\n\n function step(stepBy) {\n return function() {\n if (!_oscSelectedImage) return;\n var sequenceKey = _oscSelectedImage.sequence_id;\n var sequence = _oscCache.sequences[sequenceKey];\n if (!sequence) return;\n\n var nextIndex = _oscSelectedImage.sequence_index + stepBy;\n var nextImage = sequence.images[nextIndex];\n if (!nextImage) return;\n\n context.map().centerEase(nextImage.loc);\n\n that\n .selectImage(context, nextImage.key);\n };\n }\n\n // don't need any async loading so resolve immediately\n _loadViewerPromise = Promise.resolve();\n\n return _loadViewerPromise;\n },\n\n\n showViewer: function(context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.kartaview-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.kartaview-wrapper')\n .classed('hide', false);\n }\n\n return this;\n },\n\n\n hideViewer: function(context) {\n _oscSelectedImage = null;\n\n this.updateUrlImage(null);\n\n var viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n return this.setStyles(context, null, true);\n },\n\n\n selectImage: function(context, imageKey) {\n\n var d = this.cachedImage(imageKey);\n\n _oscSelectedImage = d;\n\n this.updateUrlImage(imageKey);\n\n var viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null, true);\n\n context.container().selectAll('.icon-sign')\n .classed('currentView', false);\n\n if (!d) return this;\n\n var wrap = context.container().select('.photoviewer .kartaview-wrapper');\n var imageWrap = wrap.selectAll('.kartaview-image-wrap');\n var attribution = wrap.selectAll('.photo-attribution').text('');\n\n wrap\n .transition()\n .duration(100)\n .call(imgZoom.transform, d3_zoomIdentity);\n\n imageWrap\n .selectAll('.kartaview-image')\n .remove();\n\n if (d) {\n var sequence = _oscCache.sequences[d.sequence_id];\n var r = (sequence && sequence.rotation) || 0;\n\n imageWrap\n .append('img')\n .attr('class', 'kartaview-image')\n .attr('src', (apibase + '/' + d.imagePath).replace(/^https:\\/\\/kartaview\\.org\\/storage(\\d+)\\//, 'https://storage$1.openstreetcam.org/'))\n .style('transform', 'rotate(' + r + 'deg)');\n\n if (d.captured_by) {\n attribution\n .append('a')\n .attr('class', 'captured_by')\n .attr('target', '_blank')\n .attr('href', 'https://kartaview.org/user/' + encodeURIComponent(d.captured_by))\n .text('@' + d.captured_by);\n\n attribution\n .append('span')\n .text('|');\n }\n\n if (d.captured_at) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeDateString(d.captured_at));\n\n attribution\n .append('span')\n .text('|');\n }\n\n attribution\n .append('a')\n .attr('class', 'image-link')\n .attr('target', '_blank')\n .attr('href', 'https://kartaview.org/details/' + d.sequence_id + '/' + d.sequence_index)\n .text('kartaview.org');\n }\n\n return this;\n },\n\n\n getSelectedImage: function() {\n return _oscSelectedImage;\n },\n\n\n getSequenceKeyForImage: function(d) {\n return d && d.sequence_id;\n },\n\n\n // Updates the currently highlighted sequence and selected bubble.\n // Reset is only necessary when interacting with the viewport because\n // this implicitly changes the currently selected bubble/sequence\n setStyles: function(context, hovered, reset) {\n if (reset) { // reset all layers\n context.container().selectAll('.viewfield-group')\n .classed('highlighted', false)\n .classed('hovered', false)\n .classed('currentView', false);\n\n context.container().selectAll('.sequence')\n .classed('highlighted', false)\n .classed('currentView', false);\n }\n\n var hoveredImageId = hovered && hovered.key;\n var hoveredSequenceId = this.getSequenceKeyForImage(hovered);\n\n var viewer = context.container().select('.photoviewer');\n var selected = viewer.empty() ? undefined : viewer.datum();\n var selectedImageId = selected && selected.key;\n var selectedSequenceId = this.getSequenceKeyForImage(selected);\n\n // highlight sibling viewfields on either the selected or the hovered sequences\n context.container().selectAll('.layer-kartaview .viewfield-group')\n .classed('highlighted', function(d) { return d.sequence_id === selectedSequenceId || d.id === hoveredImageId; })\n .classed('hovered', function(d) { return d.key === hoveredImageId; })\n .classed('currentView', function(d) { return d.key === selectedImageId; });\n\n context.container().selectAll('.layer-kartaview .sequence')\n .classed('highlighted', function(d) { return d.properties.key === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.key === selectedSequenceId; });\n\n // update viewfields if needed\n context.container().selectAll('.layer-kartaview .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n var d = this.parentNode.__data__;\n if (d.pano && d.key !== selectedImageId) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n return this;\n },\n\n\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'kartaview/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n\n cache: function() {\n return _oscCache;\n }\n\n};\n", "import type { Feature, FeatureCollection, Geometry, Position } from 'geojson';\nimport whichPolygon from 'which-polygon';\nimport rawBorders from './data/borders.json';\n\ninterface RegionFeatureProperties {\n // Unique identifier specific to country-coder\n id: string;\n\n // ISO 3166-1 alpha-2 code\n iso1A2: string | undefined;\n\n // ISO 3166-1 alpha-3 code\n iso1A3: string | undefined;\n\n // ISO 3166-1 numeric-3 code\n iso1N3: string | undefined;\n\n // UN M49 code\n m49: string | undefined;\n\n // Wikidata QID\n wikidata: string;\n\n // The emoji flag sequence derived from this feature's ISO 3166-1 alpha-2 code\n emojiFlag: string | undefined;\n\n // The ccTLD (country code top-level domain)\n ccTLD: string | undefined;\n\n // The common English name\n nameEn: string;\n\n // Additional identifiers which can be used to look up this feature;\n // these cannot collide with the identifiers for any other feature\n aliases: Array | undefined;\n\n // For features entirely within a country, the ISO 3166-1 alpha-2 code for that country\n country: string | undefined;\n\n // The ISO 3166-1 alpha-2, M49, or QIDs of other features this feature is entirely within, including its country\n groups: Array;\n\n // The ISO 3166-1 alpha-2, M49, or QIDs of other features this feature contains;\n // the inverse of `groups`\n members: Array | undefined;\n\n // The rough geographic type of this feature.\n // Levels do not necessarily nest cleanly within each other.\n // - `world`: all features\n\n // - `unitedNations`: United Nations\n // - `union`: European Union\n // - `subunion`: Outermost Regions of the EU, Overseas Countries and Territories of the EU\n\n // Defined by the UN\n // - `region`: Africa, Americas, Antarctica, Asia, Europe, Oceania\n // - `subregion`: Sub-Saharan Africa, North America, Micronesia, etc.\n // - `intermediateRegion`: Eastern Africa, South America, Channel Islands, etc.\n\n // - `sharedLandform`: Great Britain, Macaronesia, Mariana Islands, etc.\n // - `country`: Ethiopia, Brazil, United States, etc.\n // - `subcountryGroup`\n // - `territory`: Puerto Rico, Gurnsey, Hong Kong, etc.\n // - `subterritory`: Sark, Ascension Island, Diego Garcia, etc.\n level: string;\n\n // The status of this feature's ISO 3166-1 code(s), if any\n // - `official`: officially-assigned\n // - `excRes`: exceptionally-reserved\n // - `usrAssn`: user-assigned\n isoStatus: string | undefined;\n\n // The side of the road that traffic drives on within this feature\n // - `right`\n // - `left`\n driveSide: 'left' | 'right' | undefined;\n\n // The unit used for road traffic speeds within this feature\n // - `mph`: miles per hour\n // - `km/h`: kilometers per hour\n roadSpeedUnit: 'mph' | 'km/h' | undefined;\n\n // The unit used for road vehicle height restrictions within this feature\n // - `ft`: feet and inches\n // - `m`: meters\n roadHeightUnit: 'ft' | 'm' | undefined;\n\n // The international calling codes for this feature, sometimes including area codes\n // e.g. `1`, `1 340`\n callingCodes: Array | undefined;\n};\n\ntype RegionFeature = Feature;\ntype RegionFeatureCollection = FeatureCollection;\ntype Vec2 = [number, number]; // [lon, lat]\ntype Bbox = [number, number, number, number]; // [minLon, minLat, maxLon, maxLat]\n\ninterface PointGeometry {\n type: string;\n coordinates: Vec2\n};\n\ninterface PointFeature {\n type: string;\n geometry: PointGeometry;\n properties: any\n};\ntype Location = Vec2 | PointGeometry | PointFeature;\n\ninterface CodingOptions {\n // For overlapping features, the division level of the one to get. If no feature\n // exists at the given level, the feature at the next higher level is returned.\n // See the `level` property of `RegionFeatureProperties` for possible values.\n level?: string | undefined;\n // Only a feature at the specified level or lower will be returned.\n maxLevel?: string | undefined;\n // Only a feature with the specified property will be returned.\n withProp?: string | undefined;\n};\n\n// The base GeoJSON feature collection\nexport const borders: RegionFeatureCollection = rawBorders as RegionFeatureCollection;\n\n// The whichPolygon interface for looking up a feature by point\nlet _whichPolygon: any = {};\n// The cache for looking up a feature by identifier\nconst _featuresByCode: any = {};\n\n// discard special characters and instances of and/the/of that aren't the only characters\nconst idFilterRegex =\n /(?=(?!^(and|the|of|el|la|de)$))(\\b(and|the|of|el|la|de)\\b)|[-_ .,'()&[\\]/]/gi;\n\nfunction canonicalID(id: string | null): string {\n const s = id || '';\n if (s.charAt(0) === '.') {\n // skip replace if it leads with a '.' (e.g. a ccTLD like '.de', '.la')\n return s.toUpperCase();\n } else {\n return s.replace(idFilterRegex, '').toUpperCase();\n }\n}\n\n// Geographic levels, roughly from most to least granular\nconst levels = [\n 'subterritory',\n 'territory',\n 'subcountryGroup',\n 'country',\n 'sharedLandform',\n 'intermediateRegion',\n 'subregion',\n 'region',\n 'subunion',\n 'union',\n 'unitedNations',\n 'world'\n];\n\nloadDerivedDataAndCaches(borders);\n\n// Loads implicit feature data and the getter index caches\nfunction loadDerivedDataAndCaches(borders) {\n const identifierProps = ['iso1A2', 'iso1A3', 'm49', 'wikidata', 'emojiFlag', 'ccTLD', 'nameEn'];\n const geometryFeatures: Array = [];\n\n for (const feature of borders.features) {\n // generate a unique ID for each feature\n const props = feature.properties;\n props.id = props.iso1A2 || props.m49 || props.wikidata;\n\n loadM49(feature);\n loadTLD(feature);\n loadIsoStatus(feature);\n loadLevel(feature);\n loadGroups(feature);\n loadFlag(feature);\n // cache only after loading derived IDs\n cacheFeatureByIDs(feature);\n\n if (feature.geometry) {\n geometryFeatures.push(feature);\n }\n }\n\n // must load `members` only after fully loading `featuresByID`\n for (const feature of borders.features) {\n // ensure all groups are listed by their ID\n feature.properties.groups = feature.properties.groups.map(groupID => {\n return _featuresByCode[groupID].properties.id;\n });\n loadMembersForGroupsOf(feature);\n }\n\n // must load attributes only after loading geometry features into `members`\n for (const feature of borders.features) {\n loadRoadSpeedUnit(feature);\n loadRoadHeightUnit(feature);\n loadDriveSide(feature);\n loadCallingCodes(feature);\n loadGroupGroups(feature);\n }\n\n for (const feature of borders.features) {\n // order groups by their `level`\n feature.properties.groups.sort((groupID1, groupID2) => {\n return (\n levels.indexOf(_featuresByCode[groupID1].properties.level) -\n levels.indexOf(_featuresByCode[groupID2].properties.level)\n );\n });\n // order members by their `level` and then by order in borders\n if (feature.properties.members) {\n feature.properties.members.sort((id1, id2) => {\n const diff =\n levels.indexOf(_featuresByCode[id1].properties.level) -\n levels.indexOf(_featuresByCode[id2].properties.level);\n if (diff === 0) {\n return (\n borders.features.indexOf(_featuresByCode[id1]) -\n borders.features.indexOf(_featuresByCode[id2])\n );\n }\n return diff;\n });\n }\n }\n\n // whichPolygon doesn't support null geometry even though GeoJSON does\n const geometryOnlyCollection: RegionFeatureCollection = {\n type: 'FeatureCollection',\n features: geometryFeatures\n };\n _whichPolygon = whichPolygon(geometryOnlyCollection);\n\n function loadGroups(feature: RegionFeature) {\n const props = feature.properties;\n if (!props.groups) {\n props.groups = [];\n }\n if (feature.geometry && props.country) {\n // Add `country` to `groups`\n props.groups.push(props.country);\n }\n if (props.m49 !== '001') { // all features are in the world feature except the world itself\n props.groups.push('001');\n }\n }\n\n function loadM49(feature: RegionFeature) {\n const props = feature.properties;\n if (!props.m49 && props.iso1N3) { // M49 is a superset of ISO numerics so we only need to store one\n props.m49 = props.iso1N3;\n }\n }\n\n function loadTLD(feature: RegionFeature) {\n const props = feature.properties;\n if (props.level === 'unitedNations') return; // `.un` is not a ccTLD\n if (props.ccTLD === null) return; // e.g. Sark is an ISO code but not a ccTLD\n if (!props.ccTLD && props.iso1A2) { // ccTLD is nearly the same as iso1A2, so we only need to explicitly code any exceptions\n props.ccTLD = '.' + props.iso1A2.toLowerCase();\n }\n }\n\n function loadIsoStatus(feature: RegionFeature) {\n const props = feature.properties;\n if (!props.isoStatus && props.iso1A2) { // Features with an ISO code but no explicit status are officially-assigned\n props.isoStatus = 'official';\n }\n }\n\n function loadLevel(feature: RegionFeature) {\n const props = feature.properties;\n if (props.level) return;\n if (!props.country) { // a feature without an explicit `level` or `country` is itself a country\n props.level = 'country';\n } else if (!props.iso1A2 || props.isoStatus === 'official') {\n props.level = 'territory';\n } else {\n props.level = 'subterritory';\n }\n }\n\n function loadGroupGroups(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry || !props.members) return;\n\n const featureLevelIndex = levels.indexOf(props.level);\n let sharedGroups: Array = [];\n\n props.members.forEach((memberID, index) => {\n const member = _featuresByCode[memberID];\n const memberGroups = member.properties.groups.filter((groupID) => {\n return (\n groupID !== feature.properties.id &&\n featureLevelIndex < levels.indexOf(_featuresByCode[groupID].properties.level)\n );\n });\n if (index === 0) {\n sharedGroups = memberGroups;\n } else {\n sharedGroups = sharedGroups.filter((groupID) => memberGroups.indexOf(groupID) !== -1);\n }\n });\n\n props.groups = props.groups.concat(\n sharedGroups.filter((groupID) => props.groups.indexOf(groupID) === -1)\n );\n\n for (const groupID of sharedGroups) {\n const groupFeature = _featuresByCode[groupID];\n if (groupFeature.properties.members.indexOf(props.id) === -1) {\n groupFeature.properties.members.push(props.id);\n }\n }\n }\n\n function loadRoadSpeedUnit(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry) {\n // only `mph` regions are listed explicitly, else assume `km/h`\n if (!props.roadSpeedUnit) props.roadSpeedUnit = 'km/h';\n } else if (props.members) {\n const vals = Array.from(\n new Set(\n props.members\n .map(id => {\n const member = _featuresByCode[id];\n if (member.geometry) {\n return member.properties.roadSpeedUnit || 'km/h';\n } else {\n return null;\n }\n })\n .filter(Boolean)\n )\n );\n // if all members have the same value then that's also the value for this feature\n if (vals.length === 1) props.roadSpeedUnit = vals[0];\n }\n }\n\n function loadRoadHeightUnit(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry) {\n // only `ft` regions are listed explicitly, else assume `m`\n if (!props.roadHeightUnit) props.roadHeightUnit = 'm';\n } else if (props.members) {\n const vals = Array.from(\n new Set(\n props.members\n .map(id => {\n const member = _featuresByCode[id];\n if (member.geometry) {\n return member.properties.roadHeightUnit || 'm';\n } else {\n return null;\n }\n })\n .filter(Boolean)\n )\n );\n // if all members have the same value then that's also the value for this feature\n if (vals.length === 1) props.roadHeightUnit = vals[0];\n }\n }\n\n function loadDriveSide(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry) {\n // only `left` regions are listed explicitly, else assume `right`\n if (!props.driveSide) props.driveSide = 'right';\n } else if (props.members) {\n const vals = Array.from(\n new Set(\n props.members\n .map(id => {\n const member = _featuresByCode[id];\n if (member.geometry) {\n return member.properties.driveSide || 'right';\n } else {\n return null;\n }\n })\n .filter(Boolean)\n )\n );\n // if all members have the same value then that's also the value for this feature\n if (vals.length === 1) props.driveSide = vals[0];\n }\n }\n\n function loadCallingCodes(feature: RegionFeature) {\n const props = feature.properties;\n if (!feature.geometry && props.members) {\n props.callingCodes = Array.from(\n new Set(\n props.members.reduce((array, id) => {\n const member = _featuresByCode[id];\n if (member.geometry && member.properties.callingCodes) {\n return array.concat(member.properties.callingCodes);\n }\n return array;\n }, [])\n )\n );\n }\n }\n\n // Calculates the emoji flag (if any) and caches it\n function loadFlag(feature: RegionFeature) {\n let flag = '';\n\n // Most emoji flags can be generated from their 2 letter code.\n // Skip 'FX' (Metropolitan France), allow it to roll up to 'FR' - #25\n const country = feature.properties.iso1A2;\n if (country && country !== 'FX') {\n flag = _toEmojiCountryFlag(country);\n }\n\n // Support a few regional flags - #157\n const regionStrings = {\n Q21: 'gbeng', // GB-ENG (England)\n Q22: 'gbsct', // GB-SCT (Scotland)\n Q25: 'gbwls' // GB-WLS (Wales)\n };\n const region = regionStrings[feature.properties.wikidata];\n if (region) {\n flag = _toEmojiRegionFlag(region);\n }\n\n if (flag) {\n feature.properties.emojiFlag = flag;\n }\n\n // Normally, take isoA2 chars and jump up into \"Enclosed Alphanumeric Supplement\" block\n // see https://en.wikipedia.org/wiki/Regional_indicator_symbol\n // see https://en.wikipedia.org/wiki/Enclosed_Alphanumeric_Supplement\n function _toEmojiCountryFlag(s: string): string {\n return s.replace(/./g, c => String.fromCodePoint(c.charCodeAt(0) + 0x1F1A5));\n }\n\n // Regional flags are encoded as U+1F3F4 (black flag) + the region string (jump up to \"Tags\" block) + U+E007F (end)\n // see https://en.wikipedia.org/wiki/Tags_(Unicode_block)\n function _toEmojiRegionFlag(s: string) {\n const codepoints = [0x1F3F4];\n for (const c of [...s]) {\n codepoints.push(c.codePointAt(0) as number + 0xE0000);\n }\n codepoints.push(0xE007F);\n return String.fromCodePoint.apply(null, codepoints);\n }\n }\n\n // Populate `members` as the inverse relationship of `groups`\n function loadMembersForGroupsOf(feature: RegionFeature) {\n for (const groupID of feature.properties.groups) {\n const groupFeature = _featuresByCode[groupID];\n\n if (!groupFeature.properties.members) {\n groupFeature.properties.members = [];\n }\n groupFeature.properties.members.push(feature.properties.id);\n }\n }\n\n // Caches features by their identifying strings for rapid lookup\n function cacheFeatureByIDs(feature: RegionFeature) {\n const ids: Array = [];\n\n for (const prop of identifierProps) {\n const id = feature.properties[prop];\n if (id) {\n ids.push(id);\n }\n }\n\n for (const alias of (feature.properties.aliases || [])) {\n ids.push(alias);\n }\n\n for (const id of ids) {\n const cid = canonicalID(id);\n _featuresByCode[cid] = feature;\n }\n }\n}\n\n// Returns the [longitude, latitude] for the location argument\nfunction locArray(loc: Location): Vec2 {\n if (Array.isArray(loc)) {\n return loc as Vec2;\n } else if ((loc as PointGeometry).coordinates) {\n return (loc as PointGeometry).coordinates;\n } else {\n return (loc as PointFeature).geometry.coordinates;\n }\n}\n\n// Returns the smallest feature of any kind containing `loc`, if any\nfunction smallestFeature(loc: Location): RegionFeature | null {\n const query = locArray(loc);\n const featureProperties: RegionFeatureProperties = _whichPolygon(query);\n if (!featureProperties) return null;\n return _featuresByCode[featureProperties.id];\n}\n\n// Returns the country feature containing `loc`, if any\nfunction countryFeature(loc: Location): RegionFeature | null {\n const feature = smallestFeature(loc);\n if (!feature) return null;\n\n // a feature without `country` but with geometry is itself a country\n const countryCode = feature.properties.country || feature.properties.iso1A2;\n return _featuresByCode[countryCode as string] || null;\n}\n\nconst defaultOpts = {\n level: undefined,\n maxLevel: undefined,\n withProp: undefined\n};\n\n// Returns the feature containing `loc` for the `opts`, if any\nfunction featureForLoc(loc: Location, opts: CodingOptions): RegionFeature | null {\n const targetLevel = opts.level || 'country';\n const maxLevel = opts.maxLevel || 'world';\n const withProp = opts.withProp;\n\n const targetLevelIndex = levels.indexOf(targetLevel);\n if (targetLevelIndex === -1) return null;\n\n const maxLevelIndex = levels.indexOf(maxLevel);\n if (maxLevelIndex === -1) return null;\n if (maxLevelIndex < targetLevelIndex) return null;\n\n if (targetLevel === 'country') {\n // attempt fast path for country-level coding\n const fastFeature = countryFeature(loc);\n if (fastFeature) {\n if (!withProp || fastFeature.properties[withProp]) {\n return fastFeature;\n }\n }\n }\n\n const features = featuresContaining(loc);\n\n const match = features.find((feature) => {\n const levelIndex = levels.indexOf(feature.properties.level);\n if (\n feature.properties.level === targetLevel ||\n // if no feature exists at the target level, return the first feature at the next level up\n (levelIndex > targetLevelIndex && levelIndex <= maxLevelIndex)\n ) {\n if (!withProp || feature.properties[withProp]) {\n return feature;\n }\n }\n return false;\n });\n\n return match || null;\n}\n\n// Returns the feature with an identifying property matching `id`, if any\nfunction featureForID(id: string | number): RegionFeature | null {\n let stringID: string;\n\n if (typeof id === 'number') {\n stringID = id.toString();\n if (stringID.length === 1) {\n stringID = '00' + stringID;\n } else if (stringID.length === 2) {\n stringID = '0' + stringID;\n }\n } else {\n stringID = canonicalID(id);\n }\n return _featuresByCode[stringID] || null;\n}\n\nfunction smallestFeaturesForBbox(bbox: Bbox): [RegionFeature] {\n return _whichPolygon.bbox(bbox).map(props => _featuresByCode[props.id]);\n}\n\nfunction smallestOrMatchingFeature(query: Location | string | number): RegionFeature | null {\n if (typeof query === 'object') {\n return smallestFeature(query as Location);\n }\n return featureForID(query);\n}\n\n// Returns the feature matching the given arguments, if any\nexport function feature(query: Location | string | number, opts: CodingOptions = defaultOpts): RegionFeature | null {\n if (typeof query === 'object') {\n return featureForLoc(query as Location, opts);\n }\n return featureForID(query);\n}\n\n// Returns the ISO 3166-1 alpha-2 code for the feature matching the arguments, if any\nexport function iso1A2Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'iso1A2';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.iso1A2 || null;\n}\n\n// Returns the ISO 3166-1 alpha-3 code for the feature matching the arguments, if any\nexport function iso1A3Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'iso1A3';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.iso1A3 || null;\n}\n\n// Returns the ISO 3166-1 numeric-3 code for the feature matching the arguments, if any\nexport function iso1N3Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'iso1N3';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.iso1N3 || null;\n}\n\n// Returns the UN M49 code for the feature matching the arguments, if any\nexport function m49Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'm49';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.m49 || null;\n}\n\n// Returns the Wikidata QID code for the feature matching the arguments, if any\nexport function wikidataQID(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'wikidata';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.wikidata;\n}\n\n// Returns the emoji emojiFlag sequence for the feature matching the arguments, if any\nexport function emojiFlag(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'emojiFlag';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.emojiFlag || null;\n}\n\n// Returns the ccTLD (country code top-level domain) for the feature matching the arguments, if any\nexport function ccTLD(\n query: Location | string | number,\n opts: CodingOptions = defaultOpts\n): string | null {\n opts.withProp = 'ccTLD';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.ccTLD || null;\n}\n\nfunction propertiesForQuery(query: Location | Bbox, property: string): Array {\n const features = featuresContaining(query, false);\n return features.map(feature => feature.properties[property]).filter(Boolean);\n}\n\n// Returns all the ISO 3166-1 alpha-2 codes of features at the location\nexport function iso1A2Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'iso1A2');\n}\n\n// Returns all the ISO 3166-1 alpha-3 codes of features at the location\nexport function iso1A3Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'iso1A3');\n}\n\n// Returns all the ISO 3166-1 numeric-3 codes of features at the location\nexport function iso1N3Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'iso1N3');\n}\n\n// Returns all the UN M49 codes of features at the location\nexport function m49Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'm49');\n}\n\n// Returns all the Wikidata QIDs of features at the location\nexport function wikidataQIDs(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'wikidata');\n}\n\n// Returns all the emoji flag sequences of features at the location\nexport function emojiFlags(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'emojiFlag');\n}\n\n// Returns all the ccTLD (country code top-level domain) sequences of features at the location\nexport function ccTLDs(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'ccTLD');\n}\n\n// Returns the feature matching `query` and all features containing it, if any.\n// If passing `true` for `strict`, an exact match will not be included\nexport function featuresContaining(query: Location | Bbox | string | number, strict?: boolean): Array {\n let matchingFeatures: Array;\n\n if (Array.isArray(query) && query.length === 4) { // check if bounding box\n matchingFeatures = smallestFeaturesForBbox(query as Bbox);\n } else {\n const smallestOrMatching = smallestOrMatchingFeature(query as Location | string | number);\n matchingFeatures = smallestOrMatching ? [smallestOrMatching] : [];\n }\n\n if (!matchingFeatures.length) return [];\n\n let returnFeatures: Array;\n if (!strict || typeof query === 'object') {\n returnFeatures = matchingFeatures.slice();\n } else {\n returnFeatures = [];\n }\n\n for (const feature of matchingFeatures) {\n const properties = feature.properties;\n for (const groupID of properties.groups) {\n const groupFeature = _featuresByCode[groupID];\n if (returnFeatures.indexOf(groupFeature) === -1) {\n returnFeatures.push(groupFeature);\n }\n }\n }\n\n return returnFeatures;\n}\n\n// Returns the feature matching `id` and all features it contains, if any.\n// If passing `true` for `strict`, an exact match will not be included\nexport function featuresIn(id: string | number, strict?: boolean): Array {\n const feature = featureForID(id);\n if (!feature) return [];\n\n const features: Array = [];\n if (!strict) {\n features.push(feature);\n }\n\n const properties = feature.properties;\n for (const memberID of (properties.members || [])) {\n features.push(_featuresByCode[memberID]);\n }\n\n return features;\n}\n\n// Returns a new feature with the properties of the feature matching `id`\n// and the combined geometry of all its component features\nexport function aggregateFeature(id: string | number): RegionFeature | null {\n const features = featuresIn(id, false);\n if (features.length === 0) return null;\n\n let aggregateCoordinates: Position[][][] = [];\n for (const feature of features) {\n if (feature.geometry?.type === 'MultiPolygon' && feature.geometry.coordinates) {\n aggregateCoordinates = aggregateCoordinates.concat(feature.geometry.coordinates);\n }\n }\n\n return {\n type: 'Feature',\n properties: features[0].properties,\n geometry: {\n type: 'MultiPolygon',\n coordinates: aggregateCoordinates\n }\n };\n}\n\n// Returns true if the feature matching `query` is, or is a part of, the feature matching `bounds`\nexport function isIn(query: Location | string | number, bounds: string | number): boolean | null {\n const queryFeature = smallestOrMatchingFeature(query);\n const boundsFeature = featureForID(bounds);\n\n if (!queryFeature || !boundsFeature) return null;\n\n if (queryFeature.properties.id === boundsFeature.properties.id) return true;\n return queryFeature.properties.groups.indexOf(boundsFeature.properties.id) !== -1;\n}\n\n// Returns true if the feature matching `query` is within EU jurisdiction\nexport function isInEuropeanUnion(query: Location | string | number): boolean | null {\n return isIn(query, 'EU');\n}\n\n// Returns true if the feature matching `query` is, or is within, a United Nations member state\nexport function isInUnitedNations(query: Location | string | number): boolean | null {\n return isIn(query, 'UN');\n}\n\n// Returns the side traffic drives on in the feature matching `query` as a string (`right` or `left`)\nexport function driveSide(query: Location | string | number): string | null {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.driveSide) || null;\n}\n\n// Returns the road speed unit for the feature matching `query` as a string (`mph` or `km/h`)\nexport function roadSpeedUnit(query: Location | string | number): string | null {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.roadSpeedUnit) || null;\n}\n\n// Returns the road vehicle height restriction unit for the feature matching `query` as a string (`ft` or `m`)\nexport function roadHeightUnit(query: Location | string | number): string | null {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.roadHeightUnit) || null;\n}\n\n// Returns the full international calling codes for phone numbers in the feature matching `query`, if any\nexport function callingCodes(query: Location | string | number): Array {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.callingCodes) || [];\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { utilRebind } from '../util';\n\n\nconst pannellumViewerCSS = 'pannellum/pannellum.css';\nconst pannellumViewerJS = 'pannellum/pannellum.js';\n\nexport async function pannellumPhotoFrame(context, selection) {\n const dispatch = d3_dispatch('viewerChanged');\n\n const module = {};\n module.event = utilRebind(module, dispatch, 'on');\n module.loadPannellum = function(context) {\n const head = d3_select('head');\n\n return Promise.all([\n new Promise((resolve, reject) => {\n // load pannellum viewer css\n head\n .selectAll('#ideditor-pannellum-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-pannellum-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(pannellumViewerCSS))\n .on('load.pannellum', resolve)\n .on('error.pannellum', reject);\n }),\n new Promise((resolve, reject) => {\n // load pannellum viewer js\n head\n .selectAll('#ideditor-pannellum-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-pannellum-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(pannellumViewerJS))\n .on('load.pannellum', resolve)\n .on('error.pannellum', reject);\n })\n ]);\n };\n\n let _currScenes = [];\n let _pannellumViewer;\n let _activeSceneKey;\n\n selection\n .append('div')\n .attr('class', 'photo-frame pannellum-frame')\n .attr('id', 'ideditor-pannellum-viewer')\n .classed('hide', true)\n .on('mousedown', function(e) { e.stopPropagation(); });\n\n if (!window.pannellum) {\n await module.loadPannellum(context);\n }\n\n const options = {\n 'default': { firstScene: '' },\n scenes: {},\n minHfov: 20,\n disableKeyboardCtrl: true,\n sceneFadeDuration: 0\n };\n\n _pannellumViewer = window.pannellum.viewer('ideditor-pannellum-viewer', options);\n\n _pannellumViewer\n .on('mousedown', () => d3_select(window)\n .on('pointermove.pannellum mousemove.pannellum', () => dispatch.call('viewerChanged')))\n .on('mouseup', () => d3_select(window)\n .on('pointermove.pannellum mousemove.pannellum', null))\n .on('animatefinished', () => dispatch.call('viewerChanged'));\n\n context.ui().photoviewer.on('resize.pannellum', () => {\n _pannellumViewer.resize();\n });\n\n /**\n * Shows the photo frame if hidden\n * @param {*} context the HTML wrap of the frame\n */\n module.showPhotoFrame = function(context) {\n const isHidden = context.selectAll('.photo-frame.pannellum-frame.hide').size();\n\n if (isHidden) {\n context\n .selectAll('.photo-frame:not(.pannellum-frame)')\n .classed('hide', true);\n\n context\n .selectAll('.photo-frame.pannellum-frame')\n .classed('hide', false);\n }\n\n return module;\n };\n\n /**\n * Hides the photo frame if shown\n * @param {*} context the HTML wrap of the frame\n */\n module.hidePhotoFrame = function(viewerContext) {\n viewerContext\n .select('photo-frame.pannellum-frame')\n .classed('hide', false);\n\n return module;\n };\n\n /**\n * Renders an image inside the frame\n * @param {*} data the image data, it should contain an image_path attribute, a link to the actual image.\n * @param {boolean} keepOrientation if true, HFOV, pitch and yaw will be kept between images\n */\n module.selectPhoto = function(data, keepOrientation) {\n const key = data.image_path;\n _activeSceneKey = key;\n if (!_currScenes.includes(key)) {\n let newSceneOptions = {\n showFullscreenCtrl: false,\n autoLoad: false,\n compass: false,\n yaw: 0,\n type: 'equirectangular',\n preview: data.preview_path,\n panorama: data.image_path,\n northOffset: data.ca\n };\n\n _currScenes.push(key);\n _pannellumViewer.addScene(key, newSceneOptions);\n }\n\n let yaw = 0;\n let pitch = 0;\n let hfov = 0;\n\n if (keepOrientation) {\n yaw = module.getYaw();\n pitch = module.getPitch();\n hfov = module.getHfov();\n }\n if (_pannellumViewer.isLoaded() !== false) {\n _pannellumViewer.loadScene(key, pitch, yaw, hfov);\n dispatch.call('viewerChanged');\n } else {\n // pannellum is currently loading another scene: wait for it to finish\n // loading the previous panorama first\n const retry = setInterval(() => {\n if (_pannellumViewer.isLoaded() === false) {\n // still not done: wait a bit longer\n return;\n }\n if (_activeSceneKey === key) {\n // only load scene if no other photo has been selected in the meantime\n _pannellumViewer.loadScene(key, pitch, yaw, hfov);\n dispatch.call('viewerChanged');\n }\n clearInterval(retry);\n }, 100);\n }\n\n if (_currScenes.length > 3) {\n const old_key = _currScenes.shift();\n _pannellumViewer.removeScene(old_key);\n }\n\n _pannellumViewer.resize();\n\n return module;\n };\n\n module.getYaw = function() {\n return _pannellumViewer.getYaw();\n };\n\n module.getPitch = function() {\n return _pannellumViewer.getPitch();\n };\n\n module.getHfov = function() {\n return _pannellumViewer.getHfov();\n };\n\n return module;\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\nimport { utilSetTransform, utilRebind } from '../util';\n\n\nexport async function planePhotoFrame(context, selection) {\n const dispatch = d3_dispatch('viewerChanged');\n\n const module = {};\n module.event = utilRebind(module, dispatch, 'on');\n\n let _photo;\n let _imageWrapper;\n let _planeWrapper;\n let _viewerDimensions = [];\n let _photoDimensions = [];\n const _imgZoom = d3_zoom()\n .on('zoom', zoomPan)\n .on('start', () => _imageWrapper.classed('grabbing', true))\n .on('end', () => _imageWrapper.classed('grabbing', false));\n\n function zoomPan(d3_event) {\n let t = d3_event.transform;\n _imageWrapper.call(utilSetTransform, t.x, t.y, t.k);\n }\n\n function loadImage(selection, path) {\n return new Promise((resolve) => {\n selection.attr('src', path);\n selection.on('load', () => {\n resolve(selection);\n });\n });\n }\n\n function updateTransform() {\n const xScale = _viewerDimensions[0] / _photoDimensions[0];\n const yScale = _viewerDimensions[1] / _photoDimensions[1];\n const fitScale = Math.max(xScale, yScale);\n const minScale = Math.min(xScale, yScale);\n _imgZoom\n .extent([[0, 0], _viewerDimensions])\n .translateExtent([[0, 0], _photoDimensions])\n .scaleExtent([minScale, 4]);\n const centerOffset = [0, 0];\n if (xScale < yScale) {\n centerOffset[0] = (_viewerDimensions[0] / fitScale - _photoDimensions[0]) / 2;\n } else {\n centerOffset[1] = (_viewerDimensions[1] / fitScale - _photoDimensions[1]) / 2;\n }\n const transform = d3_zoomIdentity.scale(fitScale).translate(centerOffset[0], centerOffset[1]);\n _planeWrapper.call(_imgZoom.transform, transform);\n }\n\n _planeWrapper = selection.append('div')\n .classed('plane-frame-wrapper', true);\n _planeWrapper.call(_imgZoom);\n\n _imageWrapper = _planeWrapper\n .append('div')\n .classed('photo-frame', true)\n .classed('plane-frame', true)\n .classed('hide', true);\n\n _photo = _imageWrapper\n .append('img')\n .attr('class', 'plane-photo');\n\n context.ui().photoviewer.on('resize.plane', function(dimensions) {\n _viewerDimensions = dimensions;\n updateTransform();\n });\n\n await Promise.resolve();\n\n /**\n * Shows the photo frame if hidden\n * @param {*} selection the HTML wrap of the frame\n */\n module.showPhotoFrame = function(selection) {\n const isHidden = selection.selectAll('.photo-frame.plane-frame.hide').size();\n\n if (isHidden) {\n selection\n .selectAll('.photo-frame:not(.plane-frame)')\n .classed('hide', true);\n\n selection\n .selectAll('.photo-frame.plane-frame')\n .classed('hide', false);\n }\n\n // set initial viewer size\n _viewerDimensions = context.ui().photoviewer.viewerSize();\n updateTransform();\n\n return module;\n };\n\n /**\n * Hides the photo frame if shown\n * @param {*} context the HTML wrap of the frame\n */\n module.hidePhotoFrame = function(context) {\n context\n .select('photo-frame.plane-frame')\n .classed('hide', false);\n\n return module;\n };\n\n /**\n * Renders an image inside the frame\n * @param {*} data the image data, it should contain an image_path attribute, a link to the actual image.\n */\n module.selectPhoto = function(data) {\n dispatch.call('viewerChanged');\n\n loadImage(_photo, '');\n _planeWrapper.classed('show-loader', true);\n loadImage(_photo, data.image_path)\n .then(selection => {\n _planeWrapper.classed('show-loader', false);\n const { naturalWidth, naturalHeight } = selection.node();\n _photoDimensions = [naturalWidth, naturalHeight];\n updateTransform();\n });\n return module;\n };\n\n module.getYaw = function() {\n return 0;\n };\n\n return module;\n};\n", "import { json as d3_json, xml as d3_xml} from 'd3-fetch';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { pairs as d3_pairs } from 'd3-array';\nimport RBush from 'rbush';\nimport { iso1A2Codes } from '@rapideditor/country-coder';\nimport { t } from '../core/localizer';\nimport { utilQsString, utilTiler, utilRebind, utilArrayUnion, utilStringQs } from '../util';\nimport { searchLimited } from '../util/partition';\nimport { localeTimestamp } from '../util/date';\nimport { geoExtent, geoVecAngle, geoVecEqual } from '../geo';\nimport { pannellumPhotoFrame } from './pannellum_photo';\nimport { planePhotoFrame } from './plane_photo';\nimport { services } from './';\n\n\nconst owsEndpoint = 'https://www.vegvesen.no/kart/ogc/vegbilder_1_0/ows?';\nconst tileZoom = 14;\nconst tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);\nconst dispatch = d3_dispatch('loadedImages', 'viewerChanged');\nconst directionEnum = Object.freeze({\n forward: Symbol(0),\n backward: Symbol(1)\n});\n\nlet _planeFrame;\nlet _pannellumFrame;\nlet _currentFrame;\nlet _loadViewerPromise;\nlet _vegbilderCache;\n\nasync function fetchAvailableLayers() {\n const params = {\n service: 'WFS',\n request: 'GetCapabilities',\n version: '2.0.0',\n };\n\n const urlForRequest = owsEndpoint + utilQsString(params);\n const response = await d3_xml(urlForRequest);\n const regexMatcher = /^vegbilder_1_0:Vegbilder(?_360)?_(?\\d{4})$/;\n const availableLayers = [];\n for (const node of response.querySelectorAll('FeatureType > Name')) {\n const match = node.textContent?.match(regexMatcher);\n if (match) {\n availableLayers.push({\n name: match[0],\n is_sphere: !!match.groups?.image_type,\n year: parseInt(match.groups?.year, 10)\n });\n }\n }\n return availableLayers;\n}\n\nfunction filterAvailableLayers(photoContex) {\n const fromDateString = photoContex.fromDate();\n const toDateString = photoContex.toDate();\n const fromYear = fromDateString ? new Date(fromDateString).getFullYear() : 2016;\n const toYear = toDateString ? new Date(toDateString).getFullYear() : null;\n const showsFlat = photoContex.showsFlat();\n const showsPano = photoContex.showsPanoramic();\n return Array.from(_vegbilderCache.wfslayers.values()).filter(({layerInfo}) => (\n (layerInfo.year >= fromYear) &&\n (!toYear || (layerInfo.year <= toYear)) &&\n ((!layerInfo.is_sphere && showsFlat) || (layerInfo.is_sphere && showsPano))\n ));\n}\n\nfunction loadWFSLayers(projection, margin, wfslayers) {\n const tiles = tiler.margin(margin).getTiles(projection);\n for (const cache of wfslayers) {\n loadWFSLayer(projection, cache, tiles);\n }\n}\n\nfunction loadWFSLayer(projection, cache, tiles) {\n // abort inflight requests that are no longer needed\n for (const [key, controller] of cache.inflight.entries()) {\n const wanted = tiles.some(tile => key === tile.id);\n if (!wanted) {\n controller.abort();\n cache.inflight.delete(key);\n }\n }\n\n Promise.all(tiles.map(\n tile => loadTile(cache, cache.layerInfo.name, tile)\n )).then(() => orderSequences(projection, cache));\n}\n\n/**\n* loadNextTilePage() load data for the next tile page in line.\n*/\nasync function loadTile(cache, typename, tile) {\n const bbox = tile.extent.bbox();\n const tileid = tile.id;\n if ((cache.loaded.get(tileid) === true) || cache.inflight.has(tileid)) return;\n\n const params = {\n service: 'WFS',\n request: 'GetFeature',\n version: '2.0.0',\n typenames: typename,\n bbox: [bbox.minY, bbox.minX, bbox.maxY, bbox.maxX].join(','),\n outputFormat: 'json'\n };\n\n const controller = new AbortController();\n cache.inflight.set(tileid, controller);\n\n const options = {\n method: 'GET',\n signal: controller.signal,\n };\n\n const urlForRequest = owsEndpoint + utilQsString(params);\n\n let featureCollection;\n try {\n featureCollection = await d3_json(urlForRequest, options);\n } catch {\n cache.loaded.set(tileid, false);\n return;\n } finally {\n cache.inflight.delete(tileid);\n }\n\n cache.loaded.set(tileid, true);\n\n if (featureCollection.features.length === 0) { return; }\n\n const features = featureCollection.features.map(feature => {\n const loc = feature.geometry.coordinates;\n const key = feature.id;\n const properties = feature.properties;\n const {\n RETNING: ca,\n TIDSPUNKT: captured_at,\n URL: image_path,\n URLPREVIEW : preview_path,\n BILDETYPE: image_type,\n METER: metering,\n FELTKODE: lane_code\n } = properties;\n const lane_number = parseInt((lane_code.match(/^[0-9]+/) || [])[0], 10);\n const direction = lane_number % 2 === 0 ? directionEnum.backward : directionEnum.forward;\n const data = {\n service: 'photo',\n loc,\n key,\n ca,\n image_path,\n preview_path,\n road_reference: roadReference(properties),\n metering,\n lane_code,\n direction,\n captured_at: new Date(captured_at),\n is_sphere: image_type === '360'\n };\n\n cache.points.set(key, data);\n\n return {\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data\n };\n });\n\n _vegbilderCache.rtree.load(features);\n dispatch.call('loadedImages');\n}\n\nfunction orderSequences(projection, cache) {\n const {points} = cache;\n\n const grouped = Array.from(points.values()).reduce((grouped, image) => {\n const key = image.road_reference;\n if (grouped.has(key)) {\n grouped.get(key).push(image);\n } else {\n grouped.set(key, [image]);\n }\n return grouped;\n }, new Map());\n\n const imageSequences = Array.from(grouped.values()).reduce((imageSequences, imageGroup) => {\n imageGroup.sort((a, b) => {\n if (a.captured_at.valueOf() > b.captured_at.valueOf()) {\n return 1;\n } else if (a.captured_at.valueOf() < b.captured_at.valueOf()) {\n return -1;\n } else {\n const {direction} = a;\n if (direction === directionEnum.forward) {\n return a.metering - b.metering;\n } else {\n return b.metering - a.metering;\n }\n }\n });\n let imageSequence = [imageGroup[0]];\n let angle = null;\n for (const [lastImage, image] of d3_pairs(imageGroup)) {\n if (lastImage.ca === null) {\n const b = projection(lastImage.loc);\n const a = projection(image.loc);\n if (!geoVecEqual(a, b)) {\n angle = geoVecAngle(a, b);\n angle *= (180 / Math.PI);\n angle -= 90;\n angle = angle >= 0 ? angle : angle + 360;\n }\n lastImage.ca = angle;\n }\n if (\n image.direction === lastImage.direction &&\n image.captured_at.valueOf() - lastImage.captured_at.valueOf() <= 20000\n ) {\n imageSequence.push(image);\n } else {\n imageSequences.push(imageSequence);\n imageSequence = [image];\n }\n }\n imageSequences.push(imageSequence);\n return imageSequences;\n }, []);\n\n cache.sequences = imageSequences.map(images => {\n const sequence = {\n images,\n key: images[0].key,\n geometry : {\n type : 'LineString',\n coordinates : images.map(image => image.loc)\n }\n };\n for (const image of images) {\n _vegbilderCache.image2sequence_map.set(image.key, sequence);\n }\n return sequence;\n });\n}\n\nfunction roadReference(properties) {\n const {\n FYLKENUMMER: county_number,\n VEGKATEGORI: road_class,\n VEGSTATUS: road_status,\n VEGNUMMER: road_number,\n STREKNING: section,\n DELSTREKNING: subsection,\n HP: parcel,\n KRYSSDEL: junction_part,\n SIDEANLEGGSDEL: services_part,\n ANKERPUNKT: anker_point,\n AAR: year,\n } = properties;\n\n let reference;\n\n if (year >= 2020) {\n reference = `${road_class}${road_status}${road_number} S${section}D${subsection}`;\n if (junction_part) {\n reference = `${reference} M${anker_point} KD${junction_part}`;\n } else if (services_part) {\n reference = `${reference} M${anker_point} SD${services_part}`;\n }\n } else {\n reference = `${county_number}${road_class}${road_status}${road_number} HP${parcel}`;\n }\n\n return reference;\n}\n\nexport default {\n\n init: function () {\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset: async function () {\n if (_vegbilderCache) {\n for (const layer of _vegbilderCache.wfslayers.values()) {\n for (const controller of layer.inflight.values()) {\n controller.abort();\n }\n }\n }\n\n _vegbilderCache = {\n wfslayers: new Map(),\n rtree: new RBush(),\n image2sequence_map: new Map()\n };\n\n const availableLayers = await fetchAvailableLayers();\n const {wfslayers} = _vegbilderCache;\n\n for (const layerInfo of availableLayers) {\n const cache = {\n layerInfo,\n loaded: new Map(),\n inflight: new Map(),\n points: new Map(),\n sequences: []\n };\n wfslayers.set(layerInfo.name, cache);\n }\n },\n\n images: function (projection) {\n const limit = 5;\n return searchLimited(limit, projection, _vegbilderCache.rtree);\n },\n\n\n sequences: function (projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const seen = new Set();\n const line_strings = [];\n\n for (const {data} of _vegbilderCache.rtree.search(bbox)) {\n const sequence = _vegbilderCache.image2sequence_map.get(data.key);\n if (!sequence) continue;\n const {key, geometry, images} = sequence;\n if (seen.has(key)) continue;\n seen.add(key);\n const line = {\n type: 'LineString',\n coordinates: geometry.coordinates,\n key,\n images\n };\n line_strings.push(line);\n }\n return line_strings;\n },\n\n cachedImage: function (key) {\n for (const {points} of _vegbilderCache.wfslayers.values()) {\n if (points.has(key)) return points.get(key);\n }\n },\n\n getSequenceForImage: function (image) {\n return _vegbilderCache?.image2sequence_map.get(image?.key);\n },\n\n loadImages: async function (context, margin) {\n if (!_vegbilderCache) {\n await this.reset();\n }\n margin ??= 1;\n const wfslayers = filterAvailableLayers(context.photos());\n loadWFSLayers(context.projection, margin, wfslayers);\n },\n\n photoFrame: function() {\n return _currentFrame;\n },\n\n ensureViewerLoaded: function(context) {\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n const step = (stepBy) => () => {\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n if (!selected) return;\n\n const sequence = this.getSequenceForImage(selected);\n const nextIndex = sequence.images.indexOf(selected) + stepBy;\n const nextImage = sequence.images[nextIndex];\n\n if (!nextImage) return;\n\n context.map().centerEase(nextImage.loc);\n this.selectImage(context, nextImage.key, true);\n };\n\n const wrap = context.container().select('.photoviewer')\n .selectAll('.vegbilder-wrapper')\n .data([0]);\n\n const wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper vegbilder-wrapper')\n .classed('hide', true);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n const controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls');\n\n controlsEnter\n .append('button')\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .on('click.forward', step(1))\n .text('\u25BA');\n\n _loadViewerPromise = Promise.all([\n pannellumPhotoFrame(context, wrapEnter),\n planePhotoFrame(context, wrapEnter)\n ]).then(([pannellumPhotoFrame, planePhotoFrame]) => {\n _pannellumFrame = pannellumPhotoFrame;\n _pannellumFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n _planeFrame = planePhotoFrame;\n _planeFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n });\n\n return _loadViewerPromise;\n },\n\n selectImage: function(context, key, keepOrientation) {\n const d = this.cachedImage(key);\n this.updateUrlImage(key);\n\n const viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) { viewer.datum(d); }\n\n this.setStyles(context, null, true);\n\n if (!d) return this;\n\n const wrap = context.container().select('.photoviewer .vegbilder-wrapper');\n const attribution = wrap.selectAll('.photo-attribution').text('');\n\n if (d.captured_at) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeTimestamp(d.captured_at));\n }\n\n attribution\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://vegvesen.no')\n .call(t.append('vegbilder.publisher'));\n\n attribution\n .append('a')\n .attr('target', '_blank')\n .attr('href', `https://vegbilder.atlas.vegvesen.no/?year=${d.captured_at.getFullYear()}&lat=${d.loc[1]}&lng=${d.loc[0]}&view=image&imageId=${d.key}`)\n .call(t.append('vegbilder.view_on'));\n\n _currentFrame = d.is_sphere? _pannellumFrame : _planeFrame;\n\n _currentFrame\n .showPhotoFrame(wrap)\n .selectPhoto(d, keepOrientation);\n\n return this;\n },\n\n showViewer: function (context) {\n const viewer = context.container().select('.photoviewer');\n const isHidden = viewer.selectAll('.photo-wrapper.vegbilder-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n viewer\n .classed('hide', false)\n .selectAll('.photo-wrapper.vegbilder-wrapper')\n .classed('hide', false);\n }\n return this;\n },\n\n hideViewer: function(context) {\n this.updateUrlImage(null);\n\n const viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence')\n .classed('currentView', false);\n\n return this.setStyles(context, null, true);\n },\n\n\n // Updates the currently highlighted sequence and selected bubble.\n // Reset is only necessary when interacting with the viewport because\n // this implicitly changes the currently selected bubble/sequence\n setStyles: function (context, hovered, reset) {\n if (reset) { // reset all layers\n context.container().selectAll('.viewfield-group')\n .classed('highlighted', false)\n .classed('hovered', false)\n .classed('currentView', false);\n\n context.container().selectAll('.sequence')\n .classed('highlighted', false)\n .classed('currentView', false);\n }\n\n const hoveredImageKey = hovered?.key;\n const hoveredSequence = this.getSequenceForImage(hovered);\n const hoveredSequenceKey = hoveredSequence?.key;\n const hoveredImageKeys = hoveredSequence?.images.map(d => d.key) ?? [];\n\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n const selectedImageKey = selected?.key;\n const selectedSequence = this.getSequenceForImage(selected);\n const selectedSequenceKey = selectedSequence?.key;\n const selectedImageKeys = selectedSequence?.images.map(d => d.key) ?? [];\n\n // highlight sibling viewfields on either the selected or the hovered sequences\n const highlightedImageKeys = utilArrayUnion(hoveredImageKeys, selectedImageKeys);\n\n context.container().selectAll('.layer-vegbilder .viewfield-group')\n .classed('highlighted', d => highlightedImageKeys.indexOf(d.key) !== -1)\n .classed('hovered', d => d.key === hoveredImageKey)\n .classed('currentView', d => d.key === selectedImageKey);\n\n context.container().selectAll('.layer-vegbilder .sequence')\n .classed('highlighted', d => d.key === hoveredSequenceKey)\n .classed('currentView', d => d.key === selectedSequenceKey);\n\n // update viewfields if needed\n context.container().selectAll('.layer-vegbilder .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n const d = this.parentNode.__data__;\n if (d.is_sphere && d.key !== selectedImageKey) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n return this;\n },\n\n updateUrlImage: function (key) {\n const hash = utilStringQs(window.location.hash);\n if (key) {\n hash.photo = 'vegbilder/' + key;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n validHere: function(extent) {\n const bbox = Object.values(extent.bbox());\n return iso1A2Codes(bbox).includes('NO');\n },\n\n\n cache: function () {\n return _vegbilderCache;\n }\n\n};\n", "\n/**\n * osmAuth\n * Easy authentication with OpenStreetMap over OAuth 2.0.\n * @module\n *\n * @param o `Object` containing options:\n * @param o.scope OAuth2 scopes requested (e.g. \"read_prefs write_api\")\n * @param o.client_id OAuth2 client ID\n * @param o.redirect_uri OAuth2 redirect URI (e.g. \"http://127.0.0.1:8080/land.html\")\n * @param o.access_token Can pre-authorize with an OAuth2 bearer token if you have one\n * @param o.apiUrl A base url for the OSM API (default: \"https://api.openstreetmap.org\")\n * @param o.url A base url for the OAuth2 handshake (default: \"https://www.openstreetmap.org\")\n * @param o.auto If `true`, attempt to authenticate automatically when calling `.xhr()` or `.fetch()` (default: `false`)\n * @param o.singlepage If `true`, use page redirection instead of a popup (default: `false`)\n * @param o.loading Function called when auth-related xhr calls start\n * @param o.done Function called when auth-related xhr calls end\n * @param o.locale The locale to use on the OAuth2 authentication page. Optional.\n * @return `self`\n */\nexport function osmAuth(o) {\n var oauth = {};\n\n var CHANNEL_ID = 'osm-api-auth-complete';\n\n // Mock localStorage if needed.\n // Note that accessing localStorage may throw a `SecurityError`, so wrap in a try/catch.\n var _store = null;\n try {\n if (!('localStorage' in globalThis)) {\n throw new Error('No localStorage');\n }\n _store = globalThis.localStorage;\n\n } catch (e) {\n var _mock = new Map();\n _store = {\n isMocked: true,\n hasItem: (k) => _mock.has(k),\n getItem: (k) => _mock.get(k),\n setItem: (k, v) => _mock.set(k, v),\n removeItem: (k) => _mock.delete(k),\n clear: () => _mock.clear()\n };\n }\n\n /**\n * token\n * Get/Set tokens. These are prefixed with the base URL so that `osm-auth`\n * can be used with multiple APIs and the keys in `localStorage` will not clash\n * @param {string} k key\n * @param {string?} v value\n * @return {string?} If getting, returns the stored value or `null`. If setting, returns `undefined`.\n */\n function token(k, v) {\n var key = o.url + k;\n if (arguments.length === 1) {\n var val = _store.getItem(key) || '';\n // Note: legacy tokens might be wrapped in double quotes - remove them, see #129\n return val.replace(/\"/g, '');\n\n } else if (arguments.length === 2) {\n if (v) {\n return _store.setItem(key, v);\n } else {\n return _store.removeItem(key);\n }\n }\n }\n\n\n /**\n * authenticated\n * Test whether the user is currently authenticated\n *\n * @return {boolean} `true` if authenticated, `false` if not\n */\n oauth.authenticated = function() {\n return !!token('oauth2_access_token');\n };\n\n\n /**\n * logout\n * Removes any stored authentication tokens (legacy OAuth1 tokens too)\n *\n * @return `self`\n */\n oauth.logout = function () {\n token('oauth2_access_token', ''); // OAuth2\n token('oauth_token', ''); // OAuth1\n token('oauth_token_secret', ''); // OAuth1\n token('oauth_request_token_secret', ''); // OAuth1\n return oauth;\n };\n\n\n /**\n * authenticate\n * First logs out, then runs the authentiation flow, finally calls the callback.\n * TODO: detect lack of click event (probably can settimeout it)\n *\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @param {LoginOptions} [options] Other options\n * @return none\n */\n oauth.authenticate = function(callback, options) {\n if (oauth.authenticated()) {\n callback(null, oauth);\n return;\n }\n\n oauth.logout();\n\n _preopenPopup(function(error, popup) {\n if (error) {\n callback(error);\n } else {\n _generatePkceChallenge(function(pkce) {\n _authenticate(pkce, options, popup, callback);\n });\n }\n });\n };\n\n\n /**\n * authenticateAsync\n * Promisified version of `authenticate`\n * @param {LoginOptions} [options]\n * @return {Promise} Promise settled with whatever `_authenticate` did\n */\n oauth.authenticateAsync = function(options) {\n if (oauth.authenticated()) {\n return Promise.resolve(oauth);\n }\n\n oauth.logout();\n\n return new Promise((resolve, reject) => {\n var errback = (err, result) => {\n if (err) {\n reject(err);\n } else {\n resolve(result);\n }\n };\n\n _preopenPopup((error, popup) => {\n if (error) {\n errback(error);\n } else {\n _generatePkceChallenge(pkce => _authenticate(pkce, options, popup, errback));\n }\n });\n });\n };\n\n\n /**\n * opens an empty popup to be later used for the authentication page\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @return none\n */\n function _preopenPopup(callback) {\n if (o.singlepage) {\n callback(null, undefined);\n return;\n }\n\n // Create a 550x610 popup window in the center of the screen\n var w = 550;\n var h = 610;\n var settings = [\n ['width', w],\n ['height', h],\n ['left', window.screen.width / 2 - w / 2],\n ['top', window.screen.height / 2 - h / 2],\n ]\n .map(function (x) { return x.join('='); })\n .join(',');\n var popup = window.open('about:blank', 'oauth_window', settings);\n if (popup) {\n callback(null, popup);\n } else {\n var error = new Error('Popup was blocked');\n error.status = 'popup-blocked';\n callback(error);\n }\n }\n\n\n /**\n * _authenticate\n * internal authenticate\n *\n * @typedef {{ switchUser?: boolean }} LoginOptions\n *\n * @param {Object} pkce Object containing PKCE code challenge properties\n * @param {LoginOptions=} options Other options\n * @param {Window} popup Popup Window to use for the authentication page, should be undefined when using singlepage mode\n * @param {function} callback Errback-style callback that accepts `(err, result)`\n */\n function _authenticate(pkce, options, popup, callback) {\n var state = generateState();\n\n // ## Request authorization to access resources from the user\n // and receive authorization code\n var path =\n '/oauth2/authorize?' +\n utilQsString({\n client_id: o.client_id,\n redirect_uri: o.redirect_uri,\n response_type: 'code',\n scope: o.scope,\n state: state,\n code_challenge: pkce.code_challenge,\n code_challenge_method: pkce.code_challenge_method,\n locale: o.locale || '',\n });\n\n var url = options?.switchUser\n ? `${o.url}/logout?referer=${encodeURIComponent(`/login?referer=${encodeURIComponent(path)}`)}`\n : o.url + path;\n\n if (o.singlepage) {\n if (_store.isMocked) {\n // in singlepage mode, PKCE requires working non-volatile storage\n var error = new Error('localStorage unavailable, but required in singlepage mode');\n error.status = 'pkce-localstorage-unavailable';\n callback(error);\n return;\n }\n var params = utilStringQs(window.location.search.slice(1));\n if (params.code) {\n oauth.bootstrapToken(params.code, callback);\n } else {\n // save OAuth2 state and PKCE challenge in local storage, for later use\n // in the `/oauth/token` request\n token('oauth2_state', state);\n token('oauth2_pkce_code_verifier', pkce.code_verifier);\n window.location = url;\n }\n } else {\n oauth.popupWindow = popup;\n popup.location = url;\n }\n\n // Called by a function in the redirect URL page, in the popup window. The\n // window closes itself.\n var bc = new BroadcastChannel(CHANNEL_ID);\n bc.addEventListener('message', (event) => {\n var url = event.data;\n var params = utilStringQs(url.split('?')[1]);\n if (params.state !== state) {\n var error = new Error('Invalid state');\n error.status = 'invalid-state';\n callback(error);\n return;\n } else if (params.error !== undefined) {\n var err = new Error(params.error_description.replace(/\\+/g, ' '));\n err.status = params.error;\n callback(err);\n return;\n }\n _getAccessToken(params.code, pkce.code_verifier, accessTokenDone);\n bc.close();\n });\n\n function accessTokenDone(err, xhr) {\n o.done();\n if (err) {\n callback(err);\n return;\n }\n var access_token = JSON.parse(xhr.response);\n token('oauth2_access_token', access_token.access_token);\n callback(null, oauth);\n }\n }\n\n\n /**\n * _getAccessToken\n * The client requests an access token by authenticating with the\n * authorization server and presenting the `auth_code`, brought\n * in from a function call on a landing page popup.\n * @param {string} auth_code\n * @param {string} code_verifier\n * @param {function} accessTokenDone Errback-style callback `(err, result)`, called when complete\n */\n function _getAccessToken(auth_code, code_verifier, accessTokenDone) {\n var url =\n o.url +\n '/oauth2/token?' +\n utilQsString({\n client_id: o.client_id,\n redirect_uri: o.redirect_uri,\n grant_type: 'authorization_code',\n code: auth_code,\n code_verifier: code_verifier\n });\n\n // The authorization server authenticates the client and validates\n // the authorization grant, and if valid, issues an access token.\n oauth.rawxhr('POST', url, null, null, null, accessTokenDone);\n o.loading();\n }\n\n\n /**\n * bringPopupWindowToFront\n * Tries to bring an existing authentication popup to the front.\n *\n * @return {boolean} `true` if it succeeded, `false` if not\n */\n oauth.bringPopupWindowToFront = function() {\n var broughtPopupToFront = false;\n try {\n // This may cause a cross-origin error:\n // `DOMException: Blocked a frame with origin \"...\" from accessing a cross-origin frame.`\n if (oauth.popupWindow && !oauth.popupWindow.closed) {\n oauth.popupWindow.focus();\n broughtPopupToFront = true;\n }\n } catch (err) {\n // Bringing popup window to front failed (probably because of the cross-origin error mentioned above)\n }\n return broughtPopupToFront;\n };\n\n\n /**\n * bootstrapToken\n * The authorization code is a temporary code that a client can exchange for an access token.\n * If using this library in single-page mode, you'll need to call this once your application\n * has an `auth_code` and wants to get an access_token.\n *\n * @param {string} auth_code The OAuth2 `auth_code`\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @return none\n */\n oauth.bootstrapToken = function(auth_code, callback) {\n var state = token('oauth2_state');\n token('oauth2_state', '');\n var params = utilStringQs(window.location.search.slice(1));\n if (params.state !== state) {\n var error = new Error('Invalid state');\n error.status = 'invalid-state';\n callback(error);\n return;\n }\n var code_verifier = token('oauth2_pkce_code_verifier');\n token('oauth2_pkce_code_verifier', '');\n _getAccessToken(auth_code, code_verifier, accessTokenDone);\n\n function accessTokenDone(err, xhr) {\n o.done();\n if (err) {\n callback(err);\n return;\n }\n var access_token = JSON.parse(xhr.response);\n token('oauth2_access_token', access_token.access_token);\n callback(null, oauth);\n }\n };\n\n\n /**\n * fetch\n * A `fetch` wrapper that includes the Authorization header if the user is authenticated.\n * https://developer.mozilla.org/en-US/docs/Web/API/fetch\n *\n * @param {string} resource Resource passed to `fetch`\n * @param {Object} options Options passed to `fetch`\n * @return {Promise} Promise that wraps `authenticateAsync` then `fetch`\n */\n oauth.fetch = function(resource, options) {\n if (oauth.authenticated()) {\n return _doFetch();\n } else {\n if (o.auto) {\n return oauth.authenticateAsync().then(_doFetch);\n } else {\n return Promise.reject(new Error('not authenticated'));\n }\n }\n\n function _doFetch() {\n options = options || {};\n if (!options.headers) {\n options.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };\n }\n options.headers.Authorization = 'Bearer ' + token('oauth2_access_token');\n return fetch(resource, options);\n }\n };\n\n\n /**\n * xhr\n * A `XMLHttpRequest` wrapper that does authenticated calls if the user has logged in.\n * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest\n *\n * @param {Object} options\n * @param options.method Passed to `xhr.open` (e.g. 'GET', 'POST')\n * @param options.prefix If `true` path contains a path, if `false` path contains the full url\n * @param options.path The URL path (e.g. \"/api/0.6/user/details\") (or full url, if `prefix`=`false`)\n * @param options.content Passed to `xhr.send`\n * @param options.headers `Object` containing request headers\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @return {XMLHttpRequest} `XMLHttpRequest` if authenticated, otherwise `null`\n */\n oauth.xhr = function (options, callback) {\n if (oauth.authenticated()) {\n return _doXHR();\n } else {\n if (o.auto) {\n oauth.authenticate(_doXHR);\n return;\n } else {\n callback('not authenticated', null);\n return;\n }\n }\n\n function _doXHR() {\n var url = options.prefix !== false ? (o.apiUrl + options.path) : options.path;\n return oauth.rawxhr(\n options.method,\n url,\n token('oauth2_access_token'),\n options.content,\n options.headers,\n done\n );\n }\n\n function done(err, xhr) {\n if (err) {\n callback(err);\n } else if (xhr.responseXML) {\n callback(err, xhr.responseXML);\n } else {\n callback(err, xhr.response);\n }\n }\n };\n\n\n /**\n * rawxhr\n * Creates the XMLHttpRequest set up with a header and response handling\n * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest\n *\n * @param method Passed to `xhr.open` (e.g. 'GET', 'POST')\n * @param url Passed to `xhr.open`\n * @param access_token OAuth2 bearer token\n * @param data Passed to `xhr.send`\n * @param headers `Object` containing request headers\n * @param callback An \"errback\"-style callback (`err`, `result`), called when complete\n * @return `XMLHttpRequest`\n */\n oauth.rawxhr = function(method, url, access_token, data, headers, callback) {\n headers = headers || { 'Content-Type': 'application/x-www-form-urlencoded' };\n\n if (access_token) {\n headers.Authorization = 'Bearer ' + access_token;\n }\n\n var xhr = new XMLHttpRequest();\n\n xhr.onreadystatechange = function () {\n if (4 === xhr.readyState && 0 !== xhr.status) {\n if (/^20\\d$/.test(xhr.status)) { // a 20x status code - OK\n callback(null, xhr);\n } else {\n callback(xhr, null);\n }\n }\n };\n xhr.onerror = function (e) {\n callback(e, null);\n };\n\n xhr.open(method, url, true);\n for (var h in headers) xhr.setRequestHeader(h, headers[h]);\n\n xhr.send(data);\n return xhr;\n };\n\n\n /**\n * preauth\n * Pre-authorize this object, if we already have access token from the start\n *\n * @param {Object} val Object containing `access_token` property\n * @return `self`\n */\n oauth.preauth = function(val) {\n if (val && val.access_token) {\n token('oauth2_access_token', val.access_token);\n }\n return oauth;\n };\n\n\n /**\n * options (getter / setter)\n * If passed with no arguments, just return the options\n * If passed an Object, set the options then attempt to pre-authorize\n *\n * @param val? Object containing options\n * @return current `options` (if getting), or `self` (if setting)\n */\n oauth.options = function(val) {\n if (!arguments.length) return o;\n\n o = val;\n o.apiUrl = o.apiUrl || 'https://api.openstreetmap.org';\n o.url = o.url || 'https://www.openstreetmap.org';\n o.auto = o.auto || false;\n o.singlepage = o.singlepage || false;\n\n // Optional loading and loading-done functions for nice UI feedback.\n // by default, no-ops\n o.loading = o.loading || function () {};\n o.done = o.done || function () {};\n return oauth.preauth(o);\n };\n\n\n // Everything below here is initialization/setup code\n // Handle options and attempt to pre-authorize\n oauth.options(o);\n\n return oauth;\n}\n\n\n/**\n * utilQsString\n * Transforms object of `key=value` pairs into query string\n * @param {Object} Object of `key=value` pairs\n * @returns {string} query string\n */\nfunction utilQsString(obj) {\n return Object.keys(obj)\n .filter(function(key) {\n return obj[key] !== undefined;\n })\n .sort()\n .map(function(key) {\n return (encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));\n })\n .join('&');\n}\n\n/**\n * utilStringQs\n * Transforms query string into object of `key=value` pairs\n * @param {string} query string\n * @returns {Object} Object of `key=value` pairs\n */\nfunction utilStringQs(str) {\n var i = 0; // advance past any leading '?' or '#' characters\n while (i < str.length && (str[i] === '?' || str[i] === '#')) i++;\n str = str.slice(i);\n\n return str.split('&').reduce(function(obj, pair) {\n var parts = pair.split('=');\n if (parts.length === 2) {\n obj[parts[0]] = decodeURIComponent(parts[1]);\n }\n return obj;\n }, {});\n}\n\n\n/**\n * Generates a challenge/verifier pair for PKCE.\n * If the browser does not support the WebCryptoAPI, the \"plain\" method is\n * used as a fallback instead of a SHA-256 hash.\n * @param {callback} callback called with the result of the generated PKCE challenge\n */\nfunction _generatePkceChallenge(callback) {\n var code_verifier;\n // generate a random code_verifier\n // https://datatracker.ietf.org/doc/html/rfc7636#section-7.1\n var random = globalThis.crypto.getRandomValues(new Uint8Array(32));\n code_verifier = base64(random.buffer);\n var verifier = Uint8Array.from(Array.from(code_verifier).map(function(char) {\n return char.charCodeAt(0);\n }));\n\n // generate challenge for code verifier\n globalThis.crypto.subtle.digest('SHA-256', verifier).then(function(hash) {\n var code_challenge = base64(hash);\n\n callback({\n code_challenge: code_challenge,\n code_verifier: code_verifier,\n code_challenge_method: 'S256'\n });\n });\n}\n\n\n/**\n * Returns a random state to be used as the \"state\" of the OAuth2 authentication\n * See https://datatracker.ietf.org/doc/html/rfc6749#section-10.12\n */\nfunction generateState() {\n var state;\n var random = globalThis.crypto.getRandomValues(new Uint8Array(32));\n state = base64(random.buffer);\n\n return state;\n}\n\n\n/**\n * base64\n * Converts binary buffer to base64 encoded string, as used in rfc7636\n * @param {ArrayBuffer} buffer\n * @returns {string} base64 encoded\n */\nfunction base64(buffer) {\n return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))\n .replace(/\\//g, '_')\n .replace(/\\+/g, '-')\n .replace(/[=]/g, '');\n}\n", "export var JXON = new (function () {\n var\n sValueProp = 'keyValue', sAttributesProp = 'keyAttributes', sAttrPref = '@', /* you can customize these values */\n aCache = [], rIsNull = /^\\s*$/, rIsBool = /^(?:true|false)$/i;\n\n function parseText (sValue) {\n if (rIsNull.test(sValue)) { return null; }\n if (rIsBool.test(sValue)) { return sValue.toLowerCase() === 'true'; }\n if (isFinite(sValue)) { return parseFloat(sValue); }\n if (isFinite(Date.parse(sValue))) { return new Date(sValue); }\n return sValue;\n }\n\n function EmptyTree () { }\n EmptyTree.prototype.toString = function () { return 'null'; };\n EmptyTree.prototype.valueOf = function () { return null; };\n\n function objectify (vValue) {\n return vValue === null ? new EmptyTree() : vValue instanceof Object ? vValue : new vValue.constructor(vValue);\n }\n\n function createObjTree (oParentNode, nVerb, bFreeze, bNesteAttr) {\n var\n nLevelStart = aCache.length, bChildren = oParentNode.hasChildNodes(),\n bAttributes = oParentNode.hasAttributes(), bHighVerb = Boolean(nVerb & 2);\n\n var\n sProp, vContent, nLength = 0, sCollectedTxt = '',\n vResult = bHighVerb ? {} : /* put here the default value for empty nodes: */ true;\n\n if (bChildren) {\n for (var oNode, nItem = 0; nItem < oParentNode.childNodes.length; nItem++) {\n oNode = oParentNode.childNodes.item(nItem);\n if (oNode.nodeType === 4) {\n /* nodeType is 'CDATASection' (4) */\n sCollectedTxt += oNode.nodeValue;\n } else if (oNode.nodeType === 3) {\n /* nodeType is 'Text' (3) */\n sCollectedTxt += oNode.nodeValue.trim();\n } else if (oNode.nodeType === 1 && !oNode.prefix) {\n /* nodeType is 'Element' (1) */\n aCache.push(oNode);\n }\n }\n }\n\n var nLevelEnd = aCache.length, vBuiltVal = parseText(sCollectedTxt);\n\n if (!bHighVerb && (bChildren || bAttributes)) { vResult = nVerb === 0 ? objectify(vBuiltVal) : {}; }\n\n for (var nElId = nLevelStart; nElId < nLevelEnd; nElId++) {\n sProp = aCache[nElId].nodeName.toLowerCase();\n vContent = createObjTree(aCache[nElId], nVerb, bFreeze, bNesteAttr);\n if (vResult.hasOwnProperty(sProp)) {\n if (vResult[sProp].constructor !== Array) { vResult[sProp] = [vResult[sProp]]; }\n vResult[sProp].push(vContent);\n } else {\n vResult[sProp] = vContent;\n nLength++;\n }\n }\n\n if (bAttributes) {\n var\n nAttrLen = oParentNode.attributes.length,\n sAPrefix = bNesteAttr ? '' : sAttrPref, oAttrParent = bNesteAttr ? {} : vResult;\n\n for (var oAttrib, nAttrib = 0; nAttrib < nAttrLen; nLength++, nAttrib++) {\n oAttrib = oParentNode.attributes.item(nAttrib);\n oAttrParent[sAPrefix + oAttrib.name.toLowerCase()] = parseText(oAttrib.value.trim());\n }\n\n if (bNesteAttr) {\n if (bFreeze) { Object.freeze(oAttrParent); }\n vResult[sAttributesProp] = oAttrParent;\n nLength -= nAttrLen - 1;\n }\n }\n\n if (nVerb === 3 || (nVerb === 2 || nVerb === 1 && nLength > 0) && sCollectedTxt) {\n vResult[sValueProp] = vBuiltVal;\n } else if (!bHighVerb && nLength === 0 && sCollectedTxt) {\n vResult = vBuiltVal;\n }\n\n if (bFreeze && (bHighVerb || nLength > 0)) { Object.freeze(vResult); }\n\n aCache.length = nLevelStart;\n\n return vResult;\n }\n\n function loadObjTree (oXMLDoc, oParentEl, oParentObj) {\n var vValue, oChild;\n\n if (oParentObj instanceof String || oParentObj instanceof Number || oParentObj instanceof Boolean) {\n oParentEl.appendChild(oXMLDoc.createTextNode(oParentObj.toString())); /* verbosity level is 0 */\n } else if (oParentObj.constructor === Date) {\n oParentEl.appendChild(oXMLDoc.createTextNode(oParentObj.toGMTString()));\n }\n\n for (var sName in oParentObj) {\n vValue = oParentObj[sName];\n if (isFinite(sName) || vValue instanceof Function) { continue; } /* verbosity level is 0 */\n if (sName === sValueProp) {\n if (vValue !== null && vValue !== true) { oParentEl.appendChild(oXMLDoc.createTextNode(vValue.constructor === Date ? vValue.toGMTString() : String(vValue))); }\n } else if (sName === sAttributesProp) { /* verbosity level is 3 */\n for (var sAttrib in vValue) { oParentEl.setAttribute(sAttrib, vValue[sAttrib]); }\n } else if (sName.charAt(0) === sAttrPref) {\n oParentEl.setAttribute(sName.slice(1), vValue);\n } else if (vValue.constructor === Array) {\n for (var nItem = 0; nItem < vValue.length; nItem++) {\n oChild = oXMLDoc.createElementNS(null, sName);\n loadObjTree(oXMLDoc, oChild, vValue[nItem]);\n oParentEl.appendChild(oChild);\n }\n } else {\n oChild = oXMLDoc.createElementNS(null, sName);\n if (vValue instanceof Object) {\n loadObjTree(oXMLDoc, oChild, vValue);\n } else if (vValue !== null && vValue !== true) {\n oChild.appendChild(oXMLDoc.createTextNode(vValue.toString()));\n }\n oParentEl.appendChild(oChild);\n }\n }\n }\n\n this.build = function (oXMLParent, nVerbosity /* optional */, bFreeze /* optional */, bNesteAttributes /* optional */) {\n var _nVerb = arguments.length > 1 && typeof nVerbosity === 'number' ? nVerbosity & 3 : /* put here the default verbosity level: */ 1;\n return createObjTree(oXMLParent, _nVerb, bFreeze || false, arguments.length > 3 ? bNesteAttributes : _nVerb === 3);\n };\n\n this.unbuild = function (oObjTree) {\n var oNewDoc = document.implementation.createDocument('', '', null);\n loadObjTree(oNewDoc, oNewDoc, oObjTree);\n return oNewDoc;\n };\n\n this.stringify = function (oObjTree) {\n return (new XMLSerializer()).serializeToString(JXON.unbuild(oObjTree));\n };\n})();\n\n// var myObject = JXON.build(doc);\n// we got our javascript object! try: alert(JSON.stringify(myObject));\n\n// var newDoc = JXON.unbuild(myObject);\n// we got our Document instance! try: alert((new XMLSerializer()).serializeToString(newDoc));\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { json as d3_json } from 'd3-fetch';\nimport { osmAuth } from 'osm-auth';\nimport RBush from 'rbush';\n\nimport { JXON } from '../util/jxon';\nimport { geoExtent, geoRawMercator, geoVecAdd, geoZoomToScale } from '../geo';\nimport { osmEntity, osmNode, osmNote, osmRelation, osmWay } from '../osm';\nimport { utilArrayChunk, utilArrayGroupBy, utilArrayUniq, utilObjectOmit, utilRebind, utilTiler, utilQsString } from '../util';\nimport { localizer } from '../core/localizer.js';\nimport { utilGzip } from '../util/util';\nimport { osmApiConnections } from '../../config/id.js';\n\n\nvar tiler = utilTiler();\nvar dispatch = d3_dispatch('apiStatusChange', 'authLoading', 'authDone', 'change', 'loading', 'loaded', 'loadedNotes');\n\nvar urlroot = osmApiConnections[0].url;\nvar apiUrlroot = osmApiConnections[0].apiUrl || urlroot;\nvar redirectPath = window.location.origin + window.location.pathname;\nvar oauth = new osmAuth({\n url: urlroot,\n apiUrl: apiUrlroot,\n client_id: osmApiConnections[0].client_id,\n scope: 'read_prefs write_prefs write_api read_gpx write_notes',\n redirect_uri: redirectPath + 'land.html',\n loading: authLoading,\n done: authDone\n});\nvar _apiConnections = osmApiConnections;\n\n// hardcode default block of Google Maps\nvar _imageryBlocklists = [/.*\\.google(apis)?\\..*\\/(vt|kh)[\\?\\/].*([xyz]=.*){3}.*/];\nvar _tileCache = { toLoad: {}, loaded: {}, inflight: {}, seen: {}, rtree: new RBush() };\nvar _noteCache = { toLoad: {}, loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: new RBush() };\nvar _userCache = { toLoad: {}, user: {} };\nvar _cachedApiStatus;\nvar _changeset = {};\n\nvar _deferred = new Set();\nvar _connectionID = 1;\nvar _tileZoom = 16;\nvar _noteZoom = 12;\nvar _rateLimitError;\nvar _userChangesets;\nvar _userDetails;\nvar _off;\n\n// set a default but also load this from the API status\nvar _maxWayNodes = 2000;\nlet _maxChangesetElements = 10_000;\n\n\nfunction authLoading() {\n dispatch.call('authLoading');\n}\n\n\nfunction authDone() {\n dispatch.call('authDone');\n}\n\n\nfunction abortRequest(controllerOrXHR) {\n if (controllerOrXHR) {\n controllerOrXHR.abort();\n }\n}\n\n\nfunction hasInflightRequests(cache) {\n return Object.keys(cache.inflight).length;\n}\n\n\nfunction abortUnwantedRequests(cache, visibleTiles) {\n Object.keys(cache.inflight).forEach(function(k) {\n if (cache.toLoad[k]) return;\n if (visibleTiles.find(function(tile) { return k === tile.id; })) return;\n\n abortRequest(cache.inflight[k]);\n delete cache.inflight[k];\n });\n}\n\nfunction getNodesJSON(obj) {\n var elems = obj.nodes;\n var nodes = new Array(elems.length);\n for (var i = 0, l = elems.length; i < l; i++) {\n nodes[i] = 'n' + elems[i];\n }\n return nodes;\n}\n\nfunction getMembersJSON(obj) {\n var elems = obj.members;\n var members = new Array(elems.length);\n for (var i = 0, l = elems.length; i < l; i++) {\n var attrs = elems[i];\n members[i] = {\n id: attrs.type[0] + attrs.ref,\n type: attrs.type,\n role: attrs.role\n };\n }\n return members;\n}\n\nfunction encodeNoteRtree(note) {\n return {\n minX: note.loc[0],\n minY: note.loc[1],\n maxX: note.loc[0],\n maxY: note.loc[1],\n data: note\n };\n}\n\n\nvar jsonparsers = {\n\n node: function nodeData(obj, uid) {\n return new osmNode({\n id: uid,\n visible: typeof obj.visible === 'boolean' ? obj.visible : true,\n version: obj.version && obj.version.toString(),\n changeset: obj.changeset && obj.changeset.toString(),\n timestamp: obj.timestamp,\n user: obj.user,\n uid: obj.uid && obj.uid.toString(),\n loc: [Number(obj.lon), Number(obj.lat)],\n tags: obj.tags\n });\n },\n\n way: function wayData(obj, uid) {\n return new osmWay({\n id: uid,\n visible: typeof obj.visible === 'boolean' ? obj.visible : true,\n version: obj.version && obj.version.toString(),\n changeset: obj.changeset && obj.changeset.toString(),\n timestamp: obj.timestamp,\n user: obj.user,\n uid: obj.uid && obj.uid.toString(),\n tags: obj.tags,\n nodes: getNodesJSON(obj)\n });\n },\n\n relation: function relationData(obj, uid) {\n return new osmRelation({\n id: uid,\n visible: typeof obj.visible === 'boolean' ? obj.visible : true,\n version: obj.version && obj.version.toString(),\n changeset: obj.changeset && obj.changeset.toString(),\n timestamp: obj.timestamp,\n user: obj.user,\n uid: obj.uid && obj.uid.toString(),\n tags: obj.tags,\n members: getMembersJSON(obj)\n });\n },\n\n user: function parseUser(obj, uid) {\n return {\n id: uid,\n display_name: obj.display_name,\n account_created: obj.account_created,\n image_url: obj.img && obj.img.href,\n changesets_count: obj.changesets && obj.changesets.count && obj.changesets.count.toString() || '0',\n active_blocks: obj.blocks && obj.blocks.received && obj.blocks.received.active && obj.blocks.received.active.toString() || '0'\n };\n }\n};\n\nfunction parseJSON(payload, callback, options) {\n options = Object.assign({ skipSeen: true }, options);\n if (!payload) {\n return callback({ message: 'No JSON', status: -1 });\n }\n\n var json = payload;\n if (typeof json !== 'object') json = JSON.parse(payload);\n\n if (!json.elements) return callback({ message: 'No JSON', status: -1 });\n\n var children = json.elements;\n\n var handle = window.requestIdleCallback(function() {\n _deferred.delete(handle);\n var results = [];\n var result;\n for (var i = 0; i < children.length; i++) {\n result = parseChild(children[i]);\n if (result) results.push(result);\n }\n callback(null, results);\n });\n _deferred.add(handle);\n\n function parseChild(child) {\n var parser = jsonparsers[child.type];\n if (!parser) return null;\n\n var uid;\n\n uid = osmEntity.id.fromOSM(child.type, child.id);\n if (options.skipSeen) {\n if (_tileCache.seen[uid]) return null; // avoid reparsing a \"seen\" entity\n _tileCache.seen[uid] = true;\n }\n\n return parser(child, uid);\n }\n}\n\nfunction parseUserJSON(payload, callback, options) {\n options = Object.assign({ skipSeen: true }, options);\n if (!payload) {\n return callback({ message: 'No JSON', status: -1 });\n }\n\n var json = payload;\n if (typeof json !== 'object') json = JSON.parse(payload);\n\n if (!json.users && !json.user) return callback({ message: 'No JSON', status: -1 });\n\n var objs = json.users || [json];\n\n var handle = window.requestIdleCallback(function() {\n _deferred.delete(handle);\n var results = [];\n var result;\n for (var i = 0; i < objs.length; i++) {\n result = parseObj(objs[i]);\n if (result) results.push(result);\n }\n callback(null, results);\n });\n _deferred.add(handle);\n\n function parseObj(obj) {\n var uid = obj.user.id && obj.user.id.toString();\n if (options.skipSeen && _userCache.user[uid]) {\n delete _userCache.toLoad[uid];\n return null;\n }\n var user = jsonparsers.user(obj.user, uid);\n _userCache.user[uid] = user;\n delete _userCache.toLoad[uid];\n return user;\n }\n}\n\nfunction parseNoteJSON(payload, callback, _options) {\n const options = { skipSeen: true, ..._options };\n if (!payload) {\n return callback({ message: 'No JSON', status: -1 });\n }\n\n const features = payload.type === 'FeatureCollection' ? payload.features : [payload];\n\n const notes = features.map(feature => {\n const uid = feature.properties.id;\n if (options.skipSeen) {\n if (_tileCache.seen[uid]) return null; // avoid reparsing a \"seen\" entity\n _tileCache.seen[uid] = true;\n }\n\n const props = {\n ...feature.properties,\n loc: feature.geometry.coordinates,\n };\n\n // if notes are coincident, move them apart slightly\n if (!_noteCache.note[uid]) {\n let coincident = false;\n const epsilon = 0.00001;\n do {\n if (coincident) {\n props.loc = geoVecAdd(props.loc, [epsilon, epsilon]);\n }\n const bbox = geoExtent(props.loc).bbox();\n coincident = _noteCache.rtree.search(bbox).length;\n } while (coincident);\n } else {\n // we already saw this note: don't change its location again\n props.loc = _noteCache.note[uid].loc;\n }\n\n var note = new osmNote(props);\n var item = encodeNoteRtree(note);\n _noteCache.note[note.id] = note;\n updateRtree(item, true);\n\n return note;\n });\n callback(undefined, notes);\n}\n\n// replace or remove note from rtree\nfunction updateRtree(item, replace) {\n _noteCache.rtree.remove(item, function isEql(a, b) { return a.data.id === b.data.id; });\n\n if (replace) {\n _noteCache.rtree.insert(item);\n }\n}\n\n\nfunction wrapcb(thisArg, callback, cid) {\n return function(err, result) {\n if (err) {\n return callback.call(thisArg, err);\n\n } else if (thisArg.getConnectionId() !== cid) {\n return callback.call(thisArg, { message: 'Connection Switched', status: -1 });\n\n } else {\n return callback.call(thisArg, err, result);\n }\n };\n}\n\n\nexport default {\n\n init: function() {\n utilRebind(this, dispatch, 'on');\n },\n\n\n reset: function() {\n Array.from(_deferred).forEach(function(handle) {\n window.cancelIdleCallback(handle);\n _deferred.delete(handle);\n });\n\n _connectionID++;\n _userChangesets = undefined;\n _userDetails = undefined;\n _rateLimitError = undefined;\n\n Object.values(_tileCache.inflight).forEach(abortRequest);\n Object.values(_noteCache.inflight).forEach(abortRequest);\n Object.values(_noteCache.inflightPost).forEach(abortRequest);\n if (_changeset.inflight) abortRequest(_changeset.inflight);\n\n _tileCache = { toLoad: {}, loaded: {}, inflight: {}, seen: {}, rtree: new RBush() };\n _noteCache = { toLoad: {}, loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: new RBush() };\n _userCache = { toLoad: {}, user: {} };\n _cachedApiStatus = undefined;\n _changeset = {};\n\n return this;\n },\n\n\n getConnectionId: function() {\n return _connectionID;\n },\n\n\n getUrlRoot: function() {\n return urlroot;\n },\n\n\n getApiUrlRoot: function() {\n return apiUrlroot;\n },\n\n\n changesetURL: function(changesetID) {\n return urlroot + '/changeset/' + changesetID;\n },\n\n\n changesetsURL: function(center, zoom) {\n var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));\n return urlroot + '/history#map=' +\n Math.floor(zoom) + '/' +\n center[1].toFixed(precision) + '/' +\n center[0].toFixed(precision);\n },\n\n\n entityURL: function(entity) {\n return urlroot + '/' + entity.type + '/' + entity.osmId();\n },\n\n\n historyURL: function(entity) {\n return urlroot + '/' + entity.type + '/' + entity.osmId() + '/history';\n },\n\n\n userURL: function(username) {\n return urlroot + '/user/' + encodeURIComponent(username);\n },\n\n\n noteURL: function(note) {\n return urlroot + '/note/' + note.id;\n },\n\n\n noteReportURL: function(note) {\n return urlroot + '/reports/new?reportable_type=Note&reportable_id=' + note.id;\n },\n\n\n // Generic method to load data from the OSM API\n // Can handle either auth or unauth calls.\n loadFromAPI: function(path, callback, options) {\n options = Object.assign({ skipSeen: true }, options);\n var that = this;\n var cid = _connectionID;\n\n function done(err, payloadString) {\n if (that.getConnectionId() !== cid) {\n if (callback) callback({ message: 'Connection Switched', status: -1 });\n return;\n }\n\n if ((err && _cachedApiStatus === 'online') ||\n (!err && _cachedApiStatus !== 'online')) {\n // If the response's error state doesn't match the status,\n // it's likely we lost or gained the connection so reload the status\n that.reloadApiStatus();\n }\n\n if (callback) {\n if (err) {\n // eslint-disable-next-line no-console\n console.error('API error:', err);\n return callback(err);\n } else {\n const payload = typeof payloadString === 'string' ? JSON.parse(payloadString) : payloadString;\n\n if (payload.type === 'FeatureCollection' || payload.type === 'Feature') {\n return parseNoteJSON(payload, callback, options);\n } else {\n return parseJSON(payload, callback, options);\n }\n }\n }\n }\n\n if (this.authenticated()) {\n return oauth.xhr({\n method: 'GET',\n path\n }, done);\n } else {\n var url = apiUrlroot + path;\n var controller = new AbortController();\n\n d3_json(url, { signal: controller.signal })\n .then(function(data) {\n done(null, data);\n })\n .catch(function(err) {\n if (err.name === 'AbortError') return;\n // d3-fetch includes status in the error message,\n // but we can't access the response itself\n // https://github.com/d3/d3-fetch/issues/27\n var match = err.message.match(/^\\d{3}/);\n if (match) {\n done({ status: +match[0], statusText: err.message });\n } else {\n done(err.message);\n }\n });\n return controller;\n }\n },\n\n\n // Load a single entity by id (ways and relations use the `/full` call to include\n // nodes and members). Parent relations are not included, see `loadEntityRelations`.\n // GET /api/0.6/node/#id\n // GET /api/0.6/[way|relation]/#id/full\n loadEntity: function(id, callback) {\n var type = osmEntity.id.type(id);\n var osmID = osmEntity.id.toOSM(id);\n var options = { skipSeen: false };\n\n this.loadFromAPI(\n '/api/0.6/' + type + '/' + osmID + (type !== 'node' ? '/full' : '') + '.json',\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n // Load a single note by id , XML format\n // GET /api/0.6/notes/#id\n loadEntityNote: function(id, callback) {\n var options = { skipSeen: false };\n this.loadFromAPI(\n `/api/0.6/notes/${id}.json`,\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n\n // Load a single entity with a specific version\n // GET /api/0.6/[node|way|relation]/#id/#version\n loadEntityVersion: function(id, version, callback) {\n var type = osmEntity.id.type(id);\n var osmID = osmEntity.id.toOSM(id);\n var options = { skipSeen: false };\n\n this.loadFromAPI(\n '/api/0.6/' + type + '/' + osmID + '/' + version + '.json',\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n\n // Load the relations of a single entity with the given.\n // GET /api/0.6/[node|way|relation]/#id/relations\n loadEntityRelations: function(id, callback) {\n var type = osmEntity.id.type(id);\n var osmID = osmEntity.id.toOSM(id);\n var options = { skipSeen: false };\n\n this.loadFromAPI(\n '/api/0.6/' + type + '/' + osmID + '/relations.json',\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n\n // Load multiple entities in chunks\n // (note: callback may be called multiple times)\n // Unlike `loadEntity`, child nodes and members are not fetched\n // GET /api/0.6/[nodes|ways|relations]?#parameters\n loadMultiple: function(ids, callback) {\n var that = this;\n var groups = utilArrayGroupBy(utilArrayUniq(ids), osmEntity.id.type);\n\n Object.keys(groups).forEach(function(k) {\n var type = k + 's'; // nodes, ways, relations\n var osmIDs = groups[k].map(function(id) { return osmEntity.id.toOSM(id); });\n var options = { skipSeen: false };\n\n utilArrayChunk(osmIDs, 150).forEach(function(arr) {\n that.loadFromAPI(\n '/api/0.6/' + type + '.json?' + type + '=' + arr.join(),\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n });\n });\n },\n\n\n // Create, upload, and close a changeset\n // PUT /api/0.6/changeset/create\n // POST /api/0.6/changeset/#id/upload\n // PUT /api/0.6/changeset/#id/close\n putChangeset: function(changeset, changes, callback) {\n var cid = _connectionID;\n\n if (_changeset.inflight) {\n return callback({ message: 'Changeset already inflight', status: -2 }, changeset);\n\n } else if (_changeset.open) { // reuse existing open changeset..\n return createdChangeset.call(this, null, _changeset.open);\n\n } else { // Open a new changeset..\n var options = {\n method: 'PUT',\n path: '/api/0.6/changeset/create',\n headers: { 'Content-Type': 'text/xml' },\n content: JXON.stringify(changeset.asJXON())\n };\n _changeset.inflight = oauth.xhr(\n options,\n wrapcb(this, createdChangeset, cid)\n );\n }\n\n\n async function createdChangeset(err, changesetID) {\n _changeset.inflight = null;\n if (err) { return callback(err, changeset); }\n\n _changeset.open = changesetID;\n changeset = changeset.update({ id: changesetID });\n\n // Upload the changeset..\n const xml = JXON.stringify(changeset.osmChangeJXON(changes));\n const compressed = await utilGzip(xml);\n\n const headers = { 'Content-Type': 'text/xml' };\n if (compressed) headers['Content-Encoding'] = 'gzip';\n\n var options = {\n method: 'POST',\n path: '/api/0.6/changeset/' + changesetID + '/upload',\n headers,\n content: compressed || xml,\n };\n _changeset.inflight = oauth.xhr(\n options,\n wrapcb(this, uploadedChangeset, cid)\n );\n }\n\n\n function uploadedChangeset(err) {\n _changeset.inflight = null;\n if (err) return callback(err, changeset);\n\n // Upload was successful, safe to call the callback.\n // Add delay to allow for postgres replication #1646 #2678\n window.setTimeout(function() { callback(null, changeset); }, 2500);\n _changeset.open = null;\n\n // At this point, we don't really care if the connection was switched..\n // Only try to close the changeset if we're still talking to the same server.\n if (this.getConnectionId() === cid) {\n // Still attempt to close changeset, but ignore response because #2667\n oauth.xhr({\n method: 'PUT',\n path: '/api/0.6/changeset/' + changeset.id + '/close',\n headers: { 'Content-Type': 'text/xml' }\n }, function() { return true; });\n }\n }\n },\n\n /** updates the tags on an existing unclosed changeset */\n // PUT /api/0.6/changeset/#id\n updateChangesetTags: (changeset) => {\n return oauth.fetch(`${oauth.options().apiUrl}/api/0.6/changeset/${changeset.id}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'text/xml' },\n body: JXON.stringify(changeset.asJXON())\n });\n },\n\n\n // Load multiple users in chunks\n // (note: callback may be called multiple times)\n // GET /api/0.6/users?users=#id1,#id2,...,#idn\n loadUsers: function(uids, callback) {\n var toLoad = [];\n var cached = [];\n\n utilArrayUniq(uids).forEach(function(uid) {\n if (_userCache.user[uid]) {\n delete _userCache.toLoad[uid];\n cached.push(_userCache.user[uid]);\n } else {\n toLoad.push(uid);\n }\n });\n\n if (cached.length || !this.authenticated()) {\n callback(undefined, cached);\n if (!this.authenticated()) return; // require auth\n }\n\n utilArrayChunk(toLoad, 150).forEach(function(arr) {\n oauth.xhr({\n method: 'GET',\n path: '/api/0.6/users.json?users=' + arr.join()\n }, wrapcb(this, done, _connectionID));\n }.bind(this));\n\n function done(err, payload) {\n if (err) return callback(err);\n\n var options = { skipSeen: true };\n return parseUserJSON(payload, function(err, results) {\n if (err) return callback(err);\n return callback(undefined, results);\n }, options);\n }\n },\n\n\n // Load a given user by id\n // GET /api/0.6/user/#id\n loadUser: function(uid, callback) {\n if (_userCache.user[uid] || !this.authenticated()) { // require auth\n delete _userCache.toLoad[uid];\n return callback(undefined, _userCache.user[uid]);\n }\n\n oauth.xhr({\n method: 'GET',\n path: '/api/0.6/user/' + uid + '.json'\n }, wrapcb(this, done, _connectionID));\n\n function done(err, payload) {\n if (err) return callback(err);\n\n var options = { skipSeen: true };\n return parseUserJSON(payload, function(err, results) {\n if (err) return callback(err);\n return callback(undefined, results[0]);\n }, options);\n }\n },\n\n\n // Load the details of the logged-in user\n // GET /api/0.6/user/details\n userDetails: function(callback) {\n if (_userDetails) { // retrieve cached\n return callback(undefined, _userDetails);\n }\n\n oauth.xhr({\n method: 'GET',\n path: '/api/0.6/user/details.json'\n }, wrapcb(this, done, _connectionID));\n\n function done(err, payload) {\n if (err) return callback(err);\n\n var options = { skipSeen: false };\n return parseUserJSON(payload, function(err, results) {\n if (err) return callback(err);\n _userDetails = results[0];\n return callback(undefined, _userDetails);\n }, options);\n }\n },\n\n\n // Load previous changesets for the logged in user\n // GET /api/0.6/changesets?user=#id\n userChangesets: function(callback) {\n if (_userChangesets) { // retrieve cached\n return callback(undefined, _userChangesets);\n }\n\n this.userDetails(\n wrapcb(this, gotDetails, _connectionID)\n );\n\n\n function gotDetails(err, user) {\n if (err) { return callback(err); }\n\n oauth.xhr({\n method: 'GET',\n path: `/api/0.6/changesets.json?user=${user.id}`\n }, wrapcb(this, done, _connectionID));\n }\n\n function done(err, payloadString) {\n if (err) { return callback(err); }\n\n const payload = JSON.parse(payloadString);\n _userChangesets = payload.changesets.filter(tags => tags.tags.comment);\n\n return callback(undefined, _userChangesets);\n }\n },\n\n\n // Fetch the status of the OSM API\n // GET /api/capabilities\n status: function(callback) {\n const url = `${apiUrlroot}/api/capabilities.json`;\n var errback = wrapcb(this, done, _connectionID);\n d3_json(url)\n .then(function(data) { errback(null, data); })\n .catch(function(err) { errback(err.message); });\n\n function done(err, payload) {\n if (err) {\n // the status is null if no response could be retrieved\n return callback(err, null);\n }\n\n if (_rateLimitError) {\n return callback(_rateLimitError, 'rateLimited');\n } else {\n _maxWayNodes = payload.api.waynodes.maximum;\n\n _imageryBlocklists = payload.policy.imagery.blacklist.map(item => new RegExp(item.regex, 'i'));\n\n const maxChangesetElements = payload.api.changesets.maximum_elements;\n if (!Number.isNaN(maxChangesetElements)) _maxChangesetElements = maxChangesetElements;\n\n return callback(undefined, payload.api.status.api);\n }\n }\n },\n\n // Calls `status` and dispatches an `apiStatusChange` event if the returned\n // status differs from the cached status.\n reloadApiStatus: function() {\n // throttle to avoid unnecessary API calls\n if (!this.throttledReloadApiStatus) {\n var that = this;\n this.throttledReloadApiStatus = throttle(function() {\n that.status(function(err, status) {\n if (status !== _cachedApiStatus) {\n _cachedApiStatus = status;\n dispatch.call('apiStatusChange', that, err, status);\n }\n });\n }, 500);\n }\n this.throttledReloadApiStatus();\n },\n\n\n // Returns the maximum number of nodes a single way can have\n maxWayNodes: function() {\n return _maxWayNodes;\n },\n\n\n maxChangesetElements: () => _maxChangesetElements,\n\n\n // Load data (entities) from the API in tiles\n // GET /api/0.6/map?bbox=\n loadTiles: function(projection, callback) {\n if (_off) return;\n\n // determine the needed tiles to cover the view\n var tiles = tiler.zoomExtent([_tileZoom, _tileZoom]).getTiles(projection);\n\n // abort inflight requests that are no longer needed\n var hadRequests = hasInflightRequests(_tileCache);\n abortUnwantedRequests(_tileCache, tiles);\n if (hadRequests && !hasInflightRequests(_tileCache)) {\n if (_rateLimitError) {\n // was rate limited, but has settled\n _rateLimitError = undefined;\n dispatch.call('change');\n this.reloadApiStatus();\n }\n dispatch.call('loaded'); // stop the spinner\n }\n\n // issue new requests..\n tiles.forEach(function(tile) {\n this.loadTile(tile, callback);\n }, this);\n },\n\n\n // Load a single data tile\n // GET /api/0.6/map?bbox=\n loadTile: function(tile, callback) {\n if (_off) return;\n if (_tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return;\n\n if (!hasInflightRequests(_tileCache)) {\n dispatch.call('loading'); // start the spinner\n }\n\n var path = '/api/0.6/map.json?bbox=';\n var options = { skipSeen: true };\n\n _tileCache.inflight[tile.id] = this.loadFromAPI(\n path + tile.extent.toParam(),\n tileCallback.bind(this),\n options\n );\n\n function tileCallback(err, parsed) {\n if (!err) {\n delete _tileCache.inflight[tile.id];\n delete _tileCache.toLoad[tile.id];\n _tileCache.loaded[tile.id] = true;\n var bbox = tile.extent.bbox();\n bbox.id = tile.id;\n _tileCache.rtree.insert(bbox);\n } else {\n // map tile loading error: e.g. network connection error,\n // 509 Bandwidth Limit Exceeded, 429 Too Many Requests\n if (!_rateLimitError && err.status === 509 || err.status === 429) {\n // show \"API rate limiting\" warning\n _rateLimitError = err;\n dispatch.call('change');\n this.reloadApiStatus();\n }\n setTimeout(() => {\n // retry loading the tiles\n delete _tileCache.inflight[tile.id];\n this.loadTile(tile, callback);\n }, 8000);\n }\n if (callback) {\n callback(err, Object.assign({ data: parsed }, tile));\n }\n if (!hasInflightRequests(_tileCache)) {\n if (_rateLimitError) {\n // was rate limited, but has settled\n _rateLimitError = undefined;\n dispatch.call('change');\n this.reloadApiStatus();\n }\n dispatch.call('loaded'); // stop the spinner\n }\n }\n },\n\n\n isDataLoaded: function(loc) {\n var bbox = { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1] };\n return _tileCache.rtree.collides(bbox);\n },\n\n\n // load the tile that covers the given `loc`\n loadTileAtLoc: function(loc, callback) {\n // Back off if the toLoad queue is filling up.. re #6417\n // (Currently `loadTileAtLoc` requests are considered low priority - used by operations to\n // let users safely edit geometries which extend to unloaded tiles. We can drop some.)\n if (Object.keys(_tileCache.toLoad).length > 50) return;\n\n var k = geoZoomToScale(_tileZoom + 1);\n var offset = geoRawMercator().scale(k)(loc);\n var projection = geoRawMercator().transform({ k: k, x: -offset[0], y: -offset[1] });\n var tiles = tiler.zoomExtent([_tileZoom, _tileZoom]).getTiles(projection);\n\n tiles.forEach(function(tile) {\n if (_tileCache.toLoad[tile.id] || _tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return;\n\n _tileCache.toLoad[tile.id] = true;\n this.loadTile(tile, callback);\n }, this);\n },\n\n\n // Load notes from the API in tiles\n // GET /api/0.6/notes?bbox=\n loadNotes: function(projection, noteOptions) {\n noteOptions = Object.assign({ limit: 10000, closed: 7 }, noteOptions);\n if (_off) return;\n\n var that = this;\n var path = `/api/0.6/notes.json?limit=${noteOptions.limit}&closed=${noteOptions.closed}&bbox=`;\n var throttleLoadUsers = throttle(function() {\n var uids = Object.keys(_userCache.toLoad);\n if (!uids.length) return;\n that.loadUsers(uids, function() {}); // eagerly load user details\n }, 750);\n\n // determine the needed tiles to cover the view\n var tiles = tiler.zoomExtent([_noteZoom, _noteZoom]).getTiles(projection);\n\n // abort inflight requests that are no longer needed\n abortUnwantedRequests(_noteCache, tiles);\n\n // issue new requests..\n tiles.forEach(function(tile) {\n if (_noteCache.loaded[tile.id] || _noteCache.inflight[tile.id]) return;\n\n var options = { skipSeen: false };\n _noteCache.inflight[tile.id] = that.loadFromAPI(\n path + tile.extent.toParam(),\n function(err) {\n delete _noteCache.inflight[tile.id];\n if (!err) {\n _noteCache.loaded[tile.id] = true;\n }\n throttleLoadUsers();\n dispatch.call('loadedNotes');\n },\n options\n );\n });\n },\n\n\n // Create a note\n // POST /api/0.6/notes?params\n postNoteCreate: function(note, callback) {\n if (!this.authenticated()) {\n return callback({ message: 'Not Authenticated', status: -3 }, note);\n }\n if (_noteCache.inflightPost[note.id]) {\n return callback({ message: 'Note update already inflight', status: -2 }, note);\n }\n\n if (!note.loc[0] || !note.loc[1] || !note.newComment) return; // location & description required\n\n var comment = note.newComment;\n if (note.newCategory && note.newCategory !== 'None') { comment += ' #' + note.newCategory; }\n\n var path = '/api/0.6/notes.json?' + utilQsString({ lon: note.loc[0], lat: note.loc[1], text: comment });\n\n _noteCache.inflightPost[note.id] = oauth.xhr({\n method: 'POST',\n path: path\n }, wrapcb(this, done, _connectionID));\n\n\n function done(err, payload) {\n delete _noteCache.inflightPost[note.id];\n if (err) { return callback(err); }\n\n // we get the updated note back, remove from caches and reparse..\n this.removeNote(note);\n\n var options = { skipSeen: false };\n return parseNoteJSON(JSON.parse(payload), function(err, results) {\n if (err) {\n return callback(err);\n } else {\n return callback(undefined, results[0]);\n }\n }, options);\n }\n },\n\n\n // Update a note\n // POST /api/0.6/notes/#id/comment?text=comment\n // POST /api/0.6/notes/#id/close?text=comment\n // POST /api/0.6/notes/#id/reopen?text=comment\n postNoteUpdate: function(note, newStatus, callback) {\n if (!this.authenticated()) {\n return callback({ message: 'Not Authenticated', status: -3 }, note);\n }\n if (_noteCache.inflightPost[note.id]) {\n return callback({ message: 'Note update already inflight', status: -2 }, note);\n }\n\n var action;\n if (note.status !== 'closed' && newStatus === 'closed') {\n action = 'close';\n } else if (note.status !== 'open' && newStatus === 'open') {\n action = 'reopen';\n } else {\n action = 'comment';\n if (!note.newComment) return; // when commenting, comment required\n }\n\n var path = `/api/0.6/notes/${note.id}/${action}.json`;\n if (note.newComment) {\n path += '?' + utilQsString({ text: note.newComment });\n }\n\n _noteCache.inflightPost[note.id] = oauth.xhr({\n method: 'POST',\n path: path\n }, wrapcb(this, done, _connectionID));\n\n\n function done(err, payload) {\n delete _noteCache.inflightPost[note.id];\n if (err) { return callback(err); }\n\n // we get the updated note back, remove from caches and reparse..\n this.removeNote(note);\n\n // update closed note cache - used to populate `closed:note` changeset tag\n if (action === 'close') {\n _noteCache.closed[note.id] = true;\n } else if (action === 'reopen') {\n delete _noteCache.closed[note.id];\n }\n\n var options = { skipSeen: false };\n return parseNoteJSON(JSON.parse(payload), function(err, results) {\n if (err) {\n return callback(err);\n } else {\n return callback(undefined, results[0]);\n }\n }, options);\n }\n },\n\n\n /* connection options for source switcher (optional) */\n apiConnections: function(val) {\n if (!arguments.length) return _apiConnections;\n _apiConnections = val;\n return this;\n },\n\n\n switch: function(newOptions) {\n urlroot = newOptions.url;\n apiUrlroot = newOptions.apiUrl || urlroot;\n if (newOptions.url && !newOptions.apiUrl) {\n newOptions = {\n ...newOptions,\n apiUrl: newOptions.url\n };\n }\n\n // Copy the existing options, but omit 'access_token'.\n // (if we did preauth, access_token won't work on a different server)\n const oldOptions = utilObjectOmit(oauth.options(), 'access_token');\n oauth.options({...oldOptions, ...newOptions});\n\n this.reset();\n this.userChangesets(function() {}); // eagerly load user details/changesets\n dispatch.call('change');\n return this;\n },\n\n\n toggle: function(val) {\n _off = !val;\n return this;\n },\n\n\n isChangesetInflight: function() {\n return !!_changeset.inflight;\n },\n\n\n // get/set cached data\n // This is used to save/restore the state when entering/exiting the walkthrough\n // Also used for testing purposes.\n caches: function(obj) {\n function cloneCache(source) {\n var target = {};\n Object.keys(source).forEach(function(k) {\n if (k === 'rtree') {\n target.rtree = new RBush().fromJSON(source.rtree.toJSON()); // clone rbush\n } else if (k === 'note') {\n target.note = {};\n Object.keys(source.note).forEach(function(id) {\n target.note[id] = osmNote(source.note[id]); // copy notes\n });\n } else {\n target[k] = JSON.parse(JSON.stringify(source[k])); // clone deep\n }\n });\n return target;\n }\n\n if (!arguments.length) {\n return {\n tile: cloneCache(_tileCache),\n note: cloneCache(_noteCache),\n user: cloneCache(_userCache)\n };\n }\n\n // access caches directly for testing (e.g., loading notes rtree)\n if (obj === 'get') {\n return {\n tile: _tileCache,\n note: _noteCache,\n user: _userCache\n };\n }\n\n if (obj.tile) {\n _tileCache = obj.tile;\n _tileCache.inflight = {};\n }\n if (obj.note) {\n _noteCache = obj.note;\n _noteCache.inflight = {};\n _noteCache.inflightPost = {};\n }\n if (obj.user) {\n _userCache = obj.user;\n }\n\n return this;\n },\n\n\n logout: function() {\n _userChangesets = undefined;\n _userDetails = undefined;\n oauth.logout();\n dispatch.call('change');\n return this;\n },\n\n\n authenticated: function() {\n return oauth.authenticated();\n },\n\n\n /** @param {import('osm-auth').LoginOptions} options */\n authenticate: function(callback, options) {\n var that = this;\n var cid = _connectionID;\n _userChangesets = undefined;\n _userDetails = undefined;\n\n function done(err, res) {\n if (err) {\n if (callback) callback(err);\n return;\n }\n if (that.getConnectionId() !== cid) {\n if (callback) callback({ message: 'Connection Switched', status: -1 });\n return;\n }\n _rateLimitError = undefined;\n dispatch.call('change');\n if (callback) callback(err, res);\n that.userChangesets(function() {}); // eagerly load user details/changesets\n }\n\n // ensure the locale is correctly set before opening the popup\n oauth.options({\n ...oauth.options(),\n locale: localizer.localeCode(),\n });\n\n oauth.authenticate(done, options);\n },\n\n\n imageryBlocklists: function() {\n return _imageryBlocklists;\n },\n\n\n tileZoom: function(val) {\n if (!arguments.length) return _tileZoom;\n _tileZoom = val;\n return this;\n },\n\n\n // get all cached notes covering the viewport\n notes: function(projection) {\n var viewport = projection.clipExtent();\n var min = [viewport[0][0], viewport[1][1]];\n var max = [viewport[1][0], viewport[0][1]];\n var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n\n return _noteCache.rtree.search(bbox)\n .map(function(d) { return d.data; });\n },\n\n\n // get a single note from the cache\n getNote: function(id) {\n return _noteCache.note[id];\n },\n\n\n // remove a single note from the cache\n removeNote: function(note) {\n if (!(note instanceof osmNote) || !note.id) return;\n\n delete _noteCache.note[note.id];\n updateRtree(encodeNoteRtree(note), false); // false = remove\n },\n\n\n // replace a single note in the cache\n replaceNote: function(note) {\n if (!(note instanceof osmNote) || !note.id) return;\n\n _noteCache.note[note.id] = note;\n updateRtree(encodeNoteRtree(note), true); // true = replace\n return note;\n },\n\n\n // Get an array of note IDs closed during this session.\n // Used to populate `closed:note` changeset tag\n getClosedIDs: function() {\n return Object.keys(_noteCache.closed).sort();\n }\n\n};\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { json as d3_json } from 'd3-fetch';\n\nimport { localizer } from '../core/localizer';\nimport { utilQsString } from '../util';\n\n\nvar apibase = 'https://wiki.openstreetmap.org/w/api.php';\nvar _inflight = {};\nvar _wikibaseCache = {};\nvar _localeIDs = { en: false };\n\n\nvar debouncedRequest = debounce(request, 500, { leading: false });\n\nfunction request(url, callback) {\n if (_inflight[url]) return;\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, { signal: controller.signal })\n .then(function(result) {\n delete _inflight[url];\n if (callback) callback(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (callback) callback(err.message);\n });\n}\n\n\nexport default {\n\n init: function() {\n _inflight = {};\n _wikibaseCache = {};\n _localeIDs = {};\n },\n\n\n reset: function() {\n Object.values(_inflight).forEach(function(controller) { controller.abort(); });\n _inflight = {};\n },\n\n\n /**\n * Get the best value for the property, or undefined if not found\n * @param entity object from wikibase\n * @param property string e.g. 'P4' for image\n * @param langCode string e.g. 'fr' for French\n */\n claimToValue: function(entity, property, langCode) {\n if (!entity.claims[property]) return undefined;\n var locale = _localeIDs[langCode];\n var preferredPick, localePick;\n\n entity.claims[property].forEach(function(stmt) {\n // If exists, use value limited to the needed language (has a qualifier P26 = locale)\n // Or if not found, use the first value with the \"preferred\" rank\n if (!preferredPick && stmt.rank === 'preferred') {\n preferredPick = stmt;\n }\n if (locale && stmt.qualifiers && stmt.qualifiers.P26 &&\n stmt.qualifiers.P26[0].datavalue.value.id === locale\n ) {\n localePick = stmt;\n }\n });\n\n var result = localePick || preferredPick;\n if (result) {\n var datavalue = result.mainsnak.datavalue;\n return datavalue.type === 'wikibase-entityid' ? datavalue.value.id : datavalue.value;\n } else {\n return undefined;\n }\n },\n\n\n /**\n * Convert monolingual property into a key-value object (language -> value)\n * @param entity object from wikibase\n * @param property string e.g. 'P31' for monolingual wiki page title\n */\n monolingualClaimToValueObj: function(entity, property) {\n if (!entity || !entity.claims[property]) return undefined;\n\n return entity.claims[property].reduce(function(acc, obj) {\n var value = obj.mainsnak.datavalue.value;\n acc[value.language] = value.text;\n return acc;\n }, {});\n },\n\n\n toSitelink: function(key, value, isHistorical) {\n var type = value ? 'Tag' : 'Key';\n var prefix = '';\n if (isHistorical) {\n prefix = `OpenHistoricalMap/Tags/${type}/`;\n } else {\n prefix = type + ':';\n }\n return (prefix + (value ? `${key}=${value}` : key).replace(/_/g, ' ')).trim();\n },\n\n /**\n * Converts text like `tag:...=...` into clickable links\n *\n * @param {string} unsafeText - unsanitized text\n */\n linkifyWikiText(unsafeText) {\n /** @param {import('d3').Selection} selection */\n return (selection) => {\n const segments = unsafeText.split(/(key|tag):([\\w-]+)(=([\\w-]+))?/g);\n\n for (let i = 0; i < segments.length; i += 5) {\n const [plainText, , key, , value] = segments.slice(i);\n\n if (plainText) {\n selection\n .append('span')\n .text(plainText);\n }\n\n if (key) {\n selection\n .append('a')\n .attr('href', `https://wiki.openstreetmap.org/wiki/${this.toSitelink(key, value)}`)\n .attr('target', '_blank')\n .attr('rel', 'noreferrer')\n .append('code')\n .text(`${key}=${value || '*'}`);\n }\n }\n };\n },\n\n\n //\n // Pass params object of the form:\n // {\n // key: 'string',\n // value: 'string',\n // langCode: 'string'\n // }\n //\n getEntity: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n var that = this;\n var titles = [];\n var result = {};\n var rtypeSitelink = (params.key === 'type' && params.value) ? ('Relation:' + params.value).replace(/_/g, ' ').trim() : false;\n var rtypeSitelinkHistorical = (params.key === 'type' && params.value) ? ('OpenHistoricalMap/Tags/Relation/' + params.value.replace(/_/g, ' ').trim()) : false;\n var keySitelink = params.key ? this.toSitelink(params.key) : false;\n var keySitelinkHistorical = params.key ? this.toSitelink(params.key, null, true) : false;\n var tagSitelink = (params.key && params.value) ? this.toSitelink(params.key, params.value) : false;\n var tagSitelinkHistorical = (params.key && params.value) ? this.toSitelink(params.key, params.value, true) : false;\n\n if (params.langCodes) {\n params.langCodes.forEach(function(langCode) {\n if (_localeIDs[langCode] === undefined) {\n // If this is the first time we are asking about this locale,\n // fetch corresponding entity (if it exists), and cache it.\n // If there is no such entry, cache `false` value to avoid re-requesting it.\n let localeSitelink = ('Locale:' + langCode).replace(/_/g, ' ').trim();\n titles.push(localeSitelink);\n\n // initialize with false, such that if locale ID is not found in first request,\n // it will not be retried in further queries\n that.addLocale(langCode, false);\n }\n });\n }\n\n if (rtypeSitelink) {\n if (_wikibaseCache[rtypeSitelinkHistorical]) {\n result.rtype = _wikibaseCache[rtypeSitelinkHistorical];\n } else if (_wikibaseCache[rtypeSitelink]) {\n result.rtype = _wikibaseCache[rtypeSitelink];\n } else {\n titles.push(rtypeSitelink, rtypeSitelinkHistorical);\n }\n }\n\n if (keySitelink) {\n if (_wikibaseCache[keySitelinkHistorical]) {\n result.key = _wikibaseCache[keySitelinkHistorical];\n } else if (_wikibaseCache[keySitelink]) {\n result.key = _wikibaseCache[keySitelink];\n } else {\n titles.push(keySitelink, keySitelinkHistorical);\n }\n }\n\n if (tagSitelink) {\n if (_wikibaseCache[tagSitelinkHistorical]) {\n result.tag = _wikibaseCache[tagSitelinkHistorical];\n } else if (_wikibaseCache[tagSitelink]) {\n result.tag = _wikibaseCache[tagSitelink];\n } else {\n titles.push(tagSitelink, tagSitelinkHistorical);\n }\n }\n\n if (!titles.length) {\n // Nothing to do, we already had everything in the cache\n return callback(null, result);\n }\n\n // Requesting just the user language code\n // If backend recognizes the code, it will perform proper fallbacks,\n // and the result will contain the requested code. If not, all values are returned:\n // {\"zh-tw\":{\"value\":\"...\",\"language\":\"zh-tw\",\"source-language\":\"zh-hant\"}\n // {\"pt-br\":{\"value\":\"...\",\"language\":\"pt\",\"for-language\":\"pt-br\"}}\n var obj = {\n action: 'wbgetentities',\n sites: 'wiki',\n titles: titles.join('|'),\n languages: params.langCodes.join('|'),\n languagefallback: 1,\n origin: '*',\n format: 'json',\n // There is an MW Wikibase API bug https://phabricator.wikimedia.org/T212069\n // We shouldn't use v1 until it gets fixed, but should switch to it afterwards\n // formatversion: 2,\n };\n\n var url = apibase + '?' + utilQsString(obj);\n doRequest(url, function(err, d) {\n if (err) {\n callback(err);\n } else if (!d.success || d.error) {\n callback(d.error.messages.map(function(v) { return v.html['*']; }).join('
    '));\n } else {\n Object.values(d.entities).forEach(function(res) {\n if (res.missing !== '') {\n\n var title = res.sitelinks.wiki.title;\n if (title === rtypeSitelinkHistorical) {\n _wikibaseCache[rtypeSitelinkHistorical] = res;\n result.rtype = res;\n } else if (title === rtypeSitelink) {\n _wikibaseCache[rtypeSitelink] = res;\n result.rtype = res;\n } else if (title === keySitelinkHistorical) {\n _wikibaseCache[keySitelinkHistorical] = res;\n result.key = res;\n } else if (title === keySitelink) {\n _wikibaseCache[keySitelink] = res;\n result.key = res;\n } else if (title === tagSitelinkHistorical) {\n _wikibaseCache[tagSitelinkHistorical] = res;\n result.tag = res;\n } else if (title === tagSitelink) {\n _wikibaseCache[tagSitelink] = res;\n result.tag = res;\n } else if (title.startsWith('Locale:')) {\n const langCode = title.replace(/ /g, '_').replace(/^Locale:/, '');\n that.addLocale(langCode, res.id);\n } else {\n console.log('Unexpected title ' + title); // eslint-disable-line no-console\n }\n }\n });\n\n callback(null, result);\n }\n });\n },\n\n\n //\n // Pass params object of the form:\n // {\n // key: 'string', // required\n // value: 'string' // optional\n // }\n //\n // Get an result object used to display tag documentation\n // {\n // title: 'string',\n // description: 'string',\n // editURL: 'string',\n // imageURL: 'string',\n // wiki: { title: 'string', text: 'string', url: 'string' }\n // }\n //\n getDocs: function(params, callback) {\n var that = this;\n var langCodes = localizer.localeCodes().map(function(code) {\n return code.toLowerCase();\n });\n params.langCodes = langCodes;\n\n this.getEntity(params, function(err, data) {\n if (err) {\n callback(err);\n return;\n }\n\n var entity = data.rtype || data.tag || data.key;\n if (!entity) {\n callback('No entity');\n return;\n }\n\n var i;\n var description;\n for (i in langCodes) {\n let code = langCodes[i];\n if (entity.descriptions[code] && entity.descriptions[code].language === code) {\n description = entity.descriptions[code];\n break;\n }\n }\n if (!description && Object.values(entity.descriptions).length) description = Object.values(entity.descriptions)[0];\n\n // prepare result\n var result = {\n title: entity.title,\n description: that.linkifyWikiText(description?.value || ''),\n descriptionLocaleCode: description ? description.language : '',\n editURL: 'https://wiki.openstreetmap.org/wiki/' + entity.title\n };\n\n // add image\n if (entity.claims) {\n var imageroot;\n var image = that.claimToValue(entity, 'P4', langCodes[0]);\n if (image) {\n imageroot = 'https://commons.wikimedia.org/w/index.php';\n } else {\n image = that.claimToValue(entity, 'P28', langCodes[0]);\n if (image) {\n imageroot = 'https://wiki.openstreetmap.org/w/index.php';\n }\n }\n if (imageroot && image) {\n result.imageURL = imageroot + '?' + utilQsString({\n title: 'Special:Redirect/file/' + image,\n width: 400\n });\n }\n }\n\n // Try to get a wiki page from tag data item first, followed by the corresponding key data item.\n // If neither tag nor key data item contain a wiki page in the needed language nor English,\n // get the first found wiki page from either the tag or the key item.\n var rtypeWiki = that.monolingualClaimToValueObj(data.rtype, 'P31');\n var tagWiki = that.monolingualClaimToValueObj(data.tag, 'P31');\n var keyWiki = that.monolingualClaimToValueObj(data.key, 'P31');\n\n var wikis = [rtypeWiki, tagWiki, keyWiki];\n for (i in wikis) {\n var wiki = wikis[i];\n for (var j in langCodes) {\n var code = langCodes[j];\n var referenceId = (langCodes[0].split('-')[0] !== 'en' && code.split('-')[0] === 'en') ? 'inspector.wiki_en_reference' : 'inspector.wiki_reference';\n var info = getWikiInfo(wiki, code, referenceId);\n if (info) {\n result.wiki = info;\n break;\n }\n }\n if (result.wiki) break;\n }\n\n callback(null, result);\n\n\n // Helper method to get wiki info if a given language exists\n function getWikiInfo(wiki, langCode, tKey) {\n if (wiki && wiki[langCode]) {\n return {\n title: wiki[langCode],\n text: tKey,\n url: 'https://wiki.openstreetmap.org/wiki/' + wiki[langCode]\n };\n }\n }\n });\n },\n\n getLocaleIDs: () => _localeIDs,\n\n\n addLocale: function(langCode, qid) {\n // Makes it easier to unit test\n _localeIDs[langCode] = qid;\n },\n\n\n apibase: function(val) {\n if (!arguments.length) return apibase;\n apibase = val;\n return this;\n }\n\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { timer as d3_timer } from 'd3-timer';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport RBush from 'rbush';\nimport { t } from '../core/localizer';\n\nimport {\n geoExtent, geoMetersToLat, geoMetersToLon, geoPointInPolygon,\n geoRotate, geoVecLength\n} from '../geo';\n\nimport { utilAesDecrypt, utilArrayUnion, utilQsString, utilRebind, utilStringQs, utilTiler, utilUniqueDomId } from '../util';\n\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\nimport { localeTimestamp } from '../util/date';\n\n\nconst streetsideApi = 'https://dev.virtualearth.net/REST/v1/Imagery/MetaData/Streetside?mapArea={bbox}&key={key}&count={count}&uriScheme=https';\nconst maxResults = 500;\nconst bubbleAppKey = utilAesDecrypt('5c875730b09c6b422433e807e1ff060b6536c791dbfffcffc4c6b18a1bdba1f14593d151adb50e19e1be1ab19aef813bf135d0f103475e5c724dec94389e45d0');\nconst pannellumViewerCSS = 'pannellum/pannellum.css';\nconst pannellumViewerJS = 'pannellum/pannellum.js';\nconst tileZoom = 16.5;\nconst tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);\nconst dispatch = d3_dispatch('loadedImages', 'viewerChanged');\nconst minHfov = 10; // zoom in degrees: 20, 10, 5\nconst maxHfov = 90; // zoom out degrees\nconst defaultHfov = 45;\n\nlet _hires = false;\nlet _resolution = 512; // higher numbers are slower - 512, 1024, 2048, 4096\nlet _currScene = 0;\nlet _ssCache;\nlet _pannellumViewer;\nlet _sceneOptions = {\n showFullscreenCtrl: false,\n autoLoad: true,\n compass: true,\n yaw: 0,\n minHfov: minHfov,\n maxHfov: maxHfov,\n hfov: defaultHfov,\n type: 'cubemap',\n cubeMap: []\n};\nlet _loadViewerPromise;\n\n\n/**\n * abortRequest().\n */\nfunction abortRequest(i) {\n i.abort();\n}\n\n/**\n * loadTiles() wraps the process of generating tiles and then fetching image points for each tile.\n */\nfunction loadTiles(which, url, projection, margin) {\n const tiles = tiler.margin(margin).getTiles(projection);\n\n // abort inflight requests that are no longer needed\n const cache = _ssCache[which];\n Object.keys(cache.inflight).forEach(k => {\n const wanted = tiles.find(tile => k.indexOf(tile.id + ',') === 0);\n if (!wanted) {\n abortRequest(cache.inflight[k]);\n delete cache.inflight[k];\n }\n });\n\n tiles.forEach(tile => loadNextTilePage(which, url, tile));\n}\n\n\n/**\n * loadNextTilePage() load data for the next tile page in line.\n */\nfunction loadNextTilePage(which, url, tile) {\n const cache = _ssCache[which];\n const nextPage = cache.nextPage[tile.id] || 0;\n const id = tile.id + ',' + String(nextPage);\n if (cache.loaded[id] || cache.inflight[id]) return;\n\n cache.inflight[id] = getBubbles(url, tile, response => {\n cache.loaded[id] = true;\n delete cache.inflight[id];\n if (!response) return;\n\n if (response.resourceSets[0].resources.length === maxResults) {\n // there are more bubbles than the response can fit: re-fetch using tile split into 4\n const split = tile.extent.split();\n loadNextTilePage(which, url, { id: tile.id + ',a', extent: split[0] });\n loadNextTilePage(which, url, { id: tile.id + ',b', extent: split[1] });\n loadNextTilePage(which, url, { id: tile.id + ',c', extent: split[2] });\n loadNextTilePage(which, url, { id: tile.id + ',d', extent: split[3] });\n }\n\n const features = response.resourceSets[0].resources.map(bubble => {\n const bubbleId = bubble.imageUrl;\n if (cache.points[bubbleId]) return null; // skip duplicates\n\n // workaround for https://github.com/openstreetmap/iD/issues/10341#issuecomment-2275724738\n const loc = [\n bubble.lon || bubble.longitude,\n bubble.lat || bubble.latitude\n ];\n const d = {\n service: 'photo',\n loc: loc,\n key: bubbleId,\n imageUrl: bubble.imageUrl\n .replace('{subdomain}', bubble.imageUrlSubdomains[0]),\n ca: bubble.he || bubble.heading,\n captured_at: bubble.vintageEnd,\n captured_by: 'microsoft',\n pano: true,\n sequenceKey: null\n };\n\n cache.points[bubbleId] = d;\n\n return {\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n };\n\n }).filter(Boolean);\n\n cache.rtree.load(features);\n\n if (which === 'bubbles') {\n dispatch.call('loadedImages');\n }\n });\n}\n\n\n/**\n * getBubbles() handles the request to the server for a tile extent of 'bubbles' (streetside image locations).\n */\nfunction getBubbles(url, tile, callback) {\n let rect = tile.extent.rectangle();\n let urlForRequest = url\n .replace('{key}', bubbleAppKey)\n .replace('{bbox}', [rect[1], rect[0], rect[3], rect[2]].join(','))\n .replace('{count}', maxResults);\n\n const controller = new AbortController();\n fetch(urlForRequest, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n }).then(function(result) {\n if (!result) {\n callback(null);\n }\n return callback(result || []);\n }).catch(function(err) {\n if (err.name === 'AbortError') {\n // ignore aborted requests, e.g. from duplicate requests while zooming/panning the map\n } else {\n throw new Error(err);\n }\n });\n return controller;\n }\n\n\n/**\n * loadImage()\n */\nfunction loadImage(imgInfo) {\n return new Promise(resolve => {\n let img = new Image();\n img.onload = () => {\n let canvas = document.getElementById('ideditor-canvas' + imgInfo.face);\n let ctx = canvas.getContext('2d');\n ctx.drawImage(img, imgInfo.x, imgInfo.y);\n resolve({ imgInfo: imgInfo, status: 'ok' });\n };\n img.onerror = () => {\n resolve({ data: imgInfo, status: 'error' });\n };\n img.setAttribute('crossorigin', '');\n img.src = imgInfo.url;\n });\n}\n\n\n/**\n * loadCanvas()\n */\nfunction loadCanvas(imageGroup) {\n return Promise.all(imageGroup.map(loadImage))\n .then((data) => {\n let canvas = document.getElementById('ideditor-canvas' + data[0].imgInfo.face);\n const which = { '01': 0, '02': 1, '03': 2, '10': 3, '11': 4, '12': 5 };\n let face = data[0].imgInfo.face;\n _sceneOptions.cubeMap[which[face]] = canvas.toDataURL('image/jpeg', 1.0);\n return { status: 'loadCanvas for face ' + data[0].imgInfo.face + 'ok'};\n });\n}\n\n\n/**\n * loadFaces()\n */\nfunction loadFaces(faceGroup) {\n return Promise.all(faceGroup.map(loadCanvas))\n .then(() => { return { status: 'loadFaces done' }; });\n}\n\n\nfunction setupCanvas(selection, reset) {\n if (reset) {\n selection.selectAll('#ideditor-stitcher-canvases')\n .remove();\n }\n\n // Add the Streetside working canvases. These are used for 'stitching', or combining,\n // multiple images for each of the six faces, before passing to the Pannellum control as DataUrls\n selection.selectAll('#ideditor-stitcher-canvases')\n .data([0])\n .enter()\n .append('div')\n .attr('id', 'ideditor-stitcher-canvases')\n .attr('display', 'none')\n .selectAll('canvas')\n .data(['canvas01', 'canvas02', 'canvas03', 'canvas10', 'canvas11', 'canvas12'])\n .enter()\n .append('canvas')\n .attr('id', d => 'ideditor-' + d)\n .attr('width', _resolution)\n .attr('height', _resolution);\n}\n\n\nfunction qkToXY(qk) {\n let x = 0;\n let y = 0;\n let scale = 256;\n for (let i = qk.length; i > 0; i--) {\n const key = qk[i-1];\n x += (+(key === '1' || key === '3')) * scale;\n y += (+(key === '2' || key === '3')) * scale;\n scale *= 2;\n }\n return [x, y];\n}\n\n\nfunction getQuadKeys() {\n let dim = _resolution / 256;\n let quadKeys;\n\n if (dim === 16) {\n quadKeys = [\n '0000','0001','0010','0011','0100','0101','0110','0111', '1000','1001','1010','1011','1100','1101','1110','1111',\n '0002','0003','0012','0013','0102','0103','0112','0113', '1002','1003','1012','1013','1102','1103','1112','1113',\n '0020','0021','0030','0031','0120','0121','0130','0131', '1020','1021','1030','1031','1120','1121','1130','1131',\n '0022','0023','0032','0033','0122','0123','0132','0133', '1022','1023','1032','1033','1122','1123','1132','1133',\n '0200','0201','0210','0211','0300','0301','0310','0311', '1200','1201','1210','1211','1300','1301','1310','1311',\n '0202','0203','0212','0213','0302','0303','0312','0313', '1202','1203','1212','1213','1302','1303','1312','1313',\n '0220','0221','0230','0231','0320','0321','0330','0331', '1220','1221','1230','1231','1320','1321','1330','1331',\n '0222','0223','0232','0233','0322','0323','0332','0333', '1222','1223','1232','1233','1322','1323','1332','1333',\n\n '2000','2001','2010','2011','2100','2101','2110','2111', '3000','3001','3010','3011','3100','3101','3110','3111',\n '2002','2003','2012','2013','2102','2103','2112','2113', '3002','3003','3012','3013','3102','3103','3112','3113',\n '2020','2021','2030','2031','2120','2121','2130','2131', '3020','3021','3030','3031','3120','3121','3130','3131',\n '2022','2023','2032','2033','2122','2123','2132','2133', '3022','3023','3032','3033','3122','3123','3132','3133',\n '2200','2201','2210','2211','2300','2301','2310','2311', '3200','3201','3210','3211','3300','3301','3310','3311',\n '2202','2203','2212','2213','2302','2303','2312','2313', '3202','3203','3212','3213','3302','3303','3312','3313',\n '2220','2221','2230','2231','2320','2321','2330','2331', '3220','3221','3230','3231','3320','3321','3330','3331',\n '2222','2223','2232','2233','2322','2323','2332','2333', '3222','3223','3232','3233','3322','3323','3332','3333'\n ];\n\n } else if (dim === 8) {\n quadKeys = [\n '000','001','010','011', '100','101','110','111',\n '002','003','012','013', '102','103','112','113',\n '020','021','030','031', '120','121','130','131',\n '022','023','032','033', '122','123','132','133',\n\n '200','201','210','211', '300','301','310','311',\n '202','203','212','213', '302','303','312','313',\n '220','221','230','231', '320','321','330','331',\n '222','223','232','233', '322','323','332','333'\n ];\n\n } else if (dim === 4) {\n quadKeys = [\n '00','01', '10','11',\n '02','03', '12','13',\n\n '20','21', '30','31',\n '22','23', '32','33'\n ];\n\n } else { // dim === 2\n quadKeys = [\n '0', '1',\n '2', '3'\n ];\n }\n\n return quadKeys;\n}\n\n\n\nexport default {\n /**\n * init() initialize streetside.\n */\n init: function() {\n if (!_ssCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n /**\n * reset() reset the cache.\n */\n reset: function() {\n if (_ssCache) {\n Object.values(_ssCache.bubbles.inflight).forEach(abortRequest);\n }\n\n _ssCache = {\n bubbles: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), points: {} },\n sequences: {}\n };\n },\n\n /**\n * bubbles()\n */\n bubbles: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _ssCache.bubbles.rtree);\n },\n\n\n cachedImage: function(imageKey) {\n return _ssCache.bubbles.points[imageKey];\n },\n\n\n sequences: function(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n let seen = {};\n let results = [];\n\n // all sequences for bubbles in viewport\n _ssCache.bubbles.rtree.search(bbox)\n .forEach(d => {\n const key = d.data.sequenceKey;\n if (key && !seen[key]) {\n seen[key] = true;\n results.push(_ssCache.sequences[key].geojson);\n }\n });\n\n return results;\n },\n\n\n /**\n * loadBubbles()\n */\n loadBubbles: function(projection, margin) {\n // by default: request 2 nearby tiles so we can connect sequences.\n if (margin === undefined) margin = 2;\n\n loadTiles('bubbles', streetsideApi, projection, margin);\n },\n\n\n viewer: function() {\n return _pannellumViewer;\n },\n\n\n initViewer: function () {\n if (!window.pannellum) return;\n if (_pannellumViewer) return;\n\n _currScene += 1;\n const sceneID = _currScene.toString();\n const options = {\n 'default': { firstScene: sceneID },\n scenes: {}\n };\n options.scenes[sceneID] = _sceneOptions;\n\n _pannellumViewer = window.pannellum.viewer('ideditor-viewer-streetside', options);\n },\n\n\n ensureViewerLoaded: function(context) {\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n // create ms-wrapper, a photo wrapper class\n let wrap = context.container().select('.photoviewer').selectAll('.ms-wrapper')\n .data([0]);\n\n // inject ms-wrapper into the photoviewer div\n // (used by all to house each custom photo viewer)\n let wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper ms-wrapper')\n .classed('hide', true);\n\n let that = this;\n\n let pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n // inject div to support streetside viewer (pannellum) and attribution line\n wrapEnter\n .append('div')\n .attr('id', 'ideditor-viewer-streetside')\n .on(pointerPrefix + 'down.streetside', () => {\n d3_select(window)\n .on(pointerPrefix + 'move.streetside', () => {\n dispatch.call('viewerChanged');\n }, true);\n })\n .on(pointerPrefix + 'up.streetside pointercancel.streetside', () => {\n d3_select(window)\n .on(pointerPrefix + 'move.streetside', null);\n\n // continue dispatching events for a few seconds, in case viewer has inertia.\n let t = d3_timer(elapsed => {\n dispatch.call('viewerChanged');\n if (elapsed > 2000) {\n t.stop();\n }\n });\n })\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n let controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls');\n\n controlsEnter\n .append('button')\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .on('click.forward', step(1))\n .text('\u25BA');\n\n\n // create working canvas for stitching together images\n wrap\n .merge(wrapEnter)\n .call(setupCanvas, true);\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.streetside', () => {\n if (_pannellumViewer) {\n _pannellumViewer.resize();\n }\n });\n\n _loadViewerPromise = new Promise((resolve, reject) => {\n\n let loadedCount = 0;\n function loaded() {\n loadedCount += 1;\n // wait until both files are loaded\n if (loadedCount === 2) resolve();\n }\n\n const head = d3_select('head');\n\n // load streetside pannellum viewer css\n head.selectAll('#ideditor-streetside-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-streetside-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(pannellumViewerCSS))\n .on('load.serviceStreetside', loaded)\n .on('error.serviceStreetside', function() {\n reject();\n });\n\n // load streetside pannellum viewer js\n head.selectAll('#ideditor-streetside-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-streetside-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(pannellumViewerJS))\n .on('load.serviceStreetside', loaded)\n .on('error.serviceStreetside', function() {\n reject();\n });\n })\n .catch(function() {\n _loadViewerPromise = null;\n });\n\n return _loadViewerPromise;\n\n function step(stepBy) {\n return () => {\n let viewer = context.container().select('.photoviewer');\n let selected = viewer.empty() ? undefined : viewer.datum();\n if (!selected) return;\n\n let nextID = (stepBy === 1 ? selected.ne : selected.pr);\n let yaw = _pannellumViewer.getYaw();\n let ca = selected.ca + yaw;\n let origin = selected.loc;\n\n // construct a search trapezoid pointing out from current bubble\n const meters = 35;\n let p1 = [\n origin[0] + geoMetersToLon(meters / 5, origin[1]),\n origin[1]\n ];\n let p2 = [\n origin[0] + geoMetersToLon(meters / 2, origin[1]),\n origin[1] + geoMetersToLat(meters)\n ];\n let p3 = [\n origin[0] - geoMetersToLon(meters / 2, origin[1]),\n origin[1] + geoMetersToLat(meters)\n ];\n let p4 = [\n origin[0] - geoMetersToLon(meters / 5, origin[1]),\n origin[1]\n ];\n\n let poly = [p1, p2, p3, p4, p1];\n\n // rotate it to face forward/backward\n let angle = (stepBy === 1 ? ca : ca + 180) * (Math.PI / 180);\n poly = geoRotate(poly, -angle, origin);\n\n let extent = poly.reduce((extent, point) => {\n return extent.extend(geoExtent(point));\n }, geoExtent());\n\n // find nearest other bubble in the search polygon\n let minDist = Infinity;\n _ssCache.bubbles.rtree.search(extent.bbox())\n .forEach(d => {\n if (d.data.key === selected.key) return;\n if (!geoPointInPolygon(d.data.loc, poly)) return;\n\n let dist = geoVecLength(d.data.loc, selected.loc);\n let theta = selected.ca - d.data.ca;\n let minTheta = Math.min(Math.abs(theta), 360 - Math.abs(theta));\n if (minTheta > 20) {\n dist += 5; // penalize distance if camera angles don't match\n }\n\n if (dist < minDist) {\n nextID = d.data.key;\n minDist = dist;\n }\n });\n\n let nextBubble = nextID && that.cachedImage(nextID);\n if (!nextBubble) return;\n\n context.map().centerEase(nextBubble.loc);\n\n that.selectImage(context, nextBubble.key)\n .yaw(yaw)\n .showViewer(context);\n };\n }\n },\n\n\n yaw: function(yaw) {\n if (typeof yaw !== 'number') return yaw;\n _sceneOptions.yaw = yaw;\n return this;\n },\n\n /**\n * showViewer()\n */\n showViewer: function(context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.ms-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap\n .classed('hide', false)\n .selectAll('.photo-wrapper.ms-wrapper')\n .classed('hide', false);\n }\n\n return this;\n },\n\n\n /**\n * hideViewer()\n */\n hideViewer: function (context) {\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n this.updateUrlImage(null);\n\n return this.setStyles(context, null, true);\n },\n\n\n /**\n * selectImage().\n */\n selectImage: function (context, key) {\n let that = this;\n\n let d = this.cachedImage(key);\n\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null, true);\n\n let wrap = context.container().select('.photoviewer .ms-wrapper');\n let attribution = wrap.selectAll('.photo-attribution').html('');\n\n wrap.selectAll('.pnlm-load-box') // display \"loading..\"\n .style('display', 'block');\n\n if (!d) return this;\n\n this.updateUrlImage(key);\n\n _sceneOptions.northOffset = d.ca;\n\n let line1 = attribution\n .append('div')\n .attr('class', 'attribution-row');\n\n const hiresDomId = utilUniqueDomId('streetside-hires');\n\n // Add hires checkbox\n let label = line1\n .append('label')\n .attr('for', hiresDomId)\n .attr('class', 'streetside-hires');\n\n label\n .append('input')\n .attr('type', 'checkbox')\n .attr('id', hiresDomId)\n .property('checked', _hires)\n .on('click', (d3_event) => {\n d3_event.stopPropagation();\n\n _hires = !_hires;\n _resolution = _hires ? 1024 : 512;\n wrap.call(setupCanvas, true);\n\n let viewstate = {\n yaw: _pannellumViewer.getYaw(),\n pitch: _pannellumViewer.getPitch(),\n hfov: _pannellumViewer.getHfov()\n };\n\n _sceneOptions = Object.assign(_sceneOptions, viewstate);\n that.selectImage(context, d.key)\n .showViewer(context);\n });\n\n label\n .append('span')\n .call(t.append('streetside.hires'));\n\n\n let captureInfo = line1\n .append('div')\n .attr('class', 'attribution-capture-info');\n\n // Add capture date\n if (d.captured_by) {\n const yyyy = (new Date()).getFullYear();\n\n captureInfo\n .append('a')\n .attr('class', 'captured_by')\n .attr('target', '_blank')\n .attr('href', 'https://www.microsoft.com/en-us/maps/streetside')\n .text('\u00A9' + yyyy + ' Microsoft');\n\n captureInfo\n .append('span')\n .text('|');\n }\n\n if (d.captured_at) {\n captureInfo\n .append('span')\n .attr('class', 'captured_at')\n .text(localeTimestamp(d.captured_at));\n }\n\n // Add image links\n let line2 = attribution\n .append('div')\n .attr('class', 'attribution-row');\n\n line2\n .append('a')\n .attr('class', 'image-view-link')\n .attr('target', '_blank')\n .attr('href', 'https://www.bing.com/maps?cp=' + d.loc[1] + '~' + d.loc[0] +\n '&lvl=17&dir=' + d.ca + '&style=x&v=2&sV=1')\n .call(t.append('streetside.view_on_bing'));\n\n line2\n .append('a')\n .attr('class', 'image-report-link')\n .attr('target', '_blank')\n .attr('href', 'https://www.bing.com/maps/privacyreport/streetsideprivacyreport?bubbleid=' +\n encodeURIComponent(d.key) + '&focus=photo&lat=' + d.loc[1] + '&lng=' + d.loc[0] + '&z=17')\n .call(t.append('streetside.report'));\n\n // Cubemap face code order matters here: front=01, right=02, back=03, left=10, up=11, down=12\n const faceKeys = ['01','02','03','10','11','12'];\n\n // Map images to cube faces\n let quadKeys = getQuadKeys();\n let faces = faceKeys.map((faceKey) => {\n return quadKeys.map((quadKey) => {\n const xy = qkToXY(quadKey);\n return {\n face: faceKey,\n url: d.imageUrl\n .replace('{faceId}', faceKey)\n .replace('{tileId}', quadKey),\n x: xy[0],\n y: xy[1]\n };\n });\n });\n\n loadFaces(faces)\n .then(function() {\n\n if (!_pannellumViewer) {\n that.initViewer();\n } else {\n // make a new scene\n _currScene += 1;\n let sceneID = _currScene.toString();\n _pannellumViewer\n .addScene(sceneID, _sceneOptions)\n .loadScene(sceneID);\n\n // remove previous scene\n if (_currScene > 2) {\n sceneID = (_currScene - 1).toString();\n _pannellumViewer\n .removeScene(sceneID);\n }\n }\n });\n\n return this;\n },\n\n\n getSequenceKeyForBubble: function(d) {\n return d && d.sequenceKey;\n },\n\n\n // Updates the currently highlighted sequence and selected bubble.\n // Reset is only necessary when interacting with the viewport because\n // this implicitly changes the currently selected bubble/sequence\n setStyles: function (context, hovered, reset) {\n if (reset) { // reset all layers\n context.container().selectAll('.viewfield-group')\n .classed('highlighted', false)\n .classed('hovered', false)\n .classed('currentView', false);\n\n context.container().selectAll('.sequence')\n .classed('highlighted', false)\n .classed('currentView', false);\n }\n\n let hoveredBubbleKey = hovered && hovered.key;\n let hoveredSequenceKey = this.getSequenceKeyForBubble(hovered);\n let hoveredSequence = hoveredSequenceKey && _ssCache.sequences[hoveredSequenceKey];\n let hoveredBubbleKeys = (hoveredSequence && hoveredSequence.bubbles.map(d => d.key)) || [];\n\n let viewer = context.container().select('.photoviewer');\n let selected = viewer.empty() ? undefined : viewer.datum();\n let selectedBubbleKey = selected && selected.key;\n let selectedSequenceKey = this.getSequenceKeyForBubble(selected);\n let selectedSequence = selectedSequenceKey && _ssCache.sequences[selectedSequenceKey];\n let selectedBubbleKeys = (selectedSequence && selectedSequence.bubbles.map(d => d.key)) || [];\n\n // highlight sibling viewfields on either the selected or the hovered sequences\n let highlightedBubbleKeys = utilArrayUnion(hoveredBubbleKeys, selectedBubbleKeys);\n\n context.container().selectAll('.layer-streetside-images .viewfield-group')\n .classed('highlighted', d => highlightedBubbleKeys.indexOf(d.key) !== -1)\n .classed('hovered', d => d.key === hoveredBubbleKey)\n .classed('currentView', d => d.key === selectedBubbleKey);\n\n context.container().selectAll('.layer-streetside-images .sequence')\n .classed('highlighted', d => d.properties.key === hoveredSequenceKey)\n .classed('currentView', d => d.properties.key === selectedSequenceKey);\n\n // update viewfields if needed\n context.container().selectAll('.layer-streetside-images .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n let d = this.parentNode.__data__;\n if (d.pano && d.key !== selectedBubbleKey) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n return this;\n },\n\n\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'streetside/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n\n /**\n * cache().\n */\n cache: function () {\n return _ssCache;\n }\n};\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { json as d3_json } from 'd3-fetch';\n\nimport { utilObjectOmit, utilQsString } from '../util';\nimport { localizer } from '../core/localizer';\nimport { allowUpperCaseTagValues } from '../osm/tags';\n\nimport { taginfoApiUrl } from '../../config/id.js';\n\nvar apibase = taginfoApiUrl;\nvar _inflight = {};\nvar _popularKeys = {};\n// manually exclude some additional keys \u2013 #5377, #7485, #10287, #11733\n// these will be returned by keys(), but taginfo will not be queried for values() requests\nvar _extraExcludedKeys = /^(addr:.+|postal_code|via|((int_|loc_|nat_|official_|old_|ref_|reg_|short_|full_|sorting_|alt_|artist_|long_|bridge:|tunnel:)?name(:left|:right)?(:[a-z]+)?))$/;\n\nvar _extraExcludedKeyNames = /^(hashtags?|created_by)$/;\n\nvar _taginfoCache = {};\n\nvar tag_sorts = {\n point: 'count_nodes',\n vertex: 'count_nodes',\n area: 'count_ways',\n line: 'count_ways'\n};\nvar tag_sort_members = {\n point: 'count_node_members',\n vertex: 'count_node_members',\n area: 'count_way_members',\n line: 'count_way_members',\n relation: 'count_relation_members'\n};\nvar tag_filters = {\n point: 'nodes',\n vertex: 'nodes',\n area: 'ways',\n line: 'ways'\n};\nvar tag_members_fractions = {\n point: 'count_node_members_fraction',\n vertex: 'count_node_members_fraction',\n area: 'count_way_members_fraction',\n line: 'count_way_members_fraction',\n relation: 'count_relation_members_fraction'\n};\n\n\nfunction sets(params, n, o) {\n if (params.geometry && o[params.geometry]) {\n params[n] = o[params.geometry];\n }\n return params;\n}\n\n\nfunction setFilter(params) {\n return sets(params, 'filter', tag_filters);\n}\n\n\nfunction setSort(params) {\n return sets(params, 'sortname', tag_sorts);\n}\n\n\nfunction setSortMembers(params) {\n return sets(params, 'sortname', tag_sort_members);\n}\n\n\nfunction clean(params) {\n return utilObjectOmit(params, ['geometry', 'debounce']);\n}\n\n\nfunction filterKeys(type) {\n var count_type = type ? 'count_' + type : 'count_all';\n return function(d) {\n return Number(d[count_type]) > 2500 || d.in_wiki;\n };\n}\n\n\nfunction filterMultikeys(prefix) {\n return function(d) {\n // d.key begins with prefix, and d.key contains no additional ':'s\n var re = new RegExp('^' + prefix + '(.*)$', 'i');\n var matches = d.key.match(re) || [];\n return (matches.length === 2 && matches[1].indexOf(':') === -1);\n };\n}\n\n\nfunction filterValues(allowUpperCase, key) {\n return function(d) {\n if (d.value.match(/[;,]/) !== null) return false; // exclude some punctuation\n if (!allowUpperCase &&\n !(key === 'type' && d.value === 'associatedStreet') &&\n d.value.match(/[A-Z*]/) !== null) return false; // exclude uppercase letters\n return d.count > 100; // exclude rare tags\n };\n}\n\n\nfunction filterRoles(geometry) {\n return function(d) {\n if (d.role === '') return false; // exclude empty role\n if (d.role.match(/[A-Z*;,]/) !== null) return false; // exclude uppercase letters and some punctuation\n return Number(d[tag_members_fractions[geometry]]) > 0.0;\n };\n}\n\n\nfunction valKey(d) {\n return {\n value: d.key,\n title: d.key\n };\n}\n\n\nfunction valKeyDescription(d) {\n var obj = {\n value: d.value,\n title: d.description || d.value\n };\n return obj;\n}\n\n\nfunction roleKey(d) {\n return {\n value: d.role,\n title: d.role\n };\n}\n\n\n// sort keys with ':' lower than keys without ':'\nfunction sortKeys(a, b) {\n return (a.key.indexOf(':') === -1 && b.key.indexOf(':') !== -1) ? -1\n : (a.key.indexOf(':') !== -1 && b.key.indexOf(':') === -1) ? 1\n : 0;\n}\n\n\nvar debouncedRequest = debounce(request, 300, { leading: false });\n\nfunction request(url, params, exactMatch, callback, loaded) {\n if (_inflight[url]) return;\n\n if (checkCache(url, params, exactMatch, callback)) return;\n\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, { signal: controller.signal })\n .then(function(result) {\n delete _inflight[url];\n if (loaded) loaded(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (loaded) loaded(err.message);\n });\n}\n\n\nfunction checkCache(url, params, exactMatch, callback) {\n var rp = params.rp || 25;\n var testQuery = params.query || '';\n var testUrl = url;\n\n do {\n var hit = _taginfoCache[testUrl];\n\n // exact match, or shorter match yielding fewer than max results (rp)\n if (hit && (url === testUrl || hit.length < rp)) {\n callback(null, hit);\n return true;\n }\n\n // don't try to shorten the query\n if (exactMatch || !testQuery.length) return false;\n\n // do shorten the query to see if we already have a cached result\n // that has returned fewer than max results (rp)\n testQuery = testQuery.slice(0, -1);\n testUrl = url.replace(/&query=(.*?)&/, '&query=' + testQuery + '&');\n } while (testQuery.length >= 0);\n\n return false;\n}\n\n\nexport default {\n\n init: function() {\n _inflight = {};\n _taginfoCache = {};\n _popularKeys = [];\n\n // Fetch popular keys. We'll exclude these from `values`\n // lookups because they stress taginfo, and they aren't likely\n // to yield meaningful autocomplete results.. see #3955\n var params = {\n rp: 100,\n sortname: 'values_all',\n sortorder: 'desc',\n page: 1,\n debounce: false,\n lang: localizer.languageCode()\n };\n this.keys(params, function(err, data) {\n if (err) return;\n data.forEach(function(d) {\n if (d.value === 'opening_hours') return; // exception\n _popularKeys[d.value] = true;\n });\n });\n },\n\n\n reset: function() {\n Object.values(_inflight).forEach(function(controller) { controller.abort(); });\n _inflight = {};\n },\n\n\n keys: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(params));\n params = Object.assign({\n rp: 10,\n sortname: 'count_all',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var url = apibase + 'keys/all?' + utilQsString(params);\n doRequest(url, params, false, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n var f = filterKeys(params.filter);\n var result = d.data.filter(f).filter(d => !_extraExcludedKeyNames.test(d.key)).sort(sortKeys).map(valKey);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n multikeys: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(params));\n params = Object.assign({\n rp: 25,\n sortname: 'count_all',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var prefix = params.query;\n var url = apibase + 'keys/all?' + utilQsString(params);\n doRequest(url, params, true, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n var f = filterMultikeys(prefix);\n var result = d.data.filter(f).map(valKey);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n values: function(params, callback) {\n // Exclude popular keys from values lookups.. see #3955\n var key = params.key;\n if (key && _popularKeys[key] === true || _extraExcludedKeys.test(key)) {\n callback(null, []);\n return;\n }\n\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(setFilter(params)));\n params = Object.assign({\n rp: 25,\n sortname: 'count_all',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var url = apibase + 'key/values?' + utilQsString(params);\n doRequest(url, params, false, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n // In most cases we prefer taginfo value results with lowercase letters.\n // A few OSM keys expect values to contain uppercase values (see #3377).\n // This is not an exhaustive list (e.g. `name` also has uppercase values)\n // but these are the fields where taginfo value lookup is most useful.\n var allowUpperCase = allowUpperCaseTagValues.test(params.key);\n var f = filterValues(allowUpperCase, params.key);\n\n var result = d.data.filter(f).map(valKeyDescription);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n roles: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n var geometry = params.geometry;\n params = clean(setSortMembers(params));\n params = Object.assign({\n rp: 25,\n sortname: 'count_all_members',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var url = apibase + 'relation/roles?' + utilQsString(params);\n doRequest(url, params, true, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n var f = filterRoles(geometry);\n var result = d.data.filter(f).map(roleKey);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n docs: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(params));\n\n var path = 'key/wiki_pages?';\n if (params.value) {\n path = 'tag/wiki_pages?';\n } else if (params.rtype) {\n path = 'relation/wiki_pages?';\n }\n\n var url = apibase + path + utilQsString(params);\n doRequest(url, params, true, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n _taginfoCache[url] = d.data;\n callback(null, d.data);\n }\n });\n },\n\n\n apibase: function(_) {\n if (!arguments.length) return apibase;\n apibase = _;\n return this;\n }\n\n};\n", "import {\n BBox,\n Feature,\n FeatureCollection,\n Geometry,\n GeometryCollection,\n GeometryObject,\n LineString,\n MultiLineString,\n MultiPoint,\n MultiPolygon,\n Point,\n Polygon,\n Position,\n GeoJsonProperties,\n} from \"geojson\";\n\nimport { Id } from \"./lib/geojson.js\";\nexport * from \"./lib/geojson.js\";\n\n/**\n * @module helpers\n */\n\n// TurfJS Combined Types\nexport type Coord = Feature | Point | Position;\n\n/**\n * Linear measurement units.\n *\n * ⚠️ Warning. Be aware of the implications of using radian or degree units to\n * measure distance. The distance represented by a degree of longitude *varies*\n * depending on latitude.\n *\n * See https://www.thoughtco.com/degree-of-latitude-and-longitude-distance-4070616\n * for an illustration of this behaviour.\n *\n * @typedef\n */\nexport type Units =\n | \"meters\"\n | \"metres\"\n | \"millimeters\"\n | \"millimetres\"\n | \"centimeters\"\n | \"centimetres\"\n | \"kilometers\"\n | \"kilometres\"\n | \"miles\"\n | \"nauticalmiles\"\n | \"inches\"\n | \"yards\"\n | \"feet\"\n | \"radians\"\n | \"degrees\";\n\n/**\n * Area measurement units.\n *\n * @typedef\n */\nexport type AreaUnits =\n | Exclude\n | \"acres\"\n | \"hectares\";\n\n/**\n * Grid types.\n *\n * @typedef\n */\nexport type Grid = \"point\" | \"square\" | \"hex\" | \"triangle\";\n\n/**\n * Shorthand corner identifiers.\n *\n * @typedef\n */\nexport type Corners = \"sw\" | \"se\" | \"nw\" | \"ne\" | \"center\" | \"centroid\";\n\n/**\n * Geometries made up of lines i.e. lines and polygons.\n *\n * @typedef\n */\nexport type Lines = LineString | MultiLineString | Polygon | MultiPolygon;\n\n/**\n * Convenience type for all possible GeoJSON.\n *\n * @typedef\n */\nexport type AllGeoJSON =\n | Feature\n | FeatureCollection\n | Geometry\n | GeometryCollection;\n\n/**\n * The Earth radius in meters. Used by Turf modules that model the Earth as a sphere. The {@link https://en.wikipedia.org/wiki/Earth_radius#Arithmetic_mean_radius mean radius} was selected because it is {@link https://rosettacode.org/wiki/Haversine_formula#:~:text=This%20value%20is%20recommended recommended } by the Haversine formula (used by turf/distance) to reduce error.\n *\n * @constant\n */\nexport const earthRadius = 6371008.8;\n\n/**\n * Unit of measurement factors based on earthRadius.\n *\n * Keys are the name of the unit, values are the number of that unit in a single radian\n *\n * @constant\n */\nexport const factors: Record = {\n centimeters: earthRadius * 100,\n centimetres: earthRadius * 100,\n degrees: 360 / (2 * Math.PI),\n feet: earthRadius * 3.28084,\n inches: earthRadius * 39.37,\n kilometers: earthRadius / 1000,\n kilometres: earthRadius / 1000,\n meters: earthRadius,\n metres: earthRadius,\n miles: earthRadius / 1609.344,\n millimeters: earthRadius * 1000,\n millimetres: earthRadius * 1000,\n nauticalmiles: earthRadius / 1852,\n radians: 1,\n yards: earthRadius * 1.0936,\n};\n\n/**\n\n * Area of measurement factors based on 1 square meter.\n *\n * @constant\n */\nexport const areaFactors: Record = {\n acres: 0.000247105,\n centimeters: 10000,\n centimetres: 10000,\n feet: 10.763910417,\n hectares: 0.0001,\n inches: 1550.003100006,\n kilometers: 0.000001,\n kilometres: 0.000001,\n meters: 1,\n metres: 1,\n miles: 3.86e-7,\n nauticalmiles: 2.9155334959812285e-7,\n millimeters: 1000000,\n millimetres: 1000000,\n yards: 1.195990046,\n};\n\n/**\n * Wraps a GeoJSON {@link Geometry} in a GeoJSON {@link Feature}.\n *\n * @function\n * @param {GeometryObject} geometry input geometry\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a GeoJSON Feature\n * @example\n * var geometry = {\n * \"type\": \"Point\",\n * \"coordinates\": [110, 50]\n * };\n *\n * var feature = turf.feature(geometry);\n *\n * //=feature\n */\nexport function feature<\n G extends GeometryObject = Geometry,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geom: G | null,\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const feat: any = { type: \"Feature\" };\n if (options.id === 0 || options.id) {\n feat.id = options.id;\n }\n if (options.bbox) {\n feat.bbox = options.bbox;\n }\n feat.properties = properties || {};\n feat.geometry = geom;\n return feat;\n}\n\n/**\n * Creates a GeoJSON {@link Geometry} from a Geometry string type & coordinates.\n * For GeometryCollection type use `helpers.geometryCollection`\n *\n * @function\n * @param {(\"Point\" | \"LineString\" | \"Polygon\" | \"MultiPoint\" | \"MultiLineString\" | \"MultiPolygon\")} type Geometry Type\n * @param {Array} coordinates Coordinates\n * @param {Object} [options={}] Optional Parameters\n * @returns {Geometry} a GeoJSON Geometry\n * @example\n * var type = \"Point\";\n * var coordinates = [110, 50];\n * var geometry = turf.geometry(type, coordinates);\n * // => geometry\n */\nexport function geometry<\n T extends\n | \"Point\"\n | \"LineString\"\n | \"Polygon\"\n | \"MultiPoint\"\n | \"MultiLineString\"\n | \"MultiPolygon\",\n>(\n type: T,\n coordinates: any[],\n _options: Record = {}\n): Extract {\n switch (type) {\n case \"Point\":\n return point(coordinates).geometry as Extract;\n case \"LineString\":\n return lineString(coordinates).geometry as Extract;\n case \"Polygon\":\n return polygon(coordinates).geometry as Extract;\n case \"MultiPoint\":\n return multiPoint(coordinates).geometry as Extract;\n case \"MultiLineString\":\n return multiLineString(coordinates).geometry as Extract<\n Geometry,\n { type: T }\n >;\n case \"MultiPolygon\":\n return multiPolygon(coordinates).geometry as Extract<\n Geometry,\n { type: T }\n >;\n default:\n throw new Error(type + \" is invalid\");\n }\n}\n\n/**\n * Creates a {@link Point} {@link Feature} from a Position.\n *\n * @function\n * @param {Position} coordinates longitude, latitude position (each in decimal degrees)\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a Point feature\n * @example\n * var point = turf.point([-75.343, 39.984]);\n *\n * //=point\n */\nexport function point

    (\n coordinates: Position,\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n if (!coordinates) {\n throw new Error(\"coordinates is required\");\n }\n if (!Array.isArray(coordinates)) {\n throw new Error(\"coordinates must be an Array\");\n }\n if (coordinates.length < 2) {\n throw new Error(\"coordinates must be at least 2 numbers long\");\n }\n if (!isNumber(coordinates[0]) || !isNumber(coordinates[1])) {\n throw new Error(\"coordinates must contain numbers\");\n }\n\n const geom: Point = {\n type: \"Point\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Point} {@link FeatureCollection} from an Array of Point coordinates.\n *\n * @function\n * @param {Position[]} coordinates an array of Points\n * @param {GeoJsonProperties} [properties={}] Translate these properties to each Feature\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north]\n * associated with the FeatureCollection\n * @param {Id} [options.id] Identifier associated with the FeatureCollection\n * @returns {FeatureCollection} Point Feature\n * @example\n * var points = turf.points([\n * [-75, 39],\n * [-80, 45],\n * [-78, 50]\n * ]);\n *\n * //=points\n */\nexport function points

    (\n coordinates: Position[],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n return featureCollection(\n coordinates.map((coords) => {\n return point(coords, properties);\n }),\n options\n );\n}\n\n/**\n * Creates a {@link Polygon} {@link Feature} from an Array of LinearRings.\n *\n * @function\n * @param {Position[][]} coordinates an array of LinearRings\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} Polygon Feature\n * @example\n * var polygon = turf.polygon([[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]], { name: 'poly1' });\n *\n * //=polygon\n */\nexport function polygon

    (\n coordinates: Position[][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n for (const ring of coordinates) {\n if (ring.length < 4) {\n throw new Error(\n \"Each LinearRing of a Polygon must have 4 or more Positions.\"\n );\n }\n\n if (ring[ring.length - 1].length !== ring[0].length) {\n throw new Error(\"First and last Position are not equivalent.\");\n }\n\n for (let j = 0; j < ring[ring.length - 1].length; j++) {\n // Check if first point of Polygon contains two numbers\n if (ring[ring.length - 1][j] !== ring[0][j]) {\n throw new Error(\"First and last Position are not equivalent.\");\n }\n }\n }\n const geom: Polygon = {\n type: \"Polygon\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Polygon} {@link FeatureCollection} from an Array of Polygon coordinates.\n *\n * @function\n * @param {Position[][][]} coordinates an array of Polygon coordinates\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the FeatureCollection\n * @returns {FeatureCollection} Polygon FeatureCollection\n * @example\n * var polygons = turf.polygons([\n * [[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]],\n * [[[-15, 42], [-14, 46], [-12, 41], [-17, 44], [-15, 42]]],\n * ]);\n *\n * //=polygons\n */\nexport function polygons

    (\n coordinates: Position[][][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n return featureCollection(\n coordinates.map((coords) => {\n return polygon(coords, properties);\n }),\n options\n );\n}\n\n/**\n * Creates a {@link LineString} {@link Feature} from an Array of Positions.\n *\n * @function\n * @param {Position[]} coordinates an array of Positions\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} LineString Feature\n * @example\n * var linestring1 = turf.lineString([[-24, 63], [-23, 60], [-25, 65], [-20, 69]], {name: 'line 1'});\n * var linestring2 = turf.lineString([[-14, 43], [-13, 40], [-15, 45], [-10, 49]], {name: 'line 2'});\n *\n * //=linestring1\n * //=linestring2\n */\nexport function lineString

    (\n coordinates: Position[],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n if (coordinates.length < 2) {\n throw new Error(\"coordinates must be an array of two or more positions\");\n }\n const geom: LineString = {\n type: \"LineString\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link LineString} {@link FeatureCollection} from an Array of LineString coordinates.\n *\n * @function\n * @param {Position[][]} coordinates an array of LinearRings\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north]\n * associated with the FeatureCollection\n * @param {Id} [options.id] Identifier associated with the FeatureCollection\n * @returns {FeatureCollection} LineString FeatureCollection\n * @example\n * var linestrings = turf.lineStrings([\n * [[-24, 63], [-23, 60], [-25, 65], [-20, 69]],\n * [[-14, 43], [-13, 40], [-15, 45], [-10, 49]]\n * ]);\n *\n * //=linestrings\n */\nexport function lineStrings

    (\n coordinates: Position[][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n return featureCollection(\n coordinates.map((coords) => {\n return lineString(coords, properties);\n }),\n options\n );\n}\n\n/**\n * Takes one or more {@link Feature|Features} and creates a {@link FeatureCollection}.\n *\n * @function\n * @param {Array>} features input features\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {FeatureCollection} FeatureCollection of Features\n * @example\n * var locationA = turf.point([-75.343, 39.984], {name: 'Location A'});\n * var locationB = turf.point([-75.833, 39.284], {name: 'Location B'});\n * var locationC = turf.point([-75.534, 39.123], {name: 'Location C'});\n *\n * var collection = turf.featureCollection([\n * locationA,\n * locationB,\n * locationC\n * ]);\n *\n * //=collection\n */\nexport function featureCollection<\n G extends GeometryObject = Geometry,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n features: Array>,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n const fc: any = { type: \"FeatureCollection\" };\n if (options.id) {\n fc.id = options.id;\n }\n if (options.bbox) {\n fc.bbox = options.bbox;\n }\n fc.features = features;\n return fc;\n}\n\n/**\n * Creates a {@link Feature}<{@link MultiLineString}> based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Position[][]} coordinates an array of LineStrings\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a MultiLineString feature\n * @throws {Error} if no coordinates are passed\n * @example\n * var multiLine = turf.multiLineString([[[0,0],[10,10]]]);\n *\n * //=multiLine\n */\nexport function multiLineString<\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n coordinates: Position[][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const geom: MultiLineString = {\n type: \"MultiLineString\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Feature}<{@link MultiPoint}> based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Position[]} coordinates an array of Positions\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a MultiPoint feature\n * @throws {Error} if no coordinates are passed\n * @example\n * var multiPt = turf.multiPoint([[0,0],[10,10]]);\n *\n * //=multiPt\n */\nexport function multiPoint

    (\n coordinates: Position[],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const geom: MultiPoint = {\n type: \"MultiPoint\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Feature}<{@link MultiPolygon}> based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Position[][][]} coordinates an array of Polygons\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a multipolygon feature\n * @throws {Error} if no coordinates are passed\n * @example\n * var multiPoly = turf.multiPolygon([[[[0,0],[0,10],[10,10],[10,0],[0,0]]]]);\n *\n * //=multiPoly\n *\n */\nexport function multiPolygon

    (\n coordinates: Position[][][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const geom: MultiPolygon = {\n type: \"MultiPolygon\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a Feature based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Array} geometries an array of GeoJSON Geometries\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a GeoJSON GeometryCollection Feature\n * @example\n * var pt = turf.geometry(\"Point\", [100, 0]);\n * var line = turf.geometry(\"LineString\", [[101, 0], [102, 1]]);\n * var collection = turf.geometryCollection([pt, line]);\n *\n * // => collection\n */\nexport function geometryCollection<\n G extends\n | Point\n | LineString\n | Polygon\n | MultiPoint\n | MultiLineString\n | MultiPolygon,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geometries: Array,\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature, P> {\n const geom: GeometryCollection = {\n type: \"GeometryCollection\",\n geometries,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Round number to precision\n *\n * @function\n * @param {number} num Number\n * @param {number} [precision=0] Precision\n * @returns {number} rounded number\n * @example\n * turf.round(120.4321)\n * //=120\n *\n * turf.round(120.4321, 2)\n * //=120.43\n */\nexport function round(num: number, precision = 0): number {\n if (precision && !(precision >= 0)) {\n throw new Error(\"precision must be a positive number\");\n }\n const multiplier = Math.pow(10, precision || 0);\n return Math.round(num * multiplier) / multiplier;\n}\n\n/**\n * Convert a distance measurement (assuming a spherical Earth) from radians to a more friendly unit.\n * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet\n *\n * @function\n * @param {number} radians in radians across the sphere\n * @param {Units} [units=\"kilometers\"] can be degrees, radians, miles, inches, yards, metres,\n * meters, kilometres, kilometers.\n * @returns {number} distance\n */\nexport function radiansToLength(\n radians: number,\n units: Units = \"kilometers\"\n): number {\n const factor = factors[units];\n if (!factor) {\n throw new Error(units + \" units is invalid\");\n }\n return radians * factor;\n}\n\n/**\n * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into radians\n * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet\n *\n * @function\n * @param {number} distance in real units\n * @param {Units} [units=\"kilometers\"] can be degrees, radians, miles, inches, yards, metres,\n * meters, kilometres, kilometers.\n * @returns {number} radians\n */\nexport function lengthToRadians(\n distance: number,\n units: Units = \"kilometers\"\n): number {\n const factor = factors[units];\n if (!factor) {\n throw new Error(units + \" units is invalid\");\n }\n return distance / factor;\n}\n\n/**\n * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into degrees\n * Valid units: miles, nauticalmiles, inches, yards, meters, metres, centimeters, kilometres, feet\n *\n * @function\n * @param {number} distance in real units\n * @param {Units} [units=\"kilometers\"] can be degrees, radians, miles, inches, yards, metres,\n * meters, kilometres, kilometers.\n * @returns {number} degrees\n */\nexport function lengthToDegrees(distance: number, units?: Units): number {\n return radiansToDegrees(lengthToRadians(distance, units));\n}\n\n/**\n * Converts any bearing angle from the north line direction (positive clockwise)\n * and returns an angle between 0-360 degrees (positive clockwise), 0 being the north line\n *\n * @function\n * @param {number} bearing angle, between -180 and +180 degrees\n * @returns {number} angle between 0 and 360 degrees\n */\nexport function bearingToAzimuth(bearing: number): number {\n let angle = bearing % 360;\n if (angle < 0) {\n angle += 360;\n }\n return angle;\n}\n\n/**\n * Converts any azimuth angle from the north line direction (positive clockwise)\n * and returns an angle between -180 and +180 degrees (positive clockwise), 0 being the north line\n *\n * @function\n * @param {number} angle between 0 and 360 degrees\n * @returns {number} bearing between -180 and +180 degrees\n */\nexport function azimuthToBearing(angle: number): number {\n // Ignore full revolutions (multiples of 360)\n angle = angle % 360;\n\n if (angle > 180) {\n return angle - 360;\n } else if (angle < -180) {\n return angle + 360;\n }\n\n return angle;\n}\n\n/**\n * Converts an angle in radians to degrees\n *\n * @function\n * @param {number} radians angle in radians\n * @returns {number} degrees between 0 and 360 degrees\n */\nexport function radiansToDegrees(radians: number): number {\n // % (2 * Math.PI) radians in case someone passes value > 2π\n const normalisedRadians = radians % (2 * Math.PI);\n return (normalisedRadians * 180) / Math.PI;\n}\n\n/**\n * Converts an angle in degrees to radians\n *\n * @function\n * @param {number} degrees angle between 0 and 360 degrees\n * @returns {number} angle in radians\n */\nexport function degreesToRadians(degrees: number): number {\n // % 360 degrees in case someone passes value > 360\n const normalisedDegrees = degrees % 360;\n return (normalisedDegrees * Math.PI) / 180;\n}\n\n/**\n * Converts a length from one unit to another.\n *\n * @function\n * @param {number} length Length to be converted\n * @param {Units} [originalUnit=\"kilometers\"] Input length unit\n * @param {Units} [finalUnit=\"kilometers\"] Returned length unit\n * @returns {number} The converted length\n */\nexport function convertLength(\n length: number,\n originalUnit: Units = \"kilometers\",\n finalUnit: Units = \"kilometers\"\n): number {\n if (!(length >= 0)) {\n throw new Error(\"length must be a positive number\");\n }\n return radiansToLength(lengthToRadians(length, originalUnit), finalUnit);\n}\n\n/**\n * Converts an area from one unit to another.\n *\n * @function\n * @param {number} area Area to be converted\n * @param {AreaUnits} [originalUnit=\"meters\"] Input area unit\n * @param {AreaUnits} [finalUnit=\"kilometers\"] Returned area unit\n * @returns {number} The converted length\n */\nexport function convertArea(\n area: number,\n originalUnit: AreaUnits = \"meters\",\n finalUnit: AreaUnits = \"kilometers\"\n): number {\n if (!(area >= 0)) {\n throw new Error(\"area must be a positive number\");\n }\n\n const startFactor = areaFactors[originalUnit];\n if (!startFactor) {\n throw new Error(\"invalid original units\");\n }\n\n const finalFactor = areaFactors[finalUnit];\n if (!finalFactor) {\n throw new Error(\"invalid final units\");\n }\n\n return (area / startFactor) * finalFactor;\n}\n\n/**\n * isNumber\n *\n * @function\n * @param {any} num Number to validate\n * @returns {boolean} true/false\n * @example\n * turf.isNumber(123)\n * //=true\n * turf.isNumber('foo')\n * //=false\n */\nexport function isNumber(num: any): boolean {\n return !isNaN(num) && num !== null && !Array.isArray(num);\n}\n\n/**\n * isObject\n *\n * @function\n * @param {any} input variable to validate\n * @returns {boolean} true/false, including false for Arrays and Functions\n * @example\n * turf.isObject({elevation: 10})\n * //=true\n * turf.isObject('foo')\n * //=false\n */\nexport function isObject(input: any): boolean {\n return input !== null && typeof input === \"object\" && !Array.isArray(input);\n}\n\n/**\n * Validate BBox\n *\n * @private\n * @param {any} bbox BBox to validate\n * @returns {void}\n * @throws {Error} if BBox is not valid\n * @example\n * validateBBox([-180, -40, 110, 50])\n * //=OK\n * validateBBox([-180, -40])\n * //=Error\n * validateBBox('Foo')\n * //=Error\n * validateBBox(5)\n * //=Error\n * validateBBox(null)\n * //=Error\n * validateBBox(undefined)\n * //=Error\n */\nexport function validateBBox(bbox: any): void {\n if (!bbox) {\n throw new Error(\"bbox is required\");\n }\n if (!Array.isArray(bbox)) {\n throw new Error(\"bbox must be an Array\");\n }\n if (bbox.length !== 4 && bbox.length !== 6) {\n throw new Error(\"bbox must be an Array of 4 or 6 numbers\");\n }\n bbox.forEach((num) => {\n if (!isNumber(num)) {\n throw new Error(\"bbox must only contain numbers\");\n }\n });\n}\n\n/**\n * Validate Id\n *\n * @private\n * @param {any} id Id to validate\n * @returns {void}\n * @throws {Error} if Id is not valid\n * @example\n * validateId([-180, -40, 110, 50])\n * //=Error\n * validateId([-180, -40])\n * //=Error\n * validateId('Foo')\n * //=OK\n * validateId(5)\n * //=OK\n * validateId(null)\n * //=Error\n * validateId(undefined)\n * //=Error\n */\nexport function validateId(id: any): void {\n if (!id) {\n throw new Error(\"id is required\");\n }\n if ([\"string\", \"number\"].indexOf(typeof id) === -1) {\n throw new Error(\"id must be a number or a string\");\n }\n}\n", "import {\n Feature,\n FeatureCollection,\n Geometry,\n LineString,\n MultiPoint,\n MultiLineString,\n MultiPolygon,\n Point,\n Polygon,\n} from \"geojson\";\nimport { isNumber } from \"@turf/helpers\";\n\n/**\n * Unwrap a coordinate from a Point Feature, Geometry or a single coordinate.\n *\n * @function\n * @param {Array|Geometry|Feature} coord GeoJSON Point or an Array of numbers\n * @returns {Array} coordinates\n * @example\n * var pt = turf.point([10, 10]);\n *\n * var coord = turf.getCoord(pt);\n * //= [10, 10]\n */\nfunction getCoord(coord: Feature | Point | number[]): number[] {\n if (!coord) {\n throw new Error(\"coord is required\");\n }\n\n if (!Array.isArray(coord)) {\n if (\n coord.type === \"Feature\" &&\n coord.geometry !== null &&\n coord.geometry.type === \"Point\"\n ) {\n return [...coord.geometry.coordinates];\n }\n if (coord.type === \"Point\") {\n return [...coord.coordinates];\n }\n }\n if (\n Array.isArray(coord) &&\n coord.length >= 2 &&\n !Array.isArray(coord[0]) &&\n !Array.isArray(coord[1])\n ) {\n return [...coord];\n }\n\n throw new Error(\"coord must be GeoJSON Point or an Array of numbers\");\n}\n\n/**\n * Unwrap coordinates from a Feature, Geometry Object or an Array\n *\n * @function\n * @param {Array|Geometry|Feature} coords Feature, Geometry Object or an Array\n * @returns {Array} coordinates\n * @example\n * var poly = turf.polygon([[[119.32, -8.7], [119.55, -8.69], [119.51, -8.54], [119.32, -8.7]]]);\n *\n * var coords = turf.getCoords(poly);\n * //= [[[119.32, -8.7], [119.55, -8.69], [119.51, -8.54], [119.32, -8.7]]]\n */\nfunction getCoords<\n G extends\n | Point\n | LineString\n | Polygon\n | MultiPoint\n | MultiLineString\n | MultiPolygon,\n>(coords: any[] | Feature | G): any[] {\n if (Array.isArray(coords)) {\n return coords;\n }\n\n // Feature\n if (coords.type === \"Feature\") {\n if (coords.geometry !== null) {\n return coords.geometry.coordinates;\n }\n } else {\n // Geometry\n if (coords.coordinates) {\n return coords.coordinates;\n }\n }\n\n throw new Error(\n \"coords must be GeoJSON Feature, Geometry Object or an Array\"\n );\n}\n\n/**\n * Checks if coordinates contains a number\n *\n * @function\n * @param {Array} coordinates GeoJSON Coordinates\n * @returns {boolean} true if Array contains a number\n */\nfunction containsNumber(coordinates: any[]): boolean {\n if (\n coordinates.length > 1 &&\n isNumber(coordinates[0]) &&\n isNumber(coordinates[1])\n ) {\n return true;\n }\n\n if (Array.isArray(coordinates[0]) && coordinates[0].length) {\n return containsNumber(coordinates[0]);\n }\n throw new Error(\"coordinates must only contain numbers\");\n}\n\n/**\n * Enforce expectations about types of GeoJSON objects for Turf.\n *\n * @function\n * @param {GeoJSON} value any GeoJSON object\n * @param {string} type expected GeoJSON type\n * @param {string} name name of calling function\n * @throws {Error} if value is not the expected type.\n */\nfunction geojsonType(value: any, type: string, name: string): void {\n if (!type || !name) {\n throw new Error(\"type and name required\");\n }\n\n if (!value || value.type !== type) {\n throw new Error(\n \"Invalid input to \" +\n name +\n \": must be a \" +\n type +\n \", given \" +\n value.type\n );\n }\n}\n\n/**\n * Enforce expectations about types of {@link Feature} inputs for Turf.\n * Internally this uses {@link geojsonType} to judge geometry types.\n *\n * @function\n * @param {Feature} feature a feature with an expected geometry type\n * @param {string} type expected GeoJSON type\n * @param {string} name name of calling function\n * @throws {Error} error if value is not the expected type.\n */\nfunction featureOf(feature: Feature, type: string, name: string): void {\n if (!feature) {\n throw new Error(\"No feature passed\");\n }\n if (!name) {\n throw new Error(\".featureOf() requires a name\");\n }\n if (!feature || feature.type !== \"Feature\" || !feature.geometry) {\n throw new Error(\n \"Invalid input to \" + name + \", Feature with geometry required\"\n );\n }\n if (!feature.geometry || feature.geometry.type !== type) {\n throw new Error(\n \"Invalid input to \" +\n name +\n \": must be a \" +\n type +\n \", given \" +\n feature.geometry.type\n );\n }\n}\n\n/**\n * Enforce expectations about types of {@link FeatureCollection} inputs for Turf.\n * Internally this uses {@link geojsonType} to judge geometry types.\n *\n * @function\n * @param {FeatureCollection} featureCollection a FeatureCollection for which features will be judged\n * @param {string} type expected GeoJSON type\n * @param {string} name name of calling function\n * @throws {Error} if value is not the expected type.\n */\nfunction collectionOf(\n featureCollection: FeatureCollection,\n type: string,\n name: string\n) {\n if (!featureCollection) {\n throw new Error(\"No featureCollection passed\");\n }\n if (!name) {\n throw new Error(\".collectionOf() requires a name\");\n }\n if (!featureCollection || featureCollection.type !== \"FeatureCollection\") {\n throw new Error(\n \"Invalid input to \" + name + \", FeatureCollection required\"\n );\n }\n for (const feature of featureCollection.features) {\n if (!feature || feature.type !== \"Feature\" || !feature.geometry) {\n throw new Error(\n \"Invalid input to \" + name + \", Feature with geometry required\"\n );\n }\n if (!feature.geometry || feature.geometry.type !== type) {\n throw new Error(\n \"Invalid input to \" +\n name +\n \": must be a \" +\n type +\n \", given \" +\n feature.geometry.type\n );\n }\n }\n}\n\n/**\n * Get Geometry from Feature or Geometry Object\n *\n * @param {Feature|Geometry} geojson GeoJSON Feature or Geometry Object\n * @returns {Geometry|null} GeoJSON Geometry Object\n * @throws {Error} if geojson is not a Feature or Geometry Object\n * @example\n * var point = {\n * \"type\": \"Feature\",\n * \"properties\": {},\n * \"geometry\": {\n * \"type\": \"Point\",\n * \"coordinates\": [110, 40]\n * }\n * }\n * var geom = turf.getGeom(point)\n * //={\"type\": \"Point\", \"coordinates\": [110, 40]}\n */\nfunction getGeom(geojson: Feature | G): G {\n if (geojson.type === \"Feature\") {\n return geojson.geometry;\n }\n return geojson;\n}\n\n/**\n * Get GeoJSON object's type, Geometry type is prioritize.\n *\n * @param {GeoJSON} geojson GeoJSON object\n * @param {string} [name=\"geojson\"] name of the variable to display in error message (unused)\n * @returns {string} GeoJSON type\n * @example\n * var point = {\n * \"type\": \"Feature\",\n * \"properties\": {},\n * \"geometry\": {\n * \"type\": \"Point\",\n * \"coordinates\": [110, 40]\n * }\n * }\n * var geom = turf.getType(point)\n * //=\"Point\"\n */\nfunction getType(\n geojson: Feature | FeatureCollection | Geometry,\n _name?: string\n): string {\n if (geojson.type === \"FeatureCollection\") {\n return \"FeatureCollection\";\n }\n if (geojson.type === \"GeometryCollection\") {\n return \"GeometryCollection\";\n }\n if (geojson.type === \"Feature\" && geojson.geometry !== null) {\n return geojson.geometry.type;\n }\n return geojson.type;\n}\n\nexport {\n getCoord,\n getCoords,\n containsNumber,\n geojsonType,\n featureOf,\n collectionOf,\n getGeom,\n getType,\n};\n// No default export!\n", "import {\n BBox,\n Feature,\n LineString,\n MultiLineString,\n MultiPolygon,\n GeoJsonProperties,\n Polygon,\n} from \"geojson\";\n\nimport {\n lineString,\n multiLineString,\n multiPolygon,\n polygon,\n} from \"@turf/helpers\";\nimport { getGeom } from \"@turf/invariant\";\nimport { lineclip, polygonclip } from \"./lib/lineclip.js\";\n\n/**\n * Takes a {@link Feature} and a bbox and clips the feature to the bbox using\n * [lineclip](https://github.com/mapbox/lineclip).\n * May result in degenerate edges when clipping Polygons.\n *\n * @function\n * @param {Feature} feature feature to clip to the bbox\n * @param {BBox} bbox extent in [minX, minY, maxX, maxY] order\n * @returns {Feature} clipped Feature\n * @example\n * var bbox = [0, 0, 10, 10];\n * var poly = turf.polygon([[[2, 2], [8, 4], [12, 8], [3, 7], [2, 2]]]);\n *\n * var clipped = turf.bboxClip(poly, bbox);\n *\n * //addToMap\n * var addToMap = [bbox, poly, clipped]\n */\nfunction bboxClip<\n G extends Polygon | MultiPolygon | LineString | MultiLineString,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(feature: Feature | G, bbox: BBox) {\n const geom = getGeom(feature);\n const type = geom.type;\n const properties = feature.type === \"Feature\" ? feature.properties : {};\n let coords: any[] = geom.coordinates;\n\n switch (type) {\n case \"LineString\":\n case \"MultiLineString\": {\n const lines: any[] = [];\n if (type === \"LineString\") {\n coords = [coords];\n }\n coords.forEach((line) => {\n lineclip(line, bbox, lines);\n });\n if (lines.length === 1) {\n return lineString(lines[0], properties);\n }\n return multiLineString(lines, properties);\n }\n case \"Polygon\":\n return polygon(clipPolygon(coords, bbox), properties);\n case \"MultiPolygon\":\n return multiPolygon(\n coords.map((poly) => {\n return clipPolygon(poly, bbox);\n }),\n properties\n );\n default:\n throw new Error(\"geometry \" + type + \" not supported\");\n }\n}\n\nfunction clipPolygon(rings: number[][][], bbox: BBox) {\n const outRings = [];\n for (const ring of rings) {\n const clipped = polygonclip(ring, bbox);\n if (clipped.length > 0) {\n if (\n clipped[0][0] !== clipped[clipped.length - 1][0] ||\n clipped[0][1] !== clipped[clipped.length - 1][1]\n ) {\n clipped.push(clipped[0]);\n }\n if (clipped.length >= 4) {\n outRings.push(clipped);\n }\n }\n }\n return outRings;\n}\n\nexport { bboxClip };\nexport default bboxClip;\n", "// Cohen-Sutherland line clipping algorithm, adapted to efficiently\n// handle polylines rather than just segments\nimport { BBox } from \"geojson\";\n\nexport function lineclip(\n points: number[][],\n bbox: BBox,\n result?: number[][][]\n): number[][][] {\n var len = points.length,\n codeA = bitCode(points[0], bbox),\n part = [] as number[][],\n i,\n codeB,\n lastCode;\n let a: number[];\n let b: number[];\n\n if (!result) result = [];\n\n for (i = 1; i < len; i++) {\n a = points[i - 1];\n b = points[i];\n codeB = lastCode = bitCode(b, bbox);\n\n while (true) {\n if (!(codeA | codeB)) {\n // accept\n part.push(a);\n\n if (codeB !== lastCode) {\n // segment went outside\n part.push(b);\n\n if (i < len - 1) {\n // start a new line\n result.push(part);\n part = [];\n }\n } else if (i === len - 1) {\n part.push(b);\n }\n break;\n } else if (codeA & codeB) {\n // trivial reject\n break;\n } else if (codeA) {\n // a outside, intersect with clip edge\n a = intersect(a, b, codeA, bbox)!;\n codeA = bitCode(a, bbox);\n } else {\n // b outside\n b = intersect(a, b, codeB, bbox)!;\n codeB = bitCode(b, bbox);\n }\n }\n\n codeA = lastCode;\n }\n\n if (part.length) result.push(part);\n\n return result;\n}\n\n// Sutherland-Hodgeman polygon clipping algorithm\n\nexport function polygonclip(points: number[][], bbox: BBox): number[][] {\n var result: number[][], edge, prev, prevInside, i, p, inside;\n\n // clip against each side of the clip rectangle\n for (edge = 1; edge <= 8; edge *= 2) {\n result = [];\n prev = points[points.length - 1];\n prevInside = !(bitCode(prev, bbox) & edge);\n\n for (i = 0; i < points.length; i++) {\n p = points[i];\n inside = !(bitCode(p, bbox) & edge);\n\n // if segment goes through the clip window, add an intersection\n if (inside !== prevInside) result.push(intersect(prev, p, edge, bbox)!);\n\n if (inside) result.push(p); // add a point if it's inside\n\n prev = p;\n prevInside = inside;\n }\n\n points = result;\n\n if (!points.length) break;\n }\n\n return result!;\n}\n\n// intersect a segment against one of the 4 lines that make up the bbox\n\nfunction intersect(\n a: number[],\n b: number[],\n edge: number,\n bbox: BBox\n): number[] | null {\n return edge & 8\n ? [a[0] + ((b[0] - a[0]) * (bbox[3] - a[1])) / (b[1] - a[1]), bbox[3]] // top\n : edge & 4\n ? [a[0] + ((b[0] - a[0]) * (bbox[1] - a[1])) / (b[1] - a[1]), bbox[1]] // bottom\n : edge & 2\n ? [bbox[2], a[1] + ((b[1] - a[1]) * (bbox[2] - a[0])) / (b[0] - a[0])] // right\n : edge & 1\n ? [bbox[0], a[1] + ((b[1] - a[1]) * (bbox[0] - a[0])) / (b[0] - a[0])] // left\n : null;\n}\n\n// bit code reflects the point position relative to the bbox:\n\n// left mid right\n// top 1001 1000 1010\n// mid 0001 0000 0010\n// bottom 0101 0100 0110\n\nfunction bitCode(p: number[], bbox: BBox) {\n var code = 0;\n\n if (p[0] < bbox[0]) code |= 1;\n // left\n else if (p[0] > bbox[2]) code |= 2; // right\n\n if (p[1] < bbox[1]) code |= 4;\n // bottom\n else if (p[1] > bbox[3]) code |= 8; // top\n\n return code;\n}\n", "'use strict';\n\nmodule.exports = function (data, opts) {\n if (!opts) opts = {};\n if (typeof opts === 'function') opts = { cmp: opts };\n var cycles = (typeof opts.cycles === 'boolean') ? opts.cycles : false;\n\n var cmp = opts.cmp && (function (f) {\n return function (node) {\n return function (a, b) {\n var aobj = { key: a, value: node[a] };\n var bobj = { key: b, value: node[b] };\n return f(aobj, bobj);\n };\n };\n })(opts.cmp);\n\n var seen = [];\n return (function stringify (node) {\n if (node && node.toJSON && typeof node.toJSON === 'function') {\n node = node.toJSON();\n }\n\n if (node === undefined) return;\n if (typeof node == 'number') return isFinite(node) ? '' + node : 'null';\n if (typeof node !== 'object') return JSON.stringify(node);\n\n var i, out;\n if (Array.isArray(node)) {\n out = '[';\n for (i = 0; i < node.length; i++) {\n if (i) out += ',';\n out += stringify(node[i]) || 'null';\n }\n return out + ']';\n }\n\n if (node === null) return 'null';\n\n if (seen.indexOf(node) !== -1) {\n if (cycles) return JSON.stringify('__cycle__');\n throw new TypeError('Converting circular structure to JSON');\n }\n\n var seenIndex = seen.push(node) - 1;\n var keys = Object.keys(node).sort(cmp && cmp(node));\n out = '';\n for (i = 0; i < keys.length; i++) {\n var key = keys[i];\n var value = stringify(node[key]);\n\n if (!value) continue;\n if (out) out += ',';\n out += JSON.stringify(key) + ':' + value;\n }\n seen.splice(seenIndex, 1);\n return '{' + out + '}';\n })(data);\n};\n", "(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.polygonClipping = factory());\n})(this, (function () { 'use strict';\n\n /**\n * splaytree v3.1.2\n * Fast Splay tree for Node and browser\n *\n * @author Alexander Milevski \n * @license MIT\n * @preserve\n */\n\n /*! *****************************************************************************\r\n Copyright (c) Microsoft Corporation. All rights reserved.\r\n Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use\r\n this file except in compliance with the License. You may obtain a copy of the\r\n License at http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r\n KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED\r\n WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,\r\n MERCHANTABLITY OR NON-INFRINGEMENT.\r\n\r\n See the Apache Version 2.0 License for specific language governing permissions\r\n and limitations under the License.\r\n ***************************************************************************** */\n\n function __generator(thisArg, body) {\n var _ = {\n label: 0,\n sent: function () {\n if (t[0] & 1) throw t[1];\n return t[1];\n },\n trys: [],\n ops: []\n },\n f,\n y,\n t,\n g;\n return g = {\n next: verb(0),\n \"throw\": verb(1),\n \"return\": verb(2)\n }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function () {\n return this;\n }), g;\n function verb(n) {\n return function (v) {\n return step([n, v]);\n };\n }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (_) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0:\n case 1:\n t = op;\n break;\n case 4:\n _.label++;\n return {\n value: op[1],\n done: false\n };\n case 5:\n _.label++;\n y = op[1];\n op = [0];\n continue;\n case 7:\n op = _.ops.pop();\n _.trys.pop();\n continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {\n _ = 0;\n continue;\n }\n if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {\n _.label = op[1];\n break;\n }\n if (op[0] === 6 && _.label < t[1]) {\n _.label = t[1];\n t = op;\n break;\n }\n if (t && _.label < t[2]) {\n _.label = t[2];\n _.ops.push(op);\n break;\n }\n if (t[2]) _.ops.pop();\n _.trys.pop();\n continue;\n }\n op = body.call(thisArg, _);\n } catch (e) {\n op = [6, e];\n y = 0;\n } finally {\n f = t = 0;\n }\n if (op[0] & 5) throw op[1];\n return {\n value: op[0] ? op[1] : void 0,\n done: true\n };\n }\n }\n var Node = /** @class */function () {\n function Node(key, data) {\n this.next = null;\n this.key = key;\n this.data = data;\n this.left = null;\n this.right = null;\n }\n return Node;\n }();\n\n /* follows \"An implementation of top-down splaying\"\r\n * by D. Sleator March 1992\r\n */\n function DEFAULT_COMPARE(a, b) {\n return a > b ? 1 : a < b ? -1 : 0;\n }\n /**\r\n * Simple top down splay, not requiring i to be in the tree t.\r\n */\n function splay(i, t, comparator) {\n var N = new Node(null, null);\n var l = N;\n var r = N;\n while (true) {\n var cmp = comparator(i, t.key);\n //if (i < t.key) {\n if (cmp < 0) {\n if (t.left === null) break;\n //if (i < t.left.key) {\n if (comparator(i, t.left.key) < 0) {\n var y = t.left; /* rotate right */\n t.left = y.right;\n y.right = t;\n t = y;\n if (t.left === null) break;\n }\n r.left = t; /* link right */\n r = t;\n t = t.left;\n //} else if (i > t.key) {\n } else if (cmp > 0) {\n if (t.right === null) break;\n //if (i > t.right.key) {\n if (comparator(i, t.right.key) > 0) {\n var y = t.right; /* rotate left */\n t.right = y.left;\n y.left = t;\n t = y;\n if (t.right === null) break;\n }\n l.right = t; /* link left */\n l = t;\n t = t.right;\n } else break;\n }\n /* assemble */\n l.right = t.left;\n r.left = t.right;\n t.left = N.right;\n t.right = N.left;\n return t;\n }\n function insert(i, data, t, comparator) {\n var node = new Node(i, data);\n if (t === null) {\n node.left = node.right = null;\n return node;\n }\n t = splay(i, t, comparator);\n var cmp = comparator(i, t.key);\n if (cmp < 0) {\n node.left = t.left;\n node.right = t;\n t.left = null;\n } else if (cmp >= 0) {\n node.right = t.right;\n node.left = t;\n t.right = null;\n }\n return node;\n }\n function split(key, v, comparator) {\n var left = null;\n var right = null;\n if (v) {\n v = splay(key, v, comparator);\n var cmp = comparator(v.key, key);\n if (cmp === 0) {\n left = v.left;\n right = v.right;\n } else if (cmp < 0) {\n right = v.right;\n v.right = null;\n left = v;\n } else {\n left = v.left;\n v.left = null;\n right = v;\n }\n }\n return {\n left: left,\n right: right\n };\n }\n function merge(left, right, comparator) {\n if (right === null) return left;\n if (left === null) return right;\n right = splay(left.key, right, comparator);\n right.left = left;\n return right;\n }\n /**\r\n * Prints level of the tree\r\n */\n function printRow(root, prefix, isTail, out, printNode) {\n if (root) {\n out(\"\" + prefix + (isTail ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ') + printNode(root) + \"\\n\");\n var indent = prefix + (isTail ? ' ' : '\u2502 ');\n if (root.left) printRow(root.left, indent, false, out, printNode);\n if (root.right) printRow(root.right, indent, true, out, printNode);\n }\n }\n var Tree = /** @class */function () {\n function Tree(comparator) {\n if (comparator === void 0) {\n comparator = DEFAULT_COMPARE;\n }\n this._root = null;\n this._size = 0;\n this._comparator = comparator;\n }\n /**\r\n * Inserts a key, allows duplicates\r\n */\n Tree.prototype.insert = function (key, data) {\n this._size++;\n return this._root = insert(key, data, this._root, this._comparator);\n };\n /**\r\n * Adds a key, if it is not present in the tree\r\n */\n Tree.prototype.add = function (key, data) {\n var node = new Node(key, data);\n if (this._root === null) {\n node.left = node.right = null;\n this._size++;\n this._root = node;\n }\n var comparator = this._comparator;\n var t = splay(key, this._root, comparator);\n var cmp = comparator(key, t.key);\n if (cmp === 0) this._root = t;else {\n if (cmp < 0) {\n node.left = t.left;\n node.right = t;\n t.left = null;\n } else if (cmp > 0) {\n node.right = t.right;\n node.left = t;\n t.right = null;\n }\n this._size++;\n this._root = node;\n }\n return this._root;\n };\n /**\r\n * @param {Key} key\r\n * @return {Node|null}\r\n */\n Tree.prototype.remove = function (key) {\n this._root = this._remove(key, this._root, this._comparator);\n };\n /**\r\n * Deletes i from the tree if it's there\r\n */\n Tree.prototype._remove = function (i, t, comparator) {\n var x;\n if (t === null) return null;\n t = splay(i, t, comparator);\n var cmp = comparator(i, t.key);\n if (cmp === 0) {\n /* found it */\n if (t.left === null) {\n x = t.right;\n } else {\n x = splay(i, t.left, comparator);\n x.right = t.right;\n }\n this._size--;\n return x;\n }\n return t; /* It wasn't there */\n };\n /**\r\n * Removes and returns the node with smallest key\r\n */\n Tree.prototype.pop = function () {\n var node = this._root;\n if (node) {\n while (node.left) node = node.left;\n this._root = splay(node.key, this._root, this._comparator);\n this._root = this._remove(node.key, this._root, this._comparator);\n return {\n key: node.key,\n data: node.data\n };\n }\n return null;\n };\n /**\r\n * Find without splaying\r\n */\n Tree.prototype.findStatic = function (key) {\n var current = this._root;\n var compare = this._comparator;\n while (current) {\n var cmp = compare(key, current.key);\n if (cmp === 0) return current;else if (cmp < 0) current = current.left;else current = current.right;\n }\n return null;\n };\n Tree.prototype.find = function (key) {\n if (this._root) {\n this._root = splay(key, this._root, this._comparator);\n if (this._comparator(key, this._root.key) !== 0) return null;\n }\n return this._root;\n };\n Tree.prototype.contains = function (key) {\n var current = this._root;\n var compare = this._comparator;\n while (current) {\n var cmp = compare(key, current.key);\n if (cmp === 0) return true;else if (cmp < 0) current = current.left;else current = current.right;\n }\n return false;\n };\n Tree.prototype.forEach = function (visitor, ctx) {\n var current = this._root;\n var Q = []; /* Initialize stack s */\n var done = false;\n while (!done) {\n if (current !== null) {\n Q.push(current);\n current = current.left;\n } else {\n if (Q.length !== 0) {\n current = Q.pop();\n visitor.call(ctx, current);\n current = current.right;\n } else done = true;\n }\n }\n return this;\n };\n /**\r\n * Walk key range from `low` to `high`. Stops if `fn` returns a value.\r\n */\n Tree.prototype.range = function (low, high, fn, ctx) {\n var Q = [];\n var compare = this._comparator;\n var node = this._root;\n var cmp;\n while (Q.length !== 0 || node) {\n if (node) {\n Q.push(node);\n node = node.left;\n } else {\n node = Q.pop();\n cmp = compare(node.key, high);\n if (cmp > 0) {\n break;\n } else if (compare(node.key, low) >= 0) {\n if (fn.call(ctx, node)) return this; // stop if smth is returned\n }\n node = node.right;\n }\n }\n return this;\n };\n /**\r\n * Returns array of keys\r\n */\n Tree.prototype.keys = function () {\n var keys = [];\n this.forEach(function (_a) {\n var key = _a.key;\n return keys.push(key);\n });\n return keys;\n };\n /**\r\n * Returns array of all the data in the nodes\r\n */\n Tree.prototype.values = function () {\n var values = [];\n this.forEach(function (_a) {\n var data = _a.data;\n return values.push(data);\n });\n return values;\n };\n Tree.prototype.min = function () {\n if (this._root) return this.minNode(this._root).key;\n return null;\n };\n Tree.prototype.max = function () {\n if (this._root) return this.maxNode(this._root).key;\n return null;\n };\n Tree.prototype.minNode = function (t) {\n if (t === void 0) {\n t = this._root;\n }\n if (t) while (t.left) t = t.left;\n return t;\n };\n Tree.prototype.maxNode = function (t) {\n if (t === void 0) {\n t = this._root;\n }\n if (t) while (t.right) t = t.right;\n return t;\n };\n /**\r\n * Returns node at given index\r\n */\n Tree.prototype.at = function (index) {\n var current = this._root;\n var done = false;\n var i = 0;\n var Q = [];\n while (!done) {\n if (current) {\n Q.push(current);\n current = current.left;\n } else {\n if (Q.length > 0) {\n current = Q.pop();\n if (i === index) return current;\n i++;\n current = current.right;\n } else done = true;\n }\n }\n return null;\n };\n Tree.prototype.next = function (d) {\n var root = this._root;\n var successor = null;\n if (d.right) {\n successor = d.right;\n while (successor.left) successor = successor.left;\n return successor;\n }\n var comparator = this._comparator;\n while (root) {\n var cmp = comparator(d.key, root.key);\n if (cmp === 0) break;else if (cmp < 0) {\n successor = root;\n root = root.left;\n } else root = root.right;\n }\n return successor;\n };\n Tree.prototype.prev = function (d) {\n var root = this._root;\n var predecessor = null;\n if (d.left !== null) {\n predecessor = d.left;\n while (predecessor.right) predecessor = predecessor.right;\n return predecessor;\n }\n var comparator = this._comparator;\n while (root) {\n var cmp = comparator(d.key, root.key);\n if (cmp === 0) break;else if (cmp < 0) root = root.left;else {\n predecessor = root;\n root = root.right;\n }\n }\n return predecessor;\n };\n Tree.prototype.clear = function () {\n this._root = null;\n this._size = 0;\n return this;\n };\n Tree.prototype.toList = function () {\n return toList(this._root);\n };\n /**\r\n * Bulk-load items. Both array have to be same size\r\n */\n Tree.prototype.load = function (keys, values, presort) {\n if (values === void 0) {\n values = [];\n }\n if (presort === void 0) {\n presort = false;\n }\n var size = keys.length;\n var comparator = this._comparator;\n // sort if needed\n if (presort) sort(keys, values, 0, size - 1, comparator);\n if (this._root === null) {\n // empty tree\n this._root = loadRecursive(keys, values, 0, size);\n this._size = size;\n } else {\n // that re-builds the whole tree from two in-order traversals\n var mergedList = mergeLists(this.toList(), createList(keys, values), comparator);\n size = this._size + size;\n this._root = sortedListToBST({\n head: mergedList\n }, 0, size);\n }\n return this;\n };\n Tree.prototype.isEmpty = function () {\n return this._root === null;\n };\n Object.defineProperty(Tree.prototype, \"size\", {\n get: function () {\n return this._size;\n },\n enumerable: true,\n configurable: true\n });\n Object.defineProperty(Tree.prototype, \"root\", {\n get: function () {\n return this._root;\n },\n enumerable: true,\n configurable: true\n });\n Tree.prototype.toString = function (printNode) {\n if (printNode === void 0) {\n printNode = function (n) {\n return String(n.key);\n };\n }\n var out = [];\n printRow(this._root, '', true, function (v) {\n return out.push(v);\n }, printNode);\n return out.join('');\n };\n Tree.prototype.update = function (key, newKey, newData) {\n var comparator = this._comparator;\n var _a = split(key, this._root, comparator),\n left = _a.left,\n right = _a.right;\n if (comparator(key, newKey) < 0) {\n right = insert(newKey, newData, right, comparator);\n } else {\n left = insert(newKey, newData, left, comparator);\n }\n this._root = merge(left, right, comparator);\n };\n Tree.prototype.split = function (key) {\n return split(key, this._root, this._comparator);\n };\n Tree.prototype[Symbol.iterator] = function () {\n var current, Q, done;\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0:\n current = this._root;\n Q = [];\n done = false;\n _a.label = 1;\n case 1:\n if (!!done) return [3 /*break*/, 6];\n if (!(current !== null)) return [3 /*break*/, 2];\n Q.push(current);\n current = current.left;\n return [3 /*break*/, 5];\n case 2:\n if (!(Q.length !== 0)) return [3 /*break*/, 4];\n current = Q.pop();\n return [4 /*yield*/, current];\n case 3:\n _a.sent();\n current = current.right;\n return [3 /*break*/, 5];\n case 4:\n done = true;\n _a.label = 5;\n case 5:\n return [3 /*break*/, 1];\n case 6:\n return [2 /*return*/];\n }\n });\n };\n return Tree;\n }();\n function loadRecursive(keys, values, start, end) {\n var size = end - start;\n if (size > 0) {\n var middle = start + Math.floor(size / 2);\n var key = keys[middle];\n var data = values[middle];\n var node = new Node(key, data);\n node.left = loadRecursive(keys, values, start, middle);\n node.right = loadRecursive(keys, values, middle + 1, end);\n return node;\n }\n return null;\n }\n function createList(keys, values) {\n var head = new Node(null, null);\n var p = head;\n for (var i = 0; i < keys.length; i++) {\n p = p.next = new Node(keys[i], values[i]);\n }\n p.next = null;\n return head.next;\n }\n function toList(root) {\n var current = root;\n var Q = [];\n var done = false;\n var head = new Node(null, null);\n var p = head;\n while (!done) {\n if (current) {\n Q.push(current);\n current = current.left;\n } else {\n if (Q.length > 0) {\n current = p = p.next = Q.pop();\n current = current.right;\n } else done = true;\n }\n }\n p.next = null; // that'll work even if the tree was empty\n return head.next;\n }\n function sortedListToBST(list, start, end) {\n var size = end - start;\n if (size > 0) {\n var middle = start + Math.floor(size / 2);\n var left = sortedListToBST(list, start, middle);\n var root = list.head;\n root.left = left;\n list.head = list.head.next;\n root.right = sortedListToBST(list, middle + 1, end);\n return root;\n }\n return null;\n }\n function mergeLists(l1, l2, compare) {\n var head = new Node(null, null); // dummy\n var p = head;\n var p1 = l1;\n var p2 = l2;\n while (p1 !== null && p2 !== null) {\n if (compare(p1.key, p2.key) < 0) {\n p.next = p1;\n p1 = p1.next;\n } else {\n p.next = p2;\n p2 = p2.next;\n }\n p = p.next;\n }\n if (p1 !== null) {\n p.next = p1;\n } else if (p2 !== null) {\n p.next = p2;\n }\n return head.next;\n }\n function sort(keys, values, left, right, compare) {\n if (left >= right) return;\n var pivot = keys[left + right >> 1];\n var i = left - 1;\n var j = right + 1;\n while (true) {\n do i++; while (compare(keys[i], pivot) < 0);\n do j--; while (compare(keys[j], pivot) > 0);\n if (i >= j) break;\n var tmp = keys[i];\n keys[i] = keys[j];\n keys[j] = tmp;\n tmp = values[i];\n values[i] = values[j];\n values[j] = tmp;\n }\n sort(keys, values, left, j, compare);\n sort(keys, values, j + 1, right, compare);\n }\n\n /**\n * A bounding box has the format:\n *\n * { ll: { x: xmin, y: ymin }, ur: { x: xmax, y: ymax } }\n *\n */\n\n const isInBbox = (bbox, point) => {\n return bbox.ll.x <= point.x && point.x <= bbox.ur.x && bbox.ll.y <= point.y && point.y <= bbox.ur.y;\n };\n\n /* Returns either null, or a bbox (aka an ordered pair of points)\n * If there is only one point of overlap, a bbox with identical points\n * will be returned */\n const getBboxOverlap = (b1, b2) => {\n // check if the bboxes overlap at all\n if (b2.ur.x < b1.ll.x || b1.ur.x < b2.ll.x || b2.ur.y < b1.ll.y || b1.ur.y < b2.ll.y) return null;\n\n // find the middle two X values\n const lowerX = b1.ll.x < b2.ll.x ? b2.ll.x : b1.ll.x;\n const upperX = b1.ur.x < b2.ur.x ? b1.ur.x : b2.ur.x;\n\n // find the middle two Y values\n const lowerY = b1.ll.y < b2.ll.y ? b2.ll.y : b1.ll.y;\n const upperY = b1.ur.y < b2.ur.y ? b1.ur.y : b2.ur.y;\n\n // put those middle values together to get the overlap\n return {\n ll: {\n x: lowerX,\n y: lowerY\n },\n ur: {\n x: upperX,\n y: upperY\n }\n };\n };\n\n /* Javascript doesn't do integer math. Everything is\n * floating point with percision Number.EPSILON.\n *\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON\n */\n\n let epsilon$1 = Number.EPSILON;\n\n // IE Polyfill\n if (epsilon$1 === undefined) epsilon$1 = Math.pow(2, -52);\n const EPSILON_SQ = epsilon$1 * epsilon$1;\n\n /* FLP comparator */\n const cmp = (a, b) => {\n // check if they're both 0\n if (-epsilon$1 < a && a < epsilon$1) {\n if (-epsilon$1 < b && b < epsilon$1) {\n return 0;\n }\n }\n\n // check if they're flp equal\n const ab = a - b;\n if (ab * ab < EPSILON_SQ * a * b) {\n return 0;\n }\n\n // normal comparison\n return a < b ? -1 : 1;\n };\n\n /**\n * This class rounds incoming values sufficiently so that\n * floating points problems are, for the most part, avoided.\n *\n * Incoming points are have their x & y values tested against\n * all previously seen x & y values. If either is 'too close'\n * to a previously seen value, it's value is 'snapped' to the\n * previously seen value.\n *\n * All points should be rounded by this class before being\n * stored in any data structures in the rest of this algorithm.\n */\n\n class PtRounder {\n constructor() {\n this.reset();\n }\n reset() {\n this.xRounder = new CoordRounder();\n this.yRounder = new CoordRounder();\n }\n round(x, y) {\n return {\n x: this.xRounder.round(x),\n y: this.yRounder.round(y)\n };\n }\n }\n class CoordRounder {\n constructor() {\n this.tree = new Tree();\n // preseed with 0 so we don't end up with values < Number.EPSILON\n this.round(0);\n }\n\n // Note: this can rounds input values backwards or forwards.\n // You might ask, why not restrict this to just rounding\n // forwards? Wouldn't that allow left endpoints to always\n // remain left endpoints during splitting (never change to\n // right). No - it wouldn't, because we snap intersections\n // to endpoints (to establish independence from the segment\n // angle for t-intersections).\n round(coord) {\n const node = this.tree.add(coord);\n const prevNode = this.tree.prev(node);\n if (prevNode !== null && cmp(node.key, prevNode.key) === 0) {\n this.tree.remove(coord);\n return prevNode.key;\n }\n const nextNode = this.tree.next(node);\n if (nextNode !== null && cmp(node.key, nextNode.key) === 0) {\n this.tree.remove(coord);\n return nextNode.key;\n }\n return coord;\n }\n }\n\n // singleton available by import\n const rounder = new PtRounder();\n\n const epsilon = 1.1102230246251565e-16;\n const splitter = 134217729;\n const resulterrbound = (3 + 8 * epsilon) * epsilon;\n\n // fast_expansion_sum_zeroelim routine from oritinal code\n function sum(elen, e, flen, f, h) {\n let Q, Qnew, hh, bvirt;\n let enow = e[0];\n let fnow = f[0];\n let eindex = 0;\n let findex = 0;\n if (fnow > enow === fnow > -enow) {\n Q = enow;\n enow = e[++eindex];\n } else {\n Q = fnow;\n fnow = f[++findex];\n }\n let hindex = 0;\n if (eindex < elen && findex < flen) {\n if (fnow > enow === fnow > -enow) {\n Qnew = enow + Q;\n hh = Q - (Qnew - enow);\n enow = e[++eindex];\n } else {\n Qnew = fnow + Q;\n hh = Q - (Qnew - fnow);\n fnow = f[++findex];\n }\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n while (eindex < elen && findex < flen) {\n if (fnow > enow === fnow > -enow) {\n Qnew = Q + enow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (enow - bvirt);\n enow = e[++eindex];\n } else {\n Qnew = Q + fnow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (fnow - bvirt);\n fnow = f[++findex];\n }\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n }\n }\n while (eindex < elen) {\n Qnew = Q + enow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (enow - bvirt);\n enow = e[++eindex];\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n }\n while (findex < flen) {\n Qnew = Q + fnow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (fnow - bvirt);\n fnow = f[++findex];\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n }\n if (Q !== 0 || hindex === 0) {\n h[hindex++] = Q;\n }\n return hindex;\n }\n function estimate(elen, e) {\n let Q = e[0];\n for (let i = 1; i < elen; i++) Q += e[i];\n return Q;\n }\n function vec(n) {\n return new Float64Array(n);\n }\n\n const ccwerrboundA = (3 + 16 * epsilon) * epsilon;\n const ccwerrboundB = (2 + 12 * epsilon) * epsilon;\n const ccwerrboundC = (9 + 64 * epsilon) * epsilon * epsilon;\n const B = vec(4);\n const C1 = vec(8);\n const C2 = vec(12);\n const D = vec(16);\n const u = vec(4);\n function orient2dadapt(ax, ay, bx, by, cx, cy, detsum) {\n let acxtail, acytail, bcxtail, bcytail;\n let bvirt, c, ahi, alo, bhi, blo, _i, _j, _0, s1, s0, t1, t0, u3;\n const acx = ax - cx;\n const bcx = bx - cx;\n const acy = ay - cy;\n const bcy = by - cy;\n s1 = acx * bcy;\n c = splitter * acx;\n ahi = c - (c - acx);\n alo = acx - ahi;\n c = splitter * bcy;\n bhi = c - (c - bcy);\n blo = bcy - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acy * bcx;\n c = splitter * acy;\n ahi = c - (c - acy);\n alo = acy - ahi;\n c = splitter * bcx;\n bhi = c - (c - bcx);\n blo = bcx - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n B[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n B[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n B[2] = _j - (u3 - bvirt) + (_i - bvirt);\n B[3] = u3;\n let det = estimate(4, B);\n let errbound = ccwerrboundB * detsum;\n if (det >= errbound || -det >= errbound) {\n return det;\n }\n bvirt = ax - acx;\n acxtail = ax - (acx + bvirt) + (bvirt - cx);\n bvirt = bx - bcx;\n bcxtail = bx - (bcx + bvirt) + (bvirt - cx);\n bvirt = ay - acy;\n acytail = ay - (acy + bvirt) + (bvirt - cy);\n bvirt = by - bcy;\n bcytail = by - (bcy + bvirt) + (bvirt - cy);\n if (acxtail === 0 && acytail === 0 && bcxtail === 0 && bcytail === 0) {\n return det;\n }\n errbound = ccwerrboundC * detsum + resulterrbound * Math.abs(det);\n det += acx * bcytail + bcy * acxtail - (acy * bcxtail + bcx * acytail);\n if (det >= errbound || -det >= errbound) return det;\n s1 = acxtail * bcy;\n c = splitter * acxtail;\n ahi = c - (c - acxtail);\n alo = acxtail - ahi;\n c = splitter * bcy;\n bhi = c - (c - bcy);\n blo = bcy - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acytail * bcx;\n c = splitter * acytail;\n ahi = c - (c - acytail);\n alo = acytail - ahi;\n c = splitter * bcx;\n bhi = c - (c - bcx);\n blo = bcx - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n u[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n u[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n u[2] = _j - (u3 - bvirt) + (_i - bvirt);\n u[3] = u3;\n const C1len = sum(4, B, 4, u, C1);\n s1 = acx * bcytail;\n c = splitter * acx;\n ahi = c - (c - acx);\n alo = acx - ahi;\n c = splitter * bcytail;\n bhi = c - (c - bcytail);\n blo = bcytail - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acy * bcxtail;\n c = splitter * acy;\n ahi = c - (c - acy);\n alo = acy - ahi;\n c = splitter * bcxtail;\n bhi = c - (c - bcxtail);\n blo = bcxtail - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n u[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n u[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n u[2] = _j - (u3 - bvirt) + (_i - bvirt);\n u[3] = u3;\n const C2len = sum(C1len, C1, 4, u, C2);\n s1 = acxtail * bcytail;\n c = splitter * acxtail;\n ahi = c - (c - acxtail);\n alo = acxtail - ahi;\n c = splitter * bcytail;\n bhi = c - (c - bcytail);\n blo = bcytail - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acytail * bcxtail;\n c = splitter * acytail;\n ahi = c - (c - acytail);\n alo = acytail - ahi;\n c = splitter * bcxtail;\n bhi = c - (c - bcxtail);\n blo = bcxtail - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n u[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n u[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n u[2] = _j - (u3 - bvirt) + (_i - bvirt);\n u[3] = u3;\n const Dlen = sum(C2len, C2, 4, u, D);\n return D[Dlen - 1];\n }\n function orient2d(ax, ay, bx, by, cx, cy) {\n const detleft = (ay - cy) * (bx - cx);\n const detright = (ax - cx) * (by - cy);\n const det = detleft - detright;\n const detsum = Math.abs(detleft + detright);\n if (Math.abs(det) >= ccwerrboundA * detsum) return det;\n return -orient2dadapt(ax, ay, bx, by, cx, cy, detsum);\n }\n\n /* Cross Product of two vectors with first point at origin */\n const crossProduct = (a, b) => a.x * b.y - a.y * b.x;\n\n /* Dot Product of two vectors with first point at origin */\n const dotProduct = (a, b) => a.x * b.x + a.y * b.y;\n\n /* Comparator for two vectors with same starting point */\n const compareVectorAngles = (basePt, endPt1, endPt2) => {\n const res = orient2d(basePt.x, basePt.y, endPt1.x, endPt1.y, endPt2.x, endPt2.y);\n if (res > 0) return -1;\n if (res < 0) return 1;\n return 0;\n };\n const length = v => Math.sqrt(dotProduct(v, v));\n\n /* Get the sine of the angle from pShared -> pAngle to pShaed -> pBase */\n const sineOfAngle = (pShared, pBase, pAngle) => {\n const vBase = {\n x: pBase.x - pShared.x,\n y: pBase.y - pShared.y\n };\n const vAngle = {\n x: pAngle.x - pShared.x,\n y: pAngle.y - pShared.y\n };\n return crossProduct(vAngle, vBase) / length(vAngle) / length(vBase);\n };\n\n /* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */\n const cosineOfAngle = (pShared, pBase, pAngle) => {\n const vBase = {\n x: pBase.x - pShared.x,\n y: pBase.y - pShared.y\n };\n const vAngle = {\n x: pAngle.x - pShared.x,\n y: pAngle.y - pShared.y\n };\n return dotProduct(vAngle, vBase) / length(vAngle) / length(vBase);\n };\n\n /* Get the x coordinate where the given line (defined by a point and vector)\n * crosses the horizontal line with the given y coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\n const horizontalIntersection = (pt, v, y) => {\n if (v.y === 0) return null;\n return {\n x: pt.x + v.x / v.y * (y - pt.y),\n y: y\n };\n };\n\n /* Get the y coordinate where the given line (defined by a point and vector)\n * crosses the vertical line with the given x coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\n const verticalIntersection = (pt, v, x) => {\n if (v.x === 0) return null;\n return {\n x: x,\n y: pt.y + v.y / v.x * (x - pt.x)\n };\n };\n\n /* Get the intersection of two lines, each defined by a base point and a vector.\n * In the case of parrallel lines (including overlapping ones) returns null. */\n const intersection$1 = (pt1, v1, pt2, v2) => {\n // take some shortcuts for vertical and horizontal lines\n // this also ensures we don't calculate an intersection and then discover\n // it's actually outside the bounding box of the line\n if (v1.x === 0) return verticalIntersection(pt2, v2, pt1.x);\n if (v2.x === 0) return verticalIntersection(pt1, v1, pt2.x);\n if (v1.y === 0) return horizontalIntersection(pt2, v2, pt1.y);\n if (v2.y === 0) return horizontalIntersection(pt1, v1, pt2.y);\n\n // General case for non-overlapping segments.\n // This algorithm is based on Schneider and Eberly.\n // http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf - pg 244\n\n const kross = crossProduct(v1, v2);\n if (kross == 0) return null;\n const ve = {\n x: pt2.x - pt1.x,\n y: pt2.y - pt1.y\n };\n const d1 = crossProduct(ve, v1) / kross;\n const d2 = crossProduct(ve, v2) / kross;\n\n // take the average of the two calculations to minimize rounding error\n const x1 = pt1.x + d2 * v1.x,\n x2 = pt2.x + d1 * v2.x;\n const y1 = pt1.y + d2 * v1.y,\n y2 = pt2.y + d1 * v2.y;\n const x = (x1 + x2) / 2;\n const y = (y1 + y2) / 2;\n return {\n x: x,\n y: y\n };\n };\n\n class SweepEvent {\n // for ordering sweep events in the sweep event queue\n static compare(a, b) {\n // favor event with a point that the sweep line hits first\n const ptCmp = SweepEvent.comparePoints(a.point, b.point);\n if (ptCmp !== 0) return ptCmp;\n\n // the points are the same, so link them if needed\n if (a.point !== b.point) a.link(b);\n\n // favor right events over left\n if (a.isLeft !== b.isLeft) return a.isLeft ? 1 : -1;\n\n // we have two matching left or right endpoints\n // ordering of this case is the same as for their segments\n return Segment.compare(a.segment, b.segment);\n }\n\n // for ordering points in sweep line order\n static comparePoints(aPt, bPt) {\n if (aPt.x < bPt.x) return -1;\n if (aPt.x > bPt.x) return 1;\n if (aPt.y < bPt.y) return -1;\n if (aPt.y > bPt.y) return 1;\n return 0;\n }\n\n // Warning: 'point' input will be modified and re-used (for performance)\n constructor(point, isLeft) {\n if (point.events === undefined) point.events = [this];else point.events.push(this);\n this.point = point;\n this.isLeft = isLeft;\n // this.segment, this.otherSE set by factory\n }\n link(other) {\n if (other.point === this.point) {\n throw new Error(\"Tried to link already linked events\");\n }\n const otherEvents = other.point.events;\n for (let i = 0, iMax = otherEvents.length; i < iMax; i++) {\n const evt = otherEvents[i];\n this.point.events.push(evt);\n evt.point = this.point;\n }\n this.checkForConsuming();\n }\n\n /* Do a pass over our linked events and check to see if any pair\n * of segments match, and should be consumed. */\n checkForConsuming() {\n // FIXME: The loops in this method run O(n^2) => no good.\n // Maintain little ordered sweep event trees?\n // Can we maintaining an ordering that avoids the need\n // for the re-sorting with getLeftmostComparator in geom-out?\n\n // Compare each pair of events to see if other events also match\n const numEvents = this.point.events.length;\n for (let i = 0; i < numEvents; i++) {\n const evt1 = this.point.events[i];\n if (evt1.segment.consumedBy !== undefined) continue;\n for (let j = i + 1; j < numEvents; j++) {\n const evt2 = this.point.events[j];\n if (evt2.consumedBy !== undefined) continue;\n if (evt1.otherSE.point.events !== evt2.otherSE.point.events) continue;\n evt1.segment.consume(evt2.segment);\n }\n }\n }\n getAvailableLinkedEvents() {\n // point.events is always of length 2 or greater\n const events = [];\n for (let i = 0, iMax = this.point.events.length; i < iMax; i++) {\n const evt = this.point.events[i];\n if (evt !== this && !evt.segment.ringOut && evt.segment.isInResult()) {\n events.push(evt);\n }\n }\n return events;\n }\n\n /**\n * Returns a comparator function for sorting linked events that will\n * favor the event that will give us the smallest left-side angle.\n * All ring construction starts as low as possible heading to the right,\n * so by always turning left as sharp as possible we'll get polygons\n * without uncessary loops & holes.\n *\n * The comparator function has a compute cache such that it avoids\n * re-computing already-computed values.\n */\n getLeftmostComparator(baseEvent) {\n const cache = new Map();\n const fillCache = linkedEvent => {\n const nextEvent = linkedEvent.otherSE;\n cache.set(linkedEvent, {\n sine: sineOfAngle(this.point, baseEvent.point, nextEvent.point),\n cosine: cosineOfAngle(this.point, baseEvent.point, nextEvent.point)\n });\n };\n return (a, b) => {\n if (!cache.has(a)) fillCache(a);\n if (!cache.has(b)) fillCache(b);\n const {\n sine: asine,\n cosine: acosine\n } = cache.get(a);\n const {\n sine: bsine,\n cosine: bcosine\n } = cache.get(b);\n\n // both on or above x-axis\n if (asine >= 0 && bsine >= 0) {\n if (acosine < bcosine) return 1;\n if (acosine > bcosine) return -1;\n return 0;\n }\n\n // both below x-axis\n if (asine < 0 && bsine < 0) {\n if (acosine < bcosine) return -1;\n if (acosine > bcosine) return 1;\n return 0;\n }\n\n // one above x-axis, one below\n if (bsine < asine) return -1;\n if (bsine > asine) return 1;\n return 0;\n };\n }\n }\n\n // Give segments unique ID's to get consistent sorting of\n // segments and sweep events when all else is identical\n let segmentId = 0;\n class Segment {\n /* This compare() function is for ordering segments in the sweep\n * line tree, and does so according to the following criteria:\n *\n * Consider the vertical line that lies an infinestimal step to the\n * right of the right-more of the two left endpoints of the input\n * segments. Imagine slowly moving a point up from negative infinity\n * in the increasing y direction. Which of the two segments will that\n * point intersect first? That segment comes 'before' the other one.\n *\n * If neither segment would be intersected by such a line, (if one\n * or more of the segments are vertical) then the line to be considered\n * is directly on the right-more of the two left inputs.\n */\n static compare(a, b) {\n const alx = a.leftSE.point.x;\n const blx = b.leftSE.point.x;\n const arx = a.rightSE.point.x;\n const brx = b.rightSE.point.x;\n\n // check if they're even in the same vertical plane\n if (brx < alx) return 1;\n if (arx < blx) return -1;\n const aly = a.leftSE.point.y;\n const bly = b.leftSE.point.y;\n const ary = a.rightSE.point.y;\n const bry = b.rightSE.point.y;\n\n // is left endpoint of segment B the right-more?\n if (alx < blx) {\n // are the two segments in the same horizontal plane?\n if (bly < aly && bly < ary) return 1;\n if (bly > aly && bly > ary) return -1;\n\n // is the B left endpoint colinear to segment A?\n const aCmpBLeft = a.comparePoint(b.leftSE.point);\n if (aCmpBLeft < 0) return 1;\n if (aCmpBLeft > 0) return -1;\n\n // is the A right endpoint colinear to segment B ?\n const bCmpARight = b.comparePoint(a.rightSE.point);\n if (bCmpARight !== 0) return bCmpARight;\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return -1;\n }\n\n // is left endpoint of segment A the right-more?\n if (alx > blx) {\n if (aly < bly && aly < bry) return -1;\n if (aly > bly && aly > bry) return 1;\n\n // is the A left endpoint colinear to segment B?\n const bCmpALeft = b.comparePoint(a.leftSE.point);\n if (bCmpALeft !== 0) return bCmpALeft;\n\n // is the B right endpoint colinear to segment A?\n const aCmpBRight = a.comparePoint(b.rightSE.point);\n if (aCmpBRight < 0) return 1;\n if (aCmpBRight > 0) return -1;\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return 1;\n }\n\n // if we get here, the two left endpoints are in the same\n // vertical plane, ie alx === blx\n\n // consider the lower left-endpoint to come first\n if (aly < bly) return -1;\n if (aly > bly) return 1;\n\n // left endpoints are identical\n // check for colinearity by using the left-more right endpoint\n\n // is the A right endpoint more left-more?\n if (arx < brx) {\n const bCmpARight = b.comparePoint(a.rightSE.point);\n if (bCmpARight !== 0) return bCmpARight;\n }\n\n // is the B right endpoint more left-more?\n if (arx > brx) {\n const aCmpBRight = a.comparePoint(b.rightSE.point);\n if (aCmpBRight < 0) return 1;\n if (aCmpBRight > 0) return -1;\n }\n if (arx !== brx) {\n // are these two [almost] vertical segments with opposite orientation?\n // if so, the one with the lower right endpoint comes first\n const ay = ary - aly;\n const ax = arx - alx;\n const by = bry - bly;\n const bx = brx - blx;\n if (ay > ax && by < bx) return 1;\n if (ay < ax && by > bx) return -1;\n }\n\n // we have colinear segments with matching orientation\n // consider the one with more left-more right endpoint to be first\n if (arx > brx) return 1;\n if (arx < brx) return -1;\n\n // if we get here, two two right endpoints are in the same\n // vertical plane, ie arx === brx\n\n // consider the lower right-endpoint to come first\n if (ary < bry) return -1;\n if (ary > bry) return 1;\n\n // right endpoints identical as well, so the segments are idential\n // fall back on creation order as consistent tie-breaker\n if (a.id < b.id) return -1;\n if (a.id > b.id) return 1;\n\n // identical segment, ie a === b\n return 0;\n }\n\n /* Warning: a reference to ringWindings input will be stored,\n * and possibly will be later modified */\n constructor(leftSE, rightSE, rings, windings) {\n this.id = ++segmentId;\n this.leftSE = leftSE;\n leftSE.segment = this;\n leftSE.otherSE = rightSE;\n this.rightSE = rightSE;\n rightSE.segment = this;\n rightSE.otherSE = leftSE;\n this.rings = rings;\n this.windings = windings;\n // left unset for performance, set later in algorithm\n // this.ringOut, this.consumedBy, this.prev\n }\n static fromRing(pt1, pt2, ring) {\n let leftPt, rightPt, winding;\n\n // ordering the two points according to sweep line ordering\n const cmpPts = SweepEvent.comparePoints(pt1, pt2);\n if (cmpPts < 0) {\n leftPt = pt1;\n rightPt = pt2;\n winding = 1;\n } else if (cmpPts > 0) {\n leftPt = pt2;\n rightPt = pt1;\n winding = -1;\n } else throw new Error(`Tried to create degenerate segment at [${pt1.x}, ${pt1.y}]`);\n const leftSE = new SweepEvent(leftPt, true);\n const rightSE = new SweepEvent(rightPt, false);\n return new Segment(leftSE, rightSE, [ring], [winding]);\n }\n\n /* When a segment is split, the rightSE is replaced with a new sweep event */\n replaceRightSE(newRightSE) {\n this.rightSE = newRightSE;\n this.rightSE.segment = this;\n this.rightSE.otherSE = this.leftSE;\n this.leftSE.otherSE = this.rightSE;\n }\n bbox() {\n const y1 = this.leftSE.point.y;\n const y2 = this.rightSE.point.y;\n return {\n ll: {\n x: this.leftSE.point.x,\n y: y1 < y2 ? y1 : y2\n },\n ur: {\n x: this.rightSE.point.x,\n y: y1 > y2 ? y1 : y2\n }\n };\n }\n\n /* A vector from the left point to the right */\n vector() {\n return {\n x: this.rightSE.point.x - this.leftSE.point.x,\n y: this.rightSE.point.y - this.leftSE.point.y\n };\n }\n isAnEndpoint(pt) {\n return pt.x === this.leftSE.point.x && pt.y === this.leftSE.point.y || pt.x === this.rightSE.point.x && pt.y === this.rightSE.point.y;\n }\n\n /* Compare this segment with a point.\n *\n * A point P is considered to be colinear to a segment if there\n * exists a distance D such that if we travel along the segment\n * from one * endpoint towards the other a distance D, we find\n * ourselves at point P.\n *\n * Return value indicates:\n *\n * 1: point lies above the segment (to the left of vertical)\n * 0: point is colinear to segment\n * -1: point lies below the segment (to the right of vertical)\n */\n comparePoint(point) {\n if (this.isAnEndpoint(point)) return 0;\n const lPt = this.leftSE.point;\n const rPt = this.rightSE.point;\n const v = this.vector();\n\n // Exactly vertical segments.\n if (lPt.x === rPt.x) {\n if (point.x === lPt.x) return 0;\n return point.x < lPt.x ? 1 : -1;\n }\n\n // Nearly vertical segments with an intersection.\n // Check to see where a point on the line with matching Y coordinate is.\n const yDist = (point.y - lPt.y) / v.y;\n const xFromYDist = lPt.x + yDist * v.x;\n if (point.x === xFromYDist) return 0;\n\n // General case.\n // Check to see where a point on the line with matching X coordinate is.\n const xDist = (point.x - lPt.x) / v.x;\n const yFromXDist = lPt.y + xDist * v.y;\n if (point.y === yFromXDist) return 0;\n return point.y < yFromXDist ? -1 : 1;\n }\n\n /**\n * Given another segment, returns the first non-trivial intersection\n * between the two segments (in terms of sweep line ordering), if it exists.\n *\n * A 'non-trivial' intersection is one that will cause one or both of the\n * segments to be split(). As such, 'trivial' vs. 'non-trivial' intersection:\n *\n * * endpoint of segA with endpoint of segB --> trivial\n * * endpoint of segA with point along segB --> non-trivial\n * * endpoint of segB with point along segA --> non-trivial\n * * point along segA with point along segB --> non-trivial\n *\n * If no non-trivial intersection exists, return null\n * Else, return null.\n */\n getIntersection(other) {\n // If bboxes don't overlap, there can't be any intersections\n const tBbox = this.bbox();\n const oBbox = other.bbox();\n const bboxOverlap = getBboxOverlap(tBbox, oBbox);\n if (bboxOverlap === null) return null;\n\n // We first check to see if the endpoints can be considered intersections.\n // This will 'snap' intersections to endpoints if possible, and will\n // handle cases of colinearity.\n\n const tlp = this.leftSE.point;\n const trp = this.rightSE.point;\n const olp = other.leftSE.point;\n const orp = other.rightSE.point;\n\n // does each endpoint touch the other segment?\n // note that we restrict the 'touching' definition to only allow segments\n // to touch endpoints that lie forward from where we are in the sweep line pass\n const touchesOtherLSE = isInBbox(tBbox, olp) && this.comparePoint(olp) === 0;\n const touchesThisLSE = isInBbox(oBbox, tlp) && other.comparePoint(tlp) === 0;\n const touchesOtherRSE = isInBbox(tBbox, orp) && this.comparePoint(orp) === 0;\n const touchesThisRSE = isInBbox(oBbox, trp) && other.comparePoint(trp) === 0;\n\n // do left endpoints match?\n if (touchesThisLSE && touchesOtherLSE) {\n // these two cases are for colinear segments with matching left\n // endpoints, and one segment being longer than the other\n if (touchesThisRSE && !touchesOtherRSE) return trp;\n if (!touchesThisRSE && touchesOtherRSE) return orp;\n // either the two segments match exactly (two trival intersections)\n // or just on their left endpoint (one trivial intersection\n return null;\n }\n\n // does this left endpoint matches (other doesn't)\n if (touchesThisLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesOtherRSE) {\n if (tlp.x === orp.x && tlp.y === orp.y) return null;\n }\n // t-intersection on left endpoint\n return tlp;\n }\n\n // does other left endpoint matches (this doesn't)\n if (touchesOtherLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesThisRSE) {\n if (trp.x === olp.x && trp.y === olp.y) return null;\n }\n // t-intersection on left endpoint\n return olp;\n }\n\n // trivial intersection on right endpoints\n if (touchesThisRSE && touchesOtherRSE) return null;\n\n // t-intersections on just one right endpoint\n if (touchesThisRSE) return trp;\n if (touchesOtherRSE) return orp;\n\n // None of our endpoints intersect. Look for a general intersection between\n // infinite lines laid over the segments\n const pt = intersection$1(tlp, this.vector(), olp, other.vector());\n\n // are the segments parrallel? Note that if they were colinear with overlap,\n // they would have an endpoint intersection and that case was already handled above\n if (pt === null) return null;\n\n // is the intersection found between the lines not on the segments?\n if (!isInBbox(bboxOverlap, pt)) return null;\n\n // round the the computed point if needed\n return rounder.round(pt.x, pt.y);\n }\n\n /**\n * Split the given segment into multiple segments on the given points.\n * * Each existing segment will retain its leftSE and a new rightSE will be\n * generated for it.\n * * A new segment will be generated which will adopt the original segment's\n * rightSE, and a new leftSE will be generated for it.\n * * If there are more than two points given to split on, new segments\n * in the middle will be generated with new leftSE and rightSE's.\n * * An array of the newly generated SweepEvents will be returned.\n *\n * Warning: input array of points is modified\n */\n split(point) {\n const newEvents = [];\n const alreadyLinked = point.events !== undefined;\n const newLeftSE = new SweepEvent(point, true);\n const newRightSE = new SweepEvent(point, false);\n const oldRightSE = this.rightSE;\n this.replaceRightSE(newRightSE);\n newEvents.push(newRightSE);\n newEvents.push(newLeftSE);\n const newSeg = new Segment(newLeftSE, oldRightSE, this.rings.slice(), this.windings.slice());\n\n // when splitting a nearly vertical downward-facing segment,\n // sometimes one of the resulting new segments is vertical, in which\n // case its left and right events may need to be swapped\n if (SweepEvent.comparePoints(newSeg.leftSE.point, newSeg.rightSE.point) > 0) {\n newSeg.swapEvents();\n }\n if (SweepEvent.comparePoints(this.leftSE.point, this.rightSE.point) > 0) {\n this.swapEvents();\n }\n\n // in the point we just used to create new sweep events with was already\n // linked to other events, we need to check if either of the affected\n // segments should be consumed\n if (alreadyLinked) {\n newLeftSE.checkForConsuming();\n newRightSE.checkForConsuming();\n }\n return newEvents;\n }\n\n /* Swap which event is left and right */\n swapEvents() {\n const tmpEvt = this.rightSE;\n this.rightSE = this.leftSE;\n this.leftSE = tmpEvt;\n this.leftSE.isLeft = true;\n this.rightSE.isLeft = false;\n for (let i = 0, iMax = this.windings.length; i < iMax; i++) {\n this.windings[i] *= -1;\n }\n }\n\n /* Consume another segment. We take their rings under our wing\n * and mark them as consumed. Use for perfectly overlapping segments */\n consume(other) {\n let consumer = this;\n let consumee = other;\n while (consumer.consumedBy) consumer = consumer.consumedBy;\n while (consumee.consumedBy) consumee = consumee.consumedBy;\n const cmp = Segment.compare(consumer, consumee);\n if (cmp === 0) return; // already consumed\n // the winner of the consumption is the earlier segment\n // according to sweep line ordering\n if (cmp > 0) {\n const tmp = consumer;\n consumer = consumee;\n consumee = tmp;\n }\n\n // make sure a segment doesn't consume it's prev\n if (consumer.prev === consumee) {\n const tmp = consumer;\n consumer = consumee;\n consumee = tmp;\n }\n for (let i = 0, iMax = consumee.rings.length; i < iMax; i++) {\n const ring = consumee.rings[i];\n const winding = consumee.windings[i];\n const index = consumer.rings.indexOf(ring);\n if (index === -1) {\n consumer.rings.push(ring);\n consumer.windings.push(winding);\n } else consumer.windings[index] += winding;\n }\n consumee.rings = null;\n consumee.windings = null;\n consumee.consumedBy = consumer;\n\n // mark sweep events consumed as to maintain ordering in sweep event queue\n consumee.leftSE.consumedBy = consumer.leftSE;\n consumee.rightSE.consumedBy = consumer.rightSE;\n }\n\n /* The first segment previous segment chain that is in the result */\n prevInResult() {\n if (this._prevInResult !== undefined) return this._prevInResult;\n if (!this.prev) this._prevInResult = null;else if (this.prev.isInResult()) this._prevInResult = this.prev;else this._prevInResult = this.prev.prevInResult();\n return this._prevInResult;\n }\n beforeState() {\n if (this._beforeState !== undefined) return this._beforeState;\n if (!this.prev) this._beforeState = {\n rings: [],\n windings: [],\n multiPolys: []\n };else {\n const seg = this.prev.consumedBy || this.prev;\n this._beforeState = seg.afterState();\n }\n return this._beforeState;\n }\n afterState() {\n if (this._afterState !== undefined) return this._afterState;\n const beforeState = this.beforeState();\n this._afterState = {\n rings: beforeState.rings.slice(0),\n windings: beforeState.windings.slice(0),\n multiPolys: []\n };\n const ringsAfter = this._afterState.rings;\n const windingsAfter = this._afterState.windings;\n const mpsAfter = this._afterState.multiPolys;\n\n // calculate ringsAfter, windingsAfter\n for (let i = 0, iMax = this.rings.length; i < iMax; i++) {\n const ring = this.rings[i];\n const winding = this.windings[i];\n const index = ringsAfter.indexOf(ring);\n if (index === -1) {\n ringsAfter.push(ring);\n windingsAfter.push(winding);\n } else windingsAfter[index] += winding;\n }\n\n // calcualte polysAfter\n const polysAfter = [];\n const polysExclude = [];\n for (let i = 0, iMax = ringsAfter.length; i < iMax; i++) {\n if (windingsAfter[i] === 0) continue; // non-zero rule\n const ring = ringsAfter[i];\n const poly = ring.poly;\n if (polysExclude.indexOf(poly) !== -1) continue;\n if (ring.isExterior) polysAfter.push(poly);else {\n if (polysExclude.indexOf(poly) === -1) polysExclude.push(poly);\n const index = polysAfter.indexOf(ring.poly);\n if (index !== -1) polysAfter.splice(index, 1);\n }\n }\n\n // calculate multiPolysAfter\n for (let i = 0, iMax = polysAfter.length; i < iMax; i++) {\n const mp = polysAfter[i].multiPoly;\n if (mpsAfter.indexOf(mp) === -1) mpsAfter.push(mp);\n }\n return this._afterState;\n }\n\n /* Is this segment part of the final result? */\n isInResult() {\n // if we've been consumed, we're not in the result\n if (this.consumedBy) return false;\n if (this._isInResult !== undefined) return this._isInResult;\n const mpsBefore = this.beforeState().multiPolys;\n const mpsAfter = this.afterState().multiPolys;\n switch (operation.type) {\n case \"union\":\n {\n // UNION - included iff:\n // * On one side of us there is 0 poly interiors AND\n // * On the other side there is 1 or more.\n const noBefores = mpsBefore.length === 0;\n const noAfters = mpsAfter.length === 0;\n this._isInResult = noBefores !== noAfters;\n break;\n }\n case \"intersection\":\n {\n // INTERSECTION - included iff:\n // * on one side of us all multipolys are rep. with poly interiors AND\n // * on the other side of us, not all multipolys are repsented\n // with poly interiors\n let least;\n let most;\n if (mpsBefore.length < mpsAfter.length) {\n least = mpsBefore.length;\n most = mpsAfter.length;\n } else {\n least = mpsAfter.length;\n most = mpsBefore.length;\n }\n this._isInResult = most === operation.numMultiPolys && least < most;\n break;\n }\n case \"xor\":\n {\n // XOR - included iff:\n // * the difference between the number of multipolys represented\n // with poly interiors on our two sides is an odd number\n const diff = Math.abs(mpsBefore.length - mpsAfter.length);\n this._isInResult = diff % 2 === 1;\n break;\n }\n case \"difference\":\n {\n // DIFFERENCE included iff:\n // * on exactly one side, we have just the subject\n const isJustSubject = mps => mps.length === 1 && mps[0].isSubject;\n this._isInResult = isJustSubject(mpsBefore) !== isJustSubject(mpsAfter);\n break;\n }\n default:\n throw new Error(`Unrecognized operation type found ${operation.type}`);\n }\n return this._isInResult;\n }\n }\n\n class RingIn {\n constructor(geomRing, poly, isExterior) {\n if (!Array.isArray(geomRing) || geomRing.length === 0) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n this.poly = poly;\n this.isExterior = isExterior;\n this.segments = [];\n if (typeof geomRing[0][0] !== \"number\" || typeof geomRing[0][1] !== \"number\") {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n const firstPoint = rounder.round(geomRing[0][0], geomRing[0][1]);\n this.bbox = {\n ll: {\n x: firstPoint.x,\n y: firstPoint.y\n },\n ur: {\n x: firstPoint.x,\n y: firstPoint.y\n }\n };\n let prevPoint = firstPoint;\n for (let i = 1, iMax = geomRing.length; i < iMax; i++) {\n if (typeof geomRing[i][0] !== \"number\" || typeof geomRing[i][1] !== \"number\") {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n let point = rounder.round(geomRing[i][0], geomRing[i][1]);\n // skip repeated points\n if (point.x === prevPoint.x && point.y === prevPoint.y) continue;\n this.segments.push(Segment.fromRing(prevPoint, point, this));\n if (point.x < this.bbox.ll.x) this.bbox.ll.x = point.x;\n if (point.y < this.bbox.ll.y) this.bbox.ll.y = point.y;\n if (point.x > this.bbox.ur.x) this.bbox.ur.x = point.x;\n if (point.y > this.bbox.ur.y) this.bbox.ur.y = point.y;\n prevPoint = point;\n }\n // add segment from last to first if last is not the same as first\n if (firstPoint.x !== prevPoint.x || firstPoint.y !== prevPoint.y) {\n this.segments.push(Segment.fromRing(prevPoint, firstPoint, this));\n }\n }\n getSweepEvents() {\n const sweepEvents = [];\n for (let i = 0, iMax = this.segments.length; i < iMax; i++) {\n const segment = this.segments[i];\n sweepEvents.push(segment.leftSE);\n sweepEvents.push(segment.rightSE);\n }\n return sweepEvents;\n }\n }\n class PolyIn {\n constructor(geomPoly, multiPoly) {\n if (!Array.isArray(geomPoly)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n this.exteriorRing = new RingIn(geomPoly[0], this, true);\n // copy by value\n this.bbox = {\n ll: {\n x: this.exteriorRing.bbox.ll.x,\n y: this.exteriorRing.bbox.ll.y\n },\n ur: {\n x: this.exteriorRing.bbox.ur.x,\n y: this.exteriorRing.bbox.ur.y\n }\n };\n this.interiorRings = [];\n for (let i = 1, iMax = geomPoly.length; i < iMax; i++) {\n const ring = new RingIn(geomPoly[i], this, false);\n if (ring.bbox.ll.x < this.bbox.ll.x) this.bbox.ll.x = ring.bbox.ll.x;\n if (ring.bbox.ll.y < this.bbox.ll.y) this.bbox.ll.y = ring.bbox.ll.y;\n if (ring.bbox.ur.x > this.bbox.ur.x) this.bbox.ur.x = ring.bbox.ur.x;\n if (ring.bbox.ur.y > this.bbox.ur.y) this.bbox.ur.y = ring.bbox.ur.y;\n this.interiorRings.push(ring);\n }\n this.multiPoly = multiPoly;\n }\n getSweepEvents() {\n const sweepEvents = this.exteriorRing.getSweepEvents();\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringSweepEvents = this.interiorRings[i].getSweepEvents();\n for (let j = 0, jMax = ringSweepEvents.length; j < jMax; j++) {\n sweepEvents.push(ringSweepEvents[j]);\n }\n }\n return sweepEvents;\n }\n }\n class MultiPolyIn {\n constructor(geom, isSubject) {\n if (!Array.isArray(geom)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n try {\n // if the input looks like a polygon, convert it to a multipolygon\n if (typeof geom[0][0][0] === \"number\") geom = [geom];\n } catch (ex) {\n // The input is either malformed or has empty arrays.\n // In either case, it will be handled later on.\n }\n this.polys = [];\n this.bbox = {\n ll: {\n x: Number.POSITIVE_INFINITY,\n y: Number.POSITIVE_INFINITY\n },\n ur: {\n x: Number.NEGATIVE_INFINITY,\n y: Number.NEGATIVE_INFINITY\n }\n };\n for (let i = 0, iMax = geom.length; i < iMax; i++) {\n const poly = new PolyIn(geom[i], this);\n if (poly.bbox.ll.x < this.bbox.ll.x) this.bbox.ll.x = poly.bbox.ll.x;\n if (poly.bbox.ll.y < this.bbox.ll.y) this.bbox.ll.y = poly.bbox.ll.y;\n if (poly.bbox.ur.x > this.bbox.ur.x) this.bbox.ur.x = poly.bbox.ur.x;\n if (poly.bbox.ur.y > this.bbox.ur.y) this.bbox.ur.y = poly.bbox.ur.y;\n this.polys.push(poly);\n }\n this.isSubject = isSubject;\n }\n getSweepEvents() {\n const sweepEvents = [];\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polySweepEvents = this.polys[i].getSweepEvents();\n for (let j = 0, jMax = polySweepEvents.length; j < jMax; j++) {\n sweepEvents.push(polySweepEvents[j]);\n }\n }\n return sweepEvents;\n }\n }\n\n class RingOut {\n /* Given the segments from the sweep line pass, compute & return a series\n * of closed rings from all the segments marked to be part of the result */\n static factory(allSegments) {\n const ringsOut = [];\n for (let i = 0, iMax = allSegments.length; i < iMax; i++) {\n const segment = allSegments[i];\n if (!segment.isInResult() || segment.ringOut) continue;\n let prevEvent = null;\n let event = segment.leftSE;\n let nextEvent = segment.rightSE;\n const events = [event];\n const startingPoint = event.point;\n const intersectionLEs = [];\n\n /* Walk the chain of linked events to form a closed ring */\n while (true) {\n prevEvent = event;\n event = nextEvent;\n events.push(event);\n\n /* Is the ring complete? */\n if (event.point === startingPoint) break;\n while (true) {\n const availableLEs = event.getAvailableLinkedEvents();\n\n /* Did we hit a dead end? This shouldn't happen.\n * Indicates some earlier part of the algorithm malfunctioned. */\n if (availableLEs.length === 0) {\n const firstPt = events[0].point;\n const lastPt = events[events.length - 1].point;\n throw new Error(`Unable to complete output ring starting at [${firstPt.x},` + ` ${firstPt.y}]. Last matching segment found ends at` + ` [${lastPt.x}, ${lastPt.y}].`);\n }\n\n /* Only one way to go, so cotinue on the path */\n if (availableLEs.length === 1) {\n nextEvent = availableLEs[0].otherSE;\n break;\n }\n\n /* We must have an intersection. Check for a completed loop */\n let indexLE = null;\n for (let j = 0, jMax = intersectionLEs.length; j < jMax; j++) {\n if (intersectionLEs[j].point === event.point) {\n indexLE = j;\n break;\n }\n }\n /* Found a completed loop. Cut that off and make a ring */\n if (indexLE !== null) {\n const intersectionLE = intersectionLEs.splice(indexLE)[0];\n const ringEvents = events.splice(intersectionLE.index);\n ringEvents.unshift(ringEvents[0].otherSE);\n ringsOut.push(new RingOut(ringEvents.reverse()));\n continue;\n }\n /* register the intersection */\n intersectionLEs.push({\n index: events.length,\n point: event.point\n });\n /* Choose the left-most option to continue the walk */\n const comparator = event.getLeftmostComparator(prevEvent);\n nextEvent = availableLEs.sort(comparator)[0].otherSE;\n break;\n }\n }\n ringsOut.push(new RingOut(events));\n }\n return ringsOut;\n }\n constructor(events) {\n this.events = events;\n for (let i = 0, iMax = events.length; i < iMax; i++) {\n events[i].segment.ringOut = this;\n }\n this.poly = null;\n }\n getGeom() {\n // Remove superfluous points (ie extra points along a straight line),\n let prevPt = this.events[0].point;\n const points = [prevPt];\n for (let i = 1, iMax = this.events.length - 1; i < iMax; i++) {\n const pt = this.events[i].point;\n const nextPt = this.events[i + 1].point;\n if (compareVectorAngles(pt, prevPt, nextPt) === 0) continue;\n points.push(pt);\n prevPt = pt;\n }\n\n // ring was all (within rounding error of angle calc) colinear points\n if (points.length === 1) return null;\n\n // check if the starting point is necessary\n const pt = points[0];\n const nextPt = points[1];\n if (compareVectorAngles(pt, prevPt, nextPt) === 0) points.shift();\n points.push(points[0]);\n const step = this.isExteriorRing() ? 1 : -1;\n const iStart = this.isExteriorRing() ? 0 : points.length - 1;\n const iEnd = this.isExteriorRing() ? points.length : -1;\n const orderedPoints = [];\n for (let i = iStart; i != iEnd; i += step) orderedPoints.push([points[i].x, points[i].y]);\n return orderedPoints;\n }\n isExteriorRing() {\n if (this._isExteriorRing === undefined) {\n const enclosing = this.enclosingRing();\n this._isExteriorRing = enclosing ? !enclosing.isExteriorRing() : true;\n }\n return this._isExteriorRing;\n }\n enclosingRing() {\n if (this._enclosingRing === undefined) {\n this._enclosingRing = this._calcEnclosingRing();\n }\n return this._enclosingRing;\n }\n\n /* Returns the ring that encloses this one, if any */\n _calcEnclosingRing() {\n // start with the ealier sweep line event so that the prevSeg\n // chain doesn't lead us inside of a loop of ours\n let leftMostEvt = this.events[0];\n for (let i = 1, iMax = this.events.length; i < iMax; i++) {\n const evt = this.events[i];\n if (SweepEvent.compare(leftMostEvt, evt) > 0) leftMostEvt = evt;\n }\n let prevSeg = leftMostEvt.segment.prevInResult();\n let prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null;\n while (true) {\n // no segment found, thus no ring can enclose us\n if (!prevSeg) return null;\n\n // no segments below prev segment found, thus the ring of the prev\n // segment must loop back around and enclose us\n if (!prevPrevSeg) return prevSeg.ringOut;\n\n // if the two segments are of different rings, the ring of the prev\n // segment must either loop around us or the ring of the prev prev\n // seg, which would make us and the ring of the prev peers\n if (prevPrevSeg.ringOut !== prevSeg.ringOut) {\n if (prevPrevSeg.ringOut.enclosingRing() !== prevSeg.ringOut) {\n return prevSeg.ringOut;\n } else return prevSeg.ringOut.enclosingRing();\n }\n\n // two segments are from the same ring, so this was a penisula\n // of that ring. iterate downward, keep searching\n prevSeg = prevPrevSeg.prevInResult();\n prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null;\n }\n }\n }\n class PolyOut {\n constructor(exteriorRing) {\n this.exteriorRing = exteriorRing;\n exteriorRing.poly = this;\n this.interiorRings = [];\n }\n addInterior(ring) {\n this.interiorRings.push(ring);\n ring.poly = this;\n }\n getGeom() {\n const geom = [this.exteriorRing.getGeom()];\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (geom[0] === null) return null;\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringGeom = this.interiorRings[i].getGeom();\n // interior ring was all (within rounding error of angle calc) colinear points\n if (ringGeom === null) continue;\n geom.push(ringGeom);\n }\n return geom;\n }\n }\n class MultiPolyOut {\n constructor(rings) {\n this.rings = rings;\n this.polys = this._composePolys(rings);\n }\n getGeom() {\n const geom = [];\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polyGeom = this.polys[i].getGeom();\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (polyGeom === null) continue;\n geom.push(polyGeom);\n }\n return geom;\n }\n _composePolys(rings) {\n const polys = [];\n for (let i = 0, iMax = rings.length; i < iMax; i++) {\n const ring = rings[i];\n if (ring.poly) continue;\n if (ring.isExteriorRing()) polys.push(new PolyOut(ring));else {\n const enclosingRing = ring.enclosingRing();\n if (!enclosingRing.poly) polys.push(new PolyOut(enclosingRing));\n enclosingRing.poly.addInterior(ring);\n }\n }\n return polys;\n }\n }\n\n /**\n * NOTE: We must be careful not to change any segments while\n * they are in the SplayTree. AFAIK, there's no way to tell\n * the tree to rebalance itself - thus before splitting\n * a segment that's in the tree, we remove it from the tree,\n * do the split, then re-insert it. (Even though splitting a\n * segment *shouldn't* change its correct position in the\n * sweep line tree, the reality is because of rounding errors,\n * it sometimes does.)\n */\n\n class SweepLine {\n constructor(queue) {\n let comparator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Segment.compare;\n this.queue = queue;\n this.tree = new Tree(comparator);\n this.segments = [];\n }\n process(event) {\n const segment = event.segment;\n const newEvents = [];\n\n // if we've already been consumed by another segment,\n // clean up our body parts and get out\n if (event.consumedBy) {\n if (event.isLeft) this.queue.remove(event.otherSE);else this.tree.remove(segment);\n return newEvents;\n }\n const node = event.isLeft ? this.tree.add(segment) : this.tree.find(segment);\n if (!node) throw new Error(`Unable to find segment #${segment.id} ` + `[${segment.leftSE.point.x}, ${segment.leftSE.point.y}] -> ` + `[${segment.rightSE.point.x}, ${segment.rightSE.point.y}] ` + \"in SweepLine tree.\");\n let prevNode = node;\n let nextNode = node;\n let prevSeg = undefined;\n let nextSeg = undefined;\n\n // skip consumed segments still in tree\n while (prevSeg === undefined) {\n prevNode = this.tree.prev(prevNode);\n if (prevNode === null) prevSeg = null;else if (prevNode.key.consumedBy === undefined) prevSeg = prevNode.key;\n }\n\n // skip consumed segments still in tree\n while (nextSeg === undefined) {\n nextNode = this.tree.next(nextNode);\n if (nextNode === null) nextSeg = null;else if (nextNode.key.consumedBy === undefined) nextSeg = nextNode.key;\n }\n if (event.isLeft) {\n // Check for intersections against the previous segment in the sweep line\n let prevMySplitter = null;\n if (prevSeg) {\n const prevInter = prevSeg.getIntersection(segment);\n if (prevInter !== null) {\n if (!segment.isAnEndpoint(prevInter)) prevMySplitter = prevInter;\n if (!prevSeg.isAnEndpoint(prevInter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, prevInter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n }\n }\n\n // Check for intersections against the next segment in the sweep line\n let nextMySplitter = null;\n if (nextSeg) {\n const nextInter = nextSeg.getIntersection(segment);\n if (nextInter !== null) {\n if (!segment.isAnEndpoint(nextInter)) nextMySplitter = nextInter;\n if (!nextSeg.isAnEndpoint(nextInter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, nextInter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n }\n }\n\n // For simplicity, even if we find more than one intersection we only\n // spilt on the 'earliest' (sweep-line style) of the intersections.\n // The other intersection will be handled in a future process().\n if (prevMySplitter !== null || nextMySplitter !== null) {\n let mySplitter = null;\n if (prevMySplitter === null) mySplitter = nextMySplitter;else if (nextMySplitter === null) mySplitter = prevMySplitter;else {\n const cmpSplitters = SweepEvent.comparePoints(prevMySplitter, nextMySplitter);\n mySplitter = cmpSplitters <= 0 ? prevMySplitter : nextMySplitter;\n }\n\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n this.queue.remove(segment.rightSE);\n newEvents.push(segment.rightSE);\n const newEventsFromSplit = segment.split(mySplitter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n if (newEvents.length > 0) {\n // We found some intersections, so re-do the current event to\n // make sure sweep line ordering is totally consistent for later\n // use with the segment 'prev' pointers\n this.tree.remove(segment);\n newEvents.push(event);\n } else {\n // done with left event\n this.segments.push(segment);\n segment.prev = prevSeg;\n }\n } else {\n // event.isRight\n\n // since we're about to be removed from the sweep line, check for\n // intersections between our previous and next segments\n if (prevSeg && nextSeg) {\n const inter = prevSeg.getIntersection(nextSeg);\n if (inter !== null) {\n if (!prevSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, inter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n if (!nextSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, inter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n }\n }\n this.tree.remove(segment);\n }\n return newEvents;\n }\n\n /* Safely split a segment that is currently in the datastructures\n * IE - a segment other than the one that is currently being processed. */\n _splitSafely(seg, pt) {\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n // removeNode() doesn't work, so have re-find the seg\n // https://github.com/w8r/splay-tree/pull/5\n this.tree.remove(seg);\n const rightSE = seg.rightSE;\n this.queue.remove(rightSE);\n const newEvents = seg.split(pt);\n newEvents.push(rightSE);\n // splitting can trigger consumption\n if (seg.consumedBy === undefined) this.tree.add(seg);\n return newEvents;\n }\n }\n\n // Limits on iterative processes to prevent infinite loops - usually caused by floating-point math round-off errors.\n const POLYGON_CLIPPING_MAX_QUEUE_SIZE = typeof process !== \"undefined\" && process.env.POLYGON_CLIPPING_MAX_QUEUE_SIZE || 1000000;\n const POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS = typeof process !== \"undefined\" && process.env.POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS || 1000000;\n class Operation {\n run(type, geom, moreGeoms) {\n operation.type = type;\n rounder.reset();\n\n /* Convert inputs to MultiPoly objects */\n const multipolys = [new MultiPolyIn(geom, true)];\n for (let i = 0, iMax = moreGeoms.length; i < iMax; i++) {\n multipolys.push(new MultiPolyIn(moreGeoms[i], false));\n }\n operation.numMultiPolys = multipolys.length;\n\n /* BBox optimization for difference operation\n * If the bbox of a multipolygon that's part of the clipping doesn't\n * intersect the bbox of the subject at all, we can just drop that\n * multiploygon. */\n if (operation.type === \"difference\") {\n // in place removal\n const subject = multipolys[0];\n let i = 1;\n while (i < multipolys.length) {\n if (getBboxOverlap(multipolys[i].bbox, subject.bbox) !== null) i++;else multipolys.splice(i, 1);\n }\n }\n\n /* BBox optimization for intersection operation\n * If we can find any pair of multipolygons whose bbox does not overlap,\n * then the result will be empty. */\n if (operation.type === \"intersection\") {\n // TODO: this is O(n^2) in number of polygons. By sorting the bboxes,\n // it could be optimized to O(n * ln(n))\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const mpA = multipolys[i];\n for (let j = i + 1, jMax = multipolys.length; j < jMax; j++) {\n if (getBboxOverlap(mpA.bbox, multipolys[j].bbox) === null) return [];\n }\n }\n }\n\n /* Put segment endpoints in a priority queue */\n const queue = new Tree(SweepEvent.compare);\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const sweepEvents = multipolys[i].getSweepEvents();\n for (let j = 0, jMax = sweepEvents.length; j < jMax; j++) {\n queue.insert(sweepEvents[j]);\n if (queue.size > POLYGON_CLIPPING_MAX_QUEUE_SIZE) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n throw new Error(\"Infinite loop when putting segment endpoints in a priority queue \" + \"(queue size too big).\");\n }\n }\n }\n\n /* Pass the sweep line over those endpoints */\n const sweepLine = new SweepLine(queue);\n let prevQueueSize = queue.size;\n let node = queue.pop();\n while (node) {\n const evt = node.key;\n if (queue.size === prevQueueSize) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n const seg = evt.segment;\n throw new Error(`Unable to pop() ${evt.isLeft ? \"left\" : \"right\"} SweepEvent ` + `[${evt.point.x}, ${evt.point.y}] from segment #${seg.id} ` + `[${seg.leftSE.point.x}, ${seg.leftSE.point.y}] -> ` + `[${seg.rightSE.point.x}, ${seg.rightSE.point.y}] from queue.`);\n }\n if (queue.size > POLYGON_CLIPPING_MAX_QUEUE_SIZE) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n throw new Error(\"Infinite loop when passing sweep line over endpoints \" + \"(queue size too big).\");\n }\n if (sweepLine.segments.length > POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n throw new Error(\"Infinite loop when passing sweep line over endpoints \" + \"(too many sweep line segments).\");\n }\n const newEvents = sweepLine.process(evt);\n for (let i = 0, iMax = newEvents.length; i < iMax; i++) {\n const evt = newEvents[i];\n if (evt.consumedBy === undefined) queue.insert(evt);\n }\n prevQueueSize = queue.size;\n node = queue.pop();\n }\n\n // free some memory we don't need anymore\n rounder.reset();\n\n /* Collect and compile segments we're keeping into a multipolygon */\n const ringsOut = RingOut.factory(sweepLine.segments);\n const result = new MultiPolyOut(ringsOut);\n return result.getGeom();\n }\n }\n\n // singleton available by import\n const operation = new Operation();\n\n const union = function (geom) {\n for (var _len = arguments.length, moreGeoms = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n moreGeoms[_key - 1] = arguments[_key];\n }\n return operation.run(\"union\", geom, moreGeoms);\n };\n const intersection = function (geom) {\n for (var _len2 = arguments.length, moreGeoms = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n moreGeoms[_key2 - 1] = arguments[_key2];\n }\n return operation.run(\"intersection\", geom, moreGeoms);\n };\n const xor = function (geom) {\n for (var _len3 = arguments.length, moreGeoms = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {\n moreGeoms[_key3 - 1] = arguments[_key3];\n }\n return operation.run(\"xor\", geom, moreGeoms);\n };\n const difference = function (subjectGeom) {\n for (var _len4 = arguments.length, clippingGeoms = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {\n clippingGeoms[_key4 - 1] = arguments[_key4];\n }\n return operation.run(\"difference\", subjectGeom, clippingGeoms);\n };\n var index = {\n union: union,\n intersection: intersection,\n xor: xor,\n difference: difference\n };\n\n return index;\n\n}));\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { deepEqual } from 'fast-equals';\nimport turf_bboxClip from '@turf/bbox-clip';\nimport stringify from 'fast-json-stable-stringify';\nimport polygonClipping from 'polygon-clipping';\n\nimport Protobuf from 'pbf';\nimport { VectorTile } from '@mapbox/vector-tile';\n\nimport { utilHashcode, utilRebind, utilTiler } from '../util';\n\n\nvar tiler = utilTiler().tileSize(512).margin(1);\nvar dispatch = d3_dispatch('loadedData');\nvar _vtCache;\n\n\nfunction abortRequest(controller) {\n controller.abort();\n}\n\n\nfunction vtToGeoJSON(data, tile, mergeCache) {\n var vectorTile = new VectorTile(new Protobuf(data));\n var layers = Object.keys(vectorTile.layers);\n if (!Array.isArray(layers)) { layers = [layers]; }\n\n var features = [];\n layers.forEach(function(layerID) {\n var layer = vectorTile.layers[layerID];\n if (layer) {\n for (var i = 0; i < layer.length; i++) {\n var feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n var geometry = feature.geometry;\n\n // Treat all Polygons as MultiPolygons\n if (geometry.type === 'Polygon') {\n geometry.type = 'MultiPolygon';\n geometry.coordinates = [geometry.coordinates];\n }\n\n var isClipped = false;\n\n // Clip to tile bounds\n if (geometry.type === 'MultiPolygon') {\n var featureClip = turf_bboxClip(feature, tile.extent.rectangle());\n if (!deepEqual(feature.geometry, featureClip.geometry)) {\n // feature = featureClip;\n isClipped = true;\n }\n if (!feature.geometry.coordinates.length) continue; // not actually on this tile\n if (!feature.geometry.coordinates[0].length) continue; // not actually on this tile\n }\n\n // Generate some unique IDs and add some metadata\n var featurehash = utilHashcode(stringify(feature));\n var propertyhash = utilHashcode(stringify(feature.properties || {}));\n feature.__layerID__ = layerID.replace(/[^_a-zA-Z0-9\\-]/g, '_');\n feature.__featurehash__ = featurehash;\n feature.__propertyhash__ = propertyhash;\n features.push(feature);\n\n // Clipped Polygons at same zoom with identical properties can get merged\n if (isClipped && geometry.type === 'MultiPolygon') {\n var merged = mergeCache[propertyhash];\n if (merged && merged.length) {\n var other = merged[0];\n var coords = polygonClipping.union(\n feature.geometry.coordinates,\n other.geometry.coordinates\n );\n\n if (!coords || !coords.length) {\n continue; // something failed in polygon union\n }\n\n merged.push(feature);\n for (var j = 0; j < merged.length; j++) { // all these features get...\n merged[j].geometry.coordinates = coords; // same coords\n merged[j].__featurehash__ = featurehash; // same hash, so deduplication works\n }\n } else {\n mergeCache[propertyhash] = [feature];\n }\n }\n }\n }\n });\n\n return features;\n}\n\n\nfunction loadTile(source, tile) {\n if (source.loaded[tile.id] || source.inflight[tile.id]) return;\n\n var url = source.template\n .replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n // TMS-flipped y coordinate\n .replace(/\\{[t-]y\\}/, Math.pow(2, tile.xyz[2]) - tile.xyz[1] - 1)\n .replace(/\\{z(oom)?\\}/, tile.xyz[2])\n .replace(/\\{switch:([^}]+)\\}/, function(s, r) {\n var subdomains = r.split(',');\n return subdomains[(tile.xyz[0] + tile.xyz[1]) % subdomains.length];\n });\n\n\n var controller = new AbortController();\n source.inflight[tile.id] = controller;\n\n fetch(url, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n source.loaded[tile.id] = [];\n delete source.inflight[tile.id];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (!data) {\n throw new Error('No Data');\n }\n\n var z = tile.xyz[2];\n if (!source.canMerge[z]) {\n source.canMerge[z] = {}; // initialize mergeCache\n }\n\n source.loaded[tile.id] = vtToGeoJSON(data, tile, source.canMerge[z]);\n dispatch.call('loadedData');\n })\n .catch(function() {\n source.loaded[tile.id] = [];\n delete source.inflight[tile.id];\n });\n}\n\n\nexport default {\n\n init: function() {\n if (!_vtCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n\n reset: function() {\n for (var sourceID in _vtCache) {\n var source = _vtCache[sourceID];\n if (source && source.inflight) {\n Object.values(source.inflight).forEach(abortRequest);\n }\n }\n\n _vtCache = {};\n },\n\n\n addSource: function(sourceID, template) {\n _vtCache[sourceID] = { template: template, inflight: {}, loaded: {}, canMerge: {} };\n return _vtCache[sourceID];\n },\n\n\n data: function(sourceID, projection) {\n var source = _vtCache[sourceID];\n if (!source) return [];\n\n var tiles = tiler.getTiles(projection);\n var seen = {};\n var results = [];\n\n for (var i = 0; i < tiles.length; i++) {\n var features = source.loaded[tiles[i].id];\n if (!features || !features.length) continue;\n\n for (var j = 0; j < features.length; j++) {\n var feature = features[j];\n var hash = feature.__featurehash__;\n if (seen[hash]) continue;\n seen[hash] = true;\n\n // return a shallow copy, because the hash may change\n // later if this feature gets merged with another\n results.push(Object.assign({}, feature)); // shallow copy\n }\n }\n\n return results;\n },\n\n\n loadTiles: function(sourceID, template, projection) {\n var source = _vtCache[sourceID];\n if (!source) {\n source = this.addSource(sourceID, template);\n }\n\n var tiles = tiler.getTiles(projection);\n\n // abort inflight requests that are no longer needed\n Object.keys(source.inflight).forEach(function(k) {\n var wanted = tiles.find(function(tile) { return k === tile.id; });\n if (!wanted) {\n abortRequest(source.inflight[k]);\n delete source.inflight[k];\n }\n });\n\n tiles.forEach(function(tile) {\n loadTile(source, tile);\n });\n },\n\n\n cache: function() {\n return _vtCache;\n }\n\n};\n", "import { json as d3_json } from 'd3-fetch';\n\nimport { utilQsString } from '../util';\nimport { localizer } from '../core/localizer';\n\nvar apibase = 'https://www.wikidata.org/w/api.php?';\nvar _wikidataCache = {};\n\n\nexport default {\n\n init: function() {},\n\n reset: function() {\n _wikidataCache = {};\n },\n\n\n // Search for Wikidata items matching the query\n itemsForSearchQuery: function(query, callback, language) {\n if (!query) {\n if (callback) callback('No query', {});\n return;\n }\n\n var lang = this.languagesToQuery()[0];\n\n var url = apibase + utilQsString({\n action: 'wbsearchentities',\n format: 'json',\n formatversion: 2,\n search: query,\n type: 'item',\n // the language to search\n language: language || lang,\n // the language for the label and description in the result\n uselang: lang,\n limit: 10,\n origin: '*'\n });\n\n d3_json(url)\n .then(result => {\n if (result && result.error) {\n if (result.error.code === 'badvalue' &&\n result.error.info.includes(lang) &&\n !language && lang.includes('-')) {\n // retry without \"country suffix\" region subtag\n this.itemsForSearchQuery(query, callback, lang.split('-')[0]);\n return;\n } else {\n throw new Error(result.error);\n }\n }\n if (callback) callback(null, result.search || {});\n })\n .catch(function(err) {\n if (callback) callback(err.message, {});\n });\n },\n\n\n // Given a Wikipedia language and article title,\n // return an array of corresponding Wikidata entities.\n itemsByTitle: function(lang, title, callback) {\n if (!title) {\n if (callback) callback('No title', {});\n return;\n }\n\n lang = lang || 'en';\n var url = apibase + utilQsString({\n action: 'wbgetentities',\n format: 'json',\n formatversion: 2,\n sites: lang.replace(/-/g, '_') + 'wiki',\n titles: title,\n languages: 'en', // shrink response by filtering to one language\n origin: '*'\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n }\n if (callback) callback(null, result.entities || {});\n })\n .catch(function(err) {\n if (callback) callback(err.message, {});\n });\n },\n\n\n languagesToQuery: function() {\n return localizer.localeCodes().map(function(code) {\n return code.toLowerCase();\n }).filter(function(code) {\n // HACK: en-us isn't a wikidata language. We should really be filtering by\n // the languages known to be supported by wikidata.\n return code !== 'en-us';\n });\n },\n\n\n entityByQID: function(qid, callback) {\n if (!qid) {\n callback('No qid', {});\n return;\n }\n if (_wikidataCache[qid]) {\n if (callback) callback(null, _wikidataCache[qid]);\n return;\n }\n\n var langs = this.languagesToQuery();\n var url = apibase + utilQsString({\n action: 'wbgetentities',\n format: 'json',\n formatversion: 2,\n ids: qid,\n props: 'labels|descriptions|claims|sitelinks',\n sitefilter: langs.map(function(d) { return d + 'wiki'; }).join('|'),\n languages: langs.join('|'),\n languagefallback: 1,\n origin: '*'\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n }\n if (callback) callback(null, result.entities[qid] || {});\n })\n .catch(function(err) {\n if (callback) callback(err.message, {});\n });\n },\n\n\n // Pass `params` object of the form:\n // {\n // qid: 'string' // brand wikidata (e.g. 'Q37158')\n // }\n //\n // Get an result object used to display tag documentation\n // {\n // title: 'string',\n // description: 'string',\n // editURL: 'string',\n // imageURL: 'string',\n // wiki: { title: 'string', text: 'string', url: 'string' }\n // }\n //\n getDocs: function(params, callback) {\n var langs = this.languagesToQuery();\n this.entityByQID(params.qid, function(err, entity) {\n if (err || !entity) {\n callback(err || 'No entity');\n return;\n }\n\n var i;\n var description;\n for (i in langs) {\n let code = langs[i];\n if (entity.descriptions[code] && entity.descriptions[code].language === code) {\n description = entity.descriptions[code];\n break;\n }\n }\n if (!description && Object.values(entity.descriptions).length) description = Object.values(entity.descriptions)[0];\n\n // prepare result\n var result = {\n title: entity.id,\n description: selection => selection.text(description ? description.value : ''),\n descriptionLocaleCode: description ? description.language : '',\n editURL: 'https://www.wikidata.org/wiki/' + entity.id\n };\n\n // add image\n if (entity.claims) {\n var imageroot = 'https://commons.wikimedia.org/w/index.php';\n var props = ['P154','P18']; // logo image, image\n var prop, image;\n for (i = 0; i < props.length; i++) {\n prop = entity.claims[props[i]];\n if (prop && Object.keys(prop).length > 0) {\n image = prop[Object.keys(prop)[0]].mainsnak.datavalue.value;\n if (image) {\n result.imageURL = imageroot + '?' + utilQsString({\n title: 'Special:Redirect/file/' + image,\n width: 400\n });\n break;\n }\n }\n }\n }\n\n if (entity.sitelinks) {\n var englishLocale = localizer.languageCode().toLowerCase() === 'en';\n\n // must be one of these that we requested..\n for (i = 0; i < langs.length; i++) { // check each, in order of preference\n var w = langs[i] + 'wiki';\n if (entity.sitelinks[w]) {\n var title = entity.sitelinks[w].title;\n var tKey = 'inspector.wiki_reference';\n if (!englishLocale && langs[i] === 'en') { // user's locale isn't English but\n tKey = 'inspector.wiki_en_reference'; // we are sending them to enwiki anyway..\n }\n\n result.wiki = {\n title: title,\n text: tKey,\n url: 'https://' + langs[i] + '.wikipedia.org/wiki/' + title.replace(/ /g, '_')\n };\n break;\n }\n }\n }\n\n callback(null, result);\n });\n }\n\n};\n", "import { json as d3_json } from 'd3-fetch';\n\nimport { utilQsString } from '../util';\n\n\nvar endpoint = 'https://en.wikipedia.org/w/api.php?';\n\nexport default {\n\n init: function() {},\n reset: function() {},\n\n\n search: function(lang, query, callback) {\n if (!query) {\n if (callback) callback('No Query', []);\n return;\n }\n\n lang = lang || 'en';\n var url = endpoint.replace('en', lang) +\n utilQsString({\n action: 'query',\n list: 'search',\n srlimit: '10',\n srinfo: 'suggestion',\n format: 'json',\n origin: '*',\n srsearch: query\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n } else if (!result || !result.query || !result.query.search) {\n throw new Error('No Results');\n }\n if (callback) {\n var titles = result.query.search.map(function(d) { return d.title; });\n callback(null, titles);\n }\n })\n .catch(function(err) {\n if (callback) callback(err, []);\n });\n },\n\n\n suggestions: function(lang, query, callback) {\n if (!query) {\n if (callback) callback('', []);\n return;\n }\n\n lang = lang || 'en';\n var url = endpoint.replace('en', lang) +\n utilQsString({\n action: 'opensearch',\n namespace: 0,\n suggest: '',\n format: 'json',\n origin: '*',\n search: query\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n } else if (!result || result.length < 2) {\n throw new Error('No Results');\n }\n if (callback) callback(null, result[1] || []);\n })\n .catch(function(err) {\n if (callback) callback(err.message, []);\n });\n },\n\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport { deepEqual } from 'fast-equals';\nimport Protobuf from 'pbf';\nimport RBush from 'rbush';\nimport { VectorTile } from '@mapbox/vector-tile';\n\nimport { utilRebind, utilTiler, utilQsString, utilStringQs, utilSetTransform } from '../util';\nimport { geoExtent } from '../geo';\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\nimport { localeDateString } from '../util/date';\n\nconst apiUrl = 'https://end.mapilio.com';\nconst imageBaseUrl = 'https://cdn.mapilio.com/im';\nconst baseTileUrl = 'https://geo.mapilio.com/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=mapilio:';\nconst pointLayer = 'map_points';\nconst lineLayer = 'map_roads_line';\nconst tileStyle = '&STYLE=&TILEMATRIX=EPSG:900913:{z}&TILEMATRIXSET=EPSG:900913&FORMAT=application/vnd.mapbox-vector-tile&TILECOL={x}&TILEROW={y}';\n\nconst minZoom = 14;\nconst dispatch = d3_dispatch('loadedImages', 'loadedLines');\nconst imgZoom = d3_zoom()\n .extent([[0, 0], [320, 240]])\n .translateExtent([[0, 0], [320, 240]])\n .scaleExtent([1, 15]);\nconst pannellumViewerCSS = 'pannellum/pannellum.css';\nconst pannellumViewerJS = 'pannellum/pannellum.js';\nconst resolution = 1080;\n\nlet _activeImage;\nlet _cache;\nlet _loadViewerPromise;\nlet _pannellumViewer;\nlet _sceneOptions = {\n showFullscreenCtrl: false,\n autoLoad: true,\n yaw: 0,\n minHfov: 10,\n maxHfov: 90,\n hfov: 60,\n};\nlet _currScene = 0;\n\n// Load all data for the specified type from Mapilio vector tiles\nfunction loadTiles(which, url, maxZoom, projection) {\n const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);\n const tiles = tiler.getTiles(projection);\n\n tiles.forEach(function(tile) {\n loadTile(which, url, tile);\n });\n}\n\n\n// Load all data for the specified type from one vector tile\nfunction loadTile(which, url, tile) {\n const cache = _cache.requests;\n const tileId = `${tile.id}-${which}`;\n if (cache.loaded[tileId] || cache.inflight[tileId]) return;\n const controller = new AbortController();\n cache.inflight[tileId] = controller;\n const requestUrl = url.replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n .replace('{z}', tile.xyz[2]);\n\n fetch(requestUrl, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (data.byteLength === 0) {\n throw new Error('No Data');\n }\n\n loadTileDataToCache(data, tile, which);\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n } else {\n dispatch.call('loadedLines');\n }\n })\n .catch(function (e) {\n if (e.message === 'No Data') {\n cache.loaded[tileId] = true;\n } else {\n console.error(e); // eslint-disable-line no-console\n }\n });\n}\n\n\n// Load the data from the vector tile into cache\nfunction loadTileDataToCache(data, tile) {\n const vectorTile = new VectorTile(new Protobuf(data));\n if (vectorTile.layers.hasOwnProperty(pointLayer)) {\n const features = [];\n const cache = _cache.images;\n const layer = vectorTile.layers[pointLayer];\n\n for (let i = 0; i < layer.length; i++) {\n const feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n const loc = feature.geometry.coordinates;\n\n let resolutionArr = feature.properties.resolution.split('x');\n let sourceWidth = Math.max(resolutionArr[0], resolutionArr[1]);\n let sourceHeight = Math.min(resolutionArr[0] ,resolutionArr[1]);\n let isPano = sourceWidth % sourceHeight === 0;\n\n const d = {\n service: 'photo',\n loc: loc,\n capture_time: feature.properties.capture_time,\n created_by_id: feature.properties.created_by_id,\n id: feature.properties.id,\n sequence_id: feature.properties.sequence_uuid,\n heading: feature.properties.heading,\n resolution: feature.properties.resolution,\n isPano: isPano\n };\n cache.forImageId[d.id] = d;\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty(lineLayer)) {\n const cache = _cache.sequences;\n const layer = vectorTile.layers[lineLayer];\n\n for (let i = 0; i < layer.length; i++) {\n const feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n if (cache.lineString[feature.properties.sequence_uuid]) {\n const cacheEntry = cache.lineString[feature.properties.sequence_uuid];\n if (cacheEntry.some(f => {\n // for some reason, mapilio sometimes returns a large amount of duplicate\n // sequence lines, causing very poor performance. this de-duplicates them,\n // see https://github.com/openstreetmap/iD/issues/10532\n const cachedCoords = f.geometry.coordinates;\n const featureCoords = feature.geometry.coordinates;\n return deepEqual(cachedCoords, featureCoords);\n })) continue;\n cacheEntry.push(feature);\n } else {\n cache.lineString[feature.properties.sequence_uuid] = [feature];\n }\n }\n }\n\n}\n\nfunction getImageData(imageId, sequenceId) {\n\n return fetch(apiUrl + `/api/sequence-detail?sequence_uuid=${sequenceId}`, {method: 'GET'})\n .then(function (response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n })\n .then(function (data) {\n let index = data.data.findIndex((feature) => feature.id === imageId);\n const {filename, uploaded_hash} = data.data[index];\n _sceneOptions.panorama = imageBaseUrl + '/' + uploaded_hash + '/' + filename + '/' + resolution;\n });\n}\n\nfunction getUserData(userId) {\n return fetch(apiUrl + `/api/search-user?options[parameters][id]=${userId}`, {method: 'GET'})\n .then(function (response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n })\n .then(function (data) {\n return data.data[0].username;\n });\n}\n\n\nexport default {\n // Initialize Mapilio\n init: function() {\n if (!_cache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n // Reset cache and state\n reset: function() {\n if (_cache) {\n Object.values(_cache.requests.inflight).forEach(function(request) { request.abort(); });\n }\n\n _cache = {\n images: { rtree: new RBush(), forImageId: {} },\n sequences: { rtree: new RBush(), lineString: {} },\n requests: { loaded: {}, inflight: {} }\n };\n },\n\n // Get visible images\n images: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _cache.images.rtree);\n },\n\n cachedImage: function(imageKey) {\n return _cache.images.forImageId[imageKey];\n },\n\n\n // Load images in the visible area\n loadImages: function(projection) {\n let url = baseTileUrl + pointLayer + tileStyle;\n loadTiles('images', url, 14, projection);\n },\n\n // Load line in the visible area\n loadLines: function(projection) {\n let url = baseTileUrl + lineLayer + tileStyle;\n loadTiles('line', url, 14, projection);\n },\n\n // Get visible sequences\n sequences: function(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const sequenceIds = {};\n let lineStrings = [];\n\n _cache.images.rtree.search(bbox)\n .forEach(function(d) {\n if (d.data.sequence_id) {\n sequenceIds[d.data.sequence_id] = true;\n }\n });\n\n Object.keys(sequenceIds).forEach(function(sequenceId) {\n if (_cache.sequences.lineString[sequenceId]) {\n lineStrings = lineStrings.concat(_cache.sequences.lineString[sequenceId]);\n }\n });\n\n return lineStrings;\n },\n\n // Set the currently visible image\n setActiveImage: function(image) {\n if (image) {\n _activeImage = {\n id: image.id,\n sequence_id: image.sequence_id\n };\n } else {\n _activeImage = null;\n }\n },\n\n\n // Update the currently highlighted sequence and selected bubble.\n setStyles: function(context, hovered) {\n const hoveredImageId = hovered && hovered.id;\n const hoveredSequenceId = hovered && hovered.sequence_id;\n const selectedSequenceId = _activeImage && _activeImage.sequence_id;\n const selectedImageId = _activeImage && _activeImage.id;\n\n const markers = context.container().selectAll('.layer-mapilio .viewfield-group');\n const sequences = context.container().selectAll('.layer-mapilio .sequence');\n\n markers.classed('highlighted', function(d) { return d.id === hoveredImageId; })\n .classed('hovered', function(d) { return d.id === hoveredImageId; })\n .classed('currentView', function(d) { return d.id === selectedImageId; });\n\n sequences.classed('highlighted', function(d) { return d.properties.sequence_uuid === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.sequence_uuid === selectedSequenceId; });\n\n return this;\n },\n\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'mapilio/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n initViewer: function () {\n if (!window.pannellum) return;\n if (_pannellumViewer) return;\n\n _currScene += 1;\n const sceneID = _currScene.toString();\n const options = {\n 'default': { firstScene: sceneID },\n scenes: {}\n };\n options.scenes[sceneID] = _sceneOptions;\n\n _pannellumViewer = window.pannellum.viewer('ideditor-viewer-mapilio-pnlm', options);\n },\n\n selectImage: function (context, id) {\n\n let that = this;\n\n let d = this.cachedImage(id);\n\n this.setActiveImage(d);\n\n this.updateUrlImage(d.id);\n\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null);\n\n if (!d) return this;\n\n let wrap = context.container().select('.photoviewer .mapilio-wrapper');\n let attribution = wrap.selectAll('.photo-attribution').text('\\u00A0');\n\n getUserData(d.created_by_id).then((username) => {\n if (username) {\n attribution\n .append('span')\n .attr('class', 'captured_by')\n .text('@' + username);\n attribution\n .append('span')\n .text('|');\n }\n }).finally(() => {\n if (d.capture_time) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeDateString(d.capture_time));\n attribution\n .append('span')\n .text('|');\n }\n attribution\n .append('a')\n .attr('class', 'image-link')\n .attr('target', '_blank')\n .attr('href', `https://mapilio.com/app?lat=${d.loc[1]}&lng=${d.loc[0]}&zoom=17&pId=${d.id}`)\n .text('mapilio.com');\n });\n\n wrap\n .transition()\n .duration(100)\n .call(imgZoom.transform, d3_zoomIdentity);\n\n wrap\n .selectAll('img')\n .remove();\n\n wrap\n .selectAll('button.back')\n .classed('hide', !_cache.images.forImageId.hasOwnProperty(+id - 1));\n wrap\n .selectAll('button.forward')\n .classed('hide', !_cache.images.forImageId.hasOwnProperty(+id + 1));\n\n\n getImageData(d.id,d.sequence_id).then(function () {\n\n if (d.isPano) {\n if (!_pannellumViewer) {\n that.initViewer();\n } else {\n // make a new scene\n _currScene += 1;\n let sceneID = _currScene.toString();\n _pannellumViewer\n .addScene(sceneID, _sceneOptions)\n .loadScene(sceneID);\n\n // remove previous scene\n if (_currScene > 2) {\n sceneID = (_currScene - 1).toString();\n _pannellumViewer\n .removeScene(sceneID);\n }\n }\n } else {\n // make non-panoramic photo viewer\n that.initOnlyPhoto(context);\n }\n });\n\n return this;\n },\n\n initOnlyPhoto: function (context) {\n\n if (_pannellumViewer) {\n _pannellumViewer.destroy();\n _pannellumViewer = null;\n }\n\n let wrap = context.container().select('#ideditor-viewer-mapilio-simple');\n\n let imgWrap = wrap.select('img');\n\n if (!imgWrap.empty()) {\n imgWrap.attr('src',_sceneOptions.panorama);\n } else {\n wrap.append('img')\n .attr('src',_sceneOptions.panorama);\n }\n\n },\n\n ensureViewerLoaded: function(context) {\n\n let that = this;\n\n let imgWrap = context.container().select('#ideditor-viewer-mapilio-simple > img');\n\n if (!imgWrap.empty()) {\n imgWrap.remove();\n }\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n let wrap = context.container().select('.photoviewer').selectAll('.mapilio-wrapper')\n .data([0]);\n\n let wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper mapilio-wrapper')\n .classed('hide', true)\n .on('dblclick.zoom', null);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n const controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls-mapilio');\n\n controlsEnter\n .append('button')\n .classed('back', true)\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .classed('forward', true)\n .on('click.forward', step(1))\n .text('\u25BA');\n\n wrapEnter\n .append('div')\n .attr('id', 'ideditor-viewer-mapilio-pnlm');\n\n wrapEnter\n .append('div')\n .attr('id', 'ideditor-viewer-mapilio-simple-wrap')\n .call(imgZoom.on('zoom', zoomPan))\n .append('div')\n .attr('id', 'ideditor-viewer-mapilio-simple');\n\n\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.mapilio', () => {\n if (_pannellumViewer) {\n _pannellumViewer.resize();\n }\n });\n\n _loadViewerPromise = new Promise((resolve, reject) => {\n let loadedCount = 0;\n function loaded() {\n loadedCount += 1;\n\n // wait until both files are loaded\n if (loadedCount === 2) resolve();\n }\n\n const head = d3_select('head');\n\n // load pannellum-viewercss\n head.selectAll('#ideditor-mapilio-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-mapilio-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(pannellumViewerCSS))\n .on('load.serviceMapilio', loaded)\n .on('error.serviceMapilio', function() {\n reject();\n });\n\n // load pannellum-viewerjs\n head.selectAll('#ideditor-mapilio-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-mapilio-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(pannellumViewerJS))\n .on('load.serviceMapilio', loaded)\n .on('error.serviceMapilio', function() {\n reject();\n });\n })\n .catch(function() {\n _loadViewerPromise = null;\n });\n\n function step(stepBy) {\n return function () {\n if (!_activeImage) return;\n const imageId = _activeImage.id;\n\n const nextIndex = imageId + stepBy;\n if (!nextIndex) return;\n\n const nextImage = _cache.images.forImageId[nextIndex];\n\n context.map().centerEase(nextImage.loc);\n\n that.selectImage(context, nextImage.id);\n };\n }\n\n function zoomPan(d3_event) {\n var t = d3_event.transform;\n context.container().select('.photoviewer #ideditor-viewer-mapilio-simple')\n .call(utilSetTransform, t.x, t.y, t.k);\n }\n\n return _loadViewerPromise;\n },\n\n showViewer:function (context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.mapilio-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.mapilio-wrapper')\n .classed('hide', false);\n }\n\n return this;\n },\n\n /**\n * hideViewer()\n */\n hideViewer: function (context) {\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n this.updateUrlImage(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n this.setActiveImage();\n return this.setStyles(context, null);\n },\n\n // Return the current cache\n cache: function() {\n return _cache;\n }\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport Protobuf from 'pbf';\nimport RBush from 'rbush';\nimport { VectorTile } from '@mapbox/vector-tile';\nimport { utilRebind, utilTiler, utilQsString, utilStringQs, utilUniqueDomId } from '../util';\nimport { geoExtent } from '../geo';\nimport { t } from '../core/localizer';\nimport { pannellumPhotoFrame } from './pannellum_photo';\nimport { planePhotoFrame } from './plane_photo';\nimport { services } from './';\nimport { partitionViewport } from '../util/partition';\nimport { localeDateString } from '../util/date';\n\n\nconst apiUrl = 'https://api.panoramax.xyz/';\nconst tileUrl = apiUrl + 'api/map/{z}/{x}/{y}.mvt';\nconst imageDataUrl = apiUrl + 'api/collections/{collectionId}/items/{itemId}';\nconst sequenceDataUrl = apiUrl + 'api/collections/{collectionId}/items?limit=1000';\nconst userIdUrl = apiUrl + 'api/users/search?q={username}';\nconst usernameURL = apiUrl + 'api/users/{userId}';\nconst viewerUrl = apiUrl;\n\nconst highDefinition = 'hd';\nconst standardDefinition = 'sd';\n\nconst pictureLayer = 'pictures';\nconst sequenceLayer = 'sequences';\n\nconst minZoom = 10;\nconst imageMinZoom = 15;\nconst lineMinZoom = 10;\nconst dispatch = d3_dispatch('loadedImages', 'loadedLines', 'viewerChanged');\n\nlet _cache;\nlet _loadViewerPromise;\nlet _definition = standardDefinition;\nlet _isHD = false;\n\nlet _planeFrame;\nlet _pannellumFrame;\nlet _currentFrame;\n\nlet _currentScene = {\n currentImage : null,\n nextImage : null,\n prevImage : null\n};\n\nlet _activeImage;\nlet _isViewerOpen = false;\n\n\n/**\n * Return no more than `limit` results per partition.\n * @param {number} limit Number of maximum objects to return\n * @param {*} projection Current projection\n * @param {*} rtree The cache\n * @returns Data found\n */\nfunction searchLimited(limit, projection, rtree) {\n limit = limit || 5;\n\n return partitionViewport(projection)\n .reduce(function(result, extent) {\n let found = rtree.search(extent.bbox());\n const spacing = Math.max(1, Math.floor(found.length / limit));\n found = found\n .filter((d, idx) => idx % spacing === 0 ||\n d.data.id === _activeImage?.id)\n .sort((a, b) => {\n if (a.data.id === _activeImage?.id) return -1;\n if (b.data.id === _activeImage?.id) return 1;\n return 0;\n })\n .slice(0, limit)\n .map(d => d.data);\n\n return (found.length ? result.concat(found) : result);\n }, []);\n}\n\n/**\n * Load all data for the specified type from Panoramax vector tiles\n * @param {string} which Either 'images' or 'lines'\n * @param {string} url Tile endpoint\n * @param {number} maxZoom Maximum zoom out\n * @param {*} projection Current projection\n * @param {number} zoom current zoom\n */\nfunction loadTiles(which, url, maxZoom, projection, zoom) {\n const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);\n const tiles = tiler.getTiles(projection);\n\n tiles.forEach(function(tile) {\n loadTile(which, url, tile, zoom);\n });\n}\n\n/**\n * Load all data for the specified type from one vector tile\n * @param {*} which Either 'images' or 'lines'\n * @param {*} url Tile endpoint\n * @param {*} tile Current tile\n * @param {*} zoom Current zoom\n */\nfunction loadTile(which, url, tile, zoom) {\n const cache = _cache.requests;\n const tileId = `${tile.id}-${which}`;\n if (cache.loaded[tileId] || cache.inflight[tileId]) return;\n const controller = new AbortController();\n cache.inflight[tileId] = controller;\n const requestUrl = url.replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n .replace('{z}', tile.xyz[2]);\n\n fetch(requestUrl, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (data.byteLength === 0) {\n throw new Error('No Data');\n }\n\n loadTileDataToCache(data, tile, zoom);\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n } else {\n dispatch.call('loadedLines');\n }\n })\n .catch(function (e) {\n if (e.message === 'No Data') {\n cache.loaded[tileId] = true;\n } else {\n console.error(e); // eslint-disable-line no-console\n }\n });\n}\n\n/**\n * Fetches all data for the specified tile and adds them to cache\n * @param {*} data Tile data\n * @param {*} tile Current tile\n * @param {*} zoom Current zoom\n */\nfunction loadTileDataToCache(data, tile, zoom) {\n const vectorTile = new VectorTile(new Protobuf(data));\n\n let features,\n cache,\n layer,\n i,\n feature,\n loc,\n d;\n\n if (vectorTile.layers.hasOwnProperty(pictureLayer)) {\n features = [];\n cache = _cache.images;\n layer = vectorTile.layers[pictureLayer];\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n\n d = {\n service: 'photo',\n loc: loc,\n capture_time: feature.properties.ts,\n capture_time_parsed: new Date(feature.properties.ts),\n id: feature.properties.id,\n account_id: feature.properties.account_id,\n sequence_id: feature.properties.first_sequence,\n heading: parseInt(feature.properties.heading, 10),\n image_path: '',\n isPano: feature.properties.type === 'equirectangular',\n model: feature.properties.model,\n };\n cache.forImageId[d.id] = d;\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty(sequenceLayer)) {\n\n cache = _cache.sequences;\n\n if (zoom >= lineMinZoom && zoom < imageMinZoom) cache = _cache.mockSequences;\n\n layer = vectorTile.layers[sequenceLayer];\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n if (cache.lineString[feature.properties.id]) {\n cache.lineString[feature.properties.id].push(feature);\n } else {\n cache.lineString[feature.properties.id] = [feature];\n }\n }\n }\n}\n\n/**\n * Fetches the username from Panoramax\n * @param {string} userId\n * @returns the username\n */\nasync function getUsername(userId) {\n const cache = _cache.users;\n if (cache[userId]) return cache[userId].name;\n\n const requestUrl = usernameURL.replace('{userId}', userId);\n\n const response = await fetch(requestUrl, { method: 'GET' });\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n const data = await response.json();\n cache[userId] = data;\n\n return data.name;\n}\n\nexport default {\n init: function() {\n if (!_cache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset: function() {\n if (_cache) {\n Object.values(_cache.requests.inflight).forEach(function(request) { request.abort(); });\n }\n\n _cache = {\n images: { rtree: new RBush(), forImageId: {} },\n sequences: { rtree: new RBush(), lineString: {}, items: {} },\n users: {},\n mockSequences: { rtree: new RBush(), lineString: {} },\n requests: { loaded: {}, inflight: {} }\n };\n },\n\n /**\n * Get visible images from cache\n * @param {*} projection Current Projection\n * @returns images data for the current projection\n */\n images: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _cache.images.rtree);\n },\n\n /**\n * Get a specific image from cache\n * @param {*} imageKey the image id\n * @returns\n */\n cachedImage: function(imageKey) {\n return _cache.images.forImageId[imageKey];\n },\n\n /**\n * Fetches images data for the visible area\n * @param {*} projection Current Projection\n */\n loadImages: function(projection) {\n loadTiles('images', tileUrl, imageMinZoom, projection);\n },\n\n /**\n * Fetches sequences data for the visible area\n * @param {*} projection Current Projection\n */\n loadLines: function(projection, zoom) {\n loadTiles('line', tileUrl, lineMinZoom, projection, zoom);\n },\n\n /**\n * Fetches all possible userIDs from Panoramax\n * @param {string} usernames one or multiple usernames\n * @returns userIDs\n */\n getUserIds: async function(usernames) {\n const requestUrls = usernames.map(username =>\n userIdUrl.replace('{username}', username));\n\n const responses = await Promise.all(requestUrls.map(requestUrl =>\n fetch(requestUrl, { method: 'GET' })));\n if (responses.some(response => !response.ok)) {\n const response = responses.find(response => !response.ok);\n throw new Error(response.status + ' ' + response.statusText);\n }\n const data = await Promise.all(responses.map(response => response.json()));\n // in panoramax, a username can have multiple ids, when the same name is\n // used on different servers\n return data.flatMap((d, i) => d.features.filter(f => f.name === usernames[i]).map(f => f.id));\n },\n\n /**\n * Get visible sequences from cache\n * @param {*} projection Current Projection\n * @param {number} zoom Current zoom (if zoom < `lineMinZoom` less accurate lines will be drawn)\n * @returns sequences data for the current projection\n */\n sequences: function(projection, zoom) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const sequenceIds = {};\n let lineStrings = [];\n\n if (zoom >= imageMinZoom){\n _cache.images.rtree.search(bbox).forEach(function(d) {\n if (d.data.sequence_id) {\n sequenceIds[d.data.sequence_id] = true;\n }\n });\n Object.keys(sequenceIds).forEach(function(sequenceId) {\n if (_cache.sequences.lineString[sequenceId]) {\n lineStrings = lineStrings.concat(_cache.sequences.lineString[sequenceId]);\n }\n });\n return lineStrings;\n }\n if (zoom >= lineMinZoom){\n Object.keys(_cache.mockSequences.lineString).forEach(function(sequenceId) {\n lineStrings = lineStrings.concat(_cache.mockSequences.lineString[sequenceId]);\n });\n }\n return lineStrings;\n },\n\n /**\n * Updates the data for the currently visible image\n * @param {*} image Image data\n */\n setActiveImage: function(image) {\n if (image && image.id && image.sequence_id) {\n _activeImage = {\n id: image.id,\n sequence_id: image.sequence_id,\n loc: image.loc\n };\n } else {\n _activeImage = null;\n }\n },\n\n getActiveImage: function(){\n return _activeImage;\n },\n\n /**\n * Update the currently highlighted sequence and selected bubble\n * @param {*} context Current HTML context\n * @param {*} [hovered] The hovered bubble image\n */\n setStyles: function(context, hovered) {\n const hoveredImageId = hovered && hovered.id;\n const hoveredSequenceId = hovered && hovered.sequence_id;\n const selectedSequenceId = _activeImage && _activeImage.sequence_id;\n const selectedImageId = _activeImage && _activeImage.id;\n\n const markers = context.container().selectAll('.layer-panoramax .viewfield-group');\n const sequences = context.container().selectAll('.layer-panoramax .sequence');\n\n markers\n .classed('highlighted', function(d) { return d.sequence_id === selectedSequenceId || d.id === hoveredImageId; })\n .classed('hovered', function(d) { return d.id === hoveredImageId; })\n .classed('currentView', function(d) { return d.id === selectedImageId; });\n\n sequences\n .classed('highlighted', function(d) { return d.properties.id === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.id === selectedSequenceId; });\n\n // update viewfields if needed\n context.container().selectAll('.layer-panoramax .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n let d = this.parentNode.__data__;\n if (d.isPano && d.id !== selectedImageId) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n return this;\n },\n\n // Get viewer status\n isViewerOpen: function() {\n return _isViewerOpen;\n },\n\n /**\n * Updates the URL to save the current shown image\n * @param {*} imageKey\n */\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'panoramax/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n /**\n * Loads the selected image in the frame\n * @param {*} context Current HTML context\n * @param {*} id of the selected image\n * @returns\n */\n selectImage: function (context, id) {\n let that = this;\n\n let d = that.cachedImage(id);\n that.setActiveImage(d);\n that.updateUrlImage(d.id);\n\n const viewerLink = `${viewerUrl}#pic=${d.id}&focus=pic`;\n\n let viewer = context.container()\n .select('.photoviewer');\n\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null);\n\n if (!d) return this;\n\n let wrap = context.container()\n .select('.photoviewer .panoramax-wrapper');\n\n let attribution = wrap.selectAll('.photo-attribution').text('');\n\n let line1 = attribution\n .append('div')\n .attr('class', 'attribution-row');\n\n const hdDomId = utilUniqueDomId('panoramax-hd');\n\n let label = line1\n .append('label')\n .attr('for', hdDomId)\n .attr('class', 'panoramax-hd');\n\n label\n .append('input')\n .attr('type', 'checkbox')\n .attr('id', hdDomId)\n .property('checked', _isHD)\n .on('click', (d3_event) => {\n d3_event.stopPropagation();\n _isHD = !_isHD;\n _definition = _isHD ? highDefinition : standardDefinition;\n that.selectImage(context, d.id)\n .showViewer(context);\n });\n\n label\n .append('span')\n .call(t.append('panoramax.hd'));\n\n if (d.capture_time) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeDateString(d.capture_time));\n\n attribution\n .append('span')\n .text('|');\n }\n\n attribution\n .append('a')\n .attr('class', 'report-photo')\n .attr('href', 'mailto:signalement.ign@panoramax.fr')\n .call(t.append('panoramax.report'));\n\n attribution\n .append('span')\n .text('|');\n\n attribution\n .append('a')\n .attr('class', 'image-link')\n .attr('target', '_blank')\n .attr('href', viewerLink)\n .text('panoramax.xyz');\n\n this.getImageData(d.sequence_id, d.id).then(function(data) {\n _currentScene = {\n currentImage: null,\n nextImage: null,\n prevImage: null\n };\n _currentScene.currentImage = data.assets[_definition];\n const nextIndex = data.links.findIndex(x => x.rel === 'next');\n const prevIndex = data.links.findIndex(x => x.rel === 'prev');\n\n if (nextIndex !== -1){\n _currentScene.nextImage = data.links[nextIndex];\n }\n if (prevIndex !== -1){\n _currentScene.prevImage = data.links[prevIndex];\n }\n\n d.image_path = _currentScene.currentImage.href;\n\n wrap\n .selectAll('button.back')\n .classed('hide', _currentScene.prevImage === null);\n wrap\n .selectAll('button.forward')\n .classed('hide', _currentScene.nextImage === null);\n\n _currentFrame = d.isPano ? _pannellumFrame : _planeFrame;\n\n _currentFrame\n .showPhotoFrame(wrap)\n .selectPhoto(d, true);\n });\n\n if (d.account_id) {\n attribution\n .append('span')\n .text('|');\n\n let line2 = attribution\n .append('span')\n .attr('class', 'attribution-row');\n\n getUsername(d.account_id).then(function(username){\n line2\n .append('span')\n .attr('class', 'captured_by')\n .text('@' + username);\n });\n }\n\n return this;\n },\n\n photoFrame: function() {\n return _currentFrame;\n },\n\n /**\n * Fetches the data for a specific image\n * @param {*} collectionId\n * @param {*} imageId\n * @returns The fetched image data\n */\n getImageData: async function(collectionId, imageId) {\n const cache = _cache.sequences.items;\n if (cache[collectionId]) {\n const cached = cache[collectionId]\n .find(d => d.id === imageId);\n if (cached) return cached;\n } else {\n // prime the cache with data from sequence\n const response = await fetch(sequenceDataUrl\n .replace('{collectionId}', collectionId),\n { method: 'GET' });\n\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n const data = (await response.json()).features;\n cache[collectionId] = data;\n }\n\n const result = cache[collectionId]\n .find(d => d.id === imageId);\n if (result) return result;\n\n // not found in sequence: retry to load single item data\n // ideally, we'd use the `withPicture` parameter, but it is buggy:\n // https://gitlab.com/panoramax/server/api/-/issues/268\n const itemResponse = await fetch(imageDataUrl\n .replace('{collectionId}', collectionId)\n .replace('{itemId}', imageId),\n { method: 'GET' });\n\n if (!itemResponse.ok) {\n throw new Error(itemResponse.status + ' ' + itemResponse.statusText);\n }\n const itemData = await itemResponse.json();\n cache[collectionId].push(itemData);\n return itemData;\n },\n\n ensureViewerLoaded: function(context) {\n\n let that = this;\n\n let imgWrap = context.container()\n .select('#ideditor-viewer-panoramax-simple > img');\n\n if (!imgWrap.empty()) {\n imgWrap.remove();\n }\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n let wrap = context.container()\n .select('.photoviewer')\n .selectAll('.panoramax-wrapper')\n .data([0]);\n\n let wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper panoramax-wrapper')\n .classed('hide', true)\n .on('dblclick.zoom', null);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n const controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls-panoramax');\n\n controlsEnter\n .append('button')\n .classed('back', true)\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .classed('forward', true)\n .on('click.forward', step(1))\n .text('\u25BA');\n\n // Register viewer resize handler\n _loadViewerPromise = Promise.all([\n pannellumPhotoFrame(context, wrapEnter),\n planePhotoFrame(context, wrapEnter)\n ]).then(([pannellumPhotoFrame, planePhotoFrame]) => {\n _pannellumFrame = pannellumPhotoFrame;\n _pannellumFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n _planeFrame = planePhotoFrame;\n _planeFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n });\n\n /**\n * Loads the next image in the sequence\n * @param {number} stepBy '-1' if backwards or '1' if forward\n * @returns\n */\n function step(stepBy) {\n return function () {\n if (!_currentScene.currentImage) return;\n\n let nextId;\n if (stepBy === 1) nextId = _currentScene.nextImage.id;\n else nextId = _currentScene.prevImage.id;\n\n if (!nextId) return;\n\n const nextImage = _cache.images.forImageId[nextId];\n\n if (nextImage){\n context.map().centerEase(nextImage.loc);\n that.selectImage(context, nextImage.id);\n }\n };\n }\n\n return _loadViewerPromise;\n },\n\n /**\n * Shows the current viewer if hidden\n * @param {*} context\n */\n showViewer: function (context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.panoramax-wrapper.hide').size();\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.panoramax-wrapper')\n .classed('hide', false);\n }\n\n _isViewerOpen = true;\n return this;\n },\n\n /**\n * Hides the current viewer if shown, resets the active image and sequence\n * @param {*} context\n */\n hideViewer: function (context) {\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n this.updateUrlImage(null);\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n this.setActiveImage(null);\n _isViewerOpen = false;\n\n return this.setStyles(context, null);\n },\n\n cache: function() {\n return _cache;\n }\n};\n", "import serviceOsmose from './osmose';\nimport serviceMapillary from './mapillary';\nimport serviceMapRules from './maprules';\nimport serviceNominatim from './nominatim';\nimport serviceNsi from './nsi';\nimport serviceKartaview from './kartaview';\nimport serviceVegbilder from './vegbilder';\nimport serviceOsm from './osm';\nimport serviceOsmWikibase from './osm_wikibase';\nimport serviceStreetside from './streetside';\nimport serviceTaginfo from './taginfo';\nimport serviceVectorTile from './vector_tile';\nimport serviceWikidata from './wikidata';\nimport serviceWikipedia from './wikipedia';\nimport serviceMapilio from './mapilio';\nimport servicePanoramax from './panoramax';\n\n\nexport let services = {\n geocoder: serviceNominatim,\n osmose: serviceOsmose,\n mapillary: serviceMapillary,\n nsi: serviceNsi,\n kartaview: serviceKartaview,\n vegbilder: serviceVegbilder,\n osm: serviceOsm,\n osmWikibase: serviceOsmWikibase,\n maprules: serviceMapRules,\n streetside: serviceStreetside,\n taginfo: serviceTaginfo,\n vectorTile: serviceVectorTile,\n wikidata: serviceWikidata,\n wikipedia: serviceWikipedia,\n mapilio: serviceMapilio,\n panoramax: servicePanoramax\n};\n\nexport {\n serviceOsmose,\n serviceMapillary,\n serviceMapRules,\n serviceNominatim,\n serviceNsi,\n serviceKartaview,\n serviceVegbilder,\n serviceOsm,\n serviceOsmWikibase,\n serviceStreetside,\n serviceTaginfo,\n serviceVectorTile,\n serviceWikidata,\n serviceWikipedia,\n serviceMapilio,\n servicePanoramax\n};\n", "import {\n geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon,\n geoSphericalDistance, geoVecInterp, geoHasSelfIntersections,\n geoSphericalClosestNode, geoAngle\n} from '../geo';\n\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionMergeNodes } from '../actions/merge_nodes';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilDatesOverlap } from '../util';\nimport { osmRoutableHighwayTagValues } from '../osm/tags';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\n\n/**\n * Look for roads that can be connected to other roads with a short extension\n */\nexport function validationAlmostJunction(context) {\n const type = 'almost_junction';\n const EXTEND_TH_METERS = 5;\n const WELD_TH_METERS = 0.75;\n // Comes from considering bounding case of parallel ways\n const CLOSE_NODE_TH = EXTEND_TH_METERS - WELD_TH_METERS;\n // Comes from considering bounding case of perpendicular ways\n const SIG_ANGLE_TH = Math.atan(WELD_TH_METERS / EXTEND_TH_METERS);\n\n function isHighway(entity) {\n return entity.type === 'way'\n && osmRoutableHighwayTagValues[entity.tags.highway];\n }\n\n function isTaggedAsNotContinuing(node) {\n return node.tags.noexit === 'yes'\n || node.tags.amenity === 'parking_entrance'\n || (node.tags.entrance && node.tags.entrance !== 'no');\n }\n\n\n const validation = function checkAlmostJunction(entity, graph) {\n if (!isHighway(entity)) return [];\n if (entity.isDegenerate()) return [];\n\n const tree = context.history().tree();\n const extendableNodeInfos = findConnectableEndNodesByExtension(entity);\n\n let issues = [];\n\n extendableNodeInfos.forEach(extendableNodeInfo => {\n issues.push(new validationIssue({\n type,\n subtype: 'highway-highway',\n severity: 'warning',\n message: function(context) {\n const entity1 = context.hasEntity(this.entityIds[0]);\n if (this.entityIds[0] === this.entityIds[2]) {\n return entity1 ? t.append('issues.almost_junction.self.message', {\n feature: utilDisplayLabel(entity1, context.graph())\n }) : '';\n } else {\n const entity2 = context.hasEntity(this.entityIds[2]);\n return (entity1 && entity2) ? t.append('issues.almost_junction.message', {\n feature: utilDisplayLabel(entity1, context.graph()),\n feature2: utilDisplayLabel(entity2, context.graph())\n }) : '';\n }\n },\n reference: showReference,\n entityIds: [\n entity.id,\n extendableNodeInfo.node.id,\n extendableNodeInfo.wid,\n ],\n loc: extendableNodeInfo.node.loc,\n hash: JSON.stringify(extendableNodeInfo.node.loc),\n data: {\n midId: extendableNodeInfo.mid.id,\n edge: extendableNodeInfo.edge,\n cross_loc: extendableNodeInfo.cross_loc\n },\n dynamicFixes: makeFixes\n }));\n });\n\n return issues;\n\n function makeFixes(context) {\n let fixes = [new validationIssueFix({\n icon: 'iD-icon-abutment',\n title: t.append('issues.fix.connect_features.title'),\n onClick: function(context) {\n const annotation = t('issues.fix.connect_almost_junction.annotation');\n const [, endNodeId, crossWayId] = this.issue.entityIds;\n const midNode = context.entity(this.issue.data.midId);\n const endNode = context.entity(endNodeId);\n const crossWay = context.entity(crossWayId);\n\n // When endpoints are close, just join if resulting small change in angle (#7201)\n const nearEndNodes = findNearbyEndNodes(endNode, crossWay);\n if (nearEndNodes.length > 0) {\n const collinear = findSmallJoinAngle(midNode, endNode, nearEndNodes);\n if (collinear) {\n context.perform(\n actionMergeNodes([collinear.id, endNode.id], collinear.loc),\n annotation\n );\n return;\n }\n }\n\n const targetEdge = this.issue.data.edge;\n const crossLoc = this.issue.data.cross_loc;\n const edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])];\n const closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc);\n\n // already a point nearby, just connect to that\n if (closestNodeInfo.distance < WELD_TH_METERS) {\n context.perform(\n actionMergeNodes([closestNodeInfo.node.id, endNode.id], closestNodeInfo.node.loc),\n annotation\n );\n // else add the end node to the edge way\n } else {\n context.perform(\n actionAddMidpoint({loc: crossLoc, edge: targetEdge}, endNode),\n annotation\n );\n }\n }\n })];\n\n const node = context.hasEntity(this.entityIds[1]);\n if (node && !node.hasInterestingTags()) {\n // node has no descriptive tags, suggest noexit fix\n fixes.push(new validationIssueFix({\n icon: 'maki-barrier',\n title: t.append('issues.fix.tag_as_disconnected.title'),\n onClick: function(context) {\n const nodeID = this.issue.entityIds[1];\n const tags = Object.assign({}, context.entity(nodeID).tags);\n tags.noexit = 'yes';\n context.perform(\n actionChangeTags(nodeID, tags),\n t('issues.fix.tag_as_disconnected.annotation')\n );\n }\n }));\n }\n\n // changing the date range is always an option\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-change-date-range',\n title: t.append('issues.fix.change_date_range.title')\n }));\n\n return fixes;\n }\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.almost_junction.highway-highway.reference'));\n }\n\n function isExtendableCandidate(node, way) {\n // can not accurately test vertices on tiles not downloaded from osm - #5938\n const osm = services.osm;\n if (osm && !osm.isDataLoaded(node.loc)) {\n return false;\n }\n if (isTaggedAsNotContinuing(node) || graph.parentWays(node).length !== 1) {\n return false;\n }\n\n let occurrences = 0;\n for (const index in way.nodes) {\n if (way.nodes[index] === node.id) {\n occurrences += 1;\n if (occurrences > 1) {\n return false;\n }\n }\n }\n return true;\n }\n\n function findConnectableEndNodesByExtension(way) {\n let results = [];\n if (way.isClosed()) return results;\n\n let testNodes;\n const indices = [0, way.nodes.length - 1];\n indices.forEach(nodeIndex => {\n const nodeID = way.nodes[nodeIndex];\n const node = graph.entity(nodeID);\n\n if (!isExtendableCandidate(node, way)) return;\n\n const connectionInfo = canConnectByExtend(way, nodeIndex);\n if (!connectionInfo) return;\n\n testNodes = graph.childNodes(way).slice(); // shallow copy\n testNodes[nodeIndex] = testNodes[nodeIndex].move(connectionInfo.cross_loc);\n\n // don't flag issue if connecting the ways would cause self-intersection\n if (geoHasSelfIntersections(testNodes, nodeID)) return;\n\n results.push(connectionInfo);\n });\n\n return results;\n }\n\n function findNearbyEndNodes(node, way) {\n return [\n way.nodes[0],\n way.nodes[way.nodes.length - 1]\n ].map(d => graph.entity(d))\n .filter(d => {\n // Node cannot be near to itself, but other endnode of same way could be\n return d.id !== node.id\n && geoSphericalDistance(node.loc, d.loc) <= CLOSE_NODE_TH;\n });\n }\n\n function findSmallJoinAngle(midNode, tipNode, endNodes) {\n // Both nodes could be close, so want to join whichever is closest to collinear\n let joinTo;\n let minAngle = Infinity;\n\n // Checks midNode -> tipNode -> endNode for collinearity\n endNodes.forEach(endNode => {\n const a1 = geoAngle(midNode, tipNode, context.projection) + Math.PI;\n const a2 = geoAngle(midNode, endNode, context.projection) + Math.PI;\n const diff = Math.max(a1, a2) - Math.min(a1, a2);\n\n if (diff < minAngle) {\n joinTo = endNode;\n minAngle = diff;\n }\n });\n\n /* Threshold set by considering right angle triangle\n based on node joining threshold and extension distance */\n if (minAngle <= SIG_ANGLE_TH) return joinTo;\n\n return null;\n }\n\n function hasTag(tags, key) {\n return tags[key] !== undefined && tags[key] !== 'no';\n }\n\n function canConnectWays(way, way2) {\n\n // allow self-connections\n if (way.id === way2.id) return true;\n\n // if one is bridge or tunnel, both must be bridge or tunnel\n if ((hasTag(way.tags, 'bridge') || hasTag(way2.tags, 'bridge')) &&\n !(hasTag(way.tags, 'bridge') && hasTag(way2.tags, 'bridge'))) return false;\n if ((hasTag(way.tags, 'tunnel') || hasTag(way2.tags, 'tunnel')) &&\n !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false;\n\n // must have equivalent layers and levels\n const layer1 = way.tags.layer || '0',\n layer2 = way2.tags.layer || '0';\n if (layer1 !== layer2) return false;\n\n const level1 = way.tags.level || '0',\n level2 = way2.tags.level || '0';\n if (level1 !== level2) return false;\n\n // must have overlapping date ranges\n if ((way.tags.start_date || way.tags.end_date) && (way2.tags.start_date || way2.tags.end_date)) {\n if (!utilDatesOverlap(way.tags, way2.tags)) return false;\n }\n\n return true;\n }\n\n function canConnectByExtend(way, endNodeIdx) {\n const tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point\n const midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge\n const tipNode = graph.entity(tipNid);\n const midNode = graph.entity(midNid);\n const lon = tipNode.loc[0];\n const lat = tipNode.loc[1];\n const lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2;\n const lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2;\n const queryExtent = geoExtent([\n [lon - lon_range, lat - lat_range],\n [lon + lon_range, lat + lat_range]\n ]);\n\n // first, extend the edge of [midNode -> tipNode] by EXTEND_TH_METERS and find the \"extended tip\" location\n const edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc);\n const t = EXTEND_TH_METERS / edgeLen + 1.0;\n const extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t);\n\n // then, check if the extension part [tipNode.loc -> extTipLoc] intersects any other ways\n const segmentInfos = tree.waySegments(queryExtent, graph);\n for (let i = 0; i < segmentInfos.length; i++) {\n let segmentInfo = segmentInfos[i];\n\n let way2 = graph.entity(segmentInfo.wayId);\n\n if (!isHighway(way2)) continue;\n\n if (!canConnectWays(way, way2)) continue;\n\n let nAid = segmentInfo.nodes[0],\n nBid = segmentInfo.nodes[1];\n\n if (nAid === tipNid || nBid === tipNid) continue;\n\n let nA = graph.entity(nAid),\n nB = graph.entity(nBid);\n let crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]);\n if (crossLoc) {\n return {\n mid: midNode,\n node: tipNode,\n wid: way2.id,\n edge: [nA.id, nB.id],\n cross_loc: crossLoc\n };\n }\n }\n return null;\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionMergeNodes } from '../actions/merge_nodes';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilDatesOverlap } from '../util';\nimport { t } from '../core/localizer';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { osmPathHighwayTagValues } from '../osm/tags';\nimport { geoMetersToLat, geoMetersToLon, geoSphericalDistance } from '../geo/geo';\nimport { geoExtent } from '../geo/extent';\n\nexport function validationCloseNodes(context) {\n var type = 'close_nodes';\n\n var pointThresholdMeters = 0.2;\n\n var validation = function(entity, graph) {\n if (entity.type === 'node') {\n return getIssuesForNode(entity);\n } else if (entity.type === 'way') {\n return getIssuesForWay(entity);\n }\n return [];\n\n function getIssuesForNode(node) {\n var parentWays = graph.parentWays(node);\n if (parentWays.length) {\n return getIssuesForVertex(node, parentWays);\n } else {\n return getIssuesForDetachedPoint(node);\n }\n }\n\n function wayTypeFor(way) {\n\n if (way.tags.boundary && way.tags.boundary !== 'no') return 'boundary';\n if (way.tags.indoor && way.tags.indoor !== 'no') return 'indoor';\n if ((way.tags.building && way.tags.building !== 'no') ||\n (way.tags['building:part'] && way.tags['building:part'] !== 'no')) return 'building';\n if (osmPathHighwayTagValues[way.tags.highway]) return 'path';\n\n var parentRelations = graph.parentRelations(way);\n for (var i in parentRelations) {\n var relation = parentRelations[i];\n\n if (relation.tags.type === 'boundary') return 'boundary';\n\n if (relation.isMultipolygon()) {\n if (relation.tags.indoor && relation.tags.indoor !== 'no') return 'indoor';\n if ((relation.tags.building && relation.tags.building !== 'no') ||\n (relation.tags['building:part'] && relation.tags['building:part'] !== 'no')) return 'building';\n }\n }\n\n return 'other';\n }\n\n function shouldCheckWay(way) {\n\n // don't flag issues where merging would create degenerate ways\n if (way.nodes.length <= 2 ||\n (way.isClosed() && way.nodes.length <= 4)) return false;\n\n var bbox = way.extent(graph).bbox();\n var hypotenuseMeters = geoSphericalDistance([bbox.minX, bbox.minY], [bbox.maxX, bbox.maxY]);\n // don't flag close nodes in very small ways\n if (hypotenuseMeters < 1.5) return false;\n\n return true;\n }\n\n function getIssuesForWay(way) {\n if (!shouldCheckWay(way)) return [];\n\n var issues = [],\n nodes = graph.childNodes(way);\n for (var i = 0; i < nodes.length - 1; i++) {\n var node1 = nodes[i];\n var node2 = nodes[i+1];\n\n var issue = getWayIssueIfAny(node1, node2, way);\n if (issue) issues.push(issue);\n }\n return issues;\n }\n\n function getIssuesForVertex(node, parentWays) {\n var issues = [];\n\n function checkForCloseness(node1, node2, way) {\n var issue = getWayIssueIfAny(node1, node2, way);\n if (issue) issues.push(issue);\n }\n\n for (var i = 0; i < parentWays.length; i++) {\n var parentWay = parentWays[i];\n\n if (!shouldCheckWay(parentWay)) continue;\n\n var lastIndex = parentWay.nodes.length - 1;\n for (var j = 0; j < parentWay.nodes.length; j++) {\n if (j !== 0) {\n if (parentWay.nodes[j-1] === node.id) {\n checkForCloseness(node, graph.entity(parentWay.nodes[j]), parentWay);\n }\n }\n if (j !== lastIndex) {\n if (parentWay.nodes[j+1] === node.id) {\n checkForCloseness(graph.entity(parentWay.nodes[j]), node, parentWay);\n }\n }\n }\n }\n return issues;\n }\n\n function thresholdMetersForWay(way) {\n if (!shouldCheckWay(way)) return 0;\n\n var wayType = wayTypeFor(way);\n\n // don't flag boundaries since they might be highly detailed and can't be easily verified\n if (wayType === 'boundary') return 0;\n // expect some features to be mapped with higher levels of detail\n if (wayType === 'indoor') return 0.01;\n if (wayType === 'building') return 0.05;\n if (wayType === 'path') return 0.1;\n return 0.2;\n }\n\n function getIssuesForDetachedPoint(node) {\n\n var issues = [];\n\n var lon = node.loc[0];\n var lat = node.loc[1];\n var lon_range = geoMetersToLon(pointThresholdMeters, lat) / 2;\n var lat_range = geoMetersToLat(pointThresholdMeters) / 2;\n var queryExtent = geoExtent([\n [lon - lon_range, lat - lat_range],\n [lon + lon_range, lat + lat_range]\n ]);\n\n var intersected = context.history().tree().intersects(queryExtent, graph);\n for (var j = 0; j < intersected.length; j++) {\n var nearby = intersected[j];\n\n if (nearby.id === node.id) continue;\n if (nearby.type !== 'node' || nearby.geometry(graph) !== 'point') continue;\n\n if (nearby.loc === node.loc ||\n geoSphericalDistance(node.loc, nearby.loc) < pointThresholdMeters) {\n\n // ignore stolperstein (https://wiki.openstreetmap.org/wiki/DE:Stolpersteine)\n if ('memorial:type' in node.tags && 'memorial:type' in nearby.tags && node.tags['memorial:type']==='stolperstein' && nearby.tags['memorial:type']==='stolperstein') continue;\n if ('memorial' in node.tags && 'memorial' in nearby.tags && node.tags.memorial==='stolperstein' && nearby.tags.memorial === 'stolperstein') continue;\n\n // allow very close points if tags indicate the z-axis might vary\n var zAxisKeys = { layer: true, level: true, 'addr:housenumber': true, 'addr:unit': true };\n var zAxisDifferentiates = false;\n for (var key in zAxisKeys) {\n var nodeValue = node.tags[key] || '0';\n var nearbyValue = nearby.tags[key] || '0';\n if (nodeValue !== nearbyValue) {\n zAxisDifferentiates = true;\n break;\n }\n }\n if (zAxisDifferentiates) continue;\n\n // allow features to overlap spatially if they don't overlap temporally\n if ((node.tags.start_date || node.tags.end_date) && (nearby.tags.start_date || nearby.tags.end_date)) {\n if (!utilDatesOverlap(node.tags, nearby.tags)) continue;\n }\n\n issues.push(new validationIssue({\n type: type,\n subtype: 'detached',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]),\n entity2 = context.hasEntity(this.entityIds[1]);\n return (entity && entity2) ? t.append('issues.close_nodes.detached.message', {\n feature: utilDisplayLabel(entity, context.graph()),\n feature2: utilDisplayLabel(entity2, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [node.id, nearby.id],\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-disconnect',\n title: t.append('issues.fix.move_points_apart.title')\n }),\n new validationIssueFix({\n icon: 'iD-icon-layers',\n title: t.append('issues.fix.use_different_layers_or_levels.title')\n }),\n new validationIssueFix({\n icon: 'iD-operation-change-date-range',\n title: t.append('issues.fix.change_date_range.title')\n })\n ];\n }\n }));\n }\n }\n\n return issues;\n\n function showReference(selection) {\n var referenceText = t('issues.close_nodes.detached.reference');\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .html(referenceText);\n }\n }\n\n function getWayIssueIfAny(node1, node2, way) {\n if (node1.id === node2.id ||\n (node1.hasInterestingTags() && node2.hasInterestingTags())) {\n return null;\n }\n\n if (node1.loc !== node2.loc) {\n var parentWays1 = graph.parentWays(node1);\n var parentWays2 = new Set(graph.parentWays(node2));\n\n var sharedWays = parentWays1.filter(function(parentWay) {\n return parentWays2.has(parentWay);\n });\n\n var thresholds = sharedWays.map(function(parentWay) {\n return thresholdMetersForWay(parentWay);\n });\n\n var threshold = Math.min(...thresholds);\n var distance = geoSphericalDistance(node1.loc, node2.loc);\n if (distance > threshold) return null;\n }\n\n return new validationIssue({\n type: type,\n subtype: 'vertices',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.close_nodes.message', { way: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: showReference,\n entityIds: [way.id, node1.id, node2.id],\n loc: node1.loc,\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-icon-plus',\n title: t.append('issues.fix.merge_points.title'),\n onClick: function(context) {\n var entityIds = this.issue.entityIds;\n var action = actionMergeNodes([entityIds[1], entityIds[2]]);\n context.perform(action, t('issues.fix.merge_close_vertices.annotation'));\n }\n }),\n new validationIssueFix({\n icon: 'iD-operation-disconnect',\n title: t.append('issues.fix.move_points_apart.title')\n })\n ];\n }\n });\n\n function showReference(selection) {\n var referenceText = t('issues.close_nodes.reference');\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .html(referenceText);\n }\n }\n\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { deepEqual } from 'fast-equals';\n\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionMergeNodes } from '../actions/merge_nodes';\nimport { actionSplit } from '../actions/split';\nimport { modeSelect } from '../modes/select';\nimport { geoAngle, geoExtent, geoLatToMeters, geoLonToMeters, geoLineIntersection,\n geoSphericalClosestNode, geoSphericalDistance, geoVecAngle, geoVecLength, geoMetersToLat, geoMetersToLon } from '../geo';\nimport { osmNode } from '../osm/node';\nimport { osmFlowingWaterwayTagValues, osmPathHighwayTagValues, osmRailwayTrackTagValues, osmRoutableAerowayTags, osmRoutableHighwayTagValues } from '../osm/tags';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilDatesOverlap } from '../util';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationCrossingWays(context) {\n var type = 'crossing_ways';\n\n // returns the way or its parent relation, whichever has a useful feature type\n function getFeatureWithFeatureTypeTagsForWay(way, graph) {\n if (getFeatureType(way, graph) === null) {\n // if the way doesn't match a feature type, check its parent relations\n var parentRels = graph.parentRelations(way);\n for (var i = 0; i < parentRels.length; i++) {\n var rel = parentRels[i];\n if (getFeatureType(rel, graph) !== null) {\n return rel;\n }\n }\n }\n return way;\n }\n\n\n function hasTag(tags, key) {\n return tags[key] !== undefined && tags[key] !== 'no';\n }\n\n function taggedAsIndoor(tags) {\n return hasTag(tags, 'indoor') ||\n hasTag(tags, 'level') ||\n tags.highway === 'corridor';\n }\n\n function allowsBridge(featureType) {\n return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway' || featureType === 'aeroway';\n }\n function allowsTunnel(featureType) {\n return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway';\n }\n\n // discard\n var ignoredBuildings = {\n demolished: true, dismantled: true, proposed: true, razed: true\n };\n\n\n function getFeatureType(entity, graph) {\n\n var geometry = entity.geometry(graph);\n if (geometry !== 'line' && geometry !== 'area') return null;\n\n var tags = entity.tags;\n\n if (tags.aeroway in osmRoutableAerowayTags) return 'aeroway';\n\n if (hasTag(tags, 'building') && !ignoredBuildings[tags.building]) return 'building';\n if (hasTag(tags, 'highway') && osmRoutableHighwayTagValues[tags.highway]) return 'highway';\n\n // don't check railway or waterway areas\n if (geometry !== 'line') return null;\n\n if (hasTag(tags, 'railway') && osmRailwayTrackTagValues[tags.railway]) return 'railway';\n if (hasTag(tags, 'waterway') && osmFlowingWaterwayTagValues[tags.waterway]) return 'waterway';\n\n return null;\n }\n\n\n function isLegitCrossing(tags1, featureType1, tags2, featureType2) {\n\n // assume 0 by default\n var level1 = tags1.level || '0';\n var level2 = tags2.level || '0';\n\n if (taggedAsIndoor(tags1) && taggedAsIndoor(tags2) && level1 !== level2) {\n // assume features don't interact if they're indoor on different levels\n return true;\n }\n\n // don't flag crossing waterways and pier/highways\n if (featureType1 === 'waterway' && featureType2 === 'highway' && tags2.man_made === 'pier') return true;\n if (featureType2 === 'waterway' && featureType1 === 'highway' && tags1.man_made === 'pier') return true;\n\n // allow features to overlap spatially if they don't overlap temporally\n if ((tags1.start_date || tags1.end_date) && (tags2.start_date || tags2.end_date)) {\n if (!utilDatesOverlap(tags1, tags2)) return true;\n }\n\n if (tags1.layer !== undefined && tags1.layer === tags2.layer) return false; // Warn if both have the same defined layer\n\n const isElement1Bridge = allowsBridge(featureType1) && hasTag(tags1, 'bridge');\n const isElement2Bridge = allowsBridge(featureType2) && hasTag(tags2, 'bridge');\n if (isElement1Bridge !== isElement2Bridge) return true; // Either one is bridge, the other is not\n\n const isElement1Tunnel = allowsTunnel(featureType1) && hasTag(tags1, 'tunnel');\n const isElement2Tunnel = allowsTunnel(featureType2) && hasTag(tags2, 'tunnel');\n if (isElement1Tunnel !== isElement2Tunnel ) return true; // Either one is tunnel, the other is not\n\n return (tags1.layer || '0') !== (tags2.layer || '0');\n }\n\n\n // highway values for which we shouldn't recommend connecting to waterways\n var highwaysDisallowingFords = {\n motorway: true, motorway_link: true, trunk: true, trunk_link: true,\n primary: true, primary_link: true, secondary: true, secondary_link: true\n };\n\n /**\n * @returns {object | null} the tags for the connecting node, or null if the entities should not be joined\n */\n function tagsForConnectionNodeIfAllowed(entity1, entity2, graph, lessLikelyTags) {\n var featureType1 = getFeatureType(entity1, graph);\n var featureType2 = getFeatureType(entity2, graph);\n\n var geometry1 = entity1.geometry(graph);\n var geometry2 = entity2.geometry(graph);\n var bothLines = geometry1 === 'line' && geometry2 === 'line';\n\n /**\n * @typedef {NonNullable>} FeatureType\n * @type {`${FeatureType}-${FeatureType}`}\n */\n const featureTypes = [featureType1, featureType2].sort().join('-');\n\n if (featureTypes === 'aeroway-aeroway') return {};\n\n if (featureTypes === 'aeroway-highway') {\n const isServiceRoad = entity1.tags.highway === 'service' || entity2.tags.highway === 'service';\n const isPath = entity1.tags.highway in osmPathHighwayTagValues || entity2.tags.highway in osmPathHighwayTagValues;\n // only significant roads get the aeroway=aircraft_crossing tag\n return isServiceRoad || isPath ? {} : { aeroway: 'aircraft_crossing' };\n }\n\n if (featureTypes === 'aeroway-railway') {\n return { aeroway: 'aircraft_crossing', railway: 'level_crossing' };\n }\n\n if (featureTypes === 'aeroway-waterway') return null;\n\n if (featureType1 === featureType2) {\n if (featureType1 === 'highway') {\n var entity1IsPath = osmPathHighwayTagValues[entity1.tags.highway];\n var entity2IsPath = osmPathHighwayTagValues[entity2.tags.highway];\n if ((entity1IsPath || entity2IsPath) && entity1IsPath !== entity2IsPath) {\n // one feature is a path but not both\n\n if (!bothLines) return {};\n\n var roadFeature = entity1IsPath ? entity2 : entity1;\n var pathFeature = entity1IsPath ? entity1 : entity2;\n // don't mark path connections with tracks as crossings\n if (roadFeature.tags.highway === 'track') {\n return {};\n }\n // a sidewalk crossing a driveway is unremarkable and unlikely to be interrupted by the driveway\n // a sidewalk crossing another kind of service road may be similarly unremarkable\n if (!lessLikelyTags &&\n roadFeature.tags.highway === 'service' &&\n pathFeature.tags.highway === 'footway' && pathFeature.tags.footway === 'sidewalk') {\n return {};\n }\n if (['marked', 'unmarked', 'traffic_signals', 'uncontrolled'].indexOf(pathFeature.tags.crossing) !== -1) {\n // if the path is a crossing, match the crossing type and markings\n var tags = { highway: 'crossing', crossing: pathFeature.tags.crossing };\n if ('crossing:markings' in pathFeature.tags) {\n tags['crossing:markings'] = pathFeature.tags['crossing:markings'];\n }\n return tags;\n }\n // don't add a `crossing` subtag to ambiguous crossings\n return { highway: 'crossing' };\n }\n return {};\n }\n if (featureType1 === 'waterway') return {};\n if (featureType1 === 'railway') {\n return { railway: 'railway_crossing' };\n }\n\n } else {\n if (featureTypes.indexOf('highway') !== -1) {\n if (featureTypes.indexOf('railway') !== -1) {\n if (!bothLines) return {};\n\n var isTram = entity1.tags.railway === 'tram' || entity2.tags.railway === 'tram';\n\n if (osmPathHighwayTagValues[entity1.tags.highway] ||\n osmPathHighwayTagValues[entity2.tags.highway]) {\n\n // path-tram connections use this tag\n if (isTram) return { railway: 'tram_crossing' };\n\n // other path-rail connections use this tag\n return { railway: 'crossing' };\n } else {\n // path-tram connections use this tag\n if (isTram) return { railway: 'tram_level_crossing' };\n\n // other road-rail connections use this tag\n return { railway: 'level_crossing' };\n }\n }\n\n if (featureTypes.indexOf('waterway') !== -1) {\n // do not allow fords on structures\n if (hasTag(entity1.tags, 'tunnel') && hasTag(entity2.tags, 'tunnel')) return null;\n if (hasTag(entity1.tags, 'bridge') && hasTag(entity2.tags, 'bridge')) return null;\n\n if (highwaysDisallowingFords[entity1.tags.highway] ||\n highwaysDisallowingFords[entity2.tags.highway]) {\n // do not allow fords on major highways\n return null;\n }\n return bothLines ? { ford: 'yes' } : {};\n }\n }\n }\n return null;\n }\n\n\n function findCrossingsByWay(way1, graph, tree) {\n var edgeCrossInfos = [];\n if (way1.type !== 'way') return edgeCrossInfos;\n\n var taggedFeature1 = getFeatureWithFeatureTypeTagsForWay(way1, graph);\n var way1FeatureType = getFeatureType(taggedFeature1, graph);\n if (way1FeatureType === null) return edgeCrossInfos;\n\n var checkedSingleCrossingWays = {};\n\n // declare vars ahead of time to reduce garbage collection\n var i, j;\n var extent;\n var n1, n2, nA, nB, nAId, nBId;\n var segment1, segment2;\n var oneOnly;\n var segmentInfos, segment2Info, way2, taggedFeature2, way2FeatureType;\n var way1Nodes = graph.childNodes(way1);\n var comparedWays = {};\n for (i = 0; i < way1Nodes.length - 1; i++) {\n n1 = way1Nodes[i];\n n2 = way1Nodes[i + 1];\n extent = geoExtent([\n [\n Math.min(n1.loc[0], n2.loc[0]),\n Math.min(n1.loc[1], n2.loc[1])\n ],\n [\n Math.max(n1.loc[0], n2.loc[0]),\n Math.max(n1.loc[1], n2.loc[1])\n ]\n ]);\n\n // Optimize by only checking overlapping segments, not every segment\n // of overlapping ways\n segmentInfos = tree.waySegments(extent, graph);\n\n for (j = 0; j < segmentInfos.length; j++) {\n segment2Info = segmentInfos[j];\n\n // don't check for self-intersection in this validation\n if (segment2Info.wayId === way1.id) continue;\n\n // skip if this way was already checked and only one issue is needed\n if (checkedSingleCrossingWays[segment2Info.wayId]) continue;\n\n // mark this way as checked even if there are no crossings\n comparedWays[segment2Info.wayId] = true;\n\n way2 = graph.hasEntity(segment2Info.wayId);\n if (!way2) continue;\n taggedFeature2 = getFeatureWithFeatureTypeTagsForWay(way2, graph);\n // only check crossing highway, waterway, building, and railway\n way2FeatureType = getFeatureType(taggedFeature2, graph);\n\n if (way2FeatureType === null ||\n isLegitCrossing(taggedFeature1.tags, way1FeatureType, taggedFeature2.tags, way2FeatureType)) {\n continue;\n }\n\n // create only one issue for building crossings\n oneOnly = way1FeatureType === 'building' || way2FeatureType === 'building';\n\n nAId = segment2Info.nodes[0];\n nBId = segment2Info.nodes[1];\n if (nAId === n1.id || nAId === n2.id ||\n nBId === n1.id || nBId === n2.id) {\n // n1 or n2 is a connection node; skip\n continue;\n }\n nA = graph.hasEntity(nAId);\n if (!nA) continue;\n nB = graph.hasEntity(nBId);\n if (!nB) continue;\n\n segment1 = [n1.loc, n2.loc];\n segment2 = [nA.loc, nB.loc];\n var point = geoLineIntersection(segment1, segment2);\n if (point) {\n edgeCrossInfos.push({\n wayInfos: [\n {\n way: way1,\n featureType: way1FeatureType,\n edge: [n1.id, n2.id]\n },\n {\n way: way2,\n featureType: way2FeatureType,\n edge: [nA.id, nB.id]\n }\n ],\n crossPoint: point\n });\n if (oneOnly) {\n checkedSingleCrossingWays[way2.id] = true;\n break;\n }\n }\n }\n }\n return edgeCrossInfos;\n }\n\n\n function waysToCheck(entity, graph) {\n var featureType = getFeatureType(entity, graph);\n if (!featureType) return [];\n\n if (entity.type === 'way') {\n return [entity];\n } else if (entity.type === 'relation') {\n return entity.members.reduce(function(array, member) {\n if (member.type === 'way' &&\n // only look at geometry ways\n (!member.role || member.role === 'outer' || member.role === 'inner')) {\n var entity = graph.hasEntity(member.id);\n // don't add duplicates\n if (entity && array.indexOf(entity) === -1) {\n array.push(entity);\n }\n }\n return array;\n }, []);\n }\n return [];\n }\n\n\n var validation = function checkCrossingWays(entity, graph) {\n\n var tree = context.history().tree();\n\n var ways = waysToCheck(entity, graph);\n\n var issues = [];\n // declare these here to reduce garbage collection\n var wayIndex, crossingIndex, crossings;\n for (wayIndex in ways) {\n crossings = findCrossingsByWay(ways[wayIndex], graph, tree);\n for (crossingIndex in crossings) {\n issues.push(createIssue(crossings[crossingIndex], graph));\n }\n }\n return issues;\n };\n\n\n function createIssue(crossing, graph) {\n\n // use the entities with the tags that define the feature type\n crossing.wayInfos.sort(function(way1Info, way2Info) {\n var type1 = way1Info.featureType;\n var type2 = way2Info.featureType;\n if (type1 === type2) {\n return utilDisplayLabel(way1Info.way, graph) > utilDisplayLabel(way2Info.way, graph);\n } else if (type1 === 'waterway') {\n return true;\n } else if (type2 === 'waterway') {\n return false;\n }\n return type1 < type2;\n });\n var entities = crossing.wayInfos.map(function(wayInfo) {\n return getFeatureWithFeatureTypeTagsForWay(wayInfo.way, graph);\n });\n var edges = [crossing.wayInfos[0].edge, crossing.wayInfos[1].edge];\n var featureTypes = [crossing.wayInfos[0].featureType, crossing.wayInfos[1].featureType];\n\n var connectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1], graph);\n\n var featureType1 = crossing.wayInfos[0].featureType;\n var featureType2 = crossing.wayInfos[1].featureType;\n\n var isCrossingIndoors = taggedAsIndoor(entities[0].tags) && taggedAsIndoor(entities[1].tags);\n var isCrossingTunnels = allowsTunnel(featureType1) && hasTag(entities[0].tags, 'tunnel') &&\n allowsTunnel(featureType2) && hasTag(entities[1].tags, 'tunnel');\n var isCrossingBridges = allowsBridge(featureType1) && hasTag(entities[0].tags, 'bridge') &&\n allowsBridge(featureType2) && hasTag(entities[1].tags, 'bridge');\n\n var subtype = [featureType1, featureType2].sort().join('-');\n\n var crossingTypeID = subtype;\n\n if (isCrossingIndoors) {\n crossingTypeID = 'indoor-indoor';\n } else if (isCrossingTunnels) {\n crossingTypeID = 'tunnel-tunnel';\n } else if (isCrossingBridges) {\n crossingTypeID = 'bridge-bridge';\n }\n if (connectionTags && (isCrossingIndoors || isCrossingTunnels || isCrossingBridges)) {\n crossingTypeID += '_connectable';\n }\n\n // Differentiate based on the loc rounded to 4 digits, since two ways can cross multiple times.\n var uniqueID = crossing.crossPoint[0].toFixed(4) + ',' + crossing.crossPoint[1].toFixed(4);\n\n return new validationIssue({\n type: type,\n subtype: subtype,\n severity: 'warning',\n message: function(context) {\n var graph = context.graph();\n var entity1 = graph.hasEntity(this.entityIds[0]),\n entity2 = graph.hasEntity(this.entityIds[1]);\n return (entity1 && entity2) ? t.append('issues.crossing_ways.message', {\n feature: utilDisplayLabel(entity1, graph, featureType1 === 'building'),\n feature2: utilDisplayLabel(entity2, graph, featureType2 === 'building')\n }) : '';\n },\n reference: showReference,\n entityIds: entities.map(function(entity) {\n return entity.id;\n }),\n data: {\n edges: edges,\n featureTypes: featureTypes,\n connectionTags: connectionTags\n },\n hash: uniqueID,\n loc: crossing.crossPoint,\n dynamicFixes: function(context) {\n var mode = context.mode();\n if (!mode || mode.id !== 'select' || mode.selectedIDs().length !== 1) return [];\n\n var selectedIndex = this.entityIds[0] === mode.selectedIDs()[0] ? 0 : 1;\n var selectedFeatureType = this.data.featureTypes[selectedIndex];\n var otherFeatureType = this.data.featureTypes[selectedIndex === 0 ? 1 : 0];\n\n var fixes = [];\n\n if (connectionTags) {\n fixes.push(makeConnectWaysFix(this.data.connectionTags));\n let lessLikelyConnectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1], graph, true);\n if (lessLikelyConnectionTags && !deepEqual(connectionTags, lessLikelyConnectionTags)) {\n fixes.push(makeConnectWaysFix(lessLikelyConnectionTags));\n }\n }\n\n if (isCrossingIndoors) {\n fixes.push(new validationIssueFix({\n icon: 'iD-icon-layers',\n title: t.append('issues.fix.use_different_levels.title')\n }));\n } else if (isCrossingTunnels ||\n isCrossingBridges ||\n featureType1 === 'building' ||\n featureType2 === 'building') {\n\n fixes.push(makeChangeLayerFix('higher'));\n fixes.push(makeChangeLayerFix('lower'));\n\n // can only add bridge/tunnel if both features are lines\n } else if (context.graph().geometry(this.entityIds[0]) === 'line' &&\n context.graph().geometry(this.entityIds[1]) === 'line') {\n\n // don't recommend adding bridges to waterways since they're uncommon\n if (allowsBridge(selectedFeatureType) && selectedFeatureType !== 'waterway') {\n fixes.push(makeAddBridgeOrTunnelFix('add_a_bridge', 'temaki-bridge', 'bridge'));\n }\n\n // don't recommend adding tunnels under waterways since they're uncommon\n var skipTunnelFix = otherFeatureType === 'waterway' && selectedFeatureType !== 'waterway';\n if (allowsTunnel(selectedFeatureType) && !skipTunnelFix) {\n if (selectedFeatureType === 'waterway') {\n // naming piped waterway \"tunnel\" is a confusing osmism, culvert should be more clear\n fixes.push(makeAddBridgeOrTunnelFix('add_a_culvert', 'temaki-waste', 'tunnel'));\n } else {\n fixes.push(makeAddBridgeOrTunnelFix('add_a_tunnel', 'temaki-tunnel', 'tunnel'));\n }\n }\n }\n\n // changing the date range is always an option\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-change-date-range',\n title: t.append('issues.fix.change_date_range.title')\n }));\n\n // repositioning the features is always an option\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-move',\n title: t.append('issues.fix.reposition_features.title')\n }));\n\n if (featureType1 === 'building' || featureType2 === 'building') {\n // if the validation is about overlapping buildings:\n // show \"reposition features\" suggestion first, as that is most often\n // most sensible fix for those errors, see #11329\n fixes.unshift(fixes.pop());\n }\n\n return fixes;\n }\n });\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.crossing_ways.' + crossingTypeID + '.reference'));\n }\n }\n\n function makeAddBridgeOrTunnelFix(fixTitleID, iconName, bridgeOrTunnel){\n return new validationIssueFix({\n icon: iconName,\n title: t.append('issues.fix.' + fixTitleID + '.title'),\n onClick: function(context) {\n var mode = context.mode();\n if (!mode || mode.id !== 'select') return;\n\n var selectedIDs = mode.selectedIDs();\n if (selectedIDs.length !== 1) return;\n\n var selectedWayID = selectedIDs[0];\n if (!context.hasEntity(selectedWayID)) return;\n\n var resultWayIDs = [selectedWayID];\n\n var edge, crossedEdge, crossedWayID;\n if (this.issue.entityIds[0] === selectedWayID) {\n edge = this.issue.data.edges[0];\n crossedEdge = this.issue.data.edges[1];\n crossedWayID = this.issue.entityIds[1];\n } else {\n edge = this.issue.data.edges[1];\n crossedEdge = this.issue.data.edges[0];\n crossedWayID = this.issue.entityIds[0];\n }\n\n var crossingLoc = this.issue.loc;\n\n var projection = context.projection;\n\n var action = function actionAddStructure(graph) {\n\n var edgeNodes = [graph.entity(edge[0]), graph.entity(edge[1])];\n\n var crossedWay = graph.hasEntity(crossedWayID);\n // use the explicit width of the crossed feature as the structure length, if available\n var structLengthMeters = crossedWay && isFinite(crossedWay.tags.width) && Number(crossedWay.tags.width);\n if (!structLengthMeters) {\n // if no explicit width is set, approximate the width based on the tags\n structLengthMeters = crossedWay && crossedWay.impliedLineWidthMeters();\n }\n if (structLengthMeters) {\n if (getFeatureType(crossedWay, graph) === 'railway') {\n // bridges over railways are generally much longer than the rail bed itself, compensate\n structLengthMeters *= 2;\n }\n } else {\n // should ideally never land here since all rail/water/road tags should have an implied width\n structLengthMeters = 8;\n }\n\n var a1 = geoAngle(edgeNodes[0], edgeNodes[1], projection) + Math.PI;\n var a2 = geoAngle(graph.entity(crossedEdge[0]), graph.entity(crossedEdge[1]), projection) + Math.PI;\n var crossingAngle = Math.max(a1, a2) - Math.min(a1, a2);\n if (crossingAngle > Math.PI) crossingAngle -= Math.PI;\n // lengthen the structure to account for the angle of the crossing\n structLengthMeters = ((structLengthMeters / 2) / Math.sin(crossingAngle)) * 2;\n\n // add padding since the structure must extend past the edges of the crossed feature\n structLengthMeters += 4;\n\n // clamp the length to a reasonable range\n structLengthMeters = Math.min(Math.max(structLengthMeters, 4), 50);\n\n function geomToProj(geoPoint) {\n return [\n geoLonToMeters(geoPoint[0], geoPoint[1]),\n geoLatToMeters(geoPoint[1])\n ];\n }\n function projToGeom(projPoint) {\n var lat = geoMetersToLat(projPoint[1]);\n return [\n geoMetersToLon(projPoint[0], lat),\n lat\n ];\n }\n\n var projEdgeNode1 = geomToProj(edgeNodes[0].loc);\n var projEdgeNode2 = geomToProj(edgeNodes[1].loc);\n\n var projectedAngle = geoVecAngle(projEdgeNode1, projEdgeNode2);\n\n var projectedCrossingLoc = geomToProj(crossingLoc);\n var linearToSphericalMetersRatio = geoVecLength(projEdgeNode1, projEdgeNode2) /\n geoSphericalDistance(edgeNodes[0].loc, edgeNodes[1].loc);\n\n function locSphericalDistanceFromCrossingLoc(angle, distanceMeters) {\n var lengthSphericalMeters = distanceMeters * linearToSphericalMetersRatio;\n return projToGeom([\n projectedCrossingLoc[0] + Math.cos(angle) * lengthSphericalMeters,\n projectedCrossingLoc[1] + Math.sin(angle) * lengthSphericalMeters\n ]);\n }\n\n var endpointLocGetter1 = function(lengthMeters) {\n return locSphericalDistanceFromCrossingLoc(projectedAngle, lengthMeters);\n };\n var endpointLocGetter2 = function(lengthMeters) {\n return locSphericalDistanceFromCrossingLoc(projectedAngle + Math.PI, lengthMeters);\n };\n\n // avoid creating very short edges from splitting too close to another node\n var minEdgeLengthMeters = 0.55;\n\n // decide where to bound the structure along the way, splitting as necessary\n function determineEndpoint(edge, endNode, locGetter) {\n var newNode;\n\n var idealLengthMeters = structLengthMeters / 2;\n\n // distance between the crossing location and the end of the edge,\n // the maximum length of this side of the structure\n var crossingToEdgeEndDistance = geoSphericalDistance(crossingLoc, endNode.loc);\n\n if (crossingToEdgeEndDistance - idealLengthMeters > minEdgeLengthMeters) {\n // the edge is long enough to insert a new node\n\n // the loc that would result in the full expected length\n var idealNodeLoc = locGetter(idealLengthMeters);\n\n newNode = osmNode();\n graph = actionAddMidpoint({ loc: idealNodeLoc, edge: edge }, newNode)(graph);\n\n } else {\n var edgeCount = 0;\n endNode.parentIntersectionWays(graph).forEach(function(way) {\n way.nodes.forEach(function(nodeID) {\n if (nodeID === endNode.id) {\n if ((endNode.id === way.first() && endNode.id !== way.last()) ||\n (endNode.id === way.last() && endNode.id !== way.first())) {\n edgeCount += 1;\n } else {\n edgeCount += 2;\n }\n }\n });\n });\n\n if (edgeCount >= 3) {\n // the end node is a junction, try to leave a segment\n // between it and the structure - #7202\n\n var insetLength = crossingToEdgeEndDistance - minEdgeLengthMeters;\n if (insetLength > minEdgeLengthMeters) {\n var insetNodeLoc = locGetter(insetLength);\n newNode = osmNode();\n graph = actionAddMidpoint({ loc: insetNodeLoc, edge: edge }, newNode)(graph);\n }\n }\n }\n\n // if the edge is too short to subdivide as desired, then\n // just bound the structure at the existing end node\n if (!newNode) newNode = endNode;\n\n var splitAction = actionSplit([newNode.id])\n .limitWays(resultWayIDs); // only split selected or created ways\n\n // do the split\n graph = splitAction(graph);\n if (splitAction.getCreatedWayIDs().length) {\n resultWayIDs.push(splitAction.getCreatedWayIDs()[0]);\n }\n\n return newNode;\n }\n\n var structEndNode1 = determineEndpoint(edge, edgeNodes[1], endpointLocGetter1);\n var structEndNode2 = determineEndpoint([edgeNodes[0].id, structEndNode1.id], edgeNodes[0], endpointLocGetter2);\n\n var structureWay = resultWayIDs.map(function(id) {\n return graph.entity(id);\n }).find(function(way) {\n return way.nodes.indexOf(structEndNode1.id) !== -1 &&\n way.nodes.indexOf(structEndNode2.id) !== -1;\n });\n\n var tags = Object.assign({}, structureWay.tags); // copy tags\n if (bridgeOrTunnel === 'bridge'){\n tags.bridge = 'yes';\n tags.layer = '1';\n } else {\n var tunnelValue = 'yes';\n if (getFeatureType(structureWay, graph) === 'waterway') {\n // use `tunnel=culvert` for waterways by default\n tunnelValue = 'culvert';\n }\n tags.tunnel = tunnelValue;\n tags.layer = '-1';\n }\n // apply the structure tags to the way\n graph = actionChangeTags(structureWay.id, tags)(graph);\n return graph;\n };\n\n context.perform(action, t('issues.fix.' + fixTitleID + '.annotation'));\n context.enter(modeSelect(context, resultWayIDs));\n }\n });\n }\n\n function makeConnectWaysFix(connectionTags) {\n\n var fixTitleID = 'connect_features';\n var fixIcon = 'iD-icon-crossing';\n if (connectionTags.highway === 'crossing') {\n fixTitleID = 'connect_using_crossing';\n fixIcon = 'temaki-pedestrian';\n }\n if (connectionTags.ford) {\n fixTitleID = 'connect_using_ford';\n fixIcon = 'roentgen-ford';\n }\n\n const fix = new validationIssueFix({\n icon: fixIcon,\n title: t.append('issues.fix.' + fixTitleID + '.title'),\n onClick: function(context) {\n var loc = this.issue.loc;\n var edges = this.issue.data.edges;\n\n context.perform(\n function actionConnectCrossingWays(graph) {\n // create the new node for the points\n var node = osmNode({ loc: loc, tags: connectionTags });\n graph = graph.replace(node);\n\n var nodesToMerge = [node.id];\n var mergeThresholdInMeters = 0.75;\n\n edges.forEach(function(edge) {\n var edgeNodes = [graph.entity(edge[0]), graph.entity(edge[1])];\n var nearby = geoSphericalClosestNode(edgeNodes, loc);\n // if there is already a suitable node nearby, use that\n // use the node if node has no interesting tags or if it is a crossing node #8326\n if ((!nearby.node.hasInterestingTags() || nearby.node.isCrossing()) && nearby.distance < mergeThresholdInMeters) {\n nodesToMerge.push(nearby.node.id);\n // else add the new node to the way\n } else {\n graph = actionAddMidpoint({loc: loc, edge: edge}, node)(graph);\n }\n });\n\n if (nodesToMerge.length > 1) {\n // if we're using nearby nodes, merge them with the new node\n graph = actionMergeNodes(nodesToMerge, loc)(graph);\n }\n\n return graph;\n },\n t('issues.fix.connect_crossing_features.annotation')\n );\n }\n });\n fix._connectionTags = connectionTags;\n return fix;\n }\n\n /** @returns {osmEntity | undefined} */\n function getSelectedFeature() {\n const mode = context.mode();\n if (!mode || mode.id !== 'select') return undefined;\n\n const selectedIDs = mode.selectedIDs();\n if (selectedIDs.length !== 1) return undefined;\n\n const selectedID = selectedIDs[0];\n\n const entity = context.hasEntity(selectedID);\n return entity;\n }\n\n /**\n * @param {\"higher\" | \"lower\"} higherOrLower\n * @returns {validationIssueFix | undefined}\n */\n function makeChangeLayerFix(higherOrLower) {\n const selectedFeature = getSelectedFeature();\n return new validationIssueFix({\n id: selectedFeature.id,\n icon: 'iD-icon-' + (higherOrLower === 'higher' ? 'up' : 'down'),\n title: selectedFeature\n ? t.append('issues.fix.tag_this_as_' + higherOrLower + '.informative_title', {\n feature: utilDisplayLabel(selectedFeature, context.graph())\n })\n // in this context, there is no selected feature so we\n // have to show a generic name\n : t.append('issues.fix.tag_this_as_' + higherOrLower + '.title'),\n\n onClick: function(context) {\n const entity = getSelectedFeature();\n const selectedID = entity.id;\n if (!entity) return;\n\n\n if (!this.issue.entityIds.some(function(entityId) {\n return entityId === selectedID;\n })) return;\n\n var tags = Object.assign({}, entity.tags); // shallow copy\n var layer = tags.layer && Number(tags.layer);\n if (layer && !isNaN(layer)) {\n if (higherOrLower === 'higher') {\n layer += 1;\n } else {\n layer -= 1;\n }\n } else {\n if (higherOrLower === 'higher') {\n layer = 1;\n } else {\n layer = -1;\n }\n }\n tags.layer = layer.toString();\n context.perform(\n actionChangeTags(entity.id, tags),\n t('operations.change_tags.annotation')\n );\n }\n });\n }\n\n validation.type = type;\n\n return validation;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { t } from '../core/localizer';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionMoveNode } from '../actions/move_node';\nimport { actionNoop } from '../actions/noop';\nimport { behaviorDraw } from './draw';\nimport { geoChooseEdge, geoHasSelfIntersections } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect } from '../modes/select';\nimport { osmNode } from '../osm/node';\nimport { utilRebind } from '../util/rebind';\nimport { utilKeybinding } from '../util';\n\nexport function behaviorDrawWay(context, wayID, mode, startGraph) {\n const keybinding = utilKeybinding('drawWay');\n\n var dispatch = d3_dispatch('rejectedSelfIntersection');\n\n var behavior = behaviorDraw(context);\n\n // Must be set by `drawWay.nodeIndex` before each install of this behavior.\n var _nodeIndex;\n\n var _origWay;\n var _wayGeometry;\n var _headNodeID;\n var _annotation;\n\n var _pointerHasMoved = false;\n\n // The osmNode to be placed.\n // This is temporary and just follows the mouse cursor until an \"add\" event occurs.\n var _drawNode;\n\n var _didResolveTempEdit = false;\n\n function createDrawNode(loc) {\n // don't make the draw node until we actually need it\n _drawNode = osmNode({ loc: loc });\n\n context.pauseChangeDispatch();\n context.replace(function actionAddDrawNode(graph) {\n // add the draw node to the graph and insert it into the way\n var way = graph.entity(wayID);\n return graph\n .replace(_drawNode)\n .replace(way.addNode(_drawNode.id, _nodeIndex));\n }, _annotation);\n context.resumeChangeDispatch();\n\n setActiveElements();\n }\n\n function removeDrawNode() {\n\n context.pauseChangeDispatch();\n context.replace(\n function actionDeleteDrawNode(graph) {\n var way = graph.entity(wayID);\n return graph\n .replace(way.removeNode(_drawNode.id))\n .remove(_drawNode);\n },\n _annotation\n );\n _drawNode = undefined;\n context.resumeChangeDispatch();\n }\n\n\n function keydown(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope')) {\n context.surface()\n .classed('nope-suppressed', true);\n }\n context.surface()\n .classed('nope', false)\n .classed('nope-disabled', true);\n }\n }\n\n\n function keyup(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope-suppressed')) {\n context.surface()\n .classed('nope', true);\n }\n context.surface()\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false);\n }\n }\n\n\n function allowsVertex(d) {\n return d.geometry(context.graph()) === 'vertex' || presetManager.allowsVertex(d, context.graph());\n }\n\n\n // related code\n // - `mode/drag_node.js` `doMove()`\n // - `behavior/draw.js` `click()`\n // - `behavior/draw_way.js` `move()`\n function move(d3_event, datum) {\n\n var loc = context.map().mouseCoordinates();\n\n if (!_drawNode) createDrawNode(loc);\n\n context.surface().classed('nope-disabled', d3_event.altKey);\n\n var targetLoc = datum && datum.properties && datum.properties.entity &&\n allowsVertex(datum.properties.entity) && datum.properties.entity.loc;\n var targetNodes = datum && datum.properties && datum.properties.nodes;\n\n if (targetLoc) { // snap to node/vertex - a point target with `.loc`\n loc = targetLoc;\n\n } else if (targetNodes) { // snap to way - a line target with `.nodes`\n var choice = geoChooseEdge(targetNodes, context.map().mouse(), context.projection, _drawNode.id);\n if (choice) {\n loc = choice.loc;\n }\n }\n\n context.replace(actionMoveNode(_drawNode.id, loc), _annotation);\n _drawNode = context.entity(_drawNode.id);\n checkGeometry(true /* includeDrawNode */);\n }\n\n\n // Check whether this edit causes the geometry to break.\n // If so, class the surface with a nope cursor.\n // `includeDrawNode` - Only check the relevant line segments if finishing drawing\n function checkGeometry(includeDrawNode) {\n var nopeDisabled = context.surface().classed('nope-disabled');\n var isInvalid = isInvalidGeometry(includeDrawNode);\n\n if (nopeDisabled) {\n context.surface()\n .classed('nope', false)\n .classed('nope-suppressed', isInvalid);\n } else {\n context.surface()\n .classed('nope', isInvalid)\n .classed('nope-suppressed', false);\n }\n }\n\n\n function isInvalidGeometry(includeDrawNode) {\n\n var testNode = _drawNode;\n\n // we only need to test the single way we're drawing\n var parentWay = context.graph().entity(wayID);\n var nodes = context.graph().childNodes(parentWay).slice(); // shallow copy\n\n if (includeDrawNode) {\n if (parentWay.isClosed()) {\n // don't test the last segment for closed ways - #4655\n // (still test the first segment)\n nodes.pop();\n }\n } else { // discount the draw node\n\n if (parentWay.isClosed()) {\n if (nodes.length < 3) return false;\n if (_drawNode) nodes.splice(-2, 1);\n testNode = nodes[nodes.length - 2];\n } else {\n // there's nothing we need to test if we ignore the draw node on open ways\n return false;\n }\n }\n\n return testNode && geoHasSelfIntersections(nodes, testNode.id);\n }\n\n\n function undone() {\n\n // undoing removed the temp edit\n _didResolveTempEdit = true;\n\n context.pauseChangeDispatch();\n\n var nextMode;\n\n if (context.graph() === startGraph) {\n // We've undone back to the initial state before we started drawing.\n // Just exit the draw mode without undoing whatever we did before\n // we entered the draw mode.\n nextMode = modeSelect(context, [wayID]);\n } else {\n // The `undo` only removed the temporary edit, so here we have to\n // manually undo to actually remove the last node we added. We can't\n // use the `undo` function since the initial \"add\" graph doesn't have\n // an annotation and so cannot be undone to.\n context.pop(1);\n\n // continue drawing\n nextMode = mode;\n }\n\n // clear the redo stack by adding and removing a blank edit\n context.perform(actionNoop());\n context.pop(1);\n\n context.resumeChangeDispatch();\n context.enter(nextMode);\n }\n\n\n function setActiveElements() {\n if (!_drawNode) return;\n\n context.surface().selectAll('.' + _drawNode.id)\n .classed('active', true);\n }\n\n\n function resetToStartGraph() {\n while (context.graph() !== startGraph) {\n context.pop();\n }\n }\n\n\n var drawWay = function(surface) {\n _drawNode = undefined;\n _didResolveTempEdit = false;\n _origWay = context.entity(wayID);\n\n if (typeof _nodeIndex === 'number') {\n _headNodeID = _origWay.nodes[_nodeIndex];\n } else if (_origWay.isClosed()) {\n _headNodeID = _origWay.nodes[_origWay.nodes.length - 2];\n } else {\n _headNodeID = _origWay.nodes[_origWay.nodes.length - 1];\n }\n\n _wayGeometry = _origWay.geometry(context.graph());\n _annotation = t((_origWay.nodes.length === (_origWay.isClosed() ? 2 : 1) ?\n 'operations.start.annotation.' :\n 'operations.continue.annotation.') + _wayGeometry\n );\n _pointerHasMoved = false;\n\n // Push an annotated state for undo to return back to.\n // We must make sure to replace or remove it later.\n context.pauseChangeDispatch();\n context.perform(actionNoop(), _annotation);\n context.resumeChangeDispatch();\n\n behavior.hover()\n .initialNodeID(_headNodeID);\n\n behavior\n .on('move', function() {\n _pointerHasMoved = true;\n move.apply(this, arguments);\n })\n .on('down', function() {\n move.apply(this, arguments);\n })\n .on('downcancel', function() {\n if (_drawNode) removeDrawNode();\n })\n .on('click', drawWay.add)\n .on('clickWay', drawWay.addWay)\n .on('clickNode', drawWay.addNode)\n .on('undo', context.undo)\n .on('cancel', drawWay.cancel)\n .on('finish', drawWay.finish);\n\n d3_select(window)\n .on('keydown.drawWay', keydown)\n .on('keyup.drawWay', keyup);\n\n context.map()\n .dblclickZoomEnable(false)\n .on('drawn.draw', setActiveElements);\n\n setActiveElements();\n\n surface.call(behavior);\n\n context.history()\n .on('undone.draw', undone);\n };\n\n\n drawWay.off = function(surface) {\n\n if (!_didResolveTempEdit) {\n // Drawing was interrupted unexpectedly.\n // This can happen if the user changes modes,\n // clicks geolocate button, a hashchange event occurs, etc.\n\n context.pauseChangeDispatch();\n resetToStartGraph();\n context.resumeChangeDispatch();\n }\n\n _drawNode = undefined;\n _nodeIndex = undefined;\n\n context.map()\n .on('drawn.draw', null);\n\n surface.call(behavior.off)\n .selectAll('.active')\n .classed('active', false);\n\n surface\n .classed('nope', false)\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false);\n\n d3_select(window)\n .on('keydown.drawWay', null)\n .on('keyup.drawWay', null);\n\n context.history()\n .on('undone.draw', null);\n };\n\n\n function attemptAdd(d, loc, doAdd) {\n\n if (_drawNode) {\n // move the node to the final loc in case move wasn't called\n // consistently (e.g. on touch devices)\n context.replace(actionMoveNode(_drawNode.id, loc), _annotation);\n _drawNode = context.entity(_drawNode.id);\n } else {\n createDrawNode(loc);\n }\n\n checkGeometry(true /* includeDrawNode */);\n if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {\n if (!_pointerHasMoved) {\n // prevent the temporary draw node from appearing on touch devices\n removeDrawNode();\n }\n dispatch.call('rejectedSelfIntersection', this);\n return; // can't click here\n }\n\n context.pauseChangeDispatch();\n doAdd();\n // we just replaced the temporary edit with the real one\n _didResolveTempEdit = true;\n context.resumeChangeDispatch();\n\n context.enter(mode);\n }\n\n\n // Accept the current position of the drawing node\n drawWay.add = function(loc, d) {\n attemptAdd(d, loc, function() {\n // don't need to do anything extra\n });\n };\n\n\n // Connect the way to an existing way\n drawWay.addWay = function(loc, edge, d) {\n attemptAdd(d, loc, function() {\n context.replace(\n actionAddMidpoint({ loc: loc, edge: edge }, _drawNode),\n _annotation\n );\n });\n };\n\n\n // Connect the way to an existing node\n drawWay.addNode = function(node, d) {\n\n // finish drawing if the mapper targets the prior node\n if (node.id === _headNodeID ||\n // or the first node when drawing an area\n (_origWay.isClosed() && node.id === _origWay.first())) {\n drawWay.finish();\n return;\n }\n\n attemptAdd(d, node.loc, function() {\n context.replace(\n function actionReplaceDrawNode(graph) {\n // remove the temporary draw node and insert the existing node\n // at the same index\n\n graph = graph\n .replace(graph.entity(wayID).removeNode(_drawNode.id))\n .remove(_drawNode);\n return graph\n .replace(graph.entity(wayID).addNode(node.id, _nodeIndex));\n },\n _annotation\n );\n });\n };\n\n /**\n * @param {(typeof osmWay)[]} ways\n * @returns {\"line\" | \"area\" | \"generic\"}\n */\n function getFeatureType(ways) {\n if (ways.every(way => way.isClosed())) return 'area';\n if (ways.every(way => !way.isClosed())) return 'line';\n return 'generic';\n }\n\n /** see PR #8671 */\n function followMode() {\n if (_didResolveTempEdit) return;\n\n try {\n\n // get the last 2 added nodes.\n // check if they are both part of only oneway (the same one)\n // check if the ways that they're part of are the same way\n // find index of the last two nodes, to determine the direction to travel around the existing way\n // add the next node to the way we are drawing\n\n // if we're drawing an area, the first node = last node.\n const isDrawingArea = _origWay.nodes[0] === _origWay.nodes.slice(-1)[0];\n\n const [secondLastNodeId, lastNodeId] = _origWay.nodes.slice(isDrawingArea ? -3 : -2);\n\n // Unlike startGraph, the full history graph may contain unsaved vertices to follow.\n // https://github.com/openstreetmap/iD/issues/8749\n const historyGraph = context.history().graph();\n if (!lastNodeId || !secondLastNodeId || !historyGraph.hasEntity(lastNodeId) || !historyGraph.hasEntity(secondLastNodeId)) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('operations.follow.error.needs_more_initial_nodes'))();\n return;\n }\n\n // If the way has looped over itself, follow some other way.\n const lastNodesParents = historyGraph.parentWays(historyGraph.entity(lastNodeId)).filter(w => w.id !== wayID);\n const secondLastNodesParents = historyGraph.parentWays(historyGraph.entity(secondLastNodeId)).filter(w => w.id !== wayID);\n\n const featureType = getFeatureType(lastNodesParents);\n\n if (lastNodesParents.length !== 1 || secondLastNodesParents.length === 0) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append(`operations.follow.error.intersection_of_multiple_ways.${featureType}`))();\n return;\n }\n\n // Check if the last node's parent is also the parent of the second last node.\n // The last node must only have one parent, but the second last node can have\n // multiple parents.\n if (!secondLastNodesParents.some(n => n.id === lastNodesParents[0].id)) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append(`operations.follow.error.intersection_of_different_ways.${featureType}`))();\n return;\n }\n\n const way = lastNodesParents[0];\n\n const indexOfLast = way.nodes.indexOf(lastNodeId);\n const indexOfSecondLast = way.nodes.indexOf(secondLastNodeId);\n\n // for a closed way, the first/last node is the same so it appears twice in the array,\n // but indexOf always finds the first occurrence. This is only an issue when following a way\n // in descending order\n const isDescendingPastZero = indexOfLast === way.nodes.length - 2 && indexOfSecondLast === 0;\n\n let nextNodeIndex = indexOfLast + (indexOfLast > indexOfSecondLast && !isDescendingPastZero ? 1 : -1);\n // if we're following a closed way and we pass the first/last node, the next index will be -1\n if (nextNodeIndex === -1) nextNodeIndex = indexOfSecondLast === 1 ? way.nodes.length - 2 : 1;\n\n const nextNode = historyGraph.entity(way.nodes[nextNodeIndex]);\n\n drawWay.addNode(nextNode, {\n geometry: { type: 'Point', coordinates: nextNode.loc },\n id: nextNode.id,\n properties: { target: true, entity: nextNode },\n });\n } catch {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('operations.follow.error.unknown'))();\n }\n }\n\n keybinding.on(t('operations.follow.key'), followMode);\n d3_select(document).call(keybinding);\n\n // Finish the draw operation, removing the temporary edit.\n // If the way has enough nodes to be valid, it's selected.\n // Otherwise, delete everything and return to browse mode.\n drawWay.finish = function() {\n checkGeometry(false /* includeDrawNode */);\n if (context.surface().classed('nope')) {\n dispatch.call('rejectedSelfIntersection', this);\n return; // can't click here\n }\n\n context.pauseChangeDispatch();\n // remove the temporary edit\n context.pop(1);\n _didResolveTempEdit = true;\n context.resumeChangeDispatch();\n\n var way = context.hasEntity(wayID);\n if (!way || way.isDegenerate()) {\n drawWay.cancel();\n return;\n }\n\n window.setTimeout(function() {\n context.map().dblclickZoomEnable(true);\n }, 1000);\n\n var isNewFeature = !mode.isContinuing;\n context.enter(modeSelect(context, [wayID]).newFeature(isNewFeature));\n };\n\n\n // Cancel the draw operation, delete everything, and return to browse mode.\n drawWay.cancel = function() {\n context.pauseChangeDispatch();\n resetToStartGraph();\n context.resumeChangeDispatch();\n\n window.setTimeout(function() {\n context.map().dblclickZoomEnable(true);\n }, 1000);\n\n context.surface()\n .classed('nope', false)\n .classed('nope-disabled', false)\n .classed('nope-suppressed', false);\n\n context.enter(modeBrowse(context));\n };\n\n\n drawWay.nodeIndex = function(val) {\n if (!arguments.length) return _nodeIndex;\n _nodeIndex = val;\n return drawWay;\n };\n\n\n drawWay.activeID = function() {\n if (!arguments.length) return _drawNode && _drawNode.id;\n // no assign\n return drawWay;\n };\n\n\n return utilRebind(drawWay, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDrawWay } from '../behavior/draw_way';\n\n\nexport function modeDrawLine(context, wayID, startGraph, button, affix, continuing) {\n var mode = {\n button: button,\n id: 'draw-line'\n };\n\n var behavior = behaviorDrawWay(context, wayID, mode, startGraph)\n .on('rejectedSelfIntersection.modeDrawLine', function() {\n context.ui().flash\n .iconName('#iD-icon-no')\n .label(t.append('self_intersection.error.lines'))();\n });\n\n mode.wayID = wayID;\n\n mode.isContinuing = continuing;\n\n mode.enter = function() {\n behavior\n .nodeIndex(affix === 'prefix' ? 0 : undefined);\n\n context.install(behavior);\n };\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n mode.selectedIDs = function() {\n return [wayID];\n };\n\n mode.activeID = function() {\n return (behavior && behavior.activeID()) || [];\n };\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { utilDetect } from '../util/detect';\n\n\n// Translate a MacOS key command into the appropriate Windows/Linux equivalent.\n// For example, \u2318Z -> Ctrl+Z\nexport var uiCmd = function (code) {\n var detected = utilDetect();\n\n if (detected.os === 'mac') {\n return code;\n }\n\n if (detected.os === 'win') {\n if (code === '\u2318\u21E7Z') return 'Ctrl+Y';\n }\n\n var result = '',\n replacements = {\n '\u2318': 'Ctrl',\n '\u21E7': 'Shift',\n '\u2325': 'Alt',\n '\u232B': 'Backspace',\n '\u2326': 'Delete'\n };\n\n for (var i = 0; i < code.length; i++) {\n if (code[i] in replacements) {\n result += replacements[code[i]] + (i < code.length - 1 ? '+' : '');\n } else {\n result += code[i];\n }\n }\n\n return result;\n};\n\n\n// return a display-focused string for a given keyboard code\nuiCmd.display = function(code) {\n if (code.length !== 1) return code;\n\n var detected = utilDetect();\n var mac = (detected.os === 'mac');\n var replacements = {\n '\u2318': mac ? '\u2318 ' + t('shortcuts.key.cmd') : t('shortcuts.key.ctrl'),\n '\u21E7': mac ? '\u21E7 ' + t('shortcuts.key.shift') : t('shortcuts.key.shift'),\n '\u2325': mac ? '\u2325 ' + t('shortcuts.key.option') : t('shortcuts.key.alt'),\n '\u2303': mac ? '\u2303 ' + t('shortcuts.key.ctrl') : t('shortcuts.key.ctrl'),\n '\u232B': mac ? '\u232B ' + t('shortcuts.key.delete') : t('shortcuts.key.backspace'),\n '\u2326': mac ? '\u2326 ' + t('shortcuts.key.del') : t('shortcuts.key.del'),\n '\u2196': mac ? '\u2196 ' + t('shortcuts.key.pgup') : t('shortcuts.key.pgup'),\n '\u2198': mac ? '\u2198 ' + t('shortcuts.key.pgdn') : t('shortcuts.key.pgdn'),\n '\u21DE': mac ? '\u21DE ' + t('shortcuts.key.home') : t('shortcuts.key.home'),\n '\u21DF': mac ? '\u21DF ' + t('shortcuts.key.end') : t('shortcuts.key.end'),\n '\u21B5': mac ? '\u23CE ' + t('shortcuts.key.return') : t('shortcuts.key.enter'),\n '\u238B': mac ? '\u238B ' + t('shortcuts.key.esc') : t('shortcuts.key.esc'),\n '\u2630': mac ? '\u2630 ' + t('shortcuts.key.menu') : t('shortcuts.key.menu'),\n };\n\n return replacements[code] || code;\n};\n", "import { t } from '../core/localizer';\nimport { actionDeleteMultiple } from '../actions/delete_multiple';\nimport { behaviorOperation } from '../behavior/operation';\nimport { geoSphericalDistance } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect } from '../modes/select';\nimport { uiCmd } from '../ui/cmd';\nimport { utilGetAllNodes, utilTotalExtent } from '../util';\n\n\nexport function operationDelete(context, selectedIDs) {\n var multi = (selectedIDs.length === 1 ? 'single' : 'multiple');\n var action = actionDeleteMultiple(selectedIDs);\n var nodes = utilGetAllNodes(selectedIDs, context.graph());\n var coords = nodes.map(function(n) { return n.loc; });\n var extent = utilTotalExtent(selectedIDs, context.graph());\n\n\n var operation = function() {\n var nextSelectedID;\n var nextSelectedLoc;\n\n if (selectedIDs.length === 1) {\n var id = selectedIDs[0];\n var entity = context.entity(id);\n var geometry = entity.geometry(context.graph());\n var parents = context.graph().parentWays(entity);\n var parent = parents[0];\n\n // Select the next closest node in the way.\n if (geometry === 'vertex') {\n var nodes = parent.nodes;\n var i = nodes.indexOf(id);\n\n if (i === 0) {\n i++;\n } else if (i === nodes.length - 1) {\n i--;\n } else {\n var a = geoSphericalDistance(entity.loc, context.entity(nodes[i - 1]).loc);\n var b = geoSphericalDistance(entity.loc, context.entity(nodes[i + 1]).loc);\n i = a < b ? i - 1 : i + 1;\n }\n\n nextSelectedID = nodes[i];\n nextSelectedLoc = context.entity(nextSelectedID).loc;\n }\n }\n\n context.perform(action, operation.annotation());\n context.validator().validate();\n\n if (nextSelectedID && nextSelectedLoc) {\n if (context.hasEntity(nextSelectedID)) {\n context.enter(modeSelect(context, [nextSelectedID]).follow(true));\n } else {\n context.map().centerEase(nextSelectedLoc);\n context.enter(modeBrowse(context));\n }\n } else {\n context.enter(modeBrowse(context));\n }\n\n };\n\n\n operation.available = function() {\n return true;\n };\n\n\n operation.disabled = function() {\n if (extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n } else if (selectedIDs.some(protectedMember)) {\n return 'part_of_relation';\n } else if (selectedIDs.some(incompleteRelation)) {\n return 'incomplete_relation';\n } else if (selectedIDs.some(hasWikidataTag)) {\n return 'has_wikidata_tag';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n\n function hasWikidataTag(id) {\n var entity = context.entity(id);\n return entity.tags.wikidata && entity.tags.wikidata.trim().length > 0;\n }\n\n function incompleteRelation(id) {\n var entity = context.entity(id);\n return entity.type === 'relation' && !entity.isComplete(context.graph());\n }\n\n function protectedMember(id) {\n var entity = context.entity(id);\n if (entity.type !== 'way') return false;\n\n var parents = context.graph().parentRelations(entity);\n for (var i = 0; i < parents.length; i++) {\n var parent = parents[i];\n var type = parent.tags.type;\n var role = parent.memberById(id).role || 'outer';\n if (type === 'route' || type === 'boundary' || (type === 'multipolygon' && role === 'outer')) {\n return true;\n }\n }\n return false;\n }\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.delete.' + disable + '.' + multi) :\n t.append('operations.delete.description.' + multi);\n };\n\n\n operation.annotation = function() {\n return selectedIDs.length === 1 ?\n t('operations.delete.annotation.' + context.graph().geometry(selectedIDs[0])) :\n t('operations.delete.annotation.feature', { n: selectedIDs.length });\n };\n\n\n operation.id = 'delete';\n operation.keys = [uiCmd('\u2318\u232B'), uiCmd('\u2318\u2326'), uiCmd('\u2326')];\n operation.title = t.append('operations.delete.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import { t, localizer } from '../core/localizer';\nimport { modeDrawLine } from '../modes/draw_line';\nimport { operationDelete } from '../operations/delete';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { osmRoutableHighwayTagValues } from '../osm/tags';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\nexport function validationDisconnectedWay() {\n var type = 'disconnected_way';\n\n function isTaggedAsHighway(entity) {\n return osmRoutableHighwayTagValues[entity.tags.highway];\n }\n\n var validation = function checkDisconnectedWay(entity, graph) {\n\n var routingIslandWays = routingIslandForEntity(entity);\n if (!routingIslandWays) return [];\n\n return [new validationIssue({\n type: type,\n subtype: 'highway',\n severity: 'warning',\n message: function(context) {\n var entity = this.entityIds.length && context.hasEntity(this.entityIds[0]);\n var label = entity && utilDisplayLabel(entity, context.graph());\n return t.append('issues.disconnected_way.routable.message', { count: this.entityIds.length, highway: label });\n },\n reference: showReference,\n entityIds: Array.from(routingIslandWays).map(function(way) { return way.id; }),\n dynamicFixes: makeFixes\n })];\n\n\n function makeFixes(context) {\n\n var fixes = [];\n\n var singleEntity = this.entityIds.length === 1 && context.hasEntity(this.entityIds[0]);\n\n if (singleEntity) {\n\n if (singleEntity.type === 'way' && !singleEntity.isClosed()) {\n\n var textDirection = localizer.textDirection();\n\n var startFix = makeContinueDrawingFixIfAllowed(textDirection, singleEntity.first(), 'start');\n if (startFix) fixes.push(startFix);\n\n var endFix = makeContinueDrawingFixIfAllowed(textDirection, singleEntity.last(), 'end');\n if (endFix) fixes.push(endFix);\n }\n if (!fixes.length) {\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.connect_feature.title')\n }));\n }\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.delete_feature.title'),\n entityIds: [singleEntity.id],\n onClick: function(context) {\n var id = this.issue.entityIds[0];\n var operation = operationDelete(context, [id]);\n if (!operation.disabled()) {\n operation();\n }\n }\n }));\n } else {\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.connect_features.title')\n }));\n }\n\n return fixes;\n }\n\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.disconnected_way.routable.reference'));\n }\n\n function routingIslandForEntity(entity) {\n\n var routingIsland = new Set(); // the interconnected routable features\n var waysToCheck = []; // the queue of remaining routable ways to traverse\n\n function queueParentWays(node) {\n graph.parentWays(node).forEach(function(parentWay) {\n if (!routingIsland.has(parentWay) && // only check each feature once\n isRoutableWay(parentWay, false)) { // only check routable features\n routingIsland.add(parentWay);\n waysToCheck.push(parentWay);\n }\n });\n }\n\n if (entity.type === 'way' && isRoutableWay(entity, true)) {\n\n routingIsland.add(entity);\n waysToCheck.push(entity);\n\n } else if (entity.type === 'node' && isRoutableNode(entity)) {\n\n routingIsland.add(entity);\n queueParentWays(entity);\n\n } else {\n // this feature isn't routable, cannot be a routing island\n return null;\n }\n\n while (waysToCheck.length) {\n var wayToCheck = waysToCheck.pop();\n var childNodes = graph.childNodes(wayToCheck);\n for (var i in childNodes) {\n var vertex = childNodes[i];\n\n if (isConnectedVertex(vertex)) {\n // found a link to the wider network, not a routing island\n return null;\n }\n\n if (isRoutableNode(vertex)) {\n routingIsland.add(vertex);\n }\n\n queueParentWays(vertex);\n }\n }\n\n // no network link found, this is a routing island, return its members\n return routingIsland;\n }\n\n function isConnectedVertex(vertex) {\n // assume ways overlapping unloaded tiles are connected to the wider road network - #5938\n var osm = services.osm;\n if (osm && !osm.isDataLoaded(vertex.loc)) return true;\n\n // entrances are considered connected\n if (vertex.tags.entrance &&\n vertex.tags.entrance !== 'no') return true;\n if (vertex.tags.amenity === 'parking_entrance') return true;\n\n return false;\n }\n\n function isRoutableNode(node) {\n // treat elevators as distinct features in the highway network\n if (node.tags.highway === 'elevator') return true;\n return false;\n }\n\n function isRoutableWay(way, ignoreInnerWays) {\n if (way.tags.golf === 'path' || way.tags.golf === 'cartpath') {\n // skip golf paths #11863\n return false;\n }\n\n if (isTaggedAsHighway(way) || way.tags.route === 'ferry') return true;\n\n return graph.parentRelations(way).some(function(parentRelation) {\n if (parentRelation.tags.type === 'route' &&\n parentRelation.tags.route === 'ferry') return true;\n\n if (parentRelation.isMultipolygon() &&\n isTaggedAsHighway(parentRelation) &&\n (!ignoreInnerWays || parentRelation.memberById(way.id).role !== 'inner')) return true;\n\n return false;\n });\n }\n\n function makeContinueDrawingFixIfAllowed(textDirection, vertexID, whichEnd) {\n var vertex = graph.hasEntity(vertexID);\n if (!vertex || vertex.tags.noexit === 'yes') return null;\n\n var useLeftContinue = (whichEnd === 'start' && textDirection === 'ltr') ||\n (whichEnd === 'end' && textDirection === 'rtl');\n\n return new validationIssueFix({\n icon: 'iD-operation-continue' + (useLeftContinue ? '-left' : ''),\n title: t.append('issues.fix.continue_from_' + whichEnd + '.title'),\n entityIds: [vertexID],\n onClick: function(context) {\n var wayId = this.issue.entityIds[0];\n var way = context.hasEntity(wayId);\n var vertexId = this.entityIds[0];\n var vertex = context.hasEntity(vertexId);\n\n if (!way || !vertex) return;\n\n // make sure the vertex is actually visible and editable\n var map = context.map();\n if (!context.editable() || !map.trimmedExtent().contains(vertex.loc)) {\n map.zoomToEase(vertex);\n }\n\n context.enter(\n modeDrawLine(context, wayId, context.graph(), 'line', way.affix(vertexId), true)\n );\n }\n });\n }\n\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { osmTimelessFeatureTagValues } from '../osm/tags';\n\nexport function validationMissingStartDate(context) {\n const type = 'missing_start_date';\n\n const validation = function checkMissingStartDate(entity, graph) {\n // If start_date is not empty, return nothing\n if (entity.tags && (entity.tags.start_date || entity.tags['start_date:edtf'])) return [];\n // If entity has no tags, return nothing\n if (Object.keys(entity.tags).length === 0) return [];\n // Rule should be ignored for natural entities and waterways\n if (entity.tags && (\n (entity.tags.natural && osmTimelessFeatureTagValues[entity.tags.natural]) ||\n (entity.tags.waterway && osmTimelessFeatureTagValues[entity.tags.waterway]) ||\n (entity.tags.water && osmTimelessFeatureTagValues[entity.tags.water]))) return [];\n\n // If entity is a vertex node\n var osm = context.connection();\n var isUnloadedNode = entity.type === 'node' && osm && !osm.isDataLoaded(entity.loc);\n\n // Should skip this validation if node is unloaded, is a vertex or has parent relations\n if (isUnloadedNode ||\n // allow untagged nodes that are part of ways\n entity.geometry(graph) === 'vertex' ||\n // allow untagged entities that are part of relations\n entity.hasParentRelations(graph)) return [];\n\n const entityID = entity.id;\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.missing_start_date.reference'));\n }\n\n return [new validationIssue({\n type: type,\n severity: 'warning',\n message: (context) => {\n const entity = context.hasEntity(entityID);\n return entity ? t.append('issues.missing_start_date.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [entityID],\n dynamicFixes: () => {\n return [\n new validationIssueFix({ title: t.append('issues.fix.add_start_date.title')})\n ];\n }\n })];\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { localizer, t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilNormalizeDateString, utilEDTFFromOSMDateString } from '../util';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { actionChangeTags } from '../actions/change_tags';\n\nimport * as edtf from 'edtf';\n\nexport function validationFormatting() {\n var type = 'invalid_format';\n\n var validation = function(entity) {\n var issues = [];\n\n function showReferenceDate(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.invalid_format.date.reference'));\n }\n\n function validateDate(key, msgKey) {\n if (!entity.tags[key]) return;\n var normalized = utilNormalizeDateString(entity.tags[key]);\n if (normalized !== null && entity.tags[key] === normalized.value) return;\n issues.push(new validationIssue({\n type: type,\n subtype: 'date',\n severity: 'error',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.date.message_' + msgKey,\n { feature: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: showReferenceDate,\n entityIds: [entity.id],\n hash: key + entity.tags[key],\n dynamicFixes: function() {\n var fixes = [];\n\n let alternatives = [];\n if (normalized !== null) {\n let label = normalized.date.toLocaleDateString(localizer.localeCodes(), normalized.localeOptions);\n alternatives.push({\n date: normalized.value,\n label: label || normalized.value,\n });\n }\n let edtfFromOSM = utilEDTFFromOSMDateString(entity.tags[key]);\n if (edtfFromOSM) {\n let label;\n try {\n label = edtf.default(edtfFromOSM).format(localizer.localeCode());\n } catch {\n label = edtfFromOSM;\n }\n alternatives.push({\n edtf: edtfFromOSM,\n label: label,\n });\n }\n\n fixes.push(...alternatives.map(alt => new validationIssueFix({\n title: t.append('issues.fix.reformat_date.title', { date: alt.label }),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n if (alt.date) {\n newTags[key] = alt.date;\n } else {\n delete newTags[key];\n }\n newTags[key + ':edtf'] = alt.edtf;\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.reformat_date.annotation'));\n }\n })));\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n delete newTags[key];\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.remove_tag.annotation'));\n }\n }));\n\n return fixes;\n }\n }));\n }\n validateDate('start_date', 'start');\n validateDate('end_date', 'end');\n\n function showReferenceEDTF(selection, parserError) {\n let message;\n if (typeof parserError.offset === 'number' && parserError.token) {\n message = t.append('issues.invalid_format.edtf.reference', {\n token: parserError.token.value,\n position: (parserError.offset + 1).toLocaleString(localizer.localeCodes()),\n });\n } else if (parserError.message) {\n message = selection => selection.append('span')\n .attr('class', 'localized-text')\n .attr('lang', 'en')\n .text(parserError.message.replace(/^edtf: /, ''));\n }\n if (!message) {\n return;\n }\n\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(message);\n }\n\n function validateEDTF(key, msgKey) {\n key += ':edtf';\n if (!entity.tags[key]) return;\n let parserError;\n try {\n edtf.parse(entity.tags[key]);\n return;\n } catch (e) {\n parserError = e;\n }\n issues.push(new validationIssue({\n type: type,\n subtype: 'date',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.edtf.message_' + msgKey,\n { feature: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: selection => showReferenceEDTF(selection, parserError),\n entityIds: [entity.id],\n hash: key + entity.tags[key],\n dynamicFixes: function() {\n var fixes = [];\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n delete newTags[key];\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.remove_tag.annotation'));\n }\n }));\n return fixes;\n }\n }));\n }\n validateEDTF('start_date', 'start');\n validateEDTF('end_date', 'end');\n\n function isValidEmail(email) {\n // Emails in OSM are going to be official so they should be pretty simple\n // Using negated lists to better support all possible unicode characters (#6494)\n var valid_email = /^[^\\(\\)\\\\,\":;<>@\\[\\]]+@[^\\(\\)\\\\,\":;<>@\\[\\]\\.]+(?:\\.[a-z0-9-]+)*$/i;\n\n // An empty value is also acceptable\n return (!email || valid_email.test(email));\n }\n\n function showReferenceEmail(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.invalid_format.email.reference'));\n }\n\n function isValidURL(url, strict = false) {\n try {\n // First try strict WHATWG parsing\n const link = new URL(url);\n return link.href.includes(url);\n } catch {\n if (strict) return false;\n // Fallback: accept if it looks like a valid scheme://something, even if semicolons are present\n return /^https?:\\/\\/\\S+$/i.test(url);\n }\n }\n\n function cleanWikimediaCommonsReference(value) {\n if (!value) return null;\n for (const prefix of ['file', 'datei', 'fichier', 'plik']) {\n if (!value.toLowerCase().startsWith(prefix + ':')) continue;\n return 'File' + decodeURIComponent(value.slice(prefix.length));\n }\n if (value.startsWith('Category:')) return decodeURIComponent(value);\n return null;\n }\n\n function showReferenceWebsite(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.invalid_format.website.reference'));\n }\n\n const websiteValidationIssueBase = {\n type: type,\n subtype: 'website',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.website.message' + (this.data?.count > 1 ? '_multi' : ''),\n { feature: utilDisplayLabel(entity, context.graph()), site: this.data?.value }) : '';\n },\n dynamicFixes: function(context) {\n const wikimedia_commons_reference = cleanWikimediaCommonsReference(this.data?.value);\n const fixes = [{ protocol: 'https', icon: 'temaki-lock' }, { protocol: 'http' }]\n .filter(fix => isValidURL(fix.protocol + '://' + this.data?.value, true))\n .map(fix => new validationIssueFix({\n icon: fix.icon,\n title: t.append('issues.fix.add_protocol_'+ fix.protocol +'.title'),\n onClick: function() {\n const entityID = this.issue.entityIds[0];\n const entity = context.entity(entityID);\n if (!entity) return;\n const key = this.issue.data.key;\n const tags = Object.assign({}, entity.tags);\n tags[key] = entity.tags[key]\n .split(';')\n .map(s => s.trim())\n .map(s => isValidURL(s) ? s : fix.protocol + '://' + s)\n .join(';');\n\n context.perform(\n actionChangeTags(entityID, tags),\n t('issues.fix.add_protocol_'+ fix.protocol +'.annotation')\n );\n }\n }));\n if (this.data?.key === 'image' && !entity.tags.wikimedia_commons && wikimedia_commons_reference) {\n fixes.push(new validationIssueFix({\n icon: 'iD-icon-out-link',\n title: t.append('issues.fix.move_value_to_wikimedia_commons.title'),\n onClick: function() {\n const entityID = this.issue.entityIds[0];\n const entity = context.entity(entityID);\n if (!entity) return;\n const key = this.issue.data.key;\n const tags = Object.assign({}, entity.tags);\n tags.wikimedia_commons = wikimedia_commons_reference;\n delete tags[key];\n\n context.perform(\n actionChangeTags(entityID, tags),\n t('issues.fix.move_value_to_wikimedia_commons.annotation')\n );\n }\n }));\n }\n return fixes;\n },\n reference: showReferenceWebsite,\n entityIds: [entity.id]\n };\n\n Object.entries(entity.tags).map(function([key, tag]) {\n if (!/\\b(website|url)\\b|^image$/i.test(key)) return null;\n if (!tag) return null;\n const value = tag.trim();\n if (!value) return null;\n if (!value.includes(';')) {\n // No semicolon, validate whole value\n if (isValidURL(value)) return null;\n return {\n ...websiteValidationIssueBase,\n data: { key, value },\n hash: key + '=' + value\n };\n }\n const invalidParts = value.split(';').map(s => s.trim()).filter(x => !isValidURL(x));\n if (!invalidParts.length) {\n if (isValidURL(value)) return null;\n // All split parts valid, but whole value still invalid\n return {\n ...websiteValidationIssueBase,\n data: { key, value },\n hash: key + '=' + value\n };\n }\n return {\n ...websiteValidationIssueBase,\n data: { key, value: invalidParts.join(', '), count: invalidParts.length },\n hash: key + '=' + invalidParts.join()\n };\n }).filter(issue => issue !== null).forEach(issueData => issues.push(new validationIssue(issueData)));\n\n if (entity.tags.email) {\n // Multiple emails are possible\n var emails = entity.tags.email\n .split(';')\n .map(function(s) { return s.trim(); })\n .filter(function(x) { return !isValidEmail(x); });\n\n if (emails.length) {\n issues.push(new validationIssue({\n type: type,\n subtype: 'email',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.email.message' + this.data,\n { feature: utilDisplayLabel(entity, context.graph()), email: emails.join(', ') }) : '';\n },\n reference: showReferenceEmail,\n entityIds: [entity.id],\n hash: emails.join(),\n data: (emails.length > 1) ? '_multi' : ''\n }));\n }\n }\n\n return issues;\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationHelpRequest(context) {\n var type = 'help_request';\n\n var validation = function checkFixmeTag(entity) {\n\n if (!entity.tags.fixme) return [];\n\n // don't flag fixmes on features added by the user\n if (entity.version === undefined) return [];\n\n if (entity.v !== undefined) {\n var baseEntity = context.history().base().hasEntity(entity.id);\n // don't flag fixmes added by the user on existing features\n if (!baseEntity || !baseEntity.tags.fixme) return [];\n }\n\n return [new validationIssue({\n type: type,\n subtype: 'fixme_tag',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.fixme_tag.message', {\n feature: utilDisplayLabel(entity, context.graph(), true /* verbose */)\n }) : '';\n },\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n title: t.append('issues.fix.address_the_concern.title')\n })\n ];\n },\n reference: showReference,\n entityIds: [entity.id]\n })];\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.fixme_tag.reference'));\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t, localizer } from '../core/localizer';\nimport { modeDrawLine } from '../modes/draw_line';\nimport { actionReverse } from '../actions/reverse';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { osmFlowingWaterwayTagValues, osmRoutableHighwayTagValues } from '../osm/tags';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\nexport function validationImpossibleOneway() {\n const type = 'impossible_oneway';\n\n const validation = function checkImpossibleOneway(entity, graph) {\n if (entity.type !== 'way' || entity.geometry(graph) !== 'line') return [];\n if (entity.isClosed()) return [];\n if (!typeForWay(entity)) return [];\n if (!entity.isOneWay()) return [];\n\n return [\n ...issuesForNode(entity, entity.first()),\n ...issuesForNode(entity, entity.last())\n ];\n\n function typeForWay(way) {\n if (way.geometry(graph) !== 'line') return null;\n\n if (osmRoutableHighwayTagValues[way.tags.highway]) return 'highway';\n if (osmFlowingWaterwayTagValues[way.tags.waterway]) return 'waterway';\n return null;\n }\n\n function nodeOccursMoreThanOnce(way, nodeID) {\n let occurrences = 0;\n for (const index in way.nodes) {\n if (way.nodes[index] === nodeID) {\n occurrences++;\n if (occurrences > 1) return true;\n }\n }\n return false;\n }\n\n function isConnectedViaOtherTypes(way, node) {\n\n var wayType = typeForWay(way);\n\n if (wayType === 'highway') {\n // entrances are considered connected\n if (node.tags.entrance && node.tags.entrance !== 'no') return true;\n if (node.tags.amenity === 'parking_entrance') return true;\n } else if (wayType === 'waterway') {\n if (node.id === way.first()) {\n // multiple waterways may start at the same spring\n if (node.tags.natural === 'spring') return true;\n } else {\n // multiple waterways may end at the same drain\n if (node.tags.manhole === 'drain') return true;\n }\n }\n\n return graph.parentWays(node).some(function(parentWay) {\n if (parentWay.id === way.id) return false;\n\n if (wayType === 'highway') {\n\n // allow connections to highway areas\n if (parentWay.geometry(graph) === 'area' &&\n osmRoutableHighwayTagValues[parentWay.tags.highway]) return true;\n\n // count connections to ferry routes as connected\n if (parentWay.tags.route === 'ferry') return true;\n\n return graph.parentRelations(parentWay).some(function(parentRelation) {\n if (parentRelation.tags.type === 'route' &&\n parentRelation.tags.route === 'ferry') return true;\n\n // allow connections to highway multipolygons\n return parentRelation.isMultipolygon() && osmRoutableHighwayTagValues[parentRelation.tags.highway];\n });\n } else if (wayType === 'waterway') {\n // multiple waterways may start or end at a water body at the same node\n if (parentWay.tags.natural === 'water' ||\n parentWay.tags.natural === 'coastline') return true;\n }\n return false;\n });\n }\n\n function issuesForNode(way, nodeID) {\n const isFirst = (nodeID === way.first()) ^ way.isOneWayBackwards();\n const wayType = typeForWay(way);\n\n // ignore if this way is self-connected at this node\n if (nodeOccursMoreThanOnce(way, nodeID)) return [];\n\n const osm = services.osm;\n if (!osm) return [];\n const node = graph.hasEntity(nodeID);\n // ignore if this node or its tile are unloaded\n if (!node || !osm.isDataLoaded(node.loc)) return [];\n\n if (isConnectedViaOtherTypes(way, node)) return [];\n\n const attachedWaysOfSameType = graph.parentWays(node).filter(parentWay => {\n if (parentWay.id === way.id) return false;\n return typeForWay(parentWay) === wayType;\n });\n\n // assume it's okay for waterways to start or end disconnected for now\n if (wayType === 'waterway' && attachedWaysOfSameType.length === 0) return [];\n\n const attachedOneways = attachedWaysOfSameType\n .filter(attachedWay => attachedWay.isOneWay());\n\n // ignore if the way is connected to some non-oneway features\n if (attachedOneways.length < attachedWaysOfSameType.length) return [];\n\n if (attachedOneways.length) {\n const connectedEndpointsOkay = attachedOneways.some(attachedOneway => {\n const isAttachedBackwards = attachedOneway.isOneWayBackwards();\n if ((isFirst ^ isAttachedBackwards\n ? attachedOneway.first()\n : attachedOneway.last()\n ) !== nodeID) {\n return true;\n }\n if (nodeOccursMoreThanOnce(attachedOneway, nodeID)) return true;\n return false;\n });\n if (connectedEndpointsOkay) return [];\n }\n\n const placement = isFirst ? 'start' : 'end';\n let messageID = wayType + '.';\n let referenceID = wayType + '.';\n\n if (wayType === 'waterway') {\n messageID += 'connected.' + placement;\n referenceID += 'connected';\n } else {\n messageID += placement;\n referenceID += placement;\n }\n\n return [new validationIssue({\n type: type,\n subtype: wayType,\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.impossible_oneway.' + messageID + '.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: getReference(referenceID),\n entityIds: [way.id, node.id],\n dynamicFixes: function() {\n\n var fixes = [];\n\n if (attachedOneways.length) {\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-reverse',\n title: t.append('issues.fix.reverse_feature.title'),\n entityIds: [way.id],\n onClick: function(context) {\n var id = this.issue.entityIds[0];\n context.perform(actionReverse(id), t('operations.reverse.annotation.line', { n: 1 }));\n }\n }));\n }\n if (node.tags.noexit !== 'yes') {\n var textDirection = localizer.textDirection();\n var useLeftContinue = (isFirst && textDirection === 'ltr') ||\n (!isFirst && textDirection === 'rtl');\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-continue' + (useLeftContinue ? '-left' : ''),\n title: t.append('issues.fix.continue_from_' + (isFirst ? 'start' : 'end') + '.title'),\n onClick: function(context) {\n var entityID = this.issue.entityIds[0];\n var vertexID = this.issue.entityIds[1];\n var way = context.entity(entityID);\n var vertex = context.entity(vertexID);\n continueDrawing(way, vertex, context);\n }\n }));\n }\n\n return fixes;\n },\n loc: node.loc\n })];\n\n function getReference(referenceID) {\n return function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.impossible_oneway.' + referenceID + '.reference'));\n };\n }\n }\n };\n\n function continueDrawing(way, vertex, context) {\n // make sure the vertex is actually visible and editable\n var map = context.map();\n if (!context.editable() || !map.trimmedExtent().contains(vertex.loc)) {\n map.zoomToEase(vertex);\n }\n\n context.enter(\n modeDrawLine(context, way.id, context.graph(), 'line', way.affix(vertex.id), true)\n );\n }\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\nconst incompatibleRules = [\n {\n id: 'amap',\n regex: /(^amap$|^amap\\.com|autonavi|mapabc|\u9AD8\u5FB7)/i\n },\n {\n id: 'baidu',\n regex: /(baidu|mapbar|\u767E\u5EA6)/i\n },\n {\n id: 'google',\n regex: /(google)/i,\n exceptRegex: /((books|drive)\\.google|google\\s?(books|drive|plus))|(esri\\/Google_(Africa|Open)_Buildings)|(:\\/\\/\\S+\\/\\S+(google)\\S+)/i\n }\n];\n\n/**\n * @param {string} str String (e.g. tag value) to check for incompatible sources\n * @returns {{id:string, regex: RegExp, exceptRegex?: RegExp}[]}\n */\nexport function getIncompatibleSources(str) {\n return incompatibleRules\n .filter(rule =>\n rule.regex.test(str) &&\n !rule.exceptRegex?.test(str)\n );\n}\n\nexport function validationIncompatibleSource() {\n const type = 'incompatible_source';\n\n const validation = function checkIncompatibleSource(entity) {\n const entitySources = entity.tags && entity.tags.source && entity.tags.source.split(';');\n if (!entitySources) return [];\n\n const entityID = entity.id;\n\n return entitySources\n .flatMap(source => getIncompatibleSources(source)\n .map(matchRule => new validationIssue({\n type: type,\n severity: 'warning',\n message: (context) => {\n const entity = context.hasEntity(entityID);\n return entity ? t.append('issues.incompatible_source.feature.message', {\n feature: utilDisplayLabel(entity, context.graph(), true /* verbose */),\n value: source\n }) : '';\n },\n reference: getReference(matchRule.id),\n entityIds: [entityID],\n hash: source,\n dynamicFixes: () => {\n return [\n new validationIssueFix({ title: t.append('issues.fix.remove_proprietary_data.title') })\n ];\n }\n }))\n );\n\n function getReference(id) {\n return function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append(`issues.incompatible_source.reference.${id}`));\n };\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { services } from '../services';\n\n\nexport function validationMaprules() {\n var type = 'maprules';\n\n var validation = function checkMaprules(entity, graph) {\n if (!services.maprules) return [];\n\n var rules = services.maprules.validationRules();\n var issues = [];\n\n for (var i = 0; i < rules.length; i++) {\n var rule = rules[i];\n rule.findIssues(entity, graph, issues);\n }\n\n return issues;\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { localizer, t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilNormalizeDateString } from '../util/ohm_date';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\nimport * as edtf from 'edtf';\n\nexport function validationMismatchedDates() {\n let type = 'mismatched_dates';\n\n function parseEDTF(value) {\n try {\n let parsed = edtf.default(value);\n\n // According to edtf.js, an extended interval with an unknown start or end covers no date.\n // This isn't useful for the purpose of testing whether the basic date matches, so treat it as an unspecified start or end.\n if (parsed.lower === null) {\n parsed.lower = Infinity;\n }\n if (parsed.upper === null) {\n parsed.upper = Infinity;\n }\n\n return parsed;\n } catch {\n // Already handled by invalid_format rule.\n return;\n }\n }\n\n function getReplacementDates(parsed) {\n let likelyDates = new Set();\n\n let valueFromDate = (date, precision) => {\n date.precision = precision;\n return date.edtf.split('T')[0];\n };\n\n if (Number.isFinite(parsed.min)) {\n let min = edtf.default(parsed.min);\n let precision = (parsed.lower || parsed.first || parsed).precision;\n likelyDates.add(valueFromDate(min, precision));\n }\n\n if (Number.isFinite(parsed.max)) {\n let max = edtf.default(parsed.max);\n let precision = (parsed.upper || parsed.last || parsed).precision;\n likelyDates.add(valueFromDate(max, precision));\n }\n\n let sortedDates = [...likelyDates];\n sortedDates.sort();\n return sortedDates;\n }\n\n let validation = function(entity) {\n let issues = [];\n\n function showReferenceEDTF(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.mismatched_dates.edtf.reference'));\n }\n\n function getDynamicFixes(key, parsed) {\n let fixes = [];\n\n let replacementDates = getReplacementDates(parsed);\n fixes.push(...replacementDates.map(value => {\n let normalized = utilNormalizeDateString(value);\n let localeDateString = normalized.date.toLocaleDateString(localizer.languageCode(), normalized.localeOptions);\n return new validationIssueFix({\n title: t.append('issues.fix.reformat_date.title', { date: localeDateString }),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n newTags[key] = normalized.value;\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.reformat_date.annotation'));\n }\n });\n }));\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n delete newTags[key];\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.remove_tag.annotation'));\n }\n }));\n\n return fixes;\n }\n\n function validateEDTF(key, msgKey) {\n if (!entity.tags[key] || !entity.tags[key + ':edtf']) return;\n let basic = entity.tags[key];\n let basicAsEDTF = parseEDTF(basic);\n let parsed = parseEDTF(entity.tags[key + ':edtf']);\n if (!basicAsEDTF || !parsed || parsed.covers(basicAsEDTF) || basicAsEDTF.covers(parsed)) return;\n\n // start_date and end_date disallow time precision. Transform the basic date into a daylong range in EDTF to allow a comparison to an EDTF date with time precision.\n // https://github.com/OpenHistoricalMap/issues/issues/764\n if (basic.match('^-?[0-9]+-[0-9]{2}-[0-9]{2}$')) {\n let basicTime = parseEDTF(`${basic}T00:00:00/${basic}T24:00:00`);\n if (basicTime && (parsed.covers(basicTime) || basicTime.covers(parsed))) return;\n }\n\n issues.push(new validationIssue({\n type: type,\n subtype: 'date',\n severity: 'warning',\n message: function(context) {\n let entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.mismatched_dates.edtf.message_' + msgKey,\n { feature: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: showReferenceEDTF,\n entityIds: [entity.id],\n hash: key + entity.tags[key + ':edtf'],\n dynamicFixes: () => getDynamicFixes(key, parsed),\n }));\n }\n validateEDTF('start_date', 'start');\n validateEDTF('end_date', 'end');\n\n return issues;\n };\n\n validation.type = type;\n validation.parseEDTF = parseEDTF;\n validation.getReplacementDates = getReplacementDates;\n\n return validation;\n}\n", "import { deepEqual } from 'fast-equals';\n\nimport { actionAddVertex } from '../actions/add_vertex';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionMergeNodes } from '../actions/merge_nodes';\nimport { actionExtract } from '../actions/extract';\nimport { modeSelect } from '../modes/select';\nimport { osmJoinWays } from '../osm/multipolygon';\nimport { osmNodeGeometriesForTags, osmTagSuggestingArea } from '../osm/tags';\nimport { presetManager } from '../presets';\nimport { geoHasSelfIntersections, geoSphericalDistance } from '../geo';\nimport { t } from '../core/localizer';\nimport { utilTagText } from '../util';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationMismatchedGeometry() {\n var type = 'mismatched_geometry';\n\n function tagSuggestingLineIsArea(entity) {\n if (entity.type !== 'way' || entity.isClosed()) return null;\n\n var tagSuggestingArea = entity.tagSuggestingArea();\n\n if (!tagSuggestingArea) {\n return null;\n }\n\n var asLine = presetManager.matchTags(tagSuggestingArea, 'line');\n var asArea = presetManager.matchTags(tagSuggestingArea, 'area');\n if (asLine && asArea && deepEqual(asLine.tags, asArea.tags)) {\n // this tag also allows lines and making this an area wouldn't matter\n return null;\n }\n\n if (asLine.isFallback() && asArea.isFallback() && !deepEqual(tagSuggestingArea, { area: 'yes' })) {\n // if the entity matches the fallback preset, regardless of the\n // geometry, then changing the geometry will not help.\n return null;\n }\n\n return tagSuggestingArea;\n }\n\n\n function makeConnectEndpointsFixOnClick(way, graph) {\n // must have at least three nodes to close this automatically\n if (way.nodes.length < 3) return null;\n\n var nodes = graph.childNodes(way), testNodes;\n var firstToLastDistanceMeters = geoSphericalDistance(nodes[0].loc, nodes[nodes.length-1].loc);\n\n // if the distance is very small, attempt to merge the endpoints\n if (firstToLastDistanceMeters < 0.75) {\n testNodes = nodes.slice(); // shallow copy\n testNodes.pop();\n testNodes.push(testNodes[0]);\n // make sure this will not create a self-intersection\n if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {\n return function(context) {\n var way = context.entity(this.issue.entityIds[0]);\n context.perform(\n actionMergeNodes([way.nodes[0], way.nodes[way.nodes.length-1]], nodes[0].loc),\n t('issues.fix.connect_endpoints.annotation')\n );\n };\n }\n }\n\n // if the points were not merged, attempt to close the way\n testNodes = nodes.slice(); // shallow copy\n testNodes.push(testNodes[0]);\n // make sure this will not create a self-intersection\n if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {\n return function(context) {\n var wayId = this.issue.entityIds[0];\n var way = context.entity(wayId);\n var nodeId = way.nodes[0];\n var index = way.nodes.length;\n context.perform(\n actionAddVertex(wayId, nodeId, index),\n t('issues.fix.connect_endpoints.annotation')\n );\n };\n }\n }\n\n function lineTaggedAsAreaIssue(entity) {\n\n var tagSuggestingArea = tagSuggestingLineIsArea(entity);\n if (!tagSuggestingArea) return null;\n\n var validAsLine = false;\n var presetAsLine = presetManager.matchTags(entity.tags, 'line');\n if (presetAsLine) {\n validAsLine = true;\n var key = Object.keys(tagSuggestingArea)[0];\n if (presetAsLine.tags[key] && presetAsLine.tags[key] === '*') {\n // only matches a fallback preset of the tag which is suggesting to be an area\n validAsLine = false;\n }\n if (Object.keys(presetAsLine.tags).length === 0) {\n // only matches the fallback \"line\" preset\n validAsLine = false;\n }\n }\n\n return new validationIssue({\n type: type,\n subtype: 'area_as_line',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.tag_suggests_area.message', {\n feature: utilDisplayLabel(entity, 'area', true /* verbose */),\n tag: utilTagText({ tags: tagSuggestingArea })\n }) : '';\n },\n reference: showReference,\n entityIds: [entity.id],\n hash: JSON.stringify(tagSuggestingArea),\n dynamicFixes: function(context) {\n\n var fixes = [];\n\n var entity = context.entity(this.entityIds[0]);\n var connectEndsOnClick = makeConnectEndpointsFixOnClick(entity, context.graph());\n\n if (!validAsLine) {\n // only suggest to \"connect the ends\" if the feature is not also valid as a line\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.connect_endpoints.title'),\n onClick: connectEndsOnClick\n }));\n }\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n var entityId = this.issue.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n for (var key in tagSuggestingArea) {\n delete tags[key];\n }\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.remove_tag.annotation')\n );\n }\n }));\n\n return fixes;\n }\n });\n\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.tag_suggests_area.reference'));\n }\n }\n\n function vertexPointIssue(entity, graph) {\n // we only care about nodes\n if (entity.type !== 'node') return null;\n\n // ignore tagless points\n if (Object.keys(entity.tags).length === 0) return null;\n\n // address lines are special so just ignore them\n if (entity.isOnAddressLine(graph)) return null;\n\n var geometry = entity.geometry(graph);\n var allowedGeometries = osmNodeGeometriesForTags(entity.tags);\n\n if (geometry === 'point' && !allowedGeometries.point && allowedGeometries.vertex) {\n\n return new validationIssue({\n type: type,\n subtype: 'vertex_as_point',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.vertex_as_point.message', {\n feature: utilDisplayLabel(entity, 'vertex', true /* verbose */)\n }) : '';\n },\n reference: function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.vertex_as_point.reference'));\n },\n entityIds: [entity.id],\n dynamicFixes: () => [new validationIssueFix({\n icon: 'iD-operation-move',\n title: t.append('issues.fix.reposition_point_to_vertex.title')\n })]\n });\n\n } else if (geometry === 'vertex' && !allowedGeometries.vertex && allowedGeometries.point) {\n\n return new validationIssue({\n type: type,\n subtype: 'point_as_vertex',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.point_as_vertex.message', {\n feature: utilDisplayLabel(entity, 'point', true /* verbose */)\n }) : '';\n },\n reference: function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.point_as_vertex.reference'));\n },\n entityIds: [entity.id],\n dynamicFixes: extractPointDynamicFixes\n });\n }\n\n return null;\n }\n\n\n function otherMismatchIssue(entity, graph) {\n // ignore boring features\n if (!entity.hasInterestingTags()) return null;\n\n if (entity.type !== 'node' && entity.type !== 'way') return null;\n\n // address lines are special so just ignore them\n if (entity.type === 'node' && entity.isOnAddressLine(graph)) return null;\n\n var sourceGeom = entity.geometry(graph);\n\n var targetGeoms = entity.type === 'way' ? ['point', 'vertex'] : ['line', 'area'];\n\n if (sourceGeom === 'area') targetGeoms.unshift('line');\n\n var asSource = presetManager.match(entity, graph);\n\n const originalTargetGeom = targetGeoms.find(nodeGeom => {\n const asTarget = presetManager.matchTags(\n entity.tags,\n nodeGeom,\n entity.extent(graph).center(),\n );\n if (!asSource || !asTarget ||\n asSource === asTarget ||\n // sometimes there are two presets with the same tags for different geometries\n deepEqual(asSource.tags, asTarget.tags)) return false;\n\n if (asTarget.isFallback()) return false;\n\n var primaryKey = Object.keys(asTarget.tags)[0];\n\n // special case: buildings-as-points are discouraged by iD, but common in OSM, so ignore them\n if (primaryKey === 'building') return false;\n\n if (asTarget.tags[primaryKey] === '*') return false;\n\n return asSource.isFallback() || asSource.tags[primaryKey] === '*';\n });\n\n let targetGeom = originalTargetGeom;\n\n if (!targetGeom) return null;\n\n var subtype = targetGeom + '_as_' + sourceGeom;\n\n if (targetGeom === 'vertex') targetGeom = 'point';\n if (sourceGeom === 'vertex') sourceGeom = 'point';\n\n var referenceId = targetGeom + '_as_' + sourceGeom;\n\n var dynamicFixes;\n if (targetGeom === 'point') {\n dynamicFixes = extractPointDynamicFixes;\n\n } else if (sourceGeom === 'area' && targetGeom === 'line') {\n dynamicFixes = lineToAreaDynamicFixes;\n }\n\n return new validationIssue({\n type: type,\n subtype: subtype,\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.' + referenceId + '.message', {\n feature: utilDisplayLabel(entity, originalTargetGeom, true /* verbose */)\n }) : '';\n },\n reference: function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.mismatched_geometry.reference'));\n },\n entityIds: [entity.id],\n dynamicFixes: dynamicFixes\n });\n }\n\n function lineToAreaDynamicFixes(context) {\n\n var convertOnClick;\n\n var entityId = this.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n delete tags.area;\n if (!osmTagSuggestingArea(tags)) {\n // if removing the area tag would make this a line, offer that as a quick fix\n convertOnClick = function(context) {\n var entityId = this.issue.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n if (tags.area) {\n delete tags.area;\n }\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.convert_to_line.annotation')\n );\n };\n }\n\n return [\n new validationIssueFix({\n icon: 'iD-icon-line',\n title: t.append('issues.fix.convert_to_line.title'),\n onClick: convertOnClick\n })\n ];\n }\n\n function extractPointDynamicFixes(context) {\n\n var entityId = this.entityIds[0];\n\n var extractOnClick = null;\n if (!context.hasHiddenConnections(entityId)) {\n\n extractOnClick = function(context) {\n var entityId = this.issue.entityIds[0];\n var action = actionExtract(entityId, context.projection);\n context.perform(\n action,\n t('operations.extract.annotation', { n: 1 })\n );\n // re-enter mode to trigger updates\n context.enter(modeSelect(context, [action.getExtractedNodeID()]));\n };\n }\n\n return [\n new validationIssueFix({\n icon: 'iD-operation-extract',\n title: t.append('issues.fix.extract_point.title'),\n onClick: extractOnClick\n })\n ];\n }\n\n function unclosedMultipolygonPartIssues(entity, graph) {\n\n if (entity.type !== 'relation' ||\n !entity.isMultipolygon() ||\n entity.isDegenerate() ||\n // cannot determine issues for incompletely-downloaded relations\n !entity.isComplete(graph)) return [];\n\n var sequences = osmJoinWays(entity.members, graph);\n\n var issues = [];\n\n for (var i in sequences) {\n var sequence = sequences[i];\n\n if (!sequence.nodes) continue;\n\n var firstNode = sequence.nodes[0];\n var lastNode = sequence.nodes[sequence.nodes.length - 1];\n\n // part is closed if the first and last nodes are the same\n if (firstNode === lastNode) continue;\n\n var issue = new validationIssue({\n type: type,\n subtype: 'unclosed_multipolygon_part',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.unclosed_multipolygon_part.message', {\n feature: utilDisplayLabel(entity, context.graph(), true /* verbose */)\n }) : '';\n },\n reference: showReference,\n loc: sequence.nodes[0].loc,\n entityIds: [entity.id],\n hash: sequence.map(function(way) {\n return way.id;\n }).join()\n });\n issues.push(issue);\n }\n\n return issues;\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.unclosed_multipolygon_part.reference'));\n }\n }\n\n var validation = function checkMismatchedGeometry(entity, graph) {\n var vertexPoint = vertexPointIssue(entity, graph);\n if (vertexPoint) return [vertexPoint];\n\n var lineAsArea = lineTaggedAsAreaIssue(entity);\n if (lineAsArea) return [lineAsArea];\n\n var mismatch = otherMismatchIssue(entity, graph);\n if (mismatch) return [mismatch];\n\n return unclosedMultipolygonPartIssues(entity, graph);\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeMember } from '../actions/change_member';\nimport { actionDeleteMember } from '../actions/delete_member';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationMissingRole() {\n var type = 'missing_role';\n\n var validation = function checkMissingRole(entity, graph) {\n var issues = [];\n if (entity.type === 'way') {\n graph.parentRelations(entity).forEach(function(relation) {\n if (!relation.isMultipolygon()) return;\n\n var member = relation.memberById(entity.id);\n if (member && isMissingRole(member)) {\n issues.push(makeIssue(entity, relation, member));\n }\n });\n } else if (entity.type === 'relation' && entity.isMultipolygon()) {\n entity.indexedMembers().forEach(function(member) {\n var way = graph.hasEntity(member.id);\n if (way && isMissingRole(member)) {\n issues.push(makeIssue(way, entity, member));\n }\n });\n }\n\n return issues;\n };\n\n\n function isMissingRole(member) {\n return !member.role || !member.role.trim().length;\n }\n\n\n function makeIssue(way, relation, member) {\n return new validationIssue({\n type: type,\n severity: 'warning',\n message: function(context) {\n const member = context.hasEntity(this.entityIds[0]),\n relation = context.hasEntity(this.entityIds[1]);\n return (member && relation) ? t.append('issues.missing_role.message', {\n member: utilDisplayLabel(member, context.graph()),\n relation: utilDisplayLabel(relation, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [way.id, relation.id],\n extent: function(graph) {\n return graph.entity(this.entityIds[0]).extent(graph);\n },\n data: {\n member: member\n },\n hash: member.index.toString(),\n dynamicFixes: function() {\n return [\n makeAddRoleFix('inner'),\n makeAddRoleFix('outer'),\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_from_relation.title'),\n onClick: function(context) {\n context.perform(\n actionDeleteMember(this.issue.entityIds[1], this.issue.data.member.index),\n t('operations.delete_member.annotation', {\n n: 1\n })\n );\n }\n })\n ];\n }\n });\n\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.missing_role.multipolygon.reference'));\n }\n }\n\n\n function makeAddRoleFix(role) {\n return new validationIssueFix({\n title: t.append('issues.fix.set_as_' + role + '.title'),\n onClick: function(context) {\n var oldMember = this.issue.data.member;\n var member = { id: this.issue.entityIds[0], type: oldMember.type, role: role };\n context.perform(\n actionChangeMember(this.issue.entityIds[1], member, oldMember.index),\n t('operations.change_role.annotation', {\n n: 1\n })\n );\n }\n });\n }\n\n validation.type = type;\n\n return validation;\n}\n", "import { operationDelete } from '../operations/delete';\nimport { osmIsInterestingTag } from '../osm/tags';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationMissingTag(context) {\n var type = 'missing_tag';\n\n function hasDescriptiveTags(entity) {\n var onlyAttributeKeys = ['description', 'name', 'start_date', 'oneway'];\n var entityDescriptiveKeys = Object.keys(entity.tags)\n .filter(function(k) {\n if (k === 'area' || !osmIsInterestingTag(k)) return false;\n\n return !onlyAttributeKeys.some(function(attributeKey) {\n return k === attributeKey || k.indexOf(attributeKey + ':') === 0;\n });\n });\n\n if (entity.type === 'relation' &&\n entityDescriptiveKeys.length === 1 &&\n entity.tags.type === 'multipolygon') {\n // this relation's only interesting tag just says its a multipolygon,\n // which is not descriptive enough\n return false;\n }\n\n return entityDescriptiveKeys.length > 0;\n }\n\n function isUnknownRoad(entity) {\n return entity.type === 'way' && entity.tags.highway === 'road';\n }\n\n function isUntypedRelation(entity) {\n return entity.type === 'relation' && !entity.tags.type;\n }\n\n var validation = function checkMissingTag(entity, graph) {\n\n var subtype;\n\n var osm = context.connection();\n var isUnloadedNode = entity.type === 'node' && osm && !osm.isDataLoaded(entity.loc);\n\n // we can't know if the node is a vertex if the tile is undownloaded\n if (!isUnloadedNode &&\n // allow untagged nodes that are part of ways\n entity.geometry(graph) !== 'vertex' &&\n // allow untagged entities that are part of relations\n !entity.hasParentRelations(graph)) {\n\n if (Object.keys(entity.tags).length === 0) {\n subtype = 'any';\n } else if (!hasDescriptiveTags(entity)) {\n subtype = 'descriptive';\n } else if (isUntypedRelation(entity)) {\n subtype = 'relation_type';\n }\n }\n\n // flag an unknown road even if it's a member of a relation\n if (!subtype && isUnknownRoad(entity)) {\n subtype = 'highway_classification';\n }\n\n if (!subtype) return [];\n\n var messageID = subtype === 'highway_classification' ? 'unknown_road' : 'missing_tag.' + subtype;\n var referenceID = subtype === 'highway_classification' ? 'unknown_road' : 'missing_tag';\n\n // can always delete if the user created it in the first place..\n var canDelete = (entity.version === undefined || entity.v !== undefined);\n var severity = (canDelete && subtype !== 'highway_classification') ? 'error' : 'warning';\n\n return [new validationIssue({\n type: type,\n subtype: subtype,\n severity: severity,\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.' + messageID + '.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [entity.id],\n dynamicFixes: function(context) {\n\n var fixes = [];\n\n var selectFixType = subtype === 'highway_classification' ? 'select_road_type' : 'select_preset';\n\n fixes.push(new validationIssueFix({\n icon: 'iD-icon-search',\n title: t.append('issues.fix.' + selectFixType + '.title'),\n onClick: function(context) {\n context.ui().sidebar.showPresetList();\n }\n }));\n\n var deleteOnClick;\n\n var id = this.entityIds[0];\n var operation = operationDelete(context, [id]);\n var disabledReasonID = operation.disabled();\n if (!disabledReasonID) {\n deleteOnClick = function(context) {\n var id = this.issue.entityIds[0];\n var operation = operationDelete(context, [id]);\n if (!operation.disabled()) {\n operation();\n }\n };\n }\n\n fixes.push(\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.delete_feature.title'),\n disabledReason: disabledReasonID ? t('operations.delete.' + disabledReasonID + '.single') : undefined,\n onClick: deleteOnClick\n })\n );\n\n return fixes;\n }\n })];\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.' + referenceID + '.reference'));\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { osmMutuallyExclusiveTagPairs } from '../osm/tags';\n\nexport function validationMutuallyExclusiveTags(/* context */) {\n const type = 'mutually_exclusive_tags';\n\n // https://wiki.openstreetmap.org/wiki/Special:WhatLinksHere/Property:P44\n const tagKeyPairs = osmMutuallyExclusiveTagPairs;\n\n const validation = function checkMutuallyExclusiveTags(entity /*, graph */) {\n\n let pairsFounds = tagKeyPairs.filter((pair) => {\n return (pair[0] in entity.tags && pair[1] in entity.tags);\n }).filter((pair) => {\n // noname=no is double-negation, thus positive and not conflicting. We'll ignore those\n return !((pair[0].match(/^(addr:)?no[a-z]/) && entity.tags[pair[0]] === 'no') ||\n (pair[1].match(/^(addr:)?no[a-z]/) && entity.tags[pair[1]] === 'no'));\n });\n\n // Additional:\n // Check if name and not:name (and similar) are set and both have the same value\n // not:name can actually have multiple values, separate by ;\n // https://taginfo.openstreetmap.org/search?q=not%3A#keys\n Object.keys(entity.tags).forEach((key) => {\n let negative_key = 'not:' + key;\n if (negative_key in entity.tags && entity.tags[negative_key].split(';').includes(entity.tags[key])) {\n pairsFounds.push([negative_key, key, 'same_value']);\n }\n // For name:xx we also compare against the not:name tag\n if (key.match(/^name:[a-z]+/)) {\n negative_key = 'not:name';\n if (negative_key in entity.tags && entity.tags[negative_key].split(';').includes(entity.tags[key])) {\n pairsFounds.push([negative_key, key, 'same_value']);\n }\n }\n });\n\n let issues = pairsFounds.map((pair) => {\n const subtype = pair[2] || 'default';\n return new validationIssue({\n type: type,\n subtype: subtype,\n severity: 'warning',\n message: function(context) {\n let entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append(`issues.${type}.${subtype}.message`, {\n feature: utilDisplayLabel(entity, context.graph()),\n tag1: pair[0],\n tag2: pair[1]\n }) : '';\n },\n reference: (selection) => showReference(selection, pair, subtype),\n entityIds: [entity.id],\n dynamicFixes: () => pair.slice(0,2).map((tagToRemove) => createIssueFix(tagToRemove))\n });\n });\n\n function createIssueFix(tagToRemove) {\n return new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_named_tag.title', { tag: tagToRemove }),\n onClick: function(context) {\n const entityId = this.issue.entityIds[0];\n const entity = context.entity(entityId);\n let tags = Object.assign({}, entity.tags); // shallow copy\n delete tags[tagToRemove];\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.remove_named_tag.annotation', { tag: tagToRemove })\n );\n }\n });\n }\n\n function showReference(selection, pair, subtype) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append(`issues.${type}.${subtype}.reference`, { tag1: pair[0], tag2: pair[1] }));\n }\n\n return issues;\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { actionSplit } from '../actions/split';\nimport { behaviorOperation } from '../behavior/operation';\nimport { modeSelect } from '../modes/select';\n\n\nexport function operationSplit(context, selectedIDs) {\n var _vertexIds = selectedIDs.filter(function(id) {\n return context.graph().geometry(id) === 'vertex';\n });\n var _selectedWayIds = selectedIDs.filter(function(id) {\n var entity = context.graph().hasEntity(id);\n return entity && entity.type === 'way';\n });\n var _isAvailable = _vertexIds.length > 0 &&\n _vertexIds.length + _selectedWayIds.length === selectedIDs.length;\n var _action = actionSplit(_vertexIds);\n var _ways = [];\n var _geometry = 'feature';\n var _waysAmount = 'single';\n var _nodesAmount = _vertexIds.length === 1 ? 'single' : 'multiple';\n\n if (_isAvailable) {\n if (_selectedWayIds.length) _action.limitWays(_selectedWayIds);\n _ways = _action.ways(context.graph());\n var geometries = {};\n _ways.forEach(function(way) {\n geometries[way.geometry(context.graph())] = true;\n });\n if (Object.keys(geometries).length === 1) {\n _geometry = Object.keys(geometries)[0];\n }\n _waysAmount = _ways.length === 1 ? 'single' : 'multiple';\n }\n\n\n var operation = function() {\n var difference = context.perform(_action, operation.annotation());\n // select both the nodes and the ways so the mapper can immediately disconnect them if desired\n var idsToSelect = _vertexIds.concat(difference.extantIDs().filter(function(id) {\n // filter out relations that may have had member additions\n return context.entity(id).type === 'way';\n }));\n context.enter(modeSelect(context, idsToSelect));\n };\n\n\n operation.relatedEntityIds = function() {\n return _selectedWayIds.length ? [] : _ways.map(way => way.id);\n };\n\n\n operation.available = function() {\n return _isAvailable;\n };\n\n\n operation.disabled = function() {\n var reason = _action.disabled(context.graph());\n if (reason) {\n return reason;\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n }\n return false;\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.split.' + disable) :\n t.append('operations.split.description.' + _geometry + '.' + _waysAmount + '.' + _nodesAmount + '_node');\n };\n\n\n operation.annotation = function() {\n return t('operations.split.annotation.' + _geometry, { n: _ways.length });\n };\n\n\n operation.icon = function() {\n if (_waysAmount === 'multiple') {\n return '#iD-operation-split-multiple';\n } else {\n return '#iD-operation-split';\n }\n };\n\n\n operation.id = 'split';\n operation.keys = [t('operations.split.key')];\n operation.title = t.append('operations.split.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import { t } from '../core/localizer';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { operationSplit } from '../operations/split';\n\nexport function validationOsmApiLimits(context) {\n const type = 'osm_api_limits';\n\n const validation = function checkOsmApiLimits(entity) {\n const issues = [];\n const osm = context.connection();\n if (!osm) return issues; // cannot check if there is no connection to the osm api, e.g. during unit tests\n const maxWayNodes = osm.maxWayNodes();\n\n if (entity.type === 'way') {\n if (entity.nodes.length > maxWayNodes) {\n issues.push(new validationIssue({\n type: type,\n subtype: 'exceededMaxWayNodes',\n severity: 'error',\n message: function() {\n return t.append('issues.osm_api_limits.max_way_nodes.message');\n },\n reference: function(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.osm_api_limits.max_way_nodes.reference', { maxWayNodes }));\n },\n entityIds: [entity.id],\n dynamicFixes: splitWayIntoSmallChunks\n }));\n }\n }\n\n return issues;\n };\n\n function splitWayIntoSmallChunks() {\n const fix = new validationIssueFix({\n icon: 'iD-operation-split',\n title: t.append('issues.fix.split_way.title'),\n entityIds: this.entityIds,\n onClick: function(context) {\n const maxWayNodes = context.connection().maxWayNodes();\n const g = context.graph();\n\n const entityId = this.entityIds[0];\n const entity = context.graph().entities[entityId];\n const numberOfParts = Math.ceil(entity.nodes.length / maxWayNodes);\n let splitVertices;\n\n if (numberOfParts === 2) {\n // simple case: try to split at the an intersection vertex\n const splitIntersections = entity.nodes\n .map(nid => g.entity(nid))\n .filter(n => g.parentWays(n).length > 1)\n .map(n => n.id)\n .filter(nid => {\n const splitIndex = entity.nodes.indexOf(nid);\n return splitIndex < maxWayNodes &&\n entity.nodes.length - splitIndex < maxWayNodes;\n });\n if (splitIntersections.length > 0) {\n splitVertices = [\n splitIntersections[Math.floor(splitIntersections.length / 2)]\n ];\n }\n }\n\n if (splitVertices === undefined) {\n // general case: either more than one split is needed or no possible\n // intersection split point was found -> just split at regular intervals\n splitVertices = [...Array(numberOfParts - 1)].map((_, i) =>\n entity.nodes[Math.floor(entity.nodes.length * (i + 1) / numberOfParts)]);\n }\n\n if (entity.isClosed()) {\n // add extra split for closed ways at start of way\n splitVertices.push(entity.nodes[0]);\n }\n\n const operation = operationSplit(context, splitVertices.concat(entityId));\n if (!operation.disabled()) {\n operation();\n }\n }\n });\n\n return [fix];\n }\n\n\n validation.type = type;\n\n return validation;\n}\n", "/** @typedef {{ old: Tags; replace?: Tags }[]} DataDeprecated */\n\n/** @param {Tags} tags @param {DataDeprecated} dataDeprecated */\nexport function getDeprecatedTags(tags, dataDeprecated) {\n // if there are no tags, none can be deprecated\n if (Object.keys(tags).length === 0) return [];\n\n /** @type {DataDeprecated} */\n var deprecated = [];\n dataDeprecated.forEach((d) => {\n const oldKeys = Object.keys(d.old);\n const transferKeys = oldKeys.filter(key => d.old[key] === '*');\n if (d.replace) {\n var hasExistingValues = Object.keys(d.replace).some((replaceKey) => {\n if (!tags[replaceKey] || d.old[replaceKey]) return false;\n var replaceValue = d.replace[replaceKey];\n if (replaceValue === '*') return false;\n if (replaceValue.startsWith('$1') && tags[replaceKey] === tags[transferKeys[+replaceValue.substring(1) - 1]]) return false;\n if (replaceValue === tags[replaceKey]) return false;\n return true;\n });\n // don't flag deprecated tags if the upgrade path would overwrite existing data - #7843\n if (hasExistingValues) return;\n }\n\n var matchesDeprecatedTags = oldKeys.every((oldKey) => {\n if (!tags[oldKey]) return false;\n if (d.old[oldKey] === '*') return true;\n if (d.old[oldKey] === tags[oldKey]) return true;\n\n var vals = tags[oldKey].split(';').filter(Boolean);\n if (vals.length === 0) {\n return false;\n } else if (vals.length > 1) {\n return vals.indexOf(d.old[oldKey]) !== -1;\n } else {\n if (tags[oldKey] === d.old[oldKey]) {\n if (d.replace && d.old[oldKey] === d.replace[oldKey]) {\n var replaceKeys = Object.keys(d.replace);\n return !replaceKeys.every((replaceKey) => {\n return tags[replaceKey] === d.replace[replaceKey];\n });\n } else {\n return true;\n }\n }\n }\n\n return false;\n });\n\n if (matchesDeprecatedTags) {\n deprecated.push(d);\n }\n });\n\n return deprecated;\n}\n\n/** @type {{ [key: string]: string[] }} */\nvar _deprecatedTagValuesByKey;\n\n/** @param {DataDeprecated} dataDeprecated */\nexport function deprecatedTagValuesByKey(dataDeprecated) {\n if (!_deprecatedTagValuesByKey) {\n _deprecatedTagValuesByKey = {};\n dataDeprecated.forEach((d) => {\n var oldKeys = Object.keys(d.old);\n if (oldKeys.length === 1) {\n var oldKey = oldKeys[0];\n var oldValue = d.old[oldKey];\n if (oldValue !== '*') {\n if (!_deprecatedTagValuesByKey[oldKey]) {\n _deprecatedTagValuesByKey[oldKey] = [oldValue];\n } else {\n _deprecatedTagValuesByKey[oldKey].push(oldValue);\n }\n }\n }\n });\n }\n return _deprecatedTagValuesByKey;\n};\n", "import { t } from '../core/localizer';\n\nimport { actionChangePreset } from '../actions/change_preset';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionUpgradeTags } from '../actions/upgrade_tags';\nimport { fileFetcher } from '../core';\nimport { presetManager } from '../presets';\nimport { services } from '../services';\nimport { utilHashcode, utilTagDiff } from '../util';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { getDeprecatedTags } from '../osm/deprecated';\n\n/** @import { TagDiff } from '../util/util'. */\n\n\nexport function validationOutdatedTags() {\n const type = 'outdated_tags';\n let _waitingForDeprecated = true;\n let _dataDeprecated;\n\n // fetch deprecated tags\n fileFetcher.get('deprecated')\n .then(d => _dataDeprecated = d)\n .catch(() => { /* ignore */ })\n .finally(() => _waitingForDeprecated = false);\n\n\n function oldTagIssues(entity, graph) {\n if (!entity.hasInterestingTags()) return [];\n\n let preset = presetManager.match(entity, graph);\n if (!preset) return [];\n\n const oldTags = Object.assign({}, entity.tags); // shallow copy\n\n // Upgrade preset, if a replacement is available..\n if (preset.replacement) {\n const newPreset = presetManager.item(preset.replacement);\n graph = actionChangePreset(entity.id, preset, newPreset, true /* skip field defaults */)(graph);\n entity = graph.entity(entity.id);\n preset = newPreset;\n }\n\n // Attempt to match a canonical record in the name-suggestion-index.\n const nsi = services.nsi;\n let waitingForNsi = false;\n let nsiResult;\n if (nsi) {\n waitingForNsi = (nsi.status() === 'loading');\n if (!waitingForNsi) {\n const loc = entity.extent(graph).center();\n nsiResult = nsi.upgradeTags(oldTags, loc);\n }\n }\n const nsiDiff = nsiResult ? utilTagDiff(oldTags, nsiResult.newTags) : [];\n\n // Upgrade deprecated tags\n let deprecatedTags;\n if (_dataDeprecated) {\n deprecatedTags = getDeprecatedTags(entity.tags, _dataDeprecated);\n if (entity.type === 'way' && entity.isClosed() &&\n entity.tags.traffic_calming === 'island' && !entity.tags.highway) {\n // https://github.com/openstreetmap/id-tagging-schema/issues/1162#issuecomment-2000356902\n deprecatedTags.push({\n old: {traffic_calming: 'island'},\n replace: {'area:highway': 'traffic_island'}\n });\n }\n if (deprecatedTags.length) {\n deprecatedTags.forEach(tag => {\n graph = actionUpgradeTags(entity.id, tag.old, tag.replace)(graph);\n });\n entity = graph.entity(entity.id);\n }\n }\n\n // Add missing addTags from the detected preset\n let newTags = Object.assign({}, entity.tags); // shallow copy\n if (preset.tags !== preset.addTags) {\n Object.keys(preset.addTags).filter(k => {\n // if nsi suggestion already includes this tag: don't repeat it in \"incomplete tags\"\n return !nsiResult?.newTags[k];\n }).forEach(k => {\n if (!newTags[k]) {\n if (preset.addTags[k] === '*') {\n newTags[k] = 'yes';\n } else if (preset.addTags[k]) {\n newTags[k] = preset.addTags[k];\n }\n }\n });\n }\n const deprecationDiff = utilTagDiff(oldTags, newTags);\n const deprecationDiffContext = Object.keys(oldTags)\n .filter(key => deprecatedTags?.some(deprecated => deprecated.replace?.[key] !== undefined))\n .filter(key => newTags[key] === oldTags[key])\n .map(key => ({\n type: '~',\n key,\n oldVal: oldTags[key],\n newVal: newTags[key],\n display: '  ' + key + '=' + oldTags[key]\n }));\n\n let issues = [];\n issues.provisional = (_waitingForDeprecated || waitingForNsi);\n\n if (deprecationDiff.length) {\n const isOnlyAddingTags = !deprecationDiff.some(d => d.type === '-');\n const prefix = isOnlyAddingTags ? 'incomplete.' : '';\n\n issues.push(new validationIssue({\n type: type,\n subtype: isOnlyAddingTags ? 'incomplete_tags' : 'deprecated_tags',\n severity: 'warning',\n message: (context) => {\n const currEntity = context.hasEntity(entity.id);\n if (!currEntity) return '';\n\n const feature = utilDisplayLabel(currEntity, context.graph(), /* verbose */ true);\n\n return t.append(`issues.outdated_tags.${prefix}message`, { feature });\n },\n reference: selection => showReference(\n selection,\n t.append(`issues.outdated_tags.${prefix}reference`),\n [...deprecationDiff, ...deprecationDiffContext]\n ),\n entityIds: [entity.id],\n hash: utilHashcode(JSON.stringify(deprecationDiff)),\n dynamicFixes: () => {\n let fixes = [\n new validationIssueFix({\n title: t.append('issues.fix.upgrade_tags.title'),\n onClick: (context) => {\n context.perform(graph => doUpgrade(graph, deprecationDiff), t('issues.fix.upgrade_tags.annotation'));\n }\n })\n ];\n return fixes;\n }\n }));\n }\n\n if (nsiDiff.length) {\n const isOnlyAddingTags = nsiDiff.every(d => d.type === '+');\n\n issues.push(new validationIssue({\n type: type,\n subtype: 'noncanonical_brand',\n severity: 'warning',\n message: (context) => {\n const currEntity = context.hasEntity(entity.id);\n if (!currEntity) return '';\n\n const feature = utilDisplayLabel(currEntity, context.graph(), /* verbose */ true);\n\n return isOnlyAddingTags\n ? t.append('issues.outdated_tags.noncanonical_brand.message_incomplete', { feature })\n : t.append('issues.outdated_tags.noncanonical_brand.message', { feature });\n },\n reference: selection => showReference(\n selection,\n t.append('issues.outdated_tags.noncanonical_brand.reference'),\n nsiDiff\n ),\n entityIds: [entity.id],\n hash: utilHashcode(JSON.stringify(nsiDiff)),\n dynamicFixes: () => {\n let fixes = [\n new validationIssueFix({\n title: t.append('issues.fix.upgrade_tags.title'),\n onClick: (context) => {\n context.perform(graph => doUpgrade(graph, nsiDiff), t('issues.fix.upgrade_tags.annotation'));\n }\n }),\n new validationIssueFix({\n title: t.append('issues.fix.tag_as_not.title', { name: nsiResult.matched.displayName }),\n onClick: (context) => {\n context.perform(addNotTag, t('issues.fix.tag_as_not.annotation'));\n }\n })\n ];\n return fixes;\n }\n }));\n }\n\n return issues;\n\n\n /** @param {iD.Graph} graph @param {TagDiff[]} diff */\n function doUpgrade(graph, diff) {\n const currEntity = graph.hasEntity(entity.id);\n if (!currEntity) return graph;\n\n let newTags = Object.assign({}, currEntity.tags); // shallow copy\n diff.forEach(diff => {\n if (diff.type === '-') {\n delete newTags[diff.key];\n } else if (diff.type === '+') {\n newTags[diff.key] = diff.newVal;\n }\n });\n\n return actionChangeTags(currEntity.id, newTags)(graph);\n }\n\n\n function addNotTag(graph) {\n const currEntity = graph.hasEntity(entity.id);\n if (!currEntity) return graph;\n\n const item = nsiResult && nsiResult.matched;\n if (!item) return graph;\n\n let newTags = Object.assign({}, currEntity.tags); // shallow copy\n const wd = item.mainTag; // e.g. `brand:wikidata`\n const notwd = `not:${wd}`; // e.g. `not:brand:wikidata`\n const qid = item.tags[wd];\n newTags[notwd] = qid;\n\n if (newTags[wd] === qid) { // if `brand:wikidata` was set to that qid\n const wp = item.mainTag.replace('wikidata', 'wikipedia');\n delete newTags[wd]; // remove `brand:wikidata`\n delete newTags[wp]; // remove `brand:wikipedia`\n }\n\n return actionChangeTags(currEntity.id, newTags)(graph);\n }\n\n\n function showReference(selection, reference, tagDiff) {\n let enter = selection.selectAll('.issue-reference')\n .data([0])\n .enter();\n\n enter\n .append('div')\n .attr('class', 'issue-reference')\n .call(reference);\n\n enter\n .append('strong')\n .call(t.append('issues.suggested'));\n\n enter\n .append('table')\n .attr('class', 'tagDiff-table')\n .selectAll('.tagDiff-row')\n .data(tagDiff)\n .enter()\n .append('tr')\n .attr('class', 'tagDiff-row')\n .append('td')\n .attr('class', d => {\n const klass = 'tagDiff-cell';\n switch (d.type) {\n case '+':\n return `${klass} tagDiff-cell-add`;\n case '-':\n return `${klass} tagDiff-cell-remove`;\n default:\n return `${klass} tagDiff-cell-unchanged`;\n }\n })\n .html(d => d.display);\n }\n }\n\n\n let validation = oldTagIssues;\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { t } from '../core/localizer';\nimport { utilTagDiff } from '../util';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationPrivateData() {\n var type = 'private_data';\n\n // assume that some buildings are private\n var privateBuildingValues = {\n detached: true,\n farm: true,\n house: true,\n houseboat: true,\n residential: true,\n semidetached_house: true,\n static_caravan: true\n };\n\n // but they might be public if they have one of these other tags\n var publicKeys = {\n amenity: true,\n craft: true,\n historic: true,\n leisure: true,\n office: true,\n shop: true,\n tourism: true\n };\n\n // these tags may contain personally identifying info\n var personalTags = {\n 'contact:email': true,\n 'contact:fax': true,\n 'contact:phone': true,\n email: true,\n fax: true,\n phone: true\n };\n\n\n var validation = function checkPrivateData(entity) {\n var tags = entity.tags;\n if (!tags.building || !privateBuildingValues[tags.building]) return [];\n\n var keepTags = {};\n for (var k in tags) {\n if (publicKeys[k]) return []; // probably a public feature\n if (!personalTags[k]) {\n keepTags[k] = tags[k];\n }\n }\n\n var tagDiff = utilTagDiff(tags, keepTags);\n if (!tagDiff.length) return [];\n\n var fixID = tagDiff.length === 1 ? 'remove_tag' : 'remove_tags';\n\n return [new validationIssue({\n type: type,\n severity: 'warning',\n message: showMessage,\n reference: showReference,\n entityIds: [entity.id],\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.' + fixID + '.title'),\n onClick: function(context) {\n context.perform(doUpgrade, t('issues.fix.remove_tag.annotation'));\n }\n })\n ];\n }\n })];\n\n\n function doUpgrade(graph) {\n var currEntity = graph.hasEntity(entity.id);\n if (!currEntity) return graph;\n\n var newTags = Object.assign({}, currEntity.tags); // shallow copy\n tagDiff.forEach(function(diff) {\n if (diff.type === '-') {\n delete newTags[diff.key];\n } else if (diff.type === '+') {\n newTags[diff.key] = diff.newVal;\n }\n });\n\n return actionChangeTags(currEntity.id, newTags)(graph);\n }\n\n\n function showMessage(context) {\n var currEntity = context.hasEntity(this.entityIds[0]);\n if (!currEntity) return '';\n\n return t.append('issues.private_data.contact.message',\n { feature: utilDisplayLabel(currEntity, context.graph()) }\n );\n }\n\n\n function showReference(selection) {\n var enter = selection.selectAll('.issue-reference')\n .data([0])\n .enter();\n\n enter\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.private_data.reference'));\n\n enter\n .append('strong')\n .call(t.append('issues.suggested'));\n\n enter\n .append('table')\n .attr('class', 'tagDiff-table')\n .selectAll('.tagDiff-row')\n .data(tagDiff)\n .enter()\n .append('tr')\n .attr('class', 'tagDiff-row')\n .append('td')\n .attr('class', function(d) {\n var klass = d.type === '+' ? 'add' : 'remove';\n return 'tagDiff-cell tagDiff-cell-' + klass;\n })\n .html(function(d) { return d.display; });\n }\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { presetManager } from '../presets';\nimport { services } from '../services';\nimport { t, localizer } from '../core/localizer';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationSuspiciousName(context) {\n const type = 'suspicious_name';\n const keysToTestForGenericValues = [\n 'aerialway', 'aeroway', 'amenity', 'building', 'craft', 'highway',\n 'leisure', 'railway', 'man_made', 'office', 'shop', 'tourism', 'waterway'\n ];\n const ignoredPresets = new Set([\n 'amenity/place_of_worship/christian/jehovahs_witness',\n '__test__ignored_preset' // for unit tests\n ]);\n let _waitingForNsi = false;\n\n\n // Attempt to match a generic record in the name-suggestion-index.\n function isGenericMatchInNsi(tags) {\n const nsi = services.nsi;\n if (nsi) {\n _waitingForNsi = (nsi.status() === 'loading');\n if (!_waitingForNsi) {\n return nsi.isGenericName(tags);\n }\n }\n return false;\n }\n\n\n // Test if the name is just the key or tag value (e.g. \"park\")\n function nameMatchesRawTag(lowercaseName, tags) {\n for (let i = 0; i < keysToTestForGenericValues.length; i++) {\n let key = keysToTestForGenericValues[i];\n let val = tags[key];\n if (val) {\n val = val.toLowerCase();\n if (key === lowercaseName ||\n val === lowercaseName ||\n key.replace(/\\_/g, ' ') === lowercaseName ||\n val.replace(/\\_/g, ' ') === lowercaseName) {\n return true;\n }\n }\n }\n return false;\n }\n\n /** @param {string} name */\n function nameMatchesPresetName(name, preset) {\n if (!preset) return false;\n if (ignoredPresets.has(preset.id)) return false;\n\n name = name.toLowerCase();\n return name === preset.name().toLowerCase() || preset.aliases().some(alias => name === alias.toLowerCase());\n }\n\n /** @param {string} name */\n function isGenericName(name, tags, preset) {\n name = name.toLowerCase();\n return nameMatchesRawTag(name, tags) || nameMatchesPresetName(name, preset) || isGenericMatchInNsi(tags);\n }\n\n function makeGenericNameIssue(entityId, nameKey, genericName, langCode) {\n return new validationIssue({\n type: type,\n subtype: 'generic_name',\n severity: 'warning',\n message: function(context) {\n let entity = context.hasEntity(this.entityIds[0]);\n if (!entity) return '';\n let preset = presetManager.match(entity, context.graph());\n let langName = langCode && localizer.languageName(langCode);\n return t.append('issues.generic_name.message' + (langName ? '_language' : ''),\n { feature: preset.name(), name: genericName, language: langName }\n );\n },\n reference: showReference,\n entityIds: [entityId],\n hash: `${nameKey}=${genericName}`,\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_the_name.title'),\n onClick: function(context) {\n let entityId = this.issue.entityIds[0];\n let entity = context.entity(entityId);\n let tags = Object.assign({}, entity.tags); // shallow copy\n delete tags[nameKey];\n context.perform(\n actionChangeTags(entityId, tags), t('issues.fix.remove_generic_name.annotation')\n );\n }\n })\n ];\n }\n });\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.generic_name.reference'));\n }\n }\n\n let validation = function checkGenericName(entity) {\n const tags = entity.tags;\n\n // a generic name is allowed if it's a known brand or entity\n const hasWikidata = (!!tags.wikidata || !!tags['brand:wikidata'] || !!tags['operator:wikidata']);\n if (hasWikidata) return [];\n\n let issues = [];\n\n const preset = presetManager.match(entity, context.graph());\n\n for (let key in tags) {\n const m = key.match(/^name(?:(?::)([a-zA-Z_-]+))?$/);\n if (!m) continue;\n\n const langCode = m.length >= 2 ? m[1] : null;\n const value = tags[key];\n\n if (isGenericName(value, tags, preset)) {\n issues.provisional = _waitingForNsi; // retry later if we are waiting on NSI to finish loading\n issues.push(makeGenericNameIssue(entity.id, key, value, langCode));\n }\n }\n\n return issues;\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { prefs } from '../core/preferences';\nimport { t } from '../core/localizer';\n//import { actionChangeTags } from '../actions/change_tags';\nimport { actionOrthogonalize } from '../actions/orthogonalize';\nimport { geoOrthoCanOrthogonalize } from '../geo/ortho';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\nexport function validationUnsquareWay(context) {\n var type = 'unsquare_way';\n var DEFAULT_DEG_THRESHOLD = 5; // see also issues.js\n\n // use looser epsilon for detection to reduce warnings of buildings that are essentially square already\n var epsilon = 0.05;\n var nodeThreshold = 10;\n\n function isBuilding(entity, graph) {\n if (entity.type !== 'way' || entity.geometry(graph) !== 'area') return false;\n return entity.tags.building && entity.tags.building !== 'no';\n }\n\n\n var validation = function checkUnsquareWay(entity, graph) {\n\n if (!isBuilding(entity, graph)) return [];\n\n // don't flag ways marked as physically unsquare\n if (entity.tags.nonsquare === 'yes') return [];\n\n var isClosed = entity.isClosed();\n if (!isClosed) return []; // this building has bigger problems\n\n // don't flag ways with lots of nodes since they are likely detail-mapped\n var nodes = graph.childNodes(entity).slice(); // shallow copy\n if (nodes.length > nodeThreshold + 1) return []; // +1 because closing node appears twice\n\n // ignore if not all nodes are fully downloaded\n var osm = services.osm;\n if (!osm || nodes.some(function(node) { return !osm.isDataLoaded(node.loc); })) return [];\n\n // don't flag connected ways to avoid unresolvable unsquare loops\n var hasConnectedSquarableWays = nodes.some(function(node) {\n return graph.parentWays(node).some(function(way) {\n if (way.id === entity.id) return false;\n if (isBuilding(way, graph)) return true;\n return graph.parentRelations(way).some(function(parentRelation) {\n return parentRelation.isMultipolygon() &&\n parentRelation.tags.building &&\n parentRelation.tags.building !== 'no';\n });\n });\n });\n if (hasConnectedSquarableWays) return [];\n\n\n // user-configurable square threshold\n var storedDegreeThreshold = prefs('validate-square-degrees');\n var degreeThreshold = isFinite(storedDegreeThreshold) ? Number(storedDegreeThreshold) : DEFAULT_DEG_THRESHOLD;\n\n var points = nodes.map(function(node) { return context.projection(node.loc); });\n if (!geoOrthoCanOrthogonalize(points, isClosed, epsilon, degreeThreshold, true)) return [];\n\n return [new validationIssue({\n type: type,\n subtype: 'building',\n severity: 'suggestion',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.unsquare_way.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [entity.id],\n hash: degreeThreshold,\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-orthogonalize',\n title: t.append('issues.fix.square_feature.title'),\n onClick: function(context, completionHandler) {\n var entityId = this.issue.entityIds[0];\n // use same degree threshold as for detection\n context.perform(\n actionOrthogonalize(entityId, context.projection, undefined, degreeThreshold),\n t('operations.orthogonalize.annotation.feature', { n: 1 })\n );\n // run after the squaring transition (currently 150ms)\n window.setTimeout(function() { completionHandler(); }, 175);\n }\n }),\n /*\n new validationIssueFix({\n title: t.append('issues.fix.tag_as_unsquare.title'),\n onClick: function(context) {\n var entityId = this.issue.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n tags.nonsquare = 'yes';\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.tag_as_unsquare.annotation')\n );\n }\n })\n */\n ];\n }\n })];\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.unsquare_way.buildings.reference'));\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "export { validationAlmostJunction } from './almost_junction';\nexport { validationCloseNodes } from './close_nodes';\nexport { validationCrossingWays } from './crossing_ways';\nexport { validationDisconnectedWay } from './disconnected_way';\nexport { validationMissingStartDate } from './missing_start_date';\nexport { validationFormatting } from './invalid_format';\nexport { validationHelpRequest } from './help_request';\nexport { validationImpossibleOneway } from './impossible_oneway';\nexport { validationIncompatibleSource } from './incompatible_source';\nexport { validationMaprules } from './maprules';\nexport { validationMismatchedDates } from './mismatched_dates';\nexport { validationMismatchedGeometry } from './mismatched_geometry';\nexport { validationMissingRole } from './missing_role';\nexport { validationMissingTag } from './missing_tag';\nexport { validationMutuallyExclusiveTags } from './mutually_exclusive_tags';\nexport { validationOsmApiLimits } from './osm_api_limits';\nexport { validationOutdatedTags } from './outdated_tags';\nexport { validationPrivateData } from './private_data';\nexport { validationSuspiciousName } from './suspicious_name';\nexport { validationUnsquareWay } from './unsquare_way';\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from './preferences';\nimport { coreDifference } from './difference';\nimport { geoExtent } from '../geo/extent';\nimport { modeSelect } from '../modes/select';\nimport { utilArrayChunk, utilArrayDifference, utilArrayGroupBy, utilArrayIntersection, utilArrayUnion, utilEntityAndDeepMemberIDs, utilRebind } from '../util';\nimport * as Validations from '../validations/index';\n\n\nexport function coreValidator(context) {\n let dispatch = d3_dispatch('validated', 'focusedIssue');\n const validator = {};\n\n let _rules = {};\n let _disabledRules = {};\n\n let _ignoredIssueIDs = new Set();\n let _resolvedIssueIDs = new Set();\n let _baseCache = validationCache('base'); // issues before any user edits\n let _headCache = validationCache('head'); // issues after all user edits\n let _completeDiff = {}; // complete diff base -> head of what the user changed\n let _headIsCurrent = false;\n\n let _deferredRIC = {}; // Object( RequestIdleCallback handle : rejectPromise method )\n let _deferredST = new Set(); // Set( SetTimeout handles )\n let _headPromise; // Promise fulfilled when validation is performed up to headGraph snapshot\n\n const RETRY = 5000; // wait 5sec before revalidating provisional entities\n\n\n // Allow validation severity to be overridden by url queryparams...\n // See: https://github.com/openstreetmap/iD/pull/8243\n //\n // Each param should contain a urlencoded comma separated list of\n // `type/subtype` rules. `*` may be used as a wildcard..\n // Examples:\n // `validationError=disconnected_way/*`\n // `validationError=disconnected_way/highway`\n // `validationError=crossing_ways/bridge*`\n // `validationError=crossing_ways/bridge*,crossing_ways/tunnel*`\n\n const _errorOverrides = parseHashParam(context.initialHashParams.validationError);\n const _warningOverrides = parseHashParam(context.initialHashParams.validationWarning);\n const _suggestionOverrides = parseHashParam(context.initialHashParams.validationSuggestion);\n const _disableOverrides = parseHashParam(context.initialHashParams.validationDisable);\n\n // `parseHashParam()` (private)\n // Checks hash parameters for severity overrides\n // Arguments\n // `param` - a url hash parameter (`validationError`, `validationWarning`, `validationSuggestion`, or `validationDisable`)\n // Returns\n // Array of Objects like { type: RegExp, subtype: RegExp }\n //\n function parseHashParam(param) {\n let result = [];\n let rules = (param || '').split(',');\n rules.forEach(rule => {\n rule = rule.trim();\n const parts = rule.split('/', 2); // \"type/subtype\"\n const type = parts[0];\n const subtype = parts[1] || '*';\n if (!type || !subtype) return;\n result.push({ type: makeRegExp(type), subtype: makeRegExp(subtype) });\n });\n return result;\n\n function makeRegExp(str) {\n const escaped = str\n .replace(/[-\\/\\\\^$+?.()|[\\]{}]/g, '\\\\$&') // escape all reserved chars except for the '*'\n .replace(/\\*/g, '.*'); // treat a '*' like '.*'\n return new RegExp('^' + escaped + '$');\n }\n }\n\n\n // `init()`\n // Initialize the validator, called once on iD startup\n //\n validator.init = () => {\n Object.values(Validations).forEach(validation => {\n if (typeof validation !== 'function') return;\n const fn = validation(context);\n const key = fn.type;\n _rules[key] = fn;\n });\n\n let disabledRules = prefs('validate-disabledRules');\n if (disabledRules) {\n disabledRules.split(',').forEach(k => _disabledRules[k] = true);\n }\n };\n\n\n // `reset()` (private)\n // Cancels deferred work and resets all caches\n //\n // Arguments\n // `resetIgnored` - `true` to clear the list of user-ignored issues\n //\n function reset(resetIgnored) {\n // empty queues\n _baseCache.queue = [];\n _headCache.queue = [];\n\n // cancel deferred work and reject any pending promise\n Object.keys(_deferredRIC).forEach(key => {\n window.cancelIdleCallback(key);\n _deferredRIC[key]();\n });\n _deferredRIC = {};\n _deferredST.forEach(window.clearTimeout);\n _deferredST.clear();\n\n // clear caches\n if (resetIgnored) _ignoredIssueIDs.clear();\n _resolvedIssueIDs.clear();\n _baseCache = validationCache('base');\n _headCache = validationCache('head');\n _completeDiff = {};\n _headIsCurrent = false;\n }\n\n\n // `reset()`\n // clear caches, called whenever iD resets after a save or switches sources\n // (clears out the _ignoredIssueIDs set also)\n //\n validator.reset = () => {\n reset(true);\n };\n\n\n // `resetIgnoredIssues()`\n // clears out the _ignoredIssueIDs Set\n //\n validator.resetIgnoredIssues = () => {\n _ignoredIssueIDs.clear();\n dispatch.call('validated'); // redraw UI\n };\n\n\n // `revalidateUnsquare()`\n // Called whenever the user changes the unsquare threshold\n // It reruns just the \"unsquare_way\" validation on all buildings.\n //\n validator.revalidateUnsquare = () => {\n revalidateUnsquare(_headCache);\n revalidateUnsquare(_baseCache);\n dispatch.call('validated');\n };\n\n function revalidateUnsquare(cache) {\n const checkUnsquareWay = _rules.unsquare_way;\n if (!cache.graph || typeof checkUnsquareWay !== 'function') return;\n\n // uncache existing\n cache.uncacheIssuesOfType('unsquare_way');\n\n const buildings = context.history().tree().intersects(geoExtent([-180,-90],[180, 90]), cache.graph) // everywhere\n .filter(entity => (entity.type === 'way' && entity.tags.building && entity.tags.building !== 'no'));\n\n // rerun for all buildings\n buildings.forEach(entity => {\n const detected = checkUnsquareWay(entity, cache.graph);\n if (!detected.length) return;\n cache.cacheIssues(detected);\n });\n }\n\n\n // `getIssues()`\n // Gets all issues that match the given options\n // This is called by many other places\n //\n // Arguments\n // `options` Object like:\n // {\n // what: 'all', // 'all' or 'edited'\n // where: 'all', // 'all' or 'visible'\n // includeIgnored: false, // true, false, or 'only'\n // includeDisabledRules: false // true, false, or 'only'\n // }\n //\n // Returns\n // An Array containing the issues\n //\n validator.getIssues = (options) => {\n const opts = Object.assign({ what: 'all', where: 'all', includeIgnored: false, includeDisabledRules: false }, options);\n const view = context.map().extent();\n let seen = new Set();\n let results = [];\n\n // collect head issues - present in the user edits\n if (_headCache.graph && _headCache.graph !== _baseCache.graph) {\n Object.values(_headCache.issuesByIssueID).forEach(issue => {\n // In the head cache, only count features that the user is responsible for - #8632\n // For example, a user can undo some work and an issue will still present in the\n // head graph, but we don't want to credit the user for causing that issue.\n const userModified = (issue.entityIds || []).some(id => _completeDiff.hasOwnProperty(id));\n if (opts.what === 'edited' && !userModified) return; // present in head but user didn't touch it\n\n if (!filter(issue)) return;\n seen.add(issue.id);\n results.push(issue);\n });\n }\n\n // collect base issues - present before user edits\n if (opts.what === 'all') {\n Object.values(_baseCache.issuesByIssueID).forEach(issue => {\n if (!filter(issue)) return;\n seen.add(issue.id);\n results.push(issue);\n });\n }\n\n return results;\n\n\n // Filter the issue set to include only what the calling code wants to see.\n // Note that we use `context.graph()`/`context.hasEntity()` here, not `cache.graph`,\n // because that is the graph that the calling code will be using.\n function filter(issue) {\n if (!issue) return false;\n if (seen.has(issue.id)) return false;\n if (_resolvedIssueIDs.has(issue.id)) return false;\n if (opts.includeDisabledRules === 'only' && !_disabledRules[issue.type]) return false;\n if (!opts.includeDisabledRules && _disabledRules[issue.type]) return false;\n\n if (opts.includeIgnored === 'only' && !_ignoredIssueIDs.has(issue.id)) return false;\n if (!opts.includeIgnored && _ignoredIssueIDs.has(issue.id)) return false;\n\n // This issue may involve an entity that doesn't exist in context.graph()\n // This can happen because validation is async and rendering the issue lists is async.\n if ((issue.entityIds || []).some(id => !context.hasEntity(id))) return false;\n\n if (opts.where === 'visible') {\n const extent = issue.extent(context.graph());\n if (!view.intersects(extent)) return false;\n }\n\n return true;\n }\n };\n\n\n // `getResolvedIssues()`\n // Gets the issues that have been fixed by the user.\n //\n // Resolved issues are tracked in the `_resolvedIssueIDs` Set,\n // and they should all be issues that exist in the _baseCache.\n //\n // Returns\n // An Array containing the issues\n //\n validator.getResolvedIssues = () => {\n return Array.from(_resolvedIssueIDs)\n .map(issueID => _baseCache.issuesByIssueID[issueID])\n .filter(Boolean);\n };\n\n\n // `focusIssue()`\n // Adjusts the map to focus on the given issue.\n // (requires the issue to have a reasonable extent defined)\n //\n // Arguments\n // `issue` - the issue to focus on\n //\n validator.focusIssue = (issue) => {\n // Note that we use `context.graph()`/`context.hasEntity()` here, not `cache.graph`,\n // because that is the graph that the calling code will be using.\n const graph = context.graph();\n let selectID;\n\n // Try to focus the map at the center of the issue..\n let issueExtent = issue.extent(graph);\n\n // Try to select the first entity in the issue..\n if (issue.entityIds && issue.entityIds.length) {\n selectID = issue.entityIds[0];\n\n // If a relation, focus on one of its members instead.\n // Otherwise we might be focusing on a part of map where the relation is not visible.\n if (selectID && selectID.charAt(0) === 'r') { // relation\n const ids = utilEntityAndDeepMemberIDs([selectID], graph);\n let nodeID = ids.find(id => id.charAt(0) === 'n' && graph.hasEntity(id));\n\n if (!nodeID) { // relation has no downloaded nodes to focus on\n const wayID = ids.find(id => id.charAt(0) === 'w' && graph.hasEntity(id));\n if (wayID) {\n nodeID = graph.entity(wayID).first(); // focus on the first node of this way\n }\n }\n\n if (nodeID) {\n issueExtent = graph.entity(nodeID).extent(graph);\n }\n }\n }\n\n // Adjust the view\n context.map().zoomToEase(issueExtent);\n\n if (selectID) { // Enter select mode\n window.setTimeout(() => {\n context.enter(modeSelect(context, [selectID]));\n dispatch.call('focusedIssue', this, issue);\n }, 250); // after ease\n }\n };\n\n\n // `getIssuesBySeverity()`\n // Gets the issues then groups them by error/warning/suggestion\n // (This just calls getIssues, then puts issues in groups)\n //\n // Arguments\n // `options` - (see `getIssues`)\n // Returns\n // Object result like:\n // {\n // error: Array of errors,\n // warning: Array of warnings,\n // suggestion: Array of suggestions,\n // }\n //\n validator.getIssuesBySeverity = (options) => {\n let groups = utilArrayGroupBy(validator.getIssues(options), 'severity');\n groups.error = groups.error || [];\n groups.warning = groups.warning || [];\n groups.suggestion = groups.suggestion || [];\n return groups;\n };\n\n\n // `getEntityIssues()`\n // Gets the issues that the given entity IDs have in common, matching the given options\n // (This just calls getIssues, then filters for the given entity IDs)\n // The issues are sorted for relevance\n //\n // Arguments\n // `entityIDs` - Array or Set of entityIDs to get issues for\n // `options` - (see `getIssues`)\n // Returns\n // An Array containing the issues\n //\n validator.getSharedEntityIssues = (entityIDs, options) => {\n const orderedIssueTypes = [ // Show some issue types in a particular order:\n 'missing_tag', 'missing_role', // - missing data first\n 'outdated_tags', 'mismatched_geometry', // - identity issues\n 'crossing_ways', 'almost_junction', // - geometry issues where fixing them might solve connectivity issues\n 'disconnected_way', 'impossible_oneway' // - finally connectivity issues\n ];\n\n const allIssues = validator.getIssues(options);\n const forEntityIDs = new Set(entityIDs);\n\n return allIssues\n .filter(issue => (issue.entityIds || []).some(entityID => forEntityIDs.has(entityID)))\n .sort((issue1, issue2) => {\n if (issue1.type === issue2.type) { // issues of the same type, sort deterministically\n return issue1.id < issue2.id ? -1 : 1;\n }\n const index1 = orderedIssueTypes.indexOf(issue1.type);\n const index2 = orderedIssueTypes.indexOf(issue2.type);\n if (index1 !== -1 && index2 !== -1) { // both issue types have explicit sort orders\n return index1 - index2;\n } else if (index1 === -1 && index2 === -1) { // neither issue type has an explicit sort order, sort by type\n return issue1.type < issue2.type ? -1 : 1;\n } else { // order explicit types before everything else\n return index1 !== -1 ? -1 : 1;\n }\n });\n };\n\n\n // `getEntityIssues()`\n // Get an array of detected issues for the given entityID.\n // (This just calls getSharedEntityIssues for a single entity)\n //\n // Arguments\n // `entityID` - the entity ID to get the issues for\n // `options` - (see `getIssues`)\n // Returns\n // An Array containing the issues\n //\n validator.getEntityIssues = (entityID, options) => {\n return validator.getSharedEntityIssues([entityID], options);\n };\n\n\n // `getRuleKeys()`\n //\n // Returns\n // An Array containing the rule keys\n //\n validator.getRuleKeys = () => {\n return Object.keys(_rules);\n };\n\n\n // `isRuleEnabled()`\n //\n // Arguments\n // `key` - the rule to check (e.g. 'crossing_ways')\n // Returns\n // `true`/`false`\n //\n validator.isRuleEnabled = (key) => {\n return !_disabledRules[key];\n };\n\n\n // `toggleRule()`\n // Toggles a single validation rule,\n // then reruns the validation so that the user sees something happen in the UI\n //\n // Arguments\n // `key` - the rule to toggle (e.g. 'crossing_ways')\n //\n validator.toggleRule = (key) => {\n if (_disabledRules[key]) {\n delete _disabledRules[key];\n } else {\n _disabledRules[key] = true;\n }\n\n prefs('validate-disabledRules', Object.keys(_disabledRules).join(','));\n validator.validate();\n };\n\n\n // `disableRules()`\n // Disables given validation rules,\n // then reruns the validation so that the user sees something happen in the UI\n //\n // Arguments\n // `keys` - Array or Set containing rule keys to disable\n //\n validator.disableRules = (keys) => {\n _disabledRules = {};\n keys.forEach(k => _disabledRules[k] = true);\n\n prefs('validate-disabledRules', Object.keys(_disabledRules).join(','));\n validator.validate();\n };\n\n\n // `ignoreIssue()`\n // Don't show the given issue in lists\n //\n // Arguments\n // `issueID` - the issueID\n //\n validator.ignoreIssue = (issueID) => {\n _ignoredIssueIDs.add(issueID);\n };\n\n\n // `validate()`\n // Validates anything that has changed in the head graph since the last time it was run.\n // (head graph contains user's edits)\n //\n // Returns\n // A Promise fulfilled when the validation has completed and then dispatches a `validated` event.\n // This may take time but happen in the background during browser idle time.\n //\n validator.validate = () => {\n // Make sure the caches have graphs assigned to them.\n // (we don't do this in `reset` because context is still resetting things and `history.base()` is unstable then)\n const baseGraph = context.history().base();\n if (!_headCache.graph) _headCache.graph = baseGraph;\n if (!_baseCache.graph) _baseCache.graph = baseGraph;\n\n const prevGraph = _headCache.graph;\n const currGraph = context.graph();\n\n if (currGraph === prevGraph) { // _headCache.graph is current - we are caught up\n _headIsCurrent = true;\n dispatch.call('validated');\n return Promise.resolve();\n }\n\n if (_headPromise) { // Validation already in process, but we aren't caught up to current\n _headIsCurrent = false; // We will need to catch up after the validation promise fulfills\n return _headPromise;\n }\n\n // If we get here, its time to start validating stuff.\n _headCache.graph = currGraph; // take snapshot\n _completeDiff = context.history().difference().complete();\n const incrementalDiff = coreDifference(prevGraph, currGraph);\n const diff = Object.keys(incrementalDiff.complete());\n const entityIDs = _headCache.withAllRelatedEntities(diff); // expand set\n\n if (!entityIDs.size) {\n dispatch.call('validated');\n return Promise.resolve();\n }\n\n // revalidate also connected (or previously connected) entities to the current way\n // https://github.com/openstreetmap/iD/issues/8758\n const addConnectedWays = graph => diff\n .filter(entityID => graph.hasEntity(entityID))\n .map(entityID => graph.entity(entityID))\n .flatMap(entity => graph.childNodes(entity))\n .flatMap(vertex => graph.parentWays(vertex))\n .forEach(way => entityIDs.add(way.id));\n addConnectedWays(currGraph);\n addConnectedWays(prevGraph);\n\n // revalidate entities with changed relation memberships\n // https://github.com/openstreetmap/iD/issues/10786\n Object.values({...incrementalDiff.created(), ...incrementalDiff.deleted()})\n .filter(e => e.type === 'relation')\n .flatMap(r => r.members)\n .forEach(m => entityIDs.add(m.id));\n Object.values(incrementalDiff.modified())\n .filter(e => e.type === 'relation')\n .map(r => ({ baseEntity: prevGraph.entity(r.id), headEntity: r }))\n .forEach(({ baseEntity, headEntity }) => {\n const bm = baseEntity.members.map(m => m.id);\n const hm = headEntity.members.map(m => m.id);\n const symDiff = utilArrayDifference(utilArrayUnion(bm, hm), utilArrayIntersection(bm, hm));\n symDiff.forEach(id => entityIDs.add(id));\n });\n\n _headPromise = validateEntitiesAsync(entityIDs, _headCache)\n .then(() => updateResolvedIssues(entityIDs))\n .then(() => dispatch.call('validated'))\n .catch(() => { /* ignore */ })\n .then(() => {\n _headPromise = null;\n if (!_headIsCurrent) {\n validator.validate(); // run it again to catch up to current graph\n }\n });\n\n return _headPromise;\n };\n\n\n // register event handlers:\n\n // WHEN TO RUN VALIDATION:\n // When history changes:\n context.history()\n .on('restore.validator', validator.validate) // on restore saved history\n .on('undone.validator', validator.validate) // on undo\n .on('redone.validator', validator.validate) // on redo\n .on('reset.validator', () => { // on history reset - happens after save, or enter/exit walkthrough\n reset(false); // cached issues aren't valid any longer if the history has been reset\n validator.validate();\n });\n // but not on 'change' (e.g. while drawing)\n\n // When user changes editing modes (to catch recent changes e.g. drawing)\n context\n .on('exit.validator', validator.validate);\n\n // When merging fetched data, validate base graph:\n context.history()\n .on('merge.validator', entities => {\n if (!entities) return;\n\n // Make sure the caches have graphs assigned to them.\n // (we don't do this in `reset` because context is still resetting things and `history.base()` is unstable then)\n const baseGraph = context.history().base();\n if (!_headCache.graph) _headCache.graph = baseGraph;\n if (!_baseCache.graph) _baseCache.graph = baseGraph;\n\n let entityIDs = entities.map(entity => entity.id);\n entityIDs = _baseCache.withAllRelatedEntities(entityIDs); // expand set\n validateEntitiesAsync(entityIDs, _baseCache);\n });\n\n\n\n // `validateEntity()` (private)\n // Runs all validation rules on a single entity.\n // Some things to note:\n // - Graph is passed in from whenever the validation was started. Validators shouldn't use\n // `context.graph()` because this all happens async, and the graph might have changed\n // (for example, nodes getting deleted before the validation can run)\n // - Validator functions may still be waiting on something and return a \"provisional\" result.\n // In this situation, we will schedule to revalidate the entity sometime later.\n //\n // Arguments\n // `entity` - The entity\n // `graph` - graph containing the entity\n //\n // Returns\n // Object result like:\n // {\n // issues: Array of detected issues\n // provisional: `true` if provisional result, `false` if final result\n // }\n //\n function validateEntity(entity, graph) {\n let result = { issues: [], provisional: false };\n Object.keys(_rules).forEach(runValidation); // run all rules\n return result;\n\n\n // runs validation and appends resulting issues\n function runValidation(key) {\n const fn = _rules[key];\n if (typeof fn !== 'function') {\n console.error('no such validation rule = ' + key); // eslint-disable-line no-console\n return;\n }\n\n let detected = fn(entity, graph);\n if (detected.provisional) { // this validation should be run again later\n result.provisional = true;\n }\n detected = detected.filter(applySeverityOverrides);\n result.issues = result.issues.concat(detected);\n\n\n // If there are any override rules that match the issue type/subtype,\n // adjust severity (or disable it) and keep/discard as quickly as possible.\n function applySeverityOverrides(issue) {\n const type = issue.type;\n const subtype = issue.subtype || '';\n let i;\n\n for (i = 0; i < _errorOverrides.length; i++) {\n if (_errorOverrides[i].type.test(type) && _errorOverrides[i].subtype.test(subtype)) {\n issue.severity = 'error';\n return true;\n }\n }\n for (i = 0; i < _warningOverrides.length; i++) {\n if (_warningOverrides[i].type.test(type) && _warningOverrides[i].subtype.test(subtype)) {\n issue.severity = 'warning';\n return true;\n }\n }\n for (i = 0; i < _suggestionOverrides.length; i++) {\n if (_suggestionOverrides[i].type.test(type) && _suggestionOverrides[i].subtype.test(subtype)) {\n issue.severity = 'suggestion';\n return true;\n }\n }\n for (i = 0; i < _disableOverrides.length; i++) {\n if (_disableOverrides[i].type.test(type) && _disableOverrides[i].subtype.test(subtype)) {\n return false;\n }\n }\n return true;\n }\n }\n }\n\n\n // `updateResolvedIssues()` (private)\n // Determine if any issues were resolved for the given entities.\n // This is called by `validate()` after validation of the head graph\n //\n // Give the user credit for fixing an issue if:\n // - the issue is in the base cache\n // - the issue is not in the head cache\n // - the user did something to one of the entities involved in the issue\n //\n // Arguments\n // `entityIDs` - Array or Set containing entity IDs.\n //\n function updateResolvedIssues(entityIDs) {\n entityIDs.forEach(entityID => {\n const baseIssues = _baseCache.issuesByEntityID[entityID];\n if (!baseIssues) return;\n\n baseIssues.forEach(issueID => {\n // Check if the user did something to one of the entities involved in this issue.\n // (This issue could involve multiple entities, e.g. disconnected routable features)\n const issue = _baseCache.issuesByIssueID[issueID];\n const userModified = (issue.entityIds || []).some(id => _completeDiff.hasOwnProperty(id));\n\n if (userModified && !_headCache.issuesByIssueID[issueID]) { // issue seems fixed\n _resolvedIssueIDs.add(issueID);\n } else { // issue still not resolved\n _resolvedIssueIDs.delete(issueID); // (did undo, or possibly fixed and then re-caused the issue)\n }\n });\n });\n }\n\n\n // `validateEntitiesAsync()` (private)\n // Schedule validation for many entities.\n //\n // Arguments\n // `entityIDs` - Array or Set containing entityIDs.\n // `graph` - the graph to validate that contains those entities\n // `cache` - the cache to store results in (_headCache or _baseCache)\n //\n // Returns\n // A Promise fulfilled when the validation has completed.\n // This may take time but happen in the background during browser idle time.\n //\n function validateEntitiesAsync(entityIDs, cache) {\n // Enqueue the work\n const jobs = Array.from(entityIDs).map(entityID => {\n if (cache.queuedEntityIDs.has(entityID)) return null; // queued already\n cache.queuedEntityIDs.add(entityID);\n\n // Clear caches for existing issues related to this entity\n cache.uncacheEntityID(entityID);\n\n return () => {\n cache.queuedEntityIDs.delete(entityID);\n\n const graph = cache.graph;\n if (!graph) return; // was reset?\n\n const entity = graph.hasEntity(entityID); // Sanity check: don't validate deleted entities\n if (!entity) return;\n\n // detect new issues and update caches\n const result = validateEntity(entity, graph);\n if (result.provisional) { // provisional result\n cache.provisionalEntityIDs.add(entityID); // we'll need to revalidate this entity again later\n }\n\n cache.cacheIssues(result.issues); // update cache\n };\n\n }).filter(Boolean);\n\n\n // Perform the work in chunks.\n // Because this will happen during idle callbacks, we want to choose a chunk size\n // that won't make the browser stutter too badly.\n cache.queue = cache.queue.concat(utilArrayChunk(jobs, 100));\n\n // Perform the work\n if (cache.queuePromise) return cache.queuePromise;\n\n cache.queuePromise = processQueue(cache)\n .then(() => revalidateProvisionalEntities(cache))\n .catch(() => { /* ignore */ })\n .finally(() => cache.queuePromise = null);\n\n return cache.queuePromise;\n }\n\n\n // `revalidateProvisionalEntities()` (private)\n // Sometimes a validator will return a \"provisional\" result.\n // In this situation, we'll need to revalidate the entity later.\n // This function waits a delay, then places them back into the validation queue.\n //\n // Arguments\n // `cache` - The cache (_headCache or _baseCache)\n //\n function revalidateProvisionalEntities(cache) {\n if (!cache.provisionalEntityIDs.size) return; // nothing to do\n\n const handle = window.setTimeout(() => {\n _deferredST.delete(handle);\n if (!cache.provisionalEntityIDs.size) return; // nothing to do\n validateEntitiesAsync(Array.from(cache.provisionalEntityIDs), cache);\n }, RETRY);\n\n _deferredST.add(handle);\n }\n\n\n // `processQueue(queue)` (private)\n // Process the next chunk of deferred validation work\n //\n // Arguments\n // `cache` - The cache (_headCache or _baseCache)\n //\n // Returns\n // A Promise fulfilled when the validation has completed.\n // This may take time but happen in the background during browser idle time.\n //\n function processQueue(cache) {\n // console.log(`${cache.which} queue length ${cache.queue.length}`);\n\n if (!cache.queue.length) return Promise.resolve(); // we're done\n const chunk = cache.queue.pop();\n\n return new Promise((resolvePromise, rejectPromise) => {\n const handle = window.requestIdleCallback(() => {\n delete (_deferredRIC[handle]);\n // const t0 = performance.now();\n chunk.forEach(job => job());\n // const t1 = performance.now();\n // console.log('chunk processed in ' + (t1 - t0) + ' ms');\n resolvePromise();\n });\n _deferredRIC[handle] = rejectPromise;\n })\n .then(() => { // dispatch an event sometimes to redraw various UI things\n if (cache.queue.length % 25 === 0) dispatch.call('validated');\n })\n .then(() => processQueue(cache));\n }\n\n\n return utilRebind(validator, dispatch, 'on');\n}\n\n\n// `validationCache()` (private)\n// Creates a cache to store validation state\n// We create 2 of these:\n// `_baseCache` for validation on the base graph (unedited)\n// `_headCache` for validation on the head graph (user edits applied)\n//\n// Arguments\n// `which` - just a String 'base' or 'head' to keep track of it\n//\nfunction validationCache(which) {\n let cache = {\n which: which,\n graph: null,\n queue: [],\n queuePromise: null,\n queuedEntityIDs: new Set(),\n provisionalEntityIDs: new Set(),\n issuesByIssueID: {}, // issue.id -> issue\n issuesByEntityID: {} // entity.id -> Set(issue.id)\n };\n\n\n cache.cacheIssue = (issue) => {\n (issue.entityIds || []).forEach(entityID => {\n if (!cache.issuesByEntityID[entityID]) {\n cache.issuesByEntityID[entityID] = new Set();\n }\n cache.issuesByEntityID[entityID].add(issue.id);\n });\n cache.issuesByIssueID[issue.id] = issue;\n };\n\n\n cache.uncacheIssue = (issue) => {\n (issue.entityIds || []).forEach(entityID => {\n if (cache.issuesByEntityID[entityID]) {\n cache.issuesByEntityID[entityID].delete(issue.id);\n }\n });\n delete cache.issuesByIssueID[issue.id];\n };\n\n\n cache.cacheIssues = (issues) => {\n issues.forEach(cache.cacheIssue);\n };\n\n\n cache.uncacheIssues = (issues) => {\n issues.forEach(cache.uncacheIssue);\n };\n\n\n cache.uncacheIssuesOfType = (type) => {\n const issuesOfType = Object.values(cache.issuesByIssueID)\n .filter(issue => issue.type === type);\n cache.uncacheIssues(issuesOfType);\n };\n\n\n // Remove a single entity and all its related issues from the caches\n cache.uncacheEntityID = (entityID) => {\n const entityIssueIDs = cache.issuesByEntityID[entityID];\n if (entityIssueIDs) {\n entityIssueIDs.forEach(issueID => {\n const issue = cache.issuesByIssueID[issueID];\n if (issue) {\n cache.uncacheIssue(issue);\n } else { // shouldn't happen, clean up\n delete cache.issuesByIssueID[issueID];\n }\n });\n }\n\n delete cache.issuesByEntityID[entityID];\n cache.provisionalEntityIDs.delete(entityID);\n };\n\n\n // Return the expandeded set of entityIDs related to issues for the given entityIDs\n //\n // Arguments\n // `entityIDs` - Array or Set containing entityIDs.\n //\n cache.withAllRelatedEntities = (entityIDs) => {\n let result = new Set();\n (entityIDs || []).forEach(entityID => {\n result.add(entityID); // include self\n\n const entityIssueIDs = cache.issuesByEntityID[entityID];\n if (entityIssueIDs) {\n entityIssueIDs.forEach(issueID => {\n const issue = cache.issuesByIssueID[issueID];\n if (issue) {\n (issue.entityIds || []).forEach(relatedID => result.add(relatedID));\n } else { // shouldn't happen, clean up\n delete cache.issuesByIssueID[issueID];\n }\n });\n }\n });\n\n return result;\n };\n\n\n return cache;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { fileFetcher } from './file_fetcher';\nimport { actionDiscardTags } from '../actions/discard_tags';\nimport { actionMergeRemoteChanges } from '../actions/merge_remote_changes';\nimport { actionNoop } from '../actions/noop';\nimport { actionRevert } from '../actions/revert';\nimport { coreGraph } from '../core/graph';\nimport { t } from '../core/localizer';\nimport { utilArrayUnion, utilArrayUniq, utilDisplayName, utilDisplayType, utilRebind } from '../util';\n\n\n/** @param {iD.Context} context */\nexport function coreUploader(context) {\n\n var dispatch = d3_dispatch(\n // Start and end events are dispatched exactly once each per legitimate outside call to `save`\n 'saveStarted', // dispatched as soon as a call to `save` has been deemed legitimate\n 'saveEnded', // dispatched after the result event has been dispatched\n\n 'willAttemptUpload', // dispatched before the actual upload call occurs, if it will\n 'progressChanged',\n\n // Each save results in one of these outcomes:\n 'resultNoChanges', // upload wasn't attempted since there were no edits\n 'resultErrors', // upload failed due to errors\n 'resultConflicts', // upload failed due to data conflicts\n 'resultSuccess' // upload completed without errors\n );\n\n var _isSaving = false;\n\n let _anyConflictsAutomaticallyResolved = false;\n var _conflicts = [];\n var _errors = [];\n var _origChanges;\n\n var _discardTags = {};\n fileFetcher.get('discarded')\n .then(function(d) { _discardTags = d; })\n .catch(function() { /* ignore */ });\n\n const uploader = {};\n\n uploader.isSaving = function() {\n return _isSaving;\n };\n\n uploader.save = function(changeset, tryAgain, checkConflicts) {\n // Guard against accidentally entering save code twice - #4641\n if (_isSaving && !tryAgain) {\n return;\n }\n\n var osm = context.connection();\n if (!osm) return;\n\n // If user somehow got logged out mid-save, try to reauthenticate..\n // This can happen if they were logged in from before, but the tokens are no longer valid.\n if (!osm.authenticated()) {\n osm.authenticate(function(err) {\n if (!err) {\n uploader.save(changeset, tryAgain, checkConflicts); // continue where we left off..\n }\n });\n return;\n }\n\n if (!_isSaving) {\n _isSaving = true;\n dispatch.call('saveStarted', this);\n }\n\n var history = context.history();\n\n _anyConflictsAutomaticallyResolved = false;\n _conflicts = [];\n _errors = [];\n\n // Store original changes, in case user wants to download them as an .osc file\n _origChanges = history.changes(actionDiscardTags(history.difference(), _discardTags));\n\n // First time, `history.perform` a no-op action.\n // Any conflict resolutions will be done as `history.replace`\n // Remember to pop this later if needed\n if (!tryAgain) {\n history.perform(actionNoop());\n }\n\n // Attempt a fast upload.. If there are conflicts, re-enter with `checkConflicts = true`\n if (!checkConflicts) {\n upload(changeset);\n\n // Do the full (slow) conflict check..\n } else {\n performFullConflictCheck(changeset);\n }\n\n };\n\n\n function performFullConflictCheck(changeset) {\n\n var osm = context.connection();\n if (!osm) return;\n\n var history = context.history();\n\n var localGraph = context.graph();\n var remoteGraph = coreGraph(history.base(), true);\n\n var summary = history.difference().summary();\n var _toCheck = [];\n for (var i = 0; i < summary.length; i++) {\n var item = summary[i];\n if (item.changeType === 'modified') {\n _toCheck.push(item.entity.id);\n }\n }\n\n var _toLoad = withChildNodes(_toCheck, localGraph);\n var _loaded = {};\n var _toLoadCount = 0;\n var _toLoadTotal = _toLoad.length;\n\n if (_toCheck.length) {\n dispatch.call('progressChanged', this, _toLoadCount, _toLoadTotal);\n _toLoad.forEach(function(id) { _loaded[id] = false; });\n osm.loadMultiple(_toLoad, loaded);\n } else {\n upload(changeset);\n }\n\n return;\n\n function withChildNodes(ids, graph) {\n var s = new Set(ids);\n ids.forEach(function(id) {\n var entity = graph.entity(id);\n if (entity.type !== 'way') return;\n\n graph.childNodes(entity).forEach(function(child) {\n if (child.version !== undefined) {\n s.add(child.id);\n }\n });\n });\n\n return Array.from(s);\n }\n\n\n // Reload modified entities into an alternate graph and check for conflicts..\n function loaded(err, result) {\n if (_errors.length) return;\n\n if (err) {\n _errors.push({\n msg: err.message || err.responseText,\n details: [ t('save.status_code', { code: err.status }) ]\n });\n didResultInErrors();\n\n } else {\n var loadMore = [];\n\n result.data.forEach(function(entity) {\n remoteGraph.replace(entity);\n _loaded[entity.id] = true;\n _toLoad = _toLoad.filter(function(val) { return val !== entity.id; });\n\n if (!entity.visible) return;\n\n // Because loadMultiple doesn't download /full like loadEntity,\n // need to also load children that aren't already being checked..\n var i, id;\n if (entity.type === 'way') {\n for (i = 0; i < entity.nodes.length; i++) {\n id = entity.nodes[i];\n if (_loaded[id] === undefined) {\n _loaded[id] = false;\n loadMore.push(id);\n }\n }\n } else if (entity.type === 'relation' && entity.isMultipolygon()) {\n for (i = 0; i < entity.members.length; i++) {\n id = entity.members[i].id;\n if (_loaded[id] === undefined) {\n _loaded[id] = false;\n loadMore.push(id);\n }\n }\n }\n });\n\n _toLoadCount += result.data.length;\n _toLoadTotal += loadMore.length;\n dispatch.call('progressChanged', this, _toLoadCount, _toLoadTotal);\n\n if (loadMore.length) {\n _toLoad.push.apply(_toLoad, loadMore);\n osm.loadMultiple(loadMore, loaded);\n }\n\n if (!_toLoad.length) {\n detectConflicts();\n upload(changeset);\n }\n }\n }\n\n\n function detectConflicts() {\n function choice(id, text, action) {\n return {\n id: id,\n text: text,\n action: function() {\n history.replace(action);\n }\n };\n }\n function formatUser(selection, d) {\n selection\n .append('a')\n .attr('href', osm.userURL(d))\n .attr('target', '_blank')\n .text(d);\n }\n function entityName(entity) {\n return utilDisplayName(entity) || (utilDisplayType(entity.id) + ' ' + entity.id);\n }\n\n function sameVersions(local, remote) {\n if (local.version !== remote.version) return false;\n\n if (local.type === 'way') {\n var children = utilArrayUnion(local.nodes, remote.nodes);\n for (var i = 0; i < children.length; i++) {\n var a = localGraph.hasEntity(children[i]);\n var b = remoteGraph.hasEntity(children[i]);\n if (a && b && a.version !== b.version) return false;\n }\n }\n\n return true;\n }\n\n _toCheck.forEach(function(id) {\n var local = localGraph.entity(id);\n var remote = remoteGraph.entity(id);\n\n if (sameVersions(local, remote)) return;\n\n var merge = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags, formatUser);\n\n history.replace(merge);\n\n var mergeConflicts = merge.conflicts();\n if (!mergeConflicts.length) {\n _anyConflictsAutomaticallyResolved = true;\n return; // merged safely\n }\n\n var forceLocal = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags).withOption('force_local');\n var forceRemote = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags).withOption('force_remote');\n var keepMine = t('save.conflict.' + (remote.visible ? 'keep_local' : 'restore'));\n var keepTheirs = t('save.conflict.' + (remote.visible ? 'keep_remote' : 'delete'));\n\n _conflicts.push({\n id: id,\n name: entityName(local),\n details: mergeConflicts,\n chosen: 1,\n choices: [\n choice(id, keepMine, forceLocal),\n choice(id, keepTheirs, forceRemote)\n ]\n });\n });\n }\n }\n\n\n async function upload(changeset) {\n var osm = context.connection();\n if (!osm) {\n _errors.push({ msg: 'No OSM Service' });\n }\n\n if (_conflicts.length) {\n didResultInConflicts(changeset);\n\n } else if (_errors.length) {\n didResultInErrors();\n\n } else {\n if (_anyConflictsAutomaticallyResolved) {\n // add a changeset tag to aid reviewers\n changeset.tags.merge_conflict_resolved = 'automatically';\n await osm.updateChangesetTags(changeset);\n }\n var history = context.history();\n var changes = history.changes(actionDiscardTags(history.difference(), _discardTags));\n if (changes.modified.length || changes.created.length || changes.deleted.length) {\n\n dispatch.call('willAttemptUpload', this);\n\n osm.putChangeset(changeset, changes, uploadCallback);\n\n } else {\n // changes were insignificant or reverted by user\n didResultInNoChanges();\n }\n }\n }\n\n\n function uploadCallback(err, changeset) {\n if (err) {\n if (err.status === 409) { // 409 Conflict\n uploader.save(changeset, true, true); // tryAgain = true, checkConflicts = true\n } else {\n _errors.push({\n msg: err.message || err.responseText,\n details: [ t('save.status_code', { code: err.status }) ]\n });\n didResultInErrors();\n }\n\n } else {\n didResultInSuccess(changeset);\n }\n }\n\n function didResultInNoChanges() {\n\n dispatch.call('resultNoChanges', this);\n\n endSave();\n\n context.flush(); // reset iD\n }\n\n function didResultInErrors() {\n\n context.history().pop();\n\n dispatch.call('resultErrors', this, _errors);\n\n endSave();\n }\n\n\n function didResultInConflicts(changeset) {\n // add a changeset tag to aid reviewers\n changeset.tags.merge_conflict_resolved = 'manually';\n context.connection().updateChangesetTags(changeset);\n\n _conflicts.sort(function(a, b) { return b.id.localeCompare(a.id); });\n\n dispatch.call('resultConflicts', this, changeset, _conflicts, _origChanges);\n\n endSave();\n }\n\n\n function didResultInSuccess(changeset) {\n\n // delete the edit stack cached to local storage\n context.history().clearSaved();\n\n dispatch.call('resultSuccess', this, changeset);\n\n // Add delay to allow for postgres replication #1646 #2678\n window.setTimeout(function() {\n\n endSave();\n\n context.flush(); // reset iD\n }, 2500);\n }\n\n\n function endSave() {\n _isSaving = false;\n\n dispatch.call('saveEnded', this);\n }\n\n\n uploader.cancelConflictResolution = function() {\n context.history().pop();\n };\n\n\n uploader.processResolvedConflicts = function(changeset) {\n var history = context.history();\n\n for (var i = 0; i < _conflicts.length; i++) {\n if (_conflicts[i].chosen === 1) { // user chose \"use theirs\"\n var entity = context.hasEntity(_conflicts[i].id);\n if (entity && entity.type === 'way') {\n var children = utilArrayUniq(entity.nodes);\n for (var j = 0; j < children.length; j++) {\n history.replace(actionRevert(children[j]));\n }\n }\n history.replace(actionRevert(_conflicts[i].id));\n }\n }\n\n uploader.save(changeset, true, false); // tryAgain = true, checkConflicts = false\n };\n\n\n uploader.reset = function() {\n\n };\n\n\n return utilRebind(uploader, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDrawWay } from '../behavior/draw_way';\n\n\nexport function modeDrawArea(context, wayID, startGraph, button) {\n var mode = {\n button: button,\n id: 'draw-area'\n };\n\n var behavior = behaviorDrawWay(context, wayID, mode, startGraph)\n .on('rejectedSelfIntersection.modeDrawArea', function() {\n context.ui().flash\n .iconName('#iD-icon-no')\n .label(t.append('self_intersection.error.areas'))();\n });\n\n mode.wayID = wayID;\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n mode.selectedIDs = function() {\n return [wayID];\n };\n\n mode.activeID = function() {\n return (behavior && behavior.activeID()) || [];\n };\n\n return mode;\n}\n", "import { actionAddEntity } from '../actions/add_entity';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionAddVertex } from '../actions/add_vertex';\n\nimport { behaviorAddWay } from '../behavior/add_way';\nimport { modeDrawArea } from './draw_area';\nimport { osmNode, osmWay } from '../osm';\n\n\nexport function modeAddArea(context, mode) {\n mode.id = 'add-area';\n\n var behavior = behaviorAddWay(context)\n .on('start', start)\n .on('startFromWay', startFromWay)\n .on('startFromNode', startFromNode);\n\n function defaultTags(loc) {\n var defaultTags = { area: 'yes' };\n if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'area', false, loc);\n return defaultTags;\n }\n\n function actionClose(wayId) {\n return function (graph) {\n return graph.replace(graph.entity(wayId).close());\n };\n }\n\n\n function start(loc) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionClose(way.id)\n );\n\n context.enter(modeDrawArea(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromWay(loc, edge) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionClose(way.id),\n actionAddMidpoint({ loc: loc, edge: edge }, node)\n );\n\n context.enter(modeDrawArea(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromNode(node) {\n var startGraph = context.graph();\n var way = osmWay({ tags: defaultTags(node.loc) });\n\n context.perform(\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionClose(way.id)\n );\n\n context.enter(modeDrawArea(context, way.id, startGraph, mode.button));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n\n return mode;\n}\n", "import { actionAddEntity } from '../actions/add_entity';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionAddVertex } from '../actions/add_vertex';\n\nimport { behaviorAddWay } from '../behavior/add_way';\nimport { modeDrawLine } from './draw_line';\nimport { osmNode, osmWay } from '../osm';\n\n\nexport function modeAddLine(context, mode) {\n mode.id = 'add-line';\n\n var behavior = behaviorAddWay(context)\n .on('start', start)\n .on('startFromWay', startFromWay)\n .on('startFromNode', startFromNode);\n\n function defaultTags(loc) {\n var defaultTags = {};\n if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'line', false, loc);\n return defaultTags;\n }\n\n\n function start(loc) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id)\n );\n\n context.enter(modeDrawLine(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromWay(loc, edge) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionAddMidpoint({ loc: loc, edge: edge }, node)\n );\n\n context.enter(modeDrawLine(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromNode(node) {\n var startGraph = context.graph();\n var way = osmWay({ tags: defaultTags(node.loc) });\n\n context.perform(\n actionAddEntity(way),\n actionAddVertex(way.id, node.id)\n );\n\n context.enter(modeDrawLine(context, way.id, startGraph, mode.button));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDraw } from '../behavior/draw';\nimport { modeBrowse } from './browse';\nimport { modeSelect } from './select';\nimport { osmNode } from '../osm/node';\nimport { actionAddEntity } from '../actions/add_entity';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\n\n\nexport function modeAddPoint(context, mode) {\n\n mode.id = 'add-point';\n\n var behavior = behaviorDraw(context)\n .on('click', add)\n .on('clickWay', addWay)\n .on('clickNode', addNode)\n .on('cancel', cancel)\n .on('finish', cancel);\n\n function defaultTags(loc) {\n var defaultTags = {};\n if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'point', false, loc);\n return defaultTags;\n }\n\n\n function add(loc) {\n var node = osmNode({ loc: loc, tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n t('operations.add.annotation.point')\n );\n\n enterSelectMode(node);\n }\n\n\n function addWay(loc, edge) {\n var node = osmNode({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddMidpoint({loc: loc, edge: edge}, node),\n t('operations.add.annotation.vertex')\n );\n\n enterSelectMode(node);\n }\n\n function enterSelectMode(node) {\n context.enter(\n modeSelect(context, [node.id]).newFeature(true)\n );\n }\n\n\n function addNode(node) {\n const _defaultTags = defaultTags(node.loc);\n if (Object.keys(_defaultTags).length === 0) {\n enterSelectMode(node);\n return;\n }\n\n var tags = Object.assign({}, node.tags); // shallow copy\n for (var key in _defaultTags) {\n tags[key] = _defaultTags[key];\n }\n\n context.perform(\n actionChangeTags(node.id, tags),\n t('operations.add.annotation.point')\n );\n\n enterSelectMode(node);\n }\n\n\n function cancel() {\n context.enter(modeBrowse(context));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n\n return mode;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select,\n selection as d3_selection\n} from 'd3-selection';\n\nimport { geoVecLength } from '../geo';\nimport { osmNote } from '../osm';\nimport { utilRebind } from '../util/rebind';\nimport { utilFastMouse, utilPrefixCSSProperty, utilPrefixDOMProperty } from '../util';\n\n\n/*\n `behaviorDrag` is like `d3_behavior.drag`, with the following differences:\n\n * The `origin` function is expected to return an [x, y] tuple rather than an\n {x, y} object.\n * The events are `start`, `move`, and `end`.\n (https://github.com/mbostock/d3/issues/563)\n * The `start` event is not dispatched until the first cursor movement occurs.\n (https://github.com/mbostock/d3/pull/368)\n * The `move` event has a `point` and `delta` [x, y] tuple properties rather\n than `x`, `y`, `dx`, and `dy` properties.\n * The `end` event is not dispatched if no movement occurs.\n * An `off` function is available that unbinds the drag's internal event handlers.\n */\n\nexport function behaviorDrag() {\n var dispatch = d3_dispatch('start', 'move', 'end');\n\n // see also behaviorSelect\n var _tolerancePx = 1; // keep this low to facilitate pixel-perfect micromapping\n var _penTolerancePx = 4; // styluses can be touchy so require greater movement - #1981\n\n var _origin = null;\n var _selector = '';\n var _targetNode;\n var _targetEntity;\n var _surface;\n var _pointerId;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n var d3_event_userSelectProperty = utilPrefixCSSProperty('UserSelect');\n var d3_event_userSelectSuppress = function() {\n var selection = d3_selection();\n var select = selection.style(d3_event_userSelectProperty);\n selection.style(d3_event_userSelectProperty, 'none');\n return function() {\n selection.style(d3_event_userSelectProperty, select);\n };\n };\n\n\n function pointerdown(d3_event) {\n\n if (_pointerId) return;\n\n _pointerId = d3_event.pointerId || 'mouse';\n\n _targetNode = this;\n\n // only force reflow once per drag\n var pointerLocGetter = utilFastMouse(_surface || _targetNode.parentNode);\n\n var offset;\n var startOrigin = pointerLocGetter(d3_event);\n var started = false;\n var selectEnable = d3_event_userSelectSuppress();\n\n d3_select(window)\n .on(_pointerPrefix + 'move.drag', pointermove)\n .on(_pointerPrefix + 'up.drag pointercancel.drag', pointerup, true);\n\n if (_origin) {\n offset = _origin.call(_targetNode, _targetEntity);\n offset = [offset[0] - startOrigin[0], offset[1] - startOrigin[1]];\n } else {\n offset = [0, 0];\n }\n\n d3_event.stopPropagation();\n\n\n function pointermove(d3_event) {\n if (_pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n var p = pointerLocGetter(d3_event);\n\n if (!started) {\n var dist = geoVecLength(startOrigin, p);\n var tolerance = d3_event.pointerType === 'pen' ? _penTolerancePx : _tolerancePx;\n // don't start until the drag has actually moved somewhat\n if (dist < tolerance) return;\n\n started = true;\n dispatch.call('start', this, d3_event, _targetEntity);\n\n // Don't send a `move` event in the same cycle as `start` since dragging\n // a midpoint will convert the target to a node.\n } else {\n\n startOrigin = p;\n d3_event.stopPropagation();\n d3_event.preventDefault();\n\n var dx = p[0] - startOrigin[0];\n var dy = p[1] - startOrigin[1];\n dispatch.call('move', this, d3_event, _targetEntity, [p[0] + offset[0], p[1] + offset[1]], [dx, dy]);\n }\n }\n\n\n function pointerup(d3_event) {\n if (_pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n _pointerId = null;\n\n if (started) {\n dispatch.call('end', this, d3_event, _targetEntity);\n\n d3_event.preventDefault();\n }\n\n d3_select(window)\n .on(_pointerPrefix + 'move.drag', null)\n .on(_pointerPrefix + 'up.drag pointercancel.drag', null);\n\n selectEnable();\n }\n }\n\n\n function behavior(selection) {\n var matchesSelector = utilPrefixDOMProperty('matchesSelector');\n var delegate = pointerdown;\n\n if (_selector) {\n delegate = function(d3_event) {\n var root = this;\n var target = d3_event.target;\n for (; target && target !== root; target = target.parentNode) {\n var datum = target.__data__;\n\n _targetEntity = datum instanceof osmNote ? datum\n : datum && datum.properties && datum.properties.entity;\n\n if (_targetEntity && target[matchesSelector](_selector)) {\n return pointerdown.call(target, d3_event);\n }\n }\n };\n }\n\n selection\n .on(_pointerPrefix + 'down.drag' + _selector, delegate);\n }\n\n\n behavior.off = function(selection) {\n selection\n .on(_pointerPrefix + 'down.drag' + _selector, null);\n };\n\n\n behavior.selector = function(_) {\n if (!arguments.length) return _selector;\n _selector = _;\n return behavior;\n };\n\n\n behavior.origin = function(_) {\n if (!arguments.length) return _origin;\n _origin = _;\n return behavior;\n };\n\n\n behavior.cancel = function() {\n d3_select(window)\n .on(_pointerPrefix + 'move.drag', null)\n .on(_pointerPrefix + 'up.drag pointercancel.drag', null);\n return behavior;\n };\n\n\n behavior.targetNode = function(_) {\n if (!arguments.length) return _targetNode;\n _targetNode = _;\n return behavior;\n };\n\n\n behavior.targetEntity = function(_) {\n if (!arguments.length) return _targetEntity;\n _targetEntity = _;\n return behavior;\n };\n\n\n behavior.surface = function(_) {\n if (!arguments.length) return _surface;\n _surface = _;\n return behavior;\n };\n\n\n return utilRebind(behavior, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { t } from '../core/localizer';\n\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionConnect } from '../actions/connect';\nimport { actionMoveNode } from '../actions/move_node';\nimport { actionNoop } from '../actions/noop';\n\nimport { behaviorDrag } from '../behavior/drag';\nimport { behaviorEdit } from '../behavior/edit';\nimport { behaviorHover } from '../behavior/hover';\n\nimport {\n geoChooseEdge,\n geoHasLineIntersections,\n geoHasSelfIntersections,\n geoVecSubtract,\n geoViewportEdge\n} from '../geo';\n\nimport { modeBrowse } from './browse';\nimport { modeSelect } from './select';\nimport { osmJoinWays, osmNode } from '../osm';\nimport { utilArrayIntersection, utilKeybinding } from '../util';\n\n\n\nexport function modeDragNode(context) {\n var mode = {\n id: 'drag-node',\n button: 'browse'\n };\n var hover = behaviorHover(context).altDisables(true)\n .on('hover', context.ui().sidebar.hover);\n var edit = behaviorEdit(context);\n\n var _nudgeInterval;\n var _restoreSelectedIDs = [];\n var _wasMidpoint = false;\n var _isCancelled = false;\n var _activeEntity;\n var _startLoc;\n var _lastLoc;\n\n\n function startNudge(d3_event, entity, nudge) {\n if (_nudgeInterval) window.clearInterval(_nudgeInterval);\n _nudgeInterval = window.setInterval(function() {\n context.map().pan(nudge);\n doMove(d3_event, entity, nudge);\n }, 50);\n }\n\n\n function stopNudge() {\n if (_nudgeInterval) {\n window.clearInterval(_nudgeInterval);\n _nudgeInterval = null;\n }\n }\n\n\n function moveAnnotation(entity) {\n return t('operations.move.annotation.' + entity.geometry(context.graph()));\n }\n\n\n function connectAnnotation(nodeEntity, targetEntity) {\n var nodeGeometry = nodeEntity.geometry(context.graph());\n var targetGeometry = targetEntity.geometry(context.graph());\n if (nodeGeometry === 'vertex' && targetGeometry === 'vertex') {\n var nodeParentWayIDs = context.graph().parentWays(nodeEntity);\n var targetParentWayIDs = context.graph().parentWays(targetEntity);\n var sharedParentWays = utilArrayIntersection(nodeParentWayIDs, targetParentWayIDs);\n // if both vertices are part of the same way\n if (sharedParentWays.length !== 0) {\n // if the nodes are next to each other, they are merged\n if (sharedParentWays[0].areAdjacent(nodeEntity.id, targetEntity.id)) {\n return t('operations.connect.annotation.from_vertex.to_adjacent_vertex');\n }\n return t('operations.connect.annotation.from_vertex.to_sibling_vertex');\n }\n }\n return t('operations.connect.annotation.from_' + nodeGeometry + '.to_' + targetGeometry);\n }\n\n\n function shouldSnapToNode(target) {\n if (!_activeEntity) return false;\n return _activeEntity.geometry(context.graph()) !== 'vertex' ||\n (target.geometry(context.graph()) === 'vertex' || presetManager.allowsVertex(target, context.graph()));\n }\n\n\n function origin(entity) {\n return context.projection(entity.loc);\n }\n\n\n function keydown(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope')) {\n context.surface()\n .classed('nope-suppressed', true);\n }\n context.surface()\n .classed('nope', false)\n .classed('nope-disabled', true);\n }\n }\n\n\n function keyup(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope-suppressed')) {\n context.surface()\n .classed('nope', true);\n }\n context.surface()\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false);\n }\n }\n\n\n function start(d3_event, entity) {\n _wasMidpoint = entity.type === 'midpoint';\n var hasHidden = context.features().hasHiddenConnections(entity, context.graph());\n _isCancelled = !context.editable() || d3_event.shiftKey || hasHidden;\n\n\n if (_isCancelled) {\n if (hasHidden) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('modes.drag_node.connected_to_hidden'))();\n }\n return drag.cancel();\n }\n\n if (_wasMidpoint) {\n var midpoint = entity;\n entity = osmNode();\n context.perform(actionAddMidpoint(midpoint, entity));\n entity = context.entity(entity.id); // get post-action entity\n\n var vertex = context.surface().selectAll('.' + entity.id);\n drag.targetNode(vertex.node())\n .targetEntity(entity);\n\n } else {\n context.perform(actionNoop());\n }\n\n _activeEntity = entity;\n _startLoc = entity.loc;\n\n hover.ignoreVertex(entity.geometry(context.graph()) === 'vertex');\n\n context.surface().selectAll('.' + _activeEntity.id)\n .classed('active', true);\n\n context.enter(mode);\n }\n\n\n // related code\n // - `behavior/draw.js` `datum()`\n function datum(d3_event) {\n if (!d3_event || d3_event.altKey) {\n return {};\n } else {\n // When dragging, snap only to touch targets..\n // (this excludes area fills and active drawing elements)\n var d = d3_event.target.__data__;\n return (d && d.properties && d.properties.target) ? d : {};\n }\n }\n\n\n function doMove(d3_event, entity, nudge) {\n nudge = nudge || [0, 0];\n\n var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc);\n var currMouse = geoVecSubtract(currPoint, nudge);\n var loc = context.projection.invert(currMouse);\n\n var target, edge;\n\n if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap..\n // related code\n // - `mode/drag_node.js` `doMove()`\n // - `behavior/draw.js` `click()`\n // - `behavior/draw_way.js` `move()`\n var d = datum(d3_event);\n target = d && d.properties && d.properties.entity;\n var targetLoc = target && target.loc;\n var targetNodes = d && d.properties && d.properties.nodes;\n\n if (targetLoc) { // snap to node/vertex - a point target with `.loc`\n if (shouldSnapToNode(target)) {\n loc = targetLoc;\n }\n\n } else if (targetNodes) { // snap to way - a line target with `.nodes`\n edge = geoChooseEdge(targetNodes, context.map().mouse(), context.projection, end.id);\n if (edge) {\n loc = edge.loc;\n }\n }\n }\n\n context.replace(\n actionMoveNode(entity.id, loc)\n );\n\n // Below here: validations\n var isInvalid = false;\n\n // Check if this connection to `target` could cause relations to break..\n if (target) {\n isInvalid = hasRelationConflict(entity, target, edge, context.graph());\n }\n\n // Check if this drag causes the geometry to break..\n if (!isInvalid) {\n isInvalid = hasInvalidGeometry(entity, context.graph());\n }\n\n\n var nope = context.surface().classed('nope');\n if (isInvalid === 'relation' || isInvalid === 'restriction') {\n if (!nope) { // about to nope - show hint\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('operations.connect.' + isInvalid,\n { relation: presetManager.item('type/restriction').name() }\n ))();\n }\n } else if (isInvalid) {\n var errorID = isInvalid === 'line' ? 'lines' : 'areas';\n context.ui().flash\n .duration(3000)\n .iconName('#iD-icon-no')\n .label(t.append('self_intersection.error.' + errorID))();\n } else {\n if (nope) { // about to un-nope, remove hint\n context.ui().flash\n .duration(1)\n .label('')();\n }\n }\n\n\n var nopeDisabled = context.surface().classed('nope-disabled');\n if (nopeDisabled) {\n context.surface()\n .classed('nope', false)\n .classed('nope-suppressed', isInvalid);\n } else {\n context.surface()\n .classed('nope', isInvalid)\n .classed('nope-suppressed', false);\n }\n\n _lastLoc = loc;\n }\n\n\n // Uses `actionConnect.disabled()` to know whether this connection is ok..\n function hasRelationConflict(entity, target, edge, graph) {\n var testGraph = graph.update(); // copy\n\n // if snapping to way - add midpoint there and consider that the target..\n if (edge) {\n var midpoint = osmNode();\n var action = actionAddMidpoint({\n loc: edge.loc,\n edge: [target.nodes[edge.index - 1], target.nodes[edge.index]]\n }, midpoint);\n\n testGraph = action(testGraph);\n target = midpoint;\n }\n\n // can we connect to it?\n var ids = [entity.id, target.id];\n return actionConnect(ids).disabled(testGraph);\n }\n\n\n function hasInvalidGeometry(entity, graph) {\n var parents = graph.parentWays(entity);\n var i, j, k;\n\n for (i = 0; i < parents.length; i++) {\n var parent = parents[i];\n var activeIndex = null; // which multipolygon ring contains node being dragged\n\n // test any parent multipolygons for valid geometry\n var relations = graph.parentRelations(parent);\n for (j = 0; j < relations.length; j++) {\n if (!relations[j].isMultipolygon()) continue;\n\n var rings = osmJoinWays(relations[j].members, graph);\n\n // find active ring and test it for self intersections\n for (k = 0; k < rings.length; k++) {\n const nodes = rings[k].nodes;\n if (nodes.find(function(n) { return n.id === entity.id; })) {\n activeIndex = k;\n if (geoHasSelfIntersections(nodes, entity.id)) {\n return 'multipolygonMember';\n }\n }\n rings[k].coords = nodes.map(function(n) { return n.loc; });\n }\n\n // test active ring for intersections with other rings in the multipolygon\n for (k = 0; k < rings.length; k++) {\n if (k === activeIndex) continue;\n\n // make sure active ring doesn't cross passive rings\n if (geoHasLineIntersections(rings[activeIndex].nodes, rings[k].nodes, entity.id)) {\n return 'multipolygonRing';\n }\n }\n }\n\n\n // If we still haven't tested this node's parent way for self-intersections.\n // (because it's not a member of a multipolygon), test it now.\n if (activeIndex === null) {\n const nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); });\n if (nodes.length && geoHasSelfIntersections(nodes, entity.id)) {\n return parent.geometry(graph);\n }\n }\n\n }\n\n return false;\n }\n\n\n function move(d3_event, entity, point) {\n if (_isCancelled) return;\n d3_event.stopPropagation();\n\n context.surface().classed('nope-disabled', d3_event.altKey);\n\n _lastLoc = context.projection.invert(point);\n\n doMove(d3_event, entity);\n var nudge = geoViewportEdge(point, context.map().dimensions());\n if (nudge) {\n startNudge(d3_event, entity, nudge);\n } else {\n stopNudge();\n }\n }\n\n function end(d3_event, entity) {\n if (_isCancelled) return;\n\n var wasPoint = entity.geometry(context.graph()) === 'point';\n\n var d = datum(d3_event);\n var nope = (d && d.properties && d.properties.nope) || context.surface().classed('nope');\n var target = d && d.properties && d.properties.entity; // entity to snap to\n\n if (nope) { // bounce back\n context.perform(\n _actionBounceBack(entity.id, _startLoc)\n );\n\n } else if (target && target.type === 'way') {\n var choice = geoChooseEdge(context.graph().childNodes(target), context.map().mouse(), context.projection, entity.id);\n context.replace(\n actionAddMidpoint({\n loc: choice.loc,\n edge: [target.nodes[choice.index - 1], target.nodes[choice.index]]\n }, entity),\n connectAnnotation(entity, target)\n );\n\n } else if (target && target.type === 'node' && shouldSnapToNode(target)) {\n context.replace(\n actionConnect([target.id, entity.id]),\n connectAnnotation(entity, target)\n );\n\n } else if (_wasMidpoint) {\n context.replace(\n actionNoop(),\n t('operations.add.annotation.vertex')\n );\n\n } else {\n context.replace(\n actionNoop(),\n moveAnnotation(entity)\n );\n }\n\n if (wasPoint) {\n context.enter(modeSelect(context, [entity.id]));\n\n } else {\n var reselection = _restoreSelectedIDs.filter(function(id) {\n return context.graph().hasEntity(id);\n });\n\n if (reselection.length) {\n context.enter(modeSelect(context, reselection));\n } else {\n context.enter(modeBrowse(context));\n }\n }\n }\n\n\n function _actionBounceBack(nodeID, toLoc) {\n var moveNode = actionMoveNode(nodeID, toLoc);\n var action = function(graph, t) {\n // last time through, pop off the bounceback perform.\n // it will then overwrite the initial perform with a moveNode that does nothing\n if (t === 1) context.pop();\n return moveNode(graph, t);\n };\n action.transitionable = true;\n return action;\n }\n\n\n function cancel() {\n drag.cancel();\n context.enter(modeBrowse(context));\n }\n\n\n var drag = behaviorDrag()\n .selector('.layer-touch.points .target')\n .surface(context.container().select('.main-map').node())\n .origin(origin)\n .on('start', start)\n .on('move', move)\n .on('end', end);\n\n\n mode.enter = function() {\n context.install(hover);\n context.install(edit);\n\n d3_select(window)\n .on('keydown.dragNode', keydown)\n .on('keyup.dragNode', keyup);\n\n context.history()\n .on('undone.drag-node', cancel);\n };\n\n\n mode.exit = function() {\n context.ui().sidebar.hover.cancel();\n context.uninstall(hover);\n context.uninstall(edit);\n\n d3_select(window)\n .on('keydown.dragNode', null)\n .on('keyup.dragNode', null);\n\n context.history()\n .on('undone.drag-node', null);\n\n _activeEntity = null;\n\n context.surface()\n .classed('nope', false)\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false)\n .selectAll('.active')\n .classed('active', false);\n\n stopNudge();\n };\n\n\n mode.selectedIDs = function() {\n if (!arguments.length) return _activeEntity ? [_activeEntity.id] : [];\n // no assign\n return mode;\n };\n\n\n mode.activeID = function() {\n if (!arguments.length) return _activeEntity && _activeEntity.id;\n // no assign\n return mode;\n };\n\n\n mode.restoreSelectedIDs = function(_) {\n if (!arguments.length) return _restoreSelectedIDs;\n _restoreSelectedIDs = _;\n return mode;\n };\n\n\n mode.behavior = drag;\n\n\n return mode;\n}\n", "import { services } from '../services';\nimport { actionNoop } from '../actions/noop';\nimport { behaviorDrag } from '../behavior/drag';\nimport { behaviorEdit } from '../behavior/edit';\nimport { geoVecSubtract, geoViewportEdge } from '../geo';\nimport { modeSelectNote } from './select_note';\n\n\nexport function modeDragNote(context) {\n var mode = {\n id: 'drag-note',\n button: 'browse'\n };\n\n var edit = behaviorEdit(context);\n\n var _nudgeInterval;\n var _lastLoc;\n var _note; // most current note.. dragged note may have stale datum.\n\n\n function startNudge(d3_event, nudge) {\n if (_nudgeInterval) window.clearInterval(_nudgeInterval);\n _nudgeInterval = window.setInterval(function() {\n context.map().pan(nudge);\n doMove(d3_event, nudge);\n }, 50);\n }\n\n\n function stopNudge() {\n if (_nudgeInterval) {\n window.clearInterval(_nudgeInterval);\n _nudgeInterval = null;\n }\n }\n\n\n function origin(note) {\n return context.projection(note.loc);\n }\n\n\n function start(d3_event, note) {\n _note = note;\n var osm = services.osm;\n if (osm) {\n // Get latest note from cache.. The marker may have a stale datum bound to it\n // and dragging it around can sometimes delete the users note comment.\n _note = osm.getNote(_note.id);\n }\n\n context.surface().selectAll('.note-' + _note.id)\n .classed('active', true);\n\n context.perform(actionNoop());\n context.enter(mode);\n context.selectedNoteID(_note.id);\n }\n\n\n function move(d3_event, entity, point) {\n d3_event.stopPropagation();\n _lastLoc = context.projection.invert(point);\n\n doMove(d3_event);\n var nudge = geoViewportEdge(point, context.map().dimensions());\n if (nudge) {\n startNudge(d3_event, nudge);\n } else {\n stopNudge();\n }\n }\n\n\n function doMove(d3_event, nudge) {\n nudge = nudge || [0, 0];\n\n var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc);\n var currMouse = geoVecSubtract(currPoint, nudge);\n var loc = context.projection.invert(currMouse);\n\n _note = _note.move(loc);\n\n var osm = services.osm;\n if (osm) {\n osm.replaceNote(_note); // update note cache\n }\n\n context.replace(actionNoop()); // trigger redraw\n }\n\n\n function end() {\n context.replace(actionNoop()); // trigger redraw\n\n context\n .selectedNoteID(_note.id)\n .enter(modeSelectNote(context, _note.id));\n }\n\n\n var drag = behaviorDrag()\n .selector('.layer-touch.markers .target.note.new')\n .surface(context.container().select('.main-map').node())\n .origin(origin)\n .on('start', start)\n .on('move', move)\n .on('end', end);\n\n\n mode.enter = function() {\n context.install(edit);\n };\n\n\n mode.exit = function() {\n context.ui().sidebar.hover.cancel();\n context.uninstall(edit);\n\n context.surface()\n .selectAll('.active')\n .classed('active', false);\n\n stopNudge();\n };\n\n mode.behavior = drag;\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiDataHeader() {\n var _datum;\n\n\n function dataHeader(selection) {\n var header = selection.selectAll('.data-header')\n .data(\n (_datum ? [_datum] : []),\n function(d) { return d.__featurehash__; }\n );\n\n header.exit()\n .remove();\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'data-header');\n\n var iconEnter = headerEnter\n .append('div')\n .attr('class', 'data-header-icon');\n\n iconEnter\n .append('div')\n .attr('class', 'preset-icon-28')\n .call(svgIcon('#iD-icon-data', 'note-fill'));\n\n headerEnter\n .append('div')\n .attr('class', 'data-header-label')\n .call(t.append('map_data.layers.custom.title'));\n }\n\n\n dataHeader.datum = function(val) {\n if (!arguments.length) return _datum;\n _datum = val;\n return this;\n };\n\n\n return dataHeader;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { utilGetSetValue, utilRebind, utilTriggerEvent } from '../util';\n\n\n// This code assumes that the combobox values will not have duplicate entries.\n// It is keyed on the `value` of the entry. Data should be an array of objects like:\n// [{\n// value: 'string value', // required\n// display: 'label function' // optional, if present will be called with d3 selection\n// to modify/append, see localizer's t.append\n// title: 'hover text' // optional\n// terms: ['search terms'] // optional\n// }, ...]\n\nvar _comboHideTimerID;\n\nexport function uiCombobox(context, klass) {\n var dispatch = d3_dispatch('accept', 'cancel', 'update');\n var container = context.container();\n\n var _suggestions = [];\n var _data = [];\n var _fetched = {};\n var _selected = null;\n var _canAutocomplete = true;\n var _caseSensitive = false;\n var _cancelFetch = false;\n var _minItems = 2;\n var _tDown = 0;\n var _mouseEnterHandler, _mouseLeaveHandler;\n\n var _fetcher = function(val, cb) {\n cb(_data.filter(function(d) {\n var terms = d.terms || [];\n terms.push(d.value);\n if (d.key) {\n terms.push(d.key);\n }\n return terms.some(function(term) {\n return term\n .toString()\n .toLowerCase()\n .indexOf(val.toLowerCase()) !== -1;\n });\n }));\n };\n\n var combobox = function(input, attachTo) {\n if (!input || input.empty()) return;\n\n input\n .classed('combobox-input', true)\n .on('focus.combo-input', focus)\n .on('blur.combo-input', blur)\n .on('keydown.combo-input', keydown)\n .on('keyup.combo-input', keyup)\n .on('input.combo-input', change)\n .on('mousedown.combo-input', mousedown)\n .each(function() {\n var parent = this.parentNode;\n var sibling = this.nextSibling;\n\n d3_select(parent).selectAll('.combobox-caret')\n .filter(function(d) { return d === input.node(); })\n .data([input.node()])\n .enter()\n .insert('div', function() { return sibling; })\n .attr('class', 'combobox-caret')\n .on('mousedown.combo-caret', function(d3_event) {\n d3_event.preventDefault(); // don't steal focus from input\n input.node().focus(); // focus the input as if it was clicked\n mousedown(d3_event);\n })\n .on('mouseup.combo-caret', function(d3_event) {\n d3_event.preventDefault(); // don't steal focus from input\n mouseup(d3_event);\n });\n });\n\n\n function mousedown(d3_event) {\n if (d3_event.button !== 0) return; // left click only\n if (input.classed('disabled')) return;\n _tDown = +new Date();\n\n // mousedown should never bubble up (see #10481)\n d3_event.stopPropagation();\n\n // clear selection\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n if (start !== end) {\n var val = utilGetSetValue(input);\n input.node().setSelectionRange(val.length, val.length);\n return;\n }\n\n input.on('mouseup.combo-input', mouseup);\n }\n\n\n function mouseup(d3_event) {\n input.on('mouseup.combo-input', null);\n if (d3_event.button !== 0) return; // left click only\n if (input.classed('disabled')) return;\n if (input.node() !== document.activeElement) return; // exit if this input is not focused\n\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n if (start !== end) return; // exit if user is selecting\n\n // not showing or showing for a different field - try to show it.\n var combo = container.selectAll('.combobox');\n if (combo.empty() || combo.datum() !== input.node()) {\n var tOrig = _tDown;\n window.setTimeout(function() {\n if (tOrig !== _tDown) return; // exit if user double clicked\n fetchComboData('', function() {\n show();\n render();\n });\n }, 250);\n\n } else {\n hide();\n }\n }\n\n\n function focus() {\n fetchComboData(''); // prefetch values (may warm taginfo cache)\n }\n\n\n function blur() {\n _comboHideTimerID = window.setTimeout(hide, 75);\n }\n\n\n function show() {\n hide(); // remove any existing\n\n container\n .insert('div', ':first-child')\n .datum(input.node())\n .attr('class', 'combobox' + (klass ? ' combobox-' + klass : ''))\n .style('position', 'absolute')\n .style('display', 'block')\n .style('left', '0px')\n .on('mousedown.combo-container', function (d3_event) {\n // prevent moving focus out of the input field\n d3_event.preventDefault();\n });\n\n container\n .on('scroll.combo-scroll', render, true);\n }\n\n function hide() {\n _hide(container);\n }\n\n\n function keydown(d3_event) {\n var shown = !container.selectAll('.combobox').empty();\n var tagName = input.node() ? input.node().tagName.toLowerCase() : '';\n\n switch (d3_event.keyCode) {\n case 8: // \u232B Backspace\n case 46: // \u2326 Delete\n d3_event.stopPropagation();\n _selected = null;\n render();\n input.on('input.combo-input', function() {\n var start = input.property('selectionStart');\n input.node().setSelectionRange(start, start);\n input.on('input.combo-input', change); // reset event handler\n change(false);\n });\n break;\n\n case 9: // \u21E5 Tab\n accept(d3_event);\n break;\n\n case 13: // \u21A9 Return\n d3_event.preventDefault();\n d3_event.stopPropagation();\n accept(d3_event);\n break;\n\n case 38: // \u2191 Up arrow\n if (tagName === 'textarea' && !shown) return;\n d3_event.preventDefault();\n if (tagName === 'input' && !shown) {\n show();\n }\n nav(-1);\n break;\n\n case 40: // \u2193 Down arrow\n if (tagName === 'textarea' && !shown) return;\n d3_event.preventDefault();\n if (tagName === 'input' && !shown) {\n show();\n }\n nav(+1);\n break;\n }\n }\n\n\n function keyup(d3_event) {\n switch (d3_event.keyCode) {\n case 27: // \u238B Escape\n cancel();\n break;\n }\n }\n\n\n // Called whenever the input value is changed (e.g. on typing)\n function change(doAutoComplete) {\n if (doAutoComplete === undefined) doAutoComplete = true;\n fetchComboData(value(), function(skipAutosuggest) {\n _selected = null;\n var val = input.property('value');\n\n if (_suggestions.length) {\n if (doAutoComplete && !skipAutosuggest && input.property('selectionEnd') === val.length) {\n _selected = tryAutocomplete();\n }\n\n if (!_selected) {\n _selected = val;\n }\n }\n\n if (val.length) {\n var combo = container.selectAll('.combobox');\n if (combo.empty()) {\n show();\n }\n } else {\n hide();\n }\n\n render();\n });\n }\n\n\n // Called when the user presses up/down arrows to navigate the list\n function nav(dir) {\n if (_suggestions.length) {\n // try to determine previously selected index..\n var index = -1;\n for (var i = 0; i < _suggestions.length; i++) {\n if (_selected && _suggestions[i].value === _selected) {\n index = i;\n break;\n }\n }\n\n // pick new _selected\n index = Math.max(Math.min(index + dir, _suggestions.length - 1), 0);\n _selected = _suggestions[index].value;\n utilGetSetValue(input, _selected);\n dispatch.call('update');\n }\n\n render();\n ensureVisible();\n }\n\n\n function ensureVisible() {\n var combo = container.selectAll('.combobox');\n if (combo.empty()) return;\n\n var containerRect = container.node().getBoundingClientRect();\n var comboRect = combo.node().getBoundingClientRect();\n\n if (comboRect.bottom > containerRect.bottom) {\n var node = attachTo ? attachTo.node() : input.node();\n node.scrollIntoView({ behavior: 'instant', block: 'center' });\n render();\n }\n\n // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move\n var selected = combo.selectAll('.combobox-option.selected').node();\n if (selected) {\n selected.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' });\n }\n }\n\n\n function value() {\n var value = input.property('value');\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n\n if (start && end) {\n value = value.substring(0, start);\n }\n\n return value;\n }\n\n\n function fetchComboData(v, cb) {\n _cancelFetch = false;\n\n _fetcher.call(input, v, function(results, skipAutosuggest) {\n // already chose a value, don't overwrite or autocomplete it\n if (_cancelFetch) return;\n\n _suggestions = results;\n results.forEach(function(d) { _fetched[d.value] = d; });\n\n if (cb) {\n cb(skipAutosuggest);\n }\n });\n }\n\n\n function tryAutocomplete() {\n if (!_canAutocomplete) return;\n\n var val = _caseSensitive ? value() : value().toLowerCase();\n if (!val) return;\n\n // Don't autocomplete if user is typing a number - #4935\n if (isFinite(val)) return;\n\n const suggestionValues = [];\n _suggestions.forEach(s => {\n suggestionValues.push(s.value);\n if (s.key && s.key !== s.value) {\n suggestionValues.push(s.key);\n }\n });\n\n var bestIndex = -1;\n for (var i = 0; i < suggestionValues.length; i++) {\n var suggestion = suggestionValues[i];\n var compare = _caseSensitive ? suggestion : suggestion.toLowerCase();\n\n // if search string matches suggestion exactly, pick it..\n if (compare === val) {\n bestIndex = i;\n break;\n\n // otherwise lock in the first result that starts with the search string..\n } else if (bestIndex === -1 && compare.indexOf(val) === 0) {\n bestIndex = i;\n }\n }\n\n if (bestIndex !== -1) {\n var bestVal = suggestionValues[bestIndex];\n input.property('value', bestVal);\n input.node().setSelectionRange(val.length, bestVal.length);\n dispatch.call('update');\n return bestVal;\n }\n }\n\n\n function render() {\n if (_suggestions.length < _minItems || document.activeElement !== input.node()) {\n hide();\n return;\n }\n\n var shown = !container.selectAll('.combobox').empty();\n if (!shown) return;\n\n var combo = container.selectAll('.combobox');\n var options = combo.selectAll('.combobox-option')\n .data(_suggestions, function(d) { return d.value; });\n\n options.exit()\n .remove();\n\n // enter/update\n const enter = options.enter()\n .append('a')\n .attr('class', function(d) {\n return 'combobox-option ' + (d.klass || '') + (d.description ? ' has-description' : '');\n })\n .attr('title', function(d) { return d.title; });\n\n enter.each(function(d) {\n const sel = d3_select(this);\n const labelSpan = sel.append('span')\n .attr('class', 'combobox-option-label');\n if (d.display) {\n d.display(labelSpan);\n } else {\n labelSpan.text(d.value);\n }\n if (d.description) {\n sel.append('span')\n .attr('class', 'combobox-option-description')\n .text(d.description);\n }\n });\n\n enter\n .on('mouseenter', _mouseEnterHandler)\n .on('mouseleave', _mouseLeaveHandler)\n .merge(options)\n .classed('selected', function(d) { return d.value === _selected || d.key === _selected; })\n .on('click.combo-option', accept)\n .order();\n\n var node = attachTo ? attachTo.node() : input.node();\n var containerRect = container.node().getBoundingClientRect();\n var rect = node.getBoundingClientRect();\n\n combo\n .style('left', (rect.left + 5 - containerRect.left) + 'px')\n .style('width', (rect.width - 10) + 'px')\n .style('top', (rect.height + rect.top - containerRect.top) + 'px');\n }\n\n\n // Dispatches an 'accept' event\n // Then hides the combobox.\n function accept(d3_event, d) {\n _cancelFetch = true;\n var thiz = input.node();\n\n if (d) { // user clicked on a suggestion\n utilGetSetValue(input, d.value); // replace field contents\n utilTriggerEvent(input, 'change');\n }\n\n // clear (and keep) selection\n var val = utilGetSetValue(input);\n thiz.setSelectionRange(val.length, val.length);\n\n if (!d) {\n d = _fetched[val];\n }\n dispatch.call('accept', thiz, d, val);\n hide();\n }\n\n\n // Dispatches an 'cancel' event\n // Then hides the combobox.\n function cancel() {\n _cancelFetch = true;\n var thiz = input.node();\n\n // clear (and remove) selection, and replace field contents\n var val = utilGetSetValue(input);\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n val = val.slice(0, start) + val.slice(end);\n utilGetSetValue(input, val);\n thiz.setSelectionRange(val.length, val.length);\n\n dispatch.call('cancel', thiz);\n\n hide();\n }\n\n };\n\n\n combobox.canAutocomplete = function(val) {\n if (!arguments.length) return _canAutocomplete;\n _canAutocomplete = val;\n return combobox;\n };\n\n combobox.caseSensitive = function(val) {\n if (!arguments.length) return _caseSensitive;\n _caseSensitive = val;\n return combobox;\n };\n\n combobox.data = function(val) {\n if (!arguments.length) return _data;\n _data = val;\n return combobox;\n };\n\n combobox.fetcher = function(val) {\n if (!arguments.length) return _fetcher;\n _fetcher = val;\n return combobox;\n };\n\n combobox.minItems = function(val) {\n if (!arguments.length) return _minItems;\n _minItems = val;\n return combobox;\n };\n\n combobox.itemsMouseEnter = function(val) {\n if (!arguments.length) return _mouseEnterHandler;\n _mouseEnterHandler = val;\n return combobox;\n };\n\n combobox.itemsMouseLeave = function(val) {\n if (!arguments.length) return _mouseLeaveHandler;\n _mouseLeaveHandler = val;\n return combobox;\n };\n\n return utilRebind(combobox, dispatch, 'on');\n}\n\n\nfunction _hide(container) {\n if (_comboHideTimerID) {\n window.clearTimeout(_comboHideTimerID);\n _comboHideTimerID = undefined;\n }\n\n container.selectAll('.combobox')\n .remove();\n\n container\n .on('scroll.combo-scroll', null);\n}\n\n\nuiCombobox.off = function(input, context) {\n _hide(context.container());\n input\n .on('focus.combo-input', null)\n .on('blur.combo-input', null)\n .on('keydown.combo-input', null)\n .on('keyup.combo-input', null)\n .on('input.combo-input', null)\n .on('mousedown.combo-input', null)\n .on('mouseup.combo-input', null);\n\n\n context.container()\n .on('scroll.combo-scroll', null);\n};\n", "import { select as d3_select } from 'd3-selection';\n\n\n// toggles the visibility of ui elements, using a combination of the\n// hide class, which sets display=none, and a d3 transition for opacity.\n// this will cause blinking when called repeatedly, so check that the\n// value actually changes between calls.\n//\n// When the selection is a direct child of a

    element, the\n// parent's `open` property is used instead of the `hide` class.\nexport function uiToggle(show, callback) {\n return function(selection) {\n const parent = selection.node().parentNode;\n const isDetails = parent && parent.tagName === 'DETAILS';\n\n // ensure content is visible before animating\n if (isDetails) {\n if (show) parent.open = true;\n } else {\n selection.classed('hide', false);\n }\n\n selection\n .style('opacity', show ? 0 : 1)\n .transition()\n .style('opacity', show ? 1 : 0)\n .on('end', function() {\n d3_select(this).style('opacity', null);\n // hide content after fade-out completes\n if (isDetails) {\n if (!show) parent.open = false;\n } else {\n d3_select(this).classed('hide', !show);\n }\n if (callback) callback.apply(this);\n });\n };\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from '../core/preferences';\nimport { svgIcon } from '../svg/icon';\nimport { utilFunctor } from '../util';\nimport { utilRebind } from '../util/rebind';\nimport { uiToggle } from './toggle';\nimport { t, localizer } from '../core/localizer';\n\n\nexport function uiDisclosure(context, key, expandedDefault) {\n const dispatch = d3_dispatch('toggled');\n let _expanded;\n let _label = utilFunctor('');\n let _updatePreference = true;\n let _content = function () {};\n\n\n const disclosure = function(selection) {\n\n if (_expanded === undefined || _expanded === null) {\n // loading _expanded here allows it to be reset by calling `disclosure.expanded(null)`\n\n const preference = prefs('disclosure.' + key + '.expanded');\n _expanded = preference === null ? !!expandedDefault : (preference === 'true');\n }\n\n let details = selection.selectAll('.disclosure-wrap-' + key)\n .data([0]);\n\n // enter\n const detailsEnter = details.enter()\n .append('details')\n .attr('class', 'disclosure-wrap disclosure-wrap-' + key);\n\n const summaryEnter = detailsEnter\n .append('summary')\n .attr('class', 'hide-toggle hide-toggle-' + key)\n .call(svgIcon('', 'pre-text', 'hide-toggle-icon'));\n\n summaryEnter\n .append('span')\n .attr('class', 'hide-toggle-text');\n\n detailsEnter\n .append('div')\n .attr('class', 'disclosure-content');\n\n // update\n details = detailsEnter\n .merge(details);\n\n details\n .property('open', _expanded);\n\n const summary = details.selectAll('summary.hide-toggle');\n\n summary\n .on('click', toggle);\n\n updateSummary();\n\n const label = _label();\n const labelSelection = summary.selectAll('.hide-toggle-text');\n if (typeof label !== 'function') {\n labelSelection.text(label);\n } else {\n labelSelection.text('').call(label);\n }\n\n const contentWrap = details.selectAll('.disclosure-content');\n\n if (_expanded) {\n contentWrap\n .call(_content);\n }\n\n\n function updateSummary() {\n summary\n .classed('expanded', _expanded)\n .attr('title', t(`icons.${_expanded ? 'collapse' : 'expand'}`));\n\n summary.selectAll('.hide-toggle-icon')\n .attr('xlink:href', _expanded ? '#iD-icon-down'\n : (localizer.textDirection() === 'rtl') ? '#iD-icon-backward' : '#iD-icon-forward'\n );\n }\n\n\n function toggle(d3_event) {\n d3_event.preventDefault();\n\n _expanded = !_expanded;\n\n if (_updatePreference) {\n prefs('disclosure.' + key + '.expanded', _expanded);\n }\n\n updateSummary();\n\n contentWrap.call(uiToggle(_expanded));\n\n if (_expanded) {\n contentWrap.call(_content);\n }\n\n dispatch.call('toggled', this, _expanded);\n }\n };\n\n\n disclosure.label = function(val) {\n if (!arguments.length) return _label;\n _label = utilFunctor(val);\n return disclosure;\n };\n\n\n disclosure.expanded = function(val) {\n if (!arguments.length) return _expanded;\n _expanded = val;\n return disclosure;\n };\n\n\n disclosure.updatePreference = function(val) {\n if (!arguments.length) return _updatePreference;\n _updatePreference = val;\n return disclosure;\n };\n\n\n disclosure.content = function(val) {\n if (!arguments.length) return _content;\n _content = val;\n return disclosure;\n };\n\n\n return utilRebind(disclosure, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { uiDisclosure } from './disclosure';\nimport { utilFunctor } from '../util';\n\n// A unit of controls or info to be used in a layout, such as within a pane.\n// Can be labeled and collapsible.\nexport function uiSection(id, context) {\n\n var _classes = utilFunctor('');\n var _shouldDisplay;\n var _content;\n\n var _disclosure;\n var _label;\n var _expandedByDefault = utilFunctor(true);\n var _disclosureContent;\n var _disclosureExpanded;\n\n var _containerSelection = d3_select(null);\n\n var section = {\n id: id\n };\n\n section.classes = function(val) {\n if (!arguments.length) return _classes;\n _classes = utilFunctor(val);\n return section;\n };\n\n section.label = function(val) {\n if (!arguments.length) return _label;\n _label = utilFunctor(val);\n return section;\n };\n\n section.expandedByDefault = function(val) {\n if (!arguments.length) return _expandedByDefault;\n _expandedByDefault = utilFunctor(val);\n return section;\n };\n\n section.shouldDisplay = function(val) {\n if (!arguments.length) return _shouldDisplay;\n _shouldDisplay = utilFunctor(val);\n return section;\n };\n\n section.content = function(val) {\n if (!arguments.length) return _content;\n _content = val;\n return section;\n };\n\n section.disclosureContent = function(val) {\n if (!arguments.length) return _disclosureContent;\n _disclosureContent = val;\n return section;\n };\n\n section.disclosureExpanded = function(val) {\n if (!arguments.length) return _disclosureExpanded;\n _disclosureExpanded = val;\n return section;\n };\n\n // may be called multiple times\n section.render = function(selection) {\n\n _containerSelection = selection\n .selectAll('.section-' + id)\n .data([0]);\n\n var sectionEnter = _containerSelection\n .enter()\n .append('div')\n .attr('class', 'section section-' + id + ' ' + (_classes && _classes() || ''));\n\n _containerSelection = sectionEnter\n .merge(_containerSelection);\n\n _containerSelection\n .call(renderContent);\n };\n\n section.reRender = function() {\n _containerSelection\n .call(renderContent);\n };\n\n section.selection = function() {\n return _containerSelection;\n };\n\n section.disclosure = function() {\n return _disclosure;\n };\n\n // may be called multiple times\n function renderContent(selection) {\n if (_shouldDisplay) {\n var shouldDisplay = _shouldDisplay();\n selection.classed('hide', !shouldDisplay);\n if (!shouldDisplay) {\n selection.html('');\n return;\n }\n }\n\n if (_disclosureContent) {\n if (!_disclosure) {\n _disclosure = uiDisclosure(context, id.replace(/-/g, '_'), _expandedByDefault())\n .label(_label || '')\n /*.on('toggled', function(expanded) {\n if (expanded) { selection.node().parentNode.scrollTop += 200; }\n })*/\n .content(_disclosureContent);\n }\n if (_disclosureExpanded !== undefined) {\n _disclosure.expanded(_disclosureExpanded);\n _disclosureExpanded = undefined;\n }\n selection\n .call(_disclosure);\n\n return;\n }\n\n if (_content) {\n selection\n .call(_content);\n }\n }\n\n return section;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\n\n\n// Pass `what` object of the form:\n// {\n// key: 'string', // required\n// value: 'string' // optional\n// }\n// -or-\n// {\n// qid: 'string' // brand wikidata (e.g. 'Q37158')\n// }\n//\nexport function uiTagReference(what) {\n var wikibase = what.qid ? services.wikidata : services.osmWikibase;\n var tagReference = {};\n\n var _button = d3_select(null);\n var _body = d3_select(null);\n var _loaded;\n var _showing;\n\n\n function load() {\n if (!wikibase) return;\n\n _button\n .classed('tag-reference-loading', true);\n\n wikibase.getDocs(what, gotDocs);\n }\n\n\n function gotDocs(err, docs) {\n _body.html('');\n\n if (!docs || !docs.title) {\n _body\n .append('p')\n .attr('class', 'tag-reference-description')\n .call(t.append('inspector.no_documentation_key'));\n done();\n return;\n }\n\n if (docs.imageURL) {\n _body\n .append('img')\n .attr('class', 'tag-reference-wiki-image')\n .attr('alt', docs.title)\n .attr('src', docs.imageURL)\n .on('load', function() { done(); })\n .on('error', function() { d3_select(this).remove(); done(); });\n } else {\n done();\n }\n\n var tagReferenceDescription = _body\n .append('p')\n .attr('class', 'tag-reference-description')\n .append('span');\n if (docs.description) {\n tagReferenceDescription = tagReferenceDescription\n .attr('class', 'localized-text')\n .attr('lang', docs.descriptionLocaleCode || 'und')\n .call(docs.description);\n } else {\n tagReferenceDescription = tagReferenceDescription\n .call(t.append('inspector.no_documentation_key'));\n }\n tagReferenceDescription\n .append('a')\n .attr('class', 'tag-reference-edit')\n .attr('target', '_blank')\n .attr('title', t('inspector.edit_reference'))\n .attr('href', docs.editURL)\n .call(svgIcon('#iD-icon-edit', 'inline'));\n\n if (docs.wiki) {\n _body\n .append('a')\n .attr('class', 'tag-reference-link')\n .attr('target', '_blank')\n .attr('href', docs.wiki.url)\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append(docs.wiki.text));\n }\n\n // Add link to info about \"good changeset comments\" - #2923\n if (what.key === 'comment') {\n _body\n .append('a')\n .attr('class', 'tag-reference-comment-link')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', t('commit.about_changeset_comments_link'))\n .append('span')\n .call(t.append('commit.about_changeset_comments'));\n }\n }\n\n\n function done() {\n _loaded = true;\n\n _button\n .classed('tag-reference-loading', false);\n\n _body\n .classed('expanded', true)\n .transition()\n .duration(200)\n .style('max-height', '200px')\n .style('opacity', '1');\n\n _showing = true;\n\n _button.selectAll('svg.icon use').each(function() {\n var iconUse = d3_select(this);\n if (iconUse.attr('href') === '#iD-icon-info') {\n iconUse.attr('href', '#iD-icon-info-filled');\n }\n });\n }\n\n\n function hide() {\n _body\n .transition()\n .duration(200)\n .style('max-height', '0px')\n .style('opacity', '0')\n .on('end', function () {\n _body.classed('expanded', false);\n });\n\n _showing = false;\n\n _button.selectAll('svg.icon use').each(function() {\n var iconUse = d3_select(this);\n if (iconUse.attr('href') === '#iD-icon-info-filled') {\n iconUse.attr('href', '#iD-icon-info');\n }\n });\n\n }\n\n\n tagReference.button = function(selection, klass, iconName) {\n _button = selection.selectAll('.tag-reference-button')\n .data([0]);\n\n _button = _button.enter()\n .append('button')\n .attr('class', 'tag-reference-button ' + (klass || ''))\n .attr('title', t('icons.information'))\n .call(svgIcon('#iD-icon-' + (iconName || 'inspect')))\n .merge(_button);\n\n _button\n .on('click', function (d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n this.blur(); // avoid keeping focus on the button - #4641\n if (_showing) {\n hide();\n } else if (_loaded) {\n done();\n } else {\n load();\n }\n });\n };\n\n\n tagReference.body = function(selection) {\n var itemID = what.qid || (what.key + '-' + (what.value || ''));\n _body = selection.selectAll('.tag-reference-body')\n .data([itemID], function(d) { return d; });\n\n _body.exit()\n .remove();\n\n _body = _body.enter()\n .append('div')\n .attr('class', 'tag-reference-body')\n .style('max-height', '0')\n .style('opacity', '0')\n .merge(_body);\n\n if (_showing === false) {\n hide();\n }\n };\n\n\n tagReference.showing = function(val) {\n if (!arguments.length) return _showing;\n _showing = val;\n return tagReference;\n };\n\n\n return tagReference;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { isEmpty } from 'es-toolkit/compat';\n\nimport { services } from '../../services';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCombobox } from '../combobox';\nimport { uiSection } from '../section';\nimport { uiTagReference } from '../tag_reference';\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { utilArrayDifference, utilArrayIdentical } from '../../util/array';\nimport { utilGetSetValue, utilNoAuto, utilRebind, utilTagDiff } from '../../util';\nimport { allowUpperCaseTagValues } from '../../osm/tags';\nimport { fileFetcher } from '../../core';\n\n\nexport function uiSectionRawTagEditor(id, context) {\n\n var section = uiSection(id, context)\n .classes('raw-tag-editor')\n .label(function() {\n var count = Object.keys(_tags).filter(function(d) { return d; }).length;\n return t.append('inspector.title_count', { title: t.append('inspector.tags'), count: count });\n })\n .expandedByDefault(false)\n .disclosureContent(renderDisclosureContent);\n\n var taginfo = services.taginfo;\n var dispatch = d3_dispatch('change');\n var availableViews = [\n { id: 'list', icon: '#fas-th-list' },\n { id: 'text', icon: '#fas-i-cursor' }\n ];\n\n let _discardTags = {};\n fileFetcher.get('discarded')\n .then((d) => { _discardTags = d; })\n .catch(() => { /* ignore */ });\n\n var _tagView = (prefs('raw-tag-editor-view') || 'list'); // 'list, 'text'\n var _readOnlyTags = [];\n // the keys in the order we want them to display\n var _orderedKeys = [];\n var _pendingChange = null;\n var _state;\n var _presets;\n var _tags;\n var _entityIDs;\n var _didInteract = false;\n\n function interacted() {\n _didInteract = true;\n }\n\n function renderDisclosureContent(wrap) {\n\n // remove deleted keys\n _orderedKeys = _orderedKeys.filter(function(key) {\n return _tags[key] !== undefined;\n });\n\n // When switching to a different entity or changing the state (hover/select)\n // reorder the keys alphabetically.\n // We trigger this by emptying the `_orderedKeys` array, then it will be rebuilt here.\n // Otherwise leave their order alone - #5857, #5927\n var all = Object.keys(_tags).sort();\n var missingKeys = utilArrayDifference(all, _orderedKeys);\n for (var i in missingKeys) {\n _orderedKeys.push(missingKeys[i]);\n }\n\n // assemble row data\n var rowData = _orderedKeys.map(function(key, i) {\n return { index: i, key: key, value: _tags[key] };\n });\n\n // append blank row last\n rowData.push({ index: rowData.length, key: '', value: '' });\n\n\n // View Options\n var options = wrap.selectAll('.raw-tag-options')\n .data([0]);\n\n options.exit()\n .remove();\n\n var optionsEnter = options.enter()\n .insert('div', ':first-child')\n .attr('class', 'raw-tag-options')\n .attr('role', 'tablist');\n\n var optionEnter = optionsEnter.selectAll('.raw-tag-option')\n .data(availableViews, function(d) { return d.id; })\n .enter();\n\n optionEnter\n .append('button')\n .attr('class', function(d) {\n return 'raw-tag-option raw-tag-option-' + d.id + (_tagView === d.id ? ' selected' : '');\n })\n .attr('aria-selected', function(d) { return _tagView === d.id; })\n .attr('role', 'tab')\n .attr('title', function(d) { return t('icons.' + d.id); })\n .on('click', function(d3_event, d) {\n _tagView = d.id;\n prefs('raw-tag-editor-view', d.id);\n\n wrap.selectAll('.raw-tag-option')\n .classed('selected', function(datum) { return datum === d; })\n .attr('aria-selected', function(datum) { return datum === d; });\n\n wrap.selectAll('.tag-text')\n .classed('hide', (d.id !== 'text'))\n .each(setTextareaHeight);\n\n wrap.selectAll('.tag-list, .add-row')\n .classed('hide', (d.id !== 'list'));\n })\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(d.icon));\n });\n\n\n // View as Text\n var textData = rowsToText(rowData);\n var textarea = wrap.selectAll('.tag-text')\n .data([0]);\n\n textarea = textarea.enter()\n .append('textarea')\n .attr('class', 'tag-text' + (_tagView !== 'text' ? ' hide' : ''))\n .call(utilNoAuto)\n .attr('placeholder', t('inspector.key_value'))\n .attr('spellcheck', 'false')\n .style('direction', 'ltr')\n .merge(textarea);\n\n textarea\n .call(utilGetSetValue, textData)\n .each(setTextareaHeight)\n .on('input', setTextareaHeight)\n .on('focus', interacted)\n .on('blur', textChanged)\n .on('change', textChanged);\n\n\n // View as List\n var list = wrap.selectAll('.tag-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'tag-list' + (_tagView !== 'list' ? ' hide' : ''))\n .merge(list);\n\n\n // Tag list items\n var items = list.selectAll('.tag-row')\n .data(rowData, d => d.key);\n\n items.exit()\n .each(unbind)\n .remove();\n\n\n // Enter\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', 'tag-row')\n .classed('readonly', isReadOnly);\n\n var innerWrap = itemsEnter.append('div')\n .attr('class', 'inner-wrap');\n\n innerWrap\n .append('div')\n .attr('class', 'key-wrap')\n .append('input')\n .property('type', 'text')\n .attr('class', 'key')\n .call(utilNoAuto)\n .on('focus', interacted)\n .on('blur', keyChange)\n .on('change', keyChange);\n\n innerWrap\n .append('div')\n .attr('class', 'value-wrap')\n .append('input')\n .property('type', 'text')\n .attr('dir', 'auto')\n .attr('class', 'value')\n .call(utilNoAuto)\n .on('focus', interacted)\n .on('blur', valueChange)\n .on('change', valueChange);\n\n innerWrap\n .append('button')\n .attr('tabindex', -1)\n .attr('class', 'form-field-button remove')\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'));\n\n\n // Update\n items = items\n .merge(itemsEnter)\n .sort(function(a, b) { return a.index - b.index; });\n\n items\n .classed('add-tag', d => d.key === '')\n .each(function(d) {\n var row = d3_select(this);\n var key = row.select('input.key'); // propagate bound data\n var value = row.select('input.value'); // propagate bound data\n\n if (_entityIDs && taginfo && _state !== 'hover') {\n bindTypeahead(key, value);\n }\n\n var referenceOptions = { key: d.key };\n if (typeof d.value === 'string') {\n referenceOptions.value = d.value;\n }\n var reference = uiTagReference(referenceOptions, context);\n\n if (_state === 'hover') {\n reference.showing(false);\n }\n\n row.select('.inner-wrap') // propagate bound data\n .call(reference.button)\n .select('.tag-reference-button')\n .attr('tabindex', -1)\n .classed('disabled', d => d.key === '')\n .attr('disabled', d => d.key === '' ? 'disabled' : null);\n\n row.call(reference.body);\n\n row.select('button.remove'); // propagate bound data\n });\n\n items.selectAll('input.key')\n .attr('title', function(d) { return d.key; })\n .attr('placeholder', function(d) {\n return d.key === '' ? t('inspector.add_tag') : null;\n })\n .attr('readonly', function(d) {\n return isReadOnly(d) || null;\n })\n .call(utilGetSetValue,\n d => d.key,\n (_, newKey) => _pendingChange === null || isEmpty(_pendingChange) || _pendingChange[newKey] // if there are pending changes: skip untouched tags\n );\n\n items.selectAll('input.value')\n .attr('title', function(d) {\n return Array.isArray(d.value) ? d.value.filter(Boolean).join('\\n') : d.value;\n })\n .classed('mixed', function(d) {\n return Array.isArray(d.value);\n })\n .attr('placeholder', function(d) {\n return Array.isArray(d.value) ? t('inspector.multiple_values') : null;\n })\n .attr('readonly', function(d) {\n return isReadOnly(d) || null;\n })\n .call(utilGetSetValue, d => {\n if (_pendingChange !== null && !isEmpty(_pendingChange) && !_pendingChange[d.key]) {\n // if there are pending changes: skip untouched tags\n return null;\n }\n return Array.isArray(d.value) ? '' : d.value;\n });\n\n items.selectAll('button.remove')\n .classed('disabled', d => d.key === '')\n .on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'down', // 'click' fires too late - #5878\n (d3_event, d) => {\n if (d3_event.button !== 0) return;\n removeTag(d3_event, d);\n });\n\n }\n\n function isReadOnly(d) {\n for (var i = 0; i < _readOnlyTags.length; i++) {\n if (d.key.match(_readOnlyTags[i]) !== null) {\n return true;\n }\n }\n return false;\n }\n\n function setTextareaHeight() {\n if (_tagView !== 'text') return;\n\n var selection = d3_select(this);\n var matches = selection.node().value.match(/\\n/g);\n var lineCount = 2 + Number(matches && matches.length);\n var lineHeight = 20;\n\n selection.style('height', lineCount * lineHeight + 'px');\n }\n\n function stringify(s) {\n const stringified = JSON.stringify(s).slice(1, -1); // without leading/trailing \"\n if (stringified !== s) {\n return `\"${stringified}\"`;\n } else {\n return s;\n }\n }\n\n function unstringify(s) {\n const isQuoted = s.length > 1 && s.charAt(0) === '\"' && s.charAt(s.length - 1) === '\"';\n if (isQuoted) {\n try {\n return JSON.parse(s);\n } catch {\n return s;\n }\n } else {\n return s;\n }\n }\n\n function rowsToText(rows) {\n var str = rows\n .filter(function(row) { return row.key && row.key.trim() !== ''; })\n .map(function(row) {\n var rawVal = row.value;\n if (Array.isArray(rawVal)) rawVal = '*';\n var val = rawVal ? stringify(rawVal) : '';\n return stringify(row.key) + '=' + val;\n })\n .join('\\n');\n\n if (_state !== 'hover' && str.length) {\n return str + '\\n';\n }\n return str;\n }\n\n function textChanged() {\n var newText = this.value.trim();\n var newTags = {};\n newText.split('\\n').forEach(function(row) {\n var m = row.match(/^\\s*([^=]+)=(.*)$/);\n if (m !== null) {\n var k = context.cleanTagKey(unstringify(m[1].trim()));\n var v = context.cleanTagValue(unstringify(m[2].trim()));\n newTags[k] = v;\n }\n });\n\n var tagDiff = utilTagDiff(_tags, newTags);\n\n _pendingChange = _pendingChange || {};\n\n tagDiff.forEach(function(change) {\n if (isReadOnly({ key: change.key })) return;\n\n // skip unchanged multiselection placeholders\n if (change.newVal === '*' && Array.isArray(change.oldVal)) return;\n\n if (change.type === '-') {\n _pendingChange[change.key] = undefined;\n } else if (change.type === '+') {\n _pendingChange[change.key] = change.newVal || '';\n }\n });\n\n if (isEmpty(_pendingChange)) {\n _pendingChange = null;\n section.reRender();\n return;\n }\n\n scheduleChange();\n }\n\n function bindTypeahead(key, value) {\n if (isReadOnly(key.datum())) return;\n\n if (Array.isArray(value.datum().value)) {\n value.call(uiCombobox(context, 'tag-value')\n .minItems(1)\n .fetcher(function(value, callback) {\n var keyString = utilGetSetValue(key);\n if (!_tags[keyString]) return;\n var data = _tags[keyString].map(function(tagValue) {\n if (!tagValue) {\n return {\n value: ' ',\n title: t('inspector.empty'),\n display: selection => selection.text('')\n .classed('virtual-option', true)\n .call(t.append('inspector.empty'))\n };\n }\n return {\n value: tagValue,\n title: tagValue\n };\n });\n callback(data);\n }));\n return;\n }\n\n var geometry = context.graph().geometry(_entityIDs[0]);\n\n key.call(uiCombobox(context, 'tag-key')\n .fetcher(function(value, callback) {\n taginfo.keys({\n debounce: true,\n geometry: geometry,\n query: value\n }, function(err, data) {\n if (!err) {\n const filtered = data\n .filter(d => _tags[d.value] === undefined) // already used tag\n .filter(d => !(d.value in _discardTags)) // do not suggest discardable tags (see #9817)\n .filter(d => !/_\\d$/.test(d.value)) // tag like name_1 (see #9422)\n .filter(d => d.value.toLowerCase().includes(value.toLowerCase())); // tag does not match user input\n callback(sort(value, filtered));\n }\n });\n }));\n\n value.call(uiCombobox(context, 'tag-value')\n .fetcher(function(value, callback) {\n taginfo.values({\n debounce: true,\n key: utilGetSetValue(key),\n geometry: geometry,\n query: value\n }, function(err, data) {\n if (!err) {\n const filtered = data.filter(d => d.value.toLowerCase().includes(value.toLowerCase()));\n callback(sort(value, filtered));\n }\n });\n })\n .caseSensitive(allowUpperCaseTagValues.test(utilGetSetValue(key))));\n\n\n function sort(value, data) {\n var sameletter = [];\n var other = [];\n for (var i = 0; i < data.length; i++) {\n if (data[i].value.substring(0, value.length) === value) {\n sameletter.push(data[i]);\n } else {\n other.push(data[i]);\n }\n }\n return sameletter.concat(other);\n }\n }\n\n function unbind() {\n var row = d3_select(this);\n\n row.selectAll('input.key')\n .call(uiCombobox.off, context);\n\n row.selectAll('input.value')\n .call(uiCombobox.off, context);\n }\n\n function keyChange(d3_event, d) {\n const input = d3_select(this);\n if (input.attr('readonly')) return;\n\n var kOld = d.key;\n\n // exit if we are currently about to delete this row anyway - #6366\n if (_pendingChange && _pendingChange.hasOwnProperty(kOld) && _pendingChange[kOld] === undefined) return;\n\n var kNew = context.cleanTagKey(this.value.trim());\n\n // allow no change if the key should be readonly\n if (isReadOnly({ key: kNew })) {\n this.value = kOld;\n return;\n }\n\n if (kNew !== this.value) {\n utilGetSetValue(input, kNew);\n }\n\n if (kNew &&\n kNew !== kOld &&\n _tags[kNew] !== undefined) {\n // new key is already in use, switch focus to the existing row\n\n this.value = kOld; // reset the key\n section.selection().selectAll('.tag-list input.value')\n .each(function(d) {\n if (d.key === kNew) { // send focus to that other value combo instead\n var input = d3_select(this).node();\n input.focus();\n input.select();\n }\n });\n return;\n }\n\n\n _pendingChange = _pendingChange || {};\n\n if (kOld) {\n if (kOld === kNew) return;\n // a tag key was renamed\n _pendingChange[kNew] = _pendingChange[kOld] || { oldKey: kOld };\n _pendingChange[kOld] = undefined;\n } else {\n // a new tag was added\n let row = this.parentNode.parentNode;\n let inputVal = d3_select(row).selectAll('input.value');\n let vNew = context.cleanTagValue(utilGetSetValue(inputVal));\n _pendingChange[kNew] = vNew;\n utilGetSetValue(inputVal, vNew);\n }\n\n // update the ordered key index so this row doesn't change position\n var existingKeyIndex = _orderedKeys.indexOf(kOld);\n if (existingKeyIndex !== -1) _orderedKeys[existingKeyIndex] = kNew;\n\n d.key = kNew; // update datum to avoid exit/enter on tag update\n\n this.value = kNew;\n scheduleChange();\n }\n\n function valueChange(d3_event, d) {\n if (isReadOnly(d)) return;\n\n // exit if this is a multiselection and no value was entered\n if (Array.isArray(d.value) && !this.value) return;\n\n // exit if we are currently about to delete this row anyway - #6366\n if (_pendingChange && _pendingChange.hasOwnProperty(d.key) && _pendingChange[d.key] === undefined) return;\n\n _pendingChange = _pendingChange || {};\n\n const vNew = context.cleanTagValue(this.value);\n if (vNew !== this.value) {\n utilGetSetValue(d3_select(this), vNew);\n }\n\n _pendingChange[d.key] = vNew;\n scheduleChange();\n }\n\n function removeTag(d3_event, d) {\n if (isReadOnly(d)) return;\n\n // remove the key from the ordered key index\n _orderedKeys = _orderedKeys.filter(function(key) { return key !== d.key; });\n\n _pendingChange = _pendingChange || {};\n _pendingChange[d.key] = undefined;\n scheduleChange();\n }\n\n function scheduleChange() {\n if (!_pendingChange) return;\n\n for (const key in _pendingChange) {\n _tags[key] = _pendingChange[key];\n }\n dispatch.call('change', this, _entityIDs, _pendingChange);\n _pendingChange = null;\n }\n\n\n section.state = function(val) {\n if (!arguments.length) return _state;\n if (_state !== val) {\n _orderedKeys = [];\n _state = val;\n }\n return section;\n };\n\n\n section.presets = function(val) {\n if (!arguments.length) return _presets;\n _presets = val;\n if (_presets && _presets.length && _presets[0].isFallback()) {\n section.disclosureExpanded(true);\n\n // don't collapse the disclosure if the mapper used the raw tag editor - #1881\n } else if (!_didInteract) {\n section.disclosureExpanded(null);\n }\n return section;\n };\n\n\n section.tags = function(val) {\n if (!arguments.length) return _tags;\n _tags = val;\n return section;\n };\n\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n if (!_entityIDs || !val || !utilArrayIdentical(_entityIDs, val)) {\n _entityIDs = val;\n _orderedKeys = [];\n }\n return section;\n };\n\n\n // pass an array of regular expressions to test against the tag key\n section.readOnlyTags = function(val) {\n if (!arguments.length) return _readOnlyTags;\n _readOnlyTags = val;\n return section;\n };\n\n\n return utilRebind(section, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\nimport { stringifyProperties } from '../util/object';\nimport { uiDataHeader } from './data_header';\nimport { uiSectionRawTagEditor } from './sections/raw_tag_editor';\n\n\nexport function uiDataEditor(context) {\n var dataHeader = uiDataHeader();\n var rawTagEditor = uiSectionRawTagEditor('custom-data-tag-editor', context)\n .expandedByDefault(true)\n .readOnlyTags([/.*/]);\n var _datum;\n\n\n function dataEditor(selection) {\n\n var header = selection.selectAll('.header')\n .data([0]);\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function() {\n context.enter(modeBrowse(context));\n })\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('map_data.title'));\n\n\n var body = selection.selectAll('.body')\n .data([0]);\n\n body = body.enter()\n .append('div')\n .attr('class', 'body')\n .merge(body);\n\n var editor = body.selectAll('.data-editor')\n .data([0]);\n\n // enter/update\n editor.enter()\n .append('div')\n .attr('class', 'modal-section data-editor')\n .merge(editor)\n .call(dataHeader.datum(_datum));\n\n var rte = body.selectAll('.raw-tag-editor')\n .data([0]);\n\n // enter/update\n rte.enter()\n .append('div')\n .attr('class', 'raw-tag-editor data-editor')\n .merge(rte)\n .call(rawTagEditor\n .tags(stringifyProperties(_datum?.properties || {}))\n .state('hover')\n .render\n )\n .selectAll('textarea.tag-text')\n .attr('readonly', true)\n .classed('readonly', true);\n }\n\n\n dataEditor.datum = function(val) {\n if (!arguments.length) return _datum;\n _datum = val;\n return this;\n };\n\n\n return dataEditor;\n}\n", "import { geoBounds as d3_geoBounds } from 'd3-geo';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { behaviorBreathe } from '../behavior/breathe';\nimport { behaviorHover } from '../behavior/hover';\nimport { behaviorLasso } from '../behavior/lasso';\nimport { behaviorSelect } from '../behavior/select';\n\nimport { t } from '../core/localizer';\n\nimport { geoExtent } from '../geo';\nimport { modeBrowse } from './browse';\nimport { modeDragNode } from './drag_node';\nimport { modeDragNote } from './drag_note';\nimport { uiDataEditor } from '../ui/data_editor';\nimport { utilKeybinding } from '../util';\n\n\nexport function modeSelectData(context, selectedDatum) {\n var mode = {\n id: 'select-data',\n button: 'browse'\n };\n\n var keybinding = utilKeybinding('select-data');\n var dataEditor = uiDataEditor(context);\n\n var behaviors = [\n behaviorBreathe(context),\n behaviorHover(context),\n behaviorSelect(context),\n behaviorLasso(context),\n modeDragNode(context).behavior,\n modeDragNote(context).behavior\n ];\n\n\n // class the data as selected, or return to browse mode if the data is gone\n function selectData(d3_event, drawn) {\n var selection = context.surface().selectAll('.layer-mapdata .data' + selectedDatum.__featurehash__);\n\n if (selection.empty()) {\n // Return to browse mode if selected DOM elements have\n // disappeared because the user moved them out of view..\n var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;\n if (drawn && source && (source.type === 'pointermove' || source.type === 'mousemove' || source.type === 'touchmove')) {\n context.enter(modeBrowse(context));\n }\n } else {\n selection.classed('selected', true);\n }\n }\n\n\n function esc() {\n if (context.container().select('.combobox').size()) return;\n context.enter(modeBrowse(context));\n }\n\n\n mode.zoomToSelected = function() {\n var extent = geoExtent(d3_geoBounds(selectedDatum));\n context.map().centerZoomEase(extent.center(), context.map().trimmedExtentZoom(extent));\n };\n\n\n mode.enter = function() {\n behaviors.forEach(context.install);\n\n keybinding\n .on(t('inspector.zoom_to.key'), mode.zoomToSelected)\n .on('\u238B', esc, true);\n\n d3_select(document)\n .call(keybinding);\n\n selectData();\n\n var sidebar = context.ui().sidebar;\n sidebar.show(dataEditor.datum(selectedDatum));\n\n // expand the sidebar, avoid obscuring the data if needed\n var extent = geoExtent(d3_geoBounds(selectedDatum));\n sidebar.expand(sidebar.intersects(extent));\n\n context.map()\n .on('drawn.select-data', selectData);\n };\n\n\n mode.exit = function() {\n behaviors.forEach(context.uninstall);\n\n d3_select(document)\n .call(keybinding.unbind);\n\n context.surface()\n .selectAll('.layer-mapdata .selected')\n .classed('selected hover', false);\n\n context.map()\n .on('drawn.select-data', null);\n\n context.ui().sidebar\n .hide();\n };\n\n\n return mode;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { modeSelect } from '../modes/select';\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { utilDisplayName, utilHighlightEntities } from '../util';\n\n\nexport function uiOsmoseDetails(context) {\n let _qaItem;\n\n function issueString(d, type) {\n if (!d) return '';\n\n // Issue strings are cached from Osmose API\n const s = services.osmose.getStrings(d.itemType);\n return (type in s) ? s[type] : '';\n }\n\n\n function osmoseDetails(selection) {\n const details = selection.selectAll('.error-details')\n .data(\n _qaItem ? [_qaItem] : [],\n d => `${d.id}-${d.status || 0}`\n );\n\n details.exit()\n .remove();\n\n const detailsEnter = details.enter()\n .append('div')\n .attr('class', 'error-details qa-details-container');\n\n\n // Description\n if (issueString(_qaItem, 'detail')) {\n const div = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n div\n .append('h4')\n .call(t.append('QA.keepRight.detail_description'));\n\n div\n .append('p')\n .attr('class', 'qa-details-description-text')\n .html(d => issueString(d, 'detail'))\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Elements (populated later as data is requested)\n const detailsDiv = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n const elemsDiv = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n // Suggested Fix (mustn't exist for every issue type)\n if (issueString(_qaItem, 'fix')) {\n const div = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n div\n .append('h4')\n .call(t.append('QA.osmose.fix_title'));\n\n div\n .append('p')\n .html(d => issueString(d, 'fix'))\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Common Pitfalls (mustn't exist for every issue type)\n if (issueString(_qaItem, 'trap')) {\n const div = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n div\n .append('h4')\n .call(t.append('QA.osmose.trap_title'));\n\n div\n .append('p')\n .html(d => issueString(d, 'trap'))\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Save current item to check if UI changed by time request resolves\n const thisItem = _qaItem;\n services.osmose.loadIssueDetail(_qaItem)\n .then(d => {\n // No details to add if there are no associated issue elements\n if (!d.elems || d.elems.length === 0) return;\n\n // Do nothing if UI has moved on by the time this resolves\n if (\n context.selectedErrorID() !== thisItem.id\n && context.container().selectAll(`.qaItem.osmose.hover.itemId-${thisItem.id}`).empty()\n ) return;\n\n // Things like keys and values are dynamically added to a subtitle string\n if (d.detail) {\n detailsDiv\n .append('h4')\n .call(t.append('QA.osmose.detail_title'));\n\n detailsDiv\n .append('p')\n .html(d => d.detail)\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Create list of linked issue elements\n elemsDiv\n .append('h4')\n .call(t.append('QA.osmose.elems_title'));\n\n elemsDiv\n .append('ul').selectAll('li')\n .data(d.elems)\n .enter()\n .append('li')\n .append('a')\n .attr('href', '#')\n .attr('class', 'error_entity_link')\n .text(d => d)\n .each(function() {\n const link = d3_select(this);\n const entityID = this.textContent;\n const entity = context.hasEntity(entityID);\n\n // Add click handler\n link\n .on('mouseenter', () => {\n utilHighlightEntities([entityID], true, context);\n })\n .on('mouseleave', () => {\n utilHighlightEntities([entityID], false, context);\n })\n .on('click', (d3_event) => {\n d3_event.preventDefault();\n\n utilHighlightEntities([entityID], false, context);\n\n const osmlayer = context.layers().layer('osm');\n if (!osmlayer.enabled()) {\n osmlayer.enabled(true);\n }\n\n context.map().centerZoom(d.loc, 20);\n\n if (entity) {\n context.enter(modeSelect(context, [entityID]));\n } else {\n context.loadEntity(entityID, (err, result) => {\n if (err) return;\n const entity = result.data.find(e => e.id === entityID);\n if (entity) context.enter(modeSelect(context, [entityID]));\n });\n }\n });\n\n // Replace with friendly name if possible\n // (The entity may not yet be loaded into the graph)\n if (entity) {\n let name = utilDisplayName(entity); // try to use common name\n\n if (!name) {\n const preset = presetManager.match(entity, context.graph());\n name = preset && !preset.isFallback() && preset.name(); // fallback to preset name\n }\n\n if (name) {\n this.innerText = name;\n }\n }\n });\n\n // Don't hide entities related to this issue - #5880\n context.features().forceVisible(d.elems);\n context.map().pan([0,0]); // trigger a redraw\n })\n .catch(err => {\n console.log(err); // eslint-disable-line no-console\n });\n }\n\n\n osmoseDetails.issue = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return osmoseDetails;\n };\n\n\n return osmoseDetails;\n}\n", "import { services } from '../services';\nimport { t } from '../core/localizer';\n\n\nexport function uiOsmoseHeader() {\n let _qaItem;\n\n function issueTitle(d) {\n const unknown = t('inspector.unknown');\n\n if (!d) return unknown;\n\n // Issue titles supplied by Osmose\n const s = services.osmose.getStrings(d.itemType);\n return ('title' in s) ? s.title : unknown;\n }\n\n function osmoseHeader(selection) {\n const header = selection.selectAll('.qa-header')\n .data(\n (_qaItem ? [_qaItem] : []),\n d => `${d.id}-${d.status || 0}`\n );\n\n header.exit()\n .remove();\n\n const headerEnter = header.enter()\n .append('div')\n .attr('class', 'qa-header');\n\n const svgEnter = headerEnter\n .append('div')\n .attr('class', 'qa-header-icon')\n .classed('new', d => d.id < 0)\n .append('svg')\n .attr('width', '20px')\n .attr('height', '30px')\n .attr('viewbox', '0 0 20 30')\n .attr('class', d => `preset-icon-28 qaItem ${d.service} itemId-${d.id} itemType-${d.itemType}`);\n\n svgEnter\n .append('polygon')\n .attr('fill', d => services.osmose.getColor(d.item))\n .attr('class', 'qaItem-fill')\n .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');\n\n svgEnter\n .append('use')\n .attr('class', 'icon-annotation')\n .attr('width', '12px')\n .attr('height', '12px')\n .attr('transform', 'translate(4, 5.5)')\n .attr('xlink:href', d => d.icon ? '#' + d.icon : '');\n\n headerEnter\n .append('div')\n .attr('class', 'qa-header-label')\n .text(issueTitle);\n }\n\n osmoseHeader.issue = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return osmoseHeader;\n };\n\n return osmoseHeader;\n}\n", "import { t } from '../core/localizer';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\nimport { QAItem } from '../osm';\n\nexport function uiViewOnOsmose() {\n let _qaItem;\n\n function viewOnOsmose(selection) {\n let url;\n if (services.osmose && (_qaItem instanceof QAItem)) {\n url = services.osmose.itemURL(_qaItem);\n }\n\n const link = selection.selectAll('.view-on-osmose')\n .data(url ? [url] : []);\n\n // exit\n link.exit()\n .remove();\n\n // enter\n const linkEnter = link.enter()\n .append('a')\n .attr('class', 'view-on-osmose')\n .attr('target', '_blank')\n .attr('rel', 'noopener') // security measure\n .attr('href', d => d)\n .call(svgIcon('#iD-icon-out-link', 'inline'));\n\n linkEnter\n .append('span')\n .call(t.append('inspector.view_on_osmose'));\n }\n\n viewOnOsmose.what = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return viewOnOsmose;\n };\n\n return viewOnOsmose;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\n\nimport { uiOsmoseDetails } from './osmose_details';\nimport { uiOsmoseHeader } from './osmose_header';\nimport { uiViewOnOsmose } from './view_on_osmose';\n\nimport { utilRebind } from '../util';\n\nexport function uiOsmoseEditor(context) {\n const dispatch = d3_dispatch('change');\n const qaDetails = uiOsmoseDetails(context);\n const qaHeader = uiOsmoseHeader(context);\n\n let _qaItem;\n\n function osmoseEditor(selection) {\n\n const header = selection.selectAll('.header')\n .data([0]);\n\n const headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', () => context.enter(modeBrowse(context)))\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('QA.osmose.title'));\n\n let body = selection.selectAll('.body')\n .data([0]);\n\n body = body.enter()\n .append('div')\n .attr('class', 'body')\n .merge(body);\n\n let editor = body.selectAll('.qa-editor')\n .data([0]);\n\n editor.enter()\n .append('div')\n .attr('class', 'modal-section qa-editor')\n .merge(editor)\n .call(qaHeader.issue(_qaItem))\n .call(qaDetails.issue(_qaItem))\n .call(osmoseSaveSection);\n\n const footer = selection.selectAll('.footer')\n .data([0]);\n\n footer.enter()\n .append('div')\n .attr('class', 'footer')\n .merge(footer)\n .call(uiViewOnOsmose(context).what(_qaItem));\n }\n\n function osmoseSaveSection(selection) {\n const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());\n const isShown = (_qaItem && isSelected);\n let saveSection = selection.selectAll('.qa-save')\n .data(\n (isShown ? [_qaItem] : []),\n d => `${d.id}-${d.status || 0}`\n );\n\n // exit\n saveSection.exit()\n .remove();\n\n // enter\n const saveSectionEnter = saveSection.enter()\n .append('div')\n .attr('class', 'qa-save save-section cf');\n\n // update\n saveSectionEnter\n .merge(saveSection)\n .call(qaSaveButtons);\n }\n\n function qaSaveButtons(selection) {\n const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());\n let buttonSection = selection.selectAll('.buttons')\n .data((isSelected ? [_qaItem] : []), d => d.status + d.id);\n\n // exit\n buttonSection.exit()\n .remove();\n\n // enter\n const buttonEnter = buttonSection.enter()\n .append('div')\n .attr('class', 'buttons');\n\n buttonEnter\n .append('button')\n .attr('class', 'button close-button action');\n\n buttonEnter\n .append('button')\n .attr('class', 'button ignore-button action');\n\n // update\n buttonSection = buttonSection\n .merge(buttonEnter);\n\n buttonSection.select('.close-button')\n .call(t.append('QA.keepRight.close'))\n .on('click.close', function(d3_event, d) {\n this.blur(); // avoid keeping focus on the button - #4641\n const qaService = services.osmose;\n if (qaService) {\n d.newStatus = 'done';\n qaService.postUpdate(d, (err, item) => dispatch.call('change', item));\n }\n });\n\n buttonSection.select('.ignore-button')\n .call(t.append('QA.keepRight.ignore'))\n .on('click.ignore', function(d3_event, d) {\n this.blur(); // avoid keeping focus on the button - #4641\n const qaService = services.osmose;\n if (qaService) {\n d.newStatus = 'false';\n qaService.postUpdate(d, (err, item) => dispatch.call('change', item));\n }\n });\n }\n\n // NOTE: Don't change method name until UI v3 is merged\n osmoseEditor.error = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return osmoseEditor;\n };\n\n return utilRebind(osmoseEditor, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { behaviorBreathe } from '../behavior/breathe';\nimport { behaviorHover } from '../behavior/hover';\nimport { behaviorLasso } from '../behavior/lasso';\nimport { behaviorSelect } from '../behavior/select';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { modeBrowse } from './browse';\nimport { modeDragNode } from './drag_node';\nimport { modeDragNote } from './drag_note';\nimport { uiOsmoseEditor } from '../ui/osmose_editor';\nimport { utilKeybinding } from '../util';\n\n// NOTE: Don't change name of this until UI v3 is merged\nexport function modeSelectError(context, selectedErrorID, selectedErrorService) {\n var mode = {\n id: 'select-error',\n button: 'browse'\n };\n\n var keybinding = utilKeybinding('select-error');\n\n var errorService = services[selectedErrorService];\n var errorEditor;\n switch (selectedErrorService) {\n case 'osmose':\n errorEditor = uiOsmoseEditor(context)\n .on('change', function() {\n context.map().pan([0,0]); // trigger a redraw\n var error = checkSelectedID();\n if (!error) return;\n context.ui().sidebar\n .show(errorEditor.error(error));\n });\n break;\n }\n\n\n var behaviors = [\n behaviorBreathe(context),\n behaviorHover(context),\n behaviorSelect(context),\n behaviorLasso(context),\n modeDragNode(context).behavior,\n modeDragNote(context).behavior\n ];\n\n\n function checkSelectedID() {\n if (!errorService) return;\n var error = errorService.getError(selectedErrorID);\n if (!error) {\n context.enter(modeBrowse(context));\n }\n return error;\n }\n\n\n mode.zoomToSelected = function() {\n if (!errorService) return;\n var error = errorService.getError(selectedErrorID);\n if (error) {\n context.map().centerZoomEase(error.loc, 20);\n }\n };\n\n\n mode.enter = function() {\n var error = checkSelectedID();\n if (!error) return;\n\n behaviors.forEach(context.install);\n keybinding\n .on(t('inspector.zoom_to.key'), mode.zoomToSelected)\n .on('\u238B', esc, true);\n\n d3_select(document)\n .call(keybinding);\n\n selectError();\n\n var sidebar = context.ui().sidebar;\n sidebar.show(errorEditor.error(error));\n\n context.map()\n .on('drawn.select-error', selectError);\n\n\n // class the error as selected, or return to browse mode if the error is gone\n function selectError(d3_event, drawn) {\n if (!checkSelectedID()) return;\n\n var selection = context.surface()\n .selectAll('.itemId-' + selectedErrorID + '.' + selectedErrorService);\n\n if (selection.empty()) {\n // Return to browse mode if selected DOM elements have\n // disappeared because the user moved them out of view..\n var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;\n if (drawn && source && (source.type === 'pointermove' || source.type === 'mousemove' || source.type === 'touchmove')) {\n context.enter(modeBrowse(context));\n }\n\n } else {\n selection\n .classed('selected', true);\n\n context.selectedErrorID(selectedErrorID);\n }\n }\n\n function esc() {\n if (context.container().select('.combobox').size()) return;\n context.enter(modeBrowse(context));\n }\n };\n\n\n mode.exit = function() {\n behaviors.forEach(context.uninstall);\n\n d3_select(document)\n .call(keybinding.unbind);\n\n context.surface()\n .selectAll('.qaItem.selected')\n .classed('selected hover', false);\n\n context.map()\n .on('drawn.select-error', null);\n\n context.ui().sidebar\n .hide();\n\n context.selectedErrorID(null);\n context.features().forceVisible([]);\n };\n\n\n return mode;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { geoVecLength } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect } from '../modes/select';\nimport { modeSelectData } from '../modes/select_data';\nimport { modeSelectNote } from '../modes/select_note';\nimport { modeSelectError } from '../modes/select_error';\nimport { osmEntity, osmNote, QAItem } from '../osm';\nimport { utilFastMouse } from '../util/util';\n\n\nexport function behaviorSelect(context) {\n var _tolerancePx = 4; // see also behaviorDrag\n var _lastMouseEvent = null;\n var _showMenu = false;\n var _downPointers = {};\n var _longPressTimeout = null;\n var _lastInteractionType = null;\n // the id of the down pointer that's enabling multiselection while down\n var _multiselectionPointerId = null;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function keydown(d3_event) {\n\n if (d3_event.keyCode === 32) {\n // don't react to spacebar events during text input\n var activeNode = document.activeElement;\n if (activeNode && new Set(['INPUT', 'TEXTAREA']).has(activeNode.nodeName)) return;\n }\n\n if (d3_event.keyCode === 93 || // context menu key\n d3_event.keyCode === 32) { // spacebar\n d3_event.preventDefault();\n }\n\n if (d3_event.repeat) return; // ignore repeated events for held keys\n\n // if any key is pressed the user is probably doing something other than long-pressing\n cancelLongPress();\n\n if (d3_event.shiftKey) {\n context.surface()\n .classed('behavior-multiselect', true);\n }\n\n if (d3_event.keyCode === 32) { // spacebar\n if (!_downPointers.spacebar && _lastMouseEvent) {\n cancelLongPress();\n _longPressTimeout = window.setTimeout(didLongPress, 500, 'spacebar', 'spacebar');\n\n _downPointers.spacebar = {\n firstEvent: _lastMouseEvent,\n lastEvent: _lastMouseEvent\n };\n }\n }\n }\n\n\n function keyup(d3_event) {\n cancelLongPress();\n\n if (!d3_event.shiftKey) {\n context.surface()\n .classed('behavior-multiselect', false);\n }\n\n if (d3_event.keyCode === 93) { // context menu key\n d3_event.preventDefault();\n _lastInteractionType = 'menukey';\n contextmenu(d3_event);\n } else if (d3_event.keyCode === 32) { // spacebar\n var pointer = _downPointers.spacebar;\n if (pointer) {\n delete _downPointers.spacebar;\n\n if (pointer.done) return;\n\n d3_event.preventDefault();\n _lastInteractionType = 'spacebar';\n click(pointer.firstEvent, pointer.lastEvent, 'spacebar');\n }\n }\n }\n\n\n function pointerdown(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n\n cancelLongPress();\n\n if (d3_event.buttons && d3_event.buttons !== 1) return;\n\n context.ui().closeEditMenu();\n\n if (d3_event.pointerType !== 'mouse') {\n _longPressTimeout = window.setTimeout(didLongPress, 500, id, 'longdown-' + (d3_event.pointerType || 'mouse'));\n }\n\n _downPointers[id] = {\n firstEvent: d3_event,\n lastEvent: d3_event\n };\n }\n\n\n function didLongPress(id, interactionType) {\n var pointer = _downPointers[id];\n if (!pointer) return;\n\n for (var i in _downPointers) {\n // don't allow this or any currently down pointer to trigger another click\n _downPointers[i].done = true;\n }\n\n // treat long presses like right-clicks\n _longPressTimeout = null;\n _lastInteractionType = interactionType;\n _showMenu = true;\n\n click(pointer.firstEvent, pointer.lastEvent, id);\n }\n\n\n function pointermove(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n if (_downPointers[id]) {\n _downPointers[id].lastEvent = d3_event;\n }\n if (!d3_event.pointerType || d3_event.pointerType === 'mouse') {\n _lastMouseEvent = d3_event;\n if (_downPointers.spacebar) {\n _downPointers.spacebar.lastEvent = d3_event;\n }\n }\n }\n\n\n function pointerup(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n var pointer = _downPointers[id];\n if (!pointer) return;\n\n delete _downPointers[id];\n\n if (_multiselectionPointerId === id) {\n _multiselectionPointerId = null;\n }\n\n if (pointer.done) return;\n\n click(pointer.firstEvent, d3_event, id);\n }\n\n\n function pointercancel(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n if (!_downPointers[id]) return;\n\n delete _downPointers[id];\n\n if (_multiselectionPointerId === id) {\n _multiselectionPointerId = null;\n }\n }\n\n\n function contextmenu(d3_event) {\n d3_event.preventDefault();\n\n if (!+d3_event.clientX && !+d3_event.clientY) {\n if (_lastMouseEvent) {\n d3_event = _lastMouseEvent;\n } else {\n return;\n }\n } else {\n _lastMouseEvent = d3_event;\n if (d3_event.pointerType === 'touch' || d3_event.pointerType === 'pen' ||\n d3_event.mozInputSource && ( // firefox doesn't give a pointerType on contextmenu events\n d3_event.mozInputSource === MouseEvent.MOZ_SOURCE_TOUCH ||\n d3_event.mozInputSource === MouseEvent.MOZ_SOURCE_PEN)) {\n _lastInteractionType = 'touch';\n } else {\n _lastInteractionType = 'rightclick';\n }\n }\n\n _showMenu = true;\n click(d3_event, d3_event);\n }\n\n\n function click(firstEvent, lastEvent, pointerId) {\n cancelLongPress();\n\n var mapNode = context.container().select('.main-map').node();\n\n // Use the `main-map` coordinate system since the surface and supersurface\n // are transformed when drag-panning.\n var pointGetter = utilFastMouse(mapNode);\n var p1 = pointGetter(firstEvent);\n var p2 = pointGetter(lastEvent);\n var dist = geoVecLength(p1, p2);\n\n if (dist > _tolerancePx ||\n !mapContains(lastEvent)) {\n\n resetProperties();\n return;\n }\n\n var targetDatum = lastEvent.target.__data__;\n if (targetDatum === 0 && lastEvent.target.parentNode.__data__) {\n // some targets (like markers of the street level photo\n // layers) have the data bound to the parent node\n targetDatum = lastEvent.target.parentNode.__data__;\n }\n\n var multiselectEntityId;\n\n if (!_multiselectionPointerId) {\n // If a different pointer than the one triggering this click is down on a\n // feature, treat this and all future clicks as multiselection until that\n // pointer is raised.\n var selectPointerInfo = pointerDownOnSelection(pointerId);\n if (selectPointerInfo) {\n _multiselectionPointerId = selectPointerInfo.pointerId;\n // if the other feature isn't selected yet, make sure we select it\n multiselectEntityId = !selectPointerInfo.selected && selectPointerInfo.entityId;\n _downPointers[selectPointerInfo.pointerId].done = true;\n }\n }\n\n // support multiselect if data is already selected\n var isMultiselect = context.mode().id === 'select' && (\n // and shift key is down\n (lastEvent && lastEvent.shiftKey) ||\n // or we're lasso-selecting\n context.surface().select('.lasso').node() ||\n // or a pointer is down over a selected feature\n (_multiselectionPointerId && !multiselectEntityId)\n );\n\n processClick(targetDatum, isMultiselect, p2, multiselectEntityId);\n\n function mapContains(event) {\n var rect = mapNode.getBoundingClientRect();\n return event.clientX >= rect.left &&\n event.clientX <= rect.right &&\n event.clientY >= rect.top &&\n event.clientY <= rect.bottom;\n }\n\n function pointerDownOnSelection(skipPointerId) {\n var mode = context.mode();\n var selectedIDs = mode.id === 'select' ? mode.selectedIDs() : [];\n for (var pointerId in _downPointers) {\n if (pointerId === 'spacebar' || pointerId === skipPointerId) continue;\n\n var pointerInfo = _downPointers[pointerId];\n\n var p1 = pointGetter(pointerInfo.firstEvent);\n var p2 = pointGetter(pointerInfo.lastEvent);\n if (geoVecLength(p1, p2) > _tolerancePx) continue;\n\n var datum = pointerInfo.firstEvent.target.__data__;\n var entity = (datum && datum.properties && datum.properties.entity) || datum;\n if (context.graph().hasEntity(entity.id)) {\n return {\n pointerId: pointerId,\n entityId: entity.id,\n selected: selectedIDs.indexOf(entity.id) !== -1\n };\n }\n }\n return null;\n }\n }\n\n\n function processClick(datum, isMultiselect, point, alsoSelectId) {\n var mode = context.mode();\n var showMenu = _showMenu;\n var interactionType = _lastInteractionType;\n\n var entity = datum && datum.properties && datum.properties.entity;\n if (entity) datum = entity;\n\n if (datum && datum.type === 'midpoint') {\n // treat targeting midpoints as if targeting the parent way\n datum = datum.parents[0];\n }\n\n var newMode;\n\n if (datum instanceof osmEntity) {\n // targeting an entity\n var selectedIDs = context.selectedIDs();\n context.selectedNoteID(null);\n context.selectedErrorID(null);\n\n if (!isMultiselect) {\n // don't change the selection if we're toggling the menu atop a multiselection\n if (!showMenu ||\n selectedIDs.length <= 1 ||\n selectedIDs.indexOf(datum.id) === -1) {\n\n if (alsoSelectId === datum.id) alsoSelectId = null;\n\n selectedIDs = (alsoSelectId ? [alsoSelectId] : []).concat([datum.id]);\n // always enter modeSelect even if the entity is already\n // selected since listeners may expect `context.enter` events,\n // e.g. in the walkthrough\n newMode = mode.id === 'select' ? mode.selectedIDs(selectedIDs) : modeSelect(context, selectedIDs).selectBehavior(behavior);\n context.enter(newMode);\n }\n\n } else {\n if (selectedIDs.indexOf(datum.id) !== -1) {\n // clicked entity is already in the selectedIDs list..\n if (!showMenu) {\n // deselect clicked entity, then reenter select mode or return to browse mode..\n selectedIDs = selectedIDs.filter(function(id) { return id !== datum.id; });\n newMode = selectedIDs.length ? mode.selectedIDs(selectedIDs) : modeBrowse(context).selectBehavior(behavior);\n context.enter(newMode);\n }\n } else {\n // clicked entity is not in the selected list, add it..\n selectedIDs = selectedIDs.concat([datum.id]);\n newMode = mode.selectedIDs(selectedIDs);\n context.enter(newMode);\n }\n }\n\n } else if (datum && datum.__featurehash__ && !isMultiselect) {\n // targeting custom data\n context\n .selectedNoteID(null)\n .enter(modeSelectData(context, datum));\n\n } else if (datum instanceof osmNote && !isMultiselect) {\n // targeting a note\n context\n .selectedNoteID(datum.id)\n .enter(modeSelectNote(context, datum.id));\n\n } else if (datum instanceof QAItem && !isMultiselect) {\n // targeting an external QA issue\n context\n .selectedErrorID(datum.id)\n .enter(modeSelectError(context, datum.id, datum.service));\n\n } else if (datum.service === 'photo') {\n // street level photo was selected:\n // don't change mode and selection\n } else {\n // targeting nothing\n context.selectedNoteID(null);\n context.selectedErrorID(null);\n if (!isMultiselect && mode.id !== 'browse') {\n context.enter(modeBrowse(context));\n }\n }\n\n context.ui().closeEditMenu();\n\n // always request to show the edit menu in case the mode needs it\n if (showMenu) context.ui().showEditMenu(point, interactionType);\n\n resetProperties();\n }\n\n\n function cancelLongPress() {\n if (_longPressTimeout) window.clearTimeout(_longPressTimeout);\n _longPressTimeout = null;\n }\n\n\n function resetProperties() {\n cancelLongPress();\n _showMenu = false;\n _lastInteractionType = null;\n // don't reset _lastMouseEvent since it might still be useful\n }\n\n\n function behavior(selection) {\n resetProperties();\n _lastMouseEvent = context.map().lastPointerEvent();\n\n d3_select(window)\n .on('keydown.select', keydown)\n .on('keyup.select', keyup)\n .on(_pointerPrefix + 'move.select', pointermove, true)\n .on(_pointerPrefix + 'up.select', pointerup, true)\n .on('pointercancel.select', pointercancel, true)\n .on('contextmenu.select-window', function(d3_event) {\n // Edge and IE really like to show the contextmenu on the\n // menubar when user presses a keyboard menu button\n // even after we've already preventdefaulted the key event.\n var e = d3_event;\n if (+e.clientX === 0 && +e.clientY === 0) {\n d3_event.preventDefault();\n }\n });\n\n selection\n .on(_pointerPrefix + 'down.select', pointerdown)\n .on('contextmenu.select', contextmenu);\n\n /*if (d3_event && d3_event.shiftKey) {\n context.surface()\n .classed('behavior-multiselect', true);\n }*/\n }\n\n\n behavior.off = function(selection) {\n cancelLongPress();\n\n d3_select(window)\n .on('keydown.select', null)\n .on('keyup.select', null)\n .on('contextmenu.select-window', null)\n .on(_pointerPrefix + 'move.select', null, true)\n .on(_pointerPrefix + 'up.select', null, true)\n .on('pointercancel.select', null, true);\n\n selection\n .on(_pointerPrefix + 'down.select', null)\n .on('contextmenu.select', null);\n\n context.surface()\n .classed('behavior-multiselect', false);\n };\n\n\n return behavior;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../core/preferences';\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { services } from '../services';\nimport { localeDateString } from '../util/date';\n\n\nexport function uiNoteComments() {\n var _note;\n\n\n function noteComments(selection) {\n if (_note.isNew()) return; // don't draw .comments-container\n\n var comments = selection.selectAll('.comments-container')\n .data([0]);\n\n comments = comments.enter()\n .append('div')\n .attr('class', 'comments-container')\n .merge(comments);\n\n var commentEnter = comments.selectAll('.comment')\n .data(_note.comments)\n .enter()\n .append('div')\n .attr('class', 'comment');\n\n commentEnter\n .append('div')\n .attr('class', function(d) { return 'comment-avatar user-' + d.uid; })\n .call(svgIcon('#iD-icon-avatar', 'comment-avatar-icon'));\n\n var mainEnter = commentEnter\n .append('div')\n .attr('class', 'comment-main');\n\n var metadataEnter = mainEnter\n .append('div')\n .attr('class', 'comment-metadata');\n\n metadataEnter\n .append('div')\n .attr('class', 'comment-author')\n .each(function(d) {\n var selection = d3_select(this);\n var osm = services.osm;\n if (osm && d.user) {\n selection = selection\n .append('a')\n .attr('class', 'comment-author-link')\n .attr('href', osm.userURL(d.user))\n .attr('target', '_blank');\n }\n if (d.user) {\n selection.text(d.user);\n } else {\n selection.call(t.append('note.anonymous'));\n }\n });\n\n metadataEnter\n .append('div')\n .attr('class', 'comment-date')\n .each(function(d) {\n d3_select(this).call(\n t.addOrUpdate('note.status.' + d.action, {\n when: localeDateString(d.date.replace(' UTC', 'Z').replace(' ', 'T')),\n }));\n });\n\n mainEnter\n .append('div')\n .attr('class', 'comment-text')\n .html(function(d) { return d.html; })\n .selectAll('a')\n .attr('rel', 'noopener nofollow')\n .attr('target', '_blank');\n\n comments\n .call(replaceAvatars);\n }\n\n\n function replaceAvatars(selection) {\n var showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n var osm = services.osm;\n if (showThirdPartyIcons !== 'true' || !osm) return;\n\n var uids = {}; // gather uids in the comment thread\n _note.comments.forEach(function(d) {\n if (d.uid) uids[d.uid] = true;\n });\n\n Object.keys(uids).forEach(function(uid) {\n osm.loadUser(uid, function(err, user) {\n if (!user || !user.image_url) return;\n\n selection.selectAll('.comment-avatar.user-' + uid)\n .html('')\n .append('img')\n .attr('class', 'icon comment-avatar-icon')\n .attr('src', user.image_url)\n .attr('alt', user.display_name);\n });\n });\n }\n\n\n noteComments.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteComments;\n };\n\n\n return noteComments;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiNoteHeader() {\n var _note;\n\n\n function noteHeader(selection) {\n var header = selection.selectAll('.note-header')\n .data(\n (_note ? [_note] : []),\n function(d) { return d.status + d.id; }\n );\n\n header.exit()\n .remove();\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'note-header');\n\n var iconEnter = headerEnter\n .append('div')\n .attr('class', function(d) { return 'note-header-icon ' + d.status; })\n .classed('new', function(d) { return d.id < 0; });\n\n iconEnter\n .append('div')\n .attr('class', 'preset-icon-28')\n .call(svgIcon('#iD-icon-note', 'note-fill'));\n\n iconEnter.each(function(d) {\n var statusIcon;\n if (d.id < 0) {\n statusIcon = '#iD-icon-plus';\n } else if (d.status === 'open') {\n statusIcon = '#iD-icon-close';\n } else {\n statusIcon = '#iD-icon-apply';\n }\n iconEnter\n .append('div')\n .attr('class', 'note-icon-annotation')\n .attr('title', t('icons.close'))\n .call(svgIcon(statusIcon, 'icon-annotation'));\n });\n\n headerEnter\n .append('div')\n .attr('class', 'note-header-label')\n .each(function(d) {\n const selection = d3_select(this);\n selection.text('');\n if (_note.isNew()) {\n selection.call(t.append('note.new'));\n } else {\n selection.call(t.append('note.note'));\n selection\n .append('span')\n .text(` ${d.id} `);\n if (d.status === 'closed') {\n selection.call(t.append('note.closed'));\n }\n }\n });\n }\n\n\n noteHeader.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteHeader;\n };\n\n\n return noteHeader;\n}\n", "import { t } from '../core/localizer';\nimport { osmNote } from '../osm';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiNoteReport() {\n var _note;\n\n function noteReport(selection) {\n var url;\n if (services.osm && (_note instanceof osmNote) && (!_note.isNew())) {\n url = services.osm.noteReportURL(_note);\n }\n\n var link = selection.selectAll('.note-report')\n .data(url ? [url] : []);\n\n // exit\n link.exit()\n .remove();\n\n // enter\n var linkEnter = link.enter()\n .append('a')\n .attr('class', 'note-report')\n .attr('target', '_blank')\n .attr('href', function(d) { return d; })\n .call(svgIcon('#iD-icon-out-link', 'inline'));\n\n linkEnter\n .append('span')\n .call(t.append('note.report'));\n }\n\n\n noteReport.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteReport;\n };\n\n return noteReport;\n}\n", "import { t } from '../core/localizer';\nimport { osmEntity, osmNote, osmRelation, osmWay } from '../osm';\nimport { svgIcon } from '../svg/icon';\nimport { getRelativeDate } from '../util/date';\n\n\nexport function uiViewOnOSM(context) {\n var _what; // an osmEntity or osmNote\n\n\n function viewOnOSM(selection) {\n var url;\n if (_what instanceof osmEntity) {\n url = context.connection().historyURL(_what);\n } else if (_what instanceof osmNote) {\n url = context.connection().noteURL(_what);\n }\n\n var data = ((!_what || _what.isNew()) ? [] : [_what]);\n var link = selection.selectAll('.view-on-osm')\n .data(data, function(d) { return d.id; });\n\n // exit\n link.exit()\n .remove();\n\n // enter\n var linkEnter = link.enter()\n .append('a')\n .attr('class', 'view-on-osm')\n .attr('target', '_blank')\n .attr('href', url)\n .call(svgIcon('#iD-icon-out-link', 'inline'));\n\n\n if (_what && !(_what instanceof osmNote)) {\n // node/way/relation\n const { user, timestamp } = uiViewOnOSM.findLastModifiedChild(context.history().base(), _what);\n\n linkEnter\n .call(t.append('inspector.last_touched', {\n timeago: getRelativeDate(new Date(timestamp)),\n user\n }))\n .attr('title', t('inspector.view_on_osm'));\n } else {\n linkEnter\n .append('span')\n .call(t.append('inspector.view_on_osm'));\n }\n }\n\n\n viewOnOSM.what = function(_) {\n if (!arguments.length) return _what;\n _what = _;\n return viewOnOSM;\n };\n\n return viewOnOSM;\n}\n\n\n/**\n * @param {iD.Graph} graph\n * @param {iD.OsmEntity} feature\n */\nuiViewOnOSM.findLastModifiedChild = (graph, feature) => {\n let latest = feature;\n\n /** @param {iD.OsmEntity} obj */\n function recurseChilds(obj) {\n if (obj.timestamp > latest.timestamp) {\n latest = obj;\n }\n if (obj instanceof osmWay) {\n obj.nodes\n .map(id => graph.hasEntity(id))\n .filter(Boolean)\n .forEach(recurseChilds);\n } else if (obj instanceof osmRelation) {\n obj.members\n .map(m => graph.hasEntity(m.id))\n .filter(e => e instanceof osmWay || e instanceof osmRelation)\n .forEach(recurseChilds);\n }\n }\n\n recurseChilds(feature);\n return latest;\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\n\n// import { uiField } from './field';\n// import { uiFormFields } from './form_fields';\n\nimport { uiNoteComments } from './note_comments';\nimport { uiNoteHeader } from './note_header';\nimport { uiNoteReport } from './note_report';\nimport { uiViewOnOSM } from './view_on_osm';\n\nimport {\n utilNoAuto,\n utilRebind\n} from '../util';\nimport { osmNote } from '../osm';\n\n\nexport function uiNoteEditor(context) {\n var dispatch = d3_dispatch('change');\n var noteComments = uiNoteComments(context);\n var noteHeader = uiNoteHeader();\n\n // var formFields = uiFormFields(context);\n\n var _note;\n var _newNote;\n // var _fieldsArr;\n\n\n function noteEditor(selection) {\n\n var header = selection.selectAll('.header')\n .data([0]);\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function() {\n context.enter(modeBrowse(context));\n })\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('note.title'));\n\n\n var body = selection.selectAll('.body')\n .data([0]);\n\n body = body.enter()\n .append('div')\n .attr('class', 'body')\n .merge(body);\n\n var editor = body.selectAll('.note-editor')\n .data([0]);\n\n editor.enter()\n .append('div')\n .attr('class', 'modal-section note-editor')\n .merge(editor)\n .call(noteHeader.note(_note))\n .call(noteComments.note(_note))\n .call(noteSaveSection);\n\n var footer = selection.selectAll('.footer')\n .data([0]);\n\n footer.enter()\n .append('div')\n .attr('class', 'footer')\n .merge(footer)\n .call(uiViewOnOSM(context).what(_note))\n .call(uiNoteReport(context).note(_note));\n\n\n // rerender the note editor on any auth change\n var osm = services.osm;\n if (osm) {\n osm.on('change.note-save', function() {\n selection.call(noteEditor);\n });\n }\n }\n\n\n function noteSaveSection(selection) {\n var isSelected = (_note && _note.id === context.selectedNoteID());\n var noteSave = selection.selectAll('.note-save')\n .data((isSelected ? [_note].filter(d => d.status !== 'hidden') : []), d => d.status + d.id);\n\n // exit\n noteSave.exit()\n .remove();\n\n // enter\n var noteSaveEnter = noteSave.enter()\n .append('div')\n .attr('class', 'note-save save-section cf');\n\n // // if new note, show categories to pick from\n // if (_note.isNew()) {\n // var presets = presetManager;\n\n // // NOTE: this key isn't a age and therefore there is no documentation (yet)\n // _fieldsArr = [\n // uiField(context, presets.field('category'), null, { show: true, revert: false }),\n // ];\n\n // _fieldsArr.forEach(function(field) {\n // field\n // .on('change', changeCategory);\n // });\n\n // noteSaveEnter\n // .append('div')\n // .attr('class', 'note-category')\n // .call(formFields.fieldsArr(_fieldsArr));\n // }\n\n // function changeCategory() {\n // // NOTE: perhaps there is a better way to get value\n // var val = context.container().select('input[name=\\'category\\']:checked').property('__data__') || undefined;\n\n // // store the unsaved category with the note itself\n // _note = _note.update({ newCategory: val });\n // var osm = services.osm;\n // if (osm) {\n // osm.replaceNote(_note); // update note cache\n // }\n // noteSave\n // .call(noteSaveButtons);\n // }\n\n noteSaveEnter\n .append('h4')\n .attr('class', '.note-save-header')\n .text('')\n .each(function() {\n if (_note.isNew()) {\n t.append('note.newDescription')(d3_select(this));\n } else {\n t.append('note.newComment')(d3_select(this));\n }\n });\n\n var commentTextarea = noteSaveEnter\n .append('textarea')\n .attr('class', 'new-comment-input')\n .attr('placeholder', t('note.inputPlaceholder'))\n .attr('maxlength', 1000)\n .property('value', function(d) { return d.newComment; })\n .call(utilNoAuto)\n .on('keydown.note-input', keydown)\n .on('input.note-input', changeInput)\n .on('blur.note-input', changeInput);\n\n if (!commentTextarea.empty() && _newNote) {\n // autofocus the comment field for new notes\n commentTextarea.node().focus();\n }\n\n // update\n noteSave = noteSaveEnter\n .merge(noteSave)\n .call(userDetails)\n .call(noteSaveButtons);\n\n\n // fast submit if user presses cmd+enter\n function keydown(d3_event) {\n if (!(d3_event.keyCode === 13 && // \u21A9 Return\n d3_event.metaKey)) return;\n\n var osm = services.osm;\n if (!osm) return;\n\n var hasAuth = osm.authenticated();\n if (!hasAuth) return;\n\n if (!_note.newComment) return;\n\n d3_event.preventDefault();\n\n d3_select(this)\n .on('keydown.note-input', null);\n\n // focus on button and submit\n window.setTimeout(function() {\n if (_note.isNew()) {\n noteSave.selectAll('.save-button').node().focus();\n clickSave(_note);\n } else {\n noteSave.selectAll('.comment-button').node().focus();\n postCommentAndStatus(_note);\n }\n }, 10);\n }\n\n\n function changeInput() {\n var input = d3_select(this);\n var val = input.property('value').trim() || undefined;\n\n // store the unsaved comment with the note itself\n _note = _note.update({ newComment: val });\n\n var osm = services.osm;\n if (osm) {\n osm.replaceNote(_note); // update note cache\n }\n\n noteSave\n .call(noteSaveButtons);\n }\n }\n\n\n function userDetails(selection) {\n var detailSection = selection.selectAll('.detail-section')\n .data([0]);\n\n detailSection = detailSection.enter()\n .append('div')\n .attr('class', 'detail-section')\n .merge(detailSection);\n\n var osm = services.osm;\n if (!osm) return;\n\n // Add warning if user is not logged in\n var hasAuth = osm.authenticated();\n var authWarning = detailSection.selectAll('.auth-warning')\n .data(hasAuth ? [] : [0]);\n\n authWarning.exit()\n .transition()\n .duration(200)\n .style('opacity', 0)\n .remove();\n\n var authEnter = authWarning.enter()\n .insert('div', '.tag-reference-body')\n .attr('class', 'field-warning auth-warning')\n .style('opacity', 0);\n\n authEnter\n .call(svgIcon('#iD-icon-alert', 'inline'));\n\n authEnter\n .append('span')\n .call(t.append('note.login'));\n\n authEnter\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append('login'))\n .on('click.note-login', function(d3_event) {\n d3_event.preventDefault();\n osm.authenticate();\n });\n\n authEnter\n .transition()\n .duration(200)\n .style('opacity', 1);\n\n\n var prose = detailSection.selectAll('.note-save-prose')\n .data(hasAuth ? [0] : []);\n\n prose.exit()\n .remove();\n\n prose = prose.enter()\n .append('p')\n .attr('class', 'note-save-prose')\n .call(t.append('note.upload_explanation'))\n .merge(prose);\n\n osm.userDetails(function(err, user) {\n if (err) return;\n\n const userLink = selection => {\n if (user.image_url) {\n selection\n .append('img')\n .attr('src', user.image_url)\n .attr('class', 'icon pre-text user-icon');\n }\n\n selection\n .append('a')\n .attr('class', 'user-info')\n .text(user.display_name)\n .attr('href', osm.userURL(user.display_name))\n .attr('target', '_blank');\n };\n\n prose\n .call(t.addOrUpdate('note.upload_explanation_with_user', { user: userLink }));\n });\n }\n\n\n function noteSaveButtons(selection) {\n var osm = services.osm;\n var hasAuth = osm && osm.authenticated();\n\n var isSelected = (_note && _note.id === context.selectedNoteID());\n var buttonSection = selection.selectAll('.buttons')\n .data((isSelected ? [_note] : []), d => d.status + d.id);\n\n // exit\n buttonSection.exit()\n .remove();\n\n // enter\n var buttonEnter = buttonSection.enter()\n .append('div')\n .attr('class', 'buttons');\n\n if (_note.isNew()) {\n buttonEnter\n .append('button')\n .attr('class', 'button cancel-button secondary-action')\n .call(t.append('confirm.cancel'));\n\n buttonEnter\n .append('button')\n .attr('class', 'button save-button action')\n .call(t.append('note.save'));\n\n } else {\n buttonEnter\n .append('button')\n .attr('class', 'button status-button action');\n\n buttonEnter\n .append('button')\n .attr('class', 'button comment-button action')\n .call(t.append('note.comment'));\n }\n\n\n // update\n buttonSection = buttonSection\n .merge(buttonEnter);\n\n buttonSection.select('.cancel-button') // select and propagate data\n .on('click.cancel', clickCancel);\n\n buttonSection.select('.save-button') // select and propagate data\n .attr('disabled', isSaveDisabled)\n .on('click.save', clickSave);\n\n buttonSection.select('.status-button') // select and propagate data\n .attr('disabled', (hasAuth ? null : true))\n .each(function(d) {\n var action = (d.status === 'open' ? 'close' : 'open');\n var andComment = (d.newComment ? '_comment' : '');\n t.addOrUpdate('note.' + action + andComment)(d3_select(this));\n })\n .on('click.status', function(d3_event, note) {\n const setStatus = (note.status === 'open' ? 'closed' : 'open');\n postCommentAndStatus.bind(this)(d3_event, note, setStatus);\n });\n\n buttonSection.select('.comment-button') // select and propagate data\n .attr('disabled', isSaveDisabled)\n .on('click.comment', postCommentAndStatus);\n\n\n function isSaveDisabled(d) {\n return (hasAuth && d.status === 'open' && d.newComment) ? null : true;\n }\n }\n\n\n\n function clickCancel(d3_event, d) {\n this.blur(); // avoid keeping focus on the button - #4641\n var osm = services.osm;\n if (osm) {\n osm.removeNote(d);\n }\n context.enter(modeBrowse(context));\n dispatch.call('change', d);\n }\n\n\n function clickSave(d3_event, note) {\n this.blur(); // avoid keeping focus on the button - #4641\n var osm = services.osm;\n if (osm) {\n osm.postNoteCreate(note, (err, d) => dispatch.call('change', d));\n }\n }\n\n\n function postCommentAndStatus(d3_event, note, newStatus) {\n this.blur(); // avoid keeping focus on the button - #4641\n var osm = services.osm;\n if (osm) {\n osm.postNoteUpdate(note, newStatus || note.status, function(err, d) {\n if (!err) {\n dispatch.call('change', d);\n } else if (err.status === 409) {\n // note was probably closed in the meantime: reload it - #8464\n osm.loadEntityNote(note.id, (err, d) => {\n dispatch.call('change', osmNote({ ...d.data[0], newComment: note.newComment }));\n });\n } else if (err.status === 410) {\n // note was deleted/hidden by a moderator\n osm.removeNote(note);\n dispatch.call('change', osmNote({ id: note.id, status: 'hidden', comments: [...note.comments, { action: 'hidden' }] }));\n }\n });\n }\n }\n\n\n noteEditor.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteEditor;\n };\n\n noteEditor.newNote = function(val) {\n if (!arguments.length) return _newNote;\n _newNote = val;\n return noteEditor;\n };\n\n\n return utilRebind(noteEditor, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { behaviorBreathe } from '../behavior/breathe';\nimport { behaviorHover } from '../behavior/hover';\nimport { behaviorLasso } from '../behavior/lasso';\nimport { behaviorSelect } from '../behavior/select';\n\nimport { t } from '../core/localizer';\n\nimport { modeBrowse } from './browse';\nimport { modeDragNode } from './drag_node';\nimport { modeDragNote } from './drag_note';\nimport { services } from '../services';\nimport { uiNoteEditor } from '../ui/note_editor';\nimport { utilKeybinding } from '../util';\n\n\nexport function modeSelectNote(context, selectedNoteID) {\n var mode = {\n id: 'select-note',\n button: 'browse'\n };\n\n var _keybinding = utilKeybinding('select-note');\n var _noteEditor = uiNoteEditor(context)\n .on('change', function() {\n context.map().pan([0,0]); // trigger a redraw\n var note = this || checkSelectedID();\n if (!note) return;\n context.ui().sidebar\n .show(_noteEditor.note(note));\n });\n\n var _behaviors = [\n behaviorBreathe(context),\n behaviorHover(context),\n behaviorSelect(context),\n behaviorLasso(context),\n modeDragNode(context).behavior,\n modeDragNote(context).behavior\n ];\n\n var _newFeature = false;\n\n\n function checkSelectedID() {\n if (!services.osm) return;\n var note = services.osm.getNote(selectedNoteID);\n if (!note) {\n context.enter(modeBrowse(context));\n }\n return note;\n }\n\n\n // class the note as selected, or return to browse mode if the note is gone\n function selectNote(d3_event, drawn) {\n if (!checkSelectedID()) return;\n\n var selection = context.surface().selectAll('.layer-notes .note-' + selectedNoteID);\n\n if (selection.empty()) {\n // Return to browse mode if selected DOM elements have\n // disappeared because the user moved them out of view..\n var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;\n if (drawn && source && (source.type === 'pointermove' || source.type === 'mousemove' || source.type === 'touchmove')) {\n context.enter(modeBrowse(context));\n }\n\n } else {\n selection\n .classed('selected', true);\n\n context.selectedNoteID(selectedNoteID);\n }\n }\n\n\n function esc() {\n if (context.container().select('.combobox').size()) return;\n context.enter(modeBrowse(context));\n }\n\n\n mode.zoomToSelected = function() {\n if (!services.osm) return;\n var note = services.osm.getNote(selectedNoteID);\n if (note) {\n context.map().centerZoomEase(note.loc, 20);\n }\n };\n\n\n mode.newFeature = function(val) {\n if (!arguments.length) return _newFeature;\n _newFeature = val;\n return mode;\n };\n\n\n mode.enter = function() {\n var note = checkSelectedID();\n if (!note) return;\n\n _behaviors.forEach(context.install);\n\n _keybinding\n .on(t('inspector.zoom_to.key'), mode.zoomToSelected)\n .on('\u238B', esc, true);\n\n d3_select(document)\n .call(_keybinding);\n\n selectNote();\n\n var sidebar = context.ui().sidebar;\n sidebar.show(_noteEditor.note(note).newNote(_newFeature));\n\n // expand the sidebar, avoid obscuring the note if needed\n sidebar.expand(sidebar.intersects(note.extent()));\n\n context.map()\n .on('drawn.select', selectNote);\n };\n\n\n mode.exit = function() {\n _behaviors.forEach(context.uninstall);\n\n d3_select(document)\n .call(_keybinding.unbind);\n\n context.surface()\n .selectAll('.layer-notes .selected')\n .classed('selected hover', false);\n\n context.map()\n .on('drawn.select', null);\n\n context.ui().sidebar\n .hide();\n\n context.selectedNoteID(null);\n };\n\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDraw } from '../behavior/draw';\nimport { modeBrowse } from './browse';\nimport { modeSelectNote } from './select_note';\nimport { osmNote } from '../osm';\nimport { services } from '../services';\n\n\nexport function modeAddNote(context) {\n var mode = {\n id: 'add-note',\n button: 'note',\n description: t.append('modes.add_note.description'),\n key: t('modes.add_note.key')\n };\n\n var behavior = behaviorDraw(context)\n .on('click', add)\n .on('cancel', cancel)\n .on('finish', cancel);\n\n\n function add(loc) {\n var osm = services.osm;\n if (!osm) return;\n\n var note = osmNote({ loc: loc, status: 'open', comments: [] });\n osm.replaceNote(note);\n\n // force a reraw (there is no history change that would otherwise do this)\n context.map().pan([0,0]);\n\n context\n .selectedNoteID(note.id)\n .enter(modeSelectNote(context, note.id).newFeature(true));\n }\n\n\n function cancel() {\n context.enter(modeBrowse(context));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { behaviorOperation } from '../behavior/operation';\nimport { modeMove } from '../modes/move';\nimport { utilGetAllNodes, utilTotalExtent } from '../util/util';\n\n\nexport function operationMove(context, selectedIDs) {\n var multi = (selectedIDs.length === 1 ? 'single' : 'multiple');\n var nodes = utilGetAllNodes(selectedIDs, context.graph());\n var coords = nodes.map(function(n) { return n.loc; });\n var extent = utilTotalExtent(selectedIDs, context.graph());\n\n\n var operation = function() {\n context.enter(modeMove(context, selectedIDs));\n };\n\n\n operation.available = function() {\n return selectedIDs.length > 0;\n };\n\n\n operation.disabled = function() {\n if (extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n } else if (selectedIDs.some(incompleteRelation)) {\n return 'incomplete_relation';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n\n function incompleteRelation(id) {\n var entity = context.entity(id);\n return entity.type === 'relation' && !entity.isComplete(context.graph());\n }\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.move.' + disable + '.' + multi) :\n t.append('operations.move.description.' + multi);\n };\n\n\n operation.annotation = function() {\n return selectedIDs.length === 1 ?\n t('operations.move.annotation.' + context.graph().geometry(selectedIDs[0])) :\n t('operations.move.annotation.feature', { n: selectedIDs.length });\n };\n\n\n operation.id = 'move';\n operation.keys = [t('operations.move.key')];\n operation.title = t.append('operations.move.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n operation.mouseOnly = true;\n\n return operation;\n}\n", "import {\n geoIdentity as d3_geoIdentity,\n geoPath as d3_geoPath,\n geoStream as d3_geoStream\n} from 'd3-geo';\n\nimport { geoVecAdd, geoVecAngle, geoVecLength } from '../geo';\n\n\n// Touch targets control which other vertices we can drag a vertex onto.\n//\n// - the activeID - nope\n// - 1 away (adjacent) to the activeID - yes (vertices will be merged)\n// - 2 away from the activeID - nope (would create a self intersecting segment)\n// - all others on a linear way - yes\n// - all others on a closed way - nope (would create a self intersecting polygon)\n//\n// returns\n// 0 = active vertex - no touch/connect\n// 1 = passive vertex - yes touch/connect\n// 2 = adjacent vertex - yes but pay attention segmenting a line here\n//\nexport function svgPassiveVertex(node, graph, activeID) {\n if (!activeID) return 1;\n if (activeID === node.id) return 0;\n\n var parents = graph.parentWays(node);\n\n var i, j, nodes, isClosed, ix1, ix2, ix3, ix4, max;\n\n for (i = 0; i < parents.length; i++) {\n nodes = parents[i].nodes;\n isClosed = parents[i].isClosed();\n for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby\n if (nodes[j] === node.id) {\n ix1 = j - 2;\n ix2 = j - 1;\n ix3 = j + 1;\n ix4 = j + 2;\n\n if (isClosed) { // wraparound if needed\n max = nodes.length - 1;\n if (ix1 < 0) ix1 = max + ix1;\n if (ix2 < 0) ix2 = max + ix2;\n if (ix3 > max) ix3 = ix3 - max;\n if (ix4 > max) ix4 = ix4 - max;\n }\n\n if (nodes[ix1] === activeID) return 0; // no - prevent self intersect\n else if (nodes[ix2] === activeID) return 2; // ok - adjacent\n else if (nodes[ix3] === activeID) return 2; // ok - adjacent\n else if (nodes[ix4] === activeID) return 0; // no - prevent self intersect\n else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // no - prevent self intersect\n }\n }\n }\n\n return 1; // ok\n}\n\n\n/**\n *\n * @param {iD.Projection} projection\n * @param {iD.Graph} graph\n * @param {Number} dt spacing between segments\n * @param {Function} [shouldReverse]\n * @param {Function} [bothDirections]\n */\nexport function svgMarkerSegments(projection, graph, dt, shouldReverse = () => false, bothDirections = () => false) {\n /**\n * @param {iD.OsmWay} entity\n * @returns {[{id: String, d: String}]} list of svg path segments corres\n */\n return function(entity) {\n let i = 0;\n let offset = dt / 2;\n const segments = [];\n\n const clip = paddedClipExtent(projection);\n\n const coordinates = graph.childNodes(entity).map(function(n) { return n.loc; });\n let a, b;\n\n const _shouldReverse = shouldReverse(entity);\n const _bothDirections = bothDirections(entity);\n\n d3_geoStream({\n type: 'LineString',\n coordinates: coordinates\n }, projection.stream(clip({\n lineStart: function() {},\n lineEnd: function() { a = null; },\n point: function(x, y) {\n b = [x, y];\n\n if (a) {\n let span = geoVecLength(a, b) - offset;\n\n if (span >= 0) {\n const heading = geoVecAngle(a, b);\n const dx = dt * Math.cos(heading);\n const dy = dt * Math.sin(heading);\n let p = [\n a[0] + offset * Math.cos(heading),\n a[1] + offset * Math.sin(heading)\n ];\n\n // gather coordinates\n const coord = [a, p];\n for (span -= dt; span >= 0; span -= dt) {\n p = geoVecAdd(p, [dx, dy]);\n coord.push(p);\n }\n coord.push(b);\n\n // generate svg paths\n let segment = '';\n\n if (!_shouldReverse || _bothDirections) {\n for (let j = 0; j < coord.length; j++) {\n segment += (j === 0 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];\n }\n segments.push({ id: entity.id, index: i++, d: segment });\n }\n\n if (_shouldReverse || _bothDirections) {\n segment = '';\n for (let j = coord.length - 1; j >= 0; j--) {\n segment += (j === coord.length - 1 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];\n }\n segments.push({ id: entity.id, index: i++, d: segment });\n }\n }\n\n offset = -span;\n }\n\n a = b;\n }\n })));\n\n return segments;\n };\n}\n\n\n/**\n * @param {iD.Projection} projection\n * @param {iD.Graph} graph\n * @param {Boolean} isArea\n */\nexport function svgPath(projection, graph, isArea) {\n const cache = {};\n const project = projection.stream;\n const clip = paddedClipExtent(projection, isArea);\n const path = d3_geoPath()\n .projection({stream: function(output) { return project(clip(output)); }});\n\n const svgpath = function(entity) {\n if (entity.id in cache) {\n return cache[entity.id];\n } else {\n return cache[entity.id] = path(entity.asGeoJSON(graph));\n }\n };\n\n svgpath.geojson = function(d) {\n if (d.__featurehash__ !== undefined) {\n if (d.__featurehash__ in cache) {\n return cache[d.__featurehash__];\n } else {\n return cache[d.__featurehash__] = path(d);\n }\n } else {\n return path(d);\n }\n };\n\n return svgpath;\n}\n\n\nexport function svgPointTransform(projection) {\n var svgpoint = function(entity) {\n // http://jsperf.com/short-array-join\n var pt = projection(entity.loc);\n return 'translate(' + pt[0] + ',' + pt[1] + ')';\n };\n\n svgpoint.geojson = function(d) {\n return svgpoint(d.properties.entity);\n };\n\n return svgpoint;\n}\n\n\nexport function svgRelationMemberTags(graph) {\n return function(entity) {\n var tags = entity.tags;\n var shouldCopyMultipolygonTags = !entity.hasInterestingTags();\n graph.parentRelations(entity).forEach(function(relation) {\n var type = relation.tags.type;\n if ((type === 'multipolygon' && shouldCopyMultipolygonTags) || type === 'boundary') {\n tags = Object.assign({}, relation.tags, tags);\n }\n });\n return tags;\n };\n}\n\n\nexport function svgSegmentWay(way, graph, activeID) {\n // When there is no activeID, we can memoize this expensive computation\n if (activeID === undefined) {\n return graph.transient(way, 'waySegments', getWaySegments);\n } else {\n return getWaySegments();\n }\n\n function getWaySegments() {\n const isActiveWay = (way.nodes.indexOf(activeID) !== -1);\n const features = { passive: [], active: [] };\n let start = {};\n\n for (var i = 0; i < way.nodes.length; i++) {\n const node = graph.entity(way.nodes[i]);\n const type = svgPassiveVertex(node, graph, activeID);\n const end = { node: node, type: type };\n\n if (start.type !== undefined) {\n if (start.node.id === activeID || end.node.id === activeID) {\n // push nothing\n } else if (isActiveWay && (start.type === 2 || end.type === 2)) { // one adjacent vertex\n pushActive(start, end, i);\n } else if (start.type === 0 && end.type === 0) { // both active vertices\n pushActive(start, end, i);\n } else {\n pushPassive(start, end, i);\n }\n }\n\n start = end;\n }\n\n return features;\n\n function pushActive(start, end, index) {\n features.active.push({\n type: 'Feature',\n id: way.id + '-' + index + '-nope',\n properties: {\n nope: true,\n target: true,\n entity: way,\n nodes: [start.node, end.node],\n index: index\n },\n geometry: {\n type: 'LineString',\n coordinates: [start.node.loc, end.node.loc]\n }\n });\n }\n\n function pushPassive(start, end, index) {\n features.passive.push({\n type: 'Feature',\n id: way.id + '-' + index,\n properties: {\n target: true,\n entity: way,\n nodes: [start.node, end.node],\n index: index\n },\n geometry: {\n type: 'LineString',\n coordinates: [start.node.loc, end.node.loc]\n }\n });\n }\n }\n}\n\n\n/**\n * Returns a d3 projection stream that clips the given geometries to an\n * extent that is slightly padded.\n *\n * Explanation of magic numbers:\n * \"padding\" here allows space for strokes to extend beyond the viewport,\n * so that the stroke isn't drawn along the edge of the viewport when\n * the shape is clipped.\n * When drawing lines, pad viewport by 5px.\n * When drawing areas, pad viewport by 65px in each direction to allow\n * for 60px area fill stroke (see \".fill-partial path.fill\" css rule)\n *\n * @param {import('../geo/raw_mercator').Projection} projection\n * @param {Boolean} isArea\n */\nfunction paddedClipExtent(projection, isArea = false) {\n var padding = isArea ? 65 : 5;\n var viewport = projection.clipExtent();\n var paddedExtent = [\n [viewport[0][0] - padding, viewport[0][1] - padding],\n [viewport[1][0] + padding, viewport[1][1] + padding]\n ];\n return d3_geoIdentity().clipExtent(paddedExtent).stream;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { osmPathHighwayTagValues, osmPavedTags, osmSemipavedTags, osmLifecyclePrefixes } from '../osm/tags';\n\n\nexport function svgTagClasses() {\n var primaries = [\n 'building', 'highway', 'railway', 'waterway', 'aeroway', 'aerialway',\n 'piste:type', 'boundary', 'power', 'amenity', 'natural', 'landuse',\n 'leisure', 'military', 'place', 'man_made', 'route', 'attraction',\n 'roller_coaster', 'building:part', 'indoor', 'climbing'\n ];\n var statuses = Object.keys(osmLifecyclePrefixes);\n var secondaries = [\n 'oneway', 'bridge', 'tunnel', 'embankment', 'cutting', 'barrier',\n 'surface', 'tracktype', 'footway', 'crossing', 'service', 'sport',\n 'public_transport', 'location', 'parking', 'golf', 'type', 'leisure',\n 'man_made', 'indoor', 'construction', 'proposed'\n ];\n var _tags = function(entity) { return entity.tags; };\n\n\n var tagClasses = function(selection) {\n selection.each(function tagClassesEach(entity) {\n var value = this.className;\n\n if (value.baseVal !== undefined) {\n value = value.baseVal;\n }\n\n var t = _tags(entity);\n\n var computed = tagClasses.getClassesString(t, value);\n\n if (computed !== value) {\n d3_select(this).attr('class', computed);\n }\n });\n };\n\n\n tagClasses.getClassesString = function(t, value) {\n var primary, status;\n var i, j, k, v;\n\n // in some situations we want to render perimeter strokes a certain way\n var overrideGeometry;\n if (/\\bstroke\\b/.test(value)) {\n if (!!t.barrier && t.barrier !== 'no') {\n overrideGeometry = 'line';\n }\n }\n\n // preserve base classes (nothing with `tag-`)\n var classes = value.trim().split(/\\s+/)\n .filter(function(klass) {\n return klass.length && !/^tag-/.test(klass);\n })\n .map(function(klass) { // special overrides for some perimeter strokes\n return (klass === 'line' || klass === 'area') ? (overrideGeometry || klass) : klass;\n });\n\n // pick at most one primary classification tag..\n for (i = 0; i < primaries.length; i++) {\n k = primaries[i];\n v = t[k];\n if (!v || v === 'no') continue;\n\n if (k === 'piste:type') { // avoid a ':' in the class name\n k = 'piste';\n } else if (k === 'building:part') { // avoid a ':' in the class name\n k = 'building_part';\n }\n\n primary = k;\n if (statuses.indexOf(v) !== -1) { // e.g. `railway=abandoned`\n status = v;\n classes.push('tag-' + k);\n } else {\n classes.push('tag-' + k);\n classes.push('tag-' + k + '-' + v);\n }\n\n break;\n }\n\n if (!primary) {\n for (i = 0; i < statuses.length; i++) {\n for (j = 0; j < primaries.length; j++) {\n k = statuses[i] + ':' + primaries[j]; // e.g. `demolished:building=yes`\n v = t[k];\n if (!v || v === 'no') continue;\n\n status = statuses[i];\n break;\n }\n }\n }\n\n // add at most one status tag, only if relates to primary tag..\n if (!status) {\n for (i = 0; i < statuses.length; i++) {\n k = statuses[i];\n v = t[k];\n if (!v || v === 'no') continue;\n\n if (v === 'yes') { // e.g. `railway=rail + abandoned=yes`\n status = k;\n } else if (primary && primary === v) { // e.g. `railway=rail + abandoned=railway`\n status = k;\n } else if (!primary && primaries.indexOf(v) !== -1) { // e.g. `abandoned=railway`\n status = k;\n primary = v;\n classes.push('tag-' + v);\n } // else ignore e.g. `highway=path + abandoned=railway`\n\n if (status) break;\n }\n }\n\n if (status) {\n classes.push('tag-status');\n classes.push('tag-status-' + status);\n }\n\n // add any secondary tags\n for (i = 0; i < secondaries.length; i++) {\n k = secondaries[i];\n v = t[k];\n if (!v || v === 'no' || k === primary) continue;\n classes.push('tag-' + k);\n classes.push('tag-' + k + '-' + v);\n }\n\n // For highways, look for surface tagging..\n if ((primary === 'highway' && !osmPathHighwayTagValues[t.highway]) || primary === 'aeroway') {\n var surface = t.highway === 'track' ? 'unpaved' : 'paved';\n for (k in t) {\n v = t[k];\n if (k in osmPavedTags) {\n surface = osmPavedTags[k][v] ? 'paved' : 'unpaved';\n }\n if (k in osmSemipavedTags && !!osmSemipavedTags[k][v]) {\n surface = 'semipaved';\n }\n }\n classes.push('tag-' + surface);\n }\n\n // If this is a wikidata-tagged item, add a class for that..\n var qid = (\n t.wikidata ||\n t['flag:wikidata'] ||\n t['brand:wikidata'] ||\n t['network:wikidata'] ||\n t['operator:wikidata']\n );\n\n if (qid) {\n classes.push('tag-wikidata');\n }\n\n // ensure that classes for tags keys/values with special characters like spaces\n // are not added to the DOM, because it can cause bizarre issues (#9448)\n return classes\n .filter(klass => /^[-_a-z0-9]+$/.test(klass))\n .join(' ')\n .trim();\n };\n\n\n tagClasses.tags = function(val) {\n if (!arguments.length) return _tags;\n _tags = val;\n return tagClasses;\n };\n\n return tagClasses;\n}\n", "// Patterns only work in Firefox when set directly on element.\n// (This is not a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=750632)\nvar patterns = {\n // tag - pattern name\n // -or-\n // tag - value - pattern name\n // -or-\n // tag - value - rules (optional tag-values, pattern name)\n // (matches earlier rules first, so fallback should be last entry)\n amenity: {\n grave_yard: 'cemetery',\n fountain: 'water_standing'\n },\n landuse: {\n cemetery: [\n { religion: 'christian', pattern: 'cemetery_christian' },\n { religion: 'buddhist', pattern: 'cemetery_buddhist' },\n { religion: 'muslim', pattern: 'cemetery_muslim' },\n { religion: 'jewish', pattern: 'cemetery_jewish' },\n { pattern: 'cemetery' }\n ],\n construction: 'construction',\n farmland: 'farmland',\n farmyard: 'farmyard',\n forest: [\n { leaf_type: 'broadleaved', pattern: 'forest_broadleaved' },\n { leaf_type: 'needleleaved', pattern: 'forest_needleleaved' },\n { leaf_type: 'leafless', pattern: 'forest_leafless' },\n { pattern: 'forest' } // same as 'leaf_type:mixed'\n ],\n grave_yard: 'cemetery',\n grass: 'grass',\n landfill: 'landfill',\n meadow: 'meadow',\n military: 'construction',\n orchard: 'orchard',\n quarry: 'quarry',\n vineyard: 'vineyard'\n },\n leisure: {\n horse_riding: 'farmyard'\n },\n natural: {\n beach: 'beach',\n grassland: 'grass',\n sand: 'beach',\n scrub: 'scrub',\n water: [\n { water: 'pond', pattern: 'pond' },\n { water: 'reservoir', pattern: 'water_standing' },\n { pattern: 'waves' }\n ],\n wetland: [\n { wetland: 'marsh', pattern: 'wetland_marsh' },\n { wetland: 'swamp', pattern: 'wetland_swamp' },\n { wetland: 'bog', pattern: 'wetland_bog' },\n { wetland: 'reedbed', pattern: 'wetland_reedbed' },\n { pattern: 'wetland' }\n ],\n wood: [\n { leaf_type: 'broadleaved', pattern: 'forest_broadleaved' },\n { leaf_type: 'needleleaved', pattern: 'forest_needleleaved' },\n { leaf_type: 'leafless', pattern: 'forest_leafless' },\n { pattern: 'forest' } // same as 'leaf_type:mixed'\n ]\n },\n golf: {\n green: 'golf_green',\n tee: 'grass',\n fairway: 'grass',\n rough: 'scrub'\n },\n surface: {\n grass: 'grass',\n sand: 'beach'\n }\n};\n\nexport function svgTagPattern(tags) {\n // Skip pattern filling if this is a building (buildings don't get patterns applied)\n if (tags.building && tags.building !== 'no') {\n return null;\n }\n\n for (var tag in patterns) {\n var entityValue = tags[tag];\n if (!entityValue) continue;\n\n if (typeof patterns[tag] === 'string') { // extra short syntax (just tag) - pattern name\n return 'pattern-' + patterns[tag];\n } else {\n var values = patterns[tag];\n for (var value in values) {\n if (entityValue !== value) continue;\n\n var rules = values[value];\n if (typeof rules === 'string') { // short syntax - pattern name\n return 'pattern-' + rules;\n }\n\n // long syntax - rule array\n for (var ruleKey in rules) {\n var rule = rules[ruleKey];\n\n var pass = true;\n for (var criterion in rule) {\n if (criterion !== 'pattern') { // reserved for pattern name\n // The only rule is a required tag-value pair\n var v = tags[criterion];\n if (!v || v !== rule[criterion]) {\n pass = false;\n break;\n }\n }\n }\n\n if (pass) {\n return 'pattern-' + rule.pattern;\n }\n }\n }\n }\n }\n\n return null;\n}\n", "import { deepEqual } from 'fast-equals';\nimport { bisector as d3_bisector } from 'd3-array';\n\nimport { osmEntity } from '../osm';\nimport { svgPath, svgSegmentWay } from './helpers';\nimport { svgTagClasses } from './tag_classes';\nimport { svgTagPattern } from './tag_pattern';\n\nexport function svgAreas(projection, context) {\n\n\n function getPatternStyle(tags) {\n var imageID = svgTagPattern(tags);\n if (imageID) {\n return 'url(\"#ideditor-' + imageID + '\")';\n }\n return '';\n }\n\n\n function drawTargets(selection, graph, entities, filter) {\n var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';\n var getPath = svgPath(projection).geojson;\n var activeID = context.activeID();\n var base = context.history().base();\n\n // The targets and nopes will be MultiLineString sub-segments of the ways\n var data = { targets: [], nopes: [] };\n\n entities.forEach(function(way) {\n var features = svgSegmentWay(way, graph, activeID);\n data.targets.push.apply(data.targets, features.passive);\n data.nopes.push.apply(data.nopes, features.active);\n });\n\n\n // Targets allow hover and vertex snapping\n var targetData = data.targets.filter(getPath);\n var targets = selection.selectAll('.area.target-allowed')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(targetData, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n var segmentWasEdited = function(d) {\n var wayID = d.properties.entity.id;\n // if the whole line was edited, don't draw segment changes\n if (!base.entities[wayID] ||\n !deepEqual(graph.entities[wayID].nodes, base.entities[wayID].nodes)) {\n return false;\n }\n return d.properties.nodes.some(function(n) {\n return !base.entities[n.id] ||\n !deepEqual(graph.entities[n.id].loc, base.entities[n.id].loc);\n });\n };\n\n // enter/update\n targets.enter()\n .append('path')\n .merge(targets)\n .attr('d', getPath)\n .attr('class', function(d) { return 'way area target target-allowed ' + targetClass + d.id; })\n .classed('segment-edited', segmentWasEdited);\n\n\n // NOPE\n var nopeData = data.nopes.filter(getPath);\n var nopes = selection.selectAll('.area.target-nope')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(nopeData, function key(d) { return d.id; });\n\n // exit\n nopes.exit()\n .remove();\n\n // enter/update\n nopes.enter()\n .append('path')\n .merge(nopes)\n .attr('d', getPath)\n .attr('class', function(d) { return 'way area target target-nope ' + nopeClass + d.id; })\n .classed('segment-edited', segmentWasEdited);\n }\n\n\n function drawAreas(selection, graph, entities, filter) {\n var path = svgPath(projection, graph, true);\n var areas = {};\n var base = context.history().base();\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n if (entity.geometry(graph) !== 'area') continue;\n if (!areas[entity.id]) {\n areas[entity.id] = {\n entity: entity,\n area: Math.abs(entity.area(graph))\n };\n }\n }\n\n var fills = Object.values(areas).filter(function hasPath(a) { return path(a.entity); });\n fills.sort(function areaSort(a, b) { return b.area - a.area; });\n fills = fills.map(function(a) { return a.entity; });\n\n var strokes = fills.filter(function(area) { return area.type === 'way'; });\n\n var data = {\n clip: fills,\n shadow: strokes,\n stroke: strokes,\n fill: fills\n };\n\n var clipPaths = context.surface().selectAll('defs').selectAll('.clipPath-osm')\n .filter(filter)\n .data(data.clip, osmEntity.key);\n\n clipPaths.exit()\n .remove();\n\n var clipPathsEnter = clipPaths.enter()\n .append('clipPath')\n .attr('class', 'clipPath-osm')\n .attr('id', function(entity) { return 'ideditor-' + entity.id + '-clippath'; });\n\n clipPathsEnter\n .append('path');\n\n clipPaths.merge(clipPathsEnter)\n .selectAll('path')\n .attr('d', path);\n\n\n var drawLayer = selection.selectAll('.layer-osm.areas');\n var touchLayer = selection.selectAll('.layer-touch.areas');\n\n // Draw areas..\n var areagroup = drawLayer\n .selectAll('g.areagroup')\n .data(['fill', 'shadow', 'stroke']);\n\n areagroup = areagroup.enter()\n .append('g')\n .attr('class', function(d) { return 'areagroup area-' + d; })\n .merge(areagroup);\n\n var paths = areagroup\n .selectAll('path')\n .filter(filter)\n .data(function(layer) { return data[layer]; }, osmEntity.key);\n\n paths.exit()\n .remove();\n\n\n var fillpaths = selection.selectAll('.area-fill path.area').nodes();\n var bisect = d3_bisector(function(node) { return -node.__data__.area(graph); }).left;\n\n function sortedByArea(entity) {\n if (this._parent.__data__ === 'fill') {\n return fillpaths[bisect(fillpaths, -entity.area(graph))];\n }\n }\n\n paths.enter()\n .insert('path', sortedByArea)\n .merge(paths)\n .each(function(entity) {\n var layer = this.parentNode.__data__;\n this.setAttribute('class', entity.type + ' area ' + layer + ' ' + entity.id);\n\n if (layer === 'fill') {\n this.setAttribute('clip-path', 'url(#ideditor-' + entity.id + '-clippath)');\n this.style.fill = getPatternStyle(entity.tags);\n this.style.stroke = this.style.fill;\n }\n })\n .classed('added', function(d) {\n return !base.entities[d.id];\n })\n .classed('geometry-edited', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].nodes, base.entities[d.id].nodes);\n })\n .classed('retagged', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses())\n .attr('d', path);\n\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, data.stroke, filter);\n }\n\n return drawAreas;\n}\n", "import type { Feature, Geometry } from \"geojson\";\n\nexport function $(element: Element | Document, tagName: string): Element[] {\n return Array.from(element.getElementsByTagName(tagName));\n}\n\nexport type P = NonNullable;\nexport type F = Feature;\n\nexport type StyleMap = { [key: string]: P };\n\nexport type NS = [string, string][];\n\nexport function normalizeId(id: string) {\n return id[0] === \"#\" ? id : `#${id}`;\n}\n\nexport function $ns(\n element: Element | Document,\n tagName: string,\n ns: string\n): Element[] {\n return Array.from(element.getElementsByTagNameNS(ns, tagName));\n}\n\n/**\n * get the content of a text node, if any\n */\nexport function nodeVal(node: Element | null) {\n node?.normalize();\n return node?.textContent || \"\";\n}\n\n/**\n * Get one Y child of X, if any, otherwise null\n */\nexport function get1(\n node: Element,\n tagName: string,\n callback?: (elem: Element) => unknown\n) {\n const n = node.getElementsByTagName(tagName);\n const result = n.length ? n[0] : null;\n if (result && callback) callback(result);\n return result;\n}\n\nexport function get(\n node: Element | null,\n tagName: string,\n callback?: (elem: Element, properties: P) => P\n) {\n const properties: Feature[\"properties\"] = {};\n if (!node) return properties;\n const n = node.getElementsByTagName(tagName);\n const result = n.length ? n[0] : null;\n if (result && callback) {\n return callback(result, properties);\n }\n return properties;\n}\n\nexport function val1(\n node: Element,\n tagName: string,\n callback: (val: string) => P | undefined | void\n): P {\n const val = nodeVal(get1(node, tagName));\n if (val && callback) return callback(val) || {};\n return {};\n}\n\nexport function $num(\n node: Element,\n tagName: string,\n callback: (val: number) => Feature[\"properties\"]\n) {\n const val = Number.parseFloat(nodeVal(get1(node, tagName)));\n if (Number.isNaN(val)) return undefined;\n if (val && callback) return callback(val) || {};\n return {};\n}\n\nexport function num1(\n node: Element,\n tagName: string,\n callback?: (val: number) => unknown\n) {\n const val = Number.parseFloat(nodeVal(get1(node, tagName)));\n if (Number.isNaN(val)) return undefined;\n if (callback) callback(val);\n return val;\n}\n\nexport function getMulti(node: Element, propertyNames: string[]): P {\n const properties: P = {};\n for (const property of propertyNames) {\n val1(node, property, (val) => {\n properties[property] = val;\n });\n }\n return properties;\n}\n\nexport function isElement(node: Node | null): node is Element {\n return node?.nodeType === 1;\n}\n", "import { isElement, nodeVal } from \"../shared\";\n\nexport type ExtendedValues = [string, string | number][];\n\nexport function getExtensions(node: Element | null): ExtendedValues {\n let values: [string, string | number][] = [];\n if (node === null) return values;\n for (const child of Array.from(node.childNodes)) {\n if (!isElement(child)) continue;\n const name = abbreviateName(child.nodeName);\n if (name === \"gpxtpx:TrackPointExtension\") {\n // loop again for nested garmin extensions (eg. \"gpxtpx:hr\")\n values = values.concat(getExtensions(child));\n } else {\n // push custom extension (eg. \"power\")\n const val = nodeVal(child);\n values.push([name, parseNumeric(val)]);\n }\n }\n return values;\n}\n\nfunction abbreviateName(name: string) {\n return [\"heart\", \"gpxtpx:hr\", \"hr\"].includes(name) ? \"heart\" : name;\n}\n\nfunction parseNumeric(val: string) {\n const num = Number.parseFloat(val);\n return Number.isNaN(num) ? val : num;\n}\n", "import type { Position } from \"geojson\";\nimport { get1, nodeVal, num1 } from \"../shared\";\nimport { type ExtendedValues, getExtensions } from \"./extensions\";\n\ninterface CoordPair {\n coordinates: Position;\n time: string | null;\n extendedValues: ExtendedValues;\n}\n\nexport function coordPair(node: Element): CoordPair | null {\n const ll = [\n Number.parseFloat(node.getAttribute(\"lon\") || \"\"),\n Number.parseFloat(node.getAttribute(\"lat\") || \"\"),\n ];\n\n if (Number.isNaN(ll[0]) || Number.isNaN(ll[1])) {\n return null;\n }\n\n num1(node, \"ele\", (val) => {\n ll.push(val);\n });\n\n const time = get1(node, \"time\");\n return {\n coordinates: ll,\n time: time ? nodeVal(time) : null,\n extendedValues: getExtensions(get1(node, \"extensions\")),\n };\n}\n", "import { $num, type P, get, val1 } from \"../shared\";\n\nexport function getLineStyle(node: Element | null) {\n return get(node, \"line\", (lineStyle) => {\n const val: P = Object.assign(\n {},\n val1(lineStyle, \"color\", (color) => {\n return { stroke: `#${color}` };\n }),\n $num(lineStyle, \"opacity\", (opacity) => {\n return { \"stroke-opacity\": opacity };\n }),\n $num(lineStyle, \"width\", (width) => {\n // GPX width is in mm, convert to px with 96 px per inch\n return { \"stroke-width\": (width * 96) / 25.4 };\n })\n );\n return val;\n });\n}\n", "import { $, type NS, getMulti, nodeVal } from \"../shared\";\n\nexport function extractProperties(ns: NS, node: Element) {\n const properties = getMulti(node, [\n \"name\",\n \"cmt\",\n \"desc\",\n \"type\",\n \"time\",\n \"keywords\",\n ]);\n\n for (const [n, url] of ns) {\n for (const child of Array.from(node.getElementsByTagNameNS(url, \"*\"))) {\n properties[child.tagName.replace(\":\", \"_\")] = nodeVal(child)?.trim();\n }\n }\n\n const links = $(node, \"link\");\n if (links.length) {\n properties.links = links.map((link) =>\n Object.assign(\n { href: link.getAttribute(\"href\") },\n getMulti(link, [\"text\", \"type\"])\n )\n );\n }\n\n return properties;\n}\n", "import type { Document as XDocument } from \"@xmldom/xmldom\";\nimport type {\n Feature,\n FeatureCollection,\n LineString,\n MultiLineString,\n Point,\n Position,\n} from \"geojson\";\nimport { coordPair } from \"./gpx/coord_pair\";\nimport { getLineStyle } from \"./gpx/line\";\nimport { extractProperties } from \"./gpx/properties\";\nimport { $, type NS, type P, get1, getMulti } from \"./shared\";\n\n/**\n * Extract points from a trkseg or rte element.\n */\nfunction getPoints(node: Element, pointname: \"trkpt\" | \"rtept\") {\n const pts = $(node, pointname);\n const line: Position[] = [];\n const times = [];\n const extendedValues: P = {};\n\n for (let i = 0; i < pts.length; i++) {\n const c = coordPair(pts[i]);\n if (!c) {\n continue;\n }\n line.push(c.coordinates);\n if (c.time) times.push(c.time);\n for (const [name, val] of c.extendedValues) {\n const plural =\n name === \"heart\" ? name : `${name.replace(\"gpxtpx:\", \"\")}s`;\n if (!extendedValues[plural]) {\n extendedValues[plural] = Array(pts.length).fill(null);\n }\n extendedValues[plural][i] = val;\n }\n }\n\n if (line.length < 2) return; // Invalid line in GeoJSON\n\n return {\n line: line,\n times: times,\n extendedValues: extendedValues,\n };\n}\n\n/**\n * Extract a LineString geometry from a rte\n * element.\n */\nfunction getRoute(ns: NS, node: Element): Feature | undefined {\n const line = getPoints(node, \"rtept\");\n if (!line) return;\n return {\n type: \"Feature\",\n properties: Object.assign(\n { _gpxType: \"rte\" },\n extractProperties(ns, node),\n getLineStyle(get1(node, \"extensions\"))\n ),\n geometry: {\n type: \"LineString\",\n coordinates: line.line,\n },\n };\n}\n\nfunction getTrack(\n ns: NS,\n node: Element\n): Feature | null {\n const segments = $(node, \"trkseg\");\n const track = [];\n const times = [];\n const extractedLines = [];\n\n for (const segment of segments) {\n const line = getPoints(segment, \"trkpt\");\n if (line) {\n extractedLines.push(line);\n if (line.times?.length) times.push(line.times);\n }\n }\n\n if (extractedLines.length === 0) return null;\n\n const multi = extractedLines.length > 1;\n\n const properties: Feature[\"properties\"] = Object.assign(\n { _gpxType: \"trk\" },\n extractProperties(ns, node),\n getLineStyle(get1(node, \"extensions\")),\n times.length\n ? {\n coordinateProperties: {\n times: multi ? times : times[0],\n },\n }\n : {}\n );\n\n for (let i = 0; i < extractedLines.length; i++) {\n const line = extractedLines[i];\n track.push(line.line);\n if (!properties.coordinateProperties) {\n properties.coordinateProperties = {};\n }\n const props = properties.coordinateProperties;\n // Generally extendedValues will be things like heart\n // rate, and this is an array like { heart: [100, 101...] }\n for (const [name, val] of Object.entries(line.extendedValues)) {\n if (multi) {\n if (!props[name]) {\n props[name] = extractedLines.map((line) =>\n new Array(line.line.length).fill(null)\n );\n }\n props[name][i] = val;\n } else {\n props[name] = val;\n }\n }\n }\n\n return {\n type: \"Feature\",\n properties: properties,\n geometry: multi\n ? {\n type: \"MultiLineString\",\n coordinates: track,\n }\n : {\n type: \"LineString\",\n coordinates: track[0],\n },\n };\n}\n\n/**\n * Extract a point, if possible, from a given node,\n * which is usually a wpt or trkpt\n */\nfunction getPoint(ns: NS, node: Element): Feature | null {\n const properties: Feature[\"properties\"] = Object.assign(\n extractProperties(ns, node),\n getMulti(node, [\"sym\"])\n );\n const pair = coordPair(node);\n if (!pair) return null;\n return {\n type: \"Feature\",\n properties,\n geometry: {\n type: \"Point\",\n coordinates: pair.coordinates,\n },\n };\n}\n\n/**\n * Convert GPX to GeoJSON incrementally, returning\n * a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)\n * that yields output feature by feature.\n */\nexport function* gpxGen(node: Document | XDocument): Generator {\n const n = node as Document;\n const GPXX = \"gpxx\";\n const GPXX_URI = \"http://www.garmin.com/xmlschemas/GpxExtensions/v3\";\n // Namespaces\n const ns: NS = [[GPXX, GPXX_URI]];\n const attrs = n.getElementsByTagName(\"gpx\")[0]?.attributes;\n if (attrs) {\n for (const attr of Array.from(attrs)) {\n if (attr.name?.startsWith(\"xmlns:\") && attr.value !== GPXX_URI) {\n ns.push([attr.name, attr.value]);\n }\n }\n }\n\n for (const track of $(n, \"trk\")) {\n const feature = getTrack(ns, track);\n if (feature) yield feature;\n }\n\n for (const route of $(n, \"rte\")) {\n const feature = getRoute(ns, route);\n if (feature) yield feature;\n }\n\n for (const waypoint of $(n, \"wpt\")) {\n const point = getPoint(ns, waypoint);\n if (point) yield point;\n }\n}\n\n/**\n *\n * Convert a GPX document to GeoJSON. The first argument, `doc`, must be a GPX\n * document as an XML DOM - not as a string. You can get this using jQuery's default\n * `.ajax` function or using a bare XMLHttpRequest with the `.response` property\n * holding an XML DOM.\n *\n * The output is a JavaScript object of GeoJSON data, same as `.kml` outputs, with the\n * addition of a `_gpxType` property on each `LineString` feature that indicates whether\n * the feature was encoded as a route (`rte`) or track (`trk`) in the GPX document.\n */\nexport function gpx(node: Document | XDocument): FeatureCollection {\n return {\n type: \"FeatureCollection\",\n features: Array.from(gpxGen(node)),\n };\n}\n", "import type { Document as XDocument } from \"@xmldom/xmldom\";\nimport type { Feature, FeatureCollection, Position } from \"geojson\";\nimport { $, type P, get, get1, nodeVal, num1 } from \"./shared\";\n\ntype PropertyMapping = readonly [string, string][];\n\nconst EXTENSIONS_NS = \"http://www.garmin.com/xmlschemas/ActivityExtension/v2\";\n\nconst TRACKPOINT_ATTRIBUTES: PropertyMapping = [\n [\"heartRate\", \"heartRates\"],\n [\"Cadence\", \"cadences\"],\n // Extended Trackpoint attributes\n [\"Speed\", \"speeds\"],\n [\"Watts\", \"watts\"],\n];\n\nconst LAP_ATTRIBUTES: PropertyMapping = [\n [\"TotalTimeSeconds\", \"totalTimeSeconds\"],\n [\"DistanceMeters\", \"distanceMeters\"],\n [\"MaximumSpeed\", \"maxSpeed\"],\n [\"AverageHeartRateBpm\", \"avgHeartRate\"],\n [\"MaximumHeartRateBpm\", \"maxHeartRate\"],\n\n // Extended Lap attributes\n [\"AvgSpeed\", \"avgSpeed\"],\n [\"AvgWatts\", \"avgWatts\"],\n [\"MaxWatts\", \"maxWatts\"],\n];\n\nfunction getProperties(node: Element, attributeNames: PropertyMapping) {\n const properties = [];\n\n for (const [tag, alias] of attributeNames) {\n let elem = get1(node, tag);\n if (!elem) {\n const elements = node.getElementsByTagNameNS(EXTENSIONS_NS, tag);\n if (elements.length) {\n elem = elements[0];\n }\n }\n const val = Number.parseFloat(nodeVal(elem));\n if (!Number.isNaN(val)) {\n properties.push([alias, val]);\n }\n }\n\n return properties;\n}\n\nfunction coordPair(node: Element) {\n const ll = [num1(node, \"LongitudeDegrees\"), num1(node, \"LatitudeDegrees\")];\n if (\n ll[0] === undefined ||\n Number.isNaN(ll[0]) ||\n ll[1] === undefined ||\n Number.isNaN(ll[1])\n ) {\n return null;\n }\n const heartRate = get1(node, \"HeartRateBpm\");\n const time = nodeVal(get1(node, \"Time\"));\n get1(node, \"AltitudeMeters\", (alt) => {\n const a = Number.parseFloat(nodeVal(alt));\n if (!Number.isNaN(a)) {\n ll.push(a);\n }\n });\n return {\n coordinates: ll as number[],\n time: time || null,\n heartRate: heartRate ? Number.parseFloat(nodeVal(heartRate)) : null,\n extensions: getProperties(node, TRACKPOINT_ATTRIBUTES),\n };\n}\n\nfunction getPoints(node: Element) {\n const pts = $(node, \"Trackpoint\");\n const line: Position[] = [];\n const times = [];\n const heartRates = [];\n if (pts.length < 2) return null; // Invalid line in GeoJSON\n const extendedProperties: P = {};\n const result = { extendedProperties };\n for (let i = 0; i < pts.length; i++) {\n const c = coordPair(pts[i]);\n if (c === null) continue;\n line.push(c.coordinates);\n const { time, heartRate, extensions } = c;\n if (time) times.push(time);\n if (heartRate) heartRates.push(heartRate);\n for (const [alias, value] of extensions) {\n if (!extendedProperties[alias]) {\n extendedProperties[alias] = Array(pts.length).fill(null);\n }\n extendedProperties[alias][i] = value;\n }\n }\n if (line.length < 2) return null;\n return Object.assign(result, {\n line: line,\n times: times,\n heartRates: heartRates,\n });\n}\n\nfunction getLap(node: Element): Feature | null {\n const segments = $(node, \"Track\");\n const track = [];\n const times = [];\n const heartRates = [];\n const allExtendedProperties = [];\n let line: any;\n const properties: P = Object.assign(\n Object.fromEntries(getProperties(node, LAP_ATTRIBUTES)),\n get(node, \"Name\", (nameElement) => {\n return { name: nodeVal(nameElement) };\n })\n );\n\n for (const segment of segments) {\n line = getPoints(segment);\n if (line) {\n track.push(line.line);\n if (line.times.length) times.push(line.times);\n if (line.heartRates.length) heartRates.push(line.heartRates);\n allExtendedProperties.push(line.extendedProperties);\n }\n }\n for (let i = 0; i < allExtendedProperties.length; i++) {\n const extendedProperties = allExtendedProperties[i];\n for (const property in extendedProperties) {\n if (segments.length === 1) {\n if (line) {\n properties[property] = line.extendedProperties[property];\n }\n } else {\n if (!properties[property]) {\n properties[property] = track.map((track) =>\n Array(track.length).fill(null)\n );\n }\n properties[property][i] = extendedProperties[property];\n }\n }\n }\n\n if (track.length === 0) return null;\n\n if (times.length || heartRates.length) {\n properties.coordinateProperties = Object.assign(\n times.length\n ? {\n times: track.length === 1 ? times[0] : times,\n }\n : {},\n heartRates.length\n ? {\n heart: track.length === 1 ? heartRates[0] : heartRates,\n }\n : {}\n );\n }\n\n return {\n type: \"Feature\",\n properties: properties,\n geometry:\n track.length === 1\n ? {\n type: \"LineString\",\n coordinates: track[0],\n }\n : {\n type: \"MultiLineString\",\n coordinates: track,\n },\n };\n}\n\n/**\n * Incrementally convert a TCX document to GeoJSON. The\n * first argument, `doc`, must be a TCX\n * document as an XML DOM - not as a string.\n */\nexport function* tcxGen(node: Document | XDocument): Generator {\n for (const lap of $(node as Document, \"Lap\")) {\n const feature = getLap(lap);\n if (feature) yield feature;\n }\n\n for (const course of $(node as Document, \"Courses\")) {\n const feature = getLap(course);\n if (feature) yield feature;\n }\n}\n\n/**\n * Convert a TCX document to GeoJSON. The first argument, `doc`, must be a TCX\n * document as an XML DOM - not as a string.\n */\nexport function tcx(node: Document | XDocument): FeatureCollection {\n return {\n type: \"FeatureCollection\",\n features: Array.from(tcxGen(node)),\n };\n}\n", "import type { P } from \"../shared\";\n\nexport function fixColor(v: string, prefix: string): P {\n const properties: P = {};\n const colorProp =\n prefix === \"stroke\" || prefix === \"fill\" ? prefix : `${prefix}-color`;\n if (v[0] === \"#\") {\n v = v.substring(1);\n }\n if (v.length === 6 || v.length === 3) {\n properties[colorProp] = `#${v}`;\n } else if (v.length === 8) {\n properties[`${prefix}-opacity`] =\n Number.parseInt(v.substring(0, 2), 16) / 255;\n properties[colorProp] =\n `#${v.substring(6, 8)}${v.substring(4, 6)}${v.substring(2, 4)}`;\n }\n return properties;\n}\n", "import { type P, get, nodeVal, num1, val1 } from \"../shared\";\nimport { fixColor } from \"./fixColor\";\n\nfunction numericProperty(node: Element, source: string, target: string): P {\n const properties: P = {};\n num1(node, source, (val) => {\n properties[target] = val;\n });\n return properties;\n}\n\nfunction getColor(node: Element, output: string): P {\n return get(node, \"color\", (elem) => fixColor(nodeVal(elem), output));\n}\n\nexport function extractIconHref(node: Element) {\n return get(node, \"Icon\", (icon, properties) => {\n val1(icon, \"href\", (href) => {\n properties.icon = href;\n });\n return properties;\n });\n}\n\nexport function extractIcon(node: Element) {\n return get(node, \"IconStyle\", (iconStyle) => {\n return Object.assign(\n getColor(iconStyle, \"icon\"),\n numericProperty(iconStyle, \"scale\", \"icon-scale\"),\n numericProperty(iconStyle, \"heading\", \"icon-heading\"),\n get(iconStyle, \"hotSpot\", (hotspot) => {\n const left = Number.parseFloat(hotspot.getAttribute(\"x\") || \"\");\n const top = Number.parseFloat(hotspot.getAttribute(\"y\") || \"\");\n const xunits = hotspot.getAttribute(\"xunits\") || \"\";\n const yunits = hotspot.getAttribute(\"yunits\") || \"\";\n if (!Number.isNaN(left) && !Number.isNaN(top))\n return {\n \"icon-offset\": [left, top],\n \"icon-offset-units\": [xunits, yunits],\n };\n return {};\n }),\n extractIconHref(iconStyle)\n );\n });\n}\n\nexport function extractLabel(node: Element) {\n return get(node, \"LabelStyle\", (labelStyle) => {\n return Object.assign(\n getColor(labelStyle, \"label\"),\n numericProperty(labelStyle, \"scale\", \"label-scale\")\n );\n });\n}\n\nexport function extractLine(node: Element) {\n return get(node, \"LineStyle\", (lineStyle) => {\n return Object.assign(\n getColor(lineStyle, \"stroke\"),\n numericProperty(lineStyle, \"width\", \"stroke-width\")\n );\n });\n}\n\nexport function extractPoly(node: Element) {\n return get(node, \"PolyStyle\", (polyStyle, properties) => {\n return Object.assign(\n properties,\n get(polyStyle, \"color\", (elem) => fixColor(nodeVal(elem), \"fill\")),\n val1(polyStyle, \"fill\", (fill) => {\n if (fill === \"0\") return { \"fill-opacity\": 0 };\n }),\n val1(polyStyle, \"outline\", (outline) => {\n if (outline === \"0\") return { \"stroke-opacity\": 0 };\n })\n );\n });\n}\n\nexport function extractStyle(node: Element) {\n return Object.assign(\n {},\n extractPoly(node),\n extractLine(node),\n extractLabel(node),\n extractIcon(node)\n );\n}\n", "import type { Geometry, LineString, Point, Position } from \"geojson\";\nimport { $, $ns, get1, isElement, nodeVal } from \"../shared\";\n\nconst removeSpace = /\\s*/g;\nconst trimSpace = /^\\s*|\\s*$/g;\nconst splitSpace = /\\s+/;\n\n/**\n * Get one coordinate from a coordinate array, if any\n */\nexport function coord1(value: string): Position {\n return value\n .replace(removeSpace, \"\")\n .split(\",\")\n .map(Number.parseFloat)\n .filter((num) => !Number.isNaN(num))\n .slice(0, 3);\n}\n\n/**\n * Get all coordinates from a coordinate array as [[],[]]\n */\nexport function coord(value: string): Position[] {\n return value\n .replace(trimSpace, \"\")\n .split(splitSpace)\n .map(coord1)\n .filter((coord) => {\n return coord.length >= 2;\n });\n}\n\nfunction gxCoords(\n node: Element\n): { geometry: Point | LineString; times: string[] } | null {\n let elems = $(node, \"coord\");\n if (elems.length === 0) {\n elems = $ns(node, \"coord\", \"*\");\n }\n\n const coordinates = elems.map((elem) => {\n return nodeVal(elem).split(\" \").map(Number.parseFloat);\n });\n\n if (coordinates.length === 0) {\n return null;\n }\n\n return {\n geometry:\n coordinates.length > 2\n ? {\n type: \"LineString\",\n coordinates,\n }\n : {\n type: \"Point\",\n coordinates: coordinates[0],\n },\n times: $(node, \"when\").map((elem) => nodeVal(elem)),\n };\n}\n\nexport function fixRing(ring: Position[]) {\n if (ring.length === 0) return ring;\n const first = ring[0];\n const last = ring[ring.length - 1];\n let equal = true;\n for (let i = 0; i < Math.max(first.length, last.length); i++) {\n if (first[i] !== last[i]) {\n equal = false;\n break;\n }\n }\n if (!equal) {\n return ring.concat([ring[0]]);\n }\n return ring;\n}\n\nexport function getCoordinates(node: Element) {\n return nodeVal(get1(node, \"coordinates\"));\n}\n\ninterface GeometriesAndTimes {\n geometries: Geometry[];\n coordTimes: string[][];\n}\n\nexport function getGeometry(node: Element): GeometriesAndTimes {\n let geometries: Geometry[] = [];\n let coordTimes: string[][] = [];\n\n for (let i = 0; i < node.childNodes.length; i++) {\n const child = node.childNodes.item(i);\n if (isElement(child)) {\n switch (child.tagName) {\n case \"MultiGeometry\":\n case \"MultiTrack\":\n case \"gx:MultiTrack\": {\n const childGeometries = getGeometry(child);\n geometries = geometries.concat(childGeometries.geometries);\n coordTimes = coordTimes.concat(childGeometries.coordTimes);\n break;\n }\n\n case \"Point\": {\n const coordinates = coord1(getCoordinates(child));\n if (coordinates.length >= 2) {\n geometries.push({\n type: \"Point\",\n coordinates,\n });\n }\n break;\n }\n case \"LinearRing\":\n case \"LineString\": {\n const coordinates = coord(getCoordinates(child));\n if (coordinates.length >= 2) {\n geometries.push({\n type: \"LineString\",\n coordinates,\n });\n }\n break;\n }\n case \"Polygon\": {\n const coords = [];\n for (const linearRing of $(child, \"LinearRing\")) {\n const ring = fixRing(coord(getCoordinates(linearRing)));\n if (ring.length >= 4) {\n coords.push(ring);\n }\n }\n if (coords.length) {\n geometries.push({\n type: \"Polygon\",\n coordinates: coords,\n });\n }\n break;\n }\n case \"Track\":\n case \"gx:Track\": {\n const gx = gxCoords(child);\n if (!gx) break;\n const { times, geometry } = gx;\n geometries.push(geometry);\n if (times.length) coordTimes.push(times);\n break;\n }\n }\n }\n }\n\n return {\n geometries,\n coordTimes,\n };\n}\n", "import {\n $,\n type P,\n type StyleMap,\n get,\n get1,\n nodeVal,\n normalizeId,\n val1,\n} from \"../shared\";\n\nexport type TypeConverter = (x: string) => unknown;\nexport type Schema = { [key: string]: TypeConverter };\n\nconst toNumber: TypeConverter = (x) => Number(x);\nexport const typeConverters: Record = {\n string: (x) => x,\n int: toNumber,\n uint: toNumber,\n short: toNumber,\n ushort: toNumber,\n float: toNumber,\n double: toNumber,\n bool: (x) => Boolean(x),\n};\n\nexport function extractExtendedData(node: Element, schema: Schema) {\n return get(node, \"ExtendedData\", (extendedData, properties) => {\n for (const data of $(extendedData, \"Data\")) {\n properties[data.getAttribute(\"name\") || \"\"] = nodeVal(\n get1(data, \"value\")\n );\n }\n for (const simpleData of $(extendedData, \"SimpleData\")) {\n const name = simpleData.getAttribute(\"name\") || \"\";\n const typeConverter = schema[name] || typeConverters.string;\n properties[name] = typeConverter(nodeVal(simpleData));\n }\n return properties;\n });\n}\n\nexport function getMaybeHTMLDescription(node: Element) {\n const descriptionNode = get1(node, \"description\");\n for (const c of Array.from(descriptionNode?.childNodes || [])) {\n if (c.nodeType === 4) {\n return {\n description: {\n \"@type\": \"html\",\n value: nodeVal(c as Element),\n },\n };\n }\n }\n return {};\n}\n\nexport function extractTimeSpan(node: Element): P {\n return get(node, \"TimeSpan\", (timeSpan) => {\n return {\n timespan: {\n begin: nodeVal(get1(timeSpan, \"begin\")),\n end: nodeVal(get1(timeSpan, \"end\")),\n },\n };\n });\n}\n\nexport function extractTimeStamp(node: Element): P {\n return get(node, \"TimeStamp\", (timeStamp) => {\n return { timestamp: nodeVal(get1(timeStamp, \"when\")) };\n });\n}\n\nexport function extractCascadedStyle(node: Element, styleMap: StyleMap): P {\n return val1(node, \"styleUrl\", (styleUrl) => {\n styleUrl = normalizeId(styleUrl);\n if (styleMap[styleUrl]) {\n return Object.assign({ styleUrl }, styleMap[styleUrl]);\n }\n // For backward-compatibility. Should we still include\n // styleUrl even if it's not resolved?\n return { styleUrl };\n });\n}\n\nexport enum AltitudeMode {\n ABSOLUTE = \"absolute\",\n RELATIVE_TO_GROUND = \"relativeToGround\",\n CLAMP_TO_GROUND = \"clampToGround\",\n CLAMP_TO_SEAFLOOR = \"clampToSeaFloor\",\n RELATIVE_TO_SEAFLOOR = \"relativeToSeaFloor\",\n}\n\nexport function processAltitudeMode(mode: Element | null): AltitudeMode | null {\n switch (mode?.textContent) {\n case AltitudeMode.ABSOLUTE:\n return AltitudeMode.ABSOLUTE;\n case AltitudeMode.CLAMP_TO_GROUND:\n return AltitudeMode.CLAMP_TO_GROUND;\n case AltitudeMode.CLAMP_TO_SEAFLOOR:\n return AltitudeMode.CLAMP_TO_SEAFLOOR;\n case AltitudeMode.RELATIVE_TO_GROUND:\n return AltitudeMode.RELATIVE_TO_GROUND;\n case AltitudeMode.RELATIVE_TO_SEAFLOOR:\n return AltitudeMode.RELATIVE_TO_SEAFLOOR;\n default:\n break;\n }\n return null;\n}\n\nexport type BBox = [number, number, number, number];\n", "import type { Feature, Polygon } from \"geojson\";\nimport type { KMLOptions } from \"lib/kml\";\nimport { type StyleMap, get1, getMulti, num1 } from \"../shared\";\nimport { extractIconHref, extractStyle } from \"./extractStyle\";\nimport { coord, fixRing, getCoordinates } from \"./geometry\";\nimport {\n type BBox,\n type Schema,\n extractCascadedStyle,\n extractExtendedData,\n extractTimeSpan,\n extractTimeStamp,\n getMaybeHTMLDescription,\n} from \"./shared\";\n\ninterface BoxGeometry {\n bbox?: BBox;\n geometry: Polygon;\n}\n\nfunction getGroundOverlayBox(node: Element): BoxGeometry | null {\n const latLonQuad = get1(node, \"gx:LatLonQuad\");\n\n if (latLonQuad) {\n const ring = fixRing(coord(getCoordinates(node)));\n return {\n geometry: {\n type: \"Polygon\",\n coordinates: [ring],\n },\n };\n }\n\n return getLatLonBox(node);\n}\n\nconst DEGREES_TO_RADIANS = Math.PI / 180;\n\nfunction rotateBox(\n bbox: BBox,\n coordinates: Polygon[\"coordinates\"],\n rotation: number\n): Polygon[\"coordinates\"] {\n const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];\n\n return [\n coordinates[0].map((coordinate) => {\n const dy = coordinate[1] - center[1];\n const dx = coordinate[0] - center[0];\n const distance = Math.sqrt(dy ** 2 + dx ** 2);\n const angle = Math.atan2(dy, dx) + rotation * DEGREES_TO_RADIANS;\n\n return [\n center[0] + Math.cos(angle) * distance,\n center[1] + Math.sin(angle) * distance,\n ];\n }),\n ];\n}\n\nfunction getLatLonBox(node: Element): BoxGeometry | null {\n const latLonBox = get1(node, \"LatLonBox\");\n\n if (latLonBox) {\n const north = num1(latLonBox, \"north\");\n const west = num1(latLonBox, \"west\");\n const east = num1(latLonBox, \"east\");\n const south = num1(latLonBox, \"south\");\n const rotation = num1(latLonBox, \"rotation\");\n\n if (\n typeof north === \"number\" &&\n typeof south === \"number\" &&\n typeof west === \"number\" &&\n typeof east === \"number\"\n ) {\n const bbox: BBox = [west, south, east, north];\n let coordinates = [\n [\n [west, north], // top left\n [east, north], // top right\n [east, south], // top right\n [west, south], // bottom left\n [west, north], // top left (again)\n ],\n ];\n if (typeof rotation === \"number\") {\n coordinates = rotateBox(bbox, coordinates, rotation);\n }\n return {\n bbox,\n geometry: {\n type: \"Polygon\",\n coordinates,\n },\n };\n }\n }\n\n return null;\n}\n\nexport function getGroundOverlay(\n node: Element,\n styleMap: StyleMap,\n schema: Schema,\n options: KMLOptions\n): Feature | null {\n const box = getGroundOverlayBox(node);\n\n const geometry = box?.geometry || null;\n\n if (!geometry && options.skipNullGeometry) {\n return null;\n }\n\n const feature: Feature = {\n type: \"Feature\",\n geometry,\n properties: Object.assign(\n /**\n * Related to\n * https://gist.github.com/tmcw/037a1cb6660d74a392e9da7446540f46\n */\n { \"@geometry-type\": \"groundoverlay\" },\n getMulti(node, [\n \"name\",\n \"address\",\n \"visibility\",\n \"open\",\n \"phoneNumber\",\n \"description\",\n ]),\n getMaybeHTMLDescription(node),\n extractCascadedStyle(node, styleMap),\n extractStyle(node),\n extractIconHref(node),\n extractExtendedData(node, schema),\n extractTimeSpan(node),\n extractTimeStamp(node)\n ),\n };\n\n if (box?.bbox) {\n feature.bbox = box.bbox;\n }\n\n if (feature.properties?.visibility !== undefined) {\n feature.properties.visibility = feature.properties.visibility !== \"0\";\n }\n\n const id = node.getAttribute(\"id\");\n if (id !== null && id !== \"\") feature.id = id;\n return feature;\n}\n", "import type { Feature, Polygon } from \"geojson\";\nimport type { KMLOptions } from \"lib/kml\";\nimport { type StyleMap, get1, getMulti, num1 } from \"../shared\";\nimport { extractIconHref, extractStyle } from \"./extractStyle\";\nimport {\n AltitudeMode,\n type BBox,\n type Schema,\n extractCascadedStyle,\n extractExtendedData,\n extractTimeSpan,\n extractTimeStamp,\n getMaybeHTMLDescription,\n processAltitudeMode,\n} from \"./shared\";\n\ninterface BoxGeometry {\n bbox?: BBox;\n geometry: Polygon;\n}\n\ntype LOD = [number, number | null, number | null, number | null];\ninterface IRegion {\n coordinateBox: BoxGeometry | null;\n lod: LOD | null;\n}\n\nfunction getNetworkLinkRegion(node: Element): IRegion | null {\n const region = get1(node, \"Region\");\n\n if (region) {\n return {\n coordinateBox: getLatLonAltBox(region),\n lod: getLod(node),\n };\n }\n return null;\n}\n\nfunction getLod(node: Element): LOD | null {\n const lod = get1(node, \"Lod\");\n\n if (lod) {\n return [\n num1(lod, \"minLodPixels\") ?? -1,\n num1(lod, \"maxLodPixels\") ?? -1,\n num1(lod, \"minFadeExtent\") ?? null,\n num1(lod, \"maxFadeExtent\") ?? null,\n ];\n }\n\n return null;\n}\n\nfunction getLatLonAltBox(node: Element): BoxGeometry | null {\n const latLonAltBox = get1(node, \"LatLonAltBox\");\n\n if (latLonAltBox) {\n const north = num1(latLonAltBox, \"north\");\n const west = num1(latLonAltBox, \"west\");\n const east = num1(latLonAltBox, \"east\");\n const south = num1(latLonAltBox, \"south\");\n const altitudeMode = processAltitudeMode(\n get1(latLonAltBox, \"altitudeMode\") ||\n get1(latLonAltBox, \"gx:altitudeMode\")\n );\n\n if (altitudeMode) {\n console.debug(\n \"Encountered an unsupported feature of KML for togeojson: please contact developers for support of altitude mode.\"\n );\n }\n if (\n typeof north === \"number\" &&\n typeof south === \"number\" &&\n typeof west === \"number\" &&\n typeof east === \"number\"\n ) {\n const bbox: BBox = [west, south, east, north];\n const coordinates = [\n [\n [west, north], // top left\n [east, north], // top right\n [east, south], // top right\n [west, south], // bottom left\n [west, north], // top left (again)\n ],\n ];\n return {\n bbox,\n geometry: {\n type: \"Polygon\",\n coordinates,\n },\n };\n }\n }\n\n return null;\n}\n\nfunction getLinkObject(node: Element) {\n /*\n \n \n ... \n onChange\n \n 4 \n never\n \n 4 \n 1 \n BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]\n \n ... \n \n */\n const linkObj = get1(node, \"Link\");\n\n if (linkObj) {\n return getMulti(linkObj, [\n \"href\",\n \"refreshMode\",\n \"refreshInterval\",\n \"viewRefreshMode\",\n \"viewRefreshTime\",\n \"viewBoundScale\",\n \"viewFormat\",\n \"httpQuery\",\n ]);\n }\n\n return {};\n}\n\nexport function getNetworkLink(\n node: Element,\n styleMap: StyleMap,\n schema: Schema,\n options: KMLOptions\n): Feature | null {\n const box = getNetworkLinkRegion(node);\n\n const geometry = box?.coordinateBox?.geometry || null;\n\n if (!geometry && options.skipNullGeometry) {\n return null;\n }\n\n const feature: Feature = {\n type: \"Feature\",\n geometry,\n properties: Object.assign(\n /**\n * Related to\n * https://gist.github.com/tmcw/037a1cb6660d74a392e9da7446540f46\n */\n { \"@geometry-type\": \"networklink\" },\n getMulti(node, [\n \"name\",\n \"address\",\n \"visibility\",\n \"open\",\n \"phoneNumber\",\n \"styleUrl\",\n \"refreshVisibility\",\n \"flyToView\",\n \"description\",\n ]),\n getMaybeHTMLDescription(node),\n extractCascadedStyle(node, styleMap),\n extractStyle(node),\n extractIconHref(node),\n extractExtendedData(node, schema),\n extractTimeSpan(node),\n extractTimeStamp(node),\n getLinkObject(node),\n box?.lod ? { lod: box.lod } : {}\n ),\n };\n\n if (box?.coordinateBox?.bbox) {\n feature.bbox = box.coordinateBox.bbox;\n }\n\n if (feature.properties?.visibility !== undefined) {\n feature.properties.visibility = feature.properties.visibility !== \"0\";\n }\n\n const id = node.getAttribute(\"id\");\n if (id !== null && id !== \"\") feature.id = id;\n return feature;\n}\n", "import type { Feature, Geometry } from \"geojson\";\nimport type { KMLOptions } from \"lib/kml\";\nimport { type StyleMap, getMulti } from \"../shared\";\nimport { extractStyle } from \"./extractStyle\";\nimport { getGeometry } from \"./geometry\";\nimport {\n type Schema,\n extractCascadedStyle,\n extractExtendedData,\n extractTimeSpan,\n extractTimeStamp,\n getMaybeHTMLDescription,\n} from \"./shared\";\n\nfunction geometryListToGeometry(geometries: Geometry[]): Geometry | null {\n return geometries.length === 0\n ? null\n : geometries.length === 1\n ? geometries[0]\n : {\n type: \"GeometryCollection\",\n geometries,\n };\n}\n\nexport function getPlacemark(\n node: Element,\n styleMap: StyleMap,\n schema: Schema,\n options: KMLOptions\n): Feature | null {\n const { coordTimes, geometries } = getGeometry(node);\n\n const geometry = geometryListToGeometry(geometries);\n\n if (!geometry && options.skipNullGeometry) {\n return null;\n }\n\n const feature: Feature = {\n type: \"Feature\",\n geometry,\n properties: Object.assign(\n getMulti(node, [\n \"name\",\n \"address\",\n \"visibility\",\n \"open\",\n \"phoneNumber\",\n \"description\",\n ]),\n getMaybeHTMLDescription(node),\n extractCascadedStyle(node, styleMap),\n extractStyle(node),\n extractExtendedData(node, schema),\n extractTimeSpan(node),\n extractTimeStamp(node),\n coordTimes.length\n ? {\n coordinateProperties: {\n times: coordTimes.length === 1 ? coordTimes[0] : coordTimes,\n },\n }\n : {}\n ),\n };\n\n if (feature.properties?.visibility !== undefined) {\n feature.properties.visibility = feature.properties.visibility !== \"0\";\n }\n\n const id = node.getAttribute(\"id\");\n if (id !== null && id !== \"\") feature.id = id;\n return feature;\n}\n", "import type { Document as XDocument } from \"@xmldom/xmldom\";\nimport type { FeatureCollection, Geometry } from \"geojson\";\nimport { extractStyle } from \"./kml/extractStyle\";\nimport { getGroundOverlay } from \"./kml/ground_overlay\";\nimport { getNetworkLink } from \"./kml/networklink\";\nimport { getPlacemark } from \"./kml/placemark\";\nimport { type Schema, typeConverters } from \"./kml/shared\";\nimport {\n $,\n type F,\n type P,\n type StyleMap,\n isElement,\n nodeVal,\n normalizeId,\n val1,\n} from \"./shared\";\n\n/**\n * Options to customize KML output.\n *\n * The only option currently\n * is `skipNullGeometry`. Both the KML and GeoJSON formats support\n * the idea of features that don't have geometries: in KML,\n * this is a Placemark without a Point, etc element, and in GeoJSON\n * it's a geometry member with a value of `null`.\n *\n * toGeoJSON, by default, translates null geometries in KML to\n * null geometries in GeoJSON. For systems that use GeoJSON but\n * don't support null geometries, you can specify `skipNullGeometry`\n * to omit these features entirely and only include\n * features that have a geometry defined.\n */\nexport interface KMLOptions {\n skipNullGeometry?: boolean;\n}\n\n/**\n * A folder including metadata. Folders\n * may contain other folders or features,\n * or nothing at all.\n */\nexport interface Folder {\n type: \"folder\";\n /**\n * Standard values:\n *\n * * \"name\",\n * * \"visibility\",\n * * \"open\",\n * * \"address\",\n * * \"description\",\n * * \"phoneNumber\",\n * * \"visibility\",\n */\n meta: {\n [key: string]: unknown;\n };\n children: Array;\n}\n\n/**\n * A nested folder structure, represented\n * as a tree with folders and features.\n */\nexport interface Root {\n type: \"root\";\n children: Array;\n}\n\ntype TreeContainer = Root | Folder;\n\nfunction getStyleId(style: Element) {\n let id = style.getAttribute(\"id\");\n const parentNode = style.parentNode;\n if (\n !id &&\n isElement(parentNode) &&\n parentNode.localName === \"CascadingStyle\"\n ) {\n id = parentNode.getAttribute(\"kml:id\") || parentNode.getAttribute(\"id\");\n }\n return normalizeId(id || \"\");\n}\n\nfunction buildStyleMap(node: Document): StyleMap {\n const styleMap: StyleMap = {};\n for (const style of $(node, \"Style\")) {\n styleMap[getStyleId(style)] = extractStyle(style);\n }\n for (const map of $(node, \"StyleMap\")) {\n const id = normalizeId(map.getAttribute(\"id\") || \"\");\n val1(map, \"styleUrl\", (styleUrl) => {\n styleUrl = normalizeId(styleUrl);\n if (styleMap[styleUrl]) {\n styleMap[id] = styleMap[styleUrl];\n }\n });\n }\n return styleMap;\n}\n\nfunction buildSchema(node: Document): Schema {\n const schema: Schema = {};\n for (const field of $(node, \"SimpleField\")) {\n schema[field.getAttribute(\"name\") || \"\"] =\n typeConverters[field.getAttribute(\"type\") || \"\"] || typeConverters.string;\n }\n return schema;\n}\n\nconst FOLDER_PROPS = [\n \"name\",\n \"visibility\",\n \"open\",\n \"address\",\n \"description\",\n \"phoneNumber\",\n \"visibility\",\n] as const;\n\nfunction getFolder(node: Element): Folder {\n const meta: P = {};\n\n for (const child of Array.from(node.childNodes)) {\n if (isElement(child) && FOLDER_PROPS.includes(child.tagName as any)) {\n meta[child.tagName] = nodeVal(child);\n }\n }\n\n return {\n type: \"folder\",\n meta,\n children: [],\n };\n}\n\n/**\n * Yield a nested tree with KML folder structure\n *\n * This generates a tree with the given structure:\n *\n * ```js\n * {\n * \"type\": \"root\",\n * \"children\": [\n * {\n * \"type\": \"folder\",\n * \"meta\": {\n * \"name\": \"Test\"\n * },\n * \"children\": [\n * // ...features and folders\n * ]\n * }\n * // ...features\n * ]\n * }\n * ```\n *\n * ### GroundOverlay\n *\n * GroundOverlay elements are converted into\n * `Feature` objects with `Polygon` geometries,\n * a property like:\n *\n * ```json\n * {\n * \"@geometry-type\": \"groundoverlay\"\n * }\n * ```\n *\n * And the ground overlay's image URL in the `href`\n * property. Ground overlays will need to be displayed\n * with a separate method to other features, depending\n * on which map framework you're using.\n */\nexport function kmlWithFolders(\n node: Document | XDocument,\n options: KMLOptions = {\n skipNullGeometry: false,\n }\n): Root {\n const n = node as Document;\n const styleMap = buildStyleMap(n);\n const schema = buildSchema(n);\n\n // atomic geospatial types supported by KML - MultiGeometry is\n // handled separately\n // all root placemarks in the file\n const placemarks = [];\n const networkLinks = [];\n const tree: Root = { type: \"root\", children: [] };\n\n function traverse(\n node: Document | ChildNode | Element,\n pointer: TreeContainer,\n options: KMLOptions\n ) {\n if (isElement(node)) {\n switch (node.tagName) {\n case \"GroundOverlay\": {\n placemarks.push(node);\n const placemark = getGroundOverlay(node, styleMap, schema, options);\n if (placemark) {\n pointer.children.push(placemark);\n }\n break;\n }\n case \"Placemark\": {\n placemarks.push(node);\n const placemark = getPlacemark(node, styleMap, schema, options);\n if (placemark) {\n pointer.children.push(placemark);\n }\n break;\n }\n case \"Folder\": {\n const folder = getFolder(node);\n pointer.children.push(folder);\n pointer = folder;\n break;\n }\n case \"NetworkLink\": {\n networkLinks.push(node);\n const networkLink = getNetworkLink(node, styleMap, schema, options);\n if (networkLink) {\n pointer.children.push(networkLink);\n }\n break;\n }\n }\n }\n\n if (node.childNodes) {\n for (let i = 0; i < node.childNodes.length; i++) {\n traverse(node.childNodes[i], pointer, options);\n }\n }\n }\n\n traverse(n, tree, options);\n\n return tree;\n}\n\n/**\n * Convert KML to GeoJSON incrementally, returning\n * a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)\n * that yields output feature by feature.\n */\nexport function* kmlGen(\n node: Document | XDocument,\n options: KMLOptions = {\n skipNullGeometry: false,\n }\n): Generator {\n const n = node as Document;\n const styleMap = buildStyleMap(n);\n const schema = buildSchema(n);\n for (const placemark of $(n, \"Placemark\")) {\n const feature = getPlacemark(placemark, styleMap, schema, options);\n if (feature) yield feature;\n }\n for (const groundOverlay of $(n, \"GroundOverlay\")) {\n const feature = getGroundOverlay(groundOverlay, styleMap, schema, options);\n if (feature) yield feature;\n }\n for (const networkLink of $(n, \"NetworkLink\")) {\n const feature = getNetworkLink(networkLink, styleMap, schema, options);\n if (feature) yield feature;\n }\n}\n\n/**\n * Convert a KML document to GeoJSON. The first argument, `doc`, must be a KML\n * document as an XML DOM - not as a string. You can get this using jQuery's default\n * `.ajax` function or using a bare XMLHttpRequest with the `.response` property\n * holding an XML DOM.\n *\n * The output is a JavaScript object of GeoJSON data. You can convert it to a string\n * with [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)\n * or use it directly in libraries.\n */\nexport function kml(\n node: Document | XDocument,\n options: KMLOptions = {\n skipNullGeometry: false,\n }\n): FeatureCollection {\n return {\n type: \"FeatureCollection\",\n features: Array.from(kmlGen(node as Document, options)),\n };\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { geoBounds as d3_geoBounds, geoPath as d3_geoPath } from 'd3-geo';\nimport { text as d3_text } from 'd3-fetch';\nimport { select as d3_select } from 'd3-selection';\n\nimport stringify from 'fast-json-stable-stringify';\nimport { gpx, kml } from '@tmcw/togeojson';\n\nimport { geoExtent, geoPolygonIntersectsPolygon } from '../geo';\nimport { services } from '../services';\nimport { svgPath } from './helpers';\nimport { utilDetect } from '../util/detect';\nimport { utilArrayFlatten, utilArrayUnion, utilHashcode } from '../util';\n\n\nvar _initialized = false;\nvar _enabled = false;\nvar _geojson;\n\n\nexport function svgData(projection, context, dispatch) {\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var _showLabels = true;\n var detected = utilDetect();\n var layer = d3_select(null);\n var _vtService;\n var _fileList;\n var _template;\n var _src;\n\n const supportedFormats = [\n '.gpx',\n '.kml',\n '.geojson',\n '.json'\n ];\n\n\n function init() {\n if (_initialized) return; // run once\n\n _geojson = {};\n _enabled = true;\n\n function over(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n d3_event.dataTransfer.dropEffect = 'copy';\n }\n\n context.container()\n .attr('dropzone', 'copy')\n .on('drop.svgData', function(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n if (!detected.filedrop) return;\n var f = d3_event.dataTransfer.files[0];\n var extension = getExtension(f.name);\n if (!supportedFormats.includes(extension)) return;\n drawData.fileList(d3_event.dataTransfer.files);\n })\n .on('dragenter.svgData', over)\n .on('dragexit.svgData', over)\n .on('dragover.svgData', over);\n\n _initialized = true;\n }\n\n\n function getService() {\n if (services.vectorTile && !_vtService) {\n _vtService = services.vectorTile;\n _vtService.event.on('loadedData', throttledRedraw);\n } else if (!services.vectorTile && _vtService) {\n _vtService = null;\n }\n\n return _vtService;\n }\n\n\n function showLayer() {\n layerOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', layerOff);\n }\n\n\n function layerOn() {\n layer.style('display', 'block');\n }\n\n\n function layerOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n // ensure that all geojson features in a collection have IDs\n function ensureIDs(gj) {\n if (!gj) return null;\n\n if (gj.type === 'FeatureCollection') {\n for (var i = 0; i < gj.features.length; i++) {\n ensureFeatureID(gj.features[i]);\n }\n } else {\n ensureFeatureID(gj);\n }\n return gj;\n }\n\n\n // ensure that each single Feature object has a unique ID\n function ensureFeatureID(feature) {\n if (!feature) return;\n feature.__featurehash__ = utilHashcode(stringify(feature));\n return feature;\n }\n\n\n // Prefer an array of Features instead of a FeatureCollection\n function getFeatures(gj) {\n if (!gj) return [];\n\n if (gj.type === 'FeatureCollection') {\n return gj.features;\n } else {\n return [gj];\n }\n }\n\n\n function featureKey(d) {\n return d.__featurehash__;\n }\n\n\n function isPolygon(d) {\n return d.geometry.type === 'Polygon' || d.geometry.type === 'MultiPolygon';\n }\n\n\n function clipPathID(d) {\n return 'ideditor-data-' + d.__featurehash__ + '-clippath';\n }\n\n\n function featureClasses(d) {\n return [\n 'data' + d.__featurehash__,\n d.geometry.type,\n isPolygon(d) ? 'area' : '',\n d.__layerID__ || ''\n ].filter(Boolean).join(' ');\n }\n\n\n function drawData(selection) {\n var vtService = getService();\n var getPath = svgPath(projection).geojson;\n var getAreaPath = svgPath(projection, null, true).geojson;\n var hasData = drawData.hasData();\n\n layer = selection.selectAll('.layer-mapdata')\n .data(_enabled && hasData ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-mapdata')\n .merge(layer);\n\n var surface = context.surface();\n if (!surface || surface.empty()) return; // not ready to draw yet, starting up\n\n\n // Gather data\n var geoData, polygonData;\n if (_template && vtService) { // fetch data from vector tile service\n var sourceID = _template;\n vtService.loadTiles(sourceID, _template, projection);\n geoData = vtService.data(sourceID, projection);\n } else {\n geoData = getFeatures(_geojson);\n }\n geoData = geoData.filter(getPath);\n polygonData = geoData.filter(isPolygon);\n\n\n // Draw clip paths for polygons\n var clipPaths = surface.selectAll('defs').selectAll('.clipPath-data')\n .data(polygonData, featureKey);\n\n clipPaths.exit()\n .remove();\n\n var clipPathsEnter = clipPaths.enter()\n .append('clipPath')\n .attr('class', 'clipPath-data')\n .attr('id', clipPathID);\n\n clipPathsEnter\n .append('path');\n\n clipPaths.merge(clipPathsEnter)\n .selectAll('path')\n .attr('d', getAreaPath);\n\n\n // Draw fill, shadow, stroke layers\n var datagroups = layer\n .selectAll('g.datagroup')\n .data(['fill', 'shadow', 'stroke']);\n\n datagroups = datagroups.enter()\n .append('g')\n .attr('class', function(d) { return 'datagroup datagroup-' + d; })\n .merge(datagroups);\n\n\n // Draw paths\n var pathData = {\n fill: polygonData,\n shadow: geoData,\n stroke: geoData\n };\n\n var paths = datagroups\n .selectAll('path')\n .data(function(layer) { return pathData[layer]; }, featureKey);\n\n // exit\n paths.exit()\n .remove();\n\n // enter/update\n paths.enter()\n .append('path')\n .attr('class', function(d) {\n var datagroup = this.parentNode.__data__;\n return 'pathdata ' + datagroup + ' ' + featureClasses(d);\n })\n .attr('clip-path', function(d) {\n var datagroup = this.parentNode.__data__;\n return datagroup === 'fill' ? ('url(#' + clipPathID(d) + ')') : null;\n })\n .merge(paths)\n .attr('d', function(d) {\n var datagroup = this.parentNode.__data__;\n return datagroup === 'fill' ? getAreaPath(d) : getPath(d);\n });\n\n\n // Draw labels\n layer\n .call(drawLabels, 'label-halo', geoData)\n .call(drawLabels, 'label', geoData);\n\n\n function drawLabels(selection, textClass, data) {\n var labelPath = d3_geoPath(projection);\n var labelData = data.filter(function(d) {\n return _showLabels && d.properties && (d.properties.desc || d.properties.name);\n });\n\n var labels = selection.selectAll('text.' + textClass)\n .data(labelData, featureKey);\n\n // exit\n labels.exit()\n .remove();\n\n // enter/update\n labels.enter()\n .append('text')\n .attr('class', function(d) { return textClass + ' ' + featureClasses(d); })\n .merge(labels)\n .text(function(d) {\n return d.properties.desc || d.properties.name;\n })\n .attr('x', function(d) {\n var centroid = labelPath.centroid(d);\n return centroid[0] + 11;\n })\n .attr('y', function(d) {\n var centroid = labelPath.centroid(d);\n return centroid[1];\n });\n }\n }\n\n\n function getExtension(fileName) {\n if (!fileName) return;\n\n var re = /\\.(gpx|kml|(geo)?json|png)$/i;\n var match = fileName.toLowerCase().match(re);\n return match && match.length && match[0];\n }\n\n\n function xmlToDom(textdata) {\n return (new DOMParser()).parseFromString(textdata, 'text/xml');\n }\n\n\n function stringifyGeojsonProperties(feature) {\n const properties = feature.properties;\n for (const key in properties) {\n const property = properties[key];\n if (typeof property === 'number' || typeof property === 'boolean' || Array.isArray(property)) {\n properties[key] = property.toString();\n } else if (property === null) {\n properties[key] = 'null';\n } else if (typeof property === 'object') {\n properties[key] = JSON.stringify(property);\n }\n }\n }\n\n\n drawData.setFile = function(extension, data) {\n _template = null;\n _fileList = null;\n _geojson = null;\n _src = null;\n\n var gj;\n switch (extension) {\n case '.gpx':\n gj = gpx(xmlToDom(data));\n break;\n case '.kml':\n gj = kml(xmlToDom(data));\n break;\n case '.geojson':\n case '.json':\n gj = JSON.parse(data);\n if (gj.type === 'FeatureCollection') {\n gj.features.forEach(stringifyGeojsonProperties);\n } else if (gj.type === 'Feature') {\n stringifyGeojsonProperties(gj);\n }\n break;\n }\n\n gj = gj || {};\n if (Object.keys(gj).length) {\n _geojson = ensureIDs(gj);\n _src = extension + ' data file';\n this.fitZoom();\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.showLabels = function(val) {\n if (!arguments.length) return _showLabels;\n\n _showLabels = val;\n return this;\n };\n\n\n drawData.enabled = function(val) {\n if (!arguments.length) return _enabled;\n\n _enabled = val;\n if (_enabled) {\n showLayer();\n } else {\n hideLayer();\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.hasData = function() {\n var gj = _geojson || {};\n return !!(_template || Object.keys(gj).length);\n };\n\n\n drawData.template = function(val, src) {\n if (!arguments.length) return _template;\n\n // test source against OSM imagery blocklists..\n var osm = context.connection();\n if (osm) {\n for (const regex of osm.imageryBlocklists()) {\n if (regex.test(val)) {\n // matches a blocked sources -> do not set template\n return;\n };\n }\n }\n\n _template = val;\n _fileList = null;\n _geojson = null;\n\n // strip off the querystring/hash from the template,\n // it often includes the access token\n _src = src || ('vectortile:' + val.split(/[?#]/)[0]);\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.geojson = function(gj, src) {\n if (!arguments.length) return _geojson;\n\n _template = null;\n _fileList = null;\n _geojson = null;\n _src = null;\n\n gj = gj || {};\n if (Object.keys(gj).length) {\n _geojson = ensureIDs(gj);\n _src = src || 'unknown.geojson';\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.fileList = function(fileList) {\n if (!arguments.length) return _fileList;\n\n _template = null;\n _geojson = null;\n _src = null;\n _fileList = fileList;\n\n if (!fileList || !fileList.length) return this;\n var f = fileList[0];\n var extension = getExtension(f.name);\n var reader = new FileReader();\n reader.onload = (function() {\n return function(e) {\n drawData.setFile(extension, e.target.result);\n };\n })(f);\n\n reader.readAsText(f);\n\n return this;\n };\n\n\n drawData.url = function(url, defaultExtension) {\n _template = null;\n _fileList = null;\n _geojson = null;\n _src = null;\n\n // strip off any querystring/hash from the url before checking extension\n var testUrl = url.split(/[?#]/)[0];\n var extension = getExtension(testUrl) || defaultExtension;\n if (extension) {\n _template = null;\n d3_text(url)\n .then(function(data) {\n drawData.setFile(extension, data);\n })\n .catch(function() {\n /* ignore */\n });\n\n } else {\n drawData.template(url);\n }\n\n return this;\n };\n\n\n drawData.getSrc = function() {\n return _src || '';\n };\n\n\n drawData.fitZoom = function() {\n var features = getFeatures(_geojson);\n if (!features.length) return;\n\n var map = context.map();\n var viewport = map.trimmedExtent().polygon();\n var coords = features.reduce(function(coords, feature) {\n var geom = feature.geometry;\n if (!geom) return coords;\n\n var c = geom.coordinates;\n\n /* eslint-disable no-fallthrough */\n switch (geom.type) {\n case 'Point':\n c = [c];\n case 'MultiPoint':\n case 'LineString':\n break;\n\n case 'MultiPolygon':\n c = utilArrayFlatten(c);\n case 'Polygon':\n case 'MultiLineString':\n c = utilArrayFlatten(c);\n break;\n }\n /* eslint-enable no-fallthrough */\n\n return utilArrayUnion(coords, c);\n }, []);\n\n if (!geoPolygonIntersectsPolygon(viewport, coords, true)) {\n var extent = geoExtent(d3_geoBounds({ type: 'LineString', coordinates: coords }));\n map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));\n }\n\n return this;\n };\n\n\n init();\n return drawData;\n}\n", "import { fileFetcher } from '../core/file_fetcher';\nimport { svgPath } from './helpers';\n\n\nexport function svgDebug(projection, context) {\n\n function drawDebug(selection) {\n const showTile = context.getDebug('tile');\n const showCollision = context.getDebug('collision');\n const showImagery = context.getDebug('imagery');\n const showTouchTargets = context.getDebug('target');\n const showDownloaded = context.getDebug('downloaded');\n\n let debugData = [];\n if (showTile) {\n debugData.push({ class: 'red', label: 'tile' });\n }\n if (showCollision) {\n debugData.push({ class: 'yellow', label: 'collision' });\n }\n if (showImagery) {\n debugData.push({ class: 'orange', label: 'imagery' });\n }\n if (showTouchTargets) {\n debugData.push({ class: 'pink', label: 'touchTargets' });\n }\n if (showDownloaded) {\n debugData.push({ class: 'purple', label: 'downloaded' });\n }\n\n\n let legend = context.container().select('.main-content')\n .selectAll('.debug-legend')\n .data(debugData.length ? [0] : []);\n\n legend.exit()\n .remove();\n\n legend = legend.enter()\n .append('div')\n .attr('class', 'fillD debug-legend')\n .merge(legend);\n\n\n let legendItems = legend.selectAll('.debug-legend-item')\n .data(debugData, d => d.label);\n\n legendItems.exit()\n .remove();\n\n legendItems.enter()\n .append('span')\n .attr('class', d => `debug-legend-item ${d.class}`)\n .text(d => d.label);\n\n\n let layer = selection.selectAll('.layer-debug')\n .data(showImagery || showDownloaded ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-debug')\n .merge(layer);\n\n\n // imagery\n const extent = context.map().extent();\n fileFetcher.get('imagery')\n .then(d => {\n const hits = (showImagery && d.query.bbox(extent.rectangle(), true)) || [];\n const features = hits.map(d => d.features[d.id]);\n\n let imagery = layer.selectAll('path.debug-imagery')\n .data(features);\n\n imagery.exit()\n .remove();\n\n imagery.enter()\n .append('path')\n .attr('class', 'debug-imagery debug orange');\n })\n .catch(() => { /* ignore */ });\n\n // downloaded\n const osm = context.connection();\n let dataDownloaded = [];\n if (osm && showDownloaded) {\n const rtree = osm.caches('get').tile.rtree;\n dataDownloaded = rtree.all().map(bbox => {\n return {\n type: 'Feature',\n properties: { id: bbox.id },\n geometry: {\n type: 'Polygon',\n coordinates: [[\n [ bbox.minX, bbox.minY ],\n [ bbox.minX, bbox.maxY ],\n [ bbox.maxX, bbox.maxY ],\n [ bbox.maxX, bbox.minY ],\n [ bbox.minX, bbox.minY ]\n ]]\n }\n };\n });\n }\n\n let downloaded = layer\n .selectAll('path.debug-downloaded')\n .data(showDownloaded ? dataDownloaded : []);\n\n downloaded.exit()\n .remove();\n\n downloaded.enter()\n .append('path')\n .attr('class', 'debug-downloaded debug purple');\n\n // update\n layer.selectAll('path')\n .attr('d', svgPath(projection).geojson);\n }\n\n\n // This looks strange because `enabled` methods on other layers are\n // chainable getter/setters, and this one is just a getter.\n drawDebug.enabled = function() {\n if (!arguments.length) {\n return context.getDebug('tile') ||\n context.getDebug('collision') ||\n context.getDebug('imagery') ||\n context.getDebug('target') ||\n context.getDebug('downloaded');\n } else {\n return this;\n }\n };\n\n\n return drawDebug;\n}\n", "import { svg as d3_svg } from 'd3-fetch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { utilArrayUniq } from '../util';\n\n\n/*\n A standalone SVG element that contains only a `defs` sub-element. To be\n used once globally, since defs IDs must be unique within a document.\n*/\nexport function svgDefs(context) {\n\n var _defsSelection = d3_select(null);\n\n var _spritesheetIds = [\n 'iD-sprite', 'maki-sprite', 'temaki-sprite', 'fa-sprite', 'roentgen-sprite', 'community-sprite'\n ];\n\n function drawDefs(selection) {\n _defsSelection = selection.append('defs');\n\n // add markers\n\n // SVG markers have to be given a colour where they're defined\n // (they can't inherit it from the line they're attached to),\n // so we need to manually define markers for each color of tag\n // (also, it's slightly nicer if we can control the\n // positioning for different tags)\n\n /** @param {string} name @param {string} colour */\n function addOnewayMarker(name, colour) {\n _defsSelection\n .append('marker')\n .attr('id', `ideditor-oneway-marker-${name}`)\n .attr('viewBox', '0 0 10 5')\n .attr('refX', 4)\n .attr('refY', 2.5)\n .attr('markerWidth', 2)\n .attr('markerHeight', 2)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'oneway-marker-path')\n .attr('d', 'M 6,3 L 0,3 L 0,2 L 6,2 L 5,0 L 10,2.5 L 5,5 z')\n .attr('stroke', 'none')\n .attr('fill', colour)\n .attr('opacity', '1');\n }\n addOnewayMarker('black', '#333'); // default\n addOnewayMarker('white', '#fff'); // for dark lines (bridges under construction, railways, etc.)\n addOnewayMarker('gray', '#eee'); // for railway lines\n\n\n function addSidedMarker(name, color, offset, style) {\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-sided-marker-' + name)\n .attr('viewBox', '0 0 2 2')\n .attr('refX', 1)\n .attr('refY', -offset)\n .attr('markerWidth', 1.5)\n .attr('markerHeight', 1.5)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'sided-marker-path sided-marker-' + name + '-path')\n .attr('d', style === 'circle'\n ? 'M 0,0.5 a 0.5,0.5 0 1,0 1,0 a 0.5,0.5 0 1,0 -1,0'\n : 'M 0,0 L 1,1 L 2,0 z')\n .attr('stroke', 'none')\n .attr('fill', color);\n }\n addSidedMarker('natural', 'rgb(170, 170, 170)', 0);\n // for a coastline, the arrows are (somewhat unintuitively) on\n // the water side, so let's color them blue (with a gap) to\n // give a stronger indication\n addSidedMarker('coastline', '#77dede', 1);\n addSidedMarker('waterway', '#77dede', 1);\n // barriers have a dashed line, and separating the triangle\n // from the line visually suits that\n addSidedMarker('barrier', '#ddd', 1);\n // dedicated style for guard rails (#9594):\n // marker on opposite side, circles instead of triangles\n addSidedMarker('guard_rail', '#ddd', -1.5, 'circle');\n addSidedMarker('man_made', '#fff', 0);\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z')\n .attr('fill', '#333')\n .attr('fill-opacity', '0.75')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker-wireframe')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z')\n .attr('fill', 'none')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker-side')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 3 14 C 8 13 8 13 13 14 L 8 5 Z')\n .attr('fill', '#333')\n .attr('fill-opacity', '0.75')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker-side-wireframe')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 3 14 C 8 13 8 13 13 14 L 8 5 Z')\n .attr('fill', 'none')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n\n // add patterns\n var patterns = _defsSelection.selectAll('pattern')\n .data([\n // pattern name, pattern image name\n ['beach', 'dots'],\n ['construction', 'construction'],\n ['cemetery', 'cemetery'],\n ['cemetery_christian', 'cemetery_christian'],\n ['cemetery_buddhist', 'cemetery_buddhist'],\n ['cemetery_muslim', 'cemetery_muslim'],\n ['cemetery_jewish', 'cemetery_jewish'],\n ['farmland', 'farmland'],\n ['farmyard', 'farmyard'],\n ['forest', 'forest'],\n ['forest_broadleaved', 'forest_broadleaved'],\n ['forest_needleleaved', 'forest_needleleaved'],\n ['forest_leafless', 'forest_leafless'],\n ['golf_green', 'grass'],\n ['grass', 'grass'],\n ['landfill', 'landfill'],\n ['meadow', 'grass'],\n ['orchard', 'orchard'],\n ['pond', 'pond'],\n ['quarry', 'quarry'],\n ['scrub', 'bushes'],\n ['vineyard', 'vineyard'],\n ['water_standing', 'lines'],\n ['waves', 'waves'],\n ['wetland', 'wetland'],\n ['wetland_marsh', 'wetland_marsh'],\n ['wetland_swamp', 'wetland_swamp'],\n ['wetland_bog', 'wetland_bog'],\n ['wetland_reedbed', 'wetland_reedbed']\n ])\n .enter()\n .append('pattern')\n .attr('id', function (d) { return 'ideditor-pattern-' + d[0]; })\n .attr('width', 32)\n .attr('height', 32)\n .attr('patternUnits', 'userSpaceOnUse');\n\n patterns\n .append('rect')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', 32)\n .attr('height', 32)\n .attr('class', function (d) { return 'pattern-color-' + d[0]; });\n\n patterns\n .append('image')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', 32)\n .attr('height', 32)\n .attr('xlink:href', function (d) {\n return context.imagePath('pattern/' + d[1] + '.png');\n });\n\n // add clip paths\n _defsSelection.selectAll('clipPath')\n .data([12, 18, 20, 32, 45])\n .enter()\n .append('clipPath')\n .attr('id', function (d) { return 'ideditor-clip-square-' + d; })\n .append('rect')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', function (d) { return d; })\n .attr('height', function (d) { return d; });\n\n // add svg filters\n const filters = _defsSelection.selectAll('filter')\n .data(['alpha-slope5'])\n .enter()\n .append('filter')\n .attr('id', d => d);\n // Alters the alpha channel such that everything but\n // (almost) transparent pixels are rendered fully opaque:\n // This is used in a workaround for how chrome is rendering\n // the edges of `img` elements when the page zoom is not a\n // \"round value\": the semi-transparent pixels of neighboring\n // tiles cannot \"add up\" to a fully opaque background layer.\n // See https://github.com/openstreetmap/iD/issues/10747\n // and https://github.com/openstreetmap/iD/pull/10594\n const alphaSlope5 = filters.filter('#alpha-slope5')\n .append('feComponentTransfer');\n alphaSlope5.append('feFuncR').attr('type', 'identity');\n alphaSlope5.append('feFuncG').attr('type', 'identity');\n alphaSlope5.append('feFuncB').attr('type', 'identity');\n alphaSlope5.append('feFuncA')\n .attr('type', 'linear')\n .attr('slope', 5);\n\n // add symbol spritesheets\n addSprites(_spritesheetIds, true);\n }\n\n function addSprites(ids, overrideColors) {\n _spritesheetIds = utilArrayUniq(_spritesheetIds.concat(ids));\n\n var spritesheets = _defsSelection\n .selectAll('.spritesheet')\n .data(_spritesheetIds);\n\n spritesheets\n .enter()\n .append('g')\n .attr('class', function(d) { return 'spritesheet spritesheet-' + d; })\n .each(function(d) {\n var url = context.imagePath(d + '.svg');\n var node = d3_select(this).node();\n\n d3_svg(url)\n .then(function(svg) {\n node.appendChild(\n d3_select(svg.documentElement).attr('id', 'ideditor-' + d).node()\n );\n if (overrideColors && d !== 'iD-sprite') { // allow icon colors to be overridden..\n d3_select(node).selectAll('path')\n .attr('fill', 'currentColor');\n }\n })\n .catch(function() {\n /* ignore */\n });\n });\n\n spritesheets\n .exit()\n .remove();\n }\n\n drawDefs.addSprites = addSprites;\n\n return drawDefs;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { svgPointTransform } from './helpers';\nimport { geoMetersToLat } from '../geo';\n\n\nexport function svgGeolocate(projection) {\n var layer = d3_select(null);\n var _position;\n\n\n function init() {\n if (svgGeolocate.initialized) return; // run once\n svgGeolocate.enabled = false;\n svgGeolocate.initialized = true;\n }\n\n function showLayer() {\n layer.style('display', 'block');\n }\n\n\n function hideLayer() {\n layer\n .transition()\n .duration(250)\n .style('opacity', 0);\n }\n\n function layerOn() {\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1);\n\n }\n\n function layerOff() {\n layer.style('display', 'none');\n }\n\n function transform(d) {\n return svgPointTransform(projection)(d);\n }\n\n function accuracy(accuracy, loc) { // converts accuracy to pixels...\n var degreesRadius = geoMetersToLat(accuracy),\n tangentLoc = [loc[0], loc[1] + degreesRadius],\n projectedTangent = projection(tangentLoc),\n projectedLoc = projection([loc[0], loc[1]]);\n\n // southern most point will have higher pixel value...\n return Math.round(projectedLoc[1] - projectedTangent[1]).toString();\n }\n\n function update() {\n var geolocation = { loc: [_position.coords.longitude, _position.coords.latitude] };\n\n var groups = layer.selectAll('.geolocations').selectAll('.geolocation')\n .data([geolocation]);\n\n groups.exit()\n .remove();\n\n var pointsEnter = groups.enter()\n .append('g')\n .attr('class', 'geolocation');\n\n pointsEnter\n .append('circle')\n .attr('class', 'geolocate-radius')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('fill', 'rgb(15,128,225)')\n .attr('fill-opacity', '0.3')\n .attr('r', '0');\n\n pointsEnter\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('fill', 'rgb(15,128,225)')\n .attr('stroke', 'white')\n .attr('stroke-width', '1.5')\n .attr('r', '6');\n\n groups.merge(pointsEnter)\n .attr('transform', transform);\n\n layer.select('.geolocate-radius').attr('r', accuracy(_position.coords.accuracy, geolocation.loc));\n }\n\n function drawLocation(selection) {\n var enabled = svgGeolocate.enabled;\n\n layer = selection.selectAll('.layer-geolocate')\n .data([0]);\n\n layer.exit()\n .remove();\n\n var layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-geolocate')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'geolocations');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n update();\n } else {\n layerOff();\n }\n }\n\n drawLocation.enabled = function (position, enabled) {\n if (!arguments.length) return svgGeolocate.enabled;\n _position = position;\n svgGeolocate.enabled = enabled;\n if (svgGeolocate.enabled) {\n showLayer();\n layerOn();\n } else {\n hideLayer();\n }\n return this;\n };\n\n init();\n return drawLocation;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { geoPath as d3_geoPath } from 'd3-geo';\nimport RBush from 'rbush';\nimport { localizer } from '../core/localizer';\n\nimport {\n geoExtent, geoPolygonIntersectsPolygon, geoPathLength,\n geoScaleToZoom, geoVecInterp, geoVecLength\n} from '../geo';\nimport { presetManager } from '../presets';\nimport { osmEntity, osmIsInterestingTag } from '../osm';\nimport { utilDetect } from '../util/detect';\nimport { utilArrayDifference, utilArrayUniq, utilDisplayName, utilDisplayNameForPath, utilEntitySelector } from '../util';\n\n\n\nexport function svgLabels(projection, context) {\n var path = d3_geoPath(projection);\n var detected = utilDetect();\n var baselineHack = detected.browser.toLowerCase() === 'safari';\n\n var _rdrawn = new RBush();\n var _rskipped = new RBush();\n var _entitybboxes = {};\n\n // Listed from highest to lowest priority\n const labelStack = [\n ['line', 'aeroway', '*', 12],\n ['line', 'highway', 'motorway', 12],\n ['line', 'highway', 'trunk', 12],\n ['line', 'highway', 'primary', 12],\n ['line', 'highway', 'secondary', 12],\n ['line', 'highway', 'tertiary', 12],\n ['line', 'highway', '*', 12],\n ['line', 'railway', '*', 12],\n ['line', 'waterway', '*', 12],\n ['area', 'aeroway', '*', 12],\n ['area', 'amenity', '*', 12],\n ['area', 'building', '*', 12],\n ['area', 'historic', '*', 12],\n ['area', 'leisure', '*', 12],\n ['area', 'man_made', '*', 12],\n ['area', 'natural', '*', 12],\n ['area', 'shop', '*', 12],\n ['area', 'craft', '*', 12],\n ['area', 'tourism', '*', 12],\n ['area', 'camp_site', '*', 12],\n ['point', 'aeroway', '*', 10],\n ['point', 'amenity', '*', 10],\n ['point', 'building', '*', 10],\n ['point', 'historic', '*', 10],\n ['point', 'leisure', '*', 10],\n ['point', 'man_made', '*', 10],\n ['point', 'natural', '*', 10],\n ['point', 'shop', '*', 10],\n ['point', 'tourism', '*', 10],\n ['point', 'camp_site', '*', 10],\n ['*', 'alt_name', '*', 12],\n ['*', 'official_name', '*', 12],\n ['*', 'loc_name', '*', 12],\n ['*', 'loc_ref', '*', 12],\n ['*', 'unsigned_ref', '*', 12],\n ['*', 'seamark:name', '*', 12],\n ['*', 'sector:name', '*', 12],\n ['*', 'lock_name', '*', 12],\n ['*', 'distance', '*', 12],\n ['*', 'railway:position', '*', 12],\n ['line', 'ref', '*', 12],\n ['area', 'ref', '*', 12],\n ['point', 'ref', '*', 10],\n ['line', 'name', '*', 12],\n ['area', 'name', '*', 12],\n ['point', 'name', '*', 10],\n ['point', 'addr:housenumber', '*', 10],\n ['point', 'addr:housename', '*', 10]\n ];\n\n\n function shouldSkipIcon(preset) {\n var noIcons = ['building', 'landuse', 'natural'];\n return noIcons.some(function(s) {\n return preset.id.indexOf(s) >= 0;\n });\n }\n\n\n function drawLinePaths(selection, labels, filter, classes) {\n var paths = selection.selectAll('path:not(.debug)')\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n paths.exit()\n .remove();\n\n // enter/update\n paths.enter()\n .append('path')\n .style('stroke-width', d => d.position['font-size'])\n .attr('id', d => 'ideditor-labelpath-' + d.entity.id)\n .attr('class', classes)\n .merge(paths)\n .attr('d', d => d.position.lineString);\n }\n\n\n function drawLineLabels(selection, labels, filter, classes) {\n var texts = selection.selectAll('text.' + classes)\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n texts.exit()\n .remove();\n\n // enter\n texts.enter()\n .append('text')\n .attr('class', d => classes + ' ' + d.position.classes + ' ' + d.entity.id)\n .attr('dy', baselineHack ? '0.35em' : null)\n .append('textPath')\n .attr('class', 'textpath');\n\n // update\n selection.selectAll('text.' + classes).selectAll('.textpath')\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity))\n .attr('startOffset', '50%')\n .attr('xlink:href', function(d) { return '#ideditor-labelpath-' + d.entity.id; })\n .text(d => d.name);\n }\n\n\n function drawPointLabels(selection, labels, filter, classes) {\n if (classes.includes('pointlabel-halo')) {\n labels = labels.filter(d => !d.position.isAddr);\n }\n var texts = selection.selectAll('text.' + classes)\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n texts.exit()\n .remove();\n\n // enter/update\n texts.enter()\n .append('text')\n .attr('class', d => classes + ' ' + d.position.classes + ' ' + d.entity.id)\n .style('text-anchor', d => d.position.textAnchor)\n .text(d => d.name)\n .merge(texts)\n .attr('x', d => d.position.x)\n .attr('y', d => d.position.y);\n }\n\n\n function drawAreaLabels(selection, labels, filter, classes) {\n labels = labels.filter(hasText);\n drawPointLabels(selection, labels, filter, classes);\n\n function hasText(d) {\n return d.position.hasOwnProperty('x') && d.position.hasOwnProperty('y');\n }\n }\n\n\n function drawAreaIcons(selection, labels, filter, classes) {\n var icons = selection.selectAll('use.' + classes)\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n icons.exit()\n .remove();\n\n // enter/update\n icons.enter()\n .append('use')\n .attr('class', 'icon ' + classes)\n .attr('width', '17px')\n .attr('height', '17px')\n .merge(icons)\n .attr('transform', d => d.position.transform)\n .attr('xlink:href', function(d) {\n var preset = presetManager.match(d.entity, context.graph());\n var picon = preset && preset.icon;\n return picon ? '#' + picon : '';\n });\n }\n\n\n function drawCollisionBoxes(selection, rtree, which) {\n var classes = 'debug ' + which + ' ' + (which === 'debug-skipped' ? 'orange' : 'yellow');\n\n var gj = [];\n if (context.getDebug('collision')) {\n gj = rtree.all().map(function(d) {\n return { type: 'Polygon', coordinates: [[\n [d.minX, d.minY],\n [d.maxX, d.minY],\n [d.maxX, d.maxY],\n [d.minX, d.maxY],\n [d.minX, d.minY]\n ]]};\n });\n }\n\n var boxes = selection.selectAll('.' + which)\n .data(gj);\n\n // exit\n boxes.exit()\n .remove();\n\n // enter/update\n boxes.enter()\n .append('path')\n .attr('class', classes)\n .merge(boxes)\n .attr('d', d3_geoPath());\n }\n\n\n function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) {\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n\n var labelable = [];\n var renderNodeAs = {};\n var i, j, k, entity, geometry;\n\n for (i = 0; i < labelStack.length; i++) {\n labelable.push([]);\n }\n\n if (fullRedraw) {\n _rdrawn.clear();\n _rskipped.clear();\n _entitybboxes = {};\n\n } else {\n for (i = 0; i < entities.length; i++) {\n entity = entities[i];\n var toRemove = []\n .concat(_entitybboxes[entity.id] || [])\n .concat(_entitybboxes[entity.id + 'I'] || []);\n\n for (j = 0; j < toRemove.length; j++) {\n _rdrawn.remove(toRemove[j]);\n _rskipped.remove(toRemove[j]);\n }\n }\n }\n\n // Loop through all the entities to do some preprocessing\n for (i = 0; i < entities.length; i++) {\n entity = entities[i];\n geometry = entity.geometry(graph);\n\n // Insert collision boxes around interesting points/vertices\n if (geometry === 'point' || (geometry === 'vertex' && isInterestingVertex(entity))) {\n const isAddr = isAddressPoint(entity.tags);\n var hasDirections = entity.directions(graph, projection).length;\n var markerPadding = 0;\n\n if (wireframe) {\n renderNodeAs[entity.id] = { geometry: 'vertex', isAddr };\n } else if (geometry === 'vertex') {\n renderNodeAs[entity.id] = { geometry: 'vertex', isAddr };\n } else if (zoom >= 18 && hasDirections) {\n renderNodeAs[entity.id] = { geometry: 'vertex', isAddr };\n } else {\n renderNodeAs[entity.id] = { geometry: 'point', isAddr };\n markerPadding = 20; // extra y for marker height\n }\n\n if (isAddr) {\n undoInsert(entity.id + 'P');\n } else {\n var coord = projection(entity.loc);\n var nodePadding = 10;\n var bbox = {\n minX: coord[0] - nodePadding,\n minY: coord[1] - nodePadding - markerPadding,\n maxX: coord[0] + nodePadding,\n maxY: coord[1] + nodePadding\n };\n doInsert(bbox, entity.id + 'P');\n }\n }\n\n // From here on, treat vertices like points\n if (geometry === 'vertex') {\n geometry = 'point';\n }\n\n // Determine which entities are label-able\n var preset = geometry === 'area' && presetManager.match(entity, graph);\n var icon = preset && !shouldSkipIcon(preset) && preset.icon;\n\n if (!icon && !utilDisplayName(entity, { isMapLabel: true })) continue;\n\n for (k = 0; k < labelStack.length; k++) {\n var matchGeom = labelStack[k][0];\n var matchKey = labelStack[k][1];\n var matchVal = labelStack[k][2];\n var hasVal = entity.tags[matchKey];\n\n if ((matchGeom === '*' || geometry === matchGeom) && hasVal && (matchVal === '*' || matchVal === hasVal)) {\n labelable[k].push(entity);\n break;\n }\n }\n }\n\n var labelled = {\n point: [],\n line: [],\n area: []\n };\n\n // Try and find a valid label for labellable entities\n for (k = 0; k < labelable.length; k++) {\n var fontSize = labelStack[k][3];\n\n for (i = 0; i < labelable[k].length; i++) {\n entity = labelable[k][i];\n geometry = entity.geometry(graph);\n\n let name = geometry === 'line'\n ? utilDisplayNameForPath(entity)\n : utilDisplayName(entity, { isMapLabel: true });\n var width = name && textWidth(name, fontSize, selection.select('g.layer-osm.labels').node());\n var p = null;\n\n if (geometry === 'point' || geometry === 'vertex') {\n // no point or vertex labels in wireframe mode\n // no vertex labels at low zooms (vertices have no icons)\n if (wireframe) continue;\n var renderAs = renderNodeAs[entity.id];\n if (renderAs.geometry === 'vertex' && zoom < 17) continue;\n while (renderAs.isAddr && width > 36) {\n name = `${name.substring(0, name.replace(/\u2026$/, '').length - 1)}\u2026`;\n width = textWidth(name, fontSize, selection.select('g.layer-osm.labels').node());\n }\n\n p = getPointLabel(entity, width, fontSize, renderAs);\n } else if (geometry === 'line') {\n p = getLineLabel(entity, width, fontSize);\n\n } else if (geometry === 'area') {\n p = getAreaLabel(entity, width, fontSize);\n }\n\n if (p) {\n if (geometry === 'vertex') { geometry = 'point'; } // treat vertex like point\n p.classes = geometry + ' tag-' + labelStack[k][1];\n labelled[geometry].push({\n entity,\n name,\n position: p\n });\n }\n }\n }\n\n\n function isInterestingVertex(entity) {\n var selectedIDs = context.selectedIDs();\n\n return entity.hasInterestingTags() ||\n entity.isEndpoint(graph) ||\n entity.isConnected(graph) ||\n selectedIDs.indexOf(entity.id) !== -1 ||\n graph.parentWays(entity).some(function(parent) {\n return selectedIDs.indexOf(parent.id) !== -1;\n });\n }\n\n\n function getPointLabel(entity, width, height, style) {\n var y = (style.geometry === 'point' ? -12 : 0);\n var pointOffsets = {\n ltr: [15, y, 'start'],\n rtl: [-15, y, 'end']\n };\n const isAddrMarker = style.isAddr && style.geometry !== 'vertex';\n\n var textDirection = localizer.textDirection();\n\n var coord = projection(entity.loc);\n var textPadding = 2;\n var offset = pointOffsets[textDirection];\n if (isAddrMarker) offset = [0, 1, 'middle'];\n var p = {\n height: height,\n width: width,\n x: coord[0] + offset[0],\n y: coord[1] + offset[1],\n textAnchor: offset[2]\n };\n\n // insert a collision box for the text label..\n let bbox;\n if (isAddrMarker) {\n bbox = {\n minX: p.x - (width / 2) - textPadding,\n minY: p.y - (height / 2) - textPadding,\n maxX: p.x + (width / 2) + textPadding,\n maxY: p.y + (height / 2) + textPadding\n };\n } else if (textDirection === 'rtl') {\n bbox = {\n minX: p.x - width - textPadding,\n minY: p.y - (height / 2) - textPadding,\n maxX: p.x + textPadding,\n maxY: p.y + (height / 2) + textPadding\n };\n } else {\n bbox = {\n minX: p.x - textPadding,\n minY: p.y - (height / 2) - textPadding,\n maxX: p.x + width + textPadding,\n maxY: p.y + (height / 2) + textPadding\n };\n }\n\n if (tryInsert([bbox], entity.id, true)) {\n return p;\n }\n }\n\n\n function getLineLabel(entity, width, height) {\n var viewport = geoExtent(context.projection.clipExtent()).polygon();\n var points = graph.childNodes(entity)\n .map(function(node) { return projection(node.loc); });\n var length = geoPathLength(points);\n\n if (length < width + 20) return;\n\n // % along the line to attempt to place the label\n var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70,\n 25, 75, 20, 80, 15, 95, 10, 90, 5, 95];\n var padding = 3;\n\n for (var i = 0; i < lineOffsets.length; i++) {\n var offset = lineOffsets[i];\n var middle = offset / 100 * length;\n var start = middle - width / 2;\n\n if (start < 0 || start + width > length) continue;\n\n // generate subpath and ignore paths that are invalid or don't cross viewport.\n var sub = subpath(points, start, start + width);\n if (!sub || !geoPolygonIntersectsPolygon(viewport, sub, true)) {\n continue;\n }\n\n var isReverse = reverse(sub);\n if (isReverse) {\n sub = sub.reverse();\n }\n\n var bboxes = [];\n var boxsize = (height + 2) / 2;\n\n for (var j = 0; j < sub.length - 1; j++) {\n var a = sub[j];\n var b = sub[j + 1];\n\n // split up the text into small collision boxes\n var num = Math.max(1, Math.floor(geoVecLength(a, b) / boxsize / 2));\n\n for (var box = 0; box < num; box++) {\n var p = geoVecInterp(a, b, box / num);\n var x0 = p[0] - boxsize - padding;\n var y0 = p[1] - boxsize - padding;\n var x1 = p[0] + boxsize + padding;\n var y1 = p[1] + boxsize + padding;\n\n bboxes.push({\n minX: Math.min(x0, x1),\n minY: Math.min(y0, y1),\n maxX: Math.max(x0, x1),\n maxY: Math.max(y0, y1)\n });\n }\n }\n\n if (tryInsert(bboxes, entity.id, false)) { // accept this one\n return {\n 'font-size': height + 2,\n lineString: lineString(sub),\n startOffset: offset + '%'\n };\n }\n }\n\n function reverse(p) {\n var angle = Math.atan2(p[1][1] - p[0][1], p[1][0] - p[0][0]);\n return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2);\n }\n\n function lineString(points) {\n return 'M' + points.join('L');\n }\n\n function subpath(points, from, to) {\n var sofar = 0;\n var start, end, i0, i1;\n\n for (var i = 0; i < points.length - 1; i++) {\n var a = points[i];\n var b = points[i + 1];\n var current = geoVecLength(a, b);\n var portion;\n if (!start && sofar + current >= from) {\n portion = (from - sofar) / current;\n start = [\n a[0] + portion * (b[0] - a[0]),\n a[1] + portion * (b[1] - a[1])\n ];\n i0 = i + 1;\n }\n if (!end && sofar + current >= to) {\n portion = (to - sofar) / current;\n end = [\n a[0] + portion * (b[0] - a[0]),\n a[1] + portion * (b[1] - a[1])\n ];\n i1 = i + 1;\n }\n sofar += current;\n }\n\n var result = points.slice(i0, i1);\n result.unshift(start);\n result.push(end);\n return result;\n }\n }\n\n\n function getAreaLabel(entity, width, height) {\n var centroid = path.centroid(entity.asGeoJSON(graph));\n var extent = entity.extent(graph);\n var areaWidth = projection(extent[1])[0] - projection(extent[0])[0];\n\n if (isNaN(centroid[0]) || areaWidth < 20) return;\n\n var preset = presetManager.match(entity, context.graph());\n var picon = preset && preset.icon;\n var iconSize = 17;\n var padding = 2;\n var p = {};\n\n if (picon && !shouldSkipIcon(preset)) { // icon and label..\n if (addIcon()) {\n addLabel(iconSize + padding);\n return p;\n }\n } else { // label only..\n if (addLabel(0)) {\n return p;\n }\n }\n\n\n function addIcon() {\n var iconX = centroid[0] - (iconSize / 2);\n var iconY = centroid[1] - (iconSize / 2);\n var bbox = {\n minX: iconX,\n minY: iconY,\n maxX: iconX + iconSize,\n maxY: iconY + iconSize\n };\n\n if (tryInsert([bbox], entity.id + 'I', true)) {\n p.transform = 'translate(' + iconX + ',' + iconY + ')';\n return true;\n }\n return false;\n }\n\n function addLabel(yOffset) {\n if (width && areaWidth >= width + 20) {\n var labelX = centroid[0];\n var labelY = centroid[1] + yOffset;\n var bbox = {\n minX: labelX - (width / 2) - padding,\n minY: labelY - (height / 2) - padding,\n maxX: labelX + (width / 2) + padding,\n maxY: labelY + (height / 2) + padding\n };\n\n if (tryInsert([bbox], entity.id, true)) {\n p.x = labelX;\n p.y = labelY;\n p.textAnchor = 'middle';\n p.height = height;\n return true;\n }\n }\n return false;\n }\n }\n\n\n // force insert a singular bounding box\n // singular box only, no array, id better be unique\n function doInsert(bbox, id) {\n bbox.id = id;\n\n var oldbox = _entitybboxes[id];\n if (oldbox) {\n _rdrawn.remove(oldbox);\n }\n _entitybboxes[id] = bbox;\n _rdrawn.insert(bbox);\n }\n\n function undoInsert(id) {\n var oldbox = _entitybboxes[id];\n if (oldbox) {\n _rdrawn.remove(oldbox);\n }\n delete _entitybboxes[id];\n }\n\n function tryInsert(bboxes, id, saveSkipped) {\n var skipped = false;\n\n for (var i = 0; i < bboxes.length; i++) {\n var bbox = bboxes[i];\n bbox.id = id;\n\n // Check that label is visible\n if (bbox.minX < 0 || bbox.minY < 0 || bbox.maxX > dimensions[0] || bbox.maxY > dimensions[1]) {\n skipped = true;\n break;\n }\n if (_rdrawn.collides(bbox)) {\n skipped = true;\n break;\n }\n }\n\n _entitybboxes[id] = bboxes;\n\n if (skipped) {\n if (saveSkipped) {\n _rskipped.load(bboxes);\n }\n } else {\n _rdrawn.load(bboxes);\n }\n\n return !skipped;\n }\n\n\n var layer = selection.selectAll('.layer-osm.labels');\n layer.selectAll('.labels-group')\n .data(['halo', 'label', 'debug'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'labels-group ' + d; });\n\n var halo = layer.selectAll('.labels-group.halo');\n var label = layer.selectAll('.labels-group.label');\n var debug = layer.selectAll('.labels-group.debug');\n\n // points\n drawPointLabels(label, labelled.point, filter, 'pointlabel');\n drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo');\n\n // lines\n drawLinePaths(layer, labelled.line, filter, '');\n drawLineLabels(label, labelled.line, filter, 'linelabel');\n drawLineLabels(halo, labelled.line, filter, 'linelabel-halo');\n\n // areas\n drawAreaLabels(label, labelled.area, filter, 'arealabel');\n drawAreaLabels(halo, labelled.area, filter, 'arealabel-halo');\n drawAreaIcons(label, labelled.area, filter, 'areaicon');\n drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo');\n\n // debug\n drawCollisionBoxes(debug, _rskipped, 'debug-skipped');\n drawCollisionBoxes(debug, _rdrawn, 'debug-drawn');\n\n layer.call(filterLabels);\n }\n\n\n function filterLabels(selection) {\n var drawLayer = selection.selectAll('.layer-osm.labels');\n var layers = drawLayer.selectAll('.labels-group.halo, .labels-group.label');\n\n layers.selectAll('.nolabel')\n .classed('nolabel', false);\n\n const graph = context.graph();\n const mouse = context.map().mouse();\n let bbox;\n let hideIds = [];\n\n // hide labels near the mouse\n if (mouse && context.mode().id !== 'browse') {\n const pad = 20;\n bbox = { minX: mouse[0] - pad, minY: mouse[1] - pad, maxX: mouse[0] + pad, maxY: mouse[1] + pad };\n const nearMouse = _rdrawn.search(bbox)\n .map(entity => entity.id)\n .filter(id =>\n context.mode().id !== 'select' ||\n // in select mode: hide labels of currently selected line(s)\n // to still allow accessing midpoints\n // https://github.com/openstreetmap/iD/issues/11220\n context.mode().selectedIDs().includes(id) && graph.hasEntity(id)?.geometry(graph) === 'line');\n hideIds.push.apply(hideIds, nearMouse);\n hideIds = utilArrayUniq(hideIds);\n }\n\n // don't hide label of currently selected entity while in e.g. drag mode\n const selected = (context.mode()?.selectedIDs?.() || [])\n .filter(id => graph.hasEntity(id)?.geometry(graph) !== 'line');\n hideIds = utilArrayDifference(hideIds, selected);\n\n layers.selectAll(utilEntitySelector(hideIds))\n .classed('nolabel', true);\n\n\n // draw the mouse bbox if debugging is on..\n var debug = selection.selectAll('.labels-group.debug');\n var gj = [];\n if (context.getDebug('collision')) {\n gj = bbox ? [{\n type: 'Polygon',\n coordinates: [[\n [bbox.minX, bbox.minY],\n [bbox.maxX, bbox.minY],\n [bbox.maxX, bbox.maxY],\n [bbox.minX, bbox.maxY],\n [bbox.minX, bbox.minY]\n ]]\n }] : [];\n }\n\n var box = debug.selectAll('.debug-mouse')\n .data(gj);\n\n // exit\n box.exit()\n .remove();\n\n // enter/update\n box.enter()\n .append('path')\n .attr('class', 'debug debug-mouse yellow')\n .merge(box)\n .attr('d', d3_geoPath());\n }\n\n\n var throttleFilterLabels = throttle(filterLabels, 100);\n\n\n drawLabels.observe = function(selection) {\n var listener = function() { throttleFilterLabels(selection); };\n selection.on('mousemove.hidelabels', listener);\n context.on('enter.hidelabels', listener);\n };\n\n\n drawLabels.off = function(selection) {\n throttleFilterLabels.cancel();\n selection.on('mousemove.hidelabels', null);\n context.on('enter.hidelabels', null);\n };\n\n\n return drawLabels;\n}\n\n\nconst _textWidthCache = {};\nexport function textWidth(text, size, container) {\n _textWidthCache[size] ||= {};\n let c = _textWidthCache[size];\n\n if (c[text]) {\n return c[text];\n }\n const elem = document.createElementNS('http://www.w3.org/2000/svg', 'text');\n elem.style.fontSize = `${size}px`;\n elem.style.fontWeight = 'bold';\n elem.textContent = text;\n container.appendChild(elem);\n c[text] = elem.getComputedTextLength();\n elem.remove();\n return c[text];\n}\n\n\nconst nonPrimaryKeys = new Set([\n 'building:flats',\n 'check_date',\n 'fixme',\n 'layer',\n 'level',\n 'level:ref',\n 'note'\n]);\nconst nonPrimaryKeysRegex = /^(ref|survey|note|([^:]+:|old_|alt_)addr):/;\nexport function isAddressPoint(tags) {\n const keys = Object.keys(tags).filter(key =>\n osmIsInterestingTag(key) &&\n !nonPrimaryKeys.has(key) &&\n !nonPrimaryKeysRegex.test(key)\n );\n return keys.length > 0 && keys.every(key =>\n key.startsWith('addr:')\n );\n}\n", "var e=\"undefined\"!=typeof self?self:global;const t=\"undefined\"!=typeof navigator,i=t&&\"undefined\"==typeof HTMLImageElement,n=!(\"undefined\"==typeof global||\"undefined\"==typeof process||!process.versions||!process.versions.node),s=e.Buffer,r=e.BigInt,a=!!s,o=e=>e;function l(e,t=o){if(n)try{return\"function\"==typeof require?Promise.resolve(t(require(e))):import(/* webpackIgnore: true */ e).then(t)}catch(t){console.warn(`Couldn't load ${e}`)}}let h=e.fetch;const u=e=>h=e;if(!e.fetch){const e=l(\"http\",(e=>e)),t=l(\"https\",(e=>e)),i=(n,{headers:s}={})=>new Promise((async(r,a)=>{let{port:o,hostname:l,pathname:h,protocol:u,search:c}=new URL(n);const f={method:\"GET\",hostname:l,path:encodeURI(h)+c,headers:s};\"\"!==o&&(f.port=Number(o));const d=(\"https:\"===u?await t:await e).request(f,(e=>{if(301===e.statusCode||302===e.statusCode){let t=new URL(e.headers.location,n).toString();return i(t,{headers:s}).then(r).catch(a)}r({status:e.statusCode,arrayBuffer:()=>new Promise((t=>{let i=[];e.on(\"data\",(e=>i.push(e))),e.on(\"end\",(()=>t(Buffer.concat(i))))}))})}));d.on(\"error\",a),d.end()}));u(i)}function c(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}const f=e=>p(e)?void 0:e,d=e=>void 0!==e;function p(e){return void 0===e||(e instanceof Map?0===e.size:0===Object.values(e).filter(d).length)}function g(e){let t=new Error(e);throw delete t.stack,t}function m(e){return\"\"===(e=function(e){for(;e.endsWith(\"\\0\");)e=e.slice(0,-1);return e}(e).trim())?void 0:e}function S(e){let t=function(e){let t=0;return e.ifd0.enabled&&(t+=1024),e.exif.enabled&&(t+=2048),e.makerNote&&(t+=2048),e.userComment&&(t+=1024),e.gps.enabled&&(t+=512),e.interop.enabled&&(t+=100),e.ifd1.enabled&&(t+=1024),t+2048}(e);return e.jfif.enabled&&(t+=50),e.xmp.enabled&&(t+=2e4),e.iptc.enabled&&(t+=14e3),e.icc.enabled&&(t+=6e3),t}const C=e=>String.fromCharCode.apply(null,e),y=\"undefined\"!=typeof TextDecoder?new TextDecoder(\"utf-8\"):void 0;function b(e){return y?y.decode(e):a?Buffer.from(e).toString(\"utf8\"):decodeURIComponent(escape(C(e)))}class I{static from(e,t){return e instanceof this&&e.le===t?e:new I(e,void 0,void 0,t)}constructor(e,t=0,i,n){if(\"boolean\"==typeof n&&(this.le=n),Array.isArray(e)&&(e=new Uint8Array(e)),0===e)this.byteOffset=0,this.byteLength=0;else if(e instanceof ArrayBuffer){void 0===i&&(i=e.byteLength-t);let n=new DataView(e,t,i);this._swapDataView(n)}else if(e instanceof Uint8Array||e instanceof DataView||e instanceof I){void 0===i&&(i=e.byteLength-t),(t+=e.byteOffset)+i>e.byteOffset+e.byteLength&&g(\"Creating view outside of available memory in ArrayBuffer\");let n=new DataView(e.buffer,t,i);this._swapDataView(n)}else if(\"number\"==typeof e){let t=new DataView(new ArrayBuffer(e));this._swapDataView(t)}else g(\"Invalid input argument for BufferView: \"+e)}_swapArrayBuffer(e){this._swapDataView(new DataView(e))}_swapBuffer(e){this._swapDataView(new DataView(e.buffer,e.byteOffset,e.byteLength))}_swapDataView(e){this.dataView=e,this.buffer=e.buffer,this.byteOffset=e.byteOffset,this.byteLength=e.byteLength}_lengthToEnd(e){return this.byteLength-e}set(e,t,i=I){return e instanceof DataView||e instanceof I?e=new Uint8Array(e.buffer,e.byteOffset,e.byteLength):e instanceof ArrayBuffer&&(e=new Uint8Array(e)),e instanceof Uint8Array||g(\"BufferView.set(): Invalid data argument.\"),this.toUint8().set(e,t),new i(this,t,e.byteLength)}subarray(e,t){return t=t||this._lengthToEnd(e),new I(this,e,t)}toUint8(){return new Uint8Array(this.buffer,this.byteOffset,this.byteLength)}getUint8Array(e,t){return new Uint8Array(this.buffer,this.byteOffset+e,t)}getString(e=0,t=this.byteLength){return b(this.getUint8Array(e,t))}getLatin1String(e=0,t=this.byteLength){let i=this.getUint8Array(e,t);return C(i)}getUnicodeString(e=0,t=this.byteLength){const i=[];for(let n=0;n1e4?v(e,i,\"base64\"):n&&e.includes(\"://\")?x(e,i,\"url\",M):n?v(e,i,\"fs\"):t?x(e,i,\"url\",M):void g(\"Invalid input argument\");var s}async function x(e,t,i,n){return A.has(i)?v(e,t,i):n?async function(e,t){let i=await t(e);return new I(i)}(e,n):void g(`Parser ${i} is not loaded`)}async function v(e,t,i){let n=new(A.get(i))(e,t);return await n.read(),n}const M=e=>h(e).then((e=>e.arrayBuffer())),R=e=>new Promise(((t,i)=>{let n=new FileReader;n.onloadend=()=>t(n.result||new ArrayBuffer),n.onerror=i,n.readAsArrayBuffer(e)}));class L extends Map{get tagKeys(){return this.allKeys||(this.allKeys=Array.from(this.keys())),this.allKeys}get tagValues(){return this.allValues||(this.allValues=Array.from(this.values())),this.allValues}}function U(e,t,i){let n=new L;for(let[e,t]of i)n.set(e,t);if(Array.isArray(t))for(let i of t)e.set(i,n);else e.set(t,n);return n}function F(e,t,i){let n,s=e.get(t);for(n of i)s.set(n[0],n[1])}const E=new Map,B=new Map,N=new Map,G=[\"chunked\",\"firstChunkSize\",\"firstChunkSizeNode\",\"firstChunkSizeBrowser\",\"chunkSize\",\"chunkLimit\"],V=[\"jfif\",\"xmp\",\"icc\",\"iptc\",\"ihdr\"],z=[\"tiff\",...V],H=[\"ifd0\",\"ifd1\",\"exif\",\"gps\",\"interop\"],j=[...z,...H],W=[\"makerNote\",\"userComment\"],K=[\"translateKeys\",\"translateValues\",\"reviveValues\",\"multiSegment\"],X=[...K,\"sanitize\",\"mergeOutput\",\"silentErrors\"];class _{get translate(){return this.translateKeys||this.translateValues||this.reviveValues}}class Y extends _{get needed(){return this.enabled||this.deps.size>0}constructor(e,t,i,n){if(super(),c(this,\"enabled\",!1),c(this,\"skip\",new Set),c(this,\"pick\",new Set),c(this,\"deps\",new Set),c(this,\"translateKeys\",!1),c(this,\"translateValues\",!1),c(this,\"reviveValues\",!1),this.key=e,this.enabled=t,this.parse=this.enabled,this.applyInheritables(n),this.canBeFiltered=H.includes(e),this.canBeFiltered&&(this.dict=E.get(e)),void 0!==i)if(Array.isArray(i))this.parse=this.enabled=!0,this.canBeFiltered&&i.length>0&&this.translateTagSet(i,this.pick);else if(\"object\"==typeof i){if(this.enabled=!0,this.parse=!1!==i.parse,this.canBeFiltered){let{pick:e,skip:t}=i;e&&e.length>0&&this.translateTagSet(e,this.pick),t&&t.length>0&&this.translateTagSet(t,this.skip)}this.applyInheritables(i)}else!0===i||!1===i?this.parse=this.enabled=i:g(`Invalid options argument: ${i}`)}applyInheritables(e){let t,i;for(t of K)i=e[t],void 0!==i&&(this[t]=i)}translateTagSet(e,t){if(this.dict){let i,n,{tagKeys:s,tagValues:r}=this.dict;for(i of e)\"string\"==typeof i?(n=r.indexOf(i),-1===n&&(n=s.indexOf(Number(i))),-1!==n&&t.add(Number(s[n]))):t.add(i)}else for(let i of e)t.add(i)}finalizeFilters(){!this.enabled&&this.deps.size>0?(this.enabled=!0,ee(this.pick,this.deps)):this.enabled&&this.pick.size>0&&ee(this.pick,this.deps)}}var $={jfif:!1,tiff:!0,xmp:!1,icc:!1,iptc:!1,ifd0:!0,ifd1:!1,exif:!0,gps:!0,interop:!1,ihdr:void 0,makerNote:!1,userComment:!1,multiSegment:!1,skip:[],pick:[],translateKeys:!0,translateValues:!0,reviveValues:!0,sanitize:!0,mergeOutput:!0,silentErrors:!0,chunked:!0,firstChunkSize:void 0,firstChunkSizeNode:512,firstChunkSizeBrowser:65536,chunkSize:65536,chunkLimit:5},J=new Map;class q extends _{static useCached(e){let t=J.get(e);return void 0!==t||(t=new this(e),J.set(e,t)),t}constructor(e){super(),!0===e?this.setupFromTrue():void 0===e?this.setupFromUndefined():Array.isArray(e)?this.setupFromArray(e):\"object\"==typeof e?this.setupFromObject(e):g(`Invalid options argument ${e}`),void 0===this.firstChunkSize&&(this.firstChunkSize=t?this.firstChunkSizeBrowser:this.firstChunkSizeNode),this.mergeOutput&&(this.ifd1.enabled=!1),this.filterNestedSegmentTags(),this.traverseTiffDependencyTree(),this.checkLoadedPlugins()}setupFromUndefined(){let e;for(e of G)this[e]=$[e];for(e of X)this[e]=$[e];for(e of W)this[e]=$[e];for(e of j)this[e]=new Y(e,$[e],void 0,this)}setupFromTrue(){let e;for(e of G)this[e]=$[e];for(e of X)this[e]=$[e];for(e of W)this[e]=!0;for(e of j)this[e]=new Y(e,!0,void 0,this)}setupFromArray(e){let t;for(t of G)this[t]=$[t];for(t of X)this[t]=$[t];for(t of W)this[t]=$[t];for(t of j)this[t]=new Y(t,!1,void 0,this);this.setupGlobalFilters(e,void 0,H)}setupFromObject(e){let t;for(t of(H.ifd0=H.ifd0||H.image,H.ifd1=H.ifd1||H.thumbnail,Object.assign(this,e),G))this[t]=Z(e[t],$[t]);for(t of X)this[t]=Z(e[t],$[t]);for(t of W)this[t]=Z(e[t],$[t]);for(t of z)this[t]=new Y(t,$[t],e[t],this);for(t of H)this[t]=new Y(t,$[t],e[t],this.tiff);this.setupGlobalFilters(e.pick,e.skip,H,j),!0===e.tiff?this.batchEnableWithBool(H,!0):!1===e.tiff?this.batchEnableWithUserValue(H,e):Array.isArray(e.tiff)?this.setupGlobalFilters(e.tiff,void 0,H):\"object\"==typeof e.tiff&&this.setupGlobalFilters(e.tiff.pick,e.tiff.skip,H)}batchEnableWithBool(e,t){for(let i of e)this[i].enabled=t}batchEnableWithUserValue(e,t){for(let i of e){let e=t[i];this[i].enabled=!1!==e&&void 0!==e}}setupGlobalFilters(e,t,i,n=i){if(e&&e.length){for(let e of n)this[e].enabled=!1;let t=Q(e,i);for(let[e,i]of t)ee(this[e].pick,i),this[e].enabled=!0}else if(t&&t.length){let e=Q(t,i);for(let[t,i]of e)ee(this[t].skip,i)}}filterNestedSegmentTags(){let{ifd0:e,exif:t,xmp:i,iptc:n,icc:s}=this;this.makerNote?t.deps.add(37500):t.skip.add(37500),this.userComment?t.deps.add(37510):t.skip.add(37510),i.enabled||e.skip.add(700),n.enabled||e.skip.add(33723),s.enabled||e.skip.add(34675)}traverseTiffDependencyTree(){let{ifd0:e,exif:t,gps:i,interop:n}=this;n.needed&&(t.deps.add(40965),e.deps.add(40965)),t.needed&&e.deps.add(34665),i.needed&&e.deps.add(34853),this.tiff.enabled=H.some((e=>!0===this[e].enabled))||this.makerNote||this.userComment;for(let e of H)this[e].finalizeFilters()}get onlyTiff(){return!V.map((e=>this[e].enabled)).some((e=>!0===e))&&this.tiff.enabled}checkLoadedPlugins(){for(let e of z)this[e].enabled&&!T.has(e)&&P(\"segment parser\",e)}}function Q(e,t){let i,n,s,r,a=[];for(s of t){for(r of(i=E.get(s),n=[],i))(e.includes(r[0])||e.includes(r[1]))&&n.push(r[0]);n.length&&a.push([s,n])}return a}function Z(e,t){return void 0!==e?e:void 0!==t?t:void 0}function ee(e,t){for(let i of t)e.add(i)}c(q,\"default\",$);class te{constructor(e){c(this,\"parsers\",{}),c(this,\"output\",{}),c(this,\"errors\",[]),c(this,\"pushToErrors\",(e=>this.errors.push(e))),this.options=q.useCached(e)}async read(e){this.file=await D(e,this.options)}setup(){if(this.fileParser)return;let{file:e}=this,t=e.getUint16(0);for(let[i,n]of w)if(n.canHandle(e,t))return this.fileParser=new n(this.options,this.file,this.parsers),e[i]=!0;this.file.close&&this.file.close(),g(\"Unknown file format\")}async parse(){let{output:e,errors:t}=this;return this.setup(),this.options.silentErrors?(await this.executeParsers().catch(this.pushToErrors),t.push(...this.fileParser.errors)):await this.executeParsers(),this.file.close&&this.file.close(),this.options.silentErrors&&t.length>0&&(e.errors=t),f(e)}async executeParsers(){let{output:e}=this;await this.fileParser.parse();let t=Object.values(this.parsers).map((async t=>{let i=await t.parse();t.assignToOutput(e,i)}));this.options.silentErrors&&(t=t.map((e=>e.catch(this.pushToErrors)))),await Promise.all(t)}async extractThumbnail(){this.setup();let{options:e,file:t}=this,i=T.get(\"tiff\",e);var n;if(t.tiff?n={start:0,type:\"tiff\"}:t.jpeg&&(n=await this.fileParser.getOrFindSegment(\"tiff\")),void 0===n)return;let s=await this.fileParser.ensureSegmentChunk(n),r=this.parsers.tiff=new i(s,e,t),a=await r.extractThumbnail();return t.close&&t.close(),a}}async function ie(e,t){let i=new te(t);return await i.read(e),i.parse()}var ne=Object.freeze({__proto__:null,parse:ie,Exifr:te,fileParsers:w,segmentParsers:T,fileReaders:A,tagKeys:E,tagValues:B,tagRevivers:N,createDictionary:U,extendDictionary:F,fetchUrlAsArrayBuffer:M,readBlobAsArrayBuffer:R,chunkedProps:G,otherSegments:V,segments:z,tiffBlocks:H,segmentsAndBlocks:j,tiffExtractables:W,inheritables:K,allFormatters:X,Options:q});class se{constructor(e,t,i){c(this,\"errors\",[]),c(this,\"ensureSegmentChunk\",(async e=>{let t=e.start,i=e.size||65536;if(this.file.chunked)if(this.file.available(t,i))e.chunk=this.file.subarray(t,i);else try{e.chunk=await this.file.readChunk(t,i)}catch(t){g(`Couldn't read segment: ${JSON.stringify(e)}. ${t.message}`)}else this.file.byteLength>t+i?e.chunk=this.file.subarray(t,i):void 0===e.size?e.chunk=this.file.subarray(t):g(\"Segment unreachable: \"+JSON.stringify(e));return e.chunk})),this.extendOptions&&this.extendOptions(e),this.options=e,this.file=t,this.parsers=i}injectSegment(e,t){this.options[e].enabled&&this.createParser(e,t)}createParser(e,t){let i=new(T.get(e))(t,this.options,this.file);return this.parsers[e]=i}createParsers(e){for(let t of e){let{type:e,chunk:i}=t,n=this.options[e];if(n&&n.enabled){let t=this.parsers[e];t&&t.append||t||this.createParser(e,i)}}}async readSegments(e){let t=e.map(this.ensureSegmentChunk);await Promise.all(t)}}class re{static findPosition(e,t){let i=e.getUint16(t+2)+2,n=\"function\"==typeof this.headerLength?this.headerLength(e,t,i):this.headerLength,s=t+n,r=i-n;return{offset:t,length:i,headerLength:n,start:s,size:r,end:s+r}}static parse(e,t={}){return new this(e,new q({[this.type]:t}),e).parse()}normalizeInput(e){return e instanceof I?e:new I(e)}constructor(e,t={},i){c(this,\"errors\",[]),c(this,\"raw\",new Map),c(this,\"handleError\",(e=>{if(!this.options.silentErrors)throw e;this.errors.push(e.message)})),this.chunk=this.normalizeInput(e),this.file=i,this.type=this.constructor.type,this.globalOptions=this.options=t,this.localOptions=t[this.type],this.canTranslate=this.localOptions&&this.localOptions.translate}translate(){this.canTranslate&&(this.translated=this.translateBlock(this.raw,this.type))}get output(){return this.translated?this.translated:this.raw?Object.fromEntries(this.raw):void 0}translateBlock(e,t){let i=N.get(t),n=B.get(t),s=E.get(t),r=this.options[t],a=r.reviveValues&&!!i,o=r.translateValues&&!!n,l=r.translateKeys&&!!s,h={};for(let[t,r]of e)a&&i.has(t)?r=i.get(t)(r):o&&n.has(t)&&(r=this.translateValue(r,n.get(t))),l&&s.has(t)&&(t=s.get(t)||t),h[t]=r;return h}translateValue(e,t){return t[e]||t.DEFAULT||e}assignToOutput(e,t){this.assignObjectToOutput(e,this.constructor.type,t)}assignObjectToOutput(e,t,i){if(this.globalOptions.mergeOutput)return Object.assign(e,i);e[t]?Object.assign(e[t],i):e[t]=i}}c(re,\"headerLength\",4),c(re,\"type\",void 0),c(re,\"multiSegment\",!1),c(re,\"canHandle\",(()=>!1));function ae(e){return 192===e||194===e||196===e||219===e||221===e||218===e||254===e}function oe(e){return e>=224&&e<=239}function le(e,t,i){for(let[n,s]of T)if(s.canHandle(e,t,i))return n}class he extends se{constructor(...e){super(...e),c(this,\"appSegments\",[]),c(this,\"jpegSegments\",[]),c(this,\"unknownSegments\",[])}static canHandle(e,t){return 65496===t}async parse(){await this.findAppSegments(),await this.readSegments(this.appSegments),this.mergeMultiSegments(),this.createParsers(this.mergedAppSegments||this.appSegments)}setupSegmentFinderArgs(e){!0===e?(this.findAll=!0,this.wanted=new Set(T.keyList())):(e=void 0===e?T.keyList().filter((e=>this.options[e].enabled)):e.filter((e=>this.options[e].enabled&&T.has(e))),this.findAll=!1,this.remaining=new Set(e),this.wanted=new Set(e)),this.unfinishedMultiSegment=!1}async findAppSegments(e=0,t){this.setupSegmentFinderArgs(t);let{file:i,findAll:n,wanted:s,remaining:r}=this;if(!n&&this.file.chunked&&(n=Array.from(s).some((e=>{let t=T.get(e),i=this.options[e];return t.multiSegment&&i.multiSegment})),n&&await this.file.readWhole()),e=this.findAppSegmentsInRange(e,i.byteLength),!this.options.onlyTiff&&i.chunked){let t=!1;for(;r.size>0&&!t&&(i.canReadNextChunk||this.unfinishedMultiSegment);){let{nextChunkOffset:n}=i,s=this.appSegments.some((e=>!this.file.available(e.offset||e.start,e.length||e.size)));if(t=e>n&&!s?!await i.readNextChunk(e):!await i.readNextChunk(n),void 0===(e=this.findAppSegmentsInRange(e,i.byteLength)))return}}}findAppSegmentsInRange(e,t){t-=2;let i,n,s,r,a,o,{file:l,findAll:h,wanted:u,remaining:c,options:f}=this;for(;ee.multiSegment)))return;let e=function(e,t){let i,n,s,r=new Map;for(let a=0;a{let i=T.get(e,this.options);if(i.handleMultiSegments){return{type:e,chunk:i.handleMultiSegments(t)}}return t[0]}))}getSegment(e){return this.appSegments.find((t=>t.type===e))}async getOrFindSegment(e){let t=this.getSegment(e);return void 0===t&&(await this.findAppSegments(0,[e]),t=this.getSegment(e)),t}}c(he,\"type\",\"jpeg\"),w.set(\"jpeg\",he);const ue=[void 0,1,1,2,4,8,1,1,2,4,8,4,8,4];class ce extends re{parseHeader(){var e=this.chunk.getUint16();18761===e?this.le=!0:19789===e&&(this.le=!1),this.chunk.le=this.le,this.headerParsed=!0}parseTags(e,t,i=new Map){let{pick:n,skip:s}=this.options[t];n=new Set(n);let r=n.size>0,a=0===s.size,o=this.chunk.getUint16(e);e+=2;for(let l=0;l13)&&g(`Invalid TIFF value type. block: ${i.toUpperCase()}, tag: ${t.toString(16)}, type: ${s}, offset ${e}`),e>n.byteLength&&g(`Invalid TIFF value offset. block: ${i.toUpperCase()}, tag: ${t.toString(16)}, type: ${s}, offset ${e} is outside of chunk size ${n.byteLength}`),1===s)return n.getUint8Array(e,r);if(2===s)return m(n.getString(e,r));if(7===s)return n.getUint8Array(e,r);if(1===r)return this.parseTagValue(s,e);{let t=new(function(e){switch(e){case 1:return Uint8Array;case 3:return Uint16Array;case 4:return Uint32Array;case 5:return Array;case 6:return Int8Array;case 8:return Int16Array;case 9:return Int32Array;case 10:return Array;case 11:return Float32Array;case 12:return Float64Array;default:return Array}}(s))(r),i=a;for(let n=0;ne.byteLength&&g(`IFD0 offset points to outside of file.\\nthis.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e.byteLength}`),e.tiff&&await e.ensureChunk(this.ifd0Offset,S(this.options));let t=this.parseBlock(this.ifd0Offset,\"ifd0\");return 0!==t.size?(this.exifOffset=t.get(34665),this.interopOffset=t.get(40965),this.gpsOffset=t.get(34853),this.xmp=t.get(700),this.iptc=t.get(33723),this.icc=t.get(34675),this.options.sanitize&&(t.delete(34665),t.delete(40965),t.delete(34853),t.delete(700),t.delete(33723),t.delete(34675)),t):void 0}async parseExifBlock(){if(this.exif)return;if(this.ifd0||await this.parseIfd0Block(),void 0===this.exifOffset)return;this.file.tiff&&await this.file.ensureChunk(this.exifOffset,S(this.options));let e=this.parseBlock(this.exifOffset,\"exif\");return this.interopOffset||(this.interopOffset=e.get(40965)),this.makerNote=e.get(37500),this.userComment=e.get(37510),this.options.sanitize&&(e.delete(40965),e.delete(37500),e.delete(37510)),this.unpack(e,41728),this.unpack(e,41729),e}unpack(e,t){let i=e.get(t);i&&1===i.length&&e.set(t,i[0])}async parseGpsBlock(){if(this.gps)return;if(this.ifd0||await this.parseIfd0Block(),void 0===this.gpsOffset)return;let e=this.parseBlock(this.gpsOffset,\"gps\");return e&&e.has(2)&&e.has(4)&&(e.set(\"latitude\",de(...e.get(2),e.get(1))),e.set(\"longitude\",de(...e.get(4),e.get(3)))),e}async parseInteropBlock(){if(!this.interop&&(this.ifd0||await this.parseIfd0Block(),void 0!==this.interopOffset||this.exif||await this.parseExifBlock(),void 0!==this.interopOffset))return this.parseBlock(this.interopOffset,\"interop\")}async parseThumbnailBlock(e=!1){if(!this.ifd1&&!this.ifd1Parsed&&(!this.options.mergeOutput||e))return this.findIfd1Offset(),this.ifd1Offset>0&&(this.parseBlock(this.ifd1Offset,\"ifd1\"),this.ifd1Parsed=!0),this.ifd1}async extractThumbnail(){if(this.headerParsed||this.parseHeader(),this.ifd1Parsed||await this.parseThumbnailBlock(!0),void 0===this.ifd1)return;let e=this.ifd1.get(513),t=this.ifd1.get(514);return this.chunk.getUint8Array(e,t)}get image(){return this.ifd0}get thumbnail(){return this.ifd1}createOutput(){let e,t,i,n={};for(t of H)if(e=this[t],!p(e))if(i=this.canTranslate?this.translateBlock(e,t):Object.fromEntries(e),this.options.mergeOutput){if(\"ifd1\"===t)continue;Object.assign(n,i)}else n[t]=i;return this.makerNote&&(n.makerNote=this.makerNote),this.userComment&&(n.userComment=this.userComment),n}assignToOutput(e,t){if(this.globalOptions.mergeOutput)Object.assign(e,t);else for(let[i,n]of Object.entries(t))this.assignObjectToOutput(e,i,n)}}function de(e,t,i,n){var s=e+t/60+i/3600;return\"S\"!==n&&\"W\"!==n||(s*=-1),s}c(fe,\"type\",\"tiff\"),c(fe,\"headerLength\",10),T.set(\"tiff\",fe);var pe=Object.freeze({__proto__:null,default:ne,Exifr:te,fileParsers:w,segmentParsers:T,fileReaders:A,tagKeys:E,tagValues:B,tagRevivers:N,createDictionary:U,extendDictionary:F,fetchUrlAsArrayBuffer:M,readBlobAsArrayBuffer:R,chunkedProps:G,otherSegments:V,segments:z,tiffBlocks:H,segmentsAndBlocks:j,tiffExtractables:W,inheritables:K,allFormatters:X,Options:q,parse:ie});const ge={ifd0:!1,ifd1:!1,exif:!1,gps:!1,interop:!1,sanitize:!1,reviveValues:!0,translateKeys:!1,translateValues:!1,mergeOutput:!1},me=Object.assign({},ge,{firstChunkSize:4e4,gps:[1,2,3,4]});async function Se(e){let t=new te(me);await t.read(e);let i=await t.parse();if(i&&i.gps){let{latitude:e,longitude:t}=i.gps;return{latitude:e,longitude:t}}}const Ce=Object.assign({},ge,{tiff:!1,ifd1:!0,mergeOutput:!1});async function ye(e){let t=new te(Ce);await t.read(e);let i=await t.extractThumbnail();return i&&a?s.from(i):i}async function be(e){let t=await this.thumbnail(e);if(void 0!==t){let e=new Blob([t]);return URL.createObjectURL(e)}}const Ie=Object.assign({},ge,{firstChunkSize:4e4,ifd0:[274]});async function Pe(e){let t=new te(Ie);await t.read(e);let i=await t.parse();if(i&&i.ifd0)return i.ifd0[274]}const ke=Object.freeze({1:{dimensionSwapped:!1,scaleX:1,scaleY:1,deg:0,rad:0},2:{dimensionSwapped:!1,scaleX:-1,scaleY:1,deg:0,rad:0},3:{dimensionSwapped:!1,scaleX:1,scaleY:1,deg:180,rad:180*Math.PI/180},4:{dimensionSwapped:!1,scaleX:-1,scaleY:1,deg:180,rad:180*Math.PI/180},5:{dimensionSwapped:!0,scaleX:1,scaleY:-1,deg:90,rad:90*Math.PI/180},6:{dimensionSwapped:!0,scaleX:1,scaleY:1,deg:90,rad:90*Math.PI/180},7:{dimensionSwapped:!0,scaleX:1,scaleY:-1,deg:270,rad:270*Math.PI/180},8:{dimensionSwapped:!0,scaleX:1,scaleY:1,deg:270,rad:270*Math.PI/180}});let we=!0,Te=!0;if(\"object\"==typeof navigator){let e=navigator.userAgent;if(e.includes(\"iPad\")||e.includes(\"iPhone\")){let t=e.match(/OS (\\d+)_(\\d+)/);if(t){let[,e,i]=t,n=Number(e)+.1*Number(i);we=n<13.4,Te=!1}}else if(e.includes(\"OS X 10\")){let[,t]=e.match(/OS X 10[_.](\\d+)/);we=Te=Number(t)<15}if(e.includes(\"Chrome/\")){let[,t]=e.match(/Chrome\\/(\\d+)/);we=Te=Number(t)<81}else if(e.includes(\"Firefox/\")){let[,t]=e.match(/Firefox\\/(\\d+)/);we=Te=Number(t)<77}}async function Ae(e){let t=await Pe(e);return Object.assign({canvas:we,css:Te},ke[t])}class De extends I{constructor(...e){super(...e),c(this,\"ranges\",new Oe),0!==this.byteLength&&this.ranges.add(0,this.byteLength)}_tryExtend(e,t,i){if(0===e&&0===this.byteLength&&i){let e=new DataView(i.buffer||i,i.byteOffset,i.byteLength);this._swapDataView(e)}else{let i=e+t;if(i>this.byteLength){let{dataView:e}=this._extend(i);this._swapDataView(e)}}}_extend(e){let t;t=a?s.allocUnsafe(e):new Uint8Array(e);let i=new DataView(t.buffer,t.byteOffset,t.byteLength);return t.set(new Uint8Array(this.buffer,this.byteOffset,this.byteLength),0),{uintView:t,dataView:i}}subarray(e,t,i=!1){return t=t||this._lengthToEnd(e),i&&this._tryExtend(e,t),this.ranges.add(e,t),super.subarray(e,t)}set(e,t,i=!1){i&&this._tryExtend(t,e.byteLength,e);let n=super.set(e,t);return this.ranges.add(t,n.byteLength),n}async ensureChunk(e,t){this.chunked&&(this.ranges.available(e,t)||await this.readChunk(e,t))}available(e,t){return this.ranges.available(e,t)}}class Oe{constructor(){c(this,\"list\",[])}get length(){return this.list.length}add(e,t,i=0){let n=e+t,s=this.list.filter((t=>xe(e,t.offset,n)||xe(e,t.end,n)));if(s.length>0){e=Math.min(e,...s.map((e=>e.offset))),n=Math.max(n,...s.map((e=>e.end))),t=n-e;let i=s.shift();i.offset=e,i.length=t,i.end=n,this.list=this.list.filter((e=>!s.includes(e)))}else this.list.push({offset:e,length:t,end:n})}available(e,t){let i=e+t;return this.list.some((t=>t.offset<=e&&i<=t.end))}}function xe(e,t,i){return e<=t&&t<=i}class ve extends De{constructor(e,t){super(0),c(this,\"chunksRead\",0),this.input=e,this.options=t}async readWhole(){this.chunked=!1,await this.readChunk(this.nextChunkOffset)}async readChunked(){this.chunked=!0,await this.readChunk(0,this.options.firstChunkSize)}async readNextChunk(e=this.nextChunkOffset){if(this.fullyRead)return this.chunksRead++,!1;let t=this.options.chunkSize,i=await this.readChunk(e,t);return!!i&&i.byteLength===t}async readChunk(e,t){if(this.chunksRead++,0!==(t=this.safeWrapAddress(e,t)))return this._readChunk(e,t)}safeWrapAddress(e,t){return void 0!==this.size&&e+t>this.size?Math.max(0,this.size-e):t}get nextChunkOffset(){if(0!==this.ranges.list.length)return this.ranges.list[0].length}get canReadNextChunk(){return this.chunksReade.kind===t))}parseBoxHead(e){let t=this.file.getUint32(e),i=this.file.getString(e+4,4),n=e+8;return 1===t&&(t=this.file.getUint64(e+8),n+=8),{offset:e,length:t,kind:i,start:n}}parseBoxFullHead(e){if(void 0!==e.version)return;let t=this.file.getUint32(e.start);e.version=t>>24,e.start+=4}}class Le extends Re{static canHandle(e,t){if(0!==t)return!1;let i=e.getUint16(2);if(i>50)return!1;let n=16,s=[];for(;n=2&&(n=3===t.version?4:2,s=this.file.getString(i+n+2,4),\"Exif\"===s))return this.file.getUintBytes(i,n);r+=t.length}}get8bits(e){let t=this.file.getUint8(e);return[t>>4,15&t]}findExtentInIloc(e,t){this.parseBoxFullHead(e);let i=e.start,[n,s]=this.get8bits(i++),[r,a]=this.get8bits(i++),o=2===e.version?4:2,l=1===e.version||2===e.version?2:0,h=a+n+s,u=2===e.version?4:2,c=this.file.getUintBytes(i,u);for(i+=u;c--;){let e=this.file.getUintBytes(i,o);i+=o+l+2+r;let u=this.file.getUint16(i);if(i+=2,e===t)return u>1&&console.warn(\"ILOC box has more than one extent but we're only processing one\\nPlease create an issue at https://github.com/MikeKovarik/exifr with this file\"),[this.file.getUintBytes(i+a,n),this.file.getUintBytes(i+a+n,s)];i+=u*h}}}class Ue extends Le{}c(Ue,\"type\",\"heic\");class Fe extends Le{}c(Fe,\"type\",\"avif\"),w.set(\"heic\",Ue),w.set(\"avif\",Fe),U(E,[\"ifd0\",\"ifd1\"],[[256,\"ImageWidth\"],[257,\"ImageHeight\"],[258,\"BitsPerSample\"],[259,\"Compression\"],[262,\"PhotometricInterpretation\"],[270,\"ImageDescription\"],[271,\"Make\"],[272,\"Model\"],[273,\"StripOffsets\"],[274,\"Orientation\"],[277,\"SamplesPerPixel\"],[278,\"RowsPerStrip\"],[279,\"StripByteCounts\"],[282,\"XResolution\"],[283,\"YResolution\"],[284,\"PlanarConfiguration\"],[296,\"ResolutionUnit\"],[301,\"TransferFunction\"],[305,\"Software\"],[306,\"ModifyDate\"],[315,\"Artist\"],[316,\"HostComputer\"],[317,\"Predictor\"],[318,\"WhitePoint\"],[319,\"PrimaryChromaticities\"],[513,\"ThumbnailOffset\"],[514,\"ThumbnailLength\"],[529,\"YCbCrCoefficients\"],[530,\"YCbCrSubSampling\"],[531,\"YCbCrPositioning\"],[532,\"ReferenceBlackWhite\"],[700,\"ApplicationNotes\"],[33432,\"Copyright\"],[33723,\"IPTC\"],[34665,\"ExifIFD\"],[34675,\"ICC\"],[34853,\"GpsIFD\"],[330,\"SubIFD\"],[40965,\"InteropIFD\"],[40091,\"XPTitle\"],[40092,\"XPComment\"],[40093,\"XPAuthor\"],[40094,\"XPKeywords\"],[40095,\"XPSubject\"]]),U(E,\"exif\",[[33434,\"ExposureTime\"],[33437,\"FNumber\"],[34850,\"ExposureProgram\"],[34852,\"SpectralSensitivity\"],[34855,\"ISO\"],[34858,\"TimeZoneOffset\"],[34859,\"SelfTimerMode\"],[34864,\"SensitivityType\"],[34865,\"StandardOutputSensitivity\"],[34866,\"RecommendedExposureIndex\"],[34867,\"ISOSpeed\"],[34868,\"ISOSpeedLatitudeyyy\"],[34869,\"ISOSpeedLatitudezzz\"],[36864,\"ExifVersion\"],[36867,\"DateTimeOriginal\"],[36868,\"CreateDate\"],[36873,\"GooglePlusUploadCode\"],[36880,\"OffsetTime\"],[36881,\"OffsetTimeOriginal\"],[36882,\"OffsetTimeDigitized\"],[37121,\"ComponentsConfiguration\"],[37122,\"CompressedBitsPerPixel\"],[37377,\"ShutterSpeedValue\"],[37378,\"ApertureValue\"],[37379,\"BrightnessValue\"],[37380,\"ExposureCompensation\"],[37381,\"MaxApertureValue\"],[37382,\"SubjectDistance\"],[37383,\"MeteringMode\"],[37384,\"LightSource\"],[37385,\"Flash\"],[37386,\"FocalLength\"],[37393,\"ImageNumber\"],[37394,\"SecurityClassification\"],[37395,\"ImageHistory\"],[37396,\"SubjectArea\"],[37500,\"MakerNote\"],[37510,\"UserComment\"],[37520,\"SubSecTime\"],[37521,\"SubSecTimeOriginal\"],[37522,\"SubSecTimeDigitized\"],[37888,\"AmbientTemperature\"],[37889,\"Humidity\"],[37890,\"Pressure\"],[37891,\"WaterDepth\"],[37892,\"Acceleration\"],[37893,\"CameraElevationAngle\"],[40960,\"FlashpixVersion\"],[40961,\"ColorSpace\"],[40962,\"ExifImageWidth\"],[40963,\"ExifImageHeight\"],[40964,\"RelatedSoundFile\"],[41483,\"FlashEnergy\"],[41486,\"FocalPlaneXResolution\"],[41487,\"FocalPlaneYResolution\"],[41488,\"FocalPlaneResolutionUnit\"],[41492,\"SubjectLocation\"],[41493,\"ExposureIndex\"],[41495,\"SensingMethod\"],[41728,\"FileSource\"],[41729,\"SceneType\"],[41730,\"CFAPattern\"],[41985,\"CustomRendered\"],[41986,\"ExposureMode\"],[41987,\"WhiteBalance\"],[41988,\"DigitalZoomRatio\"],[41989,\"FocalLengthIn35mmFormat\"],[41990,\"SceneCaptureType\"],[41991,\"GainControl\"],[41992,\"Contrast\"],[41993,\"Saturation\"],[41994,\"Sharpness\"],[41996,\"SubjectDistanceRange\"],[42016,\"ImageUniqueID\"],[42032,\"OwnerName\"],[42033,\"SerialNumber\"],[42034,\"LensInfo\"],[42035,\"LensMake\"],[42036,\"LensModel\"],[42037,\"LensSerialNumber\"],[42080,\"CompositeImage\"],[42081,\"CompositeImageCount\"],[42082,\"CompositeImageExposureTimes\"],[42240,\"Gamma\"],[59932,\"Padding\"],[59933,\"OffsetSchema\"],[65e3,\"OwnerName\"],[65001,\"SerialNumber\"],[65002,\"Lens\"],[65100,\"RawFile\"],[65101,\"Converter\"],[65102,\"WhiteBalance\"],[65105,\"Exposure\"],[65106,\"Shadows\"],[65107,\"Brightness\"],[65108,\"Contrast\"],[65109,\"Saturation\"],[65110,\"Sharpness\"],[65111,\"Smoothness\"],[65112,\"MoireFilter\"],[40965,\"InteropIFD\"]]),U(E,\"gps\",[[0,\"GPSVersionID\"],[1,\"GPSLatitudeRef\"],[2,\"GPSLatitude\"],[3,\"GPSLongitudeRef\"],[4,\"GPSLongitude\"],[5,\"GPSAltitudeRef\"],[6,\"GPSAltitude\"],[7,\"GPSTimeStamp\"],[8,\"GPSSatellites\"],[9,\"GPSStatus\"],[10,\"GPSMeasureMode\"],[11,\"GPSDOP\"],[12,\"GPSSpeedRef\"],[13,\"GPSSpeed\"],[14,\"GPSTrackRef\"],[15,\"GPSTrack\"],[16,\"GPSImgDirectionRef\"],[17,\"GPSImgDirection\"],[18,\"GPSMapDatum\"],[19,\"GPSDestLatitudeRef\"],[20,\"GPSDestLatitude\"],[21,\"GPSDestLongitudeRef\"],[22,\"GPSDestLongitude\"],[23,\"GPSDestBearingRef\"],[24,\"GPSDestBearing\"],[25,\"GPSDestDistanceRef\"],[26,\"GPSDestDistance\"],[27,\"GPSProcessingMethod\"],[28,\"GPSAreaInformation\"],[29,\"GPSDateStamp\"],[30,\"GPSDifferential\"],[31,\"GPSHPositioningError\"]]),U(B,[\"ifd0\",\"ifd1\"],[[274,{1:\"Horizontal (normal)\",2:\"Mirror horizontal\",3:\"Rotate 180\",4:\"Mirror vertical\",5:\"Mirror horizontal and rotate 270 CW\",6:\"Rotate 90 CW\",7:\"Mirror horizontal and rotate 90 CW\",8:\"Rotate 270 CW\"}],[296,{1:\"None\",2:\"inches\",3:\"cm\"}]]);let Ee=U(B,\"exif\",[[34850,{0:\"Not defined\",1:\"Manual\",2:\"Normal program\",3:\"Aperture priority\",4:\"Shutter priority\",5:\"Creative program\",6:\"Action program\",7:\"Portrait mode\",8:\"Landscape mode\"}],[37121,{0:\"-\",1:\"Y\",2:\"Cb\",3:\"Cr\",4:\"R\",5:\"G\",6:\"B\"}],[37383,{0:\"Unknown\",1:\"Average\",2:\"CenterWeightedAverage\",3:\"Spot\",4:\"MultiSpot\",5:\"Pattern\",6:\"Partial\",255:\"Other\"}],[37384,{0:\"Unknown\",1:\"Daylight\",2:\"Fluorescent\",3:\"Tungsten (incandescent light)\",4:\"Flash\",9:\"Fine weather\",10:\"Cloudy weather\",11:\"Shade\",12:\"Daylight fluorescent (D 5700 - 7100K)\",13:\"Day white fluorescent (N 4600 - 5400K)\",14:\"Cool white fluorescent (W 3900 - 4500K)\",15:\"White fluorescent (WW 3200 - 3700K)\",17:\"Standard light A\",18:\"Standard light B\",19:\"Standard light C\",20:\"D55\",21:\"D65\",22:\"D75\",23:\"D50\",24:\"ISO studio tungsten\",255:\"Other\"}],[37385,{0:\"Flash did not fire\",1:\"Flash fired\",5:\"Strobe return light not detected\",7:\"Strobe return light detected\",9:\"Flash fired, compulsory flash mode\",13:\"Flash fired, compulsory flash mode, return light not detected\",15:\"Flash fired, compulsory flash mode, return light detected\",16:\"Flash did not fire, compulsory flash mode\",24:\"Flash did not fire, auto mode\",25:\"Flash fired, auto mode\",29:\"Flash fired, auto mode, return light not detected\",31:\"Flash fired, auto mode, return light detected\",32:\"No flash function\",65:\"Flash fired, red-eye reduction mode\",69:\"Flash fired, red-eye reduction mode, return light not detected\",71:\"Flash fired, red-eye reduction mode, return light detected\",73:\"Flash fired, compulsory flash mode, red-eye reduction mode\",77:\"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected\",79:\"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected\",89:\"Flash fired, auto mode, red-eye reduction mode\",93:\"Flash fired, auto mode, return light not detected, red-eye reduction mode\",95:\"Flash fired, auto mode, return light detected, red-eye reduction mode\"}],[41495,{1:\"Not defined\",2:\"One-chip color area sensor\",3:\"Two-chip color area sensor\",4:\"Three-chip color area sensor\",5:\"Color sequential area sensor\",7:\"Trilinear sensor\",8:\"Color sequential linear sensor\"}],[41728,{1:\"Film Scanner\",2:\"Reflection Print Scanner\",3:\"Digital Camera\"}],[41729,{1:\"Directly photographed\"}],[41985,{0:\"Normal\",1:\"Custom\",2:\"HDR (no original saved)\",3:\"HDR (original saved)\",4:\"Original (for HDR)\",6:\"Panorama\",7:\"Portrait HDR\",8:\"Portrait\"}],[41986,{0:\"Auto\",1:\"Manual\",2:\"Auto bracket\"}],[41987,{0:\"Auto\",1:\"Manual\"}],[41990,{0:\"Standard\",1:\"Landscape\",2:\"Portrait\",3:\"Night\",4:\"Other\"}],[41991,{0:\"None\",1:\"Low gain up\",2:\"High gain up\",3:\"Low gain down\",4:\"High gain down\"}],[41996,{0:\"Unknown\",1:\"Macro\",2:\"Close\",3:\"Distant\"}],[42080,{0:\"Unknown\",1:\"Not a Composite Image\",2:\"General Composite Image\",3:\"Composite Image Captured While Shooting\"}]]);const Be={1:\"No absolute unit of measurement\",2:\"Inch\",3:\"Centimeter\"};Ee.set(37392,Be),Ee.set(41488,Be);const Ne={0:\"Normal\",1:\"Low\",2:\"High\"};function Ge(e){return\"object\"==typeof e&&void 0!==e.length?e[0]:e}function Ve(e){let t=Array.from(e).slice(1);return t[1]>15&&(t=t.map((e=>String.fromCharCode(e)))),\"0\"!==t[2]&&0!==t[2]||t.pop(),t.join(\".\")}function ze(e){if(\"string\"==typeof e){var[t,i,n,s,r,a]=e.trim().split(/[-: ]/g).map(Number),o=new Date(t,i-1,n);return Number.isNaN(s)||Number.isNaN(r)||Number.isNaN(a)||(o.setHours(s),o.setMinutes(r),o.setSeconds(a)),Number.isNaN(+o)?e:o}}function He(e){if(\"string\"==typeof e)return e;let t=[];if(0===e[1]&&0===e[e.length-1])for(let i=0;iArray.from(e).join(\".\")],[7,e=>Array.from(e).join(\":\")]]);class We extends re{static canHandle(e,t){return 225===e.getUint8(t+1)&&1752462448===e.getUint32(t+4)&&\"http://ns.adobe.com/\"===e.getString(t+4,\"http://ns.adobe.com/\".length)}static headerLength(e,t){return\"http://ns.adobe.com/xmp/extension/\"===e.getString(t+4,\"http://ns.adobe.com/xmp/extension/\".length)?79:4+\"http://ns.adobe.com/xap/1.0/\".length+1}static findPosition(e,t){let i=super.findPosition(e,t);return i.multiSegment=i.extended=79===i.headerLength,i.multiSegment?(i.chunkCount=e.getUint8(t+72),i.chunkNumber=e.getUint8(t+76),0!==e.getUint8(t+77)&&i.chunkNumber++):(i.chunkCount=1/0,i.chunkNumber=-1),i}static handleMultiSegments(e){return e.map((e=>e.chunk.getString())).join(\"\")}normalizeInput(e){return\"string\"==typeof e?e:I.from(e).getString()}parse(e=this.chunk){if(!this.localOptions.parse)return e;e=function(e){let t={},i={};for(let e of Ze)t[e]=[],i[e]=0;return e.replace(et,((e,n,s)=>{if(\"<\"===n){let n=++i[s];return t[s].push(n),`${e}#${n}`}return`${e}#${t[s].pop()}`}))}(e);let t=Xe.findAll(e,\"rdf\",\"Description\");0===t.length&&t.push(new Xe(\"rdf\",\"Description\",void 0,e));let i,n={};for(let e of t)for(let t of e.properties)i=Je(t.ns,n),_e(t,i);return function(e){let t;for(let i in e)t=e[i]=f(e[i]),void 0===t&&delete e[i];return f(e)}(n)}assignToOutput(e,t){if(this.localOptions.parse)for(let[i,n]of Object.entries(t))switch(i){case\"tiff\":this.assignObjectToOutput(e,\"ifd0\",n);break;case\"exif\":this.assignObjectToOutput(e,\"exif\",n);break;case\"xmlns\":break;default:this.assignObjectToOutput(e,i,n)}else e.xmp=t}}c(We,\"type\",\"xmp\"),c(We,\"multiSegment\",!0),T.set(\"xmp\",We);class Ke{static findAll(e){return qe(e,/([a-zA-Z0-9-]+):([a-zA-Z0-9-]+)=(\"[^\"]*\"|'[^']*')/gm).map(Ke.unpackMatch)}static unpackMatch(e){let t=e[1],i=e[2],n=e[3].slice(1,-1);return n=Qe(n),new Ke(t,i,n)}constructor(e,t,i){this.ns=e,this.name=t,this.value=i}serialize(){return this.value}}class Xe{static findAll(e,t,i){if(void 0!==t||void 0!==i){t=t||\"[\\\\w\\\\d-]+\",i=i||\"[\\\\w\\\\d-]+\";var n=new RegExp(`<(${t}):(${i})(#\\\\d+)?((\\\\s+?[\\\\w\\\\d-:]+=(\"[^\"]*\"|'[^']*'))*\\\\s*)(\\\\/>|>([\\\\s\\\\S]*?)<\\\\/\\\\1:\\\\2\\\\3>)`,\"gm\")}else n=/<([\\w\\d-]+):([\\w\\d-]+)(#\\d+)?((\\s+?[\\w\\d-:]+=(\"[^\"]*\"|'[^']*'))*\\s*)(\\/>|>([\\s\\S]*?)<\\/\\1:\\2\\3>)/gm;return qe(e,n).map(Xe.unpackMatch)}static unpackMatch(e){let t=e[1],i=e[2],n=e[4],s=e[8];return new Xe(t,i,n,s)}constructor(e,t,i,n){this.ns=e,this.name=t,this.attrString=i,this.innerXml=n,this.attrs=Ke.findAll(i),this.children=Xe.findAll(n),this.value=0===this.children.length?Qe(n):void 0,this.properties=[...this.attrs,...this.children]}get isPrimitive(){return void 0!==this.value&&0===this.attrs.length&&0===this.children.length}get isListContainer(){return 1===this.children.length&&this.children[0].isList}get isList(){let{ns:e,name:t}=this;return\"rdf\"===e&&(\"Seq\"===t||\"Bag\"===t||\"Alt\"===t)}get isListItem(){return\"rdf\"===this.ns&&\"li\"===this.name}serialize(){if(0===this.properties.length&&void 0===this.value)return;if(this.isPrimitive)return this.value;if(this.isListContainer)return this.children[0].serialize();if(this.isList)return $e(this.children.map(Ye));if(this.isListItem&&1===this.children.length&&0===this.attrs.length)return this.children[0].serialize();let e={};for(let t of this.properties)_e(t,e);return void 0!==this.value&&(e.value=this.value),f(e)}}function _e(e,t){let i=e.serialize();void 0!==i&&(t[e.name]=i)}var Ye=e=>e.serialize(),$e=e=>1===e.length?e[0]:e,Je=(e,t)=>t[e]?t[e]:t[e]={};function qe(e,t){let i,n=[];if(!e)return n;for(;null!==(i=t.exec(e));)n.push(i);return n}function Qe(e){if(function(e){return null==e||\"null\"===e||\"undefined\"===e||\"\"===e||\"\"===e.trim()}(e))return;let t=Number(e);if(!Number.isNaN(t))return t;let i=e.toLowerCase();return\"true\"===i||\"false\"!==i&&e.trim()}const Ze=[\"rdf:li\",\"rdf:Seq\",\"rdf:Bag\",\"rdf:Alt\",\"rdf:Description\"],et=new RegExp(`(<|\\\\/)(${Ze.join(\"|\")})`,\"g\");var tt=Object.freeze({__proto__:null,default:Me,Exifr:te,fileParsers:w,segmentParsers:T,fileReaders:A,tagKeys:E,tagValues:B,tagRevivers:N,createDictionary:U,extendDictionary:F,fetchUrlAsArrayBuffer:M,readBlobAsArrayBuffer:R,chunkedProps:G,otherSegments:V,segments:z,tiffBlocks:H,segmentsAndBlocks:j,tiffExtractables:W,inheritables:K,allFormatters:X,Options:q,parse:ie,gpsOnlyOptions:me,gps:Se,thumbnailOnlyOptions:Ce,thumbnail:ye,thumbnailUrl:be,orientationOnlyOptions:Ie,orientation:Pe,rotations:ke,get rotateCanvas(){return we},get rotateCss(){return Te},rotation:Ae});const it=[\"xmp\",\"icc\",\"iptc\",\"tiff\"],nt=()=>{};async function st(e,t,i){let n=new q(t);n.chunked=!1,void 0===i&&\"string\"==typeof e&&(i=function(e){let t=e.toLowerCase().split(\".\").pop();if(function(e){return\"exif\"===e||\"tiff\"===e||\"tif\"===e}(t))return\"tiff\";if(it.includes(t))return t}(e));let s=await D(e,n);if(i){if(it.includes(i))return rt(i,s,n);g(\"Invalid segment type\")}else{if(function(e){let t=e.getString(0,50).trim();return t.includes(\"e.promises));A.set(\"fs\",class extends ve{async readWhole(){this.chunked=!1,this.fs=await at;let e=await this.fs.readFile(this.input);this._swapBuffer(e)}async readChunked(){this.chunked=!0,this.fs=await at,await this.open(),await this.readChunk(0,this.options.firstChunkSize)}async open(){void 0===this.fh&&(this.fh=await this.fs.open(this.input,\"r\"),this.size=(await this.fh.stat(this.input)).size)}async _readChunk(e,t){void 0===this.fh&&await this.open(),e+t>this.size&&(t=this.size-e);var i=this.subarray(e,t,!0);return await this.fh.read(i.dataView,0,t,e),i}async close(){if(this.fh){let e=this.fh;this.fh=void 0,await e.close()}}});A.set(\"base64\",class extends ve{constructor(...e){super(...e),this.input=this.input.replace(/^data:([^;]+);base64,/gim,\"\"),this.size=this.input.length/4*3,this.input.endsWith(\"==\")?this.size-=2:this.input.endsWith(\"=\")&&(this.size-=1)}async _readChunk(e,t){let i,n,r=this.input;void 0===e?(e=0,i=0,n=0):(i=4*Math.floor(e/3),n=e-i/4*3),void 0===t&&(t=this.size);let o=e+t,l=i+4*Math.ceil(o/3);r=r.slice(i,l);let h=Math.min(t,this.size-e);if(a){let t=s.from(r,\"base64\").slice(n,n+h);return this.set(t,e,!0)}{let t=this.subarray(e,h,!0),i=atob(r),s=t.toUint8();for(let e=0;ethis.errors.push(e))),c(this,\"metaChunks\",[]),c(this,\"unknownChunks\",[])}static canHandle(e,t){return 35152===t&&2303741511===e.getUint32(0)&&218765834===e.getUint32(4)}async parse(){let{file:e}=this;await this.findPngChunksInRange(\"\u0089PNG\\r\\n\u001A\\n\".length,e.byteLength),await this.readSegments(this.metaChunks),this.findIhdr(),this.parseTextChunks(),await this.findExif().catch(this.catchError),await this.findXmp().catch(this.catchError),await this.findIcc().catch(this.catchError)}async findPngChunksInRange(e,t){let{file:i}=this;for(;e\"text\"===e.type));for(let t of e){let[e,i]=this.file.getString(t.start,t.size).split(\"\\0\");this.injectKeyValToIhdr(e,i)}}injectKeyValToIhdr(e,t){let i=this.parsers.ihdr;i&&i.raw.set(e,t)}findIhdr(){let e=this.metaChunks.find((e=>\"ihdr\"===e.type));e&&!1!==this.options.ihdr.enabled&&this.createParser(\"ihdr\",e.chunk)}async findExif(){let e=this.metaChunks.find((e=>\"exif\"===e.type));e&&this.injectSegment(\"tiff\",e.chunk)}async findXmp(){let e=this.metaChunks.filter((e=>\"itxt\"===e.type));for(let t of e){\"XML:com.adobe.xmp\"===t.chunk.getString(0,\"XML:com.adobe.xmp\".length)&&this.injectSegment(\"xmp\",t.chunk)}}async findIcc(){let e=this.metaChunks.find((e=>\"iccp\"===e.type));if(!e)return;let{chunk:t}=e,i=t.getUint8Array(0,81),s=0;for(;s<80&&0!==i[s];)s++;let r=s+2,a=t.getString(0,s);if(this.injectKeyValToIhdr(\"ProfileName\",a),n){let e=await lt,i=t.getUint8Array(r);i=e.inflateSync(i),this.injectSegment(\"icc\",i)}}}c(ut,\"type\",\"png\"),w.set(\"png\",ut),U(E,\"interop\",[[1,\"InteropIndex\"],[2,\"InteropVersion\"],[4096,\"RelatedImageFileFormat\"],[4097,\"RelatedImageWidth\"],[4098,\"RelatedImageHeight\"]]),F(E,\"ifd0\",[[11,\"ProcessingSoftware\"],[254,\"SubfileType\"],[255,\"OldSubfileType\"],[263,\"Thresholding\"],[264,\"CellWidth\"],[265,\"CellLength\"],[266,\"FillOrder\"],[269,\"DocumentName\"],[280,\"MinSampleValue\"],[281,\"MaxSampleValue\"],[285,\"PageName\"],[286,\"XPosition\"],[287,\"YPosition\"],[290,\"GrayResponseUnit\"],[297,\"PageNumber\"],[321,\"HalftoneHints\"],[322,\"TileWidth\"],[323,\"TileLength\"],[332,\"InkSet\"],[337,\"TargetPrinter\"],[18246,\"Rating\"],[18249,\"RatingPercent\"],[33550,\"PixelScale\"],[34264,\"ModelTransform\"],[34377,\"PhotoshopSettings\"],[50706,\"DNGVersion\"],[50707,\"DNGBackwardVersion\"],[50708,\"UniqueCameraModel\"],[50709,\"LocalizedCameraModel\"],[50736,\"DNGLensInfo\"],[50739,\"ShadowScale\"],[50740,\"DNGPrivateData\"],[33920,\"IntergraphMatrix\"],[33922,\"ModelTiePoint\"],[34118,\"SEMInfo\"],[34735,\"GeoTiffDirectory\"],[34736,\"GeoTiffDoubleParams\"],[34737,\"GeoTiffAsciiParams\"],[50341,\"PrintIM\"],[50721,\"ColorMatrix1\"],[50722,\"ColorMatrix2\"],[50723,\"CameraCalibration1\"],[50724,\"CameraCalibration2\"],[50725,\"ReductionMatrix1\"],[50726,\"ReductionMatrix2\"],[50727,\"AnalogBalance\"],[50728,\"AsShotNeutral\"],[50729,\"AsShotWhiteXY\"],[50730,\"BaselineExposure\"],[50731,\"BaselineNoise\"],[50732,\"BaselineSharpness\"],[50734,\"LinearResponseLimit\"],[50735,\"CameraSerialNumber\"],[50741,\"MakerNoteSafety\"],[50778,\"CalibrationIlluminant1\"],[50779,\"CalibrationIlluminant2\"],[50781,\"RawDataUniqueID\"],[50827,\"OriginalRawFileName\"],[50828,\"OriginalRawFileData\"],[50831,\"AsShotICCProfile\"],[50832,\"AsShotPreProfileMatrix\"],[50833,\"CurrentICCProfile\"],[50834,\"CurrentPreProfileMatrix\"],[50879,\"ColorimetricReference\"],[50885,\"SRawType\"],[50898,\"PanasonicTitle\"],[50899,\"PanasonicTitle2\"],[50931,\"CameraCalibrationSig\"],[50932,\"ProfileCalibrationSig\"],[50933,\"ProfileIFD\"],[50934,\"AsShotProfileName\"],[50936,\"ProfileName\"],[50937,\"ProfileHueSatMapDims\"],[50938,\"ProfileHueSatMapData1\"],[50939,\"ProfileHueSatMapData2\"],[50940,\"ProfileToneCurve\"],[50941,\"ProfileEmbedPolicy\"],[50942,\"ProfileCopyright\"],[50964,\"ForwardMatrix1\"],[50965,\"ForwardMatrix2\"],[50966,\"PreviewApplicationName\"],[50967,\"PreviewApplicationVersion\"],[50968,\"PreviewSettingsName\"],[50969,\"PreviewSettingsDigest\"],[50970,\"PreviewColorSpace\"],[50971,\"PreviewDateTime\"],[50972,\"RawImageDigest\"],[50973,\"OriginalRawFileDigest\"],[50981,\"ProfileLookTableDims\"],[50982,\"ProfileLookTableData\"],[51043,\"TimeCodes\"],[51044,\"FrameRate\"],[51058,\"TStop\"],[51081,\"ReelName\"],[51089,\"OriginalDefaultFinalSize\"],[51090,\"OriginalBestQualitySize\"],[51091,\"OriginalDefaultCropSize\"],[51105,\"CameraLabel\"],[51107,\"ProfileHueSatMapEncoding\"],[51108,\"ProfileLookTableEncoding\"],[51109,\"BaselineExposureOffset\"],[51110,\"DefaultBlackRender\"],[51111,\"NewRawImageDigest\"],[51112,\"RawToPreviewGain\"]]);let ct=[[273,\"StripOffsets\"],[279,\"StripByteCounts\"],[288,\"FreeOffsets\"],[289,\"FreeByteCounts\"],[291,\"GrayResponseCurve\"],[292,\"T4Options\"],[293,\"T6Options\"],[300,\"ColorResponseUnit\"],[320,\"ColorMap\"],[324,\"TileOffsets\"],[325,\"TileByteCounts\"],[326,\"BadFaxLines\"],[327,\"CleanFaxData\"],[328,\"ConsecutiveBadFaxLines\"],[330,\"SubIFD\"],[333,\"InkNames\"],[334,\"NumberofInks\"],[336,\"DotRange\"],[338,\"ExtraSamples\"],[339,\"SampleFormat\"],[340,\"SMinSampleValue\"],[341,\"SMaxSampleValue\"],[342,\"TransferRange\"],[343,\"ClipPath\"],[344,\"XClipPathUnits\"],[345,\"YClipPathUnits\"],[346,\"Indexed\"],[347,\"JPEGTables\"],[351,\"OPIProxy\"],[400,\"GlobalParametersIFD\"],[401,\"ProfileType\"],[402,\"FaxProfile\"],[403,\"CodingMethods\"],[404,\"VersionYear\"],[405,\"ModeNumber\"],[433,\"Decode\"],[434,\"DefaultImageColor\"],[435,\"T82Options\"],[437,\"JPEGTables\"],[512,\"JPEGProc\"],[515,\"JPEGRestartInterval\"],[517,\"JPEGLosslessPredictors\"],[518,\"JPEGPointTransforms\"],[519,\"JPEGQTables\"],[520,\"JPEGDCTables\"],[521,\"JPEGACTables\"],[559,\"StripRowCounts\"],[999,\"USPTOMiscellaneous\"],[18247,\"XP_DIP_XML\"],[18248,\"StitchInfo\"],[28672,\"SonyRawFileType\"],[28688,\"SonyToneCurve\"],[28721,\"VignettingCorrection\"],[28722,\"VignettingCorrParams\"],[28724,\"ChromaticAberrationCorrection\"],[28725,\"ChromaticAberrationCorrParams\"],[28726,\"DistortionCorrection\"],[28727,\"DistortionCorrParams\"],[29895,\"SonyCropTopLeft\"],[29896,\"SonyCropSize\"],[32781,\"ImageID\"],[32931,\"WangTag1\"],[32932,\"WangAnnotation\"],[32933,\"WangTag3\"],[32934,\"WangTag4\"],[32953,\"ImageReferencePoints\"],[32954,\"RegionXformTackPoint\"],[32955,\"WarpQuadrilateral\"],[32956,\"AffineTransformMat\"],[32995,\"Matteing\"],[32996,\"DataType\"],[32997,\"ImageDepth\"],[32998,\"TileDepth\"],[33300,\"ImageFullWidth\"],[33301,\"ImageFullHeight\"],[33302,\"TextureFormat\"],[33303,\"WrapModes\"],[33304,\"FovCot\"],[33305,\"MatrixWorldToScreen\"],[33306,\"MatrixWorldToCamera\"],[33405,\"Model2\"],[33421,\"CFARepeatPatternDim\"],[33422,\"CFAPattern2\"],[33423,\"BatteryLevel\"],[33424,\"KodakIFD\"],[33445,\"MDFileTag\"],[33446,\"MDScalePixel\"],[33447,\"MDColorTable\"],[33448,\"MDLabName\"],[33449,\"MDSampleInfo\"],[33450,\"MDPrepDate\"],[33451,\"MDPrepTime\"],[33452,\"MDFileUnits\"],[33589,\"AdventScale\"],[33590,\"AdventRevision\"],[33628,\"UIC1Tag\"],[33629,\"UIC2Tag\"],[33630,\"UIC3Tag\"],[33631,\"UIC4Tag\"],[33918,\"IntergraphPacketData\"],[33919,\"IntergraphFlagRegisters\"],[33921,\"INGRReserved\"],[34016,\"Site\"],[34017,\"ColorSequence\"],[34018,\"IT8Header\"],[34019,\"RasterPadding\"],[34020,\"BitsPerRunLength\"],[34021,\"BitsPerExtendedRunLength\"],[34022,\"ColorTable\"],[34023,\"ImageColorIndicator\"],[34024,\"BackgroundColorIndicator\"],[34025,\"ImageColorValue\"],[34026,\"BackgroundColorValue\"],[34027,\"PixelIntensityRange\"],[34028,\"TransparencyIndicator\"],[34029,\"ColorCharacterization\"],[34030,\"HCUsage\"],[34031,\"TrapIndicator\"],[34032,\"CMYKEquivalent\"],[34152,\"AFCP_IPTC\"],[34232,\"PixelMagicJBIGOptions\"],[34263,\"JPLCartoIFD\"],[34306,\"WB_GRGBLevels\"],[34310,\"LeafData\"],[34687,\"TIFF_FXExtensions\"],[34688,\"MultiProfiles\"],[34689,\"SharedData\"],[34690,\"T88Options\"],[34732,\"ImageLayer\"],[34750,\"JBIGOptions\"],[34856,\"Opto-ElectricConvFactor\"],[34857,\"Interlace\"],[34908,\"FaxRecvParams\"],[34909,\"FaxSubAddress\"],[34910,\"FaxRecvTime\"],[34929,\"FedexEDR\"],[34954,\"LeafSubIFD\"],[37387,\"FlashEnergy\"],[37388,\"SpatialFrequencyResponse\"],[37389,\"Noise\"],[37390,\"FocalPlaneXResolution\"],[37391,\"FocalPlaneYResolution\"],[37392,\"FocalPlaneResolutionUnit\"],[37397,\"ExposureIndex\"],[37398,\"TIFF-EPStandardID\"],[37399,\"SensingMethod\"],[37434,\"CIP3DataFile\"],[37435,\"CIP3Sheet\"],[37436,\"CIP3Side\"],[37439,\"StoNits\"],[37679,\"MSDocumentText\"],[37680,\"MSPropertySetStorage\"],[37681,\"MSDocumentTextPosition\"],[37724,\"ImageSourceData\"],[40965,\"InteropIFD\"],[40976,\"SamsungRawPointersOffset\"],[40977,\"SamsungRawPointersLength\"],[41217,\"SamsungRawByteOrder\"],[41218,\"SamsungRawUnknown\"],[41484,\"SpatialFrequencyResponse\"],[41485,\"Noise\"],[41489,\"ImageNumber\"],[41490,\"SecurityClassification\"],[41491,\"ImageHistory\"],[41494,\"TIFF-EPStandardID\"],[41995,\"DeviceSettingDescription\"],[42112,\"GDALMetadata\"],[42113,\"GDALNoData\"],[44992,\"ExpandSoftware\"],[44993,\"ExpandLens\"],[44994,\"ExpandFilm\"],[44995,\"ExpandFilterLens\"],[44996,\"ExpandScanner\"],[44997,\"ExpandFlashLamp\"],[46275,\"HasselbladRawImage\"],[48129,\"PixelFormat\"],[48130,\"Transformation\"],[48131,\"Uncompressed\"],[48132,\"ImageType\"],[48256,\"ImageWidth\"],[48257,\"ImageHeight\"],[48258,\"WidthResolution\"],[48259,\"HeightResolution\"],[48320,\"ImageOffset\"],[48321,\"ImageByteCount\"],[48322,\"AlphaOffset\"],[48323,\"AlphaByteCount\"],[48324,\"ImageDataDiscard\"],[48325,\"AlphaDataDiscard\"],[50215,\"OceScanjobDesc\"],[50216,\"OceApplicationSelector\"],[50217,\"OceIDNumber\"],[50218,\"OceImageLogic\"],[50255,\"Annotations\"],[50459,\"HasselbladExif\"],[50547,\"OriginalFileName\"],[50560,\"USPTOOriginalContentType\"],[50656,\"CR2CFAPattern\"],[50710,\"CFAPlaneColor\"],[50711,\"CFALayout\"],[50712,\"LinearizationTable\"],[50713,\"BlackLevelRepeatDim\"],[50714,\"BlackLevel\"],[50715,\"BlackLevelDeltaH\"],[50716,\"BlackLevelDeltaV\"],[50717,\"WhiteLevel\"],[50718,\"DefaultScale\"],[50719,\"DefaultCropOrigin\"],[50720,\"DefaultCropSize\"],[50733,\"BayerGreenSplit\"],[50737,\"ChromaBlurRadius\"],[50738,\"AntiAliasStrength\"],[50752,\"RawImageSegmentation\"],[50780,\"BestQualityScale\"],[50784,\"AliasLayerMetadata\"],[50829,\"ActiveArea\"],[50830,\"MaskedAreas\"],[50935,\"NoiseReductionApplied\"],[50974,\"SubTileBlockSize\"],[50975,\"RowInterleaveFactor\"],[51008,\"OpcodeList1\"],[51009,\"OpcodeList2\"],[51022,\"OpcodeList3\"],[51041,\"NoiseProfile\"],[51114,\"CacheVersion\"],[51125,\"DefaultUserCrop\"],[51157,\"NikonNEFInfo\"],[65024,\"KdcIFD\"]];F(E,\"ifd0\",ct),F(E,\"exif\",ct),U(B,\"gps\",[[23,{M:\"Magnetic North\",T:\"True North\"}],[25,{K:\"Kilometers\",M:\"Miles\",N:\"Nautical Miles\"}]]);class ft extends re{static canHandle(e,t){return 224===e.getUint8(t+1)&&1246120262===e.getUint32(t+4)&&0===e.getUint8(t+8)}parse(){return this.parseTags(),this.translate(),this.output}parseTags(){this.raw=new Map([[0,this.chunk.getUint16(0)],[2,this.chunk.getUint8(2)],[3,this.chunk.getUint16(3)],[5,this.chunk.getUint16(5)],[7,this.chunk.getUint8(7)],[8,this.chunk.getUint8(8)]])}}c(ft,\"type\",\"jfif\"),c(ft,\"headerLength\",9),T.set(\"jfif\",ft),U(E,\"jfif\",[[0,\"JFIFVersion\"],[2,\"ResolutionUnit\"],[3,\"XResolution\"],[5,\"YResolution\"],[7,\"ThumbnailWidth\"],[8,\"ThumbnailHeight\"]]);class dt extends re{parse(){return this.parseTags(),this.translate(),this.output}parseTags(){this.raw=new Map([[0,this.chunk.getUint32(0)],[4,this.chunk.getUint32(4)],[8,this.chunk.getUint8(8)],[9,this.chunk.getUint8(9)],[10,this.chunk.getUint8(10)],[11,this.chunk.getUint8(11)],[12,this.chunk.getUint8(12)],...Array.from(this.raw)])}}c(dt,\"type\",\"ihdr\"),T.set(\"ihdr\",dt),U(E,\"ihdr\",[[0,\"ImageWidth\"],[4,\"ImageHeight\"],[8,\"BitDepth\"],[9,\"ColorType\"],[10,\"Compression\"],[11,\"Filter\"],[12,\"Interlace\"]]),U(B,\"ihdr\",[[9,{0:\"Grayscale\",2:\"RGB\",3:\"Palette\",4:\"Grayscale with Alpha\",6:\"RGB with Alpha\",DEFAULT:\"Unknown\"}],[10,{0:\"Deflate/Inflate\",DEFAULT:\"Unknown\"}],[11,{0:\"Adaptive\",DEFAULT:\"Unknown\"}],[12,{0:\"Noninterlaced\",1:\"Adam7 Interlace\",DEFAULT:\"Unknown\"}]]);class pt extends re{static canHandle(e,t){return 226===e.getUint8(t+1)&&1229144927===e.getUint32(t+4)}static findPosition(e,t){let i=super.findPosition(e,t);return i.chunkNumber=e.getUint8(t+16),i.chunkCount=e.getUint8(t+17),i.multiSegment=i.chunkCount>1,i}static handleMultiSegments(e){return function(e){let t=function(e){let t=e[0].constructor,i=0;for(let t of e)i+=t.length;let n=new t(i),s=0;for(let t of e)n.set(t,s),s+=t.length;return n}(e.map((e=>e.chunk.toUint8())));return new I(t)}(e)}parse(){return this.raw=new Map,this.parseHeader(),this.parseTags(),this.translate(),this.output}parseHeader(){let{raw:e}=this;this.chunk.byteLength<84&&g(\"ICC header is too short\");for(let[t,i]of Object.entries(gt)){t=parseInt(t,10);let n=i(this.chunk,t);\"\\0\\0\\0\\0\"!==n&&e.set(t,n)}}parseTags(){let e,t,i,n,s,{raw:r}=this,a=this.chunk.getUint32(128),o=132,l=this.chunk.byteLength;for(;a--;){if(e=this.chunk.getString(o,4),t=this.chunk.getUint32(o+4),i=this.chunk.getUint32(o+8),n=this.chunk.getString(t,4),t+i>l)return void console.warn(\"reached the end of the first ICC chunk. Enable options.tiff.multiSegment to read all ICC segments.\");s=this.parseTag(n,t,i),void 0!==s&&\"\\0\\0\\0\\0\"!==s&&r.set(e,s),o+=12}}parseTag(e,t,i){switch(e){case\"desc\":return this.parseDesc(t);case\"mluc\":return this.parseMluc(t);case\"text\":return this.parseText(t,i);case\"sig \":return this.parseSig(t)}if(!(t+i>this.chunk.byteLength))return this.chunk.getUint8Array(t,i)}parseDesc(e){let t=this.chunk.getUint32(e+8)-1;return m(this.chunk.getString(e+12,t))}parseText(e,t){return m(this.chunk.getString(e+8,t-8))}parseSig(e){return m(this.chunk.getString(e+8,4))}parseMluc(e){let{chunk:t}=this,i=t.getUint32(e+8),n=t.getUint32(e+12),s=e+16,r=[];for(let a=0;a>4,e.getUint8(t+1)%16].map((e=>e.toString(10))).join(\".\")},12:mt,16:mt,20:mt,24:function(e,t){const i=e.getUint16(t),n=e.getUint16(t+2)-1,s=e.getUint16(t+4),r=e.getUint16(t+6),a=e.getUint16(t+8),o=e.getUint16(t+10);return new Date(Date.UTC(i,n,s,r,a,o))},36:mt,40:mt,48:mt,52:mt,64:(e,t)=>e.getUint32(t),80:mt};function mt(e,t){return m(e.getString(t,4))}T.set(\"icc\",pt),U(E,\"icc\",[[4,\"ProfileCMMType\"],[8,\"ProfileVersion\"],[12,\"ProfileClass\"],[16,\"ColorSpaceData\"],[20,\"ProfileConnectionSpace\"],[24,\"ProfileDateTime\"],[36,\"ProfileFileSignature\"],[40,\"PrimaryPlatform\"],[44,\"CMMFlags\"],[48,\"DeviceManufacturer\"],[52,\"DeviceModel\"],[56,\"DeviceAttributes\"],[64,\"RenderingIntent\"],[68,\"ConnectionSpaceIlluminant\"],[80,\"ProfileCreator\"],[84,\"ProfileID\"],[\"Header\",\"ProfileHeader\"],[\"MS00\",\"WCSProfiles\"],[\"bTRC\",\"BlueTRC\"],[\"bXYZ\",\"BlueMatrixColumn\"],[\"bfd\",\"UCRBG\"],[\"bkpt\",\"MediaBlackPoint\"],[\"calt\",\"CalibrationDateTime\"],[\"chad\",\"ChromaticAdaptation\"],[\"chrm\",\"Chromaticity\"],[\"ciis\",\"ColorimetricIntentImageState\"],[\"clot\",\"ColorantTableOut\"],[\"clro\",\"ColorantOrder\"],[\"clrt\",\"ColorantTable\"],[\"cprt\",\"ProfileCopyright\"],[\"crdi\",\"CRDInfo\"],[\"desc\",\"ProfileDescription\"],[\"devs\",\"DeviceSettings\"],[\"dmdd\",\"DeviceModelDesc\"],[\"dmnd\",\"DeviceMfgDesc\"],[\"dscm\",\"ProfileDescriptionML\"],[\"fpce\",\"FocalPlaneColorimetryEstimates\"],[\"gTRC\",\"GreenTRC\"],[\"gXYZ\",\"GreenMatrixColumn\"],[\"gamt\",\"Gamut\"],[\"kTRC\",\"GrayTRC\"],[\"lumi\",\"Luminance\"],[\"meas\",\"Measurement\"],[\"meta\",\"Metadata\"],[\"mmod\",\"MakeAndModel\"],[\"ncl2\",\"NamedColor2\"],[\"ncol\",\"NamedColor\"],[\"ndin\",\"NativeDisplayInfo\"],[\"pre0\",\"Preview0\"],[\"pre1\",\"Preview1\"],[\"pre2\",\"Preview2\"],[\"ps2i\",\"PS2RenderingIntent\"],[\"ps2s\",\"PostScript2CSA\"],[\"psd0\",\"PostScript2CRD0\"],[\"psd1\",\"PostScript2CRD1\"],[\"psd2\",\"PostScript2CRD2\"],[\"psd3\",\"PostScript2CRD3\"],[\"pseq\",\"ProfileSequenceDesc\"],[\"psid\",\"ProfileSequenceIdentifier\"],[\"psvm\",\"PS2CRDVMSize\"],[\"rTRC\",\"RedTRC\"],[\"rXYZ\",\"RedMatrixColumn\"],[\"resp\",\"OutputResponse\"],[\"rhoc\",\"ReflectionHardcopyOrigColorimetry\"],[\"rig0\",\"PerceptualRenderingIntentGamut\"],[\"rig2\",\"SaturationRenderingIntentGamut\"],[\"rpoc\",\"ReflectionPrintOutputColorimetry\"],[\"sape\",\"SceneAppearanceEstimates\"],[\"scoe\",\"SceneColorimetryEstimates\"],[\"scrd\",\"ScreeningDesc\"],[\"scrn\",\"Screening\"],[\"targ\",\"CharTarget\"],[\"tech\",\"Technology\"],[\"vcgt\",\"VideoCardGamma\"],[\"view\",\"ViewingConditions\"],[\"vued\",\"ViewingCondDesc\"],[\"wtpt\",\"MediaWhitePoint\"]]);const St={\"4d2p\":\"Erdt Systems\",AAMA:\"Aamazing Technologies\",ACER:\"Acer\",ACLT:\"Acolyte Color Research\",ACTI:\"Actix Sytems\",ADAR:\"Adara Technology\",ADBE:\"Adobe\",ADI:\"ADI Systems\",AGFA:\"Agfa Graphics\",ALMD:\"Alps Electric\",ALPS:\"Alps Electric\",ALWN:\"Alwan Color Expertise\",AMTI:\"Amiable Technologies\",AOC:\"AOC International\",APAG:\"Apago\",APPL:\"Apple Computer\",AST:\"AST\",\"AT&T\":\"AT&T\",BAEL:\"BARBIERI electronic\",BRCO:\"Barco NV\",BRKP:\"Breakpoint\",BROT:\"Brother\",BULL:\"Bull\",BUS:\"Bus Computer Systems\",\"C-IT\":\"C-Itoh\",CAMR:\"Intel\",CANO:\"Canon\",CARR:\"Carroll Touch\",CASI:\"Casio\",CBUS:\"Colorbus PL\",CEL:\"Crossfield\",CELx:\"Crossfield\",CGS:\"CGS Publishing Technologies International\",CHM:\"Rochester Robotics\",CIGL:\"Colour Imaging Group, London\",CITI:\"Citizen\",CL00:\"Candela\",CLIQ:\"Color IQ\",CMCO:\"Chromaco\",CMiX:\"CHROMiX\",COLO:\"Colorgraphic Communications\",COMP:\"Compaq\",COMp:\"Compeq/Focus Technology\",CONR:\"Conrac Display Products\",CORD:\"Cordata Technologies\",CPQ:\"Compaq\",CPRO:\"ColorPro\",CRN:\"Cornerstone\",CTX:\"CTX International\",CVIS:\"ColorVision\",CWC:\"Fujitsu Laboratories\",DARI:\"Darius Technology\",DATA:\"Dataproducts\",DCP:\"Dry Creek Photo\",DCRC:\"Digital Contents Resource Center, Chung-Ang University\",DELL:\"Dell Computer\",DIC:\"Dainippon Ink and Chemicals\",DICO:\"Diconix\",DIGI:\"Digital\",\"DL&C\":\"Digital Light & Color\",DPLG:\"Doppelganger\",DS:\"Dainippon Screen\",DSOL:\"DOOSOL\",DUPN:\"DuPont\",EPSO:\"Epson\",ESKO:\"Esko-Graphics\",ETRI:\"Electronics and Telecommunications Research Institute\",EVER:\"Everex Systems\",EXAC:\"ExactCODE\",Eizo:\"Eizo\",FALC:\"Falco Data Products\",FF:\"Fuji Photo Film\",FFEI:\"FujiFilm Electronic Imaging\",FNRD:\"Fnord Software\",FORA:\"Fora\",FORE:\"Forefront Technology\",FP:\"Fujitsu\",FPA:\"WayTech Development\",FUJI:\"Fujitsu\",FX:\"Fuji Xerox\",GCC:\"GCC Technologies\",GGSL:\"Global Graphics Software\",GMB:\"Gretagmacbeth\",GMG:\"GMG\",GOLD:\"GoldStar Technology\",GOOG:\"Google\",GPRT:\"Giantprint\",GTMB:\"Gretagmacbeth\",GVC:\"WayTech Development\",GW2K:\"Sony\",HCI:\"HCI\",HDM:\"Heidelberger Druckmaschinen\",HERM:\"Hermes\",HITA:\"Hitachi America\",HP:\"Hewlett-Packard\",HTC:\"Hitachi\",HiTi:\"HiTi Digital\",IBM:\"IBM\",IDNT:\"Scitex\",IEC:\"Hewlett-Packard\",IIYA:\"Iiyama North America\",IKEG:\"Ikegami Electronics\",IMAG:\"Image Systems\",IMI:\"Ingram Micro\",INTC:\"Intel\",INTL:\"N/A (INTL)\",INTR:\"Intra Electronics\",IOCO:\"Iocomm International Technology\",IPS:\"InfoPrint Solutions Company\",IRIS:\"Scitex\",ISL:\"Ichikawa Soft Laboratory\",ITNL:\"N/A (ITNL)\",IVM:\"IVM\",IWAT:\"Iwatsu Electric\",Idnt:\"Scitex\",Inca:\"Inca Digital Printers\",Iris:\"Scitex\",JPEG:\"Joint Photographic Experts Group\",JSFT:\"Jetsoft Development\",JVC:\"JVC Information Products\",KART:\"Scitex\",KFC:\"KFC Computek Components\",KLH:\"KLH Computers\",KMHD:\"Konica Minolta\",KNCA:\"Konica\",KODA:\"Kodak\",KYOC:\"Kyocera\",Kart:\"Scitex\",LCAG:\"Leica\",LCCD:\"Leeds Colour\",LDAK:\"Left Dakota\",LEAD:\"Leading Technology\",LEXM:\"Lexmark International\",LINK:\"Link Computer\",LINO:\"Linotronic\",LITE:\"Lite-On\",Leaf:\"Leaf\",Lino:\"Linotronic\",MAGC:\"Mag Computronic\",MAGI:\"MAG Innovision\",MANN:\"Mannesmann\",MICN:\"Micron Technology\",MICR:\"Microtek\",MICV:\"Microvitec\",MINO:\"Minolta\",MITS:\"Mitsubishi Electronics America\",MITs:\"Mitsuba\",MNLT:\"Minolta\",MODG:\"Modgraph\",MONI:\"Monitronix\",MONS:\"Monaco Systems\",MORS:\"Morse Technology\",MOTI:\"Motive Systems\",MSFT:\"Microsoft\",MUTO:\"MUTOH INDUSTRIES\",Mits:\"Mitsubishi Electric\",NANA:\"NANAO\",NEC:\"NEC\",NEXP:\"NexPress Solutions\",NISS:\"Nissei Sangyo America\",NKON:\"Nikon\",NONE:\"none\",OCE:\"Oce Technologies\",OCEC:\"OceColor\",OKI:\"Oki\",OKID:\"Okidata\",OKIP:\"Okidata\",OLIV:\"Olivetti\",OLYM:\"Olympus\",ONYX:\"Onyx Graphics\",OPTI:\"Optiquest\",PACK:\"Packard Bell\",PANA:\"Matsushita Electric Industrial\",PANT:\"Pantone\",PBN:\"Packard Bell\",PFU:\"PFU\",PHIL:\"Philips Consumer Electronics\",PNTX:\"HOYA\",POne:\"Phase One A/S\",PREM:\"Premier Computer Innovations\",PRIN:\"Princeton Graphic Systems\",PRIP:\"Princeton Publishing Labs\",QLUX:\"Hong Kong\",QMS:\"QMS\",QPCD:\"QPcard AB\",QUAD:\"QuadLaser\",QUME:\"Qume\",RADI:\"Radius\",RDDx:\"Integrated Color Solutions\",RDG:\"Roland DG\",REDM:\"REDMS Group\",RELI:\"Relisys\",RGMS:\"Rolf Gierling Multitools\",RICO:\"Ricoh\",RNLD:\"Edmund Ronald\",ROYA:\"Royal\",RPC:\"Ricoh Printing Systems\",RTL:\"Royal Information Electronics\",SAMP:\"Sampo\",SAMS:\"Samsung\",SANT:\"Jaime Santana Pomares\",SCIT:\"Scitex\",SCRN:\"Dainippon Screen\",SDP:\"Scitex\",SEC:\"Samsung\",SEIK:\"Seiko Instruments\",SEIk:\"Seikosha\",SGUY:\"ScanGuy.com\",SHAR:\"Sharp Laboratories\",SICC:\"International Color Consortium\",SONY:\"Sony\",SPCL:\"SpectraCal\",STAR:\"Star\",STC:\"Sampo Technology\",Scit:\"Scitex\",Sdp:\"Scitex\",Sony:\"Sony\",TALO:\"Talon Technology\",TAND:\"Tandy\",TATU:\"Tatung\",TAXA:\"TAXAN America\",TDS:\"Tokyo Denshi Sekei\",TECO:\"TECO Information Systems\",TEGR:\"Tegra\",TEKT:\"Tektronix\",TI:\"Texas Instruments\",TMKR:\"TypeMaker\",TOSB:\"Toshiba\",TOSH:\"Toshiba\",TOTK:\"TOTOKU ELECTRIC\",TRIU:\"Triumph\",TSBT:\"Toshiba\",TTX:\"TTX Computer Products\",TVM:\"TVM Professional Monitor\",TW:\"TW Casper\",ULSX:\"Ulead Systems\",UNIS:\"Unisys\",UTZF:\"Utz Fehlau & Sohn\",VARI:\"Varityper\",VIEW:\"Viewsonic\",VISL:\"Visual communication\",VIVO:\"Vivo Mobile Communication\",WANG:\"Wang\",WLBR:\"Wilbur Imaging\",WTG2:\"Ware To Go\",WYSE:\"WYSE Technology\",XERX:\"Xerox\",XRIT:\"X-Rite\",ZRAN:\"Zoran\",Zebr:\"Zebra Technologies\",appl:\"Apple Computer\",bICC:\"basICColor\",berg:\"bergdesign\",ceyd:\"Integrated Color Solutions\",clsp:\"MacDermid ColorSpan\",ds:\"Dainippon Screen\",dupn:\"DuPont\",ffei:\"FujiFilm Electronic Imaging\",flux:\"FluxData\",iris:\"Scitex\",kart:\"Scitex\",lcms:\"Little CMS\",lino:\"Linotronic\",none:\"none\",ob4d:\"Erdt Systems\",obic:\"Medigraph\",quby:\"Qubyx Sarl\",scit:\"Scitex\",scrn:\"Dainippon Screen\",sdp:\"Scitex\",siwi:\"SIWI GRAFIKA\",yxym:\"YxyMaster\"},Ct={scnr:\"Scanner\",mntr:\"Monitor\",prtr:\"Printer\",link:\"Device Link\",abst:\"Abstract\",spac:\"Color Space Conversion Profile\",nmcl:\"Named Color\",cenc:\"ColorEncodingSpace profile\",mid:\"MultiplexIdentification profile\",mlnk:\"MultiplexLink profile\",mvis:\"MultiplexVisualization profile\",nkpf:\"Nikon Input Device Profile (NON-STANDARD!)\"};U(B,\"icc\",[[4,St],[12,Ct],[40,Object.assign({},St,Ct)],[48,St],[80,St],[64,{0:\"Perceptual\",1:\"Relative Colorimetric\",2:\"Saturation\",3:\"Absolute Colorimetric\"}],[\"tech\",{amd:\"Active Matrix Display\",crt:\"Cathode Ray Tube Display\",kpcd:\"Photo CD\",pmd:\"Passive Matrix Display\",dcam:\"Digital Camera\",dcpj:\"Digital Cinema Projector\",dmpc:\"Digital Motion Picture Camera\",dsub:\"Dye Sublimation Printer\",epho:\"Electrophotographic Printer\",esta:\"Electrostatic Printer\",flex:\"Flexography\",fprn:\"Film Writer\",fscn:\"Film Scanner\",grav:\"Gravure\",ijet:\"Ink Jet Printer\",imgs:\"Photo Image Setter\",mpfr:\"Motion Picture Film Recorder\",mpfs:\"Motion Picture Film Scanner\",offs:\"Offset Lithography\",pjtv:\"Projection Television\",rpho:\"Photographic Paper Printer\",rscn:\"Reflective Scanner\",silk:\"Silkscreen\",twax:\"Thermal Wax Printer\",vidc:\"Video Camera\",vidm:\"Video Monitor\"}]]);class yt extends re{static canHandle(e,t,i){return 237===e.getUint8(t+1)&&\"Photoshop\"===e.getString(t+4,9)&&void 0!==this.containsIptc8bim(e,t,i)}static headerLength(e,t,i){let n,s=this.containsIptc8bim(e,t,i);if(void 0!==s)return n=e.getUint8(t+s+7),n%2!=0&&(n+=1),0===n&&(n=4),s+8+n}static containsIptc8bim(e,t,i){for(let n=0;n {\n if (loaded.length > 0) {\n drawPhotos.fitZoom(false);\n }\n });\n })\n .on('dragenter.svgLocalPhotos', over)\n .on('dragexit.svgLocalPhotos', over)\n .on('dragover.svgLocalPhotos', over);\n\n _initialized = true;\n }\n\n function ensureViewerLoaded(context) {\n if (_photoFrame) {\n return Promise.resolve(_photoFrame);\n }\n\n const viewer = context.container().select('.photoviewer')\n .selectAll('.local-photos-wrapper')\n .data([0]);\n\n const viewerEnter = viewer.enter()\n .append('div')\n .attr('class', 'photo-wrapper local-photos-wrapper')\n .classed('hide', true);\n\n viewerEnter\n .append('div')\n .attr('class', 'photo-attribution photo-attribution-dual fillD');\n\n const controlsEnter = viewerEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls-local');\n\n controlsEnter\n .append('button')\n .classed('back', true)\n .on('click.back', () => stepPhotos(-1))\n .text('\u25C0');\n\n controlsEnter\n .append('button')\n .classed('forward', true)\n .on('click.forward', () => stepPhotos(1))\n .text('\u25B6');\n\n return planePhotoFrame(context, viewerEnter)\n .then(planePhotoFrame => _photoFrame = planePhotoFrame);\n }\n\n function stepPhotos(stepBy){\n if (!_photos || _photos.length === 0) return;\n if (_activePhotoIdx === undefined) _activePhotoIdx = 0;\n\n const newIndex = _activePhotoIdx + stepBy;\n _activePhotoIdx = Math.max(0, Math.min(_photos.length - 1, newIndex));\n\n click(null, _photos[_activePhotoIdx], false);\n }\n\n // opens the image at bottom left\n function click(d3_event, image, zoomTo) {\n _activePhotoIdx = _photos.indexOf(image);\n ensureViewerLoaded(context).then(() => {\n const viewer = context.container().select('.photoviewer')\n .datum(image);\n\n const viewerWrap = viewer.select('.local-photos-wrapper');\n const isHidden = viewerWrap.classed('hide');\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n viewer.classed('hide', false);\n viewerWrap.classed('hide', false);\n }\n\n const controlsWrap = viewerWrap.select('.photo-controls-wrap');\n\n controlsWrap.select('.back')\n .attr('disabled', _activePhotoIdx <= 0 ? true: null);\n controlsWrap.select('.forward')\n .attr('disabled', _activePhotoIdx >= _photos.length - 1 ? true: null);\n\n const attribution = viewerWrap.selectAll('.photo-attribution').text('');\n\n if (image.date) {\n attribution\n .append('span')\n .text(image.date.toLocaleString(localizer.localeCode()));\n }\n if (image.name) {\n attribution\n .append('span')\n .classed('filename', true)\n .text(image.name);\n }\n\n _photoFrame.selectPhoto({ image_path: '' });\n image.getSrc().then(src => {\n _photoFrame\n .selectPhoto({ image_path: src })\n .showPhotoFrame(viewerWrap);\n\n setStyles();\n });\n });\n\n // centers the map with image location\n if (zoomTo) {\n context.map().centerEase(image.loc);\n }\n }\n\n\n function transform(d) {\n // projection expects [long, lat]\n var svgpoint = projection(d.loc);\n return 'translate(' + svgpoint[0] + ',' + svgpoint[1] + ')';\n }\n\n function setStyles(hovered) {\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n\n context.container().selectAll('.layer-local-photos .viewfield-group')\n .classed('hovered', d => d.id === hovered?.id)\n .classed('highlighted', d => d.id === hovered?.id || d.id === selected?.id)\n .classed('currentView', d => d.id === selected?.id);\n }\n\n // puts the image markers on the map\n function display_markers(imageList) {\n imageList = imageList.filter(image => isArray(image.loc) && isNumber(image.loc[0]) && isNumber(image.loc[1]));\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(imageList, function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', (d3_event, d) => setStyles(d))\n .on('mouseleave', () => setStyles(null))\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const showViewfields = context.map().zoom() >= minViewfieldZoom;\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n // viewfields may or may not be drawn...\n // but if they are, draw below the circles\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', function() {\n const d = this.parentNode.__data__;\n return `rotate(${Math.round(d.direction ?? 0)},0,0),scale(1.5,1.5),translate(-8,-13)`;\n })\n .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z')\n .style('visibility', function() {\n const d = this.parentNode.__data__;\n return isNumber(d.direction) ? 'visible' : 'hidden';\n });\n }\n\n function drawPhotos(selection) {\n layer = selection.selectAll('.layer-local-photos')\n .data(_photos ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-local-photos');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (_photos) {\n display_markers(_photos);\n }\n }\n\n\n function readFileAsDataURL(file) {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result);\n reader.onerror = error => reject(error);\n reader.readAsDataURL(file);\n });\n }\n /**\n * Reads and parses files\n * @param {Array} files - Holds array of file - [file_1, file_2, ...]\n */\n async function readmultifiles(files, callback) {\n const loaded = [];\n\n for (const file of files) {\n try {\n const exifData = await exifr.parse(file); // eslint-disable-line no-await-in-loop\n const photo = {\n service: 'photo',\n id: _idAutoinc++,\n name: file.name,\n getSrc: () => readFileAsDataURL(file),\n file: file,\n loc: [exifData.longitude, exifData.latitude],\n direction: exifData.GPSImgDirection,\n date: exifData.CreateDate || exifData.DateTimeOriginal || exifData.ModifyDate,\n };\n loaded.push(photo);\n const sameName = _photos.filter(i => i.name === photo.name);\n if (sameName.length === 0) {\n _photos.push(photo);\n } else {\n const thisContent = await photo.getSrc(); // eslint-disable-line no-await-in-loop\n const sameNameContent = await Promise.allSettled(sameName.map(i => i.getSrc())); // eslint-disable-line no-await-in-loop\n if (!sameNameContent.some(i => i.value === thisContent)) {\n _photos.push(photo);\n }\n }\n } catch {\n // skip files which are not a supported image file\n }\n }\n\n if (typeof callback === 'function') callback(loaded);\n dispatch.call('change');\n }\n\n drawPhotos.setFiles = function(fileList, callback) {\n // read and parse asynchronously\n readmultifiles(Array.from(fileList), callback);\n return this;\n };\n\n // Step 1: entry point\n /**\n * Sets the fileList\n * @param {Object} fileList - The uploaded files. fileList is an object, not an array object\n * @param {Object} fileList.0 - A File - {name: \"Das.png\", lastModified: 1625064498536, lastModifiedDate: Wed Jun 30 2021 20:18:18 GMT+0530 (India Standard Time), webkitRelativePath: \"\", size: 859658, \u2026}\n * @param {Function} callback - A callback to be called after the photos have been loaded and parsed\n */\n drawPhotos.fileList = function(fileList, callback) {\n if (!arguments.length) return _fileList;\n\n _fileList = fileList;\n\n if (!fileList || !fileList.length) return this;\n\n drawPhotos.setFiles(_fileList, callback);\n\n return this;\n };\n\n drawPhotos.getPhotos = function() {\n return _photos;\n };\n\n drawPhotos.removePhoto = function(id) {\n _photos = _photos.filter(i => i.id !== id);\n dispatch.call('change');\n return _photos;\n };\n\n drawPhotos.openPhoto = click;\n\n drawPhotos.fitZoom = function(force) {\n const coords = _photos\n .map(image => image.loc)\n .filter(l => isArray(l) && isNumber(l[0]) && isNumber(l[1]));\n if (coords.length === 0) return;\n const extent = coords\n .map(l => geoExtent(l, l))\n .reduce((a, b) => a.extend(b));\n\n const map = context.map();\n var viewport = map.trimmedExtent().polygon();\n\n if (force !== false || !geoPolygonIntersectsPolygon(viewport, coords, true)) {\n map.centerZoom(extent.center(), Math.min(18, map.trimmedExtentZoom(extent)));\n }\n };\n\n function showLayer() {\n layer.style('display', 'block');\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', () => {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n });\n }\n\n drawPhotos.enabled = function(val) {\n if (!arguments.length) return _enabled;\n\n _enabled = val;\n if (_enabled) {\n showLayer();\n } else {\n hideLayer();\n }\n\n dispatch.call('change');\n return this;\n };\n\n drawPhotos.hasData = function() {\n return isArray(_photos) && _photos.length > 0;\n };\n\n\n init();\n return drawPhotos;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\n\nimport { modeBrowse } from '../modes/browse';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\n\nlet _layerEnabled = false;\nlet _qaService;\n\nexport function svgOsmose(projection, context, dispatch) {\n const throttledRedraw = throttle(() => dispatch.call('change'), 1000);\n const minZoom = 12;\n\n let touchLayer = d3_select(null);\n let drawLayer = d3_select(null);\n let layerVisible = false;\n\n function markerPath(selection, klass) {\n selection\n .attr('class', klass)\n .attr('transform', 'translate(-10, -28)')\n .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');\n }\n\n // Loosely-coupled osmose service for fetching issues\n function getService() {\n if (services.osmose && !_qaService) {\n _qaService = services.osmose;\n _qaService.on('loaded', throttledRedraw);\n } else if (!services.osmose && _qaService) {\n _qaService = null;\n }\n\n return _qaService;\n }\n\n // Show the markers\n function editOn() {\n if (!layerVisible) {\n layerVisible = true;\n drawLayer\n .style('display', 'block');\n }\n }\n\n // Immediately remove the markers and their touch targets\n function editOff() {\n if (layerVisible) {\n layerVisible = false;\n drawLayer\n .style('display', 'none');\n drawLayer.selectAll('.qaItem.osmose')\n .remove();\n touchLayer.selectAll('.qaItem.osmose')\n .remove();\n }\n }\n\n // Enable the layer. This shows the markers and transitions them to visible.\n function layerOn() {\n editOn();\n\n drawLayer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end interrupt', () => dispatch.call('change'));\n }\n\n // Disable the layer. This transitions the layer invisible and then hides the markers.\n function layerOff() {\n throttledRedraw.cancel();\n drawLayer.interrupt();\n touchLayer.selectAll('.qaItem.osmose')\n .remove();\n\n drawLayer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end interrupt', () => {\n editOff();\n dispatch.call('change');\n });\n }\n\n // Update the issue markers\n function updateMarkers() {\n if (!layerVisible || !_layerEnabled) return;\n\n const service = getService();\n const selectedID = context.selectedErrorID();\n const data = (service ? service.getItems(projection) : []);\n const getTransform = svgPointTransform(projection);\n\n // Draw markers..\n const markers = drawLayer.selectAll('.qaItem.osmose')\n .data(data, d => d.id);\n\n // exit\n markers.exit()\n .remove();\n\n // enter\n const markersEnter = markers.enter()\n .append('g')\n .attr('class', d => `qaItem ${d.service} itemId-${d.id} itemType-${d.itemType}`);\n\n markersEnter\n .append('polygon')\n .call(markerPath, 'shadow');\n\n markersEnter\n .append('ellipse')\n .attr('cx', 0)\n .attr('cy', 0)\n .attr('rx', 4.5)\n .attr('ry', 2)\n .attr('class', 'stroke');\n\n markersEnter\n .append('polygon')\n .attr('fill', d => service.getColor(d.item))\n .call(markerPath, 'qaItem-fill');\n\n markersEnter\n .append('use')\n .attr('class', 'icon-annotation')\n .attr('transform', 'translate(-6, -22)')\n .attr('width', '12px')\n .attr('height', '12px')\n .attr('xlink:href', d => d.icon ? '#' + d.icon : '');\n\n // update\n markers\n .merge(markersEnter)\n .sort(sortY)\n .classed('selected', d => d.id === selectedID)\n .attr('transform', getTransform);\n\n // Draw targets..\n if (touchLayer.empty()) return;\n const fillClass = context.getDebug('target') ? 'pink' : 'nocolor';\n\n const targets = touchLayer.selectAll('.qaItem.osmose')\n .data(data, d => d.id);\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('rect')\n .attr('width', '20px')\n .attr('height', '30px')\n .attr('x', '-10px')\n .attr('y', '-28px')\n .merge(targets)\n .sort(sortY)\n .attr('class', d => `qaItem ${d.service} target ${fillClass} itemId-${d.id}`)\n .attr('transform', getTransform);\n\n function sortY(a, b) {\n return (a.id === selectedID) ? 1\n : (b.id === selectedID) ? -1\n : b.loc[1] - a.loc[1];\n }\n }\n\n // Draw the Osmose layer and schedule loading issues and updating markers.\n function drawOsmose(selection) {\n const service = getService();\n\n const surface = context.surface();\n if (surface && !surface.empty()) {\n touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');\n }\n\n drawLayer = selection.selectAll('.layer-osmose')\n .data(service ? [0] : []);\n\n drawLayer.exit()\n .remove();\n\n drawLayer = drawLayer.enter()\n .append('g')\n .attr('class', 'layer-osmose')\n .style('display', _layerEnabled ? 'block' : 'none')\n .merge(drawLayer);\n\n if (_layerEnabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n service.loadIssues(projection);\n updateMarkers();\n } else {\n editOff();\n }\n }\n }\n\n // Toggles the layer on and off\n drawOsmose.enabled = function(val) {\n if (!arguments.length) return _layerEnabled;\n\n _layerEnabled = val;\n if (_layerEnabled) {\n // Strings supplied by Osmose fetched before showing layer for first time\n // NOTE: Currently no way to change locale in iD at runtime, would need to re-call this method if that's ever implemented\n // Also, If layer is toggled quickly multiple requests are sent\n getService().loadStrings()\n .then(layerOn)\n .catch(err => {\n console.log(err); // eslint-disable-line no-console\n });\n } else {\n layerOff();\n if (context.selectedErrorID()) {\n context.enter(modeBrowse(context));\n }\n }\n\n dispatch.call('change');\n return this;\n };\n\n drawOsmose.supported = () => !!getService();\n\n return drawOsmose;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgStreetside(projection, context, dispatch) {\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var minZoom = 14;\n var minMarkerZoom = 16;\n var minViewfieldZoom = 18;\n var layer = d3_select(null);\n var _viewerYaw = 0;\n var _selectedSequence = null;\n var _streetside;\n\n /**\n * init().\n */\n function init() {\n if (svgStreetside.initialized) return; // run once\n svgStreetside.enabled = false;\n svgStreetside.initialized = true;\n }\n\n /**\n * getService().\n */\n function getService() {\n if (services.streetside && !_streetside) {\n _streetside = services.streetside;\n _streetside.event\n .on('viewerChanged.svgStreetside', viewerChanged)\n .on('loadedImages.svgStreetside', throttledRedraw);\n } else if (!services.streetside && _streetside) {\n _streetside = null;\n }\n\n return _streetside;\n }\n\n /**\n * showLayer().\n */\n function showLayer() {\n var service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n /**\n * hideLayer().\n */\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n /**\n * editOn().\n */\n function editOn() {\n layer.style('display', 'block');\n }\n\n /**\n * editOff().\n */\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n /**\n * click() Handles 'bubble' point click event.\n */\n function click(d3_event, d) {\n var service = getService();\n if (!service) return;\n\n // try to preserve the viewer rotation when staying on the same sequence\n if (d.sequenceKey !== _selectedSequence) {\n _viewerYaw = 0; // reset\n }\n _selectedSequence = d.sequenceKey;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, d.key)\n .yaw(_viewerYaw)\n .showViewer(context);\n });\n\n context.map().centerEase(d.loc);\n }\n\n /**\n * mouseover().\n */\n function mouseover(d3_event, d) {\n var service = getService();\n if (service) service.setStyles(context, d);\n }\n\n /**\n * mouseout().\n */\n function mouseout() {\n var service = getService();\n if (service) service.setStyles(context, null);\n }\n\n /**\n * transform().\n */\n function transform(d) {\n var t = svgPointTransform(projection)(d);\n var rot = d.ca + _viewerYaw;\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n\n function viewerChanged() {\n var service = getService();\n if (!service) return;\n\n var viewer = service.viewer();\n if (!viewer) return;\n\n // update viewfield rotation\n _viewerYaw = viewer.getYaw();\n\n // avoid updating if the map is currently transformed\n // e.g. during drags or easing.\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .attr('transform', transform);\n }\n\n\n function filterBubbles(bubbles, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n bubbles = bubbles.filter(function(bubble) {\n return new Date(bubble.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n bubbles = bubbles.filter(function(bubble) {\n return new Date(bubble.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n bubbles = bubbles.filter(function(bubble) {\n return usernames.indexOf(bubble.captured_by) !== -1;\n });\n }\n\n return bubbles;\n }\n\n function filterSequences(sequences, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n sequences = sequences.filter(function(sequences) {\n return new Date(sequences.properties.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n sequences = sequences.filter(function(sequences) {\n return new Date(sequences.properties.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n sequences = sequences.filter(function(sequences) {\n return usernames.indexOf(sequences.properties.captured_by) !== -1;\n });\n }\n\n return sequences;\n }\n\n /**\n * update().\n */\n function update() {\n var viewer = context.container().select('.photoviewer');\n var selected = viewer.empty() ? undefined : viewer.datum();\n var z = ~~context.map().zoom();\n var showMarkers = (z >= minMarkerZoom);\n var showViewfields = (z >= minViewfieldZoom);\n var service = getService();\n\n var sequences = [];\n var bubbles = [];\n\n if (context.photos().showsPanoramic()) {\n sequences = (service ? service.sequences(projection) : []);\n bubbles = (service && showMarkers ? service.bubbles(projection) : []);\n sequences = filterSequences(sequences);\n bubbles = filterBubbles(bubbles);\n }\n\n var traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.key; });\n\n dispatch.call('photoDatesChanged', this, 'streetside', [\n ...filterBubbles(bubbles, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(t => t.properties.vintageStart)]);\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n var groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(bubbles, function(d) {\n // force reenter once bubbles are attached to a sequence\n return d.key + (d.sequenceKey ? 'v1' : 'v0');\n });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n var markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n return (a === selected) ? 1\n : (b === selected) ? -1\n : b.loc[1] - a.loc[1];\n })\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n var viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n // viewfields may or may not be drawn...\n // but if they are, draw below the circles\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n var d = this.parentNode.__data__;\n if (d.pano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n }\n\n /**\n * drawImages()\n * drawImages is the method that is returned (and that runs) every time 'svgStreetside()' is called.\n * 'svgStreetside()' is called from index.js\n */\n function drawImages(selection) {\n var enabled = svgStreetside.enabled;\n var service = getService();\n\n layer = selection.selectAll('.layer-streetside-images')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n var layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-streetside-images')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadBubbles(projection);\n } else {\n dispatch.call('photoDatesChanged', this, 'streetside', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'streetside', []);\n }\n }\n\n\n /**\n * drawImages.enabled().\n */\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgStreetside.enabled;\n svgStreetside.enabled = _;\n if (svgStreetside.enabled) {\n showLayer();\n context.photos().on('change.streetside', update);\n } else {\n hideLayer();\n context.photos().on('change.streetside', null);\n }\n dispatch.call('change');\n return this;\n };\n\n /**\n * drawImages.supported().\n */\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n init();\n\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgVegbilder(projection, context, dispatch) {\n const throttledRedraw = throttle(() => dispatch.call('change'), 1000);\n const minZoom = 14;\n const minMarkerZoom = 16;\n const minViewfieldZoom = 18;\n let layer = d3_select(null);\n let _viewerYaw = 0;\n let _vegbilder;\n\n /**\n * init().\n */\n function init() {\n if (svgVegbilder.initialized) return; // run once\n svgVegbilder.enabled = false;\n svgVegbilder.initialized = true;\n }\n\n /**\n * getService().\n */\n function getService() {\n if (services.vegbilder && !_vegbilder) {\n _vegbilder = services.vegbilder;\n _vegbilder.event\n .on('viewerChanged.svgVegbilder', viewerChanged)\n .on('loadedImages.svgVegbilder', throttledRedraw);\n } else if (!services.vegbilder && _vegbilder) {\n _vegbilder = null;\n }\n\n return _vegbilder;\n }\n\n /**\n * showLayer().\n */\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', () => dispatch.call('change'));\n }\n\n /**\n * hideLayer().\n */\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n /**\n * editOn().\n */\n function editOn() {\n layer.style('display', 'block');\n }\n\n /**\n * editOff().\n */\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n function click(d3_event, d) {\n const service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(() => {\n service\n .selectImage(context, d.key)\n .showViewer(context);\n });\n\n context.map().centerEase(d.loc);\n }\n\n /**\n * mouseover().\n */\n function mouseover(d3_event, d) {\n const service = getService();\n if (service) service.setStyles(context, d);\n }\n\n /**\n * mouseout().\n */\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n /**\n * transform().\n */\n function transform(d, selected) {\n let t = svgPointTransform(projection)(d);\n let rot = d.ca;\n if (d === selected) {\n rot += _viewerYaw;\n }\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n\n function viewerChanged() {\n const service = getService();\n if (!service) return;\n\n const frame = service.photoFrame();\n if (!frame) return;\n\n // update viewfield rotation\n _viewerYaw = frame.getYaw();\n\n // avoid updating if the map is currently transformed\n // e.g. during drags or easing.\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .attr('transform', (d) => transform(d, d));\n }\n\n function filterImages(images, skipDateFilter) {\n const photoContext = context.photos();\n const fromDateString = photoContext.fromDate();\n const toDateString = photoContext.toDate();\n const showsFlat = photoContext.showsFlat();\n const showsPano = photoContext.showsPanoramic();\n\n if (fromDateString && !skipDateFilter) {\n const fromDate = new Date(fromDateString);\n images = images.filter(image => image.captured_at.getTime() >= fromDate.getTime());\n }\n\n if (toDateString && !skipDateFilter) {\n const toDate = new Date(toDateString);\n images = images.filter(image => image.captured_at.getTime() <= toDate.getTime());\n }\n\n if (!showsPano) {\n images = images.filter(image => !image.is_sphere);\n }\n\n if (!showsFlat) {\n images = images.filter(image => image.is_sphere);\n }\n\n return images;\n }\n\n function filterSequences(sequences, skipDateFilter) {\n const photoContext = context.photos();\n const fromDateString = photoContext.fromDate();\n const toDateString = photoContext.toDate();\n const showsFlat = photoContext.showsFlat();\n const showsPano = photoContext.showsPanoramic();\n\n if (fromDateString && !skipDateFilter) {\n const fromDate = new Date(fromDateString);\n sequences = sequences.filter(({images}) => images[0].captured_at.getTime() >= fromDate.getTime());\n }\n\n if (toDateString && !skipDateFilter) {\n const toDate = new Date(toDateString);\n sequences = sequences.filter(({images}) => images[images.length - 1].captured_at.getTime() <= toDate.getTime());\n }\n\n if (!showsPano) {\n sequences = sequences.filter(({images}) => !images[0].is_sphere);\n }\n\n if (!showsFlat) {\n sequences = sequences.filter(({images}) => images[0].is_sphere);\n }\n\n return sequences;\n }\n\n /**\n * update().\n */\n function update() {\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n const z = ~~context.map().zoom();\n const showMarkers = (z >= minMarkerZoom);\n const showViewfields = (z >= minViewfieldZoom);\n const service = getService();\n let sequences = [];\n let images = [];\n\n if (service) {\n // The WFS-layer for that year or image type may not be loaded after a filter is changed\n service.loadImages(context);\n\n sequences = service.sequences(projection);\n images = showMarkers ? service.images(projection) : [];\n\n dispatch.call('photoDatesChanged', this, 'vegbilder', [\n ...filterImages(images, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(s => s.images[0].captured_at)]);\n\n images = filterImages(images);\n sequences = filterSequences(sequences);\n }\n\n let traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, d => d.key);\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, (d) => d.key);\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort((a, b) => {\n return (a === selected) ? 1\n : (b === selected) ? -1\n : b.loc[1] - a.loc[1];\n })\n .attr('transform', (d) => transform(d, selected))\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n // viewfields may or may not be drawn...\n // but if they are, draw below the circles\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n const d = this.parentNode.__data__;\n if (d.is_sphere) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n }\n\n /**\n * drawImages()\n * drawImages is the method that is returned (and that runs) every time 'svgStreetside()' is called.\n * 'svgStreetside()' is called from index.js\n */\n function drawImages(selection) {\n const enabled = svgVegbilder.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-vegbilder')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-vegbilder')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadImages(context);\n } else {\n dispatch.call('photoDatesChanged', this, 'vegbilder', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'vegbilder', []);\n }\n }\n\n\n /**\n * drawImages.enabled().\n */\n drawImages.enabled = function (_) {\n if (!arguments.length) return svgVegbilder.enabled;\n svgVegbilder.enabled = _;\n if (svgVegbilder.enabled) {\n showLayer();\n context.photos().on('change.vegbilder', update);\n } else {\n hideLayer();\n context.photos().on('change.vegbilder', null);\n }\n dispatch.call('change');\n return this;\n };\n\n /**\n * drawImages.supported().\n */\n drawImages.supported = function () {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n drawImages.validHere = function(extent, zoom) {\n return zoom >= (minZoom - 2)\n && getService().validHere(extent);\n };\n\n init();\n\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgMapillaryImages(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const minZoom = 12;\n const minMarkerZoom = 16;\n const minViewfieldZoom = 18;\n let layer = d3_select(null);\n let _mapillary;\n\n\n function init() {\n if (svgMapillaryImages.initialized) return; // run once\n svgMapillaryImages.enabled = false;\n svgMapillaryImages.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('loadedImages', throttledRedraw);\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n\n return _mapillary;\n }\n\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, image) {\n const service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, image.id)\n .showViewer(context);\n });\n\n context.map().centerEase(image.loc);\n }\n\n\n function mouseover(d3_event, image) {\n const service = getService();\n\n if (service) service.setStyles(context, image);\n }\n\n\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n\n function transform(d) {\n let t = svgPointTransform(projection)(d);\n if (d.ca) {\n t += ' rotate(' + Math.floor(d.ca) + ',0,0)';\n }\n return t;\n }\n\n\n function filterImages(images, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n\n if (!showsPano || !showsFlat) {\n images = images.filter(function(image) {\n if (image.is_pano) return showsPano;\n return showsFlat;\n });\n }\n if (fromDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.captured_at).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.captured_at).getTime() <= new Date(toDate).getTime();\n });\n }\n\n return images;\n }\n\n function filterSequences(sequences, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n\n if (!showsPano || !showsFlat) {\n sequences = sequences.filter(function(sequence) {\n if (sequence.properties.hasOwnProperty('is_pano')) {\n if (sequence.properties.is_pano) return showsPano;\n return showsFlat;\n }\n return false;\n });\n }\n if (fromDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() >= new Date(fromDate).getTime().toString();\n });\n }\n if (toDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() <= new Date(toDate).getTime().toString();\n });\n }\n\n return sequences;\n }\n\n function update() {\n const z = ~~context.map().zoom();\n const showMarkers = (z >= minMarkerZoom);\n const showViewfields = (z >= minViewfieldZoom);\n\n const service = getService();\n let sequences = (service ? service.sequences(projection) : []);\n let images = (service && showMarkers ? service.images(projection) : []);\n // images[0]\n // {\n // \"loc\":[13.235349655151367,52.50694232952122],\n // \"captured_at\":1619457514500,\n // \"ca\":0,\n // \"id\":505488307476058,\n // \"is_pano\":false,\n // \"sequence_id\":\"zcyumxorbza3dq3twjybam\"\n // }\n dispatch.call('photoDatesChanged', this, 'mapillary', [\n ...filterImages(images, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(s => s.properties.captured_at)]);\n\n images = filterImages(images);\n sequences = filterSequences(sequences);\n\n service.filterViewer(context);\n\n let traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.id; });\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n return b.loc[1] - a.loc[1]; // sort Y\n })\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter() // viewfields may or may not be drawn...\n .insert('path', 'circle') // but if they are, draw below the circles\n .attr('class', 'viewfield')\n .classed('pano', function() { return this.parentNode.__data__.is_pano; })\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n if (this.parentNode.__data__.is_pano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n }\n\n\n function drawImages(selection) {\n const enabled = svgMapillaryImages.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadImages(projection);\n } else {\n dispatch.call('photoDatesChanged', this, 'mapillary', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'mapillary', []);\n }\n }\n\n\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgMapillaryImages.enabled;\n svgMapillaryImages.enabled = _;\n if (svgMapillaryImages.enabled) {\n showLayer();\n context.photos().on('change.mapillary_images', update);\n } else {\n hideLayer();\n context.photos().on('change.mapillary_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgMapillaryPosition(projection, context) {\n const throttledRedraw = throttle(function () { update(); }, 1000);\n const minZoom = 12;\n const minViewfieldZoom = 18;\n let layer = d3_select(null);\n let _mapillary;\n let viewerCompassAngle;\n\n\n function init() {\n if (svgMapillaryPosition.initialized) return; // run once\n svgMapillaryPosition.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('imageChanged', throttledRedraw);\n _mapillary.event.on('bearingChanged', function(e) {\n viewerCompassAngle = e.bearing;\n\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .filter(function(d) {\n return d.is_pano;\n })\n .attr('transform', transform);\n });\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n\n return _mapillary;\n }\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n function transform(d) {\n let t = svgPointTransform(projection)(d);\n if (d.is_pano && viewerCompassAngle !== null && isFinite(viewerCompassAngle)) {\n t += ' rotate(' + Math.floor(viewerCompassAngle) + ',0,0)';\n } else if (d.ca) {\n t += ' rotate(' + Math.floor(d.ca) + ',0,0)';\n }\n return t;\n }\n\n function update() {\n\n const z = ~~context.map().zoom();\n const showViewfields = (z >= minViewfieldZoom);\n\n const service = getService();\n const image = service && service.getActiveImage();\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(image ? [image] : [], function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group currentView highlighted');\n\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z');\n }\n\n\n function drawImages(selection) {\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary-position')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary-position');\n\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n } else {\n editOff();\n }\n }\n\n\n drawImages.enabled = function() {\n update();\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgMapillarySigns(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const minZoom = 12;\n let layer = d3_select(null);\n let _mapillary;\n\n\n function init() {\n if (svgMapillarySigns.initialized) return; // run once\n svgMapillarySigns.enabled = false;\n svgMapillarySigns.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('loadedSigns', throttledRedraw);\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n return _mapillary;\n }\n\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n service.loadSignResources(context);\n editOn();\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n editOff();\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.icon-sign').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, d) {\n const service = getService();\n if (!service) return;\n\n context.map().centerEase(d.loc);\n\n const selectedImageId = service.getActiveImage() && service.getActiveImage().id;\n\n service.getDetections(d.id).then(detections => {\n if (detections.length) {\n const imageId = detections[0].image.id;\n if (imageId === selectedImageId) {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId);\n } else {\n service.ensureViewerLoaded(context)\n .then(function() {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId)\n .showViewer(context);\n });\n }\n }\n });\n }\n\n\n function filterData(detectedFeatures) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n\n if (fromDate) {\n var fromTimestamp = new Date(fromDate).getTime();\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.last_seen_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate) {\n var toTimestamp = new Date(toDate).getTime();\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.first_seen_at).getTime() <= toTimestamp;\n });\n }\n\n return detectedFeatures;\n }\n\n\n function update() {\n const service = getService();\n let data = (service ? service.signs(projection) : []);\n data = filterData(data);\n\n const transform = svgPointTransform(projection);\n\n const signs = layer.selectAll('.icon-sign')\n .data(data, function(d) { return d.id; });\n\n // exit\n signs.exit()\n .remove();\n\n // enter\n const enter = signs.enter()\n .append('g')\n .attr('class', 'icon-sign icon-detected')\n .on('click', click);\n\n enter\n .append('use')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px')\n .attr('xlink:href', function(d) { return '#' + d.value; });\n\n enter\n .append('rect')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px');\n\n // update\n signs\n .merge(enter)\n .attr('transform', transform);\n }\n\n\n function drawSigns(selection) {\n const enabled = svgMapillarySigns.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary-signs')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary-signs layer-mapillary-detections')\n .style('display', enabled ? 'block' : 'none')\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadSigns(projection);\n service.showSignDetections(true);\n } else {\n editOff();\n }\n } else if (service) {\n service.showSignDetections(false);\n }\n }\n\n\n drawSigns.enabled = function(_) {\n if (!arguments.length) return svgMapillarySigns.enabled;\n svgMapillarySigns.enabled = _;\n if (svgMapillarySigns.enabled) {\n showLayer();\n context.photos().on('change.mapillary_signs', update);\n } else {\n hideLayer();\n context.photos().on('change.mapillary_signs', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawSigns.supported = function() {\n return !!getService();\n };\n\n drawSigns.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawSigns;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\nimport { t } from '../core/localizer';\n\nexport function svgMapillaryMapFeatures(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const minZoom = 12;\n let layer = d3_select(null);\n let _mapillary;\n\n\n function init() {\n if (svgMapillaryMapFeatures.initialized) return; // run once\n svgMapillaryMapFeatures.enabled = false;\n svgMapillaryMapFeatures.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('loadedMapFeatures', throttledRedraw);\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n return _mapillary;\n }\n\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n service.loadObjectResources(context);\n editOn();\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n editOff();\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.icon-map-feature').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, d) {\n const service = getService();\n if (!service) return;\n\n context.map().centerEase(d.loc);\n\n const selectedImageId = service.getActiveImage() && service.getActiveImage().id;\n\n service.getDetections(d.id).then(detections => {\n if (detections.length) {\n const imageId = detections[0].image.id;\n if (imageId === selectedImageId) {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId);\n } else {\n service.ensureViewerLoaded(context)\n .then(function() {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId)\n .showViewer(context);\n });\n }\n }\n });\n }\n\n\n function filterData(detectedFeatures) {\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n\n if (fromDate) {\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.last_seen_at).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate) {\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.first_seen_at).getTime() <= new Date(toDate).getTime();\n });\n }\n\n return detectedFeatures;\n }\n\n\n function update() {\n const service = getService();\n let data = (service ? service.mapFeatures(projection) : []);\n data = filterData(data);\n\n const transform = svgPointTransform(projection);\n\n const mapFeatures = layer.selectAll('.icon-map-feature')\n .data(data, function(d) { return d.id; });\n\n // exit\n mapFeatures.exit()\n .remove();\n\n // enter\n const enter = mapFeatures.enter()\n .append('g')\n .attr('class', 'icon-map-feature icon-detected')\n .on('click', click);\n\n enter\n .append('title')\n .text(function(d) {\n var id = d.value.replace(/--/g, '.').replace(/-/g, '_');\n return t('mapillary_map_features.' + id);\n });\n\n enter\n .append('use')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px')\n .attr('xlink:href', function(d) {\n if (d.value === 'object--billboard') {\n // no billboard icon right now, so use the advertisement icon\n return '#object--sign--advertisement';\n }\n return '#' + d.value;\n });\n\n enter\n .append('rect')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px');\n\n // update\n mapFeatures\n .merge(enter)\n .attr('transform', transform);\n }\n\n\n function drawMapFeatures(selection) {\n const enabled = svgMapillaryMapFeatures.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary-map-features')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary-map-features layer-mapillary-detections')\n .style('display', enabled ? 'block' : 'none')\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadMapFeatures(projection);\n service.showFeatureDetections(true);\n } else {\n editOff();\n }\n } else if (service) {\n service.showFeatureDetections(false);\n }\n }\n\n\n drawMapFeatures.enabled = function(_) {\n if (!arguments.length) return svgMapillaryMapFeatures.enabled;\n svgMapillaryMapFeatures.enabled = _;\n if (svgMapillaryMapFeatures.enabled) {\n showLayer();\n context.photos().on('change.mapillary_map_features', update);\n } else {\n hideLayer();\n context.photos().on('change.mapillary_map_features', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawMapFeatures.supported = function() {\n return !!getService();\n };\n\n drawMapFeatures.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawMapFeatures;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgKartaviewImages(projection, context, dispatch) {\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var minZoom = 12;\n var minMarkerZoom = 16;\n var minViewfieldZoom = 18;\n var layer = d3_select(null);\n var _kartaview;\n\n\n function init() {\n if (svgKartaviewImages.initialized) return; // run once\n svgKartaviewImages.enabled = false;\n svgKartaviewImages.initialized = true;\n }\n\n\n function getService() {\n if (services.kartaview && !_kartaview) {\n _kartaview = services.kartaview;\n _kartaview.event.on('loadedImages', throttledRedraw);\n } else if (!services.kartaview && _kartaview) {\n _kartaview = null;\n }\n\n return _kartaview;\n }\n\n\n function showLayer() {\n var service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, d) {\n var service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service.selectImage(context, d.key)\n .showViewer(context);\n });\n\n context.map().centerEase(d.loc);\n }\n\n\n function mouseover(d3_event, d) {\n var service = getService();\n if (service) service.setStyles(context, d);\n }\n\n\n function mouseout() {\n var service = getService();\n if (service) service.setStyles(context, null);\n }\n\n\n function transform(d) {\n var t = svgPointTransform(projection)(d);\n if (d.ca) {\n t += ' rotate(' + Math.floor(d.ca) + ',0,0)';\n }\n return t;\n }\n\n\n function filterImages(images, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n images = images.filter(function(item) {\n return new Date(item.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n images = images.filter(function(item) {\n return new Date(item.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n images = images.filter(function(item) {\n return usernames.indexOf(item.captured_by) !== -1;\n });\n }\n\n return images;\n }\n\n function filterSequences(sequences, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n sequences = sequences.filter(function(sequence) {\n return usernames.indexOf(sequence.properties.captured_by) !== -1;\n });\n }\n\n return sequences;\n }\n\n function update() {\n var viewer = context.container().select('.photoviewer');\n var selected = viewer.empty() ? undefined : viewer.datum();\n\n var z = ~~context.map().zoom();\n var showMarkers = (z >= minMarkerZoom);\n var showViewfields = (z >= minViewfieldZoom);\n\n const service = getService();\n\n let sequences = (service ? service.sequences(projection) : []);\n let images = (service && showMarkers ? service.images(projection) : []);\n dispatch.call('photoDatesChanged', this, 'kartaview', [\n ...filterImages(images, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(s => s.properties.captured_at)]);\n sequences = filterSequences(sequences);\n images = filterImages(images);\n\n var traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.key; });\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n var groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, function(d) { return d.key; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n var markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n return (a === selected) ? 1\n : (b === selected) ? -1\n : b.loc[1] - a.loc[1]; // sort Y\n })\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n var viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter() // viewfields may or may not be drawn...\n .insert('path', 'circle') // but if they are, draw below the circles\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z');\n }\n\n\n function drawImages(selection) {\n var enabled = svgKartaviewImages.enabled,\n service = getService();\n\n layer = selection.selectAll('.layer-kartaview')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n var layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-kartaview')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadImages(projection);\n } else {\n dispatch.call('photoDatesChanged', this, 'kartaview', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'kartaview', []);\n }\n }\n\n\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgKartaviewImages.enabled;\n svgKartaviewImages.enabled = _;\n if (svgKartaviewImages.enabled) {\n showLayer();\n context.photos().on('change.kartaview_images', update);\n } else {\n hideLayer();\n context.photos().on('change.kartaview_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { services } from '../services';\nimport {svgPath, svgPointTransform} from './helpers';\n\n\nexport function svgMapilioImages(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const imageMinZoom = 16;\n const lineMinZoom = 10;\n const viewFieldZoomLevel = 18;\n let layer = d3_select(null);\n let _mapilio;\n let _viewerYaw = 0;\n\n function init() {\n if (svgMapilioImages.initialized) return;\n svgMapilioImages.enabled = false;\n svgMapilioImages.initialized = true;\n }\n\n function getService() {\n if (services.mapilio && !_mapilio) {\n _mapilio = services.mapilio;\n _mapilio.event\n .on('loadedImages', throttledRedraw)\n .on('loadedLines', throttledRedraw);\n } else if (!services.mapilio && _mapilio) {\n _mapilio = null;\n }\n\n return _mapilio;\n }\n\n /**\n * Filters images\n * @param {*} images\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered images\n */\n function filterImages(images, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n\n if (fromDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() <= new Date(toDate).getTime();\n });\n }\n\n return images;\n }\n\n /**\n * Filters sequences\n * @param {*} sequences\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered sequences\n */\n function filterSequences(sequences, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n\n if (fromDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.capture_time).getTime() >= new Date(fromDate).getTime().toString();\n });\n }\n if (toDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.capture_time).getTime() <= new Date(toDate).getTime().toString();\n });\n }\n\n return sequences;\n }\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n function transform(d, selectedImageId) {\n let t = svgPointTransform(projection)(d);\n let rot = d.heading || 0;\n\n if (d.id === selectedImageId) {\n rot += _viewerYaw;\n }\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n function click(d3_event, image) {\n const service = getService();\n if (!service) return;\n\n service.ensureViewerLoaded(context, image.id)\n .then(() => {\n service.selectImage(context, image.id)\n .showViewer(context);\n });\n\n context.map().centerEase(image.loc);\n }\n\n function mouseover(d3_event, image) {\n const service = getService();\n if (service) service.setStyles(context, image);\n }\n\n\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n async function update() {\n const zoom = ~~context.map().zoom();\n const showViewfields = (zoom >= viewFieldZoomLevel);\n const service = getService();\n\n let sequences = (service ? service.sequences(projection, zoom) : []);\n let images = (service && zoom >= imageMinZoom ? service.images(projection) : []);\n\n dispatch.call('photoDatesChanged', this, 'mapilio', [\n ...filterImages(images, true).map(p => p.capture_time),\n ...filterSequences(sequences, true).map(s => s.properties.capture_time)\n ]);\n\n images = await filterImages(images);\n sequences = await filterSequences(sequences);\n\n const activeImage = service.getActiveImage?.();\n const activeImageId = activeImage ? activeImage.id : null;\n\n let traces = layer\n .selectAll('.sequences')\n .selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.id; });\n\n // exit\n traces.exit().remove();\n\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n const groups = layer\n .selectAll('.markers')\n .selectAll('.viewfield-group')\n .data(images, function(d) { return d.id; });\n\n // exit\n groups.exit().remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n if (a.id === activeImageId) return 1;\n if (b.id === activeImageId) return -1;\n return a.capture_time_parsed - b.capture_time_parsed;\n })\n .attr('transform', d => transform(d, activeImageId))\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit().remove();\n\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n service.setStyles(context, null);\n\n function viewfieldPath() {\n if (this.parentNode.__data__.isPano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n }\n\n function drawImages(selection) {\n const enabled = svgMapilioImages.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapilio')\n .data(service ? [0] : []);\n\n layer.exit().remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-mapilio')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter.merge(layer);\n\n if (enabled) {\n let zoom = ~~context.map().zoom();\n if (service) {\n if (zoom >= imageMinZoom) {\n editOn();\n update();\n service.loadImages(projection);\n service.loadLines(projection, zoom);\n } else if (zoom >= lineMinZoom) {\n editOn();\n update();\n service.loadImages(projection);\n service.loadLines(projection, zoom);\n } else {\n editOff();\n dispatch.call('photoDatesChanged', this, 'mapilio', []);\n service.selectImage(context, null);\n }\n } else {\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'mapilio', []);\n }\n }\n\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgMapilioImages.enabled;\n svgMapilioImages.enabled = _;\n if (svgMapilioImages.enabled) {\n showLayer();\n context.photos().on('change.mapilio_images', update);\n } else {\n hideLayer();\n context.photos().on('change.mapilio_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= lineMinZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { services } from '../services';\nimport {svgPath, svgPointTransform} from './helpers';\n\n\nexport function svgPanoramaxImages(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const imageMinZoom = 15;\n const lineMinZoom = 10;\n const viewFieldZoomLevel = 18;\n let layer = d3_select(null);\n let _panoramax;\n let _viewerYaw = 0;\n let _activeUsernameFilter;\n let _activeIds;\n\n function init() {\n if (svgPanoramaxImages.initialized) return;\n svgPanoramaxImages.enabled = false;\n svgPanoramaxImages.initialized = true;\n }\n\n function getService() {\n if (services.panoramax && !_panoramax) {\n _panoramax = services.panoramax;\n _panoramax.event\n .on('viewerChanged', viewerChanged)\n .on('loadedLines', throttledRedraw)\n .on('loadedImages', throttledRedraw);\n } else if (!services.panoramax && _panoramax) {\n _panoramax = null;\n }\n\n return _panoramax;\n }\n\n /**\n * Filters the images given the filters on the right panel\n * @param {*} images\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered images\n */\n async function filterImages(images, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n const username = context.photos().usernames();\n\n const service = getService();\n\n if (!showsPano || !showsFlat) {\n images = images.filter(function(image) {\n if (image.isPano) return showsPano;\n return showsFlat;\n });\n }\n if (fromDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() <= new Date(toDate).getTime();\n });\n }\n if (username && service) {\n if (_activeUsernameFilter !== username) {\n _activeUsernameFilter = username;\n\n const tempIds = await service.getUserIds(username);\n\n _activeIds = {};\n tempIds.forEach(id => {\n _activeIds[id] = true;\n });\n }\n\n images = images.filter(function(image) {\n return _activeIds[image.account_id];\n });\n }\n\n return images;\n }\n\n /**\n * Filters the sequences given the filters on the right panel\n * @param {*} sequences\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered sequences\n */\n async function filterSequences(sequences, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n const username = context.photos().usernames();\n\n const service = getService();\n\n if (!showsPano || !showsFlat) {\n sequences = sequences.filter(function(sequence) {\n if (sequence.properties.type === 'equirectangular') return showsPano;\n return showsFlat;\n });\n }\n if (fromDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.date).getTime() >= new Date(fromDate).getTime().toString();\n });\n }\n if (toDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.date).getTime() <= new Date(toDate).getTime().toString();\n });\n }\n if (username && service) {\n if (_activeUsernameFilter !== username) {\n _activeUsernameFilter = username;\n\n const tempIds = await service.getUserIds(username);\n\n _activeIds = {};\n tempIds.forEach(id => {\n _activeIds[id] = true;\n });\n }\n\n sequences = sequences.filter(function(sequence) {\n return _activeIds[sequence.properties.account_id];\n });\n }\n\n return sequences;\n }\n\n /**\n * Shows the selected layer\n */\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n /**\n * Hides the selected layer\n */\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n /**\n * Updates the viewfinder for the selected image bubble based on the frame's yaw\n * @param {*} d Current Active image Data\n * @param {*} selectedImageId The selected bubble image ID\n */\n function transform(d, selectedImageId) {\n let t = svgPointTransform(projection)(d);\n let rot = d.heading;\n if (d.id === selectedImageId) {\n rot += _viewerYaw;\n }\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n /**\n * Updates the current selected image\n * @param {*} image The selected image bubble data\n */\n function click(d3_event, image) {\n const service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, image.id)\n .showViewer(context);\n });\n\n context.map().centerEase(image.loc);\n }\n\n function mouseover(d3_event, image) {\n const service = getService();\n if (service) service.setStyles(context, image);\n }\n\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n /**\n * Updates the current view, rearranging lines and bubbles.\n */\n async function update() {\n const zoom = ~~context.map().zoom();\n const showViewfields = (zoom >= viewFieldZoomLevel);\n\n const service = getService();\n let sequences = (service ? service.sequences(projection, zoom) : []);\n let images = (service && zoom >= imageMinZoom ? service.images(projection) : []);\n dispatch.call('photoDatesChanged', this, 'panoramax', [\n ...(await filterImages(images, true)).map(p => p.capture_time),\n ...(await filterSequences(sequences, true)).map(s => s.properties.date)]);\n\n images = await filterImages(images);\n sequences = await filterSequences(sequences);\n\n let traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.id; });\n\n // exit\n traces.exit()\n .remove();\n\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n const activeImageId = service.getActiveImage()?.id;\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n // active image on top\n if (a.id === activeImageId) return 1;\n if (b.id === activeImageId) return -1;\n // else: sort by capture time (newest on top)\n return a.capture_time_parsed - b.capture_time_parsed;\n })\n .attr('transform', d => transform(d, activeImageId))\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n service.setStyles(context, null);\n\n function viewfieldPath() {\n if (this.parentNode.__data__.isPano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n }\n\n function viewerChanged() {\n const service = getService();\n if (!service) return;\n\n const frame = service.photoFrame();\n if (!frame) return;\n\n // update viewfield rotation\n _viewerYaw = frame.getYaw();\n\n // avoid updating if the map is currently transformed\n // e.g. during drags or easing.\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .attr('transform', d => transform(d, d.id));\n }\n\n\n /**\n * Draws bubbles and lines on the current view\n * @param {*} selection Current HTML Selection\n */\n function drawImages(selection) {\n\n const enabled = svgPanoramaxImages.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-panoramax')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-panoramax')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n let zoom = ~~context.map().zoom();\n if (service){\n if (zoom >= imageMinZoom) {\n editOn();\n update();\n service.loadImages(projection);\n } else if (zoom >= lineMinZoom) {\n editOn();\n update();\n service.loadLines(projection, zoom);\n } else {\n editOff();\n dispatch.call('photoDatesChanged', this, 'panoramax', []);\n }\n } else {\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'panoramax', []);\n }\n }\n\n /**\n * @returns if layer is active\n */\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgPanoramaxImages.enabled;\n svgPanoramaxImages.enabled = _;\n if (svgPanoramaxImages.enabled) {\n showLayer();\n context.photos().on('change.panoramax_images', update);\n } else {\n hideLayer();\n context.photos().on('change.panoramax_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n /**\n * @returns if layer is drawn\n */\n drawImages.rendered = function(zoom) {\n return zoom >= lineMinZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "export function svgOsm(projection, context, dispatch) {\n var enabled = true;\n\n\n function drawOsm(selection) {\n selection.selectAll('.layer-osm')\n .data(['covered', 'areas', 'lines', 'points', 'auxiliary', 'labels'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'layer-osm ' + d; });\n\n selection.selectAll('.layer-osm.points').selectAll('.points-group')\n .data(['vertices', 'midpoints', 'points', 'turns'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'points-group ' + d; });\n }\n\n\n function showLayer() {\n var layer = context.surface().selectAll('.data-layer.osm');\n layer.interrupt();\n\n layer\n .classed('disabled', false)\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end interrupt', function () {\n dispatch.call('change');\n });\n }\n\n\n function hideLayer() {\n var layer = context.surface().selectAll('.data-layer.osm');\n layer.interrupt();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end interrupt', function () {\n layer.classed('disabled', true);\n dispatch.call('change');\n });\n }\n\n\n drawOsm.enabled = function(val) {\n if (!arguments.length) return enabled;\n enabled = val;\n\n if (enabled) {\n showLayer();\n } else {\n hideLayer();\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n return drawOsm;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { modeBrowse } from '../modes/browse';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\nimport { utilStringQs } from '../util';\n\nvar hash = utilStringQs(window.location.hash);\n\nvar _notesEnabled = !!hash.notes;\nvar _osmService;\n\n\nexport function svgNotes(projection, context, dispatch) {\n if (!dispatch) { dispatch = d3_dispatch('change'); }\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var minZoom = 12;\n var touchLayer = d3_select(null);\n var drawLayer = d3_select(null);\n var _notesVisible = false;\n\n\n function markerPath(selection, klass) {\n selection\n .attr('class', klass)\n .attr('transform', 'translate(-8, -22)')\n .attr('d', 'm17.5,0l-15,0c-1.37,0 -2.5,1.12 -2.5,2.5l0,11.25c0,1.37 1.12,2.5 2.5,2.5l3.75,0l0,3.28c0,0.38 0.43,0.6 0.75,0.37l4.87,-3.65l5.62,0c1.37,0 2.5,-1.12 2.5,-2.5l0,-11.25c0,-1.37 -1.12,-2.5 -2.5,-2.5z');\n }\n\n\n // Loosely-coupled osm service for fetching notes.\n function getService() {\n if (services.osm && !_osmService) {\n _osmService = services.osm;\n _osmService.on('loadedNotes', throttledRedraw);\n } else if (!services.osm && _osmService) {\n _osmService = null;\n }\n\n return _osmService;\n }\n\n\n // Show the notes\n function editOn() {\n if (!_notesVisible) {\n _notesVisible = true;\n drawLayer\n .style('display', 'block');\n }\n }\n\n\n // Immediately remove the notes and their touch targets\n function editOff() {\n if (_notesVisible) {\n _notesVisible = false;\n drawLayer\n .style('display', 'none');\n drawLayer.selectAll('.note')\n .remove();\n touchLayer.selectAll('.note')\n .remove();\n }\n }\n\n\n // Enable the layer. This shows the notes and transitions them to visible.\n function layerOn() {\n editOn();\n\n drawLayer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end interrupt', function () {\n dispatch.call('change');\n });\n }\n\n\n // Disable the layer. This transitions the layer invisible and then hides the notes.\n function layerOff() {\n throttledRedraw.cancel();\n drawLayer.interrupt();\n touchLayer.selectAll('.note')\n .remove();\n\n drawLayer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end interrupt', function () {\n editOff();\n dispatch.call('change');\n });\n }\n\n\n // Update the note markers\n function updateMarkers() {\n if (!_notesVisible || !_notesEnabled) return;\n\n var service = getService();\n var selectedID = context.selectedNoteID();\n var data = (service ? service.notes(projection) : []);\n var getTransform = svgPointTransform(projection);\n\n // Draw markers..\n var notes = drawLayer.selectAll('.note')\n .data(data, function(d) { return d.status + d.id; });\n\n // exit\n notes.exit()\n .remove();\n\n // enter\n var notesEnter = notes.enter()\n .append('g')\n .attr('class', function(d) { return 'note note-' + d.id + ' ' + d.status; })\n .classed('new', function(d) { return d.id < 0; });\n\n notesEnter\n .append('ellipse')\n .attr('cx', 0.5)\n .attr('cy', 1)\n .attr('rx', 6.5)\n .attr('ry', 3)\n .attr('class', 'stroke');\n\n notesEnter\n .append('path')\n .call(markerPath, 'shadow');\n\n notesEnter\n .append('use')\n .attr('class', 'note-fill')\n .attr('width', '20px')\n .attr('height', '20px')\n .attr('x', '-8px')\n .attr('y', '-22px')\n .attr('xlink:href', '#iD-icon-note');\n\n notesEnter.selectAll('.icon-annotation')\n .data(function(d) { return [d]; })\n .enter()\n .append('use')\n .attr('class', 'icon-annotation')\n .attr('width', '10px')\n .attr('height', '10px')\n .attr('x', '-3px')\n .attr('y', '-19px')\n .attr('xlink:href', function(d) {\n if (d.id < 0) return '#iD-icon-plus';\n if (d.status === 'open') return '#iD-icon-close';\n return '#iD-icon-apply';\n });\n\n // update\n notes\n .merge(notesEnter)\n .sort(sortY)\n .classed('selected', function(d) {\n var mode = context.mode();\n var isMoving = mode && mode.id === 'drag-note'; // no shadows when dragging\n return !isMoving && d.id === selectedID;\n })\n .attr('transform', getTransform);\n\n\n // Draw targets..\n if (touchLayer.empty()) return;\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n\n var targets = touchLayer.selectAll('.note')\n .data(data, function(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('rect')\n .attr('width', '20px')\n .attr('height', '20px')\n .attr('x', '-8px')\n .attr('y', '-22px')\n .merge(targets)\n .sort(sortY)\n .attr('class', function(d) {\n var newClass = (d.id < 0 ? 'new' : '');\n return 'note target note-' + d.id + ' ' + fillClass + newClass;\n })\n .attr('transform', getTransform);\n\n\n function sortY(a, b) {\n if (a.id === selectedID) return 1;\n if (b.id === selectedID) return -1;\n return b.loc[1] - a.loc[1];\n }\n }\n\n\n // Draw the notes layer and schedule loading notes and updating markers.\n function drawNotes(selection) {\n var service = getService();\n\n var surface = context.surface();\n if (surface && !surface.empty()) {\n touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');\n }\n\n drawLayer = selection.selectAll('.layer-notes')\n .data(service ? [0] : []);\n\n drawLayer.exit()\n .remove();\n\n drawLayer = drawLayer.enter()\n .append('g')\n .attr('class', 'layer-notes')\n .style('display', _notesEnabled ? 'block' : 'none')\n .merge(drawLayer);\n\n if (_notesEnabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n service.loadNotes(projection);\n updateMarkers();\n } else {\n editOff();\n }\n }\n }\n\n\n // Toggles the layer on and off\n drawNotes.enabled = function(val) {\n if (!arguments.length) return _notesEnabled;\n\n _notesEnabled = val;\n if (_notesEnabled) {\n layerOn();\n } else {\n layerOff();\n if (context.selectedNoteID()) {\n context.enter(modeBrowse(context));\n }\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n return drawNotes;\n}\n", "export function svgTouch() {\n\n function drawTouch(selection) {\n selection.selectAll('.layer-touch')\n .data(['areas', 'lines', 'points', 'turns', 'markers'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'layer-touch ' + d; });\n }\n\n return drawTouch;\n}\n", "function refresh(selection, node) {\n var cr = node.getBoundingClientRect();\n var prop = [cr.width, cr.height];\n selection.property('__dimensions__', prop);\n return prop;\n}\n\nexport function utilGetDimensions(selection, force) {\n if (!selection || selection.empty()) {\n return [0, 0];\n }\n var node = selection.node(),\n cached = selection.property('__dimensions__');\n return (!cached || force) ? refresh(selection, node) : cached;\n}\n\n\nexport function utilSetDimensions(selection, dimensions) {\n if (!selection || selection.empty()) {\n return selection;\n }\n var node = selection.node();\n if (dimensions === null) {\n refresh(selection, node);\n return selection;\n }\n return selection\n .property('__dimensions__', [dimensions[0], dimensions[1]])\n .attr('width', dimensions[0])\n .attr('height', dimensions[1]);\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { svgData } from './data';\nimport { svgLocalPhotos} from './local_photos';\nimport { svgDebug } from './debug';\nimport { svgGeolocate } from './geolocate';\nimport { svgOsmose } from './osmose';\nimport { svgStreetside } from './streetside';\nimport { svgVegbilder} from './vegbilder';\nimport { svgMapillaryImages } from './mapillary_images';\nimport { svgMapillaryPosition } from './mapillary_position';\nimport { svgMapillarySigns } from './mapillary_signs';\nimport { svgMapillaryMapFeatures } from './mapillary_map_features';\nimport { svgKartaviewImages } from './kartaview_images';\nimport { svgMapilioImages } from './mapilio_images';\nimport { svgPanoramaxImages } from './panoramax_images';\nimport { svgOsm } from './osm';\nimport { svgNotes } from './notes';\nimport { svgTouch } from './touch';\nimport { utilArrayDifference, utilRebind } from '../util';\nimport { utilGetDimensions, utilSetDimensions } from '../util/dimensions';\n\n\nexport function svgLayers(projection, context) {\n var dispatch = d3_dispatch('change', 'photoDatesChanged');\n var svg = d3_select(null);\n var _layers = [\n { id: 'osm', layer: svgOsm(projection, context, dispatch) },\n { id: 'notes', layer: svgNotes(projection, context, dispatch) },\n { id: 'data', layer: svgData(projection, context, dispatch) },\n { id: 'osmose', layer: svgOsmose(projection, context, dispatch) },\n { id: 'streetside', layer: svgStreetside(projection, context, dispatch)},\n { id: 'mapillary', layer: svgMapillaryImages(projection, context, dispatch) },\n { id: 'mapillary-position', layer: svgMapillaryPosition(projection, context, dispatch) },\n { id: 'mapillary-map-features', layer: svgMapillaryMapFeatures(projection, context, dispatch) },\n { id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) },\n { id: 'kartaview', layer: svgKartaviewImages(projection, context, dispatch) },\n { id: 'mapilio', layer: svgMapilioImages(projection, context, dispatch) },\n { id: 'vegbilder', layer: svgVegbilder(projection, context, dispatch) },\n { id: 'panoramax', layer: svgPanoramaxImages(projection, context, dispatch) },\n { id: 'local-photos', layer: svgLocalPhotos(projection, context, dispatch) },\n { id: 'debug', layer: svgDebug(projection, context, dispatch) },\n { id: 'geolocate', layer: svgGeolocate(projection, context, dispatch) },\n { id: 'touch', layer: svgTouch(projection, context, dispatch) },\n ];\n\n\n function drawLayers(selection) {\n svg = selection.selectAll('.surface')\n .data([0]);\n\n svg = svg.enter()\n .append('svg')\n .attr('class', 'surface')\n .merge(svg);\n\n var defs = svg.selectAll('.surface-defs')\n .data([0]);\n\n defs.enter()\n .append('defs')\n .attr('class', 'surface-defs');\n\n var groups = svg.selectAll('.data-layer')\n .data(_layers);\n\n groups.exit()\n .remove();\n\n groups.enter()\n .append('g')\n .attr('class', function(d) { return 'data-layer ' + d.id; })\n .merge(groups)\n .each(function(d) { d3_select(this).call(d.layer); });\n }\n\n\n drawLayers.all = function() {\n return _layers;\n };\n\n\n drawLayers.layer = function(id) {\n var obj = _layers.find(function(o) { return o.id === id; });\n return obj && obj.layer;\n };\n\n\n drawLayers.only = function(what) {\n var arr = [].concat(what);\n var all = _layers.map(function(layer) { return layer.id; });\n return drawLayers.remove(utilArrayDifference(all, arr));\n };\n\n\n drawLayers.remove = function(what) {\n var arr = [].concat(what);\n arr.forEach(function(id) {\n _layers = _layers.filter(function(o) { return o.id !== id; });\n });\n dispatch.call('change');\n return this;\n };\n\n\n drawLayers.add = function(what) {\n var arr = [].concat(what);\n arr.forEach(function(obj) {\n if ('id' in obj && 'layer' in obj) {\n _layers.push(obj);\n }\n });\n dispatch.call('change');\n return this;\n };\n\n\n drawLayers.dimensions = function(val) {\n if (!arguments.length) return utilGetDimensions(svg);\n utilSetDimensions(svg, val);\n return this;\n };\n\n\n return utilRebind(drawLayers, dispatch, 'on');\n}\n", "import { deepEqual } from 'fast-equals';\nimport { range as d3_range } from 'd3-array';\n\nimport {\n svgMarkerSegments, svgPath, svgRelationMemberTags, svgSegmentWay\n} from './helpers';\nimport { svgTagClasses } from './tag_classes';\n\nimport { osmEntity } from '../osm';\nimport { utilArrayFlatten, utilArrayGroupBy } from '../util';\nimport { utilDetect } from '../util/detect';\n\n/** @param {{ [key: string ]: string }} tags */\nfunction onewayArrowColour(tags) {\n // the return value must be defined in ./defs.js\n if (tags.highway === 'construction' && tags.bridge) return 'white';\n if (tags.highway === 'pedestrian') return 'gray';\n if (tags.railway && !tags.highway) return 'gray';\n if (tags.aeroway === 'runway') return 'white';\n\n return 'black';\n}\n\nexport function svgLines(projection, context) {\n var detected = utilDetect();\n\n var highway_stack = {\n motorway: 0,\n motorway_link: 1,\n trunk: 2,\n trunk_link: 3,\n primary: 4,\n primary_link: 5,\n secondary: 6,\n tertiary: 7,\n unclassified: 8,\n residential: 9,\n service: 10,\n busway: 11,\n footway: 12\n };\n\n\n function drawTargets(selection, graph, entities, filter) {\n var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';\n var getPath = svgPath(projection).geojson;\n var activeID = context.activeID();\n var base = context.history().base();\n\n // The targets and nopes will be MultiLineString sub-segments of the ways\n var data = { targets: [], nopes: [] };\n\n entities.forEach(function(way) {\n var features = svgSegmentWay(way, graph, activeID);\n data.targets.push.apply(data.targets, features.passive);\n data.nopes.push.apply(data.nopes, features.active);\n });\n\n\n // Targets allow hover and vertex snapping\n var targetData = data.targets.filter(getPath);\n var targets = selection.selectAll('.line.target-allowed')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(targetData, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n var segmentWasEdited = function(d) {\n var wayID = d.properties.entity.id;\n // if the whole line was edited, don't draw segment changes\n if (!base.entities[wayID] ||\n !deepEqual(graph.entities[wayID].nodes, base.entities[wayID].nodes)) {\n return false;\n }\n return d.properties.nodes.some(function(n) {\n return !base.entities[n.id] ||\n !deepEqual(graph.entities[n.id].loc, base.entities[n.id].loc);\n });\n };\n\n // enter/update\n targets.enter()\n .append('path')\n .merge(targets)\n .attr('d', getPath)\n .attr('class', function(d) {\n return 'way line target target-allowed ' + targetClass + d.id;\n })\n .classed('segment-edited', segmentWasEdited);\n\n // NOPE\n var nopeData = data.nopes.filter(getPath);\n var nopes = selection.selectAll('.line.target-nope')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(nopeData, function key(d) { return d.id; });\n\n // exit\n nopes.exit()\n .remove();\n\n // enter/update\n nopes.enter()\n .append('path')\n .merge(nopes)\n .attr('d', getPath)\n .attr('class', function(d) {\n return 'way line target target-nope ' + nopeClass + d.id;\n })\n .classed('segment-edited', segmentWasEdited);\n }\n\n\n function drawLines(selection, graph, entities, filter) {\n var base = context.history().base();\n\n function waystack(a, b) {\n var selected = context.selectedIDs();\n var scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0;\n var scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0;\n\n if (a.tags.highway) { scoreA -= highway_stack[a.tags.highway]; }\n if (b.tags.highway) { scoreB -= highway_stack[b.tags.highway]; }\n return scoreA - scoreB;\n }\n\n\n function drawLineGroup(selection, klass, isSelected) {\n // Note: Don't add `.selected` class in draw modes\n var mode = context.mode();\n var isDrawing = mode && /^draw/.test(mode.id);\n var selectedClass = (!isDrawing && isSelected) ? 'selected ' : '';\n\n var lines = selection\n .selectAll('path')\n .filter(filter)\n .data(getPathData(isSelected), osmEntity.key);\n\n lines.exit()\n .remove();\n\n // Optimization: Call expensive TagClasses only on enter selection. This\n // works because osmEntity.key is defined to include the entity v attribute.\n lines.enter()\n .append('path')\n .attr('class', function(d) {\n\n var prefix = 'way line';\n\n // if this line isn't styled by its own tags\n if (!d.hasInterestingTags()) {\n\n var parentRelations = graph.parentRelations(d);\n var parentMultipolygons = parentRelations.filter(function(relation) {\n return relation.isMultipolygon();\n });\n\n // and if it's a member of at least one multipolygon relation\n if (parentMultipolygons.length > 0 &&\n // and only multipolygon relations\n parentRelations.length === parentMultipolygons.length) {\n // then fudge the classes to style this as an area edge\n prefix = 'relation area';\n }\n }\n\n var oldMPClass = oldMultiPolygonOuters[d.id] ? 'old-multipolygon ' : '';\n return prefix + ' ' + klass + ' ' + selectedClass + oldMPClass + d.id;\n })\n .classed('added', function(d) {\n return !base.entities[d.id];\n })\n .classed('geometry-edited', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].nodes, base.entities[d.id].nodes);\n })\n .classed('retagged', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses())\n .merge(lines)\n .sort(waystack)\n .attr('d', getPath)\n .call(svgTagClasses().tags(svgRelationMemberTags(graph)));\n\n return selection;\n }\n\n\n function getPathData(isSelected) {\n return function() {\n var layer = this.parentNode.__data__;\n var data = pathdata[layer] || [];\n return data.filter(function(d) {\n if (isSelected) {\n return context.selectedIDs().indexOf(d.id) !== -1;\n } else {\n return context.selectedIDs().indexOf(d.id) === -1;\n }\n });\n };\n }\n\n function addMarkers(layergroup, pathclass, groupclass, groupdata, marker) {\n var markergroup = layergroup\n .selectAll('g.' + groupclass)\n .data([pathclass]);\n\n markergroup = markergroup.enter()\n .append('g')\n .attr('class', groupclass)\n .merge(markergroup);\n\n var markers = markergroup\n .selectAll('path')\n .filter(filter)\n .data(\n function data() { return groupdata[this.parentNode.__data__] || []; },\n function key(d) { return [d.id, d.index]; }\n );\n\n markers.exit()\n .remove();\n\n markers = markers.enter()\n .append('path')\n .attr('class', pathclass)\n .merge(markers)\n .attr('marker-mid', marker)\n .attr('d', function(d) { return d.d; });\n\n if (detected.ie) {\n markers.each(function() { this.parentNode.insertBefore(this, this); });\n }\n }\n\n\n var getPath = svgPath(projection, graph);\n var ways = [];\n var onewaydata = {};\n var sideddata = {};\n var oldMultiPolygonOuters = {};\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n if (entity.geometry(graph) === 'line'\n // to render side-markers for coastlines (see\n // https://github.com/openstreetmap/iD/issues/9293)\n || entity.geometry(graph) === 'area' && entity.sidednessIdentifier\n && entity.sidednessIdentifier() === 'coastline') {\n ways.push(entity);\n }\n }\n\n ways = ways.filter(getPath);\n const pathdata = utilArrayGroupBy(ways, (way) => Math.trunc(way.layer()));\n\n Object.keys(pathdata).forEach(function(k) {\n var v = pathdata[k];\n var onewayArr = v.filter(function(d) { return d.isOneWay(); });\n var onewaySegments = svgMarkerSegments(\n projection, graph, 36,\n entity => entity.isOneWayBackwards(),\n entity => entity.isBiDirectional(),\n );\n onewaydata[k] = utilArrayFlatten(onewayArr.map(onewaySegments));\n\n var sidedArr = v.filter(function(d) { return d.isSided(); });\n var sidedSegments = svgMarkerSegments(\n projection, graph, 30\n );\n sideddata[k] = utilArrayFlatten(sidedArr.map(sidedSegments));\n });\n\n\n var covered = selection.selectAll('.layer-osm.covered'); // under areas\n var uncovered = selection.selectAll('.layer-osm.lines'); // over areas\n var touchLayer = selection.selectAll('.layer-touch.lines');\n\n // Draw lines..\n [covered, uncovered].forEach(function(selection) {\n var range = (selection === covered ? d3_range(-10,0) : d3_range(0,11));\n var layergroup = selection\n .selectAll('g.layergroup')\n .data(range);\n\n layergroup = layergroup.enter()\n .append('g')\n .attr('class', function(d) { return 'layergroup layer' + String(d); })\n .merge(layergroup);\n\n layergroup\n .selectAll('g.linegroup')\n .data(['shadow', 'casing', 'stroke', 'shadow-highlighted', 'casing-highlighted', 'stroke-highlighted'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'linegroup line-' + d; });\n\n layergroup.selectAll('g.line-shadow')\n .call(drawLineGroup, 'shadow', false);\n layergroup.selectAll('g.line-casing')\n .call(drawLineGroup, 'casing', false);\n layergroup.selectAll('g.line-stroke')\n .call(drawLineGroup, 'stroke', false);\n\n layergroup.selectAll('g.line-shadow-highlighted')\n .call(drawLineGroup, 'shadow', true);\n layergroup.selectAll('g.line-casing-highlighted')\n .call(drawLineGroup, 'casing', true);\n layergroup.selectAll('g.line-stroke-highlighted')\n .call(drawLineGroup, 'stroke', true);\n\n addMarkers(layergroup, 'oneway', 'onewaygroup', onewaydata, (d) => {\n const category = onewayArrowColour(graph.entity(d.id).tags);\n return `url(#ideditor-oneway-marker-${category})`;\n });\n addMarkers(layergroup, 'sided', 'sidedgroup', sideddata,\n function marker(d) {\n var category = graph.entity(d.id).sidednessIdentifier();\n return 'url(#ideditor-sided-marker-' + category + ')';\n }\n );\n });\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, ways, filter);\n }\n\n\n return drawLines;\n}\n", "import { svgPointTransform } from './helpers';\nimport { svgTagClasses } from './tag_classes';\nimport { geoAngle, geoLineIntersection, geoVecInterp, geoVecLength } from '../geo';\n\n\nexport function svgMidpoints(projection, context) {\n var targetRadius = 8;\n\n function drawTargets(selection, graph, entities, filter) {\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var getTransform = svgPointTransform(projection).geojson;\n\n var data = entities.map(function(midpoint) {\n return {\n type: 'Feature',\n id: midpoint.id,\n properties: {\n target: true,\n entity: midpoint\n },\n geometry: {\n type: 'Point',\n coordinates: midpoint.loc\n }\n };\n });\n\n var targets = selection.selectAll('.midpoint.target')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(data, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('circle')\n .attr('r', targetRadius)\n .merge(targets)\n .attr('class', function(d) { return 'node midpoint target ' + fillClass + d.id; })\n .attr('transform', getTransform);\n }\n\n\n function drawMidpoints(selection, graph, entities, filter, extent) {\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.midpoints');\n var touchLayer = selection.selectAll('.layer-touch.points');\n\n var mode = context.mode();\n if ((mode && mode.id !== 'select') || !context.map().withinEditableZoom()) {\n drawLayer.selectAll('.midpoint').remove();\n touchLayer.selectAll('.midpoint.target').remove();\n return;\n }\n\n var poly = extent.polygon();\n var midpoints = {};\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n\n if (entity.type !== 'way') continue;\n if (!filter(entity)) continue;\n if (context.selectedIDs().indexOf(entity.id) < 0) continue;\n\n var nodes = graph.childNodes(entity);\n for (var j = 0; j < nodes.length - 1; j++) {\n var a = nodes[j];\n var b = nodes[j + 1];\n var id = [a.id, b.id].sort().join('-');\n\n if (midpoints[id]) {\n midpoints[id].parents.push(entity);\n } else if (geoVecLength(projection(a.loc), projection(b.loc)) > 40) {\n var point = geoVecInterp(a.loc, b.loc, 0.5);\n var loc = null;\n\n if (extent.intersects(point)) {\n loc = point;\n } else {\n for (var k = 0; k < 4; k++) {\n point = geoLineIntersection([a.loc, b.loc], [poly[k], poly[k + 1]]);\n if (point &&\n geoVecLength(projection(a.loc), projection(point)) > 20 &&\n geoVecLength(projection(b.loc), projection(point)) > 20) {\n loc = point;\n break;\n }\n }\n }\n\n if (loc) {\n midpoints[id] = {\n type: 'midpoint',\n id: id,\n loc: loc,\n edge: [a.id, b.id],\n parents: [entity]\n };\n }\n }\n }\n }\n\n\n function midpointFilter(d) {\n if (midpoints[d.id]) return true;\n\n for (var i = 0; i < d.parents.length; i++) {\n if (filter(d.parents[i])) {\n return true;\n }\n }\n\n return false;\n }\n\n\n var groups = drawLayer.selectAll('.midpoint')\n .filter(midpointFilter)\n .data(Object.values(midpoints), function(d) { return d.id; });\n\n groups.exit()\n .remove();\n\n var enter = groups.enter()\n .insert('g', ':first-child')\n .attr('class', 'midpoint');\n\n enter\n .append('polygon')\n .attr('points', '-6,8 10,0 -6,-8')\n .attr('class', 'shadow');\n\n enter\n .append('polygon')\n .attr('points', '-3,4 5,0 -3,-4')\n .attr('class', 'fill');\n\n groups = groups\n .merge(enter)\n .attr('transform', function(d) {\n var translate = svgPointTransform(projection);\n var a = graph.entity(d.edge[0]);\n var b = graph.entity(d.edge[1]);\n var angle = geoAngle(a, b, projection) * (180 / Math.PI);\n return translate(d) + ' rotate(' + angle + ')';\n })\n .call(svgTagClasses().tags(\n function(d) { return d.parents[0].tags; }\n ));\n\n // Propagate data bindings.\n groups.select('polygon.shadow');\n groups.select('polygon.fill');\n\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, Object.values(midpoints), midpointFilter);\n }\n\n return drawMidpoints;\n}\n", "import { deepEqual } from 'fast-equals';\nimport { clamp } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3';\n\nimport { geoScaleToZoom } from '../geo';\nimport { osmEntity } from '../osm';\nimport { svgPointTransform } from './helpers';\nimport { svgTagClasses } from './tag_classes';\nimport { presetManager } from '../presets';\nimport { textWidth, isAddressPoint } from './labels';\n\nexport function svgPoints(projection, context) {\n\n function markerPath(selection, klass) {\n selection\n .attr('class', klass)\n .attr('transform', d => isAddressPoint(d.tags)\n ? `translate(-${addressShieldWidth(d, selection)/2}, -8)`\n : 'translate(-8, -23)')\n .attr('d', d => {\n if (!isAddressPoint(d.tags)) {\n return 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z';\n }\n const w = addressShieldWidth(d, selection);\n return `M ${w-3.5} 0 a 3.5 3.5 0 0 0 3.5 3.5 l 0 9 a 3.5 3.5 0 0 0 -3.5 3.5 l -${w-7} 0 a 3.5 3.5 0 0 0 -3.5 -3.5 l 0 -9 a 3.5 3.5 0 0 0 3.5 -3.5 z`;\n });\n }\n\n function sortY(a, b) {\n return b.loc[1] - a.loc[1];\n }\n\n function addressShieldWidth(d, selection) {\n const width = textWidth(d.tags['addr:housenumber'] || d.tags['addr:housename'] || '', 10, selection.node().parentElement);\n return clamp(width, 10, 34) + 8;\n };\n\n // Avoid exit/enter if we're just moving stuff around.\n // The node will get a new version but we only need to run the update selection.\n function fastEntityKey(d) {\n const mode = context.mode();\n const isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);\n return isMoving ? d.id : osmEntity.key(d);\n }\n\n\n function drawTargets(selection, graph, entities, filter) {\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var getTransform = svgPointTransform(projection).geojson;\n var activeID = context.activeID();\n var data = [];\n\n entities.forEach(function(node) {\n if (activeID === node.id) return; // draw no target on the activeID\n\n data.push({\n type: 'Feature',\n id: node.id,\n properties: {\n target: true,\n entity: node,\n isAddr: isAddressPoint(node.tags)\n },\n geometry: node.asGeoJSON()\n });\n });\n\n var targets = selection.selectAll('.point.target')\n .filter(d => filter(d.properties.entity))\n .data(data, d => fastEntityKey(d.properties.entity));\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('rect')\n .attr('x', d => d.properties.isAddr ? -addressShieldWidth(d.properties.entity, selection) / 2 : -10)\n .attr('y', d => d.properties.isAddr ? -8 : -26)\n .attr('width', d => d.properties.isAddr ? addressShieldWidth(d.properties.entity, selection) : 20)\n .attr('height', d => d.properties.isAddr ? 16 : 30)\n .attr('class', function(d) { return 'node point target ' + fillClass + d.id; })\n .merge(targets)\n .attr('transform', getTransform);\n }\n\n\n function drawPoints(selection, graph, entities, filter) {\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n var base = context.history().base();\n\n // Points with a direction will render as vertices at higher zooms..\n function renderAsPoint(entity) {\n return entity.geometry(graph) === 'point' &&\n !(zoom >= 18 && entity.directions(graph, projection).length);\n }\n\n // All points will render as vertices in wireframe mode too..\n var points = wireframe ? [] : entities.filter(renderAsPoint);\n points.sort(sortY);\n\n\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.points');\n var touchLayer = selection.selectAll('.layer-touch.points');\n\n // Draw points..\n var groups = drawLayer.selectAll('g.point')\n .filter(filter)\n .data(points, fastEntityKey);\n\n groups.exit()\n .remove();\n\n var enter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'node point ' + d.id; })\n .order();\n\n enter\n .append('path')\n .call(markerPath, 'shadow');\n\n enter.each(function(d) {\n if (isAddressPoint(d.tags)) return;\n d3_select(this)\n .append('ellipse')\n .attr('cx', 0.5)\n .attr('cy', 1)\n .attr('rx', 6.5)\n .attr('ry', 3)\n .attr('class', 'stroke');\n });\n\n enter\n .append('path')\n .call(markerPath, 'stroke');\n\n enter\n .append('use')\n .attr('transform', 'translate(-5.5, -20)')\n .attr('class', 'icon')\n .attr('width', '12px')\n .attr('height', '12px');\n\n groups = groups\n .merge(enter)\n .attr('transform', svgPointTransform(projection))\n .classed('added', function(d) {\n return !base.entities[d.id]; // if it doesn't exist in the base graph, it's new\n })\n .classed('moved', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].loc, base.entities[d.id].loc);\n })\n .classed('retagged', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses());\n\n groups.select('.shadow'); // propagate bound data\n groups.select('.stroke'); // propagate bound data\n groups.select('.icon') // propagate bound data\n .attr('xlink:href', function(entity) {\n var preset = presetManager.match(entity, graph);\n var picon = preset && preset.icon;\n return picon ? '#' + picon : '';\n });\n\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, points, filter);\n }\n\n\n return drawPoints;\n}\n", "import { geoAngle, geoPathLength } from '../geo';\n\n\nexport function svgTurns(projection, context) {\n\n function icon(turn) {\n var u = turn.u ? '-u' : '';\n if (turn.no) return '#iD-turn-no' + u;\n if (turn.only) return '#iD-turn-only' + u;\n return '#iD-turn-yes' + u;\n }\n\n function drawTurns(selection, graph, turns) {\n\n function turnTransform(d) {\n var pxRadius = 50;\n var toWay = graph.entity(d.to.way);\n var toPoints = graph.childNodes(toWay)\n .map(function (n) { return n.loc; })\n .map(projection);\n var toLength = geoPathLength(toPoints);\n var mid = toLength / 2; // midpoint of destination way\n\n var toNode = graph.entity(d.to.node);\n var toVertex = graph.entity(d.to.vertex);\n var a = geoAngle(toVertex, toNode, projection);\n var o = projection(toVertex.loc);\n var r = d.u ? 0 // u-turn: no radius\n : !toWay.__via ? pxRadius // leaf way: put marker at pxRadius\n : Math.min(mid, pxRadius); // via way: prefer pxRadius, fallback to mid for very short ways\n\n return 'translate(' + (r * Math.cos(a) + o[0]) + ',' + (r * Math.sin(a) + o[1]) + ') ' +\n 'rotate(' + a * 180 / Math.PI + ')';\n }\n\n\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.turns');\n var touchLayer = selection.selectAll('.layer-touch.turns');\n\n // Draw turns..\n var groups = drawLayer.selectAll('g.turn')\n .data(turns, function(d) { return d.key; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var groupsEnter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'turn ' + d.key; });\n\n var turnsEnter = groupsEnter\n .filter(function(d) { return !d.u; });\n\n turnsEnter.append('rect')\n .attr('transform', 'translate(-22, -12)')\n .attr('width', '44')\n .attr('height', '24');\n\n turnsEnter.append('use')\n .attr('transform', 'translate(-22, -12)')\n .attr('width', '44')\n .attr('height', '24');\n\n var uEnter = groupsEnter\n .filter(function(d) { return d.u; });\n\n uEnter.append('circle')\n .attr('r', '16');\n\n uEnter.append('use')\n .attr('transform', 'translate(-16, -16)')\n .attr('width', '32')\n .attr('height', '32');\n\n // update\n groups = groups\n .merge(groupsEnter)\n .attr('opacity', function(d) { return d.direct === false ? '0.7' : null; })\n .attr('transform', turnTransform);\n\n groups.select('use')\n .attr('xlink:href', icon);\n\n groups.select('rect'); // propagate bound data\n groups.select('circle'); // propagate bound data\n\n\n // Draw touch targets..\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n groups = touchLayer.selectAll('g.turn')\n .data(turns, function(d) { return d.key; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n groupsEnter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'turn ' + d.key; });\n\n turnsEnter = groupsEnter\n .filter(function(d) { return !d.u; });\n\n turnsEnter.append('rect')\n .attr('class', 'target ' + fillClass)\n .attr('transform', 'translate(-22, -12)')\n .attr('width', '44')\n .attr('height', '24');\n\n uEnter = groupsEnter\n .filter(function(d) { return d.u; });\n\n uEnter.append('circle')\n .attr('class', 'target ' + fillClass)\n .attr('r', '16');\n\n // update\n groups = groups\n .merge(groupsEnter)\n .attr('transform', turnTransform);\n\n groups.select('rect'); // propagate bound data\n groups.select('circle'); // propagate bound data\n\n\n return this;\n }\n\n return drawTurns;\n}\n", "import { deepEqual } from 'fast-equals';\nimport { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { geoScaleToZoom } from '../geo';\nimport { osmEntity } from '../osm';\nimport { svgPassiveVertex, svgPointTransform } from './helpers';\nimport { svgTagClasses } from './tag_classes';\n\nexport function svgVertices(projection, context) {\n var radiuses = {\n // z16-, z17, z18+, w/icon\n shadow: [6, 7.5, 7.5, 12],\n stroke: [2.5, 3.5, 3.5, 8],\n fill: [1, 1.5, 1.5, 1.5]\n };\n\n var _currHoverTarget;\n var _currPersistent = {};\n var _currHover = {};\n var _prevHover = {};\n var _currSelected = {};\n var _prevSelected = {};\n var _radii = {};\n\n\n function sortY(a, b) {\n return b.loc[1] - a.loc[1];\n }\n\n // Avoid exit/enter if we're just moving stuff around.\n // The node will get a new version but we only need to run the update selection.\n function fastEntityKey(d) {\n var mode = context.mode();\n var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);\n return isMoving ? d.id : osmEntity.key(d);\n }\n\n\n function draw(selection, graph, vertices, sets, filter) {\n sets = sets || { selected: {}, important: {}, hovered: {} };\n\n var icons = {};\n var directions = {};\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2);\n var activeID = context.activeID();\n var base = context.history().base();\n\n\n function getIcon(d) {\n // always check latest entity, as fastEntityKey avoids enter/exit now\n var entity = graph.entity(d.id);\n if (entity.id in icons) return icons[entity.id];\n\n icons[entity.id] =\n entity.hasInterestingTags() &&\n presetManager.match(entity, graph).icon;\n\n return icons[entity.id];\n }\n\n\n // memoize directions results, return false for empty arrays (for use in filter)\n function getDirections(entity) {\n if (entity.id in directions) return directions[entity.id];\n\n var angles = entity.directions(graph, projection);\n directions[entity.id] = angles.length ? angles : false;\n return angles;\n }\n\n\n function updateAttributes(selection) {\n ['shadow', 'stroke', 'fill'].forEach(function(klass) {\n var rads = radiuses[klass];\n selection.selectAll('.' + klass)\n .each(function(entity) {\n var i = z && getIcon(entity);\n var r = rads[i ? 3 : z];\n\n // slightly increase the size of unconnected endpoints #3775\n if (entity.id !== activeID && entity.isEndpoint(graph) && !entity.isConnected(graph)) {\n r += 1.5;\n }\n\n if (klass === 'shadow') { // remember this value, so we don't need to\n _radii[entity.id] = r; // recompute it when we draw the touch targets\n }\n\n d3_select(this)\n .attr('r', r)\n .attr('visibility', (i && klass === 'fill') ? 'hidden' : null);\n });\n });\n }\n\n vertices.sort(sortY);\n\n var groups = selection.selectAll('g.vertex')\n .filter(filter)\n .data(vertices, fastEntityKey);\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var enter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'node vertex ' + d.id; })\n .order();\n\n enter\n .append('circle')\n .attr('class', 'shadow');\n\n enter\n .append('circle')\n .attr('class', 'stroke');\n\n // Vertices with tags get a fill.\n enter.filter(function(d) { return d.hasInterestingTags(); })\n .append('circle')\n .attr('class', 'fill');\n\n // update\n groups = groups\n .merge(enter)\n .attr('transform', svgPointTransform(projection))\n .classed('sibling', function(d) { return d.id in sets.selected; })\n .classed('shared', function(d) { return graph.isShared(d); })\n .classed('endpoint', function(d) { return d.isEndpoint(graph); })\n .classed('added', function(d) {\n return !base.entities[d.id]; // if it doesn't exist in the base graph, it's new\n })\n .classed('moved', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].loc, base.entities[d.id].loc);\n })\n .classed('retagged', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses())\n .call(updateAttributes);\n\n // Vertices with icons get a `use`.\n var iconUse = groups\n .selectAll('.icon')\n .data(function data(d) { return zoom >= 17 && getIcon(d) ? [d] : []; }, fastEntityKey);\n\n // exit\n iconUse.exit()\n .remove();\n\n // enter\n iconUse.enter()\n .append('use')\n .attr('class', 'icon')\n .attr('width', '12px')\n .attr('height', '12px')\n .attr('transform', 'translate(-6, -6)')\n .attr('xlink:href', function(d) {\n var picon = getIcon(d);\n return picon ? '#' + picon : '';\n });\n\n\n // Vertices with directions get viewfields\n var dgroups = groups\n .selectAll('.viewfieldgroup')\n .data(function data(d) { return zoom >= 18 && getDirections(d) ? [d] : []; }, fastEntityKey);\n\n // exit\n dgroups.exit()\n .remove();\n\n // enter/update\n dgroups = dgroups.enter()\n .insert('g', '.shadow')\n .attr('class', 'viewfieldgroup')\n .merge(dgroups);\n\n var viewfields = dgroups.selectAll('.viewfield')\n .data(getDirections, function key(d) { return osmEntity.key(d); });\n\n // exit\n viewfields.exit()\n .remove();\n\n // enter/update\n viewfields.enter()\n .append('path')\n .attr('class', 'viewfield')\n .attr('d', 'M0,0H0')\n .merge(viewfields)\n .attr('marker-start', d => 'url(#ideditor-viewfield-marker' + (d.type === 'side' ? '-side' : '') + (wireframe ? '-wireframe' : '') + ')')\n .attr('transform', d => `rotate(${d.angle})`);\n }\n\n\n function drawTargets(selection, graph, entities, filter) {\n var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';\n var getTransform = svgPointTransform(projection).geojson;\n var activeID = context.activeID();\n var data = { targets: [], nopes: [] };\n\n entities.forEach(function(node) {\n if (activeID === node.id) return; // draw no target on the activeID\n\n var vertexType = svgPassiveVertex(node, graph, activeID);\n if (vertexType !== 0) { // passive or adjacent - allow to connect\n data.targets.push({\n type: 'Feature',\n id: node.id,\n properties: {\n target: true,\n entity: node\n },\n geometry: node.asGeoJSON()\n });\n } else {\n data.nopes.push({\n type: 'Feature',\n id: node.id + '-nope',\n properties: {\n nope: true,\n target: true,\n entity: node\n },\n geometry: node.asGeoJSON()\n });\n }\n });\n\n // Targets allow hover and vertex snapping\n var targets = selection.selectAll('.vertex.target-allowed')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(data.targets, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('circle')\n .attr('r', function(d) {\n return _radii[d.id]\n || radiuses.shadow[3];\n })\n .merge(targets)\n .attr('class', function(d) {\n return 'node vertex target target-allowed '\n + targetClass + d.id;\n })\n .attr('transform', getTransform);\n\n\n // NOPE\n var nopes = selection.selectAll('.vertex.target-nope')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(data.nopes, function key(d) { return d.id; });\n\n // exit\n nopes.exit()\n .remove();\n\n // enter/update\n nopes.enter()\n .append('circle')\n .attr('r', function(d) { return (_radii[d.properties.entity.id] || radiuses.shadow[3]); })\n .merge(nopes)\n .attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; })\n .attr('transform', getTransform);\n }\n\n\n // Points can also render as vertices:\n // 1. in wireframe mode or\n // 2. at higher zooms if they have a direction\n function renderAsVertex(entity, graph, wireframe, zoom) {\n var geometry = entity.geometry(graph);\n return geometry === 'vertex' || (geometry === 'point' && (\n wireframe || (zoom >= 18 && entity.directions(graph, projection).length)\n ));\n }\n\n\n function isEditedNode(node, base, head) {\n var baseNode = base.entities[node.id];\n var headNode = head.entities[node.id];\n return !headNode ||\n !baseNode ||\n !deepEqual(headNode.tags, baseNode.tags) ||\n !deepEqual(headNode.loc, baseNode.loc);\n }\n\n\n function getSiblingAndChildVertices(ids, graph, wireframe, zoom) {\n var results = {};\n\n var seenIds = {};\n\n function addChildVertices(entity) {\n\n // avoid redundant work and infinite recursion of circular relations\n if (seenIds[entity.id]) return;\n seenIds[entity.id] = true;\n\n var geometry = entity.geometry(graph);\n if (!context.features().isHiddenFeature(entity, graph, geometry)) {\n var i;\n if (entity.type === 'way') {\n for (i = 0; i < entity.nodes.length; i++) {\n var child = graph.hasEntity(entity.nodes[i]);\n if (child) {\n addChildVertices(child);\n }\n }\n } else if (entity.type === 'relation') {\n for (i = 0; i < entity.members.length; i++) {\n var member = graph.hasEntity(entity.members[i].id);\n if (member) {\n addChildVertices(member);\n }\n }\n } else if (renderAsVertex(entity, graph, wireframe, zoom)) {\n results[entity.id] = entity;\n }\n }\n }\n\n ids.forEach(function(id) {\n var entity = graph.hasEntity(id);\n if (!entity) return;\n\n if (entity.type === 'node') {\n if (renderAsVertex(entity, graph, wireframe, zoom)) {\n results[entity.id] = entity;\n graph.parentWays(entity).forEach(function(entity) {\n addChildVertices(entity);\n });\n }\n } else { // way, relation\n addChildVertices(entity);\n }\n });\n\n return results;\n }\n\n\n function drawVertices(selection, graph, entities, filter, extent, fullRedraw) {\n var wireframe = context.surface().classed('fill-wireframe');\n var visualDiff = context.surface().classed('highlight-edited');\n var zoom = geoScaleToZoom(projection.scale());\n var mode = context.mode();\n var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);\n var base = context.history().base();\n\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.vertices');\n var touchLayer = selection.selectAll('.layer-touch.points');\n\n if (fullRedraw) {\n _currPersistent = {};\n _radii = {};\n }\n\n // Collect important vertices from the `entities` list..\n // (during a partial redraw, it will not contain everything)\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n var geometry = entity.geometry(graph);\n var keep = false;\n\n // a point that looks like a vertex..\n if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) {\n _currPersistent[entity.id] = entity;\n keep = true;\n\n // a vertex of some importance..\n } else if (geometry === 'vertex' &&\n (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)\n || (visualDiff && isEditedNode(entity, base, graph)))) {\n _currPersistent[entity.id] = entity;\n keep = true;\n }\n\n // whatever this is, it's not a persistent vertex..\n if (!keep && !fullRedraw) {\n delete _currPersistent[entity.id];\n }\n }\n\n // 3 sets of vertices to consider:\n var sets = {\n persistent: _currPersistent, // persistent = important vertices (render always)\n selected: _currSelected, // selected + siblings of selected (render always)\n hovered: _currHover // hovered + siblings of hovered (render only in draw modes)\n };\n\n var all = Object.assign({}, (isMoving ? _currHover : {}), _currSelected, _currPersistent);\n\n // Draw the vertices..\n // The filter function controls the scope of what objects d3 will touch (exit/enter/update)\n // Adjust the filter function to expand the scope beyond whatever entities were passed in.\n var filterRendered = function(d) {\n return d.id in _currPersistent || d.id in _currSelected || d.id in _currHover || filter(d);\n };\n drawLayer\n .call(draw, graph, currentVisible(all), sets, filterRendered);\n\n // Draw touch targets..\n // When drawing, render all targets (not just those affected by a partial redraw)\n var filterTouch = function(d) {\n return isMoving ? true : filterRendered(d);\n };\n touchLayer\n .call(drawTargets, graph, currentVisible(all), filterTouch);\n\n\n function currentVisible(which) {\n return Object.keys(which)\n .map(graph.hasEntity, graph) // the current version of this entity\n .filter(function (entity) { return entity && entity.intersects(extent, graph); });\n }\n }\n\n\n // partial redraw - only update the selected items..\n drawVertices.drawSelected = function(selection, graph, extent) {\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n\n _prevSelected = _currSelected || {};\n if (context.map().isInWideSelection()) {\n _currSelected = {};\n context.selectedIDs().forEach(function(id) {\n var entity = graph.hasEntity(id);\n if (!entity) return;\n\n if (entity.type === 'node') {\n if (renderAsVertex(entity, graph, wireframe, zoom)) {\n _currSelected[entity.id] = entity;\n }\n }\n });\n\n } else {\n _currSelected = getSiblingAndChildVertices(context.selectedIDs(), graph, wireframe, zoom);\n }\n\n // note that drawVertices will add `_currSelected` automatically if needed..\n var filter = function(d) { return d.id in _prevSelected; };\n drawVertices(selection, graph, Object.values(_prevSelected), filter, extent, false);\n };\n\n\n // partial redraw - only update the hovered items..\n drawVertices.drawHover = function(selection, graph, target, extent) {\n if (target === _currHoverTarget) return; // continue only if something changed\n\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n\n _prevHover = _currHover || {};\n _currHoverTarget = target;\n var entity = target && target.properties && target.properties.entity;\n\n if (entity) {\n _currHover = getSiblingAndChildVertices([entity.id], graph, wireframe, zoom);\n } else {\n _currHover = {};\n }\n\n // note that drawVertices will add `_currHover` automatically if needed..\n var filter = function(d) { return d.id in _prevHover; };\n drawVertices(selection, graph, Object.values(_prevHover), filter, extent, false);\n };\n\n return drawVertices;\n}\n", "export { svgAreas } from './areas.js';\nexport { svgData } from './data.js';\nexport { svgDebug } from './debug.js';\nexport { svgDefs } from './defs.js';\nexport { svgIcon } from './icon.js';\nexport { svgGeolocate } from './geolocate';\nexport { svgLabels } from './labels.js';\nexport { svgLayers } from './layers.js';\nexport { svgLines } from './lines.js';\nexport { svgMapillaryImages } from './mapillary_images.js';\nexport { svgMapillarySigns } from './mapillary_signs.js';\nexport { svgMidpoints } from './midpoints.js';\nexport { svgNotes } from './notes.js';\nexport { svgMarkerSegments } from './helpers.js';\nexport { svgKartaviewImages } from './kartaview_images.js';\nexport { svgOsm } from './osm.js';\nexport { svgPassiveVertex } from './helpers.js';\nexport { svgPath } from './helpers.js';\nexport { svgPointTransform } from './helpers.js';\nexport { svgPoints } from './points.js';\nexport { svgRelationMemberTags } from './helpers.js';\nexport { svgSegmentWay } from './helpers.js';\nexport { svgStreetside } from './streetside.js';\nexport { svgVegbilder } from './vegbilder';\nexport { svgTagClasses } from './tag_classes.js';\nexport { svgTagPattern } from './tag_pattern.js';\nexport { svgTouch } from './touch.js';\nexport { svgTurns } from './turns.js';\nexport { svgVertices } from './vertices.js';\nexport { svgMapilioImages } from './mapilio_images.js';\nexport { svgPanoramaxImages } from './panoramax_images.js';\n", "import { t } from '../core/localizer';\nimport { actionOrthogonalize } from '../actions/orthogonalize';\nimport { behaviorOperation } from '../behavior/operation';\nimport { utilGetAllNodes } from '../util';\nimport { svgPath } from '../svg';\n\n\nexport function operationOrthogonalize(context, selectedIDs) {\n var _extent;\n var _type;\n var _actions = selectedIDs.map(chooseAction).filter(Boolean);\n var _amount = _actions.length === 1 ? 'single' : 'multiple';\n var _coords = utilGetAllNodes(selectedIDs, context.graph())\n .map(function(n) { return n.loc; });\n\n\n function chooseAction(entityID) {\n\n var entity = context.entity(entityID);\n var geometry = entity.geometry(context.graph());\n\n if (!_extent) {\n _extent = entity.extent(context.graph());\n } else {\n _extent = _extent.extend(entity.extent(context.graph()));\n }\n\n // square a line/area\n if (entity.type === 'way' && new Set(entity.nodes).size > 2 ) {\n if (_type && _type !== 'feature') return null;\n _type = 'feature';\n return actionOrthogonalize(entityID, context.projection);\n\n // square a single vertex\n } else if (geometry === 'vertex') {\n if (_type && _type !== 'corner') return null;\n _type = 'corner';\n var graph = context.graph();\n var parents = graph.parentWays(entity);\n if (parents.length === 1) {\n var way = parents[0];\n if (way.nodes.indexOf(entityID) !== -1) {\n return actionOrthogonalize(way.id, context.projection, entityID);\n }\n }\n }\n\n return null;\n }\n\n\n var operation = function() {\n if (!_actions.length) return;\n\n var combinedAction = function(graph, t) {\n _actions.forEach(function(action) {\n if (!action.disabled(graph)) {\n graph = action(graph, t);\n }\n });\n return graph;\n };\n combinedAction.transitionable = true;\n\n context.perform(combinedAction, operation.annotation());\n\n window.setTimeout(function() {\n context.validator().validate();\n }, 300); // after any transition\n };\n\n\n operation.available = function() {\n return _actions.length && selectedIDs.length === _actions.length;\n };\n\n\n // don't cache this because the visible extent could change\n operation.disabled = function() {\n if (!_actions.length) return '';\n\n var actionDisableds = _actions.map(function(action) {\n return action.disabled(context.graph());\n }).filter(Boolean);\n\n if (actionDisableds.length === _actions.length) {\n // none of the features can be squared\n\n if (new Set(actionDisableds).size > 1) {\n return 'multiple_blockers';\n }\n return actionDisableds[0];\n } else if (_extent &&\n _extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = _coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n };\n\n\n operation.getAuxiliaryGeometry = function() {\n const graph = context.graph();\n return _actions.map((action, idx) => {\n if (!action.disabled(graph)) {\n const previewGraph = action(graph, t);\n const way = previewGraph.hasEntity(selectedIDs[idx]);\n const getPath = svgPath(context.projection, previewGraph, false);\n return {\n id: way.id,\n path: getPath(way),\n klass: 'preview'\n };\n } else {\n return false;\n }\n }).filter(Boolean);\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.orthogonalize.' + disable + '.' + _amount) :\n t.append('operations.orthogonalize.description.' + _type + '.' + _amount);\n };\n\n\n operation.annotation = function() {\n return t('operations.orthogonalize.annotation.' + _type, { n: _actions.length });\n };\n\n\n operation.id = 'orthogonalize';\n operation.keys = [t('operations.orthogonalize.key')];\n operation.title = t.append('operations.orthogonalize.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import { t } from '../core/localizer';\nimport { actionReflect } from '../actions/reflect';\nimport { behaviorOperation } from '../behavior/operation';\nimport { utilGetAllNodes, utilTotalExtent } from '../util/util';\nimport { svgPath } from '../svg';\n\n\nexport function operationReflectShort(context, selectedIDs) {\n return operationReflect(context, selectedIDs, 'short');\n}\n\n\nexport function operationReflectLong(context, selectedIDs) {\n return operationReflect(context, selectedIDs, 'long');\n}\n\n\nexport function operationReflect(context, selectedIDs, axis) {\n axis = axis || 'long';\n var multi = (selectedIDs.length === 1 ? 'single' : 'multiple');\n var nodes = utilGetAllNodes(selectedIDs, context.graph());\n var coords = nodes.map(function(n) { return n.loc; });\n var extent = utilTotalExtent(selectedIDs, context.graph());\n\n\n var _action = actionReflect(selectedIDs, context.projection)\n .useLongAxis(Boolean(axis === 'long'));\n\n var operation = function() {\n context.perform(_action, operation.annotation());\n\n window.setTimeout(function() {\n context.validator().validate();\n }, 300); // after any transition\n };\n\n\n operation.available = function() {\n return nodes.length >= 3;\n };\n\n\n // don't cache this because the visible extent could change\n operation.disabled = function() {\n if (extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n } else if (selectedIDs.some(incompleteRelation)) {\n return 'incomplete_relation';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n\n function incompleteRelation(id) {\n var entity = context.entity(id);\n return entity.type === 'relation' && !entity.isComplete(context.graph());\n }\n };\n\n\n operation.getAuxiliaryGeometry = function() {\n const graph = context.graph();\n const [p, q] = _action.getReflectAxis(graph);\n const previewGraph = _action(graph);\n const getPath = svgPath(context.projection, previewGraph, false);\n return [{\n id: 'axis',\n path: `M ${p[0]} ${p[1]} L ${q[0]} ${q[1]}`,\n klass: 'reflect-axis'\n }, ...selectedIDs.map(entityId => {\n const entity = previewGraph.hasEntity(entityId);\n return {\n id: entity.id,\n path: getPath(entity),\n klass: 'preview'\n };\n })];\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.reflect.' + disable + '.' + multi) :\n t.append('operations.reflect.description.' + axis + '.' + multi);\n };\n\n\n operation.annotation = function() {\n return t('operations.reflect.annotation.' + axis + '.feature', { n: selectedIDs.length });\n };\n\n\n operation.id = 'reflect-' + axis;\n operation.keys = [t('operations.reflect.key.' + axis)];\n operation.title = t.append('operations.reflect.title.' + axis);\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport {\n polygonHull as d3_polygonHull,\n polygonCentroid as d3_polygonCentroid\n} from 'd3-polygon';\n\nimport { t } from '../core/localizer';\nimport { actionRotate } from '../actions/rotate';\nimport { actionNoop } from '../actions/noop';\nimport { behaviorEdit } from '../behavior/edit';\nimport { geoVecInterp, geoVecLength } from '../geo/vector';\nimport { modeBrowse } from './browse';\nimport { modeSelect } from './select';\n\nimport { operationCircularize } from '../operations/circularize';\nimport { operationDelete } from '../operations/delete';\nimport { operationMove } from '../operations/move';\nimport { operationOrthogonalize } from '../operations/orthogonalize';\nimport { operationReflectLong, operationReflectShort } from '../operations/reflect';\n\nimport { utilKeybinding } from '../util/keybinding';\nimport { utilFastMouse, utilGetAllNodes } from '../util/util';\n\n\nexport function modeRotate(context, entityIDs) {\n\n var _tolerancePx = 4; // see also behaviorDrag, behaviorSelect, modeMove\n\n var mode = {\n id: 'rotate',\n button: 'browse'\n };\n\n var keybinding = utilKeybinding('rotate');\n var behaviors = [\n behaviorEdit(context),\n operationCircularize(context, entityIDs).behavior,\n operationDelete(context, entityIDs).behavior,\n operationMove(context, entityIDs).behavior,\n operationOrthogonalize(context, entityIDs).behavior,\n operationReflectLong(context, entityIDs).behavior,\n operationReflectShort(context, entityIDs).behavior\n ];\n var annotation = entityIDs.length === 1 ?\n t('operations.rotate.annotation.' + context.graph().geometry(entityIDs[0])) :\n t('operations.rotate.annotation.feature', { n: entityIDs.length });\n\n var _prevGraph;\n var _prevAngle;\n var _prevTransform;\n var _pivot;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function doRotate(d3_event) {\n var fn;\n if (context.graph() !== _prevGraph) {\n fn = context.perform;\n } else {\n fn = context.replace;\n }\n\n // projection changed, recalculate _pivot\n var projection = context.projection;\n var currTransform = projection.transform();\n if (!_prevTransform ||\n currTransform.k !== _prevTransform.k ||\n currTransform.x !== _prevTransform.x ||\n currTransform.y !== _prevTransform.y) {\n\n var nodes = utilGetAllNodes(entityIDs, context.graph());\n var points = nodes.map(function(n) { return projection(n.loc); });\n _pivot = getPivot(points);\n _prevAngle = undefined;\n }\n\n\n var currMouse = context.map().mouse(d3_event);\n var currAngle = Math.atan2(currMouse[1] - _pivot[1], currMouse[0] - _pivot[0]);\n\n if (typeof _prevAngle === 'undefined') _prevAngle = currAngle;\n var delta = currAngle - _prevAngle;\n\n fn(actionRotate(entityIDs, _pivot, delta, projection));\n\n _prevTransform = currTransform;\n _prevAngle = currAngle;\n _prevGraph = context.graph();\n }\n\n function getPivot(points) {\n var _pivot;\n if (points.length === 1) {\n _pivot = points[0];\n } else if (points.length === 2) {\n _pivot = geoVecInterp(points[0], points[1], 0.5);\n } else {\n var polygonHull = d3_polygonHull(points);\n if (polygonHull.length === 2) {\n _pivot = geoVecInterp(points[0], points[1], 0.5);\n } else {\n _pivot = d3_polygonCentroid(d3_polygonHull(points));\n }\n }\n return _pivot;\n }\n\n\n function finish(d3_event) {\n d3_event.stopPropagation();\n context.replace(actionNoop(), annotation);\n context.enter(modeSelect(context, entityIDs));\n }\n\n\n function cancel() {\n if (_prevGraph) context.pop(); // remove the rotate\n context.enter(modeSelect(context, entityIDs));\n }\n\n\n function undone() {\n context.enter(modeBrowse(context));\n }\n\n\n mode.enter = function() {\n _prevGraph = null;\n context.features().forceVisible(entityIDs);\n\n behaviors.forEach(context.install);\n\n var downEvent;\n\n context.surface()\n .on(_pointerPrefix + 'down.modeRotate', function(d3_event) {\n downEvent = d3_event;\n });\n\n d3_select(window)\n .on(_pointerPrefix + 'move.modeRotate', doRotate, true)\n .on(_pointerPrefix + 'up.modeRotate', function(d3_event) {\n if (!downEvent) return;\n var mapNode = context.container().select('.main-map').node();\n var pointGetter = utilFastMouse(mapNode);\n var p1 = pointGetter(downEvent);\n var p2 = pointGetter(d3_event);\n var dist = geoVecLength(p1, p2);\n\n if (dist <= _tolerancePx) finish(d3_event);\n downEvent = null;\n }, true);\n\n context.history()\n .on('undone.modeRotate', undone);\n\n keybinding\n .on('\u238B', cancel)\n .on('\u21A9', finish);\n\n d3_select(document)\n .call(keybinding);\n };\n\n\n mode.exit = function() {\n behaviors.forEach(context.uninstall);\n\n context.surface()\n .on(_pointerPrefix + 'down.modeRotate', null);\n\n d3_select(window)\n .on(_pointerPrefix + 'move.modeRotate', null, true)\n .on(_pointerPrefix + 'up.modeRotate', null, true);\n\n context.history()\n .on('undone.modeRotate', null);\n\n d3_select(document)\n .call(keybinding.unbind);\n\n context.features().forceVisible([]);\n };\n\n\n mode.selectedIDs = function() {\n if (!arguments.length) return entityIDs;\n // no assign\n return mode;\n };\n\n\n return mode;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { t } from '../core/localizer';\nimport { JXON } from '../util/jxon';\nimport { geoExtent } from '../geo';\nimport { osmChangeset } from '../osm';\nimport { svgIcon } from '../svg/icon';\n\nimport {\n utilEntityOrMemberSelector,\n utilKeybinding,\n utilRebind,\n utilWrap\n} from '../util';\n\n\nexport function uiConflicts(context) {\n var dispatch = d3_dispatch('cancel', 'save');\n var keybinding = utilKeybinding('conflicts');\n var _origChanges;\n var _conflictList;\n var _shownConflictIndex;\n\n\n function keybindingOn() {\n d3_select(document)\n .call(keybinding.on('\u238B', cancel, true));\n }\n\n function keybindingOff() {\n d3_select(document)\n .call(keybinding.unbind);\n }\n\n function tryAgain() {\n keybindingOff();\n dispatch.call('save');\n }\n\n function cancel() {\n keybindingOff();\n dispatch.call('cancel');\n }\n\n\n function conflicts(selection) {\n keybindingOn();\n\n var headerEnter = selection.selectAll('.header')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'fr')\n .attr('title', t('icons.close'))\n .on('click', cancel)\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('save.conflict.header'));\n\n var bodyEnter = selection.selectAll('.body')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'body fillL');\n\n var conflictsHelpEnter = bodyEnter\n .append('div')\n .attr('class', 'conflicts-help')\n .call(t.append('save.conflict.help'));\n\n\n // Download changes link\n var changeset = new osmChangeset();\n\n delete changeset.id; // Export without changeset_id\n\n var data = JXON.stringify(changeset.osmChangeJXON(_origChanges));\n var blob = new Blob([data], { type: 'text/xml;charset=utf-8;' });\n var fileName = 'changes.osc';\n\n var linkEnter = conflictsHelpEnter.selectAll('.download-changes')\n .append('a')\n .attr('class', 'download-changes');\n\n // download the data as a file\n linkEnter\n .attr('href', window.URL.createObjectURL(blob))\n .attr('download', fileName);\n\n linkEnter\n .call(svgIcon('#iD-icon-load', 'inline'))\n .append('span')\n .call(t.append('save.conflict.download_changes'));\n\n\n bodyEnter\n .append('div')\n .attr('class', 'conflict-container fillL3')\n .call(showConflict, 0);\n\n bodyEnter\n .append('div')\n .attr('class', 'conflicts-done')\n .attr('opacity', 0)\n .style('display', 'none')\n .call(t.append('save.conflict.done'));\n\n var buttonsEnter = bodyEnter\n .append('div')\n .attr('class','buttons col12 joined conflicts-buttons');\n\n buttonsEnter\n .append('button')\n .attr('disabled', _conflictList.length > 1)\n .attr('class', 'action conflicts-button col6')\n .call(t.append('save.title'))\n .on('click.try_again', tryAgain);\n\n buttonsEnter\n .append('button')\n .attr('class', 'secondary-action conflicts-button col6')\n .call(t.append('confirm.cancel'))\n .on('click.cancel', cancel);\n }\n\n\n function showConflict(selection, index) {\n index = utilWrap(index, _conflictList.length);\n _shownConflictIndex = index;\n\n var parent = d3_select(selection.node().parentNode);\n\n // enable save button if this is the last conflict being reviewed..\n if (index === _conflictList.length - 1) {\n window.setTimeout(function() {\n parent.select('.conflicts-button')\n .attr('disabled', null);\n\n parent.select('.conflicts-done')\n .transition()\n .attr('opacity', 1)\n .style('display', 'block');\n }, 250);\n }\n\n var conflict = selection\n .selectAll('.conflict')\n .data([_conflictList[index]]);\n\n conflict.exit()\n .remove();\n\n var conflictEnter = conflict.enter()\n .append('div')\n .attr('class', 'conflict');\n\n conflictEnter\n .append('h4')\n .attr('class', 'conflict-count')\n .call(t.append('save.conflict.count', { num: index + 1, total: _conflictList.length }));\n\n conflictEnter\n .append('a')\n .attr('class', 'conflict-description')\n .attr('href', '#')\n .text(function(d) { return d.name; })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n zoomToEntity(d.id);\n });\n\n var details = conflictEnter\n .append('div')\n .attr('class', 'conflict-detail-container');\n\n details\n .append('ul')\n .attr('class', 'conflict-detail-list')\n .selectAll('li')\n .data(function(d) { return d.details || []; })\n .enter()\n .append('li')\n .attr('class', 'conflict-detail-item')\n .each(function(d) {\n d3_select(this).call(d);\n });\n\n details\n .append('div')\n .attr('class', 'conflict-choices')\n .call(addChoices);\n\n details\n .append('div')\n .attr('class', 'conflict-nav-buttons joined cf')\n .selectAll('button')\n .data(['previous', 'next'])\n .enter()\n .append('button')\n .attr('class', 'conflict-nav-button action col6')\n .attr('disabled', function(d, i) {\n return (i === 0 && index === 0) ||\n (i === 1 && index === _conflictList.length - 1) || null;\n })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n\n var container = parent.selectAll('.conflict-container');\n var sign = (d === 'previous' ? -1 : 1);\n\n container\n .selectAll('.conflict')\n .remove();\n\n container\n .call(showConflict, index + sign);\n })\n .each(function(d) { t.append('save.conflict.' + d)(d3_select(this)); });\n\n }\n\n\n function addChoices(selection) {\n var choices = selection\n .append('ul')\n .attr('class', 'layer-list')\n .selectAll('li')\n .data(function(d) { return d.choices || []; });\n\n // enter\n var choicesEnter = choices.enter()\n .append('li')\n .attr('class', 'layer');\n\n var labelEnter = choicesEnter\n .append('label');\n\n labelEnter\n .append('input')\n .attr('type', 'radio')\n .attr('name', function(d) { return d.id; })\n .on('change', function(d3_event, d) {\n var ul = this.parentNode.parentNode.parentNode;\n ul.__data__.chosen = d.id;\n choose(d3_event, ul, d);\n });\n\n labelEnter\n .append('span')\n .text(function(d) { return d.text; });\n\n // update\n choicesEnter\n .merge(choices)\n .each(function(d) {\n var ul = this.parentNode;\n if (ul.__data__.chosen === d.id) {\n choose(null, ul, d);\n }\n });\n }\n\n\n function choose(d3_event, ul, datum) {\n if (d3_event) d3_event.preventDefault();\n\n d3_select(ul)\n .selectAll('li')\n .classed('active', function(d) { return d === datum; })\n .selectAll('input')\n .property('checked', function(d) { return d === datum; });\n\n var extent = geoExtent();\n var entity;\n\n entity = context.graph().hasEntity(datum.id);\n if (entity) extent._extend(entity.extent(context.graph()));\n\n datum.action();\n\n entity = context.graph().hasEntity(datum.id);\n if (entity) extent._extend(entity.extent(context.graph()));\n\n zoomToEntity(datum.id, extent);\n }\n\n\n function zoomToEntity(id, extent) {\n context.surface().selectAll('.hover')\n .classed('hover', false);\n\n var entity = context.graph().hasEntity(id);\n if (entity) {\n if (extent) {\n context.map().trimmedExtent(extent);\n } else {\n context.map().zoomToEase(entity);\n }\n context.surface().selectAll(utilEntityOrMemberSelector([entity.id], context.graph()))\n .classed('hover', true);\n }\n }\n\n\n // The conflict list should be an array of objects like:\n // {\n // id: id,\n // name: entityName(local),\n // details: merge.conflicts(),\n // chosen: 1,\n // choices: [\n // choice(id, keepMine, forceLocal),\n // choice(id, keepTheirs, forceRemote)\n // ]\n // }\n conflicts.conflictList = function(_) {\n if (!arguments.length) return _conflictList;\n _conflictList = _;\n return conflicts;\n };\n\n\n conflicts.origChanges = function(_) {\n if (!arguments.length) return _origChanges;\n _origChanges = _;\n return conflicts;\n };\n\n\n conflicts.shownEntityIds = function() {\n if (_conflictList && typeof _shownConflictIndex === 'number') {\n return [_conflictList[_shownConflictIndex].id];\n }\n return [];\n };\n\n\n return utilRebind(conflicts, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { uiModal } from './modal';\n\n\nexport function uiConfirm(selection) {\n var modalSelection = uiModal(selection);\n\n modalSelection.select('.modal')\n .classed('modal-alert', true);\n\n var section = modalSelection.select('.content');\n\n section.append('div')\n .attr('class', 'modal-section header');\n\n section.append('div')\n .attr('class', 'modal-section message-text');\n\n var buttons = section.append('div')\n .attr('class', 'modal-section buttons cf');\n\n\n modalSelection.okButton = function() {\n buttons\n .append('button')\n .attr('class', 'button ok-button action')\n .on('click.confirm', function() {\n modalSelection.remove();\n })\n .call(t.append('confirm.okay'))\n .node()\n .focus();\n\n return modalSelection;\n };\n\n\n return modalSelection;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { utilFunctor } from '../util/util';\n\nvar _popoverID = 0;\n\nexport function uiPopover(klass) {\n var _id = _popoverID++;\n var _anchorSelection = d3_select(null);\n var popover = function(selection) {\n _anchorSelection = selection;\n selection.each(setup);\n };\n var _animation = utilFunctor(false);\n var _placement = utilFunctor('top'); // top, bottom, left, right\n var _alignment = utilFunctor('center'); // leading, center, trailing\n var _scrollContainer = utilFunctor(d3_select(null));\n var _content;\n var _displayType = utilFunctor('');\n var _hasArrow = utilFunctor(true);\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n popover.displayType = function(val) {\n if (arguments.length) {\n _displayType = utilFunctor(val);\n return popover;\n } else {\n return _displayType;\n }\n };\n\n popover.hasArrow = function(val) {\n if (arguments.length) {\n _hasArrow = utilFunctor(val);\n return popover;\n } else {\n return _hasArrow;\n }\n };\n\n popover.placement = function(val) {\n if (arguments.length) {\n _placement = utilFunctor(val);\n return popover;\n } else {\n return _placement;\n }\n };\n\n popover.alignment = function(val) {\n if (arguments.length) {\n _alignment = utilFunctor(val);\n return popover;\n } else {\n return _alignment;\n }\n };\n\n popover.scrollContainer = function(val) {\n if (arguments.length) {\n _scrollContainer = utilFunctor(val);\n return popover;\n } else {\n return _scrollContainer;\n }\n };\n\n popover.content = function(val) {\n if (arguments.length) {\n _content = val;\n return popover;\n } else {\n return _content;\n }\n };\n\n popover.isShown = function() {\n var popoverSelection = _anchorSelection.select('.popover-' + _id);\n return !popoverSelection.empty() && popoverSelection.classed('in');\n };\n\n popover.show = function() {\n _anchorSelection.each(show);\n };\n\n popover.updateContent = function() {\n _anchorSelection.each(updateContent);\n };\n\n popover.hide = function() {\n _anchorSelection.each(hide);\n };\n\n popover.toggle = function() {\n _anchorSelection.each(toggle);\n };\n\n popover.destroy = function(selection, selector) {\n // by default, just destroy the current popover\n selector = selector || '.popover-' + _id;\n\n selection\n .on(_pointerPrefix + 'enter.popover', null)\n .on(_pointerPrefix + 'leave.popover', null)\n .on(_pointerPrefix + 'up.popover', null)\n .on(_pointerPrefix + 'down.popover', null)\n .on('focus.popover', null)\n .on('blur.popover', null)\n .on('click.popover', null)\n .attr('title', function() {\n return this.getAttribute('data-original-title') || this.getAttribute('title');\n })\n .attr('data-original-title', null)\n .selectAll(selector)\n .remove();\n };\n\n\n popover.destroyAny = function(selection) {\n selection.call(popover.destroy, '.popover');\n };\n\n function setup() {\n var anchor = d3_select(this);\n var animate = _animation.apply(this, arguments);\n var popoverSelection = anchor.selectAll('.popover-' + _id)\n .data([0]);\n\n\n var enter = popoverSelection.enter()\n .append('div')\n .attr('class', 'popover popover-' + _id + ' ' + (klass ? klass : ''))\n .classed('arrowed', _hasArrow.apply(this, arguments));\n\n enter\n .append('div')\n .attr('class', 'popover-arrow');\n\n enter\n .append('div')\n .attr('class', 'popover-inner');\n\n popoverSelection = enter\n .merge(popoverSelection);\n\n if (animate) {\n popoverSelection.classed('fade', true);\n }\n\n var display = _displayType.apply(this, arguments);\n\n if (display === 'hover') {\n var _lastNonMouseEnterTime;\n anchor.on(_pointerPrefix + 'enter.popover', function(d3_event) {\n\n if (d3_event.pointerType) {\n if (d3_event.pointerType !== 'mouse') {\n _lastNonMouseEnterTime = d3_event.timeStamp;\n // only allow hover behavior for mouse input\n return;\n } else if (_lastNonMouseEnterTime &&\n d3_event.timeStamp - _lastNonMouseEnterTime < 1500) {\n // HACK: iOS 13.4 sends an erroneous `mouse` type pointerenter\n // event for non-mouse interactions right after sending\n // the correct type pointerenter event. Workaround by discarding\n // any mouse event that occurs immediately after a non-mouse event.\n return;\n }\n }\n\n // don't show if buttons are pressed, e.g. during click and drag of map\n if (d3_event.buttons !== 0) return;\n\n show.apply(this, arguments);\n })\n .on(_pointerPrefix + 'leave.popover', function() {\n hide.apply(this, arguments);\n })\n // show on focus too for better keyboard navigation support\n .on('focus.popover', function() {\n show.apply(this, arguments);\n })\n .on('blur.popover', function() {\n hide.apply(this, arguments);\n });\n\n } else if (display === 'clickFocus') {\n anchor\n .on(_pointerPrefix + 'down.popover', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n })\n .on(_pointerPrefix + 'up.popover', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n })\n .on('click.popover', toggle);\n\n popoverSelection\n // This attribute lets the popover take focus\n .attr('tabindex', 0)\n .on('blur.popover', function() {\n anchor.each(function() {\n hide.apply(this, arguments);\n });\n });\n }\n }\n\n\n function show() {\n var anchor = d3_select(this);\n var popoverSelection = anchor.selectAll('.popover-' + _id);\n\n if (popoverSelection.empty()) {\n // popover was removed somehow, put it back\n anchor.call(popover.destroy);\n anchor.each(setup);\n popoverSelection = anchor.selectAll('.popover-' + _id);\n }\n\n popoverSelection.classed('in', true);\n\n var displayType = _displayType.apply(this, arguments);\n if (displayType === 'clickFocus') {\n anchor.classed('active', true);\n popoverSelection.node().focus();\n }\n\n anchor.each(updateContent);\n }\n\n function updateContent() {\n var anchor = d3_select(this);\n\n if (_content) {\n anchor.selectAll('.popover-' + _id + ' > .popover-inner')\n .call(_content.apply(this, arguments));\n }\n\n updatePosition.apply(this, arguments);\n // hack: update multiple times to fix instances where the absolute offset is\n // set before the dynamic popover size is calculated by the browser\n updatePosition.apply(this, arguments);\n updatePosition.apply(this, arguments);\n }\n\n\n function updatePosition() {\n\n var anchor = d3_select(this);\n var popoverSelection = anchor.selectAll('.popover-' + _id);\n\n var scrollContainer = _scrollContainer && _scrollContainer.apply(this, arguments);\n var scrollNode = scrollContainer && !scrollContainer.empty() && scrollContainer.node();\n var scrollLeft = scrollNode ? scrollNode.scrollLeft : 0;\n var scrollTop = scrollNode ? scrollNode.scrollTop : 0;\n\n var placement = _placement.apply(this, arguments);\n popoverSelection\n .classed('left', false)\n .classed('right', false)\n .classed('top', false)\n .classed('bottom', false)\n .classed(placement, true);\n\n var alignment = _alignment.apply(this, arguments);\n var alignFactor = 0.5;\n if (alignment === 'leading') {\n alignFactor = 0;\n } else if (alignment === 'trailing') {\n alignFactor = 1;\n }\n var anchorFrame = getFrame(anchor.node());\n var popoverFrame = getFrame(popoverSelection.node());\n var position;\n\n switch (placement) {\n case 'top':\n position = {\n x: anchorFrame.x + (anchorFrame.w - popoverFrame.w) * alignFactor,\n y: anchorFrame.y - popoverFrame.h\n };\n break;\n case 'bottom':\n position = {\n x: anchorFrame.x + (anchorFrame.w - popoverFrame.w) * alignFactor,\n y: anchorFrame.y + anchorFrame.h\n };\n break;\n case 'left':\n position = {\n x: anchorFrame.x - popoverFrame.w,\n y: anchorFrame.y + (anchorFrame.h - popoverFrame.h) * alignFactor\n };\n break;\n case 'right':\n position = {\n x: anchorFrame.x + anchorFrame.w,\n y: anchorFrame.y + (anchorFrame.h - popoverFrame.h) * alignFactor\n };\n break;\n }\n\n if (position) {\n if (scrollNode) {\n const MIN_MARGIN = 10;\n const popoverRect = popoverSelection.node().getBoundingClientRect();\n const scrollNodeRect = scrollNode.getBoundingClientRect();\n const arrow = anchor.selectAll('.popover-' + _id + ' > .popover-arrow');\n\n if (placement === 'top' || placement === 'bottom') {\n const initialPosX = position.x;\n if (popoverRect.right > scrollNodeRect.right - MIN_MARGIN) {\n position.x -= popoverRect.right - (scrollNodeRect.right - MIN_MARGIN);\n } else if (popoverRect.left < scrollNodeRect.left) {\n position.x += (scrollNodeRect.left + MIN_MARGIN) - popoverRect.left;\n }\n // keep the arrow centered on the button, or as close as possible\n const arrowPosX = Math.min(Math.max(popoverFrame.w / 2 - (position.x - initialPosX), MIN_MARGIN), popoverFrame.w - MIN_MARGIN);\n arrow.style('left', ~~arrowPosX + 'px');\n\n } else if (placement === 'left' || placement === 'right') {\n const initialPosY = position.y;\n if (popoverRect.bottom > scrollNodeRect.bottom - MIN_MARGIN) {\n position.y -= popoverRect.bottom - (scrollNodeRect.bottom - MIN_MARGIN);\n } else if (popoverRect.top < scrollNodeRect.top + MIN_MARGIN) {\n position.y += (scrollNodeRect.top + MIN_MARGIN) - popoverRect.top;\n }\n // keep the arrow centered on the button, or as close as possible\n const arrowPosY = Math.min(Math.max(popoverFrame.h / 2 - (position.y - initialPosY), MIN_MARGIN), popoverFrame.h - MIN_MARGIN);\n arrow.style('top', ~~arrowPosY + 'px');\n }\n }\n\n popoverSelection\n .style('left', ~~position.x + 'px')\n .style('top', ~~position.y + 'px');\n } else {\n popoverSelection\n .style('left', null)\n .style('top', null);\n }\n\n function getFrame(node) {\n var positionStyle = d3_select(node).style('position');\n if (positionStyle === 'absolute' || positionStyle === 'static') {\n return {\n x: node.offsetLeft - scrollLeft,\n y: node.offsetTop - scrollTop,\n w: node.offsetWidth,\n h: node.offsetHeight\n };\n } else {\n return {\n x: 0,\n y: 0,\n w: node.offsetWidth,\n h: node.offsetHeight\n };\n }\n }\n }\n\n\n function hide() {\n var anchor = d3_select(this);\n if (_displayType.apply(this, arguments) === 'clickFocus') {\n anchor.classed('active', false);\n }\n anchor.selectAll('.popover-' + _id).classed('in', false);\n }\n\n\n function toggle() {\n if (d3_select(this).select('.popover-' + _id).classed('in')) {\n hide.apply(this, arguments);\n } else {\n show.apply(this, arguments);\n }\n }\n\n\n return popover;\n}\n", "import { utilFunctor } from '../util/util';\nimport { t } from '../core/localizer';\nimport { uiPopover } from './popover';\n\nexport function uiTooltip(klass) {\n\n var tooltip = uiPopover((klass || '') + ' tooltip')\n .displayType('hover');\n\n var _title = function() {\n var title = this.getAttribute('data-original-title');\n if (title) {\n return title;\n } else {\n title = this.getAttribute('title');\n this.removeAttribute('title');\n this.setAttribute('data-original-title', title);\n }\n return title;\n };\n\n var _heading = utilFunctor(null);\n var _keys = utilFunctor(null);\n\n tooltip.title = function(val) {\n if (!arguments.length) return _title;\n _title = utilFunctor(val);\n return tooltip;\n };\n\n tooltip.heading = function(val) {\n if (!arguments.length) return _heading;\n _heading = utilFunctor(val);\n return tooltip;\n };\n\n tooltip.keys = function(val) {\n if (!arguments.length) return _keys;\n _keys = utilFunctor(val);\n return tooltip;\n };\n\n tooltip.content(function() {\n var heading = _heading.apply(this, arguments);\n var text = _title.apply(this, arguments);\n var keys = _keys.apply(this, arguments);\n\n var headingCallback = typeof heading === 'function' ? heading : s => s.text(heading);\n var textCallback = typeof text === 'function' ? text : s => s.text(text);\n\n return function(selection) {\n\n var headingSelect = selection\n .selectAll('.tooltip-heading')\n .data(heading ? [heading] :[]);\n\n headingSelect.exit()\n .remove();\n\n headingSelect.enter()\n .append('div')\n .attr('class', 'tooltip-heading')\n .merge(headingSelect)\n .text('')\n .call(headingCallback);\n\n var textSelect = selection\n .selectAll('.tooltip-text')\n .data(text ? [text] :[]);\n\n textSelect.exit()\n .remove();\n\n textSelect.enter()\n .append('div')\n .attr('class', 'tooltip-text')\n .merge(textSelect)\n .text('')\n .call(textCallback);\n\n var keyhintWrap = selection\n .selectAll('.keyhint-wrap')\n .data(keys && keys.length ? [0] : []);\n\n keyhintWrap.exit()\n .remove();\n\n var keyhintWrapEnter = keyhintWrap.enter()\n .append('div')\n .attr('class', 'keyhint-wrap');\n\n keyhintWrapEnter\n .append('span')\n .call(t.append('tooltip_keyhint'));\n\n keyhintWrap = keyhintWrapEnter.merge(keyhintWrap);\n\n keyhintWrap.selectAll('kbd.shortcut')\n .data(keys && keys.length ? keys : [])\n .enter()\n .append('kbd')\n .attr('class', 'shortcut')\n .text(function(d) {\n return d;\n });\n };\n });\n\n return tooltip;\n}\n", "/*\r\n * bignumber.js v9.3.1\r\n * A JavaScript library for arbitrary-precision arithmetic.\r\n * https://github.com/MikeMcl/bignumber.js\r\n * Copyright (c) 2025 Michael Mclaughlin \r\n * MIT Licensed.\r\n *\r\n * BigNumber.prototype methods | BigNumber methods\r\n * |\r\n * absoluteValue abs | clone\r\n * comparedTo | config set\r\n * decimalPlaces dp | DECIMAL_PLACES\r\n * dividedBy div | ROUNDING_MODE\r\n * dividedToIntegerBy idiv | EXPONENTIAL_AT\r\n * exponentiatedBy pow | RANGE\r\n * integerValue | CRYPTO\r\n * isEqualTo eq | MODULO_MODE\r\n * isFinite | POW_PRECISION\r\n * isGreaterThan gt | FORMAT\r\n * isGreaterThanOrEqualTo gte | ALPHABET\r\n * isInteger | isBigNumber\r\n * isLessThan lt | maximum max\r\n * isLessThanOrEqualTo lte | minimum min\r\n * isNaN | random\r\n * isNegative | sum\r\n * isPositive |\r\n * isZero |\r\n * minus |\r\n * modulo mod |\r\n * multipliedBy times |\r\n * negated |\r\n * plus |\r\n * precision sd |\r\n * shiftedBy |\r\n * squareRoot sqrt |\r\n * toExponential |\r\n * toFixed |\r\n * toFormat |\r\n * toFraction |\r\n * toJSON |\r\n * toNumber |\r\n * toPrecision |\r\n * toString |\r\n * valueOf |\r\n *\r\n */\r\n\r\n\r\nvar\r\n isNumeric = /^-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?$/i,\r\n mathceil = Math.ceil,\r\n mathfloor = Math.floor,\r\n\r\n bignumberError = '[BigNumber Error] ',\r\n tooManyDigits = bignumberError + 'Number primitive has more than 15 significant digits: ',\r\n\r\n BASE = 1e14,\r\n LOG_BASE = 14,\r\n MAX_SAFE_INTEGER = 0x1fffffffffffff, // 2^53 - 1\r\n // MAX_INT32 = 0x7fffffff, // 2^31 - 1\r\n POWS_TEN = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13],\r\n SQRT_BASE = 1e7,\r\n\r\n // EDITABLE\r\n // The limit on the value of DECIMAL_PLACES, TO_EXP_NEG, TO_EXP_POS, MIN_EXP, MAX_EXP, and\r\n // the arguments to toExponential, toFixed, toFormat, and toPrecision.\r\n MAX = 1E9; // 0 to MAX_INT32\r\n\r\n\r\n/*\r\n * Create and return a BigNumber constructor.\r\n */\r\nfunction clone(configObject) {\r\n var div, convertBase, parseNumeric,\r\n P = BigNumber.prototype = { constructor: BigNumber, toString: null, valueOf: null },\r\n ONE = new BigNumber(1),\r\n\r\n\r\n //----------------------------- EDITABLE CONFIG DEFAULTS -------------------------------\r\n\r\n\r\n // The default values below must be integers within the inclusive ranges stated.\r\n // The values can also be changed at run-time using BigNumber.set.\r\n\r\n // The maximum number of decimal places for operations involving division.\r\n DECIMAL_PLACES = 20, // 0 to MAX\r\n\r\n // The rounding mode used when rounding to the above decimal places, and when using\r\n // toExponential, toFixed, toFormat and toPrecision, and round (default value).\r\n // UP 0 Away from zero.\r\n // DOWN 1 Towards zero.\r\n // CEIL 2 Towards +Infinity.\r\n // FLOOR 3 Towards -Infinity.\r\n // HALF_UP 4 Towards nearest neighbour. If equidistant, up.\r\n // HALF_DOWN 5 Towards nearest neighbour. If equidistant, down.\r\n // HALF_EVEN 6 Towards nearest neighbour. If equidistant, towards even neighbour.\r\n // HALF_CEIL 7 Towards nearest neighbour. If equidistant, towards +Infinity.\r\n // HALF_FLOOR 8 Towards nearest neighbour. If equidistant, towards -Infinity.\r\n ROUNDING_MODE = 4, // 0 to 8\r\n\r\n // EXPONENTIAL_AT : [TO_EXP_NEG , TO_EXP_POS]\r\n\r\n // The exponent value at and beneath which toString returns exponential notation.\r\n // Number type: -7\r\n TO_EXP_NEG = -7, // 0 to -MAX\r\n\r\n // The exponent value at and above which toString returns exponential notation.\r\n // Number type: 21\r\n TO_EXP_POS = 21, // 0 to MAX\r\n\r\n // RANGE : [MIN_EXP, MAX_EXP]\r\n\r\n // The minimum exponent value, beneath which underflow to zero occurs.\r\n // Number type: -324 (5e-324)\r\n MIN_EXP = -1e7, // -1 to -MAX\r\n\r\n // The maximum exponent value, above which overflow to Infinity occurs.\r\n // Number type: 308 (1.7976931348623157e+308)\r\n // For MAX_EXP > 1e7, e.g. new BigNumber('1e100000000').plus(1) may be slow.\r\n MAX_EXP = 1e7, // 1 to MAX\r\n\r\n // Whether to use cryptographically-secure random number generation, if available.\r\n CRYPTO = false, // true or false\r\n\r\n // The modulo mode used when calculating the modulus: a mod n.\r\n // The quotient (q = a / n) is calculated according to the corresponding rounding mode.\r\n // The remainder (r) is calculated as: r = a - n * q.\r\n //\r\n // UP 0 The remainder is positive if the dividend is negative, else is negative.\r\n // DOWN 1 The remainder has the same sign as the dividend.\r\n // This modulo mode is commonly known as 'truncated division' and is\r\n // equivalent to (a % n) in JavaScript.\r\n // FLOOR 3 The remainder has the same sign as the divisor (Python %).\r\n // HALF_EVEN 6 This modulo mode implements the IEEE 754 remainder function.\r\n // EUCLID 9 Euclidian division. q = sign(n) * floor(a / abs(n)).\r\n // The remainder is always positive.\r\n //\r\n // The truncated division, floored division, Euclidian division and IEEE 754 remainder\r\n // modes are commonly used for the modulus operation.\r\n // Although the other rounding modes can also be used, they may not give useful results.\r\n MODULO_MODE = 1, // 0 to 9\r\n\r\n // The maximum number of significant digits of the result of the exponentiatedBy operation.\r\n // If POW_PRECISION is 0, there will be unlimited significant digits.\r\n POW_PRECISION = 0, // 0 to MAX\r\n\r\n // The format specification used by the BigNumber.prototype.toFormat method.\r\n FORMAT = {\r\n prefix: '',\r\n groupSize: 3,\r\n secondaryGroupSize: 0,\r\n groupSeparator: ',',\r\n decimalSeparator: '.',\r\n fractionGroupSize: 0,\r\n fractionGroupSeparator: '\\xA0', // non-breaking space\r\n suffix: ''\r\n },\r\n\r\n // The alphabet used for base conversion. It must be at least 2 characters long, with no '+',\r\n // '-', '.', whitespace, or repeated character.\r\n // '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_'\r\n ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz',\r\n alphabetHasNormalDecimalDigits = true;\r\n\r\n\r\n //------------------------------------------------------------------------------------------\r\n\r\n\r\n // CONSTRUCTOR\r\n\r\n\r\n /*\r\n * The BigNumber constructor and exported function.\r\n * Create and return a new instance of a BigNumber object.\r\n *\r\n * v {number|string|BigNumber} A numeric value.\r\n * [b] {number} The base of v. Integer, 2 to ALPHABET.length inclusive.\r\n */\r\n function BigNumber(v, b) {\r\n var alphabet, c, caseChanged, e, i, isNum, len, str,\r\n x = this;\r\n\r\n // Enable constructor call without `new`.\r\n if (!(x instanceof BigNumber)) return new BigNumber(v, b);\r\n\r\n if (b == null) {\r\n\r\n if (v && v._isBigNumber === true) {\r\n x.s = v.s;\r\n\r\n if (!v.c || v.e > MAX_EXP) {\r\n x.c = x.e = null;\r\n } else if (v.e < MIN_EXP) {\r\n x.c = [x.e = 0];\r\n } else {\r\n x.e = v.e;\r\n x.c = v.c.slice();\r\n }\r\n\r\n return;\r\n }\r\n\r\n if ((isNum = typeof v == 'number') && v * 0 == 0) {\r\n\r\n // Use `1 / n` to handle minus zero also.\r\n x.s = 1 / v < 0 ? (v = -v, -1) : 1;\r\n\r\n // Fast path for integers, where n < 2147483648 (2**31).\r\n if (v === ~~v) {\r\n for (e = 0, i = v; i >= 10; i /= 10, e++);\r\n\r\n if (e > MAX_EXP) {\r\n x.c = x.e = null;\r\n } else {\r\n x.e = e;\r\n x.c = [v];\r\n }\r\n\r\n return;\r\n }\r\n\r\n str = String(v);\r\n } else {\r\n\r\n if (!isNumeric.test(str = String(v))) return parseNumeric(x, str, isNum);\r\n\r\n x.s = str.charCodeAt(0) == 45 ? (str = str.slice(1), -1) : 1;\r\n }\r\n\r\n // Decimal point?\r\n if ((e = str.indexOf('.')) > -1) str = str.replace('.', '');\r\n\r\n // Exponential form?\r\n if ((i = str.search(/e/i)) > 0) {\r\n\r\n // Determine exponent.\r\n if (e < 0) e = i;\r\n e += +str.slice(i + 1);\r\n str = str.substring(0, i);\r\n } else if (e < 0) {\r\n\r\n // Integer.\r\n e = str.length;\r\n }\r\n\r\n } else {\r\n\r\n // '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}'\r\n intCheck(b, 2, ALPHABET.length, 'Base');\r\n\r\n // Allow exponential notation to be used with base 10 argument, while\r\n // also rounding to DECIMAL_PLACES as with other bases.\r\n if (b == 10 && alphabetHasNormalDecimalDigits) {\r\n x = new BigNumber(v);\r\n return round(x, DECIMAL_PLACES + x.e + 1, ROUNDING_MODE);\r\n }\r\n\r\n str = String(v);\r\n\r\n if (isNum = typeof v == 'number') {\r\n\r\n // Avoid potential interpretation of Infinity and NaN as base 44+ values.\r\n if (v * 0 != 0) return parseNumeric(x, str, isNum, b);\r\n\r\n x.s = 1 / v < 0 ? (str = str.slice(1), -1) : 1;\r\n\r\n // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}'\r\n if (BigNumber.DEBUG && str.replace(/^0\\.0*|\\./, '').length > 15) {\r\n throw Error\r\n (tooManyDigits + v);\r\n }\r\n } else {\r\n x.s = str.charCodeAt(0) === 45 ? (str = str.slice(1), -1) : 1;\r\n }\r\n\r\n alphabet = ALPHABET.slice(0, b);\r\n e = i = 0;\r\n\r\n // Check that str is a valid base b number.\r\n // Don't use RegExp, so alphabet can contain special characters.\r\n for (len = str.length; i < len; i++) {\r\n if (alphabet.indexOf(c = str.charAt(i)) < 0) {\r\n if (c == '.') {\r\n\r\n // If '.' is not the first character and it has not be found before.\r\n if (i > e) {\r\n e = len;\r\n continue;\r\n }\r\n } else if (!caseChanged) {\r\n\r\n // Allow e.g. hexadecimal 'FF' as well as 'ff'.\r\n if (str == str.toUpperCase() && (str = str.toLowerCase()) ||\r\n str == str.toLowerCase() && (str = str.toUpperCase())) {\r\n caseChanged = true;\r\n i = -1;\r\n e = 0;\r\n continue;\r\n }\r\n }\r\n\r\n return parseNumeric(x, String(v), isNum, b);\r\n }\r\n }\r\n\r\n // Prevent later check for length on converted number.\r\n isNum = false;\r\n str = convertBase(str, b, 10, x.s);\r\n\r\n // Decimal point?\r\n if ((e = str.indexOf('.')) > -1) str = str.replace('.', '');\r\n else e = str.length;\r\n }\r\n\r\n // Determine leading zeros.\r\n for (i = 0; str.charCodeAt(i) === 48; i++);\r\n\r\n // Determine trailing zeros.\r\n for (len = str.length; str.charCodeAt(--len) === 48;);\r\n\r\n if (str = str.slice(i, ++len)) {\r\n len -= i;\r\n\r\n // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}'\r\n if (isNum && BigNumber.DEBUG &&\r\n len > 15 && (v > MAX_SAFE_INTEGER || v !== mathfloor(v))) {\r\n throw Error\r\n (tooManyDigits + (x.s * v));\r\n }\r\n\r\n // Overflow?\r\n if ((e = e - i - 1) > MAX_EXP) {\r\n\r\n // Infinity.\r\n x.c = x.e = null;\r\n\r\n // Underflow?\r\n } else if (e < MIN_EXP) {\r\n\r\n // Zero.\r\n x.c = [x.e = 0];\r\n } else {\r\n x.e = e;\r\n x.c = [];\r\n\r\n // Transform base\r\n\r\n // e is the base 10 exponent.\r\n // i is where to slice str to get the first element of the coefficient array.\r\n i = (e + 1) % LOG_BASE;\r\n if (e < 0) i += LOG_BASE; // i < 1\r\n\r\n if (i < len) {\r\n if (i) x.c.push(+str.slice(0, i));\r\n\r\n for (len -= LOG_BASE; i < len;) {\r\n x.c.push(+str.slice(i, i += LOG_BASE));\r\n }\r\n\r\n i = LOG_BASE - (str = str.slice(i)).length;\r\n } else {\r\n i -= len;\r\n }\r\n\r\n for (; i--; str += '0');\r\n x.c.push(+str);\r\n }\r\n } else {\r\n\r\n // Zero.\r\n x.c = [x.e = 0];\r\n }\r\n }\r\n\r\n\r\n // CONSTRUCTOR PROPERTIES\r\n\r\n\r\n BigNumber.clone = clone;\r\n\r\n BigNumber.ROUND_UP = 0;\r\n BigNumber.ROUND_DOWN = 1;\r\n BigNumber.ROUND_CEIL = 2;\r\n BigNumber.ROUND_FLOOR = 3;\r\n BigNumber.ROUND_HALF_UP = 4;\r\n BigNumber.ROUND_HALF_DOWN = 5;\r\n BigNumber.ROUND_HALF_EVEN = 6;\r\n BigNumber.ROUND_HALF_CEIL = 7;\r\n BigNumber.ROUND_HALF_FLOOR = 8;\r\n BigNumber.EUCLID = 9;\r\n\r\n\r\n /*\r\n * Configure infrequently-changing library-wide settings.\r\n *\r\n * Accept an object with the following optional properties (if the value of a property is\r\n * a number, it must be an integer within the inclusive range stated):\r\n *\r\n * DECIMAL_PLACES {number} 0 to MAX\r\n * ROUNDING_MODE {number} 0 to 8\r\n * EXPONENTIAL_AT {number|number[]} -MAX to MAX or [-MAX to 0, 0 to MAX]\r\n * RANGE {number|number[]} -MAX to MAX (not zero) or [-MAX to -1, 1 to MAX]\r\n * CRYPTO {boolean} true or false\r\n * MODULO_MODE {number} 0 to 9\r\n * POW_PRECISION {number} 0 to MAX\r\n * ALPHABET {string} A string of two or more unique characters which does\r\n * not contain '.'.\r\n * FORMAT {object} An object with some of the following properties:\r\n * prefix {string}\r\n * groupSize {number}\r\n * secondaryGroupSize {number}\r\n * groupSeparator {string}\r\n * decimalSeparator {string}\r\n * fractionGroupSize {number}\r\n * fractionGroupSeparator {string}\r\n * suffix {string}\r\n *\r\n * (The values assigned to the above FORMAT object properties are not checked for validity.)\r\n *\r\n * E.g.\r\n * BigNumber.config({ DECIMAL_PLACES : 20, ROUNDING_MODE : 4 })\r\n *\r\n * Ignore properties/parameters set to null or undefined, except for ALPHABET.\r\n *\r\n * Return an object with the properties current values.\r\n */\r\n BigNumber.config = BigNumber.set = function (obj) {\r\n var p, v;\r\n\r\n if (obj != null) {\r\n\r\n if (typeof obj == 'object') {\r\n\r\n // DECIMAL_PLACES {number} Integer, 0 to MAX inclusive.\r\n // '[BigNumber Error] DECIMAL_PLACES {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'DECIMAL_PLACES')) {\r\n v = obj[p];\r\n intCheck(v, 0, MAX, p);\r\n DECIMAL_PLACES = v;\r\n }\r\n\r\n // ROUNDING_MODE {number} Integer, 0 to 8 inclusive.\r\n // '[BigNumber Error] ROUNDING_MODE {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'ROUNDING_MODE')) {\r\n v = obj[p];\r\n intCheck(v, 0, 8, p);\r\n ROUNDING_MODE = v;\r\n }\r\n\r\n // EXPONENTIAL_AT {number|number[]}\r\n // Integer, -MAX to MAX inclusive or\r\n // [integer -MAX to 0 inclusive, 0 to MAX inclusive].\r\n // '[BigNumber Error] EXPONENTIAL_AT {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'EXPONENTIAL_AT')) {\r\n v = obj[p];\r\n if (v && v.pop) {\r\n intCheck(v[0], -MAX, 0, p);\r\n intCheck(v[1], 0, MAX, p);\r\n TO_EXP_NEG = v[0];\r\n TO_EXP_POS = v[1];\r\n } else {\r\n intCheck(v, -MAX, MAX, p);\r\n TO_EXP_NEG = -(TO_EXP_POS = v < 0 ? -v : v);\r\n }\r\n }\r\n\r\n // RANGE {number|number[]} Non-zero integer, -MAX to MAX inclusive or\r\n // [integer -MAX to -1 inclusive, integer 1 to MAX inclusive].\r\n // '[BigNumber Error] RANGE {not a primitive number|not an integer|out of range|cannot be zero}: {v}'\r\n if (obj.hasOwnProperty(p = 'RANGE')) {\r\n v = obj[p];\r\n if (v && v.pop) {\r\n intCheck(v[0], -MAX, -1, p);\r\n intCheck(v[1], 1, MAX, p);\r\n MIN_EXP = v[0];\r\n MAX_EXP = v[1];\r\n } else {\r\n intCheck(v, -MAX, MAX, p);\r\n if (v) {\r\n MIN_EXP = -(MAX_EXP = v < 0 ? -v : v);\r\n } else {\r\n throw Error\r\n (bignumberError + p + ' cannot be zero: ' + v);\r\n }\r\n }\r\n }\r\n\r\n // CRYPTO {boolean} true or false.\r\n // '[BigNumber Error] CRYPTO not true or false: {v}'\r\n // '[BigNumber Error] crypto unavailable'\r\n if (obj.hasOwnProperty(p = 'CRYPTO')) {\r\n v = obj[p];\r\n if (v === !!v) {\r\n if (v) {\r\n if (typeof crypto != 'undefined' && crypto &&\r\n (crypto.getRandomValues || crypto.randomBytes)) {\r\n CRYPTO = v;\r\n } else {\r\n CRYPTO = !v;\r\n throw Error\r\n (bignumberError + 'crypto unavailable');\r\n }\r\n } else {\r\n CRYPTO = v;\r\n }\r\n } else {\r\n throw Error\r\n (bignumberError + p + ' not true or false: ' + v);\r\n }\r\n }\r\n\r\n // MODULO_MODE {number} Integer, 0 to 9 inclusive.\r\n // '[BigNumber Error] MODULO_MODE {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'MODULO_MODE')) {\r\n v = obj[p];\r\n intCheck(v, 0, 9, p);\r\n MODULO_MODE = v;\r\n }\r\n\r\n // POW_PRECISION {number} Integer, 0 to MAX inclusive.\r\n // '[BigNumber Error] POW_PRECISION {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'POW_PRECISION')) {\r\n v = obj[p];\r\n intCheck(v, 0, MAX, p);\r\n POW_PRECISION = v;\r\n }\r\n\r\n // FORMAT {object}\r\n // '[BigNumber Error] FORMAT not an object: {v}'\r\n if (obj.hasOwnProperty(p = 'FORMAT')) {\r\n v = obj[p];\r\n if (typeof v == 'object') FORMAT = v;\r\n else throw Error\r\n (bignumberError + p + ' not an object: ' + v);\r\n }\r\n\r\n // ALPHABET {string}\r\n // '[BigNumber Error] ALPHABET invalid: {v}'\r\n if (obj.hasOwnProperty(p = 'ALPHABET')) {\r\n v = obj[p];\r\n\r\n // Disallow if less than two characters,\r\n // or if it contains '+', '-', '.', whitespace, or a repeated character.\r\n if (typeof v == 'string' && !/^.?$|[+\\-.\\s]|(.).*\\1/.test(v)) {\r\n alphabetHasNormalDecimalDigits = v.slice(0, 10) == '0123456789';\r\n ALPHABET = v;\r\n } else {\r\n throw Error\r\n (bignumberError + p + ' invalid: ' + v);\r\n }\r\n }\r\n\r\n } else {\r\n\r\n // '[BigNumber Error] Object expected: {v}'\r\n throw Error\r\n (bignumberError + 'Object expected: ' + obj);\r\n }\r\n }\r\n\r\n return {\r\n DECIMAL_PLACES: DECIMAL_PLACES,\r\n ROUNDING_MODE: ROUNDING_MODE,\r\n EXPONENTIAL_AT: [TO_EXP_NEG, TO_EXP_POS],\r\n RANGE: [MIN_EXP, MAX_EXP],\r\n CRYPTO: CRYPTO,\r\n MODULO_MODE: MODULO_MODE,\r\n POW_PRECISION: POW_PRECISION,\r\n FORMAT: FORMAT,\r\n ALPHABET: ALPHABET\r\n };\r\n };\r\n\r\n\r\n /*\r\n * Return true if v is a BigNumber instance, otherwise return false.\r\n *\r\n * If BigNumber.DEBUG is true, throw if a BigNumber instance is not well-formed.\r\n *\r\n * v {any}\r\n *\r\n * '[BigNumber Error] Invalid BigNumber: {v}'\r\n */\r\n BigNumber.isBigNumber = function (v) {\r\n if (!v || v._isBigNumber !== true) return false;\r\n if (!BigNumber.DEBUG) return true;\r\n\r\n var i, n,\r\n c = v.c,\r\n e = v.e,\r\n s = v.s;\r\n\r\n out: if ({}.toString.call(c) == '[object Array]') {\r\n\r\n if ((s === 1 || s === -1) && e >= -MAX && e <= MAX && e === mathfloor(e)) {\r\n\r\n // If the first element is zero, the BigNumber value must be zero.\r\n if (c[0] === 0) {\r\n if (e === 0 && c.length === 1) return true;\r\n break out;\r\n }\r\n\r\n // Calculate number of digits that c[0] should have, based on the exponent.\r\n i = (e + 1) % LOG_BASE;\r\n if (i < 1) i += LOG_BASE;\r\n\r\n // Calculate number of digits of c[0].\r\n //if (Math.ceil(Math.log(c[0] + 1) / Math.LN10) == i) {\r\n if (String(c[0]).length == i) {\r\n\r\n for (i = 0; i < c.length; i++) {\r\n n = c[i];\r\n if (n < 0 || n >= BASE || n !== mathfloor(n)) break out;\r\n }\r\n\r\n // Last element cannot be zero, unless it is the only element.\r\n if (n !== 0) return true;\r\n }\r\n }\r\n\r\n // Infinity/NaN\r\n } else if (c === null && e === null && (s === null || s === 1 || s === -1)) {\r\n return true;\r\n }\r\n\r\n throw Error\r\n (bignumberError + 'Invalid BigNumber: ' + v);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the maximum of the arguments.\r\n *\r\n * arguments {number|string|BigNumber}\r\n */\r\n BigNumber.maximum = BigNumber.max = function () {\r\n return maxOrMin(arguments, -1);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the minimum of the arguments.\r\n *\r\n * arguments {number|string|BigNumber}\r\n */\r\n BigNumber.minimum = BigNumber.min = function () {\r\n return maxOrMin(arguments, 1);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber with a random value equal to or greater than 0 and less than 1,\r\n * and with dp, or DECIMAL_PLACES if dp is omitted, decimal places (or less if trailing\r\n * zeros are produced).\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp}'\r\n * '[BigNumber Error] crypto unavailable'\r\n */\r\n BigNumber.random = (function () {\r\n var pow2_53 = 0x20000000000000;\r\n\r\n // Return a 53 bit integer n, where 0 <= n < 9007199254740992.\r\n // Check if Math.random() produces more than 32 bits of randomness.\r\n // If it does, assume at least 53 bits are produced, otherwise assume at least 30 bits.\r\n // 0x40000000 is 2^30, 0x800000 is 2^23, 0x1fffff is 2^21 - 1.\r\n var random53bitInt = (Math.random() * pow2_53) & 0x1fffff\r\n ? function () { return mathfloor(Math.random() * pow2_53); }\r\n : function () { return ((Math.random() * 0x40000000 | 0) * 0x800000) +\r\n (Math.random() * 0x800000 | 0); };\r\n\r\n return function (dp) {\r\n var a, b, e, k, v,\r\n i = 0,\r\n c = [],\r\n rand = new BigNumber(ONE);\r\n\r\n if (dp == null) dp = DECIMAL_PLACES;\r\n else intCheck(dp, 0, MAX);\r\n\r\n k = mathceil(dp / LOG_BASE);\r\n\r\n if (CRYPTO) {\r\n\r\n // Browsers supporting crypto.getRandomValues.\r\n if (crypto.getRandomValues) {\r\n\r\n a = crypto.getRandomValues(new Uint32Array(k *= 2));\r\n\r\n for (; i < k;) {\r\n\r\n // 53 bits:\r\n // ((Math.pow(2, 32) - 1) * Math.pow(2, 21)).toString(2)\r\n // 11111 11111111 11111111 11111111 11100000 00000000 00000000\r\n // ((Math.pow(2, 32) - 1) >>> 11).toString(2)\r\n // 11111 11111111 11111111\r\n // 0x20000 is 2^21.\r\n v = a[i] * 0x20000 + (a[i + 1] >>> 11);\r\n\r\n // Rejection sampling:\r\n // 0 <= v < 9007199254740992\r\n // Probability that v >= 9e15, is\r\n // 7199254740992 / 9007199254740992 ~= 0.0008, i.e. 1 in 1251\r\n if (v >= 9e15) {\r\n b = crypto.getRandomValues(new Uint32Array(2));\r\n a[i] = b[0];\r\n a[i + 1] = b[1];\r\n } else {\r\n\r\n // 0 <= v <= 8999999999999999\r\n // 0 <= (v % 1e14) <= 99999999999999\r\n c.push(v % 1e14);\r\n i += 2;\r\n }\r\n }\r\n i = k / 2;\r\n\r\n // Node.js supporting crypto.randomBytes.\r\n } else if (crypto.randomBytes) {\r\n\r\n // buffer\r\n a = crypto.randomBytes(k *= 7);\r\n\r\n for (; i < k;) {\r\n\r\n // 0x1000000000000 is 2^48, 0x10000000000 is 2^40\r\n // 0x100000000 is 2^32, 0x1000000 is 2^24\r\n // 11111 11111111 11111111 11111111 11111111 11111111 11111111\r\n // 0 <= v < 9007199254740992\r\n v = ((a[i] & 31) * 0x1000000000000) + (a[i + 1] * 0x10000000000) +\r\n (a[i + 2] * 0x100000000) + (a[i + 3] * 0x1000000) +\r\n (a[i + 4] << 16) + (a[i + 5] << 8) + a[i + 6];\r\n\r\n if (v >= 9e15) {\r\n crypto.randomBytes(7).copy(a, i);\r\n } else {\r\n\r\n // 0 <= (v % 1e14) <= 99999999999999\r\n c.push(v % 1e14);\r\n i += 7;\r\n }\r\n }\r\n i = k / 7;\r\n } else {\r\n CRYPTO = false;\r\n throw Error\r\n (bignumberError + 'crypto unavailable');\r\n }\r\n }\r\n\r\n // Use Math.random.\r\n if (!CRYPTO) {\r\n\r\n for (; i < k;) {\r\n v = random53bitInt();\r\n if (v < 9e15) c[i++] = v % 1e14;\r\n }\r\n }\r\n\r\n k = c[--i];\r\n dp %= LOG_BASE;\r\n\r\n // Convert trailing digits to zeros according to dp.\r\n if (k && dp) {\r\n v = POWS_TEN[LOG_BASE - dp];\r\n c[i] = mathfloor(k / v) * v;\r\n }\r\n\r\n // Remove trailing elements which are zero.\r\n for (; c[i] === 0; c.pop(), i--);\r\n\r\n // Zero?\r\n if (i < 0) {\r\n c = [e = 0];\r\n } else {\r\n\r\n // Remove leading elements which are zero and adjust exponent accordingly.\r\n for (e = -1 ; c[0] === 0; c.splice(0, 1), e -= LOG_BASE);\r\n\r\n // Count the digits of the first element of c to determine leading zeros, and...\r\n for (i = 1, v = c[0]; v >= 10; v /= 10, i++);\r\n\r\n // adjust the exponent accordingly.\r\n if (i < LOG_BASE) e -= LOG_BASE - i;\r\n }\r\n\r\n rand.e = e;\r\n rand.c = c;\r\n return rand;\r\n };\r\n })();\r\n\r\n\r\n /*\r\n * Return a BigNumber whose value is the sum of the arguments.\r\n *\r\n * arguments {number|string|BigNumber}\r\n */\r\n BigNumber.sum = function () {\r\n var i = 1,\r\n args = arguments,\r\n sum = new BigNumber(args[0]);\r\n for (; i < args.length;) sum = sum.plus(args[i++]);\r\n return sum;\r\n };\r\n\r\n\r\n // PRIVATE FUNCTIONS\r\n\r\n\r\n // Called by BigNumber and BigNumber.prototype.toString.\r\n convertBase = (function () {\r\n var decimal = '0123456789';\r\n\r\n /*\r\n * Convert string of baseIn to an array of numbers of baseOut.\r\n * Eg. toBaseOut('255', 10, 16) returns [15, 15].\r\n * Eg. toBaseOut('ff', 16, 10) returns [2, 5, 5].\r\n */\r\n function toBaseOut(str, baseIn, baseOut, alphabet) {\r\n var j,\r\n arr = [0],\r\n arrL,\r\n i = 0,\r\n len = str.length;\r\n\r\n for (; i < len;) {\r\n for (arrL = arr.length; arrL--; arr[arrL] *= baseIn);\r\n\r\n arr[0] += alphabet.indexOf(str.charAt(i++));\r\n\r\n for (j = 0; j < arr.length; j++) {\r\n\r\n if (arr[j] > baseOut - 1) {\r\n if (arr[j + 1] == null) arr[j + 1] = 0;\r\n arr[j + 1] += arr[j] / baseOut | 0;\r\n arr[j] %= baseOut;\r\n }\r\n }\r\n }\r\n\r\n return arr.reverse();\r\n }\r\n\r\n // Convert a numeric string of baseIn to a numeric string of baseOut.\r\n // If the caller is toString, we are converting from base 10 to baseOut.\r\n // If the caller is BigNumber, we are converting from baseIn to base 10.\r\n return function (str, baseIn, baseOut, sign, callerIsToString) {\r\n var alphabet, d, e, k, r, x, xc, y,\r\n i = str.indexOf('.'),\r\n dp = DECIMAL_PLACES,\r\n rm = ROUNDING_MODE;\r\n\r\n // Non-integer.\r\n if (i >= 0) {\r\n k = POW_PRECISION;\r\n\r\n // Unlimited precision.\r\n POW_PRECISION = 0;\r\n str = str.replace('.', '');\r\n y = new BigNumber(baseIn);\r\n x = y.pow(str.length - i);\r\n POW_PRECISION = k;\r\n\r\n // Convert str as if an integer, then restore the fraction part by dividing the\r\n // result by its base raised to a power.\r\n\r\n y.c = toBaseOut(toFixedPoint(coeffToString(x.c), x.e, '0'),\r\n 10, baseOut, decimal);\r\n y.e = y.c.length;\r\n }\r\n\r\n // Convert the number as integer.\r\n\r\n xc = toBaseOut(str, baseIn, baseOut, callerIsToString\r\n ? (alphabet = ALPHABET, decimal)\r\n : (alphabet = decimal, ALPHABET));\r\n\r\n // xc now represents str as an integer and converted to baseOut. e is the exponent.\r\n e = k = xc.length;\r\n\r\n // Remove trailing zeros.\r\n for (; xc[--k] == 0; xc.pop());\r\n\r\n // Zero?\r\n if (!xc[0]) return alphabet.charAt(0);\r\n\r\n // Does str represent an integer? If so, no need for the division.\r\n if (i < 0) {\r\n --e;\r\n } else {\r\n x.c = xc;\r\n x.e = e;\r\n\r\n // The sign is needed for correct rounding.\r\n x.s = sign;\r\n x = div(x, y, dp, rm, baseOut);\r\n xc = x.c;\r\n r = x.r;\r\n e = x.e;\r\n }\r\n\r\n // xc now represents str converted to baseOut.\r\n\r\n // The index of the rounding digit.\r\n d = e + dp + 1;\r\n\r\n // The rounding digit: the digit to the right of the digit that may be rounded up.\r\n i = xc[d];\r\n\r\n // Look at the rounding digits and mode to determine whether to round up.\r\n\r\n k = baseOut / 2;\r\n r = r || d < 0 || xc[d + 1] != null;\r\n\r\n r = rm < 4 ? (i != null || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2))\r\n : i > k || i == k &&(rm == 4 || r || rm == 6 && xc[d - 1] & 1 ||\r\n rm == (x.s < 0 ? 8 : 7));\r\n\r\n // If the index of the rounding digit is not greater than zero, or xc represents\r\n // zero, then the result of the base conversion is zero or, if rounding up, a value\r\n // such as 0.00001.\r\n if (d < 1 || !xc[0]) {\r\n\r\n // 1^-dp or 0\r\n str = r ? toFixedPoint(alphabet.charAt(1), -dp, alphabet.charAt(0)) : alphabet.charAt(0);\r\n } else {\r\n\r\n // Truncate xc to the required number of decimal places.\r\n xc.length = d;\r\n\r\n // Round up?\r\n if (r) {\r\n\r\n // Rounding up may mean the previous digit has to be rounded up and so on.\r\n for (--baseOut; ++xc[--d] > baseOut;) {\r\n xc[d] = 0;\r\n\r\n if (!d) {\r\n ++e;\r\n xc = [1].concat(xc);\r\n }\r\n }\r\n }\r\n\r\n // Determine trailing zeros.\r\n for (k = xc.length; !xc[--k];);\r\n\r\n // E.g. [4, 11, 15] becomes 4bf.\r\n for (i = 0, str = ''; i <= k; str += alphabet.charAt(xc[i++]));\r\n\r\n // Add leading zeros, decimal point and trailing zeros as required.\r\n str = toFixedPoint(str, e, alphabet.charAt(0));\r\n }\r\n\r\n // The caller will add the sign.\r\n return str;\r\n };\r\n })();\r\n\r\n\r\n // Perform division in the specified base. Called by div and convertBase.\r\n div = (function () {\r\n\r\n // Assume non-zero x and k.\r\n function multiply(x, k, base) {\r\n var m, temp, xlo, xhi,\r\n carry = 0,\r\n i = x.length,\r\n klo = k % SQRT_BASE,\r\n khi = k / SQRT_BASE | 0;\r\n\r\n for (x = x.slice(); i--;) {\r\n xlo = x[i] % SQRT_BASE;\r\n xhi = x[i] / SQRT_BASE | 0;\r\n m = khi * xlo + xhi * klo;\r\n temp = klo * xlo + ((m % SQRT_BASE) * SQRT_BASE) + carry;\r\n carry = (temp / base | 0) + (m / SQRT_BASE | 0) + khi * xhi;\r\n x[i] = temp % base;\r\n }\r\n\r\n if (carry) x = [carry].concat(x);\r\n\r\n return x;\r\n }\r\n\r\n function compare(a, b, aL, bL) {\r\n var i, cmp;\r\n\r\n if (aL != bL) {\r\n cmp = aL > bL ? 1 : -1;\r\n } else {\r\n\r\n for (i = cmp = 0; i < aL; i++) {\r\n\r\n if (a[i] != b[i]) {\r\n cmp = a[i] > b[i] ? 1 : -1;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n return cmp;\r\n }\r\n\r\n function subtract(a, b, aL, base) {\r\n var i = 0;\r\n\r\n // Subtract b from a.\r\n for (; aL--;) {\r\n a[aL] -= i;\r\n i = a[aL] < b[aL] ? 1 : 0;\r\n a[aL] = i * base + a[aL] - b[aL];\r\n }\r\n\r\n // Remove leading zeros.\r\n for (; !a[0] && a.length > 1; a.splice(0, 1));\r\n }\r\n\r\n // x: dividend, y: divisor.\r\n return function (x, y, dp, rm, base) {\r\n var cmp, e, i, more, n, prod, prodL, q, qc, rem, remL, rem0, xi, xL, yc0,\r\n yL, yz,\r\n s = x.s == y.s ? 1 : -1,\r\n xc = x.c,\r\n yc = y.c;\r\n\r\n // Either NaN, Infinity or 0?\r\n if (!xc || !xc[0] || !yc || !yc[0]) {\r\n\r\n return new BigNumber(\r\n\r\n // Return NaN if either NaN, or both Infinity or 0.\r\n !x.s || !y.s || (xc ? yc && xc[0] == yc[0] : !yc) ? NaN :\r\n\r\n // Return \u00B10 if x is \u00B10 or y is \u00B1Infinity, or return \u00B1Infinity as y is \u00B10.\r\n xc && xc[0] == 0 || !yc ? s * 0 : s / 0\r\n );\r\n }\r\n\r\n q = new BigNumber(s);\r\n qc = q.c = [];\r\n e = x.e - y.e;\r\n s = dp + e + 1;\r\n\r\n if (!base) {\r\n base = BASE;\r\n e = bitFloor(x.e / LOG_BASE) - bitFloor(y.e / LOG_BASE);\r\n s = s / LOG_BASE | 0;\r\n }\r\n\r\n // Result exponent may be one less then the current value of e.\r\n // The coefficients of the BigNumbers from convertBase may have trailing zeros.\r\n for (i = 0; yc[i] == (xc[i] || 0); i++);\r\n\r\n if (yc[i] > (xc[i] || 0)) e--;\r\n\r\n if (s < 0) {\r\n qc.push(1);\r\n more = true;\r\n } else {\r\n xL = xc.length;\r\n yL = yc.length;\r\n i = 0;\r\n s += 2;\r\n\r\n // Normalise xc and yc so highest order digit of yc is >= base / 2.\r\n\r\n n = mathfloor(base / (yc[0] + 1));\r\n\r\n // Not necessary, but to handle odd bases where yc[0] == (base / 2) - 1.\r\n // if (n > 1 || n++ == 1 && yc[0] < base / 2) {\r\n if (n > 1) {\r\n yc = multiply(yc, n, base);\r\n xc = multiply(xc, n, base);\r\n yL = yc.length;\r\n xL = xc.length;\r\n }\r\n\r\n xi = yL;\r\n rem = xc.slice(0, yL);\r\n remL = rem.length;\r\n\r\n // Add zeros to make remainder as long as divisor.\r\n for (; remL < yL; rem[remL++] = 0);\r\n yz = yc.slice();\r\n yz = [0].concat(yz);\r\n yc0 = yc[0];\r\n if (yc[1] >= base / 2) yc0++;\r\n // Not necessary, but to prevent trial digit n > base, when using base 3.\r\n // else if (base == 3 && yc0 == 1) yc0 = 1 + 1e-15;\r\n\r\n do {\r\n n = 0;\r\n\r\n // Compare divisor and remainder.\r\n cmp = compare(yc, rem, yL, remL);\r\n\r\n // If divisor < remainder.\r\n if (cmp < 0) {\r\n\r\n // Calculate trial digit, n.\r\n\r\n rem0 = rem[0];\r\n if (yL != remL) rem0 = rem0 * base + (rem[1] || 0);\r\n\r\n // n is how many times the divisor goes into the current remainder.\r\n n = mathfloor(rem0 / yc0);\r\n\r\n // Algorithm:\r\n // product = divisor multiplied by trial digit (n).\r\n // Compare product and remainder.\r\n // If product is greater than remainder:\r\n // Subtract divisor from product, decrement trial digit.\r\n // Subtract product from remainder.\r\n // If product was less than remainder at the last compare:\r\n // Compare new remainder and divisor.\r\n // If remainder is greater than divisor:\r\n // Subtract divisor from remainder, increment trial digit.\r\n\r\n if (n > 1) {\r\n\r\n // n may be > base only when base is 3.\r\n if (n >= base) n = base - 1;\r\n\r\n // product = divisor * trial digit.\r\n prod = multiply(yc, n, base);\r\n prodL = prod.length;\r\n remL = rem.length;\r\n\r\n // Compare product and remainder.\r\n // If product > remainder then trial digit n too high.\r\n // n is 1 too high about 5% of the time, and is not known to have\r\n // ever been more than 1 too high.\r\n while (compare(prod, rem, prodL, remL) == 1) {\r\n n--;\r\n\r\n // Subtract divisor from product.\r\n subtract(prod, yL < prodL ? yz : yc, prodL, base);\r\n prodL = prod.length;\r\n cmp = 1;\r\n }\r\n } else {\r\n\r\n // n is 0 or 1, cmp is -1.\r\n // If n is 0, there is no need to compare yc and rem again below,\r\n // so change cmp to 1 to avoid it.\r\n // If n is 1, leave cmp as -1, so yc and rem are compared again.\r\n if (n == 0) {\r\n\r\n // divisor < remainder, so n must be at least 1.\r\n cmp = n = 1;\r\n }\r\n\r\n // product = divisor\r\n prod = yc.slice();\r\n prodL = prod.length;\r\n }\r\n\r\n if (prodL < remL) prod = [0].concat(prod);\r\n\r\n // Subtract product from remainder.\r\n subtract(rem, prod, remL, base);\r\n remL = rem.length;\r\n\r\n // If product was < remainder.\r\n if (cmp == -1) {\r\n\r\n // Compare divisor and new remainder.\r\n // If divisor < new remainder, subtract divisor from remainder.\r\n // Trial digit n too low.\r\n // n is 1 too low about 5% of the time, and very rarely 2 too low.\r\n while (compare(yc, rem, yL, remL) < 1) {\r\n n++;\r\n\r\n // Subtract divisor from remainder.\r\n subtract(rem, yL < remL ? yz : yc, remL, base);\r\n remL = rem.length;\r\n }\r\n }\r\n } else if (cmp === 0) {\r\n n++;\r\n rem = [0];\r\n } // else cmp === 1 and n will be 0\r\n\r\n // Add the next digit, n, to the result array.\r\n qc[i++] = n;\r\n\r\n // Update the remainder.\r\n if (rem[0]) {\r\n rem[remL++] = xc[xi] || 0;\r\n } else {\r\n rem = [xc[xi]];\r\n remL = 1;\r\n }\r\n } while ((xi++ < xL || rem[0] != null) && s--);\r\n\r\n more = rem[0] != null;\r\n\r\n // Leading zero?\r\n if (!qc[0]) qc.splice(0, 1);\r\n }\r\n\r\n if (base == BASE) {\r\n\r\n // To calculate q.e, first get the number of digits of qc[0].\r\n for (i = 1, s = qc[0]; s >= 10; s /= 10, i++);\r\n\r\n round(q, dp + (q.e = i + e * LOG_BASE - 1) + 1, rm, more);\r\n\r\n // Caller is convertBase.\r\n } else {\r\n q.e = e;\r\n q.r = +more;\r\n }\r\n\r\n return q;\r\n };\r\n })();\r\n\r\n\r\n /*\r\n * Return a string representing the value of BigNumber n in fixed-point or exponential\r\n * notation rounded to the specified decimal places or significant digits.\r\n *\r\n * n: a BigNumber.\r\n * i: the index of the last digit required (i.e. the digit that may be rounded up).\r\n * rm: the rounding mode.\r\n * id: 1 (toExponential) or 2 (toPrecision).\r\n */\r\n function format(n, i, rm, id) {\r\n var c0, e, ne, len, str;\r\n\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n\r\n if (!n.c) return n.toString();\r\n\r\n c0 = n.c[0];\r\n ne = n.e;\r\n\r\n if (i == null) {\r\n str = coeffToString(n.c);\r\n str = id == 1 || id == 2 && (ne <= TO_EXP_NEG || ne >= TO_EXP_POS)\r\n ? toExponential(str, ne)\r\n : toFixedPoint(str, ne, '0');\r\n } else {\r\n n = round(new BigNumber(n), i, rm);\r\n\r\n // n.e may have changed if the value was rounded up.\r\n e = n.e;\r\n\r\n str = coeffToString(n.c);\r\n len = str.length;\r\n\r\n // toPrecision returns exponential notation if the number of significant digits\r\n // specified is less than the number of digits necessary to represent the integer\r\n // part of the value in fixed-point notation.\r\n\r\n // Exponential notation.\r\n if (id == 1 || id == 2 && (i <= e || e <= TO_EXP_NEG)) {\r\n\r\n // Append zeros?\r\n for (; len < i; str += '0', len++);\r\n str = toExponential(str, e);\r\n\r\n // Fixed-point notation.\r\n } else {\r\n i -= ne + (id === 2 && e > ne);\r\n str = toFixedPoint(str, e, '0');\r\n\r\n // Append zeros?\r\n if (e + 1 > len) {\r\n if (--i > 0) for (str += '.'; i--; str += '0');\r\n } else {\r\n i += e - len;\r\n if (i > 0) {\r\n if (e + 1 == len) str += '.';\r\n for (; i--; str += '0');\r\n }\r\n }\r\n }\r\n }\r\n\r\n return n.s < 0 && c0 ? '-' + str : str;\r\n }\r\n\r\n\r\n // Handle BigNumber.max and BigNumber.min.\r\n // If any number is NaN, return NaN.\r\n function maxOrMin(args, n) {\r\n var k, y,\r\n i = 1,\r\n x = new BigNumber(args[0]);\r\n\r\n for (; i < args.length; i++) {\r\n y = new BigNumber(args[i]);\r\n if (!y.s || (k = compare(x, y)) === n || k === 0 && x.s === n) {\r\n x = y;\r\n }\r\n }\r\n\r\n return x;\r\n }\r\n\r\n\r\n /*\r\n * Strip trailing zeros, calculate base 10 exponent and check against MIN_EXP and MAX_EXP.\r\n * Called by minus, plus and times.\r\n */\r\n function normalise(n, c, e) {\r\n var i = 1,\r\n j = c.length;\r\n\r\n // Remove trailing zeros.\r\n for (; !c[--j]; c.pop());\r\n\r\n // Calculate the base 10 exponent. First get the number of digits of c[0].\r\n for (j = c[0]; j >= 10; j /= 10, i++);\r\n\r\n // Overflow?\r\n if ((e = i + e * LOG_BASE - 1) > MAX_EXP) {\r\n\r\n // Infinity.\r\n n.c = n.e = null;\r\n\r\n // Underflow?\r\n } else if (e < MIN_EXP) {\r\n\r\n // Zero.\r\n n.c = [n.e = 0];\r\n } else {\r\n n.e = e;\r\n n.c = c;\r\n }\r\n\r\n return n;\r\n }\r\n\r\n\r\n // Handle values that fail the validity test in BigNumber.\r\n parseNumeric = (function () {\r\n var basePrefix = /^(-?)0([xbo])(?=\\w[\\w.]*$)/i,\r\n dotAfter = /^([^.]+)\\.$/,\r\n dotBefore = /^\\.([^.]+)$/,\r\n isInfinityOrNaN = /^-?(Infinity|NaN)$/,\r\n whitespaceOrPlus = /^\\s*\\+(?=[\\w.])|^\\s+|\\s+$/g;\r\n\r\n return function (x, str, isNum, b) {\r\n var base,\r\n s = isNum ? str : str.replace(whitespaceOrPlus, '');\r\n\r\n // No exception on \u00B1Infinity or NaN.\r\n if (isInfinityOrNaN.test(s)) {\r\n x.s = isNaN(s) ? null : s < 0 ? -1 : 1;\r\n } else {\r\n if (!isNum) {\r\n\r\n // basePrefix = /^(-?)0([xbo])(?=\\w[\\w.]*$)/i\r\n s = s.replace(basePrefix, function (m, p1, p2) {\r\n base = (p2 = p2.toLowerCase()) == 'x' ? 16 : p2 == 'b' ? 2 : 8;\r\n return !b || b == base ? p1 : m;\r\n });\r\n\r\n if (b) {\r\n base = b;\r\n\r\n // E.g. '1.' to '1', '.1' to '0.1'\r\n s = s.replace(dotAfter, '$1').replace(dotBefore, '0.$1');\r\n }\r\n\r\n if (str != s) return new BigNumber(s, base);\r\n }\r\n\r\n // '[BigNumber Error] Not a number: {n}'\r\n // '[BigNumber Error] Not a base {b} number: {n}'\r\n if (BigNumber.DEBUG) {\r\n throw Error\r\n (bignumberError + 'Not a' + (b ? ' base ' + b : '') + ' number: ' + str);\r\n }\r\n\r\n // NaN\r\n x.s = null;\r\n }\r\n\r\n x.c = x.e = null;\r\n }\r\n })();\r\n\r\n\r\n /*\r\n * Round x to sd significant digits using rounding mode rm. Check for over/under-flow.\r\n * If r is truthy, it is known that there are more digits after the rounding digit.\r\n */\r\n function round(x, sd, rm, r) {\r\n var d, i, j, k, n, ni, rd,\r\n xc = x.c,\r\n pows10 = POWS_TEN;\r\n\r\n // if x is not Infinity or NaN...\r\n if (xc) {\r\n\r\n // rd is the rounding digit, i.e. the digit after the digit that may be rounded up.\r\n // n is a base 1e14 number, the value of the element of array x.c containing rd.\r\n // ni is the index of n within x.c.\r\n // d is the number of digits of n.\r\n // i is the index of rd within n including leading zeros.\r\n // j is the actual index of rd within n (if < 0, rd is a leading zero).\r\n out: {\r\n\r\n // Get the number of digits of the first element of xc.\r\n for (d = 1, k = xc[0]; k >= 10; k /= 10, d++);\r\n i = sd - d;\r\n\r\n // If the rounding digit is in the first element of xc...\r\n if (i < 0) {\r\n i += LOG_BASE;\r\n j = sd;\r\n n = xc[ni = 0];\r\n\r\n // Get the rounding digit at index j of n.\r\n rd = mathfloor(n / pows10[d - j - 1] % 10);\r\n } else {\r\n ni = mathceil((i + 1) / LOG_BASE);\r\n\r\n if (ni >= xc.length) {\r\n\r\n if (r) {\r\n\r\n // Needed by sqrt.\r\n for (; xc.length <= ni; xc.push(0));\r\n n = rd = 0;\r\n d = 1;\r\n i %= LOG_BASE;\r\n j = i - LOG_BASE + 1;\r\n } else {\r\n break out;\r\n }\r\n } else {\r\n n = k = xc[ni];\r\n\r\n // Get the number of digits of n.\r\n for (d = 1; k >= 10; k /= 10, d++);\r\n\r\n // Get the index of rd within n.\r\n i %= LOG_BASE;\r\n\r\n // Get the index of rd within n, adjusted for leading zeros.\r\n // The number of leading zeros of n is given by LOG_BASE - d.\r\n j = i - LOG_BASE + d;\r\n\r\n // Get the rounding digit at index j of n.\r\n rd = j < 0 ? 0 : mathfloor(n / pows10[d - j - 1] % 10);\r\n }\r\n }\r\n\r\n r = r || sd < 0 ||\r\n\r\n // Are there any non-zero digits after the rounding digit?\r\n // The expression n % pows10[d - j - 1] returns all digits of n to the right\r\n // of the digit at j, e.g. if n is 908714 and j is 2, the expression gives 714.\r\n xc[ni + 1] != null || (j < 0 ? n : n % pows10[d - j - 1]);\r\n\r\n r = rm < 4\r\n ? (rd || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2))\r\n : rd > 5 || rd == 5 && (rm == 4 || r || rm == 6 &&\r\n\r\n // Check whether the digit to the left of the rounding digit is odd.\r\n ((i > 0 ? j > 0 ? n / pows10[d - j] : 0 : xc[ni - 1]) % 10) & 1 ||\r\n rm == (x.s < 0 ? 8 : 7));\r\n\r\n if (sd < 1 || !xc[0]) {\r\n xc.length = 0;\r\n\r\n if (r) {\r\n\r\n // Convert sd to decimal places.\r\n sd -= x.e + 1;\r\n\r\n // 1, 0.1, 0.01, 0.001, 0.0001 etc.\r\n xc[0] = pows10[(LOG_BASE - sd % LOG_BASE) % LOG_BASE];\r\n x.e = -sd || 0;\r\n } else {\r\n\r\n // Zero.\r\n xc[0] = x.e = 0;\r\n }\r\n\r\n return x;\r\n }\r\n\r\n // Remove excess digits.\r\n if (i == 0) {\r\n xc.length = ni;\r\n k = 1;\r\n ni--;\r\n } else {\r\n xc.length = ni + 1;\r\n k = pows10[LOG_BASE - i];\r\n\r\n // E.g. 56700 becomes 56000 if 7 is the rounding digit.\r\n // j > 0 means i > number of leading zeros of n.\r\n xc[ni] = j > 0 ? mathfloor(n / pows10[d - j] % pows10[j]) * k : 0;\r\n }\r\n\r\n // Round up?\r\n if (r) {\r\n\r\n for (; ;) {\r\n\r\n // If the digit to be rounded up is in the first element of xc...\r\n if (ni == 0) {\r\n\r\n // i will be the length of xc[0] before k is added.\r\n for (i = 1, j = xc[0]; j >= 10; j /= 10, i++);\r\n j = xc[0] += k;\r\n for (k = 1; j >= 10; j /= 10, k++);\r\n\r\n // if i != k the length has increased.\r\n if (i != k) {\r\n x.e++;\r\n if (xc[0] == BASE) xc[0] = 1;\r\n }\r\n\r\n break;\r\n } else {\r\n xc[ni] += k;\r\n if (xc[ni] != BASE) break;\r\n xc[ni--] = 0;\r\n k = 1;\r\n }\r\n }\r\n }\r\n\r\n // Remove trailing zeros.\r\n for (i = xc.length; xc[--i] === 0; xc.pop());\r\n }\r\n\r\n // Overflow? Infinity.\r\n if (x.e > MAX_EXP) {\r\n x.c = x.e = null;\r\n\r\n // Underflow? Zero.\r\n } else if (x.e < MIN_EXP) {\r\n x.c = [x.e = 0];\r\n }\r\n }\r\n\r\n return x;\r\n }\r\n\r\n\r\n function valueOf(n) {\r\n var str,\r\n e = n.e;\r\n\r\n if (e === null) return n.toString();\r\n\r\n str = coeffToString(n.c);\r\n\r\n str = e <= TO_EXP_NEG || e >= TO_EXP_POS\r\n ? toExponential(str, e)\r\n : toFixedPoint(str, e, '0');\r\n\r\n return n.s < 0 ? '-' + str : str;\r\n }\r\n\r\n\r\n // PROTOTYPE/INSTANCE METHODS\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the absolute value of this BigNumber.\r\n */\r\n P.absoluteValue = P.abs = function () {\r\n var x = new BigNumber(this);\r\n if (x.s < 0) x.s = 1;\r\n return x;\r\n };\r\n\r\n\r\n /*\r\n * Return\r\n * 1 if the value of this BigNumber is greater than the value of BigNumber(y, b),\r\n * -1 if the value of this BigNumber is less than the value of BigNumber(y, b),\r\n * 0 if they have the same value,\r\n * or null if the value of either is NaN.\r\n */\r\n P.comparedTo = function (y, b) {\r\n return compare(this, new BigNumber(y, b));\r\n };\r\n\r\n\r\n /*\r\n * If dp is undefined or null or true or false, return the number of decimal places of the\r\n * value of this BigNumber, or null if the value of this BigNumber is \u00B1Infinity or NaN.\r\n *\r\n * Otherwise, if dp is a number, return a new BigNumber whose value is the value of this\r\n * BigNumber rounded to a maximum of dp decimal places using rounding mode rm, or\r\n * ROUNDING_MODE if rm is omitted.\r\n *\r\n * [dp] {number} Decimal places: integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n */\r\n P.decimalPlaces = P.dp = function (dp, rm) {\r\n var c, n, v,\r\n x = this;\r\n\r\n if (dp != null) {\r\n intCheck(dp, 0, MAX);\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n\r\n return round(new BigNumber(x), dp + x.e + 1, rm);\r\n }\r\n\r\n if (!(c = x.c)) return null;\r\n n = ((v = c.length - 1) - bitFloor(this.e / LOG_BASE)) * LOG_BASE;\r\n\r\n // Subtract the number of trailing zeros of the last number.\r\n if (v = c[v]) for (; v % 10 == 0; v /= 10, n--);\r\n if (n < 0) n = 0;\r\n\r\n return n;\r\n };\r\n\r\n\r\n /*\r\n * n / 0 = I\r\n * n / N = N\r\n * n / I = 0\r\n * 0 / n = 0\r\n * 0 / 0 = N\r\n * 0 / N = N\r\n * 0 / I = 0\r\n * N / n = N\r\n * N / 0 = N\r\n * N / N = N\r\n * N / I = N\r\n * I / n = I\r\n * I / 0 = I\r\n * I / N = N\r\n * I / I = N\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber divided by the value of\r\n * BigNumber(y, b), rounded according to DECIMAL_PLACES and ROUNDING_MODE.\r\n */\r\n P.dividedBy = P.div = function (y, b) {\r\n return div(this, new BigNumber(y, b), DECIMAL_PLACES, ROUNDING_MODE);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the integer part of dividing the value of this\r\n * BigNumber by the value of BigNumber(y, b).\r\n */\r\n P.dividedToIntegerBy = P.idiv = function (y, b) {\r\n return div(this, new BigNumber(y, b), 0, 1);\r\n };\r\n\r\n\r\n /*\r\n * Return a BigNumber whose value is the value of this BigNumber exponentiated by n.\r\n *\r\n * If m is present, return the result modulo m.\r\n * If n is negative round according to DECIMAL_PLACES and ROUNDING_MODE.\r\n * If POW_PRECISION is non-zero and m is not present, round to POW_PRECISION using ROUNDING_MODE.\r\n *\r\n * The modular power operation works efficiently when x, n, and m are integers, otherwise it\r\n * is equivalent to calculating x.exponentiatedBy(n).modulo(m) with a POW_PRECISION of 0.\r\n *\r\n * n {number|string|BigNumber} The exponent. An integer.\r\n * [m] {number|string|BigNumber} The modulus.\r\n *\r\n * '[BigNumber Error] Exponent not an integer: {n}'\r\n */\r\n P.exponentiatedBy = P.pow = function (n, m) {\r\n var half, isModExp, i, k, more, nIsBig, nIsNeg, nIsOdd, y,\r\n x = this;\r\n\r\n n = new BigNumber(n);\r\n\r\n // Allow NaN and \u00B1Infinity, but not other non-integers.\r\n if (n.c && !n.isInteger()) {\r\n throw Error\r\n (bignumberError + 'Exponent not an integer: ' + valueOf(n));\r\n }\r\n\r\n if (m != null) m = new BigNumber(m);\r\n\r\n // Exponent of MAX_SAFE_INTEGER is 15.\r\n nIsBig = n.e > 14;\r\n\r\n // If x is NaN, \u00B1Infinity, \u00B10 or \u00B11, or n is \u00B1Infinity, NaN or \u00B10.\r\n if (!x.c || !x.c[0] || x.c[0] == 1 && !x.e && x.c.length == 1 || !n.c || !n.c[0]) {\r\n\r\n // The sign of the result of pow when x is negative depends on the evenness of n.\r\n // If +n overflows to \u00B1Infinity, the evenness of n would be not be known.\r\n y = new BigNumber(Math.pow(+valueOf(x), nIsBig ? n.s * (2 - isOdd(n)) : +valueOf(n)));\r\n return m ? y.mod(m) : y;\r\n }\r\n\r\n nIsNeg = n.s < 0;\r\n\r\n if (m) {\r\n\r\n // x % m returns NaN if abs(m) is zero, or m is NaN.\r\n if (m.c ? !m.c[0] : !m.s) return new BigNumber(NaN);\r\n\r\n isModExp = !nIsNeg && x.isInteger() && m.isInteger();\r\n\r\n if (isModExp) x = x.mod(m);\r\n\r\n // Overflow to \u00B1Infinity: >=2**1e10 or >=1.0000024**1e15.\r\n // Underflow to \u00B10: <=0.79**1e10 or <=0.9999975**1e15.\r\n } else if (n.e > 9 && (x.e > 0 || x.e < -1 || (x.e == 0\r\n // [1, 240000000]\r\n ? x.c[0] > 1 || nIsBig && x.c[1] >= 24e7\r\n // [80000000000000] [99999750000000]\r\n : x.c[0] < 8e13 || nIsBig && x.c[0] <= 9999975e7))) {\r\n\r\n // If x is negative and n is odd, k = -0, else k = 0.\r\n k = x.s < 0 && isOdd(n) ? -0 : 0;\r\n\r\n // If x >= 1, k = \u00B1Infinity.\r\n if (x.e > -1) k = 1 / k;\r\n\r\n // If n is negative return \u00B10, else return \u00B1Infinity.\r\n return new BigNumber(nIsNeg ? 1 / k : k);\r\n\r\n } else if (POW_PRECISION) {\r\n\r\n // Truncating each coefficient array to a length of k after each multiplication\r\n // equates to truncating significant digits to POW_PRECISION + [28, 41],\r\n // i.e. there will be a minimum of 28 guard digits retained.\r\n k = mathceil(POW_PRECISION / LOG_BASE + 2);\r\n }\r\n\r\n if (nIsBig) {\r\n half = new BigNumber(0.5);\r\n if (nIsNeg) n.s = 1;\r\n nIsOdd = isOdd(n);\r\n } else {\r\n i = Math.abs(+valueOf(n));\r\n nIsOdd = i % 2;\r\n }\r\n\r\n y = new BigNumber(ONE);\r\n\r\n // Performs 54 loop iterations for n of 9007199254740991.\r\n for (; ;) {\r\n\r\n if (nIsOdd) {\r\n y = y.times(x);\r\n if (!y.c) break;\r\n\r\n if (k) {\r\n if (y.c.length > k) y.c.length = k;\r\n } else if (isModExp) {\r\n y = y.mod(m); //y = y.minus(div(y, m, 0, MODULO_MODE).times(m));\r\n }\r\n }\r\n\r\n if (i) {\r\n i = mathfloor(i / 2);\r\n if (i === 0) break;\r\n nIsOdd = i % 2;\r\n } else {\r\n n = n.times(half);\r\n round(n, n.e + 1, 1);\r\n\r\n if (n.e > 14) {\r\n nIsOdd = isOdd(n);\r\n } else {\r\n i = +valueOf(n);\r\n if (i === 0) break;\r\n nIsOdd = i % 2;\r\n }\r\n }\r\n\r\n x = x.times(x);\r\n\r\n if (k) {\r\n if (x.c && x.c.length > k) x.c.length = k;\r\n } else if (isModExp) {\r\n x = x.mod(m); //x = x.minus(div(x, m, 0, MODULO_MODE).times(m));\r\n }\r\n }\r\n\r\n if (isModExp) return y;\r\n if (nIsNeg) y = ONE.div(y);\r\n\r\n return m ? y.mod(m) : k ? round(y, POW_PRECISION, ROUNDING_MODE, more) : y;\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the value of this BigNumber rounded to an integer\r\n * using rounding mode rm, or ROUNDING_MODE if rm is omitted.\r\n *\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {rm}'\r\n */\r\n P.integerValue = function (rm) {\r\n var n = new BigNumber(this);\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n return round(n, n.e + 1, rm);\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is equal to the value of BigNumber(y, b),\r\n * otherwise return false.\r\n */\r\n P.isEqualTo = P.eq = function (y, b) {\r\n return compare(this, new BigNumber(y, b)) === 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is a finite number, otherwise return false.\r\n */\r\n P.isFinite = function () {\r\n return !!this.c;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is greater than the value of BigNumber(y, b),\r\n * otherwise return false.\r\n */\r\n P.isGreaterThan = P.gt = function (y, b) {\r\n return compare(this, new BigNumber(y, b)) > 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is greater than or equal to the value of\r\n * BigNumber(y, b), otherwise return false.\r\n */\r\n P.isGreaterThanOrEqualTo = P.gte = function (y, b) {\r\n return (b = compare(this, new BigNumber(y, b))) === 1 || b === 0;\r\n\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is an integer, otherwise return false.\r\n */\r\n P.isInteger = function () {\r\n return !!this.c && bitFloor(this.e / LOG_BASE) > this.c.length - 2;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is less than the value of BigNumber(y, b),\r\n * otherwise return false.\r\n */\r\n P.isLessThan = P.lt = function (y, b) {\r\n return compare(this, new BigNumber(y, b)) < 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is less than or equal to the value of\r\n * BigNumber(y, b), otherwise return false.\r\n */\r\n P.isLessThanOrEqualTo = P.lte = function (y, b) {\r\n return (b = compare(this, new BigNumber(y, b))) === -1 || b === 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is NaN, otherwise return false.\r\n */\r\n P.isNaN = function () {\r\n return !this.s;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is negative, otherwise return false.\r\n */\r\n P.isNegative = function () {\r\n return this.s < 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is positive, otherwise return false.\r\n */\r\n P.isPositive = function () {\r\n return this.s > 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is 0 or -0, otherwise return false.\r\n */\r\n P.isZero = function () {\r\n return !!this.c && this.c[0] == 0;\r\n };\r\n\r\n\r\n /*\r\n * n - 0 = n\r\n * n - N = N\r\n * n - I = -I\r\n * 0 - n = -n\r\n * 0 - 0 = 0\r\n * 0 - N = N\r\n * 0 - I = -I\r\n * N - n = N\r\n * N - 0 = N\r\n * N - N = N\r\n * N - I = N\r\n * I - n = I\r\n * I - 0 = I\r\n * I - N = N\r\n * I - I = N\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber minus the value of\r\n * BigNumber(y, b).\r\n */\r\n P.minus = function (y, b) {\r\n var i, j, t, xLTy,\r\n x = this,\r\n a = x.s;\r\n\r\n y = new BigNumber(y, b);\r\n b = y.s;\r\n\r\n // Either NaN?\r\n if (!a || !b) return new BigNumber(NaN);\r\n\r\n // Signs differ?\r\n if (a != b) {\r\n y.s = -b;\r\n return x.plus(y);\r\n }\r\n\r\n var xe = x.e / LOG_BASE,\r\n ye = y.e / LOG_BASE,\r\n xc = x.c,\r\n yc = y.c;\r\n\r\n if (!xe || !ye) {\r\n\r\n // Either Infinity?\r\n if (!xc || !yc) return xc ? (y.s = -b, y) : new BigNumber(yc ? x : NaN);\r\n\r\n // Either zero?\r\n if (!xc[0] || !yc[0]) {\r\n\r\n // Return y if y is non-zero, x if x is non-zero, or zero if both are zero.\r\n return yc[0] ? (y.s = -b, y) : new BigNumber(xc[0] ? x :\r\n\r\n // IEEE 754 (2008) 6.3: n - n = -0 when rounding to -Infinity\r\n ROUNDING_MODE == 3 ? -0 : 0);\r\n }\r\n }\r\n\r\n xe = bitFloor(xe);\r\n ye = bitFloor(ye);\r\n xc = xc.slice();\r\n\r\n // Determine which is the bigger number.\r\n if (a = xe - ye) {\r\n\r\n if (xLTy = a < 0) {\r\n a = -a;\r\n t = xc;\r\n } else {\r\n ye = xe;\r\n t = yc;\r\n }\r\n\r\n t.reverse();\r\n\r\n // Prepend zeros to equalise exponents.\r\n for (b = a; b--; t.push(0));\r\n t.reverse();\r\n } else {\r\n\r\n // Exponents equal. Check digit by digit.\r\n j = (xLTy = (a = xc.length) < (b = yc.length)) ? a : b;\r\n\r\n for (a = b = 0; b < j; b++) {\r\n\r\n if (xc[b] != yc[b]) {\r\n xLTy = xc[b] < yc[b];\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // x < y? Point xc to the array of the bigger number.\r\n if (xLTy) {\r\n t = xc;\r\n xc = yc;\r\n yc = t;\r\n y.s = -y.s;\r\n }\r\n\r\n b = (j = yc.length) - (i = xc.length);\r\n\r\n // Append zeros to xc if shorter.\r\n // No need to add zeros to yc if shorter as subtract only needs to start at yc.length.\r\n if (b > 0) for (; b--; xc[i++] = 0);\r\n b = BASE - 1;\r\n\r\n // Subtract yc from xc.\r\n for (; j > a;) {\r\n\r\n if (xc[--j] < yc[j]) {\r\n for (i = j; i && !xc[--i]; xc[i] = b);\r\n --xc[i];\r\n xc[j] += BASE;\r\n }\r\n\r\n xc[j] -= yc[j];\r\n }\r\n\r\n // Remove leading zeros and adjust exponent accordingly.\r\n for (; xc[0] == 0; xc.splice(0, 1), --ye);\r\n\r\n // Zero?\r\n if (!xc[0]) {\r\n\r\n // Following IEEE 754 (2008) 6.3,\r\n // n - n = +0 but n - n = -0 when rounding towards -Infinity.\r\n y.s = ROUNDING_MODE == 3 ? -1 : 1;\r\n y.c = [y.e = 0];\r\n return y;\r\n }\r\n\r\n // No need to check for Infinity as +x - +y != Infinity && -x - -y != Infinity\r\n // for finite x and y.\r\n return normalise(y, xc, ye);\r\n };\r\n\r\n\r\n /*\r\n * n % 0 = N\r\n * n % N = N\r\n * n % I = n\r\n * 0 % n = 0\r\n * -0 % n = -0\r\n * 0 % 0 = N\r\n * 0 % N = N\r\n * 0 % I = 0\r\n * N % n = N\r\n * N % 0 = N\r\n * N % N = N\r\n * N % I = N\r\n * I % n = N\r\n * I % 0 = N\r\n * I % N = N\r\n * I % I = N\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber modulo the value of\r\n * BigNumber(y, b). The result depends on the value of MODULO_MODE.\r\n */\r\n P.modulo = P.mod = function (y, b) {\r\n var q, s,\r\n x = this;\r\n\r\n y = new BigNumber(y, b);\r\n\r\n // Return NaN if x is Infinity or NaN, or y is NaN or zero.\r\n if (!x.c || !y.s || y.c && !y.c[0]) {\r\n return new BigNumber(NaN);\r\n\r\n // Return x if y is Infinity or x is zero.\r\n } else if (!y.c || x.c && !x.c[0]) {\r\n return new BigNumber(x);\r\n }\r\n\r\n if (MODULO_MODE == 9) {\r\n\r\n // Euclidian division: q = sign(y) * floor(x / abs(y))\r\n // r = x - qy where 0 <= r < abs(y)\r\n s = y.s;\r\n y.s = 1;\r\n q = div(x, y, 0, 3);\r\n y.s = s;\r\n q.s *= s;\r\n } else {\r\n q = div(x, y, 0, MODULO_MODE);\r\n }\r\n\r\n y = x.minus(q.times(y));\r\n\r\n // To match JavaScript %, ensure sign of zero is sign of dividend.\r\n if (!y.c[0] && MODULO_MODE == 1) y.s = x.s;\r\n\r\n return y;\r\n };\r\n\r\n\r\n /*\r\n * n * 0 = 0\r\n * n * N = N\r\n * n * I = I\r\n * 0 * n = 0\r\n * 0 * 0 = 0\r\n * 0 * N = N\r\n * 0 * I = N\r\n * N * n = N\r\n * N * 0 = N\r\n * N * N = N\r\n * N * I = N\r\n * I * n = I\r\n * I * 0 = N\r\n * I * N = N\r\n * I * I = I\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber multiplied by the value\r\n * of BigNumber(y, b).\r\n */\r\n P.multipliedBy = P.times = function (y, b) {\r\n var c, e, i, j, k, m, xcL, xlo, xhi, ycL, ylo, yhi, zc,\r\n base, sqrtBase,\r\n x = this,\r\n xc = x.c,\r\n yc = (y = new BigNumber(y, b)).c;\r\n\r\n // Either NaN, \u00B1Infinity or \u00B10?\r\n if (!xc || !yc || !xc[0] || !yc[0]) {\r\n\r\n // Return NaN if either is NaN, or one is 0 and the other is Infinity.\r\n if (!x.s || !y.s || xc && !xc[0] && !yc || yc && !yc[0] && !xc) {\r\n y.c = y.e = y.s = null;\r\n } else {\r\n y.s *= x.s;\r\n\r\n // Return \u00B1Infinity if either is \u00B1Infinity.\r\n if (!xc || !yc) {\r\n y.c = y.e = null;\r\n\r\n // Return \u00B10 if either is \u00B10.\r\n } else {\r\n y.c = [0];\r\n y.e = 0;\r\n }\r\n }\r\n\r\n return y;\r\n }\r\n\r\n e = bitFloor(x.e / LOG_BASE) + bitFloor(y.e / LOG_BASE);\r\n y.s *= x.s;\r\n xcL = xc.length;\r\n ycL = yc.length;\r\n\r\n // Ensure xc points to longer array and xcL to its length.\r\n if (xcL < ycL) {\r\n zc = xc;\r\n xc = yc;\r\n yc = zc;\r\n i = xcL;\r\n xcL = ycL;\r\n ycL = i;\r\n }\r\n\r\n // Initialise the result array with zeros.\r\n for (i = xcL + ycL, zc = []; i--; zc.push(0));\r\n\r\n base = BASE;\r\n sqrtBase = SQRT_BASE;\r\n\r\n for (i = ycL; --i >= 0;) {\r\n c = 0;\r\n ylo = yc[i] % sqrtBase;\r\n yhi = yc[i] / sqrtBase | 0;\r\n\r\n for (k = xcL, j = i + k; j > i;) {\r\n xlo = xc[--k] % sqrtBase;\r\n xhi = xc[k] / sqrtBase | 0;\r\n m = yhi * xlo + xhi * ylo;\r\n xlo = ylo * xlo + ((m % sqrtBase) * sqrtBase) + zc[j] + c;\r\n c = (xlo / base | 0) + (m / sqrtBase | 0) + yhi * xhi;\r\n zc[j--] = xlo % base;\r\n }\r\n\r\n zc[j] = c;\r\n }\r\n\r\n if (c) {\r\n ++e;\r\n } else {\r\n zc.splice(0, 1);\r\n }\r\n\r\n return normalise(y, zc, e);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the value of this BigNumber negated,\r\n * i.e. multiplied by -1.\r\n */\r\n P.negated = function () {\r\n var x = new BigNumber(this);\r\n x.s = -x.s || null;\r\n return x;\r\n };\r\n\r\n\r\n /*\r\n * n + 0 = n\r\n * n + N = N\r\n * n + I = I\r\n * 0 + n = n\r\n * 0 + 0 = 0\r\n * 0 + N = N\r\n * 0 + I = I\r\n * N + n = N\r\n * N + 0 = N\r\n * N + N = N\r\n * N + I = N\r\n * I + n = I\r\n * I + 0 = I\r\n * I + N = N\r\n * I + I = I\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber plus the value of\r\n * BigNumber(y, b).\r\n */\r\n P.plus = function (y, b) {\r\n var t,\r\n x = this,\r\n a = x.s;\r\n\r\n y = new BigNumber(y, b);\r\n b = y.s;\r\n\r\n // Either NaN?\r\n if (!a || !b) return new BigNumber(NaN);\r\n\r\n // Signs differ?\r\n if (a != b) {\r\n y.s = -b;\r\n return x.minus(y);\r\n }\r\n\r\n var xe = x.e / LOG_BASE,\r\n ye = y.e / LOG_BASE,\r\n xc = x.c,\r\n yc = y.c;\r\n\r\n if (!xe || !ye) {\r\n\r\n // Return \u00B1Infinity if either \u00B1Infinity.\r\n if (!xc || !yc) return new BigNumber(a / 0);\r\n\r\n // Either zero?\r\n // Return y if y is non-zero, x if x is non-zero, or zero if both are zero.\r\n if (!xc[0] || !yc[0]) return yc[0] ? y : new BigNumber(xc[0] ? x : a * 0);\r\n }\r\n\r\n xe = bitFloor(xe);\r\n ye = bitFloor(ye);\r\n xc = xc.slice();\r\n\r\n // Prepend zeros to equalise exponents. Faster to use reverse then do unshifts.\r\n if (a = xe - ye) {\r\n if (a > 0) {\r\n ye = xe;\r\n t = yc;\r\n } else {\r\n a = -a;\r\n t = xc;\r\n }\r\n\r\n t.reverse();\r\n for (; a--; t.push(0));\r\n t.reverse();\r\n }\r\n\r\n a = xc.length;\r\n b = yc.length;\r\n\r\n // Point xc to the longer array, and b to the shorter length.\r\n if (a - b < 0) {\r\n t = yc;\r\n yc = xc;\r\n xc = t;\r\n b = a;\r\n }\r\n\r\n // Only start adding at yc.length - 1 as the further digits of xc can be ignored.\r\n for (a = 0; b;) {\r\n a = (xc[--b] = xc[b] + yc[b] + a) / BASE | 0;\r\n xc[b] = BASE === xc[b] ? 0 : xc[b] % BASE;\r\n }\r\n\r\n if (a) {\r\n xc = [a].concat(xc);\r\n ++ye;\r\n }\r\n\r\n // No need to check for zero, as +x + +y != 0 && -x + -y != 0\r\n // ye = MAX_EXP + 1 possible\r\n return normalise(y, xc, ye);\r\n };\r\n\r\n\r\n /*\r\n * If sd is undefined or null or true or false, return the number of significant digits of\r\n * the value of this BigNumber, or null if the value of this BigNumber is \u00B1Infinity or NaN.\r\n * If sd is true include integer-part trailing zeros in the count.\r\n *\r\n * Otherwise, if sd is a number, return a new BigNumber whose value is the value of this\r\n * BigNumber rounded to a maximum of sd significant digits using rounding mode rm, or\r\n * ROUNDING_MODE if rm is omitted.\r\n *\r\n * sd {number|boolean} number: significant digits: integer, 1 to MAX inclusive.\r\n * boolean: whether to count integer-part trailing zeros: true or false.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}'\r\n */\r\n P.precision = P.sd = function (sd, rm) {\r\n var c, n, v,\r\n x = this;\r\n\r\n if (sd != null && sd !== !!sd) {\r\n intCheck(sd, 1, MAX);\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n\r\n return round(new BigNumber(x), sd, rm);\r\n }\r\n\r\n if (!(c = x.c)) return null;\r\n v = c.length - 1;\r\n n = v * LOG_BASE + 1;\r\n\r\n if (v = c[v]) {\r\n\r\n // Subtract the number of trailing zeros of the last element.\r\n for (; v % 10 == 0; v /= 10, n--);\r\n\r\n // Add the number of digits of the first element.\r\n for (v = c[0]; v >= 10; v /= 10, n++);\r\n }\r\n\r\n if (sd && x.e + 1 > n) n = x.e + 1;\r\n\r\n return n;\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the value of this BigNumber shifted by k places\r\n * (powers of 10). Shift to the right if n > 0, and to the left if n < 0.\r\n *\r\n * k {number} Integer, -MAX_SAFE_INTEGER to MAX_SAFE_INTEGER inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {k}'\r\n */\r\n P.shiftedBy = function (k) {\r\n intCheck(k, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);\r\n return this.times('1e' + k);\r\n };\r\n\r\n\r\n /*\r\n * sqrt(-n) = N\r\n * sqrt(N) = N\r\n * sqrt(-I) = N\r\n * sqrt(I) = I\r\n * sqrt(0) = 0\r\n * sqrt(-0) = -0\r\n *\r\n * Return a new BigNumber whose value is the square root of the value of this BigNumber,\r\n * rounded according to DECIMAL_PLACES and ROUNDING_MODE.\r\n */\r\n P.squareRoot = P.sqrt = function () {\r\n var m, n, r, rep, t,\r\n x = this,\r\n c = x.c,\r\n s = x.s,\r\n e = x.e,\r\n dp = DECIMAL_PLACES + 4,\r\n half = new BigNumber('0.5');\r\n\r\n // Negative/NaN/Infinity/zero?\r\n if (s !== 1 || !c || !c[0]) {\r\n return new BigNumber(!s || s < 0 && (!c || c[0]) ? NaN : c ? x : 1 / 0);\r\n }\r\n\r\n // Initial estimate.\r\n s = Math.sqrt(+valueOf(x));\r\n\r\n // Math.sqrt underflow/overflow?\r\n // Pass x to Math.sqrt as integer, then adjust the exponent of the result.\r\n if (s == 0 || s == 1 / 0) {\r\n n = coeffToString(c);\r\n if ((n.length + e) % 2 == 0) n += '0';\r\n s = Math.sqrt(+n);\r\n e = bitFloor((e + 1) / 2) - (e < 0 || e % 2);\r\n\r\n if (s == 1 / 0) {\r\n n = '5e' + e;\r\n } else {\r\n n = s.toExponential();\r\n n = n.slice(0, n.indexOf('e') + 1) + e;\r\n }\r\n\r\n r = new BigNumber(n);\r\n } else {\r\n r = new BigNumber(s + '');\r\n }\r\n\r\n // Check for zero.\r\n // r could be zero if MIN_EXP is changed after the this value was created.\r\n // This would cause a division by zero (x/t) and hence Infinity below, which would cause\r\n // coeffToString to throw.\r\n if (r.c[0]) {\r\n e = r.e;\r\n s = e + dp;\r\n if (s < 3) s = 0;\r\n\r\n // Newton-Raphson iteration.\r\n for (; ;) {\r\n t = r;\r\n r = half.times(t.plus(div(x, t, dp, 1)));\r\n\r\n if (coeffToString(t.c).slice(0, s) === (n = coeffToString(r.c)).slice(0, s)) {\r\n\r\n // The exponent of r may here be one less than the final result exponent,\r\n // e.g 0.0009999 (e-4) --> 0.001 (e-3), so adjust s so the rounding digits\r\n // are indexed correctly.\r\n if (r.e < e) --s;\r\n n = n.slice(s - 3, s + 1);\r\n\r\n // The 4th rounding digit may be in error by -1 so if the 4 rounding digits\r\n // are 9999 or 4999 (i.e. approaching a rounding boundary) continue the\r\n // iteration.\r\n if (n == '9999' || !rep && n == '4999') {\r\n\r\n // On the first iteration only, check to see if rounding up gives the\r\n // exact result as the nines may infinitely repeat.\r\n if (!rep) {\r\n round(t, t.e + DECIMAL_PLACES + 2, 0);\r\n\r\n if (t.times(t).eq(x)) {\r\n r = t;\r\n break;\r\n }\r\n }\r\n\r\n dp += 4;\r\n s += 4;\r\n rep = 1;\r\n } else {\r\n\r\n // If rounding digits are null, 0{0,4} or 50{0,3}, check for exact\r\n // result. If not, then there are further digits and m will be truthy.\r\n if (!+n || !+n.slice(1) && n.charAt(0) == '5') {\r\n\r\n // Truncate to the first rounding digit.\r\n round(r, r.e + DECIMAL_PLACES + 2, 1);\r\n m = !r.times(r).eq(x);\r\n }\r\n\r\n break;\r\n }\r\n }\r\n }\r\n }\r\n\r\n return round(r, r.e + DECIMAL_PLACES + 1, ROUNDING_MODE, m);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in exponential notation and\r\n * rounded using ROUNDING_MODE to dp fixed decimal places.\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n */\r\n P.toExponential = function (dp, rm) {\r\n if (dp != null) {\r\n intCheck(dp, 0, MAX);\r\n dp++;\r\n }\r\n return format(this, dp, rm, 1);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in fixed-point notation rounding\r\n * to dp fixed decimal places using rounding mode rm, or ROUNDING_MODE if rm is omitted.\r\n *\r\n * Note: as with JavaScript's number type, (-0).toFixed(0) is '0',\r\n * but e.g. (-0.00001).toFixed(0) is '-0'.\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n */\r\n P.toFixed = function (dp, rm) {\r\n if (dp != null) {\r\n intCheck(dp, 0, MAX);\r\n dp = dp + this.e + 1;\r\n }\r\n return format(this, dp, rm);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in fixed-point notation rounded\r\n * using rm or ROUNDING_MODE to dp decimal places, and formatted according to the properties\r\n * of the format or FORMAT object (see BigNumber.set).\r\n *\r\n * The formatting object may contain some or all of the properties shown below.\r\n *\r\n * FORMAT = {\r\n * prefix: '',\r\n * groupSize: 3,\r\n * secondaryGroupSize: 0,\r\n * groupSeparator: ',',\r\n * decimalSeparator: '.',\r\n * fractionGroupSize: 0,\r\n * fractionGroupSeparator: '\\xA0', // non-breaking space\r\n * suffix: ''\r\n * };\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n * [format] {object} Formatting options. See FORMAT pbject above.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n * '[BigNumber Error] Argument not an object: {format}'\r\n */\r\n P.toFormat = function (dp, rm, format) {\r\n var str,\r\n x = this;\r\n\r\n if (format == null) {\r\n if (dp != null && rm && typeof rm == 'object') {\r\n format = rm;\r\n rm = null;\r\n } else if (dp && typeof dp == 'object') {\r\n format = dp;\r\n dp = rm = null;\r\n } else {\r\n format = FORMAT;\r\n }\r\n } else if (typeof format != 'object') {\r\n throw Error\r\n (bignumberError + 'Argument not an object: ' + format);\r\n }\r\n\r\n str = x.toFixed(dp, rm);\r\n\r\n if (x.c) {\r\n var i,\r\n arr = str.split('.'),\r\n g1 = +format.groupSize,\r\n g2 = +format.secondaryGroupSize,\r\n groupSeparator = format.groupSeparator || '',\r\n intPart = arr[0],\r\n fractionPart = arr[1],\r\n isNeg = x.s < 0,\r\n intDigits = isNeg ? intPart.slice(1) : intPart,\r\n len = intDigits.length;\r\n\r\n if (g2) {\r\n i = g1;\r\n g1 = g2;\r\n g2 = i;\r\n len -= i;\r\n }\r\n\r\n if (g1 > 0 && len > 0) {\r\n i = len % g1 || g1;\r\n intPart = intDigits.substr(0, i);\r\n for (; i < len; i += g1) intPart += groupSeparator + intDigits.substr(i, g1);\r\n if (g2 > 0) intPart += groupSeparator + intDigits.slice(i);\r\n if (isNeg) intPart = '-' + intPart;\r\n }\r\n\r\n str = fractionPart\r\n ? intPart + (format.decimalSeparator || '') + ((g2 = +format.fractionGroupSize)\r\n ? fractionPart.replace(new RegExp('\\\\d{' + g2 + '}\\\\B', 'g'),\r\n '$&' + (format.fractionGroupSeparator || ''))\r\n : fractionPart)\r\n : intPart;\r\n }\r\n\r\n return (format.prefix || '') + str + (format.suffix || '');\r\n };\r\n\r\n\r\n /*\r\n * Return an array of two BigNumbers representing the value of this BigNumber as a simple\r\n * fraction with an integer numerator and an integer denominator.\r\n * The denominator will be a positive non-zero value less than or equal to the specified\r\n * maximum denominator. If a maximum denominator is not specified, the denominator will be\r\n * the lowest value necessary to represent the number exactly.\r\n *\r\n * [md] {number|string|BigNumber} Integer >= 1, or Infinity. The maximum denominator.\r\n *\r\n * '[BigNumber Error] Argument {not an integer|out of range} : {md}'\r\n */\r\n P.toFraction = function (md) {\r\n var d, d0, d1, d2, e, exp, n, n0, n1, q, r, s,\r\n x = this,\r\n xc = x.c;\r\n\r\n if (md != null) {\r\n n = new BigNumber(md);\r\n\r\n // Throw if md is less than one or is not an integer, unless it is Infinity.\r\n if (!n.isInteger() && (n.c || n.s !== 1) || n.lt(ONE)) {\r\n throw Error\r\n (bignumberError + 'Argument ' +\r\n (n.isInteger() ? 'out of range: ' : 'not an integer: ') + valueOf(n));\r\n }\r\n }\r\n\r\n if (!xc) return new BigNumber(x);\r\n\r\n d = new BigNumber(ONE);\r\n n1 = d0 = new BigNumber(ONE);\r\n d1 = n0 = new BigNumber(ONE);\r\n s = coeffToString(xc);\r\n\r\n // Determine initial denominator.\r\n // d is a power of 10 and the minimum max denominator that specifies the value exactly.\r\n e = d.e = s.length - x.e - 1;\r\n d.c[0] = POWS_TEN[(exp = e % LOG_BASE) < 0 ? LOG_BASE + exp : exp];\r\n md = !md || n.comparedTo(d) > 0 ? (e > 0 ? d : n1) : n;\r\n\r\n exp = MAX_EXP;\r\n MAX_EXP = 1 / 0;\r\n n = new BigNumber(s);\r\n\r\n // n0 = d1 = 0\r\n n0.c[0] = 0;\r\n\r\n for (; ;) {\r\n q = div(n, d, 0, 1);\r\n d2 = d0.plus(q.times(d1));\r\n if (d2.comparedTo(md) == 1) break;\r\n d0 = d1;\r\n d1 = d2;\r\n n1 = n0.plus(q.times(d2 = n1));\r\n n0 = d2;\r\n d = n.minus(q.times(d2 = d));\r\n n = d2;\r\n }\r\n\r\n d2 = div(md.minus(d0), d1, 0, 1);\r\n n0 = n0.plus(d2.times(n1));\r\n d0 = d0.plus(d2.times(d1));\r\n n0.s = n1.s = x.s;\r\n e = e * 2;\r\n\r\n // Determine which fraction is closer to x, n0/d0 or n1/d1\r\n r = div(n1, d1, e, ROUNDING_MODE).minus(x).abs().comparedTo(\r\n div(n0, d0, e, ROUNDING_MODE).minus(x).abs()) < 1 ? [n1, d1] : [n0, d0];\r\n\r\n MAX_EXP = exp;\r\n\r\n return r;\r\n };\r\n\r\n\r\n /*\r\n * Return the value of this BigNumber converted to a number primitive.\r\n */\r\n P.toNumber = function () {\r\n return +valueOf(this);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber rounded to sd significant digits\r\n * using rounding mode rm or ROUNDING_MODE. If sd is less than the number of digits\r\n * necessary to represent the integer part of the value in fixed-point notation, then use\r\n * exponential notation.\r\n *\r\n * [sd] {number} Significant digits. Integer, 1 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}'\r\n */\r\n P.toPrecision = function (sd, rm) {\r\n if (sd != null) intCheck(sd, 1, MAX);\r\n return format(this, sd, rm, 2);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in base b, or base 10 if b is\r\n * omitted. If a base is specified, including base 10, round according to DECIMAL_PLACES and\r\n * ROUNDING_MODE. If a base is not specified, and this BigNumber has a positive exponent\r\n * that is equal to or greater than TO_EXP_POS, or a negative exponent equal to or less than\r\n * TO_EXP_NEG, return exponential notation.\r\n *\r\n * [b] {number} Integer, 2 to ALPHABET.length inclusive.\r\n *\r\n * '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}'\r\n */\r\n P.toString = function (b) {\r\n var str,\r\n n = this,\r\n s = n.s,\r\n e = n.e;\r\n\r\n // Infinity or NaN?\r\n if (e === null) {\r\n if (s) {\r\n str = 'Infinity';\r\n if (s < 0) str = '-' + str;\r\n } else {\r\n str = 'NaN';\r\n }\r\n } else {\r\n if (b == null) {\r\n str = e <= TO_EXP_NEG || e >= TO_EXP_POS\r\n ? toExponential(coeffToString(n.c), e)\r\n : toFixedPoint(coeffToString(n.c), e, '0');\r\n } else if (b === 10 && alphabetHasNormalDecimalDigits) {\r\n n = round(new BigNumber(n), DECIMAL_PLACES + e + 1, ROUNDING_MODE);\r\n str = toFixedPoint(coeffToString(n.c), n.e, '0');\r\n } else {\r\n intCheck(b, 2, ALPHABET.length, 'Base');\r\n str = convertBase(toFixedPoint(coeffToString(n.c), e, '0'), 10, b, s, true);\r\n }\r\n\r\n if (s < 0 && n.c[0]) str = '-' + str;\r\n }\r\n\r\n return str;\r\n };\r\n\r\n\r\n /*\r\n * Return as toString, but do not accept a base argument, and include the minus sign for\r\n * negative zero.\r\n */\r\n P.valueOf = P.toJSON = function () {\r\n return valueOf(this);\r\n };\r\n\r\n\r\n P._isBigNumber = true;\r\n\r\n P[Symbol.toStringTag] = 'BigNumber';\r\n\r\n // Node.js v10.12.0+\r\n P[Symbol.for('nodejs.util.inspect.custom')] = P.valueOf;\r\n\r\n if (configObject != null) BigNumber.set(configObject);\r\n\r\n return BigNumber;\r\n}\r\n\r\n\r\n// PRIVATE HELPER FUNCTIONS\r\n\r\n// These functions don't need access to variables,\r\n// e.g. DECIMAL_PLACES, in the scope of the `clone` function above.\r\n\r\n\r\nfunction bitFloor(n) {\r\n var i = n | 0;\r\n return n > 0 || n === i ? i : i - 1;\r\n}\r\n\r\n\r\n// Return a coefficient array as a string of base 10 digits.\r\nfunction coeffToString(a) {\r\n var s, z,\r\n i = 1,\r\n j = a.length,\r\n r = a[0] + '';\r\n\r\n for (; i < j;) {\r\n s = a[i++] + '';\r\n z = LOG_BASE - s.length;\r\n for (; z--; s = '0' + s);\r\n r += s;\r\n }\r\n\r\n // Determine trailing zeros.\r\n for (j = r.length; r.charCodeAt(--j) === 48;);\r\n\r\n return r.slice(0, j + 1 || 1);\r\n}\r\n\r\n\r\n// Compare the value of BigNumbers x and y.\r\nfunction compare(x, y) {\r\n var a, b,\r\n xc = x.c,\r\n yc = y.c,\r\n i = x.s,\r\n j = y.s,\r\n k = x.e,\r\n l = y.e;\r\n\r\n // Either NaN?\r\n if (!i || !j) return null;\r\n\r\n a = xc && !xc[0];\r\n b = yc && !yc[0];\r\n\r\n // Either zero?\r\n if (a || b) return a ? b ? 0 : -j : i;\r\n\r\n // Signs differ?\r\n if (i != j) return i;\r\n\r\n a = i < 0;\r\n b = k == l;\r\n\r\n // Either Infinity?\r\n if (!xc || !yc) return b ? 0 : !xc ^ a ? 1 : -1;\r\n\r\n // Compare exponents.\r\n if (!b) return k > l ^ a ? 1 : -1;\r\n\r\n j = (k = xc.length) < (l = yc.length) ? k : l;\r\n\r\n // Compare digit by digit.\r\n for (i = 0; i < j; i++) if (xc[i] != yc[i]) return xc[i] > yc[i] ^ a ? 1 : -1;\r\n\r\n // Compare lengths.\r\n return k == l ? 0 : k > l ^ a ? 1 : -1;\r\n}\r\n\r\n\r\n/*\r\n * Check that n is a primitive number, an integer, and in range, otherwise throw.\r\n */\r\nfunction intCheck(n, min, max, name) {\r\n if (n < min || n > max || n !== mathfloor(n)) {\r\n throw Error\r\n (bignumberError + (name || 'Argument') + (typeof n == 'number'\r\n ? n < min || n > max ? ' out of range: ' : ' not an integer: '\r\n : ' not a primitive number: ') + String(n));\r\n }\r\n}\r\n\r\n\r\n// Assumes finite n.\r\nfunction isOdd(n) {\r\n var k = n.c.length - 1;\r\n return bitFloor(n.e / LOG_BASE) == k && n.c[k] % 2 != 0;\r\n}\r\n\r\n\r\nfunction toExponential(str, e) {\r\n return (str.length > 1 ? str.charAt(0) + '.' + str.slice(1) : str) +\r\n (e < 0 ? 'e' : 'e+') + e;\r\n}\r\n\r\n\r\nfunction toFixedPoint(str, e, z) {\r\n var len, zs;\r\n\r\n // Negative exponent?\r\n if (e < 0) {\r\n\r\n // Prepend zeros.\r\n for (zs = z + '.'; ++e; zs += z);\r\n str = zs + str;\r\n\r\n // Positive exponent\r\n } else {\r\n len = str.length;\r\n\r\n // Append zeros.\r\n if (++e > len) {\r\n for (zs = z, e -= len; --e; zs += z);\r\n str += zs;\r\n } else if (e < len) {\r\n str = str.slice(0, e) + '.' + str.slice(e);\r\n }\r\n }\r\n\r\n return str;\r\n}\r\n\r\n\r\n// EXPORT\r\n\r\n\r\nexport var BigNumber = clone();\r\n\r\nexport default BigNumber;\r\n", "type Comparator = (a: T, b: T) => number;\ntype Predicate = (value: T) => boolean;\n\nclass SplayTreeNode> {\n readonly key: K;\n\n left: Node | null = null;\n right: Node | null = null;\n\n constructor(key: K) {\n this.key = key;\n }\n}\n\nclass SplayTreeSetNode extends SplayTreeNode> {\n constructor(key: K) {\n super(key);\n }\n}\n\nclass SplayTreeMapNode extends SplayTreeNode> {\n readonly value: V;\n\n constructor(key: K, value: V) {\n super(key);\n this.value = value;\n }\n\n replaceValue(value: V) {\n const node = new SplayTreeMapNode(this.key, value);\n node.left = this.left;\n node.right = this.right;\n return node;\n }\n}\n\nabstract class SplayTree> {\n protected abstract root: Node | null;\n\n public size = 0;\n\n protected modificationCount = 0;\n\n protected splayCount = 0;\n\n protected abstract compare: Comparator;\n\n protected abstract validKey: Predicate;\n\n protected splay(key: K) {\n const root = this.root;\n if (root == null) {\n this.compare(key, key);\n return -1;\n }\n\n let right: Node | null = null;\n let newTreeRight: Node | null = null;\n let left: Node | null = null;\n let newTreeLeft: Node | null = null;\n let current = root;\n const compare = this.compare;\n let comp: number;\n while (true) {\n comp = compare(current.key, key);\n if (comp > 0) {\n let currentLeft = current.left;\n if (currentLeft == null) break;\n comp = compare(currentLeft.key, key);\n if (comp > 0) {\n current.left = currentLeft.right;\n currentLeft.right = current;\n current = currentLeft;\n currentLeft = current.left;\n if (currentLeft == null) break;\n }\n if (right == null) {\n newTreeRight = current;\n } else {\n right.left = current;\n }\n right = current;\n current = currentLeft;\n } else if (comp < 0) {\n let currentRight = current.right;\n if (currentRight == null) break;\n comp = compare(currentRight.key, key);\n if (comp < 0) {\n current.right = currentRight.left;\n currentRight.left = current;\n current = currentRight;\n currentRight = current.right;\n if (currentRight == null) break;\n }\n if (left == null) {\n newTreeLeft = current;\n } else {\n left.right = current;\n }\n left = current;\n current = currentRight;\n } else {\n break;\n }\n }\n if (left != null) {\n left.right = current.left;\n current.left = newTreeLeft;\n }\n if (right != null) {\n right.left = current.right;\n current.right = newTreeRight;\n }\n if (this.root !== current) {\n this.root = current;\n this.splayCount++;\n }\n return comp;\n }\n\n protected splayMin(node: Node) {\n let current = node;\n let nextLeft = current.left;\n while (nextLeft != null) {\n const left = nextLeft;\n current.left = left.right;\n left.right = current;\n current = left;\n nextLeft = current.left;\n }\n return current;\n }\n\n protected splayMax(node: Node) {\n let current = node;\n let nextRight = current.right;\n while (nextRight != null) {\n const right = nextRight;\n current.right = right.left;\n right.left = current;\n current = right;\n nextRight = current.right;\n }\n return current;\n }\n\n protected _delete(key: K) {\n if (this.root == null) return null;\n const comp = this.splay(key);\n if (comp != 0) return null;\n let root = this.root;\n const result = root;\n const left = root.left;\n this.size--;\n if (left == null) {\n this.root = root.right;\n } else {\n const right = root.right;\n root = this.splayMax(left);\n\n root.right = right;\n this.root = root;\n }\n this.modificationCount++;\n return result;\n }\n\n protected addNewRoot(node: Node, comp: number) {\n this.size++;\n this.modificationCount++;\n const root = this.root;\n if (root == null) {\n this.root = node;\n return;\n }\n if (comp < 0) {\n node.left = root;\n node.right = root.right;\n root.right = null;\n } else {\n node.right = root;\n node.left = root.left;\n root.left = null;\n }\n this.root = node;\n }\n\n protected _first() {\n const root = this.root;\n if (root == null) return null;\n this.root = this.splayMin(root);\n return this.root;\n }\n\n protected _last() {\n const root = this.root;\n if (root == null) return null;\n this.root = this.splayMax(root);\n return this.root;\n }\n\n public clear() {\n this.root = null;\n this.size = 0;\n this.modificationCount++;\n }\n\n public has(key: unknown) {\n return this.validKey(key) && this.splay(key as K) == 0;\n }\n\n protected defaultCompare(): Comparator {\n return (a: K, b: K) => a < b ? -1 : a > b ? 1 : 0;\n }\n\n protected wrap(): SplayTreeWrapper {\n return {\n getRoot: () => { return this.root },\n setRoot: (root) => { this.root = root },\n getSize: () => { return this.size },\n getModificationCount: () => { return this.modificationCount },\n getSplayCount: () => { return this.splayCount },\n setSplayCount: (count) => { this.splayCount = count },\n splay: (key) => { return this.splay(key) },\n has: (key) => { return this.has(key) },\n };\n }\n}\n\nexport class SplayTreeMap extends SplayTree> implements Iterable<[K, V]>, Map {\n protected root: SplayTreeMapNode | null = null;\n\n protected compare: Comparator;\n protected validKey: Predicate;\n\n constructor(compare?: Comparator, isValidKey?: Predicate) {\n super();\n this.compare = compare ?? this.defaultCompare();\n this.validKey = isValidKey ?? ((a: unknown) => a != null && a != undefined);\n }\n\n delete(key: unknown) {\n if (!this.validKey(key)) return false;\n return this._delete(key as K) != null;\n }\n\n forEach(f: (value: V, key: K, map: Map) => void) {\n const nodes: Iterator<[K, V]> = new SplayTreeMapEntryIterableIterator(this.wrap());\n let result: IteratorResult<[K, V]>;\n while (result = nodes.next(), !result.done) {\n f(result.value[1], result.value[0], this);\n }\n }\n\n get(key: unknown): V | undefined {\n if (!this.validKey(key)) return undefined;\n if (this.root != null) {\n const comp = this.splay(key as K);\n if (comp == 0) {\n return this.root!.value;\n }\n }\n return undefined;\n }\n\n hasValue(value: unknown) {\n const initialSplayCount = this.splayCount;\n const visit = (node: SplayTreeMapNode | null) => {\n while (node != null) {\n if (node.value == value) return true;\n if (initialSplayCount != this.splayCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (node.right != null && visit(node.right)) {\n return true;\n }\n node = node.left;\n }\n return false;\n }\n\n return visit(this.root);\n }\n\n set(key: K, value: V) {\n const comp = this.splay(key);\n if (comp == 0) {\n this.root = this.root!.replaceValue(value);\n this.splayCount += 1;\n return this;\n }\n this.addNewRoot(new SplayTreeMapNode(key, value), comp);\n return this;\n }\n\n setAll(other: Map) {\n other.forEach((value: V, key: K) => {\n this.set(key, value);\n });\n }\n\n setIfAbsent(key: K, ifAbsent: () => V) {\n let comp = this.splay(key);\n if (comp == 0) {\n return this.root!.value;\n }\n const modificationCount = this.modificationCount;\n const splayCount = this.splayCount;\n const value = ifAbsent();\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (splayCount != this.splayCount) {\n comp = this.splay(key);\n }\n this.addNewRoot(new SplayTreeMapNode(key, value), comp);\n return value;\n }\n\n isEmpty() {\n return this.root == null;\n }\n\n isNotEmpty() {\n return !this.isEmpty();\n }\n\n firstKey() {\n if (this.root == null) return null;\n return this._first()!.key;\n }\n\n lastKey() {\n if (this.root == null) return null;\n return this._last()!.key;\n }\n\n lastKeyBefore(key: K) {\n if (key == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(key);\n if (comp < 0) return this.root!.key;\n let node: SplayTreeMapNode | null = this.root!.left;\n if (node == null) return null;\n let nodeRight = node.right;\n while (nodeRight != null) {\n node = nodeRight;\n nodeRight = node.right;\n }\n return node!.key;\n }\n\n firstKeyAfter(key: K) {\n if (key == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(key);\n if (comp > 0) return this.root!.key;\n let node: SplayTreeMapNode | null = this.root!.right;\n if (node == null) return null;\n let nodeLeft = node.left;\n while (nodeLeft != null) {\n node = nodeLeft;\n nodeLeft = node.left;\n }\n return node!.key;\n }\n\n update(key: K, update: (value: V) => V, ifAbsent?: () => V) {\n let comp = this.splay(key);\n if (comp == 0) {\n const modificationCount = this.modificationCount;\n const splayCount = this.splayCount;\n const newValue = update(this.root!.value);\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (splayCount != this.splayCount) {\n this.splay(key);\n }\n this.root = this.root!.replaceValue(newValue);\n this.splayCount += 1;\n return newValue;\n }\n if (ifAbsent != null) {\n const modificationCount = this.modificationCount;\n const splayCount = this.splayCount;\n const newValue = ifAbsent();\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (splayCount != this.splayCount) {\n comp = this.splay(key);\n }\n this.addNewRoot(new SplayTreeMapNode(key, newValue), comp);\n return newValue;\n }\n throw \"Invalid argument (key): Key not in map.\"\n }\n\n updateAll(update: (key: K, value: V) => V) {\n const root = this.root;\n if (root == null) return;\n const iterator = new SplayTreeMapEntryIterableIterator(this.wrap());\n let node: IteratorResult<[K, V]>;\n while (node = iterator.next(), !node.done) {\n const newValue = update(...node.value);\n iterator.replaceValue(newValue);\n }\n }\n\n keys(): IterableIterator {\n return new SplayTreeKeyIterableIterator>(this.wrap());\n }\n\n values(): IterableIterator {\n return new SplayTreeValueIterableIterator(this.wrap());\n }\n\n entries(): IterableIterator<[K, V]> {\n return this[Symbol.iterator]();\n }\n\n [Symbol.iterator](): IterableIterator<[K, V]> {\n return new SplayTreeMapEntryIterableIterator(this.wrap());\n }\n\n [Symbol.toStringTag] = '[object Map]'\n}\n\nexport class SplayTreeSet extends SplayTree> implements Iterable, Set {\n protected root: SplayTreeSetNode | null = null;\n\n protected compare: Comparator;\n protected validKey: Predicate;\n\n constructor(compare?: Comparator, isValidKey?: Predicate) {\n super();\n this.compare = compare ?? this.defaultCompare();\n this.validKey = isValidKey ?? ((v: unknown) => v != null && v != undefined );\n }\n\n delete(element: unknown) {\n if (!this.validKey(element)) return false;\n return this._delete(element as E) != null;\n }\n\n deleteAll(elements: Iterable) {\n for (const element of elements) {\n this.delete(element);\n }\n }\n\n forEach(f: (element: E, element2: E, set: Set) => void) {\n const nodes: Iterator = this[Symbol.iterator]();\n let result: IteratorResult;\n while (result = nodes.next(), !result.done) {\n f(result.value, result.value, this);\n }\n }\n\n add(element: E) {\n const compare = this.splay(element);\n if (compare != 0) this.addNewRoot(new SplayTreeSetNode(element), compare);\n return this;\n }\n\n addAndReturn(element: E) {\n const compare = this.splay(element);\n if (compare != 0) this.addNewRoot(new SplayTreeSetNode(element), compare);\n return this.root!.key;\n }\n\n addAll(elements: Iterable) {\n for (const element of elements) {\n this.add(element);\n }\n }\n\n isEmpty() {\n return this.root == null;\n }\n\n isNotEmpty() {\n return this.root != null;\n }\n\n single() {\n if (this.size == 0) throw \"Bad state: No element\";\n if (this.size > 1) throw \"Bad state: Too many element\";\n return this.root!.key;\n }\n\n first() {\n if (this.size == 0) throw \"Bad state: No element\";\n return this._first()!.key;\n }\n\n last() {\n if (this.size == 0) throw \"Bad state: No element\";\n return this._last()!.key;\n }\n\n lastBefore(element: E) {\n if (element == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(element);\n if (comp < 0) return this.root!.key;\n let node: SplayTreeSetNode | null = this.root!.left;\n if (node == null) return null;\n let nodeRight = node.right;\n while (nodeRight != null) {\n node = nodeRight;\n nodeRight = node.right;\n }\n return node!.key;\n }\n\n firstAfter(element: E) {\n if (element == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(element);\n if (comp > 0) return this.root!.key;\n let node: SplayTreeSetNode | null = this.root!.right;\n if (node == null) return null;\n let nodeLeft = node.left;\n while (nodeLeft != null) {\n node = nodeLeft;\n nodeLeft = node.left;\n }\n return node!.key;\n }\n\n retainAll(elements: Iterable) {\n const retainSet = new SplayTreeSet(this.compare, this.validKey);\n const modificationCount = this.modificationCount;\n for (const object of elements) {\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (this.validKey(object) && this.splay(object as E) == 0) {\n retainSet.add(this.root!.key);\n }\n }\n if (retainSet.size != this.size) {\n this.root = retainSet.root;\n this.size = retainSet.size;\n this.modificationCount++;\n }\n }\n\n lookup(object: unknown): E | null {\n if (!this.validKey(object)) return null;\n const comp = this.splay(object as E);\n if (comp != 0) return null;\n return this.root!.key;\n }\n\n intersection(other: Set): Set {\n const result = new SplayTreeSet(this.compare, this.validKey);\n for (const element of this) {\n if (other.has(element)) result.add(element);\n }\n return result;\n }\n\n difference(other: Set): Set {\n const result = new SplayTreeSet(this.compare, this.validKey);\n for (const element of this) {\n if (!other.has(element)) result.add(element);\n }\n return result;\n }\n\n union(other: Set): Set {\n const u = this.clone();\n u.addAll(other);\n return u;\n }\n\n protected clone() {\n const set = new SplayTreeSet(this.compare, this.validKey);\n set.size = this.size;\n set.root = this.copyNode>(this.root);\n return set;\n }\n\n protected copyNode>(node: Node | null) {\n if (node == null) return null;\n function copyChildren(node: Node, dest: SplayTreeSetNode) {\n let left: Node | null;\n let right: Node | null;\n do {\n left = node.left;\n right = node.right;\n if (left != null) {\n const newLeft = new SplayTreeSetNode(left.key);\n dest.left = newLeft;\n copyChildren(left, newLeft);\n }\n if (right != null) {\n const newRight = new SplayTreeSetNode(right.key);\n dest.right = newRight;\n node = right;\n dest = newRight;\n }\n } while (right != null);\n }\n\n const result = new SplayTreeSetNode(node.key);\n copyChildren(node, result);\n return result;\n }\n\n toSet(): Set {\n return this.clone();\n }\n\n entries(): IterableIterator<[E, E]> {\n return new SplayTreeSetEntryIterableIterator>(this.wrap());\n }\n\n keys(): IterableIterator {\n return this[Symbol.iterator]();\n }\n \n values(): IterableIterator {\n return this[Symbol.iterator]();\n }\n\n [Symbol.iterator](): IterableIterator {\n return new SplayTreeKeyIterableIterator>(this.wrap());\n }\n\n [Symbol.toStringTag] = '[object Set]'\n}\n\ninterface SplayTreeWrapper> {\n getRoot: () => Node | null;\n setRoot: (root: Node | null) => void;\n getSize: () => number;\n getModificationCount: () => number;\n getSplayCount: () => number;\n setSplayCount: (count: number) => void;\n splay: (key: K) => number;\n has: (key: unknown) => boolean;\n}\n\ntype SplayTreeMapWrapper = SplayTreeWrapper>;\n\nabstract class SplayTreeIterableIterator, T> implements IterableIterator {\n protected readonly tree: SplayTreeWrapper;\n\n protected readonly path = new Array();\n\n protected modificationCount: number | null = null;\n\n protected splayCount: number;\n\n constructor(tree: SplayTreeWrapper) {\n this.tree = tree;\n this.splayCount = tree.getSplayCount();\n }\n\n [Symbol.iterator](): IterableIterator {\n return this;\n }\n\n next(): IteratorResult {\n if (this.moveNext()) return { done: false, value: this.current()! }\n return { done: true, value: null }\n }\n\n protected current() {\n if (!this.path.length) return null;\n const node = this.path[this.path.length - 1];\n return this.getValue(node);\n }\n\n protected rebuildPath(key: K) {\n this.path.splice(0, this.path.length)\n this.tree.splay(key);\n this.path.push(this.tree.getRoot()!);\n this.splayCount = this.tree.getSplayCount();\n }\n\n protected findLeftMostDescendent(node: Node | null) {\n while (node != null) {\n this.path.push(node);\n node = node.left;\n }\n }\n\n protected moveNext() {\n if (this.modificationCount != this.tree.getModificationCount()) {\n if (this.modificationCount == null) {\n this.modificationCount = this.tree.getModificationCount();\n let node = this.tree.getRoot();\n while (node != null) {\n this.path.push(node);\n node = node.left;\n }\n return this.path.length > 0;\n }\n throw \"Concurrent modification during iteration.\";\n }\n if (!this.path.length) return false;\n if (this.splayCount != this.tree.getSplayCount()) {\n this.rebuildPath(this.path[this.path.length - 1].key);\n }\n let node = this.path[this.path.length - 1];\n let next = node.right;\n if (next != null) {\n while (next != null) {\n this.path.push(next);\n next = next.left;\n }\n return true;\n }\n this.path.pop();\n while (this.path.length && this.path[this.path.length - 1].right === node) {\n node = this.path.pop()!;\n }\n return this.path.length > 0;\n }\n\n protected abstract getValue(node: Node): T\n}\n\nclass SplayTreeKeyIterableIterator> extends SplayTreeIterableIterator {\n\n protected getValue(node: Node) {\n return node.key;\n }\n}\n\nclass SplayTreeSetEntryIterableIterator> extends SplayTreeIterableIterator {\n\n protected getValue(node: Node): [K, K] {\n return [node.key, node.key];\n }\n}\n\nclass SplayTreeValueIterableIterator extends SplayTreeIterableIterator, V> {\n\n constructor(map: SplayTreeMapWrapper) {\n super(map);\n }\n\n protected getValue(node: SplayTreeMapNode) {\n return node.value;\n }\n}\n\nclass SplayTreeMapEntryIterableIterator extends SplayTreeIterableIterator, [K, V]> {\n\n constructor(map: SplayTreeMapWrapper) {\n super(map);\n }\n\n protected getValue(node: SplayTreeMapNode): [K, V] {\n return [node.key, node.value];\n }\n\n replaceValue(value: V) {\n if (this.modificationCount != this.tree.getModificationCount()) {\n throw \"Concurrent modification during iteration.\";\n }\n if (this.splayCount != this.tree.getSplayCount()) {\n this.rebuildPath(this.path[this.path.length - 1].key);\n }\n const last = this.path.pop()!;\n const newLast = last.replaceValue(value);\n if (!this.path.length) {\n this.tree.setRoot(newLast);\n } else {\n const parent = this.path[this.path.length - 1];\n if (last === parent.left) {\n parent.left = newLast;\n } else {\n parent.right = newLast;\n }\n }\n this.path.push(newLast);\n const count = this.tree.getSplayCount() + 1;\n this.tree.setSplayCount(count);\n this.splayCount = count;\n }\n}", "import BigNumber from \"bignumber.js\";\nimport { Bbox } from \"./bbox.js\";\nimport { precision } from \"./precision.js\";\nimport Segment from \"./segment.js\";\nimport { Point } from \"./sweep-event.js\";\n\nexport type Ring = [number, number][]\nexport type Poly = Ring[]\nexport type MultiPoly = Poly[]\nexport type Geom = Poly | MultiPoly\n\nexport class RingIn {\n poly: PolyIn\n isExterior: boolean\n segments: Segment[]\n bbox: Bbox\n\n constructor(geomRing: Ring, poly: PolyIn, isExterior: boolean) {\n if (!Array.isArray(geomRing) || geomRing.length === 0) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n\n this.poly = poly\n this.isExterior = isExterior\n this.segments = []\n\n if (\n typeof geomRing[0][0] !== \"number\" ||\n typeof geomRing[0][1] !== \"number\"\n ) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n\n const firstPoint = precision.snap({ x: new BigNumber(geomRing[0][0]), y: new BigNumber(geomRing[0][1]) }) as Point\n this.bbox = {\n ll: { x: firstPoint.x, y: firstPoint.y },\n ur: { x: firstPoint.x, y: firstPoint.y },\n }\n\n let prevPoint = firstPoint\n for (let i = 1, iMax = geomRing.length; i < iMax; i++) {\n if (\n typeof geomRing[i][0] !== \"number\" ||\n typeof geomRing[i][1] !== \"number\"\n ) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n const point = precision.snap({ x: new BigNumber(geomRing[i][0]), y: new BigNumber(geomRing[i][1]) }) as Point\n // skip repeated points\n if (point.x.eq(prevPoint.x) && point.y.eq(prevPoint.y)) continue\n this.segments.push(Segment.fromRing(prevPoint, point, this))\n if (point.x.isLessThan(this.bbox.ll.x)) this.bbox.ll.x = point.x\n if (point.y.isLessThan(this.bbox.ll.y)) this.bbox.ll.y = point.y\n if (point.x.isGreaterThan(this.bbox.ur.x)) this.bbox.ur.x = point.x\n if (point.y.isGreaterThan(this.bbox.ur.y)) this.bbox.ur.y = point.y\n prevPoint = point\n }\n // add segment from last to first if last is not the same as first\n if (!firstPoint.x.eq(prevPoint.x) || !firstPoint.y.eq(prevPoint.y)) {\n this.segments.push(Segment.fromRing(prevPoint, firstPoint, this))\n }\n }\n\n getSweepEvents() {\n const sweepEvents = []\n for (let i = 0, iMax = this.segments.length; i < iMax; i++) {\n const segment = this.segments[i]\n sweepEvents.push(segment.leftSE)\n sweepEvents.push(segment.rightSE)\n }\n return sweepEvents\n }\n}\n\nexport class PolyIn {\n multiPoly: MultiPolyIn\n exteriorRing: RingIn\n interiorRings: RingIn[]\n bbox: Bbox\n\n constructor(geomPoly: Poly, multiPoly: MultiPolyIn) {\n if (!Array.isArray(geomPoly)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n this.exteriorRing = new RingIn(geomPoly[0], this, true)\n // copy by value\n this.bbox = {\n ll: { x: this.exteriorRing.bbox.ll.x, y: this.exteriorRing.bbox.ll.y },\n ur: { x: this.exteriorRing.bbox.ur.x, y: this.exteriorRing.bbox.ur.y },\n }\n this.interiorRings = []\n for (let i = 1, iMax = geomPoly.length; i < iMax; i++) {\n const ring = new RingIn(geomPoly[i], this, false)\n if (ring.bbox.ll.x.isLessThan(this.bbox.ll.x)) this.bbox.ll.x = ring.bbox.ll.x\n if (ring.bbox.ll.y.isLessThan(this.bbox.ll.y)) this.bbox.ll.y = ring.bbox.ll.y\n if (ring.bbox.ur.x.isGreaterThan(this.bbox.ur.x)) this.bbox.ur.x = ring.bbox.ur.x\n if (ring.bbox.ur.y.isGreaterThan(this.bbox.ur.y)) this.bbox.ur.y = ring.bbox.ur.y\n this.interiorRings.push(ring)\n }\n this.multiPoly = multiPoly\n }\n\n getSweepEvents() {\n const sweepEvents = this.exteriorRing.getSweepEvents()\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringSweepEvents = this.interiorRings[i].getSweepEvents()\n for (let j = 0, jMax = ringSweepEvents.length; j < jMax; j++) {\n sweepEvents.push(ringSweepEvents[j])\n }\n }\n return sweepEvents\n }\n}\n\nexport class MultiPolyIn {\n isSubject: boolean\n polys: PolyIn[]\n bbox: Bbox\n\n constructor(geom: Geom, isSubject: boolean) {\n if (!Array.isArray(geom)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n\n try {\n // if the input looks like a polygon, convert it to a multipolygon\n if (typeof geom[0][0][0] === \"number\") geom = [geom as Poly]\n } catch (ex) {\n // The input is either malformed or has empty arrays.\n // In either case, it will be handled later on.\n }\n\n this.polys = []\n this.bbox = {\n ll: { x: new BigNumber(Number.POSITIVE_INFINITY), y: new BigNumber(Number.POSITIVE_INFINITY) },\n ur: { x: new BigNumber(Number.NEGATIVE_INFINITY), y: new BigNumber(Number.NEGATIVE_INFINITY) },\n }\n for (let i = 0, iMax = geom.length; i < iMax; i++) {\n const poly = new PolyIn(geom[i] as Poly, this)\n if (poly.bbox.ll.x.isLessThan(this.bbox.ll.x)) this.bbox.ll.x = poly.bbox.ll.x\n if (poly.bbox.ll.y.isLessThan(this.bbox.ll.y)) this.bbox.ll.y = poly.bbox.ll.y\n if (poly.bbox.ur.x.isGreaterThan(this.bbox.ur.x)) this.bbox.ur.x = poly.bbox.ur.x\n if (poly.bbox.ur.y.isGreaterThan(this.bbox.ur.y)) this.bbox.ur.y = poly.bbox.ur.y\n this.polys.push(poly)\n }\n this.isSubject = isSubject\n }\n\n getSweepEvents() {\n const sweepEvents = []\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polySweepEvents = this.polys[i].getSweepEvents()\n for (let j = 0, jMax = polySweepEvents.length; j < jMax; j++) {\n sweepEvents.push(polySweepEvents[j])\n }\n }\n return sweepEvents\n }\n}\n", "export default (x: T) => {\n return () => {\n return x\n }\n}", "import BigNumber from \"bignumber.js\"\nimport constant from \"./constant.js\"\n\nexport default (eps?: number) => {\n const almostEqual = eps ? (a: BigNumber, b: BigNumber) =>\n b.minus(a).abs().isLessThanOrEqualTo(eps)\n : constant(false)\n\n return (a: BigNumber, b: BigNumber) => {\n if (almostEqual(a, b)) return 0\n\n return a.comparedTo(b)\n }\n}", "import BigNumber from \"bignumber.js\";\nimport constant from \"./constant.js\";\nimport { Vector } from \"./vector.js\";\n\nexport default function (eps?: number) {\n const almostCollinear = eps ? (area2: BigNumber, ax: BigNumber, ay: BigNumber, cx: BigNumber, cy: BigNumber) =>\n area2.exponentiatedBy(2).isLessThanOrEqualTo(\n cx.minus(ax).exponentiatedBy(2).plus(cy.minus(ay).exponentiatedBy(2))\n .times(eps))\n : constant(false)\n\n return (a: Vector, b: Vector, c: Vector) => {\n const ax = a.x, ay = a.y, cx = c.x, cy = c.y\n\n const area2 = ay.minus(cy).times(b.x.minus(cx)).minus(ax.minus(cx).times(b.y.minus(cy)))\n\n if (almostCollinear(area2, ax, ay, cx, cy)) return 0\n\n return area2.comparedTo(0)\n }\n}", "import BigNumber from \"bignumber.js\";\nimport { SplayTreeSet } from \"splaytree-ts\"\nimport compare from \"./compare.js\";\nimport identity from \"./identity.js\";\nimport { Vector } from \"./vector.js\";\n\nexport default (eps?: number) => {\n if (eps) {\n\n const xTree = new SplayTreeSet(compare(eps))\n const yTree = new SplayTreeSet(compare(eps))\n\n const snapCoord = (coord: BigNumber, tree: SplayTreeSet) => {\n return tree.addAndReturn(coord)\n }\n\n const snap = (v: Vector) => {\n return {\n x: snapCoord(v.x, xTree),\n y: snapCoord(v.y, yTree),\n } as Vector\n }\n\n snap({ x: new BigNumber(0), y: new BigNumber(0)})\n\n return snap\n }\n\n return identity\n}", "export default (x: T) => {\n return x;\n}", "import compare from \"./compare.js\";\nimport orient from \"./orient.js\";\nimport snap from \"./snap.js\";\n\nconst set = (eps?: number) => {\n return {\n set: (eps?: number) => { precision = set(eps) },\n reset: () => set(eps),\n compare: compare(eps),\n snap: snap(eps),\n orient: orient(eps)\n }\n}\n\nexport let precision: ReturnType = set()", "import { Vector } from \"./vector.js\";\n\nexport interface Bbox {\n ll: Vector;\n ur: Vector;\n}\n\n/**\n * A bounding box has the format:\n *\n * { ll: { x: xmin, y: ymin }, ur: { x: xmax, y: ymax } }\n *\n */\n\nexport const isInBbox = (bbox: Bbox, point: Vector) => {\n return (\n bbox.ll.x.isLessThanOrEqualTo(point.x) &&\n point.x.isLessThanOrEqualTo(bbox.ur.x) &&\n bbox.ll.y.isLessThanOrEqualTo(point.y) &&\n point.y.isLessThanOrEqualTo(bbox.ur.y) \n )\n}\n\n/* Returns either null, or a bbox (aka an ordered pair of points)\n * If there is only one point of overlap, a bbox with identical points\n * will be returned */\nexport const getBboxOverlap = (b1: Bbox, b2: Bbox) => {\n // check if the bboxes overlap at all\n if (\n b2.ur.x.isLessThan(b1.ll.x) ||\n b1.ur.x.isLessThan(b2.ll.x) ||\n b2.ur.y.isLessThan(b1.ll.y) ||\n b1.ur.y.isLessThan(b2.ll.y) \n )\n return null\n\n // find the middle two X values\n const lowerX = b1.ll.x.isLessThan(b2.ll.x) ? b2.ll.x : b1.ll.x\n const upperX = b1.ur.x.isLessThan(b2.ur.x) ? b1.ur.x : b2.ur.x\n\n // find the middle two Y values\n const lowerY = b1.ll.y.isLessThan(b2.ll.y) ? b2.ll.y : b1.ll.y\n const upperY = b1.ur.y.isLessThan(b2.ur.y) ? b1.ur.y : b2.ur.y\n\n // put those middle values together to get the overlap\n return { ll: { x: lowerX, y: lowerY }, ur: { x: upperX, y: upperY } } as Bbox\n}\n", "import { SplayTreeSet } from \"splaytree-ts\"\nimport { getBboxOverlap } from \"./bbox.js\"\nimport * as geomIn from \"./geom-in.js\"\nimport { Geom } from \"./geom-in.js\"\nimport * as geomOut from \"./geom-out.js\"\nimport { precision } from \"./precision.js\"\nimport SweepEvent from \"./sweep-event.js\"\nimport SweepLine from \"./sweep-line.js\"\n\nexport class Operation {\n type!: string\n numMultiPolys!: number\n\n run(type: string, geom: Geom, moreGeoms: Geom[]) {\n operation.type = type\n\n /* Convert inputs to MultiPoly objects */\n const multipolys = [new geomIn.MultiPolyIn(geom, true)]\n for (let i = 0, iMax = moreGeoms.length; i < iMax; i++) {\n multipolys.push(new geomIn.MultiPolyIn(moreGeoms[i], false))\n }\n operation.numMultiPolys = multipolys.length\n\n /* BBox optimization for difference operation\n * If the bbox of a multipolygon that's part of the clipping doesn't\n * intersect the bbox of the subject at all, we can just drop that\n * multiploygon. */\n if (operation.type === \"difference\") {\n // in place removal\n const subject = multipolys[0]\n let i = 1\n while (i < multipolys.length) {\n if (getBboxOverlap(multipolys[i].bbox, subject.bbox) !== null) i++\n else multipolys.splice(i, 1)\n }\n }\n\n /* BBox optimization for intersection operation\n * If we can find any pair of multipolygons whose bbox does not overlap,\n * then the result will be empty. */\n if (operation.type === \"intersection\") {\n // TODO: this is O(n^2) in number of polygons. By sorting the bboxes,\n // it could be optimized to O(n * ln(n))\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const mpA = multipolys[i]\n for (let j = i + 1, jMax = multipolys.length; j < jMax; j++) {\n if (getBboxOverlap(mpA.bbox, multipolys[j].bbox) === null) return []\n }\n }\n }\n\n /* Put segment endpoints in a priority queue */\n const queue = new SplayTreeSet(SweepEvent.compare)\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const sweepEvents = multipolys[i].getSweepEvents()\n for (let j = 0, jMax = sweepEvents.length; j < jMax; j++) {\n queue.add(sweepEvents[j])\n }\n }\n\n /* Pass the sweep line over those endpoints */\n const sweepLine = new SweepLine(queue)\n let evt = null\n if (queue.size != 0) {\n evt = queue.first()\n queue.delete(evt)\n }\n while (evt) {\n const newEvents = sweepLine.process(evt)\n for (let i = 0, iMax = newEvents.length; i < iMax; i++) {\n const evt = newEvents[i]\n if (evt.consumedBy === undefined) queue.add(evt)\n }\n if (queue.size != 0) {\n evt = queue.first()\n queue.delete(evt)\n } else {\n evt = null;\n }\n }\n\n // free some memory we don't need anymore\n precision.reset()\n\n /* Collect and compile segments we're keeping into a multipolygon */\n const ringsOut = geomOut.RingOut.factory(sweepLine.segments)\n const result = new geomOut.MultiPolyOut(ringsOut)\n return result.getGeom()\n }\n}\n\n// singleton available by import\nconst operation = new Operation()\n\nexport default operation\n", "import * as bn from \"bignumber.js\";\n\nexport interface Vector {\n x: bn.BigNumber;\n y: bn.BigNumber;\n}\n\n/* Cross Product of two vectors with first point at origin */\nexport const crossProduct = (a: Vector, b: Vector) => a.x.times(b.y).minus(a.y.times(b.x))\n\n/* Dot Product of two vectors with first point at origin */\nexport const dotProduct = (a: Vector, b: Vector) => a.x.times(b.x).plus(a.y.times(b.y))\n\nexport const length = (v: Vector) => dotProduct(v, v).sqrt()\n\n/* Get the sine of the angle from pShared -> pAngle to pShaed -> pBase */\nexport const sineOfAngle = (pShared: Vector, pBase: Vector, pAngle: Vector) => {\n const vBase = { x: pBase.x.minus(pShared.x), y: pBase.y.minus(pShared.y) }\n const vAngle = { x: pAngle.x.minus(pShared.x), y: pAngle.y.minus(pShared.y) }\n return crossProduct(vAngle, vBase).div(length(vAngle)).div(length(vBase))\n}\n\n/* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */\nexport const cosineOfAngle = (pShared: Vector, pBase: Vector, pAngle: Vector) => {\n const vBase = { x: pBase.x.minus(pShared.x), y: pBase.y.minus(pShared.y) }\n const vAngle = { x: pAngle.x.minus(pShared.x), y: pAngle.y.minus(pShared.y) }\n return dotProduct(vAngle, vBase).div(length(vAngle)).div(length(vBase))\n}\n\n/* Get the x coordinate where the given line (defined by a point and vector)\n * crosses the horizontal line with the given y coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\nexport const horizontalIntersection = (pt: Vector, v: Vector, y: bn.BigNumber) => {\n if (v.y.isZero()) return null\n return { x: pt.x.plus((v.x.div(v.y)).times(y.minus(pt.y))), y: y }\n}\n\n/* Get the y coordinate where the given line (defined by a point and vector)\n * crosses the vertical line with the given x coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\nexport const verticalIntersection = (pt: Vector, v: Vector, x: bn.BigNumber) => {\n if (v.x.isZero()) return null\n return { x: x, y: pt.y.plus((v.y.div(v.x)).times(x.minus(pt.x))) }\n}\n\n/* Get the intersection of two lines, each defined by a base point and a vector.\n * In the case of parrallel lines (including overlapping ones) returns null. */\nexport const intersection = (pt1: Vector, v1: Vector, pt2: Vector, v2: Vector) => {\n // take some shortcuts for vertical and horizontal lines\n // this also ensures we don't calculate an intersection and then discover\n // it's actually outside the bounding box of the line\n if (v1.x.isZero()) return verticalIntersection(pt2, v2, pt1.x)\n if (v2.x.isZero()) return verticalIntersection(pt1, v1, pt2.x)\n if (v1.y.isZero()) return horizontalIntersection(pt2, v2, pt1.y)\n if (v2.y.isZero()) return horizontalIntersection(pt1, v1, pt2.y)\n\n // General case for non-overlapping segments.\n // This algorithm is based on Schneider and Eberly.\n // http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf - pg 244\n\n const kross = crossProduct(v1, v2)\n if (kross.isZero()) return null\n\n const ve = { x: pt2.x.minus(pt1.x), y: pt2.y.minus(pt1.y) }\n const d1 = crossProduct(ve, v1).div(kross)\n const d2 = crossProduct(ve, v2).div(kross)\n\n // take the average of the two calculations to minimize rounding error\n const x1 = pt1.x.plus(d2.times(v1.x)),\n x2 = pt2.x.plus(d1.times(v2.x))\n const y1 = pt1.y.plus(d2.times(v1.y)),\n y2 = pt2.y.plus(d1.times(v2.y))\n const x = x1.plus(x2).div(2)\n const y = y1.plus(y2).div(2)\n return { x: x, y: y } as Vector\n}\n\n/* Given a vector, return one that is perpendicular */\nexport const perpendicular = (v: Vector) => {\n return { x: v.y.negated(), y: v.x }\n}", "import BigNumber from \"bignumber.js\";\nimport Segment from \"./segment.js\"\nimport { cosineOfAngle, sineOfAngle, Vector } from \"./vector.js\"\n\nexport interface Point extends Vector {\n events: SweepEvent[];\n}\n\nexport default class SweepEvent {\n point: Point;\n isLeft: boolean;\n segment!: Segment;\n otherSE!: SweepEvent;\n consumedBy: SweepEvent | undefined;\n\n // for ordering sweep events in the sweep event queue\n static compare(a: SweepEvent, b: SweepEvent) {\n // favor event with a point that the sweep line hits first\n const ptCmp = SweepEvent.comparePoints(a.point, b.point)\n if (ptCmp !== 0) return ptCmp\n\n // the points are the same, so link them if needed\n if (a.point !== b.point) a.link(b)\n\n // favor right events over left\n if (a.isLeft !== b.isLeft) return a.isLeft ? 1 : -1\n\n // we have two matching left or right endpoints\n // ordering of this case is the same as for their segments\n return Segment.compare(a.segment, b.segment)\n }\n\n // for ordering points in sweep line order\n static comparePoints(aPt: Point, bPt: Point) {\n if (aPt.x.isLessThan(bPt.x)) return -1\n if (aPt.x.isGreaterThan(bPt.x)) return 1\n\n if (aPt.y.isLessThan(bPt.y)) return -1\n if (aPt.y.isGreaterThan(bPt.y)) return 1\n\n return 0\n }\n\n // Warning: 'point' input will be modified and re-used (for performance)\n constructor(point: Point, isLeft: boolean) {\n if (point.events === undefined) point.events = [this]\n else point.events.push(this)\n this.point = point\n this.isLeft = isLeft\n // this.segment, this.otherSE set by factory\n }\n\n link(other: SweepEvent) {\n if (other.point === this.point) {\n throw new Error(\"Tried to link already linked events\")\n }\n const otherEvents = other.point.events\n for (let i = 0, iMax = otherEvents.length; i < iMax; i++) {\n const evt = otherEvents[i]\n this.point.events.push(evt)\n evt.point = this.point\n }\n this.checkForConsuming()\n }\n\n /* Do a pass over our linked events and check to see if any pair\n * of segments match, and should be consumed. */\n checkForConsuming() {\n // FIXME: The loops in this method run O(n^2) => no good.\n // Maintain little ordered sweep event trees?\n // Can we maintaining an ordering that avoids the need\n // for the re-sorting with getLeftmostComparator in geom-out?\n\n // Compare each pair of events to see if other events also match\n const numEvents = this.point.events.length\n for (let i = 0; i < numEvents; i++) {\n const evt1 = this.point.events[i]\n if (evt1.segment.consumedBy !== undefined) continue\n for (let j = i + 1; j < numEvents; j++) {\n const evt2 = this.point.events[j]\n if (evt2.consumedBy !== undefined) continue\n if (evt1.otherSE.point.events !== evt2.otherSE.point.events) continue\n evt1.segment.consume(evt2.segment)\n }\n }\n }\n\n getAvailableLinkedEvents() {\n // point.events is always of length 2 or greater\n const events = []\n for (let i = 0, iMax = this.point.events.length; i < iMax; i++) {\n const evt = this.point.events[i]\n if (evt !== this && !evt.segment.ringOut && evt.segment.isInResult()) {\n events.push(evt)\n }\n }\n return events\n }\n\n /**\n * Returns a comparator function for sorting linked events that will\n * favor the event that will give us the smallest left-side angle.\n * All ring construction starts as low as possible heading to the right,\n * so by always turning left as sharp as possible we'll get polygons\n * without uncessary loops & holes.\n *\n * The comparator function has a compute cache such that it avoids\n * re-computing already-computed values.\n */\n getLeftmostComparator(baseEvent: SweepEvent) {\n const cache = new Map()\n\n const fillCache = (linkedEvent: SweepEvent) => {\n const nextEvent = linkedEvent.otherSE\n cache.set(linkedEvent, {\n sine: sineOfAngle(this.point, baseEvent.point, nextEvent.point),\n cosine: cosineOfAngle(this.point, baseEvent.point, nextEvent.point),\n })\n }\n\n return (a: SweepEvent, b: SweepEvent) => {\n if (!cache.has(a)) fillCache(a)\n if (!cache.has(b)) fillCache(b)\n\n const { sine: asine, cosine: acosine } = cache.get(a)!\n const { sine: bsine, cosine: bcosine } = cache.get(b)!\n\n // both on or above x-axis\n if (asine.isGreaterThanOrEqualTo(0) && bsine.isGreaterThanOrEqualTo(0)) {\n if (acosine.isLessThan(bcosine)) return 1\n if (acosine.isGreaterThan(bcosine)) return -1\n return 0\n }\n\n // both below x-axis\n if (asine.isLessThan(0) && bsine.isLessThan(0)) {\n if (acosine.isLessThan(bcosine)) return -1\n if (acosine.isGreaterThan(bcosine)) return 1\n return 0\n }\n\n // one above x-axis, one below\n if (bsine.isLessThan(asine)) return -1\n if (bsine.isGreaterThan(asine)) return 1\n return 0\n }\n }\n}\n", "import { MultiPoly, Poly, Ring } from \"./geom-in.js\"\nimport { precision } from \"./precision.js\"\nimport Segment from \"./segment.js\"\nimport SweepEvent from \"./sweep-event.js\"\n\nexport class RingOut {\n events: SweepEvent[]\n poly: PolyOut | null\n _isExteriorRing: boolean | undefined\n _enclosingRing: RingOut | null | undefined\n \n /* Given the segments from the sweep line pass, compute & return a series\n * of closed rings from all the segments marked to be part of the result */\n static factory(allSegments: Segment[]) {\n const ringsOut = []\n\n for (let i = 0, iMax = allSegments.length; i < iMax; i++) {\n const segment = allSegments[i]\n if (!segment.isInResult() || segment.ringOut) continue\n\n let prevEvent = null\n let event = segment.leftSE\n let nextEvent = segment.rightSE\n const events = [event]\n\n const startingPoint = event.point\n const intersectionLEs = []\n\n /* Walk the chain of linked events to form a closed ring */\n while (true) {\n prevEvent = event\n event = nextEvent\n events.push(event)\n\n /* Is the ring complete? */\n if (event.point === startingPoint) break\n\n while (true) {\n const availableLEs = event.getAvailableLinkedEvents()\n\n /* Did we hit a dead end? This shouldn't happen. Indicates some earlier\n * part of the algorithm malfunctioned... please file a bug report. */\n if (availableLEs.length === 0) {\n const firstPt = events[0].point\n const lastPt = events[events.length - 1].point\n throw new Error(\n `Unable to complete output ring starting at [${firstPt.x},` +\n ` ${firstPt.y}]. Last matching segment found ends at` +\n ` [${lastPt.x}, ${lastPt.y}].`,\n )\n }\n\n /* Only one way to go, so cotinue on the path */\n if (availableLEs.length === 1) {\n nextEvent = availableLEs[0].otherSE\n break\n }\n\n /* We must have an intersection. Check for a completed loop */\n let indexLE = null\n for (let j = 0, jMax = intersectionLEs.length; j < jMax; j++) {\n if (intersectionLEs[j].point === event.point) {\n indexLE = j\n break\n }\n }\n /* Found a completed loop. Cut that off and make a ring */\n if (indexLE !== null) {\n const intersectionLE = intersectionLEs.splice(indexLE)[0]\n const ringEvents = events.splice(intersectionLE.index)\n ringEvents.unshift(ringEvents[0].otherSE)\n ringsOut.push(new RingOut(ringEvents.reverse()))\n continue\n }\n /* register the intersection */\n intersectionLEs.push({\n index: events.length,\n point: event.point,\n })\n /* Choose the left-most option to continue the walk */\n const comparator = event.getLeftmostComparator(prevEvent)\n nextEvent = availableLEs.sort(comparator)[0].otherSE\n break\n }\n }\n\n ringsOut.push(new RingOut(events))\n }\n return ringsOut\n }\n\n constructor(events: SweepEvent[]) {\n this.events = events\n for (let i = 0, iMax = events.length; i < iMax; i++) {\n events[i].segment.ringOut = this\n }\n this.poly = null\n }\n\n getGeom() {\n // Remove superfluous points (ie extra points along a straight line),\n let prevPt = this.events[0].point\n const points = [prevPt]\n for (let i = 1, iMax = this.events.length - 1; i < iMax; i++) {\n const pt = this.events[i].point\n const nextPt = this.events[i + 1].point\n if (precision.orient(pt, prevPt, nextPt) === 0) continue\n points.push(pt)\n prevPt = pt\n }\n\n // ring was all (within rounding error of angle calc) colinear points\n if (points.length === 1) return null\n\n // check if the starting point is necessary\n const pt = points[0]\n const nextPt = points[1]\n if (precision.orient(pt, prevPt, nextPt) === 0) points.shift()\n\n points.push(points[0])\n const step = this.isExteriorRing() ? 1 : -1\n const iStart = this.isExteriorRing() ? 0 : points.length - 1\n const iEnd = this.isExteriorRing() ? points.length : -1\n const orderedPoints: Ring = []\n for (let i = iStart; i != iEnd; i += step)\n orderedPoints.push([points[i].x.toNumber(), points[i].y.toNumber()])\n return orderedPoints\n }\n\n isExteriorRing(): boolean {\n if (this._isExteriorRing === undefined) {\n const enclosing = this.enclosingRing()\n this._isExteriorRing = enclosing ? !enclosing.isExteriorRing() : true\n }\n return this._isExteriorRing\n }\n\n enclosingRing() {\n if (this._enclosingRing === undefined) {\n this._enclosingRing = this._calcEnclosingRing()\n }\n return this._enclosingRing\n }\n\n /* Returns the ring that encloses this one, if any */\n _calcEnclosingRing(): RingOut | null | undefined {\n // start with the ealier sweep line event so that the prevSeg\n // chain doesn't lead us inside of a loop of ours\n let leftMostEvt = this.events[0]\n for (let i = 1, iMax = this.events.length; i < iMax; i++) {\n const evt = this.events[i]\n if (SweepEvent.compare(leftMostEvt, evt) > 0) leftMostEvt = evt\n }\n\n let prevSeg: Segment | null | undefined = leftMostEvt.segment.prevInResult()\n let prevPrevSeg: Segment | null | undefined = prevSeg ? prevSeg.prevInResult() : null\n\n while (true) {\n // no segment found, thus no ring can enclose us\n if (!prevSeg) return null\n\n // no segments below prev segment found, thus the ring of the prev\n // segment must loop back around and enclose us\n if (!prevPrevSeg) return prevSeg.ringOut\n\n // if the two segments are of different rings, the ring of the prev\n // segment must either loop around us or the ring of the prev prev\n // seg, which would make us and the ring of the prev peers\n if (prevPrevSeg.ringOut !== prevSeg.ringOut) {\n if (prevPrevSeg.ringOut?.enclosingRing() !== prevSeg.ringOut) {\n return prevSeg.ringOut\n } else return prevSeg.ringOut?.enclosingRing()\n }\n\n // two segments are from the same ring, so this was a penisula\n // of that ring. iterate downward, keep searching\n prevSeg = prevPrevSeg.prevInResult()\n prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null\n }\n }\n}\n\nexport class PolyOut {\n exteriorRing: RingOut;\n interiorRings: RingOut[];\n\n constructor(exteriorRing: RingOut) {\n this.exteriorRing = exteriorRing\n exteriorRing.poly = this\n this.interiorRings = []\n }\n\n addInterior(ring: RingOut) {\n this.interiorRings.push(ring)\n ring.poly = this\n }\n\n getGeom() {\n const geom0 = this.exteriorRing.getGeom()\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (geom0 === null) return null\n const geom: Poly = [geom0];\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringGeom = this.interiorRings[i].getGeom()\n // interior ring was all (within rounding error of angle calc) colinear points\n if (ringGeom === null) continue\n geom.push(ringGeom)\n }\n return geom\n }\n}\n\nexport class MultiPolyOut {\n rings: RingOut[];\n polys: PolyOut[];\n\n constructor(rings: RingOut[]) {\n this.rings = rings\n this.polys = this._composePolys(rings)\n }\n\n getGeom() {\n const geom: MultiPoly = []\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polyGeom = this.polys[i].getGeom()\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (polyGeom === null) continue\n geom.push(polyGeom)\n }\n return geom\n }\n\n _composePolys(rings: RingOut[]) {\n const polys = []\n for (let i = 0, iMax = rings.length; i < iMax; i++) {\n const ring = rings[i]\n if (ring.poly) continue\n if (ring.isExteriorRing()) polys.push(new PolyOut(ring))\n else {\n const enclosingRing = ring.enclosingRing()\n if (!enclosingRing?.poly) polys.push(new PolyOut(enclosingRing!))\n enclosingRing?.poly?.addInterior(ring)\n }\n }\n return polys\n }\n}\n", "import { SplayTreeSet } from \"splaytree-ts\"\nimport Segment from \"./segment.js\"\nimport SweepEvent, { Point } from \"./sweep-event.js\"\n\n/**\n * NOTE: We must be careful not to change any segments while\n * they are in the SplayTree. AFAIK, there's no way to tell\n * the tree to rebalance itself - thus before splitting\n * a segment that's in the tree, we remove it from the tree,\n * do the split, then re-insert it. (Even though splitting a\n * segment *shouldn't* change its correct position in the\n * sweep line tree, the reality is because of rounding errors,\n * it sometimes does.)\n */\n\nexport default class SweepLine {\n private queue: SplayTreeSet\n private tree: SplayTreeSet\n segments: Segment[]\n\n constructor(queue: SplayTreeSet, comparator = Segment.compare) {\n this.queue = queue\n this.tree = new SplayTreeSet(comparator)\n this.segments = []\n }\n\n process(event: SweepEvent) {\n const segment = event.segment\n const newEvents: SweepEvent[] = []\n\n // if we've already been consumed by another segment,\n // clean up our body parts and get out\n if (event.consumedBy) {\n if (event.isLeft) this.queue.delete(event.otherSE)\n else this.tree.delete(segment)\n return newEvents\n }\n\n if (event.isLeft) this.tree.add(segment);\n\n let prevSeg: Segment | null = segment\n let nextSeg: Segment | null = segment\n\n // skip consumed segments still in tree\n do {\n prevSeg = this.tree.lastBefore(prevSeg)\n } while (prevSeg != null && prevSeg.consumedBy != undefined)\n\n // skip consumed segments still in tree\n do {\n nextSeg = this.tree.firstAfter(nextSeg)\n } while (nextSeg != null && nextSeg.consumedBy != undefined)\n\n if (event.isLeft) {\n // Check for intersections against the previous segment in the sweep line\n let prevMySplitter = null\n if (prevSeg) {\n const prevInter = prevSeg.getIntersection(segment)\n if (prevInter !== null) {\n if (!segment.isAnEndpoint(prevInter)) prevMySplitter = prevInter\n if (!prevSeg.isAnEndpoint(prevInter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, prevInter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n }\n }\n\n // Check for intersections against the next segment in the sweep line\n let nextMySplitter = null\n if (nextSeg) {\n const nextInter = nextSeg.getIntersection(segment)\n if (nextInter !== null) {\n if (!segment.isAnEndpoint(nextInter)) nextMySplitter = nextInter\n if (!nextSeg.isAnEndpoint(nextInter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, nextInter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n }\n }\n\n // For simplicity, even if we find more than one intersection we only\n // spilt on the 'earliest' (sweep-line style) of the intersections.\n // The other intersection will be handled in a future process().\n if (prevMySplitter !== null || nextMySplitter !== null) {\n let mySplitter = null\n if (prevMySplitter === null) mySplitter = nextMySplitter\n else if (nextMySplitter === null) mySplitter = prevMySplitter\n else {\n const cmpSplitters = SweepEvent.comparePoints(\n prevMySplitter,\n nextMySplitter,\n )\n mySplitter = cmpSplitters <= 0 ? prevMySplitter : nextMySplitter\n }\n\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n this.queue.delete(segment.rightSE)\n newEvents.push(segment.rightSE)\n\n const newEventsFromSplit = segment.split(mySplitter!)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n\n if (newEvents.length > 0) {\n // We found some intersections, so re-do the current event to\n // make sure sweep line ordering is totally consistent for later\n // use with the segment 'prev' pointers\n this.tree.delete(segment)\n newEvents.push(event)\n } else {\n // done with left event\n this.segments.push(segment)\n segment.prev = prevSeg\n }\n } else {\n // event.isRight\n\n // since we're about to be removed from the sweep line, check for\n // intersections between our previous and next segments\n if (prevSeg && nextSeg) {\n const inter = prevSeg.getIntersection(nextSeg)\n if (inter !== null) {\n if (!prevSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, inter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n if (!nextSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, inter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n }\n }\n\n this.tree.delete(segment)\n }\n\n return newEvents\n }\n\n /* Safely split a segment that is currently in the datastructures\n * IE - a segment other than the one that is currently being processed. */\n _splitSafely(seg: Segment, pt: Point) {\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n // removeNode() doesn't work, so have re-find the seg\n // https://github.com/w8r/splay-tree/pull/5\n this.tree.delete(seg)\n const rightSE = seg.rightSE\n this.queue.delete(rightSE)\n const newEvents = seg.split(pt)\n newEvents.push(rightSE)\n // splitting can trigger consumption\n if (seg.consumedBy === undefined) this.tree.add(seg)\n return newEvents\n }\n}\n", "import { getBboxOverlap, isInBbox } from \"./bbox.js\"\nimport { MultiPolyIn, RingIn } from \"./geom-in.js\"\nimport { RingOut } from \"./geom-out.js\"\nimport operation from \"./operation.js\"\nimport { precision } from \"./precision.js\"\nimport SweepEvent, { Point } from \"./sweep-event.js\"\nimport { intersection } from \"./vector.js\"\n\ninterface State {\n rings: RingIn[],\n windings: number[],\n multiPolys: MultiPolyIn[]\n}\n\n// Give segments unique ID's to get consistent sorting of\n// segments and sweep events when all else is identical\nlet segmentId = 0\n\nexport default class Segment {\n id: number\n leftSE: SweepEvent\n rightSE: SweepEvent\n rings: RingIn[] | null\n windings: number[] | null\n ringOut: RingOut | undefined\n consumedBy: Segment | undefined\n prev: Segment | null | undefined\n _prevInResult: Segment | null | undefined\n _beforeState: State | undefined\n _afterState: State | undefined\n _isInResult: boolean | undefined\n\n /* This compare() function is for ordering segments in the sweep\n * line tree, and does so according to the following criteria:\n *\n * Consider the vertical line that lies an infinestimal step to the\n * right of the right-more of the two left endpoints of the input\n * segments. Imagine slowly moving a point up from negative infinity\n * in the increasing y direction. Which of the two segments will that\n * point intersect first? That segment comes 'before' the other one.\n *\n * If neither segment would be intersected by such a line, (if one\n * or more of the segments are vertical) then the line to be considered\n * is directly on the right-more of the two left inputs.\n */\n static compare(a: Segment, b: Segment) {\n const alx = a.leftSE.point.x\n const blx = b.leftSE.point.x\n const arx = a.rightSE.point.x\n const brx = b.rightSE.point.x\n\n // check if they're even in the same vertical plane\n if (brx.isLessThan(alx)) return 1\n if (arx.isLessThan(blx)) return -1\n\n const aly = a.leftSE.point.y\n const bly = b.leftSE.point.y\n const ary = a.rightSE.point.y\n const bry = b.rightSE.point.y\n\n // is left endpoint of segment B the right-more?\n if (alx.isLessThan(blx)) {\n // are the two segments in the same horizontal plane?\n if (bly.isLessThan(aly) && bly.isLessThan(ary)) return 1\n if (bly.isGreaterThan(aly) && bly.isGreaterThan(ary)) return -1\n\n // is the B left endpoint colinear to segment A?\n const aCmpBLeft = a.comparePoint(b.leftSE.point)\n if (aCmpBLeft < 0) return 1\n if (aCmpBLeft > 0) return -1\n\n // is the A right endpoint colinear to segment B ?\n const bCmpARight = b.comparePoint(a.rightSE.point)\n if (bCmpARight !== 0) return bCmpARight\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return -1\n }\n\n // is left endpoint of segment A the right-more?\n if (alx.isGreaterThan(blx)) {\n if (aly.isLessThan(bly) && aly.isLessThan(bry)) return -1\n if (aly.isGreaterThan(bly) && aly.isGreaterThan(bry)) return 1\n\n // is the A left endpoint colinear to segment B?\n const bCmpALeft = b.comparePoint(a.leftSE.point)\n if (bCmpALeft !== 0) return bCmpALeft\n\n // is the B right endpoint colinear to segment A?\n const aCmpBRight = a.comparePoint(b.rightSE.point)\n if (aCmpBRight < 0) return 1\n if (aCmpBRight > 0) return -1\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return 1\n }\n\n // if we get here, the two left endpoints are in the same\n // vertical plane, ie alx === blx\n\n // consider the lower left-endpoint to come first\n if (aly.isLessThan(bly)) return -1\n if (aly.isGreaterThan(bly)) return 1\n\n // left endpoints are identical\n // check for colinearity by using the left-more right endpoint\n\n // is the A right endpoint more left-more?\n if (arx.isLessThan(brx)) {\n const bCmpARight = b.comparePoint(a.rightSE.point)\n if (bCmpARight !== 0) return bCmpARight\n }\n\n // is the B right endpoint more left-more?\n if (arx.isGreaterThan(brx)) {\n const aCmpBRight = a.comparePoint(b.rightSE.point)\n if (aCmpBRight < 0) return 1\n if (aCmpBRight > 0) return -1\n }\n\n if (!arx.eq(brx)) {\n // are these two [almost] vertical segments with opposite orientation?\n // if so, the one with the lower right endpoint comes first\n const ay = ary.minus(aly)\n const ax = arx.minus(alx)\n const by = bry.minus(bly)\n const bx = brx.minus(blx)\n if (ay.isGreaterThan(ax) && by.isLessThan(bx)) return 1\n if (ay.isLessThan(ax) && by.isGreaterThan(bx)) return -1\n }\n\n // we have colinear segments with matching orientation\n // consider the one with more left-more right endpoint to be first\n if (arx.isGreaterThan(brx)) return 1\n if (arx.isLessThan(brx)) return -1\n\n // if we get here, two two right endpoints are in the same\n // vertical plane, ie arx === brx\n\n // consider the lower right-endpoint to come first\n if (ary.isLessThan(bry)) return -1\n if (ary.isGreaterThan(bry)) return 1\n\n // right endpoints identical as well, so the segments are idential\n // fall back on creation order as consistent tie-breaker\n if (a.id < b.id) return -1\n if (a.id > b.id) return 1\n\n // identical segment, ie a === b\n return 0\n }\n\n /* Warning: a reference to ringWindings input will be stored,\n * and possibly will be later modified */\n constructor(leftSE: SweepEvent, rightSE: SweepEvent, rings: RingIn[], windings: number[]) {\n this.id = ++segmentId\n this.leftSE = leftSE\n leftSE.segment = this\n leftSE.otherSE = rightSE\n this.rightSE = rightSE\n rightSE.segment = this\n rightSE.otherSE = leftSE\n this.rings = rings\n this.windings = windings\n // left unset for performance, set later in algorithm\n // this.ringOut, this.consumedBy, this.prev\n }\n\n static fromRing(pt1: Point, pt2: Point, ring: RingIn) {\n let leftPt: Point, rightPt: Point, winding: number\n\n // ordering the two points according to sweep line ordering\n const cmpPts = SweepEvent.comparePoints(pt1, pt2)\n if (cmpPts < 0) {\n leftPt = pt1\n rightPt = pt2\n winding = 1\n } else if (cmpPts > 0) {\n leftPt = pt2\n rightPt = pt1\n winding = -1\n } else\n throw new Error(\n `Tried to create degenerate segment at [${pt1.x}, ${pt1.y}]`,\n )\n\n const leftSE = new SweepEvent(leftPt, true)\n const rightSE = new SweepEvent(rightPt, false)\n return new Segment(leftSE, rightSE, [ring], [winding])\n }\n\n /* When a segment is split, the rightSE is replaced with a new sweep event */\n replaceRightSE(newRightSE: SweepEvent) {\n this.rightSE = newRightSE\n this.rightSE.segment = this\n this.rightSE.otherSE = this.leftSE\n this.leftSE.otherSE = this.rightSE\n }\n\n bbox() {\n const y1 = this.leftSE.point.y\n const y2 = this.rightSE.point.y\n return {\n ll: { x: this.leftSE.point.x, y: y1.isLessThan(y2) ? y1 : y2 },\n ur: { x: this.rightSE.point.x, y: y1.isGreaterThan(y2) ? y1 : y2 },\n }\n }\n\n /* A vector from the left point to the right */\n vector() {\n return {\n x: this.rightSE.point.x.minus(this.leftSE.point.x),\n y: this.rightSE.point.y.minus(this.leftSE.point.y),\n }\n }\n\n isAnEndpoint(pt: Point) {\n return (\n (pt.x.eq(this.leftSE.point.x) && pt.y.eq(this.leftSE.point.y)) ||\n (pt.x.eq(this.rightSE.point.x) && pt.y.eq(this.rightSE.point.y))\n )\n }\n\n /* Compare this segment with a point.\n *\n * A point P is considered to be colinear to a segment if there\n * exists a distance D such that if we travel along the segment\n * from one * endpoint towards the other a distance D, we find\n * ourselves at point P.\n *\n * Return value indicates:\n *\n * 1: point lies above the segment (to the left of vertical)\n * 0: point is colinear to segment\n * -1: point lies below the segment (to the right of vertical)\n */\n comparePoint(point: Point) {\n return precision.orient(this.leftSE.point, point, this.rightSE.point)\n }\n\n /**\n * Given another segment, returns the first non-trivial intersection\n * between the two segments (in terms of sweep line ordering), if it exists.\n *\n * A 'non-trivial' intersection is one that will cause one or both of the\n * segments to be split(). As such, 'trivial' vs. 'non-trivial' intersection:\n *\n * * endpoint of segA with endpoint of segB --> trivial\n * * endpoint of segA with point along segB --> non-trivial\n * * endpoint of segB with point along segA --> non-trivial\n * * point along segA with point along segB --> non-trivial\n *\n * If no non-trivial intersection exists, return null\n * Else, return null.\n */\n getIntersection(other: Segment) {\n // If bboxes don't overlap, there can't be any intersections\n const tBbox = this.bbox()\n const oBbox = other.bbox()\n const bboxOverlap = getBboxOverlap(tBbox, oBbox)\n if (bboxOverlap === null) return null\n\n // We first check to see if the endpoints can be considered intersections.\n // This will 'snap' intersections to endpoints if possible, and will\n // handle cases of colinearity.\n\n const tlp = this.leftSE.point\n const trp = this.rightSE.point\n const olp = other.leftSE.point\n const orp = other.rightSE.point\n\n // does each endpoint touch the other segment?\n // note that we restrict the 'touching' definition to only allow segments\n // to touch endpoints that lie forward from where we are in the sweep line pass\n const touchesOtherLSE = isInBbox(tBbox, olp) && this.comparePoint(olp) === 0\n const touchesThisLSE = isInBbox(oBbox, tlp) && other.comparePoint(tlp) === 0\n const touchesOtherRSE = isInBbox(tBbox, orp) && this.comparePoint(orp) === 0\n const touchesThisRSE = isInBbox(oBbox, trp) && other.comparePoint(trp) === 0\n\n // do left endpoints match?\n if (touchesThisLSE && touchesOtherLSE) {\n // these two cases are for colinear segments with matching left\n // endpoints, and one segment being longer than the other\n if (touchesThisRSE && !touchesOtherRSE) return trp\n if (!touchesThisRSE && touchesOtherRSE) return orp\n // either the two segments match exactly (two trival intersections)\n // or just on their left endpoint (one trivial intersection\n return null\n }\n\n // does this left endpoint matches (other doesn't)\n if (touchesThisLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesOtherRSE) {\n if (tlp.x.eq(orp.x) && tlp.y.eq(orp.y)) return null\n }\n // t-intersection on left endpoint\n return tlp\n }\n\n // does other left endpoint matches (this doesn't)\n if (touchesOtherLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesThisRSE) {\n if (trp.x.eq(olp.x) && trp.y.eq(olp.y)) return null\n }\n // t-intersection on left endpoint\n return olp\n }\n\n // trivial intersection on right endpoints\n if (touchesThisRSE && touchesOtherRSE) return null\n\n // t-intersections on just one right endpoint\n if (touchesThisRSE) return trp\n if (touchesOtherRSE) return orp\n\n // None of our endpoints intersect. Look for a general intersection between\n // infinite lines laid over the segments\n const pt = intersection(tlp, this.vector(), olp, other.vector())\n\n // are the segments parrallel? Note that if they were colinear with overlap,\n // they would have an endpoint intersection and that case was already handled above\n if (pt === null) return null\n\n // is the intersection found between the lines not on the segments?\n if (!isInBbox(bboxOverlap, pt)) return null\n\n // round the the computed point if needed\n return precision.snap(pt) as Point\n }\n\n /**\n * Split the given segment into multiple segments on the given points.\n * * Each existing segment will retain its leftSE and a new rightSE will be\n * generated for it.\n * * A new segment will be generated which will adopt the original segment's\n * rightSE, and a new leftSE will be generated for it.\n * * If there are more than two points given to split on, new segments\n * in the middle will be generated with new leftSE and rightSE's.\n * * An array of the newly generated SweepEvents will be returned.\n *\n * Warning: input array of points is modified\n */\n split(point: Point) {\n const newEvents = []\n const alreadyLinked = point.events !== undefined\n\n const newLeftSE = new SweepEvent(point, true)\n const newRightSE = new SweepEvent(point, false)\n const oldRightSE = this.rightSE\n this.replaceRightSE(newRightSE)\n newEvents.push(newRightSE)\n newEvents.push(newLeftSE)\n const newSeg = new Segment(\n newLeftSE,\n oldRightSE,\n this.rings!.slice(),\n this.windings!.slice(),\n )\n\n // when splitting a nearly vertical downward-facing segment,\n // sometimes one of the resulting new segments is vertical, in which\n // case its left and right events may need to be swapped\n if (\n SweepEvent.comparePoints(newSeg.leftSE.point, newSeg.rightSE.point) > 0\n ) {\n newSeg.swapEvents()\n }\n if (SweepEvent.comparePoints(this.leftSE.point, this.rightSE.point) > 0) {\n this.swapEvents()\n }\n\n // in the point we just used to create new sweep events with was already\n // linked to other events, we need to check if either of the affected\n // segments should be consumed\n if (alreadyLinked) {\n newLeftSE.checkForConsuming()\n newRightSE.checkForConsuming()\n }\n\n return newEvents\n }\n\n /* Swap which event is left and right */\n swapEvents() {\n const tmpEvt = this.rightSE\n this.rightSE = this.leftSE\n this.leftSE = tmpEvt\n this.leftSE.isLeft = true\n this.rightSE.isLeft = false\n for (let i = 0, iMax = this.windings!.length; i < iMax; i++) {\n this.windings![i] *= -1\n }\n }\n\n /* Consume another segment. We take their rings under our wing\n * and mark them as consumed. Use for perfectly overlapping segments */\n consume(other: Segment) {\n let consumer = this as Segment\n let consumee = other\n while (consumer.consumedBy) consumer = consumer.consumedBy\n while (consumee.consumedBy) consumee = consumee.consumedBy\n\n const cmp = Segment.compare(consumer, consumee)\n if (cmp === 0) return // already consumed\n // the winner of the consumption is the earlier segment\n // according to sweep line ordering\n if (cmp > 0) {\n const tmp = consumer\n consumer = consumee\n consumee = tmp\n }\n\n // make sure a segment doesn't consume it's prev\n if (consumer.prev === consumee) {\n const tmp = consumer\n consumer = consumee\n consumee = tmp\n }\n\n for (let i = 0, iMax = consumee.rings!.length; i < iMax; i++) {\n const ring = consumee.rings![i]\n const winding = consumee.windings![i]\n const index = consumer.rings!.indexOf(ring)\n if (index === -1) {\n consumer.rings!.push(ring)\n consumer.windings!.push(winding)\n } else consumer.windings![index] += winding\n }\n consumee.rings = null\n consumee.windings = null\n consumee.consumedBy = consumer\n\n // mark sweep events consumed as to maintain ordering in sweep event queue\n consumee.leftSE.consumedBy = consumer.leftSE\n consumee.rightSE.consumedBy = consumer.rightSE\n }\n\n /* The first segment previous segment chain that is in the result */\n prevInResult(): Segment | null | undefined {\n if (this._prevInResult !== undefined) return this._prevInResult\n if (!this.prev) this._prevInResult = null\n else if (this.prev.isInResult()) this._prevInResult = this.prev\n else this._prevInResult = this.prev.prevInResult()\n return this._prevInResult\n }\n\n beforeState(): State {\n if (this._beforeState !== undefined) return this._beforeState\n if (!this.prev)\n this._beforeState = {\n rings: [],\n windings: [],\n multiPolys: [],\n }\n else {\n const seg = this.prev.consumedBy || this.prev\n this._beforeState = seg.afterState()\n }\n return this._beforeState\n }\n\n afterState() {\n if (this._afterState !== undefined) return this._afterState\n\n const beforeState = this.beforeState()\n this._afterState = {\n rings: beforeState.rings.slice(0),\n windings: beforeState.windings.slice(0),\n multiPolys: [],\n }\n const ringsAfter = this._afterState.rings\n const windingsAfter = this._afterState.windings\n const mpsAfter = this._afterState.multiPolys\n\n // calculate ringsAfter, windingsAfter\n for (let i = 0, iMax = this.rings!.length; i < iMax; i++) {\n const ring = this.rings![i]\n const winding = this.windings![i]\n const index = ringsAfter.indexOf(ring)\n if (index === -1) {\n ringsAfter.push(ring)\n windingsAfter.push(winding)\n } else windingsAfter[index] += winding\n }\n\n // calcualte polysAfter\n const polysAfter = []\n const polysExclude = []\n for (let i = 0, iMax = ringsAfter.length; i < iMax; i++) {\n if (windingsAfter[i] === 0) continue // non-zero rule\n const ring = ringsAfter[i]\n const poly = ring.poly\n if (polysExclude.indexOf(poly) !== -1) continue\n if (ring.isExterior) polysAfter.push(poly)\n else {\n if (polysExclude.indexOf(poly) === -1) polysExclude.push(poly)\n const index = polysAfter.indexOf(ring.poly)\n if (index !== -1) polysAfter.splice(index, 1)\n }\n }\n\n // calculate multiPolysAfter\n for (let i = 0, iMax = polysAfter.length; i < iMax; i++) {\n const mp = polysAfter[i].multiPoly\n if (mpsAfter.indexOf(mp) === -1) mpsAfter.push(mp)\n }\n\n return this._afterState\n }\n\n /* Is this segment part of the final result? */\n isInResult() {\n // if we've been consumed, we're not in the result\n if (this.consumedBy) return false\n\n if (this._isInResult !== undefined) return this._isInResult\n\n const mpsBefore = this.beforeState().multiPolys\n const mpsAfter = this.afterState().multiPolys\n\n switch (operation.type) {\n case \"union\": {\n // UNION - included iff:\n // * On one side of us there is 0 poly interiors AND\n // * On the other side there is 1 or more.\n const noBefores = mpsBefore.length === 0\n const noAfters = mpsAfter.length === 0\n this._isInResult = noBefores !== noAfters\n break\n }\n\n case \"intersection\": {\n // INTERSECTION - included iff:\n // * on one side of us all multipolys are rep. with poly interiors AND\n // * on the other side of us, not all multipolys are repsented\n // with poly interiors\n let least\n let most\n if (mpsBefore.length < mpsAfter.length) {\n least = mpsBefore.length\n most = mpsAfter.length\n } else {\n least = mpsAfter.length\n most = mpsBefore.length\n }\n this._isInResult = most === operation.numMultiPolys && least < most\n break\n }\n\n case \"xor\": {\n // XOR - included iff:\n // * the difference between the number of multipolys represented\n // with poly interiors on our two sides is an odd number\n const diff = Math.abs(mpsBefore.length - mpsAfter.length)\n this._isInResult = diff % 2 === 1\n break\n }\n\n case \"difference\": {\n // DIFFERENCE included iff:\n // * on exactly one side, we have just the subject\n const isJustSubject = (mps: MultiPolyIn[]) => mps.length === 1 && mps[0].isSubject\n this._isInResult = isJustSubject(mpsBefore) !== isJustSubject(mpsAfter)\n break\n }\n }\n\n return this._isInResult\n }\n}\n", "import { Geom } from \"./geom-in.js\"\nimport { precision } from \"./precision.js\"\nimport operation from \"./operation.js\"\n\nexport { Geom }\n\nexport const union = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"union\", geom, moreGeoms)\n\nexport const intersection = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"intersection\", geom, moreGeoms)\n\nexport const xor = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"xor\", geom, moreGeoms)\n\nexport const difference = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"difference\", geom, moreGeoms)\n\nexport const setPrecision = precision.set", "module.exports.RADIUS = 6378137;\nmodule.exports.FLATTENING = 1/298.257223563;\nmodule.exports.POLAR_RADIUS = 6356752.3142;\n", "var wgs84 = require('wgs84');\n\nmodule.exports.geometry = geometry;\nmodule.exports.ring = ringArea;\n\nfunction geometry(_) {\n var area = 0, i;\n switch (_.type) {\n case 'Polygon':\n return polygonArea(_.coordinates);\n case 'MultiPolygon':\n for (i = 0; i < _.coordinates.length; i++) {\n area += polygonArea(_.coordinates[i]);\n }\n return area;\n case 'Point':\n case 'MultiPoint':\n case 'LineString':\n case 'MultiLineString':\n return 0;\n case 'GeometryCollection':\n for (i = 0; i < _.geometries.length; i++) {\n area += geometry(_.geometries[i]);\n }\n return area;\n }\n}\n\nfunction polygonArea(coords) {\n var area = 0;\n if (coords && coords.length > 0) {\n area += Math.abs(ringArea(coords[0]));\n for (var i = 1; i < coords.length; i++) {\n area -= Math.abs(ringArea(coords[i]));\n }\n }\n return area;\n}\n\n/**\n * Calculate the approximate area of the polygon were it projected onto\n * the earth. Note that this area will be positive if ring is oriented\n * clockwise, otherwise it will be negative.\n *\n * Reference:\n * Robert. G. Chamberlain and William H. Duquette, \"Some Algorithms for\n * Polygons on a Sphere\", JPL Publication 07-03, Jet Propulsion\n * Laboratory, Pasadena, CA, June 2007 http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409\n *\n * Returns:\n * {float} The approximate signed geodesic area of the polygon in square\n * meters.\n */\n\nfunction ringArea(coords) {\n var p1, p2, p3, lowerIndex, middleIndex, upperIndex, i,\n area = 0,\n coordsLength = coords.length;\n\n if (coordsLength > 2) {\n for (i = 0; i < coordsLength; i++) {\n if (i === coordsLength - 2) {// i = N-2\n lowerIndex = coordsLength - 2;\n middleIndex = coordsLength -1;\n upperIndex = 0;\n } else if (i === coordsLength - 1) {// i = N-1\n lowerIndex = coordsLength - 1;\n middleIndex = 0;\n upperIndex = 1;\n } else { // i = 0 to N-3\n lowerIndex = i;\n middleIndex = i+1;\n upperIndex = i+2;\n }\n p1 = coords[lowerIndex];\n p2 = coords[middleIndex];\n p3 = coords[upperIndex];\n area += ( rad(p3[0]) - rad(p1[0]) ) * Math.sin( rad(p2[1]));\n }\n\n area = area * wgs84.RADIUS * wgs84.RADIUS / 2;\n }\n\n return area;\n}\n\nfunction rad(_) {\n return _ * Math.PI / 180;\n}", "exports.validateCenter = function validateCenter(center) {\n var validCenterLengths = [2, 3];\n if (!Array.isArray(center) || !validCenterLengths.includes(center.length)) {\n throw new Error(\"ERROR! Center has to be an array of length two or three\");\n }\n\n var [lng, lat] = center;\n if (typeof lng !== \"number\" || typeof lat !== \"number\") {\n throw new Error(\n `ERROR! Longitude and Latitude has to be numbers but where ${typeof lng} and ${typeof lat}`\n );\n }\n if (lng > 180 || lng < -180) {\n throw new Error(`ERROR! Longitude has to be between -180 and 180 but was ${lng}`);\n }\n\n if (lat > 90 || lat < -90) {\n throw new Error(`ERROR! Latitude has to be between -90 and 90 but was ${lat}`);\n }\n};\n", "exports.validateRadius = function validateRadius(radius) {\n if (typeof radius !== \"number\") {\n throw new Error(`ERROR! Radius has to be a positive number but was: ${typeof radius}`);\n }\n\n if (radius <= 0) {\n throw new Error(`ERROR! Radius has to be a positive number but was: ${radius}`);\n }\n};\n", "exports.validateNumberOfEdges = function validateNumberOfEdges(numberOfEdges) {\n if (typeof numberOfEdges !== \"number\") {\n const ARGUMENT_TYPE = Array.isArray(numberOfEdges) ? \"array\" : typeof numberOfEdges;\n throw new Error(`ERROR! Number of edges has to be a number but was: ${ARGUMENT_TYPE}`);\n }\n\n if (numberOfEdges < 3) {\n throw new Error(`ERROR! Number of edges has to be at least 3 but was: ${numberOfEdges}`);\n }\n};\n", "exports.validateEarthRadius = function validateEarthRadius(earthRadius) {\n if (typeof earthRadius !== \"number\") {\n const ARGUMENT_TYPE = Array.isArray(earthRadius) ? \"array\" : typeof earthRadius;\n throw new Error(`ERROR! Earth radius has to be a number but was: ${ARGUMENT_TYPE}`);\n }\n\n if (earthRadius <= 0) {\n throw new Error(`ERROR! Earth radius has to be a positive number but was: ${earthRadius}`);\n }\n};\n", "exports.validateBearing = function validateBearing(bearing) {\n if (typeof bearing !== \"number\") {\n const ARGUMENT_TYPE = Array.isArray(bearing) ? \"array\" : typeof bearing;\n throw new Error(`ERROR! Bearing has to be a number but was: ${ARGUMENT_TYPE}`);\n }\n};\n", "var validateCenter = require(\"./validateCenter\").validateCenter;\nvar validateRadius = require(\"./validateRadius\").validateRadius;\nvar validateNumberOfEdges = require(\"./validateNumberOfEdges\").validateNumberOfEdges;\nvar validateEarthRadius = require(\"./validateEarthRadius\").validateEarthRadius;\nvar validateBearing = require(\"./validateBearing\").validateBearing;\n\nfunction validateInput({ center, radius, numberOfEdges, earthRadius, bearing }) {\n validateCenter(center);\n validateRadius(radius);\n validateNumberOfEdges(numberOfEdges);\n validateEarthRadius(earthRadius);\n validateBearing(bearing);\n}\n\nexports.validateCenter = validateCenter;\nexports.validateRadius = validateRadius;\nexports.validateNumberOfEdges = validateNumberOfEdges;\nexports.validateEarthRadius = validateEarthRadius;\nexports.validateBearing = validateBearing;\nexports.validateInput = validateInput;\n", "\"use strict\";\nvar { validateInput } = require(\"./input-validation\");\n\nconst defaultEarthRadius = 6378137; // equatorial Earth radius\n\nfunction toRadians(angleInDegrees) {\n return (angleInDegrees * Math.PI) / 180;\n}\n\nfunction toDegrees(angleInRadians) {\n return (angleInRadians * 180) / Math.PI;\n}\n\nfunction offset(c1, distance, earthRadius, bearing) {\n var lat1 = toRadians(c1[1]);\n var lon1 = toRadians(c1[0]);\n var dByR = distance / earthRadius;\n var lat = Math.asin(\n Math.sin(lat1) * Math.cos(dByR) + Math.cos(lat1) * Math.sin(dByR) * Math.cos(bearing)\n );\n var lon =\n lon1 +\n Math.atan2(\n Math.sin(bearing) * Math.sin(dByR) * Math.cos(lat1),\n Math.cos(dByR) - Math.sin(lat1) * Math.sin(lat)\n );\n return [toDegrees(lon), toDegrees(lat)];\n}\n\nmodule.exports = function circleToPolygon(center, radius, options) {\n var n = getNumberOfEdges(options);\n var earthRadius = getEarthRadius(options);\n var bearing = getBearing(options);\n var direction = getDirection(options);\n\n // validateInput() throws error on invalid input and do nothing on valid input\n validateInput({ center, radius, numberOfEdges: n, earthRadius, bearing });\n\n var start = toRadians(bearing);\n var coordinates = [];\n for (var i = 0; i < n; ++i) {\n coordinates.push(\n offset(\n center, radius, earthRadius, start + (direction * 2 * Math.PI * -i) / n\n )\n );\n }\n coordinates.push(coordinates[0]);\n\n return {\n type: \"Polygon\",\n coordinates: [coordinates]\n };\n};\n\nfunction getNumberOfEdges(options) {\n if (isUndefinedOrNull(options)) {\n return 32;\n } else if (isObjectNotArray(options)) {\n var numberOfEdges = options.numberOfEdges;\n return numberOfEdges === undefined ? 32 : numberOfEdges;\n }\n return options;\n}\n\nfunction getEarthRadius(options) {\n if (isUndefinedOrNull(options)) {\n return defaultEarthRadius;\n } else if (isObjectNotArray(options)) {\n var earthRadius = options.earthRadius;\n return earthRadius === undefined ? defaultEarthRadius : earthRadius;\n }\n return defaultEarthRadius;\n}\n\nfunction getDirection(options){\n if (isObjectNotArray(options) && options.rightHandRule){\n return -1;\n }\n return 1;\n}\n\nfunction getBearing(options) {\n if (isUndefinedOrNull(options)) {\n return 0;\n } else if (isObjectNotArray(options)) {\n var bearing = options.bearing;\n return bearing === undefined ? 0 : bearing;\n }\n return 0;\n}\n\nfunction isObjectNotArray(argument) {\n return argument !== null && typeof argument === \"object\" && !Array.isArray(argument);\n}\n\nfunction isUndefinedOrNull(argument) {\n return argument === null || argument === undefined;\n}\n", "(function() {\n\n function parse(t, coordinatePrecision, extrasPrecision) {\n\n function point(p) {\n return p.map(function(e, index) {\n if (index < 2) {\n return 1 * e.toFixed(coordinatePrecision);\n } else {\n return 1 * e.toFixed(extrasPrecision);\n }\n });\n }\n\n function multi(l) {\n return l.map(point);\n }\n\n function poly(p) {\n return p.map(multi);\n }\n\n function multiPoly(m) {\n return m.map(poly);\n }\n\n function geometry(obj) {\n if (!obj) {\n return {};\n }\n \n switch (obj.type) {\n case \"Point\":\n obj.coordinates = point(obj.coordinates);\n return obj;\n case \"LineString\":\n case \"MultiPoint\":\n obj.coordinates = multi(obj.coordinates);\n return obj;\n case \"Polygon\":\n case \"MultiLineString\":\n obj.coordinates = poly(obj.coordinates);\n return obj;\n case \"MultiPolygon\":\n obj.coordinates = multiPoly(obj.coordinates);\n return obj;\n case \"GeometryCollection\":\n obj.geometries = obj.geometries.map(geometry);\n return obj;\n default :\n return {};\n }\n }\n\n function feature(obj) {\n obj.geometry = geometry(obj.geometry);\n return obj\n }\n\n function featureCollection(f) {\n f.features = f.features.map(feature);\n return f;\n }\n\n function geometryCollection(g) {\n g.geometries = g.geometries.map(geometry);\n return g;\n }\n\n if (!t) {\n return t;\n }\n\n switch (t.type) {\n case \"Feature\":\n return feature(t);\n case \"GeometryCollection\" :\n return geometryCollection(t);\n case \"FeatureCollection\" :\n return featureCollection(t);\n case \"Point\":\n case \"LineString\":\n case \"Polygon\":\n case \"MultiPoint\":\n case \"MultiPolygon\":\n case \"MultiLineString\":\n return geometry(t);\n default :\n return t;\n }\n \n }\n\n module.exports = parse;\n module.exports.parse = parse;\n\n}());\n \n", "// Note: This regex matches even invalid JSON strings, but since we\u2019re\n// working on the output of `JSON.stringify` we know that only valid strings\n// are present (unless the user supplied a weird `options.indent` but in\n// that case we don\u2019t care since the output would be invalid anyway).\nconst stringOrChar = /(\"(?:[^\\\\\"]|\\\\.)*\")|[:,]/g;\n\nexport default function stringify(passedObj, options = {}) {\n const indent = JSON.stringify(\n [1],\n undefined,\n options.indent === undefined ? 2 : options.indent\n ).slice(2, -3);\n\n const maxLength =\n indent === \"\"\n ? Infinity\n : options.maxLength === undefined\n ? 80\n : options.maxLength;\n\n let { replacer } = options;\n\n return (function _stringify(obj, currentIndent, reserved) {\n if (obj && typeof obj.toJSON === \"function\") {\n obj = obj.toJSON();\n }\n\n const string = JSON.stringify(obj, replacer);\n\n if (string === undefined) {\n return string;\n }\n\n const length = maxLength - currentIndent.length - reserved;\n\n if (string.length <= length) {\n const prettified = string.replace(\n stringOrChar,\n (match, stringLiteral) => {\n return stringLiteral || `${match} `;\n }\n );\n if (prettified.length <= length) {\n return prettified;\n }\n }\n\n if (replacer != null) {\n obj = JSON.parse(string);\n replacer = undefined;\n }\n\n if (typeof obj === \"object\" && obj !== null) {\n const nextIndent = currentIndent + indent;\n const items = [];\n let index = 0;\n let start;\n let end;\n\n if (Array.isArray(obj)) {\n start = \"[\";\n end = \"]\";\n const { length } = obj;\n for (; index < length; index++) {\n items.push(\n _stringify(obj[index], nextIndent, index === length - 1 ? 0 : 1) ||\n \"null\"\n );\n }\n } else {\n start = \"{\";\n end = \"}\";\n const keys = Object.keys(obj);\n const { length } = keys;\n for (; index < length; index++) {\n const key = keys[index];\n const keyPart = `${JSON.stringify(key)}: `;\n const value = _stringify(\n obj[key],\n nextIndent,\n keyPart.length + (index === length - 1 ? 0 : 1)\n );\n if (value !== undefined) {\n items.push(keyPart + value);\n }\n }\n }\n\n if (items.length > 0) {\n return [start, indent + items.join(`,\\n${nextIndent}`), end].join(\n `\\n${currentIndent}`\n );\n }\n }\n\n return string;\n })(passedObj, \"\", 0);\n}\n", "import * as CountryCoder from '@rapideditor/country-coder';\nimport * as Polyclip from 'polyclip-ts';\nimport calcArea from '@mapbox/geojson-area';\nimport circleToPolygon from 'circle-to-polygon';\nimport precision from 'geojson-precision';\nimport prettyStringify from 'json-stringify-pretty-compact';\nimport type { Polygon, MultiPolygon } from 'geojson';\n\n// Type definitions\nexport type Vec2 = [number, number];\nexport type Vec3 = [number, number, number];\nexport type Location = Vec2 | Vec3 | string | number;\n\nexport interface FeatureProperties {\n id: string;\n area: number;\n members?: string[];\n [key: string]: unknown;\n}\n\nexport type GeoJSONGeometry = Polygon | MultiPolygon;\n\nexport interface GeoJSONFeature {\n type: 'Feature';\n id: string;\n properties: FeatureProperties;\n geometry: GeoJSONGeometry;\n}\n\nexport interface FeatureCollection {\n type: 'FeatureCollection';\n features: GeoJSONFeature[];\n}\n\nexport interface LocationSet {\n include?: Location[];\n exclude?: Location[];\n}\n\nexport interface ValidatedLocation {\n type: 'point' | 'geojson' | 'countrycoder';\n location: Location;\n id: string;\n}\n\nexport interface ResolvedLocation extends ValidatedLocation {\n feature: GeoJSONFeature;\n}\n\nexport interface ValidatedLocationSet {\n type: 'locationset';\n locationSet: LocationSet;\n id: string;\n}\n\nexport interface ResolvedLocationSet extends ValidatedLocationSet {\n feature: GeoJSONFeature;\n}\n\nexport type ClipOperation = 'UNION' | 'DIFFERENCE';\n\nexport type StringifyOptions = Parameters[1];\n\nexport class LocationConflation {\n public _cache: Map;\n public strict: boolean;\n\n /**\n * Creates a new LocationConflation instance\n * @param fc - Optional FeatureCollection of known features with filename-like IDs (e.g., \"something.geojson\")\n */\n constructor(fc?: FeatureCollection) {\n // The _cache retains resolved features, so if you ask for the same thing multiple times\n // we don't repeat the expensive resolving/clipping operations.\n //\n // Each feature has a stable identifier that is used as the cache key.\n // The identifiers look like:\n // - for point locations, the stringified point: e.g. '[8.67039,49.41882]'\n // - for geojson locations, the geojson id: e.g. 'de-hamburg.geojson'\n // - for countrycoder locations, feature.id property: e.g. 'Q2' (countrycoder uses Wikidata identifiers)\n // - for aggregated locationSets, +[include]-[exclude]: e.g '+[Q2]-[Q18,Q27611]'\n this._cache = new Map();\n\n // When strict mode = true, throw on invalid locations or locationSets.\n // When strict mode = false, return `null` for invalid locations or locationSets.\n this.strict = true;\n\n // process input FeatureCollection\n if (fc?.type === 'FeatureCollection' && Array.isArray(fc.features)) {\n for (const feature of fc.features) {\n feature.properties = feature.properties || ({} as FeatureProperties);\n const props = feature.properties;\n\n // Get `id` from either `id` or `properties`\n let id = feature.id || props.id;\n if (!id || !/^\\S+\\.geojson$/i.test(id)) continue;\n\n // Ensure `id` exists and is lowercase\n id = id.toLowerCase();\n feature.id = id;\n props.id = id;\n\n // Ensure `area` property exists\n if (!props.area) {\n const area = calcArea.geometry(feature.geometry) / 1e6; // m² to km²\n props.area = Number(area.toFixed(2));\n }\n\n this._cache.set(id, feature);\n }\n }\n\n // Replace CountryCoder world geometry to be a polygon covering the world.\n const worldFeature = CountryCoder.feature('Q2');\n const world = cloneDeep(worldFeature) as unknown as GeoJSONFeature;\n world.geometry = {\n type: 'Polygon',\n coordinates: [[[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]]],\n };\n world.id = 'Q2';\n world.properties.id = 'Q2';\n world.properties.area = calcArea.geometry(world.geometry) / 1e6; // m² to km²\n this._cache.set('Q2', world);\n }\n\n /**\n * Validates a location and returns its type and stable identifier\n * @param location - Location to validate (point, geojson filename, or country code)\n * @returns Validated location object or null if invalid\n *\n * @example\n * ```typescript\n * // Point location with default 25km radius\n * loco.validateLocation([8.67039, 49.41882]);\n * // => { type: 'point', location: [8.67039, 49.41882], id: '[8.67039,49.41882]' }\n *\n * // Point location with custom radius\n * loco.validateLocation([-77.0369, 38.9072, 10]);\n * // => { type: 'point', location: [-77.0369, 38.9072, 10], id: '[-77.0369,38.9072,10]' }\n *\n * // Country code\n * loco.validateLocation('de');\n * // => { type: 'countrycoder', location: 'de', id: 'Q183' }\n *\n * // GeoJSON file\n * loco.validateLocation('philly_metro.geojson');\n * // => { type: 'geojson', location: 'philly_metro.geojson', id: 'philly_metro.geojson' }\n * ```\n */\n validateLocation(location: Location): ValidatedLocation | null {\n // [lon, lat] or [lon, lat, radius] point?\n if (Array.isArray(location) && (location.length === 2 || location.length === 3)) {\n const lon = location[0];\n const lat = location[1];\n const radius = location[2];\n\n if (\n Number.isFinite(lon) &&\n lon >= -180 &&\n lon <= 180 &&\n Number.isFinite(lat) &&\n lat >= -90 &&\n lat <= 90 &&\n (location.length === 2 || (radius !== undefined && Number.isFinite(radius) && radius > 0))\n ) {\n const id = '[' + location.toString() + ']';\n return { type: 'point', location, id };\n }\n } else if (typeof location === 'string' && /^\\S+\\.geojson$/i.test(location)) {\n // a .geojson filename?\n const id = location.toLowerCase();\n if (this._cache.has(id)) {\n return { type: 'geojson', location, id };\n }\n } else if (typeof location === 'string' || typeof location === 'number') {\n // a country-coder value?\n const feature = CountryCoder.feature(location);\n if (feature) {\n // Use wikidata QID as the identifier, since that seems to be the one\n // property that everything in CountryCoder is guaranteed to have.\n const id = feature.properties.wikidata;\n return { type: 'countrycoder', location, id };\n }\n }\n\n if (this.strict) {\n throw new Error(`validateLocation: Invalid location: \"${location}\".`);\n }\n return null;\n }\n\n /**\n * Resolves a location to a GeoJSON feature\n * @param location - Location to resolve\n * @returns Resolved location with GeoJSON feature or null if invalid\n *\n * @example\n * ```typescript\n * // Resolve a point location to a circular polygon\n * const result = loco.resolveLocation([8.67039, 49.41882]);\n * // result.feature is a GeoJSON Feature with a circular Polygon geometry\n *\n * // Resolve a country code\n * const germany = loco.resolveLocation('de');\n * // germany.feature is a GeoJSON Feature with Germany's boundary\n *\n * // Resolve a custom GeoJSON file\n * const metro = loco.resolveLocation('philly_metro.geojson');\n * // metro.feature is the pre-loaded GeoJSON Feature\n * ```\n */\n resolveLocation(location: Location): ResolvedLocation | null {\n const valid = this.validateLocation(location);\n if (!valid) return null;\n\n const id = valid.id;\n\n // Return a result from cache if we can\n if (this._cache.has(id)) {\n return { ...valid, feature: this._cache.get(id)! };\n }\n\n // A [lon,lat] coordinate pair?\n if (valid.type === 'point' && Array.isArray(location)) {\n const lon = location[0];\n const lat = location[1];\n const radius = location[2] || 25; // km\n const EDGES = 10;\n const PRECISION = 3;\n const area = Math.PI * radius * radius;\n const feature = precision(\n {\n type: 'Feature',\n id,\n properties: { id, area: Number(area.toFixed(2)) },\n geometry: circleToPolygon([lon, lat], radius * 1000, EDGES), // km to m\n },\n PRECISION\n ) as GeoJSONFeature;\n this._cache.set(id, feature);\n return { ...valid, feature };\n }\n\n // A .geojson filename?\n if (valid.type === 'geojson') {\n // nothing to do here - these are all in _cache and would have returned already\n }\n\n // A country-coder identifier?\n if (valid.type === 'countrycoder') {\n const ccFeature = CountryCoder.feature(id);\n const feature = cloneDeep(ccFeature) as unknown as GeoJSONFeature;\n const props = feature.properties;\n\n // -> This block of code is weird and requires some explanation. <-\n // CountryCoder includes higher level features which are made up of members.\n // These features don't have their own geometry, but CountryCoder provides an\n // `aggregateFeature` method to combine these members into a MultiPolygon.\n // In the past, Turf/JSTS/martinez could not handle the aggregated features,\n // so we'd iteratively union them all together. (this was slow)\n // But now mfogel/polygon-clipping handles these MultiPolygons like a boss.\n // This approach also has the benefit of removing all the internal borders and\n // simplifying the regional polygons a lot.\n if (Array.isArray(props.members)) {\n const aggregate = CountryCoder.aggregateFeature(id);\n if (aggregate) {\n const clipped = clip([aggregate as unknown as GeoJSONFeature], 'UNION');\n if (clipped) {\n feature.geometry = clipped.geometry;\n }\n }\n }\n\n // Ensure `area` property exists\n if (!props.area) {\n const area = calcArea.geometry(feature.geometry) / 1e6; // m² to km²\n props.area = Number(area.toFixed(2));\n }\n\n // Ensure `id` property exists\n feature.id = id;\n props.id = id;\n\n this._cache.set(id, feature);\n return { ...valid, feature };\n }\n\n if (this.strict) {\n throw new Error(`resolveLocation: Couldn't resolve location \"${location}\".`);\n }\n return null;\n }\n\n /**\n * Validates a locationSet and returns its stable identifier\n * @param locationSet - LocationSet with include/exclude arrays\n * @returns Validated locationSet object or null if invalid\n *\n * @example\n * ```typescript\n * // Include multiple countries\n * loco.validateLocationSet({ include: ['de', 'fr', 'it'] });\n * // => { type: 'locationset', locationSet: {...}, id: '+[Q183,Q142,Q38]' }\n *\n * // Include with exclusions\n * loco.validateLocationSet({\n * include: ['de'],\n * exclude: ['de-berlin.geojson']\n * });\n * // => { type: 'locationset', locationSet: {...}, id: '+[Q183]-[de-berlin.geojson]' }\n *\n * // Mix different location types\n * loco.validateLocationSet({\n * include: ['us', [8.67039, 49.41882], 'philly_metro.geojson']\n * });\n * ```\n */\n validateLocationSet(locationSet?: LocationSet): ValidatedLocationSet {\n locationSet = locationSet || {};\n const validator = this.validateLocation.bind(this);\n let include = (locationSet.include || []).map(validator).filter(Boolean) as ValidatedLocation[];\n const exclude = (locationSet.exclude || []).map(validator).filter(Boolean) as ValidatedLocation[];\n\n if (!include.length) {\n if (this.strict) {\n throw new Error('validateLocationSet: LocationSet includes nothing.');\n } else {\n // non-strict mode, replace an empty locationSet with one that includes \"the world\"\n locationSet.include = ['Q2'];\n include = [{ type: 'countrycoder', location: 'Q2', id: 'Q2' }];\n }\n }\n\n // Generate stable identifier\n include.sort(sortLocations);\n let id = '+[' + include.map((d) => d.id).join(',') + ']';\n if (exclude.length) {\n exclude.sort(sortLocations);\n id += '-[' + exclude.map((d) => d.id).join(',') + ']';\n }\n\n return { type: 'locationset', locationSet, id };\n }\n\n /**\n * Resolves a locationSet to a GeoJSON feature by combining included/excluded regions\n * @param locationSet - LocationSet with include/exclude arrays\n * @returns Resolved locationSet with GeoJSON feature or null if invalid\n *\n * @example\n * ```typescript\n * // Combine multiple countries into one feature\n * const benelux = loco.resolveLocationSet({\n * include: ['be', 'nl', 'lu']\n * });\n * // benelux.feature is a GeoJSON Feature with combined boundaries\n *\n * // Germany excluding Berlin\n * const germanyNoCapital = loco.resolveLocationSet({\n * include: ['de'],\n * exclude: ['de-berlin.geojson']\n * });\n * // Result is Germany with Berlin cut out\n *\n * // Complex region definition\n * const customRegion = loco.resolveLocationSet({\n * include: ['us-ca', 'us-or', 'us-wa'],\n * exclude: [[8.67039, 49.41882, 50]]\n * });\n * // West coast states minus a 50km circle\n * ```\n */\n resolveLocationSet(locationSet?: LocationSet): ResolvedLocationSet | null {\n locationSet = locationSet || {};\n const valid = this.validateLocationSet(locationSet);\n if (!valid) return null;\n\n const id = valid.id;\n\n // Return a result from cache if we can\n if (this._cache.has(id)) {\n return { ...valid, feature: this._cache.get(id)! };\n }\n\n const resolver = this.resolveLocation.bind(this);\n const includes = (locationSet.include || []).map(resolver).filter(Boolean) as ResolvedLocation[];\n const excludes = (locationSet.exclude || []).map(resolver).filter(Boolean) as ResolvedLocation[];\n\n // Return quickly if it's a single included location..\n if (includes.length === 1 && excludes.length === 0) {\n return { ...valid, feature: includes[0].feature };\n }\n\n // Calculate unions\n const includeGeoJSON = clip(includes.map((d) => d.feature), 'UNION')!;\n const excludeGeoJSON = clip(excludes.map((d) => d.feature), 'UNION');\n\n // Calculate difference, update `area` and return result\n const resultGeoJSON = excludeGeoJSON ? clip([includeGeoJSON, excludeGeoJSON], 'DIFFERENCE')! : includeGeoJSON;\n const area = calcArea.geometry(resultGeoJSON.geometry) / 1e6; // m² to km²\n resultGeoJSON.id = id;\n resultGeoJSON.properties = { id, area: Number(area.toFixed(2)) };\n\n this._cache.set(id, resultGeoJSON);\n return { ...valid, feature: resultGeoJSON };\n }\n\n /**\n * Convenience method to pretty-stringify an object\n * @param obj - Object to stringify\n * @param options - Stringify options\n * @returns Pretty-formatted JSON string\n *\n * @example\n * ```typescript\n * const result = loco.resolveLocation('de');\n * console.log(loco.stringify(result.feature));\n * // Outputs a compact, readable JSON representation of the feature\n *\n * // Custom formatting options\n * console.log(loco.stringify(result.feature, { indent: 2, maxLength: 80 }));\n * ```\n */\n stringify(obj: unknown, options?: StringifyOptions): string {\n return prettyStringify(obj, options);\n }\n}\n\n/**\n * Wraps the polyclip-ts library and returns a GeoJSON feature\n * @param features - Array of features to clip\n * @param which - Operation type (UNION or DIFFERENCE)\n * @returns Clipped GeoJSON feature or null if features array is empty\n */\nfunction clip(features: GeoJSONFeature[], which: ClipOperation): GeoJSONFeature | null {\n if (!Array.isArray(features) || !features.length) return null;\n\n const fn = { UNION: Polyclip.union, DIFFERENCE: Polyclip.difference }[which];\n const args = features.map((feature) => feature.geometry.coordinates);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const coords = (fn as any)(...args);\n\n return {\n type: 'Feature',\n properties: {} as FeatureProperties,\n geometry: {\n type: whichType(coords),\n coordinates: coords,\n } as GeoJSONGeometry,\n id: '',\n };\n\n // is this a Polygon or a MultiPolygon?\n function whichType(coords: unknown): 'Polygon' | 'MultiPolygon' {\n const a = Array.isArray(coords);\n const b = a && Array.isArray(coords[0]);\n const c = b && Array.isArray(coords[0][0]);\n const d = c && Array.isArray(coords[0][0][0]);\n return d ? 'MultiPolygon' : 'Polygon';\n }\n}\n\n/**\n * Deep clones an object using JSON serialization\n * @param obj - Object to clone\n * @returns Cloned object\n */\nfunction cloneDeep(obj: T): T {\n return JSON.parse(JSON.stringify(obj));\n}\n\n/**\n * Sorting function for locations to generate deterministic IDs\n * Sorting the location lists is ok because they end up unioned together.\n * @param a - First location\n * @param b - Second location\n * @returns Sort order\n */\nfunction sortLocations(a: ValidatedLocation, b: ValidatedLocation): number {\n const rank = { countrycoder: 1, geojson: 2, point: 3 };\n const aRank = rank[a.type];\n const bRank = rank[b.type];\n\n return aRank > bRank ? 1 : aRank < bRank ? -1 : a.id.localeCompare(b.id);\n}\n\nexport default LocationConflation;\n", "import { LocationConflation, type LocationSet, type GeoJSONFeature, type FeatureProperties, type FeatureCollection } from '@rapideditor/location-conflation';\nimport whichPolygon from 'which-polygon';\nimport calcArea from '@mapbox/geojson-area';\nimport type { Vec2 } from '../geo/vector';\n\nexport interface ObjectWithLocationSet {\n locationSetID?: string;\n locationSet: LocationSet;\n}\n\nconst _loco = new LocationConflation(); // instance of a location-conflation resolver\n\n\n/**\n * `LocationManager` maintains an internal index of all the boundaries/geofences used by iD.\n * It's used by presets, community index, background imagery, to know where in the world these things are valid.\n * These geofences should be defined by `locationSet` objects:\n *\n * let locationSet = {\n * include: [ Array of locations ],\n * exclude: [ Array of locations ]\n * };\n *\n * For more info see the location-conflation and country-coder projects, see:\n * https://github.com/rapideditor/location-conflation\n * https://github.com/rapideditor/country-coder\n */\nexport class LocationManager {\n /** A which-polygon index */\n _wp!: whichPolygon.Query;\n /** Map (id -> GeoJSON feature) */\n _resolved = new Map();\n /** Map (locationSetID -> Number area) */\n _knownLocationSets = new Map();\n /** Map (locationID -> Set(locationSetID) ) */\n _locationIncludedIn = new Map>();\n /** Map (locationID -> Set(locationSetID) ) */\n _locationExcludedIn = new Map>();\n\n /**\n * @constructor\n */\n constructor() {\n // pre-resolve the worldwide locationSet\n const world = { locationSet: { include: ['Q2'] } };\n this._resolveLocationSet(world);\n this._rebuildIndex();\n }\n\n\n /**\n * _validateLocationSet\n * Pass an Object with a `locationSet` property.\n * Validates the `locationSet` and sets a `locationSetID` property on the object.\n * To avoid so much computation we only resolve the include and exclude regions, but not the locationSet itself.\n *\n * Use `_resolveLocationSet()` instead if you need to resolve geojson of locationSet, for example to render it.\n * Note: You need to call `_rebuildIndex()` after you're all finished validating the locationSets.\n *\n * @param `obj` Object to check, it should have `locationSet` property\n */\n _validateLocationSet(obj: ObjectWithLocationSet) {\n if (obj.locationSetID) return; // work was done already\n\n try {\n let locationSet = obj.locationSet;\n if (!locationSet) {\n throw new Error('object missing locationSet property');\n }\n if (!locationSet.include) { // missing `include`, default to worldwide include\n locationSet.include = ['Q2']; // https://github.com/openstreetmap/iD/pull/8305#discussion_r662344647\n }\n\n // Validate the locationSet only\n // Resolve the include/excludes\n const locationSetID = _loco.validateLocationSet(locationSet).id;\n obj.locationSetID = locationSetID;\n if (this._knownLocationSets.has(locationSetID)) return; // seen one like this before\n\n let area = 0;\n\n // Resolve and index the 'includes'\n (locationSet.include || []).forEach(location => {\n const locationID = _loco.validateLocation(location)!.id;\n let geojson = this._resolved.get(locationID);\n\n if (!geojson) { // first time seeing a location like this\n geojson = _loco.resolveLocation(location)!.feature; // resolve to GeoJSON\n this._resolved.set(locationID, geojson);\n }\n area += geojson.properties.area;\n\n let s = this._locationIncludedIn.get(locationID);\n if (!s) {\n s = new Set();\n this._locationIncludedIn.set(locationID, s);\n }\n s.add(locationSetID);\n });\n\n // Resolve and index the 'excludes'\n (locationSet.exclude || []).forEach(location => {\n const locationID = _loco.validateLocation(location)!.id;\n let geojson = this._resolved.get(locationID);\n\n if (!geojson) { // first time seeing a location like this\n geojson = _loco.resolveLocation(location)!.feature; // resolve to GeoJSON\n this._resolved.set(locationID, geojson);\n }\n area -= geojson.properties.area;\n\n let s = this._locationExcludedIn.get(locationID);\n if (!s) {\n s = new Set();\n this._locationExcludedIn.set(locationID, s);\n }\n s.add(locationSetID);\n });\n\n this._knownLocationSets.set(locationSetID, area);\n\n } catch {\n obj.locationSet = { include: ['Q2'] }; // default worldwide\n obj.locationSetID = '+[Q2]';\n }\n }\n\n\n /**\n * _resolveLocationSet\n * Does everything that `_validateLocationSet()` does, but then \"resolves\" the locationSet into GeoJSON.\n * This step is a bit more computationally expensive, so really only needed if you intend to render the shape.\n *\n * Note: You need to call `_rebuildIndex()` after you're all finished validating the locationSets.\n *\n * @param `obj` Object to check, it should have `locationSet` property\n */\n _resolveLocationSet(obj: ObjectWithLocationSet) {\n this._validateLocationSet(obj);\n\n if (this._resolved.has(obj.locationSetID)) return; // work was done already\n\n try {\n const result = _loco.resolveLocationSet(obj.locationSet)!;\n const locationSetID = result.id;\n obj.locationSetID = locationSetID;\n\n if (!result.feature.geometry.coordinates.length || !result.feature.properties.area) {\n throw new Error(`locationSet ${locationSetID} resolves to an empty feature.`);\n }\n\n let geojson = JSON.parse(JSON.stringify(result.feature)); // deep clone\n geojson.id = locationSetID; // Important: always use the locationSet `id` (`+[Q30]`), not the feature `id` (`Q30`)\n geojson.properties.id = locationSetID;\n this._resolved.set(locationSetID, geojson);\n\n } catch {\n obj.locationSet = { include: ['Q2'] }; // default worldwide\n obj.locationSetID = '+[Q2]';\n }\n }\n\n\n /**\n * _rebuildIndex\n * Rebuilds the whichPolygon index with whatever features have been resolved into GeoJSON.\n */\n _rebuildIndex() {\n this._wp = whichPolygon({\n type: 'FeatureCollection',\n features: [...this._resolved.values()],\n });\n }\n\n\n /**\n * mergeCustomGeoJSON\n * Accepts a FeatureCollection-like object containing custom locations\n * Each feature must have a filename-like `id`, for example: `something.geojson`\n * {\n * \"type\": \"FeatureCollection\"\n * \"features\": [\n * {\n * \"type\": \"Feature\",\n * \"id\": \"philly_metro.geojson\",\n * \"properties\": { \u2026 },\n * \"geometry\": { \u2026 }\n * }\n * ]\n * }\n *\n * @param `fc` FeatureCollection-like Object containing custom locations\n */\n mergeCustomGeoJSON(fc: FeatureCollection) {\n if (!fc || fc.type !== 'FeatureCollection' || !Array.isArray(fc.features)) return;\n\n fc.features.forEach(feature => {\n feature.properties = feature.properties || {};\n let props = feature.properties;\n\n // Get `id` from either `id` or `properties`\n let id = feature.id || props.id;\n if (!id || !/^\\S+\\.geojson$/i.test(id)) return;\n\n // Ensure `id` exists and is lowercase\n id = id.toLowerCase();\n feature.id = id;\n props.id = id;\n\n // Ensure `area` property exists\n if (!props.area) {\n const area = calcArea.geometry(feature.geometry) / 1e6; // m\u00B2 to km\u00B2\n props.area = Number(area.toFixed(2));\n }\n\n _loco._cache.set(id, feature); // insert directly into LocationConflations internal cache\n });\n }\n\n\n /**\n * mergeLocationSets\n * Accepts an Array of Objects containing `locationSet` properties:\n * [\n * { id: 'preset1', locationSet: {\u2026} },\n * { id: 'preset2', locationSet: {\u2026} },\n * \u2026\n * ]\n * After validating, the Objects will be decorated with a `locationSetID` property:\n * [\n * { id: 'preset1', locationSet: {\u2026}, locationSetID: '+[Q2]' },\n * { id: 'preset2', locationSet: {\u2026}, locationSetID: '+[Q30]' },\n * \u2026\n * ]\n *\n * @param `objects` Objects to check - they should have `locationSet` property\n * @return Promise resolved true (this function used to be slow/async, now it's faster and sync)\n */\n mergeLocationSets(objects: ObjectWithLocationSet[]) {\n if (!Array.isArray(objects)) return Promise.reject('nothing to do');\n\n objects.forEach(obj => this._validateLocationSet(obj));\n this._rebuildIndex();\n return Promise.resolve(objects);\n }\n\n\n /**\n * locationSetID\n * Returns a locationSetID for a given locationSet (fallback to `+[Q2]`, world)\n * (The locationSet doesn't necessarily need to be resolved to compute its `id`)\n *\n * @param `locationSet` A locationSet Object, e.g. `{ include: ['us'] }`\n * @return String locationSetID, e.g. `+[Q30]`\n */\n locationSetID(locationSet: LocationSet) {\n let locationSetID;\n try {\n locationSetID = _loco.validateLocationSet(locationSet).id;\n } catch {\n locationSetID = '+[Q2]'; // the world\n }\n return locationSetID;\n }\n\n\n /**\n * feature\n * Returns the resolved GeoJSON feature for a given locationSetID (fallback to 'world')\n * A GeoJSON feature:\n * {\n * type: 'Feature',\n * id: '+[Q30]',\n * properties: { id: '+[Q30]', area: 21817019.17, \u2026 },\n * geometry: { \u2026 }\n * }\n *\n * @param `locationSetID` String identifier, e.g. `+[Q30]`\n * @return GeoJSON object (fallback to world)\n */\n feature(locationSetID = '+[Q2]') {\n const feature = this._resolved.get(locationSetID);\n return feature || this._resolved.get('+[Q2]');\n }\n\n\n /**\n * locationSetsAt\n * Find all the locationSets valid at the given location.\n * Results include the area (in km\u00B2) to facilitate sorting.\n *\n * Object of locationSetIDs to areas (in km\u00B2)\n * {\n * \"+[Q2]\": 511207893.3958111,\n * \"+[Q30]\": 21817019.17,\n * \"+[new_jersey.geojson]\": 22390.77,\n * \u2026\n * }\n *\n * @param `loc` `[lon,lat]` location to query, e.g. `[-74.4813, 40.7967]`\n * @return Object of locationSetIDs valid at given location\n */\n locationSetsAt(loc: Vec2) {\n const result: { [locationSetID: string]: number } = {};\n\n const hits = this._wp(loc, true) || [];\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n const thiz = this;\n\n // locationSets\n hits.forEach(prop => {\n if (prop.id[0] !== '+') return; // skip - it's a location\n const locationSetID = prop.id;\n const area = thiz._knownLocationSets.get(locationSetID);\n if (area) {\n result[locationSetID] = area;\n }\n });\n\n // locations included\n hits.forEach(prop => {\n if (prop.id[0] === '+') return; // skip - it's a locationset\n const locationID = prop.id;\n const included = thiz._locationIncludedIn.get(locationID);\n (included || []).forEach(locationSetID => {\n const area = thiz._knownLocationSets.get(locationSetID);\n if (area) {\n result[locationSetID] = area;\n }\n });\n });\n\n // locations excluded\n hits.forEach(prop => {\n if (prop.id[0] === '+') return; // skip - it's a locationset\n const locationID = prop.id;\n const excluded = thiz._locationExcludedIn.get(locationID);\n (excluded || []).forEach(locationSetID => {\n delete result[locationSetID];\n });\n });\n\n return result;\n }\n\n\n // Direct access to the location-conflation resolver\n loco() {\n return _loco;\n }\n}\n\n\nconst _sharedLocationManager = new LocationManager();\nexport { _sharedLocationManager as locationManager };\n\n", "import { t, localizer } from '../../core/localizer';\nimport { geoSphericalDistance, geoVecNormalizedDot } from '../../geo';\nimport { uiCmd } from '../cmd';\n\nexport function pointBox(loc, context) {\n var rect = context.surfaceRect();\n var point = context.curtainProjection(loc);\n return {\n left: point[0] + rect.left - 40,\n top: point[1] + rect.top - 60,\n width: 80,\n height: 90\n };\n}\n\n\nexport function pad(locOrBox, padding, context) {\n var box;\n if (locOrBox instanceof Array) {\n var rect = context.surfaceRect();\n var point = context.curtainProjection(locOrBox);\n box = {\n left: point[0] + rect.left,\n top: point[1] + rect.top\n };\n } else {\n box = locOrBox;\n }\n\n return {\n left: box.left - padding,\n top: box.top - padding,\n width: (box.width || 0) + 2 * padding,\n height: (box.width || 0) + 2 * padding\n };\n}\n\n\nexport function icon(name, svgklass, useklass) {\n return '' +\n '';\n}\n\nvar helpStringReplacements;\n\n// Returns the localized HTML element for `id` with a standardized set of icon, key, and\n// label replacements suitable for tutorials and documentation. Optionally supplemented\n// with custom `replacements`\nexport function helpHtml(id, replacements) {\n // only load these the first time\n if (!helpStringReplacements) {\n /* eslint-disable sort-keys */\n helpStringReplacements = {\n // insert icons corresponding to various UI elements\n point_icon: icon('#iD-icon-point', 'inline'),\n line_icon: icon('#iD-icon-line', 'inline'),\n area_icon: icon('#iD-icon-area', 'inline'),\n note_icon: icon('#iD-icon-note', 'inline add-note'),\n plus: icon('#iD-icon-plus', 'inline'),\n minus: icon('#iD-icon-minus', 'inline'),\n layers_icon: icon('#iD-icon-layers', 'inline'),\n data_icon: icon('#iD-icon-data', 'inline'),\n inspect: icon('#iD-icon-inspect', 'inline'),\n help_icon: icon('#iD-icon-help', 'inline'),\n undo_icon: icon(localizer.textDirection() === 'rtl' ? '#iD-icon-redo' : '#iD-icon-undo', 'inline'),\n redo_icon: icon(localizer.textDirection() === 'rtl' ? '#iD-icon-undo' : '#iD-icon-redo', 'inline'),\n save_icon: icon('#iD-icon-save', 'inline'),\n\n // operation icons\n circularize_icon: icon('#iD-operation-circularize', 'inline operation'),\n continue_icon: icon('#iD-operation-continue', 'inline operation'),\n copy_icon: icon('#iD-operation-copy', 'inline operation'),\n delete_icon: icon('#iD-operation-delete', 'inline operation'),\n disconnect_icon: icon('#iD-operation-disconnect', 'inline operation'),\n downgrade_icon: icon('#iD-operation-downgrade', 'inline operation'),\n extract_icon: icon('#iD-operation-extract', 'inline operation'),\n merge_icon: icon('#iD-operation-merge', 'inline operation'),\n move_icon: icon('#iD-operation-move', 'inline operation'),\n orthogonalize_icon: icon('#iD-operation-orthogonalize', 'inline operation'),\n paste_icon: icon('#iD-operation-paste', 'inline operation'),\n reflect_long_icon: icon('#iD-operation-reflect-long', 'inline operation'),\n reflect_short_icon: icon('#iD-operation-reflect-short', 'inline operation'),\n reverse_icon: icon('#iD-operation-reverse', 'inline operation'),\n rotate_icon: icon('#iD-operation-rotate', 'inline operation'),\n split_icon: icon('#iD-operation-split', 'inline operation'),\n straighten_icon: icon('#iD-operation-straighten', 'inline operation'),\n\n // interaction icons\n leftclick: icon('#iD-walkthrough-mouse-left', 'inline operation'),\n rightclick: icon('#iD-walkthrough-mouse-right', 'inline operation'),\n mousewheel_icon: icon('#iD-walkthrough-mousewheel', 'inline operation'),\n tap_icon: icon('#iD-walkthrough-tap', 'inline operation'),\n doubletap_icon: icon('#iD-walkthrough-doubletap', 'inline operation'),\n longpress_icon: icon('#iD-walkthrough-longpress', 'inline operation'),\n touchdrag_icon: icon('#iD-walkthrough-touchdrag', 'inline operation'),\n pinch_icon: icon('#iD-walkthrough-pinch-apart', 'inline operation'),\n\n // insert keys; may be localized and platform-dependent\n shift: uiCmd.display('\u21E7'),\n alt: uiCmd.display('\u2325'),\n return: uiCmd.display('\u21B5'),\n esc: t.html('shortcuts.key.esc'),\n space: t.html('shortcuts.key.space'),\n add_note_key: t.html('modes.add_note.key'),\n help_key: t.html('help.key'),\n shortcuts_key: t.html('shortcuts.toggle.key'),\n\n // reference localized UI labels directly so that they'll always match\n save: t.html('save.title'),\n undo: t.html('undo.title'),\n redo: t.html('redo.title'),\n upload: t.html('commit.save'),\n point: t.html('modes.add_point.title'),\n line: t.html('modes.add_line.title'),\n area: t.html('modes.add_area.title'),\n note: t.html('modes.add_note.label'),\n\n circularize: t.html('operations.circularize.title'),\n continue: t.html('operations.continue.title'),\n copy: t.html('operations.copy.title'),\n delete: t.html('operations.delete.title'),\n disconnect: t.html('operations.disconnect.title'),\n downgrade: t.html('operations.downgrade.title'),\n extract: t.html('operations.extract.title'),\n merge: t.html('operations.merge.title'),\n move: t.html('operations.move.title'),\n orthogonalize: t.html('operations.orthogonalize.title'),\n paste: t.html('operations.paste.title'),\n reflect_long: t.html('operations.reflect.title.long'),\n reflect_short: t.html('operations.reflect.title.short'),\n reverse: t.html('operations.reverse.title'),\n rotate: t.html('operations.rotate.title'),\n split: t.html('operations.split.title'),\n straighten: t.html('operations.straighten.title'),\n\n map_data: t.html('map_data.title'),\n osm_notes: t.html('map_data.layers.notes.title'),\n fields: t.html('inspector.fields'),\n tags: t.html('inspector.tags'),\n relations: t.html('inspector.relations'),\n new_relation: t.html('inspector.new_relation'),\n turn_restrictions: t.html('_tagging.presets.fields.restrictions.label'),\n background_settings: t.html('background.description'),\n imagery_offset: t.html('background.fix_misalignment'),\n start_the_walkthrough: t.html('splash.walkthrough'),\n help: t.html('help.title'),\n ok: t.html('intro.ok')\n };\n /* eslint-enable sort-keys */\n for (var key in helpStringReplacements) {\n helpStringReplacements[key] = { html: helpStringReplacements[key] };\n }\n }\n\n var reps;\n if (replacements) {\n reps = Object.assign(replacements, helpStringReplacements);\n } else {\n reps = helpStringReplacements;\n }\n\n return t.html(id, reps)\n // use keyboard key styling for shortcuts\n .replace(/\\`(.*?)\\`/g, '$1');\n}\n\n\nfunction slugify(text) {\n return text.toString().toLowerCase()\n .replace(/\\s+/g, '-') // Replace spaces with -\n .replace(/[^\\w\\-]+/g, '') // Remove all non-word chars\n .replace(/\\-\\-+/g, '-') // Replace multiple - with single -\n .replace(/^-+/, '') // Trim - from start of text\n .replace(/-+$/, ''); // Trim - from end of text\n}\n\n\n// console warning for missing walkthrough names\nexport var missingStrings = {};\nfunction checkKey(key, text) {\n if (t(key, { default: undefined}) === undefined) {\n if (missingStrings.hasOwnProperty(key)) return; // warn once\n missingStrings[key] = text;\n var missing = key + ': ' + text;\n if (typeof console !== 'undefined') console.log(missing); // eslint-disable-line\n }\n}\n\n\nexport function localize(obj) {\n var key;\n\n // Assign name if entity has one..\n var name = obj.tags && obj.tags.name;\n if (name) {\n key = 'intro.graph.name.' + slugify(name);\n obj.tags.name = t(key, { default: name });\n checkKey(key, name);\n }\n\n // Assign street name if entity has one..\n var street = obj.tags && obj.tags['addr:street'];\n if (street) {\n key = 'intro.graph.name.' + slugify(street);\n obj.tags['addr:street'] = t(key, { default: street });\n checkKey(key, street);\n\n // Add address details common across walkthrough..\n var addrTags = [\n 'block_number', 'city', 'county', 'district', 'hamlet', 'neighbourhood',\n 'postcode', 'province', 'quarter', 'state', 'subdistrict', 'suburb'\n ];\n addrTags.forEach(function(k) {\n var key = 'intro.graph.' + k;\n var tag = 'addr:' + k;\n var val = obj.tags && obj.tags[tag];\n var str = t(key, { default: val });\n\n if (str) {\n if (str.match(/^<.*>$/) !== null) {\n delete obj.tags[tag];\n } else {\n obj.tags[tag] = str;\n }\n }\n });\n }\n\n return obj;\n}\n\n\n// Used to detect squareness.. some duplicataion of code from actionOrthogonalize.\nexport function isMostlySquare(points) {\n // note: uses 15 here instead of the 12 from actionOrthogonalize because\n // actionOrthogonalize can actually straighten some larger angles as it iterates\n var threshold = 15; // degrees within right or straight\n var lowerBound = Math.cos((90 - threshold) * Math.PI / 180); // near right\n var upperBound = Math.cos(threshold * Math.PI / 180); // near straight\n\n for (var i = 0; i < points.length; i++) {\n var a = points[(i - 1 + points.length) % points.length];\n var origin = points[i];\n var b = points[(i + 1) % points.length];\n\n var dotp = geoVecNormalizedDot(a, b, origin);\n var mag = Math.abs(dotp);\n if (mag > lowerBound && mag < upperBound) {\n return false;\n }\n }\n\n return true;\n}\n\n\nexport function selectMenuItem(context, operation) {\n return context.container().select('.edit-menu .edit-menu-item-' + operation);\n}\n\n\nexport function transitionTime(point1, point2) {\n var distance = geoSphericalDistance(point1, point2);\n if (distance === 0) {\n return 0;\n } else if (distance < 80) {\n return 500;\n } else {\n return 1000;\n }\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { marked } from 'marked';\nimport { t, localizer } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { icon } from './intro/helper';\n\n\n// This currently only works with the 'restrictions' field\n// It borrows some code from uiHelp\n\nexport function uiFieldHelp(context, fieldName) {\n var fieldHelp = {};\n var _inspector = d3_select(null);\n var _wrap = d3_select(null);\n var _body = d3_select(null);\n\n var fieldHelpKeys = {\n restrictions: [\n ['about',[\n 'about',\n 'from_via_to',\n 'maxdist',\n 'maxvia'\n ]],\n ['inspecting',[\n 'about',\n 'from_shadow',\n 'allow_shadow',\n 'restrict_shadow',\n 'only_shadow',\n 'restricted',\n 'only'\n ]],\n ['modifying',[\n 'about',\n 'indicators',\n 'allow_turn',\n 'restrict_turn',\n 'only_turn'\n ]],\n ['tips',[\n 'simple',\n 'simple_example',\n 'indirect',\n 'indirect_example',\n 'indirect_noedit'\n ]]\n ]\n };\n\n var fieldHelpHeadings = {};\n\n var replacements = {\n distField: { html: t.html('restriction.controls.distance') },\n viaField: { html: t.html('restriction.controls.via') },\n fromShadow: { html: icon('#iD-turn-shadow', 'inline shadow from') },\n allowShadow: { html: icon('#iD-turn-shadow', 'inline shadow allow') },\n restrictShadow: { html: icon('#iD-turn-shadow', 'inline shadow restrict') },\n onlyShadow: { html: icon('#iD-turn-shadow', 'inline shadow only') },\n allowTurn: { html: icon('#iD-turn-yes', 'inline turn') },\n restrictTurn: { html: icon('#iD-turn-no', 'inline turn') },\n onlyTurn: { html: icon('#iD-turn-only', 'inline turn') }\n };\n\n\n // For each section, squash all the texts into a single markdown document\n var docs = fieldHelpKeys[fieldName].map(function(key) {\n var helpkey = 'help.field.' + fieldName + '.' + key[0];\n var text = key[1].reduce(function(all, part) {\n var subkey = helpkey + '.' + part;\n var depth = fieldHelpHeadings[subkey]; // is this subkey a heading?\n var hhh = depth ? Array(depth + 1).join('#') + ' ' : ''; // if so, prepend with some ##'s\n return all + hhh + t.html(subkey, replacements) + '\\n\\n';\n }, '');\n\n return {\n key: helpkey,\n title: t.html(helpkey + '.title'),\n html: marked(text.trim())\n };\n });\n\n\n function show() {\n updatePosition();\n\n _body\n .classed('hide', false)\n .style('opacity', '0')\n .transition()\n .duration(200)\n .style('opacity', '1');\n }\n\n\n function hide() {\n _body\n .classed('hide', true)\n .transition()\n .duration(200)\n .style('opacity', '0')\n .on('end', function () {\n _body.classed('hide', true);\n });\n }\n\n\n function clickHelp(index) {\n var d = docs[index];\n var tkeys = fieldHelpKeys[fieldName][index][1];\n\n _body.selectAll('.field-help-nav-item')\n .classed('active', function(d, i) { return i === index; });\n\n var content = _body.selectAll('.field-help-content')\n .html(d.html);\n\n // class the paragraphs so we can find and style them\n content.selectAll('p')\n .attr('class', function(d, i) { return tkeys[i]; });\n\n // insert special content for certain help sections\n if (d.key === 'help.field.restrictions.inspecting') {\n content\n .insert('img', 'p.from_shadow')\n .attr('class', 'field-help-image cf')\n .attr('src', context.imagePath('tr_inspect.gif'));\n\n } else if (d.key === 'help.field.restrictions.modifying') {\n content\n .insert('img', 'p.allow_turn')\n .attr('class', 'field-help-image cf')\n .attr('src', context.imagePath('tr_modify.gif'));\n }\n }\n\n\n fieldHelp.button = function(selection) {\n if (_body.empty()) return;\n\n var button = selection.selectAll('.field-help-button')\n .data([0]);\n\n // enter/update\n button.enter()\n .append('button')\n .attr('class', 'field-help-button')\n .call(svgIcon('#iD-icon-help'))\n .merge(button)\n .on('click', function (d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n if (_body.classed('hide')) {\n show();\n } else {\n hide();\n }\n });\n };\n\n\n function updatePosition() {\n var wrap = _wrap.node();\n var inspector = _inspector.node();\n var wRect = wrap.getBoundingClientRect();\n var iRect = inspector.getBoundingClientRect();\n\n _body\n .style('top', wRect.top + inspector.scrollTop - iRect.top + 'px');\n }\n\n\n fieldHelp.body = function(selection) {\n // This control expects the field to have a form-field-input-wrap div\n _wrap = selection.selectAll('.form-field-input-wrap');\n if (_wrap.empty()) return;\n\n // absolute position relative to the inspector, so it \"floats\" above the fields\n _inspector = context.container().select('.sidebar .entity-editor-pane .inspector-body');\n if (_inspector.empty()) return;\n\n _body = _inspector.selectAll('.field-help-body')\n .data([0]);\n\n var enter = _body.enter()\n .append('div')\n .attr('class', 'field-help-body hide'); // initially hidden\n\n var titleEnter = enter\n .append('div')\n .attr('class', 'field-help-title cf');\n\n titleEnter\n .append('h2')\n .attr('class', ((localizer.textDirection() === 'rtl') ? 'fr' : 'fl'))\n .call(t.append('help.field.' + fieldName + '.title'));\n\n titleEnter\n .append('button')\n .attr('class', 'fr close')\n .attr('title', t('icons.close'))\n .on('click', function(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n hide();\n })\n .call(svgIcon('#iD-icon-close'));\n\n var navEnter = enter\n .append('div')\n .attr('class', 'field-help-nav cf');\n\n var titles = docs.map(function(d) { return d.title; });\n navEnter.selectAll('.field-help-nav-item')\n .data(titles)\n .enter()\n .append('div')\n .attr('class', 'field-help-nav-item')\n .html(function(d) { return d; })\n .on('click', function(d3_event, d) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n clickHelp(titles.indexOf(d));\n });\n\n enter\n .append('div')\n .attr('class', 'field-help-content');\n\n _body = _body\n .merge(enter);\n\n clickHelp(0);\n };\n\n\n return fieldHelp;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport {\n select as d3_select\n} from 'd3-selection';\nimport { omit } from 'es-toolkit/compat';\n\nimport { utilRebind } from '../../util/rebind';\nimport { t } from '../../core/localizer';\nimport { actionReverse } from '../../actions/reverse';\nimport { svgIcon } from '../../svg/icon';\nimport { utilCheckTagDictionary } from '../../util';\nimport { osmOneWayTags } from '../../osm/tags';\n\nexport { uiFieldCheck as uiFieldDefaultCheck };\nexport { uiFieldCheck as uiFieldOnewayCheck };\n\n\nexport function uiFieldCheck(field, context) {\n var dispatch = d3_dispatch('change');\n var options = field.options;\n var values = [];\n var texts = [];\n\n var _tags;\n\n var input = d3_select(null);\n var text = d3_select(null);\n var label = d3_select(null);\n var reverser = d3_select(null);\n\n var _impliedYes;\n var _entityIDs = [];\n var _value;\n\n\n var stringsField = field.resolveReference('stringsCrossReference');\n if (!options && stringsField.options) {\n options = stringsField.options;\n }\n\n if (options) {\n for (var i in options) {\n var v = options[i];\n values.push(v === 'undefined' ? undefined : v);\n texts.push(stringsField.t.append('options.' + v, { 'default': v }));\n }\n } else {\n values = [undefined, 'yes'];\n texts = [t.append('inspector.unknown'), t.append('inspector.check.yes')];\n if (field.type !== 'defaultCheck') {\n values.push('no');\n texts.push(t.append('inspector.check.no'));\n }\n }\n\n\n // Checks tags to see whether an undefined value is \"Assumed to be Yes\"\n function checkImpliedYes() {\n _impliedYes = (field.id === 'oneway_yes');\n\n // hack: pretend `oneway` field is a `oneway_yes` field\n // where implied oneway tag exists (e.g. `junction=roundabout`) #2220, #1841\n if (field.id === 'oneway') {\n var entity = context.entity(_entityIDs[0]);\n if (entity.type === 'way' && !!utilCheckTagDictionary(entity.tags, omit(osmOneWayTags, 'oneway'))) {\n _impliedYes = true;\n texts[0] = t.append('_tagging.presets.fields.oneway_yes.options.undefined');\n }\n }\n }\n\n\n function reverserHidden() {\n if (!context.container().select('div.inspector-hover').empty()) return true;\n return !(_value === 'yes' || (_impliedYes && !_value));\n }\n\n\n function reverserSetText(selection) {\n var entity = _entityIDs.length && context.hasEntity(_entityIDs[0]);\n if (reverserHidden() || !entity) return selection;\n\n var first = entity.first();\n var last = entity.isClosed() ? entity.nodes[entity.nodes.length - 2] : entity.last();\n var pseudoDirection = first < last;\n var icon = pseudoDirection ? '#iD-icon-forward' : '#iD-icon-backward';\n\n selection.selectAll('.reverser-span')\n .text('')\n .call(t.append('inspector.check.reverser'))\n .call(svgIcon(icon, 'inline'));\n\n return selection;\n }\n\n\n var check = function(selection) {\n checkImpliedYes();\n\n label = selection.selectAll('.form-field-input-wrap')\n .data([0]);\n\n var enter = label.enter()\n .append('label')\n .attr('class', 'form-field-input-wrap form-field-input-check');\n\n enter\n .append('input')\n .property('indeterminate', field.type !== 'defaultCheck')\n .attr('type', 'checkbox')\n .attr('id', field.domId);\n\n enter\n .append('span')\n .call(texts[0])\n .attr('class', 'value');\n\n if (field.type === 'onewayCheck') {\n enter\n .append('button')\n .attr('class', 'reverser' + (reverserHidden() ? ' hide' : ''))\n .append('span')\n .attr('class', 'reverser-span');\n }\n\n label = label.merge(enter);\n input = label.selectAll('input');\n text = label.selectAll('span.value');\n\n input\n .on('click', function(d3_event) {\n d3_event.stopPropagation();\n var t = {};\n\n if (Array.isArray(_tags[field.key])) {\n if (values.indexOf('yes') !== -1) {\n t[field.key] = 'yes';\n } else {\n t[field.key] = values[0];\n }\n } else {\n t[field.key] = values[(values.indexOf(_value) + 1) % values.length];\n }\n\n // Don't cycle through `alternating` or `reversible` states - #4970\n // (They are supported as translated strings, but should not toggle with clicks)\n if (t[field.key] === 'reversible' || t[field.key] === 'alternating') {\n t[field.key] = values[0];\n }\n\n dispatch.call('change', this, t);\n });\n\n if (field.type === 'onewayCheck') {\n reverser = label.selectAll('.reverser');\n\n reverser\n .call(reverserSetText)\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n context.perform(\n function(graph) {\n for (var i in _entityIDs) {\n graph = actionReverse(_entityIDs[i])(graph);\n }\n return graph;\n },\n t('operations.reverse.annotation.line', { n: 1 })\n );\n\n // must manually revalidate since no 'change' event was called\n context.validator().validate();\n\n d3_select(this)\n .call(reverserSetText);\n });\n }\n };\n\n\n check.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return check;\n };\n\n\n check.tags = function(tags) {\n\n _tags = tags;\n\n function isChecked(val) {\n return val !== 'no' && val !== '' && val !== undefined && val !== null;\n }\n\n function textFor(val) {\n if (val === '') val = undefined;\n var index = values.indexOf(val);\n return (index !== -1 ? texts[index] : ('\"' + val + '\"'));\n }\n\n checkImpliedYes();\n\n var isMixed = Array.isArray(tags[field.key]);\n\n _value = !isMixed && tags[field.key] && tags[field.key].toLowerCase();\n\n if (field.type === 'onewayCheck' && (_value === '1' || _value === '-1')) {\n _value = 'yes';\n }\n\n input\n .property('indeterminate', isMixed || (field.type !== 'defaultCheck' && !_value))\n .property('checked', isChecked(_value));\n\n const textForValue = textFor(_value);\n text\n .text('')\n .call(isMixed\n ? t.append('inspector.multiple_values')\n : typeof textForValue === 'string' ? selection => selection.text(textForValue) : textForValue)\n .classed('mixed', isMixed);\n\n label\n .classed('set', !!_value);\n\n if (field.type === 'onewayCheck') {\n reverser\n .classed('hide', reverserHidden())\n .call(reverserSetText);\n }\n };\n\n\n check.focus = function() {\n input.node().focus();\n };\n\n return utilRebind(check, dispatch, 'on');\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg';\nimport {\n utilUnicodeCharsCount,\n utilCleanOsmString\n} from '../util';\nimport { uiPopover } from './popover';\n\n\nexport function uiLengthIndicator(maxChars) {\n var _wrap = d3_select(null);\n var _tooltip = uiPopover('tooltip max-length-warning')\n .placement('bottom')\n .hasArrow(true)\n .content(() => selection => {\n selection.text('');\n selection.call(svgIcon('#iD-icon-alert', 'inline'));\n selection.call(t.append('inspector.max_length_reached', { maxChars }));\n });\n var _silent = false;\n\n var lengthIndicator = function(selection) {\n _wrap = selection.selectAll('span.length-indicator-wrap').data([0]);\n _wrap = _wrap.enter()\n .append('span')\n .merge(_wrap)\n .classed('length-indicator-wrap', true);\n selection.call(_tooltip);\n };\n\n lengthIndicator.update = function(val) {\n const strLen = utilUnicodeCharsCount(utilCleanOsmString(val, Number.POSITIVE_INFINITY));\n\n let indicator = _wrap.selectAll('span.length-indicator')\n .data([strLen]);\n\n indicator.enter()\n .append('span')\n .merge(indicator)\n .classed('length-indicator', true)\n .classed('limit-reached', d => d > maxChars)\n .style('border-right-width', d => `${Math.abs(maxChars - d) * 2}px`)\n .style('margin-right', d => d > maxChars\n ? `${(maxChars - d) * 2}px`\n : 0)\n .style('opacity', d => d > maxChars * 0.8\n ? Math.min(1, (d / maxChars - 0.8) / (1 - 0.8))\n : 0)\n .style('pointer-events', d => d > maxChars * 0.8 ? null: 'none');\n\n if (_silent) return;\n if (strLen > maxChars) {\n _tooltip.show();\n } else {\n _tooltip.hide();\n }\n };\n\n lengthIndicator.silent = function(val) {\n if (!arguments.length) return _silent;\n _silent = val;\n return lengthIndicator;\n };\n\n return lengthIndicator;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { drag as d3_drag } from 'd3-drag';\nimport * as countryCoder from '@rapideditor/country-coder';\n\nimport { fileFetcher } from '../../core/file_fetcher';\nimport { localizer, t } from '../../core/localizer';\nimport { services } from '../../services';\nimport { uiCombobox } from '../combobox';\nimport { svgIcon } from '../../svg/icon';\n\nimport { utilKeybinding } from '../../util/keybinding';\nimport { utilArrayUniq, utilDetect, utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent, utilUnicodeCharsCount } from '../../util';\nimport { uiLengthIndicator } from '../length_indicator';\nimport { deprecatedTagValuesByKey } from '../../osm/deprecated';\nimport { osmIsoCountryKeys } from '../../osm/tags';\n\nexport {\n uiFieldCombo as uiFieldManyCombo,\n uiFieldCombo as uiFieldMultiCombo,\n uiFieldCombo as uiFieldNetworkCombo,\n uiFieldCombo as uiFieldSemiCombo,\n uiFieldCombo as uiFieldTypeCombo\n};\n\nexport function uiFieldCombo(field, context) {\n var dispatch = d3_dispatch('change');\n var _isMulti = (field.type === 'multiCombo' || field.type === 'manyCombo');\n var _isNetwork = (field.type === 'networkCombo');\n var _isSemi = (field.type === 'semiCombo');\n var _showTagInfoSuggestions = field.type !== 'manyCombo' && field.autoSuggestions !== false;\n var _allowCustomValues = field.type !== 'manyCombo' && field.customValues !== false;\n var _snake_case = (field.snake_case || (field.snake_case === undefined));\n var _combobox = uiCombobox(context, 'combo-' + field.safeid)\n .caseSensitive(field.caseSensitive)\n .minItems(1);\n var _container = d3_select(null);\n var _inputWrap = d3_select(null);\n var _input = d3_select(null);\n var _lengthIndicator = uiLengthIndicator(context.maxCharsForTagValue());\n var _comboData = [];\n var _multiData = [];\n var _entityIDs = [];\n var _tags;\n var _countryCode;\n var _staticPlaceholder;\n var _customOptions = [];\n\n // initialize deprecated tags array\n var _dataDeprecated = [];\n fileFetcher.get('deprecated')\n .then(function(d) { _dataDeprecated = d; })\n .catch(function() { /* ignore */ });\n\n\n // ensure multiCombo field.key ends with a ':'\n if (_isMulti && field.key && /[^:]$/.test(field.key)) {\n field.key += ':';\n }\n\n\n function snake(s) {\n return s.replace(/\\s+/g, '_');\n }\n\n function clean(s) {\n return s.split(';')\n .map(function(s) { return s.trim(); })\n .join(';');\n }\n\n // windows does not support emoji flags\n const showEmojiFlags = utilDetect().os !== 'win';\n\n // adds emoji flags to country dropdown and input\n function addFlagIcon(selection, name, flag) {\n if (showEmojiFlags && flag) {\n const icon = selection.insert('span', ':first-child')\n .attr('class', 'tag-value-icon');\n\n icon.append('span')\n .attr('class', 'emoji')\n .text(flag);\n }\n selection.insert('span')\n .attr('class', 'tag-value')\n .text(name);\n }\n\n // cache for language and region maps\n let languageByCodes = null;\n let regionByCodes = null;\n\n // Helper to get regionByCodes\n const buildCountry = () => {\n if (regionByCodes) return regionByCodes;\n const localeCode = localizer.localeCode();\n\n const regionNames = new Intl.DisplayNames(localeCode, { type: 'region' });\n const features = countryCoder.borders.features;\n\n regionByCodes = {};\n\n for (const feature of features) {\n const code = feature.properties.iso1A2;\n let flag = feature.properties.emojiFlag;\n // if the flag is not present like for 'FX' code, we will look for the corresponding country flag for that code\n if (!flag && features?.properties?.country) {\n flag = countryCoder.feature(feature.properties.country).properties.emojiFlag;\n }\n if (!code) continue;\n\n try {\n const name = regionNames.of(code);\n if (!name) continue;\n regionByCodes[code] = {name, flag};\n } catch {\n continue;\n }\n }\n\n return regionByCodes;\n };\n\n // Helper to get languageByCodes\n const buildLanguages = () => {\n if (languageByCodes) return languageByCodes;\n\n languageByCodes = {};\n let codes = Object.keys(localizer.languages());\n\n for (const code of codes) {\n const name = localizer.languageName(code);\n if (!name || name === code) continue;\n languageByCodes[code] = name;\n }\n\n return languageByCodes;\n };\n\n // returns the tag value for a display value\n // (for multiCombo, dval should be the key suffix, not the entire key)\n function tagValue(dval) {\n dval = clean(dval || '');\n\n var found = getOptions(true).find(function(o) {\n return o.key && clean(o.value) === dval;\n });\n if (found) return found.key;\n\n if (field.type === 'typeCombo' && !dval) {\n return 'yes';\n }\n\n return restrictTagValueSpelling(dval) || undefined;\n }\n\n function restrictTagValueSpelling(dval) {\n if (_snake_case) {\n dval = snake(dval);\n }\n\n if (!field.caseSensitive) {\n if (!(field.key === 'type' && dval === 'associatedStreet')) {\n // don't lowercase \"type=associatedStreet\" tag\n // https://github.com/openstreetmap/iD/issues/9639\n dval = dval.toLowerCase();\n }\n }\n\n return dval;\n }\n\n\n function getLabelId(field, v) {\n return field.hasTextForStringId(`options.${v}.title`)\n ? `options.${v}.title`\n : `options.${v}`;\n }\n\n\n // returns the display value for a tag value\n // (for multiCombo, tval should be the key suffix, not the entire key)\n function displayValue(tval) {\n tval = tval || '';\n // Issue #11652\n // Ignore language: key and when tval is not others\n if (field.key === 'language:' && tval !== 'others'){\n let langName = buildLanguages()[tval];\n if (langName) return langName;\n }\n\n if (osmIsoCountryKeys.has(field.key) && tval) {\n const data = buildCountry()[tval];\n if (data) return data.name;\n return tval;\n }\n\n var stringsField = field.resolveReference('stringsCrossReference');\n const labelId = getLabelId(stringsField, tval);\n if (stringsField.hasTextForStringId(labelId)) {\n return stringsField.t(labelId, { default: tval });\n }\n\n if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {\n return '';\n }\n\n return tval;\n }\n\n\n // returns function which renders the display value for a tag value\n // (for multiCombo, tval should be the key suffix, not the entire key)\n function renderValue(tval) {\n tval = tval || '';\n\n // Issue #11652\n // Ignore language: key and when tval is not others\n if (field.key === 'language:' && tval !== 'others') {\n let langName = buildLanguages()[tval];\n if (langName) return selection => selection.text(langName);\n }\n\n if (osmIsoCountryKeys.has(field.key) && tval) {\n const data = buildCountry()[tval];\n if (data) return selection => addFlagIcon(selection, data.name, data.flag);\n return selection => selection.text(tval);\n }\n\n var stringsField = field.resolveReference('stringsCrossReference');\n const labelId = getLabelId(stringsField, tval);\n if (stringsField.hasTextForStringId(labelId)) {\n return stringsField.t.append(labelId, { default: tval });\n }\n\n if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {\n tval = '';\n }\n\n return selection => selection.text(tval);\n }\n\n\n // Compute the difference between arrays of objects by `value` property\n //\n // objectDifference([{value:1}, {value:2}, {value:3}], [{value:2}])\n // > [{value:1}, {value:3}]\n //\n function objectDifference(a, b) {\n return a.filter(function(d1) {\n return !b.some(function(d2) {\n return d1.value === d2.value;\n });\n });\n }\n\n\n function initCombo(selection, attachTo) {\n if (!_allowCustomValues) {\n selection.attr('readonly', 'readonly');\n }\n\n if (_showTagInfoSuggestions && services.taginfo) {\n selection.call(_combobox.fetcher(setTaginfoValues), attachTo);\n setTaginfoValues('', setPlaceholder);\n } else {\n selection.call(_combobox, attachTo);\n setTimeout(() => setStaticValues(setPlaceholder), 0);\n }\n }\n\n function getOptions(allOptions) {\n var stringsField = field.resolveReference('stringsCrossReference');\n const localeCode = localizer.localeCode();\n // Get dropdown list for language: key via localizer instead of taginfo\n if (field.key === 'language:') {\n const langMap = buildLanguages();\n\n let options = Object.entries(langMap).map(([code, name]) => {\n return {\n key: code,\n value: name,\n title: code, // the tooltip should show the raw-tag value\n display: selection => selection.text(name)\n };\n });\n\n const localeCode = localizer.localeCode();\n\n options.sort((a, b) => {\n return a.value.localeCompare(b.value, localeCode);\n });\n\n const v = 'others';\n const labelId = getLabelId(stringsField, v);\n\n // inserting others because it does not come via _dataLanguages\n options.push({\n key: v,\n value: stringsField.t(labelId, { default: v }),\n title: stringsField.t(`options.${v}.description`, { default: v }),\n description: stringsField.hasTextForStringId(`options.${v}.description`)\n ? stringsField.t(`options.${v}.description`) : undefined,\n display: addComboboxIcons(stringsField.t.append(labelId, { default: v }), v),\n klass: stringsField.hasTextForStringId(labelId) ? '' : 'raw-option'\n });\n\n\n return options;\n }\n\n // Get dropdown list for country key\n if (osmIsoCountryKeys.has(field.key)) {\n const countryMap = buildCountry();\n\n let options = Object.entries(countryMap).map(([c, {name, flag}]) => {\n return {\n key: c,\n value: name,\n title: c, // the tooltip should show the raw-tag value\n display: selection => addFlagIcon(selection, name, flag),\n sortname: name, // store just the name without emojis to sort the names\n klass: 'has-icon' // to specifically target the emoji css\n };\n });\n\n options.sort((a, b) => {\n return a.sortname.localeCompare(b.sortname, localeCode);\n });\n\n return options;\n }\n\n if (!(field.options || stringsField.options)) return [];\n\n let options;\n if (allOptions !== true) {\n options = field.options || stringsField.options;\n } else {\n options = [].concat(field.options, stringsField.options).filter(Boolean);\n }\n const result = options.map(function(v) {\n const labelId = getLabelId(stringsField, v);\n return {\n key: v,\n value: stringsField.t(labelId, { default: v }),\n title: stringsField.t(`options.${v}.description`, { default: v }),\n description: stringsField.hasTextForStringId(`options.${v}.description`)\n ? stringsField.t(`options.${v}.description`) : undefined,\n display: addComboboxIcons(stringsField.t.append(labelId, { default: v }), v),\n klass: stringsField.hasTextForStringId(labelId) ? '' : 'raw-option'\n };\n });\n return [...result, ..._customOptions];\n }\n\n\n function hasStaticValues() {\n return getOptions().length > 0;\n }\n\n\n function setStaticValues(callback, filter) {\n _comboData = getOptions();\n\n if (filter !== undefined) {\n _comboData = _comboData.filter(filter);\n }\n\n if (!field.allowDuplicates) {\n _comboData = objectDifference(_comboData, _multiData);\n }\n _combobox.data(_comboData);\n\n // hide the caret if there are no suggestions\n _container.classed('empty-combobox', _comboData.length === 0);\n\n if (callback) callback(_comboData);\n }\n\n\n function setTaginfoValues(q, callback) {\n var queryFilter = d => d.value.toLowerCase().includes(q.toLowerCase()) || d.key.toLowerCase().includes(q.toLowerCase());\n if (hasStaticValues()) {\n setStaticValues(callback, queryFilter);\n\n // If it is language field or a country field, we don't need to request for values, we get it from getOptions\n if (field.key === 'language:') return;\n if (osmIsoCountryKeys.has(field.key)) return;\n }\n\n var stringsField = field.resolveReference('stringsCrossReference');\n var fn = _isMulti ? 'multikeys' : 'values';\n var query = (_isMulti ? field.key : '') + q;\n var hasCountryPrefix = _isNetwork && _countryCode && _countryCode.indexOf(q.toLowerCase()) === 0;\n if (hasCountryPrefix) {\n query = _countryCode + ':';\n }\n\n var params = {\n debounce: (q !== ''),\n key: field.key,\n query: query\n };\n\n if (_entityIDs.length) {\n params.geometry = context.graph().geometry(_entityIDs[0]);\n }\n\n services.taginfo[fn](params, function(err, data) {\n if (err) return;\n\n // don't show the fallback value\n data = data.filter(d =>\n field.type !== 'typeCombo' || d.value !== 'yes');\n\n // don't show misspelled values\n data = data.filter(d => {\n var value = d.value;\n if (_isMulti) {\n value = value.slice(field.key.length);\n }\n return value === restrictTagValueSpelling(value);\n });\n\n var deprecatedValues = deprecatedTagValuesByKey(_dataDeprecated)[field.key];\n if (deprecatedValues) {\n // don't suggest deprecated tag values\n data = data.filter(d =>\n !deprecatedValues.includes(d.value));\n }\n\n if (hasCountryPrefix) {\n data = data.filter(d =>\n d.value.toLowerCase().indexOf(_countryCode + ':') === 0);\n }\n\n const additionalOptions = (field.options || stringsField.options || [])\n .filter(v => !data.some(dv => dv.value === (_isMulti ? field.key + v : v)))\n .map(v => ({ value: v }));\n\n // hide the caret if there are no suggestions\n _container.classed('empty-combobox', data.length === 0);\n\n _comboData = data.concat(additionalOptions).map(function(d) {\n var v = d.value;\n if (_isMulti) v = v.replace(field.key, '');\n const labelId = getLabelId(stringsField, v);\n var isLocalizable = stringsField.hasTextForStringId(labelId);\n var label = stringsField.t(labelId, { default: v });\n return {\n key: v,\n value: label,\n title: stringsField.t(`options.${v}.description`, { default:\n isLocalizable ? label : (d.title !== label ? d.title : '') }),\n description: stringsField.hasTextForStringId(`options.${v}.description`)\n ? stringsField.t(`options.${v}.description`) : undefined,\n display: addComboboxIcons(stringsField.t.append(labelId, { default: label }), v),\n klass: isLocalizable ? '' : 'raw-option'\n };\n });\n\n _comboData = _comboData.filter(queryFilter);\n\n if (!field.allowDuplicates) {\n _comboData = objectDifference(_comboData, _multiData);\n }\n if (callback) callback(_comboData, hasStaticValues());\n });\n }\n\n // adds icons to tag values which have one\n function addComboboxIcons(disp, value) {\n const iconsField = field.resolveReference('iconsCrossReference');\n if (iconsField.icons) {\n return function(selection) {\n var span = selection\n .insert('span', ':first-child')\n .attr('class', 'tag-value-icon');\n if (iconsField.icons[value]) {\n span.call(svgIcon(`#${iconsField.icons[value]}`));\n }\n disp.call(this, selection);\n };\n }\n return disp;\n }\n\n\n function setPlaceholder(values) {\n\n if (_isMulti || _isSemi) {\n _staticPlaceholder = field.placeholder() || t('inspector.add');\n } else {\n var vals = values\n .map(function(d) { return d.value; })\n .filter(function(s) { return s.length < 20; });\n\n var placeholders = vals.length > 1 ? vals : values.map(function(d) { return d.key; });\n _staticPlaceholder = field.placeholder() || placeholders.slice(0, 3).join(', ');\n }\n\n if (!/(\u2026|\\.\\.\\.)$/.test(_staticPlaceholder)) {\n _staticPlaceholder += '\u2026';\n }\n\n var ph;\n if (!_isMulti && !_isSemi && _tags && Array.isArray(_tags[field.key])) {\n ph = t('inspector.multiple_values');\n } else {\n ph = _staticPlaceholder;\n }\n\n _container.selectAll('input')\n .attr('placeholder', ph);\n\n // Hide 'Add' button if this field uses fixed set of\n // options and they're all currently used\n var hideAdd = (!_allowCustomValues && !values.length);\n _container.selectAll('.chiplist .input-wrap')\n .style('display', hideAdd ? 'none' : null);\n }\n\n\n function change() {\n var t = {};\n var val;\n\n if (_isMulti || _isSemi) {\n var vals;\n if (_isMulti) {\n vals = [tagValue(utilGetSetValue(_input))];\n } else if (_isSemi) {\n val = tagValue(utilGetSetValue(_input)) || '';\n val = val.replace(/,/g, ';');\n vals = val.split(';');\n }\n vals = vals.filter(Boolean);\n\n if (!vals.length) return;\n\n _container.classed('active', false);\n utilGetSetValue(_input, '');\n\n if (_isMulti) {\n utilArrayUniq(vals).forEach(function(v) {\n var key = (field.key || '') + v;\n if (_tags) {\n // don't set a multicombo value to 'yes' if it already has a non-'no' value\n // e.g. `language:de=main`\n var old = _tags[key];\n if (typeof old === 'string' && old.toLowerCase() !== 'no') return;\n }\n key = context.cleanTagKey(key);\n field.keys.push(key);\n t[key] = 'yes';\n });\n\n } else if (_isSemi) {\n var arr = _multiData.map(function(d) { return d.key; });\n arr = arr.concat(vals);\n if (!field.allowDuplicates) {\n arr = utilArrayUniq(arr);\n }\n t[field.key] = context.cleanTagValue(arr.filter(Boolean).join(';'));\n }\n\n window.setTimeout(function() { _input.node().focus(); }, 10);\n\n } else {\n var rawValue = utilGetSetValue(_input);\n\n // don't override multiple values with blank string\n if (!rawValue && Array.isArray(_tags[field.key])) return;\n\n val = context.cleanTagValue(tagValue(rawValue));\n t[field.key] = val || undefined;\n }\n\n dispatch.call('change', this, t);\n }\n\n\n function removeMultikey(d3_event, d) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n var t = {};\n if (_isMulti) {\n t[d.key] = undefined;\n } else if (_isSemi) {\n let arr = _multiData.map(item => item.key);\n\n // delete the value using the index, since a value\n // may exist multiple times in the array.\n arr.splice(d.index, 1);\n\n if (!field.allowDuplicates) {\n arr = utilArrayUniq(arr);\n }\n t[field.key] = arr.length ? arr.join(';') : undefined;\n\n _lengthIndicator.update(t[field.key]);\n }\n dispatch.call('change', this, t);\n }\n\n\n function invertMultikey(d3_event, d) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n var t = {};\n if (_isMulti) {\n t[d.key] = _tags[d.key] === 'yes' ? 'no' : 'yes';\n }\n dispatch.call('change', this, t);\n }\n\n\n function combo(selection) {\n _container = selection.selectAll('.form-field-input-wrap')\n .data([0]);\n\n var type = (_isMulti || _isSemi) ? 'multicombo': 'combo';\n _container = _container.enter()\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-' + type)\n .merge(_container);\n\n if (_isMulti || _isSemi) {\n _container = _container.selectAll('.chiplist')\n .data([0]);\n\n var listClass = 'chiplist';\n\n // Use a separate line for each value in the Destinations and Via fields\n // to mimic highway exit signs\n if (field.key === 'destination' || field.key === 'via') {\n listClass += ' full-line-chips';\n }\n\n _container = _container.enter()\n .append('ul')\n .attr('class', listClass)\n .on('click', function() {\n window.setTimeout(function() { _input.node().focus(); }, 10);\n })\n .merge(_container);\n\n\n _inputWrap = _container.selectAll('.input-wrap')\n .data([0]);\n\n _inputWrap = _inputWrap.enter()\n .append('li')\n .attr('class', 'input-wrap')\n .merge(_inputWrap);\n\n // Hide 'Add' button if this field uses fixed set of\n // options and they're all currently used\n var hideAdd = (!_allowCustomValues && !_comboData.length);\n _inputWrap.style('display', hideAdd ? 'none' : null);\n\n _input = _inputWrap.selectAll('input')\n .data([0]);\n } else {\n _input = _container.selectAll('input')\n .data([0]);\n }\n\n _input = _input.enter()\n .append('input')\n .attr('type', 'text')\n .attr('dir', 'auto')\n .attr('id', field.domId)\n .call(utilNoAuto)\n .call(initCombo, _container)\n .merge(_input);\n\n if (_isSemi) {\n _inputWrap.call(_lengthIndicator);\n } else if (!_isMulti) {\n _container.call(_lengthIndicator);\n }\n\n if (_isNetwork) {\n var extent = combinedEntityExtent();\n var countryCode = extent && countryCoder.iso1A2Code(extent.center());\n _countryCode = countryCode && countryCode.toLowerCase();\n }\n\n _input\n .on('change', change)\n .on('blur', change)\n .on('input', function() {\n let val = utilGetSetValue(_input);\n updateIcon(val);\n if (_isSemi && _tags[field.key]) {\n // when adding a new value to existing ones\n val += ';' + _tags[field.key];\n }\n _lengthIndicator.update(val);\n });\n\n _input\n .on('keydown.field', function(d3_event) {\n switch (d3_event.keyCode) {\n case 13: // \u21A9 Return\n _input.node().blur(); // blurring also enters the value\n d3_event.stopPropagation();\n break;\n }\n });\n\n if (_isMulti || _isSemi) {\n _combobox\n .on('accept', function() {\n _input.node().blur();\n _input.node().focus();\n });\n\n _input\n .on('focus', function() { _container.classed('active', true); });\n }\n\n _combobox\n .on('cancel', function() {\n _input.node().blur();\n })\n .on('update', function() {\n updateIcon(utilGetSetValue(_input));\n });\n }\n\n function updateIcon(value) {\n value = tagValue(value);\n let container = _container;\n if (field.type === 'multiCombo' || field.type === 'semiCombo') {\n container = _container.select('.input-wrap');\n }\n\n // For the country emoji flags\n container.selectAll('.tag-value-icon').remove();\n if (osmIsoCountryKeys.has(field.key) && value) {\n const data = buildCountry()[value];\n\n if (data && data.flag && showEmojiFlags) {\n container.selectAll('.tag-value-icon')\n .data([value])\n .enter()\n .insert('div', 'input')\n .attr('class', 'tag-value-icon')\n .append('span')\n .attr('class', 'emoji')\n .text(data.flag);\n return;\n }\n };\n\n const iconsField = field.resolveReference('iconsCrossReference');\n if (iconsField.icons) {\n container.selectAll('.tag-value-icon').remove();\n if (iconsField.icons[value]) {\n container.selectAll('.tag-value-icon')\n .data([value])\n .enter()\n .insert('div', 'input')\n .attr('class', 'tag-value-icon')\n .call(svgIcon(`#${iconsField.icons[value]}`));\n }\n }\n }\n\n combo.tags = function(tags) {\n _tags = tags;\n var stringsField = field.resolveReference('stringsCrossReference');\n\n var isMixed = Array.isArray(tags[field.key]);\n var showsValue = value => !isMixed && value && !(field.type === 'typeCombo' && value === 'yes');\n var isRawValue = value => showsValue(value)\n && !stringsField.hasTextForStringId(`options.${value}`)\n && !stringsField.hasTextForStringId(`options.${value}.title`)\n && !(osmIsoCountryKeys.has(field.key) && value in buildCountry());\n var isKnownValue = value => showsValue(value) && !isRawValue(value);\n var isReadOnly = !_allowCustomValues;\n\n if (_isMulti || _isSemi) {\n _multiData = [];\n\n var maxLength;\n\n if (_isMulti) {\n // Build _multiData array containing keys already set..\n for (var k in tags) {\n if (field.key && k.indexOf(field.key) !== 0) continue;\n if (!field.key && field.keys.indexOf(k) === -1) continue;\n\n var v = tags[k];\n\n var suffix = field.key ? k.slice(field.key.length) : k;\n _multiData.push({\n key: k,\n value: displayValue(suffix),\n display: addComboboxIcons(renderValue(suffix), suffix),\n state: typeof v === 'string' ? v.toLowerCase() : '',\n isMixed: Array.isArray(v)\n });\n }\n\n if (field.key) {\n // Set keys for form-field modified (needed for undo and reset buttons)..\n field.keys = _multiData.map(function(d) { return d.key; });\n\n // limit the input length so it fits after prepending the key prefix\n maxLength = context.maxCharsForTagKey() - utilUnicodeCharsCount(field.key);\n } else {\n maxLength = context.maxCharsForTagKey();\n }\n\n } else if (_isSemi) {\n\n var allValues = [];\n var commonValues;\n if (Array.isArray(tags[field.key])) {\n\n tags[field.key].forEach(function(tagVal) {\n var thisVals = (tagVal || '').split(';').filter(Boolean);\n allValues = allValues.concat(thisVals);\n if (!commonValues) {\n commonValues = thisVals;\n } else {\n commonValues = commonValues.filter(value => thisVals.includes(value));\n }\n });\n allValues = allValues.filter(Boolean);\n\n } else {\n allValues = (tags[field.key] || '').split(';').filter(Boolean);\n commonValues = allValues;\n }\n\n if (!field.allowDuplicates) {\n commonValues = utilArrayUniq(commonValues);\n allValues = utilArrayUniq(allValues);\n }\n\n _multiData = allValues.map(function(v) {\n return {\n key: v,\n value: displayValue(v),\n display: addComboboxIcons(renderValue(v), v),\n isMixed: !commonValues.includes(v)\n };\n });\n\n var currLength = utilUnicodeCharsCount(commonValues.join(';'));\n\n // limit the input length to the remaining available characters\n maxLength = context.maxCharsForTagValue() - currLength;\n\n if (currLength > 0) {\n // account for the separator if a new value will be appended to existing\n maxLength -= 1;\n }\n }\n // a negative maxlength doesn't make sense\n maxLength = Math.max(0, maxLength);\n\n // Hide 'Add' button if this field is already at its character limit\n var hideAdd = maxLength <= 0 || (!_allowCustomValues && !_comboData.length);\n _container.selectAll('.chiplist .input-wrap')\n .style('display', hideAdd ? 'none' : null);\n\n var allowDragAndDrop = _isSemi // only semiCombo values are ordered\n && !Array.isArray(tags[field.key]);\n\n // Render chips\n var chips = _container.selectAll('.chip')\n .data(_multiData.map((item, index) => ({ ...item, index })));\n\n chips.exit()\n .remove();\n\n var enter = chips.enter()\n .insert('li', '.input-wrap')\n .attr('class', 'chip');\n\n enter.append('span');\n const field_buttons = enter\n .append('div')\n .attr('class', 'field_buttons');\n field_buttons\n .append('a')\n .attr('class', 'remove');\n\n chips = chips.merge(enter)\n .order()\n .classed('raw-value', function(d) {\n var k = d.key;\n if (_isMulti) k = k.replace(field.key, '');\n // Ignore the raw-value class for key language:\n if (field.key === 'language:' && localizer.languageName(k) !== k) return false;\n return !stringsField.hasTextForStringId('options.' + k);\n })\n .classed('draggable', allowDragAndDrop)\n .classed('mixed', function(d) {\n return d.isMixed;\n })\n .attr('title', function(d) {\n if (d.isMixed) {\n return t('inspector.unshared_value_tooltip');\n }\n if (!['yes', 'no'].includes(d.state)) {\n return d.state;\n }\n return null;\n })\n .classed('negated', d => d.state === 'no');\n\n if (!_isSemi) {\n chips.selectAll('input[type=checkbox]').remove();\n chips.insert('input', 'span')\n .attr('type', 'checkbox')\n .property('checked', d => d.state === 'yes')\n .property('indeterminate', d => d.isMixed || !['yes', 'no'].includes(d.state))\n .on('click', invertMultikey);\n }\n\n if (allowDragAndDrop) {\n registerDragAndDrop(chips);\n }\n\n chips.each(function(d) {\n const selection = d3_select(this);\n const text_span = selection.select('span');\n const field_buttons = selection.select('.field_buttons');\n const clean_value = d.value.trim();\n text_span.text('');\n if (!field_buttons.select('button').empty()) {\n field_buttons.select('button').remove();\n }\n if (clean_value.startsWith('https://')) {\n // create a button to open the link in a new tab\n text_span.text(clean_value);\n field_buttons.append('button')\n .call(svgIcon('#iD-icon-out-link'))\n .attr('class', 'form-field-button foreign-id-permalink')\n .attr('title', () => t('icons.visit_website'))\n .attr('aria-label', () => t('icons.visit_website'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n window.open(clean_value, '_blank');\n });\n return;\n }\n if (d.display) {\n d.display(text_span);\n return;\n }\n text_span.text(d.value);\n });\n\n chips.select('a.remove')\n .attr('href', '#')\n .on('click', removeMultikey)\n .attr('class', 'remove')\n .text('\u00D7');\n\n updateIcon('');\n } else {\n var mixedValues = isMixed && tags[field.key].map(function(val) {\n return displayValue(val);\n }).filter(Boolean);\n\n utilGetSetValue(_input, !isMixed ? displayValue(tags[field.key]) : '')\n .data([tags[field.key]])\n .classed('raw-value', isRawValue)\n .classed('known-value', isKnownValue)\n .attr('readonly', isReadOnly ? 'readonly' : undefined)\n .attr('title', isMixed ? mixedValues.join('\\n') : undefined)\n .attr('placeholder', isMixed ? t('inspector.multiple_values') : _staticPlaceholder || '')\n .classed('mixed', isMixed)\n .on('keydown.deleteCapture', function(d3_event) {\n if (isReadOnly &&\n isKnownValue(tags[field.key]) &&\n (d3_event.keyCode === utilKeybinding.keyCodes['\u232B'] ||\n d3_event.keyCode === utilKeybinding.keyCodes['\u2326'])) {\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n var t = {};\n t[field.key] = undefined;\n dispatch.call('change', this, t);\n }\n });\n\n if (!Array.isArray(tags[field.key])) {\n updateIcon(tags[field.key]);\n }\n\n if (!isMixed) {\n _lengthIndicator.update(tags[field.key]);\n }\n }\n\n const refreshStyles = () => {\n _input\n .data([tagValue(utilGetSetValue(_input))])\n .classed('raw-value', isRawValue)\n .classed('known-value', isKnownValue);\n };\n _input.on('input.refreshStyles', refreshStyles);\n _combobox.on('update.refreshStyles', refreshStyles);\n refreshStyles();\n };\n\n function registerDragAndDrop(selection) {\n\n // allow drag and drop re-ordering of chips\n var dragOrigin, targetIndex;\n selection.call(d3_drag()\n .on('start', function(d3_event) {\n dragOrigin = {\n x: d3_event.x,\n y: d3_event.y\n };\n targetIndex = null;\n })\n .on('drag', function(d3_event) {\n var x = d3_event.x - dragOrigin.x,\n y = d3_event.y - dragOrigin.y;\n\n if (!d3_select(this).classed('dragging') &&\n // don't display drag until dragging beyond a distance threshold\n Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) <= 5) return;\n\n var index = selection.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', true);\n\n targetIndex = null;\n var targetIndexOffsetTop = null;\n var draggedTagWidth = d3_select(this).node().offsetWidth;\n\n if (field.key === 'destination' || field.key === 'via') { // meaning tags are full width\n _container.selectAll('.chip')\n .style('transform', function(d2, index2) {\n var node = d3_select(this).node();\n\n if (index === index2) {\n return 'translate(' + x + 'px, ' + y + 'px)';\n // move the dragged tag up the order\n } else if (index2 > index && d3_event.y > node.offsetTop) {\n if (targetIndex === null || index2 > targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(-100%)';\n // move the dragged tag down the order\n } else if (index2 < index && d3_event.y < node.offsetTop + node.offsetHeight) {\n if (targetIndex === null || index2 < targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(100%)';\n }\n return null;\n });\n } else {\n _container.selectAll('.chip')\n .each(function(d2, index2) {\n var node = d3_select(this).node();\n\n // check the cursor is in the bounding box\n if (\n index !== index2 &&\n d3_event.x < node.offsetLeft + node.offsetWidth + 5 &&\n d3_event.x > node.offsetLeft &&\n d3_event.y < node.offsetTop + node.offsetHeight &&\n d3_event.y > node.offsetTop\n ) {\n targetIndex = index2;\n targetIndexOffsetTop = node.offsetTop;\n }\n })\n .style('transform', function(d2, index2) {\n var node = d3_select(this).node();\n\n if (index === index2) {\n return 'translate(' + x + 'px, ' + y + 'px)';\n }\n\n // only translate tags in the same row\n if (node.offsetTop === targetIndexOffsetTop) {\n if (index2 < index && index2 >= targetIndex) {\n return 'translateX(' + draggedTagWidth + 'px)';\n } else if (index2 > index && index2 <= targetIndex) {\n return 'translateX(-' + draggedTagWidth + 'px)';\n }\n }\n return null;\n });\n }\n })\n .on('end', function() {\n if (!d3_select(this).classed('dragging')) {\n return;\n }\n var index = selection.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', false);\n\n _container.selectAll('.chip')\n .style('transform', null);\n\n if (typeof targetIndex === 'number') {\n var element = _multiData[index];\n _multiData.splice(index, 1);\n _multiData.splice(targetIndex, 0, element);\n\n var t = {};\n\n if (_multiData.length) {\n t[field.key] = _multiData.map(function(element) {\n return element.key;\n }).join(';');\n } else {\n t[field.key] = undefined;\n }\n\n dispatch.call('change', this, t);\n }\n dragOrigin = undefined;\n targetIndex = undefined;\n })\n );\n }\n\n combo.setCustomOptions = (newValue) => {\n _customOptions = newValue;\n };\n\n\n combo.focus = function() {\n _input.node().focus();\n };\n\n\n combo.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return combo;\n };\n\n\n function combinedEntityExtent() {\n return _entityIDs && _entityIDs.length && utilTotalExtent(_entityIDs, context.graph());\n }\n\n\n return utilRebind(combo, dispatch, 'on');\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport { geoSphericalDistance } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect, modeSelectNote } from '../modes';\nimport { utilObjectOmit, utilQsString, utilStringQs } from '../util';\nimport { utilArrayIdentical } from '../util/array';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { localizer, t } from '../core/localizer';\nimport { prefs } from '../core/preferences';\n\n\nexport function behaviorHash(context) {\n\n // cached window.location.hash\n var _cachedHash = null;\n // allowable latitude range\n var _latitudeLimit = 90 - 1e-8;\n\n function computedHashParameters() {\n var map = context.map();\n var center = map.center();\n var zoom = map.zoom();\n var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));\n var oldParams = utilObjectOmit(utilStringQs(window.location.hash),\n ['comment', 'source', 'hashtags', 'walkthrough']\n );\n var newParams = {};\n\n delete oldParams.id;\n var selected = context.selectedIDs().filter(function(id) {\n return context.hasEntity(id);\n });\n if (selected.length) {\n newParams.id = selected.join(',');\n } else if (context.selectedNoteID()) {\n newParams.id = `note/${context.selectedNoteID()}`;\n }\n\n newParams.map = zoom.toFixed(2) +\n '/' + center[1].toFixed(precision) +\n '/' + center[0].toFixed(precision);\n\n return Object.assign(oldParams, newParams);\n }\n\n function computedHash() {\n return '#' + utilQsString(computedHashParameters(), true);\n }\n\n function computedTitle(includeChangeCount) {\n\n var baseTitle = context.documentTitleBase() || 'iD';\n var contextual;\n var changeCount;\n var titleID;\n\n var selected = context.selectedIDs().filter(function(id) {\n return context.hasEntity(id);\n });\n if (selected.length) {\n var firstLabel = utilDisplayLabel(context.entity(selected[0]), context.graph());\n if (selected.length > 1) {\n contextual = t('title.labeled_and_more', {\n labeled: firstLabel,\n count: selected.length - 1\n });\n } else {\n contextual = firstLabel;\n }\n titleID = 'context';\n }\n\n if (includeChangeCount) {\n changeCount = context.history().difference().summary().length;\n if (changeCount > 0) {\n titleID = contextual ? 'changes_context' : 'changes';\n }\n }\n\n if (titleID) {\n return t('title.format.' + titleID, {\n changes: changeCount,\n base: baseTitle,\n context: contextual\n });\n }\n\n return baseTitle;\n }\n\n function updateTitle(includeChangeCount) {\n if (!context.setsDocumentTitle()) return;\n\n var newTitle = computedTitle(includeChangeCount);\n if (document.title !== newTitle) {\n document.title = newTitle;\n }\n }\n\n function updateHashIfNeeded() {\n if (context.inIntro()) return;\n\n var latestHash = computedHash();\n if (_cachedHash !== latestHash) {\n _cachedHash = latestHash;\n\n // Update the URL hash without affecting the browser navigation stack,\n // though unavoidably creating a browser history entry\n window.history.replaceState(null, '', latestHash);\n\n // set the title we want displayed for the browser tab/window\n updateTitle(true /* includeChangeCount */);\n\n // save last used map location for future\n const q = utilStringQs(latestHash);\n if (q.map) {\n prefs('map-location', q.map);\n }\n }\n }\n\n var _throttledUpdate = throttle(updateHashIfNeeded, 500);\n var _throttledUpdateTitle = throttle(function() {\n updateTitle(true /* includeChangeCount */);\n }, 500);\n\n function hashchange() {\n // ignore spurious hashchange events\n if (window.location.hash === _cachedHash) return;\n\n _cachedHash = window.location.hash;\n\n var q = utilStringQs(_cachedHash);\n\n if (q.theme) {\n context.theme(q.theme);\n }\n\n if (q.locale && q.locale !== localizer.preferredLocaleCodes().join(',')) {\n localizer.preferredLocaleCodes(q.locale);\n context.ui().restart();\n }\n\n var mapArgs = (q.map || '').split('/').map(Number);\n if (mapArgs.length < 3 || mapArgs.some(isNaN)) {\n // replace bogus hash\n updateHashIfNeeded();\n\n } else {\n // don't update if the new hash already reflects the state of iD\n if (_cachedHash === computedHash()) return;\n\n var mode = context.mode();\n\n context.map().centerZoom([mapArgs[2], Math.min(_latitudeLimit, Math.max(-_latitudeLimit, mapArgs[1]))], mapArgs[0]);\n\n if (q.id && mode) {\n var ids = q.id.split(',').filter(function(id) {\n return context.hasEntity(id) || id.startsWith('note/');\n });\n if (ids.length && ['browse', 'select-note', 'select'].includes(mode.id)) {\n if (ids.length === 1 && ids[0].startsWith('note/')) {\n context.enter(modeSelectNote(context, ids[0]));\n } else if (!utilArrayIdentical(mode.selectedIDs(), ids)) {\n context.enter(modeSelect(context, ids));\n }\n return;\n }\n }\n\n var center = context.map().center();\n var dist = geoSphericalDistance(center, [mapArgs[2], mapArgs[1]]);\n var maxdist = 500;\n\n // Don't allow the hash location to change too much while drawing\n // This can happen if the user accidentally hit the back button. #3996\n if (mode && mode.id.match(/^draw/) !== null && dist > maxdist) {\n context.enter(modeBrowse(context));\n return;\n }\n }\n }\n\n function behavior() {\n context.map()\n .on('move.behaviorHash', _throttledUpdate);\n\n context.history()\n .on('change.behaviorHash', _throttledUpdateTitle);\n\n context\n .on('enter.behaviorHash', _throttledUpdate);\n\n d3_select(window)\n .on('hashchange.behaviorHash', hashchange);\n\n var q = utilStringQs(window.location.hash);\n\n if (q.id) {\n // targeting specific features: download, select, and zoom to them\n const selectIds = q.id.split(',');\n if (selectIds.length === 1 && selectIds[0].startsWith('note/')) {\n const noteId = selectIds[0].split('/')[1];\n context.moveToNote(noteId, !q.map);\n } else {\n context.zoomToEntities(\n // convert ids to short form id: node/123 -> n123\n selectIds.map(id => id.replace(/([nwr])[^/]*\\//, '$1')),\n !q.map);\n }\n }\n\n if (q.walkthrough === 'true') {\n behavior.startWalkthrough = true;\n }\n\n if (q.map) {\n behavior.hadLocation = true;\n } else if (!q.id && prefs('map-location')) {\n // center map at last visited map location\n const mapArgs = prefs('map-location').split('/').map(Number);\n context.map().centerZoom([mapArgs[2], Math.min(_latitudeLimit, Math.max(-_latitudeLimit, mapArgs[1]))], mapArgs[0]);\n\n updateHashIfNeeded();\n\n behavior.hadLocation = true;\n }\n\n hashchange();\n\n updateTitle(false);\n }\n\n behavior.off = function() {\n _throttledUpdate.cancel();\n _throttledUpdateTitle.cancel();\n\n context.map()\n .on('move.behaviorHash', null);\n\n context\n .on('enter.behaviorHash', null);\n\n d3_select(window)\n .on('hashchange.behaviorHash', null);\n\n window.location.hash = '';\n };\n\n return behavior;\n}\n", "export { behaviorAddWay } from './add_way';\nexport { behaviorBreathe } from './breathe';\nexport { behaviorDrag } from './drag';\nexport { behaviorDrawWay } from './draw_way';\nexport { behaviorDraw } from './draw';\nexport { behaviorEdit } from './edit';\nexport { behaviorHash } from './hash';\nexport { behaviorHover } from './hover';\nexport { behaviorLasso } from './lasso';\nexport { behaviorOperation } from './operation';\nexport { behaviorPaste } from './paste';\nexport { behaviorSelect } from './select';\n", "import { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiAccount(context) {\n const osm = context.connection();\n\n\n function updateUserDetails(selection) {\n if (!osm) return;\n\n if (!osm.authenticated()) { // logged out\n render(selection, null);\n } else {\n osm.userDetails((err, user) => {\n if (err && err.status === 401) {\n // 401 Unauthorized\n // cannot load own user data: there must be something wrong (e.g. API token was revoked)\n // -> log out to allow user to reauthenticate\n osm.logout();\n }\n render(selection, user);\n });\n }\n }\n\n\n function render(selection, user) {\n let userInfo = selection.select('.userInfo');\n let loginLogout = selection.select('.loginLogout');\n\n if (user) {\n userInfo\n .html('')\n .classed('hide', false);\n\n let userLink = userInfo\n .append('a')\n .attr('href', osm.userURL(user.display_name))\n .attr('target', '_blank');\n\n // Add user's image or placeholder\n if (user.image_url) {\n userLink.append('img')\n .attr('class', 'icon pre-text user-icon')\n .attr('src', user.image_url);\n } else {\n userLink\n .call(svgIcon('#iD-icon-avatar', 'pre-text light'));\n }\n\n // Add user name\n userLink.append('span')\n .attr('class', 'label')\n .text(user.display_name);\n\n // show \"Log Out\"\n loginLogout\n .classed('hide', false)\n .select('a')\n .text(t('logout'))\n .on('click', e => {\n e.preventDefault();\n osm.logout();\n // OAuth2's idea of \"logout\" is just to get rid of the bearer token.\n // If we try to \"login\" again, it will just grab the token again.\n // What a user probably _really_ expects is to logout of OSM so that they can switch users.\n // So, we open a popup with a \"Logout\" button. After logging out, they can login again using\n // the same popup window.\n osm.authenticate(undefined, { switchUser: true });\n });\n\n } else { // no user\n userInfo\n .html('')\n .classed('hide', true);\n\n // show \"Log In\"\n loginLogout\n .classed('hide', false)\n .select('a')\n .text(t('login'))\n .on('click', e => {\n e.preventDefault();\n osm.authenticate();\n });\n }\n }\n\n\n return function(selection) {\n if (!osm) return;\n\n selection.append('li')\n .attr('class', 'userInfo')\n .classed('hide', true);\n\n selection.append('li')\n .attr('class', 'loginLogout')\n .classed('hide', true)\n .append('a')\n .attr('href', '#');\n\n osm.on('change.account', () => updateUserDetails(selection));\n updateUserDetails(selection);\n };\n\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { t } from '../core/localizer';\n\n\nexport function uiAttribution(context) {\n let _selection = d3_select(null);\n\n\n function render(selection, data, klass) {\n let div = selection.selectAll(`.${klass}`)\n .data([0]);\n\n div = div.enter()\n .append('div')\n .attr('class', klass)\n .merge(div);\n\n\n let attributions = div.selectAll('.attribution')\n .data(data, d => d.id);\n\n attributions.exit()\n .remove();\n\n attributions = attributions.enter()\n .append('span')\n .attr('class', 'attribution')\n .each((d, i, nodes) => {\n let attribution = d3_select(nodes[i]);\n\n if (d.terms_html) {\n attribution.html(d.terms_html);\n return;\n }\n\n if (d.terms_url) {\n attribution = attribution\n .append('a')\n .attr('href', d.terms_url)\n .attr('target', '_blank');\n }\n\n const sourceID = d.id.replace(/\\./g, '');\n const terms_text = t(`imagery.${sourceID}.attribution.text`,\n { default: d.terms_text || d.id || d.name() }\n );\n\n if (d.icon && !d.overlay) {\n attribution\n .append('img')\n .attr('class', 'source-image')\n .attr('src', d.icon);\n }\n\n attribution\n .append('span')\n .attr('class', 'attribution-text')\n .text(terms_text);\n })\n .merge(attributions);\n\n\n let copyright = attributions.selectAll('.copyright-notice')\n .data(d => {\n let notice = d.copyrightNotices(context.map().zoom(), context.map().extent());\n return notice ? [notice] : [];\n });\n\n copyright.exit()\n .remove();\n\n copyright = copyright.enter()\n .append('span')\n .attr('class', 'copyright-notice')\n .merge(copyright);\n\n copyright\n .text(String);\n }\n\n\n function update() {\n let baselayer = context.background().baseLayerSource();\n _selection\n .call(render, (baselayer ? [baselayer] : []), 'base-layer-attribution');\n\n const z = context.map().zoom();\n let overlays = context.background().overlayLayerSources() || [];\n _selection\n .call(render, overlays.filter(s => s.validZoom(z)), 'overlay-layer-attribution');\n }\n\n\n return function(selection) {\n _selection = selection;\n\n context.background()\n .on('change.attribution', update);\n\n context.map()\n .on('move.attribution', throttle(update, 400, { leading: false }));\n\n update();\n };\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/index';\n\n\nexport function uiContributors(context) {\n var osm = context.connection(),\n debouncedUpdate = debounce(function() { update(); }, 1000),\n limit = 4,\n hidden = false,\n wrap = d3_select(null);\n\n\n function update() {\n if (!osm) return;\n\n var users = {},\n entities = context.history().intersects(context.map().extent());\n\n entities.forEach(function(entity) {\n if (entity && entity.user) users[entity.user] = true;\n });\n\n var u = Object.keys(users),\n subset = u.slice(0, u.length > limit ? limit - 1 : limit);\n\n wrap.html('')\n .call(svgIcon('#iD-icon-nearby', 'pre-text light'));\n\n const userList = selection => selection.selectAll()\n .data(subset)\n .enter()\n .append('a')\n .attr('class', 'user-link')\n .attr('href', d => osm.userURL(d))\n .attr('target', '_blank')\n .text(String);\n\n if (u.length > limit) {\n var othersNum = u.length - limit + 1;\n\n const count = selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', () => osm.changesetsURL(context.map().center(), context.map().zoom()))\n .text(othersNum);\n\n wrap.append('span')\n .call(t.append('contributors.truncated_list', { n: othersNum, users: userList, count }));\n\n } else {\n wrap.append('span')\n .call(t.append('contributors.list', { users: userList }));\n }\n\n if (!u.length) {\n hidden = true;\n wrap\n .transition()\n .style('opacity', 0);\n\n } else if (hidden) {\n wrap\n .transition()\n .style('opacity', 1);\n }\n }\n\n\n return function(selection) {\n if (!osm) return;\n wrap = selection;\n update();\n\n osm.on('loaded.contributors', debouncedUpdate);\n context.map().on('move.contributors', debouncedUpdate);\n };\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { geoVecAdd } from '../geo';\nimport { localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\nimport { utilRebind } from '../util/rebind';\nimport { utilHighlightEntities } from '../util/util';\nimport { utilGetDimensions } from '../util/dimensions';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiEditMenu(context) {\n var dispatch = d3_dispatch('toggled');\n\n var _menu = d3_select(null);\n var _operations = [];\n // the position the menu should be displayed relative to\n var _anchorLoc = [0, 0];\n var _anchorLocLonLat = [0, 0];\n // a string indicating how the menu was opened\n var _triggerType = '';\n\n var _vpTopMargin = 85; // viewport top margin\n var _vpBottomMargin = 45; // viewport bottom margin\n var _vpSideMargin = 35; // viewport side margin\n\n var _menuTop = false;\n var _menuHeight;\n var _menuWidth;\n\n // hardcode these values to make menu positioning easier\n var _verticalPadding = 4;\n\n // see also `.edit-menu .tooltip` CSS; include margin\n var _tooltipWidth = 210;\n\n // offset the menu slightly from the target location\n var _menuSideMargin = 10;\n\n var _tooltips = [];\n\n var editMenu = function(selection) {\n\n var isTouchMenu = _triggerType.includes('touch') || _triggerType.includes('pen');\n\n var ops = _operations.filter(function(op) {\n return !isTouchMenu || !op.mouseOnly;\n });\n\n if (!ops.length) return;\n\n _tooltips = [];\n\n // Position the menu above the anchor for stylus and finger input\n // since the mapper's hand likely obscures the screen below the anchor\n _menuTop = isTouchMenu;\n\n // Show labels for touch input since there aren't hover tooltips\n var showLabels = isTouchMenu;\n\n var buttonHeight = showLabels ? 32 : 34;\n if (showLabels) {\n // Get a general idea of the width based on the length of the label\n _menuWidth = 52 + Math.min(120, 6 * Math.max.apply(Math, ops.map(function(op) {\n return op.title.length;\n })));\n } else {\n _menuWidth = 44;\n }\n\n _menuHeight = _verticalPadding * 2 + ops.length * buttonHeight;\n\n _menu = selection\n .append('div')\n .attr('class', 'edit-menu')\n .classed('touch-menu', isTouchMenu)\n .style('padding', _verticalPadding + 'px 0');\n\n var buttons = _menu.selectAll('.edit-menu-item')\n .data(ops);\n\n // enter\n var buttonsEnter = buttons.enter()\n .append('button')\n .attr('class', function (d) { return 'edit-menu-item edit-menu-item-' + d.id; })\n .style('height', buttonHeight + 'px')\n .on('click', click)\n // don't listen for `mouseup` because we only care about non-mouse pointer types\n .on('pointerup', pointerup)\n .on('pointerdown mousedown', function pointerdown(d3_event) {\n // don't let button presses also act as map input - #1869\n d3_event.stopPropagation();\n })\n .on('mouseenter.highlight', function(d3_event, d) {\n if (d3_select(this).classed('disabled')) return;\n\n if (d.relatedEntityIds) {\n utilHighlightEntities(d.relatedEntityIds(), true, context);\n }\n\n if (d.getAuxiliaryGeometry) {\n drawAuxiliaryGeometry(context, d.getAuxiliaryGeometry());\n }\n })\n .on('mouseleave.highlight', function(d3_event, d) {\n if (d.relatedEntityIds) {\n utilHighlightEntities(d.relatedEntityIds(), false, context);\n }\n\n if (d.getAuxiliaryGeometry) {\n drawAuxiliaryGeometry(context, []);\n }\n });\n\n buttonsEnter.each(function(d) {\n var tooltip = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .heading(() => d.title)\n .title(d.tooltip)\n .keys([d.keys[0]]);\n\n _tooltips.push(tooltip);\n\n d3_select(this)\n .call(tooltip)\n .append('div')\n .attr('class', 'icon-wrap')\n .call(svgIcon(d.icon && d.icon() || '#iD-operation-' + d.id, 'operation'));\n });\n\n if (showLabels) {\n buttonsEnter.append('span')\n .attr('class', 'label')\n .each(function(d) {\n d3_select(this).call(d.title);\n });\n }\n\n // update\n buttonsEnter\n .merge(buttons)\n .classed('disabled', function(d) { return d.disabled(); });\n\n updatePosition();\n\n var initialScale = context.projection.scale();\n context.map()\n .on('move.edit-menu', function() {\n if (initialScale !== context.projection.scale()) {\n editMenu.close();\n }\n })\n .on('drawn.edit-menu', function(info) {\n if (info.full) updatePosition();\n });\n\n var lastPointerUpType;\n // `pointerup` is always called before `click`\n function pointerup(d3_event) {\n lastPointerUpType = d3_event.pointerType;\n }\n\n function click(d3_event, operation) {\n d3_event.stopPropagation();\n\n if (operation.relatedEntityIds) {\n utilHighlightEntities(operation.relatedEntityIds(), false, context);\n }\n\n if (operation.disabled()) {\n if (lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen') {\n // there are no tooltips for touch interactions so flash feedback instead\n context.ui().flash\n .duration(4000)\n .iconName('#iD-operation-' + operation.id)\n .iconClass('operation disabled')\n .label(operation.tooltip())();\n }\n } else {\n if (lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen') {\n context.ui().flash\n .duration(2000)\n .iconName('#iD-operation-' + operation.id)\n .iconClass('operation')\n .label(operation.annotation() || operation.title)();\n }\n\n operation();\n editMenu.close();\n }\n lastPointerUpType = null;\n }\n\n dispatch.call('toggled', this, true);\n };\n\n function updatePosition() {\n\n if (!_menu || _menu.empty()) return;\n\n var anchorLoc = context.projection(_anchorLocLonLat);\n\n var viewport = context.surfaceRect();\n\n if (anchorLoc[0] < 0 ||\n anchorLoc[0] > viewport.width ||\n anchorLoc[1] < 0 ||\n anchorLoc[1] > viewport.height) {\n // close the menu if it's gone offscreen\n\n editMenu.close();\n return;\n }\n\n var menuLeft = displayOnLeft(viewport);\n\n var offset = [0, 0];\n\n offset[0] = menuLeft ? -1 * (_menuSideMargin + _menuWidth) : _menuSideMargin;\n\n if (_menuTop) {\n if (anchorLoc[1] - _menuHeight < _vpTopMargin) {\n // menu is near top viewport edge, shift downward\n offset[1] = -anchorLoc[1] + _vpTopMargin;\n } else {\n offset[1] = -_menuHeight;\n }\n } else {\n if (anchorLoc[1] + _menuHeight > (viewport.height - _vpBottomMargin)) {\n // menu is near bottom viewport edge, shift upwards\n offset[1] = -anchorLoc[1] - _menuHeight + viewport.height - _vpBottomMargin;\n } else {\n offset[1] = 0;\n }\n }\n\n var origin = geoVecAdd(anchorLoc, offset);\n // repositioning the menu to account for the top menu height\n var _verticalOffset = parseFloat(utilGetDimensions(d3_select('.top-toolbar-wrap'))[1]);\n origin[1] -= _verticalOffset;\n\n _menu\n .style('left', origin[0] + 'px')\n .style('top', origin[1] + 'px');\n\n var tooltipSide = tooltipPosition(viewport, menuLeft);\n _tooltips.forEach(function(tooltip) {\n tooltip.placement(tooltipSide);\n });\n\n function displayOnLeft(viewport) {\n if (localizer.textDirection() === 'ltr') {\n if ((anchorLoc[0] + _menuSideMargin + _menuWidth) > (viewport.width - _vpSideMargin)) {\n // right menu would be too close to the right viewport edge, go left\n return true;\n }\n // prefer right menu\n return false;\n\n } else { // rtl\n if ((anchorLoc[0] - _menuSideMargin - _menuWidth) < _vpSideMargin) {\n // left menu would be too close to the left viewport edge, go right\n return false;\n }\n // prefer left menu\n return true;\n }\n }\n\n function tooltipPosition(viewport, menuLeft) {\n if (localizer.textDirection() === 'ltr') {\n if (menuLeft) {\n // if there's not room for a right-side menu then there definitely\n // isn't room for right-side tooltips\n return 'left';\n }\n if ((anchorLoc[0] + _menuSideMargin + _menuWidth + _tooltipWidth) > (viewport.width - _vpSideMargin)) {\n // right tooltips would be too close to the right viewport edge, go left\n return 'left';\n }\n // prefer right tooltips\n return 'right';\n\n } else { // rtl\n if (!menuLeft) {\n return 'right';\n }\n if ((anchorLoc[0] - _menuSideMargin - _menuWidth - _tooltipWidth) < _vpSideMargin) {\n // left tooltips would be too close to the left viewport edge, go right\n return 'right';\n }\n // prefer left tooltips\n return 'left';\n }\n }\n }\n\n editMenu.close = function () {\n\n context.map()\n .on('move.edit-menu', null)\n .on('drawn.edit-menu', null);\n\n _menu.remove();\n _tooltips = [];\n\n // Clean up any auxiliary overlays\n drawAuxiliaryGeometry(context, []);\n\n dispatch.call('toggled', this, false);\n };\n\n editMenu.anchorLoc = function(val) {\n if (!arguments.length) return _anchorLoc;\n _anchorLoc = val;\n _anchorLocLonLat = context.projection.invert(_anchorLoc);\n return editMenu;\n };\n\n editMenu.triggerType = function(val) {\n if (!arguments.length) return _triggerType;\n _triggerType = val;\n return editMenu;\n };\n\n editMenu.operations = function(val) {\n if (!arguments.length) return _operations;\n _operations = val;\n return editMenu;\n };\n\n return utilRebind(editMenu, dispatch, 'on');\n}\n\n\n// Helper function to draw/remove reflect axis overlay\nfunction drawAuxiliaryGeometry(context, d) {\n const surface = context.surface();\n // Append to the OSM data layer to be in the same coordinate space as map features\n const container = surface.selectAll('.data-layer.osm .auxiliary');\n const paths = container.selectAll('path')\n .data(d, d => d.id);\n\n paths.exit().remove();\n const enter = paths.enter()\n .append('path');\n\n enter.merge(paths)\n .attr('class', d => d.klass)\n .attr('d', d => d.path);\n}\n", "import { t } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\n\nexport function uiFeatureInfo(context) {\n function update(selection) {\n var features = context.features();\n var stats = features.stats();\n var dateMatchCount = features.dateMatchCount();\n var count = 0;\n var hiddenList = features.hidden().map(function(k) {\n if (stats[k]) {\n count += stats[k];\n return t.append('inspector.title_count', {\n title: t('feature.' + k + '.description'),\n count: stats[k]\n });\n }\n return null;\n }).filter(Boolean);\n count += dateMatchCount;\n\n selection.text('');\n\n if (hiddenList.length || dateMatchCount > 0) {\n var tooltipBehavior = uiTooltip()\n .placement('top')\n .title(function() {\n return selection => {\n hiddenList.forEach(hiddenFeature => {\n selection.append('div').call(hiddenFeature);\n });\n };\n });\n\n selection.append('a')\n .attr('class', 'chip')\n .attr('href', '#')\n .call(t.append('feature_info.hidden_warning', { count: count }))\n .call(tooltipBehavior)\n .on('click', function(d3_event) {\n tooltipBehavior.hide();\n d3_event.preventDefault();\n // open the Map Data pane\n context.ui().togglePanes(context.container().select('.map-panes .map-data-pane'));\n });\n }\n\n selection\n .classed('hide', !hiddenList.length && !dateMatchCount);\n }\n\n\n return function(selection) {\n update(selection);\n\n context.features().on('change.feature_info', function() {\n update(selection);\n });\n };\n}\n", "import { timeout as d3_timeout } from 'd3-timer';\n\nexport function uiFlash(context) {\n var _flashTimer;\n\n var _duration = 2000;\n var _iconName = '#iD-icon-no';\n var _iconClass = 'disabled';\n var _label = s => s.text('');\n\n function flash() {\n if (_flashTimer) {\n _flashTimer.stop();\n }\n\n context.container().select('.main-footer-wrap')\n .classed('footer-hide', true)\n .classed('footer-show', false);\n context.container().select('.flash-wrap')\n .classed('footer-hide', false)\n .classed('footer-show', true);\n\n var content = context.container().select('.flash-wrap').selectAll('.flash-content')\n .data([0]);\n\n // Enter\n var contentEnter = content.enter()\n .append('div')\n .attr('class', 'flash-content');\n\n var iconEnter = contentEnter\n .append('svg')\n .attr('class', 'flash-icon icon')\n .append('g')\n .attr('transform', 'translate(10,10)');\n\n iconEnter\n .append('circle')\n .attr('r', 9);\n\n iconEnter\n .append('use')\n .attr('transform', 'translate(-7,-7)')\n .attr('width', '14')\n .attr('height', '14');\n\n contentEnter\n .append('div')\n .attr('class', 'flash-text');\n\n\n // Update\n content = content\n .merge(contentEnter);\n\n content\n .selectAll('.flash-icon')\n .attr('class', 'icon flash-icon ' + (_iconClass || ''));\n\n content\n .selectAll('.flash-icon use')\n .attr('xlink:href', _iconName);\n\n content\n .selectAll('.flash-text')\n .attr('class', 'flash-text')\n .call(_label);\n\n\n _flashTimer = d3_timeout(function() {\n _flashTimer = null;\n context.container().select('.main-footer-wrap')\n .classed('footer-hide', false)\n .classed('footer-show', true);\n context.container().select('.flash-wrap')\n .classed('footer-hide', true)\n .classed('footer-show', false);\n }, _duration);\n\n return content;\n }\n\n\n flash.duration = function(_) {\n if (!arguments.length) return _duration;\n _duration = _;\n return flash;\n };\n\n flash.label = function(_) {\n if (!arguments.length) return _label;\n if (typeof _ !== 'function') {\n _label = selection => selection.text(_);\n } else {\n _label = selection => selection.text('').call(_);\n }\n return flash;\n };\n\n flash.iconName = function(_) {\n if (!arguments.length) return _iconName;\n _iconName = _;\n return flash;\n };\n\n flash.iconClass = function(_) {\n if (!arguments.length) return _iconClass;\n _iconClass = _;\n return flash;\n };\n\n return flash;\n}\n", "import { uiCmd } from './cmd';\nimport { utilDetect } from '../util/detect';\n\nexport function uiFullScreen(context) {\n var element = context.container().node();\n // var button = d3_select(null);\n\n\n function getFullScreenFn() {\n if (element.requestFullscreen) {\n return element.requestFullscreen;\n } else if (element.msRequestFullscreen) {\n return element.msRequestFullscreen;\n } else if (element.mozRequestFullScreen) {\n return element.mozRequestFullScreen;\n } else if (element.webkitRequestFullscreen) {\n return element.webkitRequestFullscreen;\n }\n }\n\n\n function getExitFullScreenFn() {\n if (document.exitFullscreen) {\n return document.exitFullscreen;\n } else if (document.msExitFullscreen) {\n return document.msExitFullscreen;\n } else if (document.mozCancelFullScreen) {\n return document.mozCancelFullScreen;\n } else if (document.webkitExitFullscreen) {\n return document.webkitExitFullscreen;\n }\n }\n\n\n function isFullScreen() {\n return document.fullscreenElement ||\n document.mozFullScreenElement ||\n document.webkitFullscreenElement ||\n document.msFullscreenElement;\n }\n\n\n function isSupported() {\n return !!getFullScreenFn();\n }\n\n\n function fullScreen(d3_event) {\n d3_event.preventDefault();\n if (!isFullScreen()) {\n // button.classed('active', true);\n getFullScreenFn().apply(element);\n } else {\n // button.classed('active', false);\n getExitFullScreenFn().apply(document);\n }\n }\n\n\n return function() { // selection) {\n if (!isSupported()) return;\n\n // button = selection.append('button')\n // .attr('title', t('full_screen'))\n // .on('click', fullScreen)\n // .call(tooltip);\n\n // button.append('span')\n // .attr('class', 'icon full-screen');\n\n var detected = utilDetect();\n var keys = (detected.os === 'mac' ? [uiCmd('\u2303\u2318F'), 'f11'] : ['f11']);\n context.keybinding().on(keys, fullScreen);\n };\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t, localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\nimport { geoExtent } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\nimport { uiLoading } from './loading';\n\nexport function uiGeolocate(context) {\n var _geolocationOptions = {\n // prioritize speed and power usage over precision\n enableHighAccuracy: false,\n // don't hang indefinitely getting the location\n timeout: 6000 // 6sec\n };\n var _locating = uiLoading(context).message(t.addOrUpdate('geolocate.locating')).blocking(true);\n var _layer = context.layers().layer('geolocate');\n var _position;\n var _extent;\n var _timeoutID;\n var _button = d3_select(null);\n\n function click() {\n if (context.inIntro()) return;\n if (!_layer.enabled() && !_locating.isShown()) {\n\n // This timeout ensures that we still call finish() even if\n // the user declines to share their location in Firefox\n _timeoutID = setTimeout(error, 10000 /* 10sec */ );\n\n context.container().call(_locating);\n // get the latest position even if we already have one\n navigator.geolocation.getCurrentPosition(success, error, _geolocationOptions);\n } else {\n _locating.close();\n _layer.enabled(null, false);\n updateButtonState();\n }\n }\n\n function zoomTo() {\n context.enter(modeBrowse(context));\n\n var map = context.map();\n _layer.enabled(_position, true);\n updateButtonState();\n map.centerZoomEase(_extent.center(), Math.min(20, map.extentZoom(_extent)));\n }\n\n function success(geolocation) {\n _position = geolocation;\n var coords = _position.coords;\n _extent = geoExtent([coords.longitude, coords.latitude]).padByMeters(coords.accuracy);\n zoomTo();\n finish();\n }\n\n function error() {\n if (_position) {\n // use the position from a previous call if we have one\n zoomTo();\n } else {\n context.ui().flash\n .label(t.append('geolocate.location_unavailable'))\n .iconName('#iD-icon-geolocate')();\n }\n\n finish();\n }\n\n function finish() {\n _locating.close(); // unblock ui\n if (_timeoutID) { clearTimeout(_timeoutID); }\n _timeoutID = undefined;\n }\n\n function updateButtonState() {\n _button.classed('active', _layer.enabled());\n _button.attr('aria-pressed', _layer.enabled());\n }\n\n return function(selection) {\n if (!navigator.geolocation || !navigator.geolocation.getCurrentPosition) return;\n\n _button = selection\n .append('button')\n .on('click', click)\n .attr('aria-pressed', false)\n .call(svgIcon('#iD-icon-geolocate', 'light'))\n .call(uiTooltip()\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(() => t.append('geolocate.title'))\n );\n };\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { t } from '../../core/localizer';\n\n\nexport function uiPanelBackground(context) {\n const background = context.background();\n let _currSource = null;\n let _metadata = {};\n const _metadataKeys = [\n 'zoom', 'vintage', 'source', 'description', 'resolution', 'accuracy'\n ];\n\n var debouncedRedraw = debounce(redraw, 250);\n\n function redraw(selection) {\n var source = background.baseLayerSource();\n if (!source) return;\n\n if (_currSource?.id !== source.id) {\n _currSource = source;\n _metadata = {};\n }\n\n selection.text('');\n\n var list = selection\n .append('ul')\n .attr('class', 'background-info');\n\n list\n .append('li')\n .call(_currSource.label());\n\n _metadataKeys.forEach(function(k) {\n list\n .append('li')\n .attr('class', 'background-info-list-' + k)\n .classed('hide', !_metadata[k])\n .call(t.append('info_panels.background.' + k, { suffix: ':' }))\n .append('span')\n .attr('class', 'background-info-span-' + k)\n .text(_metadata[k]);\n });\n\n debouncedGetMetadata(selection);\n\n var toggleTiles = context.getDebug('tile') ? 'hide_tiles' : 'show_tiles';\n\n selection\n .append('a')\n .call(t.append('info_panels.background.' + toggleTiles))\n .attr('href', '#')\n .attr('class', 'button button-toggle-tiles')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.setDebug('tile', !context.getDebug('tile'));\n selection.call(redraw);\n });\n }\n\n\n var debouncedGetMetadata = debounce(getMetadata, 250);\n\n function getMetadata(selection) {\n var tile = context.container().select('.layer-background img.tile-center'); // tile near viewport center\n if (tile.empty()) return;\n\n var sourceId = _currSource.id;\n var d = tile.datum();\n var zoom = (d && d.length >= 3 && d[2]) || Math.floor(context.map().zoom());\n var center = context.map().center();\n\n // update zoom\n _metadata.zoom = String(zoom);\n selection.selectAll('.background-info-list-zoom')\n .classed('hide', false)\n .selectAll('.background-info-span-zoom')\n .text(_metadata.zoom);\n\n if (!d || !d.length >= 3) return;\n\n background.baseLayerSource().getMetadata(center, d, function(err, result) {\n if (err || _currSource.id !== sourceId) return;\n\n // update vintage\n var vintage = result.vintage;\n _metadata.vintage = (vintage && vintage.range) || t('info_panels.background.unknown');\n selection.selectAll('.background-info-list-vintage')\n .classed('hide', false)\n .selectAll('.background-info-span-vintage')\n .text(_metadata.vintage);\n\n // update other _metadata\n _metadataKeys.forEach(function(k) {\n if (k === 'zoom' || k === 'vintage') return; // done already\n var val = result[k];\n _metadata[k] = val;\n selection.selectAll('.background-info-list-' + k)\n .classed('hide', !val)\n .selectAll('.background-info-span-' + k)\n .text(val);\n });\n });\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.map()\n .on('drawn.info-background', function() {\n selection.call(debouncedRedraw);\n })\n .on('move.info-background', function() {\n selection.call(debouncedGetMetadata);\n });\n\n };\n\n panel.off = function() {\n context.map()\n .on('drawn.info-background', null)\n .on('move.info-background', null);\n };\n\n panel.id = 'background';\n panel.label = t.append('info_panels.background.title');\n panel.key = t('info_panels.background.key');\n\n\n return panel;\n}\n", "import { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\n\n\nexport function uiPanelHistory(context) {\n var osm;\n\n function displayTimestamp(timestamp) {\n if (!timestamp) return t('info_panels.history.unknown');\n var options = {\n day: 'numeric', month: 'short', year: 'numeric',\n hour: 'numeric', minute: 'numeric', second: 'numeric'\n };\n var d = new Date(timestamp);\n if (isNaN(d.getTime())) return t('info_panels.history.unknown');\n return d.toLocaleString(localizer.localeCode(), options);\n }\n\n\n function displayUser(selection, userName) {\n if (!userName) {\n selection\n .append('span')\n .call(t.append('info_panels.history.unknown'));\n return;\n }\n\n selection\n .append('span')\n .attr('class', 'user-name')\n .text(userName);\n\n var links = selection\n .append('div')\n .attr('class', 'links');\n\n if (osm) {\n links\n .append('a')\n .attr('class', 'user-osm-link')\n .attr('href', osm.userURL(userName))\n .attr('target', '_blank')\n .call(t.append('info_panels.history.profile_link'));\n }\n }\n\n\n function displayChangeset(selection, changeset) {\n if (!changeset) {\n selection\n .append('span')\n .call(t.append('info_panels.history.unknown'));\n return;\n }\n\n selection\n .append('span')\n .attr('class', 'changeset-id')\n .text(changeset);\n\n var links = selection\n .append('div')\n .attr('class', 'links');\n\n if (osm) {\n links\n .append('a')\n .attr('class', 'changeset-osm-link')\n .attr('href', osm.changesetURL(changeset))\n .attr('target', '_blank')\n .call(t.append('info_panels.history.changeset_link'));\n }\n\n links\n .append('a')\n .attr('class', 'changeset-osmcha-link')\n .attr('href', 'https://osmcha.openhistoricalmap.org/changesets/' + changeset)\n .attr('target', '_blank')\n .text('OSMCha');\n }\n\n\n function redraw(selection) {\n var selectedNoteID = context.selectedNoteID();\n osm = context.connection();\n var selected, note, entity;\n if (selectedNoteID && osm) { // selected 1 note\n selected = [ t.append('note.note', { suffix: ' ' + selectedNoteID }) ];\n note = osm.getNote(selectedNoteID);\n } else { // selected 1..n entities\n selected = context.selectedIDs()\n .filter(function(e) { return context.hasEntity(e); });\n if (selected.length) {\n entity = context.entity(selected[0]);\n }\n }\n\n var singular = selected.length === 1 ? selected[0] : null;\n\n selection.text('');\n\n const heading = selection\n .append('h4')\n .attr('class', 'history-heading');\n\n if (singular) {\n if (typeof singular === 'function') {\n heading.call(singular);\n } else {\n heading.text(singular);\n }\n } else {\n heading.call(t.append('info_panels.selected', { n: selected.length }));\n }\n\n if (!singular) return;\n\n if (entity) {\n selection.call(redrawEntity, entity);\n } else if (note) {\n selection.call(redrawNote, note);\n }\n }\n\n\n function redrawNote(selection, note) {\n if (!note || note.isNew()) {\n selection\n .append('div')\n .call(t.append('info_panels.history.note_no_history'));\n return;\n }\n\n var list = selection\n .append('ul');\n\n list\n .append('li')\n .call(t.append('info_panels.history.note_comments', { suffix: ':' }))\n .append('span')\n .text(note.comments.length);\n\n if (note.comments.length) {\n list\n .append('li')\n .call(t.append('info_panels.history.note_created_date', { suffix: ':' }))\n .append('span')\n .text(displayTimestamp(note.comments[0].date));\n\n list\n .append('li')\n .call(t.append('info_panels.history.note_created_user', { suffix: ':' }))\n .call(displayUser, note.comments[0].user);\n }\n\n if (osm) {\n selection\n .append('a')\n .attr('class', 'view-history-on-osm')\n .attr('target', '_blank')\n .attr('href', osm.noteURL(note))\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append('info_panels.history.note_link_text'));\n }\n }\n\n\n function redrawEntity(selection, entity) {\n if (!entity || entity.isNew()) {\n selection\n .append('div')\n .call(t.append('info_panels.history.no_history'));\n return;\n }\n\n var links = selection\n .append('div')\n .attr('class', 'links');\n\n if (osm) {\n links\n .append('a')\n .attr('class', 'view-history-on-osm')\n .attr('href', osm.historyURL(entity))\n .attr('target', '_blank')\n .call(t.append('info_panels.history.history_link'));\n }\n\n var list = selection\n .append('ul');\n\n list\n .append('li')\n .call(t.append('info_panels.history.version', { suffix: ':' }))\n .append('span')\n .text(entity.version);\n\n list\n .append('li')\n .call(t.append('info_panels.history.last_edit', { suffix: ':' }))\n .append('span')\n .text(displayTimestamp(entity.timestamp));\n\n list\n .append('li')\n .call(t.append('info_panels.history.edited_by', { suffix: ':' }))\n .call(displayUser, entity.user);\n\n list\n .append('li')\n .call(t.append('info_panels.history.changeset', { suffix: ':' }))\n .call(displayChangeset, entity.changeset);\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.map()\n .on('drawn.info-history', function() {\n selection.call(redraw);\n });\n\n context\n .on('enter.info-history', function() {\n selection.call(redraw);\n });\n };\n\n panel.off = function() {\n context.map().on('drawn.info-history', null);\n context.on('enter.info-history', null);\n };\n\n panel.id = 'history';\n panel.label = t.append('info_panels.history.title');\n panel.key = t('info_panels.history.key');\n\n\n return panel;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { decimalCoordinatePair, dmsCoordinatePair } from '../../util/units';\nimport { t } from '../../core/localizer';\nimport { services } from '../../services';\n\n\nexport function uiPanelLocation(context) {\n var currLocation = '';\n\n\n function redraw(selection) {\n selection.html('');\n\n var list = selection\n .append('ul');\n\n // Mouse coordinates\n var coord = context.map().mouseCoordinates();\n if (coord.some(isNaN)) {\n coord = context.map().center();\n }\n\n list\n .append('li')\n .text(dmsCoordinatePair(coord))\n .append('li')\n .text(decimalCoordinatePair(coord));\n\n // Location Info\n selection\n .append('div')\n .attr('class', 'location-info')\n .text(currLocation || ' ');\n\n debouncedGetLocation(selection, coord);\n }\n\n\n var debouncedGetLocation = debounce(getLocation, 250);\n function getLocation(selection, coord) {\n if (!services.geocoder) {\n currLocation = t('info_panels.location.unknown_location');\n selection.selectAll('.location-info')\n .text(currLocation);\n } else {\n services.geocoder.reverse(coord, function(err, result) {\n currLocation = result ? result.display_name : t('info_panels.location.unknown_location');\n selection.selectAll('.location-info')\n .text(currLocation);\n });\n }\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.surface()\n .on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'move.info-location', function() {\n selection.call(redraw);\n });\n };\n\n panel.off = function() {\n context.surface()\n .on('.info-location', null);\n };\n\n panel.id = 'location';\n panel.label = t.append('info_panels.location.title');\n panel.key = t('info_panels.location.key');\n\n\n return panel;\n}\n", "import {\n geoLength as d3_geoLength,\n geoPath as d3_geoPath\n} from 'd3-geo';\n\nimport { t, localizer } from '../../core/localizer';\nimport { displayArea, displayLength, decimalCoordinatePair, dmsCoordinatePair } from '../../util/units';\nimport { geoExtent, geoSphericalDistance } from '../../geo';\nimport { services } from '../../services';\nimport { utilGetAllNodes } from '../../util';\n\nexport function uiPanelMeasurement(context) {\n\n function radiansToMeters(r) {\n // using WGS84 authalic radius (6371007.1809 m)\n return r * 6371007.1809;\n }\n\n function steradiansToSqmeters(r) {\n // http://gis.stackexchange.com/a/124857/40446\n return r / (4 * Math.PI) * 510065621724000;\n }\n\n\n function toLineString(feature) {\n if (feature.type === 'LineString') return feature;\n\n var result = { type: 'LineString', coordinates: [] };\n if (feature.type === 'Polygon') {\n result.coordinates = feature.coordinates[0];\n } else if (feature.type === 'MultiPolygon') {\n result.coordinates = feature.coordinates[0][0];\n }\n\n return result;\n }\n\n var _isImperial = !localizer.usesMetric();\n\n function redraw(selection) {\n var graph = context.graph();\n var selectedNoteID = context.selectedNoteID();\n var osm = services.osm;\n\n var localeCode = localizer.localeCode();\n\n var heading;\n var center, location, centroid;\n var closed, geometry;\n var totalNodeCount, length = 0, area = 0, distance;\n\n if (selectedNoteID && osm) { // selected 1 note\n var note = osm.getNote(selectedNoteID);\n heading = t.append('note.note', { suffix: ' ' + selectedNoteID });\n location = note.loc;\n geometry = 'note';\n\n } else { // selected 1..n entities\n var selectedIDs = context.selectedIDs().filter(function(id) {\n return context.hasEntity(id);\n });\n var selected = selectedIDs.map(function(id) {\n return context.entity(id);\n });\n\n heading = selected.length === 1 ? selected[0].id :\n t.append('info_panels.selected', { n: selected.length });\n\n if (selected.length) {\n var extent = geoExtent();\n for (var i in selected) {\n var entity = selected[i];\n extent._extend(entity.extent(graph));\n\n geometry = entity.geometry(graph);\n if (geometry === 'line' || geometry === 'area') {\n closed = (entity.type === 'relation') || (entity.isClosed() && !entity.isDegenerate());\n var feature = entity.asGeoJSON(graph);\n length += radiansToMeters(d3_geoLength(toLineString(feature)));\n centroid = d3_geoPath(context.projection).centroid(entity.asGeoJSON(graph));\n centroid = centroid && context.projection.invert(centroid);\n if (!centroid || !isFinite(centroid[0]) || !isFinite(centroid[1])) {\n centroid = entity.extent(graph).center();\n }\n if (closed) {\n area += steradiansToSqmeters(entity.area(graph));\n }\n }\n }\n\n if (selected.length > 1) {\n geometry = null;\n closed = null;\n centroid = null;\n }\n\n if (selected.length === 2 &&\n selected[0].type === 'node' &&\n selected[1].type === 'node') {\n distance = geoSphericalDistance(selected[0].loc, selected[1].loc);\n }\n\n if (selected.length === 1 && selected[0].type === 'node') {\n location = selected[0].loc;\n } else {\n totalNodeCount = utilGetAllNodes(selectedIDs, context.graph()).length;\n }\n\n if (!location && !centroid) {\n center = extent.center();\n }\n }\n }\n\n selection.html('');\n\n if (heading && typeof heading === 'function') {\n selection\n .append('h4')\n .attr('class', 'measurement-heading')\n .call(heading);\n } else {\n selection\n .append('h4')\n .attr('class', 'measurement-heading')\n .text(heading);\n }\n\n var list = selection\n .append('ul');\n var coordItem;\n\n if (geometry) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.geometry', { suffix: ':' }))\n .append('span')\n .call(\n closed ? t.append('info_panels.measurement.closed_' + geometry) : t.append('geometry.' + geometry)\n );\n }\n\n if (totalNodeCount) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.node_count', { suffix: ':' }))\n .append('span')\n .text(totalNodeCount.toLocaleString(localeCode));\n }\n\n if (area) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.area', { suffix: ':' }))\n .append('span')\n .text(displayArea(area, _isImperial));\n }\n\n if (length) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.' + (closed ? 'perimeter' : 'length'), { suffix: ':' }))\n .append('span')\n .text(displayLength(length, _isImperial));\n }\n\n if (typeof distance === 'number') {\n list\n .append('li')\n .call(t.append('info_panels.measurement.distance', { suffix: ':' }))\n .append('span')\n .text(displayLength(distance, _isImperial));\n }\n\n if (location) {\n coordItem = list\n .append('li')\n .call(t.append('info_panels.measurement.location', { suffix: ':' }));\n coordItem.append('span')\n .text(dmsCoordinatePair(location));\n coordItem.append('span')\n .text(decimalCoordinatePair(location));\n }\n\n if (centroid) {\n coordItem = list\n .append('li')\n .call(t.append('info_panels.measurement.centroid', { suffix: ':' }));\n coordItem.append('span')\n .text(dmsCoordinatePair(centroid));\n coordItem.append('span')\n .text(decimalCoordinatePair(centroid));\n }\n\n if (center) {\n coordItem = list\n .append('li')\n .call(t.append('info_panels.measurement.center', { suffix: ':' }));\n coordItem.append('span')\n .text(dmsCoordinatePair(center));\n coordItem.append('span')\n .text(decimalCoordinatePair(center));\n }\n\n if (length || area || typeof distance === 'number') {\n var toggle = _isImperial ? 'imperial' : 'metric';\n selection\n .append('a')\n .call(t.append('info_panels.measurement.' + toggle))\n .attr('href', '#')\n .attr('class', 'button button-toggle-units')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n _isImperial = !_isImperial;\n selection.call(redraw);\n });\n }\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.map()\n .on('drawn.info-measurement', function() {\n selection.call(redraw);\n });\n\n context\n .on('enter.info-measurement', function() {\n selection.call(redraw);\n });\n };\n\n panel.off = function() {\n context.map().on('drawn.info-measurement', null);\n context.on('enter.info-measurement', null);\n };\n\n panel.id = 'measurement';\n panel.label = t.append('info_panels.measurement.title');\n panel.key = t('info_panels.measurement.key');\n\n\n return panel;\n}\n", "export * from './background';\nexport * from './history';\nexport * from './location';\nexport * from './measurement';\n\nimport { uiPanelBackground } from './background';\nimport { uiPanelHistory } from './history';\nimport { uiPanelLocation } from './location';\nimport { uiPanelMeasurement } from './measurement';\n\nexport var uiInfoPanels = {\n background: uiPanelBackground,\n history: uiPanelHistory,\n location: uiPanelLocation,\n measurement: uiPanelMeasurement,\n};\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\nimport { uiInfoPanels } from './panels';\n\n\nexport function uiInfo(context) {\n var ids = Object.keys(uiInfoPanels);\n var wasActive = ['measurement'];\n var panels = {};\n var active = {};\n\n // create panels\n ids.forEach(function(k) {\n if (!panels[k]) {\n panels[k] = uiInfoPanels[k](context);\n active[k] = false;\n }\n });\n\n\n function info(selection) {\n\n function redraw() {\n var activeids = ids.filter(function(k) { return active[k]; }).sort();\n\n var containers = infoPanels.selectAll('.panel-container')\n .data(activeids, function(k) { return k; });\n\n containers.exit()\n .style('opacity', 1)\n .transition()\n .duration(200)\n .style('opacity', 0)\n .on('end', function(d) {\n d3_select(this)\n .call(panels[d].off)\n .remove();\n });\n\n var enter = containers.enter()\n .append('div')\n .attr('class', function(d) { return 'fillD2 panel-container panel-container-' + d; });\n\n enter\n .style('opacity', 0)\n .transition()\n .duration(200)\n .style('opacity', 1);\n\n var title = enter\n .append('div')\n .attr('class', 'panel-title fillD2');\n\n title\n .append('h3')\n .each(function(d) { return panels[d].label(d3_select(this)); });\n\n title\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function(d3_event, d) {\n d3_event.stopImmediatePropagation();\n d3_event.preventDefault();\n info.toggle(d);\n })\n .call(svgIcon('#iD-icon-close'));\n\n enter\n .append('div')\n .attr('class', function(d) { return 'panel-content panel-content-' + d; });\n\n\n // redraw the panels\n infoPanels.selectAll('.panel-content')\n .each(function(d) {\n d3_select(this).call(panels[d]);\n });\n }\n\n\n info.toggle = function(which) {\n var activeids = ids.filter(function(k) { return active[k]; });\n\n if (which) { // toggle one\n active[which] = !active[which];\n if (activeids.length === 1 && activeids[0] === which) { // none active anymore\n wasActive = [which];\n }\n\n context.container().select('.' + which + '-panel-toggle-item')\n .classed('active', active[which])\n .select('input')\n .property('checked', active[which]);\n\n } else { // toggle all\n if (activeids.length) {\n wasActive = activeids;\n activeids.forEach(function(k) { active[k] = false; });\n } else {\n wasActive.forEach(function(k) { active[k] = true; });\n }\n }\n\n redraw();\n };\n\n\n var infoPanels = selection.selectAll('.info-panels')\n .data([0]);\n\n infoPanels = infoPanels.enter()\n .append('div')\n .attr('class', 'info-panels')\n .merge(infoPanels);\n\n redraw();\n\n context.keybinding()\n .on(uiCmd('\u2318' + t('info_panels.key')), function(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.stopImmediatePropagation();\n d3_event.preventDefault();\n info.toggle();\n });\n\n ids.forEach(function(k) {\n var key = t('info_panels.' + k + '.key', { default: null });\n if (!key) return;\n context.keybinding()\n .on(uiCmd('\u2318\u21E7' + key), function(d3_event) {\n d3_event.stopImmediatePropagation();\n d3_event.preventDefault();\n info.toggle(k);\n });\n });\n }\n\n return info;\n}\n", "import { easeLinear as d3_easeLinear } from 'd3-ease';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { localizer } from '../core/localizer';\nimport { uiToggle } from './toggle';\n\n\n// Tooltips and svg mask used to highlight certain features\nexport function uiCurtain(containerNode) {\n\n var surface = d3_select(null),\n tooltip = d3_select(null),\n darkness = d3_select(null);\n\n function curtain(selection) {\n surface = selection\n .append('svg')\n .attr('class', 'curtain')\n .style('top', 0)\n .style('left', 0);\n\n darkness = surface.append('path')\n .attr('x', 0)\n .attr('y', 0)\n .attr('class', 'curtain-darkness');\n\n d3_select(window).on('resize.curtain', resize);\n\n tooltip = selection.append('div')\n .attr('class', 'tooltip');\n\n tooltip\n .append('div')\n .attr('class', 'popover-arrow');\n\n tooltip\n .append('div')\n .attr('class', 'popover-inner');\n\n resize();\n\n\n function resize() {\n surface\n .attr('width', containerNode.clientWidth)\n .attr('height', containerNode.clientHeight);\n curtain.cut(darkness.datum());\n }\n }\n\n\n /**\n * Reveal cuts the curtain to highlight the given box,\n * and shows a tooltip with instructions next to the box.\n *\n * @param {String|ClientRect|HTMLElement} [box] box used to cut the curtain\n * @param {String} [text] text for a tooltip\n * @param {Object} [options]\n * @param {string} [options.tooltipClass] optional class to add to the tooltip\n * @param {integer} [options.duration] transition time in milliseconds\n * @param {string} [options.buttonText] if set, create a button with this text label\n * @param {function} [options.buttonCallback] if set, the callback for the button\n * @param {function} [options.padding] extra margin in px to put around bbox\n * @param {String|ClientRect} [options.tooltipBox] box for tooltip position, if different from box for the curtain\n */\n curtain.reveal = function(box, html, options) {\n options = options || {};\n\n if (typeof box === 'string') {\n box = d3_select(box).node();\n }\n if (box && box.getBoundingClientRect) {\n box = copyBox(box.getBoundingClientRect());\n }\n if (box) {\n var containerRect = containerNode.getBoundingClientRect();\n box.top -= containerRect.top;\n box.left -= containerRect.left;\n }\n if (box && options.padding) {\n box.top -= options.padding;\n box.left -= options.padding;\n box.bottom += options.padding;\n box.right += options.padding;\n box.height += options.padding * 2;\n box.width += options.padding * 2;\n }\n\n var tooltipBox;\n if (options.tooltipBox) {\n tooltipBox = options.tooltipBox;\n if (typeof tooltipBox === 'string') {\n tooltipBox = d3_select(tooltipBox).node();\n }\n if (tooltipBox && tooltipBox.getBoundingClientRect) {\n tooltipBox = copyBox(tooltipBox.getBoundingClientRect());\n }\n } else {\n tooltipBox = box;\n }\n\n if (tooltipBox && html) {\n\n if (html.indexOf('**') !== -1) {\n if (html.indexOf(')(.+?)(\\*\\*)/, '$1$2$3');\n } else {\n html = html.replace(/^(.+?)(\\*\\*)/, '$1$2');\n }\n // pseudo markdown bold text for the instruction section..\n html = html.replace(/\\*\\*(.*?)\\*\\*/g, '$1');\n }\n\n html = html.replace(/\\*(.*?)\\*/g, '$1'); // emphasis\n html = html.replace(/\\{br\\}/g, '

    '); // linebreak\n\n if (options.buttonText && options.buttonCallback) {\n html += '
    ' +\n '
    ';\n }\n\n var classes = 'curtain-tooltip popover tooltip arrowed in ' + (options.tooltipClass || '');\n tooltip\n .classed(classes, true)\n .selectAll('.popover-inner')\n .html(html);\n\n if (options.buttonText && options.buttonCallback) {\n var button = tooltip.selectAll('.button-section .button.action');\n button\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n options.buttonCallback();\n });\n }\n\n var tip = copyBox(tooltip.node().getBoundingClientRect()),\n w = containerNode.clientWidth,\n h = containerNode.clientHeight,\n tooltipWidth = 200,\n tooltipArrow = 5,\n side, pos;\n\n\n // hack: this will have bottom placement,\n // so need to reserve extra space for the tooltip illustration.\n if (options.tooltipClass === 'intro-mouse') {\n tip.height += 80;\n }\n\n // trim box dimensions to just the portion that fits in the container..\n if (tooltipBox.top + tooltipBox.height > h) {\n tooltipBox.height -= (tooltipBox.top + tooltipBox.height - h);\n }\n if (tooltipBox.left + tooltipBox.width > w) {\n tooltipBox.width -= (tooltipBox.left + tooltipBox.width - w);\n }\n\n // determine tooltip placement..\n const onLeftOrRightEdge = tooltipBox.left + tooltipBox.width / 2 > w - 100 || tooltipBox.left + tooltipBox.width / 2 < 100;\n if (tooltipBox.top + tooltipBox.height < 100 && !onLeftOrRightEdge) {\n // tooltip below box..\n side = 'bottom';\n pos = [\n tooltipBox.left + tooltipBox.width / 2 - tip.width / 2,\n tooltipBox.top + tooltipBox.height\n ];\n\n } else if (tooltipBox.top > h - 140 && !onLeftOrRightEdge) {\n // tooltip above box..\n side = 'top';\n pos = [\n tooltipBox.left + tooltipBox.width / 2 - tip.width / 2,\n tooltipBox.top - tip.height\n ];\n\n } else {\n // tooltip to the side of the tooltipBox..\n var tipY = tooltipBox.top + tooltipBox.height / 2 - tip.height / 2;\n\n if (localizer.textDirection() === 'rtl') {\n if (tooltipBox.left - tooltipWidth - tooltipArrow < 70) {\n side = 'right';\n pos = [tooltipBox.left + tooltipBox.width + tooltipArrow, tipY];\n\n } else {\n side = 'left';\n pos = [tooltipBox.left - tooltipWidth - tooltipArrow, tipY];\n }\n\n } else {\n if (tooltipBox.left + tooltipBox.width + tooltipArrow + tooltipWidth > w - 70) {\n side = 'left';\n pos = [tooltipBox.left - tooltipWidth - tooltipArrow, tipY];\n } else {\n side = 'right';\n pos = [tooltipBox.left + tooltipBox.width + tooltipArrow, tipY];\n }\n }\n }\n\n if (options.duration !== 0 || !tooltip.classed(side)) {\n tooltip.call(uiToggle(true));\n }\n\n tooltip\n .style('top', pos[1] + 'px')\n .style('left', pos[0] + 'px')\n .attr('class', classes + ' ' + side);\n\n\n // shift popover-inner if it is very close to the top or bottom edge\n // (doesn't affect the placement of the popover-arrow)\n var shiftY = 0;\n if (side === 'left' || side === 'right') {\n if (pos[1] < 60) {\n shiftY = 60 - pos[1];\n } else if (pos[1] + tip.height > h - 100) {\n shiftY = h - pos[1] - tip.height - 100;\n }\n }\n tooltip.selectAll('.popover-inner')\n .style('top', shiftY + 'px');\n\n } else {\n tooltip\n .classed('in', false)\n .call(uiToggle(false));\n }\n\n curtain.cut(box, options.duration);\n\n return tooltip;\n };\n\n\n curtain.cut = function(datum, duration) {\n darkness.datum(datum)\n .interrupt();\n\n var selection;\n if (duration === 0) {\n selection = darkness;\n } else {\n selection = darkness\n .transition()\n .duration(duration || 600)\n .ease(d3_easeLinear);\n }\n\n selection\n .attr('d', function(d) {\n var containerWidth = containerNode.clientWidth;\n var containerHeight = containerNode.clientHeight;\n var string = 'M 0,0 L 0,' + containerHeight + ' L ' +\n containerWidth + ',' + containerHeight + 'L' +\n containerWidth + ',0 Z';\n\n if (!d) return string;\n return string + 'M' +\n d.left + ',' + d.top + 'L' +\n d.left + ',' + (d.top + d.height) + 'L' +\n (d.left + d.width) + ',' + (d.top + d.height) + 'L' +\n (d.left + d.width) + ',' + (d.top) + 'Z';\n\n });\n };\n\n\n curtain.remove = function() {\n surface.remove();\n tooltip.remove();\n d3_select(window).on('resize.curtain', null);\n };\n\n\n // ClientRects are immutable, so copy them to an object,\n // in case we need to trim the height/width.\n function copyBox(src) {\n return {\n top: src.top,\n right: src.right,\n bottom: src.bottom,\n left: src.left,\n width: src.width,\n height: src.height\n };\n }\n\n\n return curtain;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { helpHtml } from './helper';\nimport { t } from '../../core/localizer';\nimport { utilRebind } from '../../util/rebind';\n\n\nexport function uiIntroWelcome(context, reveal) {\n var dispatch = d3_dispatch('done');\n\n var chapter = {\n title: 'intro.welcome.title'\n };\n\n\n function welcome() {\n context.map().centerZoom([-85.63591, 41.94285], 19);\n reveal('.intro-nav-wrap .chapter-welcome',\n helpHtml('intro.welcome.welcome'),\n { buttonText: t.html('intro.ok'), buttonCallback: practice }\n );\n }\n\n function practice() {\n reveal('.intro-nav-wrap .chapter-welcome',\n helpHtml('intro.welcome.practice'),\n { buttonText: t.html('intro.ok'), buttonCallback: words }\n );\n }\n\n function words() {\n reveal('.intro-nav-wrap .chapter-welcome',\n helpHtml('intro.welcome.words'),\n { buttonText: t.html('intro.ok'), buttonCallback: chapters }\n );\n }\n\n\n function chapters() {\n dispatch.call('done');\n reveal('.intro-nav-wrap .chapter-navigation',\n helpHtml('intro.welcome.chapters', { next: t('intro.navigation.title') })\n );\n }\n\n\n chapter.enter = function() {\n welcome();\n };\n\n\n chapter.exit = function() {\n context.container().select('.curtain-tooltip.intro-mouse')\n .selectAll('.counter')\n .remove();\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pointBox, transitionTime } from './helper';\n\n\nexport function uiIntroNavigation(context, reveal) {\n var dispatch = d3_dispatch('done');\n var timeouts = [];\n var hallId = 'n2061';\n var townHall = [-85.63591, 41.94285];\n var springStreetId = 'w397';\n var springStreetEndId = 'n1834';\n var springStreet = [-85.63582, 41.94255];\n var onewayField = presetManager.field('oneway');\n var maxspeedField = presetManager.field('maxspeed');\n\n\n var chapter = {\n title: 'intro.navigation.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function isTownHallSelected() {\n var ids = context.selectedIDs();\n return ids.length === 1 && ids[0] === hallId;\n }\n\n\n function dragMap() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var msec = transitionTime(townHall, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(townHall, 19, msec);\n\n timeout(function() {\n var centerStart = context.map().center();\n\n var textId = context.lastPointerType() === 'mouse' ? 'drag' : 'drag_touch';\n var dragString = helpHtml('intro.navigation.map_info') + '{br}' + helpHtml('intro.navigation.' + textId);\n reveal('.main-map .surface', dragString);\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', dragString, { duration: 0 });\n });\n\n context.map().on('move.intro', function() {\n var centerNow = context.map().center();\n if (centerStart[0] !== centerNow[0] || centerStart[1] !== centerNow[1]) {\n context.map().on('move.intro', null);\n timeout(function() { continueTo(zoomMap); }, 3000);\n }\n });\n\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function zoomMap() {\n var zoomStart = context.map().zoom();\n\n var textId = context.lastPointerType() === 'mouse' ? 'zoom' : 'zoom_touch';\n var zoomString = helpHtml('intro.navigation.' + textId);\n\n reveal('.main-map .surface', zoomString);\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', zoomString, { duration: 0 });\n });\n\n context.map().on('move.intro', function() {\n if (context.map().zoom() !== zoomStart) {\n context.map().on('move.intro', null);\n timeout(function() { continueTo(features); }, 3000);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function features() {\n var onClick = function() { continueTo(pointsLinesAreas); };\n\n reveal('.main-map .surface', helpHtml('intro.navigation.features'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', helpHtml('intro.navigation.features'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('drawn.intro', null);\n nextStep();\n }\n }\n\n function pointsLinesAreas() {\n var onClick = function() { continueTo(nodesWays); };\n\n reveal('.main-map .surface', helpHtml('intro.navigation.points_lines_areas'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', helpHtml('intro.navigation.points_lines_areas'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('drawn.intro', null);\n nextStep();\n }\n }\n\n function nodesWays() {\n var onClick = function() { continueTo(clickTownHall); };\n\n reveal('.main-map .surface', helpHtml('intro.navigation.nodes_ways'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', helpHtml('intro.navigation.nodes_ways'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('drawn.intro', null);\n nextStep();\n }\n }\n\n function clickTownHall() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n reveal(null, null, { duration: 0 });\n context.map().centerZoomEase(entity.loc, 19, 500);\n\n timeout(function() {\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n var textId = context.lastPointerType() === 'mouse' ? 'click_townhall' : 'tap_townhall';\n reveal(box, helpHtml('intro.navigation.' + textId));\n\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.navigation.' + textId), { duration: 0 });\n });\n\n context.on('enter.intro', function() {\n if (isTownHallSelected()) continueTo(selectedTownHall);\n });\n\n }, 550); // after centerZoomEase\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function selectedTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n var entity = context.hasEntity(hallId);\n if (!entity) return clickTownHall();\n\n var box = pointBox(entity.loc, context);\n var onClick = function() { continueTo(editorTownHall); };\n\n reveal(box, helpHtml('intro.navigation.selected_townhall'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.navigation.selected_townhall'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function editorTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n var onClick = function() { continueTo(presetTownHall); };\n\n reveal('.entity-editor-pane',\n helpHtml('intro.navigation.editor_townhall'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.on('exit.intro', function() {\n continueTo(clickTownHall);\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n nextStep();\n }\n }\n\n\n function presetTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n // preset match, in case the user happened to change it.\n var entity = context.entity(context.selectedIDs()[0]);\n var preset = presetManager.match(entity, context.graph());\n\n var onClick = function() { continueTo(fieldsTownHall); };\n\n reveal('.entity-editor-pane .section-feature-type',\n helpHtml('intro.navigation.preset_townhall', { preset: preset.name() }),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.on('exit.intro', function() {\n continueTo(clickTownHall);\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n nextStep();\n }\n }\n\n\n function fieldsTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n var onClick = function() { continueTo(closeTownHall); };\n\n reveal('.entity-editor-pane .section-preset-fields',\n helpHtml('intro.navigation.fields_townhall'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.on('exit.intro', function() {\n continueTo(clickTownHall);\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n nextStep();\n }\n }\n\n\n function closeTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane',\n helpHtml('intro.navigation.close_townhall', { button: { html: icon(href, 'inline') } })\n );\n\n context.on('exit.intro', function() {\n continueTo(searchStreet);\n });\n\n context.history().on('change.intro', function() {\n // update the close icon in the tooltip if the user edits something.\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane',\n helpHtml('intro.navigation.close_townhall', { button: { html: icon(href, 'inline') } }),\n { duration: 0 }\n );\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function searchStreet() {\n context.enter(modeBrowse(context));\n context.history().reset('initial'); // ensure spring street exists\n\n var msec = transitionTime(springStreet, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(springStreet, 19, msec); // ..and user can see it\n\n timeout(function() {\n reveal('.search-header input',\n helpHtml('intro.navigation.search_street', { name: t('intro.graph.name.spring-street') })\n );\n\n context.container().select('.search-header input')\n .on('keyup.intro', checkSearchResult);\n }, msec + 100);\n }\n\n\n function checkSearchResult() {\n var first = context.container().select('.feature-list-item:nth-child(0n+2)'); // skip \"No Results\" item\n var firstName = first.select('.entity-name');\n var name = t('intro.graph.name.spring-street');\n\n if (!firstName.empty() && firstName.html() === name) {\n reveal(first.node(),\n helpHtml('intro.navigation.choose_street', { name: name }),\n { duration: 300 }\n );\n\n context.on('exit.intro', function() {\n continueTo(selectedStreet);\n });\n\n context.container().select('.search-header input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n }\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.container().select('.search-header input')\n .on('keydown.intro', null)\n .on('keyup.intro', null);\n nextStep();\n }\n }\n\n\n function selectedStreet() {\n if (!context.hasEntity(springStreetEndId) || !context.hasEntity(springStreetId)) {\n return searchStreet();\n }\n\n var onClick = function() { continueTo(editorStreet); };\n var entity = context.entity(springStreetEndId);\n var box = pointBox(entity.loc, context);\n box.height = 500;\n\n reveal(box,\n helpHtml('intro.navigation.selected_street', { name: t('intro.graph.name.spring-street') }),\n { duration: 600, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(springStreetEndId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n box.height = 500;\n reveal(box,\n helpHtml('intro.navigation.selected_street', { name: t('intro.graph.name.spring-street') }),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n }, 600); // after reveal.\n\n context.on('enter.intro', function(mode) {\n if (!context.hasEntity(springStreetId)) {\n return continueTo(searchStreet);\n }\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== springStreetId) {\n // keep Spring Street selected..\n context.enter(modeSelect(context, [springStreetId]));\n }\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(springStreetEndId) || !context.hasEntity(springStreetId)) {\n timeout(function() {\n continueTo(searchStreet);\n }, 300); // after any transition (e.g. if user deleted intersection)\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function editorStreet() {\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane', helpHtml('intro.navigation.street_different_fields') + '{br}' +\n helpHtml('intro.navigation.editor_street', {\n button: { html: icon(href, 'inline') },\n field1: onewayField.title(),\n field2: maxspeedField.title()\n }));\n\n context.on('exit.intro', function() {\n continueTo(play);\n });\n\n context.history().on('change.intro', function() {\n // update the close icon in the tooltip if the user edits something.\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane', helpHtml('intro.navigation.street_different_fields') + '{br}' +\n helpHtml('intro.navigation.editor_street', {\n button: { html: icon(href, 'inline') },\n field1: onewayField.title(),\n field2: maxspeedField.title()\n }), { duration: 0 }\n );\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.navigation.play', { next: t('intro.points.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-point',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n dragMap();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.search-header input').on('keydown.intro keyup.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { actionChangePreset } from '../../actions/change_preset';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pointBox, pad, selectMenuItem, transitionTime } from './helper';\n\n\nexport function uiIntroPoint(context, reveal) {\n var dispatch = d3_dispatch('done');\n var timeouts = [];\n var intersection = [-85.63279, 41.94394];\n var building = [-85.632422, 41.944045];\n var cafePreset = presetManager.item('amenity/cafe');\n var _pointID = null;\n\n\n var chapter = {\n title: 'intro.points.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function addPoint() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var msec = transitionTime(intersection, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(intersection, 19, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-point',\n helpHtml('intro.points.points_info') + '{br}' + helpHtml('intro.points.add_point'));\n\n _pointID = null;\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-points');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-point') return;\n continueTo(placePoint);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function placePoint() {\n if (context.mode().id !== 'add-point') {\n return chapter.restart();\n }\n\n var pointBox = pad(building, 150, context);\n var textId = context.lastPointerType() === 'mouse' ? 'place_point' : 'place_point_touch';\n reveal(pointBox, helpHtml('intro.points.' + textId));\n\n context.map().on('move.intro drawn.intro', function() {\n pointBox = pad(building, 150, context);\n reveal(pointBox, helpHtml('intro.points.' + textId), { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return chapter.restart();\n _pointID = context.mode().selectedIDs()[0];\n\n if (context.graph().geometry(_pointID) === 'vertex'){\n\n //disallow all\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n\n reveal(pointBox, helpHtml('intro.points.place_point_error'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { return chapter.restart(); }\n });\n } else {\n continueTo(searchPreset);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function searchPreset() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return addPoint();\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.points.search_cafe', { preset: cafePreset.name() })\n );\n\n context.on('enter.intro', function(mode) {\n if (!_pointID || !context.hasEntity(_pointID)) {\n return continueTo(addPoint);\n }\n\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _pointID) {\n // keep the user's point selected..\n context.enter(modeSelect(context, [_pointID]));\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.points.search_cafe', { preset: cafePreset.name() })\n );\n\n context.history().on('change.intro', null);\n }\n });\n\n\n function checkPresetSearch() {\n var first = context.container().select('.preset-list-item:first-child');\n\n if (first.classed('preset-amenity-cafe')) {\n context.container().select('.preset-search-input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n\n reveal(first.select('.preset-list-button').node(),\n helpHtml('intro.points.choose_cafe', { preset: cafePreset.name() }),\n { duration: 300 }\n );\n\n context.history().on('change.intro', function() {\n continueTo(aboutFeatureEditor);\n });\n }\n }\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n nextStep();\n }\n }\n\n\n function aboutFeatureEditor() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return addPoint();\n }\n\n timeout(function() {\n reveal('.entity-editor-pane', helpHtml('intro.points.feature_editor'), {\n tooltipClass: 'intro-points-describe',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(addName); }\n });\n }, 400);\n\n context.on('exit.intro', function() {\n // if user leaves select mode here, just continue with the tutorial.\n continueTo(reselectPoint);\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function addName() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return addPoint();\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n var addNameString = helpHtml('intro.points.fields_info') + '{br}' + helpHtml('intro.points.add_name') + '{br}' + helpHtml('intro.points.add_reminder');\n\n timeout(function() {\n // It's possible for the user to add a name in a previous step..\n // If so, don't tell them to add the name in this step.\n // Give them an OK button instead.\n var entity = context.entity(_pointID);\n if (entity.tags.name) {\n var tooltip = reveal('.entity-editor-pane', addNameString, {\n tooltipClass: 'intro-points-describe',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(addCloseEditor); }\n });\n tooltip.select('.instruction').style('display', 'none');\n\n } else {\n reveal('.entity-editor-pane', addNameString,\n { tooltipClass: 'intro-points-describe' }\n );\n }\n }, 400);\n\n context.history().on('change.intro', function() {\n continueTo(addCloseEditor);\n });\n\n context.on('exit.intro', function() {\n // if user leaves select mode here, just continue with the tutorial.\n continueTo(reselectPoint);\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function addCloseEditor() {\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n context.on('exit.intro', function() {\n continueTo(reselectPoint);\n });\n\n reveal('.entity-editor-pane',\n helpHtml('intro.points.add_close', { button: { html: icon(href, 'inline') } })\n );\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function reselectPoint() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n\n // make sure it's still a cafe, in case user somehow changed it..\n var oldPreset = presetManager.match(entity, context.graph());\n context.replace(actionChangePreset(_pointID, oldPreset, cafePreset));\n\n context.enter(modeBrowse(context));\n\n var msec = transitionTime(entity.loc, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerEase(entity.loc, msec);\n\n timeout(function() {\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.points.reselect'), { duration: 600 });\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.points.reselect'), { duration: 0 });\n });\n }, 600); // after reveal..\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n continueTo(updatePoint);\n });\n\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function updatePoint() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return continueTo(reselectPoint);\n }\n\n // reset pane, in case user happened to untag the point..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n context.on('exit.intro', function() {\n continueTo(reselectPoint);\n });\n\n context.history().on('change.intro', function() {\n continueTo(updateCloseEditor);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane', helpHtml('intro.points.update'),\n { tooltipClass: 'intro-points-describe' }\n );\n }, 400);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function updateCloseEditor() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return continueTo(reselectPoint);\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n context.on('exit.intro', function() {\n continueTo(rightClickPoint);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.points.update_close', { button: { html: icon('#iD-icon-close', 'inline') } })\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickPoint() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n\n context.enter(modeBrowse(context));\n\n var box = pointBox(entity.loc, context);\n var textId = context.lastPointerType() === 'mouse' ? 'rightclick' : 'edit_menu_touch';\n reveal(box, helpHtml('intro.points.' + textId), { duration: 600 });\n\n timeout(function() {\n context.map().on('move.intro', function() {\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.points.' + textId), { duration: 0 });\n });\n }, 600); // after reveal\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== _pointID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'delete').node();\n if (!node) return;\n continueTo(enterDelete);\n }, 50); // after menu visible\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro', null);\n nextStep();\n }\n }\n\n\n function enterDelete() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n\n var node = selectMenuItem(context, 'delete').node();\n if (!node) { return continueTo(rightClickPoint); }\n\n reveal('.edit-menu',\n helpHtml('intro.points.delete'),\n { padding: 50 }\n );\n\n timeout(function() {\n context.map().on('move.intro', function() {\n if (selectMenuItem(context, 'delete').empty()) {\n return continueTo(rightClickPoint);\n }\n reveal('.edit-menu',\n helpHtml('intro.points.delete'),\n { duration: 0, padding: 50 }\n );\n });\n }, 300); // after menu visible\n\n context.on('exit.intro', function() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (entity) return continueTo(rightClickPoint); // point still exists\n });\n\n context.history().on('change.intro', function(changed) {\n if (changed.deleted().length) {\n continueTo(undo);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro', null);\n context.history().on('change.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function undo() {\n context.history().on('change.intro', function() {\n continueTo(play);\n });\n\n reveal('.top-toolbar button.undo-button',\n helpHtml('intro.points.undo')\n );\n\n function continueTo(nextStep) {\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.points.play', { next: t('intro.areas.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-area',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addPoint();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n interpolateNumber as d3_interpolateNumber\n} from 'd3-interpolate';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pad, transitionTime } from './helper';\n\n\nexport function uiIntroArea(context, reveal) {\n var dispatch = d3_dispatch('done');\n var playground = [-85.63552, 41.94159];\n var playgroundPreset = presetManager.item('leisure/playground');\n var nameField = presetManager.field('name');\n var descriptionField = presetManager.field('description');\n var timeouts = [];\n var _areaID;\n\n\n var chapter = {\n title: 'intro.areas.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function revealPlayground(center, text, options) {\n var padding = 180 * Math.pow(2, context.map().zoom() - 19.5);\n var box = pad(center, padding, context);\n reveal(box, text, options);\n }\n\n\n function addArea() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n _areaID = null;\n\n var msec = transitionTime(playground, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(playground, 19, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-area',\n helpHtml('intro.areas.add_playground'));\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-areas');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-area') return;\n continueTo(startPlayground);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startPlayground() {\n if (context.mode().id !== 'add-area') {\n return chapter.restart();\n }\n\n _areaID = null;\n context.map().zoomEase(19.5, 500);\n\n timeout(function() {\n var textId = context.lastPointerType() === 'mouse' ? 'starting_node_click' : 'starting_node_tap';\n var startDrawString = helpHtml('intro.areas.start_playground') + helpHtml('intro.areas.' + textId);\n revealPlayground(playground,\n startDrawString, { duration: 250 }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n revealPlayground(playground,\n startDrawString, { duration: 0 }\n );\n });\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-area') return chapter.restart();\n continueTo(continuePlayground);\n });\n }, 250); // after reveal\n\n }, 550); // after easing\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continuePlayground() {\n if (context.mode().id !== 'draw-area') {\n return chapter.restart();\n }\n\n _areaID = null;\n revealPlayground(playground,\n helpHtml('intro.areas.continue_playground'),\n { duration: 250 }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n revealPlayground(playground,\n helpHtml('intro.areas.continue_playground'),\n { duration: 0 }\n );\n });\n }, 250); // after reveal\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n var entity = context.hasEntity(context.selectedIDs()[0]);\n if (entity && entity.nodes.length >= 6) {\n return continueTo(finishPlayground);\n } else {\n return;\n }\n } else if (mode.id === 'select') {\n _areaID = context.selectedIDs()[0];\n return continueTo(searchPresets);\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function finishPlayground() {\n if (context.mode().id !== 'draw-area') {\n return chapter.restart();\n }\n\n _areaID = null;\n\n var finishString = helpHtml('intro.areas.finish_area_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.areas.finish_playground');\n revealPlayground(playground,\n finishString, { duration: 250 }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n revealPlayground(playground,\n finishString, { duration: 0 }\n );\n });\n }, 250); // after reveal\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n return;\n } else if (mode.id === 'select') {\n _areaID = context.selectedIDs()[0];\n return continueTo(searchPresets);\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function searchPresets() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n context.enter(modeSelect(context, [_areaID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.areas.search_playground', { preset: playgroundPreset.name() })\n );\n }, 400); // after preset list pane visible..\n\n context.on('enter.intro', function(mode) {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return continueTo(addArea);\n }\n\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _areaID) {\n // keep the user's area selected..\n context.enter(modeSelect(context, [_areaID]));\n\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.areas.search_playground', { preset: playgroundPreset.name() })\n );\n\n context.history().on('change.intro', null);\n }\n });\n\n function checkPresetSearch() {\n var first = context.container().select('.preset-list-item:first-child');\n\n if (first.classed('preset-leisure-playground')) {\n reveal(first.select('.preset-list-button').node(),\n helpHtml('intro.areas.choose_playground', { preset: playgroundPreset.name() }),\n { duration: 300 }\n );\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n\n context.history().on('change.intro', function() {\n continueTo(clickAddField);\n });\n }\n }\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n nextStep();\n }\n }\n\n\n function clickAddField() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n if (!context.container().select('.form-field-description').empty()) {\n return continueTo(describePlayground);\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n // It's possible for the user to add a description in a previous step..\n // If they did this already, just continue to next step.\n var entity = context.entity(_areaID);\n if (entity.tags.description) {\n return continueTo(play);\n }\n\n // scroll \"Add field\" into view\n var box = context.container().select('.more-fields').node().getBoundingClientRect();\n if (box.top > 300) {\n var pane = context.container().select('.entity-editor-pane .inspector-body');\n var start = pane.node().scrollTop;\n var end = start + (box.top - 300);\n\n pane\n .transition()\n .duration(250)\n .tween('scroll.inspector', function() {\n var node = this;\n var i = d3_interpolateNumber(start, end);\n return function(t) {\n node.scrollTop = i(t);\n };\n });\n }\n\n timeout(function() {\n reveal('.more-fields .combobox-input',\n helpHtml('intro.areas.add_field', {\n name: nameField.title(),\n description: descriptionField.title()\n }),\n { duration: 300 }\n );\n\n context.container().select('.more-fields .combobox-input')\n .on('click.intro', function() {\n // Watch for the combobox to appear...\n var watcher;\n watcher = window.setInterval(function() {\n if (!context.container().select('div.combobox').empty()) {\n window.clearInterval(watcher);\n continueTo(chooseDescriptionField);\n }\n }, 300);\n });\n }, 300); // after \"Add Field\" visible\n\n }, 400); // after editor pane visible\n\n context.on('exit.intro', function() {\n return continueTo(searchPresets);\n });\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.more-fields .combobox-input').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function chooseDescriptionField() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n if (!context.container().select('.form-field-description').empty()) {\n return continueTo(describePlayground);\n }\n\n // Make sure combobox is ready..\n if (context.container().select('div.combobox').empty()) {\n return continueTo(clickAddField);\n }\n // Watch for the combobox to go away..\n var watcher;\n watcher = window.setInterval(function() {\n if (context.container().select('div.combobox').empty()) {\n window.clearInterval(watcher);\n timeout(function() {\n if (context.container().select('.form-field-description').empty()) {\n continueTo(retryChooseDescription);\n } else {\n continueTo(describePlayground);\n }\n }, 300); // after description field added.\n }\n }, 300);\n\n reveal('div.combobox',\n helpHtml('intro.areas.choose_field', { field: descriptionField.title() }),\n { duration: 300 }\n );\n\n context.on('exit.intro', function() {\n return continueTo(searchPresets);\n });\n\n function continueTo(nextStep) {\n if (watcher) window.clearInterval(watcher);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function describePlayground() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n if (context.container().select('.form-field-description').empty()) {\n return continueTo(retryChooseDescription);\n }\n\n context.on('exit.intro', function() {\n continueTo(play);\n });\n\n reveal('.entity-editor-pane',\n helpHtml('intro.areas.describe_playground', { button: { html: icon('#iD-icon-close', 'inline') } }),\n { duration: 300 }\n );\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function retryChooseDescription() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n reveal('.entity-editor-pane',\n helpHtml('intro.areas.retry_add_field', { field: descriptionField.title() }), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(clickAddField); }\n });\n\n context.on('exit.intro', function() {\n return continueTo(searchPresets);\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.areas.play', { next: t('intro.lines.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-line',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addArea();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n context.container().select('.more-fields .combobox-input').on('click.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { geoSphericalDistance } from '../../geo';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pad, selectMenuItem, transitionTime } from './helper';\n\n\nexport function uiIntroLine(context, reveal) {\n var dispatch = d3_dispatch('done');\n var timeouts = [];\n var _tulipRoadID = null;\n var flowerRoadID = 'w646';\n var tulipRoadStart = [-85.6297754121684, 41.95805253325314];\n var tulipRoadMidpoint = [-85.62975395449628, 41.95787501510204];\n var tulipRoadIntersection = [-85.62974496187628, 41.95742515554585];\n var roadCategory = presetManager.item('category-road_minor');\n var residentialPreset = presetManager.item('highway/residential');\n var woodRoadID = 'w525';\n var woodRoadEndID = 'n2862';\n var woodRoadAddNode = [-85.62390110349587, 41.95397111462291];\n var woodRoadDragEndpoint = [-85.623867390213, 41.95466987786487];\n var woodRoadDragMidpoint = [-85.62386254803509, 41.95430395953872];\n var washingtonStreetID = 'w522';\n var twelfthAvenueID = 'w1';\n var eleventhAvenueEndID = 'n3550';\n var twelfthAvenueEndID = 'n5';\n var _washingtonSegmentID = null;\n var eleventhAvenueEnd = context.entity(eleventhAvenueEndID).loc;\n var twelfthAvenueEnd = context.entity(twelfthAvenueEndID).loc;\n var deleteLinesLoc = [-85.6219395542764, 41.95228033922477];\n var twelfthAvenue = [-85.62219310052491, 41.952505413152956];\n\n\n var chapter = {\n title: 'intro.lines.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function addLine() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var msec = transitionTime(tulipRoadStart, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(tulipRoadStart, 18.5, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-line',\n helpHtml('intro.lines.add_line'));\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-lines');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-line') return;\n continueTo(startLine);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startLine() {\n if (context.mode().id !== 'add-line') return chapter.restart();\n\n _tulipRoadID = null;\n\n var padding = 70 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(tulipRoadStart, padding, context);\n box.height = box.height + 100;\n\n var textId = context.lastPointerType() === 'mouse' ? 'start_line' : 'start_line_tap';\n var startLineString = helpHtml('intro.lines.missing_road') + '{br}' +\n helpHtml('intro.lines.line_draw_info') +\n helpHtml('intro.lines.' + textId);\n reveal(box, startLineString);\n\n context.map().on('move.intro drawn.intro', function() {\n padding = 70 * Math.pow(2, context.map().zoom() - 18);\n box = pad(tulipRoadStart, padding, context);\n box.height = box.height + 100;\n reveal(box, startLineString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-line') return chapter.restart();\n continueTo(drawLine);\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function drawLine() {\n if (context.mode().id !== 'draw-line') return chapter.restart();\n\n _tulipRoadID = context.mode().selectedIDs()[0];\n context.map().centerEase(tulipRoadMidpoint, 500);\n\n timeout(function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18.5);\n var box = pad(tulipRoadMidpoint, padding, context);\n box.height = box.height * 2;\n reveal(box,\n helpHtml('intro.lines.intersect', { name: t('intro.graph.name.flower-street') })\n );\n\n context.map().on('move.intro drawn.intro', function() {\n padding = 200 * Math.pow(2, context.map().zoom() - 18.5);\n box = pad(tulipRoadMidpoint, padding, context);\n box.height = box.height * 2;\n reveal(box,\n helpHtml('intro.lines.intersect', { name: t('intro.graph.name.flower-street') }),\n { duration: 0 }\n );\n });\n }, 550); // after easing..\n\n context.history().on('change.intro', function() {\n if (isLineConnected()) {\n continueTo(continueLine);\n }\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-line') {\n return;\n } else if (mode.id === 'select') {\n continueTo(retryIntersect);\n return;\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function isLineConnected() {\n var entity = _tulipRoadID && context.hasEntity(_tulipRoadID);\n if (!entity) return false;\n\n var drawNodes = context.graph().childNodes(entity);\n return drawNodes.some(function(node) {\n return context.graph().parentWays(node).some(function(parent) {\n return parent.id === flowerRoadID;\n });\n });\n }\n\n\n function retryIntersect() {\n d3_select(window).on('pointerdown.intro mousedown.intro', eventCancel, true);\n\n var box = pad(tulipRoadIntersection, 80, context);\n reveal(box,\n helpHtml('intro.lines.retry_intersect', { name: t('intro.graph.name.flower-street') })\n );\n\n timeout(chapter.restart, 3000);\n }\n\n\n function continueLine() {\n if (context.mode().id !== 'draw-line') return chapter.restart();\n var entity = _tulipRoadID && context.hasEntity(_tulipRoadID);\n if (!entity) return chapter.restart();\n\n context.map().centerEase(tulipRoadIntersection, 500);\n\n var continueLineText = helpHtml('intro.lines.continue_line') + '{br}' +\n helpHtml('intro.lines.finish_line_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.lines.finish_road');\n\n reveal('.main-map .surface', continueLineText);\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-line') {\n return;\n } else if (mode.id === 'select') {\n return continueTo(chooseCategoryRoad);\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function chooseCategoryRoad() {\n if (context.mode().id !== 'select') return chapter.restart();\n\n context.on('exit.intro', function() {\n return chapter.restart();\n });\n\n var button = context.container().select('.preset-category-road_minor .preset-list-button');\n if (button.empty()) return chapter.restart();\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n reveal(button.node(),\n helpHtml('intro.lines.choose_category_road', { category: roadCategory.name() })\n );\n\n button.on('click.intro', function() {\n continueTo(choosePresetResidential);\n });\n\n }, 400); // after editor pane visible\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function choosePresetResidential() {\n if (context.mode().id !== 'select') return chapter.restart();\n\n context.on('exit.intro', function() {\n return chapter.restart();\n });\n\n var subgrid = context.container().select('.preset-category-road_minor .subgrid');\n if (subgrid.empty()) return chapter.restart();\n\n subgrid.selectAll(':not(.preset-highway-residential) .preset-list-button')\n .on('click.intro', function() {\n continueTo(retryPresetResidential);\n });\n\n subgrid.selectAll('.preset-highway-residential .preset-list-button')\n .on('click.intro', function() {\n continueTo(nameRoad);\n });\n\n timeout(function() {\n reveal(subgrid.node(),\n helpHtml('intro.lines.choose_preset_residential', { preset: residentialPreset.name() }),\n { tooltipBox: '.preset-highway-residential .preset-list-button', duration: 300 }\n );\n }, 300);\n\n function continueTo(nextStep) {\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n // selected wrong road type\n function retryPresetResidential() {\n if (context.mode().id !== 'select') return chapter.restart();\n\n context.on('exit.intro', function() {\n return chapter.restart();\n });\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n var button = context.container().select('.entity-editor-pane .preset-list-button');\n\n reveal(button.node(),\n helpHtml('intro.lines.retry_preset_residential', { preset: residentialPreset.name() })\n );\n\n button.on('click.intro', function() {\n continueTo(chooseCategoryRoad);\n });\n\n }, 500);\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function nameRoad() {\n context.on('exit.intro', function() {\n continueTo(didNameRoad);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.lines.name_road', { button: { html: icon('#iD-icon-close', 'inline') } }),\n { tooltipClass: 'intro-lines-name_road' }\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function didNameRoad() {\n context.history().checkpoint('doneAddLine');\n\n timeout(function() {\n reveal('.main-map .surface', helpHtml('intro.lines.did_name_road'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(updateLine); }\n });\n }, 500);\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function updateLine() {\n context.history().reset('doneAddLine');\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return chapter.restart();\n }\n\n var msec = transitionTime(woodRoadDragMidpoint, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(woodRoadDragMidpoint, 19, msec);\n\n timeout(function() {\n var padding = 250 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n var advance = function() { continueTo(addNode); };\n\n reveal(box, helpHtml('intro.lines.update_line'),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 250 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n reveal(box, helpHtml('intro.lines.update_line'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function addNode() {\n context.history().reset('doneAddLine');\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return chapter.restart();\n }\n\n var padding = 40 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadAddNode, padding, context);\n var addNodeString = helpHtml('intro.lines.add_node' + (context.lastPointerType() === 'mouse' ? '' : '_touch'));\n reveal(box, addNodeString);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 40 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadAddNode, padding, context);\n reveal(box, addNodeString, { duration: 0 });\n });\n\n context.history().on('change.intro', function(changed) {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n if (changed.created().length === 1) {\n timeout(function() { continueTo(startDragEndpoint); }, 500);\n }\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') {\n continueTo(updateLine);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startDragEndpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n var startDragString = helpHtml('intro.lines.start_drag_endpoint' + (context.lastPointerType() === 'mouse' ? '' : '_touch')) +\n helpHtml('intro.lines.drag_to_intersection');\n reveal(box, startDragString);\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n reveal(box, startDragString, { duration: 0 });\n\n var entity = context.entity(woodRoadEndID);\n if (geoSphericalDistance(entity.loc, woodRoadDragEndpoint) <= 4) {\n continueTo(finishDragEndpoint);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function finishDragEndpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n var finishDragString = helpHtml('intro.lines.spot_looks_good') +\n helpHtml('intro.lines.finish_drag_endpoint' + (context.lastPointerType() === 'mouse' ? '' : '_touch'));\n reveal(box, finishDragString);\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n reveal(box, finishDragString, { duration: 0 });\n\n var entity = context.entity(woodRoadEndID);\n if (geoSphericalDistance(entity.loc, woodRoadDragEndpoint) > 4) {\n continueTo(startDragEndpoint);\n }\n });\n\n context.on('enter.intro', function() {\n continueTo(startDragMidpoint);\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startDragMidpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n if (context.selectedIDs().indexOf(woodRoadID) === -1) {\n context.enter(modeSelect(context, [woodRoadID]));\n }\n\n var padding = 80 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n reveal(box, helpHtml('intro.lines.start_drag_midpoint'));\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 80 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n reveal(box, helpHtml('intro.lines.start_drag_midpoint'), { duration: 0 });\n });\n\n context.history().on('change.intro', function(changed) {\n if (changed.created().length === 1) {\n continueTo(continueDragMidpoint);\n }\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') {\n // keep Wood Road selected so midpoint triangles are drawn..\n context.enter(modeSelect(context, [woodRoadID]));\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continueDragMidpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n box.height += 400;\n\n var advance = function() {\n context.history().checkpoint('doneUpdateLine');\n continueTo(deleteLines);\n };\n\n reveal(box, helpHtml('intro.lines.continue_drag_midpoint'),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n box.height += 400;\n reveal(box, helpHtml('intro.lines.continue_drag_midpoint'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function deleteLines() {\n context.history().reset('doneUpdateLine');\n context.enter(modeBrowse(context));\n\n if (!context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return chapter.restart();\n }\n\n var msec = transitionTime(deleteLinesLoc, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(deleteLinesLoc, 18, msec);\n\n timeout(function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(deleteLinesLoc, padding, context);\n box.top -= 200;\n box.height += 400;\n var advance = function() { continueTo(rightClickIntersection); };\n\n reveal(box, helpHtml('intro.lines.delete_lines', { street: t('intro.graph.name.12th-avenue') }),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(deleteLinesLoc, padding, context);\n box.top -= 200;\n box.height += 400;\n reveal(box, helpHtml('intro.lines.delete_lines', { street: t('intro.graph.name.12th-avenue') }),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n\n context.history().on('change.intro', function() {\n timeout(function() {\n continueTo(deleteLines);\n }, 500); // after any transition (e.g. if user deleted intersection)\n });\n\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickIntersection() {\n context.history().reset('doneUpdateLine');\n context.enter(modeBrowse(context));\n\n context.map().centerZoomEase(eleventhAvenueEnd, 18, 500);\n\n var rightClickString = helpHtml('intro.lines.split_street', {\n street1: t('intro.graph.name.11th-avenue'),\n street2: t('intro.graph.name.washington-street')\n }) +\n helpHtml('intro.lines.' + (context.lastPointerType() === 'mouse' ? 'rightclick_intersection' : 'edit_menu_intersection_touch'));\n\n timeout(function() {\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, rightClickString);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, rightClickString,\n { duration: 0 }\n );\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== eleventhAvenueEndID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'split').node();\n if (!node) return;\n continueTo(splitIntersection);\n }, 50); // after menu visible\n });\n\n context.history().on('change.intro', function() {\n timeout(function() {\n continueTo(deleteLines);\n }, 300); // after any transition (e.g. if user deleted intersection)\n });\n\n }, 600);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function splitIntersection() {\n if (!context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(deleteLines);\n }\n\n var node = selectMenuItem(context, 'split').node();\n if (!node) { return continueTo(rightClickIntersection); }\n\n var wasChanged = false;\n _washingtonSegmentID = null;\n\n reveal('.edit-menu', helpHtml('intro.lines.split_intersection',\n { street: t('intro.graph.name.washington-street') }),\n { padding: 50 }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var node = selectMenuItem(context, 'split').node();\n if (!wasChanged && !node) { return continueTo(rightClickIntersection); }\n\n reveal('.edit-menu', helpHtml('intro.lines.split_intersection',\n { street: t('intro.graph.name.washington-street') }),\n { duration: 0, padding: 50 }\n );\n });\n\n context.history().on('change.intro', function(changed) {\n wasChanged = true;\n timeout(function() {\n if (context.history().undoAnnotation() === t('operations.split.annotation.line', { n: 1 })) {\n _washingtonSegmentID = changed.created()[0].id;\n continueTo(didSplit);\n } else {\n _washingtonSegmentID = null;\n continueTo(retrySplit);\n }\n }, 300); // after any transition (e.g. if user deleted intersection)\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retrySplit() {\n context.enter(modeBrowse(context));\n context.map().centerZoomEase(eleventhAvenueEnd, 18, 500);\n var advance = function() { continueTo(rightClickIntersection); };\n\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, helpHtml('intro.lines.retry_split'),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, helpHtml('intro.lines.retry_split'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function didSplit() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var ids = context.selectedIDs();\n var string = 'intro.lines.did_split_' + (ids.length > 1 ? 'multi' : 'single');\n var street = t('intro.graph.name.washington-street');\n\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n box.width = box.width / 2;\n reveal(box, helpHtml(string, { street1: street, street2: street }),\n { duration: 500 }\n );\n\n timeout(function() {\n context.map().centerZoomEase(twelfthAvenue, 18, 500);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n box.width = box.width / 2;\n reveal(box, helpHtml(string, { street1: street, street2: street }),\n { duration: 0 }\n );\n });\n }, 600); // after initial reveal and curtain cut\n\n context.on('enter.intro', function() {\n var ids = context.selectedIDs();\n if (ids.length === 1 && ids[0] === _washingtonSegmentID) {\n continueTo(multiSelect);\n }\n });\n\n context.history().on('change.intro', function() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function multiSelect() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var ids = context.selectedIDs();\n var hasWashington = ids.indexOf(_washingtonSegmentID) !== -1;\n var hasTwelfth = ids.indexOf(twelfthAvenueID) !== -1;\n\n if (hasWashington && hasTwelfth) {\n return continueTo(multiRightClick);\n } else if (!hasWashington && !hasTwelfth) {\n return continueTo(didSplit);\n }\n\n context.map().centerZoomEase(twelfthAvenue, 18, 500);\n\n timeout(function() {\n var selected, other, padding, box;\n if (hasWashington) {\n selected = t('intro.graph.name.washington-street');\n other = t('intro.graph.name.12th-avenue');\n padding = 60 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenueEnd, padding, context);\n box.width *= 3;\n } else {\n selected = t('intro.graph.name.12th-avenue');\n other = t('intro.graph.name.washington-street');\n padding = 200 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenue, padding, context);\n box.width /= 2;\n }\n\n reveal(box,\n helpHtml('intro.lines.multi_select',\n { selected: selected, other1: other }) + ' ' +\n helpHtml('intro.lines.add_to_selection_' + (context.lastPointerType() === 'mouse' ? 'click' : 'touch'),\n { selected: selected, other2: other })\n );\n\n context.map().on('move.intro drawn.intro', function() {\n if (hasWashington) {\n selected = t('intro.graph.name.washington-street');\n other = t('intro.graph.name.12th-avenue');\n padding = 60 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenueEnd, padding, context);\n box.width *= 3;\n } else {\n selected = t('intro.graph.name.12th-avenue');\n other = t('intro.graph.name.washington-street');\n padding = 200 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenue, padding, context);\n box.width /= 2;\n }\n\n reveal(box,\n helpHtml('intro.lines.multi_select',\n { selected: selected, other1: other }) + ' ' +\n helpHtml('intro.lines.add_to_selection_' + (context.lastPointerType() === 'mouse' ? 'click' : 'touch'),\n { selected: selected, other2: other }),\n { duration: 0 }\n );\n });\n\n context.on('enter.intro', function() {\n continueTo(multiSelect);\n });\n\n context.history().on('change.intro', function() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n });\n }, 600);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function multiRightClick() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n\n var rightClickString = helpHtml('intro.lines.multi_select_success') +\n helpHtml('intro.lines.multi_' + (context.lastPointerType() === 'mouse' ? 'rightclick' : 'edit_menu_touch'));\n reveal(box, rightClickString);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n reveal(box, rightClickString, { duration: 0 });\n });\n\n context.ui().editMenu().on('toggled.intro', function(open) {\n if (!open) return;\n\n timeout(function() {\n var ids = context.selectedIDs();\n if (ids.length === 2 &&\n ids.indexOf(twelfthAvenueID) !== -1 &&\n ids.indexOf(_washingtonSegmentID) !== -1) {\n var node = selectMenuItem(context, 'delete').node();\n if (!node) return;\n continueTo(multiDelete);\n } else if (ids.length === 1 &&\n ids.indexOf(_washingtonSegmentID) !== -1) {\n return continueTo(multiSelect);\n } else {\n return continueTo(didSplit);\n }\n }, 300); // after edit menu visible\n });\n\n context.history().on('change.intro', function() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.ui().editMenu().on('toggled.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function multiDelete() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var node = selectMenuItem(context, 'delete').node();\n if (!node) return continueTo(multiRightClick);\n\n reveal('.edit-menu',\n helpHtml('intro.lines.multi_delete'),\n { padding: 50 }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n reveal('.edit-menu',\n helpHtml('intro.lines.multi_delete'),\n { duration: 0, padding: 50 }\n );\n });\n\n context.on('exit.intro', function() {\n if (context.hasEntity(_washingtonSegmentID) || context.hasEntity(twelfthAvenueID)) {\n return continueTo(multiSelect); // left select mode but roads still exist\n }\n });\n\n context.history().on('change.intro', function() {\n if (context.hasEntity(_washingtonSegmentID) || context.hasEntity(twelfthAvenueID)) {\n continueTo(retryDelete); // changed something but roads still exist\n } else {\n continueTo(play);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retryDelete() {\n context.enter(modeBrowse(context));\n\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n reveal(box, helpHtml('intro.lines.retry_delete'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(multiSelect); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.lines.play', { next: t('intro.buildings.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-building',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addLine();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n d3_select(window).on('pointerdown.intro mousedown.intro', null, true);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilArrayUniq, utilRebind } from '../../util';\nimport { helpHtml, icon, pad, isMostlySquare, selectMenuItem, transitionTime } from './helper';\n\n\nexport function uiIntroBuilding(context, reveal) {\n var dispatch = d3_dispatch('done');\n var house = [-85.62815, 41.95638];\n var tank = [-85.62732, 41.95347];\n var buildingCatetory = presetManager.item('category-building');\n var housePreset = presetManager.item('building/house');\n var tankPreset = presetManager.item('man_made/storage_tank');\n var timeouts = [];\n var _houseID = null;\n var _tankID = null;\n\n\n var chapter = {\n title: 'intro.buildings.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function revealHouse(center, text, options) {\n var padding = 160 * Math.pow(2, context.map().zoom() - 20);\n var box = pad(center, padding, context);\n reveal(box, text, options);\n }\n\n\n function revealTank(center, text, options) {\n var padding = 190 * Math.pow(2, context.map().zoom() - 19.5);\n var box = pad(center, padding, context);\n reveal(box, text, options);\n }\n\n\n function addHouse() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n _houseID = null;\n\n var msec = transitionTime(house, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(house, 19, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-area',\n helpHtml('intro.buildings.add_building'));\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-buildings');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-area') return;\n continueTo(startHouse);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startHouse() {\n if (context.mode().id !== 'add-area') {\n return continueTo(addHouse);\n }\n\n _houseID = null;\n context.map().zoomEase(20, 500);\n\n timeout(function() {\n var startString = helpHtml('intro.buildings.start_building') +\n helpHtml('intro.buildings.building_corner_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap'));\n revealHouse(house, startString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealHouse(house, startString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-area') return chapter.restart();\n continueTo(continueHouse);\n });\n\n }, 550); // after easing\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continueHouse() {\n if (context.mode().id !== 'draw-area') {\n return continueTo(addHouse);\n }\n\n _houseID = null;\n\n var continueString = helpHtml('intro.buildings.continue_building') + '{br}' +\n helpHtml('intro.areas.finish_area_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.buildings.finish_building');\n\n revealHouse(house, continueString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealHouse(house, continueString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n return;\n } else if (mode.id === 'select') {\n var graph = context.graph();\n var way = context.entity(context.selectedIDs()[0]);\n var nodes = graph.childNodes(way);\n var points = utilArrayUniq(nodes)\n .map(function(n) { return context.projection(n.loc); });\n\n if (isMostlySquare(points)) {\n _houseID = way.id;\n return continueTo(chooseCategoryBuilding);\n } else {\n return continueTo(retryHouse);\n }\n\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function retryHouse() {\n var onClick = function() { continueTo(addHouse); };\n\n revealHouse(house, helpHtml('intro.buildings.retry_building'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n revealHouse(house, helpHtml('intro.buildings.retry_building'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function chooseCategoryBuilding() {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return addHouse();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {\n context.enter(modeSelect(context, [_houseID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n var button = context.container().select('.preset-category-building .preset-list-button');\n\n reveal(button.node(),\n helpHtml('intro.buildings.choose_category_building', { category: buildingCatetory.name() })\n );\n\n button.on('click.intro', function() {\n button.on('click.intro', null);\n continueTo(choosePresetHouse);\n });\n\n }, 400); // after preset list pane visible..\n\n\n context.on('enter.intro', function(mode) {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return continueTo(addHouse);\n }\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _houseID) {\n return continueTo(chooseCategoryBuilding);\n }\n });\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function choosePresetHouse() {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return addHouse();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {\n context.enter(modeSelect(context, [_houseID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n var button = context.container().select('.preset-building-house .preset-list-button');\n\n reveal(button.node(),\n helpHtml('intro.buildings.choose_preset_house', { preset: housePreset.name() }),\n { duration: 300 }\n );\n\n button.on('click.intro', function() {\n button.on('click.intro', null);\n continueTo(closeEditorHouse);\n });\n\n }, 400); // after preset list pane visible..\n\n context.on('enter.intro', function(mode) {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return continueTo(addHouse);\n }\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _houseID) {\n return continueTo(chooseCategoryBuilding);\n }\n });\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function closeEditorHouse() {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return addHouse();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {\n context.enter(modeSelect(context, [_houseID]));\n }\n\n context.history().checkpoint('hasHouse');\n\n context.on('exit.intro', function() {\n continueTo(rightClickHouse);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.buildings.close', { button: { html: icon('#iD-icon-close', 'inline') } })\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickHouse() {\n if (!_houseID) return chapter.restart();\n\n context.enter(modeBrowse(context));\n context.history().reset('hasHouse');\n var zoom = context.map().zoom();\n if (zoom < 20) {\n zoom = 20;\n }\n context.map().centerZoomEase(house, zoom, 500);\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== _houseID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'orthogonalize').node();\n if (!node) return;\n continueTo(clickSquare);\n }, 50); // after menu visible\n });\n\n context.map().on('move.intro drawn.intro', function() {\n var rightclickString = helpHtml('intro.buildings.' + (context.lastPointerType() === 'mouse' ? 'rightclick_building' : 'edit_menu_building_touch'));\n revealHouse(house, rightclickString, { duration: 0 });\n });\n\n context.history().on('change.intro', function() {\n continueTo(rightClickHouse);\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function clickSquare() {\n if (!_houseID) return chapter.restart();\n var entity = context.hasEntity(_houseID);\n if (!entity) return continueTo(rightClickHouse);\n\n var node = selectMenuItem(context, 'orthogonalize').node();\n if (!node) { return continueTo(rightClickHouse); }\n\n var wasChanged = false;\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.square_building'),\n { padding: 50 }\n );\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'browse') {\n continueTo(rightClickHouse);\n } else if (mode.id === 'move' || mode.id === 'rotate') {\n continueTo(retryClickSquare);\n }\n });\n\n context.map().on('move.intro', function() {\n var node = selectMenuItem(context, 'orthogonalize').node();\n if (!wasChanged && !node) { return continueTo(rightClickHouse); }\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.square_building'),\n { duration: 0, padding: 50 }\n );\n });\n\n context.history().on('change.intro', function() {\n wasChanged = true;\n context.history().on('change.intro', null);\n\n // Something changed. Wait for transition to complete and check undo annotation.\n timeout(function() {\n if (context.history().undoAnnotation() === t('operations.orthogonalize.annotation.feature', { n: 1 })) {\n continueTo(doneSquare);\n } else {\n continueTo(retryClickSquare);\n }\n }, 500); // after transitioned actions\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retryClickSquare() {\n context.enter(modeBrowse(context));\n\n revealHouse(house, helpHtml('intro.buildings.retry_square'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(rightClickHouse); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function doneSquare() {\n context.history().checkpoint('doneSquare');\n\n revealHouse(house, helpHtml('intro.buildings.done_square'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(addTank); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function addTank() {\n context.enter(modeBrowse(context));\n context.history().reset('doneSquare');\n _tankID = null;\n\n var msec = transitionTime(tank, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(tank, 19.5, msec);\n\n timeout(function() {\n reveal('button.add-area',\n helpHtml('intro.buildings.add_tank')\n );\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-area') return;\n continueTo(startTank);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startTank() {\n if (context.mode().id !== 'add-area') {\n return continueTo(addTank);\n }\n\n _tankID = null;\n\n timeout(function() {\n var startString = helpHtml('intro.buildings.start_tank') +\n helpHtml('intro.buildings.tank_edge_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap'));\n revealTank(tank, startString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealTank(tank, startString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-area') return chapter.restart();\n continueTo(continueTank);\n });\n\n }, 550); // after easing\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continueTank() {\n if (context.mode().id !== 'draw-area') {\n return continueTo(addTank);\n }\n\n _tankID = null;\n\n var continueString = helpHtml('intro.buildings.continue_tank') + '{br}' +\n helpHtml('intro.areas.finish_area_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.buildings.finish_tank');\n\n revealTank(tank, continueString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealTank(tank, continueString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n return;\n } else if (mode.id === 'select') {\n _tankID = context.selectedIDs()[0];\n return continueTo(searchPresetTank);\n } else {\n return continueTo(addTank);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function searchPresetTank() {\n if (!_tankID || !context.hasEntity(_tankID)) {\n return addTank();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _tankID) {\n context.enter(modeSelect(context, [_tankID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.buildings.search_tank', { preset: tankPreset.name() })\n );\n }, 400); // after preset list pane visible..\n\n context.on('enter.intro', function(mode) {\n if (!_tankID || !context.hasEntity(_tankID)) {\n return continueTo(addTank);\n }\n\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _tankID) {\n // keep the user's area selected..\n context.enter(modeSelect(context, [_tankID]));\n\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.buildings.search_tank', { preset: tankPreset.name() })\n );\n\n context.history().on('change.intro', null);\n }\n });\n\n function checkPresetSearch() {\n var first = context.container().select('.preset-list-item:first-child');\n\n if (first.classed('preset-man_made-storage_tank')) {\n reveal(first.select('.preset-list-button').node(),\n helpHtml('intro.buildings.choose_tank', { preset: tankPreset.name() }),\n { duration: 300 }\n );\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n\n context.history().on('change.intro', function() {\n continueTo(closeEditorTank);\n });\n }\n }\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n nextStep();\n }\n }\n\n\n function closeEditorTank() {\n if (!_tankID || !context.hasEntity(_tankID)) {\n return addTank();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _tankID) {\n context.enter(modeSelect(context, [_tankID]));\n }\n\n context.history().checkpoint('hasTank');\n\n context.on('exit.intro', function() {\n continueTo(rightClickTank);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.buildings.close', { button: { html: icon('#iD-icon-close', 'inline') } })\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickTank() {\n if (!_tankID) return continueTo(addTank);\n\n context.enter(modeBrowse(context));\n context.history().reset('hasTank');\n context.map().centerEase(tank, 500);\n\n timeout(function() {\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== _tankID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'circularize').node();\n if (!node) return;\n continueTo(clickCircle);\n }, 50); // after menu visible\n });\n\n var rightclickString = helpHtml('intro.buildings.' + (context.lastPointerType() === 'mouse' ? 'rightclick_tank' : 'edit_menu_tank_touch'));\n\n revealTank(tank, rightclickString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealTank(tank, rightclickString, { duration: 0 });\n });\n\n context.history().on('change.intro', function() {\n continueTo(rightClickTank);\n });\n\n }, 600);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function clickCircle() {\n if (!_tankID) return chapter.restart();\n var entity = context.hasEntity(_tankID);\n if (!entity) return continueTo(rightClickTank);\n\n var node = selectMenuItem(context, 'circularize').node();\n if (!node) { return continueTo(rightClickTank); }\n\n var wasChanged = false;\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.circle_tank'),\n { padding: 50 }\n );\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'browse') {\n continueTo(rightClickTank);\n } else if (mode.id === 'move' || mode.id === 'rotate') {\n continueTo(retryClickCircle);\n }\n });\n\n context.map().on('move.intro', function() {\n var node = selectMenuItem(context, 'circularize').node();\n if (!wasChanged && !node) { return continueTo(rightClickTank); }\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.circle_tank'),\n { duration: 0, padding: 50 }\n );\n });\n\n context.history().on('change.intro', function() {\n wasChanged = true;\n context.history().on('change.intro', null);\n\n // Something changed. Wait for transition to complete and check undo annotation.\n timeout(function() {\n if (context.history().undoAnnotation() === t('operations.circularize.annotation.feature', { n: 1 })) {\n continueTo(play);\n } else {\n continueTo(retryClickCircle);\n }\n }, 500); // after transitioned actions\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retryClickCircle() {\n context.enter(modeBrowse(context));\n\n revealTank(tank, helpHtml('intro.buildings.retry_circle'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(rightClickTank); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.buildings.play', { next: t('intro.startediting.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-startEditing',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addHouse();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n context.container().select('.more-fields .combobox-input').on('click.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { helpHtml } from './helper';\nimport { uiModal } from '../modal';\nimport { utilRebind } from '../../util/rebind';\n\n\nexport function uiIntroStartEditing(context, reveal) {\n var dispatch = d3_dispatch('done', 'startEditing');\n var modalSelection = d3_select(null);\n\n\n var chapter = {\n title: 'intro.startediting.title'\n };\n\n function showHelp() {\n reveal('.map-control.help-control',\n helpHtml('intro.startediting.help'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { shortcuts(); }\n }\n );\n }\n\n function shortcuts() {\n reveal('.map-control.help-control',\n helpHtml('intro.startediting.shortcuts'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { showSave(); }\n }\n );\n }\n\n function showSave() {\n context.container().selectAll('.shaded').remove(); // in case user opened keyboard shortcuts\n reveal('.top-toolbar button.save',\n helpHtml('intro.startediting.save'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { showStart(); }\n }\n );\n }\n\n function showStart() {\n context.container().selectAll('.shaded').remove(); // in case user opened keyboard shortcuts\n\n modalSelection = uiModal(context.container());\n\n modalSelection.select('.modal')\n .attr('class', 'modal-splash modal');\n\n modalSelection.selectAll('.close').remove();\n\n var startbutton = modalSelection.select('.content')\n .attr('class', 'fillL')\n .append('button')\n .attr('class', 'modal-section huge-modal-button')\n .on('click', function() {\n modalSelection.remove();\n });\n\n startbutton\n .append('svg')\n .attr('class', 'illustration')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n startbutton\n .append('h2')\n .call(t.append('intro.startediting.start'));\n\n dispatch.call('startEditing');\n }\n\n\n chapter.enter = function() {\n showHelp();\n };\n\n\n chapter.exit = function() {\n modalSelection.remove();\n context.container().selectAll('.shaded').remove(); // in case user opened keyboard shortcuts\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { t, localizer } from '../../core/localizer';\nimport { localize } from './helper';\n\nimport { prefs } from '../../core/preferences';\nimport { fileFetcher } from '../../core/file_fetcher';\nimport { coreGraph } from '../../core/graph';\nimport { modeBrowse } from '../../modes/browse';\nimport { osmEntity } from '../../osm/entity';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCurtain } from '../curtain';\nimport { utilArrayDifference, utilArrayUniq } from '../../util';\n\nimport { uiIntroWelcome } from './welcome';\nimport { uiIntroNavigation } from './navigation';\nimport { uiIntroPoint } from './point';\nimport { uiIntroArea } from './area';\nimport { uiIntroLine } from './line';\nimport { uiIntroBuilding } from './building';\nimport { uiIntroStartEditing } from './start_editing';\n\n\nconst chapterUi = {\n welcome: uiIntroWelcome,\n navigation: uiIntroNavigation,\n point: uiIntroPoint,\n area: uiIntroArea,\n line: uiIntroLine,\n building: uiIntroBuilding,\n startEditing: uiIntroStartEditing\n};\n\nconst chapterFlow = [\n 'welcome',\n 'navigation',\n 'point',\n 'area',\n 'line',\n 'building',\n 'startEditing'\n];\n\n\nexport function uiIntro(context) {\n const INTRO_IMAGERY = 'Bing';\n let _introGraph = {};\n let _currChapter;\n\n\n function intro(selection) {\n fileFetcher.get('intro_graph')\n .then(dataIntroGraph => {\n // create entities for intro graph and localize names\n for (let id in dataIntroGraph) {\n if (!_introGraph[id]) {\n _introGraph[id] = osmEntity(localize(dataIntroGraph[id]));\n }\n }\n selection.call(startIntro);\n })\n .catch(function() { /* ignore */ });\n }\n\n\n function startIntro(selection) {\n context.enter(modeBrowse(context));\n\n // Save current map state\n let osm = context.connection();\n let history = context.history().toJSON();\n let hash = window.location.hash;\n let center = context.map().center();\n let zoom = context.map().zoom();\n let background = context.background().baseLayerSource();\n let overlays = context.background().overlayLayerSources();\n let opacity = context.container().selectAll('.main-map .layer-background').style('opacity');\n let caches = osm && osm.caches();\n let baseEntities = context.history().graph().base().entities;\n\n // Show sidebar and disable the sidebar resizing button\n // (this needs to be before `context.inIntro(true)`)\n context.ui().sidebar.expand();\n context.container().selectAll('button.sidebar-toggle').classed('disabled', true);\n\n // Block saving\n context.inIntro(true);\n\n // Load semi-real data used in intro\n if (osm) { osm.toggle(false).reset(); }\n context.history().reset();\n context.history().merge(Object.values(coreGraph().load(_introGraph).entities));\n context.history().checkpoint('initial');\n\n // Setup imagery\n let imagery = context.background().findSource(INTRO_IMAGERY);\n if (imagery) {\n context.background().baseLayerSource(imagery);\n } else {\n context.background().bing();\n }\n overlays.forEach(d => context.background().toggleOverlayLayer(d));\n\n // Setup data layers (only OSM)\n let layers = context.layers();\n layers.all().forEach(item => {\n // if the layer has the function `enabled`\n if (typeof item.layer.enabled === 'function') {\n item.layer.enabled(item.id === 'osm');\n }\n });\n\n\n context.container().selectAll('.main-map .layer-background').style('opacity', 1);\n\n let curtain = uiCurtain(context.container().node());\n selection.call(curtain);\n\n // Store that the user started the walkthrough..\n prefs('walkthrough_started', 'yes');\n\n // Restore previous walkthrough progress..\n let storedProgress = prefs('walkthrough_progress') || '';\n let progress = storedProgress.split(';').filter(Boolean);\n\n let chapters = chapterFlow.map((chapter, i) => {\n let s = chapterUi[chapter](context, curtain.reveal)\n .on('done', () => {\n\n buttons\n .filter(d => d.title === s.title)\n .classed('finished', true);\n\n if (i < chapterFlow.length - 1) {\n const next = chapterFlow[i + 1];\n context.container().select(`button.chapter-${next}`)\n .classed('next', true);\n }\n\n // Store walkthrough progress..\n progress.push(chapter);\n prefs('walkthrough_progress', utilArrayUniq(progress).join(';'));\n });\n return s;\n });\n\n chapters[chapters.length - 1].on('startEditing', () => {\n // Store walkthrough progress..\n progress.push('startEditing');\n prefs('walkthrough_progress', utilArrayUniq(progress).join(';'));\n\n // Store if walkthrough is completed..\n let incomplete = utilArrayDifference(chapterFlow, progress);\n if (!incomplete.length) {\n prefs('walkthrough_completed', 'yes');\n }\n\n curtain.remove();\n navwrap.remove();\n context.container().selectAll('.main-map .layer-background').style('opacity', opacity);\n context.container().selectAll('button.sidebar-toggle').classed('disabled', false);\n if (osm) { osm.toggle(true).reset().caches(caches); }\n context.history().reset().merge(Object.values(baseEntities));\n context.background().baseLayerSource(background);\n overlays.forEach(d => context.background().toggleOverlayLayer(d));\n if (history) { context.history().fromJSON(history, false); }\n context.map().centerZoom(center, zoom);\n window.history.replaceState(null, '', hash);\n context.inIntro(false);\n });\n\n let navwrap = selection\n .append('div')\n .attr('class', 'intro-nav-wrap fillD');\n\n navwrap\n .append('svg')\n .attr('class', 'intro-nav-wrap-logo')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n let buttonwrap = navwrap\n .append('div')\n .attr('class', 'joined')\n .selectAll('button.chapter');\n\n let buttons = buttonwrap\n .data(chapters)\n .enter()\n .append('button')\n .attr('class', (d, i) => `chapter chapter-${chapterFlow[i]}`)\n .on('click', enterChapter);\n\n buttons\n .append('span')\n .html(d => t.html(d.title));\n\n buttons\n .append('span')\n .attr('class', 'status')\n .call(svgIcon((localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'));\n\n enterChapter(null, chapters[0]);\n\n\n function enterChapter(d3_event, newChapter) {\n if (_currChapter) { _currChapter.exit(); }\n context.enter(modeBrowse(context));\n\n _currChapter = newChapter;\n _currChapter.enter();\n\n buttons\n .classed('next', false)\n .classed('active', d => d.title === _currChapter.title);\n }\n }\n\n\n return intro;\n}\n", "export { uiIntro } from './intro';\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../core/preferences';\nimport { svgIcon } from '../svg/icon';\nimport { t } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\n\n\nexport function uiIssuesInfo(context) {\n\n var warningsItem = {\n id: 'warnings',\n count: 0,\n iconID: 'iD-icon-alert',\n descriptionID: 'issues.warnings_and_errors'\n };\n\n var resolvedItem = {\n id: 'resolved',\n count: 0,\n iconID: 'iD-icon-apply',\n descriptionID: 'issues.user_resolved_issues'\n };\n\n function update(selection) {\n\n var shownItems = [];\n\n var liveIssues = context.validator().getIssues({\n what: prefs('validate-what') || 'edited',\n where: prefs('validate-where') || 'all'\n }).filter(issue => issue.severity !== 'suggestion');\n\n if (liveIssues.length) {\n warningsItem.count = liveIssues.length;\n shownItems.push(warningsItem);\n }\n\n if (prefs('validate-what') === 'all') {\n var resolvedIssues = context.validator().getResolvedIssues();\n if (resolvedIssues.length) {\n resolvedItem.count = resolvedIssues.length;\n shownItems.push(resolvedItem);\n }\n }\n\n var chips = selection.selectAll('.chip')\n .data(shownItems, function(d) {\n return d.id;\n });\n\n chips.exit().remove();\n\n var enter = chips.enter()\n .append('a')\n .attr('class', function(d) {\n return 'chip ' + d.id + '-count';\n })\n .attr('href', '#')\n .each(function(d) {\n\n var chipSelection = d3_select(this);\n\n var tooltipBehavior = uiTooltip()\n .placement('top')\n .title(() => t.append(d.descriptionID));\n\n chipSelection\n .call(tooltipBehavior)\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n\n tooltipBehavior.hide(d3_select(this));\n // open the Issues pane\n context.ui().togglePanes(context.container().select('.map-panes .issues-pane'));\n });\n\n chipSelection.call(svgIcon('#' + d.iconID));\n\n });\n\n enter.append('span')\n .attr('class', 'count');\n\n enter.merge(chips)\n .selectAll('span.count')\n .text(function(d) {\n return d.count.toString();\n });\n }\n\n\n return function(selection) {\n update(selection);\n\n context.validator().on('validated.infobox', function() {\n update(selection);\n });\n };\n}\n", "/**\n * IntervalTasksQueue\n * Enabled task execution under interval limit\n */\nexport class IntervalTasksQueue {\n readonly intervalInMs: number;\n pendingHandles: ReturnType[];\n time: number;\n\n /**\n * Interval in milliseconds inside which only 1 task can execute.\n * e.g. if interval is 200ms, and 5 async tasks are unqueued,\n * they will complete in ~1s if not cleared\n * @param {number} intervalInMs\n */\n constructor(intervalInMs: number) {\n this.intervalInMs = intervalInMs;\n this.pendingHandles = [];\n this.time = 0;\n }\n\n enqueue(task: () => void) {\n let taskTimeout = this.time;\n this.time += this.intervalInMs;\n this.pendingHandles.push(setTimeout(() => {\n this.time -= this.intervalInMs;\n task();\n }, taskTimeout));\n }\n\n clear() {\n this.pendingHandles.forEach((timeoutHandle) => {\n clearTimeout(timeoutHandle);\n });\n this.pendingHandles = [];\n this.time = 0;\n }\n}\n", "import { geoArea as d3_geoArea, geoMercatorRaw as d3_geoMercatorRaw } from 'd3-geo';\nimport { json as d3_json } from 'd3-fetch';\n\nimport { t, localizer } from '../core/localizer';\nimport { geoExtent, geoSphericalDistance } from '../geo';\nimport { utilQsString, utilStringQs } from '../util';\nimport { utilAesDecrypt } from '../util/aes';\nimport { IntervalTasksQueue } from '../util/IntervalTasksQueue';\nimport { localeDateString } from '../util/date';\n\nvar isRetina = window.devicePixelRatio && window.devicePixelRatio >= 2;\n\n// listen for DPI change, e.g. when dragging a browser window from a retina to non-retina screen\nwindow.matchMedia?.(`\n (-webkit-min-device-pixel-ratio: 2), /* Safari */\n (min-resolution: 2dppx), /* standard */\n (min-resolution: 192dpi) /* fallback */\n `).addListener(function() {\n\n isRetina = window.devicePixelRatio && window.devicePixelRatio >= 2;\n});\n\n\nfunction vintageRange(vintage) {\n var s;\n if (vintage.start || vintage.end) {\n s = (vintage.start || '?');\n if (vintage.start !== vintage.end) {\n s += ' - ' + (vintage.end || '?');\n }\n }\n return s;\n}\n\n\nexport function rendererBackgroundSource(data) {\n var source = Object.assign({}, data); // shallow copy\n var _offset = [0, 0];\n var _name = source.name;\n var _description = source.description;\n var _best = !!source.best;\n var _template = source.encrypted ? utilAesDecrypt(source.template) : source.template;\n\n source.tileSize = data.tileSize || 256;\n source.zoomExtent = data.zoomExtent || [0, 22];\n source.overzoom = data.overzoom !== false;\n\n source.offset = function(val) {\n if (!arguments.length) return _offset;\n _offset = val;\n return source;\n };\n\n\n source.nudge = function(val, zoomlevel) {\n _offset[0] += val[0] / Math.pow(2, zoomlevel);\n _offset[1] += val[1] / Math.pow(2, zoomlevel);\n return source;\n };\n\n\n source.name = function() {\n var id_safe = source.id.replace(/\\./g, '');\n return t('imagery.' + id_safe + '.name', { default: _name });\n };\n\n\n source.label = function() {\n var id_safe = source.id.replace(/\\./g, '');\n return t.append('imagery.' + id_safe + '.name', { default: _name });\n };\n\n\n source.hasDescription = function() {\n var id_safe = source.id.replace(/\\./g, '');\n var descriptionText = localizer.tInfo('imagery.' + id_safe + '.description', { default: escape(_description) }).texts.join('');\n return !!descriptionText;\n };\n\n\n source.description = function() {\n var id_safe = source.id.replace(/\\./g, '');\n return t.append('imagery.' + id_safe + '.description', { default: _description });\n };\n\n\n source.best = function() {\n return _best;\n };\n\n\n source.area = function() {\n if (!data.polygon) return Number.MAX_VALUE; // worldwide\n var area = d3_geoArea({ type: 'MultiPolygon', coordinates: [ data.polygon ] });\n return isNaN(area) ? 0 : area;\n };\n\n\n source.imageryUsed = function() {\n return _name || source.id;\n };\n\n\n source.template = function(val) {\n if (!arguments.length) return _template;\n if (source.id === 'custom' || source.id === 'Bing') {\n _template = val;\n }\n return source;\n };\n\n\n source.url = function(coord) {\n var result = _template.replace(/#[\\s\\S]*/u, ''); // strip hash part of URL\n if (result === '') return result; // source 'none'\n\n\n // Guess a type based on the tokens present in the template\n // (This is for 'custom' source, where we don't know)\n if (!source.type || source.id === 'custom') {\n if (/SERVICE=WMS|\\{(proj|wkid|bbox)\\}/.test(result)) {\n source.type = 'wms';\n source.projection = 'EPSG:3857'; // guess\n } else if (/\\{(x|y)\\}/.test(result)) {\n source.type = 'tms';\n } else if (/\\{u\\}/.test(result)) {\n source.type = 'bing';\n }\n }\n\n\n if (source.type === 'wms') {\n var tileToProjectedCoords = (function(x, y, z) {\n var zoomSize = Math.pow(2, z);\n var lon = x / zoomSize * Math.PI * 2 - Math.PI;\n var lat = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / zoomSize)));\n\n switch (source.projection) {\n case 'EPSG:4326':\n return {\n x: lon * 180 / Math.PI,\n y: lat * 180 / Math.PI\n };\n default: // EPSG:3857 and synonyms\n var mercCoords = d3_geoMercatorRaw(lon, lat);\n return {\n x: 20037508.34 / Math.PI * mercCoords[0],\n y: 20037508.34 / Math.PI * mercCoords[1]\n };\n }\n });\n\n var tileSize = source.tileSize;\n var projection = source.projection;\n var minXmaxY = tileToProjectedCoords(coord[0], coord[1], coord[2]);\n var maxXminY = tileToProjectedCoords(coord[0]+1, coord[1]+1, coord[2]);\n\n result = result.replace(/\\{(\\w+)\\}/g, function (token, key) {\n switch (key) {\n case 'width':\n case 'height':\n return tileSize;\n case 'proj':\n return projection;\n case 'wkid':\n return projection.replace(/^EPSG:/, '');\n case 'bbox':\n // WMS 1.3 flips x/y for some coordinate systems including EPSG:4326 - #7557\n if (projection === 'EPSG:4326' &&\n // The CRS parameter implies version 1.3 (prior versions use SRS)\n /VERSION=1.3|CRS={proj}/.test(source.template().toUpperCase())) {\n return maxXminY.y + ',' + minXmaxY.x + ',' + minXmaxY.y + ',' + maxXminY.x;\n } else {\n return minXmaxY.x + ',' + maxXminY.y + ',' + maxXminY.x + ',' + minXmaxY.y;\n }\n case 'w':\n return minXmaxY.x;\n case 's':\n return maxXminY.y;\n case 'n':\n return maxXminY.x;\n case 'e':\n return minXmaxY.y;\n default:\n return token;\n }\n });\n\n } else if (source.type === 'tms') {\n result = result\n .replace('{x}', coord[0])\n .replace('{y}', coord[1])\n // TMS-flipped y coordinate\n .replace(/\\{[t-]y\\}/, Math.pow(2, coord[2]) - coord[1] - 1)\n .replace(/\\{z(oom)?\\}/, coord[2])\n // only fetch retina tiles for retina screens\n .replace(/\\{@2x\\}|\\{r\\}/, isRetina ? '@2x' : '');\n\n } else if (source.type === 'bing') {\n result = result\n .replace('{u}', function() {\n var u = '';\n for (var zoom = coord[2]; zoom > 0; zoom--) {\n var b = 0;\n var mask = 1 << (zoom - 1);\n if ((coord[0] & mask) !== 0) b++;\n if ((coord[1] & mask) !== 0) b += 2;\n u += b.toString();\n }\n return u;\n });\n }\n\n // these apply to any type..\n result = result.replace(/\\{switch:([^}]+)\\}/, function(s, r) {\n var subdomains = r.split(',');\n return subdomains[(coord[0] + coord[1]) % subdomains.length];\n });\n\n\n return result;\n };\n\n\n source.validZoom = function(z, underzoom) {\n if (underzoom === undefined) underzoom = 0;\n return source.zoomExtent[0] - underzoom <= z &&\n (source.overzoom || source.zoomExtent[1] > z);\n };\n\n\n source.isLocatorOverlay = function() {\n return source.id === 'mapbox_locator_overlay';\n };\n\n\n /* hides a source from the list, but leaves it available for use */\n source.isHidden = function() {\n return false; // currently there are no hidden layers\n };\n\n\n source.copyrightNotices = function() {};\n\n\n source.getMetadata = function(center, tileCoord, callback) {\n var vintage = {\n start: localeDateString(source.startDate),\n end: localeDateString(source.endDate)\n };\n vintage.range = vintageRange(vintage);\n\n var metadata = { vintage: vintage };\n callback(null, metadata);\n };\n\n\n return source;\n}\n\n\nrendererBackgroundSource.Bing = function(data, dispatch) {\n // https://docs.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata\n // https://docs.microsoft.com/en-us/bingmaps/rest-services/directly-accessing-the-bing-maps-tiles\n\n //fallback url template\n data.template = 'https://ecn.t{switch:0,1,2,3}.tiles.virtualearth.net/tiles/a{u}.jpeg?g=1&pr=odbl&n=z';\n\n var bing = rendererBackgroundSource(data);\n var key = utilAesDecrypt('5c875730b09c6b422433e807e1ff060b6536c791dbfffcffc4c6b18a1bdba1f14593d151adb50e19e1be1ab19aef813bf135d0f103475e5c724dec94389e45d0');\n /*\n missing tile image strictness param (n=)\n \u2022\tn=f -> (Fail) returns a 404\n \u2022\tn=z -> (Empty) returns a 200 with 0 bytes (no content)\n \u2022\tn=t -> (Transparent) returns a 200 with a transparent (png) tile\n */\n const strictParam = 'n';\n\n var url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/AerialOSM?include=ImageryProviders&uriScheme=https&key=' + key;\n var cache = {};\n var inflight = {};\n var providers = [];\n var taskQueue = new IntervalTasksQueue(250);\n var metadataLastZoom = -1;\n\n d3_json(url)\n .then(function(json) {\n let imageryResource = json.resourceSets[0].resources[0];\n\n //retrieve and prepare up to date imagery template\n let template = imageryResource.imageUrl; //https://ecn.{subdomain}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=10339\n let subDomains = imageryResource.imageUrlSubdomains; //[\"t0, t1, t2, t3\"]\n let subDomainNumbers = subDomains.map((subDomain) => {\n return subDomain.substring(1);\n } ).join(',');\n\n template = template.replace('{subdomain}', `t{switch:${subDomainNumbers}}`).replace('{quadkey}', '{u}');\n if (!new URLSearchParams(template).has(strictParam)){\n template += `&${strictParam}=z`;\n }\n bing.template(template);\n\n providers = imageryResource.imageryProviders.map(function(provider) {\n return {\n attribution: provider.attribution,\n areas: provider.coverageAreas.map(function(area) {\n return {\n zoom: [area.zoomMin, area.zoomMax],\n extent: geoExtent([area.bbox[1], area.bbox[0]], [area.bbox[3], area.bbox[2]])\n };\n })\n };\n });\n dispatch.call('change');\n })\n .catch(function() {\n /* ignore */\n });\n\n\n bing.copyrightNotices = function(zoom, extent) {\n zoom = Math.min(zoom, 21);\n return providers.filter(function(provider) {\n return provider.areas.some(function(area) {\n return extent.intersects(area.extent) &&\n area.zoom[0] <= zoom &&\n area.zoom[1] >= zoom;\n });\n }).map(function(provider) {\n return provider.attribution;\n }).join(', ');\n };\n\n\n bing.getMetadata = function(center, tileCoord, callback) {\n var tileID = tileCoord.slice(0, 3).join('/');\n var zoom = Math.min(tileCoord[2], 21);\n var centerPoint = center[1] + ',' + center[0]; // lat,lng\n var url = 'https://dev.virtualearth.net/REST/v1/Imagery/BasicMetadata/AerialOSM/' + centerPoint +\n '?zl=' + zoom + '&key=' + key;\n\n if (inflight[tileID]) return;\n\n if (!cache[tileID]) {\n cache[tileID] = {};\n }\n if (cache[tileID] && cache[tileID].metadata) {\n return callback(null, cache[tileID].metadata);\n }\n\n inflight[tileID] = true;\n\n if (metadataLastZoom !== tileCoord[2]){\n metadataLastZoom = tileCoord[2];\n taskQueue.clear();\n }\n\n taskQueue.enqueue(() => {\n d3_json(url)\n .then(function (result) {\n delete inflight[tileID];\n if (!result) {\n throw new Error('Unknown Error');\n }\n var vintage = {\n start: localeDateString(result.resourceSets[0].resources[0].vintageStart),\n end: localeDateString(result.resourceSets[0].resources[0].vintageEnd)\n };\n vintage.range = vintageRange(vintage);\n\n var metadata = { vintage: vintage };\n cache[tileID].metadata = metadata;\n if (callback) callback(null, metadata);\n })\n .catch(function (err) {\n delete inflight[tileID];\n if (callback) callback(err.message);\n });\n });\n };\n\n\n bing.terms_url = 'https://blog.openstreetmap.org/2010/11/30/microsoft-imagery-details';\n\n\n return bing;\n};\n\n\n\nrendererBackgroundSource.Esri = function(data) {\n // in addition to using the tilemap at zoom level 20, overzoom real tiles - #4327 (deprecated technique, but it works)\n if (data.template.match(/blankTile/) === null) {\n data.template = data.template + '?blankTile=false';\n }\n\n var esri = rendererBackgroundSource(data);\n var cache = {};\n var inflight = {};\n var _prevCenter;\n\n // use a tilemap service to set maximum zoom for esri tiles dynamically\n // https://developers.arcgis.com/documentation/tiled-elevation-service/\n esri.fetchTilemap = function(center) {\n // skip if we have already fetched a tilemap within 5km\n if (_prevCenter && geoSphericalDistance(center, _prevCenter) < 5000) return;\n _prevCenter = center;\n\n // tiles are available globally to zoom level 19, afterward they may or may not be present\n var z = 20;\n\n // first generate a random url using the template\n var dummyUrl = esri.url([1,2,3]);\n\n // calculate url z/y/x from the lat/long of the center of the map\n var x = (Math.floor((center[0] + 180) / 360 * Math.pow(2, z)));\n var y = (Math.floor((1 - Math.log(Math.tan(center[1] * Math.PI / 180) + 1 / Math.cos(center[1] * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, z)));\n\n // fetch an 8x8 grid to leverage cache\n var tilemapUrl = dummyUrl.replace(/tile\\/[0-9]+\\/[0-9]+\\/[0-9]+\\?blankTile=false/, 'tilemap') + '/' + z + '/' + y + '/' + x + '/8/8';\n\n // make the request and introspect the response from the tilemap server\n d3_json(tilemapUrl)\n .then(function(tilemap) {\n if (!tilemap) {\n throw new Error('Unknown Error');\n }\n var hasTiles = true;\n for (var i = 0; i < tilemap.data.length; i++) {\n // 0 means an individual tile in the grid doesn't exist\n if (!tilemap.data[i]) {\n hasTiles = false;\n break;\n }\n }\n\n // if any tiles are missing at level 20 we restrict maxZoom to 19\n esri.zoomExtent[1] = (hasTiles ? 22 : 19);\n })\n .catch(function() {\n /* ignore */\n });\n };\n\n\n esri.getMetadata = function(center, tileCoord, callback) {\n let mapServerUrl = esri.metadata;\n if (esri.id === 'EsriWorldImagery') {\n mapServerUrl = 'https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer';\n }\n if (!mapServerUrl) {\n // rest endpoint is not available for ESRI's \"clarity\" imagery\n return callback(null, {});\n }\n var tileID = tileCoord.slice(0, 3).join('/');\n var zoom = Math.min(tileCoord[2], esri.zoomExtent[1]);\n var centerPoint = center[0] + ',' + center[1]; // long, lat (as it should be)\n var unknown = t('info_panels.background.unknown');\n var vintage = {};\n var metadata = {};\n\n if (inflight[tileID]) return;\n\n // build up query using the layer appropriate to the current zoom\n var url = mapServerUrl + '/4/query';\n url += '?returnGeometry=false&geometry=' + centerPoint + '&inSR=4326&geometryType=esriGeometryPoint&outFields=*&f=json';\n\n if (!cache[tileID]) {\n cache[tileID] = {};\n }\n if (cache[tileID] && cache[tileID].metadata) {\n return callback(null, cache[tileID].metadata);\n }\n\n inflight[tileID] = true;\n d3_json(url)\n .then(function(result) {\n delete inflight[tileID];\n\n result = result.features.map(f => f.attributes)\n .filter(a => a.MinMapLevel <= zoom && a.MaxMapLevel >= zoom)[0];\n\n if (!result) {\n throw new Error('Unknown Error');\n } else if (result.features && result.features.length < 1) {\n throw new Error('No Results');\n } else if (result.error && result.error.message) {\n throw new Error(result.error.message);\n }\n\n // pass through the discrete capture date from metadata\n var captureDate = localeDateString(result.SRC_DATE2);\n vintage = {\n start: captureDate,\n end: captureDate,\n range: captureDate\n };\n metadata = {\n vintage: vintage,\n source: clean(result.NICE_NAME),\n description: clean(result.NICE_DESC),\n resolution: clean(+Number(result.SRC_RES).toFixed(4)),\n accuracy: clean(+Number(result.SRC_ACC).toFixed(4))\n };\n\n // append units - meters\n if (isFinite(metadata.resolution)) {\n metadata.resolution += ' m';\n }\n if (isFinite(metadata.accuracy)) {\n metadata.accuracy += ' m';\n }\n\n cache[tileID].metadata = metadata;\n if (callback) callback(null, metadata);\n })\n .catch(function(err) {\n delete inflight[tileID];\n if (callback) callback(err.message);\n });\n\n\n function clean(val) {\n return String(val).trim() || unknown;\n }\n };\n\n return esri;\n};\n\n\nrendererBackgroundSource.None = function() {\n var source = rendererBackgroundSource({ id: 'none', template: '' });\n\n\n source.name = function() {\n return t('background.none');\n };\n\n\n source.label = function() {\n return t.append('background.none');\n };\n\n\n source.imageryUsed = function() {\n return null;\n };\n\n\n source.area = function() {\n return -1; // sources in background pane are sorted by area\n };\n\n\n return source;\n};\n\n\nrendererBackgroundSource.Custom = function(template) {\n var source = rendererBackgroundSource({ id: 'custom', template: template });\n\n\n source.name = function() {\n return t('background.custom');\n };\n\n source.label = function() {\n return t.append('background.custom');\n };\n\n\n source.imageryUsed = function() {\n // sanitize personal connection tokens - #6801\n var cleaned = source.template();\n\n // from query string parameters\n if (cleaned.indexOf('?') !== -1) {\n var parts = cleaned.split('?', 2);\n var qs = utilStringQs(parts[1]);\n\n ['access_token', 'connectId', 'token', 'Signature'].forEach(function(param) {\n if (qs[param]) {\n qs[param] = '{apikey}';\n }\n });\n cleaned = parts[0] + '?' + utilQsString(qs, true); // true = soft encode\n }\n\n // from wms/wmts api path parameters\n cleaned = cleaned\n .replace(/token\\/(\\w+)/, 'token/{apikey}')\n .replace(/key=(\\w+)/, 'key={apikey}');\n return 'Custom (' + cleaned + ' )';\n };\n\n\n source.area = function() {\n return -2; // sources in background pane are sorted by area\n };\n\n\n return source;\n};\n", "import { feature, point, lineString, isObject } from \"@turf/helpers\";\nimport {\n Point,\n LineString,\n Polygon,\n MultiLineString,\n MultiPolygon,\n FeatureCollection,\n Feature,\n Geometry,\n GeometryObject,\n GeometryCollection,\n GeoJsonProperties,\n BBox,\n GeoJsonTypes,\n} from \"geojson\";\nimport { AllGeoJSON, Lines, Id } from \"@turf/helpers\";\n\n/**\n * Callback for coordEach\n *\n * @callback coordEachCallback\n * @param {number[]} currentCoord The current coordinate being processed.\n * @param {number} coordIndex The current index of the coordinate being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over coordinates in any GeoJSON object, similar to Array.forEach()\n *\n * @function\n * @param {AllGeoJSON} geojson any GeoJSON object\n * @param {coordEachCallback} callback a method that takes (currentCoord, coordIndex, featureIndex, multiFeatureIndex)\n * @param {boolean} [excludeWrapCoord=false] whether or not to include the final coordinate of LinearRings that wraps the ring in its iteration.\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {\"foo\": \"bar\"}),\n * turf.point([36, 53], {\"hello\": \"world\"})\n * ]);\n *\n * turf.coordEach(features, function (currentCoord, coordIndex, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=currentCoord\n * //=coordIndex\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * });\n */\nfunction coordEach(\n geojson: AllGeoJSON,\n callback: (\n currentCoord: number[],\n coordIndex: number,\n featureIndex: number,\n multiFeatureIndex: number,\n geometryIndex: number\n ) => void,\n excludeWrapCoord?: boolean\n): void {\n // Handles null Geometry -- Skips this GeoJSON\n if (geojson === null) return;\n var j,\n k,\n l,\n geometry,\n stopG,\n coords,\n geometryMaybeCollection,\n wrapShrink = 0,\n coordIndex = 0,\n isGeometryCollection,\n type = geojson.type,\n isFeatureCollection = type === \"FeatureCollection\",\n isFeature = type === \"Feature\",\n // @ts-expect-error: Known type conflict\n stop = isFeatureCollection ? geojson.features.length : 1;\n\n // This logic may look a little weird. The reason why it is that way\n // is because it's trying to be fast. GeoJSON supports multiple kinds\n // of objects at its root: FeatureCollection, Features, Geometries.\n // This function has the responsibility of handling all of them, and that\n // means that some of the `for` loops you see below actually just don't apply\n // to certain inputs. For instance, if you give this just a\n // Point geometry, then both loops are short-circuited and all we do\n // is gradually rename the input until it's called 'geometry'.\n //\n // This also aims to allocate as few resources as possible: just a\n // few numbers and booleans, rather than any temporary arrays as would\n // be required with the normalization approach.\n for (var featureIndex = 0; featureIndex < stop; featureIndex++) {\n geometryMaybeCollection = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[featureIndex].geometry\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.geometry\n : geojson;\n isGeometryCollection = geometryMaybeCollection\n ? geometryMaybeCollection.type === \"GeometryCollection\"\n : false;\n stopG = isGeometryCollection\n ? geometryMaybeCollection.geometries.length\n : 1;\n\n for (var geomIndex = 0; geomIndex < stopG; geomIndex++) {\n var multiFeatureIndex = 0;\n var geometryIndex = 0;\n geometry = isGeometryCollection\n ? geometryMaybeCollection.geometries[geomIndex]\n : geometryMaybeCollection;\n\n // Handles null Geometry -- Skips this geometry\n if (geometry === null) continue;\n coords = geometry.coordinates;\n var geomType = geometry.type;\n\n wrapShrink =\n excludeWrapCoord &&\n (geomType === \"Polygon\" || geomType === \"MultiPolygon\")\n ? 1\n : 0;\n\n switch (geomType) {\n case null:\n break;\n case \"Point\":\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords,\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n multiFeatureIndex++;\n break;\n case \"LineString\":\n case \"MultiPoint\":\n for (j = 0; j < coords.length; j++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords[j],\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n if (geomType === \"MultiPoint\") multiFeatureIndex++;\n }\n if (geomType === \"LineString\") multiFeatureIndex++;\n break;\n case \"Polygon\":\n case \"MultiLineString\":\n for (j = 0; j < coords.length; j++) {\n for (k = 0; k < coords[j].length - wrapShrink; k++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords[j][k],\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n }\n if (geomType === \"MultiLineString\") multiFeatureIndex++;\n if (geomType === \"Polygon\") geometryIndex++;\n }\n if (geomType === \"Polygon\") multiFeatureIndex++;\n break;\n case \"MultiPolygon\":\n for (j = 0; j < coords.length; j++) {\n geometryIndex = 0;\n for (k = 0; k < coords[j].length; k++) {\n for (l = 0; l < coords[j][k].length - wrapShrink; l++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords[j][k][l],\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n }\n geometryIndex++;\n }\n multiFeatureIndex++;\n }\n break;\n case \"GeometryCollection\":\n for (j = 0; j < geometry.geometries.length; j++)\n if (\n // @ts-expect-error: Known type conflict\n coordEach(geometry.geometries[j], callback, excludeWrapCoord) ===\n false\n )\n // @ts-expect-error: Known type conflict\n return false;\n break;\n default:\n throw new Error(\"Unknown Geometry Type\");\n }\n }\n }\n}\n\n/**\n * Callback for coordReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback coordReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {number[]} currentCoord The current coordinate being processed.\n * @param {number} coordIndex The current index of the coordinate being processed.\n * Starts at index 0, if an initialValue is provided, and at index 1 otherwise.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce coordinates in any GeoJSON object, similar to Array.reduce()\n *\n * @function\n * @param {AllGeoJSON} geojson any GeoJSON object\n * @param {coordReduceCallback} callback a method that takes (previousValue, currentCoord, coordIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @param {boolean} [excludeWrapCoord=false] whether or not to include the final coordinate of LinearRings that wraps the ring in its iteration.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {\"foo\": \"bar\"}),\n * turf.point([36, 53], {\"hello\": \"world\"})\n * ]);\n *\n * turf.coordReduce(features, function (previousValue, currentCoord, coordIndex, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=previousValue\n * //=currentCoord\n * //=coordIndex\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * return currentCoord;\n * });\n */\nfunction coordReduce(\n geojson: AllGeoJSON,\n callback: (\n previousValue: Reducer,\n currentCoord: number[],\n coordIndex: number,\n featureIndex: number,\n multiFeatureIndex: number,\n geometryIndex: number\n ) => Reducer,\n initialValue?: Reducer,\n excludeWrapCoord?: boolean\n): Reducer {\n var previousValue = initialValue;\n coordEach(\n geojson,\n function (\n currentCoord,\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) {\n if (coordIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentCoord;\n else\n previousValue = callback(\n // @ts-expect-error: Known type conflict\n previousValue,\n currentCoord,\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n );\n },\n excludeWrapCoord\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for propEach\n *\n * @callback propEachCallback\n * @param {GeoJsonProperties} currentProperties The current Properties being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over properties in any GeoJSON object, similar to Array.forEach()\n *\n * @function\n * @param {FeatureCollection|Feature} geojson any GeoJSON object\n * @param {propEachCallback} callback a method that takes (currentProperties, featureIndex)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.propEach(features, function (currentProperties, featureIndex) {\n * //=currentProperties\n * //=featureIndex\n * });\n */\nfunction propEach(\n geojson: Feature | FeatureCollection | Feature,\n callback: (currentProperties: Props, featureIndex: number) => void\n): void {\n var i;\n switch (geojson.type) {\n case \"FeatureCollection\":\n for (i = 0; i < geojson.features.length; i++) {\n // @ts-expect-error: Known type conflict\n if (callback(geojson.features[i].properties, i) === false) break;\n }\n break;\n case \"Feature\":\n // @ts-expect-error: Known type conflict\n callback(geojson.properties, 0);\n break;\n }\n}\n\n/**\n * Callback for propReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback propReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {GeoJsonProperties} currentProperties The current Properties being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce properties in any GeoJSON object into a single value,\n * similar to how Array.reduce works. However, in this case we lazily run\n * the reduction, so an array of all properties is unnecessary.\n *\n * @function\n * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON object\n * @param {propReduceCallback} callback a method that takes (previousValue, currentProperties, featureIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.propReduce(features, function (previousValue, currentProperties, featureIndex) {\n * //=previousValue\n * //=currentProperties\n * //=featureIndex\n * return currentProperties\n * });\n */\nfunction propReduce(\n geojson: Feature | FeatureCollection | Geometry,\n callback: (\n previousValue: Reducer,\n currentProperties: P,\n featureIndex: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n // @ts-expect-error: Known type conflict\n propEach(geojson, function (currentProperties, featureIndex) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentProperties;\n else\n // @ts-expect-error: Known type conflict\n previousValue = callback(previousValue, currentProperties, featureIndex);\n });\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for featureEach\n *\n * @callback featureEachCallback\n * @param {Feature} currentFeature The current Feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over features in any GeoJSON object, similar to\n * Array.forEach.\n *\n * @function\n * @param {FeatureCollection|Feature|Feature} geojson any GeoJSON object\n * @param {featureEachCallback} callback a method that takes (currentFeature, featureIndex)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.featureEach(features, function (currentFeature, featureIndex) {\n * //=currentFeature\n * //=featureIndex\n * });\n */\nfunction featureEach<\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | Feature,\n callback: (currentFeature: Feature, featureIndex: number) => void\n): void {\n if (geojson.type === \"Feature\") {\n // @ts-expect-error: Known type conflict\n callback(geojson, 0);\n } else if (geojson.type === \"FeatureCollection\") {\n for (var i = 0; i < geojson.features.length; i++) {\n // @ts-expect-error: Known type conflict\n if (callback(geojson.features[i], i) === false) break;\n }\n }\n}\n\n/**\n * Callback for featureReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback featureReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentFeature The current Feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce features in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|Feature} geojson any GeoJSON object\n * @param {featureReduceCallback} callback a method that takes (previousValue, currentFeature, featureIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {\"foo\": \"bar\"}),\n * turf.point([36, 53], {\"hello\": \"world\"})\n * ]);\n *\n * turf.featureReduce(features, function (previousValue, currentFeature, featureIndex) {\n * //=previousValue\n * //=currentFeature\n * //=featureIndex\n * return currentFeature\n * });\n */\nfunction featureReduce<\n Reducer,\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | Feature,\n callback: (\n previousValue: Reducer,\n currentFeature: Feature,\n featureIndex: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n featureEach(geojson, function (currentFeature, featureIndex) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentFeature;\n // @ts-expect-error: Known type conflict\n else previousValue = callback(previousValue, currentFeature, featureIndex);\n });\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Get all coordinates from any GeoJSON object.\n *\n * @function\n * @param {AllGeoJSON} geojson any GeoJSON object\n * @returns {Array>} coordinate position array\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * var coords = turf.coordAll(features);\n * //= [[26, 37], [36, 53]]\n */\nfunction coordAll(geojson: AllGeoJSON): number[][] {\n // @ts-expect-error: Known type conflict\n var coords = [];\n coordEach(geojson, function (coord) {\n coords.push(coord);\n });\n // @ts-expect-error: Known type conflict\n return coords;\n}\n\n/**\n * Callback for geomEach\n *\n * @callback geomEachCallback\n * @param {GeometryObject} currentGeometry The current Geometry being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {GeoJsonProperties} featureProperties The current Feature Properties being processed.\n * @param {BBox} featureBBox The current Feature BBox being processed.\n * @param {Id} featureId The current Feature Id being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over each geometry in any GeoJSON object, similar to Array.forEach()\n *\n * @function\n * @param {FeatureCollection|Feature|Geometry|GeometryObject|Feature} geojson any GeoJSON object\n * @param {geomEachCallback} callback a method that takes (currentGeometry, featureIndex, featureProperties, featureBBox, featureId)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.geomEach(features, function (currentGeometry, featureIndex, featureProperties, featureBBox, featureId) {\n * //=currentGeometry\n * //=featureIndex\n * //=featureProperties\n * //=featureBBox\n * //=featureId\n * });\n */\nfunction geomEach<\n G extends GeometryObject | null,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n currentGeometry: G,\n featureIndex: number,\n featureProperties: P,\n featureBBox: BBox,\n featureId: Id\n ) => void\n): void {\n var i,\n j,\n g,\n geometry,\n stopG,\n geometryMaybeCollection,\n isGeometryCollection,\n featureProperties,\n featureBBox,\n featureId,\n featureIndex = 0,\n // @ts-expect-error: Known type conflict\n isFeatureCollection = geojson.type === \"FeatureCollection\",\n // @ts-expect-error: Known type conflict\n isFeature = geojson.type === \"Feature\",\n // @ts-expect-error: Known type conflict\n stop = isFeatureCollection ? geojson.features.length : 1;\n\n // This logic may look a little weird. The reason why it is that way\n // is because it's trying to be fast. GeoJSON supports multiple kinds\n // of objects at its root: FeatureCollection, Features, Geometries.\n // This function has the responsibility of handling all of them, and that\n // means that some of the `for` loops you see below actually just don't apply\n // to certain inputs. For instance, if you give this just a\n // Point geometry, then both loops are short-circuited and all we do\n // is gradually rename the input until it's called 'geometry'.\n //\n // This also aims to allocate as few resources as possible: just a\n // few numbers and booleans, rather than any temporary arrays as would\n // be required with the normalization approach.\n for (i = 0; i < stop; i++) {\n geometryMaybeCollection = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].geometry\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.geometry\n : geojson;\n featureProperties = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].properties\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.properties\n : {};\n featureBBox = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].bbox\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.bbox\n : undefined;\n featureId = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].id\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.id\n : undefined;\n isGeometryCollection = geometryMaybeCollection\n ? geometryMaybeCollection.type === \"GeometryCollection\"\n : false;\n stopG = isGeometryCollection\n ? geometryMaybeCollection.geometries.length\n : 1;\n\n for (g = 0; g < stopG; g++) {\n geometry = isGeometryCollection\n ? geometryMaybeCollection.geometries[g]\n : geometryMaybeCollection;\n\n // Handle null Geometry\n if (geometry === null) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n // @ts-expect-error: Known type conflict\n null,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n continue;\n }\n switch (geometry.type) {\n case \"Point\":\n case \"LineString\":\n case \"MultiPoint\":\n case \"Polygon\":\n case \"MultiLineString\":\n case \"MultiPolygon\": {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n geometry,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n break;\n }\n case \"GeometryCollection\": {\n for (j = 0; j < geometry.geometries.length; j++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n geometry.geometries[j],\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n }\n break;\n }\n default:\n throw new Error(\"Unknown Geometry Type\");\n }\n }\n // Only increase `featureIndex` per each feature\n featureIndex++;\n }\n}\n\n/**\n * Callback for geomReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback geomReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {GeometryObject} currentGeometry The current Geometry being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {GeoJsonProperties} featureProperties The current Feature Properties being processed.\n * @param {BBox} featureBBox The current Feature BBox being processed.\n * @param {Id} featureId The current Feature Id being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce geometry in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|GeometryObject|GeometryCollection|Feature} geojson any GeoJSON object\n * @param {geomReduceCallback} callback a method that takes (previousValue, currentGeometry, featureIndex, featureProperties, featureBBox, featureId)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.geomReduce(features, function (previousValue, currentGeometry, featureIndex, featureProperties, featureBBox, featureId) {\n * //=previousValue\n * //=currentGeometry\n * //=featureIndex\n * //=featureProperties\n * //=featureBBox\n * //=featureId\n * return currentGeometry\n * });\n */\nfunction geomReduce<\n Reducer,\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n previousValue: Reducer,\n currentGeometry: G,\n featureIndex: number,\n featureProperties: P,\n featureBBox: BBox,\n featureId: Id\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n geomEach(\n geojson,\n function (\n currentGeometry,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentGeometry;\n else\n previousValue = callback(\n // @ts-expect-error: Known type conflict\n previousValue,\n currentGeometry,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n );\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for flattenEach\n *\n * @callback flattenEachCallback\n * @param {Feature} currentFeature The current flattened feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over flattened features in any GeoJSON object, similar to\n * Array.forEach.\n *\n * @function\n * @param {FeatureCollection|Feature|GeometryObject|GeometryCollection|Feature} geojson any GeoJSON object\n * @param {flattenEachCallback} callback a method that takes (currentFeature, featureIndex, multiFeatureIndex)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.multiPoint([[40, 30], [36, 53]], {hello: 'world'})\n * ]);\n *\n * turf.flattenEach(features, function (currentFeature, featureIndex, multiFeatureIndex) {\n * //=currentFeature\n * //=featureIndex\n * //=multiFeatureIndex\n * });\n */\nfunction flattenEach<\n G extends GeometryObject = GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n currentFeature: Feature,\n featureIndex: number,\n multiFeatureIndex: number\n ) => void\n): void {\n geomEach(geojson, function (geometry, featureIndex, properties, bbox, id) {\n // Callback for single geometry\n var type = geometry === null ? null : geometry.type;\n switch (type) {\n case null:\n case \"Point\":\n case \"LineString\":\n case \"Polygon\":\n if (\n // @ts-expect-error: Known type conflict\n callback(\n feature(geometry, properties, { bbox: bbox, id: id }),\n featureIndex,\n 0\n ) === false\n )\n return false;\n return;\n }\n\n var geomType;\n\n // Callback for multi-geometry\n switch (type) {\n case \"MultiPoint\":\n geomType = \"Point\";\n break;\n case \"MultiLineString\":\n geomType = \"LineString\";\n break;\n case \"MultiPolygon\":\n geomType = \"Polygon\";\n break;\n }\n\n for (\n var multiFeatureIndex = 0;\n // @ts-expect-error: Known type conflict\n multiFeatureIndex < geometry.coordinates.length;\n multiFeatureIndex++\n ) {\n // @ts-expect-error: Known type conflict\n var coordinate = geometry.coordinates[multiFeatureIndex];\n var geom = {\n type: geomType,\n coordinates: coordinate,\n };\n if (\n // @ts-expect-error: Known type conflict\n callback(feature(geom, properties), featureIndex, multiFeatureIndex) ===\n false\n )\n return false;\n }\n });\n}\n\n/**\n * Callback for flattenReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback flattenReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentFeature The current Feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce flattened features in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|GeometryObject|GeometryCollection|Feature} geojson any GeoJSON object\n * @param {flattenReduceCallback} callback a method that takes (previousValue, currentFeature, featureIndex, multiFeatureIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.multiPoint([[40, 30], [36, 53]], {hello: 'world'})\n * ]);\n *\n * turf.flattenReduce(features, function (previousValue, currentFeature, featureIndex, multiFeatureIndex) {\n * //=previousValue\n * //=currentFeature\n * //=featureIndex\n * //=multiFeatureIndex\n * return currentFeature\n * });\n */\nfunction flattenReduce<\n Reducer,\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n previousValue: Reducer,\n currentFeature: Feature,\n featureIndex: number,\n multiFeatureIndex: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n flattenEach(\n geojson,\n function (currentFeature, featureIndex, multiFeatureIndex) {\n if (\n featureIndex === 0 &&\n multiFeatureIndex === 0 &&\n initialValue === undefined\n )\n // @ts-expect-error: Known type conflict\n previousValue = currentFeature;\n else\n previousValue = callback(\n // @ts-expect-error: Known type conflict\n previousValue,\n currentFeature,\n featureIndex,\n multiFeatureIndex\n );\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for segmentEach\n *\n * @callback segmentEachCallback\n * @param {Feature} currentSegment The current Segment being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @param {number} segmentIndex The current index of the Segment being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over 2-vertex line segment in any GeoJSON object, similar to Array.forEach()\n * (Multi)Point geometries do not contain segments therefore they are ignored during this operation.\n *\n * @param {AllGeoJSON} geojson any GeoJSON\n * @param {segmentEachCallback} callback a method that takes (currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex)\n * @returns {void}\n * @example\n * var polygon = turf.polygon([[[-50, 5], [-40, -10], [-50, -10], [-40, 5], [-50, 5]]]);\n *\n * // Iterate over GeoJSON by 2-vertex segments\n * turf.segmentEach(polygon, function (currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex) {\n * //=currentSegment\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * //=segmentIndex\n * });\n *\n * // Calculate the total number of segments\n * var total = 0;\n * turf.segmentEach(polygon, function () {\n * total++;\n * });\n */\nfunction segmentEach

    (\n geojson: AllGeoJSON,\n callback: (\n currentSegment?: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n segmentIndex?: number,\n geometryIndex?: number\n ) => void\n): void {\n flattenEach(geojson, function (feature, featureIndex, multiFeatureIndex) {\n var segmentIndex = 0;\n\n // Exclude null Geometries\n if (!feature.geometry) return;\n // (Multi)Point geometries do not contain segments therefore they are ignored during this operation.\n var type = feature.geometry.type;\n if (type === \"Point\" || type === \"MultiPoint\") return;\n\n // Generate 2-vertex line segments\n // @ts-expect-error: Known type conflict\n var previousCoords;\n var previousFeatureIndex = 0;\n var previousMultiIndex = 0;\n var prevGeomIndex = 0;\n if (\n // @ts-expect-error: Known type conflict\n coordEach(\n feature,\n function (\n currentCoord,\n coordIndex,\n featureIndexCoord,\n multiPartIndexCoord,\n geometryIndex\n ) {\n // Simulating a meta.coordReduce() since `reduce` operations cannot be stopped by returning `false`\n if (\n // @ts-expect-error: Known type conflict\n previousCoords === undefined ||\n featureIndex > previousFeatureIndex ||\n multiPartIndexCoord > previousMultiIndex ||\n geometryIndex > prevGeomIndex\n ) {\n previousCoords = currentCoord;\n previousFeatureIndex = featureIndex;\n previousMultiIndex = multiPartIndexCoord;\n prevGeomIndex = geometryIndex;\n segmentIndex = 0;\n return;\n }\n var currentSegment = lineString(\n // @ts-expect-error: Known type conflict\n [previousCoords, currentCoord],\n feature.properties\n );\n if (\n // @ts-expect-error: Known type conflict\n callback(\n // @ts-expect-error: Known type conflict\n currentSegment,\n featureIndex,\n multiFeatureIndex,\n geometryIndex,\n segmentIndex\n ) === false\n )\n return false;\n segmentIndex++;\n previousCoords = currentCoord;\n }\n ) === false\n )\n return false;\n });\n}\n\n/**\n * Callback for segmentReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback segmentReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentSegment The current Segment being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @param {number} segmentIndex The current index of the Segment being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce 2-vertex line segment in any GeoJSON object, similar to Array.reduce()\n * (Multi)Point geometries do not contain segments therefore they are ignored during this operation.\n *\n * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON\n * @param {segmentReduceCallback} callback a method that takes (previousValue, currentSegment, currentIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer}\n * @example\n * var polygon = turf.polygon([[[-50, 5], [-40, -10], [-50, -10], [-40, 5], [-50, 5]]]);\n *\n * // Iterate over GeoJSON by 2-vertex segments\n * turf.segmentReduce(polygon, function (previousSegment, currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex) {\n * //= previousSegment\n * //= currentSegment\n * //= featureIndex\n * //= multiFeatureIndex\n * //= geometryIndex\n * //= segmentIndex\n * return currentSegment\n * });\n *\n * // Calculate the total number of segments\n * var initialValue = 0\n * var total = turf.segmentReduce(polygon, function (previousValue) {\n * previousValue++;\n * return previousValue;\n * }, initialValue);\n */\nfunction segmentReduce<\n Reducer,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | FeatureCollection\n | Feature\n | Lines\n | Feature\n | GeometryCollection,\n callback: (\n previousValue?: Reducer,\n currentSegment?: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n segmentIndex?: number,\n geometryIndex?: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n var started = false;\n segmentEach(\n geojson,\n function (\n currentSegment,\n featureIndex,\n multiFeatureIndex,\n geometryIndex,\n segmentIndex\n ) {\n if (started === false && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentSegment;\n else\n previousValue = callback(\n previousValue,\n // @ts-expect-error: Known type conflict\n currentSegment,\n featureIndex,\n multiFeatureIndex,\n geometryIndex,\n segmentIndex\n );\n started = true;\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for lineEach\n *\n * @callback lineEachCallback\n * @param {Feature} currentLine The current LineString|LinearRing being processed\n * @param {number} featureIndex The current index of the Feature being processed\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed\n * @param {number} geometryIndex The current index of the Geometry being processed\n * @returns {void}\n */\n\n/**\n * Iterate over line or ring coordinates in LineString, Polygon, MultiLineString, MultiPolygon Features or Geometries,\n * similar to Array.forEach.\n *\n * @function\n * @param {FeatureCollection|Feature|Lines|Feature|GeometryCollection} geojson object\n * @param {lineEachCallback} callback a method that takes (currentLine, featureIndex, multiFeatureIndex, geometryIndex)\n * @returns {void}\n * @example\n * var multiLine = turf.multiLineString([\n * [[26, 37], [35, 45]],\n * [[36, 53], [38, 50], [41, 55]]\n * ]);\n *\n * turf.lineEach(multiLine, function (currentLine, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=currentLine\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * });\n */\nfunction lineEach

    (\n geojson:\n | FeatureCollection\n | Feature\n | Lines\n | Feature\n | GeometryCollection,\n callback: (\n currentLine: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n geometryIndex?: number\n ) => void\n): void {\n // validation\n if (!geojson) throw new Error(\"geojson is required\");\n\n flattenEach(geojson, function (feature, featureIndex, multiFeatureIndex) {\n if (feature.geometry === null) return;\n var type = feature.geometry.type;\n var coords = feature.geometry.coordinates;\n switch (type) {\n case \"LineString\":\n // @ts-expect-error: Known type conflict\n if (callback(feature, featureIndex, multiFeatureIndex, 0, 0) === false)\n return false;\n break;\n case \"Polygon\":\n for (\n var geometryIndex = 0;\n geometryIndex < coords.length;\n geometryIndex++\n ) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n // @ts-expect-error: Known type conflict\n lineString(coords[geometryIndex], feature.properties),\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n return false;\n }\n break;\n }\n });\n}\n\n/**\n * Callback for lineReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback lineReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentLine The current LineString|LinearRing being processed.\n * @param {number} featureIndex The current index of the Feature being processed\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed\n * @param {number} geometryIndex The current index of the Geometry being processed\n * @returns {Reducer}\n */\n\n/**\n * Reduce features in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|Lines|Feature|GeometryCollection} geojson object\n * @param {Function} callback a method that takes (previousValue, currentLine, featureIndex, multiFeatureIndex, geometryIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var multiPoly = turf.multiPolygon([\n * turf.polygon([[[12,48],[2,41],[24,38],[12,48]], [[9,44],[13,41],[13,45],[9,44]]]),\n * turf.polygon([[[5, 5], [0, 0], [2, 2], [4, 4], [5, 5]]])\n * ]);\n *\n * turf.lineReduce(multiPoly, function (previousValue, currentLine, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=previousValue\n * //=currentLine\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * return currentLine\n * });\n */\nfunction lineReduce(\n geojson:\n | FeatureCollection\n | Feature\n | Lines\n | Feature\n | GeometryCollection,\n callback: (\n previousValue?: Reducer,\n currentLine?: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n geometryIndex?: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n lineEach(\n geojson,\n function (currentLine, featureIndex, multiFeatureIndex, geometryIndex) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentLine;\n else\n previousValue = callback(\n previousValue,\n currentLine,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n );\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Finds a particular 2-vertex LineString Segment from a GeoJSON using `@turf/meta` indexes.\n *\n * Negative indexes are permitted.\n * Point & MultiPoint will always return null.\n *\n * @param {FeatureCollection|Feature|Geometry} geojson Any GeoJSON Feature or Geometry\n * @param {Object} [options={}] Optional parameters\n * @param {number} [options.featureIndex=0] Feature Index\n * @param {number} [options.multiFeatureIndex=0] Multi-Feature Index\n * @param {number} [options.geometryIndex=0] Geometry Index\n * @param {number} [options.segmentIndex=0] Segment Index\n * @param {Object} [options.properties={}] Translate Properties to output LineString\n * @param {BBox} [options.bbox={}] Translate BBox to output LineString\n * @param {number|string} [options.id={}] Translate Id to output LineString\n * @returns {Feature} 2-vertex GeoJSON Feature LineString\n * @example\n * var multiLine = turf.multiLineString([\n * [[10, 10], [50, 30], [30, 40]],\n * [[-10, -10], [-50, -30], [-30, -40]]\n * ]);\n *\n * // First Segment (defaults are 0)\n * turf.findSegment(multiLine);\n * // => Feature>\n *\n * // First Segment of 2nd Multi Feature\n * turf.findSegment(multiLine, {multiFeatureIndex: 1});\n * // => Feature>\n *\n * // Last Segment of Last Multi Feature\n * turf.findSegment(multiLine, {multiFeatureIndex: -1, segmentIndex: -1});\n * // => Feature>\n */\nfunction findSegment<\n G extends LineString | MultiLineString | Polygon | MultiPolygon,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson: Feature | FeatureCollection | G,\n options?: {\n featureIndex?: number;\n multiFeatureIndex?: number;\n geometryIndex?: number;\n segmentIndex?: number;\n properties?: P;\n bbox?: BBox;\n id?: Id;\n }\n): Feature {\n // Optional Parameters\n options = options || {};\n if (!isObject(options)) throw new Error(\"options is invalid\");\n var featureIndex = options.featureIndex || 0;\n var multiFeatureIndex = options.multiFeatureIndex || 0;\n var geometryIndex = options.geometryIndex || 0;\n var segmentIndex = options.segmentIndex || 0;\n\n // Find FeatureIndex\n var properties = options.properties;\n var geometry;\n\n switch (geojson.type) {\n case \"FeatureCollection\":\n if (featureIndex < 0)\n featureIndex = geojson.features.length + featureIndex;\n properties = properties || geojson.features[featureIndex].properties;\n geometry = geojson.features[featureIndex].geometry;\n break;\n case \"Feature\":\n properties = properties || geojson.properties;\n geometry = geojson.geometry;\n break;\n case \"Point\" as GeoJsonTypes:\n case \"MultiPoint\" as GeoJsonTypes:\n // @ts-expect-error: Known type conflict\n return null;\n case \"LineString\":\n case \"Polygon\":\n case \"MultiLineString\":\n case \"MultiPolygon\":\n geometry = geojson;\n break;\n default:\n throw new Error(\"geojson is invalid\");\n }\n\n // Find SegmentIndex\n // @ts-expect-error: Known type conflict\n if (geometry === null) return null;\n var coords = geometry.coordinates;\n switch (geometry.type) {\n case \"Point\" as GeoJsonTypes:\n case \"MultiPoint\" as GeoJsonTypes:\n // @ts-expect-error: Known type conflict\n return null;\n case \"LineString\":\n if (segmentIndex < 0) segmentIndex = coords.length + segmentIndex - 1;\n return lineString(\n // @ts-expect-error: Known type conflict\n [coords[segmentIndex], coords[segmentIndex + 1]],\n properties,\n options\n );\n case \"Polygon\":\n if (geometryIndex < 0) geometryIndex = coords.length + geometryIndex;\n if (segmentIndex < 0)\n segmentIndex = coords[geometryIndex].length + segmentIndex - 1;\n return lineString(\n [\n // @ts-expect-error: Known type conflict\n coords[geometryIndex][segmentIndex],\n // @ts-expect-error: Known type conflict\n coords[geometryIndex][segmentIndex + 1],\n ],\n properties,\n options\n );\n case \"MultiLineString\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (segmentIndex < 0)\n segmentIndex = coords[multiFeatureIndex].length + segmentIndex - 1;\n return lineString(\n [\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][segmentIndex],\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][segmentIndex + 1],\n ],\n properties,\n options\n );\n case \"MultiPolygon\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (geometryIndex < 0)\n geometryIndex = coords[multiFeatureIndex].length + geometryIndex;\n if (segmentIndex < 0)\n segmentIndex =\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][geometryIndex].length - segmentIndex - 1;\n return lineString(\n [\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][geometryIndex][segmentIndex],\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][geometryIndex][segmentIndex + 1],\n ],\n properties,\n options\n );\n }\n throw new Error(\"geojson is invalid\");\n}\n\n/**\n * Finds a particular Point from a GeoJSON using `@turf/meta` indexes.\n *\n * Negative indexes are permitted.\n *\n * @param {FeatureCollection|Feature|Geometry} geojson Any GeoJSON Feature or Geometry\n * @param {Object} [options={}] Optional parameters\n * @param {number} [options.featureIndex=0] Feature Index\n * @param {number} [options.multiFeatureIndex=0] Multi-Feature Index\n * @param {number} [options.geometryIndex=0] Geometry Index\n * @param {number} [options.coordIndex=0] Coord Index\n * @param {Object} [options.properties={}] Translate Properties to output Point\n * @param {BBox} [options.bbox={}] Translate BBox to output Point\n * @param {number|string} [options.id={}] Translate Id to output Point\n * @returns {Feature} 2-vertex GeoJSON Feature Point\n * @example\n * var multiLine = turf.multiLineString([\n * [[10, 10], [50, 30], [30, 40]],\n * [[-10, -10], [-50, -30], [-30, -40]]\n * ]);\n *\n * // First Segment (defaults are 0)\n * turf.findPoint(multiLine);\n * // => Feature>\n *\n * // First Segment of the 2nd Multi-Feature\n * turf.findPoint(multiLine, {multiFeatureIndex: 1});\n * // => Feature>\n *\n * // Last Segment of last Multi-Feature\n * turf.findPoint(multiLine, {multiFeatureIndex: -1, coordIndex: -1});\n * // => Feature>\n */\nfunction findPoint<\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson: Feature | FeatureCollection | G,\n options?: {\n featureIndex?: number;\n multiFeatureIndex?: number;\n geometryIndex?: number;\n coordIndex?: number;\n properties?: P;\n bbox?: BBox;\n id?: Id;\n }\n): Feature {\n // Optional Parameters\n options = options || {};\n if (!isObject(options)) throw new Error(\"options is invalid\");\n var featureIndex = options.featureIndex || 0;\n var multiFeatureIndex = options.multiFeatureIndex || 0;\n var geometryIndex = options.geometryIndex || 0;\n var coordIndex = options.coordIndex || 0;\n\n // Find FeatureIndex\n var properties = options.properties;\n var geometry;\n\n switch (geojson.type) {\n case \"FeatureCollection\":\n if (featureIndex < 0)\n featureIndex = geojson.features.length + featureIndex;\n properties = properties || geojson.features[featureIndex].properties;\n geometry = geojson.features[featureIndex].geometry;\n break;\n case \"Feature\":\n properties = properties || geojson.properties;\n geometry = geojson.geometry;\n break;\n case \"Point\":\n case \"MultiPoint\":\n // @ts-expect-error: Known type conflict\n return null;\n case \"LineString\":\n case \"Polygon\":\n case \"MultiLineString\":\n case \"MultiPolygon\":\n geometry = geojson;\n break;\n default:\n throw new Error(\"geojson is invalid\");\n }\n\n // Find Coord Index\n // @ts-expect-error: Known type conflict\n if (geometry === null) return null;\n // @ts-expect-error: Known type conflict\n var coords = geometry.coordinates;\n switch (geometry.type) {\n case \"Point\":\n return point(coords, properties, options);\n case \"MultiPoint\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n return point(coords[multiFeatureIndex], properties, options);\n case \"LineString\":\n if (coordIndex < 0) coordIndex = coords.length + coordIndex;\n return point(coords[coordIndex], properties, options);\n case \"Polygon\":\n if (geometryIndex < 0) geometryIndex = coords.length + geometryIndex;\n if (coordIndex < 0)\n coordIndex = coords[geometryIndex].length + coordIndex;\n return point(coords[geometryIndex][coordIndex], properties, options);\n case \"MultiLineString\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (coordIndex < 0)\n coordIndex = coords[multiFeatureIndex].length + coordIndex;\n return point(coords[multiFeatureIndex][coordIndex], properties, options);\n case \"MultiPolygon\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (geometryIndex < 0)\n geometryIndex = coords[multiFeatureIndex].length + geometryIndex;\n if (coordIndex < 0)\n coordIndex =\n coords[multiFeatureIndex][geometryIndex].length - coordIndex;\n return point(\n coords[multiFeatureIndex][geometryIndex][coordIndex],\n properties,\n options\n );\n }\n throw new Error(\"geojson is invalid\");\n}\n\nexport {\n coordReduce,\n coordEach,\n propEach,\n propReduce,\n featureReduce,\n featureEach,\n coordAll,\n geomReduce,\n geomEach,\n flattenReduce,\n flattenEach,\n segmentReduce,\n segmentEach,\n lineReduce,\n lineEach,\n findSegment,\n findPoint,\n};\n", "import { BBox } from \"geojson\";\nimport { AllGeoJSON } from \"@turf/helpers\";\nimport { coordEach } from \"@turf/meta\";\n\n/**\n * Calculates the bounding box for any GeoJSON object, including FeatureCollection.\n * Uses geojson.bbox if available and options.recompute is not set.\n *\n * @function\n * @param {GeoJSON} geojson any GeoJSON object\n * @param {Object} [options={}] Optional parameters\n * @param {boolean} [options.recompute] Whether to ignore an existing bbox property on geojson\n * @returns {BBox} bbox extent in [minX, minY, maxX, maxY] order\n * @example\n * var line = turf.lineString([[-74, 40], [-78, 42], [-82, 35]]);\n * var bbox = turf.bbox(line);\n * var bboxPolygon = turf.bboxPolygon(bbox);\n *\n * //addToMap\n * var addToMap = [line, bboxPolygon]\n */\nfunction bbox(\n geojson: AllGeoJSON,\n options: {\n recompute?: boolean;\n } = {}\n): BBox {\n if (geojson.bbox != null && true !== options.recompute) {\n return geojson.bbox;\n }\n const result: BBox = [Infinity, Infinity, -Infinity, -Infinity];\n coordEach(geojson, (coord) => {\n if (result[0] > coord[0]) {\n result[0] = coord[0];\n }\n if (result[1] > coord[1]) {\n result[1] = coord[1];\n }\n if (result[2] < coord[0]) {\n result[2] = coord[0];\n }\n if (result[3] < coord[1]) {\n result[3] = coord[1];\n }\n });\n return result;\n}\n\nexport { bbox };\nexport default bbox;\n", "import { select as d3_select } from 'd3-selection';\nimport { t } from '../core/localizer';\n\nimport { geoScaleToZoom, geoVecLength } from '../geo';\nimport { utilPrefixCSSProperty, utilTiler } from '../util';\n\n\nexport function rendererTileLayer(context) {\n var transformProp = utilPrefixCSSProperty('Transform');\n var tiler = utilTiler();\n\n var _tileSize = 256;\n var _projection;\n var _cache = {};\n var _tileOrigin;\n var _zoom;\n var _source;\n var _underzoom = 0;\n\n function tileSizeAtZoom(d, z) {\n return (d.tileSize * Math.pow(2, z - d[2])) / d.tileSize;\n }\n\n\n function atZoom(t, distance) {\n var power = Math.pow(2, distance);\n return [\n Math.floor(t[0] * power),\n Math.floor(t[1] * power),\n t[2] + distance\n ];\n }\n\n\n function lookUp(d) {\n for (var up = -1; up > -d[2]; up--) {\n var tile = atZoom(d, up);\n if (_cache[_source.url(tile)] !== false) {\n return tile;\n }\n }\n }\n\n\n function uniqueBy(a, n) {\n var o = [];\n var seen = {};\n for (var i = 0; i < a.length; i++) {\n if (seen[a[i][n]] === undefined) {\n o.push(a[i]);\n seen[a[i][n]] = true;\n }\n }\n return o;\n }\n\n\n function addSource(d) {\n d.url = _source.url(d);\n d.tileSize = _tileSize;\n d.source = _source;\n return d;\n }\n\n\n // Update tiles based on current state of `projection`.\n function background(selection) {\n _zoom = geoScaleToZoom(_projection.scale(), _tileSize);\n\n var pixelOffset;\n if (_source) {\n pixelOffset = [\n _source.offset()[0] * Math.pow(2, _zoom),\n _source.offset()[1] * Math.pow(2, _zoom)\n ];\n } else {\n pixelOffset = [0, 0];\n }\n\n\n tiler\n .scale(_projection.scale() * 2 * Math.PI)\n .translate([\n _projection.translate()[0] + pixelOffset[0],\n _projection.translate()[1] + pixelOffset[1]\n ]);\n\n _tileOrigin = [\n _projection.scale() * Math.PI - _projection.translate()[0],\n _projection.scale() * Math.PI - _projection.translate()[1]\n ];\n\n render(selection);\n }\n\n\n // Derive the tiles onscreen, remove those offscreen and position them.\n // Important that this part not depend on `_projection` because it's\n // rendered when tiles load/error (see #644).\n function render(selection) {\n if (!_source) return;\n var requests = [];\n var showDebug = context.getDebug('tile') && !_source.overlay;\n\n if (_source.validZoom(_zoom, _underzoom)) {\n tiler.skipNullIsland(!!_source.overlay);\n\n tiler().forEach(function(d) {\n addSource(d);\n if (d.url === '') return;\n if (typeof d.url !== 'string') return; // Workaround for #2295\n requests.push(d);\n if (_cache[d.url] === false && lookUp(d)) {\n requests.push(addSource(lookUp(d)));\n }\n });\n\n requests = uniqueBy(requests, 'url').filter(function(r) {\n // don't re-request tiles which have failed in the past\n return _cache[r.url] !== false;\n });\n }\n\n function load(d3_event, d) {\n _cache[d.url] = true;\n d3_select(this)\n .on('error', null)\n .on('load', null);\n render(selection);\n }\n\n function error(d3_event, d) {\n _cache[d.url] = false;\n d3_select(this)\n .on('error', null)\n .on('load', null)\n .remove();\n render(selection);\n }\n\n function imageTransform(d) {\n var ts = d.tileSize * Math.pow(2, _zoom - d[2]);\n var scale = tileSizeAtZoom(d, _zoom);\n return 'translate(' +\n ((d[0] * ts + d.source.offset()[0] * Math.pow(2, _zoom)) * _tileSize / d.tileSize - _tileOrigin[0]\n ) + 'px,' +\n ((d[1] * ts + d.source.offset()[1] * Math.pow(2, _zoom)) * _tileSize / d.tileSize - _tileOrigin[1]\n ) + 'px) ' +\n 'scale(' + scale * _tileSize / d.tileSize + ',' + scale * _tileSize / d.tileSize + ')';\n }\n\n function tileCenter(d) {\n var ts = d.tileSize * Math.pow(2, _zoom - d[2]);\n return [\n ((d[0] * ts) - _tileOrigin[0] + (ts / 2)),\n ((d[1] * ts) - _tileOrigin[1] + (ts / 2))\n ];\n }\n\n function debugTransform(d) {\n var coord = tileCenter(d);\n return 'translate(' + coord[0] + 'px,' + coord[1] + 'px)';\n }\n\n\n // Pick a representative tile near the center of the viewport\n // (This is useful for sampling the imagery vintage)\n var dims = tiler.size();\n var mapCenter = [dims[0] / 2, dims[1] / 2];\n var minDist = Math.max(dims[0], dims[1]);\n var nearCenter;\n\n requests.forEach(function(d) {\n var c = tileCenter(d);\n var dist = geoVecLength(c, mapCenter);\n if (dist < minDist) {\n minDist = dist;\n nearCenter = d;\n }\n });\n\n\n var image = selection.selectAll('img')\n .data(requests, function(d) { return d.url; });\n\n image.exit()\n .style(transformProp, imageTransform)\n .classed('tile-removing', true)\n .classed('tile-center', false)\n .on('transitionend', function() {\n const tile = d3_select(this);\n if (tile.classed('tile-removing')) {\n tile.remove();\n }\n });\n\n image.enter()\n .append('img')\n .attr('class', 'tile')\n .attr('alt', '')\n .attr('draggable', 'false')\n .style('width', _tileSize + 'px')\n .style('height', _tileSize + 'px')\n .attr('src', function(d) { return d.url; })\n .on('error', error)\n .on('load', load)\n .merge(image)\n .style(transformProp, imageTransform)\n .classed('tile-debug', showDebug)\n .classed('tile-removing', false)\n .classed('tile-center', function(d) { return d === nearCenter; })\n .sort((a, b) => a[2] - b[2]);\n\n\n\n var debug = selection.selectAll('.tile-label-debug')\n .data(showDebug ? requests : [], function(d) { return d.url; });\n\n debug.exit()\n .remove();\n\n if (showDebug) {\n var debugEnter = debug.enter()\n .append('div')\n .attr('class', 'tile-label-debug');\n\n debugEnter\n .append('div')\n .attr('class', 'tile-label-debug-coord');\n\n debugEnter\n .append('div')\n .attr('class', 'tile-label-debug-vintage');\n\n debug = debug.merge(debugEnter);\n\n debug\n .style(transformProp, debugTransform);\n\n debug\n .selectAll('.tile-label-debug-coord')\n .text(function(d) { return d[2] + ' / ' + d[0] + ' / ' + d[1]; });\n\n debug\n .selectAll('.tile-label-debug-vintage')\n .each(function(d) {\n var span = d3_select(this);\n var center = context.projection.invert(tileCenter(d));\n _source.getMetadata(center, d, function(err, result) {\n if (result && result.vintage && result.vintage.range) {\n span.text(result.vintage.range);\n } else {\n span.text('');\n span.call(t.append('info_panels.background.vintage'));\n span.append('span').text(': ');\n span.call(t.append('info_panels.background.unknown'));\n }\n });\n });\n }\n\n }\n\n\n background.projection = function(val) {\n if (!arguments.length) return _projection;\n _projection = val;\n return background;\n };\n\n\n background.dimensions = function(val) {\n if (!arguments.length) return tiler.size();\n tiler.size(val);\n return background;\n };\n\n\n background.source = function(val) {\n if (!arguments.length) return _source;\n _source = val;\n _tileSize = _source.tileSize;\n _cache = {};\n tiler.tileSize(_source.tileSize).zoomExtent(_source.zoomExtent);\n return background;\n };\n\n\n background.underzoom = function(amount) {\n if (!arguments.length) return _underzoom;\n _underzoom = amount;\n return background;\n };\n\n\n return background;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate';\nimport { select as d3_select } from 'd3-selection';\nimport turf_bboxClip from '@turf/bbox-clip';\nimport turf_bbox from '@turf/bbox';\n\nimport whichPolygon from 'which-polygon';\n\nimport { prefs } from '../core/preferences';\nimport { fileFetcher } from '../core/file_fetcher';\nimport { geoMetersToOffset, geoOffsetToMeters, geoExtent } from '../geo';\nimport { rendererBackgroundSource } from './background_source';\nimport { rendererTileLayer } from './tile_layer';\nimport { utilQsString, utilStringQs } from '../util';\nimport { utilRebind } from '../util/rebind';\n\n\nlet _imageryIndex = null;\nlet _waybackIndex = null;\n\nexport function rendererBackground(context) {\n const dispatch = d3_dispatch('change');\n const baseLayer = rendererTileLayer(context).projection(context.projection);\n let _checkedBlocklists = [];\n let _isValid = true;\n let _overlayLayers = [];\n let _brightness = 1;\n let _contrast = 1;\n let _saturation = 1;\n let _sharpness = 1;\n\n\n function ensureImageryIndex() {\n return fileFetcher.get('wayback')\n .then(groups => {\n // eslint-disable-next-line no-warning-comments\n // TODO: Follow pagination via nextStart property.\n // Extracts the layer's date from the title.\n let extractDateFromTitle = title => {\n const dateComponents = title.match(/\\(Wayback (\\d{4})-(\\d\\d)-(\\d\\d)\\)/);\n if (!dateComponents) return;\n return new Date(Date.UTC(parseInt(dateComponents[1], 10),\n parseInt(dateComponents[2], 10) - 1,\n parseInt(dateComponents[3], 10)));\n };\n\n if (!_waybackIndex) {\n // Index the metadata MapServer URLs by the date of the World Imagery map.\n let metadataMapServersByDate = Object.fromEntries(groups.items\n .filter(item => item.type === 'Map Service')\n .map(item => {\n // Extract the layer's date from the title to avoid having to hit each MapServer right away.\n const date = extractDateFromTitle(item.title) || new Date(item.created);\n const dateString = date.toISOString().split('T')[0];\n return [dateString, item.url];\n }));\n\n _waybackIndex = groups.items\n .filter(item => item.type === 'WMTS')\n .map(item => {\n const date = extractDateFromTitle(item.title) || new Date(item.created);\n const dateString = date && date.toISOString().split('T')[0];\n\n // Convert the bounding box to a polygon.\n const bbox = {\n min_lon: item.extent[0][0],\n min_lat: item.extent[0][1],\n max_lon: item.extent[1][0],\n max_lat: item.extent[1][1],\n };\n const polygon = [[\n [bbox.min_lon, bbox.min_lat],\n [bbox.min_lon, bbox.max_lat],\n [bbox.max_lon, bbox.max_lat],\n [bbox.max_lon, bbox.min_lat],\n [bbox.min_lon, bbox.min_lat],\n ]];\n\n // Convert placeholder tokens in the URL template from Esri's format to OSM's.\n const template = item.url\n .replaceAll('{level}', '{zoom}')\n .replaceAll('{row}', '{y}')\n .replaceAll('{col}', '{x}');\n\n return {\n id: 'EsriWorldImagery_' + dateString,\n name: item.title,\n type: 'tms',\n template: template,\n metadata: metadataMapServersByDate[dateString],\n startDate: date.toISOString(),\n endDate: date.toISOString(),\n polygon: polygon,\n terms_text: item.accessInformation,\n description: item.snippet,\n // Match Esri World Imagery layer\n 'default': true,\n zoomExtent: [0, 22],\n terms_url: 'https://wiki.openstreetmap.org/wiki/Esri',\n icon: 'https://osmlab.github.io/editor-layer-index/sources/world/EsriImageryClarity.png',\n };\n });\n }\n return fileFetcher.get('imagery');\n })\n .catch(() => {\n return fileFetcher.get('imagery');\n })\n .then(sources => {\n if (_imageryIndex) return _imageryIndex;\n\n // Append Esri World Imagery Wayback sources.\n if (_waybackIndex) {\n sources.push(..._waybackIndex);\n }\n\n _imageryIndex = {\n imagery: sources,\n features: {}\n };\n\n // use which-polygon to support efficient index and querying for imagery\n const features = sources.map(source => {\n if (!source.polygon) return null;\n // workaround for editor-layer-index weirdness..\n // Add an extra array nest to each element in `source.polygon`\n // so the rings are not treated as a bunch of holes:\n // what we have: [ [[outer],[hole],[hole]] ]\n // what we want: [ [[outer]],[[outer]],[[outer]] ]\n const rings = source.polygon.map(ring => [ring]);\n\n const feature = {\n type: 'Feature',\n properties: { id: source.id },\n geometry: { type: 'MultiPolygon', coordinates: rings }\n };\n\n _imageryIndex.features[source.id] = feature;\n return feature;\n\n }).filter(Boolean);\n\n _imageryIndex.query = whichPolygon({ type: 'FeatureCollection', features: features });\n\n\n // Instantiate `rendererBackgroundSource` objects for each source\n _imageryIndex.backgrounds = sources.map(source => {\n if (source.type === 'bing') {\n return rendererBackgroundSource.Bing(source, dispatch);\n } else if (/^EsriWorldImagery/.test(source.id)) {\n return rendererBackgroundSource.Esri(source);\n } else {\n return rendererBackgroundSource(source);\n }\n });\n\n // Add 'None'\n _imageryIndex.backgrounds.unshift(rendererBackgroundSource.None());\n\n // Add 'Custom'\n let template = prefs('background-custom-template') || '';\n const custom = rendererBackgroundSource.Custom(template);\n _imageryIndex.backgrounds.unshift(custom);\n\n return _imageryIndex;\n });\n }\n\n\n function background(selection) {\n const currSource = baseLayer.source();\n\n // If we are displaying an Esri basemap at high zoom,\n // check its tilemap to see how high the zoom can go\n if (context.map().zoom() > 18) {\n if (currSource && /^EsriWorldImagery/.test(currSource.id)) {\n const center = context.map().center();\n currSource.fetchTilemap(center);\n }\n }\n\n // Is the imagery valid here? - #4827\n const sources = background.sources(context.map().extent());\n const wasValid = _isValid;\n _isValid = !!sources.filter(d => d === currSource).length;\n\n if (wasValid !== _isValid) { // change in valid status\n background.updateImagery();\n }\n\n\n let baseFilter = '';\n if (_brightness !== 1) {\n baseFilter += ` brightness(${_brightness})`;\n }\n if (_contrast !== 1) {\n baseFilter += ` contrast(${_contrast})`;\n }\n if (_saturation !== 1) {\n baseFilter += ` saturate(${_saturation})`;\n }\n if (_sharpness < 1) { // gaussian blur\n const blur = d3_interpolateNumber(0.5, 5)(1 - _sharpness);\n baseFilter += ` blur(${blur}px)`;\n }\n\n let base = selection.selectAll('.layer-background')\n .data([0]);\n\n base = base.enter()\n .insert('div', '.layer-data')\n .attr('class', 'layer layer-background')\n .merge(base);\n\n base.style('filter', baseFilter || null);\n\n\n let imagery = base.selectAll('.layer-imagery')\n .data([0]);\n\n imagery.enter()\n .append('div')\n .attr('class', 'layer layer-imagery')\n .merge(imagery)\n .call(baseLayer);\n\n\n let maskFilter = '';\n let mixBlendMode = '';\n if (_sharpness > 1) { // apply unsharp mask\n mixBlendMode = 'overlay';\n maskFilter = 'saturate(0) blur(3px) invert(1)';\n\n let contrast = _sharpness - 1;\n maskFilter += ` contrast(${contrast})`;\n\n let brightness = d3_interpolateNumber(1, 0.85)(_sharpness - 1);\n maskFilter += ` brightness(${brightness})`;\n }\n\n let mask = base.selectAll('.layer-unsharp-mask')\n .data(_sharpness > 1 ? [0] : []);\n\n mask.exit()\n .remove();\n\n mask.enter()\n .append('div')\n .attr('class', 'layer layer-mask layer-unsharp-mask')\n .merge(mask)\n .call(baseLayer)\n .style('filter', maskFilter || null)\n .style('mix-blend-mode', mixBlendMode || null);\n\n\n let overlays = selection.selectAll('.layer-overlay')\n .data(_overlayLayers, d => d.source().name());\n\n overlays.exit()\n .remove();\n\n overlays.enter()\n .insert('div', '.layer-data')\n .attr('class', 'layer layer-overlay')\n .merge(overlays)\n .each((layer, i, nodes) => d3_select(nodes[i]).call(layer));\n }\n\n\n background.updateImagery = function() {\n let currSource = baseLayer.source();\n if (context.inIntro() || !currSource) return;\n\n let o = _overlayLayers\n .filter(d => !d.source().isLocatorOverlay() && !d.source().isHidden())\n .map(d => d.source().id)\n .join(',');\n\n const meters = geoOffsetToMeters(currSource.offset());\n const EPSILON = 0.01;\n const x = +meters[0].toFixed(2);\n const y = +meters[1].toFixed(2);\n let hash = utilStringQs(window.location.hash);\n\n let id = currSource.id;\n if (id === 'custom') {\n id = `custom:${currSource.template()}`;\n }\n\n if (id) {\n hash.background = id;\n } else {\n delete hash.background;\n }\n\n if (o) {\n hash.overlays = o;\n } else {\n delete hash.overlays;\n }\n\n if (Math.abs(x) > EPSILON || Math.abs(y) > EPSILON) {\n hash.offset = `${x},${y}`;\n } else {\n delete hash.offset;\n }\n\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n\n let imageryUsed = [];\n let photoOverlaysUsed = [];\n\n const currUsed = currSource.imageryUsed();\n if (currUsed && _isValid) {\n imageryUsed.push(currUsed);\n }\n\n _overlayLayers\n .filter(d => !d.source().isLocatorOverlay() && !d.source().isHidden())\n .forEach(d => imageryUsed.push(d.source().imageryUsed()));\n\n const dataLayer = context.layers().layer('data');\n if (dataLayer && dataLayer.enabled() && dataLayer.hasData()) {\n imageryUsed.push(dataLayer.getSrc());\n }\n\n const photoOverlayLayers = {\n streetside: 'Bing Streetside',\n mapillary: 'Mapillary Images',\n 'mapillary-map-features': 'Mapillary Map Features',\n 'mapillary-signs': 'Mapillary Signs',\n kartaview: 'KartaView Images',\n vegbilder: 'Norwegian Road Administration Images',\n mapilio: 'Mapilio Images',\n panoramax: 'Panoramax Images'\n };\n\n for (let layerID in photoOverlayLayers) {\n const layer = context.layers().layer(layerID);\n if (layer && layer.enabled()) {\n photoOverlaysUsed.push(layerID);\n imageryUsed.push(photoOverlayLayers[layerID]);\n }\n }\n\n context.history().imageryUsed(imageryUsed);\n context.history().photoOverlaysUsed(photoOverlaysUsed);\n };\n\n\n background.sources = (extent, zoom, includeCurrent) => {\n if (!_imageryIndex) return []; // called before init()?\n\n let visible = {};\n (_imageryIndex.query.bbox(extent.rectangle(), true) || [])\n .forEach(d => visible[d.id] = true);\n\n const currSource = baseLayer.source();\n\n // Recheck blocked sources only if we detect new blocklists pulled from the OSM API.\n const osm = context.connection();\n const blocklists = (osm && osm.imageryBlocklists()) || [];\n const blocklistChanged = (blocklists.length !== _checkedBlocklists.length) ||\n blocklists.some((regex, index) => String(regex) !== _checkedBlocklists[index]);\n\n if (blocklistChanged) {\n _imageryIndex.backgrounds.forEach(source => {\n source.isBlocked = blocklists.some(regex => regex.test(source.template()));\n });\n _checkedBlocklists = blocklists.map(regex => String(regex));\n }\n\n return _imageryIndex.backgrounds.filter(source => {\n if (includeCurrent && currSource === source) return true; // optionally always include the current imagery\n if (source.isBlocked) return false; // even bundled sources may be blocked - #7905\n if (!source.polygon) return true; // always include imagery with worldwide coverage\n if (zoom && zoom < 6) return false; // optionally exclude local imagery at low zooms\n return visible[source.id]; // include imagery visible in given extent\n });\n };\n\n\n background.dimensions = (val) => {\n if (!val) return;\n baseLayer.dimensions(val);\n _overlayLayers.forEach(layer => layer.dimensions(val));\n };\n\n\n background.baseLayerSource = function(d) {\n if (!arguments.length) return baseLayer.source();\n\n // test source against OSM imagery blocklists..\n const osm = context.connection();\n if (!osm) return background;\n\n const blocklists = osm.imageryBlocklists();\n const template = d.template();\n let fail = false;\n let tested = 0;\n let regex;\n\n for (let i = 0; i < blocklists.length; i++) {\n regex = blocklists[i];\n fail = regex.test(template);\n tested++;\n if (fail) break;\n }\n\n // ensure at least one test was run.\n if (!tested) {\n regex = /.*\\.google(apis)?\\..*\\/(vt|kh)[\\?\\/].*([xyz]=.*){3}.*/;\n fail = regex.test(template);\n }\n\n baseLayer.source(!fail ? d : background.findSource('none'));\n dispatch.call('change');\n background.updateImagery();\n return background;\n };\n\n\n background.findSource = (id) => {\n if (!id || !_imageryIndex) return null; // called before init()?\n return _imageryIndex.backgrounds.find(d => d.id && d.id === id);\n };\n\n\n background.bing = () => {\n background.baseLayerSource(background.findSource('Bing'));\n };\n\n\n background.showsLayer = (d) => {\n const currSource = baseLayer.source();\n if (!d || !currSource) return false;\n return d.id === currSource.id || _overlayLayers.some(layer => d.id === layer.source().id);\n };\n\n\n background.overlayLayerSources = () => {\n return _overlayLayers.map(layer => layer.source());\n };\n\n\n background.toggleOverlayLayer = (d) => {\n let layer;\n for (let i = 0; i < _overlayLayers.length; i++) {\n layer = _overlayLayers[i];\n if (layer.source() === d) {\n _overlayLayers.splice(i, 1);\n dispatch.call('change');\n background.updateImagery();\n return;\n }\n }\n\n layer = rendererTileLayer(context)\n .source(d)\n .projection(context.projection)\n .dimensions(baseLayer.dimensions()\n );\n\n _overlayLayers.push(layer);\n dispatch.call('change');\n background.updateImagery();\n };\n\n\n background.nudge = (d, zoom) => {\n const currSource = baseLayer.source();\n if (currSource) {\n currSource.nudge(d, zoom);\n dispatch.call('change');\n background.updateImagery();\n }\n return background;\n };\n\n\n background.offset = function(d) {\n const currSource = baseLayer.source();\n if (!arguments.length) {\n return (currSource && currSource.offset()) || [0, 0];\n }\n if (currSource) {\n currSource.offset(d);\n dispatch.call('change');\n background.updateImagery();\n }\n return background;\n };\n\n\n background.brightness = function(d) {\n if (!arguments.length) return _brightness;\n _brightness = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n\n background.contrast = function(d) {\n if (!arguments.length) return _contrast;\n _contrast = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n\n background.saturation = function(d) {\n if (!arguments.length) return _saturation;\n _saturation = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n\n background.sharpness = function(d) {\n if (!arguments.length) return _sharpness;\n _sharpness = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n let _loadPromise;\n\n background.ensureLoaded = () => {\n if (_loadPromise) return _loadPromise;\n\n return _loadPromise = ensureImageryIndex();\n };\n\n background.init = () => {\n const loadPromise = background.ensureLoaded();\n\n const hash = utilStringQs(window.location.hash);\n const requestedBackground = hash.background || hash.layer;\n const lastUsedBackground = prefs('background-last-used');\n\n return loadPromise.then(imageryIndex => {\n const extent = context.map().extent();\n const validBackgrounds = background.sources(extent).filter(d => d.id !== 'none' && d.id !== 'custom');\n const first = validBackgrounds.length && validBackgrounds[0];\n const isLastUsedValid = !!validBackgrounds.find(d => d.id && d.id === lastUsedBackground);\n\n let best;\n if (!requestedBackground && extent) {\n const viewArea = extent.area();\n best = validBackgrounds.find(s => {\n if (!s.best() || s.overlay) return false;\n let bbox = turf_bbox(turf_bboxClip(\n { type: 'MultiPolygon', coordinates: [ s.polygon || [extent.polygon()] ] },\n extent.rectangle()));\n let area = geoExtent(bbox.slice(0,2), bbox.slice(2,4)).area();\n return area / viewArea > 0.5; // min visible size: 50% of viewport area\n });\n }\n\n // Decide which background layer to display\n if (requestedBackground && requestedBackground.indexOf('custom:') === 0) {\n const template = requestedBackground.replace(/^custom:/, '');\n const custom = background.findSource('custom');\n background.baseLayerSource(custom.template(template));\n prefs('background-custom-template', template);\n } else {\n background.baseLayerSource(\n background.findSource(requestedBackground) ||\n best ||\n isLastUsedValid && background.findSource(lastUsedBackground) ||\n background.findSource('Bing') ||\n first ||\n background.findSource('none')\n );\n }\n\n const locator = imageryIndex.backgrounds.find(d => d.overlay && d.default);\n if (locator) {\n background.toggleOverlayLayer(locator);\n }\n\n const overlays = (hash.overlays || '').split(',');\n overlays.forEach(overlay => {\n overlay = background.findSource(overlay);\n if (overlay) {\n background.toggleOverlayLayer(overlay);\n }\n });\n\n if (hash.gpx) {\n const gpx = context.layers().layer('data');\n if (gpx) {\n gpx.url(hash.gpx, '.gpx');\n }\n }\n\n if (hash.offset) {\n const offset = hash.offset\n .replace(/;/g, ',')\n .split(',')\n .map(n => !isNaN(n) && n);\n\n if (offset.length === 2) {\n background.offset(geoMetersToOffset(offset));\n }\n }\n })\n .catch(err => {\n /* eslint-disable no-console */\n console.error(err);\n /* eslint-enable no-console */\n });\n };\n\n\n return utilRebind(background, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from '../core/preferences';\nimport { osmEntity } from '../osm';\nimport { osmLanduseTags, osmLifecyclePrefixes } from '../osm/tags.js';\nimport { utilRebind } from '../util/rebind';\nimport { utilArrayGroupBy, utilArrayUnion, utilQsString, utilStringQs, utilDatesOverlap } from '../util';\nimport { isAddressPoint } from '../svg/labels';\n\n\nexport function rendererFeatures(context) {\n var dispatch = d3_dispatch('change', 'redraw');\n const features = {};\n var _deferred = new Set();\n\n var traffic_roads = {\n 'motorway': true,\n 'motorway_link': true,\n 'trunk': true,\n 'trunk_link': true,\n 'primary': true,\n 'primary_link': true,\n 'secondary': true,\n 'secondary_link': true,\n 'tertiary': true,\n 'tertiary_link': true,\n 'residential': true,\n 'unclassified': true,\n 'living_street': true,\n 'busway': true\n };\n\n var service_roads = {\n 'service': true,\n 'road': true,\n 'track': true\n };\n\n var paths = {\n 'path': true,\n 'footway': true,\n 'cycleway': true,\n 'bridleway': true,\n 'steps': true,\n 'ladder': true,\n 'pedestrian': true\n };\n\n var _cullFactor = 1;\n var _cache = {};\n var _rules = {};\n var _dateMatchCount = 0;\n var _stats = {};\n var _keys = [];\n var _hidden = [];\n var _forceVisible = {};\n\n\n function update() {\n const hash = utilStringQs(window.location.hash);\n const disabled = features.disabled();\n if (disabled.length) {\n hash.disable_features = disabled.join(',');\n } else {\n delete hash.disable_features;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n prefs('disabled-features', disabled.join(','));\n _hidden = features.hidden();\n dispatch.call('change');\n dispatch.call('redraw');\n }\n\n\n /**\n * @callback FilterFunction\n * @param {Record} tags\n * @param {string} [geometry]\n * @returns {boolean}\n */\n\n /**\n * @param {string} k\n * @param {FilterFunction} filter\n * @param {number} [max]\n */\n function defineRule(k, filter, max) {\n var isEnabled = true;\n\n _keys.push(k);\n _rules[k] = {\n filter: filter,\n enabled: isEnabled, // whether the user wants it enabled..\n count: 0,\n currentMax: (max || Infinity),\n defaultMax: (max || Infinity),\n enable: function() { this.enabled = true; this.currentMax = this.defaultMax; },\n disable: function() { this.enabled = false; this.currentMax = 0; },\n hidden: function() {\n return (this.count === 0 && !this.enabled) ||\n this.count > this.currentMax * _cullFactor;\n },\n autoHidden: function() { return this.hidden() && this.currentMax > 0; }\n };\n }\n\n defineRule('address_points', (tags, geometry) =>\n geometry === 'point' && isAddressPoint(tags),\n 100);\n\n defineRule('points', (tags, geometry) =>\n geometry === 'point' && !isAddressPoint(tags, geometry),\n 200);\n\n defineRule('traffic_roads', function isTrafficRoad(tags) {\n return traffic_roads[tags.highway];\n });\n\n defineRule('service_roads', function isServiceRoad(tags) {\n return service_roads[tags.highway];\n });\n\n defineRule('paths', function isPath(tags) {\n return paths[tags.highway];\n });\n\n defineRule('buildings', function isBuilding(tags) {\n return (\n (!!tags.building && tags.building !== 'no') ||\n tags.parking === 'multi-storey' ||\n tags.parking === 'sheds' ||\n tags.parking === 'carports' ||\n tags.parking === 'garage_boxes'\n );\n }, 250);\n\n defineRule('building_parts', function isBuildingPart(tags) {\n return !!tags['building:part'];\n });\n\n defineRule('indoor', function isIndoor(tags) {\n return (\n (!!tags.indoor && tags.indoor !== 'no') ||\n (!!tags.indoormark && tags.indoormark !== 'no')\n );\n });\n\n defineRule('landuse', function isLanduse(tags, geometry) {\n if (geometry !== 'area') return false;\n let hasLanduseTag = false;\n for (const key in osmLanduseTags) {\n if (osmLanduseTags[key] === true && tags[key] ||\n osmLanduseTags[key][tags[key]] === true) {\n hasLanduseTag = true;\n }\n }\n return hasLanduseTag &&\n !_rules.buildings.filter(tags) &&\n !_rules.building_parts.filter(tags) &&\n !_rules.indoor.filter(tags) &&\n !_rules.water.filter(tags) &&\n !_rules.pistes.filter(tags);\n });\n\n defineRule('boundaries', function isBoundary(tags, geometry) {\n // This rule applies if the object has no interesting tags, and if either:\n // (a) is a way having a `boundary=*` tag, or\n // (b) is a relation of `type=boundary`.\n return (\n (geometry === 'line' && !!tags.boundary) ||\n (geometry === 'relation' && tags.type === 'boundary')\n ) && !(\n traffic_roads[tags.highway] ||\n service_roads[tags.highway] ||\n paths[tags.highway] ||\n tags.waterway ||\n tags.railway ||\n tags.landuse ||\n tags.natural ||\n tags.building ||\n tags.power\n );\n });\n\n defineRule('water', function isWater(tags) {\n return (\n !!tags.waterway ||\n tags.natural === 'water' ||\n tags.natural === 'coastline' ||\n tags.natural === 'bay' ||\n tags.landuse === 'pond' ||\n tags.landuse === 'basin' ||\n tags.landuse === 'reservoir' ||\n tags.landuse === 'salt_pond'\n );\n });\n\n defineRule('rail', function isRail(tags) {\n return (\n !!tags.railway ||\n tags.landuse === 'railway'\n ) && !(\n traffic_roads[tags.highway] ||\n service_roads[tags.highway] ||\n paths[tags.highway]\n );\n });\n\n defineRule('pistes', function isPiste(tags) {\n return tags['piste:type'];\n });\n\n defineRule('aerialways', function isAerialways(tags) {\n return !!tags?.aerialway &&\n tags.aerialway !== 'yes' &&\n tags.aerialway !== 'station';\n });\n\n defineRule('power', function isPower(tags) {\n return !!tags.power;\n });\n\n // contains a past/future tag, but not in active use as a road/path/cycleway/etc..\n defineRule('past_future', function isPastFuture(tags) {\n if (\n traffic_roads[tags.highway] ||\n service_roads[tags.highway] ||\n paths[tags.highway]\n ) { return false; }\n\n const keys = Object.keys(tags);\n\n for (const key of keys) {\n if (osmLifecyclePrefixes[tags[key]]) return true; // legacy tagging, e.g. `highway=construction`\n const parts = key.split(':');\n if (parts.length === 1) continue;\n const prefix = parts[0];\n if (osmLifecyclePrefixes[prefix]) return true; // lifecycle tagging, e.g. `demolished:building=yes`\n }\n return false;\n });\n\n // Lines or areas that don't match another feature filter.\n // IMPORTANT: The 'others' feature must be the last one defined,\n // so that code in getMatches can skip this test if `hasMatch = true`\n defineRule('others', function isOther(tags, geometry) {\n return (geometry === 'line' || geometry === 'area');\n });\n\n\n\n features.features = function() {\n return _rules;\n };\n\n\n features.keys = function() {\n return _keys;\n };\n\n\n features.enabled = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return _rules[k].enabled; });\n }\n return _rules[k] && _rules[k].enabled;\n };\n\n\n features.disabled = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return !_rules[k].enabled; });\n }\n return _rules[k] && !_rules[k].enabled;\n };\n\n\n features.hidden = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return _rules[k].hidden(); });\n }\n return _rules[k]?.hidden();\n };\n\n\n features.autoHidden = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return _rules[k].autoHidden(); });\n }\n return _rules[k] && _rules[k].autoHidden();\n };\n\n\n features.enable = function(k) {\n if (_rules[k] && !_rules[k].enabled) {\n _rules[k].enable();\n update();\n }\n };\n\n features.enableAll = function() {\n var didEnable = false;\n for (var k in _rules) {\n if (!_rules[k].enabled) {\n didEnable = true;\n _rules[k].enable();\n }\n }\n if (didEnable) update();\n };\n\n\n features.disable = function(k) {\n if (_rules[k] && _rules[k].enabled) {\n _rules[k].disable();\n update();\n }\n };\n\n features.disableAll = function() {\n var didDisable = false;\n for (var k in _rules) {\n if (_rules[k].enabled) {\n didDisable = true;\n _rules[k].disable();\n }\n }\n if (didDisable) update();\n };\n\n\n features.toggle = function(k) {\n if (_rules[k]) {\n (function(f) { return f.enabled ? f.disable() : f.enable(); }(_rules[k]));\n update();\n }\n };\n\n\n features.redraw = function() {\n update();\n };\n\n\n features.resetStats = function() {\n for (var i = 0; i < _keys.length; i++) {\n _rules[_keys[i]].count = 0;\n }\n _dateMatchCount = 0;\n dispatch.call('change');\n };\n\n\n features.gatherStats = function(d, resolver, dimensions) {\n var needsRedraw = false;\n var types = utilArrayGroupBy(d, 'type');\n var entities = [].concat(types.relation || [], types.way || [], types.node || []);\n var currHidden, geometry, matches, i, j;\n\n for (i = 0; i < _keys.length; i++) {\n _rules[_keys[i]].count = 0;\n }\n _dateMatchCount = 0;\n\n // adjust the threshold for point/building culling based on viewport size..\n // a _cullFactor of 1 corresponds to a 1000x1000px viewport..\n _cullFactor = dimensions[0] * dimensions[1] / 1000000;\n\n for (i = 0; i < entities.length; i++) {\n geometry = entities[i].geometry(resolver);\n matches = Object.keys(features.getMatches(entities[i], resolver, geometry));\n for (j = 0; j < matches.length; j++) {\n _rules[matches[j]].count++;\n }\n if (!features.featureFitsDateRange(entities[i])) _dateMatchCount++;\n }\n\n currHidden = features.hidden();\n if (currHidden !== _hidden) {\n _hidden = currHidden;\n needsRedraw = true;\n dispatch.call('change');\n }\n\n return needsRedraw;\n };\n\n\n features.stats = function() {\n for (var i = 0; i < _keys.length; i++) {\n _stats[_keys[i]] = _rules[_keys[i]].count;\n }\n\n return _stats;\n };\n\n\n features.dateMatchCount = () => _dateMatchCount;\n\n\n features.clear = function(d) {\n for (var i = 0; i < d.length; i++) {\n features.clearEntity(d[i]);\n }\n };\n\n\n features.clearEntity = function(entity) {\n delete _cache[osmEntity.key(entity)];\n for (const key in _cache) {\n if (_cache[key].parents) {\n for (const parent of _cache[key].parents) {\n if (parent.id === entity.id) {\n delete _cache[key];\n break;\n }\n }\n }\n }\n };\n\n\n features.reset = function() {\n Array.from(_deferred).forEach(function(handle) {\n window.cancelIdleCallback(handle);\n _deferred.delete(handle);\n });\n\n _cache = {};\n };\n\n // only certain relations are worth checking\n function relationShouldBeChecked(relation) {\n // multipolygon features have `area` geometry and aren't checked here\n return relation.tags.type === 'boundary';\n }\n\n features.getMatches = function(entity, resolver, geometry) {\n if (geometry === 'vertex' ||\n (geometry === 'relation' && !relationShouldBeChecked(entity))) return {};\n\n var ent = osmEntity.key(entity);\n if (!_cache[ent]) {\n _cache[ent] = {};\n }\n\n if (!_cache[ent].matches) {\n var matches = {};\n var hasMatch = false;\n\n for (var i = 0; i < _keys.length; i++) {\n if (_keys[i] === 'others') {\n if (hasMatch) continue;\n\n // If an entity...\n // 1. is a way that hasn't matched other 'interesting' feature rules,\n if (entity.type === 'way') {\n var parents = features.getParents(entity, resolver, geometry);\n\n // 2a. belongs only to a single multipolygon relation\n if ((parents.length === 1 && parents[0].isMultipolygon()) ||\n // 2b. or belongs only to boundary relations\n (parents.length > 0 && parents.every(function(parent) { return parent.tags.type === 'boundary'; }))) {\n\n // ...then match whatever feature rules the parent relation has matched.\n // see #2548, #2887\n //\n // IMPORTANT:\n // For this to work, getMatches must be called on relations before ways.\n //\n var pkey = osmEntity.key(parents[0]);\n if (_cache[pkey] && _cache[pkey].matches) {\n matches = Object.assign({}, _cache[pkey].matches); // shallow copy\n continue;\n }\n }\n }\n }\n\n if (_rules[_keys[i]].filter(entity.tags, geometry)) {\n matches[_keys[i]] = true;\n hasMatch = true;\n }\n }\n _cache[ent].matches = matches;\n }\n\n return _cache[ent].matches;\n };\n\n\n features.getParents = function(entity, resolver, geometry) {\n if (geometry === 'point') return [];\n\n const ent = osmEntity.key(entity);\n if (!_cache[ent]) {\n _cache[ent] = {};\n }\n\n if (!_cache[ent].parents) {\n let parents;\n if (geometry === 'vertex') {\n parents = resolver.parentWays(entity);\n } else { // 'line', 'area', 'relation'\n parents = resolver.parentRelations(entity);\n }\n _cache[ent].parents = parents;\n }\n\n return _cache[ent].parents;\n };\n\n\n features.isHiddenPreset = function(preset, geometry) {\n // if (!_hidden.length) return false;\n if (!preset.tags) return false;\n\n var test = preset.setTags({...preset.tags}, geometry);\n for (var key in _rules) {\n if (_rules[key].filter(test, geometry)) {\n if (_hidden.indexOf(key) !== -1) {\n return key;\n }\n return false;\n }\n }\n return false;\n };\n\n\n features.isHiddenFeature = function(entity, resolver, geometry) {\n // if (!_hidden.length) return false;\n if (!entity.version) return false;\n if (_forceVisible[entity.id]) return false;\n if (!features.featureFitsDateRange(entity)) return true;\n // if (!_hidden.length) return false;\n\n var matches = Object.keys(features.getMatches(entity, resolver, geometry));\n return matches.length && matches.every(function(k) { return features.hidden(k); });\n };\n\n\n features.isHiddenChild = function(entity, resolver, geometry) {\n // if (!_hidden.length) return false;\n if (!entity.version || geometry === 'point') return false;\n if (_forceVisible[entity.id]) return false;\n if (!features.featureFitsDateRange(entity)) return true;\n\n var parents = features.getParents(entity, resolver, geometry);\n if (!parents.length) return false;\n\n for (var i = 0; i < parents.length; i++) {\n if (!features.isHidden(parents[i], resolver, parents[i].geometry(resolver))) {\n return false;\n }\n }\n return true;\n };\n\n\n features.hasHiddenConnections = function(entity, resolver) {\n // if (!_hidden.length) return false;\n\n var childNodes, connections;\n if (entity.type === 'midpoint') {\n childNodes = [resolver.entity(entity.edge[0]), resolver.entity(entity.edge[1])];\n connections = [];\n } else {\n childNodes = entity.nodes ? resolver.childNodes(entity) : [];\n connections = features.getParents(entity, resolver, entity.geometry(resolver));\n }\n\n // gather ways connected to child nodes..\n connections = childNodes.reduce(function(result, e) {\n return resolver.isShared(e) ? utilArrayUnion(result, resolver.parentWays(e)) : result;\n }, connections);\n\n return connections.some(function(e) {\n return features.isHidden(e, resolver, e.geometry(resolver));\n });\n };\n\n\n features.isHidden = function(entity, resolver, geometry) {\n // if (!_hidden.length) return false;\n if (!entity.version) return false;\n\n var fn = (geometry === 'vertex' ? features.isHiddenChild : features.isHiddenFeature);\n return fn(entity, resolver, geometry);\n };\n\n\n features.featureFitsDateRange = function (entity) {\n if (!features.dateRange) return true; // no Date Range e.g. unit tests\n\n // entity's start & end date + the Date Range from the on-screen controls\n // utilDatesOverlap() treats malformed start_date/end_date as 9999/-9999\n // uiSectionDateRange already standardizes the dateRange inputs\n // so we don't need much validation here\n const entityRange = {\n 'start_date': entity.tags.start_date,\n 'end_date': entity.tags.end_date\n };\n const selectedRange = {\n 'start_date': features.dateRange[0],\n 'end_date': features.dateRange[1]\n };\n\n // out of range = feature started after range ends, or feature ends before range starts\n const withinrange = utilDatesOverlap(selectedRange, entityRange, true);\n return withinrange;\n };\n\n\n features.filter = function(d, resolver) {\n // if (!_hidden.length) return d;\n\n var result = [];\n for (var i = 0; i < d.length; i++) {\n var entity = d[i];\n if (!features.isHidden(entity, resolver, entity.geometry(resolver))) {\n result.push(entity);\n }\n }\n return result;\n };\n\n\n features.forceVisible = function(entityIDs) {\n if (!arguments.length) return Object.keys(_forceVisible);\n\n _forceVisible = {};\n for (var i = 0; i < entityIDs.length; i++) {\n _forceVisible[entityIDs[i]] = true;\n var entity = context.hasEntity(entityIDs[i]);\n if (entity && entity.type === 'relation') {\n // also show relation members (one level deep)\n for (var j in entity.members) {\n _forceVisible[entity.members[j].id] = true;\n }\n }\n }\n return features;\n };\n\n\n features.init = function() {\n var storage = prefs('disabled-features');\n if (storage) {\n var storageDisabled = storage.replace(/;/g, ',').split(',');\n storageDisabled.forEach(features.disable);\n }\n\n var hash = utilStringQs(window.location.hash);\n if (hash.disable_features) {\n var hashDisabled = hash.disable_features.replace(/;/g, ',').split(',');\n hashDisabled.forEach(features.disable);\n }\n };\n\n\n // warm up the feature matching cache upon merging fetched data\n context.history().on('merge.features', function(newEntities) {\n if (!newEntities) return;\n var handle = window.requestIdleCallback(function() {\n var graph = context.graph();\n var types = utilArrayGroupBy(newEntities, 'type');\n // ensure that getMatches is called on relations before ways\n var entities = [].concat(types.relation || [], types.way || [], types.node || []);\n for (var i = 0; i < entities.length; i++) {\n var geometry = entities[i].geometry(graph);\n features.getMatches(entities[i], graph, geometry);\n }\n });\n _deferred.add(handle);\n });\n\n\n return utilRebind(features, dispatch, 'on');\n}\n", "export function utilBindOnce(target, type, listener, capture) {\n var typeOnce = type + '.once';\n function one() {\n target.on(typeOnce, null);\n listener.apply(this, arguments);\n }\n target.on(typeOnce, one, capture);\n return this;\n}\n", "// Adapted from d3-zoom to handle pointer events.\n// https://github.com/d3/d3-zoom/blob/523ccff340187a3e3c044eaa4d4a7391ea97272b/src/zoom.js\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { interpolateZoom } from 'd3-interpolate';\nimport { select as d3_select } from 'd3-selection';\nimport { interrupt as d3_interrupt } from 'd3-transition';\nimport { zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\nimport { Transform } from '../../node_modules/d3-zoom/src/transform.js';\n\nimport { utilFastMouse, utilFunctor } from './util';\nimport { utilRebind } from './rebind';\n\n// Ignore right-click, since that should open the context menu.\nfunction defaultFilter(d3_event) {\n return !d3_event.ctrlKey && !d3_event.button;\n}\n\nfunction defaultExtent() {\n var e = this;\n if (e instanceof SVGElement) {\n e = e.ownerSVGElement || e;\n if (e.hasAttribute('viewBox')) {\n e = e.viewBox.baseVal;\n return [[e.x, e.y], [e.x + e.width, e.y + e.height]];\n }\n return [[0, 0], [e.width.baseVal.value, e.height.baseVal.value]];\n }\n return [[0, 0], [e.clientWidth, e.clientHeight]];\n}\n\nfunction defaultWheelDelta(d3_event) {\n return -d3_event.deltaY * (d3_event.deltaMode === 1 ? 0.05 : d3_event.deltaMode ? 1 : 0.002);\n}\n\nfunction defaultConstrain(transform, extent, translateExtent) {\n var dx0 = transform.invertX(extent[0][0]) - translateExtent[0][0],\n dx1 = transform.invertX(extent[1][0]) - translateExtent[1][0],\n dy0 = transform.invertY(extent[0][1]) - translateExtent[0][1],\n dy1 = transform.invertY(extent[1][1]) - translateExtent[1][1];\n return transform.translate(\n dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),\n dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)\n );\n}\n\nexport function utilZoomPan() {\n var filter = defaultFilter,\n extent = defaultExtent,\n constrain = defaultConstrain,\n wheelDelta = defaultWheelDelta,\n scaleExtent = [0, Infinity],\n translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]],\n interpolate = interpolateZoom,\n dispatch = d3_dispatch('start', 'zoom', 'end'),\n _wheelDelay = 150,\n _transform = d3_zoomIdentity,\n _activeGesture;\n\n function zoom(selection) {\n selection\n .on('pointerdown.zoom', pointerdown)\n .on('wheel.zoom', wheeled)\n .style('touch-action', 'none')\n .style('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');\n\n d3_select(window)\n .on('pointermove.zoompan', pointermove)\n .on('pointerup.zoompan pointercancel.zoompan', pointerup);\n }\n\n zoom.transform = function(collection, transform, point) {\n var selection = collection.selection ? collection.selection() : collection;\n if (collection !== selection) {\n schedule(collection, transform, point);\n } else {\n selection.interrupt().each(function() {\n gesture(this, arguments)\n .start(null)\n .zoom(null, null, typeof transform === 'function' ? transform.apply(this, arguments) : transform)\n .end(null);\n });\n }\n };\n\n zoom.scaleBy = function(selection, k, p) {\n zoom.scaleTo(selection, function() {\n var k0 = _transform.k,\n k1 = typeof k === 'function' ? k.apply(this, arguments) : k;\n return k0 * k1;\n }, p);\n };\n\n zoom.scaleTo = function(selection, k, p) {\n zoom.transform(selection, function() {\n var e = extent.apply(this, arguments),\n t0 = _transform,\n p0 = !p ? centroid(e) : typeof p === 'function' ? p.apply(this, arguments) : p,\n p1 = t0.invert(p0),\n k1 = typeof k === 'function' ? k.apply(this, arguments) : k;\n return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent);\n }, p);\n };\n\n zoom.translateBy = function(selection, x, y) {\n zoom.transform(selection, function() {\n return constrain(_transform.translate(\n typeof x === 'function' ? x.apply(this, arguments) : x,\n typeof y === 'function' ? y.apply(this, arguments) : y\n ), extent.apply(this, arguments), translateExtent);\n });\n };\n\n zoom.translateTo = function(selection, x, y, p) {\n zoom.transform(selection, function() {\n var e = extent.apply(this, arguments),\n t = _transform,\n p0 = !p ? centroid(e) : typeof p === 'function' ? p.apply(this, arguments) : p;\n return constrain(d3_zoomIdentity.translate(p0[0], p0[1]).scale(t.k).translate(\n typeof x === 'function' ? -x.apply(this, arguments) : -x,\n typeof y === 'function' ? -y.apply(this, arguments) : -y\n ), e, translateExtent);\n }, p);\n };\n\n function scale(transform, k) {\n k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], k));\n return k === transform.k ? transform : new Transform(k, transform.x, transform.y);\n }\n\n function translate(transform, p0, p1) {\n var x = p0[0] - p1[0] * transform.k, y = p0[1] - p1[1] * transform.k;\n return x === transform.x && y === transform.y ? transform : new Transform(transform.k, x, y);\n }\n\n function centroid(extent) {\n return [(+extent[0][0] + +extent[1][0]) / 2, (+extent[0][1] + +extent[1][1]) / 2];\n }\n\n function schedule(transition, transform, point) {\n transition\n .on('start.zoom', function() { gesture(this, arguments).start(null); })\n .on('interrupt.zoom end.zoom', function() { gesture(this, arguments).end(null); })\n .tween('zoom', function() {\n var that = this,\n args = arguments,\n g = gesture(that, args),\n e = extent.apply(that, args),\n p = !point ? centroid(e) : typeof point === 'function' ? point.apply(that, args) : point,\n w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]),\n a = _transform,\n b = typeof transform === 'function' ? transform.apply(that, args) : transform,\n i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k));\n return function(t) {\n if (t === 1) {\n // Avoid rounding error on end.\n t = b;\n } else {\n var l = i(t);\n var k = w / l[2];\n t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k);\n }\n g.zoom(null, null, t);\n };\n });\n }\n\n function gesture(that, args, clean) {\n return (!clean && _activeGesture) || new Gesture(that, args);\n }\n\n function Gesture(that, args) {\n this.that = that;\n this.args = args;\n this.active = 0;\n this.extent = extent.apply(that, args);\n }\n\n Gesture.prototype = {\n start: function(d3_event) {\n if (++this.active === 1) {\n _activeGesture = this;\n dispatch.call('start', this, d3_event);\n }\n return this;\n },\n zoom: function(d3_event, key, transform) {\n if (this.mouse && key !== 'mouse') this.mouse[1] = transform.invert(this.mouse[0]);\n if (this.pointer0 && key !== 'touch') this.pointer0[1] = transform.invert(this.pointer0[0]);\n if (this.pointer1 && key !== 'touch') this.pointer1[1] = transform.invert(this.pointer1[0]);\n _transform = transform;\n dispatch.call('zoom', this, d3_event, key, transform);\n return this;\n },\n end: function(d3_event) {\n if (--this.active === 0) {\n _activeGesture = null;\n dispatch.call('end', this, d3_event);\n }\n return this;\n }\n };\n\n function wheeled(d3_event) {\n if (!filter.apply(this, arguments)) return;\n var g = gesture(this, arguments),\n t = _transform,\n k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k * Math.pow(2, wheelDelta.apply(this, arguments)))),\n p = utilFastMouse(this)(d3_event);\n\n // If the mouse is in the same location as before, reuse it.\n // If there were recent wheel events, reset the wheel idle timeout.\n if (g.wheel) {\n if (g.mouse[0][0] !== p[0] || g.mouse[0][1] !== p[1]) {\n g.mouse[1] = t.invert(g.mouse[0] = p);\n }\n clearTimeout(g.wheel);\n\n // Otherwise, capture the mouse point and location at the start.\n } else {\n g.mouse = [p, t.invert(p)];\n d3_interrupt(this);\n g.start(d3_event);\n }\n\n d3_event.preventDefault();\n d3_event.stopImmediatePropagation();\n g.wheel = setTimeout(wheelidled, _wheelDelay);\n g.zoom(d3_event, 'mouse', constrain(translate(scale(t, k), g.mouse[0], g.mouse[1]), g.extent, translateExtent));\n\n function wheelidled() {\n g.wheel = null;\n g.end(d3_event);\n }\n }\n\n var _downPointerIDs = new Set();\n var _pointerLocGetter;\n\n function pointerdown(d3_event) {\n _downPointerIDs.add(d3_event.pointerId);\n\n if (!filter.apply(this, arguments)) return;\n\n var g = gesture(this, arguments, _downPointerIDs.size === 1);\n var started;\n\n d3_event.stopImmediatePropagation();\n _pointerLocGetter = utilFastMouse(this);\n var loc = _pointerLocGetter(d3_event);\n var p = [loc, _transform.invert(loc), d3_event.pointerId];\n if (!g.pointer0) {\n g.pointer0 = p;\n started = true;\n\n } else if (!g.pointer1 && g.pointer0[2] !== p[2]) {\n g.pointer1 = p;\n }\n\n if (started) {\n d3_interrupt(this);\n g.start(d3_event);\n }\n }\n\n function pointermove(d3_event) {\n if (!_downPointerIDs.has(d3_event.pointerId)) return;\n\n if (!_activeGesture || !_pointerLocGetter) return;\n\n var g = gesture(this, arguments);\n\n var isPointer0 = g.pointer0 && g.pointer0[2] === d3_event.pointerId;\n var isPointer1 = !isPointer0 && g.pointer1 && g.pointer1[2] === d3_event.pointerId;\n\n if ((isPointer0 || isPointer1) && 'buttons' in d3_event && !d3_event.buttons) {\n // The pointer went up without ending the gesture somehow, e.g.\n // a down mouse was moved off the map and released. End it here.\n if (g.pointer0) _downPointerIDs.delete(g.pointer0[2]);\n if (g.pointer1) _downPointerIDs.delete(g.pointer1[2]);\n g.end(d3_event);\n return;\n }\n\n d3_event.preventDefault();\n d3_event.stopImmediatePropagation();\n\n var loc = _pointerLocGetter(d3_event);\n var t, p, l;\n\n if (isPointer0) g.pointer0[0] = loc;\n else if (isPointer1) g.pointer1[0] = loc;\n\n t = _transform;\n if (g.pointer1) {\n var p0 = g.pointer0[0], l0 = g.pointer0[1],\n p1 = g.pointer1[0], l1 = g.pointer1[1],\n dp = (dp = p1[0] - p0[0]) * dp + (dp = p1[1] - p0[1]) * dp,\n dl = (dl = l1[0] - l0[0]) * dl + (dl = l1[1] - l0[1]) * dl;\n t = scale(t, Math.sqrt(dp / dl));\n p = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];\n l = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2];\n } else if (g.pointer0) {\n p = g.pointer0[0];\n l = g.pointer0[1];\n } else {\n return;\n }\n g.zoom(d3_event, 'touch', constrain(translate(t, p, l), g.extent, translateExtent));\n }\n\n function pointerup(d3_event) {\n if (!_downPointerIDs.has(d3_event.pointerId)) return;\n\n _downPointerIDs.delete(d3_event.pointerId);\n\n if (!_activeGesture) return;\n\n var g = gesture(this, arguments);\n\n d3_event.stopImmediatePropagation();\n\n if (g.pointer0 && g.pointer0[2] === d3_event.pointerId) delete g.pointer0;\n else if (g.pointer1 && g.pointer1[2] === d3_event.pointerId) delete g.pointer1;\n\n if (g.pointer1 && !g.pointer0) {\n g.pointer0 = g.pointer1;\n delete g.pointer1;\n }\n if (g.pointer0) {\n g.pointer0[1] = _transform.invert(g.pointer0[0]);\n } else {\n g.end(d3_event);\n }\n }\n\n zoom.wheelDelta = function(_) {\n return arguments.length ? (wheelDelta = utilFunctor(+_), zoom) : wheelDelta;\n };\n\n zoom.filter = function(_) {\n return arguments.length ? (filter = utilFunctor(!!_), zoom) : filter;\n };\n\n zoom.extent = function(_) {\n return arguments.length ? (extent = utilFunctor([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;\n };\n\n zoom.scaleExtent = function(_) {\n return arguments.length ? (scaleExtent[0] = +_[0], scaleExtent[1] = +_[1], zoom) : [scaleExtent[0], scaleExtent[1]];\n };\n\n zoom.translateExtent = function(_) {\n return arguments.length ? (translateExtent[0][0] = +_[0][0], translateExtent[1][0] = +_[1][0], translateExtent[0][1] = +_[0][1], translateExtent[1][1] = +_[1][1], zoom) : [[translateExtent[0][0], translateExtent[0][1]], [translateExtent[1][0], translateExtent[1][1]]];\n };\n\n zoom.constrain = function(_) {\n return arguments.length ? (constrain = _, zoom) : constrain;\n };\n\n zoom.interpolate = function(_) {\n return arguments.length ? (interpolate = _, zoom) : interpolate;\n };\n\n zoom._transform = function(_) {\n return arguments.length ? (_transform = _, zoom) : _transform;\n };\n\n return utilRebind(zoom, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { utilFastMouse } from './util';\nimport { utilRebind } from './rebind';\nimport { geoVecLength } from '../geo/vector';\n\n// A custom double-click / double-tap event detector that works on touch devices\n// if pointer events are supported. Falls back to default `dblclick` event.\nexport function utilDoubleUp() {\n\n var dispatch = d3_dispatch('doubleUp');\n\n var _maxTimespan = 500; // milliseconds\n var _maxDistance = 20; // web pixels; be somewhat generous to account for touch devices\n var _pointer; // object representing the pointer that could trigger double up\n\n function pointerIsValidFor(loc) {\n // second pointerup must occur within a small timeframe after the first pointerdown\n return new Date().getTime() - _pointer.startTime <= _maxTimespan &&\n // all pointer events must occur within a small distance of the first pointerdown\n geoVecLength(_pointer.startLoc, loc) <= _maxDistance;\n }\n\n function pointerdown(d3_event) {\n\n // ignore right-click\n if (d3_event.ctrlKey || d3_event.button === 2) return;\n\n var loc = [d3_event.clientX, d3_event.clientY];\n\n // Don't rely on pointerId here since it can change between pointerdown\n // events on touch devices\n if (_pointer && !pointerIsValidFor(loc)) {\n // if this pointer is no longer valid, clear it so another can be started\n _pointer = undefined;\n }\n\n if (!_pointer) {\n _pointer = {\n startLoc: loc,\n startTime: new Date().getTime(),\n upCount: 0,\n pointerId: d3_event.pointerId\n };\n } else { // double down\n _pointer.pointerId = d3_event.pointerId;\n }\n }\n\n function pointerup(d3_event) {\n\n // ignore right-click\n if (d3_event.ctrlKey || d3_event.button === 2) return;\n\n if (!_pointer || _pointer.pointerId !== d3_event.pointerId) return;\n\n _pointer.upCount += 1;\n\n if (_pointer.upCount === 2) { // double up!\n var loc = [d3_event.clientX, d3_event.clientY];\n if (pointerIsValidFor(loc)) {\n var locInThis = utilFastMouse(this)(d3_event);\n dispatch.call('doubleUp', this, d3_event, locInThis);\n }\n // clear the pointer info in any case\n _pointer = undefined;\n }\n }\n\n function doubleUp(selection) {\n if ('PointerEvent' in window) {\n // dblclick isn't well supported on touch devices so manually use\n // pointer events if they're available\n selection\n .on('pointerdown.doubleUp', pointerdown)\n .on('pointerup.doubleUp', pointerup);\n } else {\n // fallback to dblclick\n selection\n .on('dblclick.doubleUp', function(d3_event) {\n dispatch.call('doubleUp', this, d3_event, utilFastMouse(this)(d3_event));\n });\n }\n }\n\n doubleUp.off = function(selection) {\n selection\n .on('pointerdown.doubleUp', null)\n .on('pointerup.doubleUp', null)\n .on('dblclick.doubleUp', null);\n };\n\n return utilRebind(doubleUp, dispatch, 'on');\n}\n", "import { throttle, isArray, clamp } from 'es-toolkit/compat';\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { interpolate as d3_interpolate } from 'd3-interpolate';\nimport { scaleLinear as d3_scaleLinear } from 'd3-scale';\nimport { select as d3_select } from 'd3-selection';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport { prefs } from '../core/preferences';\nimport { geoExtent, geoRawMercator, geoScaleToZoom, geoZoomToScale } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { svgAreas, svgLabels, svgLayers, svgLines, svgMidpoints, svgPoints, svgVertices } from '../svg';\nimport { utilFastMouse, utilFunctor, utilSetTransform, utilEntityAndDeepMemberIDs } from '../util/util';\nimport { utilBindOnce } from '../util/bind_once';\nimport { utilDetect } from '../util/detect';\nimport { utilGetDimensions } from '../util/dimensions';\nimport { utilRebind } from '../util/rebind';\nimport { utilZoomPan } from '../util/zoom_pan';\nimport { utilDoubleUp } from '../util/double_up';\n\n// constants\nvar TILESIZE = 256;\nvar minZoom = 2;\nvar maxZoom = 24;\nvar kMin = geoZoomToScale(minZoom, TILESIZE);\nvar kMax = geoZoomToScale(maxZoom, TILESIZE);\n\n\nexport function rendererMap(context) {\n var dispatch = d3_dispatch(\n 'move', 'drawn',\n 'crossEditableZoom', 'hitMinZoom',\n 'changeHighlighting', 'changeAreaFill'\n );\n var projection = context.projection;\n var curtainProjection = context.curtainProjection;\n var drawLayers;\n var drawPoints;\n var drawVertices;\n var drawLines;\n var drawAreas;\n var drawMidpoints;\n var drawLabels;\n\n var _selection = d3_select(null);\n var supersurface = d3_select(null);\n var wrapper = d3_select(null);\n var surface = d3_select(null);\n\n var _dimensions = [1, 1];\n var _dblClickZoomEnabled = true;\n var _redrawEnabled = true;\n var _gestureTransformStart;\n var _transformStart = projection.transform();\n var _transformLast;\n var _isTransformed = false;\n var _minzoom = 0;\n var _getMouseCoords;\n var _lastPointerEvent;\n var _lastWithinEditableZoom;\n\n // whether a pointerdown event started the zoom\n var _pointerDown = false;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n // use pointer event interaction if supported; fallback to touch/mouse events in d3-zoom\n var _zoomerPannerFunction = 'PointerEvent' in window ? utilZoomPan : d3_zoom;\n\n var _zoomerPanner = _zoomerPannerFunction()\n .scaleExtent([kMin, kMax])\n .interpolate(d3_interpolate)\n .filter(zoomEventFilter)\n .on('zoom.map', zoomPan)\n .on('start.map', function(d3_event) {\n _pointerDown = d3_event && (d3_event.type === 'pointerdown' ||\n (d3_event.sourceEvent && d3_event.sourceEvent.type === 'pointerdown'));\n })\n .on('end.map', function() {\n _pointerDown = false;\n });\n var _doubleUpHandler = utilDoubleUp();\n\n var scheduleRedraw = throttle(redraw, 750);\n // var isRedrawScheduled = false;\n // var pendingRedrawCall;\n // function scheduleRedraw() {\n // // Only schedule the redraw if one has not already been set.\n // if (isRedrawScheduled) return;\n // isRedrawScheduled = true;\n // var that = this;\n // var args = arguments;\n // pendingRedrawCall = window.requestIdleCallback(function () {\n // // Reset the boolean so future redraws can be set.\n // isRedrawScheduled = false;\n // redraw.apply(that, args);\n // }, { timeout: 1400 });\n // }\n\n function cancelPendingRedraw() {\n scheduleRedraw.cancel();\n // isRedrawScheduled = false;\n // window.cancelIdleCallback(pendingRedrawCall);\n }\n\n\n function map(selection) {\n _selection = selection;\n\n context\n .on('change.map', immediateRedraw);\n\n var osm = context.connection();\n if (osm) {\n osm.on('change.map', immediateRedraw);\n }\n\n function didUndoOrRedo(targetTransform) {\n var mode = context.mode().id;\n if (mode !== 'browse' && mode !== 'select') return;\n if (targetTransform) {\n map.transformEase(targetTransform);\n }\n }\n\n context.history()\n .on('merge.map', function() { scheduleRedraw(); })\n .on('change.map', immediateRedraw)\n .on('undone.map', function(stack, fromStack) {\n didUndoOrRedo(fromStack.transform);\n })\n .on('redone.map', function(stack) {\n didUndoOrRedo(stack.transform);\n });\n\n context.background()\n .on('change.map', immediateRedraw);\n\n context.features()\n .on('redraw.map', immediateRedraw);\n\n drawLayers\n .on('change.map', function() {\n context.background().updateImagery();\n immediateRedraw();\n });\n\n selection\n .on('wheel.map mousewheel.map', function(d3_event) {\n // disable swipe-to-navigate browser pages on trackpad/magic mouse \u2013 #5552\n d3_event.preventDefault();\n })\n .call(_zoomerPanner)\n .call(_zoomerPanner.transform, projection.transform())\n .on('dblclick.zoom', null); // override d3-zoom dblclick handling\n\n map.supersurface = selection.append('div')\n .attr('class', 'supersurface')\n .call(utilSetTransform, 0, 0);\n supersurface = map.supersurface;\n\n // Need a wrapper div because Opera can't cope with an absolutely positioned\n // SVG element: http://bl.ocks.org/jfirebaugh/6fbfbd922552bf776c16\n wrapper = supersurface\n .append('div')\n .attr('class', 'layer layer-data');\n\n map.surface = wrapper\n .call(drawLayers)\n .selectAll('.surface');\n surface = map.surface;\n\n surface\n .call(drawLabels.observe)\n .call(_doubleUpHandler)\n .on(_pointerPrefix + 'down.zoom', function(d3_event) {\n _lastPointerEvent = d3_event;\n if (d3_event.button === 2) {\n d3_event.stopPropagation();\n }\n }, true)\n .on(_pointerPrefix + 'up.zoom', function(d3_event) {\n _lastPointerEvent = d3_event;\n if (resetTransform()) {\n immediateRedraw();\n }\n })\n .on(_pointerPrefix + 'move.map', function(d3_event) {\n _lastPointerEvent = d3_event;\n })\n .on(_pointerPrefix + 'over.vertices', function(d3_event) {\n if (map.editableDataEnabled() && !_isTransformed) {\n var hover = d3_event.target.__data__;\n surface.call(drawVertices.drawHover, context.graph(), hover, map.extent());\n dispatch.call('drawn', this, { full: false });\n }\n })\n .on(_pointerPrefix + 'out.vertices', function(d3_event) {\n if (map.editableDataEnabled() && !_isTransformed) {\n var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__;\n surface.call(drawVertices.drawHover, context.graph(), hover, map.extent());\n dispatch.call('drawn', this, { full: false });\n }\n });\n\n var detected = utilDetect();\n\n // only WebKit supports gesture events\n if ('GestureEvent' in window &&\n // Listening for gesture events on iOS 13.4+ breaks double-tapping,\n // but we only need to do this on desktop Safari anyway. \u2013 #7694\n !detected.isMobileWebKit) {\n\n // Desktop Safari sends gesture events for multitouch trackpad pinches.\n // We can listen for these and translate them into map zooms.\n surface\n .on('gesturestart.surface', function(d3_event) {\n d3_event.preventDefault();\n _gestureTransformStart = projection.transform();\n })\n .on('gesturechange.surface', gestureChange);\n }\n\n // must call after surface init\n updateAreaFill();\n\n _doubleUpHandler.on('doubleUp.map', function(d3_event, p0) {\n if (!_dblClickZoomEnabled) return;\n\n // don't zoom if targeting something other than the map itself\n if (typeof d3_event.target.__data__ === 'object' &&\n // or area fills\n !d3_select(d3_event.target).classed('fill')) return;\n\n var zoomOut = d3_event.shiftKey;\n\n var t = projection.transform();\n\n var p1 = t.invert(p0);\n\n t = t.scale(zoomOut ? 0.5 : 2);\n\n t.x = p0[0] - p1[0] * t.k;\n t.y = p0[1] - p1[1] * t.k;\n\n map.transformEase(t);\n });\n\n context.on('enter.map', function() {\n if (!map.editableDataEnabled(true /* skip zoom check */)) return;\n if (_isTransformed) return;\n\n // redraw immediately any objects affected by a change in selectedIDs.\n var graph = context.graph();\n var selectedAndParents = {};\n context.selectedIDs().forEach(function(id) {\n var entity = graph.hasEntity(id);\n if (entity) {\n selectedAndParents[entity.id] = entity;\n if (entity.type === 'node') {\n graph.parentWays(entity).forEach(function(parent) {\n selectedAndParents[parent.id] = parent;\n });\n }\n }\n });\n var data = Object.values(selectedAndParents);\n var filter = function(d) { return d.id in selectedAndParents; };\n\n data = context.features().filter(data, graph);\n\n surface\n .call(drawVertices.drawSelected, graph, map.extent())\n .call(drawLines, graph, data, filter)\n .call(drawAreas, graph, data, filter)\n .call(drawMidpoints, graph, data, filter, map.trimmedExtent());\n\n dispatch.call('drawn', this, { full: false });\n\n // redraw everything else later\n scheduleRedraw();\n });\n\n map.dimensions(utilGetDimensions(selection));\n }\n\n\n function zoomEventFilter(d3_event) {\n // Fix for #2151, (see also d3/d3-zoom#60, d3/d3-brush#18)\n // Intercept `mousedown` and check if there is an orphaned zoom gesture.\n // This can happen if a previous `mousedown` occurred without a `mouseup`.\n // If we detect this, dispatch `mouseup` to complete the orphaned gesture,\n // so that d3-zoom won't stop propagation of new `mousedown` events.\n if (d3_event.type === 'mousedown') {\n var hasOrphan = false;\n var listeners = window.__on;\n for (var i = 0; i < listeners.length; i++) {\n var listener = listeners[i];\n if (listener.name === 'zoom' && listener.type === 'mouseup') {\n hasOrphan = true;\n break;\n }\n }\n if (hasOrphan) {\n const event = new Event('mouseup');\n // Event needs to be dispatched with an event.view property.\n event.view = window;\n window.dispatchEvent(event);\n }\n }\n\n return d3_event.button !== 2; // ignore right clicks\n }\n\n\n function pxCenter() {\n return [_dimensions[0] / 2, _dimensions[1] / 2];\n }\n\n\n function drawEditable(difference, extent) {\n var mode = context.mode();\n var graph = context.graph();\n var features = context.features();\n var all = context.history().intersects(map.extent());\n var fullRedraw = false;\n var data;\n var set;\n var filter;\n var applyFeatureLayerFilters = true;\n\n if (map.isInWideSelection()) {\n data = [];\n utilEntityAndDeepMemberIDs(mode.selectedIDs(), context.graph()).forEach(function(id) {\n var entity = context.hasEntity(id);\n if (entity) data.push(entity);\n });\n fullRedraw = true;\n filter = utilFunctor(true);\n // selected features should always be visible, so we can skip filtering\n applyFeatureLayerFilters = false;\n\n } else if (difference) {\n var complete = difference.complete(map.extent());\n data = Object.values(complete).filter(Boolean);\n set = new Set(Object.keys(complete));\n filter = function(d) { return set.has(d.id); };\n features.clear(data);\n\n } else {\n // force a full redraw if gatherStats detects that a feature\n // should be auto-hidden (e.g. points or buildings)..\n if (features.gatherStats(all, graph, _dimensions)) {\n extent = undefined;\n }\n\n if (extent) {\n data = context.history().intersects(map.extent().intersection(extent));\n set = new Set(data.map(function(entity) { return entity.id; }));\n filter = function(d) { return set.has(d.id); };\n\n } else {\n data = all;\n fullRedraw = true;\n filter = utilFunctor(true);\n }\n }\n\n if (applyFeatureLayerFilters) {\n data = features.filter(data, graph);\n } else {\n context.features().resetStats();\n }\n\n if (mode && mode.id === 'select') {\n // update selected vertices - the user might have just double-clicked a way,\n // creating a new vertex, triggering a partial redraw without a mode change\n surface.call(drawVertices.drawSelected, graph, map.extent());\n }\n\n surface\n .call(drawVertices, graph, data, filter, map.extent(), fullRedraw)\n .call(drawLines, graph, data, filter)\n .call(drawAreas, graph, data, filter)\n .call(drawMidpoints, graph, data, filter, map.trimmedExtent())\n .call(drawPoints, graph, data, filter)\n .call(drawLabels, graph, data, filter, _dimensions, fullRedraw);\n\n dispatch.call('drawn', this, {full: true});\n }\n\n map.init = function() {\n drawLayers = svgLayers(projection, context);\n drawPoints = svgPoints(projection, context);\n drawVertices = svgVertices(projection, context);\n drawLines = svgLines(projection, context);\n drawAreas = svgAreas(projection, context);\n drawMidpoints = svgMidpoints(projection, context);\n drawLabels = svgLabels(projection, context);\n };\n\n function editOff() {\n context.features().resetStats();\n surface.selectAll('.layer-osm *').remove();\n surface.selectAll('.layer-touch:not(.markers) *').remove();\n\n var allowed = {\n 'browse': true,\n 'save': true,\n 'select-note': true,\n 'select-data': true,\n 'select-error': true\n };\n\n var mode = context.mode();\n if (mode && !allowed[mode.id]) {\n context.enter(modeBrowse(context));\n }\n\n dispatch.call('drawn', this, {full: true});\n }\n\n\n\n\n\n function gestureChange(d3_event) {\n // Remap Safari gesture events to wheel events - #5492\n // We want these disabled most places, but enabled for zoom/unzoom on map surface\n // https://developer.mozilla.org/en-US/docs/Web/API/GestureEvent\n var e = d3_event;\n e.preventDefault();\n\n var props = {\n deltaMode: 0, // dummy values to ignore in zoomPan\n deltaY: 1, // dummy values to ignore in zoomPan\n clientX: e.clientX,\n clientY: e.clientY,\n screenX: e.screenX,\n screenY: e.screenY,\n x: e.x,\n y: e.y\n };\n\n var e2 = new WheelEvent('wheel', props);\n e2._scale = e.scale; // preserve the original scale\n e2._rotation = e.rotation; // preserve the original rotation\n\n _selection.node().dispatchEvent(e2);\n }\n\n\n function zoomPan(event, key, transform) {\n var source = event && event.sourceEvent || event;\n var eventTransform = transform || (event && event.transform);\n var x = eventTransform.x;\n var y = eventTransform.y;\n var k = eventTransform.k;\n\n // Special handling of 'wheel' events:\n // They might be triggered by the user scrolling the mouse wheel,\n // or 2-finger pinch/zoom gestures, the transform may need adjustment.\n if (source && source.type === 'wheel') {\n\n // assume that the gesture is already handled by pointer events\n if (_pointerDown) return;\n\n var detected = utilDetect();\n var dX = source.deltaX;\n var dY = source.deltaY;\n var x2 = x;\n var y2 = y;\n var k2 = k;\n var t0, p0, p1;\n\n // Normalize mousewheel scroll speed (Firefox) - #3029\n // If wheel delta is provided in LINE units, recalculate it in PIXEL units\n // We are essentially redoing the calculations that occur here:\n // https://github.com/d3/d3-zoom/blob/78563a8348aa4133b07cac92e2595c2227ca7cd7/src/zoom.js#L203\n // See this for more info:\n // https://github.com/basilfx/normalize-wheel/blob/master/src/normalizeWheel.js\n if (source.deltaMode === 1 /* LINE */) {\n // Convert from lines to pixels, more if the user is scrolling fast.\n // (I made up the exp function to roughly match Firefox to what Chrome does)\n // These numbers should be floats, because integers are treated as pan gesture below.\n var lines = Math.abs(source.deltaY);\n var sign = (source.deltaY > 0) ? 1 : -1;\n dY = sign * clamp(\n lines * 18.001,\n 4.000244140625, // min\n 350.000244140625 // max\n );\n\n // recalculate x2,y2,k2\n t0 = _isTransformed ? _transformLast : _transformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * Math.pow(2, -dY / 500);\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // 2 finger map pinch zooming (Safari) - #5492\n // These are fake `wheel` events we made from Safari `gesturechange` events..\n } else if (source._scale) {\n // recalculate x2,y2,k2\n t0 = _gestureTransformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * source._scale;\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // 2 finger map pinch zooming (all browsers except Safari) - #5492\n // Pinch zooming via the `wheel` event will always have:\n // - `ctrlKey = true`\n // - `deltaY` is not round integer pixels (ignore `deltaX`)\n } else if (source.ctrlKey && !isInteger(dY)) {\n dY *= 6; // slightly scale up whatever the browser gave us\n\n // recalculate x2,y2,k2\n t0 = _isTransformed ? _transformLast : _transformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * Math.pow(2, -dY / 500);\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // Trackpad scroll zooming with shift or alt/option key down\n } else if ((source.altKey || source.shiftKey) && isInteger(dY)) {\n // recalculate x2,y2,k2\n t0 = _isTransformed ? _transformLast : _transformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * Math.pow(2, -dY / 500);\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // 2 finger map panning (Mac only, all browsers except Firefox #8595) - #5492, #5512\n // Panning via the `wheel` event will always have:\n // - `ctrlKey = false`\n // - `deltaX`,`deltaY` are round integer pixels\n } else if (detected.os === 'mac' && detected.browser !== 'Firefox' && !source.ctrlKey && isInteger(dX) && isInteger(dY)) {\n p1 = projection.translate();\n x2 = p1[0] - dX;\n y2 = p1[1] - dY;\n k2 = projection.scale();\n k2 = clamp(k2, kMin, kMax);\n }\n\n // something changed - replace the event transform\n if (x2 !== x || y2 !== y || k2 !== k) {\n x = x2;\n y = y2;\n k = k2;\n eventTransform = d3_zoomIdentity.translate(x2, y2).scale(k2);\n if (_zoomerPanner._transform) {\n // utilZoomPan interface\n _zoomerPanner._transform(eventTransform);\n } else {\n // d3_zoom interface\n _selection.node().__zoom = eventTransform;\n }\n }\n\n }\n\n if (_transformStart.x === x &&\n _transformStart.y === y &&\n _transformStart.k === k) {\n return; // no change\n }\n\n if (geoScaleToZoom(k, TILESIZE) < _minzoom) {\n surface.interrupt();\n dispatch.call('hitMinZoom', this, map);\n setCenterZoom(map.center(), context.minEditableZoom(), 0, true);\n scheduleRedraw();\n dispatch.call('move', this, map);\n return;\n }\n\n projection.transform(eventTransform);\n\n var withinEditableZoom = map.withinEditableZoom();\n if (_lastWithinEditableZoom !== withinEditableZoom) {\n if (_lastWithinEditableZoom !== undefined) {\n // notify that the map zoomed in or out over the editable zoom threshold\n dispatch.call('crossEditableZoom', this, withinEditableZoom);\n }\n _lastWithinEditableZoom = withinEditableZoom;\n }\n\n var scale = k / _transformStart.k;\n var tX = (x / scale - _transformStart.x) * scale;\n var tY = (y / scale - _transformStart.y) * scale;\n\n if (context.inIntro()) {\n curtainProjection.transform({\n x: x - tX,\n y: y - tY,\n k: k\n });\n }\n\n if (source) {\n _lastPointerEvent = event;\n }\n _isTransformed = true;\n _transformLast = eventTransform;\n utilSetTransform(supersurface, tX, tY, scale);\n scheduleRedraw();\n\n dispatch.call('move', this, map);\n\n\n function isInteger(val) {\n return typeof val === 'number' && isFinite(val) && Math.floor(val) === val;\n }\n }\n\n\n function resetTransform() {\n if (!_isTransformed) return false;\n\n utilSetTransform(supersurface, 0, 0);\n _isTransformed = false;\n if (context.inIntro()) {\n curtainProjection.transform(projection.transform());\n }\n return true;\n }\n\n\n function redraw(difference, extent) {\n // in unit tests, we need to abort if the test has already completed\n if (typeof window === 'undefined') return;\n\n if (surface.empty() || !_redrawEnabled) return;\n\n // If we are in the middle of a zoom/pan, we can't do differenced redraws.\n // It would result in artifacts where differenced entities are redrawn with\n // one transform and unchanged entities with another.\n if (resetTransform()) {\n difference = undefined;\n extent = undefined;\n }\n\n var zoom = map.zoom();\n var z = String(~~zoom);\n\n if (surface.attr('data-zoom') !== z) {\n surface.attr('data-zoom', z);\n }\n\n // class surface as `lowzoom` around z17-z18.5 (based on latitude)\n var lat = map.center()[1];\n var lowzoom = d3_scaleLinear()\n .domain([-60, 0, 60])\n .range([17, 18.5, 17])\n .clamp(true);\n\n surface\n .classed('low-zoom', zoom <= lowzoom(lat));\n\n\n if (!difference) {\n supersurface.call(context.background());\n wrapper.call(drawLayers);\n }\n\n // OSM\n if (map.editableDataEnabled() || map.isInWideSelection()) {\n context.loadTiles(projection);\n drawEditable(difference, extent);\n } else {\n editOff();\n }\n\n _transformStart = projection.transform();\n\n return map;\n }\n\n\n\n var immediateRedraw = function(difference, extent) {\n if (!difference && !extent) cancelPendingRedraw();\n redraw(difference, extent);\n };\n\n\n map.lastPointerEvent = function() {\n return _lastPointerEvent;\n };\n\n\n map.mouse = function(d3_event) {\n var event = d3_event || _lastPointerEvent;\n if (event) {\n var s;\n while ((s = event.sourceEvent)) { event = s; }\n return _getMouseCoords(event);\n }\n return null;\n };\n\n\n // returns Lng/Lat\n map.mouseCoordinates = function() {\n var coord = map.mouse() || pxCenter();\n return projection.invert(coord);\n };\n\n\n map.dblclickZoomEnable = function(val) {\n if (!arguments.length) return _dblClickZoomEnabled;\n _dblClickZoomEnabled = val;\n return map;\n };\n\n\n map.redrawEnable = function(val) {\n if (!arguments.length) return _redrawEnabled;\n _redrawEnabled = val;\n return map;\n };\n\n\n map.isTransformed = function() {\n return _isTransformed;\n };\n\n\n function setTransform(t2, duration, force) {\n var t = projection.transform();\n if (!force && t2.k === t.k && t2.x === t.x && t2.y === t.y) return false;\n\n if (duration) {\n _selection\n .transition()\n .duration(duration)\n .on('start', function() { map.startEase(); })\n .call(_zoomerPanner.transform, d3_zoomIdentity.translate(t2.x, t2.y).scale(t2.k));\n } else {\n projection.transform(t2);\n _transformStart = t2;\n _selection.call(_zoomerPanner.transform, _transformStart);\n }\n\n return true;\n }\n\n\n function setCenterZoom(loc2, z2, duration, force) {\n var c = map.center();\n var z = map.zoom();\n if (loc2[0] === c[0] && loc2[1] === c[1] && z2 === z && !force) return false;\n\n var proj = geoRawMercator().transform(projection.transform()); // copy projection\n\n var k2 = clamp(geoZoomToScale(z2, TILESIZE), kMin, kMax);\n proj.scale(k2);\n\n var t = proj.translate();\n var point = proj(loc2);\n\n var center = pxCenter();\n t[0] += center[0] - point[0];\n t[1] += center[1] - point[1];\n\n return setTransform(d3_zoomIdentity.translate(t[0], t[1]).scale(k2), duration, force);\n }\n\n\n map.pan = function(delta, duration) {\n var t = projection.translate();\n var k = projection.scale();\n\n t[0] += delta[0];\n t[1] += delta[1];\n\n if (duration) {\n _selection\n .transition()\n .duration(duration)\n .on('start', function() { map.startEase(); })\n .call(_zoomerPanner.transform, d3_zoomIdentity.translate(t[0], t[1]).scale(k));\n } else {\n projection.translate(t);\n _transformStart = projection.transform();\n _selection.call(_zoomerPanner.transform, _transformStart);\n dispatch.call('move', this, map);\n immediateRedraw();\n }\n\n return map;\n };\n\n\n map.dimensions = function(val) {\n if (!arguments.length) return _dimensions;\n\n _dimensions = val;\n drawLayers.dimensions(_dimensions);\n context.background().dimensions(_dimensions);\n projection.clipExtent([[0, 0], _dimensions]);\n _getMouseCoords = utilFastMouse(supersurface.node());\n\n scheduleRedraw();\n return map;\n };\n\n\n function zoomIn(delta) {\n setCenterZoom(map.center(), Math.trunc(map.zoom() + 0.45) + delta, 150, true);\n }\n\n function zoomOut(delta) {\n setCenterZoom(map.center(), Math.ceil(map.zoom() - 0.45) - delta, 150, true);\n }\n\n map.zoomIn = function() { zoomIn(1); };\n map.zoomInFurther = function() { zoomIn(4); };\n map.canZoomIn = function() { return map.zoom() < maxZoom; };\n\n map.zoomOut = function() { zoomOut(1); };\n map.zoomOutFurther = function() { zoomOut(4); };\n map.canZoomOut = function() { return map.zoom() > minZoom; };\n\n map.center = function(loc2) {\n if (!arguments.length) {\n return projection.invert(pxCenter());\n }\n\n if (setCenterZoom(loc2, map.zoom())) {\n dispatch.call('move', this, map);\n }\n\n scheduleRedraw();\n return map;\n };\n\n function trimmedCenter(loc, zoom) {\n var offset = [paneWidth() / 2, (footerHeight() - toolbarHeight()) / 2];\n\n var proj = geoRawMercator().transform(projection.transform()); // copy projection\n // use the target zoom to calculate the offset center\n proj.scale(geoZoomToScale(zoom, TILESIZE));\n\n var locPx = proj(loc);\n var offsetLocPx = [locPx[0] + offset[0], locPx[1] + offset[1]];\n var offsetLoc = proj.invert(offsetLocPx);\n\n return offsetLoc;\n };\n\n function paneWidth() {\n const openPane = context.container().select('.map-panes .map-pane.shown');\n if (!openPane.empty()) {\n return openPane.node().offsetWidth;\n }\n return 0;\n };\n\n function toolbarHeight() {\n const toolbar = context.container().select('.top-toolbar');\n return toolbar.node().offsetHeight;\n };\n\n function footerHeight() {\n const footer = context.container().select('.map-footer-bar');\n return footer.node().offsetHeight;\n }\n\n map.zoom = function(z2) {\n if (!arguments.length) {\n return Math.max(geoScaleToZoom(projection.scale(), TILESIZE), 0);\n }\n\n if (z2 < _minzoom) {\n surface.interrupt();\n dispatch.call('hitMinZoom', this, map);\n z2 = context.minEditableZoom();\n }\n\n if (setCenterZoom(map.center(), z2)) {\n dispatch.call('move', this, map);\n }\n\n scheduleRedraw();\n return map;\n };\n\n\n map.centerZoom = function(loc2, z2) {\n if (setCenterZoom(loc2, z2)) {\n dispatch.call('move', this, map);\n }\n\n scheduleRedraw();\n return map;\n };\n\n\n map.zoomTo = function(what) {\n return map.zoomToEase(what, 0);\n };\n\n\n map.centerEase = function(loc2, duration) {\n duration = duration || 250;\n setCenterZoom(loc2, map.zoom(), duration);\n return map;\n };\n\n\n map.zoomEase = function(z2, duration) {\n duration = duration || 250;\n setCenterZoom(map.center(), z2, duration, false);\n return map;\n };\n\n\n map.centerZoomEase = function(loc2, z2, duration) {\n duration = duration || 250;\n setCenterZoom(loc2, z2, duration, false);\n return map;\n };\n\n\n map.transformEase = function(t2, duration) {\n duration = duration || 250;\n setTransform(t2, duration, false /* don't force */);\n return map;\n };\n\n\n map.zoomToEase = function(what, duration) {\n let extent;\n if (what instanceof geoExtent) {\n // we've directly been given an extent\n extent = what;\n } else {\n // we're given one or more entities to zoom to\n if (!isArray(what)) what = [what];\n extent = what\n .map(entity => entity.extent(context.graph()))\n .reduce((a, b) => a.extend(b));\n }\n\n if (!isFinite(extent.area())) return map;\n\n var z = clamp(map.trimmedExtentZoom(extent), 0, 20);\n const loc = trimmedCenter(extent.center(), z);\n\n if (duration === 0) {\n return map.centerZoom(loc, z);\n } else {\n return map.centerZoomEase(loc, z, duration);\n }\n };\n\n\n map.startEase = function() {\n utilBindOnce(surface, _pointerPrefix + 'down.ease', function() {\n map.cancelEase();\n });\n return map;\n };\n\n\n map.cancelEase = function() {\n _selection.interrupt();\n return map;\n };\n\n\n map.extent = function(val) {\n if (!arguments.length) {\n return new geoExtent(\n projection.invert([0, _dimensions[1]]),\n projection.invert([_dimensions[0], 0])\n );\n } else {\n var extent = geoExtent(val);\n map.centerZoom(extent.center(), map.extentZoom(extent));\n }\n };\n\n\n map.trimmedExtent = function(val) {\n if (!arguments.length) {\n var headerY = 71;\n var footerY = 30;\n var pad = 10;\n return new geoExtent(\n projection.invert([pad, _dimensions[1] - footerY - pad]),\n projection.invert([_dimensions[0] - pad, headerY + pad])\n );\n } else {\n var extent = geoExtent(val);\n map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));\n }\n };\n\n\n function calcExtentZoom(extent, dim) {\n var tl = projection([extent[0][0], extent[1][1]]);\n var br = projection([extent[1][0], extent[0][1]]);\n\n // Calculate maximum zoom that fits extent\n var hFactor = (br[0] - tl[0]) / dim[0];\n var vFactor = (br[1] - tl[1]) / dim[1];\n var hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2;\n var vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2;\n var newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff);\n\n return newZoom;\n }\n\n\n map.extentZoom = function(val) {\n return calcExtentZoom(geoExtent(val), _dimensions);\n };\n\n\n map.trimmedExtentZoom = function(val) {\n const trim = 40;\n const trimmed = [\n _dimensions[0] - trim - paneWidth(),\n _dimensions[1] - trim - toolbarHeight() - footerHeight()\n ];\n return calcExtentZoom(geoExtent(val), trimmed);\n };\n\n\n map.withinEditableZoom = function() {\n return map.zoom() >= context.minEditableZoom();\n };\n\n\n map.isInWideSelection = function() {\n return !map.withinEditableZoom() && context.selectedIDs().length;\n };\n\n\n map.editableDataEnabled = function(skipZoomCheck) {\n\n var layer = context.layers().layer('osm');\n if (!layer || !layer.enabled()) return false;\n\n return skipZoomCheck || map.withinEditableZoom();\n };\n\n\n map.notesEditable = function() {\n var layer = context.layers().layer('notes');\n if (!layer || !layer.enabled()) return false;\n\n return map.withinEditableZoom();\n };\n\n\n map.minzoom = function(val) {\n if (!arguments.length) return _minzoom;\n _minzoom = val;\n return map;\n };\n\n\n map.toggleHighlightEdited = function() {\n surface.classed('highlight-edited', !surface.classed('highlight-edited'));\n map.pan([0,0]); // trigger a redraw\n dispatch.call('changeHighlighting', this);\n };\n\n\n map.areaFillOptions = ['wireframe', 'partial', 'full'];\n\n map.activeAreaFill = function(val) {\n if (!arguments.length) return prefs('area-fill') || 'partial';\n\n prefs('area-fill', val);\n if (val !== 'wireframe') {\n prefs('area-fill-toggle', val);\n }\n updateAreaFill();\n map.pan([0,0]); // trigger a redraw\n dispatch.call('changeAreaFill', this);\n return map;\n };\n\n map.toggleWireframe = function() {\n\n var activeFill = map.activeAreaFill();\n\n if (activeFill === 'wireframe') {\n activeFill = prefs('area-fill-toggle') || 'partial';\n } else {\n activeFill = 'wireframe';\n }\n\n map.activeAreaFill(activeFill);\n };\n\n function updateAreaFill() {\n var activeFill = map.activeAreaFill();\n map.areaFillOptions.forEach(function(opt) {\n surface.classed('fill-' + opt, Boolean(opt === activeFill));\n });\n }\n\n\n map.layers = () => drawLayers;\n\n\n map.doubleUpHandler = function() {\n return _doubleUpHandler;\n };\n\n\n return utilRebind(map, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { services } from '../services';\nimport { utilRebind } from '../util/rebind';\nimport { utilQsString, utilStringQs } from '../util';\n\n\nexport function rendererPhotos(context) {\n var dispatch = d3_dispatch('change');\n var _layerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'kartaview', 'mapilio', 'vegbilder', 'panoramax'];\n var _allPhotoTypes = ['flat', 'panoramic'];\n var _shownPhotoTypes = _allPhotoTypes.slice(); // shallow copy\n var _dateFilters = ['fromDate', 'toDate'];\n var _fromDate;\n var _toDate;\n var _usernames;\n\n function photos() {}\n\n function updateStorage() {\n var hash = utilStringQs(window.location.hash);\n var enabled = context.layers().all().filter(function(d) {\n return _layerIDs.indexOf(d.id) !== -1 && d.layer && d.layer.supported() && d.layer.enabled();\n }).map(function(d) {\n return d.id;\n });\n if (enabled.length) {\n hash.photo_overlay = enabled.join(',');\n } else {\n delete hash.photo_overlay;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n }\n\n /**\n * @returns The layer ID\n */\n photos.overlayLayerIDs = function() {\n return _layerIDs;\n };\n\n /**\n * @returns All the photo types\n */\n photos.allPhotoTypes = function() {\n return _allPhotoTypes;\n };\n\n /**\n * @returns The date filters value\n */\n photos.dateFilters = function() {\n return _dateFilters;\n };\n\n photos.dateFilterValue = function(val) {\n return val === _dateFilters[0] ? _fromDate : _toDate;\n };\n\n /**\n * Sets the date filter (min/max date)\n * @param {*} type Either 'fromDate' or 'toDate'\n * @param {*} val The actual Date\n * @param {boolean} updateUrl Whether the URL should update or not\n */\n photos.setDateFilter = function(type, val, updateUrl) {\n // validate the date\n var date = val && new Date(val);\n if (date && !isNaN(date)) {\n val = date.toISOString().slice(0, 10);\n } else {\n val = null;\n }\n if (type === _dateFilters[0]) {\n _fromDate = val;\n if (_fromDate && _toDate && new Date(_toDate) < new Date(_fromDate)) {\n _toDate = _fromDate;\n }\n }\n if (type === _dateFilters[1]) {\n _toDate = val;\n if (_fromDate && _toDate && new Date(_toDate) < new Date(_fromDate)) {\n _fromDate = _toDate;\n }\n }\n dispatch.call('change', this);\n if (updateUrl) {\n var rangeString;\n if (_fromDate || _toDate) {\n rangeString = (_fromDate || '') + '_' + (_toDate || '');\n }\n setUrlFilterValue('photo_dates', rangeString);\n }\n };\n\n /**\n * Sets the username filter\n * @param {string} val The username\n * @param {boolean} updateUrl Whether the URL should update or not\n */\n photos.setUsernameFilter = function(val, updateUrl) {\n if (val && typeof val === 'string') val = val.replace(/;/g, ',').split(',');\n if (val) {\n val = val.map(d => d.trim()).filter(Boolean);\n if (!val.length) {\n val = null;\n }\n }\n _usernames = val;\n dispatch.call('change', this);\n if (updateUrl) {\n var hashString;\n if (_usernames) {\n hashString = _usernames.join(',');\n }\n setUrlFilterValue('photo_username', hashString);\n }\n };\n\n /**\n * Util function to set the slider date filter\n * @param {*} val Either 'panoramic' or 'flat'\n * @param {boolean} updateUrl Whether the URL should update or not\n */\n photos.togglePhotoType = function(val, updateUrl) {\n var index = _shownPhotoTypes.indexOf(val);\n if (index !== -1) {\n _shownPhotoTypes.splice(index, 1);\n } else {\n _shownPhotoTypes.push(val);\n }\n\n if (updateUrl) {\n var hashString;\n if (_shownPhotoTypes) {\n hashString = _shownPhotoTypes.join(',');\n }\n setUrlFilterValue('photo_type', hashString);\n }\n\n dispatch.call('change', this);\n return photos;\n };\n\n /**\n * Updates the URL with new values\n * @param {*} val value to save\n * @param {string} property Name of the value\n */\n function setUrlFilterValue(property, val) {\n const hash = utilStringQs(window.location.hash);\n if (val) {\n if (hash[property] === val) return;\n hash[property] = val;\n } else {\n if (!(property in hash)) return;\n delete hash[property];\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n }\n\n function showsLayer(id) {\n var layer = context.layers().layer(id);\n return layer && layer.supported() && layer.enabled();\n }\n\n /**\n * @returns If the Date Slider filter should be drawn\n */\n photos.shouldFilterDateBySlider = function(){\n return showsLayer('mapillary') || showsLayer('kartaview') || showsLayer('mapilio')\n || showsLayer('streetside') || showsLayer('vegbilder') || showsLayer('panoramax');\n };\n\n /**\n * @returns If the Photo Type filter should be drawn\n */\n photos.shouldFilterByPhotoType = function() {\n return showsLayer('mapillary') ||\n (showsLayer('streetside') && showsLayer('kartaview')) || showsLayer('vegbilder') || showsLayer('panoramax');\n };\n\n /**\n * @returns If the Username filter should be drawn\n */\n photos.shouldFilterByUsername = function() {\n return !showsLayer('mapillary') && showsLayer('kartaview') && !showsLayer('streetside') || showsLayer('panoramax');\n };\n\n photos.showsPhotoType = function(val) {\n if (!photos.shouldFilterByPhotoType()) return true;\n\n return _shownPhotoTypes.indexOf(val) !== -1;\n };\n\n photos.showsFlat = function() {\n return photos.showsPhotoType('flat');\n };\n\n photos.showsPanoramic = function() {\n return photos.showsPhotoType('panoramic');\n };\n\n photos.fromDate = function() {\n return _fromDate;\n };\n\n photos.toDate = function() {\n return _toDate;\n };\n\n photos.usernames = function() {\n return _usernames;\n };\n\n /**\n * Inits the streetlevel layer given the saved values in the URL\n */\n photos.init = function() {\n var hash = utilStringQs(window.location.hash);\n var parts;\n if (hash.photo_dates) {\n // expect format like `photo_dates=2019-01-01_2020-12-31`, but allow a couple different separators\n parts = /^(.*)[\u2013_](.*)$/g.exec(hash.photo_dates.trim());\n this.setDateFilter('fromDate', parts && parts.length >= 2 && parts[1], false);\n this.setDateFilter('toDate', parts && parts.length >= 3 && parts[2], false);\n }\n if (hash.photo_username) {\n this.setUsernameFilter(hash.photo_username, false);\n }\n if (hash.photo_type) {\n parts = hash.photo_type.replace(/;/g, ',').split(',');\n _allPhotoTypes.forEach(d => {\n if (!parts.includes(d)) this.togglePhotoType(d, false);\n });\n }\n if (hash.photo_overlay) {\n // support enabling photo layers by default via a URL parameter, e.g. `photo_overlay=kartaview;mapillary;streetside`\n var hashOverlayIDs = hash.photo_overlay.replace(/;/g, ',').split(',');\n hashOverlayIDs.forEach(function(id) {\n if (id === 'openstreetcam') id = 'kartaview'; // legacy alias\n var layer = _layerIDs.indexOf(id) !== -1 && context.layers().layer(id);\n if (layer && !layer.enabled()) layer.enabled(true);\n });\n }\n if (hash.photo) {\n // support opening a photo via a URL parameter, e.g. `photo=mapillary-fztgSDtLpa08ohPZFZjeRQ`\n var photoIds = hash.photo.replace(/;/g, ',').split(',');\n var photoId = photoIds.length && photoIds[0].trim();\n var results = /(.*)\\/(.*)/g.exec(photoId);\n if (results && results.length >= 3) {\n var serviceId = results[1];\n if (serviceId === 'openstreetcam') serviceId = 'kartaview'; // legacy alias\n var photoKey = results[2];\n var service = services[serviceId];\n if (service && service.ensureViewerLoaded) {\n\n // if we're showing a photo then make sure its layer is enabled too\n var layer = _layerIDs.indexOf(serviceId) !== -1 && context.layers().layer(serviceId);\n if (layer && !layer.enabled()) layer.enabled(true);\n\n var baselineTime = Date.now();\n\n service.on('loadedImages.rendererPhotos', function() {\n // don't open the viewer if too much time has elapsed\n if (Date.now() - baselineTime > 45000) {\n service.on('loadedImages.rendererPhotos', null);\n return;\n }\n\n if (!service.cachedImage(photoKey)) return;\n\n service.on('loadedImages.rendererPhotos', null);\n service.ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, photoKey)\n .showViewer(context);\n });\n });\n }\n }\n }\n\n context.layers().on('change.rendererPhotos', updateStorage);\n };\n\n return utilRebind(photos, dispatch, 'on');\n}\n", "export { rendererBackgroundSource } from './background_source';\nexport { rendererBackground } from './background';\nexport { rendererFeatures } from './features';\nexport { rendererMap } from './map';\nexport { rendererPhotos } from './photos';\nexport { rendererTileLayer } from './tile_layer';\n", "import { geoPath as d3_geoPath } from 'd3-geo';\nimport { select as d3_select } from 'd3-selection';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport { t } from '../core/localizer';\nimport { geoRawMercator, geoScaleToZoom, geoVecSubtract, geoVecScale, geoZoomToScale } from '../geo';\nimport { rendererTileLayer } from '../renderer';\nimport { svgDebug, svgData } from '../svg';\nimport { utilSetTransform } from '../util';\n// import { utilGetDimensions } from '../util/dimensions';\n\n\nexport function uiMapInMap(context) {\n\n function mapInMap(selection) {\n var backgroundLayer = rendererTileLayer(context)\n .underzoom(2);\n var overlayLayers = {};\n var projection = geoRawMercator();\n var dataLayer = svgData(projection, context).showLabels(false);\n var debugLayer = svgDebug(projection, context);\n var zoom = d3_zoom()\n .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(24)])\n .on('start', zoomStarted)\n .on('zoom', zoomed)\n .on('end', zoomEnded);\n\n var wrap = d3_select(null);\n var tiles = d3_select(null);\n var viewport = d3_select(null);\n\n var _isTransformed = false;\n var _isHidden = true;\n var _skipEvents = false;\n var _gesture = null;\n var _zDiff = 6; // by default, minimap renders at (main zoom - 6)\n var _dMini; // dimensions of minimap\n var _cMini; // center pixel of minimap\n var _tStart; // transform at start of gesture\n var _tCurr; // transform at most recent event\n var _timeoutID;\n\n\n function zoomStarted() {\n if (_skipEvents) return;\n _tCurr = projection.transform();\n _tStart = _tCurr;\n _gesture = null;\n }\n\n\n function zoomed(d3_event) {\n if (_skipEvents) return;\n\n var x = d3_event.transform.x;\n var y = d3_event.transform.y;\n var k = d3_event.transform.k;\n var isZooming = (k !== _tStart.k);\n var isPanning = (x !== _tStart.x || y !== _tStart.y);\n\n if (!isZooming && !isPanning) {\n return; // no change\n }\n\n // lock in either zooming or panning, don't allow both in minimap.\n if (!_gesture) {\n _gesture = isZooming ? 'zoom' : 'pan';\n }\n\n var tMini = projection.transform();\n var tX, tY, scale;\n\n if (_gesture === 'zoom') {\n scale = k / tMini.k;\n tX = (_cMini[0] / scale - _cMini[0]) * scale;\n tY = (_cMini[1] / scale - _cMini[1]) * scale;\n } else {\n k = tMini.k;\n scale = 1;\n tX = x - tMini.x;\n tY = y - tMini.y;\n }\n\n utilSetTransform(tiles, tX, tY, scale);\n utilSetTransform(viewport, 0, 0, scale);\n _isTransformed = true;\n _tCurr = d3_zoomIdentity.translate(x, y).scale(k);\n\n var zMain = geoScaleToZoom(context.projection.scale());\n var zMini = geoScaleToZoom(k);\n\n _zDiff = zMain - zMini;\n\n queueRedraw();\n }\n\n\n function zoomEnded() {\n if (_skipEvents) return;\n if (_gesture !== 'pan') return;\n\n updateProjection();\n _gesture = null;\n context.map().center(projection.invert(_cMini)); // recenter main map..\n }\n\n\n function updateProjection() {\n var loc = context.map().center();\n var tMain = context.projection.transform();\n var zMain = geoScaleToZoom(tMain.k);\n var zMini = Math.max(zMain - _zDiff, 0.5);\n var kMini = geoZoomToScale(zMini);\n\n projection\n .translate([tMain.x, tMain.y])\n .scale(kMini);\n\n var point = projection(loc);\n var mouse = (_gesture === 'pan') ? geoVecSubtract([_tCurr.x, _tCurr.y], [_tStart.x, _tStart.y]) : [0, 0];\n var xMini = _cMini[0] - point[0] + tMain.x + mouse[0];\n var yMini = _cMini[1] - point[1] + tMain.y + mouse[1];\n\n projection\n .translate([xMini, yMini])\n .clipExtent([[0, 0], _dMini]);\n\n _tCurr = projection.transform();\n\n if (_isTransformed) {\n utilSetTransform(tiles, 0, 0);\n utilSetTransform(viewport, 0, 0);\n _isTransformed = false;\n }\n\n zoom\n .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(zMain - 3)]);\n\n _skipEvents = true;\n wrap.call(zoom.transform, _tCurr);\n _skipEvents = false;\n }\n\n\n function redraw() {\n clearTimeout(_timeoutID);\n if (_isHidden) return;\n\n updateProjection();\n var zMini = geoScaleToZoom(projection.scale());\n\n // setup tile container\n tiles = wrap\n .selectAll('.map-in-map-tiles')\n .data([0]);\n\n tiles = tiles.enter()\n .append('div')\n .attr('class', 'map-in-map-tiles')\n .merge(tiles);\n\n // redraw background\n backgroundLayer\n .source(context.background().baseLayerSource())\n .projection(projection)\n .dimensions(_dMini);\n\n var background = tiles\n .selectAll('.map-in-map-background')\n .data([0]);\n\n background.enter()\n .append('div')\n .attr('class', 'map-in-map-background')\n .merge(background)\n .call(backgroundLayer);\n\n\n // redraw overlay\n var overlaySources = context.background().overlayLayerSources();\n var activeOverlayLayers = [];\n for (var i = 0; i < overlaySources.length; i++) {\n if (overlaySources[i].validZoom(zMini)) {\n if (!overlayLayers[i]) overlayLayers[i] = rendererTileLayer(context);\n activeOverlayLayers.push(overlayLayers[i]\n .source(overlaySources[i])\n .projection(projection)\n .dimensions(_dMini));\n }\n }\n\n var overlay = tiles\n .selectAll('.map-in-map-overlay')\n .data([0]);\n\n overlay = overlay.enter()\n .append('div')\n .attr('class', 'map-in-map-overlay')\n .merge(overlay);\n\n\n var overlays = overlay\n .selectAll('div')\n .data(activeOverlayLayers, function(d) { return d.source().name(); });\n\n overlays.exit()\n .remove();\n\n overlays.enter()\n .append('div')\n .merge(overlays)\n .each(function(layer) { d3_select(this).call(layer); });\n\n\n var dataLayers = tiles\n .selectAll('.map-in-map-data')\n .data([0]);\n\n dataLayers.exit()\n .remove();\n\n dataLayers.enter()\n .append('svg')\n .attr('class', 'map-in-map-data')\n .merge(dataLayers)\n .call(dataLayer)\n .call(debugLayer);\n\n\n // redraw viewport bounding box\n if (_gesture !== 'pan') {\n var getPath = d3_geoPath(projection);\n var bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] };\n\n viewport = wrap.selectAll('.map-in-map-viewport')\n .data([0]);\n\n viewport = viewport.enter()\n .append('svg')\n .attr('class', 'map-in-map-viewport')\n .merge(viewport);\n\n\n var path = viewport.selectAll('.map-in-map-bbox')\n .data([bbox]);\n\n path.enter()\n .append('path')\n .attr('class', 'map-in-map-bbox')\n .merge(path)\n .attr('d', getPath)\n .classed('thick', function(d) { return getPath.area(d) < 30; });\n }\n }\n\n\n function queueRedraw() {\n clearTimeout(_timeoutID);\n _timeoutID = setTimeout(function() { redraw(); }, 750);\n }\n\n\n function toggle(d3_event) {\n if (d3_event) d3_event.preventDefault();\n\n _isHidden = !_isHidden;\n\n context.container().select('.minimap-toggle-item')\n .classed('active', !_isHidden)\n .select('input')\n .property('checked', !_isHidden);\n\n if (_isHidden) {\n wrap\n .style('display', 'block')\n .style('opacity', '1')\n .transition()\n .duration(200)\n .style('opacity', '0')\n .on('end', function() {\n selection.selectAll('.map-in-map')\n .style('display', 'none');\n });\n } else {\n wrap\n .style('display', 'block')\n .style('opacity', '0')\n .transition()\n .duration(200)\n .style('opacity', '1')\n .on('end', function() {\n redraw();\n });\n }\n }\n\n\n uiMapInMap.toggle = toggle;\n\n wrap = selection.selectAll('.map-in-map')\n .data([0]);\n\n wrap = wrap.enter()\n .append('div')\n .attr('class', 'map-in-map')\n .style('display', (_isHidden ? 'none' : 'block'))\n .call(zoom)\n .on('dblclick.zoom', null)\n .merge(wrap);\n\n // reflow warning: Hardcode dimensions - currently can't resize it anyway..\n _dMini = [200,150]; //utilGetDimensions(wrap);\n _cMini = geoVecScale(_dMini, 0.5);\n\n context.map()\n .on('drawn.map-in-map', function(drawn) {\n if (drawn.full === true) {\n redraw();\n }\n });\n\n redraw();\n\n context.keybinding()\n .on(t('background.minimap.key'), toggle);\n }\n\n return mapInMap;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/index';\n\n\nexport function uiNotice(context) {\n\n return function(selection) {\n var div = selection\n .append('div')\n .attr('class', 'notice');\n\n var button = div\n .append('button')\n .attr('class', 'zoom-to notice fillD')\n .on('click', function() {\n context.map().zoomEase(context.minEditableZoom());\n })\n .on('wheel', function(d3_event) { // let wheel events pass through #4482\n var e2 = new WheelEvent(d3_event.type, d3_event);\n context.surface().node().dispatchEvent(e2);\n });\n\n button\n .call(svgIcon('#iD-icon-plus', 'pre-text'))\n .append('span')\n .attr('class', 'label')\n .call(t.append('zoom_in_edit'));\n\n\n function disableTooHigh() {\n var canEdit = context.map().zoom() >= context.minEditableZoom();\n div.style('display', canEdit ? 'none' : 'block');\n }\n\n context.map()\n .on('move.notice', debounce(disableTooHigh, 500));\n\n disableTooHigh();\n };\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\nimport { clamp } from 'es-toolkit/compat';\n\nimport { t } from '../core/localizer';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { svgIcon } from '../svg/icon';\nimport { utilGetDimensions } from '../util/dimensions';\nimport { utilRebind } from '../util';\nimport { services } from '../services';\nimport { uiTooltip } from './tooltip';\nimport { actionChangeTags } from '../actions';\nimport { geoSphericalDistance } from '../geo';\n\nexport function uiPhotoviewer(context) {\n\n var dispatch = d3_dispatch('resize');\n\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n const addPhotoIdButton = new Set(['mapillary', 'panoramax']);\n\n function photoviewer(selection) {\n selection\n .append('button')\n .attr('class', 'thumb-hide')\n .attr('title', t('icons.close'))\n .on('click', function () {\n for (const service of Object.values(services)) {\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n })\n .append('div')\n .call(svgIcon('#iD-icon-close'));\n\n function preventDefault(d3_event) {\n d3_event.preventDefault();\n }\n\n selection\n .append('button')\n .attr('class', 'resize-handle-xy')\n .on('touchstart touchdown touchend', preventDefault)\n .on(\n _pointerPrefix + 'down',\n buildResizeListener(selection, 'resize', dispatch, { resizeOnX: true, resizeOnY: true })\n );\n\n selection\n .append('button')\n .attr('class', 'resize-handle-x')\n .on('touchstart touchdown touchend', preventDefault)\n .on(\n _pointerPrefix + 'down',\n buildResizeListener(selection, 'resize', dispatch, { resizeOnX: true })\n );\n\n selection\n .append('button')\n .attr('class', 'resize-handle-y')\n .on('touchstart touchdown touchend', preventDefault)\n .on(\n _pointerPrefix + 'down',\n buildResizeListener(selection, 'resize', dispatch, { resizeOnY: true })\n );\n\n // update sett_photo_from_viewer button on selection change and when tags change\n context.features().on('change.setPhotoFromViewer', function() {\n setPhotoTagButton();\n });\n context.history().on('change.setPhotoFromViewer', function() {\n setPhotoTagButton();\n });\n\n\n function setPhotoTagButton() {\n const service = getServiceId();\n const isActiveForService = addPhotoIdButton.has(service) &&\n services[service].isViewerOpen() &&\n layerEnabled(service) &&\n context.mode().id === 'select';\n\n renderAddPhotoIdButton(service, isActiveForService);\n\n function layerEnabled(which) {\n const layers = context.layers();\n const layer = layers.layer(which);\n return layer.enabled();\n }\n\n function getServiceId() {\n for (const serviceId in services) {\n const service = services[serviceId];\n if (typeof service.isViewerOpen === 'function') {\n if (service.isViewerOpen()) {\n return serviceId;\n }\n }\n }\n return false;\n }\n\n function renderAddPhotoIdButton(service, shouldDisplay) {\n const button = selection.selectAll('.set-photo-from-viewer')\n .data(shouldDisplay ? [0] : []);\n\n button.exit()\n .remove();\n\n const buttonEnter = button.enter()\n .append('button')\n .attr('class', 'set-photo-from-viewer')\n .call(svgIcon('#fas-eye-dropper'))\n .call(uiTooltip()\n .title(() => t.append('inspector.set_photo_from_viewer.enable'))\n .placement('right')\n );\n\n buttonEnter\n .select('.tooltip')\n .classed('dark', true)\n .style('width', '300px');\n\n buttonEnter\n .merge(button)\n .on('click', function (e) {\n e.preventDefault();\n e.stopPropagation();\n const activeServiceId = getServiceId();\n const image = services[activeServiceId].getActiveImage();\n\n const action = graph =>\n context.selectedIDs().reduce((graph, entityID) => {\n const tags = graph.entity(entityID).tags;\n const action = actionChangeTags(entityID, {...tags, [activeServiceId]: image.id});\n return action(graph);\n }, graph);\n\n const annotation = t('operations.change_tags.annotation');\n context.perform(action, annotation);\n buttonDisable('already_set');\n });\n\n if (service === 'panoramax') {\n const panoramaxControls = selection.select('.panoramax-wrapper .pnlm-zoom-controls.pnlm-controls');\n\n panoramaxControls\n .style('margin-top', shouldDisplay ? '36px' : '6px');\n }\n\n if (!shouldDisplay) return;\n\n const activeImage = services[service].getActiveImage();\n\n const graph = context.graph();\n const entities = context.selectedIDs()\n .map(id => graph.hasEntity(id))\n .filter(Boolean);\n\n if (entities.map(entity => entity.tags[service])\n .every(value => value === activeImage?.id)) {\n buttonDisable('already_set');\n } else if (activeImage && entities\n .map(entity => entity.extent(context.graph()).center())\n .every(loc => geoSphericalDistance(loc, activeImage.loc) > 100)) {\n buttonDisable('too_far');\n } else {\n buttonDisable(false);\n }\n }\n\n function buttonDisable(reason) {\n const disabled = reason !== false;\n const button = selection.selectAll('.set-photo-from-viewer').data([0]);\n button.attr('disabled', disabled ? 'true' : null);\n button.classed('disabled', disabled);\n button.call(uiTooltip().destroyAny);\n if (disabled) {\n button.call(uiTooltip()\n .title(() => t.append(`inspector.set_photo_from_viewer.disable.${reason}`))\n .placement('right')\n );\n } else {\n button.call(uiTooltip()\n .title(() => t.append('inspector.set_photo_from_viewer.enable'))\n .placement('right')\n );\n }\n\n button.select('.tooltip')\n .classed('dark', true)\n .style('width', '300px');\n }\n }\n\n function buildResizeListener(target, eventName, dispatch, options) {\n\n var resizeOnX = !!options.resizeOnX;\n var resizeOnY = !!options.resizeOnY;\n var minHeight = options.minHeight || 240;\n var minWidth = options.minWidth || 320;\n var pointerId;\n var startX;\n var startY;\n var startWidth;\n var startHeight;\n\n function startResize(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n var mapSize = context.map().dimensions();\n\n if (resizeOnX) {\n var mapWidth = mapSize[0];\n const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-left'), 10);\n var newWidth = clamp((startWidth + d3_event.clientX - startX), minWidth, mapWidth - viewerMargin * 2);\n target.style('width', newWidth + 'px');\n }\n\n if (resizeOnY) {\n const menuHeight = utilGetDimensions(d3_select('.top-toolbar'))[1] +\n utilGetDimensions(d3_select('.map-footer'))[1];\n const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-bottom'), 10);\n var maxHeight = mapSize[1] - menuHeight - viewerMargin * 2; // preserve space at top/bottom of map\n var newHeight = clamp((startHeight + startY - d3_event.clientY), minHeight, maxHeight);\n target.style('height', newHeight + 'px');\n }\n\n dispatch.call(eventName, target, subtractPadding(utilGetDimensions(target, true), target));\n }\n\n function stopResize(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n // remove all the listeners we added\n d3_select(window)\n .on('.' + eventName, null);\n }\n\n return function initResize(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n pointerId = d3_event.pointerId || 'mouse';\n\n startX = d3_event.clientX;\n startY = d3_event.clientY;\n var targetRect = target.node().getBoundingClientRect();\n startWidth = targetRect.width;\n startHeight = targetRect.height;\n\n d3_select(window)\n .on(_pointerPrefix + 'move.' + eventName, startResize, false)\n .on(_pointerPrefix + 'up.' + eventName, stopResize, false);\n\n if (_pointerPrefix === 'pointer') {\n d3_select(window)\n .on('pointercancel.' + eventName, stopResize, false);\n }\n };\n }\n }\n\n photoviewer.onMapResize = function() {\n var photoviewer = context.container().select('.photoviewer');\n var content = context.container().select('.main-content');\n var mapDimensions = utilGetDimensions(content, true);\n const menuHeight = utilGetDimensions(d3_select('.top-toolbar'))[1] +\n utilGetDimensions(d3_select('.map-footer'))[1];\n const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-bottom'), 10);\n // shrink photo viewer if it is too big (preserves space at top and bottom of map used by menus)\n var photoDimensions = utilGetDimensions(photoviewer, true);\n if (photoDimensions[0] > mapDimensions[0] || photoDimensions[1] > (mapDimensions[1] - menuHeight - viewerMargin * 2)) {\n var setPhotoDimensions = [\n Math.min(photoDimensions[0], mapDimensions[0]),\n Math.min(photoDimensions[1], mapDimensions[1] - menuHeight - viewerMargin * 2),\n ];\n\n photoviewer\n .style('width', setPhotoDimensions[0] + 'px')\n .style('height', setPhotoDimensions[1] + 'px');\n\n dispatch.call('resize', photoviewer, subtractPadding(setPhotoDimensions, photoviewer));\n } else {\n dispatch.call('resize', photoviewer, subtractPadding(photoDimensions, photoviewer));\n }\n };\n\n function subtractPadding(dimensions, selection) {\n return [\n dimensions[0] - parseFloat(selection.style('padding-left')) - parseFloat(selection.style('padding-right')),\n dimensions[1] - parseFloat(selection.style('padding-top')) - parseFloat(selection.style('padding-bottom'))\n ];\n }\n\n photoviewer.viewerSize = function() {\n const photoviewer = context.container().select('.photoviewer');\n return subtractPadding(utilGetDimensions(photoviewer, true), photoviewer);\n };\n\n return utilRebind(photoviewer, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { uiModal } from './modal';\n\n\nexport function uiRestore(context) {\n return function(selection) {\n if (!context.history().hasRestorableChanges()) return;\n\n let modalSelection = uiModal(selection, true);\n\n modalSelection.select('.modal')\n .attr('class', 'modal fillL');\n\n let introModal = modalSelection.select('.content');\n\n introModal\n .append('div')\n .attr('class', 'modal-section')\n .append('h3')\n .call(t.append('restore.heading'));\n\n introModal\n .append('div')\n .attr('class','modal-section')\n .append('p')\n .call(t.append('restore.description'));\n\n let buttonWrap = introModal\n .append('div')\n .attr('class', 'modal-actions');\n\n let restore = buttonWrap\n .append('button')\n .attr('class', 'restore')\n .on('click', () => {\n context.history().restore();\n modalSelection.remove();\n });\n\n restore\n .append('svg')\n .attr('class', 'logo logo-restore')\n .append('use')\n .attr('xlink:href', '#iD-logo-restore');\n\n restore\n .append('div')\n .call(t.append('restore.restore'));\n\n let reset = buttonWrap\n .append('button')\n .attr('class', 'reset')\n .on('click', () => {\n context.history().clearSaved();\n modalSelection.remove();\n });\n\n reset\n .append('svg')\n .attr('class', 'logo logo-reset')\n .append('use')\n .attr('xlink:href', '#iD-logo-reset');\n\n reset\n .append('div')\n .call(t.append('restore.reset'));\n\n restore.node().focus();\n };\n}\n", "import { displayLength } from '../util/units';\nimport { geoLonToMeters, geoMetersToLon } from '../geo';\nimport { localizer } from '../core/localizer';\n\n\nexport function uiScale(context) {\n var projection = context.projection,\n isImperial = !localizer.usesMetric(),\n maxLength = 180,\n tickHeight = 8;\n\n\n function scaleDefs(loc1, loc2) {\n var lat = (loc2[1] + loc1[1]) / 2,\n conversion = (isImperial ? 3.28084 : 1),\n dist = geoLonToMeters(loc2[0] - loc1[0], lat) * conversion,\n scale = { dist: 0, px: 0, text: '' },\n buckets, i, val, dLon;\n\n if (isImperial) {\n buckets = [5280000, 528000, 52800, 5280, 500, 50, 5, 1];\n } else {\n buckets = [5000000, 500000, 50000, 5000, 500, 50, 5, 1];\n }\n\n // determine a user-friendly endpoint for the scale\n for (i = 0; i < buckets.length; i++) {\n val = buckets[i];\n if (dist >= val) {\n scale.dist = Math.floor(dist / val) * val;\n break;\n } else {\n scale.dist = +dist.toFixed(2);\n }\n }\n\n dLon = geoMetersToLon(scale.dist / conversion, lat);\n scale.px = Math.round(projection([loc1[0] + dLon, loc1[1]])[0]);\n\n scale.text = displayLength(scale.dist / conversion, isImperial);\n\n return scale;\n }\n\n\n function update(selection) {\n // choose loc1, loc2 along bottom of viewport (near where the scale will be drawn)\n var dims = context.map().dimensions(),\n loc1 = projection.invert([0, dims[1]]),\n loc2 = projection.invert([maxLength, dims[1]]),\n scale = scaleDefs(loc1, loc2);\n\n selection.select('.scale-path')\n .attr('d', 'M0.5,0.5v' + tickHeight + 'h' + scale.px + 'v-' + tickHeight);\n\n selection.select('.scale-text')\n .style(localizer.textDirection() === 'ltr' ? 'left' : 'right', (scale.px + 16) + 'px')\n .text(scale.text);\n }\n\n\n return function(selection) {\n function switchUnits() {\n isImperial = !isImperial;\n selection.call(update);\n }\n\n var scalegroup = selection.append('svg')\n .attr('class', 'scale')\n .on('click', switchUnits)\n .append('g')\n .attr('transform', 'translate(10,11)');\n\n scalegroup\n .append('path')\n .attr('class', 'scale-path');\n\n selection\n .append('div')\n .attr('class', 'scale-text');\n\n selection.call(update);\n\n context.map().on('move.scale', function() {\n update(selection);\n });\n };\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { fileFetcher } from '../core/file_fetcher';\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\nimport { uiModal } from './modal';\nimport { utilArrayUniq } from '../util';\nimport { utilDetect } from '../util/detect';\n\n\nexport function uiShortcuts(context) {\n var detected = utilDetect();\n var _activeTab = 0;\n var _modalSelection;\n var _selection = d3_select(null);\n var _dataShortcuts;\n\n\n function shortcutsModal(_modalSelection) {\n _modalSelection.select('.modal')\n .classed('modal-shortcuts', true);\n\n var content = _modalSelection.select('.content');\n\n content\n .append('div')\n .attr('class', 'modal-section header')\n .append('h2')\n .call(t.append('shortcuts.title'));\n\n fileFetcher.get('shortcuts')\n .then(function(data) {\n _dataShortcuts = data;\n content.call(render);\n })\n .catch(function() { /* ignore */ });\n }\n\n\n function render(selection) {\n if (!_dataShortcuts) return;\n\n var wrapper = selection\n .selectAll('.wrapper')\n .data([0]);\n\n var wrapperEnter = wrapper\n .enter()\n .append('div')\n .attr('class', 'wrapper modal-section');\n\n var tabsBar = wrapperEnter\n .append('div')\n .attr('class', 'tabs-bar');\n\n var shortcutsList = wrapperEnter\n .append('div')\n .attr('class', 'shortcuts-list');\n\n wrapper = wrapper.merge(wrapperEnter);\n\n var tabs = tabsBar\n .selectAll('.tab')\n .data(_dataShortcuts);\n\n var tabsEnter = tabs\n .enter()\n .append('a')\n .attr('class', 'tab')\n .attr('href', '#')\n .on('click', function (d3_event, d) {\n d3_event.preventDefault();\n var i = _dataShortcuts.indexOf(d);\n _activeTab = i;\n render(selection);\n });\n\n tabsEnter\n .append('span')\n .each(function (d) {\n d3_select(this).call(t.addOrUpdate(d.text));\n });\n\n // Update\n wrapper.selectAll('.tab')\n .classed('active', function (d, i) {\n return i === _activeTab;\n });\n\n\n var shortcuts = shortcutsList\n .selectAll('.shortcut-tab')\n .data(_dataShortcuts);\n\n var shortcutsEnter = shortcuts\n .enter()\n .append('div')\n .attr('class', function(d) { return 'shortcut-tab shortcut-tab-' + d.tab; });\n\n var columnsEnter = shortcutsEnter\n .selectAll('.shortcut-column')\n .data(function (d) { return d.columns; })\n .enter()\n .append('table')\n .attr('class', 'shortcut-column');\n\n var rowsEnter = columnsEnter\n .selectAll('.shortcut-row')\n .data(function (d) { return d.rows; })\n .enter()\n .append('tr')\n .attr('class', 'shortcut-row');\n\n\n var sectionRows = rowsEnter\n .filter(function (d) { return !d.shortcuts; });\n\n sectionRows\n .append('td');\n\n sectionRows\n .append('td')\n .attr('class', 'shortcut-section')\n .append('h3')\n .each(function (d) {\n d3_select(this).call(t.addOrUpdate(d.text));\n });\n\n\n var shortcutRows = rowsEnter\n .filter(function (d) { return d.shortcuts; });\n\n var shortcutKeys = shortcutRows\n .append('td')\n .attr('class', 'shortcut-keys');\n\n var modifierKeys = shortcutKeys\n .filter(function (d) { return d.modifiers; });\n\n modifierKeys\n .selectAll('kbd.modifier')\n .data(function (d) {\n if (detected.os === 'win' && d.text === 'shortcuts.editing.commands.redo') {\n return ['\u2318'];\n } else if (detected.os !== 'mac' && d.text === 'shortcuts.browsing.display_options.fullscreen') {\n return [];\n } else {\n return d.modifiers;\n }\n })\n .enter()\n .each(function () {\n var selection = d3_select(this);\n\n selection\n .append('kbd')\n .attr('class', 'modifier')\n .text(function (d) { return uiCmd.display(d); });\n\n selection\n .append('span')\n .text('+');\n });\n\n\n shortcutKeys\n .selectAll('kbd.shortcut')\n .data(function (d) {\n var arr = d.shortcuts;\n if (detected.os === 'win' && d.text === 'shortcuts.editing.commands.redo') {\n arr = ['Y'];\n } else if (detected.os !== 'mac' && d.text === 'shortcuts.browsing.display_options.fullscreen') {\n arr = ['F11'];\n }\n\n // replace translations\n arr = arr.map(function(s) {\n return uiCmd.display(s.indexOf('.') !== -1 ? t(s) : s);\n });\n\n return utilArrayUniq(arr).map(function(s) {\n return {\n shortcut: s,\n separator: d.separator,\n suffix: d.suffix\n };\n });\n })\n .enter()\n .each(function (d, i, nodes) {\n var selection = d3_select(this);\n var click = d.shortcut.toLowerCase().match(/(.*).click/);\n\n if (click && click[1]) { // replace \"left_click\", \"right_click\" with mouse icon\n selection\n .call(svgIcon('#iD-walkthrough-mouse-' + click[1], 'operation'));\n } else if (d.shortcut.toLowerCase() === 'long-press') {\n selection\n .call(svgIcon('#iD-walkthrough-longpress', 'longpress operation'));\n } else if (d.shortcut.toLowerCase() === 'tap') {\n selection\n .call(svgIcon('#iD-walkthrough-tap', 'tap operation'));\n } else {\n selection\n .append('kbd')\n .attr('class', 'shortcut')\n .text(function (d) { return d.shortcut; });\n }\n\n if (i < nodes.length - 1) {\n if (d.separator) {\n selection\n .append('span')\n .text(d.separator);\n } else {\n selection.append('span').text('\\u00a0');\n selection.append('span').call(t.append('shortcuts.or'));\n selection.append('span').text('\\u00a0');\n }\n } else if (i === nodes.length - 1 && d.suffix) {\n selection\n .append('span')\n .text(d.suffix);\n }\n });\n\n\n shortcutKeys\n .filter(function(d) { return d.gesture; })\n .each(function () {\n var selection = d3_select(this);\n\n selection\n .append('span')\n .text('+');\n\n selection\n .append('span')\n .attr('class', 'gesture')\n .each(function (d) {\n d3_select(this).call(t.addOrUpdate(d.gesture));\n });\n });\n\n\n shortcutRows\n .append('td')\n .attr('class', 'shortcut-desc')\n .each(function (d) {\n if (d.text) {\n d3_select(this).call(t.addOrUpdate(d.text));\n } else {\n d3_select(this).text('\\u00a0');\n }\n });\n\n\n // Update\n wrapper.selectAll('.shortcut-tab')\n .style('display', function (d, i) {\n return i === _activeTab ? 'flex' : 'none';\n });\n }\n\n\n return function(selection, show) {\n _selection = selection;\n if (show) {\n _modalSelection = uiModal(selection);\n _modalSelection.call(shortcutsModal);\n } else {\n context.keybinding()\n .on([t('shortcuts.toggle.key'), '?'], function () {\n if (context.container().selectAll('.modal-shortcuts').size()) { // already showing\n if (_modalSelection) {\n _modalSelection.close();\n _modalSelection = null;\n }\n } else {\n _modalSelection = uiModal(_selection);\n _modalSelection.call(shortcutsModal);\n }\n });\n }\n };\n}\n", "module.exports = element;\nmodule.exports.pair = pair;\nmodule.exports.format = format;\nmodule.exports.formatPair = formatPair;\nmodule.exports.coordToDMS = coordToDMS;\n\n\nfunction element(input, dims) {\n var result = search(input, dims);\n return (result === null) ? null : result.val;\n}\n\n\nfunction formatPair(input) {\n return format(input.lat, 'lat') + ' ' + format(input.lon, 'lon');\n}\n\n\n// Is 0 North or South?\nfunction format(input, dim) {\n var dms = coordToDMS(input, dim);\n return dms.whole + '\u00B0 ' +\n (dms.minutes ? dms.minutes + '\\' ' : '') +\n (dms.seconds ? dms.seconds + '\" ' : '') + dms.dir;\n}\n\n\nfunction coordToDMS(input, dim) {\n var dirs = { lat: ['N', 'S'], lon: ['E', 'W'] }[dim] || '';\n var dir = dirs[input >= 0 ? 0 : 1];\n var abs = Math.abs(input);\n var whole = Math.floor(abs);\n var fraction = abs - whole;\n var fractionMinutes = fraction * 60;\n var minutes = Math.floor(fractionMinutes);\n var seconds = Math.floor((fractionMinutes - minutes) * 60);\n\n return {\n whole: whole,\n minutes: minutes,\n seconds: seconds,\n dir: dir\n };\n}\n\n\nfunction search(input, dims) {\n if (!dims) dims = 'NSEW';\n if (typeof input !== 'string') return null;\n\n input = input.toUpperCase();\n var regex = /^[\\s\\,]*([NSEW])?\\s*([\\-|\\\u2014|\\\u2015]?[0-9.]+)[\u00B0\u00BA\u02DA]?\\s*(?:([0-9.]+)['\u2019\u2032\u2018]\\s*)?(?:([0-9.]+)(?:''|\"|\u201D|\u2033)\\s*)?([NSEW])?/;\n\n var m = input.match(regex);\n if (!m) return null; // no match\n\n var matched = m[0];\n\n // extract dimension.. m[1] = leading, m[5] = trailing\n var dim;\n if (m[1] && m[5]) { // if matched both..\n dim = m[1]; // keep leading\n matched = matched.slice(0, -1); // remove trailing dimension from match\n } else {\n dim = m[1] || m[5];\n }\n\n // if unrecognized dimension\n if (dim && dims.indexOf(dim) === -1) return null;\n\n // extract DMS\n var deg = m[2] ? parseFloat(m[2]) : 0;\n var min = m[3] ? parseFloat(m[3]) / 60 : 0;\n var sec = m[4] ? parseFloat(m[4]) / 3600 : 0;\n var sign = (deg < 0) ? -1 : 1;\n if (dim === 'S' || dim === 'W') sign *= -1;\n\n return {\n val: (Math.abs(deg) + min + sec) * sign,\n dim: dim,\n matched: matched,\n remain: input.slice(matched.length)\n };\n}\n\n\nfunction pair(input, dims) {\n input = input.trim();\n var one = search(input, dims);\n if (!one) return null;\n\n input = one.remain.trim();\n var two = search(input, dims);\n if (!two || two.remain) return null;\n\n if (one.dim) {\n return swapdim(one.val, two.val, one.dim);\n } else {\n return [one.val, two.val];\n }\n}\n\n\nfunction swapdim(a, b, dim) {\n if (dim === 'N' || dim === 'S') return [a, b];\n if (dim === 'W' || dim === 'E') return [b, a];\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\nimport * as sexagesimal from '@mapbox/sexagesimal';\n\nimport { presetManager } from '../presets';\nimport { t } from '../core/localizer';\nimport { dmsCoordinatePair, dmsMatcher } from '../util/units';\nimport { coreGraph } from '../core/graph';\nimport { geoSphericalDistance } from '../geo/geo';\nimport { geoExtent } from '../geo';\nimport { modeSelect } from '../modes/select';\nimport { osmEntity } from '../osm/entity';\nimport { getRelationColor } from '../osm/tags';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\n\nimport {\n utilDisplayName,\n utilDisplayType,\n utilHighlightEntities,\n utilNoAuto\n} from '../util';\n\n\nexport const idMatch = q => {\n const idMatchRegex = /(?:^|\\W)(node|way|relation|note|[nwr])\\W{0,2}0*([1-9]\\d*)(?:\\W|$)/i;\n const idMatch = q.match(idMatchRegex);\n if (!idMatch) return false;\n\n return {\n type: idMatch[1] === 'note' ? idMatch[1] : idMatch[1].charAt(0),\n id: idMatch[2]\n };\n};\n\nexport function uiFeatureList(context) {\n var _geocodeResults;\n\n\n function featureList(selection) {\n var header = selection\n .append('div')\n .attr('class', 'header fillL');\n\n header\n .append('h2')\n .call(t.append('inspector.feature_list'));\n\n var searchWrap = selection\n .append('div')\n .attr('class', 'search-header');\n\n searchWrap\n .call(svgIcon('#iD-icon-search', 'pre-text'));\n\n var search = searchWrap\n .append('input')\n .attr('placeholder', t('inspector.search'))\n .attr('type', 'search')\n .call(utilNoAuto)\n .on('keypress', keypress)\n .on('keydown', keydown)\n .on('input', inputevent);\n\n var listWrap = selection\n .append('div')\n .attr('class', 'inspector-body');\n\n var list = listWrap\n .append('div')\n .attr('class', 'feature-list');\n\n context\n .on('exit.feature-list', clearSearch);\n context.map()\n .on('drawn.feature-list', mapDrawn);\n\n context.keybinding()\n .on(uiCmd('\u2318F'), focusSearch);\n\n\n function focusSearch(d3_event) {\n var mode = context.mode() && context.mode().id;\n if (mode !== 'browse') return;\n\n d3_event.preventDefault();\n search.node().focus();\n }\n\n\n function keydown(d3_event) {\n if (d3_event.keyCode === 27) { // escape\n search.node().blur();\n }\n }\n\n\n function keypress(d3_event) {\n var q = search.property('value'),\n items = list.selectAll('.feature-list-item');\n if (d3_event.keyCode === 13 && // \u21A9 Return\n q.length &&\n items.size()) {\n click(d3_event, items.datum());\n }\n }\n\n\n function inputevent() {\n _geocodeResults = undefined;\n drawList();\n }\n\n\n function clearSearch() {\n search.property('value', '');\n drawList();\n }\n\n\n function mapDrawn(e) {\n if (e.full) {\n drawList();\n }\n }\n\n\n function features() {\n var graph = context.graph();\n var visibleCenter = context.map().extent().center();\n var q = search.property('value').toLowerCase().trim();\n\n if (!q) return [];\n\n const locationMatch = sexagesimal.pair(q.toUpperCase()) || dmsMatcher(q);\n\n const coordResult = [];\n if (locationMatch) {\n const latLon = [Number(locationMatch[0]), Number(locationMatch[1])];\n const lonLat = [latLon[1], latLon[0]]; // also try swapped order\n\n const isLatLonValid = latLon[0] >= -90 && latLon[0] <= 90 && latLon[1] >= -180 && latLon[1] <= 180;\n let isLonLatValid = lonLat[0] >= -90 && lonLat[0] <= 90 && lonLat[1] >= -180 && lonLat[1] <= 180;\n isLonLatValid &&= !q.match(/[NSEW]/i); // don't flip coords with explicit cardinal directions\n isLonLatValid &&= !locationMatch[2]; // don't flip zoom/x/y coords\n isLonLatValid &&= lonLat[0] !== lonLat[1]; // don't flip when lat=lon\n\n if (isLatLonValid) {\n coordResult.push({\n id: latLon[0] + '/' + latLon[1],\n geometry: 'point',\n type: t('inspector.location'),\n name: dmsCoordinatePair([latLon[1], latLon[0]]),\n location: latLon,\n zoom: locationMatch[2]\n });\n }\n if (isLonLatValid) {\n coordResult.push({\n id: lonLat[0] + '/' + lonLat[1],\n geometry: 'point',\n type: t('inspector.location'),\n name: dmsCoordinatePair([lonLat[1], lonLat[0]]),\n location: lonLat\n });\n }\n }\n\n // A location search takes priority over an ID search\n const idMatchResult = !locationMatch && idMatch(q);\n const idResult = [];\n if (idMatchResult) {\n const elemType = idMatchResult.type;\n const elemId = idMatchResult.id;\n idResult.push({\n id: elemType + elemId,\n geometry: elemType === 'n' ? 'point' : elemType === 'w' ? 'line' : elemType === 'note' ? 'note' : 'relation',\n type: elemType === 'n' ? t('inspector.node') : elemType === 'w' ? t('inspector.way') : elemType === 'note' ? t('note.note') : t('inspector.relation'),\n name: elemId\n });\n }\n\n var allEntities = graph.entities;\n const localResults = [];\n for (var id in allEntities) {\n var entity = allEntities[id];\n if (!entity) continue;\n\n var matched = presetManager.match(entity, graph);\n var name = utilDisplayName(entity, { hideNetwork: matched.suggestion }) || '';\n if (name.toLowerCase().indexOf(q) < 0) continue;\n var type = (matched && matched.name()) || utilDisplayType(entity.id);\n var extent = entity.extent(graph);\n var distance = extent ? geoSphericalDistance(visibleCenter, extent.center()) : 0;\n\n localResults.push({\n id: entity.id,\n entity: entity,\n geometry: entity.geometry(graph),\n type: type,\n name: name,\n distance: distance\n });\n\n if (localResults.length > 100) break;\n }\n localResults.sort((a, b) => a.distance - b.distance);\n\n const geocodeResults = [];\n (_geocodeResults || []).forEach(function(d) {\n if (d.osm_type && d.osm_id) { // some results may be missing these - #1890\n\n // Make a temporary osmEntity so we can preset match\n // and better localize the search result - #4725\n var id = osmEntity.id.fromOSM(d.osm_type, d.osm_id);\n var tags = {};\n tags[d.class] = d.type;\n\n var attrs = { id: id, type: d.osm_type, tags: tags };\n if (d.osm_type === 'way') { // for ways, add some fake closed nodes\n attrs.nodes = ['a','a']; // so that geometry area is possible\n }\n\n var tempEntity = osmEntity(attrs);\n var tempGraph = coreGraph([tempEntity]);\n var matched = presetManager.match(tempEntity, tempGraph);\n var type = (matched && matched.name()) || utilDisplayType(id);\n\n geocodeResults.push({\n id: tempEntity.id,\n geometry: tempEntity.geometry(tempGraph),\n type: type,\n name: d.display_name,\n extent: new geoExtent(\n [Number(d.boundingbox[3]), Number(d.boundingbox[0])],\n [Number(d.boundingbox[2]), Number(d.boundingbox[1])])\n });\n }\n });\n\n const extraResults = [];\n if (q.match(/^[0-9]+$/)) {\n // if query is just a number, possibly an OSM ID without a prefix\n extraResults.push({\n id: 'n' + q,\n geometry: 'point',\n type: t('inspector.node'),\n name: q\n });\n extraResults.push({\n id: 'w' + q,\n geometry: 'line',\n type: t('inspector.way'),\n name: q\n });\n extraResults.push({\n id: 'r' + q,\n geometry: 'relation',\n type: t('inspector.relation'),\n name: q\n });\n extraResults.push({\n id: 'note' + q,\n geometry: 'note',\n type: t('note.note'),\n name: q\n });\n }\n\n return [...idResult, ...localResults, ...coordResult, ...geocodeResults, ...extraResults];\n }\n\n\n function drawList() {\n var value = search.property('value');\n var results = features();\n\n list.classed('filtered', value.length);\n\n var resultsIndicator = list.selectAll('.no-results-item')\n .data([0])\n .enter()\n .append('button')\n .property('disabled', true)\n .attr('class', 'no-results-item')\n .call(svgIcon('#iD-icon-alert', 'pre-text'));\n\n resultsIndicator.append('span')\n .attr('class', 'entity-name');\n\n list.selectAll('.no-results-item .entity-name')\n .html('')\n .call(t.append('geocoder.no_results_worldwide'));\n\n if (services.geocoder) {\n list.selectAll('.geocode-item')\n .data([0])\n .enter()\n .append('button')\n .attr('class', 'geocode-item secondary-action')\n .on('click', geocoderSearch)\n .append('div')\n .attr('class', 'label')\n .append('span')\n .attr('class', 'entity-name')\n .call(t.append('geocoder.search'));\n }\n\n list.selectAll('.no-results-item')\n .style('display', (value.length && !results.length) ? 'block' : 'none');\n\n list.selectAll('.geocode-item')\n .style('display', (value && _geocodeResults === undefined) ? 'block' : 'none');\n\n var items = list.selectAll('.feature-list-item')\n .data(results, function(d) { return d.id; });\n\n var enter = items.enter()\n .insert('button', '.geocode-item')\n .attr('class', 'feature-list-item')\n .on('pointerenter', mouseover)\n .on('pointerleave', mouseout)\n .on('focus', mouseover)\n .on('blur', mouseout)\n .on('click', click);\n\n var label = enter\n .append('div')\n .attr('class', 'label')\n .attr('title', d => d.name);\n\n label\n .each(function(d) {\n d3_select(this)\n .call(svgIcon('#iD-icon-' + d.geometry, 'pre-text'));\n });\n\n label\n .append('span')\n .attr('class', 'entity-type')\n .text(function(d) { return d.type; });\n\n label.each(function(d) {\n if (d.entity?.type !== 'relation') return;\n\n const hasRef = d.entity.tags.ref;\n const relColors = getRelationColor(d.entity.tags, '#555');\n if (relColors.isValid || hasRef) {\n const refs = (d.entity.tags.ref || '').split(';');\n for (const ref of refs) {\n d3_select(this)\n .append('span')\n .classed('member-entity-ref-color', true)\n .style('border-color', relColors.color)\n .style('background-color', relColors.color)\n .style('color', relColors.textColor)\n .text(ref);\n }\n }\n });\n\n label\n .append('span')\n .attr('class', 'entity-name')\n .text(d => d.name);\n\n enter\n .style('opacity', 0)\n .transition()\n .style('opacity', 1);\n\n items.exit()\n .each(d => mouseout(undefined, d))\n .remove();\n\n items.merge(enter)\n .order();\n }\n\n\n function mouseover(d3_event, d) {\n if (d.location !== undefined) return;\n\n utilHighlightEntities([d.id], true, context);\n }\n\n\n function mouseout(d3_event, d) {\n if (d.location !== undefined) return;\n\n utilHighlightEntities([d.id], false, context);\n }\n\n\n function click(d3_event, d) {\n d3_event.preventDefault();\n\n if (d.location) {\n context.map().centerZoomEase([d.location[1], d.location[0]], d.zoom || 19);\n\n } else if (d.entity) {\n utilHighlightEntities([d.id], false, context);\n\n context.enter(modeSelect(context, [d.entity.id]));\n context.map().zoomToEase(d.entity);\n\n } else if (d.geometry === 'note') {\n // note\n // get number part 'note12345'\n const noteId = d.id.replace(/\\D/g, '');\n\n // load note\n context.moveToNote(noteId);\n } else {\n // download, zoom to, and select the entity with the given ID\n context.zoomToEntity(d.id);\n }\n }\n\n\n function geocoderSearch() {\n services.geocoder.search(search.property('value'), function (err, resp) {\n _geocodeResults = resp || [];\n drawList();\n });\n }\n }\n\n\n return featureList;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { svgIcon } from '../../svg/icon';\nimport { utilArrayIdentical } from '../../util/array';\nimport { t } from '../../core/localizer';\nimport { utilHighlightEntities } from '../../util';\nimport { uiSection } from '../section';\nimport { validationIssue } from '../../core/validation';\n\n\nexport function uiSectionEntityIssues(context) {\n // Does the user prefer to expand the active issue? Useful for viewing tag diff.\n // Expand by default so first timers see it - #6408, #8143\n var preference = prefs('entity-issues.reference.expanded');\n var _expanded = preference === null ? true : (preference === 'true');\n\n var _entityIDs = [];\n var _issues = [];\n var _activeIssueID;\n\n\n var section = uiSection('entity-issues', context)\n .shouldDisplay(function() {\n return _issues.length > 0;\n })\n .label(function() {\n return t.append('inspector.title_count', { title: t.append('issues.list_title'), count: _issues.length });\n })\n .disclosureContent(renderDisclosureContent);\n\n context.validator()\n .on('validated.entity_issues', function() {\n // Refresh on validated events\n reloadIssues();\n section.reRender();\n })\n .on('focusedIssue.entity_issues', function(issue) {\n makeActiveIssue(issue.id);\n });\n\n function reloadIssues() {\n _issues = context.validator().getSharedEntityIssues(_entityIDs, { includeDisabledRules: true });\n }\n\n function makeActiveIssue(issueID) {\n _activeIssueID = issueID;\n section.selection().selectAll('.issue-container')\n .classed('active', function(d) { return d.id === _activeIssueID; });\n }\n\n function renderDisclosureContent(selection) {\n\n selection.classed('grouped-items-area', true);\n\n _activeIssueID = _issues.length > 0 ? _issues[0].id : null;\n\n var containers = selection.selectAll('.issue-container')\n .data(_issues, function(d) { return d.key; });\n\n // Exit\n containers.exit()\n .remove();\n\n // Enter\n var containersEnter = containers.enter()\n .append('div')\n .attr('class', 'issue-container');\n\n\n var itemsEnter = containersEnter\n .append('div')\n .attr('class', function(d) { return 'issue severity-' + d.severity; })\n .on('mouseover.highlight', function(d3_event, d) {\n // don't hover-highlight the selected entity\n var ids = d.entityIds\n .filter(function(e) { return _entityIDs.indexOf(e) === -1; });\n\n utilHighlightEntities(ids, true, context);\n })\n .on('mouseout.highlight', function(d3_event, d) {\n var ids = d.entityIds\n .filter(function(e) { return _entityIDs.indexOf(e) === -1; });\n\n utilHighlightEntities(ids, false, context);\n });\n\n var labelsEnter = itemsEnter\n .append('div')\n .attr('class', 'issue-label');\n\n var textEnter = labelsEnter\n .append('button')\n .attr('class', 'issue-text')\n .on('click', function(d3_event, d) {\n makeActiveIssue(d.id); // expand only the clicked item\n\n const extent = d.extent(context.graph());\n if (extent) {\n context.map().zoomToEase(extent);\n }\n });\n\n textEnter\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(validationIssue.ICONS[d.severity], 'issue-icon'));\n });\n\n textEnter\n .append('span')\n .attr('class', 'issue-message');\n\n\n var infoButton = labelsEnter\n .append('button')\n .attr('class', 'issue-info-button')\n .attr('title', t('icons.information'))\n .call(svgIcon('#iD-icon-inspect'));\n\n infoButton\n .on('click', function (d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n this.blur(); // avoid keeping focus on the button - #4641\n\n var container = d3_select(this.parentNode.parentNode.parentNode);\n var info = container.selectAll('.issue-info');\n var isExpanded = info.classed('expanded');\n _expanded = !isExpanded;\n prefs('entity-issues.reference.expanded', _expanded); // update preference\n\n if (isExpanded) {\n info\n .transition()\n .duration(200)\n .style('max-height', '0px')\n .style('opacity', '0')\n .on('end', function () {\n info.classed('expanded', false);\n });\n } else {\n info\n .classed('expanded', true)\n .transition()\n .duration(200)\n .style('max-height', '200px')\n .style('opacity', '1')\n .on('end', function () {\n info.style('max-height', null);\n });\n }\n });\n\n itemsEnter\n .append('ul')\n .attr('class', 'issue-fix-list');\n\n containersEnter\n .append('div')\n .attr('class', 'issue-info' + (_expanded ? ' expanded' : ''))\n .style('max-height', (_expanded ? null : '0'))\n .style('opacity', (_expanded ? '1' : '0'))\n .each(function(d) {\n if (typeof d.reference === 'function') {\n d3_select(this)\n .call(d.reference);\n } else {\n d3_select(this)\n .call(t.append('inspector.no_documentation_key'));\n }\n });\n\n\n // Update\n containers = containers\n .merge(containersEnter)\n .classed('active', function(d) { return d.id === _activeIssueID; });\n\n containers.selectAll('.issue-message')\n .text('')\n .each(function(d) {\n return d.message(context)(d3_select(this));\n });\n\n // fixes\n var fixLists = containers.selectAll('.issue-fix-list');\n\n var fixes = fixLists.selectAll('.issue-fix-item')\n .data(function(d) { return d.fixes ? d.fixes(context) : []; }, function(fix) { return fix.id; });\n\n fixes.exit()\n .remove();\n\n var fixesEnter = fixes.enter()\n .append('li')\n .attr('class', 'issue-fix-item');\n\n var buttons = fixesEnter\n .append('button')\n .on('click', function(d3_event, d) {\n // not all fixes are actionable\n if (d3_select(this).attr('disabled') || !d.onClick) return;\n\n // Don't run another fix for this issue within a second of running one\n // (Necessary for \"Select a feature type\" fix. Most fixes should only ever run once)\n if (d.issue.dateLastRanFix && new Date() - d.issue.dateLastRanFix < 1000) return;\n d.issue.dateLastRanFix = new Date();\n\n // remove hover-highlighting\n utilHighlightEntities(d.issue.entityIds.concat(d.entityIds), false, context);\n\n new Promise(function(resolve, reject) {\n d.onClick(context, resolve, reject);\n if (d.onClick.length <= 1) {\n // if the fix doesn't take any completion parameters then consider it resolved\n resolve();\n }\n })\n .then(function() {\n // revalidate whenever the fix has finished running successfully\n context.validator().validate();\n });\n })\n .on('mouseover.highlight', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, true, context);\n })\n .on('mouseout.highlight', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, false, context);\n });\n\n buttons\n .each(function(d) {\n var iconName = d.icon || 'iD-icon-wrench';\n if (iconName.startsWith('maki')) {\n iconName += '-15';\n }\n d3_select(this).call(svgIcon('#' + iconName, 'fix-icon'));\n });\n\n buttons\n .append('span')\n .attr('class', 'fix-message')\n .each(function(d) { return d.title(d3_select(this)); });\n\n fixesEnter.merge(fixes)\n .selectAll('button')\n .classed('actionable', function(d) {\n return d.onClick;\n })\n .attr('disabled', function(d) {\n return d.onClick ? null : 'true';\n })\n .attr('title', function(d) {\n if (d.disabledReason) {\n return d.disabledReason;\n }\n return null;\n });\n }\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n if (!_entityIDs || !val || !utilArrayIdentical(_entityIDs, val)) {\n _entityIDs = val;\n _activeIssueID = null;\n reloadIssues();\n }\n return section;\n };\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { prefs } from '../core/preferences';\nimport { svgIcon, svgTagClasses } from '../svg';\nimport { utilFunctor } from '../util';\n\n\nexport function uiPresetIcon() {\n let _preset;\n let _geometry;\n\n\n function presetIcon(selection) {\n selection.each(render);\n }\n\n\n function getIcon(p, geom) {\n if (p.isFallback && p.isFallback()) return geom === 'vertex' ? '' : 'iD-icon-' + p.id;\n if (p.icon) return p.icon;\n if (geom === 'line') return 'iD-other-line';\n if (geom === 'vertex') return 'temaki-vertex';\n return 'maki-marker-stroked';\n }\n\n\n function renderPointBorder(container, drawPoint) {\n const pointBorder = container.selectAll('.preset-icon-point-border')\n .data(drawPoint ? [0] : []);\n\n pointBorder.exit()\n .remove();\n\n const pointBorderEnter = pointBorder.enter();\n\n const w = 40;\n const h = 40;\n\n pointBorderEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-point-border')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`)\n .append('path')\n .attr('transform', 'translate(11.5, 8)')\n .attr('d', 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z');\n }\n\n\n function renderCategoryBorder(container, category) {\n const categoryBorder = container.selectAll('.preset-icon-category-border')\n .data(category ? [0] : []);\n\n categoryBorder.exit()\n .remove();\n\n const categoryBorderEnter = categoryBorder.enter();\n\n const d = 60;\n\n let svgEnter = categoryBorderEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-category-border')\n .attr('width', d)\n .attr('height', d)\n .attr('viewBox', `0 0 ${d} ${d}`);\n\n svgEnter\n .append('path')\n .attr('class', 'area')\n .attr('d', 'M9.5,7.5 L25.5,7.5 L28.5,12.5 L49.5,12.5 C51.709139,12.5 53.5,14.290861 53.5,16.5 L53.5,43.5 C53.5,45.709139 51.709139,47.5 49.5,47.5 L10.5,47.5 C8.290861,47.5 6.5,45.709139 6.5,43.5 L6.5,12.5 L9.5,7.5 Z');\n\n if (category) {\n categoryBorderEnter.merge(categoryBorder)\n .selectAll('path')\n .attr('class', `area ${category.id}`);\n }\n }\n\n\n function renderCircleFill(container, drawVertex) {\n const vertexFill = container.selectAll('.preset-icon-fill-vertex')\n .data(drawVertex ? [0] : []);\n\n vertexFill.exit()\n .remove();\n\n const vertexFillEnter = vertexFill.enter();\n\n const w = 60;\n const h = 60;\n const d = 40;\n\n vertexFillEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-fill-vertex')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`)\n .append('circle')\n .attr('cx', w / 2)\n .attr('cy', h / 2)\n .attr('r', d / 2);\n }\n\n\n function renderSquareFill(container, drawArea, tagClasses) {\n\n let fill = container.selectAll('.preset-icon-fill-area')\n .data(drawArea ? [0] : []);\n\n fill.exit()\n .remove();\n\n let fillEnter = fill.enter();\n\n const d = 60;\n const w = d;\n const h = d;\n const l = d * 2/3;\n const c1 = (w-l) / 2;\n const c2 = c1 + l;\n\n fillEnter = fillEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-fill-area')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`);\n\n ['fill', 'stroke'].forEach(klass => {\n fillEnter\n .append('path')\n .attr('d', `M${c1} ${c1} L${c1} ${c2} L${c2} ${c2} L${c2} ${c1} Z`)\n .attr('class', `area ${klass}`);\n });\n\n const rVertex = 2.5;\n [[c1, c1], [c1, c2], [c2, c2], [c2, c1]].forEach(point => {\n fillEnter\n .append('circle')\n .attr('class', 'vertex')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', rVertex);\n });\n\n const rMidpoint = 1.25;\n [[c1, w/2], [c2, w/2], [h/2, c1], [h/2, c2]].forEach(point => {\n fillEnter\n .append('circle')\n .attr('class', 'midpoint')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', rMidpoint);\n });\n\n fill = fillEnter.merge(fill);\n\n fill.selectAll('path.stroke')\n .attr('class', `area stroke ${tagClasses}`);\n fill.selectAll('path.fill')\n .attr('class', `area fill ${tagClasses}`);\n }\n\n\n function renderLine(container, drawLine, tagClasses) {\n\n let line = container.selectAll('.preset-icon-line')\n .data(drawLine ? [0] : []);\n\n line.exit()\n .remove();\n\n let lineEnter = line.enter();\n\n const d = 60;\n // draw the line parametrically\n const w = d;\n const h = d;\n const y = Math.round(d * 0.72);\n const l = Math.round(d * 0.6);\n const r = 2.5;\n const x1 = (w - l) / 2;\n const x2 = x1 + l;\n\n lineEnter = lineEnter\n .append('svg')\n .attr('class', 'preset-icon-line')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`);\n\n ['casing', 'stroke'].forEach(klass => {\n lineEnter\n .append('path')\n .attr('d', `M${x1} ${y} L${x2} ${y}`)\n .attr('class', `line ${klass}`);\n });\n\n [[x1-1, y], [x2+1, y]].forEach(point => {\n lineEnter\n .append('circle')\n .attr('class', 'vertex')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', r);\n });\n\n line = lineEnter.merge(line);\n\n line.selectAll('path.stroke')\n .attr('class', `line stroke ${tagClasses}`);\n line.selectAll('path.casing')\n .attr('class', `line casing ${tagClasses}`);\n }\n\n\n function renderRoute(container, drawRoute, p) {\n\n let route = container.selectAll('.preset-icon-route')\n .data(drawRoute ? [0] : []);\n\n route.exit()\n .remove();\n\n let routeEnter = route.enter();\n\n const d = 60;\n // draw the route parametrically\n const w = d;\n const h = d;\n const y1 = Math.round(d * 0.80);\n const y2 = Math.round(d * 0.68);\n const l = Math.round(d * 0.6);\n const r = 2;\n const x1 = (w - l) / 2;\n const x2 = x1 + l / 3;\n const x3 = x2 + l / 3;\n const x4 = x3 + l / 3;\n\n routeEnter = routeEnter\n .append('svg')\n .attr('class', 'preset-icon-route')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`);\n\n ['casing', 'stroke'].forEach(klass => {\n routeEnter\n .append('path')\n .attr('d', `M${x1} ${y1} L${x2} ${y2}`)\n .attr('class', `segment0 line ${klass}`);\n routeEnter\n .append('path')\n .attr('d', `M${x2} ${y2} L${x3} ${y1}`)\n .attr('class', `segment1 line ${klass}`);\n routeEnter\n .append('path')\n .attr('d', `M${x3} ${y1} L${x4} ${y2}`)\n .attr('class', `segment2 line ${klass}`);\n });\n\n [[x1, y1], [x2, y2], [x3, y1], [x4, y2]].forEach(point => {\n routeEnter\n .append('circle')\n .attr('class', 'vertex')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', r);\n });\n\n route = routeEnter.merge(route);\n\n if (drawRoute) {\n let routeType = p.tags.type === 'waterway' ? 'waterway' : p.tags.route;\n const segmentPresetIDs = routeSegments[routeType];\n for (let i in segmentPresetIDs) {\n const segmentPreset = presetManager.item(segmentPresetIDs[i]);\n const segmentTagClasses = svgTagClasses().getClassesString(segmentPreset.tags, '');\n route.selectAll(`path.stroke.segment${i}`)\n .attr('class', `segment${i} line stroke ${segmentTagClasses}`);\n route.selectAll(`path.casing.segment${i}`)\n .attr('class', `segment${i} line casing ${segmentTagClasses}`);\n }\n }\n }\n\n function renderSvgIcon(container, picon, geom, isFramed, category, tagClasses) {\n const isMaki = picon && /^maki-/.test(picon);\n const isTemaki = picon && /^temaki-/.test(picon);\n const isFa = picon && /^fa[srb]-/.test(picon);\n const isR\u00F6ntgen = picon && /^roentgen-/.test(picon);\n const isiDIcon = picon && !(isMaki || isTemaki || isFa || isR\u00F6ntgen);\n\n let icon = container.selectAll('.preset-icon')\n .data(picon ? [0] : []);\n\n icon.exit()\n .remove();\n\n icon = icon.enter()\n .append('div')\n .attr('class', 'preset-icon')\n .call(svgIcon(''))\n .merge(icon);\n\n icon\n .attr('class', 'preset-icon ' + (geom ? geom + '-geom' : ''))\n .classed('category', category)\n .classed('framed', isFramed)\n .classed('preset-icon-iD', isiDIcon);\n\n icon.selectAll('svg')\n .attr('class', 'icon ' + picon + ' ' + (!isiDIcon && geom !== 'line' ? '' : tagClasses));\n\n icon.selectAll('use')\n .attr('href', '#' + picon);\n }\n\n\n function renderImageIcon(container, imageURL) {\n let imageIcon = container.selectAll('img.image-icon')\n .data(imageURL ? [0] : []);\n\n imageIcon.exit()\n .remove();\n\n imageIcon = imageIcon.enter()\n .append('img')\n .attr('class', 'image-icon')\n .on('load', () => container.classed('showing-img', true) )\n .on('error', () => container.classed('showing-img', false) )\n .merge(imageIcon);\n\n imageIcon\n .attr('src', imageURL);\n }\n\n // Route icons are drawn with a zigzag annotation underneath:\n // o o\n // / \\ /\n // o o\n // This dataset defines the styles that are used to draw the zigzag segments.\n const routeSegments = {\n bicycle: ['highway/cycleway', 'highway/cycleway', 'highway/cycleway'],\n bus: ['highway/unclassified', 'highway/secondary', 'highway/primary'],\n climbing: ['climbing/route', 'climbing/route', 'climbing/route'],\n trolleybus: ['highway/unclassified', 'highway/secondary', 'highway/primary'],\n detour: ['highway/tertiary', 'highway/residential', 'highway/unclassified'],\n ferry: ['route/ferry', 'route/ferry', 'route/ferry'],\n foot: ['highway/footway', 'highway/footway', 'highway/footway'],\n hiking: ['highway/path', 'highway/path', 'highway/path'],\n horse: ['highway/bridleway', 'highway/bridleway', 'highway/bridleway'],\n light_rail: ['railway/light_rail', 'railway/light_rail', 'railway/light_rail'],\n monorail: ['railway/monorail', 'railway/monorail', 'railway/monorail'],\n mtb: ['highway/path', 'highway/track', 'highway/bridleway'],\n pipeline: ['man_made/pipeline', 'man_made/pipeline', 'man_made/pipeline'],\n piste: ['piste/downhill', 'piste/hike', 'piste/nordic'],\n power: ['power/line', 'power/line', 'power/line'],\n road: ['highway/secondary', 'highway/primary', 'highway/trunk'],\n subway: ['railway/subway', 'railway/subway', 'railway/subway'],\n train: ['railway/rail', 'railway/rail', 'railway/rail'],\n tram: ['railway/tram', 'railway/tram', 'railway/tram'],\n railway: ['railway/rail', 'railway/rail', 'railway/rail'],\n waterway: ['waterway/stream', 'waterway/stream', 'waterway/stream']\n };\n\n\n function render() {\n let p = _preset.apply(this, arguments);\n let geom = _geometry ? _geometry.apply(this, arguments) : null;\n if (geom === 'relation' &&\n p.tags &&\n ((p.tags.type === 'route' && p.tags.route && routeSegments[p.tags.route]) || p.tags.type === 'waterway')) {\n geom = 'route';\n }\n\n const showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n const isFallback = p.isFallback && p.isFallback();\n const imageURL = (showThirdPartyIcons === 'true') && p.imageURL;\n const picon = getIcon(p, geom);\n const isCategory = !p.setTags;\n const drawPoint = false;\n const drawVertex = picon !== null && geom === 'vertex';\n const drawLine = picon && geom === 'line' && !isFallback && !isCategory;\n const drawArea = picon && geom === 'area' && !isFallback && !isCategory;\n const drawRoute = picon && geom === 'route';\n const isFramed = drawVertex || drawArea || drawLine || drawRoute || isCategory;\n\n let tags = !isCategory ? p.setTags({}, geom) : {};\n for (let k in tags) {\n if (tags[k] === '*') {\n tags[k] = 'yes';\n }\n }\n\n let tagClasses = svgTagClasses().getClassesString(tags, '');\n let selection = d3_select(this);\n\n let container = selection.selectAll('.preset-icon-container')\n .data([0]);\n\n container = container.enter()\n .append('div')\n .attr('class', 'preset-icon-container')\n .merge(container);\n\n container\n .classed('showing-img', !!imageURL)\n .classed('fallback', isFallback);\n\n renderCategoryBorder(container, isCategory && p);\n renderPointBorder(container, drawPoint);\n renderCircleFill(container, drawVertex);\n renderSquareFill(container, drawArea, tagClasses);\n renderLine(container, drawLine, tagClasses);\n renderRoute(container, drawRoute, p);\n renderSvgIcon(container, picon, geom, isFramed, isCategory, tagClasses);\n renderImageIcon(container, imageURL);\n }\n\n\n presetIcon.preset = function(val) {\n if (!arguments.length) return _preset;\n _preset = utilFunctor(val);\n return presetIcon;\n };\n\n\n presetIcon.geometry = function(val) {\n if (!arguments.length) return _geometry;\n _geometry = utilFunctor(val);\n return presetIcon;\n };\n\n return presetIcon;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { utilArrayIdentical } from '../../util/array';\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { utilRebind } from '../../util';\nimport { uiPresetIcon } from '../preset_icon';\nimport { uiSection } from '../section';\nimport { uiTagReference } from '../tag_reference';\n\n\nexport function uiSectionFeatureType(context) {\n\n var dispatch = d3_dispatch('choose');\n\n var _entityIDs = [];\n var _presets = [];\n\n var _tagReference;\n\n var section = uiSection('feature-type', context)\n .label(() => t.append('inspector.feature_type'))\n .disclosureContent(renderDisclosureContent);\n\n function renderDisclosureContent(selection) {\n\n selection.classed('preset-list-item', true);\n selection.classed('mixed-types', _presets.length > 1);\n\n var presetButtonWrap = selection\n .selectAll('.preset-list-button-wrap')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'preset-list-button-wrap');\n\n var presetButton = presetButtonWrap\n .append('button')\n .attr('class', 'preset-list-button preset-reset')\n .call(uiTooltip()\n .title(() => t.append('inspector.back_tooltip'))\n .placement('bottom')\n );\n\n presetButton.append('div')\n .attr('class', 'preset-icon-container');\n\n presetButton\n .append('div')\n .attr('class', 'label')\n .append('div')\n .attr('class', 'label-inner');\n\n presetButtonWrap.append('div')\n .attr('class', 'accessory-buttons');\n\n var tagReferenceBodyWrap = selection\n .selectAll('.tag-reference-body-wrap')\n .data([0]);\n\n tagReferenceBodyWrap = tagReferenceBodyWrap\n .enter()\n .append('div')\n .attr('class', 'tag-reference-body-wrap')\n .merge(tagReferenceBodyWrap);\n\n // update header\n if (_tagReference) {\n selection.selectAll('.preset-list-button-wrap .accessory-buttons')\n .style('display', _presets.length === 1 ? null : 'none')\n .call(_tagReference.button);\n\n tagReferenceBodyWrap\n .style('display', _presets.length === 1 ? null : 'none')\n .call(_tagReference.body);\n }\n\n selection.selectAll('.preset-reset')\n .on('click', function() {\n dispatch.call('choose', this, _presets);\n })\n .on('pointerdown pointerup mousedown mouseup', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n });\n\n var geometries = entityGeometries();\n selection.select('.preset-list-item button')\n .call(uiPresetIcon()\n .geometry(_presets.length === 1 ? (geometries.length === 1 && geometries[0]) : null)\n .preset(_presets.length === 1 ? _presets[0] : presetManager.item('point'))\n );\n\n var names = _presets.length === 1 ? [\n _presets[0].nameLabel(),\n _presets[0].subtitleLabel()\n ].filter(Boolean) : [ t.append('inspector.multiple_types') ];\n\n var label = selection.select('.label-inner');\n var nameparts = label.selectAll('.namepart')\n .data(names, d => d.stringId);\n\n nameparts.exit()\n .remove();\n\n nameparts\n .enter()\n .append('div')\n .attr('class', 'namepart')\n .text('')\n .each(function(d) { d(d3_select(this)); });\n }\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return section;\n };\n\n section.presets = function(val) {\n if (!arguments.length) return _presets;\n\n // don't reload the same preset\n if (!utilArrayIdentical(val, _presets)) {\n _presets = val;\n\n if (_presets.length === 1) {\n _tagReference = uiTagReference(_presets[0].reference(), context)\n .showing(false);\n }\n }\n\n return section;\n };\n\n function entityGeometries() {\n\n var counts = {};\n\n for (var i in _entityIDs) {\n var geometry = context.graph().geometry(_entityIDs[i]);\n if (!counts[geometry]) counts[geometry] = 0;\n counts[geometry] += 1;\n }\n\n return Object.keys(counts).sort(function(geom1, geom2) {\n return counts[geom2] - counts[geom1];\n });\n }\n\n return utilRebind(section, dispatch, 'on');\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { uiCombobox } from './combobox';\nimport { utilGetSetValue, utilNoAuto } from '../util';\n\n\nexport function uiFormFields(context) {\n var moreCombo = uiCombobox(context, 'more-fields').minItems(1);\n var _fieldsArr = [];\n var _lastPlaceholder = '';\n var _state = '';\n var _klass = '';\n\n\n function formFields(selection) {\n var allowedFields = _fieldsArr.filter(function(field) { return field.isAllowed(); });\n var shown = allowedFields.filter(function(field) { return field.isShown(); });\n var notShown = allowedFields.filter(function(field) { return !field.isShown(); })\n .sort(function(a, b) { return (a.universal === b.universal ? 0 : a.universal ? 1 : -1); });\n\n var container = selection.selectAll('.form-fields-container')\n .data([0]);\n\n container = container.enter()\n .append('div')\n .attr('class', 'form-fields-container ' + (_klass || ''))\n .merge(container);\n\n\n var fields = container.selectAll('.wrap-form-field')\n .data(shown, function(d) { return d.id + (d.entityIDs ? d.entityIDs.join() : ''); });\n\n fields.exit()\n .remove();\n\n // Enter\n var enter = fields.enter()\n .append('div')\n .attr('class', function(d) { return 'wrap-form-field wrap-form-field-' + d.safeid; });\n\n // Update\n fields = fields\n .merge(enter);\n\n fields\n .order()\n .each(function(d) {\n d3_select(this)\n .call(d.render);\n });\n\n\n var titles = [];\n var moreFields = notShown.map(function(field) {\n var title = field.title();\n titles.push(title);\n\n var terms = field.terms();\n if (field.key) terms.push(field.key);\n if (field.keys) terms = terms.concat(field.keys);\n\n return {\n display: field.label(),\n value: title,\n title: title,\n field: field,\n terms: terms\n };\n });\n\n var placeholder = titles.slice(0,3).join(', ') + ((titles.length > 3) ? '\u2026' : '');\n\n\n var more = selection.selectAll('.more-fields')\n .data((_state === 'hover' || moreFields.length === 0) ? [] : [0]);\n\n more.exit()\n .remove();\n\n var moreEnter = more.enter()\n .append('div')\n .attr('class', 'more-fields')\n .append('label');\n\n moreEnter\n .append('span')\n .call(t.append('inspector.add_fields'));\n\n more = moreEnter\n .merge(more);\n\n\n var input = more.selectAll('.value')\n .data([0]);\n\n input.exit()\n .remove();\n\n input = input.enter()\n .append('input')\n .attr('class', 'value')\n .attr('type', 'text')\n .attr('placeholder', placeholder)\n .call(utilNoAuto)\n .merge(input);\n\n input\n .call(utilGetSetValue, '')\n .call(moreCombo\n .data(moreFields)\n .on('accept', function (d) {\n if (!d) return; // user entered something that was not matched\n var field = d.field;\n field.show();\n selection.call(formFields); // rerender\n field.focus();\n })\n );\n\n // avoid updating placeholder excessively (triggers style recalc)\n if (_lastPlaceholder !== placeholder) {\n input.attr('placeholder', placeholder);\n _lastPlaceholder = placeholder;\n }\n }\n\n\n formFields.fieldsArr = function(val) {\n if (!arguments.length) return _fieldsArr;\n _fieldsArr = val || [];\n return formFields;\n };\n\n formFields.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n return formFields;\n };\n\n formFields.klass = function(val) {\n if (!arguments.length) return _klass;\n _klass = val;\n return formFields;\n };\n\n\n return formFields;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { presetManager } from '../../presets';\nimport { t, localizer } from '../../core/localizer';\nimport { utilArrayIdentical } from '../../util/array';\nimport { utilArrayUnion, utilRebind } from '../../util';\nimport { geoExtent } from '../../geo/extent';\nimport { uiField } from '../field';\nimport { uiFormFields } from '../form_fields';\nimport { uiSection } from '../section';\n\nexport function uiSectionPresetFields(context) {\n\n var section = uiSection('preset-fields', context)\n .label(() => t.append('inspector.fields'))\n .disclosureContent(renderDisclosureContent);\n\n var dispatch = d3_dispatch('change', 'revert');\n var formFields = uiFormFields(context);\n var _state;\n var _fieldsArr;\n var _presets = [];\n var _tags;\n var _entityIDs;\n\n function renderDisclosureContent(selection) {\n if (!_fieldsArr) {\n\n var graph = context.graph();\n\n var geometries = Object.keys(_entityIDs.reduce(function(geoms, entityID) {\n geoms[graph.entity(entityID).geometry(graph)] = true;\n return geoms;\n }, {}));\n\n const loc = _entityIDs.reduce(function(extent, entityID) {\n var entity = context.graph().entity(entityID);\n return extent.extend(entity.extent(context.graph()));\n }, geoExtent()).center();\n\n var presetsManager = presetManager;\n\n var allFields = [];\n var allMoreFields = [];\n var sharedTotalFields;\n\n _presets.forEach(function(preset) {\n var fields = preset.fields(loc);\n var moreFields = preset.moreFields(loc);\n\n allFields = utilArrayUnion(allFields, fields);\n allMoreFields = utilArrayUnion(allMoreFields, moreFields);\n\n if (!sharedTotalFields) {\n sharedTotalFields = utilArrayUnion(fields, moreFields);\n } else {\n sharedTotalFields = sharedTotalFields.filter(function(field) {\n return fields.indexOf(field) !== -1 || moreFields.indexOf(field) !== -1;\n });\n }\n });\n\n var sharedFields = allFields.filter(function(field) {\n return sharedTotalFields.indexOf(field) !== -1;\n });\n var sharedMoreFields = allMoreFields.filter(function(field) {\n return sharedTotalFields.indexOf(field) !== -1;\n });\n\n _fieldsArr = [];\n\n // Ideally, everything in OpenHistoricalMap is dated and sourced.\n let coreKeys = ['start_date', 'end_date', 'source_preset'];\n coreKeys.forEach(key => {\n let field = presetsManager.field(key);\n if (field) {\n _fieldsArr.push(uiField(context, field, _entityIDs));\n }\n });\n\n let optionalCoreKeys = ['source_preset:1', 'source_preset:2', 'source_preset:3'];\n optionalCoreKeys.forEach(key => {\n let field = presetsManager.field(key);\n if (field && !_fieldsArr.includes(field)) {\n _fieldsArr.push(uiField(context, field, _entityIDs, { show: false }));\n }\n });\n\n\n sharedFields.forEach(function(field) {\n if (!coreKeys.includes(field.id) && !optionalCoreKeys.includes(field.id) && field.matchAllGeometry(geometries)) {\n _fieldsArr.push(\n uiField(context, field, _entityIDs)\n );\n }\n });\n\n var singularEntity = _entityIDs.length === 1 && graph.hasEntity(_entityIDs[0]);\n if (singularEntity && singularEntity.type === 'node' && singularEntity.isHighwayIntersection(graph) && presetsManager.field('restrictions')) {\n _fieldsArr.push(\n uiField(context, presetsManager.field('restrictions'), _entityIDs)\n );\n }\n\n var additionalFields = utilArrayUnion(sharedMoreFields, presetsManager.universal());\n additionalFields.sort(function(field1, field2) {\n return field1.title().localeCompare(field2.title(), localizer.localeCode());\n });\n\n additionalFields.forEach(function(field) {\n if (sharedFields.indexOf(field) === -1 &&\n !coreKeys.includes(field.id) &&\n !optionalCoreKeys.includes(field.id) &&\n field.matchAllGeometry(geometries)) {\n _fieldsArr.push(\n uiField(context, field, _entityIDs, { show: false })\n );\n }\n });\n\n _fieldsArr.forEach(function(field) {\n field\n .on('change', function(t, onInput) {\n dispatch.call('change', field, _entityIDs, t, onInput);\n })\n .on('revert', function(keys) {\n dispatch.call('revert', field, keys);\n });\n });\n }\n\n _fieldsArr.forEach(function(field) {\n field\n .state(_state)\n .tags(_tags);\n });\n\n\n selection\n .call(formFields\n .fieldsArr(_fieldsArr)\n .state(_state)\n .klass('grouped-items-area')\n );\n }\n\n section.presets = function(val) {\n if (!arguments.length) return _presets;\n if (!_presets || !val || !utilArrayIdentical(_presets, val)) {\n _presets = val;\n _fieldsArr = null;\n }\n return section;\n };\n\n section.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n return section;\n };\n\n section.tags = function(val) {\n if (!arguments.length) return _tags;\n _tags = val;\n // Don't reset _fieldsArr here.\n return section;\n };\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n if (!val || !_entityIDs || !utilArrayIdentical(_entityIDs, val)) {\n _entityIDs = val;\n _fieldsArr = null;\n }\n return section;\n };\n\n return utilRebind(section, dispatch, 'on');\n}\n", "import { drag as d3_drag } from 'd3-drag';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { actionChangeMember } from '../../actions/change_member';\nimport { actionDeleteMember } from '../../actions/delete_member';\nimport { actionMoveMember } from '../../actions/move_member';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { osmEntity } from '../../osm';\nimport { getRelationColor } from '../../osm/tags';\nimport { svgIcon } from '../../svg/icon';\nimport { services } from '../../services';\nimport { uiCombobox } from '../combobox';\nimport { uiSection } from '../section';\nimport { utilDisplayName, utilDisplayType, utilHighlightEntities, utilNoAuto, utilUniqueDomId } from '../../util';\nimport { prefs } from '../../core';\n\n\nexport function uiSectionRawMemberEditor(context) {\n\n var section = uiSection('raw-member-editor', context)\n .shouldDisplay(function() {\n if (!_entityIDs || _entityIDs.length !== 1) return false;\n\n var entity = context.hasEntity(_entityIDs[0]);\n return entity && entity.type === 'relation';\n })\n .label(function() {\n var entity = context.hasEntity(_entityIDs[0]);\n if (!entity) return '';\n\n var gt = entity.members.length > _maxMembers ? '>' : '';\n var count = gt + entity.members.slice(0, _maxMembers).length;\n return t.append('inspector.title_count', { title: t.append('inspector.members'), count: count });\n })\n .disclosureContent(renderDisclosureContent);\n\n var taginfo = services.taginfo;\n var _entityIDs;\n var _maxMembers = 1000;\n\n function downloadMember(d3_event, d) {\n d3_event.preventDefault();\n\n // display the loading indicator\n d3_select(this).classed('loading', true);\n context.loadEntity(d.id, function() {\n section.reRender();\n });\n }\n\n function zoomToMember(d3_event, d) {\n d3_event.preventDefault();\n\n var entity = context.entity(d.id);\n context.map().zoomToEase(entity);\n\n // highlight the feature in case it wasn't previously on-screen\n utilHighlightEntities([d.id], true, context);\n }\n\n\n function selectMember(d3_event, d) {\n d3_event.preventDefault();\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.id], false, context);\n\n var entity = context.entity(d.id);\n var mapExtent = context.map().extent();\n if (!entity.intersects(mapExtent, context.graph())) {\n // zoom to the entity if its extent is not visible now\n context.map().zoomToEase(entity);\n }\n\n context.enter(modeSelect(context, [d.id]));\n }\n\n\n function changeRole(d3_event, d) {\n var oldRole = d.role;\n var newRole = context.cleanRelationRole(d3_select(this).property('value'));\n\n if (oldRole !== newRole) {\n var member = { id: d.id, type: d.type, role: newRole };\n context.perform(\n actionChangeMember(d.relation.id, member, d.index),\n t('operations.change_role.annotation', {\n n: 1\n })\n );\n context.validator().validate();\n }\n }\n\n\n function deleteMember(d3_event, d) {\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.id], false, context);\n\n context.perform(\n actionDeleteMember(d.relation.id, d.index),\n t('operations.delete_member.annotation', {\n n: 1\n })\n );\n\n if (!context.hasEntity(d.relation.id)) {\n // Removing the last member will also delete the relation.\n // If this happens we need to exit the selection mode\n context.enter(modeBrowse(context));\n } else {\n // Changing the mode also runs `validate`, but otherwise we need to\n // rerun it manually\n context.validator().validate();\n }\n }\n\n function renderDisclosureContent(selection) {\n\n var entityID = _entityIDs[0];\n\n var memberships = [];\n var entity = context.entity(entityID);\n entity.members.slice(0, _maxMembers).forEach(function(member, index) {\n memberships.push({\n index: index,\n id: member.id,\n type: member.type,\n role: member.role,\n relation: entity,\n member: context.hasEntity(member.id),\n domId: utilUniqueDomId(entityID + '-member-' + index)\n });\n });\n\n var list = selection.selectAll('.member-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'member-list')\n .merge(list);\n\n\n var items = list.selectAll('li')\n .data(memberships, function(d) {\n return osmEntity.key(d.relation) + ',' + d.index + ',' +\n (d.member ? osmEntity.key(d.member) : 'incomplete');\n });\n\n items.exit()\n .each(unbind)\n .remove();\n\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', 'member-row form-field')\n .classed('member-incomplete', function(d) { return !d.member; });\n\n itemsEnter\n .each(function(d) {\n var item = d3_select(this);\n\n var label = item\n .append('label')\n .attr('class', 'field-label')\n .attr('for', d.domId);\n\n if (d.member) {\n // highlight the member feature in the map while hovering on the list item\n item\n .on('mouseover', function() {\n utilHighlightEntities([d.id], true, context);\n })\n .on('mouseout', function() {\n utilHighlightEntities([d.id], false, context);\n });\n\n var labelLink = label\n .append('span')\n .attr('class', 'label-text')\n .append('a')\n .attr('href', '#')\n .on('click', selectMember);\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-type')\n .text(function(d) {\n var matched = presetManager.match(d.member, context.graph());\n while (matched?.suggestion) {\n // if is NSI preset: look for a parent preset\n matched = matched.getParentPreset();\n }\n return (matched && matched.name()) || utilDisplayType(d.member.id);\n });\n\n const showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n labelLink.each(function(d) {\n if (!showThirdPartyIcons) return;\n const matched = presetManager.match(d, context.graph());\n if (matched.suggestion) {\n // if matching an NSI preset: append icon\n d3_select(this)\n .append('img')\n .classed('member-entity-icon', true)\n .attr('src', matched.imageURL);\n }\n });\n\n labelLink.each(function(d) {\n if (d.type !== 'relation') return;\n const relColors = getRelationColor(d.member.tags, '#555');\n const hasRef = d.member.tags.ref;\n if (relColors.isValid || hasRef) {\n const refs = (d.member.tags.ref || '').split(';');\n for (const ref of refs) {\n d3_select(this)\n .append('span')\n .classed('member-entity-ref-color', true)\n .style('border-color', relColors.color)\n .style('background-color', relColors.color)\n .style('color', relColors.textColor)\n .text(ref);\n }\n }\n });\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-name')\n .text(function(d) { return utilDisplayName(d.member, { hideRef: true }); });\n\n label\n .append('button')\n .attr('title', t('icons.remove'))\n .attr('class', 'remove member-delete')\n .call(svgIcon('#iD-operation-delete'));\n\n label\n .append('button')\n .attr('class', 'member-zoom')\n .attr('title', t('icons.zoom_to'))\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'))\n .on('click', zoomToMember);\n\n } else {\n var labelText = label\n .append('span')\n .attr('class', 'label-text');\n\n labelText\n .append('span')\n .attr('class', 'member-entity-type')\n .call(t.append('inspector.' + d.type, { id: d.id }));\n\n labelText\n .append('span')\n .attr('class', 'member-entity-name')\n .call(t.append('inspector.incomplete', { id: d.id }));\n\n label\n .append('button')\n .attr('class', 'member-download')\n .attr('title', t('icons.download'))\n .call(svgIcon('#iD-icon-load'))\n .on('click', downloadMember);\n }\n });\n\n var wrapEnter = itemsEnter\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-member');\n\n wrapEnter\n .append('input')\n .attr('class', 'member-role')\n .attr('id', function(d) {\n return d.domId;\n })\n .property('type', 'text')\n .attr('placeholder', t('inspector.role'))\n .call(utilNoAuto);\n\n if (taginfo) {\n wrapEnter.each(bindTypeahead);\n }\n\n // update\n items = items\n .merge(itemsEnter)\n .order();\n\n items.select('input.member-role')\n .property('value', function(d) { return d.role; })\n .on('blur', changeRole)\n .on('change', changeRole);\n\n items.select('button.member-delete')\n .on('click', deleteMember);\n\n var dragOrigin, targetIndex;\n\n items.call(d3_drag()\n .on('start', function(d3_event) {\n dragOrigin = {\n x: d3_event.x,\n y: d3_event.y\n };\n targetIndex = null;\n })\n .on('drag', function(d3_event) {\n var x = d3_event.x - dragOrigin.x,\n y = d3_event.y - dragOrigin.y;\n\n if (!d3_select(this).classed('dragging') &&\n // don't display drag until dragging beyond a distance threshold\n Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) <= 5) return;\n\n var index = items.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', true);\n\n targetIndex = null;\n\n selection.selectAll('li.member-row')\n .style('transform', function(d2, index2) {\n var node = d3_select(this).node();\n if (index === index2) {\n return 'translate(' + x + 'px, ' + y + 'px)';\n } else if (index2 > index && d3_event.y > node.offsetTop) {\n if (targetIndex === null || index2 > targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(-100%)';\n } else if (index2 < index && d3_event.y < node.offsetTop + node.offsetHeight) {\n if (targetIndex === null || index2 < targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(100%)';\n }\n return null;\n });\n })\n .on('end', function(d3_event, d) {\n\n if (!d3_select(this).classed('dragging')) return;\n\n var index = items.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', false);\n\n selection.selectAll('li.member-row')\n .style('transform', null);\n\n if (targetIndex !== null) {\n // dragged to a new position, reorder\n context.perform(\n actionMoveMember(d.relation.id, index, targetIndex),\n t('operations.reorder_members.annotation')\n );\n context.validator().validate();\n }\n })\n );\n\n\n\n function bindTypeahead(d) {\n var row = d3_select(this);\n var role = row.selectAll('input.member-role');\n var origValue = role.property('value');\n\n function sort(value, data) {\n var sameletter = [];\n var other = [];\n for (var i = 0; i < data.length; i++) {\n if (data[i].value.substring(0, value.length) === value) {\n sameletter.push(data[i]);\n } else {\n other.push(data[i]);\n }\n }\n return sameletter.concat(other);\n }\n\n role.call(uiCombobox(context, 'member-role')\n .fetcher(function(role, callback) {\n // The `geometry` param is used in the `taginfo.js` interface for\n // filtering results, as a key into the `tag_members_fractions`\n // object. If we don't know the geometry because the member is\n // not yet downloaded, it's ok to guess based on type.\n var geometry;\n if (d.member) {\n geometry = context.graph().geometry(d.member.id);\n } else if (d.type === 'relation') {\n geometry = 'relation';\n } else if (d.type === 'way') {\n geometry = 'line';\n } else {\n geometry = 'point';\n }\n\n var rtype = entity.tags.type;\n taginfo.roles({\n debounce: true,\n rtype: rtype || '',\n geometry: geometry,\n query: role\n }, function(err, data) {\n if (!err) callback(sort(role, data));\n });\n })\n .on('cancel', function() {\n role.property('value', origValue);\n })\n );\n }\n\n\n function unbind() {\n var row = d3_select(this);\n\n row.selectAll('input.member-role')\n .call(uiCombobox.off, context);\n }\n }\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return section;\n };\n\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t, localizer } from '../../core/localizer';\n\nimport { actionAddEntity } from '../../actions/add_entity';\nimport { actionAddMember } from '../../actions/add_member';\nimport { actionChangeMember } from '../../actions/change_member';\nimport { actionDeleteMembers } from '../../actions/delete_members';\n\nimport { modeSelect } from '../../modes/select';\nimport { osmEntity, osmRelation } from '../../osm';\nimport { getRelationColor, isColorValid } from '../../osm/tags';\nimport { services } from '../../services';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCombobox } from '../combobox';\nimport { uiSection } from '../section';\nimport { uiTooltip } from '../tooltip';\nimport { utilArrayGroupBy, utilArrayIntersection } from '../../util/array';\nimport { utilDisplayName, utilNoAuto, utilHighlightEntities, utilUniqueDomId } from '../../util';\nimport { prefs } from '../../core';\nimport { idMatch } from '../feature_list';\n\n\nexport function uiSectionRawMembershipEditor(context) {\n\n var section = uiSection('raw-membership-editor', context)\n .shouldDisplay(function() {\n return _entityIDs && _entityIDs.length;\n })\n .label(function() {\n var parents = getSharedParentRelations();\n var gt = parents.length > _maxMemberships ? '>' : '';\n var count = gt + parents.slice(0, _maxMemberships).length;\n return t.append('inspector.title_count', { title: t.append('inspector.relations'), count: count });\n })\n .disclosureContent(renderDisclosureContent);\n\n var taginfo = services.taginfo;\n var nearbyCombo = uiCombobox(context, 'parent-relation')\n .minItems(1)\n .fetcher(fetchNearbyRelations)\n .itemsMouseEnter(function(d3_event, d) {\n if (d.relation) utilHighlightEntities([d.relation.id], true, context);\n })\n .itemsMouseLeave(function(d3_event, d) {\n if (d.relation) utilHighlightEntities([d.relation.id], false, context);\n });\n var _inChange = false;\n var _entityIDs = [];\n var _showBlank;\n var _maxMemberships = 1000;\n /** @type {Set} relations that were added after this panel was opened */\n const recentlyAdded = new Set();\n\n function getSharedParentRelations() {\n var parents = [];\n for (var i = 0; i < _entityIDs.length; i++) {\n var entity = context.graph().hasEntity(_entityIDs[i]);\n if (!entity) continue;\n\n if (i === 0) {\n parents = context.graph().parentRelations(entity);\n } else {\n parents = utilArrayIntersection(parents, context.graph().parentRelations(entity));\n }\n if (!parents.length) break;\n }\n return parents;\n }\n\n function getMemberships() {\n\n var memberships = [];\n var relations = getSharedParentRelations().slice(0, _maxMemberships);\n\n var isMultiselect = _entityIDs.length > 1;\n\n var i, relation, membership, index, member, indexedMember;\n for (i = 0; i < relations.length; i++) {\n relation = relations[i];\n membership = {\n relation: relation,\n members: [],\n hash: osmEntity.key(relation)\n };\n for (index = 0; index < relation.members.length; index++) {\n member = relation.members[index];\n if (_entityIDs.indexOf(member.id) !== -1) {\n indexedMember = Object.assign({}, member, { index: index });\n membership.members.push(indexedMember);\n membership.hash += ',' + index.toString();\n\n if (!isMultiselect) {\n // For single selections, list one entry per membership per relation.\n // For multiselections, list one entry per relation.\n\n memberships.push(membership);\n membership = {\n relation: relation,\n members: [],\n hash: osmEntity.key(relation)\n };\n }\n }\n }\n if (membership.members.length) memberships.push(membership);\n }\n\n memberships.forEach(function(membership) {\n membership.domId = utilUniqueDomId('membership-' + membership.relation.id);\n var roles = [];\n membership.members.forEach(function(member) {\n if (roles.indexOf(member.role) === -1) roles.push(member.role);\n });\n membership.role = roles.length === 1 ? roles[0] : roles;\n });\n\n const existingRelations = memberships\n .filter(membership => !recentlyAdded.has(membership.relation.id))\n .map(membership => ({\n ...membership,\n // We only sort relations that were not added just now.\n // Sorting uses the same label as shown in the UI.\n // If the label is not unique, the relation ID ensures\n // that the sort order is still stable.\n _sortKey: [\n baseDisplayValue(membership.relation),\n membership.relation.id,\n ].join('-'),\n }))\n .sort((a, b) => {\n return a._sortKey.localeCompare(\n b._sortKey,\n localizer.localeCodes(),\n { numeric: true },\n );\n });\n\n\n const newlyAddedRelations = memberships\n .filter(membership => recentlyAdded.has(membership.relation.id));\n\n return [\n // the sorted relations come first\n ...existingRelations,\n // then the ones that were just added from this panel\n ...newlyAddedRelations,\n ];\n }\n\n function selectRelation(d3_event, d) {\n d3_event.preventDefault();\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.relation.id], false, context);\n\n context.enter(modeSelect(context, [d.relation.id]));\n }\n\n function zoomToRelation(d3_event, d) {\n d3_event.preventDefault();\n\n var entity = context.entity(d.relation.id);\n context.map().zoomToEase(entity);\n\n // highlight the relation in case it wasn't previously on-screen\n utilHighlightEntities([d.relation.id], true, context);\n }\n\n\n function changeRole(d3_event, d) {\n if (d === 0) return; // called on newrow (shouldn't happen)\n if (_inChange) return; // avoid accidental recursive call #5731\n\n var newRole = context.cleanRelationRole(d3_select(this).property('value'));\n\n if (!newRole.trim() && typeof d.role !== 'string') return;\n\n var membersToUpdate = d.members.filter(function(member) {\n return member.role !== newRole;\n });\n\n if (membersToUpdate.length) {\n _inChange = true;\n context.perform(\n function actionChangeMemberRoles(graph) {\n membersToUpdate.forEach(function(member) {\n var newMember = Object.assign({}, member, { role: newRole });\n delete newMember.index;\n graph = actionChangeMember(d.relation.id, newMember, member.index)(graph);\n });\n return graph;\n },\n t('operations.change_role.annotation', {\n n: membersToUpdate.length\n })\n );\n context.validator().validate();\n }\n _inChange = false;\n }\n\n\n function addMembership(d, role) {\n _showBlank = false;\n\n function actionAddMembers(relationId, ids, role) {\n return function(graph) {\n for (var i in ids) {\n var member = { id: ids[i], type: graph.entity(ids[i]).type, role: role };\n graph = actionAddMember(relationId, member)(graph);\n }\n return graph;\n };\n }\n\n if (d.relation) {\n recentlyAdded.add(d.relation.id);\n context.perform(\n actionAddMembers(d.relation.id, _entityIDs, role),\n t('operations.add_member.annotation', {\n n: _entityIDs.length\n })\n );\n context.validator().validate();\n\n } else {\n var relation = osmRelation();\n context.perform(\n actionAddEntity(relation),\n actionAddMembers(relation.id, _entityIDs, role),\n t('operations.add.annotation.relation')\n );\n // changing the mode also runs `validate`\n context.enter(modeSelect(context, [relation.id]).newFeature(true));\n }\n }\n\n\n function downloadMembers(d3_event, d) {\n d3_event.preventDefault();\n const button = d3_select(this);\n\n // display the loading indicator\n button.classed('loading', true);\n context.loadEntity(d.relation.id, function() {\n section.reRender();\n });\n }\n\n\n function deleteMembership(d3_event, d) {\n this.blur(); // avoid keeping focus on the button\n if (d === 0) return; // called on newrow (shouldn't happen)\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.relation.id], false, context);\n\n var indexes = d.members.map(function(member) {\n return member.index;\n });\n\n context.perform(\n actionDeleteMembers(d.relation.id, indexes),\n t('operations.delete_member.annotation', {\n n: _entityIDs.length\n })\n );\n context.validator().validate();\n }\n\n\n function fetchNearbyRelations(q, callback) {\n var newRelation = {\n relation: null,\n value: t('inspector.new_relation'),\n display: t.append('inspector.new_relation')\n };\n\n var entityID = _entityIDs[0];\n\n var result = [];\n\n var graph = context.graph();\n\n function baseDisplayLabel(entity) {\n var matched = presetManager.match(entity, graph);\n var presetName = (matched && matched.name()) || t('inspector.relation');\n var entityName = utilDisplayName(entity) || '';\n\n return selection => {\n selection\n .append('b')\n .text(presetName + ' ');\n selection\n .append('span')\n .classed('has-colour', entity.tags.colour && isColorValid(entity.tags.colour))\n .style('border-color', entity.tags.colour)\n .text(entityName);\n };\n }\n\n\n // A location search takes priority over an ID search\n const idMatchResult = q && idMatch(q);\n var explicitRelation = context.hasEntity(`r${idMatchResult?.id || q}`);\n if (explicitRelation && explicitRelation.type === 'relation' && explicitRelation.id !== entityID) {\n // loaded relation is specified explicitly, only show that\n\n result.push({\n relation: explicitRelation,\n value: baseDisplayValue(explicitRelation) + ' ' + explicitRelation.id,\n display: baseDisplayLabel(explicitRelation)\n });\n } else {\n\n context.history().intersects(context.map().extent()).forEach(function(entity) {\n if (entity.type !== 'relation' || entity.id === entityID) return;\n\n var value = baseDisplayValue(entity);\n if (q && (value + ' ' + entity.id).toLowerCase().indexOf(q.toLowerCase()) === -1) return;\n\n result.push({\n relation: entity,\n value,\n display: baseDisplayLabel(entity)\n });\n });\n\n result.sort(function(a, b) {\n return osmRelation.creationOrder(a.relation, b.relation);\n });\n\n // Dedupe identical names by appending relation id - see #2891\n Object.values(utilArrayGroupBy(result, 'value'))\n .filter(v => v.length > 1)\n .flat()\n .forEach(obj => obj.value += ' ' + obj.relation.id);\n }\n\n result.forEach(function(obj) {\n obj.title = obj.value;\n });\n\n result.unshift(newRelation);\n callback(result);\n }\n\n function baseDisplayValue(entity) {\n const graph = context.graph();\n var matched = presetManager.match(entity, graph);\n var presetName = (matched && matched.name()) || t('inspector.relation');\n var entityName = utilDisplayName(entity) || '';\n\n return presetName + ' ' + entityName;\n }\n\n function renderDisclosureContent(selection) {\n\n var memberships = getMemberships();\n\n var list = selection.selectAll('.member-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'member-list')\n .merge(list);\n\n\n var items = list.selectAll('li.member-row-normal')\n .data(memberships, function(d) {\n return d.hash;\n });\n\n items.exit()\n .each(unbind)\n .remove();\n\n // Enter\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', 'member-row member-row-normal form-field');\n\n // highlight the relation in the map while hovering on the list item\n itemsEnter.on('mouseover', function(d3_event, d) {\n utilHighlightEntities([d.relation.id], true, context);\n })\n .on('mouseout', function(d3_event, d) {\n utilHighlightEntities([d.relation.id], false, context);\n });\n\n var labelEnter = itemsEnter\n .append('label')\n .attr('class', 'field-label')\n .attr('for', function(d) {\n return d.domId;\n });\n\n var labelLink = labelEnter\n .append('span')\n .attr('class', 'label-text')\n .append('a')\n .attr('href', '#')\n .on('click', selectRelation);\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-type')\n .text(d => {\n let matched = presetManager.match(d.relation, context.graph());\n while (matched?.suggestion) {\n // if is NSI preset: look for a parent preset\n matched = matched.getParentPreset();\n }\n return (matched && matched.name()) || t('inspector.relation');\n });\n\n const showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n labelLink.each(function(d) {\n if (!showThirdPartyIcons) return;\n const matched = presetManager.match(d.relation, context.graph());\n if (matched.suggestion) {\n // if matching an NSI preset: append icon\n d3_select(this)\n .append('img')\n .classed('member-entity-icon', true)\n .attr('src', matched.imageURL);\n }\n });\n\n labelLink.each(function(d) {\n const relColors = getRelationColor(d.relation.tags, '#555');\n const hasRef = d.relation.tags.ref;\n if (relColors.isValid || hasRef) {\n const refs = (d.relation.tags.ref || '').split(';');\n for (const ref of refs) {\n d3_select(this)\n .append('span')\n .classed('member-entity-ref-color', true)\n .style('border-color', relColors.color)\n .style('background-color', relColors.color)\n .style('color', relColors.textColor)\n .text(ref);\n }\n }\n });\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-name')\n .text(d => utilDisplayName(d.relation, { hideRef: true }));\n\n labelEnter\n .append('button')\n .attr('class', 'members-download')\n .attr('title', t('icons.download'))\n .call(svgIcon('#iD-icon-load'))\n .on('click', downloadMembers);\n\n labelEnter\n .append('button')\n .attr('class', 'remove member-delete')\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'))\n .on('click', deleteMembership);\n\n labelEnter\n .append('button')\n .attr('class', 'member-zoom')\n .attr('title', t('icons.zoom_to'))\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'))\n .on('click', zoomToRelation);\n\n items = items.merge(itemsEnter);\n items.selectAll('button.members-download')\n .classed('hide', d => {\n const graph = context.graph();\n return d.relation.members.every(m => graph.hasEntity(m.id));\n });\n\n const dupeLabels = new WeakSet(Object.values(\n utilArrayGroupBy(items.selectAll('.label-text').nodes(), 'textContent'))\n .filter(v => v.length > 1)\n .flat());\n\n items.select('.label-text').each(function() {\n const label = d3_select(this);\n const entityName = label.select('.member-entity-name');\n if (dupeLabels.has(this)) {\n // Dedupe identical names in hover text by appending relation id - see #2891, #10184\n label.attr('title', d => `${entityName.text()} ${d.relation.id}`);\n } else {\n // set full label also as hover text: useful if a (long) label is cut off with an \u2026 ellipsis\n label.attr('title', () => entityName.text());\n }\n });\n\n var wrapEnter = itemsEnter\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-member');\n\n wrapEnter\n .append('input')\n .attr('class', 'member-role')\n .attr('id', function(d) {\n return d.domId;\n })\n .property('type', 'text')\n .property('value', function(d) {\n return typeof d.role === 'string' ? d.role : '';\n })\n .attr('title', function(d) {\n return Array.isArray(d.role) ? d.role.filter(Boolean).join('\\n') : d.role;\n })\n .attr('placeholder', function(d) {\n return Array.isArray(d.role) ? t('inspector.multiple_roles') : t('inspector.role');\n })\n .classed('mixed', function(d) {\n return Array.isArray(d.role);\n })\n .call(utilNoAuto)\n .on('blur', changeRole)\n .on('change', changeRole);\n\n if (taginfo) {\n wrapEnter.each(bindTypeahead);\n }\n\n var newMembership = list.selectAll('.member-row-new')\n .data(_showBlank ? [0] : []);\n\n // Exit\n newMembership.exit()\n .remove();\n\n // Enter\n var newMembershipEnter = newMembership.enter()\n .append('li')\n .attr('class', 'member-row member-row-new form-field');\n\n var newLabelEnter = newMembershipEnter\n .append('label')\n .attr('class', 'field-label');\n\n newLabelEnter\n .append('input')\n .attr('placeholder', t('inspector.choose_relation'))\n .attr('type', 'text')\n .attr('class', 'member-entity-input')\n .call(utilNoAuto);\n\n newLabelEnter\n .append('button')\n .attr('class', 'remove member-delete')\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'))\n .on('click', function() {\n list.selectAll('.member-row-new')\n .remove();\n });\n\n var newWrapEnter = newMembershipEnter\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-member');\n\n newWrapEnter\n .append('input')\n .attr('class', 'member-role')\n .property('type', 'text')\n .attr('placeholder', t('inspector.role'))\n .call(utilNoAuto);\n\n // Update\n newMembership = newMembership\n .merge(newMembershipEnter);\n\n newMembership.selectAll('.member-entity-input')\n .on('blur', cancelEntity) // if it wasn't accepted normally, cancel it\n .call(nearbyCombo\n .on('accept', function(d) {\n this.blur(); // always blurs the triggering element\n acceptEntity.call(this, d);\n })\n .on('cancel', cancelEntity)\n );\n\n\n // Container for the Add button\n var addRow = selection.selectAll('.add-row')\n .data([0]);\n\n // enter\n var addRowEnter = addRow.enter()\n .append('div')\n .attr('class', 'add-row');\n\n var addRelationButton = addRowEnter\n .append('button')\n .attr('class', 'add-relation')\n .attr('aria-label', t('inspector.add_to_relation'));\n\n addRelationButton\n .call(svgIcon('#iD-icon-plus', 'light'));\n addRelationButton\n .call(uiTooltip()\n .title(() => t.append('inspector.add_to_relation'))\n .placement(localizer.textDirection() === 'ltr' ? 'right' : 'left'));\n\n addRowEnter\n .append('div')\n .attr('class', 'space-value'); // preserve space\n\n addRowEnter\n .append('div')\n .attr('class', 'space-buttons'); // preserve space\n\n // update\n addRow = addRow\n .merge(addRowEnter);\n\n addRow.select('.add-relation')\n .on('click', function() {\n _showBlank = true;\n section.reRender();\n list.selectAll('.member-entity-input').node().focus();\n });\n\n\n function acceptEntity(d) {\n if (!d) {\n cancelEntity();\n return;\n }\n // remove hover-higlighting\n if (d.relation) utilHighlightEntities([d.relation.id], false, context);\n\n var role = context.cleanRelationRole(list.selectAll('.member-row-new .member-role').property('value'));\n addMembership(d, role);\n }\n\n\n function cancelEntity() {\n var input = newMembership.selectAll('.member-entity-input');\n input.property('value', '');\n\n // remove hover-higlighting\n context.surface().selectAll('.highlighted')\n .classed('highlighted', false);\n }\n\n\n function bindTypeahead(d) {\n var row = d3_select(this);\n var role = row.selectAll('input.member-role');\n var origValue = role.property('value');\n\n function sort(value, data) {\n var sameletter = [];\n var other = [];\n for (var i = 0; i < data.length; i++) {\n if (data[i].value.substring(0, value.length) === value) {\n sameletter.push(data[i]);\n } else {\n other.push(data[i]);\n }\n }\n return sameletter.concat(other);\n }\n\n role.call(uiCombobox(context, 'member-role')\n .fetcher(function(role, callback) {\n var rtype = d.relation.tags.type;\n taginfo.roles({\n debounce: true,\n rtype: rtype || '',\n geometry: context.graph().geometry(_entityIDs[0]),\n query: role\n }, function(err, data) {\n if (!err) callback(sort(role, data));\n });\n })\n .on('cancel', function() {\n role.property('value', origValue);\n })\n );\n }\n\n\n function unbind() {\n var row = d3_select(this);\n\n row.selectAll('input.member-role')\n .call(uiCombobox.off, context);\n }\n }\n\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n const didChange = _entityIDs.join(',') !== val.join(',');\n _entityIDs = val;\n _showBlank = false;\n if (didChange) {\n recentlyAdded.clear(); // reset when the selected feature changes\n }\n return section;\n };\n\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { modeSelect } from '../../modes/select';\nimport { osmEntity } from '../../osm';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\nimport { t } from '../../core/localizer';\nimport { utilDisplayName, utilHighlightEntities } from '../../util';\n\nexport function uiSectionSelectionList(context) {\n\n var _selectedIDs = [];\n\n var section = uiSection('selected-features', context)\n .shouldDisplay(function() {\n return _selectedIDs.length > 1;\n })\n .label(function() {\n return t.append('inspector.title_count', { title: t.append('inspector.features'), count: _selectedIDs.length });\n })\n .disclosureContent(renderDisclosureContent);\n\n context.history()\n .on('change.selectionList', function(difference) {\n if (difference) {\n section.reRender();\n }\n });\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _selectedIDs;\n _selectedIDs = val;\n return section;\n };\n\n function selectEntity(d3_event, entity) {\n context.enter(modeSelect(context, [entity.id]));\n }\n\n function deselectEntity(d3_event, entity) {\n var selectedIDs = _selectedIDs.slice();\n var index = selectedIDs.indexOf(entity.id);\n if (index > -1) {\n selectedIDs.splice(index, 1);\n context.enter(modeSelect(context, selectedIDs));\n }\n }\n\n function renderDisclosureContent(selection) {\n\n var list = selection.selectAll('.feature-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'feature-list')\n .merge(list);\n\n var entities = _selectedIDs\n .map(function(id) { return context.hasEntity(id); })\n .filter(Boolean);\n\n var items = list.selectAll('.feature-list-item')\n .data(entities, osmEntity.key);\n\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li')\n .attr('class', 'feature-list-item')\n .each(function(d) {\n d3_select(this)\n .on('mouseover', function() {\n utilHighlightEntities([d.id], true, context);\n })\n .on('mouseout', function() {\n utilHighlightEntities([d.id], false, context);\n });\n });\n\n var label = enter\n .append('button')\n .attr('class', 'label')\n .on('click', selectEntity);\n\n label\n .append('span')\n .attr('class', 'entity-geom-icon')\n .call(svgIcon('', 'pre-text'));\n\n label\n .append('span')\n .attr('class', 'entity-type');\n\n label\n .append('span')\n .attr('class', 'entity-name');\n\n enter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.deselect'))\n .on('click', deselectEntity)\n .call(svgIcon('#iD-icon-close'));\n\n // Update\n items = items.merge(enter);\n\n items.selectAll('.entity-geom-icon use')\n .attr('href', function() {\n var entity = this.parentNode.parentNode.__data__;\n return '#iD-icon-' + entity.geometry(context.graph());\n });\n\n items.selectAll('.entity-type')\n .text(function(entity) { return presetManager.match(entity, context.graph()).name(); });\n\n items.selectAll('.entity-name')\n .text(function(d) {\n // fetch latest entity\n var entity = context.entity(d.id);\n return utilDisplayName(entity);\n });\n }\n\n return section;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { deepEqual } from 'fast-equals';\n\nimport { presetManager } from '../presets';\nimport { t, localizer } from '../core/localizer';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\nimport { utilArrayIdentical } from '../util/array';\nimport { utilCleanTags, utilCombinedTags, utilRebind } from '../util';\n\nimport { uiSectionEntityIssues } from './sections/entity_issues';\nimport { uiSectionFeatureType } from './sections/feature_type';\nimport { uiSectionPresetFields } from './sections/preset_fields';\nimport { uiSectionRawMemberEditor } from './sections/raw_member_editor';\nimport { uiSectionRawMembershipEditor } from './sections/raw_membership_editor';\nimport { uiSectionRawTagEditor } from './sections/raw_tag_editor';\nimport { uiSectionSelectionList } from './sections/selection_list';\n\nexport function uiEntityEditor(context) {\n var dispatch = d3_dispatch('choose');\n var _state = 'select';\n var _coalesceChanges = false;\n var _modified = false;\n var _base;\n var _entityIDs;\n var _activePresets = [];\n var _newFeature;\n\n var _sections;\n\n function entityEditor(selection) {\n\n var combinedTags = utilCombinedTags(_entityIDs, context.graph());\n\n // Header\n var header = selection.selectAll('.header')\n .data([0]);\n\n // Enter\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n var direction = (localizer.textDirection() === 'rtl') ? 'forward' : 'backward';\n\n headerEnter\n .append('button')\n .attr('class', 'preset-reset preset-choose')\n .attr('title', t('inspector.back_tooltip'))\n .call(svgIcon(`#iD-icon-${direction}`));\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function() { context.enter(modeBrowse(context)); })\n .call(svgIcon(_modified ? '#iD-icon-apply' : '#iD-icon-close'));\n\n headerEnter\n .append('h2');\n\n // Update\n header = header\n .merge(headerEnter);\n\n header.selectAll('h2')\n .text('')\n .call(_entityIDs.length === 1 ? t.append('inspector.edit') : t.append('inspector.edit_features'));\n\n header.selectAll('.preset-reset')\n .on('click', function() {\n dispatch.call('choose', this, _activePresets);\n });\n\n // Body\n var body = selection.selectAll('.inspector-body')\n .data([0]);\n\n // Enter\n var bodyEnter = body.enter()\n .append('div')\n .attr('class', 'entity-editor inspector-body sep-top');\n\n // Update\n body = body\n .merge(bodyEnter);\n\n if (!_sections) {\n _sections = [\n uiSectionSelectionList(context),\n uiSectionFeatureType(context).on('choose', function(presets) {\n dispatch.call('choose', this, presets);\n }),\n uiSectionEntityIssues(context),\n uiSectionPresetFields(context).on('change', changeTags).on('revert', revertTags),\n uiSectionRawTagEditor('raw-tag-editor', context).on('change', changeTags),\n uiSectionRawMemberEditor(context),\n uiSectionRawMembershipEditor(context)\n ];\n }\n\n _sections.forEach(function(section) {\n if (section.entityIDs) {\n section.entityIDs(_entityIDs);\n }\n if (section.presets) {\n section.presets(_activePresets);\n }\n if (section.tags) {\n section.tags(combinedTags);\n }\n if (section.state) {\n section.state(_state);\n }\n body.call(section.render);\n });\n\n context.history()\n .on('change.entity-editor', historyChanged);\n\n function historyChanged(difference) {\n if (selection.selectAll('.entity-editor').empty()) return;\n if (_state === 'hide') return;\n var significant = !difference ||\n difference.didChange.properties ||\n difference.didChange.addition ||\n difference.didChange.deletion;\n if (!significant) return;\n\n _entityIDs = _entityIDs.filter(context.hasEntity);\n if (!_entityIDs.length) return;\n\n var priorActivePreset = _activePresets.length === 1 && _activePresets[0];\n\n loadActivePresets();\n\n var graph = context.graph();\n entityEditor.modified(_base !== graph);\n entityEditor(selection);\n\n if (priorActivePreset && _activePresets.length === 1 && priorActivePreset !== _activePresets[0]) {\n // flash the button to indicate the preset changed\n context.container().selectAll('.entity-editor button.preset-reset .label')\n .classed('flash-bg', true)\n .on('animationend', function() {\n d3_select(this).classed('flash-bg', false);\n });\n }\n }\n }\n\n\n // Tag changes that fire on input can all get coalesced into a single\n // history operation when the user leaves the field. #2342\n // Use explicit entityIDs in case the selection changes before the event is fired.\n function changeTags(entityIDs, changed, onInput) {\n\n var actions = [];\n for (var i in entityIDs) {\n var entityID = entityIDs[i];\n var entity = context.entity(entityID);\n\n var tags = Object.assign({}, entity.tags); // shallow copy\n\n if (typeof changed === 'function') {\n // a complex callback tag change\n tags = changed(tags);\n } else {\n for (var k in changed) {\n if (!k) continue;\n var v = changed[k];\n if (typeof v === 'object') {\n // a \"key only\" tag change\n tags[k] = tags[v.oldKey];\n } else if (v !== undefined || tags.hasOwnProperty(k)) {\n tags[k] = v;\n }\n }\n }\n\n if (!onInput) {\n tags = utilCleanTags(tags);\n }\n\n if (!deepEqual(entity.tags, tags)) {\n actions.push(actionChangeTags(entityID, tags));\n }\n }\n\n if (actions.length) {\n var combinedAction = function(graph) {\n actions.forEach(function(action) {\n graph = action(graph);\n });\n return graph;\n };\n\n var annotation = t('operations.change_tags.annotation');\n\n if (_coalesceChanges) {\n context.replace(combinedAction, annotation);\n } else {\n context.perform(combinedAction, annotation);\n }\n _coalesceChanges = !!onInput;\n }\n\n // if leaving field (blur event), rerun validation\n if (!onInput) {\n context.validator().validate();\n }\n }\n\n function revertTags(keys) {\n\n var actions = [];\n for (var i in _entityIDs) {\n var entityID = _entityIDs[i];\n\n var original = context.graph().base().entities[entityID];\n var changed = {};\n for (var j in keys) {\n var key = keys[j];\n changed[key] = original ? original.tags[key] : undefined;\n }\n\n var entity = context.entity(entityID);\n var tags = Object.assign({}, entity.tags); // shallow copy\n\n for (var k in changed) {\n if (!k) continue;\n var v = changed[k];\n if (v !== undefined || tags.hasOwnProperty(k)) {\n tags[k] = v;\n }\n }\n\n\n tags = utilCleanTags(tags);\n\n if (!deepEqual(entity.tags, tags)) {\n actions.push(actionChangeTags(entityID, tags));\n }\n\n }\n\n if (actions.length) {\n var combinedAction = function(graph) {\n actions.forEach(function(action) {\n graph = action(graph);\n });\n return graph;\n };\n\n var annotation = t('operations.change_tags.annotation');\n\n if (_coalesceChanges) {\n context.replace(combinedAction, annotation);\n } else {\n context.perform(combinedAction, annotation);\n }\n }\n\n context.validator().validate();\n }\n\n\n entityEditor.modified = function(val) {\n if (!arguments.length) return _modified;\n _modified = val;\n return entityEditor;\n };\n\n\n entityEditor.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n return entityEditor;\n };\n\n\n entityEditor.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n\n // always reload these even if the entityIDs are unchanged, since we\n // could be reselecting after something like dragging a node\n _base = context.graph();\n _coalesceChanges = false;\n\n if (val && _entityIDs && utilArrayIdentical(_entityIDs, val)) return entityEditor; // exit early if no change\n\n _entityIDs = val;\n\n loadActivePresets(true);\n\n return entityEditor\n .modified(false);\n };\n\n\n entityEditor.newFeature = function(val) {\n if (!arguments.length) return _newFeature;\n _newFeature = val;\n return entityEditor;\n };\n\n\n function loadActivePresets(isForNewSelection) {\n\n var graph = context.graph();\n\n var counts = {};\n\n for (var i in _entityIDs) {\n var entity = graph.hasEntity(_entityIDs[i]);\n if (!entity) return;\n\n var match = presetManager.match(entity, graph);\n\n if (!counts[match.id]) counts[match.id] = 0;\n counts[match.id] += 1;\n }\n\n var matches = Object.keys(counts).sort(function(p1, p2) {\n return counts[p2] - counts[p1];\n }).map(function(pID) {\n return presetManager.item(pID);\n });\n\n if (!isForNewSelection) {\n // A \"weak\" preset doesn't set any tags. (e.g. \"Address\")\n var weakPreset = _activePresets.length === 1 &&\n !_activePresets[0].isFallback() &&\n Object.keys(_activePresets[0].addTags || {}).length === 0;\n // Don't replace a weak preset with a fallback preset (e.g. \"Point\")\n if (weakPreset && matches.length === 1 && matches[0].isFallback()) return;\n }\n\n entityEditor.presets(matches);\n }\n\n entityEditor.presets = function(val) {\n if (!arguments.length) return _activePresets;\n\n // don't reload the same preset\n if (!utilArrayIdentical(val, _activePresets)) {\n _activePresets = val;\n }\n return entityEditor;\n };\n\n return utilRebind(entityEditor, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { debounce } from 'es-toolkit/compat';\n\nimport { presetManager } from '../presets';\nimport { t, localizer } from '../core/localizer';\nimport { actionChangePreset } from '../actions/change_preset';\nimport { operationDelete } from '../operations/delete';\nimport { svgIcon } from '../svg/index';\nimport { uiTooltip } from './tooltip';\nimport { geoExtent } from '../geo/extent';\nimport { uiPresetIcon } from './preset_icon';\nimport { uiTagReference } from './tag_reference';\nimport { utilKeybinding, utilNoAuto, utilRebind } from '../util';\n\n\nexport function uiPresetList(context) {\n var dispatch = d3_dispatch('cancel', 'choose');\n var _entityIDs;\n var _currLoc;\n var _currentPresets;\n var _autofocus = false;\n\n\n function presetList(selection) {\n if (!_entityIDs) return;\n\n var presets = presetManager.matchAllGeometry(entityGeometries());\n\n selection.html('');\n\n var messagewrap = selection\n .append('div')\n .attr('class', 'header fillL');\n\n var message = messagewrap\n .append('h2')\n .call(t.addOrUpdate('inspector.choose'));\n\n messagewrap\n .append('button')\n .attr('class', 'preset-choose')\n .attr('title', _entityIDs.length === 1 ? t('inspector.edit') : t('inspector.edit_features'))\n .on('click', function() { dispatch.call('cancel', this); })\n .call(svgIcon('#iD-icon-close'));\n\n function initialKeydown(d3_event) {\n // hack to let delete shortcut work when search is autofocused\n if (search.property('value').length === 0 &&\n (d3_event.keyCode === utilKeybinding.keyCodes['\u232B'] ||\n d3_event.keyCode === utilKeybinding.keyCodes['\u2326'])) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n operationDelete(context, _entityIDs)();\n\n // hack to let undo work when search is autofocused\n } else if (search.property('value').length === 0 &&\n (d3_event.ctrlKey || d3_event.metaKey) &&\n d3_event.keyCode === utilKeybinding.keyCodes.z) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n context.undo();\n } else if (!d3_event.ctrlKey && !d3_event.metaKey) {\n // don't check for delete/undo hack on future keydown events\n d3_select(this).on('keydown', keydown);\n keydown.call(this, d3_event);\n }\n }\n\n function keydown(d3_event) {\n // down arrow\n if (d3_event.keyCode === utilKeybinding.keyCodes['\u2193'] &&\n // if insertion point is at the end of the string\n search.node().selectionStart === search.property('value').length) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // move focus to the first item in the preset list\n var buttons = list.selectAll('.preset-list-button');\n if (!buttons.empty()) buttons.nodes()[0].focus();\n }\n }\n\n function keypress(d3_event) {\n // enter\n var value = search.property('value');\n if (d3_event.keyCode === 13 && // \u21A9 Return\n value.length) {\n list.selectAll('.preset-list-item:first-child')\n .each(function(d) { d.choose.call(this); });\n }\n }\n\n function inputevent() {\n var value = search.property('value');\n list.classed('filtered', value.length);\n\n var results, messageText;\n if (value.length) {\n results = presets.search(value, entityGeometries()[0], _currLoc);\n messageText = t.addOrUpdate('inspector.results', {\n n: results.collection.length,\n search: value\n });\n } else {\n var entityPresets = _entityIDs.map(entityID =>\n presetManager.match(context.graph().entity(entityID), context.graph()));\n results = presetManager.defaults(entityGeometries()[0], 36, !context.inIntro(), _currLoc, entityPresets);\n messageText = t.addOrUpdate('inspector.choose');\n }\n list.call(drawList, results);\n message.call(messageText);\n }\n\n var searchWrap = selection\n .append('div')\n .attr('class', 'search-header');\n\n searchWrap\n .call(svgIcon('#iD-icon-search', 'pre-text'));\n\n var search = searchWrap\n .append('input')\n .attr('class', 'preset-search-input')\n .attr('placeholder', t('inspector.search_feature_type'))\n .attr('type', 'search')\n .call(utilNoAuto)\n .on('keydown', initialKeydown)\n .on('keypress', keypress)\n .on('input', debounce(inputevent));\n\n if (_autofocus) {\n search.node().focus();\n\n // Safari 14 doesn't always like to focus immediately,\n // so try again on the next pass\n setTimeout(function() {\n search.node().focus();\n }, 0);\n }\n\n var listWrap = selection\n .append('div')\n .attr('class', 'inspector-body');\n\n var entityPresets = _entityIDs.map(entityID =>\n presetManager.match(context.graph().entity(entityID), context.graph()));\n var list = listWrap\n .append('div')\n .attr('class', 'preset-list')\n .call(drawList, presetManager.defaults(entityGeometries()[0], 36, !context.inIntro(), _currLoc, entityPresets));\n\n context.features().on('change.preset-list', updateForFeatureHiddenState);\n }\n\n\n function drawList(list, presets) {\n presets = presets.matchAllGeometry(entityGeometries());\n var collection = presets.collection.reduce(function(collection, preset) {\n if (!preset) return collection;\n\n if (preset.members) {\n if (preset.members.collection.filter(function(preset) {\n return preset.addable();\n }).length > 1) {\n collection.push(CategoryItem(preset));\n }\n } else if (preset.addable()) {\n collection.push(PresetItem(preset));\n }\n return collection;\n }, []);\n\n var items = list.selectChildren('.preset-list-item')\n .data(collection, function(d) { return d.preset.id; });\n\n items.order();\n\n items.exit()\n .remove();\n\n items.enter()\n .append('div')\n .attr('class', function(item) { return 'preset-list-item preset-' + item.preset.id.replaceAll('/', '-'); })\n .classed('current', function(item) { return _currentPresets.indexOf(item.preset) !== -1; })\n .each(function(item) { d3_select(this).call(item); })\n .style('opacity', 0)\n .transition()\n .style('opacity', 1);\n\n updateForFeatureHiddenState();\n }\n\n function itemKeydown(d3_event) {\n // the actively focused item\n var item = d3_select(this.closest('.preset-list-item'));\n var parentItem = d3_select(item.node().parentNode.closest('.preset-list-item'));\n\n // arrow down, move focus to the next, lower item\n if (d3_event.keyCode === utilKeybinding.keyCodes['\u2193']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // the next item in the list at the same level\n var nextItem = d3_select(item.node().nextElementSibling);\n // if there is no next item in this list\n if (nextItem.empty()) {\n // if there is a parent item\n if (!parentItem.empty()) {\n // the item is the last item of a sublist,\n // select the next item at the parent level\n nextItem = d3_select(parentItem.node().nextElementSibling);\n }\n // if the focused item is expanded\n } else if (d3_select(this).classed('expanded')) {\n // select the first subitem instead\n nextItem = item.select('.subgrid .preset-list-item:first-child');\n }\n if (!nextItem.empty()) {\n // focus on the next item\n nextItem.select('.preset-list-button').node().focus();\n }\n\n // arrow up, move focus to the previous, higher item\n } else if (d3_event.keyCode === utilKeybinding.keyCodes['\u2191']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // the previous item in the list at the same level\n var previousItem = d3_select(item.node().previousElementSibling);\n\n // if there is no previous item in this list\n if (previousItem.empty()) {\n // if there is a parent item\n if (!parentItem.empty()) {\n // the item is the first subitem of a sublist select the parent item\n previousItem = parentItem;\n }\n // if the previous item is expanded\n } else if (previousItem.select('.preset-list-button').classed('expanded')) {\n // select the last subitem of the sublist of the previous item\n previousItem = previousItem.select('.subgrid .preset-list-item:last-child');\n }\n\n if (!previousItem.empty()) {\n // focus on the previous item\n previousItem.select('.preset-list-button').node().focus();\n } else {\n // the focus is at the top of the list, move focus back to the search field\n var search = d3_select(this.closest('.preset-list-pane')).select('.preset-search-input');\n search.node().focus();\n }\n\n // arrow left, move focus to the parent item if there is one\n } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2192' : '\u2190']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // if there is a parent item, focus on the parent item\n if (!parentItem.empty()) {\n parentItem.select('.preset-list-button').node().focus();\n }\n\n // arrow right, choose this item\n } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2190' : '\u2192']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n item.datum().choose.call(d3_select(this).node());\n }\n }\n\n\n function CategoryItem(preset) {\n var box, sublist, shown = false;\n\n function item(selection) {\n var wrap = selection.append('div')\n .attr('class', 'preset-list-button-wrap category');\n\n function click() {\n var isExpanded = d3_select(this).classed('expanded');\n var iconName = isExpanded ?\n (localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward') : '#iD-icon-down';\n d3_select(this)\n .classed('expanded', !isExpanded)\n .attr('title', !isExpanded ? t('icons.collapse') : t('icons.expand'));\n d3_select(this).selectAll('div.label-inner svg.icon use')\n .attr('href', iconName);\n item.choose();\n }\n\n var geometries = entityGeometries();\n\n var button = wrap\n .append('button')\n .attr('class', 'preset-list-button')\n .attr('title', t('icons.expand'))\n .classed('expanded', false)\n .call(uiPresetIcon()\n .geometry(geometries.length === 1 && geometries[0])\n .preset(preset))\n .on('click', click)\n .on('keydown', function(d3_event) {\n // right arrow, expand the focused item\n if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2190' : '\u2192']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // if the item isn't expanded\n if (!d3_select(this).classed('expanded')) {\n // toggle expansion (expand the item)\n click.call(this, d3_event);\n }\n // left arrow, collapse the focused item\n } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2192' : '\u2190']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // if the item is expanded\n if (d3_select(this).classed('expanded')) {\n // toggle expansion (collapse the item)\n click.call(this, d3_event);\n }\n } else {\n itemKeydown.call(this, d3_event);\n }\n });\n\n var label = button\n .append('div')\n .attr('class', 'label')\n .append('div')\n .attr('class', 'label-inner');\n\n label\n .append('div')\n .attr('class', 'namepart')\n .call(svgIcon((localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'))\n .append('span')\n .call(preset.nameLabel())\n .append('span').text('\u2026');\n\n box = selection.append('div')\n .attr('class', 'subgrid')\n .style('max-height', '0px')\n .style('opacity', 0);\n\n box.append('div')\n .attr('class', 'arrow');\n\n sublist = box.append('div')\n .attr('class', 'preset-list fillL3');\n }\n\n\n item.choose = function() {\n if (!box || !sublist) return;\n\n if (shown) {\n shown = false;\n box.transition()\n .duration(200)\n .style('opacity', '0')\n .style('max-height', '0px')\n .style('padding-bottom', '0px');\n } else {\n shown = true;\n var members = preset.members.matchAllGeometry(entityGeometries());\n sublist.call(drawList, members);\n box.transition()\n .duration(200)\n .style('opacity', '1')\n .style('max-height', 200 + members.collection.length * 190 + 'px')\n .style('padding-bottom', '10px');\n }\n };\n\n item.preset = preset;\n return item;\n }\n\n\n function PresetItem(preset) {\n function item(selection) {\n var wrap = selection.append('div')\n .attr('class', 'preset-list-button-wrap');\n\n var geometries = entityGeometries();\n\n var button = wrap.append('button')\n .attr('class', 'preset-list-button')\n .call(uiPresetIcon()\n .geometry(geometries.length === 1 && geometries[0])\n .preset(preset))\n .on('click', item.choose)\n .on('keydown', itemKeydown);\n\n var label = button\n .append('div')\n .attr('class', 'label')\n .append('div')\n .attr('class', 'label-inner');\n\n var nameparts = [\n preset.nameLabel(),\n preset.subtitleLabel()\n ].filter(Boolean);\n\n label.selectAll('.namepart')\n .data(nameparts, d => d.stringId)\n .enter()\n .append('div')\n .attr('class', 'namepart')\n .text('')\n .each(function(d) { d(d3_select(this)); });\n\n wrap.call(item.reference.button);\n selection.call(item.reference.body);\n }\n\n item.choose = function() {\n if (d3_select(this).classed('disabled')) return;\n if (!context.inIntro()) {\n presetManager.setMostRecent(preset, entityGeometries()[0]);\n }\n context.perform(\n function(graph) {\n for (var i in _entityIDs) {\n var entityID = _entityIDs[i];\n var oldPreset = presetManager.match(graph.entity(entityID), graph);\n graph = actionChangePreset(entityID, oldPreset, preset)(graph);\n }\n return graph;\n },\n t('operations.change_tags.annotation')\n );\n\n context.validator().validate(); // rerun validation\n dispatch.call('choose', this, preset);\n };\n\n item.help = function(d3_event) {\n d3_event.stopPropagation();\n item.reference.toggle();\n };\n\n item.preset = preset;\n item.reference = uiTagReference(preset.reference(), context);\n\n return item;\n }\n\n\n function updateForFeatureHiddenState() {\n if (!_entityIDs.every(context.hasEntity)) return;\n\n var geometries = entityGeometries();\n var button = context.container().selectAll('.preset-list .preset-list-button');\n\n // remove existing tooltips\n button.call(uiTooltip().destroyAny);\n\n button.each(function(item, index) {\n var hiddenPresetFeaturesId;\n for (var i in geometries) {\n hiddenPresetFeaturesId = context.features().isHiddenPreset(item.preset, geometries[i]);\n if (hiddenPresetFeaturesId) break;\n }\n var isHiddenPreset = !context.inIntro() &&\n !!hiddenPresetFeaturesId &&\n (_currentPresets.length !== 1 || item.preset !== _currentPresets[0]);\n\n d3_select(this)\n .classed('disabled', isHiddenPreset);\n\n if (isHiddenPreset) {\n var isAutoHidden = context.features().autoHidden(hiddenPresetFeaturesId);\n d3_select(this).call(uiTooltip()\n .title(() => t.append('inspector.hidden_preset.' + (isAutoHidden ? 'zoom' : 'manual'), {\n features: t('feature.' + hiddenPresetFeaturesId + '.description')\n }))\n .placement(index < 2 ? 'bottom' : 'top')\n );\n }\n });\n }\n\n presetList.autofocus = function(val) {\n if (!arguments.length) return _autofocus;\n _autofocus = val;\n return presetList;\n };\n\n presetList.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n\n _entityIDs = val;\n _currLoc = null;\n\n if (_entityIDs && _entityIDs.length) {\n // calculate current location\n const extent = _entityIDs.reduce(function(extent, entityID) {\n var entity = context.graph().entity(entityID);\n return extent.extend(entity.extent(context.graph()));\n }, geoExtent());\n _currLoc = extent.center();\n\n // match presets\n var presets = _entityIDs.map(function(entityID) {\n return presetManager.match(context.entity(entityID), context.graph());\n });\n presetList.presets(presets);\n }\n\n return presetList;\n };\n\n presetList.presets = function(val) {\n if (!arguments.length) return _currentPresets;\n _currentPresets = val;\n return presetList;\n };\n\n function entityGeometries() {\n\n var counts = {};\n\n for (var i in _entityIDs) {\n var entityID = _entityIDs[i];\n var entity = context.entity(entityID);\n var geometry = entity.geometry(context.graph());\n\n // Treat entities on addr:interpolation lines as points, not vertices (#3241)\n if (geometry === 'vertex' && entity.isOnAddressLine(context.graph())) {\n geometry = 'point';\n }\n\n if (!counts[geometry]) counts[geometry] = 0;\n counts[geometry] += 1;\n }\n\n return Object.keys(counts).sort(function(geom1, geom2) {\n return counts[geom2] - counts[geom1];\n });\n }\n\n return utilRebind(presetList, dispatch, 'on');\n}\n", "import { interpolate as d3_interpolate } from 'd3-interpolate';\nimport { select as d3_select } from 'd3-selection';\n\nimport { uiEntityEditor } from './entity_editor';\nimport { uiPresetList } from './preset_list';\nimport { uiViewOnOSM } from './view_on_osm';\n\n\nexport function uiInspector(context) {\n var presetList = uiPresetList(context);\n var entityEditor = uiEntityEditor(context);\n var wrap = d3_select(null),\n presetPane = d3_select(null),\n editorPane = d3_select(null);\n var _state = 'select';\n var _entityIDs;\n var _newFeature = false;\n\n\n function inspector(selection) {\n presetList\n .entityIDs(_entityIDs)\n .autofocus(_newFeature)\n .on('choose', inspector.setPreset)\n .on('cancel', function() {\n inspector.setPreset();\n });\n\n entityEditor\n .state(_state)\n .entityIDs(_entityIDs)\n .on('choose', inspector.showList);\n\n wrap = selection.selectAll('.panewrap')\n .data([0]);\n\n var enter = wrap.enter()\n .append('div')\n .attr('class', 'panewrap');\n\n enter\n .append('div')\n .attr('class', 'preset-list-pane pane');\n\n enter\n .append('div')\n .attr('class', 'entity-editor-pane pane');\n\n wrap = wrap.merge(enter);\n presetPane = wrap.selectAll('.preset-list-pane');\n editorPane = wrap.selectAll('.entity-editor-pane');\n\n function shouldDefaultToPresetList() {\n // always show the inspector on hover\n if (_state !== 'select') return false;\n\n // can only change preset on single selection\n if (_entityIDs.length !== 1) return false;\n\n var entityID = _entityIDs[0];\n var entity = context.hasEntity(entityID);\n if (!entity) return false;\n\n // default to inspector if there are already tags\n if (entity.hasNonGeometryTags()) return false;\n\n // prompt to select preset if feature is new and untagged\n if (_newFeature) return true;\n\n // all existing features except vertices should default to inspector\n if (entity.geometry(context.graph()) !== 'vertex') return false;\n\n // show vertex relations if any\n if (context.graph().parentRelations(entity).length) return false;\n\n // show vertex issues if there are any\n if (context.validator().getEntityIssues(entityID).length) return false;\n\n // show turn restriction editor for junction vertices\n if (entity.type === 'node' && entity.isHighwayIntersection(context.graph())) return false;\n\n // otherwise show preset list for uninteresting vertices\n return true;\n }\n\n if (shouldDefaultToPresetList()) {\n wrap.style('right', '-100%');\n editorPane.classed('hide', true);\n presetPane.classed('hide', false)\n .call(presetList);\n } else {\n wrap.style('right', '0%');\n presetPane.classed('hide', true);\n editorPane.classed('hide', false)\n .call(entityEditor);\n }\n\n var footer = selection.selectAll('.footer')\n .data([0]);\n\n footer = footer.enter()\n .append('div')\n .attr('class', 'footer')\n .merge(footer);\n\n footer\n .call(uiViewOnOSM(context)\n .what(context.hasEntity(_entityIDs.length === 1 && _entityIDs[0]))\n );\n }\n\n inspector.showList = function(presets) {\n\n presetPane.classed('hide', false);\n\n wrap.transition()\n .styleTween('right', function() {\n return d3_interpolate('0%', '-100%');\n })\n .on('end', function () {\n editorPane.classed('hide', true);\n });\n\n if (presets) {\n presetList.presets(presets);\n }\n\n presetPane\n .call(presetList.autofocus(true));\n };\n\n inspector.setPreset = function(preset) {\n\n // upon setting multipolygon, go to the area preset list instead of the editor\n if (preset && preset.id === 'type/multipolygon') {\n presetPane\n .call(presetList.autofocus(true));\n\n } else {\n editorPane.classed('hide', false);\n wrap.transition()\n .styleTween('right', function() {\n return d3_interpolate('-100%', '0%');\n })\n .on('end', function () {\n presetPane.classed('hide', true);\n });\n\n if (preset) {\n entityEditor.presets([preset]);\n }\n editorPane\n .call(entityEditor);\n }\n\n };\n\n inspector.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n entityEditor.state(_state);\n\n // remove any old field help overlay that might have gotten attached to the inspector\n context.container().selectAll('.field-help-body').remove();\n\n return inspector;\n };\n\n\n inspector.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return inspector;\n };\n\n\n inspector.newFeature = function(val) {\n if (!arguments.length) return _newFeature;\n _newFeature = val;\n return inspector;\n };\n\n\n return inspector;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { utilArrayIdentical } from '../util/array';\nimport { utilFastMouse } from '../util';\nimport { osmEntity, osmNote, QAItem } from '../osm';\nimport { services } from '../services';\nimport { uiDataEditor } from './data_editor';\nimport { uiFeatureList } from './feature_list';\nimport { uiInspector } from './inspector';\nimport { uiOsmoseEditor } from './osmose_editor';\nimport { uiNoteEditor } from './note_editor';\nimport { localizer } from '../core/localizer';\n\n\nexport function uiSidebar(context) {\n var inspector = uiInspector(context);\n var dataEditor = uiDataEditor(context);\n var noteEditor = uiNoteEditor(context);\n var osmoseEditor = uiOsmoseEditor(context);\n var _current;\n var _wasData = false;\n var _wasNote = false;\n var _wasQaItem = false;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function sidebar(selection) {\n var container = context.container();\n var minWidth = 240;\n var sidebarWidth;\n var containerWidth;\n var dragOffset;\n\n // Set the initial width constraints\n selection\n .style('min-width', minWidth + 'px')\n .style('max-width', '400px')\n .style('width', '33.3333%');\n\n var resizer = selection\n .append('div')\n .attr('class', 'sidebar-resizer')\n .on(_pointerPrefix + 'down.sidebar-resizer', pointerdown);\n\n var downPointerId, lastClientX, containerLocGetter;\n\n function pointerdown(d3_event) {\n if (downPointerId) return;\n\n if ('button' in d3_event && d3_event.button !== 0) return;\n\n downPointerId = d3_event.pointerId || 'mouse';\n\n lastClientX = d3_event.clientX;\n\n containerLocGetter = utilFastMouse(container.node());\n\n // offset from edge of sidebar-resizer\n dragOffset = utilFastMouse(resizer.node())(d3_event)[0] - 1;\n\n sidebarWidth = selection.node().getBoundingClientRect().width;\n containerWidth = container.node().getBoundingClientRect().width;\n var widthPct = (sidebarWidth / containerWidth) * 100;\n selection\n .style('width', widthPct + '%') // lock in current width\n .style('max-width', '85%'); // but allow larger widths\n\n resizer.classed('dragging', true);\n\n d3_select(window)\n .on('touchmove.sidebar-resizer', function(d3_event) {\n // disable page scrolling while resizing on touch input\n d3_event.preventDefault();\n }, { passive: false })\n .on(_pointerPrefix + 'move.sidebar-resizer', pointermove)\n .on(_pointerPrefix + 'up.sidebar-resizer pointercancel.sidebar-resizer', pointerup);\n }\n\n function pointermove(d3_event) {\n\n if (downPointerId !== (d3_event.pointerId || 'mouse')) return;\n\n d3_event.preventDefault();\n\n var dx = d3_event.clientX - lastClientX;\n\n lastClientX = d3_event.clientX;\n\n var isRTL = (localizer.textDirection() === 'rtl');\n var scaleX = isRTL ? 0 : 1;\n var xMarginProperty = isRTL ? 'margin-right' : 'margin-left';\n\n var x = containerLocGetter(d3_event)[0] - dragOffset;\n sidebarWidth = isRTL ? containerWidth - x : x;\n\n var isCollapsed = selection.classed('collapsed');\n var shouldCollapse = sidebarWidth < minWidth;\n\n selection.classed('collapsed', shouldCollapse);\n\n if (shouldCollapse) {\n if (!isCollapsed) {\n selection\n .style(xMarginProperty, '-400px')\n .style('width', '400px');\n\n context.ui().onResize([(sidebarWidth - dx) * scaleX, 0]);\n }\n\n } else {\n var widthPct = (sidebarWidth / containerWidth) * 100;\n selection\n .style(xMarginProperty, null)\n .style('width', widthPct + '%');\n\n if (isCollapsed) {\n context.ui().onResize([-sidebarWidth * scaleX, 0]);\n } else {\n context.ui().onResize([-dx * scaleX, 0]);\n }\n }\n }\n\n function pointerup(d3_event) {\n if (downPointerId !== (d3_event.pointerId || 'mouse')) return;\n\n downPointerId = null;\n\n resizer.classed('dragging', false);\n\n d3_select(window)\n .on('touchmove.sidebar-resizer', null)\n .on(_pointerPrefix + 'move.sidebar-resizer', null)\n .on(_pointerPrefix + 'up.sidebar-resizer pointercancel.sidebar-resizer', null);\n }\n\n var featureListWrap = selection\n .append('div')\n .attr('class', 'feature-list-pane')\n .call(uiFeatureList(context));\n\n var inspectorWrap = selection\n .append('div')\n .attr('class', 'inspector-hidden inspector-wrap');\n\n var hoverModeSelect = function(targets) {\n context.container().selectAll('.feature-list-item button').classed('hover', false);\n\n if (context.selectedIDs().length > 1 &&\n targets && targets.length) {\n\n var elements = context.container().selectAll('.feature-list-item button')\n .filter(function (node) {\n return targets.indexOf(node) !== -1;\n });\n\n if (!elements.empty()) {\n elements.classed('hover', true);\n }\n }\n };\n\n sidebar.hoverModeSelect = throttle(hoverModeSelect, 200);\n\n function hover(targets) {\n var datum = targets && targets.length && targets[0];\n if (datum && datum.__featurehash__) { // hovering on data\n _wasData = true;\n sidebar\n .show(dataEditor.datum(datum));\n\n selection.selectAll('.sidebar-component')\n .classed('inspector-hover', true);\n\n } else if (datum instanceof osmNote) {\n if (context.mode().id === 'drag-note') return;\n _wasNote = true;\n\n var osm = services.osm;\n if (osm) {\n datum = osm.getNote(datum.id); // marker may contain stale data - get latest\n }\n\n sidebar\n .show(noteEditor.note(datum));\n\n selection.selectAll('.sidebar-component')\n .classed('inspector-hover', true);\n\n } else if (datum instanceof QAItem) {\n _wasQaItem = true;\n\n var errService = services[datum.service];\n if (errService) {\n // marker may contain stale data - get latest\n datum = errService.getError(datum.id);\n }\n\n // Currently only one possible service\n var errEditor;\n if (datum.service === 'osmose') {\n errEditor = osmoseEditor;\n }\n\n context.container().selectAll('.qaItem.' + datum.service)\n .classed('hover', function(d) { return d.id === datum.id; });\n\n sidebar\n .show(errEditor.error(datum));\n\n selection.selectAll('.sidebar-component')\n .classed('inspector-hover', true);\n\n } else if (!_current && (datum instanceof osmEntity)) {\n featureListWrap\n .classed('inspector-hidden', true);\n\n inspectorWrap\n .classed('inspector-hidden', false)\n .classed('inspector-hover', true);\n\n if (!inspector.entityIDs() || !utilArrayIdentical(inspector.entityIDs(), [datum.id]) || inspector.state() !== 'hover') {\n inspector\n .state('hover')\n .entityIDs([datum.id])\n .newFeature(false);\n\n inspectorWrap\n .call(inspector);\n }\n\n } else if (!_current) {\n featureListWrap\n .classed('inspector-hidden', false);\n inspectorWrap\n .classed('inspector-hidden', true);\n inspector\n .state('hide');\n\n } else if (_wasData || _wasNote || _wasQaItem) {\n _wasNote = false;\n _wasData = false;\n _wasQaItem = false;\n context.container().selectAll('.note').classed('hover', false);\n context.container().selectAll('.qaItem').classed('hover', false);\n sidebar.hide();\n }\n }\n\n sidebar.hover = throttle(hover, 200);\n\n\n sidebar.intersects = function(extent) {\n var rect = selection.node().getBoundingClientRect();\n return extent.intersects([\n context.projection.invert([0, rect.height]),\n context.projection.invert([rect.width, 0])\n ]);\n };\n\n\n sidebar.select = function(ids, newFeature) {\n sidebar.hide();\n\n if (ids && ids.length) {\n\n var entity = ids.length === 1 && context.entity(ids[0]);\n if (entity && newFeature && selection.classed('collapsed')) {\n // uncollapse the sidebar\n var extent = entity.extent(context.graph());\n sidebar.expand(sidebar.intersects(extent));\n }\n\n featureListWrap\n .classed('inspector-hidden', true);\n\n inspectorWrap\n .classed('inspector-hidden', false)\n .classed('inspector-hover', false);\n\n // reload the UI even if the ids are the same since the entities\n // themselves may have changed\n inspector\n .state('select')\n .entityIDs(ids)\n .newFeature(newFeature);\n\n inspectorWrap\n .call(inspector);\n\n } else {\n inspector\n .state('hide');\n }\n };\n\n\n sidebar.showPresetList = function() {\n inspector.showList();\n };\n\n\n sidebar.show = function(component, element) {\n featureListWrap\n .classed('inspector-hidden', true);\n inspectorWrap\n .classed('inspector-hidden', true);\n\n if (_current) _current.remove();\n _current = selection\n .append('div')\n .attr('class', 'sidebar-component')\n .call(component, element);\n };\n\n\n sidebar.hide = function() {\n featureListWrap\n .classed('inspector-hidden', false);\n inspectorWrap\n .classed('inspector-hidden', true);\n\n if (_current) _current.remove();\n _current = null;\n };\n\n\n sidebar.expand = function(moveMap) {\n if (selection.classed('collapsed')) {\n sidebar.toggle(moveMap);\n }\n };\n\n\n sidebar.collapse = function(moveMap) {\n if (!selection.classed('collapsed')) {\n sidebar.toggle(moveMap);\n }\n };\n\n\n sidebar.toggle = function(moveMap) {\n\n // Don't allow sidebar to toggle when the user is in the walkthrough.\n if (context.inIntro()) return;\n\n var isCollapsed = selection.classed('collapsed');\n var isCollapsing = !isCollapsed;\n var isRTL = (localizer.textDirection() === 'rtl');\n var scaleX = isRTL ? 0 : 1;\n var xMarginProperty = isRTL ? 'margin-right' : 'margin-left';\n\n sidebarWidth = selection.node().getBoundingClientRect().width;\n\n // switch from % to px\n selection.style('width', sidebarWidth + 'px');\n\n var startMargin, endMargin;\n if (isCollapsing) {\n startMargin = 0;\n endMargin = -sidebarWidth;\n } else {\n startMargin = -sidebarWidth;\n endMargin = 0;\n }\n let lastMargin = startMargin;\n\n if (!isCollapsing) {\n // unhide the sidebar's content before it transitions onscreen\n selection.classed('collapsed', isCollapsing);\n }\n\n selection\n .transition()\n .style(xMarginProperty, endMargin + 'px')\n .tween('panner', function() {\n var i = d3_interpolateNumber(startMargin, endMargin);\n return function(t) {\n var dx = lastMargin - Math.round(i(t));\n lastMargin = lastMargin - dx;\n context.ui().onResize(moveMap ? undefined : [dx * scaleX, 0]);\n };\n })\n .on('end', function() {\n if (isCollapsing) {\n // hide the sidebar's content after it transitions offscreen\n selection.classed('collapsed', isCollapsing);\n }\n\n // switch back from px to %\n if (!isCollapsing) {\n var containerWidth = container.node().getBoundingClientRect().width;\n var widthPct = (sidebarWidth / containerWidth) * 100;\n selection\n .style(xMarginProperty, null)\n .style('width', widthPct + '%');\n }\n });\n };\n\n // toggle the sidebar collapse when double-clicking the resizer\n resizer.on('dblclick', function(d3_event) {\n d3_event.preventDefault();\n if (d3_event.sourceEvent) {\n d3_event.sourceEvent.preventDefault();\n }\n sidebar.toggle();\n });\n\n // ensure hover sidebar is closed when zooming out beyond editable zoom\n context.map().on('crossEditableZoom.sidebar', function(within) {\n if (!within && !selection.select('.inspector-hover').empty()) {\n hover([]);\n }\n });\n }\n\n sidebar.showPresetList = function() {};\n sidebar.hover = function() {};\n sidebar.hover.cancel = function() {};\n sidebar.intersects = function() {};\n sidebar.select = function() {};\n sidebar.show = function() {};\n sidebar.hide = function() {};\n sidebar.expand = function() {};\n sidebar.collapse = function() {};\n sidebar.toggle = function() {};\n\n return sidebar;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { modeBrowse } from '../modes/browse';\n\n\nexport function uiSourceSwitch(context) {\n var keys;\n\n\n function click(d3_event) {\n d3_event.preventDefault();\n\n var osm = context.connection();\n if (!osm) return;\n\n if (context.inIntro()) return;\n\n if (context.history().hasChanges() &&\n !window.confirm(t('source_switch.lose_changes'))) return;\n\n var isLive = d3_select(this)\n .classed('live');\n\n isLive = !isLive;\n context.enter(modeBrowse(context));\n context.history().clearSaved(); // remove saved history\n context.flush(); // remove stored data\n\n d3_select(this)\n .classed('live', isLive)\n .classed('chip', isLive)\n .text('')\n .call(isLive ? t.append('source_switch.live') : t.append('source_switch.dev'));\n\n osm.switch(isLive ? keys[0] : keys[1]); // switch connection (warning: dispatches 'change' event)\n }\n\n var sourceSwitch = function(selection) {\n selection\n .append('a')\n .attr('href', '#')\n .call(t.append('source_switch.live'))\n .attr('class', 'live chip')\n .on('click', click);\n };\n\n\n sourceSwitch.keys = function(_) {\n if (!arguments.length) return keys;\n keys = _;\n return sourceSwitch;\n };\n\n\n return sourceSwitch;\n}\n", "export function uiSpinner(context) {\n var osm = context.connection();\n\n\n return function(selection) {\n var img = selection\n .append('img')\n .attr('src', context.imagePath('loader-black.gif'))\n .style('opacity', 0);\n\n if (osm) {\n osm\n .on('loading.spinner', function() {\n img.transition()\n .style('opacity', 1);\n })\n .on('loaded.spinner', function() {\n img.transition()\n .style('opacity', 0);\n });\n }\n };\n}\n", "import { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\n\nexport function uiSectionPrivacy(context) {\n let section = uiSection('preferences-third-party', context)\n .label(() => t.append('preferences.privacy.title'))\n .disclosureContent(renderDisclosureContent);\n\n function renderDisclosureContent(selection) {\n // enter\n selection.selectAll('.privacy-options-list')\n .data([0])\n .enter()\n .append('ul')\n .attr('class', 'layer-list privacy-options-list');\n\n let thirdPartyIconsEnter = selection.select('.privacy-options-list')\n .selectAll('.privacy-third-party-icons-item')\n .data([prefs('preferences.privacy.thirdpartyicons') || 'true'])\n .enter()\n .append('li')\n .attr('class', 'privacy-third-party-icons-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('preferences.privacy.third_party_icons.tooltip'))\n .placement('bottom')\n );\n\n thirdPartyIconsEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', (d3_event, d) => {\n d3_event.preventDefault();\n prefs('preferences.privacy.thirdpartyicons', d === 'true' ? 'false' : 'true');\n });\n\n thirdPartyIconsEnter\n .append('span')\n .call(t.append('preferences.privacy.third_party_icons.description'));\n\n // update\n selection.selectAll('.privacy-third-party-icons-item')\n .classed('active', d => d === 'true')\n .select('input')\n .property('checked', d => d === 'true');\n\n // Privacy Policy link\n selection.selectAll('.privacy-link')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'privacy-link')\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', 'https://github.com/openstreetmap/iD/blob/release/PRIVACY.md')\n .append('span')\n .call(t.append('preferences.privacy.privacy_link'));\n\n }\n\n prefs.onChange('preferences.privacy.thirdpartyicons', section.reRender);\n\n return section;\n}\n", "import { prefs } from '../core/preferences';\nimport { fileFetcher } from '../core/file_fetcher';\nimport { t } from '../core/localizer';\nimport { uiIntro } from './intro';\nimport { uiModal } from './modal';\nimport { uiSectionPrivacy } from './sections/privacy';\n\n\nexport function uiSplash(context) {\n return (selection) => {\n // Exception - if there are restorable changes, skip this splash screen.\n // This is because we currently only support one `uiModal` at a time\n // and we need to show them `uiRestore`` instead of this one.\n if (context.history().hasRestorableChanges()) return;\n\n // If user has not seen this version of the privacy policy, show the splash again.\n let updateMessage = '';\n const sawPrivacyVersion = prefs('sawPrivacyVersion');\n let showSplash = !prefs('sawSplash');\n if (sawPrivacyVersion && sawPrivacyVersion !== context.privacyVersion) {\n updateMessage = t('splash.privacy_update');\n showSplash = true;\n }\n\n if (!showSplash) return;\n\n prefs('sawSplash', true);\n prefs('sawPrivacyVersion', context.privacyVersion);\n\n // fetch intro graph data now, while user is looking at the splash screen\n fileFetcher.get('intro_graph');\n\n let modalSelection = uiModal(selection);\n\n modalSelection.select('.modal')\n .attr('class', 'modal-splash modal');\n\n let introModal = modalSelection.select('.content')\n .append('div')\n .attr('class', 'fillL');\n\n introModal\n .append('div')\n .attr('class','modal-section')\n .append('h3')\n .call(t.append('splash.welcome'));\n\n let modalSection = introModal\n .append('div')\n .attr('class','modal-section');\n\n modalSection\n .append('p')\n .call(t.addOrUpdate('splash.text', {\n version: context.version,\n website: selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/openstreetmap/iD/blob/develop/CHANGELOG.md#whats-new')\n .call(t.addOrUpdate('splash.changelog')),\n github: selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/openstreetmap/iD/issues')\n .text('github.com')\n }));\n\n modalSection\n .append('p')\n .call(t.addOrUpdate('splash.privacy', {\n updateMessage: updateMessage,\n privacyLink: selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/openstreetmap/iD/blob/release/PRIVACY.md')\n .call(t.addOrUpdate('splash.privacy_policy'))\n }));\n\n uiSectionPrivacy(context)\n .label(() => t.append('splash.privacy_settings'))\n .render(modalSection);\n\n let buttonWrap = introModal\n .append('div')\n .attr('class', 'modal-actions');\n\n let walkthrough = buttonWrap\n .append('button')\n .attr('class', 'walkthrough')\n .on('click', () => {\n context.container().call(uiIntro(context));\n modalSelection.close();\n });\n\n walkthrough\n .append('svg')\n .attr('class', 'logo logo-walkthrough')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n walkthrough\n .append('div')\n .call(t.append('splash.walkthrough'));\n\n let startEditing = buttonWrap\n .append('button')\n .attr('class', 'start-editing')\n .on('click', modalSelection.close);\n\n startEditing\n .append('svg')\n .attr('class', 'logo logo-features')\n .append('use')\n .attr('xlink:href', '#iD-logo-features');\n\n startEditing\n .append('div')\n .call(t.append('splash.start'));\n\n modalSelection.select('button.close')\n .attr('class','hide');\n };\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiStatus(context) {\n var osm = context.connection();\n\n\n return function(selection) {\n if (!osm) return;\n\n function update(err, apiStatus) {\n selection.html('');\n\n if (err) {\n if (apiStatus === 'connectionSwitched') {\n // if the connection was just switched, we can't rely on\n // the status (we're getting the status of the previous api)\n return;\n\n } else if (apiStatus === 'rateLimited') {\n if (!osm.authenticated()) {\n selection\n .call(t.append('osm_api_status.message.rateLimit'))\n .append('a')\n .attr('href', '#')\n .attr('class', 'api-status-login')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append('login'))\n .on('click.login', function(d3_event) {\n d3_event.preventDefault();\n osm.authenticate();\n });\n } else {\n selection.call(t.append('osm_api_status.message.rateLimited'));\n }\n } else {\n\n // don't allow retrying too rapidly\n var throttledRetry = throttle(function() {\n // try loading the visible tiles\n context.loadTiles(context.projection);\n // manually reload the status too in case all visible tiles were already loaded\n osm.reloadApiStatus();\n }, 2000);\n\n // eslint-disable-next-line no-warning-comments\n // TODO: nice messages for different error types\n selection\n .call(t.append('osm_api_status.message.error', { suffix: ' ' }))\n .append('a')\n .attr('href', '#')\n // let the user manually retry their connection directly\n .call(t.append('osm_api_status.retry'))\n .on('click.retry', function(d3_event) {\n d3_event.preventDefault();\n throttledRetry();\n });\n }\n\n } else if (apiStatus === 'readonly') {\n selection.call(t.append('osm_api_status.message.readonly'));\n } else if (apiStatus === 'offline') {\n selection.call(t.append('osm_api_status.message.offline'));\n }\n\n selection.attr('class', 'api-status ' + (err ? 'error' : apiStatus));\n }\n\n osm.on('apiStatusChange.uiStatus', update);\n\n context.history().on('storage_error', () => {\n selection.selectAll('span.local-storage-full').remove();\n selection\n .append('span')\n .attr('class', 'local-storage-full')\n .call(t.append('osm_api_status.message.local_storage_full'));\n selection.classed('error', true);\n });\n\n // reload the status periodically regardless of other factors\n window.setInterval(function() {\n osm.reloadApiStatus();\n }, 90000);\n\n // load the initial status in case no OSM data was loaded yet\n osm.reloadApiStatus();\n };\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport {\n modeAddArea,\n modeAddLine,\n modeAddPoint,\n modeBrowse\n} from '../../modes';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiTooltip } from '../tooltip';\n\nexport function uiToolDrawModes(context) {\n\n var tool = {\n id: 'old_modes',\n label: t.append('toolbar.add_feature')\n };\n\n var modes = [\n modeAddPoint(context, {\n title: t.append('modes.add_point.title'),\n button: 'point',\n description: t.append('modes.add_point.description'),\n preset: presetManager.item('point'),\n key: '1'\n }),\n modeAddLine(context, {\n title: t.append('modes.add_line.title'),\n button: 'line',\n description: t.append('modes.add_line.description'),\n preset: presetManager.item('line'),\n key: '2'\n }),\n modeAddArea(context, {\n title: t.append('modes.add_area.title'),\n button: 'area',\n description: t.append('modes.add_area.description'),\n preset: presetManager.item('area'),\n key: '3'\n })\n ];\n\n\n function enabled(\n // eslint-disable-next-line no-unused-vars\n _mode // parameter is currently not used, but might be at some point\n ) {\n return osmEditable();\n }\n\n function osmEditable() {\n return context.editable();\n }\n\n modes.forEach(function(mode) {\n context.keybinding().on(mode.key, function() {\n if (!enabled(mode)) return;\n\n if (mode.id === context.mode().id) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(mode);\n }\n });\n });\n\n tool.render = function(selection) {\n\n var wrap = selection\n .append('div')\n .attr('class', 'joined')\n .style('display', 'flex');\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n\n context.map()\n .on('move.modes', debouncedUpdate)\n .on('drawn.modes', debouncedUpdate);\n\n context\n .on('enter.modes', update);\n\n update();\n\n\n function update() {\n\n var buttons = wrap.selectAll('button.add-button')\n .data(modes, function(d) { return d.id; });\n\n // exit\n buttons.exit()\n .remove();\n\n // enter\n var buttonsEnter = buttons.enter()\n .append('button')\n .attr('class', function(d) { return d.id + ' add-button bar-button'; })\n .on('click.mode-buttons', function(d3_event, d) {\n if (!enabled(d)) return;\n\n // When drawing, ignore accidental clicks on mode buttons - #4042\n var currMode = context.mode().id;\n if (/^draw/.test(currMode)) return;\n\n if (d.id === currMode) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(d);\n }\n })\n .call(uiTooltip()\n .placement('bottom')\n .title(function(d) { return d.description; })\n .keys(function(d) { return [d.key]; })\n .scrollContainer(context.container().select('.top-toolbar'))\n );\n\n buttonsEnter\n .each(function(d) {\n d3_select(this)\n .call(svgIcon('#iD-icon-' + d.button));\n });\n\n buttonsEnter\n .append('span')\n .attr('class', 'label')\n .text('')\n .each(function(mode) { mode.title(d3_select(this)); });\n\n // if we are adding/removing the buttons, check if toolbar has overflowed\n if (buttons.enter().size() || buttons.exit().size()) {\n context.ui().checkOverflow('.top-toolbar', true);\n }\n\n // update\n buttons\n .merge(buttonsEnter)\n .attr('aria-disabled', function(d) { return !enabled(d); })\n .classed('disabled', function(d) { return !enabled(d); })\n .attr('aria-pressed', function(d) { return context.mode() && context.mode().button === d.button; })\n .classed('active', function(d) { return context.mode() && context.mode().button === d.button; });\n }\n };\n\n return tool;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport {\n modeAddNote,\n modeBrowse\n} from '../../modes';\n\nimport { t } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiTooltip } from '../tooltip';\n\nexport function uiToolNotes(context) {\n\n var tool = {\n id: 'notes',\n label: t.append('modes.add_note.label')\n };\n\n var mode = modeAddNote(context);\n\n function enabled() {\n return notesEnabled() && notesEditable();\n }\n\n function notesEnabled() {\n var noteLayer = context.layers().layer('notes');\n return noteLayer && noteLayer.enabled();\n }\n\n function notesEditable() {\n var mode = context.mode();\n return context.map().notesEditable() && mode && mode.id !== 'save';\n }\n\n context.keybinding().on(mode.key, function() {\n if (!enabled()) return;\n\n if (mode.id === context.mode().id) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(mode);\n }\n });\n\n tool.render = function(selection) {\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n\n context.map()\n .on('move.notes', debouncedUpdate)\n .on('drawn.notes', debouncedUpdate);\n\n context\n .on('enter.notes', update);\n\n update();\n\n\n function update() {\n var showNotes = notesEnabled();\n var data = showNotes ? [mode] : [];\n\n var buttons = selection.selectAll('button.add-button')\n .data(data, function(d) { return d.id; });\n\n // exit\n buttons.exit()\n .remove();\n\n // enter\n var buttonsEnter = buttons.enter()\n .append('button')\n .attr('class', function(d) { return d.id + ' add-button bar-button'; })\n .on('click.notes', function(d3_event, d) {\n if (!enabled()) return;\n\n // When drawing, ignore accidental clicks on mode buttons - #4042\n var currMode = context.mode().id;\n if (/^draw/.test(currMode)) return;\n\n if (d.id === currMode) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(d);\n }\n })\n .call(uiTooltip()\n .placement('bottom')\n .title(function(d) { return d.description; })\n .keys(function(d) { return [d.key]; })\n .scrollContainer(context.container().select('.top-toolbar'))\n );\n\n buttonsEnter\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(d.icon || '#iD-icon-' + d.button));\n });\n\n // if we are adding/removing the buttons, check if toolbar has overflowed\n if (buttons.enter().size() || buttons.exit().size()) {\n context.ui().checkOverflow('.top-toolbar', true);\n }\n\n // update\n buttons\n .merge(buttonsEnter)\n .classed('disabled', function() { return !enabled(); })\n .attr('aria-disabled', function() { return !enabled(); })\n .classed('active', function(d) { return context.mode() && context.mode().button === d.button; })\n .attr('aria-pressed', function(d) { return context.mode() && context.mode().button === d.button; });\n }\n };\n\n tool.uninstall = function() {\n context\n .on('enter.editor.notes', null)\n .on('exit.editor.notes', null)\n .on('enter.notes', null);\n\n context.map()\n .on('move.notes', null)\n .on('drawn.notes', null);\n };\n\n return tool;\n}\n", "import { interpolateRgb as d3_interpolateRgb } from 'd3-interpolate';\n\nimport { t } from '../../core/localizer';\nimport { modeSave } from '../../modes';\nimport { svgIcon } from '../../svg';\nimport { uiCmd } from '../cmd';\nimport { uiTooltip } from '../tooltip';\n\n\nexport function uiToolSave(context) {\n\n var tool = {\n id: 'save',\n label: t.append('save.title')\n };\n\n var button = null;\n var tooltipBehavior = null;\n var history = context.history();\n var key = uiCmd('\u2318S');\n var _numChanges = 0;\n\n function isSaving() {\n var mode = context.mode();\n return mode && mode.id === 'save';\n }\n\n function isDisabled() {\n return _numChanges === 0 || isSaving();\n }\n\n function save(d3_event) {\n d3_event.preventDefault();\n if (!context.inIntro() && !isSaving() && history.hasChanges()) {\n context.enter(modeSave(context));\n }\n }\n\n function bgColor(numChanges) {\n var step;\n if (numChanges === 0) {\n return null;\n } else if (numChanges <= 50) {\n step = numChanges / 50;\n return d3_interpolateRgb('#fff0', '#ff08')(step); // transparent -> yellow\n } else {\n step = Math.min((numChanges - 50) / 50, 1.0);\n return d3_interpolateRgb('#ff08', '#f008')(step); // yellow -> red\n }\n }\n\n function updateCount() {\n var val = history.difference().summary().length;\n if (val === _numChanges) return;\n\n _numChanges = val;\n\n if (tooltipBehavior) {\n tooltipBehavior\n .title(() => t.append(_numChanges > 0 ? 'save.help' : 'save.no_changes'))\n .keys([key]);\n }\n\n if (button) {\n button\n .classed('disabled', isDisabled())\n .style('--accent-color', bgColor(_numChanges));\n\n button.select('span.count')\n .text(_numChanges);\n }\n }\n\n\n tool.render = function(selection) {\n tooltipBehavior = uiTooltip()\n .placement('bottom')\n .title(() => t.append('save.no_changes'))\n .keys([key])\n .scrollContainer(context.container().select('.top-toolbar'));\n\n var lastPointerUpType;\n\n button = selection\n .append('button')\n .attr('class', 'save disabled bar-button')\n .on('pointerup', function(d3_event) {\n lastPointerUpType = d3_event.pointerType;\n })\n .on('click', function(d3_event) {\n save(d3_event);\n\n if (_numChanges === 0 && (\n lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen')\n ) {\n // there are no tooltips for touch interactions so flash feedback instead\n context.ui().flash\n .duration(2000)\n .iconName('#iD-icon-save')\n .iconClass('disabled')\n .label(t.append('save.no_changes'))();\n }\n lastPointerUpType = null;\n })\n .call(tooltipBehavior);\n\n button\n .call(svgIcon('#iD-icon-save'));\n\n button\n .append('span')\n .attr('class', 'count')\n .attr('aria-hidden', 'true')\n .text('0');\n\n updateCount();\n\n\n context.keybinding()\n .on(key, save, true);\n\n\n context.history()\n .on('change.save', updateCount);\n\n context\n .on('enter.save', function() {\n if (button) {\n button\n .classed('disabled', isDisabled());\n\n if (isSaving()) {\n button.call(tooltipBehavior.hide);\n }\n }\n });\n };\n\n\n tool.uninstall = function() {\n context.keybinding()\n .off(key, true);\n\n context.history()\n .on('change.save', null);\n\n context\n .on('enter.save', null);\n\n button = null;\n tooltipBehavior = null;\n };\n\n return tool;\n}\n", "import { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiTooltip } from '../tooltip';\n\nexport function uiToolSidebarToggle(context) {\n\n var tool = {\n id: 'sidebar_toggle',\n label: t.append('toolbar.inspect')\n };\n\n tool.render = function(selection) {\n selection\n .append('button')\n .attr('class', 'bar-button')\n .attr('aria-label', t('sidebar.tooltip'))\n .on('click', function() {\n context.ui().sidebar.toggle();\n })\n .call(uiTooltip()\n .placement('bottom')\n .title(() => t.append('sidebar.tooltip'))\n .keys([t('sidebar.key')])\n .scrollContainer(context.container().select('.top-toolbar'))\n )\n .call(svgIcon('#iD-icon-sidebar-' + (localizer.textDirection() === 'rtl' ? 'right' : 'left')));\n };\n\n return tool;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiCmd } from '../cmd';\nimport { uiTooltip } from '../tooltip';\n\n\nexport function uiToolUndoRedo(context) {\n\n var tool = {\n id: 'undo_redo',\n label: t.append('toolbar.undo_redo')\n };\n\n var commands = [{\n id: 'undo',\n cmd: uiCmd('\u2318Z'),\n action: function() {\n context.undo();\n },\n annotation: function() {\n return context.history().undoAnnotation();\n },\n icon: 'iD-icon-' + (localizer.textDirection() === 'rtl' ? 'redo' : 'undo')\n }, {\n id: 'redo',\n cmd: uiCmd('\u2318\u21E7Z'),\n action: function() {\n context.redo();\n },\n annotation: function() {\n return context.history().redoAnnotation();\n },\n icon: 'iD-icon-' + (localizer.textDirection() === 'rtl' ? 'undo' : 'redo')\n }];\n\n\n function editable() {\n return context.mode() && context.mode().id !== 'save' && context.map().editableDataEnabled(true /* ignore min zoom */);\n }\n\n\n tool.render = function(selection) {\n var tooltipBehavior = uiTooltip()\n .placement('bottom')\n .title(function (d) {\n return d.annotation() ?\n t.append(d.id + '.tooltip', { action: d.annotation() }) :\n t.append(d.id + '.nothing');\n })\n .keys(function(d) {\n return [d.cmd];\n })\n .scrollContainer(context.container().select('.top-toolbar'));\n\n var lastPointerUpType;\n\n var buttons = selection.selectAll('button')\n .data(commands)\n .enter()\n .append('button')\n .attr('class', function(d) { return 'disabled ' + d.id + '-button bar-button'; })\n .on('pointerup', function(d3_event) {\n // `pointerup` is always called before `click`\n lastPointerUpType = d3_event.pointerType;\n })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n\n var annotation = d.annotation();\n\n if (editable() && annotation) {\n d.action();\n }\n\n if (editable() && (\n lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen')\n ) {\n // there are no tooltips for touch interactions so flash feedback instead\n\n var label = annotation ?\n t.append(d.id + '.tooltip', { action: annotation }) :\n t.append(d.id + '.nothing');\n context.ui().flash\n .duration(2000)\n .iconName('#' + d.icon)\n .iconClass(annotation ? '' : 'disabled')\n .label(label)();\n }\n lastPointerUpType = null;\n })\n .call(tooltipBehavior);\n\n buttons.each(function(d) {\n d3_select(this)\n .call(svgIcon('#' + d.icon));\n });\n\n context.keybinding()\n .on(commands[0].cmd, function(d3_event) {\n d3_event.preventDefault();\n if (editable()) commands[0].action();\n })\n .on(commands[1].cmd, function(d3_event) {\n d3_event.preventDefault();\n if (editable()) commands[1].action();\n });\n\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n\n context.map()\n .on('move.undo_redo', debouncedUpdate)\n .on('drawn.undo_redo', debouncedUpdate);\n\n context.history()\n .on('change.undo_redo', function(difference) {\n if (difference) update();\n });\n\n context\n .on('enter.undo_redo', update);\n\n\n function update() {\n buttons\n .classed('disabled', function(d) {\n return !editable() || !d.annotation();\n })\n .each(function() {\n var selection = d3_select(this);\n if (!selection.select('.tooltip.in').empty()) {\n selection.call(tooltipBehavior.updateContent);\n }\n });\n }\n };\n\n tool.uninstall = function() {\n context.keybinding()\n .off(commands[0].cmd)\n .off(commands[1].cmd);\n\n context.map()\n .on('move.undo_redo', null)\n .on('drawn.undo_redo', null);\n\n context.history()\n .on('change.undo_redo', null);\n\n context\n .on('enter.undo_redo', null);\n };\n\n return tool;\n}\n", "export * from './modes';\nexport * from './notes';\nexport * from './save';\nexport * from './sidebar_toggle';\nexport * from './undo_redo';\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { debounce } from 'es-toolkit/compat';\nimport { uiToolDrawModes, uiToolNotes, uiToolSave, uiToolSidebarToggle, uiToolUndoRedo } from './tools';\n\n\nexport function uiTopToolbar(context) {\n\n var sidebarToggle = uiToolSidebarToggle(context),\n modes = uiToolDrawModes(context),\n notes = uiToolNotes(context),\n undoRedo = uiToolUndoRedo(context),\n save = uiToolSave(context);\n\n function notesEnabled() {\n var noteLayer = context.layers().layer('notes');\n return noteLayer && noteLayer.enabled();\n }\n\n function topToolbar(bar) {\n\n bar.on('wheel.topToolbar', function(d3_event) {\n if (!d3_event.deltaX) {\n // translate vertical scrolling into horizontal scrolling in case\n // the user doesn't have an input device that can scroll horizontally\n bar.node().scrollLeft += d3_event.deltaY;\n }\n });\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n context.layers()\n .on('change.topToolbar', debouncedUpdate);\n\n update();\n\n function update() {\n\n var tools = [\n sidebarToggle,\n 'spacer',\n modes\n ];\n\n tools.push('spacer');\n\n if (notesEnabled()) {\n tools = tools.concat([notes, 'spacer']);\n }\n\n tools = tools.concat([undoRedo, save]);\n\n var toolbarItems = bar.selectAll('.toolbar-item')\n .data(tools, function(d) {\n return d.id || d;\n });\n\n toolbarItems.exit()\n .each(function(d) {\n if (d.uninstall) {\n d.uninstall();\n }\n })\n .remove();\n\n var itemsEnter = toolbarItems\n .enter()\n .append('div')\n .attr('class', function(d) {\n var classes = 'toolbar-item ' + (d.id || d).replaceAll('_', '-');\n if (d.klass) classes += ' ' + d.klass;\n return classes;\n });\n\n var actionableItems = itemsEnter.filter(function(d) { return d !== 'spacer'; });\n\n actionableItems\n .append('div')\n .attr('class', 'item-content')\n .each(function(d) {\n d3_select(this).call(d.render, bar);\n });\n\n actionableItems\n .append('div')\n .attr('class', 'item-label')\n .each(function(d) { d.label(d3_select(this)); });\n }\n\n }\n\n return topToolbar;\n}\n", "import { prefs } from '../core/preferences';\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiTooltip } from './tooltip';\n\n\n// these are module variables so they are preserved through a ui.restart()\nvar sawVersion = null;\nvar isNewVersion = false;\nvar isNewUser = false;\n\n\nexport function uiVersion(context) {\n\n var currVersion = context.version;\n var matchedVersion = currVersion.match(/\\d+\\.\\d+\\.\\d+.*/);\n\n if (sawVersion === null && matchedVersion !== null) {\n if (prefs('sawVersion')) {\n isNewUser = false;\n isNewVersion = prefs('sawVersion') !== currVersion && currVersion.indexOf('-') === -1;\n } else {\n isNewUser = true;\n isNewVersion = true;\n }\n prefs('sawVersion', currVersion);\n sawVersion = currVersion;\n }\n\n return function(selection) {\n selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/OpenHistoricalMap/iD')\n .text(currVersion);\n\n // only show new version indicator to users that have used iD before\n if (isNewVersion && !isNewUser) {\n selection\n .append('a')\n .attr('class', 'badge')\n .attr('target', '_blank')\n .attr('href', `https://github.com/OpenHistoricalMap/iD/releases/tag/v${currVersion}`)\n .call(svgIcon('#maki-gift'))\n .call(uiTooltip()\n .title(() => t.append('version.whats_new', { version: currVersion }))\n .placement('top')\n .scrollContainer(context.container().select('.main-footer-wrap'))\n );\n }\n };\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t, localizer } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\nimport { uiTooltip } from './tooltip';\nimport { utilKeybinding } from '../util/keybinding';\n\n\nexport function uiZoom(context) {\n\n var zooms = [{\n id: 'zoom-in',\n icon: 'iD-icon-plus',\n title: t.append('zoom.in'),\n action: zoomIn,\n disabled: function() {\n return !context.map().canZoomIn();\n },\n disabledTitle: t.append('zoom.disabled.in'),\n key: '+'\n }, {\n id: 'zoom-out',\n icon: 'iD-icon-minus',\n title: t.append('zoom.out'),\n action: zoomOut,\n disabled: function() {\n return !context.map().canZoomOut();\n },\n disabledTitle: t.append('zoom.disabled.out'),\n key: '-'\n }];\n\n function zoomIn(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomIn();\n }\n\n function zoomOut(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomOut();\n }\n\n function zoomInFurther(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomInFurther();\n }\n\n function zoomOutFurther(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomOutFurther();\n }\n\n return function(selection) {\n var tooltipBehavior = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(function(d) {\n if (d.disabled()) {\n return d.disabledTitle;\n }\n return d.title;\n })\n .keys(function(d) {\n return [d.key];\n });\n\n var lastPointerUpType;\n\n var buttons = selection.selectAll('button')\n .data(zooms)\n .enter()\n .append('button')\n .attr('class', function(d) { return d.id; })\n .on('pointerup.editor', function(d3_event) {\n lastPointerUpType = d3_event.pointerType;\n })\n .on('click.editor', function(d3_event, d) {\n if (!d.disabled()) {\n d.action(d3_event);\n } else if (lastPointerUpType === 'touch' || lastPointerUpType === 'pen') {\n context.ui().flash\n .duration(2000)\n .iconName('#' + d.icon)\n .iconClass('disabled')\n .label(d.disabledTitle)();\n }\n lastPointerUpType = null;\n })\n .call(tooltipBehavior);\n\n buttons.each(function(d) {\n d3_select(this)\n .call(svgIcon('#' + d.icon, 'light'));\n });\n\n utilKeybinding.plusKeys.forEach(function(key) {\n context.keybinding().on([key], zoomIn);\n context.keybinding().on([uiCmd('\u2325' + key)], zoomInFurther);\n });\n\n utilKeybinding.minusKeys.forEach(function(key) {\n context.keybinding().on([key], zoomOut);\n context.keybinding().on([uiCmd('\u2325' + key)], zoomOutFurther);\n });\n\n function updateButtonStates() {\n buttons\n .classed('disabled', function(d) {\n return d.disabled();\n })\n .each(function() {\n var selection = d3_select(this);\n if (!selection.select('.tooltip.in').empty()) {\n selection.call(tooltipBehavior.updateContent);\n }\n });\n }\n\n updateButtonStates();\n\n context.map().on('move.uiZoom', updateButtonStates);\n };\n}\n", "import { t, localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\nimport { svgIcon } from '../svg/icon';\n\nexport function uiZoomToSelection(context) {\n\n function isDisabled() {\n var mode = context.mode();\n return !mode || !mode.zoomToSelected;\n }\n\n var _lastPointerUpType;\n\n function pointerup(d3_event) {\n _lastPointerUpType = d3_event.pointerType;\n }\n\n function click(d3_event) {\n d3_event.preventDefault();\n\n if (isDisabled()) {\n if (_lastPointerUpType === 'touch' || _lastPointerUpType === 'pen') {\n context.ui().flash\n .duration(2000)\n .iconName('#iD-icon-framed-dot')\n .iconClass('disabled')\n .label(t.append('inspector.zoom_to.no_selection'))();\n }\n } else {\n var mode = context.mode();\n if (mode && mode.zoomToSelected) {\n mode.zoomToSelected();\n }\n }\n\n _lastPointerUpType = null;\n }\n\n return function(selection) {\n\n var tooltipBehavior = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(function() {\n if (isDisabled()) {\n return t.append('inspector.zoom_to.no_selection');\n }\n return t.append('inspector.zoom_to.title');\n })\n .keys([t('inspector.zoom_to.key')]);\n\n var button = selection\n .append('button')\n .on('pointerup', pointerup)\n .on('click', click)\n .call(svgIcon('#iD-icon-framed-dot', 'light'))\n .call(tooltipBehavior);\n\n function setEnabledState() {\n button.classed('disabled', isDisabled());\n if (!button.select('.tooltip.in').empty()) {\n button.call(tooltipBehavior.updateContent);\n }\n }\n\n context.on('enter.uiZoomToSelection', setEnabledState);\n\n setEnabledState();\n };\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { svgIcon } from '../svg/icon';\nimport { t, localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\n\n\nexport function uiPane(id, context) {\n\n var _key;\n var _label = '';\n var _description = '';\n var _iconName = '';\n var _sections; // array of uiSection objects\n\n var _paneSelection = d3_select(null);\n\n var _paneTooltip;\n\n var pane = {\n id: id\n };\n\n pane.label = function(val) {\n if (!arguments.length) return _label;\n _label = val;\n return pane;\n };\n\n pane.key = function(val) {\n if (!arguments.length) return _key;\n _key = val;\n return pane;\n };\n\n pane.description = function(val) {\n if (!arguments.length) return _description;\n _description = val;\n return pane;\n };\n\n pane.iconName = function(val) {\n if (!arguments.length) return _iconName;\n _iconName = val;\n return pane;\n };\n\n pane.sections = function(val) {\n if (!arguments.length) return _sections;\n _sections = val;\n return pane;\n };\n\n pane.selection = function() {\n return _paneSelection;\n };\n\n function hidePane() {\n context.ui().togglePanes();\n }\n\n pane.togglePane = function(d3_event) {\n if (d3_event) d3_event.preventDefault();\n _paneTooltip.hide();\n context.ui().togglePanes(!_paneSelection.classed('shown') ? _paneSelection : undefined);\n };\n\n pane.renderToggleButton = function(selection) {\n\n if (!_paneTooltip) {\n _paneTooltip = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(() => _description)\n .keys([_key]);\n }\n\n selection\n .append('button')\n .on('click', pane.togglePane)\n .call(svgIcon('#' + _iconName, 'light'))\n .call(_paneTooltip);\n };\n\n pane.renderContent = function(selection) {\n // override to fully customize content\n\n if (_sections) {\n _sections.forEach(function(section) {\n selection.call(section.render);\n });\n }\n };\n\n pane.renderPane = function(selection) {\n\n _paneSelection = selection\n .append('div')\n .attr('class', 'fillL map-pane hide ' + id + '-pane')\n .attr('pane', id);\n\n var heading = _paneSelection\n .append('div')\n .attr('class', 'pane-heading');\n\n heading\n .append('h2')\n .text('')\n .call(_label);\n\n heading\n .append('button')\n .attr('title', t('icons.close'))\n .on('click', hidePane)\n .call(svgIcon('#iD-icon-close'));\n\n\n _paneSelection\n .append('div')\n .attr('class', 'pane-content')\n .call(pane.renderContent);\n\n if (_key) {\n context.keybinding()\n .on(_key, pane.togglePane);\n }\n };\n\n return pane;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\nimport { clamp } from 'es-toolkit/compat';\n\nimport { prefs } from '../../core/preferences';\nimport { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\n\n\nexport function uiSectionBackgroundDisplayOptions(context) {\n\n var section = uiSection('background-display-options', context)\n .label(() => t.append('background.display_options'))\n .disclosureContent(renderDisclosureContent);\n\n var _storedOpacity = prefs('background-opacity');\n var _minVal = 0;\n var _maxVal = 3;\n\n var _sliders = ['brightness', 'contrast', 'saturation', 'sharpness'];\n\n var _options = {\n brightness: (_storedOpacity !== null ? (+_storedOpacity) : 1),\n contrast: 1,\n saturation: 1,\n sharpness: 1\n };\n\n function updateValue(d, val) {\n val = clamp(val, _minVal, _maxVal);\n\n _options[d] = val;\n context.background()[d](val);\n\n if (d === 'brightness') {\n prefs('background-opacity', val);\n }\n\n section.reRender();\n }\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.display-options-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'display-options-container controls-list');\n\n // add slider controls\n var slidersEnter = containerEnter.selectAll('.display-control')\n .data(_sliders)\n .enter()\n .append('label')\n .attr('class', function(d) { return 'display-control display-control-' + d; });\n\n slidersEnter\n .each(function(d) {\n d3_select(this).call(t.append('background.' + d));\n })\n .append('span')\n .attr('class', function(d) { return 'display-option-value display-option-value-' + d; });\n\n var sildersControlEnter = slidersEnter\n .append('div')\n .attr('class', 'control-wrap');\n\n sildersControlEnter\n .append('input')\n .attr('class', function(d) { return 'display-option-input display-option-input-' + d; })\n .attr('type', 'range')\n .attr('min', _minVal)\n .attr('max', _maxVal)\n .attr('step', '0.01')\n .on('input', function(d3_event, d) {\n var val = d3_select(this).property('value');\n if (!val && d3_event && d3_event.target) {\n val = d3_event.target.value;\n }\n updateValue(d, val);\n });\n\n sildersControlEnter\n .append('button')\n .attr('title', function(d) { return `${t('background.reset')} ${t('background.' + d)}`; })\n .attr('class', function(d) { return 'display-option-reset display-option-reset-' + d; })\n .on('click', function(d3_event, d) {\n if (d3_event.button !== 0) return;\n updateValue(d, 1);\n })\n .call(svgIcon('#iD-icon-' + (localizer.textDirection() === 'rtl' ? 'redo' : 'undo')));\n\n // reset all button\n containerEnter\n .append('a')\n .attr('class', 'display-option-resetlink')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('background.reset_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n for (var i = 0; i < _sliders.length; i++) {\n updateValue(_sliders[i], 1);\n }\n });\n\n // update\n container = containerEnter\n .merge(container);\n\n container.selectAll('.display-option-input')\n .property('value', function(d) { return _options[d]; });\n\n container.selectAll('.display-option-value')\n .text(function(d) { return Math.floor(_options[d] * 100) + '%'; });\n\n container.selectAll('.display-option-reset')\n .classed('disabled', function(d) { return _options[d] === 1; });\n\n // first time only, set brightness if needed\n if (containerEnter.size() && _options.brightness !== 1) {\n context.background().brightness(_options.brightness);\n }\n }\n\n return section;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { marked } from 'marked';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiConfirm } from '../confirm';\nimport { utilNoAuto, utilRebind } from '../../util';\n\n\nexport function uiSettingsCustomBackground() {\n var dispatch = d3_dispatch('change');\n\n function render(selection) {\n // keep separate copies of original and current settings\n var _origSettings = {\n template: prefs('background-custom-template')\n };\n var _currSettings = {\n template: prefs('background-custom-template')\n };\n\n var example = 'https://tile.openstreetmap.org/{zoom}/{x}/{y}.png';\n var modal = uiConfirm(selection).okButton();\n\n modal\n .classed('settings-modal settings-custom-background', true);\n\n modal.select('.modal-section.header')\n .append('h3')\n .call(t.append('settings.custom_background.header'));\n\n\n var textSection = modal.select('.modal-section.message-text');\n\n var instructions =\n `${t.html('settings.custom_background.instructions.info')}\\n` +\n '\\n' +\n `#### ${t.html('settings.custom_background.instructions.wms.tokens_label')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.proj')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.wkid')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.dimensions')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.bbox')}\\n` +\n '\\n' +\n `#### ${t.html('settings.custom_background.instructions.tms.tokens_label')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.xyz')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.flipped_y')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.switch')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.quadtile')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.scale_factor')}\\n` +\n '\\n' +\n `#### ${t.html('settings.custom_background.instructions.example')}\\n` +\n `\\`${example}\\``;\n\n textSection\n .append('div')\n .attr('class', 'instructions-template')\n .html(marked(instructions));\n\n textSection\n .append('textarea')\n .attr('class', 'field-template')\n .attr('placeholder', t('settings.custom_background.template.placeholder'))\n .call(utilNoAuto)\n .property('value', _currSettings.template);\n\n\n // insert a cancel button\n var buttonSection = modal.select('.modal-section.buttons');\n\n buttonSection\n .insert('button', '.ok-button')\n .attr('class', 'button cancel-button secondary-action')\n .call(t.append('confirm.cancel'));\n\n\n buttonSection.select('.cancel-button')\n .on('click.cancel', clickCancel);\n\n buttonSection.select('.ok-button')\n .attr('disabled', isSaveDisabled)\n .on('click.save', clickSave);\n\n\n function isSaveDisabled() {\n return null;\n }\n\n\n // restore the original template\n function clickCancel() {\n textSection.select('.field-template').property('value', _origSettings.template);\n prefs('background-custom-template', _origSettings.template);\n this.blur();\n modal.close();\n }\n\n // accept the current template\n function clickSave() {\n _currSettings.template = textSection.select('.field-template').property('value');\n prefs('background-custom-template', _currSettings.template);\n this.blur();\n modal.close();\n dispatch.call('change', this, _currSettings);\n }\n }\n\n return utilRebind(render, dispatch, 'on');\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport { descending as d3_descending, ascending as d3_ascending } from 'd3-array';\nimport { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t, localizer } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCmd } from '../cmd';\nimport { uiSettingsCustomBackground } from '../settings/custom_background';\nimport { uiMapInMap } from '../map_in_map';\nimport { uiSection } from '../section';\n\nexport function uiSectionBackgroundList(context) {\n\n var _backgroundList = d3_select(null);\n\n var _settingsCustomBackground = uiSettingsCustomBackground(context)\n .on('change', customChanged);\n\n var section = uiSection('background-list', context)\n .label(() => t.append('background.backgrounds'))\n .disclosureContent(renderDisclosureContent);\n\n function previousBackgroundID() {\n return prefs('background-last-used-toggle');\n }\n\n function renderDisclosureContent(selection) {\n\n // the background list\n var container = selection.selectAll('.layer-background-list')\n .data([0]);\n\n _backgroundList = container.enter()\n .append('ul')\n .attr('class', 'layer-list layer-background-list')\n .attr('dir', 'auto')\n .merge(container);\n\n\n // add minimap toggle below list\n var bgExtrasListEnter = selection.selectAll('.bg-extras-list')\n .data([0])\n .enter()\n .append('ul')\n .attr('class', 'layer-list bg-extras-list');\n\n var minimapLabelEnter = bgExtrasListEnter\n .append('li')\n .attr('class', 'minimap-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('background.minimap.tooltip'))\n .keys([t('background.minimap.key')])\n .placement('top')\n );\n\n minimapLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n uiMapInMap.toggle();\n });\n\n minimapLabelEnter\n .append('span')\n .call(t.append('background.minimap.description'));\n\n\n var panelLabelEnter = bgExtrasListEnter\n .append('li')\n .attr('class', 'background-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('background.panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.background.key'))])\n .placement('top')\n );\n\n panelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('background');\n });\n\n panelLabelEnter\n .append('span')\n .call(t.append('background.panel.description'));\n\n var locPanelLabelEnter = bgExtrasListEnter\n .append('li')\n .attr('class', 'location-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('background.location_panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.location.key'))])\n .placement('top')\n );\n\n locPanelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('location');\n });\n\n locPanelLabelEnter\n .append('span')\n .call(t.append('background.location_panel.description'));\n\n\n // \"Info / Report a Problem\" link\n selection.selectAll('.imagery-faq')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'imagery-faq')\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', 'https://github.com/openstreetmap/iD/blob/develop/FAQ.md#how-can-i-report-an-issue-with-background-imagery')\n .append('span')\n .call(t.append('background.imagery_problem_faq'));\n\n _backgroundList\n .call(drawListItems, 'radio', function(d3_event, d) {\n chooseBackground(d);\n }, function(d) {\n return !d.isHidden() && !d.overlay;\n });\n }\n\n function setTooltips(selection) {\n selection.each(function(d, i, nodes) {\n var item = d3_select(this).select('label');\n var span = item.select('span');\n var placement = (i < nodes.length / 2) ? 'bottom' : 'top';\n var hasDescription = d.hasDescription();\n var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth'));\n\n item.call(uiTooltip().destroyAny);\n\n if (d.id === previousBackgroundID()) {\n item.call(uiTooltip()\n .placement(placement)\n .title(() => t.append('background.switch'))\n .keys([uiCmd('\u2318' + t('background.key'))])\n );\n } else if (hasDescription || isOverflowing) {\n item.call(uiTooltip()\n .placement(placement)\n .title(() => hasDescription ? d.description() : d.label())\n );\n }\n });\n }\n\n function drawListItems(layerList, type, change, filter) {\n var sources = context.background()\n .sources(context.map().extent(), context.map().zoom(), true)\n .filter(filter)\n .sort(function(a, b) {\n return a.best() && !b.best() ? -1\n : b.best() && !a.best() ? 1\n : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0;\n });\n\n var layerLinks = layerList.selectAll('li')\n // We have to be a bit inefficient about reordering the list since\n // arrow key navigation of radio values likes to work in the order\n // they were added, not the display document order.\n .data(sources, function(d, i) { return d.id + '---' + i; });\n\n layerLinks.exit()\n .remove();\n\n var enter = layerLinks.enter()\n .append('li')\n .classed('layer-custom', function(d) { return d.id === 'custom'; })\n .classed('best', function(d) { return d.best(); });\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', 'background-layer')\n .attr('value', function(d) {\n return d.id;\n })\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) { d.label()(d3_select(this)); });\n\n enter.filter(function(d) { return d.id === 'custom'; })\n .append('button')\n .attr('class', 'layer-browse')\n .call(uiTooltip()\n .title(() => t.append('settings.custom_background.tooltip'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n editCustom();\n })\n .call(svgIcon('#iD-icon-more'));\n\n enter.filter(function(d) { return d.best(); })\n .append('div')\n .attr('class', 'best')\n .call(uiTooltip()\n .title(() => t.append('background.best_imagery'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .append('span')\n .text('\u2605');\n\n layerList\n .call(updateLayerSelections);\n }\n\n function updateLayerSelections(selection) {\n function active(d) {\n return context.background().showsLayer(d);\n }\n\n selection.selectAll('li')\n .classed('active', active)\n .classed('switch', function(d) { return d.id === previousBackgroundID(); })\n .call(setTooltips)\n .selectAll('input')\n .property('checked', active);\n }\n\n\n function chooseBackground(d) {\n if (d.id === 'custom' && !d.template()) {\n return editCustom();\n }\n\n var previousBackground = context.background().baseLayerSource();\n prefs('background-last-used-toggle', previousBackground.id);\n prefs('background-last-used', d.id);\n context.background().baseLayerSource(d);\n }\n\n\n function customChanged(d) {\n var background = context.background();\n var customSource = background.findSource('custom');\n if (!customSource) return;\n\n if (d && d.template) {\n customSource.template(d.template);\n chooseBackground(customSource);\n } else {\n customSource.template('');\n var noneSource = background.findSource('none');\n if (noneSource) {\n chooseBackground(noneSource);\n }\n }\n }\n\n\n function editCustom() {\n context.container()\n .call(_settingsCustomBackground);\n }\n\n\n context.background()\n .on('change.background_list', function() {\n _backgroundList.call(updateLayerSelections);\n });\n\n context.map()\n .on('move.background_list',\n debounce(function() {\n // layers in-view may have changed due to map move\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t, localizer } from '../../core/localizer';\nimport { geoMetersToOffset, geoOffsetToMeters } from '../../geo';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\n\n\nexport function uiSectionBackgroundOffset(context) {\n\n var section = uiSection('background-offset', context)\n .label(() => t.append('background.fix_misalignment'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n var _directions = [\n ['top', [0, -0.5]],\n ['left', [-0.5, 0]],\n ['right', [0.5, 0]],\n ['bottom', [0, 0.5]]\n ];\n\n\n function updateValue() {\n var meters = geoOffsetToMeters(context.background().offset());\n var x = +meters[0].toFixed(2);\n var y = +meters[1].toFixed(2);\n\n context.container().selectAll('.nudge-inner-rect')\n .select('input')\n .classed('error', false)\n .property('value', x + ', ' + y);\n\n context.container().selectAll('.nudge-reset')\n .classed('disabled', function() {\n return (x === 0 && y === 0);\n });\n }\n\n\n function resetOffset() {\n context.background().offset([0, 0]);\n updateValue();\n }\n\n\n function nudge(d) {\n context.background().nudge(d, context.map().zoom());\n updateValue();\n }\n\n\n function inputOffset() {\n var input = d3_select(this);\n var d = input.node().value;\n\n if (d === '') return resetOffset();\n\n d = d.replace(/;/g, ',').split(',').map(function(n) {\n // if n is NaN, it will always get mapped to false.\n return !isNaN(n) && n;\n });\n\n if (d.length !== 2 || !d[0] || !d[1]) {\n input.classed('error', true);\n return;\n }\n\n context.background().offset(geoMetersToOffset(d));\n updateValue();\n }\n\n\n function dragOffset(d3_event) {\n if (d3_event.button !== 0) return;\n\n var origin = [d3_event.clientX, d3_event.clientY];\n\n var pointerId = d3_event.pointerId || 'mouse';\n\n context.container()\n .append('div')\n .attr('class', 'nudge-surface');\n\n d3_select(window)\n .on(_pointerPrefix + 'move.drag-bg-offset', pointermove)\n .on(_pointerPrefix + 'up.drag-bg-offset', pointerup);\n\n if (_pointerPrefix === 'pointer') {\n d3_select(window)\n .on('pointercancel.drag-bg-offset', pointerup);\n }\n\n function pointermove(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n var latest = [d3_event.clientX, d3_event.clientY];\n var d = [\n -(origin[0] - latest[0]) / 4,\n -(origin[1] - latest[1]) / 4\n ];\n\n origin = latest;\n nudge(d);\n }\n\n function pointerup(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n if (d3_event.button !== 0) return;\n\n context.container().selectAll('.nudge-surface')\n .remove();\n\n d3_select(window)\n .on('.drag-bg-offset', null);\n }\n }\n\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.nudge-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'nudge-container');\n\n containerEnter\n .append('div')\n .attr('class', 'nudge-instructions')\n .call(t.append('background.offset'));\n\n var nudgeWrapEnter = containerEnter\n .append('div')\n .attr('class', 'nudge-controls-wrap');\n\n var nudgeEnter = nudgeWrapEnter\n .append('div')\n .attr('class', 'nudge-outer-rect')\n .on(_pointerPrefix + 'down', dragOffset);\n\n nudgeEnter\n .append('div')\n .attr('class', 'nudge-inner-rect')\n .append('input')\n .attr('type', 'text')\n .attr('aria-label', t('background.offset_label'))\n .on('change', inputOffset);\n\n nudgeWrapEnter\n .append('div')\n .selectAll('button')\n .data(_directions).enter()\n .append('button')\n .attr('title', function(d) { return t(`background.nudge.${d[0]}`); })\n .attr('class', function(d) { return d[0] + ' nudge'; })\n .on('click', function(d3_event, d) {\n nudge(d[1]);\n });\n\n nudgeWrapEnter\n .append('button')\n .attr('title', t('background.reset'))\n .attr('class', 'nudge-reset disabled')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n resetOffset();\n })\n .call(svgIcon('#iD-icon-' + (localizer.textDirection() === 'rtl' ? 'redo' : 'undo')));\n\n updateValue();\n }\n\n context.background()\n .on('change.backgroundOffset-update', updateValue);\n\n return section;\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport { descending as d3_descending, ascending as d3_ascending } from 'd3-array';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionOverlayList(context) {\n\n var section = uiSection('overlay-list', context)\n .label(() => t.append('background.overlays'))\n .disclosureContent(renderDisclosureContent);\n\n var _overlayList = d3_select(null);\n\n function setTooltips(selection) {\n selection.each(function(d, i, nodes) {\n var item = d3_select(this).select('label');\n var span = item.select('span');\n var placement = (i < nodes.length / 2) ? 'bottom' : 'top';\n var description = d.description();\n var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth'));\n\n item.call(uiTooltip().destroyAny);\n\n if (description || isOverflowing) {\n item.call(uiTooltip()\n .placement(placement)\n .title(() => description || d.name())\n );\n }\n });\n }\n\n function updateLayerSelections(selection) {\n function active(d) {\n return context.background().showsLayer(d);\n }\n\n selection.selectAll('li')\n .classed('active', active)\n .call(setTooltips)\n .selectAll('input')\n .property('checked', active);\n }\n\n\n function chooseOverlay(d3_event, d) {\n d3_event.preventDefault();\n context.background().toggleOverlayLayer(d);\n _overlayList.call(updateLayerSelections);\n document.activeElement.blur();\n }\n\n function drawListItems(layerList, type, change, filter) {\n var sources = context.background()\n .sources(context.map().extent(), context.map().zoom(), true)\n .filter(filter);\n\n var layerLinks = layerList.selectAll('li')\n .data(sources, function(d) { return d.name(); });\n\n layerLinks.exit()\n .remove();\n\n var enter = layerLinks.enter()\n .append('li');\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', 'layers')\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) { d.label()(d3_select(this)); });\n\n\n layerList.selectAll('li')\n .sort(sortSources);\n\n layerList\n .call(updateLayerSelections);\n\n\n function sortSources(a, b) {\n return a.best() && !b.best() ? -1\n : b.best() && !a.best() ? 1\n : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0;\n }\n }\n\n function renderDisclosureContent(selection) {\n\n var container = selection.selectAll('.layer-overlay-list')\n .data([0]);\n\n _overlayList = container.enter()\n .append('ul')\n .attr('class', 'layer-list layer-overlay-list')\n .attr('dir', 'auto')\n .merge(container);\n\n _overlayList\n .call(drawListItems, 'checkbox', chooseOverlay, function(d) { return !d.isHidden() && d.overlay; });\n }\n\n context.map()\n .on('move.overlay_list',\n debounce(function() {\n // layers in-view may have changed due to map move\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\n\nimport { uiSectionBackgroundDisplayOptions } from '../sections/background_display_options';\nimport { uiSectionBackgroundList } from '../sections/background_list';\nimport { uiSectionBackgroundOffset } from '../sections/background_offset';\nimport { uiSectionOverlayList } from '../sections/overlay_list';\n\nexport function uiPaneBackground(context) {\n\n var backgroundPane = uiPane('background', context)\n .key(t('background.key'))\n .label(t.append('background.title'))\n .description(t.append('background.description'))\n .iconName('iD-icon-layers')\n .sections([\n uiSectionBackgroundList(context),\n uiSectionOverlayList(context),\n uiSectionBackgroundDisplayOptions(context),\n uiSectionBackgroundOffset(context)\n ]);\n\n return backgroundPane;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { marked } from 'marked';\n\nimport { svgIcon } from '../../svg/icon';\nimport { uiIntro } from '../intro/intro';\nimport { uiPane } from '../pane';\n\nimport { t, localizer } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { helpHtml } from '../intro/helper';\n\nexport function uiPaneHelp(context) {\n\n var docKeys = [\n ['help', [\n 'welcome',\n 'open_data_h',\n 'open_data',\n 'before_start_h',\n 'before_start',\n 'open_source_h',\n 'open_source',\n 'open_source_attribution',\n 'open_source_help'\n ]],\n ['overview', [\n 'navigation_h',\n 'navigation_drag',\n 'navigation_zoom',\n 'features_h',\n 'features',\n 'nodes_ways'\n ]],\n ['editing', [\n 'select_h',\n 'select_left_click',\n 'select_right_click',\n 'select_space',\n 'multiselect_h',\n 'multiselect',\n 'multiselect_shift_click',\n 'multiselect_lasso',\n 'undo_redo_h',\n 'undo_redo',\n 'save_h',\n 'save',\n 'save_validation',\n 'upload_h',\n 'upload',\n 'backups_h',\n 'backups',\n 'keyboard_h',\n 'keyboard'\n ]],\n ['feature_editor', [\n 'intro',\n 'definitions',\n 'type_h',\n 'type',\n 'type_picker',\n 'fields_h',\n 'fields_all_fields',\n 'fields_example',\n 'fields_add_field',\n 'tags_h',\n 'tags_all_tags',\n 'tags_resources'\n ]],\n ['points', [\n 'intro',\n 'add_point_h',\n 'add_point',\n 'add_point_finish',\n 'move_point_h',\n 'move_point',\n 'delete_point_h',\n 'delete_point',\n 'delete_point_command'\n ]],\n ['lines', [\n 'intro',\n 'add_line_h',\n 'add_line',\n 'add_line_draw',\n 'add_line_continue',\n 'add_line_finish',\n 'modify_line_h',\n 'modify_line_dragnode',\n 'modify_line_addnode',\n 'connect_line_h',\n 'connect_line',\n 'connect_line_display',\n 'connect_line_drag',\n 'connect_line_tag',\n 'disconnect_line_h',\n 'disconnect_line_command',\n 'move_line_h',\n 'move_line_command',\n 'move_line_connected',\n 'delete_line_h',\n 'delete_line',\n 'delete_line_command'\n ]],\n ['areas', [\n 'intro',\n 'point_or_area_h',\n 'point_or_area',\n 'add_area_h',\n 'add_area_command',\n 'add_area_draw',\n 'add_area_continue',\n 'add_area_finish',\n 'square_area_h',\n 'square_area_command',\n 'modify_area_h',\n 'modify_area_dragnode',\n 'modify_area_addnode',\n 'delete_area_h',\n 'delete_area',\n 'delete_area_command'\n ]],\n ['relations', [\n 'intro',\n 'edit_relation_h',\n 'edit_relation',\n 'edit_relation_add',\n 'edit_relation_delete',\n 'maintain_relation_h',\n 'maintain_relation',\n 'relation_types_h',\n 'multipolygon_h',\n 'multipolygon',\n 'multipolygon_create',\n 'multipolygon_merge',\n 'turn_restriction_h',\n 'turn_restriction',\n 'turn_restriction_field',\n 'turn_restriction_editing',\n 'route_h',\n 'route',\n 'route_add',\n 'boundary_h',\n 'boundary',\n 'boundary_add'\n ]],\n ['operations', [\n 'intro',\n 'intro_2',\n 'straighten',\n 'orthogonalize',\n 'circularize',\n 'move',\n 'rotate',\n 'reflect',\n 'continue',\n 'reverse',\n 'disconnect',\n 'split',\n 'extract',\n 'merge',\n 'delete',\n 'downgrade',\n 'copy_paste'\n ]],\n ['notes', [\n 'intro',\n 'add_note_h',\n 'add_note',\n 'place_note',\n 'move_note',\n 'update_note_h',\n 'update_note',\n 'save_note_h',\n 'save_note'\n ]],\n ['imagery', [\n 'intro',\n 'sources_h',\n 'choosing',\n 'sources',\n 'offsets_h',\n 'offset',\n 'offset_change'\n ]],\n ['streetlevel', [\n 'intro',\n 'using_h',\n 'using',\n 'photos',\n 'viewer'\n ]],\n ['gps', [\n 'intro',\n 'survey',\n 'using_h',\n 'using',\n 'tracing',\n 'upload'\n ]],\n ['qa', [\n 'intro',\n 'tools_h',\n 'tools',\n 'issues_h',\n 'issues'\n ]]\n ];\n\n var headings = {\n 'help.areas.add_area_h': 3,\n 'help.areas.delete_area_h': 3,\n 'help.areas.modify_area_h': 3,\n 'help.areas.point_or_area_h': 3,\n 'help.areas.square_area_h': 3,\n 'help.editing.backups_h': 3,\n 'help.editing.keyboard_h': 3,\n 'help.editing.multiselect_h': 3,\n 'help.editing.save_h': 3,\n 'help.editing.select_h': 3,\n 'help.editing.undo_redo_h': 3,\n 'help.editing.upload_h': 3,\n 'help.feature_editor.fields_h': 3,\n 'help.feature_editor.tags_h': 3,\n 'help.feature_editor.type_h': 3,\n 'help.gps.using_h': 3,\n 'help.help.before_start_h': 3,\n 'help.help.open_data_h': 3,\n 'help.help.open_source_h': 3,\n 'help.imagery.offsets_h': 3,\n 'help.imagery.sources_h': 3,\n 'help.lines.add_line_h': 3,\n 'help.lines.connect_line_h': 3,\n 'help.lines.delete_line_h': 3,\n 'help.lines.disconnect_line_h': 3,\n 'help.lines.modify_line_h': 3,\n 'help.lines.move_line_h': 3,\n 'help.notes.add_note_h': 3,\n 'help.notes.save_note_h': 3,\n 'help.notes.update_note_h': 3,\n 'help.overview.features_h': 3,\n 'help.overview.navigation_h': 3,\n 'help.points.add_point_h': 3,\n 'help.points.delete_point_h': 3,\n 'help.points.move_point_h': 3,\n 'help.qa.issues_h': 3,\n 'help.qa.tools_h': 3,\n 'help.relations.boundary_h': 3,\n 'help.relations.edit_relation_h': 3,\n 'help.relations.maintain_relation_h': 3,\n 'help.relations.multipolygon_h': 3,\n 'help.relations.relation_types_h': 2,\n 'help.relations.route_h': 3,\n 'help.relations.turn_restriction_h': 3,\n 'help.streetlevel.using_h': 3\n };\n\n // For each section, squash all the texts into a single markdown document\n var docs = docKeys.map(function(key) {\n var helpkey = 'help.' + key[0];\n var helpPaneReplacements = { version: context.version };\n var text = key[1].reduce(function(all, part) {\n var subkey = helpkey + '.' + part;\n var depth = headings[subkey]; // is this subkey a heading?\n var hhh = depth ? Array(depth + 1).join('#') + ' ' : ''; // if so, prepend with some ##'s\n return all + hhh + helpHtml(subkey, helpPaneReplacements) + '\\n\\n';\n }, '');\n\n return {\n title: t.addOrUpdate(helpkey + '.title'),\n _title: t(helpkey + '.title'),\n content: marked(text.trim())\n // use keyboard key styling for shortcuts\n .replace(//g, '')\n .replace(/<\\/code>/g, '<\\/kbd>')\n };\n });\n\n var helpPane = uiPane('help', context)\n .key(t('help.key'))\n .label(t.append('help.title'))\n .description(t.append('help.title'))\n .iconName('iD-icon-help');\n\n helpPane.renderContent = function(content) {\n\n function clickHelp(d, i) {\n\n var rtl = (localizer.textDirection() === 'rtl');\n content.property('scrollTop', 0);\n helpPane.selection().select('.pane-heading h2').call(d.title);\n\n body.html(d.content);\n body.selectAll('a')\n .attr('target', '_blank');\n menuItems.classed('selected', function(m) {\n return m._title === d._title;\n });\n\n nav.text('');\n if (rtl) {\n nav.call(drawNext).call(drawPrevious);\n } else {\n nav.call(drawPrevious).call(drawNext);\n }\n\n\n function drawNext(selection) {\n if (i < docs.length - 1) {\n var nextLink = selection\n .append('a')\n .attr('href', '#')\n .attr('class', 'next')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n clickHelp(docs[i + 1], i + 1);\n });\n\n nextLink\n .append('span')\n .call(docs[i + 1].title)\n .call(svgIcon((rtl ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'));\n }\n }\n\n\n function drawPrevious(selection) {\n if (i > 0) {\n var prevLink = selection\n .append('a')\n .attr('href', '#')\n .attr('class', 'previous')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n clickHelp(docs[i - 1], i - 1);\n });\n\n prevLink\n .call(svgIcon((rtl ? '#iD-icon-forward' : '#iD-icon-backward'), 'inline'))\n .append('span')\n .call(docs[i - 1].title);\n }\n }\n }\n\n\n function clickWalkthrough(d3_event) {\n d3_event.preventDefault();\n if (context.inIntro()) return;\n context.container().call(uiIntro(context));\n context.ui().togglePanes();\n }\n\n\n function clickShortcuts(d3_event) {\n d3_event.preventDefault();\n context.container().call(context.ui().shortcuts, true);\n }\n\n var toc = content\n .append('ul')\n .attr('class', 'toc');\n\n var menuItems = toc.selectAll('li')\n .data(docs)\n .enter()\n .append('li')\n .append('a')\n .attr('role', 'button')\n .attr('href', '#')\n .each(function(d) {\n d3_select(this).call(d.title);\n })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n clickHelp(d, docs.indexOf(d));\n });\n\n var shortcuts = toc\n .append('li')\n .attr('class', 'shortcuts')\n .call(uiTooltip()\n .title(() => t.append('shortcuts.tooltip'))\n .keys(['?'])\n .placement('top')\n )\n .append('a')\n .attr('href', '#')\n .on('click', clickShortcuts);\n\n shortcuts\n .append('div')\n .call(t.append('shortcuts.title'));\n\n var walkthrough = toc\n .append('li')\n .attr('class', 'walkthrough')\n .append('a')\n .attr('href', '#')\n .on('click', clickWalkthrough);\n\n walkthrough\n .append('svg')\n .attr('class', 'logo logo-walkthrough')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n walkthrough\n .append('div')\n .call(t.append('splash.walkthrough'));\n\n\n var helpContent = content\n .append('div')\n .attr('class', 'left-content');\n\n var body = helpContent\n .append('div')\n .attr('class', 'body');\n\n var nav = helpContent\n .append('div')\n .attr('class', 'nav');\n\n clickHelp(docs[0], 0);\n };\n\n return helpPane;\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport {\n select as d3_select\n} from 'd3-selection';\n\n//import { actionNoop } from '../actions/noop';\nimport { geoSphericalDistance } from '../../geo';\nimport { svgIcon } from '../../svg/icon';\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { utilHighlightEntities } from '../../util';\nimport { uiSection } from '../section';\nimport { validationIssue } from '../../core/validation';\n\nexport function uiSectionValidationIssues(id, severity, context) {\n\n var _issues = [];\n\n var section = uiSection(id, context)\n .label(function() {\n if (!_issues) return '';\n var issueCountText = _issues.length > 1000 ? '1000+' : String(_issues.length);\n return t.append('inspector.title_count', { title: t.append('issues.' + severity + 's.list_title'), count: issueCountText });\n })\n .disclosureContent(renderDisclosureContent)\n .shouldDisplay(function() {\n return _issues && _issues.length;\n });\n\n function getOptions() {\n return {\n what: prefs('validate-what') || 'edited',\n where: prefs('validate-where') || 'all'\n };\n }\n\n // get and cache the issues to display, unordered\n function reloadIssues() {\n _issues = context.validator().getIssuesBySeverity(getOptions())[severity];\n }\n\n function renderDisclosureContent(selection) {\n\n var center = context.map().center();\n var graph = context.graph();\n\n // sort issues by distance away from the center of the map\n var issues = _issues.map(function withDistance(issue) {\n var extent = issue.extent(graph);\n var dist = extent ? geoSphericalDistance(center, extent.center()) : 0;\n return Object.assign(issue, { dist: dist });\n })\n .sort(function byDistance(a, b) {\n return a.dist - b.dist;\n });\n\n // cut off at 1000\n issues = issues.slice(0, 1000);\n\n //renderIgnoredIssuesReset(_warningsSelection);\n\n selection\n .call(drawIssuesList, issues);\n }\n\n function drawIssuesList(selection, issues) {\n var list = selection.selectAll('.issues-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'layer-list issues-list ' + severity + 's-list')\n .merge(list);\n\n\n var items = list.selectAll('li')\n .data(issues, function(d) { return d.key; });\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', function (d) { return 'issue severity-' + d.severity; });\n\n var labelsEnter = itemsEnter\n .append('button')\n .attr('class', 'issue-label')\n .on('click', function(d3_event, d) {\n context.validator().focusIssue(d);\n })\n .on('mouseover', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, true, context);\n })\n .on('mouseout', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, false, context);\n });\n\n var textEnter = labelsEnter\n .append('span')\n .attr('class', 'issue-text');\n\n textEnter\n .append('span')\n .attr('class', 'issue-icon')\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(validationIssue.ICONS[d.severity]));\n });\n\n textEnter\n .append('span')\n .attr('class', 'issue-message');\n\n // Update\n items = items\n .merge(itemsEnter)\n .order();\n\n items.selectAll('.issue-message')\n .text('')\n .each(function(d) {\n return d.message(context)(d3_select(this));\n });\n }\n\n context.validator().on('validated.uiSectionValidationIssues' + id, function() {\n window.requestIdleCallback(function() {\n reloadIssues();\n section.reRender();\n });\n });\n\n context.map().on('move.uiSectionValidationIssues' + id,\n debounce(function() {\n window.requestIdleCallback(function() {\n if (getOptions().where === 'visible') {\n // must refetch issues if they are viewport-dependent\n reloadIssues();\n }\n // always reload list to re-sort-by-distance\n section.reRender();\n });\n }, 1000)\n );\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiSection } from '../section';\n\nexport function uiSectionValidationOptions(context) {\n\n var section = uiSection('issues-options', context)\n .content(renderContent);\n\n function renderContent(selection) {\n\n var container = selection.selectAll('.issues-options-container')\n .data([0]);\n\n container = container.enter()\n .append('div')\n .attr('class', 'issues-options-container')\n .merge(container);\n\n var data = [\n { key: 'what', values: ['edited', 'all'] },\n { key: 'where', values: ['visible', 'all'] }\n ];\n\n var options = container.selectAll('.issues-option')\n .data(data, function(d) { return d.key; });\n\n var optionsEnter = options.enter()\n .append('div')\n .attr('class', function(d) { return 'issues-option issues-option-' + d.key; });\n\n optionsEnter\n .append('div')\n .attr('class', 'issues-option-title')\n .each(function(d) {\n d3_select(this).call(t.append('issues.options.' + d.key + '.title'));\n });\n\n var valuesEnter = optionsEnter.selectAll('label')\n .data(function(d) {\n return d.values.map(function(val) { return { value: val, key: d.key }; });\n })\n .enter()\n .append('label');\n\n valuesEnter\n .append('input')\n .attr('type', 'radio')\n .attr('name', function(d) { return 'issues-option-' + d.key; })\n .attr('value', function(d) { return d.value; })\n .property('checked', function(d) { return getOptions()[d.key] === d.value; })\n .on('change', function(d3_event, d) { updateOptionValue(d3_event, d.key, d.value); });\n\n valuesEnter\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append('issues.options.' + d.key + '.' + d.value));\n });\n }\n\n function getOptions() {\n return {\n what: prefs('validate-what') || 'edited', // 'all', 'edited'\n where: prefs('validate-where') || 'all' // 'all', 'visible'\n };\n }\n\n function updateOptionValue(d3_event, d, val) {\n if (!val && d3_event && d3_event.target) {\n val = d3_event.target.value;\n }\n\n prefs('validate-' + d, val);\n context.validator().validate();\n }\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { utilGetSetValue, utilNoAuto } from '../../util';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionValidationRules(context) {\n\n var MINSQUARE = 0;\n var MAXSQUARE = 20;\n var DEFAULTSQUARE = 5; // see also unsquare_way.js\n\n var section = uiSection('issues-rules', context)\n .disclosureContent(renderDisclosureContent)\n .label(() => t.append('issues.rules.title'));\n\n var _ruleKeys = context.validator().getRuleKeys()\n .filter(function(key) { return key !== 'maprules'; })\n .sort(function(key1, key2) {\n // alphabetize by localized title\n return t('issues.' + key1 + '.title') < t('issues.' + key2 + '.title') ? -1 : 1;\n });\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.issues-rulelist-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'issues-rulelist-container');\n\n containerEnter\n .append('ul')\n .attr('class', 'layer-list issue-rules-list');\n\n var ruleLinks = containerEnter\n .append('div')\n .attr('class', 'issue-rules-links section-footer');\n\n ruleLinks\n .append('a')\n .attr('class', 'issue-rules-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.disable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.validator().disableRules(_ruleKeys);\n });\n\n ruleLinks\n .append('a')\n .attr('class', 'issue-rules-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.enable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.validator().disableRules([]);\n });\n\n\n // Update\n container = container\n .merge(containerEnter);\n\n container.selectAll('.issue-rules-list')\n .call(drawListItems, _ruleKeys, 'checkbox', 'rule', toggleRule, isRuleEnabled);\n }\n\n function drawListItems(selection, data, type, name, change, active) {\n var items = selection.selectAll('li')\n .data(data);\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li');\n\n if (name === 'rule') {\n enter\n .call(uiTooltip()\n .title(function(d) { return t.append('issues.' + d + '.tip'); })\n .placement('top')\n );\n }\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', name)\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) {\n var params = {};\n if (d === 'unsquare_way') {\n params.val = selection => selection\n .append('span')\n .classed('square-degrees', true);\n }\n d3_select(this).call(t.append('issues.' + d + '.title', params));\n });\n\n // Update\n items = items\n .merge(enter);\n\n items\n .classed('active', active)\n .selectAll('input')\n .property('checked', active)\n .property('indeterminate', false);\n\n\n // user-configurable square threshold\n var degStr = prefs('validate-square-degrees');\n if (degStr === null) {\n degStr = DEFAULTSQUARE.toString();\n }\n\n var span = items.selectAll('.square-degrees');\n var input = span.selectAll('.square-degrees-input')\n .data([0]);\n\n // enter / update\n input.enter()\n .append('input')\n .attr('type', 'number')\n .attr('min', MINSQUARE.toString())\n .attr('max', MAXSQUARE.toString())\n .attr('step', '0.5')\n .attr('class', 'square-degrees-input')\n .call(utilNoAuto)\n .on('click', function (d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n this.select();\n })\n .on('keyup', function (d3_event) {\n if (d3_event.keyCode === 13) { // \u21A9 Return\n this.blur();\n this.select();\n }\n })\n .on('blur', changeSquare)\n .merge(input)\n .property('value', degStr);\n }\n\n function changeSquare() {\n var input = d3_select(this);\n var degStr = utilGetSetValue(input).trim();\n var degNum = Number(degStr);\n\n if (!isFinite(degNum)) {\n degNum = DEFAULTSQUARE;\n } else if (degNum > MAXSQUARE) {\n degNum = MAXSQUARE;\n } else if (degNum < MINSQUARE) {\n degNum = MINSQUARE;\n }\n\n degNum = Math.round(degNum * 10 ) / 10; // round to 1 decimal\n degStr = degNum.toString();\n\n input\n .property('value', degStr);\n\n prefs('validate-square-degrees', degStr);\n context.validator().revalidateUnsquare();\n }\n\n function isRuleEnabled(d) {\n return context.validator().isRuleEnabled(d);\n }\n\n function toggleRule(d3_event, d) {\n context.validator().toggleRule(d);\n }\n\n context.validator().on('validated.uiSectionValidationRules', function() {\n window.requestIdleCallback(section.reRender);\n });\n\n return section;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { svgIcon } from '../../svg/icon';\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiSection } from '../section';\n\nexport function uiSectionValidationStatus(context) {\n\n var section = uiSection('issues-status', context)\n .content(renderContent)\n .shouldDisplay(function() {\n var issues = context.validator().getIssues(getOptions());\n return issues.length === 0;\n });\n\n function getOptions() {\n return {\n what: prefs('validate-what') || 'edited',\n where: prefs('validate-where') || 'all'\n };\n }\n\n function renderContent(selection) {\n\n var box = selection.selectAll('.box')\n .data([0]);\n\n var boxEnter = box.enter()\n .append('div')\n .attr('class', 'box');\n\n boxEnter\n .append('div')\n .call(svgIcon('#iD-icon-apply', 'pre-text'));\n\n var noIssuesMessage = boxEnter\n .append('span');\n\n noIssuesMessage\n .append('strong')\n .attr('class', 'message');\n\n noIssuesMessage\n .append('br');\n\n noIssuesMessage\n .append('span')\n .attr('class', 'details');\n\n renderIgnoredIssuesReset(selection);\n setNoIssuesText(selection);\n }\n\n function renderIgnoredIssuesReset(selection) {\n\n var ignoredIssues = context.validator()\n .getIssues({ what: 'all', where: 'all', includeDisabledRules: true, includeIgnored: 'only' });\n\n var resetIgnored = selection.selectAll('.reset-ignored')\n .data(ignoredIssues.length ? [0] : []);\n\n // exit\n resetIgnored.exit()\n .remove();\n\n // enter\n var resetIgnoredEnter = resetIgnored.enter()\n .append('div')\n .attr('class', 'reset-ignored section-footer');\n\n resetIgnoredEnter\n .append('a')\n .attr('href', '#');\n\n // update\n resetIgnored = resetIgnored\n .merge(resetIgnoredEnter);\n\n resetIgnored.select('a')\n .call(t.addOrUpdate('inspector.title_count', { title: t.append('issues.reset_ignored'), count: ignoredIssues.length }));\n\n resetIgnored.on('click', function(d3_event) {\n d3_event.preventDefault();\n context.validator().resetIgnoredIssues();\n });\n }\n\n function setNoIssuesText(selection) {\n\n var opts = getOptions();\n\n function checkForHiddenIssues(cases) {\n for (var type in cases) {\n var hiddenOpts = cases[type];\n var hiddenIssues = context.validator().getIssues(hiddenOpts);\n if (hiddenIssues.length) {\n selection.select('.box .details')\n .html('')\n .call(t.append(\n 'issues.no_issues.hidden_issues.' + type,\n { count: hiddenIssues.length.toString() }\n ));\n return;\n }\n }\n selection.select('.box .details')\n .html('')\n .call(t.append('issues.no_issues.hidden_issues.none'));\n }\n\n var messageType;\n\n if (opts.what === 'edited' && opts.where === 'visible') {\n\n messageType = 'edits_in_view';\n\n checkForHiddenIssues({\n elsewhere: { what: 'edited', where: 'all' },\n everything_else: { what: 'all', where: 'visible' },\n disabled_rules: { what: 'edited', where: 'visible', includeDisabledRules: 'only' },\n everything_else_elsewhere: { what: 'all', where: 'all' },\n disabled_rules_elsewhere: { what: 'edited', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'edited', where: 'visible', includeIgnored: 'only' },\n ignored_issues_elsewhere: { what: 'edited', where: 'all', includeIgnored: 'only' }\n });\n\n } else if (opts.what === 'edited' && opts.where === 'all') {\n\n messageType = 'edits';\n\n checkForHiddenIssues({\n everything_else: { what: 'all', where: 'all' },\n disabled_rules: { what: 'edited', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'edited', where: 'all', includeIgnored: 'only' }\n });\n\n } else if (opts.what === 'all' && opts.where === 'visible') {\n\n messageType = 'everything_in_view';\n\n checkForHiddenIssues({\n elsewhere: { what: 'all', where: 'all' },\n disabled_rules: { what: 'all', where: 'visible', includeDisabledRules: 'only' },\n disabled_rules_elsewhere: { what: 'all', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'all', where: 'visible', includeIgnored: 'only' },\n ignored_issues_elsewhere: { what: 'all', where: 'all', includeIgnored: 'only' }\n });\n } else if (opts.what === 'all' && opts.where === 'all') {\n\n messageType = 'everything';\n\n checkForHiddenIssues({\n disabled_rules: { what: 'all', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'all', where: 'all', includeIgnored: 'only' }\n });\n }\n\n if (opts.what === 'edited' && context.history().difference().summary().length === 0) {\n messageType = 'no_edits';\n }\n\n selection.select('.box .message')\n .html('')\n .call(t.append('issues.no_issues.message.' + messageType));\n\n }\n\n context.validator().on('validated.uiSectionValidationStatus', function() {\n window.requestIdleCallback(section.reRender);\n });\n\n context.map().on('move.uiSectionValidationStatus',\n debounce(function() {\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\n\nimport { uiSectionValidationIssues } from '../sections/validation_issues';\nimport { uiSectionValidationOptions } from '../sections/validation_options';\nimport { uiSectionValidationRules } from '../sections/validation_rules';\nimport { uiSectionValidationStatus } from '../sections/validation_status';\n\nexport function uiPaneIssues(context) {\n\n var issuesPane = uiPane('issues', context)\n .key(t('issues.key'))\n .label(t.append('issues.title'))\n .description(t.append('issues.title'))\n .iconName('iD-icon-alert')\n .sections([\n uiSectionValidationOptions(context),\n uiSectionValidationStatus(context),\n uiSectionValidationIssues('issues-errors', 'error', context),\n uiSectionValidationIssues('issues-warnings', 'warning', context),\n uiSectionValidationIssues('issues-suggestions', 'suggestion', context),\n uiSectionValidationRules(context)\n ]);\n\n return issuesPane;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiConfirm } from '../confirm';\nimport { utilNoAuto, utilRebind } from '../../util';\n\n\nexport function uiSettingsCustomData(context) {\n var dispatch = d3_dispatch('change');\n\n function render(selection) {\n var dataLayer = context.layers().layer('data');\n\n // keep separate copies of original and current settings\n var _origSettings = {\n fileList: (dataLayer && dataLayer.fileList()) || null,\n url: prefs('settings-custom-data-url')\n };\n var _currSettings = {\n fileList: (dataLayer && dataLayer.fileList()) || null,\n // url: prefs('settings-custom-data-url')\n };\n\n // var example = 'https://tile.openstreetmap.org/{zoom}/{x}/{y}.png';\n var modal = uiConfirm(selection).okButton();\n\n modal\n .classed('settings-modal settings-custom-data', true);\n\n modal.select('.modal-section.header')\n .append('h3')\n .call(t.append('settings.custom_data.header'));\n\n\n var textSection = modal.select('.modal-section.message-text');\n\n textSection\n .append('pre')\n .attr('class', 'instructions-file')\n .call(t.append('settings.custom_data.file.instructions'));\n\n textSection\n .append('input')\n .attr('class', 'field-file')\n .attr('type', 'file')\n .attr('accept', '.gpx,.kml,.geojson,.json,application/gpx+xml,application/vnd.google-earth.kml+xml,application/geo+json,application/json')\n .property('files', _currSettings.fileList)\n .on('change', function(d3_event) {\n var files = d3_event.target.files;\n if (files && files.length) {\n _currSettings.url = '';\n textSection.select('.field-url').property('value', '');\n _currSettings.fileList = files;\n } else {\n _currSettings.fileList = null;\n }\n });\n\n textSection\n .append('h4')\n .call(t.append('settings.custom_data.or'));\n\n textSection\n .append('pre')\n .attr('class', 'instructions-url')\n .call(t.append('settings.custom_data.url.instructions'));\n\n textSection\n .append('textarea')\n .attr('class', 'field-url')\n .attr('placeholder', t('settings.custom_data.url.placeholder'))\n .call(utilNoAuto)\n .property('value', _currSettings.url);\n\n\n // insert a cancel button\n var buttonSection = modal.select('.modal-section.buttons');\n\n buttonSection\n .insert('button', '.ok-button')\n .attr('class', 'button cancel-button secondary-action')\n .call(t.append('confirm.cancel'));\n\n\n buttonSection.select('.cancel-button')\n .on('click.cancel', clickCancel);\n\n buttonSection.select('.ok-button')\n .attr('disabled', isSaveDisabled)\n .on('click.save', clickSave);\n\n\n function isSaveDisabled() {\n return null;\n }\n\n\n // restore the original url\n function clickCancel() {\n textSection.select('.field-url').property('value', _origSettings.url);\n prefs('settings-custom-data-url', _origSettings.url);\n this.blur();\n modal.close();\n }\n\n // accept the current url\n function clickSave() {\n _currSettings.url = textSection.select('.field-url').property('value').trim();\n\n // one or the other but not both\n if (_currSettings.url) { _currSettings.fileList = null; }\n if (_currSettings.fileList) { _currSettings.url = ''; }\n\n prefs('settings-custom-data-url', _currSettings.url);\n this.blur();\n modal.close();\n dispatch.call('change', this, _currSettings);\n }\n }\n\n return utilRebind(render, dispatch, 'on');\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t, localizer } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg/icon';\nimport { geoExtent } from '../../geo';\nimport { modeBrowse } from '../../modes/browse';\nimport { uiCmd } from '../cmd';\nimport { uiSection } from '../section';\nimport { uiSettingsCustomData } from '../settings/custom_data';\n\nexport function uiSectionDataLayers(context) {\n\n var settingsCustomData = uiSettingsCustomData(context)\n .on('change', customChanged);\n\n // refers to `modules/svg/layers.js` -> function drawLayers(selection) {...}\n var layers = context.layers();\n\n var section = uiSection('data-layers', context)\n .label(() => t.append('map_data.data_layers'))\n .disclosureContent(renderDisclosureContent);\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.data-layer-container')\n .data([0]);\n\n container.enter()\n .append('div')\n .attr('class', 'data-layer-container')\n .merge(container)\n .call(drawOsmItems)\n .call(drawQAItems)\n .call(drawCustomDataItems)\n .call(drawVectorItems) // Beta - Detroit mapping challenge\n .call(drawPanelItems);\n }\n\n function showsLayer(which) {\n var layer = layers.layer(which);\n if (layer) {\n return layer.enabled();\n }\n return false;\n }\n\n function setLayer(which, enabled) {\n // Don't allow layer changes while drawing - #6584\n var mode = context.mode();\n if (mode && /^draw/.test(mode.id)) return;\n\n var layer = layers.layer(which);\n if (layer) {\n layer.enabled(enabled);\n\n if (!enabled && (which === 'osm' || which === 'notes')) {\n context.enter(modeBrowse(context));\n }\n }\n }\n\n function toggleLayer(which) {\n setLayer(which, !showsLayer(which));\n }\n\n function drawOsmItems(selection) {\n var osmKeys = ['osm', 'notes'];\n var osmLayers = layers.all().filter(function(obj) { return osmKeys.indexOf(obj.id) !== -1; });\n\n var ul = selection\n .selectAll('.layer-list-osm')\n .data([0]);\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-osm')\n .merge(ul);\n\n var li = ul.selectAll('.list-item')\n .data(osmLayers);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) { return 'list-item list-item-' + d.id; });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n if (d.id === 'osm') {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.' + d.id + '.tooltip'))\n .keys([uiCmd('\u2325' + t('area_fill.wireframe.key'))])\n .placement('bottom')\n );\n } else {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.' + d.id + '.tooltip'))\n .placement('bottom')\n );\n }\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) { toggleLayer(d.id); });\n\n labelEnter\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append('map_data.layers.' + d.id + '.title'));\n });\n\n\n // Update\n li\n .merge(liEnter)\n .classed('active', function (d) { return d.layer.enabled(); })\n .selectAll('input')\n .property('checked', function (d) { return d.layer.enabled(); });\n }\n\n function drawQAItems(selection) {\n var qaKeys = ['osmose'];\n var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; });\n\n var ul = selection\n .selectAll('.layer-list-qa')\n .data([0]);\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-qa')\n .merge(ul);\n\n var li = ul.selectAll('.list-item')\n .data(qaLayers);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) { return 'list-item list-item-' + d.id; });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.' + d.id + '.tooltip'))\n .placement('bottom')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) { toggleLayer(d.id); });\n\n labelEnter\n .append('span')\n .each(function(d) { t.append('map_data.layers.' + d.id + '.title')(d3_select(this)); });\n\n\n // Update\n li\n .merge(liEnter)\n .classed('active', function (d) { return d.layer.enabled(); })\n .selectAll('input')\n .property('checked', function (d) { return d.layer.enabled(); });\n }\n\n // Beta feature - sample vector layers to support Detroit Mapping Challenge\n // https://github.com/osmus/detroit-mapping-challenge\n function drawVectorItems(selection) {\n var dataLayer = layers.layer('data');\n var vtData = [\n {\n name: 'Detroit Neighborhoods/Parks',\n src: 'neighborhoods-parks',\n tooltip: 'Neighborhood boundaries and parks as compiled by City of Detroit in concert with community groups.',\n template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmur6x34562qp9iv1u3ksf-54hev,jonahadkins.cjksmqxdx33jj2wp90xd9x2md-4e5y2/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'\n }, {\n name: 'Detroit Composite POIs',\n src: 'composite-poi',\n tooltip: 'Fire Inspections, Business Licenses, and other public location data collated from the City of Detroit.',\n template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmm6a02sli31myxhsr7zf3-2sw8h/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'\n }, {\n name: 'Detroit All-The-Places POIs',\n src: 'alltheplaces-poi',\n tooltip: 'Public domain business location data created by web scrapers.',\n template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmswgk340g2vo06p1w9w0j-8fjjc/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'\n }\n ];\n\n // Only show this if the map is around Detroit..\n var detroit = geoExtent([-83.5, 42.1], [-82.8, 42.5]);\n var showVectorItems = (context.map().zoom() > 9 && detroit.contains(context.map().center()));\n\n var container = selection.selectAll('.vectortile-container')\n .data(showVectorItems ? [0] : []);\n\n container.exit()\n .remove();\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'vectortile-container');\n\n containerEnter\n .append('h4')\n .attr('class', 'vectortile-header')\n .text('Detroit Vector Tiles (Beta)');\n\n containerEnter\n .append('ul')\n .attr('class', 'layer-list layer-list-vectortile');\n\n containerEnter\n .append('div')\n .attr('class', 'vectortile-footer')\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', 'https://github.com/osmus/detroit-mapping-challenge')\n .append('span')\n .text('About these layers');\n\n container = container\n .merge(containerEnter);\n\n\n var ul = container.selectAll('.layer-list-vectortile');\n\n var li = ul.selectAll('.list-item')\n .data(vtData);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) { return 'list-item list-item-' + d.src; });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n d3_select(this).call(\n uiTooltip().title(d.tooltip).placement('top')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'radio')\n .attr('name', 'vectortile')\n .on('change', selectVTLayer);\n\n labelEnter\n .append('span')\n .text(function(d) { return d.name; });\n\n // Update\n li\n .merge(liEnter)\n .classed('active', isVTLayerSelected)\n .selectAll('input')\n .property('checked', isVTLayerSelected);\n\n\n function isVTLayerSelected(d) {\n return dataLayer && dataLayer.template() === d.template;\n }\n\n function selectVTLayer(d3_event, d) {\n prefs('settings-custom-data-url', d.template);\n if (dataLayer) {\n dataLayer.template(d.template, d.src);\n dataLayer.enabled(true);\n }\n }\n }\n\n function drawCustomDataItems(selection) {\n var dataLayer = layers.layer('data');\n var hasData = dataLayer && dataLayer.hasData();\n var showsData = hasData && dataLayer.enabled();\n\n var ul = selection\n .selectAll('.layer-list-data')\n .data(dataLayer ? [0] : []);\n\n // Exit\n ul.exit()\n .remove();\n\n // Enter\n var ulEnter = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-data');\n\n var liEnter = ulEnter\n .append('li')\n .attr('class', 'list-item-data');\n\n var labelEnter = liEnter\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.custom.tooltip'))\n .placement('top')\n );\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function() { toggleLayer('data'); });\n\n labelEnter\n .append('span')\n .call(t.append('map_data.layers.custom.title'));\n\n liEnter\n .append('button')\n .attr('class', 'open-data-options')\n .call(uiTooltip()\n .title(() => t.append('settings.custom_data.tooltip'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n editCustom();\n })\n .call(svgIcon('#iD-icon-more'));\n\n liEnter\n .append('button')\n .attr('class', 'zoom-to-data')\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.custom.zoom'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n if (d3_select(this).classed('disabled')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n dataLayer.fitZoom();\n })\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'));\n\n // Update\n ul = ul\n .merge(ulEnter);\n\n ul.selectAll('.list-item-data')\n .classed('active', showsData)\n .selectAll('label')\n .classed('deemphasize', !hasData)\n .selectAll('input')\n .property('disabled', !hasData)\n .property('checked', showsData);\n\n ul.selectAll('button.zoom-to-data')\n .classed('disabled', !hasData);\n }\n\n function editCustom() {\n context.container()\n .call(settingsCustomData);\n }\n\n function customChanged(d) {\n var dataLayer = layers.layer('data');\n\n if (d && d.url) {\n dataLayer.url(d.url);\n } else if (d && d.fileList) {\n dataLayer.fileList(d.fileList);\n }\n }\n\n function drawPanelItems(selection) {\n\n var panelsListEnter = selection.selectAll('.md-extras-list')\n .data([0])\n .enter()\n .append('ul')\n .attr('class', 'layer-list md-extras-list');\n\n var historyPanelLabelEnter = panelsListEnter\n .append('li')\n .attr('class', 'history-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('map_data.history_panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.history.key'))])\n .placement('top')\n );\n\n historyPanelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('history');\n });\n\n historyPanelLabelEnter\n .append('span')\n .call(t.append('map_data.history_panel.title'));\n\n var measurementPanelLabelEnter = panelsListEnter\n .append('li')\n .attr('class', 'measurement-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('map_data.measurement_panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.measurement.key'))])\n .placement('top')\n );\n\n measurementPanelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('measurement');\n });\n\n measurementPanelLabelEnter\n .append('span')\n .call(t.append('map_data.measurement_panel.title'));\n }\n\n context.layers().on('change.uiSectionDataLayers', section.reRender);\n\n context.map()\n .on('move.uiSectionDataLayers',\n debounce(function() {\n // Detroit layers may have moved in or out of view\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionMapFeatures(context) {\n\n var _features = context.features().keys();\n\n var section = uiSection('map-features', context)\n .label(() => t.append('map_data.map_features'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n function renderDisclosureContent(selection) {\n\n var container = selection.selectAll('.layer-feature-list-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'layer-feature-list-container');\n\n containerEnter\n .append('ul')\n .attr('class', 'layer-list layer-feature-list');\n\n var footer = containerEnter\n .append('div')\n .attr('class', 'feature-list-links section-footer');\n\n footer\n .append('a')\n .attr('class', 'feature-list-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.disable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.features().disableAll();\n });\n\n footer\n .append('a')\n .attr('class', 'feature-list-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.enable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.features().enableAll();\n });\n\n // Update\n container = container\n .merge(containerEnter);\n\n container.selectAll('.layer-feature-list')\n .call(drawListItems, _features, 'checkbox', 'feature', clickFeature, showsFeature);\n }\n\n function drawListItems(selection, data, type, name, change, active) {\n var items = selection.selectAll('li')\n .data(data);\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li')\n .call(uiTooltip()\n .title(function(d) {\n var tip = t.append(name + '.' + d + '.tooltip');\n if (autoHiddenFeature(d)) {\n var msg = showsLayer('osm') ? t.append('map_data.autohidden') : t.append('map_data.osmhidden');\n return selection => {\n selection.call(tip);\n selection.append('div').call(msg);\n };\n }\n return tip;\n })\n .placement('top')\n );\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', name)\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append(name + '.' + d + '.description'));\n });\n\n // Update\n items = items\n .merge(enter);\n\n items\n .classed('active', active)\n .selectAll('input')\n .property('checked', active)\n .property('indeterminate', autoHiddenFeature);\n }\n\n function autoHiddenFeature(d) {\n return context.features().autoHidden(d);\n }\n\n function showsFeature(d) {\n return context.features().enabled(d);\n }\n\n function clickFeature(d3_event, d) {\n context.features().toggle(d);\n }\n\n function showsLayer(id) {\n var layer = context.layers().layer(id);\n return layer && layer.enabled();\n }\n\n // add listeners\n context.features()\n .on('change.map_features', section.reRender);\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionMapStyleOptions(context) {\n\n var section = uiSection('fill-area', context)\n .label(() => t.append('map_data.style_options'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.layer-fill-list')\n .data([0]);\n\n container.enter()\n .append('ul')\n .attr('class', 'layer-list layer-fill-list')\n .merge(container)\n .call(drawListItems, context.map().areaFillOptions, 'radio', 'area_fill', setFill, isActiveFill);\n\n var container2 = selection.selectAll('.layer-visual-diff-list')\n .data([0]);\n\n container2.enter()\n .append('ul')\n .attr('class', 'layer-list layer-visual-diff-list')\n .merge(container2)\n .call(drawListItems, ['highlight_edits'], 'checkbox', 'visual_diff', toggleHighlightEdited, function() {\n return context.surface().classed('highlight-edited');\n });\n }\n\n function drawListItems(selection, data, type, name, change, active) {\n var items = selection.selectAll('li')\n .data(data);\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li')\n .call(uiTooltip()\n .title(function(d) {\n return t.append(name + '.' + d + '.tooltip');\n })\n .keys(function(d) {\n var key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null);\n if (d === 'highlight_edits') key = t('map_data.highlight_edits.key');\n return key ? [key] : null;\n })\n .placement('top')\n );\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', name)\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append(name + '.' + d + '.description'));\n });\n\n // Update\n items = items\n .merge(enter);\n\n items\n .classed('active', active)\n .selectAll('input')\n .property('checked', active)\n .property('indeterminate', false);\n }\n\n function isActiveFill(d) {\n return context.map().activeAreaFill() === d;\n }\n\n function toggleHighlightEdited(d3_event) {\n d3_event.preventDefault();\n context.map().toggleHighlightEdited();\n }\n\n function setFill(d3_event, d) {\n context.map().activeAreaFill(d);\n }\n\n context.map()\n .on('changeHighlighting.ui_style, changeAreaFill.ui_style', section.reRender);\n\n return section;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { isArray, isNumber } from 'es-toolkit/compat';\n\nimport { t } from '../../core/localizer';\nimport { uiConfirm } from '../confirm';\nimport { utilRebind } from '../../util';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg';\n\n\nexport function uiSettingsLocalPhotos(context) {\n var dispatch = d3_dispatch('change');\n var photoLayer = context.layers().layer('local-photos');\n var modal;\n\n function render(selection) {\n\n modal = uiConfirm(selection).okButton();\n\n modal\n .classed('settings-modal settings-local-photos', true);\n\n modal.select('.modal-section.header')\n .append('h3')\n .call(t.append('local_photos.header'));\n\n modal.select('.modal-section.message-text')\n .append('div')\n .classed('local-photos', true);\n\n var instructionsSection = modal.select('.modal-section.message-text .local-photos')\n .append('div')\n .classed('instructions', true);\n\n instructionsSection\n .append('p')\n .classed('instructions-local-photos', true)\n .call(t.append('local_photos.file.instructions'));\n\n instructionsSection\n .append('input')\n .classed('field-file', true)\n .attr('type', 'file')\n .attr('multiple', 'multiple')\n .attr('accept', '.jpg,.jpeg,.png,image/png,image/jpeg')\n .style('visibility', 'hidden')\n .attr('id', 'local-photo-files')\n .on('change', function(d3_event) {\n var files = d3_event.target.files;\n if (files && files.length) {\n photoList\n .select('ul')\n .append('li')\n .classed('placeholder', true)\n .append('div');\n dispatch.call('change', this, files);\n }\n d3_event.target.value = null;\n });\n instructionsSection\n .append('label')\n .attr('for', 'local-photo-files')\n .classed('button', true)\n .call(t.append('local_photos.file.label'));\n\n const photoList = modal.select('.modal-section.message-text .local-photos')\n .append('div')\n .append('div')\n .classed('list-local-photos', true);\n\n photoList\n .append('ul');\n\n updatePhotoList(photoList.select('ul'));\n\n context.layers().on('change', () => updatePhotoList(photoList.select('ul')));\n }\n\n function updatePhotoList(container) {\n function locationUnavailable(d) {\n return !(isArray(d.loc) && isNumber(d.loc[0]) && isNumber(d.loc[1]));\n }\n\n container.selectAll('li.placeholder').remove();\n\n let selection = container.selectAll('li')\n .data(photoLayer.getPhotos() ?? [], d => d.id);\n selection.exit()\n .remove();\n\n const selectionEnter = selection.enter()\n .append('li');\n\n selectionEnter\n .append('span')\n .classed('filename', true);\n selectionEnter\n .append('button')\n .classed('form-field-button zoom-to-data', true)\n .attr('title', t('local_photos.zoom_single'))\n .call(svgIcon('#iD-icon-framed-dot'));\n selectionEnter\n .append('button')\n .classed('form-field-button no-geolocation', true)\n .call(svgIcon('#iD-icon-alert'))\n .call(uiTooltip()\n .title(() => t.append('local_photos.no_geolocation.tooltip'))\n .placement('left')\n );\n selectionEnter\n .append('button')\n .classed('form-field-button remove', true)\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'));\n\n selection = selection.merge(selectionEnter);\n\n selection\n .classed('invalid', locationUnavailable);\n selection.select('span.filename')\n .text(d => d.name)\n .attr('title', d => d.name);\n selection.select('span.filename')\n .on('click', (d3_event, d) => {\n photoLayer.openPhoto(d3_event, d, false);\n });\n selection.select('button.zoom-to-data')\n .on('click', (d3_event, d) => {\n photoLayer.openPhoto(d3_event, d, true);\n });\n selection.select('button.remove')\n .on('click', (d3_event, d) => {\n photoLayer.removePhoto(d.id);\n updatePhotoList(container);\n });\n }\n\n return utilRebind(render, dispatch, 'on');\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\n\nimport { localizer, t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\nimport { utilNoAuto } from '../../util';\nimport { uiSettingsLocalPhotos } from '../settings/local_photos';\nimport { svgIcon } from '../../svg';\n\nexport function uiSectionPhotoOverlays(context) {\n\n let _savedLayers = [];\n let _layersHidden = false;\n\n var settingsLocalPhotos = uiSettingsLocalPhotos(context)\n .on('change', localPhotosChanged);\n\n var layers = context.layers();\n\n var section = uiSection('photo-overlays', context)\n .label(() => t.append('photo_overlays.title'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n const photoDates = {};\n const now = +new Date();\n\n /**\n * Calls all draw function\n * @param {*} selection Current HTML selection\n */\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.photo-overlay-container')\n .data([0]);\n\n container.enter()\n .append('div')\n .attr('class', 'photo-overlay-container')\n .merge(container)\n .call(drawPhotoItems)\n .call(drawPhotoTypeItems)\n .call(drawDateSlider)\n .call(drawUsernameFilter)\n .call(drawLocalPhotos);\n }\n\n /**\n * Draws the streetlevels in the right panel\n */\n function drawPhotoItems(selection) {\n var photoKeys = context.photos().overlayLayerIDs();\n var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; });\n var data = photoLayers.filter(function(obj) {\n if (!obj.layer.supported()) return false;\n if (layerEnabled(obj)) return true;\n if (typeof obj.layer.validHere === 'function') {\n return obj.layer.validHere(context.map().extent(), context.map().zoom());\n }\n return true;\n });\n\n function layerSupported(d) {\n return d.layer && d.layer.supported();\n }\n function layerEnabled(d) {\n return layerSupported(d) && (d.layer.enabled() || _savedLayers.includes(d.id));\n }\n function layerRendered(d) {\n return d.layer.rendered?.(context.map().zoom()) ?? true;\n }\n\n var ul = selection\n .selectAll('.layer-list-photos')\n .data([0]);\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-photos')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-photos')\n .data(data, d => d.id);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) {\n var classes = 'list-item-photos list-item-' + d.id;\n if (d.id === 'mapillary-signs' || d.id === 'mapillary-map-features') {\n classes += ' indented';\n }\n return classes;\n });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n var titleID;\n if (d.id === 'mapillary-signs') titleID = 'mapillary.signs.tooltip';\n else if (d.id === 'mapillary') titleID = 'mapillary_images.tooltip';\n else if (d.id === 'kartaview') titleID = 'kartaview_images.tooltip';\n else titleID = d.id.replace(/-/g, '_') + '.tooltip';\n d3_select(this)\n .call(uiTooltip()\n .title(() => {\n if (!layerRendered(d)) {\n return t.append('street_side.minzoom_tooltip');\n } else {\n return t.append(titleID);\n }\n })\n .placement('top')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) { toggleLayer(d.id); });\n\n labelEnter\n .append('span')\n .each(function(d) {\n var id = d.id;\n if (id === 'mapillary-signs') id = 'photo_overlays.traffic_signs';\n d3_select(this).call(t.append(id.replace(/-/g, '_') + '.title'));\n });\n\n // Update\n li\n .merge(liEnter)\n .classed('active', layerEnabled)\n .selectAll('input')\n .property('disabled', d => !layerRendered(d))\n .property('checked', layerEnabled);\n }\n\n /**\n * Draws the photo type filter in the right panel\n */\n function drawPhotoTypeItems(selection) {\n var data = context.photos().allPhotoTypes();\n\n function typeEnabled(d) {\n return context.photos().showsPhotoType(d);\n }\n\n var ul = selection\n .selectAll('.layer-list-photo-types')\n .data([0]);\n\n ul.exit()\n .remove();\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-photo-types')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-photo-types')\n .data(context.photos().shouldFilterByPhotoType() ? data : []);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) {\n return 'list-item-photo-types list-item-' + d;\n });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('photo_overlays.photo_type.' + d + '.tooltip'))\n .placement('top')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) {\n context.photos().togglePhotoType(d, true);\n });\n\n labelEnter\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append('photo_overlays.photo_type.' + d + '.title'));\n });\n\n\n // Update\n li\n .merge(liEnter)\n .classed('active', typeEnabled)\n .selectAll('input')\n .property('checked', typeEnabled);\n }\n\n /**\n * Draws the date slider filter in the right panel\n */\n function drawDateSlider(selection){\n\n var ul = selection\n .selectAll('.layer-list-date-slider')\n .data([0]);\n\n ul.exit()\n .remove();\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-date-slider')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-date-slider')\n .data(context.photos().shouldFilterDateBySlider() ? ['date-slider'] : []);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', 'list-item-date-slider');\n\n var labelEnter = liEnter\n .append('label')\n .each(function() {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('photo_overlays.age_slider_filter.tooltip'))\n .placement('top')\n );\n });\n\n labelEnter\n .append('span')\n .attr('class', 'dateSliderSpan')\n .call(t.append('photo_overlays.age_slider_filter.title'));\n\n let sliderWrap = labelEnter\n .append('div')\n .attr('class','slider-wrap');\n\n sliderWrap\n .append('input')\n .attr('type', 'range')\n .attr('min', 0)\n .attr('max', 1)\n .attr('step', 0.001)\n .attr('list', 'photo-overlay-date-range')\n .attr('value', () => dateSliderValue('from'))\n .classed('list-option-date-slider', true)\n .classed('from-date', true)\n .style('direction', localizer.textDirection() === 'rtl' ? 'ltr' : 'rtl')\n .call(utilNoAuto)\n .on('change', function() {\n let value = d3_select(this).property('value');\n setYearFilter(value, true, 'from');\n });\n selection.select('input.from-date').each(function() { this.value = dateSliderValue('from'); });\n\n sliderWrap.append('div')\n .attr('class', 'date-slider-label');\n\n sliderWrap\n .append('input')\n .attr('type', 'range')\n .attr('min', 0)\n .attr('max', 1)\n .attr('step', 0.001)\n .attr('list', 'photo-overlay-date-range-inverted')\n .attr('value', () => 1 - dateSliderValue('to'))\n .classed('list-option-date-slider', true)\n .classed('to-date', true)\n // OHM variant: https://github.com/OpenHistoricalMap/issues/issues/1030 .style('display', () => dateSliderValue('to') === 0 ? 'none' : null)\n .style('direction', localizer.textDirection())\n .call(utilNoAuto)\n .on('change', function() {\n let value = d3_select(this).property('value');\n setYearFilter(1-value, true, 'to');\n });\n selection.select('input.to-date').each(function() { this.value = 1 - dateSliderValue('to'); });\n\n selection.select('.date-slider-label')\n .call(dateSliderValue('from') === 1\n ? t.addOrUpdate('photo_overlays.age_slider_filter.label_all')\n : t.addOrUpdate('photo_overlays.age_slider_filter.label_date', {\n date: new Date(now - Math.pow(dateSliderValue('from'), 1.45) * 10 * 365.25 * 86400 * 1000).toLocaleDateString(localizer.localeCode()) }));\n\n sliderWrap.append('datalist')\n .attr('class', 'date-slider-values')\n .attr('id', 'photo-overlay-date-range');\n sliderWrap.append('datalist')\n .attr('class', 'date-slider-values')\n .attr('id', 'photo-overlay-date-range-inverted');\n\n const dateTicks = new Set();\n for (const dates of Object.values(photoDates)) {\n dates.forEach(date => {\n dateTicks.add(Math.round(1000 * Math.pow((now - date) / (10 * 365.25 * 86400 * 1000), 1/1.45)) / 1000);\n });\n }\n const ticks = selection.select('datalist#photo-overlay-date-range').selectAll('option')\n .data([...dateTicks].concat([1, 0]));\n ticks.exit()\n .remove();\n ticks.enter()\n .append('option')\n .merge(ticks)\n .attr('value', d => d);\n const ticksInverted = selection.select('datalist#photo-overlay-date-range-inverted').selectAll('option')\n .data([...dateTicks].concat([1, 0]));\n ticksInverted.exit()\n .remove();\n ticksInverted.enter()\n .append('option')\n .merge(ticksInverted)\n .attr('value', d => 1 - d);\n\n\n li\n .merge(liEnter)\n .classed('active', filterEnabled);\n\n function filterEnabled() {\n return !!context.photos().fromDate();\n }\n }\n\n function dateSliderValue(which) {\n const val = which === 'from' ? context.photos().fromDate() : context.photos().toDate();\n if (val) {\n const date = +new Date(val);\n return Math.pow((now - date) / (10 * 365.25 * 86400 * 1000), 1/1.45);\n } else return which === 'from' ? 1 : 0;\n }\n\n /**\n * Util function to set the slider date filter\n * @param {Number} value The slider value\n * @param {Boolean} updateUrl whether the URL should update or not\n * @param {string} which to set either the 'from' or 'to' date\n */\n function setYearFilter(value, updateUrl, which){\n value = +value + (which === 'from' ? 0.001 : -0.001);\n\n if (value < 1 && value > 0) {\n const date = new Date(now - Math.pow(value, 1.45) * 10 * 365.25 * 86400 * 1000)\n .toISOString().substring(0,10);\n context.photos().setDateFilter(`${which}Date`, date, updateUrl);\n } else {\n context.photos().setDateFilter(`${which}Date`, null, updateUrl);\n }\n };\n\n /**\n * Draws the username filter in the right panel\n */\n function drawUsernameFilter(selection) {\n function filterEnabled() {\n return context.photos().usernames();\n }\n var ul = selection\n .selectAll('.layer-list-username-filter')\n .data([0]);\n\n ul.exit()\n .remove();\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-username-filter')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-username-filter')\n .data(context.photos().shouldFilterByUsername() ? ['username-filter'] : []);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', 'list-item-username-filter');\n\n var labelEnter = liEnter\n .append('label')\n .each(function() {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('photo_overlays.username_filter.tooltip'))\n .placement('top')\n );\n });\n\n labelEnter\n .append('span')\n .call(t.append('photo_overlays.username_filter.title'));\n\n labelEnter\n .append('input')\n .attr('type', 'text')\n .attr('class', 'list-item-input')\n .call(utilNoAuto)\n .property('value', usernameValue)\n .on('change', function() {\n var value = d3_select(this).property('value');\n context.photos().setUsernameFilter(value, true);\n d3_select(this).property('value', usernameValue);\n });\n\n li\n .merge(liEnter)\n .classed('active', filterEnabled);\n\n function usernameValue() {\n var usernames = context.photos().usernames();\n if (usernames) return usernames.join('; ');\n return usernames;\n }\n }\n\n /**\n * Toggle on/off the selected layer\n * @param {*} which Id of the selected layer\n */\n function toggleLayer(which) {\n setLayer(which, !showsLayer(which));\n }\n\n /**\n * @param {*} which Id of the selected layer\n * @returns whether the layer is enabled\n */\n function showsLayer(which) {\n var layer = layers.layer(which);\n if (layer) {\n return layer.enabled();\n }\n return false;\n }\n\n /**\n * Set the selected layer\n * @param {string} which Id of the selected layer\n * @param {boolean} enabled\n */\n function setLayer(which, enabled) {\n var layer = layers.layer(which);\n if (layer) {\n layer.enabled(enabled);\n }\n }\n\n function drawLocalPhotos(selection) {\n var photoLayer = layers.layer('local-photos');\n var hasData = photoLayer && photoLayer.hasData();\n var showsData = hasData && photoLayer.enabled();\n\n var ul = selection\n .selectAll('.layer-list-local-photos')\n .data(photoLayer ? [0] : []);\n\n // Exit\n ul.exit()\n .remove();\n\n // Enter\n var ulEnter = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-local-photos');\n\n var localPhotosEnter = ulEnter\n .append('li')\n .attr('class', 'list-item-local-photos');\n\n var localPhotosLabelEnter = localPhotosEnter\n .append('label')\n .call(uiTooltip().title(() => t.append('local_photos.tooltip')));\n\n localPhotosLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function() { toggleLayer('local-photos'); });\n\n localPhotosLabelEnter\n .call(t.append('local_photos.header'));\n\n localPhotosEnter\n .append('button')\n .attr('class', 'open-data-options')\n .call(uiTooltip()\n .title(() => t.append('local_photos.tooltip_edit'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n editLocalPhotos();\n })\n .call(svgIcon('#iD-icon-more'));\n\n localPhotosEnter\n .append('button')\n .attr('class', 'zoom-to-data')\n .call(uiTooltip()\n .title(() => t.append('local_photos.zoom'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n if (d3_select(this).classed('disabled')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n photoLayer.fitZoom();\n })\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'));\n\n // Update\n ul = ul\n .merge(ulEnter);\n\n ul.selectAll('.list-item-local-photos')\n .classed('active', showsData)\n .selectAll('label')\n .classed('deemphasize', !hasData)\n .selectAll('input')\n .property('disabled', !hasData)\n .property('checked', showsData);\n\n ul.selectAll('button.zoom-to-data')\n .classed('disabled', !hasData);\n }\n\n function editLocalPhotos() {\n context.container()\n .call(settingsLocalPhotos);\n }\n\n function localPhotosChanged(d) {\n var localPhotosLayer = layers.layer('local-photos');\n\n localPhotosLayer.fileList(d);\n }\n\n /**\n * Toggles StreetView on/off\n */\n function toggleStreetSide(){\n let layerContainer = d3_select('.photo-overlay-container');\n if (!_layersHidden){\n const streetLayerIDs = context.photos().overlayLayerIDs();\n layers.all().forEach(d => {\n if (streetLayerIDs.includes(d.id)) {\n if (showsLayer(d.id)) _savedLayers.push(d.id);\n setLayer(d.id, false);\n }\n });\n layerContainer.classed('disabled-panel', true);\n } else {\n _savedLayers.forEach(d => {\n setLayer(d, true);\n });\n _savedLayers = [];\n layerContainer.classed('disabled-panel', false);\n }\n _layersHidden = !_layersHidden;\n };\n\n context.layers().on('change.uiSectionPhotoOverlays', section.reRender);\n context.photos().on('change.uiSectionPhotoOverlays', section.reRender);\n context.layers().on('photoDatesChanged.uiSectionPhotoOverlays', function(service, dates) {\n photoDates[service] = dates.map(date => +new Date(date));\n section.reRender();\n });\n context.keybinding().on('\u21E7P', toggleStreetSide);\n\n context.map()\n .on('move.photo_overlays',\n debounce(function() {\n // layers in-view may have changed due to map move\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiSection } from '../section';\nimport { utilQsString, utilStringQs, utilNormalizeDateString } from '../../util';\n\nconst DEFAULT_MIN_DATE = '-4000-01-01';\nconst DEFAULT_MAX_DATE = (new Date()).getFullYear() + '-12-31';\n\nconst INPUT_STYLES = [\n { name: 'width', value: '125px' },\n { name: 'text-align', value: 'center' },\n];\nconst LABEL_STYLES = [\n { name: 'font-weight', value: 'bold' },\n { name: 'display', value: 'inline-block' },\n { name: 'width', value: '75px' },\n];\n\nexport function uiSectionDateRange(context) {\n // despite appearing as a separate panel, Map Features does the real filtering\n // see applyDateRange() in this panel, where the dateRange value is set\n // see modules/renderer/features.js checkDateFilter() which applies the filters\n // see modules/renderer/features.js update() which updates URL params\n\n const section = uiSection('date_ranges', context)\n .label(() => t.append('date_ranges.title'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n function renderDisclosureContent(selection) {\n const container = selection.selectAll('.date_ranges-container').data([0]);\n\n // for some reason this one uiSection keeps adding content every time it's expanded,\n // so there are 2 inputs, then 4, then 6, ...\n const alreadyhasinputs = container.enter().selectAll('input').size();\n if (alreadyhasinputs) return;\n\n // start date label & input\n const mindate_label = container.enter()\n .append('label')\n .call(t.append('date_ranges.start_date.description'))\n .merge(container);\n const mindate_input = container.enter()\n .append('input')\n .attr('type', 'text')\n .attr('value', DEFAULT_MIN_DATE)\n .attr('title', () => t('date_ranges.start_date.tooltip'))\n .attr('placeholder', () => t('date_ranges.start_date.placeholder'))\n .merge(container);\n\n // line break\n container.enter()\n .append('br')\n .merge(container);\n\n // end date label & input\n const maxdate_label = container.enter()\n .append('label')\n .call(t.append('date_ranges.end_date.description'))\n .merge(container);\n const maxdate_input = container.enter() // we will refer to this widget by its name attribute to fetch our date range\n .append('input')\n .attr('type', 'text')\n .attr('value', DEFAULT_MAX_DATE)\n .attr('title', () => t('date_ranges.end_date.tooltip'))\n .attr('placeholder', () => t('date_ranges.end_date.placeholder'))\n .merge(container);\n\n // apply styles\n INPUT_STYLES.forEach(function (style) {\n mindate_input.style(style.name, style.value);\n maxdate_input.style(style.name, style.value);\n });\n LABEL_STYLES.forEach(function (style) {\n mindate_label.style(style.name, style.value);\n maxdate_label.style(style.name, style.value);\n });\n\n // event handler for change event\n // intercept invalid & blank and correct them to our hardcoded in/max\n // then cause a re-filter/redraw\n function applyDateRange() {\n const mindate = mindate_input.property('value');\n const maxdate = maxdate_input.property('value');\n\n context.features().dateRange = [mindate, maxdate];\n context.features().redraw();\n\n updateUrlParam();\n }\n\n function ensureValidInputs() {\n // if utilNormalizeDateString() can make sense of it, so can utilDatesOverlap()\n // replace with cleaned-up value for visual feedback e.g. 5/10/2022 visibly changes to 2022-10-05\n // if not, then reset to the starting value\n const mindate = mindate_input.property('value');\n const maxdate = maxdate_input.property('value');\n\n const mindate_clean = utilNormalizeDateString(mindate);\n const maxdate_clean = utilNormalizeDateString(maxdate);\n mindate_input.property('value', mindate_clean ? mindate_clean.value : DEFAULT_MIN_DATE);\n maxdate_input.property('value', maxdate_clean ? maxdate_clean.value : DEFAULT_MAX_DATE);\n }\n\n function updateUrlParam() {\n if (!window.mocha) {\n const hash = utilStringQs(window.location.hash);\n\n const daterange = context.features().dateRange;\n if (daterange) {\n hash.daterange = daterange.join(',');\n } else {\n delete hash.daterange;\n }\n\n window.location.replace('#' + utilQsString(hash, true));\n }\n }\n\n mindate_input.on('change', function () {\n ensureValidInputs();\n applyDateRange();\n });\n maxdate_input.on('change', function () {\n ensureValidInputs();\n applyDateRange();\n });\n\n // startup\n // load the start/end date from URL params\n // then apply it so we have context().dateRange defined as early as possible\n let startingdaterange = utilStringQs(window.location.hash).daterange;\n if (startingdaterange) {\n startingdaterange = startingdaterange.split(',');\n const isvalid =startingdaterange[0].match(/^\\-?[\\d\\-]+/) && startingdaterange[1].match(/^\\-?[\\d\\-]+/);\n if (isvalid) {\n mindate_input.property('value', startingdaterange[0]);\n maxdate_input.property('value', startingdaterange[1]);\n }\n }\n applyDateRange();\n }\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\n\nimport { uiSectionDataLayers } from '../sections/data_layers';\nimport { uiSectionMapFeatures } from '../sections/map_features';\nimport { uiSectionMapStyleOptions } from '../sections/map_style_options';\nimport { uiSectionPhotoOverlays } from '../sections/photo_overlays';\nimport { uiSectionDateRange } from '../sections/map_daterange';\n\nexport function uiPaneMapData(context) {\n\n var mapDataPane = uiPane('map-data', context)\n .key(t('map_data.key'))\n .label(t.append('map_data.title'))\n .description(t.append('map_data.description'))\n .iconName('iD-icon-data')\n .sections([\n uiSectionDataLayers(context),\n uiSectionDateRange(context),\n uiSectionPhotoOverlays(context),\n uiSectionMapStyleOptions(context),\n uiSectionMapFeatures(context)\n ]);\n\n return mapDataPane;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\nimport { uiSectionPrivacy } from '../sections/privacy';\n\nexport function uiPanePreferences(context) {\n\n let preferencesPane = uiPane('preferences', context)\n .key(t('preferences.key'))\n .label(t.append('preferences.title'))\n .description(t.append('preferences.description'))\n .iconName('fas-user-cog')\n .sections([\n uiSectionPrivacy(context)\n ]);\n\n return preferencesPane;\n}\n", "import { marked } from 'marked';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { prefs } from '../core/preferences';\nimport { t, localizer } from '../core/localizer';\nimport { presetManager } from '../presets';\nimport { behaviorHash } from '../behavior';\nimport { modeBrowse } from '../modes/browse';\nimport { svgDefs, svgIcon } from '../svg';\nimport { utilDetect } from '../util/detect';\nimport { utilGetDimensions } from '../util/dimensions';\n\nimport { uiAccount } from './account';\nimport { uiAttribution } from './attribution';\nimport { uiContributors } from './contributors';\nimport { uiEditMenu } from './edit_menu';\nimport { uiFeatureInfo } from './feature_info';\nimport { uiFlash } from './flash';\nimport { uiFullScreen } from './full_screen';\nimport { uiGeolocate } from './geolocate';\nimport { uiInfo } from './info';\nimport { uiIntro } from './intro';\nimport { uiIssuesInfo } from './issues_info';\nimport { uiLoading } from './loading';\nimport { uiMapInMap } from './map_in_map';\nimport { uiNotice } from './notice';\nimport { uiPhotoviewer } from './photoviewer';\nimport { uiRestore } from './restore';\nimport { uiScale } from './scale';\nimport { uiShortcuts } from './shortcuts';\nimport { uiSidebar } from './sidebar';\nimport { uiSourceSwitch } from './source_switch';\nimport { uiSpinner } from './spinner';\nimport { uiSplash } from './splash';\nimport { uiStatus } from './status';\nimport { uiTooltip } from './tooltip';\nimport { uiTopToolbar } from './top_toolbar';\nimport { uiVersion } from './version';\nimport { uiZoom } from './zoom';\nimport { uiZoomToSelection } from './zoom_to_selection';\nimport { uiCmd } from './cmd';\n\nimport { uiPaneBackground } from './panes/background';\nimport { uiPaneHelp } from './panes/help';\nimport { uiPaneIssues } from './panes/issues';\nimport { uiPaneMapData } from './panes/map_data';\nimport { uiPanePreferences } from './panes/preferences';\n\nexport function uiInit(context) {\n var _initCounter = 0;\n var _needWidth = {};\n\n var _lastPointerType;\n\n var overMap;\n\n function render(container) {\n\n container\n .on('click.ui', function(d3_event) {\n // we're only concerned with the primary mouse button\n if (d3_event.button !== 0) return;\n\n if (!d3_event.composedPath) return;\n\n // some targets have default click events we don't want to override\n var isOkayTarget = d3_event.composedPath().some(function(node) {\n // we only care about element nodes\n return node.nodeType === 1 &&\n // clicking focuses it and/or changes a value\n (node.nodeName === 'INPUT' ||\n // clicking

    '\n        + (escaped ? code : escapeHtmlEntities(code, true))\n        + '
    \\n' as RendererOutput;\n }\n\n return '
    '\n      + (escaped ? code : escapeHtmlEntities(code, true))\n      + '
    \\n' as RendererOutput;\n }\n\n blockquote({ tokens }: Tokens.Blockquote): RendererOutput {\n const body = this.parser.parse(tokens);\n return `
    \\n${body}
    \\n` as RendererOutput;\n }\n\n html({ text }: Tokens.HTML | Tokens.Tag): RendererOutput {\n return text as RendererOutput;\n }\n\n def(token: Tokens.Def): RendererOutput {\n return '' as RendererOutput;\n }\n\n heading({ tokens, depth }: Tokens.Heading): RendererOutput {\n return `${this.parser.parseInline(tokens)}\\n` as RendererOutput;\n }\n\n hr(token: Tokens.Hr): RendererOutput {\n return '
    \\n' as RendererOutput;\n }\n\n list(token: Tokens.List): RendererOutput {\n const ordered = token.ordered;\n const start = token.start;\n\n let body = '';\n for (let j = 0; j < token.items.length; j++) {\n const item = token.items[j];\n body += this.listitem(item);\n }\n\n const type = ordered ? 'ol' : 'ul';\n const startAttr = (ordered && start !== 1) ? (' start=\"' + start + '\"') : '';\n return '<' + type + startAttr + '>\\n' + body + '\\n' as RendererOutput;\n }\n\n listitem(item: Tokens.ListItem): RendererOutput {\n return `
  • ${this.parser.parse(item.tokens)}
  • \\n` as RendererOutput;\n }\n\n checkbox({ checked }: Tokens.Checkbox): RendererOutput {\n return ' ' as RendererOutput;\n }\n\n paragraph({ tokens }: Tokens.Paragraph): RendererOutput {\n return `

    ${this.parser.parseInline(tokens)}

    \\n` as RendererOutput;\n }\n\n table(token: Tokens.Table): RendererOutput {\n let header = '';\n\n // header\n let cell = '';\n for (let j = 0; j < token.header.length; j++) {\n cell += this.tablecell(token.header[j]);\n }\n header += this.tablerow({ text: cell as ParserOutput });\n\n let body = '';\n for (let j = 0; j < token.rows.length; j++) {\n const row = token.rows[j];\n\n cell = '';\n for (let k = 0; k < row.length; k++) {\n cell += this.tablecell(row[k]);\n }\n\n body += this.tablerow({ text: cell as ParserOutput });\n }\n if (body) body = `${body}`;\n\n return '\\n'\n + '\\n'\n + header\n + '\\n'\n + body\n + '
    \\n' as RendererOutput;\n }\n\n tablerow({ text }: Tokens.TableRow): RendererOutput {\n return `\\n${text}\\n` as RendererOutput;\n }\n\n tablecell(token: Tokens.TableCell): RendererOutput {\n const content = this.parser.parseInline(token.tokens);\n const type = token.header ? 'th' : 'td';\n const tag = token.align\n ? `<${type} align=\"${token.align}\">`\n : `<${type}>`;\n return tag + content + `\\n` as RendererOutput;\n }\n\n /**\n * span level renderer\n */\n strong({ tokens }: Tokens.Strong): RendererOutput {\n return `${this.parser.parseInline(tokens)}` as RendererOutput;\n }\n\n em({ tokens }: Tokens.Em): RendererOutput {\n return `${this.parser.parseInline(tokens)}` as RendererOutput;\n }\n\n codespan({ text }: Tokens.Codespan): RendererOutput {\n return `${escapeHtmlEntities(text, true)}` as RendererOutput;\n }\n\n br(token: Tokens.Br): RendererOutput {\n return '
    ' as RendererOutput;\n }\n\n del({ tokens }: Tokens.Del): RendererOutput {\n return `${this.parser.parseInline(tokens)}` as RendererOutput;\n }\n\n link({ href, title, tokens }: Tokens.Link): RendererOutput {\n const text = this.parser.parseInline(tokens) as string;\n const cleanHref = cleanUrl(href);\n if (cleanHref === null) {\n return text as RendererOutput;\n }\n href = cleanHref;\n let out = '
    ';\n return out as RendererOutput;\n }\n\n image({ href, title, text, tokens }: Tokens.Image): RendererOutput {\n if (tokens) {\n text = this.parser.parseInline(tokens, this.parser.textRenderer) as string;\n }\n const cleanHref = cleanUrl(href);\n if (cleanHref === null) {\n return escapeHtmlEntities(text) as RendererOutput;\n }\n href = cleanHref;\n\n let out = `\"${escapeHtmlEntities(text)}\"`;\n {\n // no need for block level renderers\n strong({ text }: Tokens.Strong): RendererOutput {\n return text as RendererOutput;\n }\n\n em({ text }: Tokens.Em): RendererOutput {\n return text as RendererOutput;\n }\n\n codespan({ text }: Tokens.Codespan): RendererOutput {\n return text as RendererOutput;\n }\n\n del({ text }: Tokens.Del): RendererOutput {\n return text as RendererOutput;\n }\n\n html({ text }: Tokens.HTML | Tokens.Tag): RendererOutput {\n return text as RendererOutput;\n }\n\n text({ text }: Tokens.Text | Tokens.Escape | Tokens.Tag): RendererOutput {\n return text as RendererOutput;\n }\n\n link({ text }: Tokens.Link): RendererOutput {\n return '' + text as RendererOutput;\n }\n\n image({ text }: Tokens.Image): RendererOutput {\n return '' + text as RendererOutput;\n }\n\n br(): RendererOutput {\n return '' as RendererOutput;\n }\n\n checkbox({ raw }: Tokens.Checkbox): RendererOutput {\n return raw as RendererOutput;\n }\n}\n", "import { _Renderer } from './Renderer.ts';\nimport { _TextRenderer } from './TextRenderer.ts';\nimport { _defaults } from './defaults.ts';\nimport type { MarkedToken, Token, Tokens } from './Tokens.ts';\nimport type { MarkedOptions } from './MarkedOptions.ts';\n\n/**\n * Parsing & Compiling\n */\nexport class _Parser {\n options: MarkedOptions;\n renderer: _Renderer;\n textRenderer: _TextRenderer;\n constructor(options?: MarkedOptions) {\n this.options = options || _defaults;\n this.options.renderer = this.options.renderer || new _Renderer();\n this.renderer = this.options.renderer;\n this.renderer.options = this.options;\n this.renderer.parser = this;\n this.textRenderer = new _TextRenderer();\n }\n\n /**\n * Static Parse Method\n */\n static parse(tokens: Token[], options?: MarkedOptions) {\n const parser = new _Parser(options);\n return parser.parse(tokens);\n }\n\n /**\n * Static Parse Inline Method\n */\n static parseInline(tokens: Token[], options?: MarkedOptions) {\n const parser = new _Parser(options);\n return parser.parseInline(tokens);\n }\n\n /**\n * Parse Loop\n */\n parse(tokens: Token[]): ParserOutput {\n let out = '';\n\n for (let i = 0; i < tokens.length; i++) {\n const anyToken = tokens[i];\n\n // Run any renderer extensions\n if (this.options.extensions?.renderers?.[anyToken.type]) {\n const genericToken = anyToken as Tokens.Generic;\n const ret = this.options.extensions.renderers[genericToken.type].call({ parser: this }, genericToken);\n if (ret !== false || !['space', 'hr', 'heading', 'code', 'table', 'blockquote', 'list', 'html', 'def', 'paragraph', 'text'].includes(genericToken.type)) {\n out += ret || '';\n continue;\n }\n }\n\n const token = anyToken as MarkedToken;\n\n switch (token.type) {\n case 'space': {\n out += this.renderer.space(token);\n break;\n }\n case 'hr': {\n out += this.renderer.hr(token);\n break;\n }\n case 'heading': {\n out += this.renderer.heading(token);\n break;\n }\n case 'code': {\n out += this.renderer.code(token);\n break;\n }\n case 'table': {\n out += this.renderer.table(token);\n break;\n }\n case 'blockquote': {\n out += this.renderer.blockquote(token);\n break;\n }\n case 'list': {\n out += this.renderer.list(token);\n break;\n }\n case 'checkbox': {\n out += this.renderer.checkbox(token);\n break;\n }\n case 'html': {\n out += this.renderer.html(token);\n break;\n }\n case 'def': {\n out += this.renderer.def(token);\n break;\n }\n case 'paragraph': {\n out += this.renderer.paragraph(token);\n break;\n }\n case 'text': {\n out += this.renderer.text(token);\n break;\n }\n\n default: {\n const errMsg = 'Token with \"' + token.type + '\" type was not found.';\n if (this.options.silent) {\n console.error(errMsg);\n return '' as ParserOutput;\n } else {\n throw new Error(errMsg);\n }\n }\n }\n }\n\n return out as ParserOutput;\n }\n\n /**\n * Parse Inline Tokens\n */\n parseInline(tokens: Token[], renderer: _Renderer | _TextRenderer = this.renderer): ParserOutput {\n let out = '';\n\n for (let i = 0; i < tokens.length; i++) {\n const anyToken = tokens[i];\n\n // Run any renderer extensions\n if (this.options.extensions?.renderers?.[anyToken.type]) {\n const ret = this.options.extensions.renderers[anyToken.type].call({ parser: this }, anyToken);\n if (ret !== false || !['escape', 'html', 'link', 'image', 'strong', 'em', 'codespan', 'br', 'del', 'text'].includes(anyToken.type)) {\n out += ret || '';\n continue;\n }\n }\n\n const token = anyToken as MarkedToken;\n\n switch (token.type) {\n case 'escape': {\n out += renderer.text(token);\n break;\n }\n case 'html': {\n out += renderer.html(token);\n break;\n }\n case 'link': {\n out += renderer.link(token);\n break;\n }\n case 'image': {\n out += renderer.image(token);\n break;\n }\n case 'checkbox': {\n out += renderer.checkbox(token);\n break;\n }\n case 'strong': {\n out += renderer.strong(token);\n break;\n }\n case 'em': {\n out += renderer.em(token);\n break;\n }\n case 'codespan': {\n out += renderer.codespan(token);\n break;\n }\n case 'br': {\n out += renderer.br(token);\n break;\n }\n case 'del': {\n out += renderer.del(token);\n break;\n }\n case 'text': {\n out += renderer.text(token);\n break;\n }\n default: {\n const errMsg = 'Token with \"' + token.type + '\" type was not found.';\n if (this.options.silent) {\n console.error(errMsg);\n return '' as ParserOutput;\n } else {\n throw new Error(errMsg);\n }\n }\n }\n }\n return out as ParserOutput;\n }\n}\n", "import { _defaults } from './defaults.ts';\nimport { _Lexer } from './Lexer.ts';\nimport { _Parser } from './Parser.ts';\nimport type { MarkedOptions } from './MarkedOptions.ts';\nimport type { Token, TokensList } from './Tokens.ts';\n\nexport class _Hooks {\n options: MarkedOptions;\n block?: boolean;\n\n constructor(options?: MarkedOptions) {\n this.options = options || _defaults;\n }\n\n static passThroughHooks = new Set([\n 'preprocess',\n 'postprocess',\n 'processAllTokens',\n 'emStrongMask',\n ]);\n\n static passThroughHooksRespectAsync = new Set([\n 'preprocess',\n 'postprocess',\n 'processAllTokens',\n ]);\n\n /**\n * Process markdown before marked\n */\n preprocess(markdown: string) {\n return markdown;\n }\n\n /**\n * Process HTML after marked is finished\n */\n postprocess(html: ParserOutput) {\n return html;\n }\n\n /**\n * Process all tokens before walk tokens\n */\n processAllTokens(tokens: Token[] | TokensList) {\n return tokens;\n }\n\n /**\n * Mask contents that should not be interpreted as em/strong delimiters\n */\n emStrongMask(src: string) {\n return src;\n }\n\n /**\n * Provide function to tokenize markdown\n */\n provideLexer() {\n return this.block ? _Lexer.lex : _Lexer.lexInline;\n }\n\n /**\n * Provide function to parse tokens\n */\n provideParser() {\n return this.block ? _Parser.parse : _Parser.parseInline;\n }\n}\n", "import { _getDefaults } from './defaults.ts';\nimport { _Lexer } from './Lexer.ts';\nimport { _Parser } from './Parser.ts';\nimport { _Hooks } from './Hooks.ts';\nimport { _Renderer } from './Renderer.ts';\nimport { _Tokenizer } from './Tokenizer.ts';\nimport { _TextRenderer } from './TextRenderer.ts';\nimport { escapeHtmlEntities } from './helpers.ts';\nimport type { MarkedExtension, MarkedOptions } from './MarkedOptions.ts';\nimport type { Token, Tokens, TokensList } from './Tokens.ts';\n\nexport type MaybePromise = void | Promise;\n\ntype UnknownFunction = (...args: unknown[]) => unknown;\ntype GenericRendererFunction = (...args: unknown[]) => string | false;\n\nexport class Marked {\n defaults = _getDefaults();\n options = this.setOptions;\n\n parse = this.parseMarkdown(true);\n parseInline = this.parseMarkdown(false);\n\n Parser = _Parser;\n Renderer = _Renderer;\n TextRenderer = _TextRenderer;\n Lexer = _Lexer;\n Tokenizer = _Tokenizer;\n Hooks = _Hooks;\n\n constructor(...args: MarkedExtension[]) {\n this.use(...args);\n }\n\n /**\n * Run callback for every token\n */\n walkTokens(tokens: Token[] | TokensList, callback: (token: Token) => MaybePromise | MaybePromise[]) {\n let values: MaybePromise[] = [];\n for (const token of tokens) {\n values = values.concat(callback.call(this, token));\n switch (token.type) {\n case 'table': {\n const tableToken = token as Tokens.Table;\n for (const cell of tableToken.header) {\n values = values.concat(this.walkTokens(cell.tokens, callback));\n }\n for (const row of tableToken.rows) {\n for (const cell of row) {\n values = values.concat(this.walkTokens(cell.tokens, callback));\n }\n }\n break;\n }\n case 'list': {\n const listToken = token as Tokens.List;\n values = values.concat(this.walkTokens(listToken.items, callback));\n break;\n }\n default: {\n const genericToken = token as Tokens.Generic;\n if (this.defaults.extensions?.childTokens?.[genericToken.type]) {\n this.defaults.extensions.childTokens[genericToken.type].forEach((childTokens) => {\n const tokens = genericToken[childTokens].flat(Infinity) as Token[] | TokensList;\n values = values.concat(this.walkTokens(tokens, callback));\n });\n } else if (genericToken.tokens) {\n values = values.concat(this.walkTokens(genericToken.tokens, callback));\n }\n }\n }\n }\n return values;\n }\n\n use(...args: MarkedExtension[]) {\n const extensions: MarkedOptions['extensions'] = this.defaults.extensions || { renderers: {}, childTokens: {} };\n\n args.forEach((pack) => {\n // copy options to new object\n const opts = { ...pack } as MarkedOptions;\n\n // set async to true if it was set to true before\n opts.async = this.defaults.async || opts.async || false;\n\n // ==-- Parse \"addon\" extensions --== //\n if (pack.extensions) {\n pack.extensions.forEach((ext) => {\n if (!ext.name) {\n throw new Error('extension name required');\n }\n if ('renderer' in ext) { // Renderer extensions\n const prevRenderer = extensions.renderers[ext.name];\n if (prevRenderer) {\n // Replace extension with func to run new extension but fall back if false\n extensions.renderers[ext.name] = function(...args) {\n let ret = ext.renderer.apply(this, args);\n if (ret === false) {\n ret = prevRenderer.apply(this, args);\n }\n return ret;\n };\n } else {\n extensions.renderers[ext.name] = ext.renderer;\n }\n }\n if ('tokenizer' in ext) { // Tokenizer Extensions\n if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) {\n throw new Error(\"extension level must be 'block' or 'inline'\");\n }\n const extLevel = extensions[ext.level];\n if (extLevel) {\n extLevel.unshift(ext.tokenizer);\n } else {\n extensions[ext.level] = [ext.tokenizer];\n }\n if (ext.start) { // Function to check for start of token\n if (ext.level === 'block') {\n if (extensions.startBlock) {\n extensions.startBlock.push(ext.start);\n } else {\n extensions.startBlock = [ext.start];\n }\n } else if (ext.level === 'inline') {\n if (extensions.startInline) {\n extensions.startInline.push(ext.start);\n } else {\n extensions.startInline = [ext.start];\n }\n }\n }\n }\n if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens\n extensions.childTokens[ext.name] = ext.childTokens;\n }\n });\n opts.extensions = extensions;\n }\n\n // ==-- Parse \"overwrite\" extensions --== //\n if (pack.renderer) {\n const renderer = this.defaults.renderer || new _Renderer(this.defaults);\n for (const prop in pack.renderer) {\n if (!(prop in renderer)) {\n throw new Error(`renderer '${prop}' does not exist`);\n }\n if (['options', 'parser'].includes(prop)) {\n // ignore options property\n continue;\n }\n const rendererProp = prop as Exclude, 'options' | 'parser'>;\n const rendererFunc = pack.renderer[rendererProp] as GenericRendererFunction;\n const prevRenderer = renderer[rendererProp] as GenericRendererFunction;\n // Replace renderer with func to run extension, but fall back if false\n renderer[rendererProp] = (...args: unknown[]) => {\n let ret = rendererFunc.apply(renderer, args);\n if (ret === false) {\n ret = prevRenderer.apply(renderer, args);\n }\n return (ret || '') as RendererOutput;\n };\n }\n opts.renderer = renderer;\n }\n if (pack.tokenizer) {\n const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults);\n for (const prop in pack.tokenizer) {\n if (!(prop in tokenizer)) {\n throw new Error(`tokenizer '${prop}' does not exist`);\n }\n if (['options', 'rules', 'lexer'].includes(prop)) {\n // ignore options, rules, and lexer properties\n continue;\n }\n const tokenizerProp = prop as Exclude, 'options' | 'rules' | 'lexer'>;\n const tokenizerFunc = pack.tokenizer[tokenizerProp] as UnknownFunction;\n const prevTokenizer = tokenizer[tokenizerProp] as UnknownFunction;\n // Replace tokenizer with func to run extension, but fall back if false\n // @ts-expect-error cannot type tokenizer function dynamically\n tokenizer[tokenizerProp] = (...args: unknown[]) => {\n let ret = tokenizerFunc.apply(tokenizer, args);\n if (ret === false) {\n ret = prevTokenizer.apply(tokenizer, args);\n }\n return ret;\n };\n }\n opts.tokenizer = tokenizer;\n }\n\n // ==-- Parse Hooks extensions --== //\n if (pack.hooks) {\n const hooks = this.defaults.hooks || new _Hooks();\n for (const prop in pack.hooks) {\n if (!(prop in hooks)) {\n throw new Error(`hook '${prop}' does not exist`);\n }\n if (['options', 'block'].includes(prop)) {\n // ignore options and block properties\n continue;\n }\n const hooksProp = prop as Exclude, 'options' | 'block'>;\n const hooksFunc = pack.hooks[hooksProp] as UnknownFunction;\n const prevHook = hooks[hooksProp] as UnknownFunction;\n if (_Hooks.passThroughHooks.has(prop)) {\n // @ts-expect-error cannot type hook function dynamically\n hooks[hooksProp] = (arg: unknown) => {\n if (this.defaults.async && _Hooks.passThroughHooksRespectAsync.has(prop)) {\n return (async() => {\n const ret = await hooksFunc.call(hooks, arg);\n return prevHook.call(hooks, ret);\n })();\n }\n\n const ret = hooksFunc.call(hooks, arg);\n return prevHook.call(hooks, ret);\n };\n } else {\n // @ts-expect-error cannot type hook function dynamically\n hooks[hooksProp] = (...args: unknown[]) => {\n if (this.defaults.async) {\n return (async() => {\n let ret = await hooksFunc.apply(hooks, args);\n if (ret === false) {\n ret = await prevHook.apply(hooks, args);\n }\n return ret;\n })();\n }\n\n let ret = hooksFunc.apply(hooks, args);\n if (ret === false) {\n ret = prevHook.apply(hooks, args);\n }\n return ret;\n };\n }\n }\n opts.hooks = hooks;\n }\n\n // ==-- Parse WalkTokens extensions --== //\n if (pack.walkTokens) {\n const walkTokens = this.defaults.walkTokens;\n const packWalktokens = pack.walkTokens;\n opts.walkTokens = function(token) {\n let values: MaybePromise[] = [];\n values.push(packWalktokens.call(this, token));\n if (walkTokens) {\n values = values.concat(walkTokens.call(this, token));\n }\n return values;\n };\n }\n\n this.defaults = { ...this.defaults, ...opts };\n });\n\n return this;\n }\n\n setOptions(opt: MarkedOptions) {\n this.defaults = { ...this.defaults, ...opt };\n return this;\n }\n\n lexer(src: string, options?: MarkedOptions) {\n return _Lexer.lex(src, options ?? this.defaults);\n }\n\n parser(tokens: Token[], options?: MarkedOptions) {\n return _Parser.parse(tokens, options ?? this.defaults);\n }\n\n private parseMarkdown(blockType: boolean) {\n type overloadedParse = {\n (src: string, options: MarkedOptions & { async: true }): Promise;\n (src: string, options: MarkedOptions & { async: false }): ParserOutput;\n (src: string, options?: MarkedOptions | null): ParserOutput | Promise;\n };\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const parse: overloadedParse = (src: string, options?: MarkedOptions | null): any => {\n const origOpt = { ...options };\n const opt = { ...this.defaults, ...origOpt };\n\n const throwError = this.onError(!!opt.silent, !!opt.async);\n\n // throw error if an extension set async to true but parse was called with async: false\n if (this.defaults.async === true && origOpt.async === false) {\n return throwError(new Error('marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.'));\n }\n\n // throw error in case of non string input\n if (typeof src === 'undefined' || src === null) {\n return throwError(new Error('marked(): input parameter is undefined or null'));\n }\n if (typeof src !== 'string') {\n return throwError(new Error('marked(): input parameter is of type '\n + Object.prototype.toString.call(src) + ', string expected'));\n }\n\n if (opt.hooks) {\n opt.hooks.options = opt;\n opt.hooks.block = blockType;\n }\n\n if (opt.async) {\n return (async() => {\n const processedSrc = opt.hooks ? await opt.hooks.preprocess(src) : src;\n const lexer = opt.hooks ? await opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline);\n const tokens = await lexer(processedSrc, opt);\n const processedTokens = opt.hooks ? await opt.hooks.processAllTokens(tokens) : tokens;\n if (opt.walkTokens) {\n await Promise.all(this.walkTokens(processedTokens, opt.walkTokens));\n }\n const parser = opt.hooks ? await opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline);\n const html = await parser(processedTokens, opt);\n return opt.hooks ? await opt.hooks.postprocess(html) : html;\n })().catch(throwError);\n }\n\n try {\n if (opt.hooks) {\n src = opt.hooks.preprocess(src) as string;\n }\n const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline);\n let tokens = lexer(src, opt);\n if (opt.hooks) {\n tokens = opt.hooks.processAllTokens(tokens);\n }\n if (opt.walkTokens) {\n this.walkTokens(tokens, opt.walkTokens);\n }\n const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline);\n let html = parser(tokens, opt);\n if (opt.hooks) {\n html = opt.hooks.postprocess(html);\n }\n return html;\n } catch(e) {\n return throwError(e as Error);\n }\n };\n\n return parse;\n }\n\n private onError(silent: boolean, async: boolean) {\n return (e: Error): string | Promise => {\n e.message += '\\nPlease report this to https://github.com/markedjs/marked.';\n\n if (silent) {\n const msg = '

    An error occurred:

    '\n          + escapeHtmlEntities(e.message + '', true)\n          + '
    ';\n if (async) {\n return Promise.resolve(msg);\n }\n return msg;\n }\n\n if (async) {\n return Promise.reject(e);\n }\n throw e;\n };\n }\n}\n", "import { _Lexer } from './Lexer.ts';\nimport { _Parser } from './Parser.ts';\nimport { _Tokenizer } from './Tokenizer.ts';\nimport { _Renderer } from './Renderer.ts';\nimport { _TextRenderer } from './TextRenderer.ts';\nimport { _Hooks } from './Hooks.ts';\nimport { Marked } from './Instance.ts';\nimport {\n _getDefaults,\n changeDefaults,\n _defaults,\n} from './defaults.ts';\nimport type { MarkedExtension, MarkedOptions } from './MarkedOptions.ts';\nimport type { Token, TokensList } from './Tokens.ts';\nimport type { MaybePromise } from './Instance.ts';\n\nconst markedInstance = new Marked();\n\n/**\n * Compiles markdown to HTML asynchronously.\n *\n * @param src String of markdown source to be compiled\n * @param options Hash of options, having async: true\n * @return Promise of string of compiled HTML\n */\nexport function marked(src: string, options: MarkedOptions & { async: true }): Promise;\n\n/**\n * Compiles markdown to HTML.\n *\n * @param src String of markdown source to be compiled\n * @param options Optional hash of options\n * @return String of compiled HTML. Will be a Promise of string if async is set to true by any extensions.\n */\nexport function marked(src: string, options: MarkedOptions & { async: false }): string;\nexport function marked(src: string, options: MarkedOptions & { async: true }): Promise;\nexport function marked(src: string, options?: MarkedOptions | null): string | Promise;\nexport function marked(src: string, opt?: MarkedOptions | null): string | Promise {\n return markedInstance.parse(src, opt);\n}\n\n/**\n * Sets the default options.\n *\n * @param options Hash of options\n */\nmarked.options =\n marked.setOptions = function(options: MarkedOptions) {\n markedInstance.setOptions(options);\n marked.defaults = markedInstance.defaults;\n changeDefaults(marked.defaults);\n return marked;\n };\n\n/**\n * Gets the original marked default options.\n */\nmarked.getDefaults = _getDefaults;\n\nmarked.defaults = _defaults;\n\n/**\n * Use Extension\n */\n\nmarked.use = function(...args: MarkedExtension[]) {\n markedInstance.use(...args);\n marked.defaults = markedInstance.defaults;\n changeDefaults(marked.defaults);\n return marked;\n};\n\n/**\n * Run callback for every token\n */\n\nmarked.walkTokens = function(tokens: Token[] | TokensList, callback: (token: Token) => MaybePromise | MaybePromise[]) {\n return markedInstance.walkTokens(tokens, callback);\n};\n\n/**\n * Compiles markdown to HTML without enclosing `p` tag.\n *\n * @param src String of markdown source to be compiled\n * @param options Hash of options\n * @return String of compiled HTML\n */\nmarked.parseInline = markedInstance.parseInline;\n\n/**\n * Expose\n */\nmarked.Parser = _Parser;\nmarked.parser = _Parser.parse;\nmarked.Renderer = _Renderer;\nmarked.TextRenderer = _TextRenderer;\nmarked.Lexer = _Lexer;\nmarked.lexer = _Lexer.lex;\nmarked.Tokenizer = _Tokenizer;\nmarked.Hooks = _Hooks;\nmarked.parse = marked;\n\nexport const options = marked.options;\nexport const setOptions = marked.setOptions;\nexport const use = marked.use;\nexport const walkTokens = marked.walkTokens;\nexport const parseInline = marked.parseInline;\nexport const parse = marked;\nexport const parser = _Parser.parse;\nexport const lexer = _Lexer.lex;\nexport { _defaults as defaults, _getDefaults as getDefaults } from './defaults.ts';\nexport { _Lexer as Lexer } from './Lexer.ts';\nexport { _Parser as Parser } from './Parser.ts';\nexport { _Tokenizer as Tokenizer } from './Tokenizer.ts';\nexport { _Renderer as Renderer } from './Renderer.ts';\nexport { _TextRenderer as TextRenderer } from './TextRenderer.ts';\nexport { _Hooks as Hooks } from './Hooks.ts';\nexport { Marked } from './Instance.ts';\nexport type * from './MarkedOptions.ts';\nexport type * from './Tokens.ts';\n", "\nconst SHIFT_LEFT_32 = (1 << 16) * (1 << 16);\nconst SHIFT_RIGHT_32 = 1 / SHIFT_LEFT_32;\n\n// Threshold chosen based on both benchmarking and knowledge about browser string\n// data structures (which currently switch structure types at 12 bytes or more)\nconst TEXT_DECODER_MIN_LENGTH = 12;\nconst utf8TextDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8');\n\nconst PBF_VARINT = 0; // varint: int32, int64, uint32, uint64, sint32, sint64, bool, enum\nconst PBF_FIXED64 = 1; // 64-bit: double, fixed64, sfixed64\nconst PBF_BYTES = 2; // length-delimited: string, bytes, embedded messages, packed repeated fields\nconst PBF_FIXED32 = 5; // 32-bit: float, fixed32, sfixed32\n\nexport default class Pbf {\n /**\n * @param {Uint8Array | ArrayBuffer} [buf]\n */\n constructor(buf = new Uint8Array(16)) {\n this.buf = ArrayBuffer.isView(buf) ? buf : new Uint8Array(buf);\n this.dataView = new DataView(this.buf.buffer);\n this.pos = 0;\n this.type = 0;\n this.length = this.buf.length;\n }\n\n // === READING =================================================================\n\n /**\n * @template T\n * @param {(tag: number, result: T, pbf: Pbf) => void} readField\n * @param {T} result\n * @param {number} [end]\n */\n readFields(readField, result, end = this.length) {\n while (this.pos < end) {\n const val = this.readVarint(),\n tag = val >> 3,\n startPos = this.pos;\n\n this.type = val & 0x7;\n readField(tag, result, this);\n\n if (this.pos === startPos) this.skip(val);\n }\n return result;\n }\n\n /**\n * @template T\n * @param {(tag: number, result: T, pbf: Pbf) => void} readField\n * @param {T} result\n */\n readMessage(readField, result) {\n return this.readFields(readField, result, this.readVarint() + this.pos);\n }\n\n readFixed32() {\n const val = this.dataView.getUint32(this.pos, true);\n this.pos += 4;\n return val;\n }\n\n readSFixed32() {\n const val = this.dataView.getInt32(this.pos, true);\n this.pos += 4;\n return val;\n }\n\n // 64-bit int handling is based on github.com/dpw/node-buffer-more-ints (MIT-licensed)\n\n readFixed64() {\n const val = this.dataView.getUint32(this.pos, true) + this.dataView.getUint32(this.pos + 4, true) * SHIFT_LEFT_32;\n this.pos += 8;\n return val;\n }\n\n readSFixed64() {\n const val = this.dataView.getUint32(this.pos, true) + this.dataView.getInt32(this.pos + 4, true) * SHIFT_LEFT_32;\n this.pos += 8;\n return val;\n }\n\n readFloat() {\n const val = this.dataView.getFloat32(this.pos, true);\n this.pos += 4;\n return val;\n }\n\n readDouble() {\n const val = this.dataView.getFloat64(this.pos, true);\n this.pos += 8;\n return val;\n }\n\n /**\n * @param {boolean} [isSigned]\n */\n readVarint(isSigned) {\n const buf = this.buf;\n let val, b;\n\n b = buf[this.pos++]; val = b & 0x7f; if (b < 0x80) return val;\n b = buf[this.pos++]; val |= (b & 0x7f) << 7; if (b < 0x80) return val;\n b = buf[this.pos++]; val |= (b & 0x7f) << 14; if (b < 0x80) return val;\n b = buf[this.pos++]; val |= (b & 0x7f) << 21; if (b < 0x80) return val;\n b = buf[this.pos]; val |= (b & 0x0f) << 28;\n\n return readVarintRemainder(val, isSigned, this);\n }\n\n readVarint64() { // for compatibility with v2.0.1\n return this.readVarint(true);\n }\n\n readSVarint() {\n const num = this.readVarint();\n return num % 2 === 1 ? (num + 1) / -2 : num / 2; // zigzag encoding\n }\n\n readBoolean() {\n return Boolean(this.readVarint());\n }\n\n readString() {\n const end = this.readVarint() + this.pos;\n const pos = this.pos;\n this.pos = end;\n\n if (end - pos >= TEXT_DECODER_MIN_LENGTH && utf8TextDecoder) {\n // longer strings are fast with the built-in browser TextDecoder API\n return utf8TextDecoder.decode(this.buf.subarray(pos, end));\n }\n // short strings are fast with our custom implementation\n return readUtf8(this.buf, pos, end);\n }\n\n readBytes() {\n const end = this.readVarint() + this.pos,\n buffer = this.buf.subarray(this.pos, end);\n this.pos = end;\n return buffer;\n }\n\n // verbose for performance reasons; doesn't affect gzipped size\n\n /**\n * @param {number[]} [arr]\n * @param {boolean} [isSigned]\n */\n readPackedVarint(arr = [], isSigned) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readVarint(isSigned));\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedSVarint(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readSVarint());\n return arr;\n }\n /** @param {boolean[]} [arr] */\n readPackedBoolean(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readBoolean());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedFloat(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readFloat());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedDouble(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readDouble());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedFixed32(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readFixed32());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedSFixed32(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readSFixed32());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedFixed64(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readFixed64());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedSFixed64(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readSFixed64());\n return arr;\n }\n readPackedEnd() {\n return this.type === PBF_BYTES ? this.readVarint() + this.pos : this.pos + 1;\n }\n\n /** @param {number} val */\n skip(val) {\n const type = val & 0x7;\n if (type === PBF_VARINT) while (this.buf[this.pos++] > 0x7f) {}\n else if (type === PBF_BYTES) this.pos = this.readVarint() + this.pos;\n else if (type === PBF_FIXED32) this.pos += 4;\n else if (type === PBF_FIXED64) this.pos += 8;\n else throw new Error(`Unimplemented type: ${type}`);\n }\n\n // === WRITING =================================================================\n\n /**\n * @param {number} tag\n * @param {number} type\n */\n writeTag(tag, type) {\n this.writeVarint((tag << 3) | type);\n }\n\n /** @param {number} min */\n realloc(min) {\n let length = this.length || 16;\n\n while (length < this.pos + min) length *= 2;\n\n if (length !== this.length) {\n const buf = new Uint8Array(length);\n buf.set(this.buf);\n this.buf = buf;\n this.dataView = new DataView(buf.buffer);\n this.length = length;\n }\n }\n\n finish() {\n this.length = this.pos;\n this.pos = 0;\n return this.buf.subarray(0, this.length);\n }\n\n /** @param {number} val */\n writeFixed32(val) {\n this.realloc(4);\n this.dataView.setInt32(this.pos, val, true);\n this.pos += 4;\n }\n\n /** @param {number} val */\n writeSFixed32(val) {\n this.realloc(4);\n this.dataView.setInt32(this.pos, val, true);\n this.pos += 4;\n }\n\n /** @param {number} val */\n writeFixed64(val) {\n this.realloc(8);\n this.dataView.setInt32(this.pos, val & -1, true);\n this.dataView.setInt32(this.pos + 4, Math.floor(val * SHIFT_RIGHT_32), true);\n this.pos += 8;\n }\n\n /** @param {number} val */\n writeSFixed64(val) {\n this.realloc(8);\n this.dataView.setInt32(this.pos, val & -1, true);\n this.dataView.setInt32(this.pos + 4, Math.floor(val * SHIFT_RIGHT_32), true);\n this.pos += 8;\n }\n\n /** @param {number} val */\n writeVarint(val) {\n val = +val || 0;\n\n if (val > 0xfffffff || val < 0) {\n writeBigVarint(val, this);\n return;\n }\n\n this.realloc(4);\n\n this.buf[this.pos++] = val & 0x7f | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;\n this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;\n this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;\n this.buf[this.pos++] = (val >>> 7) & 0x7f;\n }\n\n /** @param {number} val */\n writeSVarint(val) {\n this.writeVarint(val < 0 ? -val * 2 - 1 : val * 2);\n }\n\n /** @param {boolean} val */\n writeBoolean(val) {\n this.writeVarint(+val);\n }\n\n /** @param {string} str */\n writeString(str) {\n str = String(str);\n this.realloc(str.length * 4);\n\n this.pos++; // reserve 1 byte for short string length\n\n const startPos = this.pos;\n // write the string directly to the buffer and see how much was written\n this.pos = writeUtf8(this.buf, str, this.pos);\n const len = this.pos - startPos;\n\n if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);\n\n // finally, write the message length in the reserved place and restore the position\n this.pos = startPos - 1;\n this.writeVarint(len);\n this.pos += len;\n }\n\n /** @param {number} val */\n writeFloat(val) {\n this.realloc(4);\n this.dataView.setFloat32(this.pos, val, true);\n this.pos += 4;\n }\n\n /** @param {number} val */\n writeDouble(val) {\n this.realloc(8);\n this.dataView.setFloat64(this.pos, val, true);\n this.pos += 8;\n }\n\n /** @param {Uint8Array} buffer */\n writeBytes(buffer) {\n const len = buffer.length;\n this.writeVarint(len);\n this.realloc(len);\n for (let i = 0; i < len; i++) this.buf[this.pos++] = buffer[i];\n }\n\n /**\n * @template T\n * @param {(obj: T, pbf: Pbf) => void} fn\n * @param {T} obj\n */\n writeRawMessage(fn, obj) {\n this.pos++; // reserve 1 byte for short message length\n\n // write the message directly to the buffer and see how much was written\n const startPos = this.pos;\n fn(obj, this);\n const len = this.pos - startPos;\n\n if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);\n\n // finally, write the message length in the reserved place and restore the position\n this.pos = startPos - 1;\n this.writeVarint(len);\n this.pos += len;\n }\n\n /**\n * @template T\n * @param {number} tag\n * @param {(obj: T, pbf: Pbf) => void} fn\n * @param {T} obj\n */\n writeMessage(tag, fn, obj) {\n this.writeTag(tag, PBF_BYTES);\n this.writeRawMessage(fn, obj);\n }\n\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedVarint(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedVarint, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedSVarint(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedSVarint, arr);\n }\n /**\n * @param {number} tag\n * @param {boolean[]} arr\n */\n writePackedBoolean(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedBoolean, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedFloat(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedFloat, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedDouble(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedDouble, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedFixed32(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedFixed32, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedSFixed32(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedSFixed32, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedFixed64(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedFixed64, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedSFixed64(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedSFixed64, arr);\n }\n\n /**\n * @param {number} tag\n * @param {Uint8Array} buffer\n */\n writeBytesField(tag, buffer) {\n this.writeTag(tag, PBF_BYTES);\n this.writeBytes(buffer);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeFixed32Field(tag, val) {\n this.writeTag(tag, PBF_FIXED32);\n this.writeFixed32(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeSFixed32Field(tag, val) {\n this.writeTag(tag, PBF_FIXED32);\n this.writeSFixed32(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeFixed64Field(tag, val) {\n this.writeTag(tag, PBF_FIXED64);\n this.writeFixed64(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeSFixed64Field(tag, val) {\n this.writeTag(tag, PBF_FIXED64);\n this.writeSFixed64(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeVarintField(tag, val) {\n this.writeTag(tag, PBF_VARINT);\n this.writeVarint(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeSVarintField(tag, val) {\n this.writeTag(tag, PBF_VARINT);\n this.writeSVarint(val);\n }\n /**\n * @param {number} tag\n * @param {string} str\n */\n writeStringField(tag, str) {\n this.writeTag(tag, PBF_BYTES);\n this.writeString(str);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeFloatField(tag, val) {\n this.writeTag(tag, PBF_FIXED32);\n this.writeFloat(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeDoubleField(tag, val) {\n this.writeTag(tag, PBF_FIXED64);\n this.writeDouble(val);\n }\n /**\n * @param {number} tag\n * @param {boolean} val\n */\n writeBooleanField(tag, val) {\n this.writeVarintField(tag, +val);\n }\n};\n\n/**\n * @param {number} l\n * @param {boolean | undefined} s\n * @param {Pbf} p\n */\nfunction readVarintRemainder(l, s, p) {\n const buf = p.buf;\n let h, b;\n\n b = buf[p.pos++]; h = (b & 0x70) >> 4; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 3; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 10; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 17; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 24; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x01) << 31; if (b < 0x80) return toNum(l, h, s);\n\n throw new Error('Expected varint not more than 10 bytes');\n}\n\n/**\n * @param {number} low\n * @param {number} high\n * @param {boolean} [isSigned]\n */\nfunction toNum(low, high, isSigned) {\n return isSigned ? high * 0x100000000 + (low >>> 0) : ((high >>> 0) * 0x100000000) + (low >>> 0);\n}\n\n/**\n * @param {number} val\n * @param {Pbf} pbf\n */\nfunction writeBigVarint(val, pbf) {\n let low, high;\n\n if (val >= 0) {\n low = (val % 0x100000000) | 0;\n high = (val / 0x100000000) | 0;\n } else {\n low = ~(-val % 0x100000000);\n high = ~(-val / 0x100000000);\n\n if (low ^ 0xffffffff) {\n low = (low + 1) | 0;\n } else {\n low = 0;\n high = (high + 1) | 0;\n }\n }\n\n if (val >= 0x10000000000000000 || val < -0x10000000000000000) {\n throw new Error('Given varint doesn\\'t fit into 10 bytes');\n }\n\n pbf.realloc(10);\n\n writeBigVarintLow(low, high, pbf);\n writeBigVarintHigh(high, pbf);\n}\n\n/**\n * @param {number} high\n * @param {number} low\n * @param {Pbf} pbf\n */\nfunction writeBigVarintLow(low, high, pbf) {\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos] = low & 0x7f;\n}\n\n/**\n * @param {number} high\n * @param {Pbf} pbf\n */\nfunction writeBigVarintHigh(high, pbf) {\n const lsb = (high & 0x07) << 4;\n\n pbf.buf[pbf.pos++] |= lsb | ((high >>>= 3) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f;\n}\n\n/**\n * @param {number} startPos\n * @param {number} len\n * @param {Pbf} pbf\n */\nfunction makeRoomForExtraLength(startPos, len, pbf) {\n const extraLen =\n len <= 0x3fff ? 1 :\n len <= 0x1fffff ? 2 :\n len <= 0xfffffff ? 3 : Math.floor(Math.log(len) / (Math.LN2 * 7));\n\n // if 1 byte isn't enough for encoding message length, shift the data to the right\n pbf.realloc(extraLen);\n for (let i = pbf.pos - 1; i >= startPos; i--) pbf.buf[i + extraLen] = pbf.buf[i];\n}\n\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedVarint(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeVarint(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedSVarint(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeSVarint(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedFloat(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeFloat(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedDouble(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeDouble(arr[i]);\n}\n/**\n * @param {boolean[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedBoolean(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeBoolean(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedFixed32(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeFixed32(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedSFixed32(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeSFixed32(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedFixed64(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeFixed64(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedSFixed64(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeSFixed64(arr[i]);\n}\n\n// Buffer code below from https://github.com/feross/buffer, MIT-licensed\n\n/**\n * @param {Uint8Array} buf\n * @param {number} pos\n * @param {number} end\n */\nfunction readUtf8(buf, pos, end) {\n let str = '';\n let i = pos;\n\n while (i < end) {\n const b0 = buf[i];\n let c = null; // codepoint\n let bytesPerSequence =\n b0 > 0xEF ? 4 :\n b0 > 0xDF ? 3 :\n b0 > 0xBF ? 2 : 1;\n\n if (i + bytesPerSequence > end) break;\n\n let b1, b2, b3;\n\n if (bytesPerSequence === 1) {\n if (b0 < 0x80) {\n c = b0;\n }\n } else if (bytesPerSequence === 2) {\n b1 = buf[i + 1];\n if ((b1 & 0xC0) === 0x80) {\n c = (b0 & 0x1F) << 0x6 | (b1 & 0x3F);\n if (c <= 0x7F) {\n c = null;\n }\n }\n } else if (bytesPerSequence === 3) {\n b1 = buf[i + 1];\n b2 = buf[i + 2];\n if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80) {\n c = (b0 & 0xF) << 0xC | (b1 & 0x3F) << 0x6 | (b2 & 0x3F);\n if (c <= 0x7FF || (c >= 0xD800 && c <= 0xDFFF)) {\n c = null;\n }\n }\n } else if (bytesPerSequence === 4) {\n b1 = buf[i + 1];\n b2 = buf[i + 2];\n b3 = buf[i + 3];\n if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80 && (b3 & 0xC0) === 0x80) {\n c = (b0 & 0xF) << 0x12 | (b1 & 0x3F) << 0xC | (b2 & 0x3F) << 0x6 | (b3 & 0x3F);\n if (c <= 0xFFFF || c >= 0x110000) {\n c = null;\n }\n }\n }\n\n if (c === null) {\n c = 0xFFFD;\n bytesPerSequence = 1;\n\n } else if (c > 0xFFFF) {\n c -= 0x10000;\n str += String.fromCharCode(c >>> 10 & 0x3FF | 0xD800);\n c = 0xDC00 | c & 0x3FF;\n }\n\n str += String.fromCharCode(c);\n i += bytesPerSequence;\n }\n\n return str;\n}\n\n/**\n * @param {Uint8Array} buf\n * @param {string} str\n * @param {number} pos\n */\nfunction writeUtf8(buf, str, pos) {\n for (let i = 0, c, lead; i < str.length; i++) {\n c = str.charCodeAt(i); // code point\n\n if (c > 0xD7FF && c < 0xE000) {\n if (lead) {\n if (c < 0xDC00) {\n buf[pos++] = 0xEF;\n buf[pos++] = 0xBF;\n buf[pos++] = 0xBD;\n lead = c;\n continue;\n } else {\n c = lead - 0xD800 << 10 | c - 0xDC00 | 0x10000;\n lead = null;\n }\n } else {\n if (c > 0xDBFF || (i + 1 === str.length)) {\n buf[pos++] = 0xEF;\n buf[pos++] = 0xBF;\n buf[pos++] = 0xBD;\n } else {\n lead = c;\n }\n continue;\n }\n } else if (lead) {\n buf[pos++] = 0xEF;\n buf[pos++] = 0xBF;\n buf[pos++] = 0xBD;\n lead = null;\n }\n\n if (c < 0x80) {\n buf[pos++] = c;\n } else {\n if (c < 0x800) {\n buf[pos++] = c >> 0x6 | 0xC0;\n } else {\n if (c < 0x10000) {\n buf[pos++] = c >> 0xC | 0xE0;\n } else {\n buf[pos++] = c >> 0x12 | 0xF0;\n buf[pos++] = c >> 0xC & 0x3F | 0x80;\n }\n buf[pos++] = c >> 0x6 & 0x3F | 0x80;\n }\n buf[pos++] = c & 0x3F | 0x80;\n }\n }\n return pos;\n}\n", "/**\n * A standalone point geometry with useful accessor, comparison, and\n * modification methods.\n *\n * @class\n * @param {number} x the x-coordinate. This could be longitude or screen pixels, or any other sort of unit.\n * @param {number} y the y-coordinate. This could be latitude or screen pixels, or any other sort of unit.\n *\n * @example\n * const point = new Point(-77, 38);\n */\nexport default function Point(x, y) {\n this.x = x;\n this.y = y;\n}\n\nPoint.prototype = {\n /**\n * Clone this point, returning a new point that can be modified\n * without affecting the old one.\n * @return {Point} the clone\n */\n clone() { return new Point(this.x, this.y); },\n\n /**\n * Add this point's x & y coordinates to another point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n add(p) { return this.clone()._add(p); },\n\n /**\n * Subtract this point's x & y coordinates to from point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n sub(p) { return this.clone()._sub(p); },\n\n /**\n * Multiply this point's x & y coordinates by point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n multByPoint(p) { return this.clone()._multByPoint(p); },\n\n /**\n * Divide this point's x & y coordinates by point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n divByPoint(p) { return this.clone()._divByPoint(p); },\n\n /**\n * Multiply this point's x & y coordinates by a factor,\n * yielding a new point.\n * @param {number} k factor\n * @return {Point} output point\n */\n mult(k) { return this.clone()._mult(k); },\n\n /**\n * Divide this point's x & y coordinates by a factor,\n * yielding a new point.\n * @param {number} k factor\n * @return {Point} output point\n */\n div(k) { return this.clone()._div(k); },\n\n /**\n * Rotate this point around the 0, 0 origin by an angle a,\n * given in radians\n * @param {number} a angle to rotate around, in radians\n * @return {Point} output point\n */\n rotate(a) { return this.clone()._rotate(a); },\n\n /**\n * Rotate this point around p point by an angle a,\n * given in radians\n * @param {number} a angle to rotate around, in radians\n * @param {Point} p Point to rotate around\n * @return {Point} output point\n */\n rotateAround(a, p) { return this.clone()._rotateAround(a, p); },\n\n /**\n * Multiply this point by a 4x1 transformation matrix\n * @param {[number, number, number, number]} m transformation matrix\n * @return {Point} output point\n */\n matMult(m) { return this.clone()._matMult(m); },\n\n /**\n * Calculate this point but as a unit vector from 0, 0, meaning\n * that the distance from the resulting point to the 0, 0\n * coordinate will be equal to 1 and the angle from the resulting\n * point to the 0, 0 coordinate will be the same as before.\n * @return {Point} unit vector point\n */\n unit() { return this.clone()._unit(); },\n\n /**\n * Compute a perpendicular point, where the new y coordinate\n * is the old x coordinate and the new x coordinate is the old y\n * coordinate multiplied by -1\n * @return {Point} perpendicular point\n */\n perp() { return this.clone()._perp(); },\n\n /**\n * Return a version of this point with the x & y coordinates\n * rounded to integers.\n * @return {Point} rounded point\n */\n round() { return this.clone()._round(); },\n\n /**\n * Return the magnitude of this point: this is the Euclidean\n * distance from the 0, 0 coordinate to this point's x and y\n * coordinates.\n * @return {number} magnitude\n */\n mag() {\n return Math.sqrt(this.x * this.x + this.y * this.y);\n },\n\n /**\n * Judge whether this point is equal to another point, returning\n * true or false.\n * @param {Point} other the other point\n * @return {boolean} whether the points are equal\n */\n equals(other) {\n return this.x === other.x &&\n this.y === other.y;\n },\n\n /**\n * Calculate the distance from this point to another point\n * @param {Point} p the other point\n * @return {number} distance\n */\n dist(p) {\n return Math.sqrt(this.distSqr(p));\n },\n\n /**\n * Calculate the distance from this point to another point,\n * without the square root step. Useful if you're comparing\n * relative distances.\n * @param {Point} p the other point\n * @return {number} distance\n */\n distSqr(p) {\n const dx = p.x - this.x,\n dy = p.y - this.y;\n return dx * dx + dy * dy;\n },\n\n /**\n * Get the angle from the 0, 0 coordinate to this point, in radians\n * coordinates.\n * @return {number} angle\n */\n angle() {\n return Math.atan2(this.y, this.x);\n },\n\n /**\n * Get the angle from this point to another point, in radians\n * @param {Point} b the other point\n * @return {number} angle\n */\n angleTo(b) {\n return Math.atan2(this.y - b.y, this.x - b.x);\n },\n\n /**\n * Get the angle between this point and another point, in radians\n * @param {Point} b the other point\n * @return {number} angle\n */\n angleWith(b) {\n return this.angleWithSep(b.x, b.y);\n },\n\n /**\n * Find the angle of the two vectors, solving the formula for\n * the cross product a x b = |a||b|sin(\u03B8) for \u03B8.\n * @param {number} x the x-coordinate\n * @param {number} y the y-coordinate\n * @return {number} the angle in radians\n */\n angleWithSep(x, y) {\n return Math.atan2(\n this.x * y - this.y * x,\n this.x * x + this.y * y);\n },\n\n /** @param {[number, number, number, number]} m */\n _matMult(m) {\n const x = m[0] * this.x + m[1] * this.y,\n y = m[2] * this.x + m[3] * this.y;\n this.x = x;\n this.y = y;\n return this;\n },\n\n /** @param {Point} p */\n _add(p) {\n this.x += p.x;\n this.y += p.y;\n return this;\n },\n\n /** @param {Point} p */\n _sub(p) {\n this.x -= p.x;\n this.y -= p.y;\n return this;\n },\n\n /** @param {number} k */\n _mult(k) {\n this.x *= k;\n this.y *= k;\n return this;\n },\n\n /** @param {number} k */\n _div(k) {\n this.x /= k;\n this.y /= k;\n return this;\n },\n\n /** @param {Point} p */\n _multByPoint(p) {\n this.x *= p.x;\n this.y *= p.y;\n return this;\n },\n\n /** @param {Point} p */\n _divByPoint(p) {\n this.x /= p.x;\n this.y /= p.y;\n return this;\n },\n\n _unit() {\n this._div(this.mag());\n return this;\n },\n\n _perp() {\n const y = this.y;\n this.y = this.x;\n this.x = -y;\n return this;\n },\n\n /** @param {number} angle */\n _rotate(angle) {\n const cos = Math.cos(angle),\n sin = Math.sin(angle),\n x = cos * this.x - sin * this.y,\n y = sin * this.x + cos * this.y;\n this.x = x;\n this.y = y;\n return this;\n },\n\n /**\n * @param {number} angle\n * @param {Point} p\n */\n _rotateAround(angle, p) {\n const cos = Math.cos(angle),\n sin = Math.sin(angle),\n x = p.x + cos * (this.x - p.x) - sin * (this.y - p.y),\n y = p.y + sin * (this.x - p.x) + cos * (this.y - p.y);\n this.x = x;\n this.y = y;\n return this;\n },\n\n _round() {\n this.x = Math.round(this.x);\n this.y = Math.round(this.y);\n return this;\n },\n\n constructor: Point\n};\n\n/**\n * Construct a point from an array if necessary, otherwise if the input\n * is already a Point, return it unchanged.\n * @param {Point | [number, number] | {x: number, y: number}} p input value\n * @return {Point} constructed point.\n * @example\n * // this\n * var point = Point.convert([0, 1]);\n * // is equivalent to\n * var point = new Point(0, 1);\n */\nPoint.convert = function (p) {\n if (p instanceof Point) {\n return /** @type {Point} */ (p);\n }\n if (Array.isArray(p)) {\n return new Point(+p[0], +p[1]);\n }\n if (p.x !== undefined && p.y !== undefined) {\n return new Point(+p.x, +p.y);\n }\n throw new Error('Expected [x, y] or {x, y} point format');\n};\n", "\nimport Point from '@mapbox/point-geometry';\n\n/** @import Pbf from 'pbf' */\n/** @import {Feature} from 'geojson' */\n\nexport class VectorTileFeature {\n /**\n * @param {Pbf} pbf\n * @param {number} end\n * @param {number} extent\n * @param {string[]} keys\n * @param {(number | string | boolean)[]} values\n */\n constructor(pbf, end, extent, keys, values) {\n // Public\n\n /** @type {Record} */\n this.properties = {};\n\n this.extent = extent;\n /** @type {0 | 1 | 2 | 3} */\n this.type = 0;\n\n /** @type {number | undefined} */\n this.id = undefined;\n\n /** @private */\n this._pbf = pbf;\n /** @private */\n this._geometry = -1;\n /** @private */\n this._keys = keys;\n /** @private */\n this._values = values;\n\n pbf.readFields(readFeature, this, end);\n }\n\n loadGeometry() {\n const pbf = this._pbf;\n pbf.pos = this._geometry;\n\n const end = pbf.readVarint() + pbf.pos;\n\n /** @type Point[][] */\n const lines = [];\n\n /** @type Point[] | undefined */\n let line;\n\n let cmd = 1;\n let length = 0;\n let x = 0;\n let y = 0;\n\n while (pbf.pos < end) {\n if (length <= 0) {\n const cmdLen = pbf.readVarint();\n cmd = cmdLen & 0x7;\n length = cmdLen >> 3;\n }\n\n length--;\n\n if (cmd === 1 || cmd === 2) {\n x += pbf.readSVarint();\n y += pbf.readSVarint();\n\n if (cmd === 1) { // moveTo\n if (line) lines.push(line);\n line = [];\n }\n\n if (line) line.push(new Point(x, y));\n\n } else if (cmd === 7) {\n\n // Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90\n if (line) {\n line.push(line[0].clone()); // closePolygon\n }\n\n } else {\n throw new Error(`unknown command ${cmd}`);\n }\n }\n\n if (line) lines.push(line);\n\n return lines;\n }\n\n bbox() {\n const pbf = this._pbf;\n pbf.pos = this._geometry;\n\n const end = pbf.readVarint() + pbf.pos;\n let cmd = 1,\n length = 0,\n x = 0,\n y = 0,\n x1 = Infinity,\n x2 = -Infinity,\n y1 = Infinity,\n y2 = -Infinity;\n\n while (pbf.pos < end) {\n if (length <= 0) {\n const cmdLen = pbf.readVarint();\n cmd = cmdLen & 0x7;\n length = cmdLen >> 3;\n }\n\n length--;\n\n if (cmd === 1 || cmd === 2) {\n x += pbf.readSVarint();\n y += pbf.readSVarint();\n if (x < x1) x1 = x;\n if (x > x2) x2 = x;\n if (y < y1) y1 = y;\n if (y > y2) y2 = y;\n\n } else if (cmd !== 7) {\n throw new Error(`unknown command ${cmd}`);\n }\n }\n\n return [x1, y1, x2, y2];\n }\n\n /**\n * @param {number} x\n * @param {number} y\n * @param {number} z\n * @return {Feature}\n */\n toGeoJSON(x, y, z) {\n const size = this.extent * Math.pow(2, z),\n x0 = this.extent * x,\n y0 = this.extent * y,\n vtCoords = this.loadGeometry();\n\n /** @param {Point} p */\n function projectPoint(p) {\n return [\n (p.x + x0) * 360 / size - 180,\n 360 / Math.PI * Math.atan(Math.exp((1 - (p.y + y0) * 2 / size) * Math.PI)) - 90\n ];\n }\n\n /** @param {Point[]} line */\n function projectLine(line) {\n return line.map(projectPoint);\n }\n\n /** @type {Feature[\"geometry\"]} */\n let geometry;\n\n if (this.type === 1) {\n const points = [];\n for (const line of vtCoords) {\n points.push(line[0]);\n }\n const coordinates = projectLine(points);\n geometry = points.length === 1 ?\n {type: 'Point', coordinates: coordinates[0]} :\n {type: 'MultiPoint', coordinates};\n\n } else if (this.type === 2) {\n\n const coordinates = vtCoords.map(projectLine);\n geometry = coordinates.length === 1 ?\n {type: 'LineString', coordinates: coordinates[0]} :\n {type: 'MultiLineString', coordinates};\n\n } else if (this.type === 3) {\n const polygons = classifyRings(vtCoords);\n const coordinates = [];\n for (const polygon of polygons) {\n coordinates.push(polygon.map(projectLine));\n }\n geometry = coordinates.length === 1 ?\n {type: 'Polygon', coordinates: coordinates[0]} :\n {type: 'MultiPolygon', coordinates};\n } else {\n\n throw new Error('unknown feature type');\n }\n\n /** @type {Feature} */\n const result = {\n type: 'Feature',\n geometry,\n properties: this.properties\n };\n\n if (this.id != null) {\n result.id = this.id;\n }\n\n return result;\n }\n}\n\n/** @type {['Unknown', 'Point', 'LineString', 'Polygon']} */\nVectorTileFeature.types = ['Unknown', 'Point', 'LineString', 'Polygon'];\n\n/**\n * @param {number} tag\n * @param {VectorTileFeature} feature\n * @param {Pbf} pbf\n */\nfunction readFeature(tag, feature, pbf) {\n if (tag === 1) feature.id = pbf.readVarint();\n else if (tag === 2) readTag(pbf, feature);\n else if (tag === 3) feature.type = /** @type {0 | 1 | 2 | 3} */ (pbf.readVarint());\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 4) feature._geometry = pbf.pos;\n}\n\n/**\n * @param {Pbf} pbf\n * @param {VectorTileFeature} feature\n */\nfunction readTag(pbf, feature) {\n const end = pbf.readVarint() + pbf.pos;\n\n while (pbf.pos < end) {\n // @ts-expect-error TS2341 deliberately accessing a private property\n const key = feature._keys[pbf.readVarint()];\n // @ts-expect-error TS2341 deliberately accessing a private property\n const value = feature._values[pbf.readVarint()];\n feature.properties[key] = value;\n }\n}\n\n/** classifies an array of rings into polygons with outer rings and holes\n * @param {Point[][]} rings\n */\nexport function classifyRings(rings) {\n const len = rings.length;\n\n if (len <= 1) return [rings];\n\n const polygons = [];\n let polygon, ccw;\n\n for (let i = 0; i < len; i++) {\n const area = signedArea(rings[i]);\n if (area === 0) continue;\n\n if (ccw === undefined) ccw = area < 0;\n\n if (ccw === area < 0) {\n if (polygon) polygons.push(polygon);\n polygon = [rings[i]];\n\n } else if (polygon) {\n polygon.push(rings[i]);\n }\n }\n if (polygon) polygons.push(polygon);\n\n return polygons;\n}\n\n/** @param {Point[]} ring */\nfunction signedArea(ring) {\n let sum = 0;\n for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {\n p1 = ring[i];\n p2 = ring[j];\n sum += (p2.x - p1.x) * (p1.y + p2.y);\n }\n return sum;\n}\n\nexport class VectorTileLayer {\n /**\n * @param {Pbf} pbf\n * @param {number} [end]\n */\n constructor(pbf, end) {\n // Public\n this.version = 1;\n this.name = '';\n this.extent = 4096;\n this.length = 0;\n\n /** @private */\n this._pbf = pbf;\n\n /** @private\n * @type {string[]} */\n this._keys = [];\n\n /** @private\n * @type {(number | string | boolean)[]} */\n this._values = [];\n\n /** @private\n * @type {number[]} */\n this._features = [];\n\n pbf.readFields(readLayer, this, end);\n\n this.length = this._features.length;\n }\n\n /** return feature `i` from this layer as a `VectorTileFeature`\n * @param {number} i\n */\n feature(i) {\n if (i < 0 || i >= this._features.length) throw new Error('feature index out of bounds');\n\n this._pbf.pos = this._features[i];\n\n const end = this._pbf.readVarint() + this._pbf.pos;\n return new VectorTileFeature(this._pbf, end, this.extent, this._keys, this._values);\n }\n}\n\n/**\n * @param {number} tag\n * @param {VectorTileLayer} layer\n * @param {Pbf} pbf\n */\nfunction readLayer(tag, layer, pbf) {\n if (tag === 15) layer.version = pbf.readVarint();\n else if (tag === 1) layer.name = pbf.readString();\n else if (tag === 5) layer.extent = pbf.readVarint();\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 2) layer._features.push(pbf.pos);\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 3) layer._keys.push(pbf.readString());\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 4) layer._values.push(readValueMessage(pbf));\n}\n\n/**\n * @param {Pbf} pbf\n */\nfunction readValueMessage(pbf) {\n let value = null;\n const end = pbf.readVarint() + pbf.pos;\n\n while (pbf.pos < end) {\n const tag = pbf.readVarint() >> 3;\n\n value = tag === 1 ? pbf.readString() :\n tag === 2 ? pbf.readFloat() :\n tag === 3 ? pbf.readDouble() :\n tag === 4 ? pbf.readVarint64() :\n tag === 5 ? pbf.readVarint() :\n tag === 6 ? pbf.readSVarint() :\n tag === 7 ? pbf.readBoolean() : null;\n }\n if (value == null) {\n throw new Error('unknown feature value');\n }\n\n return value;\n}\n\nexport class VectorTile {\n /**\n * @param {Pbf} pbf\n * @param {number} [end]\n */\n constructor(pbf, end) {\n /** @type {Record} */\n this.layers = pbf.readFields(readTile, {}, end);\n }\n}\n\n/**\n * @param {number} tag\n * @param {Record} layers\n * @param {Pbf} pbf\n */\nfunction readTile(tag, layers, pbf) {\n if (tag === 3) {\n const layer = new VectorTileLayer(pbf, pbf.readVarint() + pbf.pos);\n if (layer.length) layers[layer.name] = layer;\n }\n}\n", "import RBush from 'rbush';\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { json as d3_json } from 'd3-fetch';\n\nimport { marked } from 'marked';\nimport Protobuf from 'pbf';\nimport { VectorTile } from '@mapbox/vector-tile';\n\nimport { fileFetcher } from '../core/file_fetcher';\nimport { localizer } from '../core/localizer';\nimport { geoExtent, geoVecAdd } from '../geo';\nimport { QAItem } from '../osm';\nimport { utilRebind, utilTiler, utilQsString } from '../util';\n\nconst tiler = utilTiler();\nconst dispatch = d3_dispatch('loaded');\nconst _tileZoom = 14;\nconst _osmoseUrlRoot = 'https://osmose.openstreetmap.fr/api/0.3';\nlet _osmoseData = { icons: {}, items: [] };\n\n// This gets reassigned if reset\nlet _cache;\n\nfunction abortRequest(controller) {\n if (controller) {\n controller.abort();\n }\n}\n\nfunction abortUnwantedRequests(cache, tiles) {\n Object.keys(cache.inflightTile).forEach(k => {\n let wanted = tiles.find(tile => k === tile.id);\n if (!wanted) {\n abortRequest(cache.inflightTile[k]);\n delete cache.inflightTile[k];\n }\n });\n}\n\nfunction encodeIssueRtree(d) {\n return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };\n}\n\n// Replace or remove QAItem from rtree\nfunction updateRtree(item, replace) {\n _cache.rtree.remove(item, (a, b) => a.data.id === b.data.id);\n\n if (replace) {\n _cache.rtree.insert(item);\n }\n}\n\n// Issues shouldn't obscure each other\nfunction preventCoincident(loc) {\n let coincident = false;\n do {\n // first time, move marker up. after that, move marker right.\n let delta = coincident ? [0.00001, 0] : [0, 0.00001];\n loc = geoVecAdd(loc, delta);\n let bbox = geoExtent(loc).bbox();\n coincident = _cache.rtree.search(bbox).length;\n } while (coincident);\n\n return loc;\n}\n\nexport default {\n title: 'osmose',\n\n init() {\n fileFetcher.get('qa_data')\n .then(d => {\n _osmoseData = d.osmose;\n _osmoseData.items = Object.keys(d.osmose.icons)\n .map(s => s.split('-')[0])\n .reduce((unique, item) => unique.indexOf(item) !== -1 ? unique : [...unique, item], []);\n });\n\n if (!_cache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset() {\n let _strings = {};\n let _colors = {};\n if (_cache) {\n Object.values(_cache.inflightTile).forEach(abortRequest);\n // Strings and colors are static and should not be re-populated\n _strings = _cache.strings;\n _colors = _cache.colors;\n }\n _cache = {\n data: {},\n loadedTile: {},\n inflightTile: {},\n inflightPost: {},\n closed: {},\n rtree: new RBush(),\n strings: _strings,\n colors: _colors\n };\n },\n\n loadIssues(projection) {\n let params = {\n // Tiles return a maximum # of issues\n // So we want to filter our request for only types iD supports\n item: _osmoseData.items\n };\n\n // determine the needed tiles to cover the view\n let tiles = tiler\n .zoomExtent([_tileZoom, _tileZoom])\n .getTiles(projection);\n\n // abort inflight requests that are no longer needed\n abortUnwantedRequests(_cache, tiles);\n\n // issue new requests..\n tiles.forEach(tile => {\n if (_cache.loadedTile[tile.id] || _cache.inflightTile[tile.id]) return;\n\n let [ x, y, z ] = tile.xyz;\n let url = `${_osmoseUrlRoot}/issues/${z}/${x}/${y}.mvt?` + utilQsString(params);\n\n let controller = new AbortController();\n _cache.inflightTile[tile.id] = controller;\n\n fetch(url, { signal: controller.signal })\n .then(data => data.arrayBuffer())\n .then(data => {\n delete _cache.inflightTile[tile.id];\n _cache.loadedTile[tile.id] = true;\n\n var vectorTile = new VectorTile(new Protobuf(data));\n data = vectorTile.layers.issues;\n const features = [];\n for (let i = 0; i < data.length; i++) {\n features.push(data.feature(i).toGeoJSON(x, y, z));\n }\n\n if (features.length > 0) {\n features.forEach(issue => {\n const { item, class: cl, uuid: id } = issue.properties;\n /* Osmose issues are uniquely identified by a unique\n `item` and `class` combination (both integer values) */\n const itemType = `${item}-${cl}`;\n\n // Filter out unsupported issue types (some are too specific or advanced)\n if (itemType in _osmoseData.icons) {\n let loc = issue.geometry.coordinates; // lon, lat\n loc = preventCoincident(loc);\n\n let d = new QAItem(loc, this, itemType, id, { item });\n\n // Setting elems here prevents UI detail requests\n if (item === 8300 || item === 8360) {\n d.elems = [];\n }\n\n _cache.data[d.id] = d;\n _cache.rtree.insert(encodeIssueRtree(d));\n }\n });\n }\n\n dispatch.call('loaded');\n })\n .catch(() => {\n delete _cache.inflightTile[tile.id];\n _cache.loadedTile[tile.id] = true;\n });\n });\n },\n\n loadIssueDetail(issue) {\n // Issue details only need to be fetched once\n if (issue.elems !== undefined) {\n return Promise.resolve(issue);\n }\n\n const url = `${_osmoseUrlRoot}/issue/${issue.id}?langs=${localizer.localeCode()}`;\n const cacheDetails = data => {\n // Associated elements used for highlighting\n // Assign directly for immediate use in the callback\n issue.elems = data.elems.map(e => e.type.substring(0,1) + e.id);\n\n // Some issues have instance specific detail in a subtitle\n issue.detail = data.subtitle ? marked(data.subtitle.auto) : '';\n\n this.replaceItem(issue);\n };\n\n return d3_json(url).then(cacheDetails).then(() => issue);\n },\n\n loadStrings(locale=localizer.localeCode()) {\n const items = Object.keys(_osmoseData.icons);\n\n if (\n locale in _cache.strings\n && Object.keys(_cache.strings[locale]).length === items.length\n ) {\n return Promise.resolve(_cache.strings[locale]);\n }\n\n // May be partially populated already if some requests were successful\n if (!(locale in _cache.strings)) {\n _cache.strings[locale] = {};\n }\n\n // Only need to cache strings for supported issue types\n // Using multiple individual item + class requests to reduce fetched data size\n const allRequests = items.map(itemType => {\n // No need to request data we already have\n if (itemType in _cache.strings[locale]) return null;\n\n const cacheData = data => {\n // Bunch of nested single value arrays of objects\n const [ cat = {items:[]} ] = data.categories;\n const [ item = {class:[]} ] = cat.items;\n const [ cl = null ] = item.class;\n\n // If null default value is reached, data wasn't as expected (or was empty)\n if (!cl) {\n /* eslint-disable no-console */\n console.log(`Osmose strings request (${itemType}) had unexpected data`);\n /* eslint-enable no-console */\n return;\n }\n\n // Cache served item colors to automatically style issue markers later\n const { item: itemInt, color } = item;\n if (/^#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/.test(color)) {\n _cache.colors[itemInt] = color;\n }\n\n // Value of root key will be null if no string exists\n // If string exists, value is an object with key 'auto' for string\n const { title, detail, fix, trap } = cl;\n\n // Osmose titles shouldn't contain markdown\n let issueStrings = {};\n if (title) issueStrings.title = title.auto;\n if (detail) issueStrings.detail = marked(detail.auto);\n if (trap) issueStrings.trap = marked(trap.auto);\n if (fix) issueStrings.fix = marked(fix.auto);\n\n _cache.strings[locale][itemType] = issueStrings;\n };\n\n const [ item, cl ] = itemType.split('-');\n\n // Osmose API falls back to English strings where untranslated or if locale doesn't exist\n const url = `${_osmoseUrlRoot}/items/${item}/class/${cl}?langs=${locale}`;\n\n return d3_json(url).then(cacheData);\n }).filter(Boolean);\n\n return Promise.all(allRequests).then(() => _cache.strings[locale]);\n },\n\n getStrings(itemType, locale=localizer.localeCode()) {\n // No need to fallback to English, Osmose API handles this for us\n return (locale in _cache.strings) ? _cache.strings[locale][itemType] : {};\n },\n\n getColor(itemType) {\n return (itemType in _cache.colors) ? _cache.colors[itemType] : '#FFFFFF';\n },\n\n postUpdate(issue, callback) {\n if (_cache.inflightPost[issue.id]) {\n return callback({ message: 'Issue update already inflight', status: -2 }, issue);\n }\n\n // UI sets the status to either 'done' or 'false'\n const url = `${_osmoseUrlRoot}/issue/${issue.id}/${issue.newStatus}`;\n const controller = new AbortController();\n const after = () => {\n delete _cache.inflightPost[issue.id];\n\n this.removeItem(issue);\n if (issue.newStatus === 'done') {\n // Keep track of the number of issues closed per `item` to tag the changeset\n if (!(issue.item in _cache.closed)) {\n _cache.closed[issue.item] = 0;\n }\n _cache.closed[issue.item] += 1;\n }\n if (callback) callback(null, issue);\n };\n\n _cache.inflightPost[issue.id] = controller;\n\n fetch(url, { signal: controller.signal })\n .then(after)\n .catch(err => {\n delete _cache.inflightPost[issue.id];\n if (callback) callback(err.message);\n });\n },\n\n // Get all cached QAItems covering the viewport\n getItems(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n\n return _cache.rtree.search(bbox).map(d => d.data);\n },\n\n // Get a QAItem from cache\n // NOTE: Don't change method name until UI v3 is merged\n getError(id) {\n return _cache.data[id];\n },\n\n // get the name of the icon to display for this item\n getIcon(itemType) {\n return _osmoseData.icons[itemType];\n },\n\n // Replace a single QAItem in the cache\n replaceItem(item) {\n if (!(item instanceof QAItem) || !item.id) return;\n\n _cache.data[item.id] = item;\n updateRtree(encodeIssueRtree(item), true); // true = replace\n return item;\n },\n\n // Remove a single QAItem from the cache\n removeItem(item) {\n if (!(item instanceof QAItem) || !item.id) return;\n\n delete _cache.data[item.id];\n updateRtree(encodeIssueRtree(item), false); // false = remove\n },\n\n // Used to populate `closed:osmose:*` changeset tags\n getClosedCounts() {\n return _cache.closed;\n },\n\n itemURL(item) {\n return `https://osmose.openstreetmap.fr/en/error/${item.id}`;\n }\n};\n", "import { geoScaleToZoom } from '../geo';\nimport { utilTiler } from './tiler';\nimport type { Projection } from '../geo/raw_mercator';\nimport type RBush from 'rbush';\nimport type { BBox } from 'rbush';\n\nexport interface WithBbox extends BBox {\n data: T;\n}\n\nexport function partitionViewport(projection: Projection) {\n let z = geoScaleToZoom(projection.scale());\n let z2 = (Math.ceil(z * 2) / 2) + 2.5; // round to next 0.5 and add 2.5\n let tiler = utilTiler().zoomExtent([z2, z2]);\n\n return (tiler.getTiles(projection) || []).map(tile => tile.extent);\n}\n\n\n/** no more than `limit` results per partition */\nexport function searchLimited(limit: number | undefined, projection: Projection, rtree: RBush>): T[] {\n limit ||= 5;\n\n return partitionViewport(projection)\n .flatMap((extent) => rtree.search(extent.bbox()).slice(0, limit))\n .map(result => result.data);\n}\n", "/* global mapillary:false */\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport Protobuf from 'pbf';\nimport RBush from 'rbush';\nimport { VectorTile } from '@mapbox/vector-tile';\nimport { geoExtent } from '../geo';\nimport { utilQsString, utilRebind, utilTiler, utilStringQs } from '../util';\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\n\nconst accessToken = 'MLY|4100327730013843|5bb78b81720791946a9a7b956c57b7cf';\nconst apiUrl = 'https://graph.mapillary.com/';\nconst baseTileUrl = 'https://tiles.mapillary.com/maps/vtp';\nconst mapFeatureTileUrl = `${baseTileUrl}/mly_map_feature_point/2/{z}/{x}/{y}?access_token=${accessToken}`;\nconst tileUrl = `${baseTileUrl}/mly1_public/2/{z}/{x}/{y}?access_token=${accessToken}`;\nconst trafficSignTileUrl = `${baseTileUrl}/mly_map_feature_traffic_sign/2/{z}/{x}/{y}?access_token=${accessToken}`;\n\nconst viewercss = 'mapillary-js/mapillary.css';\nconst viewerjs = 'mapillary-js/mapillary.js';\nconst minZoom = 14;\nconst dispatch = d3_dispatch('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'imageChanged');\n\nlet _loadViewerPromise;\nlet _mlyActiveImage;\nlet _mlyCache;\nlet _mlyFallback = false;\nlet _mlyHighlightedDetection;\nlet _mlyShowFeatureDetections = false;\nlet _mlyShowSignDetections = false;\nlet _mlyViewer;\nlet _mlyViewerFilter = ['all'];\nlet _isViewerOpen = false;\n\n\n// Load all data for the specified type from Mapillary vector tiles\nfunction loadTiles(which, url, maxZoom, projection) {\n const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);\n const tiles = tiler.getTiles(projection);\n\n tiles.forEach(function(tile) {\n loadTile(which, url, tile);\n });\n}\n\n\n// Load all data for the specified type from one vector tile\nfunction loadTile(which, url, tile) {\n const cache = _mlyCache.requests;\n const tileId = `${tile.id}-${which}`;\n if (cache.loaded[tileId] || cache.inflight[tileId]) return;\n const controller = new AbortController();\n cache.inflight[tileId] = controller;\n const requestUrl = url.replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n .replace('{z}', tile.xyz[2]);\n\n fetch(requestUrl, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (!data) {\n throw new Error('No Data');\n }\n loadTileDataToCache(data, tile, which);\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n } else if (which === 'signs') {\n dispatch.call('loadedSigns');\n } else if (which === 'points') {\n dispatch.call('loadedMapFeatures');\n }\n })\n .catch(function() {\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n });\n}\n\n\n// Load the data from the vector tile into cache\nfunction loadTileDataToCache(data, tile, which) {\n const vectorTile = new VectorTile(new Protobuf(data));\n let features,\n cache,\n layer,\n i,\n feature,\n loc,\n d;\n\n if (vectorTile.layers.hasOwnProperty('image')) {\n features = [];\n cache = _mlyCache.images;\n layer = vectorTile.layers.image;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n d = {\n service: 'photo',\n loc: loc,\n captured_at: feature.properties.captured_at,\n ca: feature.properties.compass_angle,\n id: feature.properties.id,\n is_pano: feature.properties.is_pano,\n sequence_id: feature.properties.sequence_id,\n };\n cache.forImageId[d.id] = d;\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty('sequence')) {\n cache = _mlyCache.sequences;\n layer = vectorTile.layers.sequence;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n if (cache.lineString[feature.properties.id]) {\n cache.lineString[feature.properties.id].push(feature);\n } else {\n cache.lineString[feature.properties.id] = [feature];\n }\n }\n }\n\n if (vectorTile.layers.hasOwnProperty('point')) {\n features = [];\n cache = _mlyCache[which];\n layer = vectorTile.layers.point;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n\n d = {\n service: 'photo',\n loc: loc,\n id: feature.properties.id,\n first_seen_at: feature.properties.first_seen_at,\n last_seen_at: feature.properties.last_seen_at,\n value: feature.properties.value\n };\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty('traffic_sign')) {\n features = [];\n cache = _mlyCache[which];\n layer = vectorTile.layers.traffic_sign;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n\n d = {\n service: 'photo',\n loc: loc,\n id: feature.properties.id,\n first_seen_at: feature.properties.first_seen_at,\n last_seen_at: feature.properties.last_seen_at,\n value: feature.properties.value\n };\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n}\n\n\n// Get data from the API\nfunction loadData(url) {\n return fetch(url)\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n })\n .then(function(result) {\n if (!result) {\n return [];\n }\n return result.data || [];\n });\n}\n\nexport default {\n // Initialize Mapillary\n init: function() {\n if (!_mlyCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n // Reset cache and state\n reset: function() {\n if (_mlyCache) {\n Object.values(_mlyCache.requests.inflight).forEach(function(request) { request.abort(); });\n }\n\n _mlyCache = {\n images: { rtree: new RBush(), forImageId: {} },\n image_detections: { forImageId: {} },\n signs: { rtree: new RBush() },\n points: { rtree: new RBush() },\n sequences: { rtree: new RBush(), lineString: {} },\n requests: { loaded: {}, inflight: {} }\n };\n\n _mlyActiveImage = null;\n },\n\n // Get visible images\n images: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _mlyCache.images.rtree);\n },\n\n // Get visible traffic signs\n signs: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _mlyCache.signs.rtree);\n },\n\n // Get visible map (point) features\n mapFeatures: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _mlyCache.points.rtree);\n },\n\n // Get cached image by id\n cachedImage: function(imageId) {\n return _mlyCache.images.forImageId[imageId];\n },\n\n // Get visible sequences\n sequences: function(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const sequenceIds = {};\n let lineStrings = [];\n\n _mlyCache.images.rtree.search(bbox)\n .forEach(function(d) {\n if (d.data.sequence_id) {\n sequenceIds[d.data.sequence_id] = true;\n }\n });\n\n Object.keys(sequenceIds).forEach(function(sequenceId) {\n if (_mlyCache.sequences.lineString[sequenceId]) {\n lineStrings = lineStrings.concat(_mlyCache.sequences.lineString[sequenceId]);\n }\n });\n\n return lineStrings;\n },\n\n\n // Load images in the visible area\n loadImages: function(projection) {\n loadTiles('images', tileUrl, 14, projection);\n },\n\n\n // Load traffic signs in the visible area\n loadSigns: function(projection) {\n loadTiles('signs', trafficSignTileUrl, 14, projection);\n },\n\n\n // Load map (point) features in the visible area\n loadMapFeatures: function(projection) {\n loadTiles('points', mapFeatureTileUrl, 14, projection);\n },\n\n\n // Return a promise that resolves when the image viewer (Mapillary JS) library has finished loading\n ensureViewerLoaded: function(context) {\n if (_loadViewerPromise) return _loadViewerPromise;\n\n // add mly-wrapper\n const wrap = context.container().select('.photoviewer')\n .selectAll('.mly-wrapper')\n .data([0]);\n\n wrap.enter()\n .append('div')\n .attr('id', 'ideditor-mly')\n .attr('class', 'photo-wrapper mly-wrapper')\n .classed('hide', true);\n\n const that = this;\n\n _loadViewerPromise = new Promise((resolve, reject) => {\n let loadedCount = 0;\n function loaded() {\n loadedCount += 1;\n\n // wait until both files are loaded\n if (loadedCount === 2) resolve();\n }\n\n const head = d3_select('head');\n\n // load mapillary-viewercss\n head.selectAll('#ideditor-mapillary-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-mapillary-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(viewercss))\n .on('load.serviceMapillary', loaded)\n .on('error.serviceMapillary', function() {\n reject();\n });\n\n // load mapillary-viewerjs\n head.selectAll('#ideditor-mapillary-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-mapillary-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(viewerjs))\n .on('load.serviceMapillary', loaded)\n .on('error.serviceMapillary', function() {\n reject();\n });\n })\n .catch(function() {\n _loadViewerPromise = null;\n })\n .then(function() {\n that.initViewer(context);\n });\n\n return _loadViewerPromise;\n },\n\n\n // Load traffic sign image sprites\n loadSignResources: function(context) {\n context.ui().svgDefs.addSprites(['mapillary-sprite'], false /* don't override colors */ );\n return this;\n },\n\n\n // Load map (point) feature image sprites\n loadObjectResources: function(context) {\n context.ui().svgDefs.addSprites(['mapillary-object-sprite'], false /* don't override colors */ );\n return this;\n },\n\n\n // Remove previous detections in image viewer\n resetTags: function() {\n if (_mlyViewer && !_mlyFallback) {\n _mlyViewer.getComponent('tag').removeAll();\n }\n },\n\n\n // Show map feature detections in image viewer\n showFeatureDetections: function(value) {\n _mlyShowFeatureDetections = value;\n if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {\n this.resetTags();\n }\n },\n\n\n // Show traffic sign detections in image viewer\n showSignDetections: function(value) {\n _mlyShowSignDetections = value;\n if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {\n this.resetTags();\n }\n },\n\n\n // Apply filter to image viewer\n filterViewer: function(context) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n const filter = ['all'];\n\n if (!showsPano) filter.push([ '!=', 'cameraType', 'spherical' ]);\n if (!showsFlat && showsPano) filter.push(['==', 'pano', true]);\n if (fromDate) {\n filter.push(['>=', 'capturedAt', new Date(fromDate).getTime()]);\n }\n if (toDate) {\n filter.push(['>=', 'capturedAt', new Date(toDate).getTime()]);\n }\n\n if (_mlyViewer) {\n _mlyViewer.setFilter(filter);\n }\n _mlyViewerFilter = filter;\n\n return filter;\n },\n\n\n // Make the image viewer visible\n showViewer: function(context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.mly-wrapper.hide').size();\n\n if (isHidden && _mlyViewer) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.mly-wrapper')\n .classed('hide', false);\n\n _mlyViewer.resize();\n }\n\n _isViewerOpen = true;\n return this;\n },\n\n\n // Hide the image viewer and resets map markers\n hideViewer: function(context) {\n _mlyActiveImage = null;\n\n if (!_mlyFallback && _mlyViewer) {\n _mlyViewer.getComponent('sequence').stop();\n }\n\n const viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n this.updateUrlImage(null);\n\n dispatch.call('imageChanged');\n dispatch.call('loadedMapFeatures');\n dispatch.call('loadedSigns');\n\n _isViewerOpen = false;\n\n return this.setStyles(context, null);\n },\n\n\n // Get viewer status\n isViewerOpen: function() {\n return _isViewerOpen;\n },\n\n\n // Update the URL with current image id\n updateUrlImage: function(imageId) {\n const hash = utilStringQs(window.location.hash);\n if (imageId) {\n hash.photo = 'mapillary/' + imageId;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n\n // Highlight the detection in the viewer that is related to the clicked map feature\n highlightDetection: function(detection) {\n if (detection) {\n _mlyHighlightedDetection = detection.id;\n }\n\n return this;\n },\n\n\n // Initialize image viewer (Mapillar JS)\n initViewer: function(context) {\n if (!window.mapillary) return;\n\n const opts = {\n accessToken: accessToken,\n component: {\n cover: false,\n keyboard: false,\n tag: true\n },\n container: 'ideditor-mly',\n };\n\n // Disable components requiring WebGL support\n if (!mapillary.isSupported() && mapillary.isFallbackSupported()) {\n _mlyFallback = true;\n opts.component = {\n cover: false,\n direction: false,\n imagePlane: false,\n keyboard: false,\n mouse: false,\n sequence: false,\n tag: false,\n image: true, // fallback\n navigation: true // fallback\n };\n }\n\n _mlyViewer = new mapillary.Viewer(opts);\n _mlyViewer.on('image', imageChanged.bind(this));\n _mlyViewer.on('bearing', bearingChanged);\n\n if (_mlyViewerFilter) {\n _mlyViewer.setFilter(_mlyViewerFilter);\n }\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.mapillary', function() {\n if (_mlyViewer) _mlyViewer.resize();\n });\n\n // imageChanged: called after the viewer has changed images and is ready.\n function imageChanged(photo) {\n this.resetTags();\n const image = photo.image;\n this.setActiveImage(image);\n this.setStyles(context, null);\n const loc = [image.originalLngLat.lng, image.originalLngLat.lat];\n context.map().centerEase(loc);\n this.updateUrlImage(image.id);\n\n if (_mlyShowFeatureDetections || _mlyShowSignDetections) {\n this.updateDetections(image.id, `${apiUrl}/${image.id}/detections?access_token=${accessToken}&fields=id,image,geometry,value`);\n }\n dispatch.call('imageChanged');\n }\n\n\n // bearingChanged: called when the bearing changes in the image viewer.\n function bearingChanged(e) {\n dispatch.call('bearingChanged', undefined, e);\n }\n },\n\n\n // Move to an image\n selectImage: function(context, imageId) {\n if (_mlyViewer && imageId) {\n _mlyViewer.moveTo(imageId)\n .then(image => this.setActiveImage(image))\n .catch(function(e) {\n console.error('mly3', e); // eslint-disable-line no-console\n });\n }\n\n return this;\n },\n\n\n // Return the currently displayed image\n getActiveImage: function() {\n return _mlyActiveImage;\n },\n\n\n // Return a list of detection objects for the given id\n getDetections: function(id) {\n return loadData(`${apiUrl}/${id}/detections?access_token=${accessToken}&fields=id,value,image`);\n },\n\n\n // Set the currently visible image\n setActiveImage: function(image) {\n if (image) {\n _mlyActiveImage = {\n ca: image.originalCompassAngle,\n id: image.id,\n loc: [image.originalLngLat.lng, image.originalLngLat.lat],\n is_pano: image.cameraType === 'spherical',\n sequence_id: image.sequenceId\n };\n } else {\n _mlyActiveImage = null;\n }\n },\n\n\n // Update the currently highlighted sequence and selected bubble.\n setStyles: function(context, hovered) {\n const hoveredImageId = hovered && hovered.id;\n const hoveredSequenceId = hovered && hovered.sequence_id;\n const selectedSequenceId = _mlyActiveImage && _mlyActiveImage.sequence_id;\n\n context.container().selectAll('.layer-mapillary .viewfield-group')\n .classed('highlighted', function(d) { return (d.sequence_id === selectedSequenceId) || (d.id === hoveredImageId); })\n .classed('hovered', function(d) { return d.id === hoveredImageId; });\n\n context.container().selectAll('.layer-mapillary .sequence')\n .classed('highlighted', function(d) { return d.properties.id === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.id === selectedSequenceId; });\n\n return this;\n },\n\n\n // Get detections for the current image and shows them in the image viewer\n updateDetections: function(imageId, url) {\n if (!_mlyViewer || _mlyFallback) return;\n if (!imageId) return;\n const cache = _mlyCache.image_detections;\n if (cache.forImageId[imageId]) {\n showDetections(_mlyCache.image_detections.forImageId[imageId]);\n } else {\n loadData(url)\n .then(detections => {\n detections.forEach(function(detection) {\n if (!cache.forImageId[imageId]) {\n cache.forImageId[imageId] = [];\n }\n cache.forImageId[imageId].push({\n geometry: detection.geometry,\n id: detection.id,\n image_id: imageId,\n value:detection.value\n });\n });\n\n showDetections(_mlyCache.image_detections.forImageId[imageId] || []);\n });\n }\n\n\n // Create a tag for each detection and shows it in the image viewer\n function showDetections(detections) {\n const tagComponent = _mlyViewer.getComponent('tag');\n detections.forEach(function(data) {\n const tag = makeTag(data);\n if (tag) {\n tagComponent.add([tag]);\n }\n });\n }\n\n\n // Create a Mapillary JS tag object\n function makeTag(data) {\n const valueParts = data.value.split('--');\n if (!valueParts.length) return;\n\n let tag;\n let text;\n let color = 0xffffff;\n\n if (_mlyHighlightedDetection === data.id) {\n color = 0xffff00;\n text = valueParts[1];\n if (text === 'flat' || text === 'discrete' || text === 'sign') {\n text = valueParts[2];\n }\n text = text.replace(/-/g, ' ');\n text = text.charAt(0).toUpperCase() + text.slice(1);\n _mlyHighlightedDetection = null;\n }\n\n var decodedGeometry = window.atob(data.geometry);\n var uintArray = new Uint8Array(decodedGeometry.length);\n for (var i = 0; i < decodedGeometry.length; i++) {\n uintArray[i] = decodedGeometry.charCodeAt(i);\n }\n const tile = new VectorTile(new Protobuf(uintArray.buffer));\n const layer = tile.layers['mpy-or'];\n\n const geometries = layer.feature(0).loadGeometry();\n\n const polygon = geometries.map(ring =>\n ring.map(point =>\n [point.x / layer.extent, point.y / layer.extent]));\n\n tag = new mapillary.OutlineTag(\n data.id,\n new mapillary.PolygonGeometry(polygon[0]),\n {\n text: text,\n textColor: color,\n lineColor: color,\n lineWidth: 2,\n fillColor: color,\n fillOpacity: 0.3,\n }\n );\n\n return tag;\n }\n },\n\n\n // Return the current cache\n cache: function() {\n return _mlyCache;\n }\n};\n", "import { osmAreaKeys as areaKeys } from '../osm/tags';\nimport { utilArrayIntersection } from '../util';\nimport { validationIssue } from '../core/validation';\n\n\nvar buildRuleChecks = function() {\n return {\n equals: function (equals) {\n return function(tags) {\n return Object.keys(equals).every(function(k) {\n return equals[k] === tags[k];\n });\n };\n },\n notEquals: function (notEquals) {\n return function(tags) {\n return Object.keys(notEquals).some(function(k) {\n return notEquals[k] !== tags[k];\n });\n };\n },\n absence: function(absence) {\n return function(tags) {\n return Object.keys(tags).indexOf(absence) === -1;\n };\n },\n presence: function(presence) {\n return function(tags) {\n return Object.keys(tags).indexOf(presence) > -1;\n };\n },\n greaterThan: function(greaterThan) {\n var key = Object.keys(greaterThan)[0];\n var value = greaterThan[key];\n\n return function(tags) {\n return tags[key] > value;\n };\n },\n greaterThanEqual: function(greaterThanEqual) {\n var key = Object.keys(greaterThanEqual)[0];\n var value = greaterThanEqual[key];\n\n return function(tags) {\n return tags[key] >= value;\n };\n },\n lessThan: function(lessThan) {\n var key = Object.keys(lessThan)[0];\n var value = lessThan[key];\n\n return function(tags) {\n return tags[key] < value;\n };\n },\n lessThanEqual: function(lessThanEqual) {\n var key = Object.keys(lessThanEqual)[0];\n var value = lessThanEqual[key];\n\n return function(tags) {\n return tags[key] <= value;\n };\n },\n positiveRegex: function(positiveRegex) {\n var tagKey = Object.keys(positiveRegex)[0];\n var expression = positiveRegex[tagKey].join('|');\n var regex = new RegExp(expression);\n\n return function(tags) {\n return regex.test(tags[tagKey]);\n };\n },\n negativeRegex: function(negativeRegex) {\n var tagKey = Object.keys(negativeRegex)[0];\n var expression = negativeRegex[tagKey].join('|');\n var regex = new RegExp(expression);\n\n return function(tags) {\n return !regex.test(tags[tagKey]);\n };\n }\n };\n};\n\nvar buildLineKeys = function() {\n return {\n highway: {\n rest_area: true,\n services: true\n },\n railway: {\n roundhouse: true,\n station: true,\n traverser: true,\n turntable: true,\n wash: true\n }\n };\n};\n\nexport default {\n init: function() {\n this._ruleChecks = buildRuleChecks();\n this._validationRules = [];\n this._areaKeys = areaKeys;\n this._lineKeys = buildLineKeys();\n },\n\n // list of rules only relevant to tag checks...\n filterRuleChecks: function(selector) {\n var _ruleChecks = this._ruleChecks;\n return Object.keys(selector).reduce(function(rules, key) {\n if (['geometry', 'error', 'warning'].indexOf(key) === -1) {\n rules.push(_ruleChecks[key](selector[key]));\n }\n return rules;\n }, []);\n },\n\n // builds tagMap from mapcss-parse selector object...\n buildTagMap: function(selector) {\n var getRegexValues = function(regexes) {\n return regexes.map(function(regex) {\n return regex.replace(/\\$|\\^/g, '');\n });\n };\n\n var tagMap = Object.keys(selector).reduce(function (expectedTags, key) {\n var values;\n var isRegex = /regex/gi.test(key);\n var isEqual = /equals/gi.test(key);\n\n if (isRegex || isEqual) {\n Object.keys(selector[key]).forEach(function(selectorKey) {\n values = isEqual ? [selector[key][selectorKey]] : getRegexValues(selector[key][selectorKey]);\n\n if (expectedTags.hasOwnProperty(selectorKey)) {\n values = values.concat(expectedTags[selectorKey]);\n }\n\n expectedTags[selectorKey] = values;\n });\n\n } else if (/(greater|less)Than(Equal)?|presence/g.test(key)) {\n var tagKey = /presence/.test(key) ? selector[key] : Object.keys(selector[key])[0];\n\n values = [selector[key][tagKey]];\n\n if (expectedTags.hasOwnProperty(tagKey)) {\n values = values.concat(expectedTags[tagKey]);\n }\n\n expectedTags[tagKey] = values;\n }\n\n return expectedTags;\n }, {});\n\n return tagMap;\n },\n\n // inspired by osmWay#isArea()\n inferGeometry: function(tagMap) {\n var _lineKeys = this._lineKeys;\n var _areaKeys = this._areaKeys;\n\n var keyValueDoesNotImplyArea = function(key) {\n return utilArrayIntersection(tagMap[key], Object.keys(_areaKeys[key])).length > 0;\n };\n var keyValueImpliesLine = function(key) {\n return utilArrayIntersection(tagMap[key], Object.keys(_lineKeys[key])).length > 0;\n };\n\n if (tagMap.hasOwnProperty('area')) {\n if (tagMap.area.indexOf('yes') > -1) {\n return 'area';\n }\n if (tagMap.area.indexOf('no') > -1) {\n return 'line';\n }\n }\n\n for (var key in tagMap) {\n if (key in _areaKeys && !keyValueDoesNotImplyArea(key)) {\n return 'area';\n }\n if (key in _lineKeys && keyValueImpliesLine(key)) {\n return 'area';\n }\n }\n\n return 'line';\n },\n\n // adds from mapcss-parse selector check...\n addRule: function(selector) {\n var rule = {\n // checks relevant to mapcss-selector\n checks: this.filterRuleChecks(selector),\n // true if all conditions for a tag error are true..\n matches: function(entity) {\n return this.checks.every(function(check) {\n return check(entity.tags);\n });\n },\n // borrowed from Way#isArea()\n inferredGeometry: this.inferGeometry(this.buildTagMap(selector), this._areaKeys),\n geometryMatches: function(entity, graph) {\n if (entity.type === 'node' || entity.type === 'relation') {\n return selector.geometry === entity.type;\n } else if (entity.type === 'way') {\n return this.inferredGeometry === entity.geometry(graph);\n }\n },\n // when geometries match and tag matches are present, return a warning...\n findIssues: function (entity, graph, issues) {\n if (this.geometryMatches(entity, graph) && this.matches(entity)) {\n var severity = Object.keys(selector).indexOf('error') > -1\n ? 'error'\n : 'warning';\n var message = selector[severity];\n issues.push(new validationIssue({\n type: 'maprules',\n severity: severity,\n message: function() {\n return message;\n },\n entityIds: [entity.id]\n }));\n }\n }\n };\n this._validationRules.push(rule);\n },\n\n clearRules: function() { this._validationRules = []; },\n\n // returns validationRules...\n validationRules: function() { return this._validationRules; },\n\n // returns ruleChecks\n ruleChecks: function() { return this._ruleChecks; }\n};\n", "import { json as d3_json } from 'd3-fetch';\n\nimport RBush from 'rbush';\nimport { geoExtent } from '../geo';\nimport { utilQsString } from '../util';\nimport { localizer } from '../core';\n\nimport { nominatimApiUrl } from '../../config/id.js';\n\n\nvar apibase = nominatimApiUrl;\nvar _inflight = {};\nvar _nominatimCache;\n\n\nexport default {\n\n init: function() {\n _inflight = {};\n _nominatimCache = new RBush();\n },\n\n reset: function() {\n Object.values(_inflight).forEach(function(controller) { controller.abort(); });\n _inflight = {};\n _nominatimCache = new RBush();\n },\n\n\n countryCode: function (location, callback) {\n this.reverse(location, function(err, result) {\n if (err) {\n return callback(err);\n } else if (result.address) {\n return callback(null, result.address.country_code);\n } else {\n return callback('Unable to geocode', null);\n }\n });\n },\n\n\n reverse: function (loc, callback) {\n var cached = _nominatimCache.search(\n { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1] }\n );\n\n if (cached.length > 0) {\n if (callback) callback(null, cached[0].data);\n return;\n }\n\n var params = { zoom: 13, format: 'json', addressdetails: 1, lat: loc[1], lon: loc[0] };\n var url = apibase + 'reverse?' + utilQsString(params);\n\n if (_inflight[url]) return;\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, {\n signal: controller.signal,\n headers: {\n 'Accept-Language': localizer.localeCodes().join(',')\n }\n })\n .then(function(result) {\n delete _inflight[url];\n if (result && result.error) {\n throw new Error(result.error);\n }\n var extent = geoExtent(loc).padByMeters(200);\n _nominatimCache.insert(Object.assign(extent.bbox(), {data: result}));\n if (callback) callback(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (callback) callback(err.message);\n });\n },\n\n\n search: function (val, callback) {\n const params = {\n q: val,\n limit:10,\n format: 'json'\n };\n var url = apibase + 'search?' + utilQsString(params);\n\n if (_inflight[url]) return;\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, {\n signal: controller.signal,\n headers: {\n 'Accept-Language': localizer.localeCodes().join(',')\n }\n })\n .then(function(result) {\n delete _inflight[url];\n if (result && result.error) {\n throw new Error(result.error);\n }\n if (callback) callback(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (callback) callback(err.message);\n });\n }\n\n};\n", "(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n\ttypeof define === 'function' && define.amd ? define(factory) :\n\t(global.quickselect = factory());\n}(this, (function () { 'use strict';\n\nfunction quickselect(arr, k, left, right, compare) {\n quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);\n}\n\nfunction quickselectStep(arr, k, left, right, compare) {\n\n while (right > left) {\n if (right - left > 600) {\n var n = right - left + 1;\n var m = k - left + 1;\n var z = Math.log(n);\n var s = 0.5 * Math.exp(2 * z / 3);\n var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);\n var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));\n var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));\n quickselectStep(arr, k, newLeft, newRight, compare);\n }\n\n var t = arr[k];\n var i = left;\n var j = right;\n\n swap(arr, left, k);\n if (compare(arr[right], t) > 0) swap(arr, left, right);\n\n while (i < j) {\n swap(arr, i, j);\n i++;\n j--;\n while (compare(arr[i], t) < 0) i++;\n while (compare(arr[j], t) > 0) j--;\n }\n\n if (compare(arr[left], t) === 0) swap(arr, left, j);\n else {\n j++;\n swap(arr, j, right);\n }\n\n if (j <= k) left = j + 1;\n if (k <= j) right = j - 1;\n }\n}\n\nfunction swap(arr, i, j) {\n var tmp = arr[i];\n arr[i] = arr[j];\n arr[j] = tmp;\n}\n\nfunction defaultCompare(a, b) {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\nreturn quickselect;\n\n})));\n", "'use strict';\n\nmodule.exports = rbush;\nmodule.exports.default = rbush;\n\nvar quickselect = require('quickselect');\n\nfunction rbush(maxEntries, format) {\n if (!(this instanceof rbush)) return new rbush(maxEntries, format);\n\n // max entries in a node is 9 by default; min node fill is 40% for best performance\n this._maxEntries = Math.max(4, maxEntries || 9);\n this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));\n\n if (format) {\n this._initFormat(format);\n }\n\n this.clear();\n}\n\nrbush.prototype = {\n\n all: function () {\n return this._all(this.data, []);\n },\n\n search: function (bbox) {\n\n var node = this.data,\n result = [],\n toBBox = this.toBBox;\n\n if (!intersects(bbox, node)) return result;\n\n var nodesToSearch = [],\n i, len, child, childBBox;\n\n while (node) {\n for (i = 0, len = node.children.length; i < len; i++) {\n\n child = node.children[i];\n childBBox = node.leaf ? toBBox(child) : child;\n\n if (intersects(bbox, childBBox)) {\n if (node.leaf) result.push(child);\n else if (contains(bbox, childBBox)) this._all(child, result);\n else nodesToSearch.push(child);\n }\n }\n node = nodesToSearch.pop();\n }\n\n return result;\n },\n\n collides: function (bbox) {\n\n var node = this.data,\n toBBox = this.toBBox;\n\n if (!intersects(bbox, node)) return false;\n\n var nodesToSearch = [],\n i, len, child, childBBox;\n\n while (node) {\n for (i = 0, len = node.children.length; i < len; i++) {\n\n child = node.children[i];\n childBBox = node.leaf ? toBBox(child) : child;\n\n if (intersects(bbox, childBBox)) {\n if (node.leaf || contains(bbox, childBBox)) return true;\n nodesToSearch.push(child);\n }\n }\n node = nodesToSearch.pop();\n }\n\n return false;\n },\n\n load: function (data) {\n if (!(data && data.length)) return this;\n\n if (data.length < this._minEntries) {\n for (var i = 0, len = data.length; i < len; i++) {\n this.insert(data[i]);\n }\n return this;\n }\n\n // recursively build the tree with the given data from scratch using OMT algorithm\n var node = this._build(data.slice(), 0, data.length - 1, 0);\n\n if (!this.data.children.length) {\n // save as is if tree is empty\n this.data = node;\n\n } else if (this.data.height === node.height) {\n // split root if trees have the same height\n this._splitRoot(this.data, node);\n\n } else {\n if (this.data.height < node.height) {\n // swap trees if inserted one is bigger\n var tmpNode = this.data;\n this.data = node;\n node = tmpNode;\n }\n\n // insert the small tree into the large tree at appropriate level\n this._insert(node, this.data.height - node.height - 1, true);\n }\n\n return this;\n },\n\n insert: function (item) {\n if (item) this._insert(item, this.data.height - 1);\n return this;\n },\n\n clear: function () {\n this.data = createNode([]);\n return this;\n },\n\n remove: function (item, equalsFn) {\n if (!item) return this;\n\n var node = this.data,\n bbox = this.toBBox(item),\n path = [],\n indexes = [],\n i, parent, index, goingUp;\n\n // depth-first iterative tree traversal\n while (node || path.length) {\n\n if (!node) { // go up\n node = path.pop();\n parent = path[path.length - 1];\n i = indexes.pop();\n goingUp = true;\n }\n\n if (node.leaf) { // check current node\n index = findItem(item, node.children, equalsFn);\n\n if (index !== -1) {\n // item found, remove the item and condense tree upwards\n node.children.splice(index, 1);\n path.push(node);\n this._condense(path);\n return this;\n }\n }\n\n if (!goingUp && !node.leaf && contains(node, bbox)) { // go down\n path.push(node);\n indexes.push(i);\n i = 0;\n parent = node;\n node = node.children[0];\n\n } else if (parent) { // go right\n i++;\n node = parent.children[i];\n goingUp = false;\n\n } else node = null; // nothing found\n }\n\n return this;\n },\n\n toBBox: function (item) { return item; },\n\n compareMinX: compareNodeMinX,\n compareMinY: compareNodeMinY,\n\n toJSON: function () { return this.data; },\n\n fromJSON: function (data) {\n this.data = data;\n return this;\n },\n\n _all: function (node, result) {\n var nodesToSearch = [];\n while (node) {\n if (node.leaf) result.push.apply(result, node.children);\n else nodesToSearch.push.apply(nodesToSearch, node.children);\n\n node = nodesToSearch.pop();\n }\n return result;\n },\n\n _build: function (items, left, right, height) {\n\n var N = right - left + 1,\n M = this._maxEntries,\n node;\n\n if (N <= M) {\n // reached leaf level; return leaf\n node = createNode(items.slice(left, right + 1));\n calcBBox(node, this.toBBox);\n return node;\n }\n\n if (!height) {\n // target height of the bulk-loaded tree\n height = Math.ceil(Math.log(N) / Math.log(M));\n\n // target number of root entries to maximize storage utilization\n M = Math.ceil(N / Math.pow(M, height - 1));\n }\n\n node = createNode([]);\n node.leaf = false;\n node.height = height;\n\n // split the items into M mostly square tiles\n\n var N2 = Math.ceil(N / M),\n N1 = N2 * Math.ceil(Math.sqrt(M)),\n i, j, right2, right3;\n\n multiSelect(items, left, right, N1, this.compareMinX);\n\n for (i = left; i <= right; i += N1) {\n\n right2 = Math.min(i + N1 - 1, right);\n\n multiSelect(items, i, right2, N2, this.compareMinY);\n\n for (j = i; j <= right2; j += N2) {\n\n right3 = Math.min(j + N2 - 1, right2);\n\n // pack each entry recursively\n node.children.push(this._build(items, j, right3, height - 1));\n }\n }\n\n calcBBox(node, this.toBBox);\n\n return node;\n },\n\n _chooseSubtree: function (bbox, node, level, path) {\n\n var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;\n\n while (true) {\n path.push(node);\n\n if (node.leaf || path.length - 1 === level) break;\n\n minArea = minEnlargement = Infinity;\n\n for (i = 0, len = node.children.length; i < len; i++) {\n child = node.children[i];\n area = bboxArea(child);\n enlargement = enlargedArea(bbox, child) - area;\n\n // choose entry with the least area enlargement\n if (enlargement < minEnlargement) {\n minEnlargement = enlargement;\n minArea = area < minArea ? area : minArea;\n targetNode = child;\n\n } else if (enlargement === minEnlargement) {\n // otherwise choose one with the smallest area\n if (area < minArea) {\n minArea = area;\n targetNode = child;\n }\n }\n }\n\n node = targetNode || node.children[0];\n }\n\n return node;\n },\n\n _insert: function (item, level, isNode) {\n\n var toBBox = this.toBBox,\n bbox = isNode ? item : toBBox(item),\n insertPath = [];\n\n // find the best node for accommodating the item, saving all nodes along the path too\n var node = this._chooseSubtree(bbox, this.data, level, insertPath);\n\n // put the item into the node\n node.children.push(item);\n extend(node, bbox);\n\n // split on node overflow; propagate upwards if necessary\n while (level >= 0) {\n if (insertPath[level].children.length > this._maxEntries) {\n this._split(insertPath, level);\n level--;\n } else break;\n }\n\n // adjust bboxes along the insertion path\n this._adjustParentBBoxes(bbox, insertPath, level);\n },\n\n // split overflowed node into two\n _split: function (insertPath, level) {\n\n var node = insertPath[level],\n M = node.children.length,\n m = this._minEntries;\n\n this._chooseSplitAxis(node, m, M);\n\n var splitIndex = this._chooseSplitIndex(node, m, M);\n\n var newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));\n newNode.height = node.height;\n newNode.leaf = node.leaf;\n\n calcBBox(node, this.toBBox);\n calcBBox(newNode, this.toBBox);\n\n if (level) insertPath[level - 1].children.push(newNode);\n else this._splitRoot(node, newNode);\n },\n\n _splitRoot: function (node, newNode) {\n // split root node\n this.data = createNode([node, newNode]);\n this.data.height = node.height + 1;\n this.data.leaf = false;\n calcBBox(this.data, this.toBBox);\n },\n\n _chooseSplitIndex: function (node, m, M) {\n\n var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index;\n\n minOverlap = minArea = Infinity;\n\n for (i = m; i <= M - m; i++) {\n bbox1 = distBBox(node, 0, i, this.toBBox);\n bbox2 = distBBox(node, i, M, this.toBBox);\n\n overlap = intersectionArea(bbox1, bbox2);\n area = bboxArea(bbox1) + bboxArea(bbox2);\n\n // choose distribution with minimum overlap\n if (overlap < minOverlap) {\n minOverlap = overlap;\n index = i;\n\n minArea = area < minArea ? area : minArea;\n\n } else if (overlap === minOverlap) {\n // otherwise choose distribution with minimum area\n if (area < minArea) {\n minArea = area;\n index = i;\n }\n }\n }\n\n return index;\n },\n\n // sorts node children by the best axis for split\n _chooseSplitAxis: function (node, m, M) {\n\n var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX,\n compareMinY = node.leaf ? this.compareMinY : compareNodeMinY,\n xMargin = this._allDistMargin(node, m, M, compareMinX),\n yMargin = this._allDistMargin(node, m, M, compareMinY);\n\n // if total distributions margin value is minimal for x, sort by minX,\n // otherwise it's already sorted by minY\n if (xMargin < yMargin) node.children.sort(compareMinX);\n },\n\n // total margin of all possible split distributions where each node is at least m full\n _allDistMargin: function (node, m, M, compare) {\n\n node.children.sort(compare);\n\n var toBBox = this.toBBox,\n leftBBox = distBBox(node, 0, m, toBBox),\n rightBBox = distBBox(node, M - m, M, toBBox),\n margin = bboxMargin(leftBBox) + bboxMargin(rightBBox),\n i, child;\n\n for (i = m; i < M - m; i++) {\n child = node.children[i];\n extend(leftBBox, node.leaf ? toBBox(child) : child);\n margin += bboxMargin(leftBBox);\n }\n\n for (i = M - m - 1; i >= m; i--) {\n child = node.children[i];\n extend(rightBBox, node.leaf ? toBBox(child) : child);\n margin += bboxMargin(rightBBox);\n }\n\n return margin;\n },\n\n _adjustParentBBoxes: function (bbox, path, level) {\n // adjust bboxes along the given tree path\n for (var i = level; i >= 0; i--) {\n extend(path[i], bbox);\n }\n },\n\n _condense: function (path) {\n // go through the path, removing empty nodes and updating bboxes\n for (var i = path.length - 1, siblings; i >= 0; i--) {\n if (path[i].children.length === 0) {\n if (i > 0) {\n siblings = path[i - 1].children;\n siblings.splice(siblings.indexOf(path[i]), 1);\n\n } else this.clear();\n\n } else calcBBox(path[i], this.toBBox);\n }\n },\n\n _initFormat: function (format) {\n // data format (minX, minY, maxX, maxY accessors)\n\n // uses eval-type function compilation instead of just accepting a toBBox function\n // because the algorithms are very sensitive to sorting functions performance,\n // so they should be dead simple and without inner calls\n\n var compareArr = ['return a', ' - b', ';'];\n\n this.compareMinX = new Function('a', 'b', compareArr.join(format[0]));\n this.compareMinY = new Function('a', 'b', compareArr.join(format[1]));\n\n this.toBBox = new Function('a',\n 'return {minX: a' + format[0] +\n ', minY: a' + format[1] +\n ', maxX: a' + format[2] +\n ', maxY: a' + format[3] + '};');\n }\n};\n\nfunction findItem(item, items, equalsFn) {\n if (!equalsFn) return items.indexOf(item);\n\n for (var i = 0; i < items.length; i++) {\n if (equalsFn(item, items[i])) return i;\n }\n return -1;\n}\n\n// calculate node's bbox from bboxes of its children\nfunction calcBBox(node, toBBox) {\n distBBox(node, 0, node.children.length, toBBox, node);\n}\n\n// min bounding rectangle of node children from k to p-1\nfunction distBBox(node, k, p, toBBox, destNode) {\n if (!destNode) destNode = createNode(null);\n destNode.minX = Infinity;\n destNode.minY = Infinity;\n destNode.maxX = -Infinity;\n destNode.maxY = -Infinity;\n\n for (var i = k, child; i < p; i++) {\n child = node.children[i];\n extend(destNode, node.leaf ? toBBox(child) : child);\n }\n\n return destNode;\n}\n\nfunction extend(a, b) {\n a.minX = Math.min(a.minX, b.minX);\n a.minY = Math.min(a.minY, b.minY);\n a.maxX = Math.max(a.maxX, b.maxX);\n a.maxY = Math.max(a.maxY, b.maxY);\n return a;\n}\n\nfunction compareNodeMinX(a, b) { return a.minX - b.minX; }\nfunction compareNodeMinY(a, b) { return a.minY - b.minY; }\n\nfunction bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }\nfunction bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }\n\nfunction enlargedArea(a, b) {\n return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *\n (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));\n}\n\nfunction intersectionArea(a, b) {\n var minX = Math.max(a.minX, b.minX),\n minY = Math.max(a.minY, b.minY),\n maxX = Math.min(a.maxX, b.maxX),\n maxY = Math.min(a.maxY, b.maxY);\n\n return Math.max(0, maxX - minX) *\n Math.max(0, maxY - minY);\n}\n\nfunction contains(a, b) {\n return a.minX <= b.minX &&\n a.minY <= b.minY &&\n b.maxX <= a.maxX &&\n b.maxY <= a.maxY;\n}\n\nfunction intersects(a, b) {\n return b.minX <= a.maxX &&\n b.minY <= a.maxY &&\n b.maxX >= a.minX &&\n b.maxY >= a.minY;\n}\n\nfunction createNode(children) {\n return {\n children: children,\n height: 1,\n leaf: true,\n minX: Infinity,\n minY: Infinity,\n maxX: -Infinity,\n maxY: -Infinity\n };\n}\n\n// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;\n// combines selection algorithm with binary divide & conquer approach\n\nfunction multiSelect(arr, left, right, n, compare) {\n var stack = [left, right],\n mid;\n\n while (stack.length) {\n right = stack.pop();\n left = stack.pop();\n\n if (right - left <= n) continue;\n\n mid = left + Math.ceil((right - left) / n / 2) * n;\n quickselect(arr, mid, left, right, compare);\n\n stack.push(left, mid, mid, right);\n }\n}\n", "'use strict';\n\nmodule.exports = lineclip;\n\nlineclip.polyline = lineclip;\nlineclip.polygon = polygonclip;\n\n\n// Cohen-Sutherland line clippign algorithm, adapted to efficiently\n// handle polylines rather than just segments\n\nfunction lineclip(points, bbox, result) {\n\n var len = points.length,\n codeA = bitCode(points[0], bbox),\n part = [],\n i, a, b, codeB, lastCode;\n\n if (!result) result = [];\n\n for (i = 1; i < len; i++) {\n a = points[i - 1];\n b = points[i];\n codeB = lastCode = bitCode(b, bbox);\n\n while (true) {\n\n if (!(codeA | codeB)) { // accept\n part.push(a);\n\n if (codeB !== lastCode) { // segment went outside\n part.push(b);\n\n if (i < len - 1) { // start a new line\n result.push(part);\n part = [];\n }\n } else if (i === len - 1) {\n part.push(b);\n }\n break;\n\n } else if (codeA & codeB) { // trivial reject\n break;\n\n } else if (codeA) { // a outside, intersect with clip edge\n a = intersect(a, b, codeA, bbox);\n codeA = bitCode(a, bbox);\n\n } else { // b outside\n b = intersect(a, b, codeB, bbox);\n codeB = bitCode(b, bbox);\n }\n }\n\n codeA = lastCode;\n }\n\n if (part.length) result.push(part);\n\n return result;\n}\n\n// Sutherland-Hodgeman polygon clipping algorithm\n\nfunction polygonclip(points, bbox) {\n\n var result, edge, prev, prevInside, i, p, inside;\n\n // clip against each side of the clip rectangle\n for (edge = 1; edge <= 8; edge *= 2) {\n result = [];\n prev = points[points.length - 1];\n prevInside = !(bitCode(prev, bbox) & edge);\n\n for (i = 0; i < points.length; i++) {\n p = points[i];\n inside = !(bitCode(p, bbox) & edge);\n\n // if segment goes through the clip window, add an intersection\n if (inside !== prevInside) result.push(intersect(prev, p, edge, bbox));\n\n if (inside) result.push(p); // add a point if it's inside\n\n prev = p;\n prevInside = inside;\n }\n\n points = result;\n\n if (!points.length) break;\n }\n\n return result;\n}\n\n// intersect a segment against one of the 4 lines that make up the bbox\n\nfunction intersect(a, b, edge, bbox) {\n return edge & 8 ? [a[0] + (b[0] - a[0]) * (bbox[3] - a[1]) / (b[1] - a[1]), bbox[3]] : // top\n edge & 4 ? [a[0] + (b[0] - a[0]) * (bbox[1] - a[1]) / (b[1] - a[1]), bbox[1]] : // bottom\n edge & 2 ? [bbox[2], a[1] + (b[1] - a[1]) * (bbox[2] - a[0]) / (b[0] - a[0])] : // right\n edge & 1 ? [bbox[0], a[1] + (b[1] - a[1]) * (bbox[0] - a[0]) / (b[0] - a[0])] : // left\n null;\n}\n\n// bit code reflects the point position relative to the bbox:\n\n// left mid right\n// top 1001 1000 1010\n// mid 0001 0000 0010\n// bottom 0101 0100 0110\n\nfunction bitCode(p, bbox) {\n var code = 0;\n\n if (p[0] < bbox[0]) code |= 1; // left\n else if (p[0] > bbox[2]) code |= 2; // right\n\n if (p[1] < bbox[1]) code |= 4; // bottom\n else if (p[1] > bbox[3]) code |= 8; // top\n\n return code;\n}\n", "'use strict';\n\nvar rbush = require('rbush');\nvar lineclip = require('lineclip');\n\nmodule.exports = whichPolygon;\n\nfunction whichPolygon(data) {\n var bboxes = [];\n for (var i = 0; i < data.features.length; i++) {\n var feature = data.features[i];\n\n // unlocated GeoJSON features can have null `geometry`\n if (!feature.geometry) continue;\n\n var coords = feature.geometry.coordinates;\n\n if (feature.geometry.type === 'Polygon') {\n bboxes.push(treeItem(coords, feature.properties));\n\n } else if (feature.geometry.type === 'MultiPolygon') {\n for (var j = 0; j < coords.length; j++) {\n bboxes.push(treeItem(coords[j], feature.properties));\n }\n }\n }\n\n var tree = rbush().load(bboxes);\n\n function query(p, multi) {\n var output = [],\n result = tree.search({\n minX: p[0],\n minY: p[1],\n maxX: p[0],\n maxY: p[1]\n });\n for (var i = 0; i < result.length; i++) {\n if (insidePolygon(result[i].coords, p)) {\n if (multi)\n output.push(result[i].props);\n else\n return result[i].props;\n }\n }\n return multi && output.length ? output : null;\n }\n\n query.tree = tree;\n query.bbox = function queryBBox(bbox) {\n var output = [];\n var result = tree.search({\n minX: bbox[0],\n minY: bbox[1],\n maxX: bbox[2],\n maxY: bbox[3]\n });\n for (var i = 0; i < result.length; i++) {\n if (polygonIntersectsBBox(result[i].coords, bbox)) {\n output.push(result[i].props);\n }\n }\n return output;\n };\n\n return query;\n}\n\nfunction polygonIntersectsBBox(polygon, bbox) {\n var bboxCenter = [\n (bbox[0] + bbox[2]) / 2,\n (bbox[1] + bbox[3]) / 2\n ];\n if (insidePolygon(polygon, bboxCenter)) return true;\n for (var i = 0; i < polygon.length; i++) {\n if (lineclip(polygon[i], bbox).length > 0) return true;\n }\n return false;\n}\n\n// ray casting algorithm for detecting if point is in polygon\nfunction insidePolygon(rings, p) {\n var inside = false;\n for (var i = 0, len = rings.length; i < len; i++) {\n var ring = rings[i];\n for (var j = 0, len2 = ring.length, k = len2 - 1; j < len2; k = j++) {\n if (rayIntersect(p, ring[j], ring[k])) inside = !inside;\n }\n }\n return inside;\n}\n\nfunction rayIntersect(p, p1, p2) {\n return ((p1[1] > p[1]) !== (p2[1] > p[1])) && (p[0] < (p2[0] - p1[0]) * (p[1] - p1[1]) / (p2[1] - p1[1]) + p1[0]);\n}\n\nfunction treeItem(coords, props) {\n var item = {\n minX: Infinity,\n minY: Infinity,\n maxX: -Infinity,\n maxY: -Infinity,\n coords: coords,\n props: props\n };\n\n for (var i = 0; i < coords[0].length; i++) {\n var p = coords[0][i];\n item.minX = Math.min(item.minX, p[0]);\n item.minY = Math.min(item.minY, p[1]);\n item.maxX = Math.max(item.maxX, p[0]);\n item.maxY = Math.max(item.maxY, p[1]);\n }\n return item;\n}\n", "/* eslint @typescript-eslint/no-this-alias: \"warn\" */\nimport whichPolygon from 'which-polygon';\n\nimport { simplify } from './simplify.ts';\n\n// JSON\nimport matchGroupsJSON from '../config/matchGroups.json' with {type: 'json'};\nimport genericWordsJSON from '../config/genericWords.json' with {type: 'json'};\nimport treesJSON from '../config/trees.json' with {type: 'json'};\n\nconst matchGroups = matchGroupsJSON.matchGroups;\nconst trees = treesJSON.trees;\n\n\ntype Vec2 = [number, number];\ntype Vec3 = [number, number, number];\ntype Location = Vec2 | Vec3 | string | number;\n\ninterface LocationSet {\n include?: Array,\n exclude?: Array\n};\n\ninterface LocationConflation {\n validateLocation: (a: Location) => unknown;\n resolveLocation: (a: Location) => unknown;\n validateLocationSet: (a: LocationSet) => unknown;\n resolveLocationSet: (a: LocationSet) => unknown;\n};\n\ntype HitType = 'primary' | 'alternate' | 'excludeGeneric' | 'excludeNamed';\ninterface Hit {\n match: HitType;\n itemID?: string;\n area?: number;\n kv?: string;\n nsimple?: string;\n pattern?: string;\n};\n\n\nexport class Matcher {\n private matchIndex;\n private genericWords = new Map();\n private itemLocation;\n private locationSets;\n private locationIndex;\n private warnings: Array = [];\n\n\n // `constructor`\n // initialize the genericWords regexes\n constructor() {\n // The `matchIndex` is a specialized structure that allows us to quickly answer\n // _\"Given a [key/value tagpair, name, location], what canonical items (brands etc) can match it?\"_\n //\n // The index contains all valid combinations of k/v tagpairs and names\n // matchIndex:\n // {\n // 'k/v': {\n // 'primary': Map (String 'nsimple' -> Set (itemIDs…), // matches for tags like `name`, `name:xx`, etc.\n // 'alternate': Map (String 'nsimple' -> Set (itemIDs…), // matches for tags like `alt_name`, `brand`, etc.\n // 'excludeNamed': Map (String 'pattern' -> RegExp),\n // 'excludeGeneric': Map (String 'pattern' -> RegExp)\n // },\n // }\n //\n // {\n // 'amenity/bank': {\n // 'primary': {\n // 'firstbank': Set (\"firstbank-978cca\", \"firstbank-9794e6\", \"firstbank-f17495\", …),\n // …\n // },\n // 'alternate': {\n // '1stbank': Set (\"firstbank-f17495\"),\n // …\n // }\n // },\n // 'shop/supermarket': {\n // 'primary': {\n // 'coop': Set (\"coop-76454b\", \"coop-ebf2d9\", \"coop-36e991\", …),\n // 'coopfood': Set (\"coopfood-a8278b\", …),\n // …\n // },\n // 'alternate': {\n // 'coop': Set (\"coopfood-a8278b\", …),\n // 'federatedcooperatives': Set (\"coop-76454b\", …),\n // 'thecooperative': Set (\"coopfood-a8278b\", …),\n // …\n // }\n // }\n // }\n //\n this.matchIndex = undefined;\n\n // The `genericWords` structure matches the contents of genericWords.json to instantiated RegExp objects\n // Map (String 'pattern' -> RegExp),\n this.genericWords = new Map();\n (genericWordsJSON.genericWords || []).forEach(s => this.genericWords.set(s, new RegExp(s, 'i')));\n\n // The `itemLocation` structure maps itemIDs to locationSetIDs:\n // {\n // 'firstbank-f17495': '+[first_bank_western_us.geojson]',\n // 'firstbank-978cca': '+[first_bank_carolinas.geojson]',\n // 'coop-76454b': '+[Q16]',\n // 'coopfood-a8278b': '+[Q23666]',\n // …\n // }\n this.itemLocation = undefined;\n\n // The `locationSets` structure maps locationSetIDs to *resolved* locationSets:\n // {\n // '+[first_bank_western_us.geojson]': GeoJSON {…},\n // '+[first_bank_carolinas.geojson]': GeoJSON {…},\n // '+[Q16]': GeoJSON {…},\n // '+[Q23666]': GeoJSON {…},\n // …\n // }\n this.locationSets = undefined;\n\n // The `locationIndex` is an instance of which-polygon spatial index for the locationSets.\n this.locationIndex = undefined;\n\n // Array of match conflict pairs (currently unused)\n this.warnings = [];\n }\n\n\n //\n // `buildMatchIndex()`\n // Call this to prepare the matcher for use\n //\n // `data` needs to be an Object indexed on a 'tree/key/value' path.\n // (e.g. cache filled by `fileTree.read` or data found in `dist/nsi.json`)\n // {\n // 'brands/amenity/bank': { properties: {}, items: [ {}, {}, … ] },\n // 'brands/amenity/bar': { properties: {}, items: [ {}, {}, … ] },\n // …\n // }\n //\n buildMatchIndex(data: Record): void {\n const that = this;\n if (that.matchIndex) return; // it was built already\n that.matchIndex = new Map();\n\n const seenTree = new Map(); // warn if the same [k, v, nsimple] appears in multiple trees - #5625\n\n Object.keys(data).forEach(tkv => {\n const category = data[tkv];\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const t = parts[0];\n const k = parts[1];\n const v = parts[2];\n const thiskv = `${k}/${v}`;\n const tree = trees[t];\n\n let branch = that.matchIndex.get(thiskv);\n if (!branch) {\n branch = {\n primary: new Map(),\n alternate: new Map(),\n excludeGeneric: new Map(),\n excludeNamed: new Map()\n };\n that.matchIndex.set(thiskv, branch);\n }\n\n // ADD EXCLUSIONS\n const properties = category.properties || {};\n const exclude = properties.exclude || {};\n (exclude.generic || []).forEach(s => branch.excludeGeneric.set(s, new RegExp(s, 'i')));\n (exclude.named || []).forEach(s => branch.excludeNamed.set(s, new RegExp(s, 'i')));\n const excludeRegexes = [...branch.excludeGeneric.values(), ...branch.excludeNamed.values()];\n\n\n // ADD ITEMS\n const items = category.items;\n if (!Array.isArray(items) || !items.length) return;\n\n\n // Primary name patterns, match tags to take first\n // e.g. `name`, `name:ru`\n const primaryName = new RegExp(tree.nameTags.primary, 'i');\n\n // Alternate name patterns, match tags to consider after primary\n // e.g. `alt_name`, `short_name`, `brand`, `brand:ru`, etc..\n const alternateName = new RegExp(tree.nameTags.alternate, 'i');\n\n // There are a few exceptions to the name matching regexes.\n // Usually a tag suffix contains a language code like `name:en`, `name:ru`\n // but we want to exclude things like `operator:type`, `name:etymology`, etc..\n const notName = /:(colou?r|type|forward|backward|left|right|etymology|pronunciation|signed|wikipedia)$/i;\n\n // For certain categories we do not want to match generic KV pairs like `building/yes` or `amenity/yes`\n const skipGenericKV = skipGenericKVMatches(t, k, v);\n\n // We will collect the generic KV pairs anyway (for the purpose of filtering them out of matchTags)\n const genericKV = new Set([`${k}/yes`, `building/yes`]);\n\n // Collect alternate tagpairs for this kv category from matchGroups.\n // We might also pick up a few more generic KVs (like `shop/yes`)\n const matchGroupKV = new Set();\n Object.values(matchGroups).forEach(matchGroup => {\n const inGroup = matchGroup.some(otherkv => otherkv === thiskv);\n if (!inGroup) return;\n\n matchGroup.forEach(otherkv => {\n if (otherkv === thiskv) return; // skip self\n matchGroupKV.add(otherkv);\n\n const otherk = otherkv.split('/', 2)[0]; // we might pick up a `shop/yes`\n genericKV.add(`${otherk}/yes`);\n });\n });\n\n // For each item, insert all [key, value, name] combinations into the match index\n items.forEach(item => {\n if (!item.id) return;\n\n // Automatically remove redundant `matchTags` - #3417, #8137\n // (i.e. This kv is already covered by matchGroups, so it doesn't need to be in `item.matchTags`\n // or this kv is the primary kv, so it doesn't need to be duplicated in `item.matchTags`)\n if (Array.isArray(item.matchTags) && item.matchTags.length) {\n item.matchTags = item.matchTags\n .filter(matchTag => !matchGroupKV.has(matchTag) && (matchTag !== thiskv) && !genericKV.has(matchTag));\n\n if (!item.matchTags.length) delete item.matchTags;\n }\n\n // key/value tagpairs to insert into the match index..\n let kvTags = [`${thiskv}`]\n .concat(item.matchTags || []);\n\n if (!skipGenericKV) {\n kvTags = kvTags\n .concat(Array.from(genericKV)); // #3454 - match some generic tags\n }\n\n // Index all the namelike tag values\n Object.keys(item.tags).forEach(osmkey => {\n if (notName.test(osmkey)) return; // osmkey is not a namelike tag, skip\n const osmvalue = item.tags[osmkey];\n if (!osmvalue || excludeRegexes.some(regex => regex.test(osmvalue))) return; // osmvalue missing or excluded\n\n if (primaryName.test(osmkey)) {\n kvTags.forEach(kv => insertName('primary', t, kv, simplify(osmvalue), item.id));\n } else if (alternateName.test(osmkey)) {\n kvTags.forEach(kv => insertName('alternate', t, kv, simplify(osmvalue), item.id));\n }\n });\n\n // Index `matchNames` after indexing all other names..\n const keepMatchNames = new Set();\n (item.matchNames || []).forEach(matchName => {\n // If this matchname isn't already indexed, add it to the alternate index\n const nsimple = simplify(matchName);\n kvTags.forEach(kv => {\n const branch = that.matchIndex.get(kv);\n const primaryLeaf = branch && branch.primary.get(nsimple);\n const alternateLeaf = branch && branch.alternate.get(nsimple);\n const inPrimary = primaryLeaf && primaryLeaf.has(item.id);\n const inAlternate = alternateLeaf && alternateLeaf.has(item.id);\n\n if (!inPrimary && !inAlternate) {\n insertName('alternate', t, kv, nsimple, item.id);\n keepMatchNames.add(matchName);\n }\n });\n });\n\n // Automatically remove redundant `matchNames` - #3417\n // (i.e. This name got indexed some other way, so it doesn't need to be in `item.matchNames`)\n if (keepMatchNames.size) {\n item.matchNames = Array.from(keepMatchNames);\n } else {\n delete item.matchNames;\n }\n\n }); // each item\n }); // each tkv\n\n\n // Insert this item into the matchIndex\n function insertName(which: string, t: string, kv: string, nsimple: string, itemID: string) {\n if (!nsimple) {\n that.warnings.push(`Warning: skipping empty ${which} name for item ${t}/${kv}: ${itemID}`);\n return;\n }\n\n let branch = that.matchIndex.get(kv);\n if (!branch) {\n branch = {\n primary: new Map(),\n alternate: new Map(),\n excludeGeneric: new Map(),\n excludeNamed: new Map()\n };\n that.matchIndex.set(kv, branch);\n }\n\n let leaf = branch[which].get(nsimple);\n if (!leaf) {\n leaf = new Set();\n branch[which].set(nsimple, leaf);\n }\n\n leaf.add(itemID); // insert\n\n // check for duplicates - #5625\n if (!/yes$/.test(kv)) { // ignore genericKV like amenity/yes, building/yes, etc\n const kvnsimple = `${kv}/${nsimple}`;\n const existing = seenTree.get(kvnsimple);\n if (existing && existing !== t) {\n const items = Array.from(leaf);\n that.warnings.push(`Duplicate cache key \"${kvnsimple}\" in trees \"${t}\" and \"${existing}\", check items: ${items}`);\n return;\n }\n seenTree.set(kvnsimple, t);\n }\n }\n\n // For certain categories we do not want to match generic KV pairs like `building/yes` or `amenity/yes`\n function skipGenericKVMatches(t: string, k: string, v: string): boolean {\n return (\n t === 'flags' ||\n t === 'transit' ||\n k === 'landuse' ||\n v === 'atm' ||\n v === 'bicycle_parking' ||\n v === 'car_sharing' ||\n v === 'caravan_site' ||\n v === 'charging_station' ||\n v === 'dog_park' ||\n v === 'parking' ||\n v === 'phone' ||\n v === 'playground' ||\n v === 'post_box' ||\n v === 'public_bookcase' ||\n v === 'recycling' ||\n v === 'vending_machine'\n );\n }\n }\n\n\n //\n // `buildLocationIndex()`\n // Call this to prepare a which-polygon location index.\n // This *resolves* all the locationSets into GeoJSON, which takes some time.\n // You can skip this step if you don't care about matching within a location.\n //\n // `data` needs to be an Object indexed on a 'tree/key/value' path.\n // (e.g. cache filled by `fileTree.read` or data found in `dist/nsi.json`)\n // {\n // 'brands/amenity/bank': { properties: {}, items: [ {}, {}, … ] },\n // 'brands/amenity/bar': { properties: {}, items: [ {}, {}, … ] },\n // …\n // }\n //\n buildLocationIndex(data: Record, loco: LocationConflation): void {\n const that = this;\n if (that.locationIndex) return; // it was built already\n\n that.itemLocation = new Map();\n that.locationSets = new Map();\n\n Object.keys(data).forEach(tkv => {\n const items = data[tkv].items;\n if (!Array.isArray(items) || !items.length) return;\n\n items.forEach(item => {\n if (that.itemLocation.has(item.id)) return; // we've seen item id already - shouldn't be possible?\n\n let resolved;\n try {\n resolved = loco.resolveLocationSet(item.locationSet); // resolve a feature for this locationSet\n } catch (err: unknown) {\n const message = (err instanceof Error) ? err.message : err;\n console.warn(`buildLocationIndex: ${message}`); // couldn't resolve\n }\n if (!resolved || !resolved.id) return;\n\n that.itemLocation.set(item.id, resolved.id); // link it to the item\n if (that.locationSets.has(resolved.id)) return; // we've seen this locationSet feature before..\n\n // First time seeing this locationSet feature, make a copy and add to locationSet cache..\n const feature = _cloneDeep(resolved.feature);\n feature.id = resolved.id; // Important: always use the locationSet `id` (`+[Q30]`), not the feature `id` (`Q30`)\n feature.properties.id = resolved.id;\n\n if (!feature.geometry.coordinates.length || !feature.properties.area) {\n console.warn(`buildLocationIndex: locationSet ${resolved.id} for ${item.id} resolves to an empty feature:`);\n console.warn(JSON.stringify(feature));\n return;\n }\n\n that.locationSets.set(resolved.id, feature);\n });\n });\n\n that.locationIndex = whichPolygon({ type: 'FeatureCollection', features: [...that.locationSets.values()] });\n\n function _cloneDeep(obj) {\n return JSON.parse(JSON.stringify(obj));\n }\n }\n\n\n //\n // `match()`\n // Pass parts and return an Array of matches.\n // `k` - key\n // `v` - value\n // `n` - namelike\n // `loc` - optional - [lon,lat] location to search\n //\n // 1. If the [k,v,n] tuple matches a canonical item…\n // Return an Array of match results.\n // Each result will include the area in km² that the item is valid.\n //\n // Order of results:\n // Primary ordering will be on the \"match\" column:\n // \"primary\" - where the query matches the `name` tag, followed by\n // \"alternate\" - where the query matches an alternate name tag (e.g. short_name, brand, operator, etc)\n // Secondary ordering will be on the \"area\" column:\n // \"area descending\" if no location was provided, (worldwide before local)\n // \"area ascending\" if location was provided (local before worldwide)\n //\n // [\n // { match: 'primary', itemID: String, area: Number, kv: String, nsimple: String },\n // { match: 'primary', itemID: String, area: Number, kv: String, nsimple: String },\n // { match: 'alternate', itemID: String, area: Number, kv: String, nsimple: String },\n // { match: 'alternate', itemID: String, area: Number, kv: String, nsimple: String },\n // …\n // ]\n //\n // -or-\n //\n // 2. If the [k,v,n] tuple matches an exclude pattern…\n // Return an Array with a single exclude result, either\n //\n // [ { match: 'excludeGeneric', pattern: String, kv: String } ] // \"generic\" e.g. \"Food Court\"\n // or\n // [ { match: 'excludeNamed', pattern: String, kv: String } ] // \"named\", e.g. \"Kebabai\"\n //\n // About results\n // \"generic\" - a generic word that is probably not really a name.\n // For these, iD should warn the user \"Hey don't put 'food court' in the name tag\".\n // \"named\" - a real name like \"Kebabai\" that is just common, but not a brand.\n // For these, iD should just let it be. We don't include these in NSI, but we don't want to nag users about it either.\n //\n // -or-\n //\n // 3. If the [k,v,n] tuple matches nothing of any kind, return `null`\n //\n //\n match(k: string, v: string, n: string, loc?: Vec2): Array | null {\n const that = this;\n if (!that.matchIndex) {\n throw new Error('match: matchIndex not built.');\n }\n\n // If we were supplied a location, and a that.locationIndex has been set up,\n // get the locationSets that are valid there so we can filter results.\n let matchLocations;\n if (Array.isArray(loc) && that.locationIndex) {\n // which-polygon query returns an array of GeoJSON properties, pass true to return all results\n matchLocations = that.locationIndex([loc[0], loc[1], loc[0], loc[1]], true);\n }\n\n const nsimple = simplify(n);\n\n const seen = new Set();\n const results: Array = [];\n gatherResults('primary');\n gatherResults('alternate');\n if (results.length) return results;\n\n gatherResults('exclude');\n return results.length ? results : null;\n\n\n function gatherResults(which: string): void {\n // First try an exact match on k/v\n const kv = `${k}/${v}`;\n let didMatch = tryMatch(which, kv);\n if (didMatch) return;\n\n // If that didn't work, look in match groups for other pairs considered equivalent to k/v..\n for (const mg in matchGroups) {\n const matchGroup = matchGroups[mg];\n const inGroup = matchGroup.some(otherkv => otherkv === kv);\n if (!inGroup) continue;\n\n for (const otherkv of matchGroup) {\n if (otherkv === kv) continue; // skip self\n didMatch = tryMatch(which, otherkv);\n if (didMatch) return;\n }\n }\n\n // If finished 'exclude' pass and still haven't matched anything, try the global `genericWords.json` patterns\n if (which === 'exclude') {\n const regex = [...that.genericWords.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeGeneric', pattern: String(regex) }); // note no `branch`, no `kv`\n return;\n }\n }\n }\n\n function tryMatch(which: string, kv: string): boolean {\n const branch = that.matchIndex.get(kv);\n if (!branch) return false;\n\n if (which === 'exclude') { // Test name `n` against named and generic exclude patterns\n let regex = [...branch.excludeNamed.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeNamed', pattern: String(regex), kv: kv });\n return false;\n }\n regex = [...branch.excludeGeneric.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeGeneric', pattern: String(regex), kv: kv });\n return false;\n }\n return false;\n }\n\n const leaf = branch[which].get(nsimple);\n if (!leaf || !leaf.size) return false;\n if (!(which === 'primary' || which === 'alternate')) return false;\n\n // If we get here, we matched something..\n // Prepare the results, calculate areas (if location index was set up)\n let hits: Array = [];\n for (const itemID of [...leaf]) {\n let area = Infinity;\n if (that.itemLocation && that.locationSets) {\n const location = that.locationSets.get(that.itemLocation.get(itemID));\n area = (location && location.properties.area) || Infinity;\n }\n hits.push({ match: which, itemID: itemID, area: area, kv: kv, nsimple: nsimple });\n }\n\n let sortFn = byAreaDescending;\n\n // Filter the match to include only results valid in the requested `loc`..\n if (matchLocations) {\n hits = hits.filter(isValidLocation);\n sortFn = byAreaAscending;\n }\n\n if (!hits.length) return false;\n\n // push results\n hits.sort(sortFn).forEach(hit => {\n if (seen.has(hit.itemID)) return;\n seen.add(hit.itemID);\n results.push(hit);\n });\n\n return true;\n\n\n function isValidLocation(hit: Hit): boolean {\n if (!that.itemLocation) return true;\n return matchLocations.find(props => props.id === that.itemLocation.get(hit.itemID));\n }\n // Sort smaller (more local) locations first.\n function byAreaAscending(hitA: Hit, hitB: Hit): number {\n const areaA = hitA.area || 0;\n const areaB = hitB.area || 0;\n return areaA - areaB;\n }\n // Sort larger (more worldwide) locations first.\n function byAreaDescending(hitA: Hit, hitB: Hit): number {\n const areaA = hitA.area || 0;\n const areaB = hitB.area || 0;\n return areaB - areaA;\n }\n }\n }\n\n\n //\n // `getWarnings()`\n // Return any warnings discovered when buiding the index.\n // (currently this does nothing)\n //\n getWarnings(): Array {\n return this.warnings;\n }\n}\n", "// External\nimport diacritics from 'diacritics';\n\n// remove spaces, punctuation, diacritics\n// for punction see https://stackoverflow.com/a/21224179\nexport function simplify(str?: string): string {\n if (typeof str !== 'string') return '';\n\n return diacritics.remove(\n str\n .replace(/&/g, 'and')\n .replace(/(İ|i̇)/ig, 'i') // for BİM, İşbank - #5017, #8261\n .replace(/[\\s\\-=_!\"#%'*{},.\\/:;?\\(\\)\\[\\]@\\\\$\\^*+<>«»~`’\\u00a1\\u00a7\\u00b6\\u00b7\\u00bf\\u037e\\u0387\\u055a-\\u055f\\u0589\\u05c0\\u05c3\\u05c6\\u05f3\\u05f4\\u0609\\u060a\\u060c\\u060d\\u061b\\u061e\\u061f\\u066a-\\u066d\\u06d4\\u0700-\\u070d\\u07f7-\\u07f9\\u0830-\\u083e\\u085e\\u0964\\u0965\\u0970\\u0af0\\u0df4\\u0e4f\\u0e5a\\u0e5b\\u0f04-\\u0f12\\u0f14\\u0f85\\u0fd0-\\u0fd4\\u0fd9\\u0fda\\u104a-\\u104f\\u10fb\\u1360-\\u1368\\u166d\\u166e\\u16eb-\\u16ed\\u1735\\u1736\\u17d4-\\u17d6\\u17d8-\\u17da\\u1800-\\u1805\\u1807-\\u180a\\u1944\\u1945\\u1a1e\\u1a1f\\u1aa0-\\u1aa6\\u1aa8-\\u1aad\\u1b5a-\\u1b60\\u1bfc-\\u1bff\\u1c3b-\\u1c3f\\u1c7e\\u1c7f\\u1cc0-\\u1cc7\\u1cd3\\u2000-\\u206f\\u2cf9-\\u2cfc\\u2cfe\\u2cff\\u2d70\\u2e00-\\u2e7f\\u3001-\\u3003\\u303d\\u30fb\\ua4fe\\ua4ff\\ua60d-\\ua60f\\ua673\\ua67e\\ua6f2-\\ua6f7\\ua874-\\ua877\\ua8ce\\ua8cf\\ua8f8-\\ua8fa\\ua92e\\ua92f\\ua95f\\ua9c1-\\ua9cd\\ua9de\\ua9df\\uaa5c-\\uaa5f\\uaade\\uaadf\\uaaf0\\uaaf1\\uabeb\\ufe10-\\ufe16\\ufe19\\ufe30\\ufe45\\ufe46\\ufe49-\\ufe4c\\ufe50-\\ufe52\\ufe54-\\ufe57\\ufe5f-\\ufe61\\ufe68\\ufe6a\\ufe6b\\ufeff\\uff01-\\uff03\\uff05-\\uff07\\uff0a\\uff0c\\uff0e\\uff0f\\uff1a\\uff1b\\uff1f\\uff20\\uff3c\\uff61\\uff64\\uff65]+/g,'')\n .toLowerCase()\n );\n}\n", "// Internal\nimport { simplify } from './simplify.ts';\n\n// Removes noise from the name so that we can compare\n// similar names for catching duplicates.\nexport function stemmer(str?: string): string {\n if (typeof str !== 'string') return '';\n\n const noise = [\n /ban(k|c)(a|o)?/ig,\n /банк/ig,\n /coop/ig,\n /express/ig,\n /(gas|fuel)/ig,\n /wireless/ig,\n /(shop|store)/ig\n ];\n\n str = noise.reduce((acc, regex) => acc.replace(regex, ''), str);\n return simplify(str);\n}\n", "import { Matcher } from 'name-suggestion-index';\nimport { fileFetcher, locationManager } from '../core';\nimport { presetManager } from '../presets';\n\nimport { nsiCdnUrl } from '../../config/id.js';\n\n// Make very sure this resolves to iD's `package.json`\n// If you mess up the `../`s, the resolver may import another random package.json from somewhere else.\nimport packageJSON from '../../package.json';\n\n\n// This service contains all the code related to the **name-suggestion-index** (aka NSI)\n// NSI contains the most correct tagging for many commonly mapped features.\n// See https://github.com/osmlab/name-suggestion-index and https://nsi.guide\n\n\n// DATA\n\nlet _nsiStatus = 'loading'; // 'loading', 'ok', 'failed'\nlet _nsi = {};\n\n// Sometimes we can upgrade a feature tagged like `building=yes` to a better tag.\nconst buildingPreset = {\n 'building/commercial': true,\n 'building/government': true,\n 'building/hotel': true,\n 'building/retail': true,\n 'building/office': true,\n 'building/supermarket': true,\n 'building/yes': true\n};\n\n// Exceptions to the namelike regexes.\n// Usually a tag suffix contains a language code like `name:en`, `name:ru`\n// but we want to exclude things like `operator:type`, `name:etymology`, etc..\nconst notNames = /:(colou?r|type|forward|backward|left|right|etymology|pronunciation|wikipedia)$/i;\n\n// Exceptions to the branchlike regexes\nconst notBranches = /(coop|express|wireless|factory|outlet)/i;\n\n\n// PRIVATE FUNCTIONS\n\n// `setNsiSources()`\n// Adds the sources to iD's filemap so we can start downloading data.\n//\nfunction setNsiSources() {\n const nsiVersion = packageJSON.dependencies['name-suggestion-index'] || packageJSON.devDependencies['name-suggestion-index'];\n const cdn = nsiCdnUrl.replace('{version}', nsiVersion);\n const sources = {\n 'nsi_data': cdn + 'dist/json/nsi.min.json',\n 'nsi_dissolved': cdn + 'dist/wikidata/dissolved.min.json',\n 'nsi_features': cdn + 'dist/json/featureCollection.min.json',\n 'nsi_generics': cdn + 'dist/json/genericWords.min.json',\n 'nsi_presets': cdn + 'dist/presets/nsi-id-presets.min.json',\n 'nsi_replacements': cdn + 'dist/json/replacements.min.json',\n 'nsi_trees': cdn + 'dist/json/trees.min.json'\n };\n\n let fileMap = fileFetcher.fileMap();\n for (const k in sources) {\n if (!fileMap[k]) fileMap[k] = sources[k];\n }\n}\n\n\n// `loadNsiPresets()`\n// Returns a Promise fulfilled when the presets have been downloaded and merged into iD.\n//\nfunction loadNsiPresets() {\n return (\n Promise.all([\n fileFetcher.get('nsi_presets'),\n fileFetcher.get('nsi_features')\n ])\n .then(vals => {\n // Add `suggestion=true` to all the nsi presets\n // The preset json schema doesn't include it, but the iD code still uses it\n Object.values(vals[0].presets).forEach(preset => preset.suggestion = true);\n\n // nsi does not specify *:wikipedia (anymore):\n // clean up previous values to prevent that the wikidata/wikipedia information\n // is going to be out of sync, see #9103\n Object.values(vals[0].presets).forEach(preset => {\n if (preset.tags['brand:wikidata']) {\n preset.removeTags = {'brand:wikipedia': '*', ...(preset.removeTags || preset.addTags || preset.tags)};\n }\n if (preset.tags['operator:wikidata']) {\n preset.removeTags = {'operator:wikipedia': '*', ...(preset.removeTags || preset.addTags || preset.tags)};\n }\n if (preset.tags['network:wikidata']) {\n preset.removeTags = {'network:wikipedia': '*', ...(preset.removeTags || preset.addTags || preset.tags)};\n }\n });\n\n presetManager.merge({\n presets: vals[0].presets,\n featureCollection: vals[1]\n });\n })\n );\n}\n\n\n// `loadNsiData()`\n// Returns a Promise fulfilled when the other data have been downloaded and processed\n//\nfunction loadNsiData() {\n return (\n Promise.all([\n fileFetcher.get('nsi_data'),\n fileFetcher.get('nsi_dissolved'),\n fileFetcher.get('nsi_replacements'),\n fileFetcher.get('nsi_trees')\n ])\n .then(vals => {\n _nsi = {\n data: vals[0].nsi, // the raw name-suggestion-index data\n dissolved: vals[1].dissolved, // list of dissolved items\n replacements: vals[2].replacements, // trivial old->new qid replacements\n trees: vals[3].trees, // metadata about trees, main tags\n kvt: new Map(), // Map (k -> Map (v -> t) )\n qids: new Map(), // Map (wd/wp tag values -> qids)\n ids: new Map() // Map (id -> NSI item)\n };\n\n const matcher = new Matcher();\n _nsi.matcher = matcher;\n matcher.buildMatchIndex(_nsi.data);\n\n// *** BEGIN HACK ***\n\n// old - built in matcher will set up the locationindex by resolving all the locationSets one-by-one\n // matcher.buildLocationIndex(_nsi.data, locationManager.loco());\n\n// new - Use the location manager instead of redoing that work\n// It has already processed the presets at this point\n\n// We need to monkeypatch a few of the collections that the NSI matcher depends on.\n// The `itemLocation` structure maps itemIDs to locationSetIDs\nmatcher.itemLocation = new Map();\n\n// The `locationSets` structure maps locationSetIDs to GeoJSON\n// We definitely need this, but don't need full geojson, just { properties: { area: xxx }}\nmatcher.locationSets = new Map();\n\nObject.keys(_nsi.data).forEach(tkv => {\n const items = _nsi.data[tkv].items;\n if (!Array.isArray(items) || !items.length) return;\n\n items.forEach(item => {\n if (matcher.itemLocation.has(item.id)) return; // we've seen item id already - shouldn't be possible?\n\n const locationSetID = locationManager.locationSetID(item.locationSet);\n matcher.itemLocation.set(item.id, locationSetID);\n\n if (matcher.locationSets.has(locationSetID)) return; // we've seen this locationSet before..\n\n const fakeFeature = { id: locationSetID, properties: { id: locationSetID, area: 1 } };\n matcher.locationSets.set(locationSetID, fakeFeature);\n });\n});\n\n// The `locationIndex` is an instance of which-polygon spatial index for the locationSets.\n// We only really need this to _look like_ which-polygon query `_wp.locationIndex(bbox, true);`\n// i.e. it needs to return the properties of the locationsets\nmatcher.locationIndex = (bbox) => {\n const validHere = locationManager.locationSetsAt([bbox[0], bbox[1]]);\n const results = [];\n\n for (const [locationSetID, area] of Object.entries(validHere)) {\n const fakeFeature = matcher.locationSets.get(locationSetID);\n if (fakeFeature) {\n fakeFeature.properties.area = area;\n results.push(fakeFeature);\n }\n }\n return results;\n};\n\n// *** END HACK ***\n\n\n Object.keys(_nsi.data).forEach(tkv => {\n const category = _nsi.data[tkv];\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const t = parts[0];\n const k = parts[1];\n const v = parts[2];\n\n // Build a reverse index of keys -> values -> trees present in the name-suggestion-index\n // Collect primary keys (e.g. \"amenity\", \"craft\", \"shop\", \"man_made\", \"route\", etc)\n // \"amenity\": {\n // \"restaurant\": \"brands\"\n // }\n let vmap = _nsi.kvt.get(k);\n if (!vmap) {\n vmap = new Map();\n _nsi.kvt.set(k, vmap);\n }\n vmap.set(v, t);\n\n const tree = _nsi.trees[t]; // e.g. \"brands\", \"operators\"\n const mainTag = tree.mainTag; // e.g. \"brand:wikidata\", \"operator:wikidata\", etc\n\n const items = category.items || [];\n items.forEach(item => {\n // Remember some useful things for later, cache NSI id -> item\n item.tkv = tkv;\n item.mainTag = mainTag;\n _nsi.ids.set(item.id, item);\n\n // Cache Wikidata/Wikipedia values -> qid, for #6416\n const wd = item.tags[mainTag];\n const wp = item.tags[mainTag.replace('wikidata', 'wikipedia')];\n if (wd) _nsi.qids.set(wd, wd);\n if (wp && wd) _nsi.qids.set(wp, wd);\n });\n });\n })\n );\n}\n\n\n// `gatherKVs()`\n// Gather all the k/v pairs that we will run through the NSI matcher.\n// An OSM tags object can contain anything, but only a few tags will be interesting to NSI.\n//\n// This function will return the interesting tag pairs like:\n// \"amenity/restaurant\", \"man_made/flagpole\"\n// and fallbacks like\n// \"amenity/yes\"\n// excluding things like\n// \"tiger:reviewed\", \"surface\", \"ref\", etc.\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `Object` containing kv pairs to test:\n// {\n// 'primary': Set(),\n// 'alternate': Set()\n// }\n//\nfunction gatherKVs(tags) {\n let primary = new Set();\n let alternate = new Set();\n\n Object.keys(tags).forEach(osmkey => {\n const osmvalue = tags[osmkey];\n if (!osmvalue) return;\n\n // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184\n if (osmkey === 'route_master') osmkey = 'route';\n\n const vmap = _nsi.kvt.get(osmkey);\n if (!vmap) return; // not an interesting key\n\n if (vmap.get(osmvalue)) { // Matched a category in NSI\n primary.add(`${osmkey}/${osmvalue}`); // interesting key/value\n } else if (osmvalue === 'yes') {\n alternate.add(`${osmkey}/${osmvalue}`); // fallback key/yes\n }\n });\n\n // Can we try a generic building fallback match? - See #6122, #7197\n // Only try this if we do a preset match and find nothing else remarkable about that building.\n // For example, a way with `building=yes` + `name=Westfield` may be a Westfield department store.\n // But a way with `building=yes` + `name=Westfield` + `public_transport=station` is a train station for a town named \"Westfield\"\n const preset = presetManager.matchTags(tags, 'area');\n if (buildingPreset[preset.id]) {\n alternate.add('building/yes');\n }\n\n return { primary: primary, alternate: alternate };\n}\n\n\n// `identifyTree()`\n// NSI has a concept of trees: \"brands\", \"operators\", \"flags\", \"transit\".\n// The tree determines things like which tags are namelike, and which tags hold important wikidata.\n// This takes an Object of tags and tries to identify what tree to use.\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `string` the name of the tree if known\n// or 'unknown' if it could match several trees (e.g. amenity/yes)\n// or null if no match\n//\nfunction identifyTree(tags) {\n let unknown;\n let t;\n\n // Check all tags\n Object.keys(tags).forEach(osmkey => {\n if (t) return; // found already\n\n const osmvalue = tags[osmkey];\n if (!osmvalue) return;\n\n // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184\n if (osmkey === 'route_master') osmkey = 'route';\n\n const vmap = _nsi.kvt.get(osmkey);\n if (!vmap) return; // this key is not in nsi\n\n if (osmvalue === 'yes') {\n unknown = 'unknown';\n } else {\n t = vmap.get(osmvalue);\n }\n });\n\n return t || unknown || null;\n}\n\n\n// `gatherNames()`\n// Gather all the namelike values that we will run through the NSI matcher.\n// It will gather values primarily from tags `name`, `name:ru`, `flag:name`\n// and fallback to alternate tags like `brand`, `brand:ru`, `alt_name`\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `Object` containing namelike values to test:\n// {\n// 'primary': Set(),\n// 'fallbacks': Set()\n// }\n//\nfunction gatherNames(tags) {\n const empty = { primary: new Set(), alternate: new Set() };\n let primary = new Set();\n let alternate = new Set();\n let foundSemi = false;\n let testNameFragments = false;\n let patterns;\n\n // Patterns for matching OSM keys that might contain namelike values.\n // These roughly correspond to the \"trees\" concept in name-suggestion-index,\n let t = identifyTree(tags);\n if (!t) return empty;\n\n if (t === 'transit') {\n patterns = {\n primary: /^network$/i,\n alternate: /^(operator|operator:\\w+|network:\\w+|\\w+_name|\\w+_name:\\w+)$/i\n };\n } else if (t === 'flags') {\n patterns = {\n primary: /^(flag:name|flag:name:\\w+)$/i,\n alternate: /^(flag|flag:\\w+|subject|subject:\\w+)$/i // note: no `country`, we special-case it below\n };\n } else if (t === 'brands') {\n testNameFragments = true;\n patterns = {\n primary: /^(name|name:\\w+)$/i,\n alternate: /^(brand|brand:\\w+|operator|operator:\\w+|\\w+_name|\\w+_name:\\w+)/i,\n };\n } else if (t === 'operators') {\n testNameFragments = true;\n patterns = {\n primary: /^(name|name:\\w+|operator|operator:\\w+)$/i,\n alternate: /^(brand|brand:\\w+|\\w+_name|\\w+_name:\\w+)/i,\n };\n } else { // unknown/multiple\n testNameFragments = true;\n patterns = {\n primary: /^(name|name:\\w+)$/i,\n alternate: /^(brand|brand:\\w+|network|network:\\w+|operator|operator:\\w+|\\w+_name|\\w+_name:\\w+)/i,\n };\n }\n\n // Test `name` fragments, longest to shortest, to fit them into a \"Name Branch\" pattern.\n // e.g. \"TUI ReiseCenter - Neuss Innenstadt\" -> [\"TUI\", \"ReiseCenter\", \"Neuss\", \"Innenstadt\"]\n if (tags.name && testNameFragments) {\n const nameParts = tags.name.split(/[\\s\\-\\/,.]/);\n for (let split = nameParts.length; split > 0; split--) {\n const name = nameParts.slice(0, split).join(' '); // e.g. \"TUI ReiseCenter\"\n primary.add(name);\n }\n }\n\n // Check all tags\n Object.keys(tags).forEach(osmkey => {\n const osmvalue = tags[osmkey];\n if (!osmvalue) return;\n\n if (isNamelike(osmkey, 'primary')) {\n if (/;/.test(osmvalue)) {\n foundSemi = true;\n } else {\n primary.add(osmvalue);\n alternate.delete(osmvalue);\n }\n } else if (!primary.has(osmvalue) && isNamelike(osmkey, 'alternate')) {\n if (/;/.test(osmvalue)) {\n foundSemi = true;\n } else {\n alternate.add(osmvalue);\n }\n }\n });\n\n // For flags only, fallback to `country` tag only if no other namelike values were found.\n // See https://github.com/openstreetmap/iD/pull/8305#issuecomment-769174070\n if (tags.man_made === 'flagpole' && !primary.size && !alternate.size && !!tags.country) {\n const osmvalue = tags.country;\n if (/;/.test(osmvalue)) {\n foundSemi = true;\n } else {\n alternate.add(osmvalue);\n }\n }\n\n // If any namelike value contained a semicolon, return empty set and don't try matching anything.\n if (foundSemi) {\n return empty;\n } else {\n return { primary: primary, alternate: alternate };\n }\n\n function isNamelike(osmkey, which) {\n if (osmkey === 'old_name') return false;\n return patterns[which].test(osmkey) && !notNames.test(osmkey);\n }\n}\n\n\n// `gatherTuples()`\n// Generate all combinations of [key,value,name] that we want to test.\n// This prioritizes them so that the primary name and k/v pairs go first\n//\n// Arguments\n// `tryKVs`: `Object` containing primary and alternate k/v pairs to test\n// `tryNames`: `Object` containing primary and alternate names to test\n// Returns\n// `Array`: tuple objects ordered by priority\n//\nfunction gatherTuples(tryKVs, tryNames) {\n let tuples = [];\n ['primary', 'alternate'].forEach(whichName => {\n // test names longest to shortest\n const arr = Array.from(tryNames[whichName]).sort((a, b) => b.length - a.length);\n arr.forEach(n => {\n ['primary', 'alternate'].forEach(whichKV => {\n tryKVs[whichKV].forEach(kv => {\n const parts = kv.split('/', 2);\n const k = parts[0];\n const v = parts[1];\n tuples.push({ k: k, v: v, n: n });\n });\n });\n });\n });\n return tuples;\n}\n\n\n// `_upgradeTags()`\n// Try to match a feature to a canonical record in name-suggestion-index\n// and upgrade the tags to match.\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// `loc`: Location where this feature exists, as a [lon, lat]\n// Returns\n// `Object` containing the result, or `null` if no changes needed:\n// {\n// 'newTags': `Object` - The tags the the feature should have\n// 'matched': `Object` - The matched item\n// }\n//\nfunction _upgradeTags(tags, loc) {\n let newTags = Object.assign({}, tags); // shallow copy\n let changed = false;\n\n // Before anything, perform trivial Wikipedia/Wikidata replacements\n Object.keys(newTags).forEach(osmkey => {\n const matchTag = osmkey.match(/^(\\w+:)?wikidata$/);\n if (matchTag) { // Look at '*:wikidata' tags\n const prefix = (matchTag[1] || '');\n const wd = newTags[osmkey];\n const replace = _nsi.replacements[wd]; // If it matches a QID in the replacement list...\n\n if (replace && replace.wikidata !== undefined) { // replace or delete `*:wikidata` tag\n changed = true;\n if (replace.wikidata) {\n newTags[osmkey] = replace.wikidata;\n } else {\n delete newTags[osmkey];\n }\n }\n if (replace && replace.wikipedia !== undefined) { // replace or delete `*:wikipedia` tag\n changed = true;\n const wpkey = `${prefix}wikipedia`;\n if (replace.wikipedia) {\n newTags[wpkey] = replace.wikipedia;\n } else {\n delete newTags[wpkey];\n }\n }\n }\n });\n\n // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184\n const isRouteMaster = (tags.type === 'route_master');\n\n // Gather key/value tag pairs to try to match\n const tryKVs = gatherKVs(tags);\n if (!tryKVs.primary.size && !tryKVs.alternate.size) {\n return changed ? { newTags: newTags, matched: null } : null;\n }\n\n // Gather namelike tag values to try to match\n const tryNames = gatherNames(tags);\n\n // Do `wikidata=*` or `wikipedia=*` tags identify this entity as a chain? - See #6416\n // If so, these tags can be swapped to e.g. `brand:wikidata`/`brand:wikipedia`.\n const foundQID = _nsi.qids.get(tags.wikidata) || _nsi.qids.get(tags.wikipedia);\n if (foundQID) tryNames.primary.add(foundQID); // matcher will recognize the Wikidata QID as name too\n\n if (!tryNames.primary.size && !tryNames.alternate.size) {\n return changed ? { newTags: newTags, matched: null } : null;\n }\n\n // Order the [key,value,name] tuples - test primary before alternate\n const tuples = gatherTuples(tryKVs, tryNames);\n\n for (let i = 0; i < tuples.length; i++) {\n const tuple = tuples[i];\n const hits = _nsi.matcher.match(tuple.k, tuple.v, tuple.n, loc); // Attempt to match an item in NSI\n\n if (!hits || !hits.length) continue; // no match, try next tuple\n if (hits[0].match !== 'primary' && hits[0].match !== 'alternate') break; // a generic match, stop looking\n\n // A match may contain multiple results, the first one is likely the best one for this location\n // e.g. `['pfk-a54c14', 'kfc-1ff19c', 'kfc-658eea']`\n let itemID, item;\n for (let j = 0; j < hits.length; j++) {\n const hit = hits[j];\n itemID = hit.itemID;\n if (_nsi.dissolved[itemID]) continue; // Don't upgrade to a dissolved item\n\n item = _nsi.ids.get(itemID);\n if (!item) continue;\n const mainTag = item.mainTag; // e.g. `brand:wikidata`\n const itemQID = item.tags[mainTag]; // e.g. `brand:wikidata` qid\n const notQID = newTags[`not:${mainTag}`]; // e.g. `not:brand:wikidata` qid\n\n if ( // Exceptions, skip this hit\n (!itemQID || itemQID === notQID) || // No `*:wikidata` or matched a `not:*:wikidata`\n (newTags.office && !item.tags.office) // feature may be a corporate office for a brand? - #6416\n ) {\n item = null;\n continue; // continue looking\n } else {\n break; // use `item`\n }\n }\n\n // Can't use any of these hits, try next tuple..\n if (!item) continue;\n\n // At this point we have matched a canonical item and can suggest tag upgrades..\n item = JSON.parse(JSON.stringify(item)); // deep copy\n const tkv = item.tkv;\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const k = parts[1];\n const v = parts[2];\n const category = _nsi.data[tkv];\n const properties = category.properties || {};\n\n // Preserve some tags that we specifically don't want NSI to overwrite. ('^name', sometimes)\n let preserveTags = item.preserveTags || properties.preserveTags || [];\n\n // These tags can be toplevel tags -or- attributes - so we generally want to preserve existing values - #8615\n // We'll only _replace_ the tag value if this tag is the toplevel/defining tag for the matched item (`k`)\n ['building', 'emergency', 'internet_access', 'opening_hours', 'takeaway'].forEach(osmkey => {\n if (k !== osmkey) preserveTags.push(`^${osmkey}$`);\n });\n\n const regexes = preserveTags.map(s => new RegExp(s, 'i'));\n\n let keepTags = {};\n Object.keys(newTags).forEach(osmkey => {\n if (regexes.some(regex => regex.test(osmkey))) {\n keepTags[osmkey] = newTags[osmkey];\n }\n });\n\n // Remove any primary tags (\"amenity\", \"craft\", \"shop\", \"man_made\", \"route\", etc) that have a\n // value like `amenity=yes` or `shop=yes` (exceptions have already been added to `keepTags` above)\n _nsi.kvt.forEach((vmap, k) => {\n if (newTags[k] === 'yes') delete newTags[k];\n });\n\n // Replace mistagged `wikidata`/`wikipedia` with e.g. `brand:wikidata`/`brand:wikipedia`\n if (foundQID) {\n delete newTags.wikipedia;\n delete newTags.wikidata;\n }\n\n // Do the tag upgrade\n Object.assign(newTags, item.tags, keepTags);\n\n // Swap `route` back to `route_master` - name-suggestion-index#5184\n if (isRouteMaster) {\n newTags.route_master = newTags.route;\n delete newTags.route;\n }\n\n // Special `branch` splitting rules - IF..\n // - NSI is suggesting to replace `name`, AND\n // - `branch` doesn't already contain something, AND\n // - original name has not moved to an alternate name (e.g. \"Dunkin' Donuts\" -> \"Dunkin'\"), AND\n // - original name is \"some name\" + \"some stuff\", THEN\n // consider splitting `name` into `name`/`branch`..\n const origName = tags.name;\n const newName = newTags.name;\n if (newName && origName && newName !== origName && !newTags.branch) {\n const newNames = gatherNames(newTags);\n const newSet = new Set([...newNames.primary, ...newNames.alternate]);\n const isMoved = newSet.has(origName); // another tag holds the original name now\n\n if (!isMoved) {\n // Test name fragments, longest to shortest, to fit them into a \"Name Branch\" pattern.\n // e.g. \"TUI ReiseCenter - Neuss Innenstadt\" -> [\"TUI\", \"ReiseCenter\", \"Neuss\", \"Innenstadt\"]\n const nameParts = origName.split(/[\\s\\-\\/,.]/);\n for (let split = nameParts.length; split > 0; split--) {\n const name = nameParts.slice(0, split).join(' '); // e.g. \"TUI ReiseCenter\"\n const branch = nameParts.slice(split).join(' '); // e.g. \"Neuss Innenstadt\"\n const nameHits = _nsi.matcher.match(k, v, name, loc);\n if (!nameHits || !nameHits.length) continue; // no match, try next name fragment\n\n if (nameHits.some(hit => hit.itemID === itemID)) { // matched the name fragment to the same itemID above\n if (branch) {\n if (notBranches.test(branch)) { // \"branch\" was detected but is noise (\"factory outlet\", etc)\n newTags.name = origName; // Leave `name` alone, this part of the name may be significant..\n } else {\n const branchHits = _nsi.matcher.match(k, v, branch, loc);\n if (branchHits && branchHits.length) { // if \"branch\" matched something else in NSI..\n if (branchHits[0].match === 'primary' || branchHits[0].match === 'alternate') { // if another brand! (e.g. \"KFC - Taco Bell\"?)\n return null; // bail out - can't suggest tags in this case\n } // else a generic (e.g. \"gas\", \"cafe\") - ignore\n } else { // \"branch\" is not noise and not something in NSI\n newTags.branch = branch; // Stick it in the `branch` tag..\n }\n }\n }\n break;\n }\n }\n }\n }\n\n return { newTags: newTags, matched: item };\n }\n\n return changed ? { newTags: newTags, matched: null } : null;\n}\n\n\n// `_isGenericName()`\n// Is the `name` tag generic?\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `true` if it is generic, `false` if not\n//\nfunction _isGenericName(tags) {\n const n = tags.name;\n if (!n) return false;\n\n // tryNames just contains the `name` tag value and nothing else\n const tryNames = { primary: new Set([n]), alternate: new Set() };\n\n // Gather key/value tag pairs to try to match\n const tryKVs = gatherKVs(tags);\n if (!tryKVs.primary.size && !tryKVs.alternate.size) return false;\n\n // Order the [key,value,name] tuples - test primary before alternate\n const tuples = gatherTuples(tryKVs, tryNames);\n\n for (let i = 0; i < tuples.length; i++) {\n const tuple = tuples[i];\n const hits = _nsi.matcher.match(tuple.k, tuple.v, tuple.n); // Attempt to match an item in NSI\n\n // If we get a `excludeGeneric` hit, this is a generic name.\n if (hits && hits.length && hits[0].match === 'excludeGeneric') return true;\n }\n\n return false;\n}\n\n\n\n// PUBLIC INTERFACE\n\nexport default {\n\n // `init()`\n // On init, start preparing the name-suggestion-index\n //\n init: () => {\n // Note: service.init is called immediately after the presetManager has started loading its data.\n // We expect to chain onto an unfulfilled promise here.\n setNsiSources();\n presetManager.ensureLoaded()\n .then(() => loadNsiPresets())\n .then(() => loadNsiData())\n .then(() => _nsiStatus = 'ok')\n .catch(() => _nsiStatus = 'failed');\n },\n\n\n // `reset()`\n // Reset is called when user saves data to OSM (does nothing here)\n //\n reset: () => {},\n\n\n // `status()`\n // To let other code know how it's going...\n //\n // Returns\n // `String`: 'loading', 'ok', 'failed'\n //\n status: () => _nsiStatus,\n\n\n // `isGenericName()`\n // Is the `name` tag generic?\n //\n // Arguments\n // `tags`: `Object` containing the feature's OSM tags\n // Returns\n // `true` if it is generic, `false` if not\n //\n isGenericName: (tags) => _isGenericName(tags),\n\n\n // `upgradeTags()`\n // Suggest tag upgrades.\n // This function will not modify the input tags, it makes a copy.\n //\n // Arguments\n // `tags`: `Object` containing the feature's OSM tags\n // `loc`: Location where this feature exists, as a [lon, lat]\n // Returns\n // `Object` containing the result, or `null` if no changes needed:\n // {\n // 'newTags': `Object` - The tags the the feature should have\n // 'matched': `Object` - The matched item\n // }\n //\n upgradeTags: (tags, loc) => _upgradeTags(tags, loc),\n\n\n // `cache()`\n // Direct access to the NSI cache, useful for testing or breaking things\n //\n // Returns\n // `Object`: the internal NSI cache\n //\n cache: () => _nsi\n};\n", "import { localizer } from '../core/localizer';\n\nfunction timeSince(date: Date): [value: number, unit: Intl.RelativeTimeFormatUnit] {\n const seconds = Math.floor((+new Date() - +date) / 1000);\n const s = (n: number) => Math.floor(seconds / n);\n\n if (s(60 * 60 * 24 * 365) > 1) return [s(60 * 60 * 24 * 365), 'years'];\n if (s(60 * 60 * 24 * 30) > 1) return [s(60 * 60 * 24 * 30), 'months'];\n if (s(60 * 60 * 24) > 1) return [s(60 * 60 * 24), 'days'];\n if (s(60 * 60) > 1) return [s(60 * 60), 'hours'];\n if (s(60) > 1) return [s(60), 'minutes'];\n return [s(1), 'seconds'];\n}\n\n/**\n * Show the relative time if {@link Intl.RelativeTimeFormat} is supported\n * Otherwise fallback to the current date\n */\nexport function getRelativeDate(date: Date) {\n const preferredLanguage = localizer.localeCode();\n\n if (typeof Intl === 'undefined' || typeof Intl.RelativeTimeFormat === 'undefined') {\n return `on ${date.toLocaleDateString(preferredLanguage)}`;\n }\n\n const [number, units] = timeSince(date);\n if (!Number.isFinite(number)) return '-';\n\n return new Intl.RelativeTimeFormat(preferredLanguage).format(-number, units);\n}\n\nexport function localeDateString(date: Date | string) {\n if (!date) return null;\n const options: Intl.DateTimeFormatOptions = {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n };\n const d = new Date(date);\n if (Number.isNaN(d.getTime())) return null;\n return d.toLocaleDateString(localizer.localeCode(), options);\n}\n\nexport function localeTimestamp(date: Date) {\n const options: Intl.DateTimeFormatOptions = {\n day: '2-digit',\n month: '2-digit',\n year: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n second: 'numeric',\n };\n return date.toLocaleString(localizer.localeCode(), options);\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { json as d3_json } from 'd3-fetch';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport RBush from 'rbush';\n\nimport { geoExtent, geoScaleToZoom } from '../geo';\nimport { utilQsString, utilRebind, utilSetTransform, utilStringQs, utilTiler } from '../util';\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\nimport { localeDateString } from '../util/date';\n\n\nvar apibase = 'https://kartaview.org';\nvar maxResults = 1000;\nvar tileZoom = 14;\nvar tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);\nvar dispatch = d3_dispatch('loadedImages');\nvar imgZoom = d3_zoom()\n .extent([[0, 0], [320, 240]])\n .translateExtent([[0, 0], [320, 240]])\n .scaleExtent([1, 15]);\nvar _oscCache;\nvar _oscSelectedImage;\nvar _loadViewerPromise;\n\n\nfunction abortRequest(controller) {\n controller.abort();\n}\n\n\nfunction maxPageAtZoom(z) {\n if (z < 15) return 2;\n if (z === 15) return 5;\n if (z === 16) return 10;\n if (z === 17) return 20;\n if (z === 18) return 40;\n if (z > 18) return 80;\n}\n\n\nfunction loadTiles(which, url, projection) {\n var currZoom = Math.floor(geoScaleToZoom(projection.scale()));\n var tiles = tiler.getTiles(projection);\n\n // abort inflight requests that are no longer needed\n var cache = _oscCache[which];\n Object.keys(cache.inflight).forEach(function(k) {\n var wanted = tiles.find(function(tile) { return k.indexOf(tile.id + ',') === 0; });\n if (!wanted) {\n abortRequest(cache.inflight[k]);\n delete cache.inflight[k];\n }\n });\n\n tiles.forEach(function(tile) {\n loadNextTilePage(which, currZoom, url, tile);\n });\n}\n\n\nfunction loadNextTilePage(which, currZoom, url, tile) {\n var cache = _oscCache[which];\n var bbox = tile.extent.bbox();\n var maxPages = maxPageAtZoom(currZoom);\n var nextPage = cache.nextPage[tile.id] || 1;\n var params = utilQsString({\n ipp: maxResults,\n page: nextPage,\n // client_id: clientId,\n bbTopLeft: [bbox.maxY, bbox.minX].join(','),\n bbBottomRight: [bbox.minY, bbox.maxX].join(',')\n }, true);\n\n if (nextPage > maxPages) return;\n\n var id = tile.id + ',' + String(nextPage);\n if (cache.loaded[id] || cache.inflight[id]) return;\n\n var controller = new AbortController();\n cache.inflight[id] = controller;\n\n var options = {\n method: 'POST',\n signal: controller.signal,\n body: params,\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' }\n };\n\n d3_json(url, options)\n .then(function(data) {\n cache.loaded[id] = true;\n delete cache.inflight[id];\n if (!data || !data.currentPageItems || !data.currentPageItems.length) {\n throw new Error('No Data');\n }\n\n var features = data.currentPageItems.map(function(item) {\n var loc = [+item.lng, +item.lat];\n var d;\n\n if (which === 'images') {\n d = {\n service: 'photo',\n loc: loc,\n key: item.id,\n ca: +item.heading,\n captured_at: (item.shot_date || item.date_added),\n captured_by: item.username,\n imagePath: item.name,\n sequence_id: item.sequence_id,\n sequence_index: +item.sequence_index\n };\n\n // cache sequence info\n var seq = _oscCache.sequences[d.sequence_id];\n if (!seq) {\n seq = { rotation: 0, images: [] };\n _oscCache.sequences[d.sequence_id] = seq;\n }\n seq.images[d.sequence_index] = d;\n _oscCache.images.forImageKey[d.key] = d; // cache imageKey -> image\n }\n\n return {\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n };\n });\n\n cache.rtree.load(features);\n\n if (data.currentPageItems.length === maxResults) { // more pages to load\n cache.nextPage[tile.id] = nextPage + 1;\n loadNextTilePage(which, currZoom, url, tile);\n } else {\n cache.nextPage[tile.id] = Infinity; // no more pages to load\n }\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n }\n })\n .catch(function() {\n cache.loaded[id] = true;\n delete cache.inflight[id];\n });\n}\n\nexport default {\n\n init: function() {\n if (!_oscCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset: function() {\n if (_oscCache) {\n Object.values(_oscCache.images.inflight).forEach(abortRequest);\n }\n\n _oscCache = {\n images: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), forImageKey: {} },\n sequences: {}\n };\n },\n\n\n images: function(projection) {\n var limit = 5;\n return searchLimited(limit, projection, _oscCache.images.rtree);\n },\n\n\n sequences: function(projection) {\n var viewport = projection.clipExtent();\n var min = [viewport[0][0], viewport[1][1]];\n var max = [viewport[1][0], viewport[0][1]];\n var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n var sequenceKeys = {};\n\n // all sequences for images in viewport\n _oscCache.images.rtree.search(bbox)\n .forEach(function(d) { sequenceKeys[d.data.sequence_id] = true; });\n\n // make linestrings from those sequences\n var lineStrings = [];\n Object.keys(sequenceKeys)\n .forEach(function(sequenceKey) {\n var seq = _oscCache.sequences[sequenceKey];\n var images = seq && seq.images;\n\n if (images) {\n lineStrings.push({\n type: 'LineString',\n coordinates: images.map(function (d) { return d.loc; }).filter(Boolean),\n properties: {\n captured_at: images[0] ? images[0].captured_at: null,\n captured_by: images[0] ? images[0].captured_by: null,\n key: sequenceKey\n }\n });\n }\n });\n return lineStrings;\n },\n\n\n cachedImage: function(imageKey) {\n return _oscCache.images.forImageKey[imageKey];\n },\n\n\n loadImages: function(projection) {\n var url = apibase + '/1.0/list/nearby-photos/';\n loadTiles('images', url, projection);\n },\n\n\n ensureViewerLoaded: function(context) {\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n // add kartaview-wrapper\n var wrap = context.container().select('.photoviewer').selectAll('.kartaview-wrapper')\n .data([0]);\n\n var that = this;\n\n var wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper kartaview-wrapper')\n .classed('hide', true)\n .call(imgZoom.on('zoom', zoomPan))\n .on('dblclick.zoom', null);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n var controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls');\n\n controlsEnter\n .append('button')\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .on('click.rotate-ccw', rotate(-90))\n .text('\u293F');\n\n controlsEnter\n .append('button')\n .on('click.rotate-cw', rotate(90))\n .text('\u293E');\n\n controlsEnter\n .append('button')\n .on('click.forward', step(1))\n .text('\u25BA');\n\n wrapEnter\n .append('div')\n .attr('class', 'kartaview-image-wrap');\n\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.kartaview', function(dimensions) {\n imgZoom\n .extent([[0, 0], dimensions])\n .translateExtent([[0, 0], dimensions]);\n });\n\n\n function zoomPan(d3_event) {\n var t = d3_event.transform;\n context.container().select('.photoviewer .kartaview-image-wrap')\n .call(utilSetTransform, t.x, t.y, t.k);\n }\n\n\n function rotate(deg) {\n return function() {\n if (!_oscSelectedImage) return;\n var sequenceKey = _oscSelectedImage.sequence_id;\n var sequence = _oscCache.sequences[sequenceKey];\n if (!sequence) return;\n\n var r = sequence.rotation || 0;\n r += deg;\n\n if (r > 180) r -= 360;\n if (r < -180) r += 360;\n sequence.rotation = r;\n\n var wrap = context.container().select('.photoviewer .kartaview-wrapper');\n\n wrap\n .transition()\n .duration(100)\n .call(imgZoom.transform, d3_zoomIdentity);\n\n wrap.selectAll('.kartaview-image')\n .transition()\n .duration(100)\n .style('transform', 'rotate(' + r + 'deg)');\n };\n }\n\n function step(stepBy) {\n return function() {\n if (!_oscSelectedImage) return;\n var sequenceKey = _oscSelectedImage.sequence_id;\n var sequence = _oscCache.sequences[sequenceKey];\n if (!sequence) return;\n\n var nextIndex = _oscSelectedImage.sequence_index + stepBy;\n var nextImage = sequence.images[nextIndex];\n if (!nextImage) return;\n\n context.map().centerEase(nextImage.loc);\n\n that\n .selectImage(context, nextImage.key);\n };\n }\n\n // don't need any async loading so resolve immediately\n _loadViewerPromise = Promise.resolve();\n\n return _loadViewerPromise;\n },\n\n\n showViewer: function(context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.kartaview-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.kartaview-wrapper')\n .classed('hide', false);\n }\n\n return this;\n },\n\n\n hideViewer: function(context) {\n _oscSelectedImage = null;\n\n this.updateUrlImage(null);\n\n var viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n return this.setStyles(context, null, true);\n },\n\n\n selectImage: function(context, imageKey) {\n\n var d = this.cachedImage(imageKey);\n\n _oscSelectedImage = d;\n\n this.updateUrlImage(imageKey);\n\n var viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null, true);\n\n context.container().selectAll('.icon-sign')\n .classed('currentView', false);\n\n if (!d) return this;\n\n var wrap = context.container().select('.photoviewer .kartaview-wrapper');\n var imageWrap = wrap.selectAll('.kartaview-image-wrap');\n var attribution = wrap.selectAll('.photo-attribution').text('');\n\n wrap\n .transition()\n .duration(100)\n .call(imgZoom.transform, d3_zoomIdentity);\n\n imageWrap\n .selectAll('.kartaview-image')\n .remove();\n\n if (d) {\n var sequence = _oscCache.sequences[d.sequence_id];\n var r = (sequence && sequence.rotation) || 0;\n\n imageWrap\n .append('img')\n .attr('class', 'kartaview-image')\n .attr('src', (apibase + '/' + d.imagePath).replace(/^https:\\/\\/kartaview\\.org\\/storage(\\d+)\\//, 'https://storage$1.openstreetcam.org/'))\n .style('transform', 'rotate(' + r + 'deg)');\n\n if (d.captured_by) {\n attribution\n .append('a')\n .attr('class', 'captured_by')\n .attr('target', '_blank')\n .attr('href', 'https://kartaview.org/user/' + encodeURIComponent(d.captured_by))\n .text('@' + d.captured_by);\n\n attribution\n .append('span')\n .text('|');\n }\n\n if (d.captured_at) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeDateString(d.captured_at));\n\n attribution\n .append('span')\n .text('|');\n }\n\n attribution\n .append('a')\n .attr('class', 'image-link')\n .attr('target', '_blank')\n .attr('href', 'https://kartaview.org/details/' + d.sequence_id + '/' + d.sequence_index)\n .text('kartaview.org');\n }\n\n return this;\n },\n\n\n getSelectedImage: function() {\n return _oscSelectedImage;\n },\n\n\n getSequenceKeyForImage: function(d) {\n return d && d.sequence_id;\n },\n\n\n // Updates the currently highlighted sequence and selected bubble.\n // Reset is only necessary when interacting with the viewport because\n // this implicitly changes the currently selected bubble/sequence\n setStyles: function(context, hovered, reset) {\n if (reset) { // reset all layers\n context.container().selectAll('.viewfield-group')\n .classed('highlighted', false)\n .classed('hovered', false)\n .classed('currentView', false);\n\n context.container().selectAll('.sequence')\n .classed('highlighted', false)\n .classed('currentView', false);\n }\n\n var hoveredImageId = hovered && hovered.key;\n var hoveredSequenceId = this.getSequenceKeyForImage(hovered);\n\n var viewer = context.container().select('.photoviewer');\n var selected = viewer.empty() ? undefined : viewer.datum();\n var selectedImageId = selected && selected.key;\n var selectedSequenceId = this.getSequenceKeyForImage(selected);\n\n // highlight sibling viewfields on either the selected or the hovered sequences\n context.container().selectAll('.layer-kartaview .viewfield-group')\n .classed('highlighted', function(d) { return d.sequence_id === selectedSequenceId || d.id === hoveredImageId; })\n .classed('hovered', function(d) { return d.key === hoveredImageId; })\n .classed('currentView', function(d) { return d.key === selectedImageId; });\n\n context.container().selectAll('.layer-kartaview .sequence')\n .classed('highlighted', function(d) { return d.properties.key === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.key === selectedSequenceId; });\n\n // update viewfields if needed\n context.container().selectAll('.layer-kartaview .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n var d = this.parentNode.__data__;\n if (d.pano && d.key !== selectedImageId) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n return this;\n },\n\n\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'kartaview/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n\n cache: function() {\n return _oscCache;\n }\n\n};\n", "import type { Feature, FeatureCollection, Geometry, Position } from 'geojson';\nimport whichPolygon from 'which-polygon';\nimport rawBorders from './data/borders.json';\n\ninterface RegionFeatureProperties {\n // Unique identifier specific to country-coder\n id: string;\n\n // ISO 3166-1 alpha-2 code\n iso1A2: string | undefined;\n\n // ISO 3166-1 alpha-3 code\n iso1A3: string | undefined;\n\n // ISO 3166-1 numeric-3 code\n iso1N3: string | undefined;\n\n // UN M49 code\n m49: string | undefined;\n\n // Wikidata QID\n wikidata: string;\n\n // The emoji flag sequence derived from this feature's ISO 3166-1 alpha-2 code\n emojiFlag: string | undefined;\n\n // The ccTLD (country code top-level domain)\n ccTLD: string | undefined;\n\n // The common English name\n nameEn: string;\n\n // Additional identifiers which can be used to look up this feature;\n // these cannot collide with the identifiers for any other feature\n aliases: Array | undefined;\n\n // For features entirely within a country, the ISO 3166-1 alpha-2 code for that country\n country: string | undefined;\n\n // The ISO 3166-1 alpha-2, M49, or QIDs of other features this feature is entirely within, including its country\n groups: Array;\n\n // The ISO 3166-1 alpha-2, M49, or QIDs of other features this feature contains;\n // the inverse of `groups`\n members: Array | undefined;\n\n // The rough geographic type of this feature.\n // Levels do not necessarily nest cleanly within each other.\n // - `world`: all features\n\n // - `unitedNations`: United Nations\n // - `union`: European Union\n // - `subunion`: Outermost Regions of the EU, Overseas Countries and Territories of the EU\n\n // Defined by the UN\n // - `region`: Africa, Americas, Antarctica, Asia, Europe, Oceania\n // - `subregion`: Sub-Saharan Africa, North America, Micronesia, etc.\n // - `intermediateRegion`: Eastern Africa, South America, Channel Islands, etc.\n\n // - `sharedLandform`: Great Britain, Macaronesia, Mariana Islands, etc.\n // - `country`: Ethiopia, Brazil, United States, etc.\n // - `subcountryGroup`\n // - `territory`: Puerto Rico, Gurnsey, Hong Kong, etc.\n // - `subterritory`: Sark, Ascension Island, Diego Garcia, etc.\n level: string;\n\n // The status of this feature's ISO 3166-1 code(s), if any\n // - `official`: officially-assigned\n // - `excRes`: exceptionally-reserved\n // - `usrAssn`: user-assigned\n isoStatus: string | undefined;\n\n // The side of the road that traffic drives on within this feature\n // - `right`\n // - `left`\n driveSide: 'left' | 'right' | undefined;\n\n // The unit used for road traffic speeds within this feature\n // - `mph`: miles per hour\n // - `km/h`: kilometers per hour\n roadSpeedUnit: 'mph' | 'km/h' | undefined;\n\n // The unit used for road vehicle height restrictions within this feature\n // - `ft`: feet and inches\n // - `m`: meters\n roadHeightUnit: 'ft' | 'm' | undefined;\n\n // The international calling codes for this feature, sometimes including area codes\n // e.g. `1`, `1 340`\n callingCodes: Array | undefined;\n};\n\ntype RegionFeature = Feature;\ntype RegionFeatureCollection = FeatureCollection;\ntype Vec2 = [number, number]; // [lon, lat]\ntype Bbox = [number, number, number, number]; // [minLon, minLat, maxLon, maxLat]\n\ninterface PointGeometry {\n type: string;\n coordinates: Vec2\n};\n\ninterface PointFeature {\n type: string;\n geometry: PointGeometry;\n properties: any\n};\ntype Location = Vec2 | PointGeometry | PointFeature;\n\ninterface CodingOptions {\n // For overlapping features, the division level of the one to get. If no feature\n // exists at the given level, the feature at the next higher level is returned.\n // See the `level` property of `RegionFeatureProperties` for possible values.\n level?: string | undefined;\n // Only a feature at the specified level or lower will be returned.\n maxLevel?: string | undefined;\n // Only a feature with the specified property will be returned.\n withProp?: string | undefined;\n};\n\n// The base GeoJSON feature collection\nexport const borders: RegionFeatureCollection = rawBorders as RegionFeatureCollection;\n\n// The whichPolygon interface for looking up a feature by point\nlet _whichPolygon: any = {};\n// The cache for looking up a feature by identifier\nconst _featuresByCode: any = {};\n\n// discard special characters and instances of and/the/of that aren't the only characters\nconst idFilterRegex =\n /(?=(?!^(and|the|of|el|la|de)$))(\\b(and|the|of|el|la|de)\\b)|[-_ .,'()&[\\]/]/gi;\n\nfunction canonicalID(id: string | null): string {\n const s = id || '';\n if (s.charAt(0) === '.') {\n // skip replace if it leads with a '.' (e.g. a ccTLD like '.de', '.la')\n return s.toUpperCase();\n } else {\n return s.replace(idFilterRegex, '').toUpperCase();\n }\n}\n\n// Geographic levels, roughly from most to least granular\nconst levels = [\n 'subterritory',\n 'territory',\n 'subcountryGroup',\n 'country',\n 'sharedLandform',\n 'intermediateRegion',\n 'subregion',\n 'region',\n 'subunion',\n 'union',\n 'unitedNations',\n 'world'\n];\n\nloadDerivedDataAndCaches(borders);\n\n// Loads implicit feature data and the getter index caches\nfunction loadDerivedDataAndCaches(borders) {\n const identifierProps = ['iso1A2', 'iso1A3', 'm49', 'wikidata', 'emojiFlag', 'ccTLD', 'nameEn'];\n const geometryFeatures: Array = [];\n\n for (const feature of borders.features) {\n // generate a unique ID for each feature\n const props = feature.properties;\n props.id = props.iso1A2 || props.m49 || props.wikidata;\n\n loadM49(feature);\n loadTLD(feature);\n loadIsoStatus(feature);\n loadLevel(feature);\n loadGroups(feature);\n loadFlag(feature);\n // cache only after loading derived IDs\n cacheFeatureByIDs(feature);\n\n if (feature.geometry) {\n geometryFeatures.push(feature);\n }\n }\n\n // must load `members` only after fully loading `featuresByID`\n for (const feature of borders.features) {\n // ensure all groups are listed by their ID\n feature.properties.groups = feature.properties.groups.map(groupID => {\n return _featuresByCode[groupID].properties.id;\n });\n loadMembersForGroupsOf(feature);\n }\n\n // must load attributes only after loading geometry features into `members`\n for (const feature of borders.features) {\n loadRoadSpeedUnit(feature);\n loadRoadHeightUnit(feature);\n loadDriveSide(feature);\n loadCallingCodes(feature);\n loadGroupGroups(feature);\n }\n\n for (const feature of borders.features) {\n // order groups by their `level`\n feature.properties.groups.sort((groupID1, groupID2) => {\n return (\n levels.indexOf(_featuresByCode[groupID1].properties.level) -\n levels.indexOf(_featuresByCode[groupID2].properties.level)\n );\n });\n // order members by their `level` and then by order in borders\n if (feature.properties.members) {\n feature.properties.members.sort((id1, id2) => {\n const diff =\n levels.indexOf(_featuresByCode[id1].properties.level) -\n levels.indexOf(_featuresByCode[id2].properties.level);\n if (diff === 0) {\n return (\n borders.features.indexOf(_featuresByCode[id1]) -\n borders.features.indexOf(_featuresByCode[id2])\n );\n }\n return diff;\n });\n }\n }\n\n // whichPolygon doesn't support null geometry even though GeoJSON does\n const geometryOnlyCollection: RegionFeatureCollection = {\n type: 'FeatureCollection',\n features: geometryFeatures\n };\n _whichPolygon = whichPolygon(geometryOnlyCollection);\n\n function loadGroups(feature: RegionFeature) {\n const props = feature.properties;\n if (!props.groups) {\n props.groups = [];\n }\n if (feature.geometry && props.country) {\n // Add `country` to `groups`\n props.groups.push(props.country);\n }\n if (props.m49 !== '001') { // all features are in the world feature except the world itself\n props.groups.push('001');\n }\n }\n\n function loadM49(feature: RegionFeature) {\n const props = feature.properties;\n if (!props.m49 && props.iso1N3) { // M49 is a superset of ISO numerics so we only need to store one\n props.m49 = props.iso1N3;\n }\n }\n\n function loadTLD(feature: RegionFeature) {\n const props = feature.properties;\n if (props.level === 'unitedNations') return; // `.un` is not a ccTLD\n if (props.ccTLD === null) return; // e.g. Sark is an ISO code but not a ccTLD\n if (!props.ccTLD && props.iso1A2) { // ccTLD is nearly the same as iso1A2, so we only need to explicitly code any exceptions\n props.ccTLD = '.' + props.iso1A2.toLowerCase();\n }\n }\n\n function loadIsoStatus(feature: RegionFeature) {\n const props = feature.properties;\n if (!props.isoStatus && props.iso1A2) { // Features with an ISO code but no explicit status are officially-assigned\n props.isoStatus = 'official';\n }\n }\n\n function loadLevel(feature: RegionFeature) {\n const props = feature.properties;\n if (props.level) return;\n if (!props.country) { // a feature without an explicit `level` or `country` is itself a country\n props.level = 'country';\n } else if (!props.iso1A2 || props.isoStatus === 'official') {\n props.level = 'territory';\n } else {\n props.level = 'subterritory';\n }\n }\n\n function loadGroupGroups(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry || !props.members) return;\n\n const featureLevelIndex = levels.indexOf(props.level);\n let sharedGroups: Array = [];\n\n props.members.forEach((memberID, index) => {\n const member = _featuresByCode[memberID];\n const memberGroups = member.properties.groups.filter((groupID) => {\n return (\n groupID !== feature.properties.id &&\n featureLevelIndex < levels.indexOf(_featuresByCode[groupID].properties.level)\n );\n });\n if (index === 0) {\n sharedGroups = memberGroups;\n } else {\n sharedGroups = sharedGroups.filter((groupID) => memberGroups.indexOf(groupID) !== -1);\n }\n });\n\n props.groups = props.groups.concat(\n sharedGroups.filter((groupID) => props.groups.indexOf(groupID) === -1)\n );\n\n for (const groupID of sharedGroups) {\n const groupFeature = _featuresByCode[groupID];\n if (groupFeature.properties.members.indexOf(props.id) === -1) {\n groupFeature.properties.members.push(props.id);\n }\n }\n }\n\n function loadRoadSpeedUnit(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry) {\n // only `mph` regions are listed explicitly, else assume `km/h`\n if (!props.roadSpeedUnit) props.roadSpeedUnit = 'km/h';\n } else if (props.members) {\n const vals = Array.from(\n new Set(\n props.members\n .map(id => {\n const member = _featuresByCode[id];\n if (member.geometry) {\n return member.properties.roadSpeedUnit || 'km/h';\n } else {\n return null;\n }\n })\n .filter(Boolean)\n )\n );\n // if all members have the same value then that's also the value for this feature\n if (vals.length === 1) props.roadSpeedUnit = vals[0];\n }\n }\n\n function loadRoadHeightUnit(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry) {\n // only `ft` regions are listed explicitly, else assume `m`\n if (!props.roadHeightUnit) props.roadHeightUnit = 'm';\n } else if (props.members) {\n const vals = Array.from(\n new Set(\n props.members\n .map(id => {\n const member = _featuresByCode[id];\n if (member.geometry) {\n return member.properties.roadHeightUnit || 'm';\n } else {\n return null;\n }\n })\n .filter(Boolean)\n )\n );\n // if all members have the same value then that's also the value for this feature\n if (vals.length === 1) props.roadHeightUnit = vals[0];\n }\n }\n\n function loadDriveSide(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry) {\n // only `left` regions are listed explicitly, else assume `right`\n if (!props.driveSide) props.driveSide = 'right';\n } else if (props.members) {\n const vals = Array.from(\n new Set(\n props.members\n .map(id => {\n const member = _featuresByCode[id];\n if (member.geometry) {\n return member.properties.driveSide || 'right';\n } else {\n return null;\n }\n })\n .filter(Boolean)\n )\n );\n // if all members have the same value then that's also the value for this feature\n if (vals.length === 1) props.driveSide = vals[0];\n }\n }\n\n function loadCallingCodes(feature: RegionFeature) {\n const props = feature.properties;\n if (!feature.geometry && props.members) {\n props.callingCodes = Array.from(\n new Set(\n props.members.reduce((array, id) => {\n const member = _featuresByCode[id];\n if (member.geometry && member.properties.callingCodes) {\n return array.concat(member.properties.callingCodes);\n }\n return array;\n }, [])\n )\n );\n }\n }\n\n // Calculates the emoji flag (if any) and caches it\n function loadFlag(feature: RegionFeature) {\n let flag = '';\n\n // Most emoji flags can be generated from their 2 letter code.\n // Skip 'FX' (Metropolitan France), allow it to roll up to 'FR' - #25\n const country = feature.properties.iso1A2;\n if (country && country !== 'FX') {\n flag = _toEmojiCountryFlag(country);\n }\n\n // Support a few regional flags - #157\n const regionStrings = {\n Q21: 'gbeng', // GB-ENG (England)\n Q22: 'gbsct', // GB-SCT (Scotland)\n Q25: 'gbwls' // GB-WLS (Wales)\n };\n const region = regionStrings[feature.properties.wikidata];\n if (region) {\n flag = _toEmojiRegionFlag(region);\n }\n\n if (flag) {\n feature.properties.emojiFlag = flag;\n }\n\n // Normally, take isoA2 chars and jump up into \"Enclosed Alphanumeric Supplement\" block\n // see https://en.wikipedia.org/wiki/Regional_indicator_symbol\n // see https://en.wikipedia.org/wiki/Enclosed_Alphanumeric_Supplement\n function _toEmojiCountryFlag(s: string): string {\n return s.replace(/./g, c => String.fromCodePoint(c.charCodeAt(0) + 0x1F1A5));\n }\n\n // Regional flags are encoded as U+1F3F4 (black flag) + the region string (jump up to \"Tags\" block) + U+E007F (end)\n // see https://en.wikipedia.org/wiki/Tags_(Unicode_block)\n function _toEmojiRegionFlag(s: string) {\n const codepoints = [0x1F3F4];\n for (const c of [...s]) {\n codepoints.push(c.codePointAt(0) as number + 0xE0000);\n }\n codepoints.push(0xE007F);\n return String.fromCodePoint.apply(null, codepoints);\n }\n }\n\n // Populate `members` as the inverse relationship of `groups`\n function loadMembersForGroupsOf(feature: RegionFeature) {\n for (const groupID of feature.properties.groups) {\n const groupFeature = _featuresByCode[groupID];\n\n if (!groupFeature.properties.members) {\n groupFeature.properties.members = [];\n }\n groupFeature.properties.members.push(feature.properties.id);\n }\n }\n\n // Caches features by their identifying strings for rapid lookup\n function cacheFeatureByIDs(feature: RegionFeature) {\n const ids: Array = [];\n\n for (const prop of identifierProps) {\n const id = feature.properties[prop];\n if (id) {\n ids.push(id);\n }\n }\n\n for (const alias of (feature.properties.aliases || [])) {\n ids.push(alias);\n }\n\n for (const id of ids) {\n const cid = canonicalID(id);\n _featuresByCode[cid] = feature;\n }\n }\n}\n\n// Returns the [longitude, latitude] for the location argument\nfunction locArray(loc: Location): Vec2 {\n if (Array.isArray(loc)) {\n return loc as Vec2;\n } else if ((loc as PointGeometry).coordinates) {\n return (loc as PointGeometry).coordinates;\n } else {\n return (loc as PointFeature).geometry.coordinates;\n }\n}\n\n// Returns the smallest feature of any kind containing `loc`, if any\nfunction smallestFeature(loc: Location): RegionFeature | null {\n const query = locArray(loc);\n const featureProperties: RegionFeatureProperties = _whichPolygon(query);\n if (!featureProperties) return null;\n return _featuresByCode[featureProperties.id];\n}\n\n// Returns the country feature containing `loc`, if any\nfunction countryFeature(loc: Location): RegionFeature | null {\n const feature = smallestFeature(loc);\n if (!feature) return null;\n\n // a feature without `country` but with geometry is itself a country\n const countryCode = feature.properties.country || feature.properties.iso1A2;\n return _featuresByCode[countryCode as string] || null;\n}\n\nconst defaultOpts = {\n level: undefined,\n maxLevel: undefined,\n withProp: undefined\n};\n\n// Returns the feature containing `loc` for the `opts`, if any\nfunction featureForLoc(loc: Location, opts: CodingOptions): RegionFeature | null {\n const targetLevel = opts.level || 'country';\n const maxLevel = opts.maxLevel || 'world';\n const withProp = opts.withProp;\n\n const targetLevelIndex = levels.indexOf(targetLevel);\n if (targetLevelIndex === -1) return null;\n\n const maxLevelIndex = levels.indexOf(maxLevel);\n if (maxLevelIndex === -1) return null;\n if (maxLevelIndex < targetLevelIndex) return null;\n\n if (targetLevel === 'country') {\n // attempt fast path for country-level coding\n const fastFeature = countryFeature(loc);\n if (fastFeature) {\n if (!withProp || fastFeature.properties[withProp]) {\n return fastFeature;\n }\n }\n }\n\n const features = featuresContaining(loc);\n\n const match = features.find((feature) => {\n const levelIndex = levels.indexOf(feature.properties.level);\n if (\n feature.properties.level === targetLevel ||\n // if no feature exists at the target level, return the first feature at the next level up\n (levelIndex > targetLevelIndex && levelIndex <= maxLevelIndex)\n ) {\n if (!withProp || feature.properties[withProp]) {\n return feature;\n }\n }\n return false;\n });\n\n return match || null;\n}\n\n// Returns the feature with an identifying property matching `id`, if any\nfunction featureForID(id: string | number): RegionFeature | null {\n let stringID: string;\n\n if (typeof id === 'number') {\n stringID = id.toString();\n if (stringID.length === 1) {\n stringID = '00' + stringID;\n } else if (stringID.length === 2) {\n stringID = '0' + stringID;\n }\n } else {\n stringID = canonicalID(id);\n }\n return _featuresByCode[stringID] || null;\n}\n\nfunction smallestFeaturesForBbox(bbox: Bbox): [RegionFeature] {\n return _whichPolygon.bbox(bbox).map(props => _featuresByCode[props.id]);\n}\n\nfunction smallestOrMatchingFeature(query: Location | string | number): RegionFeature | null {\n if (typeof query === 'object') {\n return smallestFeature(query as Location);\n }\n return featureForID(query);\n}\n\n// Returns the feature matching the given arguments, if any\nexport function feature(query: Location | string | number, opts: CodingOptions = defaultOpts): RegionFeature | null {\n if (typeof query === 'object') {\n return featureForLoc(query as Location, opts);\n }\n return featureForID(query);\n}\n\n// Returns the ISO 3166-1 alpha-2 code for the feature matching the arguments, if any\nexport function iso1A2Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'iso1A2';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.iso1A2 || null;\n}\n\n// Returns the ISO 3166-1 alpha-3 code for the feature matching the arguments, if any\nexport function iso1A3Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'iso1A3';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.iso1A3 || null;\n}\n\n// Returns the ISO 3166-1 numeric-3 code for the feature matching the arguments, if any\nexport function iso1N3Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'iso1N3';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.iso1N3 || null;\n}\n\n// Returns the UN M49 code for the feature matching the arguments, if any\nexport function m49Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'm49';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.m49 || null;\n}\n\n// Returns the Wikidata QID code for the feature matching the arguments, if any\nexport function wikidataQID(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'wikidata';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.wikidata;\n}\n\n// Returns the emoji emojiFlag sequence for the feature matching the arguments, if any\nexport function emojiFlag(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'emojiFlag';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.emojiFlag || null;\n}\n\n// Returns the ccTLD (country code top-level domain) for the feature matching the arguments, if any\nexport function ccTLD(\n query: Location | string | number,\n opts: CodingOptions = defaultOpts\n): string | null {\n opts.withProp = 'ccTLD';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.ccTLD || null;\n}\n\nfunction propertiesForQuery(query: Location | Bbox, property: string): Array {\n const features = featuresContaining(query, false);\n return features.map(feature => feature.properties[property]).filter(Boolean);\n}\n\n// Returns all the ISO 3166-1 alpha-2 codes of features at the location\nexport function iso1A2Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'iso1A2');\n}\n\n// Returns all the ISO 3166-1 alpha-3 codes of features at the location\nexport function iso1A3Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'iso1A3');\n}\n\n// Returns all the ISO 3166-1 numeric-3 codes of features at the location\nexport function iso1N3Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'iso1N3');\n}\n\n// Returns all the UN M49 codes of features at the location\nexport function m49Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'm49');\n}\n\n// Returns all the Wikidata QIDs of features at the location\nexport function wikidataQIDs(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'wikidata');\n}\n\n// Returns all the emoji flag sequences of features at the location\nexport function emojiFlags(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'emojiFlag');\n}\n\n// Returns all the ccTLD (country code top-level domain) sequences of features at the location\nexport function ccTLDs(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'ccTLD');\n}\n\n// Returns the feature matching `query` and all features containing it, if any.\n// If passing `true` for `strict`, an exact match will not be included\nexport function featuresContaining(query: Location | Bbox | string | number, strict?: boolean): Array {\n let matchingFeatures: Array;\n\n if (Array.isArray(query) && query.length === 4) { // check if bounding box\n matchingFeatures = smallestFeaturesForBbox(query as Bbox);\n } else {\n const smallestOrMatching = smallestOrMatchingFeature(query as Location | string | number);\n matchingFeatures = smallestOrMatching ? [smallestOrMatching] : [];\n }\n\n if (!matchingFeatures.length) return [];\n\n let returnFeatures: Array;\n if (!strict || typeof query === 'object') {\n returnFeatures = matchingFeatures.slice();\n } else {\n returnFeatures = [];\n }\n\n for (const feature of matchingFeatures) {\n const properties = feature.properties;\n for (const groupID of properties.groups) {\n const groupFeature = _featuresByCode[groupID];\n if (returnFeatures.indexOf(groupFeature) === -1) {\n returnFeatures.push(groupFeature);\n }\n }\n }\n\n return returnFeatures;\n}\n\n// Returns the feature matching `id` and all features it contains, if any.\n// If passing `true` for `strict`, an exact match will not be included\nexport function featuresIn(id: string | number, strict?: boolean): Array {\n const feature = featureForID(id);\n if (!feature) return [];\n\n const features: Array = [];\n if (!strict) {\n features.push(feature);\n }\n\n const properties = feature.properties;\n for (const memberID of (properties.members || [])) {\n features.push(_featuresByCode[memberID]);\n }\n\n return features;\n}\n\n// Returns a new feature with the properties of the feature matching `id`\n// and the combined geometry of all its component features\nexport function aggregateFeature(id: string | number): RegionFeature | null {\n const features = featuresIn(id, false);\n if (features.length === 0) return null;\n\n let aggregateCoordinates: Position[][][] = [];\n for (const feature of features) {\n if (feature.geometry?.type === 'MultiPolygon' && feature.geometry.coordinates) {\n aggregateCoordinates = aggregateCoordinates.concat(feature.geometry.coordinates);\n }\n }\n\n return {\n type: 'Feature',\n properties: features[0].properties,\n geometry: {\n type: 'MultiPolygon',\n coordinates: aggregateCoordinates\n }\n };\n}\n\n// Returns true if the feature matching `query` is, or is a part of, the feature matching `bounds`\nexport function isIn(query: Location | string | number, bounds: string | number): boolean | null {\n const queryFeature = smallestOrMatchingFeature(query);\n const boundsFeature = featureForID(bounds);\n\n if (!queryFeature || !boundsFeature) return null;\n\n if (queryFeature.properties.id === boundsFeature.properties.id) return true;\n return queryFeature.properties.groups.indexOf(boundsFeature.properties.id) !== -1;\n}\n\n// Returns true if the feature matching `query` is within EU jurisdiction\nexport function isInEuropeanUnion(query: Location | string | number): boolean | null {\n return isIn(query, 'EU');\n}\n\n// Returns true if the feature matching `query` is, or is within, a United Nations member state\nexport function isInUnitedNations(query: Location | string | number): boolean | null {\n return isIn(query, 'UN');\n}\n\n// Returns the side traffic drives on in the feature matching `query` as a string (`right` or `left`)\nexport function driveSide(query: Location | string | number): string | null {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.driveSide) || null;\n}\n\n// Returns the road speed unit for the feature matching `query` as a string (`mph` or `km/h`)\nexport function roadSpeedUnit(query: Location | string | number): string | null {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.roadSpeedUnit) || null;\n}\n\n// Returns the road vehicle height restriction unit for the feature matching `query` as a string (`ft` or `m`)\nexport function roadHeightUnit(query: Location | string | number): string | null {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.roadHeightUnit) || null;\n}\n\n// Returns the full international calling codes for phone numbers in the feature matching `query`, if any\nexport function callingCodes(query: Location | string | number): Array {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.callingCodes) || [];\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { utilRebind } from '../util';\n\n\nconst pannellumViewerCSS = 'pannellum/pannellum.css';\nconst pannellumViewerJS = 'pannellum/pannellum.js';\n\nexport async function pannellumPhotoFrame(context, selection) {\n const dispatch = d3_dispatch('viewerChanged');\n\n const module = {};\n module.event = utilRebind(module, dispatch, 'on');\n module.loadPannellum = function(context) {\n const head = d3_select('head');\n\n return Promise.all([\n new Promise((resolve, reject) => {\n // load pannellum viewer css\n head\n .selectAll('#ideditor-pannellum-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-pannellum-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(pannellumViewerCSS))\n .on('load.pannellum', resolve)\n .on('error.pannellum', reject);\n }),\n new Promise((resolve, reject) => {\n // load pannellum viewer js\n head\n .selectAll('#ideditor-pannellum-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-pannellum-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(pannellumViewerJS))\n .on('load.pannellum', resolve)\n .on('error.pannellum', reject);\n })\n ]);\n };\n\n let _currScenes = [];\n let _pannellumViewer;\n let _activeSceneKey;\n\n selection\n .append('div')\n .attr('class', 'photo-frame pannellum-frame')\n .attr('id', 'ideditor-pannellum-viewer')\n .classed('hide', true)\n .on('mousedown', function(e) { e.stopPropagation(); });\n\n if (!window.pannellum) {\n await module.loadPannellum(context);\n }\n\n const options = {\n 'default': { firstScene: '' },\n scenes: {},\n minHfov: 20,\n disableKeyboardCtrl: true,\n sceneFadeDuration: 0\n };\n\n _pannellumViewer = window.pannellum.viewer('ideditor-pannellum-viewer', options);\n\n _pannellumViewer\n .on('mousedown', () => d3_select(window)\n .on('pointermove.pannellum mousemove.pannellum', () => dispatch.call('viewerChanged')))\n .on('mouseup', () => d3_select(window)\n .on('pointermove.pannellum mousemove.pannellum', null))\n .on('animatefinished', () => dispatch.call('viewerChanged'));\n\n context.ui().photoviewer.on('resize.pannellum', () => {\n _pannellumViewer.resize();\n });\n\n /**\n * Shows the photo frame if hidden\n * @param {*} context the HTML wrap of the frame\n */\n module.showPhotoFrame = function(context) {\n const isHidden = context.selectAll('.photo-frame.pannellum-frame.hide').size();\n\n if (isHidden) {\n context\n .selectAll('.photo-frame:not(.pannellum-frame)')\n .classed('hide', true);\n\n context\n .selectAll('.photo-frame.pannellum-frame')\n .classed('hide', false);\n }\n\n return module;\n };\n\n /**\n * Hides the photo frame if shown\n * @param {*} context the HTML wrap of the frame\n */\n module.hidePhotoFrame = function(viewerContext) {\n viewerContext\n .select('photo-frame.pannellum-frame')\n .classed('hide', false);\n\n return module;\n };\n\n /**\n * Renders an image inside the frame\n * @param {*} data the image data, it should contain an image_path attribute, a link to the actual image.\n * @param {boolean} keepOrientation if true, HFOV, pitch and yaw will be kept between images\n */\n module.selectPhoto = function(data, keepOrientation) {\n const key = data.image_path;\n _activeSceneKey = key;\n if (!_currScenes.includes(key)) {\n let newSceneOptions = {\n showFullscreenCtrl: false,\n autoLoad: false,\n compass: false,\n yaw: 0,\n type: 'equirectangular',\n preview: data.preview_path,\n panorama: data.image_path,\n northOffset: data.ca\n };\n\n _currScenes.push(key);\n _pannellumViewer.addScene(key, newSceneOptions);\n }\n\n let yaw = 0;\n let pitch = 0;\n let hfov = 0;\n\n if (keepOrientation) {\n yaw = module.getYaw();\n pitch = module.getPitch();\n hfov = module.getHfov();\n }\n if (_pannellumViewer.isLoaded() !== false) {\n _pannellumViewer.loadScene(key, pitch, yaw, hfov);\n dispatch.call('viewerChanged');\n } else {\n // pannellum is currently loading another scene: wait for it to finish\n // loading the previous panorama first\n const retry = setInterval(() => {\n if (_pannellumViewer.isLoaded() === false) {\n // still not done: wait a bit longer\n return;\n }\n if (_activeSceneKey === key) {\n // only load scene if no other photo has been selected in the meantime\n _pannellumViewer.loadScene(key, pitch, yaw, hfov);\n dispatch.call('viewerChanged');\n }\n clearInterval(retry);\n }, 100);\n }\n\n if (_currScenes.length > 3) {\n const old_key = _currScenes.shift();\n _pannellumViewer.removeScene(old_key);\n }\n\n _pannellumViewer.resize();\n\n return module;\n };\n\n module.getYaw = function() {\n return _pannellumViewer.getYaw();\n };\n\n module.getPitch = function() {\n return _pannellumViewer.getPitch();\n };\n\n module.getHfov = function() {\n return _pannellumViewer.getHfov();\n };\n\n return module;\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\nimport { utilSetTransform, utilRebind } from '../util';\n\n\nexport async function planePhotoFrame(context, selection) {\n const dispatch = d3_dispatch('viewerChanged');\n\n const module = {};\n module.event = utilRebind(module, dispatch, 'on');\n\n let _photo;\n let _imageWrapper;\n let _planeWrapper;\n let _viewerDimensions = [];\n let _photoDimensions = [];\n const _imgZoom = d3_zoom()\n .on('zoom', zoomPan)\n .on('start', () => _imageWrapper.classed('grabbing', true))\n .on('end', () => _imageWrapper.classed('grabbing', false));\n\n function zoomPan(d3_event) {\n let t = d3_event.transform;\n _imageWrapper.call(utilSetTransform, t.x, t.y, t.k);\n }\n\n function loadImage(selection, path) {\n return new Promise((resolve) => {\n selection.attr('src', path);\n selection.on('load', () => {\n resolve(selection);\n });\n });\n }\n\n function updateTransform() {\n const xScale = _viewerDimensions[0] / _photoDimensions[0];\n const yScale = _viewerDimensions[1] / _photoDimensions[1];\n const fitScale = Math.max(xScale, yScale);\n const minScale = Math.min(xScale, yScale);\n _imgZoom\n .extent([[0, 0], _viewerDimensions])\n .translateExtent([[0, 0], _photoDimensions])\n .scaleExtent([minScale, 4]);\n const centerOffset = [0, 0];\n if (xScale < yScale) {\n centerOffset[0] = (_viewerDimensions[0] / fitScale - _photoDimensions[0]) / 2;\n } else {\n centerOffset[1] = (_viewerDimensions[1] / fitScale - _photoDimensions[1]) / 2;\n }\n const transform = d3_zoomIdentity.scale(fitScale).translate(centerOffset[0], centerOffset[1]);\n _planeWrapper.call(_imgZoom.transform, transform);\n }\n\n _planeWrapper = selection.append('div')\n .classed('plane-frame-wrapper', true);\n _planeWrapper.call(_imgZoom);\n\n _imageWrapper = _planeWrapper\n .append('div')\n .classed('photo-frame', true)\n .classed('plane-frame', true)\n .classed('hide', true);\n\n _photo = _imageWrapper\n .append('img')\n .attr('class', 'plane-photo');\n\n context.ui().photoviewer.on('resize.plane', function(dimensions) {\n _viewerDimensions = dimensions;\n updateTransform();\n });\n\n await Promise.resolve();\n\n /**\n * Shows the photo frame if hidden\n * @param {*} selection the HTML wrap of the frame\n */\n module.showPhotoFrame = function(selection) {\n const isHidden = selection.selectAll('.photo-frame.plane-frame.hide').size();\n\n if (isHidden) {\n selection\n .selectAll('.photo-frame:not(.plane-frame)')\n .classed('hide', true);\n\n selection\n .selectAll('.photo-frame.plane-frame')\n .classed('hide', false);\n }\n\n // set initial viewer size\n _viewerDimensions = context.ui().photoviewer.viewerSize();\n updateTransform();\n\n return module;\n };\n\n /**\n * Hides the photo frame if shown\n * @param {*} context the HTML wrap of the frame\n */\n module.hidePhotoFrame = function(context) {\n context\n .select('photo-frame.plane-frame')\n .classed('hide', false);\n\n return module;\n };\n\n /**\n * Renders an image inside the frame\n * @param {*} data the image data, it should contain an image_path attribute, a link to the actual image.\n */\n module.selectPhoto = function(data) {\n dispatch.call('viewerChanged');\n\n loadImage(_photo, '');\n _planeWrapper.classed('show-loader', true);\n loadImage(_photo, data.image_path)\n .then(selection => {\n _planeWrapper.classed('show-loader', false);\n const { naturalWidth, naturalHeight } = selection.node();\n _photoDimensions = [naturalWidth, naturalHeight];\n updateTransform();\n });\n return module;\n };\n\n module.getYaw = function() {\n return 0;\n };\n\n return module;\n};\n", "import { json as d3_json, xml as d3_xml} from 'd3-fetch';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { pairs as d3_pairs } from 'd3-array';\nimport RBush from 'rbush';\nimport { iso1A2Codes } from '@rapideditor/country-coder';\nimport { t } from '../core/localizer';\nimport { utilQsString, utilTiler, utilRebind, utilArrayUnion, utilStringQs } from '../util';\nimport { searchLimited } from '../util/partition';\nimport { localeTimestamp } from '../util/date';\nimport { geoExtent, geoVecAngle, geoVecEqual } from '../geo';\nimport { pannellumPhotoFrame } from './pannellum_photo';\nimport { planePhotoFrame } from './plane_photo';\nimport { services } from './';\n\n\nconst owsEndpoint = 'https://www.vegvesen.no/kart/ogc/vegbilder_1_0/ows?';\nconst tileZoom = 14;\nconst tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);\nconst dispatch = d3_dispatch('loadedImages', 'viewerChanged');\nconst directionEnum = Object.freeze({\n forward: Symbol(0),\n backward: Symbol(1)\n});\n\nlet _planeFrame;\nlet _pannellumFrame;\nlet _currentFrame;\nlet _loadViewerPromise;\nlet _vegbilderCache;\n\nasync function fetchAvailableLayers() {\n const params = {\n service: 'WFS',\n request: 'GetCapabilities',\n version: '2.0.0',\n };\n\n const urlForRequest = owsEndpoint + utilQsString(params);\n const response = await d3_xml(urlForRequest);\n const regexMatcher = /^vegbilder_1_0:Vegbilder(?_360)?_(?\\d{4})$/;\n const availableLayers = [];\n for (const node of response.querySelectorAll('FeatureType > Name')) {\n const match = node.textContent?.match(regexMatcher);\n if (match) {\n availableLayers.push({\n name: match[0],\n is_sphere: !!match.groups?.image_type,\n year: parseInt(match.groups?.year, 10)\n });\n }\n }\n return availableLayers;\n}\n\nfunction filterAvailableLayers(photoContex) {\n const fromDateString = photoContex.fromDate();\n const toDateString = photoContex.toDate();\n const fromYear = fromDateString ? new Date(fromDateString).getFullYear() : 2016;\n const toYear = toDateString ? new Date(toDateString).getFullYear() : null;\n const showsFlat = photoContex.showsFlat();\n const showsPano = photoContex.showsPanoramic();\n return Array.from(_vegbilderCache.wfslayers.values()).filter(({layerInfo}) => (\n (layerInfo.year >= fromYear) &&\n (!toYear || (layerInfo.year <= toYear)) &&\n ((!layerInfo.is_sphere && showsFlat) || (layerInfo.is_sphere && showsPano))\n ));\n}\n\nfunction loadWFSLayers(projection, margin, wfslayers) {\n const tiles = tiler.margin(margin).getTiles(projection);\n for (const cache of wfslayers) {\n loadWFSLayer(projection, cache, tiles);\n }\n}\n\nfunction loadWFSLayer(projection, cache, tiles) {\n // abort inflight requests that are no longer needed\n for (const [key, controller] of cache.inflight.entries()) {\n const wanted = tiles.some(tile => key === tile.id);\n if (!wanted) {\n controller.abort();\n cache.inflight.delete(key);\n }\n }\n\n Promise.all(tiles.map(\n tile => loadTile(cache, cache.layerInfo.name, tile)\n )).then(() => orderSequences(projection, cache));\n}\n\n/**\n* loadNextTilePage() load data for the next tile page in line.\n*/\nasync function loadTile(cache, typename, tile) {\n const bbox = tile.extent.bbox();\n const tileid = tile.id;\n if ((cache.loaded.get(tileid) === true) || cache.inflight.has(tileid)) return;\n\n const params = {\n service: 'WFS',\n request: 'GetFeature',\n version: '2.0.0',\n typenames: typename,\n bbox: [bbox.minY, bbox.minX, bbox.maxY, bbox.maxX].join(','),\n outputFormat: 'json'\n };\n\n const controller = new AbortController();\n cache.inflight.set(tileid, controller);\n\n const options = {\n method: 'GET',\n signal: controller.signal,\n };\n\n const urlForRequest = owsEndpoint + utilQsString(params);\n\n let featureCollection;\n try {\n featureCollection = await d3_json(urlForRequest, options);\n } catch {\n cache.loaded.set(tileid, false);\n return;\n } finally {\n cache.inflight.delete(tileid);\n }\n\n cache.loaded.set(tileid, true);\n\n if (featureCollection.features.length === 0) { return; }\n\n const features = featureCollection.features.map(feature => {\n const loc = feature.geometry.coordinates;\n const key = feature.id;\n const properties = feature.properties;\n const {\n RETNING: ca,\n TIDSPUNKT: captured_at,\n URL: image_path,\n URLPREVIEW : preview_path,\n BILDETYPE: image_type,\n METER: metering,\n FELTKODE: lane_code\n } = properties;\n const lane_number = parseInt((lane_code.match(/^[0-9]+/) || [])[0], 10);\n const direction = lane_number % 2 === 0 ? directionEnum.backward : directionEnum.forward;\n const data = {\n service: 'photo',\n loc,\n key,\n ca,\n image_path,\n preview_path,\n road_reference: roadReference(properties),\n metering,\n lane_code,\n direction,\n captured_at: new Date(captured_at),\n is_sphere: image_type === '360'\n };\n\n cache.points.set(key, data);\n\n return {\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data\n };\n });\n\n _vegbilderCache.rtree.load(features);\n dispatch.call('loadedImages');\n}\n\nfunction orderSequences(projection, cache) {\n const {points} = cache;\n\n const grouped = Array.from(points.values()).reduce((grouped, image) => {\n const key = image.road_reference;\n if (grouped.has(key)) {\n grouped.get(key).push(image);\n } else {\n grouped.set(key, [image]);\n }\n return grouped;\n }, new Map());\n\n const imageSequences = Array.from(grouped.values()).reduce((imageSequences, imageGroup) => {\n imageGroup.sort((a, b) => {\n if (a.captured_at.valueOf() > b.captured_at.valueOf()) {\n return 1;\n } else if (a.captured_at.valueOf() < b.captured_at.valueOf()) {\n return -1;\n } else {\n const {direction} = a;\n if (direction === directionEnum.forward) {\n return a.metering - b.metering;\n } else {\n return b.metering - a.metering;\n }\n }\n });\n let imageSequence = [imageGroup[0]];\n let angle = null;\n for (const [lastImage, image] of d3_pairs(imageGroup)) {\n if (lastImage.ca === null) {\n const b = projection(lastImage.loc);\n const a = projection(image.loc);\n if (!geoVecEqual(a, b)) {\n angle = geoVecAngle(a, b);\n angle *= (180 / Math.PI);\n angle -= 90;\n angle = angle >= 0 ? angle : angle + 360;\n }\n lastImage.ca = angle;\n }\n if (\n image.direction === lastImage.direction &&\n image.captured_at.valueOf() - lastImage.captured_at.valueOf() <= 20000\n ) {\n imageSequence.push(image);\n } else {\n imageSequences.push(imageSequence);\n imageSequence = [image];\n }\n }\n imageSequences.push(imageSequence);\n return imageSequences;\n }, []);\n\n cache.sequences = imageSequences.map(images => {\n const sequence = {\n images,\n key: images[0].key,\n geometry : {\n type : 'LineString',\n coordinates : images.map(image => image.loc)\n }\n };\n for (const image of images) {\n _vegbilderCache.image2sequence_map.set(image.key, sequence);\n }\n return sequence;\n });\n}\n\nfunction roadReference(properties) {\n const {\n FYLKENUMMER: county_number,\n VEGKATEGORI: road_class,\n VEGSTATUS: road_status,\n VEGNUMMER: road_number,\n STREKNING: section,\n DELSTREKNING: subsection,\n HP: parcel,\n KRYSSDEL: junction_part,\n SIDEANLEGGSDEL: services_part,\n ANKERPUNKT: anker_point,\n AAR: year,\n } = properties;\n\n let reference;\n\n if (year >= 2020) {\n reference = `${road_class}${road_status}${road_number} S${section}D${subsection}`;\n if (junction_part) {\n reference = `${reference} M${anker_point} KD${junction_part}`;\n } else if (services_part) {\n reference = `${reference} M${anker_point} SD${services_part}`;\n }\n } else {\n reference = `${county_number}${road_class}${road_status}${road_number} HP${parcel}`;\n }\n\n return reference;\n}\n\nexport default {\n\n init: function () {\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset: async function () {\n if (_vegbilderCache) {\n for (const layer of _vegbilderCache.wfslayers.values()) {\n for (const controller of layer.inflight.values()) {\n controller.abort();\n }\n }\n }\n\n _vegbilderCache = {\n wfslayers: new Map(),\n rtree: new RBush(),\n image2sequence_map: new Map()\n };\n\n const availableLayers = await fetchAvailableLayers();\n const {wfslayers} = _vegbilderCache;\n\n for (const layerInfo of availableLayers) {\n const cache = {\n layerInfo,\n loaded: new Map(),\n inflight: new Map(),\n points: new Map(),\n sequences: []\n };\n wfslayers.set(layerInfo.name, cache);\n }\n },\n\n images: function (projection) {\n const limit = 5;\n return searchLimited(limit, projection, _vegbilderCache.rtree);\n },\n\n\n sequences: function (projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const seen = new Set();\n const line_strings = [];\n\n for (const {data} of _vegbilderCache.rtree.search(bbox)) {\n const sequence = _vegbilderCache.image2sequence_map.get(data.key);\n if (!sequence) continue;\n const {key, geometry, images} = sequence;\n if (seen.has(key)) continue;\n seen.add(key);\n const line = {\n type: 'LineString',\n coordinates: geometry.coordinates,\n key,\n images\n };\n line_strings.push(line);\n }\n return line_strings;\n },\n\n cachedImage: function (key) {\n for (const {points} of _vegbilderCache.wfslayers.values()) {\n if (points.has(key)) return points.get(key);\n }\n },\n\n getSequenceForImage: function (image) {\n return _vegbilderCache?.image2sequence_map.get(image?.key);\n },\n\n loadImages: async function (context, margin) {\n if (!_vegbilderCache) {\n await this.reset();\n }\n margin ??= 1;\n const wfslayers = filterAvailableLayers(context.photos());\n loadWFSLayers(context.projection, margin, wfslayers);\n },\n\n photoFrame: function() {\n return _currentFrame;\n },\n\n ensureViewerLoaded: function(context) {\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n const step = (stepBy) => () => {\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n if (!selected) return;\n\n const sequence = this.getSequenceForImage(selected);\n const nextIndex = sequence.images.indexOf(selected) + stepBy;\n const nextImage = sequence.images[nextIndex];\n\n if (!nextImage) return;\n\n context.map().centerEase(nextImage.loc);\n this.selectImage(context, nextImage.key, true);\n };\n\n const wrap = context.container().select('.photoviewer')\n .selectAll('.vegbilder-wrapper')\n .data([0]);\n\n const wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper vegbilder-wrapper')\n .classed('hide', true);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n const controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls');\n\n controlsEnter\n .append('button')\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .on('click.forward', step(1))\n .text('\u25BA');\n\n _loadViewerPromise = Promise.all([\n pannellumPhotoFrame(context, wrapEnter),\n planePhotoFrame(context, wrapEnter)\n ]).then(([pannellumPhotoFrame, planePhotoFrame]) => {\n _pannellumFrame = pannellumPhotoFrame;\n _pannellumFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n _planeFrame = planePhotoFrame;\n _planeFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n });\n\n return _loadViewerPromise;\n },\n\n selectImage: function(context, key, keepOrientation) {\n const d = this.cachedImage(key);\n this.updateUrlImage(key);\n\n const viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) { viewer.datum(d); }\n\n this.setStyles(context, null, true);\n\n if (!d) return this;\n\n const wrap = context.container().select('.photoviewer .vegbilder-wrapper');\n const attribution = wrap.selectAll('.photo-attribution').text('');\n\n if (d.captured_at) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeTimestamp(d.captured_at));\n }\n\n attribution\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://vegvesen.no')\n .call(t.append('vegbilder.publisher'));\n\n attribution\n .append('a')\n .attr('target', '_blank')\n .attr('href', `https://vegbilder.atlas.vegvesen.no/?year=${d.captured_at.getFullYear()}&lat=${d.loc[1]}&lng=${d.loc[0]}&view=image&imageId=${d.key}`)\n .call(t.append('vegbilder.view_on'));\n\n _currentFrame = d.is_sphere? _pannellumFrame : _planeFrame;\n\n _currentFrame\n .showPhotoFrame(wrap)\n .selectPhoto(d, keepOrientation);\n\n return this;\n },\n\n showViewer: function (context) {\n const viewer = context.container().select('.photoviewer');\n const isHidden = viewer.selectAll('.photo-wrapper.vegbilder-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n viewer\n .classed('hide', false)\n .selectAll('.photo-wrapper.vegbilder-wrapper')\n .classed('hide', false);\n }\n return this;\n },\n\n hideViewer: function(context) {\n this.updateUrlImage(null);\n\n const viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence')\n .classed('currentView', false);\n\n return this.setStyles(context, null, true);\n },\n\n\n // Updates the currently highlighted sequence and selected bubble.\n // Reset is only necessary when interacting with the viewport because\n // this implicitly changes the currently selected bubble/sequence\n setStyles: function (context, hovered, reset) {\n if (reset) { // reset all layers\n context.container().selectAll('.viewfield-group')\n .classed('highlighted', false)\n .classed('hovered', false)\n .classed('currentView', false);\n\n context.container().selectAll('.sequence')\n .classed('highlighted', false)\n .classed('currentView', false);\n }\n\n const hoveredImageKey = hovered?.key;\n const hoveredSequence = this.getSequenceForImage(hovered);\n const hoveredSequenceKey = hoveredSequence?.key;\n const hoveredImageKeys = hoveredSequence?.images.map(d => d.key) ?? [];\n\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n const selectedImageKey = selected?.key;\n const selectedSequence = this.getSequenceForImage(selected);\n const selectedSequenceKey = selectedSequence?.key;\n const selectedImageKeys = selectedSequence?.images.map(d => d.key) ?? [];\n\n // highlight sibling viewfields on either the selected or the hovered sequences\n const highlightedImageKeys = utilArrayUnion(hoveredImageKeys, selectedImageKeys);\n\n context.container().selectAll('.layer-vegbilder .viewfield-group')\n .classed('highlighted', d => highlightedImageKeys.indexOf(d.key) !== -1)\n .classed('hovered', d => d.key === hoveredImageKey)\n .classed('currentView', d => d.key === selectedImageKey);\n\n context.container().selectAll('.layer-vegbilder .sequence')\n .classed('highlighted', d => d.key === hoveredSequenceKey)\n .classed('currentView', d => d.key === selectedSequenceKey);\n\n // update viewfields if needed\n context.container().selectAll('.layer-vegbilder .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n const d = this.parentNode.__data__;\n if (d.is_sphere && d.key !== selectedImageKey) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n return this;\n },\n\n updateUrlImage: function (key) {\n const hash = utilStringQs(window.location.hash);\n if (key) {\n hash.photo = 'vegbilder/' + key;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n validHere: function(extent) {\n const bbox = Object.values(extent.bbox());\n return iso1A2Codes(bbox).includes('NO');\n },\n\n\n cache: function () {\n return _vegbilderCache;\n }\n\n};\n", "\n/**\n * osmAuth\n * Easy authentication with OpenStreetMap over OAuth 2.0.\n * @module\n *\n * @param o `Object` containing options:\n * @param o.scope OAuth2 scopes requested (e.g. \"read_prefs write_api\")\n * @param o.client_id OAuth2 client ID\n * @param o.redirect_uri OAuth2 redirect URI (e.g. \"http://127.0.0.1:8080/land.html\")\n * @param o.access_token Can pre-authorize with an OAuth2 bearer token if you have one\n * @param o.apiUrl A base url for the OSM API (default: \"https://api.openstreetmap.org\")\n * @param o.url A base url for the OAuth2 handshake (default: \"https://www.openstreetmap.org\")\n * @param o.auto If `true`, attempt to authenticate automatically when calling `.xhr()` or `.fetch()` (default: `false`)\n * @param o.singlepage If `true`, use page redirection instead of a popup (default: `false`)\n * @param o.loading Function called when auth-related xhr calls start\n * @param o.done Function called when auth-related xhr calls end\n * @param o.locale The locale to use on the OAuth2 authentication page. Optional.\n * @return `self`\n */\nexport function osmAuth(o) {\n var oauth = {};\n\n var CHANNEL_ID = 'osm-api-auth-complete';\n\n // Mock localStorage if needed.\n // Note that accessing localStorage may throw a `SecurityError`, so wrap in a try/catch.\n var _store = null;\n try {\n if (!('localStorage' in globalThis)) {\n throw new Error('No localStorage');\n }\n _store = globalThis.localStorage;\n\n } catch (e) {\n var _mock = new Map();\n _store = {\n isMocked: true,\n hasItem: (k) => _mock.has(k),\n getItem: (k) => _mock.get(k),\n setItem: (k, v) => _mock.set(k, v),\n removeItem: (k) => _mock.delete(k),\n clear: () => _mock.clear()\n };\n }\n\n /**\n * token\n * Get/Set tokens. These are prefixed with the base URL so that `osm-auth`\n * can be used with multiple APIs and the keys in `localStorage` will not clash\n * @param {string} k key\n * @param {string?} v value\n * @return {string?} If getting, returns the stored value or `null`. If setting, returns `undefined`.\n */\n function token(k, v) {\n var key = o.url + k;\n if (arguments.length === 1) {\n var val = _store.getItem(key) || '';\n // Note: legacy tokens might be wrapped in double quotes - remove them, see #129\n return val.replace(/\"/g, '');\n\n } else if (arguments.length === 2) {\n if (v) {\n return _store.setItem(key, v);\n } else {\n return _store.removeItem(key);\n }\n }\n }\n\n\n /**\n * authenticated\n * Test whether the user is currently authenticated\n *\n * @return {boolean} `true` if authenticated, `false` if not\n */\n oauth.authenticated = function() {\n return !!token('oauth2_access_token');\n };\n\n\n /**\n * logout\n * Removes any stored authentication tokens (legacy OAuth1 tokens too)\n *\n * @return `self`\n */\n oauth.logout = function () {\n token('oauth2_access_token', ''); // OAuth2\n token('oauth_token', ''); // OAuth1\n token('oauth_token_secret', ''); // OAuth1\n token('oauth_request_token_secret', ''); // OAuth1\n return oauth;\n };\n\n\n /**\n * authenticate\n * First logs out, then runs the authentiation flow, finally calls the callback.\n * TODO: detect lack of click event (probably can settimeout it)\n *\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @param {LoginOptions} [options] Other options\n * @return none\n */\n oauth.authenticate = function(callback, options) {\n if (oauth.authenticated()) {\n callback(null, oauth);\n return;\n }\n\n oauth.logout();\n\n _preopenPopup(function(error, popup) {\n if (error) {\n callback(error);\n } else {\n _generatePkceChallenge(function(pkce) {\n _authenticate(pkce, options, popup, callback);\n });\n }\n });\n };\n\n\n /**\n * authenticateAsync\n * Promisified version of `authenticate`\n * @param {LoginOptions} [options]\n * @return {Promise} Promise settled with whatever `_authenticate` did\n */\n oauth.authenticateAsync = function(options) {\n if (oauth.authenticated()) {\n return Promise.resolve(oauth);\n }\n\n oauth.logout();\n\n return new Promise((resolve, reject) => {\n var errback = (err, result) => {\n if (err) {\n reject(err);\n } else {\n resolve(result);\n }\n };\n\n _preopenPopup((error, popup) => {\n if (error) {\n errback(error);\n } else {\n _generatePkceChallenge(pkce => _authenticate(pkce, options, popup, errback));\n }\n });\n });\n };\n\n\n /**\n * opens an empty popup to be later used for the authentication page\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @return none\n */\n function _preopenPopup(callback) {\n if (o.singlepage) {\n callback(null, undefined);\n return;\n }\n\n // Create a 550x610 popup window in the center of the screen\n var w = 550;\n var h = 610;\n var settings = [\n ['width', w],\n ['height', h],\n ['left', window.screen.width / 2 - w / 2],\n ['top', window.screen.height / 2 - h / 2],\n ]\n .map(function (x) { return x.join('='); })\n .join(',');\n var popup = window.open('about:blank', 'oauth_window', settings);\n if (popup) {\n callback(null, popup);\n } else {\n var error = new Error('Popup was blocked');\n error.status = 'popup-blocked';\n callback(error);\n }\n }\n\n\n /**\n * _authenticate\n * internal authenticate\n *\n * @typedef {{ switchUser?: boolean }} LoginOptions\n *\n * @param {Object} pkce Object containing PKCE code challenge properties\n * @param {LoginOptions=} options Other options\n * @param {Window} popup Popup Window to use for the authentication page, should be undefined when using singlepage mode\n * @param {function} callback Errback-style callback that accepts `(err, result)`\n */\n function _authenticate(pkce, options, popup, callback) {\n var state = generateState();\n\n // ## Request authorization to access resources from the user\n // and receive authorization code\n var path =\n '/oauth2/authorize?' +\n utilQsString({\n client_id: o.client_id,\n redirect_uri: o.redirect_uri,\n response_type: 'code',\n scope: o.scope,\n state: state,\n code_challenge: pkce.code_challenge,\n code_challenge_method: pkce.code_challenge_method,\n locale: o.locale || '',\n });\n\n var url = options?.switchUser\n ? `${o.url}/logout?referer=${encodeURIComponent(`/login?referer=${encodeURIComponent(path)}`)}`\n : o.url + path;\n\n if (o.singlepage) {\n if (_store.isMocked) {\n // in singlepage mode, PKCE requires working non-volatile storage\n var error = new Error('localStorage unavailable, but required in singlepage mode');\n error.status = 'pkce-localstorage-unavailable';\n callback(error);\n return;\n }\n var params = utilStringQs(window.location.search.slice(1));\n if (params.code) {\n oauth.bootstrapToken(params.code, callback);\n } else {\n // save OAuth2 state and PKCE challenge in local storage, for later use\n // in the `/oauth/token` request\n token('oauth2_state', state);\n token('oauth2_pkce_code_verifier', pkce.code_verifier);\n window.location = url;\n }\n } else {\n oauth.popupWindow = popup;\n popup.location = url;\n }\n\n // Called by a function in the redirect URL page, in the popup window. The\n // window closes itself.\n var bc = new BroadcastChannel(CHANNEL_ID);\n bc.addEventListener('message', (event) => {\n var url = event.data;\n var params = utilStringQs(url.split('?')[1]);\n if (params.state !== state) {\n var error = new Error('Invalid state');\n error.status = 'invalid-state';\n callback(error);\n return;\n } else if (params.error !== undefined) {\n var err = new Error(params.error_description.replace(/\\+/g, ' '));\n err.status = params.error;\n callback(err);\n return;\n }\n _getAccessToken(params.code, pkce.code_verifier, accessTokenDone);\n bc.close();\n });\n\n function accessTokenDone(err, xhr) {\n o.done();\n if (err) {\n callback(err);\n return;\n }\n var access_token = JSON.parse(xhr.response);\n token('oauth2_access_token', access_token.access_token);\n callback(null, oauth);\n }\n }\n\n\n /**\n * _getAccessToken\n * The client requests an access token by authenticating with the\n * authorization server and presenting the `auth_code`, brought\n * in from a function call on a landing page popup.\n * @param {string} auth_code\n * @param {string} code_verifier\n * @param {function} accessTokenDone Errback-style callback `(err, result)`, called when complete\n */\n function _getAccessToken(auth_code, code_verifier, accessTokenDone) {\n var url =\n o.url +\n '/oauth2/token?' +\n utilQsString({\n client_id: o.client_id,\n redirect_uri: o.redirect_uri,\n grant_type: 'authorization_code',\n code: auth_code,\n code_verifier: code_verifier\n });\n\n // The authorization server authenticates the client and validates\n // the authorization grant, and if valid, issues an access token.\n oauth.rawxhr('POST', url, null, null, null, accessTokenDone);\n o.loading();\n }\n\n\n /**\n * bringPopupWindowToFront\n * Tries to bring an existing authentication popup to the front.\n *\n * @return {boolean} `true` if it succeeded, `false` if not\n */\n oauth.bringPopupWindowToFront = function() {\n var broughtPopupToFront = false;\n try {\n // This may cause a cross-origin error:\n // `DOMException: Blocked a frame with origin \"...\" from accessing a cross-origin frame.`\n if (oauth.popupWindow && !oauth.popupWindow.closed) {\n oauth.popupWindow.focus();\n broughtPopupToFront = true;\n }\n } catch (err) {\n // Bringing popup window to front failed (probably because of the cross-origin error mentioned above)\n }\n return broughtPopupToFront;\n };\n\n\n /**\n * bootstrapToken\n * The authorization code is a temporary code that a client can exchange for an access token.\n * If using this library in single-page mode, you'll need to call this once your application\n * has an `auth_code` and wants to get an access_token.\n *\n * @param {string} auth_code The OAuth2 `auth_code`\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @return none\n */\n oauth.bootstrapToken = function(auth_code, callback) {\n var state = token('oauth2_state');\n token('oauth2_state', '');\n var params = utilStringQs(window.location.search.slice(1));\n if (params.state !== state) {\n var error = new Error('Invalid state');\n error.status = 'invalid-state';\n callback(error);\n return;\n }\n var code_verifier = token('oauth2_pkce_code_verifier');\n token('oauth2_pkce_code_verifier', '');\n _getAccessToken(auth_code, code_verifier, accessTokenDone);\n\n function accessTokenDone(err, xhr) {\n o.done();\n if (err) {\n callback(err);\n return;\n }\n var access_token = JSON.parse(xhr.response);\n token('oauth2_access_token', access_token.access_token);\n callback(null, oauth);\n }\n };\n\n\n /**\n * fetch\n * A `fetch` wrapper that includes the Authorization header if the user is authenticated.\n * https://developer.mozilla.org/en-US/docs/Web/API/fetch\n *\n * @param {string} resource Resource passed to `fetch`\n * @param {Object} options Options passed to `fetch`\n * @return {Promise} Promise that wraps `authenticateAsync` then `fetch`\n */\n oauth.fetch = function(resource, options) {\n if (oauth.authenticated()) {\n return _doFetch();\n } else {\n if (o.auto) {\n return oauth.authenticateAsync().then(_doFetch);\n } else {\n return Promise.reject(new Error('not authenticated'));\n }\n }\n\n function _doFetch() {\n options = options || {};\n if (!options.headers) {\n options.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };\n }\n options.headers.Authorization = 'Bearer ' + token('oauth2_access_token');\n return fetch(resource, options);\n }\n };\n\n\n /**\n * xhr\n * A `XMLHttpRequest` wrapper that does authenticated calls if the user has logged in.\n * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest\n *\n * @param {Object} options\n * @param options.method Passed to `xhr.open` (e.g. 'GET', 'POST')\n * @param options.prefix If `true` path contains a path, if `false` path contains the full url\n * @param options.path The URL path (e.g. \"/api/0.6/user/details\") (or full url, if `prefix`=`false`)\n * @param options.content Passed to `xhr.send`\n * @param options.headers `Object` containing request headers\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @return {XMLHttpRequest} `XMLHttpRequest` if authenticated, otherwise `null`\n */\n oauth.xhr = function (options, callback) {\n if (oauth.authenticated()) {\n return _doXHR();\n } else {\n if (o.auto) {\n oauth.authenticate(_doXHR);\n return;\n } else {\n callback('not authenticated', null);\n return;\n }\n }\n\n function _doXHR() {\n var url = options.prefix !== false ? (o.apiUrl + options.path) : options.path;\n return oauth.rawxhr(\n options.method,\n url,\n token('oauth2_access_token'),\n options.content,\n options.headers,\n done\n );\n }\n\n function done(err, xhr) {\n if (err) {\n callback(err);\n } else if (xhr.responseXML) {\n callback(err, xhr.responseXML);\n } else {\n callback(err, xhr.response);\n }\n }\n };\n\n\n /**\n * rawxhr\n * Creates the XMLHttpRequest set up with a header and response handling\n * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest\n *\n * @param method Passed to `xhr.open` (e.g. 'GET', 'POST')\n * @param url Passed to `xhr.open`\n * @param access_token OAuth2 bearer token\n * @param data Passed to `xhr.send`\n * @param headers `Object` containing request headers\n * @param callback An \"errback\"-style callback (`err`, `result`), called when complete\n * @return `XMLHttpRequest`\n */\n oauth.rawxhr = function(method, url, access_token, data, headers, callback) {\n headers = headers || { 'Content-Type': 'application/x-www-form-urlencoded' };\n\n if (access_token) {\n headers.Authorization = 'Bearer ' + access_token;\n }\n\n var xhr = new XMLHttpRequest();\n\n xhr.onreadystatechange = function () {\n if (4 === xhr.readyState && 0 !== xhr.status) {\n if (/^20\\d$/.test(xhr.status)) { // a 20x status code - OK\n callback(null, xhr);\n } else {\n callback(xhr, null);\n }\n }\n };\n xhr.onerror = function (e) {\n callback(e, null);\n };\n\n xhr.open(method, url, true);\n for (var h in headers) xhr.setRequestHeader(h, headers[h]);\n\n xhr.send(data);\n return xhr;\n };\n\n\n /**\n * preauth\n * Pre-authorize this object, if we already have access token from the start\n *\n * @param {Object} val Object containing `access_token` property\n * @return `self`\n */\n oauth.preauth = function(val) {\n if (val && val.access_token) {\n token('oauth2_access_token', val.access_token);\n }\n return oauth;\n };\n\n\n /**\n * options (getter / setter)\n * If passed with no arguments, just return the options\n * If passed an Object, set the options then attempt to pre-authorize\n *\n * @param val? Object containing options\n * @return current `options` (if getting), or `self` (if setting)\n */\n oauth.options = function(val) {\n if (!arguments.length) return o;\n\n o = val;\n o.apiUrl = o.apiUrl || 'https://api.openstreetmap.org';\n o.url = o.url || 'https://www.openstreetmap.org';\n o.auto = o.auto || false;\n o.singlepage = o.singlepage || false;\n\n // Optional loading and loading-done functions for nice UI feedback.\n // by default, no-ops\n o.loading = o.loading || function () {};\n o.done = o.done || function () {};\n return oauth.preauth(o);\n };\n\n\n // Everything below here is initialization/setup code\n // Handle options and attempt to pre-authorize\n oauth.options(o);\n\n return oauth;\n}\n\n\n/**\n * utilQsString\n * Transforms object of `key=value` pairs into query string\n * @param {Object} Object of `key=value` pairs\n * @returns {string} query string\n */\nfunction utilQsString(obj) {\n return Object.keys(obj)\n .filter(function(key) {\n return obj[key] !== undefined;\n })\n .sort()\n .map(function(key) {\n return (encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));\n })\n .join('&');\n}\n\n/**\n * utilStringQs\n * Transforms query string into object of `key=value` pairs\n * @param {string} query string\n * @returns {Object} Object of `key=value` pairs\n */\nfunction utilStringQs(str) {\n var i = 0; // advance past any leading '?' or '#' characters\n while (i < str.length && (str[i] === '?' || str[i] === '#')) i++;\n str = str.slice(i);\n\n return str.split('&').reduce(function(obj, pair) {\n var parts = pair.split('=');\n if (parts.length === 2) {\n obj[parts[0]] = decodeURIComponent(parts[1]);\n }\n return obj;\n }, {});\n}\n\n\n/**\n * Generates a challenge/verifier pair for PKCE.\n * If the browser does not support the WebCryptoAPI, the \"plain\" method is\n * used as a fallback instead of a SHA-256 hash.\n * @param {callback} callback called with the result of the generated PKCE challenge\n */\nfunction _generatePkceChallenge(callback) {\n var code_verifier;\n // generate a random code_verifier\n // https://datatracker.ietf.org/doc/html/rfc7636#section-7.1\n var random = globalThis.crypto.getRandomValues(new Uint8Array(32));\n code_verifier = base64(random.buffer);\n var verifier = Uint8Array.from(Array.from(code_verifier).map(function(char) {\n return char.charCodeAt(0);\n }));\n\n // generate challenge for code verifier\n globalThis.crypto.subtle.digest('SHA-256', verifier).then(function(hash) {\n var code_challenge = base64(hash);\n\n callback({\n code_challenge: code_challenge,\n code_verifier: code_verifier,\n code_challenge_method: 'S256'\n });\n });\n}\n\n\n/**\n * Returns a random state to be used as the \"state\" of the OAuth2 authentication\n * See https://datatracker.ietf.org/doc/html/rfc6749#section-10.12\n */\nfunction generateState() {\n var state;\n var random = globalThis.crypto.getRandomValues(new Uint8Array(32));\n state = base64(random.buffer);\n\n return state;\n}\n\n\n/**\n * base64\n * Converts binary buffer to base64 encoded string, as used in rfc7636\n * @param {ArrayBuffer} buffer\n * @returns {string} base64 encoded\n */\nfunction base64(buffer) {\n return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))\n .replace(/\\//g, '_')\n .replace(/\\+/g, '-')\n .replace(/[=]/g, '');\n}\n", "export var JXON = new (function () {\n var\n sValueProp = 'keyValue', sAttributesProp = 'keyAttributes', sAttrPref = '@', /* you can customize these values */\n aCache = [], rIsNull = /^\\s*$/, rIsBool = /^(?:true|false)$/i;\n\n function parseText (sValue) {\n if (rIsNull.test(sValue)) { return null; }\n if (rIsBool.test(sValue)) { return sValue.toLowerCase() === 'true'; }\n if (isFinite(sValue)) { return parseFloat(sValue); }\n if (isFinite(Date.parse(sValue))) { return new Date(sValue); }\n return sValue;\n }\n\n function EmptyTree () { }\n EmptyTree.prototype.toString = function () { return 'null'; };\n EmptyTree.prototype.valueOf = function () { return null; };\n\n function objectify (vValue) {\n return vValue === null ? new EmptyTree() : vValue instanceof Object ? vValue : new vValue.constructor(vValue);\n }\n\n function createObjTree (oParentNode, nVerb, bFreeze, bNesteAttr) {\n var\n nLevelStart = aCache.length, bChildren = oParentNode.hasChildNodes(),\n bAttributes = oParentNode.hasAttributes(), bHighVerb = Boolean(nVerb & 2);\n\n var\n sProp, vContent, nLength = 0, sCollectedTxt = '',\n vResult = bHighVerb ? {} : /* put here the default value for empty nodes: */ true;\n\n if (bChildren) {\n for (var oNode, nItem = 0; nItem < oParentNode.childNodes.length; nItem++) {\n oNode = oParentNode.childNodes.item(nItem);\n if (oNode.nodeType === 4) {\n /* nodeType is 'CDATASection' (4) */\n sCollectedTxt += oNode.nodeValue;\n } else if (oNode.nodeType === 3) {\n /* nodeType is 'Text' (3) */\n sCollectedTxt += oNode.nodeValue.trim();\n } else if (oNode.nodeType === 1 && !oNode.prefix) {\n /* nodeType is 'Element' (1) */\n aCache.push(oNode);\n }\n }\n }\n\n var nLevelEnd = aCache.length, vBuiltVal = parseText(sCollectedTxt);\n\n if (!bHighVerb && (bChildren || bAttributes)) { vResult = nVerb === 0 ? objectify(vBuiltVal) : {}; }\n\n for (var nElId = nLevelStart; nElId < nLevelEnd; nElId++) {\n sProp = aCache[nElId].nodeName.toLowerCase();\n vContent = createObjTree(aCache[nElId], nVerb, bFreeze, bNesteAttr);\n if (vResult.hasOwnProperty(sProp)) {\n if (vResult[sProp].constructor !== Array) { vResult[sProp] = [vResult[sProp]]; }\n vResult[sProp].push(vContent);\n } else {\n vResult[sProp] = vContent;\n nLength++;\n }\n }\n\n if (bAttributes) {\n var\n nAttrLen = oParentNode.attributes.length,\n sAPrefix = bNesteAttr ? '' : sAttrPref, oAttrParent = bNesteAttr ? {} : vResult;\n\n for (var oAttrib, nAttrib = 0; nAttrib < nAttrLen; nLength++, nAttrib++) {\n oAttrib = oParentNode.attributes.item(nAttrib);\n oAttrParent[sAPrefix + oAttrib.name.toLowerCase()] = parseText(oAttrib.value.trim());\n }\n\n if (bNesteAttr) {\n if (bFreeze) { Object.freeze(oAttrParent); }\n vResult[sAttributesProp] = oAttrParent;\n nLength -= nAttrLen - 1;\n }\n }\n\n if (nVerb === 3 || (nVerb === 2 || nVerb === 1 && nLength > 0) && sCollectedTxt) {\n vResult[sValueProp] = vBuiltVal;\n } else if (!bHighVerb && nLength === 0 && sCollectedTxt) {\n vResult = vBuiltVal;\n }\n\n if (bFreeze && (bHighVerb || nLength > 0)) { Object.freeze(vResult); }\n\n aCache.length = nLevelStart;\n\n return vResult;\n }\n\n function loadObjTree (oXMLDoc, oParentEl, oParentObj) {\n var vValue, oChild;\n\n if (oParentObj instanceof String || oParentObj instanceof Number || oParentObj instanceof Boolean) {\n oParentEl.appendChild(oXMLDoc.createTextNode(oParentObj.toString())); /* verbosity level is 0 */\n } else if (oParentObj.constructor === Date) {\n oParentEl.appendChild(oXMLDoc.createTextNode(oParentObj.toGMTString()));\n }\n\n for (var sName in oParentObj) {\n vValue = oParentObj[sName];\n if (isFinite(sName) || vValue instanceof Function) { continue; } /* verbosity level is 0 */\n if (sName === sValueProp) {\n if (vValue !== null && vValue !== true) { oParentEl.appendChild(oXMLDoc.createTextNode(vValue.constructor === Date ? vValue.toGMTString() : String(vValue))); }\n } else if (sName === sAttributesProp) { /* verbosity level is 3 */\n for (var sAttrib in vValue) { oParentEl.setAttribute(sAttrib, vValue[sAttrib]); }\n } else if (sName.charAt(0) === sAttrPref) {\n oParentEl.setAttribute(sName.slice(1), vValue);\n } else if (vValue.constructor === Array) {\n for (var nItem = 0; nItem < vValue.length; nItem++) {\n oChild = oXMLDoc.createElementNS(null, sName);\n loadObjTree(oXMLDoc, oChild, vValue[nItem]);\n oParentEl.appendChild(oChild);\n }\n } else {\n oChild = oXMLDoc.createElementNS(null, sName);\n if (vValue instanceof Object) {\n loadObjTree(oXMLDoc, oChild, vValue);\n } else if (vValue !== null && vValue !== true) {\n oChild.appendChild(oXMLDoc.createTextNode(vValue.toString()));\n }\n oParentEl.appendChild(oChild);\n }\n }\n }\n\n this.build = function (oXMLParent, nVerbosity /* optional */, bFreeze /* optional */, bNesteAttributes /* optional */) {\n var _nVerb = arguments.length > 1 && typeof nVerbosity === 'number' ? nVerbosity & 3 : /* put here the default verbosity level: */ 1;\n return createObjTree(oXMLParent, _nVerb, bFreeze || false, arguments.length > 3 ? bNesteAttributes : _nVerb === 3);\n };\n\n this.unbuild = function (oObjTree) {\n var oNewDoc = document.implementation.createDocument('', '', null);\n loadObjTree(oNewDoc, oNewDoc, oObjTree);\n return oNewDoc;\n };\n\n this.stringify = function (oObjTree) {\n return (new XMLSerializer()).serializeToString(JXON.unbuild(oObjTree));\n };\n})();\n\n// var myObject = JXON.build(doc);\n// we got our javascript object! try: alert(JSON.stringify(myObject));\n\n// var newDoc = JXON.unbuild(myObject);\n// we got our Document instance! try: alert((new XMLSerializer()).serializeToString(newDoc));\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { json as d3_json } from 'd3-fetch';\nimport { osmAuth } from 'osm-auth';\nimport RBush from 'rbush';\n\nimport { JXON } from '../util/jxon';\nimport { geoExtent, geoRawMercator, geoVecAdd, geoZoomToScale } from '../geo';\nimport { osmEntity, osmNode, osmNote, osmRelation, osmWay } from '../osm';\nimport { utilArrayChunk, utilArrayGroupBy, utilArrayUniq, utilObjectOmit, utilRebind, utilTiler, utilQsString } from '../util';\nimport { localizer } from '../core/localizer.js';\nimport { utilGzip } from '../util/util';\nimport { osmApiConnections } from '../../config/id.js';\n\n\nvar tiler = utilTiler();\nvar dispatch = d3_dispatch('apiStatusChange', 'authLoading', 'authDone', 'change', 'loading', 'loaded', 'loadedNotes');\n\nvar urlroot = osmApiConnections[0].url;\nvar apiUrlroot = osmApiConnections[0].apiUrl || urlroot;\nvar redirectPath = window.location.origin + window.location.pathname;\nvar oauth = new osmAuth({\n url: urlroot,\n apiUrl: apiUrlroot,\n client_id: osmApiConnections[0].client_id,\n scope: 'read_prefs write_prefs write_api read_gpx write_notes',\n redirect_uri: redirectPath + 'land.html',\n loading: authLoading,\n done: authDone\n});\nvar _apiConnections = osmApiConnections;\n\n// hardcode default block of Google Maps\nvar _imageryBlocklists = [/.*\\.google(apis)?\\..*\\/(vt|kh)[\\?\\/].*([xyz]=.*){3}.*/];\nvar _tileCache = { toLoad: {}, loaded: {}, inflight: {}, seen: {}, rtree: new RBush() };\nvar _noteCache = { toLoad: {}, loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: new RBush() };\nvar _userCache = { toLoad: {}, user: {} };\nvar _cachedApiStatus;\nvar _changeset = {};\n\nvar _deferred = new Set();\nvar _connectionID = 1;\nvar _tileZoom = 16;\nvar _noteZoom = 12;\nvar _rateLimitError;\nvar _userChangesets;\nvar _userDetails;\nvar _off;\n\n// set a default but also load this from the API status\nvar _maxWayNodes = 2000;\nlet _maxChangesetElements = 10_000;\n\n\nfunction authLoading() {\n dispatch.call('authLoading');\n}\n\n\nfunction authDone() {\n dispatch.call('authDone');\n}\n\n\nfunction abortRequest(controllerOrXHR) {\n if (controllerOrXHR) {\n controllerOrXHR.abort();\n }\n}\n\n\nfunction hasInflightRequests(cache) {\n return Object.keys(cache.inflight).length;\n}\n\n\nfunction abortUnwantedRequests(cache, visibleTiles) {\n Object.keys(cache.inflight).forEach(function(k) {\n if (cache.toLoad[k]) return;\n if (visibleTiles.find(function(tile) { return k === tile.id; })) return;\n\n abortRequest(cache.inflight[k]);\n delete cache.inflight[k];\n });\n}\n\nfunction getNodesJSON(obj) {\n var elems = obj.nodes;\n var nodes = new Array(elems.length);\n for (var i = 0, l = elems.length; i < l; i++) {\n nodes[i] = 'n' + elems[i];\n }\n return nodes;\n}\n\nfunction getMembersJSON(obj) {\n var elems = obj.members;\n var members = new Array(elems.length);\n for (var i = 0, l = elems.length; i < l; i++) {\n var attrs = elems[i];\n members[i] = {\n id: attrs.type[0] + attrs.ref,\n type: attrs.type,\n role: attrs.role\n };\n }\n return members;\n}\n\nfunction encodeNoteRtree(note) {\n return {\n minX: note.loc[0],\n minY: note.loc[1],\n maxX: note.loc[0],\n maxY: note.loc[1],\n data: note\n };\n}\n\n\nvar jsonparsers = {\n\n node: function nodeData(obj, uid) {\n return new osmNode({\n id: uid,\n visible: typeof obj.visible === 'boolean' ? obj.visible : true,\n version: obj.version && obj.version.toString(),\n changeset: obj.changeset && obj.changeset.toString(),\n timestamp: obj.timestamp,\n user: obj.user,\n uid: obj.uid && obj.uid.toString(),\n loc: [Number(obj.lon), Number(obj.lat)],\n tags: obj.tags\n });\n },\n\n way: function wayData(obj, uid) {\n return new osmWay({\n id: uid,\n visible: typeof obj.visible === 'boolean' ? obj.visible : true,\n version: obj.version && obj.version.toString(),\n changeset: obj.changeset && obj.changeset.toString(),\n timestamp: obj.timestamp,\n user: obj.user,\n uid: obj.uid && obj.uid.toString(),\n tags: obj.tags,\n nodes: getNodesJSON(obj)\n });\n },\n\n relation: function relationData(obj, uid) {\n return new osmRelation({\n id: uid,\n visible: typeof obj.visible === 'boolean' ? obj.visible : true,\n version: obj.version && obj.version.toString(),\n changeset: obj.changeset && obj.changeset.toString(),\n timestamp: obj.timestamp,\n user: obj.user,\n uid: obj.uid && obj.uid.toString(),\n tags: obj.tags,\n members: getMembersJSON(obj)\n });\n },\n\n user: function parseUser(obj, uid) {\n return {\n id: uid,\n display_name: obj.display_name,\n account_created: obj.account_created,\n image_url: obj.img && obj.img.href,\n changesets_count: obj.changesets && obj.changesets.count && obj.changesets.count.toString() || '0',\n active_blocks: obj.blocks && obj.blocks.received && obj.blocks.received.active && obj.blocks.received.active.toString() || '0'\n };\n }\n};\n\nfunction parseJSON(payload, callback, options) {\n options = Object.assign({ skipSeen: true }, options);\n if (!payload) {\n return callback({ message: 'No JSON', status: -1 });\n }\n\n var json = payload;\n if (typeof json !== 'object') json = JSON.parse(payload);\n\n if (!json.elements) return callback({ message: 'No JSON', status: -1 });\n\n var children = json.elements;\n\n var handle = window.requestIdleCallback(function() {\n _deferred.delete(handle);\n var results = [];\n var result;\n for (var i = 0; i < children.length; i++) {\n result = parseChild(children[i]);\n if (result) results.push(result);\n }\n callback(null, results);\n });\n _deferred.add(handle);\n\n function parseChild(child) {\n var parser = jsonparsers[child.type];\n if (!parser) return null;\n\n var uid;\n\n uid = osmEntity.id.fromOSM(child.type, child.id);\n if (options.skipSeen) {\n if (_tileCache.seen[uid]) return null; // avoid reparsing a \"seen\" entity\n _tileCache.seen[uid] = true;\n }\n\n return parser(child, uid);\n }\n}\n\nfunction parseUserJSON(payload, callback, options) {\n options = Object.assign({ skipSeen: true }, options);\n if (!payload) {\n return callback({ message: 'No JSON', status: -1 });\n }\n\n var json = payload;\n if (typeof json !== 'object') json = JSON.parse(payload);\n\n if (!json.users && !json.user) return callback({ message: 'No JSON', status: -1 });\n\n var objs = json.users || [json];\n\n var handle = window.requestIdleCallback(function() {\n _deferred.delete(handle);\n var results = [];\n var result;\n for (var i = 0; i < objs.length; i++) {\n result = parseObj(objs[i]);\n if (result) results.push(result);\n }\n callback(null, results);\n });\n _deferred.add(handle);\n\n function parseObj(obj) {\n var uid = obj.user.id && obj.user.id.toString();\n if (options.skipSeen && _userCache.user[uid]) {\n delete _userCache.toLoad[uid];\n return null;\n }\n var user = jsonparsers.user(obj.user, uid);\n _userCache.user[uid] = user;\n delete _userCache.toLoad[uid];\n return user;\n }\n}\n\nfunction parseNoteJSON(payload, callback, _options) {\n const options = { skipSeen: true, ..._options };\n if (!payload) {\n return callback({ message: 'No JSON', status: -1 });\n }\n\n const features = payload.type === 'FeatureCollection' ? payload.features : [payload];\n\n const notes = features.map(feature => {\n const uid = feature.properties.id;\n if (options.skipSeen) {\n if (_tileCache.seen[uid]) return null; // avoid reparsing a \"seen\" entity\n _tileCache.seen[uid] = true;\n }\n\n const props = {\n ...feature.properties,\n loc: feature.geometry.coordinates,\n };\n\n // if notes are coincident, move them apart slightly\n if (!_noteCache.note[uid]) {\n let coincident = false;\n const epsilon = 0.00001;\n do {\n if (coincident) {\n props.loc = geoVecAdd(props.loc, [epsilon, epsilon]);\n }\n const bbox = geoExtent(props.loc).bbox();\n coincident = _noteCache.rtree.search(bbox).length;\n } while (coincident);\n } else {\n // we already saw this note: don't change its location again\n props.loc = _noteCache.note[uid].loc;\n }\n\n var note = new osmNote(props);\n var item = encodeNoteRtree(note);\n _noteCache.note[note.id] = note;\n updateRtree(item, true);\n\n return note;\n });\n callback(undefined, notes);\n}\n\n// replace or remove note from rtree\nfunction updateRtree(item, replace) {\n _noteCache.rtree.remove(item, function isEql(a, b) { return a.data.id === b.data.id; });\n\n if (replace) {\n _noteCache.rtree.insert(item);\n }\n}\n\n\nfunction wrapcb(thisArg, callback, cid) {\n return function(err, result) {\n if (err) {\n return callback.call(thisArg, err);\n\n } else if (thisArg.getConnectionId() !== cid) {\n return callback.call(thisArg, { message: 'Connection Switched', status: -1 });\n\n } else {\n return callback.call(thisArg, err, result);\n }\n };\n}\n\n\nexport default {\n\n init: function() {\n utilRebind(this, dispatch, 'on');\n },\n\n\n reset: function() {\n Array.from(_deferred).forEach(function(handle) {\n window.cancelIdleCallback(handle);\n _deferred.delete(handle);\n });\n\n _connectionID++;\n _userChangesets = undefined;\n _userDetails = undefined;\n _rateLimitError = undefined;\n\n Object.values(_tileCache.inflight).forEach(abortRequest);\n Object.values(_noteCache.inflight).forEach(abortRequest);\n Object.values(_noteCache.inflightPost).forEach(abortRequest);\n if (_changeset.inflight) abortRequest(_changeset.inflight);\n\n _tileCache = { toLoad: {}, loaded: {}, inflight: {}, seen: {}, rtree: new RBush() };\n _noteCache = { toLoad: {}, loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: new RBush() };\n _userCache = { toLoad: {}, user: {} };\n _cachedApiStatus = undefined;\n _changeset = {};\n\n return this;\n },\n\n\n getConnectionId: function() {\n return _connectionID;\n },\n\n\n getUrlRoot: function() {\n return urlroot;\n },\n\n\n getApiUrlRoot: function() {\n return apiUrlroot;\n },\n\n\n changesetURL: function(changesetID) {\n return urlroot + '/changeset/' + changesetID;\n },\n\n\n changesetsURL: function(center, zoom) {\n var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));\n return urlroot + '/history#map=' +\n Math.floor(zoom) + '/' +\n center[1].toFixed(precision) + '/' +\n center[0].toFixed(precision);\n },\n\n\n entityURL: function(entity) {\n return urlroot + '/' + entity.type + '/' + entity.osmId();\n },\n\n\n historyURL: function(entity) {\n return urlroot + '/' + entity.type + '/' + entity.osmId() + '/history';\n },\n\n\n userURL: function(username) {\n return urlroot + '/user/' + encodeURIComponent(username);\n },\n\n\n noteURL: function(note) {\n return urlroot + '/note/' + note.id;\n },\n\n\n noteReportURL: function(note) {\n return urlroot + '/reports/new?reportable_type=Note&reportable_id=' + note.id;\n },\n\n\n // Generic method to load data from the OSM API\n // Can handle either auth or unauth calls.\n loadFromAPI: function(path, callback, options) {\n options = Object.assign({ skipSeen: true }, options);\n var that = this;\n var cid = _connectionID;\n\n function done(err, payloadString) {\n if (that.getConnectionId() !== cid) {\n if (callback) callback({ message: 'Connection Switched', status: -1 });\n return;\n }\n\n if ((err && _cachedApiStatus === 'online') ||\n (!err && _cachedApiStatus !== 'online')) {\n // If the response's error state doesn't match the status,\n // it's likely we lost or gained the connection so reload the status\n that.reloadApiStatus();\n }\n\n if (callback) {\n if (err) {\n // eslint-disable-next-line no-console\n console.error('API error:', err);\n return callback(err);\n } else {\n const payload = typeof payloadString === 'string' ? JSON.parse(payloadString) : payloadString;\n\n if (payload.type === 'FeatureCollection' || payload.type === 'Feature') {\n return parseNoteJSON(payload, callback, options);\n } else {\n return parseJSON(payload, callback, options);\n }\n }\n }\n }\n\n if (this.authenticated()) {\n return oauth.xhr({\n method: 'GET',\n path\n }, done);\n } else {\n var url = apiUrlroot + path;\n var controller = new AbortController();\n\n d3_json(url, { signal: controller.signal })\n .then(function(data) {\n done(null, data);\n })\n .catch(function(err) {\n if (err.name === 'AbortError') return;\n // d3-fetch includes status in the error message,\n // but we can't access the response itself\n // https://github.com/d3/d3-fetch/issues/27\n var match = err.message.match(/^\\d{3}/);\n if (match) {\n done({ status: +match[0], statusText: err.message });\n } else {\n done(err.message);\n }\n });\n return controller;\n }\n },\n\n\n // Load a single entity by id (ways and relations use the `/full` call to include\n // nodes and members). Parent relations are not included, see `loadEntityRelations`.\n // GET /api/0.6/node/#id\n // GET /api/0.6/[way|relation]/#id/full\n loadEntity: function(id, callback) {\n var type = osmEntity.id.type(id);\n var osmID = osmEntity.id.toOSM(id);\n var options = { skipSeen: false };\n\n this.loadFromAPI(\n '/api/0.6/' + type + '/' + osmID + (type !== 'node' ? '/full' : '') + '.json',\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n // Load a single note by id , XML format\n // GET /api/0.6/notes/#id\n loadEntityNote: function(id, callback) {\n var options = { skipSeen: false };\n this.loadFromAPI(\n `/api/0.6/notes/${id}.json`,\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n\n // Load a single entity with a specific version\n // GET /api/0.6/[node|way|relation]/#id/#version\n loadEntityVersion: function(id, version, callback) {\n var type = osmEntity.id.type(id);\n var osmID = osmEntity.id.toOSM(id);\n var options = { skipSeen: false };\n\n this.loadFromAPI(\n '/api/0.6/' + type + '/' + osmID + '/' + version + '.json',\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n\n // Load the relations of a single entity with the given.\n // GET /api/0.6/[node|way|relation]/#id/relations\n loadEntityRelations: function(id, callback) {\n var type = osmEntity.id.type(id);\n var osmID = osmEntity.id.toOSM(id);\n var options = { skipSeen: false };\n\n this.loadFromAPI(\n '/api/0.6/' + type + '/' + osmID + '/relations.json',\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n\n // Load multiple entities in chunks\n // (note: callback may be called multiple times)\n // Unlike `loadEntity`, child nodes and members are not fetched\n // GET /api/0.6/[nodes|ways|relations]?#parameters\n loadMultiple: function(ids, callback) {\n var that = this;\n var groups = utilArrayGroupBy(utilArrayUniq(ids), osmEntity.id.type);\n\n Object.keys(groups).forEach(function(k) {\n var type = k + 's'; // nodes, ways, relations\n var osmIDs = groups[k].map(function(id) { return osmEntity.id.toOSM(id); });\n var options = { skipSeen: false };\n\n utilArrayChunk(osmIDs, 150).forEach(function(arr) {\n that.loadFromAPI(\n '/api/0.6/' + type + '.json?' + type + '=' + arr.join(),\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n });\n });\n },\n\n\n // Create, upload, and close a changeset\n // PUT /api/0.6/changeset/create\n // POST /api/0.6/changeset/#id/upload\n // PUT /api/0.6/changeset/#id/close\n putChangeset: function(changeset, changes, callback) {\n var cid = _connectionID;\n\n if (_changeset.inflight) {\n return callback({ message: 'Changeset already inflight', status: -2 }, changeset);\n\n } else if (_changeset.open) { // reuse existing open changeset..\n return createdChangeset.call(this, null, _changeset.open);\n\n } else { // Open a new changeset..\n var options = {\n method: 'PUT',\n path: '/api/0.6/changeset/create',\n headers: { 'Content-Type': 'text/xml' },\n content: JXON.stringify(changeset.asJXON())\n };\n _changeset.inflight = oauth.xhr(\n options,\n wrapcb(this, createdChangeset, cid)\n );\n }\n\n\n async function createdChangeset(err, changesetID) {\n _changeset.inflight = null;\n if (err) { return callback(err, changeset); }\n\n _changeset.open = changesetID;\n changeset = changeset.update({ id: changesetID });\n\n // Upload the changeset..\n const xml = JXON.stringify(changeset.osmChangeJXON(changes));\n const compressed = await utilGzip(xml);\n\n const headers = { 'Content-Type': 'text/xml' };\n if (compressed) headers['Content-Encoding'] = 'gzip';\n\n var options = {\n method: 'POST',\n path: '/api/0.6/changeset/' + changesetID + '/upload',\n headers,\n content: compressed || xml,\n };\n _changeset.inflight = oauth.xhr(\n options,\n wrapcb(this, uploadedChangeset, cid)\n );\n }\n\n\n function uploadedChangeset(err) {\n _changeset.inflight = null;\n if (err) return callback(err, changeset);\n\n // Upload was successful, safe to call the callback.\n // Add delay to allow for postgres replication #1646 #2678\n window.setTimeout(function() { callback(null, changeset); }, 2500);\n _changeset.open = null;\n\n // At this point, we don't really care if the connection was switched..\n // Only try to close the changeset if we're still talking to the same server.\n if (this.getConnectionId() === cid) {\n // Still attempt to close changeset, but ignore response because #2667\n oauth.xhr({\n method: 'PUT',\n path: '/api/0.6/changeset/' + changeset.id + '/close',\n headers: { 'Content-Type': 'text/xml' }\n }, function() { return true; });\n }\n }\n },\n\n /** updates the tags on an existing unclosed changeset */\n // PUT /api/0.6/changeset/#id\n updateChangesetTags: (changeset) => {\n return oauth.fetch(`${oauth.options().apiUrl}/api/0.6/changeset/${changeset.id}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'text/xml' },\n body: JXON.stringify(changeset.asJXON())\n });\n },\n\n\n // Load multiple users in chunks\n // (note: callback may be called multiple times)\n // GET /api/0.6/users?users=#id1,#id2,...,#idn\n loadUsers: function(uids, callback) {\n var toLoad = [];\n var cached = [];\n\n utilArrayUniq(uids).forEach(function(uid) {\n if (_userCache.user[uid]) {\n delete _userCache.toLoad[uid];\n cached.push(_userCache.user[uid]);\n } else {\n toLoad.push(uid);\n }\n });\n\n if (cached.length || !this.authenticated()) {\n callback(undefined, cached);\n if (!this.authenticated()) return; // require auth\n }\n\n utilArrayChunk(toLoad, 150).forEach(function(arr) {\n oauth.xhr({\n method: 'GET',\n path: '/api/0.6/users.json?users=' + arr.join()\n }, wrapcb(this, done, _connectionID));\n }.bind(this));\n\n function done(err, payload) {\n if (err) return callback(err);\n\n var options = { skipSeen: true };\n return parseUserJSON(payload, function(err, results) {\n if (err) return callback(err);\n return callback(undefined, results);\n }, options);\n }\n },\n\n\n // Load a given user by id\n // GET /api/0.6/user/#id\n loadUser: function(uid, callback) {\n if (_userCache.user[uid] || !this.authenticated()) { // require auth\n delete _userCache.toLoad[uid];\n return callback(undefined, _userCache.user[uid]);\n }\n\n oauth.xhr({\n method: 'GET',\n path: '/api/0.6/user/' + uid + '.json'\n }, wrapcb(this, done, _connectionID));\n\n function done(err, payload) {\n if (err) return callback(err);\n\n var options = { skipSeen: true };\n return parseUserJSON(payload, function(err, results) {\n if (err) return callback(err);\n return callback(undefined, results[0]);\n }, options);\n }\n },\n\n\n // Load the details of the logged-in user\n // GET /api/0.6/user/details\n userDetails: function(callback) {\n if (_userDetails) { // retrieve cached\n return callback(undefined, _userDetails);\n }\n\n oauth.xhr({\n method: 'GET',\n path: '/api/0.6/user/details.json'\n }, wrapcb(this, done, _connectionID));\n\n function done(err, payload) {\n if (err) return callback(err);\n\n var options = { skipSeen: false };\n return parseUserJSON(payload, function(err, results) {\n if (err) return callback(err);\n _userDetails = results[0];\n return callback(undefined, _userDetails);\n }, options);\n }\n },\n\n\n // Load previous changesets for the logged in user\n // GET /api/0.6/changesets?user=#id\n userChangesets: function(callback) {\n if (_userChangesets) { // retrieve cached\n return callback(undefined, _userChangesets);\n }\n\n this.userDetails(\n wrapcb(this, gotDetails, _connectionID)\n );\n\n\n function gotDetails(err, user) {\n if (err) { return callback(err); }\n\n oauth.xhr({\n method: 'GET',\n path: `/api/0.6/changesets.json?user=${user.id}`\n }, wrapcb(this, done, _connectionID));\n }\n\n function done(err, payloadString) {\n if (err) { return callback(err); }\n\n const payload = JSON.parse(payloadString);\n _userChangesets = payload.changesets.filter(tags => tags.tags.comment);\n\n return callback(undefined, _userChangesets);\n }\n },\n\n\n // Fetch the status of the OSM API\n // GET /api/capabilities\n status: function(callback) {\n const url = `${apiUrlroot}/api/capabilities.json`;\n var errback = wrapcb(this, done, _connectionID);\n d3_json(url)\n .then(function(data) { errback(null, data); })\n .catch(function(err) { errback(err.message); });\n\n function done(err, payload) {\n if (err) {\n // the status is null if no response could be retrieved\n return callback(err, null);\n }\n\n if (_rateLimitError) {\n return callback(_rateLimitError, 'rateLimited');\n } else {\n _maxWayNodes = payload.api.waynodes.maximum;\n\n _imageryBlocklists = payload.policy.imagery.blacklist.map(item => new RegExp(item.regex, 'i'));\n\n const maxChangesetElements = payload.api.changesets.maximum_elements;\n if (!Number.isNaN(maxChangesetElements)) _maxChangesetElements = maxChangesetElements;\n\n return callback(undefined, payload.api.status.api);\n }\n }\n },\n\n // Calls `status` and dispatches an `apiStatusChange` event if the returned\n // status differs from the cached status.\n reloadApiStatus: function() {\n // throttle to avoid unnecessary API calls\n if (!this.throttledReloadApiStatus) {\n var that = this;\n this.throttledReloadApiStatus = throttle(function() {\n that.status(function(err, status) {\n if (status !== _cachedApiStatus) {\n _cachedApiStatus = status;\n dispatch.call('apiStatusChange', that, err, status);\n }\n });\n }, 500);\n }\n this.throttledReloadApiStatus();\n },\n\n\n // Returns the maximum number of nodes a single way can have\n maxWayNodes: function() {\n return _maxWayNodes;\n },\n\n\n maxChangesetElements: () => _maxChangesetElements,\n\n\n // Load data (entities) from the API in tiles\n // GET /api/0.6/map?bbox=\n loadTiles: function(projection, callback) {\n if (_off) return;\n\n // determine the needed tiles to cover the view\n var tiles = tiler.zoomExtent([_tileZoom, _tileZoom]).getTiles(projection);\n\n // abort inflight requests that are no longer needed\n var hadRequests = hasInflightRequests(_tileCache);\n abortUnwantedRequests(_tileCache, tiles);\n if (hadRequests && !hasInflightRequests(_tileCache)) {\n if (_rateLimitError) {\n // was rate limited, but has settled\n _rateLimitError = undefined;\n dispatch.call('change');\n this.reloadApiStatus();\n }\n dispatch.call('loaded'); // stop the spinner\n }\n\n // issue new requests..\n tiles.forEach(function(tile) {\n this.loadTile(tile, callback);\n }, this);\n },\n\n\n // Load a single data tile\n // GET /api/0.6/map?bbox=\n loadTile: function(tile, callback) {\n if (_off) return;\n if (_tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return;\n\n if (!hasInflightRequests(_tileCache)) {\n dispatch.call('loading'); // start the spinner\n }\n\n var path = '/api/0.6/map.json?bbox=';\n var options = { skipSeen: true };\n\n _tileCache.inflight[tile.id] = this.loadFromAPI(\n path + tile.extent.toParam(),\n tileCallback.bind(this),\n options\n );\n\n function tileCallback(err, parsed) {\n if (!err) {\n delete _tileCache.inflight[tile.id];\n delete _tileCache.toLoad[tile.id];\n _tileCache.loaded[tile.id] = true;\n var bbox = tile.extent.bbox();\n bbox.id = tile.id;\n _tileCache.rtree.insert(bbox);\n } else {\n // map tile loading error: e.g. network connection error,\n // 509 Bandwidth Limit Exceeded, 429 Too Many Requests\n if (!_rateLimitError && err.status === 509 || err.status === 429) {\n // show \"API rate limiting\" warning\n _rateLimitError = err;\n dispatch.call('change');\n this.reloadApiStatus();\n }\n setTimeout(() => {\n // retry loading the tiles\n delete _tileCache.inflight[tile.id];\n this.loadTile(tile, callback);\n }, 8000);\n }\n if (callback) {\n callback(err, Object.assign({ data: parsed }, tile));\n }\n if (!hasInflightRequests(_tileCache)) {\n if (_rateLimitError) {\n // was rate limited, but has settled\n _rateLimitError = undefined;\n dispatch.call('change');\n this.reloadApiStatus();\n }\n dispatch.call('loaded'); // stop the spinner\n }\n }\n },\n\n\n isDataLoaded: function(loc) {\n var bbox = { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1] };\n return _tileCache.rtree.collides(bbox);\n },\n\n\n // load the tile that covers the given `loc`\n loadTileAtLoc: function(loc, callback) {\n // Back off if the toLoad queue is filling up.. re #6417\n // (Currently `loadTileAtLoc` requests are considered low priority - used by operations to\n // let users safely edit geometries which extend to unloaded tiles. We can drop some.)\n if (Object.keys(_tileCache.toLoad).length > 50) return;\n\n var k = geoZoomToScale(_tileZoom + 1);\n var offset = geoRawMercator().scale(k)(loc);\n var projection = geoRawMercator().transform({ k: k, x: -offset[0], y: -offset[1] });\n var tiles = tiler.zoomExtent([_tileZoom, _tileZoom]).getTiles(projection);\n\n tiles.forEach(function(tile) {\n if (_tileCache.toLoad[tile.id] || _tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return;\n\n _tileCache.toLoad[tile.id] = true;\n this.loadTile(tile, callback);\n }, this);\n },\n\n\n // Load notes from the API in tiles\n // GET /api/0.6/notes?bbox=\n loadNotes: function(projection, noteOptions) {\n noteOptions = Object.assign({ limit: 10000, closed: 7 }, noteOptions);\n if (_off) return;\n\n var that = this;\n var path = `/api/0.6/notes.json?limit=${noteOptions.limit}&closed=${noteOptions.closed}&bbox=`;\n var throttleLoadUsers = throttle(function() {\n var uids = Object.keys(_userCache.toLoad);\n if (!uids.length) return;\n that.loadUsers(uids, function() {}); // eagerly load user details\n }, 750);\n\n // determine the needed tiles to cover the view\n var tiles = tiler.zoomExtent([_noteZoom, _noteZoom]).getTiles(projection);\n\n // abort inflight requests that are no longer needed\n abortUnwantedRequests(_noteCache, tiles);\n\n // issue new requests..\n tiles.forEach(function(tile) {\n if (_noteCache.loaded[tile.id] || _noteCache.inflight[tile.id]) return;\n\n var options = { skipSeen: false };\n _noteCache.inflight[tile.id] = that.loadFromAPI(\n path + tile.extent.toParam(),\n function(err) {\n delete _noteCache.inflight[tile.id];\n if (!err) {\n _noteCache.loaded[tile.id] = true;\n }\n throttleLoadUsers();\n dispatch.call('loadedNotes');\n },\n options\n );\n });\n },\n\n\n // Create a note\n // POST /api/0.6/notes?params\n postNoteCreate: function(note, callback) {\n if (!this.authenticated()) {\n return callback({ message: 'Not Authenticated', status: -3 }, note);\n }\n if (_noteCache.inflightPost[note.id]) {\n return callback({ message: 'Note update already inflight', status: -2 }, note);\n }\n\n if (!note.loc[0] || !note.loc[1] || !note.newComment) return; // location & description required\n\n var comment = note.newComment;\n if (note.newCategory && note.newCategory !== 'None') { comment += ' #' + note.newCategory; }\n\n var path = '/api/0.6/notes.json?' + utilQsString({ lon: note.loc[0], lat: note.loc[1], text: comment });\n\n _noteCache.inflightPost[note.id] = oauth.xhr({\n method: 'POST',\n path: path\n }, wrapcb(this, done, _connectionID));\n\n\n function done(err, payload) {\n delete _noteCache.inflightPost[note.id];\n if (err) { return callback(err); }\n\n // we get the updated note back, remove from caches and reparse..\n this.removeNote(note);\n\n var options = { skipSeen: false };\n return parseNoteJSON(JSON.parse(payload), function(err, results) {\n if (err) {\n return callback(err);\n } else {\n return callback(undefined, results[0]);\n }\n }, options);\n }\n },\n\n\n // Update a note\n // POST /api/0.6/notes/#id/comment?text=comment\n // POST /api/0.6/notes/#id/close?text=comment\n // POST /api/0.6/notes/#id/reopen?text=comment\n postNoteUpdate: function(note, newStatus, callback) {\n if (!this.authenticated()) {\n return callback({ message: 'Not Authenticated', status: -3 }, note);\n }\n if (_noteCache.inflightPost[note.id]) {\n return callback({ message: 'Note update already inflight', status: -2 }, note);\n }\n\n var action;\n if (note.status !== 'closed' && newStatus === 'closed') {\n action = 'close';\n } else if (note.status !== 'open' && newStatus === 'open') {\n action = 'reopen';\n } else {\n action = 'comment';\n if (!note.newComment) return; // when commenting, comment required\n }\n\n var path = `/api/0.6/notes/${note.id}/${action}.json`;\n if (note.newComment) {\n path += '?' + utilQsString({ text: note.newComment });\n }\n\n _noteCache.inflightPost[note.id] = oauth.xhr({\n method: 'POST',\n path: path\n }, wrapcb(this, done, _connectionID));\n\n\n function done(err, payload) {\n delete _noteCache.inflightPost[note.id];\n if (err) { return callback(err); }\n\n // we get the updated note back, remove from caches and reparse..\n this.removeNote(note);\n\n // update closed note cache - used to populate `closed:note` changeset tag\n if (action === 'close') {\n _noteCache.closed[note.id] = true;\n } else if (action === 'reopen') {\n delete _noteCache.closed[note.id];\n }\n\n var options = { skipSeen: false };\n return parseNoteJSON(JSON.parse(payload), function(err, results) {\n if (err) {\n return callback(err);\n } else {\n return callback(undefined, results[0]);\n }\n }, options);\n }\n },\n\n\n /* connection options for source switcher (optional) */\n apiConnections: function(val) {\n if (!arguments.length) return _apiConnections;\n _apiConnections = val;\n return this;\n },\n\n\n switch: function(newOptions) {\n urlroot = newOptions.url;\n apiUrlroot = newOptions.apiUrl || urlroot;\n if (newOptions.url && !newOptions.apiUrl) {\n newOptions = {\n ...newOptions,\n apiUrl: newOptions.url\n };\n }\n\n // Copy the existing options, but omit 'access_token'.\n // (if we did preauth, access_token won't work on a different server)\n const oldOptions = utilObjectOmit(oauth.options(), 'access_token');\n oauth.options({...oldOptions, ...newOptions});\n\n this.reset();\n this.userChangesets(function() {}); // eagerly load user details/changesets\n dispatch.call('change');\n return this;\n },\n\n\n toggle: function(val) {\n _off = !val;\n return this;\n },\n\n\n isChangesetInflight: function() {\n return !!_changeset.inflight;\n },\n\n\n // get/set cached data\n // This is used to save/restore the state when entering/exiting the walkthrough\n // Also used for testing purposes.\n caches: function(obj) {\n function cloneCache(source) {\n var target = {};\n Object.keys(source).forEach(function(k) {\n if (k === 'rtree') {\n target.rtree = new RBush().fromJSON(source.rtree.toJSON()); // clone rbush\n } else if (k === 'note') {\n target.note = {};\n Object.keys(source.note).forEach(function(id) {\n target.note[id] = osmNote(source.note[id]); // copy notes\n });\n } else {\n target[k] = JSON.parse(JSON.stringify(source[k])); // clone deep\n }\n });\n return target;\n }\n\n if (!arguments.length) {\n return {\n tile: cloneCache(_tileCache),\n note: cloneCache(_noteCache),\n user: cloneCache(_userCache)\n };\n }\n\n // access caches directly for testing (e.g., loading notes rtree)\n if (obj === 'get') {\n return {\n tile: _tileCache,\n note: _noteCache,\n user: _userCache\n };\n }\n\n if (obj.tile) {\n _tileCache = obj.tile;\n _tileCache.inflight = {};\n }\n if (obj.note) {\n _noteCache = obj.note;\n _noteCache.inflight = {};\n _noteCache.inflightPost = {};\n }\n if (obj.user) {\n _userCache = obj.user;\n }\n\n return this;\n },\n\n\n logout: function() {\n _userChangesets = undefined;\n _userDetails = undefined;\n oauth.logout();\n dispatch.call('change');\n return this;\n },\n\n\n authenticated: function() {\n return oauth.authenticated();\n },\n\n\n /** @param {import('osm-auth').LoginOptions} options */\n authenticate: function(callback, options) {\n var that = this;\n var cid = _connectionID;\n _userChangesets = undefined;\n _userDetails = undefined;\n\n function done(err, res) {\n if (err) {\n if (callback) callback(err);\n return;\n }\n if (that.getConnectionId() !== cid) {\n if (callback) callback({ message: 'Connection Switched', status: -1 });\n return;\n }\n _rateLimitError = undefined;\n dispatch.call('change');\n if (callback) callback(err, res);\n that.userChangesets(function() {}); // eagerly load user details/changesets\n }\n\n // ensure the locale is correctly set before opening the popup\n oauth.options({\n ...oauth.options(),\n locale: localizer.localeCode(),\n });\n\n oauth.authenticate(done, options);\n },\n\n\n imageryBlocklists: function() {\n return _imageryBlocklists;\n },\n\n\n tileZoom: function(val) {\n if (!arguments.length) return _tileZoom;\n _tileZoom = val;\n return this;\n },\n\n\n // get all cached notes covering the viewport\n notes: function(projection) {\n var viewport = projection.clipExtent();\n var min = [viewport[0][0], viewport[1][1]];\n var max = [viewport[1][0], viewport[0][1]];\n var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n\n return _noteCache.rtree.search(bbox)\n .map(function(d) { return d.data; });\n },\n\n\n // get a single note from the cache\n getNote: function(id) {\n return _noteCache.note[id];\n },\n\n\n // remove a single note from the cache\n removeNote: function(note) {\n if (!(note instanceof osmNote) || !note.id) return;\n\n delete _noteCache.note[note.id];\n updateRtree(encodeNoteRtree(note), false); // false = remove\n },\n\n\n // replace a single note in the cache\n replaceNote: function(note) {\n if (!(note instanceof osmNote) || !note.id) return;\n\n _noteCache.note[note.id] = note;\n updateRtree(encodeNoteRtree(note), true); // true = replace\n return note;\n },\n\n\n // Get an array of note IDs closed during this session.\n // Used to populate `closed:note` changeset tag\n getClosedIDs: function() {\n return Object.keys(_noteCache.closed).sort();\n }\n\n};\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { json as d3_json } from 'd3-fetch';\n\nimport { localizer } from '../core/localizer';\nimport { utilQsString } from '../util';\n\n\nvar apibase = 'https://wiki.openstreetmap.org/w/api.php';\nvar _inflight = {};\nvar _wikibaseCache = {};\nvar _localeIDs = { en: false };\n\n\nvar debouncedRequest = debounce(request, 500, { leading: false });\n\nfunction request(url, callback) {\n if (_inflight[url]) return;\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, { signal: controller.signal })\n .then(function(result) {\n delete _inflight[url];\n if (callback) callback(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (callback) callback(err.message);\n });\n}\n\n\nexport default {\n\n init: function() {\n _inflight = {};\n _wikibaseCache = {};\n _localeIDs = {};\n },\n\n\n reset: function() {\n Object.values(_inflight).forEach(function(controller) { controller.abort(); });\n _inflight = {};\n },\n\n\n /**\n * Get the best value for the property, or undefined if not found\n * @param entity object from wikibase\n * @param property string e.g. 'P4' for image\n * @param langCode string e.g. 'fr' for French\n */\n claimToValue: function(entity, property, langCode) {\n if (!entity.claims[property]) return undefined;\n var locale = _localeIDs[langCode];\n var preferredPick, localePick;\n\n entity.claims[property].forEach(function(stmt) {\n // If exists, use value limited to the needed language (has a qualifier P26 = locale)\n // Or if not found, use the first value with the \"preferred\" rank\n if (!preferredPick && stmt.rank === 'preferred') {\n preferredPick = stmt;\n }\n if (locale && stmt.qualifiers && stmt.qualifiers.P26 &&\n stmt.qualifiers.P26[0].datavalue.value.id === locale\n ) {\n localePick = stmt;\n }\n });\n\n var result = localePick || preferredPick;\n if (result) {\n var datavalue = result.mainsnak.datavalue;\n return datavalue.type === 'wikibase-entityid' ? datavalue.value.id : datavalue.value;\n } else {\n return undefined;\n }\n },\n\n\n /**\n * Convert monolingual property into a key-value object (language -> value)\n * @param entity object from wikibase\n * @param property string e.g. 'P31' for monolingual wiki page title\n */\n monolingualClaimToValueObj: function(entity, property) {\n if (!entity || !entity.claims[property]) return undefined;\n\n return entity.claims[property].reduce(function(acc, obj) {\n var value = obj.mainsnak.datavalue.value;\n acc[value.language] = value.text;\n return acc;\n }, {});\n },\n\n\n toSitelink: function(key, value, isHistorical) {\n var type = value ? 'Tag' : 'Key';\n var prefix = '';\n if (isHistorical) {\n prefix = `OpenHistoricalMap/Tags/${type}/`;\n } else {\n prefix = type + ':';\n }\n return (prefix + (value ? `${key}=${value}` : key).replace(/_/g, ' ')).trim();\n },\n\n /**\n * Converts text like `tag:...=...` into clickable links\n *\n * @param {string} unsafeText - unsanitized text\n */\n linkifyWikiText(unsafeText) {\n /** @param {import('d3').Selection} selection */\n return (selection) => {\n const segments = unsafeText.split(/(key|tag):([\\w-]+)(=([\\w-]+))?/g);\n\n for (let i = 0; i < segments.length; i += 5) {\n const [plainText, , key, , value] = segments.slice(i);\n\n if (plainText) {\n selection\n .append('span')\n .text(plainText);\n }\n\n if (key) {\n selection\n .append('a')\n .attr('href', `https://wiki.openstreetmap.org/wiki/${this.toSitelink(key, value)}`)\n .attr('target', '_blank')\n .attr('rel', 'noreferrer')\n .append('code')\n .text(`${key}=${value || '*'}`);\n }\n }\n };\n },\n\n\n //\n // Pass params object of the form:\n // {\n // key: 'string',\n // value: 'string',\n // langCode: 'string'\n // }\n //\n getEntity: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n var that = this;\n var titles = [];\n var result = {};\n var rtypeSitelink = (params.key === 'type' && params.value) ? ('Relation:' + params.value).replace(/_/g, ' ').trim() : false;\n var rtypeSitelinkHistorical = (params.key === 'type' && params.value) ? ('OpenHistoricalMap/Tags/Relation/' + params.value.replace(/_/g, ' ').trim()) : false;\n var keySitelink = params.key ? this.toSitelink(params.key) : false;\n var keySitelinkHistorical = params.key ? this.toSitelink(params.key, null, true) : false;\n var tagSitelink = (params.key && params.value) ? this.toSitelink(params.key, params.value) : false;\n var tagSitelinkHistorical = (params.key && params.value) ? this.toSitelink(params.key, params.value, true) : false;\n\n if (params.langCodes) {\n params.langCodes.forEach(function(langCode) {\n if (_localeIDs[langCode] === undefined) {\n // If this is the first time we are asking about this locale,\n // fetch corresponding entity (if it exists), and cache it.\n // If there is no such entry, cache `false` value to avoid re-requesting it.\n let localeSitelink = ('Locale:' + langCode).replace(/_/g, ' ').trim();\n titles.push(localeSitelink);\n\n // initialize with false, such that if locale ID is not found in first request,\n // it will not be retried in further queries\n that.addLocale(langCode, false);\n }\n });\n }\n\n if (rtypeSitelink) {\n if (_wikibaseCache[rtypeSitelinkHistorical]) {\n result.rtype = _wikibaseCache[rtypeSitelinkHistorical];\n } else if (_wikibaseCache[rtypeSitelink]) {\n result.rtype = _wikibaseCache[rtypeSitelink];\n } else {\n titles.push(rtypeSitelink, rtypeSitelinkHistorical);\n }\n }\n\n if (keySitelink) {\n if (_wikibaseCache[keySitelinkHistorical]) {\n result.key = _wikibaseCache[keySitelinkHistorical];\n } else if (_wikibaseCache[keySitelink]) {\n result.key = _wikibaseCache[keySitelink];\n } else {\n titles.push(keySitelink, keySitelinkHistorical);\n }\n }\n\n if (tagSitelink) {\n if (_wikibaseCache[tagSitelinkHistorical]) {\n result.tag = _wikibaseCache[tagSitelinkHistorical];\n } else if (_wikibaseCache[tagSitelink]) {\n result.tag = _wikibaseCache[tagSitelink];\n } else {\n titles.push(tagSitelink, tagSitelinkHistorical);\n }\n }\n\n if (!titles.length) {\n // Nothing to do, we already had everything in the cache\n return callback(null, result);\n }\n\n // Requesting just the user language code\n // If backend recognizes the code, it will perform proper fallbacks,\n // and the result will contain the requested code. If not, all values are returned:\n // {\"zh-tw\":{\"value\":\"...\",\"language\":\"zh-tw\",\"source-language\":\"zh-hant\"}\n // {\"pt-br\":{\"value\":\"...\",\"language\":\"pt\",\"for-language\":\"pt-br\"}}\n var obj = {\n action: 'wbgetentities',\n sites: 'wiki',\n titles: titles.join('|'),\n languages: params.langCodes.join('|'),\n languagefallback: 1,\n origin: '*',\n format: 'json',\n // There is an MW Wikibase API bug https://phabricator.wikimedia.org/T212069\n // We shouldn't use v1 until it gets fixed, but should switch to it afterwards\n // formatversion: 2,\n };\n\n var url = apibase + '?' + utilQsString(obj);\n doRequest(url, function(err, d) {\n if (err) {\n callback(err);\n } else if (!d.success || d.error) {\n callback(d.error.messages.map(function(v) { return v.html['*']; }).join('
    '));\n } else {\n Object.values(d.entities).forEach(function(res) {\n if (res.missing !== '') {\n\n var title = res.sitelinks.wiki.title;\n if (title === rtypeSitelinkHistorical) {\n _wikibaseCache[rtypeSitelinkHistorical] = res;\n result.rtype = res;\n } else if (title === rtypeSitelink) {\n _wikibaseCache[rtypeSitelink] = res;\n result.rtype = res;\n } else if (title === keySitelinkHistorical) {\n _wikibaseCache[keySitelinkHistorical] = res;\n result.key = res;\n } else if (title === keySitelink) {\n _wikibaseCache[keySitelink] = res;\n result.key = res;\n } else if (title === tagSitelinkHistorical) {\n _wikibaseCache[tagSitelinkHistorical] = res;\n result.tag = res;\n } else if (title === tagSitelink) {\n _wikibaseCache[tagSitelink] = res;\n result.tag = res;\n } else if (title.startsWith('Locale:')) {\n const langCode = title.replace(/ /g, '_').replace(/^Locale:/, '');\n that.addLocale(langCode, res.id);\n } else {\n console.log('Unexpected title ' + title); // eslint-disable-line no-console\n }\n }\n });\n\n callback(null, result);\n }\n });\n },\n\n\n //\n // Pass params object of the form:\n // {\n // key: 'string', // required\n // value: 'string' // optional\n // }\n //\n // Get an result object used to display tag documentation\n // {\n // title: 'string',\n // description: 'string',\n // editURL: 'string',\n // imageURL: 'string',\n // wiki: { title: 'string', text: 'string', url: 'string' }\n // }\n //\n getDocs: function(params, callback) {\n var that = this;\n var langCodes = localizer.localeCodes().map(function(code) {\n return code.toLowerCase();\n });\n params.langCodes = langCodes;\n\n this.getEntity(params, function(err, data) {\n if (err) {\n callback(err);\n return;\n }\n\n var entity = data.rtype || data.tag || data.key;\n if (!entity) {\n callback('No entity');\n return;\n }\n\n var i;\n var description;\n for (i in langCodes) {\n let code = langCodes[i];\n if (entity.descriptions[code] && entity.descriptions[code].language === code) {\n description = entity.descriptions[code];\n break;\n }\n }\n if (!description && Object.values(entity.descriptions).length) description = Object.values(entity.descriptions)[0];\n\n // prepare result\n var result = {\n title: entity.title,\n description: that.linkifyWikiText(description?.value || ''),\n descriptionLocaleCode: description ? description.language : '',\n editURL: 'https://wiki.openstreetmap.org/wiki/' + entity.title\n };\n\n // add image\n if (entity.claims) {\n var imageroot;\n var image = that.claimToValue(entity, 'P4', langCodes[0]);\n if (image) {\n imageroot = 'https://commons.wikimedia.org/w/index.php';\n } else {\n image = that.claimToValue(entity, 'P28', langCodes[0]);\n if (image) {\n imageroot = 'https://wiki.openstreetmap.org/w/index.php';\n }\n }\n if (imageroot && image) {\n result.imageURL = imageroot + '?' + utilQsString({\n title: 'Special:Redirect/file/' + image,\n width: 400\n });\n }\n }\n\n // Try to get a wiki page from tag data item first, followed by the corresponding key data item.\n // If neither tag nor key data item contain a wiki page in the needed language nor English,\n // get the first found wiki page from either the tag or the key item.\n var rtypeWiki = that.monolingualClaimToValueObj(data.rtype, 'P31');\n var tagWiki = that.monolingualClaimToValueObj(data.tag, 'P31');\n var keyWiki = that.monolingualClaimToValueObj(data.key, 'P31');\n\n var wikis = [rtypeWiki, tagWiki, keyWiki];\n for (i in wikis) {\n var wiki = wikis[i];\n for (var j in langCodes) {\n var code = langCodes[j];\n var referenceId = (langCodes[0].split('-')[0] !== 'en' && code.split('-')[0] === 'en') ? 'inspector.wiki_en_reference' : 'inspector.wiki_reference';\n var info = getWikiInfo(wiki, code, referenceId);\n if (info) {\n result.wiki = info;\n break;\n }\n }\n if (result.wiki) break;\n }\n\n callback(null, result);\n\n\n // Helper method to get wiki info if a given language exists\n function getWikiInfo(wiki, langCode, tKey) {\n if (wiki && wiki[langCode]) {\n return {\n title: wiki[langCode],\n text: tKey,\n url: 'https://wiki.openstreetmap.org/wiki/' + wiki[langCode]\n };\n }\n }\n });\n },\n\n getLocaleIDs: () => _localeIDs,\n\n\n addLocale: function(langCode, qid) {\n // Makes it easier to unit test\n _localeIDs[langCode] = qid;\n },\n\n\n apibase: function(val) {\n if (!arguments.length) return apibase;\n apibase = val;\n return this;\n }\n\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { timer as d3_timer } from 'd3-timer';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport RBush from 'rbush';\nimport { t } from '../core/localizer';\n\nimport {\n geoExtent, geoMetersToLat, geoMetersToLon, geoPointInPolygon,\n geoRotate, geoVecLength\n} from '../geo';\n\nimport { utilAesDecrypt, utilArrayUnion, utilQsString, utilRebind, utilStringQs, utilTiler, utilUniqueDomId } from '../util';\n\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\nimport { localeTimestamp } from '../util/date';\n\n\nconst streetsideApi = 'https://dev.virtualearth.net/REST/v1/Imagery/MetaData/Streetside?mapArea={bbox}&key={key}&count={count}&uriScheme=https';\nconst maxResults = 500;\nconst bubbleAppKey = utilAesDecrypt('5c875730b09c6b422433e807e1ff060b6536c791dbfffcffc4c6b18a1bdba1f14593d151adb50e19e1be1ab19aef813bf135d0f103475e5c724dec94389e45d0');\nconst pannellumViewerCSS = 'pannellum/pannellum.css';\nconst pannellumViewerJS = 'pannellum/pannellum.js';\nconst tileZoom = 16.5;\nconst tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);\nconst dispatch = d3_dispatch('loadedImages', 'viewerChanged');\nconst minHfov = 10; // zoom in degrees: 20, 10, 5\nconst maxHfov = 90; // zoom out degrees\nconst defaultHfov = 45;\n\nlet _hires = false;\nlet _resolution = 512; // higher numbers are slower - 512, 1024, 2048, 4096\nlet _currScene = 0;\nlet _ssCache;\nlet _pannellumViewer;\nlet _sceneOptions = {\n showFullscreenCtrl: false,\n autoLoad: true,\n compass: true,\n yaw: 0,\n minHfov: minHfov,\n maxHfov: maxHfov,\n hfov: defaultHfov,\n type: 'cubemap',\n cubeMap: []\n};\nlet _loadViewerPromise;\n\n\n/**\n * abortRequest().\n */\nfunction abortRequest(i) {\n i.abort();\n}\n\n/**\n * loadTiles() wraps the process of generating tiles and then fetching image points for each tile.\n */\nfunction loadTiles(which, url, projection, margin) {\n const tiles = tiler.margin(margin).getTiles(projection);\n\n // abort inflight requests that are no longer needed\n const cache = _ssCache[which];\n Object.keys(cache.inflight).forEach(k => {\n const wanted = tiles.find(tile => k.indexOf(tile.id + ',') === 0);\n if (!wanted) {\n abortRequest(cache.inflight[k]);\n delete cache.inflight[k];\n }\n });\n\n tiles.forEach(tile => loadNextTilePage(which, url, tile));\n}\n\n\n/**\n * loadNextTilePage() load data for the next tile page in line.\n */\nfunction loadNextTilePage(which, url, tile) {\n const cache = _ssCache[which];\n const nextPage = cache.nextPage[tile.id] || 0;\n const id = tile.id + ',' + String(nextPage);\n if (cache.loaded[id] || cache.inflight[id]) return;\n\n cache.inflight[id] = getBubbles(url, tile, response => {\n cache.loaded[id] = true;\n delete cache.inflight[id];\n if (!response) return;\n\n if (response.resourceSets[0].resources.length === maxResults) {\n // there are more bubbles than the response can fit: re-fetch using tile split into 4\n const split = tile.extent.split();\n loadNextTilePage(which, url, { id: tile.id + ',a', extent: split[0] });\n loadNextTilePage(which, url, { id: tile.id + ',b', extent: split[1] });\n loadNextTilePage(which, url, { id: tile.id + ',c', extent: split[2] });\n loadNextTilePage(which, url, { id: tile.id + ',d', extent: split[3] });\n }\n\n const features = response.resourceSets[0].resources.map(bubble => {\n const bubbleId = bubble.imageUrl;\n if (cache.points[bubbleId]) return null; // skip duplicates\n\n // workaround for https://github.com/openstreetmap/iD/issues/10341#issuecomment-2275724738\n const loc = [\n bubble.lon || bubble.longitude,\n bubble.lat || bubble.latitude\n ];\n const d = {\n service: 'photo',\n loc: loc,\n key: bubbleId,\n imageUrl: bubble.imageUrl\n .replace('{subdomain}', bubble.imageUrlSubdomains[0]),\n ca: bubble.he || bubble.heading,\n captured_at: bubble.vintageEnd,\n captured_by: 'microsoft',\n pano: true,\n sequenceKey: null\n };\n\n cache.points[bubbleId] = d;\n\n return {\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n };\n\n }).filter(Boolean);\n\n cache.rtree.load(features);\n\n if (which === 'bubbles') {\n dispatch.call('loadedImages');\n }\n });\n}\n\n\n/**\n * getBubbles() handles the request to the server for a tile extent of 'bubbles' (streetside image locations).\n */\nfunction getBubbles(url, tile, callback) {\n let rect = tile.extent.rectangle();\n let urlForRequest = url\n .replace('{key}', bubbleAppKey)\n .replace('{bbox}', [rect[1], rect[0], rect[3], rect[2]].join(','))\n .replace('{count}', maxResults);\n\n const controller = new AbortController();\n fetch(urlForRequest, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n }).then(function(result) {\n if (!result) {\n callback(null);\n }\n return callback(result || []);\n }).catch(function(err) {\n if (err.name === 'AbortError') {\n // ignore aborted requests, e.g. from duplicate requests while zooming/panning the map\n } else {\n throw new Error(err);\n }\n });\n return controller;\n }\n\n\n/**\n * loadImage()\n */\nfunction loadImage(imgInfo) {\n return new Promise(resolve => {\n let img = new Image();\n img.onload = () => {\n let canvas = document.getElementById('ideditor-canvas' + imgInfo.face);\n let ctx = canvas.getContext('2d');\n ctx.drawImage(img, imgInfo.x, imgInfo.y);\n resolve({ imgInfo: imgInfo, status: 'ok' });\n };\n img.onerror = () => {\n resolve({ data: imgInfo, status: 'error' });\n };\n img.setAttribute('crossorigin', '');\n img.src = imgInfo.url;\n });\n}\n\n\n/**\n * loadCanvas()\n */\nfunction loadCanvas(imageGroup) {\n return Promise.all(imageGroup.map(loadImage))\n .then((data) => {\n let canvas = document.getElementById('ideditor-canvas' + data[0].imgInfo.face);\n const which = { '01': 0, '02': 1, '03': 2, '10': 3, '11': 4, '12': 5 };\n let face = data[0].imgInfo.face;\n _sceneOptions.cubeMap[which[face]] = canvas.toDataURL('image/jpeg', 1.0);\n return { status: 'loadCanvas for face ' + data[0].imgInfo.face + 'ok'};\n });\n}\n\n\n/**\n * loadFaces()\n */\nfunction loadFaces(faceGroup) {\n return Promise.all(faceGroup.map(loadCanvas))\n .then(() => { return { status: 'loadFaces done' }; });\n}\n\n\nfunction setupCanvas(selection, reset) {\n if (reset) {\n selection.selectAll('#ideditor-stitcher-canvases')\n .remove();\n }\n\n // Add the Streetside working canvases. These are used for 'stitching', or combining,\n // multiple images for each of the six faces, before passing to the Pannellum control as DataUrls\n selection.selectAll('#ideditor-stitcher-canvases')\n .data([0])\n .enter()\n .append('div')\n .attr('id', 'ideditor-stitcher-canvases')\n .attr('display', 'none')\n .selectAll('canvas')\n .data(['canvas01', 'canvas02', 'canvas03', 'canvas10', 'canvas11', 'canvas12'])\n .enter()\n .append('canvas')\n .attr('id', d => 'ideditor-' + d)\n .attr('width', _resolution)\n .attr('height', _resolution);\n}\n\n\nfunction qkToXY(qk) {\n let x = 0;\n let y = 0;\n let scale = 256;\n for (let i = qk.length; i > 0; i--) {\n const key = qk[i-1];\n x += (+(key === '1' || key === '3')) * scale;\n y += (+(key === '2' || key === '3')) * scale;\n scale *= 2;\n }\n return [x, y];\n}\n\n\nfunction getQuadKeys() {\n let dim = _resolution / 256;\n let quadKeys;\n\n if (dim === 16) {\n quadKeys = [\n '0000','0001','0010','0011','0100','0101','0110','0111', '1000','1001','1010','1011','1100','1101','1110','1111',\n '0002','0003','0012','0013','0102','0103','0112','0113', '1002','1003','1012','1013','1102','1103','1112','1113',\n '0020','0021','0030','0031','0120','0121','0130','0131', '1020','1021','1030','1031','1120','1121','1130','1131',\n '0022','0023','0032','0033','0122','0123','0132','0133', '1022','1023','1032','1033','1122','1123','1132','1133',\n '0200','0201','0210','0211','0300','0301','0310','0311', '1200','1201','1210','1211','1300','1301','1310','1311',\n '0202','0203','0212','0213','0302','0303','0312','0313', '1202','1203','1212','1213','1302','1303','1312','1313',\n '0220','0221','0230','0231','0320','0321','0330','0331', '1220','1221','1230','1231','1320','1321','1330','1331',\n '0222','0223','0232','0233','0322','0323','0332','0333', '1222','1223','1232','1233','1322','1323','1332','1333',\n\n '2000','2001','2010','2011','2100','2101','2110','2111', '3000','3001','3010','3011','3100','3101','3110','3111',\n '2002','2003','2012','2013','2102','2103','2112','2113', '3002','3003','3012','3013','3102','3103','3112','3113',\n '2020','2021','2030','2031','2120','2121','2130','2131', '3020','3021','3030','3031','3120','3121','3130','3131',\n '2022','2023','2032','2033','2122','2123','2132','2133', '3022','3023','3032','3033','3122','3123','3132','3133',\n '2200','2201','2210','2211','2300','2301','2310','2311', '3200','3201','3210','3211','3300','3301','3310','3311',\n '2202','2203','2212','2213','2302','2303','2312','2313', '3202','3203','3212','3213','3302','3303','3312','3313',\n '2220','2221','2230','2231','2320','2321','2330','2331', '3220','3221','3230','3231','3320','3321','3330','3331',\n '2222','2223','2232','2233','2322','2323','2332','2333', '3222','3223','3232','3233','3322','3323','3332','3333'\n ];\n\n } else if (dim === 8) {\n quadKeys = [\n '000','001','010','011', '100','101','110','111',\n '002','003','012','013', '102','103','112','113',\n '020','021','030','031', '120','121','130','131',\n '022','023','032','033', '122','123','132','133',\n\n '200','201','210','211', '300','301','310','311',\n '202','203','212','213', '302','303','312','313',\n '220','221','230','231', '320','321','330','331',\n '222','223','232','233', '322','323','332','333'\n ];\n\n } else if (dim === 4) {\n quadKeys = [\n '00','01', '10','11',\n '02','03', '12','13',\n\n '20','21', '30','31',\n '22','23', '32','33'\n ];\n\n } else { // dim === 2\n quadKeys = [\n '0', '1',\n '2', '3'\n ];\n }\n\n return quadKeys;\n}\n\n\n\nexport default {\n /**\n * init() initialize streetside.\n */\n init: function() {\n if (!_ssCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n /**\n * reset() reset the cache.\n */\n reset: function() {\n if (_ssCache) {\n Object.values(_ssCache.bubbles.inflight).forEach(abortRequest);\n }\n\n _ssCache = {\n bubbles: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), points: {} },\n sequences: {}\n };\n },\n\n /**\n * bubbles()\n */\n bubbles: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _ssCache.bubbles.rtree);\n },\n\n\n cachedImage: function(imageKey) {\n return _ssCache.bubbles.points[imageKey];\n },\n\n\n sequences: function(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n let seen = {};\n let results = [];\n\n // all sequences for bubbles in viewport\n _ssCache.bubbles.rtree.search(bbox)\n .forEach(d => {\n const key = d.data.sequenceKey;\n if (key && !seen[key]) {\n seen[key] = true;\n results.push(_ssCache.sequences[key].geojson);\n }\n });\n\n return results;\n },\n\n\n /**\n * loadBubbles()\n */\n loadBubbles: function(projection, margin) {\n // by default: request 2 nearby tiles so we can connect sequences.\n if (margin === undefined) margin = 2;\n\n loadTiles('bubbles', streetsideApi, projection, margin);\n },\n\n\n viewer: function() {\n return _pannellumViewer;\n },\n\n\n initViewer: function () {\n if (!window.pannellum) return;\n if (_pannellumViewer) return;\n\n _currScene += 1;\n const sceneID = _currScene.toString();\n const options = {\n 'default': { firstScene: sceneID },\n scenes: {}\n };\n options.scenes[sceneID] = _sceneOptions;\n\n _pannellumViewer = window.pannellum.viewer('ideditor-viewer-streetside', options);\n },\n\n\n ensureViewerLoaded: function(context) {\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n // create ms-wrapper, a photo wrapper class\n let wrap = context.container().select('.photoviewer').selectAll('.ms-wrapper')\n .data([0]);\n\n // inject ms-wrapper into the photoviewer div\n // (used by all to house each custom photo viewer)\n let wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper ms-wrapper')\n .classed('hide', true);\n\n let that = this;\n\n let pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n // inject div to support streetside viewer (pannellum) and attribution line\n wrapEnter\n .append('div')\n .attr('id', 'ideditor-viewer-streetside')\n .on(pointerPrefix + 'down.streetside', () => {\n d3_select(window)\n .on(pointerPrefix + 'move.streetside', () => {\n dispatch.call('viewerChanged');\n }, true);\n })\n .on(pointerPrefix + 'up.streetside pointercancel.streetside', () => {\n d3_select(window)\n .on(pointerPrefix + 'move.streetside', null);\n\n // continue dispatching events for a few seconds, in case viewer has inertia.\n let t = d3_timer(elapsed => {\n dispatch.call('viewerChanged');\n if (elapsed > 2000) {\n t.stop();\n }\n });\n })\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n let controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls');\n\n controlsEnter\n .append('button')\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .on('click.forward', step(1))\n .text('\u25BA');\n\n\n // create working canvas for stitching together images\n wrap\n .merge(wrapEnter)\n .call(setupCanvas, true);\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.streetside', () => {\n if (_pannellumViewer) {\n _pannellumViewer.resize();\n }\n });\n\n _loadViewerPromise = new Promise((resolve, reject) => {\n\n let loadedCount = 0;\n function loaded() {\n loadedCount += 1;\n // wait until both files are loaded\n if (loadedCount === 2) resolve();\n }\n\n const head = d3_select('head');\n\n // load streetside pannellum viewer css\n head.selectAll('#ideditor-streetside-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-streetside-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(pannellumViewerCSS))\n .on('load.serviceStreetside', loaded)\n .on('error.serviceStreetside', function() {\n reject();\n });\n\n // load streetside pannellum viewer js\n head.selectAll('#ideditor-streetside-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-streetside-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(pannellumViewerJS))\n .on('load.serviceStreetside', loaded)\n .on('error.serviceStreetside', function() {\n reject();\n });\n })\n .catch(function() {\n _loadViewerPromise = null;\n });\n\n return _loadViewerPromise;\n\n function step(stepBy) {\n return () => {\n let viewer = context.container().select('.photoviewer');\n let selected = viewer.empty() ? undefined : viewer.datum();\n if (!selected) return;\n\n let nextID = (stepBy === 1 ? selected.ne : selected.pr);\n let yaw = _pannellumViewer.getYaw();\n let ca = selected.ca + yaw;\n let origin = selected.loc;\n\n // construct a search trapezoid pointing out from current bubble\n const meters = 35;\n let p1 = [\n origin[0] + geoMetersToLon(meters / 5, origin[1]),\n origin[1]\n ];\n let p2 = [\n origin[0] + geoMetersToLon(meters / 2, origin[1]),\n origin[1] + geoMetersToLat(meters)\n ];\n let p3 = [\n origin[0] - geoMetersToLon(meters / 2, origin[1]),\n origin[1] + geoMetersToLat(meters)\n ];\n let p4 = [\n origin[0] - geoMetersToLon(meters / 5, origin[1]),\n origin[1]\n ];\n\n let poly = [p1, p2, p3, p4, p1];\n\n // rotate it to face forward/backward\n let angle = (stepBy === 1 ? ca : ca + 180) * (Math.PI / 180);\n poly = geoRotate(poly, -angle, origin);\n\n let extent = poly.reduce((extent, point) => {\n return extent.extend(geoExtent(point));\n }, geoExtent());\n\n // find nearest other bubble in the search polygon\n let minDist = Infinity;\n _ssCache.bubbles.rtree.search(extent.bbox())\n .forEach(d => {\n if (d.data.key === selected.key) return;\n if (!geoPointInPolygon(d.data.loc, poly)) return;\n\n let dist = geoVecLength(d.data.loc, selected.loc);\n let theta = selected.ca - d.data.ca;\n let minTheta = Math.min(Math.abs(theta), 360 - Math.abs(theta));\n if (minTheta > 20) {\n dist += 5; // penalize distance if camera angles don't match\n }\n\n if (dist < minDist) {\n nextID = d.data.key;\n minDist = dist;\n }\n });\n\n let nextBubble = nextID && that.cachedImage(nextID);\n if (!nextBubble) return;\n\n context.map().centerEase(nextBubble.loc);\n\n that.selectImage(context, nextBubble.key)\n .yaw(yaw)\n .showViewer(context);\n };\n }\n },\n\n\n yaw: function(yaw) {\n if (typeof yaw !== 'number') return yaw;\n _sceneOptions.yaw = yaw;\n return this;\n },\n\n /**\n * showViewer()\n */\n showViewer: function(context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.ms-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap\n .classed('hide', false)\n .selectAll('.photo-wrapper.ms-wrapper')\n .classed('hide', false);\n }\n\n return this;\n },\n\n\n /**\n * hideViewer()\n */\n hideViewer: function (context) {\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n this.updateUrlImage(null);\n\n return this.setStyles(context, null, true);\n },\n\n\n /**\n * selectImage().\n */\n selectImage: function (context, key) {\n let that = this;\n\n let d = this.cachedImage(key);\n\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null, true);\n\n let wrap = context.container().select('.photoviewer .ms-wrapper');\n let attribution = wrap.selectAll('.photo-attribution').html('');\n\n wrap.selectAll('.pnlm-load-box') // display \"loading..\"\n .style('display', 'block');\n\n if (!d) return this;\n\n this.updateUrlImage(key);\n\n _sceneOptions.northOffset = d.ca;\n\n let line1 = attribution\n .append('div')\n .attr('class', 'attribution-row');\n\n const hiresDomId = utilUniqueDomId('streetside-hires');\n\n // Add hires checkbox\n let label = line1\n .append('label')\n .attr('for', hiresDomId)\n .attr('class', 'streetside-hires');\n\n label\n .append('input')\n .attr('type', 'checkbox')\n .attr('id', hiresDomId)\n .property('checked', _hires)\n .on('click', (d3_event) => {\n d3_event.stopPropagation();\n\n _hires = !_hires;\n _resolution = _hires ? 1024 : 512;\n wrap.call(setupCanvas, true);\n\n let viewstate = {\n yaw: _pannellumViewer.getYaw(),\n pitch: _pannellumViewer.getPitch(),\n hfov: _pannellumViewer.getHfov()\n };\n\n _sceneOptions = Object.assign(_sceneOptions, viewstate);\n that.selectImage(context, d.key)\n .showViewer(context);\n });\n\n label\n .append('span')\n .call(t.append('streetside.hires'));\n\n\n let captureInfo = line1\n .append('div')\n .attr('class', 'attribution-capture-info');\n\n // Add capture date\n if (d.captured_by) {\n const yyyy = (new Date()).getFullYear();\n\n captureInfo\n .append('a')\n .attr('class', 'captured_by')\n .attr('target', '_blank')\n .attr('href', 'https://www.microsoft.com/en-us/maps/streetside')\n .text('\u00A9' + yyyy + ' Microsoft');\n\n captureInfo\n .append('span')\n .text('|');\n }\n\n if (d.captured_at) {\n captureInfo\n .append('span')\n .attr('class', 'captured_at')\n .text(localeTimestamp(d.captured_at));\n }\n\n // Add image links\n let line2 = attribution\n .append('div')\n .attr('class', 'attribution-row');\n\n line2\n .append('a')\n .attr('class', 'image-view-link')\n .attr('target', '_blank')\n .attr('href', 'https://www.bing.com/maps?cp=' + d.loc[1] + '~' + d.loc[0] +\n '&lvl=17&dir=' + d.ca + '&style=x&v=2&sV=1')\n .call(t.append('streetside.view_on_bing'));\n\n line2\n .append('a')\n .attr('class', 'image-report-link')\n .attr('target', '_blank')\n .attr('href', 'https://www.bing.com/maps/privacyreport/streetsideprivacyreport?bubbleid=' +\n encodeURIComponent(d.key) + '&focus=photo&lat=' + d.loc[1] + '&lng=' + d.loc[0] + '&z=17')\n .call(t.append('streetside.report'));\n\n // Cubemap face code order matters here: front=01, right=02, back=03, left=10, up=11, down=12\n const faceKeys = ['01','02','03','10','11','12'];\n\n // Map images to cube faces\n let quadKeys = getQuadKeys();\n let faces = faceKeys.map((faceKey) => {\n return quadKeys.map((quadKey) => {\n const xy = qkToXY(quadKey);\n return {\n face: faceKey,\n url: d.imageUrl\n .replace('{faceId}', faceKey)\n .replace('{tileId}', quadKey),\n x: xy[0],\n y: xy[1]\n };\n });\n });\n\n loadFaces(faces)\n .then(function() {\n\n if (!_pannellumViewer) {\n that.initViewer();\n } else {\n // make a new scene\n _currScene += 1;\n let sceneID = _currScene.toString();\n _pannellumViewer\n .addScene(sceneID, _sceneOptions)\n .loadScene(sceneID);\n\n // remove previous scene\n if (_currScene > 2) {\n sceneID = (_currScene - 1).toString();\n _pannellumViewer\n .removeScene(sceneID);\n }\n }\n });\n\n return this;\n },\n\n\n getSequenceKeyForBubble: function(d) {\n return d && d.sequenceKey;\n },\n\n\n // Updates the currently highlighted sequence and selected bubble.\n // Reset is only necessary when interacting with the viewport because\n // this implicitly changes the currently selected bubble/sequence\n setStyles: function (context, hovered, reset) {\n if (reset) { // reset all layers\n context.container().selectAll('.viewfield-group')\n .classed('highlighted', false)\n .classed('hovered', false)\n .classed('currentView', false);\n\n context.container().selectAll('.sequence')\n .classed('highlighted', false)\n .classed('currentView', false);\n }\n\n let hoveredBubbleKey = hovered && hovered.key;\n let hoveredSequenceKey = this.getSequenceKeyForBubble(hovered);\n let hoveredSequence = hoveredSequenceKey && _ssCache.sequences[hoveredSequenceKey];\n let hoveredBubbleKeys = (hoveredSequence && hoveredSequence.bubbles.map(d => d.key)) || [];\n\n let viewer = context.container().select('.photoviewer');\n let selected = viewer.empty() ? undefined : viewer.datum();\n let selectedBubbleKey = selected && selected.key;\n let selectedSequenceKey = this.getSequenceKeyForBubble(selected);\n let selectedSequence = selectedSequenceKey && _ssCache.sequences[selectedSequenceKey];\n let selectedBubbleKeys = (selectedSequence && selectedSequence.bubbles.map(d => d.key)) || [];\n\n // highlight sibling viewfields on either the selected or the hovered sequences\n let highlightedBubbleKeys = utilArrayUnion(hoveredBubbleKeys, selectedBubbleKeys);\n\n context.container().selectAll('.layer-streetside-images .viewfield-group')\n .classed('highlighted', d => highlightedBubbleKeys.indexOf(d.key) !== -1)\n .classed('hovered', d => d.key === hoveredBubbleKey)\n .classed('currentView', d => d.key === selectedBubbleKey);\n\n context.container().selectAll('.layer-streetside-images .sequence')\n .classed('highlighted', d => d.properties.key === hoveredSequenceKey)\n .classed('currentView', d => d.properties.key === selectedSequenceKey);\n\n // update viewfields if needed\n context.container().selectAll('.layer-streetside-images .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n let d = this.parentNode.__data__;\n if (d.pano && d.key !== selectedBubbleKey) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n return this;\n },\n\n\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'streetside/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n\n /**\n * cache().\n */\n cache: function () {\n return _ssCache;\n }\n};\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { json as d3_json } from 'd3-fetch';\n\nimport { utilObjectOmit, utilQsString } from '../util';\nimport { localizer } from '../core/localizer';\nimport { allowUpperCaseTagValues } from '../osm/tags';\n\nimport { taginfoApiUrl } from '../../config/id.js';\n\nvar apibase = taginfoApiUrl;\nvar _inflight = {};\nvar _popularKeys = {};\n// manually exclude some additional keys \u2013 #5377, #7485, #10287, #11733\n// these will be returned by keys(), but taginfo will not be queried for values() requests\nvar _extraExcludedKeys = /^(addr:.+|postal_code|via|((int_|loc_|nat_|official_|old_|ref_|reg_|short_|full_|sorting_|alt_|artist_|long_|bridge:|tunnel:)?name(:left|:right)?(:[a-z]+)?))$/;\n\nvar _extraExcludedKeyNames = /^(hashtags?|created_by)$/;\n\nvar _taginfoCache = {};\n\nvar tag_sorts = {\n point: 'count_nodes',\n vertex: 'count_nodes',\n area: 'count_ways',\n line: 'count_ways'\n};\nvar tag_sort_members = {\n point: 'count_node_members',\n vertex: 'count_node_members',\n area: 'count_way_members',\n line: 'count_way_members',\n relation: 'count_relation_members'\n};\nvar tag_filters = {\n point: 'nodes',\n vertex: 'nodes',\n area: 'ways',\n line: 'ways'\n};\nvar tag_members_fractions = {\n point: 'count_node_members_fraction',\n vertex: 'count_node_members_fraction',\n area: 'count_way_members_fraction',\n line: 'count_way_members_fraction',\n relation: 'count_relation_members_fraction'\n};\n\n\nfunction sets(params, n, o) {\n if (params.geometry && o[params.geometry]) {\n params[n] = o[params.geometry];\n }\n return params;\n}\n\n\nfunction setFilter(params) {\n return sets(params, 'filter', tag_filters);\n}\n\n\nfunction setSort(params) {\n return sets(params, 'sortname', tag_sorts);\n}\n\n\nfunction setSortMembers(params) {\n return sets(params, 'sortname', tag_sort_members);\n}\n\n\nfunction clean(params) {\n return utilObjectOmit(params, ['geometry', 'debounce']);\n}\n\n\nfunction filterKeys(type) {\n var count_type = type ? 'count_' + type : 'count_all';\n return function(d) {\n return Number(d[count_type]) > 2500 || d.in_wiki;\n };\n}\n\n\nfunction filterMultikeys(prefix) {\n return function(d) {\n // d.key begins with prefix, and d.key contains no additional ':'s\n var re = new RegExp('^' + prefix + '(.*)$', 'i');\n var matches = d.key.match(re) || [];\n return (matches.length === 2 && matches[1].indexOf(':') === -1);\n };\n}\n\n\nfunction filterValues(allowUpperCase, key) {\n return function(d) {\n if (d.value.match(/[;,]/) !== null) return false; // exclude some punctuation\n if (!allowUpperCase &&\n !(key === 'type' && d.value === 'associatedStreet') &&\n d.value.match(/[A-Z*]/) !== null) return false; // exclude uppercase letters\n return d.count > 100; // exclude rare tags\n };\n}\n\n\nfunction filterRoles(geometry) {\n return function(d) {\n if (d.role === '') return false; // exclude empty role\n if (d.role.match(/[A-Z*;,]/) !== null) return false; // exclude uppercase letters and some punctuation\n return Number(d[tag_members_fractions[geometry]]) > 0.0;\n };\n}\n\n\nfunction valKey(d) {\n return {\n value: d.key,\n title: d.key\n };\n}\n\n\nfunction valKeyDescription(d) {\n var obj = {\n value: d.value,\n title: d.description || d.value\n };\n return obj;\n}\n\n\nfunction roleKey(d) {\n return {\n value: d.role,\n title: d.role\n };\n}\n\n\n// sort keys with ':' lower than keys without ':'\nfunction sortKeys(a, b) {\n return (a.key.indexOf(':') === -1 && b.key.indexOf(':') !== -1) ? -1\n : (a.key.indexOf(':') !== -1 && b.key.indexOf(':') === -1) ? 1\n : 0;\n}\n\n\nvar debouncedRequest = debounce(request, 300, { leading: false });\n\nfunction request(url, params, exactMatch, callback, loaded) {\n if (_inflight[url]) return;\n\n if (checkCache(url, params, exactMatch, callback)) return;\n\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, { signal: controller.signal })\n .then(function(result) {\n delete _inflight[url];\n if (loaded) loaded(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (loaded) loaded(err.message);\n });\n}\n\n\nfunction checkCache(url, params, exactMatch, callback) {\n var rp = params.rp || 25;\n var testQuery = params.query || '';\n var testUrl = url;\n\n do {\n var hit = _taginfoCache[testUrl];\n\n // exact match, or shorter match yielding fewer than max results (rp)\n if (hit && (url === testUrl || hit.length < rp)) {\n callback(null, hit);\n return true;\n }\n\n // don't try to shorten the query\n if (exactMatch || !testQuery.length) return false;\n\n // do shorten the query to see if we already have a cached result\n // that has returned fewer than max results (rp)\n testQuery = testQuery.slice(0, -1);\n testUrl = url.replace(/&query=(.*?)&/, '&query=' + testQuery + '&');\n } while (testQuery.length >= 0);\n\n return false;\n}\n\n\nexport default {\n\n init: function() {\n _inflight = {};\n _taginfoCache = {};\n _popularKeys = [];\n\n // Fetch popular keys. We'll exclude these from `values`\n // lookups because they stress taginfo, and they aren't likely\n // to yield meaningful autocomplete results.. see #3955\n var params = {\n rp: 100,\n sortname: 'values_all',\n sortorder: 'desc',\n page: 1,\n debounce: false,\n lang: localizer.languageCode()\n };\n this.keys(params, function(err, data) {\n if (err) return;\n data.forEach(function(d) {\n if (d.value === 'opening_hours') return; // exception\n _popularKeys[d.value] = true;\n });\n });\n },\n\n\n reset: function() {\n Object.values(_inflight).forEach(function(controller) { controller.abort(); });\n _inflight = {};\n },\n\n\n keys: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(params));\n params = Object.assign({\n rp: 10,\n sortname: 'count_all',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var url = apibase + 'keys/all?' + utilQsString(params);\n doRequest(url, params, false, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n var f = filterKeys(params.filter);\n var result = d.data.filter(f).filter(d => !_extraExcludedKeyNames.test(d.key)).sort(sortKeys).map(valKey);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n multikeys: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(params));\n params = Object.assign({\n rp: 25,\n sortname: 'count_all',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var prefix = params.query;\n var url = apibase + 'keys/all?' + utilQsString(params);\n doRequest(url, params, true, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n var f = filterMultikeys(prefix);\n var result = d.data.filter(f).map(valKey);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n values: function(params, callback) {\n // Exclude popular keys from values lookups.. see #3955\n var key = params.key;\n if (key && _popularKeys[key] === true || _extraExcludedKeys.test(key)) {\n callback(null, []);\n return;\n }\n\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(setFilter(params)));\n params = Object.assign({\n rp: 25,\n sortname: 'count_all',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var url = apibase + 'key/values?' + utilQsString(params);\n doRequest(url, params, false, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n // In most cases we prefer taginfo value results with lowercase letters.\n // A few OSM keys expect values to contain uppercase values (see #3377).\n // This is not an exhaustive list (e.g. `name` also has uppercase values)\n // but these are the fields where taginfo value lookup is most useful.\n var allowUpperCase = allowUpperCaseTagValues.test(params.key);\n var f = filterValues(allowUpperCase, params.key);\n\n var result = d.data.filter(f).map(valKeyDescription);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n roles: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n var geometry = params.geometry;\n params = clean(setSortMembers(params));\n params = Object.assign({\n rp: 25,\n sortname: 'count_all_members',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var url = apibase + 'relation/roles?' + utilQsString(params);\n doRequest(url, params, true, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n var f = filterRoles(geometry);\n var result = d.data.filter(f).map(roleKey);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n docs: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(params));\n\n var path = 'key/wiki_pages?';\n if (params.value) {\n path = 'tag/wiki_pages?';\n } else if (params.rtype) {\n path = 'relation/wiki_pages?';\n }\n\n var url = apibase + path + utilQsString(params);\n doRequest(url, params, true, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n _taginfoCache[url] = d.data;\n callback(null, d.data);\n }\n });\n },\n\n\n apibase: function(_) {\n if (!arguments.length) return apibase;\n apibase = _;\n return this;\n }\n\n};\n", "import {\n BBox,\n Feature,\n FeatureCollection,\n Geometry,\n GeometryCollection,\n GeometryObject,\n LineString,\n MultiLineString,\n MultiPoint,\n MultiPolygon,\n Point,\n Polygon,\n Position,\n GeoJsonProperties,\n} from \"geojson\";\n\nimport { Id } from \"./lib/geojson.js\";\nexport * from \"./lib/geojson.js\";\n\n/**\n * @module helpers\n */\n\n// TurfJS Combined Types\nexport type Coord = Feature | Point | Position;\n\n/**\n * Linear measurement units.\n *\n * ⚠️ Warning. Be aware of the implications of using radian or degree units to\n * measure distance. The distance represented by a degree of longitude *varies*\n * depending on latitude.\n *\n * See https://www.thoughtco.com/degree-of-latitude-and-longitude-distance-4070616\n * for an illustration of this behaviour.\n *\n * @typedef\n */\nexport type Units =\n | \"meters\"\n | \"metres\"\n | \"millimeters\"\n | \"millimetres\"\n | \"centimeters\"\n | \"centimetres\"\n | \"kilometers\"\n | \"kilometres\"\n | \"miles\"\n | \"nauticalmiles\"\n | \"inches\"\n | \"yards\"\n | \"feet\"\n | \"radians\"\n | \"degrees\";\n\n/**\n * Area measurement units.\n *\n * @typedef\n */\nexport type AreaUnits =\n | Exclude\n | \"acres\"\n | \"hectares\";\n\n/**\n * Grid types.\n *\n * @typedef\n */\nexport type Grid = \"point\" | \"square\" | \"hex\" | \"triangle\";\n\n/**\n * Shorthand corner identifiers.\n *\n * @typedef\n */\nexport type Corners = \"sw\" | \"se\" | \"nw\" | \"ne\" | \"center\" | \"centroid\";\n\n/**\n * Geometries made up of lines i.e. lines and polygons.\n *\n * @typedef\n */\nexport type Lines = LineString | MultiLineString | Polygon | MultiPolygon;\n\n/**\n * Convenience type for all possible GeoJSON.\n *\n * @typedef\n */\nexport type AllGeoJSON =\n | Feature\n | FeatureCollection\n | Geometry\n | GeometryCollection;\n\n/**\n * The Earth radius in meters. Used by Turf modules that model the Earth as a sphere. The {@link https://en.wikipedia.org/wiki/Earth_radius#Arithmetic_mean_radius mean radius} was selected because it is {@link https://rosettacode.org/wiki/Haversine_formula#:~:text=This%20value%20is%20recommended recommended } by the Haversine formula (used by turf/distance) to reduce error.\n *\n * @constant\n */\nexport const earthRadius = 6371008.8;\n\n/**\n * Unit of measurement factors based on earthRadius.\n *\n * Keys are the name of the unit, values are the number of that unit in a single radian\n *\n * @constant\n */\nexport const factors: Record = {\n centimeters: earthRadius * 100,\n centimetres: earthRadius * 100,\n degrees: 360 / (2 * Math.PI),\n feet: earthRadius * 3.28084,\n inches: earthRadius * 39.37,\n kilometers: earthRadius / 1000,\n kilometres: earthRadius / 1000,\n meters: earthRadius,\n metres: earthRadius,\n miles: earthRadius / 1609.344,\n millimeters: earthRadius * 1000,\n millimetres: earthRadius * 1000,\n nauticalmiles: earthRadius / 1852,\n radians: 1,\n yards: earthRadius * 1.0936,\n};\n\n/**\n\n * Area of measurement factors based on 1 square meter.\n *\n * @constant\n */\nexport const areaFactors: Record = {\n acres: 0.000247105,\n centimeters: 10000,\n centimetres: 10000,\n feet: 10.763910417,\n hectares: 0.0001,\n inches: 1550.003100006,\n kilometers: 0.000001,\n kilometres: 0.000001,\n meters: 1,\n metres: 1,\n miles: 3.86e-7,\n nauticalmiles: 2.9155334959812285e-7,\n millimeters: 1000000,\n millimetres: 1000000,\n yards: 1.195990046,\n};\n\n/**\n * Wraps a GeoJSON {@link Geometry} in a GeoJSON {@link Feature}.\n *\n * @function\n * @param {GeometryObject} geometry input geometry\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a GeoJSON Feature\n * @example\n * var geometry = {\n * \"type\": \"Point\",\n * \"coordinates\": [110, 50]\n * };\n *\n * var feature = turf.feature(geometry);\n *\n * //=feature\n */\nexport function feature<\n G extends GeometryObject = Geometry,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geom: G | null,\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const feat: any = { type: \"Feature\" };\n if (options.id === 0 || options.id) {\n feat.id = options.id;\n }\n if (options.bbox) {\n feat.bbox = options.bbox;\n }\n feat.properties = properties || {};\n feat.geometry = geom;\n return feat;\n}\n\n/**\n * Creates a GeoJSON {@link Geometry} from a Geometry string type & coordinates.\n * For GeometryCollection type use `helpers.geometryCollection`\n *\n * @function\n * @param {(\"Point\" | \"LineString\" | \"Polygon\" | \"MultiPoint\" | \"MultiLineString\" | \"MultiPolygon\")} type Geometry Type\n * @param {Array} coordinates Coordinates\n * @param {Object} [options={}] Optional Parameters\n * @returns {Geometry} a GeoJSON Geometry\n * @example\n * var type = \"Point\";\n * var coordinates = [110, 50];\n * var geometry = turf.geometry(type, coordinates);\n * // => geometry\n */\nexport function geometry<\n T extends\n | \"Point\"\n | \"LineString\"\n | \"Polygon\"\n | \"MultiPoint\"\n | \"MultiLineString\"\n | \"MultiPolygon\",\n>(\n type: T,\n coordinates: any[],\n _options: Record = {}\n): Extract {\n switch (type) {\n case \"Point\":\n return point(coordinates).geometry as Extract;\n case \"LineString\":\n return lineString(coordinates).geometry as Extract;\n case \"Polygon\":\n return polygon(coordinates).geometry as Extract;\n case \"MultiPoint\":\n return multiPoint(coordinates).geometry as Extract;\n case \"MultiLineString\":\n return multiLineString(coordinates).geometry as Extract<\n Geometry,\n { type: T }\n >;\n case \"MultiPolygon\":\n return multiPolygon(coordinates).geometry as Extract<\n Geometry,\n { type: T }\n >;\n default:\n throw new Error(type + \" is invalid\");\n }\n}\n\n/**\n * Creates a {@link Point} {@link Feature} from a Position.\n *\n * @function\n * @param {Position} coordinates longitude, latitude position (each in decimal degrees)\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a Point feature\n * @example\n * var point = turf.point([-75.343, 39.984]);\n *\n * //=point\n */\nexport function point

    (\n coordinates: Position,\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n if (!coordinates) {\n throw new Error(\"coordinates is required\");\n }\n if (!Array.isArray(coordinates)) {\n throw new Error(\"coordinates must be an Array\");\n }\n if (coordinates.length < 2) {\n throw new Error(\"coordinates must be at least 2 numbers long\");\n }\n if (!isNumber(coordinates[0]) || !isNumber(coordinates[1])) {\n throw new Error(\"coordinates must contain numbers\");\n }\n\n const geom: Point = {\n type: \"Point\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Point} {@link FeatureCollection} from an Array of Point coordinates.\n *\n * @function\n * @param {Position[]} coordinates an array of Points\n * @param {GeoJsonProperties} [properties={}] Translate these properties to each Feature\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north]\n * associated with the FeatureCollection\n * @param {Id} [options.id] Identifier associated with the FeatureCollection\n * @returns {FeatureCollection} Point Feature\n * @example\n * var points = turf.points([\n * [-75, 39],\n * [-80, 45],\n * [-78, 50]\n * ]);\n *\n * //=points\n */\nexport function points

    (\n coordinates: Position[],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n return featureCollection(\n coordinates.map((coords) => {\n return point(coords, properties);\n }),\n options\n );\n}\n\n/**\n * Creates a {@link Polygon} {@link Feature} from an Array of LinearRings.\n *\n * @function\n * @param {Position[][]} coordinates an array of LinearRings\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} Polygon Feature\n * @example\n * var polygon = turf.polygon([[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]], { name: 'poly1' });\n *\n * //=polygon\n */\nexport function polygon

    (\n coordinates: Position[][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n for (const ring of coordinates) {\n if (ring.length < 4) {\n throw new Error(\n \"Each LinearRing of a Polygon must have 4 or more Positions.\"\n );\n }\n\n if (ring[ring.length - 1].length !== ring[0].length) {\n throw new Error(\"First and last Position are not equivalent.\");\n }\n\n for (let j = 0; j < ring[ring.length - 1].length; j++) {\n // Check if first point of Polygon contains two numbers\n if (ring[ring.length - 1][j] !== ring[0][j]) {\n throw new Error(\"First and last Position are not equivalent.\");\n }\n }\n }\n const geom: Polygon = {\n type: \"Polygon\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Polygon} {@link FeatureCollection} from an Array of Polygon coordinates.\n *\n * @function\n * @param {Position[][][]} coordinates an array of Polygon coordinates\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the FeatureCollection\n * @returns {FeatureCollection} Polygon FeatureCollection\n * @example\n * var polygons = turf.polygons([\n * [[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]],\n * [[[-15, 42], [-14, 46], [-12, 41], [-17, 44], [-15, 42]]],\n * ]);\n *\n * //=polygons\n */\nexport function polygons

    (\n coordinates: Position[][][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n return featureCollection(\n coordinates.map((coords) => {\n return polygon(coords, properties);\n }),\n options\n );\n}\n\n/**\n * Creates a {@link LineString} {@link Feature} from an Array of Positions.\n *\n * @function\n * @param {Position[]} coordinates an array of Positions\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} LineString Feature\n * @example\n * var linestring1 = turf.lineString([[-24, 63], [-23, 60], [-25, 65], [-20, 69]], {name: 'line 1'});\n * var linestring2 = turf.lineString([[-14, 43], [-13, 40], [-15, 45], [-10, 49]], {name: 'line 2'});\n *\n * //=linestring1\n * //=linestring2\n */\nexport function lineString

    (\n coordinates: Position[],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n if (coordinates.length < 2) {\n throw new Error(\"coordinates must be an array of two or more positions\");\n }\n const geom: LineString = {\n type: \"LineString\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link LineString} {@link FeatureCollection} from an Array of LineString coordinates.\n *\n * @function\n * @param {Position[][]} coordinates an array of LinearRings\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north]\n * associated with the FeatureCollection\n * @param {Id} [options.id] Identifier associated with the FeatureCollection\n * @returns {FeatureCollection} LineString FeatureCollection\n * @example\n * var linestrings = turf.lineStrings([\n * [[-24, 63], [-23, 60], [-25, 65], [-20, 69]],\n * [[-14, 43], [-13, 40], [-15, 45], [-10, 49]]\n * ]);\n *\n * //=linestrings\n */\nexport function lineStrings

    (\n coordinates: Position[][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n return featureCollection(\n coordinates.map((coords) => {\n return lineString(coords, properties);\n }),\n options\n );\n}\n\n/**\n * Takes one or more {@link Feature|Features} and creates a {@link FeatureCollection}.\n *\n * @function\n * @param {Array>} features input features\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {FeatureCollection} FeatureCollection of Features\n * @example\n * var locationA = turf.point([-75.343, 39.984], {name: 'Location A'});\n * var locationB = turf.point([-75.833, 39.284], {name: 'Location B'});\n * var locationC = turf.point([-75.534, 39.123], {name: 'Location C'});\n *\n * var collection = turf.featureCollection([\n * locationA,\n * locationB,\n * locationC\n * ]);\n *\n * //=collection\n */\nexport function featureCollection<\n G extends GeometryObject = Geometry,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n features: Array>,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n const fc: any = { type: \"FeatureCollection\" };\n if (options.id) {\n fc.id = options.id;\n }\n if (options.bbox) {\n fc.bbox = options.bbox;\n }\n fc.features = features;\n return fc;\n}\n\n/**\n * Creates a {@link Feature}<{@link MultiLineString}> based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Position[][]} coordinates an array of LineStrings\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a MultiLineString feature\n * @throws {Error} if no coordinates are passed\n * @example\n * var multiLine = turf.multiLineString([[[0,0],[10,10]]]);\n *\n * //=multiLine\n */\nexport function multiLineString<\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n coordinates: Position[][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const geom: MultiLineString = {\n type: \"MultiLineString\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Feature}<{@link MultiPoint}> based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Position[]} coordinates an array of Positions\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a MultiPoint feature\n * @throws {Error} if no coordinates are passed\n * @example\n * var multiPt = turf.multiPoint([[0,0],[10,10]]);\n *\n * //=multiPt\n */\nexport function multiPoint

    (\n coordinates: Position[],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const geom: MultiPoint = {\n type: \"MultiPoint\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Feature}<{@link MultiPolygon}> based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Position[][][]} coordinates an array of Polygons\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a multipolygon feature\n * @throws {Error} if no coordinates are passed\n * @example\n * var multiPoly = turf.multiPolygon([[[[0,0],[0,10],[10,10],[10,0],[0,0]]]]);\n *\n * //=multiPoly\n *\n */\nexport function multiPolygon

    (\n coordinates: Position[][][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const geom: MultiPolygon = {\n type: \"MultiPolygon\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a Feature based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Array} geometries an array of GeoJSON Geometries\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a GeoJSON GeometryCollection Feature\n * @example\n * var pt = turf.geometry(\"Point\", [100, 0]);\n * var line = turf.geometry(\"LineString\", [[101, 0], [102, 1]]);\n * var collection = turf.geometryCollection([pt, line]);\n *\n * // => collection\n */\nexport function geometryCollection<\n G extends\n | Point\n | LineString\n | Polygon\n | MultiPoint\n | MultiLineString\n | MultiPolygon,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geometries: Array,\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature, P> {\n const geom: GeometryCollection = {\n type: \"GeometryCollection\",\n geometries,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Round number to precision\n *\n * @function\n * @param {number} num Number\n * @param {number} [precision=0] Precision\n * @returns {number} rounded number\n * @example\n * turf.round(120.4321)\n * //=120\n *\n * turf.round(120.4321, 2)\n * //=120.43\n */\nexport function round(num: number, precision = 0): number {\n if (precision && !(precision >= 0)) {\n throw new Error(\"precision must be a positive number\");\n }\n const multiplier = Math.pow(10, precision || 0);\n return Math.round(num * multiplier) / multiplier;\n}\n\n/**\n * Convert a distance measurement (assuming a spherical Earth) from radians to a more friendly unit.\n * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet\n *\n * @function\n * @param {number} radians in radians across the sphere\n * @param {Units} [units=\"kilometers\"] can be degrees, radians, miles, inches, yards, metres,\n * meters, kilometres, kilometers.\n * @returns {number} distance\n */\nexport function radiansToLength(\n radians: number,\n units: Units = \"kilometers\"\n): number {\n const factor = factors[units];\n if (!factor) {\n throw new Error(units + \" units is invalid\");\n }\n return radians * factor;\n}\n\n/**\n * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into radians\n * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet\n *\n * @function\n * @param {number} distance in real units\n * @param {Units} [units=\"kilometers\"] can be degrees, radians, miles, inches, yards, metres,\n * meters, kilometres, kilometers.\n * @returns {number} radians\n */\nexport function lengthToRadians(\n distance: number,\n units: Units = \"kilometers\"\n): number {\n const factor = factors[units];\n if (!factor) {\n throw new Error(units + \" units is invalid\");\n }\n return distance / factor;\n}\n\n/**\n * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into degrees\n * Valid units: miles, nauticalmiles, inches, yards, meters, metres, centimeters, kilometres, feet\n *\n * @function\n * @param {number} distance in real units\n * @param {Units} [units=\"kilometers\"] can be degrees, radians, miles, inches, yards, metres,\n * meters, kilometres, kilometers.\n * @returns {number} degrees\n */\nexport function lengthToDegrees(distance: number, units?: Units): number {\n return radiansToDegrees(lengthToRadians(distance, units));\n}\n\n/**\n * Converts any bearing angle from the north line direction (positive clockwise)\n * and returns an angle between 0-360 degrees (positive clockwise), 0 being the north line\n *\n * @function\n * @param {number} bearing angle, between -180 and +180 degrees\n * @returns {number} angle between 0 and 360 degrees\n */\nexport function bearingToAzimuth(bearing: number): number {\n let angle = bearing % 360;\n if (angle < 0) {\n angle += 360;\n }\n return angle;\n}\n\n/**\n * Converts any azimuth angle from the north line direction (positive clockwise)\n * and returns an angle between -180 and +180 degrees (positive clockwise), 0 being the north line\n *\n * @function\n * @param {number} angle between 0 and 360 degrees\n * @returns {number} bearing between -180 and +180 degrees\n */\nexport function azimuthToBearing(angle: number): number {\n // Ignore full revolutions (multiples of 360)\n angle = angle % 360;\n\n if (angle > 180) {\n return angle - 360;\n } else if (angle < -180) {\n return angle + 360;\n }\n\n return angle;\n}\n\n/**\n * Converts an angle in radians to degrees\n *\n * @function\n * @param {number} radians angle in radians\n * @returns {number} degrees between 0 and 360 degrees\n */\nexport function radiansToDegrees(radians: number): number {\n // % (2 * Math.PI) radians in case someone passes value > 2π\n const normalisedRadians = radians % (2 * Math.PI);\n return (normalisedRadians * 180) / Math.PI;\n}\n\n/**\n * Converts an angle in degrees to radians\n *\n * @function\n * @param {number} degrees angle between 0 and 360 degrees\n * @returns {number} angle in radians\n */\nexport function degreesToRadians(degrees: number): number {\n // % 360 degrees in case someone passes value > 360\n const normalisedDegrees = degrees % 360;\n return (normalisedDegrees * Math.PI) / 180;\n}\n\n/**\n * Converts a length from one unit to another.\n *\n * @function\n * @param {number} length Length to be converted\n * @param {Units} [originalUnit=\"kilometers\"] Input length unit\n * @param {Units} [finalUnit=\"kilometers\"] Returned length unit\n * @returns {number} The converted length\n */\nexport function convertLength(\n length: number,\n originalUnit: Units = \"kilometers\",\n finalUnit: Units = \"kilometers\"\n): number {\n if (!(length >= 0)) {\n throw new Error(\"length must be a positive number\");\n }\n return radiansToLength(lengthToRadians(length, originalUnit), finalUnit);\n}\n\n/**\n * Converts an area from one unit to another.\n *\n * @function\n * @param {number} area Area to be converted\n * @param {AreaUnits} [originalUnit=\"meters\"] Input area unit\n * @param {AreaUnits} [finalUnit=\"kilometers\"] Returned area unit\n * @returns {number} The converted length\n */\nexport function convertArea(\n area: number,\n originalUnit: AreaUnits = \"meters\",\n finalUnit: AreaUnits = \"kilometers\"\n): number {\n if (!(area >= 0)) {\n throw new Error(\"area must be a positive number\");\n }\n\n const startFactor = areaFactors[originalUnit];\n if (!startFactor) {\n throw new Error(\"invalid original units\");\n }\n\n const finalFactor = areaFactors[finalUnit];\n if (!finalFactor) {\n throw new Error(\"invalid final units\");\n }\n\n return (area / startFactor) * finalFactor;\n}\n\n/**\n * isNumber\n *\n * @function\n * @param {any} num Number to validate\n * @returns {boolean} true/false\n * @example\n * turf.isNumber(123)\n * //=true\n * turf.isNumber('foo')\n * //=false\n */\nexport function isNumber(num: any): boolean {\n return !isNaN(num) && num !== null && !Array.isArray(num);\n}\n\n/**\n * isObject\n *\n * @function\n * @param {any} input variable to validate\n * @returns {boolean} true/false, including false for Arrays and Functions\n * @example\n * turf.isObject({elevation: 10})\n * //=true\n * turf.isObject('foo')\n * //=false\n */\nexport function isObject(input: any): boolean {\n return input !== null && typeof input === \"object\" && !Array.isArray(input);\n}\n\n/**\n * Validate BBox\n *\n * @private\n * @param {any} bbox BBox to validate\n * @returns {void}\n * @throws {Error} if BBox is not valid\n * @example\n * validateBBox([-180, -40, 110, 50])\n * //=OK\n * validateBBox([-180, -40])\n * //=Error\n * validateBBox('Foo')\n * //=Error\n * validateBBox(5)\n * //=Error\n * validateBBox(null)\n * //=Error\n * validateBBox(undefined)\n * //=Error\n */\nexport function validateBBox(bbox: any): void {\n if (!bbox) {\n throw new Error(\"bbox is required\");\n }\n if (!Array.isArray(bbox)) {\n throw new Error(\"bbox must be an Array\");\n }\n if (bbox.length !== 4 && bbox.length !== 6) {\n throw new Error(\"bbox must be an Array of 4 or 6 numbers\");\n }\n bbox.forEach((num) => {\n if (!isNumber(num)) {\n throw new Error(\"bbox must only contain numbers\");\n }\n });\n}\n\n/**\n * Validate Id\n *\n * @private\n * @param {any} id Id to validate\n * @returns {void}\n * @throws {Error} if Id is not valid\n * @example\n * validateId([-180, -40, 110, 50])\n * //=Error\n * validateId([-180, -40])\n * //=Error\n * validateId('Foo')\n * //=OK\n * validateId(5)\n * //=OK\n * validateId(null)\n * //=Error\n * validateId(undefined)\n * //=Error\n */\nexport function validateId(id: any): void {\n if (!id) {\n throw new Error(\"id is required\");\n }\n if ([\"string\", \"number\"].indexOf(typeof id) === -1) {\n throw new Error(\"id must be a number or a string\");\n }\n}\n", "import {\n Feature,\n FeatureCollection,\n Geometry,\n LineString,\n MultiPoint,\n MultiLineString,\n MultiPolygon,\n Point,\n Polygon,\n} from \"geojson\";\nimport { isNumber } from \"@turf/helpers\";\n\n/**\n * Unwrap a coordinate from a Point Feature, Geometry or a single coordinate.\n *\n * @function\n * @param {Array|Geometry|Feature} coord GeoJSON Point or an Array of numbers\n * @returns {Array} coordinates\n * @example\n * var pt = turf.point([10, 10]);\n *\n * var coord = turf.getCoord(pt);\n * //= [10, 10]\n */\nfunction getCoord(coord: Feature | Point | number[]): number[] {\n if (!coord) {\n throw new Error(\"coord is required\");\n }\n\n if (!Array.isArray(coord)) {\n if (\n coord.type === \"Feature\" &&\n coord.geometry !== null &&\n coord.geometry.type === \"Point\"\n ) {\n return [...coord.geometry.coordinates];\n }\n if (coord.type === \"Point\") {\n return [...coord.coordinates];\n }\n }\n if (\n Array.isArray(coord) &&\n coord.length >= 2 &&\n !Array.isArray(coord[0]) &&\n !Array.isArray(coord[1])\n ) {\n return [...coord];\n }\n\n throw new Error(\"coord must be GeoJSON Point or an Array of numbers\");\n}\n\n/**\n * Unwrap coordinates from a Feature, Geometry Object or an Array\n *\n * @function\n * @param {Array|Geometry|Feature} coords Feature, Geometry Object or an Array\n * @returns {Array} coordinates\n * @example\n * var poly = turf.polygon([[[119.32, -8.7], [119.55, -8.69], [119.51, -8.54], [119.32, -8.7]]]);\n *\n * var coords = turf.getCoords(poly);\n * //= [[[119.32, -8.7], [119.55, -8.69], [119.51, -8.54], [119.32, -8.7]]]\n */\nfunction getCoords<\n G extends\n | Point\n | LineString\n | Polygon\n | MultiPoint\n | MultiLineString\n | MultiPolygon,\n>(coords: any[] | Feature | G): any[] {\n if (Array.isArray(coords)) {\n return coords;\n }\n\n // Feature\n if (coords.type === \"Feature\") {\n if (coords.geometry !== null) {\n return coords.geometry.coordinates;\n }\n } else {\n // Geometry\n if (coords.coordinates) {\n return coords.coordinates;\n }\n }\n\n throw new Error(\n \"coords must be GeoJSON Feature, Geometry Object or an Array\"\n );\n}\n\n/**\n * Checks if coordinates contains a number\n *\n * @function\n * @param {Array} coordinates GeoJSON Coordinates\n * @returns {boolean} true if Array contains a number\n */\nfunction containsNumber(coordinates: any[]): boolean {\n if (\n coordinates.length > 1 &&\n isNumber(coordinates[0]) &&\n isNumber(coordinates[1])\n ) {\n return true;\n }\n\n if (Array.isArray(coordinates[0]) && coordinates[0].length) {\n return containsNumber(coordinates[0]);\n }\n throw new Error(\"coordinates must only contain numbers\");\n}\n\n/**\n * Enforce expectations about types of GeoJSON objects for Turf.\n *\n * @function\n * @param {GeoJSON} value any GeoJSON object\n * @param {string} type expected GeoJSON type\n * @param {string} name name of calling function\n * @throws {Error} if value is not the expected type.\n */\nfunction geojsonType(value: any, type: string, name: string): void {\n if (!type || !name) {\n throw new Error(\"type and name required\");\n }\n\n if (!value || value.type !== type) {\n throw new Error(\n \"Invalid input to \" +\n name +\n \": must be a \" +\n type +\n \", given \" +\n value.type\n );\n }\n}\n\n/**\n * Enforce expectations about types of {@link Feature} inputs for Turf.\n * Internally this uses {@link geojsonType} to judge geometry types.\n *\n * @function\n * @param {Feature} feature a feature with an expected geometry type\n * @param {string} type expected GeoJSON type\n * @param {string} name name of calling function\n * @throws {Error} error if value is not the expected type.\n */\nfunction featureOf(feature: Feature, type: string, name: string): void {\n if (!feature) {\n throw new Error(\"No feature passed\");\n }\n if (!name) {\n throw new Error(\".featureOf() requires a name\");\n }\n if (!feature || feature.type !== \"Feature\" || !feature.geometry) {\n throw new Error(\n \"Invalid input to \" + name + \", Feature with geometry required\"\n );\n }\n if (!feature.geometry || feature.geometry.type !== type) {\n throw new Error(\n \"Invalid input to \" +\n name +\n \": must be a \" +\n type +\n \", given \" +\n feature.geometry.type\n );\n }\n}\n\n/**\n * Enforce expectations about types of {@link FeatureCollection} inputs for Turf.\n * Internally this uses {@link geojsonType} to judge geometry types.\n *\n * @function\n * @param {FeatureCollection} featureCollection a FeatureCollection for which features will be judged\n * @param {string} type expected GeoJSON type\n * @param {string} name name of calling function\n * @throws {Error} if value is not the expected type.\n */\nfunction collectionOf(\n featureCollection: FeatureCollection,\n type: string,\n name: string\n) {\n if (!featureCollection) {\n throw new Error(\"No featureCollection passed\");\n }\n if (!name) {\n throw new Error(\".collectionOf() requires a name\");\n }\n if (!featureCollection || featureCollection.type !== \"FeatureCollection\") {\n throw new Error(\n \"Invalid input to \" + name + \", FeatureCollection required\"\n );\n }\n for (const feature of featureCollection.features) {\n if (!feature || feature.type !== \"Feature\" || !feature.geometry) {\n throw new Error(\n \"Invalid input to \" + name + \", Feature with geometry required\"\n );\n }\n if (!feature.geometry || feature.geometry.type !== type) {\n throw new Error(\n \"Invalid input to \" +\n name +\n \": must be a \" +\n type +\n \", given \" +\n feature.geometry.type\n );\n }\n }\n}\n\n/**\n * Get Geometry from Feature or Geometry Object\n *\n * @param {Feature|Geometry} geojson GeoJSON Feature or Geometry Object\n * @returns {Geometry|null} GeoJSON Geometry Object\n * @throws {Error} if geojson is not a Feature or Geometry Object\n * @example\n * var point = {\n * \"type\": \"Feature\",\n * \"properties\": {},\n * \"geometry\": {\n * \"type\": \"Point\",\n * \"coordinates\": [110, 40]\n * }\n * }\n * var geom = turf.getGeom(point)\n * //={\"type\": \"Point\", \"coordinates\": [110, 40]}\n */\nfunction getGeom(geojson: Feature | G): G {\n if (geojson.type === \"Feature\") {\n return geojson.geometry;\n }\n return geojson;\n}\n\n/**\n * Get GeoJSON object's type, Geometry type is prioritize.\n *\n * @param {GeoJSON} geojson GeoJSON object\n * @param {string} [name=\"geojson\"] name of the variable to display in error message (unused)\n * @returns {string} GeoJSON type\n * @example\n * var point = {\n * \"type\": \"Feature\",\n * \"properties\": {},\n * \"geometry\": {\n * \"type\": \"Point\",\n * \"coordinates\": [110, 40]\n * }\n * }\n * var geom = turf.getType(point)\n * //=\"Point\"\n */\nfunction getType(\n geojson: Feature | FeatureCollection | Geometry,\n _name?: string\n): string {\n if (geojson.type === \"FeatureCollection\") {\n return \"FeatureCollection\";\n }\n if (geojson.type === \"GeometryCollection\") {\n return \"GeometryCollection\";\n }\n if (geojson.type === \"Feature\" && geojson.geometry !== null) {\n return geojson.geometry.type;\n }\n return geojson.type;\n}\n\nexport {\n getCoord,\n getCoords,\n containsNumber,\n geojsonType,\n featureOf,\n collectionOf,\n getGeom,\n getType,\n};\n// No default export!\n", "import {\n BBox,\n Feature,\n LineString,\n MultiLineString,\n MultiPolygon,\n GeoJsonProperties,\n Polygon,\n} from \"geojson\";\n\nimport {\n lineString,\n multiLineString,\n multiPolygon,\n polygon,\n} from \"@turf/helpers\";\nimport { getGeom } from \"@turf/invariant\";\nimport { lineclip, polygonclip } from \"./lib/lineclip.js\";\n\n/**\n * Takes a {@link Feature} and a bbox and clips the feature to the bbox using\n * [lineclip](https://github.com/mapbox/lineclip).\n * May result in degenerate edges when clipping Polygons.\n *\n * @function\n * @param {Feature} feature feature to clip to the bbox\n * @param {BBox} bbox extent in [minX, minY, maxX, maxY] order\n * @returns {Feature} clipped Feature\n * @example\n * var bbox = [0, 0, 10, 10];\n * var poly = turf.polygon([[[2, 2], [8, 4], [12, 8], [3, 7], [2, 2]]]);\n *\n * var clipped = turf.bboxClip(poly, bbox);\n *\n * //addToMap\n * var addToMap = [bbox, poly, clipped]\n */\nfunction bboxClip<\n G extends Polygon | MultiPolygon | LineString | MultiLineString,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(feature: Feature | G, bbox: BBox) {\n const geom = getGeom(feature);\n const type = geom.type;\n const properties = feature.type === \"Feature\" ? feature.properties : {};\n let coords: any[] = geom.coordinates;\n\n switch (type) {\n case \"LineString\":\n case \"MultiLineString\": {\n const lines: any[] = [];\n if (type === \"LineString\") {\n coords = [coords];\n }\n coords.forEach((line) => {\n lineclip(line, bbox, lines);\n });\n if (lines.length === 1) {\n return lineString(lines[0], properties);\n }\n return multiLineString(lines, properties);\n }\n case \"Polygon\":\n return polygon(clipPolygon(coords, bbox), properties);\n case \"MultiPolygon\":\n return multiPolygon(\n coords.map((poly) => {\n return clipPolygon(poly, bbox);\n }),\n properties\n );\n default:\n throw new Error(\"geometry \" + type + \" not supported\");\n }\n}\n\nfunction clipPolygon(rings: number[][][], bbox: BBox) {\n const outRings = [];\n for (const ring of rings) {\n const clipped = polygonclip(ring, bbox);\n if (clipped.length > 0) {\n if (\n clipped[0][0] !== clipped[clipped.length - 1][0] ||\n clipped[0][1] !== clipped[clipped.length - 1][1]\n ) {\n clipped.push(clipped[0]);\n }\n if (clipped.length >= 4) {\n outRings.push(clipped);\n }\n }\n }\n return outRings;\n}\n\nexport { bboxClip };\nexport default bboxClip;\n", "// Cohen-Sutherland line clipping algorithm, adapted to efficiently\n// handle polylines rather than just segments\nimport { BBox } from \"geojson\";\n\nexport function lineclip(\n points: number[][],\n bbox: BBox,\n result?: number[][][]\n): number[][][] {\n var len = points.length,\n codeA = bitCode(points[0], bbox),\n part = [] as number[][],\n i,\n codeB,\n lastCode;\n let a: number[];\n let b: number[];\n\n if (!result) result = [];\n\n for (i = 1; i < len; i++) {\n a = points[i - 1];\n b = points[i];\n codeB = lastCode = bitCode(b, bbox);\n\n while (true) {\n if (!(codeA | codeB)) {\n // accept\n part.push(a);\n\n if (codeB !== lastCode) {\n // segment went outside\n part.push(b);\n\n if (i < len - 1) {\n // start a new line\n result.push(part);\n part = [];\n }\n } else if (i === len - 1) {\n part.push(b);\n }\n break;\n } else if (codeA & codeB) {\n // trivial reject\n break;\n } else if (codeA) {\n // a outside, intersect with clip edge\n a = intersect(a, b, codeA, bbox)!;\n codeA = bitCode(a, bbox);\n } else {\n // b outside\n b = intersect(a, b, codeB, bbox)!;\n codeB = bitCode(b, bbox);\n }\n }\n\n codeA = lastCode;\n }\n\n if (part.length) result.push(part);\n\n return result;\n}\n\n// Sutherland-Hodgeman polygon clipping algorithm\n\nexport function polygonclip(points: number[][], bbox: BBox): number[][] {\n var result: number[][], edge, prev, prevInside, i, p, inside;\n\n // clip against each side of the clip rectangle\n for (edge = 1; edge <= 8; edge *= 2) {\n result = [];\n prev = points[points.length - 1];\n prevInside = !(bitCode(prev, bbox) & edge);\n\n for (i = 0; i < points.length; i++) {\n p = points[i];\n inside = !(bitCode(p, bbox) & edge);\n\n // if segment goes through the clip window, add an intersection\n if (inside !== prevInside) result.push(intersect(prev, p, edge, bbox)!);\n\n if (inside) result.push(p); // add a point if it's inside\n\n prev = p;\n prevInside = inside;\n }\n\n points = result;\n\n if (!points.length) break;\n }\n\n return result!;\n}\n\n// intersect a segment against one of the 4 lines that make up the bbox\n\nfunction intersect(\n a: number[],\n b: number[],\n edge: number,\n bbox: BBox\n): number[] | null {\n return edge & 8\n ? [a[0] + ((b[0] - a[0]) * (bbox[3] - a[1])) / (b[1] - a[1]), bbox[3]] // top\n : edge & 4\n ? [a[0] + ((b[0] - a[0]) * (bbox[1] - a[1])) / (b[1] - a[1]), bbox[1]] // bottom\n : edge & 2\n ? [bbox[2], a[1] + ((b[1] - a[1]) * (bbox[2] - a[0])) / (b[0] - a[0])] // right\n : edge & 1\n ? [bbox[0], a[1] + ((b[1] - a[1]) * (bbox[0] - a[0])) / (b[0] - a[0])] // left\n : null;\n}\n\n// bit code reflects the point position relative to the bbox:\n\n// left mid right\n// top 1001 1000 1010\n// mid 0001 0000 0010\n// bottom 0101 0100 0110\n\nfunction bitCode(p: number[], bbox: BBox) {\n var code = 0;\n\n if (p[0] < bbox[0]) code |= 1;\n // left\n else if (p[0] > bbox[2]) code |= 2; // right\n\n if (p[1] < bbox[1]) code |= 4;\n // bottom\n else if (p[1] > bbox[3]) code |= 8; // top\n\n return code;\n}\n", "'use strict';\n\nmodule.exports = function (data, opts) {\n if (!opts) opts = {};\n if (typeof opts === 'function') opts = { cmp: opts };\n var cycles = (typeof opts.cycles === 'boolean') ? opts.cycles : false;\n\n var cmp = opts.cmp && (function (f) {\n return function (node) {\n return function (a, b) {\n var aobj = { key: a, value: node[a] };\n var bobj = { key: b, value: node[b] };\n return f(aobj, bobj);\n };\n };\n })(opts.cmp);\n\n var seen = [];\n return (function stringify (node) {\n if (node && node.toJSON && typeof node.toJSON === 'function') {\n node = node.toJSON();\n }\n\n if (node === undefined) return;\n if (typeof node == 'number') return isFinite(node) ? '' + node : 'null';\n if (typeof node !== 'object') return JSON.stringify(node);\n\n var i, out;\n if (Array.isArray(node)) {\n out = '[';\n for (i = 0; i < node.length; i++) {\n if (i) out += ',';\n out += stringify(node[i]) || 'null';\n }\n return out + ']';\n }\n\n if (node === null) return 'null';\n\n if (seen.indexOf(node) !== -1) {\n if (cycles) return JSON.stringify('__cycle__');\n throw new TypeError('Converting circular structure to JSON');\n }\n\n var seenIndex = seen.push(node) - 1;\n var keys = Object.keys(node).sort(cmp && cmp(node));\n out = '';\n for (i = 0; i < keys.length; i++) {\n var key = keys[i];\n var value = stringify(node[key]);\n\n if (!value) continue;\n if (out) out += ',';\n out += JSON.stringify(key) + ':' + value;\n }\n seen.splice(seenIndex, 1);\n return '{' + out + '}';\n })(data);\n};\n", "(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.polygonClipping = factory());\n})(this, (function () { 'use strict';\n\n /**\n * splaytree v3.1.2\n * Fast Splay tree for Node and browser\n *\n * @author Alexander Milevski \n * @license MIT\n * @preserve\n */\n\n /*! *****************************************************************************\r\n Copyright (c) Microsoft Corporation. All rights reserved.\r\n Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use\r\n this file except in compliance with the License. You may obtain a copy of the\r\n License at http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r\n KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED\r\n WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,\r\n MERCHANTABLITY OR NON-INFRINGEMENT.\r\n\r\n See the Apache Version 2.0 License for specific language governing permissions\r\n and limitations under the License.\r\n ***************************************************************************** */\n\n function __generator(thisArg, body) {\n var _ = {\n label: 0,\n sent: function () {\n if (t[0] & 1) throw t[1];\n return t[1];\n },\n trys: [],\n ops: []\n },\n f,\n y,\n t,\n g;\n return g = {\n next: verb(0),\n \"throw\": verb(1),\n \"return\": verb(2)\n }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function () {\n return this;\n }), g;\n function verb(n) {\n return function (v) {\n return step([n, v]);\n };\n }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (_) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0:\n case 1:\n t = op;\n break;\n case 4:\n _.label++;\n return {\n value: op[1],\n done: false\n };\n case 5:\n _.label++;\n y = op[1];\n op = [0];\n continue;\n case 7:\n op = _.ops.pop();\n _.trys.pop();\n continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {\n _ = 0;\n continue;\n }\n if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {\n _.label = op[1];\n break;\n }\n if (op[0] === 6 && _.label < t[1]) {\n _.label = t[1];\n t = op;\n break;\n }\n if (t && _.label < t[2]) {\n _.label = t[2];\n _.ops.push(op);\n break;\n }\n if (t[2]) _.ops.pop();\n _.trys.pop();\n continue;\n }\n op = body.call(thisArg, _);\n } catch (e) {\n op = [6, e];\n y = 0;\n } finally {\n f = t = 0;\n }\n if (op[0] & 5) throw op[1];\n return {\n value: op[0] ? op[1] : void 0,\n done: true\n };\n }\n }\n var Node = /** @class */function () {\n function Node(key, data) {\n this.next = null;\n this.key = key;\n this.data = data;\n this.left = null;\n this.right = null;\n }\n return Node;\n }();\n\n /* follows \"An implementation of top-down splaying\"\r\n * by D. Sleator March 1992\r\n */\n function DEFAULT_COMPARE(a, b) {\n return a > b ? 1 : a < b ? -1 : 0;\n }\n /**\r\n * Simple top down splay, not requiring i to be in the tree t.\r\n */\n function splay(i, t, comparator) {\n var N = new Node(null, null);\n var l = N;\n var r = N;\n while (true) {\n var cmp = comparator(i, t.key);\n //if (i < t.key) {\n if (cmp < 0) {\n if (t.left === null) break;\n //if (i < t.left.key) {\n if (comparator(i, t.left.key) < 0) {\n var y = t.left; /* rotate right */\n t.left = y.right;\n y.right = t;\n t = y;\n if (t.left === null) break;\n }\n r.left = t; /* link right */\n r = t;\n t = t.left;\n //} else if (i > t.key) {\n } else if (cmp > 0) {\n if (t.right === null) break;\n //if (i > t.right.key) {\n if (comparator(i, t.right.key) > 0) {\n var y = t.right; /* rotate left */\n t.right = y.left;\n y.left = t;\n t = y;\n if (t.right === null) break;\n }\n l.right = t; /* link left */\n l = t;\n t = t.right;\n } else break;\n }\n /* assemble */\n l.right = t.left;\n r.left = t.right;\n t.left = N.right;\n t.right = N.left;\n return t;\n }\n function insert(i, data, t, comparator) {\n var node = new Node(i, data);\n if (t === null) {\n node.left = node.right = null;\n return node;\n }\n t = splay(i, t, comparator);\n var cmp = comparator(i, t.key);\n if (cmp < 0) {\n node.left = t.left;\n node.right = t;\n t.left = null;\n } else if (cmp >= 0) {\n node.right = t.right;\n node.left = t;\n t.right = null;\n }\n return node;\n }\n function split(key, v, comparator) {\n var left = null;\n var right = null;\n if (v) {\n v = splay(key, v, comparator);\n var cmp = comparator(v.key, key);\n if (cmp === 0) {\n left = v.left;\n right = v.right;\n } else if (cmp < 0) {\n right = v.right;\n v.right = null;\n left = v;\n } else {\n left = v.left;\n v.left = null;\n right = v;\n }\n }\n return {\n left: left,\n right: right\n };\n }\n function merge(left, right, comparator) {\n if (right === null) return left;\n if (left === null) return right;\n right = splay(left.key, right, comparator);\n right.left = left;\n return right;\n }\n /**\r\n * Prints level of the tree\r\n */\n function printRow(root, prefix, isTail, out, printNode) {\n if (root) {\n out(\"\" + prefix + (isTail ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ') + printNode(root) + \"\\n\");\n var indent = prefix + (isTail ? ' ' : '\u2502 ');\n if (root.left) printRow(root.left, indent, false, out, printNode);\n if (root.right) printRow(root.right, indent, true, out, printNode);\n }\n }\n var Tree = /** @class */function () {\n function Tree(comparator) {\n if (comparator === void 0) {\n comparator = DEFAULT_COMPARE;\n }\n this._root = null;\n this._size = 0;\n this._comparator = comparator;\n }\n /**\r\n * Inserts a key, allows duplicates\r\n */\n Tree.prototype.insert = function (key, data) {\n this._size++;\n return this._root = insert(key, data, this._root, this._comparator);\n };\n /**\r\n * Adds a key, if it is not present in the tree\r\n */\n Tree.prototype.add = function (key, data) {\n var node = new Node(key, data);\n if (this._root === null) {\n node.left = node.right = null;\n this._size++;\n this._root = node;\n }\n var comparator = this._comparator;\n var t = splay(key, this._root, comparator);\n var cmp = comparator(key, t.key);\n if (cmp === 0) this._root = t;else {\n if (cmp < 0) {\n node.left = t.left;\n node.right = t;\n t.left = null;\n } else if (cmp > 0) {\n node.right = t.right;\n node.left = t;\n t.right = null;\n }\n this._size++;\n this._root = node;\n }\n return this._root;\n };\n /**\r\n * @param {Key} key\r\n * @return {Node|null}\r\n */\n Tree.prototype.remove = function (key) {\n this._root = this._remove(key, this._root, this._comparator);\n };\n /**\r\n * Deletes i from the tree if it's there\r\n */\n Tree.prototype._remove = function (i, t, comparator) {\n var x;\n if (t === null) return null;\n t = splay(i, t, comparator);\n var cmp = comparator(i, t.key);\n if (cmp === 0) {\n /* found it */\n if (t.left === null) {\n x = t.right;\n } else {\n x = splay(i, t.left, comparator);\n x.right = t.right;\n }\n this._size--;\n return x;\n }\n return t; /* It wasn't there */\n };\n /**\r\n * Removes and returns the node with smallest key\r\n */\n Tree.prototype.pop = function () {\n var node = this._root;\n if (node) {\n while (node.left) node = node.left;\n this._root = splay(node.key, this._root, this._comparator);\n this._root = this._remove(node.key, this._root, this._comparator);\n return {\n key: node.key,\n data: node.data\n };\n }\n return null;\n };\n /**\r\n * Find without splaying\r\n */\n Tree.prototype.findStatic = function (key) {\n var current = this._root;\n var compare = this._comparator;\n while (current) {\n var cmp = compare(key, current.key);\n if (cmp === 0) return current;else if (cmp < 0) current = current.left;else current = current.right;\n }\n return null;\n };\n Tree.prototype.find = function (key) {\n if (this._root) {\n this._root = splay(key, this._root, this._comparator);\n if (this._comparator(key, this._root.key) !== 0) return null;\n }\n return this._root;\n };\n Tree.prototype.contains = function (key) {\n var current = this._root;\n var compare = this._comparator;\n while (current) {\n var cmp = compare(key, current.key);\n if (cmp === 0) return true;else if (cmp < 0) current = current.left;else current = current.right;\n }\n return false;\n };\n Tree.prototype.forEach = function (visitor, ctx) {\n var current = this._root;\n var Q = []; /* Initialize stack s */\n var done = false;\n while (!done) {\n if (current !== null) {\n Q.push(current);\n current = current.left;\n } else {\n if (Q.length !== 0) {\n current = Q.pop();\n visitor.call(ctx, current);\n current = current.right;\n } else done = true;\n }\n }\n return this;\n };\n /**\r\n * Walk key range from `low` to `high`. Stops if `fn` returns a value.\r\n */\n Tree.prototype.range = function (low, high, fn, ctx) {\n var Q = [];\n var compare = this._comparator;\n var node = this._root;\n var cmp;\n while (Q.length !== 0 || node) {\n if (node) {\n Q.push(node);\n node = node.left;\n } else {\n node = Q.pop();\n cmp = compare(node.key, high);\n if (cmp > 0) {\n break;\n } else if (compare(node.key, low) >= 0) {\n if (fn.call(ctx, node)) return this; // stop if smth is returned\n }\n node = node.right;\n }\n }\n return this;\n };\n /**\r\n * Returns array of keys\r\n */\n Tree.prototype.keys = function () {\n var keys = [];\n this.forEach(function (_a) {\n var key = _a.key;\n return keys.push(key);\n });\n return keys;\n };\n /**\r\n * Returns array of all the data in the nodes\r\n */\n Tree.prototype.values = function () {\n var values = [];\n this.forEach(function (_a) {\n var data = _a.data;\n return values.push(data);\n });\n return values;\n };\n Tree.prototype.min = function () {\n if (this._root) return this.minNode(this._root).key;\n return null;\n };\n Tree.prototype.max = function () {\n if (this._root) return this.maxNode(this._root).key;\n return null;\n };\n Tree.prototype.minNode = function (t) {\n if (t === void 0) {\n t = this._root;\n }\n if (t) while (t.left) t = t.left;\n return t;\n };\n Tree.prototype.maxNode = function (t) {\n if (t === void 0) {\n t = this._root;\n }\n if (t) while (t.right) t = t.right;\n return t;\n };\n /**\r\n * Returns node at given index\r\n */\n Tree.prototype.at = function (index) {\n var current = this._root;\n var done = false;\n var i = 0;\n var Q = [];\n while (!done) {\n if (current) {\n Q.push(current);\n current = current.left;\n } else {\n if (Q.length > 0) {\n current = Q.pop();\n if (i === index) return current;\n i++;\n current = current.right;\n } else done = true;\n }\n }\n return null;\n };\n Tree.prototype.next = function (d) {\n var root = this._root;\n var successor = null;\n if (d.right) {\n successor = d.right;\n while (successor.left) successor = successor.left;\n return successor;\n }\n var comparator = this._comparator;\n while (root) {\n var cmp = comparator(d.key, root.key);\n if (cmp === 0) break;else if (cmp < 0) {\n successor = root;\n root = root.left;\n } else root = root.right;\n }\n return successor;\n };\n Tree.prototype.prev = function (d) {\n var root = this._root;\n var predecessor = null;\n if (d.left !== null) {\n predecessor = d.left;\n while (predecessor.right) predecessor = predecessor.right;\n return predecessor;\n }\n var comparator = this._comparator;\n while (root) {\n var cmp = comparator(d.key, root.key);\n if (cmp === 0) break;else if (cmp < 0) root = root.left;else {\n predecessor = root;\n root = root.right;\n }\n }\n return predecessor;\n };\n Tree.prototype.clear = function () {\n this._root = null;\n this._size = 0;\n return this;\n };\n Tree.prototype.toList = function () {\n return toList(this._root);\n };\n /**\r\n * Bulk-load items. Both array have to be same size\r\n */\n Tree.prototype.load = function (keys, values, presort) {\n if (values === void 0) {\n values = [];\n }\n if (presort === void 0) {\n presort = false;\n }\n var size = keys.length;\n var comparator = this._comparator;\n // sort if needed\n if (presort) sort(keys, values, 0, size - 1, comparator);\n if (this._root === null) {\n // empty tree\n this._root = loadRecursive(keys, values, 0, size);\n this._size = size;\n } else {\n // that re-builds the whole tree from two in-order traversals\n var mergedList = mergeLists(this.toList(), createList(keys, values), comparator);\n size = this._size + size;\n this._root = sortedListToBST({\n head: mergedList\n }, 0, size);\n }\n return this;\n };\n Tree.prototype.isEmpty = function () {\n return this._root === null;\n };\n Object.defineProperty(Tree.prototype, \"size\", {\n get: function () {\n return this._size;\n },\n enumerable: true,\n configurable: true\n });\n Object.defineProperty(Tree.prototype, \"root\", {\n get: function () {\n return this._root;\n },\n enumerable: true,\n configurable: true\n });\n Tree.prototype.toString = function (printNode) {\n if (printNode === void 0) {\n printNode = function (n) {\n return String(n.key);\n };\n }\n var out = [];\n printRow(this._root, '', true, function (v) {\n return out.push(v);\n }, printNode);\n return out.join('');\n };\n Tree.prototype.update = function (key, newKey, newData) {\n var comparator = this._comparator;\n var _a = split(key, this._root, comparator),\n left = _a.left,\n right = _a.right;\n if (comparator(key, newKey) < 0) {\n right = insert(newKey, newData, right, comparator);\n } else {\n left = insert(newKey, newData, left, comparator);\n }\n this._root = merge(left, right, comparator);\n };\n Tree.prototype.split = function (key) {\n return split(key, this._root, this._comparator);\n };\n Tree.prototype[Symbol.iterator] = function () {\n var current, Q, done;\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0:\n current = this._root;\n Q = [];\n done = false;\n _a.label = 1;\n case 1:\n if (!!done) return [3 /*break*/, 6];\n if (!(current !== null)) return [3 /*break*/, 2];\n Q.push(current);\n current = current.left;\n return [3 /*break*/, 5];\n case 2:\n if (!(Q.length !== 0)) return [3 /*break*/, 4];\n current = Q.pop();\n return [4 /*yield*/, current];\n case 3:\n _a.sent();\n current = current.right;\n return [3 /*break*/, 5];\n case 4:\n done = true;\n _a.label = 5;\n case 5:\n return [3 /*break*/, 1];\n case 6:\n return [2 /*return*/];\n }\n });\n };\n return Tree;\n }();\n function loadRecursive(keys, values, start, end) {\n var size = end - start;\n if (size > 0) {\n var middle = start + Math.floor(size / 2);\n var key = keys[middle];\n var data = values[middle];\n var node = new Node(key, data);\n node.left = loadRecursive(keys, values, start, middle);\n node.right = loadRecursive(keys, values, middle + 1, end);\n return node;\n }\n return null;\n }\n function createList(keys, values) {\n var head = new Node(null, null);\n var p = head;\n for (var i = 0; i < keys.length; i++) {\n p = p.next = new Node(keys[i], values[i]);\n }\n p.next = null;\n return head.next;\n }\n function toList(root) {\n var current = root;\n var Q = [];\n var done = false;\n var head = new Node(null, null);\n var p = head;\n while (!done) {\n if (current) {\n Q.push(current);\n current = current.left;\n } else {\n if (Q.length > 0) {\n current = p = p.next = Q.pop();\n current = current.right;\n } else done = true;\n }\n }\n p.next = null; // that'll work even if the tree was empty\n return head.next;\n }\n function sortedListToBST(list, start, end) {\n var size = end - start;\n if (size > 0) {\n var middle = start + Math.floor(size / 2);\n var left = sortedListToBST(list, start, middle);\n var root = list.head;\n root.left = left;\n list.head = list.head.next;\n root.right = sortedListToBST(list, middle + 1, end);\n return root;\n }\n return null;\n }\n function mergeLists(l1, l2, compare) {\n var head = new Node(null, null); // dummy\n var p = head;\n var p1 = l1;\n var p2 = l2;\n while (p1 !== null && p2 !== null) {\n if (compare(p1.key, p2.key) < 0) {\n p.next = p1;\n p1 = p1.next;\n } else {\n p.next = p2;\n p2 = p2.next;\n }\n p = p.next;\n }\n if (p1 !== null) {\n p.next = p1;\n } else if (p2 !== null) {\n p.next = p2;\n }\n return head.next;\n }\n function sort(keys, values, left, right, compare) {\n if (left >= right) return;\n var pivot = keys[left + right >> 1];\n var i = left - 1;\n var j = right + 1;\n while (true) {\n do i++; while (compare(keys[i], pivot) < 0);\n do j--; while (compare(keys[j], pivot) > 0);\n if (i >= j) break;\n var tmp = keys[i];\n keys[i] = keys[j];\n keys[j] = tmp;\n tmp = values[i];\n values[i] = values[j];\n values[j] = tmp;\n }\n sort(keys, values, left, j, compare);\n sort(keys, values, j + 1, right, compare);\n }\n\n /**\n * A bounding box has the format:\n *\n * { ll: { x: xmin, y: ymin }, ur: { x: xmax, y: ymax } }\n *\n */\n\n const isInBbox = (bbox, point) => {\n return bbox.ll.x <= point.x && point.x <= bbox.ur.x && bbox.ll.y <= point.y && point.y <= bbox.ur.y;\n };\n\n /* Returns either null, or a bbox (aka an ordered pair of points)\n * If there is only one point of overlap, a bbox with identical points\n * will be returned */\n const getBboxOverlap = (b1, b2) => {\n // check if the bboxes overlap at all\n if (b2.ur.x < b1.ll.x || b1.ur.x < b2.ll.x || b2.ur.y < b1.ll.y || b1.ur.y < b2.ll.y) return null;\n\n // find the middle two X values\n const lowerX = b1.ll.x < b2.ll.x ? b2.ll.x : b1.ll.x;\n const upperX = b1.ur.x < b2.ur.x ? b1.ur.x : b2.ur.x;\n\n // find the middle two Y values\n const lowerY = b1.ll.y < b2.ll.y ? b2.ll.y : b1.ll.y;\n const upperY = b1.ur.y < b2.ur.y ? b1.ur.y : b2.ur.y;\n\n // put those middle values together to get the overlap\n return {\n ll: {\n x: lowerX,\n y: lowerY\n },\n ur: {\n x: upperX,\n y: upperY\n }\n };\n };\n\n /* Javascript doesn't do integer math. Everything is\n * floating point with percision Number.EPSILON.\n *\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON\n */\n\n let epsilon$1 = Number.EPSILON;\n\n // IE Polyfill\n if (epsilon$1 === undefined) epsilon$1 = Math.pow(2, -52);\n const EPSILON_SQ = epsilon$1 * epsilon$1;\n\n /* FLP comparator */\n const cmp = (a, b) => {\n // check if they're both 0\n if (-epsilon$1 < a && a < epsilon$1) {\n if (-epsilon$1 < b && b < epsilon$1) {\n return 0;\n }\n }\n\n // check if they're flp equal\n const ab = a - b;\n if (ab * ab < EPSILON_SQ * a * b) {\n return 0;\n }\n\n // normal comparison\n return a < b ? -1 : 1;\n };\n\n /**\n * This class rounds incoming values sufficiently so that\n * floating points problems are, for the most part, avoided.\n *\n * Incoming points are have their x & y values tested against\n * all previously seen x & y values. If either is 'too close'\n * to a previously seen value, it's value is 'snapped' to the\n * previously seen value.\n *\n * All points should be rounded by this class before being\n * stored in any data structures in the rest of this algorithm.\n */\n\n class PtRounder {\n constructor() {\n this.reset();\n }\n reset() {\n this.xRounder = new CoordRounder();\n this.yRounder = new CoordRounder();\n }\n round(x, y) {\n return {\n x: this.xRounder.round(x),\n y: this.yRounder.round(y)\n };\n }\n }\n class CoordRounder {\n constructor() {\n this.tree = new Tree();\n // preseed with 0 so we don't end up with values < Number.EPSILON\n this.round(0);\n }\n\n // Note: this can rounds input values backwards or forwards.\n // You might ask, why not restrict this to just rounding\n // forwards? Wouldn't that allow left endpoints to always\n // remain left endpoints during splitting (never change to\n // right). No - it wouldn't, because we snap intersections\n // to endpoints (to establish independence from the segment\n // angle for t-intersections).\n round(coord) {\n const node = this.tree.add(coord);\n const prevNode = this.tree.prev(node);\n if (prevNode !== null && cmp(node.key, prevNode.key) === 0) {\n this.tree.remove(coord);\n return prevNode.key;\n }\n const nextNode = this.tree.next(node);\n if (nextNode !== null && cmp(node.key, nextNode.key) === 0) {\n this.tree.remove(coord);\n return nextNode.key;\n }\n return coord;\n }\n }\n\n // singleton available by import\n const rounder = new PtRounder();\n\n const epsilon = 1.1102230246251565e-16;\n const splitter = 134217729;\n const resulterrbound = (3 + 8 * epsilon) * epsilon;\n\n // fast_expansion_sum_zeroelim routine from oritinal code\n function sum(elen, e, flen, f, h) {\n let Q, Qnew, hh, bvirt;\n let enow = e[0];\n let fnow = f[0];\n let eindex = 0;\n let findex = 0;\n if (fnow > enow === fnow > -enow) {\n Q = enow;\n enow = e[++eindex];\n } else {\n Q = fnow;\n fnow = f[++findex];\n }\n let hindex = 0;\n if (eindex < elen && findex < flen) {\n if (fnow > enow === fnow > -enow) {\n Qnew = enow + Q;\n hh = Q - (Qnew - enow);\n enow = e[++eindex];\n } else {\n Qnew = fnow + Q;\n hh = Q - (Qnew - fnow);\n fnow = f[++findex];\n }\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n while (eindex < elen && findex < flen) {\n if (fnow > enow === fnow > -enow) {\n Qnew = Q + enow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (enow - bvirt);\n enow = e[++eindex];\n } else {\n Qnew = Q + fnow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (fnow - bvirt);\n fnow = f[++findex];\n }\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n }\n }\n while (eindex < elen) {\n Qnew = Q + enow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (enow - bvirt);\n enow = e[++eindex];\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n }\n while (findex < flen) {\n Qnew = Q + fnow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (fnow - bvirt);\n fnow = f[++findex];\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n }\n if (Q !== 0 || hindex === 0) {\n h[hindex++] = Q;\n }\n return hindex;\n }\n function estimate(elen, e) {\n let Q = e[0];\n for (let i = 1; i < elen; i++) Q += e[i];\n return Q;\n }\n function vec(n) {\n return new Float64Array(n);\n }\n\n const ccwerrboundA = (3 + 16 * epsilon) * epsilon;\n const ccwerrboundB = (2 + 12 * epsilon) * epsilon;\n const ccwerrboundC = (9 + 64 * epsilon) * epsilon * epsilon;\n const B = vec(4);\n const C1 = vec(8);\n const C2 = vec(12);\n const D = vec(16);\n const u = vec(4);\n function orient2dadapt(ax, ay, bx, by, cx, cy, detsum) {\n let acxtail, acytail, bcxtail, bcytail;\n let bvirt, c, ahi, alo, bhi, blo, _i, _j, _0, s1, s0, t1, t0, u3;\n const acx = ax - cx;\n const bcx = bx - cx;\n const acy = ay - cy;\n const bcy = by - cy;\n s1 = acx * bcy;\n c = splitter * acx;\n ahi = c - (c - acx);\n alo = acx - ahi;\n c = splitter * bcy;\n bhi = c - (c - bcy);\n blo = bcy - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acy * bcx;\n c = splitter * acy;\n ahi = c - (c - acy);\n alo = acy - ahi;\n c = splitter * bcx;\n bhi = c - (c - bcx);\n blo = bcx - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n B[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n B[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n B[2] = _j - (u3 - bvirt) + (_i - bvirt);\n B[3] = u3;\n let det = estimate(4, B);\n let errbound = ccwerrboundB * detsum;\n if (det >= errbound || -det >= errbound) {\n return det;\n }\n bvirt = ax - acx;\n acxtail = ax - (acx + bvirt) + (bvirt - cx);\n bvirt = bx - bcx;\n bcxtail = bx - (bcx + bvirt) + (bvirt - cx);\n bvirt = ay - acy;\n acytail = ay - (acy + bvirt) + (bvirt - cy);\n bvirt = by - bcy;\n bcytail = by - (bcy + bvirt) + (bvirt - cy);\n if (acxtail === 0 && acytail === 0 && bcxtail === 0 && bcytail === 0) {\n return det;\n }\n errbound = ccwerrboundC * detsum + resulterrbound * Math.abs(det);\n det += acx * bcytail + bcy * acxtail - (acy * bcxtail + bcx * acytail);\n if (det >= errbound || -det >= errbound) return det;\n s1 = acxtail * bcy;\n c = splitter * acxtail;\n ahi = c - (c - acxtail);\n alo = acxtail - ahi;\n c = splitter * bcy;\n bhi = c - (c - bcy);\n blo = bcy - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acytail * bcx;\n c = splitter * acytail;\n ahi = c - (c - acytail);\n alo = acytail - ahi;\n c = splitter * bcx;\n bhi = c - (c - bcx);\n blo = bcx - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n u[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n u[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n u[2] = _j - (u3 - bvirt) + (_i - bvirt);\n u[3] = u3;\n const C1len = sum(4, B, 4, u, C1);\n s1 = acx * bcytail;\n c = splitter * acx;\n ahi = c - (c - acx);\n alo = acx - ahi;\n c = splitter * bcytail;\n bhi = c - (c - bcytail);\n blo = bcytail - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acy * bcxtail;\n c = splitter * acy;\n ahi = c - (c - acy);\n alo = acy - ahi;\n c = splitter * bcxtail;\n bhi = c - (c - bcxtail);\n blo = bcxtail - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n u[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n u[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n u[2] = _j - (u3 - bvirt) + (_i - bvirt);\n u[3] = u3;\n const C2len = sum(C1len, C1, 4, u, C2);\n s1 = acxtail * bcytail;\n c = splitter * acxtail;\n ahi = c - (c - acxtail);\n alo = acxtail - ahi;\n c = splitter * bcytail;\n bhi = c - (c - bcytail);\n blo = bcytail - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acytail * bcxtail;\n c = splitter * acytail;\n ahi = c - (c - acytail);\n alo = acytail - ahi;\n c = splitter * bcxtail;\n bhi = c - (c - bcxtail);\n blo = bcxtail - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n u[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n u[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n u[2] = _j - (u3 - bvirt) + (_i - bvirt);\n u[3] = u3;\n const Dlen = sum(C2len, C2, 4, u, D);\n return D[Dlen - 1];\n }\n function orient2d(ax, ay, bx, by, cx, cy) {\n const detleft = (ay - cy) * (bx - cx);\n const detright = (ax - cx) * (by - cy);\n const det = detleft - detright;\n const detsum = Math.abs(detleft + detright);\n if (Math.abs(det) >= ccwerrboundA * detsum) return det;\n return -orient2dadapt(ax, ay, bx, by, cx, cy, detsum);\n }\n\n /* Cross Product of two vectors with first point at origin */\n const crossProduct = (a, b) => a.x * b.y - a.y * b.x;\n\n /* Dot Product of two vectors with first point at origin */\n const dotProduct = (a, b) => a.x * b.x + a.y * b.y;\n\n /* Comparator for two vectors with same starting point */\n const compareVectorAngles = (basePt, endPt1, endPt2) => {\n const res = orient2d(basePt.x, basePt.y, endPt1.x, endPt1.y, endPt2.x, endPt2.y);\n if (res > 0) return -1;\n if (res < 0) return 1;\n return 0;\n };\n const length = v => Math.sqrt(dotProduct(v, v));\n\n /* Get the sine of the angle from pShared -> pAngle to pShaed -> pBase */\n const sineOfAngle = (pShared, pBase, pAngle) => {\n const vBase = {\n x: pBase.x - pShared.x,\n y: pBase.y - pShared.y\n };\n const vAngle = {\n x: pAngle.x - pShared.x,\n y: pAngle.y - pShared.y\n };\n return crossProduct(vAngle, vBase) / length(vAngle) / length(vBase);\n };\n\n /* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */\n const cosineOfAngle = (pShared, pBase, pAngle) => {\n const vBase = {\n x: pBase.x - pShared.x,\n y: pBase.y - pShared.y\n };\n const vAngle = {\n x: pAngle.x - pShared.x,\n y: pAngle.y - pShared.y\n };\n return dotProduct(vAngle, vBase) / length(vAngle) / length(vBase);\n };\n\n /* Get the x coordinate where the given line (defined by a point and vector)\n * crosses the horizontal line with the given y coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\n const horizontalIntersection = (pt, v, y) => {\n if (v.y === 0) return null;\n return {\n x: pt.x + v.x / v.y * (y - pt.y),\n y: y\n };\n };\n\n /* Get the y coordinate where the given line (defined by a point and vector)\n * crosses the vertical line with the given x coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\n const verticalIntersection = (pt, v, x) => {\n if (v.x === 0) return null;\n return {\n x: x,\n y: pt.y + v.y / v.x * (x - pt.x)\n };\n };\n\n /* Get the intersection of two lines, each defined by a base point and a vector.\n * In the case of parrallel lines (including overlapping ones) returns null. */\n const intersection$1 = (pt1, v1, pt2, v2) => {\n // take some shortcuts for vertical and horizontal lines\n // this also ensures we don't calculate an intersection and then discover\n // it's actually outside the bounding box of the line\n if (v1.x === 0) return verticalIntersection(pt2, v2, pt1.x);\n if (v2.x === 0) return verticalIntersection(pt1, v1, pt2.x);\n if (v1.y === 0) return horizontalIntersection(pt2, v2, pt1.y);\n if (v2.y === 0) return horizontalIntersection(pt1, v1, pt2.y);\n\n // General case for non-overlapping segments.\n // This algorithm is based on Schneider and Eberly.\n // http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf - pg 244\n\n const kross = crossProduct(v1, v2);\n if (kross == 0) return null;\n const ve = {\n x: pt2.x - pt1.x,\n y: pt2.y - pt1.y\n };\n const d1 = crossProduct(ve, v1) / kross;\n const d2 = crossProduct(ve, v2) / kross;\n\n // take the average of the two calculations to minimize rounding error\n const x1 = pt1.x + d2 * v1.x,\n x2 = pt2.x + d1 * v2.x;\n const y1 = pt1.y + d2 * v1.y,\n y2 = pt2.y + d1 * v2.y;\n const x = (x1 + x2) / 2;\n const y = (y1 + y2) / 2;\n return {\n x: x,\n y: y\n };\n };\n\n class SweepEvent {\n // for ordering sweep events in the sweep event queue\n static compare(a, b) {\n // favor event with a point that the sweep line hits first\n const ptCmp = SweepEvent.comparePoints(a.point, b.point);\n if (ptCmp !== 0) return ptCmp;\n\n // the points are the same, so link them if needed\n if (a.point !== b.point) a.link(b);\n\n // favor right events over left\n if (a.isLeft !== b.isLeft) return a.isLeft ? 1 : -1;\n\n // we have two matching left or right endpoints\n // ordering of this case is the same as for their segments\n return Segment.compare(a.segment, b.segment);\n }\n\n // for ordering points in sweep line order\n static comparePoints(aPt, bPt) {\n if (aPt.x < bPt.x) return -1;\n if (aPt.x > bPt.x) return 1;\n if (aPt.y < bPt.y) return -1;\n if (aPt.y > bPt.y) return 1;\n return 0;\n }\n\n // Warning: 'point' input will be modified and re-used (for performance)\n constructor(point, isLeft) {\n if (point.events === undefined) point.events = [this];else point.events.push(this);\n this.point = point;\n this.isLeft = isLeft;\n // this.segment, this.otherSE set by factory\n }\n link(other) {\n if (other.point === this.point) {\n throw new Error(\"Tried to link already linked events\");\n }\n const otherEvents = other.point.events;\n for (let i = 0, iMax = otherEvents.length; i < iMax; i++) {\n const evt = otherEvents[i];\n this.point.events.push(evt);\n evt.point = this.point;\n }\n this.checkForConsuming();\n }\n\n /* Do a pass over our linked events and check to see if any pair\n * of segments match, and should be consumed. */\n checkForConsuming() {\n // FIXME: The loops in this method run O(n^2) => no good.\n // Maintain little ordered sweep event trees?\n // Can we maintaining an ordering that avoids the need\n // for the re-sorting with getLeftmostComparator in geom-out?\n\n // Compare each pair of events to see if other events also match\n const numEvents = this.point.events.length;\n for (let i = 0; i < numEvents; i++) {\n const evt1 = this.point.events[i];\n if (evt1.segment.consumedBy !== undefined) continue;\n for (let j = i + 1; j < numEvents; j++) {\n const evt2 = this.point.events[j];\n if (evt2.consumedBy !== undefined) continue;\n if (evt1.otherSE.point.events !== evt2.otherSE.point.events) continue;\n evt1.segment.consume(evt2.segment);\n }\n }\n }\n getAvailableLinkedEvents() {\n // point.events is always of length 2 or greater\n const events = [];\n for (let i = 0, iMax = this.point.events.length; i < iMax; i++) {\n const evt = this.point.events[i];\n if (evt !== this && !evt.segment.ringOut && evt.segment.isInResult()) {\n events.push(evt);\n }\n }\n return events;\n }\n\n /**\n * Returns a comparator function for sorting linked events that will\n * favor the event that will give us the smallest left-side angle.\n * All ring construction starts as low as possible heading to the right,\n * so by always turning left as sharp as possible we'll get polygons\n * without uncessary loops & holes.\n *\n * The comparator function has a compute cache such that it avoids\n * re-computing already-computed values.\n */\n getLeftmostComparator(baseEvent) {\n const cache = new Map();\n const fillCache = linkedEvent => {\n const nextEvent = linkedEvent.otherSE;\n cache.set(linkedEvent, {\n sine: sineOfAngle(this.point, baseEvent.point, nextEvent.point),\n cosine: cosineOfAngle(this.point, baseEvent.point, nextEvent.point)\n });\n };\n return (a, b) => {\n if (!cache.has(a)) fillCache(a);\n if (!cache.has(b)) fillCache(b);\n const {\n sine: asine,\n cosine: acosine\n } = cache.get(a);\n const {\n sine: bsine,\n cosine: bcosine\n } = cache.get(b);\n\n // both on or above x-axis\n if (asine >= 0 && bsine >= 0) {\n if (acosine < bcosine) return 1;\n if (acosine > bcosine) return -1;\n return 0;\n }\n\n // both below x-axis\n if (asine < 0 && bsine < 0) {\n if (acosine < bcosine) return -1;\n if (acosine > bcosine) return 1;\n return 0;\n }\n\n // one above x-axis, one below\n if (bsine < asine) return -1;\n if (bsine > asine) return 1;\n return 0;\n };\n }\n }\n\n // Give segments unique ID's to get consistent sorting of\n // segments and sweep events when all else is identical\n let segmentId = 0;\n class Segment {\n /* This compare() function is for ordering segments in the sweep\n * line tree, and does so according to the following criteria:\n *\n * Consider the vertical line that lies an infinestimal step to the\n * right of the right-more of the two left endpoints of the input\n * segments. Imagine slowly moving a point up from negative infinity\n * in the increasing y direction. Which of the two segments will that\n * point intersect first? That segment comes 'before' the other one.\n *\n * If neither segment would be intersected by such a line, (if one\n * or more of the segments are vertical) then the line to be considered\n * is directly on the right-more of the two left inputs.\n */\n static compare(a, b) {\n const alx = a.leftSE.point.x;\n const blx = b.leftSE.point.x;\n const arx = a.rightSE.point.x;\n const brx = b.rightSE.point.x;\n\n // check if they're even in the same vertical plane\n if (brx < alx) return 1;\n if (arx < blx) return -1;\n const aly = a.leftSE.point.y;\n const bly = b.leftSE.point.y;\n const ary = a.rightSE.point.y;\n const bry = b.rightSE.point.y;\n\n // is left endpoint of segment B the right-more?\n if (alx < blx) {\n // are the two segments in the same horizontal plane?\n if (bly < aly && bly < ary) return 1;\n if (bly > aly && bly > ary) return -1;\n\n // is the B left endpoint colinear to segment A?\n const aCmpBLeft = a.comparePoint(b.leftSE.point);\n if (aCmpBLeft < 0) return 1;\n if (aCmpBLeft > 0) return -1;\n\n // is the A right endpoint colinear to segment B ?\n const bCmpARight = b.comparePoint(a.rightSE.point);\n if (bCmpARight !== 0) return bCmpARight;\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return -1;\n }\n\n // is left endpoint of segment A the right-more?\n if (alx > blx) {\n if (aly < bly && aly < bry) return -1;\n if (aly > bly && aly > bry) return 1;\n\n // is the A left endpoint colinear to segment B?\n const bCmpALeft = b.comparePoint(a.leftSE.point);\n if (bCmpALeft !== 0) return bCmpALeft;\n\n // is the B right endpoint colinear to segment A?\n const aCmpBRight = a.comparePoint(b.rightSE.point);\n if (aCmpBRight < 0) return 1;\n if (aCmpBRight > 0) return -1;\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return 1;\n }\n\n // if we get here, the two left endpoints are in the same\n // vertical plane, ie alx === blx\n\n // consider the lower left-endpoint to come first\n if (aly < bly) return -1;\n if (aly > bly) return 1;\n\n // left endpoints are identical\n // check for colinearity by using the left-more right endpoint\n\n // is the A right endpoint more left-more?\n if (arx < brx) {\n const bCmpARight = b.comparePoint(a.rightSE.point);\n if (bCmpARight !== 0) return bCmpARight;\n }\n\n // is the B right endpoint more left-more?\n if (arx > brx) {\n const aCmpBRight = a.comparePoint(b.rightSE.point);\n if (aCmpBRight < 0) return 1;\n if (aCmpBRight > 0) return -1;\n }\n if (arx !== brx) {\n // are these two [almost] vertical segments with opposite orientation?\n // if so, the one with the lower right endpoint comes first\n const ay = ary - aly;\n const ax = arx - alx;\n const by = bry - bly;\n const bx = brx - blx;\n if (ay > ax && by < bx) return 1;\n if (ay < ax && by > bx) return -1;\n }\n\n // we have colinear segments with matching orientation\n // consider the one with more left-more right endpoint to be first\n if (arx > brx) return 1;\n if (arx < brx) return -1;\n\n // if we get here, two two right endpoints are in the same\n // vertical plane, ie arx === brx\n\n // consider the lower right-endpoint to come first\n if (ary < bry) return -1;\n if (ary > bry) return 1;\n\n // right endpoints identical as well, so the segments are idential\n // fall back on creation order as consistent tie-breaker\n if (a.id < b.id) return -1;\n if (a.id > b.id) return 1;\n\n // identical segment, ie a === b\n return 0;\n }\n\n /* Warning: a reference to ringWindings input will be stored,\n * and possibly will be later modified */\n constructor(leftSE, rightSE, rings, windings) {\n this.id = ++segmentId;\n this.leftSE = leftSE;\n leftSE.segment = this;\n leftSE.otherSE = rightSE;\n this.rightSE = rightSE;\n rightSE.segment = this;\n rightSE.otherSE = leftSE;\n this.rings = rings;\n this.windings = windings;\n // left unset for performance, set later in algorithm\n // this.ringOut, this.consumedBy, this.prev\n }\n static fromRing(pt1, pt2, ring) {\n let leftPt, rightPt, winding;\n\n // ordering the two points according to sweep line ordering\n const cmpPts = SweepEvent.comparePoints(pt1, pt2);\n if (cmpPts < 0) {\n leftPt = pt1;\n rightPt = pt2;\n winding = 1;\n } else if (cmpPts > 0) {\n leftPt = pt2;\n rightPt = pt1;\n winding = -1;\n } else throw new Error(`Tried to create degenerate segment at [${pt1.x}, ${pt1.y}]`);\n const leftSE = new SweepEvent(leftPt, true);\n const rightSE = new SweepEvent(rightPt, false);\n return new Segment(leftSE, rightSE, [ring], [winding]);\n }\n\n /* When a segment is split, the rightSE is replaced with a new sweep event */\n replaceRightSE(newRightSE) {\n this.rightSE = newRightSE;\n this.rightSE.segment = this;\n this.rightSE.otherSE = this.leftSE;\n this.leftSE.otherSE = this.rightSE;\n }\n bbox() {\n const y1 = this.leftSE.point.y;\n const y2 = this.rightSE.point.y;\n return {\n ll: {\n x: this.leftSE.point.x,\n y: y1 < y2 ? y1 : y2\n },\n ur: {\n x: this.rightSE.point.x,\n y: y1 > y2 ? y1 : y2\n }\n };\n }\n\n /* A vector from the left point to the right */\n vector() {\n return {\n x: this.rightSE.point.x - this.leftSE.point.x,\n y: this.rightSE.point.y - this.leftSE.point.y\n };\n }\n isAnEndpoint(pt) {\n return pt.x === this.leftSE.point.x && pt.y === this.leftSE.point.y || pt.x === this.rightSE.point.x && pt.y === this.rightSE.point.y;\n }\n\n /* Compare this segment with a point.\n *\n * A point P is considered to be colinear to a segment if there\n * exists a distance D such that if we travel along the segment\n * from one * endpoint towards the other a distance D, we find\n * ourselves at point P.\n *\n * Return value indicates:\n *\n * 1: point lies above the segment (to the left of vertical)\n * 0: point is colinear to segment\n * -1: point lies below the segment (to the right of vertical)\n */\n comparePoint(point) {\n if (this.isAnEndpoint(point)) return 0;\n const lPt = this.leftSE.point;\n const rPt = this.rightSE.point;\n const v = this.vector();\n\n // Exactly vertical segments.\n if (lPt.x === rPt.x) {\n if (point.x === lPt.x) return 0;\n return point.x < lPt.x ? 1 : -1;\n }\n\n // Nearly vertical segments with an intersection.\n // Check to see where a point on the line with matching Y coordinate is.\n const yDist = (point.y - lPt.y) / v.y;\n const xFromYDist = lPt.x + yDist * v.x;\n if (point.x === xFromYDist) return 0;\n\n // General case.\n // Check to see where a point on the line with matching X coordinate is.\n const xDist = (point.x - lPt.x) / v.x;\n const yFromXDist = lPt.y + xDist * v.y;\n if (point.y === yFromXDist) return 0;\n return point.y < yFromXDist ? -1 : 1;\n }\n\n /**\n * Given another segment, returns the first non-trivial intersection\n * between the two segments (in terms of sweep line ordering), if it exists.\n *\n * A 'non-trivial' intersection is one that will cause one or both of the\n * segments to be split(). As such, 'trivial' vs. 'non-trivial' intersection:\n *\n * * endpoint of segA with endpoint of segB --> trivial\n * * endpoint of segA with point along segB --> non-trivial\n * * endpoint of segB with point along segA --> non-trivial\n * * point along segA with point along segB --> non-trivial\n *\n * If no non-trivial intersection exists, return null\n * Else, return null.\n */\n getIntersection(other) {\n // If bboxes don't overlap, there can't be any intersections\n const tBbox = this.bbox();\n const oBbox = other.bbox();\n const bboxOverlap = getBboxOverlap(tBbox, oBbox);\n if (bboxOverlap === null) return null;\n\n // We first check to see if the endpoints can be considered intersections.\n // This will 'snap' intersections to endpoints if possible, and will\n // handle cases of colinearity.\n\n const tlp = this.leftSE.point;\n const trp = this.rightSE.point;\n const olp = other.leftSE.point;\n const orp = other.rightSE.point;\n\n // does each endpoint touch the other segment?\n // note that we restrict the 'touching' definition to only allow segments\n // to touch endpoints that lie forward from where we are in the sweep line pass\n const touchesOtherLSE = isInBbox(tBbox, olp) && this.comparePoint(olp) === 0;\n const touchesThisLSE = isInBbox(oBbox, tlp) && other.comparePoint(tlp) === 0;\n const touchesOtherRSE = isInBbox(tBbox, orp) && this.comparePoint(orp) === 0;\n const touchesThisRSE = isInBbox(oBbox, trp) && other.comparePoint(trp) === 0;\n\n // do left endpoints match?\n if (touchesThisLSE && touchesOtherLSE) {\n // these two cases are for colinear segments with matching left\n // endpoints, and one segment being longer than the other\n if (touchesThisRSE && !touchesOtherRSE) return trp;\n if (!touchesThisRSE && touchesOtherRSE) return orp;\n // either the two segments match exactly (two trival intersections)\n // or just on their left endpoint (one trivial intersection\n return null;\n }\n\n // does this left endpoint matches (other doesn't)\n if (touchesThisLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesOtherRSE) {\n if (tlp.x === orp.x && tlp.y === orp.y) return null;\n }\n // t-intersection on left endpoint\n return tlp;\n }\n\n // does other left endpoint matches (this doesn't)\n if (touchesOtherLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesThisRSE) {\n if (trp.x === olp.x && trp.y === olp.y) return null;\n }\n // t-intersection on left endpoint\n return olp;\n }\n\n // trivial intersection on right endpoints\n if (touchesThisRSE && touchesOtherRSE) return null;\n\n // t-intersections on just one right endpoint\n if (touchesThisRSE) return trp;\n if (touchesOtherRSE) return orp;\n\n // None of our endpoints intersect. Look for a general intersection between\n // infinite lines laid over the segments\n const pt = intersection$1(tlp, this.vector(), olp, other.vector());\n\n // are the segments parrallel? Note that if they were colinear with overlap,\n // they would have an endpoint intersection and that case was already handled above\n if (pt === null) return null;\n\n // is the intersection found between the lines not on the segments?\n if (!isInBbox(bboxOverlap, pt)) return null;\n\n // round the the computed point if needed\n return rounder.round(pt.x, pt.y);\n }\n\n /**\n * Split the given segment into multiple segments on the given points.\n * * Each existing segment will retain its leftSE and a new rightSE will be\n * generated for it.\n * * A new segment will be generated which will adopt the original segment's\n * rightSE, and a new leftSE will be generated for it.\n * * If there are more than two points given to split on, new segments\n * in the middle will be generated with new leftSE and rightSE's.\n * * An array of the newly generated SweepEvents will be returned.\n *\n * Warning: input array of points is modified\n */\n split(point) {\n const newEvents = [];\n const alreadyLinked = point.events !== undefined;\n const newLeftSE = new SweepEvent(point, true);\n const newRightSE = new SweepEvent(point, false);\n const oldRightSE = this.rightSE;\n this.replaceRightSE(newRightSE);\n newEvents.push(newRightSE);\n newEvents.push(newLeftSE);\n const newSeg = new Segment(newLeftSE, oldRightSE, this.rings.slice(), this.windings.slice());\n\n // when splitting a nearly vertical downward-facing segment,\n // sometimes one of the resulting new segments is vertical, in which\n // case its left and right events may need to be swapped\n if (SweepEvent.comparePoints(newSeg.leftSE.point, newSeg.rightSE.point) > 0) {\n newSeg.swapEvents();\n }\n if (SweepEvent.comparePoints(this.leftSE.point, this.rightSE.point) > 0) {\n this.swapEvents();\n }\n\n // in the point we just used to create new sweep events with was already\n // linked to other events, we need to check if either of the affected\n // segments should be consumed\n if (alreadyLinked) {\n newLeftSE.checkForConsuming();\n newRightSE.checkForConsuming();\n }\n return newEvents;\n }\n\n /* Swap which event is left and right */\n swapEvents() {\n const tmpEvt = this.rightSE;\n this.rightSE = this.leftSE;\n this.leftSE = tmpEvt;\n this.leftSE.isLeft = true;\n this.rightSE.isLeft = false;\n for (let i = 0, iMax = this.windings.length; i < iMax; i++) {\n this.windings[i] *= -1;\n }\n }\n\n /* Consume another segment. We take their rings under our wing\n * and mark them as consumed. Use for perfectly overlapping segments */\n consume(other) {\n let consumer = this;\n let consumee = other;\n while (consumer.consumedBy) consumer = consumer.consumedBy;\n while (consumee.consumedBy) consumee = consumee.consumedBy;\n const cmp = Segment.compare(consumer, consumee);\n if (cmp === 0) return; // already consumed\n // the winner of the consumption is the earlier segment\n // according to sweep line ordering\n if (cmp > 0) {\n const tmp = consumer;\n consumer = consumee;\n consumee = tmp;\n }\n\n // make sure a segment doesn't consume it's prev\n if (consumer.prev === consumee) {\n const tmp = consumer;\n consumer = consumee;\n consumee = tmp;\n }\n for (let i = 0, iMax = consumee.rings.length; i < iMax; i++) {\n const ring = consumee.rings[i];\n const winding = consumee.windings[i];\n const index = consumer.rings.indexOf(ring);\n if (index === -1) {\n consumer.rings.push(ring);\n consumer.windings.push(winding);\n } else consumer.windings[index] += winding;\n }\n consumee.rings = null;\n consumee.windings = null;\n consumee.consumedBy = consumer;\n\n // mark sweep events consumed as to maintain ordering in sweep event queue\n consumee.leftSE.consumedBy = consumer.leftSE;\n consumee.rightSE.consumedBy = consumer.rightSE;\n }\n\n /* The first segment previous segment chain that is in the result */\n prevInResult() {\n if (this._prevInResult !== undefined) return this._prevInResult;\n if (!this.prev) this._prevInResult = null;else if (this.prev.isInResult()) this._prevInResult = this.prev;else this._prevInResult = this.prev.prevInResult();\n return this._prevInResult;\n }\n beforeState() {\n if (this._beforeState !== undefined) return this._beforeState;\n if (!this.prev) this._beforeState = {\n rings: [],\n windings: [],\n multiPolys: []\n };else {\n const seg = this.prev.consumedBy || this.prev;\n this._beforeState = seg.afterState();\n }\n return this._beforeState;\n }\n afterState() {\n if (this._afterState !== undefined) return this._afterState;\n const beforeState = this.beforeState();\n this._afterState = {\n rings: beforeState.rings.slice(0),\n windings: beforeState.windings.slice(0),\n multiPolys: []\n };\n const ringsAfter = this._afterState.rings;\n const windingsAfter = this._afterState.windings;\n const mpsAfter = this._afterState.multiPolys;\n\n // calculate ringsAfter, windingsAfter\n for (let i = 0, iMax = this.rings.length; i < iMax; i++) {\n const ring = this.rings[i];\n const winding = this.windings[i];\n const index = ringsAfter.indexOf(ring);\n if (index === -1) {\n ringsAfter.push(ring);\n windingsAfter.push(winding);\n } else windingsAfter[index] += winding;\n }\n\n // calcualte polysAfter\n const polysAfter = [];\n const polysExclude = [];\n for (let i = 0, iMax = ringsAfter.length; i < iMax; i++) {\n if (windingsAfter[i] === 0) continue; // non-zero rule\n const ring = ringsAfter[i];\n const poly = ring.poly;\n if (polysExclude.indexOf(poly) !== -1) continue;\n if (ring.isExterior) polysAfter.push(poly);else {\n if (polysExclude.indexOf(poly) === -1) polysExclude.push(poly);\n const index = polysAfter.indexOf(ring.poly);\n if (index !== -1) polysAfter.splice(index, 1);\n }\n }\n\n // calculate multiPolysAfter\n for (let i = 0, iMax = polysAfter.length; i < iMax; i++) {\n const mp = polysAfter[i].multiPoly;\n if (mpsAfter.indexOf(mp) === -1) mpsAfter.push(mp);\n }\n return this._afterState;\n }\n\n /* Is this segment part of the final result? */\n isInResult() {\n // if we've been consumed, we're not in the result\n if (this.consumedBy) return false;\n if (this._isInResult !== undefined) return this._isInResult;\n const mpsBefore = this.beforeState().multiPolys;\n const mpsAfter = this.afterState().multiPolys;\n switch (operation.type) {\n case \"union\":\n {\n // UNION - included iff:\n // * On one side of us there is 0 poly interiors AND\n // * On the other side there is 1 or more.\n const noBefores = mpsBefore.length === 0;\n const noAfters = mpsAfter.length === 0;\n this._isInResult = noBefores !== noAfters;\n break;\n }\n case \"intersection\":\n {\n // INTERSECTION - included iff:\n // * on one side of us all multipolys are rep. with poly interiors AND\n // * on the other side of us, not all multipolys are repsented\n // with poly interiors\n let least;\n let most;\n if (mpsBefore.length < mpsAfter.length) {\n least = mpsBefore.length;\n most = mpsAfter.length;\n } else {\n least = mpsAfter.length;\n most = mpsBefore.length;\n }\n this._isInResult = most === operation.numMultiPolys && least < most;\n break;\n }\n case \"xor\":\n {\n // XOR - included iff:\n // * the difference between the number of multipolys represented\n // with poly interiors on our two sides is an odd number\n const diff = Math.abs(mpsBefore.length - mpsAfter.length);\n this._isInResult = diff % 2 === 1;\n break;\n }\n case \"difference\":\n {\n // DIFFERENCE included iff:\n // * on exactly one side, we have just the subject\n const isJustSubject = mps => mps.length === 1 && mps[0].isSubject;\n this._isInResult = isJustSubject(mpsBefore) !== isJustSubject(mpsAfter);\n break;\n }\n default:\n throw new Error(`Unrecognized operation type found ${operation.type}`);\n }\n return this._isInResult;\n }\n }\n\n class RingIn {\n constructor(geomRing, poly, isExterior) {\n if (!Array.isArray(geomRing) || geomRing.length === 0) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n this.poly = poly;\n this.isExterior = isExterior;\n this.segments = [];\n if (typeof geomRing[0][0] !== \"number\" || typeof geomRing[0][1] !== \"number\") {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n const firstPoint = rounder.round(geomRing[0][0], geomRing[0][1]);\n this.bbox = {\n ll: {\n x: firstPoint.x,\n y: firstPoint.y\n },\n ur: {\n x: firstPoint.x,\n y: firstPoint.y\n }\n };\n let prevPoint = firstPoint;\n for (let i = 1, iMax = geomRing.length; i < iMax; i++) {\n if (typeof geomRing[i][0] !== \"number\" || typeof geomRing[i][1] !== \"number\") {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n let point = rounder.round(geomRing[i][0], geomRing[i][1]);\n // skip repeated points\n if (point.x === prevPoint.x && point.y === prevPoint.y) continue;\n this.segments.push(Segment.fromRing(prevPoint, point, this));\n if (point.x < this.bbox.ll.x) this.bbox.ll.x = point.x;\n if (point.y < this.bbox.ll.y) this.bbox.ll.y = point.y;\n if (point.x > this.bbox.ur.x) this.bbox.ur.x = point.x;\n if (point.y > this.bbox.ur.y) this.bbox.ur.y = point.y;\n prevPoint = point;\n }\n // add segment from last to first if last is not the same as first\n if (firstPoint.x !== prevPoint.x || firstPoint.y !== prevPoint.y) {\n this.segments.push(Segment.fromRing(prevPoint, firstPoint, this));\n }\n }\n getSweepEvents() {\n const sweepEvents = [];\n for (let i = 0, iMax = this.segments.length; i < iMax; i++) {\n const segment = this.segments[i];\n sweepEvents.push(segment.leftSE);\n sweepEvents.push(segment.rightSE);\n }\n return sweepEvents;\n }\n }\n class PolyIn {\n constructor(geomPoly, multiPoly) {\n if (!Array.isArray(geomPoly)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n this.exteriorRing = new RingIn(geomPoly[0], this, true);\n // copy by value\n this.bbox = {\n ll: {\n x: this.exteriorRing.bbox.ll.x,\n y: this.exteriorRing.bbox.ll.y\n },\n ur: {\n x: this.exteriorRing.bbox.ur.x,\n y: this.exteriorRing.bbox.ur.y\n }\n };\n this.interiorRings = [];\n for (let i = 1, iMax = geomPoly.length; i < iMax; i++) {\n const ring = new RingIn(geomPoly[i], this, false);\n if (ring.bbox.ll.x < this.bbox.ll.x) this.bbox.ll.x = ring.bbox.ll.x;\n if (ring.bbox.ll.y < this.bbox.ll.y) this.bbox.ll.y = ring.bbox.ll.y;\n if (ring.bbox.ur.x > this.bbox.ur.x) this.bbox.ur.x = ring.bbox.ur.x;\n if (ring.bbox.ur.y > this.bbox.ur.y) this.bbox.ur.y = ring.bbox.ur.y;\n this.interiorRings.push(ring);\n }\n this.multiPoly = multiPoly;\n }\n getSweepEvents() {\n const sweepEvents = this.exteriorRing.getSweepEvents();\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringSweepEvents = this.interiorRings[i].getSweepEvents();\n for (let j = 0, jMax = ringSweepEvents.length; j < jMax; j++) {\n sweepEvents.push(ringSweepEvents[j]);\n }\n }\n return sweepEvents;\n }\n }\n class MultiPolyIn {\n constructor(geom, isSubject) {\n if (!Array.isArray(geom)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n try {\n // if the input looks like a polygon, convert it to a multipolygon\n if (typeof geom[0][0][0] === \"number\") geom = [geom];\n } catch (ex) {\n // The input is either malformed or has empty arrays.\n // In either case, it will be handled later on.\n }\n this.polys = [];\n this.bbox = {\n ll: {\n x: Number.POSITIVE_INFINITY,\n y: Number.POSITIVE_INFINITY\n },\n ur: {\n x: Number.NEGATIVE_INFINITY,\n y: Number.NEGATIVE_INFINITY\n }\n };\n for (let i = 0, iMax = geom.length; i < iMax; i++) {\n const poly = new PolyIn(geom[i], this);\n if (poly.bbox.ll.x < this.bbox.ll.x) this.bbox.ll.x = poly.bbox.ll.x;\n if (poly.bbox.ll.y < this.bbox.ll.y) this.bbox.ll.y = poly.bbox.ll.y;\n if (poly.bbox.ur.x > this.bbox.ur.x) this.bbox.ur.x = poly.bbox.ur.x;\n if (poly.bbox.ur.y > this.bbox.ur.y) this.bbox.ur.y = poly.bbox.ur.y;\n this.polys.push(poly);\n }\n this.isSubject = isSubject;\n }\n getSweepEvents() {\n const sweepEvents = [];\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polySweepEvents = this.polys[i].getSweepEvents();\n for (let j = 0, jMax = polySweepEvents.length; j < jMax; j++) {\n sweepEvents.push(polySweepEvents[j]);\n }\n }\n return sweepEvents;\n }\n }\n\n class RingOut {\n /* Given the segments from the sweep line pass, compute & return a series\n * of closed rings from all the segments marked to be part of the result */\n static factory(allSegments) {\n const ringsOut = [];\n for (let i = 0, iMax = allSegments.length; i < iMax; i++) {\n const segment = allSegments[i];\n if (!segment.isInResult() || segment.ringOut) continue;\n let prevEvent = null;\n let event = segment.leftSE;\n let nextEvent = segment.rightSE;\n const events = [event];\n const startingPoint = event.point;\n const intersectionLEs = [];\n\n /* Walk the chain of linked events to form a closed ring */\n while (true) {\n prevEvent = event;\n event = nextEvent;\n events.push(event);\n\n /* Is the ring complete? */\n if (event.point === startingPoint) break;\n while (true) {\n const availableLEs = event.getAvailableLinkedEvents();\n\n /* Did we hit a dead end? This shouldn't happen.\n * Indicates some earlier part of the algorithm malfunctioned. */\n if (availableLEs.length === 0) {\n const firstPt = events[0].point;\n const lastPt = events[events.length - 1].point;\n throw new Error(`Unable to complete output ring starting at [${firstPt.x},` + ` ${firstPt.y}]. Last matching segment found ends at` + ` [${lastPt.x}, ${lastPt.y}].`);\n }\n\n /* Only one way to go, so cotinue on the path */\n if (availableLEs.length === 1) {\n nextEvent = availableLEs[0].otherSE;\n break;\n }\n\n /* We must have an intersection. Check for a completed loop */\n let indexLE = null;\n for (let j = 0, jMax = intersectionLEs.length; j < jMax; j++) {\n if (intersectionLEs[j].point === event.point) {\n indexLE = j;\n break;\n }\n }\n /* Found a completed loop. Cut that off and make a ring */\n if (indexLE !== null) {\n const intersectionLE = intersectionLEs.splice(indexLE)[0];\n const ringEvents = events.splice(intersectionLE.index);\n ringEvents.unshift(ringEvents[0].otherSE);\n ringsOut.push(new RingOut(ringEvents.reverse()));\n continue;\n }\n /* register the intersection */\n intersectionLEs.push({\n index: events.length,\n point: event.point\n });\n /* Choose the left-most option to continue the walk */\n const comparator = event.getLeftmostComparator(prevEvent);\n nextEvent = availableLEs.sort(comparator)[0].otherSE;\n break;\n }\n }\n ringsOut.push(new RingOut(events));\n }\n return ringsOut;\n }\n constructor(events) {\n this.events = events;\n for (let i = 0, iMax = events.length; i < iMax; i++) {\n events[i].segment.ringOut = this;\n }\n this.poly = null;\n }\n getGeom() {\n // Remove superfluous points (ie extra points along a straight line),\n let prevPt = this.events[0].point;\n const points = [prevPt];\n for (let i = 1, iMax = this.events.length - 1; i < iMax; i++) {\n const pt = this.events[i].point;\n const nextPt = this.events[i + 1].point;\n if (compareVectorAngles(pt, prevPt, nextPt) === 0) continue;\n points.push(pt);\n prevPt = pt;\n }\n\n // ring was all (within rounding error of angle calc) colinear points\n if (points.length === 1) return null;\n\n // check if the starting point is necessary\n const pt = points[0];\n const nextPt = points[1];\n if (compareVectorAngles(pt, prevPt, nextPt) === 0) points.shift();\n points.push(points[0]);\n const step = this.isExteriorRing() ? 1 : -1;\n const iStart = this.isExteriorRing() ? 0 : points.length - 1;\n const iEnd = this.isExteriorRing() ? points.length : -1;\n const orderedPoints = [];\n for (let i = iStart; i != iEnd; i += step) orderedPoints.push([points[i].x, points[i].y]);\n return orderedPoints;\n }\n isExteriorRing() {\n if (this._isExteriorRing === undefined) {\n const enclosing = this.enclosingRing();\n this._isExteriorRing = enclosing ? !enclosing.isExteriorRing() : true;\n }\n return this._isExteriorRing;\n }\n enclosingRing() {\n if (this._enclosingRing === undefined) {\n this._enclosingRing = this._calcEnclosingRing();\n }\n return this._enclosingRing;\n }\n\n /* Returns the ring that encloses this one, if any */\n _calcEnclosingRing() {\n // start with the ealier sweep line event so that the prevSeg\n // chain doesn't lead us inside of a loop of ours\n let leftMostEvt = this.events[0];\n for (let i = 1, iMax = this.events.length; i < iMax; i++) {\n const evt = this.events[i];\n if (SweepEvent.compare(leftMostEvt, evt) > 0) leftMostEvt = evt;\n }\n let prevSeg = leftMostEvt.segment.prevInResult();\n let prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null;\n while (true) {\n // no segment found, thus no ring can enclose us\n if (!prevSeg) return null;\n\n // no segments below prev segment found, thus the ring of the prev\n // segment must loop back around and enclose us\n if (!prevPrevSeg) return prevSeg.ringOut;\n\n // if the two segments are of different rings, the ring of the prev\n // segment must either loop around us or the ring of the prev prev\n // seg, which would make us and the ring of the prev peers\n if (prevPrevSeg.ringOut !== prevSeg.ringOut) {\n if (prevPrevSeg.ringOut.enclosingRing() !== prevSeg.ringOut) {\n return prevSeg.ringOut;\n } else return prevSeg.ringOut.enclosingRing();\n }\n\n // two segments are from the same ring, so this was a penisula\n // of that ring. iterate downward, keep searching\n prevSeg = prevPrevSeg.prevInResult();\n prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null;\n }\n }\n }\n class PolyOut {\n constructor(exteriorRing) {\n this.exteriorRing = exteriorRing;\n exteriorRing.poly = this;\n this.interiorRings = [];\n }\n addInterior(ring) {\n this.interiorRings.push(ring);\n ring.poly = this;\n }\n getGeom() {\n const geom = [this.exteriorRing.getGeom()];\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (geom[0] === null) return null;\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringGeom = this.interiorRings[i].getGeom();\n // interior ring was all (within rounding error of angle calc) colinear points\n if (ringGeom === null) continue;\n geom.push(ringGeom);\n }\n return geom;\n }\n }\n class MultiPolyOut {\n constructor(rings) {\n this.rings = rings;\n this.polys = this._composePolys(rings);\n }\n getGeom() {\n const geom = [];\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polyGeom = this.polys[i].getGeom();\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (polyGeom === null) continue;\n geom.push(polyGeom);\n }\n return geom;\n }\n _composePolys(rings) {\n const polys = [];\n for (let i = 0, iMax = rings.length; i < iMax; i++) {\n const ring = rings[i];\n if (ring.poly) continue;\n if (ring.isExteriorRing()) polys.push(new PolyOut(ring));else {\n const enclosingRing = ring.enclosingRing();\n if (!enclosingRing.poly) polys.push(new PolyOut(enclosingRing));\n enclosingRing.poly.addInterior(ring);\n }\n }\n return polys;\n }\n }\n\n /**\n * NOTE: We must be careful not to change any segments while\n * they are in the SplayTree. AFAIK, there's no way to tell\n * the tree to rebalance itself - thus before splitting\n * a segment that's in the tree, we remove it from the tree,\n * do the split, then re-insert it. (Even though splitting a\n * segment *shouldn't* change its correct position in the\n * sweep line tree, the reality is because of rounding errors,\n * it sometimes does.)\n */\n\n class SweepLine {\n constructor(queue) {\n let comparator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Segment.compare;\n this.queue = queue;\n this.tree = new Tree(comparator);\n this.segments = [];\n }\n process(event) {\n const segment = event.segment;\n const newEvents = [];\n\n // if we've already been consumed by another segment,\n // clean up our body parts and get out\n if (event.consumedBy) {\n if (event.isLeft) this.queue.remove(event.otherSE);else this.tree.remove(segment);\n return newEvents;\n }\n const node = event.isLeft ? this.tree.add(segment) : this.tree.find(segment);\n if (!node) throw new Error(`Unable to find segment #${segment.id} ` + `[${segment.leftSE.point.x}, ${segment.leftSE.point.y}] -> ` + `[${segment.rightSE.point.x}, ${segment.rightSE.point.y}] ` + \"in SweepLine tree.\");\n let prevNode = node;\n let nextNode = node;\n let prevSeg = undefined;\n let nextSeg = undefined;\n\n // skip consumed segments still in tree\n while (prevSeg === undefined) {\n prevNode = this.tree.prev(prevNode);\n if (prevNode === null) prevSeg = null;else if (prevNode.key.consumedBy === undefined) prevSeg = prevNode.key;\n }\n\n // skip consumed segments still in tree\n while (nextSeg === undefined) {\n nextNode = this.tree.next(nextNode);\n if (nextNode === null) nextSeg = null;else if (nextNode.key.consumedBy === undefined) nextSeg = nextNode.key;\n }\n if (event.isLeft) {\n // Check for intersections against the previous segment in the sweep line\n let prevMySplitter = null;\n if (prevSeg) {\n const prevInter = prevSeg.getIntersection(segment);\n if (prevInter !== null) {\n if (!segment.isAnEndpoint(prevInter)) prevMySplitter = prevInter;\n if (!prevSeg.isAnEndpoint(prevInter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, prevInter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n }\n }\n\n // Check for intersections against the next segment in the sweep line\n let nextMySplitter = null;\n if (nextSeg) {\n const nextInter = nextSeg.getIntersection(segment);\n if (nextInter !== null) {\n if (!segment.isAnEndpoint(nextInter)) nextMySplitter = nextInter;\n if (!nextSeg.isAnEndpoint(nextInter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, nextInter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n }\n }\n\n // For simplicity, even if we find more than one intersection we only\n // spilt on the 'earliest' (sweep-line style) of the intersections.\n // The other intersection will be handled in a future process().\n if (prevMySplitter !== null || nextMySplitter !== null) {\n let mySplitter = null;\n if (prevMySplitter === null) mySplitter = nextMySplitter;else if (nextMySplitter === null) mySplitter = prevMySplitter;else {\n const cmpSplitters = SweepEvent.comparePoints(prevMySplitter, nextMySplitter);\n mySplitter = cmpSplitters <= 0 ? prevMySplitter : nextMySplitter;\n }\n\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n this.queue.remove(segment.rightSE);\n newEvents.push(segment.rightSE);\n const newEventsFromSplit = segment.split(mySplitter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n if (newEvents.length > 0) {\n // We found some intersections, so re-do the current event to\n // make sure sweep line ordering is totally consistent for later\n // use with the segment 'prev' pointers\n this.tree.remove(segment);\n newEvents.push(event);\n } else {\n // done with left event\n this.segments.push(segment);\n segment.prev = prevSeg;\n }\n } else {\n // event.isRight\n\n // since we're about to be removed from the sweep line, check for\n // intersections between our previous and next segments\n if (prevSeg && nextSeg) {\n const inter = prevSeg.getIntersection(nextSeg);\n if (inter !== null) {\n if (!prevSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, inter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n if (!nextSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, inter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n }\n }\n this.tree.remove(segment);\n }\n return newEvents;\n }\n\n /* Safely split a segment that is currently in the datastructures\n * IE - a segment other than the one that is currently being processed. */\n _splitSafely(seg, pt) {\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n // removeNode() doesn't work, so have re-find the seg\n // https://github.com/w8r/splay-tree/pull/5\n this.tree.remove(seg);\n const rightSE = seg.rightSE;\n this.queue.remove(rightSE);\n const newEvents = seg.split(pt);\n newEvents.push(rightSE);\n // splitting can trigger consumption\n if (seg.consumedBy === undefined) this.tree.add(seg);\n return newEvents;\n }\n }\n\n // Limits on iterative processes to prevent infinite loops - usually caused by floating-point math round-off errors.\n const POLYGON_CLIPPING_MAX_QUEUE_SIZE = typeof process !== \"undefined\" && process.env.POLYGON_CLIPPING_MAX_QUEUE_SIZE || 1000000;\n const POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS = typeof process !== \"undefined\" && process.env.POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS || 1000000;\n class Operation {\n run(type, geom, moreGeoms) {\n operation.type = type;\n rounder.reset();\n\n /* Convert inputs to MultiPoly objects */\n const multipolys = [new MultiPolyIn(geom, true)];\n for (let i = 0, iMax = moreGeoms.length; i < iMax; i++) {\n multipolys.push(new MultiPolyIn(moreGeoms[i], false));\n }\n operation.numMultiPolys = multipolys.length;\n\n /* BBox optimization for difference operation\n * If the bbox of a multipolygon that's part of the clipping doesn't\n * intersect the bbox of the subject at all, we can just drop that\n * multiploygon. */\n if (operation.type === \"difference\") {\n // in place removal\n const subject = multipolys[0];\n let i = 1;\n while (i < multipolys.length) {\n if (getBboxOverlap(multipolys[i].bbox, subject.bbox) !== null) i++;else multipolys.splice(i, 1);\n }\n }\n\n /* BBox optimization for intersection operation\n * If we can find any pair of multipolygons whose bbox does not overlap,\n * then the result will be empty. */\n if (operation.type === \"intersection\") {\n // TODO: this is O(n^2) in number of polygons. By sorting the bboxes,\n // it could be optimized to O(n * ln(n))\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const mpA = multipolys[i];\n for (let j = i + 1, jMax = multipolys.length; j < jMax; j++) {\n if (getBboxOverlap(mpA.bbox, multipolys[j].bbox) === null) return [];\n }\n }\n }\n\n /* Put segment endpoints in a priority queue */\n const queue = new Tree(SweepEvent.compare);\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const sweepEvents = multipolys[i].getSweepEvents();\n for (let j = 0, jMax = sweepEvents.length; j < jMax; j++) {\n queue.insert(sweepEvents[j]);\n if (queue.size > POLYGON_CLIPPING_MAX_QUEUE_SIZE) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n throw new Error(\"Infinite loop when putting segment endpoints in a priority queue \" + \"(queue size too big).\");\n }\n }\n }\n\n /* Pass the sweep line over those endpoints */\n const sweepLine = new SweepLine(queue);\n let prevQueueSize = queue.size;\n let node = queue.pop();\n while (node) {\n const evt = node.key;\n if (queue.size === prevQueueSize) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n const seg = evt.segment;\n throw new Error(`Unable to pop() ${evt.isLeft ? \"left\" : \"right\"} SweepEvent ` + `[${evt.point.x}, ${evt.point.y}] from segment #${seg.id} ` + `[${seg.leftSE.point.x}, ${seg.leftSE.point.y}] -> ` + `[${seg.rightSE.point.x}, ${seg.rightSE.point.y}] from queue.`);\n }\n if (queue.size > POLYGON_CLIPPING_MAX_QUEUE_SIZE) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n throw new Error(\"Infinite loop when passing sweep line over endpoints \" + \"(queue size too big).\");\n }\n if (sweepLine.segments.length > POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n throw new Error(\"Infinite loop when passing sweep line over endpoints \" + \"(too many sweep line segments).\");\n }\n const newEvents = sweepLine.process(evt);\n for (let i = 0, iMax = newEvents.length; i < iMax; i++) {\n const evt = newEvents[i];\n if (evt.consumedBy === undefined) queue.insert(evt);\n }\n prevQueueSize = queue.size;\n node = queue.pop();\n }\n\n // free some memory we don't need anymore\n rounder.reset();\n\n /* Collect and compile segments we're keeping into a multipolygon */\n const ringsOut = RingOut.factory(sweepLine.segments);\n const result = new MultiPolyOut(ringsOut);\n return result.getGeom();\n }\n }\n\n // singleton available by import\n const operation = new Operation();\n\n const union = function (geom) {\n for (var _len = arguments.length, moreGeoms = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n moreGeoms[_key - 1] = arguments[_key];\n }\n return operation.run(\"union\", geom, moreGeoms);\n };\n const intersection = function (geom) {\n for (var _len2 = arguments.length, moreGeoms = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n moreGeoms[_key2 - 1] = arguments[_key2];\n }\n return operation.run(\"intersection\", geom, moreGeoms);\n };\n const xor = function (geom) {\n for (var _len3 = arguments.length, moreGeoms = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {\n moreGeoms[_key3 - 1] = arguments[_key3];\n }\n return operation.run(\"xor\", geom, moreGeoms);\n };\n const difference = function (subjectGeom) {\n for (var _len4 = arguments.length, clippingGeoms = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {\n clippingGeoms[_key4 - 1] = arguments[_key4];\n }\n return operation.run(\"difference\", subjectGeom, clippingGeoms);\n };\n var index = {\n union: union,\n intersection: intersection,\n xor: xor,\n difference: difference\n };\n\n return index;\n\n}));\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { deepEqual } from 'fast-equals';\nimport turf_bboxClip from '@turf/bbox-clip';\nimport stringify from 'fast-json-stable-stringify';\nimport polygonClipping from 'polygon-clipping';\n\nimport Protobuf from 'pbf';\nimport { VectorTile } from '@mapbox/vector-tile';\n\nimport { utilHashcode, utilRebind, utilTiler } from '../util';\n\n\nvar tiler = utilTiler().tileSize(512).margin(1);\nvar dispatch = d3_dispatch('loadedData');\nvar _vtCache;\n\n\nfunction abortRequest(controller) {\n controller.abort();\n}\n\n\nfunction vtToGeoJSON(data, tile, mergeCache) {\n var vectorTile = new VectorTile(new Protobuf(data));\n var layers = Object.keys(vectorTile.layers);\n if (!Array.isArray(layers)) { layers = [layers]; }\n\n var features = [];\n layers.forEach(function(layerID) {\n var layer = vectorTile.layers[layerID];\n if (layer) {\n for (var i = 0; i < layer.length; i++) {\n var feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n var geometry = feature.geometry;\n\n // Treat all Polygons as MultiPolygons\n if (geometry.type === 'Polygon') {\n geometry.type = 'MultiPolygon';\n geometry.coordinates = [geometry.coordinates];\n }\n\n var isClipped = false;\n\n // Clip to tile bounds\n if (geometry.type === 'MultiPolygon') {\n var featureClip = turf_bboxClip(feature, tile.extent.rectangle());\n if (!deepEqual(feature.geometry, featureClip.geometry)) {\n // feature = featureClip;\n isClipped = true;\n }\n if (!feature.geometry.coordinates.length) continue; // not actually on this tile\n if (!feature.geometry.coordinates[0].length) continue; // not actually on this tile\n }\n\n // Generate some unique IDs and add some metadata\n var featurehash = utilHashcode(stringify(feature));\n var propertyhash = utilHashcode(stringify(feature.properties || {}));\n feature.__layerID__ = layerID.replace(/[^_a-zA-Z0-9\\-]/g, '_');\n feature.__featurehash__ = featurehash;\n feature.__propertyhash__ = propertyhash;\n features.push(feature);\n\n // Clipped Polygons at same zoom with identical properties can get merged\n if (isClipped && geometry.type === 'MultiPolygon') {\n var merged = mergeCache[propertyhash];\n if (merged && merged.length) {\n var other = merged[0];\n var coords = polygonClipping.union(\n feature.geometry.coordinates,\n other.geometry.coordinates\n );\n\n if (!coords || !coords.length) {\n continue; // something failed in polygon union\n }\n\n merged.push(feature);\n for (var j = 0; j < merged.length; j++) { // all these features get...\n merged[j].geometry.coordinates = coords; // same coords\n merged[j].__featurehash__ = featurehash; // same hash, so deduplication works\n }\n } else {\n mergeCache[propertyhash] = [feature];\n }\n }\n }\n }\n });\n\n return features;\n}\n\n\nfunction loadTile(source, tile) {\n if (source.loaded[tile.id] || source.inflight[tile.id]) return;\n\n var url = source.template\n .replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n // TMS-flipped y coordinate\n .replace(/\\{[t-]y\\}/, Math.pow(2, tile.xyz[2]) - tile.xyz[1] - 1)\n .replace(/\\{z(oom)?\\}/, tile.xyz[2])\n .replace(/\\{switch:([^}]+)\\}/, function(s, r) {\n var subdomains = r.split(',');\n return subdomains[(tile.xyz[0] + tile.xyz[1]) % subdomains.length];\n });\n\n\n var controller = new AbortController();\n source.inflight[tile.id] = controller;\n\n fetch(url, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n source.loaded[tile.id] = [];\n delete source.inflight[tile.id];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (!data) {\n throw new Error('No Data');\n }\n\n var z = tile.xyz[2];\n if (!source.canMerge[z]) {\n source.canMerge[z] = {}; // initialize mergeCache\n }\n\n source.loaded[tile.id] = vtToGeoJSON(data, tile, source.canMerge[z]);\n dispatch.call('loadedData');\n })\n .catch(function() {\n source.loaded[tile.id] = [];\n delete source.inflight[tile.id];\n });\n}\n\n\nexport default {\n\n init: function() {\n if (!_vtCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n\n reset: function() {\n for (var sourceID in _vtCache) {\n var source = _vtCache[sourceID];\n if (source && source.inflight) {\n Object.values(source.inflight).forEach(abortRequest);\n }\n }\n\n _vtCache = {};\n },\n\n\n addSource: function(sourceID, template) {\n _vtCache[sourceID] = { template: template, inflight: {}, loaded: {}, canMerge: {} };\n return _vtCache[sourceID];\n },\n\n\n data: function(sourceID, projection) {\n var source = _vtCache[sourceID];\n if (!source) return [];\n\n var tiles = tiler.getTiles(projection);\n var seen = {};\n var results = [];\n\n for (var i = 0; i < tiles.length; i++) {\n var features = source.loaded[tiles[i].id];\n if (!features || !features.length) continue;\n\n for (var j = 0; j < features.length; j++) {\n var feature = features[j];\n var hash = feature.__featurehash__;\n if (seen[hash]) continue;\n seen[hash] = true;\n\n // return a shallow copy, because the hash may change\n // later if this feature gets merged with another\n results.push(Object.assign({}, feature)); // shallow copy\n }\n }\n\n return results;\n },\n\n\n loadTiles: function(sourceID, template, projection) {\n var source = _vtCache[sourceID];\n if (!source) {\n source = this.addSource(sourceID, template);\n }\n\n var tiles = tiler.getTiles(projection);\n\n // abort inflight requests that are no longer needed\n Object.keys(source.inflight).forEach(function(k) {\n var wanted = tiles.find(function(tile) { return k === tile.id; });\n if (!wanted) {\n abortRequest(source.inflight[k]);\n delete source.inflight[k];\n }\n });\n\n tiles.forEach(function(tile) {\n loadTile(source, tile);\n });\n },\n\n\n cache: function() {\n return _vtCache;\n }\n\n};\n", "import { json as d3_json } from 'd3-fetch';\n\nimport { utilQsString } from '../util';\nimport { localizer } from '../core/localizer';\n\nvar apibase = 'https://www.wikidata.org/w/api.php?';\nvar _wikidataCache = {};\n\n\nexport default {\n\n init: function() {},\n\n reset: function() {\n _wikidataCache = {};\n },\n\n\n // Search for Wikidata items matching the query\n itemsForSearchQuery: function(query, callback, language) {\n if (!query) {\n if (callback) callback('No query', {});\n return;\n }\n\n var lang = this.languagesToQuery()[0];\n\n var url = apibase + utilQsString({\n action: 'wbsearchentities',\n format: 'json',\n formatversion: 2,\n search: query,\n type: 'item',\n // the language to search\n language: language || lang,\n // the language for the label and description in the result\n uselang: lang,\n limit: 10,\n origin: '*'\n });\n\n d3_json(url)\n .then(result => {\n if (result && result.error) {\n if (result.error.code === 'badvalue' &&\n result.error.info.includes(lang) &&\n !language && lang.includes('-')) {\n // retry without \"country suffix\" region subtag\n this.itemsForSearchQuery(query, callback, lang.split('-')[0]);\n return;\n } else {\n throw new Error(result.error);\n }\n }\n if (callback) callback(null, result.search || {});\n })\n .catch(function(err) {\n if (callback) callback(err.message, {});\n });\n },\n\n\n // Given a Wikipedia language and article title,\n // return an array of corresponding Wikidata entities.\n itemsByTitle: function(lang, title, callback) {\n if (!title) {\n if (callback) callback('No title', {});\n return;\n }\n\n lang = lang || 'en';\n var url = apibase + utilQsString({\n action: 'wbgetentities',\n format: 'json',\n formatversion: 2,\n sites: lang.replace(/-/g, '_') + 'wiki',\n titles: title,\n languages: 'en', // shrink response by filtering to one language\n origin: '*'\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n }\n if (callback) callback(null, result.entities || {});\n })\n .catch(function(err) {\n if (callback) callback(err.message, {});\n });\n },\n\n\n languagesToQuery: function() {\n return localizer.localeCodes().map(function(code) {\n return code.toLowerCase();\n }).filter(function(code) {\n // HACK: en-us isn't a wikidata language. We should really be filtering by\n // the languages known to be supported by wikidata.\n return code !== 'en-us';\n });\n },\n\n\n entityByQID: function(qid, callback) {\n if (!qid) {\n callback('No qid', {});\n return;\n }\n if (_wikidataCache[qid]) {\n if (callback) callback(null, _wikidataCache[qid]);\n return;\n }\n\n var langs = this.languagesToQuery();\n var url = apibase + utilQsString({\n action: 'wbgetentities',\n format: 'json',\n formatversion: 2,\n ids: qid,\n props: 'labels|descriptions|claims|sitelinks',\n sitefilter: langs.map(function(d) { return d + 'wiki'; }).join('|'),\n languages: langs.join('|'),\n languagefallback: 1,\n origin: '*'\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n }\n if (callback) callback(null, result.entities[qid] || {});\n })\n .catch(function(err) {\n if (callback) callback(err.message, {});\n });\n },\n\n\n // Pass `params` object of the form:\n // {\n // qid: 'string' // brand wikidata (e.g. 'Q37158')\n // }\n //\n // Get an result object used to display tag documentation\n // {\n // title: 'string',\n // description: 'string',\n // editURL: 'string',\n // imageURL: 'string',\n // wiki: { title: 'string', text: 'string', url: 'string' }\n // }\n //\n getDocs: function(params, callback) {\n var langs = this.languagesToQuery();\n this.entityByQID(params.qid, function(err, entity) {\n if (err || !entity) {\n callback(err || 'No entity');\n return;\n }\n\n var i;\n var description;\n for (i in langs) {\n let code = langs[i];\n if (entity.descriptions[code] && entity.descriptions[code].language === code) {\n description = entity.descriptions[code];\n break;\n }\n }\n if (!description && Object.values(entity.descriptions).length) description = Object.values(entity.descriptions)[0];\n\n // prepare result\n var result = {\n title: entity.id,\n description: selection => selection.text(description ? description.value : ''),\n descriptionLocaleCode: description ? description.language : '',\n editURL: 'https://www.wikidata.org/wiki/' + entity.id\n };\n\n // add image\n if (entity.claims) {\n var imageroot = 'https://commons.wikimedia.org/w/index.php';\n var props = ['P154','P18']; // logo image, image\n var prop, image;\n for (i = 0; i < props.length; i++) {\n prop = entity.claims[props[i]];\n if (prop && Object.keys(prop).length > 0) {\n image = prop[Object.keys(prop)[0]].mainsnak.datavalue.value;\n if (image) {\n result.imageURL = imageroot + '?' + utilQsString({\n title: 'Special:Redirect/file/' + image,\n width: 400\n });\n break;\n }\n }\n }\n }\n\n if (entity.sitelinks) {\n var englishLocale = localizer.languageCode().toLowerCase() === 'en';\n\n // must be one of these that we requested..\n for (i = 0; i < langs.length; i++) { // check each, in order of preference\n var w = langs[i] + 'wiki';\n if (entity.sitelinks[w]) {\n var title = entity.sitelinks[w].title;\n var tKey = 'inspector.wiki_reference';\n if (!englishLocale && langs[i] === 'en') { // user's locale isn't English but\n tKey = 'inspector.wiki_en_reference'; // we are sending them to enwiki anyway..\n }\n\n result.wiki = {\n title: title,\n text: tKey,\n url: 'https://' + langs[i] + '.wikipedia.org/wiki/' + title.replace(/ /g, '_')\n };\n break;\n }\n }\n }\n\n callback(null, result);\n });\n }\n\n};\n", "import { json as d3_json } from 'd3-fetch';\n\nimport { utilQsString } from '../util';\n\n\nvar endpoint = 'https://en.wikipedia.org/w/api.php?';\n\nexport default {\n\n init: function() {},\n reset: function() {},\n\n\n search: function(lang, query, callback) {\n if (!query) {\n if (callback) callback('No Query', []);\n return;\n }\n\n lang = lang || 'en';\n var url = endpoint.replace('en', lang) +\n utilQsString({\n action: 'query',\n list: 'search',\n srlimit: '10',\n srinfo: 'suggestion',\n format: 'json',\n origin: '*',\n srsearch: query\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n } else if (!result || !result.query || !result.query.search) {\n throw new Error('No Results');\n }\n if (callback) {\n var titles = result.query.search.map(function(d) { return d.title; });\n callback(null, titles);\n }\n })\n .catch(function(err) {\n if (callback) callback(err, []);\n });\n },\n\n\n suggestions: function(lang, query, callback) {\n if (!query) {\n if (callback) callback('', []);\n return;\n }\n\n lang = lang || 'en';\n var url = endpoint.replace('en', lang) +\n utilQsString({\n action: 'opensearch',\n namespace: 0,\n suggest: '',\n format: 'json',\n origin: '*',\n search: query\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n } else if (!result || result.length < 2) {\n throw new Error('No Results');\n }\n if (callback) callback(null, result[1] || []);\n })\n .catch(function(err) {\n if (callback) callback(err.message, []);\n });\n },\n\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport { deepEqual } from 'fast-equals';\nimport Protobuf from 'pbf';\nimport RBush from 'rbush';\nimport { VectorTile } from '@mapbox/vector-tile';\n\nimport { utilRebind, utilTiler, utilQsString, utilStringQs, utilSetTransform } from '../util';\nimport { geoExtent } from '../geo';\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\nimport { localeDateString } from '../util/date';\n\nconst apiUrl = 'https://end.mapilio.com';\nconst imageBaseUrl = 'https://cdn.mapilio.com/im';\nconst baseTileUrl = 'https://geo.mapilio.com/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=mapilio:';\nconst pointLayer = 'map_points';\nconst lineLayer = 'map_roads_line';\nconst tileStyle = '&STYLE=&TILEMATRIX=EPSG:900913:{z}&TILEMATRIXSET=EPSG:900913&FORMAT=application/vnd.mapbox-vector-tile&TILECOL={x}&TILEROW={y}';\n\nconst minZoom = 14;\nconst dispatch = d3_dispatch('loadedImages', 'loadedLines');\nconst imgZoom = d3_zoom()\n .extent([[0, 0], [320, 240]])\n .translateExtent([[0, 0], [320, 240]])\n .scaleExtent([1, 15]);\nconst pannellumViewerCSS = 'pannellum/pannellum.css';\nconst pannellumViewerJS = 'pannellum/pannellum.js';\nconst resolution = 1080;\n\nlet _activeImage;\nlet _cache;\nlet _loadViewerPromise;\nlet _pannellumViewer;\nlet _sceneOptions = {\n showFullscreenCtrl: false,\n autoLoad: true,\n yaw: 0,\n minHfov: 10,\n maxHfov: 90,\n hfov: 60,\n};\nlet _currScene = 0;\n\n// Load all data for the specified type from Mapilio vector tiles\nfunction loadTiles(which, url, maxZoom, projection) {\n const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);\n const tiles = tiler.getTiles(projection);\n\n tiles.forEach(function(tile) {\n loadTile(which, url, tile);\n });\n}\n\n\n// Load all data for the specified type from one vector tile\nfunction loadTile(which, url, tile) {\n const cache = _cache.requests;\n const tileId = `${tile.id}-${which}`;\n if (cache.loaded[tileId] || cache.inflight[tileId]) return;\n const controller = new AbortController();\n cache.inflight[tileId] = controller;\n const requestUrl = url.replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n .replace('{z}', tile.xyz[2]);\n\n fetch(requestUrl, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (data.byteLength === 0) {\n throw new Error('No Data');\n }\n\n loadTileDataToCache(data, tile, which);\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n } else {\n dispatch.call('loadedLines');\n }\n })\n .catch(function (e) {\n if (e.message === 'No Data') {\n cache.loaded[tileId] = true;\n } else {\n console.error(e); // eslint-disable-line no-console\n }\n });\n}\n\n\n// Load the data from the vector tile into cache\nfunction loadTileDataToCache(data, tile) {\n const vectorTile = new VectorTile(new Protobuf(data));\n if (vectorTile.layers.hasOwnProperty(pointLayer)) {\n const features = [];\n const cache = _cache.images;\n const layer = vectorTile.layers[pointLayer];\n\n for (let i = 0; i < layer.length; i++) {\n const feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n const loc = feature.geometry.coordinates;\n\n let resolutionArr = feature.properties.resolution.split('x');\n let sourceWidth = Math.max(resolutionArr[0], resolutionArr[1]);\n let sourceHeight = Math.min(resolutionArr[0] ,resolutionArr[1]);\n let isPano = sourceWidth % sourceHeight === 0;\n\n const d = {\n service: 'photo',\n loc: loc,\n capture_time: feature.properties.capture_time,\n created_by_id: feature.properties.created_by_id,\n id: feature.properties.id,\n sequence_id: feature.properties.sequence_uuid,\n heading: feature.properties.heading,\n resolution: feature.properties.resolution,\n isPano: isPano\n };\n cache.forImageId[d.id] = d;\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty(lineLayer)) {\n const cache = _cache.sequences;\n const layer = vectorTile.layers[lineLayer];\n\n for (let i = 0; i < layer.length; i++) {\n const feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n if (cache.lineString[feature.properties.sequence_uuid]) {\n const cacheEntry = cache.lineString[feature.properties.sequence_uuid];\n if (cacheEntry.some(f => {\n // for some reason, mapilio sometimes returns a large amount of duplicate\n // sequence lines, causing very poor performance. this de-duplicates them,\n // see https://github.com/openstreetmap/iD/issues/10532\n const cachedCoords = f.geometry.coordinates;\n const featureCoords = feature.geometry.coordinates;\n return deepEqual(cachedCoords, featureCoords);\n })) continue;\n cacheEntry.push(feature);\n } else {\n cache.lineString[feature.properties.sequence_uuid] = [feature];\n }\n }\n }\n\n}\n\nfunction getImageData(imageId, sequenceId) {\n\n return fetch(apiUrl + `/api/sequence-detail?sequence_uuid=${sequenceId}`, {method: 'GET'})\n .then(function (response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n })\n .then(function (data) {\n let index = data.data.findIndex((feature) => feature.id === imageId);\n const {filename, uploaded_hash} = data.data[index];\n _sceneOptions.panorama = imageBaseUrl + '/' + uploaded_hash + '/' + filename + '/' + resolution;\n });\n}\n\nfunction getUserData(userId) {\n return fetch(apiUrl + `/api/search-user?options[parameters][id]=${userId}`, {method: 'GET'})\n .then(function (response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n })\n .then(function (data) {\n return data.data[0].username;\n });\n}\n\n\nexport default {\n // Initialize Mapilio\n init: function() {\n if (!_cache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n // Reset cache and state\n reset: function() {\n if (_cache) {\n Object.values(_cache.requests.inflight).forEach(function(request) { request.abort(); });\n }\n\n _cache = {\n images: { rtree: new RBush(), forImageId: {} },\n sequences: { rtree: new RBush(), lineString: {} },\n requests: { loaded: {}, inflight: {} }\n };\n },\n\n // Get visible images\n images: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _cache.images.rtree);\n },\n\n cachedImage: function(imageKey) {\n return _cache.images.forImageId[imageKey];\n },\n\n\n // Load images in the visible area\n loadImages: function(projection) {\n let url = baseTileUrl + pointLayer + tileStyle;\n loadTiles('images', url, 14, projection);\n },\n\n // Load line in the visible area\n loadLines: function(projection) {\n let url = baseTileUrl + lineLayer + tileStyle;\n loadTiles('line', url, 14, projection);\n },\n\n // Get visible sequences\n sequences: function(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const sequenceIds = {};\n let lineStrings = [];\n\n _cache.images.rtree.search(bbox)\n .forEach(function(d) {\n if (d.data.sequence_id) {\n sequenceIds[d.data.sequence_id] = true;\n }\n });\n\n Object.keys(sequenceIds).forEach(function(sequenceId) {\n if (_cache.sequences.lineString[sequenceId]) {\n lineStrings = lineStrings.concat(_cache.sequences.lineString[sequenceId]);\n }\n });\n\n return lineStrings;\n },\n\n // Set the currently visible image\n setActiveImage: function(image) {\n if (image) {\n _activeImage = {\n id: image.id,\n sequence_id: image.sequence_id\n };\n } else {\n _activeImage = null;\n }\n },\n\n\n // Update the currently highlighted sequence and selected bubble.\n setStyles: function(context, hovered) {\n const hoveredImageId = hovered && hovered.id;\n const hoveredSequenceId = hovered && hovered.sequence_id;\n const selectedSequenceId = _activeImage && _activeImage.sequence_id;\n const selectedImageId = _activeImage && _activeImage.id;\n\n const markers = context.container().selectAll('.layer-mapilio .viewfield-group');\n const sequences = context.container().selectAll('.layer-mapilio .sequence');\n\n markers.classed('highlighted', function(d) { return d.id === hoveredImageId; })\n .classed('hovered', function(d) { return d.id === hoveredImageId; })\n .classed('currentView', function(d) { return d.id === selectedImageId; });\n\n sequences.classed('highlighted', function(d) { return d.properties.sequence_uuid === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.sequence_uuid === selectedSequenceId; });\n\n return this;\n },\n\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'mapilio/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n initViewer: function () {\n if (!window.pannellum) return;\n if (_pannellumViewer) return;\n\n _currScene += 1;\n const sceneID = _currScene.toString();\n const options = {\n 'default': { firstScene: sceneID },\n scenes: {}\n };\n options.scenes[sceneID] = _sceneOptions;\n\n _pannellumViewer = window.pannellum.viewer('ideditor-viewer-mapilio-pnlm', options);\n },\n\n selectImage: function (context, id) {\n\n let that = this;\n\n let d = this.cachedImage(id);\n\n this.setActiveImage(d);\n\n this.updateUrlImage(d.id);\n\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null);\n\n if (!d) return this;\n\n let wrap = context.container().select('.photoviewer .mapilio-wrapper');\n let attribution = wrap.selectAll('.photo-attribution').text('\\u00A0');\n\n getUserData(d.created_by_id).then((username) => {\n if (username) {\n attribution\n .append('span')\n .attr('class', 'captured_by')\n .text('@' + username);\n attribution\n .append('span')\n .text('|');\n }\n }).finally(() => {\n if (d.capture_time) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeDateString(d.capture_time));\n attribution\n .append('span')\n .text('|');\n }\n attribution\n .append('a')\n .attr('class', 'image-link')\n .attr('target', '_blank')\n .attr('href', `https://mapilio.com/app?lat=${d.loc[1]}&lng=${d.loc[0]}&zoom=17&pId=${d.id}`)\n .text('mapilio.com');\n });\n\n wrap\n .transition()\n .duration(100)\n .call(imgZoom.transform, d3_zoomIdentity);\n\n wrap\n .selectAll('img')\n .remove();\n\n wrap\n .selectAll('button.back')\n .classed('hide', !_cache.images.forImageId.hasOwnProperty(+id - 1));\n wrap\n .selectAll('button.forward')\n .classed('hide', !_cache.images.forImageId.hasOwnProperty(+id + 1));\n\n\n getImageData(d.id,d.sequence_id).then(function () {\n\n if (d.isPano) {\n if (!_pannellumViewer) {\n that.initViewer();\n } else {\n // make a new scene\n _currScene += 1;\n let sceneID = _currScene.toString();\n _pannellumViewer\n .addScene(sceneID, _sceneOptions)\n .loadScene(sceneID);\n\n // remove previous scene\n if (_currScene > 2) {\n sceneID = (_currScene - 1).toString();\n _pannellumViewer\n .removeScene(sceneID);\n }\n }\n } else {\n // make non-panoramic photo viewer\n that.initOnlyPhoto(context);\n }\n });\n\n return this;\n },\n\n initOnlyPhoto: function (context) {\n\n if (_pannellumViewer) {\n _pannellumViewer.destroy();\n _pannellumViewer = null;\n }\n\n let wrap = context.container().select('#ideditor-viewer-mapilio-simple');\n\n let imgWrap = wrap.select('img');\n\n if (!imgWrap.empty()) {\n imgWrap.attr('src',_sceneOptions.panorama);\n } else {\n wrap.append('img')\n .attr('src',_sceneOptions.panorama);\n }\n\n },\n\n ensureViewerLoaded: function(context) {\n\n let that = this;\n\n let imgWrap = context.container().select('#ideditor-viewer-mapilio-simple > img');\n\n if (!imgWrap.empty()) {\n imgWrap.remove();\n }\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n let wrap = context.container().select('.photoviewer').selectAll('.mapilio-wrapper')\n .data([0]);\n\n let wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper mapilio-wrapper')\n .classed('hide', true)\n .on('dblclick.zoom', null);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n const controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls-mapilio');\n\n controlsEnter\n .append('button')\n .classed('back', true)\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .classed('forward', true)\n .on('click.forward', step(1))\n .text('\u25BA');\n\n wrapEnter\n .append('div')\n .attr('id', 'ideditor-viewer-mapilio-pnlm');\n\n wrapEnter\n .append('div')\n .attr('id', 'ideditor-viewer-mapilio-simple-wrap')\n .call(imgZoom.on('zoom', zoomPan))\n .append('div')\n .attr('id', 'ideditor-viewer-mapilio-simple');\n\n\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.mapilio', () => {\n if (_pannellumViewer) {\n _pannellumViewer.resize();\n }\n });\n\n _loadViewerPromise = new Promise((resolve, reject) => {\n let loadedCount = 0;\n function loaded() {\n loadedCount += 1;\n\n // wait until both files are loaded\n if (loadedCount === 2) resolve();\n }\n\n const head = d3_select('head');\n\n // load pannellum-viewercss\n head.selectAll('#ideditor-mapilio-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-mapilio-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(pannellumViewerCSS))\n .on('load.serviceMapilio', loaded)\n .on('error.serviceMapilio', function() {\n reject();\n });\n\n // load pannellum-viewerjs\n head.selectAll('#ideditor-mapilio-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-mapilio-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(pannellumViewerJS))\n .on('load.serviceMapilio', loaded)\n .on('error.serviceMapilio', function() {\n reject();\n });\n })\n .catch(function() {\n _loadViewerPromise = null;\n });\n\n function step(stepBy) {\n return function () {\n if (!_activeImage) return;\n const imageId = _activeImage.id;\n\n const nextIndex = imageId + stepBy;\n if (!nextIndex) return;\n\n const nextImage = _cache.images.forImageId[nextIndex];\n\n context.map().centerEase(nextImage.loc);\n\n that.selectImage(context, nextImage.id);\n };\n }\n\n function zoomPan(d3_event) {\n var t = d3_event.transform;\n context.container().select('.photoviewer #ideditor-viewer-mapilio-simple')\n .call(utilSetTransform, t.x, t.y, t.k);\n }\n\n return _loadViewerPromise;\n },\n\n showViewer:function (context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.mapilio-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.mapilio-wrapper')\n .classed('hide', false);\n }\n\n return this;\n },\n\n /**\n * hideViewer()\n */\n hideViewer: function (context) {\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n this.updateUrlImage(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n this.setActiveImage();\n return this.setStyles(context, null);\n },\n\n // Return the current cache\n cache: function() {\n return _cache;\n }\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport Protobuf from 'pbf';\nimport RBush from 'rbush';\nimport { VectorTile } from '@mapbox/vector-tile';\nimport { utilRebind, utilTiler, utilQsString, utilStringQs, utilUniqueDomId } from '../util';\nimport { geoExtent } from '../geo';\nimport { t } from '../core/localizer';\nimport { pannellumPhotoFrame } from './pannellum_photo';\nimport { planePhotoFrame } from './plane_photo';\nimport { services } from './';\nimport { partitionViewport } from '../util/partition';\nimport { localeDateString } from '../util/date';\n\n\nconst apiUrl = 'https://api.panoramax.xyz/';\nconst tileUrl = apiUrl + 'api/map/{z}/{x}/{y}.mvt';\nconst imageDataUrl = apiUrl + 'api/collections/{collectionId}/items/{itemId}';\nconst sequenceDataUrl = apiUrl + 'api/collections/{collectionId}/items?limit=1000';\nconst userIdUrl = apiUrl + 'api/users/search?q={username}';\nconst usernameURL = apiUrl + 'api/users/{userId}';\nconst viewerUrl = apiUrl;\n\nconst highDefinition = 'hd';\nconst standardDefinition = 'sd';\n\nconst pictureLayer = 'pictures';\nconst sequenceLayer = 'sequences';\n\nconst minZoom = 10;\nconst imageMinZoom = 15;\nconst lineMinZoom = 10;\nconst dispatch = d3_dispatch('loadedImages', 'loadedLines', 'viewerChanged');\n\nlet _cache;\nlet _loadViewerPromise;\nlet _definition = standardDefinition;\nlet _isHD = false;\n\nlet _planeFrame;\nlet _pannellumFrame;\nlet _currentFrame;\n\nlet _currentScene = {\n currentImage : null,\n nextImage : null,\n prevImage : null\n};\n\nlet _activeImage;\nlet _isViewerOpen = false;\n\n\n/**\n * Return no more than `limit` results per partition.\n * @param {number} limit Number of maximum objects to return\n * @param {*} projection Current projection\n * @param {*} rtree The cache\n * @returns Data found\n */\nfunction searchLimited(limit, projection, rtree) {\n limit = limit || 5;\n\n return partitionViewport(projection)\n .reduce(function(result, extent) {\n let found = rtree.search(extent.bbox());\n const spacing = Math.max(1, Math.floor(found.length / limit));\n found = found\n .filter((d, idx) => idx % spacing === 0 ||\n d.data.id === _activeImage?.id)\n .sort((a, b) => {\n if (a.data.id === _activeImage?.id) return -1;\n if (b.data.id === _activeImage?.id) return 1;\n return 0;\n })\n .slice(0, limit)\n .map(d => d.data);\n\n return (found.length ? result.concat(found) : result);\n }, []);\n}\n\n/**\n * Load all data for the specified type from Panoramax vector tiles\n * @param {string} which Either 'images' or 'lines'\n * @param {string} url Tile endpoint\n * @param {number} maxZoom Maximum zoom out\n * @param {*} projection Current projection\n * @param {number} zoom current zoom\n */\nfunction loadTiles(which, url, maxZoom, projection, zoom) {\n const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);\n const tiles = tiler.getTiles(projection);\n\n tiles.forEach(function(tile) {\n loadTile(which, url, tile, zoom);\n });\n}\n\n/**\n * Load all data for the specified type from one vector tile\n * @param {*} which Either 'images' or 'lines'\n * @param {*} url Tile endpoint\n * @param {*} tile Current tile\n * @param {*} zoom Current zoom\n */\nfunction loadTile(which, url, tile, zoom) {\n const cache = _cache.requests;\n const tileId = `${tile.id}-${which}`;\n if (cache.loaded[tileId] || cache.inflight[tileId]) return;\n const controller = new AbortController();\n cache.inflight[tileId] = controller;\n const requestUrl = url.replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n .replace('{z}', tile.xyz[2]);\n\n fetch(requestUrl, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (data.byteLength === 0) {\n throw new Error('No Data');\n }\n\n loadTileDataToCache(data, tile, zoom);\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n } else {\n dispatch.call('loadedLines');\n }\n })\n .catch(function (e) {\n if (e.message === 'No Data') {\n cache.loaded[tileId] = true;\n } else {\n console.error(e); // eslint-disable-line no-console\n }\n });\n}\n\n/**\n * Fetches all data for the specified tile and adds them to cache\n * @param {*} data Tile data\n * @param {*} tile Current tile\n * @param {*} zoom Current zoom\n */\nfunction loadTileDataToCache(data, tile, zoom) {\n const vectorTile = new VectorTile(new Protobuf(data));\n\n let features,\n cache,\n layer,\n i,\n feature,\n loc,\n d;\n\n if (vectorTile.layers.hasOwnProperty(pictureLayer)) {\n features = [];\n cache = _cache.images;\n layer = vectorTile.layers[pictureLayer];\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n\n d = {\n service: 'photo',\n loc: loc,\n capture_time: feature.properties.ts,\n capture_time_parsed: new Date(feature.properties.ts),\n id: feature.properties.id,\n account_id: feature.properties.account_id,\n sequence_id: feature.properties.first_sequence,\n heading: parseInt(feature.properties.heading, 10),\n image_path: '',\n isPano: feature.properties.type === 'equirectangular',\n model: feature.properties.model,\n };\n cache.forImageId[d.id] = d;\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty(sequenceLayer)) {\n\n cache = _cache.sequences;\n\n if (zoom >= lineMinZoom && zoom < imageMinZoom) cache = _cache.mockSequences;\n\n layer = vectorTile.layers[sequenceLayer];\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n if (cache.lineString[feature.properties.id]) {\n cache.lineString[feature.properties.id].push(feature);\n } else {\n cache.lineString[feature.properties.id] = [feature];\n }\n }\n }\n}\n\n/**\n * Fetches the username from Panoramax\n * @param {string} userId\n * @returns the username\n */\nasync function getUsername(userId) {\n const cache = _cache.users;\n if (cache[userId]) return cache[userId].name;\n\n const requestUrl = usernameURL.replace('{userId}', userId);\n\n const response = await fetch(requestUrl, { method: 'GET' });\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n const data = await response.json();\n cache[userId] = data;\n\n return data.name;\n}\n\nexport default {\n init: function() {\n if (!_cache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset: function() {\n if (_cache) {\n Object.values(_cache.requests.inflight).forEach(function(request) { request.abort(); });\n }\n\n _cache = {\n images: { rtree: new RBush(), forImageId: {} },\n sequences: { rtree: new RBush(), lineString: {}, items: {} },\n users: {},\n mockSequences: { rtree: new RBush(), lineString: {} },\n requests: { loaded: {}, inflight: {} }\n };\n },\n\n /**\n * Get visible images from cache\n * @param {*} projection Current Projection\n * @returns images data for the current projection\n */\n images: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _cache.images.rtree);\n },\n\n /**\n * Get a specific image from cache\n * @param {*} imageKey the image id\n * @returns\n */\n cachedImage: function(imageKey) {\n return _cache.images.forImageId[imageKey];\n },\n\n /**\n * Fetches images data for the visible area\n * @param {*} projection Current Projection\n */\n loadImages: function(projection) {\n loadTiles('images', tileUrl, imageMinZoom, projection);\n },\n\n /**\n * Fetches sequences data for the visible area\n * @param {*} projection Current Projection\n */\n loadLines: function(projection, zoom) {\n loadTiles('line', tileUrl, lineMinZoom, projection, zoom);\n },\n\n /**\n * Fetches all possible userIDs from Panoramax\n * @param {string} usernames one or multiple usernames\n * @returns userIDs\n */\n getUserIds: async function(usernames) {\n const requestUrls = usernames.map(username =>\n userIdUrl.replace('{username}', username));\n\n const responses = await Promise.all(requestUrls.map(requestUrl =>\n fetch(requestUrl, { method: 'GET' })));\n if (responses.some(response => !response.ok)) {\n const response = responses.find(response => !response.ok);\n throw new Error(response.status + ' ' + response.statusText);\n }\n const data = await Promise.all(responses.map(response => response.json()));\n // in panoramax, a username can have multiple ids, when the same name is\n // used on different servers\n return data.flatMap((d, i) => d.features.filter(f => f.name === usernames[i]).map(f => f.id));\n },\n\n /**\n * Get visible sequences from cache\n * @param {*} projection Current Projection\n * @param {number} zoom Current zoom (if zoom < `lineMinZoom` less accurate lines will be drawn)\n * @returns sequences data for the current projection\n */\n sequences: function(projection, zoom) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const sequenceIds = {};\n let lineStrings = [];\n\n if (zoom >= imageMinZoom){\n _cache.images.rtree.search(bbox).forEach(function(d) {\n if (d.data.sequence_id) {\n sequenceIds[d.data.sequence_id] = true;\n }\n });\n Object.keys(sequenceIds).forEach(function(sequenceId) {\n if (_cache.sequences.lineString[sequenceId]) {\n lineStrings = lineStrings.concat(_cache.sequences.lineString[sequenceId]);\n }\n });\n return lineStrings;\n }\n if (zoom >= lineMinZoom){\n Object.keys(_cache.mockSequences.lineString).forEach(function(sequenceId) {\n lineStrings = lineStrings.concat(_cache.mockSequences.lineString[sequenceId]);\n });\n }\n return lineStrings;\n },\n\n /**\n * Updates the data for the currently visible image\n * @param {*} image Image data\n */\n setActiveImage: function(image) {\n if (image && image.id && image.sequence_id) {\n _activeImage = {\n id: image.id,\n sequence_id: image.sequence_id,\n loc: image.loc\n };\n } else {\n _activeImage = null;\n }\n },\n\n getActiveImage: function(){\n return _activeImage;\n },\n\n /**\n * Update the currently highlighted sequence and selected bubble\n * @param {*} context Current HTML context\n * @param {*} [hovered] The hovered bubble image\n */\n setStyles: function(context, hovered) {\n const hoveredImageId = hovered && hovered.id;\n const hoveredSequenceId = hovered && hovered.sequence_id;\n const selectedSequenceId = _activeImage && _activeImage.sequence_id;\n const selectedImageId = _activeImage && _activeImage.id;\n\n const markers = context.container().selectAll('.layer-panoramax .viewfield-group');\n const sequences = context.container().selectAll('.layer-panoramax .sequence');\n\n markers\n .classed('highlighted', function(d) { return d.sequence_id === selectedSequenceId || d.id === hoveredImageId; })\n .classed('hovered', function(d) { return d.id === hoveredImageId; })\n .classed('currentView', function(d) { return d.id === selectedImageId; });\n\n sequences\n .classed('highlighted', function(d) { return d.properties.id === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.id === selectedSequenceId; });\n\n // update viewfields if needed\n context.container().selectAll('.layer-panoramax .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n let d = this.parentNode.__data__;\n if (d.isPano && d.id !== selectedImageId) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n return this;\n },\n\n // Get viewer status\n isViewerOpen: function() {\n return _isViewerOpen;\n },\n\n /**\n * Updates the URL to save the current shown image\n * @param {*} imageKey\n */\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'panoramax/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n /**\n * Loads the selected image in the frame\n * @param {*} context Current HTML context\n * @param {*} id of the selected image\n * @returns\n */\n selectImage: function (context, id) {\n let that = this;\n\n let d = that.cachedImage(id);\n that.setActiveImage(d);\n that.updateUrlImage(d.id);\n\n const viewerLink = `${viewerUrl}#pic=${d.id}&focus=pic`;\n\n let viewer = context.container()\n .select('.photoviewer');\n\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null);\n\n if (!d) return this;\n\n let wrap = context.container()\n .select('.photoviewer .panoramax-wrapper');\n\n let attribution = wrap.selectAll('.photo-attribution').text('');\n\n let line1 = attribution\n .append('div')\n .attr('class', 'attribution-row');\n\n const hdDomId = utilUniqueDomId('panoramax-hd');\n\n let label = line1\n .append('label')\n .attr('for', hdDomId)\n .attr('class', 'panoramax-hd');\n\n label\n .append('input')\n .attr('type', 'checkbox')\n .attr('id', hdDomId)\n .property('checked', _isHD)\n .on('click', (d3_event) => {\n d3_event.stopPropagation();\n _isHD = !_isHD;\n _definition = _isHD ? highDefinition : standardDefinition;\n that.selectImage(context, d.id)\n .showViewer(context);\n });\n\n label\n .append('span')\n .call(t.append('panoramax.hd'));\n\n if (d.capture_time) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeDateString(d.capture_time));\n\n attribution\n .append('span')\n .text('|');\n }\n\n attribution\n .append('a')\n .attr('class', 'report-photo')\n .attr('href', 'mailto:signalement.ign@panoramax.fr')\n .call(t.append('panoramax.report'));\n\n attribution\n .append('span')\n .text('|');\n\n attribution\n .append('a')\n .attr('class', 'image-link')\n .attr('target', '_blank')\n .attr('href', viewerLink)\n .text('panoramax.xyz');\n\n this.getImageData(d.sequence_id, d.id).then(function(data) {\n _currentScene = {\n currentImage: null,\n nextImage: null,\n prevImage: null\n };\n _currentScene.currentImage = data.assets[_definition];\n const nextIndex = data.links.findIndex(x => x.rel === 'next');\n const prevIndex = data.links.findIndex(x => x.rel === 'prev');\n\n if (nextIndex !== -1){\n _currentScene.nextImage = data.links[nextIndex];\n }\n if (prevIndex !== -1){\n _currentScene.prevImage = data.links[prevIndex];\n }\n\n d.image_path = _currentScene.currentImage.href;\n\n wrap\n .selectAll('button.back')\n .classed('hide', _currentScene.prevImage === null);\n wrap\n .selectAll('button.forward')\n .classed('hide', _currentScene.nextImage === null);\n\n _currentFrame = d.isPano ? _pannellumFrame : _planeFrame;\n\n _currentFrame\n .showPhotoFrame(wrap)\n .selectPhoto(d, true);\n });\n\n if (d.account_id) {\n attribution\n .append('span')\n .text('|');\n\n let line2 = attribution\n .append('span')\n .attr('class', 'attribution-row');\n\n getUsername(d.account_id).then(function(username){\n line2\n .append('span')\n .attr('class', 'captured_by')\n .text('@' + username);\n });\n }\n\n return this;\n },\n\n photoFrame: function() {\n return _currentFrame;\n },\n\n /**\n * Fetches the data for a specific image\n * @param {*} collectionId\n * @param {*} imageId\n * @returns The fetched image data\n */\n getImageData: async function(collectionId, imageId) {\n const cache = _cache.sequences.items;\n if (cache[collectionId]) {\n const cached = cache[collectionId]\n .find(d => d.id === imageId);\n if (cached) return cached;\n } else {\n // prime the cache with data from sequence\n const response = await fetch(sequenceDataUrl\n .replace('{collectionId}', collectionId),\n { method: 'GET' });\n\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n const data = (await response.json()).features;\n cache[collectionId] = data;\n }\n\n const result = cache[collectionId]\n .find(d => d.id === imageId);\n if (result) return result;\n\n // not found in sequence: retry to load single item data\n // ideally, we'd use the `withPicture` parameter, but it is buggy:\n // https://gitlab.com/panoramax/server/api/-/issues/268\n const itemResponse = await fetch(imageDataUrl\n .replace('{collectionId}', collectionId)\n .replace('{itemId}', imageId),\n { method: 'GET' });\n\n if (!itemResponse.ok) {\n throw new Error(itemResponse.status + ' ' + itemResponse.statusText);\n }\n const itemData = await itemResponse.json();\n cache[collectionId].push(itemData);\n return itemData;\n },\n\n ensureViewerLoaded: function(context) {\n\n let that = this;\n\n let imgWrap = context.container()\n .select('#ideditor-viewer-panoramax-simple > img');\n\n if (!imgWrap.empty()) {\n imgWrap.remove();\n }\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n let wrap = context.container()\n .select('.photoviewer')\n .selectAll('.panoramax-wrapper')\n .data([0]);\n\n let wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper panoramax-wrapper')\n .classed('hide', true)\n .on('dblclick.zoom', null);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n const controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls-panoramax');\n\n controlsEnter\n .append('button')\n .classed('back', true)\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .classed('forward', true)\n .on('click.forward', step(1))\n .text('\u25BA');\n\n // Register viewer resize handler\n _loadViewerPromise = Promise.all([\n pannellumPhotoFrame(context, wrapEnter),\n planePhotoFrame(context, wrapEnter)\n ]).then(([pannellumPhotoFrame, planePhotoFrame]) => {\n _pannellumFrame = pannellumPhotoFrame;\n _pannellumFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n _planeFrame = planePhotoFrame;\n _planeFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n });\n\n /**\n * Loads the next image in the sequence\n * @param {number} stepBy '-1' if backwards or '1' if forward\n * @returns\n */\n function step(stepBy) {\n return function () {\n if (!_currentScene.currentImage) return;\n\n let nextId;\n if (stepBy === 1) nextId = _currentScene.nextImage.id;\n else nextId = _currentScene.prevImage.id;\n\n if (!nextId) return;\n\n const nextImage = _cache.images.forImageId[nextId];\n\n if (nextImage){\n context.map().centerEase(nextImage.loc);\n that.selectImage(context, nextImage.id);\n }\n };\n }\n\n return _loadViewerPromise;\n },\n\n /**\n * Shows the current viewer if hidden\n * @param {*} context\n */\n showViewer: function (context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.panoramax-wrapper.hide').size();\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.panoramax-wrapper')\n .classed('hide', false);\n }\n\n _isViewerOpen = true;\n return this;\n },\n\n /**\n * Hides the current viewer if shown, resets the active image and sequence\n * @param {*} context\n */\n hideViewer: function (context) {\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n this.updateUrlImage(null);\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n this.setActiveImage(null);\n _isViewerOpen = false;\n\n return this.setStyles(context, null);\n },\n\n cache: function() {\n return _cache;\n }\n};\n", "import serviceOsmose from './osmose';\nimport serviceMapillary from './mapillary';\nimport serviceMapRules from './maprules';\nimport serviceNominatim from './nominatim';\nimport serviceNsi from './nsi';\nimport serviceKartaview from './kartaview';\nimport serviceVegbilder from './vegbilder';\nimport serviceOsm from './osm';\nimport serviceOsmWikibase from './osm_wikibase';\nimport serviceStreetside from './streetside';\nimport serviceTaginfo from './taginfo';\nimport serviceVectorTile from './vector_tile';\nimport serviceWikidata from './wikidata';\nimport serviceWikipedia from './wikipedia';\nimport serviceMapilio from './mapilio';\nimport servicePanoramax from './panoramax';\n\n\nexport let services = {\n geocoder: serviceNominatim,\n osmose: serviceOsmose,\n mapillary: serviceMapillary,\n nsi: serviceNsi,\n kartaview: serviceKartaview,\n vegbilder: serviceVegbilder,\n osm: serviceOsm,\n osmWikibase: serviceOsmWikibase,\n maprules: serviceMapRules,\n streetside: serviceStreetside,\n taginfo: serviceTaginfo,\n vectorTile: serviceVectorTile,\n wikidata: serviceWikidata,\n wikipedia: serviceWikipedia,\n mapilio: serviceMapilio,\n panoramax: servicePanoramax\n};\n\nexport {\n serviceOsmose,\n serviceMapillary,\n serviceMapRules,\n serviceNominatim,\n serviceNsi,\n serviceKartaview,\n serviceVegbilder,\n serviceOsm,\n serviceOsmWikibase,\n serviceStreetside,\n serviceTaginfo,\n serviceVectorTile,\n serviceWikidata,\n serviceWikipedia,\n serviceMapilio,\n servicePanoramax\n};\n", "import {\n geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon,\n geoSphericalDistance, geoVecInterp, geoHasSelfIntersections,\n geoSphericalClosestNode, geoAngle\n} from '../geo';\n\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionMergeNodes } from '../actions/merge_nodes';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilDatesOverlap } from '../util';\nimport { osmRoutableHighwayTagValues } from '../osm/tags';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\n\n/**\n * Look for roads that can be connected to other roads with a short extension\n */\nexport function validationAlmostJunction(context) {\n const type = 'almost_junction';\n const EXTEND_TH_METERS = 5;\n const WELD_TH_METERS = 0.75;\n // Comes from considering bounding case of parallel ways\n const CLOSE_NODE_TH = EXTEND_TH_METERS - WELD_TH_METERS;\n // Comes from considering bounding case of perpendicular ways\n const SIG_ANGLE_TH = Math.atan(WELD_TH_METERS / EXTEND_TH_METERS);\n\n function isHighway(entity) {\n return entity.type === 'way'\n && osmRoutableHighwayTagValues[entity.tags.highway];\n }\n\n function isTaggedAsNotContinuing(node) {\n return node.tags.noexit === 'yes'\n || node.tags.amenity === 'parking_entrance'\n || (node.tags.entrance && node.tags.entrance !== 'no');\n }\n\n\n const validation = function checkAlmostJunction(entity, graph) {\n if (!isHighway(entity)) return [];\n if (entity.isDegenerate()) return [];\n\n const tree = context.history().tree();\n const extendableNodeInfos = findConnectableEndNodesByExtension(entity);\n\n let issues = [];\n\n extendableNodeInfos.forEach(extendableNodeInfo => {\n issues.push(new validationIssue({\n type,\n subtype: 'highway-highway',\n severity: 'warning',\n message: function(context) {\n const entity1 = context.hasEntity(this.entityIds[0]);\n if (this.entityIds[0] === this.entityIds[2]) {\n return entity1 ? t.append('issues.almost_junction.self.message', {\n feature: utilDisplayLabel(entity1, context.graph())\n }) : '';\n } else {\n const entity2 = context.hasEntity(this.entityIds[2]);\n return (entity1 && entity2) ? t.append('issues.almost_junction.message', {\n feature: utilDisplayLabel(entity1, context.graph()),\n feature2: utilDisplayLabel(entity2, context.graph())\n }) : '';\n }\n },\n reference: showReference,\n entityIds: [\n entity.id,\n extendableNodeInfo.node.id,\n extendableNodeInfo.wid,\n ],\n loc: extendableNodeInfo.node.loc,\n hash: JSON.stringify(extendableNodeInfo.node.loc),\n data: {\n midId: extendableNodeInfo.mid.id,\n edge: extendableNodeInfo.edge,\n cross_loc: extendableNodeInfo.cross_loc\n },\n dynamicFixes: makeFixes\n }));\n });\n\n return issues;\n\n function makeFixes(context) {\n let fixes = [new validationIssueFix({\n icon: 'iD-icon-abutment',\n title: t.append('issues.fix.connect_features.title'),\n onClick: function(context) {\n const annotation = t('issues.fix.connect_almost_junction.annotation');\n const [, endNodeId, crossWayId] = this.issue.entityIds;\n const midNode = context.entity(this.issue.data.midId);\n const endNode = context.entity(endNodeId);\n const crossWay = context.entity(crossWayId);\n\n // When endpoints are close, just join if resulting small change in angle (#7201)\n const nearEndNodes = findNearbyEndNodes(endNode, crossWay);\n if (nearEndNodes.length > 0) {\n const collinear = findSmallJoinAngle(midNode, endNode, nearEndNodes);\n if (collinear) {\n context.perform(\n actionMergeNodes([collinear.id, endNode.id], collinear.loc),\n annotation\n );\n return;\n }\n }\n\n const targetEdge = this.issue.data.edge;\n const crossLoc = this.issue.data.cross_loc;\n const edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])];\n const closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc);\n\n // already a point nearby, just connect to that\n if (closestNodeInfo.distance < WELD_TH_METERS) {\n context.perform(\n actionMergeNodes([closestNodeInfo.node.id, endNode.id], closestNodeInfo.node.loc),\n annotation\n );\n // else add the end node to the edge way\n } else {\n context.perform(\n actionAddMidpoint({loc: crossLoc, edge: targetEdge}, endNode),\n annotation\n );\n }\n }\n })];\n\n const node = context.hasEntity(this.entityIds[1]);\n if (node && !node.hasInterestingTags()) {\n // node has no descriptive tags, suggest noexit fix\n fixes.push(new validationIssueFix({\n icon: 'maki-barrier',\n title: t.append('issues.fix.tag_as_disconnected.title'),\n onClick: function(context) {\n const nodeID = this.issue.entityIds[1];\n const tags = Object.assign({}, context.entity(nodeID).tags);\n tags.noexit = 'yes';\n context.perform(\n actionChangeTags(nodeID, tags),\n t('issues.fix.tag_as_disconnected.annotation')\n );\n }\n }));\n }\n\n // changing the date range is always an option\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-change-date-range',\n title: t.append('issues.fix.change_date_range.title')\n }));\n\n return fixes;\n }\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.almost_junction.highway-highway.reference'));\n }\n\n function isExtendableCandidate(node, way) {\n // can not accurately test vertices on tiles not downloaded from osm - #5938\n const osm = services.osm;\n if (osm && !osm.isDataLoaded(node.loc)) {\n return false;\n }\n if (isTaggedAsNotContinuing(node) || graph.parentWays(node).length !== 1) {\n return false;\n }\n\n let occurrences = 0;\n for (const index in way.nodes) {\n if (way.nodes[index] === node.id) {\n occurrences += 1;\n if (occurrences > 1) {\n return false;\n }\n }\n }\n return true;\n }\n\n function findConnectableEndNodesByExtension(way) {\n let results = [];\n if (way.isClosed()) return results;\n\n let testNodes;\n const indices = [0, way.nodes.length - 1];\n indices.forEach(nodeIndex => {\n const nodeID = way.nodes[nodeIndex];\n const node = graph.entity(nodeID);\n\n if (!isExtendableCandidate(node, way)) return;\n\n const connectionInfo = canConnectByExtend(way, nodeIndex);\n if (!connectionInfo) return;\n\n testNodes = graph.childNodes(way).slice(); // shallow copy\n testNodes[nodeIndex] = testNodes[nodeIndex].move(connectionInfo.cross_loc);\n\n // don't flag issue if connecting the ways would cause self-intersection\n if (geoHasSelfIntersections(testNodes, nodeID)) return;\n\n results.push(connectionInfo);\n });\n\n return results;\n }\n\n function findNearbyEndNodes(node, way) {\n return [\n way.nodes[0],\n way.nodes[way.nodes.length - 1]\n ].map(d => graph.entity(d))\n .filter(d => {\n // Node cannot be near to itself, but other endnode of same way could be\n return d.id !== node.id\n && geoSphericalDistance(node.loc, d.loc) <= CLOSE_NODE_TH;\n });\n }\n\n function findSmallJoinAngle(midNode, tipNode, endNodes) {\n // Both nodes could be close, so want to join whichever is closest to collinear\n let joinTo;\n let minAngle = Infinity;\n\n // Checks midNode -> tipNode -> endNode for collinearity\n endNodes.forEach(endNode => {\n const a1 = geoAngle(midNode, tipNode, context.projection) + Math.PI;\n const a2 = geoAngle(midNode, endNode, context.projection) + Math.PI;\n const diff = Math.max(a1, a2) - Math.min(a1, a2);\n\n if (diff < minAngle) {\n joinTo = endNode;\n minAngle = diff;\n }\n });\n\n /* Threshold set by considering right angle triangle\n based on node joining threshold and extension distance */\n if (minAngle <= SIG_ANGLE_TH) return joinTo;\n\n return null;\n }\n\n function hasTag(tags, key) {\n return tags[key] !== undefined && tags[key] !== 'no';\n }\n\n function canConnectWays(way, way2) {\n\n // allow self-connections\n if (way.id === way2.id) return true;\n\n // if one is bridge or tunnel, both must be bridge or tunnel\n if ((hasTag(way.tags, 'bridge') || hasTag(way2.tags, 'bridge')) &&\n !(hasTag(way.tags, 'bridge') && hasTag(way2.tags, 'bridge'))) return false;\n if ((hasTag(way.tags, 'tunnel') || hasTag(way2.tags, 'tunnel')) &&\n !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false;\n\n // must have equivalent layers and levels\n const layer1 = way.tags.layer || '0',\n layer2 = way2.tags.layer || '0';\n if (layer1 !== layer2) return false;\n\n const level1 = way.tags.level || '0',\n level2 = way2.tags.level || '0';\n if (level1 !== level2) return false;\n\n // must have overlapping date ranges\n if ((way.tags.start_date || way.tags.end_date) && (way2.tags.start_date || way2.tags.end_date)) {\n if (!utilDatesOverlap(way.tags, way2.tags)) return false;\n }\n\n return true;\n }\n\n function canConnectByExtend(way, endNodeIdx) {\n const tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point\n const midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge\n const tipNode = graph.entity(tipNid);\n const midNode = graph.entity(midNid);\n const lon = tipNode.loc[0];\n const lat = tipNode.loc[1];\n const lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2;\n const lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2;\n const queryExtent = geoExtent([\n [lon - lon_range, lat - lat_range],\n [lon + lon_range, lat + lat_range]\n ]);\n\n // first, extend the edge of [midNode -> tipNode] by EXTEND_TH_METERS and find the \"extended tip\" location\n const edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc);\n const t = EXTEND_TH_METERS / edgeLen + 1.0;\n const extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t);\n\n // then, check if the extension part [tipNode.loc -> extTipLoc] intersects any other ways\n const segmentInfos = tree.waySegments(queryExtent, graph);\n for (let i = 0; i < segmentInfos.length; i++) {\n let segmentInfo = segmentInfos[i];\n\n let way2 = graph.entity(segmentInfo.wayId);\n\n if (!isHighway(way2)) continue;\n\n if (!canConnectWays(way, way2)) continue;\n\n let nAid = segmentInfo.nodes[0],\n nBid = segmentInfo.nodes[1];\n\n if (nAid === tipNid || nBid === tipNid) continue;\n\n let nA = graph.entity(nAid),\n nB = graph.entity(nBid);\n let crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]);\n if (crossLoc) {\n return {\n mid: midNode,\n node: tipNode,\n wid: way2.id,\n edge: [nA.id, nB.id],\n cross_loc: crossLoc\n };\n }\n }\n return null;\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionMergeNodes } from '../actions/merge_nodes';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilDatesOverlap } from '../util';\nimport { t } from '../core/localizer';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { osmPathHighwayTagValues } from '../osm/tags';\nimport { geoMetersToLat, geoMetersToLon, geoSphericalDistance } from '../geo/geo';\nimport { geoExtent } from '../geo/extent';\n\nexport function validationCloseNodes(context) {\n var type = 'close_nodes';\n\n var pointThresholdMeters = 0.2;\n\n var validation = function(entity, graph) {\n if (entity.type === 'node') {\n return getIssuesForNode(entity);\n } else if (entity.type === 'way') {\n return getIssuesForWay(entity);\n }\n return [];\n\n function getIssuesForNode(node) {\n var parentWays = graph.parentWays(node);\n if (parentWays.length) {\n return getIssuesForVertex(node, parentWays);\n } else {\n return getIssuesForDetachedPoint(node);\n }\n }\n\n function wayTypeFor(way) {\n\n if (way.tags.boundary && way.tags.boundary !== 'no') return 'boundary';\n if (way.tags.indoor && way.tags.indoor !== 'no') return 'indoor';\n if ((way.tags.building && way.tags.building !== 'no') ||\n (way.tags['building:part'] && way.tags['building:part'] !== 'no')) return 'building';\n if (osmPathHighwayTagValues[way.tags.highway]) return 'path';\n\n var parentRelations = graph.parentRelations(way);\n for (var i in parentRelations) {\n var relation = parentRelations[i];\n\n if (relation.tags.type === 'boundary') return 'boundary';\n\n if (relation.isMultipolygon()) {\n if (relation.tags.indoor && relation.tags.indoor !== 'no') return 'indoor';\n if ((relation.tags.building && relation.tags.building !== 'no') ||\n (relation.tags['building:part'] && relation.tags['building:part'] !== 'no')) return 'building';\n }\n }\n\n return 'other';\n }\n\n function shouldCheckWay(way) {\n\n // don't flag issues where merging would create degenerate ways\n if (way.nodes.length <= 2 ||\n (way.isClosed() && way.nodes.length <= 4)) return false;\n\n var bbox = way.extent(graph).bbox();\n var hypotenuseMeters = geoSphericalDistance([bbox.minX, bbox.minY], [bbox.maxX, bbox.maxY]);\n // don't flag close nodes in very small ways\n if (hypotenuseMeters < 1.5) return false;\n\n return true;\n }\n\n function getIssuesForWay(way) {\n if (!shouldCheckWay(way)) return [];\n\n var issues = [],\n nodes = graph.childNodes(way);\n for (var i = 0; i < nodes.length - 1; i++) {\n var node1 = nodes[i];\n var node2 = nodes[i+1];\n\n var issue = getWayIssueIfAny(node1, node2, way);\n if (issue) issues.push(issue);\n }\n return issues;\n }\n\n function getIssuesForVertex(node, parentWays) {\n var issues = [];\n\n function checkForCloseness(node1, node2, way) {\n var issue = getWayIssueIfAny(node1, node2, way);\n if (issue) issues.push(issue);\n }\n\n for (var i = 0; i < parentWays.length; i++) {\n var parentWay = parentWays[i];\n\n if (!shouldCheckWay(parentWay)) continue;\n\n var lastIndex = parentWay.nodes.length - 1;\n for (var j = 0; j < parentWay.nodes.length; j++) {\n if (j !== 0) {\n if (parentWay.nodes[j-1] === node.id) {\n checkForCloseness(node, graph.entity(parentWay.nodes[j]), parentWay);\n }\n }\n if (j !== lastIndex) {\n if (parentWay.nodes[j+1] === node.id) {\n checkForCloseness(graph.entity(parentWay.nodes[j]), node, parentWay);\n }\n }\n }\n }\n return issues;\n }\n\n function thresholdMetersForWay(way) {\n if (!shouldCheckWay(way)) return 0;\n\n var wayType = wayTypeFor(way);\n\n // don't flag boundaries since they might be highly detailed and can't be easily verified\n if (wayType === 'boundary') return 0;\n // expect some features to be mapped with higher levels of detail\n if (wayType === 'indoor') return 0.01;\n if (wayType === 'building') return 0.05;\n if (wayType === 'path') return 0.1;\n return 0.2;\n }\n\n function getIssuesForDetachedPoint(node) {\n\n var issues = [];\n\n var lon = node.loc[0];\n var lat = node.loc[1];\n var lon_range = geoMetersToLon(pointThresholdMeters, lat) / 2;\n var lat_range = geoMetersToLat(pointThresholdMeters) / 2;\n var queryExtent = geoExtent([\n [lon - lon_range, lat - lat_range],\n [lon + lon_range, lat + lat_range]\n ]);\n\n var intersected = context.history().tree().intersects(queryExtent, graph);\n for (var j = 0; j < intersected.length; j++) {\n var nearby = intersected[j];\n\n if (nearby.id === node.id) continue;\n if (nearby.type !== 'node' || nearby.geometry(graph) !== 'point') continue;\n\n if (nearby.loc === node.loc ||\n geoSphericalDistance(node.loc, nearby.loc) < pointThresholdMeters) {\n\n // ignore stolperstein (https://wiki.openstreetmap.org/wiki/DE:Stolpersteine)\n if ('memorial:type' in node.tags && 'memorial:type' in nearby.tags && node.tags['memorial:type']==='stolperstein' && nearby.tags['memorial:type']==='stolperstein') continue;\n if ('memorial' in node.tags && 'memorial' in nearby.tags && node.tags.memorial==='stolperstein' && nearby.tags.memorial === 'stolperstein') continue;\n\n // allow very close points if tags indicate the z-axis might vary\n var zAxisKeys = { layer: true, level: true, 'addr:housenumber': true, 'addr:unit': true };\n var zAxisDifferentiates = false;\n for (var key in zAxisKeys) {\n var nodeValue = node.tags[key] || '0';\n var nearbyValue = nearby.tags[key] || '0';\n if (nodeValue !== nearbyValue) {\n zAxisDifferentiates = true;\n break;\n }\n }\n if (zAxisDifferentiates) continue;\n\n // allow features to overlap spatially if they don't overlap temporally\n if ((node.tags.start_date || node.tags.end_date) && (nearby.tags.start_date || nearby.tags.end_date)) {\n if (!utilDatesOverlap(node.tags, nearby.tags)) continue;\n }\n\n issues.push(new validationIssue({\n type: type,\n subtype: 'detached',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]),\n entity2 = context.hasEntity(this.entityIds[1]);\n return (entity && entity2) ? t.append('issues.close_nodes.detached.message', {\n feature: utilDisplayLabel(entity, context.graph()),\n feature2: utilDisplayLabel(entity2, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [node.id, nearby.id],\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-disconnect',\n title: t.append('issues.fix.move_points_apart.title')\n }),\n new validationIssueFix({\n icon: 'iD-icon-layers',\n title: t.append('issues.fix.use_different_layers_or_levels.title')\n }),\n new validationIssueFix({\n icon: 'iD-operation-change-date-range',\n title: t.append('issues.fix.change_date_range.title')\n })\n ];\n }\n }));\n }\n }\n\n return issues;\n\n function showReference(selection) {\n var referenceText = t('issues.close_nodes.detached.reference');\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .html(referenceText);\n }\n }\n\n function getWayIssueIfAny(node1, node2, way) {\n if (node1.id === node2.id ||\n (node1.hasInterestingTags() && node2.hasInterestingTags())) {\n return null;\n }\n\n if (node1.loc !== node2.loc) {\n var parentWays1 = graph.parentWays(node1);\n var parentWays2 = new Set(graph.parentWays(node2));\n\n var sharedWays = parentWays1.filter(function(parentWay) {\n return parentWays2.has(parentWay);\n });\n\n var thresholds = sharedWays.map(function(parentWay) {\n return thresholdMetersForWay(parentWay);\n });\n\n var threshold = Math.min(...thresholds);\n var distance = geoSphericalDistance(node1.loc, node2.loc);\n if (distance > threshold) return null;\n }\n\n return new validationIssue({\n type: type,\n subtype: 'vertices',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.close_nodes.message', { way: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: showReference,\n entityIds: [way.id, node1.id, node2.id],\n loc: node1.loc,\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-icon-plus',\n title: t.append('issues.fix.merge_points.title'),\n onClick: function(context) {\n var entityIds = this.issue.entityIds;\n var action = actionMergeNodes([entityIds[1], entityIds[2]]);\n context.perform(action, t('issues.fix.merge_close_vertices.annotation'));\n }\n }),\n new validationIssueFix({\n icon: 'iD-operation-disconnect',\n title: t.append('issues.fix.move_points_apart.title')\n })\n ];\n }\n });\n\n function showReference(selection) {\n var referenceText = t('issues.close_nodes.reference');\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .html(referenceText);\n }\n }\n\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { deepEqual } from 'fast-equals';\n\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionMergeNodes } from '../actions/merge_nodes';\nimport { actionSplit } from '../actions/split';\nimport { modeSelect } from '../modes/select';\nimport { geoAngle, geoExtent, geoLatToMeters, geoLonToMeters, geoLineIntersection,\n geoSphericalClosestNode, geoSphericalDistance, geoVecAngle, geoVecLength, geoMetersToLat, geoMetersToLon } from '../geo';\nimport { osmNode } from '../osm/node';\nimport { osmFlowingWaterwayTagValues, osmPathHighwayTagValues, osmRailwayTrackTagValues, osmRoutableAerowayTags, osmRoutableHighwayTagValues } from '../osm/tags';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilDatesOverlap } from '../util';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationCrossingWays(context) {\n var type = 'crossing_ways';\n\n // returns the way or its parent relation, whichever has a useful feature type\n function getFeatureWithFeatureTypeTagsForWay(way, graph) {\n if (getFeatureType(way, graph) === null) {\n // if the way doesn't match a feature type, check its parent relations\n var parentRels = graph.parentRelations(way);\n for (var i = 0; i < parentRels.length; i++) {\n var rel = parentRels[i];\n if (getFeatureType(rel, graph) !== null) {\n return rel;\n }\n }\n }\n return way;\n }\n\n\n function hasTag(tags, key) {\n return tags[key] !== undefined && tags[key] !== 'no';\n }\n\n function taggedAsIndoor(tags) {\n return hasTag(tags, 'indoor') ||\n hasTag(tags, 'level') ||\n tags.highway === 'corridor';\n }\n\n function allowsBridge(featureType) {\n return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway' || featureType === 'aeroway';\n }\n function allowsTunnel(featureType) {\n return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway';\n }\n\n // discard\n var ignoredBuildings = {\n demolished: true, dismantled: true, proposed: true, razed: true\n };\n\n\n function getFeatureType(entity, graph) {\n\n var geometry = entity.geometry(graph);\n if (geometry !== 'line' && geometry !== 'area') return null;\n\n var tags = entity.tags;\n\n if (tags.aeroway in osmRoutableAerowayTags) return 'aeroway';\n\n if (hasTag(tags, 'building') && !ignoredBuildings[tags.building]) return 'building';\n if (hasTag(tags, 'highway') && osmRoutableHighwayTagValues[tags.highway]) return 'highway';\n\n // don't check railway or waterway areas\n if (geometry !== 'line') return null;\n\n if (hasTag(tags, 'railway') && osmRailwayTrackTagValues[tags.railway]) return 'railway';\n if (hasTag(tags, 'waterway') && osmFlowingWaterwayTagValues[tags.waterway]) return 'waterway';\n\n return null;\n }\n\n\n function isLegitCrossing(tags1, featureType1, tags2, featureType2) {\n\n // assume 0 by default\n var level1 = tags1.level || '0';\n var level2 = tags2.level || '0';\n\n if (taggedAsIndoor(tags1) && taggedAsIndoor(tags2) && level1 !== level2) {\n // assume features don't interact if they're indoor on different levels\n return true;\n }\n\n // don't flag crossing waterways and pier/highways\n if (featureType1 === 'waterway' && featureType2 === 'highway' && tags2.man_made === 'pier') return true;\n if (featureType2 === 'waterway' && featureType1 === 'highway' && tags1.man_made === 'pier') return true;\n\n // allow features to overlap spatially if they don't overlap temporally\n if ((tags1.start_date || tags1.end_date) && (tags2.start_date || tags2.end_date)) {\n if (!utilDatesOverlap(tags1, tags2)) return true;\n }\n\n if (tags1.layer !== undefined && tags1.layer === tags2.layer) return false; // Warn if both have the same defined layer\n\n const isElement1Bridge = allowsBridge(featureType1) && hasTag(tags1, 'bridge');\n const isElement2Bridge = allowsBridge(featureType2) && hasTag(tags2, 'bridge');\n if (isElement1Bridge !== isElement2Bridge) return true; // Either one is bridge, the other is not\n\n const isElement1Tunnel = allowsTunnel(featureType1) && hasTag(tags1, 'tunnel');\n const isElement2Tunnel = allowsTunnel(featureType2) && hasTag(tags2, 'tunnel');\n if (isElement1Tunnel !== isElement2Tunnel ) return true; // Either one is tunnel, the other is not\n\n return (tags1.layer || '0') !== (tags2.layer || '0');\n }\n\n\n // highway values for which we shouldn't recommend connecting to waterways\n var highwaysDisallowingFords = {\n motorway: true, motorway_link: true, trunk: true, trunk_link: true,\n primary: true, primary_link: true, secondary: true, secondary_link: true\n };\n\n /**\n * @returns {object | null} the tags for the connecting node, or null if the entities should not be joined\n */\n function tagsForConnectionNodeIfAllowed(entity1, entity2, graph, lessLikelyTags) {\n var featureType1 = getFeatureType(entity1, graph);\n var featureType2 = getFeatureType(entity2, graph);\n\n var geometry1 = entity1.geometry(graph);\n var geometry2 = entity2.geometry(graph);\n var bothLines = geometry1 === 'line' && geometry2 === 'line';\n\n /**\n * @typedef {NonNullable>} FeatureType\n * @type {`${FeatureType}-${FeatureType}`}\n */\n const featureTypes = [featureType1, featureType2].sort().join('-');\n\n if (featureTypes === 'aeroway-aeroway') return {};\n\n if (featureTypes === 'aeroway-highway') {\n const isServiceRoad = entity1.tags.highway === 'service' || entity2.tags.highway === 'service';\n const isPath = entity1.tags.highway in osmPathHighwayTagValues || entity2.tags.highway in osmPathHighwayTagValues;\n // only significant roads get the aeroway=aircraft_crossing tag\n return isServiceRoad || isPath ? {} : { aeroway: 'aircraft_crossing' };\n }\n\n if (featureTypes === 'aeroway-railway') {\n return { aeroway: 'aircraft_crossing', railway: 'level_crossing' };\n }\n\n if (featureTypes === 'aeroway-waterway') return null;\n\n if (featureType1 === featureType2) {\n if (featureType1 === 'highway') {\n var entity1IsPath = osmPathHighwayTagValues[entity1.tags.highway];\n var entity2IsPath = osmPathHighwayTagValues[entity2.tags.highway];\n if ((entity1IsPath || entity2IsPath) && entity1IsPath !== entity2IsPath) {\n // one feature is a path but not both\n\n if (!bothLines) return {};\n\n var roadFeature = entity1IsPath ? entity2 : entity1;\n var pathFeature = entity1IsPath ? entity1 : entity2;\n // don't mark path connections with tracks as crossings\n if (roadFeature.tags.highway === 'track') {\n return {};\n }\n // a sidewalk crossing a driveway is unremarkable and unlikely to be interrupted by the driveway\n // a sidewalk crossing another kind of service road may be similarly unremarkable\n if (!lessLikelyTags &&\n roadFeature.tags.highway === 'service' &&\n pathFeature.tags.highway === 'footway' && pathFeature.tags.footway === 'sidewalk') {\n return {};\n }\n if (['marked', 'unmarked', 'traffic_signals', 'uncontrolled'].indexOf(pathFeature.tags.crossing) !== -1) {\n // if the path is a crossing, match the crossing type and markings\n var tags = { highway: 'crossing', crossing: pathFeature.tags.crossing };\n if ('crossing:markings' in pathFeature.tags) {\n tags['crossing:markings'] = pathFeature.tags['crossing:markings'];\n }\n return tags;\n }\n // don't add a `crossing` subtag to ambiguous crossings\n return { highway: 'crossing' };\n }\n return {};\n }\n if (featureType1 === 'waterway') return {};\n if (featureType1 === 'railway') {\n return { railway: 'railway_crossing' };\n }\n\n } else {\n if (featureTypes.indexOf('highway') !== -1) {\n if (featureTypes.indexOf('railway') !== -1) {\n if (!bothLines) return {};\n\n var isTram = entity1.tags.railway === 'tram' || entity2.tags.railway === 'tram';\n\n if (osmPathHighwayTagValues[entity1.tags.highway] ||\n osmPathHighwayTagValues[entity2.tags.highway]) {\n\n // path-tram connections use this tag\n if (isTram) return { railway: 'tram_crossing' };\n\n // other path-rail connections use this tag\n return { railway: 'crossing' };\n } else {\n // path-tram connections use this tag\n if (isTram) return { railway: 'tram_level_crossing' };\n\n // other road-rail connections use this tag\n return { railway: 'level_crossing' };\n }\n }\n\n if (featureTypes.indexOf('waterway') !== -1) {\n // do not allow fords on structures\n if (hasTag(entity1.tags, 'tunnel') && hasTag(entity2.tags, 'tunnel')) return null;\n if (hasTag(entity1.tags, 'bridge') && hasTag(entity2.tags, 'bridge')) return null;\n\n if (highwaysDisallowingFords[entity1.tags.highway] ||\n highwaysDisallowingFords[entity2.tags.highway]) {\n // do not allow fords on major highways\n return null;\n }\n return bothLines ? { ford: 'yes' } : {};\n }\n }\n }\n return null;\n }\n\n\n function findCrossingsByWay(way1, graph, tree) {\n var edgeCrossInfos = [];\n if (way1.type !== 'way') return edgeCrossInfos;\n\n var taggedFeature1 = getFeatureWithFeatureTypeTagsForWay(way1, graph);\n var way1FeatureType = getFeatureType(taggedFeature1, graph);\n if (way1FeatureType === null) return edgeCrossInfos;\n\n var checkedSingleCrossingWays = {};\n\n // declare vars ahead of time to reduce garbage collection\n var i, j;\n var extent;\n var n1, n2, nA, nB, nAId, nBId;\n var segment1, segment2;\n var oneOnly;\n var segmentInfos, segment2Info, way2, taggedFeature2, way2FeatureType;\n var way1Nodes = graph.childNodes(way1);\n var comparedWays = {};\n for (i = 0; i < way1Nodes.length - 1; i++) {\n n1 = way1Nodes[i];\n n2 = way1Nodes[i + 1];\n extent = geoExtent([\n [\n Math.min(n1.loc[0], n2.loc[0]),\n Math.min(n1.loc[1], n2.loc[1])\n ],\n [\n Math.max(n1.loc[0], n2.loc[0]),\n Math.max(n1.loc[1], n2.loc[1])\n ]\n ]);\n\n // Optimize by only checking overlapping segments, not every segment\n // of overlapping ways\n segmentInfos = tree.waySegments(extent, graph);\n\n for (j = 0; j < segmentInfos.length; j++) {\n segment2Info = segmentInfos[j];\n\n // don't check for self-intersection in this validation\n if (segment2Info.wayId === way1.id) continue;\n\n // skip if this way was already checked and only one issue is needed\n if (checkedSingleCrossingWays[segment2Info.wayId]) continue;\n\n // mark this way as checked even if there are no crossings\n comparedWays[segment2Info.wayId] = true;\n\n way2 = graph.hasEntity(segment2Info.wayId);\n if (!way2) continue;\n taggedFeature2 = getFeatureWithFeatureTypeTagsForWay(way2, graph);\n // only check crossing highway, waterway, building, and railway\n way2FeatureType = getFeatureType(taggedFeature2, graph);\n\n if (way2FeatureType === null ||\n isLegitCrossing(taggedFeature1.tags, way1FeatureType, taggedFeature2.tags, way2FeatureType)) {\n continue;\n }\n\n // create only one issue for building crossings\n oneOnly = way1FeatureType === 'building' || way2FeatureType === 'building';\n\n nAId = segment2Info.nodes[0];\n nBId = segment2Info.nodes[1];\n if (nAId === n1.id || nAId === n2.id ||\n nBId === n1.id || nBId === n2.id) {\n // n1 or n2 is a connection node; skip\n continue;\n }\n nA = graph.hasEntity(nAId);\n if (!nA) continue;\n nB = graph.hasEntity(nBId);\n if (!nB) continue;\n\n segment1 = [n1.loc, n2.loc];\n segment2 = [nA.loc, nB.loc];\n var point = geoLineIntersection(segment1, segment2);\n if (point) {\n edgeCrossInfos.push({\n wayInfos: [\n {\n way: way1,\n featureType: way1FeatureType,\n edge: [n1.id, n2.id]\n },\n {\n way: way2,\n featureType: way2FeatureType,\n edge: [nA.id, nB.id]\n }\n ],\n crossPoint: point\n });\n if (oneOnly) {\n checkedSingleCrossingWays[way2.id] = true;\n break;\n }\n }\n }\n }\n return edgeCrossInfos;\n }\n\n\n function waysToCheck(entity, graph) {\n var featureType = getFeatureType(entity, graph);\n if (!featureType) return [];\n\n if (entity.type === 'way') {\n return [entity];\n } else if (entity.type === 'relation') {\n return entity.members.reduce(function(array, member) {\n if (member.type === 'way' &&\n // only look at geometry ways\n (!member.role || member.role === 'outer' || member.role === 'inner')) {\n var entity = graph.hasEntity(member.id);\n // don't add duplicates\n if (entity && array.indexOf(entity) === -1) {\n array.push(entity);\n }\n }\n return array;\n }, []);\n }\n return [];\n }\n\n\n var validation = function checkCrossingWays(entity, graph) {\n\n var tree = context.history().tree();\n\n var ways = waysToCheck(entity, graph);\n\n var issues = [];\n // declare these here to reduce garbage collection\n var wayIndex, crossingIndex, crossings;\n for (wayIndex in ways) {\n crossings = findCrossingsByWay(ways[wayIndex], graph, tree);\n for (crossingIndex in crossings) {\n issues.push(createIssue(crossings[crossingIndex], graph));\n }\n }\n return issues;\n };\n\n\n function createIssue(crossing, graph) {\n\n // use the entities with the tags that define the feature type\n crossing.wayInfos.sort(function(way1Info, way2Info) {\n var type1 = way1Info.featureType;\n var type2 = way2Info.featureType;\n if (type1 === type2) {\n return utilDisplayLabel(way1Info.way, graph) > utilDisplayLabel(way2Info.way, graph);\n } else if (type1 === 'waterway') {\n return true;\n } else if (type2 === 'waterway') {\n return false;\n }\n return type1 < type2;\n });\n var entities = crossing.wayInfos.map(function(wayInfo) {\n return getFeatureWithFeatureTypeTagsForWay(wayInfo.way, graph);\n });\n var edges = [crossing.wayInfos[0].edge, crossing.wayInfos[1].edge];\n var featureTypes = [crossing.wayInfos[0].featureType, crossing.wayInfos[1].featureType];\n\n var connectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1], graph);\n\n var featureType1 = crossing.wayInfos[0].featureType;\n var featureType2 = crossing.wayInfos[1].featureType;\n\n var isCrossingIndoors = taggedAsIndoor(entities[0].tags) && taggedAsIndoor(entities[1].tags);\n var isCrossingTunnels = allowsTunnel(featureType1) && hasTag(entities[0].tags, 'tunnel') &&\n allowsTunnel(featureType2) && hasTag(entities[1].tags, 'tunnel');\n var isCrossingBridges = allowsBridge(featureType1) && hasTag(entities[0].tags, 'bridge') &&\n allowsBridge(featureType2) && hasTag(entities[1].tags, 'bridge');\n\n var subtype = [featureType1, featureType2].sort().join('-');\n\n var crossingTypeID = subtype;\n\n if (isCrossingIndoors) {\n crossingTypeID = 'indoor-indoor';\n } else if (isCrossingTunnels) {\n crossingTypeID = 'tunnel-tunnel';\n } else if (isCrossingBridges) {\n crossingTypeID = 'bridge-bridge';\n }\n if (connectionTags && (isCrossingIndoors || isCrossingTunnels || isCrossingBridges)) {\n crossingTypeID += '_connectable';\n }\n\n // Differentiate based on the loc rounded to 4 digits, since two ways can cross multiple times.\n var uniqueID = crossing.crossPoint[0].toFixed(4) + ',' + crossing.crossPoint[1].toFixed(4);\n\n return new validationIssue({\n type: type,\n subtype: subtype,\n severity: 'warning',\n message: function(context) {\n var graph = context.graph();\n var entity1 = graph.hasEntity(this.entityIds[0]),\n entity2 = graph.hasEntity(this.entityIds[1]);\n return (entity1 && entity2) ? t.append('issues.crossing_ways.message', {\n feature: utilDisplayLabel(entity1, graph, featureType1 === 'building'),\n feature2: utilDisplayLabel(entity2, graph, featureType2 === 'building')\n }) : '';\n },\n reference: showReference,\n entityIds: entities.map(function(entity) {\n return entity.id;\n }),\n data: {\n edges: edges,\n featureTypes: featureTypes,\n connectionTags: connectionTags\n },\n hash: uniqueID,\n loc: crossing.crossPoint,\n dynamicFixes: function(context) {\n var mode = context.mode();\n if (!mode || mode.id !== 'select' || mode.selectedIDs().length !== 1) return [];\n\n var selectedIndex = this.entityIds[0] === mode.selectedIDs()[0] ? 0 : 1;\n var selectedFeatureType = this.data.featureTypes[selectedIndex];\n var otherFeatureType = this.data.featureTypes[selectedIndex === 0 ? 1 : 0];\n\n var fixes = [];\n\n if (connectionTags) {\n fixes.push(makeConnectWaysFix(this.data.connectionTags));\n let lessLikelyConnectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1], graph, true);\n if (lessLikelyConnectionTags && !deepEqual(connectionTags, lessLikelyConnectionTags)) {\n fixes.push(makeConnectWaysFix(lessLikelyConnectionTags));\n }\n }\n\n if (isCrossingIndoors) {\n fixes.push(new validationIssueFix({\n icon: 'iD-icon-layers',\n title: t.append('issues.fix.use_different_levels.title')\n }));\n } else if (isCrossingTunnels ||\n isCrossingBridges ||\n featureType1 === 'building' ||\n featureType2 === 'building') {\n\n fixes.push(makeChangeLayerFix('higher'));\n fixes.push(makeChangeLayerFix('lower'));\n\n // can only add bridge/tunnel if both features are lines\n } else if (context.graph().geometry(this.entityIds[0]) === 'line' &&\n context.graph().geometry(this.entityIds[1]) === 'line') {\n\n // don't recommend adding bridges to waterways since they're uncommon\n if (allowsBridge(selectedFeatureType) && selectedFeatureType !== 'waterway') {\n fixes.push(makeAddBridgeOrTunnelFix('add_a_bridge', 'temaki-bridge', 'bridge'));\n }\n\n // don't recommend adding tunnels under waterways since they're uncommon\n var skipTunnelFix = otherFeatureType === 'waterway' && selectedFeatureType !== 'waterway';\n if (allowsTunnel(selectedFeatureType) && !skipTunnelFix) {\n if (selectedFeatureType === 'waterway') {\n // naming piped waterway \"tunnel\" is a confusing osmism, culvert should be more clear\n fixes.push(makeAddBridgeOrTunnelFix('add_a_culvert', 'temaki-waste', 'tunnel'));\n } else {\n fixes.push(makeAddBridgeOrTunnelFix('add_a_tunnel', 'temaki-tunnel', 'tunnel'));\n }\n }\n }\n\n // changing the date range is always an option\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-change-date-range',\n title: t.append('issues.fix.change_date_range.title')\n }));\n\n // repositioning the features is always an option\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-move',\n title: t.append('issues.fix.reposition_features.title')\n }));\n\n if (featureType1 === 'building' || featureType2 === 'building') {\n // if the validation is about overlapping buildings:\n // show \"reposition features\" suggestion first, as that is most often\n // most sensible fix for those errors, see #11329\n fixes.unshift(fixes.pop());\n }\n\n return fixes;\n }\n });\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.crossing_ways.' + crossingTypeID + '.reference'));\n }\n }\n\n function makeAddBridgeOrTunnelFix(fixTitleID, iconName, bridgeOrTunnel){\n return new validationIssueFix({\n icon: iconName,\n title: t.append('issues.fix.' + fixTitleID + '.title'),\n onClick: function(context) {\n var mode = context.mode();\n if (!mode || mode.id !== 'select') return;\n\n var selectedIDs = mode.selectedIDs();\n if (selectedIDs.length !== 1) return;\n\n var selectedWayID = selectedIDs[0];\n if (!context.hasEntity(selectedWayID)) return;\n\n var resultWayIDs = [selectedWayID];\n\n var edge, crossedEdge, crossedWayID;\n if (this.issue.entityIds[0] === selectedWayID) {\n edge = this.issue.data.edges[0];\n crossedEdge = this.issue.data.edges[1];\n crossedWayID = this.issue.entityIds[1];\n } else {\n edge = this.issue.data.edges[1];\n crossedEdge = this.issue.data.edges[0];\n crossedWayID = this.issue.entityIds[0];\n }\n\n var crossingLoc = this.issue.loc;\n\n var projection = context.projection;\n\n var action = function actionAddStructure(graph) {\n\n var edgeNodes = [graph.entity(edge[0]), graph.entity(edge[1])];\n\n var crossedWay = graph.hasEntity(crossedWayID);\n // use the explicit width of the crossed feature as the structure length, if available\n var structLengthMeters = crossedWay && isFinite(crossedWay.tags.width) && Number(crossedWay.tags.width);\n if (!structLengthMeters) {\n // if no explicit width is set, approximate the width based on the tags\n structLengthMeters = crossedWay && crossedWay.impliedLineWidthMeters();\n }\n if (structLengthMeters) {\n if (getFeatureType(crossedWay, graph) === 'railway') {\n // bridges over railways are generally much longer than the rail bed itself, compensate\n structLengthMeters *= 2;\n }\n } else {\n // should ideally never land here since all rail/water/road tags should have an implied width\n structLengthMeters = 8;\n }\n\n var a1 = geoAngle(edgeNodes[0], edgeNodes[1], projection) + Math.PI;\n var a2 = geoAngle(graph.entity(crossedEdge[0]), graph.entity(crossedEdge[1]), projection) + Math.PI;\n var crossingAngle = Math.max(a1, a2) - Math.min(a1, a2);\n if (crossingAngle > Math.PI) crossingAngle -= Math.PI;\n // lengthen the structure to account for the angle of the crossing\n structLengthMeters = ((structLengthMeters / 2) / Math.sin(crossingAngle)) * 2;\n\n // add padding since the structure must extend past the edges of the crossed feature\n structLengthMeters += 4;\n\n // clamp the length to a reasonable range\n structLengthMeters = Math.min(Math.max(structLengthMeters, 4), 50);\n\n function geomToProj(geoPoint) {\n return [\n geoLonToMeters(geoPoint[0], geoPoint[1]),\n geoLatToMeters(geoPoint[1])\n ];\n }\n function projToGeom(projPoint) {\n var lat = geoMetersToLat(projPoint[1]);\n return [\n geoMetersToLon(projPoint[0], lat),\n lat\n ];\n }\n\n var projEdgeNode1 = geomToProj(edgeNodes[0].loc);\n var projEdgeNode2 = geomToProj(edgeNodes[1].loc);\n\n var projectedAngle = geoVecAngle(projEdgeNode1, projEdgeNode2);\n\n var projectedCrossingLoc = geomToProj(crossingLoc);\n var linearToSphericalMetersRatio = geoVecLength(projEdgeNode1, projEdgeNode2) /\n geoSphericalDistance(edgeNodes[0].loc, edgeNodes[1].loc);\n\n function locSphericalDistanceFromCrossingLoc(angle, distanceMeters) {\n var lengthSphericalMeters = distanceMeters * linearToSphericalMetersRatio;\n return projToGeom([\n projectedCrossingLoc[0] + Math.cos(angle) * lengthSphericalMeters,\n projectedCrossingLoc[1] + Math.sin(angle) * lengthSphericalMeters\n ]);\n }\n\n var endpointLocGetter1 = function(lengthMeters) {\n return locSphericalDistanceFromCrossingLoc(projectedAngle, lengthMeters);\n };\n var endpointLocGetter2 = function(lengthMeters) {\n return locSphericalDistanceFromCrossingLoc(projectedAngle + Math.PI, lengthMeters);\n };\n\n // avoid creating very short edges from splitting too close to another node\n var minEdgeLengthMeters = 0.55;\n\n // decide where to bound the structure along the way, splitting as necessary\n function determineEndpoint(edge, endNode, locGetter) {\n var newNode;\n\n var idealLengthMeters = structLengthMeters / 2;\n\n // distance between the crossing location and the end of the edge,\n // the maximum length of this side of the structure\n var crossingToEdgeEndDistance = geoSphericalDistance(crossingLoc, endNode.loc);\n\n if (crossingToEdgeEndDistance - idealLengthMeters > minEdgeLengthMeters) {\n // the edge is long enough to insert a new node\n\n // the loc that would result in the full expected length\n var idealNodeLoc = locGetter(idealLengthMeters);\n\n newNode = osmNode();\n graph = actionAddMidpoint({ loc: idealNodeLoc, edge: edge }, newNode)(graph);\n\n } else {\n var edgeCount = 0;\n endNode.parentIntersectionWays(graph).forEach(function(way) {\n way.nodes.forEach(function(nodeID) {\n if (nodeID === endNode.id) {\n if ((endNode.id === way.first() && endNode.id !== way.last()) ||\n (endNode.id === way.last() && endNode.id !== way.first())) {\n edgeCount += 1;\n } else {\n edgeCount += 2;\n }\n }\n });\n });\n\n if (edgeCount >= 3) {\n // the end node is a junction, try to leave a segment\n // between it and the structure - #7202\n\n var insetLength = crossingToEdgeEndDistance - minEdgeLengthMeters;\n if (insetLength > minEdgeLengthMeters) {\n var insetNodeLoc = locGetter(insetLength);\n newNode = osmNode();\n graph = actionAddMidpoint({ loc: insetNodeLoc, edge: edge }, newNode)(graph);\n }\n }\n }\n\n // if the edge is too short to subdivide as desired, then\n // just bound the structure at the existing end node\n if (!newNode) newNode = endNode;\n\n var splitAction = actionSplit([newNode.id])\n .limitWays(resultWayIDs); // only split selected or created ways\n\n // do the split\n graph = splitAction(graph);\n if (splitAction.getCreatedWayIDs().length) {\n resultWayIDs.push(splitAction.getCreatedWayIDs()[0]);\n }\n\n return newNode;\n }\n\n var structEndNode1 = determineEndpoint(edge, edgeNodes[1], endpointLocGetter1);\n var structEndNode2 = determineEndpoint([edgeNodes[0].id, structEndNode1.id], edgeNodes[0], endpointLocGetter2);\n\n var structureWay = resultWayIDs.map(function(id) {\n return graph.entity(id);\n }).find(function(way) {\n return way.nodes.indexOf(structEndNode1.id) !== -1 &&\n way.nodes.indexOf(structEndNode2.id) !== -1;\n });\n\n var tags = Object.assign({}, structureWay.tags); // copy tags\n if (bridgeOrTunnel === 'bridge'){\n tags.bridge = 'yes';\n tags.layer = '1';\n } else {\n var tunnelValue = 'yes';\n if (getFeatureType(structureWay, graph) === 'waterway') {\n // use `tunnel=culvert` for waterways by default\n tunnelValue = 'culvert';\n }\n tags.tunnel = tunnelValue;\n tags.layer = '-1';\n }\n // apply the structure tags to the way\n graph = actionChangeTags(structureWay.id, tags)(graph);\n return graph;\n };\n\n context.perform(action, t('issues.fix.' + fixTitleID + '.annotation'));\n context.enter(modeSelect(context, resultWayIDs));\n }\n });\n }\n\n function makeConnectWaysFix(connectionTags) {\n\n var fixTitleID = 'connect_features';\n var fixIcon = 'iD-icon-crossing';\n if (connectionTags.highway === 'crossing') {\n fixTitleID = 'connect_using_crossing';\n fixIcon = 'temaki-pedestrian';\n }\n if (connectionTags.ford) {\n fixTitleID = 'connect_using_ford';\n fixIcon = 'roentgen-ford';\n }\n\n const fix = new validationIssueFix({\n icon: fixIcon,\n title: t.append('issues.fix.' + fixTitleID + '.title'),\n onClick: function(context) {\n var loc = this.issue.loc;\n var edges = this.issue.data.edges;\n\n context.perform(\n function actionConnectCrossingWays(graph) {\n // create the new node for the points\n var node = osmNode({ loc: loc, tags: connectionTags });\n graph = graph.replace(node);\n\n var nodesToMerge = [node.id];\n var mergeThresholdInMeters = 0.75;\n\n edges.forEach(function(edge) {\n var edgeNodes = [graph.entity(edge[0]), graph.entity(edge[1])];\n var nearby = geoSphericalClosestNode(edgeNodes, loc);\n // if there is already a suitable node nearby, use that\n // use the node if node has no interesting tags or if it is a crossing node #8326\n if ((!nearby.node.hasInterestingTags() || nearby.node.isCrossing()) && nearby.distance < mergeThresholdInMeters) {\n nodesToMerge.push(nearby.node.id);\n // else add the new node to the way\n } else {\n graph = actionAddMidpoint({loc: loc, edge: edge}, node)(graph);\n }\n });\n\n if (nodesToMerge.length > 1) {\n // if we're using nearby nodes, merge them with the new node\n graph = actionMergeNodes(nodesToMerge, loc)(graph);\n }\n\n return graph;\n },\n t('issues.fix.connect_crossing_features.annotation')\n );\n }\n });\n fix._connectionTags = connectionTags;\n return fix;\n }\n\n /** @returns {osmEntity | undefined} */\n function getSelectedFeature() {\n const mode = context.mode();\n if (!mode || mode.id !== 'select') return undefined;\n\n const selectedIDs = mode.selectedIDs();\n if (selectedIDs.length !== 1) return undefined;\n\n const selectedID = selectedIDs[0];\n\n const entity = context.hasEntity(selectedID);\n return entity;\n }\n\n /**\n * @param {\"higher\" | \"lower\"} higherOrLower\n * @returns {validationIssueFix | undefined}\n */\n function makeChangeLayerFix(higherOrLower) {\n const selectedFeature = getSelectedFeature();\n return new validationIssueFix({\n id: selectedFeature.id,\n icon: 'iD-icon-' + (higherOrLower === 'higher' ? 'up' : 'down'),\n title: selectedFeature\n ? t.append('issues.fix.tag_this_as_' + higherOrLower + '.informative_title', {\n feature: utilDisplayLabel(selectedFeature, context.graph())\n })\n // in this context, there is no selected feature so we\n // have to show a generic name\n : t.append('issues.fix.tag_this_as_' + higherOrLower + '.title'),\n\n onClick: function(context) {\n const entity = getSelectedFeature();\n const selectedID = entity.id;\n if (!entity) return;\n\n\n if (!this.issue.entityIds.some(function(entityId) {\n return entityId === selectedID;\n })) return;\n\n var tags = Object.assign({}, entity.tags); // shallow copy\n var layer = tags.layer && Number(tags.layer);\n if (layer && !isNaN(layer)) {\n if (higherOrLower === 'higher') {\n layer += 1;\n } else {\n layer -= 1;\n }\n } else {\n if (higherOrLower === 'higher') {\n layer = 1;\n } else {\n layer = -1;\n }\n }\n tags.layer = layer.toString();\n context.perform(\n actionChangeTags(entity.id, tags),\n t('operations.change_tags.annotation')\n );\n }\n });\n }\n\n validation.type = type;\n\n return validation;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { t } from '../core/localizer';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionMoveNode } from '../actions/move_node';\nimport { actionNoop } from '../actions/noop';\nimport { behaviorDraw } from './draw';\nimport { geoChooseEdge, geoHasSelfIntersections } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect } from '../modes/select';\nimport { osmNode } from '../osm/node';\nimport { utilRebind } from '../util/rebind';\nimport { utilKeybinding } from '../util';\n\nexport function behaviorDrawWay(context, wayID, mode, startGraph) {\n const keybinding = utilKeybinding('drawWay');\n\n var dispatch = d3_dispatch('rejectedSelfIntersection');\n\n var behavior = behaviorDraw(context);\n\n // Must be set by `drawWay.nodeIndex` before each install of this behavior.\n var _nodeIndex;\n\n var _origWay;\n var _wayGeometry;\n var _headNodeID;\n var _annotation;\n\n var _pointerHasMoved = false;\n\n // The osmNode to be placed.\n // This is temporary and just follows the mouse cursor until an \"add\" event occurs.\n var _drawNode;\n\n var _didResolveTempEdit = false;\n\n function createDrawNode(loc) {\n // don't make the draw node until we actually need it\n _drawNode = osmNode({ loc: loc });\n\n context.pauseChangeDispatch();\n context.replace(function actionAddDrawNode(graph) {\n // add the draw node to the graph and insert it into the way\n var way = graph.entity(wayID);\n return graph\n .replace(_drawNode)\n .replace(way.addNode(_drawNode.id, _nodeIndex));\n }, _annotation);\n context.resumeChangeDispatch();\n\n setActiveElements();\n }\n\n function removeDrawNode() {\n\n context.pauseChangeDispatch();\n context.replace(\n function actionDeleteDrawNode(graph) {\n var way = graph.entity(wayID);\n return graph\n .replace(way.removeNode(_drawNode.id))\n .remove(_drawNode);\n },\n _annotation\n );\n _drawNode = undefined;\n context.resumeChangeDispatch();\n }\n\n\n function keydown(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope')) {\n context.surface()\n .classed('nope-suppressed', true);\n }\n context.surface()\n .classed('nope', false)\n .classed('nope-disabled', true);\n }\n }\n\n\n function keyup(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope-suppressed')) {\n context.surface()\n .classed('nope', true);\n }\n context.surface()\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false);\n }\n }\n\n\n function allowsVertex(d) {\n return d.geometry(context.graph()) === 'vertex' || presetManager.allowsVertex(d, context.graph());\n }\n\n\n // related code\n // - `mode/drag_node.js` `doMove()`\n // - `behavior/draw.js` `click()`\n // - `behavior/draw_way.js` `move()`\n function move(d3_event, datum) {\n\n var loc = context.map().mouseCoordinates();\n\n if (!_drawNode) createDrawNode(loc);\n\n context.surface().classed('nope-disabled', d3_event.altKey);\n\n var targetLoc = datum && datum.properties && datum.properties.entity &&\n allowsVertex(datum.properties.entity) && datum.properties.entity.loc;\n var targetNodes = datum && datum.properties && datum.properties.nodes;\n\n if (targetLoc) { // snap to node/vertex - a point target with `.loc`\n loc = targetLoc;\n\n } else if (targetNodes) { // snap to way - a line target with `.nodes`\n var choice = geoChooseEdge(targetNodes, context.map().mouse(), context.projection, _drawNode.id);\n if (choice) {\n loc = choice.loc;\n }\n }\n\n context.replace(actionMoveNode(_drawNode.id, loc), _annotation);\n _drawNode = context.entity(_drawNode.id);\n checkGeometry(true /* includeDrawNode */);\n }\n\n\n // Check whether this edit causes the geometry to break.\n // If so, class the surface with a nope cursor.\n // `includeDrawNode` - Only check the relevant line segments if finishing drawing\n function checkGeometry(includeDrawNode) {\n var nopeDisabled = context.surface().classed('nope-disabled');\n var isInvalid = isInvalidGeometry(includeDrawNode);\n\n if (nopeDisabled) {\n context.surface()\n .classed('nope', false)\n .classed('nope-suppressed', isInvalid);\n } else {\n context.surface()\n .classed('nope', isInvalid)\n .classed('nope-suppressed', false);\n }\n }\n\n\n function isInvalidGeometry(includeDrawNode) {\n\n var testNode = _drawNode;\n\n // we only need to test the single way we're drawing\n var parentWay = context.graph().entity(wayID);\n var nodes = context.graph().childNodes(parentWay).slice(); // shallow copy\n\n if (includeDrawNode) {\n if (parentWay.isClosed()) {\n // don't test the last segment for closed ways - #4655\n // (still test the first segment)\n nodes.pop();\n }\n } else { // discount the draw node\n\n if (parentWay.isClosed()) {\n if (nodes.length < 3) return false;\n if (_drawNode) nodes.splice(-2, 1);\n testNode = nodes[nodes.length - 2];\n } else {\n // there's nothing we need to test if we ignore the draw node on open ways\n return false;\n }\n }\n\n return testNode && geoHasSelfIntersections(nodes, testNode.id);\n }\n\n\n function undone() {\n\n // undoing removed the temp edit\n _didResolveTempEdit = true;\n\n context.pauseChangeDispatch();\n\n var nextMode;\n\n if (context.graph() === startGraph) {\n // We've undone back to the initial state before we started drawing.\n // Just exit the draw mode without undoing whatever we did before\n // we entered the draw mode.\n nextMode = modeSelect(context, [wayID]);\n } else {\n // The `undo` only removed the temporary edit, so here we have to\n // manually undo to actually remove the last node we added. We can't\n // use the `undo` function since the initial \"add\" graph doesn't have\n // an annotation and so cannot be undone to.\n context.pop(1);\n\n // continue drawing\n nextMode = mode;\n }\n\n // clear the redo stack by adding and removing a blank edit\n context.perform(actionNoop());\n context.pop(1);\n\n context.resumeChangeDispatch();\n context.enter(nextMode);\n }\n\n\n function setActiveElements() {\n if (!_drawNode) return;\n\n context.surface().selectAll('.' + _drawNode.id)\n .classed('active', true);\n }\n\n\n function resetToStartGraph() {\n while (context.graph() !== startGraph) {\n context.pop();\n }\n }\n\n\n var drawWay = function(surface) {\n _drawNode = undefined;\n _didResolveTempEdit = false;\n _origWay = context.entity(wayID);\n\n if (typeof _nodeIndex === 'number') {\n _headNodeID = _origWay.nodes[_nodeIndex];\n } else if (_origWay.isClosed()) {\n _headNodeID = _origWay.nodes[_origWay.nodes.length - 2];\n } else {\n _headNodeID = _origWay.nodes[_origWay.nodes.length - 1];\n }\n\n _wayGeometry = _origWay.geometry(context.graph());\n _annotation = t((_origWay.nodes.length === (_origWay.isClosed() ? 2 : 1) ?\n 'operations.start.annotation.' :\n 'operations.continue.annotation.') + _wayGeometry\n );\n _pointerHasMoved = false;\n\n // Push an annotated state for undo to return back to.\n // We must make sure to replace or remove it later.\n context.pauseChangeDispatch();\n context.perform(actionNoop(), _annotation);\n context.resumeChangeDispatch();\n\n behavior.hover()\n .initialNodeID(_headNodeID);\n\n behavior\n .on('move', function() {\n _pointerHasMoved = true;\n move.apply(this, arguments);\n })\n .on('down', function() {\n move.apply(this, arguments);\n })\n .on('downcancel', function() {\n if (_drawNode) removeDrawNode();\n })\n .on('click', drawWay.add)\n .on('clickWay', drawWay.addWay)\n .on('clickNode', drawWay.addNode)\n .on('undo', context.undo)\n .on('cancel', drawWay.cancel)\n .on('finish', drawWay.finish);\n\n d3_select(window)\n .on('keydown.drawWay', keydown)\n .on('keyup.drawWay', keyup);\n\n context.map()\n .dblclickZoomEnable(false)\n .on('drawn.draw', setActiveElements);\n\n setActiveElements();\n\n surface.call(behavior);\n\n context.history()\n .on('undone.draw', undone);\n };\n\n\n drawWay.off = function(surface) {\n\n if (!_didResolveTempEdit) {\n // Drawing was interrupted unexpectedly.\n // This can happen if the user changes modes,\n // clicks geolocate button, a hashchange event occurs, etc.\n\n context.pauseChangeDispatch();\n resetToStartGraph();\n context.resumeChangeDispatch();\n }\n\n _drawNode = undefined;\n _nodeIndex = undefined;\n\n context.map()\n .on('drawn.draw', null);\n\n surface.call(behavior.off)\n .selectAll('.active')\n .classed('active', false);\n\n surface\n .classed('nope', false)\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false);\n\n d3_select(window)\n .on('keydown.drawWay', null)\n .on('keyup.drawWay', null);\n\n context.history()\n .on('undone.draw', null);\n };\n\n\n function attemptAdd(d, loc, doAdd) {\n\n if (_drawNode) {\n // move the node to the final loc in case move wasn't called\n // consistently (e.g. on touch devices)\n context.replace(actionMoveNode(_drawNode.id, loc), _annotation);\n _drawNode = context.entity(_drawNode.id);\n } else {\n createDrawNode(loc);\n }\n\n checkGeometry(true /* includeDrawNode */);\n if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {\n if (!_pointerHasMoved) {\n // prevent the temporary draw node from appearing on touch devices\n removeDrawNode();\n }\n dispatch.call('rejectedSelfIntersection', this);\n return; // can't click here\n }\n\n context.pauseChangeDispatch();\n doAdd();\n // we just replaced the temporary edit with the real one\n _didResolveTempEdit = true;\n context.resumeChangeDispatch();\n\n context.enter(mode);\n }\n\n\n // Accept the current position of the drawing node\n drawWay.add = function(loc, d) {\n attemptAdd(d, loc, function() {\n // don't need to do anything extra\n });\n };\n\n\n // Connect the way to an existing way\n drawWay.addWay = function(loc, edge, d) {\n attemptAdd(d, loc, function() {\n context.replace(\n actionAddMidpoint({ loc: loc, edge: edge }, _drawNode),\n _annotation\n );\n });\n };\n\n\n // Connect the way to an existing node\n drawWay.addNode = function(node, d) {\n\n // finish drawing if the mapper targets the prior node\n if (node.id === _headNodeID ||\n // or the first node when drawing an area\n (_origWay.isClosed() && node.id === _origWay.first())) {\n drawWay.finish();\n return;\n }\n\n attemptAdd(d, node.loc, function() {\n context.replace(\n function actionReplaceDrawNode(graph) {\n // remove the temporary draw node and insert the existing node\n // at the same index\n\n graph = graph\n .replace(graph.entity(wayID).removeNode(_drawNode.id))\n .remove(_drawNode);\n return graph\n .replace(graph.entity(wayID).addNode(node.id, _nodeIndex));\n },\n _annotation\n );\n });\n };\n\n /**\n * @param {(typeof osmWay)[]} ways\n * @returns {\"line\" | \"area\" | \"generic\"}\n */\n function getFeatureType(ways) {\n if (ways.every(way => way.isClosed())) return 'area';\n if (ways.every(way => !way.isClosed())) return 'line';\n return 'generic';\n }\n\n /** see PR #8671 */\n function followMode() {\n if (_didResolveTempEdit) return;\n\n try {\n\n // get the last 2 added nodes.\n // check if they are both part of only oneway (the same one)\n // check if the ways that they're part of are the same way\n // find index of the last two nodes, to determine the direction to travel around the existing way\n // add the next node to the way we are drawing\n\n // if we're drawing an area, the first node = last node.\n const isDrawingArea = _origWay.nodes[0] === _origWay.nodes.slice(-1)[0];\n\n const [secondLastNodeId, lastNodeId] = _origWay.nodes.slice(isDrawingArea ? -3 : -2);\n\n // Unlike startGraph, the full history graph may contain unsaved vertices to follow.\n // https://github.com/openstreetmap/iD/issues/8749\n const historyGraph = context.history().graph();\n if (!lastNodeId || !secondLastNodeId || !historyGraph.hasEntity(lastNodeId) || !historyGraph.hasEntity(secondLastNodeId)) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('operations.follow.error.needs_more_initial_nodes'))();\n return;\n }\n\n // If the way has looped over itself, follow some other way.\n const lastNodesParents = historyGraph.parentWays(historyGraph.entity(lastNodeId)).filter(w => w.id !== wayID);\n const secondLastNodesParents = historyGraph.parentWays(historyGraph.entity(secondLastNodeId)).filter(w => w.id !== wayID);\n\n const featureType = getFeatureType(lastNodesParents);\n\n if (lastNodesParents.length !== 1 || secondLastNodesParents.length === 0) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append(`operations.follow.error.intersection_of_multiple_ways.${featureType}`))();\n return;\n }\n\n // Check if the last node's parent is also the parent of the second last node.\n // The last node must only have one parent, but the second last node can have\n // multiple parents.\n if (!secondLastNodesParents.some(n => n.id === lastNodesParents[0].id)) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append(`operations.follow.error.intersection_of_different_ways.${featureType}`))();\n return;\n }\n\n const way = lastNodesParents[0];\n\n const indexOfLast = way.nodes.indexOf(lastNodeId);\n const indexOfSecondLast = way.nodes.indexOf(secondLastNodeId);\n\n // for a closed way, the first/last node is the same so it appears twice in the array,\n // but indexOf always finds the first occurrence. This is only an issue when following a way\n // in descending order\n const isDescendingPastZero = indexOfLast === way.nodes.length - 2 && indexOfSecondLast === 0;\n\n let nextNodeIndex = indexOfLast + (indexOfLast > indexOfSecondLast && !isDescendingPastZero ? 1 : -1);\n // if we're following a closed way and we pass the first/last node, the next index will be -1\n if (nextNodeIndex === -1) nextNodeIndex = indexOfSecondLast === 1 ? way.nodes.length - 2 : 1;\n\n const nextNode = historyGraph.entity(way.nodes[nextNodeIndex]);\n\n drawWay.addNode(nextNode, {\n geometry: { type: 'Point', coordinates: nextNode.loc },\n id: nextNode.id,\n properties: { target: true, entity: nextNode },\n });\n } catch {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('operations.follow.error.unknown'))();\n }\n }\n\n keybinding.on(t('operations.follow.key'), followMode);\n d3_select(document).call(keybinding);\n\n // Finish the draw operation, removing the temporary edit.\n // If the way has enough nodes to be valid, it's selected.\n // Otherwise, delete everything and return to browse mode.\n drawWay.finish = function() {\n checkGeometry(false /* includeDrawNode */);\n if (context.surface().classed('nope')) {\n dispatch.call('rejectedSelfIntersection', this);\n return; // can't click here\n }\n\n context.pauseChangeDispatch();\n // remove the temporary edit\n context.pop(1);\n _didResolveTempEdit = true;\n context.resumeChangeDispatch();\n\n var way = context.hasEntity(wayID);\n if (!way || way.isDegenerate()) {\n drawWay.cancel();\n return;\n }\n\n window.setTimeout(function() {\n context.map().dblclickZoomEnable(true);\n }, 1000);\n\n var isNewFeature = !mode.isContinuing;\n context.enter(modeSelect(context, [wayID]).newFeature(isNewFeature));\n };\n\n\n // Cancel the draw operation, delete everything, and return to browse mode.\n drawWay.cancel = function() {\n context.pauseChangeDispatch();\n resetToStartGraph();\n context.resumeChangeDispatch();\n\n window.setTimeout(function() {\n context.map().dblclickZoomEnable(true);\n }, 1000);\n\n context.surface()\n .classed('nope', false)\n .classed('nope-disabled', false)\n .classed('nope-suppressed', false);\n\n context.enter(modeBrowse(context));\n };\n\n\n drawWay.nodeIndex = function(val) {\n if (!arguments.length) return _nodeIndex;\n _nodeIndex = val;\n return drawWay;\n };\n\n\n drawWay.activeID = function() {\n if (!arguments.length) return _drawNode && _drawNode.id;\n // no assign\n return drawWay;\n };\n\n\n return utilRebind(drawWay, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDrawWay } from '../behavior/draw_way';\n\n\nexport function modeDrawLine(context, wayID, startGraph, button, affix, continuing) {\n var mode = {\n button: button,\n id: 'draw-line'\n };\n\n var behavior = behaviorDrawWay(context, wayID, mode, startGraph)\n .on('rejectedSelfIntersection.modeDrawLine', function() {\n context.ui().flash\n .iconName('#iD-icon-no')\n .label(t.append('self_intersection.error.lines'))();\n });\n\n mode.wayID = wayID;\n\n mode.isContinuing = continuing;\n\n mode.enter = function() {\n behavior\n .nodeIndex(affix === 'prefix' ? 0 : undefined);\n\n context.install(behavior);\n };\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n mode.selectedIDs = function() {\n return [wayID];\n };\n\n mode.activeID = function() {\n return (behavior && behavior.activeID()) || [];\n };\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { utilDetect } from '../util/detect';\n\n\n// Translate a MacOS key command into the appropriate Windows/Linux equivalent.\n// For example, \u2318Z -> Ctrl+Z\nexport var uiCmd = function (code) {\n var detected = utilDetect();\n\n if (detected.os === 'mac') {\n return code;\n }\n\n if (detected.os === 'win') {\n if (code === '\u2318\u21E7Z') return 'Ctrl+Y';\n }\n\n var result = '',\n replacements = {\n '\u2318': 'Ctrl',\n '\u21E7': 'Shift',\n '\u2325': 'Alt',\n '\u232B': 'Backspace',\n '\u2326': 'Delete'\n };\n\n for (var i = 0; i < code.length; i++) {\n if (code[i] in replacements) {\n result += replacements[code[i]] + (i < code.length - 1 ? '+' : '');\n } else {\n result += code[i];\n }\n }\n\n return result;\n};\n\n\n// return a display-focused string for a given keyboard code\nuiCmd.display = function(code) {\n if (code.length !== 1) return code;\n\n var detected = utilDetect();\n var mac = (detected.os === 'mac');\n var replacements = {\n '\u2318': mac ? '\u2318 ' + t('shortcuts.key.cmd') : t('shortcuts.key.ctrl'),\n '\u21E7': mac ? '\u21E7 ' + t('shortcuts.key.shift') : t('shortcuts.key.shift'),\n '\u2325': mac ? '\u2325 ' + t('shortcuts.key.option') : t('shortcuts.key.alt'),\n '\u2303': mac ? '\u2303 ' + t('shortcuts.key.ctrl') : t('shortcuts.key.ctrl'),\n '\u232B': mac ? '\u232B ' + t('shortcuts.key.delete') : t('shortcuts.key.backspace'),\n '\u2326': mac ? '\u2326 ' + t('shortcuts.key.del') : t('shortcuts.key.del'),\n '\u2196': mac ? '\u2196 ' + t('shortcuts.key.pgup') : t('shortcuts.key.pgup'),\n '\u2198': mac ? '\u2198 ' + t('shortcuts.key.pgdn') : t('shortcuts.key.pgdn'),\n '\u21DE': mac ? '\u21DE ' + t('shortcuts.key.home') : t('shortcuts.key.home'),\n '\u21DF': mac ? '\u21DF ' + t('shortcuts.key.end') : t('shortcuts.key.end'),\n '\u21B5': mac ? '\u23CE ' + t('shortcuts.key.return') : t('shortcuts.key.enter'),\n '\u238B': mac ? '\u238B ' + t('shortcuts.key.esc') : t('shortcuts.key.esc'),\n '\u2630': mac ? '\u2630 ' + t('shortcuts.key.menu') : t('shortcuts.key.menu'),\n };\n\n return replacements[code] || code;\n};\n", "import { t } from '../core/localizer';\nimport { actionDeleteMultiple } from '../actions/delete_multiple';\nimport { behaviorOperation } from '../behavior/operation';\nimport { geoSphericalDistance } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect } from '../modes/select';\nimport { uiCmd } from '../ui/cmd';\nimport { utilGetAllNodes, utilTotalExtent } from '../util';\n\n\nexport function operationDelete(context, selectedIDs) {\n var multi = (selectedIDs.length === 1 ? 'single' : 'multiple');\n var action = actionDeleteMultiple(selectedIDs);\n var nodes = utilGetAllNodes(selectedIDs, context.graph());\n var coords = nodes.map(function(n) { return n.loc; });\n var extent = utilTotalExtent(selectedIDs, context.graph());\n\n\n var operation = function() {\n var nextSelectedID;\n var nextSelectedLoc;\n\n if (selectedIDs.length === 1) {\n var id = selectedIDs[0];\n var entity = context.entity(id);\n var geometry = entity.geometry(context.graph());\n var parents = context.graph().parentWays(entity);\n var parent = parents[0];\n\n // Select the next closest node in the way.\n if (geometry === 'vertex') {\n var nodes = parent.nodes;\n var i = nodes.indexOf(id);\n\n if (i === 0) {\n i++;\n } else if (i === nodes.length - 1) {\n i--;\n } else {\n var a = geoSphericalDistance(entity.loc, context.entity(nodes[i - 1]).loc);\n var b = geoSphericalDistance(entity.loc, context.entity(nodes[i + 1]).loc);\n i = a < b ? i - 1 : i + 1;\n }\n\n nextSelectedID = nodes[i];\n nextSelectedLoc = context.entity(nextSelectedID).loc;\n }\n }\n\n context.perform(action, operation.annotation());\n context.validator().validate();\n\n if (nextSelectedID && nextSelectedLoc) {\n if (context.hasEntity(nextSelectedID)) {\n context.enter(modeSelect(context, [nextSelectedID]).follow(true));\n } else {\n context.map().centerEase(nextSelectedLoc);\n context.enter(modeBrowse(context));\n }\n } else {\n context.enter(modeBrowse(context));\n }\n\n };\n\n\n operation.available = function() {\n return true;\n };\n\n\n operation.disabled = function() {\n if (extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n } else if (selectedIDs.some(protectedMember)) {\n return 'part_of_relation';\n } else if (selectedIDs.some(incompleteRelation)) {\n return 'incomplete_relation';\n } else if (selectedIDs.some(hasWikidataTag)) {\n return 'has_wikidata_tag';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n\n function hasWikidataTag(id) {\n var entity = context.entity(id);\n return entity.tags.wikidata && entity.tags.wikidata.trim().length > 0;\n }\n\n function incompleteRelation(id) {\n var entity = context.entity(id);\n return entity.type === 'relation' && !entity.isComplete(context.graph());\n }\n\n function protectedMember(id) {\n var entity = context.entity(id);\n if (entity.type !== 'way') return false;\n\n var parents = context.graph().parentRelations(entity);\n for (var i = 0; i < parents.length; i++) {\n var parent = parents[i];\n var type = parent.tags.type;\n var role = parent.memberById(id).role || 'outer';\n if (type === 'route' || type === 'boundary' || (type === 'multipolygon' && role === 'outer')) {\n return true;\n }\n }\n return false;\n }\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.delete.' + disable + '.' + multi) :\n t.append('operations.delete.description.' + multi);\n };\n\n\n operation.annotation = function() {\n return selectedIDs.length === 1 ?\n t('operations.delete.annotation.' + context.graph().geometry(selectedIDs[0])) :\n t('operations.delete.annotation.feature', { n: selectedIDs.length });\n };\n\n\n operation.id = 'delete';\n operation.keys = [uiCmd('\u2318\u232B'), uiCmd('\u2318\u2326'), uiCmd('\u2326')];\n operation.title = t.append('operations.delete.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import { t, localizer } from '../core/localizer';\nimport { modeDrawLine } from '../modes/draw_line';\nimport { operationDelete } from '../operations/delete';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { osmRoutableHighwayTagValues } from '../osm/tags';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\nexport function validationDisconnectedWay() {\n var type = 'disconnected_way';\n\n function isTaggedAsHighway(entity) {\n return osmRoutableHighwayTagValues[entity.tags.highway];\n }\n\n var validation = function checkDisconnectedWay(entity, graph) {\n\n var routingIslandWays = routingIslandForEntity(entity);\n if (!routingIslandWays) return [];\n\n return [new validationIssue({\n type: type,\n subtype: 'highway',\n severity: 'warning',\n message: function(context) {\n var entity = this.entityIds.length && context.hasEntity(this.entityIds[0]);\n var label = entity && utilDisplayLabel(entity, context.graph());\n return t.append('issues.disconnected_way.routable.message', { count: this.entityIds.length, highway: label });\n },\n reference: showReference,\n entityIds: Array.from(routingIslandWays).map(function(way) { return way.id; }),\n dynamicFixes: makeFixes\n })];\n\n\n function makeFixes(context) {\n\n var fixes = [];\n\n var singleEntity = this.entityIds.length === 1 && context.hasEntity(this.entityIds[0]);\n\n if (singleEntity) {\n\n if (singleEntity.type === 'way' && !singleEntity.isClosed()) {\n\n var textDirection = localizer.textDirection();\n\n var startFix = makeContinueDrawingFixIfAllowed(textDirection, singleEntity.first(), 'start');\n if (startFix) fixes.push(startFix);\n\n var endFix = makeContinueDrawingFixIfAllowed(textDirection, singleEntity.last(), 'end');\n if (endFix) fixes.push(endFix);\n }\n if (!fixes.length) {\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.connect_feature.title')\n }));\n }\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.delete_feature.title'),\n entityIds: [singleEntity.id],\n onClick: function(context) {\n var id = this.issue.entityIds[0];\n var operation = operationDelete(context, [id]);\n if (!operation.disabled()) {\n operation();\n }\n }\n }));\n } else {\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.connect_features.title')\n }));\n }\n\n return fixes;\n }\n\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.disconnected_way.routable.reference'));\n }\n\n function routingIslandForEntity(entity) {\n\n var routingIsland = new Set(); // the interconnected routable features\n var waysToCheck = []; // the queue of remaining routable ways to traverse\n\n function queueParentWays(node) {\n graph.parentWays(node).forEach(function(parentWay) {\n if (!routingIsland.has(parentWay) && // only check each feature once\n isRoutableWay(parentWay, false)) { // only check routable features\n routingIsland.add(parentWay);\n waysToCheck.push(parentWay);\n }\n });\n }\n\n if (entity.type === 'way' && isRoutableWay(entity, true)) {\n\n routingIsland.add(entity);\n waysToCheck.push(entity);\n\n } else if (entity.type === 'node' && isRoutableNode(entity)) {\n\n routingIsland.add(entity);\n queueParentWays(entity);\n\n } else {\n // this feature isn't routable, cannot be a routing island\n return null;\n }\n\n while (waysToCheck.length) {\n var wayToCheck = waysToCheck.pop();\n var childNodes = graph.childNodes(wayToCheck);\n for (var i in childNodes) {\n var vertex = childNodes[i];\n\n if (isConnectedVertex(vertex)) {\n // found a link to the wider network, not a routing island\n return null;\n }\n\n if (isRoutableNode(vertex)) {\n routingIsland.add(vertex);\n }\n\n queueParentWays(vertex);\n }\n }\n\n // no network link found, this is a routing island, return its members\n return routingIsland;\n }\n\n function isConnectedVertex(vertex) {\n // assume ways overlapping unloaded tiles are connected to the wider road network - #5938\n var osm = services.osm;\n if (osm && !osm.isDataLoaded(vertex.loc)) return true;\n\n // entrances are considered connected\n if (vertex.tags.entrance &&\n vertex.tags.entrance !== 'no') return true;\n if (vertex.tags.amenity === 'parking_entrance') return true;\n\n return false;\n }\n\n function isRoutableNode(node) {\n // treat elevators as distinct features in the highway network\n if (node.tags.highway === 'elevator') return true;\n return false;\n }\n\n function isRoutableWay(way, ignoreInnerWays) {\n if (way.tags.golf === 'path' || way.tags.golf === 'cartpath') {\n // skip golf paths #11863\n return false;\n }\n\n if (isTaggedAsHighway(way) || way.tags.route === 'ferry') return true;\n\n return graph.parentRelations(way).some(function(parentRelation) {\n if (parentRelation.tags.type === 'route' &&\n parentRelation.tags.route === 'ferry') return true;\n\n if (parentRelation.isMultipolygon() &&\n isTaggedAsHighway(parentRelation) &&\n (!ignoreInnerWays || parentRelation.memberById(way.id).role !== 'inner')) return true;\n\n return false;\n });\n }\n\n function makeContinueDrawingFixIfAllowed(textDirection, vertexID, whichEnd) {\n var vertex = graph.hasEntity(vertexID);\n if (!vertex || vertex.tags.noexit === 'yes') return null;\n\n var useLeftContinue = (whichEnd === 'start' && textDirection === 'ltr') ||\n (whichEnd === 'end' && textDirection === 'rtl');\n\n return new validationIssueFix({\n icon: 'iD-operation-continue' + (useLeftContinue ? '-left' : ''),\n title: t.append('issues.fix.continue_from_' + whichEnd + '.title'),\n entityIds: [vertexID],\n onClick: function(context) {\n var wayId = this.issue.entityIds[0];\n var way = context.hasEntity(wayId);\n var vertexId = this.entityIds[0];\n var vertex = context.hasEntity(vertexId);\n\n if (!way || !vertex) return;\n\n // make sure the vertex is actually visible and editable\n var map = context.map();\n if (!context.editable() || !map.trimmedExtent().contains(vertex.loc)) {\n map.zoomToEase(vertex);\n }\n\n context.enter(\n modeDrawLine(context, wayId, context.graph(), 'line', way.affix(vertexId), true)\n );\n }\n });\n }\n\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { osmTimelessFeatureTagValues } from '../osm/tags';\n\nexport function validationMissingStartDate(context) {\n const type = 'missing_start_date';\n\n const validation = function checkMissingStartDate(entity, graph) {\n // If start_date is not empty, return nothing\n if (entity.tags && (entity.tags.start_date || entity.tags['start_date:edtf'])) return [];\n // If entity has no tags, return nothing\n if (Object.keys(entity.tags).length === 0) return [];\n // Rule should be ignored for natural entities and waterways\n if (entity.tags && (\n (entity.tags.natural && osmTimelessFeatureTagValues[entity.tags.natural]) ||\n (entity.tags.waterway && osmTimelessFeatureTagValues[entity.tags.waterway]) ||\n (entity.tags.water && osmTimelessFeatureTagValues[entity.tags.water]))) return [];\n\n // If entity is a vertex node\n var osm = context.connection();\n var isUnloadedNode = entity.type === 'node' && osm && !osm.isDataLoaded(entity.loc);\n\n // Should skip this validation if node is unloaded, is a vertex or has parent relations\n if (isUnloadedNode ||\n // allow untagged nodes that are part of ways\n entity.geometry(graph) === 'vertex' ||\n // allow untagged entities that are part of relations\n entity.hasParentRelations(graph)) return [];\n\n const entityID = entity.id;\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.missing_start_date.reference'));\n }\n\n return [new validationIssue({\n type: type,\n severity: 'warning',\n message: (context) => {\n const entity = context.hasEntity(entityID);\n return entity ? t.append('issues.missing_start_date.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [entityID],\n dynamicFixes: () => {\n return [\n new validationIssueFix({ title: t.append('issues.fix.add_start_date.title')})\n ];\n }\n })];\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { localizer, t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilNormalizeDateString, utilEDTFFromOSMDateString } from '../util';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { actionChangeTags } from '../actions/change_tags';\n\nimport * as edtf from 'edtf';\n\nexport function validationFormatting() {\n var type = 'invalid_format';\n\n var validation = function(entity) {\n var issues = [];\n\n function showReferenceDate(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.invalid_format.date.reference'));\n }\n\n function validateDate(key, msgKey) {\n if (!entity.tags[key]) return;\n var normalized = utilNormalizeDateString(entity.tags[key]);\n if (normalized !== null && entity.tags[key] === normalized.value) return;\n issues.push(new validationIssue({\n type: type,\n subtype: 'date',\n severity: 'error',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.date.message_' + msgKey,\n { feature: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: showReferenceDate,\n entityIds: [entity.id],\n hash: key + entity.tags[key],\n dynamicFixes: function() {\n var fixes = [];\n\n let alternatives = [];\n if (normalized !== null) {\n let label = normalized.date.toLocaleDateString(localizer.localeCodes(), normalized.localeOptions);\n alternatives.push({\n date: normalized.value,\n label: label || normalized.value,\n });\n }\n let edtfFromOSM = utilEDTFFromOSMDateString(entity.tags[key]);\n if (edtfFromOSM) {\n let label;\n try {\n label = edtf.default(edtfFromOSM).format(localizer.localeCode());\n } catch {\n label = edtfFromOSM;\n }\n alternatives.push({\n edtf: edtfFromOSM,\n label: label,\n });\n }\n\n fixes.push(...alternatives.map(alt => new validationIssueFix({\n title: t.append('issues.fix.reformat_date.title', { date: alt.label }),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n if (alt.date) {\n newTags[key] = alt.date;\n } else {\n delete newTags[key];\n }\n newTags[key + ':edtf'] = alt.edtf;\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.reformat_date.annotation'));\n }\n })));\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n delete newTags[key];\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.remove_tag.annotation'));\n }\n }));\n\n return fixes;\n }\n }));\n }\n validateDate('start_date', 'start');\n validateDate('end_date', 'end');\n\n function showReferenceEDTF(selection, parserError) {\n let message;\n if (typeof parserError.offset === 'number' && parserError.token) {\n message = t.append('issues.invalid_format.edtf.reference', {\n token: parserError.token.value,\n position: (parserError.offset + 1).toLocaleString(localizer.localeCodes()),\n });\n } else if (parserError.message) {\n message = selection => selection.append('span')\n .attr('class', 'localized-text')\n .attr('lang', 'en')\n .text(parserError.message.replace(/^edtf: /, ''));\n }\n if (!message) {\n return;\n }\n\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(message);\n }\n\n function validateEDTF(key, msgKey) {\n key += ':edtf';\n if (!entity.tags[key]) return;\n let parserError;\n try {\n edtf.parse(entity.tags[key]);\n return;\n } catch (e) {\n parserError = e;\n }\n issues.push(new validationIssue({\n type: type,\n subtype: 'date',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.edtf.message_' + msgKey,\n { feature: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: selection => showReferenceEDTF(selection, parserError),\n entityIds: [entity.id],\n hash: key + entity.tags[key],\n dynamicFixes: function() {\n var fixes = [];\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n delete newTags[key];\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.remove_tag.annotation'));\n }\n }));\n return fixes;\n }\n }));\n }\n validateEDTF('start_date', 'start');\n validateEDTF('end_date', 'end');\n\n function isValidEmail(email) {\n // Emails in OSM are going to be official so they should be pretty simple\n // Using negated lists to better support all possible unicode characters (#6494)\n var valid_email = /^[^\\(\\)\\\\,\":;<>@\\[\\]]+@[^\\(\\)\\\\,\":;<>@\\[\\]\\.]+(?:\\.[a-z0-9-]+)*$/i;\n\n // An empty value is also acceptable\n return (!email || valid_email.test(email));\n }\n\n function showReferenceEmail(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.invalid_format.email.reference'));\n }\n\n function isValidURL(url, strict = false) {\n try {\n // First try strict WHATWG parsing\n const link = new URL(url);\n return link.href.includes(url);\n } catch {\n if (strict) return false;\n // Fallback: accept if it looks like a valid scheme://something, even if semicolons are present\n return /^https?:\\/\\/\\S+$/i.test(url);\n }\n }\n\n function cleanWikimediaCommonsReference(value) {\n if (!value) return null;\n for (const prefix of ['file', 'datei', 'fichier', 'plik']) {\n if (!value.toLowerCase().startsWith(prefix + ':')) continue;\n return 'File' + decodeURIComponent(value.slice(prefix.length));\n }\n if (value.startsWith('Category:')) return decodeURIComponent(value);\n return null;\n }\n\n function showReferenceWebsite(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.invalid_format.website.reference'));\n }\n\n const websiteValidationIssueBase = {\n type: type,\n subtype: 'website',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.website.message' + (this.data?.count > 1 ? '_multi' : ''),\n { feature: utilDisplayLabel(entity, context.graph()), site: this.data?.value }) : '';\n },\n dynamicFixes: function(context) {\n const wikimedia_commons_reference = cleanWikimediaCommonsReference(this.data?.value);\n const fixes = [{ protocol: 'https', icon: 'temaki-lock' }, { protocol: 'http' }]\n .filter(fix => isValidURL(fix.protocol + '://' + this.data?.value, true))\n .map(fix => new validationIssueFix({\n icon: fix.icon,\n title: t.append('issues.fix.add_protocol_'+ fix.protocol +'.title'),\n onClick: function() {\n const entityID = this.issue.entityIds[0];\n const entity = context.entity(entityID);\n if (!entity) return;\n const key = this.issue.data.key;\n const tags = Object.assign({}, entity.tags);\n tags[key] = entity.tags[key]\n .split(';')\n .map(s => s.trim())\n .map(s => isValidURL(s) ? s : fix.protocol + '://' + s)\n .join(';');\n\n context.perform(\n actionChangeTags(entityID, tags),\n t('issues.fix.add_protocol_'+ fix.protocol +'.annotation')\n );\n }\n }));\n if (this.data?.key === 'image' && !entity.tags.wikimedia_commons && wikimedia_commons_reference) {\n fixes.push(new validationIssueFix({\n icon: 'iD-icon-out-link',\n title: t.append('issues.fix.move_value_to_wikimedia_commons.title'),\n onClick: function() {\n const entityID = this.issue.entityIds[0];\n const entity = context.entity(entityID);\n if (!entity) return;\n const key = this.issue.data.key;\n const tags = Object.assign({}, entity.tags);\n tags.wikimedia_commons = wikimedia_commons_reference;\n delete tags[key];\n\n context.perform(\n actionChangeTags(entityID, tags),\n t('issues.fix.move_value_to_wikimedia_commons.annotation')\n );\n }\n }));\n }\n return fixes;\n },\n reference: showReferenceWebsite,\n entityIds: [entity.id]\n };\n\n Object.entries(entity.tags).map(function([key, tag]) {\n if (!/\\b(website|url)\\b|^image$/i.test(key)) return null;\n if (!tag) return null;\n const value = tag.trim();\n if (!value) return null;\n if (!value.includes(';')) {\n // No semicolon, validate whole value\n if (isValidURL(value)) return null;\n return {\n ...websiteValidationIssueBase,\n data: { key, value },\n hash: key + '=' + value\n };\n }\n const invalidParts = value.split(';').map(s => s.trim()).filter(x => !isValidURL(x));\n if (!invalidParts.length) {\n if (isValidURL(value)) return null;\n // All split parts valid, but whole value still invalid\n return {\n ...websiteValidationIssueBase,\n data: { key, value },\n hash: key + '=' + value\n };\n }\n return {\n ...websiteValidationIssueBase,\n data: { key, value: invalidParts.join(', '), count: invalidParts.length },\n hash: key + '=' + invalidParts.join()\n };\n }).filter(issue => issue !== null).forEach(issueData => issues.push(new validationIssue(issueData)));\n\n if (entity.tags.email) {\n // Multiple emails are possible\n var emails = entity.tags.email\n .split(';')\n .map(function(s) { return s.trim(); })\n .filter(function(x) { return !isValidEmail(x); });\n\n if (emails.length) {\n issues.push(new validationIssue({\n type: type,\n subtype: 'email',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.email.message' + this.data,\n { feature: utilDisplayLabel(entity, context.graph()), email: emails.join(', ') }) : '';\n },\n reference: showReferenceEmail,\n entityIds: [entity.id],\n hash: emails.join(),\n data: (emails.length > 1) ? '_multi' : ''\n }));\n }\n }\n\n return issues;\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationHelpRequest(context) {\n var type = 'help_request';\n\n var validation = function checkFixmeTag(entity) {\n\n if (!entity.tags.fixme) return [];\n\n // don't flag fixmes on features added by the user\n if (entity.version === undefined) return [];\n\n if (entity.v !== undefined) {\n var baseEntity = context.history().base().hasEntity(entity.id);\n // don't flag fixmes added by the user on existing features\n if (!baseEntity || !baseEntity.tags.fixme) return [];\n }\n\n return [new validationIssue({\n type: type,\n subtype: 'fixme_tag',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.fixme_tag.message', {\n feature: utilDisplayLabel(entity, context.graph(), true /* verbose */)\n }) : '';\n },\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n title: t.append('issues.fix.address_the_concern.title')\n })\n ];\n },\n reference: showReference,\n entityIds: [entity.id]\n })];\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.fixme_tag.reference'));\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t, localizer } from '../core/localizer';\nimport { modeDrawLine } from '../modes/draw_line';\nimport { actionReverse } from '../actions/reverse';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { osmFlowingWaterwayTagValues, osmRoutableHighwayTagValues } from '../osm/tags';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\nexport function validationImpossibleOneway() {\n const type = 'impossible_oneway';\n\n const validation = function checkImpossibleOneway(entity, graph) {\n if (entity.type !== 'way' || entity.geometry(graph) !== 'line') return [];\n if (entity.isClosed()) return [];\n if (!typeForWay(entity)) return [];\n if (!entity.isOneWay()) return [];\n\n return [\n ...issuesForNode(entity, entity.first()),\n ...issuesForNode(entity, entity.last())\n ];\n\n function typeForWay(way) {\n if (way.geometry(graph) !== 'line') return null;\n\n if (osmRoutableHighwayTagValues[way.tags.highway]) return 'highway';\n if (osmFlowingWaterwayTagValues[way.tags.waterway]) return 'waterway';\n return null;\n }\n\n function nodeOccursMoreThanOnce(way, nodeID) {\n let occurrences = 0;\n for (const index in way.nodes) {\n if (way.nodes[index] === nodeID) {\n occurrences++;\n if (occurrences > 1) return true;\n }\n }\n return false;\n }\n\n function isConnectedViaOtherTypes(way, node) {\n\n var wayType = typeForWay(way);\n\n if (wayType === 'highway') {\n // entrances are considered connected\n if (node.tags.entrance && node.tags.entrance !== 'no') return true;\n if (node.tags.amenity === 'parking_entrance') return true;\n } else if (wayType === 'waterway') {\n if (node.id === way.first()) {\n // multiple waterways may start at the same spring\n if (node.tags.natural === 'spring') return true;\n } else {\n // multiple waterways may end at the same drain\n if (node.tags.manhole === 'drain') return true;\n }\n }\n\n return graph.parentWays(node).some(function(parentWay) {\n if (parentWay.id === way.id) return false;\n\n if (wayType === 'highway') {\n\n // allow connections to highway areas\n if (parentWay.geometry(graph) === 'area' &&\n osmRoutableHighwayTagValues[parentWay.tags.highway]) return true;\n\n // count connections to ferry routes as connected\n if (parentWay.tags.route === 'ferry') return true;\n\n return graph.parentRelations(parentWay).some(function(parentRelation) {\n if (parentRelation.tags.type === 'route' &&\n parentRelation.tags.route === 'ferry') return true;\n\n // allow connections to highway multipolygons\n return parentRelation.isMultipolygon() && osmRoutableHighwayTagValues[parentRelation.tags.highway];\n });\n } else if (wayType === 'waterway') {\n // multiple waterways may start or end at a water body at the same node\n if (parentWay.tags.natural === 'water' ||\n parentWay.tags.natural === 'coastline') return true;\n }\n return false;\n });\n }\n\n function issuesForNode(way, nodeID) {\n const isFirst = (nodeID === way.first()) ^ way.isOneWayBackwards();\n const wayType = typeForWay(way);\n\n // ignore if this way is self-connected at this node\n if (nodeOccursMoreThanOnce(way, nodeID)) return [];\n\n const osm = services.osm;\n if (!osm) return [];\n const node = graph.hasEntity(nodeID);\n // ignore if this node or its tile are unloaded\n if (!node || !osm.isDataLoaded(node.loc)) return [];\n\n if (isConnectedViaOtherTypes(way, node)) return [];\n\n const attachedWaysOfSameType = graph.parentWays(node).filter(parentWay => {\n if (parentWay.id === way.id) return false;\n return typeForWay(parentWay) === wayType;\n });\n\n // assume it's okay for waterways to start or end disconnected for now\n if (wayType === 'waterway' && attachedWaysOfSameType.length === 0) return [];\n\n const attachedOneways = attachedWaysOfSameType\n .filter(attachedWay => attachedWay.isOneWay());\n\n // ignore if the way is connected to some non-oneway features\n if (attachedOneways.length < attachedWaysOfSameType.length) return [];\n\n if (attachedOneways.length) {\n const connectedEndpointsOkay = attachedOneways.some(attachedOneway => {\n const isAttachedBackwards = attachedOneway.isOneWayBackwards();\n if ((isFirst ^ isAttachedBackwards\n ? attachedOneway.first()\n : attachedOneway.last()\n ) !== nodeID) {\n return true;\n }\n if (nodeOccursMoreThanOnce(attachedOneway, nodeID)) return true;\n return false;\n });\n if (connectedEndpointsOkay) return [];\n }\n\n const placement = isFirst ? 'start' : 'end';\n let messageID = wayType + '.';\n let referenceID = wayType + '.';\n\n if (wayType === 'waterway') {\n messageID += 'connected.' + placement;\n referenceID += 'connected';\n } else {\n messageID += placement;\n referenceID += placement;\n }\n\n return [new validationIssue({\n type: type,\n subtype: wayType,\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.impossible_oneway.' + messageID + '.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: getReference(referenceID),\n entityIds: [way.id, node.id],\n dynamicFixes: function() {\n\n var fixes = [];\n\n if (attachedOneways.length) {\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-reverse',\n title: t.append('issues.fix.reverse_feature.title'),\n entityIds: [way.id],\n onClick: function(context) {\n var id = this.issue.entityIds[0];\n context.perform(actionReverse(id), t('operations.reverse.annotation.line', { n: 1 }));\n }\n }));\n }\n if (node.tags.noexit !== 'yes') {\n var textDirection = localizer.textDirection();\n var useLeftContinue = (isFirst && textDirection === 'ltr') ||\n (!isFirst && textDirection === 'rtl');\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-continue' + (useLeftContinue ? '-left' : ''),\n title: t.append('issues.fix.continue_from_' + (isFirst ? 'start' : 'end') + '.title'),\n onClick: function(context) {\n var entityID = this.issue.entityIds[0];\n var vertexID = this.issue.entityIds[1];\n var way = context.entity(entityID);\n var vertex = context.entity(vertexID);\n continueDrawing(way, vertex, context);\n }\n }));\n }\n\n return fixes;\n },\n loc: node.loc\n })];\n\n function getReference(referenceID) {\n return function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.impossible_oneway.' + referenceID + '.reference'));\n };\n }\n }\n };\n\n function continueDrawing(way, vertex, context) {\n // make sure the vertex is actually visible and editable\n var map = context.map();\n if (!context.editable() || !map.trimmedExtent().contains(vertex.loc)) {\n map.zoomToEase(vertex);\n }\n\n context.enter(\n modeDrawLine(context, way.id, context.graph(), 'line', way.affix(vertex.id), true)\n );\n }\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\nconst incompatibleRules = [\n {\n id: 'amap',\n regex: /(^amap$|^amap\\.com|autonavi|mapabc|\u9AD8\u5FB7)/i\n },\n {\n id: 'baidu',\n regex: /(baidu|mapbar|\u767E\u5EA6)/i\n },\n {\n id: 'google',\n regex: /(google)/i,\n exceptRegex: /((books|drive)\\.google|google\\s?(books|drive|plus))|(esri\\/Google_(Africa|Open)_Buildings)|(:\\/\\/\\S+\\/\\S+(google)\\S+)/i\n }\n];\n\n/**\n * @param {string} str String (e.g. tag value) to check for incompatible sources\n * @returns {{id:string, regex: RegExp, exceptRegex?: RegExp}[]}\n */\nexport function getIncompatibleSources(str) {\n return incompatibleRules\n .filter(rule =>\n rule.regex.test(str) &&\n !rule.exceptRegex?.test(str)\n );\n}\n\nexport function validationIncompatibleSource() {\n const type = 'incompatible_source';\n\n const validation = function checkIncompatibleSource(entity) {\n const entitySources = entity.tags && entity.tags.source && entity.tags.source.split(';');\n if (!entitySources) return [];\n\n const entityID = entity.id;\n\n return entitySources\n .flatMap(source => getIncompatibleSources(source)\n .map(matchRule => new validationIssue({\n type: type,\n severity: 'warning',\n message: (context) => {\n const entity = context.hasEntity(entityID);\n return entity ? t.append('issues.incompatible_source.feature.message', {\n feature: utilDisplayLabel(entity, context.graph(), true /* verbose */),\n value: source\n }) : '';\n },\n reference: getReference(matchRule.id),\n entityIds: [entityID],\n hash: source,\n dynamicFixes: () => {\n return [\n new validationIssueFix({ title: t.append('issues.fix.remove_proprietary_data.title') })\n ];\n }\n }))\n );\n\n function getReference(id) {\n return function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append(`issues.incompatible_source.reference.${id}`));\n };\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { services } from '../services';\n\n\nexport function validationMaprules() {\n var type = 'maprules';\n\n var validation = function checkMaprules(entity, graph) {\n if (!services.maprules) return [];\n\n var rules = services.maprules.validationRules();\n var issues = [];\n\n for (var i = 0; i < rules.length; i++) {\n var rule = rules[i];\n rule.findIssues(entity, graph, issues);\n }\n\n return issues;\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { localizer, t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilNormalizeDateString } from '../util/ohm_date';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\nimport * as edtf from 'edtf';\n\nexport function validationMismatchedDates() {\n let type = 'mismatched_dates';\n\n function parseEDTF(value) {\n try {\n let parsed = edtf.default(value);\n\n // According to edtf.js, an extended interval with an unknown start or end covers no date.\n // This isn't useful for the purpose of testing whether the basic date matches, so treat it as an unspecified start or end.\n if (parsed.lower === null) {\n parsed.lower = Infinity;\n }\n if (parsed.upper === null) {\n parsed.upper = Infinity;\n }\n\n return parsed;\n } catch {\n // Already handled by invalid_format rule.\n return;\n }\n }\n\n function getReplacementDates(parsed) {\n let likelyDates = new Set();\n\n let valueFromDate = (date, precision) => {\n date.precision = precision;\n return date.edtf.split('T')[0];\n };\n\n if (Number.isFinite(parsed.min)) {\n let min = edtf.default(parsed.min);\n let precision = (parsed.lower || parsed.first || parsed).precision;\n likelyDates.add(valueFromDate(min, precision));\n }\n\n if (Number.isFinite(parsed.max)) {\n let max = edtf.default(parsed.max);\n let precision = (parsed.upper || parsed.last || parsed).precision;\n likelyDates.add(valueFromDate(max, precision));\n }\n\n let sortedDates = [...likelyDates];\n sortedDates.sort();\n return sortedDates;\n }\n\n let validation = function(entity) {\n let issues = [];\n\n function showReferenceEDTF(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.mismatched_dates.edtf.reference'));\n }\n\n function getDynamicFixes(key, parsed) {\n let fixes = [];\n\n let replacementDates = getReplacementDates(parsed);\n fixes.push(...replacementDates.map(value => {\n let normalized = utilNormalizeDateString(value);\n let localeDateString = normalized.date.toLocaleDateString(localizer.languageCode(), normalized.localeOptions);\n return new validationIssueFix({\n title: t.append('issues.fix.reformat_date.title', { date: localeDateString }),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n newTags[key] = normalized.value;\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.reformat_date.annotation'));\n }\n });\n }));\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n delete newTags[key];\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.remove_tag.annotation'));\n }\n }));\n\n return fixes;\n }\n\n function validateEDTF(key, msgKey) {\n if (!entity.tags[key] || !entity.tags[key + ':edtf']) return;\n let basic = entity.tags[key];\n let basicAsEDTF = parseEDTF(basic);\n let parsed = parseEDTF(entity.tags[key + ':edtf']);\n if (!basicAsEDTF || !parsed || parsed.covers(basicAsEDTF) || basicAsEDTF.covers(parsed)) return;\n\n // start_date and end_date disallow time precision. Transform the basic date into a daylong range in EDTF to allow a comparison to an EDTF date with time precision.\n // https://github.com/OpenHistoricalMap/issues/issues/764\n if (basic.match('^-?[0-9]+-[0-9]{2}-[0-9]{2}$')) {\n let basicTime = parseEDTF(`${basic}T00:00:00/${basic}T24:00:00`);\n if (basicTime && (parsed.covers(basicTime) || basicTime.covers(parsed))) return;\n }\n\n issues.push(new validationIssue({\n type: type,\n subtype: 'date',\n severity: 'warning',\n message: function(context) {\n let entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.mismatched_dates.edtf.message_' + msgKey,\n { feature: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: showReferenceEDTF,\n entityIds: [entity.id],\n hash: key + entity.tags[key + ':edtf'],\n dynamicFixes: () => getDynamicFixes(key, parsed),\n }));\n }\n validateEDTF('start_date', 'start');\n validateEDTF('end_date', 'end');\n\n return issues;\n };\n\n validation.type = type;\n validation.parseEDTF = parseEDTF;\n validation.getReplacementDates = getReplacementDates;\n\n return validation;\n}\n", "import { deepEqual } from 'fast-equals';\n\nimport { actionAddVertex } from '../actions/add_vertex';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionMergeNodes } from '../actions/merge_nodes';\nimport { actionExtract } from '../actions/extract';\nimport { modeSelect } from '../modes/select';\nimport { osmJoinWays } from '../osm/multipolygon';\nimport { osmNodeGeometriesForTags, osmTagSuggestingArea } from '../osm/tags';\nimport { presetManager } from '../presets';\nimport { geoHasSelfIntersections, geoSphericalDistance } from '../geo';\nimport { t } from '../core/localizer';\nimport { utilTagText } from '../util';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationMismatchedGeometry() {\n var type = 'mismatched_geometry';\n\n function tagSuggestingLineIsArea(entity) {\n if (entity.type !== 'way' || entity.isClosed()) return null;\n\n var tagSuggestingArea = entity.tagSuggestingArea();\n\n if (!tagSuggestingArea) {\n return null;\n }\n\n var asLine = presetManager.matchTags(tagSuggestingArea, 'line');\n var asArea = presetManager.matchTags(tagSuggestingArea, 'area');\n if (asLine && asArea && deepEqual(asLine.tags, asArea.tags)) {\n // this tag also allows lines and making this an area wouldn't matter\n return null;\n }\n\n if (asLine.isFallback() && asArea.isFallback() && !deepEqual(tagSuggestingArea, { area: 'yes' })) {\n // if the entity matches the fallback preset, regardless of the\n // geometry, then changing the geometry will not help.\n return null;\n }\n\n return tagSuggestingArea;\n }\n\n\n function makeConnectEndpointsFixOnClick(way, graph) {\n // must have at least three nodes to close this automatically\n if (way.nodes.length < 3) return null;\n\n var nodes = graph.childNodes(way), testNodes;\n var firstToLastDistanceMeters = geoSphericalDistance(nodes[0].loc, nodes[nodes.length-1].loc);\n\n // if the distance is very small, attempt to merge the endpoints\n if (firstToLastDistanceMeters < 0.75) {\n testNodes = nodes.slice(); // shallow copy\n testNodes.pop();\n testNodes.push(testNodes[0]);\n // make sure this will not create a self-intersection\n if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {\n return function(context) {\n var way = context.entity(this.issue.entityIds[0]);\n context.perform(\n actionMergeNodes([way.nodes[0], way.nodes[way.nodes.length-1]], nodes[0].loc),\n t('issues.fix.connect_endpoints.annotation')\n );\n };\n }\n }\n\n // if the points were not merged, attempt to close the way\n testNodes = nodes.slice(); // shallow copy\n testNodes.push(testNodes[0]);\n // make sure this will not create a self-intersection\n if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {\n return function(context) {\n var wayId = this.issue.entityIds[0];\n var way = context.entity(wayId);\n var nodeId = way.nodes[0];\n var index = way.nodes.length;\n context.perform(\n actionAddVertex(wayId, nodeId, index),\n t('issues.fix.connect_endpoints.annotation')\n );\n };\n }\n }\n\n function lineTaggedAsAreaIssue(entity) {\n\n var tagSuggestingArea = tagSuggestingLineIsArea(entity);\n if (!tagSuggestingArea) return null;\n\n var validAsLine = false;\n var presetAsLine = presetManager.matchTags(entity.tags, 'line');\n if (presetAsLine) {\n validAsLine = true;\n var key = Object.keys(tagSuggestingArea)[0];\n if (presetAsLine.tags[key] && presetAsLine.tags[key] === '*') {\n // only matches a fallback preset of the tag which is suggesting to be an area\n validAsLine = false;\n }\n if (Object.keys(presetAsLine.tags).length === 0) {\n // only matches the fallback \"line\" preset\n validAsLine = false;\n }\n }\n\n return new validationIssue({\n type: type,\n subtype: 'area_as_line',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.tag_suggests_area.message', {\n feature: utilDisplayLabel(entity, 'area', true /* verbose */),\n tag: utilTagText({ tags: tagSuggestingArea })\n }) : '';\n },\n reference: showReference,\n entityIds: [entity.id],\n hash: JSON.stringify(tagSuggestingArea),\n dynamicFixes: function(context) {\n\n var fixes = [];\n\n var entity = context.entity(this.entityIds[0]);\n var connectEndsOnClick = makeConnectEndpointsFixOnClick(entity, context.graph());\n\n if (!validAsLine) {\n // only suggest to \"connect the ends\" if the feature is not also valid as a line\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.connect_endpoints.title'),\n onClick: connectEndsOnClick\n }));\n }\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n var entityId = this.issue.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n for (var key in tagSuggestingArea) {\n delete tags[key];\n }\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.remove_tag.annotation')\n );\n }\n }));\n\n return fixes;\n }\n });\n\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.tag_suggests_area.reference'));\n }\n }\n\n function vertexPointIssue(entity, graph) {\n // we only care about nodes\n if (entity.type !== 'node') return null;\n\n // ignore tagless points\n if (Object.keys(entity.tags).length === 0) return null;\n\n // address lines are special so just ignore them\n if (entity.isOnAddressLine(graph)) return null;\n\n var geometry = entity.geometry(graph);\n var allowedGeometries = osmNodeGeometriesForTags(entity.tags);\n\n if (geometry === 'point' && !allowedGeometries.point && allowedGeometries.vertex) {\n\n return new validationIssue({\n type: type,\n subtype: 'vertex_as_point',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.vertex_as_point.message', {\n feature: utilDisplayLabel(entity, 'vertex', true /* verbose */)\n }) : '';\n },\n reference: function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.vertex_as_point.reference'));\n },\n entityIds: [entity.id],\n dynamicFixes: () => [new validationIssueFix({\n icon: 'iD-operation-move',\n title: t.append('issues.fix.reposition_point_to_vertex.title')\n })]\n });\n\n } else if (geometry === 'vertex' && !allowedGeometries.vertex && allowedGeometries.point) {\n\n return new validationIssue({\n type: type,\n subtype: 'point_as_vertex',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.point_as_vertex.message', {\n feature: utilDisplayLabel(entity, 'point', true /* verbose */)\n }) : '';\n },\n reference: function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.point_as_vertex.reference'));\n },\n entityIds: [entity.id],\n dynamicFixes: extractPointDynamicFixes\n });\n }\n\n return null;\n }\n\n\n function otherMismatchIssue(entity, graph) {\n // ignore boring features\n if (!entity.hasInterestingTags()) return null;\n\n if (entity.type !== 'node' && entity.type !== 'way') return null;\n\n // address lines are special so just ignore them\n if (entity.type === 'node' && entity.isOnAddressLine(graph)) return null;\n\n var sourceGeom = entity.geometry(graph);\n\n var targetGeoms = entity.type === 'way' ? ['point', 'vertex'] : ['line', 'area'];\n\n if (sourceGeom === 'area') targetGeoms.unshift('line');\n\n var asSource = presetManager.match(entity, graph);\n\n const originalTargetGeom = targetGeoms.find(nodeGeom => {\n const asTarget = presetManager.matchTags(\n entity.tags,\n nodeGeom,\n entity.extent(graph).center(),\n );\n if (!asSource || !asTarget ||\n asSource === asTarget ||\n // sometimes there are two presets with the same tags for different geometries\n deepEqual(asSource.tags, asTarget.tags)) return false;\n\n if (asTarget.isFallback()) return false;\n\n var primaryKey = Object.keys(asTarget.tags)[0];\n\n // special case: buildings-as-points are discouraged by iD, but common in OSM, so ignore them\n if (primaryKey === 'building') return false;\n\n if (asTarget.tags[primaryKey] === '*') return false;\n\n return asSource.isFallback() || asSource.tags[primaryKey] === '*';\n });\n\n let targetGeom = originalTargetGeom;\n\n if (!targetGeom) return null;\n\n var subtype = targetGeom + '_as_' + sourceGeom;\n\n if (targetGeom === 'vertex') targetGeom = 'point';\n if (sourceGeom === 'vertex') sourceGeom = 'point';\n\n var referenceId = targetGeom + '_as_' + sourceGeom;\n\n var dynamicFixes;\n if (targetGeom === 'point') {\n dynamicFixes = extractPointDynamicFixes;\n\n } else if (sourceGeom === 'area' && targetGeom === 'line') {\n dynamicFixes = lineToAreaDynamicFixes;\n }\n\n return new validationIssue({\n type: type,\n subtype: subtype,\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.' + referenceId + '.message', {\n feature: utilDisplayLabel(entity, originalTargetGeom, true /* verbose */)\n }) : '';\n },\n reference: function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.mismatched_geometry.reference'));\n },\n entityIds: [entity.id],\n dynamicFixes: dynamicFixes\n });\n }\n\n function lineToAreaDynamicFixes(context) {\n\n var convertOnClick;\n\n var entityId = this.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n delete tags.area;\n if (!osmTagSuggestingArea(tags)) {\n // if removing the area tag would make this a line, offer that as a quick fix\n convertOnClick = function(context) {\n var entityId = this.issue.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n if (tags.area) {\n delete tags.area;\n }\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.convert_to_line.annotation')\n );\n };\n }\n\n return [\n new validationIssueFix({\n icon: 'iD-icon-line',\n title: t.append('issues.fix.convert_to_line.title'),\n onClick: convertOnClick\n })\n ];\n }\n\n function extractPointDynamicFixes(context) {\n\n var entityId = this.entityIds[0];\n\n var extractOnClick = null;\n if (!context.hasHiddenConnections(entityId)) {\n\n extractOnClick = function(context) {\n var entityId = this.issue.entityIds[0];\n var action = actionExtract(entityId, context.projection);\n context.perform(\n action,\n t('operations.extract.annotation', { n: 1 })\n );\n // re-enter mode to trigger updates\n context.enter(modeSelect(context, [action.getExtractedNodeID()]));\n };\n }\n\n return [\n new validationIssueFix({\n icon: 'iD-operation-extract',\n title: t.append('issues.fix.extract_point.title'),\n onClick: extractOnClick\n })\n ];\n }\n\n function unclosedMultipolygonPartIssues(entity, graph) {\n\n if (entity.type !== 'relation' ||\n !entity.isMultipolygon() ||\n entity.isDegenerate() ||\n // cannot determine issues for incompletely-downloaded relations\n !entity.isComplete(graph)) return [];\n\n var sequences = osmJoinWays(entity.members, graph);\n\n var issues = [];\n\n for (var i in sequences) {\n var sequence = sequences[i];\n\n if (!sequence.nodes) continue;\n\n var firstNode = sequence.nodes[0];\n var lastNode = sequence.nodes[sequence.nodes.length - 1];\n\n // part is closed if the first and last nodes are the same\n if (firstNode === lastNode) continue;\n\n var issue = new validationIssue({\n type: type,\n subtype: 'unclosed_multipolygon_part',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.unclosed_multipolygon_part.message', {\n feature: utilDisplayLabel(entity, context.graph(), true /* verbose */)\n }) : '';\n },\n reference: showReference,\n loc: sequence.nodes[0].loc,\n entityIds: [entity.id],\n hash: sequence.map(function(way) {\n return way.id;\n }).join()\n });\n issues.push(issue);\n }\n\n return issues;\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.unclosed_multipolygon_part.reference'));\n }\n }\n\n var validation = function checkMismatchedGeometry(entity, graph) {\n var vertexPoint = vertexPointIssue(entity, graph);\n if (vertexPoint) return [vertexPoint];\n\n var lineAsArea = lineTaggedAsAreaIssue(entity);\n if (lineAsArea) return [lineAsArea];\n\n var mismatch = otherMismatchIssue(entity, graph);\n if (mismatch) return [mismatch];\n\n return unclosedMultipolygonPartIssues(entity, graph);\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeMember } from '../actions/change_member';\nimport { actionDeleteMember } from '../actions/delete_member';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationMissingRole() {\n var type = 'missing_role';\n\n var validation = function checkMissingRole(entity, graph) {\n var issues = [];\n if (entity.type === 'way') {\n graph.parentRelations(entity).forEach(function(relation) {\n if (!relation.isMultipolygon()) return;\n\n var member = relation.memberById(entity.id);\n if (member && isMissingRole(member)) {\n issues.push(makeIssue(entity, relation, member));\n }\n });\n } else if (entity.type === 'relation' && entity.isMultipolygon()) {\n entity.indexedMembers().forEach(function(member) {\n var way = graph.hasEntity(member.id);\n if (way && isMissingRole(member)) {\n issues.push(makeIssue(way, entity, member));\n }\n });\n }\n\n return issues;\n };\n\n\n function isMissingRole(member) {\n return !member.role || !member.role.trim().length;\n }\n\n\n function makeIssue(way, relation, member) {\n return new validationIssue({\n type: type,\n severity: 'warning',\n message: function(context) {\n const member = context.hasEntity(this.entityIds[0]),\n relation = context.hasEntity(this.entityIds[1]);\n return (member && relation) ? t.append('issues.missing_role.message', {\n member: utilDisplayLabel(member, context.graph()),\n relation: utilDisplayLabel(relation, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [way.id, relation.id],\n extent: function(graph) {\n return graph.entity(this.entityIds[0]).extent(graph);\n },\n data: {\n member: member\n },\n hash: member.index.toString(),\n dynamicFixes: function() {\n return [\n makeAddRoleFix('inner'),\n makeAddRoleFix('outer'),\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_from_relation.title'),\n onClick: function(context) {\n context.perform(\n actionDeleteMember(this.issue.entityIds[1], this.issue.data.member.index),\n t('operations.delete_member.annotation', {\n n: 1\n })\n );\n }\n })\n ];\n }\n });\n\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.missing_role.multipolygon.reference'));\n }\n }\n\n\n function makeAddRoleFix(role) {\n return new validationIssueFix({\n title: t.append('issues.fix.set_as_' + role + '.title'),\n onClick: function(context) {\n var oldMember = this.issue.data.member;\n var member = { id: this.issue.entityIds[0], type: oldMember.type, role: role };\n context.perform(\n actionChangeMember(this.issue.entityIds[1], member, oldMember.index),\n t('operations.change_role.annotation', {\n n: 1\n })\n );\n }\n });\n }\n\n validation.type = type;\n\n return validation;\n}\n", "import { operationDelete } from '../operations/delete';\nimport { osmIsInterestingTag } from '../osm/tags';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationMissingTag(context) {\n var type = 'missing_tag';\n\n function hasDescriptiveTags(entity) {\n var onlyAttributeKeys = ['description', 'name', 'start_date', 'oneway'];\n var entityDescriptiveKeys = Object.keys(entity.tags)\n .filter(function(k) {\n if (k === 'area' || !osmIsInterestingTag(k)) return false;\n\n return !onlyAttributeKeys.some(function(attributeKey) {\n return k === attributeKey || k.indexOf(attributeKey + ':') === 0;\n });\n });\n\n if (entity.type === 'relation' &&\n entityDescriptiveKeys.length === 1 &&\n entity.tags.type === 'multipolygon') {\n // this relation's only interesting tag just says its a multipolygon,\n // which is not descriptive enough\n return false;\n }\n\n return entityDescriptiveKeys.length > 0;\n }\n\n function isUnknownRoad(entity) {\n return entity.type === 'way' && entity.tags.highway === 'road';\n }\n\n function isUntypedRelation(entity) {\n return entity.type === 'relation' && !entity.tags.type;\n }\n\n var validation = function checkMissingTag(entity, graph) {\n\n var subtype;\n\n var osm = context.connection();\n var isUnloadedNode = entity.type === 'node' && osm && !osm.isDataLoaded(entity.loc);\n\n // we can't know if the node is a vertex if the tile is undownloaded\n if (!isUnloadedNode &&\n // allow untagged nodes that are part of ways\n entity.geometry(graph) !== 'vertex' &&\n // allow untagged entities that are part of relations\n !entity.hasParentRelations(graph)) {\n\n if (Object.keys(entity.tags).length === 0) {\n subtype = 'any';\n } else if (!hasDescriptiveTags(entity)) {\n subtype = 'descriptive';\n } else if (isUntypedRelation(entity)) {\n subtype = 'relation_type';\n }\n }\n\n // flag an unknown road even if it's a member of a relation\n if (!subtype && isUnknownRoad(entity)) {\n subtype = 'highway_classification';\n }\n\n if (!subtype) return [];\n\n var messageID = subtype === 'highway_classification' ? 'unknown_road' : 'missing_tag.' + subtype;\n var referenceID = subtype === 'highway_classification' ? 'unknown_road' : 'missing_tag';\n\n // can always delete if the user created it in the first place..\n var canDelete = (entity.version === undefined || entity.v !== undefined);\n var severity = (canDelete && subtype !== 'highway_classification') ? 'error' : 'warning';\n\n return [new validationIssue({\n type: type,\n subtype: subtype,\n severity: severity,\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.' + messageID + '.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [entity.id],\n dynamicFixes: function(context) {\n\n var fixes = [];\n\n var selectFixType = subtype === 'highway_classification' ? 'select_road_type' : 'select_preset';\n\n fixes.push(new validationIssueFix({\n icon: 'iD-icon-search',\n title: t.append('issues.fix.' + selectFixType + '.title'),\n onClick: function(context) {\n context.ui().sidebar.showPresetList();\n }\n }));\n\n var deleteOnClick;\n\n var id = this.entityIds[0];\n var operation = operationDelete(context, [id]);\n var disabledReasonID = operation.disabled();\n if (!disabledReasonID) {\n deleteOnClick = function(context) {\n var id = this.issue.entityIds[0];\n var operation = operationDelete(context, [id]);\n if (!operation.disabled()) {\n operation();\n }\n };\n }\n\n fixes.push(\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.delete_feature.title'),\n disabledReason: disabledReasonID ? t('operations.delete.' + disabledReasonID + '.single') : undefined,\n onClick: deleteOnClick\n })\n );\n\n return fixes;\n }\n })];\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.' + referenceID + '.reference'));\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { osmMutuallyExclusiveTagPairs } from '../osm/tags';\n\nexport function validationMutuallyExclusiveTags(/* context */) {\n const type = 'mutually_exclusive_tags';\n\n // https://wiki.openstreetmap.org/wiki/Special:WhatLinksHere/Property:P44\n const tagKeyPairs = osmMutuallyExclusiveTagPairs;\n\n const validation = function checkMutuallyExclusiveTags(entity /*, graph */) {\n\n let pairsFounds = tagKeyPairs.filter((pair) => {\n return (pair[0] in entity.tags && pair[1] in entity.tags);\n }).filter((pair) => {\n // noname=no is double-negation, thus positive and not conflicting. We'll ignore those\n return !((pair[0].match(/^(addr:)?no[a-z]/) && entity.tags[pair[0]] === 'no') ||\n (pair[1].match(/^(addr:)?no[a-z]/) && entity.tags[pair[1]] === 'no'));\n });\n\n // Additional:\n // Check if name and not:name (and similar) are set and both have the same value\n // not:name can actually have multiple values, separate by ;\n // https://taginfo.openstreetmap.org/search?q=not%3A#keys\n Object.keys(entity.tags).forEach((key) => {\n let negative_key = 'not:' + key;\n if (negative_key in entity.tags && entity.tags[negative_key].split(';').includes(entity.tags[key])) {\n pairsFounds.push([negative_key, key, 'same_value']);\n }\n // For name:xx we also compare against the not:name tag\n if (key.match(/^name:[a-z]+/)) {\n negative_key = 'not:name';\n if (negative_key in entity.tags && entity.tags[negative_key].split(';').includes(entity.tags[key])) {\n pairsFounds.push([negative_key, key, 'same_value']);\n }\n }\n });\n\n let issues = pairsFounds.map((pair) => {\n const subtype = pair[2] || 'default';\n return new validationIssue({\n type: type,\n subtype: subtype,\n severity: 'warning',\n message: function(context) {\n let entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append(`issues.${type}.${subtype}.message`, {\n feature: utilDisplayLabel(entity, context.graph()),\n tag1: pair[0],\n tag2: pair[1]\n }) : '';\n },\n reference: (selection) => showReference(selection, pair, subtype),\n entityIds: [entity.id],\n dynamicFixes: () => pair.slice(0,2).map((tagToRemove) => createIssueFix(tagToRemove))\n });\n });\n\n function createIssueFix(tagToRemove) {\n return new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_named_tag.title', { tag: tagToRemove }),\n onClick: function(context) {\n const entityId = this.issue.entityIds[0];\n const entity = context.entity(entityId);\n let tags = Object.assign({}, entity.tags); // shallow copy\n delete tags[tagToRemove];\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.remove_named_tag.annotation', { tag: tagToRemove })\n );\n }\n });\n }\n\n function showReference(selection, pair, subtype) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append(`issues.${type}.${subtype}.reference`, { tag1: pair[0], tag2: pair[1] }));\n }\n\n return issues;\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { actionSplit } from '../actions/split';\nimport { behaviorOperation } from '../behavior/operation';\nimport { modeSelect } from '../modes/select';\n\n\nexport function operationSplit(context, selectedIDs) {\n var _vertexIds = selectedIDs.filter(function(id) {\n return context.graph().geometry(id) === 'vertex';\n });\n var _selectedWayIds = selectedIDs.filter(function(id) {\n var entity = context.graph().hasEntity(id);\n return entity && entity.type === 'way';\n });\n var _isAvailable = _vertexIds.length > 0 &&\n _vertexIds.length + _selectedWayIds.length === selectedIDs.length;\n var _action = actionSplit(_vertexIds);\n var _ways = [];\n var _geometry = 'feature';\n var _waysAmount = 'single';\n var _nodesAmount = _vertexIds.length === 1 ? 'single' : 'multiple';\n\n if (_isAvailable) {\n if (_selectedWayIds.length) _action.limitWays(_selectedWayIds);\n _ways = _action.ways(context.graph());\n var geometries = {};\n _ways.forEach(function(way) {\n geometries[way.geometry(context.graph())] = true;\n });\n if (Object.keys(geometries).length === 1) {\n _geometry = Object.keys(geometries)[0];\n }\n _waysAmount = _ways.length === 1 ? 'single' : 'multiple';\n }\n\n\n var operation = function() {\n var difference = context.perform(_action, operation.annotation());\n // select both the nodes and the ways so the mapper can immediately disconnect them if desired\n var idsToSelect = _vertexIds.concat(difference.extantIDs().filter(function(id) {\n // filter out relations that may have had member additions\n return context.entity(id).type === 'way';\n }));\n context.enter(modeSelect(context, idsToSelect));\n };\n\n\n operation.relatedEntityIds = function() {\n return _selectedWayIds.length ? [] : _ways.map(way => way.id);\n };\n\n\n operation.available = function() {\n return _isAvailable;\n };\n\n\n operation.disabled = function() {\n var reason = _action.disabled(context.graph());\n if (reason) {\n return reason;\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n }\n return false;\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.split.' + disable) :\n t.append('operations.split.description.' + _geometry + '.' + _waysAmount + '.' + _nodesAmount + '_node');\n };\n\n\n operation.annotation = function() {\n return t('operations.split.annotation.' + _geometry, { n: _ways.length });\n };\n\n\n operation.icon = function() {\n if (_waysAmount === 'multiple') {\n return '#iD-operation-split-multiple';\n } else {\n return '#iD-operation-split';\n }\n };\n\n\n operation.id = 'split';\n operation.keys = [t('operations.split.key')];\n operation.title = t.append('operations.split.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import { t } from '../core/localizer';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { operationSplit } from '../operations/split';\n\nexport function validationOsmApiLimits(context) {\n const type = 'osm_api_limits';\n\n const validation = function checkOsmApiLimits(entity) {\n const issues = [];\n const osm = context.connection();\n if (!osm) return issues; // cannot check if there is no connection to the osm api, e.g. during unit tests\n const maxWayNodes = osm.maxWayNodes();\n\n if (entity.type === 'way') {\n if (entity.nodes.length > maxWayNodes) {\n issues.push(new validationIssue({\n type: type,\n subtype: 'exceededMaxWayNodes',\n severity: 'error',\n message: function() {\n return t.append('issues.osm_api_limits.max_way_nodes.message');\n },\n reference: function(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.osm_api_limits.max_way_nodes.reference', { maxWayNodes }));\n },\n entityIds: [entity.id],\n dynamicFixes: splitWayIntoSmallChunks\n }));\n }\n }\n\n return issues;\n };\n\n function splitWayIntoSmallChunks() {\n const fix = new validationIssueFix({\n icon: 'iD-operation-split',\n title: t.append('issues.fix.split_way.title'),\n entityIds: this.entityIds,\n onClick: function(context) {\n const maxWayNodes = context.connection().maxWayNodes();\n const g = context.graph();\n\n const entityId = this.entityIds[0];\n const entity = context.graph().entities[entityId];\n const numberOfParts = Math.ceil(entity.nodes.length / maxWayNodes);\n let splitVertices;\n\n if (numberOfParts === 2) {\n // simple case: try to split at the an intersection vertex\n const splitIntersections = entity.nodes\n .map(nid => g.entity(nid))\n .filter(n => g.parentWays(n).length > 1)\n .map(n => n.id)\n .filter(nid => {\n const splitIndex = entity.nodes.indexOf(nid);\n return splitIndex < maxWayNodes &&\n entity.nodes.length - splitIndex < maxWayNodes;\n });\n if (splitIntersections.length > 0) {\n splitVertices = [\n splitIntersections[Math.floor(splitIntersections.length / 2)]\n ];\n }\n }\n\n if (splitVertices === undefined) {\n // general case: either more than one split is needed or no possible\n // intersection split point was found -> just split at regular intervals\n splitVertices = [...Array(numberOfParts - 1)].map((_, i) =>\n entity.nodes[Math.floor(entity.nodes.length * (i + 1) / numberOfParts)]);\n }\n\n if (entity.isClosed()) {\n // add extra split for closed ways at start of way\n splitVertices.push(entity.nodes[0]);\n }\n\n const operation = operationSplit(context, splitVertices.concat(entityId));\n if (!operation.disabled()) {\n operation();\n }\n }\n });\n\n return [fix];\n }\n\n\n validation.type = type;\n\n return validation;\n}\n", "/** @typedef {{ old: Tags; replace?: Tags }[]} DataDeprecated */\n\n/** @param {Tags} tags @param {DataDeprecated} dataDeprecated */\nexport function getDeprecatedTags(tags, dataDeprecated) {\n // if there are no tags, none can be deprecated\n if (Object.keys(tags).length === 0) return [];\n\n /** @type {DataDeprecated} */\n var deprecated = [];\n dataDeprecated.forEach((d) => {\n const oldKeys = Object.keys(d.old);\n const transferKeys = oldKeys.filter(key => d.old[key] === '*');\n if (d.replace) {\n var hasExistingValues = Object.keys(d.replace).some((replaceKey) => {\n if (!tags[replaceKey] || d.old[replaceKey]) return false;\n var replaceValue = d.replace[replaceKey];\n if (replaceValue === '*') return false;\n if (replaceValue.startsWith('$1') && tags[replaceKey] === tags[transferKeys[+replaceValue.substring(1) - 1]]) return false;\n if (replaceValue === tags[replaceKey]) return false;\n return true;\n });\n // don't flag deprecated tags if the upgrade path would overwrite existing data - #7843\n if (hasExistingValues) return;\n }\n\n var matchesDeprecatedTags = oldKeys.every((oldKey) => {\n if (!tags[oldKey]) return false;\n if (d.old[oldKey] === '*') return true;\n if (d.old[oldKey] === tags[oldKey]) return true;\n\n var vals = tags[oldKey].split(';').filter(Boolean);\n if (vals.length === 0) {\n return false;\n } else if (vals.length > 1) {\n return vals.indexOf(d.old[oldKey]) !== -1;\n } else {\n if (tags[oldKey] === d.old[oldKey]) {\n if (d.replace && d.old[oldKey] === d.replace[oldKey]) {\n var replaceKeys = Object.keys(d.replace);\n return !replaceKeys.every((replaceKey) => {\n return tags[replaceKey] === d.replace[replaceKey];\n });\n } else {\n return true;\n }\n }\n }\n\n return false;\n });\n\n if (matchesDeprecatedTags) {\n deprecated.push(d);\n }\n });\n\n return deprecated;\n}\n\n/** @type {{ [key: string]: string[] }} */\nvar _deprecatedTagValuesByKey;\n\n/** @param {DataDeprecated} dataDeprecated */\nexport function deprecatedTagValuesByKey(dataDeprecated) {\n if (!_deprecatedTagValuesByKey) {\n _deprecatedTagValuesByKey = {};\n dataDeprecated.forEach((d) => {\n var oldKeys = Object.keys(d.old);\n if (oldKeys.length === 1) {\n var oldKey = oldKeys[0];\n var oldValue = d.old[oldKey];\n if (oldValue !== '*') {\n if (!_deprecatedTagValuesByKey[oldKey]) {\n _deprecatedTagValuesByKey[oldKey] = [oldValue];\n } else {\n _deprecatedTagValuesByKey[oldKey].push(oldValue);\n }\n }\n }\n });\n }\n return _deprecatedTagValuesByKey;\n};\n", "import { t } from '../core/localizer';\n\nimport { actionChangePreset } from '../actions/change_preset';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionUpgradeTags } from '../actions/upgrade_tags';\nimport { fileFetcher } from '../core';\nimport { presetManager } from '../presets';\nimport { services } from '../services';\nimport { utilHashcode, utilTagDiff } from '../util';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { getDeprecatedTags } from '../osm/deprecated';\n\n/** @import { TagDiff } from '../util/util'. */\n\n\nexport function validationOutdatedTags() {\n const type = 'outdated_tags';\n let _waitingForDeprecated = true;\n let _dataDeprecated;\n\n // fetch deprecated tags\n fileFetcher.get('deprecated')\n .then(d => _dataDeprecated = d)\n .catch(() => { /* ignore */ })\n .finally(() => _waitingForDeprecated = false);\n\n\n function oldTagIssues(entity, graph) {\n if (!entity.hasInterestingTags()) return [];\n\n let preset = presetManager.match(entity, graph);\n if (!preset) return [];\n\n const oldTags = Object.assign({}, entity.tags); // shallow copy\n\n // Upgrade preset, if a replacement is available..\n if (preset.replacement) {\n const newPreset = presetManager.item(preset.replacement);\n graph = actionChangePreset(entity.id, preset, newPreset, true /* skip field defaults */)(graph);\n entity = graph.entity(entity.id);\n preset = newPreset;\n }\n\n // Attempt to match a canonical record in the name-suggestion-index.\n const nsi = services.nsi;\n let waitingForNsi = false;\n let nsiResult;\n if (nsi) {\n waitingForNsi = (nsi.status() === 'loading');\n if (!waitingForNsi) {\n const loc = entity.extent(graph).center();\n nsiResult = nsi.upgradeTags(oldTags, loc);\n }\n }\n const nsiDiff = nsiResult ? utilTagDiff(oldTags, nsiResult.newTags) : [];\n\n // Upgrade deprecated tags\n let deprecatedTags;\n if (_dataDeprecated) {\n deprecatedTags = getDeprecatedTags(entity.tags, _dataDeprecated);\n if (entity.type === 'way' && entity.isClosed() &&\n entity.tags.traffic_calming === 'island' && !entity.tags.highway) {\n // https://github.com/openstreetmap/id-tagging-schema/issues/1162#issuecomment-2000356902\n deprecatedTags.push({\n old: {traffic_calming: 'island'},\n replace: {'area:highway': 'traffic_island'}\n });\n }\n if (deprecatedTags.length) {\n deprecatedTags.forEach(tag => {\n graph = actionUpgradeTags(entity.id, tag.old, tag.replace)(graph);\n });\n entity = graph.entity(entity.id);\n }\n }\n\n // Add missing addTags from the detected preset\n let newTags = Object.assign({}, entity.tags); // shallow copy\n if (preset.tags !== preset.addTags) {\n Object.keys(preset.addTags).filter(k => {\n // if nsi suggestion already includes this tag: don't repeat it in \"incomplete tags\"\n return !nsiResult?.newTags[k];\n }).forEach(k => {\n if (!newTags[k]) {\n if (preset.addTags[k] === '*') {\n newTags[k] = 'yes';\n } else if (preset.addTags[k]) {\n newTags[k] = preset.addTags[k];\n }\n }\n });\n }\n const deprecationDiff = utilTagDiff(oldTags, newTags);\n const deprecationDiffContext = Object.keys(oldTags)\n .filter(key => deprecatedTags?.some(deprecated => deprecated.replace?.[key] !== undefined))\n .filter(key => newTags[key] === oldTags[key])\n .map(key => ({\n type: '~',\n key,\n oldVal: oldTags[key],\n newVal: newTags[key],\n display: '  ' + key + '=' + oldTags[key]\n }));\n\n let issues = [];\n issues.provisional = (_waitingForDeprecated || waitingForNsi);\n\n if (deprecationDiff.length) {\n const isOnlyAddingTags = !deprecationDiff.some(d => d.type === '-');\n const prefix = isOnlyAddingTags ? 'incomplete.' : '';\n\n issues.push(new validationIssue({\n type: type,\n subtype: isOnlyAddingTags ? 'incomplete_tags' : 'deprecated_tags',\n severity: 'warning',\n message: (context) => {\n const currEntity = context.hasEntity(entity.id);\n if (!currEntity) return '';\n\n const feature = utilDisplayLabel(currEntity, context.graph(), /* verbose */ true);\n\n return t.append(`issues.outdated_tags.${prefix}message`, { feature });\n },\n reference: selection => showReference(\n selection,\n t.append(`issues.outdated_tags.${prefix}reference`),\n [...deprecationDiff, ...deprecationDiffContext]\n ),\n entityIds: [entity.id],\n hash: utilHashcode(JSON.stringify(deprecationDiff)),\n dynamicFixes: () => {\n let fixes = [\n new validationIssueFix({\n title: t.append('issues.fix.upgrade_tags.title'),\n onClick: (context) => {\n context.perform(graph => doUpgrade(graph, deprecationDiff), t('issues.fix.upgrade_tags.annotation'));\n }\n })\n ];\n return fixes;\n }\n }));\n }\n\n if (nsiDiff.length) {\n const isOnlyAddingTags = nsiDiff.every(d => d.type === '+');\n\n issues.push(new validationIssue({\n type: type,\n subtype: 'noncanonical_brand',\n severity: 'warning',\n message: (context) => {\n const currEntity = context.hasEntity(entity.id);\n if (!currEntity) return '';\n\n const feature = utilDisplayLabel(currEntity, context.graph(), /* verbose */ true);\n\n return isOnlyAddingTags\n ? t.append('issues.outdated_tags.noncanonical_brand.message_incomplete', { feature })\n : t.append('issues.outdated_tags.noncanonical_brand.message', { feature });\n },\n reference: selection => showReference(\n selection,\n t.append('issues.outdated_tags.noncanonical_brand.reference'),\n nsiDiff\n ),\n entityIds: [entity.id],\n hash: utilHashcode(JSON.stringify(nsiDiff)),\n dynamicFixes: () => {\n let fixes = [\n new validationIssueFix({\n title: t.append('issues.fix.upgrade_tags.title'),\n onClick: (context) => {\n context.perform(graph => doUpgrade(graph, nsiDiff), t('issues.fix.upgrade_tags.annotation'));\n }\n }),\n new validationIssueFix({\n title: t.append('issues.fix.tag_as_not.title', { name: nsiResult.matched.displayName }),\n onClick: (context) => {\n context.perform(addNotTag, t('issues.fix.tag_as_not.annotation'));\n }\n })\n ];\n return fixes;\n }\n }));\n }\n\n return issues;\n\n\n /** @param {iD.Graph} graph @param {TagDiff[]} diff */\n function doUpgrade(graph, diff) {\n const currEntity = graph.hasEntity(entity.id);\n if (!currEntity) return graph;\n\n let newTags = Object.assign({}, currEntity.tags); // shallow copy\n diff.forEach(diff => {\n if (diff.type === '-') {\n delete newTags[diff.key];\n } else if (diff.type === '+') {\n newTags[diff.key] = diff.newVal;\n }\n });\n\n return actionChangeTags(currEntity.id, newTags)(graph);\n }\n\n\n function addNotTag(graph) {\n const currEntity = graph.hasEntity(entity.id);\n if (!currEntity) return graph;\n\n const item = nsiResult && nsiResult.matched;\n if (!item) return graph;\n\n let newTags = Object.assign({}, currEntity.tags); // shallow copy\n const wd = item.mainTag; // e.g. `brand:wikidata`\n const notwd = `not:${wd}`; // e.g. `not:brand:wikidata`\n const qid = item.tags[wd];\n newTags[notwd] = qid;\n\n if (newTags[wd] === qid) { // if `brand:wikidata` was set to that qid\n const wp = item.mainTag.replace('wikidata', 'wikipedia');\n delete newTags[wd]; // remove `brand:wikidata`\n delete newTags[wp]; // remove `brand:wikipedia`\n }\n\n return actionChangeTags(currEntity.id, newTags)(graph);\n }\n\n\n function showReference(selection, reference, tagDiff) {\n let enter = selection.selectAll('.issue-reference')\n .data([0])\n .enter();\n\n enter\n .append('div')\n .attr('class', 'issue-reference')\n .call(reference);\n\n enter\n .append('strong')\n .call(t.append('issues.suggested'));\n\n enter\n .append('table')\n .attr('class', 'tagDiff-table')\n .selectAll('.tagDiff-row')\n .data(tagDiff)\n .enter()\n .append('tr')\n .attr('class', 'tagDiff-row')\n .append('td')\n .attr('class', d => {\n const klass = 'tagDiff-cell';\n switch (d.type) {\n case '+':\n return `${klass} tagDiff-cell-add`;\n case '-':\n return `${klass} tagDiff-cell-remove`;\n default:\n return `${klass} tagDiff-cell-unchanged`;\n }\n })\n .html(d => d.display);\n }\n }\n\n\n let validation = oldTagIssues;\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { t } from '../core/localizer';\nimport { utilTagDiff } from '../util';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationPrivateData() {\n var type = 'private_data';\n\n // assume that some buildings are private\n var privateBuildingValues = {\n detached: true,\n farm: true,\n house: true,\n houseboat: true,\n residential: true,\n semidetached_house: true,\n static_caravan: true\n };\n\n // but they might be public if they have one of these other tags\n var publicKeys = {\n amenity: true,\n craft: true,\n historic: true,\n leisure: true,\n office: true,\n shop: true,\n tourism: true\n };\n\n // these tags may contain personally identifying info\n var personalTags = {\n 'contact:email': true,\n 'contact:fax': true,\n 'contact:phone': true,\n email: true,\n fax: true,\n phone: true\n };\n\n\n var validation = function checkPrivateData(entity) {\n var tags = entity.tags;\n if (!tags.building || !privateBuildingValues[tags.building]) return [];\n\n var keepTags = {};\n for (var k in tags) {\n if (publicKeys[k]) return []; // probably a public feature\n if (!personalTags[k]) {\n keepTags[k] = tags[k];\n }\n }\n\n var tagDiff = utilTagDiff(tags, keepTags);\n if (!tagDiff.length) return [];\n\n var fixID = tagDiff.length === 1 ? 'remove_tag' : 'remove_tags';\n\n return [new validationIssue({\n type: type,\n severity: 'warning',\n message: showMessage,\n reference: showReference,\n entityIds: [entity.id],\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.' + fixID + '.title'),\n onClick: function(context) {\n context.perform(doUpgrade, t('issues.fix.remove_tag.annotation'));\n }\n })\n ];\n }\n })];\n\n\n function doUpgrade(graph) {\n var currEntity = graph.hasEntity(entity.id);\n if (!currEntity) return graph;\n\n var newTags = Object.assign({}, currEntity.tags); // shallow copy\n tagDiff.forEach(function(diff) {\n if (diff.type === '-') {\n delete newTags[diff.key];\n } else if (diff.type === '+') {\n newTags[diff.key] = diff.newVal;\n }\n });\n\n return actionChangeTags(currEntity.id, newTags)(graph);\n }\n\n\n function showMessage(context) {\n var currEntity = context.hasEntity(this.entityIds[0]);\n if (!currEntity) return '';\n\n return t.append('issues.private_data.contact.message',\n { feature: utilDisplayLabel(currEntity, context.graph()) }\n );\n }\n\n\n function showReference(selection) {\n var enter = selection.selectAll('.issue-reference')\n .data([0])\n .enter();\n\n enter\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.private_data.reference'));\n\n enter\n .append('strong')\n .call(t.append('issues.suggested'));\n\n enter\n .append('table')\n .attr('class', 'tagDiff-table')\n .selectAll('.tagDiff-row')\n .data(tagDiff)\n .enter()\n .append('tr')\n .attr('class', 'tagDiff-row')\n .append('td')\n .attr('class', function(d) {\n var klass = d.type === '+' ? 'add' : 'remove';\n return 'tagDiff-cell tagDiff-cell-' + klass;\n })\n .html(function(d) { return d.display; });\n }\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { presetManager } from '../presets';\nimport { services } from '../services';\nimport { t, localizer } from '../core/localizer';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationSuspiciousName(context) {\n const type = 'suspicious_name';\n const keysToTestForGenericValues = [\n 'aerialway', 'aeroway', 'amenity', 'building', 'craft', 'highway',\n 'leisure', 'railway', 'man_made', 'office', 'shop', 'tourism', 'waterway'\n ];\n const ignoredPresets = new Set([\n 'amenity/place_of_worship/christian/jehovahs_witness',\n '__test__ignored_preset' // for unit tests\n ]);\n let _waitingForNsi = false;\n\n\n // Attempt to match a generic record in the name-suggestion-index.\n function isGenericMatchInNsi(tags) {\n const nsi = services.nsi;\n if (nsi) {\n _waitingForNsi = (nsi.status() === 'loading');\n if (!_waitingForNsi) {\n return nsi.isGenericName(tags);\n }\n }\n return false;\n }\n\n\n // Test if the name is just the key or tag value (e.g. \"park\")\n function nameMatchesRawTag(lowercaseName, tags) {\n for (let i = 0; i < keysToTestForGenericValues.length; i++) {\n let key = keysToTestForGenericValues[i];\n let val = tags[key];\n if (val) {\n val = val.toLowerCase();\n if (key === lowercaseName ||\n val === lowercaseName ||\n key.replace(/\\_/g, ' ') === lowercaseName ||\n val.replace(/\\_/g, ' ') === lowercaseName) {\n return true;\n }\n }\n }\n return false;\n }\n\n /** @param {string} name */\n function nameMatchesPresetName(name, preset) {\n if (!preset) return false;\n if (ignoredPresets.has(preset.id)) return false;\n\n name = name.toLowerCase();\n return name === preset.name().toLowerCase() || preset.aliases().some(alias => name === alias.toLowerCase());\n }\n\n /** @param {string} name */\n function isGenericName(name, tags, preset) {\n name = name.toLowerCase();\n return nameMatchesRawTag(name, tags) || nameMatchesPresetName(name, preset) || isGenericMatchInNsi(tags);\n }\n\n function makeGenericNameIssue(entityId, nameKey, genericName, langCode) {\n return new validationIssue({\n type: type,\n subtype: 'generic_name',\n severity: 'warning',\n message: function(context) {\n let entity = context.hasEntity(this.entityIds[0]);\n if (!entity) return '';\n let preset = presetManager.match(entity, context.graph());\n let langName = langCode && localizer.languageName(langCode);\n return t.append('issues.generic_name.message' + (langName ? '_language' : ''),\n { feature: preset.name(), name: genericName, language: langName }\n );\n },\n reference: showReference,\n entityIds: [entityId],\n hash: `${nameKey}=${genericName}`,\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_the_name.title'),\n onClick: function(context) {\n let entityId = this.issue.entityIds[0];\n let entity = context.entity(entityId);\n let tags = Object.assign({}, entity.tags); // shallow copy\n delete tags[nameKey];\n context.perform(\n actionChangeTags(entityId, tags), t('issues.fix.remove_generic_name.annotation')\n );\n }\n })\n ];\n }\n });\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.generic_name.reference'));\n }\n }\n\n let validation = function checkGenericName(entity) {\n const tags = entity.tags;\n\n // a generic name is allowed if it's a known brand or entity\n const hasWikidata = (!!tags.wikidata || !!tags['brand:wikidata'] || !!tags['operator:wikidata']);\n if (hasWikidata) return [];\n\n let issues = [];\n\n const preset = presetManager.match(entity, context.graph());\n\n for (let key in tags) {\n const m = key.match(/^name(?:(?::)([a-zA-Z_-]+))?$/);\n if (!m) continue;\n\n const langCode = m.length >= 2 ? m[1] : null;\n const value = tags[key];\n\n if (isGenericName(value, tags, preset)) {\n issues.provisional = _waitingForNsi; // retry later if we are waiting on NSI to finish loading\n issues.push(makeGenericNameIssue(entity.id, key, value, langCode));\n }\n }\n\n return issues;\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { prefs } from '../core/preferences';\nimport { t } from '../core/localizer';\n//import { actionChangeTags } from '../actions/change_tags';\nimport { actionOrthogonalize } from '../actions/orthogonalize';\nimport { geoOrthoCanOrthogonalize } from '../geo/ortho';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\nexport function validationUnsquareWay(context) {\n var type = 'unsquare_way';\n var DEFAULT_DEG_THRESHOLD = 5; // see also issues.js\n\n // use looser epsilon for detection to reduce warnings of buildings that are essentially square already\n var epsilon = 0.05;\n var nodeThreshold = 10;\n\n function isBuilding(entity, graph) {\n if (entity.type !== 'way' || entity.geometry(graph) !== 'area') return false;\n return entity.tags.building && entity.tags.building !== 'no';\n }\n\n\n var validation = function checkUnsquareWay(entity, graph) {\n\n if (!isBuilding(entity, graph)) return [];\n\n // don't flag ways marked as physically unsquare\n if (entity.tags.nonsquare === 'yes') return [];\n\n var isClosed = entity.isClosed();\n if (!isClosed) return []; // this building has bigger problems\n\n // don't flag ways with lots of nodes since they are likely detail-mapped\n var nodes = graph.childNodes(entity).slice(); // shallow copy\n if (nodes.length > nodeThreshold + 1) return []; // +1 because closing node appears twice\n\n // ignore if not all nodes are fully downloaded\n var osm = services.osm;\n if (!osm || nodes.some(function(node) { return !osm.isDataLoaded(node.loc); })) return [];\n\n // don't flag connected ways to avoid unresolvable unsquare loops\n var hasConnectedSquarableWays = nodes.some(function(node) {\n return graph.parentWays(node).some(function(way) {\n if (way.id === entity.id) return false;\n if (isBuilding(way, graph)) return true;\n return graph.parentRelations(way).some(function(parentRelation) {\n return parentRelation.isMultipolygon() &&\n parentRelation.tags.building &&\n parentRelation.tags.building !== 'no';\n });\n });\n });\n if (hasConnectedSquarableWays) return [];\n\n\n // user-configurable square threshold\n var storedDegreeThreshold = prefs('validate-square-degrees');\n var degreeThreshold = isFinite(storedDegreeThreshold) ? Number(storedDegreeThreshold) : DEFAULT_DEG_THRESHOLD;\n\n var points = nodes.map(function(node) { return context.projection(node.loc); });\n if (!geoOrthoCanOrthogonalize(points, isClosed, epsilon, degreeThreshold, true)) return [];\n\n return [new validationIssue({\n type: type,\n subtype: 'building',\n severity: 'suggestion',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.unsquare_way.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [entity.id],\n hash: degreeThreshold,\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-orthogonalize',\n title: t.append('issues.fix.square_feature.title'),\n onClick: function(context, completionHandler) {\n var entityId = this.issue.entityIds[0];\n // use same degree threshold as for detection\n context.perform(\n actionOrthogonalize(entityId, context.projection, undefined, degreeThreshold),\n t('operations.orthogonalize.annotation.feature', { n: 1 })\n );\n // run after the squaring transition (currently 150ms)\n window.setTimeout(function() { completionHandler(); }, 175);\n }\n }),\n /*\n new validationIssueFix({\n title: t.append('issues.fix.tag_as_unsquare.title'),\n onClick: function(context) {\n var entityId = this.issue.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n tags.nonsquare = 'yes';\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.tag_as_unsquare.annotation')\n );\n }\n })\n */\n ];\n }\n })];\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.unsquare_way.buildings.reference'));\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "export { validationAlmostJunction } from './almost_junction';\nexport { validationCloseNodes } from './close_nodes';\nexport { validationCrossingWays } from './crossing_ways';\nexport { validationDisconnectedWay } from './disconnected_way';\nexport { validationMissingStartDate } from './missing_start_date';\nexport { validationFormatting } from './invalid_format';\nexport { validationHelpRequest } from './help_request';\nexport { validationImpossibleOneway } from './impossible_oneway';\nexport { validationIncompatibleSource } from './incompatible_source';\nexport { validationMaprules } from './maprules';\nexport { validationMismatchedDates } from './mismatched_dates';\nexport { validationMismatchedGeometry } from './mismatched_geometry';\nexport { validationMissingRole } from './missing_role';\nexport { validationMissingTag } from './missing_tag';\nexport { validationMutuallyExclusiveTags } from './mutually_exclusive_tags';\nexport { validationOsmApiLimits } from './osm_api_limits';\nexport { validationOutdatedTags } from './outdated_tags';\nexport { validationPrivateData } from './private_data';\nexport { validationSuspiciousName } from './suspicious_name';\nexport { validationUnsquareWay } from './unsquare_way';\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from './preferences';\nimport { coreDifference } from './difference';\nimport { geoExtent } from '../geo/extent';\nimport { modeSelect } from '../modes/select';\nimport { utilArrayChunk, utilArrayDifference, utilArrayGroupBy, utilArrayIntersection, utilArrayUnion, utilEntityAndDeepMemberIDs, utilRebind } from '../util';\nimport * as Validations from '../validations/index';\n\n\nexport function coreValidator(context) {\n let dispatch = d3_dispatch('validated', 'focusedIssue');\n const validator = {};\n\n let _rules = {};\n let _disabledRules = {};\n\n let _ignoredIssueIDs = new Set();\n let _resolvedIssueIDs = new Set();\n let _baseCache = validationCache('base'); // issues before any user edits\n let _headCache = validationCache('head'); // issues after all user edits\n let _completeDiff = {}; // complete diff base -> head of what the user changed\n let _headIsCurrent = false;\n\n let _deferredRIC = {}; // Object( RequestIdleCallback handle : rejectPromise method )\n let _deferredST = new Set(); // Set( SetTimeout handles )\n let _headPromise; // Promise fulfilled when validation is performed up to headGraph snapshot\n\n const RETRY = 5000; // wait 5sec before revalidating provisional entities\n\n\n // Allow validation severity to be overridden by url queryparams...\n // See: https://github.com/openstreetmap/iD/pull/8243\n //\n // Each param should contain a urlencoded comma separated list of\n // `type/subtype` rules. `*` may be used as a wildcard..\n // Examples:\n // `validationError=disconnected_way/*`\n // `validationError=disconnected_way/highway`\n // `validationError=crossing_ways/bridge*`\n // `validationError=crossing_ways/bridge*,crossing_ways/tunnel*`\n\n const _errorOverrides = parseHashParam(context.initialHashParams.validationError);\n const _warningOverrides = parseHashParam(context.initialHashParams.validationWarning);\n const _suggestionOverrides = parseHashParam(context.initialHashParams.validationSuggestion);\n const _disableOverrides = parseHashParam(context.initialHashParams.validationDisable);\n\n // `parseHashParam()` (private)\n // Checks hash parameters for severity overrides\n // Arguments\n // `param` - a url hash parameter (`validationError`, `validationWarning`, `validationSuggestion`, or `validationDisable`)\n // Returns\n // Array of Objects like { type: RegExp, subtype: RegExp }\n //\n function parseHashParam(param) {\n let result = [];\n let rules = (param || '').split(',');\n rules.forEach(rule => {\n rule = rule.trim();\n const parts = rule.split('/', 2); // \"type/subtype\"\n const type = parts[0];\n const subtype = parts[1] || '*';\n if (!type || !subtype) return;\n result.push({ type: makeRegExp(type), subtype: makeRegExp(subtype) });\n });\n return result;\n\n function makeRegExp(str) {\n const escaped = str\n .replace(/[-\\/\\\\^$+?.()|[\\]{}]/g, '\\\\$&') // escape all reserved chars except for the '*'\n .replace(/\\*/g, '.*'); // treat a '*' like '.*'\n return new RegExp('^' + escaped + '$');\n }\n }\n\n\n // `init()`\n // Initialize the validator, called once on iD startup\n //\n validator.init = () => {\n Object.values(Validations).forEach(validation => {\n if (typeof validation !== 'function') return;\n const fn = validation(context);\n const key = fn.type;\n _rules[key] = fn;\n });\n\n let disabledRules = prefs('validate-disabledRules');\n if (disabledRules) {\n disabledRules.split(',').forEach(k => _disabledRules[k] = true);\n }\n };\n\n\n // `reset()` (private)\n // Cancels deferred work and resets all caches\n //\n // Arguments\n // `resetIgnored` - `true` to clear the list of user-ignored issues\n //\n function reset(resetIgnored) {\n // empty queues\n _baseCache.queue = [];\n _headCache.queue = [];\n\n // cancel deferred work and reject any pending promise\n Object.keys(_deferredRIC).forEach(key => {\n window.cancelIdleCallback(key);\n _deferredRIC[key]();\n });\n _deferredRIC = {};\n _deferredST.forEach(window.clearTimeout);\n _deferredST.clear();\n\n // clear caches\n if (resetIgnored) _ignoredIssueIDs.clear();\n _resolvedIssueIDs.clear();\n _baseCache = validationCache('base');\n _headCache = validationCache('head');\n _completeDiff = {};\n _headIsCurrent = false;\n }\n\n\n // `reset()`\n // clear caches, called whenever iD resets after a save or switches sources\n // (clears out the _ignoredIssueIDs set also)\n //\n validator.reset = () => {\n reset(true);\n };\n\n\n // `resetIgnoredIssues()`\n // clears out the _ignoredIssueIDs Set\n //\n validator.resetIgnoredIssues = () => {\n _ignoredIssueIDs.clear();\n dispatch.call('validated'); // redraw UI\n };\n\n\n // `revalidateUnsquare()`\n // Called whenever the user changes the unsquare threshold\n // It reruns just the \"unsquare_way\" validation on all buildings.\n //\n validator.revalidateUnsquare = () => {\n revalidateUnsquare(_headCache);\n revalidateUnsquare(_baseCache);\n dispatch.call('validated');\n };\n\n function revalidateUnsquare(cache) {\n const checkUnsquareWay = _rules.unsquare_way;\n if (!cache.graph || typeof checkUnsquareWay !== 'function') return;\n\n // uncache existing\n cache.uncacheIssuesOfType('unsquare_way');\n\n const buildings = context.history().tree().intersects(geoExtent([-180,-90],[180, 90]), cache.graph) // everywhere\n .filter(entity => (entity.type === 'way' && entity.tags.building && entity.tags.building !== 'no'));\n\n // rerun for all buildings\n buildings.forEach(entity => {\n const detected = checkUnsquareWay(entity, cache.graph);\n if (!detected.length) return;\n cache.cacheIssues(detected);\n });\n }\n\n\n // `getIssues()`\n // Gets all issues that match the given options\n // This is called by many other places\n //\n // Arguments\n // `options` Object like:\n // {\n // what: 'all', // 'all' or 'edited'\n // where: 'all', // 'all' or 'visible'\n // includeIgnored: false, // true, false, or 'only'\n // includeDisabledRules: false // true, false, or 'only'\n // }\n //\n // Returns\n // An Array containing the issues\n //\n validator.getIssues = (options) => {\n const opts = Object.assign({ what: 'all', where: 'all', includeIgnored: false, includeDisabledRules: false }, options);\n const view = context.map().extent();\n let seen = new Set();\n let results = [];\n\n // collect head issues - present in the user edits\n if (_headCache.graph && _headCache.graph !== _baseCache.graph) {\n Object.values(_headCache.issuesByIssueID).forEach(issue => {\n // In the head cache, only count features that the user is responsible for - #8632\n // For example, a user can undo some work and an issue will still present in the\n // head graph, but we don't want to credit the user for causing that issue.\n const userModified = (issue.entityIds || []).some(id => _completeDiff.hasOwnProperty(id));\n if (opts.what === 'edited' && !userModified) return; // present in head but user didn't touch it\n\n if (!filter(issue)) return;\n seen.add(issue.id);\n results.push(issue);\n });\n }\n\n // collect base issues - present before user edits\n if (opts.what === 'all') {\n Object.values(_baseCache.issuesByIssueID).forEach(issue => {\n if (!filter(issue)) return;\n seen.add(issue.id);\n results.push(issue);\n });\n }\n\n return results;\n\n\n // Filter the issue set to include only what the calling code wants to see.\n // Note that we use `context.graph()`/`context.hasEntity()` here, not `cache.graph`,\n // because that is the graph that the calling code will be using.\n function filter(issue) {\n if (!issue) return false;\n if (seen.has(issue.id)) return false;\n if (_resolvedIssueIDs.has(issue.id)) return false;\n if (opts.includeDisabledRules === 'only' && !_disabledRules[issue.type]) return false;\n if (!opts.includeDisabledRules && _disabledRules[issue.type]) return false;\n\n if (opts.includeIgnored === 'only' && !_ignoredIssueIDs.has(issue.id)) return false;\n if (!opts.includeIgnored && _ignoredIssueIDs.has(issue.id)) return false;\n\n // This issue may involve an entity that doesn't exist in context.graph()\n // This can happen because validation is async and rendering the issue lists is async.\n if ((issue.entityIds || []).some(id => !context.hasEntity(id))) return false;\n\n if (opts.where === 'visible') {\n const extent = issue.extent(context.graph());\n if (!view.intersects(extent)) return false;\n }\n\n return true;\n }\n };\n\n\n // `getResolvedIssues()`\n // Gets the issues that have been fixed by the user.\n //\n // Resolved issues are tracked in the `_resolvedIssueIDs` Set,\n // and they should all be issues that exist in the _baseCache.\n //\n // Returns\n // An Array containing the issues\n //\n validator.getResolvedIssues = () => {\n return Array.from(_resolvedIssueIDs)\n .map(issueID => _baseCache.issuesByIssueID[issueID])\n .filter(Boolean);\n };\n\n\n // `focusIssue()`\n // Adjusts the map to focus on the given issue.\n // (requires the issue to have a reasonable extent defined)\n //\n // Arguments\n // `issue` - the issue to focus on\n //\n validator.focusIssue = (issue) => {\n // Note that we use `context.graph()`/`context.hasEntity()` here, not `cache.graph`,\n // because that is the graph that the calling code will be using.\n const graph = context.graph();\n let selectID;\n\n // Try to focus the map at the center of the issue..\n let issueExtent = issue.extent(graph);\n\n // Try to select the first entity in the issue..\n if (issue.entityIds && issue.entityIds.length) {\n selectID = issue.entityIds[0];\n\n // If a relation, focus on one of its members instead.\n // Otherwise we might be focusing on a part of map where the relation is not visible.\n if (selectID && selectID.charAt(0) === 'r') { // relation\n const ids = utilEntityAndDeepMemberIDs([selectID], graph);\n let nodeID = ids.find(id => id.charAt(0) === 'n' && graph.hasEntity(id));\n\n if (!nodeID) { // relation has no downloaded nodes to focus on\n const wayID = ids.find(id => id.charAt(0) === 'w' && graph.hasEntity(id));\n if (wayID) {\n nodeID = graph.entity(wayID).first(); // focus on the first node of this way\n }\n }\n\n if (nodeID) {\n issueExtent = graph.entity(nodeID).extent(graph);\n }\n }\n }\n\n // Adjust the view\n context.map().zoomToEase(issueExtent);\n\n if (selectID) { // Enter select mode\n window.setTimeout(() => {\n context.enter(modeSelect(context, [selectID]));\n dispatch.call('focusedIssue', this, issue);\n }, 250); // after ease\n }\n };\n\n\n // `getIssuesBySeverity()`\n // Gets the issues then groups them by error/warning/suggestion\n // (This just calls getIssues, then puts issues in groups)\n //\n // Arguments\n // `options` - (see `getIssues`)\n // Returns\n // Object result like:\n // {\n // error: Array of errors,\n // warning: Array of warnings,\n // suggestion: Array of suggestions,\n // }\n //\n validator.getIssuesBySeverity = (options) => {\n let groups = utilArrayGroupBy(validator.getIssues(options), 'severity');\n groups.error = groups.error || [];\n groups.warning = groups.warning || [];\n groups.suggestion = groups.suggestion || [];\n return groups;\n };\n\n\n // `getEntityIssues()`\n // Gets the issues that the given entity IDs have in common, matching the given options\n // (This just calls getIssues, then filters for the given entity IDs)\n // The issues are sorted for relevance\n //\n // Arguments\n // `entityIDs` - Array or Set of entityIDs to get issues for\n // `options` - (see `getIssues`)\n // Returns\n // An Array containing the issues\n //\n validator.getSharedEntityIssues = (entityIDs, options) => {\n const orderedIssueTypes = [ // Show some issue types in a particular order:\n 'missing_tag', 'missing_role', // - missing data first\n 'outdated_tags', 'mismatched_geometry', // - identity issues\n 'crossing_ways', 'almost_junction', // - geometry issues where fixing them might solve connectivity issues\n 'disconnected_way', 'impossible_oneway' // - finally connectivity issues\n ];\n\n const allIssues = validator.getIssues(options);\n const forEntityIDs = new Set(entityIDs);\n\n return allIssues\n .filter(issue => (issue.entityIds || []).some(entityID => forEntityIDs.has(entityID)))\n .sort((issue1, issue2) => {\n if (issue1.type === issue2.type) { // issues of the same type, sort deterministically\n return issue1.id < issue2.id ? -1 : 1;\n }\n const index1 = orderedIssueTypes.indexOf(issue1.type);\n const index2 = orderedIssueTypes.indexOf(issue2.type);\n if (index1 !== -1 && index2 !== -1) { // both issue types have explicit sort orders\n return index1 - index2;\n } else if (index1 === -1 && index2 === -1) { // neither issue type has an explicit sort order, sort by type\n return issue1.type < issue2.type ? -1 : 1;\n } else { // order explicit types before everything else\n return index1 !== -1 ? -1 : 1;\n }\n });\n };\n\n\n // `getEntityIssues()`\n // Get an array of detected issues for the given entityID.\n // (This just calls getSharedEntityIssues for a single entity)\n //\n // Arguments\n // `entityID` - the entity ID to get the issues for\n // `options` - (see `getIssues`)\n // Returns\n // An Array containing the issues\n //\n validator.getEntityIssues = (entityID, options) => {\n return validator.getSharedEntityIssues([entityID], options);\n };\n\n\n // `getRuleKeys()`\n //\n // Returns\n // An Array containing the rule keys\n //\n validator.getRuleKeys = () => {\n return Object.keys(_rules);\n };\n\n\n // `isRuleEnabled()`\n //\n // Arguments\n // `key` - the rule to check (e.g. 'crossing_ways')\n // Returns\n // `true`/`false`\n //\n validator.isRuleEnabled = (key) => {\n return !_disabledRules[key];\n };\n\n\n // `toggleRule()`\n // Toggles a single validation rule,\n // then reruns the validation so that the user sees something happen in the UI\n //\n // Arguments\n // `key` - the rule to toggle (e.g. 'crossing_ways')\n //\n validator.toggleRule = (key) => {\n if (_disabledRules[key]) {\n delete _disabledRules[key];\n } else {\n _disabledRules[key] = true;\n }\n\n prefs('validate-disabledRules', Object.keys(_disabledRules).join(','));\n validator.validate();\n };\n\n\n // `disableRules()`\n // Disables given validation rules,\n // then reruns the validation so that the user sees something happen in the UI\n //\n // Arguments\n // `keys` - Array or Set containing rule keys to disable\n //\n validator.disableRules = (keys) => {\n _disabledRules = {};\n keys.forEach(k => _disabledRules[k] = true);\n\n prefs('validate-disabledRules', Object.keys(_disabledRules).join(','));\n validator.validate();\n };\n\n\n // `ignoreIssue()`\n // Don't show the given issue in lists\n //\n // Arguments\n // `issueID` - the issueID\n //\n validator.ignoreIssue = (issueID) => {\n _ignoredIssueIDs.add(issueID);\n };\n\n\n // `validate()`\n // Validates anything that has changed in the head graph since the last time it was run.\n // (head graph contains user's edits)\n //\n // Returns\n // A Promise fulfilled when the validation has completed and then dispatches a `validated` event.\n // This may take time but happen in the background during browser idle time.\n //\n validator.validate = () => {\n // Make sure the caches have graphs assigned to them.\n // (we don't do this in `reset` because context is still resetting things and `history.base()` is unstable then)\n const baseGraph = context.history().base();\n if (!_headCache.graph) _headCache.graph = baseGraph;\n if (!_baseCache.graph) _baseCache.graph = baseGraph;\n\n const prevGraph = _headCache.graph;\n const currGraph = context.graph();\n\n if (currGraph === prevGraph) { // _headCache.graph is current - we are caught up\n _headIsCurrent = true;\n dispatch.call('validated');\n return Promise.resolve();\n }\n\n if (_headPromise) { // Validation already in process, but we aren't caught up to current\n _headIsCurrent = false; // We will need to catch up after the validation promise fulfills\n return _headPromise;\n }\n\n // If we get here, its time to start validating stuff.\n _headCache.graph = currGraph; // take snapshot\n _completeDiff = context.history().difference().complete();\n const incrementalDiff = coreDifference(prevGraph, currGraph);\n const diff = Object.keys(incrementalDiff.complete());\n const entityIDs = _headCache.withAllRelatedEntities(diff); // expand set\n\n if (!entityIDs.size) {\n dispatch.call('validated');\n return Promise.resolve();\n }\n\n // revalidate also connected (or previously connected) entities to the current way\n // https://github.com/openstreetmap/iD/issues/8758\n const addConnectedWays = graph => diff\n .filter(entityID => graph.hasEntity(entityID))\n .map(entityID => graph.entity(entityID))\n .flatMap(entity => graph.childNodes(entity))\n .flatMap(vertex => graph.parentWays(vertex))\n .forEach(way => entityIDs.add(way.id));\n addConnectedWays(currGraph);\n addConnectedWays(prevGraph);\n\n // revalidate entities with changed relation memberships\n // https://github.com/openstreetmap/iD/issues/10786\n Object.values({...incrementalDiff.created(), ...incrementalDiff.deleted()})\n .filter(e => e.type === 'relation')\n .flatMap(r => r.members)\n .forEach(m => entityIDs.add(m.id));\n Object.values(incrementalDiff.modified())\n .filter(e => e.type === 'relation')\n .map(r => ({ baseEntity: prevGraph.entity(r.id), headEntity: r }))\n .forEach(({ baseEntity, headEntity }) => {\n const bm = baseEntity.members.map(m => m.id);\n const hm = headEntity.members.map(m => m.id);\n const symDiff = utilArrayDifference(utilArrayUnion(bm, hm), utilArrayIntersection(bm, hm));\n symDiff.forEach(id => entityIDs.add(id));\n });\n\n _headPromise = validateEntitiesAsync(entityIDs, _headCache)\n .then(() => updateResolvedIssues(entityIDs))\n .then(() => dispatch.call('validated'))\n .catch(() => { /* ignore */ })\n .then(() => {\n _headPromise = null;\n if (!_headIsCurrent) {\n validator.validate(); // run it again to catch up to current graph\n }\n });\n\n return _headPromise;\n };\n\n\n // register event handlers:\n\n // WHEN TO RUN VALIDATION:\n // When history changes:\n context.history()\n .on('restore.validator', validator.validate) // on restore saved history\n .on('undone.validator', validator.validate) // on undo\n .on('redone.validator', validator.validate) // on redo\n .on('reset.validator', () => { // on history reset - happens after save, or enter/exit walkthrough\n reset(false); // cached issues aren't valid any longer if the history has been reset\n validator.validate();\n });\n // but not on 'change' (e.g. while drawing)\n\n // When user changes editing modes (to catch recent changes e.g. drawing)\n context\n .on('exit.validator', validator.validate);\n\n // When merging fetched data, validate base graph:\n context.history()\n .on('merge.validator', entities => {\n if (!entities) return;\n\n // Make sure the caches have graphs assigned to them.\n // (we don't do this in `reset` because context is still resetting things and `history.base()` is unstable then)\n const baseGraph = context.history().base();\n if (!_headCache.graph) _headCache.graph = baseGraph;\n if (!_baseCache.graph) _baseCache.graph = baseGraph;\n\n let entityIDs = entities.map(entity => entity.id);\n entityIDs = _baseCache.withAllRelatedEntities(entityIDs); // expand set\n validateEntitiesAsync(entityIDs, _baseCache);\n });\n\n\n\n // `validateEntity()` (private)\n // Runs all validation rules on a single entity.\n // Some things to note:\n // - Graph is passed in from whenever the validation was started. Validators shouldn't use\n // `context.graph()` because this all happens async, and the graph might have changed\n // (for example, nodes getting deleted before the validation can run)\n // - Validator functions may still be waiting on something and return a \"provisional\" result.\n // In this situation, we will schedule to revalidate the entity sometime later.\n //\n // Arguments\n // `entity` - The entity\n // `graph` - graph containing the entity\n //\n // Returns\n // Object result like:\n // {\n // issues: Array of detected issues\n // provisional: `true` if provisional result, `false` if final result\n // }\n //\n function validateEntity(entity, graph) {\n let result = { issues: [], provisional: false };\n Object.keys(_rules).forEach(runValidation); // run all rules\n return result;\n\n\n // runs validation and appends resulting issues\n function runValidation(key) {\n const fn = _rules[key];\n if (typeof fn !== 'function') {\n console.error('no such validation rule = ' + key); // eslint-disable-line no-console\n return;\n }\n\n let detected = fn(entity, graph);\n if (detected.provisional) { // this validation should be run again later\n result.provisional = true;\n }\n detected = detected.filter(applySeverityOverrides);\n result.issues = result.issues.concat(detected);\n\n\n // If there are any override rules that match the issue type/subtype,\n // adjust severity (or disable it) and keep/discard as quickly as possible.\n function applySeverityOverrides(issue) {\n const type = issue.type;\n const subtype = issue.subtype || '';\n let i;\n\n for (i = 0; i < _errorOverrides.length; i++) {\n if (_errorOverrides[i].type.test(type) && _errorOverrides[i].subtype.test(subtype)) {\n issue.severity = 'error';\n return true;\n }\n }\n for (i = 0; i < _warningOverrides.length; i++) {\n if (_warningOverrides[i].type.test(type) && _warningOverrides[i].subtype.test(subtype)) {\n issue.severity = 'warning';\n return true;\n }\n }\n for (i = 0; i < _suggestionOverrides.length; i++) {\n if (_suggestionOverrides[i].type.test(type) && _suggestionOverrides[i].subtype.test(subtype)) {\n issue.severity = 'suggestion';\n return true;\n }\n }\n for (i = 0; i < _disableOverrides.length; i++) {\n if (_disableOverrides[i].type.test(type) && _disableOverrides[i].subtype.test(subtype)) {\n return false;\n }\n }\n return true;\n }\n }\n }\n\n\n // `updateResolvedIssues()` (private)\n // Determine if any issues were resolved for the given entities.\n // This is called by `validate()` after validation of the head graph\n //\n // Give the user credit for fixing an issue if:\n // - the issue is in the base cache\n // - the issue is not in the head cache\n // - the user did something to one of the entities involved in the issue\n //\n // Arguments\n // `entityIDs` - Array or Set containing entity IDs.\n //\n function updateResolvedIssues(entityIDs) {\n entityIDs.forEach(entityID => {\n const baseIssues = _baseCache.issuesByEntityID[entityID];\n if (!baseIssues) return;\n\n baseIssues.forEach(issueID => {\n // Check if the user did something to one of the entities involved in this issue.\n // (This issue could involve multiple entities, e.g. disconnected routable features)\n const issue = _baseCache.issuesByIssueID[issueID];\n const userModified = (issue.entityIds || []).some(id => _completeDiff.hasOwnProperty(id));\n\n if (userModified && !_headCache.issuesByIssueID[issueID]) { // issue seems fixed\n _resolvedIssueIDs.add(issueID);\n } else { // issue still not resolved\n _resolvedIssueIDs.delete(issueID); // (did undo, or possibly fixed and then re-caused the issue)\n }\n });\n });\n }\n\n\n // `validateEntitiesAsync()` (private)\n // Schedule validation for many entities.\n //\n // Arguments\n // `entityIDs` - Array or Set containing entityIDs.\n // `graph` - the graph to validate that contains those entities\n // `cache` - the cache to store results in (_headCache or _baseCache)\n //\n // Returns\n // A Promise fulfilled when the validation has completed.\n // This may take time but happen in the background during browser idle time.\n //\n function validateEntitiesAsync(entityIDs, cache) {\n // Enqueue the work\n const jobs = Array.from(entityIDs).map(entityID => {\n if (cache.queuedEntityIDs.has(entityID)) return null; // queued already\n cache.queuedEntityIDs.add(entityID);\n\n // Clear caches for existing issues related to this entity\n cache.uncacheEntityID(entityID);\n\n return () => {\n cache.queuedEntityIDs.delete(entityID);\n\n const graph = cache.graph;\n if (!graph) return; // was reset?\n\n const entity = graph.hasEntity(entityID); // Sanity check: don't validate deleted entities\n if (!entity) return;\n\n // detect new issues and update caches\n const result = validateEntity(entity, graph);\n if (result.provisional) { // provisional result\n cache.provisionalEntityIDs.add(entityID); // we'll need to revalidate this entity again later\n }\n\n cache.cacheIssues(result.issues); // update cache\n };\n\n }).filter(Boolean);\n\n\n // Perform the work in chunks.\n // Because this will happen during idle callbacks, we want to choose a chunk size\n // that won't make the browser stutter too badly.\n cache.queue = cache.queue.concat(utilArrayChunk(jobs, 100));\n\n // Perform the work\n if (cache.queuePromise) return cache.queuePromise;\n\n cache.queuePromise = processQueue(cache)\n .then(() => revalidateProvisionalEntities(cache))\n .catch(() => { /* ignore */ })\n .finally(() => cache.queuePromise = null);\n\n return cache.queuePromise;\n }\n\n\n // `revalidateProvisionalEntities()` (private)\n // Sometimes a validator will return a \"provisional\" result.\n // In this situation, we'll need to revalidate the entity later.\n // This function waits a delay, then places them back into the validation queue.\n //\n // Arguments\n // `cache` - The cache (_headCache or _baseCache)\n //\n function revalidateProvisionalEntities(cache) {\n if (!cache.provisionalEntityIDs.size) return; // nothing to do\n\n const handle = window.setTimeout(() => {\n _deferredST.delete(handle);\n if (!cache.provisionalEntityIDs.size) return; // nothing to do\n validateEntitiesAsync(Array.from(cache.provisionalEntityIDs), cache);\n }, RETRY);\n\n _deferredST.add(handle);\n }\n\n\n // `processQueue(queue)` (private)\n // Process the next chunk of deferred validation work\n //\n // Arguments\n // `cache` - The cache (_headCache or _baseCache)\n //\n // Returns\n // A Promise fulfilled when the validation has completed.\n // This may take time but happen in the background during browser idle time.\n //\n function processQueue(cache) {\n // console.log(`${cache.which} queue length ${cache.queue.length}`);\n\n if (!cache.queue.length) return Promise.resolve(); // we're done\n const chunk = cache.queue.pop();\n\n return new Promise((resolvePromise, rejectPromise) => {\n const handle = window.requestIdleCallback(() => {\n delete (_deferredRIC[handle]);\n // const t0 = performance.now();\n chunk.forEach(job => job());\n // const t1 = performance.now();\n // console.log('chunk processed in ' + (t1 - t0) + ' ms');\n resolvePromise();\n });\n _deferredRIC[handle] = rejectPromise;\n })\n .then(() => { // dispatch an event sometimes to redraw various UI things\n if (cache.queue.length % 25 === 0) dispatch.call('validated');\n })\n .then(() => processQueue(cache));\n }\n\n\n return utilRebind(validator, dispatch, 'on');\n}\n\n\n// `validationCache()` (private)\n// Creates a cache to store validation state\n// We create 2 of these:\n// `_baseCache` for validation on the base graph (unedited)\n// `_headCache` for validation on the head graph (user edits applied)\n//\n// Arguments\n// `which` - just a String 'base' or 'head' to keep track of it\n//\nfunction validationCache(which) {\n let cache = {\n which: which,\n graph: null,\n queue: [],\n queuePromise: null,\n queuedEntityIDs: new Set(),\n provisionalEntityIDs: new Set(),\n issuesByIssueID: {}, // issue.id -> issue\n issuesByEntityID: {} // entity.id -> Set(issue.id)\n };\n\n\n cache.cacheIssue = (issue) => {\n (issue.entityIds || []).forEach(entityID => {\n if (!cache.issuesByEntityID[entityID]) {\n cache.issuesByEntityID[entityID] = new Set();\n }\n cache.issuesByEntityID[entityID].add(issue.id);\n });\n cache.issuesByIssueID[issue.id] = issue;\n };\n\n\n cache.uncacheIssue = (issue) => {\n (issue.entityIds || []).forEach(entityID => {\n if (cache.issuesByEntityID[entityID]) {\n cache.issuesByEntityID[entityID].delete(issue.id);\n }\n });\n delete cache.issuesByIssueID[issue.id];\n };\n\n\n cache.cacheIssues = (issues) => {\n issues.forEach(cache.cacheIssue);\n };\n\n\n cache.uncacheIssues = (issues) => {\n issues.forEach(cache.uncacheIssue);\n };\n\n\n cache.uncacheIssuesOfType = (type) => {\n const issuesOfType = Object.values(cache.issuesByIssueID)\n .filter(issue => issue.type === type);\n cache.uncacheIssues(issuesOfType);\n };\n\n\n // Remove a single entity and all its related issues from the caches\n cache.uncacheEntityID = (entityID) => {\n const entityIssueIDs = cache.issuesByEntityID[entityID];\n if (entityIssueIDs) {\n entityIssueIDs.forEach(issueID => {\n const issue = cache.issuesByIssueID[issueID];\n if (issue) {\n cache.uncacheIssue(issue);\n } else { // shouldn't happen, clean up\n delete cache.issuesByIssueID[issueID];\n }\n });\n }\n\n delete cache.issuesByEntityID[entityID];\n cache.provisionalEntityIDs.delete(entityID);\n };\n\n\n // Return the expandeded set of entityIDs related to issues for the given entityIDs\n //\n // Arguments\n // `entityIDs` - Array or Set containing entityIDs.\n //\n cache.withAllRelatedEntities = (entityIDs) => {\n let result = new Set();\n (entityIDs || []).forEach(entityID => {\n result.add(entityID); // include self\n\n const entityIssueIDs = cache.issuesByEntityID[entityID];\n if (entityIssueIDs) {\n entityIssueIDs.forEach(issueID => {\n const issue = cache.issuesByIssueID[issueID];\n if (issue) {\n (issue.entityIds || []).forEach(relatedID => result.add(relatedID));\n } else { // shouldn't happen, clean up\n delete cache.issuesByIssueID[issueID];\n }\n });\n }\n });\n\n return result;\n };\n\n\n return cache;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { fileFetcher } from './file_fetcher';\nimport { actionDiscardTags } from '../actions/discard_tags';\nimport { actionMergeRemoteChanges } from '../actions/merge_remote_changes';\nimport { actionNoop } from '../actions/noop';\nimport { actionRevert } from '../actions/revert';\nimport { coreGraph } from '../core/graph';\nimport { t } from '../core/localizer';\nimport { utilArrayUnion, utilArrayUniq, utilDisplayName, utilDisplayType, utilRebind } from '../util';\n\n\n/** @param {iD.Context} context */\nexport function coreUploader(context) {\n\n var dispatch = d3_dispatch(\n // Start and end events are dispatched exactly once each per legitimate outside call to `save`\n 'saveStarted', // dispatched as soon as a call to `save` has been deemed legitimate\n 'saveEnded', // dispatched after the result event has been dispatched\n\n 'willAttemptUpload', // dispatched before the actual upload call occurs, if it will\n 'progressChanged',\n\n // Each save results in one of these outcomes:\n 'resultNoChanges', // upload wasn't attempted since there were no edits\n 'resultErrors', // upload failed due to errors\n 'resultConflicts', // upload failed due to data conflicts\n 'resultSuccess' // upload completed without errors\n );\n\n var _isSaving = false;\n\n let _anyConflictsAutomaticallyResolved = false;\n var _conflicts = [];\n var _errors = [];\n var _origChanges;\n\n var _discardTags = {};\n fileFetcher.get('discarded')\n .then(function(d) { _discardTags = d; })\n .catch(function() { /* ignore */ });\n\n const uploader = {};\n\n uploader.isSaving = function() {\n return _isSaving;\n };\n\n uploader.save = function(changeset, tryAgain, checkConflicts) {\n // Guard against accidentally entering save code twice - #4641\n if (_isSaving && !tryAgain) {\n return;\n }\n\n var osm = context.connection();\n if (!osm) return;\n\n // If user somehow got logged out mid-save, try to reauthenticate..\n // This can happen if they were logged in from before, but the tokens are no longer valid.\n if (!osm.authenticated()) {\n osm.authenticate(function(err) {\n if (!err) {\n uploader.save(changeset, tryAgain, checkConflicts); // continue where we left off..\n }\n });\n return;\n }\n\n if (!_isSaving) {\n _isSaving = true;\n dispatch.call('saveStarted', this);\n }\n\n var history = context.history();\n\n _anyConflictsAutomaticallyResolved = false;\n _conflicts = [];\n _errors = [];\n\n // Store original changes, in case user wants to download them as an .osc file\n _origChanges = history.changes(actionDiscardTags(history.difference(), _discardTags));\n\n // First time, `history.perform` a no-op action.\n // Any conflict resolutions will be done as `history.replace`\n // Remember to pop this later if needed\n if (!tryAgain) {\n history.perform(actionNoop());\n }\n\n // Attempt a fast upload.. If there are conflicts, re-enter with `checkConflicts = true`\n if (!checkConflicts) {\n upload(changeset);\n\n // Do the full (slow) conflict check..\n } else {\n performFullConflictCheck(changeset);\n }\n\n };\n\n\n function performFullConflictCheck(changeset) {\n\n var osm = context.connection();\n if (!osm) return;\n\n var history = context.history();\n\n var localGraph = context.graph();\n var remoteGraph = coreGraph(history.base(), true);\n\n var summary = history.difference().summary();\n var _toCheck = [];\n for (var i = 0; i < summary.length; i++) {\n var item = summary[i];\n if (item.changeType === 'modified') {\n _toCheck.push(item.entity.id);\n }\n }\n\n var _toLoad = withChildNodes(_toCheck, localGraph);\n var _loaded = {};\n var _toLoadCount = 0;\n var _toLoadTotal = _toLoad.length;\n\n if (_toCheck.length) {\n dispatch.call('progressChanged', this, _toLoadCount, _toLoadTotal);\n _toLoad.forEach(function(id) { _loaded[id] = false; });\n osm.loadMultiple(_toLoad, loaded);\n } else {\n upload(changeset);\n }\n\n return;\n\n function withChildNodes(ids, graph) {\n var s = new Set(ids);\n ids.forEach(function(id) {\n var entity = graph.entity(id);\n if (entity.type !== 'way') return;\n\n graph.childNodes(entity).forEach(function(child) {\n if (child.version !== undefined) {\n s.add(child.id);\n }\n });\n });\n\n return Array.from(s);\n }\n\n\n // Reload modified entities into an alternate graph and check for conflicts..\n function loaded(err, result) {\n if (_errors.length) return;\n\n if (err) {\n _errors.push({\n msg: err.message || err.responseText,\n details: [ t('save.status_code', { code: err.status }) ]\n });\n didResultInErrors();\n\n } else {\n var loadMore = [];\n\n result.data.forEach(function(entity) {\n remoteGraph.replace(entity);\n _loaded[entity.id] = true;\n _toLoad = _toLoad.filter(function(val) { return val !== entity.id; });\n\n if (!entity.visible) return;\n\n // Because loadMultiple doesn't download /full like loadEntity,\n // need to also load children that aren't already being checked..\n var i, id;\n if (entity.type === 'way') {\n for (i = 0; i < entity.nodes.length; i++) {\n id = entity.nodes[i];\n if (_loaded[id] === undefined) {\n _loaded[id] = false;\n loadMore.push(id);\n }\n }\n } else if (entity.type === 'relation' && entity.isMultipolygon()) {\n for (i = 0; i < entity.members.length; i++) {\n id = entity.members[i].id;\n if (_loaded[id] === undefined) {\n _loaded[id] = false;\n loadMore.push(id);\n }\n }\n }\n });\n\n _toLoadCount += result.data.length;\n _toLoadTotal += loadMore.length;\n dispatch.call('progressChanged', this, _toLoadCount, _toLoadTotal);\n\n if (loadMore.length) {\n _toLoad.push.apply(_toLoad, loadMore);\n osm.loadMultiple(loadMore, loaded);\n }\n\n if (!_toLoad.length) {\n detectConflicts();\n upload(changeset);\n }\n }\n }\n\n\n function detectConflicts() {\n function choice(id, text, action) {\n return {\n id: id,\n text: text,\n action: function() {\n history.replace(action);\n }\n };\n }\n function formatUser(selection, d) {\n selection\n .append('a')\n .attr('href', osm.userURL(d))\n .attr('target', '_blank')\n .text(d);\n }\n function entityName(entity) {\n return utilDisplayName(entity) || (utilDisplayType(entity.id) + ' ' + entity.id);\n }\n\n function sameVersions(local, remote) {\n if (local.version !== remote.version) return false;\n\n if (local.type === 'way') {\n var children = utilArrayUnion(local.nodes, remote.nodes);\n for (var i = 0; i < children.length; i++) {\n var a = localGraph.hasEntity(children[i]);\n var b = remoteGraph.hasEntity(children[i]);\n if (a && b && a.version !== b.version) return false;\n }\n }\n\n return true;\n }\n\n _toCheck.forEach(function(id) {\n var local = localGraph.entity(id);\n var remote = remoteGraph.entity(id);\n\n if (sameVersions(local, remote)) return;\n\n var merge = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags, formatUser);\n\n history.replace(merge);\n\n var mergeConflicts = merge.conflicts();\n if (!mergeConflicts.length) {\n _anyConflictsAutomaticallyResolved = true;\n return; // merged safely\n }\n\n var forceLocal = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags).withOption('force_local');\n var forceRemote = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags).withOption('force_remote');\n var keepMine = t('save.conflict.' + (remote.visible ? 'keep_local' : 'restore'));\n var keepTheirs = t('save.conflict.' + (remote.visible ? 'keep_remote' : 'delete'));\n\n _conflicts.push({\n id: id,\n name: entityName(local),\n details: mergeConflicts,\n chosen: 1,\n choices: [\n choice(id, keepMine, forceLocal),\n choice(id, keepTheirs, forceRemote)\n ]\n });\n });\n }\n }\n\n\n async function upload(changeset) {\n var osm = context.connection();\n if (!osm) {\n _errors.push({ msg: 'No OSM Service' });\n }\n\n if (_conflicts.length) {\n didResultInConflicts(changeset);\n\n } else if (_errors.length) {\n didResultInErrors();\n\n } else {\n if (_anyConflictsAutomaticallyResolved) {\n // add a changeset tag to aid reviewers\n changeset.tags.merge_conflict_resolved = 'automatically';\n await osm.updateChangesetTags(changeset);\n }\n var history = context.history();\n var changes = history.changes(actionDiscardTags(history.difference(), _discardTags));\n if (changes.modified.length || changes.created.length || changes.deleted.length) {\n\n dispatch.call('willAttemptUpload', this);\n\n osm.putChangeset(changeset, changes, uploadCallback);\n\n } else {\n // changes were insignificant or reverted by user\n didResultInNoChanges();\n }\n }\n }\n\n\n function uploadCallback(err, changeset) {\n if (err) {\n if (err.status === 409) { // 409 Conflict\n uploader.save(changeset, true, true); // tryAgain = true, checkConflicts = true\n } else {\n _errors.push({\n msg: err.message || err.responseText,\n details: [ t('save.status_code', { code: err.status }) ]\n });\n didResultInErrors();\n }\n\n } else {\n didResultInSuccess(changeset);\n }\n }\n\n function didResultInNoChanges() {\n\n dispatch.call('resultNoChanges', this);\n\n endSave();\n\n context.flush(); // reset iD\n }\n\n function didResultInErrors() {\n\n context.history().pop();\n\n dispatch.call('resultErrors', this, _errors);\n\n endSave();\n }\n\n\n function didResultInConflicts(changeset) {\n // add a changeset tag to aid reviewers\n changeset.tags.merge_conflict_resolved = 'manually';\n context.connection().updateChangesetTags(changeset);\n\n _conflicts.sort(function(a, b) { return b.id.localeCompare(a.id); });\n\n dispatch.call('resultConflicts', this, changeset, _conflicts, _origChanges);\n\n endSave();\n }\n\n\n function didResultInSuccess(changeset) {\n\n // delete the edit stack cached to local storage\n context.history().clearSaved();\n\n dispatch.call('resultSuccess', this, changeset);\n\n // Add delay to allow for postgres replication #1646 #2678\n window.setTimeout(function() {\n\n endSave();\n\n context.flush(); // reset iD\n }, 2500);\n }\n\n\n function endSave() {\n _isSaving = false;\n\n dispatch.call('saveEnded', this);\n }\n\n\n uploader.cancelConflictResolution = function() {\n context.history().pop();\n };\n\n\n uploader.processResolvedConflicts = function(changeset) {\n var history = context.history();\n\n for (var i = 0; i < _conflicts.length; i++) {\n if (_conflicts[i].chosen === 1) { // user chose \"use theirs\"\n var entity = context.hasEntity(_conflicts[i].id);\n if (entity && entity.type === 'way') {\n var children = utilArrayUniq(entity.nodes);\n for (var j = 0; j < children.length; j++) {\n history.replace(actionRevert(children[j]));\n }\n }\n history.replace(actionRevert(_conflicts[i].id));\n }\n }\n\n uploader.save(changeset, true, false); // tryAgain = true, checkConflicts = false\n };\n\n\n uploader.reset = function() {\n\n };\n\n\n return utilRebind(uploader, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDrawWay } from '../behavior/draw_way';\n\n\nexport function modeDrawArea(context, wayID, startGraph, button) {\n var mode = {\n button: button,\n id: 'draw-area'\n };\n\n var behavior = behaviorDrawWay(context, wayID, mode, startGraph)\n .on('rejectedSelfIntersection.modeDrawArea', function() {\n context.ui().flash\n .iconName('#iD-icon-no')\n .label(t.append('self_intersection.error.areas'))();\n });\n\n mode.wayID = wayID;\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n mode.selectedIDs = function() {\n return [wayID];\n };\n\n mode.activeID = function() {\n return (behavior && behavior.activeID()) || [];\n };\n\n return mode;\n}\n", "import { actionAddEntity } from '../actions/add_entity';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionAddVertex } from '../actions/add_vertex';\n\nimport { behaviorAddWay } from '../behavior/add_way';\nimport { modeDrawArea } from './draw_area';\nimport { osmNode, osmWay } from '../osm';\n\n\nexport function modeAddArea(context, mode) {\n mode.id = 'add-area';\n\n var behavior = behaviorAddWay(context)\n .on('start', start)\n .on('startFromWay', startFromWay)\n .on('startFromNode', startFromNode);\n\n function defaultTags(loc) {\n var defaultTags = { area: 'yes' };\n if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'area', false, loc);\n return defaultTags;\n }\n\n function actionClose(wayId) {\n return function (graph) {\n return graph.replace(graph.entity(wayId).close());\n };\n }\n\n\n function start(loc) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionClose(way.id)\n );\n\n context.enter(modeDrawArea(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromWay(loc, edge) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionClose(way.id),\n actionAddMidpoint({ loc: loc, edge: edge }, node)\n );\n\n context.enter(modeDrawArea(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromNode(node) {\n var startGraph = context.graph();\n var way = osmWay({ tags: defaultTags(node.loc) });\n\n context.perform(\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionClose(way.id)\n );\n\n context.enter(modeDrawArea(context, way.id, startGraph, mode.button));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n\n return mode;\n}\n", "import { actionAddEntity } from '../actions/add_entity';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionAddVertex } from '../actions/add_vertex';\n\nimport { behaviorAddWay } from '../behavior/add_way';\nimport { modeDrawLine } from './draw_line';\nimport { osmNode, osmWay } from '../osm';\n\n\nexport function modeAddLine(context, mode) {\n mode.id = 'add-line';\n\n var behavior = behaviorAddWay(context)\n .on('start', start)\n .on('startFromWay', startFromWay)\n .on('startFromNode', startFromNode);\n\n function defaultTags(loc) {\n var defaultTags = {};\n if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'line', false, loc);\n return defaultTags;\n }\n\n\n function start(loc) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id)\n );\n\n context.enter(modeDrawLine(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromWay(loc, edge) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionAddMidpoint({ loc: loc, edge: edge }, node)\n );\n\n context.enter(modeDrawLine(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromNode(node) {\n var startGraph = context.graph();\n var way = osmWay({ tags: defaultTags(node.loc) });\n\n context.perform(\n actionAddEntity(way),\n actionAddVertex(way.id, node.id)\n );\n\n context.enter(modeDrawLine(context, way.id, startGraph, mode.button));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDraw } from '../behavior/draw';\nimport { modeBrowse } from './browse';\nimport { modeSelect } from './select';\nimport { osmNode } from '../osm/node';\nimport { actionAddEntity } from '../actions/add_entity';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\n\n\nexport function modeAddPoint(context, mode) {\n\n mode.id = 'add-point';\n\n var behavior = behaviorDraw(context)\n .on('click', add)\n .on('clickWay', addWay)\n .on('clickNode', addNode)\n .on('cancel', cancel)\n .on('finish', cancel);\n\n function defaultTags(loc) {\n var defaultTags = {};\n if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'point', false, loc);\n return defaultTags;\n }\n\n\n function add(loc) {\n var node = osmNode({ loc: loc, tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n t('operations.add.annotation.point')\n );\n\n enterSelectMode(node);\n }\n\n\n function addWay(loc, edge) {\n var node = osmNode({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddMidpoint({loc: loc, edge: edge}, node),\n t('operations.add.annotation.vertex')\n );\n\n enterSelectMode(node);\n }\n\n function enterSelectMode(node) {\n context.enter(\n modeSelect(context, [node.id]).newFeature(true)\n );\n }\n\n\n function addNode(node) {\n const _defaultTags = defaultTags(node.loc);\n if (Object.keys(_defaultTags).length === 0) {\n enterSelectMode(node);\n return;\n }\n\n var tags = Object.assign({}, node.tags); // shallow copy\n for (var key in _defaultTags) {\n tags[key] = _defaultTags[key];\n }\n\n context.perform(\n actionChangeTags(node.id, tags),\n t('operations.add.annotation.point')\n );\n\n enterSelectMode(node);\n }\n\n\n function cancel() {\n context.enter(modeBrowse(context));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n\n return mode;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select,\n selection as d3_selection\n} from 'd3-selection';\n\nimport { geoVecLength } from '../geo';\nimport { osmNote } from '../osm';\nimport { utilRebind } from '../util/rebind';\nimport { utilFastMouse, utilPrefixCSSProperty, utilPrefixDOMProperty } from '../util';\n\n\n/*\n `behaviorDrag` is like `d3_behavior.drag`, with the following differences:\n\n * The `origin` function is expected to return an [x, y] tuple rather than an\n {x, y} object.\n * The events are `start`, `move`, and `end`.\n (https://github.com/mbostock/d3/issues/563)\n * The `start` event is not dispatched until the first cursor movement occurs.\n (https://github.com/mbostock/d3/pull/368)\n * The `move` event has a `point` and `delta` [x, y] tuple properties rather\n than `x`, `y`, `dx`, and `dy` properties.\n * The `end` event is not dispatched if no movement occurs.\n * An `off` function is available that unbinds the drag's internal event handlers.\n */\n\nexport function behaviorDrag() {\n var dispatch = d3_dispatch('start', 'move', 'end');\n\n // see also behaviorSelect\n var _tolerancePx = 1; // keep this low to facilitate pixel-perfect micromapping\n var _penTolerancePx = 4; // styluses can be touchy so require greater movement - #1981\n\n var _origin = null;\n var _selector = '';\n var _targetNode;\n var _targetEntity;\n var _surface;\n var _pointerId;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n var d3_event_userSelectProperty = utilPrefixCSSProperty('UserSelect');\n var d3_event_userSelectSuppress = function() {\n var selection = d3_selection();\n var select = selection.style(d3_event_userSelectProperty);\n selection.style(d3_event_userSelectProperty, 'none');\n return function() {\n selection.style(d3_event_userSelectProperty, select);\n };\n };\n\n\n function pointerdown(d3_event) {\n\n if (_pointerId) return;\n\n _pointerId = d3_event.pointerId || 'mouse';\n\n _targetNode = this;\n\n // only force reflow once per drag\n var pointerLocGetter = utilFastMouse(_surface || _targetNode.parentNode);\n\n var offset;\n var startOrigin = pointerLocGetter(d3_event);\n var started = false;\n var selectEnable = d3_event_userSelectSuppress();\n\n d3_select(window)\n .on(_pointerPrefix + 'move.drag', pointermove)\n .on(_pointerPrefix + 'up.drag pointercancel.drag', pointerup, true);\n\n if (_origin) {\n offset = _origin.call(_targetNode, _targetEntity);\n offset = [offset[0] - startOrigin[0], offset[1] - startOrigin[1]];\n } else {\n offset = [0, 0];\n }\n\n d3_event.stopPropagation();\n\n\n function pointermove(d3_event) {\n if (_pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n var p = pointerLocGetter(d3_event);\n\n if (!started) {\n var dist = geoVecLength(startOrigin, p);\n var tolerance = d3_event.pointerType === 'pen' ? _penTolerancePx : _tolerancePx;\n // don't start until the drag has actually moved somewhat\n if (dist < tolerance) return;\n\n started = true;\n dispatch.call('start', this, d3_event, _targetEntity);\n\n // Don't send a `move` event in the same cycle as `start` since dragging\n // a midpoint will convert the target to a node.\n } else {\n\n startOrigin = p;\n d3_event.stopPropagation();\n d3_event.preventDefault();\n\n var dx = p[0] - startOrigin[0];\n var dy = p[1] - startOrigin[1];\n dispatch.call('move', this, d3_event, _targetEntity, [p[0] + offset[0], p[1] + offset[1]], [dx, dy]);\n }\n }\n\n\n function pointerup(d3_event) {\n if (_pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n _pointerId = null;\n\n if (started) {\n dispatch.call('end', this, d3_event, _targetEntity);\n\n d3_event.preventDefault();\n }\n\n d3_select(window)\n .on(_pointerPrefix + 'move.drag', null)\n .on(_pointerPrefix + 'up.drag pointercancel.drag', null);\n\n selectEnable();\n }\n }\n\n\n function behavior(selection) {\n var matchesSelector = utilPrefixDOMProperty('matchesSelector');\n var delegate = pointerdown;\n\n if (_selector) {\n delegate = function(d3_event) {\n var root = this;\n var target = d3_event.target;\n for (; target && target !== root; target = target.parentNode) {\n var datum = target.__data__;\n\n _targetEntity = datum instanceof osmNote ? datum\n : datum && datum.properties && datum.properties.entity;\n\n if (_targetEntity && target[matchesSelector](_selector)) {\n return pointerdown.call(target, d3_event);\n }\n }\n };\n }\n\n selection\n .on(_pointerPrefix + 'down.drag' + _selector, delegate);\n }\n\n\n behavior.off = function(selection) {\n selection\n .on(_pointerPrefix + 'down.drag' + _selector, null);\n };\n\n\n behavior.selector = function(_) {\n if (!arguments.length) return _selector;\n _selector = _;\n return behavior;\n };\n\n\n behavior.origin = function(_) {\n if (!arguments.length) return _origin;\n _origin = _;\n return behavior;\n };\n\n\n behavior.cancel = function() {\n d3_select(window)\n .on(_pointerPrefix + 'move.drag', null)\n .on(_pointerPrefix + 'up.drag pointercancel.drag', null);\n return behavior;\n };\n\n\n behavior.targetNode = function(_) {\n if (!arguments.length) return _targetNode;\n _targetNode = _;\n return behavior;\n };\n\n\n behavior.targetEntity = function(_) {\n if (!arguments.length) return _targetEntity;\n _targetEntity = _;\n return behavior;\n };\n\n\n behavior.surface = function(_) {\n if (!arguments.length) return _surface;\n _surface = _;\n return behavior;\n };\n\n\n return utilRebind(behavior, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { t } from '../core/localizer';\n\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionConnect } from '../actions/connect';\nimport { actionMoveNode } from '../actions/move_node';\nimport { actionNoop } from '../actions/noop';\n\nimport { behaviorDrag } from '../behavior/drag';\nimport { behaviorEdit } from '../behavior/edit';\nimport { behaviorHover } from '../behavior/hover';\n\nimport {\n geoChooseEdge,\n geoHasLineIntersections,\n geoHasSelfIntersections,\n geoVecSubtract,\n geoViewportEdge\n} from '../geo';\n\nimport { modeBrowse } from './browse';\nimport { modeSelect } from './select';\nimport { osmJoinWays, osmNode } from '../osm';\nimport { utilArrayIntersection, utilKeybinding } from '../util';\n\n\n\nexport function modeDragNode(context) {\n var mode = {\n id: 'drag-node',\n button: 'browse'\n };\n var hover = behaviorHover(context).altDisables(true)\n .on('hover', context.ui().sidebar.hover);\n var edit = behaviorEdit(context);\n\n var _nudgeInterval;\n var _restoreSelectedIDs = [];\n var _wasMidpoint = false;\n var _isCancelled = false;\n var _activeEntity;\n var _startLoc;\n var _lastLoc;\n\n\n function startNudge(d3_event, entity, nudge) {\n if (_nudgeInterval) window.clearInterval(_nudgeInterval);\n _nudgeInterval = window.setInterval(function() {\n context.map().pan(nudge);\n doMove(d3_event, entity, nudge);\n }, 50);\n }\n\n\n function stopNudge() {\n if (_nudgeInterval) {\n window.clearInterval(_nudgeInterval);\n _nudgeInterval = null;\n }\n }\n\n\n function moveAnnotation(entity) {\n return t('operations.move.annotation.' + entity.geometry(context.graph()));\n }\n\n\n function connectAnnotation(nodeEntity, targetEntity) {\n var nodeGeometry = nodeEntity.geometry(context.graph());\n var targetGeometry = targetEntity.geometry(context.graph());\n if (nodeGeometry === 'vertex' && targetGeometry === 'vertex') {\n var nodeParentWayIDs = context.graph().parentWays(nodeEntity);\n var targetParentWayIDs = context.graph().parentWays(targetEntity);\n var sharedParentWays = utilArrayIntersection(nodeParentWayIDs, targetParentWayIDs);\n // if both vertices are part of the same way\n if (sharedParentWays.length !== 0) {\n // if the nodes are next to each other, they are merged\n if (sharedParentWays[0].areAdjacent(nodeEntity.id, targetEntity.id)) {\n return t('operations.connect.annotation.from_vertex.to_adjacent_vertex');\n }\n return t('operations.connect.annotation.from_vertex.to_sibling_vertex');\n }\n }\n return t('operations.connect.annotation.from_' + nodeGeometry + '.to_' + targetGeometry);\n }\n\n\n function shouldSnapToNode(target) {\n if (!_activeEntity) return false;\n return _activeEntity.geometry(context.graph()) !== 'vertex' ||\n (target.geometry(context.graph()) === 'vertex' || presetManager.allowsVertex(target, context.graph()));\n }\n\n\n function origin(entity) {\n return context.projection(entity.loc);\n }\n\n\n function keydown(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope')) {\n context.surface()\n .classed('nope-suppressed', true);\n }\n context.surface()\n .classed('nope', false)\n .classed('nope-disabled', true);\n }\n }\n\n\n function keyup(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope-suppressed')) {\n context.surface()\n .classed('nope', true);\n }\n context.surface()\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false);\n }\n }\n\n\n function start(d3_event, entity) {\n _wasMidpoint = entity.type === 'midpoint';\n var hasHidden = context.features().hasHiddenConnections(entity, context.graph());\n _isCancelled = !context.editable() || d3_event.shiftKey || hasHidden;\n\n\n if (_isCancelled) {\n if (hasHidden) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('modes.drag_node.connected_to_hidden'))();\n }\n return drag.cancel();\n }\n\n if (_wasMidpoint) {\n var midpoint = entity;\n entity = osmNode();\n context.perform(actionAddMidpoint(midpoint, entity));\n entity = context.entity(entity.id); // get post-action entity\n\n var vertex = context.surface().selectAll('.' + entity.id);\n drag.targetNode(vertex.node())\n .targetEntity(entity);\n\n } else {\n context.perform(actionNoop());\n }\n\n _activeEntity = entity;\n _startLoc = entity.loc;\n\n hover.ignoreVertex(entity.geometry(context.graph()) === 'vertex');\n\n context.surface().selectAll('.' + _activeEntity.id)\n .classed('active', true);\n\n context.enter(mode);\n }\n\n\n // related code\n // - `behavior/draw.js` `datum()`\n function datum(d3_event) {\n if (!d3_event || d3_event.altKey) {\n return {};\n } else {\n // When dragging, snap only to touch targets..\n // (this excludes area fills and active drawing elements)\n var d = d3_event.target.__data__;\n return (d && d.properties && d.properties.target) ? d : {};\n }\n }\n\n\n function doMove(d3_event, entity, nudge) {\n nudge = nudge || [0, 0];\n\n var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc);\n var currMouse = geoVecSubtract(currPoint, nudge);\n var loc = context.projection.invert(currMouse);\n\n var target, edge;\n\n if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap..\n // related code\n // - `mode/drag_node.js` `doMove()`\n // - `behavior/draw.js` `click()`\n // - `behavior/draw_way.js` `move()`\n var d = datum(d3_event);\n target = d && d.properties && d.properties.entity;\n var targetLoc = target && target.loc;\n var targetNodes = d && d.properties && d.properties.nodes;\n\n if (targetLoc) { // snap to node/vertex - a point target with `.loc`\n if (shouldSnapToNode(target)) {\n loc = targetLoc;\n }\n\n } else if (targetNodes) { // snap to way - a line target with `.nodes`\n edge = geoChooseEdge(targetNodes, context.map().mouse(), context.projection, end.id);\n if (edge) {\n loc = edge.loc;\n }\n }\n }\n\n context.replace(\n actionMoveNode(entity.id, loc)\n );\n\n // Below here: validations\n var isInvalid = false;\n\n // Check if this connection to `target` could cause relations to break..\n if (target) {\n isInvalid = hasRelationConflict(entity, target, edge, context.graph());\n }\n\n // Check if this drag causes the geometry to break..\n if (!isInvalid) {\n isInvalid = hasInvalidGeometry(entity, context.graph());\n }\n\n\n var nope = context.surface().classed('nope');\n if (isInvalid === 'relation' || isInvalid === 'restriction') {\n if (!nope) { // about to nope - show hint\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('operations.connect.' + isInvalid,\n { relation: presetManager.item('type/restriction').name() }\n ))();\n }\n } else if (isInvalid) {\n var errorID = isInvalid === 'line' ? 'lines' : 'areas';\n context.ui().flash\n .duration(3000)\n .iconName('#iD-icon-no')\n .label(t.append('self_intersection.error.' + errorID))();\n } else {\n if (nope) { // about to un-nope, remove hint\n context.ui().flash\n .duration(1)\n .label('')();\n }\n }\n\n\n var nopeDisabled = context.surface().classed('nope-disabled');\n if (nopeDisabled) {\n context.surface()\n .classed('nope', false)\n .classed('nope-suppressed', isInvalid);\n } else {\n context.surface()\n .classed('nope', isInvalid)\n .classed('nope-suppressed', false);\n }\n\n _lastLoc = loc;\n }\n\n\n // Uses `actionConnect.disabled()` to know whether this connection is ok..\n function hasRelationConflict(entity, target, edge, graph) {\n var testGraph = graph.update(); // copy\n\n // if snapping to way - add midpoint there and consider that the target..\n if (edge) {\n var midpoint = osmNode();\n var action = actionAddMidpoint({\n loc: edge.loc,\n edge: [target.nodes[edge.index - 1], target.nodes[edge.index]]\n }, midpoint);\n\n testGraph = action(testGraph);\n target = midpoint;\n }\n\n // can we connect to it?\n var ids = [entity.id, target.id];\n return actionConnect(ids).disabled(testGraph);\n }\n\n\n function hasInvalidGeometry(entity, graph) {\n var parents = graph.parentWays(entity);\n var i, j, k;\n\n for (i = 0; i < parents.length; i++) {\n var parent = parents[i];\n var activeIndex = null; // which multipolygon ring contains node being dragged\n\n // test any parent multipolygons for valid geometry\n var relations = graph.parentRelations(parent);\n for (j = 0; j < relations.length; j++) {\n if (!relations[j].isMultipolygon()) continue;\n\n var rings = osmJoinWays(relations[j].members, graph);\n\n // find active ring and test it for self intersections\n for (k = 0; k < rings.length; k++) {\n const nodes = rings[k].nodes;\n if (nodes.find(function(n) { return n.id === entity.id; })) {\n activeIndex = k;\n if (geoHasSelfIntersections(nodes, entity.id)) {\n return 'multipolygonMember';\n }\n }\n rings[k].coords = nodes.map(function(n) { return n.loc; });\n }\n\n // test active ring for intersections with other rings in the multipolygon\n for (k = 0; k < rings.length; k++) {\n if (k === activeIndex) continue;\n\n // make sure active ring doesn't cross passive rings\n if (geoHasLineIntersections(rings[activeIndex].nodes, rings[k].nodes, entity.id)) {\n return 'multipolygonRing';\n }\n }\n }\n\n\n // If we still haven't tested this node's parent way for self-intersections.\n // (because it's not a member of a multipolygon), test it now.\n if (activeIndex === null) {\n const nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); });\n if (nodes.length && geoHasSelfIntersections(nodes, entity.id)) {\n return parent.geometry(graph);\n }\n }\n\n }\n\n return false;\n }\n\n\n function move(d3_event, entity, point) {\n if (_isCancelled) return;\n d3_event.stopPropagation();\n\n context.surface().classed('nope-disabled', d3_event.altKey);\n\n _lastLoc = context.projection.invert(point);\n\n doMove(d3_event, entity);\n var nudge = geoViewportEdge(point, context.map().dimensions());\n if (nudge) {\n startNudge(d3_event, entity, nudge);\n } else {\n stopNudge();\n }\n }\n\n function end(d3_event, entity) {\n if (_isCancelled) return;\n\n var wasPoint = entity.geometry(context.graph()) === 'point';\n\n var d = datum(d3_event);\n var nope = (d && d.properties && d.properties.nope) || context.surface().classed('nope');\n var target = d && d.properties && d.properties.entity; // entity to snap to\n\n if (nope) { // bounce back\n context.perform(\n _actionBounceBack(entity.id, _startLoc)\n );\n\n } else if (target && target.type === 'way') {\n var choice = geoChooseEdge(context.graph().childNodes(target), context.map().mouse(), context.projection, entity.id);\n context.replace(\n actionAddMidpoint({\n loc: choice.loc,\n edge: [target.nodes[choice.index - 1], target.nodes[choice.index]]\n }, entity),\n connectAnnotation(entity, target)\n );\n\n } else if (target && target.type === 'node' && shouldSnapToNode(target)) {\n context.replace(\n actionConnect([target.id, entity.id]),\n connectAnnotation(entity, target)\n );\n\n } else if (_wasMidpoint) {\n context.replace(\n actionNoop(),\n t('operations.add.annotation.vertex')\n );\n\n } else {\n context.replace(\n actionNoop(),\n moveAnnotation(entity)\n );\n }\n\n if (wasPoint) {\n context.enter(modeSelect(context, [entity.id]));\n\n } else {\n var reselection = _restoreSelectedIDs.filter(function(id) {\n return context.graph().hasEntity(id);\n });\n\n if (reselection.length) {\n context.enter(modeSelect(context, reselection));\n } else {\n context.enter(modeBrowse(context));\n }\n }\n }\n\n\n function _actionBounceBack(nodeID, toLoc) {\n var moveNode = actionMoveNode(nodeID, toLoc);\n var action = function(graph, t) {\n // last time through, pop off the bounceback perform.\n // it will then overwrite the initial perform with a moveNode that does nothing\n if (t === 1) context.pop();\n return moveNode(graph, t);\n };\n action.transitionable = true;\n return action;\n }\n\n\n function cancel() {\n drag.cancel();\n context.enter(modeBrowse(context));\n }\n\n\n var drag = behaviorDrag()\n .selector('.layer-touch.points .target')\n .surface(context.container().select('.main-map').node())\n .origin(origin)\n .on('start', start)\n .on('move', move)\n .on('end', end);\n\n\n mode.enter = function() {\n context.install(hover);\n context.install(edit);\n\n d3_select(window)\n .on('keydown.dragNode', keydown)\n .on('keyup.dragNode', keyup);\n\n context.history()\n .on('undone.drag-node', cancel);\n };\n\n\n mode.exit = function() {\n context.ui().sidebar.hover.cancel();\n context.uninstall(hover);\n context.uninstall(edit);\n\n d3_select(window)\n .on('keydown.dragNode', null)\n .on('keyup.dragNode', null);\n\n context.history()\n .on('undone.drag-node', null);\n\n _activeEntity = null;\n\n context.surface()\n .classed('nope', false)\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false)\n .selectAll('.active')\n .classed('active', false);\n\n stopNudge();\n };\n\n\n mode.selectedIDs = function() {\n if (!arguments.length) return _activeEntity ? [_activeEntity.id] : [];\n // no assign\n return mode;\n };\n\n\n mode.activeID = function() {\n if (!arguments.length) return _activeEntity && _activeEntity.id;\n // no assign\n return mode;\n };\n\n\n mode.restoreSelectedIDs = function(_) {\n if (!arguments.length) return _restoreSelectedIDs;\n _restoreSelectedIDs = _;\n return mode;\n };\n\n\n mode.behavior = drag;\n\n\n return mode;\n}\n", "import { services } from '../services';\nimport { actionNoop } from '../actions/noop';\nimport { behaviorDrag } from '../behavior/drag';\nimport { behaviorEdit } from '../behavior/edit';\nimport { geoVecSubtract, geoViewportEdge } from '../geo';\nimport { modeSelectNote } from './select_note';\n\n\nexport function modeDragNote(context) {\n var mode = {\n id: 'drag-note',\n button: 'browse'\n };\n\n var edit = behaviorEdit(context);\n\n var _nudgeInterval;\n var _lastLoc;\n var _note; // most current note.. dragged note may have stale datum.\n\n\n function startNudge(d3_event, nudge) {\n if (_nudgeInterval) window.clearInterval(_nudgeInterval);\n _nudgeInterval = window.setInterval(function() {\n context.map().pan(nudge);\n doMove(d3_event, nudge);\n }, 50);\n }\n\n\n function stopNudge() {\n if (_nudgeInterval) {\n window.clearInterval(_nudgeInterval);\n _nudgeInterval = null;\n }\n }\n\n\n function origin(note) {\n return context.projection(note.loc);\n }\n\n\n function start(d3_event, note) {\n _note = note;\n var osm = services.osm;\n if (osm) {\n // Get latest note from cache.. The marker may have a stale datum bound to it\n // and dragging it around can sometimes delete the users note comment.\n _note = osm.getNote(_note.id);\n }\n\n context.surface().selectAll('.note-' + _note.id)\n .classed('active', true);\n\n context.perform(actionNoop());\n context.enter(mode);\n context.selectedNoteID(_note.id);\n }\n\n\n function move(d3_event, entity, point) {\n d3_event.stopPropagation();\n _lastLoc = context.projection.invert(point);\n\n doMove(d3_event);\n var nudge = geoViewportEdge(point, context.map().dimensions());\n if (nudge) {\n startNudge(d3_event, nudge);\n } else {\n stopNudge();\n }\n }\n\n\n function doMove(d3_event, nudge) {\n nudge = nudge || [0, 0];\n\n var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc);\n var currMouse = geoVecSubtract(currPoint, nudge);\n var loc = context.projection.invert(currMouse);\n\n _note = _note.move(loc);\n\n var osm = services.osm;\n if (osm) {\n osm.replaceNote(_note); // update note cache\n }\n\n context.replace(actionNoop()); // trigger redraw\n }\n\n\n function end() {\n context.replace(actionNoop()); // trigger redraw\n\n context\n .selectedNoteID(_note.id)\n .enter(modeSelectNote(context, _note.id));\n }\n\n\n var drag = behaviorDrag()\n .selector('.layer-touch.markers .target.note.new')\n .surface(context.container().select('.main-map').node())\n .origin(origin)\n .on('start', start)\n .on('move', move)\n .on('end', end);\n\n\n mode.enter = function() {\n context.install(edit);\n };\n\n\n mode.exit = function() {\n context.ui().sidebar.hover.cancel();\n context.uninstall(edit);\n\n context.surface()\n .selectAll('.active')\n .classed('active', false);\n\n stopNudge();\n };\n\n mode.behavior = drag;\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiDataHeader() {\n var _datum;\n\n\n function dataHeader(selection) {\n var header = selection.selectAll('.data-header')\n .data(\n (_datum ? [_datum] : []),\n function(d) { return d.__featurehash__; }\n );\n\n header.exit()\n .remove();\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'data-header');\n\n var iconEnter = headerEnter\n .append('div')\n .attr('class', 'data-header-icon');\n\n iconEnter\n .append('div')\n .attr('class', 'preset-icon-28')\n .call(svgIcon('#iD-icon-data', 'note-fill'));\n\n headerEnter\n .append('div')\n .attr('class', 'data-header-label')\n .call(t.append('map_data.layers.custom.title'));\n }\n\n\n dataHeader.datum = function(val) {\n if (!arguments.length) return _datum;\n _datum = val;\n return this;\n };\n\n\n return dataHeader;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { utilGetSetValue, utilRebind, utilTriggerEvent } from '../util';\n\n\n// This code assumes that the combobox values will not have duplicate entries.\n// It is keyed on the `value` of the entry. Data should be an array of objects like:\n// [{\n// value: 'string value', // required\n// display: 'label function' // optional, if present will be called with d3 selection\n// to modify/append, see localizer's t.append\n// title: 'hover text' // optional\n// terms: ['search terms'] // optional\n// }, ...]\n\nvar _comboHideTimerID;\n\nexport function uiCombobox(context, klass) {\n var dispatch = d3_dispatch('accept', 'cancel', 'update');\n var container = context.container();\n\n var _suggestions = [];\n var _data = [];\n var _fetched = {};\n var _selected = null;\n var _canAutocomplete = true;\n var _caseSensitive = false;\n var _cancelFetch = false;\n var _minItems = 2;\n var _tDown = 0;\n var _mouseEnterHandler, _mouseLeaveHandler;\n\n var _fetcher = function(val, cb) {\n cb(_data.filter(function(d) {\n var terms = d.terms || [];\n terms.push(d.value);\n if (d.key) {\n terms.push(d.key);\n }\n return terms.some(function(term) {\n return term\n .toString()\n .toLowerCase()\n .indexOf(val.toLowerCase()) !== -1;\n });\n }));\n };\n\n var combobox = function(input, attachTo) {\n if (!input || input.empty()) return;\n\n input\n .classed('combobox-input', true)\n .on('focus.combo-input', focus)\n .on('blur.combo-input', blur)\n .on('keydown.combo-input', keydown)\n .on('keyup.combo-input', keyup)\n .on('input.combo-input', change)\n .on('mousedown.combo-input', mousedown)\n .each(function() {\n var parent = this.parentNode;\n var sibling = this.nextSibling;\n\n d3_select(parent).selectAll('.combobox-caret')\n .filter(function(d) { return d === input.node(); })\n .data([input.node()])\n .enter()\n .insert('div', function() { return sibling; })\n .attr('class', 'combobox-caret')\n .on('mousedown.combo-caret', function(d3_event) {\n d3_event.preventDefault(); // don't steal focus from input\n input.node().focus(); // focus the input as if it was clicked\n mousedown(d3_event);\n })\n .on('mouseup.combo-caret', function(d3_event) {\n d3_event.preventDefault(); // don't steal focus from input\n mouseup(d3_event);\n });\n });\n\n\n function mousedown(d3_event) {\n if (d3_event.button !== 0) return; // left click only\n if (input.classed('disabled')) return;\n _tDown = +new Date();\n\n // mousedown should never bubble up (see #10481)\n d3_event.stopPropagation();\n\n // clear selection\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n if (start !== end) {\n var val = utilGetSetValue(input);\n input.node().setSelectionRange(val.length, val.length);\n return;\n }\n\n input.on('mouseup.combo-input', mouseup);\n }\n\n\n function mouseup(d3_event) {\n input.on('mouseup.combo-input', null);\n if (d3_event.button !== 0) return; // left click only\n if (input.classed('disabled')) return;\n if (input.node() !== document.activeElement) return; // exit if this input is not focused\n\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n if (start !== end) return; // exit if user is selecting\n\n // not showing or showing for a different field - try to show it.\n var combo = container.selectAll('.combobox');\n if (combo.empty() || combo.datum() !== input.node()) {\n var tOrig = _tDown;\n window.setTimeout(function() {\n if (tOrig !== _tDown) return; // exit if user double clicked\n fetchComboData('', function() {\n show();\n render();\n });\n }, 250);\n\n } else {\n hide();\n }\n }\n\n\n function focus() {\n fetchComboData(''); // prefetch values (may warm taginfo cache)\n }\n\n\n function blur() {\n _comboHideTimerID = window.setTimeout(hide, 75);\n }\n\n\n function show() {\n hide(); // remove any existing\n\n container\n .insert('div', ':first-child')\n .datum(input.node())\n .attr('class', 'combobox' + (klass ? ' combobox-' + klass : ''))\n .style('position', 'absolute')\n .style('display', 'block')\n .style('left', '0px')\n .on('mousedown.combo-container', function (d3_event) {\n // prevent moving focus out of the input field\n d3_event.preventDefault();\n });\n\n container\n .on('scroll.combo-scroll', render, true);\n }\n\n function hide() {\n _hide(container);\n }\n\n\n function keydown(d3_event) {\n var shown = !container.selectAll('.combobox').empty();\n var tagName = input.node() ? input.node().tagName.toLowerCase() : '';\n\n switch (d3_event.keyCode) {\n case 8: // \u232B Backspace\n case 46: // \u2326 Delete\n d3_event.stopPropagation();\n _selected = null;\n render();\n input.on('input.combo-input', function() {\n var start = input.property('selectionStart');\n input.node().setSelectionRange(start, start);\n input.on('input.combo-input', change); // reset event handler\n change(false);\n });\n break;\n\n case 9: // \u21E5 Tab\n accept(d3_event);\n break;\n\n case 13: // \u21A9 Return\n d3_event.preventDefault();\n d3_event.stopPropagation();\n accept(d3_event);\n break;\n\n case 38: // \u2191 Up arrow\n if (tagName === 'textarea' && !shown) return;\n d3_event.preventDefault();\n if (tagName === 'input' && !shown) {\n show();\n }\n nav(-1);\n break;\n\n case 40: // \u2193 Down arrow\n if (tagName === 'textarea' && !shown) return;\n d3_event.preventDefault();\n if (tagName === 'input' && !shown) {\n show();\n }\n nav(+1);\n break;\n }\n }\n\n\n function keyup(d3_event) {\n switch (d3_event.keyCode) {\n case 27: // \u238B Escape\n cancel();\n break;\n }\n }\n\n\n // Called whenever the input value is changed (e.g. on typing)\n function change(doAutoComplete) {\n if (doAutoComplete === undefined) doAutoComplete = true;\n fetchComboData(value(), function(skipAutosuggest) {\n _selected = null;\n var val = input.property('value');\n\n if (_suggestions.length) {\n if (doAutoComplete && !skipAutosuggest && input.property('selectionEnd') === val.length) {\n _selected = tryAutocomplete();\n }\n\n if (!_selected) {\n _selected = val;\n }\n }\n\n if (val.length) {\n var combo = container.selectAll('.combobox');\n if (combo.empty()) {\n show();\n }\n } else {\n hide();\n }\n\n render();\n });\n }\n\n\n // Called when the user presses up/down arrows to navigate the list\n function nav(dir) {\n if (_suggestions.length) {\n // try to determine previously selected index..\n var index = -1;\n for (var i = 0; i < _suggestions.length; i++) {\n if (_selected && _suggestions[i].value === _selected) {\n index = i;\n break;\n }\n }\n\n // pick new _selected\n index = Math.max(Math.min(index + dir, _suggestions.length - 1), 0);\n _selected = _suggestions[index].value;\n utilGetSetValue(input, _selected);\n dispatch.call('update');\n }\n\n render();\n ensureVisible();\n }\n\n\n function ensureVisible() {\n var combo = container.selectAll('.combobox');\n if (combo.empty()) return;\n\n var containerRect = container.node().getBoundingClientRect();\n var comboRect = combo.node().getBoundingClientRect();\n\n if (comboRect.bottom > containerRect.bottom) {\n var node = attachTo ? attachTo.node() : input.node();\n node.scrollIntoView({ behavior: 'instant', block: 'center' });\n render();\n }\n\n // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move\n var selected = combo.selectAll('.combobox-option.selected').node();\n if (selected) {\n selected.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' });\n }\n }\n\n\n function value() {\n var value = input.property('value');\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n\n if (start && end) {\n value = value.substring(0, start);\n }\n\n return value;\n }\n\n\n function fetchComboData(v, cb) {\n _cancelFetch = false;\n\n _fetcher.call(input, v, function(results, skipAutosuggest) {\n // already chose a value, don't overwrite or autocomplete it\n if (_cancelFetch) return;\n\n _suggestions = results;\n results.forEach(function(d) { _fetched[d.value] = d; });\n\n if (cb) {\n cb(skipAutosuggest);\n }\n });\n }\n\n\n function tryAutocomplete() {\n if (!_canAutocomplete) return;\n\n var val = _caseSensitive ? value() : value().toLowerCase();\n if (!val) return;\n\n // Don't autocomplete if user is typing a number - #4935\n if (isFinite(val)) return;\n\n const suggestionValues = [];\n _suggestions.forEach(s => {\n suggestionValues.push(s.value);\n if (s.key && s.key !== s.value) {\n suggestionValues.push(s.key);\n }\n });\n\n var bestIndex = -1;\n for (var i = 0; i < suggestionValues.length; i++) {\n var suggestion = suggestionValues[i];\n var compare = _caseSensitive ? suggestion : suggestion.toLowerCase();\n\n // if search string matches suggestion exactly, pick it..\n if (compare === val) {\n bestIndex = i;\n break;\n\n // otherwise lock in the first result that starts with the search string..\n } else if (bestIndex === -1 && compare.indexOf(val) === 0) {\n bestIndex = i;\n }\n }\n\n if (bestIndex !== -1) {\n var bestVal = suggestionValues[bestIndex];\n input.property('value', bestVal);\n input.node().setSelectionRange(val.length, bestVal.length);\n dispatch.call('update');\n return bestVal;\n }\n }\n\n\n function render() {\n if (_suggestions.length < _minItems || document.activeElement !== input.node()) {\n hide();\n return;\n }\n\n var shown = !container.selectAll('.combobox').empty();\n if (!shown) return;\n\n var combo = container.selectAll('.combobox');\n var options = combo.selectAll('.combobox-option')\n .data(_suggestions, function(d) { return d.value; });\n\n options.exit()\n .remove();\n\n // enter/update\n const enter = options.enter()\n .append('a')\n .attr('class', function(d) {\n return 'combobox-option ' + (d.klass || '') + (d.description ? ' has-description' : '');\n })\n .attr('title', function(d) { return d.title; });\n\n enter.each(function(d) {\n const sel = d3_select(this);\n const labelSpan = sel.append('span')\n .attr('class', 'combobox-option-label');\n if (d.display) {\n d.display(labelSpan);\n } else {\n labelSpan.text(d.value);\n }\n if (d.description) {\n sel.append('span')\n .attr('class', 'combobox-option-description')\n .text(d.description);\n }\n });\n\n enter\n .on('mouseenter', _mouseEnterHandler)\n .on('mouseleave', _mouseLeaveHandler)\n .merge(options)\n .classed('selected', function(d) { return d.value === _selected || d.key === _selected; })\n .on('click.combo-option', accept)\n .order();\n\n var node = attachTo ? attachTo.node() : input.node();\n var containerRect = container.node().getBoundingClientRect();\n var rect = node.getBoundingClientRect();\n\n combo\n .style('left', (rect.left + 5 - containerRect.left) + 'px')\n .style('width', (rect.width - 10) + 'px')\n .style('top', (rect.height + rect.top - containerRect.top) + 'px');\n }\n\n\n // Dispatches an 'accept' event\n // Then hides the combobox.\n function accept(d3_event, d) {\n _cancelFetch = true;\n var thiz = input.node();\n\n if (d) { // user clicked on a suggestion\n utilGetSetValue(input, d.value); // replace field contents\n utilTriggerEvent(input, 'change');\n }\n\n // clear (and keep) selection\n var val = utilGetSetValue(input);\n thiz.setSelectionRange(val.length, val.length);\n\n if (!d) {\n d = _fetched[val];\n }\n dispatch.call('accept', thiz, d, val);\n hide();\n }\n\n\n // Dispatches an 'cancel' event\n // Then hides the combobox.\n function cancel() {\n _cancelFetch = true;\n var thiz = input.node();\n\n // clear (and remove) selection, and replace field contents\n var val = utilGetSetValue(input);\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n val = val.slice(0, start) + val.slice(end);\n utilGetSetValue(input, val);\n thiz.setSelectionRange(val.length, val.length);\n\n dispatch.call('cancel', thiz);\n\n hide();\n }\n\n };\n\n\n combobox.canAutocomplete = function(val) {\n if (!arguments.length) return _canAutocomplete;\n _canAutocomplete = val;\n return combobox;\n };\n\n combobox.caseSensitive = function(val) {\n if (!arguments.length) return _caseSensitive;\n _caseSensitive = val;\n return combobox;\n };\n\n combobox.data = function(val) {\n if (!arguments.length) return _data;\n _data = val;\n return combobox;\n };\n\n combobox.fetcher = function(val) {\n if (!arguments.length) return _fetcher;\n _fetcher = val;\n return combobox;\n };\n\n combobox.minItems = function(val) {\n if (!arguments.length) return _minItems;\n _minItems = val;\n return combobox;\n };\n\n combobox.itemsMouseEnter = function(val) {\n if (!arguments.length) return _mouseEnterHandler;\n _mouseEnterHandler = val;\n return combobox;\n };\n\n combobox.itemsMouseLeave = function(val) {\n if (!arguments.length) return _mouseLeaveHandler;\n _mouseLeaveHandler = val;\n return combobox;\n };\n\n return utilRebind(combobox, dispatch, 'on');\n}\n\n\nfunction _hide(container) {\n if (_comboHideTimerID) {\n window.clearTimeout(_comboHideTimerID);\n _comboHideTimerID = undefined;\n }\n\n container.selectAll('.combobox')\n .remove();\n\n container\n .on('scroll.combo-scroll', null);\n}\n\n\nuiCombobox.off = function(input, context) {\n _hide(context.container());\n input\n .on('focus.combo-input', null)\n .on('blur.combo-input', null)\n .on('keydown.combo-input', null)\n .on('keyup.combo-input', null)\n .on('input.combo-input', null)\n .on('mousedown.combo-input', null)\n .on('mouseup.combo-input', null);\n\n\n context.container()\n .on('scroll.combo-scroll', null);\n};\n", "import { select as d3_select } from 'd3-selection';\n\n\n// toggles the visibility of ui elements, using a combination of the\n// hide class, which sets display=none, and a d3 transition for opacity.\n// this will cause blinking when called repeatedly, so check that the\n// value actually changes between calls.\n//\n// When the selection is a direct child of a

    element, the\n// parent's `open` property is used instead of the `hide` class.\nexport function uiToggle(show, callback) {\n return function(selection) {\n const parent = selection.node().parentNode;\n const isDetails = parent && parent.tagName === 'DETAILS';\n\n // ensure content is visible before animating\n if (isDetails) {\n if (show) parent.open = true;\n } else {\n selection.classed('hide', false);\n }\n\n selection\n .style('opacity', show ? 0 : 1)\n .transition()\n .style('opacity', show ? 1 : 0)\n .on('end', function() {\n d3_select(this).style('opacity', null);\n // hide content after fade-out completes\n if (isDetails) {\n if (!show) parent.open = false;\n } else {\n d3_select(this).classed('hide', !show);\n }\n if (callback) callback.apply(this);\n });\n };\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from '../core/preferences';\nimport { svgIcon } from '../svg/icon';\nimport { utilFunctor } from '../util';\nimport { utilRebind } from '../util/rebind';\nimport { uiToggle } from './toggle';\nimport { t, localizer } from '../core/localizer';\n\n\nexport function uiDisclosure(context, key, expandedDefault) {\n const dispatch = d3_dispatch('toggled');\n let _expanded;\n let _label = utilFunctor('');\n let _updatePreference = true;\n let _content = function () {};\n\n\n const disclosure = function(selection) {\n\n if (_expanded === undefined || _expanded === null) {\n // loading _expanded here allows it to be reset by calling `disclosure.expanded(null)`\n\n const preference = prefs('disclosure.' + key + '.expanded');\n _expanded = preference === null ? !!expandedDefault : (preference === 'true');\n }\n\n let details = selection.selectAll('.disclosure-wrap-' + key)\n .data([0]);\n\n // enter\n const detailsEnter = details.enter()\n .append('details')\n .attr('class', 'disclosure-wrap disclosure-wrap-' + key);\n\n const summaryEnter = detailsEnter\n .append('summary')\n .attr('class', 'hide-toggle hide-toggle-' + key)\n .call(svgIcon('', 'pre-text', 'hide-toggle-icon'));\n\n summaryEnter\n .append('span')\n .attr('class', 'hide-toggle-text');\n\n detailsEnter\n .append('div')\n .attr('class', 'disclosure-content');\n\n // update\n details = detailsEnter\n .merge(details);\n\n details\n .property('open', _expanded);\n\n const summary = details.selectAll('summary.hide-toggle');\n\n summary\n .on('click', toggle);\n\n updateSummary();\n\n const label = _label();\n const labelSelection = summary.selectAll('.hide-toggle-text');\n if (typeof label !== 'function') {\n labelSelection.text(label);\n } else {\n labelSelection.text('').call(label);\n }\n\n const contentWrap = details.selectAll('.disclosure-content');\n\n if (_expanded) {\n contentWrap\n .call(_content);\n }\n\n\n function updateSummary() {\n summary\n .classed('expanded', _expanded)\n .attr('title', t(`icons.${_expanded ? 'collapse' : 'expand'}`));\n\n summary.selectAll('.hide-toggle-icon')\n .attr('xlink:href', _expanded ? '#iD-icon-down'\n : (localizer.textDirection() === 'rtl') ? '#iD-icon-backward' : '#iD-icon-forward'\n );\n }\n\n\n function toggle(d3_event) {\n d3_event.preventDefault();\n\n _expanded = !_expanded;\n\n if (_updatePreference) {\n prefs('disclosure.' + key + '.expanded', _expanded);\n }\n\n updateSummary();\n\n contentWrap.call(uiToggle(_expanded));\n\n if (_expanded) {\n contentWrap.call(_content);\n }\n\n dispatch.call('toggled', this, _expanded);\n }\n };\n\n\n disclosure.label = function(val) {\n if (!arguments.length) return _label;\n _label = utilFunctor(val);\n return disclosure;\n };\n\n\n disclosure.expanded = function(val) {\n if (!arguments.length) return _expanded;\n _expanded = val;\n return disclosure;\n };\n\n\n disclosure.updatePreference = function(val) {\n if (!arguments.length) return _updatePreference;\n _updatePreference = val;\n return disclosure;\n };\n\n\n disclosure.content = function(val) {\n if (!arguments.length) return _content;\n _content = val;\n return disclosure;\n };\n\n\n return utilRebind(disclosure, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { uiDisclosure } from './disclosure';\nimport { utilFunctor } from '../util';\n\n// A unit of controls or info to be used in a layout, such as within a pane.\n// Can be labeled and collapsible.\nexport function uiSection(id, context) {\n\n var _classes = utilFunctor('');\n var _shouldDisplay;\n var _content;\n\n var _disclosure;\n var _label;\n var _expandedByDefault = utilFunctor(true);\n var _disclosureContent;\n var _disclosureExpanded;\n\n var _containerSelection = d3_select(null);\n\n var section = {\n id: id\n };\n\n section.classes = function(val) {\n if (!arguments.length) return _classes;\n _classes = utilFunctor(val);\n return section;\n };\n\n section.label = function(val) {\n if (!arguments.length) return _label;\n _label = utilFunctor(val);\n return section;\n };\n\n section.expandedByDefault = function(val) {\n if (!arguments.length) return _expandedByDefault;\n _expandedByDefault = utilFunctor(val);\n return section;\n };\n\n section.shouldDisplay = function(val) {\n if (!arguments.length) return _shouldDisplay;\n _shouldDisplay = utilFunctor(val);\n return section;\n };\n\n section.content = function(val) {\n if (!arguments.length) return _content;\n _content = val;\n return section;\n };\n\n section.disclosureContent = function(val) {\n if (!arguments.length) return _disclosureContent;\n _disclosureContent = val;\n return section;\n };\n\n section.disclosureExpanded = function(val) {\n if (!arguments.length) return _disclosureExpanded;\n _disclosureExpanded = val;\n return section;\n };\n\n // may be called multiple times\n section.render = function(selection) {\n\n _containerSelection = selection\n .selectAll('.section-' + id)\n .data([0]);\n\n var sectionEnter = _containerSelection\n .enter()\n .append('div')\n .attr('class', 'section section-' + id + ' ' + (_classes && _classes() || ''));\n\n _containerSelection = sectionEnter\n .merge(_containerSelection);\n\n _containerSelection\n .call(renderContent);\n };\n\n section.reRender = function() {\n _containerSelection\n .call(renderContent);\n };\n\n section.selection = function() {\n return _containerSelection;\n };\n\n section.disclosure = function() {\n return _disclosure;\n };\n\n // may be called multiple times\n function renderContent(selection) {\n if (_shouldDisplay) {\n var shouldDisplay = _shouldDisplay();\n selection.classed('hide', !shouldDisplay);\n if (!shouldDisplay) {\n selection.html('');\n return;\n }\n }\n\n if (_disclosureContent) {\n if (!_disclosure) {\n _disclosure = uiDisclosure(context, id.replace(/-/g, '_'), _expandedByDefault())\n .label(_label || '')\n /*.on('toggled', function(expanded) {\n if (expanded) { selection.node().parentNode.scrollTop += 200; }\n })*/\n .content(_disclosureContent);\n }\n if (_disclosureExpanded !== undefined) {\n _disclosure.expanded(_disclosureExpanded);\n _disclosureExpanded = undefined;\n }\n selection\n .call(_disclosure);\n\n return;\n }\n\n if (_content) {\n selection\n .call(_content);\n }\n }\n\n return section;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\n\n\n// Pass `what` object of the form:\n// {\n// key: 'string', // required\n// value: 'string' // optional\n// }\n// -or-\n// {\n// qid: 'string' // brand wikidata (e.g. 'Q37158')\n// }\n//\nexport function uiTagReference(what) {\n var wikibase = what.qid ? services.wikidata : services.osmWikibase;\n var tagReference = {};\n\n var _button = d3_select(null);\n var _body = d3_select(null);\n var _loaded;\n var _showing;\n\n\n function load() {\n if (!wikibase) return;\n\n _button\n .classed('tag-reference-loading', true);\n\n wikibase.getDocs(what, gotDocs);\n }\n\n\n function gotDocs(err, docs) {\n _body.html('');\n\n if (!docs || !docs.title) {\n _body\n .append('p')\n .attr('class', 'tag-reference-description')\n .call(t.append('inspector.no_documentation_key'));\n done();\n return;\n }\n\n if (docs.imageURL) {\n _body\n .append('img')\n .attr('class', 'tag-reference-wiki-image')\n .attr('alt', docs.title)\n .attr('src', docs.imageURL)\n .on('load', function() { done(); })\n .on('error', function() { d3_select(this).remove(); done(); });\n } else {\n done();\n }\n\n var tagReferenceDescription = _body\n .append('p')\n .attr('class', 'tag-reference-description')\n .append('span');\n if (docs.description) {\n tagReferenceDescription = tagReferenceDescription\n .attr('class', 'localized-text')\n .attr('lang', docs.descriptionLocaleCode || 'und')\n .call(docs.description);\n } else {\n tagReferenceDescription = tagReferenceDescription\n .call(t.append('inspector.no_documentation_key'));\n }\n tagReferenceDescription\n .append('a')\n .attr('class', 'tag-reference-edit')\n .attr('target', '_blank')\n .attr('title', t('inspector.edit_reference'))\n .attr('href', docs.editURL)\n .call(svgIcon('#iD-icon-edit', 'inline'));\n\n if (docs.wiki) {\n _body\n .append('a')\n .attr('class', 'tag-reference-link')\n .attr('target', '_blank')\n .attr('href', docs.wiki.url)\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append(docs.wiki.text));\n }\n\n // Add link to info about \"good changeset comments\" - #2923\n if (what.key === 'comment') {\n _body\n .append('a')\n .attr('class', 'tag-reference-comment-link')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', t('commit.about_changeset_comments_link'))\n .append('span')\n .call(t.append('commit.about_changeset_comments'));\n }\n }\n\n\n function done() {\n _loaded = true;\n\n _button\n .classed('tag-reference-loading', false);\n\n _body\n .classed('expanded', true)\n .transition()\n .duration(200)\n .style('max-height', '200px')\n .style('opacity', '1');\n\n _showing = true;\n\n _button.selectAll('svg.icon use').each(function() {\n var iconUse = d3_select(this);\n if (iconUse.attr('href') === '#iD-icon-info') {\n iconUse.attr('href', '#iD-icon-info-filled');\n }\n });\n }\n\n\n function hide() {\n _body\n .transition()\n .duration(200)\n .style('max-height', '0px')\n .style('opacity', '0')\n .on('end', function () {\n _body.classed('expanded', false);\n });\n\n _showing = false;\n\n _button.selectAll('svg.icon use').each(function() {\n var iconUse = d3_select(this);\n if (iconUse.attr('href') === '#iD-icon-info-filled') {\n iconUse.attr('href', '#iD-icon-info');\n }\n });\n\n }\n\n\n tagReference.button = function(selection, klass, iconName) {\n _button = selection.selectAll('.tag-reference-button')\n .data([0]);\n\n _button = _button.enter()\n .append('button')\n .attr('class', 'tag-reference-button ' + (klass || ''))\n .attr('title', t('icons.information'))\n .call(svgIcon('#iD-icon-' + (iconName || 'inspect')))\n .merge(_button);\n\n _button\n .on('click', function (d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n this.blur(); // avoid keeping focus on the button - #4641\n if (_showing) {\n hide();\n } else if (_loaded) {\n done();\n } else {\n load();\n }\n });\n };\n\n\n tagReference.body = function(selection) {\n var itemID = what.qid || (what.key + '-' + (what.value || ''));\n _body = selection.selectAll('.tag-reference-body')\n .data([itemID], function(d) { return d; });\n\n _body.exit()\n .remove();\n\n _body = _body.enter()\n .append('div')\n .attr('class', 'tag-reference-body')\n .style('max-height', '0')\n .style('opacity', '0')\n .merge(_body);\n\n if (_showing === false) {\n hide();\n }\n };\n\n\n tagReference.showing = function(val) {\n if (!arguments.length) return _showing;\n _showing = val;\n return tagReference;\n };\n\n\n return tagReference;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { isEmpty } from 'es-toolkit/compat';\n\nimport { services } from '../../services';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCombobox } from '../combobox';\nimport { uiSection } from '../section';\nimport { uiTagReference } from '../tag_reference';\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { utilArrayDifference, utilArrayIdentical } from '../../util/array';\nimport { utilGetSetValue, utilNoAuto, utilRebind, utilTagDiff } from '../../util';\nimport { allowUpperCaseTagValues } from '../../osm/tags';\nimport { fileFetcher } from '../../core';\n\n\nexport function uiSectionRawTagEditor(id, context) {\n\n var section = uiSection(id, context)\n .classes('raw-tag-editor')\n .label(function() {\n var count = Object.keys(_tags).filter(function(d) { return d; }).length;\n return t.append('inspector.title_count', { title: t.append('inspector.tags'), count: count });\n })\n .expandedByDefault(false)\n .disclosureContent(renderDisclosureContent);\n\n var taginfo = services.taginfo;\n var dispatch = d3_dispatch('change');\n var availableViews = [\n { id: 'list', icon: '#fas-th-list' },\n { id: 'text', icon: '#fas-i-cursor' }\n ];\n\n let _discardTags = {};\n fileFetcher.get('discarded')\n .then((d) => { _discardTags = d; })\n .catch(() => { /* ignore */ });\n\n var _tagView = (prefs('raw-tag-editor-view') || 'list'); // 'list, 'text'\n var _readOnlyTags = [];\n // the keys in the order we want them to display\n var _orderedKeys = [];\n var _pendingChange = null;\n var _state;\n var _presets;\n var _tags;\n var _entityIDs;\n var _didInteract = false;\n\n function interacted() {\n _didInteract = true;\n }\n\n function renderDisclosureContent(wrap) {\n\n // remove deleted keys\n _orderedKeys = _orderedKeys.filter(function(key) {\n return _tags[key] !== undefined;\n });\n\n // When switching to a different entity or changing the state (hover/select)\n // reorder the keys alphabetically.\n // We trigger this by emptying the `_orderedKeys` array, then it will be rebuilt here.\n // Otherwise leave their order alone - #5857, #5927\n var all = Object.keys(_tags).sort();\n var missingKeys = utilArrayDifference(all, _orderedKeys);\n for (var i in missingKeys) {\n _orderedKeys.push(missingKeys[i]);\n }\n\n // assemble row data\n var rowData = _orderedKeys.map(function(key, i) {\n return { index: i, key: key, value: _tags[key] };\n });\n\n // append blank row last\n rowData.push({ index: rowData.length, key: '', value: '' });\n\n\n // View Options\n var options = wrap.selectAll('.raw-tag-options')\n .data([0]);\n\n options.exit()\n .remove();\n\n var optionsEnter = options.enter()\n .insert('div', ':first-child')\n .attr('class', 'raw-tag-options')\n .attr('role', 'tablist');\n\n var optionEnter = optionsEnter.selectAll('.raw-tag-option')\n .data(availableViews, function(d) { return d.id; })\n .enter();\n\n optionEnter\n .append('button')\n .attr('class', function(d) {\n return 'raw-tag-option raw-tag-option-' + d.id + (_tagView === d.id ? ' selected' : '');\n })\n .attr('aria-selected', function(d) { return _tagView === d.id; })\n .attr('role', 'tab')\n .attr('title', function(d) { return t('icons.' + d.id); })\n .on('click', function(d3_event, d) {\n _tagView = d.id;\n prefs('raw-tag-editor-view', d.id);\n\n wrap.selectAll('.raw-tag-option')\n .classed('selected', function(datum) { return datum === d; })\n .attr('aria-selected', function(datum) { return datum === d; });\n\n wrap.selectAll('.tag-text')\n .classed('hide', (d.id !== 'text'))\n .each(setTextareaHeight);\n\n wrap.selectAll('.tag-list, .add-row')\n .classed('hide', (d.id !== 'list'));\n })\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(d.icon));\n });\n\n\n // View as Text\n var textData = rowsToText(rowData);\n var textarea = wrap.selectAll('.tag-text')\n .data([0]);\n\n textarea = textarea.enter()\n .append('textarea')\n .attr('class', 'tag-text' + (_tagView !== 'text' ? ' hide' : ''))\n .call(utilNoAuto)\n .attr('placeholder', t('inspector.key_value'))\n .attr('spellcheck', 'false')\n .style('direction', 'ltr')\n .merge(textarea);\n\n textarea\n .call(utilGetSetValue, textData)\n .each(setTextareaHeight)\n .on('input', setTextareaHeight)\n .on('focus', interacted)\n .on('blur', textChanged)\n .on('change', textChanged);\n\n\n // View as List\n var list = wrap.selectAll('.tag-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'tag-list' + (_tagView !== 'list' ? ' hide' : ''))\n .merge(list);\n\n\n // Tag list items\n var items = list.selectAll('.tag-row')\n .data(rowData, d => d.key);\n\n items.exit()\n .each(unbind)\n .remove();\n\n\n // Enter\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', 'tag-row')\n .classed('readonly', isReadOnly);\n\n var innerWrap = itemsEnter.append('div')\n .attr('class', 'inner-wrap');\n\n innerWrap\n .append('div')\n .attr('class', 'key-wrap')\n .append('input')\n .property('type', 'text')\n .attr('class', 'key')\n .call(utilNoAuto)\n .on('focus', interacted)\n .on('blur', keyChange)\n .on('change', keyChange);\n\n innerWrap\n .append('div')\n .attr('class', 'value-wrap')\n .append('input')\n .property('type', 'text')\n .attr('dir', 'auto')\n .attr('class', 'value')\n .call(utilNoAuto)\n .on('focus', interacted)\n .on('blur', valueChange)\n .on('change', valueChange);\n\n innerWrap\n .append('button')\n .attr('tabindex', -1)\n .attr('class', 'form-field-button remove')\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'));\n\n\n // Update\n items = items\n .merge(itemsEnter)\n .sort(function(a, b) { return a.index - b.index; });\n\n items\n .classed('add-tag', d => d.key === '')\n .each(function(d) {\n var row = d3_select(this);\n var key = row.select('input.key'); // propagate bound data\n var value = row.select('input.value'); // propagate bound data\n\n if (_entityIDs && taginfo && _state !== 'hover') {\n bindTypeahead(key, value);\n }\n\n var referenceOptions = { key: d.key };\n if (typeof d.value === 'string') {\n referenceOptions.value = d.value;\n }\n var reference = uiTagReference(referenceOptions, context);\n\n if (_state === 'hover') {\n reference.showing(false);\n }\n\n row.select('.inner-wrap') // propagate bound data\n .call(reference.button)\n .select('.tag-reference-button')\n .attr('tabindex', -1)\n .classed('disabled', d => d.key === '')\n .attr('disabled', d => d.key === '' ? 'disabled' : null);\n\n row.call(reference.body);\n\n row.select('button.remove'); // propagate bound data\n });\n\n items.selectAll('input.key')\n .attr('title', function(d) { return d.key; })\n .attr('placeholder', function(d) {\n return d.key === '' ? t('inspector.add_tag') : null;\n })\n .attr('readonly', function(d) {\n return isReadOnly(d) || null;\n })\n .call(utilGetSetValue,\n d => d.key,\n (_, newKey) => _pendingChange === null || isEmpty(_pendingChange) || _pendingChange[newKey] // if there are pending changes: skip untouched tags\n );\n\n items.selectAll('input.value')\n .attr('title', function(d) {\n return Array.isArray(d.value) ? d.value.filter(Boolean).join('\\n') : d.value;\n })\n .classed('mixed', function(d) {\n return Array.isArray(d.value);\n })\n .attr('placeholder', function(d) {\n return Array.isArray(d.value) ? t('inspector.multiple_values') : null;\n })\n .attr('readonly', function(d) {\n return isReadOnly(d) || null;\n })\n .call(utilGetSetValue, d => {\n if (_pendingChange !== null && !isEmpty(_pendingChange) && !_pendingChange[d.key]) {\n // if there are pending changes: skip untouched tags\n return null;\n }\n return Array.isArray(d.value) ? '' : d.value;\n });\n\n items.selectAll('button.remove')\n .classed('disabled', d => d.key === '')\n .on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'down', // 'click' fires too late - #5878\n (d3_event, d) => {\n if (d3_event.button !== 0) return;\n removeTag(d3_event, d);\n });\n\n }\n\n function isReadOnly(d) {\n for (var i = 0; i < _readOnlyTags.length; i++) {\n if (d.key.match(_readOnlyTags[i]) !== null) {\n return true;\n }\n }\n return false;\n }\n\n function setTextareaHeight() {\n if (_tagView !== 'text') return;\n\n var selection = d3_select(this);\n var matches = selection.node().value.match(/\\n/g);\n var lineCount = 2 + Number(matches && matches.length);\n var lineHeight = 20;\n\n selection.style('height', lineCount * lineHeight + 'px');\n }\n\n function stringify(s) {\n const stringified = JSON.stringify(s).slice(1, -1); // without leading/trailing \"\n if (stringified !== s) {\n return `\"${stringified}\"`;\n } else {\n return s;\n }\n }\n\n function unstringify(s) {\n const isQuoted = s.length > 1 && s.charAt(0) === '\"' && s.charAt(s.length - 1) === '\"';\n if (isQuoted) {\n try {\n return JSON.parse(s);\n } catch {\n return s;\n }\n } else {\n return s;\n }\n }\n\n function rowsToText(rows) {\n var str = rows\n .filter(function(row) { return row.key && row.key.trim() !== ''; })\n .map(function(row) {\n var rawVal = row.value;\n if (Array.isArray(rawVal)) rawVal = '*';\n var val = rawVal ? stringify(rawVal) : '';\n return stringify(row.key) + '=' + val;\n })\n .join('\\n');\n\n if (_state !== 'hover' && str.length) {\n return str + '\\n';\n }\n return str;\n }\n\n function textChanged() {\n var newText = this.value.trim();\n var newTags = {};\n newText.split('\\n').forEach(function(row) {\n var m = row.match(/^\\s*([^=]+)=(.*)$/);\n if (m !== null) {\n var k = context.cleanTagKey(unstringify(m[1].trim()));\n var v = context.cleanTagValue(unstringify(m[2].trim()));\n newTags[k] = v;\n }\n });\n\n var tagDiff = utilTagDiff(_tags, newTags);\n\n _pendingChange = _pendingChange || {};\n\n tagDiff.forEach(function(change) {\n if (isReadOnly({ key: change.key })) return;\n\n // skip unchanged multiselection placeholders\n if (change.newVal === '*' && Array.isArray(change.oldVal)) return;\n\n if (change.type === '-') {\n _pendingChange[change.key] = undefined;\n } else if (change.type === '+') {\n _pendingChange[change.key] = change.newVal || '';\n }\n });\n\n if (isEmpty(_pendingChange)) {\n _pendingChange = null;\n section.reRender();\n return;\n }\n\n scheduleChange();\n }\n\n function bindTypeahead(key, value) {\n if (isReadOnly(key.datum())) return;\n\n if (Array.isArray(value.datum().value)) {\n value.call(uiCombobox(context, 'tag-value')\n .minItems(1)\n .fetcher(function(value, callback) {\n var keyString = utilGetSetValue(key);\n if (!_tags[keyString]) return;\n var data = _tags[keyString].map(function(tagValue) {\n if (!tagValue) {\n return {\n value: ' ',\n title: t('inspector.empty'),\n display: selection => selection.text('')\n .classed('virtual-option', true)\n .call(t.append('inspector.empty'))\n };\n }\n return {\n value: tagValue,\n title: tagValue\n };\n });\n callback(data);\n }));\n return;\n }\n\n var geometry = context.graph().geometry(_entityIDs[0]);\n\n key.call(uiCombobox(context, 'tag-key')\n .fetcher(function(value, callback) {\n taginfo.keys({\n debounce: true,\n geometry: geometry,\n query: value\n }, function(err, data) {\n if (!err) {\n const filtered = data\n .filter(d => _tags[d.value] === undefined) // already used tag\n .filter(d => !(d.value in _discardTags)) // do not suggest discardable tags (see #9817)\n .filter(d => !/_\\d$/.test(d.value)) // tag like name_1 (see #9422)\n .filter(d => d.value.toLowerCase().includes(value.toLowerCase())); // tag does not match user input\n callback(sort(value, filtered));\n }\n });\n }));\n\n value.call(uiCombobox(context, 'tag-value')\n .fetcher(function(value, callback) {\n taginfo.values({\n debounce: true,\n key: utilGetSetValue(key),\n geometry: geometry,\n query: value\n }, function(err, data) {\n if (!err) {\n const filtered = data.filter(d => d.value.toLowerCase().includes(value.toLowerCase()));\n callback(sort(value, filtered));\n }\n });\n })\n .caseSensitive(allowUpperCaseTagValues.test(utilGetSetValue(key))));\n\n\n function sort(value, data) {\n var sameletter = [];\n var other = [];\n for (var i = 0; i < data.length; i++) {\n if (data[i].value.substring(0, value.length) === value) {\n sameletter.push(data[i]);\n } else {\n other.push(data[i]);\n }\n }\n return sameletter.concat(other);\n }\n }\n\n function unbind() {\n var row = d3_select(this);\n\n row.selectAll('input.key')\n .call(uiCombobox.off, context);\n\n row.selectAll('input.value')\n .call(uiCombobox.off, context);\n }\n\n function keyChange(d3_event, d) {\n const input = d3_select(this);\n if (input.attr('readonly')) return;\n\n var kOld = d.key;\n\n // exit if we are currently about to delete this row anyway - #6366\n if (_pendingChange && _pendingChange.hasOwnProperty(kOld) && _pendingChange[kOld] === undefined) return;\n\n var kNew = context.cleanTagKey(this.value.trim());\n\n // allow no change if the key should be readonly\n if (isReadOnly({ key: kNew })) {\n this.value = kOld;\n return;\n }\n\n if (kNew !== this.value) {\n utilGetSetValue(input, kNew);\n }\n\n if (kNew &&\n kNew !== kOld &&\n _tags[kNew] !== undefined) {\n // new key is already in use, switch focus to the existing row\n\n this.value = kOld; // reset the key\n section.selection().selectAll('.tag-list input.value')\n .each(function(d) {\n if (d.key === kNew) { // send focus to that other value combo instead\n var input = d3_select(this).node();\n input.focus();\n input.select();\n }\n });\n return;\n }\n\n\n _pendingChange = _pendingChange || {};\n\n if (kOld) {\n if (kOld === kNew) return;\n // a tag key was renamed\n _pendingChange[kNew] = _pendingChange[kOld] || { oldKey: kOld };\n _pendingChange[kOld] = undefined;\n } else {\n // a new tag was added\n let row = this.parentNode.parentNode;\n let inputVal = d3_select(row).selectAll('input.value');\n let vNew = context.cleanTagValue(utilGetSetValue(inputVal));\n _pendingChange[kNew] = vNew;\n utilGetSetValue(inputVal, vNew);\n }\n\n // update the ordered key index so this row doesn't change position\n var existingKeyIndex = _orderedKeys.indexOf(kOld);\n if (existingKeyIndex !== -1) _orderedKeys[existingKeyIndex] = kNew;\n\n d.key = kNew; // update datum to avoid exit/enter on tag update\n\n this.value = kNew;\n scheduleChange();\n }\n\n function valueChange(d3_event, d) {\n if (isReadOnly(d)) return;\n\n // exit if this is a multiselection and no value was entered\n if (Array.isArray(d.value) && !this.value) return;\n\n // exit if we are currently about to delete this row anyway - #6366\n if (_pendingChange && _pendingChange.hasOwnProperty(d.key) && _pendingChange[d.key] === undefined) return;\n\n _pendingChange = _pendingChange || {};\n\n const vNew = context.cleanTagValue(this.value);\n if (vNew !== this.value) {\n utilGetSetValue(d3_select(this), vNew);\n }\n\n _pendingChange[d.key] = vNew;\n scheduleChange();\n }\n\n function removeTag(d3_event, d) {\n if (isReadOnly(d)) return;\n\n // remove the key from the ordered key index\n _orderedKeys = _orderedKeys.filter(function(key) { return key !== d.key; });\n\n _pendingChange = _pendingChange || {};\n _pendingChange[d.key] = undefined;\n scheduleChange();\n }\n\n function scheduleChange() {\n if (!_pendingChange) return;\n\n for (const key in _pendingChange) {\n _tags[key] = _pendingChange[key];\n }\n dispatch.call('change', this, _entityIDs, _pendingChange);\n _pendingChange = null;\n }\n\n\n section.state = function(val) {\n if (!arguments.length) return _state;\n if (_state !== val) {\n _orderedKeys = [];\n _state = val;\n }\n return section;\n };\n\n\n section.presets = function(val) {\n if (!arguments.length) return _presets;\n _presets = val;\n if (_presets && _presets.length && _presets[0].isFallback()) {\n section.disclosureExpanded(true);\n\n // don't collapse the disclosure if the mapper used the raw tag editor - #1881\n } else if (!_didInteract) {\n section.disclosureExpanded(null);\n }\n return section;\n };\n\n\n section.tags = function(val) {\n if (!arguments.length) return _tags;\n _tags = val;\n return section;\n };\n\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n if (!_entityIDs || !val || !utilArrayIdentical(_entityIDs, val)) {\n _entityIDs = val;\n _orderedKeys = [];\n }\n return section;\n };\n\n\n // pass an array of regular expressions to test against the tag key\n section.readOnlyTags = function(val) {\n if (!arguments.length) return _readOnlyTags;\n _readOnlyTags = val;\n return section;\n };\n\n\n return utilRebind(section, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\nimport { stringifyProperties } from '../util/object';\nimport { uiDataHeader } from './data_header';\nimport { uiSectionRawTagEditor } from './sections/raw_tag_editor';\n\n\nexport function uiDataEditor(context) {\n var dataHeader = uiDataHeader();\n var rawTagEditor = uiSectionRawTagEditor('custom-data-tag-editor', context)\n .expandedByDefault(true)\n .readOnlyTags([/.*/]);\n var _datum;\n\n\n function dataEditor(selection) {\n\n var header = selection.selectAll('.header')\n .data([0]);\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function() {\n context.enter(modeBrowse(context));\n })\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('map_data.title'));\n\n\n var body = selection.selectAll('.body')\n .data([0]);\n\n body = body.enter()\n .append('div')\n .attr('class', 'body')\n .merge(body);\n\n var editor = body.selectAll('.data-editor')\n .data([0]);\n\n // enter/update\n editor.enter()\n .append('div')\n .attr('class', 'modal-section data-editor')\n .merge(editor)\n .call(dataHeader.datum(_datum));\n\n var rte = body.selectAll('.raw-tag-editor')\n .data([0]);\n\n // enter/update\n rte.enter()\n .append('div')\n .attr('class', 'raw-tag-editor data-editor')\n .merge(rte)\n .call(rawTagEditor\n .tags(stringifyProperties(_datum?.properties || {}))\n .state('hover')\n .render\n )\n .selectAll('textarea.tag-text')\n .attr('readonly', true)\n .classed('readonly', true);\n }\n\n\n dataEditor.datum = function(val) {\n if (!arguments.length) return _datum;\n _datum = val;\n return this;\n };\n\n\n return dataEditor;\n}\n", "import { geoBounds as d3_geoBounds } from 'd3-geo';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { behaviorBreathe } from '../behavior/breathe';\nimport { behaviorHover } from '../behavior/hover';\nimport { behaviorLasso } from '../behavior/lasso';\nimport { behaviorSelect } from '../behavior/select';\n\nimport { t } from '../core/localizer';\n\nimport { geoExtent } from '../geo';\nimport { modeBrowse } from './browse';\nimport { modeDragNode } from './drag_node';\nimport { modeDragNote } from './drag_note';\nimport { uiDataEditor } from '../ui/data_editor';\nimport { utilKeybinding } from '../util';\n\n\nexport function modeSelectData(context, selectedDatum) {\n var mode = {\n id: 'select-data',\n button: 'browse'\n };\n\n var keybinding = utilKeybinding('select-data');\n var dataEditor = uiDataEditor(context);\n\n var behaviors = [\n behaviorBreathe(context),\n behaviorHover(context),\n behaviorSelect(context),\n behaviorLasso(context),\n modeDragNode(context).behavior,\n modeDragNote(context).behavior\n ];\n\n\n // class the data as selected, or return to browse mode if the data is gone\n function selectData(d3_event, drawn) {\n var selection = context.surface().selectAll('.layer-mapdata .data' + selectedDatum.__featurehash__);\n\n if (selection.empty()) {\n // Return to browse mode if selected DOM elements have\n // disappeared because the user moved them out of view..\n var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;\n if (drawn && source && (source.type === 'pointermove' || source.type === 'mousemove' || source.type === 'touchmove')) {\n context.enter(modeBrowse(context));\n }\n } else {\n selection.classed('selected', true);\n }\n }\n\n\n function esc() {\n if (context.container().select('.combobox').size()) return;\n context.enter(modeBrowse(context));\n }\n\n\n mode.zoomToSelected = function() {\n var extent = geoExtent(d3_geoBounds(selectedDatum));\n context.map().centerZoomEase(extent.center(), context.map().trimmedExtentZoom(extent));\n };\n\n\n mode.enter = function() {\n behaviors.forEach(context.install);\n\n keybinding\n .on(t('inspector.zoom_to.key'), mode.zoomToSelected)\n .on('\u238B', esc, true);\n\n d3_select(document)\n .call(keybinding);\n\n selectData();\n\n var sidebar = context.ui().sidebar;\n sidebar.show(dataEditor.datum(selectedDatum));\n\n // expand the sidebar, avoid obscuring the data if needed\n var extent = geoExtent(d3_geoBounds(selectedDatum));\n sidebar.expand(sidebar.intersects(extent));\n\n context.map()\n .on('drawn.select-data', selectData);\n };\n\n\n mode.exit = function() {\n behaviors.forEach(context.uninstall);\n\n d3_select(document)\n .call(keybinding.unbind);\n\n context.surface()\n .selectAll('.layer-mapdata .selected')\n .classed('selected hover', false);\n\n context.map()\n .on('drawn.select-data', null);\n\n context.ui().sidebar\n .hide();\n };\n\n\n return mode;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { modeSelect } from '../modes/select';\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { utilDisplayName, utilHighlightEntities } from '../util';\n\n\nexport function uiOsmoseDetails(context) {\n let _qaItem;\n\n function issueString(d, type) {\n if (!d) return '';\n\n // Issue strings are cached from Osmose API\n const s = services.osmose.getStrings(d.itemType);\n return (type in s) ? s[type] : '';\n }\n\n\n function osmoseDetails(selection) {\n const details = selection.selectAll('.error-details')\n .data(\n _qaItem ? [_qaItem] : [],\n d => `${d.id}-${d.status || 0}`\n );\n\n details.exit()\n .remove();\n\n const detailsEnter = details.enter()\n .append('div')\n .attr('class', 'error-details qa-details-container');\n\n\n // Description\n if (issueString(_qaItem, 'detail')) {\n const div = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n div\n .append('h4')\n .call(t.append('QA.keepRight.detail_description'));\n\n div\n .append('p')\n .attr('class', 'qa-details-description-text')\n .html(d => issueString(d, 'detail'))\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Elements (populated later as data is requested)\n const detailsDiv = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n const elemsDiv = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n // Suggested Fix (mustn't exist for every issue type)\n if (issueString(_qaItem, 'fix')) {\n const div = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n div\n .append('h4')\n .call(t.append('QA.osmose.fix_title'));\n\n div\n .append('p')\n .html(d => issueString(d, 'fix'))\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Common Pitfalls (mustn't exist for every issue type)\n if (issueString(_qaItem, 'trap')) {\n const div = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n div\n .append('h4')\n .call(t.append('QA.osmose.trap_title'));\n\n div\n .append('p')\n .html(d => issueString(d, 'trap'))\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Save current item to check if UI changed by time request resolves\n const thisItem = _qaItem;\n services.osmose.loadIssueDetail(_qaItem)\n .then(d => {\n // No details to add if there are no associated issue elements\n if (!d.elems || d.elems.length === 0) return;\n\n // Do nothing if UI has moved on by the time this resolves\n if (\n context.selectedErrorID() !== thisItem.id\n && context.container().selectAll(`.qaItem.osmose.hover.itemId-${thisItem.id}`).empty()\n ) return;\n\n // Things like keys and values are dynamically added to a subtitle string\n if (d.detail) {\n detailsDiv\n .append('h4')\n .call(t.append('QA.osmose.detail_title'));\n\n detailsDiv\n .append('p')\n .html(d => d.detail)\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Create list of linked issue elements\n elemsDiv\n .append('h4')\n .call(t.append('QA.osmose.elems_title'));\n\n elemsDiv\n .append('ul').selectAll('li')\n .data(d.elems)\n .enter()\n .append('li')\n .append('a')\n .attr('href', '#')\n .attr('class', 'error_entity_link')\n .text(d => d)\n .each(function() {\n const link = d3_select(this);\n const entityID = this.textContent;\n const entity = context.hasEntity(entityID);\n\n // Add click handler\n link\n .on('mouseenter', () => {\n utilHighlightEntities([entityID], true, context);\n })\n .on('mouseleave', () => {\n utilHighlightEntities([entityID], false, context);\n })\n .on('click', (d3_event) => {\n d3_event.preventDefault();\n\n utilHighlightEntities([entityID], false, context);\n\n const osmlayer = context.layers().layer('osm');\n if (!osmlayer.enabled()) {\n osmlayer.enabled(true);\n }\n\n context.map().centerZoom(d.loc, 20);\n\n if (entity) {\n context.enter(modeSelect(context, [entityID]));\n } else {\n context.loadEntity(entityID, (err, result) => {\n if (err) return;\n const entity = result.data.find(e => e.id === entityID);\n if (entity) context.enter(modeSelect(context, [entityID]));\n });\n }\n });\n\n // Replace with friendly name if possible\n // (The entity may not yet be loaded into the graph)\n if (entity) {\n let name = utilDisplayName(entity); // try to use common name\n\n if (!name) {\n const preset = presetManager.match(entity, context.graph());\n name = preset && !preset.isFallback() && preset.name(); // fallback to preset name\n }\n\n if (name) {\n this.innerText = name;\n }\n }\n });\n\n // Don't hide entities related to this issue - #5880\n context.features().forceVisible(d.elems);\n context.map().pan([0,0]); // trigger a redraw\n })\n .catch(err => {\n console.log(err); // eslint-disable-line no-console\n });\n }\n\n\n osmoseDetails.issue = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return osmoseDetails;\n };\n\n\n return osmoseDetails;\n}\n", "import { services } from '../services';\nimport { t } from '../core/localizer';\n\n\nexport function uiOsmoseHeader() {\n let _qaItem;\n\n function issueTitle(d) {\n const unknown = t('inspector.unknown');\n\n if (!d) return unknown;\n\n // Issue titles supplied by Osmose\n const s = services.osmose.getStrings(d.itemType);\n return ('title' in s) ? s.title : unknown;\n }\n\n function osmoseHeader(selection) {\n const header = selection.selectAll('.qa-header')\n .data(\n (_qaItem ? [_qaItem] : []),\n d => `${d.id}-${d.status || 0}`\n );\n\n header.exit()\n .remove();\n\n const headerEnter = header.enter()\n .append('div')\n .attr('class', 'qa-header');\n\n const svgEnter = headerEnter\n .append('div')\n .attr('class', 'qa-header-icon')\n .classed('new', d => d.id < 0)\n .append('svg')\n .attr('width', '20px')\n .attr('height', '30px')\n .attr('viewbox', '0 0 20 30')\n .attr('class', d => `preset-icon-28 qaItem ${d.service} itemId-${d.id} itemType-${d.itemType}`);\n\n svgEnter\n .append('polygon')\n .attr('fill', d => services.osmose.getColor(d.item))\n .attr('class', 'qaItem-fill')\n .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');\n\n svgEnter\n .append('use')\n .attr('class', 'icon-annotation')\n .attr('width', '12px')\n .attr('height', '12px')\n .attr('transform', 'translate(4, 5.5)')\n .attr('xlink:href', d => d.icon ? '#' + d.icon : '');\n\n headerEnter\n .append('div')\n .attr('class', 'qa-header-label')\n .text(issueTitle);\n }\n\n osmoseHeader.issue = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return osmoseHeader;\n };\n\n return osmoseHeader;\n}\n", "import { t } from '../core/localizer';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\nimport { QAItem } from '../osm';\n\nexport function uiViewOnOsmose() {\n let _qaItem;\n\n function viewOnOsmose(selection) {\n let url;\n if (services.osmose && (_qaItem instanceof QAItem)) {\n url = services.osmose.itemURL(_qaItem);\n }\n\n const link = selection.selectAll('.view-on-osmose')\n .data(url ? [url] : []);\n\n // exit\n link.exit()\n .remove();\n\n // enter\n const linkEnter = link.enter()\n .append('a')\n .attr('class', 'view-on-osmose')\n .attr('target', '_blank')\n .attr('rel', 'noopener') // security measure\n .attr('href', d => d)\n .call(svgIcon('#iD-icon-out-link', 'inline'));\n\n linkEnter\n .append('span')\n .call(t.append('inspector.view_on_osmose'));\n }\n\n viewOnOsmose.what = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return viewOnOsmose;\n };\n\n return viewOnOsmose;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\n\nimport { uiOsmoseDetails } from './osmose_details';\nimport { uiOsmoseHeader } from './osmose_header';\nimport { uiViewOnOsmose } from './view_on_osmose';\n\nimport { utilRebind } from '../util';\n\nexport function uiOsmoseEditor(context) {\n const dispatch = d3_dispatch('change');\n const qaDetails = uiOsmoseDetails(context);\n const qaHeader = uiOsmoseHeader(context);\n\n let _qaItem;\n\n function osmoseEditor(selection) {\n\n const header = selection.selectAll('.header')\n .data([0]);\n\n const headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', () => context.enter(modeBrowse(context)))\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('QA.osmose.title'));\n\n let body = selection.selectAll('.body')\n .data([0]);\n\n body = body.enter()\n .append('div')\n .attr('class', 'body')\n .merge(body);\n\n let editor = body.selectAll('.qa-editor')\n .data([0]);\n\n editor.enter()\n .append('div')\n .attr('class', 'modal-section qa-editor')\n .merge(editor)\n .call(qaHeader.issue(_qaItem))\n .call(qaDetails.issue(_qaItem))\n .call(osmoseSaveSection);\n\n const footer = selection.selectAll('.footer')\n .data([0]);\n\n footer.enter()\n .append('div')\n .attr('class', 'footer')\n .merge(footer)\n .call(uiViewOnOsmose(context).what(_qaItem));\n }\n\n function osmoseSaveSection(selection) {\n const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());\n const isShown = (_qaItem && isSelected);\n let saveSection = selection.selectAll('.qa-save')\n .data(\n (isShown ? [_qaItem] : []),\n d => `${d.id}-${d.status || 0}`\n );\n\n // exit\n saveSection.exit()\n .remove();\n\n // enter\n const saveSectionEnter = saveSection.enter()\n .append('div')\n .attr('class', 'qa-save save-section cf');\n\n // update\n saveSectionEnter\n .merge(saveSection)\n .call(qaSaveButtons);\n }\n\n function qaSaveButtons(selection) {\n const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());\n let buttonSection = selection.selectAll('.buttons')\n .data((isSelected ? [_qaItem] : []), d => d.status + d.id);\n\n // exit\n buttonSection.exit()\n .remove();\n\n // enter\n const buttonEnter = buttonSection.enter()\n .append('div')\n .attr('class', 'buttons');\n\n buttonEnter\n .append('button')\n .attr('class', 'button close-button action');\n\n buttonEnter\n .append('button')\n .attr('class', 'button ignore-button action');\n\n // update\n buttonSection = buttonSection\n .merge(buttonEnter);\n\n buttonSection.select('.close-button')\n .call(t.append('QA.keepRight.close'))\n .on('click.close', function(d3_event, d) {\n this.blur(); // avoid keeping focus on the button - #4641\n const qaService = services.osmose;\n if (qaService) {\n d.newStatus = 'done';\n qaService.postUpdate(d, (err, item) => dispatch.call('change', item));\n }\n });\n\n buttonSection.select('.ignore-button')\n .call(t.append('QA.keepRight.ignore'))\n .on('click.ignore', function(d3_event, d) {\n this.blur(); // avoid keeping focus on the button - #4641\n const qaService = services.osmose;\n if (qaService) {\n d.newStatus = 'false';\n qaService.postUpdate(d, (err, item) => dispatch.call('change', item));\n }\n });\n }\n\n // NOTE: Don't change method name until UI v3 is merged\n osmoseEditor.error = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return osmoseEditor;\n };\n\n return utilRebind(osmoseEditor, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { behaviorBreathe } from '../behavior/breathe';\nimport { behaviorHover } from '../behavior/hover';\nimport { behaviorLasso } from '../behavior/lasso';\nimport { behaviorSelect } from '../behavior/select';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { modeBrowse } from './browse';\nimport { modeDragNode } from './drag_node';\nimport { modeDragNote } from './drag_note';\nimport { uiOsmoseEditor } from '../ui/osmose_editor';\nimport { utilKeybinding } from '../util';\n\n// NOTE: Don't change name of this until UI v3 is merged\nexport function modeSelectError(context, selectedErrorID, selectedErrorService) {\n var mode = {\n id: 'select-error',\n button: 'browse'\n };\n\n var keybinding = utilKeybinding('select-error');\n\n var errorService = services[selectedErrorService];\n var errorEditor;\n switch (selectedErrorService) {\n case 'osmose':\n errorEditor = uiOsmoseEditor(context)\n .on('change', function() {\n context.map().pan([0,0]); // trigger a redraw\n var error = checkSelectedID();\n if (!error) return;\n context.ui().sidebar\n .show(errorEditor.error(error));\n });\n break;\n }\n\n\n var behaviors = [\n behaviorBreathe(context),\n behaviorHover(context),\n behaviorSelect(context),\n behaviorLasso(context),\n modeDragNode(context).behavior,\n modeDragNote(context).behavior\n ];\n\n\n function checkSelectedID() {\n if (!errorService) return;\n var error = errorService.getError(selectedErrorID);\n if (!error) {\n context.enter(modeBrowse(context));\n }\n return error;\n }\n\n\n mode.zoomToSelected = function() {\n if (!errorService) return;\n var error = errorService.getError(selectedErrorID);\n if (error) {\n context.map().centerZoomEase(error.loc, 20);\n }\n };\n\n\n mode.enter = function() {\n var error = checkSelectedID();\n if (!error) return;\n\n behaviors.forEach(context.install);\n keybinding\n .on(t('inspector.zoom_to.key'), mode.zoomToSelected)\n .on('\u238B', esc, true);\n\n d3_select(document)\n .call(keybinding);\n\n selectError();\n\n var sidebar = context.ui().sidebar;\n sidebar.show(errorEditor.error(error));\n\n context.map()\n .on('drawn.select-error', selectError);\n\n\n // class the error as selected, or return to browse mode if the error is gone\n function selectError(d3_event, drawn) {\n if (!checkSelectedID()) return;\n\n var selection = context.surface()\n .selectAll('.itemId-' + selectedErrorID + '.' + selectedErrorService);\n\n if (selection.empty()) {\n // Return to browse mode if selected DOM elements have\n // disappeared because the user moved them out of view..\n var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;\n if (drawn && source && (source.type === 'pointermove' || source.type === 'mousemove' || source.type === 'touchmove')) {\n context.enter(modeBrowse(context));\n }\n\n } else {\n selection\n .classed('selected', true);\n\n context.selectedErrorID(selectedErrorID);\n }\n }\n\n function esc() {\n if (context.container().select('.combobox').size()) return;\n context.enter(modeBrowse(context));\n }\n };\n\n\n mode.exit = function() {\n behaviors.forEach(context.uninstall);\n\n d3_select(document)\n .call(keybinding.unbind);\n\n context.surface()\n .selectAll('.qaItem.selected')\n .classed('selected hover', false);\n\n context.map()\n .on('drawn.select-error', null);\n\n context.ui().sidebar\n .hide();\n\n context.selectedErrorID(null);\n context.features().forceVisible([]);\n };\n\n\n return mode;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { geoVecLength } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect } from '../modes/select';\nimport { modeSelectData } from '../modes/select_data';\nimport { modeSelectNote } from '../modes/select_note';\nimport { modeSelectError } from '../modes/select_error';\nimport { osmEntity, osmNote, QAItem } from '../osm';\nimport { utilFastMouse } from '../util/util';\n\n\nexport function behaviorSelect(context) {\n var _tolerancePx = 4; // see also behaviorDrag\n var _lastMouseEvent = null;\n var _showMenu = false;\n var _downPointers = {};\n var _longPressTimeout = null;\n var _lastInteractionType = null;\n // the id of the down pointer that's enabling multiselection while down\n var _multiselectionPointerId = null;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function keydown(d3_event) {\n\n if (d3_event.keyCode === 32) {\n // don't react to spacebar events during text input\n var activeNode = document.activeElement;\n if (activeNode && new Set(['INPUT', 'TEXTAREA']).has(activeNode.nodeName)) return;\n }\n\n if (d3_event.keyCode === 93 || // context menu key\n d3_event.keyCode === 32) { // spacebar\n d3_event.preventDefault();\n }\n\n if (d3_event.repeat) return; // ignore repeated events for held keys\n\n // if any key is pressed the user is probably doing something other than long-pressing\n cancelLongPress();\n\n if (d3_event.shiftKey) {\n context.surface()\n .classed('behavior-multiselect', true);\n }\n\n if (d3_event.keyCode === 32) { // spacebar\n if (!_downPointers.spacebar && _lastMouseEvent) {\n cancelLongPress();\n _longPressTimeout = window.setTimeout(didLongPress, 500, 'spacebar', 'spacebar');\n\n _downPointers.spacebar = {\n firstEvent: _lastMouseEvent,\n lastEvent: _lastMouseEvent\n };\n }\n }\n }\n\n\n function keyup(d3_event) {\n cancelLongPress();\n\n if (!d3_event.shiftKey) {\n context.surface()\n .classed('behavior-multiselect', false);\n }\n\n if (d3_event.keyCode === 93) { // context menu key\n d3_event.preventDefault();\n _lastInteractionType = 'menukey';\n contextmenu(d3_event);\n } else if (d3_event.keyCode === 32) { // spacebar\n var pointer = _downPointers.spacebar;\n if (pointer) {\n delete _downPointers.spacebar;\n\n if (pointer.done) return;\n\n d3_event.preventDefault();\n _lastInteractionType = 'spacebar';\n click(pointer.firstEvent, pointer.lastEvent, 'spacebar');\n }\n }\n }\n\n\n function pointerdown(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n\n cancelLongPress();\n\n if (d3_event.buttons && d3_event.buttons !== 1) return;\n\n context.ui().closeEditMenu();\n\n if (d3_event.pointerType !== 'mouse') {\n _longPressTimeout = window.setTimeout(didLongPress, 500, id, 'longdown-' + (d3_event.pointerType || 'mouse'));\n }\n\n _downPointers[id] = {\n firstEvent: d3_event,\n lastEvent: d3_event\n };\n }\n\n\n function didLongPress(id, interactionType) {\n var pointer = _downPointers[id];\n if (!pointer) return;\n\n for (var i in _downPointers) {\n // don't allow this or any currently down pointer to trigger another click\n _downPointers[i].done = true;\n }\n\n // treat long presses like right-clicks\n _longPressTimeout = null;\n _lastInteractionType = interactionType;\n _showMenu = true;\n\n click(pointer.firstEvent, pointer.lastEvent, id);\n }\n\n\n function pointermove(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n if (_downPointers[id]) {\n _downPointers[id].lastEvent = d3_event;\n }\n if (!d3_event.pointerType || d3_event.pointerType === 'mouse') {\n _lastMouseEvent = d3_event;\n if (_downPointers.spacebar) {\n _downPointers.spacebar.lastEvent = d3_event;\n }\n }\n }\n\n\n function pointerup(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n var pointer = _downPointers[id];\n if (!pointer) return;\n\n delete _downPointers[id];\n\n if (_multiselectionPointerId === id) {\n _multiselectionPointerId = null;\n }\n\n if (pointer.done) return;\n\n click(pointer.firstEvent, d3_event, id);\n }\n\n\n function pointercancel(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n if (!_downPointers[id]) return;\n\n delete _downPointers[id];\n\n if (_multiselectionPointerId === id) {\n _multiselectionPointerId = null;\n }\n }\n\n\n function contextmenu(d3_event) {\n d3_event.preventDefault();\n\n if (!+d3_event.clientX && !+d3_event.clientY) {\n if (_lastMouseEvent) {\n d3_event = _lastMouseEvent;\n } else {\n return;\n }\n } else {\n _lastMouseEvent = d3_event;\n if (d3_event.pointerType === 'touch' || d3_event.pointerType === 'pen' ||\n d3_event.mozInputSource && ( // firefox doesn't give a pointerType on contextmenu events\n d3_event.mozInputSource === MouseEvent.MOZ_SOURCE_TOUCH ||\n d3_event.mozInputSource === MouseEvent.MOZ_SOURCE_PEN)) {\n _lastInteractionType = 'touch';\n } else {\n _lastInteractionType = 'rightclick';\n }\n }\n\n _showMenu = true;\n click(d3_event, d3_event);\n }\n\n\n function click(firstEvent, lastEvent, pointerId) {\n cancelLongPress();\n\n var mapNode = context.container().select('.main-map').node();\n\n // Use the `main-map` coordinate system since the surface and supersurface\n // are transformed when drag-panning.\n var pointGetter = utilFastMouse(mapNode);\n var p1 = pointGetter(firstEvent);\n var p2 = pointGetter(lastEvent);\n var dist = geoVecLength(p1, p2);\n\n if (dist > _tolerancePx ||\n !mapContains(lastEvent)) {\n\n resetProperties();\n return;\n }\n\n var targetDatum = lastEvent.target.__data__;\n if (targetDatum === 0 && lastEvent.target.parentNode.__data__) {\n // some targets (like markers of the street level photo\n // layers) have the data bound to the parent node\n targetDatum = lastEvent.target.parentNode.__data__;\n }\n\n var multiselectEntityId;\n\n if (!_multiselectionPointerId) {\n // If a different pointer than the one triggering this click is down on a\n // feature, treat this and all future clicks as multiselection until that\n // pointer is raised.\n var selectPointerInfo = pointerDownOnSelection(pointerId);\n if (selectPointerInfo) {\n _multiselectionPointerId = selectPointerInfo.pointerId;\n // if the other feature isn't selected yet, make sure we select it\n multiselectEntityId = !selectPointerInfo.selected && selectPointerInfo.entityId;\n _downPointers[selectPointerInfo.pointerId].done = true;\n }\n }\n\n // support multiselect if data is already selected\n var isMultiselect = context.mode().id === 'select' && (\n // and shift key is down\n (lastEvent && lastEvent.shiftKey) ||\n // or we're lasso-selecting\n context.surface().select('.lasso').node() ||\n // or a pointer is down over a selected feature\n (_multiselectionPointerId && !multiselectEntityId)\n );\n\n processClick(targetDatum, isMultiselect, p2, multiselectEntityId);\n\n function mapContains(event) {\n var rect = mapNode.getBoundingClientRect();\n return event.clientX >= rect.left &&\n event.clientX <= rect.right &&\n event.clientY >= rect.top &&\n event.clientY <= rect.bottom;\n }\n\n function pointerDownOnSelection(skipPointerId) {\n var mode = context.mode();\n var selectedIDs = mode.id === 'select' ? mode.selectedIDs() : [];\n for (var pointerId in _downPointers) {\n if (pointerId === 'spacebar' || pointerId === skipPointerId) continue;\n\n var pointerInfo = _downPointers[pointerId];\n\n var p1 = pointGetter(pointerInfo.firstEvent);\n var p2 = pointGetter(pointerInfo.lastEvent);\n if (geoVecLength(p1, p2) > _tolerancePx) continue;\n\n var datum = pointerInfo.firstEvent.target.__data__;\n var entity = (datum && datum.properties && datum.properties.entity) || datum;\n if (context.graph().hasEntity(entity.id)) {\n return {\n pointerId: pointerId,\n entityId: entity.id,\n selected: selectedIDs.indexOf(entity.id) !== -1\n };\n }\n }\n return null;\n }\n }\n\n\n function processClick(datum, isMultiselect, point, alsoSelectId) {\n var mode = context.mode();\n var showMenu = _showMenu;\n var interactionType = _lastInteractionType;\n\n var entity = datum && datum.properties && datum.properties.entity;\n if (entity) datum = entity;\n\n if (datum && datum.type === 'midpoint') {\n // treat targeting midpoints as if targeting the parent way\n datum = datum.parents[0];\n }\n\n var newMode;\n\n if (datum instanceof osmEntity) {\n // targeting an entity\n var selectedIDs = context.selectedIDs();\n context.selectedNoteID(null);\n context.selectedErrorID(null);\n\n if (!isMultiselect) {\n // don't change the selection if we're toggling the menu atop a multiselection\n if (!showMenu ||\n selectedIDs.length <= 1 ||\n selectedIDs.indexOf(datum.id) === -1) {\n\n if (alsoSelectId === datum.id) alsoSelectId = null;\n\n selectedIDs = (alsoSelectId ? [alsoSelectId] : []).concat([datum.id]);\n // always enter modeSelect even if the entity is already\n // selected since listeners may expect `context.enter` events,\n // e.g. in the walkthrough\n newMode = mode.id === 'select' ? mode.selectedIDs(selectedIDs) : modeSelect(context, selectedIDs).selectBehavior(behavior);\n context.enter(newMode);\n }\n\n } else {\n if (selectedIDs.indexOf(datum.id) !== -1) {\n // clicked entity is already in the selectedIDs list..\n if (!showMenu) {\n // deselect clicked entity, then reenter select mode or return to browse mode..\n selectedIDs = selectedIDs.filter(function(id) { return id !== datum.id; });\n newMode = selectedIDs.length ? mode.selectedIDs(selectedIDs) : modeBrowse(context).selectBehavior(behavior);\n context.enter(newMode);\n }\n } else {\n // clicked entity is not in the selected list, add it..\n selectedIDs = selectedIDs.concat([datum.id]);\n newMode = mode.selectedIDs(selectedIDs);\n context.enter(newMode);\n }\n }\n\n } else if (datum && datum.__featurehash__ && !isMultiselect) {\n // targeting custom data\n context\n .selectedNoteID(null)\n .enter(modeSelectData(context, datum));\n\n } else if (datum instanceof osmNote && !isMultiselect) {\n // targeting a note\n context\n .selectedNoteID(datum.id)\n .enter(modeSelectNote(context, datum.id));\n\n } else if (datum instanceof QAItem && !isMultiselect) {\n // targeting an external QA issue\n context\n .selectedErrorID(datum.id)\n .enter(modeSelectError(context, datum.id, datum.service));\n\n } else if (datum.service === 'photo') {\n // street level photo was selected:\n // don't change mode and selection\n } else {\n // targeting nothing\n context.selectedNoteID(null);\n context.selectedErrorID(null);\n if (!isMultiselect && mode.id !== 'browse') {\n context.enter(modeBrowse(context));\n }\n }\n\n context.ui().closeEditMenu();\n\n // always request to show the edit menu in case the mode needs it\n if (showMenu) context.ui().showEditMenu(point, interactionType);\n\n resetProperties();\n }\n\n\n function cancelLongPress() {\n if (_longPressTimeout) window.clearTimeout(_longPressTimeout);\n _longPressTimeout = null;\n }\n\n\n function resetProperties() {\n cancelLongPress();\n _showMenu = false;\n _lastInteractionType = null;\n // don't reset _lastMouseEvent since it might still be useful\n }\n\n\n function behavior(selection) {\n resetProperties();\n _lastMouseEvent = context.map().lastPointerEvent();\n\n d3_select(window)\n .on('keydown.select', keydown)\n .on('keyup.select', keyup)\n .on(_pointerPrefix + 'move.select', pointermove, true)\n .on(_pointerPrefix + 'up.select', pointerup, true)\n .on('pointercancel.select', pointercancel, true)\n .on('contextmenu.select-window', function(d3_event) {\n // Edge and IE really like to show the contextmenu on the\n // menubar when user presses a keyboard menu button\n // even after we've already preventdefaulted the key event.\n var e = d3_event;\n if (+e.clientX === 0 && +e.clientY === 0) {\n d3_event.preventDefault();\n }\n });\n\n selection\n .on(_pointerPrefix + 'down.select', pointerdown)\n .on('contextmenu.select', contextmenu);\n\n /*if (d3_event && d3_event.shiftKey) {\n context.surface()\n .classed('behavior-multiselect', true);\n }*/\n }\n\n\n behavior.off = function(selection) {\n cancelLongPress();\n\n d3_select(window)\n .on('keydown.select', null)\n .on('keyup.select', null)\n .on('contextmenu.select-window', null)\n .on(_pointerPrefix + 'move.select', null, true)\n .on(_pointerPrefix + 'up.select', null, true)\n .on('pointercancel.select', null, true);\n\n selection\n .on(_pointerPrefix + 'down.select', null)\n .on('contextmenu.select', null);\n\n context.surface()\n .classed('behavior-multiselect', false);\n };\n\n\n return behavior;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../core/preferences';\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { services } from '../services';\nimport { localeDateString } from '../util/date';\n\n\nexport function uiNoteComments() {\n var _note;\n\n\n function noteComments(selection) {\n if (_note.isNew()) return; // don't draw .comments-container\n\n var comments = selection.selectAll('.comments-container')\n .data([0]);\n\n comments = comments.enter()\n .append('div')\n .attr('class', 'comments-container')\n .merge(comments);\n\n var commentEnter = comments.selectAll('.comment')\n .data(_note.comments)\n .enter()\n .append('div')\n .attr('class', 'comment');\n\n commentEnter\n .append('div')\n .attr('class', function(d) { return 'comment-avatar user-' + d.uid; })\n .call(svgIcon('#iD-icon-avatar', 'comment-avatar-icon'));\n\n var mainEnter = commentEnter\n .append('div')\n .attr('class', 'comment-main');\n\n var metadataEnter = mainEnter\n .append('div')\n .attr('class', 'comment-metadata');\n\n metadataEnter\n .append('div')\n .attr('class', 'comment-author')\n .each(function(d) {\n var selection = d3_select(this);\n var osm = services.osm;\n if (osm && d.user) {\n selection = selection\n .append('a')\n .attr('class', 'comment-author-link')\n .attr('href', osm.userURL(d.user))\n .attr('target', '_blank');\n }\n if (d.user) {\n selection.text(d.user);\n } else {\n selection.call(t.append('note.anonymous'));\n }\n });\n\n metadataEnter\n .append('div')\n .attr('class', 'comment-date')\n .each(function(d) {\n d3_select(this).call(\n t.addOrUpdate('note.status.' + d.action, {\n when: localeDateString(d.date.replace(' UTC', 'Z').replace(' ', 'T')),\n }));\n });\n\n mainEnter\n .append('div')\n .attr('class', 'comment-text')\n .html(function(d) { return d.html; })\n .selectAll('a')\n .attr('rel', 'noopener nofollow')\n .attr('target', '_blank');\n\n comments\n .call(replaceAvatars);\n }\n\n\n function replaceAvatars(selection) {\n var showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n var osm = services.osm;\n if (showThirdPartyIcons !== 'true' || !osm) return;\n\n var uids = {}; // gather uids in the comment thread\n _note.comments.forEach(function(d) {\n if (d.uid) uids[d.uid] = true;\n });\n\n Object.keys(uids).forEach(function(uid) {\n osm.loadUser(uid, function(err, user) {\n if (!user || !user.image_url) return;\n\n selection.selectAll('.comment-avatar.user-' + uid)\n .html('')\n .append('img')\n .attr('class', 'icon comment-avatar-icon')\n .attr('src', user.image_url)\n .attr('alt', user.display_name);\n });\n });\n }\n\n\n noteComments.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteComments;\n };\n\n\n return noteComments;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiNoteHeader() {\n var _note;\n\n\n function noteHeader(selection) {\n var header = selection.selectAll('.note-header')\n .data(\n (_note ? [_note] : []),\n function(d) { return d.status + d.id; }\n );\n\n header.exit()\n .remove();\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'note-header');\n\n var iconEnter = headerEnter\n .append('div')\n .attr('class', function(d) { return 'note-header-icon ' + d.status; })\n .classed('new', function(d) { return d.id < 0; });\n\n iconEnter\n .append('div')\n .attr('class', 'preset-icon-28')\n .call(svgIcon('#iD-icon-note', 'note-fill'));\n\n iconEnter.each(function(d) {\n var statusIcon;\n if (d.id < 0) {\n statusIcon = '#iD-icon-plus';\n } else if (d.status === 'open') {\n statusIcon = '#iD-icon-close';\n } else {\n statusIcon = '#iD-icon-apply';\n }\n iconEnter\n .append('div')\n .attr('class', 'note-icon-annotation')\n .attr('title', t('icons.close'))\n .call(svgIcon(statusIcon, 'icon-annotation'));\n });\n\n headerEnter\n .append('div')\n .attr('class', 'note-header-label')\n .each(function(d) {\n const selection = d3_select(this);\n selection.text('');\n if (_note.isNew()) {\n selection.call(t.append('note.new'));\n } else {\n selection.call(t.append('note.note'));\n selection\n .append('span')\n .text(` ${d.id} `);\n if (d.status === 'closed') {\n selection.call(t.append('note.closed'));\n }\n }\n });\n }\n\n\n noteHeader.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteHeader;\n };\n\n\n return noteHeader;\n}\n", "import { t } from '../core/localizer';\nimport { osmNote } from '../osm';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiNoteReport() {\n var _note;\n\n function noteReport(selection) {\n var url;\n if (services.osm && (_note instanceof osmNote) && (!_note.isNew())) {\n url = services.osm.noteReportURL(_note);\n }\n\n var link = selection.selectAll('.note-report')\n .data(url ? [url] : []);\n\n // exit\n link.exit()\n .remove();\n\n // enter\n var linkEnter = link.enter()\n .append('a')\n .attr('class', 'note-report')\n .attr('target', '_blank')\n .attr('href', function(d) { return d; })\n .call(svgIcon('#iD-icon-out-link', 'inline'));\n\n linkEnter\n .append('span')\n .call(t.append('note.report'));\n }\n\n\n noteReport.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteReport;\n };\n\n return noteReport;\n}\n", "import { t } from '../core/localizer';\nimport { osmEntity, osmNote, osmRelation, osmWay } from '../osm';\nimport { svgIcon } from '../svg/icon';\nimport { getRelativeDate } from '../util/date';\n\n\nexport function uiViewOnOSM(context) {\n var _what; // an osmEntity or osmNote\n\n\n function viewOnOSM(selection) {\n var url;\n if (_what instanceof osmEntity) {\n url = context.connection().historyURL(_what);\n } else if (_what instanceof osmNote) {\n url = context.connection().noteURL(_what);\n }\n\n var data = ((!_what || _what.isNew()) ? [] : [_what]);\n var link = selection.selectAll('.view-on-osm')\n .data(data, function(d) { return d.id; });\n\n // exit\n link.exit()\n .remove();\n\n // enter\n var linkEnter = link.enter()\n .append('a')\n .attr('class', 'view-on-osm')\n .attr('target', '_blank')\n .attr('href', url)\n .call(svgIcon('#iD-icon-out-link', 'inline'));\n\n\n if (_what && !(_what instanceof osmNote)) {\n // node/way/relation\n const { user, timestamp } = uiViewOnOSM.findLastModifiedChild(context.history().base(), _what);\n\n linkEnter\n .call(t.append('inspector.last_touched', {\n timeago: getRelativeDate(new Date(timestamp)),\n user\n }))\n .attr('title', t('inspector.view_on_osm'));\n } else {\n linkEnter\n .append('span')\n .call(t.append('inspector.view_on_osm'));\n }\n }\n\n\n viewOnOSM.what = function(_) {\n if (!arguments.length) return _what;\n _what = _;\n return viewOnOSM;\n };\n\n return viewOnOSM;\n}\n\n\n/**\n * @param {iD.Graph} graph\n * @param {iD.OsmEntity} feature\n */\nuiViewOnOSM.findLastModifiedChild = (graph, feature) => {\n let latest = feature;\n\n /** @param {iD.OsmEntity} obj */\n function recurseChilds(obj) {\n if (obj.timestamp > latest.timestamp) {\n latest = obj;\n }\n if (obj instanceof osmWay) {\n obj.nodes\n .map(id => graph.hasEntity(id))\n .filter(Boolean)\n .forEach(recurseChilds);\n } else if (obj instanceof osmRelation) {\n obj.members\n .map(m => graph.hasEntity(m.id))\n .filter(e => e instanceof osmWay || e instanceof osmRelation)\n .forEach(recurseChilds);\n }\n }\n\n recurseChilds(feature);\n return latest;\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\n\n// import { uiField } from './field';\n// import { uiFormFields } from './form_fields';\n\nimport { uiNoteComments } from './note_comments';\nimport { uiNoteHeader } from './note_header';\nimport { uiNoteReport } from './note_report';\nimport { uiViewOnOSM } from './view_on_osm';\n\nimport {\n utilNoAuto,\n utilRebind\n} from '../util';\nimport { osmNote } from '../osm';\n\n\nexport function uiNoteEditor(context) {\n var dispatch = d3_dispatch('change');\n var noteComments = uiNoteComments(context);\n var noteHeader = uiNoteHeader();\n\n // var formFields = uiFormFields(context);\n\n var _note;\n var _newNote;\n // var _fieldsArr;\n\n\n function noteEditor(selection) {\n\n var header = selection.selectAll('.header')\n .data([0]);\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function() {\n context.enter(modeBrowse(context));\n })\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('note.title'));\n\n\n var body = selection.selectAll('.body')\n .data([0]);\n\n body = body.enter()\n .append('div')\n .attr('class', 'body')\n .merge(body);\n\n var editor = body.selectAll('.note-editor')\n .data([0]);\n\n editor.enter()\n .append('div')\n .attr('class', 'modal-section note-editor')\n .merge(editor)\n .call(noteHeader.note(_note))\n .call(noteComments.note(_note))\n .call(noteSaveSection);\n\n var footer = selection.selectAll('.footer')\n .data([0]);\n\n footer.enter()\n .append('div')\n .attr('class', 'footer')\n .merge(footer)\n .call(uiViewOnOSM(context).what(_note))\n .call(uiNoteReport(context).note(_note));\n\n\n // rerender the note editor on any auth change\n var osm = services.osm;\n if (osm) {\n osm.on('change.note-save', function() {\n selection.call(noteEditor);\n });\n }\n }\n\n\n function noteSaveSection(selection) {\n var isSelected = (_note && _note.id === context.selectedNoteID());\n var noteSave = selection.selectAll('.note-save')\n .data((isSelected ? [_note].filter(d => d.status !== 'hidden') : []), d => d.status + d.id);\n\n // exit\n noteSave.exit()\n .remove();\n\n // enter\n var noteSaveEnter = noteSave.enter()\n .append('div')\n .attr('class', 'note-save save-section cf');\n\n // // if new note, show categories to pick from\n // if (_note.isNew()) {\n // var presets = presetManager;\n\n // // NOTE: this key isn't a age and therefore there is no documentation (yet)\n // _fieldsArr = [\n // uiField(context, presets.field('category'), null, { show: true, revert: false }),\n // ];\n\n // _fieldsArr.forEach(function(field) {\n // field\n // .on('change', changeCategory);\n // });\n\n // noteSaveEnter\n // .append('div')\n // .attr('class', 'note-category')\n // .call(formFields.fieldsArr(_fieldsArr));\n // }\n\n // function changeCategory() {\n // // NOTE: perhaps there is a better way to get value\n // var val = context.container().select('input[name=\\'category\\']:checked').property('__data__') || undefined;\n\n // // store the unsaved category with the note itself\n // _note = _note.update({ newCategory: val });\n // var osm = services.osm;\n // if (osm) {\n // osm.replaceNote(_note); // update note cache\n // }\n // noteSave\n // .call(noteSaveButtons);\n // }\n\n noteSaveEnter\n .append('h4')\n .attr('class', '.note-save-header')\n .text('')\n .each(function() {\n if (_note.isNew()) {\n t.append('note.newDescription')(d3_select(this));\n } else {\n t.append('note.newComment')(d3_select(this));\n }\n });\n\n var commentTextarea = noteSaveEnter\n .append('textarea')\n .attr('class', 'new-comment-input')\n .attr('placeholder', t('note.inputPlaceholder'))\n .attr('maxlength', 1000)\n .property('value', function(d) { return d.newComment; })\n .call(utilNoAuto)\n .on('keydown.note-input', keydown)\n .on('input.note-input', changeInput)\n .on('blur.note-input', changeInput);\n\n if (!commentTextarea.empty() && _newNote) {\n // autofocus the comment field for new notes\n commentTextarea.node().focus();\n }\n\n // update\n noteSave = noteSaveEnter\n .merge(noteSave)\n .call(userDetails)\n .call(noteSaveButtons);\n\n\n // fast submit if user presses cmd+enter\n function keydown(d3_event) {\n if (!(d3_event.keyCode === 13 && // \u21A9 Return\n d3_event.metaKey)) return;\n\n var osm = services.osm;\n if (!osm) return;\n\n var hasAuth = osm.authenticated();\n if (!hasAuth) return;\n\n if (!_note.newComment) return;\n\n d3_event.preventDefault();\n\n d3_select(this)\n .on('keydown.note-input', null);\n\n // focus on button and submit\n window.setTimeout(function() {\n if (_note.isNew()) {\n noteSave.selectAll('.save-button').node().focus();\n clickSave(_note);\n } else {\n noteSave.selectAll('.comment-button').node().focus();\n postCommentAndStatus(_note);\n }\n }, 10);\n }\n\n\n function changeInput() {\n var input = d3_select(this);\n var val = input.property('value').trim() || undefined;\n\n // store the unsaved comment with the note itself\n _note = _note.update({ newComment: val });\n\n var osm = services.osm;\n if (osm) {\n osm.replaceNote(_note); // update note cache\n }\n\n noteSave\n .call(noteSaveButtons);\n }\n }\n\n\n function userDetails(selection) {\n var detailSection = selection.selectAll('.detail-section')\n .data([0]);\n\n detailSection = detailSection.enter()\n .append('div')\n .attr('class', 'detail-section')\n .merge(detailSection);\n\n var osm = services.osm;\n if (!osm) return;\n\n // Add warning if user is not logged in\n var hasAuth = osm.authenticated();\n var authWarning = detailSection.selectAll('.auth-warning')\n .data(hasAuth ? [] : [0]);\n\n authWarning.exit()\n .transition()\n .duration(200)\n .style('opacity', 0)\n .remove();\n\n var authEnter = authWarning.enter()\n .insert('div', '.tag-reference-body')\n .attr('class', 'field-warning auth-warning')\n .style('opacity', 0);\n\n authEnter\n .call(svgIcon('#iD-icon-alert', 'inline'));\n\n authEnter\n .append('span')\n .call(t.append('note.login'));\n\n authEnter\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append('login'))\n .on('click.note-login', function(d3_event) {\n d3_event.preventDefault();\n osm.authenticate();\n });\n\n authEnter\n .transition()\n .duration(200)\n .style('opacity', 1);\n\n\n var prose = detailSection.selectAll('.note-save-prose')\n .data(hasAuth ? [0] : []);\n\n prose.exit()\n .remove();\n\n prose = prose.enter()\n .append('p')\n .attr('class', 'note-save-prose')\n .call(t.append('note.upload_explanation'))\n .merge(prose);\n\n osm.userDetails(function(err, user) {\n if (err) return;\n\n const userLink = selection => {\n if (user.image_url) {\n selection\n .append('img')\n .attr('src', user.image_url)\n .attr('class', 'icon pre-text user-icon');\n }\n\n selection\n .append('a')\n .attr('class', 'user-info')\n .text(user.display_name)\n .attr('href', osm.userURL(user.display_name))\n .attr('target', '_blank');\n };\n\n prose\n .call(t.addOrUpdate('note.upload_explanation_with_user', { user: userLink }));\n });\n }\n\n\n function noteSaveButtons(selection) {\n var osm = services.osm;\n var hasAuth = osm && osm.authenticated();\n\n var isSelected = (_note && _note.id === context.selectedNoteID());\n var buttonSection = selection.selectAll('.buttons')\n .data((isSelected ? [_note] : []), d => d.status + d.id);\n\n // exit\n buttonSection.exit()\n .remove();\n\n // enter\n var buttonEnter = buttonSection.enter()\n .append('div')\n .attr('class', 'buttons');\n\n if (_note.isNew()) {\n buttonEnter\n .append('button')\n .attr('class', 'button cancel-button secondary-action')\n .call(t.append('confirm.cancel'));\n\n buttonEnter\n .append('button')\n .attr('class', 'button save-button action')\n .call(t.append('note.save'));\n\n } else {\n buttonEnter\n .append('button')\n .attr('class', 'button status-button action');\n\n buttonEnter\n .append('button')\n .attr('class', 'button comment-button action')\n .call(t.append('note.comment'));\n }\n\n\n // update\n buttonSection = buttonSection\n .merge(buttonEnter);\n\n buttonSection.select('.cancel-button') // select and propagate data\n .on('click.cancel', clickCancel);\n\n buttonSection.select('.save-button') // select and propagate data\n .attr('disabled', isSaveDisabled)\n .on('click.save', clickSave);\n\n buttonSection.select('.status-button') // select and propagate data\n .attr('disabled', (hasAuth ? null : true))\n .each(function(d) {\n var action = (d.status === 'open' ? 'close' : 'open');\n var andComment = (d.newComment ? '_comment' : '');\n t.addOrUpdate('note.' + action + andComment)(d3_select(this));\n })\n .on('click.status', function(d3_event, note) {\n const setStatus = (note.status === 'open' ? 'closed' : 'open');\n postCommentAndStatus.bind(this)(d3_event, note, setStatus);\n });\n\n buttonSection.select('.comment-button') // select and propagate data\n .attr('disabled', isSaveDisabled)\n .on('click.comment', postCommentAndStatus);\n\n\n function isSaveDisabled(d) {\n return (hasAuth && d.status === 'open' && d.newComment) ? null : true;\n }\n }\n\n\n\n function clickCancel(d3_event, d) {\n this.blur(); // avoid keeping focus on the button - #4641\n var osm = services.osm;\n if (osm) {\n osm.removeNote(d);\n }\n context.enter(modeBrowse(context));\n dispatch.call('change', d);\n }\n\n\n function clickSave(d3_event, note) {\n this.blur(); // avoid keeping focus on the button - #4641\n var osm = services.osm;\n if (osm) {\n osm.postNoteCreate(note, (err, d) => dispatch.call('change', d));\n }\n }\n\n\n function postCommentAndStatus(d3_event, note, newStatus) {\n this.blur(); // avoid keeping focus on the button - #4641\n var osm = services.osm;\n if (osm) {\n osm.postNoteUpdate(note, newStatus || note.status, function(err, d) {\n if (!err) {\n dispatch.call('change', d);\n } else if (err.status === 409) {\n // note was probably closed in the meantime: reload it - #8464\n osm.loadEntityNote(note.id, (err, d) => {\n dispatch.call('change', osmNote({ ...d.data[0], newComment: note.newComment }));\n });\n } else if (err.status === 410) {\n // note was deleted/hidden by a moderator\n osm.removeNote(note);\n dispatch.call('change', osmNote({ id: note.id, status: 'hidden', comments: [...note.comments, { action: 'hidden' }] }));\n }\n });\n }\n }\n\n\n noteEditor.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteEditor;\n };\n\n noteEditor.newNote = function(val) {\n if (!arguments.length) return _newNote;\n _newNote = val;\n return noteEditor;\n };\n\n\n return utilRebind(noteEditor, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { behaviorBreathe } from '../behavior/breathe';\nimport { behaviorHover } from '../behavior/hover';\nimport { behaviorLasso } from '../behavior/lasso';\nimport { behaviorSelect } from '../behavior/select';\n\nimport { t } from '../core/localizer';\n\nimport { modeBrowse } from './browse';\nimport { modeDragNode } from './drag_node';\nimport { modeDragNote } from './drag_note';\nimport { services } from '../services';\nimport { uiNoteEditor } from '../ui/note_editor';\nimport { utilKeybinding } from '../util';\n\n\nexport function modeSelectNote(context, selectedNoteID) {\n var mode = {\n id: 'select-note',\n button: 'browse'\n };\n\n var _keybinding = utilKeybinding('select-note');\n var _noteEditor = uiNoteEditor(context)\n .on('change', function() {\n context.map().pan([0,0]); // trigger a redraw\n var note = this || checkSelectedID();\n if (!note) return;\n context.ui().sidebar\n .show(_noteEditor.note(note));\n });\n\n var _behaviors = [\n behaviorBreathe(context),\n behaviorHover(context),\n behaviorSelect(context),\n behaviorLasso(context),\n modeDragNode(context).behavior,\n modeDragNote(context).behavior\n ];\n\n var _newFeature = false;\n\n\n function checkSelectedID() {\n if (!services.osm) return;\n var note = services.osm.getNote(selectedNoteID);\n if (!note) {\n context.enter(modeBrowse(context));\n }\n return note;\n }\n\n\n // class the note as selected, or return to browse mode if the note is gone\n function selectNote(d3_event, drawn) {\n if (!checkSelectedID()) return;\n\n var selection = context.surface().selectAll('.layer-notes .note-' + selectedNoteID);\n\n if (selection.empty()) {\n // Return to browse mode if selected DOM elements have\n // disappeared because the user moved them out of view..\n var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;\n if (drawn && source && (source.type === 'pointermove' || source.type === 'mousemove' || source.type === 'touchmove')) {\n context.enter(modeBrowse(context));\n }\n\n } else {\n selection\n .classed('selected', true);\n\n context.selectedNoteID(selectedNoteID);\n }\n }\n\n\n function esc() {\n if (context.container().select('.combobox').size()) return;\n context.enter(modeBrowse(context));\n }\n\n\n mode.zoomToSelected = function() {\n if (!services.osm) return;\n var note = services.osm.getNote(selectedNoteID);\n if (note) {\n context.map().centerZoomEase(note.loc, 20);\n }\n };\n\n\n mode.newFeature = function(val) {\n if (!arguments.length) return _newFeature;\n _newFeature = val;\n return mode;\n };\n\n\n mode.enter = function() {\n var note = checkSelectedID();\n if (!note) return;\n\n _behaviors.forEach(context.install);\n\n _keybinding\n .on(t('inspector.zoom_to.key'), mode.zoomToSelected)\n .on('\u238B', esc, true);\n\n d3_select(document)\n .call(_keybinding);\n\n selectNote();\n\n var sidebar = context.ui().sidebar;\n sidebar.show(_noteEditor.note(note).newNote(_newFeature));\n\n // expand the sidebar, avoid obscuring the note if needed\n sidebar.expand(sidebar.intersects(note.extent()));\n\n context.map()\n .on('drawn.select', selectNote);\n };\n\n\n mode.exit = function() {\n _behaviors.forEach(context.uninstall);\n\n d3_select(document)\n .call(_keybinding.unbind);\n\n context.surface()\n .selectAll('.layer-notes .selected')\n .classed('selected hover', false);\n\n context.map()\n .on('drawn.select', null);\n\n context.ui().sidebar\n .hide();\n\n context.selectedNoteID(null);\n };\n\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDraw } from '../behavior/draw';\nimport { modeBrowse } from './browse';\nimport { modeSelectNote } from './select_note';\nimport { osmNote } from '../osm';\nimport { services } from '../services';\n\n\nexport function modeAddNote(context) {\n var mode = {\n id: 'add-note',\n button: 'note',\n description: t.append('modes.add_note.description'),\n key: t('modes.add_note.key')\n };\n\n var behavior = behaviorDraw(context)\n .on('click', add)\n .on('cancel', cancel)\n .on('finish', cancel);\n\n\n function add(loc) {\n var osm = services.osm;\n if (!osm) return;\n\n var note = osmNote({ loc: loc, status: 'open', comments: [] });\n osm.replaceNote(note);\n\n // force a reraw (there is no history change that would otherwise do this)\n context.map().pan([0,0]);\n\n context\n .selectedNoteID(note.id)\n .enter(modeSelectNote(context, note.id).newFeature(true));\n }\n\n\n function cancel() {\n context.enter(modeBrowse(context));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { behaviorOperation } from '../behavior/operation';\nimport { modeMove } from '../modes/move';\nimport { utilGetAllNodes, utilTotalExtent } from '../util/util';\n\n\nexport function operationMove(context, selectedIDs) {\n var multi = (selectedIDs.length === 1 ? 'single' : 'multiple');\n var nodes = utilGetAllNodes(selectedIDs, context.graph());\n var coords = nodes.map(function(n) { return n.loc; });\n var extent = utilTotalExtent(selectedIDs, context.graph());\n\n\n var operation = function() {\n context.enter(modeMove(context, selectedIDs));\n };\n\n\n operation.available = function() {\n return selectedIDs.length > 0;\n };\n\n\n operation.disabled = function() {\n if (extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n } else if (selectedIDs.some(incompleteRelation)) {\n return 'incomplete_relation';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n\n function incompleteRelation(id) {\n var entity = context.entity(id);\n return entity.type === 'relation' && !entity.isComplete(context.graph());\n }\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.move.' + disable + '.' + multi) :\n t.append('operations.move.description.' + multi);\n };\n\n\n operation.annotation = function() {\n return selectedIDs.length === 1 ?\n t('operations.move.annotation.' + context.graph().geometry(selectedIDs[0])) :\n t('operations.move.annotation.feature', { n: selectedIDs.length });\n };\n\n\n operation.id = 'move';\n operation.keys = [t('operations.move.key')];\n operation.title = t.append('operations.move.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n operation.mouseOnly = true;\n\n return operation;\n}\n", "import {\n geoIdentity as d3_geoIdentity,\n geoPath as d3_geoPath,\n geoStream as d3_geoStream\n} from 'd3-geo';\n\nimport { geoVecAdd, geoVecAngle, geoVecLength } from '../geo';\n\n\n// Touch targets control which other vertices we can drag a vertex onto.\n//\n// - the activeID - nope\n// - 1 away (adjacent) to the activeID - yes (vertices will be merged)\n// - 2 away from the activeID - nope (would create a self intersecting segment)\n// - all others on a linear way - yes\n// - all others on a closed way - nope (would create a self intersecting polygon)\n//\n// returns\n// 0 = active vertex - no touch/connect\n// 1 = passive vertex - yes touch/connect\n// 2 = adjacent vertex - yes but pay attention segmenting a line here\n//\nexport function svgPassiveVertex(node, graph, activeID) {\n if (!activeID) return 1;\n if (activeID === node.id) return 0;\n\n var parents = graph.parentWays(node);\n\n var i, j, nodes, isClosed, ix1, ix2, ix3, ix4, max;\n\n for (i = 0; i < parents.length; i++) {\n nodes = parents[i].nodes;\n isClosed = parents[i].isClosed();\n for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby\n if (nodes[j] === node.id) {\n ix1 = j - 2;\n ix2 = j - 1;\n ix3 = j + 1;\n ix4 = j + 2;\n\n if (isClosed) { // wraparound if needed\n max = nodes.length - 1;\n if (ix1 < 0) ix1 = max + ix1;\n if (ix2 < 0) ix2 = max + ix2;\n if (ix3 > max) ix3 = ix3 - max;\n if (ix4 > max) ix4 = ix4 - max;\n }\n\n if (nodes[ix1] === activeID) return 0; // no - prevent self intersect\n else if (nodes[ix2] === activeID) return 2; // ok - adjacent\n else if (nodes[ix3] === activeID) return 2; // ok - adjacent\n else if (nodes[ix4] === activeID) return 0; // no - prevent self intersect\n else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // no - prevent self intersect\n }\n }\n }\n\n return 1; // ok\n}\n\n\n/**\n *\n * @param {iD.Projection} projection\n * @param {iD.Graph} graph\n * @param {Number} dt spacing between segments\n * @param {Function} [shouldReverse]\n * @param {Function} [bothDirections]\n */\nexport function svgMarkerSegments(projection, graph, dt, shouldReverse = () => false, bothDirections = () => false) {\n /**\n * @param {iD.OsmWay} entity\n * @returns {[{id: String, d: String}]} list of svg path segments corres\n */\n return function(entity) {\n let i = 0;\n let offset = dt / 2;\n const segments = [];\n\n const clip = paddedClipExtent(projection);\n\n const coordinates = graph.childNodes(entity).map(function(n) { return n.loc; });\n let a, b;\n\n const _shouldReverse = shouldReverse(entity);\n const _bothDirections = bothDirections(entity);\n\n d3_geoStream({\n type: 'LineString',\n coordinates: coordinates\n }, projection.stream(clip({\n lineStart: function() {},\n lineEnd: function() { a = null; },\n point: function(x, y) {\n b = [x, y];\n\n if (a) {\n let span = geoVecLength(a, b) - offset;\n\n if (span >= 0) {\n const heading = geoVecAngle(a, b);\n const dx = dt * Math.cos(heading);\n const dy = dt * Math.sin(heading);\n let p = [\n a[0] + offset * Math.cos(heading),\n a[1] + offset * Math.sin(heading)\n ];\n\n // gather coordinates\n const coord = [a, p];\n for (span -= dt; span >= 0; span -= dt) {\n p = geoVecAdd(p, [dx, dy]);\n coord.push(p);\n }\n coord.push(b);\n\n // generate svg paths\n let segment = '';\n\n if (!_shouldReverse || _bothDirections) {\n for (let j = 0; j < coord.length; j++) {\n segment += (j === 0 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];\n }\n segments.push({ id: entity.id, index: i++, d: segment });\n }\n\n if (_shouldReverse || _bothDirections) {\n segment = '';\n for (let j = coord.length - 1; j >= 0; j--) {\n segment += (j === coord.length - 1 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];\n }\n segments.push({ id: entity.id, index: i++, d: segment });\n }\n }\n\n offset = -span;\n }\n\n a = b;\n }\n })));\n\n return segments;\n };\n}\n\n\n/**\n * @param {iD.Projection} projection\n * @param {iD.Graph} graph\n * @param {Boolean} isArea\n */\nexport function svgPath(projection, graph, isArea) {\n const cache = {};\n const project = projection.stream;\n const clip = paddedClipExtent(projection, isArea);\n const path = d3_geoPath()\n .projection({stream: function(output) { return project(clip(output)); }});\n\n const svgpath = function(entity) {\n if (entity.id in cache) {\n return cache[entity.id];\n } else {\n return cache[entity.id] = path(entity.asGeoJSON(graph));\n }\n };\n\n svgpath.geojson = function(d) {\n if (d.__featurehash__ !== undefined) {\n if (d.__featurehash__ in cache) {\n return cache[d.__featurehash__];\n } else {\n return cache[d.__featurehash__] = path(d);\n }\n } else {\n return path(d);\n }\n };\n\n return svgpath;\n}\n\n\nexport function svgPointTransform(projection) {\n var svgpoint = function(entity) {\n // http://jsperf.com/short-array-join\n var pt = projection(entity.loc);\n return 'translate(' + pt[0] + ',' + pt[1] + ')';\n };\n\n svgpoint.geojson = function(d) {\n return svgpoint(d.properties.entity);\n };\n\n return svgpoint;\n}\n\n\nexport function svgRelationMemberTags(graph) {\n return function(entity) {\n var tags = entity.tags;\n var shouldCopyMultipolygonTags = !entity.hasInterestingTags();\n graph.parentRelations(entity).forEach(function(relation) {\n var type = relation.tags.type;\n if ((type === 'multipolygon' && shouldCopyMultipolygonTags) || type === 'boundary') {\n tags = Object.assign({}, relation.tags, tags);\n }\n });\n return tags;\n };\n}\n\n\nexport function svgSegmentWay(way, graph, activeID) {\n // When there is no activeID, we can memoize this expensive computation\n if (activeID === undefined) {\n return graph.transient(way, 'waySegments', getWaySegments);\n } else {\n return getWaySegments();\n }\n\n function getWaySegments() {\n const isActiveWay = (way.nodes.indexOf(activeID) !== -1);\n const features = { passive: [], active: [] };\n let start = {};\n\n for (var i = 0; i < way.nodes.length; i++) {\n const node = graph.entity(way.nodes[i]);\n const type = svgPassiveVertex(node, graph, activeID);\n const end = { node: node, type: type };\n\n if (start.type !== undefined) {\n if (start.node.id === activeID || end.node.id === activeID) {\n // push nothing\n } else if (isActiveWay && (start.type === 2 || end.type === 2)) { // one adjacent vertex\n pushActive(start, end, i);\n } else if (start.type === 0 && end.type === 0) { // both active vertices\n pushActive(start, end, i);\n } else {\n pushPassive(start, end, i);\n }\n }\n\n start = end;\n }\n\n return features;\n\n function pushActive(start, end, index) {\n features.active.push({\n type: 'Feature',\n id: way.id + '-' + index + '-nope',\n properties: {\n nope: true,\n target: true,\n entity: way,\n nodes: [start.node, end.node],\n index: index\n },\n geometry: {\n type: 'LineString',\n coordinates: [start.node.loc, end.node.loc]\n }\n });\n }\n\n function pushPassive(start, end, index) {\n features.passive.push({\n type: 'Feature',\n id: way.id + '-' + index,\n properties: {\n target: true,\n entity: way,\n nodes: [start.node, end.node],\n index: index\n },\n geometry: {\n type: 'LineString',\n coordinates: [start.node.loc, end.node.loc]\n }\n });\n }\n }\n}\n\n\n/**\n * Returns a d3 projection stream that clips the given geometries to an\n * extent that is slightly padded.\n *\n * Explanation of magic numbers:\n * \"padding\" here allows space for strokes to extend beyond the viewport,\n * so that the stroke isn't drawn along the edge of the viewport when\n * the shape is clipped.\n * When drawing lines, pad viewport by 5px.\n * When drawing areas, pad viewport by 65px in each direction to allow\n * for 60px area fill stroke (see \".fill-partial path.fill\" css rule)\n *\n * @param {import('../geo/raw_mercator').Projection} projection\n * @param {Boolean} isArea\n */\nfunction paddedClipExtent(projection, isArea = false) {\n var padding = isArea ? 65 : 5;\n var viewport = projection.clipExtent();\n var paddedExtent = [\n [viewport[0][0] - padding, viewport[0][1] - padding],\n [viewport[1][0] + padding, viewport[1][1] + padding]\n ];\n return d3_geoIdentity().clipExtent(paddedExtent).stream;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { osmPathHighwayTagValues, osmPavedTags, osmSemipavedTags, osmLifecyclePrefixes } from '../osm/tags';\n\n\nexport function svgTagClasses() {\n var primaries = [\n 'building', 'highway', 'railway', 'waterway', 'aeroway', 'aerialway',\n 'piste:type', 'boundary', 'power', 'amenity', 'natural', 'landuse',\n 'leisure', 'military', 'place', 'man_made', 'route', 'attraction',\n 'roller_coaster', 'building:part', 'indoor', 'climbing'\n ];\n var statuses = Object.keys(osmLifecyclePrefixes);\n var secondaries = [\n 'oneway', 'bridge', 'tunnel', 'embankment', 'cutting', 'barrier',\n 'surface', 'tracktype', 'footway', 'crossing', 'service', 'sport',\n 'public_transport', 'location', 'parking', 'golf', 'type', 'leisure',\n 'man_made', 'indoor', 'construction', 'proposed'\n ];\n var _tags = function(entity) { return entity.tags; };\n\n\n var tagClasses = function(selection) {\n selection.each(function tagClassesEach(entity) {\n var value = this.className;\n\n if (value.baseVal !== undefined) {\n value = value.baseVal;\n }\n\n var t = _tags(entity);\n\n var computed = tagClasses.getClassesString(t, value);\n\n if (computed !== value) {\n d3_select(this).attr('class', computed);\n }\n });\n };\n\n\n tagClasses.getClassesString = function(t, value) {\n var primary, status;\n var i, j, k, v;\n\n // in some situations we want to render perimeter strokes a certain way\n var overrideGeometry;\n if (/\\bstroke\\b/.test(value)) {\n if (!!t.barrier && t.barrier !== 'no') {\n overrideGeometry = 'line';\n }\n }\n\n // preserve base classes (nothing with `tag-`)\n var classes = value.trim().split(/\\s+/)\n .filter(function(klass) {\n return klass.length && !/^tag-/.test(klass);\n })\n .map(function(klass) { // special overrides for some perimeter strokes\n return (klass === 'line' || klass === 'area') ? (overrideGeometry || klass) : klass;\n });\n\n // pick at most one primary classification tag..\n for (i = 0; i < primaries.length; i++) {\n k = primaries[i];\n v = t[k];\n if (!v || v === 'no') continue;\n\n if (k === 'piste:type') { // avoid a ':' in the class name\n k = 'piste';\n } else if (k === 'building:part') { // avoid a ':' in the class name\n k = 'building_part';\n }\n\n primary = k;\n if (statuses.indexOf(v) !== -1) { // e.g. `railway=abandoned`\n status = v;\n classes.push('tag-' + k);\n } else {\n classes.push('tag-' + k);\n classes.push('tag-' + k + '-' + v);\n }\n\n break;\n }\n\n if (!primary) {\n for (i = 0; i < statuses.length; i++) {\n for (j = 0; j < primaries.length; j++) {\n k = statuses[i] + ':' + primaries[j]; // e.g. `demolished:building=yes`\n v = t[k];\n if (!v || v === 'no') continue;\n\n status = statuses[i];\n break;\n }\n }\n }\n\n // add at most one status tag, only if relates to primary tag..\n if (!status) {\n for (i = 0; i < statuses.length; i++) {\n k = statuses[i];\n v = t[k];\n if (!v || v === 'no') continue;\n\n if (v === 'yes') { // e.g. `railway=rail + abandoned=yes`\n status = k;\n } else if (primary && primary === v) { // e.g. `railway=rail + abandoned=railway`\n status = k;\n } else if (!primary && primaries.indexOf(v) !== -1) { // e.g. `abandoned=railway`\n status = k;\n primary = v;\n classes.push('tag-' + v);\n } // else ignore e.g. `highway=path + abandoned=railway`\n\n if (status) break;\n }\n }\n\n if (status) {\n classes.push('tag-status');\n classes.push('tag-status-' + status);\n }\n\n // add any secondary tags\n for (i = 0; i < secondaries.length; i++) {\n k = secondaries[i];\n v = t[k];\n if (!v || v === 'no' || k === primary) continue;\n classes.push('tag-' + k);\n classes.push('tag-' + k + '-' + v);\n }\n\n // For highways, look for surface tagging..\n if ((primary === 'highway' && !osmPathHighwayTagValues[t.highway]) || primary === 'aeroway') {\n var surface = t.highway === 'track' ? 'unpaved' : 'paved';\n for (k in t) {\n v = t[k];\n if (k in osmPavedTags) {\n surface = osmPavedTags[k][v] ? 'paved' : 'unpaved';\n }\n if (k in osmSemipavedTags && !!osmSemipavedTags[k][v]) {\n surface = 'semipaved';\n }\n }\n classes.push('tag-' + surface);\n }\n\n // If this is a wikidata-tagged item, add a class for that..\n var qid = (\n t.wikidata ||\n t['flag:wikidata'] ||\n t['brand:wikidata'] ||\n t['network:wikidata'] ||\n t['operator:wikidata']\n );\n\n if (qid) {\n classes.push('tag-wikidata');\n }\n\n // ensure that classes for tags keys/values with special characters like spaces\n // are not added to the DOM, because it can cause bizarre issues (#9448)\n return classes\n .filter(klass => /^[-_a-z0-9]+$/.test(klass))\n .join(' ')\n .trim();\n };\n\n\n tagClasses.tags = function(val) {\n if (!arguments.length) return _tags;\n _tags = val;\n return tagClasses;\n };\n\n return tagClasses;\n}\n", "// Patterns only work in Firefox when set directly on element.\n// (This is not a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=750632)\nvar patterns = {\n // tag - pattern name\n // -or-\n // tag - value - pattern name\n // -or-\n // tag - value - rules (optional tag-values, pattern name)\n // (matches earlier rules first, so fallback should be last entry)\n amenity: {\n grave_yard: 'cemetery',\n fountain: 'water_standing'\n },\n landuse: {\n cemetery: [\n { religion: 'christian', pattern: 'cemetery_christian' },\n { religion: 'buddhist', pattern: 'cemetery_buddhist' },\n { religion: 'muslim', pattern: 'cemetery_muslim' },\n { religion: 'jewish', pattern: 'cemetery_jewish' },\n { pattern: 'cemetery' }\n ],\n construction: 'construction',\n farmland: 'farmland',\n farmyard: 'farmyard',\n forest: [\n { leaf_type: 'broadleaved', pattern: 'forest_broadleaved' },\n { leaf_type: 'needleleaved', pattern: 'forest_needleleaved' },\n { leaf_type: 'leafless', pattern: 'forest_leafless' },\n { pattern: 'forest' } // same as 'leaf_type:mixed'\n ],\n grave_yard: 'cemetery',\n grass: 'grass',\n landfill: 'landfill',\n meadow: 'meadow',\n military: 'construction',\n orchard: 'orchard',\n quarry: 'quarry',\n vineyard: 'vineyard'\n },\n leisure: {\n horse_riding: 'farmyard'\n },\n natural: {\n beach: 'beach',\n grassland: 'grass',\n sand: 'beach',\n scrub: 'scrub',\n water: [\n { water: 'pond', pattern: 'pond' },\n { water: 'reservoir', pattern: 'water_standing' },\n { pattern: 'waves' }\n ],\n wetland: [\n { wetland: 'marsh', pattern: 'wetland_marsh' },\n { wetland: 'swamp', pattern: 'wetland_swamp' },\n { wetland: 'bog', pattern: 'wetland_bog' },\n { wetland: 'reedbed', pattern: 'wetland_reedbed' },\n { pattern: 'wetland' }\n ],\n wood: [\n { leaf_type: 'broadleaved', pattern: 'forest_broadleaved' },\n { leaf_type: 'needleleaved', pattern: 'forest_needleleaved' },\n { leaf_type: 'leafless', pattern: 'forest_leafless' },\n { pattern: 'forest' } // same as 'leaf_type:mixed'\n ]\n },\n golf: {\n green: 'golf_green',\n tee: 'grass',\n fairway: 'grass',\n rough: 'scrub'\n },\n surface: {\n grass: 'grass',\n sand: 'beach'\n }\n};\n\nexport function svgTagPattern(tags) {\n // Skip pattern filling if this is a building (buildings don't get patterns applied)\n if (tags.building && tags.building !== 'no') {\n return null;\n }\n\n for (var tag in patterns) {\n var entityValue = tags[tag];\n if (!entityValue) continue;\n\n if (typeof patterns[tag] === 'string') { // extra short syntax (just tag) - pattern name\n return 'pattern-' + patterns[tag];\n } else {\n var values = patterns[tag];\n for (var value in values) {\n if (entityValue !== value) continue;\n\n var rules = values[value];\n if (typeof rules === 'string') { // short syntax - pattern name\n return 'pattern-' + rules;\n }\n\n // long syntax - rule array\n for (var ruleKey in rules) {\n var rule = rules[ruleKey];\n\n var pass = true;\n for (var criterion in rule) {\n if (criterion !== 'pattern') { // reserved for pattern name\n // The only rule is a required tag-value pair\n var v = tags[criterion];\n if (!v || v !== rule[criterion]) {\n pass = false;\n break;\n }\n }\n }\n\n if (pass) {\n return 'pattern-' + rule.pattern;\n }\n }\n }\n }\n }\n\n return null;\n}\n", "import { deepEqual } from 'fast-equals';\nimport { bisector as d3_bisector } from 'd3-array';\n\nimport { osmEntity } from '../osm';\nimport { svgPath, svgSegmentWay } from './helpers';\nimport { svgTagClasses } from './tag_classes';\nimport { svgTagPattern } from './tag_pattern';\n\nexport function svgAreas(projection, context) {\n\n\n function getPatternStyle(tags) {\n var imageID = svgTagPattern(tags);\n if (imageID) {\n return 'url(\"#ideditor-' + imageID + '\")';\n }\n return '';\n }\n\n\n function drawTargets(selection, graph, entities, filter) {\n var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';\n var getPath = svgPath(projection).geojson;\n var activeID = context.activeID();\n var base = context.history().base();\n\n // The targets and nopes will be MultiLineString sub-segments of the ways\n var data = { targets: [], nopes: [] };\n\n entities.forEach(function(way) {\n var features = svgSegmentWay(way, graph, activeID);\n data.targets.push.apply(data.targets, features.passive);\n data.nopes.push.apply(data.nopes, features.active);\n });\n\n\n // Targets allow hover and vertex snapping\n var targetData = data.targets.filter(getPath);\n var targets = selection.selectAll('.area.target-allowed')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(targetData, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n var segmentWasEdited = function(d) {\n var wayID = d.properties.entity.id;\n // if the whole line was edited, don't draw segment changes\n if (!base.entities[wayID] ||\n !deepEqual(graph.entities[wayID].nodes, base.entities[wayID].nodes)) {\n return false;\n }\n return d.properties.nodes.some(function(n) {\n return !base.entities[n.id] ||\n !deepEqual(graph.entities[n.id].loc, base.entities[n.id].loc);\n });\n };\n\n // enter/update\n targets.enter()\n .append('path')\n .merge(targets)\n .attr('d', getPath)\n .attr('class', function(d) { return 'way area target target-allowed ' + targetClass + d.id; })\n .classed('segment-edited', segmentWasEdited);\n\n\n // NOPE\n var nopeData = data.nopes.filter(getPath);\n var nopes = selection.selectAll('.area.target-nope')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(nopeData, function key(d) { return d.id; });\n\n // exit\n nopes.exit()\n .remove();\n\n // enter/update\n nopes.enter()\n .append('path')\n .merge(nopes)\n .attr('d', getPath)\n .attr('class', function(d) { return 'way area target target-nope ' + nopeClass + d.id; })\n .classed('segment-edited', segmentWasEdited);\n }\n\n\n function drawAreas(selection, graph, entities, filter) {\n var path = svgPath(projection, graph, true);\n var areas = {};\n var base = context.history().base();\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n if (entity.geometry(graph) !== 'area') continue;\n if (!areas[entity.id]) {\n areas[entity.id] = {\n entity: entity,\n area: Math.abs(entity.area(graph))\n };\n }\n }\n\n var fills = Object.values(areas).filter(function hasPath(a) { return path(a.entity); });\n fills.sort(function areaSort(a, b) { return b.area - a.area; });\n fills = fills.map(function(a) { return a.entity; });\n\n var strokes = fills.filter(function(area) { return area.type === 'way'; });\n\n var data = {\n clip: fills,\n shadow: strokes,\n stroke: strokes,\n fill: fills\n };\n\n var clipPaths = context.surface().selectAll('defs').selectAll('.clipPath-osm')\n .filter(filter)\n .data(data.clip, osmEntity.key);\n\n clipPaths.exit()\n .remove();\n\n var clipPathsEnter = clipPaths.enter()\n .append('clipPath')\n .attr('class', 'clipPath-osm')\n .attr('id', function(entity) { return 'ideditor-' + entity.id + '-clippath'; });\n\n clipPathsEnter\n .append('path');\n\n clipPaths.merge(clipPathsEnter)\n .selectAll('path')\n .attr('d', path);\n\n\n var drawLayer = selection.selectAll('.layer-osm.areas');\n var touchLayer = selection.selectAll('.layer-touch.areas');\n\n // Draw areas..\n var areagroup = drawLayer\n .selectAll('g.areagroup')\n .data(['fill', 'shadow', 'stroke']);\n\n areagroup = areagroup.enter()\n .append('g')\n .attr('class', function(d) { return 'areagroup area-' + d; })\n .merge(areagroup);\n\n var paths = areagroup\n .selectAll('path')\n .filter(filter)\n .data(function(layer) { return data[layer]; }, osmEntity.key);\n\n paths.exit()\n .remove();\n\n\n var fillpaths = selection.selectAll('.area-fill path.area').nodes();\n var bisect = d3_bisector(function(node) { return -node.__data__.area(graph); }).left;\n\n function sortedByArea(entity) {\n if (this._parent.__data__ === 'fill') {\n return fillpaths[bisect(fillpaths, -entity.area(graph))];\n }\n }\n\n paths.enter()\n .insert('path', sortedByArea)\n .merge(paths)\n .each(function(entity) {\n var layer = this.parentNode.__data__;\n this.setAttribute('class', entity.type + ' area ' + layer + ' ' + entity.id);\n\n if (layer === 'fill') {\n this.setAttribute('clip-path', 'url(#ideditor-' + entity.id + '-clippath)');\n this.style.fill = getPatternStyle(entity.tags);\n this.style.stroke = this.style.fill;\n }\n })\n .classed('added', function(d) {\n return !base.entities[d.id];\n })\n .classed('geometry-edited', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].nodes, base.entities[d.id].nodes);\n })\n .classed('retagged', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses())\n .attr('d', path);\n\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, data.stroke, filter);\n }\n\n return drawAreas;\n}\n", "import type { Feature, Geometry } from \"geojson\";\n\nexport function $(element: Element | Document, tagName: string): Element[] {\n return Array.from(element.getElementsByTagName(tagName));\n}\n\nexport type P = NonNullable;\nexport type F = Feature;\n\nexport type StyleMap = { [key: string]: P };\n\nexport type NS = [string, string][];\n\nexport function normalizeId(id: string) {\n return id[0] === \"#\" ? id : `#${id}`;\n}\n\nexport function $ns(\n element: Element | Document,\n tagName: string,\n ns: string\n): Element[] {\n return Array.from(element.getElementsByTagNameNS(ns, tagName));\n}\n\n/**\n * get the content of a text node, if any\n */\nexport function nodeVal(node: Element | null) {\n node?.normalize();\n return node?.textContent || \"\";\n}\n\n/**\n * Get one Y child of X, if any, otherwise null\n */\nexport function get1(\n node: Element,\n tagName: string,\n callback?: (elem: Element) => unknown\n) {\n const n = node.getElementsByTagName(tagName);\n const result = n.length ? n[0] : null;\n if (result && callback) callback(result);\n return result;\n}\n\nexport function get(\n node: Element | null,\n tagName: string,\n callback?: (elem: Element, properties: P) => P\n) {\n const properties: Feature[\"properties\"] = {};\n if (!node) return properties;\n const n = node.getElementsByTagName(tagName);\n const result = n.length ? n[0] : null;\n if (result && callback) {\n return callback(result, properties);\n }\n return properties;\n}\n\nexport function val1(\n node: Element,\n tagName: string,\n callback: (val: string) => P | undefined | void\n): P {\n const val = nodeVal(get1(node, tagName));\n if (val && callback) return callback(val) || {};\n return {};\n}\n\nexport function $num(\n node: Element,\n tagName: string,\n callback: (val: number) => Feature[\"properties\"]\n) {\n const val = Number.parseFloat(nodeVal(get1(node, tagName)));\n if (Number.isNaN(val)) return undefined;\n if (val && callback) return callback(val) || {};\n return {};\n}\n\nexport function num1(\n node: Element,\n tagName: string,\n callback?: (val: number) => unknown\n) {\n const val = Number.parseFloat(nodeVal(get1(node, tagName)));\n if (Number.isNaN(val)) return undefined;\n if (callback) callback(val);\n return val;\n}\n\nexport function getMulti(node: Element, propertyNames: string[]): P {\n const properties: P = {};\n for (const property of propertyNames) {\n val1(node, property, (val) => {\n properties[property] = val;\n });\n }\n return properties;\n}\n\nexport function isElement(node: Node | null): node is Element {\n return node?.nodeType === 1;\n}\n", "import { isElement, nodeVal } from \"../shared\";\n\nexport type ExtendedValues = [string, string | number][];\n\nexport function getExtensions(node: Element | null): ExtendedValues {\n let values: [string, string | number][] = [];\n if (node === null) return values;\n for (const child of Array.from(node.childNodes)) {\n if (!isElement(child)) continue;\n const name = abbreviateName(child.nodeName);\n if (name === \"gpxtpx:TrackPointExtension\") {\n // loop again for nested garmin extensions (eg. \"gpxtpx:hr\")\n values = values.concat(getExtensions(child));\n } else {\n // push custom extension (eg. \"power\")\n const val = nodeVal(child);\n values.push([name, parseNumeric(val)]);\n }\n }\n return values;\n}\n\nfunction abbreviateName(name: string) {\n return [\"heart\", \"gpxtpx:hr\", \"hr\"].includes(name) ? \"heart\" : name;\n}\n\nfunction parseNumeric(val: string) {\n const num = Number.parseFloat(val);\n return Number.isNaN(num) ? val : num;\n}\n", "import type { Position } from \"geojson\";\nimport { get1, nodeVal, num1 } from \"../shared\";\nimport { type ExtendedValues, getExtensions } from \"./extensions\";\n\ninterface CoordPair {\n coordinates: Position;\n time: string | null;\n extendedValues: ExtendedValues;\n}\n\nexport function coordPair(node: Element): CoordPair | null {\n const ll = [\n Number.parseFloat(node.getAttribute(\"lon\") || \"\"),\n Number.parseFloat(node.getAttribute(\"lat\") || \"\"),\n ];\n\n if (Number.isNaN(ll[0]) || Number.isNaN(ll[1])) {\n return null;\n }\n\n num1(node, \"ele\", (val) => {\n ll.push(val);\n });\n\n const time = get1(node, \"time\");\n return {\n coordinates: ll,\n time: time ? nodeVal(time) : null,\n extendedValues: getExtensions(get1(node, \"extensions\")),\n };\n}\n", "import { $num, type P, get, val1 } from \"../shared\";\n\nexport function getLineStyle(node: Element | null) {\n return get(node, \"line\", (lineStyle) => {\n const val: P = Object.assign(\n {},\n val1(lineStyle, \"color\", (color) => {\n return { stroke: `#${color}` };\n }),\n $num(lineStyle, \"opacity\", (opacity) => {\n return { \"stroke-opacity\": opacity };\n }),\n $num(lineStyle, \"width\", (width) => {\n // GPX width is in mm, convert to px with 96 px per inch\n return { \"stroke-width\": (width * 96) / 25.4 };\n })\n );\n return val;\n });\n}\n", "import { $, type NS, getMulti, nodeVal } from \"../shared\";\n\nexport function extractProperties(ns: NS, node: Element) {\n const properties = getMulti(node, [\n \"name\",\n \"cmt\",\n \"desc\",\n \"type\",\n \"time\",\n \"keywords\",\n ]);\n\n for (const [n, url] of ns) {\n for (const child of Array.from(node.getElementsByTagNameNS(url, \"*\"))) {\n properties[child.tagName.replace(\":\", \"_\")] = nodeVal(child)?.trim();\n }\n }\n\n const links = $(node, \"link\");\n if (links.length) {\n properties.links = links.map((link) =>\n Object.assign(\n { href: link.getAttribute(\"href\") },\n getMulti(link, [\"text\", \"type\"])\n )\n );\n }\n\n return properties;\n}\n", "import type { Document as XDocument } from \"@xmldom/xmldom\";\nimport type {\n Feature,\n FeatureCollection,\n LineString,\n MultiLineString,\n Point,\n Position,\n} from \"geojson\";\nimport { coordPair } from \"./gpx/coord_pair\";\nimport { getLineStyle } from \"./gpx/line\";\nimport { extractProperties } from \"./gpx/properties\";\nimport { $, type NS, type P, get1, getMulti } from \"./shared\";\n\n/**\n * Extract points from a trkseg or rte element.\n */\nfunction getPoints(node: Element, pointname: \"trkpt\" | \"rtept\") {\n const pts = $(node, pointname);\n const line: Position[] = [];\n const times = [];\n const extendedValues: P = {};\n\n for (let i = 0; i < pts.length; i++) {\n const c = coordPair(pts[i]);\n if (!c) {\n continue;\n }\n line.push(c.coordinates);\n if (c.time) times.push(c.time);\n for (const [name, val] of c.extendedValues) {\n const plural =\n name === \"heart\" ? name : `${name.replace(\"gpxtpx:\", \"\")}s`;\n if (!extendedValues[plural]) {\n extendedValues[plural] = Array(pts.length).fill(null);\n }\n extendedValues[plural][i] = val;\n }\n }\n\n if (line.length < 2) return; // Invalid line in GeoJSON\n\n return {\n line: line,\n times: times,\n extendedValues: extendedValues,\n };\n}\n\n/**\n * Extract a LineString geometry from a rte\n * element.\n */\nfunction getRoute(ns: NS, node: Element): Feature | undefined {\n const line = getPoints(node, \"rtept\");\n if (!line) return;\n return {\n type: \"Feature\",\n properties: Object.assign(\n { _gpxType: \"rte\" },\n extractProperties(ns, node),\n getLineStyle(get1(node, \"extensions\"))\n ),\n geometry: {\n type: \"LineString\",\n coordinates: line.line,\n },\n };\n}\n\nfunction getTrack(\n ns: NS,\n node: Element\n): Feature | null {\n const segments = $(node, \"trkseg\");\n const track = [];\n const times = [];\n const extractedLines = [];\n\n for (const segment of segments) {\n const line = getPoints(segment, \"trkpt\");\n if (line) {\n extractedLines.push(line);\n if (line.times?.length) times.push(line.times);\n }\n }\n\n if (extractedLines.length === 0) return null;\n\n const multi = extractedLines.length > 1;\n\n const properties: Feature[\"properties\"] = Object.assign(\n { _gpxType: \"trk\" },\n extractProperties(ns, node),\n getLineStyle(get1(node, \"extensions\")),\n times.length\n ? {\n coordinateProperties: {\n times: multi ? times : times[0],\n },\n }\n : {}\n );\n\n for (let i = 0; i < extractedLines.length; i++) {\n const line = extractedLines[i];\n track.push(line.line);\n if (!properties.coordinateProperties) {\n properties.coordinateProperties = {};\n }\n const props = properties.coordinateProperties;\n // Generally extendedValues will be things like heart\n // rate, and this is an array like { heart: [100, 101...] }\n for (const [name, val] of Object.entries(line.extendedValues)) {\n if (multi) {\n if (!props[name]) {\n props[name] = extractedLines.map((line) =>\n new Array(line.line.length).fill(null)\n );\n }\n props[name][i] = val;\n } else {\n props[name] = val;\n }\n }\n }\n\n return {\n type: \"Feature\",\n properties: properties,\n geometry: multi\n ? {\n type: \"MultiLineString\",\n coordinates: track,\n }\n : {\n type: \"LineString\",\n coordinates: track[0],\n },\n };\n}\n\n/**\n * Extract a point, if possible, from a given node,\n * which is usually a wpt or trkpt\n */\nfunction getPoint(ns: NS, node: Element): Feature | null {\n const properties: Feature[\"properties\"] = Object.assign(\n extractProperties(ns, node),\n getMulti(node, [\"sym\"])\n );\n const pair = coordPair(node);\n if (!pair) return null;\n return {\n type: \"Feature\",\n properties,\n geometry: {\n type: \"Point\",\n coordinates: pair.coordinates,\n },\n };\n}\n\n/**\n * Convert GPX to GeoJSON incrementally, returning\n * a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)\n * that yields output feature by feature.\n */\nexport function* gpxGen(node: Document | XDocument): Generator {\n const n = node as Document;\n const GPXX = \"gpxx\";\n const GPXX_URI = \"http://www.garmin.com/xmlschemas/GpxExtensions/v3\";\n // Namespaces\n const ns: NS = [[GPXX, GPXX_URI]];\n const attrs = n.getElementsByTagName(\"gpx\")[0]?.attributes;\n if (attrs) {\n for (const attr of Array.from(attrs)) {\n if (attr.name?.startsWith(\"xmlns:\") && attr.value !== GPXX_URI) {\n ns.push([attr.name, attr.value]);\n }\n }\n }\n\n for (const track of $(n, \"trk\")) {\n const feature = getTrack(ns, track);\n if (feature) yield feature;\n }\n\n for (const route of $(n, \"rte\")) {\n const feature = getRoute(ns, route);\n if (feature) yield feature;\n }\n\n for (const waypoint of $(n, \"wpt\")) {\n const point = getPoint(ns, waypoint);\n if (point) yield point;\n }\n}\n\n/**\n *\n * Convert a GPX document to GeoJSON. The first argument, `doc`, must be a GPX\n * document as an XML DOM - not as a string. You can get this using jQuery's default\n * `.ajax` function or using a bare XMLHttpRequest with the `.response` property\n * holding an XML DOM.\n *\n * The output is a JavaScript object of GeoJSON data, same as `.kml` outputs, with the\n * addition of a `_gpxType` property on each `LineString` feature that indicates whether\n * the feature was encoded as a route (`rte`) or track (`trk`) in the GPX document.\n */\nexport function gpx(node: Document | XDocument): FeatureCollection {\n return {\n type: \"FeatureCollection\",\n features: Array.from(gpxGen(node)),\n };\n}\n", "import type { Document as XDocument } from \"@xmldom/xmldom\";\nimport type { Feature, FeatureCollection, Position } from \"geojson\";\nimport { $, type P, get, get1, nodeVal, num1 } from \"./shared\";\n\ntype PropertyMapping = readonly [string, string][];\n\nconst EXTENSIONS_NS = \"http://www.garmin.com/xmlschemas/ActivityExtension/v2\";\n\nconst TRACKPOINT_ATTRIBUTES: PropertyMapping = [\n [\"heartRate\", \"heartRates\"],\n [\"Cadence\", \"cadences\"],\n // Extended Trackpoint attributes\n [\"Speed\", \"speeds\"],\n [\"Watts\", \"watts\"],\n];\n\nconst LAP_ATTRIBUTES: PropertyMapping = [\n [\"TotalTimeSeconds\", \"totalTimeSeconds\"],\n [\"DistanceMeters\", \"distanceMeters\"],\n [\"MaximumSpeed\", \"maxSpeed\"],\n [\"AverageHeartRateBpm\", \"avgHeartRate\"],\n [\"MaximumHeartRateBpm\", \"maxHeartRate\"],\n\n // Extended Lap attributes\n [\"AvgSpeed\", \"avgSpeed\"],\n [\"AvgWatts\", \"avgWatts\"],\n [\"MaxWatts\", \"maxWatts\"],\n];\n\nfunction getProperties(node: Element, attributeNames: PropertyMapping) {\n const properties = [];\n\n for (const [tag, alias] of attributeNames) {\n let elem = get1(node, tag);\n if (!elem) {\n const elements = node.getElementsByTagNameNS(EXTENSIONS_NS, tag);\n if (elements.length) {\n elem = elements[0];\n }\n }\n const val = Number.parseFloat(nodeVal(elem));\n if (!Number.isNaN(val)) {\n properties.push([alias, val]);\n }\n }\n\n return properties;\n}\n\nfunction coordPair(node: Element) {\n const ll = [num1(node, \"LongitudeDegrees\"), num1(node, \"LatitudeDegrees\")];\n if (\n ll[0] === undefined ||\n Number.isNaN(ll[0]) ||\n ll[1] === undefined ||\n Number.isNaN(ll[1])\n ) {\n return null;\n }\n const heartRate = get1(node, \"HeartRateBpm\");\n const time = nodeVal(get1(node, \"Time\"));\n get1(node, \"AltitudeMeters\", (alt) => {\n const a = Number.parseFloat(nodeVal(alt));\n if (!Number.isNaN(a)) {\n ll.push(a);\n }\n });\n return {\n coordinates: ll as number[],\n time: time || null,\n heartRate: heartRate ? Number.parseFloat(nodeVal(heartRate)) : null,\n extensions: getProperties(node, TRACKPOINT_ATTRIBUTES),\n };\n}\n\nfunction getPoints(node: Element) {\n const pts = $(node, \"Trackpoint\");\n const line: Position[] = [];\n const times = [];\n const heartRates = [];\n if (pts.length < 2) return null; // Invalid line in GeoJSON\n const extendedProperties: P = {};\n const result = { extendedProperties };\n for (let i = 0; i < pts.length; i++) {\n const c = coordPair(pts[i]);\n if (c === null) continue;\n line.push(c.coordinates);\n const { time, heartRate, extensions } = c;\n if (time) times.push(time);\n if (heartRate) heartRates.push(heartRate);\n for (const [alias, value] of extensions) {\n if (!extendedProperties[alias]) {\n extendedProperties[alias] = Array(pts.length).fill(null);\n }\n extendedProperties[alias][i] = value;\n }\n }\n if (line.length < 2) return null;\n return Object.assign(result, {\n line: line,\n times: times,\n heartRates: heartRates,\n });\n}\n\nfunction getLap(node: Element): Feature | null {\n const segments = $(node, \"Track\");\n const track = [];\n const times = [];\n const heartRates = [];\n const allExtendedProperties = [];\n let line: any;\n const properties: P = Object.assign(\n Object.fromEntries(getProperties(node, LAP_ATTRIBUTES)),\n get(node, \"Name\", (nameElement) => {\n return { name: nodeVal(nameElement) };\n })\n );\n\n for (const segment of segments) {\n line = getPoints(segment);\n if (line) {\n track.push(line.line);\n if (line.times.length) times.push(line.times);\n if (line.heartRates.length) heartRates.push(line.heartRates);\n allExtendedProperties.push(line.extendedProperties);\n }\n }\n for (let i = 0; i < allExtendedProperties.length; i++) {\n const extendedProperties = allExtendedProperties[i];\n for (const property in extendedProperties) {\n if (segments.length === 1) {\n if (line) {\n properties[property] = line.extendedProperties[property];\n }\n } else {\n if (!properties[property]) {\n properties[property] = track.map((track) =>\n Array(track.length).fill(null)\n );\n }\n properties[property][i] = extendedProperties[property];\n }\n }\n }\n\n if (track.length === 0) return null;\n\n if (times.length || heartRates.length) {\n properties.coordinateProperties = Object.assign(\n times.length\n ? {\n times: track.length === 1 ? times[0] : times,\n }\n : {},\n heartRates.length\n ? {\n heart: track.length === 1 ? heartRates[0] : heartRates,\n }\n : {}\n );\n }\n\n return {\n type: \"Feature\",\n properties: properties,\n geometry:\n track.length === 1\n ? {\n type: \"LineString\",\n coordinates: track[0],\n }\n : {\n type: \"MultiLineString\",\n coordinates: track,\n },\n };\n}\n\n/**\n * Incrementally convert a TCX document to GeoJSON. The\n * first argument, `doc`, must be a TCX\n * document as an XML DOM - not as a string.\n */\nexport function* tcxGen(node: Document | XDocument): Generator {\n for (const lap of $(node as Document, \"Lap\")) {\n const feature = getLap(lap);\n if (feature) yield feature;\n }\n\n for (const course of $(node as Document, \"Courses\")) {\n const feature = getLap(course);\n if (feature) yield feature;\n }\n}\n\n/**\n * Convert a TCX document to GeoJSON. The first argument, `doc`, must be a TCX\n * document as an XML DOM - not as a string.\n */\nexport function tcx(node: Document | XDocument): FeatureCollection {\n return {\n type: \"FeatureCollection\",\n features: Array.from(tcxGen(node)),\n };\n}\n", "import type { P } from \"../shared\";\n\nexport function fixColor(v: string, prefix: string): P {\n const properties: P = {};\n const colorProp =\n prefix === \"stroke\" || prefix === \"fill\" ? prefix : `${prefix}-color`;\n if (v[0] === \"#\") {\n v = v.substring(1);\n }\n if (v.length === 6 || v.length === 3) {\n properties[colorProp] = `#${v}`;\n } else if (v.length === 8) {\n properties[`${prefix}-opacity`] =\n Number.parseInt(v.substring(0, 2), 16) / 255;\n properties[colorProp] =\n `#${v.substring(6, 8)}${v.substring(4, 6)}${v.substring(2, 4)}`;\n }\n return properties;\n}\n", "import { type P, get, nodeVal, num1, val1 } from \"../shared\";\nimport { fixColor } from \"./fixColor\";\n\nfunction numericProperty(node: Element, source: string, target: string): P {\n const properties: P = {};\n num1(node, source, (val) => {\n properties[target] = val;\n });\n return properties;\n}\n\nfunction getColor(node: Element, output: string): P {\n return get(node, \"color\", (elem) => fixColor(nodeVal(elem), output));\n}\n\nexport function extractIconHref(node: Element) {\n return get(node, \"Icon\", (icon, properties) => {\n val1(icon, \"href\", (href) => {\n properties.icon = href;\n });\n return properties;\n });\n}\n\nexport function extractIcon(node: Element) {\n return get(node, \"IconStyle\", (iconStyle) => {\n return Object.assign(\n getColor(iconStyle, \"icon\"),\n numericProperty(iconStyle, \"scale\", \"icon-scale\"),\n numericProperty(iconStyle, \"heading\", \"icon-heading\"),\n get(iconStyle, \"hotSpot\", (hotspot) => {\n const left = Number.parseFloat(hotspot.getAttribute(\"x\") || \"\");\n const top = Number.parseFloat(hotspot.getAttribute(\"y\") || \"\");\n const xunits = hotspot.getAttribute(\"xunits\") || \"\";\n const yunits = hotspot.getAttribute(\"yunits\") || \"\";\n if (!Number.isNaN(left) && !Number.isNaN(top))\n return {\n \"icon-offset\": [left, top],\n \"icon-offset-units\": [xunits, yunits],\n };\n return {};\n }),\n extractIconHref(iconStyle)\n );\n });\n}\n\nexport function extractLabel(node: Element) {\n return get(node, \"LabelStyle\", (labelStyle) => {\n return Object.assign(\n getColor(labelStyle, \"label\"),\n numericProperty(labelStyle, \"scale\", \"label-scale\")\n );\n });\n}\n\nexport function extractLine(node: Element) {\n return get(node, \"LineStyle\", (lineStyle) => {\n return Object.assign(\n getColor(lineStyle, \"stroke\"),\n numericProperty(lineStyle, \"width\", \"stroke-width\")\n );\n });\n}\n\nexport function extractPoly(node: Element) {\n return get(node, \"PolyStyle\", (polyStyle, properties) => {\n return Object.assign(\n properties,\n get(polyStyle, \"color\", (elem) => fixColor(nodeVal(elem), \"fill\")),\n val1(polyStyle, \"fill\", (fill) => {\n if (fill === \"0\") return { \"fill-opacity\": 0 };\n }),\n val1(polyStyle, \"outline\", (outline) => {\n if (outline === \"0\") return { \"stroke-opacity\": 0 };\n })\n );\n });\n}\n\nexport function extractStyle(node: Element) {\n return Object.assign(\n {},\n extractPoly(node),\n extractLine(node),\n extractLabel(node),\n extractIcon(node)\n );\n}\n", "import type { Geometry, LineString, Point, Position } from \"geojson\";\nimport { $, $ns, get1, isElement, nodeVal } from \"../shared\";\n\nconst removeSpace = /\\s*/g;\nconst trimSpace = /^\\s*|\\s*$/g;\nconst splitSpace = /\\s+/;\n\n/**\n * Get one coordinate from a coordinate array, if any\n */\nexport function coord1(value: string): Position {\n return value\n .replace(removeSpace, \"\")\n .split(\",\")\n .map(Number.parseFloat)\n .filter((num) => !Number.isNaN(num))\n .slice(0, 3);\n}\n\n/**\n * Get all coordinates from a coordinate array as [[],[]]\n */\nexport function coord(value: string): Position[] {\n return value\n .replace(trimSpace, \"\")\n .split(splitSpace)\n .map(coord1)\n .filter((coord) => {\n return coord.length >= 2;\n });\n}\n\nfunction gxCoords(\n node: Element\n): { geometry: Point | LineString; times: string[] } | null {\n let elems = $(node, \"coord\");\n if (elems.length === 0) {\n elems = $ns(node, \"coord\", \"*\");\n }\n\n const coordinates = elems.map((elem) => {\n return nodeVal(elem).split(\" \").map(Number.parseFloat);\n });\n\n if (coordinates.length === 0) {\n return null;\n }\n\n return {\n geometry:\n coordinates.length > 2\n ? {\n type: \"LineString\",\n coordinates,\n }\n : {\n type: \"Point\",\n coordinates: coordinates[0],\n },\n times: $(node, \"when\").map((elem) => nodeVal(elem)),\n };\n}\n\nexport function fixRing(ring: Position[]) {\n if (ring.length === 0) return ring;\n const first = ring[0];\n const last = ring[ring.length - 1];\n let equal = true;\n for (let i = 0; i < Math.max(first.length, last.length); i++) {\n if (first[i] !== last[i]) {\n equal = false;\n break;\n }\n }\n if (!equal) {\n return ring.concat([ring[0]]);\n }\n return ring;\n}\n\nexport function getCoordinates(node: Element) {\n return nodeVal(get1(node, \"coordinates\"));\n}\n\ninterface GeometriesAndTimes {\n geometries: Geometry[];\n coordTimes: string[][];\n}\n\nexport function getGeometry(node: Element): GeometriesAndTimes {\n let geometries: Geometry[] = [];\n let coordTimes: string[][] = [];\n\n for (let i = 0; i < node.childNodes.length; i++) {\n const child = node.childNodes.item(i);\n if (isElement(child)) {\n switch (child.tagName) {\n case \"MultiGeometry\":\n case \"MultiTrack\":\n case \"gx:MultiTrack\": {\n const childGeometries = getGeometry(child);\n geometries = geometries.concat(childGeometries.geometries);\n coordTimes = coordTimes.concat(childGeometries.coordTimes);\n break;\n }\n\n case \"Point\": {\n const coordinates = coord1(getCoordinates(child));\n if (coordinates.length >= 2) {\n geometries.push({\n type: \"Point\",\n coordinates,\n });\n }\n break;\n }\n case \"LinearRing\":\n case \"LineString\": {\n const coordinates = coord(getCoordinates(child));\n if (coordinates.length >= 2) {\n geometries.push({\n type: \"LineString\",\n coordinates,\n });\n }\n break;\n }\n case \"Polygon\": {\n const coords = [];\n for (const linearRing of $(child, \"LinearRing\")) {\n const ring = fixRing(coord(getCoordinates(linearRing)));\n if (ring.length >= 4) {\n coords.push(ring);\n }\n }\n if (coords.length) {\n geometries.push({\n type: \"Polygon\",\n coordinates: coords,\n });\n }\n break;\n }\n case \"Track\":\n case \"gx:Track\": {\n const gx = gxCoords(child);\n if (!gx) break;\n const { times, geometry } = gx;\n geometries.push(geometry);\n if (times.length) coordTimes.push(times);\n break;\n }\n }\n }\n }\n\n return {\n geometries,\n coordTimes,\n };\n}\n", "import {\n $,\n type P,\n type StyleMap,\n get,\n get1,\n nodeVal,\n normalizeId,\n val1,\n} from \"../shared\";\n\nexport type TypeConverter = (x: string) => unknown;\nexport type Schema = { [key: string]: TypeConverter };\n\nconst toNumber: TypeConverter = (x) => Number(x);\nexport const typeConverters: Record = {\n string: (x) => x,\n int: toNumber,\n uint: toNumber,\n short: toNumber,\n ushort: toNumber,\n float: toNumber,\n double: toNumber,\n bool: (x) => Boolean(x),\n};\n\nexport function extractExtendedData(node: Element, schema: Schema) {\n return get(node, \"ExtendedData\", (extendedData, properties) => {\n for (const data of $(extendedData, \"Data\")) {\n properties[data.getAttribute(\"name\") || \"\"] = nodeVal(\n get1(data, \"value\")\n );\n }\n for (const simpleData of $(extendedData, \"SimpleData\")) {\n const name = simpleData.getAttribute(\"name\") || \"\";\n const typeConverter = schema[name] || typeConverters.string;\n properties[name] = typeConverter(nodeVal(simpleData));\n }\n return properties;\n });\n}\n\nexport function getMaybeHTMLDescription(node: Element) {\n const descriptionNode = get1(node, \"description\");\n for (const c of Array.from(descriptionNode?.childNodes || [])) {\n if (c.nodeType === 4) {\n return {\n description: {\n \"@type\": \"html\",\n value: nodeVal(c as Element),\n },\n };\n }\n }\n return {};\n}\n\nexport function extractTimeSpan(node: Element): P {\n return get(node, \"TimeSpan\", (timeSpan) => {\n return {\n timespan: {\n begin: nodeVal(get1(timeSpan, \"begin\")),\n end: nodeVal(get1(timeSpan, \"end\")),\n },\n };\n });\n}\n\nexport function extractTimeStamp(node: Element): P {\n return get(node, \"TimeStamp\", (timeStamp) => {\n return { timestamp: nodeVal(get1(timeStamp, \"when\")) };\n });\n}\n\nexport function extractCascadedStyle(node: Element, styleMap: StyleMap): P {\n return val1(node, \"styleUrl\", (styleUrl) => {\n styleUrl = normalizeId(styleUrl);\n if (styleMap[styleUrl]) {\n return Object.assign({ styleUrl }, styleMap[styleUrl]);\n }\n // For backward-compatibility. Should we still include\n // styleUrl even if it's not resolved?\n return { styleUrl };\n });\n}\n\nexport enum AltitudeMode {\n ABSOLUTE = \"absolute\",\n RELATIVE_TO_GROUND = \"relativeToGround\",\n CLAMP_TO_GROUND = \"clampToGround\",\n CLAMP_TO_SEAFLOOR = \"clampToSeaFloor\",\n RELATIVE_TO_SEAFLOOR = \"relativeToSeaFloor\",\n}\n\nexport function processAltitudeMode(mode: Element | null): AltitudeMode | null {\n switch (mode?.textContent) {\n case AltitudeMode.ABSOLUTE:\n return AltitudeMode.ABSOLUTE;\n case AltitudeMode.CLAMP_TO_GROUND:\n return AltitudeMode.CLAMP_TO_GROUND;\n case AltitudeMode.CLAMP_TO_SEAFLOOR:\n return AltitudeMode.CLAMP_TO_SEAFLOOR;\n case AltitudeMode.RELATIVE_TO_GROUND:\n return AltitudeMode.RELATIVE_TO_GROUND;\n case AltitudeMode.RELATIVE_TO_SEAFLOOR:\n return AltitudeMode.RELATIVE_TO_SEAFLOOR;\n default:\n break;\n }\n return null;\n}\n\nexport type BBox = [number, number, number, number];\n", "import type { Feature, Polygon } from \"geojson\";\nimport type { KMLOptions } from \"lib/kml\";\nimport { type StyleMap, get1, getMulti, num1 } from \"../shared\";\nimport { extractIconHref, extractStyle } from \"./extractStyle\";\nimport { coord, fixRing, getCoordinates } from \"./geometry\";\nimport {\n type BBox,\n type Schema,\n extractCascadedStyle,\n extractExtendedData,\n extractTimeSpan,\n extractTimeStamp,\n getMaybeHTMLDescription,\n} from \"./shared\";\n\ninterface BoxGeometry {\n bbox?: BBox;\n geometry: Polygon;\n}\n\nfunction getGroundOverlayBox(node: Element): BoxGeometry | null {\n const latLonQuad = get1(node, \"gx:LatLonQuad\");\n\n if (latLonQuad) {\n const ring = fixRing(coord(getCoordinates(node)));\n return {\n geometry: {\n type: \"Polygon\",\n coordinates: [ring],\n },\n };\n }\n\n return getLatLonBox(node);\n}\n\nconst DEGREES_TO_RADIANS = Math.PI / 180;\n\nfunction rotateBox(\n bbox: BBox,\n coordinates: Polygon[\"coordinates\"],\n rotation: number\n): Polygon[\"coordinates\"] {\n const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];\n\n return [\n coordinates[0].map((coordinate) => {\n const dy = coordinate[1] - center[1];\n const dx = coordinate[0] - center[0];\n const distance = Math.sqrt(dy ** 2 + dx ** 2);\n const angle = Math.atan2(dy, dx) + rotation * DEGREES_TO_RADIANS;\n\n return [\n center[0] + Math.cos(angle) * distance,\n center[1] + Math.sin(angle) * distance,\n ];\n }),\n ];\n}\n\nfunction getLatLonBox(node: Element): BoxGeometry | null {\n const latLonBox = get1(node, \"LatLonBox\");\n\n if (latLonBox) {\n const north = num1(latLonBox, \"north\");\n const west = num1(latLonBox, \"west\");\n const east = num1(latLonBox, \"east\");\n const south = num1(latLonBox, \"south\");\n const rotation = num1(latLonBox, \"rotation\");\n\n if (\n typeof north === \"number\" &&\n typeof south === \"number\" &&\n typeof west === \"number\" &&\n typeof east === \"number\"\n ) {\n const bbox: BBox = [west, south, east, north];\n let coordinates = [\n [\n [west, north], // top left\n [east, north], // top right\n [east, south], // top right\n [west, south], // bottom left\n [west, north], // top left (again)\n ],\n ];\n if (typeof rotation === \"number\") {\n coordinates = rotateBox(bbox, coordinates, rotation);\n }\n return {\n bbox,\n geometry: {\n type: \"Polygon\",\n coordinates,\n },\n };\n }\n }\n\n return null;\n}\n\nexport function getGroundOverlay(\n node: Element,\n styleMap: StyleMap,\n schema: Schema,\n options: KMLOptions\n): Feature | null {\n const box = getGroundOverlayBox(node);\n\n const geometry = box?.geometry || null;\n\n if (!geometry && options.skipNullGeometry) {\n return null;\n }\n\n const feature: Feature = {\n type: \"Feature\",\n geometry,\n properties: Object.assign(\n /**\n * Related to\n * https://gist.github.com/tmcw/037a1cb6660d74a392e9da7446540f46\n */\n { \"@geometry-type\": \"groundoverlay\" },\n getMulti(node, [\n \"name\",\n \"address\",\n \"visibility\",\n \"open\",\n \"phoneNumber\",\n \"description\",\n ]),\n getMaybeHTMLDescription(node),\n extractCascadedStyle(node, styleMap),\n extractStyle(node),\n extractIconHref(node),\n extractExtendedData(node, schema),\n extractTimeSpan(node),\n extractTimeStamp(node)\n ),\n };\n\n if (box?.bbox) {\n feature.bbox = box.bbox;\n }\n\n if (feature.properties?.visibility !== undefined) {\n feature.properties.visibility = feature.properties.visibility !== \"0\";\n }\n\n const id = node.getAttribute(\"id\");\n if (id !== null && id !== \"\") feature.id = id;\n return feature;\n}\n", "import type { Feature, Polygon } from \"geojson\";\nimport type { KMLOptions } from \"lib/kml\";\nimport { type StyleMap, get1, getMulti, num1 } from \"../shared\";\nimport { extractIconHref, extractStyle } from \"./extractStyle\";\nimport {\n AltitudeMode,\n type BBox,\n type Schema,\n extractCascadedStyle,\n extractExtendedData,\n extractTimeSpan,\n extractTimeStamp,\n getMaybeHTMLDescription,\n processAltitudeMode,\n} from \"./shared\";\n\ninterface BoxGeometry {\n bbox?: BBox;\n geometry: Polygon;\n}\n\ntype LOD = [number, number | null, number | null, number | null];\ninterface IRegion {\n coordinateBox: BoxGeometry | null;\n lod: LOD | null;\n}\n\nfunction getNetworkLinkRegion(node: Element): IRegion | null {\n const region = get1(node, \"Region\");\n\n if (region) {\n return {\n coordinateBox: getLatLonAltBox(region),\n lod: getLod(node),\n };\n }\n return null;\n}\n\nfunction getLod(node: Element): LOD | null {\n const lod = get1(node, \"Lod\");\n\n if (lod) {\n return [\n num1(lod, \"minLodPixels\") ?? -1,\n num1(lod, \"maxLodPixels\") ?? -1,\n num1(lod, \"minFadeExtent\") ?? null,\n num1(lod, \"maxFadeExtent\") ?? null,\n ];\n }\n\n return null;\n}\n\nfunction getLatLonAltBox(node: Element): BoxGeometry | null {\n const latLonAltBox = get1(node, \"LatLonAltBox\");\n\n if (latLonAltBox) {\n const north = num1(latLonAltBox, \"north\");\n const west = num1(latLonAltBox, \"west\");\n const east = num1(latLonAltBox, \"east\");\n const south = num1(latLonAltBox, \"south\");\n const altitudeMode = processAltitudeMode(\n get1(latLonAltBox, \"altitudeMode\") ||\n get1(latLonAltBox, \"gx:altitudeMode\")\n );\n\n if (altitudeMode) {\n console.debug(\n \"Encountered an unsupported feature of KML for togeojson: please contact developers for support of altitude mode.\"\n );\n }\n if (\n typeof north === \"number\" &&\n typeof south === \"number\" &&\n typeof west === \"number\" &&\n typeof east === \"number\"\n ) {\n const bbox: BBox = [west, south, east, north];\n const coordinates = [\n [\n [west, north], // top left\n [east, north], // top right\n [east, south], // top right\n [west, south], // bottom left\n [west, north], // top left (again)\n ],\n ];\n return {\n bbox,\n geometry: {\n type: \"Polygon\",\n coordinates,\n },\n };\n }\n }\n\n return null;\n}\n\nfunction getLinkObject(node: Element) {\n /*\n \n \n ... \n onChange\n \n 4 \n never\n \n 4 \n 1 \n BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]\n \n ... \n \n */\n const linkObj = get1(node, \"Link\");\n\n if (linkObj) {\n return getMulti(linkObj, [\n \"href\",\n \"refreshMode\",\n \"refreshInterval\",\n \"viewRefreshMode\",\n \"viewRefreshTime\",\n \"viewBoundScale\",\n \"viewFormat\",\n \"httpQuery\",\n ]);\n }\n\n return {};\n}\n\nexport function getNetworkLink(\n node: Element,\n styleMap: StyleMap,\n schema: Schema,\n options: KMLOptions\n): Feature | null {\n const box = getNetworkLinkRegion(node);\n\n const geometry = box?.coordinateBox?.geometry || null;\n\n if (!geometry && options.skipNullGeometry) {\n return null;\n }\n\n const feature: Feature = {\n type: \"Feature\",\n geometry,\n properties: Object.assign(\n /**\n * Related to\n * https://gist.github.com/tmcw/037a1cb6660d74a392e9da7446540f46\n */\n { \"@geometry-type\": \"networklink\" },\n getMulti(node, [\n \"name\",\n \"address\",\n \"visibility\",\n \"open\",\n \"phoneNumber\",\n \"styleUrl\",\n \"refreshVisibility\",\n \"flyToView\",\n \"description\",\n ]),\n getMaybeHTMLDescription(node),\n extractCascadedStyle(node, styleMap),\n extractStyle(node),\n extractIconHref(node),\n extractExtendedData(node, schema),\n extractTimeSpan(node),\n extractTimeStamp(node),\n getLinkObject(node),\n box?.lod ? { lod: box.lod } : {}\n ),\n };\n\n if (box?.coordinateBox?.bbox) {\n feature.bbox = box.coordinateBox.bbox;\n }\n\n if (feature.properties?.visibility !== undefined) {\n feature.properties.visibility = feature.properties.visibility !== \"0\";\n }\n\n const id = node.getAttribute(\"id\");\n if (id !== null && id !== \"\") feature.id = id;\n return feature;\n}\n", "import type { Feature, Geometry } from \"geojson\";\nimport type { KMLOptions } from \"lib/kml\";\nimport { type StyleMap, getMulti } from \"../shared\";\nimport { extractStyle } from \"./extractStyle\";\nimport { getGeometry } from \"./geometry\";\nimport {\n type Schema,\n extractCascadedStyle,\n extractExtendedData,\n extractTimeSpan,\n extractTimeStamp,\n getMaybeHTMLDescription,\n} from \"./shared\";\n\nfunction geometryListToGeometry(geometries: Geometry[]): Geometry | null {\n return geometries.length === 0\n ? null\n : geometries.length === 1\n ? geometries[0]\n : {\n type: \"GeometryCollection\",\n geometries,\n };\n}\n\nexport function getPlacemark(\n node: Element,\n styleMap: StyleMap,\n schema: Schema,\n options: KMLOptions\n): Feature | null {\n const { coordTimes, geometries } = getGeometry(node);\n\n const geometry = geometryListToGeometry(geometries);\n\n if (!geometry && options.skipNullGeometry) {\n return null;\n }\n\n const feature: Feature = {\n type: \"Feature\",\n geometry,\n properties: Object.assign(\n getMulti(node, [\n \"name\",\n \"address\",\n \"visibility\",\n \"open\",\n \"phoneNumber\",\n \"description\",\n ]),\n getMaybeHTMLDescription(node),\n extractCascadedStyle(node, styleMap),\n extractStyle(node),\n extractExtendedData(node, schema),\n extractTimeSpan(node),\n extractTimeStamp(node),\n coordTimes.length\n ? {\n coordinateProperties: {\n times: coordTimes.length === 1 ? coordTimes[0] : coordTimes,\n },\n }\n : {}\n ),\n };\n\n if (feature.properties?.visibility !== undefined) {\n feature.properties.visibility = feature.properties.visibility !== \"0\";\n }\n\n const id = node.getAttribute(\"id\");\n if (id !== null && id !== \"\") feature.id = id;\n return feature;\n}\n", "import type { Document as XDocument } from \"@xmldom/xmldom\";\nimport type { FeatureCollection, Geometry } from \"geojson\";\nimport { extractStyle } from \"./kml/extractStyle\";\nimport { getGroundOverlay } from \"./kml/ground_overlay\";\nimport { getNetworkLink } from \"./kml/networklink\";\nimport { getPlacemark } from \"./kml/placemark\";\nimport { type Schema, typeConverters } from \"./kml/shared\";\nimport {\n $,\n type F,\n type P,\n type StyleMap,\n isElement,\n nodeVal,\n normalizeId,\n val1,\n} from \"./shared\";\n\n/**\n * Options to customize KML output.\n *\n * The only option currently\n * is `skipNullGeometry`. Both the KML and GeoJSON formats support\n * the idea of features that don't have geometries: in KML,\n * this is a Placemark without a Point, etc element, and in GeoJSON\n * it's a geometry member with a value of `null`.\n *\n * toGeoJSON, by default, translates null geometries in KML to\n * null geometries in GeoJSON. For systems that use GeoJSON but\n * don't support null geometries, you can specify `skipNullGeometry`\n * to omit these features entirely and only include\n * features that have a geometry defined.\n */\nexport interface KMLOptions {\n skipNullGeometry?: boolean;\n}\n\n/**\n * A folder including metadata. Folders\n * may contain other folders or features,\n * or nothing at all.\n */\nexport interface Folder {\n type: \"folder\";\n /**\n * Standard values:\n *\n * * \"name\",\n * * \"visibility\",\n * * \"open\",\n * * \"address\",\n * * \"description\",\n * * \"phoneNumber\",\n * * \"visibility\",\n */\n meta: {\n [key: string]: unknown;\n };\n children: Array;\n}\n\n/**\n * A nested folder structure, represented\n * as a tree with folders and features.\n */\nexport interface Root {\n type: \"root\";\n children: Array;\n}\n\ntype TreeContainer = Root | Folder;\n\nfunction getStyleId(style: Element) {\n let id = style.getAttribute(\"id\");\n const parentNode = style.parentNode;\n if (\n !id &&\n isElement(parentNode) &&\n parentNode.localName === \"CascadingStyle\"\n ) {\n id = parentNode.getAttribute(\"kml:id\") || parentNode.getAttribute(\"id\");\n }\n return normalizeId(id || \"\");\n}\n\nfunction buildStyleMap(node: Document): StyleMap {\n const styleMap: StyleMap = {};\n for (const style of $(node, \"Style\")) {\n styleMap[getStyleId(style)] = extractStyle(style);\n }\n for (const map of $(node, \"StyleMap\")) {\n const id = normalizeId(map.getAttribute(\"id\") || \"\");\n val1(map, \"styleUrl\", (styleUrl) => {\n styleUrl = normalizeId(styleUrl);\n if (styleMap[styleUrl]) {\n styleMap[id] = styleMap[styleUrl];\n }\n });\n }\n return styleMap;\n}\n\nfunction buildSchema(node: Document): Schema {\n const schema: Schema = {};\n for (const field of $(node, \"SimpleField\")) {\n schema[field.getAttribute(\"name\") || \"\"] =\n typeConverters[field.getAttribute(\"type\") || \"\"] || typeConverters.string;\n }\n return schema;\n}\n\nconst FOLDER_PROPS = [\n \"name\",\n \"visibility\",\n \"open\",\n \"address\",\n \"description\",\n \"phoneNumber\",\n \"visibility\",\n] as const;\n\nfunction getFolder(node: Element): Folder {\n const meta: P = {};\n\n for (const child of Array.from(node.childNodes)) {\n if (isElement(child) && FOLDER_PROPS.includes(child.tagName as any)) {\n meta[child.tagName] = nodeVal(child);\n }\n }\n\n return {\n type: \"folder\",\n meta,\n children: [],\n };\n}\n\n/**\n * Yield a nested tree with KML folder structure\n *\n * This generates a tree with the given structure:\n *\n * ```js\n * {\n * \"type\": \"root\",\n * \"children\": [\n * {\n * \"type\": \"folder\",\n * \"meta\": {\n * \"name\": \"Test\"\n * },\n * \"children\": [\n * // ...features and folders\n * ]\n * }\n * // ...features\n * ]\n * }\n * ```\n *\n * ### GroundOverlay\n *\n * GroundOverlay elements are converted into\n * `Feature` objects with `Polygon` geometries,\n * a property like:\n *\n * ```json\n * {\n * \"@geometry-type\": \"groundoverlay\"\n * }\n * ```\n *\n * And the ground overlay's image URL in the `href`\n * property. Ground overlays will need to be displayed\n * with a separate method to other features, depending\n * on which map framework you're using.\n */\nexport function kmlWithFolders(\n node: Document | XDocument,\n options: KMLOptions = {\n skipNullGeometry: false,\n }\n): Root {\n const n = node as Document;\n const styleMap = buildStyleMap(n);\n const schema = buildSchema(n);\n\n // atomic geospatial types supported by KML - MultiGeometry is\n // handled separately\n // all root placemarks in the file\n const placemarks = [];\n const networkLinks = [];\n const tree: Root = { type: \"root\", children: [] };\n\n function traverse(\n node: Document | ChildNode | Element,\n pointer: TreeContainer,\n options: KMLOptions\n ) {\n if (isElement(node)) {\n switch (node.tagName) {\n case \"GroundOverlay\": {\n placemarks.push(node);\n const placemark = getGroundOverlay(node, styleMap, schema, options);\n if (placemark) {\n pointer.children.push(placemark);\n }\n break;\n }\n case \"Placemark\": {\n placemarks.push(node);\n const placemark = getPlacemark(node, styleMap, schema, options);\n if (placemark) {\n pointer.children.push(placemark);\n }\n break;\n }\n case \"Folder\": {\n const folder = getFolder(node);\n pointer.children.push(folder);\n pointer = folder;\n break;\n }\n case \"NetworkLink\": {\n networkLinks.push(node);\n const networkLink = getNetworkLink(node, styleMap, schema, options);\n if (networkLink) {\n pointer.children.push(networkLink);\n }\n break;\n }\n }\n }\n\n if (node.childNodes) {\n for (let i = 0; i < node.childNodes.length; i++) {\n traverse(node.childNodes[i], pointer, options);\n }\n }\n }\n\n traverse(n, tree, options);\n\n return tree;\n}\n\n/**\n * Convert KML to GeoJSON incrementally, returning\n * a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)\n * that yields output feature by feature.\n */\nexport function* kmlGen(\n node: Document | XDocument,\n options: KMLOptions = {\n skipNullGeometry: false,\n }\n): Generator {\n const n = node as Document;\n const styleMap = buildStyleMap(n);\n const schema = buildSchema(n);\n for (const placemark of $(n, \"Placemark\")) {\n const feature = getPlacemark(placemark, styleMap, schema, options);\n if (feature) yield feature;\n }\n for (const groundOverlay of $(n, \"GroundOverlay\")) {\n const feature = getGroundOverlay(groundOverlay, styleMap, schema, options);\n if (feature) yield feature;\n }\n for (const networkLink of $(n, \"NetworkLink\")) {\n const feature = getNetworkLink(networkLink, styleMap, schema, options);\n if (feature) yield feature;\n }\n}\n\n/**\n * Convert a KML document to GeoJSON. The first argument, `doc`, must be a KML\n * document as an XML DOM - not as a string. You can get this using jQuery's default\n * `.ajax` function or using a bare XMLHttpRequest with the `.response` property\n * holding an XML DOM.\n *\n * The output is a JavaScript object of GeoJSON data. You can convert it to a string\n * with [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)\n * or use it directly in libraries.\n */\nexport function kml(\n node: Document | XDocument,\n options: KMLOptions = {\n skipNullGeometry: false,\n }\n): FeatureCollection {\n return {\n type: \"FeatureCollection\",\n features: Array.from(kmlGen(node as Document, options)),\n };\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { geoBounds as d3_geoBounds, geoPath as d3_geoPath } from 'd3-geo';\nimport { text as d3_text } from 'd3-fetch';\nimport { select as d3_select } from 'd3-selection';\n\nimport stringify from 'fast-json-stable-stringify';\nimport { gpx, kml } from '@tmcw/togeojson';\n\nimport { geoExtent, geoPolygonIntersectsPolygon } from '../geo';\nimport { services } from '../services';\nimport { svgPath } from './helpers';\nimport { utilDetect } from '../util/detect';\nimport { utilArrayFlatten, utilArrayUnion, utilHashcode } from '../util';\n\n\nvar _initialized = false;\nvar _enabled = false;\nvar _geojson;\n\n\nexport function svgData(projection, context, dispatch) {\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var _showLabels = true;\n var detected = utilDetect();\n var layer = d3_select(null);\n var _vtService;\n var _fileList;\n var _template;\n var _src;\n\n const supportedFormats = [\n '.gpx',\n '.kml',\n '.geojson',\n '.json'\n ];\n\n\n function init() {\n if (_initialized) return; // run once\n\n _geojson = {};\n _enabled = true;\n\n function over(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n d3_event.dataTransfer.dropEffect = 'copy';\n }\n\n context.container()\n .attr('dropzone', 'copy')\n .on('drop.svgData', function(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n if (!detected.filedrop) return;\n var f = d3_event.dataTransfer.files[0];\n var extension = getExtension(f.name);\n if (!supportedFormats.includes(extension)) return;\n drawData.fileList(d3_event.dataTransfer.files);\n })\n .on('dragenter.svgData', over)\n .on('dragexit.svgData', over)\n .on('dragover.svgData', over);\n\n _initialized = true;\n }\n\n\n function getService() {\n if (services.vectorTile && !_vtService) {\n _vtService = services.vectorTile;\n _vtService.event.on('loadedData', throttledRedraw);\n } else if (!services.vectorTile && _vtService) {\n _vtService = null;\n }\n\n return _vtService;\n }\n\n\n function showLayer() {\n layerOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', layerOff);\n }\n\n\n function layerOn() {\n layer.style('display', 'block');\n }\n\n\n function layerOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n // ensure that all geojson features in a collection have IDs\n function ensureIDs(gj) {\n if (!gj) return null;\n\n if (gj.type === 'FeatureCollection') {\n for (var i = 0; i < gj.features.length; i++) {\n ensureFeatureID(gj.features[i]);\n }\n } else {\n ensureFeatureID(gj);\n }\n return gj;\n }\n\n\n // ensure that each single Feature object has a unique ID\n function ensureFeatureID(feature) {\n if (!feature) return;\n feature.__featurehash__ = utilHashcode(stringify(feature));\n return feature;\n }\n\n\n // Prefer an array of Features instead of a FeatureCollection\n function getFeatures(gj) {\n if (!gj) return [];\n\n if (gj.type === 'FeatureCollection') {\n return gj.features;\n } else {\n return [gj];\n }\n }\n\n\n function featureKey(d) {\n return d.__featurehash__;\n }\n\n\n function isPolygon(d) {\n return d.geometry.type === 'Polygon' || d.geometry.type === 'MultiPolygon';\n }\n\n\n function clipPathID(d) {\n return 'ideditor-data-' + d.__featurehash__ + '-clippath';\n }\n\n\n function featureClasses(d) {\n return [\n 'data' + d.__featurehash__,\n d.geometry.type,\n isPolygon(d) ? 'area' : '',\n d.__layerID__ || ''\n ].filter(Boolean).join(' ');\n }\n\n\n function drawData(selection) {\n var vtService = getService();\n var getPath = svgPath(projection).geojson;\n var getAreaPath = svgPath(projection, null, true).geojson;\n var hasData = drawData.hasData();\n\n layer = selection.selectAll('.layer-mapdata')\n .data(_enabled && hasData ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-mapdata')\n .merge(layer);\n\n var surface = context.surface();\n if (!surface || surface.empty()) return; // not ready to draw yet, starting up\n\n\n // Gather data\n var geoData, polygonData;\n if (_template && vtService) { // fetch data from vector tile service\n var sourceID = _template;\n vtService.loadTiles(sourceID, _template, projection);\n geoData = vtService.data(sourceID, projection);\n } else {\n geoData = getFeatures(_geojson);\n }\n geoData = geoData.filter(getPath);\n polygonData = geoData.filter(isPolygon);\n\n\n // Draw clip paths for polygons\n var clipPaths = surface.selectAll('defs').selectAll('.clipPath-data')\n .data(polygonData, featureKey);\n\n clipPaths.exit()\n .remove();\n\n var clipPathsEnter = clipPaths.enter()\n .append('clipPath')\n .attr('class', 'clipPath-data')\n .attr('id', clipPathID);\n\n clipPathsEnter\n .append('path');\n\n clipPaths.merge(clipPathsEnter)\n .selectAll('path')\n .attr('d', getAreaPath);\n\n\n // Draw fill, shadow, stroke layers\n var datagroups = layer\n .selectAll('g.datagroup')\n .data(['fill', 'shadow', 'stroke']);\n\n datagroups = datagroups.enter()\n .append('g')\n .attr('class', function(d) { return 'datagroup datagroup-' + d; })\n .merge(datagroups);\n\n\n // Draw paths\n var pathData = {\n fill: polygonData,\n shadow: geoData,\n stroke: geoData\n };\n\n var paths = datagroups\n .selectAll('path')\n .data(function(layer) { return pathData[layer]; }, featureKey);\n\n // exit\n paths.exit()\n .remove();\n\n // enter/update\n paths.enter()\n .append('path')\n .attr('class', function(d) {\n var datagroup = this.parentNode.__data__;\n return 'pathdata ' + datagroup + ' ' + featureClasses(d);\n })\n .attr('clip-path', function(d) {\n var datagroup = this.parentNode.__data__;\n return datagroup === 'fill' ? ('url(#' + clipPathID(d) + ')') : null;\n })\n .merge(paths)\n .attr('d', function(d) {\n var datagroup = this.parentNode.__data__;\n return datagroup === 'fill' ? getAreaPath(d) : getPath(d);\n });\n\n\n // Draw labels\n layer\n .call(drawLabels, 'label-halo', geoData)\n .call(drawLabels, 'label', geoData);\n\n\n function drawLabels(selection, textClass, data) {\n var labelPath = d3_geoPath(projection);\n var labelData = data.filter(function(d) {\n return _showLabels && d.properties && (d.properties.desc || d.properties.name);\n });\n\n var labels = selection.selectAll('text.' + textClass)\n .data(labelData, featureKey);\n\n // exit\n labels.exit()\n .remove();\n\n // enter/update\n labels.enter()\n .append('text')\n .attr('class', function(d) { return textClass + ' ' + featureClasses(d); })\n .merge(labels)\n .text(function(d) {\n return d.properties.desc || d.properties.name;\n })\n .attr('x', function(d) {\n var centroid = labelPath.centroid(d);\n return centroid[0] + 11;\n })\n .attr('y', function(d) {\n var centroid = labelPath.centroid(d);\n return centroid[1];\n });\n }\n }\n\n\n function getExtension(fileName) {\n if (!fileName) return;\n\n var re = /\\.(gpx|kml|(geo)?json|png)$/i;\n var match = fileName.toLowerCase().match(re);\n return match && match.length && match[0];\n }\n\n\n function xmlToDom(textdata) {\n return (new DOMParser()).parseFromString(textdata, 'text/xml');\n }\n\n\n function stringifyGeojsonProperties(feature) {\n const properties = feature.properties;\n for (const key in properties) {\n const property = properties[key];\n if (typeof property === 'number' || typeof property === 'boolean' || Array.isArray(property)) {\n properties[key] = property.toString();\n } else if (property === null) {\n properties[key] = 'null';\n } else if (typeof property === 'object') {\n properties[key] = JSON.stringify(property);\n }\n }\n }\n\n\n drawData.setFile = function(extension, data) {\n _template = null;\n _fileList = null;\n _geojson = null;\n _src = null;\n\n var gj;\n switch (extension) {\n case '.gpx':\n gj = gpx(xmlToDom(data));\n break;\n case '.kml':\n gj = kml(xmlToDom(data));\n break;\n case '.geojson':\n case '.json':\n gj = JSON.parse(data);\n if (gj.type === 'FeatureCollection') {\n gj.features.forEach(stringifyGeojsonProperties);\n } else if (gj.type === 'Feature') {\n stringifyGeojsonProperties(gj);\n }\n break;\n }\n\n gj = gj || {};\n if (Object.keys(gj).length) {\n _geojson = ensureIDs(gj);\n _src = extension + ' data file';\n this.fitZoom();\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.showLabels = function(val) {\n if (!arguments.length) return _showLabels;\n\n _showLabels = val;\n return this;\n };\n\n\n drawData.enabled = function(val) {\n if (!arguments.length) return _enabled;\n\n _enabled = val;\n if (_enabled) {\n showLayer();\n } else {\n hideLayer();\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.hasData = function() {\n var gj = _geojson || {};\n return !!(_template || Object.keys(gj).length);\n };\n\n\n drawData.template = function(val, src) {\n if (!arguments.length) return _template;\n\n // test source against OSM imagery blocklists..\n var osm = context.connection();\n if (osm) {\n for (const regex of osm.imageryBlocklists()) {\n if (regex.test(val)) {\n // matches a blocked sources -> do not set template\n return;\n };\n }\n }\n\n _template = val;\n _fileList = null;\n _geojson = null;\n\n // strip off the querystring/hash from the template,\n // it often includes the access token\n _src = src || ('vectortile:' + val.split(/[?#]/)[0]);\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.geojson = function(gj, src) {\n if (!arguments.length) return _geojson;\n\n _template = null;\n _fileList = null;\n _geojson = null;\n _src = null;\n\n gj = gj || {};\n if (Object.keys(gj).length) {\n _geojson = ensureIDs(gj);\n _src = src || 'unknown.geojson';\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.fileList = function(fileList) {\n if (!arguments.length) return _fileList;\n\n _template = null;\n _geojson = null;\n _src = null;\n _fileList = fileList;\n\n if (!fileList || !fileList.length) return this;\n var f = fileList[0];\n var extension = getExtension(f.name);\n var reader = new FileReader();\n reader.onload = (function() {\n return function(e) {\n drawData.setFile(extension, e.target.result);\n };\n })(f);\n\n reader.readAsText(f);\n\n return this;\n };\n\n\n drawData.url = function(url, defaultExtension) {\n _template = null;\n _fileList = null;\n _geojson = null;\n _src = null;\n\n // strip off any querystring/hash from the url before checking extension\n var testUrl = url.split(/[?#]/)[0];\n var extension = getExtension(testUrl) || defaultExtension;\n if (extension) {\n _template = null;\n d3_text(url)\n .then(function(data) {\n drawData.setFile(extension, data);\n })\n .catch(function() {\n /* ignore */\n });\n\n } else {\n drawData.template(url);\n }\n\n return this;\n };\n\n\n drawData.getSrc = function() {\n return _src || '';\n };\n\n\n drawData.fitZoom = function() {\n var features = getFeatures(_geojson);\n if (!features.length) return;\n\n var map = context.map();\n var viewport = map.trimmedExtent().polygon();\n var coords = features.reduce(function(coords, feature) {\n var geom = feature.geometry;\n if (!geom) return coords;\n\n var c = geom.coordinates;\n\n /* eslint-disable no-fallthrough */\n switch (geom.type) {\n case 'Point':\n c = [c];\n case 'MultiPoint':\n case 'LineString':\n break;\n\n case 'MultiPolygon':\n c = utilArrayFlatten(c);\n case 'Polygon':\n case 'MultiLineString':\n c = utilArrayFlatten(c);\n break;\n }\n /* eslint-enable no-fallthrough */\n\n return utilArrayUnion(coords, c);\n }, []);\n\n if (!geoPolygonIntersectsPolygon(viewport, coords, true)) {\n var extent = geoExtent(d3_geoBounds({ type: 'LineString', coordinates: coords }));\n map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));\n }\n\n return this;\n };\n\n\n init();\n return drawData;\n}\n", "import { fileFetcher } from '../core/file_fetcher';\nimport { svgPath } from './helpers';\n\n\nexport function svgDebug(projection, context) {\n\n function drawDebug(selection) {\n const showTile = context.getDebug('tile');\n const showCollision = context.getDebug('collision');\n const showImagery = context.getDebug('imagery');\n const showTouchTargets = context.getDebug('target');\n const showDownloaded = context.getDebug('downloaded');\n\n let debugData = [];\n if (showTile) {\n debugData.push({ class: 'red', label: 'tile' });\n }\n if (showCollision) {\n debugData.push({ class: 'yellow', label: 'collision' });\n }\n if (showImagery) {\n debugData.push({ class: 'orange', label: 'imagery' });\n }\n if (showTouchTargets) {\n debugData.push({ class: 'pink', label: 'touchTargets' });\n }\n if (showDownloaded) {\n debugData.push({ class: 'purple', label: 'downloaded' });\n }\n\n\n let legend = context.container().select('.main-content')\n .selectAll('.debug-legend')\n .data(debugData.length ? [0] : []);\n\n legend.exit()\n .remove();\n\n legend = legend.enter()\n .append('div')\n .attr('class', 'fillD debug-legend')\n .merge(legend);\n\n\n let legendItems = legend.selectAll('.debug-legend-item')\n .data(debugData, d => d.label);\n\n legendItems.exit()\n .remove();\n\n legendItems.enter()\n .append('span')\n .attr('class', d => `debug-legend-item ${d.class}`)\n .text(d => d.label);\n\n\n let layer = selection.selectAll('.layer-debug')\n .data(showImagery || showDownloaded ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-debug')\n .merge(layer);\n\n\n // imagery\n const extent = context.map().extent();\n fileFetcher.get('imagery')\n .then(d => {\n const hits = (showImagery && d.query.bbox(extent.rectangle(), true)) || [];\n const features = hits.map(d => d.features[d.id]);\n\n let imagery = layer.selectAll('path.debug-imagery')\n .data(features);\n\n imagery.exit()\n .remove();\n\n imagery.enter()\n .append('path')\n .attr('class', 'debug-imagery debug orange');\n })\n .catch(() => { /* ignore */ });\n\n // downloaded\n const osm = context.connection();\n let dataDownloaded = [];\n if (osm && showDownloaded) {\n const rtree = osm.caches('get').tile.rtree;\n dataDownloaded = rtree.all().map(bbox => {\n return {\n type: 'Feature',\n properties: { id: bbox.id },\n geometry: {\n type: 'Polygon',\n coordinates: [[\n [ bbox.minX, bbox.minY ],\n [ bbox.minX, bbox.maxY ],\n [ bbox.maxX, bbox.maxY ],\n [ bbox.maxX, bbox.minY ],\n [ bbox.minX, bbox.minY ]\n ]]\n }\n };\n });\n }\n\n let downloaded = layer\n .selectAll('path.debug-downloaded')\n .data(showDownloaded ? dataDownloaded : []);\n\n downloaded.exit()\n .remove();\n\n downloaded.enter()\n .append('path')\n .attr('class', 'debug-downloaded debug purple');\n\n // update\n layer.selectAll('path')\n .attr('d', svgPath(projection).geojson);\n }\n\n\n // This looks strange because `enabled` methods on other layers are\n // chainable getter/setters, and this one is just a getter.\n drawDebug.enabled = function() {\n if (!arguments.length) {\n return context.getDebug('tile') ||\n context.getDebug('collision') ||\n context.getDebug('imagery') ||\n context.getDebug('target') ||\n context.getDebug('downloaded');\n } else {\n return this;\n }\n };\n\n\n return drawDebug;\n}\n", "import { svg as d3_svg } from 'd3-fetch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { utilArrayUniq } from '../util';\n\n\n/*\n A standalone SVG element that contains only a `defs` sub-element. To be\n used once globally, since defs IDs must be unique within a document.\n*/\nexport function svgDefs(context) {\n\n var _defsSelection = d3_select(null);\n\n var _spritesheetIds = [\n 'iD-sprite', 'maki-sprite', 'temaki-sprite', 'fa-sprite', 'roentgen-sprite', 'community-sprite'\n ];\n\n function drawDefs(selection) {\n _defsSelection = selection.append('defs');\n\n // add markers\n\n // SVG markers have to be given a colour where they're defined\n // (they can't inherit it from the line they're attached to),\n // so we need to manually define markers for each color of tag\n // (also, it's slightly nicer if we can control the\n // positioning for different tags)\n\n /** @param {string} name @param {string} colour */\n function addOnewayMarker(name, colour) {\n _defsSelection\n .append('marker')\n .attr('id', `ideditor-oneway-marker-${name}`)\n .attr('viewBox', '0 0 10 5')\n .attr('refX', 4)\n .attr('refY', 2.5)\n .attr('markerWidth', 2)\n .attr('markerHeight', 2)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'oneway-marker-path')\n .attr('d', 'M 6,3 L 0,3 L 0,2 L 6,2 L 5,0 L 10,2.5 L 5,5 z')\n .attr('stroke', 'none')\n .attr('fill', colour)\n .attr('opacity', '1');\n }\n addOnewayMarker('black', '#333'); // default\n addOnewayMarker('white', '#fff'); // for dark lines (bridges under construction, railways, etc.)\n addOnewayMarker('gray', '#eee'); // for railway lines\n\n\n function addSidedMarker(name, color, offset, style) {\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-sided-marker-' + name)\n .attr('viewBox', '0 0 2 2')\n .attr('refX', 1)\n .attr('refY', -offset)\n .attr('markerWidth', 1.5)\n .attr('markerHeight', 1.5)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'sided-marker-path sided-marker-' + name + '-path')\n .attr('d', style === 'circle'\n ? 'M 0,0.5 a 0.5,0.5 0 1,0 1,0 a 0.5,0.5 0 1,0 -1,0'\n : 'M 0,0 L 1,1 L 2,0 z')\n .attr('stroke', 'none')\n .attr('fill', color);\n }\n addSidedMarker('natural', 'rgb(170, 170, 170)', 0);\n // for a coastline, the arrows are (somewhat unintuitively) on\n // the water side, so let's color them blue (with a gap) to\n // give a stronger indication\n addSidedMarker('coastline', '#77dede', 1);\n addSidedMarker('waterway', '#77dede', 1);\n // barriers have a dashed line, and separating the triangle\n // from the line visually suits that\n addSidedMarker('barrier', '#ddd', 1);\n // dedicated style for guard rails (#9594):\n // marker on opposite side, circles instead of triangles\n addSidedMarker('guard_rail', '#ddd', -1.5, 'circle');\n addSidedMarker('man_made', '#fff', 0);\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z')\n .attr('fill', '#333')\n .attr('fill-opacity', '0.75')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker-wireframe')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z')\n .attr('fill', 'none')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker-side')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 3 14 C 8 13 8 13 13 14 L 8 5 Z')\n .attr('fill', '#333')\n .attr('fill-opacity', '0.75')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker-side-wireframe')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 3 14 C 8 13 8 13 13 14 L 8 5 Z')\n .attr('fill', 'none')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n\n // add patterns\n var patterns = _defsSelection.selectAll('pattern')\n .data([\n // pattern name, pattern image name\n ['beach', 'dots'],\n ['construction', 'construction'],\n ['cemetery', 'cemetery'],\n ['cemetery_christian', 'cemetery_christian'],\n ['cemetery_buddhist', 'cemetery_buddhist'],\n ['cemetery_muslim', 'cemetery_muslim'],\n ['cemetery_jewish', 'cemetery_jewish'],\n ['farmland', 'farmland'],\n ['farmyard', 'farmyard'],\n ['forest', 'forest'],\n ['forest_broadleaved', 'forest_broadleaved'],\n ['forest_needleleaved', 'forest_needleleaved'],\n ['forest_leafless', 'forest_leafless'],\n ['golf_green', 'grass'],\n ['grass', 'grass'],\n ['landfill', 'landfill'],\n ['meadow', 'grass'],\n ['orchard', 'orchard'],\n ['pond', 'pond'],\n ['quarry', 'quarry'],\n ['scrub', 'bushes'],\n ['vineyard', 'vineyard'],\n ['water_standing', 'lines'],\n ['waves', 'waves'],\n ['wetland', 'wetland'],\n ['wetland_marsh', 'wetland_marsh'],\n ['wetland_swamp', 'wetland_swamp'],\n ['wetland_bog', 'wetland_bog'],\n ['wetland_reedbed', 'wetland_reedbed']\n ])\n .enter()\n .append('pattern')\n .attr('id', function (d) { return 'ideditor-pattern-' + d[0]; })\n .attr('width', 32)\n .attr('height', 32)\n .attr('patternUnits', 'userSpaceOnUse');\n\n patterns\n .append('rect')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', 32)\n .attr('height', 32)\n .attr('class', function (d) { return 'pattern-color-' + d[0]; });\n\n patterns\n .append('image')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', 32)\n .attr('height', 32)\n .attr('xlink:href', function (d) {\n return context.imagePath('pattern/' + d[1] + '.png');\n });\n\n // add clip paths\n _defsSelection.selectAll('clipPath')\n .data([12, 18, 20, 32, 45])\n .enter()\n .append('clipPath')\n .attr('id', function (d) { return 'ideditor-clip-square-' + d; })\n .append('rect')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', function (d) { return d; })\n .attr('height', function (d) { return d; });\n\n // add svg filters\n const filters = _defsSelection.selectAll('filter')\n .data(['alpha-slope5'])\n .enter()\n .append('filter')\n .attr('id', d => d);\n // Alters the alpha channel such that everything but\n // (almost) transparent pixels are rendered fully opaque:\n // This is used in a workaround for how chrome is rendering\n // the edges of `img` elements when the page zoom is not a\n // \"round value\": the semi-transparent pixels of neighboring\n // tiles cannot \"add up\" to a fully opaque background layer.\n // See https://github.com/openstreetmap/iD/issues/10747\n // and https://github.com/openstreetmap/iD/pull/10594\n const alphaSlope5 = filters.filter('#alpha-slope5')\n .append('feComponentTransfer');\n alphaSlope5.append('feFuncR').attr('type', 'identity');\n alphaSlope5.append('feFuncG').attr('type', 'identity');\n alphaSlope5.append('feFuncB').attr('type', 'identity');\n alphaSlope5.append('feFuncA')\n .attr('type', 'linear')\n .attr('slope', 5);\n\n // add symbol spritesheets\n addSprites(_spritesheetIds, true);\n }\n\n function addSprites(ids, overrideColors) {\n _spritesheetIds = utilArrayUniq(_spritesheetIds.concat(ids));\n\n var spritesheets = _defsSelection\n .selectAll('.spritesheet')\n .data(_spritesheetIds);\n\n spritesheets\n .enter()\n .append('g')\n .attr('class', function(d) { return 'spritesheet spritesheet-' + d; })\n .each(function(d) {\n var url = context.imagePath(d + '.svg');\n var node = d3_select(this).node();\n\n d3_svg(url)\n .then(function(svg) {\n node.appendChild(\n d3_select(svg.documentElement).attr('id', 'ideditor-' + d).node()\n );\n if (overrideColors && d !== 'iD-sprite') { // allow icon colors to be overridden..\n d3_select(node).selectAll('path')\n .attr('fill', 'currentColor');\n }\n })\n .catch(function() {\n /* ignore */\n });\n });\n\n spritesheets\n .exit()\n .remove();\n }\n\n drawDefs.addSprites = addSprites;\n\n return drawDefs;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { svgPointTransform } from './helpers';\nimport { geoMetersToLat } from '../geo';\n\n\nexport function svgGeolocate(projection) {\n var layer = d3_select(null);\n var _position;\n\n\n function init() {\n if (svgGeolocate.initialized) return; // run once\n svgGeolocate.enabled = false;\n svgGeolocate.initialized = true;\n }\n\n function showLayer() {\n layer.style('display', 'block');\n }\n\n\n function hideLayer() {\n layer\n .transition()\n .duration(250)\n .style('opacity', 0);\n }\n\n function layerOn() {\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1);\n\n }\n\n function layerOff() {\n layer.style('display', 'none');\n }\n\n function transform(d) {\n return svgPointTransform(projection)(d);\n }\n\n function accuracy(accuracy, loc) { // converts accuracy to pixels...\n var degreesRadius = geoMetersToLat(accuracy),\n tangentLoc = [loc[0], loc[1] + degreesRadius],\n projectedTangent = projection(tangentLoc),\n projectedLoc = projection([loc[0], loc[1]]);\n\n // southern most point will have higher pixel value...\n return Math.round(projectedLoc[1] - projectedTangent[1]).toString();\n }\n\n function update() {\n var geolocation = { loc: [_position.coords.longitude, _position.coords.latitude] };\n\n var groups = layer.selectAll('.geolocations').selectAll('.geolocation')\n .data([geolocation]);\n\n groups.exit()\n .remove();\n\n var pointsEnter = groups.enter()\n .append('g')\n .attr('class', 'geolocation');\n\n pointsEnter\n .append('circle')\n .attr('class', 'geolocate-radius')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('fill', 'rgb(15,128,225)')\n .attr('fill-opacity', '0.3')\n .attr('r', '0');\n\n pointsEnter\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('fill', 'rgb(15,128,225)')\n .attr('stroke', 'white')\n .attr('stroke-width', '1.5')\n .attr('r', '6');\n\n groups.merge(pointsEnter)\n .attr('transform', transform);\n\n layer.select('.geolocate-radius').attr('r', accuracy(_position.coords.accuracy, geolocation.loc));\n }\n\n function drawLocation(selection) {\n var enabled = svgGeolocate.enabled;\n\n layer = selection.selectAll('.layer-geolocate')\n .data([0]);\n\n layer.exit()\n .remove();\n\n var layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-geolocate')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'geolocations');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n update();\n } else {\n layerOff();\n }\n }\n\n drawLocation.enabled = function (position, enabled) {\n if (!arguments.length) return svgGeolocate.enabled;\n _position = position;\n svgGeolocate.enabled = enabled;\n if (svgGeolocate.enabled) {\n showLayer();\n layerOn();\n } else {\n hideLayer();\n }\n return this;\n };\n\n init();\n return drawLocation;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { geoPath as d3_geoPath } from 'd3-geo';\nimport RBush from 'rbush';\nimport { localizer } from '../core/localizer';\n\nimport {\n geoExtent, geoPolygonIntersectsPolygon, geoPathLength,\n geoScaleToZoom, geoVecInterp, geoVecLength\n} from '../geo';\nimport { presetManager } from '../presets';\nimport { osmEntity, osmIsInterestingTag } from '../osm';\nimport { utilDetect } from '../util/detect';\nimport { utilArrayDifference, utilArrayUniq, utilDisplayName, utilDisplayNameForPath, utilEntitySelector } from '../util';\n\n\n\nexport function svgLabels(projection, context) {\n var path = d3_geoPath(projection);\n var detected = utilDetect();\n var baselineHack = detected.browser.toLowerCase() === 'safari';\n\n var _rdrawn = new RBush();\n var _rskipped = new RBush();\n var _entitybboxes = {};\n\n // Listed from highest to lowest priority\n const labelStack = [\n ['line', 'aeroway', '*', 12],\n ['line', 'highway', 'motorway', 12],\n ['line', 'highway', 'trunk', 12],\n ['line', 'highway', 'primary', 12],\n ['line', 'highway', 'secondary', 12],\n ['line', 'highway', 'tertiary', 12],\n ['line', 'highway', '*', 12],\n ['line', 'railway', '*', 12],\n ['line', 'waterway', '*', 12],\n ['area', 'aeroway', '*', 12],\n ['area', 'amenity', '*', 12],\n ['area', 'building', '*', 12],\n ['area', 'historic', '*', 12],\n ['area', 'leisure', '*', 12],\n ['area', 'man_made', '*', 12],\n ['area', 'natural', '*', 12],\n ['area', 'shop', '*', 12],\n ['area', 'craft', '*', 12],\n ['area', 'tourism', '*', 12],\n ['area', 'camp_site', '*', 12],\n ['point', 'aeroway', '*', 10],\n ['point', 'amenity', '*', 10],\n ['point', 'building', '*', 10],\n ['point', 'historic', '*', 10],\n ['point', 'leisure', '*', 10],\n ['point', 'man_made', '*', 10],\n ['point', 'natural', '*', 10],\n ['point', 'shop', '*', 10],\n ['point', 'tourism', '*', 10],\n ['point', 'camp_site', '*', 10],\n ['*', 'alt_name', '*', 12],\n ['*', 'official_name', '*', 12],\n ['*', 'loc_name', '*', 12],\n ['*', 'loc_ref', '*', 12],\n ['*', 'unsigned_ref', '*', 12],\n ['*', 'seamark:name', '*', 12],\n ['*', 'sector:name', '*', 12],\n ['*', 'lock_name', '*', 12],\n ['*', 'distance', '*', 12],\n ['*', 'railway:position', '*', 12],\n ['line', 'ref', '*', 12],\n ['area', 'ref', '*', 12],\n ['point', 'ref', '*', 10],\n ['line', 'name', '*', 12],\n ['area', 'name', '*', 12],\n ['point', 'name', '*', 10],\n ['point', 'addr:housenumber', '*', 10],\n ['point', 'addr:housename', '*', 10]\n ];\n\n\n function shouldSkipIcon(preset) {\n var noIcons = ['building', 'landuse', 'natural'];\n return noIcons.some(function(s) {\n return preset.id.indexOf(s) >= 0;\n });\n }\n\n\n function drawLinePaths(selection, labels, filter, classes) {\n var paths = selection.selectAll('path:not(.debug)')\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n paths.exit()\n .remove();\n\n // enter/update\n paths.enter()\n .append('path')\n .style('stroke-width', d => d.position['font-size'])\n .attr('id', d => 'ideditor-labelpath-' + d.entity.id)\n .attr('class', classes)\n .merge(paths)\n .attr('d', d => d.position.lineString);\n }\n\n\n function drawLineLabels(selection, labels, filter, classes) {\n var texts = selection.selectAll('text.' + classes)\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n texts.exit()\n .remove();\n\n // enter\n texts.enter()\n .append('text')\n .attr('class', d => classes + ' ' + d.position.classes + ' ' + d.entity.id)\n .attr('dy', baselineHack ? '0.35em' : null)\n .append('textPath')\n .attr('class', 'textpath');\n\n // update\n selection.selectAll('text.' + classes).selectAll('.textpath')\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity))\n .attr('startOffset', '50%')\n .attr('xlink:href', function(d) { return '#ideditor-labelpath-' + d.entity.id; })\n .text(d => d.name);\n }\n\n\n function drawPointLabels(selection, labels, filter, classes) {\n if (classes.includes('pointlabel-halo')) {\n labels = labels.filter(d => !d.position.isAddr);\n }\n var texts = selection.selectAll('text.' + classes)\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n texts.exit()\n .remove();\n\n // enter/update\n texts.enter()\n .append('text')\n .attr('class', d => classes + ' ' + d.position.classes + ' ' + d.entity.id)\n .style('text-anchor', d => d.position.textAnchor)\n .text(d => d.name)\n .merge(texts)\n .attr('x', d => d.position.x)\n .attr('y', d => d.position.y);\n }\n\n\n function drawAreaLabels(selection, labels, filter, classes) {\n labels = labels.filter(hasText);\n drawPointLabels(selection, labels, filter, classes);\n\n function hasText(d) {\n return d.position.hasOwnProperty('x') && d.position.hasOwnProperty('y');\n }\n }\n\n\n function drawAreaIcons(selection, labels, filter, classes) {\n var icons = selection.selectAll('use.' + classes)\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n icons.exit()\n .remove();\n\n // enter/update\n icons.enter()\n .append('use')\n .attr('class', 'icon ' + classes)\n .attr('width', '17px')\n .attr('height', '17px')\n .merge(icons)\n .attr('transform', d => d.position.transform)\n .attr('xlink:href', function(d) {\n var preset = presetManager.match(d.entity, context.graph());\n var picon = preset && preset.icon;\n return picon ? '#' + picon : '';\n });\n }\n\n\n function drawCollisionBoxes(selection, rtree, which) {\n var classes = 'debug ' + which + ' ' + (which === 'debug-skipped' ? 'orange' : 'yellow');\n\n var gj = [];\n if (context.getDebug('collision')) {\n gj = rtree.all().map(function(d) {\n return { type: 'Polygon', coordinates: [[\n [d.minX, d.minY],\n [d.maxX, d.minY],\n [d.maxX, d.maxY],\n [d.minX, d.maxY],\n [d.minX, d.minY]\n ]]};\n });\n }\n\n var boxes = selection.selectAll('.' + which)\n .data(gj);\n\n // exit\n boxes.exit()\n .remove();\n\n // enter/update\n boxes.enter()\n .append('path')\n .attr('class', classes)\n .merge(boxes)\n .attr('d', d3_geoPath());\n }\n\n\n function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) {\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n\n var labelable = [];\n var renderNodeAs = {};\n var i, j, k, entity, geometry;\n\n for (i = 0; i < labelStack.length; i++) {\n labelable.push([]);\n }\n\n if (fullRedraw) {\n _rdrawn.clear();\n _rskipped.clear();\n _entitybboxes = {};\n\n } else {\n for (i = 0; i < entities.length; i++) {\n entity = entities[i];\n var toRemove = []\n .concat(_entitybboxes[entity.id] || [])\n .concat(_entitybboxes[entity.id + 'I'] || []);\n\n for (j = 0; j < toRemove.length; j++) {\n _rdrawn.remove(toRemove[j]);\n _rskipped.remove(toRemove[j]);\n }\n }\n }\n\n // Loop through all the entities to do some preprocessing\n for (i = 0; i < entities.length; i++) {\n entity = entities[i];\n geometry = entity.geometry(graph);\n\n // Insert collision boxes around interesting points/vertices\n if (geometry === 'point' || (geometry === 'vertex' && isInterestingVertex(entity))) {\n const isAddr = isAddressPoint(entity.tags);\n var hasDirections = entity.directions(graph, projection).length;\n var markerPadding = 0;\n\n if (wireframe) {\n renderNodeAs[entity.id] = { geometry: 'vertex', isAddr };\n } else if (geometry === 'vertex') {\n renderNodeAs[entity.id] = { geometry: 'vertex', isAddr };\n } else if (zoom >= 18 && hasDirections) {\n renderNodeAs[entity.id] = { geometry: 'vertex', isAddr };\n } else {\n renderNodeAs[entity.id] = { geometry: 'point', isAddr };\n markerPadding = 20; // extra y for marker height\n }\n\n if (isAddr) {\n undoInsert(entity.id + 'P');\n } else {\n var coord = projection(entity.loc);\n var nodePadding = 10;\n var bbox = {\n minX: coord[0] - nodePadding,\n minY: coord[1] - nodePadding - markerPadding,\n maxX: coord[0] + nodePadding,\n maxY: coord[1] + nodePadding\n };\n doInsert(bbox, entity.id + 'P');\n }\n }\n\n // From here on, treat vertices like points\n if (geometry === 'vertex') {\n geometry = 'point';\n }\n\n // Determine which entities are label-able\n var preset = geometry === 'area' && presetManager.match(entity, graph);\n var icon = preset && !shouldSkipIcon(preset) && preset.icon;\n\n if (!icon && !utilDisplayName(entity, { isMapLabel: true })) continue;\n\n for (k = 0; k < labelStack.length; k++) {\n var matchGeom = labelStack[k][0];\n var matchKey = labelStack[k][1];\n var matchVal = labelStack[k][2];\n var hasVal = entity.tags[matchKey];\n\n if ((matchGeom === '*' || geometry === matchGeom) && hasVal && (matchVal === '*' || matchVal === hasVal)) {\n labelable[k].push(entity);\n break;\n }\n }\n }\n\n var labelled = {\n point: [],\n line: [],\n area: []\n };\n\n // Try and find a valid label for labellable entities\n for (k = 0; k < labelable.length; k++) {\n var fontSize = labelStack[k][3];\n\n for (i = 0; i < labelable[k].length; i++) {\n entity = labelable[k][i];\n geometry = entity.geometry(graph);\n\n let name = geometry === 'line'\n ? utilDisplayNameForPath(entity)\n : utilDisplayName(entity, { isMapLabel: true });\n var width = name && textWidth(name, fontSize, selection.select('g.layer-osm.labels').node());\n var p = null;\n\n if (geometry === 'point' || geometry === 'vertex') {\n // no point or vertex labels in wireframe mode\n // no vertex labels at low zooms (vertices have no icons)\n if (wireframe) continue;\n var renderAs = renderNodeAs[entity.id];\n if (renderAs.geometry === 'vertex' && zoom < 17) continue;\n while (renderAs.isAddr && width > 36) {\n name = `${name.substring(0, name.replace(/\u2026$/, '').length - 1)}\u2026`;\n width = textWidth(name, fontSize, selection.select('g.layer-osm.labels').node());\n }\n\n p = getPointLabel(entity, width, fontSize, renderAs);\n } else if (geometry === 'line') {\n p = getLineLabel(entity, width, fontSize);\n\n } else if (geometry === 'area') {\n p = getAreaLabel(entity, width, fontSize);\n }\n\n if (p) {\n if (geometry === 'vertex') { geometry = 'point'; } // treat vertex like point\n p.classes = geometry + ' tag-' + labelStack[k][1];\n labelled[geometry].push({\n entity,\n name,\n position: p\n });\n }\n }\n }\n\n\n function isInterestingVertex(entity) {\n var selectedIDs = context.selectedIDs();\n\n return entity.hasInterestingTags() ||\n entity.isEndpoint(graph) ||\n entity.isConnected(graph) ||\n selectedIDs.indexOf(entity.id) !== -1 ||\n graph.parentWays(entity).some(function(parent) {\n return selectedIDs.indexOf(parent.id) !== -1;\n });\n }\n\n\n function getPointLabel(entity, width, height, style) {\n var y = (style.geometry === 'point' ? -12 : 0);\n var pointOffsets = {\n ltr: [15, y, 'start'],\n rtl: [-15, y, 'end']\n };\n const isAddrMarker = style.isAddr && style.geometry !== 'vertex';\n\n var textDirection = localizer.textDirection();\n\n var coord = projection(entity.loc);\n var textPadding = 2;\n var offset = pointOffsets[textDirection];\n if (isAddrMarker) offset = [0, 1, 'middle'];\n var p = {\n height: height,\n width: width,\n x: coord[0] + offset[0],\n y: coord[1] + offset[1],\n textAnchor: offset[2]\n };\n\n // insert a collision box for the text label..\n let bbox;\n if (isAddrMarker) {\n bbox = {\n minX: p.x - (width / 2) - textPadding,\n minY: p.y - (height / 2) - textPadding,\n maxX: p.x + (width / 2) + textPadding,\n maxY: p.y + (height / 2) + textPadding\n };\n } else if (textDirection === 'rtl') {\n bbox = {\n minX: p.x - width - textPadding,\n minY: p.y - (height / 2) - textPadding,\n maxX: p.x + textPadding,\n maxY: p.y + (height / 2) + textPadding\n };\n } else {\n bbox = {\n minX: p.x - textPadding,\n minY: p.y - (height / 2) - textPadding,\n maxX: p.x + width + textPadding,\n maxY: p.y + (height / 2) + textPadding\n };\n }\n\n if (tryInsert([bbox], entity.id, true)) {\n return p;\n }\n }\n\n\n function getLineLabel(entity, width, height) {\n var viewport = geoExtent(context.projection.clipExtent()).polygon();\n var points = graph.childNodes(entity)\n .map(function(node) { return projection(node.loc); });\n var length = geoPathLength(points);\n\n if (length < width + 20) return;\n\n // % along the line to attempt to place the label\n var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70,\n 25, 75, 20, 80, 15, 95, 10, 90, 5, 95];\n var padding = 3;\n\n for (var i = 0; i < lineOffsets.length; i++) {\n var offset = lineOffsets[i];\n var middle = offset / 100 * length;\n var start = middle - width / 2;\n\n if (start < 0 || start + width > length) continue;\n\n // generate subpath and ignore paths that are invalid or don't cross viewport.\n var sub = subpath(points, start, start + width);\n if (!sub || !geoPolygonIntersectsPolygon(viewport, sub, true)) {\n continue;\n }\n\n var isReverse = reverse(sub);\n if (isReverse) {\n sub = sub.reverse();\n }\n\n var bboxes = [];\n var boxsize = (height + 2) / 2;\n\n for (var j = 0; j < sub.length - 1; j++) {\n var a = sub[j];\n var b = sub[j + 1];\n\n // split up the text into small collision boxes\n var num = Math.max(1, Math.floor(geoVecLength(a, b) / boxsize / 2));\n\n for (var box = 0; box < num; box++) {\n var p = geoVecInterp(a, b, box / num);\n var x0 = p[0] - boxsize - padding;\n var y0 = p[1] - boxsize - padding;\n var x1 = p[0] + boxsize + padding;\n var y1 = p[1] + boxsize + padding;\n\n bboxes.push({\n minX: Math.min(x0, x1),\n minY: Math.min(y0, y1),\n maxX: Math.max(x0, x1),\n maxY: Math.max(y0, y1)\n });\n }\n }\n\n if (tryInsert(bboxes, entity.id, false)) { // accept this one\n return {\n 'font-size': height + 2,\n lineString: lineString(sub),\n startOffset: offset + '%'\n };\n }\n }\n\n function reverse(p) {\n var angle = Math.atan2(p[1][1] - p[0][1], p[1][0] - p[0][0]);\n return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2);\n }\n\n function lineString(points) {\n return 'M' + points.join('L');\n }\n\n function subpath(points, from, to) {\n var sofar = 0;\n var start, end, i0, i1;\n\n for (var i = 0; i < points.length - 1; i++) {\n var a = points[i];\n var b = points[i + 1];\n var current = geoVecLength(a, b);\n var portion;\n if (!start && sofar + current >= from) {\n portion = (from - sofar) / current;\n start = [\n a[0] + portion * (b[0] - a[0]),\n a[1] + portion * (b[1] - a[1])\n ];\n i0 = i + 1;\n }\n if (!end && sofar + current >= to) {\n portion = (to - sofar) / current;\n end = [\n a[0] + portion * (b[0] - a[0]),\n a[1] + portion * (b[1] - a[1])\n ];\n i1 = i + 1;\n }\n sofar += current;\n }\n\n var result = points.slice(i0, i1);\n result.unshift(start);\n result.push(end);\n return result;\n }\n }\n\n\n function getAreaLabel(entity, width, height) {\n var centroid = path.centroid(entity.asGeoJSON(graph));\n var extent = entity.extent(graph);\n var areaWidth = projection(extent[1])[0] - projection(extent[0])[0];\n\n if (isNaN(centroid[0]) || areaWidth < 20) return;\n\n var preset = presetManager.match(entity, context.graph());\n var picon = preset && preset.icon;\n var iconSize = 17;\n var padding = 2;\n var p = {};\n\n if (picon && !shouldSkipIcon(preset)) { // icon and label..\n if (addIcon()) {\n addLabel(iconSize + padding);\n return p;\n }\n } else { // label only..\n if (addLabel(0)) {\n return p;\n }\n }\n\n\n function addIcon() {\n var iconX = centroid[0] - (iconSize / 2);\n var iconY = centroid[1] - (iconSize / 2);\n var bbox = {\n minX: iconX,\n minY: iconY,\n maxX: iconX + iconSize,\n maxY: iconY + iconSize\n };\n\n if (tryInsert([bbox], entity.id + 'I', true)) {\n p.transform = 'translate(' + iconX + ',' + iconY + ')';\n return true;\n }\n return false;\n }\n\n function addLabel(yOffset) {\n if (width && areaWidth >= width + 20) {\n var labelX = centroid[0];\n var labelY = centroid[1] + yOffset;\n var bbox = {\n minX: labelX - (width / 2) - padding,\n minY: labelY - (height / 2) - padding,\n maxX: labelX + (width / 2) + padding,\n maxY: labelY + (height / 2) + padding\n };\n\n if (tryInsert([bbox], entity.id, true)) {\n p.x = labelX;\n p.y = labelY;\n p.textAnchor = 'middle';\n p.height = height;\n return true;\n }\n }\n return false;\n }\n }\n\n\n // force insert a singular bounding box\n // singular box only, no array, id better be unique\n function doInsert(bbox, id) {\n bbox.id = id;\n\n var oldbox = _entitybboxes[id];\n if (oldbox) {\n _rdrawn.remove(oldbox);\n }\n _entitybboxes[id] = bbox;\n _rdrawn.insert(bbox);\n }\n\n function undoInsert(id) {\n var oldbox = _entitybboxes[id];\n if (oldbox) {\n _rdrawn.remove(oldbox);\n }\n delete _entitybboxes[id];\n }\n\n function tryInsert(bboxes, id, saveSkipped) {\n var skipped = false;\n\n for (var i = 0; i < bboxes.length; i++) {\n var bbox = bboxes[i];\n bbox.id = id;\n\n // Check that label is visible\n if (bbox.minX < 0 || bbox.minY < 0 || bbox.maxX > dimensions[0] || bbox.maxY > dimensions[1]) {\n skipped = true;\n break;\n }\n if (_rdrawn.collides(bbox)) {\n skipped = true;\n break;\n }\n }\n\n _entitybboxes[id] = bboxes;\n\n if (skipped) {\n if (saveSkipped) {\n _rskipped.load(bboxes);\n }\n } else {\n _rdrawn.load(bboxes);\n }\n\n return !skipped;\n }\n\n\n var layer = selection.selectAll('.layer-osm.labels');\n layer.selectAll('.labels-group')\n .data(['halo', 'label', 'debug'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'labels-group ' + d; });\n\n var halo = layer.selectAll('.labels-group.halo');\n var label = layer.selectAll('.labels-group.label');\n var debug = layer.selectAll('.labels-group.debug');\n\n // points\n drawPointLabels(label, labelled.point, filter, 'pointlabel');\n drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo');\n\n // lines\n drawLinePaths(layer, labelled.line, filter, '');\n drawLineLabels(label, labelled.line, filter, 'linelabel');\n drawLineLabels(halo, labelled.line, filter, 'linelabel-halo');\n\n // areas\n drawAreaLabels(label, labelled.area, filter, 'arealabel');\n drawAreaLabels(halo, labelled.area, filter, 'arealabel-halo');\n drawAreaIcons(label, labelled.area, filter, 'areaicon');\n drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo');\n\n // debug\n drawCollisionBoxes(debug, _rskipped, 'debug-skipped');\n drawCollisionBoxes(debug, _rdrawn, 'debug-drawn');\n\n layer.call(filterLabels);\n }\n\n\n function filterLabels(selection) {\n var drawLayer = selection.selectAll('.layer-osm.labels');\n var layers = drawLayer.selectAll('.labels-group.halo, .labels-group.label');\n\n layers.selectAll('.nolabel')\n .classed('nolabel', false);\n\n const graph = context.graph();\n const mouse = context.map().mouse();\n let bbox;\n let hideIds = [];\n\n // hide labels near the mouse\n if (mouse && context.mode().id !== 'browse') {\n const pad = 20;\n bbox = { minX: mouse[0] - pad, minY: mouse[1] - pad, maxX: mouse[0] + pad, maxY: mouse[1] + pad };\n const nearMouse = _rdrawn.search(bbox)\n .map(entity => entity.id)\n .filter(id =>\n context.mode().id !== 'select' ||\n // in select mode: hide labels of currently selected line(s)\n // to still allow accessing midpoints\n // https://github.com/openstreetmap/iD/issues/11220\n context.mode().selectedIDs().includes(id) && graph.hasEntity(id)?.geometry(graph) === 'line');\n hideIds.push.apply(hideIds, nearMouse);\n hideIds = utilArrayUniq(hideIds);\n }\n\n // don't hide label of currently selected entity while in e.g. drag mode\n const selected = (context.mode()?.selectedIDs?.() || [])\n .filter(id => graph.hasEntity(id)?.geometry(graph) !== 'line');\n hideIds = utilArrayDifference(hideIds, selected);\n\n layers.selectAll(utilEntitySelector(hideIds))\n .classed('nolabel', true);\n\n\n // draw the mouse bbox if debugging is on..\n var debug = selection.selectAll('.labels-group.debug');\n var gj = [];\n if (context.getDebug('collision')) {\n gj = bbox ? [{\n type: 'Polygon',\n coordinates: [[\n [bbox.minX, bbox.minY],\n [bbox.maxX, bbox.minY],\n [bbox.maxX, bbox.maxY],\n [bbox.minX, bbox.maxY],\n [bbox.minX, bbox.minY]\n ]]\n }] : [];\n }\n\n var box = debug.selectAll('.debug-mouse')\n .data(gj);\n\n // exit\n box.exit()\n .remove();\n\n // enter/update\n box.enter()\n .append('path')\n .attr('class', 'debug debug-mouse yellow')\n .merge(box)\n .attr('d', d3_geoPath());\n }\n\n\n var throttleFilterLabels = throttle(filterLabels, 100);\n\n\n drawLabels.observe = function(selection) {\n var listener = function() { throttleFilterLabels(selection); };\n selection.on('mousemove.hidelabels', listener);\n context.on('enter.hidelabels', listener);\n };\n\n\n drawLabels.off = function(selection) {\n throttleFilterLabels.cancel();\n selection.on('mousemove.hidelabels', null);\n context.on('enter.hidelabels', null);\n };\n\n\n return drawLabels;\n}\n\n\nconst _textWidthCache = {};\nexport function textWidth(text, size, container) {\n _textWidthCache[size] ||= {};\n let c = _textWidthCache[size];\n\n if (c[text]) {\n return c[text];\n }\n const elem = document.createElementNS('http://www.w3.org/2000/svg', 'text');\n elem.style.fontSize = `${size}px`;\n elem.style.fontWeight = 'bold';\n elem.textContent = text;\n container.appendChild(elem);\n c[text] = elem.getComputedTextLength();\n elem.remove();\n return c[text];\n}\n\n\nconst nonPrimaryKeys = new Set([\n 'building:flats',\n 'check_date',\n 'fixme',\n 'layer',\n 'level',\n 'level:ref',\n 'note'\n]);\nconst nonPrimaryKeysRegex = /^(ref|survey|note|([^:]+:|old_|alt_)addr):/;\nexport function isAddressPoint(tags) {\n const keys = Object.keys(tags).filter(key =>\n osmIsInterestingTag(key) &&\n !nonPrimaryKeys.has(key) &&\n !nonPrimaryKeysRegex.test(key)\n );\n return keys.length > 0 && keys.every(key =>\n key.startsWith('addr:')\n );\n}\n", "var e=\"undefined\"!=typeof self?self:global;const t=\"undefined\"!=typeof navigator,i=t&&\"undefined\"==typeof HTMLImageElement,n=!(\"undefined\"==typeof global||\"undefined\"==typeof process||!process.versions||!process.versions.node),s=e.Buffer,r=e.BigInt,a=!!s,o=e=>e;function l(e,t=o){if(n)try{return\"function\"==typeof require?Promise.resolve(t(require(e))):import(/* webpackIgnore: true */ e).then(t)}catch(t){console.warn(`Couldn't load ${e}`)}}let h=e.fetch;const u=e=>h=e;if(!e.fetch){const e=l(\"http\",(e=>e)),t=l(\"https\",(e=>e)),i=(n,{headers:s}={})=>new Promise((async(r,a)=>{let{port:o,hostname:l,pathname:h,protocol:u,search:c}=new URL(n);const f={method:\"GET\",hostname:l,path:encodeURI(h)+c,headers:s};\"\"!==o&&(f.port=Number(o));const d=(\"https:\"===u?await t:await e).request(f,(e=>{if(301===e.statusCode||302===e.statusCode){let t=new URL(e.headers.location,n).toString();return i(t,{headers:s}).then(r).catch(a)}r({status:e.statusCode,arrayBuffer:()=>new Promise((t=>{let i=[];e.on(\"data\",(e=>i.push(e))),e.on(\"end\",(()=>t(Buffer.concat(i))))}))})}));d.on(\"error\",a),d.end()}));u(i)}function c(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}const f=e=>p(e)?void 0:e,d=e=>void 0!==e;function p(e){return void 0===e||(e instanceof Map?0===e.size:0===Object.values(e).filter(d).length)}function g(e){let t=new Error(e);throw delete t.stack,t}function m(e){return\"\"===(e=function(e){for(;e.endsWith(\"\\0\");)e=e.slice(0,-1);return e}(e).trim())?void 0:e}function S(e){let t=function(e){let t=0;return e.ifd0.enabled&&(t+=1024),e.exif.enabled&&(t+=2048),e.makerNote&&(t+=2048),e.userComment&&(t+=1024),e.gps.enabled&&(t+=512),e.interop.enabled&&(t+=100),e.ifd1.enabled&&(t+=1024),t+2048}(e);return e.jfif.enabled&&(t+=50),e.xmp.enabled&&(t+=2e4),e.iptc.enabled&&(t+=14e3),e.icc.enabled&&(t+=6e3),t}const C=e=>String.fromCharCode.apply(null,e),y=\"undefined\"!=typeof TextDecoder?new TextDecoder(\"utf-8\"):void 0;function b(e){return y?y.decode(e):a?Buffer.from(e).toString(\"utf8\"):decodeURIComponent(escape(C(e)))}class I{static from(e,t){return e instanceof this&&e.le===t?e:new I(e,void 0,void 0,t)}constructor(e,t=0,i,n){if(\"boolean\"==typeof n&&(this.le=n),Array.isArray(e)&&(e=new Uint8Array(e)),0===e)this.byteOffset=0,this.byteLength=0;else if(e instanceof ArrayBuffer){void 0===i&&(i=e.byteLength-t);let n=new DataView(e,t,i);this._swapDataView(n)}else if(e instanceof Uint8Array||e instanceof DataView||e instanceof I){void 0===i&&(i=e.byteLength-t),(t+=e.byteOffset)+i>e.byteOffset+e.byteLength&&g(\"Creating view outside of available memory in ArrayBuffer\");let n=new DataView(e.buffer,t,i);this._swapDataView(n)}else if(\"number\"==typeof e){let t=new DataView(new ArrayBuffer(e));this._swapDataView(t)}else g(\"Invalid input argument for BufferView: \"+e)}_swapArrayBuffer(e){this._swapDataView(new DataView(e))}_swapBuffer(e){this._swapDataView(new DataView(e.buffer,e.byteOffset,e.byteLength))}_swapDataView(e){this.dataView=e,this.buffer=e.buffer,this.byteOffset=e.byteOffset,this.byteLength=e.byteLength}_lengthToEnd(e){return this.byteLength-e}set(e,t,i=I){return e instanceof DataView||e instanceof I?e=new Uint8Array(e.buffer,e.byteOffset,e.byteLength):e instanceof ArrayBuffer&&(e=new Uint8Array(e)),e instanceof Uint8Array||g(\"BufferView.set(): Invalid data argument.\"),this.toUint8().set(e,t),new i(this,t,e.byteLength)}subarray(e,t){return t=t||this._lengthToEnd(e),new I(this,e,t)}toUint8(){return new Uint8Array(this.buffer,this.byteOffset,this.byteLength)}getUint8Array(e,t){return new Uint8Array(this.buffer,this.byteOffset+e,t)}getString(e=0,t=this.byteLength){return b(this.getUint8Array(e,t))}getLatin1String(e=0,t=this.byteLength){let i=this.getUint8Array(e,t);return C(i)}getUnicodeString(e=0,t=this.byteLength){const i=[];for(let n=0;n1e4?v(e,i,\"base64\"):n&&e.includes(\"://\")?x(e,i,\"url\",M):n?v(e,i,\"fs\"):t?x(e,i,\"url\",M):void g(\"Invalid input argument\");var s}async function x(e,t,i,n){return A.has(i)?v(e,t,i):n?async function(e,t){let i=await t(e);return new I(i)}(e,n):void g(`Parser ${i} is not loaded`)}async function v(e,t,i){let n=new(A.get(i))(e,t);return await n.read(),n}const M=e=>h(e).then((e=>e.arrayBuffer())),R=e=>new Promise(((t,i)=>{let n=new FileReader;n.onloadend=()=>t(n.result||new ArrayBuffer),n.onerror=i,n.readAsArrayBuffer(e)}));class L extends Map{get tagKeys(){return this.allKeys||(this.allKeys=Array.from(this.keys())),this.allKeys}get tagValues(){return this.allValues||(this.allValues=Array.from(this.values())),this.allValues}}function U(e,t,i){let n=new L;for(let[e,t]of i)n.set(e,t);if(Array.isArray(t))for(let i of t)e.set(i,n);else e.set(t,n);return n}function F(e,t,i){let n,s=e.get(t);for(n of i)s.set(n[0],n[1])}const E=new Map,B=new Map,N=new Map,G=[\"chunked\",\"firstChunkSize\",\"firstChunkSizeNode\",\"firstChunkSizeBrowser\",\"chunkSize\",\"chunkLimit\"],V=[\"jfif\",\"xmp\",\"icc\",\"iptc\",\"ihdr\"],z=[\"tiff\",...V],H=[\"ifd0\",\"ifd1\",\"exif\",\"gps\",\"interop\"],j=[...z,...H],W=[\"makerNote\",\"userComment\"],K=[\"translateKeys\",\"translateValues\",\"reviveValues\",\"multiSegment\"],X=[...K,\"sanitize\",\"mergeOutput\",\"silentErrors\"];class _{get translate(){return this.translateKeys||this.translateValues||this.reviveValues}}class Y extends _{get needed(){return this.enabled||this.deps.size>0}constructor(e,t,i,n){if(super(),c(this,\"enabled\",!1),c(this,\"skip\",new Set),c(this,\"pick\",new Set),c(this,\"deps\",new Set),c(this,\"translateKeys\",!1),c(this,\"translateValues\",!1),c(this,\"reviveValues\",!1),this.key=e,this.enabled=t,this.parse=this.enabled,this.applyInheritables(n),this.canBeFiltered=H.includes(e),this.canBeFiltered&&(this.dict=E.get(e)),void 0!==i)if(Array.isArray(i))this.parse=this.enabled=!0,this.canBeFiltered&&i.length>0&&this.translateTagSet(i,this.pick);else if(\"object\"==typeof i){if(this.enabled=!0,this.parse=!1!==i.parse,this.canBeFiltered){let{pick:e,skip:t}=i;e&&e.length>0&&this.translateTagSet(e,this.pick),t&&t.length>0&&this.translateTagSet(t,this.skip)}this.applyInheritables(i)}else!0===i||!1===i?this.parse=this.enabled=i:g(`Invalid options argument: ${i}`)}applyInheritables(e){let t,i;for(t of K)i=e[t],void 0!==i&&(this[t]=i)}translateTagSet(e,t){if(this.dict){let i,n,{tagKeys:s,tagValues:r}=this.dict;for(i of e)\"string\"==typeof i?(n=r.indexOf(i),-1===n&&(n=s.indexOf(Number(i))),-1!==n&&t.add(Number(s[n]))):t.add(i)}else for(let i of e)t.add(i)}finalizeFilters(){!this.enabled&&this.deps.size>0?(this.enabled=!0,ee(this.pick,this.deps)):this.enabled&&this.pick.size>0&&ee(this.pick,this.deps)}}var $={jfif:!1,tiff:!0,xmp:!1,icc:!1,iptc:!1,ifd0:!0,ifd1:!1,exif:!0,gps:!0,interop:!1,ihdr:void 0,makerNote:!1,userComment:!1,multiSegment:!1,skip:[],pick:[],translateKeys:!0,translateValues:!0,reviveValues:!0,sanitize:!0,mergeOutput:!0,silentErrors:!0,chunked:!0,firstChunkSize:void 0,firstChunkSizeNode:512,firstChunkSizeBrowser:65536,chunkSize:65536,chunkLimit:5},J=new Map;class q extends _{static useCached(e){let t=J.get(e);return void 0!==t||(t=new this(e),J.set(e,t)),t}constructor(e){super(),!0===e?this.setupFromTrue():void 0===e?this.setupFromUndefined():Array.isArray(e)?this.setupFromArray(e):\"object\"==typeof e?this.setupFromObject(e):g(`Invalid options argument ${e}`),void 0===this.firstChunkSize&&(this.firstChunkSize=t?this.firstChunkSizeBrowser:this.firstChunkSizeNode),this.mergeOutput&&(this.ifd1.enabled=!1),this.filterNestedSegmentTags(),this.traverseTiffDependencyTree(),this.checkLoadedPlugins()}setupFromUndefined(){let e;for(e of G)this[e]=$[e];for(e of X)this[e]=$[e];for(e of W)this[e]=$[e];for(e of j)this[e]=new Y(e,$[e],void 0,this)}setupFromTrue(){let e;for(e of G)this[e]=$[e];for(e of X)this[e]=$[e];for(e of W)this[e]=!0;for(e of j)this[e]=new Y(e,!0,void 0,this)}setupFromArray(e){let t;for(t of G)this[t]=$[t];for(t of X)this[t]=$[t];for(t of W)this[t]=$[t];for(t of j)this[t]=new Y(t,!1,void 0,this);this.setupGlobalFilters(e,void 0,H)}setupFromObject(e){let t;for(t of(H.ifd0=H.ifd0||H.image,H.ifd1=H.ifd1||H.thumbnail,Object.assign(this,e),G))this[t]=Z(e[t],$[t]);for(t of X)this[t]=Z(e[t],$[t]);for(t of W)this[t]=Z(e[t],$[t]);for(t of z)this[t]=new Y(t,$[t],e[t],this);for(t of H)this[t]=new Y(t,$[t],e[t],this.tiff);this.setupGlobalFilters(e.pick,e.skip,H,j),!0===e.tiff?this.batchEnableWithBool(H,!0):!1===e.tiff?this.batchEnableWithUserValue(H,e):Array.isArray(e.tiff)?this.setupGlobalFilters(e.tiff,void 0,H):\"object\"==typeof e.tiff&&this.setupGlobalFilters(e.tiff.pick,e.tiff.skip,H)}batchEnableWithBool(e,t){for(let i of e)this[i].enabled=t}batchEnableWithUserValue(e,t){for(let i of e){let e=t[i];this[i].enabled=!1!==e&&void 0!==e}}setupGlobalFilters(e,t,i,n=i){if(e&&e.length){for(let e of n)this[e].enabled=!1;let t=Q(e,i);for(let[e,i]of t)ee(this[e].pick,i),this[e].enabled=!0}else if(t&&t.length){let e=Q(t,i);for(let[t,i]of e)ee(this[t].skip,i)}}filterNestedSegmentTags(){let{ifd0:e,exif:t,xmp:i,iptc:n,icc:s}=this;this.makerNote?t.deps.add(37500):t.skip.add(37500),this.userComment?t.deps.add(37510):t.skip.add(37510),i.enabled||e.skip.add(700),n.enabled||e.skip.add(33723),s.enabled||e.skip.add(34675)}traverseTiffDependencyTree(){let{ifd0:e,exif:t,gps:i,interop:n}=this;n.needed&&(t.deps.add(40965),e.deps.add(40965)),t.needed&&e.deps.add(34665),i.needed&&e.deps.add(34853),this.tiff.enabled=H.some((e=>!0===this[e].enabled))||this.makerNote||this.userComment;for(let e of H)this[e].finalizeFilters()}get onlyTiff(){return!V.map((e=>this[e].enabled)).some((e=>!0===e))&&this.tiff.enabled}checkLoadedPlugins(){for(let e of z)this[e].enabled&&!T.has(e)&&P(\"segment parser\",e)}}function Q(e,t){let i,n,s,r,a=[];for(s of t){for(r of(i=E.get(s),n=[],i))(e.includes(r[0])||e.includes(r[1]))&&n.push(r[0]);n.length&&a.push([s,n])}return a}function Z(e,t){return void 0!==e?e:void 0!==t?t:void 0}function ee(e,t){for(let i of t)e.add(i)}c(q,\"default\",$);class te{constructor(e){c(this,\"parsers\",{}),c(this,\"output\",{}),c(this,\"errors\",[]),c(this,\"pushToErrors\",(e=>this.errors.push(e))),this.options=q.useCached(e)}async read(e){this.file=await D(e,this.options)}setup(){if(this.fileParser)return;let{file:e}=this,t=e.getUint16(0);for(let[i,n]of w)if(n.canHandle(e,t))return this.fileParser=new n(this.options,this.file,this.parsers),e[i]=!0;this.file.close&&this.file.close(),g(\"Unknown file format\")}async parse(){let{output:e,errors:t}=this;return this.setup(),this.options.silentErrors?(await this.executeParsers().catch(this.pushToErrors),t.push(...this.fileParser.errors)):await this.executeParsers(),this.file.close&&this.file.close(),this.options.silentErrors&&t.length>0&&(e.errors=t),f(e)}async executeParsers(){let{output:e}=this;await this.fileParser.parse();let t=Object.values(this.parsers).map((async t=>{let i=await t.parse();t.assignToOutput(e,i)}));this.options.silentErrors&&(t=t.map((e=>e.catch(this.pushToErrors)))),await Promise.all(t)}async extractThumbnail(){this.setup();let{options:e,file:t}=this,i=T.get(\"tiff\",e);var n;if(t.tiff?n={start:0,type:\"tiff\"}:t.jpeg&&(n=await this.fileParser.getOrFindSegment(\"tiff\")),void 0===n)return;let s=await this.fileParser.ensureSegmentChunk(n),r=this.parsers.tiff=new i(s,e,t),a=await r.extractThumbnail();return t.close&&t.close(),a}}async function ie(e,t){let i=new te(t);return await i.read(e),i.parse()}var ne=Object.freeze({__proto__:null,parse:ie,Exifr:te,fileParsers:w,segmentParsers:T,fileReaders:A,tagKeys:E,tagValues:B,tagRevivers:N,createDictionary:U,extendDictionary:F,fetchUrlAsArrayBuffer:M,readBlobAsArrayBuffer:R,chunkedProps:G,otherSegments:V,segments:z,tiffBlocks:H,segmentsAndBlocks:j,tiffExtractables:W,inheritables:K,allFormatters:X,Options:q});class se{constructor(e,t,i){c(this,\"errors\",[]),c(this,\"ensureSegmentChunk\",(async e=>{let t=e.start,i=e.size||65536;if(this.file.chunked)if(this.file.available(t,i))e.chunk=this.file.subarray(t,i);else try{e.chunk=await this.file.readChunk(t,i)}catch(t){g(`Couldn't read segment: ${JSON.stringify(e)}. ${t.message}`)}else this.file.byteLength>t+i?e.chunk=this.file.subarray(t,i):void 0===e.size?e.chunk=this.file.subarray(t):g(\"Segment unreachable: \"+JSON.stringify(e));return e.chunk})),this.extendOptions&&this.extendOptions(e),this.options=e,this.file=t,this.parsers=i}injectSegment(e,t){this.options[e].enabled&&this.createParser(e,t)}createParser(e,t){let i=new(T.get(e))(t,this.options,this.file);return this.parsers[e]=i}createParsers(e){for(let t of e){let{type:e,chunk:i}=t,n=this.options[e];if(n&&n.enabled){let t=this.parsers[e];t&&t.append||t||this.createParser(e,i)}}}async readSegments(e){let t=e.map(this.ensureSegmentChunk);await Promise.all(t)}}class re{static findPosition(e,t){let i=e.getUint16(t+2)+2,n=\"function\"==typeof this.headerLength?this.headerLength(e,t,i):this.headerLength,s=t+n,r=i-n;return{offset:t,length:i,headerLength:n,start:s,size:r,end:s+r}}static parse(e,t={}){return new this(e,new q({[this.type]:t}),e).parse()}normalizeInput(e){return e instanceof I?e:new I(e)}constructor(e,t={},i){c(this,\"errors\",[]),c(this,\"raw\",new Map),c(this,\"handleError\",(e=>{if(!this.options.silentErrors)throw e;this.errors.push(e.message)})),this.chunk=this.normalizeInput(e),this.file=i,this.type=this.constructor.type,this.globalOptions=this.options=t,this.localOptions=t[this.type],this.canTranslate=this.localOptions&&this.localOptions.translate}translate(){this.canTranslate&&(this.translated=this.translateBlock(this.raw,this.type))}get output(){return this.translated?this.translated:this.raw?Object.fromEntries(this.raw):void 0}translateBlock(e,t){let i=N.get(t),n=B.get(t),s=E.get(t),r=this.options[t],a=r.reviveValues&&!!i,o=r.translateValues&&!!n,l=r.translateKeys&&!!s,h={};for(let[t,r]of e)a&&i.has(t)?r=i.get(t)(r):o&&n.has(t)&&(r=this.translateValue(r,n.get(t))),l&&s.has(t)&&(t=s.get(t)||t),h[t]=r;return h}translateValue(e,t){return t[e]||t.DEFAULT||e}assignToOutput(e,t){this.assignObjectToOutput(e,this.constructor.type,t)}assignObjectToOutput(e,t,i){if(this.globalOptions.mergeOutput)return Object.assign(e,i);e[t]?Object.assign(e[t],i):e[t]=i}}c(re,\"headerLength\",4),c(re,\"type\",void 0),c(re,\"multiSegment\",!1),c(re,\"canHandle\",(()=>!1));function ae(e){return 192===e||194===e||196===e||219===e||221===e||218===e||254===e}function oe(e){return e>=224&&e<=239}function le(e,t,i){for(let[n,s]of T)if(s.canHandle(e,t,i))return n}class he extends se{constructor(...e){super(...e),c(this,\"appSegments\",[]),c(this,\"jpegSegments\",[]),c(this,\"unknownSegments\",[])}static canHandle(e,t){return 65496===t}async parse(){await this.findAppSegments(),await this.readSegments(this.appSegments),this.mergeMultiSegments(),this.createParsers(this.mergedAppSegments||this.appSegments)}setupSegmentFinderArgs(e){!0===e?(this.findAll=!0,this.wanted=new Set(T.keyList())):(e=void 0===e?T.keyList().filter((e=>this.options[e].enabled)):e.filter((e=>this.options[e].enabled&&T.has(e))),this.findAll=!1,this.remaining=new Set(e),this.wanted=new Set(e)),this.unfinishedMultiSegment=!1}async findAppSegments(e=0,t){this.setupSegmentFinderArgs(t);let{file:i,findAll:n,wanted:s,remaining:r}=this;if(!n&&this.file.chunked&&(n=Array.from(s).some((e=>{let t=T.get(e),i=this.options[e];return t.multiSegment&&i.multiSegment})),n&&await this.file.readWhole()),e=this.findAppSegmentsInRange(e,i.byteLength),!this.options.onlyTiff&&i.chunked){let t=!1;for(;r.size>0&&!t&&(i.canReadNextChunk||this.unfinishedMultiSegment);){let{nextChunkOffset:n}=i,s=this.appSegments.some((e=>!this.file.available(e.offset||e.start,e.length||e.size)));if(t=e>n&&!s?!await i.readNextChunk(e):!await i.readNextChunk(n),void 0===(e=this.findAppSegmentsInRange(e,i.byteLength)))return}}}findAppSegmentsInRange(e,t){t-=2;let i,n,s,r,a,o,{file:l,findAll:h,wanted:u,remaining:c,options:f}=this;for(;ee.multiSegment)))return;let e=function(e,t){let i,n,s,r=new Map;for(let a=0;a{let i=T.get(e,this.options);if(i.handleMultiSegments){return{type:e,chunk:i.handleMultiSegments(t)}}return t[0]}))}getSegment(e){return this.appSegments.find((t=>t.type===e))}async getOrFindSegment(e){let t=this.getSegment(e);return void 0===t&&(await this.findAppSegments(0,[e]),t=this.getSegment(e)),t}}c(he,\"type\",\"jpeg\"),w.set(\"jpeg\",he);const ue=[void 0,1,1,2,4,8,1,1,2,4,8,4,8,4];class ce extends re{parseHeader(){var e=this.chunk.getUint16();18761===e?this.le=!0:19789===e&&(this.le=!1),this.chunk.le=this.le,this.headerParsed=!0}parseTags(e,t,i=new Map){let{pick:n,skip:s}=this.options[t];n=new Set(n);let r=n.size>0,a=0===s.size,o=this.chunk.getUint16(e);e+=2;for(let l=0;l13)&&g(`Invalid TIFF value type. block: ${i.toUpperCase()}, tag: ${t.toString(16)}, type: ${s}, offset ${e}`),e>n.byteLength&&g(`Invalid TIFF value offset. block: ${i.toUpperCase()}, tag: ${t.toString(16)}, type: ${s}, offset ${e} is outside of chunk size ${n.byteLength}`),1===s)return n.getUint8Array(e,r);if(2===s)return m(n.getString(e,r));if(7===s)return n.getUint8Array(e,r);if(1===r)return this.parseTagValue(s,e);{let t=new(function(e){switch(e){case 1:return Uint8Array;case 3:return Uint16Array;case 4:return Uint32Array;case 5:return Array;case 6:return Int8Array;case 8:return Int16Array;case 9:return Int32Array;case 10:return Array;case 11:return Float32Array;case 12:return Float64Array;default:return Array}}(s))(r),i=a;for(let n=0;ne.byteLength&&g(`IFD0 offset points to outside of file.\\nthis.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e.byteLength}`),e.tiff&&await e.ensureChunk(this.ifd0Offset,S(this.options));let t=this.parseBlock(this.ifd0Offset,\"ifd0\");return 0!==t.size?(this.exifOffset=t.get(34665),this.interopOffset=t.get(40965),this.gpsOffset=t.get(34853),this.xmp=t.get(700),this.iptc=t.get(33723),this.icc=t.get(34675),this.options.sanitize&&(t.delete(34665),t.delete(40965),t.delete(34853),t.delete(700),t.delete(33723),t.delete(34675)),t):void 0}async parseExifBlock(){if(this.exif)return;if(this.ifd0||await this.parseIfd0Block(),void 0===this.exifOffset)return;this.file.tiff&&await this.file.ensureChunk(this.exifOffset,S(this.options));let e=this.parseBlock(this.exifOffset,\"exif\");return this.interopOffset||(this.interopOffset=e.get(40965)),this.makerNote=e.get(37500),this.userComment=e.get(37510),this.options.sanitize&&(e.delete(40965),e.delete(37500),e.delete(37510)),this.unpack(e,41728),this.unpack(e,41729),e}unpack(e,t){let i=e.get(t);i&&1===i.length&&e.set(t,i[0])}async parseGpsBlock(){if(this.gps)return;if(this.ifd0||await this.parseIfd0Block(),void 0===this.gpsOffset)return;let e=this.parseBlock(this.gpsOffset,\"gps\");return e&&e.has(2)&&e.has(4)&&(e.set(\"latitude\",de(...e.get(2),e.get(1))),e.set(\"longitude\",de(...e.get(4),e.get(3)))),e}async parseInteropBlock(){if(!this.interop&&(this.ifd0||await this.parseIfd0Block(),void 0!==this.interopOffset||this.exif||await this.parseExifBlock(),void 0!==this.interopOffset))return this.parseBlock(this.interopOffset,\"interop\")}async parseThumbnailBlock(e=!1){if(!this.ifd1&&!this.ifd1Parsed&&(!this.options.mergeOutput||e))return this.findIfd1Offset(),this.ifd1Offset>0&&(this.parseBlock(this.ifd1Offset,\"ifd1\"),this.ifd1Parsed=!0),this.ifd1}async extractThumbnail(){if(this.headerParsed||this.parseHeader(),this.ifd1Parsed||await this.parseThumbnailBlock(!0),void 0===this.ifd1)return;let e=this.ifd1.get(513),t=this.ifd1.get(514);return this.chunk.getUint8Array(e,t)}get image(){return this.ifd0}get thumbnail(){return this.ifd1}createOutput(){let e,t,i,n={};for(t of H)if(e=this[t],!p(e))if(i=this.canTranslate?this.translateBlock(e,t):Object.fromEntries(e),this.options.mergeOutput){if(\"ifd1\"===t)continue;Object.assign(n,i)}else n[t]=i;return this.makerNote&&(n.makerNote=this.makerNote),this.userComment&&(n.userComment=this.userComment),n}assignToOutput(e,t){if(this.globalOptions.mergeOutput)Object.assign(e,t);else for(let[i,n]of Object.entries(t))this.assignObjectToOutput(e,i,n)}}function de(e,t,i,n){var s=e+t/60+i/3600;return\"S\"!==n&&\"W\"!==n||(s*=-1),s}c(fe,\"type\",\"tiff\"),c(fe,\"headerLength\",10),T.set(\"tiff\",fe);var pe=Object.freeze({__proto__:null,default:ne,Exifr:te,fileParsers:w,segmentParsers:T,fileReaders:A,tagKeys:E,tagValues:B,tagRevivers:N,createDictionary:U,extendDictionary:F,fetchUrlAsArrayBuffer:M,readBlobAsArrayBuffer:R,chunkedProps:G,otherSegments:V,segments:z,tiffBlocks:H,segmentsAndBlocks:j,tiffExtractables:W,inheritables:K,allFormatters:X,Options:q,parse:ie});const ge={ifd0:!1,ifd1:!1,exif:!1,gps:!1,interop:!1,sanitize:!1,reviveValues:!0,translateKeys:!1,translateValues:!1,mergeOutput:!1},me=Object.assign({},ge,{firstChunkSize:4e4,gps:[1,2,3,4]});async function Se(e){let t=new te(me);await t.read(e);let i=await t.parse();if(i&&i.gps){let{latitude:e,longitude:t}=i.gps;return{latitude:e,longitude:t}}}const Ce=Object.assign({},ge,{tiff:!1,ifd1:!0,mergeOutput:!1});async function ye(e){let t=new te(Ce);await t.read(e);let i=await t.extractThumbnail();return i&&a?s.from(i):i}async function be(e){let t=await this.thumbnail(e);if(void 0!==t){let e=new Blob([t]);return URL.createObjectURL(e)}}const Ie=Object.assign({},ge,{firstChunkSize:4e4,ifd0:[274]});async function Pe(e){let t=new te(Ie);await t.read(e);let i=await t.parse();if(i&&i.ifd0)return i.ifd0[274]}const ke=Object.freeze({1:{dimensionSwapped:!1,scaleX:1,scaleY:1,deg:0,rad:0},2:{dimensionSwapped:!1,scaleX:-1,scaleY:1,deg:0,rad:0},3:{dimensionSwapped:!1,scaleX:1,scaleY:1,deg:180,rad:180*Math.PI/180},4:{dimensionSwapped:!1,scaleX:-1,scaleY:1,deg:180,rad:180*Math.PI/180},5:{dimensionSwapped:!0,scaleX:1,scaleY:-1,deg:90,rad:90*Math.PI/180},6:{dimensionSwapped:!0,scaleX:1,scaleY:1,deg:90,rad:90*Math.PI/180},7:{dimensionSwapped:!0,scaleX:1,scaleY:-1,deg:270,rad:270*Math.PI/180},8:{dimensionSwapped:!0,scaleX:1,scaleY:1,deg:270,rad:270*Math.PI/180}});let we=!0,Te=!0;if(\"object\"==typeof navigator){let e=navigator.userAgent;if(e.includes(\"iPad\")||e.includes(\"iPhone\")){let t=e.match(/OS (\\d+)_(\\d+)/);if(t){let[,e,i]=t,n=Number(e)+.1*Number(i);we=n<13.4,Te=!1}}else if(e.includes(\"OS X 10\")){let[,t]=e.match(/OS X 10[_.](\\d+)/);we=Te=Number(t)<15}if(e.includes(\"Chrome/\")){let[,t]=e.match(/Chrome\\/(\\d+)/);we=Te=Number(t)<81}else if(e.includes(\"Firefox/\")){let[,t]=e.match(/Firefox\\/(\\d+)/);we=Te=Number(t)<77}}async function Ae(e){let t=await Pe(e);return Object.assign({canvas:we,css:Te},ke[t])}class De extends I{constructor(...e){super(...e),c(this,\"ranges\",new Oe),0!==this.byteLength&&this.ranges.add(0,this.byteLength)}_tryExtend(e,t,i){if(0===e&&0===this.byteLength&&i){let e=new DataView(i.buffer||i,i.byteOffset,i.byteLength);this._swapDataView(e)}else{let i=e+t;if(i>this.byteLength){let{dataView:e}=this._extend(i);this._swapDataView(e)}}}_extend(e){let t;t=a?s.allocUnsafe(e):new Uint8Array(e);let i=new DataView(t.buffer,t.byteOffset,t.byteLength);return t.set(new Uint8Array(this.buffer,this.byteOffset,this.byteLength),0),{uintView:t,dataView:i}}subarray(e,t,i=!1){return t=t||this._lengthToEnd(e),i&&this._tryExtend(e,t),this.ranges.add(e,t),super.subarray(e,t)}set(e,t,i=!1){i&&this._tryExtend(t,e.byteLength,e);let n=super.set(e,t);return this.ranges.add(t,n.byteLength),n}async ensureChunk(e,t){this.chunked&&(this.ranges.available(e,t)||await this.readChunk(e,t))}available(e,t){return this.ranges.available(e,t)}}class Oe{constructor(){c(this,\"list\",[])}get length(){return this.list.length}add(e,t,i=0){let n=e+t,s=this.list.filter((t=>xe(e,t.offset,n)||xe(e,t.end,n)));if(s.length>0){e=Math.min(e,...s.map((e=>e.offset))),n=Math.max(n,...s.map((e=>e.end))),t=n-e;let i=s.shift();i.offset=e,i.length=t,i.end=n,this.list=this.list.filter((e=>!s.includes(e)))}else this.list.push({offset:e,length:t,end:n})}available(e,t){let i=e+t;return this.list.some((t=>t.offset<=e&&i<=t.end))}}function xe(e,t,i){return e<=t&&t<=i}class ve extends De{constructor(e,t){super(0),c(this,\"chunksRead\",0),this.input=e,this.options=t}async readWhole(){this.chunked=!1,await this.readChunk(this.nextChunkOffset)}async readChunked(){this.chunked=!0,await this.readChunk(0,this.options.firstChunkSize)}async readNextChunk(e=this.nextChunkOffset){if(this.fullyRead)return this.chunksRead++,!1;let t=this.options.chunkSize,i=await this.readChunk(e,t);return!!i&&i.byteLength===t}async readChunk(e,t){if(this.chunksRead++,0!==(t=this.safeWrapAddress(e,t)))return this._readChunk(e,t)}safeWrapAddress(e,t){return void 0!==this.size&&e+t>this.size?Math.max(0,this.size-e):t}get nextChunkOffset(){if(0!==this.ranges.list.length)return this.ranges.list[0].length}get canReadNextChunk(){return this.chunksReade.kind===t))}parseBoxHead(e){let t=this.file.getUint32(e),i=this.file.getString(e+4,4),n=e+8;return 1===t&&(t=this.file.getUint64(e+8),n+=8),{offset:e,length:t,kind:i,start:n}}parseBoxFullHead(e){if(void 0!==e.version)return;let t=this.file.getUint32(e.start);e.version=t>>24,e.start+=4}}class Le extends Re{static canHandle(e,t){if(0!==t)return!1;let i=e.getUint16(2);if(i>50)return!1;let n=16,s=[];for(;n=2&&(n=3===t.version?4:2,s=this.file.getString(i+n+2,4),\"Exif\"===s))return this.file.getUintBytes(i,n);r+=t.length}}get8bits(e){let t=this.file.getUint8(e);return[t>>4,15&t]}findExtentInIloc(e,t){this.parseBoxFullHead(e);let i=e.start,[n,s]=this.get8bits(i++),[r,a]=this.get8bits(i++),o=2===e.version?4:2,l=1===e.version||2===e.version?2:0,h=a+n+s,u=2===e.version?4:2,c=this.file.getUintBytes(i,u);for(i+=u;c--;){let e=this.file.getUintBytes(i,o);i+=o+l+2+r;let u=this.file.getUint16(i);if(i+=2,e===t)return u>1&&console.warn(\"ILOC box has more than one extent but we're only processing one\\nPlease create an issue at https://github.com/MikeKovarik/exifr with this file\"),[this.file.getUintBytes(i+a,n),this.file.getUintBytes(i+a+n,s)];i+=u*h}}}class Ue extends Le{}c(Ue,\"type\",\"heic\");class Fe extends Le{}c(Fe,\"type\",\"avif\"),w.set(\"heic\",Ue),w.set(\"avif\",Fe),U(E,[\"ifd0\",\"ifd1\"],[[256,\"ImageWidth\"],[257,\"ImageHeight\"],[258,\"BitsPerSample\"],[259,\"Compression\"],[262,\"PhotometricInterpretation\"],[270,\"ImageDescription\"],[271,\"Make\"],[272,\"Model\"],[273,\"StripOffsets\"],[274,\"Orientation\"],[277,\"SamplesPerPixel\"],[278,\"RowsPerStrip\"],[279,\"StripByteCounts\"],[282,\"XResolution\"],[283,\"YResolution\"],[284,\"PlanarConfiguration\"],[296,\"ResolutionUnit\"],[301,\"TransferFunction\"],[305,\"Software\"],[306,\"ModifyDate\"],[315,\"Artist\"],[316,\"HostComputer\"],[317,\"Predictor\"],[318,\"WhitePoint\"],[319,\"PrimaryChromaticities\"],[513,\"ThumbnailOffset\"],[514,\"ThumbnailLength\"],[529,\"YCbCrCoefficients\"],[530,\"YCbCrSubSampling\"],[531,\"YCbCrPositioning\"],[532,\"ReferenceBlackWhite\"],[700,\"ApplicationNotes\"],[33432,\"Copyright\"],[33723,\"IPTC\"],[34665,\"ExifIFD\"],[34675,\"ICC\"],[34853,\"GpsIFD\"],[330,\"SubIFD\"],[40965,\"InteropIFD\"],[40091,\"XPTitle\"],[40092,\"XPComment\"],[40093,\"XPAuthor\"],[40094,\"XPKeywords\"],[40095,\"XPSubject\"]]),U(E,\"exif\",[[33434,\"ExposureTime\"],[33437,\"FNumber\"],[34850,\"ExposureProgram\"],[34852,\"SpectralSensitivity\"],[34855,\"ISO\"],[34858,\"TimeZoneOffset\"],[34859,\"SelfTimerMode\"],[34864,\"SensitivityType\"],[34865,\"StandardOutputSensitivity\"],[34866,\"RecommendedExposureIndex\"],[34867,\"ISOSpeed\"],[34868,\"ISOSpeedLatitudeyyy\"],[34869,\"ISOSpeedLatitudezzz\"],[36864,\"ExifVersion\"],[36867,\"DateTimeOriginal\"],[36868,\"CreateDate\"],[36873,\"GooglePlusUploadCode\"],[36880,\"OffsetTime\"],[36881,\"OffsetTimeOriginal\"],[36882,\"OffsetTimeDigitized\"],[37121,\"ComponentsConfiguration\"],[37122,\"CompressedBitsPerPixel\"],[37377,\"ShutterSpeedValue\"],[37378,\"ApertureValue\"],[37379,\"BrightnessValue\"],[37380,\"ExposureCompensation\"],[37381,\"MaxApertureValue\"],[37382,\"SubjectDistance\"],[37383,\"MeteringMode\"],[37384,\"LightSource\"],[37385,\"Flash\"],[37386,\"FocalLength\"],[37393,\"ImageNumber\"],[37394,\"SecurityClassification\"],[37395,\"ImageHistory\"],[37396,\"SubjectArea\"],[37500,\"MakerNote\"],[37510,\"UserComment\"],[37520,\"SubSecTime\"],[37521,\"SubSecTimeOriginal\"],[37522,\"SubSecTimeDigitized\"],[37888,\"AmbientTemperature\"],[37889,\"Humidity\"],[37890,\"Pressure\"],[37891,\"WaterDepth\"],[37892,\"Acceleration\"],[37893,\"CameraElevationAngle\"],[40960,\"FlashpixVersion\"],[40961,\"ColorSpace\"],[40962,\"ExifImageWidth\"],[40963,\"ExifImageHeight\"],[40964,\"RelatedSoundFile\"],[41483,\"FlashEnergy\"],[41486,\"FocalPlaneXResolution\"],[41487,\"FocalPlaneYResolution\"],[41488,\"FocalPlaneResolutionUnit\"],[41492,\"SubjectLocation\"],[41493,\"ExposureIndex\"],[41495,\"SensingMethod\"],[41728,\"FileSource\"],[41729,\"SceneType\"],[41730,\"CFAPattern\"],[41985,\"CustomRendered\"],[41986,\"ExposureMode\"],[41987,\"WhiteBalance\"],[41988,\"DigitalZoomRatio\"],[41989,\"FocalLengthIn35mmFormat\"],[41990,\"SceneCaptureType\"],[41991,\"GainControl\"],[41992,\"Contrast\"],[41993,\"Saturation\"],[41994,\"Sharpness\"],[41996,\"SubjectDistanceRange\"],[42016,\"ImageUniqueID\"],[42032,\"OwnerName\"],[42033,\"SerialNumber\"],[42034,\"LensInfo\"],[42035,\"LensMake\"],[42036,\"LensModel\"],[42037,\"LensSerialNumber\"],[42080,\"CompositeImage\"],[42081,\"CompositeImageCount\"],[42082,\"CompositeImageExposureTimes\"],[42240,\"Gamma\"],[59932,\"Padding\"],[59933,\"OffsetSchema\"],[65e3,\"OwnerName\"],[65001,\"SerialNumber\"],[65002,\"Lens\"],[65100,\"RawFile\"],[65101,\"Converter\"],[65102,\"WhiteBalance\"],[65105,\"Exposure\"],[65106,\"Shadows\"],[65107,\"Brightness\"],[65108,\"Contrast\"],[65109,\"Saturation\"],[65110,\"Sharpness\"],[65111,\"Smoothness\"],[65112,\"MoireFilter\"],[40965,\"InteropIFD\"]]),U(E,\"gps\",[[0,\"GPSVersionID\"],[1,\"GPSLatitudeRef\"],[2,\"GPSLatitude\"],[3,\"GPSLongitudeRef\"],[4,\"GPSLongitude\"],[5,\"GPSAltitudeRef\"],[6,\"GPSAltitude\"],[7,\"GPSTimeStamp\"],[8,\"GPSSatellites\"],[9,\"GPSStatus\"],[10,\"GPSMeasureMode\"],[11,\"GPSDOP\"],[12,\"GPSSpeedRef\"],[13,\"GPSSpeed\"],[14,\"GPSTrackRef\"],[15,\"GPSTrack\"],[16,\"GPSImgDirectionRef\"],[17,\"GPSImgDirection\"],[18,\"GPSMapDatum\"],[19,\"GPSDestLatitudeRef\"],[20,\"GPSDestLatitude\"],[21,\"GPSDestLongitudeRef\"],[22,\"GPSDestLongitude\"],[23,\"GPSDestBearingRef\"],[24,\"GPSDestBearing\"],[25,\"GPSDestDistanceRef\"],[26,\"GPSDestDistance\"],[27,\"GPSProcessingMethod\"],[28,\"GPSAreaInformation\"],[29,\"GPSDateStamp\"],[30,\"GPSDifferential\"],[31,\"GPSHPositioningError\"]]),U(B,[\"ifd0\",\"ifd1\"],[[274,{1:\"Horizontal (normal)\",2:\"Mirror horizontal\",3:\"Rotate 180\",4:\"Mirror vertical\",5:\"Mirror horizontal and rotate 270 CW\",6:\"Rotate 90 CW\",7:\"Mirror horizontal and rotate 90 CW\",8:\"Rotate 270 CW\"}],[296,{1:\"None\",2:\"inches\",3:\"cm\"}]]);let Ee=U(B,\"exif\",[[34850,{0:\"Not defined\",1:\"Manual\",2:\"Normal program\",3:\"Aperture priority\",4:\"Shutter priority\",5:\"Creative program\",6:\"Action program\",7:\"Portrait mode\",8:\"Landscape mode\"}],[37121,{0:\"-\",1:\"Y\",2:\"Cb\",3:\"Cr\",4:\"R\",5:\"G\",6:\"B\"}],[37383,{0:\"Unknown\",1:\"Average\",2:\"CenterWeightedAverage\",3:\"Spot\",4:\"MultiSpot\",5:\"Pattern\",6:\"Partial\",255:\"Other\"}],[37384,{0:\"Unknown\",1:\"Daylight\",2:\"Fluorescent\",3:\"Tungsten (incandescent light)\",4:\"Flash\",9:\"Fine weather\",10:\"Cloudy weather\",11:\"Shade\",12:\"Daylight fluorescent (D 5700 - 7100K)\",13:\"Day white fluorescent (N 4600 - 5400K)\",14:\"Cool white fluorescent (W 3900 - 4500K)\",15:\"White fluorescent (WW 3200 - 3700K)\",17:\"Standard light A\",18:\"Standard light B\",19:\"Standard light C\",20:\"D55\",21:\"D65\",22:\"D75\",23:\"D50\",24:\"ISO studio tungsten\",255:\"Other\"}],[37385,{0:\"Flash did not fire\",1:\"Flash fired\",5:\"Strobe return light not detected\",7:\"Strobe return light detected\",9:\"Flash fired, compulsory flash mode\",13:\"Flash fired, compulsory flash mode, return light not detected\",15:\"Flash fired, compulsory flash mode, return light detected\",16:\"Flash did not fire, compulsory flash mode\",24:\"Flash did not fire, auto mode\",25:\"Flash fired, auto mode\",29:\"Flash fired, auto mode, return light not detected\",31:\"Flash fired, auto mode, return light detected\",32:\"No flash function\",65:\"Flash fired, red-eye reduction mode\",69:\"Flash fired, red-eye reduction mode, return light not detected\",71:\"Flash fired, red-eye reduction mode, return light detected\",73:\"Flash fired, compulsory flash mode, red-eye reduction mode\",77:\"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected\",79:\"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected\",89:\"Flash fired, auto mode, red-eye reduction mode\",93:\"Flash fired, auto mode, return light not detected, red-eye reduction mode\",95:\"Flash fired, auto mode, return light detected, red-eye reduction mode\"}],[41495,{1:\"Not defined\",2:\"One-chip color area sensor\",3:\"Two-chip color area sensor\",4:\"Three-chip color area sensor\",5:\"Color sequential area sensor\",7:\"Trilinear sensor\",8:\"Color sequential linear sensor\"}],[41728,{1:\"Film Scanner\",2:\"Reflection Print Scanner\",3:\"Digital Camera\"}],[41729,{1:\"Directly photographed\"}],[41985,{0:\"Normal\",1:\"Custom\",2:\"HDR (no original saved)\",3:\"HDR (original saved)\",4:\"Original (for HDR)\",6:\"Panorama\",7:\"Portrait HDR\",8:\"Portrait\"}],[41986,{0:\"Auto\",1:\"Manual\",2:\"Auto bracket\"}],[41987,{0:\"Auto\",1:\"Manual\"}],[41990,{0:\"Standard\",1:\"Landscape\",2:\"Portrait\",3:\"Night\",4:\"Other\"}],[41991,{0:\"None\",1:\"Low gain up\",2:\"High gain up\",3:\"Low gain down\",4:\"High gain down\"}],[41996,{0:\"Unknown\",1:\"Macro\",2:\"Close\",3:\"Distant\"}],[42080,{0:\"Unknown\",1:\"Not a Composite Image\",2:\"General Composite Image\",3:\"Composite Image Captured While Shooting\"}]]);const Be={1:\"No absolute unit of measurement\",2:\"Inch\",3:\"Centimeter\"};Ee.set(37392,Be),Ee.set(41488,Be);const Ne={0:\"Normal\",1:\"Low\",2:\"High\"};function Ge(e){return\"object\"==typeof e&&void 0!==e.length?e[0]:e}function Ve(e){let t=Array.from(e).slice(1);return t[1]>15&&(t=t.map((e=>String.fromCharCode(e)))),\"0\"!==t[2]&&0!==t[2]||t.pop(),t.join(\".\")}function ze(e){if(\"string\"==typeof e){var[t,i,n,s,r,a]=e.trim().split(/[-: ]/g).map(Number),o=new Date(t,i-1,n);return Number.isNaN(s)||Number.isNaN(r)||Number.isNaN(a)||(o.setHours(s),o.setMinutes(r),o.setSeconds(a)),Number.isNaN(+o)?e:o}}function He(e){if(\"string\"==typeof e)return e;let t=[];if(0===e[1]&&0===e[e.length-1])for(let i=0;iArray.from(e).join(\".\")],[7,e=>Array.from(e).join(\":\")]]);class We extends re{static canHandle(e,t){return 225===e.getUint8(t+1)&&1752462448===e.getUint32(t+4)&&\"http://ns.adobe.com/\"===e.getString(t+4,\"http://ns.adobe.com/\".length)}static headerLength(e,t){return\"http://ns.adobe.com/xmp/extension/\"===e.getString(t+4,\"http://ns.adobe.com/xmp/extension/\".length)?79:4+\"http://ns.adobe.com/xap/1.0/\".length+1}static findPosition(e,t){let i=super.findPosition(e,t);return i.multiSegment=i.extended=79===i.headerLength,i.multiSegment?(i.chunkCount=e.getUint8(t+72),i.chunkNumber=e.getUint8(t+76),0!==e.getUint8(t+77)&&i.chunkNumber++):(i.chunkCount=1/0,i.chunkNumber=-1),i}static handleMultiSegments(e){return e.map((e=>e.chunk.getString())).join(\"\")}normalizeInput(e){return\"string\"==typeof e?e:I.from(e).getString()}parse(e=this.chunk){if(!this.localOptions.parse)return e;e=function(e){let t={},i={};for(let e of Ze)t[e]=[],i[e]=0;return e.replace(et,((e,n,s)=>{if(\"<\"===n){let n=++i[s];return t[s].push(n),`${e}#${n}`}return`${e}#${t[s].pop()}`}))}(e);let t=Xe.findAll(e,\"rdf\",\"Description\");0===t.length&&t.push(new Xe(\"rdf\",\"Description\",void 0,e));let i,n={};for(let e of t)for(let t of e.properties)i=Je(t.ns,n),_e(t,i);return function(e){let t;for(let i in e)t=e[i]=f(e[i]),void 0===t&&delete e[i];return f(e)}(n)}assignToOutput(e,t){if(this.localOptions.parse)for(let[i,n]of Object.entries(t))switch(i){case\"tiff\":this.assignObjectToOutput(e,\"ifd0\",n);break;case\"exif\":this.assignObjectToOutput(e,\"exif\",n);break;case\"xmlns\":break;default:this.assignObjectToOutput(e,i,n)}else e.xmp=t}}c(We,\"type\",\"xmp\"),c(We,\"multiSegment\",!0),T.set(\"xmp\",We);class Ke{static findAll(e){return qe(e,/([a-zA-Z0-9-]+):([a-zA-Z0-9-]+)=(\"[^\"]*\"|'[^']*')/gm).map(Ke.unpackMatch)}static unpackMatch(e){let t=e[1],i=e[2],n=e[3].slice(1,-1);return n=Qe(n),new Ke(t,i,n)}constructor(e,t,i){this.ns=e,this.name=t,this.value=i}serialize(){return this.value}}class Xe{static findAll(e,t,i){if(void 0!==t||void 0!==i){t=t||\"[\\\\w\\\\d-]+\",i=i||\"[\\\\w\\\\d-]+\";var n=new RegExp(`<(${t}):(${i})(#\\\\d+)?((\\\\s+?[\\\\w\\\\d-:]+=(\"[^\"]*\"|'[^']*'))*\\\\s*)(\\\\/>|>([\\\\s\\\\S]*?)<\\\\/\\\\1:\\\\2\\\\3>)`,\"gm\")}else n=/<([\\w\\d-]+):([\\w\\d-]+)(#\\d+)?((\\s+?[\\w\\d-:]+=(\"[^\"]*\"|'[^']*'))*\\s*)(\\/>|>([\\s\\S]*?)<\\/\\1:\\2\\3>)/gm;return qe(e,n).map(Xe.unpackMatch)}static unpackMatch(e){let t=e[1],i=e[2],n=e[4],s=e[8];return new Xe(t,i,n,s)}constructor(e,t,i,n){this.ns=e,this.name=t,this.attrString=i,this.innerXml=n,this.attrs=Ke.findAll(i),this.children=Xe.findAll(n),this.value=0===this.children.length?Qe(n):void 0,this.properties=[...this.attrs,...this.children]}get isPrimitive(){return void 0!==this.value&&0===this.attrs.length&&0===this.children.length}get isListContainer(){return 1===this.children.length&&this.children[0].isList}get isList(){let{ns:e,name:t}=this;return\"rdf\"===e&&(\"Seq\"===t||\"Bag\"===t||\"Alt\"===t)}get isListItem(){return\"rdf\"===this.ns&&\"li\"===this.name}serialize(){if(0===this.properties.length&&void 0===this.value)return;if(this.isPrimitive)return this.value;if(this.isListContainer)return this.children[0].serialize();if(this.isList)return $e(this.children.map(Ye));if(this.isListItem&&1===this.children.length&&0===this.attrs.length)return this.children[0].serialize();let e={};for(let t of this.properties)_e(t,e);return void 0!==this.value&&(e.value=this.value),f(e)}}function _e(e,t){let i=e.serialize();void 0!==i&&(t[e.name]=i)}var Ye=e=>e.serialize(),$e=e=>1===e.length?e[0]:e,Je=(e,t)=>t[e]?t[e]:t[e]={};function qe(e,t){let i,n=[];if(!e)return n;for(;null!==(i=t.exec(e));)n.push(i);return n}function Qe(e){if(function(e){return null==e||\"null\"===e||\"undefined\"===e||\"\"===e||\"\"===e.trim()}(e))return;let t=Number(e);if(!Number.isNaN(t))return t;let i=e.toLowerCase();return\"true\"===i||\"false\"!==i&&e.trim()}const Ze=[\"rdf:li\",\"rdf:Seq\",\"rdf:Bag\",\"rdf:Alt\",\"rdf:Description\"],et=new RegExp(`(<|\\\\/)(${Ze.join(\"|\")})`,\"g\");var tt=Object.freeze({__proto__:null,default:Me,Exifr:te,fileParsers:w,segmentParsers:T,fileReaders:A,tagKeys:E,tagValues:B,tagRevivers:N,createDictionary:U,extendDictionary:F,fetchUrlAsArrayBuffer:M,readBlobAsArrayBuffer:R,chunkedProps:G,otherSegments:V,segments:z,tiffBlocks:H,segmentsAndBlocks:j,tiffExtractables:W,inheritables:K,allFormatters:X,Options:q,parse:ie,gpsOnlyOptions:me,gps:Se,thumbnailOnlyOptions:Ce,thumbnail:ye,thumbnailUrl:be,orientationOnlyOptions:Ie,orientation:Pe,rotations:ke,get rotateCanvas(){return we},get rotateCss(){return Te},rotation:Ae});const it=[\"xmp\",\"icc\",\"iptc\",\"tiff\"],nt=()=>{};async function st(e,t,i){let n=new q(t);n.chunked=!1,void 0===i&&\"string\"==typeof e&&(i=function(e){let t=e.toLowerCase().split(\".\").pop();if(function(e){return\"exif\"===e||\"tiff\"===e||\"tif\"===e}(t))return\"tiff\";if(it.includes(t))return t}(e));let s=await D(e,n);if(i){if(it.includes(i))return rt(i,s,n);g(\"Invalid segment type\")}else{if(function(e){let t=e.getString(0,50).trim();return t.includes(\"e.promises));A.set(\"fs\",class extends ve{async readWhole(){this.chunked=!1,this.fs=await at;let e=await this.fs.readFile(this.input);this._swapBuffer(e)}async readChunked(){this.chunked=!0,this.fs=await at,await this.open(),await this.readChunk(0,this.options.firstChunkSize)}async open(){void 0===this.fh&&(this.fh=await this.fs.open(this.input,\"r\"),this.size=(await this.fh.stat(this.input)).size)}async _readChunk(e,t){void 0===this.fh&&await this.open(),e+t>this.size&&(t=this.size-e);var i=this.subarray(e,t,!0);return await this.fh.read(i.dataView,0,t,e),i}async close(){if(this.fh){let e=this.fh;this.fh=void 0,await e.close()}}});A.set(\"base64\",class extends ve{constructor(...e){super(...e),this.input=this.input.replace(/^data:([^;]+);base64,/gim,\"\"),this.size=this.input.length/4*3,this.input.endsWith(\"==\")?this.size-=2:this.input.endsWith(\"=\")&&(this.size-=1)}async _readChunk(e,t){let i,n,r=this.input;void 0===e?(e=0,i=0,n=0):(i=4*Math.floor(e/3),n=e-i/4*3),void 0===t&&(t=this.size);let o=e+t,l=i+4*Math.ceil(o/3);r=r.slice(i,l);let h=Math.min(t,this.size-e);if(a){let t=s.from(r,\"base64\").slice(n,n+h);return this.set(t,e,!0)}{let t=this.subarray(e,h,!0),i=atob(r),s=t.toUint8();for(let e=0;ethis.errors.push(e))),c(this,\"metaChunks\",[]),c(this,\"unknownChunks\",[])}static canHandle(e,t){return 35152===t&&2303741511===e.getUint32(0)&&218765834===e.getUint32(4)}async parse(){let{file:e}=this;await this.findPngChunksInRange(\"\u0089PNG\\r\\n\u001A\\n\".length,e.byteLength),await this.readSegments(this.metaChunks),this.findIhdr(),this.parseTextChunks(),await this.findExif().catch(this.catchError),await this.findXmp().catch(this.catchError),await this.findIcc().catch(this.catchError)}async findPngChunksInRange(e,t){let{file:i}=this;for(;e\"text\"===e.type));for(let t of e){let[e,i]=this.file.getString(t.start,t.size).split(\"\\0\");this.injectKeyValToIhdr(e,i)}}injectKeyValToIhdr(e,t){let i=this.parsers.ihdr;i&&i.raw.set(e,t)}findIhdr(){let e=this.metaChunks.find((e=>\"ihdr\"===e.type));e&&!1!==this.options.ihdr.enabled&&this.createParser(\"ihdr\",e.chunk)}async findExif(){let e=this.metaChunks.find((e=>\"exif\"===e.type));e&&this.injectSegment(\"tiff\",e.chunk)}async findXmp(){let e=this.metaChunks.filter((e=>\"itxt\"===e.type));for(let t of e){\"XML:com.adobe.xmp\"===t.chunk.getString(0,\"XML:com.adobe.xmp\".length)&&this.injectSegment(\"xmp\",t.chunk)}}async findIcc(){let e=this.metaChunks.find((e=>\"iccp\"===e.type));if(!e)return;let{chunk:t}=e,i=t.getUint8Array(0,81),s=0;for(;s<80&&0!==i[s];)s++;let r=s+2,a=t.getString(0,s);if(this.injectKeyValToIhdr(\"ProfileName\",a),n){let e=await lt,i=t.getUint8Array(r);i=e.inflateSync(i),this.injectSegment(\"icc\",i)}}}c(ut,\"type\",\"png\"),w.set(\"png\",ut),U(E,\"interop\",[[1,\"InteropIndex\"],[2,\"InteropVersion\"],[4096,\"RelatedImageFileFormat\"],[4097,\"RelatedImageWidth\"],[4098,\"RelatedImageHeight\"]]),F(E,\"ifd0\",[[11,\"ProcessingSoftware\"],[254,\"SubfileType\"],[255,\"OldSubfileType\"],[263,\"Thresholding\"],[264,\"CellWidth\"],[265,\"CellLength\"],[266,\"FillOrder\"],[269,\"DocumentName\"],[280,\"MinSampleValue\"],[281,\"MaxSampleValue\"],[285,\"PageName\"],[286,\"XPosition\"],[287,\"YPosition\"],[290,\"GrayResponseUnit\"],[297,\"PageNumber\"],[321,\"HalftoneHints\"],[322,\"TileWidth\"],[323,\"TileLength\"],[332,\"InkSet\"],[337,\"TargetPrinter\"],[18246,\"Rating\"],[18249,\"RatingPercent\"],[33550,\"PixelScale\"],[34264,\"ModelTransform\"],[34377,\"PhotoshopSettings\"],[50706,\"DNGVersion\"],[50707,\"DNGBackwardVersion\"],[50708,\"UniqueCameraModel\"],[50709,\"LocalizedCameraModel\"],[50736,\"DNGLensInfo\"],[50739,\"ShadowScale\"],[50740,\"DNGPrivateData\"],[33920,\"IntergraphMatrix\"],[33922,\"ModelTiePoint\"],[34118,\"SEMInfo\"],[34735,\"GeoTiffDirectory\"],[34736,\"GeoTiffDoubleParams\"],[34737,\"GeoTiffAsciiParams\"],[50341,\"PrintIM\"],[50721,\"ColorMatrix1\"],[50722,\"ColorMatrix2\"],[50723,\"CameraCalibration1\"],[50724,\"CameraCalibration2\"],[50725,\"ReductionMatrix1\"],[50726,\"ReductionMatrix2\"],[50727,\"AnalogBalance\"],[50728,\"AsShotNeutral\"],[50729,\"AsShotWhiteXY\"],[50730,\"BaselineExposure\"],[50731,\"BaselineNoise\"],[50732,\"BaselineSharpness\"],[50734,\"LinearResponseLimit\"],[50735,\"CameraSerialNumber\"],[50741,\"MakerNoteSafety\"],[50778,\"CalibrationIlluminant1\"],[50779,\"CalibrationIlluminant2\"],[50781,\"RawDataUniqueID\"],[50827,\"OriginalRawFileName\"],[50828,\"OriginalRawFileData\"],[50831,\"AsShotICCProfile\"],[50832,\"AsShotPreProfileMatrix\"],[50833,\"CurrentICCProfile\"],[50834,\"CurrentPreProfileMatrix\"],[50879,\"ColorimetricReference\"],[50885,\"SRawType\"],[50898,\"PanasonicTitle\"],[50899,\"PanasonicTitle2\"],[50931,\"CameraCalibrationSig\"],[50932,\"ProfileCalibrationSig\"],[50933,\"ProfileIFD\"],[50934,\"AsShotProfileName\"],[50936,\"ProfileName\"],[50937,\"ProfileHueSatMapDims\"],[50938,\"ProfileHueSatMapData1\"],[50939,\"ProfileHueSatMapData2\"],[50940,\"ProfileToneCurve\"],[50941,\"ProfileEmbedPolicy\"],[50942,\"ProfileCopyright\"],[50964,\"ForwardMatrix1\"],[50965,\"ForwardMatrix2\"],[50966,\"PreviewApplicationName\"],[50967,\"PreviewApplicationVersion\"],[50968,\"PreviewSettingsName\"],[50969,\"PreviewSettingsDigest\"],[50970,\"PreviewColorSpace\"],[50971,\"PreviewDateTime\"],[50972,\"RawImageDigest\"],[50973,\"OriginalRawFileDigest\"],[50981,\"ProfileLookTableDims\"],[50982,\"ProfileLookTableData\"],[51043,\"TimeCodes\"],[51044,\"FrameRate\"],[51058,\"TStop\"],[51081,\"ReelName\"],[51089,\"OriginalDefaultFinalSize\"],[51090,\"OriginalBestQualitySize\"],[51091,\"OriginalDefaultCropSize\"],[51105,\"CameraLabel\"],[51107,\"ProfileHueSatMapEncoding\"],[51108,\"ProfileLookTableEncoding\"],[51109,\"BaselineExposureOffset\"],[51110,\"DefaultBlackRender\"],[51111,\"NewRawImageDigest\"],[51112,\"RawToPreviewGain\"]]);let ct=[[273,\"StripOffsets\"],[279,\"StripByteCounts\"],[288,\"FreeOffsets\"],[289,\"FreeByteCounts\"],[291,\"GrayResponseCurve\"],[292,\"T4Options\"],[293,\"T6Options\"],[300,\"ColorResponseUnit\"],[320,\"ColorMap\"],[324,\"TileOffsets\"],[325,\"TileByteCounts\"],[326,\"BadFaxLines\"],[327,\"CleanFaxData\"],[328,\"ConsecutiveBadFaxLines\"],[330,\"SubIFD\"],[333,\"InkNames\"],[334,\"NumberofInks\"],[336,\"DotRange\"],[338,\"ExtraSamples\"],[339,\"SampleFormat\"],[340,\"SMinSampleValue\"],[341,\"SMaxSampleValue\"],[342,\"TransferRange\"],[343,\"ClipPath\"],[344,\"XClipPathUnits\"],[345,\"YClipPathUnits\"],[346,\"Indexed\"],[347,\"JPEGTables\"],[351,\"OPIProxy\"],[400,\"GlobalParametersIFD\"],[401,\"ProfileType\"],[402,\"FaxProfile\"],[403,\"CodingMethods\"],[404,\"VersionYear\"],[405,\"ModeNumber\"],[433,\"Decode\"],[434,\"DefaultImageColor\"],[435,\"T82Options\"],[437,\"JPEGTables\"],[512,\"JPEGProc\"],[515,\"JPEGRestartInterval\"],[517,\"JPEGLosslessPredictors\"],[518,\"JPEGPointTransforms\"],[519,\"JPEGQTables\"],[520,\"JPEGDCTables\"],[521,\"JPEGACTables\"],[559,\"StripRowCounts\"],[999,\"USPTOMiscellaneous\"],[18247,\"XP_DIP_XML\"],[18248,\"StitchInfo\"],[28672,\"SonyRawFileType\"],[28688,\"SonyToneCurve\"],[28721,\"VignettingCorrection\"],[28722,\"VignettingCorrParams\"],[28724,\"ChromaticAberrationCorrection\"],[28725,\"ChromaticAberrationCorrParams\"],[28726,\"DistortionCorrection\"],[28727,\"DistortionCorrParams\"],[29895,\"SonyCropTopLeft\"],[29896,\"SonyCropSize\"],[32781,\"ImageID\"],[32931,\"WangTag1\"],[32932,\"WangAnnotation\"],[32933,\"WangTag3\"],[32934,\"WangTag4\"],[32953,\"ImageReferencePoints\"],[32954,\"RegionXformTackPoint\"],[32955,\"WarpQuadrilateral\"],[32956,\"AffineTransformMat\"],[32995,\"Matteing\"],[32996,\"DataType\"],[32997,\"ImageDepth\"],[32998,\"TileDepth\"],[33300,\"ImageFullWidth\"],[33301,\"ImageFullHeight\"],[33302,\"TextureFormat\"],[33303,\"WrapModes\"],[33304,\"FovCot\"],[33305,\"MatrixWorldToScreen\"],[33306,\"MatrixWorldToCamera\"],[33405,\"Model2\"],[33421,\"CFARepeatPatternDim\"],[33422,\"CFAPattern2\"],[33423,\"BatteryLevel\"],[33424,\"KodakIFD\"],[33445,\"MDFileTag\"],[33446,\"MDScalePixel\"],[33447,\"MDColorTable\"],[33448,\"MDLabName\"],[33449,\"MDSampleInfo\"],[33450,\"MDPrepDate\"],[33451,\"MDPrepTime\"],[33452,\"MDFileUnits\"],[33589,\"AdventScale\"],[33590,\"AdventRevision\"],[33628,\"UIC1Tag\"],[33629,\"UIC2Tag\"],[33630,\"UIC3Tag\"],[33631,\"UIC4Tag\"],[33918,\"IntergraphPacketData\"],[33919,\"IntergraphFlagRegisters\"],[33921,\"INGRReserved\"],[34016,\"Site\"],[34017,\"ColorSequence\"],[34018,\"IT8Header\"],[34019,\"RasterPadding\"],[34020,\"BitsPerRunLength\"],[34021,\"BitsPerExtendedRunLength\"],[34022,\"ColorTable\"],[34023,\"ImageColorIndicator\"],[34024,\"BackgroundColorIndicator\"],[34025,\"ImageColorValue\"],[34026,\"BackgroundColorValue\"],[34027,\"PixelIntensityRange\"],[34028,\"TransparencyIndicator\"],[34029,\"ColorCharacterization\"],[34030,\"HCUsage\"],[34031,\"TrapIndicator\"],[34032,\"CMYKEquivalent\"],[34152,\"AFCP_IPTC\"],[34232,\"PixelMagicJBIGOptions\"],[34263,\"JPLCartoIFD\"],[34306,\"WB_GRGBLevels\"],[34310,\"LeafData\"],[34687,\"TIFF_FXExtensions\"],[34688,\"MultiProfiles\"],[34689,\"SharedData\"],[34690,\"T88Options\"],[34732,\"ImageLayer\"],[34750,\"JBIGOptions\"],[34856,\"Opto-ElectricConvFactor\"],[34857,\"Interlace\"],[34908,\"FaxRecvParams\"],[34909,\"FaxSubAddress\"],[34910,\"FaxRecvTime\"],[34929,\"FedexEDR\"],[34954,\"LeafSubIFD\"],[37387,\"FlashEnergy\"],[37388,\"SpatialFrequencyResponse\"],[37389,\"Noise\"],[37390,\"FocalPlaneXResolution\"],[37391,\"FocalPlaneYResolution\"],[37392,\"FocalPlaneResolutionUnit\"],[37397,\"ExposureIndex\"],[37398,\"TIFF-EPStandardID\"],[37399,\"SensingMethod\"],[37434,\"CIP3DataFile\"],[37435,\"CIP3Sheet\"],[37436,\"CIP3Side\"],[37439,\"StoNits\"],[37679,\"MSDocumentText\"],[37680,\"MSPropertySetStorage\"],[37681,\"MSDocumentTextPosition\"],[37724,\"ImageSourceData\"],[40965,\"InteropIFD\"],[40976,\"SamsungRawPointersOffset\"],[40977,\"SamsungRawPointersLength\"],[41217,\"SamsungRawByteOrder\"],[41218,\"SamsungRawUnknown\"],[41484,\"SpatialFrequencyResponse\"],[41485,\"Noise\"],[41489,\"ImageNumber\"],[41490,\"SecurityClassification\"],[41491,\"ImageHistory\"],[41494,\"TIFF-EPStandardID\"],[41995,\"DeviceSettingDescription\"],[42112,\"GDALMetadata\"],[42113,\"GDALNoData\"],[44992,\"ExpandSoftware\"],[44993,\"ExpandLens\"],[44994,\"ExpandFilm\"],[44995,\"ExpandFilterLens\"],[44996,\"ExpandScanner\"],[44997,\"ExpandFlashLamp\"],[46275,\"HasselbladRawImage\"],[48129,\"PixelFormat\"],[48130,\"Transformation\"],[48131,\"Uncompressed\"],[48132,\"ImageType\"],[48256,\"ImageWidth\"],[48257,\"ImageHeight\"],[48258,\"WidthResolution\"],[48259,\"HeightResolution\"],[48320,\"ImageOffset\"],[48321,\"ImageByteCount\"],[48322,\"AlphaOffset\"],[48323,\"AlphaByteCount\"],[48324,\"ImageDataDiscard\"],[48325,\"AlphaDataDiscard\"],[50215,\"OceScanjobDesc\"],[50216,\"OceApplicationSelector\"],[50217,\"OceIDNumber\"],[50218,\"OceImageLogic\"],[50255,\"Annotations\"],[50459,\"HasselbladExif\"],[50547,\"OriginalFileName\"],[50560,\"USPTOOriginalContentType\"],[50656,\"CR2CFAPattern\"],[50710,\"CFAPlaneColor\"],[50711,\"CFALayout\"],[50712,\"LinearizationTable\"],[50713,\"BlackLevelRepeatDim\"],[50714,\"BlackLevel\"],[50715,\"BlackLevelDeltaH\"],[50716,\"BlackLevelDeltaV\"],[50717,\"WhiteLevel\"],[50718,\"DefaultScale\"],[50719,\"DefaultCropOrigin\"],[50720,\"DefaultCropSize\"],[50733,\"BayerGreenSplit\"],[50737,\"ChromaBlurRadius\"],[50738,\"AntiAliasStrength\"],[50752,\"RawImageSegmentation\"],[50780,\"BestQualityScale\"],[50784,\"AliasLayerMetadata\"],[50829,\"ActiveArea\"],[50830,\"MaskedAreas\"],[50935,\"NoiseReductionApplied\"],[50974,\"SubTileBlockSize\"],[50975,\"RowInterleaveFactor\"],[51008,\"OpcodeList1\"],[51009,\"OpcodeList2\"],[51022,\"OpcodeList3\"],[51041,\"NoiseProfile\"],[51114,\"CacheVersion\"],[51125,\"DefaultUserCrop\"],[51157,\"NikonNEFInfo\"],[65024,\"KdcIFD\"]];F(E,\"ifd0\",ct),F(E,\"exif\",ct),U(B,\"gps\",[[23,{M:\"Magnetic North\",T:\"True North\"}],[25,{K:\"Kilometers\",M:\"Miles\",N:\"Nautical Miles\"}]]);class ft extends re{static canHandle(e,t){return 224===e.getUint8(t+1)&&1246120262===e.getUint32(t+4)&&0===e.getUint8(t+8)}parse(){return this.parseTags(),this.translate(),this.output}parseTags(){this.raw=new Map([[0,this.chunk.getUint16(0)],[2,this.chunk.getUint8(2)],[3,this.chunk.getUint16(3)],[5,this.chunk.getUint16(5)],[7,this.chunk.getUint8(7)],[8,this.chunk.getUint8(8)]])}}c(ft,\"type\",\"jfif\"),c(ft,\"headerLength\",9),T.set(\"jfif\",ft),U(E,\"jfif\",[[0,\"JFIFVersion\"],[2,\"ResolutionUnit\"],[3,\"XResolution\"],[5,\"YResolution\"],[7,\"ThumbnailWidth\"],[8,\"ThumbnailHeight\"]]);class dt extends re{parse(){return this.parseTags(),this.translate(),this.output}parseTags(){this.raw=new Map([[0,this.chunk.getUint32(0)],[4,this.chunk.getUint32(4)],[8,this.chunk.getUint8(8)],[9,this.chunk.getUint8(9)],[10,this.chunk.getUint8(10)],[11,this.chunk.getUint8(11)],[12,this.chunk.getUint8(12)],...Array.from(this.raw)])}}c(dt,\"type\",\"ihdr\"),T.set(\"ihdr\",dt),U(E,\"ihdr\",[[0,\"ImageWidth\"],[4,\"ImageHeight\"],[8,\"BitDepth\"],[9,\"ColorType\"],[10,\"Compression\"],[11,\"Filter\"],[12,\"Interlace\"]]),U(B,\"ihdr\",[[9,{0:\"Grayscale\",2:\"RGB\",3:\"Palette\",4:\"Grayscale with Alpha\",6:\"RGB with Alpha\",DEFAULT:\"Unknown\"}],[10,{0:\"Deflate/Inflate\",DEFAULT:\"Unknown\"}],[11,{0:\"Adaptive\",DEFAULT:\"Unknown\"}],[12,{0:\"Noninterlaced\",1:\"Adam7 Interlace\",DEFAULT:\"Unknown\"}]]);class pt extends re{static canHandle(e,t){return 226===e.getUint8(t+1)&&1229144927===e.getUint32(t+4)}static findPosition(e,t){let i=super.findPosition(e,t);return i.chunkNumber=e.getUint8(t+16),i.chunkCount=e.getUint8(t+17),i.multiSegment=i.chunkCount>1,i}static handleMultiSegments(e){return function(e){let t=function(e){let t=e[0].constructor,i=0;for(let t of e)i+=t.length;let n=new t(i),s=0;for(let t of e)n.set(t,s),s+=t.length;return n}(e.map((e=>e.chunk.toUint8())));return new I(t)}(e)}parse(){return this.raw=new Map,this.parseHeader(),this.parseTags(),this.translate(),this.output}parseHeader(){let{raw:e}=this;this.chunk.byteLength<84&&g(\"ICC header is too short\");for(let[t,i]of Object.entries(gt)){t=parseInt(t,10);let n=i(this.chunk,t);\"\\0\\0\\0\\0\"!==n&&e.set(t,n)}}parseTags(){let e,t,i,n,s,{raw:r}=this,a=this.chunk.getUint32(128),o=132,l=this.chunk.byteLength;for(;a--;){if(e=this.chunk.getString(o,4),t=this.chunk.getUint32(o+4),i=this.chunk.getUint32(o+8),n=this.chunk.getString(t,4),t+i>l)return void console.warn(\"reached the end of the first ICC chunk. Enable options.tiff.multiSegment to read all ICC segments.\");s=this.parseTag(n,t,i),void 0!==s&&\"\\0\\0\\0\\0\"!==s&&r.set(e,s),o+=12}}parseTag(e,t,i){switch(e){case\"desc\":return this.parseDesc(t);case\"mluc\":return this.parseMluc(t);case\"text\":return this.parseText(t,i);case\"sig \":return this.parseSig(t)}if(!(t+i>this.chunk.byteLength))return this.chunk.getUint8Array(t,i)}parseDesc(e){let t=this.chunk.getUint32(e+8)-1;return m(this.chunk.getString(e+12,t))}parseText(e,t){return m(this.chunk.getString(e+8,t-8))}parseSig(e){return m(this.chunk.getString(e+8,4))}parseMluc(e){let{chunk:t}=this,i=t.getUint32(e+8),n=t.getUint32(e+12),s=e+16,r=[];for(let a=0;a>4,e.getUint8(t+1)%16].map((e=>e.toString(10))).join(\".\")},12:mt,16:mt,20:mt,24:function(e,t){const i=e.getUint16(t),n=e.getUint16(t+2)-1,s=e.getUint16(t+4),r=e.getUint16(t+6),a=e.getUint16(t+8),o=e.getUint16(t+10);return new Date(Date.UTC(i,n,s,r,a,o))},36:mt,40:mt,48:mt,52:mt,64:(e,t)=>e.getUint32(t),80:mt};function mt(e,t){return m(e.getString(t,4))}T.set(\"icc\",pt),U(E,\"icc\",[[4,\"ProfileCMMType\"],[8,\"ProfileVersion\"],[12,\"ProfileClass\"],[16,\"ColorSpaceData\"],[20,\"ProfileConnectionSpace\"],[24,\"ProfileDateTime\"],[36,\"ProfileFileSignature\"],[40,\"PrimaryPlatform\"],[44,\"CMMFlags\"],[48,\"DeviceManufacturer\"],[52,\"DeviceModel\"],[56,\"DeviceAttributes\"],[64,\"RenderingIntent\"],[68,\"ConnectionSpaceIlluminant\"],[80,\"ProfileCreator\"],[84,\"ProfileID\"],[\"Header\",\"ProfileHeader\"],[\"MS00\",\"WCSProfiles\"],[\"bTRC\",\"BlueTRC\"],[\"bXYZ\",\"BlueMatrixColumn\"],[\"bfd\",\"UCRBG\"],[\"bkpt\",\"MediaBlackPoint\"],[\"calt\",\"CalibrationDateTime\"],[\"chad\",\"ChromaticAdaptation\"],[\"chrm\",\"Chromaticity\"],[\"ciis\",\"ColorimetricIntentImageState\"],[\"clot\",\"ColorantTableOut\"],[\"clro\",\"ColorantOrder\"],[\"clrt\",\"ColorantTable\"],[\"cprt\",\"ProfileCopyright\"],[\"crdi\",\"CRDInfo\"],[\"desc\",\"ProfileDescription\"],[\"devs\",\"DeviceSettings\"],[\"dmdd\",\"DeviceModelDesc\"],[\"dmnd\",\"DeviceMfgDesc\"],[\"dscm\",\"ProfileDescriptionML\"],[\"fpce\",\"FocalPlaneColorimetryEstimates\"],[\"gTRC\",\"GreenTRC\"],[\"gXYZ\",\"GreenMatrixColumn\"],[\"gamt\",\"Gamut\"],[\"kTRC\",\"GrayTRC\"],[\"lumi\",\"Luminance\"],[\"meas\",\"Measurement\"],[\"meta\",\"Metadata\"],[\"mmod\",\"MakeAndModel\"],[\"ncl2\",\"NamedColor2\"],[\"ncol\",\"NamedColor\"],[\"ndin\",\"NativeDisplayInfo\"],[\"pre0\",\"Preview0\"],[\"pre1\",\"Preview1\"],[\"pre2\",\"Preview2\"],[\"ps2i\",\"PS2RenderingIntent\"],[\"ps2s\",\"PostScript2CSA\"],[\"psd0\",\"PostScript2CRD0\"],[\"psd1\",\"PostScript2CRD1\"],[\"psd2\",\"PostScript2CRD2\"],[\"psd3\",\"PostScript2CRD3\"],[\"pseq\",\"ProfileSequenceDesc\"],[\"psid\",\"ProfileSequenceIdentifier\"],[\"psvm\",\"PS2CRDVMSize\"],[\"rTRC\",\"RedTRC\"],[\"rXYZ\",\"RedMatrixColumn\"],[\"resp\",\"OutputResponse\"],[\"rhoc\",\"ReflectionHardcopyOrigColorimetry\"],[\"rig0\",\"PerceptualRenderingIntentGamut\"],[\"rig2\",\"SaturationRenderingIntentGamut\"],[\"rpoc\",\"ReflectionPrintOutputColorimetry\"],[\"sape\",\"SceneAppearanceEstimates\"],[\"scoe\",\"SceneColorimetryEstimates\"],[\"scrd\",\"ScreeningDesc\"],[\"scrn\",\"Screening\"],[\"targ\",\"CharTarget\"],[\"tech\",\"Technology\"],[\"vcgt\",\"VideoCardGamma\"],[\"view\",\"ViewingConditions\"],[\"vued\",\"ViewingCondDesc\"],[\"wtpt\",\"MediaWhitePoint\"]]);const St={\"4d2p\":\"Erdt Systems\",AAMA:\"Aamazing Technologies\",ACER:\"Acer\",ACLT:\"Acolyte Color Research\",ACTI:\"Actix Sytems\",ADAR:\"Adara Technology\",ADBE:\"Adobe\",ADI:\"ADI Systems\",AGFA:\"Agfa Graphics\",ALMD:\"Alps Electric\",ALPS:\"Alps Electric\",ALWN:\"Alwan Color Expertise\",AMTI:\"Amiable Technologies\",AOC:\"AOC International\",APAG:\"Apago\",APPL:\"Apple Computer\",AST:\"AST\",\"AT&T\":\"AT&T\",BAEL:\"BARBIERI electronic\",BRCO:\"Barco NV\",BRKP:\"Breakpoint\",BROT:\"Brother\",BULL:\"Bull\",BUS:\"Bus Computer Systems\",\"C-IT\":\"C-Itoh\",CAMR:\"Intel\",CANO:\"Canon\",CARR:\"Carroll Touch\",CASI:\"Casio\",CBUS:\"Colorbus PL\",CEL:\"Crossfield\",CELx:\"Crossfield\",CGS:\"CGS Publishing Technologies International\",CHM:\"Rochester Robotics\",CIGL:\"Colour Imaging Group, London\",CITI:\"Citizen\",CL00:\"Candela\",CLIQ:\"Color IQ\",CMCO:\"Chromaco\",CMiX:\"CHROMiX\",COLO:\"Colorgraphic Communications\",COMP:\"Compaq\",COMp:\"Compeq/Focus Technology\",CONR:\"Conrac Display Products\",CORD:\"Cordata Technologies\",CPQ:\"Compaq\",CPRO:\"ColorPro\",CRN:\"Cornerstone\",CTX:\"CTX International\",CVIS:\"ColorVision\",CWC:\"Fujitsu Laboratories\",DARI:\"Darius Technology\",DATA:\"Dataproducts\",DCP:\"Dry Creek Photo\",DCRC:\"Digital Contents Resource Center, Chung-Ang University\",DELL:\"Dell Computer\",DIC:\"Dainippon Ink and Chemicals\",DICO:\"Diconix\",DIGI:\"Digital\",\"DL&C\":\"Digital Light & Color\",DPLG:\"Doppelganger\",DS:\"Dainippon Screen\",DSOL:\"DOOSOL\",DUPN:\"DuPont\",EPSO:\"Epson\",ESKO:\"Esko-Graphics\",ETRI:\"Electronics and Telecommunications Research Institute\",EVER:\"Everex Systems\",EXAC:\"ExactCODE\",Eizo:\"Eizo\",FALC:\"Falco Data Products\",FF:\"Fuji Photo Film\",FFEI:\"FujiFilm Electronic Imaging\",FNRD:\"Fnord Software\",FORA:\"Fora\",FORE:\"Forefront Technology\",FP:\"Fujitsu\",FPA:\"WayTech Development\",FUJI:\"Fujitsu\",FX:\"Fuji Xerox\",GCC:\"GCC Technologies\",GGSL:\"Global Graphics Software\",GMB:\"Gretagmacbeth\",GMG:\"GMG\",GOLD:\"GoldStar Technology\",GOOG:\"Google\",GPRT:\"Giantprint\",GTMB:\"Gretagmacbeth\",GVC:\"WayTech Development\",GW2K:\"Sony\",HCI:\"HCI\",HDM:\"Heidelberger Druckmaschinen\",HERM:\"Hermes\",HITA:\"Hitachi America\",HP:\"Hewlett-Packard\",HTC:\"Hitachi\",HiTi:\"HiTi Digital\",IBM:\"IBM\",IDNT:\"Scitex\",IEC:\"Hewlett-Packard\",IIYA:\"Iiyama North America\",IKEG:\"Ikegami Electronics\",IMAG:\"Image Systems\",IMI:\"Ingram Micro\",INTC:\"Intel\",INTL:\"N/A (INTL)\",INTR:\"Intra Electronics\",IOCO:\"Iocomm International Technology\",IPS:\"InfoPrint Solutions Company\",IRIS:\"Scitex\",ISL:\"Ichikawa Soft Laboratory\",ITNL:\"N/A (ITNL)\",IVM:\"IVM\",IWAT:\"Iwatsu Electric\",Idnt:\"Scitex\",Inca:\"Inca Digital Printers\",Iris:\"Scitex\",JPEG:\"Joint Photographic Experts Group\",JSFT:\"Jetsoft Development\",JVC:\"JVC Information Products\",KART:\"Scitex\",KFC:\"KFC Computek Components\",KLH:\"KLH Computers\",KMHD:\"Konica Minolta\",KNCA:\"Konica\",KODA:\"Kodak\",KYOC:\"Kyocera\",Kart:\"Scitex\",LCAG:\"Leica\",LCCD:\"Leeds Colour\",LDAK:\"Left Dakota\",LEAD:\"Leading Technology\",LEXM:\"Lexmark International\",LINK:\"Link Computer\",LINO:\"Linotronic\",LITE:\"Lite-On\",Leaf:\"Leaf\",Lino:\"Linotronic\",MAGC:\"Mag Computronic\",MAGI:\"MAG Innovision\",MANN:\"Mannesmann\",MICN:\"Micron Technology\",MICR:\"Microtek\",MICV:\"Microvitec\",MINO:\"Minolta\",MITS:\"Mitsubishi Electronics America\",MITs:\"Mitsuba\",MNLT:\"Minolta\",MODG:\"Modgraph\",MONI:\"Monitronix\",MONS:\"Monaco Systems\",MORS:\"Morse Technology\",MOTI:\"Motive Systems\",MSFT:\"Microsoft\",MUTO:\"MUTOH INDUSTRIES\",Mits:\"Mitsubishi Electric\",NANA:\"NANAO\",NEC:\"NEC\",NEXP:\"NexPress Solutions\",NISS:\"Nissei Sangyo America\",NKON:\"Nikon\",NONE:\"none\",OCE:\"Oce Technologies\",OCEC:\"OceColor\",OKI:\"Oki\",OKID:\"Okidata\",OKIP:\"Okidata\",OLIV:\"Olivetti\",OLYM:\"Olympus\",ONYX:\"Onyx Graphics\",OPTI:\"Optiquest\",PACK:\"Packard Bell\",PANA:\"Matsushita Electric Industrial\",PANT:\"Pantone\",PBN:\"Packard Bell\",PFU:\"PFU\",PHIL:\"Philips Consumer Electronics\",PNTX:\"HOYA\",POne:\"Phase One A/S\",PREM:\"Premier Computer Innovations\",PRIN:\"Princeton Graphic Systems\",PRIP:\"Princeton Publishing Labs\",QLUX:\"Hong Kong\",QMS:\"QMS\",QPCD:\"QPcard AB\",QUAD:\"QuadLaser\",QUME:\"Qume\",RADI:\"Radius\",RDDx:\"Integrated Color Solutions\",RDG:\"Roland DG\",REDM:\"REDMS Group\",RELI:\"Relisys\",RGMS:\"Rolf Gierling Multitools\",RICO:\"Ricoh\",RNLD:\"Edmund Ronald\",ROYA:\"Royal\",RPC:\"Ricoh Printing Systems\",RTL:\"Royal Information Electronics\",SAMP:\"Sampo\",SAMS:\"Samsung\",SANT:\"Jaime Santana Pomares\",SCIT:\"Scitex\",SCRN:\"Dainippon Screen\",SDP:\"Scitex\",SEC:\"Samsung\",SEIK:\"Seiko Instruments\",SEIk:\"Seikosha\",SGUY:\"ScanGuy.com\",SHAR:\"Sharp Laboratories\",SICC:\"International Color Consortium\",SONY:\"Sony\",SPCL:\"SpectraCal\",STAR:\"Star\",STC:\"Sampo Technology\",Scit:\"Scitex\",Sdp:\"Scitex\",Sony:\"Sony\",TALO:\"Talon Technology\",TAND:\"Tandy\",TATU:\"Tatung\",TAXA:\"TAXAN America\",TDS:\"Tokyo Denshi Sekei\",TECO:\"TECO Information Systems\",TEGR:\"Tegra\",TEKT:\"Tektronix\",TI:\"Texas Instruments\",TMKR:\"TypeMaker\",TOSB:\"Toshiba\",TOSH:\"Toshiba\",TOTK:\"TOTOKU ELECTRIC\",TRIU:\"Triumph\",TSBT:\"Toshiba\",TTX:\"TTX Computer Products\",TVM:\"TVM Professional Monitor\",TW:\"TW Casper\",ULSX:\"Ulead Systems\",UNIS:\"Unisys\",UTZF:\"Utz Fehlau & Sohn\",VARI:\"Varityper\",VIEW:\"Viewsonic\",VISL:\"Visual communication\",VIVO:\"Vivo Mobile Communication\",WANG:\"Wang\",WLBR:\"Wilbur Imaging\",WTG2:\"Ware To Go\",WYSE:\"WYSE Technology\",XERX:\"Xerox\",XRIT:\"X-Rite\",ZRAN:\"Zoran\",Zebr:\"Zebra Technologies\",appl:\"Apple Computer\",bICC:\"basICColor\",berg:\"bergdesign\",ceyd:\"Integrated Color Solutions\",clsp:\"MacDermid ColorSpan\",ds:\"Dainippon Screen\",dupn:\"DuPont\",ffei:\"FujiFilm Electronic Imaging\",flux:\"FluxData\",iris:\"Scitex\",kart:\"Scitex\",lcms:\"Little CMS\",lino:\"Linotronic\",none:\"none\",ob4d:\"Erdt Systems\",obic:\"Medigraph\",quby:\"Qubyx Sarl\",scit:\"Scitex\",scrn:\"Dainippon Screen\",sdp:\"Scitex\",siwi:\"SIWI GRAFIKA\",yxym:\"YxyMaster\"},Ct={scnr:\"Scanner\",mntr:\"Monitor\",prtr:\"Printer\",link:\"Device Link\",abst:\"Abstract\",spac:\"Color Space Conversion Profile\",nmcl:\"Named Color\",cenc:\"ColorEncodingSpace profile\",mid:\"MultiplexIdentification profile\",mlnk:\"MultiplexLink profile\",mvis:\"MultiplexVisualization profile\",nkpf:\"Nikon Input Device Profile (NON-STANDARD!)\"};U(B,\"icc\",[[4,St],[12,Ct],[40,Object.assign({},St,Ct)],[48,St],[80,St],[64,{0:\"Perceptual\",1:\"Relative Colorimetric\",2:\"Saturation\",3:\"Absolute Colorimetric\"}],[\"tech\",{amd:\"Active Matrix Display\",crt:\"Cathode Ray Tube Display\",kpcd:\"Photo CD\",pmd:\"Passive Matrix Display\",dcam:\"Digital Camera\",dcpj:\"Digital Cinema Projector\",dmpc:\"Digital Motion Picture Camera\",dsub:\"Dye Sublimation Printer\",epho:\"Electrophotographic Printer\",esta:\"Electrostatic Printer\",flex:\"Flexography\",fprn:\"Film Writer\",fscn:\"Film Scanner\",grav:\"Gravure\",ijet:\"Ink Jet Printer\",imgs:\"Photo Image Setter\",mpfr:\"Motion Picture Film Recorder\",mpfs:\"Motion Picture Film Scanner\",offs:\"Offset Lithography\",pjtv:\"Projection Television\",rpho:\"Photographic Paper Printer\",rscn:\"Reflective Scanner\",silk:\"Silkscreen\",twax:\"Thermal Wax Printer\",vidc:\"Video Camera\",vidm:\"Video Monitor\"}]]);class yt extends re{static canHandle(e,t,i){return 237===e.getUint8(t+1)&&\"Photoshop\"===e.getString(t+4,9)&&void 0!==this.containsIptc8bim(e,t,i)}static headerLength(e,t,i){let n,s=this.containsIptc8bim(e,t,i);if(void 0!==s)return n=e.getUint8(t+s+7),n%2!=0&&(n+=1),0===n&&(n=4),s+8+n}static containsIptc8bim(e,t,i){for(let n=0;n {\n if (loaded.length > 0) {\n drawPhotos.fitZoom(false);\n }\n });\n })\n .on('dragenter.svgLocalPhotos', over)\n .on('dragexit.svgLocalPhotos', over)\n .on('dragover.svgLocalPhotos', over);\n\n _initialized = true;\n }\n\n function ensureViewerLoaded(context) {\n if (_photoFrame) {\n return Promise.resolve(_photoFrame);\n }\n\n const viewer = context.container().select('.photoviewer')\n .selectAll('.local-photos-wrapper')\n .data([0]);\n\n const viewerEnter = viewer.enter()\n .append('div')\n .attr('class', 'photo-wrapper local-photos-wrapper')\n .classed('hide', true);\n\n viewerEnter\n .append('div')\n .attr('class', 'photo-attribution photo-attribution-dual fillD');\n\n const controlsEnter = viewerEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls-local');\n\n controlsEnter\n .append('button')\n .classed('back', true)\n .on('click.back', () => stepPhotos(-1))\n .text('\u25C0');\n\n controlsEnter\n .append('button')\n .classed('forward', true)\n .on('click.forward', () => stepPhotos(1))\n .text('\u25B6');\n\n return planePhotoFrame(context, viewerEnter)\n .then(planePhotoFrame => _photoFrame = planePhotoFrame);\n }\n\n function stepPhotos(stepBy){\n if (!_photos || _photos.length === 0) return;\n if (_activePhotoIdx === undefined) _activePhotoIdx = 0;\n\n const newIndex = _activePhotoIdx + stepBy;\n _activePhotoIdx = Math.max(0, Math.min(_photos.length - 1, newIndex));\n\n click(null, _photos[_activePhotoIdx], false);\n }\n\n // opens the image at bottom left\n function click(d3_event, image, zoomTo) {\n _activePhotoIdx = _photos.indexOf(image);\n ensureViewerLoaded(context).then(() => {\n const viewer = context.container().select('.photoviewer')\n .datum(image);\n\n const viewerWrap = viewer.select('.local-photos-wrapper');\n const isHidden = viewerWrap.classed('hide');\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n viewer.classed('hide', false);\n viewerWrap.classed('hide', false);\n }\n\n const controlsWrap = viewerWrap.select('.photo-controls-wrap');\n\n controlsWrap.select('.back')\n .attr('disabled', _activePhotoIdx <= 0 ? true: null);\n controlsWrap.select('.forward')\n .attr('disabled', _activePhotoIdx >= _photos.length - 1 ? true: null);\n\n const attribution = viewerWrap.selectAll('.photo-attribution').text('');\n\n if (image.date) {\n attribution\n .append('span')\n .text(image.date.toLocaleString(localizer.localeCode()));\n }\n if (image.name) {\n attribution\n .append('span')\n .classed('filename', true)\n .text(image.name);\n }\n\n _photoFrame.selectPhoto({ image_path: '' });\n image.getSrc().then(src => {\n _photoFrame\n .selectPhoto({ image_path: src })\n .showPhotoFrame(viewerWrap);\n\n setStyles();\n });\n });\n\n // centers the map with image location\n if (zoomTo) {\n context.map().centerEase(image.loc);\n }\n }\n\n\n function transform(d) {\n // projection expects [long, lat]\n var svgpoint = projection(d.loc);\n return 'translate(' + svgpoint[0] + ',' + svgpoint[1] + ')';\n }\n\n function setStyles(hovered) {\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n\n context.container().selectAll('.layer-local-photos .viewfield-group')\n .classed('hovered', d => d.id === hovered?.id)\n .classed('highlighted', d => d.id === hovered?.id || d.id === selected?.id)\n .classed('currentView', d => d.id === selected?.id);\n }\n\n // puts the image markers on the map\n function display_markers(imageList) {\n imageList = imageList.filter(image => isArray(image.loc) && isNumber(image.loc[0]) && isNumber(image.loc[1]));\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(imageList, function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', (d3_event, d) => setStyles(d))\n .on('mouseleave', () => setStyles(null))\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const showViewfields = context.map().zoom() >= minViewfieldZoom;\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n // viewfields may or may not be drawn...\n // but if they are, draw below the circles\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', function() {\n const d = this.parentNode.__data__;\n return `rotate(${Math.round(d.direction ?? 0)},0,0),scale(1.5,1.5),translate(-8,-13)`;\n })\n .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z')\n .style('visibility', function() {\n const d = this.parentNode.__data__;\n return isNumber(d.direction) ? 'visible' : 'hidden';\n });\n }\n\n function drawPhotos(selection) {\n layer = selection.selectAll('.layer-local-photos')\n .data(_photos ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-local-photos');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (_photos) {\n display_markers(_photos);\n }\n }\n\n\n function readFileAsDataURL(file) {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result);\n reader.onerror = error => reject(error);\n reader.readAsDataURL(file);\n });\n }\n /**\n * Reads and parses files\n * @param {Array} files - Holds array of file - [file_1, file_2, ...]\n */\n async function readmultifiles(files, callback) {\n const loaded = [];\n\n for (const file of files) {\n try {\n const exifData = await exifr.parse(file); // eslint-disable-line no-await-in-loop\n const photo = {\n service: 'photo',\n id: _idAutoinc++,\n name: file.name,\n getSrc: () => readFileAsDataURL(file),\n file: file,\n loc: [exifData.longitude, exifData.latitude],\n direction: exifData.GPSImgDirection,\n date: exifData.CreateDate || exifData.DateTimeOriginal || exifData.ModifyDate,\n };\n loaded.push(photo);\n const sameName = _photos.filter(i => i.name === photo.name);\n if (sameName.length === 0) {\n _photos.push(photo);\n } else {\n const thisContent = await photo.getSrc(); // eslint-disable-line no-await-in-loop\n const sameNameContent = await Promise.allSettled(sameName.map(i => i.getSrc())); // eslint-disable-line no-await-in-loop\n if (!sameNameContent.some(i => i.value === thisContent)) {\n _photos.push(photo);\n }\n }\n } catch {\n // skip files which are not a supported image file\n }\n }\n\n if (typeof callback === 'function') callback(loaded);\n dispatch.call('change');\n }\n\n drawPhotos.setFiles = function(fileList, callback) {\n // read and parse asynchronously\n readmultifiles(Array.from(fileList), callback);\n return this;\n };\n\n // Step 1: entry point\n /**\n * Sets the fileList\n * @param {Object} fileList - The uploaded files. fileList is an object, not an array object\n * @param {Object} fileList.0 - A File - {name: \"Das.png\", lastModified: 1625064498536, lastModifiedDate: Wed Jun 30 2021 20:18:18 GMT+0530 (India Standard Time), webkitRelativePath: \"\", size: 859658, \u2026}\n * @param {Function} callback - A callback to be called after the photos have been loaded and parsed\n */\n drawPhotos.fileList = function(fileList, callback) {\n if (!arguments.length) return _fileList;\n\n _fileList = fileList;\n\n if (!fileList || !fileList.length) return this;\n\n drawPhotos.setFiles(_fileList, callback);\n\n return this;\n };\n\n drawPhotos.getPhotos = function() {\n return _photos;\n };\n\n drawPhotos.removePhoto = function(id) {\n _photos = _photos.filter(i => i.id !== id);\n dispatch.call('change');\n return _photos;\n };\n\n drawPhotos.openPhoto = click;\n\n drawPhotos.fitZoom = function(force) {\n const coords = _photos\n .map(image => image.loc)\n .filter(l => isArray(l) && isNumber(l[0]) && isNumber(l[1]));\n if (coords.length === 0) return;\n const extent = coords\n .map(l => geoExtent(l, l))\n .reduce((a, b) => a.extend(b));\n\n const map = context.map();\n var viewport = map.trimmedExtent().polygon();\n\n if (force !== false || !geoPolygonIntersectsPolygon(viewport, coords, true)) {\n map.centerZoom(extent.center(), Math.min(18, map.trimmedExtentZoom(extent)));\n }\n };\n\n function showLayer() {\n layer.style('display', 'block');\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', () => {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n });\n }\n\n drawPhotos.enabled = function(val) {\n if (!arguments.length) return _enabled;\n\n _enabled = val;\n if (_enabled) {\n showLayer();\n } else {\n hideLayer();\n }\n\n dispatch.call('change');\n return this;\n };\n\n drawPhotos.hasData = function() {\n return isArray(_photos) && _photos.length > 0;\n };\n\n\n init();\n return drawPhotos;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\n\nimport { modeBrowse } from '../modes/browse';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\n\nlet _layerEnabled = false;\nlet _qaService;\n\nexport function svgOsmose(projection, context, dispatch) {\n const throttledRedraw = throttle(() => dispatch.call('change'), 1000);\n const minZoom = 12;\n\n let touchLayer = d3_select(null);\n let drawLayer = d3_select(null);\n let layerVisible = false;\n\n function markerPath(selection, klass) {\n selection\n .attr('class', klass)\n .attr('transform', 'translate(-10, -28)')\n .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');\n }\n\n // Loosely-coupled osmose service for fetching issues\n function getService() {\n if (services.osmose && !_qaService) {\n _qaService = services.osmose;\n _qaService.on('loaded', throttledRedraw);\n } else if (!services.osmose && _qaService) {\n _qaService = null;\n }\n\n return _qaService;\n }\n\n // Show the markers\n function editOn() {\n if (!layerVisible) {\n layerVisible = true;\n drawLayer\n .style('display', 'block');\n }\n }\n\n // Immediately remove the markers and their touch targets\n function editOff() {\n if (layerVisible) {\n layerVisible = false;\n drawLayer\n .style('display', 'none');\n drawLayer.selectAll('.qaItem.osmose')\n .remove();\n touchLayer.selectAll('.qaItem.osmose')\n .remove();\n }\n }\n\n // Enable the layer. This shows the markers and transitions them to visible.\n function layerOn() {\n editOn();\n\n drawLayer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end interrupt', () => dispatch.call('change'));\n }\n\n // Disable the layer. This transitions the layer invisible and then hides the markers.\n function layerOff() {\n throttledRedraw.cancel();\n drawLayer.interrupt();\n touchLayer.selectAll('.qaItem.osmose')\n .remove();\n\n drawLayer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end interrupt', () => {\n editOff();\n dispatch.call('change');\n });\n }\n\n // Update the issue markers\n function updateMarkers() {\n if (!layerVisible || !_layerEnabled) return;\n\n const service = getService();\n const selectedID = context.selectedErrorID();\n const data = (service ? service.getItems(projection) : []);\n const getTransform = svgPointTransform(projection);\n\n // Draw markers..\n const markers = drawLayer.selectAll('.qaItem.osmose')\n .data(data, d => d.id);\n\n // exit\n markers.exit()\n .remove();\n\n // enter\n const markersEnter = markers.enter()\n .append('g')\n .attr('class', d => `qaItem ${d.service} itemId-${d.id} itemType-${d.itemType}`);\n\n markersEnter\n .append('polygon')\n .call(markerPath, 'shadow');\n\n markersEnter\n .append('ellipse')\n .attr('cx', 0)\n .attr('cy', 0)\n .attr('rx', 4.5)\n .attr('ry', 2)\n .attr('class', 'stroke');\n\n markersEnter\n .append('polygon')\n .attr('fill', d => service.getColor(d.item))\n .call(markerPath, 'qaItem-fill');\n\n markersEnter\n .append('use')\n .attr('class', 'icon-annotation')\n .attr('transform', 'translate(-6, -22)')\n .attr('width', '12px')\n .attr('height', '12px')\n .attr('xlink:href', d => d.icon ? '#' + d.icon : '');\n\n // update\n markers\n .merge(markersEnter)\n .sort(sortY)\n .classed('selected', d => d.id === selectedID)\n .attr('transform', getTransform);\n\n // Draw targets..\n if (touchLayer.empty()) return;\n const fillClass = context.getDebug('target') ? 'pink' : 'nocolor';\n\n const targets = touchLayer.selectAll('.qaItem.osmose')\n .data(data, d => d.id);\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('rect')\n .attr('width', '20px')\n .attr('height', '30px')\n .attr('x', '-10px')\n .attr('y', '-28px')\n .merge(targets)\n .sort(sortY)\n .attr('class', d => `qaItem ${d.service} target ${fillClass} itemId-${d.id}`)\n .attr('transform', getTransform);\n\n function sortY(a, b) {\n return (a.id === selectedID) ? 1\n : (b.id === selectedID) ? -1\n : b.loc[1] - a.loc[1];\n }\n }\n\n // Draw the Osmose layer and schedule loading issues and updating markers.\n function drawOsmose(selection) {\n const service = getService();\n\n const surface = context.surface();\n if (surface && !surface.empty()) {\n touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');\n }\n\n drawLayer = selection.selectAll('.layer-osmose')\n .data(service ? [0] : []);\n\n drawLayer.exit()\n .remove();\n\n drawLayer = drawLayer.enter()\n .append('g')\n .attr('class', 'layer-osmose')\n .style('display', _layerEnabled ? 'block' : 'none')\n .merge(drawLayer);\n\n if (_layerEnabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n service.loadIssues(projection);\n updateMarkers();\n } else {\n editOff();\n }\n }\n }\n\n // Toggles the layer on and off\n drawOsmose.enabled = function(val) {\n if (!arguments.length) return _layerEnabled;\n\n _layerEnabled = val;\n if (_layerEnabled) {\n // Strings supplied by Osmose fetched before showing layer for first time\n // NOTE: Currently no way to change locale in iD at runtime, would need to re-call this method if that's ever implemented\n // Also, If layer is toggled quickly multiple requests are sent\n getService().loadStrings()\n .then(layerOn)\n .catch(err => {\n console.log(err); // eslint-disable-line no-console\n });\n } else {\n layerOff();\n if (context.selectedErrorID()) {\n context.enter(modeBrowse(context));\n }\n }\n\n dispatch.call('change');\n return this;\n };\n\n drawOsmose.supported = () => !!getService();\n\n return drawOsmose;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgStreetside(projection, context, dispatch) {\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var minZoom = 14;\n var minMarkerZoom = 16;\n var minViewfieldZoom = 18;\n var layer = d3_select(null);\n var _viewerYaw = 0;\n var _selectedSequence = null;\n var _streetside;\n\n /**\n * init().\n */\n function init() {\n if (svgStreetside.initialized) return; // run once\n svgStreetside.enabled = false;\n svgStreetside.initialized = true;\n }\n\n /**\n * getService().\n */\n function getService() {\n if (services.streetside && !_streetside) {\n _streetside = services.streetside;\n _streetside.event\n .on('viewerChanged.svgStreetside', viewerChanged)\n .on('loadedImages.svgStreetside', throttledRedraw);\n } else if (!services.streetside && _streetside) {\n _streetside = null;\n }\n\n return _streetside;\n }\n\n /**\n * showLayer().\n */\n function showLayer() {\n var service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n /**\n * hideLayer().\n */\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n /**\n * editOn().\n */\n function editOn() {\n layer.style('display', 'block');\n }\n\n /**\n * editOff().\n */\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n /**\n * click() Handles 'bubble' point click event.\n */\n function click(d3_event, d) {\n var service = getService();\n if (!service) return;\n\n // try to preserve the viewer rotation when staying on the same sequence\n if (d.sequenceKey !== _selectedSequence) {\n _viewerYaw = 0; // reset\n }\n _selectedSequence = d.sequenceKey;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, d.key)\n .yaw(_viewerYaw)\n .showViewer(context);\n });\n\n context.map().centerEase(d.loc);\n }\n\n /**\n * mouseover().\n */\n function mouseover(d3_event, d) {\n var service = getService();\n if (service) service.setStyles(context, d);\n }\n\n /**\n * mouseout().\n */\n function mouseout() {\n var service = getService();\n if (service) service.setStyles(context, null);\n }\n\n /**\n * transform().\n */\n function transform(d) {\n var t = svgPointTransform(projection)(d);\n var rot = d.ca + _viewerYaw;\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n\n function viewerChanged() {\n var service = getService();\n if (!service) return;\n\n var viewer = service.viewer();\n if (!viewer) return;\n\n // update viewfield rotation\n _viewerYaw = viewer.getYaw();\n\n // avoid updating if the map is currently transformed\n // e.g. during drags or easing.\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .attr('transform', transform);\n }\n\n\n function filterBubbles(bubbles, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n bubbles = bubbles.filter(function(bubble) {\n return new Date(bubble.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n bubbles = bubbles.filter(function(bubble) {\n return new Date(bubble.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n bubbles = bubbles.filter(function(bubble) {\n return usernames.indexOf(bubble.captured_by) !== -1;\n });\n }\n\n return bubbles;\n }\n\n function filterSequences(sequences, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n sequences = sequences.filter(function(sequences) {\n return new Date(sequences.properties.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n sequences = sequences.filter(function(sequences) {\n return new Date(sequences.properties.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n sequences = sequences.filter(function(sequences) {\n return usernames.indexOf(sequences.properties.captured_by) !== -1;\n });\n }\n\n return sequences;\n }\n\n /**\n * update().\n */\n function update() {\n var viewer = context.container().select('.photoviewer');\n var selected = viewer.empty() ? undefined : viewer.datum();\n var z = ~~context.map().zoom();\n var showMarkers = (z >= minMarkerZoom);\n var showViewfields = (z >= minViewfieldZoom);\n var service = getService();\n\n var sequences = [];\n var bubbles = [];\n\n if (context.photos().showsPanoramic()) {\n sequences = (service ? service.sequences(projection) : []);\n bubbles = (service && showMarkers ? service.bubbles(projection) : []);\n sequences = filterSequences(sequences);\n bubbles = filterBubbles(bubbles);\n }\n\n var traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.key; });\n\n dispatch.call('photoDatesChanged', this, 'streetside', [\n ...filterBubbles(bubbles, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(t => t.properties.vintageStart)]);\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n var groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(bubbles, function(d) {\n // force reenter once bubbles are attached to a sequence\n return d.key + (d.sequenceKey ? 'v1' : 'v0');\n });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n var markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n return (a === selected) ? 1\n : (b === selected) ? -1\n : b.loc[1] - a.loc[1];\n })\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n var viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n // viewfields may or may not be drawn...\n // but if they are, draw below the circles\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n var d = this.parentNode.__data__;\n if (d.pano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n }\n\n /**\n * drawImages()\n * drawImages is the method that is returned (and that runs) every time 'svgStreetside()' is called.\n * 'svgStreetside()' is called from index.js\n */\n function drawImages(selection) {\n var enabled = svgStreetside.enabled;\n var service = getService();\n\n layer = selection.selectAll('.layer-streetside-images')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n var layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-streetside-images')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadBubbles(projection);\n } else {\n dispatch.call('photoDatesChanged', this, 'streetside', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'streetside', []);\n }\n }\n\n\n /**\n * drawImages.enabled().\n */\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgStreetside.enabled;\n svgStreetside.enabled = _;\n if (svgStreetside.enabled) {\n showLayer();\n context.photos().on('change.streetside', update);\n } else {\n hideLayer();\n context.photos().on('change.streetside', null);\n }\n dispatch.call('change');\n return this;\n };\n\n /**\n * drawImages.supported().\n */\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n init();\n\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgVegbilder(projection, context, dispatch) {\n const throttledRedraw = throttle(() => dispatch.call('change'), 1000);\n const minZoom = 14;\n const minMarkerZoom = 16;\n const minViewfieldZoom = 18;\n let layer = d3_select(null);\n let _viewerYaw = 0;\n let _vegbilder;\n\n /**\n * init().\n */\n function init() {\n if (svgVegbilder.initialized) return; // run once\n svgVegbilder.enabled = false;\n svgVegbilder.initialized = true;\n }\n\n /**\n * getService().\n */\n function getService() {\n if (services.vegbilder && !_vegbilder) {\n _vegbilder = services.vegbilder;\n _vegbilder.event\n .on('viewerChanged.svgVegbilder', viewerChanged)\n .on('loadedImages.svgVegbilder', throttledRedraw);\n } else if (!services.vegbilder && _vegbilder) {\n _vegbilder = null;\n }\n\n return _vegbilder;\n }\n\n /**\n * showLayer().\n */\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', () => dispatch.call('change'));\n }\n\n /**\n * hideLayer().\n */\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n /**\n * editOn().\n */\n function editOn() {\n layer.style('display', 'block');\n }\n\n /**\n * editOff().\n */\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n function click(d3_event, d) {\n const service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(() => {\n service\n .selectImage(context, d.key)\n .showViewer(context);\n });\n\n context.map().centerEase(d.loc);\n }\n\n /**\n * mouseover().\n */\n function mouseover(d3_event, d) {\n const service = getService();\n if (service) service.setStyles(context, d);\n }\n\n /**\n * mouseout().\n */\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n /**\n * transform().\n */\n function transform(d, selected) {\n let t = svgPointTransform(projection)(d);\n let rot = d.ca;\n if (d === selected) {\n rot += _viewerYaw;\n }\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n\n function viewerChanged() {\n const service = getService();\n if (!service) return;\n\n const frame = service.photoFrame();\n if (!frame) return;\n\n // update viewfield rotation\n _viewerYaw = frame.getYaw();\n\n // avoid updating if the map is currently transformed\n // e.g. during drags or easing.\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .attr('transform', (d) => transform(d, d));\n }\n\n function filterImages(images, skipDateFilter) {\n const photoContext = context.photos();\n const fromDateString = photoContext.fromDate();\n const toDateString = photoContext.toDate();\n const showsFlat = photoContext.showsFlat();\n const showsPano = photoContext.showsPanoramic();\n\n if (fromDateString && !skipDateFilter) {\n const fromDate = new Date(fromDateString);\n images = images.filter(image => image.captured_at.getTime() >= fromDate.getTime());\n }\n\n if (toDateString && !skipDateFilter) {\n const toDate = new Date(toDateString);\n images = images.filter(image => image.captured_at.getTime() <= toDate.getTime());\n }\n\n if (!showsPano) {\n images = images.filter(image => !image.is_sphere);\n }\n\n if (!showsFlat) {\n images = images.filter(image => image.is_sphere);\n }\n\n return images;\n }\n\n function filterSequences(sequences, skipDateFilter) {\n const photoContext = context.photos();\n const fromDateString = photoContext.fromDate();\n const toDateString = photoContext.toDate();\n const showsFlat = photoContext.showsFlat();\n const showsPano = photoContext.showsPanoramic();\n\n if (fromDateString && !skipDateFilter) {\n const fromDate = new Date(fromDateString);\n sequences = sequences.filter(({images}) => images[0].captured_at.getTime() >= fromDate.getTime());\n }\n\n if (toDateString && !skipDateFilter) {\n const toDate = new Date(toDateString);\n sequences = sequences.filter(({images}) => images[images.length - 1].captured_at.getTime() <= toDate.getTime());\n }\n\n if (!showsPano) {\n sequences = sequences.filter(({images}) => !images[0].is_sphere);\n }\n\n if (!showsFlat) {\n sequences = sequences.filter(({images}) => images[0].is_sphere);\n }\n\n return sequences;\n }\n\n /**\n * update().\n */\n function update() {\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n const z = ~~context.map().zoom();\n const showMarkers = (z >= minMarkerZoom);\n const showViewfields = (z >= minViewfieldZoom);\n const service = getService();\n let sequences = [];\n let images = [];\n\n if (service) {\n // The WFS-layer for that year or image type may not be loaded after a filter is changed\n service.loadImages(context);\n\n sequences = service.sequences(projection);\n images = showMarkers ? service.images(projection) : [];\n\n dispatch.call('photoDatesChanged', this, 'vegbilder', [\n ...filterImages(images, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(s => s.images[0].captured_at)]);\n\n images = filterImages(images);\n sequences = filterSequences(sequences);\n }\n\n let traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, d => d.key);\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, (d) => d.key);\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort((a, b) => {\n return (a === selected) ? 1\n : (b === selected) ? -1\n : b.loc[1] - a.loc[1];\n })\n .attr('transform', (d) => transform(d, selected))\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n // viewfields may or may not be drawn...\n // but if they are, draw below the circles\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n const d = this.parentNode.__data__;\n if (d.is_sphere) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n }\n\n /**\n * drawImages()\n * drawImages is the method that is returned (and that runs) every time 'svgStreetside()' is called.\n * 'svgStreetside()' is called from index.js\n */\n function drawImages(selection) {\n const enabled = svgVegbilder.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-vegbilder')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-vegbilder')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadImages(context);\n } else {\n dispatch.call('photoDatesChanged', this, 'vegbilder', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'vegbilder', []);\n }\n }\n\n\n /**\n * drawImages.enabled().\n */\n drawImages.enabled = function (_) {\n if (!arguments.length) return svgVegbilder.enabled;\n svgVegbilder.enabled = _;\n if (svgVegbilder.enabled) {\n showLayer();\n context.photos().on('change.vegbilder', update);\n } else {\n hideLayer();\n context.photos().on('change.vegbilder', null);\n }\n dispatch.call('change');\n return this;\n };\n\n /**\n * drawImages.supported().\n */\n drawImages.supported = function () {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n drawImages.validHere = function(extent, zoom) {\n return zoom >= (minZoom - 2)\n && getService().validHere(extent);\n };\n\n init();\n\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgMapillaryImages(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const minZoom = 12;\n const minMarkerZoom = 16;\n const minViewfieldZoom = 18;\n let layer = d3_select(null);\n let _mapillary;\n\n\n function init() {\n if (svgMapillaryImages.initialized) return; // run once\n svgMapillaryImages.enabled = false;\n svgMapillaryImages.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('loadedImages', throttledRedraw);\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n\n return _mapillary;\n }\n\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, image) {\n const service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, image.id)\n .showViewer(context);\n });\n\n context.map().centerEase(image.loc);\n }\n\n\n function mouseover(d3_event, image) {\n const service = getService();\n\n if (service) service.setStyles(context, image);\n }\n\n\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n\n function transform(d) {\n let t = svgPointTransform(projection)(d);\n if (d.ca) {\n t += ' rotate(' + Math.floor(d.ca) + ',0,0)';\n }\n return t;\n }\n\n\n function filterImages(images, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n\n if (!showsPano || !showsFlat) {\n images = images.filter(function(image) {\n if (image.is_pano) return showsPano;\n return showsFlat;\n });\n }\n if (fromDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.captured_at).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.captured_at).getTime() <= new Date(toDate).getTime();\n });\n }\n\n return images;\n }\n\n function filterSequences(sequences, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n\n if (!showsPano || !showsFlat) {\n sequences = sequences.filter(function(sequence) {\n if (sequence.properties.hasOwnProperty('is_pano')) {\n if (sequence.properties.is_pano) return showsPano;\n return showsFlat;\n }\n return false;\n });\n }\n if (fromDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() >= new Date(fromDate).getTime().toString();\n });\n }\n if (toDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() <= new Date(toDate).getTime().toString();\n });\n }\n\n return sequences;\n }\n\n function update() {\n const z = ~~context.map().zoom();\n const showMarkers = (z >= minMarkerZoom);\n const showViewfields = (z >= minViewfieldZoom);\n\n const service = getService();\n let sequences = (service ? service.sequences(projection) : []);\n let images = (service && showMarkers ? service.images(projection) : []);\n // images[0]\n // {\n // \"loc\":[13.235349655151367,52.50694232952122],\n // \"captured_at\":1619457514500,\n // \"ca\":0,\n // \"id\":505488307476058,\n // \"is_pano\":false,\n // \"sequence_id\":\"zcyumxorbza3dq3twjybam\"\n // }\n dispatch.call('photoDatesChanged', this, 'mapillary', [\n ...filterImages(images, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(s => s.properties.captured_at)]);\n\n images = filterImages(images);\n sequences = filterSequences(sequences);\n\n service.filterViewer(context);\n\n let traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.id; });\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n return b.loc[1] - a.loc[1]; // sort Y\n })\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter() // viewfields may or may not be drawn...\n .insert('path', 'circle') // but if they are, draw below the circles\n .attr('class', 'viewfield')\n .classed('pano', function() { return this.parentNode.__data__.is_pano; })\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n if (this.parentNode.__data__.is_pano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n }\n\n\n function drawImages(selection) {\n const enabled = svgMapillaryImages.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadImages(projection);\n } else {\n dispatch.call('photoDatesChanged', this, 'mapillary', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'mapillary', []);\n }\n }\n\n\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgMapillaryImages.enabled;\n svgMapillaryImages.enabled = _;\n if (svgMapillaryImages.enabled) {\n showLayer();\n context.photos().on('change.mapillary_images', update);\n } else {\n hideLayer();\n context.photos().on('change.mapillary_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgMapillaryPosition(projection, context) {\n const throttledRedraw = throttle(function () { update(); }, 1000);\n const minZoom = 12;\n const minViewfieldZoom = 18;\n let layer = d3_select(null);\n let _mapillary;\n let viewerCompassAngle;\n\n\n function init() {\n if (svgMapillaryPosition.initialized) return; // run once\n svgMapillaryPosition.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('imageChanged', throttledRedraw);\n _mapillary.event.on('bearingChanged', function(e) {\n viewerCompassAngle = e.bearing;\n\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .filter(function(d) {\n return d.is_pano;\n })\n .attr('transform', transform);\n });\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n\n return _mapillary;\n }\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n function transform(d) {\n let t = svgPointTransform(projection)(d);\n if (d.is_pano && viewerCompassAngle !== null && isFinite(viewerCompassAngle)) {\n t += ' rotate(' + Math.floor(viewerCompassAngle) + ',0,0)';\n } else if (d.ca) {\n t += ' rotate(' + Math.floor(d.ca) + ',0,0)';\n }\n return t;\n }\n\n function update() {\n\n const z = ~~context.map().zoom();\n const showViewfields = (z >= minViewfieldZoom);\n\n const service = getService();\n const image = service && service.getActiveImage();\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(image ? [image] : [], function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group currentView highlighted');\n\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z');\n }\n\n\n function drawImages(selection) {\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary-position')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary-position');\n\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n } else {\n editOff();\n }\n }\n\n\n drawImages.enabled = function() {\n update();\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgMapillarySigns(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const minZoom = 12;\n let layer = d3_select(null);\n let _mapillary;\n\n\n function init() {\n if (svgMapillarySigns.initialized) return; // run once\n svgMapillarySigns.enabled = false;\n svgMapillarySigns.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('loadedSigns', throttledRedraw);\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n return _mapillary;\n }\n\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n service.loadSignResources(context);\n editOn();\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n editOff();\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.icon-sign').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, d) {\n const service = getService();\n if (!service) return;\n\n context.map().centerEase(d.loc);\n\n const selectedImageId = service.getActiveImage() && service.getActiveImage().id;\n\n service.getDetections(d.id).then(detections => {\n if (detections.length) {\n const imageId = detections[0].image.id;\n if (imageId === selectedImageId) {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId);\n } else {\n service.ensureViewerLoaded(context)\n .then(function() {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId)\n .showViewer(context);\n });\n }\n }\n });\n }\n\n\n function filterData(detectedFeatures) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n\n if (fromDate) {\n var fromTimestamp = new Date(fromDate).getTime();\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.last_seen_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate) {\n var toTimestamp = new Date(toDate).getTime();\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.first_seen_at).getTime() <= toTimestamp;\n });\n }\n\n return detectedFeatures;\n }\n\n\n function update() {\n const service = getService();\n let data = (service ? service.signs(projection) : []);\n data = filterData(data);\n\n const transform = svgPointTransform(projection);\n\n const signs = layer.selectAll('.icon-sign')\n .data(data, function(d) { return d.id; });\n\n // exit\n signs.exit()\n .remove();\n\n // enter\n const enter = signs.enter()\n .append('g')\n .attr('class', 'icon-sign icon-detected')\n .on('click', click);\n\n enter\n .append('use')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px')\n .attr('xlink:href', function(d) { return '#' + d.value; });\n\n enter\n .append('rect')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px');\n\n // update\n signs\n .merge(enter)\n .attr('transform', transform);\n }\n\n\n function drawSigns(selection) {\n const enabled = svgMapillarySigns.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary-signs')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary-signs layer-mapillary-detections')\n .style('display', enabled ? 'block' : 'none')\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadSigns(projection);\n service.showSignDetections(true);\n } else {\n editOff();\n }\n } else if (service) {\n service.showSignDetections(false);\n }\n }\n\n\n drawSigns.enabled = function(_) {\n if (!arguments.length) return svgMapillarySigns.enabled;\n svgMapillarySigns.enabled = _;\n if (svgMapillarySigns.enabled) {\n showLayer();\n context.photos().on('change.mapillary_signs', update);\n } else {\n hideLayer();\n context.photos().on('change.mapillary_signs', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawSigns.supported = function() {\n return !!getService();\n };\n\n drawSigns.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawSigns;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\nimport { t } from '../core/localizer';\n\nexport function svgMapillaryMapFeatures(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const minZoom = 12;\n let layer = d3_select(null);\n let _mapillary;\n\n\n function init() {\n if (svgMapillaryMapFeatures.initialized) return; // run once\n svgMapillaryMapFeatures.enabled = false;\n svgMapillaryMapFeatures.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('loadedMapFeatures', throttledRedraw);\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n return _mapillary;\n }\n\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n service.loadObjectResources(context);\n editOn();\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n editOff();\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.icon-map-feature').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, d) {\n const service = getService();\n if (!service) return;\n\n context.map().centerEase(d.loc);\n\n const selectedImageId = service.getActiveImage() && service.getActiveImage().id;\n\n service.getDetections(d.id).then(detections => {\n if (detections.length) {\n const imageId = detections[0].image.id;\n if (imageId === selectedImageId) {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId);\n } else {\n service.ensureViewerLoaded(context)\n .then(function() {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId)\n .showViewer(context);\n });\n }\n }\n });\n }\n\n\n function filterData(detectedFeatures) {\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n\n if (fromDate) {\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.last_seen_at).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate) {\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.first_seen_at).getTime() <= new Date(toDate).getTime();\n });\n }\n\n return detectedFeatures;\n }\n\n\n function update() {\n const service = getService();\n let data = (service ? service.mapFeatures(projection) : []);\n data = filterData(data);\n\n const transform = svgPointTransform(projection);\n\n const mapFeatures = layer.selectAll('.icon-map-feature')\n .data(data, function(d) { return d.id; });\n\n // exit\n mapFeatures.exit()\n .remove();\n\n // enter\n const enter = mapFeatures.enter()\n .append('g')\n .attr('class', 'icon-map-feature icon-detected')\n .on('click', click);\n\n enter\n .append('title')\n .text(function(d) {\n var id = d.value.replace(/--/g, '.').replace(/-/g, '_');\n return t('mapillary_map_features.' + id);\n });\n\n enter\n .append('use')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px')\n .attr('xlink:href', function(d) {\n if (d.value === 'object--billboard') {\n // no billboard icon right now, so use the advertisement icon\n return '#object--sign--advertisement';\n }\n return '#' + d.value;\n });\n\n enter\n .append('rect')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px');\n\n // update\n mapFeatures\n .merge(enter)\n .attr('transform', transform);\n }\n\n\n function drawMapFeatures(selection) {\n const enabled = svgMapillaryMapFeatures.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary-map-features')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary-map-features layer-mapillary-detections')\n .style('display', enabled ? 'block' : 'none')\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadMapFeatures(projection);\n service.showFeatureDetections(true);\n } else {\n editOff();\n }\n } else if (service) {\n service.showFeatureDetections(false);\n }\n }\n\n\n drawMapFeatures.enabled = function(_) {\n if (!arguments.length) return svgMapillaryMapFeatures.enabled;\n svgMapillaryMapFeatures.enabled = _;\n if (svgMapillaryMapFeatures.enabled) {\n showLayer();\n context.photos().on('change.mapillary_map_features', update);\n } else {\n hideLayer();\n context.photos().on('change.mapillary_map_features', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawMapFeatures.supported = function() {\n return !!getService();\n };\n\n drawMapFeatures.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawMapFeatures;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgKartaviewImages(projection, context, dispatch) {\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var minZoom = 12;\n var minMarkerZoom = 16;\n var minViewfieldZoom = 18;\n var layer = d3_select(null);\n var _kartaview;\n\n\n function init() {\n if (svgKartaviewImages.initialized) return; // run once\n svgKartaviewImages.enabled = false;\n svgKartaviewImages.initialized = true;\n }\n\n\n function getService() {\n if (services.kartaview && !_kartaview) {\n _kartaview = services.kartaview;\n _kartaview.event.on('loadedImages', throttledRedraw);\n } else if (!services.kartaview && _kartaview) {\n _kartaview = null;\n }\n\n return _kartaview;\n }\n\n\n function showLayer() {\n var service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, d) {\n var service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service.selectImage(context, d.key)\n .showViewer(context);\n });\n\n context.map().centerEase(d.loc);\n }\n\n\n function mouseover(d3_event, d) {\n var service = getService();\n if (service) service.setStyles(context, d);\n }\n\n\n function mouseout() {\n var service = getService();\n if (service) service.setStyles(context, null);\n }\n\n\n function transform(d) {\n var t = svgPointTransform(projection)(d);\n if (d.ca) {\n t += ' rotate(' + Math.floor(d.ca) + ',0,0)';\n }\n return t;\n }\n\n\n function filterImages(images, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n images = images.filter(function(item) {\n return new Date(item.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n images = images.filter(function(item) {\n return new Date(item.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n images = images.filter(function(item) {\n return usernames.indexOf(item.captured_by) !== -1;\n });\n }\n\n return images;\n }\n\n function filterSequences(sequences, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n sequences = sequences.filter(function(sequence) {\n return usernames.indexOf(sequence.properties.captured_by) !== -1;\n });\n }\n\n return sequences;\n }\n\n function update() {\n var viewer = context.container().select('.photoviewer');\n var selected = viewer.empty() ? undefined : viewer.datum();\n\n var z = ~~context.map().zoom();\n var showMarkers = (z >= minMarkerZoom);\n var showViewfields = (z >= minViewfieldZoom);\n\n const service = getService();\n\n let sequences = (service ? service.sequences(projection) : []);\n let images = (service && showMarkers ? service.images(projection) : []);\n dispatch.call('photoDatesChanged', this, 'kartaview', [\n ...filterImages(images, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(s => s.properties.captured_at)]);\n sequences = filterSequences(sequences);\n images = filterImages(images);\n\n var traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.key; });\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n var groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, function(d) { return d.key; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n var markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n return (a === selected) ? 1\n : (b === selected) ? -1\n : b.loc[1] - a.loc[1]; // sort Y\n })\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n var viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter() // viewfields may or may not be drawn...\n .insert('path', 'circle') // but if they are, draw below the circles\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z');\n }\n\n\n function drawImages(selection) {\n var enabled = svgKartaviewImages.enabled,\n service = getService();\n\n layer = selection.selectAll('.layer-kartaview')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n var layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-kartaview')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadImages(projection);\n } else {\n dispatch.call('photoDatesChanged', this, 'kartaview', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'kartaview', []);\n }\n }\n\n\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgKartaviewImages.enabled;\n svgKartaviewImages.enabled = _;\n if (svgKartaviewImages.enabled) {\n showLayer();\n context.photos().on('change.kartaview_images', update);\n } else {\n hideLayer();\n context.photos().on('change.kartaview_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { services } from '../services';\nimport {svgPath, svgPointTransform} from './helpers';\n\n\nexport function svgMapilioImages(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const imageMinZoom = 16;\n const lineMinZoom = 10;\n const viewFieldZoomLevel = 18;\n let layer = d3_select(null);\n let _mapilio;\n let _viewerYaw = 0;\n\n function init() {\n if (svgMapilioImages.initialized) return;\n svgMapilioImages.enabled = false;\n svgMapilioImages.initialized = true;\n }\n\n function getService() {\n if (services.mapilio && !_mapilio) {\n _mapilio = services.mapilio;\n _mapilio.event\n .on('loadedImages', throttledRedraw)\n .on('loadedLines', throttledRedraw);\n } else if (!services.mapilio && _mapilio) {\n _mapilio = null;\n }\n\n return _mapilio;\n }\n\n /**\n * Filters images\n * @param {*} images\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered images\n */\n function filterImages(images, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n\n if (fromDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() <= new Date(toDate).getTime();\n });\n }\n\n return images;\n }\n\n /**\n * Filters sequences\n * @param {*} sequences\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered sequences\n */\n function filterSequences(sequences, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n\n if (fromDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.capture_time).getTime() >= new Date(fromDate).getTime().toString();\n });\n }\n if (toDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.capture_time).getTime() <= new Date(toDate).getTime().toString();\n });\n }\n\n return sequences;\n }\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n function transform(d, selectedImageId) {\n let t = svgPointTransform(projection)(d);\n let rot = d.heading || 0;\n\n if (d.id === selectedImageId) {\n rot += _viewerYaw;\n }\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n function click(d3_event, image) {\n const service = getService();\n if (!service) return;\n\n service.ensureViewerLoaded(context, image.id)\n .then(() => {\n service.selectImage(context, image.id)\n .showViewer(context);\n });\n\n context.map().centerEase(image.loc);\n }\n\n function mouseover(d3_event, image) {\n const service = getService();\n if (service) service.setStyles(context, image);\n }\n\n\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n async function update() {\n const zoom = ~~context.map().zoom();\n const showViewfields = (zoom >= viewFieldZoomLevel);\n const service = getService();\n\n let sequences = (service ? service.sequences(projection, zoom) : []);\n let images = (service && zoom >= imageMinZoom ? service.images(projection) : []);\n\n dispatch.call('photoDatesChanged', this, 'mapilio', [\n ...filterImages(images, true).map(p => p.capture_time),\n ...filterSequences(sequences, true).map(s => s.properties.capture_time)\n ]);\n\n images = await filterImages(images);\n sequences = await filterSequences(sequences);\n\n const activeImage = service.getActiveImage?.();\n const activeImageId = activeImage ? activeImage.id : null;\n\n let traces = layer\n .selectAll('.sequences')\n .selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.id; });\n\n // exit\n traces.exit().remove();\n\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n const groups = layer\n .selectAll('.markers')\n .selectAll('.viewfield-group')\n .data(images, function(d) { return d.id; });\n\n // exit\n groups.exit().remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n if (a.id === activeImageId) return 1;\n if (b.id === activeImageId) return -1;\n return a.capture_time_parsed - b.capture_time_parsed;\n })\n .attr('transform', d => transform(d, activeImageId))\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit().remove();\n\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n service.setStyles(context, null);\n\n function viewfieldPath() {\n if (this.parentNode.__data__.isPano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n }\n\n function drawImages(selection) {\n const enabled = svgMapilioImages.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapilio')\n .data(service ? [0] : []);\n\n layer.exit().remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-mapilio')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter.merge(layer);\n\n if (enabled) {\n let zoom = ~~context.map().zoom();\n if (service) {\n if (zoom >= imageMinZoom) {\n editOn();\n update();\n service.loadImages(projection);\n service.loadLines(projection, zoom);\n } else if (zoom >= lineMinZoom) {\n editOn();\n update();\n service.loadImages(projection);\n service.loadLines(projection, zoom);\n } else {\n editOff();\n dispatch.call('photoDatesChanged', this, 'mapilio', []);\n service.selectImage(context, null);\n }\n } else {\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'mapilio', []);\n }\n }\n\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgMapilioImages.enabled;\n svgMapilioImages.enabled = _;\n if (svgMapilioImages.enabled) {\n showLayer();\n context.photos().on('change.mapilio_images', update);\n } else {\n hideLayer();\n context.photos().on('change.mapilio_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= lineMinZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { services } from '../services';\nimport {svgPath, svgPointTransform} from './helpers';\n\n\nexport function svgPanoramaxImages(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const imageMinZoom = 15;\n const lineMinZoom = 10;\n const viewFieldZoomLevel = 18;\n let layer = d3_select(null);\n let _panoramax;\n let _viewerYaw = 0;\n let _activeUsernameFilter;\n let _activeIds;\n\n function init() {\n if (svgPanoramaxImages.initialized) return;\n svgPanoramaxImages.enabled = false;\n svgPanoramaxImages.initialized = true;\n }\n\n function getService() {\n if (services.panoramax && !_panoramax) {\n _panoramax = services.panoramax;\n _panoramax.event\n .on('viewerChanged', viewerChanged)\n .on('loadedLines', throttledRedraw)\n .on('loadedImages', throttledRedraw);\n } else if (!services.panoramax && _panoramax) {\n _panoramax = null;\n }\n\n return _panoramax;\n }\n\n /**\n * Filters the images given the filters on the right panel\n * @param {*} images\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered images\n */\n async function filterImages(images, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n const username = context.photos().usernames();\n\n const service = getService();\n\n if (!showsPano || !showsFlat) {\n images = images.filter(function(image) {\n if (image.isPano) return showsPano;\n return showsFlat;\n });\n }\n if (fromDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() <= new Date(toDate).getTime();\n });\n }\n if (username && service) {\n if (_activeUsernameFilter !== username) {\n _activeUsernameFilter = username;\n\n const tempIds = await service.getUserIds(username);\n\n _activeIds = {};\n tempIds.forEach(id => {\n _activeIds[id] = true;\n });\n }\n\n images = images.filter(function(image) {\n return _activeIds[image.account_id];\n });\n }\n\n return images;\n }\n\n /**\n * Filters the sequences given the filters on the right panel\n * @param {*} sequences\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered sequences\n */\n async function filterSequences(sequences, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n const username = context.photos().usernames();\n\n const service = getService();\n\n if (!showsPano || !showsFlat) {\n sequences = sequences.filter(function(sequence) {\n if (sequence.properties.type === 'equirectangular') return showsPano;\n return showsFlat;\n });\n }\n if (fromDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.date).getTime() >= new Date(fromDate).getTime().toString();\n });\n }\n if (toDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.date).getTime() <= new Date(toDate).getTime().toString();\n });\n }\n if (username && service) {\n if (_activeUsernameFilter !== username) {\n _activeUsernameFilter = username;\n\n const tempIds = await service.getUserIds(username);\n\n _activeIds = {};\n tempIds.forEach(id => {\n _activeIds[id] = true;\n });\n }\n\n sequences = sequences.filter(function(sequence) {\n return _activeIds[sequence.properties.account_id];\n });\n }\n\n return sequences;\n }\n\n /**\n * Shows the selected layer\n */\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n /**\n * Hides the selected layer\n */\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n /**\n * Updates the viewfinder for the selected image bubble based on the frame's yaw\n * @param {*} d Current Active image Data\n * @param {*} selectedImageId The selected bubble image ID\n */\n function transform(d, selectedImageId) {\n let t = svgPointTransform(projection)(d);\n let rot = d.heading;\n if (d.id === selectedImageId) {\n rot += _viewerYaw;\n }\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n /**\n * Updates the current selected image\n * @param {*} image The selected image bubble data\n */\n function click(d3_event, image) {\n const service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, image.id)\n .showViewer(context);\n });\n\n context.map().centerEase(image.loc);\n }\n\n function mouseover(d3_event, image) {\n const service = getService();\n if (service) service.setStyles(context, image);\n }\n\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n /**\n * Updates the current view, rearranging lines and bubbles.\n */\n async function update() {\n const zoom = ~~context.map().zoom();\n const showViewfields = (zoom >= viewFieldZoomLevel);\n\n const service = getService();\n let sequences = (service ? service.sequences(projection, zoom) : []);\n let images = (service && zoom >= imageMinZoom ? service.images(projection) : []);\n dispatch.call('photoDatesChanged', this, 'panoramax', [\n ...(await filterImages(images, true)).map(p => p.capture_time),\n ...(await filterSequences(sequences, true)).map(s => s.properties.date)]);\n\n images = await filterImages(images);\n sequences = await filterSequences(sequences);\n\n let traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.id; });\n\n // exit\n traces.exit()\n .remove();\n\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n const activeImageId = service.getActiveImage()?.id;\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n // active image on top\n if (a.id === activeImageId) return 1;\n if (b.id === activeImageId) return -1;\n // else: sort by capture time (newest on top)\n return a.capture_time_parsed - b.capture_time_parsed;\n })\n .attr('transform', d => transform(d, activeImageId))\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n service.setStyles(context, null);\n\n function viewfieldPath() {\n if (this.parentNode.__data__.isPano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n }\n\n function viewerChanged() {\n const service = getService();\n if (!service) return;\n\n const frame = service.photoFrame();\n if (!frame) return;\n\n // update viewfield rotation\n _viewerYaw = frame.getYaw();\n\n // avoid updating if the map is currently transformed\n // e.g. during drags or easing.\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .attr('transform', d => transform(d, d.id));\n }\n\n\n /**\n * Draws bubbles and lines on the current view\n * @param {*} selection Current HTML Selection\n */\n function drawImages(selection) {\n\n const enabled = svgPanoramaxImages.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-panoramax')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-panoramax')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n let zoom = ~~context.map().zoom();\n if (service){\n if (zoom >= imageMinZoom) {\n editOn();\n update();\n service.loadImages(projection);\n } else if (zoom >= lineMinZoom) {\n editOn();\n update();\n service.loadLines(projection, zoom);\n } else {\n editOff();\n dispatch.call('photoDatesChanged', this, 'panoramax', []);\n }\n } else {\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'panoramax', []);\n }\n }\n\n /**\n * @returns if layer is active\n */\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgPanoramaxImages.enabled;\n svgPanoramaxImages.enabled = _;\n if (svgPanoramaxImages.enabled) {\n showLayer();\n context.photos().on('change.panoramax_images', update);\n } else {\n hideLayer();\n context.photos().on('change.panoramax_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n /**\n * @returns if layer is drawn\n */\n drawImages.rendered = function(zoom) {\n return zoom >= lineMinZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "export function svgOsm(projection, context, dispatch) {\n var enabled = true;\n\n\n function drawOsm(selection) {\n selection.selectAll('.layer-osm')\n .data(['covered', 'areas', 'lines', 'points', 'auxiliary', 'labels'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'layer-osm ' + d; });\n\n selection.selectAll('.layer-osm.points').selectAll('.points-group')\n .data(['vertices', 'midpoints', 'points', 'turns'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'points-group ' + d; });\n }\n\n\n function showLayer() {\n var layer = context.surface().selectAll('.data-layer.osm');\n layer.interrupt();\n\n layer\n .classed('disabled', false)\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end interrupt', function () {\n dispatch.call('change');\n });\n }\n\n\n function hideLayer() {\n var layer = context.surface().selectAll('.data-layer.osm');\n layer.interrupt();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end interrupt', function () {\n layer.classed('disabled', true);\n dispatch.call('change');\n });\n }\n\n\n drawOsm.enabled = function(val) {\n if (!arguments.length) return enabled;\n enabled = val;\n\n if (enabled) {\n showLayer();\n } else {\n hideLayer();\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n return drawOsm;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { modeBrowse } from '../modes/browse';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\nimport { utilStringQs } from '../util';\n\nvar hash = utilStringQs(window.location.hash);\n\nvar _notesEnabled = !!hash.notes;\nvar _osmService;\n\n\nexport function svgNotes(projection, context, dispatch) {\n if (!dispatch) { dispatch = d3_dispatch('change'); }\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var minZoom = 12;\n var touchLayer = d3_select(null);\n var drawLayer = d3_select(null);\n var _notesVisible = false;\n\n\n function markerPath(selection, klass) {\n selection\n .attr('class', klass)\n .attr('transform', 'translate(-8, -22)')\n .attr('d', 'm17.5,0l-15,0c-1.37,0 -2.5,1.12 -2.5,2.5l0,11.25c0,1.37 1.12,2.5 2.5,2.5l3.75,0l0,3.28c0,0.38 0.43,0.6 0.75,0.37l4.87,-3.65l5.62,0c1.37,0 2.5,-1.12 2.5,-2.5l0,-11.25c0,-1.37 -1.12,-2.5 -2.5,-2.5z');\n }\n\n\n // Loosely-coupled osm service for fetching notes.\n function getService() {\n if (services.osm && !_osmService) {\n _osmService = services.osm;\n _osmService.on('loadedNotes', throttledRedraw);\n } else if (!services.osm && _osmService) {\n _osmService = null;\n }\n\n return _osmService;\n }\n\n\n // Show the notes\n function editOn() {\n if (!_notesVisible) {\n _notesVisible = true;\n drawLayer\n .style('display', 'block');\n }\n }\n\n\n // Immediately remove the notes and their touch targets\n function editOff() {\n if (_notesVisible) {\n _notesVisible = false;\n drawLayer\n .style('display', 'none');\n drawLayer.selectAll('.note')\n .remove();\n touchLayer.selectAll('.note')\n .remove();\n }\n }\n\n\n // Enable the layer. This shows the notes and transitions them to visible.\n function layerOn() {\n editOn();\n\n drawLayer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end interrupt', function () {\n dispatch.call('change');\n });\n }\n\n\n // Disable the layer. This transitions the layer invisible and then hides the notes.\n function layerOff() {\n throttledRedraw.cancel();\n drawLayer.interrupt();\n touchLayer.selectAll('.note')\n .remove();\n\n drawLayer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end interrupt', function () {\n editOff();\n dispatch.call('change');\n });\n }\n\n\n // Update the note markers\n function updateMarkers() {\n if (!_notesVisible || !_notesEnabled) return;\n\n var service = getService();\n var selectedID = context.selectedNoteID();\n var data = (service ? service.notes(projection) : []);\n var getTransform = svgPointTransform(projection);\n\n // Draw markers..\n var notes = drawLayer.selectAll('.note')\n .data(data, function(d) { return d.status + d.id; });\n\n // exit\n notes.exit()\n .remove();\n\n // enter\n var notesEnter = notes.enter()\n .append('g')\n .attr('class', function(d) { return 'note note-' + d.id + ' ' + d.status; })\n .classed('new', function(d) { return d.id < 0; });\n\n notesEnter\n .append('ellipse')\n .attr('cx', 0.5)\n .attr('cy', 1)\n .attr('rx', 6.5)\n .attr('ry', 3)\n .attr('class', 'stroke');\n\n notesEnter\n .append('path')\n .call(markerPath, 'shadow');\n\n notesEnter\n .append('use')\n .attr('class', 'note-fill')\n .attr('width', '20px')\n .attr('height', '20px')\n .attr('x', '-8px')\n .attr('y', '-22px')\n .attr('xlink:href', '#iD-icon-note');\n\n notesEnter.selectAll('.icon-annotation')\n .data(function(d) { return [d]; })\n .enter()\n .append('use')\n .attr('class', 'icon-annotation')\n .attr('width', '10px')\n .attr('height', '10px')\n .attr('x', '-3px')\n .attr('y', '-19px')\n .attr('xlink:href', function(d) {\n if (d.id < 0) return '#iD-icon-plus';\n if (d.status === 'open') return '#iD-icon-close';\n return '#iD-icon-apply';\n });\n\n // update\n notes\n .merge(notesEnter)\n .sort(sortY)\n .classed('selected', function(d) {\n var mode = context.mode();\n var isMoving = mode && mode.id === 'drag-note'; // no shadows when dragging\n return !isMoving && d.id === selectedID;\n })\n .attr('transform', getTransform);\n\n\n // Draw targets..\n if (touchLayer.empty()) return;\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n\n var targets = touchLayer.selectAll('.note')\n .data(data, function(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('rect')\n .attr('width', '20px')\n .attr('height', '20px')\n .attr('x', '-8px')\n .attr('y', '-22px')\n .merge(targets)\n .sort(sortY)\n .attr('class', function(d) {\n var newClass = (d.id < 0 ? 'new' : '');\n return 'note target note-' + d.id + ' ' + fillClass + newClass;\n })\n .attr('transform', getTransform);\n\n\n function sortY(a, b) {\n if (a.id === selectedID) return 1;\n if (b.id === selectedID) return -1;\n return b.loc[1] - a.loc[1];\n }\n }\n\n\n // Draw the notes layer and schedule loading notes and updating markers.\n function drawNotes(selection) {\n var service = getService();\n\n var surface = context.surface();\n if (surface && !surface.empty()) {\n touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');\n }\n\n drawLayer = selection.selectAll('.layer-notes')\n .data(service ? [0] : []);\n\n drawLayer.exit()\n .remove();\n\n drawLayer = drawLayer.enter()\n .append('g')\n .attr('class', 'layer-notes')\n .style('display', _notesEnabled ? 'block' : 'none')\n .merge(drawLayer);\n\n if (_notesEnabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n service.loadNotes(projection);\n updateMarkers();\n } else {\n editOff();\n }\n }\n }\n\n\n // Toggles the layer on and off\n drawNotes.enabled = function(val) {\n if (!arguments.length) return _notesEnabled;\n\n _notesEnabled = val;\n if (_notesEnabled) {\n layerOn();\n } else {\n layerOff();\n if (context.selectedNoteID()) {\n context.enter(modeBrowse(context));\n }\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n return drawNotes;\n}\n", "export function svgTouch() {\n\n function drawTouch(selection) {\n selection.selectAll('.layer-touch')\n .data(['areas', 'lines', 'points', 'turns', 'markers'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'layer-touch ' + d; });\n }\n\n return drawTouch;\n}\n", "function refresh(selection, node) {\n var cr = node.getBoundingClientRect();\n var prop = [cr.width, cr.height];\n selection.property('__dimensions__', prop);\n return prop;\n}\n\nexport function utilGetDimensions(selection, force) {\n if (!selection || selection.empty()) {\n return [0, 0];\n }\n var node = selection.node(),\n cached = selection.property('__dimensions__');\n return (!cached || force) ? refresh(selection, node) : cached;\n}\n\n\nexport function utilSetDimensions(selection, dimensions) {\n if (!selection || selection.empty()) {\n return selection;\n }\n var node = selection.node();\n if (dimensions === null) {\n refresh(selection, node);\n return selection;\n }\n return selection\n .property('__dimensions__', [dimensions[0], dimensions[1]])\n .attr('width', dimensions[0])\n .attr('height', dimensions[1]);\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { svgData } from './data';\nimport { svgLocalPhotos} from './local_photos';\nimport { svgDebug } from './debug';\nimport { svgGeolocate } from './geolocate';\nimport { svgOsmose } from './osmose';\nimport { svgStreetside } from './streetside';\nimport { svgVegbilder} from './vegbilder';\nimport { svgMapillaryImages } from './mapillary_images';\nimport { svgMapillaryPosition } from './mapillary_position';\nimport { svgMapillarySigns } from './mapillary_signs';\nimport { svgMapillaryMapFeatures } from './mapillary_map_features';\nimport { svgKartaviewImages } from './kartaview_images';\nimport { svgMapilioImages } from './mapilio_images';\nimport { svgPanoramaxImages } from './panoramax_images';\nimport { svgOsm } from './osm';\nimport { svgNotes } from './notes';\nimport { svgTouch } from './touch';\nimport { utilArrayDifference, utilRebind } from '../util';\nimport { utilGetDimensions, utilSetDimensions } from '../util/dimensions';\n\n\nexport function svgLayers(projection, context) {\n var dispatch = d3_dispatch('change', 'photoDatesChanged');\n var svg = d3_select(null);\n var _layers = [\n { id: 'osm', layer: svgOsm(projection, context, dispatch) },\n { id: 'notes', layer: svgNotes(projection, context, dispatch) },\n { id: 'data', layer: svgData(projection, context, dispatch) },\n { id: 'osmose', layer: svgOsmose(projection, context, dispatch) },\n { id: 'streetside', layer: svgStreetside(projection, context, dispatch)},\n { id: 'mapillary', layer: svgMapillaryImages(projection, context, dispatch) },\n { id: 'mapillary-position', layer: svgMapillaryPosition(projection, context, dispatch) },\n { id: 'mapillary-map-features', layer: svgMapillaryMapFeatures(projection, context, dispatch) },\n { id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) },\n { id: 'kartaview', layer: svgKartaviewImages(projection, context, dispatch) },\n { id: 'mapilio', layer: svgMapilioImages(projection, context, dispatch) },\n { id: 'vegbilder', layer: svgVegbilder(projection, context, dispatch) },\n { id: 'panoramax', layer: svgPanoramaxImages(projection, context, dispatch) },\n { id: 'local-photos', layer: svgLocalPhotos(projection, context, dispatch) },\n { id: 'debug', layer: svgDebug(projection, context, dispatch) },\n { id: 'geolocate', layer: svgGeolocate(projection, context, dispatch) },\n { id: 'touch', layer: svgTouch(projection, context, dispatch) },\n ];\n\n\n function drawLayers(selection) {\n svg = selection.selectAll('.surface')\n .data([0]);\n\n svg = svg.enter()\n .append('svg')\n .attr('class', 'surface')\n .merge(svg);\n\n var defs = svg.selectAll('.surface-defs')\n .data([0]);\n\n defs.enter()\n .append('defs')\n .attr('class', 'surface-defs');\n\n var groups = svg.selectAll('.data-layer')\n .data(_layers);\n\n groups.exit()\n .remove();\n\n groups.enter()\n .append('g')\n .attr('class', function(d) { return 'data-layer ' + d.id; })\n .merge(groups)\n .each(function(d) { d3_select(this).call(d.layer); });\n }\n\n\n drawLayers.all = function() {\n return _layers;\n };\n\n\n drawLayers.layer = function(id) {\n var obj = _layers.find(function(o) { return o.id === id; });\n return obj && obj.layer;\n };\n\n\n drawLayers.only = function(what) {\n var arr = [].concat(what);\n var all = _layers.map(function(layer) { return layer.id; });\n return drawLayers.remove(utilArrayDifference(all, arr));\n };\n\n\n drawLayers.remove = function(what) {\n var arr = [].concat(what);\n arr.forEach(function(id) {\n _layers = _layers.filter(function(o) { return o.id !== id; });\n });\n dispatch.call('change');\n return this;\n };\n\n\n drawLayers.add = function(what) {\n var arr = [].concat(what);\n arr.forEach(function(obj) {\n if ('id' in obj && 'layer' in obj) {\n _layers.push(obj);\n }\n });\n dispatch.call('change');\n return this;\n };\n\n\n drawLayers.dimensions = function(val) {\n if (!arguments.length) return utilGetDimensions(svg);\n utilSetDimensions(svg, val);\n return this;\n };\n\n\n return utilRebind(drawLayers, dispatch, 'on');\n}\n", "import { deepEqual } from 'fast-equals';\nimport { range as d3_range } from 'd3-array';\n\nimport {\n svgMarkerSegments, svgPath, svgRelationMemberTags, svgSegmentWay\n} from './helpers';\nimport { svgTagClasses } from './tag_classes';\n\nimport { osmEntity } from '../osm';\nimport { utilArrayFlatten, utilArrayGroupBy } from '../util';\nimport { utilDetect } from '../util/detect';\n\n/** @param {{ [key: string ]: string }} tags */\nfunction onewayArrowColour(tags) {\n // the return value must be defined in ./defs.js\n if (tags.highway === 'construction' && tags.bridge) return 'white';\n if (tags.highway === 'pedestrian') return 'gray';\n if (tags.railway && !tags.highway) return 'gray';\n if (tags.aeroway === 'runway') return 'white';\n\n return 'black';\n}\n\nexport function svgLines(projection, context) {\n var detected = utilDetect();\n\n var highway_stack = {\n motorway: 0,\n motorway_link: 1,\n trunk: 2,\n trunk_link: 3,\n primary: 4,\n primary_link: 5,\n secondary: 6,\n tertiary: 7,\n unclassified: 8,\n residential: 9,\n service: 10,\n busway: 11,\n footway: 12\n };\n\n\n function drawTargets(selection, graph, entities, filter) {\n var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';\n var getPath = svgPath(projection).geojson;\n var activeID = context.activeID();\n var base = context.history().base();\n\n // The targets and nopes will be MultiLineString sub-segments of the ways\n var data = { targets: [], nopes: [] };\n\n entities.forEach(function(way) {\n var features = svgSegmentWay(way, graph, activeID);\n data.targets.push.apply(data.targets, features.passive);\n data.nopes.push.apply(data.nopes, features.active);\n });\n\n\n // Targets allow hover and vertex snapping\n var targetData = data.targets.filter(getPath);\n var targets = selection.selectAll('.line.target-allowed')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(targetData, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n var segmentWasEdited = function(d) {\n var wayID = d.properties.entity.id;\n // if the whole line was edited, don't draw segment changes\n if (!base.entities[wayID] ||\n !deepEqual(graph.entities[wayID].nodes, base.entities[wayID].nodes)) {\n return false;\n }\n return d.properties.nodes.some(function(n) {\n return !base.entities[n.id] ||\n !deepEqual(graph.entities[n.id].loc, base.entities[n.id].loc);\n });\n };\n\n // enter/update\n targets.enter()\n .append('path')\n .merge(targets)\n .attr('d', getPath)\n .attr('class', function(d) {\n return 'way line target target-allowed ' + targetClass + d.id;\n })\n .classed('segment-edited', segmentWasEdited);\n\n // NOPE\n var nopeData = data.nopes.filter(getPath);\n var nopes = selection.selectAll('.line.target-nope')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(nopeData, function key(d) { return d.id; });\n\n // exit\n nopes.exit()\n .remove();\n\n // enter/update\n nopes.enter()\n .append('path')\n .merge(nopes)\n .attr('d', getPath)\n .attr('class', function(d) {\n return 'way line target target-nope ' + nopeClass + d.id;\n })\n .classed('segment-edited', segmentWasEdited);\n }\n\n\n function drawLines(selection, graph, entities, filter) {\n var base = context.history().base();\n\n function waystack(a, b) {\n var selected = context.selectedIDs();\n var scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0;\n var scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0;\n\n if (a.tags.highway) { scoreA -= highway_stack[a.tags.highway]; }\n if (b.tags.highway) { scoreB -= highway_stack[b.tags.highway]; }\n return scoreA - scoreB;\n }\n\n\n function drawLineGroup(selection, klass, isSelected) {\n // Note: Don't add `.selected` class in draw modes\n var mode = context.mode();\n var isDrawing = mode && /^draw/.test(mode.id);\n var selectedClass = (!isDrawing && isSelected) ? 'selected ' : '';\n\n var lines = selection\n .selectAll('path')\n .filter(filter)\n .data(getPathData(isSelected), osmEntity.key);\n\n lines.exit()\n .remove();\n\n // Optimization: Call expensive TagClasses only on enter selection. This\n // works because osmEntity.key is defined to include the entity v attribute.\n lines.enter()\n .append('path')\n .attr('class', function(d) {\n\n var prefix = 'way line';\n\n // if this line isn't styled by its own tags\n if (!d.hasInterestingTags()) {\n\n var parentRelations = graph.parentRelations(d);\n var parentMultipolygons = parentRelations.filter(function(relation) {\n return relation.isMultipolygon();\n });\n\n // and if it's a member of at least one multipolygon relation\n if (parentMultipolygons.length > 0 &&\n // and only multipolygon relations\n parentRelations.length === parentMultipolygons.length) {\n // then fudge the classes to style this as an area edge\n prefix = 'relation area';\n }\n }\n\n var oldMPClass = oldMultiPolygonOuters[d.id] ? 'old-multipolygon ' : '';\n return prefix + ' ' + klass + ' ' + selectedClass + oldMPClass + d.id;\n })\n .classed('added', function(d) {\n return !base.entities[d.id];\n })\n .classed('geometry-edited', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].nodes, base.entities[d.id].nodes);\n })\n .classed('retagged', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses())\n .merge(lines)\n .sort(waystack)\n .attr('d', getPath)\n .call(svgTagClasses().tags(svgRelationMemberTags(graph)));\n\n return selection;\n }\n\n\n function getPathData(isSelected) {\n return function() {\n var layer = this.parentNode.__data__;\n var data = pathdata[layer] || [];\n return data.filter(function(d) {\n if (isSelected) {\n return context.selectedIDs().indexOf(d.id) !== -1;\n } else {\n return context.selectedIDs().indexOf(d.id) === -1;\n }\n });\n };\n }\n\n function addMarkers(layergroup, pathclass, groupclass, groupdata, marker) {\n var markergroup = layergroup\n .selectAll('g.' + groupclass)\n .data([pathclass]);\n\n markergroup = markergroup.enter()\n .append('g')\n .attr('class', groupclass)\n .merge(markergroup);\n\n var markers = markergroup\n .selectAll('path')\n .filter(filter)\n .data(\n function data() { return groupdata[this.parentNode.__data__] || []; },\n function key(d) { return [d.id, d.index]; }\n );\n\n markers.exit()\n .remove();\n\n markers = markers.enter()\n .append('path')\n .attr('class', pathclass)\n .merge(markers)\n .attr('marker-mid', marker)\n .attr('d', function(d) { return d.d; });\n\n if (detected.ie) {\n markers.each(function() { this.parentNode.insertBefore(this, this); });\n }\n }\n\n\n var getPath = svgPath(projection, graph);\n var ways = [];\n var onewaydata = {};\n var sideddata = {};\n var oldMultiPolygonOuters = {};\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n if (entity.geometry(graph) === 'line'\n // to render side-markers for coastlines (see\n // https://github.com/openstreetmap/iD/issues/9293)\n || entity.geometry(graph) === 'area' && entity.sidednessIdentifier\n && entity.sidednessIdentifier() === 'coastline') {\n ways.push(entity);\n }\n }\n\n ways = ways.filter(getPath);\n const pathdata = utilArrayGroupBy(ways, (way) => Math.trunc(way.layer()));\n\n Object.keys(pathdata).forEach(function(k) {\n var v = pathdata[k];\n var onewayArr = v.filter(function(d) { return d.isOneWay(); });\n var onewaySegments = svgMarkerSegments(\n projection, graph, 36,\n entity => entity.isOneWayBackwards(),\n entity => entity.isBiDirectional(),\n );\n onewaydata[k] = utilArrayFlatten(onewayArr.map(onewaySegments));\n\n var sidedArr = v.filter(function(d) { return d.isSided(); });\n var sidedSegments = svgMarkerSegments(\n projection, graph, 30\n );\n sideddata[k] = utilArrayFlatten(sidedArr.map(sidedSegments));\n });\n\n\n var covered = selection.selectAll('.layer-osm.covered'); // under areas\n var uncovered = selection.selectAll('.layer-osm.lines'); // over areas\n var touchLayer = selection.selectAll('.layer-touch.lines');\n\n // Draw lines..\n [covered, uncovered].forEach(function(selection) {\n var range = (selection === covered ? d3_range(-10,0) : d3_range(0,11));\n var layergroup = selection\n .selectAll('g.layergroup')\n .data(range);\n\n layergroup = layergroup.enter()\n .append('g')\n .attr('class', function(d) { return 'layergroup layer' + String(d); })\n .merge(layergroup);\n\n layergroup\n .selectAll('g.linegroup')\n .data(['shadow', 'casing', 'stroke', 'shadow-highlighted', 'casing-highlighted', 'stroke-highlighted'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'linegroup line-' + d; });\n\n layergroup.selectAll('g.line-shadow')\n .call(drawLineGroup, 'shadow', false);\n layergroup.selectAll('g.line-casing')\n .call(drawLineGroup, 'casing', false);\n layergroup.selectAll('g.line-stroke')\n .call(drawLineGroup, 'stroke', false);\n\n layergroup.selectAll('g.line-shadow-highlighted')\n .call(drawLineGroup, 'shadow', true);\n layergroup.selectAll('g.line-casing-highlighted')\n .call(drawLineGroup, 'casing', true);\n layergroup.selectAll('g.line-stroke-highlighted')\n .call(drawLineGroup, 'stroke', true);\n\n addMarkers(layergroup, 'oneway', 'onewaygroup', onewaydata, (d) => {\n const category = onewayArrowColour(graph.entity(d.id).tags);\n return `url(#ideditor-oneway-marker-${category})`;\n });\n addMarkers(layergroup, 'sided', 'sidedgroup', sideddata,\n function marker(d) {\n var category = graph.entity(d.id).sidednessIdentifier();\n return 'url(#ideditor-sided-marker-' + category + ')';\n }\n );\n });\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, ways, filter);\n }\n\n\n return drawLines;\n}\n", "import { svgPointTransform } from './helpers';\nimport { svgTagClasses } from './tag_classes';\nimport { geoAngle, geoLineIntersection, geoVecInterp, geoVecLength } from '../geo';\n\n\nexport function svgMidpoints(projection, context) {\n var targetRadius = 8;\n\n function drawTargets(selection, graph, entities, filter) {\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var getTransform = svgPointTransform(projection).geojson;\n\n var data = entities.map(function(midpoint) {\n return {\n type: 'Feature',\n id: midpoint.id,\n properties: {\n target: true,\n entity: midpoint\n },\n geometry: {\n type: 'Point',\n coordinates: midpoint.loc\n }\n };\n });\n\n var targets = selection.selectAll('.midpoint.target')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(data, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('circle')\n .attr('r', targetRadius)\n .merge(targets)\n .attr('class', function(d) { return 'node midpoint target ' + fillClass + d.id; })\n .attr('transform', getTransform);\n }\n\n\n function drawMidpoints(selection, graph, entities, filter, extent) {\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.midpoints');\n var touchLayer = selection.selectAll('.layer-touch.points');\n\n var mode = context.mode();\n if ((mode && mode.id !== 'select') || !context.map().withinEditableZoom()) {\n drawLayer.selectAll('.midpoint').remove();\n touchLayer.selectAll('.midpoint.target').remove();\n return;\n }\n\n var poly = extent.polygon();\n var midpoints = {};\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n\n if (entity.type !== 'way') continue;\n if (!filter(entity)) continue;\n if (context.selectedIDs().indexOf(entity.id) < 0) continue;\n\n var nodes = graph.childNodes(entity);\n for (var j = 0; j < nodes.length - 1; j++) {\n var a = nodes[j];\n var b = nodes[j + 1];\n var id = [a.id, b.id].sort().join('-');\n\n if (midpoints[id]) {\n midpoints[id].parents.push(entity);\n } else if (geoVecLength(projection(a.loc), projection(b.loc)) > 40) {\n var point = geoVecInterp(a.loc, b.loc, 0.5);\n var loc = null;\n\n if (extent.intersects(point)) {\n loc = point;\n } else {\n for (var k = 0; k < 4; k++) {\n point = geoLineIntersection([a.loc, b.loc], [poly[k], poly[k + 1]]);\n if (point &&\n geoVecLength(projection(a.loc), projection(point)) > 20 &&\n geoVecLength(projection(b.loc), projection(point)) > 20) {\n loc = point;\n break;\n }\n }\n }\n\n if (loc) {\n midpoints[id] = {\n type: 'midpoint',\n id: id,\n loc: loc,\n edge: [a.id, b.id],\n parents: [entity]\n };\n }\n }\n }\n }\n\n\n function midpointFilter(d) {\n if (midpoints[d.id]) return true;\n\n for (var i = 0; i < d.parents.length; i++) {\n if (filter(d.parents[i])) {\n return true;\n }\n }\n\n return false;\n }\n\n\n var groups = drawLayer.selectAll('.midpoint')\n .filter(midpointFilter)\n .data(Object.values(midpoints), function(d) { return d.id; });\n\n groups.exit()\n .remove();\n\n var enter = groups.enter()\n .insert('g', ':first-child')\n .attr('class', 'midpoint');\n\n enter\n .append('polygon')\n .attr('points', '-6,8 10,0 -6,-8')\n .attr('class', 'shadow');\n\n enter\n .append('polygon')\n .attr('points', '-3,4 5,0 -3,-4')\n .attr('class', 'fill');\n\n groups = groups\n .merge(enter)\n .attr('transform', function(d) {\n var translate = svgPointTransform(projection);\n var a = graph.entity(d.edge[0]);\n var b = graph.entity(d.edge[1]);\n var angle = geoAngle(a, b, projection) * (180 / Math.PI);\n return translate(d) + ' rotate(' + angle + ')';\n })\n .call(svgTagClasses().tags(\n function(d) { return d.parents[0].tags; }\n ));\n\n // Propagate data bindings.\n groups.select('polygon.shadow');\n groups.select('polygon.fill');\n\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, Object.values(midpoints), midpointFilter);\n }\n\n return drawMidpoints;\n}\n", "import { deepEqual } from 'fast-equals';\nimport { clamp } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3';\n\nimport { geoScaleToZoom } from '../geo';\nimport { osmEntity } from '../osm';\nimport { svgPointTransform } from './helpers';\nimport { svgTagClasses } from './tag_classes';\nimport { presetManager } from '../presets';\nimport { textWidth, isAddressPoint } from './labels';\n\nexport function svgPoints(projection, context) {\n\n function markerPath(selection, klass) {\n selection\n .attr('class', klass)\n .attr('transform', d => isAddressPoint(d.tags)\n ? `translate(-${addressShieldWidth(d, selection)/2}, -8)`\n : 'translate(-8, -23)')\n .attr('d', d => {\n if (!isAddressPoint(d.tags)) {\n return 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z';\n }\n const w = addressShieldWidth(d, selection);\n return `M ${w-3.5} 0 a 3.5 3.5 0 0 0 3.5 3.5 l 0 9 a 3.5 3.5 0 0 0 -3.5 3.5 l -${w-7} 0 a 3.5 3.5 0 0 0 -3.5 -3.5 l 0 -9 a 3.5 3.5 0 0 0 3.5 -3.5 z`;\n });\n }\n\n function sortY(a, b) {\n return b.loc[1] - a.loc[1];\n }\n\n function addressShieldWidth(d, selection) {\n const width = textWidth(d.tags['addr:housenumber'] || d.tags['addr:housename'] || '', 10, selection.node().parentElement);\n return clamp(width, 10, 34) + 8;\n };\n\n // Avoid exit/enter if we're just moving stuff around.\n // The node will get a new version but we only need to run the update selection.\n function fastEntityKey(d) {\n const mode = context.mode();\n const isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);\n return isMoving ? d.id : osmEntity.key(d);\n }\n\n\n function drawTargets(selection, graph, entities, filter) {\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var getTransform = svgPointTransform(projection).geojson;\n var activeID = context.activeID();\n var data = [];\n\n entities.forEach(function(node) {\n if (activeID === node.id) return; // draw no target on the activeID\n\n data.push({\n type: 'Feature',\n id: node.id,\n properties: {\n target: true,\n entity: node,\n isAddr: isAddressPoint(node.tags)\n },\n geometry: node.asGeoJSON()\n });\n });\n\n var targets = selection.selectAll('.point.target')\n .filter(d => filter(d.properties.entity))\n .data(data, d => fastEntityKey(d.properties.entity));\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('rect')\n .attr('x', d => d.properties.isAddr ? -addressShieldWidth(d.properties.entity, selection) / 2 : -10)\n .attr('y', d => d.properties.isAddr ? -8 : -26)\n .attr('width', d => d.properties.isAddr ? addressShieldWidth(d.properties.entity, selection) : 20)\n .attr('height', d => d.properties.isAddr ? 16 : 30)\n .attr('class', function(d) { return 'node point target ' + fillClass + d.id; })\n .merge(targets)\n .attr('transform', getTransform);\n }\n\n\n function drawPoints(selection, graph, entities, filter) {\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n var base = context.history().base();\n\n // Points with a direction will render as vertices at higher zooms..\n function renderAsPoint(entity) {\n return entity.geometry(graph) === 'point' &&\n !(zoom >= 18 && entity.directions(graph, projection).length);\n }\n\n // All points will render as vertices in wireframe mode too..\n var points = wireframe ? [] : entities.filter(renderAsPoint);\n points.sort(sortY);\n\n\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.points');\n var touchLayer = selection.selectAll('.layer-touch.points');\n\n // Draw points..\n var groups = drawLayer.selectAll('g.point')\n .filter(filter)\n .data(points, fastEntityKey);\n\n groups.exit()\n .remove();\n\n var enter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'node point ' + d.id; })\n .order();\n\n enter\n .append('path')\n .call(markerPath, 'shadow');\n\n enter.each(function(d) {\n if (isAddressPoint(d.tags)) return;\n d3_select(this)\n .append('ellipse')\n .attr('cx', 0.5)\n .attr('cy', 1)\n .attr('rx', 6.5)\n .attr('ry', 3)\n .attr('class', 'stroke');\n });\n\n enter\n .append('path')\n .call(markerPath, 'stroke');\n\n enter\n .append('use')\n .attr('transform', 'translate(-5.5, -20)')\n .attr('class', 'icon')\n .attr('width', '12px')\n .attr('height', '12px');\n\n groups = groups\n .merge(enter)\n .attr('transform', svgPointTransform(projection))\n .classed('added', function(d) {\n return !base.entities[d.id]; // if it doesn't exist in the base graph, it's new\n })\n .classed('moved', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].loc, base.entities[d.id].loc);\n })\n .classed('retagged', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses());\n\n groups.select('.shadow'); // propagate bound data\n groups.select('.stroke'); // propagate bound data\n groups.select('.icon') // propagate bound data\n .attr('xlink:href', function(entity) {\n var preset = presetManager.match(entity, graph);\n var picon = preset && preset.icon;\n return picon ? '#' + picon : '';\n });\n\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, points, filter);\n }\n\n\n return drawPoints;\n}\n", "import { geoAngle, geoPathLength } from '../geo';\n\n\nexport function svgTurns(projection, context) {\n\n function icon(turn) {\n var u = turn.u ? '-u' : '';\n if (turn.no) return '#iD-turn-no' + u;\n if (turn.only) return '#iD-turn-only' + u;\n return '#iD-turn-yes' + u;\n }\n\n function drawTurns(selection, graph, turns) {\n\n function turnTransform(d) {\n var pxRadius = 50;\n var toWay = graph.entity(d.to.way);\n var toPoints = graph.childNodes(toWay)\n .map(function (n) { return n.loc; })\n .map(projection);\n var toLength = geoPathLength(toPoints);\n var mid = toLength / 2; // midpoint of destination way\n\n var toNode = graph.entity(d.to.node);\n var toVertex = graph.entity(d.to.vertex);\n var a = geoAngle(toVertex, toNode, projection);\n var o = projection(toVertex.loc);\n var r = d.u ? 0 // u-turn: no radius\n : !toWay.__via ? pxRadius // leaf way: put marker at pxRadius\n : Math.min(mid, pxRadius); // via way: prefer pxRadius, fallback to mid for very short ways\n\n return 'translate(' + (r * Math.cos(a) + o[0]) + ',' + (r * Math.sin(a) + o[1]) + ') ' +\n 'rotate(' + a * 180 / Math.PI + ')';\n }\n\n\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.turns');\n var touchLayer = selection.selectAll('.layer-touch.turns');\n\n // Draw turns..\n var groups = drawLayer.selectAll('g.turn')\n .data(turns, function(d) { return d.key; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var groupsEnter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'turn ' + d.key; });\n\n var turnsEnter = groupsEnter\n .filter(function(d) { return !d.u; });\n\n turnsEnter.append('rect')\n .attr('transform', 'translate(-22, -12)')\n .attr('width', '44')\n .attr('height', '24');\n\n turnsEnter.append('use')\n .attr('transform', 'translate(-22, -12)')\n .attr('width', '44')\n .attr('height', '24');\n\n var uEnter = groupsEnter\n .filter(function(d) { return d.u; });\n\n uEnter.append('circle')\n .attr('r', '16');\n\n uEnter.append('use')\n .attr('transform', 'translate(-16, -16)')\n .attr('width', '32')\n .attr('height', '32');\n\n // update\n groups = groups\n .merge(groupsEnter)\n .attr('opacity', function(d) { return d.direct === false ? '0.7' : null; })\n .attr('transform', turnTransform);\n\n groups.select('use')\n .attr('xlink:href', icon);\n\n groups.select('rect'); // propagate bound data\n groups.select('circle'); // propagate bound data\n\n\n // Draw touch targets..\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n groups = touchLayer.selectAll('g.turn')\n .data(turns, function(d) { return d.key; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n groupsEnter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'turn ' + d.key; });\n\n turnsEnter = groupsEnter\n .filter(function(d) { return !d.u; });\n\n turnsEnter.append('rect')\n .attr('class', 'target ' + fillClass)\n .attr('transform', 'translate(-22, -12)')\n .attr('width', '44')\n .attr('height', '24');\n\n uEnter = groupsEnter\n .filter(function(d) { return d.u; });\n\n uEnter.append('circle')\n .attr('class', 'target ' + fillClass)\n .attr('r', '16');\n\n // update\n groups = groups\n .merge(groupsEnter)\n .attr('transform', turnTransform);\n\n groups.select('rect'); // propagate bound data\n groups.select('circle'); // propagate bound data\n\n\n return this;\n }\n\n return drawTurns;\n}\n", "import { deepEqual } from 'fast-equals';\nimport { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { geoScaleToZoom } from '../geo';\nimport { osmEntity } from '../osm';\nimport { svgPassiveVertex, svgPointTransform } from './helpers';\nimport { svgTagClasses } from './tag_classes';\n\nexport function svgVertices(projection, context) {\n var radiuses = {\n // z16-, z17, z18+, w/icon\n shadow: [6, 7.5, 7.5, 12],\n stroke: [2.5, 3.5, 3.5, 8],\n fill: [1, 1.5, 1.5, 1.5]\n };\n\n var _currHoverTarget;\n var _currPersistent = {};\n var _currHover = {};\n var _prevHover = {};\n var _currSelected = {};\n var _prevSelected = {};\n var _radii = {};\n\n\n function sortY(a, b) {\n return b.loc[1] - a.loc[1];\n }\n\n // Avoid exit/enter if we're just moving stuff around.\n // The node will get a new version but we only need to run the update selection.\n function fastEntityKey(d) {\n var mode = context.mode();\n var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);\n return isMoving ? d.id : osmEntity.key(d);\n }\n\n\n function draw(selection, graph, vertices, sets, filter) {\n sets = sets || { selected: {}, important: {}, hovered: {} };\n\n var icons = {};\n var directions = {};\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2);\n var activeID = context.activeID();\n var base = context.history().base();\n\n\n function getIcon(d) {\n // always check latest entity, as fastEntityKey avoids enter/exit now\n var entity = graph.entity(d.id);\n if (entity.id in icons) return icons[entity.id];\n\n icons[entity.id] =\n entity.hasInterestingTags() &&\n presetManager.match(entity, graph).icon;\n\n return icons[entity.id];\n }\n\n\n // memoize directions results, return false for empty arrays (for use in filter)\n function getDirections(entity) {\n if (entity.id in directions) return directions[entity.id];\n\n var angles = entity.directions(graph, projection);\n directions[entity.id] = angles.length ? angles : false;\n return angles;\n }\n\n\n function updateAttributes(selection) {\n ['shadow', 'stroke', 'fill'].forEach(function(klass) {\n var rads = radiuses[klass];\n selection.selectAll('.' + klass)\n .each(function(entity) {\n var i = z && getIcon(entity);\n var r = rads[i ? 3 : z];\n\n // slightly increase the size of unconnected endpoints #3775\n if (entity.id !== activeID && entity.isEndpoint(graph) && !entity.isConnected(graph)) {\n r += 1.5;\n }\n\n if (klass === 'shadow') { // remember this value, so we don't need to\n _radii[entity.id] = r; // recompute it when we draw the touch targets\n }\n\n d3_select(this)\n .attr('r', r)\n .attr('visibility', (i && klass === 'fill') ? 'hidden' : null);\n });\n });\n }\n\n vertices.sort(sortY);\n\n var groups = selection.selectAll('g.vertex')\n .filter(filter)\n .data(vertices, fastEntityKey);\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var enter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'node vertex ' + d.id; })\n .order();\n\n enter\n .append('circle')\n .attr('class', 'shadow');\n\n enter\n .append('circle')\n .attr('class', 'stroke');\n\n // Vertices with tags get a fill.\n enter.filter(function(d) { return d.hasInterestingTags(); })\n .append('circle')\n .attr('class', 'fill');\n\n // update\n groups = groups\n .merge(enter)\n .attr('transform', svgPointTransform(projection))\n .classed('sibling', function(d) { return d.id in sets.selected; })\n .classed('shared', function(d) { return graph.isShared(d); })\n .classed('endpoint', function(d) { return d.isEndpoint(graph); })\n .classed('added', function(d) {\n return !base.entities[d.id]; // if it doesn't exist in the base graph, it's new\n })\n .classed('moved', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].loc, base.entities[d.id].loc);\n })\n .classed('retagged', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses())\n .call(updateAttributes);\n\n // Vertices with icons get a `use`.\n var iconUse = groups\n .selectAll('.icon')\n .data(function data(d) { return zoom >= 17 && getIcon(d) ? [d] : []; }, fastEntityKey);\n\n // exit\n iconUse.exit()\n .remove();\n\n // enter\n iconUse.enter()\n .append('use')\n .attr('class', 'icon')\n .attr('width', '12px')\n .attr('height', '12px')\n .attr('transform', 'translate(-6, -6)')\n .attr('xlink:href', function(d) {\n var picon = getIcon(d);\n return picon ? '#' + picon : '';\n });\n\n\n // Vertices with directions get viewfields\n var dgroups = groups\n .selectAll('.viewfieldgroup')\n .data(function data(d) { return zoom >= 18 && getDirections(d) ? [d] : []; }, fastEntityKey);\n\n // exit\n dgroups.exit()\n .remove();\n\n // enter/update\n dgroups = dgroups.enter()\n .insert('g', '.shadow')\n .attr('class', 'viewfieldgroup')\n .merge(dgroups);\n\n var viewfields = dgroups.selectAll('.viewfield')\n .data(getDirections, function key(d) { return osmEntity.key(d); });\n\n // exit\n viewfields.exit()\n .remove();\n\n // enter/update\n viewfields.enter()\n .append('path')\n .attr('class', 'viewfield')\n .attr('d', 'M0,0H0')\n .merge(viewfields)\n .attr('marker-start', d => 'url(#ideditor-viewfield-marker' + (d.type === 'side' ? '-side' : '') + (wireframe ? '-wireframe' : '') + ')')\n .attr('transform', d => `rotate(${d.angle})`);\n }\n\n\n function drawTargets(selection, graph, entities, filter) {\n var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';\n var getTransform = svgPointTransform(projection).geojson;\n var activeID = context.activeID();\n var data = { targets: [], nopes: [] };\n\n entities.forEach(function(node) {\n if (activeID === node.id) return; // draw no target on the activeID\n\n var vertexType = svgPassiveVertex(node, graph, activeID);\n if (vertexType !== 0) { // passive or adjacent - allow to connect\n data.targets.push({\n type: 'Feature',\n id: node.id,\n properties: {\n target: true,\n entity: node\n },\n geometry: node.asGeoJSON()\n });\n } else {\n data.nopes.push({\n type: 'Feature',\n id: node.id + '-nope',\n properties: {\n nope: true,\n target: true,\n entity: node\n },\n geometry: node.asGeoJSON()\n });\n }\n });\n\n // Targets allow hover and vertex snapping\n var targets = selection.selectAll('.vertex.target-allowed')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(data.targets, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('circle')\n .attr('r', function(d) {\n return _radii[d.id]\n || radiuses.shadow[3];\n })\n .merge(targets)\n .attr('class', function(d) {\n return 'node vertex target target-allowed '\n + targetClass + d.id;\n })\n .attr('transform', getTransform);\n\n\n // NOPE\n var nopes = selection.selectAll('.vertex.target-nope')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(data.nopes, function key(d) { return d.id; });\n\n // exit\n nopes.exit()\n .remove();\n\n // enter/update\n nopes.enter()\n .append('circle')\n .attr('r', function(d) { return (_radii[d.properties.entity.id] || radiuses.shadow[3]); })\n .merge(nopes)\n .attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; })\n .attr('transform', getTransform);\n }\n\n\n // Points can also render as vertices:\n // 1. in wireframe mode or\n // 2. at higher zooms if they have a direction\n function renderAsVertex(entity, graph, wireframe, zoom) {\n var geometry = entity.geometry(graph);\n return geometry === 'vertex' || (geometry === 'point' && (\n wireframe || (zoom >= 18 && entity.directions(graph, projection).length)\n ));\n }\n\n\n function isEditedNode(node, base, head) {\n var baseNode = base.entities[node.id];\n var headNode = head.entities[node.id];\n return !headNode ||\n !baseNode ||\n !deepEqual(headNode.tags, baseNode.tags) ||\n !deepEqual(headNode.loc, baseNode.loc);\n }\n\n\n function getSiblingAndChildVertices(ids, graph, wireframe, zoom) {\n var results = {};\n\n var seenIds = {};\n\n function addChildVertices(entity) {\n\n // avoid redundant work and infinite recursion of circular relations\n if (seenIds[entity.id]) return;\n seenIds[entity.id] = true;\n\n var geometry = entity.geometry(graph);\n if (!context.features().isHiddenFeature(entity, graph, geometry)) {\n var i;\n if (entity.type === 'way') {\n for (i = 0; i < entity.nodes.length; i++) {\n var child = graph.hasEntity(entity.nodes[i]);\n if (child) {\n addChildVertices(child);\n }\n }\n } else if (entity.type === 'relation') {\n for (i = 0; i < entity.members.length; i++) {\n var member = graph.hasEntity(entity.members[i].id);\n if (member) {\n addChildVertices(member);\n }\n }\n } else if (renderAsVertex(entity, graph, wireframe, zoom)) {\n results[entity.id] = entity;\n }\n }\n }\n\n ids.forEach(function(id) {\n var entity = graph.hasEntity(id);\n if (!entity) return;\n\n if (entity.type === 'node') {\n if (renderAsVertex(entity, graph, wireframe, zoom)) {\n results[entity.id] = entity;\n graph.parentWays(entity).forEach(function(entity) {\n addChildVertices(entity);\n });\n }\n } else { // way, relation\n addChildVertices(entity);\n }\n });\n\n return results;\n }\n\n\n function drawVertices(selection, graph, entities, filter, extent, fullRedraw) {\n var wireframe = context.surface().classed('fill-wireframe');\n var visualDiff = context.surface().classed('highlight-edited');\n var zoom = geoScaleToZoom(projection.scale());\n var mode = context.mode();\n var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);\n var base = context.history().base();\n\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.vertices');\n var touchLayer = selection.selectAll('.layer-touch.points');\n\n if (fullRedraw) {\n _currPersistent = {};\n _radii = {};\n }\n\n // Collect important vertices from the `entities` list..\n // (during a partial redraw, it will not contain everything)\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n var geometry = entity.geometry(graph);\n var keep = false;\n\n // a point that looks like a vertex..\n if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) {\n _currPersistent[entity.id] = entity;\n keep = true;\n\n // a vertex of some importance..\n } else if (geometry === 'vertex' &&\n (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)\n || (visualDiff && isEditedNode(entity, base, graph)))) {\n _currPersistent[entity.id] = entity;\n keep = true;\n }\n\n // whatever this is, it's not a persistent vertex..\n if (!keep && !fullRedraw) {\n delete _currPersistent[entity.id];\n }\n }\n\n // 3 sets of vertices to consider:\n var sets = {\n persistent: _currPersistent, // persistent = important vertices (render always)\n selected: _currSelected, // selected + siblings of selected (render always)\n hovered: _currHover // hovered + siblings of hovered (render only in draw modes)\n };\n\n var all = Object.assign({}, (isMoving ? _currHover : {}), _currSelected, _currPersistent);\n\n // Draw the vertices..\n // The filter function controls the scope of what objects d3 will touch (exit/enter/update)\n // Adjust the filter function to expand the scope beyond whatever entities were passed in.\n var filterRendered = function(d) {\n return d.id in _currPersistent || d.id in _currSelected || d.id in _currHover || filter(d);\n };\n drawLayer\n .call(draw, graph, currentVisible(all), sets, filterRendered);\n\n // Draw touch targets..\n // When drawing, render all targets (not just those affected by a partial redraw)\n var filterTouch = function(d) {\n return isMoving ? true : filterRendered(d);\n };\n touchLayer\n .call(drawTargets, graph, currentVisible(all), filterTouch);\n\n\n function currentVisible(which) {\n return Object.keys(which)\n .map(graph.hasEntity, graph) // the current version of this entity\n .filter(function (entity) { return entity && entity.intersects(extent, graph); });\n }\n }\n\n\n // partial redraw - only update the selected items..\n drawVertices.drawSelected = function(selection, graph, extent) {\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n\n _prevSelected = _currSelected || {};\n if (context.map().isInWideSelection()) {\n _currSelected = {};\n context.selectedIDs().forEach(function(id) {\n var entity = graph.hasEntity(id);\n if (!entity) return;\n\n if (entity.type === 'node') {\n if (renderAsVertex(entity, graph, wireframe, zoom)) {\n _currSelected[entity.id] = entity;\n }\n }\n });\n\n } else {\n _currSelected = getSiblingAndChildVertices(context.selectedIDs(), graph, wireframe, zoom);\n }\n\n // note that drawVertices will add `_currSelected` automatically if needed..\n var filter = function(d) { return d.id in _prevSelected; };\n drawVertices(selection, graph, Object.values(_prevSelected), filter, extent, false);\n };\n\n\n // partial redraw - only update the hovered items..\n drawVertices.drawHover = function(selection, graph, target, extent) {\n if (target === _currHoverTarget) return; // continue only if something changed\n\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n\n _prevHover = _currHover || {};\n _currHoverTarget = target;\n var entity = target && target.properties && target.properties.entity;\n\n if (entity) {\n _currHover = getSiblingAndChildVertices([entity.id], graph, wireframe, zoom);\n } else {\n _currHover = {};\n }\n\n // note that drawVertices will add `_currHover` automatically if needed..\n var filter = function(d) { return d.id in _prevHover; };\n drawVertices(selection, graph, Object.values(_prevHover), filter, extent, false);\n };\n\n return drawVertices;\n}\n", "export { svgAreas } from './areas.js';\nexport { svgData } from './data.js';\nexport { svgDebug } from './debug.js';\nexport { svgDefs } from './defs.js';\nexport { svgIcon } from './icon.js';\nexport { svgGeolocate } from './geolocate';\nexport { svgLabels } from './labels.js';\nexport { svgLayers } from './layers.js';\nexport { svgLines } from './lines.js';\nexport { svgMapillaryImages } from './mapillary_images.js';\nexport { svgMapillarySigns } from './mapillary_signs.js';\nexport { svgMidpoints } from './midpoints.js';\nexport { svgNotes } from './notes.js';\nexport { svgMarkerSegments } from './helpers.js';\nexport { svgKartaviewImages } from './kartaview_images.js';\nexport { svgOsm } from './osm.js';\nexport { svgPassiveVertex } from './helpers.js';\nexport { svgPath } from './helpers.js';\nexport { svgPointTransform } from './helpers.js';\nexport { svgPoints } from './points.js';\nexport { svgRelationMemberTags } from './helpers.js';\nexport { svgSegmentWay } from './helpers.js';\nexport { svgStreetside } from './streetside.js';\nexport { svgVegbilder } from './vegbilder';\nexport { svgTagClasses } from './tag_classes.js';\nexport { svgTagPattern } from './tag_pattern.js';\nexport { svgTouch } from './touch.js';\nexport { svgTurns } from './turns.js';\nexport { svgVertices } from './vertices.js';\nexport { svgMapilioImages } from './mapilio_images.js';\nexport { svgPanoramaxImages } from './panoramax_images.js';\n", "import { t } from '../core/localizer';\nimport { actionOrthogonalize } from '../actions/orthogonalize';\nimport { behaviorOperation } from '../behavior/operation';\nimport { utilGetAllNodes } from '../util';\nimport { svgPath } from '../svg';\n\n\nexport function operationOrthogonalize(context, selectedIDs) {\n var _extent;\n var _type;\n var _actions = selectedIDs.map(chooseAction).filter(Boolean);\n var _amount = _actions.length === 1 ? 'single' : 'multiple';\n var _coords = utilGetAllNodes(selectedIDs, context.graph())\n .map(function(n) { return n.loc; });\n\n\n function chooseAction(entityID) {\n\n var entity = context.entity(entityID);\n var geometry = entity.geometry(context.graph());\n\n if (!_extent) {\n _extent = entity.extent(context.graph());\n } else {\n _extent = _extent.extend(entity.extent(context.graph()));\n }\n\n // square a line/area\n if (entity.type === 'way' && new Set(entity.nodes).size > 2 ) {\n if (_type && _type !== 'feature') return null;\n _type = 'feature';\n return actionOrthogonalize(entityID, context.projection);\n\n // square a single vertex\n } else if (geometry === 'vertex') {\n if (_type && _type !== 'corner') return null;\n _type = 'corner';\n var graph = context.graph();\n var parents = graph.parentWays(entity);\n if (parents.length === 1) {\n var way = parents[0];\n if (way.nodes.indexOf(entityID) !== -1) {\n return actionOrthogonalize(way.id, context.projection, entityID);\n }\n }\n }\n\n return null;\n }\n\n\n var operation = function() {\n if (!_actions.length) return;\n\n var combinedAction = function(graph, t) {\n _actions.forEach(function(action) {\n if (!action.disabled(graph)) {\n graph = action(graph, t);\n }\n });\n return graph;\n };\n combinedAction.transitionable = true;\n\n context.perform(combinedAction, operation.annotation());\n\n window.setTimeout(function() {\n context.validator().validate();\n }, 300); // after any transition\n };\n\n\n operation.available = function() {\n return _actions.length && selectedIDs.length === _actions.length;\n };\n\n\n // don't cache this because the visible extent could change\n operation.disabled = function() {\n if (!_actions.length) return '';\n\n var actionDisableds = _actions.map(function(action) {\n return action.disabled(context.graph());\n }).filter(Boolean);\n\n if (actionDisableds.length === _actions.length) {\n // none of the features can be squared\n\n if (new Set(actionDisableds).size > 1) {\n return 'multiple_blockers';\n }\n return actionDisableds[0];\n } else if (_extent &&\n _extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = _coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n };\n\n\n operation.getAuxiliaryGeometry = function() {\n const graph = context.graph();\n return _actions.map((action, idx) => {\n if (!action.disabled(graph)) {\n const previewGraph = action(graph, t);\n const way = previewGraph.hasEntity(selectedIDs[idx]);\n const getPath = svgPath(context.projection, previewGraph, false);\n return {\n id: way.id,\n path: getPath(way),\n klass: 'preview'\n };\n } else {\n return false;\n }\n }).filter(Boolean);\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.orthogonalize.' + disable + '.' + _amount) :\n t.append('operations.orthogonalize.description.' + _type + '.' + _amount);\n };\n\n\n operation.annotation = function() {\n return t('operations.orthogonalize.annotation.' + _type, { n: _actions.length });\n };\n\n\n operation.id = 'orthogonalize';\n operation.keys = [t('operations.orthogonalize.key')];\n operation.title = t.append('operations.orthogonalize.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import { t } from '../core/localizer';\nimport { actionReflect } from '../actions/reflect';\nimport { behaviorOperation } from '../behavior/operation';\nimport { utilGetAllNodes, utilTotalExtent } from '../util/util';\nimport { svgPath } from '../svg';\n\n\nexport function operationReflectShort(context, selectedIDs) {\n return operationReflect(context, selectedIDs, 'short');\n}\n\n\nexport function operationReflectLong(context, selectedIDs) {\n return operationReflect(context, selectedIDs, 'long');\n}\n\n\nexport function operationReflect(context, selectedIDs, axis) {\n axis = axis || 'long';\n var multi = (selectedIDs.length === 1 ? 'single' : 'multiple');\n var nodes = utilGetAllNodes(selectedIDs, context.graph());\n var coords = nodes.map(function(n) { return n.loc; });\n var extent = utilTotalExtent(selectedIDs, context.graph());\n\n\n var _action = actionReflect(selectedIDs, context.projection)\n .useLongAxis(Boolean(axis === 'long'));\n\n var operation = function() {\n context.perform(_action, operation.annotation());\n\n window.setTimeout(function() {\n context.validator().validate();\n }, 300); // after any transition\n };\n\n\n operation.available = function() {\n return nodes.length >= 3;\n };\n\n\n // don't cache this because the visible extent could change\n operation.disabled = function() {\n if (extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n } else if (selectedIDs.some(incompleteRelation)) {\n return 'incomplete_relation';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n\n function incompleteRelation(id) {\n var entity = context.entity(id);\n return entity.type === 'relation' && !entity.isComplete(context.graph());\n }\n };\n\n\n operation.getAuxiliaryGeometry = function() {\n const graph = context.graph();\n const [p, q] = _action.getReflectAxis(graph);\n const previewGraph = _action(graph);\n const getPath = svgPath(context.projection, previewGraph, false);\n return [{\n id: 'axis',\n path: `M ${p[0]} ${p[1]} L ${q[0]} ${q[1]}`,\n klass: 'reflect-axis'\n }, ...selectedIDs.map(entityId => {\n const entity = previewGraph.hasEntity(entityId);\n return {\n id: entity.id,\n path: getPath(entity),\n klass: 'preview'\n };\n })];\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.reflect.' + disable + '.' + multi) :\n t.append('operations.reflect.description.' + axis + '.' + multi);\n };\n\n\n operation.annotation = function() {\n return t('operations.reflect.annotation.' + axis + '.feature', { n: selectedIDs.length });\n };\n\n\n operation.id = 'reflect-' + axis;\n operation.keys = [t('operations.reflect.key.' + axis)];\n operation.title = t.append('operations.reflect.title.' + axis);\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport {\n polygonHull as d3_polygonHull,\n polygonCentroid as d3_polygonCentroid\n} from 'd3-polygon';\n\nimport { t } from '../core/localizer';\nimport { actionRotate } from '../actions/rotate';\nimport { actionNoop } from '../actions/noop';\nimport { behaviorEdit } from '../behavior/edit';\nimport { geoVecInterp, geoVecLength } from '../geo/vector';\nimport { modeBrowse } from './browse';\nimport { modeSelect } from './select';\n\nimport { operationCircularize } from '../operations/circularize';\nimport { operationDelete } from '../operations/delete';\nimport { operationMove } from '../operations/move';\nimport { operationOrthogonalize } from '../operations/orthogonalize';\nimport { operationReflectLong, operationReflectShort } from '../operations/reflect';\n\nimport { utilKeybinding } from '../util/keybinding';\nimport { utilFastMouse, utilGetAllNodes } from '../util/util';\n\n\nexport function modeRotate(context, entityIDs) {\n\n var _tolerancePx = 4; // see also behaviorDrag, behaviorSelect, modeMove\n\n var mode = {\n id: 'rotate',\n button: 'browse'\n };\n\n var keybinding = utilKeybinding('rotate');\n var behaviors = [\n behaviorEdit(context),\n operationCircularize(context, entityIDs).behavior,\n operationDelete(context, entityIDs).behavior,\n operationMove(context, entityIDs).behavior,\n operationOrthogonalize(context, entityIDs).behavior,\n operationReflectLong(context, entityIDs).behavior,\n operationReflectShort(context, entityIDs).behavior\n ];\n var annotation = entityIDs.length === 1 ?\n t('operations.rotate.annotation.' + context.graph().geometry(entityIDs[0])) :\n t('operations.rotate.annotation.feature', { n: entityIDs.length });\n\n var _prevGraph;\n var _prevAngle;\n var _prevTransform;\n var _pivot;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function doRotate(d3_event) {\n var fn;\n if (context.graph() !== _prevGraph) {\n fn = context.perform;\n } else {\n fn = context.replace;\n }\n\n // projection changed, recalculate _pivot\n var projection = context.projection;\n var currTransform = projection.transform();\n if (!_prevTransform ||\n currTransform.k !== _prevTransform.k ||\n currTransform.x !== _prevTransform.x ||\n currTransform.y !== _prevTransform.y) {\n\n var nodes = utilGetAllNodes(entityIDs, context.graph());\n var points = nodes.map(function(n) { return projection(n.loc); });\n _pivot = getPivot(points);\n _prevAngle = undefined;\n }\n\n\n var currMouse = context.map().mouse(d3_event);\n var currAngle = Math.atan2(currMouse[1] - _pivot[1], currMouse[0] - _pivot[0]);\n\n if (typeof _prevAngle === 'undefined') _prevAngle = currAngle;\n var delta = currAngle - _prevAngle;\n\n fn(actionRotate(entityIDs, _pivot, delta, projection));\n\n _prevTransform = currTransform;\n _prevAngle = currAngle;\n _prevGraph = context.graph();\n }\n\n function getPivot(points) {\n var _pivot;\n if (points.length === 1) {\n _pivot = points[0];\n } else if (points.length === 2) {\n _pivot = geoVecInterp(points[0], points[1], 0.5);\n } else {\n var polygonHull = d3_polygonHull(points);\n if (polygonHull.length === 2) {\n _pivot = geoVecInterp(points[0], points[1], 0.5);\n } else {\n _pivot = d3_polygonCentroid(d3_polygonHull(points));\n }\n }\n return _pivot;\n }\n\n\n function finish(d3_event) {\n d3_event.stopPropagation();\n context.replace(actionNoop(), annotation);\n context.enter(modeSelect(context, entityIDs));\n }\n\n\n function cancel() {\n if (_prevGraph) context.pop(); // remove the rotate\n context.enter(modeSelect(context, entityIDs));\n }\n\n\n function undone() {\n context.enter(modeBrowse(context));\n }\n\n\n mode.enter = function() {\n _prevGraph = null;\n context.features().forceVisible(entityIDs);\n\n behaviors.forEach(context.install);\n\n var downEvent;\n\n context.surface()\n .on(_pointerPrefix + 'down.modeRotate', function(d3_event) {\n downEvent = d3_event;\n });\n\n d3_select(window)\n .on(_pointerPrefix + 'move.modeRotate', doRotate, true)\n .on(_pointerPrefix + 'up.modeRotate', function(d3_event) {\n if (!downEvent) return;\n var mapNode = context.container().select('.main-map').node();\n var pointGetter = utilFastMouse(mapNode);\n var p1 = pointGetter(downEvent);\n var p2 = pointGetter(d3_event);\n var dist = geoVecLength(p1, p2);\n\n if (dist <= _tolerancePx) finish(d3_event);\n downEvent = null;\n }, true);\n\n context.history()\n .on('undone.modeRotate', undone);\n\n keybinding\n .on('\u238B', cancel)\n .on('\u21A9', finish);\n\n d3_select(document)\n .call(keybinding);\n };\n\n\n mode.exit = function() {\n behaviors.forEach(context.uninstall);\n\n context.surface()\n .on(_pointerPrefix + 'down.modeRotate', null);\n\n d3_select(window)\n .on(_pointerPrefix + 'move.modeRotate', null, true)\n .on(_pointerPrefix + 'up.modeRotate', null, true);\n\n context.history()\n .on('undone.modeRotate', null);\n\n d3_select(document)\n .call(keybinding.unbind);\n\n context.features().forceVisible([]);\n };\n\n\n mode.selectedIDs = function() {\n if (!arguments.length) return entityIDs;\n // no assign\n return mode;\n };\n\n\n return mode;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { t } from '../core/localizer';\nimport { JXON } from '../util/jxon';\nimport { geoExtent } from '../geo';\nimport { osmChangeset } from '../osm';\nimport { svgIcon } from '../svg/icon';\n\nimport {\n utilEntityOrMemberSelector,\n utilKeybinding,\n utilRebind,\n utilWrap\n} from '../util';\n\n\nexport function uiConflicts(context) {\n var dispatch = d3_dispatch('cancel', 'save');\n var keybinding = utilKeybinding('conflicts');\n var _origChanges;\n var _conflictList;\n var _shownConflictIndex;\n\n\n function keybindingOn() {\n d3_select(document)\n .call(keybinding.on('\u238B', cancel, true));\n }\n\n function keybindingOff() {\n d3_select(document)\n .call(keybinding.unbind);\n }\n\n function tryAgain() {\n keybindingOff();\n dispatch.call('save');\n }\n\n function cancel() {\n keybindingOff();\n dispatch.call('cancel');\n }\n\n\n function conflicts(selection) {\n keybindingOn();\n\n var headerEnter = selection.selectAll('.header')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'fr')\n .attr('title', t('icons.close'))\n .on('click', cancel)\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('save.conflict.header'));\n\n var bodyEnter = selection.selectAll('.body')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'body fillL');\n\n var conflictsHelpEnter = bodyEnter\n .append('div')\n .attr('class', 'conflicts-help')\n .call(t.append('save.conflict.help'));\n\n\n // Download changes link\n var changeset = new osmChangeset();\n\n delete changeset.id; // Export without changeset_id\n\n var data = JXON.stringify(changeset.osmChangeJXON(_origChanges));\n var blob = new Blob([data], { type: 'text/xml;charset=utf-8;' });\n var fileName = 'changes.osc';\n\n var linkEnter = conflictsHelpEnter.selectAll('.download-changes')\n .append('a')\n .attr('class', 'download-changes');\n\n // download the data as a file\n linkEnter\n .attr('href', window.URL.createObjectURL(blob))\n .attr('download', fileName);\n\n linkEnter\n .call(svgIcon('#iD-icon-load', 'inline'))\n .append('span')\n .call(t.append('save.conflict.download_changes'));\n\n\n bodyEnter\n .append('div')\n .attr('class', 'conflict-container fillL3')\n .call(showConflict, 0);\n\n bodyEnter\n .append('div')\n .attr('class', 'conflicts-done')\n .attr('opacity', 0)\n .style('display', 'none')\n .call(t.append('save.conflict.done'));\n\n var buttonsEnter = bodyEnter\n .append('div')\n .attr('class','buttons col12 joined conflicts-buttons');\n\n buttonsEnter\n .append('button')\n .attr('disabled', _conflictList.length > 1)\n .attr('class', 'action conflicts-button col6')\n .call(t.append('save.title'))\n .on('click.try_again', tryAgain);\n\n buttonsEnter\n .append('button')\n .attr('class', 'secondary-action conflicts-button col6')\n .call(t.append('confirm.cancel'))\n .on('click.cancel', cancel);\n }\n\n\n function showConflict(selection, index) {\n index = utilWrap(index, _conflictList.length);\n _shownConflictIndex = index;\n\n var parent = d3_select(selection.node().parentNode);\n\n // enable save button if this is the last conflict being reviewed..\n if (index === _conflictList.length - 1) {\n window.setTimeout(function() {\n parent.select('.conflicts-button')\n .attr('disabled', null);\n\n parent.select('.conflicts-done')\n .transition()\n .attr('opacity', 1)\n .style('display', 'block');\n }, 250);\n }\n\n var conflict = selection\n .selectAll('.conflict')\n .data([_conflictList[index]]);\n\n conflict.exit()\n .remove();\n\n var conflictEnter = conflict.enter()\n .append('div')\n .attr('class', 'conflict');\n\n conflictEnter\n .append('h4')\n .attr('class', 'conflict-count')\n .call(t.append('save.conflict.count', { num: index + 1, total: _conflictList.length }));\n\n conflictEnter\n .append('a')\n .attr('class', 'conflict-description')\n .attr('href', '#')\n .text(function(d) { return d.name; })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n zoomToEntity(d.id);\n });\n\n var details = conflictEnter\n .append('div')\n .attr('class', 'conflict-detail-container');\n\n details\n .append('ul')\n .attr('class', 'conflict-detail-list')\n .selectAll('li')\n .data(function(d) { return d.details || []; })\n .enter()\n .append('li')\n .attr('class', 'conflict-detail-item')\n .each(function(d) {\n d3_select(this).call(d);\n });\n\n details\n .append('div')\n .attr('class', 'conflict-choices')\n .call(addChoices);\n\n details\n .append('div')\n .attr('class', 'conflict-nav-buttons joined cf')\n .selectAll('button')\n .data(['previous', 'next'])\n .enter()\n .append('button')\n .attr('class', 'conflict-nav-button action col6')\n .attr('disabled', function(d, i) {\n return (i === 0 && index === 0) ||\n (i === 1 && index === _conflictList.length - 1) || null;\n })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n\n var container = parent.selectAll('.conflict-container');\n var sign = (d === 'previous' ? -1 : 1);\n\n container\n .selectAll('.conflict')\n .remove();\n\n container\n .call(showConflict, index + sign);\n })\n .each(function(d) { t.append('save.conflict.' + d)(d3_select(this)); });\n\n }\n\n\n function addChoices(selection) {\n var choices = selection\n .append('ul')\n .attr('class', 'layer-list')\n .selectAll('li')\n .data(function(d) { return d.choices || []; });\n\n // enter\n var choicesEnter = choices.enter()\n .append('li')\n .attr('class', 'layer');\n\n var labelEnter = choicesEnter\n .append('label');\n\n labelEnter\n .append('input')\n .attr('type', 'radio')\n .attr('name', function(d) { return d.id; })\n .on('change', function(d3_event, d) {\n var ul = this.parentNode.parentNode.parentNode;\n ul.__data__.chosen = d.id;\n choose(d3_event, ul, d);\n });\n\n labelEnter\n .append('span')\n .text(function(d) { return d.text; });\n\n // update\n choicesEnter\n .merge(choices)\n .each(function(d) {\n var ul = this.parentNode;\n if (ul.__data__.chosen === d.id) {\n choose(null, ul, d);\n }\n });\n }\n\n\n function choose(d3_event, ul, datum) {\n if (d3_event) d3_event.preventDefault();\n\n d3_select(ul)\n .selectAll('li')\n .classed('active', function(d) { return d === datum; })\n .selectAll('input')\n .property('checked', function(d) { return d === datum; });\n\n var extent = geoExtent();\n var entity;\n\n entity = context.graph().hasEntity(datum.id);\n if (entity) extent._extend(entity.extent(context.graph()));\n\n datum.action();\n\n entity = context.graph().hasEntity(datum.id);\n if (entity) extent._extend(entity.extent(context.graph()));\n\n zoomToEntity(datum.id, extent);\n }\n\n\n function zoomToEntity(id, extent) {\n context.surface().selectAll('.hover')\n .classed('hover', false);\n\n var entity = context.graph().hasEntity(id);\n if (entity) {\n if (extent) {\n context.map().trimmedExtent(extent);\n } else {\n context.map().zoomToEase(entity);\n }\n context.surface().selectAll(utilEntityOrMemberSelector([entity.id], context.graph()))\n .classed('hover', true);\n }\n }\n\n\n // The conflict list should be an array of objects like:\n // {\n // id: id,\n // name: entityName(local),\n // details: merge.conflicts(),\n // chosen: 1,\n // choices: [\n // choice(id, keepMine, forceLocal),\n // choice(id, keepTheirs, forceRemote)\n // ]\n // }\n conflicts.conflictList = function(_) {\n if (!arguments.length) return _conflictList;\n _conflictList = _;\n return conflicts;\n };\n\n\n conflicts.origChanges = function(_) {\n if (!arguments.length) return _origChanges;\n _origChanges = _;\n return conflicts;\n };\n\n\n conflicts.shownEntityIds = function() {\n if (_conflictList && typeof _shownConflictIndex === 'number') {\n return [_conflictList[_shownConflictIndex].id];\n }\n return [];\n };\n\n\n return utilRebind(conflicts, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { uiModal } from './modal';\n\n\nexport function uiConfirm(selection) {\n var modalSelection = uiModal(selection);\n\n modalSelection.select('.modal')\n .classed('modal-alert', true);\n\n var section = modalSelection.select('.content');\n\n section.append('div')\n .attr('class', 'modal-section header');\n\n section.append('div')\n .attr('class', 'modal-section message-text');\n\n var buttons = section.append('div')\n .attr('class', 'modal-section buttons cf');\n\n\n modalSelection.okButton = function() {\n buttons\n .append('button')\n .attr('class', 'button ok-button action')\n .on('click.confirm', function() {\n modalSelection.remove();\n })\n .call(t.append('confirm.okay'))\n .node()\n .focus();\n\n return modalSelection;\n };\n\n\n return modalSelection;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { utilFunctor } from '../util/util';\n\nvar _popoverID = 0;\n\nexport function uiPopover(klass) {\n var _id = _popoverID++;\n var _anchorSelection = d3_select(null);\n var popover = function(selection) {\n _anchorSelection = selection;\n selection.each(setup);\n };\n var _animation = utilFunctor(false);\n var _placement = utilFunctor('top'); // top, bottom, left, right\n var _alignment = utilFunctor('center'); // leading, center, trailing\n var _scrollContainer = utilFunctor(d3_select(null));\n var _content;\n var _displayType = utilFunctor('');\n var _hasArrow = utilFunctor(true);\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n popover.displayType = function(val) {\n if (arguments.length) {\n _displayType = utilFunctor(val);\n return popover;\n } else {\n return _displayType;\n }\n };\n\n popover.hasArrow = function(val) {\n if (arguments.length) {\n _hasArrow = utilFunctor(val);\n return popover;\n } else {\n return _hasArrow;\n }\n };\n\n popover.placement = function(val) {\n if (arguments.length) {\n _placement = utilFunctor(val);\n return popover;\n } else {\n return _placement;\n }\n };\n\n popover.alignment = function(val) {\n if (arguments.length) {\n _alignment = utilFunctor(val);\n return popover;\n } else {\n return _alignment;\n }\n };\n\n popover.scrollContainer = function(val) {\n if (arguments.length) {\n _scrollContainer = utilFunctor(val);\n return popover;\n } else {\n return _scrollContainer;\n }\n };\n\n popover.content = function(val) {\n if (arguments.length) {\n _content = val;\n return popover;\n } else {\n return _content;\n }\n };\n\n popover.isShown = function() {\n var popoverSelection = _anchorSelection.select('.popover-' + _id);\n return !popoverSelection.empty() && popoverSelection.classed('in');\n };\n\n popover.show = function() {\n _anchorSelection.each(show);\n };\n\n popover.updateContent = function() {\n _anchorSelection.each(updateContent);\n };\n\n popover.hide = function() {\n _anchorSelection.each(hide);\n };\n\n popover.toggle = function() {\n _anchorSelection.each(toggle);\n };\n\n popover.destroy = function(selection, selector) {\n // by default, just destroy the current popover\n selector = selector || '.popover-' + _id;\n\n selection\n .on(_pointerPrefix + 'enter.popover', null)\n .on(_pointerPrefix + 'leave.popover', null)\n .on(_pointerPrefix + 'up.popover', null)\n .on(_pointerPrefix + 'down.popover', null)\n .on('focus.popover', null)\n .on('blur.popover', null)\n .on('click.popover', null)\n .attr('title', function() {\n return this.getAttribute('data-original-title') || this.getAttribute('title');\n })\n .attr('data-original-title', null)\n .selectAll(selector)\n .remove();\n };\n\n\n popover.destroyAny = function(selection) {\n selection.call(popover.destroy, '.popover');\n };\n\n function setup() {\n var anchor = d3_select(this);\n var animate = _animation.apply(this, arguments);\n var popoverSelection = anchor.selectAll('.popover-' + _id)\n .data([0]);\n\n\n var enter = popoverSelection.enter()\n .append('div')\n .attr('class', 'popover popover-' + _id + ' ' + (klass ? klass : ''))\n .classed('arrowed', _hasArrow.apply(this, arguments));\n\n enter\n .append('div')\n .attr('class', 'popover-arrow');\n\n enter\n .append('div')\n .attr('class', 'popover-inner');\n\n popoverSelection = enter\n .merge(popoverSelection);\n\n if (animate) {\n popoverSelection.classed('fade', true);\n }\n\n var display = _displayType.apply(this, arguments);\n\n if (display === 'hover') {\n var _lastNonMouseEnterTime;\n anchor.on(_pointerPrefix + 'enter.popover', function(d3_event) {\n\n if (d3_event.pointerType) {\n if (d3_event.pointerType !== 'mouse') {\n _lastNonMouseEnterTime = d3_event.timeStamp;\n // only allow hover behavior for mouse input\n return;\n } else if (_lastNonMouseEnterTime &&\n d3_event.timeStamp - _lastNonMouseEnterTime < 1500) {\n // HACK: iOS 13.4 sends an erroneous `mouse` type pointerenter\n // event for non-mouse interactions right after sending\n // the correct type pointerenter event. Workaround by discarding\n // any mouse event that occurs immediately after a non-mouse event.\n return;\n }\n }\n\n // don't show if buttons are pressed, e.g. during click and drag of map\n if (d3_event.buttons !== 0) return;\n\n show.apply(this, arguments);\n })\n .on(_pointerPrefix + 'leave.popover', function() {\n hide.apply(this, arguments);\n })\n // show on focus too for better keyboard navigation support\n .on('focus.popover', function() {\n show.apply(this, arguments);\n })\n .on('blur.popover', function() {\n hide.apply(this, arguments);\n });\n\n } else if (display === 'clickFocus') {\n anchor\n .on(_pointerPrefix + 'down.popover', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n })\n .on(_pointerPrefix + 'up.popover', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n })\n .on('click.popover', toggle);\n\n popoverSelection\n // This attribute lets the popover take focus\n .attr('tabindex', 0)\n .on('blur.popover', function() {\n anchor.each(function() {\n hide.apply(this, arguments);\n });\n });\n }\n }\n\n\n function show() {\n var anchor = d3_select(this);\n var popoverSelection = anchor.selectAll('.popover-' + _id);\n\n if (popoverSelection.empty()) {\n // popover was removed somehow, put it back\n anchor.call(popover.destroy);\n anchor.each(setup);\n popoverSelection = anchor.selectAll('.popover-' + _id);\n }\n\n popoverSelection.classed('in', true);\n\n var displayType = _displayType.apply(this, arguments);\n if (displayType === 'clickFocus') {\n anchor.classed('active', true);\n popoverSelection.node().focus();\n }\n\n anchor.each(updateContent);\n }\n\n function updateContent() {\n var anchor = d3_select(this);\n\n if (_content) {\n anchor.selectAll('.popover-' + _id + ' > .popover-inner')\n .call(_content.apply(this, arguments));\n }\n\n updatePosition.apply(this, arguments);\n // hack: update multiple times to fix instances where the absolute offset is\n // set before the dynamic popover size is calculated by the browser\n updatePosition.apply(this, arguments);\n updatePosition.apply(this, arguments);\n }\n\n\n function updatePosition() {\n\n var anchor = d3_select(this);\n var popoverSelection = anchor.selectAll('.popover-' + _id);\n\n var scrollContainer = _scrollContainer && _scrollContainer.apply(this, arguments);\n var scrollNode = scrollContainer && !scrollContainer.empty() && scrollContainer.node();\n var scrollLeft = scrollNode ? scrollNode.scrollLeft : 0;\n var scrollTop = scrollNode ? scrollNode.scrollTop : 0;\n\n var placement = _placement.apply(this, arguments);\n popoverSelection\n .classed('left', false)\n .classed('right', false)\n .classed('top', false)\n .classed('bottom', false)\n .classed(placement, true);\n\n var alignment = _alignment.apply(this, arguments);\n var alignFactor = 0.5;\n if (alignment === 'leading') {\n alignFactor = 0;\n } else if (alignment === 'trailing') {\n alignFactor = 1;\n }\n var anchorFrame = getFrame(anchor.node());\n var popoverFrame = getFrame(popoverSelection.node());\n var position;\n\n switch (placement) {\n case 'top':\n position = {\n x: anchorFrame.x + (anchorFrame.w - popoverFrame.w) * alignFactor,\n y: anchorFrame.y - popoverFrame.h\n };\n break;\n case 'bottom':\n position = {\n x: anchorFrame.x + (anchorFrame.w - popoverFrame.w) * alignFactor,\n y: anchorFrame.y + anchorFrame.h\n };\n break;\n case 'left':\n position = {\n x: anchorFrame.x - popoverFrame.w,\n y: anchorFrame.y + (anchorFrame.h - popoverFrame.h) * alignFactor\n };\n break;\n case 'right':\n position = {\n x: anchorFrame.x + anchorFrame.w,\n y: anchorFrame.y + (anchorFrame.h - popoverFrame.h) * alignFactor\n };\n break;\n }\n\n if (position) {\n if (scrollNode) {\n const MIN_MARGIN = 10;\n const popoverRect = popoverSelection.node().getBoundingClientRect();\n const scrollNodeRect = scrollNode.getBoundingClientRect();\n const arrow = anchor.selectAll('.popover-' + _id + ' > .popover-arrow');\n\n if (placement === 'top' || placement === 'bottom') {\n const initialPosX = position.x;\n if (popoverRect.right > scrollNodeRect.right - MIN_MARGIN) {\n position.x -= popoverRect.right - (scrollNodeRect.right - MIN_MARGIN);\n } else if (popoverRect.left < scrollNodeRect.left) {\n position.x += (scrollNodeRect.left + MIN_MARGIN) - popoverRect.left;\n }\n // keep the arrow centered on the button, or as close as possible\n const arrowPosX = Math.min(Math.max(popoverFrame.w / 2 - (position.x - initialPosX), MIN_MARGIN), popoverFrame.w - MIN_MARGIN);\n arrow.style('left', ~~arrowPosX + 'px');\n\n } else if (placement === 'left' || placement === 'right') {\n const initialPosY = position.y;\n if (popoverRect.bottom > scrollNodeRect.bottom - MIN_MARGIN) {\n position.y -= popoverRect.bottom - (scrollNodeRect.bottom - MIN_MARGIN);\n } else if (popoverRect.top < scrollNodeRect.top + MIN_MARGIN) {\n position.y += (scrollNodeRect.top + MIN_MARGIN) - popoverRect.top;\n }\n // keep the arrow centered on the button, or as close as possible\n const arrowPosY = Math.min(Math.max(popoverFrame.h / 2 - (position.y - initialPosY), MIN_MARGIN), popoverFrame.h - MIN_MARGIN);\n arrow.style('top', ~~arrowPosY + 'px');\n }\n }\n\n popoverSelection\n .style('left', ~~position.x + 'px')\n .style('top', ~~position.y + 'px');\n } else {\n popoverSelection\n .style('left', null)\n .style('top', null);\n }\n\n function getFrame(node) {\n var positionStyle = d3_select(node).style('position');\n if (positionStyle === 'absolute' || positionStyle === 'static') {\n return {\n x: node.offsetLeft - scrollLeft,\n y: node.offsetTop - scrollTop,\n w: node.offsetWidth,\n h: node.offsetHeight\n };\n } else {\n return {\n x: 0,\n y: 0,\n w: node.offsetWidth,\n h: node.offsetHeight\n };\n }\n }\n }\n\n\n function hide() {\n var anchor = d3_select(this);\n if (_displayType.apply(this, arguments) === 'clickFocus') {\n anchor.classed('active', false);\n }\n anchor.selectAll('.popover-' + _id).classed('in', false);\n }\n\n\n function toggle() {\n if (d3_select(this).select('.popover-' + _id).classed('in')) {\n hide.apply(this, arguments);\n } else {\n show.apply(this, arguments);\n }\n }\n\n\n return popover;\n}\n", "import { utilFunctor } from '../util/util';\nimport { t } from '../core/localizer';\nimport { uiPopover } from './popover';\n\nexport function uiTooltip(klass) {\n\n var tooltip = uiPopover((klass || '') + ' tooltip')\n .displayType('hover');\n\n var _title = function() {\n var title = this.getAttribute('data-original-title');\n if (title) {\n return title;\n } else {\n title = this.getAttribute('title');\n this.removeAttribute('title');\n this.setAttribute('data-original-title', title);\n }\n return title;\n };\n\n var _heading = utilFunctor(null);\n var _keys = utilFunctor(null);\n\n tooltip.title = function(val) {\n if (!arguments.length) return _title;\n _title = utilFunctor(val);\n return tooltip;\n };\n\n tooltip.heading = function(val) {\n if (!arguments.length) return _heading;\n _heading = utilFunctor(val);\n return tooltip;\n };\n\n tooltip.keys = function(val) {\n if (!arguments.length) return _keys;\n _keys = utilFunctor(val);\n return tooltip;\n };\n\n tooltip.content(function() {\n var heading = _heading.apply(this, arguments);\n var text = _title.apply(this, arguments);\n var keys = _keys.apply(this, arguments);\n\n var headingCallback = typeof heading === 'function' ? heading : s => s.text(heading);\n var textCallback = typeof text === 'function' ? text : s => s.text(text);\n\n return function(selection) {\n\n var headingSelect = selection\n .selectAll('.tooltip-heading')\n .data(heading ? [heading] :[]);\n\n headingSelect.exit()\n .remove();\n\n headingSelect.enter()\n .append('div')\n .attr('class', 'tooltip-heading')\n .merge(headingSelect)\n .text('')\n .call(headingCallback);\n\n var textSelect = selection\n .selectAll('.tooltip-text')\n .data(text ? [text] :[]);\n\n textSelect.exit()\n .remove();\n\n textSelect.enter()\n .append('div')\n .attr('class', 'tooltip-text')\n .merge(textSelect)\n .text('')\n .call(textCallback);\n\n var keyhintWrap = selection\n .selectAll('.keyhint-wrap')\n .data(keys && keys.length ? [0] : []);\n\n keyhintWrap.exit()\n .remove();\n\n var keyhintWrapEnter = keyhintWrap.enter()\n .append('div')\n .attr('class', 'keyhint-wrap');\n\n keyhintWrapEnter\n .append('span')\n .call(t.append('tooltip_keyhint'));\n\n keyhintWrap = keyhintWrapEnter.merge(keyhintWrap);\n\n keyhintWrap.selectAll('kbd.shortcut')\n .data(keys && keys.length ? keys : [])\n .enter()\n .append('kbd')\n .attr('class', 'shortcut')\n .text(function(d) {\n return d;\n });\n };\n });\n\n return tooltip;\n}\n", "/*\r\n * bignumber.js v9.3.1\r\n * A JavaScript library for arbitrary-precision arithmetic.\r\n * https://github.com/MikeMcl/bignumber.js\r\n * Copyright (c) 2025 Michael Mclaughlin \r\n * MIT Licensed.\r\n *\r\n * BigNumber.prototype methods | BigNumber methods\r\n * |\r\n * absoluteValue abs | clone\r\n * comparedTo | config set\r\n * decimalPlaces dp | DECIMAL_PLACES\r\n * dividedBy div | ROUNDING_MODE\r\n * dividedToIntegerBy idiv | EXPONENTIAL_AT\r\n * exponentiatedBy pow | RANGE\r\n * integerValue | CRYPTO\r\n * isEqualTo eq | MODULO_MODE\r\n * isFinite | POW_PRECISION\r\n * isGreaterThan gt | FORMAT\r\n * isGreaterThanOrEqualTo gte | ALPHABET\r\n * isInteger | isBigNumber\r\n * isLessThan lt | maximum max\r\n * isLessThanOrEqualTo lte | minimum min\r\n * isNaN | random\r\n * isNegative | sum\r\n * isPositive |\r\n * isZero |\r\n * minus |\r\n * modulo mod |\r\n * multipliedBy times |\r\n * negated |\r\n * plus |\r\n * precision sd |\r\n * shiftedBy |\r\n * squareRoot sqrt |\r\n * toExponential |\r\n * toFixed |\r\n * toFormat |\r\n * toFraction |\r\n * toJSON |\r\n * toNumber |\r\n * toPrecision |\r\n * toString |\r\n * valueOf |\r\n *\r\n */\r\n\r\n\r\nvar\r\n isNumeric = /^-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?$/i,\r\n mathceil = Math.ceil,\r\n mathfloor = Math.floor,\r\n\r\n bignumberError = '[BigNumber Error] ',\r\n tooManyDigits = bignumberError + 'Number primitive has more than 15 significant digits: ',\r\n\r\n BASE = 1e14,\r\n LOG_BASE = 14,\r\n MAX_SAFE_INTEGER = 0x1fffffffffffff, // 2^53 - 1\r\n // MAX_INT32 = 0x7fffffff, // 2^31 - 1\r\n POWS_TEN = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13],\r\n SQRT_BASE = 1e7,\r\n\r\n // EDITABLE\r\n // The limit on the value of DECIMAL_PLACES, TO_EXP_NEG, TO_EXP_POS, MIN_EXP, MAX_EXP, and\r\n // the arguments to toExponential, toFixed, toFormat, and toPrecision.\r\n MAX = 1E9; // 0 to MAX_INT32\r\n\r\n\r\n/*\r\n * Create and return a BigNumber constructor.\r\n */\r\nfunction clone(configObject) {\r\n var div, convertBase, parseNumeric,\r\n P = BigNumber.prototype = { constructor: BigNumber, toString: null, valueOf: null },\r\n ONE = new BigNumber(1),\r\n\r\n\r\n //----------------------------- EDITABLE CONFIG DEFAULTS -------------------------------\r\n\r\n\r\n // The default values below must be integers within the inclusive ranges stated.\r\n // The values can also be changed at run-time using BigNumber.set.\r\n\r\n // The maximum number of decimal places for operations involving division.\r\n DECIMAL_PLACES = 20, // 0 to MAX\r\n\r\n // The rounding mode used when rounding to the above decimal places, and when using\r\n // toExponential, toFixed, toFormat and toPrecision, and round (default value).\r\n // UP 0 Away from zero.\r\n // DOWN 1 Towards zero.\r\n // CEIL 2 Towards +Infinity.\r\n // FLOOR 3 Towards -Infinity.\r\n // HALF_UP 4 Towards nearest neighbour. If equidistant, up.\r\n // HALF_DOWN 5 Towards nearest neighbour. If equidistant, down.\r\n // HALF_EVEN 6 Towards nearest neighbour. If equidistant, towards even neighbour.\r\n // HALF_CEIL 7 Towards nearest neighbour. If equidistant, towards +Infinity.\r\n // HALF_FLOOR 8 Towards nearest neighbour. If equidistant, towards -Infinity.\r\n ROUNDING_MODE = 4, // 0 to 8\r\n\r\n // EXPONENTIAL_AT : [TO_EXP_NEG , TO_EXP_POS]\r\n\r\n // The exponent value at and beneath which toString returns exponential notation.\r\n // Number type: -7\r\n TO_EXP_NEG = -7, // 0 to -MAX\r\n\r\n // The exponent value at and above which toString returns exponential notation.\r\n // Number type: 21\r\n TO_EXP_POS = 21, // 0 to MAX\r\n\r\n // RANGE : [MIN_EXP, MAX_EXP]\r\n\r\n // The minimum exponent value, beneath which underflow to zero occurs.\r\n // Number type: -324 (5e-324)\r\n MIN_EXP = -1e7, // -1 to -MAX\r\n\r\n // The maximum exponent value, above which overflow to Infinity occurs.\r\n // Number type: 308 (1.7976931348623157e+308)\r\n // For MAX_EXP > 1e7, e.g. new BigNumber('1e100000000').plus(1) may be slow.\r\n MAX_EXP = 1e7, // 1 to MAX\r\n\r\n // Whether to use cryptographically-secure random number generation, if available.\r\n CRYPTO = false, // true or false\r\n\r\n // The modulo mode used when calculating the modulus: a mod n.\r\n // The quotient (q = a / n) is calculated according to the corresponding rounding mode.\r\n // The remainder (r) is calculated as: r = a - n * q.\r\n //\r\n // UP 0 The remainder is positive if the dividend is negative, else is negative.\r\n // DOWN 1 The remainder has the same sign as the dividend.\r\n // This modulo mode is commonly known as 'truncated division' and is\r\n // equivalent to (a % n) in JavaScript.\r\n // FLOOR 3 The remainder has the same sign as the divisor (Python %).\r\n // HALF_EVEN 6 This modulo mode implements the IEEE 754 remainder function.\r\n // EUCLID 9 Euclidian division. q = sign(n) * floor(a / abs(n)).\r\n // The remainder is always positive.\r\n //\r\n // The truncated division, floored division, Euclidian division and IEEE 754 remainder\r\n // modes are commonly used for the modulus operation.\r\n // Although the other rounding modes can also be used, they may not give useful results.\r\n MODULO_MODE = 1, // 0 to 9\r\n\r\n // The maximum number of significant digits of the result of the exponentiatedBy operation.\r\n // If POW_PRECISION is 0, there will be unlimited significant digits.\r\n POW_PRECISION = 0, // 0 to MAX\r\n\r\n // The format specification used by the BigNumber.prototype.toFormat method.\r\n FORMAT = {\r\n prefix: '',\r\n groupSize: 3,\r\n secondaryGroupSize: 0,\r\n groupSeparator: ',',\r\n decimalSeparator: '.',\r\n fractionGroupSize: 0,\r\n fractionGroupSeparator: '\\xA0', // non-breaking space\r\n suffix: ''\r\n },\r\n\r\n // The alphabet used for base conversion. It must be at least 2 characters long, with no '+',\r\n // '-', '.', whitespace, or repeated character.\r\n // '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_'\r\n ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz',\r\n alphabetHasNormalDecimalDigits = true;\r\n\r\n\r\n //------------------------------------------------------------------------------------------\r\n\r\n\r\n // CONSTRUCTOR\r\n\r\n\r\n /*\r\n * The BigNumber constructor and exported function.\r\n * Create and return a new instance of a BigNumber object.\r\n *\r\n * v {number|string|BigNumber} A numeric value.\r\n * [b] {number} The base of v. Integer, 2 to ALPHABET.length inclusive.\r\n */\r\n function BigNumber(v, b) {\r\n var alphabet, c, caseChanged, e, i, isNum, len, str,\r\n x = this;\r\n\r\n // Enable constructor call without `new`.\r\n if (!(x instanceof BigNumber)) return new BigNumber(v, b);\r\n\r\n if (b == null) {\r\n\r\n if (v && v._isBigNumber === true) {\r\n x.s = v.s;\r\n\r\n if (!v.c || v.e > MAX_EXP) {\r\n x.c = x.e = null;\r\n } else if (v.e < MIN_EXP) {\r\n x.c = [x.e = 0];\r\n } else {\r\n x.e = v.e;\r\n x.c = v.c.slice();\r\n }\r\n\r\n return;\r\n }\r\n\r\n if ((isNum = typeof v == 'number') && v * 0 == 0) {\r\n\r\n // Use `1 / n` to handle minus zero also.\r\n x.s = 1 / v < 0 ? (v = -v, -1) : 1;\r\n\r\n // Fast path for integers, where n < 2147483648 (2**31).\r\n if (v === ~~v) {\r\n for (e = 0, i = v; i >= 10; i /= 10, e++);\r\n\r\n if (e > MAX_EXP) {\r\n x.c = x.e = null;\r\n } else {\r\n x.e = e;\r\n x.c = [v];\r\n }\r\n\r\n return;\r\n }\r\n\r\n str = String(v);\r\n } else {\r\n\r\n if (!isNumeric.test(str = String(v))) return parseNumeric(x, str, isNum);\r\n\r\n x.s = str.charCodeAt(0) == 45 ? (str = str.slice(1), -1) : 1;\r\n }\r\n\r\n // Decimal point?\r\n if ((e = str.indexOf('.')) > -1) str = str.replace('.', '');\r\n\r\n // Exponential form?\r\n if ((i = str.search(/e/i)) > 0) {\r\n\r\n // Determine exponent.\r\n if (e < 0) e = i;\r\n e += +str.slice(i + 1);\r\n str = str.substring(0, i);\r\n } else if (e < 0) {\r\n\r\n // Integer.\r\n e = str.length;\r\n }\r\n\r\n } else {\r\n\r\n // '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}'\r\n intCheck(b, 2, ALPHABET.length, 'Base');\r\n\r\n // Allow exponential notation to be used with base 10 argument, while\r\n // also rounding to DECIMAL_PLACES as with other bases.\r\n if (b == 10 && alphabetHasNormalDecimalDigits) {\r\n x = new BigNumber(v);\r\n return round(x, DECIMAL_PLACES + x.e + 1, ROUNDING_MODE);\r\n }\r\n\r\n str = String(v);\r\n\r\n if (isNum = typeof v == 'number') {\r\n\r\n // Avoid potential interpretation of Infinity and NaN as base 44+ values.\r\n if (v * 0 != 0) return parseNumeric(x, str, isNum, b);\r\n\r\n x.s = 1 / v < 0 ? (str = str.slice(1), -1) : 1;\r\n\r\n // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}'\r\n if (BigNumber.DEBUG && str.replace(/^0\\.0*|\\./, '').length > 15) {\r\n throw Error\r\n (tooManyDigits + v);\r\n }\r\n } else {\r\n x.s = str.charCodeAt(0) === 45 ? (str = str.slice(1), -1) : 1;\r\n }\r\n\r\n alphabet = ALPHABET.slice(0, b);\r\n e = i = 0;\r\n\r\n // Check that str is a valid base b number.\r\n // Don't use RegExp, so alphabet can contain special characters.\r\n for (len = str.length; i < len; i++) {\r\n if (alphabet.indexOf(c = str.charAt(i)) < 0) {\r\n if (c == '.') {\r\n\r\n // If '.' is not the first character and it has not be found before.\r\n if (i > e) {\r\n e = len;\r\n continue;\r\n }\r\n } else if (!caseChanged) {\r\n\r\n // Allow e.g. hexadecimal 'FF' as well as 'ff'.\r\n if (str == str.toUpperCase() && (str = str.toLowerCase()) ||\r\n str == str.toLowerCase() && (str = str.toUpperCase())) {\r\n caseChanged = true;\r\n i = -1;\r\n e = 0;\r\n continue;\r\n }\r\n }\r\n\r\n return parseNumeric(x, String(v), isNum, b);\r\n }\r\n }\r\n\r\n // Prevent later check for length on converted number.\r\n isNum = false;\r\n str = convertBase(str, b, 10, x.s);\r\n\r\n // Decimal point?\r\n if ((e = str.indexOf('.')) > -1) str = str.replace('.', '');\r\n else e = str.length;\r\n }\r\n\r\n // Determine leading zeros.\r\n for (i = 0; str.charCodeAt(i) === 48; i++);\r\n\r\n // Determine trailing zeros.\r\n for (len = str.length; str.charCodeAt(--len) === 48;);\r\n\r\n if (str = str.slice(i, ++len)) {\r\n len -= i;\r\n\r\n // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}'\r\n if (isNum && BigNumber.DEBUG &&\r\n len > 15 && (v > MAX_SAFE_INTEGER || v !== mathfloor(v))) {\r\n throw Error\r\n (tooManyDigits + (x.s * v));\r\n }\r\n\r\n // Overflow?\r\n if ((e = e - i - 1) > MAX_EXP) {\r\n\r\n // Infinity.\r\n x.c = x.e = null;\r\n\r\n // Underflow?\r\n } else if (e < MIN_EXP) {\r\n\r\n // Zero.\r\n x.c = [x.e = 0];\r\n } else {\r\n x.e = e;\r\n x.c = [];\r\n\r\n // Transform base\r\n\r\n // e is the base 10 exponent.\r\n // i is where to slice str to get the first element of the coefficient array.\r\n i = (e + 1) % LOG_BASE;\r\n if (e < 0) i += LOG_BASE; // i < 1\r\n\r\n if (i < len) {\r\n if (i) x.c.push(+str.slice(0, i));\r\n\r\n for (len -= LOG_BASE; i < len;) {\r\n x.c.push(+str.slice(i, i += LOG_BASE));\r\n }\r\n\r\n i = LOG_BASE - (str = str.slice(i)).length;\r\n } else {\r\n i -= len;\r\n }\r\n\r\n for (; i--; str += '0');\r\n x.c.push(+str);\r\n }\r\n } else {\r\n\r\n // Zero.\r\n x.c = [x.e = 0];\r\n }\r\n }\r\n\r\n\r\n // CONSTRUCTOR PROPERTIES\r\n\r\n\r\n BigNumber.clone = clone;\r\n\r\n BigNumber.ROUND_UP = 0;\r\n BigNumber.ROUND_DOWN = 1;\r\n BigNumber.ROUND_CEIL = 2;\r\n BigNumber.ROUND_FLOOR = 3;\r\n BigNumber.ROUND_HALF_UP = 4;\r\n BigNumber.ROUND_HALF_DOWN = 5;\r\n BigNumber.ROUND_HALF_EVEN = 6;\r\n BigNumber.ROUND_HALF_CEIL = 7;\r\n BigNumber.ROUND_HALF_FLOOR = 8;\r\n BigNumber.EUCLID = 9;\r\n\r\n\r\n /*\r\n * Configure infrequently-changing library-wide settings.\r\n *\r\n * Accept an object with the following optional properties (if the value of a property is\r\n * a number, it must be an integer within the inclusive range stated):\r\n *\r\n * DECIMAL_PLACES {number} 0 to MAX\r\n * ROUNDING_MODE {number} 0 to 8\r\n * EXPONENTIAL_AT {number|number[]} -MAX to MAX or [-MAX to 0, 0 to MAX]\r\n * RANGE {number|number[]} -MAX to MAX (not zero) or [-MAX to -1, 1 to MAX]\r\n * CRYPTO {boolean} true or false\r\n * MODULO_MODE {number} 0 to 9\r\n * POW_PRECISION {number} 0 to MAX\r\n * ALPHABET {string} A string of two or more unique characters which does\r\n * not contain '.'.\r\n * FORMAT {object} An object with some of the following properties:\r\n * prefix {string}\r\n * groupSize {number}\r\n * secondaryGroupSize {number}\r\n * groupSeparator {string}\r\n * decimalSeparator {string}\r\n * fractionGroupSize {number}\r\n * fractionGroupSeparator {string}\r\n * suffix {string}\r\n *\r\n * (The values assigned to the above FORMAT object properties are not checked for validity.)\r\n *\r\n * E.g.\r\n * BigNumber.config({ DECIMAL_PLACES : 20, ROUNDING_MODE : 4 })\r\n *\r\n * Ignore properties/parameters set to null or undefined, except for ALPHABET.\r\n *\r\n * Return an object with the properties current values.\r\n */\r\n BigNumber.config = BigNumber.set = function (obj) {\r\n var p, v;\r\n\r\n if (obj != null) {\r\n\r\n if (typeof obj == 'object') {\r\n\r\n // DECIMAL_PLACES {number} Integer, 0 to MAX inclusive.\r\n // '[BigNumber Error] DECIMAL_PLACES {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'DECIMAL_PLACES')) {\r\n v = obj[p];\r\n intCheck(v, 0, MAX, p);\r\n DECIMAL_PLACES = v;\r\n }\r\n\r\n // ROUNDING_MODE {number} Integer, 0 to 8 inclusive.\r\n // '[BigNumber Error] ROUNDING_MODE {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'ROUNDING_MODE')) {\r\n v = obj[p];\r\n intCheck(v, 0, 8, p);\r\n ROUNDING_MODE = v;\r\n }\r\n\r\n // EXPONENTIAL_AT {number|number[]}\r\n // Integer, -MAX to MAX inclusive or\r\n // [integer -MAX to 0 inclusive, 0 to MAX inclusive].\r\n // '[BigNumber Error] EXPONENTIAL_AT {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'EXPONENTIAL_AT')) {\r\n v = obj[p];\r\n if (v && v.pop) {\r\n intCheck(v[0], -MAX, 0, p);\r\n intCheck(v[1], 0, MAX, p);\r\n TO_EXP_NEG = v[0];\r\n TO_EXP_POS = v[1];\r\n } else {\r\n intCheck(v, -MAX, MAX, p);\r\n TO_EXP_NEG = -(TO_EXP_POS = v < 0 ? -v : v);\r\n }\r\n }\r\n\r\n // RANGE {number|number[]} Non-zero integer, -MAX to MAX inclusive or\r\n // [integer -MAX to -1 inclusive, integer 1 to MAX inclusive].\r\n // '[BigNumber Error] RANGE {not a primitive number|not an integer|out of range|cannot be zero}: {v}'\r\n if (obj.hasOwnProperty(p = 'RANGE')) {\r\n v = obj[p];\r\n if (v && v.pop) {\r\n intCheck(v[0], -MAX, -1, p);\r\n intCheck(v[1], 1, MAX, p);\r\n MIN_EXP = v[0];\r\n MAX_EXP = v[1];\r\n } else {\r\n intCheck(v, -MAX, MAX, p);\r\n if (v) {\r\n MIN_EXP = -(MAX_EXP = v < 0 ? -v : v);\r\n } else {\r\n throw Error\r\n (bignumberError + p + ' cannot be zero: ' + v);\r\n }\r\n }\r\n }\r\n\r\n // CRYPTO {boolean} true or false.\r\n // '[BigNumber Error] CRYPTO not true or false: {v}'\r\n // '[BigNumber Error] crypto unavailable'\r\n if (obj.hasOwnProperty(p = 'CRYPTO')) {\r\n v = obj[p];\r\n if (v === !!v) {\r\n if (v) {\r\n if (typeof crypto != 'undefined' && crypto &&\r\n (crypto.getRandomValues || crypto.randomBytes)) {\r\n CRYPTO = v;\r\n } else {\r\n CRYPTO = !v;\r\n throw Error\r\n (bignumberError + 'crypto unavailable');\r\n }\r\n } else {\r\n CRYPTO = v;\r\n }\r\n } else {\r\n throw Error\r\n (bignumberError + p + ' not true or false: ' + v);\r\n }\r\n }\r\n\r\n // MODULO_MODE {number} Integer, 0 to 9 inclusive.\r\n // '[BigNumber Error] MODULO_MODE {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'MODULO_MODE')) {\r\n v = obj[p];\r\n intCheck(v, 0, 9, p);\r\n MODULO_MODE = v;\r\n }\r\n\r\n // POW_PRECISION {number} Integer, 0 to MAX inclusive.\r\n // '[BigNumber Error] POW_PRECISION {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'POW_PRECISION')) {\r\n v = obj[p];\r\n intCheck(v, 0, MAX, p);\r\n POW_PRECISION = v;\r\n }\r\n\r\n // FORMAT {object}\r\n // '[BigNumber Error] FORMAT not an object: {v}'\r\n if (obj.hasOwnProperty(p = 'FORMAT')) {\r\n v = obj[p];\r\n if (typeof v == 'object') FORMAT = v;\r\n else throw Error\r\n (bignumberError + p + ' not an object: ' + v);\r\n }\r\n\r\n // ALPHABET {string}\r\n // '[BigNumber Error] ALPHABET invalid: {v}'\r\n if (obj.hasOwnProperty(p = 'ALPHABET')) {\r\n v = obj[p];\r\n\r\n // Disallow if less than two characters,\r\n // or if it contains '+', '-', '.', whitespace, or a repeated character.\r\n if (typeof v == 'string' && !/^.?$|[+\\-.\\s]|(.).*\\1/.test(v)) {\r\n alphabetHasNormalDecimalDigits = v.slice(0, 10) == '0123456789';\r\n ALPHABET = v;\r\n } else {\r\n throw Error\r\n (bignumberError + p + ' invalid: ' + v);\r\n }\r\n }\r\n\r\n } else {\r\n\r\n // '[BigNumber Error] Object expected: {v}'\r\n throw Error\r\n (bignumberError + 'Object expected: ' + obj);\r\n }\r\n }\r\n\r\n return {\r\n DECIMAL_PLACES: DECIMAL_PLACES,\r\n ROUNDING_MODE: ROUNDING_MODE,\r\n EXPONENTIAL_AT: [TO_EXP_NEG, TO_EXP_POS],\r\n RANGE: [MIN_EXP, MAX_EXP],\r\n CRYPTO: CRYPTO,\r\n MODULO_MODE: MODULO_MODE,\r\n POW_PRECISION: POW_PRECISION,\r\n FORMAT: FORMAT,\r\n ALPHABET: ALPHABET\r\n };\r\n };\r\n\r\n\r\n /*\r\n * Return true if v is a BigNumber instance, otherwise return false.\r\n *\r\n * If BigNumber.DEBUG is true, throw if a BigNumber instance is not well-formed.\r\n *\r\n * v {any}\r\n *\r\n * '[BigNumber Error] Invalid BigNumber: {v}'\r\n */\r\n BigNumber.isBigNumber = function (v) {\r\n if (!v || v._isBigNumber !== true) return false;\r\n if (!BigNumber.DEBUG) return true;\r\n\r\n var i, n,\r\n c = v.c,\r\n e = v.e,\r\n s = v.s;\r\n\r\n out: if ({}.toString.call(c) == '[object Array]') {\r\n\r\n if ((s === 1 || s === -1) && e >= -MAX && e <= MAX && e === mathfloor(e)) {\r\n\r\n // If the first element is zero, the BigNumber value must be zero.\r\n if (c[0] === 0) {\r\n if (e === 0 && c.length === 1) return true;\r\n break out;\r\n }\r\n\r\n // Calculate number of digits that c[0] should have, based on the exponent.\r\n i = (e + 1) % LOG_BASE;\r\n if (i < 1) i += LOG_BASE;\r\n\r\n // Calculate number of digits of c[0].\r\n //if (Math.ceil(Math.log(c[0] + 1) / Math.LN10) == i) {\r\n if (String(c[0]).length == i) {\r\n\r\n for (i = 0; i < c.length; i++) {\r\n n = c[i];\r\n if (n < 0 || n >= BASE || n !== mathfloor(n)) break out;\r\n }\r\n\r\n // Last element cannot be zero, unless it is the only element.\r\n if (n !== 0) return true;\r\n }\r\n }\r\n\r\n // Infinity/NaN\r\n } else if (c === null && e === null && (s === null || s === 1 || s === -1)) {\r\n return true;\r\n }\r\n\r\n throw Error\r\n (bignumberError + 'Invalid BigNumber: ' + v);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the maximum of the arguments.\r\n *\r\n * arguments {number|string|BigNumber}\r\n */\r\n BigNumber.maximum = BigNumber.max = function () {\r\n return maxOrMin(arguments, -1);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the minimum of the arguments.\r\n *\r\n * arguments {number|string|BigNumber}\r\n */\r\n BigNumber.minimum = BigNumber.min = function () {\r\n return maxOrMin(arguments, 1);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber with a random value equal to or greater than 0 and less than 1,\r\n * and with dp, or DECIMAL_PLACES if dp is omitted, decimal places (or less if trailing\r\n * zeros are produced).\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp}'\r\n * '[BigNumber Error] crypto unavailable'\r\n */\r\n BigNumber.random = (function () {\r\n var pow2_53 = 0x20000000000000;\r\n\r\n // Return a 53 bit integer n, where 0 <= n < 9007199254740992.\r\n // Check if Math.random() produces more than 32 bits of randomness.\r\n // If it does, assume at least 53 bits are produced, otherwise assume at least 30 bits.\r\n // 0x40000000 is 2^30, 0x800000 is 2^23, 0x1fffff is 2^21 - 1.\r\n var random53bitInt = (Math.random() * pow2_53) & 0x1fffff\r\n ? function () { return mathfloor(Math.random() * pow2_53); }\r\n : function () { return ((Math.random() * 0x40000000 | 0) * 0x800000) +\r\n (Math.random() * 0x800000 | 0); };\r\n\r\n return function (dp) {\r\n var a, b, e, k, v,\r\n i = 0,\r\n c = [],\r\n rand = new BigNumber(ONE);\r\n\r\n if (dp == null) dp = DECIMAL_PLACES;\r\n else intCheck(dp, 0, MAX);\r\n\r\n k = mathceil(dp / LOG_BASE);\r\n\r\n if (CRYPTO) {\r\n\r\n // Browsers supporting crypto.getRandomValues.\r\n if (crypto.getRandomValues) {\r\n\r\n a = crypto.getRandomValues(new Uint32Array(k *= 2));\r\n\r\n for (; i < k;) {\r\n\r\n // 53 bits:\r\n // ((Math.pow(2, 32) - 1) * Math.pow(2, 21)).toString(2)\r\n // 11111 11111111 11111111 11111111 11100000 00000000 00000000\r\n // ((Math.pow(2, 32) - 1) >>> 11).toString(2)\r\n // 11111 11111111 11111111\r\n // 0x20000 is 2^21.\r\n v = a[i] * 0x20000 + (a[i + 1] >>> 11);\r\n\r\n // Rejection sampling:\r\n // 0 <= v < 9007199254740992\r\n // Probability that v >= 9e15, is\r\n // 7199254740992 / 9007199254740992 ~= 0.0008, i.e. 1 in 1251\r\n if (v >= 9e15) {\r\n b = crypto.getRandomValues(new Uint32Array(2));\r\n a[i] = b[0];\r\n a[i + 1] = b[1];\r\n } else {\r\n\r\n // 0 <= v <= 8999999999999999\r\n // 0 <= (v % 1e14) <= 99999999999999\r\n c.push(v % 1e14);\r\n i += 2;\r\n }\r\n }\r\n i = k / 2;\r\n\r\n // Node.js supporting crypto.randomBytes.\r\n } else if (crypto.randomBytes) {\r\n\r\n // buffer\r\n a = crypto.randomBytes(k *= 7);\r\n\r\n for (; i < k;) {\r\n\r\n // 0x1000000000000 is 2^48, 0x10000000000 is 2^40\r\n // 0x100000000 is 2^32, 0x1000000 is 2^24\r\n // 11111 11111111 11111111 11111111 11111111 11111111 11111111\r\n // 0 <= v < 9007199254740992\r\n v = ((a[i] & 31) * 0x1000000000000) + (a[i + 1] * 0x10000000000) +\r\n (a[i + 2] * 0x100000000) + (a[i + 3] * 0x1000000) +\r\n (a[i + 4] << 16) + (a[i + 5] << 8) + a[i + 6];\r\n\r\n if (v >= 9e15) {\r\n crypto.randomBytes(7).copy(a, i);\r\n } else {\r\n\r\n // 0 <= (v % 1e14) <= 99999999999999\r\n c.push(v % 1e14);\r\n i += 7;\r\n }\r\n }\r\n i = k / 7;\r\n } else {\r\n CRYPTO = false;\r\n throw Error\r\n (bignumberError + 'crypto unavailable');\r\n }\r\n }\r\n\r\n // Use Math.random.\r\n if (!CRYPTO) {\r\n\r\n for (; i < k;) {\r\n v = random53bitInt();\r\n if (v < 9e15) c[i++] = v % 1e14;\r\n }\r\n }\r\n\r\n k = c[--i];\r\n dp %= LOG_BASE;\r\n\r\n // Convert trailing digits to zeros according to dp.\r\n if (k && dp) {\r\n v = POWS_TEN[LOG_BASE - dp];\r\n c[i] = mathfloor(k / v) * v;\r\n }\r\n\r\n // Remove trailing elements which are zero.\r\n for (; c[i] === 0; c.pop(), i--);\r\n\r\n // Zero?\r\n if (i < 0) {\r\n c = [e = 0];\r\n } else {\r\n\r\n // Remove leading elements which are zero and adjust exponent accordingly.\r\n for (e = -1 ; c[0] === 0; c.splice(0, 1), e -= LOG_BASE);\r\n\r\n // Count the digits of the first element of c to determine leading zeros, and...\r\n for (i = 1, v = c[0]; v >= 10; v /= 10, i++);\r\n\r\n // adjust the exponent accordingly.\r\n if (i < LOG_BASE) e -= LOG_BASE - i;\r\n }\r\n\r\n rand.e = e;\r\n rand.c = c;\r\n return rand;\r\n };\r\n })();\r\n\r\n\r\n /*\r\n * Return a BigNumber whose value is the sum of the arguments.\r\n *\r\n * arguments {number|string|BigNumber}\r\n */\r\n BigNumber.sum = function () {\r\n var i = 1,\r\n args = arguments,\r\n sum = new BigNumber(args[0]);\r\n for (; i < args.length;) sum = sum.plus(args[i++]);\r\n return sum;\r\n };\r\n\r\n\r\n // PRIVATE FUNCTIONS\r\n\r\n\r\n // Called by BigNumber and BigNumber.prototype.toString.\r\n convertBase = (function () {\r\n var decimal = '0123456789';\r\n\r\n /*\r\n * Convert string of baseIn to an array of numbers of baseOut.\r\n * Eg. toBaseOut('255', 10, 16) returns [15, 15].\r\n * Eg. toBaseOut('ff', 16, 10) returns [2, 5, 5].\r\n */\r\n function toBaseOut(str, baseIn, baseOut, alphabet) {\r\n var j,\r\n arr = [0],\r\n arrL,\r\n i = 0,\r\n len = str.length;\r\n\r\n for (; i < len;) {\r\n for (arrL = arr.length; arrL--; arr[arrL] *= baseIn);\r\n\r\n arr[0] += alphabet.indexOf(str.charAt(i++));\r\n\r\n for (j = 0; j < arr.length; j++) {\r\n\r\n if (arr[j] > baseOut - 1) {\r\n if (arr[j + 1] == null) arr[j + 1] = 0;\r\n arr[j + 1] += arr[j] / baseOut | 0;\r\n arr[j] %= baseOut;\r\n }\r\n }\r\n }\r\n\r\n return arr.reverse();\r\n }\r\n\r\n // Convert a numeric string of baseIn to a numeric string of baseOut.\r\n // If the caller is toString, we are converting from base 10 to baseOut.\r\n // If the caller is BigNumber, we are converting from baseIn to base 10.\r\n return function (str, baseIn, baseOut, sign, callerIsToString) {\r\n var alphabet, d, e, k, r, x, xc, y,\r\n i = str.indexOf('.'),\r\n dp = DECIMAL_PLACES,\r\n rm = ROUNDING_MODE;\r\n\r\n // Non-integer.\r\n if (i >= 0) {\r\n k = POW_PRECISION;\r\n\r\n // Unlimited precision.\r\n POW_PRECISION = 0;\r\n str = str.replace('.', '');\r\n y = new BigNumber(baseIn);\r\n x = y.pow(str.length - i);\r\n POW_PRECISION = k;\r\n\r\n // Convert str as if an integer, then restore the fraction part by dividing the\r\n // result by its base raised to a power.\r\n\r\n y.c = toBaseOut(toFixedPoint(coeffToString(x.c), x.e, '0'),\r\n 10, baseOut, decimal);\r\n y.e = y.c.length;\r\n }\r\n\r\n // Convert the number as integer.\r\n\r\n xc = toBaseOut(str, baseIn, baseOut, callerIsToString\r\n ? (alphabet = ALPHABET, decimal)\r\n : (alphabet = decimal, ALPHABET));\r\n\r\n // xc now represents str as an integer and converted to baseOut. e is the exponent.\r\n e = k = xc.length;\r\n\r\n // Remove trailing zeros.\r\n for (; xc[--k] == 0; xc.pop());\r\n\r\n // Zero?\r\n if (!xc[0]) return alphabet.charAt(0);\r\n\r\n // Does str represent an integer? If so, no need for the division.\r\n if (i < 0) {\r\n --e;\r\n } else {\r\n x.c = xc;\r\n x.e = e;\r\n\r\n // The sign is needed for correct rounding.\r\n x.s = sign;\r\n x = div(x, y, dp, rm, baseOut);\r\n xc = x.c;\r\n r = x.r;\r\n e = x.e;\r\n }\r\n\r\n // xc now represents str converted to baseOut.\r\n\r\n // The index of the rounding digit.\r\n d = e + dp + 1;\r\n\r\n // The rounding digit: the digit to the right of the digit that may be rounded up.\r\n i = xc[d];\r\n\r\n // Look at the rounding digits and mode to determine whether to round up.\r\n\r\n k = baseOut / 2;\r\n r = r || d < 0 || xc[d + 1] != null;\r\n\r\n r = rm < 4 ? (i != null || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2))\r\n : i > k || i == k &&(rm == 4 || r || rm == 6 && xc[d - 1] & 1 ||\r\n rm == (x.s < 0 ? 8 : 7));\r\n\r\n // If the index of the rounding digit is not greater than zero, or xc represents\r\n // zero, then the result of the base conversion is zero or, if rounding up, a value\r\n // such as 0.00001.\r\n if (d < 1 || !xc[0]) {\r\n\r\n // 1^-dp or 0\r\n str = r ? toFixedPoint(alphabet.charAt(1), -dp, alphabet.charAt(0)) : alphabet.charAt(0);\r\n } else {\r\n\r\n // Truncate xc to the required number of decimal places.\r\n xc.length = d;\r\n\r\n // Round up?\r\n if (r) {\r\n\r\n // Rounding up may mean the previous digit has to be rounded up and so on.\r\n for (--baseOut; ++xc[--d] > baseOut;) {\r\n xc[d] = 0;\r\n\r\n if (!d) {\r\n ++e;\r\n xc = [1].concat(xc);\r\n }\r\n }\r\n }\r\n\r\n // Determine trailing zeros.\r\n for (k = xc.length; !xc[--k];);\r\n\r\n // E.g. [4, 11, 15] becomes 4bf.\r\n for (i = 0, str = ''; i <= k; str += alphabet.charAt(xc[i++]));\r\n\r\n // Add leading zeros, decimal point and trailing zeros as required.\r\n str = toFixedPoint(str, e, alphabet.charAt(0));\r\n }\r\n\r\n // The caller will add the sign.\r\n return str;\r\n };\r\n })();\r\n\r\n\r\n // Perform division in the specified base. Called by div and convertBase.\r\n div = (function () {\r\n\r\n // Assume non-zero x and k.\r\n function multiply(x, k, base) {\r\n var m, temp, xlo, xhi,\r\n carry = 0,\r\n i = x.length,\r\n klo = k % SQRT_BASE,\r\n khi = k / SQRT_BASE | 0;\r\n\r\n for (x = x.slice(); i--;) {\r\n xlo = x[i] % SQRT_BASE;\r\n xhi = x[i] / SQRT_BASE | 0;\r\n m = khi * xlo + xhi * klo;\r\n temp = klo * xlo + ((m % SQRT_BASE) * SQRT_BASE) + carry;\r\n carry = (temp / base | 0) + (m / SQRT_BASE | 0) + khi * xhi;\r\n x[i] = temp % base;\r\n }\r\n\r\n if (carry) x = [carry].concat(x);\r\n\r\n return x;\r\n }\r\n\r\n function compare(a, b, aL, bL) {\r\n var i, cmp;\r\n\r\n if (aL != bL) {\r\n cmp = aL > bL ? 1 : -1;\r\n } else {\r\n\r\n for (i = cmp = 0; i < aL; i++) {\r\n\r\n if (a[i] != b[i]) {\r\n cmp = a[i] > b[i] ? 1 : -1;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n return cmp;\r\n }\r\n\r\n function subtract(a, b, aL, base) {\r\n var i = 0;\r\n\r\n // Subtract b from a.\r\n for (; aL--;) {\r\n a[aL] -= i;\r\n i = a[aL] < b[aL] ? 1 : 0;\r\n a[aL] = i * base + a[aL] - b[aL];\r\n }\r\n\r\n // Remove leading zeros.\r\n for (; !a[0] && a.length > 1; a.splice(0, 1));\r\n }\r\n\r\n // x: dividend, y: divisor.\r\n return function (x, y, dp, rm, base) {\r\n var cmp, e, i, more, n, prod, prodL, q, qc, rem, remL, rem0, xi, xL, yc0,\r\n yL, yz,\r\n s = x.s == y.s ? 1 : -1,\r\n xc = x.c,\r\n yc = y.c;\r\n\r\n // Either NaN, Infinity or 0?\r\n if (!xc || !xc[0] || !yc || !yc[0]) {\r\n\r\n return new BigNumber(\r\n\r\n // Return NaN if either NaN, or both Infinity or 0.\r\n !x.s || !y.s || (xc ? yc && xc[0] == yc[0] : !yc) ? NaN :\r\n\r\n // Return \u00B10 if x is \u00B10 or y is \u00B1Infinity, or return \u00B1Infinity as y is \u00B10.\r\n xc && xc[0] == 0 || !yc ? s * 0 : s / 0\r\n );\r\n }\r\n\r\n q = new BigNumber(s);\r\n qc = q.c = [];\r\n e = x.e - y.e;\r\n s = dp + e + 1;\r\n\r\n if (!base) {\r\n base = BASE;\r\n e = bitFloor(x.e / LOG_BASE) - bitFloor(y.e / LOG_BASE);\r\n s = s / LOG_BASE | 0;\r\n }\r\n\r\n // Result exponent may be one less then the current value of e.\r\n // The coefficients of the BigNumbers from convertBase may have trailing zeros.\r\n for (i = 0; yc[i] == (xc[i] || 0); i++);\r\n\r\n if (yc[i] > (xc[i] || 0)) e--;\r\n\r\n if (s < 0) {\r\n qc.push(1);\r\n more = true;\r\n } else {\r\n xL = xc.length;\r\n yL = yc.length;\r\n i = 0;\r\n s += 2;\r\n\r\n // Normalise xc and yc so highest order digit of yc is >= base / 2.\r\n\r\n n = mathfloor(base / (yc[0] + 1));\r\n\r\n // Not necessary, but to handle odd bases where yc[0] == (base / 2) - 1.\r\n // if (n > 1 || n++ == 1 && yc[0] < base / 2) {\r\n if (n > 1) {\r\n yc = multiply(yc, n, base);\r\n xc = multiply(xc, n, base);\r\n yL = yc.length;\r\n xL = xc.length;\r\n }\r\n\r\n xi = yL;\r\n rem = xc.slice(0, yL);\r\n remL = rem.length;\r\n\r\n // Add zeros to make remainder as long as divisor.\r\n for (; remL < yL; rem[remL++] = 0);\r\n yz = yc.slice();\r\n yz = [0].concat(yz);\r\n yc0 = yc[0];\r\n if (yc[1] >= base / 2) yc0++;\r\n // Not necessary, but to prevent trial digit n > base, when using base 3.\r\n // else if (base == 3 && yc0 == 1) yc0 = 1 + 1e-15;\r\n\r\n do {\r\n n = 0;\r\n\r\n // Compare divisor and remainder.\r\n cmp = compare(yc, rem, yL, remL);\r\n\r\n // If divisor < remainder.\r\n if (cmp < 0) {\r\n\r\n // Calculate trial digit, n.\r\n\r\n rem0 = rem[0];\r\n if (yL != remL) rem0 = rem0 * base + (rem[1] || 0);\r\n\r\n // n is how many times the divisor goes into the current remainder.\r\n n = mathfloor(rem0 / yc0);\r\n\r\n // Algorithm:\r\n // product = divisor multiplied by trial digit (n).\r\n // Compare product and remainder.\r\n // If product is greater than remainder:\r\n // Subtract divisor from product, decrement trial digit.\r\n // Subtract product from remainder.\r\n // If product was less than remainder at the last compare:\r\n // Compare new remainder and divisor.\r\n // If remainder is greater than divisor:\r\n // Subtract divisor from remainder, increment trial digit.\r\n\r\n if (n > 1) {\r\n\r\n // n may be > base only when base is 3.\r\n if (n >= base) n = base - 1;\r\n\r\n // product = divisor * trial digit.\r\n prod = multiply(yc, n, base);\r\n prodL = prod.length;\r\n remL = rem.length;\r\n\r\n // Compare product and remainder.\r\n // If product > remainder then trial digit n too high.\r\n // n is 1 too high about 5% of the time, and is not known to have\r\n // ever been more than 1 too high.\r\n while (compare(prod, rem, prodL, remL) == 1) {\r\n n--;\r\n\r\n // Subtract divisor from product.\r\n subtract(prod, yL < prodL ? yz : yc, prodL, base);\r\n prodL = prod.length;\r\n cmp = 1;\r\n }\r\n } else {\r\n\r\n // n is 0 or 1, cmp is -1.\r\n // If n is 0, there is no need to compare yc and rem again below,\r\n // so change cmp to 1 to avoid it.\r\n // If n is 1, leave cmp as -1, so yc and rem are compared again.\r\n if (n == 0) {\r\n\r\n // divisor < remainder, so n must be at least 1.\r\n cmp = n = 1;\r\n }\r\n\r\n // product = divisor\r\n prod = yc.slice();\r\n prodL = prod.length;\r\n }\r\n\r\n if (prodL < remL) prod = [0].concat(prod);\r\n\r\n // Subtract product from remainder.\r\n subtract(rem, prod, remL, base);\r\n remL = rem.length;\r\n\r\n // If product was < remainder.\r\n if (cmp == -1) {\r\n\r\n // Compare divisor and new remainder.\r\n // If divisor < new remainder, subtract divisor from remainder.\r\n // Trial digit n too low.\r\n // n is 1 too low about 5% of the time, and very rarely 2 too low.\r\n while (compare(yc, rem, yL, remL) < 1) {\r\n n++;\r\n\r\n // Subtract divisor from remainder.\r\n subtract(rem, yL < remL ? yz : yc, remL, base);\r\n remL = rem.length;\r\n }\r\n }\r\n } else if (cmp === 0) {\r\n n++;\r\n rem = [0];\r\n } // else cmp === 1 and n will be 0\r\n\r\n // Add the next digit, n, to the result array.\r\n qc[i++] = n;\r\n\r\n // Update the remainder.\r\n if (rem[0]) {\r\n rem[remL++] = xc[xi] || 0;\r\n } else {\r\n rem = [xc[xi]];\r\n remL = 1;\r\n }\r\n } while ((xi++ < xL || rem[0] != null) && s--);\r\n\r\n more = rem[0] != null;\r\n\r\n // Leading zero?\r\n if (!qc[0]) qc.splice(0, 1);\r\n }\r\n\r\n if (base == BASE) {\r\n\r\n // To calculate q.e, first get the number of digits of qc[0].\r\n for (i = 1, s = qc[0]; s >= 10; s /= 10, i++);\r\n\r\n round(q, dp + (q.e = i + e * LOG_BASE - 1) + 1, rm, more);\r\n\r\n // Caller is convertBase.\r\n } else {\r\n q.e = e;\r\n q.r = +more;\r\n }\r\n\r\n return q;\r\n };\r\n })();\r\n\r\n\r\n /*\r\n * Return a string representing the value of BigNumber n in fixed-point or exponential\r\n * notation rounded to the specified decimal places or significant digits.\r\n *\r\n * n: a BigNumber.\r\n * i: the index of the last digit required (i.e. the digit that may be rounded up).\r\n * rm: the rounding mode.\r\n * id: 1 (toExponential) or 2 (toPrecision).\r\n */\r\n function format(n, i, rm, id) {\r\n var c0, e, ne, len, str;\r\n\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n\r\n if (!n.c) return n.toString();\r\n\r\n c0 = n.c[0];\r\n ne = n.e;\r\n\r\n if (i == null) {\r\n str = coeffToString(n.c);\r\n str = id == 1 || id == 2 && (ne <= TO_EXP_NEG || ne >= TO_EXP_POS)\r\n ? toExponential(str, ne)\r\n : toFixedPoint(str, ne, '0');\r\n } else {\r\n n = round(new BigNumber(n), i, rm);\r\n\r\n // n.e may have changed if the value was rounded up.\r\n e = n.e;\r\n\r\n str = coeffToString(n.c);\r\n len = str.length;\r\n\r\n // toPrecision returns exponential notation if the number of significant digits\r\n // specified is less than the number of digits necessary to represent the integer\r\n // part of the value in fixed-point notation.\r\n\r\n // Exponential notation.\r\n if (id == 1 || id == 2 && (i <= e || e <= TO_EXP_NEG)) {\r\n\r\n // Append zeros?\r\n for (; len < i; str += '0', len++);\r\n str = toExponential(str, e);\r\n\r\n // Fixed-point notation.\r\n } else {\r\n i -= ne + (id === 2 && e > ne);\r\n str = toFixedPoint(str, e, '0');\r\n\r\n // Append zeros?\r\n if (e + 1 > len) {\r\n if (--i > 0) for (str += '.'; i--; str += '0');\r\n } else {\r\n i += e - len;\r\n if (i > 0) {\r\n if (e + 1 == len) str += '.';\r\n for (; i--; str += '0');\r\n }\r\n }\r\n }\r\n }\r\n\r\n return n.s < 0 && c0 ? '-' + str : str;\r\n }\r\n\r\n\r\n // Handle BigNumber.max and BigNumber.min.\r\n // If any number is NaN, return NaN.\r\n function maxOrMin(args, n) {\r\n var k, y,\r\n i = 1,\r\n x = new BigNumber(args[0]);\r\n\r\n for (; i < args.length; i++) {\r\n y = new BigNumber(args[i]);\r\n if (!y.s || (k = compare(x, y)) === n || k === 0 && x.s === n) {\r\n x = y;\r\n }\r\n }\r\n\r\n return x;\r\n }\r\n\r\n\r\n /*\r\n * Strip trailing zeros, calculate base 10 exponent and check against MIN_EXP and MAX_EXP.\r\n * Called by minus, plus and times.\r\n */\r\n function normalise(n, c, e) {\r\n var i = 1,\r\n j = c.length;\r\n\r\n // Remove trailing zeros.\r\n for (; !c[--j]; c.pop());\r\n\r\n // Calculate the base 10 exponent. First get the number of digits of c[0].\r\n for (j = c[0]; j >= 10; j /= 10, i++);\r\n\r\n // Overflow?\r\n if ((e = i + e * LOG_BASE - 1) > MAX_EXP) {\r\n\r\n // Infinity.\r\n n.c = n.e = null;\r\n\r\n // Underflow?\r\n } else if (e < MIN_EXP) {\r\n\r\n // Zero.\r\n n.c = [n.e = 0];\r\n } else {\r\n n.e = e;\r\n n.c = c;\r\n }\r\n\r\n return n;\r\n }\r\n\r\n\r\n // Handle values that fail the validity test in BigNumber.\r\n parseNumeric = (function () {\r\n var basePrefix = /^(-?)0([xbo])(?=\\w[\\w.]*$)/i,\r\n dotAfter = /^([^.]+)\\.$/,\r\n dotBefore = /^\\.([^.]+)$/,\r\n isInfinityOrNaN = /^-?(Infinity|NaN)$/,\r\n whitespaceOrPlus = /^\\s*\\+(?=[\\w.])|^\\s+|\\s+$/g;\r\n\r\n return function (x, str, isNum, b) {\r\n var base,\r\n s = isNum ? str : str.replace(whitespaceOrPlus, '');\r\n\r\n // No exception on \u00B1Infinity or NaN.\r\n if (isInfinityOrNaN.test(s)) {\r\n x.s = isNaN(s) ? null : s < 0 ? -1 : 1;\r\n } else {\r\n if (!isNum) {\r\n\r\n // basePrefix = /^(-?)0([xbo])(?=\\w[\\w.]*$)/i\r\n s = s.replace(basePrefix, function (m, p1, p2) {\r\n base = (p2 = p2.toLowerCase()) == 'x' ? 16 : p2 == 'b' ? 2 : 8;\r\n return !b || b == base ? p1 : m;\r\n });\r\n\r\n if (b) {\r\n base = b;\r\n\r\n // E.g. '1.' to '1', '.1' to '0.1'\r\n s = s.replace(dotAfter, '$1').replace(dotBefore, '0.$1');\r\n }\r\n\r\n if (str != s) return new BigNumber(s, base);\r\n }\r\n\r\n // '[BigNumber Error] Not a number: {n}'\r\n // '[BigNumber Error] Not a base {b} number: {n}'\r\n if (BigNumber.DEBUG) {\r\n throw Error\r\n (bignumberError + 'Not a' + (b ? ' base ' + b : '') + ' number: ' + str);\r\n }\r\n\r\n // NaN\r\n x.s = null;\r\n }\r\n\r\n x.c = x.e = null;\r\n }\r\n })();\r\n\r\n\r\n /*\r\n * Round x to sd significant digits using rounding mode rm. Check for over/under-flow.\r\n * If r is truthy, it is known that there are more digits after the rounding digit.\r\n */\r\n function round(x, sd, rm, r) {\r\n var d, i, j, k, n, ni, rd,\r\n xc = x.c,\r\n pows10 = POWS_TEN;\r\n\r\n // if x is not Infinity or NaN...\r\n if (xc) {\r\n\r\n // rd is the rounding digit, i.e. the digit after the digit that may be rounded up.\r\n // n is a base 1e14 number, the value of the element of array x.c containing rd.\r\n // ni is the index of n within x.c.\r\n // d is the number of digits of n.\r\n // i is the index of rd within n including leading zeros.\r\n // j is the actual index of rd within n (if < 0, rd is a leading zero).\r\n out: {\r\n\r\n // Get the number of digits of the first element of xc.\r\n for (d = 1, k = xc[0]; k >= 10; k /= 10, d++);\r\n i = sd - d;\r\n\r\n // If the rounding digit is in the first element of xc...\r\n if (i < 0) {\r\n i += LOG_BASE;\r\n j = sd;\r\n n = xc[ni = 0];\r\n\r\n // Get the rounding digit at index j of n.\r\n rd = mathfloor(n / pows10[d - j - 1] % 10);\r\n } else {\r\n ni = mathceil((i + 1) / LOG_BASE);\r\n\r\n if (ni >= xc.length) {\r\n\r\n if (r) {\r\n\r\n // Needed by sqrt.\r\n for (; xc.length <= ni; xc.push(0));\r\n n = rd = 0;\r\n d = 1;\r\n i %= LOG_BASE;\r\n j = i - LOG_BASE + 1;\r\n } else {\r\n break out;\r\n }\r\n } else {\r\n n = k = xc[ni];\r\n\r\n // Get the number of digits of n.\r\n for (d = 1; k >= 10; k /= 10, d++);\r\n\r\n // Get the index of rd within n.\r\n i %= LOG_BASE;\r\n\r\n // Get the index of rd within n, adjusted for leading zeros.\r\n // The number of leading zeros of n is given by LOG_BASE - d.\r\n j = i - LOG_BASE + d;\r\n\r\n // Get the rounding digit at index j of n.\r\n rd = j < 0 ? 0 : mathfloor(n / pows10[d - j - 1] % 10);\r\n }\r\n }\r\n\r\n r = r || sd < 0 ||\r\n\r\n // Are there any non-zero digits after the rounding digit?\r\n // The expression n % pows10[d - j - 1] returns all digits of n to the right\r\n // of the digit at j, e.g. if n is 908714 and j is 2, the expression gives 714.\r\n xc[ni + 1] != null || (j < 0 ? n : n % pows10[d - j - 1]);\r\n\r\n r = rm < 4\r\n ? (rd || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2))\r\n : rd > 5 || rd == 5 && (rm == 4 || r || rm == 6 &&\r\n\r\n // Check whether the digit to the left of the rounding digit is odd.\r\n ((i > 0 ? j > 0 ? n / pows10[d - j] : 0 : xc[ni - 1]) % 10) & 1 ||\r\n rm == (x.s < 0 ? 8 : 7));\r\n\r\n if (sd < 1 || !xc[0]) {\r\n xc.length = 0;\r\n\r\n if (r) {\r\n\r\n // Convert sd to decimal places.\r\n sd -= x.e + 1;\r\n\r\n // 1, 0.1, 0.01, 0.001, 0.0001 etc.\r\n xc[0] = pows10[(LOG_BASE - sd % LOG_BASE) % LOG_BASE];\r\n x.e = -sd || 0;\r\n } else {\r\n\r\n // Zero.\r\n xc[0] = x.e = 0;\r\n }\r\n\r\n return x;\r\n }\r\n\r\n // Remove excess digits.\r\n if (i == 0) {\r\n xc.length = ni;\r\n k = 1;\r\n ni--;\r\n } else {\r\n xc.length = ni + 1;\r\n k = pows10[LOG_BASE - i];\r\n\r\n // E.g. 56700 becomes 56000 if 7 is the rounding digit.\r\n // j > 0 means i > number of leading zeros of n.\r\n xc[ni] = j > 0 ? mathfloor(n / pows10[d - j] % pows10[j]) * k : 0;\r\n }\r\n\r\n // Round up?\r\n if (r) {\r\n\r\n for (; ;) {\r\n\r\n // If the digit to be rounded up is in the first element of xc...\r\n if (ni == 0) {\r\n\r\n // i will be the length of xc[0] before k is added.\r\n for (i = 1, j = xc[0]; j >= 10; j /= 10, i++);\r\n j = xc[0] += k;\r\n for (k = 1; j >= 10; j /= 10, k++);\r\n\r\n // if i != k the length has increased.\r\n if (i != k) {\r\n x.e++;\r\n if (xc[0] == BASE) xc[0] = 1;\r\n }\r\n\r\n break;\r\n } else {\r\n xc[ni] += k;\r\n if (xc[ni] != BASE) break;\r\n xc[ni--] = 0;\r\n k = 1;\r\n }\r\n }\r\n }\r\n\r\n // Remove trailing zeros.\r\n for (i = xc.length; xc[--i] === 0; xc.pop());\r\n }\r\n\r\n // Overflow? Infinity.\r\n if (x.e > MAX_EXP) {\r\n x.c = x.e = null;\r\n\r\n // Underflow? Zero.\r\n } else if (x.e < MIN_EXP) {\r\n x.c = [x.e = 0];\r\n }\r\n }\r\n\r\n return x;\r\n }\r\n\r\n\r\n function valueOf(n) {\r\n var str,\r\n e = n.e;\r\n\r\n if (e === null) return n.toString();\r\n\r\n str = coeffToString(n.c);\r\n\r\n str = e <= TO_EXP_NEG || e >= TO_EXP_POS\r\n ? toExponential(str, e)\r\n : toFixedPoint(str, e, '0');\r\n\r\n return n.s < 0 ? '-' + str : str;\r\n }\r\n\r\n\r\n // PROTOTYPE/INSTANCE METHODS\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the absolute value of this BigNumber.\r\n */\r\n P.absoluteValue = P.abs = function () {\r\n var x = new BigNumber(this);\r\n if (x.s < 0) x.s = 1;\r\n return x;\r\n };\r\n\r\n\r\n /*\r\n * Return\r\n * 1 if the value of this BigNumber is greater than the value of BigNumber(y, b),\r\n * -1 if the value of this BigNumber is less than the value of BigNumber(y, b),\r\n * 0 if they have the same value,\r\n * or null if the value of either is NaN.\r\n */\r\n P.comparedTo = function (y, b) {\r\n return compare(this, new BigNumber(y, b));\r\n };\r\n\r\n\r\n /*\r\n * If dp is undefined or null or true or false, return the number of decimal places of the\r\n * value of this BigNumber, or null if the value of this BigNumber is \u00B1Infinity or NaN.\r\n *\r\n * Otherwise, if dp is a number, return a new BigNumber whose value is the value of this\r\n * BigNumber rounded to a maximum of dp decimal places using rounding mode rm, or\r\n * ROUNDING_MODE if rm is omitted.\r\n *\r\n * [dp] {number} Decimal places: integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n */\r\n P.decimalPlaces = P.dp = function (dp, rm) {\r\n var c, n, v,\r\n x = this;\r\n\r\n if (dp != null) {\r\n intCheck(dp, 0, MAX);\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n\r\n return round(new BigNumber(x), dp + x.e + 1, rm);\r\n }\r\n\r\n if (!(c = x.c)) return null;\r\n n = ((v = c.length - 1) - bitFloor(this.e / LOG_BASE)) * LOG_BASE;\r\n\r\n // Subtract the number of trailing zeros of the last number.\r\n if (v = c[v]) for (; v % 10 == 0; v /= 10, n--);\r\n if (n < 0) n = 0;\r\n\r\n return n;\r\n };\r\n\r\n\r\n /*\r\n * n / 0 = I\r\n * n / N = N\r\n * n / I = 0\r\n * 0 / n = 0\r\n * 0 / 0 = N\r\n * 0 / N = N\r\n * 0 / I = 0\r\n * N / n = N\r\n * N / 0 = N\r\n * N / N = N\r\n * N / I = N\r\n * I / n = I\r\n * I / 0 = I\r\n * I / N = N\r\n * I / I = N\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber divided by the value of\r\n * BigNumber(y, b), rounded according to DECIMAL_PLACES and ROUNDING_MODE.\r\n */\r\n P.dividedBy = P.div = function (y, b) {\r\n return div(this, new BigNumber(y, b), DECIMAL_PLACES, ROUNDING_MODE);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the integer part of dividing the value of this\r\n * BigNumber by the value of BigNumber(y, b).\r\n */\r\n P.dividedToIntegerBy = P.idiv = function (y, b) {\r\n return div(this, new BigNumber(y, b), 0, 1);\r\n };\r\n\r\n\r\n /*\r\n * Return a BigNumber whose value is the value of this BigNumber exponentiated by n.\r\n *\r\n * If m is present, return the result modulo m.\r\n * If n is negative round according to DECIMAL_PLACES and ROUNDING_MODE.\r\n * If POW_PRECISION is non-zero and m is not present, round to POW_PRECISION using ROUNDING_MODE.\r\n *\r\n * The modular power operation works efficiently when x, n, and m are integers, otherwise it\r\n * is equivalent to calculating x.exponentiatedBy(n).modulo(m) with a POW_PRECISION of 0.\r\n *\r\n * n {number|string|BigNumber} The exponent. An integer.\r\n * [m] {number|string|BigNumber} The modulus.\r\n *\r\n * '[BigNumber Error] Exponent not an integer: {n}'\r\n */\r\n P.exponentiatedBy = P.pow = function (n, m) {\r\n var half, isModExp, i, k, more, nIsBig, nIsNeg, nIsOdd, y,\r\n x = this;\r\n\r\n n = new BigNumber(n);\r\n\r\n // Allow NaN and \u00B1Infinity, but not other non-integers.\r\n if (n.c && !n.isInteger()) {\r\n throw Error\r\n (bignumberError + 'Exponent not an integer: ' + valueOf(n));\r\n }\r\n\r\n if (m != null) m = new BigNumber(m);\r\n\r\n // Exponent of MAX_SAFE_INTEGER is 15.\r\n nIsBig = n.e > 14;\r\n\r\n // If x is NaN, \u00B1Infinity, \u00B10 or \u00B11, or n is \u00B1Infinity, NaN or \u00B10.\r\n if (!x.c || !x.c[0] || x.c[0] == 1 && !x.e && x.c.length == 1 || !n.c || !n.c[0]) {\r\n\r\n // The sign of the result of pow when x is negative depends on the evenness of n.\r\n // If +n overflows to \u00B1Infinity, the evenness of n would be not be known.\r\n y = new BigNumber(Math.pow(+valueOf(x), nIsBig ? n.s * (2 - isOdd(n)) : +valueOf(n)));\r\n return m ? y.mod(m) : y;\r\n }\r\n\r\n nIsNeg = n.s < 0;\r\n\r\n if (m) {\r\n\r\n // x % m returns NaN if abs(m) is zero, or m is NaN.\r\n if (m.c ? !m.c[0] : !m.s) return new BigNumber(NaN);\r\n\r\n isModExp = !nIsNeg && x.isInteger() && m.isInteger();\r\n\r\n if (isModExp) x = x.mod(m);\r\n\r\n // Overflow to \u00B1Infinity: >=2**1e10 or >=1.0000024**1e15.\r\n // Underflow to \u00B10: <=0.79**1e10 or <=0.9999975**1e15.\r\n } else if (n.e > 9 && (x.e > 0 || x.e < -1 || (x.e == 0\r\n // [1, 240000000]\r\n ? x.c[0] > 1 || nIsBig && x.c[1] >= 24e7\r\n // [80000000000000] [99999750000000]\r\n : x.c[0] < 8e13 || nIsBig && x.c[0] <= 9999975e7))) {\r\n\r\n // If x is negative and n is odd, k = -0, else k = 0.\r\n k = x.s < 0 && isOdd(n) ? -0 : 0;\r\n\r\n // If x >= 1, k = \u00B1Infinity.\r\n if (x.e > -1) k = 1 / k;\r\n\r\n // If n is negative return \u00B10, else return \u00B1Infinity.\r\n return new BigNumber(nIsNeg ? 1 / k : k);\r\n\r\n } else if (POW_PRECISION) {\r\n\r\n // Truncating each coefficient array to a length of k after each multiplication\r\n // equates to truncating significant digits to POW_PRECISION + [28, 41],\r\n // i.e. there will be a minimum of 28 guard digits retained.\r\n k = mathceil(POW_PRECISION / LOG_BASE + 2);\r\n }\r\n\r\n if (nIsBig) {\r\n half = new BigNumber(0.5);\r\n if (nIsNeg) n.s = 1;\r\n nIsOdd = isOdd(n);\r\n } else {\r\n i = Math.abs(+valueOf(n));\r\n nIsOdd = i % 2;\r\n }\r\n\r\n y = new BigNumber(ONE);\r\n\r\n // Performs 54 loop iterations for n of 9007199254740991.\r\n for (; ;) {\r\n\r\n if (nIsOdd) {\r\n y = y.times(x);\r\n if (!y.c) break;\r\n\r\n if (k) {\r\n if (y.c.length > k) y.c.length = k;\r\n } else if (isModExp) {\r\n y = y.mod(m); //y = y.minus(div(y, m, 0, MODULO_MODE).times(m));\r\n }\r\n }\r\n\r\n if (i) {\r\n i = mathfloor(i / 2);\r\n if (i === 0) break;\r\n nIsOdd = i % 2;\r\n } else {\r\n n = n.times(half);\r\n round(n, n.e + 1, 1);\r\n\r\n if (n.e > 14) {\r\n nIsOdd = isOdd(n);\r\n } else {\r\n i = +valueOf(n);\r\n if (i === 0) break;\r\n nIsOdd = i % 2;\r\n }\r\n }\r\n\r\n x = x.times(x);\r\n\r\n if (k) {\r\n if (x.c && x.c.length > k) x.c.length = k;\r\n } else if (isModExp) {\r\n x = x.mod(m); //x = x.minus(div(x, m, 0, MODULO_MODE).times(m));\r\n }\r\n }\r\n\r\n if (isModExp) return y;\r\n if (nIsNeg) y = ONE.div(y);\r\n\r\n return m ? y.mod(m) : k ? round(y, POW_PRECISION, ROUNDING_MODE, more) : y;\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the value of this BigNumber rounded to an integer\r\n * using rounding mode rm, or ROUNDING_MODE if rm is omitted.\r\n *\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {rm}'\r\n */\r\n P.integerValue = function (rm) {\r\n var n = new BigNumber(this);\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n return round(n, n.e + 1, rm);\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is equal to the value of BigNumber(y, b),\r\n * otherwise return false.\r\n */\r\n P.isEqualTo = P.eq = function (y, b) {\r\n return compare(this, new BigNumber(y, b)) === 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is a finite number, otherwise return false.\r\n */\r\n P.isFinite = function () {\r\n return !!this.c;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is greater than the value of BigNumber(y, b),\r\n * otherwise return false.\r\n */\r\n P.isGreaterThan = P.gt = function (y, b) {\r\n return compare(this, new BigNumber(y, b)) > 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is greater than or equal to the value of\r\n * BigNumber(y, b), otherwise return false.\r\n */\r\n P.isGreaterThanOrEqualTo = P.gte = function (y, b) {\r\n return (b = compare(this, new BigNumber(y, b))) === 1 || b === 0;\r\n\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is an integer, otherwise return false.\r\n */\r\n P.isInteger = function () {\r\n return !!this.c && bitFloor(this.e / LOG_BASE) > this.c.length - 2;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is less than the value of BigNumber(y, b),\r\n * otherwise return false.\r\n */\r\n P.isLessThan = P.lt = function (y, b) {\r\n return compare(this, new BigNumber(y, b)) < 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is less than or equal to the value of\r\n * BigNumber(y, b), otherwise return false.\r\n */\r\n P.isLessThanOrEqualTo = P.lte = function (y, b) {\r\n return (b = compare(this, new BigNumber(y, b))) === -1 || b === 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is NaN, otherwise return false.\r\n */\r\n P.isNaN = function () {\r\n return !this.s;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is negative, otherwise return false.\r\n */\r\n P.isNegative = function () {\r\n return this.s < 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is positive, otherwise return false.\r\n */\r\n P.isPositive = function () {\r\n return this.s > 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is 0 or -0, otherwise return false.\r\n */\r\n P.isZero = function () {\r\n return !!this.c && this.c[0] == 0;\r\n };\r\n\r\n\r\n /*\r\n * n - 0 = n\r\n * n - N = N\r\n * n - I = -I\r\n * 0 - n = -n\r\n * 0 - 0 = 0\r\n * 0 - N = N\r\n * 0 - I = -I\r\n * N - n = N\r\n * N - 0 = N\r\n * N - N = N\r\n * N - I = N\r\n * I - n = I\r\n * I - 0 = I\r\n * I - N = N\r\n * I - I = N\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber minus the value of\r\n * BigNumber(y, b).\r\n */\r\n P.minus = function (y, b) {\r\n var i, j, t, xLTy,\r\n x = this,\r\n a = x.s;\r\n\r\n y = new BigNumber(y, b);\r\n b = y.s;\r\n\r\n // Either NaN?\r\n if (!a || !b) return new BigNumber(NaN);\r\n\r\n // Signs differ?\r\n if (a != b) {\r\n y.s = -b;\r\n return x.plus(y);\r\n }\r\n\r\n var xe = x.e / LOG_BASE,\r\n ye = y.e / LOG_BASE,\r\n xc = x.c,\r\n yc = y.c;\r\n\r\n if (!xe || !ye) {\r\n\r\n // Either Infinity?\r\n if (!xc || !yc) return xc ? (y.s = -b, y) : new BigNumber(yc ? x : NaN);\r\n\r\n // Either zero?\r\n if (!xc[0] || !yc[0]) {\r\n\r\n // Return y if y is non-zero, x if x is non-zero, or zero if both are zero.\r\n return yc[0] ? (y.s = -b, y) : new BigNumber(xc[0] ? x :\r\n\r\n // IEEE 754 (2008) 6.3: n - n = -0 when rounding to -Infinity\r\n ROUNDING_MODE == 3 ? -0 : 0);\r\n }\r\n }\r\n\r\n xe = bitFloor(xe);\r\n ye = bitFloor(ye);\r\n xc = xc.slice();\r\n\r\n // Determine which is the bigger number.\r\n if (a = xe - ye) {\r\n\r\n if (xLTy = a < 0) {\r\n a = -a;\r\n t = xc;\r\n } else {\r\n ye = xe;\r\n t = yc;\r\n }\r\n\r\n t.reverse();\r\n\r\n // Prepend zeros to equalise exponents.\r\n for (b = a; b--; t.push(0));\r\n t.reverse();\r\n } else {\r\n\r\n // Exponents equal. Check digit by digit.\r\n j = (xLTy = (a = xc.length) < (b = yc.length)) ? a : b;\r\n\r\n for (a = b = 0; b < j; b++) {\r\n\r\n if (xc[b] != yc[b]) {\r\n xLTy = xc[b] < yc[b];\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // x < y? Point xc to the array of the bigger number.\r\n if (xLTy) {\r\n t = xc;\r\n xc = yc;\r\n yc = t;\r\n y.s = -y.s;\r\n }\r\n\r\n b = (j = yc.length) - (i = xc.length);\r\n\r\n // Append zeros to xc if shorter.\r\n // No need to add zeros to yc if shorter as subtract only needs to start at yc.length.\r\n if (b > 0) for (; b--; xc[i++] = 0);\r\n b = BASE - 1;\r\n\r\n // Subtract yc from xc.\r\n for (; j > a;) {\r\n\r\n if (xc[--j] < yc[j]) {\r\n for (i = j; i && !xc[--i]; xc[i] = b);\r\n --xc[i];\r\n xc[j] += BASE;\r\n }\r\n\r\n xc[j] -= yc[j];\r\n }\r\n\r\n // Remove leading zeros and adjust exponent accordingly.\r\n for (; xc[0] == 0; xc.splice(0, 1), --ye);\r\n\r\n // Zero?\r\n if (!xc[0]) {\r\n\r\n // Following IEEE 754 (2008) 6.3,\r\n // n - n = +0 but n - n = -0 when rounding towards -Infinity.\r\n y.s = ROUNDING_MODE == 3 ? -1 : 1;\r\n y.c = [y.e = 0];\r\n return y;\r\n }\r\n\r\n // No need to check for Infinity as +x - +y != Infinity && -x - -y != Infinity\r\n // for finite x and y.\r\n return normalise(y, xc, ye);\r\n };\r\n\r\n\r\n /*\r\n * n % 0 = N\r\n * n % N = N\r\n * n % I = n\r\n * 0 % n = 0\r\n * -0 % n = -0\r\n * 0 % 0 = N\r\n * 0 % N = N\r\n * 0 % I = 0\r\n * N % n = N\r\n * N % 0 = N\r\n * N % N = N\r\n * N % I = N\r\n * I % n = N\r\n * I % 0 = N\r\n * I % N = N\r\n * I % I = N\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber modulo the value of\r\n * BigNumber(y, b). The result depends on the value of MODULO_MODE.\r\n */\r\n P.modulo = P.mod = function (y, b) {\r\n var q, s,\r\n x = this;\r\n\r\n y = new BigNumber(y, b);\r\n\r\n // Return NaN if x is Infinity or NaN, or y is NaN or zero.\r\n if (!x.c || !y.s || y.c && !y.c[0]) {\r\n return new BigNumber(NaN);\r\n\r\n // Return x if y is Infinity or x is zero.\r\n } else if (!y.c || x.c && !x.c[0]) {\r\n return new BigNumber(x);\r\n }\r\n\r\n if (MODULO_MODE == 9) {\r\n\r\n // Euclidian division: q = sign(y) * floor(x / abs(y))\r\n // r = x - qy where 0 <= r < abs(y)\r\n s = y.s;\r\n y.s = 1;\r\n q = div(x, y, 0, 3);\r\n y.s = s;\r\n q.s *= s;\r\n } else {\r\n q = div(x, y, 0, MODULO_MODE);\r\n }\r\n\r\n y = x.minus(q.times(y));\r\n\r\n // To match JavaScript %, ensure sign of zero is sign of dividend.\r\n if (!y.c[0] && MODULO_MODE == 1) y.s = x.s;\r\n\r\n return y;\r\n };\r\n\r\n\r\n /*\r\n * n * 0 = 0\r\n * n * N = N\r\n * n * I = I\r\n * 0 * n = 0\r\n * 0 * 0 = 0\r\n * 0 * N = N\r\n * 0 * I = N\r\n * N * n = N\r\n * N * 0 = N\r\n * N * N = N\r\n * N * I = N\r\n * I * n = I\r\n * I * 0 = N\r\n * I * N = N\r\n * I * I = I\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber multiplied by the value\r\n * of BigNumber(y, b).\r\n */\r\n P.multipliedBy = P.times = function (y, b) {\r\n var c, e, i, j, k, m, xcL, xlo, xhi, ycL, ylo, yhi, zc,\r\n base, sqrtBase,\r\n x = this,\r\n xc = x.c,\r\n yc = (y = new BigNumber(y, b)).c;\r\n\r\n // Either NaN, \u00B1Infinity or \u00B10?\r\n if (!xc || !yc || !xc[0] || !yc[0]) {\r\n\r\n // Return NaN if either is NaN, or one is 0 and the other is Infinity.\r\n if (!x.s || !y.s || xc && !xc[0] && !yc || yc && !yc[0] && !xc) {\r\n y.c = y.e = y.s = null;\r\n } else {\r\n y.s *= x.s;\r\n\r\n // Return \u00B1Infinity if either is \u00B1Infinity.\r\n if (!xc || !yc) {\r\n y.c = y.e = null;\r\n\r\n // Return \u00B10 if either is \u00B10.\r\n } else {\r\n y.c = [0];\r\n y.e = 0;\r\n }\r\n }\r\n\r\n return y;\r\n }\r\n\r\n e = bitFloor(x.e / LOG_BASE) + bitFloor(y.e / LOG_BASE);\r\n y.s *= x.s;\r\n xcL = xc.length;\r\n ycL = yc.length;\r\n\r\n // Ensure xc points to longer array and xcL to its length.\r\n if (xcL < ycL) {\r\n zc = xc;\r\n xc = yc;\r\n yc = zc;\r\n i = xcL;\r\n xcL = ycL;\r\n ycL = i;\r\n }\r\n\r\n // Initialise the result array with zeros.\r\n for (i = xcL + ycL, zc = []; i--; zc.push(0));\r\n\r\n base = BASE;\r\n sqrtBase = SQRT_BASE;\r\n\r\n for (i = ycL; --i >= 0;) {\r\n c = 0;\r\n ylo = yc[i] % sqrtBase;\r\n yhi = yc[i] / sqrtBase | 0;\r\n\r\n for (k = xcL, j = i + k; j > i;) {\r\n xlo = xc[--k] % sqrtBase;\r\n xhi = xc[k] / sqrtBase | 0;\r\n m = yhi * xlo + xhi * ylo;\r\n xlo = ylo * xlo + ((m % sqrtBase) * sqrtBase) + zc[j] + c;\r\n c = (xlo / base | 0) + (m / sqrtBase | 0) + yhi * xhi;\r\n zc[j--] = xlo % base;\r\n }\r\n\r\n zc[j] = c;\r\n }\r\n\r\n if (c) {\r\n ++e;\r\n } else {\r\n zc.splice(0, 1);\r\n }\r\n\r\n return normalise(y, zc, e);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the value of this BigNumber negated,\r\n * i.e. multiplied by -1.\r\n */\r\n P.negated = function () {\r\n var x = new BigNumber(this);\r\n x.s = -x.s || null;\r\n return x;\r\n };\r\n\r\n\r\n /*\r\n * n + 0 = n\r\n * n + N = N\r\n * n + I = I\r\n * 0 + n = n\r\n * 0 + 0 = 0\r\n * 0 + N = N\r\n * 0 + I = I\r\n * N + n = N\r\n * N + 0 = N\r\n * N + N = N\r\n * N + I = N\r\n * I + n = I\r\n * I + 0 = I\r\n * I + N = N\r\n * I + I = I\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber plus the value of\r\n * BigNumber(y, b).\r\n */\r\n P.plus = function (y, b) {\r\n var t,\r\n x = this,\r\n a = x.s;\r\n\r\n y = new BigNumber(y, b);\r\n b = y.s;\r\n\r\n // Either NaN?\r\n if (!a || !b) return new BigNumber(NaN);\r\n\r\n // Signs differ?\r\n if (a != b) {\r\n y.s = -b;\r\n return x.minus(y);\r\n }\r\n\r\n var xe = x.e / LOG_BASE,\r\n ye = y.e / LOG_BASE,\r\n xc = x.c,\r\n yc = y.c;\r\n\r\n if (!xe || !ye) {\r\n\r\n // Return \u00B1Infinity if either \u00B1Infinity.\r\n if (!xc || !yc) return new BigNumber(a / 0);\r\n\r\n // Either zero?\r\n // Return y if y is non-zero, x if x is non-zero, or zero if both are zero.\r\n if (!xc[0] || !yc[0]) return yc[0] ? y : new BigNumber(xc[0] ? x : a * 0);\r\n }\r\n\r\n xe = bitFloor(xe);\r\n ye = bitFloor(ye);\r\n xc = xc.slice();\r\n\r\n // Prepend zeros to equalise exponents. Faster to use reverse then do unshifts.\r\n if (a = xe - ye) {\r\n if (a > 0) {\r\n ye = xe;\r\n t = yc;\r\n } else {\r\n a = -a;\r\n t = xc;\r\n }\r\n\r\n t.reverse();\r\n for (; a--; t.push(0));\r\n t.reverse();\r\n }\r\n\r\n a = xc.length;\r\n b = yc.length;\r\n\r\n // Point xc to the longer array, and b to the shorter length.\r\n if (a - b < 0) {\r\n t = yc;\r\n yc = xc;\r\n xc = t;\r\n b = a;\r\n }\r\n\r\n // Only start adding at yc.length - 1 as the further digits of xc can be ignored.\r\n for (a = 0; b;) {\r\n a = (xc[--b] = xc[b] + yc[b] + a) / BASE | 0;\r\n xc[b] = BASE === xc[b] ? 0 : xc[b] % BASE;\r\n }\r\n\r\n if (a) {\r\n xc = [a].concat(xc);\r\n ++ye;\r\n }\r\n\r\n // No need to check for zero, as +x + +y != 0 && -x + -y != 0\r\n // ye = MAX_EXP + 1 possible\r\n return normalise(y, xc, ye);\r\n };\r\n\r\n\r\n /*\r\n * If sd is undefined or null or true or false, return the number of significant digits of\r\n * the value of this BigNumber, or null if the value of this BigNumber is \u00B1Infinity or NaN.\r\n * If sd is true include integer-part trailing zeros in the count.\r\n *\r\n * Otherwise, if sd is a number, return a new BigNumber whose value is the value of this\r\n * BigNumber rounded to a maximum of sd significant digits using rounding mode rm, or\r\n * ROUNDING_MODE if rm is omitted.\r\n *\r\n * sd {number|boolean} number: significant digits: integer, 1 to MAX inclusive.\r\n * boolean: whether to count integer-part trailing zeros: true or false.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}'\r\n */\r\n P.precision = P.sd = function (sd, rm) {\r\n var c, n, v,\r\n x = this;\r\n\r\n if (sd != null && sd !== !!sd) {\r\n intCheck(sd, 1, MAX);\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n\r\n return round(new BigNumber(x), sd, rm);\r\n }\r\n\r\n if (!(c = x.c)) return null;\r\n v = c.length - 1;\r\n n = v * LOG_BASE + 1;\r\n\r\n if (v = c[v]) {\r\n\r\n // Subtract the number of trailing zeros of the last element.\r\n for (; v % 10 == 0; v /= 10, n--);\r\n\r\n // Add the number of digits of the first element.\r\n for (v = c[0]; v >= 10; v /= 10, n++);\r\n }\r\n\r\n if (sd && x.e + 1 > n) n = x.e + 1;\r\n\r\n return n;\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the value of this BigNumber shifted by k places\r\n * (powers of 10). Shift to the right if n > 0, and to the left if n < 0.\r\n *\r\n * k {number} Integer, -MAX_SAFE_INTEGER to MAX_SAFE_INTEGER inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {k}'\r\n */\r\n P.shiftedBy = function (k) {\r\n intCheck(k, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);\r\n return this.times('1e' + k);\r\n };\r\n\r\n\r\n /*\r\n * sqrt(-n) = N\r\n * sqrt(N) = N\r\n * sqrt(-I) = N\r\n * sqrt(I) = I\r\n * sqrt(0) = 0\r\n * sqrt(-0) = -0\r\n *\r\n * Return a new BigNumber whose value is the square root of the value of this BigNumber,\r\n * rounded according to DECIMAL_PLACES and ROUNDING_MODE.\r\n */\r\n P.squareRoot = P.sqrt = function () {\r\n var m, n, r, rep, t,\r\n x = this,\r\n c = x.c,\r\n s = x.s,\r\n e = x.e,\r\n dp = DECIMAL_PLACES + 4,\r\n half = new BigNumber('0.5');\r\n\r\n // Negative/NaN/Infinity/zero?\r\n if (s !== 1 || !c || !c[0]) {\r\n return new BigNumber(!s || s < 0 && (!c || c[0]) ? NaN : c ? x : 1 / 0);\r\n }\r\n\r\n // Initial estimate.\r\n s = Math.sqrt(+valueOf(x));\r\n\r\n // Math.sqrt underflow/overflow?\r\n // Pass x to Math.sqrt as integer, then adjust the exponent of the result.\r\n if (s == 0 || s == 1 / 0) {\r\n n = coeffToString(c);\r\n if ((n.length + e) % 2 == 0) n += '0';\r\n s = Math.sqrt(+n);\r\n e = bitFloor((e + 1) / 2) - (e < 0 || e % 2);\r\n\r\n if (s == 1 / 0) {\r\n n = '5e' + e;\r\n } else {\r\n n = s.toExponential();\r\n n = n.slice(0, n.indexOf('e') + 1) + e;\r\n }\r\n\r\n r = new BigNumber(n);\r\n } else {\r\n r = new BigNumber(s + '');\r\n }\r\n\r\n // Check for zero.\r\n // r could be zero if MIN_EXP is changed after the this value was created.\r\n // This would cause a division by zero (x/t) and hence Infinity below, which would cause\r\n // coeffToString to throw.\r\n if (r.c[0]) {\r\n e = r.e;\r\n s = e + dp;\r\n if (s < 3) s = 0;\r\n\r\n // Newton-Raphson iteration.\r\n for (; ;) {\r\n t = r;\r\n r = half.times(t.plus(div(x, t, dp, 1)));\r\n\r\n if (coeffToString(t.c).slice(0, s) === (n = coeffToString(r.c)).slice(0, s)) {\r\n\r\n // The exponent of r may here be one less than the final result exponent,\r\n // e.g 0.0009999 (e-4) --> 0.001 (e-3), so adjust s so the rounding digits\r\n // are indexed correctly.\r\n if (r.e < e) --s;\r\n n = n.slice(s - 3, s + 1);\r\n\r\n // The 4th rounding digit may be in error by -1 so if the 4 rounding digits\r\n // are 9999 or 4999 (i.e. approaching a rounding boundary) continue the\r\n // iteration.\r\n if (n == '9999' || !rep && n == '4999') {\r\n\r\n // On the first iteration only, check to see if rounding up gives the\r\n // exact result as the nines may infinitely repeat.\r\n if (!rep) {\r\n round(t, t.e + DECIMAL_PLACES + 2, 0);\r\n\r\n if (t.times(t).eq(x)) {\r\n r = t;\r\n break;\r\n }\r\n }\r\n\r\n dp += 4;\r\n s += 4;\r\n rep = 1;\r\n } else {\r\n\r\n // If rounding digits are null, 0{0,4} or 50{0,3}, check for exact\r\n // result. If not, then there are further digits and m will be truthy.\r\n if (!+n || !+n.slice(1) && n.charAt(0) == '5') {\r\n\r\n // Truncate to the first rounding digit.\r\n round(r, r.e + DECIMAL_PLACES + 2, 1);\r\n m = !r.times(r).eq(x);\r\n }\r\n\r\n break;\r\n }\r\n }\r\n }\r\n }\r\n\r\n return round(r, r.e + DECIMAL_PLACES + 1, ROUNDING_MODE, m);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in exponential notation and\r\n * rounded using ROUNDING_MODE to dp fixed decimal places.\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n */\r\n P.toExponential = function (dp, rm) {\r\n if (dp != null) {\r\n intCheck(dp, 0, MAX);\r\n dp++;\r\n }\r\n return format(this, dp, rm, 1);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in fixed-point notation rounding\r\n * to dp fixed decimal places using rounding mode rm, or ROUNDING_MODE if rm is omitted.\r\n *\r\n * Note: as with JavaScript's number type, (-0).toFixed(0) is '0',\r\n * but e.g. (-0.00001).toFixed(0) is '-0'.\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n */\r\n P.toFixed = function (dp, rm) {\r\n if (dp != null) {\r\n intCheck(dp, 0, MAX);\r\n dp = dp + this.e + 1;\r\n }\r\n return format(this, dp, rm);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in fixed-point notation rounded\r\n * using rm or ROUNDING_MODE to dp decimal places, and formatted according to the properties\r\n * of the format or FORMAT object (see BigNumber.set).\r\n *\r\n * The formatting object may contain some or all of the properties shown below.\r\n *\r\n * FORMAT = {\r\n * prefix: '',\r\n * groupSize: 3,\r\n * secondaryGroupSize: 0,\r\n * groupSeparator: ',',\r\n * decimalSeparator: '.',\r\n * fractionGroupSize: 0,\r\n * fractionGroupSeparator: '\\xA0', // non-breaking space\r\n * suffix: ''\r\n * };\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n * [format] {object} Formatting options. See FORMAT pbject above.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n * '[BigNumber Error] Argument not an object: {format}'\r\n */\r\n P.toFormat = function (dp, rm, format) {\r\n var str,\r\n x = this;\r\n\r\n if (format == null) {\r\n if (dp != null && rm && typeof rm == 'object') {\r\n format = rm;\r\n rm = null;\r\n } else if (dp && typeof dp == 'object') {\r\n format = dp;\r\n dp = rm = null;\r\n } else {\r\n format = FORMAT;\r\n }\r\n } else if (typeof format != 'object') {\r\n throw Error\r\n (bignumberError + 'Argument not an object: ' + format);\r\n }\r\n\r\n str = x.toFixed(dp, rm);\r\n\r\n if (x.c) {\r\n var i,\r\n arr = str.split('.'),\r\n g1 = +format.groupSize,\r\n g2 = +format.secondaryGroupSize,\r\n groupSeparator = format.groupSeparator || '',\r\n intPart = arr[0],\r\n fractionPart = arr[1],\r\n isNeg = x.s < 0,\r\n intDigits = isNeg ? intPart.slice(1) : intPart,\r\n len = intDigits.length;\r\n\r\n if (g2) {\r\n i = g1;\r\n g1 = g2;\r\n g2 = i;\r\n len -= i;\r\n }\r\n\r\n if (g1 > 0 && len > 0) {\r\n i = len % g1 || g1;\r\n intPart = intDigits.substr(0, i);\r\n for (; i < len; i += g1) intPart += groupSeparator + intDigits.substr(i, g1);\r\n if (g2 > 0) intPart += groupSeparator + intDigits.slice(i);\r\n if (isNeg) intPart = '-' + intPart;\r\n }\r\n\r\n str = fractionPart\r\n ? intPart + (format.decimalSeparator || '') + ((g2 = +format.fractionGroupSize)\r\n ? fractionPart.replace(new RegExp('\\\\d{' + g2 + '}\\\\B', 'g'),\r\n '$&' + (format.fractionGroupSeparator || ''))\r\n : fractionPart)\r\n : intPart;\r\n }\r\n\r\n return (format.prefix || '') + str + (format.suffix || '');\r\n };\r\n\r\n\r\n /*\r\n * Return an array of two BigNumbers representing the value of this BigNumber as a simple\r\n * fraction with an integer numerator and an integer denominator.\r\n * The denominator will be a positive non-zero value less than or equal to the specified\r\n * maximum denominator. If a maximum denominator is not specified, the denominator will be\r\n * the lowest value necessary to represent the number exactly.\r\n *\r\n * [md] {number|string|BigNumber} Integer >= 1, or Infinity. The maximum denominator.\r\n *\r\n * '[BigNumber Error] Argument {not an integer|out of range} : {md}'\r\n */\r\n P.toFraction = function (md) {\r\n var d, d0, d1, d2, e, exp, n, n0, n1, q, r, s,\r\n x = this,\r\n xc = x.c;\r\n\r\n if (md != null) {\r\n n = new BigNumber(md);\r\n\r\n // Throw if md is less than one or is not an integer, unless it is Infinity.\r\n if (!n.isInteger() && (n.c || n.s !== 1) || n.lt(ONE)) {\r\n throw Error\r\n (bignumberError + 'Argument ' +\r\n (n.isInteger() ? 'out of range: ' : 'not an integer: ') + valueOf(n));\r\n }\r\n }\r\n\r\n if (!xc) return new BigNumber(x);\r\n\r\n d = new BigNumber(ONE);\r\n n1 = d0 = new BigNumber(ONE);\r\n d1 = n0 = new BigNumber(ONE);\r\n s = coeffToString(xc);\r\n\r\n // Determine initial denominator.\r\n // d is a power of 10 and the minimum max denominator that specifies the value exactly.\r\n e = d.e = s.length - x.e - 1;\r\n d.c[0] = POWS_TEN[(exp = e % LOG_BASE) < 0 ? LOG_BASE + exp : exp];\r\n md = !md || n.comparedTo(d) > 0 ? (e > 0 ? d : n1) : n;\r\n\r\n exp = MAX_EXP;\r\n MAX_EXP = 1 / 0;\r\n n = new BigNumber(s);\r\n\r\n // n0 = d1 = 0\r\n n0.c[0] = 0;\r\n\r\n for (; ;) {\r\n q = div(n, d, 0, 1);\r\n d2 = d0.plus(q.times(d1));\r\n if (d2.comparedTo(md) == 1) break;\r\n d0 = d1;\r\n d1 = d2;\r\n n1 = n0.plus(q.times(d2 = n1));\r\n n0 = d2;\r\n d = n.minus(q.times(d2 = d));\r\n n = d2;\r\n }\r\n\r\n d2 = div(md.minus(d0), d1, 0, 1);\r\n n0 = n0.plus(d2.times(n1));\r\n d0 = d0.plus(d2.times(d1));\r\n n0.s = n1.s = x.s;\r\n e = e * 2;\r\n\r\n // Determine which fraction is closer to x, n0/d0 or n1/d1\r\n r = div(n1, d1, e, ROUNDING_MODE).minus(x).abs().comparedTo(\r\n div(n0, d0, e, ROUNDING_MODE).minus(x).abs()) < 1 ? [n1, d1] : [n0, d0];\r\n\r\n MAX_EXP = exp;\r\n\r\n return r;\r\n };\r\n\r\n\r\n /*\r\n * Return the value of this BigNumber converted to a number primitive.\r\n */\r\n P.toNumber = function () {\r\n return +valueOf(this);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber rounded to sd significant digits\r\n * using rounding mode rm or ROUNDING_MODE. If sd is less than the number of digits\r\n * necessary to represent the integer part of the value in fixed-point notation, then use\r\n * exponential notation.\r\n *\r\n * [sd] {number} Significant digits. Integer, 1 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}'\r\n */\r\n P.toPrecision = function (sd, rm) {\r\n if (sd != null) intCheck(sd, 1, MAX);\r\n return format(this, sd, rm, 2);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in base b, or base 10 if b is\r\n * omitted. If a base is specified, including base 10, round according to DECIMAL_PLACES and\r\n * ROUNDING_MODE. If a base is not specified, and this BigNumber has a positive exponent\r\n * that is equal to or greater than TO_EXP_POS, or a negative exponent equal to or less than\r\n * TO_EXP_NEG, return exponential notation.\r\n *\r\n * [b] {number} Integer, 2 to ALPHABET.length inclusive.\r\n *\r\n * '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}'\r\n */\r\n P.toString = function (b) {\r\n var str,\r\n n = this,\r\n s = n.s,\r\n e = n.e;\r\n\r\n // Infinity or NaN?\r\n if (e === null) {\r\n if (s) {\r\n str = 'Infinity';\r\n if (s < 0) str = '-' + str;\r\n } else {\r\n str = 'NaN';\r\n }\r\n } else {\r\n if (b == null) {\r\n str = e <= TO_EXP_NEG || e >= TO_EXP_POS\r\n ? toExponential(coeffToString(n.c), e)\r\n : toFixedPoint(coeffToString(n.c), e, '0');\r\n } else if (b === 10 && alphabetHasNormalDecimalDigits) {\r\n n = round(new BigNumber(n), DECIMAL_PLACES + e + 1, ROUNDING_MODE);\r\n str = toFixedPoint(coeffToString(n.c), n.e, '0');\r\n } else {\r\n intCheck(b, 2, ALPHABET.length, 'Base');\r\n str = convertBase(toFixedPoint(coeffToString(n.c), e, '0'), 10, b, s, true);\r\n }\r\n\r\n if (s < 0 && n.c[0]) str = '-' + str;\r\n }\r\n\r\n return str;\r\n };\r\n\r\n\r\n /*\r\n * Return as toString, but do not accept a base argument, and include the minus sign for\r\n * negative zero.\r\n */\r\n P.valueOf = P.toJSON = function () {\r\n return valueOf(this);\r\n };\r\n\r\n\r\n P._isBigNumber = true;\r\n\r\n P[Symbol.toStringTag] = 'BigNumber';\r\n\r\n // Node.js v10.12.0+\r\n P[Symbol.for('nodejs.util.inspect.custom')] = P.valueOf;\r\n\r\n if (configObject != null) BigNumber.set(configObject);\r\n\r\n return BigNumber;\r\n}\r\n\r\n\r\n// PRIVATE HELPER FUNCTIONS\r\n\r\n// These functions don't need access to variables,\r\n// e.g. DECIMAL_PLACES, in the scope of the `clone` function above.\r\n\r\n\r\nfunction bitFloor(n) {\r\n var i = n | 0;\r\n return n > 0 || n === i ? i : i - 1;\r\n}\r\n\r\n\r\n// Return a coefficient array as a string of base 10 digits.\r\nfunction coeffToString(a) {\r\n var s, z,\r\n i = 1,\r\n j = a.length,\r\n r = a[0] + '';\r\n\r\n for (; i < j;) {\r\n s = a[i++] + '';\r\n z = LOG_BASE - s.length;\r\n for (; z--; s = '0' + s);\r\n r += s;\r\n }\r\n\r\n // Determine trailing zeros.\r\n for (j = r.length; r.charCodeAt(--j) === 48;);\r\n\r\n return r.slice(0, j + 1 || 1);\r\n}\r\n\r\n\r\n// Compare the value of BigNumbers x and y.\r\nfunction compare(x, y) {\r\n var a, b,\r\n xc = x.c,\r\n yc = y.c,\r\n i = x.s,\r\n j = y.s,\r\n k = x.e,\r\n l = y.e;\r\n\r\n // Either NaN?\r\n if (!i || !j) return null;\r\n\r\n a = xc && !xc[0];\r\n b = yc && !yc[0];\r\n\r\n // Either zero?\r\n if (a || b) return a ? b ? 0 : -j : i;\r\n\r\n // Signs differ?\r\n if (i != j) return i;\r\n\r\n a = i < 0;\r\n b = k == l;\r\n\r\n // Either Infinity?\r\n if (!xc || !yc) return b ? 0 : !xc ^ a ? 1 : -1;\r\n\r\n // Compare exponents.\r\n if (!b) return k > l ^ a ? 1 : -1;\r\n\r\n j = (k = xc.length) < (l = yc.length) ? k : l;\r\n\r\n // Compare digit by digit.\r\n for (i = 0; i < j; i++) if (xc[i] != yc[i]) return xc[i] > yc[i] ^ a ? 1 : -1;\r\n\r\n // Compare lengths.\r\n return k == l ? 0 : k > l ^ a ? 1 : -1;\r\n}\r\n\r\n\r\n/*\r\n * Check that n is a primitive number, an integer, and in range, otherwise throw.\r\n */\r\nfunction intCheck(n, min, max, name) {\r\n if (n < min || n > max || n !== mathfloor(n)) {\r\n throw Error\r\n (bignumberError + (name || 'Argument') + (typeof n == 'number'\r\n ? n < min || n > max ? ' out of range: ' : ' not an integer: '\r\n : ' not a primitive number: ') + String(n));\r\n }\r\n}\r\n\r\n\r\n// Assumes finite n.\r\nfunction isOdd(n) {\r\n var k = n.c.length - 1;\r\n return bitFloor(n.e / LOG_BASE) == k && n.c[k] % 2 != 0;\r\n}\r\n\r\n\r\nfunction toExponential(str, e) {\r\n return (str.length > 1 ? str.charAt(0) + '.' + str.slice(1) : str) +\r\n (e < 0 ? 'e' : 'e+') + e;\r\n}\r\n\r\n\r\nfunction toFixedPoint(str, e, z) {\r\n var len, zs;\r\n\r\n // Negative exponent?\r\n if (e < 0) {\r\n\r\n // Prepend zeros.\r\n for (zs = z + '.'; ++e; zs += z);\r\n str = zs + str;\r\n\r\n // Positive exponent\r\n } else {\r\n len = str.length;\r\n\r\n // Append zeros.\r\n if (++e > len) {\r\n for (zs = z, e -= len; --e; zs += z);\r\n str += zs;\r\n } else if (e < len) {\r\n str = str.slice(0, e) + '.' + str.slice(e);\r\n }\r\n }\r\n\r\n return str;\r\n}\r\n\r\n\r\n// EXPORT\r\n\r\n\r\nexport var BigNumber = clone();\r\n\r\nexport default BigNumber;\r\n", "type Comparator = (a: T, b: T) => number;\ntype Predicate = (value: T) => boolean;\n\nclass SplayTreeNode> {\n readonly key: K;\n\n left: Node | null = null;\n right: Node | null = null;\n\n constructor(key: K) {\n this.key = key;\n }\n}\n\nclass SplayTreeSetNode extends SplayTreeNode> {\n constructor(key: K) {\n super(key);\n }\n}\n\nclass SplayTreeMapNode extends SplayTreeNode> {\n readonly value: V;\n\n constructor(key: K, value: V) {\n super(key);\n this.value = value;\n }\n\n replaceValue(value: V) {\n const node = new SplayTreeMapNode(this.key, value);\n node.left = this.left;\n node.right = this.right;\n return node;\n }\n}\n\nabstract class SplayTree> {\n protected abstract root: Node | null;\n\n public size = 0;\n\n protected modificationCount = 0;\n\n protected splayCount = 0;\n\n protected abstract compare: Comparator;\n\n protected abstract validKey: Predicate;\n\n protected splay(key: K) {\n const root = this.root;\n if (root == null) {\n this.compare(key, key);\n return -1;\n }\n\n let right: Node | null = null;\n let newTreeRight: Node | null = null;\n let left: Node | null = null;\n let newTreeLeft: Node | null = null;\n let current = root;\n const compare = this.compare;\n let comp: number;\n while (true) {\n comp = compare(current.key, key);\n if (comp > 0) {\n let currentLeft = current.left;\n if (currentLeft == null) break;\n comp = compare(currentLeft.key, key);\n if (comp > 0) {\n current.left = currentLeft.right;\n currentLeft.right = current;\n current = currentLeft;\n currentLeft = current.left;\n if (currentLeft == null) break;\n }\n if (right == null) {\n newTreeRight = current;\n } else {\n right.left = current;\n }\n right = current;\n current = currentLeft;\n } else if (comp < 0) {\n let currentRight = current.right;\n if (currentRight == null) break;\n comp = compare(currentRight.key, key);\n if (comp < 0) {\n current.right = currentRight.left;\n currentRight.left = current;\n current = currentRight;\n currentRight = current.right;\n if (currentRight == null) break;\n }\n if (left == null) {\n newTreeLeft = current;\n } else {\n left.right = current;\n }\n left = current;\n current = currentRight;\n } else {\n break;\n }\n }\n if (left != null) {\n left.right = current.left;\n current.left = newTreeLeft;\n }\n if (right != null) {\n right.left = current.right;\n current.right = newTreeRight;\n }\n if (this.root !== current) {\n this.root = current;\n this.splayCount++;\n }\n return comp;\n }\n\n protected splayMin(node: Node) {\n let current = node;\n let nextLeft = current.left;\n while (nextLeft != null) {\n const left = nextLeft;\n current.left = left.right;\n left.right = current;\n current = left;\n nextLeft = current.left;\n }\n return current;\n }\n\n protected splayMax(node: Node) {\n let current = node;\n let nextRight = current.right;\n while (nextRight != null) {\n const right = nextRight;\n current.right = right.left;\n right.left = current;\n current = right;\n nextRight = current.right;\n }\n return current;\n }\n\n protected _delete(key: K) {\n if (this.root == null) return null;\n const comp = this.splay(key);\n if (comp != 0) return null;\n let root = this.root;\n const result = root;\n const left = root.left;\n this.size--;\n if (left == null) {\n this.root = root.right;\n } else {\n const right = root.right;\n root = this.splayMax(left);\n\n root.right = right;\n this.root = root;\n }\n this.modificationCount++;\n return result;\n }\n\n protected addNewRoot(node: Node, comp: number) {\n this.size++;\n this.modificationCount++;\n const root = this.root;\n if (root == null) {\n this.root = node;\n return;\n }\n if (comp < 0) {\n node.left = root;\n node.right = root.right;\n root.right = null;\n } else {\n node.right = root;\n node.left = root.left;\n root.left = null;\n }\n this.root = node;\n }\n\n protected _first() {\n const root = this.root;\n if (root == null) return null;\n this.root = this.splayMin(root);\n return this.root;\n }\n\n protected _last() {\n const root = this.root;\n if (root == null) return null;\n this.root = this.splayMax(root);\n return this.root;\n }\n\n public clear() {\n this.root = null;\n this.size = 0;\n this.modificationCount++;\n }\n\n public has(key: unknown) {\n return this.validKey(key) && this.splay(key as K) == 0;\n }\n\n protected defaultCompare(): Comparator {\n return (a: K, b: K) => a < b ? -1 : a > b ? 1 : 0;\n }\n\n protected wrap(): SplayTreeWrapper {\n return {\n getRoot: () => { return this.root },\n setRoot: (root) => { this.root = root },\n getSize: () => { return this.size },\n getModificationCount: () => { return this.modificationCount },\n getSplayCount: () => { return this.splayCount },\n setSplayCount: (count) => { this.splayCount = count },\n splay: (key) => { return this.splay(key) },\n has: (key) => { return this.has(key) },\n };\n }\n}\n\nexport class SplayTreeMap extends SplayTree> implements Iterable<[K, V]>, Map {\n protected root: SplayTreeMapNode | null = null;\n\n protected compare: Comparator;\n protected validKey: Predicate;\n\n constructor(compare?: Comparator, isValidKey?: Predicate) {\n super();\n this.compare = compare ?? this.defaultCompare();\n this.validKey = isValidKey ?? ((a: unknown) => a != null && a != undefined);\n }\n\n delete(key: unknown) {\n if (!this.validKey(key)) return false;\n return this._delete(key as K) != null;\n }\n\n forEach(f: (value: V, key: K, map: Map) => void) {\n const nodes: Iterator<[K, V]> = new SplayTreeMapEntryIterableIterator(this.wrap());\n let result: IteratorResult<[K, V]>;\n while (result = nodes.next(), !result.done) {\n f(result.value[1], result.value[0], this);\n }\n }\n\n get(key: unknown): V | undefined {\n if (!this.validKey(key)) return undefined;\n if (this.root != null) {\n const comp = this.splay(key as K);\n if (comp == 0) {\n return this.root!.value;\n }\n }\n return undefined;\n }\n\n hasValue(value: unknown) {\n const initialSplayCount = this.splayCount;\n const visit = (node: SplayTreeMapNode | null) => {\n while (node != null) {\n if (node.value == value) return true;\n if (initialSplayCount != this.splayCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (node.right != null && visit(node.right)) {\n return true;\n }\n node = node.left;\n }\n return false;\n }\n\n return visit(this.root);\n }\n\n set(key: K, value: V) {\n const comp = this.splay(key);\n if (comp == 0) {\n this.root = this.root!.replaceValue(value);\n this.splayCount += 1;\n return this;\n }\n this.addNewRoot(new SplayTreeMapNode(key, value), comp);\n return this;\n }\n\n setAll(other: Map) {\n other.forEach((value: V, key: K) => {\n this.set(key, value);\n });\n }\n\n setIfAbsent(key: K, ifAbsent: () => V) {\n let comp = this.splay(key);\n if (comp == 0) {\n return this.root!.value;\n }\n const modificationCount = this.modificationCount;\n const splayCount = this.splayCount;\n const value = ifAbsent();\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (splayCount != this.splayCount) {\n comp = this.splay(key);\n }\n this.addNewRoot(new SplayTreeMapNode(key, value), comp);\n return value;\n }\n\n isEmpty() {\n return this.root == null;\n }\n\n isNotEmpty() {\n return !this.isEmpty();\n }\n\n firstKey() {\n if (this.root == null) return null;\n return this._first()!.key;\n }\n\n lastKey() {\n if (this.root == null) return null;\n return this._last()!.key;\n }\n\n lastKeyBefore(key: K) {\n if (key == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(key);\n if (comp < 0) return this.root!.key;\n let node: SplayTreeMapNode | null = this.root!.left;\n if (node == null) return null;\n let nodeRight = node.right;\n while (nodeRight != null) {\n node = nodeRight;\n nodeRight = node.right;\n }\n return node!.key;\n }\n\n firstKeyAfter(key: K) {\n if (key == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(key);\n if (comp > 0) return this.root!.key;\n let node: SplayTreeMapNode | null = this.root!.right;\n if (node == null) return null;\n let nodeLeft = node.left;\n while (nodeLeft != null) {\n node = nodeLeft;\n nodeLeft = node.left;\n }\n return node!.key;\n }\n\n update(key: K, update: (value: V) => V, ifAbsent?: () => V) {\n let comp = this.splay(key);\n if (comp == 0) {\n const modificationCount = this.modificationCount;\n const splayCount = this.splayCount;\n const newValue = update(this.root!.value);\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (splayCount != this.splayCount) {\n this.splay(key);\n }\n this.root = this.root!.replaceValue(newValue);\n this.splayCount += 1;\n return newValue;\n }\n if (ifAbsent != null) {\n const modificationCount = this.modificationCount;\n const splayCount = this.splayCount;\n const newValue = ifAbsent();\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (splayCount != this.splayCount) {\n comp = this.splay(key);\n }\n this.addNewRoot(new SplayTreeMapNode(key, newValue), comp);\n return newValue;\n }\n throw \"Invalid argument (key): Key not in map.\"\n }\n\n updateAll(update: (key: K, value: V) => V) {\n const root = this.root;\n if (root == null) return;\n const iterator = new SplayTreeMapEntryIterableIterator(this.wrap());\n let node: IteratorResult<[K, V]>;\n while (node = iterator.next(), !node.done) {\n const newValue = update(...node.value);\n iterator.replaceValue(newValue);\n }\n }\n\n keys(): IterableIterator {\n return new SplayTreeKeyIterableIterator>(this.wrap());\n }\n\n values(): IterableIterator {\n return new SplayTreeValueIterableIterator(this.wrap());\n }\n\n entries(): IterableIterator<[K, V]> {\n return this[Symbol.iterator]();\n }\n\n [Symbol.iterator](): IterableIterator<[K, V]> {\n return new SplayTreeMapEntryIterableIterator(this.wrap());\n }\n\n [Symbol.toStringTag] = '[object Map]'\n}\n\nexport class SplayTreeSet extends SplayTree> implements Iterable, Set {\n protected root: SplayTreeSetNode | null = null;\n\n protected compare: Comparator;\n protected validKey: Predicate;\n\n constructor(compare?: Comparator, isValidKey?: Predicate) {\n super();\n this.compare = compare ?? this.defaultCompare();\n this.validKey = isValidKey ?? ((v: unknown) => v != null && v != undefined );\n }\n\n delete(element: unknown) {\n if (!this.validKey(element)) return false;\n return this._delete(element as E) != null;\n }\n\n deleteAll(elements: Iterable) {\n for (const element of elements) {\n this.delete(element);\n }\n }\n\n forEach(f: (element: E, element2: E, set: Set) => void) {\n const nodes: Iterator = this[Symbol.iterator]();\n let result: IteratorResult;\n while (result = nodes.next(), !result.done) {\n f(result.value, result.value, this);\n }\n }\n\n add(element: E) {\n const compare = this.splay(element);\n if (compare != 0) this.addNewRoot(new SplayTreeSetNode(element), compare);\n return this;\n }\n\n addAndReturn(element: E) {\n const compare = this.splay(element);\n if (compare != 0) this.addNewRoot(new SplayTreeSetNode(element), compare);\n return this.root!.key;\n }\n\n addAll(elements: Iterable) {\n for (const element of elements) {\n this.add(element);\n }\n }\n\n isEmpty() {\n return this.root == null;\n }\n\n isNotEmpty() {\n return this.root != null;\n }\n\n single() {\n if (this.size == 0) throw \"Bad state: No element\";\n if (this.size > 1) throw \"Bad state: Too many element\";\n return this.root!.key;\n }\n\n first() {\n if (this.size == 0) throw \"Bad state: No element\";\n return this._first()!.key;\n }\n\n last() {\n if (this.size == 0) throw \"Bad state: No element\";\n return this._last()!.key;\n }\n\n lastBefore(element: E) {\n if (element == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(element);\n if (comp < 0) return this.root!.key;\n let node: SplayTreeSetNode | null = this.root!.left;\n if (node == null) return null;\n let nodeRight = node.right;\n while (nodeRight != null) {\n node = nodeRight;\n nodeRight = node.right;\n }\n return node!.key;\n }\n\n firstAfter(element: E) {\n if (element == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(element);\n if (comp > 0) return this.root!.key;\n let node: SplayTreeSetNode | null = this.root!.right;\n if (node == null) return null;\n let nodeLeft = node.left;\n while (nodeLeft != null) {\n node = nodeLeft;\n nodeLeft = node.left;\n }\n return node!.key;\n }\n\n retainAll(elements: Iterable) {\n const retainSet = new SplayTreeSet(this.compare, this.validKey);\n const modificationCount = this.modificationCount;\n for (const object of elements) {\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (this.validKey(object) && this.splay(object as E) == 0) {\n retainSet.add(this.root!.key);\n }\n }\n if (retainSet.size != this.size) {\n this.root = retainSet.root;\n this.size = retainSet.size;\n this.modificationCount++;\n }\n }\n\n lookup(object: unknown): E | null {\n if (!this.validKey(object)) return null;\n const comp = this.splay(object as E);\n if (comp != 0) return null;\n return this.root!.key;\n }\n\n intersection(other: Set): Set {\n const result = new SplayTreeSet(this.compare, this.validKey);\n for (const element of this) {\n if (other.has(element)) result.add(element);\n }\n return result;\n }\n\n difference(other: Set): Set {\n const result = new SplayTreeSet(this.compare, this.validKey);\n for (const element of this) {\n if (!other.has(element)) result.add(element);\n }\n return result;\n }\n\n union(other: Set): Set {\n const u = this.clone();\n u.addAll(other);\n return u;\n }\n\n protected clone() {\n const set = new SplayTreeSet(this.compare, this.validKey);\n set.size = this.size;\n set.root = this.copyNode>(this.root);\n return set;\n }\n\n protected copyNode>(node: Node | null) {\n if (node == null) return null;\n function copyChildren(node: Node, dest: SplayTreeSetNode) {\n let left: Node | null;\n let right: Node | null;\n do {\n left = node.left;\n right = node.right;\n if (left != null) {\n const newLeft = new SplayTreeSetNode(left.key);\n dest.left = newLeft;\n copyChildren(left, newLeft);\n }\n if (right != null) {\n const newRight = new SplayTreeSetNode(right.key);\n dest.right = newRight;\n node = right;\n dest = newRight;\n }\n } while (right != null);\n }\n\n const result = new SplayTreeSetNode(node.key);\n copyChildren(node, result);\n return result;\n }\n\n toSet(): Set {\n return this.clone();\n }\n\n entries(): IterableIterator<[E, E]> {\n return new SplayTreeSetEntryIterableIterator>(this.wrap());\n }\n\n keys(): IterableIterator {\n return this[Symbol.iterator]();\n }\n \n values(): IterableIterator {\n return this[Symbol.iterator]();\n }\n\n [Symbol.iterator](): IterableIterator {\n return new SplayTreeKeyIterableIterator>(this.wrap());\n }\n\n [Symbol.toStringTag] = '[object Set]'\n}\n\ninterface SplayTreeWrapper> {\n getRoot: () => Node | null;\n setRoot: (root: Node | null) => void;\n getSize: () => number;\n getModificationCount: () => number;\n getSplayCount: () => number;\n setSplayCount: (count: number) => void;\n splay: (key: K) => number;\n has: (key: unknown) => boolean;\n}\n\ntype SplayTreeMapWrapper = SplayTreeWrapper>;\n\nabstract class SplayTreeIterableIterator, T> implements IterableIterator {\n protected readonly tree: SplayTreeWrapper;\n\n protected readonly path = new Array();\n\n protected modificationCount: number | null = null;\n\n protected splayCount: number;\n\n constructor(tree: SplayTreeWrapper) {\n this.tree = tree;\n this.splayCount = tree.getSplayCount();\n }\n\n [Symbol.iterator](): IterableIterator {\n return this;\n }\n\n next(): IteratorResult {\n if (this.moveNext()) return { done: false, value: this.current()! }\n return { done: true, value: null }\n }\n\n protected current() {\n if (!this.path.length) return null;\n const node = this.path[this.path.length - 1];\n return this.getValue(node);\n }\n\n protected rebuildPath(key: K) {\n this.path.splice(0, this.path.length)\n this.tree.splay(key);\n this.path.push(this.tree.getRoot()!);\n this.splayCount = this.tree.getSplayCount();\n }\n\n protected findLeftMostDescendent(node: Node | null) {\n while (node != null) {\n this.path.push(node);\n node = node.left;\n }\n }\n\n protected moveNext() {\n if (this.modificationCount != this.tree.getModificationCount()) {\n if (this.modificationCount == null) {\n this.modificationCount = this.tree.getModificationCount();\n let node = this.tree.getRoot();\n while (node != null) {\n this.path.push(node);\n node = node.left;\n }\n return this.path.length > 0;\n }\n throw \"Concurrent modification during iteration.\";\n }\n if (!this.path.length) return false;\n if (this.splayCount != this.tree.getSplayCount()) {\n this.rebuildPath(this.path[this.path.length - 1].key);\n }\n let node = this.path[this.path.length - 1];\n let next = node.right;\n if (next != null) {\n while (next != null) {\n this.path.push(next);\n next = next.left;\n }\n return true;\n }\n this.path.pop();\n while (this.path.length && this.path[this.path.length - 1].right === node) {\n node = this.path.pop()!;\n }\n return this.path.length > 0;\n }\n\n protected abstract getValue(node: Node): T\n}\n\nclass SplayTreeKeyIterableIterator> extends SplayTreeIterableIterator {\n\n protected getValue(node: Node) {\n return node.key;\n }\n}\n\nclass SplayTreeSetEntryIterableIterator> extends SplayTreeIterableIterator {\n\n protected getValue(node: Node): [K, K] {\n return [node.key, node.key];\n }\n}\n\nclass SplayTreeValueIterableIterator extends SplayTreeIterableIterator, V> {\n\n constructor(map: SplayTreeMapWrapper) {\n super(map);\n }\n\n protected getValue(node: SplayTreeMapNode) {\n return node.value;\n }\n}\n\nclass SplayTreeMapEntryIterableIterator extends SplayTreeIterableIterator, [K, V]> {\n\n constructor(map: SplayTreeMapWrapper) {\n super(map);\n }\n\n protected getValue(node: SplayTreeMapNode): [K, V] {\n return [node.key, node.value];\n }\n\n replaceValue(value: V) {\n if (this.modificationCount != this.tree.getModificationCount()) {\n throw \"Concurrent modification during iteration.\";\n }\n if (this.splayCount != this.tree.getSplayCount()) {\n this.rebuildPath(this.path[this.path.length - 1].key);\n }\n const last = this.path.pop()!;\n const newLast = last.replaceValue(value);\n if (!this.path.length) {\n this.tree.setRoot(newLast);\n } else {\n const parent = this.path[this.path.length - 1];\n if (last === parent.left) {\n parent.left = newLast;\n } else {\n parent.right = newLast;\n }\n }\n this.path.push(newLast);\n const count = this.tree.getSplayCount() + 1;\n this.tree.setSplayCount(count);\n this.splayCount = count;\n }\n}", "import BigNumber from \"bignumber.js\";\nimport { Bbox } from \"./bbox.js\";\nimport { precision } from \"./precision.js\";\nimport Segment from \"./segment.js\";\nimport { Point } from \"./sweep-event.js\";\n\nexport type Ring = [number, number][]\nexport type Poly = Ring[]\nexport type MultiPoly = Poly[]\nexport type Geom = Poly | MultiPoly\n\nexport class RingIn {\n poly: PolyIn\n isExterior: boolean\n segments: Segment[]\n bbox: Bbox\n\n constructor(geomRing: Ring, poly: PolyIn, isExterior: boolean) {\n if (!Array.isArray(geomRing) || geomRing.length === 0) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n\n this.poly = poly\n this.isExterior = isExterior\n this.segments = []\n\n if (\n typeof geomRing[0][0] !== \"number\" ||\n typeof geomRing[0][1] !== \"number\"\n ) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n\n const firstPoint = precision.snap({ x: new BigNumber(geomRing[0][0]), y: new BigNumber(geomRing[0][1]) }) as Point\n this.bbox = {\n ll: { x: firstPoint.x, y: firstPoint.y },\n ur: { x: firstPoint.x, y: firstPoint.y },\n }\n\n let prevPoint = firstPoint\n for (let i = 1, iMax = geomRing.length; i < iMax; i++) {\n if (\n typeof geomRing[i][0] !== \"number\" ||\n typeof geomRing[i][1] !== \"number\"\n ) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n const point = precision.snap({ x: new BigNumber(geomRing[i][0]), y: new BigNumber(geomRing[i][1]) }) as Point\n // skip repeated points\n if (point.x.eq(prevPoint.x) && point.y.eq(prevPoint.y)) continue\n this.segments.push(Segment.fromRing(prevPoint, point, this))\n if (point.x.isLessThan(this.bbox.ll.x)) this.bbox.ll.x = point.x\n if (point.y.isLessThan(this.bbox.ll.y)) this.bbox.ll.y = point.y\n if (point.x.isGreaterThan(this.bbox.ur.x)) this.bbox.ur.x = point.x\n if (point.y.isGreaterThan(this.bbox.ur.y)) this.bbox.ur.y = point.y\n prevPoint = point\n }\n // add segment from last to first if last is not the same as first\n if (!firstPoint.x.eq(prevPoint.x) || !firstPoint.y.eq(prevPoint.y)) {\n this.segments.push(Segment.fromRing(prevPoint, firstPoint, this))\n }\n }\n\n getSweepEvents() {\n const sweepEvents = []\n for (let i = 0, iMax = this.segments.length; i < iMax; i++) {\n const segment = this.segments[i]\n sweepEvents.push(segment.leftSE)\n sweepEvents.push(segment.rightSE)\n }\n return sweepEvents\n }\n}\n\nexport class PolyIn {\n multiPoly: MultiPolyIn\n exteriorRing: RingIn\n interiorRings: RingIn[]\n bbox: Bbox\n\n constructor(geomPoly: Poly, multiPoly: MultiPolyIn) {\n if (!Array.isArray(geomPoly)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n this.exteriorRing = new RingIn(geomPoly[0], this, true)\n // copy by value\n this.bbox = {\n ll: { x: this.exteriorRing.bbox.ll.x, y: this.exteriorRing.bbox.ll.y },\n ur: { x: this.exteriorRing.bbox.ur.x, y: this.exteriorRing.bbox.ur.y },\n }\n this.interiorRings = []\n for (let i = 1, iMax = geomPoly.length; i < iMax; i++) {\n const ring = new RingIn(geomPoly[i], this, false)\n if (ring.bbox.ll.x.isLessThan(this.bbox.ll.x)) this.bbox.ll.x = ring.bbox.ll.x\n if (ring.bbox.ll.y.isLessThan(this.bbox.ll.y)) this.bbox.ll.y = ring.bbox.ll.y\n if (ring.bbox.ur.x.isGreaterThan(this.bbox.ur.x)) this.bbox.ur.x = ring.bbox.ur.x\n if (ring.bbox.ur.y.isGreaterThan(this.bbox.ur.y)) this.bbox.ur.y = ring.bbox.ur.y\n this.interiorRings.push(ring)\n }\n this.multiPoly = multiPoly\n }\n\n getSweepEvents() {\n const sweepEvents = this.exteriorRing.getSweepEvents()\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringSweepEvents = this.interiorRings[i].getSweepEvents()\n for (let j = 0, jMax = ringSweepEvents.length; j < jMax; j++) {\n sweepEvents.push(ringSweepEvents[j])\n }\n }\n return sweepEvents\n }\n}\n\nexport class MultiPolyIn {\n isSubject: boolean\n polys: PolyIn[]\n bbox: Bbox\n\n constructor(geom: Geom, isSubject: boolean) {\n if (!Array.isArray(geom)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n\n try {\n // if the input looks like a polygon, convert it to a multipolygon\n if (typeof geom[0][0][0] === \"number\") geom = [geom as Poly]\n } catch (ex) {\n // The input is either malformed or has empty arrays.\n // In either case, it will be handled later on.\n }\n\n this.polys = []\n this.bbox = {\n ll: { x: new BigNumber(Number.POSITIVE_INFINITY), y: new BigNumber(Number.POSITIVE_INFINITY) },\n ur: { x: new BigNumber(Number.NEGATIVE_INFINITY), y: new BigNumber(Number.NEGATIVE_INFINITY) },\n }\n for (let i = 0, iMax = geom.length; i < iMax; i++) {\n const poly = new PolyIn(geom[i] as Poly, this)\n if (poly.bbox.ll.x.isLessThan(this.bbox.ll.x)) this.bbox.ll.x = poly.bbox.ll.x\n if (poly.bbox.ll.y.isLessThan(this.bbox.ll.y)) this.bbox.ll.y = poly.bbox.ll.y\n if (poly.bbox.ur.x.isGreaterThan(this.bbox.ur.x)) this.bbox.ur.x = poly.bbox.ur.x\n if (poly.bbox.ur.y.isGreaterThan(this.bbox.ur.y)) this.bbox.ur.y = poly.bbox.ur.y\n this.polys.push(poly)\n }\n this.isSubject = isSubject\n }\n\n getSweepEvents() {\n const sweepEvents = []\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polySweepEvents = this.polys[i].getSweepEvents()\n for (let j = 0, jMax = polySweepEvents.length; j < jMax; j++) {\n sweepEvents.push(polySweepEvents[j])\n }\n }\n return sweepEvents\n }\n}\n", "export default (x: T) => {\n return () => {\n return x\n }\n}", "import BigNumber from \"bignumber.js\"\nimport constant from \"./constant.js\"\n\nexport default (eps?: number) => {\n const almostEqual = eps ? (a: BigNumber, b: BigNumber) =>\n b.minus(a).abs().isLessThanOrEqualTo(eps)\n : constant(false)\n\n return (a: BigNumber, b: BigNumber) => {\n if (almostEqual(a, b)) return 0\n\n return a.comparedTo(b)\n }\n}", "import BigNumber from \"bignumber.js\";\nimport constant from \"./constant.js\";\nimport { Vector } from \"./vector.js\";\n\nexport default function (eps?: number) {\n const almostCollinear = eps ? (area2: BigNumber, ax: BigNumber, ay: BigNumber, cx: BigNumber, cy: BigNumber) =>\n area2.exponentiatedBy(2).isLessThanOrEqualTo(\n cx.minus(ax).exponentiatedBy(2).plus(cy.minus(ay).exponentiatedBy(2))\n .times(eps))\n : constant(false)\n\n return (a: Vector, b: Vector, c: Vector) => {\n const ax = a.x, ay = a.y, cx = c.x, cy = c.y\n\n const area2 = ay.minus(cy).times(b.x.minus(cx)).minus(ax.minus(cx).times(b.y.minus(cy)))\n\n if (almostCollinear(area2, ax, ay, cx, cy)) return 0\n\n return area2.comparedTo(0)\n }\n}", "import BigNumber from \"bignumber.js\";\nimport { SplayTreeSet } from \"splaytree-ts\"\nimport compare from \"./compare.js\";\nimport identity from \"./identity.js\";\nimport { Vector } from \"./vector.js\";\n\nexport default (eps?: number) => {\n if (eps) {\n\n const xTree = new SplayTreeSet(compare(eps))\n const yTree = new SplayTreeSet(compare(eps))\n\n const snapCoord = (coord: BigNumber, tree: SplayTreeSet) => {\n return tree.addAndReturn(coord)\n }\n\n const snap = (v: Vector) => {\n return {\n x: snapCoord(v.x, xTree),\n y: snapCoord(v.y, yTree),\n } as Vector\n }\n\n snap({ x: new BigNumber(0), y: new BigNumber(0)})\n\n return snap\n }\n\n return identity\n}", "export default (x: T) => {\n return x;\n}", "import compare from \"./compare.js\";\nimport orient from \"./orient.js\";\nimport snap from \"./snap.js\";\n\nconst set = (eps?: number) => {\n return {\n set: (eps?: number) => { precision = set(eps) },\n reset: () => set(eps),\n compare: compare(eps),\n snap: snap(eps),\n orient: orient(eps)\n }\n}\n\nexport let precision: ReturnType = set()", "import { Vector } from \"./vector.js\";\n\nexport interface Bbox {\n ll: Vector;\n ur: Vector;\n}\n\n/**\n * A bounding box has the format:\n *\n * { ll: { x: xmin, y: ymin }, ur: { x: xmax, y: ymax } }\n *\n */\n\nexport const isInBbox = (bbox: Bbox, point: Vector) => {\n return (\n bbox.ll.x.isLessThanOrEqualTo(point.x) &&\n point.x.isLessThanOrEqualTo(bbox.ur.x) &&\n bbox.ll.y.isLessThanOrEqualTo(point.y) &&\n point.y.isLessThanOrEqualTo(bbox.ur.y) \n )\n}\n\n/* Returns either null, or a bbox (aka an ordered pair of points)\n * If there is only one point of overlap, a bbox with identical points\n * will be returned */\nexport const getBboxOverlap = (b1: Bbox, b2: Bbox) => {\n // check if the bboxes overlap at all\n if (\n b2.ur.x.isLessThan(b1.ll.x) ||\n b1.ur.x.isLessThan(b2.ll.x) ||\n b2.ur.y.isLessThan(b1.ll.y) ||\n b1.ur.y.isLessThan(b2.ll.y) \n )\n return null\n\n // find the middle two X values\n const lowerX = b1.ll.x.isLessThan(b2.ll.x) ? b2.ll.x : b1.ll.x\n const upperX = b1.ur.x.isLessThan(b2.ur.x) ? b1.ur.x : b2.ur.x\n\n // find the middle two Y values\n const lowerY = b1.ll.y.isLessThan(b2.ll.y) ? b2.ll.y : b1.ll.y\n const upperY = b1.ur.y.isLessThan(b2.ur.y) ? b1.ur.y : b2.ur.y\n\n // put those middle values together to get the overlap\n return { ll: { x: lowerX, y: lowerY }, ur: { x: upperX, y: upperY } } as Bbox\n}\n", "import { SplayTreeSet } from \"splaytree-ts\"\nimport { getBboxOverlap } from \"./bbox.js\"\nimport * as geomIn from \"./geom-in.js\"\nimport { Geom } from \"./geom-in.js\"\nimport * as geomOut from \"./geom-out.js\"\nimport { precision } from \"./precision.js\"\nimport SweepEvent from \"./sweep-event.js\"\nimport SweepLine from \"./sweep-line.js\"\n\nexport class Operation {\n type!: string\n numMultiPolys!: number\n\n run(type: string, geom: Geom, moreGeoms: Geom[]) {\n operation.type = type\n\n /* Convert inputs to MultiPoly objects */\n const multipolys = [new geomIn.MultiPolyIn(geom, true)]\n for (let i = 0, iMax = moreGeoms.length; i < iMax; i++) {\n multipolys.push(new geomIn.MultiPolyIn(moreGeoms[i], false))\n }\n operation.numMultiPolys = multipolys.length\n\n /* BBox optimization for difference operation\n * If the bbox of a multipolygon that's part of the clipping doesn't\n * intersect the bbox of the subject at all, we can just drop that\n * multiploygon. */\n if (operation.type === \"difference\") {\n // in place removal\n const subject = multipolys[0]\n let i = 1\n while (i < multipolys.length) {\n if (getBboxOverlap(multipolys[i].bbox, subject.bbox) !== null) i++\n else multipolys.splice(i, 1)\n }\n }\n\n /* BBox optimization for intersection operation\n * If we can find any pair of multipolygons whose bbox does not overlap,\n * then the result will be empty. */\n if (operation.type === \"intersection\") {\n // TODO: this is O(n^2) in number of polygons. By sorting the bboxes,\n // it could be optimized to O(n * ln(n))\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const mpA = multipolys[i]\n for (let j = i + 1, jMax = multipolys.length; j < jMax; j++) {\n if (getBboxOverlap(mpA.bbox, multipolys[j].bbox) === null) return []\n }\n }\n }\n\n /* Put segment endpoints in a priority queue */\n const queue = new SplayTreeSet(SweepEvent.compare)\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const sweepEvents = multipolys[i].getSweepEvents()\n for (let j = 0, jMax = sweepEvents.length; j < jMax; j++) {\n queue.add(sweepEvents[j])\n }\n }\n\n /* Pass the sweep line over those endpoints */\n const sweepLine = new SweepLine(queue)\n let evt = null\n if (queue.size != 0) {\n evt = queue.first()\n queue.delete(evt)\n }\n while (evt) {\n const newEvents = sweepLine.process(evt)\n for (let i = 0, iMax = newEvents.length; i < iMax; i++) {\n const evt = newEvents[i]\n if (evt.consumedBy === undefined) queue.add(evt)\n }\n if (queue.size != 0) {\n evt = queue.first()\n queue.delete(evt)\n } else {\n evt = null;\n }\n }\n\n // free some memory we don't need anymore\n precision.reset()\n\n /* Collect and compile segments we're keeping into a multipolygon */\n const ringsOut = geomOut.RingOut.factory(sweepLine.segments)\n const result = new geomOut.MultiPolyOut(ringsOut)\n return result.getGeom()\n }\n}\n\n// singleton available by import\nconst operation = new Operation()\n\nexport default operation\n", "import * as bn from \"bignumber.js\";\n\nexport interface Vector {\n x: bn.BigNumber;\n y: bn.BigNumber;\n}\n\n/* Cross Product of two vectors with first point at origin */\nexport const crossProduct = (a: Vector, b: Vector) => a.x.times(b.y).minus(a.y.times(b.x))\n\n/* Dot Product of two vectors with first point at origin */\nexport const dotProduct = (a: Vector, b: Vector) => a.x.times(b.x).plus(a.y.times(b.y))\n\nexport const length = (v: Vector) => dotProduct(v, v).sqrt()\n\n/* Get the sine of the angle from pShared -> pAngle to pShaed -> pBase */\nexport const sineOfAngle = (pShared: Vector, pBase: Vector, pAngle: Vector) => {\n const vBase = { x: pBase.x.minus(pShared.x), y: pBase.y.minus(pShared.y) }\n const vAngle = { x: pAngle.x.minus(pShared.x), y: pAngle.y.minus(pShared.y) }\n return crossProduct(vAngle, vBase).div(length(vAngle)).div(length(vBase))\n}\n\n/* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */\nexport const cosineOfAngle = (pShared: Vector, pBase: Vector, pAngle: Vector) => {\n const vBase = { x: pBase.x.minus(pShared.x), y: pBase.y.minus(pShared.y) }\n const vAngle = { x: pAngle.x.minus(pShared.x), y: pAngle.y.minus(pShared.y) }\n return dotProduct(vAngle, vBase).div(length(vAngle)).div(length(vBase))\n}\n\n/* Get the x coordinate where the given line (defined by a point and vector)\n * crosses the horizontal line with the given y coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\nexport const horizontalIntersection = (pt: Vector, v: Vector, y: bn.BigNumber) => {\n if (v.y.isZero()) return null\n return { x: pt.x.plus((v.x.div(v.y)).times(y.minus(pt.y))), y: y }\n}\n\n/* Get the y coordinate where the given line (defined by a point and vector)\n * crosses the vertical line with the given x coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\nexport const verticalIntersection = (pt: Vector, v: Vector, x: bn.BigNumber) => {\n if (v.x.isZero()) return null\n return { x: x, y: pt.y.plus((v.y.div(v.x)).times(x.minus(pt.x))) }\n}\n\n/* Get the intersection of two lines, each defined by a base point and a vector.\n * In the case of parrallel lines (including overlapping ones) returns null. */\nexport const intersection = (pt1: Vector, v1: Vector, pt2: Vector, v2: Vector) => {\n // take some shortcuts for vertical and horizontal lines\n // this also ensures we don't calculate an intersection and then discover\n // it's actually outside the bounding box of the line\n if (v1.x.isZero()) return verticalIntersection(pt2, v2, pt1.x)\n if (v2.x.isZero()) return verticalIntersection(pt1, v1, pt2.x)\n if (v1.y.isZero()) return horizontalIntersection(pt2, v2, pt1.y)\n if (v2.y.isZero()) return horizontalIntersection(pt1, v1, pt2.y)\n\n // General case for non-overlapping segments.\n // This algorithm is based on Schneider and Eberly.\n // http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf - pg 244\n\n const kross = crossProduct(v1, v2)\n if (kross.isZero()) return null\n\n const ve = { x: pt2.x.minus(pt1.x), y: pt2.y.minus(pt1.y) }\n const d1 = crossProduct(ve, v1).div(kross)\n const d2 = crossProduct(ve, v2).div(kross)\n\n // take the average of the two calculations to minimize rounding error\n const x1 = pt1.x.plus(d2.times(v1.x)),\n x2 = pt2.x.plus(d1.times(v2.x))\n const y1 = pt1.y.plus(d2.times(v1.y)),\n y2 = pt2.y.plus(d1.times(v2.y))\n const x = x1.plus(x2).div(2)\n const y = y1.plus(y2).div(2)\n return { x: x, y: y } as Vector\n}\n\n/* Given a vector, return one that is perpendicular */\nexport const perpendicular = (v: Vector) => {\n return { x: v.y.negated(), y: v.x }\n}", "import BigNumber from \"bignumber.js\";\nimport Segment from \"./segment.js\"\nimport { cosineOfAngle, sineOfAngle, Vector } from \"./vector.js\"\n\nexport interface Point extends Vector {\n events: SweepEvent[];\n}\n\nexport default class SweepEvent {\n point: Point;\n isLeft: boolean;\n segment!: Segment;\n otherSE!: SweepEvent;\n consumedBy: SweepEvent | undefined;\n\n // for ordering sweep events in the sweep event queue\n static compare(a: SweepEvent, b: SweepEvent) {\n // favor event with a point that the sweep line hits first\n const ptCmp = SweepEvent.comparePoints(a.point, b.point)\n if (ptCmp !== 0) return ptCmp\n\n // the points are the same, so link them if needed\n if (a.point !== b.point) a.link(b)\n\n // favor right events over left\n if (a.isLeft !== b.isLeft) return a.isLeft ? 1 : -1\n\n // we have two matching left or right endpoints\n // ordering of this case is the same as for their segments\n return Segment.compare(a.segment, b.segment)\n }\n\n // for ordering points in sweep line order\n static comparePoints(aPt: Point, bPt: Point) {\n if (aPt.x.isLessThan(bPt.x)) return -1\n if (aPt.x.isGreaterThan(bPt.x)) return 1\n\n if (aPt.y.isLessThan(bPt.y)) return -1\n if (aPt.y.isGreaterThan(bPt.y)) return 1\n\n return 0\n }\n\n // Warning: 'point' input will be modified and re-used (for performance)\n constructor(point: Point, isLeft: boolean) {\n if (point.events === undefined) point.events = [this]\n else point.events.push(this)\n this.point = point\n this.isLeft = isLeft\n // this.segment, this.otherSE set by factory\n }\n\n link(other: SweepEvent) {\n if (other.point === this.point) {\n throw new Error(\"Tried to link already linked events\")\n }\n const otherEvents = other.point.events\n for (let i = 0, iMax = otherEvents.length; i < iMax; i++) {\n const evt = otherEvents[i]\n this.point.events.push(evt)\n evt.point = this.point\n }\n this.checkForConsuming()\n }\n\n /* Do a pass over our linked events and check to see if any pair\n * of segments match, and should be consumed. */\n checkForConsuming() {\n // FIXME: The loops in this method run O(n^2) => no good.\n // Maintain little ordered sweep event trees?\n // Can we maintaining an ordering that avoids the need\n // for the re-sorting with getLeftmostComparator in geom-out?\n\n // Compare each pair of events to see if other events also match\n const numEvents = this.point.events.length\n for (let i = 0; i < numEvents; i++) {\n const evt1 = this.point.events[i]\n if (evt1.segment.consumedBy !== undefined) continue\n for (let j = i + 1; j < numEvents; j++) {\n const evt2 = this.point.events[j]\n if (evt2.consumedBy !== undefined) continue\n if (evt1.otherSE.point.events !== evt2.otherSE.point.events) continue\n evt1.segment.consume(evt2.segment)\n }\n }\n }\n\n getAvailableLinkedEvents() {\n // point.events is always of length 2 or greater\n const events = []\n for (let i = 0, iMax = this.point.events.length; i < iMax; i++) {\n const evt = this.point.events[i]\n if (evt !== this && !evt.segment.ringOut && evt.segment.isInResult()) {\n events.push(evt)\n }\n }\n return events\n }\n\n /**\n * Returns a comparator function for sorting linked events that will\n * favor the event that will give us the smallest left-side angle.\n * All ring construction starts as low as possible heading to the right,\n * so by always turning left as sharp as possible we'll get polygons\n * without uncessary loops & holes.\n *\n * The comparator function has a compute cache such that it avoids\n * re-computing already-computed values.\n */\n getLeftmostComparator(baseEvent: SweepEvent) {\n const cache = new Map()\n\n const fillCache = (linkedEvent: SweepEvent) => {\n const nextEvent = linkedEvent.otherSE\n cache.set(linkedEvent, {\n sine: sineOfAngle(this.point, baseEvent.point, nextEvent.point),\n cosine: cosineOfAngle(this.point, baseEvent.point, nextEvent.point),\n })\n }\n\n return (a: SweepEvent, b: SweepEvent) => {\n if (!cache.has(a)) fillCache(a)\n if (!cache.has(b)) fillCache(b)\n\n const { sine: asine, cosine: acosine } = cache.get(a)!\n const { sine: bsine, cosine: bcosine } = cache.get(b)!\n\n // both on or above x-axis\n if (asine.isGreaterThanOrEqualTo(0) && bsine.isGreaterThanOrEqualTo(0)) {\n if (acosine.isLessThan(bcosine)) return 1\n if (acosine.isGreaterThan(bcosine)) return -1\n return 0\n }\n\n // both below x-axis\n if (asine.isLessThan(0) && bsine.isLessThan(0)) {\n if (acosine.isLessThan(bcosine)) return -1\n if (acosine.isGreaterThan(bcosine)) return 1\n return 0\n }\n\n // one above x-axis, one below\n if (bsine.isLessThan(asine)) return -1\n if (bsine.isGreaterThan(asine)) return 1\n return 0\n }\n }\n}\n", "import { MultiPoly, Poly, Ring } from \"./geom-in.js\"\nimport { precision } from \"./precision.js\"\nimport Segment from \"./segment.js\"\nimport SweepEvent from \"./sweep-event.js\"\n\nexport class RingOut {\n events: SweepEvent[]\n poly: PolyOut | null\n _isExteriorRing: boolean | undefined\n _enclosingRing: RingOut | null | undefined\n \n /* Given the segments from the sweep line pass, compute & return a series\n * of closed rings from all the segments marked to be part of the result */\n static factory(allSegments: Segment[]) {\n const ringsOut = []\n\n for (let i = 0, iMax = allSegments.length; i < iMax; i++) {\n const segment = allSegments[i]\n if (!segment.isInResult() || segment.ringOut) continue\n\n let prevEvent = null\n let event = segment.leftSE\n let nextEvent = segment.rightSE\n const events = [event]\n\n const startingPoint = event.point\n const intersectionLEs = []\n\n /* Walk the chain of linked events to form a closed ring */\n while (true) {\n prevEvent = event\n event = nextEvent\n events.push(event)\n\n /* Is the ring complete? */\n if (event.point === startingPoint) break\n\n while (true) {\n const availableLEs = event.getAvailableLinkedEvents()\n\n /* Did we hit a dead end? This shouldn't happen. Indicates some earlier\n * part of the algorithm malfunctioned... please file a bug report. */\n if (availableLEs.length === 0) {\n const firstPt = events[0].point\n const lastPt = events[events.length - 1].point\n throw new Error(\n `Unable to complete output ring starting at [${firstPt.x},` +\n ` ${firstPt.y}]. Last matching segment found ends at` +\n ` [${lastPt.x}, ${lastPt.y}].`,\n )\n }\n\n /* Only one way to go, so cotinue on the path */\n if (availableLEs.length === 1) {\n nextEvent = availableLEs[0].otherSE\n break\n }\n\n /* We must have an intersection. Check for a completed loop */\n let indexLE = null\n for (let j = 0, jMax = intersectionLEs.length; j < jMax; j++) {\n if (intersectionLEs[j].point === event.point) {\n indexLE = j\n break\n }\n }\n /* Found a completed loop. Cut that off and make a ring */\n if (indexLE !== null) {\n const intersectionLE = intersectionLEs.splice(indexLE)[0]\n const ringEvents = events.splice(intersectionLE.index)\n ringEvents.unshift(ringEvents[0].otherSE)\n ringsOut.push(new RingOut(ringEvents.reverse()))\n continue\n }\n /* register the intersection */\n intersectionLEs.push({\n index: events.length,\n point: event.point,\n })\n /* Choose the left-most option to continue the walk */\n const comparator = event.getLeftmostComparator(prevEvent)\n nextEvent = availableLEs.sort(comparator)[0].otherSE\n break\n }\n }\n\n ringsOut.push(new RingOut(events))\n }\n return ringsOut\n }\n\n constructor(events: SweepEvent[]) {\n this.events = events\n for (let i = 0, iMax = events.length; i < iMax; i++) {\n events[i].segment.ringOut = this\n }\n this.poly = null\n }\n\n getGeom() {\n // Remove superfluous points (ie extra points along a straight line),\n let prevPt = this.events[0].point\n const points = [prevPt]\n for (let i = 1, iMax = this.events.length - 1; i < iMax; i++) {\n const pt = this.events[i].point\n const nextPt = this.events[i + 1].point\n if (precision.orient(pt, prevPt, nextPt) === 0) continue\n points.push(pt)\n prevPt = pt\n }\n\n // ring was all (within rounding error of angle calc) colinear points\n if (points.length === 1) return null\n\n // check if the starting point is necessary\n const pt = points[0]\n const nextPt = points[1]\n if (precision.orient(pt, prevPt, nextPt) === 0) points.shift()\n\n points.push(points[0])\n const step = this.isExteriorRing() ? 1 : -1\n const iStart = this.isExteriorRing() ? 0 : points.length - 1\n const iEnd = this.isExteriorRing() ? points.length : -1\n const orderedPoints: Ring = []\n for (let i = iStart; i != iEnd; i += step)\n orderedPoints.push([points[i].x.toNumber(), points[i].y.toNumber()])\n return orderedPoints\n }\n\n isExteriorRing(): boolean {\n if (this._isExteriorRing === undefined) {\n const enclosing = this.enclosingRing()\n this._isExteriorRing = enclosing ? !enclosing.isExteriorRing() : true\n }\n return this._isExteriorRing\n }\n\n enclosingRing() {\n if (this._enclosingRing === undefined) {\n this._enclosingRing = this._calcEnclosingRing()\n }\n return this._enclosingRing\n }\n\n /* Returns the ring that encloses this one, if any */\n _calcEnclosingRing(): RingOut | null | undefined {\n // start with the ealier sweep line event so that the prevSeg\n // chain doesn't lead us inside of a loop of ours\n let leftMostEvt = this.events[0]\n for (let i = 1, iMax = this.events.length; i < iMax; i++) {\n const evt = this.events[i]\n if (SweepEvent.compare(leftMostEvt, evt) > 0) leftMostEvt = evt\n }\n\n let prevSeg: Segment | null | undefined = leftMostEvt.segment.prevInResult()\n let prevPrevSeg: Segment | null | undefined = prevSeg ? prevSeg.prevInResult() : null\n\n while (true) {\n // no segment found, thus no ring can enclose us\n if (!prevSeg) return null\n\n // no segments below prev segment found, thus the ring of the prev\n // segment must loop back around and enclose us\n if (!prevPrevSeg) return prevSeg.ringOut\n\n // if the two segments are of different rings, the ring of the prev\n // segment must either loop around us or the ring of the prev prev\n // seg, which would make us and the ring of the prev peers\n if (prevPrevSeg.ringOut !== prevSeg.ringOut) {\n if (prevPrevSeg.ringOut?.enclosingRing() !== prevSeg.ringOut) {\n return prevSeg.ringOut\n } else return prevSeg.ringOut?.enclosingRing()\n }\n\n // two segments are from the same ring, so this was a penisula\n // of that ring. iterate downward, keep searching\n prevSeg = prevPrevSeg.prevInResult()\n prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null\n }\n }\n}\n\nexport class PolyOut {\n exteriorRing: RingOut;\n interiorRings: RingOut[];\n\n constructor(exteriorRing: RingOut) {\n this.exteriorRing = exteriorRing\n exteriorRing.poly = this\n this.interiorRings = []\n }\n\n addInterior(ring: RingOut) {\n this.interiorRings.push(ring)\n ring.poly = this\n }\n\n getGeom() {\n const geom0 = this.exteriorRing.getGeom()\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (geom0 === null) return null\n const geom: Poly = [geom0];\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringGeom = this.interiorRings[i].getGeom()\n // interior ring was all (within rounding error of angle calc) colinear points\n if (ringGeom === null) continue\n geom.push(ringGeom)\n }\n return geom\n }\n}\n\nexport class MultiPolyOut {\n rings: RingOut[];\n polys: PolyOut[];\n\n constructor(rings: RingOut[]) {\n this.rings = rings\n this.polys = this._composePolys(rings)\n }\n\n getGeom() {\n const geom: MultiPoly = []\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polyGeom = this.polys[i].getGeom()\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (polyGeom === null) continue\n geom.push(polyGeom)\n }\n return geom\n }\n\n _composePolys(rings: RingOut[]) {\n const polys = []\n for (let i = 0, iMax = rings.length; i < iMax; i++) {\n const ring = rings[i]\n if (ring.poly) continue\n if (ring.isExteriorRing()) polys.push(new PolyOut(ring))\n else {\n const enclosingRing = ring.enclosingRing()\n if (!enclosingRing?.poly) polys.push(new PolyOut(enclosingRing!))\n enclosingRing?.poly?.addInterior(ring)\n }\n }\n return polys\n }\n}\n", "import { SplayTreeSet } from \"splaytree-ts\"\nimport Segment from \"./segment.js\"\nimport SweepEvent, { Point } from \"./sweep-event.js\"\n\n/**\n * NOTE: We must be careful not to change any segments while\n * they are in the SplayTree. AFAIK, there's no way to tell\n * the tree to rebalance itself - thus before splitting\n * a segment that's in the tree, we remove it from the tree,\n * do the split, then re-insert it. (Even though splitting a\n * segment *shouldn't* change its correct position in the\n * sweep line tree, the reality is because of rounding errors,\n * it sometimes does.)\n */\n\nexport default class SweepLine {\n private queue: SplayTreeSet\n private tree: SplayTreeSet\n segments: Segment[]\n\n constructor(queue: SplayTreeSet, comparator = Segment.compare) {\n this.queue = queue\n this.tree = new SplayTreeSet(comparator)\n this.segments = []\n }\n\n process(event: SweepEvent) {\n const segment = event.segment\n const newEvents: SweepEvent[] = []\n\n // if we've already been consumed by another segment,\n // clean up our body parts and get out\n if (event.consumedBy) {\n if (event.isLeft) this.queue.delete(event.otherSE)\n else this.tree.delete(segment)\n return newEvents\n }\n\n if (event.isLeft) this.tree.add(segment);\n\n let prevSeg: Segment | null = segment\n let nextSeg: Segment | null = segment\n\n // skip consumed segments still in tree\n do {\n prevSeg = this.tree.lastBefore(prevSeg)\n } while (prevSeg != null && prevSeg.consumedBy != undefined)\n\n // skip consumed segments still in tree\n do {\n nextSeg = this.tree.firstAfter(nextSeg)\n } while (nextSeg != null && nextSeg.consumedBy != undefined)\n\n if (event.isLeft) {\n // Check for intersections against the previous segment in the sweep line\n let prevMySplitter = null\n if (prevSeg) {\n const prevInter = prevSeg.getIntersection(segment)\n if (prevInter !== null) {\n if (!segment.isAnEndpoint(prevInter)) prevMySplitter = prevInter\n if (!prevSeg.isAnEndpoint(prevInter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, prevInter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n }\n }\n\n // Check for intersections against the next segment in the sweep line\n let nextMySplitter = null\n if (nextSeg) {\n const nextInter = nextSeg.getIntersection(segment)\n if (nextInter !== null) {\n if (!segment.isAnEndpoint(nextInter)) nextMySplitter = nextInter\n if (!nextSeg.isAnEndpoint(nextInter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, nextInter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n }\n }\n\n // For simplicity, even if we find more than one intersection we only\n // spilt on the 'earliest' (sweep-line style) of the intersections.\n // The other intersection will be handled in a future process().\n if (prevMySplitter !== null || nextMySplitter !== null) {\n let mySplitter = null\n if (prevMySplitter === null) mySplitter = nextMySplitter\n else if (nextMySplitter === null) mySplitter = prevMySplitter\n else {\n const cmpSplitters = SweepEvent.comparePoints(\n prevMySplitter,\n nextMySplitter,\n )\n mySplitter = cmpSplitters <= 0 ? prevMySplitter : nextMySplitter\n }\n\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n this.queue.delete(segment.rightSE)\n newEvents.push(segment.rightSE)\n\n const newEventsFromSplit = segment.split(mySplitter!)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n\n if (newEvents.length > 0) {\n // We found some intersections, so re-do the current event to\n // make sure sweep line ordering is totally consistent for later\n // use with the segment 'prev' pointers\n this.tree.delete(segment)\n newEvents.push(event)\n } else {\n // done with left event\n this.segments.push(segment)\n segment.prev = prevSeg\n }\n } else {\n // event.isRight\n\n // since we're about to be removed from the sweep line, check for\n // intersections between our previous and next segments\n if (prevSeg && nextSeg) {\n const inter = prevSeg.getIntersection(nextSeg)\n if (inter !== null) {\n if (!prevSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, inter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n if (!nextSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, inter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n }\n }\n\n this.tree.delete(segment)\n }\n\n return newEvents\n }\n\n /* Safely split a segment that is currently in the datastructures\n * IE - a segment other than the one that is currently being processed. */\n _splitSafely(seg: Segment, pt: Point) {\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n // removeNode() doesn't work, so have re-find the seg\n // https://github.com/w8r/splay-tree/pull/5\n this.tree.delete(seg)\n const rightSE = seg.rightSE\n this.queue.delete(rightSE)\n const newEvents = seg.split(pt)\n newEvents.push(rightSE)\n // splitting can trigger consumption\n if (seg.consumedBy === undefined) this.tree.add(seg)\n return newEvents\n }\n}\n", "import { getBboxOverlap, isInBbox } from \"./bbox.js\"\nimport { MultiPolyIn, RingIn } from \"./geom-in.js\"\nimport { RingOut } from \"./geom-out.js\"\nimport operation from \"./operation.js\"\nimport { precision } from \"./precision.js\"\nimport SweepEvent, { Point } from \"./sweep-event.js\"\nimport { intersection } from \"./vector.js\"\n\ninterface State {\n rings: RingIn[],\n windings: number[],\n multiPolys: MultiPolyIn[]\n}\n\n// Give segments unique ID's to get consistent sorting of\n// segments and sweep events when all else is identical\nlet segmentId = 0\n\nexport default class Segment {\n id: number\n leftSE: SweepEvent\n rightSE: SweepEvent\n rings: RingIn[] | null\n windings: number[] | null\n ringOut: RingOut | undefined\n consumedBy: Segment | undefined\n prev: Segment | null | undefined\n _prevInResult: Segment | null | undefined\n _beforeState: State | undefined\n _afterState: State | undefined\n _isInResult: boolean | undefined\n\n /* This compare() function is for ordering segments in the sweep\n * line tree, and does so according to the following criteria:\n *\n * Consider the vertical line that lies an infinestimal step to the\n * right of the right-more of the two left endpoints of the input\n * segments. Imagine slowly moving a point up from negative infinity\n * in the increasing y direction. Which of the two segments will that\n * point intersect first? That segment comes 'before' the other one.\n *\n * If neither segment would be intersected by such a line, (if one\n * or more of the segments are vertical) then the line to be considered\n * is directly on the right-more of the two left inputs.\n */\n static compare(a: Segment, b: Segment) {\n const alx = a.leftSE.point.x\n const blx = b.leftSE.point.x\n const arx = a.rightSE.point.x\n const brx = b.rightSE.point.x\n\n // check if they're even in the same vertical plane\n if (brx.isLessThan(alx)) return 1\n if (arx.isLessThan(blx)) return -1\n\n const aly = a.leftSE.point.y\n const bly = b.leftSE.point.y\n const ary = a.rightSE.point.y\n const bry = b.rightSE.point.y\n\n // is left endpoint of segment B the right-more?\n if (alx.isLessThan(blx)) {\n // are the two segments in the same horizontal plane?\n if (bly.isLessThan(aly) && bly.isLessThan(ary)) return 1\n if (bly.isGreaterThan(aly) && bly.isGreaterThan(ary)) return -1\n\n // is the B left endpoint colinear to segment A?\n const aCmpBLeft = a.comparePoint(b.leftSE.point)\n if (aCmpBLeft < 0) return 1\n if (aCmpBLeft > 0) return -1\n\n // is the A right endpoint colinear to segment B ?\n const bCmpARight = b.comparePoint(a.rightSE.point)\n if (bCmpARight !== 0) return bCmpARight\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return -1\n }\n\n // is left endpoint of segment A the right-more?\n if (alx.isGreaterThan(blx)) {\n if (aly.isLessThan(bly) && aly.isLessThan(bry)) return -1\n if (aly.isGreaterThan(bly) && aly.isGreaterThan(bry)) return 1\n\n // is the A left endpoint colinear to segment B?\n const bCmpALeft = b.comparePoint(a.leftSE.point)\n if (bCmpALeft !== 0) return bCmpALeft\n\n // is the B right endpoint colinear to segment A?\n const aCmpBRight = a.comparePoint(b.rightSE.point)\n if (aCmpBRight < 0) return 1\n if (aCmpBRight > 0) return -1\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return 1\n }\n\n // if we get here, the two left endpoints are in the same\n // vertical plane, ie alx === blx\n\n // consider the lower left-endpoint to come first\n if (aly.isLessThan(bly)) return -1\n if (aly.isGreaterThan(bly)) return 1\n\n // left endpoints are identical\n // check for colinearity by using the left-more right endpoint\n\n // is the A right endpoint more left-more?\n if (arx.isLessThan(brx)) {\n const bCmpARight = b.comparePoint(a.rightSE.point)\n if (bCmpARight !== 0) return bCmpARight\n }\n\n // is the B right endpoint more left-more?\n if (arx.isGreaterThan(brx)) {\n const aCmpBRight = a.comparePoint(b.rightSE.point)\n if (aCmpBRight < 0) return 1\n if (aCmpBRight > 0) return -1\n }\n\n if (!arx.eq(brx)) {\n // are these two [almost] vertical segments with opposite orientation?\n // if so, the one with the lower right endpoint comes first\n const ay = ary.minus(aly)\n const ax = arx.minus(alx)\n const by = bry.minus(bly)\n const bx = brx.minus(blx)\n if (ay.isGreaterThan(ax) && by.isLessThan(bx)) return 1\n if (ay.isLessThan(ax) && by.isGreaterThan(bx)) return -1\n }\n\n // we have colinear segments with matching orientation\n // consider the one with more left-more right endpoint to be first\n if (arx.isGreaterThan(brx)) return 1\n if (arx.isLessThan(brx)) return -1\n\n // if we get here, two two right endpoints are in the same\n // vertical plane, ie arx === brx\n\n // consider the lower right-endpoint to come first\n if (ary.isLessThan(bry)) return -1\n if (ary.isGreaterThan(bry)) return 1\n\n // right endpoints identical as well, so the segments are idential\n // fall back on creation order as consistent tie-breaker\n if (a.id < b.id) return -1\n if (a.id > b.id) return 1\n\n // identical segment, ie a === b\n return 0\n }\n\n /* Warning: a reference to ringWindings input will be stored,\n * and possibly will be later modified */\n constructor(leftSE: SweepEvent, rightSE: SweepEvent, rings: RingIn[], windings: number[]) {\n this.id = ++segmentId\n this.leftSE = leftSE\n leftSE.segment = this\n leftSE.otherSE = rightSE\n this.rightSE = rightSE\n rightSE.segment = this\n rightSE.otherSE = leftSE\n this.rings = rings\n this.windings = windings\n // left unset for performance, set later in algorithm\n // this.ringOut, this.consumedBy, this.prev\n }\n\n static fromRing(pt1: Point, pt2: Point, ring: RingIn) {\n let leftPt: Point, rightPt: Point, winding: number\n\n // ordering the two points according to sweep line ordering\n const cmpPts = SweepEvent.comparePoints(pt1, pt2)\n if (cmpPts < 0) {\n leftPt = pt1\n rightPt = pt2\n winding = 1\n } else if (cmpPts > 0) {\n leftPt = pt2\n rightPt = pt1\n winding = -1\n } else\n throw new Error(\n `Tried to create degenerate segment at [${pt1.x}, ${pt1.y}]`,\n )\n\n const leftSE = new SweepEvent(leftPt, true)\n const rightSE = new SweepEvent(rightPt, false)\n return new Segment(leftSE, rightSE, [ring], [winding])\n }\n\n /* When a segment is split, the rightSE is replaced with a new sweep event */\n replaceRightSE(newRightSE: SweepEvent) {\n this.rightSE = newRightSE\n this.rightSE.segment = this\n this.rightSE.otherSE = this.leftSE\n this.leftSE.otherSE = this.rightSE\n }\n\n bbox() {\n const y1 = this.leftSE.point.y\n const y2 = this.rightSE.point.y\n return {\n ll: { x: this.leftSE.point.x, y: y1.isLessThan(y2) ? y1 : y2 },\n ur: { x: this.rightSE.point.x, y: y1.isGreaterThan(y2) ? y1 : y2 },\n }\n }\n\n /* A vector from the left point to the right */\n vector() {\n return {\n x: this.rightSE.point.x.minus(this.leftSE.point.x),\n y: this.rightSE.point.y.minus(this.leftSE.point.y),\n }\n }\n\n isAnEndpoint(pt: Point) {\n return (\n (pt.x.eq(this.leftSE.point.x) && pt.y.eq(this.leftSE.point.y)) ||\n (pt.x.eq(this.rightSE.point.x) && pt.y.eq(this.rightSE.point.y))\n )\n }\n\n /* Compare this segment with a point.\n *\n * A point P is considered to be colinear to a segment if there\n * exists a distance D such that if we travel along the segment\n * from one * endpoint towards the other a distance D, we find\n * ourselves at point P.\n *\n * Return value indicates:\n *\n * 1: point lies above the segment (to the left of vertical)\n * 0: point is colinear to segment\n * -1: point lies below the segment (to the right of vertical)\n */\n comparePoint(point: Point) {\n return precision.orient(this.leftSE.point, point, this.rightSE.point)\n }\n\n /**\n * Given another segment, returns the first non-trivial intersection\n * between the two segments (in terms of sweep line ordering), if it exists.\n *\n * A 'non-trivial' intersection is one that will cause one or both of the\n * segments to be split(). As such, 'trivial' vs. 'non-trivial' intersection:\n *\n * * endpoint of segA with endpoint of segB --> trivial\n * * endpoint of segA with point along segB --> non-trivial\n * * endpoint of segB with point along segA --> non-trivial\n * * point along segA with point along segB --> non-trivial\n *\n * If no non-trivial intersection exists, return null\n * Else, return null.\n */\n getIntersection(other: Segment) {\n // If bboxes don't overlap, there can't be any intersections\n const tBbox = this.bbox()\n const oBbox = other.bbox()\n const bboxOverlap = getBboxOverlap(tBbox, oBbox)\n if (bboxOverlap === null) return null\n\n // We first check to see if the endpoints can be considered intersections.\n // This will 'snap' intersections to endpoints if possible, and will\n // handle cases of colinearity.\n\n const tlp = this.leftSE.point\n const trp = this.rightSE.point\n const olp = other.leftSE.point\n const orp = other.rightSE.point\n\n // does each endpoint touch the other segment?\n // note that we restrict the 'touching' definition to only allow segments\n // to touch endpoints that lie forward from where we are in the sweep line pass\n const touchesOtherLSE = isInBbox(tBbox, olp) && this.comparePoint(olp) === 0\n const touchesThisLSE = isInBbox(oBbox, tlp) && other.comparePoint(tlp) === 0\n const touchesOtherRSE = isInBbox(tBbox, orp) && this.comparePoint(orp) === 0\n const touchesThisRSE = isInBbox(oBbox, trp) && other.comparePoint(trp) === 0\n\n // do left endpoints match?\n if (touchesThisLSE && touchesOtherLSE) {\n // these two cases are for colinear segments with matching left\n // endpoints, and one segment being longer than the other\n if (touchesThisRSE && !touchesOtherRSE) return trp\n if (!touchesThisRSE && touchesOtherRSE) return orp\n // either the two segments match exactly (two trival intersections)\n // or just on their left endpoint (one trivial intersection\n return null\n }\n\n // does this left endpoint matches (other doesn't)\n if (touchesThisLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesOtherRSE) {\n if (tlp.x.eq(orp.x) && tlp.y.eq(orp.y)) return null\n }\n // t-intersection on left endpoint\n return tlp\n }\n\n // does other left endpoint matches (this doesn't)\n if (touchesOtherLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesThisRSE) {\n if (trp.x.eq(olp.x) && trp.y.eq(olp.y)) return null\n }\n // t-intersection on left endpoint\n return olp\n }\n\n // trivial intersection on right endpoints\n if (touchesThisRSE && touchesOtherRSE) return null\n\n // t-intersections on just one right endpoint\n if (touchesThisRSE) return trp\n if (touchesOtherRSE) return orp\n\n // None of our endpoints intersect. Look for a general intersection between\n // infinite lines laid over the segments\n const pt = intersection(tlp, this.vector(), olp, other.vector())\n\n // are the segments parrallel? Note that if they were colinear with overlap,\n // they would have an endpoint intersection and that case was already handled above\n if (pt === null) return null\n\n // is the intersection found between the lines not on the segments?\n if (!isInBbox(bboxOverlap, pt)) return null\n\n // round the the computed point if needed\n return precision.snap(pt) as Point\n }\n\n /**\n * Split the given segment into multiple segments on the given points.\n * * Each existing segment will retain its leftSE and a new rightSE will be\n * generated for it.\n * * A new segment will be generated which will adopt the original segment's\n * rightSE, and a new leftSE will be generated for it.\n * * If there are more than two points given to split on, new segments\n * in the middle will be generated with new leftSE and rightSE's.\n * * An array of the newly generated SweepEvents will be returned.\n *\n * Warning: input array of points is modified\n */\n split(point: Point) {\n const newEvents = []\n const alreadyLinked = point.events !== undefined\n\n const newLeftSE = new SweepEvent(point, true)\n const newRightSE = new SweepEvent(point, false)\n const oldRightSE = this.rightSE\n this.replaceRightSE(newRightSE)\n newEvents.push(newRightSE)\n newEvents.push(newLeftSE)\n const newSeg = new Segment(\n newLeftSE,\n oldRightSE,\n this.rings!.slice(),\n this.windings!.slice(),\n )\n\n // when splitting a nearly vertical downward-facing segment,\n // sometimes one of the resulting new segments is vertical, in which\n // case its left and right events may need to be swapped\n if (\n SweepEvent.comparePoints(newSeg.leftSE.point, newSeg.rightSE.point) > 0\n ) {\n newSeg.swapEvents()\n }\n if (SweepEvent.comparePoints(this.leftSE.point, this.rightSE.point) > 0) {\n this.swapEvents()\n }\n\n // in the point we just used to create new sweep events with was already\n // linked to other events, we need to check if either of the affected\n // segments should be consumed\n if (alreadyLinked) {\n newLeftSE.checkForConsuming()\n newRightSE.checkForConsuming()\n }\n\n return newEvents\n }\n\n /* Swap which event is left and right */\n swapEvents() {\n const tmpEvt = this.rightSE\n this.rightSE = this.leftSE\n this.leftSE = tmpEvt\n this.leftSE.isLeft = true\n this.rightSE.isLeft = false\n for (let i = 0, iMax = this.windings!.length; i < iMax; i++) {\n this.windings![i] *= -1\n }\n }\n\n /* Consume another segment. We take their rings under our wing\n * and mark them as consumed. Use for perfectly overlapping segments */\n consume(other: Segment) {\n let consumer = this as Segment\n let consumee = other\n while (consumer.consumedBy) consumer = consumer.consumedBy\n while (consumee.consumedBy) consumee = consumee.consumedBy\n\n const cmp = Segment.compare(consumer, consumee)\n if (cmp === 0) return // already consumed\n // the winner of the consumption is the earlier segment\n // according to sweep line ordering\n if (cmp > 0) {\n const tmp = consumer\n consumer = consumee\n consumee = tmp\n }\n\n // make sure a segment doesn't consume it's prev\n if (consumer.prev === consumee) {\n const tmp = consumer\n consumer = consumee\n consumee = tmp\n }\n\n for (let i = 0, iMax = consumee.rings!.length; i < iMax; i++) {\n const ring = consumee.rings![i]\n const winding = consumee.windings![i]\n const index = consumer.rings!.indexOf(ring)\n if (index === -1) {\n consumer.rings!.push(ring)\n consumer.windings!.push(winding)\n } else consumer.windings![index] += winding\n }\n consumee.rings = null\n consumee.windings = null\n consumee.consumedBy = consumer\n\n // mark sweep events consumed as to maintain ordering in sweep event queue\n consumee.leftSE.consumedBy = consumer.leftSE\n consumee.rightSE.consumedBy = consumer.rightSE\n }\n\n /* The first segment previous segment chain that is in the result */\n prevInResult(): Segment | null | undefined {\n if (this._prevInResult !== undefined) return this._prevInResult\n if (!this.prev) this._prevInResult = null\n else if (this.prev.isInResult()) this._prevInResult = this.prev\n else this._prevInResult = this.prev.prevInResult()\n return this._prevInResult\n }\n\n beforeState(): State {\n if (this._beforeState !== undefined) return this._beforeState\n if (!this.prev)\n this._beforeState = {\n rings: [],\n windings: [],\n multiPolys: [],\n }\n else {\n const seg = this.prev.consumedBy || this.prev\n this._beforeState = seg.afterState()\n }\n return this._beforeState\n }\n\n afterState() {\n if (this._afterState !== undefined) return this._afterState\n\n const beforeState = this.beforeState()\n this._afterState = {\n rings: beforeState.rings.slice(0),\n windings: beforeState.windings.slice(0),\n multiPolys: [],\n }\n const ringsAfter = this._afterState.rings\n const windingsAfter = this._afterState.windings\n const mpsAfter = this._afterState.multiPolys\n\n // calculate ringsAfter, windingsAfter\n for (let i = 0, iMax = this.rings!.length; i < iMax; i++) {\n const ring = this.rings![i]\n const winding = this.windings![i]\n const index = ringsAfter.indexOf(ring)\n if (index === -1) {\n ringsAfter.push(ring)\n windingsAfter.push(winding)\n } else windingsAfter[index] += winding\n }\n\n // calcualte polysAfter\n const polysAfter = []\n const polysExclude = []\n for (let i = 0, iMax = ringsAfter.length; i < iMax; i++) {\n if (windingsAfter[i] === 0) continue // non-zero rule\n const ring = ringsAfter[i]\n const poly = ring.poly\n if (polysExclude.indexOf(poly) !== -1) continue\n if (ring.isExterior) polysAfter.push(poly)\n else {\n if (polysExclude.indexOf(poly) === -1) polysExclude.push(poly)\n const index = polysAfter.indexOf(ring.poly)\n if (index !== -1) polysAfter.splice(index, 1)\n }\n }\n\n // calculate multiPolysAfter\n for (let i = 0, iMax = polysAfter.length; i < iMax; i++) {\n const mp = polysAfter[i].multiPoly\n if (mpsAfter.indexOf(mp) === -1) mpsAfter.push(mp)\n }\n\n return this._afterState\n }\n\n /* Is this segment part of the final result? */\n isInResult() {\n // if we've been consumed, we're not in the result\n if (this.consumedBy) return false\n\n if (this._isInResult !== undefined) return this._isInResult\n\n const mpsBefore = this.beforeState().multiPolys\n const mpsAfter = this.afterState().multiPolys\n\n switch (operation.type) {\n case \"union\": {\n // UNION - included iff:\n // * On one side of us there is 0 poly interiors AND\n // * On the other side there is 1 or more.\n const noBefores = mpsBefore.length === 0\n const noAfters = mpsAfter.length === 0\n this._isInResult = noBefores !== noAfters\n break\n }\n\n case \"intersection\": {\n // INTERSECTION - included iff:\n // * on one side of us all multipolys are rep. with poly interiors AND\n // * on the other side of us, not all multipolys are repsented\n // with poly interiors\n let least\n let most\n if (mpsBefore.length < mpsAfter.length) {\n least = mpsBefore.length\n most = mpsAfter.length\n } else {\n least = mpsAfter.length\n most = mpsBefore.length\n }\n this._isInResult = most === operation.numMultiPolys && least < most\n break\n }\n\n case \"xor\": {\n // XOR - included iff:\n // * the difference between the number of multipolys represented\n // with poly interiors on our two sides is an odd number\n const diff = Math.abs(mpsBefore.length - mpsAfter.length)\n this._isInResult = diff % 2 === 1\n break\n }\n\n case \"difference\": {\n // DIFFERENCE included iff:\n // * on exactly one side, we have just the subject\n const isJustSubject = (mps: MultiPolyIn[]) => mps.length === 1 && mps[0].isSubject\n this._isInResult = isJustSubject(mpsBefore) !== isJustSubject(mpsAfter)\n break\n }\n }\n\n return this._isInResult\n }\n}\n", "import { Geom } from \"./geom-in.js\"\nimport { precision } from \"./precision.js\"\nimport operation from \"./operation.js\"\n\nexport { Geom }\n\nexport const union = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"union\", geom, moreGeoms)\n\nexport const intersection = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"intersection\", geom, moreGeoms)\n\nexport const xor = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"xor\", geom, moreGeoms)\n\nexport const difference = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"difference\", geom, moreGeoms)\n\nexport const setPrecision = precision.set", "module.exports.RADIUS = 6378137;\nmodule.exports.FLATTENING = 1/298.257223563;\nmodule.exports.POLAR_RADIUS = 6356752.3142;\n", "var wgs84 = require('wgs84');\n\nmodule.exports.geometry = geometry;\nmodule.exports.ring = ringArea;\n\nfunction geometry(_) {\n var area = 0, i;\n switch (_.type) {\n case 'Polygon':\n return polygonArea(_.coordinates);\n case 'MultiPolygon':\n for (i = 0; i < _.coordinates.length; i++) {\n area += polygonArea(_.coordinates[i]);\n }\n return area;\n case 'Point':\n case 'MultiPoint':\n case 'LineString':\n case 'MultiLineString':\n return 0;\n case 'GeometryCollection':\n for (i = 0; i < _.geometries.length; i++) {\n area += geometry(_.geometries[i]);\n }\n return area;\n }\n}\n\nfunction polygonArea(coords) {\n var area = 0;\n if (coords && coords.length > 0) {\n area += Math.abs(ringArea(coords[0]));\n for (var i = 1; i < coords.length; i++) {\n area -= Math.abs(ringArea(coords[i]));\n }\n }\n return area;\n}\n\n/**\n * Calculate the approximate area of the polygon were it projected onto\n * the earth. Note that this area will be positive if ring is oriented\n * clockwise, otherwise it will be negative.\n *\n * Reference:\n * Robert. G. Chamberlain and William H. Duquette, \"Some Algorithms for\n * Polygons on a Sphere\", JPL Publication 07-03, Jet Propulsion\n * Laboratory, Pasadena, CA, June 2007 http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409\n *\n * Returns:\n * {float} The approximate signed geodesic area of the polygon in square\n * meters.\n */\n\nfunction ringArea(coords) {\n var p1, p2, p3, lowerIndex, middleIndex, upperIndex, i,\n area = 0,\n coordsLength = coords.length;\n\n if (coordsLength > 2) {\n for (i = 0; i < coordsLength; i++) {\n if (i === coordsLength - 2) {// i = N-2\n lowerIndex = coordsLength - 2;\n middleIndex = coordsLength -1;\n upperIndex = 0;\n } else if (i === coordsLength - 1) {// i = N-1\n lowerIndex = coordsLength - 1;\n middleIndex = 0;\n upperIndex = 1;\n } else { // i = 0 to N-3\n lowerIndex = i;\n middleIndex = i+1;\n upperIndex = i+2;\n }\n p1 = coords[lowerIndex];\n p2 = coords[middleIndex];\n p3 = coords[upperIndex];\n area += ( rad(p3[0]) - rad(p1[0]) ) * Math.sin( rad(p2[1]));\n }\n\n area = area * wgs84.RADIUS * wgs84.RADIUS / 2;\n }\n\n return area;\n}\n\nfunction rad(_) {\n return _ * Math.PI / 180;\n}", "exports.validateCenter = function validateCenter(center) {\n var validCenterLengths = [2, 3];\n if (!Array.isArray(center) || !validCenterLengths.includes(center.length)) {\n throw new Error(\"ERROR! Center has to be an array of length two or three\");\n }\n\n var [lng, lat] = center;\n if (typeof lng !== \"number\" || typeof lat !== \"number\") {\n throw new Error(\n `ERROR! Longitude and Latitude has to be numbers but where ${typeof lng} and ${typeof lat}`\n );\n }\n if (lng > 180 || lng < -180) {\n throw new Error(`ERROR! Longitude has to be between -180 and 180 but was ${lng}`);\n }\n\n if (lat > 90 || lat < -90) {\n throw new Error(`ERROR! Latitude has to be between -90 and 90 but was ${lat}`);\n }\n};\n", "exports.validateRadius = function validateRadius(radius) {\n if (typeof radius !== \"number\") {\n throw new Error(`ERROR! Radius has to be a positive number but was: ${typeof radius}`);\n }\n\n if (radius <= 0) {\n throw new Error(`ERROR! Radius has to be a positive number but was: ${radius}`);\n }\n};\n", "exports.validateNumberOfEdges = function validateNumberOfEdges(numberOfEdges) {\n if (typeof numberOfEdges !== \"number\") {\n const ARGUMENT_TYPE = Array.isArray(numberOfEdges) ? \"array\" : typeof numberOfEdges;\n throw new Error(`ERROR! Number of edges has to be a number but was: ${ARGUMENT_TYPE}`);\n }\n\n if (numberOfEdges < 3) {\n throw new Error(`ERROR! Number of edges has to be at least 3 but was: ${numberOfEdges}`);\n }\n};\n", "exports.validateEarthRadius = function validateEarthRadius(earthRadius) {\n if (typeof earthRadius !== \"number\") {\n const ARGUMENT_TYPE = Array.isArray(earthRadius) ? \"array\" : typeof earthRadius;\n throw new Error(`ERROR! Earth radius has to be a number but was: ${ARGUMENT_TYPE}`);\n }\n\n if (earthRadius <= 0) {\n throw new Error(`ERROR! Earth radius has to be a positive number but was: ${earthRadius}`);\n }\n};\n", "exports.validateBearing = function validateBearing(bearing) {\n if (typeof bearing !== \"number\") {\n const ARGUMENT_TYPE = Array.isArray(bearing) ? \"array\" : typeof bearing;\n throw new Error(`ERROR! Bearing has to be a number but was: ${ARGUMENT_TYPE}`);\n }\n};\n", "var validateCenter = require(\"./validateCenter\").validateCenter;\nvar validateRadius = require(\"./validateRadius\").validateRadius;\nvar validateNumberOfEdges = require(\"./validateNumberOfEdges\").validateNumberOfEdges;\nvar validateEarthRadius = require(\"./validateEarthRadius\").validateEarthRadius;\nvar validateBearing = require(\"./validateBearing\").validateBearing;\n\nfunction validateInput({ center, radius, numberOfEdges, earthRadius, bearing }) {\n validateCenter(center);\n validateRadius(radius);\n validateNumberOfEdges(numberOfEdges);\n validateEarthRadius(earthRadius);\n validateBearing(bearing);\n}\n\nexports.validateCenter = validateCenter;\nexports.validateRadius = validateRadius;\nexports.validateNumberOfEdges = validateNumberOfEdges;\nexports.validateEarthRadius = validateEarthRadius;\nexports.validateBearing = validateBearing;\nexports.validateInput = validateInput;\n", "\"use strict\";\nvar { validateInput } = require(\"./input-validation\");\n\nconst defaultEarthRadius = 6378137; // equatorial Earth radius\n\nfunction toRadians(angleInDegrees) {\n return (angleInDegrees * Math.PI) / 180;\n}\n\nfunction toDegrees(angleInRadians) {\n return (angleInRadians * 180) / Math.PI;\n}\n\nfunction offset(c1, distance, earthRadius, bearing) {\n var lat1 = toRadians(c1[1]);\n var lon1 = toRadians(c1[0]);\n var dByR = distance / earthRadius;\n var lat = Math.asin(\n Math.sin(lat1) * Math.cos(dByR) + Math.cos(lat1) * Math.sin(dByR) * Math.cos(bearing)\n );\n var lon =\n lon1 +\n Math.atan2(\n Math.sin(bearing) * Math.sin(dByR) * Math.cos(lat1),\n Math.cos(dByR) - Math.sin(lat1) * Math.sin(lat)\n );\n return [toDegrees(lon), toDegrees(lat)];\n}\n\nmodule.exports = function circleToPolygon(center, radius, options) {\n var n = getNumberOfEdges(options);\n var earthRadius = getEarthRadius(options);\n var bearing = getBearing(options);\n var direction = getDirection(options);\n\n // validateInput() throws error on invalid input and do nothing on valid input\n validateInput({ center, radius, numberOfEdges: n, earthRadius, bearing });\n\n var start = toRadians(bearing);\n var coordinates = [];\n for (var i = 0; i < n; ++i) {\n coordinates.push(\n offset(\n center, radius, earthRadius, start + (direction * 2 * Math.PI * -i) / n\n )\n );\n }\n coordinates.push(coordinates[0]);\n\n return {\n type: \"Polygon\",\n coordinates: [coordinates]\n };\n};\n\nfunction getNumberOfEdges(options) {\n if (isUndefinedOrNull(options)) {\n return 32;\n } else if (isObjectNotArray(options)) {\n var numberOfEdges = options.numberOfEdges;\n return numberOfEdges === undefined ? 32 : numberOfEdges;\n }\n return options;\n}\n\nfunction getEarthRadius(options) {\n if (isUndefinedOrNull(options)) {\n return defaultEarthRadius;\n } else if (isObjectNotArray(options)) {\n var earthRadius = options.earthRadius;\n return earthRadius === undefined ? defaultEarthRadius : earthRadius;\n }\n return defaultEarthRadius;\n}\n\nfunction getDirection(options){\n if (isObjectNotArray(options) && options.rightHandRule){\n return -1;\n }\n return 1;\n}\n\nfunction getBearing(options) {\n if (isUndefinedOrNull(options)) {\n return 0;\n } else if (isObjectNotArray(options)) {\n var bearing = options.bearing;\n return bearing === undefined ? 0 : bearing;\n }\n return 0;\n}\n\nfunction isObjectNotArray(argument) {\n return argument !== null && typeof argument === \"object\" && !Array.isArray(argument);\n}\n\nfunction isUndefinedOrNull(argument) {\n return argument === null || argument === undefined;\n}\n", "(function() {\n\n function parse(t, coordinatePrecision, extrasPrecision) {\n\n function point(p) {\n return p.map(function(e, index) {\n if (index < 2) {\n return 1 * e.toFixed(coordinatePrecision);\n } else {\n return 1 * e.toFixed(extrasPrecision);\n }\n });\n }\n\n function multi(l) {\n return l.map(point);\n }\n\n function poly(p) {\n return p.map(multi);\n }\n\n function multiPoly(m) {\n return m.map(poly);\n }\n\n function geometry(obj) {\n if (!obj) {\n return {};\n }\n \n switch (obj.type) {\n case \"Point\":\n obj.coordinates = point(obj.coordinates);\n return obj;\n case \"LineString\":\n case \"MultiPoint\":\n obj.coordinates = multi(obj.coordinates);\n return obj;\n case \"Polygon\":\n case \"MultiLineString\":\n obj.coordinates = poly(obj.coordinates);\n return obj;\n case \"MultiPolygon\":\n obj.coordinates = multiPoly(obj.coordinates);\n return obj;\n case \"GeometryCollection\":\n obj.geometries = obj.geometries.map(geometry);\n return obj;\n default :\n return {};\n }\n }\n\n function feature(obj) {\n obj.geometry = geometry(obj.geometry);\n return obj\n }\n\n function featureCollection(f) {\n f.features = f.features.map(feature);\n return f;\n }\n\n function geometryCollection(g) {\n g.geometries = g.geometries.map(geometry);\n return g;\n }\n\n if (!t) {\n return t;\n }\n\n switch (t.type) {\n case \"Feature\":\n return feature(t);\n case \"GeometryCollection\" :\n return geometryCollection(t);\n case \"FeatureCollection\" :\n return featureCollection(t);\n case \"Point\":\n case \"LineString\":\n case \"Polygon\":\n case \"MultiPoint\":\n case \"MultiPolygon\":\n case \"MultiLineString\":\n return geometry(t);\n default :\n return t;\n }\n \n }\n\n module.exports = parse;\n module.exports.parse = parse;\n\n}());\n \n", "// Note: This regex matches even invalid JSON strings, but since we\u2019re\n// working on the output of `JSON.stringify` we know that only valid strings\n// are present (unless the user supplied a weird `options.indent` but in\n// that case we don\u2019t care since the output would be invalid anyway).\nconst stringOrChar = /(\"(?:[^\\\\\"]|\\\\.)*\")|[:,]/g;\n\nexport default function stringify(passedObj, options = {}) {\n const indent = JSON.stringify(\n [1],\n undefined,\n options.indent === undefined ? 2 : options.indent\n ).slice(2, -3);\n\n const maxLength =\n indent === \"\"\n ? Infinity\n : options.maxLength === undefined\n ? 80\n : options.maxLength;\n\n let { replacer } = options;\n\n return (function _stringify(obj, currentIndent, reserved) {\n if (obj && typeof obj.toJSON === \"function\") {\n obj = obj.toJSON();\n }\n\n const string = JSON.stringify(obj, replacer);\n\n if (string === undefined) {\n return string;\n }\n\n const length = maxLength - currentIndent.length - reserved;\n\n if (string.length <= length) {\n const prettified = string.replace(\n stringOrChar,\n (match, stringLiteral) => {\n return stringLiteral || `${match} `;\n }\n );\n if (prettified.length <= length) {\n return prettified;\n }\n }\n\n if (replacer != null) {\n obj = JSON.parse(string);\n replacer = undefined;\n }\n\n if (typeof obj === \"object\" && obj !== null) {\n const nextIndent = currentIndent + indent;\n const items = [];\n let index = 0;\n let start;\n let end;\n\n if (Array.isArray(obj)) {\n start = \"[\";\n end = \"]\";\n const { length } = obj;\n for (; index < length; index++) {\n items.push(\n _stringify(obj[index], nextIndent, index === length - 1 ? 0 : 1) ||\n \"null\"\n );\n }\n } else {\n start = \"{\";\n end = \"}\";\n const keys = Object.keys(obj);\n const { length } = keys;\n for (; index < length; index++) {\n const key = keys[index];\n const keyPart = `${JSON.stringify(key)}: `;\n const value = _stringify(\n obj[key],\n nextIndent,\n keyPart.length + (index === length - 1 ? 0 : 1)\n );\n if (value !== undefined) {\n items.push(keyPart + value);\n }\n }\n }\n\n if (items.length > 0) {\n return [start, indent + items.join(`,\\n${nextIndent}`), end].join(\n `\\n${currentIndent}`\n );\n }\n }\n\n return string;\n })(passedObj, \"\", 0);\n}\n", "import * as CountryCoder from '@rapideditor/country-coder';\nimport * as Polyclip from 'polyclip-ts';\nimport calcArea from '@mapbox/geojson-area';\nimport circleToPolygon from 'circle-to-polygon';\nimport precision from 'geojson-precision';\nimport prettyStringify from 'json-stringify-pretty-compact';\nimport type { Polygon, MultiPolygon } from 'geojson';\n\n// Type definitions\nexport type Vec2 = [number, number];\nexport type Vec3 = [number, number, number];\nexport type Location = Vec2 | Vec3 | string | number;\n\nexport interface FeatureProperties {\n id: string;\n area: number;\n members?: string[];\n [key: string]: unknown;\n}\n\nexport type GeoJSONGeometry = Polygon | MultiPolygon;\n\nexport interface GeoJSONFeature {\n type: 'Feature';\n id: string;\n properties: FeatureProperties;\n geometry: GeoJSONGeometry;\n}\n\nexport interface FeatureCollection {\n type: 'FeatureCollection';\n features: GeoJSONFeature[];\n}\n\nexport interface LocationSet {\n include?: Location[];\n exclude?: Location[];\n}\n\nexport interface ValidatedLocation {\n type: 'point' | 'geojson' | 'countrycoder';\n location: Location;\n id: string;\n}\n\nexport interface ResolvedLocation extends ValidatedLocation {\n feature: GeoJSONFeature;\n}\n\nexport interface ValidatedLocationSet {\n type: 'locationset';\n locationSet: LocationSet;\n id: string;\n}\n\nexport interface ResolvedLocationSet extends ValidatedLocationSet {\n feature: GeoJSONFeature;\n}\n\nexport type ClipOperation = 'UNION' | 'DIFFERENCE';\n\nexport type StringifyOptions = Parameters[1];\n\nexport class LocationConflation {\n public _cache: Map;\n public strict: boolean;\n\n /**\n * Creates a new LocationConflation instance\n * @param fc - Optional FeatureCollection of known features with filename-like IDs (e.g., \"something.geojson\")\n */\n constructor(fc?: FeatureCollection) {\n // The _cache retains resolved features, so if you ask for the same thing multiple times\n // we don't repeat the expensive resolving/clipping operations.\n //\n // Each feature has a stable identifier that is used as the cache key.\n // The identifiers look like:\n // - for point locations, the stringified point: e.g. '[8.67039,49.41882]'\n // - for geojson locations, the geojson id: e.g. 'de-hamburg.geojson'\n // - for countrycoder locations, feature.id property: e.g. 'Q2' (countrycoder uses Wikidata identifiers)\n // - for aggregated locationSets, +[include]-[exclude]: e.g '+[Q2]-[Q18,Q27611]'\n this._cache = new Map();\n\n // When strict mode = true, throw on invalid locations or locationSets.\n // When strict mode = false, return `null` for invalid locations or locationSets.\n this.strict = true;\n\n // process input FeatureCollection\n if (fc?.type === 'FeatureCollection' && Array.isArray(fc.features)) {\n for (const feature of fc.features) {\n feature.properties = feature.properties || ({} as FeatureProperties);\n const props = feature.properties;\n\n // Get `id` from either `id` or `properties`\n let id = feature.id || props.id;\n if (!id || !/^\\S+\\.geojson$/i.test(id)) continue;\n\n // Ensure `id` exists and is lowercase\n id = id.toLowerCase();\n feature.id = id;\n props.id = id;\n\n // Ensure `area` property exists\n if (!props.area) {\n const area = calcArea.geometry(feature.geometry) / 1e6; // m² to km²\n props.area = Number(area.toFixed(2));\n }\n\n this._cache.set(id, feature);\n }\n }\n\n // Replace CountryCoder world geometry to be a polygon covering the world.\n const worldFeature = CountryCoder.feature('Q2');\n const world = cloneDeep(worldFeature) as unknown as GeoJSONFeature;\n world.geometry = {\n type: 'Polygon',\n coordinates: [[[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]]],\n };\n world.id = 'Q2';\n world.properties.id = 'Q2';\n world.properties.area = calcArea.geometry(world.geometry) / 1e6; // m² to km²\n this._cache.set('Q2', world);\n }\n\n /**\n * Validates a location and returns its type and stable identifier\n * @param location - Location to validate (point, geojson filename, or country code)\n * @returns Validated location object or null if invalid\n *\n * @example\n * ```typescript\n * // Point location with default 25km radius\n * loco.validateLocation([8.67039, 49.41882]);\n * // => { type: 'point', location: [8.67039, 49.41882], id: '[8.67039,49.41882]' }\n *\n * // Point location with custom radius\n * loco.validateLocation([-77.0369, 38.9072, 10]);\n * // => { type: 'point', location: [-77.0369, 38.9072, 10], id: '[-77.0369,38.9072,10]' }\n *\n * // Country code\n * loco.validateLocation('de');\n * // => { type: 'countrycoder', location: 'de', id: 'Q183' }\n *\n * // GeoJSON file\n * loco.validateLocation('philly_metro.geojson');\n * // => { type: 'geojson', location: 'philly_metro.geojson', id: 'philly_metro.geojson' }\n * ```\n */\n validateLocation(location: Location): ValidatedLocation | null {\n // [lon, lat] or [lon, lat, radius] point?\n if (Array.isArray(location) && (location.length === 2 || location.length === 3)) {\n const lon = location[0];\n const lat = location[1];\n const radius = location[2];\n\n if (\n Number.isFinite(lon) &&\n lon >= -180 &&\n lon <= 180 &&\n Number.isFinite(lat) &&\n lat >= -90 &&\n lat <= 90 &&\n (location.length === 2 || (radius !== undefined && Number.isFinite(radius) && radius > 0))\n ) {\n const id = '[' + location.toString() + ']';\n return { type: 'point', location, id };\n }\n } else if (typeof location === 'string' && /^\\S+\\.geojson$/i.test(location)) {\n // a .geojson filename?\n const id = location.toLowerCase();\n if (this._cache.has(id)) {\n return { type: 'geojson', location, id };\n }\n } else if (typeof location === 'string' || typeof location === 'number') {\n // a country-coder value?\n const feature = CountryCoder.feature(location);\n if (feature) {\n // Use wikidata QID as the identifier, since that seems to be the one\n // property that everything in CountryCoder is guaranteed to have.\n const id = feature.properties.wikidata;\n return { type: 'countrycoder', location, id };\n }\n }\n\n if (this.strict) {\n throw new Error(`validateLocation: Invalid location: \"${location}\".`);\n }\n return null;\n }\n\n /**\n * Resolves a location to a GeoJSON feature\n * @param location - Location to resolve\n * @returns Resolved location with GeoJSON feature or null if invalid\n *\n * @example\n * ```typescript\n * // Resolve a point location to a circular polygon\n * const result = loco.resolveLocation([8.67039, 49.41882]);\n * // result.feature is a GeoJSON Feature with a circular Polygon geometry\n *\n * // Resolve a country code\n * const germany = loco.resolveLocation('de');\n * // germany.feature is a GeoJSON Feature with Germany's boundary\n *\n * // Resolve a custom GeoJSON file\n * const metro = loco.resolveLocation('philly_metro.geojson');\n * // metro.feature is the pre-loaded GeoJSON Feature\n * ```\n */\n resolveLocation(location: Location): ResolvedLocation | null {\n const valid = this.validateLocation(location);\n if (!valid) return null;\n\n const id = valid.id;\n\n // Return a result from cache if we can\n if (this._cache.has(id)) {\n return { ...valid, feature: this._cache.get(id)! };\n }\n\n // A [lon,lat] coordinate pair?\n if (valid.type === 'point' && Array.isArray(location)) {\n const lon = location[0];\n const lat = location[1];\n const radius = location[2] || 25; // km\n const EDGES = 10;\n const PRECISION = 3;\n const area = Math.PI * radius * radius;\n const feature = precision(\n {\n type: 'Feature',\n id,\n properties: { id, area: Number(area.toFixed(2)) },\n geometry: circleToPolygon([lon, lat], radius * 1000, EDGES), // km to m\n },\n PRECISION\n ) as GeoJSONFeature;\n this._cache.set(id, feature);\n return { ...valid, feature };\n }\n\n // A .geojson filename?\n if (valid.type === 'geojson') {\n // nothing to do here - these are all in _cache and would have returned already\n }\n\n // A country-coder identifier?\n if (valid.type === 'countrycoder') {\n const ccFeature = CountryCoder.feature(id);\n const feature = cloneDeep(ccFeature) as unknown as GeoJSONFeature;\n const props = feature.properties;\n\n // -> This block of code is weird and requires some explanation. <-\n // CountryCoder includes higher level features which are made up of members.\n // These features don't have their own geometry, but CountryCoder provides an\n // `aggregateFeature` method to combine these members into a MultiPolygon.\n // In the past, Turf/JSTS/martinez could not handle the aggregated features,\n // so we'd iteratively union them all together. (this was slow)\n // But now mfogel/polygon-clipping handles these MultiPolygons like a boss.\n // This approach also has the benefit of removing all the internal borders and\n // simplifying the regional polygons a lot.\n if (Array.isArray(props.members)) {\n const aggregate = CountryCoder.aggregateFeature(id);\n if (aggregate) {\n const clipped = clip([aggregate as unknown as GeoJSONFeature], 'UNION');\n if (clipped) {\n feature.geometry = clipped.geometry;\n }\n }\n }\n\n // Ensure `area` property exists\n if (!props.area) {\n const area = calcArea.geometry(feature.geometry) / 1e6; // m² to km²\n props.area = Number(area.toFixed(2));\n }\n\n // Ensure `id` property exists\n feature.id = id;\n props.id = id;\n\n this._cache.set(id, feature);\n return { ...valid, feature };\n }\n\n if (this.strict) {\n throw new Error(`resolveLocation: Couldn't resolve location \"${location}\".`);\n }\n return null;\n }\n\n /**\n * Validates a locationSet and returns its stable identifier\n * @param locationSet - LocationSet with include/exclude arrays\n * @returns Validated locationSet object or null if invalid\n *\n * @example\n * ```typescript\n * // Include multiple countries\n * loco.validateLocationSet({ include: ['de', 'fr', 'it'] });\n * // => { type: 'locationset', locationSet: {...}, id: '+[Q183,Q142,Q38]' }\n *\n * // Include with exclusions\n * loco.validateLocationSet({\n * include: ['de'],\n * exclude: ['de-berlin.geojson']\n * });\n * // => { type: 'locationset', locationSet: {...}, id: '+[Q183]-[de-berlin.geojson]' }\n *\n * // Mix different location types\n * loco.validateLocationSet({\n * include: ['us', [8.67039, 49.41882], 'philly_metro.geojson']\n * });\n * ```\n */\n validateLocationSet(locationSet?: LocationSet): ValidatedLocationSet {\n locationSet = locationSet || {};\n const validator = this.validateLocation.bind(this);\n let include = (locationSet.include || []).map(validator).filter(Boolean) as ValidatedLocation[];\n const exclude = (locationSet.exclude || []).map(validator).filter(Boolean) as ValidatedLocation[];\n\n if (!include.length) {\n if (this.strict) {\n throw new Error('validateLocationSet: LocationSet includes nothing.');\n } else {\n // non-strict mode, replace an empty locationSet with one that includes \"the world\"\n locationSet.include = ['Q2'];\n include = [{ type: 'countrycoder', location: 'Q2', id: 'Q2' }];\n }\n }\n\n // Generate stable identifier\n include.sort(sortLocations);\n let id = '+[' + include.map((d) => d.id).join(',') + ']';\n if (exclude.length) {\n exclude.sort(sortLocations);\n id += '-[' + exclude.map((d) => d.id).join(',') + ']';\n }\n\n return { type: 'locationset', locationSet, id };\n }\n\n /**\n * Resolves a locationSet to a GeoJSON feature by combining included/excluded regions\n * @param locationSet - LocationSet with include/exclude arrays\n * @returns Resolved locationSet with GeoJSON feature or null if invalid\n *\n * @example\n * ```typescript\n * // Combine multiple countries into one feature\n * const benelux = loco.resolveLocationSet({\n * include: ['be', 'nl', 'lu']\n * });\n * // benelux.feature is a GeoJSON Feature with combined boundaries\n *\n * // Germany excluding Berlin\n * const germanyNoCapital = loco.resolveLocationSet({\n * include: ['de'],\n * exclude: ['de-berlin.geojson']\n * });\n * // Result is Germany with Berlin cut out\n *\n * // Complex region definition\n * const customRegion = loco.resolveLocationSet({\n * include: ['us-ca', 'us-or', 'us-wa'],\n * exclude: [[8.67039, 49.41882, 50]]\n * });\n * // West coast states minus a 50km circle\n * ```\n */\n resolveLocationSet(locationSet?: LocationSet): ResolvedLocationSet | null {\n locationSet = locationSet || {};\n const valid = this.validateLocationSet(locationSet);\n if (!valid) return null;\n\n const id = valid.id;\n\n // Return a result from cache if we can\n if (this._cache.has(id)) {\n return { ...valid, feature: this._cache.get(id)! };\n }\n\n const resolver = this.resolveLocation.bind(this);\n const includes = (locationSet.include || []).map(resolver).filter(Boolean) as ResolvedLocation[];\n const excludes = (locationSet.exclude || []).map(resolver).filter(Boolean) as ResolvedLocation[];\n\n // Return quickly if it's a single included location..\n if (includes.length === 1 && excludes.length === 0) {\n return { ...valid, feature: includes[0].feature };\n }\n\n // Calculate unions\n const includeGeoJSON = clip(includes.map((d) => d.feature), 'UNION')!;\n const excludeGeoJSON = clip(excludes.map((d) => d.feature), 'UNION');\n\n // Calculate difference, update `area` and return result\n const resultGeoJSON = excludeGeoJSON ? clip([includeGeoJSON, excludeGeoJSON], 'DIFFERENCE')! : includeGeoJSON;\n const area = calcArea.geometry(resultGeoJSON.geometry) / 1e6; // m² to km²\n resultGeoJSON.id = id;\n resultGeoJSON.properties = { id, area: Number(area.toFixed(2)) };\n\n this._cache.set(id, resultGeoJSON);\n return { ...valid, feature: resultGeoJSON };\n }\n\n /**\n * Convenience method to pretty-stringify an object\n * @param obj - Object to stringify\n * @param options - Stringify options\n * @returns Pretty-formatted JSON string\n *\n * @example\n * ```typescript\n * const result = loco.resolveLocation('de');\n * console.log(loco.stringify(result.feature));\n * // Outputs a compact, readable JSON representation of the feature\n *\n * // Custom formatting options\n * console.log(loco.stringify(result.feature, { indent: 2, maxLength: 80 }));\n * ```\n */\n stringify(obj: unknown, options?: StringifyOptions): string {\n return prettyStringify(obj, options);\n }\n}\n\n/**\n * Wraps the polyclip-ts library and returns a GeoJSON feature\n * @param features - Array of features to clip\n * @param which - Operation type (UNION or DIFFERENCE)\n * @returns Clipped GeoJSON feature or null if features array is empty\n */\nfunction clip(features: GeoJSONFeature[], which: ClipOperation): GeoJSONFeature | null {\n if (!Array.isArray(features) || !features.length) return null;\n\n const fn = { UNION: Polyclip.union, DIFFERENCE: Polyclip.difference }[which];\n const args = features.map((feature) => feature.geometry.coordinates);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const coords = (fn as any)(...args);\n\n return {\n type: 'Feature',\n properties: {} as FeatureProperties,\n geometry: {\n type: whichType(coords),\n coordinates: coords,\n } as GeoJSONGeometry,\n id: '',\n };\n\n // is this a Polygon or a MultiPolygon?\n function whichType(coords: unknown): 'Polygon' | 'MultiPolygon' {\n const a = Array.isArray(coords);\n const b = a && Array.isArray(coords[0]);\n const c = b && Array.isArray(coords[0][0]);\n const d = c && Array.isArray(coords[0][0][0]);\n return d ? 'MultiPolygon' : 'Polygon';\n }\n}\n\n/**\n * Deep clones an object using JSON serialization\n * @param obj - Object to clone\n * @returns Cloned object\n */\nfunction cloneDeep(obj: T): T {\n return JSON.parse(JSON.stringify(obj));\n}\n\n/**\n * Sorting function for locations to generate deterministic IDs\n * Sorting the location lists is ok because they end up unioned together.\n * @param a - First location\n * @param b - Second location\n * @returns Sort order\n */\nfunction sortLocations(a: ValidatedLocation, b: ValidatedLocation): number {\n const rank = { countrycoder: 1, geojson: 2, point: 3 };\n const aRank = rank[a.type];\n const bRank = rank[b.type];\n\n return aRank > bRank ? 1 : aRank < bRank ? -1 : a.id.localeCompare(b.id);\n}\n\nexport default LocationConflation;\n", "import { LocationConflation, type LocationSet, type GeoJSONFeature, type FeatureProperties, type FeatureCollection } from '@rapideditor/location-conflation';\nimport whichPolygon from 'which-polygon';\nimport calcArea from '@mapbox/geojson-area';\nimport type { Vec2 } from '../geo/vector';\n\nexport interface ObjectWithLocationSet {\n locationSetID?: string;\n locationSet: LocationSet;\n}\n\nconst _loco = new LocationConflation(); // instance of a location-conflation resolver\n\n\n/**\n * `LocationManager` maintains an internal index of all the boundaries/geofences used by iD.\n * It's used by presets, community index, background imagery, to know where in the world these things are valid.\n * These geofences should be defined by `locationSet` objects:\n *\n * let locationSet = {\n * include: [ Array of locations ],\n * exclude: [ Array of locations ]\n * };\n *\n * For more info see the location-conflation and country-coder projects, see:\n * https://github.com/rapideditor/location-conflation\n * https://github.com/rapideditor/country-coder\n */\nexport class LocationManager {\n /** A which-polygon index */\n _wp!: whichPolygon.Query;\n /** Map (id -> GeoJSON feature) */\n _resolved = new Map();\n /** Map (locationSetID -> Number area) */\n _knownLocationSets = new Map();\n /** Map (locationID -> Set(locationSetID) ) */\n _locationIncludedIn = new Map>();\n /** Map (locationID -> Set(locationSetID) ) */\n _locationExcludedIn = new Map>();\n\n /**\n * @constructor\n */\n constructor() {\n // pre-resolve the worldwide locationSet\n const world = { locationSet: { include: ['Q2'] } };\n this._resolveLocationSet(world);\n this._rebuildIndex();\n }\n\n\n /**\n * _validateLocationSet\n * Pass an Object with a `locationSet` property.\n * Validates the `locationSet` and sets a `locationSetID` property on the object.\n * To avoid so much computation we only resolve the include and exclude regions, but not the locationSet itself.\n *\n * Use `_resolveLocationSet()` instead if you need to resolve geojson of locationSet, for example to render it.\n * Note: You need to call `_rebuildIndex()` after you're all finished validating the locationSets.\n *\n * @param `obj` Object to check, it should have `locationSet` property\n */\n _validateLocationSet(obj: ObjectWithLocationSet) {\n if (obj.locationSetID) return; // work was done already\n\n try {\n let locationSet = obj.locationSet;\n if (!locationSet) {\n throw new Error('object missing locationSet property');\n }\n if (!locationSet.include) { // missing `include`, default to worldwide include\n locationSet.include = ['Q2']; // https://github.com/openstreetmap/iD/pull/8305#discussion_r662344647\n }\n\n // Validate the locationSet only\n // Resolve the include/excludes\n const locationSetID = _loco.validateLocationSet(locationSet).id;\n obj.locationSetID = locationSetID;\n if (this._knownLocationSets.has(locationSetID)) return; // seen one like this before\n\n let area = 0;\n\n // Resolve and index the 'includes'\n (locationSet.include || []).forEach(location => {\n const locationID = _loco.validateLocation(location)!.id;\n let geojson = this._resolved.get(locationID);\n\n if (!geojson) { // first time seeing a location like this\n geojson = _loco.resolveLocation(location)!.feature; // resolve to GeoJSON\n this._resolved.set(locationID, geojson);\n }\n area += geojson.properties.area;\n\n let s = this._locationIncludedIn.get(locationID);\n if (!s) {\n s = new Set();\n this._locationIncludedIn.set(locationID, s);\n }\n s.add(locationSetID);\n });\n\n // Resolve and index the 'excludes'\n (locationSet.exclude || []).forEach(location => {\n const locationID = _loco.validateLocation(location)!.id;\n let geojson = this._resolved.get(locationID);\n\n if (!geojson) { // first time seeing a location like this\n geojson = _loco.resolveLocation(location)!.feature; // resolve to GeoJSON\n this._resolved.set(locationID, geojson);\n }\n area -= geojson.properties.area;\n\n let s = this._locationExcludedIn.get(locationID);\n if (!s) {\n s = new Set();\n this._locationExcludedIn.set(locationID, s);\n }\n s.add(locationSetID);\n });\n\n this._knownLocationSets.set(locationSetID, area);\n\n } catch {\n obj.locationSet = { include: ['Q2'] }; // default worldwide\n obj.locationSetID = '+[Q2]';\n }\n }\n\n\n /**\n * _resolveLocationSet\n * Does everything that `_validateLocationSet()` does, but then \"resolves\" the locationSet into GeoJSON.\n * This step is a bit more computationally expensive, so really only needed if you intend to render the shape.\n *\n * Note: You need to call `_rebuildIndex()` after you're all finished validating the locationSets.\n *\n * @param `obj` Object to check, it should have `locationSet` property\n */\n _resolveLocationSet(obj: ObjectWithLocationSet) {\n this._validateLocationSet(obj);\n\n if (this._resolved.has(obj.locationSetID)) return; // work was done already\n\n try {\n const result = _loco.resolveLocationSet(obj.locationSet)!;\n const locationSetID = result.id;\n obj.locationSetID = locationSetID;\n\n if (!result.feature.geometry.coordinates.length || !result.feature.properties.area) {\n throw new Error(`locationSet ${locationSetID} resolves to an empty feature.`);\n }\n\n let geojson = JSON.parse(JSON.stringify(result.feature)); // deep clone\n geojson.id = locationSetID; // Important: always use the locationSet `id` (`+[Q30]`), not the feature `id` (`Q30`)\n geojson.properties.id = locationSetID;\n this._resolved.set(locationSetID, geojson);\n\n } catch {\n obj.locationSet = { include: ['Q2'] }; // default worldwide\n obj.locationSetID = '+[Q2]';\n }\n }\n\n\n /**\n * _rebuildIndex\n * Rebuilds the whichPolygon index with whatever features have been resolved into GeoJSON.\n */\n _rebuildIndex() {\n this._wp = whichPolygon({\n type: 'FeatureCollection',\n features: [...this._resolved.values()],\n });\n }\n\n\n /**\n * mergeCustomGeoJSON\n * Accepts a FeatureCollection-like object containing custom locations\n * Each feature must have a filename-like `id`, for example: `something.geojson`\n * {\n * \"type\": \"FeatureCollection\"\n * \"features\": [\n * {\n * \"type\": \"Feature\",\n * \"id\": \"philly_metro.geojson\",\n * \"properties\": { \u2026 },\n * \"geometry\": { \u2026 }\n * }\n * ]\n * }\n *\n * @param `fc` FeatureCollection-like Object containing custom locations\n */\n mergeCustomGeoJSON(fc: FeatureCollection) {\n if (!fc || fc.type !== 'FeatureCollection' || !Array.isArray(fc.features)) return;\n\n fc.features.forEach(feature => {\n feature.properties = feature.properties || {};\n let props = feature.properties;\n\n // Get `id` from either `id` or `properties`\n let id = feature.id || props.id;\n if (!id || !/^\\S+\\.geojson$/i.test(id)) return;\n\n // Ensure `id` exists and is lowercase\n id = id.toLowerCase();\n feature.id = id;\n props.id = id;\n\n // Ensure `area` property exists\n if (!props.area) {\n const area = calcArea.geometry(feature.geometry) / 1e6; // m\u00B2 to km\u00B2\n props.area = Number(area.toFixed(2));\n }\n\n _loco._cache.set(id, feature); // insert directly into LocationConflations internal cache\n });\n }\n\n\n /**\n * mergeLocationSets\n * Accepts an Array of Objects containing `locationSet` properties:\n * [\n * { id: 'preset1', locationSet: {\u2026} },\n * { id: 'preset2', locationSet: {\u2026} },\n * \u2026\n * ]\n * After validating, the Objects will be decorated with a `locationSetID` property:\n * [\n * { id: 'preset1', locationSet: {\u2026}, locationSetID: '+[Q2]' },\n * { id: 'preset2', locationSet: {\u2026}, locationSetID: '+[Q30]' },\n * \u2026\n * ]\n *\n * @param `objects` Objects to check - they should have `locationSet` property\n * @return Promise resolved true (this function used to be slow/async, now it's faster and sync)\n */\n mergeLocationSets(objects: ObjectWithLocationSet[]) {\n if (!Array.isArray(objects)) return Promise.reject('nothing to do');\n\n objects.forEach(obj => this._validateLocationSet(obj));\n this._rebuildIndex();\n return Promise.resolve(objects);\n }\n\n\n /**\n * locationSetID\n * Returns a locationSetID for a given locationSet (fallback to `+[Q2]`, world)\n * (The locationSet doesn't necessarily need to be resolved to compute its `id`)\n *\n * @param `locationSet` A locationSet Object, e.g. `{ include: ['us'] }`\n * @return String locationSetID, e.g. `+[Q30]`\n */\n locationSetID(locationSet: LocationSet) {\n let locationSetID;\n try {\n locationSetID = _loco.validateLocationSet(locationSet).id;\n } catch {\n locationSetID = '+[Q2]'; // the world\n }\n return locationSetID;\n }\n\n\n /**\n * feature\n * Returns the resolved GeoJSON feature for a given locationSetID (fallback to 'world')\n * A GeoJSON feature:\n * {\n * type: 'Feature',\n * id: '+[Q30]',\n * properties: { id: '+[Q30]', area: 21817019.17, \u2026 },\n * geometry: { \u2026 }\n * }\n *\n * @param `locationSetID` String identifier, e.g. `+[Q30]`\n * @return GeoJSON object (fallback to world)\n */\n feature(locationSetID = '+[Q2]') {\n const feature = this._resolved.get(locationSetID);\n return feature || this._resolved.get('+[Q2]');\n }\n\n\n /**\n * locationSetsAt\n * Find all the locationSets valid at the given location.\n * Results include the area (in km\u00B2) to facilitate sorting.\n *\n * Object of locationSetIDs to areas (in km\u00B2)\n * {\n * \"+[Q2]\": 511207893.3958111,\n * \"+[Q30]\": 21817019.17,\n * \"+[new_jersey.geojson]\": 22390.77,\n * \u2026\n * }\n *\n * @param `loc` `[lon,lat]` location to query, e.g. `[-74.4813, 40.7967]`\n * @return Object of locationSetIDs valid at given location\n */\n locationSetsAt(loc: Vec2) {\n const result: { [locationSetID: string]: number } = {};\n\n const hits = this._wp(loc, true) || [];\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n const thiz = this;\n\n // locationSets\n hits.forEach(prop => {\n if (prop.id[0] !== '+') return; // skip - it's a location\n const locationSetID = prop.id;\n const area = thiz._knownLocationSets.get(locationSetID);\n if (area) {\n result[locationSetID] = area;\n }\n });\n\n // locations included\n hits.forEach(prop => {\n if (prop.id[0] === '+') return; // skip - it's a locationset\n const locationID = prop.id;\n const included = thiz._locationIncludedIn.get(locationID);\n (included || []).forEach(locationSetID => {\n const area = thiz._knownLocationSets.get(locationSetID);\n if (area) {\n result[locationSetID] = area;\n }\n });\n });\n\n // locations excluded\n hits.forEach(prop => {\n if (prop.id[0] === '+') return; // skip - it's a locationset\n const locationID = prop.id;\n const excluded = thiz._locationExcludedIn.get(locationID);\n (excluded || []).forEach(locationSetID => {\n delete result[locationSetID];\n });\n });\n\n return result;\n }\n\n\n // Direct access to the location-conflation resolver\n loco() {\n return _loco;\n }\n}\n\n\nconst _sharedLocationManager = new LocationManager();\nexport { _sharedLocationManager as locationManager };\n\n", "import { t, localizer } from '../../core/localizer';\nimport { geoSphericalDistance, geoVecNormalizedDot } from '../../geo';\nimport { uiCmd } from '../cmd';\n\nexport function pointBox(loc, context) {\n var rect = context.surfaceRect();\n var point = context.curtainProjection(loc);\n return {\n left: point[0] + rect.left - 40,\n top: point[1] + rect.top - 60,\n width: 80,\n height: 90\n };\n}\n\n\nexport function pad(locOrBox, padding, context) {\n var box;\n if (locOrBox instanceof Array) {\n var rect = context.surfaceRect();\n var point = context.curtainProjection(locOrBox);\n box = {\n left: point[0] + rect.left,\n top: point[1] + rect.top\n };\n } else {\n box = locOrBox;\n }\n\n return {\n left: box.left - padding,\n top: box.top - padding,\n width: (box.width || 0) + 2 * padding,\n height: (box.width || 0) + 2 * padding\n };\n}\n\n\nexport function icon(name, svgklass, useklass) {\n return '' +\n '';\n}\n\nvar helpStringReplacements;\n\n// Returns the localized HTML element for `id` with a standardized set of icon, key, and\n// label replacements suitable for tutorials and documentation. Optionally supplemented\n// with custom `replacements`\nexport function helpHtml(id, replacements) {\n // only load these the first time\n if (!helpStringReplacements) {\n /* eslint-disable sort-keys */\n helpStringReplacements = {\n // insert icons corresponding to various UI elements\n point_icon: icon('#iD-icon-point', 'inline'),\n line_icon: icon('#iD-icon-line', 'inline'),\n area_icon: icon('#iD-icon-area', 'inline'),\n note_icon: icon('#iD-icon-note', 'inline add-note'),\n plus: icon('#iD-icon-plus', 'inline'),\n minus: icon('#iD-icon-minus', 'inline'),\n layers_icon: icon('#iD-icon-layers', 'inline'),\n data_icon: icon('#iD-icon-data', 'inline'),\n inspect: icon('#iD-icon-inspect', 'inline'),\n help_icon: icon('#iD-icon-help', 'inline'),\n undo_icon: icon(localizer.textDirection() === 'rtl' ? '#iD-icon-redo' : '#iD-icon-undo', 'inline'),\n redo_icon: icon(localizer.textDirection() === 'rtl' ? '#iD-icon-undo' : '#iD-icon-redo', 'inline'),\n save_icon: icon('#iD-icon-save', 'inline'),\n\n // operation icons\n circularize_icon: icon('#iD-operation-circularize', 'inline operation'),\n continue_icon: icon('#iD-operation-continue', 'inline operation'),\n copy_icon: icon('#iD-operation-copy', 'inline operation'),\n delete_icon: icon('#iD-operation-delete', 'inline operation'),\n disconnect_icon: icon('#iD-operation-disconnect', 'inline operation'),\n downgrade_icon: icon('#iD-operation-downgrade', 'inline operation'),\n extract_icon: icon('#iD-operation-extract', 'inline operation'),\n merge_icon: icon('#iD-operation-merge', 'inline operation'),\n move_icon: icon('#iD-operation-move', 'inline operation'),\n orthogonalize_icon: icon('#iD-operation-orthogonalize', 'inline operation'),\n paste_icon: icon('#iD-operation-paste', 'inline operation'),\n reflect_long_icon: icon('#iD-operation-reflect-long', 'inline operation'),\n reflect_short_icon: icon('#iD-operation-reflect-short', 'inline operation'),\n reverse_icon: icon('#iD-operation-reverse', 'inline operation'),\n rotate_icon: icon('#iD-operation-rotate', 'inline operation'),\n split_icon: icon('#iD-operation-split', 'inline operation'),\n straighten_icon: icon('#iD-operation-straighten', 'inline operation'),\n\n // interaction icons\n leftclick: icon('#iD-walkthrough-mouse-left', 'inline operation'),\n rightclick: icon('#iD-walkthrough-mouse-right', 'inline operation'),\n mousewheel_icon: icon('#iD-walkthrough-mousewheel', 'inline operation'),\n tap_icon: icon('#iD-walkthrough-tap', 'inline operation'),\n doubletap_icon: icon('#iD-walkthrough-doubletap', 'inline operation'),\n longpress_icon: icon('#iD-walkthrough-longpress', 'inline operation'),\n touchdrag_icon: icon('#iD-walkthrough-touchdrag', 'inline operation'),\n pinch_icon: icon('#iD-walkthrough-pinch-apart', 'inline operation'),\n\n // insert keys; may be localized and platform-dependent\n shift: uiCmd.display('\u21E7'),\n alt: uiCmd.display('\u2325'),\n return: uiCmd.display('\u21B5'),\n esc: t.html('shortcuts.key.esc'),\n space: t.html('shortcuts.key.space'),\n add_note_key: t.html('modes.add_note.key'),\n help_key: t.html('help.key'),\n shortcuts_key: t.html('shortcuts.toggle.key'),\n\n // reference localized UI labels directly so that they'll always match\n save: t.html('save.title'),\n undo: t.html('undo.title'),\n redo: t.html('redo.title'),\n upload: t.html('commit.save'),\n point: t.html('modes.add_point.title'),\n line: t.html('modes.add_line.title'),\n area: t.html('modes.add_area.title'),\n note: t.html('modes.add_note.label'),\n\n circularize: t.html('operations.circularize.title'),\n continue: t.html('operations.continue.title'),\n copy: t.html('operations.copy.title'),\n delete: t.html('operations.delete.title'),\n disconnect: t.html('operations.disconnect.title'),\n downgrade: t.html('operations.downgrade.title'),\n extract: t.html('operations.extract.title'),\n merge: t.html('operations.merge.title'),\n move: t.html('operations.move.title'),\n orthogonalize: t.html('operations.orthogonalize.title'),\n paste: t.html('operations.paste.title'),\n reflect_long: t.html('operations.reflect.title.long'),\n reflect_short: t.html('operations.reflect.title.short'),\n reverse: t.html('operations.reverse.title'),\n rotate: t.html('operations.rotate.title'),\n split: t.html('operations.split.title'),\n straighten: t.html('operations.straighten.title'),\n\n map_data: t.html('map_data.title'),\n osm_notes: t.html('map_data.layers.notes.title'),\n fields: t.html('inspector.fields'),\n tags: t.html('inspector.tags'),\n relations: t.html('inspector.relations'),\n new_relation: t.html('inspector.new_relation'),\n turn_restrictions: t.html('_tagging.presets.fields.restrictions.label'),\n background_settings: t.html('background.description'),\n imagery_offset: t.html('background.fix_misalignment'),\n start_the_walkthrough: t.html('splash.walkthrough'),\n help: t.html('help.title'),\n ok: t.html('intro.ok')\n };\n /* eslint-enable sort-keys */\n for (var key in helpStringReplacements) {\n helpStringReplacements[key] = { html: helpStringReplacements[key] };\n }\n }\n\n var reps;\n if (replacements) {\n reps = Object.assign(replacements, helpStringReplacements);\n } else {\n reps = helpStringReplacements;\n }\n\n return t.html(id, reps)\n // use keyboard key styling for shortcuts\n .replace(/\\`(.*?)\\`/g, '$1');\n}\n\n\nfunction slugify(text) {\n return text.toString().toLowerCase()\n .replace(/\\s+/g, '-') // Replace spaces with -\n .replace(/[^\\w\\-]+/g, '') // Remove all non-word chars\n .replace(/\\-\\-+/g, '-') // Replace multiple - with single -\n .replace(/^-+/, '') // Trim - from start of text\n .replace(/-+$/, ''); // Trim - from end of text\n}\n\n\n// console warning for missing walkthrough names\nexport var missingStrings = {};\nfunction checkKey(key, text) {\n if (t(key, { default: undefined}) === undefined) {\n if (missingStrings.hasOwnProperty(key)) return; // warn once\n missingStrings[key] = text;\n var missing = key + ': ' + text;\n if (typeof console !== 'undefined') console.log(missing); // eslint-disable-line\n }\n}\n\n\nexport function localize(obj) {\n var key;\n\n // Assign name if entity has one..\n var name = obj.tags && obj.tags.name;\n if (name) {\n key = 'intro.graph.name.' + slugify(name);\n obj.tags.name = t(key, { default: name });\n checkKey(key, name);\n }\n\n // Assign street name if entity has one..\n var street = obj.tags && obj.tags['addr:street'];\n if (street) {\n key = 'intro.graph.name.' + slugify(street);\n obj.tags['addr:street'] = t(key, { default: street });\n checkKey(key, street);\n\n // Add address details common across walkthrough..\n var addrTags = [\n 'block_number', 'city', 'county', 'district', 'hamlet', 'neighbourhood',\n 'postcode', 'province', 'quarter', 'state', 'subdistrict', 'suburb'\n ];\n addrTags.forEach(function(k) {\n var key = 'intro.graph.' + k;\n var tag = 'addr:' + k;\n var val = obj.tags && obj.tags[tag];\n var str = t(key, { default: val });\n\n if (str) {\n if (str.match(/^<.*>$/) !== null) {\n delete obj.tags[tag];\n } else {\n obj.tags[tag] = str;\n }\n }\n });\n }\n\n return obj;\n}\n\n\n// Used to detect squareness.. some duplicataion of code from actionOrthogonalize.\nexport function isMostlySquare(points) {\n // note: uses 15 here instead of the 12 from actionOrthogonalize because\n // actionOrthogonalize can actually straighten some larger angles as it iterates\n var threshold = 15; // degrees within right or straight\n var lowerBound = Math.cos((90 - threshold) * Math.PI / 180); // near right\n var upperBound = Math.cos(threshold * Math.PI / 180); // near straight\n\n for (var i = 0; i < points.length; i++) {\n var a = points[(i - 1 + points.length) % points.length];\n var origin = points[i];\n var b = points[(i + 1) % points.length];\n\n var dotp = geoVecNormalizedDot(a, b, origin);\n var mag = Math.abs(dotp);\n if (mag > lowerBound && mag < upperBound) {\n return false;\n }\n }\n\n return true;\n}\n\n\nexport function selectMenuItem(context, operation) {\n return context.container().select('.edit-menu .edit-menu-item-' + operation);\n}\n\n\nexport function transitionTime(point1, point2) {\n var distance = geoSphericalDistance(point1, point2);\n if (distance === 0) {\n return 0;\n } else if (distance < 80) {\n return 500;\n } else {\n return 1000;\n }\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { marked } from 'marked';\nimport { t, localizer } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { icon } from './intro/helper';\n\n\n// This currently only works with the 'restrictions' field\n// It borrows some code from uiHelp\n\nexport function uiFieldHelp(context, fieldName) {\n var fieldHelp = {};\n var _inspector = d3_select(null);\n var _wrap = d3_select(null);\n var _body = d3_select(null);\n\n var fieldHelpKeys = {\n restrictions: [\n ['about',[\n 'about',\n 'from_via_to',\n 'maxdist',\n 'maxvia'\n ]],\n ['inspecting',[\n 'about',\n 'from_shadow',\n 'allow_shadow',\n 'restrict_shadow',\n 'only_shadow',\n 'restricted',\n 'only'\n ]],\n ['modifying',[\n 'about',\n 'indicators',\n 'allow_turn',\n 'restrict_turn',\n 'only_turn'\n ]],\n ['tips',[\n 'simple',\n 'simple_example',\n 'indirect',\n 'indirect_example',\n 'indirect_noedit'\n ]]\n ]\n };\n\n var fieldHelpHeadings = {};\n\n var replacements = {\n distField: { html: t.html('restriction.controls.distance') },\n viaField: { html: t.html('restriction.controls.via') },\n fromShadow: { html: icon('#iD-turn-shadow', 'inline shadow from') },\n allowShadow: { html: icon('#iD-turn-shadow', 'inline shadow allow') },\n restrictShadow: { html: icon('#iD-turn-shadow', 'inline shadow restrict') },\n onlyShadow: { html: icon('#iD-turn-shadow', 'inline shadow only') },\n allowTurn: { html: icon('#iD-turn-yes', 'inline turn') },\n restrictTurn: { html: icon('#iD-turn-no', 'inline turn') },\n onlyTurn: { html: icon('#iD-turn-only', 'inline turn') }\n };\n\n\n // For each section, squash all the texts into a single markdown document\n var docs = fieldHelpKeys[fieldName].map(function(key) {\n var helpkey = 'help.field.' + fieldName + '.' + key[0];\n var text = key[1].reduce(function(all, part) {\n var subkey = helpkey + '.' + part;\n var depth = fieldHelpHeadings[subkey]; // is this subkey a heading?\n var hhh = depth ? Array(depth + 1).join('#') + ' ' : ''; // if so, prepend with some ##'s\n return all + hhh + t.html(subkey, replacements) + '\\n\\n';\n }, '');\n\n return {\n key: helpkey,\n title: t.html(helpkey + '.title'),\n html: marked(text.trim())\n };\n });\n\n\n function show() {\n updatePosition();\n\n _body\n .classed('hide', false)\n .style('opacity', '0')\n .transition()\n .duration(200)\n .style('opacity', '1');\n }\n\n\n function hide() {\n _body\n .classed('hide', true)\n .transition()\n .duration(200)\n .style('opacity', '0')\n .on('end', function () {\n _body.classed('hide', true);\n });\n }\n\n\n function clickHelp(index) {\n var d = docs[index];\n var tkeys = fieldHelpKeys[fieldName][index][1];\n\n _body.selectAll('.field-help-nav-item')\n .classed('active', function(d, i) { return i === index; });\n\n var content = _body.selectAll('.field-help-content')\n .html(d.html);\n\n // class the paragraphs so we can find and style them\n content.selectAll('p')\n .attr('class', function(d, i) { return tkeys[i]; });\n\n // insert special content for certain help sections\n if (d.key === 'help.field.restrictions.inspecting') {\n content\n .insert('img', 'p.from_shadow')\n .attr('class', 'field-help-image cf')\n .attr('src', context.imagePath('tr_inspect.gif'));\n\n } else if (d.key === 'help.field.restrictions.modifying') {\n content\n .insert('img', 'p.allow_turn')\n .attr('class', 'field-help-image cf')\n .attr('src', context.imagePath('tr_modify.gif'));\n }\n }\n\n\n fieldHelp.button = function(selection) {\n if (_body.empty()) return;\n\n var button = selection.selectAll('.field-help-button')\n .data([0]);\n\n // enter/update\n button.enter()\n .append('button')\n .attr('class', 'field-help-button')\n .call(svgIcon('#iD-icon-help'))\n .merge(button)\n .on('click', function (d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n if (_body.classed('hide')) {\n show();\n } else {\n hide();\n }\n });\n };\n\n\n function updatePosition() {\n var wrap = _wrap.node();\n var inspector = _inspector.node();\n var wRect = wrap.getBoundingClientRect();\n var iRect = inspector.getBoundingClientRect();\n\n _body\n .style('top', wRect.top + inspector.scrollTop - iRect.top + 'px');\n }\n\n\n fieldHelp.body = function(selection) {\n // This control expects the field to have a form-field-input-wrap div\n _wrap = selection.selectAll('.form-field-input-wrap');\n if (_wrap.empty()) return;\n\n // absolute position relative to the inspector, so it \"floats\" above the fields\n _inspector = context.container().select('.sidebar .entity-editor-pane .inspector-body');\n if (_inspector.empty()) return;\n\n _body = _inspector.selectAll('.field-help-body')\n .data([0]);\n\n var enter = _body.enter()\n .append('div')\n .attr('class', 'field-help-body hide'); // initially hidden\n\n var titleEnter = enter\n .append('div')\n .attr('class', 'field-help-title cf');\n\n titleEnter\n .append('h2')\n .attr('class', ((localizer.textDirection() === 'rtl') ? 'fr' : 'fl'))\n .call(t.append('help.field.' + fieldName + '.title'));\n\n titleEnter\n .append('button')\n .attr('class', 'fr close')\n .attr('title', t('icons.close'))\n .on('click', function(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n hide();\n })\n .call(svgIcon('#iD-icon-close'));\n\n var navEnter = enter\n .append('div')\n .attr('class', 'field-help-nav cf');\n\n var titles = docs.map(function(d) { return d.title; });\n navEnter.selectAll('.field-help-nav-item')\n .data(titles)\n .enter()\n .append('div')\n .attr('class', 'field-help-nav-item')\n .html(function(d) { return d; })\n .on('click', function(d3_event, d) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n clickHelp(titles.indexOf(d));\n });\n\n enter\n .append('div')\n .attr('class', 'field-help-content');\n\n _body = _body\n .merge(enter);\n\n clickHelp(0);\n };\n\n\n return fieldHelp;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport {\n select as d3_select\n} from 'd3-selection';\nimport { omit } from 'es-toolkit/compat';\n\nimport { utilRebind } from '../../util/rebind';\nimport { t } from '../../core/localizer';\nimport { actionReverse } from '../../actions/reverse';\nimport { svgIcon } from '../../svg/icon';\nimport { utilCheckTagDictionary } from '../../util';\nimport { osmOneWayTags } from '../../osm/tags';\n\nexport { uiFieldCheck as uiFieldDefaultCheck };\nexport { uiFieldCheck as uiFieldOnewayCheck };\n\n\nexport function uiFieldCheck(field, context) {\n var dispatch = d3_dispatch('change');\n var options = field.options;\n var values = [];\n var texts = [];\n\n var _tags;\n\n var input = d3_select(null);\n var text = d3_select(null);\n var label = d3_select(null);\n var reverser = d3_select(null);\n\n var _impliedYes;\n var _entityIDs = [];\n var _value;\n\n\n var stringsField = field.resolveReference('stringsCrossReference');\n if (!options && stringsField.options) {\n options = stringsField.options;\n }\n\n if (options) {\n for (var i in options) {\n var v = options[i];\n values.push(v === 'undefined' ? undefined : v);\n texts.push(stringsField.t.append('options.' + v, { 'default': v }));\n }\n } else {\n values = [undefined, 'yes'];\n texts = [t.append('inspector.unknown'), t.append('inspector.check.yes')];\n if (field.type !== 'defaultCheck') {\n values.push('no');\n texts.push(t.append('inspector.check.no'));\n }\n }\n\n\n // Checks tags to see whether an undefined value is \"Assumed to be Yes\"\n function checkImpliedYes() {\n _impliedYes = (field.id === 'oneway_yes');\n\n // hack: pretend `oneway` field is a `oneway_yes` field\n // where implied oneway tag exists (e.g. `junction=roundabout`) #2220, #1841\n if (field.id === 'oneway') {\n var entity = context.entity(_entityIDs[0]);\n if (entity.type === 'way' && !!utilCheckTagDictionary(entity.tags, omit(osmOneWayTags, 'oneway'))) {\n _impliedYes = true;\n texts[0] = t.append('_tagging.presets.fields.oneway_yes.options.undefined');\n }\n }\n }\n\n\n function reverserHidden() {\n if (!context.container().select('div.inspector-hover').empty()) return true;\n return !(_value === 'yes' || (_impliedYes && !_value));\n }\n\n\n function reverserSetText(selection) {\n var entity = _entityIDs.length && context.hasEntity(_entityIDs[0]);\n if (reverserHidden() || !entity) return selection;\n\n var first = entity.first();\n var last = entity.isClosed() ? entity.nodes[entity.nodes.length - 2] : entity.last();\n var pseudoDirection = first < last;\n var icon = pseudoDirection ? '#iD-icon-forward' : '#iD-icon-backward';\n\n selection.selectAll('.reverser-span')\n .text('')\n .call(t.append('inspector.check.reverser'))\n .call(svgIcon(icon, 'inline'));\n\n return selection;\n }\n\n\n var check = function(selection) {\n checkImpliedYes();\n\n label = selection.selectAll('.form-field-input-wrap')\n .data([0]);\n\n var enter = label.enter()\n .append('label')\n .attr('class', 'form-field-input-wrap form-field-input-check');\n\n enter\n .append('input')\n .property('indeterminate', field.type !== 'defaultCheck')\n .attr('type', 'checkbox')\n .attr('id', field.domId);\n\n enter\n .append('span')\n .call(texts[0])\n .attr('class', 'value');\n\n if (field.type === 'onewayCheck') {\n enter\n .append('button')\n .attr('class', 'reverser' + (reverserHidden() ? ' hide' : ''))\n .append('span')\n .attr('class', 'reverser-span');\n }\n\n label = label.merge(enter);\n input = label.selectAll('input');\n text = label.selectAll('span.value');\n\n input\n .on('click', function(d3_event) {\n d3_event.stopPropagation();\n var t = {};\n\n if (Array.isArray(_tags[field.key])) {\n if (values.indexOf('yes') !== -1) {\n t[field.key] = 'yes';\n } else {\n t[field.key] = values[0];\n }\n } else {\n t[field.key] = values[(values.indexOf(_value) + 1) % values.length];\n }\n\n // Don't cycle through `alternating` or `reversible` states - #4970\n // (They are supported as translated strings, but should not toggle with clicks)\n if (t[field.key] === 'reversible' || t[field.key] === 'alternating') {\n t[field.key] = values[0];\n }\n\n dispatch.call('change', this, t);\n });\n\n if (field.type === 'onewayCheck') {\n reverser = label.selectAll('.reverser');\n\n reverser\n .call(reverserSetText)\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n context.perform(\n function(graph) {\n for (var i in _entityIDs) {\n graph = actionReverse(_entityIDs[i])(graph);\n }\n return graph;\n },\n t('operations.reverse.annotation.line', { n: 1 })\n );\n\n // must manually revalidate since no 'change' event was called\n context.validator().validate();\n\n d3_select(this)\n .call(reverserSetText);\n });\n }\n };\n\n\n check.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return check;\n };\n\n\n check.tags = function(tags) {\n\n _tags = tags;\n\n function isChecked(val) {\n return val !== 'no' && val !== '' && val !== undefined && val !== null;\n }\n\n function textFor(val) {\n if (val === '') val = undefined;\n var index = values.indexOf(val);\n return (index !== -1 ? texts[index] : ('\"' + val + '\"'));\n }\n\n checkImpliedYes();\n\n var isMixed = Array.isArray(tags[field.key]);\n\n _value = !isMixed && tags[field.key] && tags[field.key].toLowerCase();\n\n if (field.type === 'onewayCheck' && (_value === '1' || _value === '-1')) {\n _value = 'yes';\n }\n\n input\n .property('indeterminate', isMixed || (field.type !== 'defaultCheck' && !_value))\n .property('checked', isChecked(_value));\n\n const textForValue = textFor(_value);\n text\n .text('')\n .call(isMixed\n ? t.append('inspector.multiple_values')\n : typeof textForValue === 'string' ? selection => selection.text(textForValue) : textForValue)\n .classed('mixed', isMixed);\n\n label\n .classed('set', !!_value);\n\n if (field.type === 'onewayCheck') {\n reverser\n .classed('hide', reverserHidden())\n .call(reverserSetText);\n }\n };\n\n\n check.focus = function() {\n input.node().focus();\n };\n\n return utilRebind(check, dispatch, 'on');\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg';\nimport {\n utilUnicodeCharsCount,\n utilCleanOsmString\n} from '../util';\nimport { uiPopover } from './popover';\n\n\nexport function uiLengthIndicator(maxChars) {\n var _wrap = d3_select(null);\n var _tooltip = uiPopover('tooltip max-length-warning')\n .placement('bottom')\n .hasArrow(true)\n .content(() => selection => {\n selection.text('');\n selection.call(svgIcon('#iD-icon-alert', 'inline'));\n selection.call(t.append('inspector.max_length_reached', { maxChars }));\n });\n var _silent = false;\n\n var lengthIndicator = function(selection) {\n _wrap = selection.selectAll('span.length-indicator-wrap').data([0]);\n _wrap = _wrap.enter()\n .append('span')\n .merge(_wrap)\n .classed('length-indicator-wrap', true);\n selection.call(_tooltip);\n };\n\n lengthIndicator.update = function(val) {\n const strLen = utilUnicodeCharsCount(utilCleanOsmString(val, Number.POSITIVE_INFINITY));\n\n let indicator = _wrap.selectAll('span.length-indicator')\n .data([strLen]);\n\n indicator.enter()\n .append('span')\n .merge(indicator)\n .classed('length-indicator', true)\n .classed('limit-reached', d => d > maxChars)\n .style('border-right-width', d => `${Math.abs(maxChars - d) * 2}px`)\n .style('margin-right', d => d > maxChars\n ? `${(maxChars - d) * 2}px`\n : 0)\n .style('opacity', d => d > maxChars * 0.8\n ? Math.min(1, (d / maxChars - 0.8) / (1 - 0.8))\n : 0)\n .style('pointer-events', d => d > maxChars * 0.8 ? null: 'none');\n\n if (_silent) return;\n if (strLen > maxChars) {\n _tooltip.show();\n } else {\n _tooltip.hide();\n }\n };\n\n lengthIndicator.silent = function(val) {\n if (!arguments.length) return _silent;\n _silent = val;\n return lengthIndicator;\n };\n\n return lengthIndicator;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { drag as d3_drag } from 'd3-drag';\nimport * as countryCoder from '@rapideditor/country-coder';\n\nimport { fileFetcher } from '../../core/file_fetcher';\nimport { localizer, t } from '../../core/localizer';\nimport { services } from '../../services';\nimport { uiCombobox } from '../combobox';\nimport { svgIcon } from '../../svg/icon';\n\nimport { utilKeybinding } from '../../util/keybinding';\nimport { utilArrayUniq, utilDetect, utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent, utilUnicodeCharsCount } from '../../util';\nimport { uiLengthIndicator } from '../length_indicator';\nimport { deprecatedTagValuesByKey } from '../../osm/deprecated';\nimport { osmIsoCountryKeys } from '../../osm/tags';\n\nexport {\n uiFieldCombo as uiFieldManyCombo,\n uiFieldCombo as uiFieldMultiCombo,\n uiFieldCombo as uiFieldNetworkCombo,\n uiFieldCombo as uiFieldSemiCombo,\n uiFieldCombo as uiFieldTypeCombo\n};\n\nexport function uiFieldCombo(field, context) {\n var dispatch = d3_dispatch('change');\n var _isMulti = (field.type === 'multiCombo' || field.type === 'manyCombo');\n var _isNetwork = (field.type === 'networkCombo');\n var _isSemi = (field.type === 'semiCombo');\n var _showTagInfoSuggestions = field.type !== 'manyCombo' && field.autoSuggestions !== false;\n var _allowCustomValues = field.type !== 'manyCombo' && field.customValues !== false;\n var _snake_case = (field.snake_case || (field.snake_case === undefined));\n var _combobox = uiCombobox(context, 'combo-' + field.safeid)\n .caseSensitive(field.caseSensitive)\n .minItems(1);\n var _container = d3_select(null);\n var _inputWrap = d3_select(null);\n var _input = d3_select(null);\n var _lengthIndicator = uiLengthIndicator(context.maxCharsForTagValue());\n var _comboData = [];\n var _multiData = [];\n var _entityIDs = [];\n var _tags;\n var _countryCode;\n var _staticPlaceholder;\n var _customOptions = [];\n\n // initialize deprecated tags array\n var _dataDeprecated = [];\n fileFetcher.get('deprecated')\n .then(function(d) { _dataDeprecated = d; })\n .catch(function() { /* ignore */ });\n\n\n // ensure multiCombo field.key ends with a ':'\n if (_isMulti && field.key && /[^:]$/.test(field.key)) {\n field.key += ':';\n }\n\n\n function snake(s) {\n return s.replace(/\\s+/g, '_');\n }\n\n function clean(s) {\n return s.split(';')\n .map(function(s) { return s.trim(); })\n .join(';');\n }\n\n // windows does not support emoji flags\n const showEmojiFlags = utilDetect().os !== 'win';\n\n // adds emoji flags to country dropdown and input\n function addFlagIcon(selection, name, flag) {\n if (showEmojiFlags && flag) {\n const icon = selection.insert('span', ':first-child')\n .attr('class', 'tag-value-icon');\n\n icon.append('span')\n .attr('class', 'emoji')\n .text(flag);\n }\n selection.insert('span')\n .attr('class', 'tag-value')\n .text(name);\n }\n\n // cache for language and region maps\n let languageByCodes = null;\n let regionByCodes = null;\n\n // Helper to get regionByCodes\n const buildCountry = () => {\n if (regionByCodes) return regionByCodes;\n const localeCode = localizer.localeCode();\n\n const regionNames = new Intl.DisplayNames(localeCode, { type: 'region' });\n const features = countryCoder.borders.features;\n\n regionByCodes = {};\n\n for (const feature of features) {\n const code = feature.properties.iso1A2;\n let flag = feature.properties.emojiFlag;\n // if the flag is not present like for 'FX' code, we will look for the corresponding country flag for that code\n if (!flag && features?.properties?.country) {\n flag = countryCoder.feature(feature.properties.country).properties.emojiFlag;\n }\n if (!code) continue;\n\n try {\n const name = regionNames.of(code);\n if (!name) continue;\n regionByCodes[code] = {name, flag};\n } catch {\n continue;\n }\n }\n\n return regionByCodes;\n };\n\n // Helper to get languageByCodes\n const buildLanguages = () => {\n if (languageByCodes) return languageByCodes;\n\n languageByCodes = {};\n let codes = Object.keys(localizer.languages());\n\n for (const code of codes) {\n const name = localizer.languageName(code);\n if (!name || name === code) continue;\n languageByCodes[code] = name;\n }\n\n return languageByCodes;\n };\n\n // returns the tag value for a display value\n // (for multiCombo, dval should be the key suffix, not the entire key)\n function tagValue(dval) {\n dval = clean(dval || '');\n\n var found = getOptions(true).find(function(o) {\n return o.key && clean(o.value) === dval;\n });\n if (found) return found.key;\n\n if (field.type === 'typeCombo' && !dval) {\n return 'yes';\n }\n\n return restrictTagValueSpelling(dval) || undefined;\n }\n\n function restrictTagValueSpelling(dval) {\n if (_snake_case) {\n dval = snake(dval);\n }\n\n if (!field.caseSensitive) {\n if (!(field.key === 'type' && dval === 'associatedStreet')) {\n // don't lowercase \"type=associatedStreet\" tag\n // https://github.com/openstreetmap/iD/issues/9639\n dval = dval.toLowerCase();\n }\n }\n\n return dval;\n }\n\n\n function getLabelId(field, v) {\n return field.hasTextForStringId(`options.${v}.title`)\n ? `options.${v}.title`\n : `options.${v}`;\n }\n\n\n // returns the display value for a tag value\n // (for multiCombo, tval should be the key suffix, not the entire key)\n function displayValue(tval) {\n tval = tval || '';\n // Issue #11652\n // Ignore language: key and when tval is not others\n if (field.key === 'language:' && tval !== 'others'){\n let langName = buildLanguages()[tval];\n if (langName) return langName;\n }\n\n if (osmIsoCountryKeys.has(field.key) && tval) {\n const data = buildCountry()[tval];\n if (data) return data.name;\n return tval;\n }\n\n var stringsField = field.resolveReference('stringsCrossReference');\n const labelId = getLabelId(stringsField, tval);\n if (stringsField.hasTextForStringId(labelId)) {\n return stringsField.t(labelId, { default: tval });\n }\n\n if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {\n return '';\n }\n\n return tval;\n }\n\n\n // returns function which renders the display value for a tag value\n // (for multiCombo, tval should be the key suffix, not the entire key)\n function renderValue(tval) {\n tval = tval || '';\n\n // Issue #11652\n // Ignore language: key and when tval is not others\n if (field.key === 'language:' && tval !== 'others') {\n let langName = buildLanguages()[tval];\n if (langName) return selection => selection.text(langName);\n }\n\n if (osmIsoCountryKeys.has(field.key) && tval) {\n const data = buildCountry()[tval];\n if (data) return selection => addFlagIcon(selection, data.name, data.flag);\n return selection => selection.text(tval);\n }\n\n var stringsField = field.resolveReference('stringsCrossReference');\n const labelId = getLabelId(stringsField, tval);\n if (stringsField.hasTextForStringId(labelId)) {\n return stringsField.t.append(labelId, { default: tval });\n }\n\n if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {\n tval = '';\n }\n\n return selection => selection.text(tval);\n }\n\n\n // Compute the difference between arrays of objects by `value` property\n //\n // objectDifference([{value:1}, {value:2}, {value:3}], [{value:2}])\n // > [{value:1}, {value:3}]\n //\n function objectDifference(a, b) {\n return a.filter(function(d1) {\n return !b.some(function(d2) {\n return d1.value === d2.value;\n });\n });\n }\n\n\n function initCombo(selection, attachTo) {\n if (!_allowCustomValues) {\n selection.attr('readonly', 'readonly');\n }\n\n if (_showTagInfoSuggestions && services.taginfo) {\n selection.call(_combobox.fetcher(setTaginfoValues), attachTo);\n setTaginfoValues('', setPlaceholder);\n } else {\n selection.call(_combobox, attachTo);\n setTimeout(() => setStaticValues(setPlaceholder), 0);\n }\n }\n\n function getOptions(allOptions) {\n var stringsField = field.resolveReference('stringsCrossReference');\n const localeCode = localizer.localeCode();\n // Get dropdown list for language: key via localizer instead of taginfo\n if (field.key === 'language:') {\n const langMap = buildLanguages();\n\n let options = Object.entries(langMap).map(([code, name]) => {\n return {\n key: code,\n value: name,\n title: code, // the tooltip should show the raw-tag value\n display: selection => selection.text(name)\n };\n });\n\n const localeCode = localizer.localeCode();\n\n options.sort((a, b) => {\n return a.value.localeCompare(b.value, localeCode);\n });\n\n const v = 'others';\n const labelId = getLabelId(stringsField, v);\n\n // inserting others because it does not come via _dataLanguages\n options.push({\n key: v,\n value: stringsField.t(labelId, { default: v }),\n title: stringsField.t(`options.${v}.description`, { default: v }),\n description: stringsField.hasTextForStringId(`options.${v}.description`)\n ? stringsField.t(`options.${v}.description`) : undefined,\n display: addComboboxIcons(stringsField.t.append(labelId, { default: v }), v),\n klass: stringsField.hasTextForStringId(labelId) ? '' : 'raw-option'\n });\n\n\n return options;\n }\n\n // Get dropdown list for country key\n if (osmIsoCountryKeys.has(field.key)) {\n const countryMap = buildCountry();\n\n let options = Object.entries(countryMap).map(([c, {name, flag}]) => {\n return {\n key: c,\n value: name,\n title: c, // the tooltip should show the raw-tag value\n display: selection => addFlagIcon(selection, name, flag),\n sortname: name, // store just the name without emojis to sort the names\n klass: 'has-icon' // to specifically target the emoji css\n };\n });\n\n options.sort((a, b) => {\n return a.sortname.localeCompare(b.sortname, localeCode);\n });\n\n return options;\n }\n\n if (!(field.options || stringsField.options)) return [];\n\n let options;\n if (allOptions !== true) {\n options = field.options || stringsField.options;\n } else {\n options = [].concat(field.options, stringsField.options).filter(Boolean);\n }\n const result = options.map(function(v) {\n const labelId = getLabelId(stringsField, v);\n return {\n key: v,\n value: stringsField.t(labelId, { default: v }),\n title: stringsField.t(`options.${v}.description`, { default: v }),\n description: stringsField.hasTextForStringId(`options.${v}.description`)\n ? stringsField.t(`options.${v}.description`) : undefined,\n display: addComboboxIcons(stringsField.t.append(labelId, { default: v }), v),\n klass: stringsField.hasTextForStringId(labelId) ? '' : 'raw-option'\n };\n });\n return [...result, ..._customOptions];\n }\n\n\n function hasStaticValues() {\n return getOptions().length > 0;\n }\n\n\n function setStaticValues(callback, filter) {\n _comboData = getOptions();\n\n if (filter !== undefined) {\n _comboData = _comboData.filter(filter);\n }\n\n if (!field.allowDuplicates) {\n _comboData = objectDifference(_comboData, _multiData);\n }\n _combobox.data(_comboData);\n\n // hide the caret if there are no suggestions\n _container.classed('empty-combobox', _comboData.length === 0);\n\n if (callback) callback(_comboData);\n }\n\n\n function setTaginfoValues(q, callback) {\n var queryFilter = d => d.value.toLowerCase().includes(q.toLowerCase()) || d.key.toLowerCase().includes(q.toLowerCase());\n if (hasStaticValues()) {\n setStaticValues(callback, queryFilter);\n\n // If it is language field or a country field, we don't need to request for values, we get it from getOptions\n if (field.key === 'language:') return;\n if (osmIsoCountryKeys.has(field.key)) return;\n }\n\n var stringsField = field.resolveReference('stringsCrossReference');\n var fn = _isMulti ? 'multikeys' : 'values';\n var query = (_isMulti ? field.key : '') + q;\n var hasCountryPrefix = _isNetwork && _countryCode && _countryCode.indexOf(q.toLowerCase()) === 0;\n if (hasCountryPrefix) {\n query = _countryCode + ':';\n }\n\n var params = {\n debounce: (q !== ''),\n key: field.key,\n query: query\n };\n\n if (_entityIDs.length) {\n params.geometry = context.graph().geometry(_entityIDs[0]);\n }\n\n services.taginfo[fn](params, function(err, data) {\n if (err) return;\n\n // don't show the fallback value\n data = data.filter(d =>\n field.type !== 'typeCombo' || d.value !== 'yes');\n\n // don't show misspelled values\n data = data.filter(d => {\n var value = d.value;\n if (_isMulti) {\n value = value.slice(field.key.length);\n }\n return value === restrictTagValueSpelling(value);\n });\n\n var deprecatedValues = deprecatedTagValuesByKey(_dataDeprecated)[field.key];\n if (deprecatedValues) {\n // don't suggest deprecated tag values\n data = data.filter(d =>\n !deprecatedValues.includes(d.value));\n }\n\n if (hasCountryPrefix) {\n data = data.filter(d =>\n d.value.toLowerCase().indexOf(_countryCode + ':') === 0);\n }\n\n const additionalOptions = (field.options || stringsField.options || [])\n .filter(v => !data.some(dv => dv.value === (_isMulti ? field.key + v : v)))\n .map(v => ({ value: v }));\n\n // hide the caret if there are no suggestions\n _container.classed('empty-combobox', data.length === 0);\n\n _comboData = data.concat(additionalOptions).map(function(d) {\n var v = d.value;\n if (_isMulti) v = v.replace(field.key, '');\n const labelId = getLabelId(stringsField, v);\n var isLocalizable = stringsField.hasTextForStringId(labelId);\n var label = stringsField.t(labelId, { default: v });\n return {\n key: v,\n value: label,\n title: stringsField.t(`options.${v}.description`, { default:\n isLocalizable ? label : (d.title !== label ? d.title : '') }),\n description: stringsField.hasTextForStringId(`options.${v}.description`)\n ? stringsField.t(`options.${v}.description`) : undefined,\n display: addComboboxIcons(stringsField.t.append(labelId, { default: label }), v),\n klass: isLocalizable ? '' : 'raw-option'\n };\n });\n\n _comboData = _comboData.filter(queryFilter);\n\n if (!field.allowDuplicates) {\n _comboData = objectDifference(_comboData, _multiData);\n }\n if (callback) callback(_comboData, hasStaticValues());\n });\n }\n\n // adds icons to tag values which have one\n function addComboboxIcons(disp, value) {\n const iconsField = field.resolveReference('iconsCrossReference');\n if (iconsField.icons) {\n return function(selection) {\n var span = selection\n .insert('span', ':first-child')\n .attr('class', 'tag-value-icon');\n if (iconsField.icons[value]) {\n span.call(svgIcon(`#${iconsField.icons[value]}`));\n }\n disp.call(this, selection);\n };\n }\n return disp;\n }\n\n\n function setPlaceholder(values) {\n\n if (_isMulti || _isSemi) {\n _staticPlaceholder = field.placeholder() || t('inspector.add');\n } else {\n var vals = values\n .map(function(d) { return d.value; })\n .filter(function(s) { return s.length < 20; });\n\n var placeholders = vals.length > 1 ? vals : values.map(function(d) { return d.key; });\n _staticPlaceholder = field.placeholder() || placeholders.slice(0, 3).join(', ');\n }\n\n if (!/(\u2026|\\.\\.\\.)$/.test(_staticPlaceholder)) {\n _staticPlaceholder += '\u2026';\n }\n\n var ph;\n if (!_isMulti && !_isSemi && _tags && Array.isArray(_tags[field.key])) {\n ph = t('inspector.multiple_values');\n } else {\n ph = _staticPlaceholder;\n }\n\n _container.selectAll('input')\n .attr('placeholder', ph);\n\n // Hide 'Add' button if this field uses fixed set of\n // options and they're all currently used\n var hideAdd = (!_allowCustomValues && !values.length);\n _container.selectAll('.chiplist .input-wrap')\n .style('display', hideAdd ? 'none' : null);\n }\n\n\n function change() {\n var t = {};\n var val;\n\n if (_isMulti || _isSemi) {\n var vals;\n if (_isMulti) {\n vals = [tagValue(utilGetSetValue(_input))];\n } else if (_isSemi) {\n val = tagValue(utilGetSetValue(_input)) || '';\n val = val.replace(/,/g, ';');\n vals = val.split(';');\n }\n vals = vals.filter(Boolean);\n\n if (!vals.length) return;\n\n _container.classed('active', false);\n utilGetSetValue(_input, '');\n\n if (_isMulti) {\n utilArrayUniq(vals).forEach(function(v) {\n var key = (field.key || '') + v;\n if (_tags) {\n // don't set a multicombo value to 'yes' if it already has a non-'no' value\n // e.g. `language:de=main`\n var old = _tags[key];\n if (typeof old === 'string' && old.toLowerCase() !== 'no') return;\n }\n key = context.cleanTagKey(key);\n field.keys.push(key);\n t[key] = 'yes';\n });\n\n } else if (_isSemi) {\n var arr = _multiData.map(function(d) { return d.key; });\n arr = arr.concat(vals);\n if (!field.allowDuplicates) {\n arr = utilArrayUniq(arr);\n }\n t[field.key] = context.cleanTagValue(arr.filter(Boolean).join(';'));\n }\n\n window.setTimeout(function() { _input.node().focus(); }, 10);\n\n } else {\n var rawValue = utilGetSetValue(_input);\n\n // don't override multiple values with blank string\n if (!rawValue && Array.isArray(_tags[field.key])) return;\n\n val = context.cleanTagValue(tagValue(rawValue));\n t[field.key] = val || undefined;\n }\n\n dispatch.call('change', this, t);\n }\n\n\n function removeMultikey(d3_event, d) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n var t = {};\n if (_isMulti) {\n t[d.key] = undefined;\n } else if (_isSemi) {\n let arr = _multiData.map(item => item.key);\n\n // delete the value using the index, since a value\n // may exist multiple times in the array.\n arr.splice(d.index, 1);\n\n if (!field.allowDuplicates) {\n arr = utilArrayUniq(arr);\n }\n t[field.key] = arr.length ? arr.join(';') : undefined;\n\n _lengthIndicator.update(t[field.key]);\n }\n dispatch.call('change', this, t);\n }\n\n\n function invertMultikey(d3_event, d) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n var t = {};\n if (_isMulti) {\n t[d.key] = _tags[d.key] === 'yes' ? 'no' : 'yes';\n }\n dispatch.call('change', this, t);\n }\n\n\n function combo(selection) {\n _container = selection.selectAll('.form-field-input-wrap')\n .data([0]);\n\n var type = (_isMulti || _isSemi) ? 'multicombo': 'combo';\n _container = _container.enter()\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-' + type)\n .merge(_container);\n\n if (_isMulti || _isSemi) {\n _container = _container.selectAll('.chiplist')\n .data([0]);\n\n var listClass = 'chiplist';\n\n // Use a separate line for each value in the Destinations and Via fields\n // to mimic highway exit signs\n if (field.key === 'destination' || field.key === 'via') {\n listClass += ' full-line-chips';\n }\n\n _container = _container.enter()\n .append('ul')\n .attr('class', listClass)\n .on('click', function() {\n window.setTimeout(function() { _input.node().focus(); }, 10);\n })\n .merge(_container);\n\n\n _inputWrap = _container.selectAll('.input-wrap')\n .data([0]);\n\n _inputWrap = _inputWrap.enter()\n .append('li')\n .attr('class', 'input-wrap')\n .merge(_inputWrap);\n\n // Hide 'Add' button if this field uses fixed set of\n // options and they're all currently used\n var hideAdd = (!_allowCustomValues && !_comboData.length);\n _inputWrap.style('display', hideAdd ? 'none' : null);\n\n _input = _inputWrap.selectAll('input')\n .data([0]);\n } else {\n _input = _container.selectAll('input')\n .data([0]);\n }\n\n _input = _input.enter()\n .append('input')\n .attr('type', 'text')\n .attr('dir', 'auto')\n .attr('id', field.domId)\n .call(utilNoAuto)\n .call(initCombo, _container)\n .merge(_input);\n\n if (_isSemi) {\n _inputWrap.call(_lengthIndicator);\n } else if (!_isMulti) {\n _container.call(_lengthIndicator);\n }\n\n if (_isNetwork) {\n var extent = combinedEntityExtent();\n var countryCode = extent && countryCoder.iso1A2Code(extent.center());\n _countryCode = countryCode && countryCode.toLowerCase();\n }\n\n _input\n .on('change', change)\n .on('blur', change)\n .on('input', function() {\n let val = utilGetSetValue(_input);\n updateIcon(val);\n if (_isSemi && _tags[field.key]) {\n // when adding a new value to existing ones\n val += ';' + _tags[field.key];\n }\n _lengthIndicator.update(val);\n });\n\n _input\n .on('keydown.field', function(d3_event) {\n switch (d3_event.keyCode) {\n case 13: // \u21A9 Return\n _input.node().blur(); // blurring also enters the value\n d3_event.stopPropagation();\n break;\n }\n });\n\n if (_isMulti || _isSemi) {\n _combobox\n .on('accept', function() {\n _input.node().blur();\n _input.node().focus();\n });\n\n _input\n .on('focus', function() { _container.classed('active', true); });\n }\n\n _combobox\n .on('cancel', function() {\n _input.node().blur();\n })\n .on('update', function() {\n updateIcon(utilGetSetValue(_input));\n });\n }\n\n function updateIcon(value) {\n value = tagValue(value);\n let container = _container;\n if (field.type === 'multiCombo' || field.type === 'semiCombo') {\n container = _container.select('.input-wrap');\n }\n\n // For the country emoji flags\n container.selectAll('.tag-value-icon').remove();\n if (osmIsoCountryKeys.has(field.key) && value) {\n const data = buildCountry()[value];\n\n if (data && data.flag && showEmojiFlags) {\n container.selectAll('.tag-value-icon')\n .data([value])\n .enter()\n .insert('div', 'input')\n .attr('class', 'tag-value-icon')\n .append('span')\n .attr('class', 'emoji')\n .text(data.flag);\n return;\n }\n };\n\n const iconsField = field.resolveReference('iconsCrossReference');\n if (iconsField.icons) {\n container.selectAll('.tag-value-icon').remove();\n if (iconsField.icons[value]) {\n container.selectAll('.tag-value-icon')\n .data([value])\n .enter()\n .insert('div', 'input')\n .attr('class', 'tag-value-icon')\n .call(svgIcon(`#${iconsField.icons[value]}`));\n }\n }\n }\n\n combo.tags = function(tags) {\n _tags = tags;\n var stringsField = field.resolveReference('stringsCrossReference');\n\n var isMixed = Array.isArray(tags[field.key]);\n var showsValue = value => !isMixed && value && !(field.type === 'typeCombo' && value === 'yes');\n var isRawValue = value => showsValue(value)\n && !stringsField.hasTextForStringId(`options.${value}`)\n && !stringsField.hasTextForStringId(`options.${value}.title`)\n && !(osmIsoCountryKeys.has(field.key) && value in buildCountry());\n var isKnownValue = value => showsValue(value) && !isRawValue(value);\n var isReadOnly = !_allowCustomValues;\n\n if (_isMulti || _isSemi) {\n _multiData = [];\n\n var maxLength;\n\n if (_isMulti) {\n // Build _multiData array containing keys already set..\n for (var k in tags) {\n if (field.key && k.indexOf(field.key) !== 0) continue;\n if (!field.key && field.keys.indexOf(k) === -1) continue;\n\n var v = tags[k];\n\n var suffix = field.key ? k.slice(field.key.length) : k;\n _multiData.push({\n key: k,\n value: displayValue(suffix),\n display: addComboboxIcons(renderValue(suffix), suffix),\n state: typeof v === 'string' ? v.toLowerCase() : '',\n isMixed: Array.isArray(v)\n });\n }\n\n if (field.key) {\n // Set keys for form-field modified (needed for undo and reset buttons)..\n field.keys = _multiData.map(function(d) { return d.key; });\n\n // limit the input length so it fits after prepending the key prefix\n maxLength = context.maxCharsForTagKey() - utilUnicodeCharsCount(field.key);\n } else {\n maxLength = context.maxCharsForTagKey();\n }\n\n } else if (_isSemi) {\n\n var allValues = [];\n var commonValues;\n if (Array.isArray(tags[field.key])) {\n\n tags[field.key].forEach(function(tagVal) {\n var thisVals = (tagVal || '').split(';').filter(Boolean);\n allValues = allValues.concat(thisVals);\n if (!commonValues) {\n commonValues = thisVals;\n } else {\n commonValues = commonValues.filter(value => thisVals.includes(value));\n }\n });\n allValues = allValues.filter(Boolean);\n\n } else {\n allValues = (tags[field.key] || '').split(';').filter(Boolean);\n commonValues = allValues;\n }\n\n if (!field.allowDuplicates) {\n commonValues = utilArrayUniq(commonValues);\n allValues = utilArrayUniq(allValues);\n }\n\n _multiData = allValues.map(function(v) {\n return {\n key: v,\n value: displayValue(v),\n display: addComboboxIcons(renderValue(v), v),\n isMixed: !commonValues.includes(v)\n };\n });\n\n var currLength = utilUnicodeCharsCount(commonValues.join(';'));\n\n // limit the input length to the remaining available characters\n maxLength = context.maxCharsForTagValue() - currLength;\n\n if (currLength > 0) {\n // account for the separator if a new value will be appended to existing\n maxLength -= 1;\n }\n }\n // a negative maxlength doesn't make sense\n maxLength = Math.max(0, maxLength);\n\n // Hide 'Add' button if this field is already at its character limit\n var hideAdd = maxLength <= 0 || (!_allowCustomValues && !_comboData.length);\n _container.selectAll('.chiplist .input-wrap')\n .style('display', hideAdd ? 'none' : null);\n\n var allowDragAndDrop = _isSemi // only semiCombo values are ordered\n && !Array.isArray(tags[field.key]);\n\n // Render chips\n var chips = _container.selectAll('.chip')\n .data(_multiData.map((item, index) => ({ ...item, index })));\n\n chips.exit()\n .remove();\n\n var enter = chips.enter()\n .insert('li', '.input-wrap')\n .attr('class', 'chip');\n\n enter.append('span');\n const field_buttons = enter\n .append('div')\n .attr('class', 'field_buttons');\n field_buttons\n .append('a')\n .attr('class', 'remove');\n\n chips = chips.merge(enter)\n .order()\n .classed('raw-value', function(d) {\n var k = d.key;\n if (_isMulti) k = k.replace(field.key, '');\n // Ignore the raw-value class for key language:\n if (field.key === 'language:' && localizer.languageName(k) !== k) return false;\n return !stringsField.hasTextForStringId('options.' + k);\n })\n .classed('draggable', allowDragAndDrop)\n .classed('mixed', function(d) {\n return d.isMixed;\n })\n .attr('title', function(d) {\n if (d.isMixed) {\n return t('inspector.unshared_value_tooltip');\n }\n if (!['yes', 'no'].includes(d.state)) {\n return d.state;\n }\n return null;\n })\n .classed('negated', d => d.state === 'no');\n\n if (!_isSemi) {\n chips.selectAll('input[type=checkbox]').remove();\n chips.insert('input', 'span')\n .attr('type', 'checkbox')\n .property('checked', d => d.state === 'yes')\n .property('indeterminate', d => d.isMixed || !['yes', 'no'].includes(d.state))\n .on('click', invertMultikey);\n }\n\n if (allowDragAndDrop) {\n registerDragAndDrop(chips);\n }\n\n chips.each(function(d) {\n const selection = d3_select(this);\n const text_span = selection.select('span');\n const field_buttons = selection.select('.field_buttons');\n const clean_value = d.value.trim();\n text_span.text('');\n if (!field_buttons.select('button').empty()) {\n field_buttons.select('button').remove();\n }\n if (clean_value.startsWith('https://')) {\n // create a button to open the link in a new tab\n text_span.text(clean_value);\n field_buttons.append('button')\n .call(svgIcon('#iD-icon-out-link'))\n .attr('class', 'form-field-button foreign-id-permalink')\n .attr('title', () => t('icons.visit_website'))\n .attr('aria-label', () => t('icons.visit_website'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n window.open(clean_value, '_blank');\n });\n return;\n }\n if (d.display) {\n d.display(text_span);\n return;\n }\n text_span.text(d.value);\n });\n\n chips.select('a.remove')\n .attr('href', '#')\n .on('click', removeMultikey)\n .attr('class', 'remove')\n .text('\u00D7');\n\n updateIcon('');\n } else {\n var mixedValues = isMixed && tags[field.key].map(function(val) {\n return displayValue(val);\n }).filter(Boolean);\n\n utilGetSetValue(_input, !isMixed ? displayValue(tags[field.key]) : '')\n .data([tags[field.key]])\n .classed('raw-value', isRawValue)\n .classed('known-value', isKnownValue)\n .attr('readonly', isReadOnly ? 'readonly' : undefined)\n .attr('title', isMixed ? mixedValues.join('\\n') : undefined)\n .attr('placeholder', isMixed ? t('inspector.multiple_values') : _staticPlaceholder || '')\n .classed('mixed', isMixed)\n .on('keydown.deleteCapture', function(d3_event) {\n if (isReadOnly &&\n isKnownValue(tags[field.key]) &&\n (d3_event.keyCode === utilKeybinding.keyCodes['\u232B'] ||\n d3_event.keyCode === utilKeybinding.keyCodes['\u2326'])) {\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n var t = {};\n t[field.key] = undefined;\n dispatch.call('change', this, t);\n }\n });\n\n if (!Array.isArray(tags[field.key])) {\n updateIcon(tags[field.key]);\n }\n\n if (!isMixed) {\n _lengthIndicator.update(tags[field.key]);\n }\n }\n\n const refreshStyles = () => {\n _input\n .data([tagValue(utilGetSetValue(_input))])\n .classed('raw-value', isRawValue)\n .classed('known-value', isKnownValue);\n };\n _input.on('input.refreshStyles', refreshStyles);\n _combobox.on('update.refreshStyles', refreshStyles);\n refreshStyles();\n };\n\n function registerDragAndDrop(selection) {\n\n // allow drag and drop re-ordering of chips\n var dragOrigin, targetIndex;\n selection.call(d3_drag()\n .on('start', function(d3_event) {\n dragOrigin = {\n x: d3_event.x,\n y: d3_event.y\n };\n targetIndex = null;\n })\n .on('drag', function(d3_event) {\n var x = d3_event.x - dragOrigin.x,\n y = d3_event.y - dragOrigin.y;\n\n if (!d3_select(this).classed('dragging') &&\n // don't display drag until dragging beyond a distance threshold\n Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) <= 5) return;\n\n var index = selection.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', true);\n\n targetIndex = null;\n var targetIndexOffsetTop = null;\n var draggedTagWidth = d3_select(this).node().offsetWidth;\n\n if (field.key === 'destination' || field.key === 'via') { // meaning tags are full width\n _container.selectAll('.chip')\n .style('transform', function(d2, index2) {\n var node = d3_select(this).node();\n\n if (index === index2) {\n return 'translate(' + x + 'px, ' + y + 'px)';\n // move the dragged tag up the order\n } else if (index2 > index && d3_event.y > node.offsetTop) {\n if (targetIndex === null || index2 > targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(-100%)';\n // move the dragged tag down the order\n } else if (index2 < index && d3_event.y < node.offsetTop + node.offsetHeight) {\n if (targetIndex === null || index2 < targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(100%)';\n }\n return null;\n });\n } else {\n _container.selectAll('.chip')\n .each(function(d2, index2) {\n var node = d3_select(this).node();\n\n // check the cursor is in the bounding box\n if (\n index !== index2 &&\n d3_event.x < node.offsetLeft + node.offsetWidth + 5 &&\n d3_event.x > node.offsetLeft &&\n d3_event.y < node.offsetTop + node.offsetHeight &&\n d3_event.y > node.offsetTop\n ) {\n targetIndex = index2;\n targetIndexOffsetTop = node.offsetTop;\n }\n })\n .style('transform', function(d2, index2) {\n var node = d3_select(this).node();\n\n if (index === index2) {\n return 'translate(' + x + 'px, ' + y + 'px)';\n }\n\n // only translate tags in the same row\n if (node.offsetTop === targetIndexOffsetTop) {\n if (index2 < index && index2 >= targetIndex) {\n return 'translateX(' + draggedTagWidth + 'px)';\n } else if (index2 > index && index2 <= targetIndex) {\n return 'translateX(-' + draggedTagWidth + 'px)';\n }\n }\n return null;\n });\n }\n })\n .on('end', function() {\n if (!d3_select(this).classed('dragging')) {\n return;\n }\n var index = selection.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', false);\n\n _container.selectAll('.chip')\n .style('transform', null);\n\n if (typeof targetIndex === 'number') {\n var element = _multiData[index];\n _multiData.splice(index, 1);\n _multiData.splice(targetIndex, 0, element);\n\n var t = {};\n\n if (_multiData.length) {\n t[field.key] = _multiData.map(function(element) {\n return element.key;\n }).join(';');\n } else {\n t[field.key] = undefined;\n }\n\n dispatch.call('change', this, t);\n }\n dragOrigin = undefined;\n targetIndex = undefined;\n })\n );\n }\n\n combo.setCustomOptions = (newValue) => {\n _customOptions = newValue;\n };\n\n\n combo.focus = function() {\n _input.node().focus();\n };\n\n\n combo.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return combo;\n };\n\n\n function combinedEntityExtent() {\n return _entityIDs && _entityIDs.length && utilTotalExtent(_entityIDs, context.graph());\n }\n\n\n return utilRebind(combo, dispatch, 'on');\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport { geoSphericalDistance } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect, modeSelectNote } from '../modes';\nimport { utilObjectOmit, utilQsString, utilStringQs } from '../util';\nimport { utilArrayIdentical } from '../util/array';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { localizer, t } from '../core/localizer';\nimport { prefs } from '../core/preferences';\n\n\nexport function behaviorHash(context) {\n\n // cached window.location.hash\n var _cachedHash = null;\n // allowable latitude range\n var _latitudeLimit = 90 - 1e-8;\n\n function computedHashParameters() {\n var map = context.map();\n var center = map.center();\n var zoom = map.zoom();\n var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));\n var oldParams = utilObjectOmit(utilStringQs(window.location.hash),\n ['comment', 'source', 'hashtags', 'walkthrough']\n );\n var newParams = {};\n\n delete oldParams.id;\n var selected = context.selectedIDs().filter(function(id) {\n return context.hasEntity(id);\n });\n if (selected.length) {\n newParams.id = selected.join(',');\n } else if (context.selectedNoteID()) {\n newParams.id = `note/${context.selectedNoteID()}`;\n }\n\n newParams.map = zoom.toFixed(2) +\n '/' + center[1].toFixed(precision) +\n '/' + center[0].toFixed(precision);\n\n return Object.assign(oldParams, newParams);\n }\n\n function computedHash() {\n return '#' + utilQsString(computedHashParameters(), true);\n }\n\n function computedTitle(includeChangeCount) {\n\n var baseTitle = context.documentTitleBase() || 'iD';\n var contextual;\n var changeCount;\n var titleID;\n\n var selected = context.selectedIDs().filter(function(id) {\n return context.hasEntity(id);\n });\n if (selected.length) {\n var firstLabel = utilDisplayLabel(context.entity(selected[0]), context.graph());\n if (selected.length > 1) {\n contextual = t('title.labeled_and_more', {\n labeled: firstLabel,\n count: selected.length - 1\n });\n } else {\n contextual = firstLabel;\n }\n titleID = 'context';\n }\n\n if (includeChangeCount) {\n changeCount = context.history().difference().summary().length;\n if (changeCount > 0) {\n titleID = contextual ? 'changes_context' : 'changes';\n }\n }\n\n if (titleID) {\n return t('title.format.' + titleID, {\n changes: changeCount,\n base: baseTitle,\n context: contextual\n });\n }\n\n return baseTitle;\n }\n\n function updateTitle(includeChangeCount) {\n if (!context.setsDocumentTitle()) return;\n\n var newTitle = computedTitle(includeChangeCount);\n if (document.title !== newTitle) {\n document.title = newTitle;\n }\n }\n\n function updateHashIfNeeded() {\n if (context.inIntro()) return;\n\n var latestHash = computedHash();\n if (_cachedHash !== latestHash) {\n _cachedHash = latestHash;\n\n // Update the URL hash without affecting the browser navigation stack,\n // though unavoidably creating a browser history entry\n window.history.replaceState(null, '', latestHash);\n\n // set the title we want displayed for the browser tab/window\n updateTitle(true /* includeChangeCount */);\n\n // save last used map location for future\n const q = utilStringQs(latestHash);\n if (q.map) {\n prefs('map-location', q.map);\n }\n }\n }\n\n var _throttledUpdate = throttle(updateHashIfNeeded, 500);\n var _throttledUpdateTitle = throttle(function() {\n updateTitle(true /* includeChangeCount */);\n }, 500);\n\n function hashchange() {\n // ignore spurious hashchange events\n if (window.location.hash === _cachedHash) return;\n\n _cachedHash = window.location.hash;\n\n var q = utilStringQs(_cachedHash);\n\n if (q.theme) {\n context.theme(q.theme);\n }\n\n if (q.locale && q.locale !== localizer.preferredLocaleCodes().join(',')) {\n localizer.preferredLocaleCodes(q.locale);\n context.ui().restart();\n }\n\n var mapArgs = (q.map || '').split('/').map(Number);\n if (mapArgs.length < 3 || mapArgs.some(isNaN)) {\n // replace bogus hash\n updateHashIfNeeded();\n\n } else {\n // don't update if the new hash already reflects the state of iD\n if (_cachedHash === computedHash()) return;\n\n var mode = context.mode();\n\n context.map().centerZoom([mapArgs[2], Math.min(_latitudeLimit, Math.max(-_latitudeLimit, mapArgs[1]))], mapArgs[0]);\n\n if (q.id && mode) {\n var ids = q.id.split(',').filter(function(id) {\n return context.hasEntity(id) || id.startsWith('note/');\n });\n if (ids.length && ['browse', 'select-note', 'select'].includes(mode.id)) {\n if (ids.length === 1 && ids[0].startsWith('note/')) {\n context.enter(modeSelectNote(context, ids[0]));\n } else if (!utilArrayIdentical(mode.selectedIDs(), ids)) {\n context.enter(modeSelect(context, ids));\n }\n return;\n }\n }\n\n var center = context.map().center();\n var dist = geoSphericalDistance(center, [mapArgs[2], mapArgs[1]]);\n var maxdist = 500;\n\n // Don't allow the hash location to change too much while drawing\n // This can happen if the user accidentally hit the back button. #3996\n if (mode && mode.id.match(/^draw/) !== null && dist > maxdist) {\n context.enter(modeBrowse(context));\n return;\n }\n }\n }\n\n function behavior() {\n context.map()\n .on('move.behaviorHash', _throttledUpdate);\n\n context.history()\n .on('change.behaviorHash', _throttledUpdateTitle);\n\n context\n .on('enter.behaviorHash', _throttledUpdate);\n\n d3_select(window)\n .on('hashchange.behaviorHash', hashchange);\n\n var q = utilStringQs(window.location.hash);\n\n if (q.id) {\n // targeting specific features: download, select, and zoom to them\n const selectIds = q.id.split(',');\n if (selectIds.length === 1 && selectIds[0].startsWith('note/')) {\n const noteId = selectIds[0].split('/')[1];\n context.moveToNote(noteId, !q.map);\n } else {\n context.zoomToEntities(\n // convert ids to short form id: node/123 -> n123\n selectIds.map(id => id.replace(/([nwr])[^/]*\\//, '$1')),\n !q.map);\n }\n }\n\n if (q.walkthrough === 'true') {\n behavior.startWalkthrough = true;\n }\n\n if (q.map) {\n behavior.hadLocation = true;\n } else if (!q.id && prefs('map-location')) {\n // center map at last visited map location\n const mapArgs = prefs('map-location').split('/').map(Number);\n context.map().centerZoom([mapArgs[2], Math.min(_latitudeLimit, Math.max(-_latitudeLimit, mapArgs[1]))], mapArgs[0]);\n\n updateHashIfNeeded();\n\n behavior.hadLocation = true;\n }\n\n hashchange();\n\n updateTitle(false);\n }\n\n behavior.off = function() {\n _throttledUpdate.cancel();\n _throttledUpdateTitle.cancel();\n\n context.map()\n .on('move.behaviorHash', null);\n\n context\n .on('enter.behaviorHash', null);\n\n d3_select(window)\n .on('hashchange.behaviorHash', null);\n\n window.location.hash = '';\n };\n\n return behavior;\n}\n", "export { behaviorAddWay } from './add_way';\nexport { behaviorBreathe } from './breathe';\nexport { behaviorDrag } from './drag';\nexport { behaviorDrawWay } from './draw_way';\nexport { behaviorDraw } from './draw';\nexport { behaviorEdit } from './edit';\nexport { behaviorHash } from './hash';\nexport { behaviorHover } from './hover';\nexport { behaviorLasso } from './lasso';\nexport { behaviorOperation } from './operation';\nexport { behaviorPaste } from './paste';\nexport { behaviorSelect } from './select';\n", "import { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiAccount(context) {\n const osm = context.connection();\n\n\n function updateUserDetails(selection) {\n if (!osm) return;\n\n if (!osm.authenticated()) { // logged out\n render(selection, null);\n } else {\n osm.userDetails((err, user) => {\n if (err && err.status === 401) {\n // 401 Unauthorized\n // cannot load own user data: there must be something wrong (e.g. API token was revoked)\n // -> log out to allow user to reauthenticate\n osm.logout();\n }\n render(selection, user);\n });\n }\n }\n\n\n function render(selection, user) {\n let userInfo = selection.select('.userInfo');\n let loginLogout = selection.select('.loginLogout');\n\n if (user) {\n userInfo\n .html('')\n .classed('hide', false);\n\n let userLink = userInfo\n .append('a')\n .attr('href', osm.userURL(user.display_name))\n .attr('target', '_blank');\n\n // Add user's image or placeholder\n if (user.image_url) {\n userLink.append('img')\n .attr('class', 'icon pre-text user-icon')\n .attr('src', user.image_url);\n } else {\n userLink\n .call(svgIcon('#iD-icon-avatar', 'pre-text light'));\n }\n\n // Add user name\n userLink.append('span')\n .attr('class', 'label')\n .text(user.display_name);\n\n // show \"Log Out\"\n loginLogout\n .classed('hide', false)\n .select('a')\n .text(t('logout'))\n .on('click', e => {\n e.preventDefault();\n osm.logout();\n // OAuth2's idea of \"logout\" is just to get rid of the bearer token.\n // If we try to \"login\" again, it will just grab the token again.\n // What a user probably _really_ expects is to logout of OSM so that they can switch users.\n // So, we open a popup with a \"Logout\" button. After logging out, they can login again using\n // the same popup window.\n osm.authenticate(undefined, { switchUser: true });\n });\n\n } else { // no user\n userInfo\n .html('')\n .classed('hide', true);\n\n // show \"Log In\"\n loginLogout\n .classed('hide', false)\n .select('a')\n .text(t('login'))\n .on('click', e => {\n e.preventDefault();\n osm.authenticate();\n });\n }\n }\n\n\n return function(selection) {\n if (!osm) return;\n\n selection.append('li')\n .attr('class', 'userInfo')\n .classed('hide', true);\n\n selection.append('li')\n .attr('class', 'loginLogout')\n .classed('hide', true)\n .append('a')\n .attr('href', '#');\n\n osm.on('change.account', () => updateUserDetails(selection));\n updateUserDetails(selection);\n };\n\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { t } from '../core/localizer';\n\n\nexport function uiAttribution(context) {\n let _selection = d3_select(null);\n\n\n function render(selection, data, klass) {\n let div = selection.selectAll(`.${klass}`)\n .data([0]);\n\n div = div.enter()\n .append('div')\n .attr('class', klass)\n .merge(div);\n\n\n let attributions = div.selectAll('.attribution')\n .data(data, d => d.id);\n\n attributions.exit()\n .remove();\n\n attributions = attributions.enter()\n .append('span')\n .attr('class', 'attribution')\n .each((d, i, nodes) => {\n let attribution = d3_select(nodes[i]);\n\n if (d.terms_html) {\n attribution.html(d.terms_html);\n return;\n }\n\n if (d.terms_url) {\n attribution = attribution\n .append('a')\n .attr('href', d.terms_url)\n .attr('target', '_blank');\n }\n\n const sourceID = d.id.replace(/\\./g, '');\n const terms_text = t(`imagery.${sourceID}.attribution.text`,\n { default: d.terms_text || d.id || d.name() }\n );\n\n if (d.icon && !d.overlay) {\n attribution\n .append('img')\n .attr('class', 'source-image')\n .attr('src', d.icon);\n }\n\n attribution\n .append('span')\n .attr('class', 'attribution-text')\n .text(terms_text);\n })\n .merge(attributions);\n\n\n let copyright = attributions.selectAll('.copyright-notice')\n .data(d => {\n let notice = d.copyrightNotices(context.map().zoom(), context.map().extent());\n return notice ? [notice] : [];\n });\n\n copyright.exit()\n .remove();\n\n copyright = copyright.enter()\n .append('span')\n .attr('class', 'copyright-notice')\n .merge(copyright);\n\n copyright\n .text(String);\n }\n\n\n function update() {\n let baselayer = context.background().baseLayerSource();\n _selection\n .call(render, (baselayer ? [baselayer] : []), 'base-layer-attribution');\n\n const z = context.map().zoom();\n let overlays = context.background().overlayLayerSources() || [];\n _selection\n .call(render, overlays.filter(s => s.validZoom(z)), 'overlay-layer-attribution');\n }\n\n\n return function(selection) {\n _selection = selection;\n\n context.background()\n .on('change.attribution', update);\n\n context.map()\n .on('move.attribution', throttle(update, 400, { leading: false }));\n\n update();\n };\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/index';\n\n\nexport function uiContributors(context) {\n var osm = context.connection(),\n debouncedUpdate = debounce(function() { update(); }, 1000),\n limit = 4,\n hidden = false,\n wrap = d3_select(null);\n\n\n function update() {\n if (!osm) return;\n\n var users = {},\n entities = context.history().intersects(context.map().extent());\n\n entities.forEach(function(entity) {\n if (entity && entity.user) users[entity.user] = true;\n });\n\n var u = Object.keys(users),\n subset = u.slice(0, u.length > limit ? limit - 1 : limit);\n\n wrap.html('')\n .call(svgIcon('#iD-icon-nearby', 'pre-text light'));\n\n const userList = selection => selection.selectAll()\n .data(subset)\n .enter()\n .append('a')\n .attr('class', 'user-link')\n .attr('href', d => osm.userURL(d))\n .attr('target', '_blank')\n .text(String);\n\n if (u.length > limit) {\n var othersNum = u.length - limit + 1;\n\n const count = selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', () => osm.changesetsURL(context.map().center(), context.map().zoom()))\n .text(othersNum);\n\n wrap.append('span')\n .call(t.append('contributors.truncated_list', { n: othersNum, users: userList, count }));\n\n } else {\n wrap.append('span')\n .call(t.append('contributors.list', { users: userList }));\n }\n\n if (!u.length) {\n hidden = true;\n wrap\n .transition()\n .style('opacity', 0);\n\n } else if (hidden) {\n wrap\n .transition()\n .style('opacity', 1);\n }\n }\n\n\n return function(selection) {\n if (!osm) return;\n wrap = selection;\n update();\n\n osm.on('loaded.contributors', debouncedUpdate);\n context.map().on('move.contributors', debouncedUpdate);\n };\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { geoVecAdd } from '../geo';\nimport { localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\nimport { utilRebind } from '../util/rebind';\nimport { utilHighlightEntities } from '../util/util';\nimport { utilGetDimensions } from '../util/dimensions';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiEditMenu(context) {\n var dispatch = d3_dispatch('toggled');\n\n var _menu = d3_select(null);\n var _operations = [];\n // the position the menu should be displayed relative to\n var _anchorLoc = [0, 0];\n var _anchorLocLonLat = [0, 0];\n // a string indicating how the menu was opened\n var _triggerType = '';\n\n var _vpTopMargin = 85; // viewport top margin\n var _vpBottomMargin = 45; // viewport bottom margin\n var _vpSideMargin = 35; // viewport side margin\n\n var _menuTop = false;\n var _menuHeight;\n var _menuWidth;\n\n // hardcode these values to make menu positioning easier\n var _verticalPadding = 4;\n\n // see also `.edit-menu .tooltip` CSS; include margin\n var _tooltipWidth = 210;\n\n // offset the menu slightly from the target location\n var _menuSideMargin = 10;\n\n var _tooltips = [];\n\n var editMenu = function(selection) {\n\n var isTouchMenu = _triggerType.includes('touch') || _triggerType.includes('pen');\n\n var ops = _operations.filter(function(op) {\n return !isTouchMenu || !op.mouseOnly;\n });\n\n if (!ops.length) return;\n\n _tooltips = [];\n\n // Position the menu above the anchor for stylus and finger input\n // since the mapper's hand likely obscures the screen below the anchor\n _menuTop = isTouchMenu;\n\n // Show labels for touch input since there aren't hover tooltips\n var showLabels = isTouchMenu;\n\n var buttonHeight = showLabels ? 32 : 34;\n if (showLabels) {\n // Get a general idea of the width based on the length of the label\n _menuWidth = 52 + Math.min(120, 6 * Math.max.apply(Math, ops.map(function(op) {\n return op.title.length;\n })));\n } else {\n _menuWidth = 44;\n }\n\n _menuHeight = _verticalPadding * 2 + ops.length * buttonHeight;\n\n _menu = selection\n .append('div')\n .attr('class', 'edit-menu')\n .classed('touch-menu', isTouchMenu)\n .style('padding', _verticalPadding + 'px 0');\n\n var buttons = _menu.selectAll('.edit-menu-item')\n .data(ops);\n\n // enter\n var buttonsEnter = buttons.enter()\n .append('button')\n .attr('class', function (d) { return 'edit-menu-item edit-menu-item-' + d.id; })\n .style('height', buttonHeight + 'px')\n .on('click', click)\n // don't listen for `mouseup` because we only care about non-mouse pointer types\n .on('pointerup', pointerup)\n .on('pointerdown mousedown', function pointerdown(d3_event) {\n // don't let button presses also act as map input - #1869\n d3_event.stopPropagation();\n })\n .on('mouseenter.highlight', function(d3_event, d) {\n if (d3_select(this).classed('disabled')) return;\n\n if (d.relatedEntityIds) {\n utilHighlightEntities(d.relatedEntityIds(), true, context);\n }\n\n if (d.getAuxiliaryGeometry) {\n drawAuxiliaryGeometry(context, d.getAuxiliaryGeometry());\n }\n })\n .on('mouseleave.highlight', function(d3_event, d) {\n if (d.relatedEntityIds) {\n utilHighlightEntities(d.relatedEntityIds(), false, context);\n }\n\n if (d.getAuxiliaryGeometry) {\n drawAuxiliaryGeometry(context, []);\n }\n });\n\n buttonsEnter.each(function(d) {\n var tooltip = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .heading(() => d.title)\n .title(d.tooltip)\n .keys([d.keys[0]]);\n\n _tooltips.push(tooltip);\n\n d3_select(this)\n .call(tooltip)\n .append('div')\n .attr('class', 'icon-wrap')\n .call(svgIcon(d.icon && d.icon() || '#iD-operation-' + d.id, 'operation'));\n });\n\n if (showLabels) {\n buttonsEnter.append('span')\n .attr('class', 'label')\n .each(function(d) {\n d3_select(this).call(d.title);\n });\n }\n\n // update\n buttonsEnter\n .merge(buttons)\n .classed('disabled', function(d) { return d.disabled(); });\n\n updatePosition();\n\n var initialScale = context.projection.scale();\n context.map()\n .on('move.edit-menu', function() {\n if (initialScale !== context.projection.scale()) {\n editMenu.close();\n }\n })\n .on('drawn.edit-menu', function(info) {\n if (info.full) updatePosition();\n });\n\n var lastPointerUpType;\n // `pointerup` is always called before `click`\n function pointerup(d3_event) {\n lastPointerUpType = d3_event.pointerType;\n }\n\n function click(d3_event, operation) {\n d3_event.stopPropagation();\n\n if (operation.relatedEntityIds) {\n utilHighlightEntities(operation.relatedEntityIds(), false, context);\n }\n\n if (operation.disabled()) {\n if (lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen') {\n // there are no tooltips for touch interactions so flash feedback instead\n context.ui().flash\n .duration(4000)\n .iconName('#iD-operation-' + operation.id)\n .iconClass('operation disabled')\n .label(operation.tooltip())();\n }\n } else {\n if (lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen') {\n context.ui().flash\n .duration(2000)\n .iconName('#iD-operation-' + operation.id)\n .iconClass('operation')\n .label(operation.annotation() || operation.title)();\n }\n\n operation();\n editMenu.close();\n }\n lastPointerUpType = null;\n }\n\n dispatch.call('toggled', this, true);\n };\n\n function updatePosition() {\n\n if (!_menu || _menu.empty()) return;\n\n var anchorLoc = context.projection(_anchorLocLonLat);\n\n var viewport = context.surfaceRect();\n\n if (anchorLoc[0] < 0 ||\n anchorLoc[0] > viewport.width ||\n anchorLoc[1] < 0 ||\n anchorLoc[1] > viewport.height) {\n // close the menu if it's gone offscreen\n\n editMenu.close();\n return;\n }\n\n var menuLeft = displayOnLeft(viewport);\n\n var offset = [0, 0];\n\n offset[0] = menuLeft ? -1 * (_menuSideMargin + _menuWidth) : _menuSideMargin;\n\n if (_menuTop) {\n if (anchorLoc[1] - _menuHeight < _vpTopMargin) {\n // menu is near top viewport edge, shift downward\n offset[1] = -anchorLoc[1] + _vpTopMargin;\n } else {\n offset[1] = -_menuHeight;\n }\n } else {\n if (anchorLoc[1] + _menuHeight > (viewport.height - _vpBottomMargin)) {\n // menu is near bottom viewport edge, shift upwards\n offset[1] = -anchorLoc[1] - _menuHeight + viewport.height - _vpBottomMargin;\n } else {\n offset[1] = 0;\n }\n }\n\n var origin = geoVecAdd(anchorLoc, offset);\n // repositioning the menu to account for the top menu height\n var _verticalOffset = parseFloat(utilGetDimensions(d3_select('.top-toolbar-wrap'))[1]);\n origin[1] -= _verticalOffset;\n\n _menu\n .style('left', origin[0] + 'px')\n .style('top', origin[1] + 'px');\n\n var tooltipSide = tooltipPosition(viewport, menuLeft);\n _tooltips.forEach(function(tooltip) {\n tooltip.placement(tooltipSide);\n });\n\n function displayOnLeft(viewport) {\n if (localizer.textDirection() === 'ltr') {\n if ((anchorLoc[0] + _menuSideMargin + _menuWidth) > (viewport.width - _vpSideMargin)) {\n // right menu would be too close to the right viewport edge, go left\n return true;\n }\n // prefer right menu\n return false;\n\n } else { // rtl\n if ((anchorLoc[0] - _menuSideMargin - _menuWidth) < _vpSideMargin) {\n // left menu would be too close to the left viewport edge, go right\n return false;\n }\n // prefer left menu\n return true;\n }\n }\n\n function tooltipPosition(viewport, menuLeft) {\n if (localizer.textDirection() === 'ltr') {\n if (menuLeft) {\n // if there's not room for a right-side menu then there definitely\n // isn't room for right-side tooltips\n return 'left';\n }\n if ((anchorLoc[0] + _menuSideMargin + _menuWidth + _tooltipWidth) > (viewport.width - _vpSideMargin)) {\n // right tooltips would be too close to the right viewport edge, go left\n return 'left';\n }\n // prefer right tooltips\n return 'right';\n\n } else { // rtl\n if (!menuLeft) {\n return 'right';\n }\n if ((anchorLoc[0] - _menuSideMargin - _menuWidth - _tooltipWidth) < _vpSideMargin) {\n // left tooltips would be too close to the left viewport edge, go right\n return 'right';\n }\n // prefer left tooltips\n return 'left';\n }\n }\n }\n\n editMenu.close = function () {\n\n context.map()\n .on('move.edit-menu', null)\n .on('drawn.edit-menu', null);\n\n _menu.remove();\n _tooltips = [];\n\n // Clean up any auxiliary overlays\n drawAuxiliaryGeometry(context, []);\n\n dispatch.call('toggled', this, false);\n };\n\n editMenu.anchorLoc = function(val) {\n if (!arguments.length) return _anchorLoc;\n _anchorLoc = val;\n _anchorLocLonLat = context.projection.invert(_anchorLoc);\n return editMenu;\n };\n\n editMenu.triggerType = function(val) {\n if (!arguments.length) return _triggerType;\n _triggerType = val;\n return editMenu;\n };\n\n editMenu.operations = function(val) {\n if (!arguments.length) return _operations;\n _operations = val;\n return editMenu;\n };\n\n return utilRebind(editMenu, dispatch, 'on');\n}\n\n\n// Helper function to draw/remove reflect axis overlay\nfunction drawAuxiliaryGeometry(context, d) {\n const surface = context.surface();\n // Append to the OSM data layer to be in the same coordinate space as map features\n const container = surface.selectAll('.data-layer.osm .auxiliary');\n const paths = container.selectAll('path')\n .data(d, d => d.id);\n\n paths.exit().remove();\n const enter = paths.enter()\n .append('path');\n\n enter.merge(paths)\n .attr('class', d => d.klass)\n .attr('d', d => d.path);\n}\n", "import { t } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\n\nexport function uiFeatureInfo(context) {\n function update(selection) {\n var features = context.features();\n var stats = features.stats();\n var dateMatchCount = features.dateMatchCount();\n var count = 0;\n var hiddenList = features.hidden().map(function(k) {\n if (stats[k]) {\n count += stats[k];\n return t.append('inspector.title_count', {\n title: t('feature.' + k + '.description'),\n count: stats[k]\n });\n }\n return null;\n }).filter(Boolean);\n count += dateMatchCount;\n\n selection.text('');\n\n if (hiddenList.length || dateMatchCount > 0) {\n var tooltipBehavior = uiTooltip()\n .placement('top')\n .title(function() {\n return selection => {\n hiddenList.forEach(hiddenFeature => {\n selection.append('div').call(hiddenFeature);\n });\n };\n });\n\n selection.append('a')\n .attr('class', 'chip')\n .attr('href', '#')\n .call(t.append('feature_info.hidden_warning', { count: count }))\n .call(tooltipBehavior)\n .on('click', function(d3_event) {\n tooltipBehavior.hide();\n d3_event.preventDefault();\n // open the Map Data pane\n context.ui().togglePanes(context.container().select('.map-panes .map-data-pane'));\n });\n }\n\n selection\n .classed('hide', !hiddenList.length && !dateMatchCount);\n }\n\n\n return function(selection) {\n update(selection);\n\n context.features().on('change.feature_info', function() {\n update(selection);\n });\n };\n}\n", "import { timeout as d3_timeout } from 'd3-timer';\n\nexport function uiFlash(context) {\n var _flashTimer;\n\n var _duration = 2000;\n var _iconName = '#iD-icon-no';\n var _iconClass = 'disabled';\n var _label = s => s.text('');\n\n function flash() {\n if (_flashTimer) {\n _flashTimer.stop();\n }\n\n context.container().select('.main-footer-wrap')\n .classed('footer-hide', true)\n .classed('footer-show', false);\n context.container().select('.flash-wrap')\n .classed('footer-hide', false)\n .classed('footer-show', true);\n\n var content = context.container().select('.flash-wrap').selectAll('.flash-content')\n .data([0]);\n\n // Enter\n var contentEnter = content.enter()\n .append('div')\n .attr('class', 'flash-content');\n\n var iconEnter = contentEnter\n .append('svg')\n .attr('class', 'flash-icon icon')\n .append('g')\n .attr('transform', 'translate(10,10)');\n\n iconEnter\n .append('circle')\n .attr('r', 9);\n\n iconEnter\n .append('use')\n .attr('transform', 'translate(-7,-7)')\n .attr('width', '14')\n .attr('height', '14');\n\n contentEnter\n .append('div')\n .attr('class', 'flash-text');\n\n\n // Update\n content = content\n .merge(contentEnter);\n\n content\n .selectAll('.flash-icon')\n .attr('class', 'icon flash-icon ' + (_iconClass || ''));\n\n content\n .selectAll('.flash-icon use')\n .attr('xlink:href', _iconName);\n\n content\n .selectAll('.flash-text')\n .attr('class', 'flash-text')\n .call(_label);\n\n\n _flashTimer = d3_timeout(function() {\n _flashTimer = null;\n context.container().select('.main-footer-wrap')\n .classed('footer-hide', false)\n .classed('footer-show', true);\n context.container().select('.flash-wrap')\n .classed('footer-hide', true)\n .classed('footer-show', false);\n }, _duration);\n\n return content;\n }\n\n\n flash.duration = function(_) {\n if (!arguments.length) return _duration;\n _duration = _;\n return flash;\n };\n\n flash.label = function(_) {\n if (!arguments.length) return _label;\n if (typeof _ !== 'function') {\n _label = selection => selection.text(_);\n } else {\n _label = selection => selection.text('').call(_);\n }\n return flash;\n };\n\n flash.iconName = function(_) {\n if (!arguments.length) return _iconName;\n _iconName = _;\n return flash;\n };\n\n flash.iconClass = function(_) {\n if (!arguments.length) return _iconClass;\n _iconClass = _;\n return flash;\n };\n\n return flash;\n}\n", "import { uiCmd } from './cmd';\nimport { utilDetect } from '../util/detect';\n\nexport function uiFullScreen(context) {\n var element = context.container().node();\n // var button = d3_select(null);\n\n\n function getFullScreenFn() {\n if (element.requestFullscreen) {\n return element.requestFullscreen;\n } else if (element.msRequestFullscreen) {\n return element.msRequestFullscreen;\n } else if (element.mozRequestFullScreen) {\n return element.mozRequestFullScreen;\n } else if (element.webkitRequestFullscreen) {\n return element.webkitRequestFullscreen;\n }\n }\n\n\n function getExitFullScreenFn() {\n if (document.exitFullscreen) {\n return document.exitFullscreen;\n } else if (document.msExitFullscreen) {\n return document.msExitFullscreen;\n } else if (document.mozCancelFullScreen) {\n return document.mozCancelFullScreen;\n } else if (document.webkitExitFullscreen) {\n return document.webkitExitFullscreen;\n }\n }\n\n\n function isFullScreen() {\n return document.fullscreenElement ||\n document.mozFullScreenElement ||\n document.webkitFullscreenElement ||\n document.msFullscreenElement;\n }\n\n\n function isSupported() {\n return !!getFullScreenFn();\n }\n\n\n function fullScreen(d3_event) {\n d3_event.preventDefault();\n if (!isFullScreen()) {\n // button.classed('active', true);\n getFullScreenFn().apply(element);\n } else {\n // button.classed('active', false);\n getExitFullScreenFn().apply(document);\n }\n }\n\n\n return function() { // selection) {\n if (!isSupported()) return;\n\n // button = selection.append('button')\n // .attr('title', t('full_screen'))\n // .on('click', fullScreen)\n // .call(tooltip);\n\n // button.append('span')\n // .attr('class', 'icon full-screen');\n\n var detected = utilDetect();\n var keys = (detected.os === 'mac' ? [uiCmd('\u2303\u2318F'), 'f11'] : ['f11']);\n context.keybinding().on(keys, fullScreen);\n };\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t, localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\nimport { geoExtent } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\nimport { uiLoading } from './loading';\n\nexport function uiGeolocate(context) {\n var _geolocationOptions = {\n // prioritize speed and power usage over precision\n enableHighAccuracy: false,\n // don't hang indefinitely getting the location\n timeout: 6000 // 6sec\n };\n var _locating = uiLoading(context).message(t.addOrUpdate('geolocate.locating')).blocking(true);\n var _layer = context.layers().layer('geolocate');\n var _position;\n var _extent;\n var _timeoutID;\n var _button = d3_select(null);\n\n function click() {\n if (context.inIntro()) return;\n if (!_layer.enabled() && !_locating.isShown()) {\n\n // This timeout ensures that we still call finish() even if\n // the user declines to share their location in Firefox\n _timeoutID = setTimeout(error, 10000 /* 10sec */ );\n\n context.container().call(_locating);\n // get the latest position even if we already have one\n navigator.geolocation.getCurrentPosition(success, error, _geolocationOptions);\n } else {\n _locating.close();\n _layer.enabled(null, false);\n updateButtonState();\n }\n }\n\n function zoomTo() {\n context.enter(modeBrowse(context));\n\n var map = context.map();\n _layer.enabled(_position, true);\n updateButtonState();\n map.centerZoomEase(_extent.center(), Math.min(20, map.extentZoom(_extent)));\n }\n\n function success(geolocation) {\n _position = geolocation;\n var coords = _position.coords;\n _extent = geoExtent([coords.longitude, coords.latitude]).padByMeters(coords.accuracy);\n zoomTo();\n finish();\n }\n\n function error() {\n if (_position) {\n // use the position from a previous call if we have one\n zoomTo();\n } else {\n context.ui().flash\n .label(t.append('geolocate.location_unavailable'))\n .iconName('#iD-icon-geolocate')();\n }\n\n finish();\n }\n\n function finish() {\n _locating.close(); // unblock ui\n if (_timeoutID) { clearTimeout(_timeoutID); }\n _timeoutID = undefined;\n }\n\n function updateButtonState() {\n _button.classed('active', _layer.enabled());\n _button.attr('aria-pressed', _layer.enabled());\n }\n\n return function(selection) {\n if (!navigator.geolocation || !navigator.geolocation.getCurrentPosition) return;\n\n _button = selection\n .append('button')\n .on('click', click)\n .attr('aria-pressed', false)\n .call(svgIcon('#iD-icon-geolocate', 'light'))\n .call(uiTooltip()\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(() => t.append('geolocate.title'))\n );\n };\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { t } from '../../core/localizer';\n\n\nexport function uiPanelBackground(context) {\n const background = context.background();\n let _currSource = null;\n let _metadata = {};\n const _metadataKeys = [\n 'zoom', 'vintage', 'source', 'description', 'resolution', 'accuracy'\n ];\n\n var debouncedRedraw = debounce(redraw, 250);\n\n function redraw(selection) {\n var source = background.baseLayerSource();\n if (!source) return;\n\n if (_currSource?.id !== source.id) {\n _currSource = source;\n _metadata = {};\n }\n\n selection.text('');\n\n var list = selection\n .append('ul')\n .attr('class', 'background-info');\n\n list\n .append('li')\n .call(_currSource.label());\n\n _metadataKeys.forEach(function(k) {\n list\n .append('li')\n .attr('class', 'background-info-list-' + k)\n .classed('hide', !_metadata[k])\n .call(t.append('info_panels.background.' + k, { suffix: ':' }))\n .append('span')\n .attr('class', 'background-info-span-' + k)\n .text(_metadata[k]);\n });\n\n debouncedGetMetadata(selection);\n\n var toggleTiles = context.getDebug('tile') ? 'hide_tiles' : 'show_tiles';\n\n selection\n .append('a')\n .call(t.append('info_panels.background.' + toggleTiles))\n .attr('href', '#')\n .attr('class', 'button button-toggle-tiles')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.setDebug('tile', !context.getDebug('tile'));\n selection.call(redraw);\n });\n }\n\n\n var debouncedGetMetadata = debounce(getMetadata, 250);\n\n function getMetadata(selection) {\n var tile = context.container().select('.layer-background img.tile-center'); // tile near viewport center\n if (tile.empty()) return;\n\n var sourceId = _currSource.id;\n var d = tile.datum();\n var zoom = (d && d.length >= 3 && d[2]) || Math.floor(context.map().zoom());\n var center = context.map().center();\n\n // update zoom\n _metadata.zoom = String(zoom);\n selection.selectAll('.background-info-list-zoom')\n .classed('hide', false)\n .selectAll('.background-info-span-zoom')\n .text(_metadata.zoom);\n\n if (!d || !d.length >= 3) return;\n\n background.baseLayerSource().getMetadata(center, d, function(err, result) {\n if (err || _currSource.id !== sourceId) return;\n\n // update vintage\n var vintage = result.vintage;\n _metadata.vintage = (vintage && vintage.range) || t('info_panels.background.unknown');\n selection.selectAll('.background-info-list-vintage')\n .classed('hide', false)\n .selectAll('.background-info-span-vintage')\n .text(_metadata.vintage);\n\n // update other _metadata\n _metadataKeys.forEach(function(k) {\n if (k === 'zoom' || k === 'vintage') return; // done already\n var val = result[k];\n _metadata[k] = val;\n selection.selectAll('.background-info-list-' + k)\n .classed('hide', !val)\n .selectAll('.background-info-span-' + k)\n .text(val);\n });\n });\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.map()\n .on('drawn.info-background', function() {\n selection.call(debouncedRedraw);\n })\n .on('move.info-background', function() {\n selection.call(debouncedGetMetadata);\n });\n\n };\n\n panel.off = function() {\n context.map()\n .on('drawn.info-background', null)\n .on('move.info-background', null);\n };\n\n panel.id = 'background';\n panel.label = t.append('info_panels.background.title');\n panel.key = t('info_panels.background.key');\n\n\n return panel;\n}\n", "import { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\n\n\nexport function uiPanelHistory(context) {\n var osm;\n\n function displayTimestamp(timestamp) {\n if (!timestamp) return t('info_panels.history.unknown');\n var options = {\n day: 'numeric', month: 'short', year: 'numeric',\n hour: 'numeric', minute: 'numeric', second: 'numeric'\n };\n var d = new Date(timestamp);\n if (isNaN(d.getTime())) return t('info_panels.history.unknown');\n return d.toLocaleString(localizer.localeCode(), options);\n }\n\n\n function displayUser(selection, userName) {\n if (!userName) {\n selection\n .append('span')\n .call(t.append('info_panels.history.unknown'));\n return;\n }\n\n selection\n .append('span')\n .attr('class', 'user-name')\n .text(userName);\n\n var links = selection\n .append('div')\n .attr('class', 'links');\n\n if (osm) {\n links\n .append('a')\n .attr('class', 'user-osm-link')\n .attr('href', osm.userURL(userName))\n .attr('target', '_blank')\n .call(t.append('info_panels.history.profile_link'));\n }\n }\n\n\n function displayChangeset(selection, changeset) {\n if (!changeset) {\n selection\n .append('span')\n .call(t.append('info_panels.history.unknown'));\n return;\n }\n\n selection\n .append('span')\n .attr('class', 'changeset-id')\n .text(changeset);\n\n var links = selection\n .append('div')\n .attr('class', 'links');\n\n if (osm) {\n links\n .append('a')\n .attr('class', 'changeset-osm-link')\n .attr('href', osm.changesetURL(changeset))\n .attr('target', '_blank')\n .call(t.append('info_panels.history.changeset_link'));\n }\n\n links\n .append('a')\n .attr('class', 'changeset-osmcha-link')\n .attr('href', 'https://osmcha.openhistoricalmap.org/changesets/' + changeset)\n .attr('target', '_blank')\n .text('OSMCha');\n }\n\n\n function redraw(selection) {\n var selectedNoteID = context.selectedNoteID();\n osm = context.connection();\n var selected, note, entity;\n if (selectedNoteID && osm) { // selected 1 note\n selected = [ t.append('note.note', { suffix: ' ' + selectedNoteID }) ];\n note = osm.getNote(selectedNoteID);\n } else { // selected 1..n entities\n selected = context.selectedIDs()\n .filter(function(e) { return context.hasEntity(e); });\n if (selected.length) {\n entity = context.entity(selected[0]);\n }\n }\n\n var singular = selected.length === 1 ? selected[0] : null;\n\n selection.text('');\n\n const heading = selection\n .append('h4')\n .attr('class', 'history-heading');\n\n if (singular) {\n if (typeof singular === 'function') {\n heading.call(singular);\n } else {\n heading.text(singular);\n }\n } else {\n heading.call(t.append('info_panels.selected', { n: selected.length }));\n }\n\n if (!singular) return;\n\n if (entity) {\n selection.call(redrawEntity, entity);\n } else if (note) {\n selection.call(redrawNote, note);\n }\n }\n\n\n function redrawNote(selection, note) {\n if (!note || note.isNew()) {\n selection\n .append('div')\n .call(t.append('info_panels.history.note_no_history'));\n return;\n }\n\n var list = selection\n .append('ul');\n\n list\n .append('li')\n .call(t.append('info_panels.history.note_comments', { suffix: ':' }))\n .append('span')\n .text(note.comments.length);\n\n if (note.comments.length) {\n list\n .append('li')\n .call(t.append('info_panels.history.note_created_date', { suffix: ':' }))\n .append('span')\n .text(displayTimestamp(note.comments[0].date));\n\n list\n .append('li')\n .call(t.append('info_panels.history.note_created_user', { suffix: ':' }))\n .call(displayUser, note.comments[0].user);\n }\n\n if (osm) {\n selection\n .append('a')\n .attr('class', 'view-history-on-osm')\n .attr('target', '_blank')\n .attr('href', osm.noteURL(note))\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append('info_panels.history.note_link_text'));\n }\n }\n\n\n function redrawEntity(selection, entity) {\n if (!entity || entity.isNew()) {\n selection\n .append('div')\n .call(t.append('info_panels.history.no_history'));\n return;\n }\n\n var links = selection\n .append('div')\n .attr('class', 'links');\n\n if (osm) {\n links\n .append('a')\n .attr('class', 'view-history-on-osm')\n .attr('href', osm.historyURL(entity))\n .attr('target', '_blank')\n .call(t.append('info_panels.history.history_link'));\n }\n\n var list = selection\n .append('ul');\n\n list\n .append('li')\n .call(t.append('info_panels.history.version', { suffix: ':' }))\n .append('span')\n .text(entity.version);\n\n list\n .append('li')\n .call(t.append('info_panels.history.last_edit', { suffix: ':' }))\n .append('span')\n .text(displayTimestamp(entity.timestamp));\n\n list\n .append('li')\n .call(t.append('info_panels.history.edited_by', { suffix: ':' }))\n .call(displayUser, entity.user);\n\n list\n .append('li')\n .call(t.append('info_panels.history.changeset', { suffix: ':' }))\n .call(displayChangeset, entity.changeset);\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.map()\n .on('drawn.info-history', function() {\n selection.call(redraw);\n });\n\n context\n .on('enter.info-history', function() {\n selection.call(redraw);\n });\n };\n\n panel.off = function() {\n context.map().on('drawn.info-history', null);\n context.on('enter.info-history', null);\n };\n\n panel.id = 'history';\n panel.label = t.append('info_panels.history.title');\n panel.key = t('info_panels.history.key');\n\n\n return panel;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { decimalCoordinatePair, dmsCoordinatePair } from '../../util/units';\nimport { t } from '../../core/localizer';\nimport { services } from '../../services';\n\n\nexport function uiPanelLocation(context) {\n var currLocation = '';\n\n\n function redraw(selection) {\n selection.html('');\n\n var list = selection\n .append('ul');\n\n // Mouse coordinates\n var coord = context.map().mouseCoordinates();\n if (coord.some(isNaN)) {\n coord = context.map().center();\n }\n\n list\n .append('li')\n .text(dmsCoordinatePair(coord))\n .append('li')\n .text(decimalCoordinatePair(coord));\n\n // Location Info\n selection\n .append('div')\n .attr('class', 'location-info')\n .text(currLocation || ' ');\n\n debouncedGetLocation(selection, coord);\n }\n\n\n var debouncedGetLocation = debounce(getLocation, 250);\n function getLocation(selection, coord) {\n if (!services.geocoder) {\n currLocation = t('info_panels.location.unknown_location');\n selection.selectAll('.location-info')\n .text(currLocation);\n } else {\n services.geocoder.reverse(coord, function(err, result) {\n currLocation = result ? result.display_name : t('info_panels.location.unknown_location');\n selection.selectAll('.location-info')\n .text(currLocation);\n });\n }\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.surface()\n .on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'move.info-location', function() {\n selection.call(redraw);\n });\n };\n\n panel.off = function() {\n context.surface()\n .on('.info-location', null);\n };\n\n panel.id = 'location';\n panel.label = t.append('info_panels.location.title');\n panel.key = t('info_panels.location.key');\n\n\n return panel;\n}\n", "import {\n geoLength as d3_geoLength,\n geoPath as d3_geoPath\n} from 'd3-geo';\n\nimport { t, localizer } from '../../core/localizer';\nimport { displayArea, displayLength, decimalCoordinatePair, dmsCoordinatePair } from '../../util/units';\nimport { geoExtent, geoSphericalDistance } from '../../geo';\nimport { services } from '../../services';\nimport { utilGetAllNodes } from '../../util';\n\nexport function uiPanelMeasurement(context) {\n\n function radiansToMeters(r) {\n // using WGS84 authalic radius (6371007.1809 m)\n return r * 6371007.1809;\n }\n\n function steradiansToSqmeters(r) {\n // http://gis.stackexchange.com/a/124857/40446\n return r / (4 * Math.PI) * 510065621724000;\n }\n\n\n function toLineString(feature) {\n if (feature.type === 'LineString') return feature;\n\n var result = { type: 'LineString', coordinates: [] };\n if (feature.type === 'Polygon') {\n result.coordinates = feature.coordinates[0];\n } else if (feature.type === 'MultiPolygon') {\n result.coordinates = feature.coordinates[0][0];\n }\n\n return result;\n }\n\n var _isImperial = !localizer.usesMetric();\n\n function redraw(selection) {\n var graph = context.graph();\n var selectedNoteID = context.selectedNoteID();\n var osm = services.osm;\n\n var localeCode = localizer.localeCode();\n\n var heading;\n var center, location, centroid;\n var closed, geometry;\n var totalNodeCount, length = 0, area = 0, distance;\n\n if (selectedNoteID && osm) { // selected 1 note\n var note = osm.getNote(selectedNoteID);\n heading = t.append('note.note', { suffix: ' ' + selectedNoteID });\n location = note.loc;\n geometry = 'note';\n\n } else { // selected 1..n entities\n var selectedIDs = context.selectedIDs().filter(function(id) {\n return context.hasEntity(id);\n });\n var selected = selectedIDs.map(function(id) {\n return context.entity(id);\n });\n\n heading = selected.length === 1 ? selected[0].id :\n t.append('info_panels.selected', { n: selected.length });\n\n if (selected.length) {\n var extent = geoExtent();\n for (var i in selected) {\n var entity = selected[i];\n extent._extend(entity.extent(graph));\n\n geometry = entity.geometry(graph);\n if (geometry === 'line' || geometry === 'area') {\n closed = (entity.type === 'relation') || (entity.isClosed() && !entity.isDegenerate());\n var feature = entity.asGeoJSON(graph);\n length += radiansToMeters(d3_geoLength(toLineString(feature)));\n centroid = d3_geoPath(context.projection).centroid(entity.asGeoJSON(graph));\n centroid = centroid && context.projection.invert(centroid);\n if (!centroid || !isFinite(centroid[0]) || !isFinite(centroid[1])) {\n centroid = entity.extent(graph).center();\n }\n if (closed) {\n area += steradiansToSqmeters(entity.area(graph));\n }\n }\n }\n\n if (selected.length > 1) {\n geometry = null;\n closed = null;\n centroid = null;\n }\n\n if (selected.length === 2 &&\n selected[0].type === 'node' &&\n selected[1].type === 'node') {\n distance = geoSphericalDistance(selected[0].loc, selected[1].loc);\n }\n\n if (selected.length === 1 && selected[0].type === 'node') {\n location = selected[0].loc;\n } else {\n totalNodeCount = utilGetAllNodes(selectedIDs, context.graph()).length;\n }\n\n if (!location && !centroid) {\n center = extent.center();\n }\n }\n }\n\n selection.html('');\n\n if (heading && typeof heading === 'function') {\n selection\n .append('h4')\n .attr('class', 'measurement-heading')\n .call(heading);\n } else {\n selection\n .append('h4')\n .attr('class', 'measurement-heading')\n .text(heading);\n }\n\n var list = selection\n .append('ul');\n var coordItem;\n\n if (geometry) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.geometry', { suffix: ':' }))\n .append('span')\n .call(\n closed ? t.append('info_panels.measurement.closed_' + geometry) : t.append('geometry.' + geometry)\n );\n }\n\n if (totalNodeCount) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.node_count', { suffix: ':' }))\n .append('span')\n .text(totalNodeCount.toLocaleString(localeCode));\n }\n\n if (area) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.area', { suffix: ':' }))\n .append('span')\n .text(displayArea(area, _isImperial));\n }\n\n if (length) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.' + (closed ? 'perimeter' : 'length'), { suffix: ':' }))\n .append('span')\n .text(displayLength(length, _isImperial));\n }\n\n if (typeof distance === 'number') {\n list\n .append('li')\n .call(t.append('info_panels.measurement.distance', { suffix: ':' }))\n .append('span')\n .text(displayLength(distance, _isImperial));\n }\n\n if (location) {\n coordItem = list\n .append('li')\n .call(t.append('info_panels.measurement.location', { suffix: ':' }));\n coordItem.append('span')\n .text(dmsCoordinatePair(location));\n coordItem.append('span')\n .text(decimalCoordinatePair(location));\n }\n\n if (centroid) {\n coordItem = list\n .append('li')\n .call(t.append('info_panels.measurement.centroid', { suffix: ':' }));\n coordItem.append('span')\n .text(dmsCoordinatePair(centroid));\n coordItem.append('span')\n .text(decimalCoordinatePair(centroid));\n }\n\n if (center) {\n coordItem = list\n .append('li')\n .call(t.append('info_panels.measurement.center', { suffix: ':' }));\n coordItem.append('span')\n .text(dmsCoordinatePair(center));\n coordItem.append('span')\n .text(decimalCoordinatePair(center));\n }\n\n if (length || area || typeof distance === 'number') {\n var toggle = _isImperial ? 'imperial' : 'metric';\n selection\n .append('a')\n .call(t.append('info_panels.measurement.' + toggle))\n .attr('href', '#')\n .attr('class', 'button button-toggle-units')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n _isImperial = !_isImperial;\n selection.call(redraw);\n });\n }\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.map()\n .on('drawn.info-measurement', function() {\n selection.call(redraw);\n });\n\n context\n .on('enter.info-measurement', function() {\n selection.call(redraw);\n });\n };\n\n panel.off = function() {\n context.map().on('drawn.info-measurement', null);\n context.on('enter.info-measurement', null);\n };\n\n panel.id = 'measurement';\n panel.label = t.append('info_panels.measurement.title');\n panel.key = t('info_panels.measurement.key');\n\n\n return panel;\n}\n", "export * from './background';\nexport * from './history';\nexport * from './location';\nexport * from './measurement';\n\nimport { uiPanelBackground } from './background';\nimport { uiPanelHistory } from './history';\nimport { uiPanelLocation } from './location';\nimport { uiPanelMeasurement } from './measurement';\n\nexport var uiInfoPanels = {\n background: uiPanelBackground,\n history: uiPanelHistory,\n location: uiPanelLocation,\n measurement: uiPanelMeasurement,\n};\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\nimport { uiInfoPanels } from './panels';\n\n\nexport function uiInfo(context) {\n var ids = Object.keys(uiInfoPanels);\n var wasActive = ['measurement'];\n var panels = {};\n var active = {};\n\n // create panels\n ids.forEach(function(k) {\n if (!panels[k]) {\n panels[k] = uiInfoPanels[k](context);\n active[k] = false;\n }\n });\n\n\n function info(selection) {\n\n function redraw() {\n var activeids = ids.filter(function(k) { return active[k]; }).sort();\n\n var containers = infoPanels.selectAll('.panel-container')\n .data(activeids, function(k) { return k; });\n\n containers.exit()\n .style('opacity', 1)\n .transition()\n .duration(200)\n .style('opacity', 0)\n .on('end', function(d) {\n d3_select(this)\n .call(panels[d].off)\n .remove();\n });\n\n var enter = containers.enter()\n .append('div')\n .attr('class', function(d) { return 'fillD2 panel-container panel-container-' + d; });\n\n enter\n .style('opacity', 0)\n .transition()\n .duration(200)\n .style('opacity', 1);\n\n var title = enter\n .append('div')\n .attr('class', 'panel-title fillD2');\n\n title\n .append('h3')\n .each(function(d) { return panels[d].label(d3_select(this)); });\n\n title\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function(d3_event, d) {\n d3_event.stopImmediatePropagation();\n d3_event.preventDefault();\n info.toggle(d);\n })\n .call(svgIcon('#iD-icon-close'));\n\n enter\n .append('div')\n .attr('class', function(d) { return 'panel-content panel-content-' + d; });\n\n\n // redraw the panels\n infoPanels.selectAll('.panel-content')\n .each(function(d) {\n d3_select(this).call(panels[d]);\n });\n }\n\n\n info.toggle = function(which) {\n var activeids = ids.filter(function(k) { return active[k]; });\n\n if (which) { // toggle one\n active[which] = !active[which];\n if (activeids.length === 1 && activeids[0] === which) { // none active anymore\n wasActive = [which];\n }\n\n context.container().select('.' + which + '-panel-toggle-item')\n .classed('active', active[which])\n .select('input')\n .property('checked', active[which]);\n\n } else { // toggle all\n if (activeids.length) {\n wasActive = activeids;\n activeids.forEach(function(k) { active[k] = false; });\n } else {\n wasActive.forEach(function(k) { active[k] = true; });\n }\n }\n\n redraw();\n };\n\n\n var infoPanels = selection.selectAll('.info-panels')\n .data([0]);\n\n infoPanels = infoPanels.enter()\n .append('div')\n .attr('class', 'info-panels')\n .merge(infoPanels);\n\n redraw();\n\n context.keybinding()\n .on(uiCmd('\u2318' + t('info_panels.key')), function(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.stopImmediatePropagation();\n d3_event.preventDefault();\n info.toggle();\n });\n\n ids.forEach(function(k) {\n var key = t('info_panels.' + k + '.key', { default: null });\n if (!key) return;\n context.keybinding()\n .on(uiCmd('\u2318\u21E7' + key), function(d3_event) {\n d3_event.stopImmediatePropagation();\n d3_event.preventDefault();\n info.toggle(k);\n });\n });\n }\n\n return info;\n}\n", "import { easeLinear as d3_easeLinear } from 'd3-ease';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { localizer } from '../core/localizer';\nimport { uiToggle } from './toggle';\n\n\n// Tooltips and svg mask used to highlight certain features\nexport function uiCurtain(containerNode) {\n\n var surface = d3_select(null),\n tooltip = d3_select(null),\n darkness = d3_select(null);\n\n function curtain(selection) {\n surface = selection\n .append('svg')\n .attr('class', 'curtain')\n .style('top', 0)\n .style('left', 0);\n\n darkness = surface.append('path')\n .attr('x', 0)\n .attr('y', 0)\n .attr('class', 'curtain-darkness');\n\n d3_select(window).on('resize.curtain', resize);\n\n tooltip = selection.append('div')\n .attr('class', 'tooltip');\n\n tooltip\n .append('div')\n .attr('class', 'popover-arrow');\n\n tooltip\n .append('div')\n .attr('class', 'popover-inner');\n\n resize();\n\n\n function resize() {\n surface\n .attr('width', containerNode.clientWidth)\n .attr('height', containerNode.clientHeight);\n curtain.cut(darkness.datum());\n }\n }\n\n\n /**\n * Reveal cuts the curtain to highlight the given box,\n * and shows a tooltip with instructions next to the box.\n *\n * @param {String|ClientRect|HTMLElement} [box] box used to cut the curtain\n * @param {String} [text] text for a tooltip\n * @param {Object} [options]\n * @param {string} [options.tooltipClass] optional class to add to the tooltip\n * @param {integer} [options.duration] transition time in milliseconds\n * @param {string} [options.buttonText] if set, create a button with this text label\n * @param {function} [options.buttonCallback] if set, the callback for the button\n * @param {function} [options.padding] extra margin in px to put around bbox\n * @param {String|ClientRect} [options.tooltipBox] box for tooltip position, if different from box for the curtain\n */\n curtain.reveal = function(box, html, options) {\n options = options || {};\n\n if (typeof box === 'string') {\n box = d3_select(box).node();\n }\n if (box && box.getBoundingClientRect) {\n box = copyBox(box.getBoundingClientRect());\n }\n if (box) {\n var containerRect = containerNode.getBoundingClientRect();\n box.top -= containerRect.top;\n box.left -= containerRect.left;\n }\n if (box && options.padding) {\n box.top -= options.padding;\n box.left -= options.padding;\n box.bottom += options.padding;\n box.right += options.padding;\n box.height += options.padding * 2;\n box.width += options.padding * 2;\n }\n\n var tooltipBox;\n if (options.tooltipBox) {\n tooltipBox = options.tooltipBox;\n if (typeof tooltipBox === 'string') {\n tooltipBox = d3_select(tooltipBox).node();\n }\n if (tooltipBox && tooltipBox.getBoundingClientRect) {\n tooltipBox = copyBox(tooltipBox.getBoundingClientRect());\n }\n } else {\n tooltipBox = box;\n }\n\n if (tooltipBox && html) {\n\n if (html.indexOf('**') !== -1) {\n if (html.indexOf(')(.+?)(\\*\\*)/, '$1$2$3');\n } else {\n html = html.replace(/^(.+?)(\\*\\*)/, '$1$2');\n }\n // pseudo markdown bold text for the instruction section..\n html = html.replace(/\\*\\*(.*?)\\*\\*/g, '$1');\n }\n\n html = html.replace(/\\*(.*?)\\*/g, '$1'); // emphasis\n html = html.replace(/\\{br\\}/g, '

    '); // linebreak\n\n if (options.buttonText && options.buttonCallback) {\n html += '
    ' +\n '
    ';\n }\n\n var classes = 'curtain-tooltip popover tooltip arrowed in ' + (options.tooltipClass || '');\n tooltip\n .classed(classes, true)\n .selectAll('.popover-inner')\n .html(html);\n\n if (options.buttonText && options.buttonCallback) {\n var button = tooltip.selectAll('.button-section .button.action');\n button\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n options.buttonCallback();\n });\n }\n\n var tip = copyBox(tooltip.node().getBoundingClientRect()),\n w = containerNode.clientWidth,\n h = containerNode.clientHeight,\n tooltipWidth = 200,\n tooltipArrow = 5,\n side, pos;\n\n\n // hack: this will have bottom placement,\n // so need to reserve extra space for the tooltip illustration.\n if (options.tooltipClass === 'intro-mouse') {\n tip.height += 80;\n }\n\n // trim box dimensions to just the portion that fits in the container..\n if (tooltipBox.top + tooltipBox.height > h) {\n tooltipBox.height -= (tooltipBox.top + tooltipBox.height - h);\n }\n if (tooltipBox.left + tooltipBox.width > w) {\n tooltipBox.width -= (tooltipBox.left + tooltipBox.width - w);\n }\n\n // determine tooltip placement..\n const onLeftOrRightEdge = tooltipBox.left + tooltipBox.width / 2 > w - 100 || tooltipBox.left + tooltipBox.width / 2 < 100;\n if (tooltipBox.top + tooltipBox.height < 100 && !onLeftOrRightEdge) {\n // tooltip below box..\n side = 'bottom';\n pos = [\n tooltipBox.left + tooltipBox.width / 2 - tip.width / 2,\n tooltipBox.top + tooltipBox.height\n ];\n\n } else if (tooltipBox.top > h - 140 && !onLeftOrRightEdge) {\n // tooltip above box..\n side = 'top';\n pos = [\n tooltipBox.left + tooltipBox.width / 2 - tip.width / 2,\n tooltipBox.top - tip.height\n ];\n\n } else {\n // tooltip to the side of the tooltipBox..\n var tipY = tooltipBox.top + tooltipBox.height / 2 - tip.height / 2;\n\n if (localizer.textDirection() === 'rtl') {\n if (tooltipBox.left - tooltipWidth - tooltipArrow < 70) {\n side = 'right';\n pos = [tooltipBox.left + tooltipBox.width + tooltipArrow, tipY];\n\n } else {\n side = 'left';\n pos = [tooltipBox.left - tooltipWidth - tooltipArrow, tipY];\n }\n\n } else {\n if (tooltipBox.left + tooltipBox.width + tooltipArrow + tooltipWidth > w - 70) {\n side = 'left';\n pos = [tooltipBox.left - tooltipWidth - tooltipArrow, tipY];\n } else {\n side = 'right';\n pos = [tooltipBox.left + tooltipBox.width + tooltipArrow, tipY];\n }\n }\n }\n\n if (options.duration !== 0 || !tooltip.classed(side)) {\n tooltip.call(uiToggle(true));\n }\n\n tooltip\n .style('top', pos[1] + 'px')\n .style('left', pos[0] + 'px')\n .attr('class', classes + ' ' + side);\n\n\n // shift popover-inner if it is very close to the top or bottom edge\n // (doesn't affect the placement of the popover-arrow)\n var shiftY = 0;\n if (side === 'left' || side === 'right') {\n if (pos[1] < 60) {\n shiftY = 60 - pos[1];\n } else if (pos[1] + tip.height > h - 100) {\n shiftY = h - pos[1] - tip.height - 100;\n }\n }\n tooltip.selectAll('.popover-inner')\n .style('top', shiftY + 'px');\n\n } else {\n tooltip\n .classed('in', false)\n .call(uiToggle(false));\n }\n\n curtain.cut(box, options.duration);\n\n return tooltip;\n };\n\n\n curtain.cut = function(datum, duration) {\n darkness.datum(datum)\n .interrupt();\n\n var selection;\n if (duration === 0) {\n selection = darkness;\n } else {\n selection = darkness\n .transition()\n .duration(duration || 600)\n .ease(d3_easeLinear);\n }\n\n selection\n .attr('d', function(d) {\n var containerWidth = containerNode.clientWidth;\n var containerHeight = containerNode.clientHeight;\n var string = 'M 0,0 L 0,' + containerHeight + ' L ' +\n containerWidth + ',' + containerHeight + 'L' +\n containerWidth + ',0 Z';\n\n if (!d) return string;\n return string + 'M' +\n d.left + ',' + d.top + 'L' +\n d.left + ',' + (d.top + d.height) + 'L' +\n (d.left + d.width) + ',' + (d.top + d.height) + 'L' +\n (d.left + d.width) + ',' + (d.top) + 'Z';\n\n });\n };\n\n\n curtain.remove = function() {\n surface.remove();\n tooltip.remove();\n d3_select(window).on('resize.curtain', null);\n };\n\n\n // ClientRects are immutable, so copy them to an object,\n // in case we need to trim the height/width.\n function copyBox(src) {\n return {\n top: src.top,\n right: src.right,\n bottom: src.bottom,\n left: src.left,\n width: src.width,\n height: src.height\n };\n }\n\n\n return curtain;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { helpHtml } from './helper';\nimport { t } from '../../core/localizer';\nimport { utilRebind } from '../../util/rebind';\n\n\nexport function uiIntroWelcome(context, reveal) {\n var dispatch = d3_dispatch('done');\n\n var chapter = {\n title: 'intro.welcome.title'\n };\n\n\n function welcome() {\n context.map().centerZoom([-85.63591, 41.94285], 19);\n reveal('.intro-nav-wrap .chapter-welcome',\n helpHtml('intro.welcome.welcome'),\n { buttonText: t.html('intro.ok'), buttonCallback: practice }\n );\n }\n\n function practice() {\n reveal('.intro-nav-wrap .chapter-welcome',\n helpHtml('intro.welcome.practice'),\n { buttonText: t.html('intro.ok'), buttonCallback: words }\n );\n }\n\n function words() {\n reveal('.intro-nav-wrap .chapter-welcome',\n helpHtml('intro.welcome.words'),\n { buttonText: t.html('intro.ok'), buttonCallback: chapters }\n );\n }\n\n\n function chapters() {\n dispatch.call('done');\n reveal('.intro-nav-wrap .chapter-navigation',\n helpHtml('intro.welcome.chapters', { next: t('intro.navigation.title') })\n );\n }\n\n\n chapter.enter = function() {\n welcome();\n };\n\n\n chapter.exit = function() {\n context.container().select('.curtain-tooltip.intro-mouse')\n .selectAll('.counter')\n .remove();\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pointBox, transitionTime } from './helper';\n\n\nexport function uiIntroNavigation(context, reveal) {\n var dispatch = d3_dispatch('done');\n var timeouts = [];\n var hallId = 'n2061';\n var townHall = [-85.63591, 41.94285];\n var springStreetId = 'w397';\n var springStreetEndId = 'n1834';\n var springStreet = [-85.63582, 41.94255];\n var onewayField = presetManager.field('oneway');\n var maxspeedField = presetManager.field('maxspeed');\n\n\n var chapter = {\n title: 'intro.navigation.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function isTownHallSelected() {\n var ids = context.selectedIDs();\n return ids.length === 1 && ids[0] === hallId;\n }\n\n\n function dragMap() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var msec = transitionTime(townHall, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(townHall, 19, msec);\n\n timeout(function() {\n var centerStart = context.map().center();\n\n var textId = context.lastPointerType() === 'mouse' ? 'drag' : 'drag_touch';\n var dragString = helpHtml('intro.navigation.map_info') + '{br}' + helpHtml('intro.navigation.' + textId);\n reveal('.main-map .surface', dragString);\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', dragString, { duration: 0 });\n });\n\n context.map().on('move.intro', function() {\n var centerNow = context.map().center();\n if (centerStart[0] !== centerNow[0] || centerStart[1] !== centerNow[1]) {\n context.map().on('move.intro', null);\n timeout(function() { continueTo(zoomMap); }, 3000);\n }\n });\n\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function zoomMap() {\n var zoomStart = context.map().zoom();\n\n var textId = context.lastPointerType() === 'mouse' ? 'zoom' : 'zoom_touch';\n var zoomString = helpHtml('intro.navigation.' + textId);\n\n reveal('.main-map .surface', zoomString);\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', zoomString, { duration: 0 });\n });\n\n context.map().on('move.intro', function() {\n if (context.map().zoom() !== zoomStart) {\n context.map().on('move.intro', null);\n timeout(function() { continueTo(features); }, 3000);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function features() {\n var onClick = function() { continueTo(pointsLinesAreas); };\n\n reveal('.main-map .surface', helpHtml('intro.navigation.features'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', helpHtml('intro.navigation.features'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('drawn.intro', null);\n nextStep();\n }\n }\n\n function pointsLinesAreas() {\n var onClick = function() { continueTo(nodesWays); };\n\n reveal('.main-map .surface', helpHtml('intro.navigation.points_lines_areas'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', helpHtml('intro.navigation.points_lines_areas'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('drawn.intro', null);\n nextStep();\n }\n }\n\n function nodesWays() {\n var onClick = function() { continueTo(clickTownHall); };\n\n reveal('.main-map .surface', helpHtml('intro.navigation.nodes_ways'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', helpHtml('intro.navigation.nodes_ways'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('drawn.intro', null);\n nextStep();\n }\n }\n\n function clickTownHall() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n reveal(null, null, { duration: 0 });\n context.map().centerZoomEase(entity.loc, 19, 500);\n\n timeout(function() {\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n var textId = context.lastPointerType() === 'mouse' ? 'click_townhall' : 'tap_townhall';\n reveal(box, helpHtml('intro.navigation.' + textId));\n\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.navigation.' + textId), { duration: 0 });\n });\n\n context.on('enter.intro', function() {\n if (isTownHallSelected()) continueTo(selectedTownHall);\n });\n\n }, 550); // after centerZoomEase\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function selectedTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n var entity = context.hasEntity(hallId);\n if (!entity) return clickTownHall();\n\n var box = pointBox(entity.loc, context);\n var onClick = function() { continueTo(editorTownHall); };\n\n reveal(box, helpHtml('intro.navigation.selected_townhall'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.navigation.selected_townhall'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function editorTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n var onClick = function() { continueTo(presetTownHall); };\n\n reveal('.entity-editor-pane',\n helpHtml('intro.navigation.editor_townhall'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.on('exit.intro', function() {\n continueTo(clickTownHall);\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n nextStep();\n }\n }\n\n\n function presetTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n // preset match, in case the user happened to change it.\n var entity = context.entity(context.selectedIDs()[0]);\n var preset = presetManager.match(entity, context.graph());\n\n var onClick = function() { continueTo(fieldsTownHall); };\n\n reveal('.entity-editor-pane .section-feature-type',\n helpHtml('intro.navigation.preset_townhall', { preset: preset.name() }),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.on('exit.intro', function() {\n continueTo(clickTownHall);\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n nextStep();\n }\n }\n\n\n function fieldsTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n var onClick = function() { continueTo(closeTownHall); };\n\n reveal('.entity-editor-pane .section-preset-fields',\n helpHtml('intro.navigation.fields_townhall'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.on('exit.intro', function() {\n continueTo(clickTownHall);\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n nextStep();\n }\n }\n\n\n function closeTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane',\n helpHtml('intro.navigation.close_townhall', { button: { html: icon(href, 'inline') } })\n );\n\n context.on('exit.intro', function() {\n continueTo(searchStreet);\n });\n\n context.history().on('change.intro', function() {\n // update the close icon in the tooltip if the user edits something.\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane',\n helpHtml('intro.navigation.close_townhall', { button: { html: icon(href, 'inline') } }),\n { duration: 0 }\n );\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function searchStreet() {\n context.enter(modeBrowse(context));\n context.history().reset('initial'); // ensure spring street exists\n\n var msec = transitionTime(springStreet, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(springStreet, 19, msec); // ..and user can see it\n\n timeout(function() {\n reveal('.search-header input',\n helpHtml('intro.navigation.search_street', { name: t('intro.graph.name.spring-street') })\n );\n\n context.container().select('.search-header input')\n .on('keyup.intro', checkSearchResult);\n }, msec + 100);\n }\n\n\n function checkSearchResult() {\n var first = context.container().select('.feature-list-item:nth-child(0n+2)'); // skip \"No Results\" item\n var firstName = first.select('.entity-name');\n var name = t('intro.graph.name.spring-street');\n\n if (!firstName.empty() && firstName.html() === name) {\n reveal(first.node(),\n helpHtml('intro.navigation.choose_street', { name: name }),\n { duration: 300 }\n );\n\n context.on('exit.intro', function() {\n continueTo(selectedStreet);\n });\n\n context.container().select('.search-header input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n }\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.container().select('.search-header input')\n .on('keydown.intro', null)\n .on('keyup.intro', null);\n nextStep();\n }\n }\n\n\n function selectedStreet() {\n if (!context.hasEntity(springStreetEndId) || !context.hasEntity(springStreetId)) {\n return searchStreet();\n }\n\n var onClick = function() { continueTo(editorStreet); };\n var entity = context.entity(springStreetEndId);\n var box = pointBox(entity.loc, context);\n box.height = 500;\n\n reveal(box,\n helpHtml('intro.navigation.selected_street', { name: t('intro.graph.name.spring-street') }),\n { duration: 600, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(springStreetEndId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n box.height = 500;\n reveal(box,\n helpHtml('intro.navigation.selected_street', { name: t('intro.graph.name.spring-street') }),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n }, 600); // after reveal.\n\n context.on('enter.intro', function(mode) {\n if (!context.hasEntity(springStreetId)) {\n return continueTo(searchStreet);\n }\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== springStreetId) {\n // keep Spring Street selected..\n context.enter(modeSelect(context, [springStreetId]));\n }\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(springStreetEndId) || !context.hasEntity(springStreetId)) {\n timeout(function() {\n continueTo(searchStreet);\n }, 300); // after any transition (e.g. if user deleted intersection)\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function editorStreet() {\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane', helpHtml('intro.navigation.street_different_fields') + '{br}' +\n helpHtml('intro.navigation.editor_street', {\n button: { html: icon(href, 'inline') },\n field1: onewayField.title(),\n field2: maxspeedField.title()\n }));\n\n context.on('exit.intro', function() {\n continueTo(play);\n });\n\n context.history().on('change.intro', function() {\n // update the close icon in the tooltip if the user edits something.\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane', helpHtml('intro.navigation.street_different_fields') + '{br}' +\n helpHtml('intro.navigation.editor_street', {\n button: { html: icon(href, 'inline') },\n field1: onewayField.title(),\n field2: maxspeedField.title()\n }), { duration: 0 }\n );\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.navigation.play', { next: t('intro.points.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-point',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n dragMap();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.search-header input').on('keydown.intro keyup.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { actionChangePreset } from '../../actions/change_preset';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pointBox, pad, selectMenuItem, transitionTime } from './helper';\n\n\nexport function uiIntroPoint(context, reveal) {\n var dispatch = d3_dispatch('done');\n var timeouts = [];\n var intersection = [-85.63279, 41.94394];\n var building = [-85.632422, 41.944045];\n var cafePreset = presetManager.item('amenity/cafe');\n var _pointID = null;\n\n\n var chapter = {\n title: 'intro.points.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function addPoint() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var msec = transitionTime(intersection, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(intersection, 19, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-point',\n helpHtml('intro.points.points_info') + '{br}' + helpHtml('intro.points.add_point'));\n\n _pointID = null;\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-points');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-point') return;\n continueTo(placePoint);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function placePoint() {\n if (context.mode().id !== 'add-point') {\n return chapter.restart();\n }\n\n var pointBox = pad(building, 150, context);\n var textId = context.lastPointerType() === 'mouse' ? 'place_point' : 'place_point_touch';\n reveal(pointBox, helpHtml('intro.points.' + textId));\n\n context.map().on('move.intro drawn.intro', function() {\n pointBox = pad(building, 150, context);\n reveal(pointBox, helpHtml('intro.points.' + textId), { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return chapter.restart();\n _pointID = context.mode().selectedIDs()[0];\n\n if (context.graph().geometry(_pointID) === 'vertex'){\n\n //disallow all\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n\n reveal(pointBox, helpHtml('intro.points.place_point_error'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { return chapter.restart(); }\n });\n } else {\n continueTo(searchPreset);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function searchPreset() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return addPoint();\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.points.search_cafe', { preset: cafePreset.name() })\n );\n\n context.on('enter.intro', function(mode) {\n if (!_pointID || !context.hasEntity(_pointID)) {\n return continueTo(addPoint);\n }\n\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _pointID) {\n // keep the user's point selected..\n context.enter(modeSelect(context, [_pointID]));\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.points.search_cafe', { preset: cafePreset.name() })\n );\n\n context.history().on('change.intro', null);\n }\n });\n\n\n function checkPresetSearch() {\n var first = context.container().select('.preset-list-item:first-child');\n\n if (first.classed('preset-amenity-cafe')) {\n context.container().select('.preset-search-input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n\n reveal(first.select('.preset-list-button').node(),\n helpHtml('intro.points.choose_cafe', { preset: cafePreset.name() }),\n { duration: 300 }\n );\n\n context.history().on('change.intro', function() {\n continueTo(aboutFeatureEditor);\n });\n }\n }\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n nextStep();\n }\n }\n\n\n function aboutFeatureEditor() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return addPoint();\n }\n\n timeout(function() {\n reveal('.entity-editor-pane', helpHtml('intro.points.feature_editor'), {\n tooltipClass: 'intro-points-describe',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(addName); }\n });\n }, 400);\n\n context.on('exit.intro', function() {\n // if user leaves select mode here, just continue with the tutorial.\n continueTo(reselectPoint);\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function addName() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return addPoint();\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n var addNameString = helpHtml('intro.points.fields_info') + '{br}' + helpHtml('intro.points.add_name') + '{br}' + helpHtml('intro.points.add_reminder');\n\n timeout(function() {\n // It's possible for the user to add a name in a previous step..\n // If so, don't tell them to add the name in this step.\n // Give them an OK button instead.\n var entity = context.entity(_pointID);\n if (entity.tags.name) {\n var tooltip = reveal('.entity-editor-pane', addNameString, {\n tooltipClass: 'intro-points-describe',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(addCloseEditor); }\n });\n tooltip.select('.instruction').style('display', 'none');\n\n } else {\n reveal('.entity-editor-pane', addNameString,\n { tooltipClass: 'intro-points-describe' }\n );\n }\n }, 400);\n\n context.history().on('change.intro', function() {\n continueTo(addCloseEditor);\n });\n\n context.on('exit.intro', function() {\n // if user leaves select mode here, just continue with the tutorial.\n continueTo(reselectPoint);\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function addCloseEditor() {\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n context.on('exit.intro', function() {\n continueTo(reselectPoint);\n });\n\n reveal('.entity-editor-pane',\n helpHtml('intro.points.add_close', { button: { html: icon(href, 'inline') } })\n );\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function reselectPoint() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n\n // make sure it's still a cafe, in case user somehow changed it..\n var oldPreset = presetManager.match(entity, context.graph());\n context.replace(actionChangePreset(_pointID, oldPreset, cafePreset));\n\n context.enter(modeBrowse(context));\n\n var msec = transitionTime(entity.loc, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerEase(entity.loc, msec);\n\n timeout(function() {\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.points.reselect'), { duration: 600 });\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.points.reselect'), { duration: 0 });\n });\n }, 600); // after reveal..\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n continueTo(updatePoint);\n });\n\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function updatePoint() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return continueTo(reselectPoint);\n }\n\n // reset pane, in case user happened to untag the point..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n context.on('exit.intro', function() {\n continueTo(reselectPoint);\n });\n\n context.history().on('change.intro', function() {\n continueTo(updateCloseEditor);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane', helpHtml('intro.points.update'),\n { tooltipClass: 'intro-points-describe' }\n );\n }, 400);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function updateCloseEditor() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return continueTo(reselectPoint);\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n context.on('exit.intro', function() {\n continueTo(rightClickPoint);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.points.update_close', { button: { html: icon('#iD-icon-close', 'inline') } })\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickPoint() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n\n context.enter(modeBrowse(context));\n\n var box = pointBox(entity.loc, context);\n var textId = context.lastPointerType() === 'mouse' ? 'rightclick' : 'edit_menu_touch';\n reveal(box, helpHtml('intro.points.' + textId), { duration: 600 });\n\n timeout(function() {\n context.map().on('move.intro', function() {\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.points.' + textId), { duration: 0 });\n });\n }, 600); // after reveal\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== _pointID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'delete').node();\n if (!node) return;\n continueTo(enterDelete);\n }, 50); // after menu visible\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro', null);\n nextStep();\n }\n }\n\n\n function enterDelete() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n\n var node = selectMenuItem(context, 'delete').node();\n if (!node) { return continueTo(rightClickPoint); }\n\n reveal('.edit-menu',\n helpHtml('intro.points.delete'),\n { padding: 50 }\n );\n\n timeout(function() {\n context.map().on('move.intro', function() {\n if (selectMenuItem(context, 'delete').empty()) {\n return continueTo(rightClickPoint);\n }\n reveal('.edit-menu',\n helpHtml('intro.points.delete'),\n { duration: 0, padding: 50 }\n );\n });\n }, 300); // after menu visible\n\n context.on('exit.intro', function() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (entity) return continueTo(rightClickPoint); // point still exists\n });\n\n context.history().on('change.intro', function(changed) {\n if (changed.deleted().length) {\n continueTo(undo);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro', null);\n context.history().on('change.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function undo() {\n context.history().on('change.intro', function() {\n continueTo(play);\n });\n\n reveal('.top-toolbar button.undo-button',\n helpHtml('intro.points.undo')\n );\n\n function continueTo(nextStep) {\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.points.play', { next: t('intro.areas.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-area',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addPoint();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n interpolateNumber as d3_interpolateNumber\n} from 'd3-interpolate';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pad, transitionTime } from './helper';\n\n\nexport function uiIntroArea(context, reveal) {\n var dispatch = d3_dispatch('done');\n var playground = [-85.63552, 41.94159];\n var playgroundPreset = presetManager.item('leisure/playground');\n var nameField = presetManager.field('name');\n var descriptionField = presetManager.field('description');\n var timeouts = [];\n var _areaID;\n\n\n var chapter = {\n title: 'intro.areas.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function revealPlayground(center, text, options) {\n var padding = 180 * Math.pow(2, context.map().zoom() - 19.5);\n var box = pad(center, padding, context);\n reveal(box, text, options);\n }\n\n\n function addArea() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n _areaID = null;\n\n var msec = transitionTime(playground, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(playground, 19, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-area',\n helpHtml('intro.areas.add_playground'));\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-areas');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-area') return;\n continueTo(startPlayground);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startPlayground() {\n if (context.mode().id !== 'add-area') {\n return chapter.restart();\n }\n\n _areaID = null;\n context.map().zoomEase(19.5, 500);\n\n timeout(function() {\n var textId = context.lastPointerType() === 'mouse' ? 'starting_node_click' : 'starting_node_tap';\n var startDrawString = helpHtml('intro.areas.start_playground') + helpHtml('intro.areas.' + textId);\n revealPlayground(playground,\n startDrawString, { duration: 250 }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n revealPlayground(playground,\n startDrawString, { duration: 0 }\n );\n });\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-area') return chapter.restart();\n continueTo(continuePlayground);\n });\n }, 250); // after reveal\n\n }, 550); // after easing\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continuePlayground() {\n if (context.mode().id !== 'draw-area') {\n return chapter.restart();\n }\n\n _areaID = null;\n revealPlayground(playground,\n helpHtml('intro.areas.continue_playground'),\n { duration: 250 }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n revealPlayground(playground,\n helpHtml('intro.areas.continue_playground'),\n { duration: 0 }\n );\n });\n }, 250); // after reveal\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n var entity = context.hasEntity(context.selectedIDs()[0]);\n if (entity && entity.nodes.length >= 6) {\n return continueTo(finishPlayground);\n } else {\n return;\n }\n } else if (mode.id === 'select') {\n _areaID = context.selectedIDs()[0];\n return continueTo(searchPresets);\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function finishPlayground() {\n if (context.mode().id !== 'draw-area') {\n return chapter.restart();\n }\n\n _areaID = null;\n\n var finishString = helpHtml('intro.areas.finish_area_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.areas.finish_playground');\n revealPlayground(playground,\n finishString, { duration: 250 }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n revealPlayground(playground,\n finishString, { duration: 0 }\n );\n });\n }, 250); // after reveal\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n return;\n } else if (mode.id === 'select') {\n _areaID = context.selectedIDs()[0];\n return continueTo(searchPresets);\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function searchPresets() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n context.enter(modeSelect(context, [_areaID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.areas.search_playground', { preset: playgroundPreset.name() })\n );\n }, 400); // after preset list pane visible..\n\n context.on('enter.intro', function(mode) {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return continueTo(addArea);\n }\n\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _areaID) {\n // keep the user's area selected..\n context.enter(modeSelect(context, [_areaID]));\n\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.areas.search_playground', { preset: playgroundPreset.name() })\n );\n\n context.history().on('change.intro', null);\n }\n });\n\n function checkPresetSearch() {\n var first = context.container().select('.preset-list-item:first-child');\n\n if (first.classed('preset-leisure-playground')) {\n reveal(first.select('.preset-list-button').node(),\n helpHtml('intro.areas.choose_playground', { preset: playgroundPreset.name() }),\n { duration: 300 }\n );\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n\n context.history().on('change.intro', function() {\n continueTo(clickAddField);\n });\n }\n }\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n nextStep();\n }\n }\n\n\n function clickAddField() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n if (!context.container().select('.form-field-description').empty()) {\n return continueTo(describePlayground);\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n // It's possible for the user to add a description in a previous step..\n // If they did this already, just continue to next step.\n var entity = context.entity(_areaID);\n if (entity.tags.description) {\n return continueTo(play);\n }\n\n // scroll \"Add field\" into view\n var box = context.container().select('.more-fields').node().getBoundingClientRect();\n if (box.top > 300) {\n var pane = context.container().select('.entity-editor-pane .inspector-body');\n var start = pane.node().scrollTop;\n var end = start + (box.top - 300);\n\n pane\n .transition()\n .duration(250)\n .tween('scroll.inspector', function() {\n var node = this;\n var i = d3_interpolateNumber(start, end);\n return function(t) {\n node.scrollTop = i(t);\n };\n });\n }\n\n timeout(function() {\n reveal('.more-fields .combobox-input',\n helpHtml('intro.areas.add_field', {\n name: nameField.title(),\n description: descriptionField.title()\n }),\n { duration: 300 }\n );\n\n context.container().select('.more-fields .combobox-input')\n .on('click.intro', function() {\n // Watch for the combobox to appear...\n var watcher;\n watcher = window.setInterval(function() {\n if (!context.container().select('div.combobox').empty()) {\n window.clearInterval(watcher);\n continueTo(chooseDescriptionField);\n }\n }, 300);\n });\n }, 300); // after \"Add Field\" visible\n\n }, 400); // after editor pane visible\n\n context.on('exit.intro', function() {\n return continueTo(searchPresets);\n });\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.more-fields .combobox-input').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function chooseDescriptionField() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n if (!context.container().select('.form-field-description').empty()) {\n return continueTo(describePlayground);\n }\n\n // Make sure combobox is ready..\n if (context.container().select('div.combobox').empty()) {\n return continueTo(clickAddField);\n }\n // Watch for the combobox to go away..\n var watcher;\n watcher = window.setInterval(function() {\n if (context.container().select('div.combobox').empty()) {\n window.clearInterval(watcher);\n timeout(function() {\n if (context.container().select('.form-field-description').empty()) {\n continueTo(retryChooseDescription);\n } else {\n continueTo(describePlayground);\n }\n }, 300); // after description field added.\n }\n }, 300);\n\n reveal('div.combobox',\n helpHtml('intro.areas.choose_field', { field: descriptionField.title() }),\n { duration: 300 }\n );\n\n context.on('exit.intro', function() {\n return continueTo(searchPresets);\n });\n\n function continueTo(nextStep) {\n if (watcher) window.clearInterval(watcher);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function describePlayground() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n if (context.container().select('.form-field-description').empty()) {\n return continueTo(retryChooseDescription);\n }\n\n context.on('exit.intro', function() {\n continueTo(play);\n });\n\n reveal('.entity-editor-pane',\n helpHtml('intro.areas.describe_playground', { button: { html: icon('#iD-icon-close', 'inline') } }),\n { duration: 300 }\n );\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function retryChooseDescription() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n reveal('.entity-editor-pane',\n helpHtml('intro.areas.retry_add_field', { field: descriptionField.title() }), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(clickAddField); }\n });\n\n context.on('exit.intro', function() {\n return continueTo(searchPresets);\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.areas.play', { next: t('intro.lines.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-line',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addArea();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n context.container().select('.more-fields .combobox-input').on('click.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { geoSphericalDistance } from '../../geo';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pad, selectMenuItem, transitionTime } from './helper';\n\n\nexport function uiIntroLine(context, reveal) {\n var dispatch = d3_dispatch('done');\n var timeouts = [];\n var _tulipRoadID = null;\n var flowerRoadID = 'w646';\n var tulipRoadStart = [-85.6297754121684, 41.95805253325314];\n var tulipRoadMidpoint = [-85.62975395449628, 41.95787501510204];\n var tulipRoadIntersection = [-85.62974496187628, 41.95742515554585];\n var roadCategory = presetManager.item('category-road_minor');\n var residentialPreset = presetManager.item('highway/residential');\n var woodRoadID = 'w525';\n var woodRoadEndID = 'n2862';\n var woodRoadAddNode = [-85.62390110349587, 41.95397111462291];\n var woodRoadDragEndpoint = [-85.623867390213, 41.95466987786487];\n var woodRoadDragMidpoint = [-85.62386254803509, 41.95430395953872];\n var washingtonStreetID = 'w522';\n var twelfthAvenueID = 'w1';\n var eleventhAvenueEndID = 'n3550';\n var twelfthAvenueEndID = 'n5';\n var _washingtonSegmentID = null;\n var eleventhAvenueEnd = context.entity(eleventhAvenueEndID).loc;\n var twelfthAvenueEnd = context.entity(twelfthAvenueEndID).loc;\n var deleteLinesLoc = [-85.6219395542764, 41.95228033922477];\n var twelfthAvenue = [-85.62219310052491, 41.952505413152956];\n\n\n var chapter = {\n title: 'intro.lines.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function addLine() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var msec = transitionTime(tulipRoadStart, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(tulipRoadStart, 18.5, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-line',\n helpHtml('intro.lines.add_line'));\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-lines');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-line') return;\n continueTo(startLine);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startLine() {\n if (context.mode().id !== 'add-line') return chapter.restart();\n\n _tulipRoadID = null;\n\n var padding = 70 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(tulipRoadStart, padding, context);\n box.height = box.height + 100;\n\n var textId = context.lastPointerType() === 'mouse' ? 'start_line' : 'start_line_tap';\n var startLineString = helpHtml('intro.lines.missing_road') + '{br}' +\n helpHtml('intro.lines.line_draw_info') +\n helpHtml('intro.lines.' + textId);\n reveal(box, startLineString);\n\n context.map().on('move.intro drawn.intro', function() {\n padding = 70 * Math.pow(2, context.map().zoom() - 18);\n box = pad(tulipRoadStart, padding, context);\n box.height = box.height + 100;\n reveal(box, startLineString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-line') return chapter.restart();\n continueTo(drawLine);\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function drawLine() {\n if (context.mode().id !== 'draw-line') return chapter.restart();\n\n _tulipRoadID = context.mode().selectedIDs()[0];\n context.map().centerEase(tulipRoadMidpoint, 500);\n\n timeout(function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18.5);\n var box = pad(tulipRoadMidpoint, padding, context);\n box.height = box.height * 2;\n reveal(box,\n helpHtml('intro.lines.intersect', { name: t('intro.graph.name.flower-street') })\n );\n\n context.map().on('move.intro drawn.intro', function() {\n padding = 200 * Math.pow(2, context.map().zoom() - 18.5);\n box = pad(tulipRoadMidpoint, padding, context);\n box.height = box.height * 2;\n reveal(box,\n helpHtml('intro.lines.intersect', { name: t('intro.graph.name.flower-street') }),\n { duration: 0 }\n );\n });\n }, 550); // after easing..\n\n context.history().on('change.intro', function() {\n if (isLineConnected()) {\n continueTo(continueLine);\n }\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-line') {\n return;\n } else if (mode.id === 'select') {\n continueTo(retryIntersect);\n return;\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function isLineConnected() {\n var entity = _tulipRoadID && context.hasEntity(_tulipRoadID);\n if (!entity) return false;\n\n var drawNodes = context.graph().childNodes(entity);\n return drawNodes.some(function(node) {\n return context.graph().parentWays(node).some(function(parent) {\n return parent.id === flowerRoadID;\n });\n });\n }\n\n\n function retryIntersect() {\n d3_select(window).on('pointerdown.intro mousedown.intro', eventCancel, true);\n\n var box = pad(tulipRoadIntersection, 80, context);\n reveal(box,\n helpHtml('intro.lines.retry_intersect', { name: t('intro.graph.name.flower-street') })\n );\n\n timeout(chapter.restart, 3000);\n }\n\n\n function continueLine() {\n if (context.mode().id !== 'draw-line') return chapter.restart();\n var entity = _tulipRoadID && context.hasEntity(_tulipRoadID);\n if (!entity) return chapter.restart();\n\n context.map().centerEase(tulipRoadIntersection, 500);\n\n var continueLineText = helpHtml('intro.lines.continue_line') + '{br}' +\n helpHtml('intro.lines.finish_line_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.lines.finish_road');\n\n reveal('.main-map .surface', continueLineText);\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-line') {\n return;\n } else if (mode.id === 'select') {\n return continueTo(chooseCategoryRoad);\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function chooseCategoryRoad() {\n if (context.mode().id !== 'select') return chapter.restart();\n\n context.on('exit.intro', function() {\n return chapter.restart();\n });\n\n var button = context.container().select('.preset-category-road_minor .preset-list-button');\n if (button.empty()) return chapter.restart();\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n reveal(button.node(),\n helpHtml('intro.lines.choose_category_road', { category: roadCategory.name() })\n );\n\n button.on('click.intro', function() {\n continueTo(choosePresetResidential);\n });\n\n }, 400); // after editor pane visible\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function choosePresetResidential() {\n if (context.mode().id !== 'select') return chapter.restart();\n\n context.on('exit.intro', function() {\n return chapter.restart();\n });\n\n var subgrid = context.container().select('.preset-category-road_minor .subgrid');\n if (subgrid.empty()) return chapter.restart();\n\n subgrid.selectAll(':not(.preset-highway-residential) .preset-list-button')\n .on('click.intro', function() {\n continueTo(retryPresetResidential);\n });\n\n subgrid.selectAll('.preset-highway-residential .preset-list-button')\n .on('click.intro', function() {\n continueTo(nameRoad);\n });\n\n timeout(function() {\n reveal(subgrid.node(),\n helpHtml('intro.lines.choose_preset_residential', { preset: residentialPreset.name() }),\n { tooltipBox: '.preset-highway-residential .preset-list-button', duration: 300 }\n );\n }, 300);\n\n function continueTo(nextStep) {\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n // selected wrong road type\n function retryPresetResidential() {\n if (context.mode().id !== 'select') return chapter.restart();\n\n context.on('exit.intro', function() {\n return chapter.restart();\n });\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n var button = context.container().select('.entity-editor-pane .preset-list-button');\n\n reveal(button.node(),\n helpHtml('intro.lines.retry_preset_residential', { preset: residentialPreset.name() })\n );\n\n button.on('click.intro', function() {\n continueTo(chooseCategoryRoad);\n });\n\n }, 500);\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function nameRoad() {\n context.on('exit.intro', function() {\n continueTo(didNameRoad);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.lines.name_road', { button: { html: icon('#iD-icon-close', 'inline') } }),\n { tooltipClass: 'intro-lines-name_road' }\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function didNameRoad() {\n context.history().checkpoint('doneAddLine');\n\n timeout(function() {\n reveal('.main-map .surface', helpHtml('intro.lines.did_name_road'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(updateLine); }\n });\n }, 500);\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function updateLine() {\n context.history().reset('doneAddLine');\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return chapter.restart();\n }\n\n var msec = transitionTime(woodRoadDragMidpoint, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(woodRoadDragMidpoint, 19, msec);\n\n timeout(function() {\n var padding = 250 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n var advance = function() { continueTo(addNode); };\n\n reveal(box, helpHtml('intro.lines.update_line'),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 250 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n reveal(box, helpHtml('intro.lines.update_line'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function addNode() {\n context.history().reset('doneAddLine');\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return chapter.restart();\n }\n\n var padding = 40 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadAddNode, padding, context);\n var addNodeString = helpHtml('intro.lines.add_node' + (context.lastPointerType() === 'mouse' ? '' : '_touch'));\n reveal(box, addNodeString);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 40 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadAddNode, padding, context);\n reveal(box, addNodeString, { duration: 0 });\n });\n\n context.history().on('change.intro', function(changed) {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n if (changed.created().length === 1) {\n timeout(function() { continueTo(startDragEndpoint); }, 500);\n }\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') {\n continueTo(updateLine);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startDragEndpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n var startDragString = helpHtml('intro.lines.start_drag_endpoint' + (context.lastPointerType() === 'mouse' ? '' : '_touch')) +\n helpHtml('intro.lines.drag_to_intersection');\n reveal(box, startDragString);\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n reveal(box, startDragString, { duration: 0 });\n\n var entity = context.entity(woodRoadEndID);\n if (geoSphericalDistance(entity.loc, woodRoadDragEndpoint) <= 4) {\n continueTo(finishDragEndpoint);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function finishDragEndpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n var finishDragString = helpHtml('intro.lines.spot_looks_good') +\n helpHtml('intro.lines.finish_drag_endpoint' + (context.lastPointerType() === 'mouse' ? '' : '_touch'));\n reveal(box, finishDragString);\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n reveal(box, finishDragString, { duration: 0 });\n\n var entity = context.entity(woodRoadEndID);\n if (geoSphericalDistance(entity.loc, woodRoadDragEndpoint) > 4) {\n continueTo(startDragEndpoint);\n }\n });\n\n context.on('enter.intro', function() {\n continueTo(startDragMidpoint);\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startDragMidpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n if (context.selectedIDs().indexOf(woodRoadID) === -1) {\n context.enter(modeSelect(context, [woodRoadID]));\n }\n\n var padding = 80 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n reveal(box, helpHtml('intro.lines.start_drag_midpoint'));\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 80 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n reveal(box, helpHtml('intro.lines.start_drag_midpoint'), { duration: 0 });\n });\n\n context.history().on('change.intro', function(changed) {\n if (changed.created().length === 1) {\n continueTo(continueDragMidpoint);\n }\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') {\n // keep Wood Road selected so midpoint triangles are drawn..\n context.enter(modeSelect(context, [woodRoadID]));\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continueDragMidpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n box.height += 400;\n\n var advance = function() {\n context.history().checkpoint('doneUpdateLine');\n continueTo(deleteLines);\n };\n\n reveal(box, helpHtml('intro.lines.continue_drag_midpoint'),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n box.height += 400;\n reveal(box, helpHtml('intro.lines.continue_drag_midpoint'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function deleteLines() {\n context.history().reset('doneUpdateLine');\n context.enter(modeBrowse(context));\n\n if (!context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return chapter.restart();\n }\n\n var msec = transitionTime(deleteLinesLoc, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(deleteLinesLoc, 18, msec);\n\n timeout(function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(deleteLinesLoc, padding, context);\n box.top -= 200;\n box.height += 400;\n var advance = function() { continueTo(rightClickIntersection); };\n\n reveal(box, helpHtml('intro.lines.delete_lines', { street: t('intro.graph.name.12th-avenue') }),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(deleteLinesLoc, padding, context);\n box.top -= 200;\n box.height += 400;\n reveal(box, helpHtml('intro.lines.delete_lines', { street: t('intro.graph.name.12th-avenue') }),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n\n context.history().on('change.intro', function() {\n timeout(function() {\n continueTo(deleteLines);\n }, 500); // after any transition (e.g. if user deleted intersection)\n });\n\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickIntersection() {\n context.history().reset('doneUpdateLine');\n context.enter(modeBrowse(context));\n\n context.map().centerZoomEase(eleventhAvenueEnd, 18, 500);\n\n var rightClickString = helpHtml('intro.lines.split_street', {\n street1: t('intro.graph.name.11th-avenue'),\n street2: t('intro.graph.name.washington-street')\n }) +\n helpHtml('intro.lines.' + (context.lastPointerType() === 'mouse' ? 'rightclick_intersection' : 'edit_menu_intersection_touch'));\n\n timeout(function() {\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, rightClickString);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, rightClickString,\n { duration: 0 }\n );\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== eleventhAvenueEndID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'split').node();\n if (!node) return;\n continueTo(splitIntersection);\n }, 50); // after menu visible\n });\n\n context.history().on('change.intro', function() {\n timeout(function() {\n continueTo(deleteLines);\n }, 300); // after any transition (e.g. if user deleted intersection)\n });\n\n }, 600);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function splitIntersection() {\n if (!context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(deleteLines);\n }\n\n var node = selectMenuItem(context, 'split').node();\n if (!node) { return continueTo(rightClickIntersection); }\n\n var wasChanged = false;\n _washingtonSegmentID = null;\n\n reveal('.edit-menu', helpHtml('intro.lines.split_intersection',\n { street: t('intro.graph.name.washington-street') }),\n { padding: 50 }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var node = selectMenuItem(context, 'split').node();\n if (!wasChanged && !node) { return continueTo(rightClickIntersection); }\n\n reveal('.edit-menu', helpHtml('intro.lines.split_intersection',\n { street: t('intro.graph.name.washington-street') }),\n { duration: 0, padding: 50 }\n );\n });\n\n context.history().on('change.intro', function(changed) {\n wasChanged = true;\n timeout(function() {\n if (context.history().undoAnnotation() === t('operations.split.annotation.line', { n: 1 })) {\n _washingtonSegmentID = changed.created()[0].id;\n continueTo(didSplit);\n } else {\n _washingtonSegmentID = null;\n continueTo(retrySplit);\n }\n }, 300); // after any transition (e.g. if user deleted intersection)\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retrySplit() {\n context.enter(modeBrowse(context));\n context.map().centerZoomEase(eleventhAvenueEnd, 18, 500);\n var advance = function() { continueTo(rightClickIntersection); };\n\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, helpHtml('intro.lines.retry_split'),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, helpHtml('intro.lines.retry_split'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function didSplit() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var ids = context.selectedIDs();\n var string = 'intro.lines.did_split_' + (ids.length > 1 ? 'multi' : 'single');\n var street = t('intro.graph.name.washington-street');\n\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n box.width = box.width / 2;\n reveal(box, helpHtml(string, { street1: street, street2: street }),\n { duration: 500 }\n );\n\n timeout(function() {\n context.map().centerZoomEase(twelfthAvenue, 18, 500);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n box.width = box.width / 2;\n reveal(box, helpHtml(string, { street1: street, street2: street }),\n { duration: 0 }\n );\n });\n }, 600); // after initial reveal and curtain cut\n\n context.on('enter.intro', function() {\n var ids = context.selectedIDs();\n if (ids.length === 1 && ids[0] === _washingtonSegmentID) {\n continueTo(multiSelect);\n }\n });\n\n context.history().on('change.intro', function() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function multiSelect() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var ids = context.selectedIDs();\n var hasWashington = ids.indexOf(_washingtonSegmentID) !== -1;\n var hasTwelfth = ids.indexOf(twelfthAvenueID) !== -1;\n\n if (hasWashington && hasTwelfth) {\n return continueTo(multiRightClick);\n } else if (!hasWashington && !hasTwelfth) {\n return continueTo(didSplit);\n }\n\n context.map().centerZoomEase(twelfthAvenue, 18, 500);\n\n timeout(function() {\n var selected, other, padding, box;\n if (hasWashington) {\n selected = t('intro.graph.name.washington-street');\n other = t('intro.graph.name.12th-avenue');\n padding = 60 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenueEnd, padding, context);\n box.width *= 3;\n } else {\n selected = t('intro.graph.name.12th-avenue');\n other = t('intro.graph.name.washington-street');\n padding = 200 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenue, padding, context);\n box.width /= 2;\n }\n\n reveal(box,\n helpHtml('intro.lines.multi_select',\n { selected: selected, other1: other }) + ' ' +\n helpHtml('intro.lines.add_to_selection_' + (context.lastPointerType() === 'mouse' ? 'click' : 'touch'),\n { selected: selected, other2: other })\n );\n\n context.map().on('move.intro drawn.intro', function() {\n if (hasWashington) {\n selected = t('intro.graph.name.washington-street');\n other = t('intro.graph.name.12th-avenue');\n padding = 60 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenueEnd, padding, context);\n box.width *= 3;\n } else {\n selected = t('intro.graph.name.12th-avenue');\n other = t('intro.graph.name.washington-street');\n padding = 200 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenue, padding, context);\n box.width /= 2;\n }\n\n reveal(box,\n helpHtml('intro.lines.multi_select',\n { selected: selected, other1: other }) + ' ' +\n helpHtml('intro.lines.add_to_selection_' + (context.lastPointerType() === 'mouse' ? 'click' : 'touch'),\n { selected: selected, other2: other }),\n { duration: 0 }\n );\n });\n\n context.on('enter.intro', function() {\n continueTo(multiSelect);\n });\n\n context.history().on('change.intro', function() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n });\n }, 600);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function multiRightClick() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n\n var rightClickString = helpHtml('intro.lines.multi_select_success') +\n helpHtml('intro.lines.multi_' + (context.lastPointerType() === 'mouse' ? 'rightclick' : 'edit_menu_touch'));\n reveal(box, rightClickString);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n reveal(box, rightClickString, { duration: 0 });\n });\n\n context.ui().editMenu().on('toggled.intro', function(open) {\n if (!open) return;\n\n timeout(function() {\n var ids = context.selectedIDs();\n if (ids.length === 2 &&\n ids.indexOf(twelfthAvenueID) !== -1 &&\n ids.indexOf(_washingtonSegmentID) !== -1) {\n var node = selectMenuItem(context, 'delete').node();\n if (!node) return;\n continueTo(multiDelete);\n } else if (ids.length === 1 &&\n ids.indexOf(_washingtonSegmentID) !== -1) {\n return continueTo(multiSelect);\n } else {\n return continueTo(didSplit);\n }\n }, 300); // after edit menu visible\n });\n\n context.history().on('change.intro', function() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.ui().editMenu().on('toggled.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function multiDelete() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var node = selectMenuItem(context, 'delete').node();\n if (!node) return continueTo(multiRightClick);\n\n reveal('.edit-menu',\n helpHtml('intro.lines.multi_delete'),\n { padding: 50 }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n reveal('.edit-menu',\n helpHtml('intro.lines.multi_delete'),\n { duration: 0, padding: 50 }\n );\n });\n\n context.on('exit.intro', function() {\n if (context.hasEntity(_washingtonSegmentID) || context.hasEntity(twelfthAvenueID)) {\n return continueTo(multiSelect); // left select mode but roads still exist\n }\n });\n\n context.history().on('change.intro', function() {\n if (context.hasEntity(_washingtonSegmentID) || context.hasEntity(twelfthAvenueID)) {\n continueTo(retryDelete); // changed something but roads still exist\n } else {\n continueTo(play);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retryDelete() {\n context.enter(modeBrowse(context));\n\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n reveal(box, helpHtml('intro.lines.retry_delete'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(multiSelect); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.lines.play', { next: t('intro.buildings.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-building',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addLine();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n d3_select(window).on('pointerdown.intro mousedown.intro', null, true);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilArrayUniq, utilRebind } from '../../util';\nimport { helpHtml, icon, pad, isMostlySquare, selectMenuItem, transitionTime } from './helper';\n\n\nexport function uiIntroBuilding(context, reveal) {\n var dispatch = d3_dispatch('done');\n var house = [-85.62815, 41.95638];\n var tank = [-85.62732, 41.95347];\n var buildingCatetory = presetManager.item('category-building');\n var housePreset = presetManager.item('building/house');\n var tankPreset = presetManager.item('man_made/storage_tank');\n var timeouts = [];\n var _houseID = null;\n var _tankID = null;\n\n\n var chapter = {\n title: 'intro.buildings.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function revealHouse(center, text, options) {\n var padding = 160 * Math.pow(2, context.map().zoom() - 20);\n var box = pad(center, padding, context);\n reveal(box, text, options);\n }\n\n\n function revealTank(center, text, options) {\n var padding = 190 * Math.pow(2, context.map().zoom() - 19.5);\n var box = pad(center, padding, context);\n reveal(box, text, options);\n }\n\n\n function addHouse() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n _houseID = null;\n\n var msec = transitionTime(house, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(house, 19, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-area',\n helpHtml('intro.buildings.add_building'));\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-buildings');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-area') return;\n continueTo(startHouse);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startHouse() {\n if (context.mode().id !== 'add-area') {\n return continueTo(addHouse);\n }\n\n _houseID = null;\n context.map().zoomEase(20, 500);\n\n timeout(function() {\n var startString = helpHtml('intro.buildings.start_building') +\n helpHtml('intro.buildings.building_corner_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap'));\n revealHouse(house, startString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealHouse(house, startString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-area') return chapter.restart();\n continueTo(continueHouse);\n });\n\n }, 550); // after easing\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continueHouse() {\n if (context.mode().id !== 'draw-area') {\n return continueTo(addHouse);\n }\n\n _houseID = null;\n\n var continueString = helpHtml('intro.buildings.continue_building') + '{br}' +\n helpHtml('intro.areas.finish_area_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.buildings.finish_building');\n\n revealHouse(house, continueString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealHouse(house, continueString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n return;\n } else if (mode.id === 'select') {\n var graph = context.graph();\n var way = context.entity(context.selectedIDs()[0]);\n var nodes = graph.childNodes(way);\n var points = utilArrayUniq(nodes)\n .map(function(n) { return context.projection(n.loc); });\n\n if (isMostlySquare(points)) {\n _houseID = way.id;\n return continueTo(chooseCategoryBuilding);\n } else {\n return continueTo(retryHouse);\n }\n\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function retryHouse() {\n var onClick = function() { continueTo(addHouse); };\n\n revealHouse(house, helpHtml('intro.buildings.retry_building'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n revealHouse(house, helpHtml('intro.buildings.retry_building'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function chooseCategoryBuilding() {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return addHouse();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {\n context.enter(modeSelect(context, [_houseID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n var button = context.container().select('.preset-category-building .preset-list-button');\n\n reveal(button.node(),\n helpHtml('intro.buildings.choose_category_building', { category: buildingCatetory.name() })\n );\n\n button.on('click.intro', function() {\n button.on('click.intro', null);\n continueTo(choosePresetHouse);\n });\n\n }, 400); // after preset list pane visible..\n\n\n context.on('enter.intro', function(mode) {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return continueTo(addHouse);\n }\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _houseID) {\n return continueTo(chooseCategoryBuilding);\n }\n });\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function choosePresetHouse() {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return addHouse();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {\n context.enter(modeSelect(context, [_houseID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n var button = context.container().select('.preset-building-house .preset-list-button');\n\n reveal(button.node(),\n helpHtml('intro.buildings.choose_preset_house', { preset: housePreset.name() }),\n { duration: 300 }\n );\n\n button.on('click.intro', function() {\n button.on('click.intro', null);\n continueTo(closeEditorHouse);\n });\n\n }, 400); // after preset list pane visible..\n\n context.on('enter.intro', function(mode) {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return continueTo(addHouse);\n }\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _houseID) {\n return continueTo(chooseCategoryBuilding);\n }\n });\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function closeEditorHouse() {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return addHouse();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {\n context.enter(modeSelect(context, [_houseID]));\n }\n\n context.history().checkpoint('hasHouse');\n\n context.on('exit.intro', function() {\n continueTo(rightClickHouse);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.buildings.close', { button: { html: icon('#iD-icon-close', 'inline') } })\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickHouse() {\n if (!_houseID) return chapter.restart();\n\n context.enter(modeBrowse(context));\n context.history().reset('hasHouse');\n var zoom = context.map().zoom();\n if (zoom < 20) {\n zoom = 20;\n }\n context.map().centerZoomEase(house, zoom, 500);\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== _houseID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'orthogonalize').node();\n if (!node) return;\n continueTo(clickSquare);\n }, 50); // after menu visible\n });\n\n context.map().on('move.intro drawn.intro', function() {\n var rightclickString = helpHtml('intro.buildings.' + (context.lastPointerType() === 'mouse' ? 'rightclick_building' : 'edit_menu_building_touch'));\n revealHouse(house, rightclickString, { duration: 0 });\n });\n\n context.history().on('change.intro', function() {\n continueTo(rightClickHouse);\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function clickSquare() {\n if (!_houseID) return chapter.restart();\n var entity = context.hasEntity(_houseID);\n if (!entity) return continueTo(rightClickHouse);\n\n var node = selectMenuItem(context, 'orthogonalize').node();\n if (!node) { return continueTo(rightClickHouse); }\n\n var wasChanged = false;\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.square_building'),\n { padding: 50 }\n );\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'browse') {\n continueTo(rightClickHouse);\n } else if (mode.id === 'move' || mode.id === 'rotate') {\n continueTo(retryClickSquare);\n }\n });\n\n context.map().on('move.intro', function() {\n var node = selectMenuItem(context, 'orthogonalize').node();\n if (!wasChanged && !node) { return continueTo(rightClickHouse); }\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.square_building'),\n { duration: 0, padding: 50 }\n );\n });\n\n context.history().on('change.intro', function() {\n wasChanged = true;\n context.history().on('change.intro', null);\n\n // Something changed. Wait for transition to complete and check undo annotation.\n timeout(function() {\n if (context.history().undoAnnotation() === t('operations.orthogonalize.annotation.feature', { n: 1 })) {\n continueTo(doneSquare);\n } else {\n continueTo(retryClickSquare);\n }\n }, 500); // after transitioned actions\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retryClickSquare() {\n context.enter(modeBrowse(context));\n\n revealHouse(house, helpHtml('intro.buildings.retry_square'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(rightClickHouse); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function doneSquare() {\n context.history().checkpoint('doneSquare');\n\n revealHouse(house, helpHtml('intro.buildings.done_square'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(addTank); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function addTank() {\n context.enter(modeBrowse(context));\n context.history().reset('doneSquare');\n _tankID = null;\n\n var msec = transitionTime(tank, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(tank, 19.5, msec);\n\n timeout(function() {\n reveal('button.add-area',\n helpHtml('intro.buildings.add_tank')\n );\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-area') return;\n continueTo(startTank);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startTank() {\n if (context.mode().id !== 'add-area') {\n return continueTo(addTank);\n }\n\n _tankID = null;\n\n timeout(function() {\n var startString = helpHtml('intro.buildings.start_tank') +\n helpHtml('intro.buildings.tank_edge_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap'));\n revealTank(tank, startString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealTank(tank, startString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-area') return chapter.restart();\n continueTo(continueTank);\n });\n\n }, 550); // after easing\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continueTank() {\n if (context.mode().id !== 'draw-area') {\n return continueTo(addTank);\n }\n\n _tankID = null;\n\n var continueString = helpHtml('intro.buildings.continue_tank') + '{br}' +\n helpHtml('intro.areas.finish_area_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.buildings.finish_tank');\n\n revealTank(tank, continueString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealTank(tank, continueString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n return;\n } else if (mode.id === 'select') {\n _tankID = context.selectedIDs()[0];\n return continueTo(searchPresetTank);\n } else {\n return continueTo(addTank);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function searchPresetTank() {\n if (!_tankID || !context.hasEntity(_tankID)) {\n return addTank();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _tankID) {\n context.enter(modeSelect(context, [_tankID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.buildings.search_tank', { preset: tankPreset.name() })\n );\n }, 400); // after preset list pane visible..\n\n context.on('enter.intro', function(mode) {\n if (!_tankID || !context.hasEntity(_tankID)) {\n return continueTo(addTank);\n }\n\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _tankID) {\n // keep the user's area selected..\n context.enter(modeSelect(context, [_tankID]));\n\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.buildings.search_tank', { preset: tankPreset.name() })\n );\n\n context.history().on('change.intro', null);\n }\n });\n\n function checkPresetSearch() {\n var first = context.container().select('.preset-list-item:first-child');\n\n if (first.classed('preset-man_made-storage_tank')) {\n reveal(first.select('.preset-list-button').node(),\n helpHtml('intro.buildings.choose_tank', { preset: tankPreset.name() }),\n { duration: 300 }\n );\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n\n context.history().on('change.intro', function() {\n continueTo(closeEditorTank);\n });\n }\n }\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n nextStep();\n }\n }\n\n\n function closeEditorTank() {\n if (!_tankID || !context.hasEntity(_tankID)) {\n return addTank();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _tankID) {\n context.enter(modeSelect(context, [_tankID]));\n }\n\n context.history().checkpoint('hasTank');\n\n context.on('exit.intro', function() {\n continueTo(rightClickTank);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.buildings.close', { button: { html: icon('#iD-icon-close', 'inline') } })\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickTank() {\n if (!_tankID) return continueTo(addTank);\n\n context.enter(modeBrowse(context));\n context.history().reset('hasTank');\n context.map().centerEase(tank, 500);\n\n timeout(function() {\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== _tankID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'circularize').node();\n if (!node) return;\n continueTo(clickCircle);\n }, 50); // after menu visible\n });\n\n var rightclickString = helpHtml('intro.buildings.' + (context.lastPointerType() === 'mouse' ? 'rightclick_tank' : 'edit_menu_tank_touch'));\n\n revealTank(tank, rightclickString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealTank(tank, rightclickString, { duration: 0 });\n });\n\n context.history().on('change.intro', function() {\n continueTo(rightClickTank);\n });\n\n }, 600);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function clickCircle() {\n if (!_tankID) return chapter.restart();\n var entity = context.hasEntity(_tankID);\n if (!entity) return continueTo(rightClickTank);\n\n var node = selectMenuItem(context, 'circularize').node();\n if (!node) { return continueTo(rightClickTank); }\n\n var wasChanged = false;\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.circle_tank'),\n { padding: 50 }\n );\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'browse') {\n continueTo(rightClickTank);\n } else if (mode.id === 'move' || mode.id === 'rotate') {\n continueTo(retryClickCircle);\n }\n });\n\n context.map().on('move.intro', function() {\n var node = selectMenuItem(context, 'circularize').node();\n if (!wasChanged && !node) { return continueTo(rightClickTank); }\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.circle_tank'),\n { duration: 0, padding: 50 }\n );\n });\n\n context.history().on('change.intro', function() {\n wasChanged = true;\n context.history().on('change.intro', null);\n\n // Something changed. Wait for transition to complete and check undo annotation.\n timeout(function() {\n if (context.history().undoAnnotation() === t('operations.circularize.annotation.feature', { n: 1 })) {\n continueTo(play);\n } else {\n continueTo(retryClickCircle);\n }\n }, 500); // after transitioned actions\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retryClickCircle() {\n context.enter(modeBrowse(context));\n\n revealTank(tank, helpHtml('intro.buildings.retry_circle'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(rightClickTank); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.buildings.play', { next: t('intro.startediting.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-startEditing',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addHouse();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n context.container().select('.more-fields .combobox-input').on('click.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { helpHtml } from './helper';\nimport { uiModal } from '../modal';\nimport { utilRebind } from '../../util/rebind';\n\n\nexport function uiIntroStartEditing(context, reveal) {\n var dispatch = d3_dispatch('done', 'startEditing');\n var modalSelection = d3_select(null);\n\n\n var chapter = {\n title: 'intro.startediting.title'\n };\n\n function showHelp() {\n reveal('.map-control.help-control',\n helpHtml('intro.startediting.help'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { shortcuts(); }\n }\n );\n }\n\n function shortcuts() {\n reveal('.map-control.help-control',\n helpHtml('intro.startediting.shortcuts'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { showSave(); }\n }\n );\n }\n\n function showSave() {\n context.container().selectAll('.shaded').remove(); // in case user opened keyboard shortcuts\n reveal('.top-toolbar button.save',\n helpHtml('intro.startediting.save'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { showStart(); }\n }\n );\n }\n\n function showStart() {\n context.container().selectAll('.shaded').remove(); // in case user opened keyboard shortcuts\n\n modalSelection = uiModal(context.container());\n\n modalSelection.select('.modal')\n .attr('class', 'modal-splash modal');\n\n modalSelection.selectAll('.close').remove();\n\n var startbutton = modalSelection.select('.content')\n .attr('class', 'fillL')\n .append('button')\n .attr('class', 'modal-section huge-modal-button')\n .on('click', function() {\n modalSelection.remove();\n });\n\n startbutton\n .append('svg')\n .attr('class', 'illustration')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n startbutton\n .append('h2')\n .call(t.append('intro.startediting.start'));\n\n dispatch.call('startEditing');\n }\n\n\n chapter.enter = function() {\n showHelp();\n };\n\n\n chapter.exit = function() {\n modalSelection.remove();\n context.container().selectAll('.shaded').remove(); // in case user opened keyboard shortcuts\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { t, localizer } from '../../core/localizer';\nimport { localize } from './helper';\n\nimport { prefs } from '../../core/preferences';\nimport { fileFetcher } from '../../core/file_fetcher';\nimport { coreGraph } from '../../core/graph';\nimport { modeBrowse } from '../../modes/browse';\nimport { osmEntity } from '../../osm/entity';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCurtain } from '../curtain';\nimport { utilArrayDifference, utilArrayUniq } from '../../util';\n\nimport { uiIntroWelcome } from './welcome';\nimport { uiIntroNavigation } from './navigation';\nimport { uiIntroPoint } from './point';\nimport { uiIntroArea } from './area';\nimport { uiIntroLine } from './line';\nimport { uiIntroBuilding } from './building';\nimport { uiIntroStartEditing } from './start_editing';\n\n\nconst chapterUi = {\n welcome: uiIntroWelcome,\n navigation: uiIntroNavigation,\n point: uiIntroPoint,\n area: uiIntroArea,\n line: uiIntroLine,\n building: uiIntroBuilding,\n startEditing: uiIntroStartEditing\n};\n\nconst chapterFlow = [\n 'welcome',\n 'navigation',\n 'point',\n 'area',\n 'line',\n 'building',\n 'startEditing'\n];\n\n\nexport function uiIntro(context) {\n const INTRO_IMAGERY = 'Bing';\n let _introGraph = {};\n let _currChapter;\n\n\n function intro(selection) {\n fileFetcher.get('intro_graph')\n .then(dataIntroGraph => {\n // create entities for intro graph and localize names\n for (let id in dataIntroGraph) {\n if (!_introGraph[id]) {\n _introGraph[id] = osmEntity(localize(dataIntroGraph[id]));\n }\n }\n selection.call(startIntro);\n })\n .catch(function() { /* ignore */ });\n }\n\n\n function startIntro(selection) {\n context.enter(modeBrowse(context));\n\n // Save current map state\n let osm = context.connection();\n let history = context.history().toJSON();\n let hash = window.location.hash;\n let center = context.map().center();\n let zoom = context.map().zoom();\n let background = context.background().baseLayerSource();\n let overlays = context.background().overlayLayerSources();\n let opacity = context.container().selectAll('.main-map .layer-background').style('opacity');\n let caches = osm && osm.caches();\n let baseEntities = context.history().graph().base().entities;\n\n // Show sidebar and disable the sidebar resizing button\n // (this needs to be before `context.inIntro(true)`)\n context.ui().sidebar.expand();\n context.container().selectAll('button.sidebar-toggle').classed('disabled', true);\n\n // Block saving\n context.inIntro(true);\n\n // Load semi-real data used in intro\n if (osm) { osm.toggle(false).reset(); }\n context.history().reset();\n context.history().merge(Object.values(coreGraph().load(_introGraph).entities));\n context.history().checkpoint('initial');\n\n // Setup imagery\n let imagery = context.background().findSource(INTRO_IMAGERY);\n if (imagery) {\n context.background().baseLayerSource(imagery);\n } else {\n context.background().bing();\n }\n overlays.forEach(d => context.background().toggleOverlayLayer(d));\n\n // Setup data layers (only OSM)\n let layers = context.layers();\n layers.all().forEach(item => {\n // if the layer has the function `enabled`\n if (typeof item.layer.enabled === 'function') {\n item.layer.enabled(item.id === 'osm');\n }\n });\n\n\n context.container().selectAll('.main-map .layer-background').style('opacity', 1);\n\n let curtain = uiCurtain(context.container().node());\n selection.call(curtain);\n\n // Store that the user started the walkthrough..\n prefs('walkthrough_started', 'yes');\n\n // Restore previous walkthrough progress..\n let storedProgress = prefs('walkthrough_progress') || '';\n let progress = storedProgress.split(';').filter(Boolean);\n\n let chapters = chapterFlow.map((chapter, i) => {\n let s = chapterUi[chapter](context, curtain.reveal)\n .on('done', () => {\n\n buttons\n .filter(d => d.title === s.title)\n .classed('finished', true);\n\n if (i < chapterFlow.length - 1) {\n const next = chapterFlow[i + 1];\n context.container().select(`button.chapter-${next}`)\n .classed('next', true);\n }\n\n // Store walkthrough progress..\n progress.push(chapter);\n prefs('walkthrough_progress', utilArrayUniq(progress).join(';'));\n });\n return s;\n });\n\n chapters[chapters.length - 1].on('startEditing', () => {\n // Store walkthrough progress..\n progress.push('startEditing');\n prefs('walkthrough_progress', utilArrayUniq(progress).join(';'));\n\n // Store if walkthrough is completed..\n let incomplete = utilArrayDifference(chapterFlow, progress);\n if (!incomplete.length) {\n prefs('walkthrough_completed', 'yes');\n }\n\n curtain.remove();\n navwrap.remove();\n context.container().selectAll('.main-map .layer-background').style('opacity', opacity);\n context.container().selectAll('button.sidebar-toggle').classed('disabled', false);\n if (osm) { osm.toggle(true).reset().caches(caches); }\n context.history().reset().merge(Object.values(baseEntities));\n context.background().baseLayerSource(background);\n overlays.forEach(d => context.background().toggleOverlayLayer(d));\n if (history) { context.history().fromJSON(history, false); }\n context.map().centerZoom(center, zoom);\n window.history.replaceState(null, '', hash);\n context.inIntro(false);\n });\n\n let navwrap = selection\n .append('div')\n .attr('class', 'intro-nav-wrap fillD');\n\n navwrap\n .append('svg')\n .attr('class', 'intro-nav-wrap-logo')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n let buttonwrap = navwrap\n .append('div')\n .attr('class', 'joined')\n .selectAll('button.chapter');\n\n let buttons = buttonwrap\n .data(chapters)\n .enter()\n .append('button')\n .attr('class', (d, i) => `chapter chapter-${chapterFlow[i]}`)\n .on('click', enterChapter);\n\n buttons\n .append('span')\n .html(d => t.html(d.title));\n\n buttons\n .append('span')\n .attr('class', 'status')\n .call(svgIcon((localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'));\n\n enterChapter(null, chapters[0]);\n\n\n function enterChapter(d3_event, newChapter) {\n if (_currChapter) { _currChapter.exit(); }\n context.enter(modeBrowse(context));\n\n _currChapter = newChapter;\n _currChapter.enter();\n\n buttons\n .classed('next', false)\n .classed('active', d => d.title === _currChapter.title);\n }\n }\n\n\n return intro;\n}\n", "export { uiIntro } from './intro';\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../core/preferences';\nimport { svgIcon } from '../svg/icon';\nimport { t } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\n\n\nexport function uiIssuesInfo(context) {\n\n var warningsItem = {\n id: 'warnings',\n count: 0,\n iconID: 'iD-icon-alert',\n descriptionID: 'issues.warnings_and_errors'\n };\n\n var resolvedItem = {\n id: 'resolved',\n count: 0,\n iconID: 'iD-icon-apply',\n descriptionID: 'issues.user_resolved_issues'\n };\n\n function update(selection) {\n\n var shownItems = [];\n\n var liveIssues = context.validator().getIssues({\n what: prefs('validate-what') || 'edited',\n where: prefs('validate-where') || 'all'\n }).filter(issue => issue.severity !== 'suggestion');\n\n if (liveIssues.length) {\n warningsItem.count = liveIssues.length;\n shownItems.push(warningsItem);\n }\n\n if (prefs('validate-what') === 'all') {\n var resolvedIssues = context.validator().getResolvedIssues();\n if (resolvedIssues.length) {\n resolvedItem.count = resolvedIssues.length;\n shownItems.push(resolvedItem);\n }\n }\n\n var chips = selection.selectAll('.chip')\n .data(shownItems, function(d) {\n return d.id;\n });\n\n chips.exit().remove();\n\n var enter = chips.enter()\n .append('a')\n .attr('class', function(d) {\n return 'chip ' + d.id + '-count';\n })\n .attr('href', '#')\n .each(function(d) {\n\n var chipSelection = d3_select(this);\n\n var tooltipBehavior = uiTooltip()\n .placement('top')\n .title(() => t.append(d.descriptionID));\n\n chipSelection\n .call(tooltipBehavior)\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n\n tooltipBehavior.hide(d3_select(this));\n // open the Issues pane\n context.ui().togglePanes(context.container().select('.map-panes .issues-pane'));\n });\n\n chipSelection.call(svgIcon('#' + d.iconID));\n\n });\n\n enter.append('span')\n .attr('class', 'count');\n\n enter.merge(chips)\n .selectAll('span.count')\n .text(function(d) {\n return d.count.toString();\n });\n }\n\n\n return function(selection) {\n update(selection);\n\n context.validator().on('validated.infobox', function() {\n update(selection);\n });\n };\n}\n", "/**\n * IntervalTasksQueue\n * Enabled task execution under interval limit\n */\nexport class IntervalTasksQueue {\n readonly intervalInMs: number;\n pendingHandles: ReturnType[];\n time: number;\n\n /**\n * Interval in milliseconds inside which only 1 task can execute.\n * e.g. if interval is 200ms, and 5 async tasks are unqueued,\n * they will complete in ~1s if not cleared\n * @param {number} intervalInMs\n */\n constructor(intervalInMs: number) {\n this.intervalInMs = intervalInMs;\n this.pendingHandles = [];\n this.time = 0;\n }\n\n enqueue(task: () => void) {\n let taskTimeout = this.time;\n this.time += this.intervalInMs;\n this.pendingHandles.push(setTimeout(() => {\n this.time -= this.intervalInMs;\n task();\n }, taskTimeout));\n }\n\n clear() {\n this.pendingHandles.forEach((timeoutHandle) => {\n clearTimeout(timeoutHandle);\n });\n this.pendingHandles = [];\n this.time = 0;\n }\n}\n", "import { geoArea as d3_geoArea, geoMercatorRaw as d3_geoMercatorRaw } from 'd3-geo';\nimport { json as d3_json } from 'd3-fetch';\n\nimport { t, localizer } from '../core/localizer';\nimport { geoExtent, geoSphericalDistance } from '../geo';\nimport { utilQsString, utilStringQs } from '../util';\nimport { utilAesDecrypt } from '../util/aes';\nimport { IntervalTasksQueue } from '../util/IntervalTasksQueue';\nimport { localeDateString } from '../util/date';\n\nvar isRetina = window.devicePixelRatio && window.devicePixelRatio >= 2;\n\n// listen for DPI change, e.g. when dragging a browser window from a retina to non-retina screen\nwindow.matchMedia?.(`\n (-webkit-min-device-pixel-ratio: 2), /* Safari */\n (min-resolution: 2dppx), /* standard */\n (min-resolution: 192dpi) /* fallback */\n `).addListener(function() {\n\n isRetina = window.devicePixelRatio && window.devicePixelRatio >= 2;\n});\n\n\nfunction vintageRange(vintage) {\n var s;\n if (vintage.start || vintage.end) {\n s = (vintage.start || '?');\n if (vintage.start !== vintage.end) {\n s += ' - ' + (vintage.end || '?');\n }\n }\n return s;\n}\n\n\nexport function rendererBackgroundSource(data) {\n var source = Object.assign({}, data); // shallow copy\n var _offset = [0, 0];\n var _name = source.name;\n var _description = source.description;\n var _best = !!source.best;\n var _template = source.encrypted ? utilAesDecrypt(source.template) : source.template;\n\n source.tileSize = data.tileSize || 256;\n source.zoomExtent = data.zoomExtent || [0, 22];\n source.overzoom = data.overzoom !== false;\n\n source.offset = function(val) {\n if (!arguments.length) return _offset;\n _offset = val;\n return source;\n };\n\n\n source.nudge = function(val, zoomlevel) {\n _offset[0] += val[0] / Math.pow(2, zoomlevel);\n _offset[1] += val[1] / Math.pow(2, zoomlevel);\n return source;\n };\n\n\n source.name = function() {\n var id_safe = source.id.replace(/\\./g, '');\n return t('imagery.' + id_safe + '.name', { default: _name });\n };\n\n\n source.label = function() {\n var id_safe = source.id.replace(/\\./g, '');\n return t.append('imagery.' + id_safe + '.name', { default: _name });\n };\n\n\n source.hasDescription = function() {\n var id_safe = source.id.replace(/\\./g, '');\n var descriptionText = localizer.tInfo('imagery.' + id_safe + '.description', { default: escape(_description) }).texts.join('');\n return !!descriptionText;\n };\n\n\n source.description = function() {\n var id_safe = source.id.replace(/\\./g, '');\n return t.append('imagery.' + id_safe + '.description', { default: _description });\n };\n\n\n source.best = function() {\n return _best;\n };\n\n\n source.area = function() {\n if (!data.polygon) return Number.MAX_VALUE; // worldwide\n var area = d3_geoArea({ type: 'MultiPolygon', coordinates: [ data.polygon ] });\n return isNaN(area) ? 0 : area;\n };\n\n\n source.imageryUsed = function() {\n return _name || source.id;\n };\n\n\n source.template = function(val) {\n if (!arguments.length) return _template;\n if (source.id === 'custom' || source.id === 'Bing') {\n _template = val;\n }\n return source;\n };\n\n\n source.url = function(coord) {\n var result = _template.replace(/#[\\s\\S]*/u, ''); // strip hash part of URL\n if (result === '') return result; // source 'none'\n\n\n // Guess a type based on the tokens present in the template\n // (This is for 'custom' source, where we don't know)\n if (!source.type || source.id === 'custom') {\n if (/SERVICE=WMS|\\{(proj|wkid|bbox)\\}/.test(result)) {\n source.type = 'wms';\n source.projection = 'EPSG:3857'; // guess\n } else if (/\\{(x|y)\\}/.test(result)) {\n source.type = 'tms';\n } else if (/\\{u\\}/.test(result)) {\n source.type = 'bing';\n }\n }\n\n\n if (source.type === 'wms') {\n var tileToProjectedCoords = (function(x, y, z) {\n var zoomSize = Math.pow(2, z);\n var lon = x / zoomSize * Math.PI * 2 - Math.PI;\n var lat = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / zoomSize)));\n\n switch (source.projection) {\n case 'EPSG:4326':\n return {\n x: lon * 180 / Math.PI,\n y: lat * 180 / Math.PI\n };\n default: // EPSG:3857 and synonyms\n var mercCoords = d3_geoMercatorRaw(lon, lat);\n return {\n x: 20037508.34 / Math.PI * mercCoords[0],\n y: 20037508.34 / Math.PI * mercCoords[1]\n };\n }\n });\n\n var tileSize = source.tileSize;\n var projection = source.projection;\n var minXmaxY = tileToProjectedCoords(coord[0], coord[1], coord[2]);\n var maxXminY = tileToProjectedCoords(coord[0]+1, coord[1]+1, coord[2]);\n\n result = result.replace(/\\{(\\w+)\\}/g, function (token, key) {\n switch (key) {\n case 'width':\n case 'height':\n return tileSize;\n case 'proj':\n return projection;\n case 'wkid':\n return projection.replace(/^EPSG:/, '');\n case 'bbox':\n // WMS 1.3 flips x/y for some coordinate systems including EPSG:4326 - #7557\n if (projection === 'EPSG:4326' &&\n // The CRS parameter implies version 1.3 (prior versions use SRS)\n /VERSION=1.3|CRS={proj}/.test(source.template().toUpperCase())) {\n return maxXminY.y + ',' + minXmaxY.x + ',' + minXmaxY.y + ',' + maxXminY.x;\n } else {\n return minXmaxY.x + ',' + maxXminY.y + ',' + maxXminY.x + ',' + minXmaxY.y;\n }\n case 'w':\n return minXmaxY.x;\n case 's':\n return maxXminY.y;\n case 'n':\n return maxXminY.x;\n case 'e':\n return minXmaxY.y;\n default:\n return token;\n }\n });\n\n } else if (source.type === 'tms') {\n result = result\n .replace('{x}', coord[0])\n .replace('{y}', coord[1])\n // TMS-flipped y coordinate\n .replace(/\\{[t-]y\\}/, Math.pow(2, coord[2]) - coord[1] - 1)\n .replace(/\\{z(oom)?\\}/, coord[2])\n // only fetch retina tiles for retina screens\n .replace(/\\{@2x\\}|\\{r\\}/, isRetina ? '@2x' : '');\n\n } else if (source.type === 'bing') {\n result = result\n .replace('{u}', function() {\n var u = '';\n for (var zoom = coord[2]; zoom > 0; zoom--) {\n var b = 0;\n var mask = 1 << (zoom - 1);\n if ((coord[0] & mask) !== 0) b++;\n if ((coord[1] & mask) !== 0) b += 2;\n u += b.toString();\n }\n return u;\n });\n }\n\n // these apply to any type..\n result = result.replace(/\\{switch:([^}]+)\\}/, function(s, r) {\n var subdomains = r.split(',');\n return subdomains[(coord[0] + coord[1]) % subdomains.length];\n });\n\n\n return result;\n };\n\n\n source.validZoom = function(z, underzoom) {\n if (underzoom === undefined) underzoom = 0;\n return source.zoomExtent[0] - underzoom <= z &&\n (source.overzoom || source.zoomExtent[1] > z);\n };\n\n\n source.isLocatorOverlay = function() {\n return source.id === 'mapbox_locator_overlay';\n };\n\n\n /* hides a source from the list, but leaves it available for use */\n source.isHidden = function() {\n return false; // currently there are no hidden layers\n };\n\n\n source.copyrightNotices = function() {};\n\n\n source.getMetadata = function(center, tileCoord, callback) {\n var vintage = {\n start: localeDateString(source.startDate),\n end: localeDateString(source.endDate)\n };\n vintage.range = vintageRange(vintage);\n\n var metadata = { vintage: vintage };\n callback(null, metadata);\n };\n\n\n return source;\n}\n\n\nrendererBackgroundSource.Bing = function(data, dispatch) {\n // https://docs.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata\n // https://docs.microsoft.com/en-us/bingmaps/rest-services/directly-accessing-the-bing-maps-tiles\n\n //fallback url template\n data.template = 'https://ecn.t{switch:0,1,2,3}.tiles.virtualearth.net/tiles/a{u}.jpeg?g=1&pr=odbl&n=z';\n\n var bing = rendererBackgroundSource(data);\n var key = utilAesDecrypt('5c875730b09c6b422433e807e1ff060b6536c791dbfffcffc4c6b18a1bdba1f14593d151adb50e19e1be1ab19aef813bf135d0f103475e5c724dec94389e45d0');\n /*\n missing tile image strictness param (n=)\n \u2022\tn=f -> (Fail) returns a 404\n \u2022\tn=z -> (Empty) returns a 200 with 0 bytes (no content)\n \u2022\tn=t -> (Transparent) returns a 200 with a transparent (png) tile\n */\n const strictParam = 'n';\n\n var url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/AerialOSM?include=ImageryProviders&uriScheme=https&key=' + key;\n var cache = {};\n var inflight = {};\n var providers = [];\n var taskQueue = new IntervalTasksQueue(250);\n var metadataLastZoom = -1;\n\n d3_json(url)\n .then(function(json) {\n let imageryResource = json.resourceSets[0].resources[0];\n\n //retrieve and prepare up to date imagery template\n let template = imageryResource.imageUrl; //https://ecn.{subdomain}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=10339\n let subDomains = imageryResource.imageUrlSubdomains; //[\"t0, t1, t2, t3\"]\n let subDomainNumbers = subDomains.map((subDomain) => {\n return subDomain.substring(1);\n } ).join(',');\n\n template = template.replace('{subdomain}', `t{switch:${subDomainNumbers}}`).replace('{quadkey}', '{u}');\n if (!new URLSearchParams(template).has(strictParam)){\n template += `&${strictParam}=z`;\n }\n bing.template(template);\n\n providers = imageryResource.imageryProviders.map(function(provider) {\n return {\n attribution: provider.attribution,\n areas: provider.coverageAreas.map(function(area) {\n return {\n zoom: [area.zoomMin, area.zoomMax],\n extent: geoExtent([area.bbox[1], area.bbox[0]], [area.bbox[3], area.bbox[2]])\n };\n })\n };\n });\n dispatch.call('change');\n })\n .catch(function() {\n /* ignore */\n });\n\n\n bing.copyrightNotices = function(zoom, extent) {\n zoom = Math.min(zoom, 21);\n return providers.filter(function(provider) {\n return provider.areas.some(function(area) {\n return extent.intersects(area.extent) &&\n area.zoom[0] <= zoom &&\n area.zoom[1] >= zoom;\n });\n }).map(function(provider) {\n return provider.attribution;\n }).join(', ');\n };\n\n\n bing.getMetadata = function(center, tileCoord, callback) {\n var tileID = tileCoord.slice(0, 3).join('/');\n var zoom = Math.min(tileCoord[2], 21);\n var centerPoint = center[1] + ',' + center[0]; // lat,lng\n var url = 'https://dev.virtualearth.net/REST/v1/Imagery/BasicMetadata/AerialOSM/' + centerPoint +\n '?zl=' + zoom + '&key=' + key;\n\n if (inflight[tileID]) return;\n\n if (!cache[tileID]) {\n cache[tileID] = {};\n }\n if (cache[tileID] && cache[tileID].metadata) {\n return callback(null, cache[tileID].metadata);\n }\n\n inflight[tileID] = true;\n\n if (metadataLastZoom !== tileCoord[2]){\n metadataLastZoom = tileCoord[2];\n taskQueue.clear();\n }\n\n taskQueue.enqueue(() => {\n d3_json(url)\n .then(function (result) {\n delete inflight[tileID];\n if (!result) {\n throw new Error('Unknown Error');\n }\n var vintage = {\n start: localeDateString(result.resourceSets[0].resources[0].vintageStart),\n end: localeDateString(result.resourceSets[0].resources[0].vintageEnd)\n };\n vintage.range = vintageRange(vintage);\n\n var metadata = { vintage: vintage };\n cache[tileID].metadata = metadata;\n if (callback) callback(null, metadata);\n })\n .catch(function (err) {\n delete inflight[tileID];\n if (callback) callback(err.message);\n });\n });\n };\n\n\n bing.terms_url = 'https://blog.openstreetmap.org/2010/11/30/microsoft-imagery-details';\n\n\n return bing;\n};\n\n\n\nrendererBackgroundSource.Esri = function(data) {\n // in addition to using the tilemap at zoom level 20, overzoom real tiles - #4327 (deprecated technique, but it works)\n if (data.template.match(/blankTile/) === null) {\n data.template = data.template + '?blankTile=false';\n }\n\n var esri = rendererBackgroundSource(data);\n var cache = {};\n var inflight = {};\n var _prevCenter;\n\n // use a tilemap service to set maximum zoom for esri tiles dynamically\n // https://developers.arcgis.com/documentation/tiled-elevation-service/\n esri.fetchTilemap = function(center) {\n // skip if we have already fetched a tilemap within 5km\n if (_prevCenter && geoSphericalDistance(center, _prevCenter) < 5000) return;\n _prevCenter = center;\n\n // tiles are available globally to zoom level 19, afterward they may or may not be present\n var z = 20;\n\n // first generate a random url using the template\n var dummyUrl = esri.url([1,2,3]);\n\n // calculate url z/y/x from the lat/long of the center of the map\n var x = (Math.floor((center[0] + 180) / 360 * Math.pow(2, z)));\n var y = (Math.floor((1 - Math.log(Math.tan(center[1] * Math.PI / 180) + 1 / Math.cos(center[1] * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, z)));\n\n // fetch an 8x8 grid to leverage cache\n var tilemapUrl = dummyUrl.replace(/tile\\/[0-9]+\\/[0-9]+\\/[0-9]+\\?blankTile=false/, 'tilemap') + '/' + z + '/' + y + '/' + x + '/8/8';\n\n // make the request and introspect the response from the tilemap server\n d3_json(tilemapUrl)\n .then(function(tilemap) {\n if (!tilemap) {\n throw new Error('Unknown Error');\n }\n var hasTiles = true;\n for (var i = 0; i < tilemap.data.length; i++) {\n // 0 means an individual tile in the grid doesn't exist\n if (!tilemap.data[i]) {\n hasTiles = false;\n break;\n }\n }\n\n // if any tiles are missing at level 20 we restrict maxZoom to 19\n esri.zoomExtent[1] = (hasTiles ? 22 : 19);\n })\n .catch(function() {\n /* ignore */\n });\n };\n\n\n esri.getMetadata = function(center, tileCoord, callback) {\n let mapServerUrl = esri.metadata;\n if (esri.id === 'EsriWorldImagery') {\n mapServerUrl = 'https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer';\n }\n if (!mapServerUrl) {\n // rest endpoint is not available for ESRI's \"clarity\" imagery\n return callback(null, {});\n }\n var tileID = tileCoord.slice(0, 3).join('/');\n var zoom = Math.min(tileCoord[2], esri.zoomExtent[1]);\n var centerPoint = center[0] + ',' + center[1]; // long, lat (as it should be)\n var unknown = t('info_panels.background.unknown');\n var vintage = {};\n var metadata = {};\n\n if (inflight[tileID]) return;\n\n // build up query using the layer appropriate to the current zoom\n var url = mapServerUrl + '/4/query';\n url += '?returnGeometry=false&geometry=' + centerPoint + '&inSR=4326&geometryType=esriGeometryPoint&outFields=*&f=json';\n\n if (!cache[tileID]) {\n cache[tileID] = {};\n }\n if (cache[tileID] && cache[tileID].metadata) {\n return callback(null, cache[tileID].metadata);\n }\n\n inflight[tileID] = true;\n d3_json(url)\n .then(function(result) {\n delete inflight[tileID];\n\n result = result.features.map(f => f.attributes)\n .filter(a => a.MinMapLevel <= zoom && a.MaxMapLevel >= zoom)[0];\n\n if (!result) {\n throw new Error('Unknown Error');\n } else if (result.features && result.features.length < 1) {\n throw new Error('No Results');\n } else if (result.error && result.error.message) {\n throw new Error(result.error.message);\n }\n\n // pass through the discrete capture date from metadata\n var captureDate = localeDateString(result.SRC_DATE2);\n vintage = {\n start: captureDate,\n end: captureDate,\n range: captureDate\n };\n metadata = {\n vintage: vintage,\n source: clean(result.NICE_NAME),\n description: clean(result.NICE_DESC),\n resolution: clean(+Number(result.SRC_RES).toFixed(4)),\n accuracy: clean(+Number(result.SRC_ACC).toFixed(4))\n };\n\n // append units - meters\n if (isFinite(metadata.resolution)) {\n metadata.resolution += ' m';\n }\n if (isFinite(metadata.accuracy)) {\n metadata.accuracy += ' m';\n }\n\n cache[tileID].metadata = metadata;\n if (callback) callback(null, metadata);\n })\n .catch(function(err) {\n delete inflight[tileID];\n if (callback) callback(err.message);\n });\n\n\n function clean(val) {\n return String(val).trim() || unknown;\n }\n };\n\n return esri;\n};\n\n\nrendererBackgroundSource.None = function() {\n var source = rendererBackgroundSource({ id: 'none', template: '' });\n\n\n source.name = function() {\n return t('background.none');\n };\n\n\n source.label = function() {\n return t.append('background.none');\n };\n\n\n source.imageryUsed = function() {\n return null;\n };\n\n\n source.area = function() {\n return -1; // sources in background pane are sorted by area\n };\n\n\n return source;\n};\n\n\nrendererBackgroundSource.Custom = function(template) {\n var source = rendererBackgroundSource({ id: 'custom', template: template });\n\n\n source.name = function() {\n return t('background.custom');\n };\n\n source.label = function() {\n return t.append('background.custom');\n };\n\n\n source.imageryUsed = function() {\n // sanitize personal connection tokens - #6801\n var cleaned = source.template();\n\n // from query string parameters\n if (cleaned.indexOf('?') !== -1) {\n var parts = cleaned.split('?', 2);\n var qs = utilStringQs(parts[1]);\n\n ['access_token', 'connectId', 'token', 'Signature'].forEach(function(param) {\n if (qs[param]) {\n qs[param] = '{apikey}';\n }\n });\n cleaned = parts[0] + '?' + utilQsString(qs, true); // true = soft encode\n }\n\n // from wms/wmts api path parameters\n cleaned = cleaned\n .replace(/token\\/(\\w+)/, 'token/{apikey}')\n .replace(/key=(\\w+)/, 'key={apikey}');\n return 'Custom (' + cleaned + ' )';\n };\n\n\n source.area = function() {\n return -2; // sources in background pane are sorted by area\n };\n\n\n return source;\n};\n", "import { feature, point, lineString, isObject } from \"@turf/helpers\";\nimport {\n Point,\n LineString,\n Polygon,\n MultiLineString,\n MultiPolygon,\n FeatureCollection,\n Feature,\n Geometry,\n GeometryObject,\n GeometryCollection,\n GeoJsonProperties,\n BBox,\n GeoJsonTypes,\n} from \"geojson\";\nimport { AllGeoJSON, Lines, Id } from \"@turf/helpers\";\n\n/**\n * Callback for coordEach\n *\n * @callback coordEachCallback\n * @param {number[]} currentCoord The current coordinate being processed.\n * @param {number} coordIndex The current index of the coordinate being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over coordinates in any GeoJSON object, similar to Array.forEach()\n *\n * @function\n * @param {AllGeoJSON} geojson any GeoJSON object\n * @param {coordEachCallback} callback a method that takes (currentCoord, coordIndex, featureIndex, multiFeatureIndex)\n * @param {boolean} [excludeWrapCoord=false] whether or not to include the final coordinate of LinearRings that wraps the ring in its iteration.\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {\"foo\": \"bar\"}),\n * turf.point([36, 53], {\"hello\": \"world\"})\n * ]);\n *\n * turf.coordEach(features, function (currentCoord, coordIndex, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=currentCoord\n * //=coordIndex\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * });\n */\nfunction coordEach(\n geojson: AllGeoJSON,\n callback: (\n currentCoord: number[],\n coordIndex: number,\n featureIndex: number,\n multiFeatureIndex: number,\n geometryIndex: number\n ) => void,\n excludeWrapCoord?: boolean\n): void {\n // Handles null Geometry -- Skips this GeoJSON\n if (geojson === null) return;\n var j,\n k,\n l,\n geometry,\n stopG,\n coords,\n geometryMaybeCollection,\n wrapShrink = 0,\n coordIndex = 0,\n isGeometryCollection,\n type = geojson.type,\n isFeatureCollection = type === \"FeatureCollection\",\n isFeature = type === \"Feature\",\n // @ts-expect-error: Known type conflict\n stop = isFeatureCollection ? geojson.features.length : 1;\n\n // This logic may look a little weird. The reason why it is that way\n // is because it's trying to be fast. GeoJSON supports multiple kinds\n // of objects at its root: FeatureCollection, Features, Geometries.\n // This function has the responsibility of handling all of them, and that\n // means that some of the `for` loops you see below actually just don't apply\n // to certain inputs. For instance, if you give this just a\n // Point geometry, then both loops are short-circuited and all we do\n // is gradually rename the input until it's called 'geometry'.\n //\n // This also aims to allocate as few resources as possible: just a\n // few numbers and booleans, rather than any temporary arrays as would\n // be required with the normalization approach.\n for (var featureIndex = 0; featureIndex < stop; featureIndex++) {\n geometryMaybeCollection = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[featureIndex].geometry\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.geometry\n : geojson;\n isGeometryCollection = geometryMaybeCollection\n ? geometryMaybeCollection.type === \"GeometryCollection\"\n : false;\n stopG = isGeometryCollection\n ? geometryMaybeCollection.geometries.length\n : 1;\n\n for (var geomIndex = 0; geomIndex < stopG; geomIndex++) {\n var multiFeatureIndex = 0;\n var geometryIndex = 0;\n geometry = isGeometryCollection\n ? geometryMaybeCollection.geometries[geomIndex]\n : geometryMaybeCollection;\n\n // Handles null Geometry -- Skips this geometry\n if (geometry === null) continue;\n coords = geometry.coordinates;\n var geomType = geometry.type;\n\n wrapShrink =\n excludeWrapCoord &&\n (geomType === \"Polygon\" || geomType === \"MultiPolygon\")\n ? 1\n : 0;\n\n switch (geomType) {\n case null:\n break;\n case \"Point\":\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords,\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n multiFeatureIndex++;\n break;\n case \"LineString\":\n case \"MultiPoint\":\n for (j = 0; j < coords.length; j++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords[j],\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n if (geomType === \"MultiPoint\") multiFeatureIndex++;\n }\n if (geomType === \"LineString\") multiFeatureIndex++;\n break;\n case \"Polygon\":\n case \"MultiLineString\":\n for (j = 0; j < coords.length; j++) {\n for (k = 0; k < coords[j].length - wrapShrink; k++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords[j][k],\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n }\n if (geomType === \"MultiLineString\") multiFeatureIndex++;\n if (geomType === \"Polygon\") geometryIndex++;\n }\n if (geomType === \"Polygon\") multiFeatureIndex++;\n break;\n case \"MultiPolygon\":\n for (j = 0; j < coords.length; j++) {\n geometryIndex = 0;\n for (k = 0; k < coords[j].length; k++) {\n for (l = 0; l < coords[j][k].length - wrapShrink; l++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords[j][k][l],\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n }\n geometryIndex++;\n }\n multiFeatureIndex++;\n }\n break;\n case \"GeometryCollection\":\n for (j = 0; j < geometry.geometries.length; j++)\n if (\n // @ts-expect-error: Known type conflict\n coordEach(geometry.geometries[j], callback, excludeWrapCoord) ===\n false\n )\n // @ts-expect-error: Known type conflict\n return false;\n break;\n default:\n throw new Error(\"Unknown Geometry Type\");\n }\n }\n }\n}\n\n/**\n * Callback for coordReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback coordReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {number[]} currentCoord The current coordinate being processed.\n * @param {number} coordIndex The current index of the coordinate being processed.\n * Starts at index 0, if an initialValue is provided, and at index 1 otherwise.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce coordinates in any GeoJSON object, similar to Array.reduce()\n *\n * @function\n * @param {AllGeoJSON} geojson any GeoJSON object\n * @param {coordReduceCallback} callback a method that takes (previousValue, currentCoord, coordIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @param {boolean} [excludeWrapCoord=false] whether or not to include the final coordinate of LinearRings that wraps the ring in its iteration.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {\"foo\": \"bar\"}),\n * turf.point([36, 53], {\"hello\": \"world\"})\n * ]);\n *\n * turf.coordReduce(features, function (previousValue, currentCoord, coordIndex, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=previousValue\n * //=currentCoord\n * //=coordIndex\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * return currentCoord;\n * });\n */\nfunction coordReduce(\n geojson: AllGeoJSON,\n callback: (\n previousValue: Reducer,\n currentCoord: number[],\n coordIndex: number,\n featureIndex: number,\n multiFeatureIndex: number,\n geometryIndex: number\n ) => Reducer,\n initialValue?: Reducer,\n excludeWrapCoord?: boolean\n): Reducer {\n var previousValue = initialValue;\n coordEach(\n geojson,\n function (\n currentCoord,\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) {\n if (coordIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentCoord;\n else\n previousValue = callback(\n // @ts-expect-error: Known type conflict\n previousValue,\n currentCoord,\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n );\n },\n excludeWrapCoord\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for propEach\n *\n * @callback propEachCallback\n * @param {GeoJsonProperties} currentProperties The current Properties being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over properties in any GeoJSON object, similar to Array.forEach()\n *\n * @function\n * @param {FeatureCollection|Feature} geojson any GeoJSON object\n * @param {propEachCallback} callback a method that takes (currentProperties, featureIndex)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.propEach(features, function (currentProperties, featureIndex) {\n * //=currentProperties\n * //=featureIndex\n * });\n */\nfunction propEach(\n geojson: Feature | FeatureCollection | Feature,\n callback: (currentProperties: Props, featureIndex: number) => void\n): void {\n var i;\n switch (geojson.type) {\n case \"FeatureCollection\":\n for (i = 0; i < geojson.features.length; i++) {\n // @ts-expect-error: Known type conflict\n if (callback(geojson.features[i].properties, i) === false) break;\n }\n break;\n case \"Feature\":\n // @ts-expect-error: Known type conflict\n callback(geojson.properties, 0);\n break;\n }\n}\n\n/**\n * Callback for propReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback propReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {GeoJsonProperties} currentProperties The current Properties being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce properties in any GeoJSON object into a single value,\n * similar to how Array.reduce works. However, in this case we lazily run\n * the reduction, so an array of all properties is unnecessary.\n *\n * @function\n * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON object\n * @param {propReduceCallback} callback a method that takes (previousValue, currentProperties, featureIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.propReduce(features, function (previousValue, currentProperties, featureIndex) {\n * //=previousValue\n * //=currentProperties\n * //=featureIndex\n * return currentProperties\n * });\n */\nfunction propReduce(\n geojson: Feature | FeatureCollection | Geometry,\n callback: (\n previousValue: Reducer,\n currentProperties: P,\n featureIndex: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n // @ts-expect-error: Known type conflict\n propEach(geojson, function (currentProperties, featureIndex) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentProperties;\n else\n // @ts-expect-error: Known type conflict\n previousValue = callback(previousValue, currentProperties, featureIndex);\n });\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for featureEach\n *\n * @callback featureEachCallback\n * @param {Feature} currentFeature The current Feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over features in any GeoJSON object, similar to\n * Array.forEach.\n *\n * @function\n * @param {FeatureCollection|Feature|Feature} geojson any GeoJSON object\n * @param {featureEachCallback} callback a method that takes (currentFeature, featureIndex)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.featureEach(features, function (currentFeature, featureIndex) {\n * //=currentFeature\n * //=featureIndex\n * });\n */\nfunction featureEach<\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | Feature,\n callback: (currentFeature: Feature, featureIndex: number) => void\n): void {\n if (geojson.type === \"Feature\") {\n // @ts-expect-error: Known type conflict\n callback(geojson, 0);\n } else if (geojson.type === \"FeatureCollection\") {\n for (var i = 0; i < geojson.features.length; i++) {\n // @ts-expect-error: Known type conflict\n if (callback(geojson.features[i], i) === false) break;\n }\n }\n}\n\n/**\n * Callback for featureReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback featureReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentFeature The current Feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce features in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|Feature} geojson any GeoJSON object\n * @param {featureReduceCallback} callback a method that takes (previousValue, currentFeature, featureIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {\"foo\": \"bar\"}),\n * turf.point([36, 53], {\"hello\": \"world\"})\n * ]);\n *\n * turf.featureReduce(features, function (previousValue, currentFeature, featureIndex) {\n * //=previousValue\n * //=currentFeature\n * //=featureIndex\n * return currentFeature\n * });\n */\nfunction featureReduce<\n Reducer,\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | Feature,\n callback: (\n previousValue: Reducer,\n currentFeature: Feature,\n featureIndex: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n featureEach(geojson, function (currentFeature, featureIndex) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentFeature;\n // @ts-expect-error: Known type conflict\n else previousValue = callback(previousValue, currentFeature, featureIndex);\n });\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Get all coordinates from any GeoJSON object.\n *\n * @function\n * @param {AllGeoJSON} geojson any GeoJSON object\n * @returns {Array>} coordinate position array\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * var coords = turf.coordAll(features);\n * //= [[26, 37], [36, 53]]\n */\nfunction coordAll(geojson: AllGeoJSON): number[][] {\n // @ts-expect-error: Known type conflict\n var coords = [];\n coordEach(geojson, function (coord) {\n coords.push(coord);\n });\n // @ts-expect-error: Known type conflict\n return coords;\n}\n\n/**\n * Callback for geomEach\n *\n * @callback geomEachCallback\n * @param {GeometryObject} currentGeometry The current Geometry being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {GeoJsonProperties} featureProperties The current Feature Properties being processed.\n * @param {BBox} featureBBox The current Feature BBox being processed.\n * @param {Id} featureId The current Feature Id being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over each geometry in any GeoJSON object, similar to Array.forEach()\n *\n * @function\n * @param {FeatureCollection|Feature|Geometry|GeometryObject|Feature} geojson any GeoJSON object\n * @param {geomEachCallback} callback a method that takes (currentGeometry, featureIndex, featureProperties, featureBBox, featureId)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.geomEach(features, function (currentGeometry, featureIndex, featureProperties, featureBBox, featureId) {\n * //=currentGeometry\n * //=featureIndex\n * //=featureProperties\n * //=featureBBox\n * //=featureId\n * });\n */\nfunction geomEach<\n G extends GeometryObject | null,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n currentGeometry: G,\n featureIndex: number,\n featureProperties: P,\n featureBBox: BBox,\n featureId: Id\n ) => void\n): void {\n var i,\n j,\n g,\n geometry,\n stopG,\n geometryMaybeCollection,\n isGeometryCollection,\n featureProperties,\n featureBBox,\n featureId,\n featureIndex = 0,\n // @ts-expect-error: Known type conflict\n isFeatureCollection = geojson.type === \"FeatureCollection\",\n // @ts-expect-error: Known type conflict\n isFeature = geojson.type === \"Feature\",\n // @ts-expect-error: Known type conflict\n stop = isFeatureCollection ? geojson.features.length : 1;\n\n // This logic may look a little weird. The reason why it is that way\n // is because it's trying to be fast. GeoJSON supports multiple kinds\n // of objects at its root: FeatureCollection, Features, Geometries.\n // This function has the responsibility of handling all of them, and that\n // means that some of the `for` loops you see below actually just don't apply\n // to certain inputs. For instance, if you give this just a\n // Point geometry, then both loops are short-circuited and all we do\n // is gradually rename the input until it's called 'geometry'.\n //\n // This also aims to allocate as few resources as possible: just a\n // few numbers and booleans, rather than any temporary arrays as would\n // be required with the normalization approach.\n for (i = 0; i < stop; i++) {\n geometryMaybeCollection = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].geometry\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.geometry\n : geojson;\n featureProperties = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].properties\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.properties\n : {};\n featureBBox = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].bbox\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.bbox\n : undefined;\n featureId = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].id\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.id\n : undefined;\n isGeometryCollection = geometryMaybeCollection\n ? geometryMaybeCollection.type === \"GeometryCollection\"\n : false;\n stopG = isGeometryCollection\n ? geometryMaybeCollection.geometries.length\n : 1;\n\n for (g = 0; g < stopG; g++) {\n geometry = isGeometryCollection\n ? geometryMaybeCollection.geometries[g]\n : geometryMaybeCollection;\n\n // Handle null Geometry\n if (geometry === null) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n // @ts-expect-error: Known type conflict\n null,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n continue;\n }\n switch (geometry.type) {\n case \"Point\":\n case \"LineString\":\n case \"MultiPoint\":\n case \"Polygon\":\n case \"MultiLineString\":\n case \"MultiPolygon\": {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n geometry,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n break;\n }\n case \"GeometryCollection\": {\n for (j = 0; j < geometry.geometries.length; j++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n geometry.geometries[j],\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n }\n break;\n }\n default:\n throw new Error(\"Unknown Geometry Type\");\n }\n }\n // Only increase `featureIndex` per each feature\n featureIndex++;\n }\n}\n\n/**\n * Callback for geomReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback geomReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {GeometryObject} currentGeometry The current Geometry being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {GeoJsonProperties} featureProperties The current Feature Properties being processed.\n * @param {BBox} featureBBox The current Feature BBox being processed.\n * @param {Id} featureId The current Feature Id being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce geometry in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|GeometryObject|GeometryCollection|Feature} geojson any GeoJSON object\n * @param {geomReduceCallback} callback a method that takes (previousValue, currentGeometry, featureIndex, featureProperties, featureBBox, featureId)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.geomReduce(features, function (previousValue, currentGeometry, featureIndex, featureProperties, featureBBox, featureId) {\n * //=previousValue\n * //=currentGeometry\n * //=featureIndex\n * //=featureProperties\n * //=featureBBox\n * //=featureId\n * return currentGeometry\n * });\n */\nfunction geomReduce<\n Reducer,\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n previousValue: Reducer,\n currentGeometry: G,\n featureIndex: number,\n featureProperties: P,\n featureBBox: BBox,\n featureId: Id\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n geomEach(\n geojson,\n function (\n currentGeometry,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentGeometry;\n else\n previousValue = callback(\n // @ts-expect-error: Known type conflict\n previousValue,\n currentGeometry,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n );\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for flattenEach\n *\n * @callback flattenEachCallback\n * @param {Feature} currentFeature The current flattened feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over flattened features in any GeoJSON object, similar to\n * Array.forEach.\n *\n * @function\n * @param {FeatureCollection|Feature|GeometryObject|GeometryCollection|Feature} geojson any GeoJSON object\n * @param {flattenEachCallback} callback a method that takes (currentFeature, featureIndex, multiFeatureIndex)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.multiPoint([[40, 30], [36, 53]], {hello: 'world'})\n * ]);\n *\n * turf.flattenEach(features, function (currentFeature, featureIndex, multiFeatureIndex) {\n * //=currentFeature\n * //=featureIndex\n * //=multiFeatureIndex\n * });\n */\nfunction flattenEach<\n G extends GeometryObject = GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n currentFeature: Feature,\n featureIndex: number,\n multiFeatureIndex: number\n ) => void\n): void {\n geomEach(geojson, function (geometry, featureIndex, properties, bbox, id) {\n // Callback for single geometry\n var type = geometry === null ? null : geometry.type;\n switch (type) {\n case null:\n case \"Point\":\n case \"LineString\":\n case \"Polygon\":\n if (\n // @ts-expect-error: Known type conflict\n callback(\n feature(geometry, properties, { bbox: bbox, id: id }),\n featureIndex,\n 0\n ) === false\n )\n return false;\n return;\n }\n\n var geomType;\n\n // Callback for multi-geometry\n switch (type) {\n case \"MultiPoint\":\n geomType = \"Point\";\n break;\n case \"MultiLineString\":\n geomType = \"LineString\";\n break;\n case \"MultiPolygon\":\n geomType = \"Polygon\";\n break;\n }\n\n for (\n var multiFeatureIndex = 0;\n // @ts-expect-error: Known type conflict\n multiFeatureIndex < geometry.coordinates.length;\n multiFeatureIndex++\n ) {\n // @ts-expect-error: Known type conflict\n var coordinate = geometry.coordinates[multiFeatureIndex];\n var geom = {\n type: geomType,\n coordinates: coordinate,\n };\n if (\n // @ts-expect-error: Known type conflict\n callback(feature(geom, properties), featureIndex, multiFeatureIndex) ===\n false\n )\n return false;\n }\n });\n}\n\n/**\n * Callback for flattenReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback flattenReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentFeature The current Feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce flattened features in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|GeometryObject|GeometryCollection|Feature} geojson any GeoJSON object\n * @param {flattenReduceCallback} callback a method that takes (previousValue, currentFeature, featureIndex, multiFeatureIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.multiPoint([[40, 30], [36, 53]], {hello: 'world'})\n * ]);\n *\n * turf.flattenReduce(features, function (previousValue, currentFeature, featureIndex, multiFeatureIndex) {\n * //=previousValue\n * //=currentFeature\n * //=featureIndex\n * //=multiFeatureIndex\n * return currentFeature\n * });\n */\nfunction flattenReduce<\n Reducer,\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n previousValue: Reducer,\n currentFeature: Feature,\n featureIndex: number,\n multiFeatureIndex: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n flattenEach(\n geojson,\n function (currentFeature, featureIndex, multiFeatureIndex) {\n if (\n featureIndex === 0 &&\n multiFeatureIndex === 0 &&\n initialValue === undefined\n )\n // @ts-expect-error: Known type conflict\n previousValue = currentFeature;\n else\n previousValue = callback(\n // @ts-expect-error: Known type conflict\n previousValue,\n currentFeature,\n featureIndex,\n multiFeatureIndex\n );\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for segmentEach\n *\n * @callback segmentEachCallback\n * @param {Feature} currentSegment The current Segment being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @param {number} segmentIndex The current index of the Segment being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over 2-vertex line segment in any GeoJSON object, similar to Array.forEach()\n * (Multi)Point geometries do not contain segments therefore they are ignored during this operation.\n *\n * @param {AllGeoJSON} geojson any GeoJSON\n * @param {segmentEachCallback} callback a method that takes (currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex)\n * @returns {void}\n * @example\n * var polygon = turf.polygon([[[-50, 5], [-40, -10], [-50, -10], [-40, 5], [-50, 5]]]);\n *\n * // Iterate over GeoJSON by 2-vertex segments\n * turf.segmentEach(polygon, function (currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex) {\n * //=currentSegment\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * //=segmentIndex\n * });\n *\n * // Calculate the total number of segments\n * var total = 0;\n * turf.segmentEach(polygon, function () {\n * total++;\n * });\n */\nfunction segmentEach

    (\n geojson: AllGeoJSON,\n callback: (\n currentSegment?: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n segmentIndex?: number,\n geometryIndex?: number\n ) => void\n): void {\n flattenEach(geojson, function (feature, featureIndex, multiFeatureIndex) {\n var segmentIndex = 0;\n\n // Exclude null Geometries\n if (!feature.geometry) return;\n // (Multi)Point geometries do not contain segments therefore they are ignored during this operation.\n var type = feature.geometry.type;\n if (type === \"Point\" || type === \"MultiPoint\") return;\n\n // Generate 2-vertex line segments\n // @ts-expect-error: Known type conflict\n var previousCoords;\n var previousFeatureIndex = 0;\n var previousMultiIndex = 0;\n var prevGeomIndex = 0;\n if (\n // @ts-expect-error: Known type conflict\n coordEach(\n feature,\n function (\n currentCoord,\n coordIndex,\n featureIndexCoord,\n multiPartIndexCoord,\n geometryIndex\n ) {\n // Simulating a meta.coordReduce() since `reduce` operations cannot be stopped by returning `false`\n if (\n // @ts-expect-error: Known type conflict\n previousCoords === undefined ||\n featureIndex > previousFeatureIndex ||\n multiPartIndexCoord > previousMultiIndex ||\n geometryIndex > prevGeomIndex\n ) {\n previousCoords = currentCoord;\n previousFeatureIndex = featureIndex;\n previousMultiIndex = multiPartIndexCoord;\n prevGeomIndex = geometryIndex;\n segmentIndex = 0;\n return;\n }\n var currentSegment = lineString(\n // @ts-expect-error: Known type conflict\n [previousCoords, currentCoord],\n feature.properties\n );\n if (\n // @ts-expect-error: Known type conflict\n callback(\n // @ts-expect-error: Known type conflict\n currentSegment,\n featureIndex,\n multiFeatureIndex,\n geometryIndex,\n segmentIndex\n ) === false\n )\n return false;\n segmentIndex++;\n previousCoords = currentCoord;\n }\n ) === false\n )\n return false;\n });\n}\n\n/**\n * Callback for segmentReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback segmentReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentSegment The current Segment being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @param {number} segmentIndex The current index of the Segment being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce 2-vertex line segment in any GeoJSON object, similar to Array.reduce()\n * (Multi)Point geometries do not contain segments therefore they are ignored during this operation.\n *\n * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON\n * @param {segmentReduceCallback} callback a method that takes (previousValue, currentSegment, currentIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer}\n * @example\n * var polygon = turf.polygon([[[-50, 5], [-40, -10], [-50, -10], [-40, 5], [-50, 5]]]);\n *\n * // Iterate over GeoJSON by 2-vertex segments\n * turf.segmentReduce(polygon, function (previousSegment, currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex) {\n * //= previousSegment\n * //= currentSegment\n * //= featureIndex\n * //= multiFeatureIndex\n * //= geometryIndex\n * //= segmentIndex\n * return currentSegment\n * });\n *\n * // Calculate the total number of segments\n * var initialValue = 0\n * var total = turf.segmentReduce(polygon, function (previousValue) {\n * previousValue++;\n * return previousValue;\n * }, initialValue);\n */\nfunction segmentReduce<\n Reducer,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | FeatureCollection\n | Feature\n | Lines\n | Feature\n | GeometryCollection,\n callback: (\n previousValue?: Reducer,\n currentSegment?: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n segmentIndex?: number,\n geometryIndex?: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n var started = false;\n segmentEach(\n geojson,\n function (\n currentSegment,\n featureIndex,\n multiFeatureIndex,\n geometryIndex,\n segmentIndex\n ) {\n if (started === false && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentSegment;\n else\n previousValue = callback(\n previousValue,\n // @ts-expect-error: Known type conflict\n currentSegment,\n featureIndex,\n multiFeatureIndex,\n geometryIndex,\n segmentIndex\n );\n started = true;\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for lineEach\n *\n * @callback lineEachCallback\n * @param {Feature} currentLine The current LineString|LinearRing being processed\n * @param {number} featureIndex The current index of the Feature being processed\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed\n * @param {number} geometryIndex The current index of the Geometry being processed\n * @returns {void}\n */\n\n/**\n * Iterate over line or ring coordinates in LineString, Polygon, MultiLineString, MultiPolygon Features or Geometries,\n * similar to Array.forEach.\n *\n * @function\n * @param {FeatureCollection|Feature|Lines|Feature|GeometryCollection} geojson object\n * @param {lineEachCallback} callback a method that takes (currentLine, featureIndex, multiFeatureIndex, geometryIndex)\n * @returns {void}\n * @example\n * var multiLine = turf.multiLineString([\n * [[26, 37], [35, 45]],\n * [[36, 53], [38, 50], [41, 55]]\n * ]);\n *\n * turf.lineEach(multiLine, function (currentLine, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=currentLine\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * });\n */\nfunction lineEach

    (\n geojson:\n | FeatureCollection\n | Feature\n | Lines\n | Feature\n | GeometryCollection,\n callback: (\n currentLine: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n geometryIndex?: number\n ) => void\n): void {\n // validation\n if (!geojson) throw new Error(\"geojson is required\");\n\n flattenEach(geojson, function (feature, featureIndex, multiFeatureIndex) {\n if (feature.geometry === null) return;\n var type = feature.geometry.type;\n var coords = feature.geometry.coordinates;\n switch (type) {\n case \"LineString\":\n // @ts-expect-error: Known type conflict\n if (callback(feature, featureIndex, multiFeatureIndex, 0, 0) === false)\n return false;\n break;\n case \"Polygon\":\n for (\n var geometryIndex = 0;\n geometryIndex < coords.length;\n geometryIndex++\n ) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n // @ts-expect-error: Known type conflict\n lineString(coords[geometryIndex], feature.properties),\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n return false;\n }\n break;\n }\n });\n}\n\n/**\n * Callback for lineReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback lineReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentLine The current LineString|LinearRing being processed.\n * @param {number} featureIndex The current index of the Feature being processed\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed\n * @param {number} geometryIndex The current index of the Geometry being processed\n * @returns {Reducer}\n */\n\n/**\n * Reduce features in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|Lines|Feature|GeometryCollection} geojson object\n * @param {Function} callback a method that takes (previousValue, currentLine, featureIndex, multiFeatureIndex, geometryIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var multiPoly = turf.multiPolygon([\n * turf.polygon([[[12,48],[2,41],[24,38],[12,48]], [[9,44],[13,41],[13,45],[9,44]]]),\n * turf.polygon([[[5, 5], [0, 0], [2, 2], [4, 4], [5, 5]]])\n * ]);\n *\n * turf.lineReduce(multiPoly, function (previousValue, currentLine, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=previousValue\n * //=currentLine\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * return currentLine\n * });\n */\nfunction lineReduce(\n geojson:\n | FeatureCollection\n | Feature\n | Lines\n | Feature\n | GeometryCollection,\n callback: (\n previousValue?: Reducer,\n currentLine?: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n geometryIndex?: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n lineEach(\n geojson,\n function (currentLine, featureIndex, multiFeatureIndex, geometryIndex) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentLine;\n else\n previousValue = callback(\n previousValue,\n currentLine,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n );\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Finds a particular 2-vertex LineString Segment from a GeoJSON using `@turf/meta` indexes.\n *\n * Negative indexes are permitted.\n * Point & MultiPoint will always return null.\n *\n * @param {FeatureCollection|Feature|Geometry} geojson Any GeoJSON Feature or Geometry\n * @param {Object} [options={}] Optional parameters\n * @param {number} [options.featureIndex=0] Feature Index\n * @param {number} [options.multiFeatureIndex=0] Multi-Feature Index\n * @param {number} [options.geometryIndex=0] Geometry Index\n * @param {number} [options.segmentIndex=0] Segment Index\n * @param {Object} [options.properties={}] Translate Properties to output LineString\n * @param {BBox} [options.bbox={}] Translate BBox to output LineString\n * @param {number|string} [options.id={}] Translate Id to output LineString\n * @returns {Feature} 2-vertex GeoJSON Feature LineString\n * @example\n * var multiLine = turf.multiLineString([\n * [[10, 10], [50, 30], [30, 40]],\n * [[-10, -10], [-50, -30], [-30, -40]]\n * ]);\n *\n * // First Segment (defaults are 0)\n * turf.findSegment(multiLine);\n * // => Feature>\n *\n * // First Segment of 2nd Multi Feature\n * turf.findSegment(multiLine, {multiFeatureIndex: 1});\n * // => Feature>\n *\n * // Last Segment of Last Multi Feature\n * turf.findSegment(multiLine, {multiFeatureIndex: -1, segmentIndex: -1});\n * // => Feature>\n */\nfunction findSegment<\n G extends LineString | MultiLineString | Polygon | MultiPolygon,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson: Feature | FeatureCollection | G,\n options?: {\n featureIndex?: number;\n multiFeatureIndex?: number;\n geometryIndex?: number;\n segmentIndex?: number;\n properties?: P;\n bbox?: BBox;\n id?: Id;\n }\n): Feature {\n // Optional Parameters\n options = options || {};\n if (!isObject(options)) throw new Error(\"options is invalid\");\n var featureIndex = options.featureIndex || 0;\n var multiFeatureIndex = options.multiFeatureIndex || 0;\n var geometryIndex = options.geometryIndex || 0;\n var segmentIndex = options.segmentIndex || 0;\n\n // Find FeatureIndex\n var properties = options.properties;\n var geometry;\n\n switch (geojson.type) {\n case \"FeatureCollection\":\n if (featureIndex < 0)\n featureIndex = geojson.features.length + featureIndex;\n properties = properties || geojson.features[featureIndex].properties;\n geometry = geojson.features[featureIndex].geometry;\n break;\n case \"Feature\":\n properties = properties || geojson.properties;\n geometry = geojson.geometry;\n break;\n case \"Point\" as GeoJsonTypes:\n case \"MultiPoint\" as GeoJsonTypes:\n // @ts-expect-error: Known type conflict\n return null;\n case \"LineString\":\n case \"Polygon\":\n case \"MultiLineString\":\n case \"MultiPolygon\":\n geometry = geojson;\n break;\n default:\n throw new Error(\"geojson is invalid\");\n }\n\n // Find SegmentIndex\n // @ts-expect-error: Known type conflict\n if (geometry === null) return null;\n var coords = geometry.coordinates;\n switch (geometry.type) {\n case \"Point\" as GeoJsonTypes:\n case \"MultiPoint\" as GeoJsonTypes:\n // @ts-expect-error: Known type conflict\n return null;\n case \"LineString\":\n if (segmentIndex < 0) segmentIndex = coords.length + segmentIndex - 1;\n return lineString(\n // @ts-expect-error: Known type conflict\n [coords[segmentIndex], coords[segmentIndex + 1]],\n properties,\n options\n );\n case \"Polygon\":\n if (geometryIndex < 0) geometryIndex = coords.length + geometryIndex;\n if (segmentIndex < 0)\n segmentIndex = coords[geometryIndex].length + segmentIndex - 1;\n return lineString(\n [\n // @ts-expect-error: Known type conflict\n coords[geometryIndex][segmentIndex],\n // @ts-expect-error: Known type conflict\n coords[geometryIndex][segmentIndex + 1],\n ],\n properties,\n options\n );\n case \"MultiLineString\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (segmentIndex < 0)\n segmentIndex = coords[multiFeatureIndex].length + segmentIndex - 1;\n return lineString(\n [\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][segmentIndex],\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][segmentIndex + 1],\n ],\n properties,\n options\n );\n case \"MultiPolygon\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (geometryIndex < 0)\n geometryIndex = coords[multiFeatureIndex].length + geometryIndex;\n if (segmentIndex < 0)\n segmentIndex =\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][geometryIndex].length - segmentIndex - 1;\n return lineString(\n [\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][geometryIndex][segmentIndex],\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][geometryIndex][segmentIndex + 1],\n ],\n properties,\n options\n );\n }\n throw new Error(\"geojson is invalid\");\n}\n\n/**\n * Finds a particular Point from a GeoJSON using `@turf/meta` indexes.\n *\n * Negative indexes are permitted.\n *\n * @param {FeatureCollection|Feature|Geometry} geojson Any GeoJSON Feature or Geometry\n * @param {Object} [options={}] Optional parameters\n * @param {number} [options.featureIndex=0] Feature Index\n * @param {number} [options.multiFeatureIndex=0] Multi-Feature Index\n * @param {number} [options.geometryIndex=0] Geometry Index\n * @param {number} [options.coordIndex=0] Coord Index\n * @param {Object} [options.properties={}] Translate Properties to output Point\n * @param {BBox} [options.bbox={}] Translate BBox to output Point\n * @param {number|string} [options.id={}] Translate Id to output Point\n * @returns {Feature} 2-vertex GeoJSON Feature Point\n * @example\n * var multiLine = turf.multiLineString([\n * [[10, 10], [50, 30], [30, 40]],\n * [[-10, -10], [-50, -30], [-30, -40]]\n * ]);\n *\n * // First Segment (defaults are 0)\n * turf.findPoint(multiLine);\n * // => Feature>\n *\n * // First Segment of the 2nd Multi-Feature\n * turf.findPoint(multiLine, {multiFeatureIndex: 1});\n * // => Feature>\n *\n * // Last Segment of last Multi-Feature\n * turf.findPoint(multiLine, {multiFeatureIndex: -1, coordIndex: -1});\n * // => Feature>\n */\nfunction findPoint<\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson: Feature | FeatureCollection | G,\n options?: {\n featureIndex?: number;\n multiFeatureIndex?: number;\n geometryIndex?: number;\n coordIndex?: number;\n properties?: P;\n bbox?: BBox;\n id?: Id;\n }\n): Feature {\n // Optional Parameters\n options = options || {};\n if (!isObject(options)) throw new Error(\"options is invalid\");\n var featureIndex = options.featureIndex || 0;\n var multiFeatureIndex = options.multiFeatureIndex || 0;\n var geometryIndex = options.geometryIndex || 0;\n var coordIndex = options.coordIndex || 0;\n\n // Find FeatureIndex\n var properties = options.properties;\n var geometry;\n\n switch (geojson.type) {\n case \"FeatureCollection\":\n if (featureIndex < 0)\n featureIndex = geojson.features.length + featureIndex;\n properties = properties || geojson.features[featureIndex].properties;\n geometry = geojson.features[featureIndex].geometry;\n break;\n case \"Feature\":\n properties = properties || geojson.properties;\n geometry = geojson.geometry;\n break;\n case \"Point\":\n case \"MultiPoint\":\n // @ts-expect-error: Known type conflict\n return null;\n case \"LineString\":\n case \"Polygon\":\n case \"MultiLineString\":\n case \"MultiPolygon\":\n geometry = geojson;\n break;\n default:\n throw new Error(\"geojson is invalid\");\n }\n\n // Find Coord Index\n // @ts-expect-error: Known type conflict\n if (geometry === null) return null;\n // @ts-expect-error: Known type conflict\n var coords = geometry.coordinates;\n switch (geometry.type) {\n case \"Point\":\n return point(coords, properties, options);\n case \"MultiPoint\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n return point(coords[multiFeatureIndex], properties, options);\n case \"LineString\":\n if (coordIndex < 0) coordIndex = coords.length + coordIndex;\n return point(coords[coordIndex], properties, options);\n case \"Polygon\":\n if (geometryIndex < 0) geometryIndex = coords.length + geometryIndex;\n if (coordIndex < 0)\n coordIndex = coords[geometryIndex].length + coordIndex;\n return point(coords[geometryIndex][coordIndex], properties, options);\n case \"MultiLineString\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (coordIndex < 0)\n coordIndex = coords[multiFeatureIndex].length + coordIndex;\n return point(coords[multiFeatureIndex][coordIndex], properties, options);\n case \"MultiPolygon\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (geometryIndex < 0)\n geometryIndex = coords[multiFeatureIndex].length + geometryIndex;\n if (coordIndex < 0)\n coordIndex =\n coords[multiFeatureIndex][geometryIndex].length - coordIndex;\n return point(\n coords[multiFeatureIndex][geometryIndex][coordIndex],\n properties,\n options\n );\n }\n throw new Error(\"geojson is invalid\");\n}\n\nexport {\n coordReduce,\n coordEach,\n propEach,\n propReduce,\n featureReduce,\n featureEach,\n coordAll,\n geomReduce,\n geomEach,\n flattenReduce,\n flattenEach,\n segmentReduce,\n segmentEach,\n lineReduce,\n lineEach,\n findSegment,\n findPoint,\n};\n", "import { BBox } from \"geojson\";\nimport { AllGeoJSON } from \"@turf/helpers\";\nimport { coordEach } from \"@turf/meta\";\n\n/**\n * Calculates the bounding box for any GeoJSON object, including FeatureCollection.\n * Uses geojson.bbox if available and options.recompute is not set.\n *\n * @function\n * @param {GeoJSON} geojson any GeoJSON object\n * @param {Object} [options={}] Optional parameters\n * @param {boolean} [options.recompute] Whether to ignore an existing bbox property on geojson\n * @returns {BBox} bbox extent in [minX, minY, maxX, maxY] order\n * @example\n * var line = turf.lineString([[-74, 40], [-78, 42], [-82, 35]]);\n * var bbox = turf.bbox(line);\n * var bboxPolygon = turf.bboxPolygon(bbox);\n *\n * //addToMap\n * var addToMap = [line, bboxPolygon]\n */\nfunction bbox(\n geojson: AllGeoJSON,\n options: {\n recompute?: boolean;\n } = {}\n): BBox {\n if (geojson.bbox != null && true !== options.recompute) {\n return geojson.bbox;\n }\n const result: BBox = [Infinity, Infinity, -Infinity, -Infinity];\n coordEach(geojson, (coord) => {\n if (result[0] > coord[0]) {\n result[0] = coord[0];\n }\n if (result[1] > coord[1]) {\n result[1] = coord[1];\n }\n if (result[2] < coord[0]) {\n result[2] = coord[0];\n }\n if (result[3] < coord[1]) {\n result[3] = coord[1];\n }\n });\n return result;\n}\n\nexport { bbox };\nexport default bbox;\n", "import { select as d3_select } from 'd3-selection';\nimport { t } from '../core/localizer';\n\nimport { geoScaleToZoom, geoVecLength } from '../geo';\nimport { utilPrefixCSSProperty, utilTiler } from '../util';\n\n\nexport function rendererTileLayer(context) {\n var transformProp = utilPrefixCSSProperty('Transform');\n var tiler = utilTiler();\n\n var _tileSize = 256;\n var _projection;\n var _cache = {};\n var _tileOrigin;\n var _zoom;\n var _source;\n var _underzoom = 0;\n\n function tileSizeAtZoom(d, z) {\n return (d.tileSize * Math.pow(2, z - d[2])) / d.tileSize;\n }\n\n\n function atZoom(t, distance) {\n var power = Math.pow(2, distance);\n return [\n Math.floor(t[0] * power),\n Math.floor(t[1] * power),\n t[2] + distance\n ];\n }\n\n\n function lookUp(d) {\n for (var up = -1; up > -d[2]; up--) {\n var tile = atZoom(d, up);\n if (_cache[_source.url(tile)] !== false) {\n return tile;\n }\n }\n }\n\n\n function uniqueBy(a, n) {\n var o = [];\n var seen = {};\n for (var i = 0; i < a.length; i++) {\n if (seen[a[i][n]] === undefined) {\n o.push(a[i]);\n seen[a[i][n]] = true;\n }\n }\n return o;\n }\n\n\n function addSource(d) {\n d.url = _source.url(d);\n d.tileSize = _tileSize;\n d.source = _source;\n return d;\n }\n\n\n // Update tiles based on current state of `projection`.\n function background(selection) {\n _zoom = geoScaleToZoom(_projection.scale(), _tileSize);\n\n var pixelOffset;\n if (_source) {\n pixelOffset = [\n _source.offset()[0] * Math.pow(2, _zoom),\n _source.offset()[1] * Math.pow(2, _zoom)\n ];\n } else {\n pixelOffset = [0, 0];\n }\n\n\n tiler\n .scale(_projection.scale() * 2 * Math.PI)\n .translate([\n _projection.translate()[0] + pixelOffset[0],\n _projection.translate()[1] + pixelOffset[1]\n ]);\n\n _tileOrigin = [\n _projection.scale() * Math.PI - _projection.translate()[0],\n _projection.scale() * Math.PI - _projection.translate()[1]\n ];\n\n render(selection);\n }\n\n\n // Derive the tiles onscreen, remove those offscreen and position them.\n // Important that this part not depend on `_projection` because it's\n // rendered when tiles load/error (see #644).\n function render(selection) {\n if (!_source) return;\n var requests = [];\n var showDebug = context.getDebug('tile') && !_source.overlay;\n\n if (_source.validZoom(_zoom, _underzoom)) {\n tiler.skipNullIsland(!!_source.overlay);\n\n tiler().forEach(function(d) {\n addSource(d);\n if (d.url === '') return;\n if (typeof d.url !== 'string') return; // Workaround for #2295\n requests.push(d);\n if (_cache[d.url] === false && lookUp(d)) {\n requests.push(addSource(lookUp(d)));\n }\n });\n\n requests = uniqueBy(requests, 'url').filter(function(r) {\n // don't re-request tiles which have failed in the past\n return _cache[r.url] !== false;\n });\n }\n\n function load(d3_event, d) {\n _cache[d.url] = true;\n d3_select(this)\n .on('error', null)\n .on('load', null);\n render(selection);\n }\n\n function error(d3_event, d) {\n _cache[d.url] = false;\n d3_select(this)\n .on('error', null)\n .on('load', null)\n .remove();\n render(selection);\n }\n\n function imageTransform(d) {\n var ts = d.tileSize * Math.pow(2, _zoom - d[2]);\n var scale = tileSizeAtZoom(d, _zoom);\n return 'translate(' +\n ((d[0] * ts + d.source.offset()[0] * Math.pow(2, _zoom)) * _tileSize / d.tileSize - _tileOrigin[0]\n ) + 'px,' +\n ((d[1] * ts + d.source.offset()[1] * Math.pow(2, _zoom)) * _tileSize / d.tileSize - _tileOrigin[1]\n ) + 'px) ' +\n 'scale(' + scale * _tileSize / d.tileSize + ',' + scale * _tileSize / d.tileSize + ')';\n }\n\n function tileCenter(d) {\n var ts = d.tileSize * Math.pow(2, _zoom - d[2]);\n return [\n ((d[0] * ts) - _tileOrigin[0] + (ts / 2)),\n ((d[1] * ts) - _tileOrigin[1] + (ts / 2))\n ];\n }\n\n function debugTransform(d) {\n var coord = tileCenter(d);\n return 'translate(' + coord[0] + 'px,' + coord[1] + 'px)';\n }\n\n\n // Pick a representative tile near the center of the viewport\n // (This is useful for sampling the imagery vintage)\n var dims = tiler.size();\n var mapCenter = [dims[0] / 2, dims[1] / 2];\n var minDist = Math.max(dims[0], dims[1]);\n var nearCenter;\n\n requests.forEach(function(d) {\n var c = tileCenter(d);\n var dist = geoVecLength(c, mapCenter);\n if (dist < minDist) {\n minDist = dist;\n nearCenter = d;\n }\n });\n\n\n var image = selection.selectAll('img')\n .data(requests, function(d) { return d.url; });\n\n image.exit()\n .style(transformProp, imageTransform)\n .classed('tile-removing', true)\n .classed('tile-center', false)\n .on('transitionend', function() {\n const tile = d3_select(this);\n if (tile.classed('tile-removing')) {\n tile.remove();\n }\n });\n\n image.enter()\n .append('img')\n .attr('class', 'tile')\n .attr('alt', '')\n .attr('draggable', 'false')\n .style('width', _tileSize + 'px')\n .style('height', _tileSize + 'px')\n .attr('src', function(d) { return d.url; })\n .on('error', error)\n .on('load', load)\n .merge(image)\n .style(transformProp, imageTransform)\n .classed('tile-debug', showDebug)\n .classed('tile-removing', false)\n .classed('tile-center', function(d) { return d === nearCenter; })\n .sort((a, b) => a[2] - b[2]);\n\n\n\n var debug = selection.selectAll('.tile-label-debug')\n .data(showDebug ? requests : [], function(d) { return d.url; });\n\n debug.exit()\n .remove();\n\n if (showDebug) {\n var debugEnter = debug.enter()\n .append('div')\n .attr('class', 'tile-label-debug');\n\n debugEnter\n .append('div')\n .attr('class', 'tile-label-debug-coord');\n\n debugEnter\n .append('div')\n .attr('class', 'tile-label-debug-vintage');\n\n debug = debug.merge(debugEnter);\n\n debug\n .style(transformProp, debugTransform);\n\n debug\n .selectAll('.tile-label-debug-coord')\n .text(function(d) { return d[2] + ' / ' + d[0] + ' / ' + d[1]; });\n\n debug\n .selectAll('.tile-label-debug-vintage')\n .each(function(d) {\n var span = d3_select(this);\n var center = context.projection.invert(tileCenter(d));\n _source.getMetadata(center, d, function(err, result) {\n if (result && result.vintage && result.vintage.range) {\n span.text(result.vintage.range);\n } else {\n span.text('');\n span.call(t.append('info_panels.background.vintage'));\n span.append('span').text(': ');\n span.call(t.append('info_panels.background.unknown'));\n }\n });\n });\n }\n\n }\n\n\n background.projection = function(val) {\n if (!arguments.length) return _projection;\n _projection = val;\n return background;\n };\n\n\n background.dimensions = function(val) {\n if (!arguments.length) return tiler.size();\n tiler.size(val);\n return background;\n };\n\n\n background.source = function(val) {\n if (!arguments.length) return _source;\n _source = val;\n _tileSize = _source.tileSize;\n _cache = {};\n tiler.tileSize(_source.tileSize).zoomExtent(_source.zoomExtent);\n return background;\n };\n\n\n background.underzoom = function(amount) {\n if (!arguments.length) return _underzoom;\n _underzoom = amount;\n return background;\n };\n\n\n return background;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate';\nimport { select as d3_select } from 'd3-selection';\nimport turf_bboxClip from '@turf/bbox-clip';\nimport turf_bbox from '@turf/bbox';\n\nimport whichPolygon from 'which-polygon';\n\nimport { prefs } from '../core/preferences';\nimport { fileFetcher } from '../core/file_fetcher';\nimport { geoMetersToOffset, geoOffsetToMeters, geoExtent } from '../geo';\nimport { rendererBackgroundSource } from './background_source';\nimport { rendererTileLayer } from './tile_layer';\nimport { utilQsString, utilStringQs } from '../util';\nimport { utilRebind } from '../util/rebind';\n\n\nlet _imageryIndex = null;\nlet _waybackIndex = null;\n\nexport function rendererBackground(context) {\n const dispatch = d3_dispatch('change');\n const baseLayer = rendererTileLayer(context).projection(context.projection);\n let _checkedBlocklists = [];\n let _isValid = true;\n let _overlayLayers = [];\n let _brightness = 1;\n let _contrast = 1;\n let _saturation = 1;\n let _sharpness = 1;\n\n\n function ensureImageryIndex() {\n return fileFetcher.get('wayback')\n .then(groups => {\n // eslint-disable-next-line no-warning-comments\n // TODO: Follow pagination via nextStart property.\n // Extracts the layer's date from the title.\n let extractDateFromTitle = title => {\n const dateComponents = title.match(/\\(Wayback (\\d{4})-(\\d\\d)-(\\d\\d)\\)/);\n if (!dateComponents) return;\n return new Date(Date.UTC(parseInt(dateComponents[1], 10),\n parseInt(dateComponents[2], 10) - 1,\n parseInt(dateComponents[3], 10)));\n };\n\n if (!_waybackIndex) {\n // Index the metadata MapServer URLs by the date of the World Imagery map.\n let metadataMapServersByDate = Object.fromEntries(groups.items\n .filter(item => item.type === 'Map Service')\n .map(item => {\n // Extract the layer's date from the title to avoid having to hit each MapServer right away.\n const date = extractDateFromTitle(item.title) || new Date(item.created);\n const dateString = date.toISOString().split('T')[0];\n return [dateString, item.url];\n }));\n\n _waybackIndex = groups.items\n .filter(item => item.type === 'WMTS')\n .map(item => {\n const date = extractDateFromTitle(item.title) || new Date(item.created);\n const dateString = date && date.toISOString().split('T')[0];\n\n // Convert the bounding box to a polygon.\n const bbox = {\n min_lon: item.extent[0][0],\n min_lat: item.extent[0][1],\n max_lon: item.extent[1][0],\n max_lat: item.extent[1][1],\n };\n const polygon = [[\n [bbox.min_lon, bbox.min_lat],\n [bbox.min_lon, bbox.max_lat],\n [bbox.max_lon, bbox.max_lat],\n [bbox.max_lon, bbox.min_lat],\n [bbox.min_lon, bbox.min_lat],\n ]];\n\n // Convert placeholder tokens in the URL template from Esri's format to OSM's.\n const template = item.url\n .replaceAll('{level}', '{zoom}')\n .replaceAll('{row}', '{y}')\n .replaceAll('{col}', '{x}');\n\n return {\n id: 'EsriWorldImagery_' + dateString,\n name: item.title,\n type: 'tms',\n template: template,\n metadata: metadataMapServersByDate[dateString],\n startDate: date.toISOString(),\n endDate: date.toISOString(),\n polygon: polygon,\n terms_text: item.accessInformation,\n description: item.snippet,\n // Match Esri World Imagery layer\n 'default': true,\n zoomExtent: [0, 22],\n terms_url: 'https://wiki.openstreetmap.org/wiki/Esri',\n icon: 'https://osmlab.github.io/editor-layer-index/sources/world/EsriImageryClarity.png',\n };\n });\n }\n return fileFetcher.get('imagery');\n })\n .catch(() => {\n return fileFetcher.get('imagery');\n })\n .then(sources => {\n if (_imageryIndex) return _imageryIndex;\n\n // Append Esri World Imagery Wayback sources.\n if (_waybackIndex) {\n sources.push(..._waybackIndex);\n }\n\n _imageryIndex = {\n imagery: sources,\n features: {}\n };\n\n // use which-polygon to support efficient index and querying for imagery\n const features = sources.map(source => {\n if (!source.polygon) return null;\n // workaround for editor-layer-index weirdness..\n // Add an extra array nest to each element in `source.polygon`\n // so the rings are not treated as a bunch of holes:\n // what we have: [ [[outer],[hole],[hole]] ]\n // what we want: [ [[outer]],[[outer]],[[outer]] ]\n const rings = source.polygon.map(ring => [ring]);\n\n const feature = {\n type: 'Feature',\n properties: { id: source.id },\n geometry: { type: 'MultiPolygon', coordinates: rings }\n };\n\n _imageryIndex.features[source.id] = feature;\n return feature;\n\n }).filter(Boolean);\n\n _imageryIndex.query = whichPolygon({ type: 'FeatureCollection', features: features });\n\n\n // Instantiate `rendererBackgroundSource` objects for each source\n _imageryIndex.backgrounds = sources.map(source => {\n if (source.type === 'bing') {\n return rendererBackgroundSource.Bing(source, dispatch);\n } else if (/^EsriWorldImagery/.test(source.id)) {\n return rendererBackgroundSource.Esri(source);\n } else {\n return rendererBackgroundSource(source);\n }\n });\n\n // Add 'None'\n _imageryIndex.backgrounds.unshift(rendererBackgroundSource.None());\n\n // Add 'Custom'\n let template = prefs('background-custom-template') || '';\n const custom = rendererBackgroundSource.Custom(template);\n _imageryIndex.backgrounds.unshift(custom);\n\n return _imageryIndex;\n });\n }\n\n\n function background(selection) {\n const currSource = baseLayer.source();\n\n // If we are displaying an Esri basemap at high zoom,\n // check its tilemap to see how high the zoom can go\n if (context.map().zoom() > 18) {\n if (currSource && /^EsriWorldImagery/.test(currSource.id)) {\n const center = context.map().center();\n currSource.fetchTilemap(center);\n }\n }\n\n // Is the imagery valid here? - #4827\n const sources = background.sources(context.map().extent());\n const wasValid = _isValid;\n _isValid = !!sources.filter(d => d === currSource).length;\n\n if (wasValid !== _isValid) { // change in valid status\n background.updateImagery();\n }\n\n\n let baseFilter = '';\n if (_brightness !== 1) {\n baseFilter += ` brightness(${_brightness})`;\n }\n if (_contrast !== 1) {\n baseFilter += ` contrast(${_contrast})`;\n }\n if (_saturation !== 1) {\n baseFilter += ` saturate(${_saturation})`;\n }\n if (_sharpness < 1) { // gaussian blur\n const blur = d3_interpolateNumber(0.5, 5)(1 - _sharpness);\n baseFilter += ` blur(${blur}px)`;\n }\n\n let base = selection.selectAll('.layer-background')\n .data([0]);\n\n base = base.enter()\n .insert('div', '.layer-data')\n .attr('class', 'layer layer-background')\n .merge(base);\n\n base.style('filter', baseFilter || null);\n\n\n let imagery = base.selectAll('.layer-imagery')\n .data([0]);\n\n imagery.enter()\n .append('div')\n .attr('class', 'layer layer-imagery')\n .merge(imagery)\n .call(baseLayer);\n\n\n let maskFilter = '';\n let mixBlendMode = '';\n if (_sharpness > 1) { // apply unsharp mask\n mixBlendMode = 'overlay';\n maskFilter = 'saturate(0) blur(3px) invert(1)';\n\n let contrast = _sharpness - 1;\n maskFilter += ` contrast(${contrast})`;\n\n let brightness = d3_interpolateNumber(1, 0.85)(_sharpness - 1);\n maskFilter += ` brightness(${brightness})`;\n }\n\n let mask = base.selectAll('.layer-unsharp-mask')\n .data(_sharpness > 1 ? [0] : []);\n\n mask.exit()\n .remove();\n\n mask.enter()\n .append('div')\n .attr('class', 'layer layer-mask layer-unsharp-mask')\n .merge(mask)\n .call(baseLayer)\n .style('filter', maskFilter || null)\n .style('mix-blend-mode', mixBlendMode || null);\n\n\n let overlays = selection.selectAll('.layer-overlay')\n .data(_overlayLayers, d => d.source().name());\n\n overlays.exit()\n .remove();\n\n overlays.enter()\n .insert('div', '.layer-data')\n .attr('class', 'layer layer-overlay')\n .merge(overlays)\n .each((layer, i, nodes) => d3_select(nodes[i]).call(layer));\n }\n\n\n background.updateImagery = function() {\n let currSource = baseLayer.source();\n if (context.inIntro() || !currSource) return;\n\n let o = _overlayLayers\n .filter(d => !d.source().isLocatorOverlay() && !d.source().isHidden())\n .map(d => d.source().id)\n .join(',');\n\n const meters = geoOffsetToMeters(currSource.offset());\n const EPSILON = 0.01;\n const x = +meters[0].toFixed(2);\n const y = +meters[1].toFixed(2);\n let hash = utilStringQs(window.location.hash);\n\n let id = currSource.id;\n if (id === 'custom') {\n id = `custom:${currSource.template()}`;\n }\n\n if (id) {\n hash.background = id;\n } else {\n delete hash.background;\n }\n\n if (o) {\n hash.overlays = o;\n } else {\n delete hash.overlays;\n }\n\n if (Math.abs(x) > EPSILON || Math.abs(y) > EPSILON) {\n hash.offset = `${x},${y}`;\n } else {\n delete hash.offset;\n }\n\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n\n let imageryUsed = [];\n let photoOverlaysUsed = [];\n\n const currUsed = currSource.imageryUsed();\n if (currUsed && _isValid) {\n imageryUsed.push(currUsed);\n }\n\n _overlayLayers\n .filter(d => !d.source().isLocatorOverlay() && !d.source().isHidden())\n .forEach(d => imageryUsed.push(d.source().imageryUsed()));\n\n const dataLayer = context.layers().layer('data');\n if (dataLayer && dataLayer.enabled() && dataLayer.hasData()) {\n imageryUsed.push(dataLayer.getSrc());\n }\n\n const photoOverlayLayers = {\n streetside: 'Bing Streetside',\n mapillary: 'Mapillary Images',\n 'mapillary-map-features': 'Mapillary Map Features',\n 'mapillary-signs': 'Mapillary Signs',\n kartaview: 'KartaView Images',\n vegbilder: 'Norwegian Road Administration Images',\n mapilio: 'Mapilio Images',\n panoramax: 'Panoramax Images'\n };\n\n for (let layerID in photoOverlayLayers) {\n const layer = context.layers().layer(layerID);\n if (layer && layer.enabled()) {\n photoOverlaysUsed.push(layerID);\n imageryUsed.push(photoOverlayLayers[layerID]);\n }\n }\n\n context.history().imageryUsed(imageryUsed);\n context.history().photoOverlaysUsed(photoOverlaysUsed);\n };\n\n\n background.sources = (extent, zoom, includeCurrent) => {\n if (!_imageryIndex) return []; // called before init()?\n\n let visible = {};\n (_imageryIndex.query.bbox(extent.rectangle(), true) || [])\n .forEach(d => visible[d.id] = true);\n\n const currSource = baseLayer.source();\n\n // Recheck blocked sources only if we detect new blocklists pulled from the OSM API.\n const osm = context.connection();\n const blocklists = (osm && osm.imageryBlocklists()) || [];\n const blocklistChanged = (blocklists.length !== _checkedBlocklists.length) ||\n blocklists.some((regex, index) => String(regex) !== _checkedBlocklists[index]);\n\n if (blocklistChanged) {\n _imageryIndex.backgrounds.forEach(source => {\n source.isBlocked = blocklists.some(regex => regex.test(source.template()));\n });\n _checkedBlocklists = blocklists.map(regex => String(regex));\n }\n\n return _imageryIndex.backgrounds.filter(source => {\n if (includeCurrent && currSource === source) return true; // optionally always include the current imagery\n if (source.isBlocked) return false; // even bundled sources may be blocked - #7905\n if (!source.polygon) return true; // always include imagery with worldwide coverage\n if (zoom && zoom < 6) return false; // optionally exclude local imagery at low zooms\n return visible[source.id]; // include imagery visible in given extent\n });\n };\n\n\n background.dimensions = (val) => {\n if (!val) return;\n baseLayer.dimensions(val);\n _overlayLayers.forEach(layer => layer.dimensions(val));\n };\n\n\n background.baseLayerSource = function(d) {\n if (!arguments.length) return baseLayer.source();\n\n // test source against OSM imagery blocklists..\n const osm = context.connection();\n if (!osm) return background;\n\n const blocklists = osm.imageryBlocklists();\n const template = d.template();\n let fail = false;\n let tested = 0;\n let regex;\n\n for (let i = 0; i < blocklists.length; i++) {\n regex = blocklists[i];\n fail = regex.test(template);\n tested++;\n if (fail) break;\n }\n\n // ensure at least one test was run.\n if (!tested) {\n regex = /.*\\.google(apis)?\\..*\\/(vt|kh)[\\?\\/].*([xyz]=.*){3}.*/;\n fail = regex.test(template);\n }\n\n baseLayer.source(!fail ? d : background.findSource('none'));\n dispatch.call('change');\n background.updateImagery();\n return background;\n };\n\n\n background.findSource = (id) => {\n if (!id || !_imageryIndex) return null; // called before init()?\n return _imageryIndex.backgrounds.find(d => d.id && d.id === id);\n };\n\n\n background.bing = () => {\n background.baseLayerSource(background.findSource('Bing'));\n };\n\n\n background.showsLayer = (d) => {\n const currSource = baseLayer.source();\n if (!d || !currSource) return false;\n return d.id === currSource.id || _overlayLayers.some(layer => d.id === layer.source().id);\n };\n\n\n background.overlayLayerSources = () => {\n return _overlayLayers.map(layer => layer.source());\n };\n\n\n background.toggleOverlayLayer = (d) => {\n let layer;\n for (let i = 0; i < _overlayLayers.length; i++) {\n layer = _overlayLayers[i];\n if (layer.source() === d) {\n _overlayLayers.splice(i, 1);\n dispatch.call('change');\n background.updateImagery();\n return;\n }\n }\n\n layer = rendererTileLayer(context)\n .source(d)\n .projection(context.projection)\n .dimensions(baseLayer.dimensions()\n );\n\n _overlayLayers.push(layer);\n dispatch.call('change');\n background.updateImagery();\n };\n\n\n background.nudge = (d, zoom) => {\n const currSource = baseLayer.source();\n if (currSource) {\n currSource.nudge(d, zoom);\n dispatch.call('change');\n background.updateImagery();\n }\n return background;\n };\n\n\n background.offset = function(d) {\n const currSource = baseLayer.source();\n if (!arguments.length) {\n return (currSource && currSource.offset()) || [0, 0];\n }\n if (currSource) {\n currSource.offset(d);\n dispatch.call('change');\n background.updateImagery();\n }\n return background;\n };\n\n\n background.brightness = function(d) {\n if (!arguments.length) return _brightness;\n _brightness = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n\n background.contrast = function(d) {\n if (!arguments.length) return _contrast;\n _contrast = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n\n background.saturation = function(d) {\n if (!arguments.length) return _saturation;\n _saturation = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n\n background.sharpness = function(d) {\n if (!arguments.length) return _sharpness;\n _sharpness = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n let _loadPromise;\n\n background.ensureLoaded = () => {\n if (_loadPromise) return _loadPromise;\n\n return _loadPromise = ensureImageryIndex();\n };\n\n background.init = () => {\n const loadPromise = background.ensureLoaded();\n\n const hash = utilStringQs(window.location.hash);\n const requestedBackground = hash.background || hash.layer;\n const lastUsedBackground = prefs('background-last-used');\n\n return loadPromise.then(imageryIndex => {\n const extent = context.map().extent();\n const validBackgrounds = background.sources(extent).filter(d => d.id !== 'none' && d.id !== 'custom');\n const first = validBackgrounds.length && validBackgrounds[0];\n const isLastUsedValid = !!validBackgrounds.find(d => d.id && d.id === lastUsedBackground);\n\n let best;\n if (!requestedBackground && extent) {\n const viewArea = extent.area();\n best = validBackgrounds.find(s => {\n if (!s.best() || s.overlay) return false;\n let bbox = turf_bbox(turf_bboxClip(\n { type: 'MultiPolygon', coordinates: [ s.polygon || [extent.polygon()] ] },\n extent.rectangle()));\n let area = geoExtent(bbox.slice(0,2), bbox.slice(2,4)).area();\n return area / viewArea > 0.5; // min visible size: 50% of viewport area\n });\n }\n\n // Decide which background layer to display\n if (requestedBackground && requestedBackground.indexOf('custom:') === 0) {\n const template = requestedBackground.replace(/^custom:/, '');\n const custom = background.findSource('custom');\n background.baseLayerSource(custom.template(template));\n prefs('background-custom-template', template);\n } else {\n background.baseLayerSource(\n background.findSource(requestedBackground) ||\n best ||\n isLastUsedValid && background.findSource(lastUsedBackground) ||\n background.findSource('Bing') ||\n first ||\n background.findSource('none')\n );\n }\n\n const locator = imageryIndex.backgrounds.find(d => d.overlay && d.default);\n if (locator) {\n background.toggleOverlayLayer(locator);\n }\n\n const overlays = (hash.overlays || '').split(',');\n overlays.forEach(overlay => {\n overlay = background.findSource(overlay);\n if (overlay) {\n background.toggleOverlayLayer(overlay);\n }\n });\n\n if (hash.gpx) {\n const gpx = context.layers().layer('data');\n if (gpx) {\n gpx.url(hash.gpx, '.gpx');\n }\n }\n\n if (hash.offset) {\n const offset = hash.offset\n .replace(/;/g, ',')\n .split(',')\n .map(n => !isNaN(n) && n);\n\n if (offset.length === 2) {\n background.offset(geoMetersToOffset(offset));\n }\n }\n })\n .catch(err => {\n /* eslint-disable no-console */\n console.error(err);\n /* eslint-enable no-console */\n });\n };\n\n\n return utilRebind(background, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from '../core/preferences';\nimport { osmEntity } from '../osm';\nimport { osmLanduseTags, osmLifecyclePrefixes } from '../osm/tags.js';\nimport { utilRebind } from '../util/rebind';\nimport { utilArrayGroupBy, utilArrayUnion, utilQsString, utilStringQs, utilDatesOverlap } from '../util';\nimport { isAddressPoint } from '../svg/labels';\n\n\nexport function rendererFeatures(context) {\n var dispatch = d3_dispatch('change', 'redraw');\n const features = {};\n var _deferred = new Set();\n\n var traffic_roads = {\n 'motorway': true,\n 'motorway_link': true,\n 'trunk': true,\n 'trunk_link': true,\n 'primary': true,\n 'primary_link': true,\n 'secondary': true,\n 'secondary_link': true,\n 'tertiary': true,\n 'tertiary_link': true,\n 'residential': true,\n 'unclassified': true,\n 'living_street': true,\n 'busway': true\n };\n\n var service_roads = {\n 'service': true,\n 'road': true,\n 'track': true\n };\n\n var paths = {\n 'path': true,\n 'footway': true,\n 'cycleway': true,\n 'bridleway': true,\n 'steps': true,\n 'ladder': true,\n 'pedestrian': true\n };\n\n var _cullFactor = 1;\n var _cache = {};\n var _rules = {};\n var _dateMatchCount = 0;\n var _stats = {};\n var _keys = [];\n var _hidden = [];\n var _forceVisible = {};\n\n\n function update() {\n const hash = utilStringQs(window.location.hash);\n const disabled = features.disabled();\n if (disabled.length) {\n hash.disable_features = disabled.join(',');\n } else {\n delete hash.disable_features;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n prefs('disabled-features', disabled.join(','));\n _hidden = features.hidden();\n dispatch.call('change');\n dispatch.call('redraw');\n }\n\n\n /**\n * @callback FilterFunction\n * @param {Record} tags\n * @param {string} [geometry]\n * @returns {boolean}\n */\n\n /**\n * @param {string} k\n * @param {FilterFunction} filter\n * @param {number} [max]\n */\n function defineRule(k, filter, max) {\n var isEnabled = true;\n\n _keys.push(k);\n _rules[k] = {\n filter: filter,\n enabled: isEnabled, // whether the user wants it enabled..\n count: 0,\n currentMax: (max || Infinity),\n defaultMax: (max || Infinity),\n enable: function() { this.enabled = true; this.currentMax = this.defaultMax; },\n disable: function() { this.enabled = false; this.currentMax = 0; },\n hidden: function() {\n return (this.count === 0 && !this.enabled) ||\n this.count > this.currentMax * _cullFactor;\n },\n autoHidden: function() { return this.hidden() && this.currentMax > 0; }\n };\n }\n\n defineRule('address_points', (tags, geometry) =>\n geometry === 'point' && isAddressPoint(tags),\n 100);\n\n defineRule('points', (tags, geometry) =>\n geometry === 'point' && !isAddressPoint(tags, geometry),\n 200);\n\n defineRule('traffic_roads', function isTrafficRoad(tags) {\n return traffic_roads[tags.highway];\n });\n\n defineRule('service_roads', function isServiceRoad(tags) {\n return service_roads[tags.highway];\n });\n\n defineRule('paths', function isPath(tags) {\n return paths[tags.highway];\n });\n\n defineRule('buildings', function isBuilding(tags) {\n return (\n (!!tags.building && tags.building !== 'no') ||\n tags.parking === 'multi-storey' ||\n tags.parking === 'sheds' ||\n tags.parking === 'carports' ||\n tags.parking === 'garage_boxes'\n );\n }, 250);\n\n defineRule('building_parts', function isBuildingPart(tags) {\n return !!tags['building:part'];\n });\n\n defineRule('indoor', function isIndoor(tags) {\n return (\n (!!tags.indoor && tags.indoor !== 'no') ||\n (!!tags.indoormark && tags.indoormark !== 'no')\n );\n });\n\n defineRule('landuse', function isLanduse(tags, geometry) {\n if (geometry !== 'area') return false;\n let hasLanduseTag = false;\n for (const key in osmLanduseTags) {\n if (osmLanduseTags[key] === true && tags[key] ||\n osmLanduseTags[key][tags[key]] === true) {\n hasLanduseTag = true;\n }\n }\n return hasLanduseTag &&\n !_rules.buildings.filter(tags) &&\n !_rules.building_parts.filter(tags) &&\n !_rules.indoor.filter(tags) &&\n !_rules.water.filter(tags) &&\n !_rules.pistes.filter(tags);\n });\n\n defineRule('boundaries', function isBoundary(tags, geometry) {\n // This rule applies if the object has no interesting tags, and if either:\n // (a) is a way having a `boundary=*` tag, or\n // (b) is a relation of `type=boundary`.\n return (\n (geometry === 'line' && !!tags.boundary) ||\n (geometry === 'relation' && tags.type === 'boundary')\n ) && !(\n traffic_roads[tags.highway] ||\n service_roads[tags.highway] ||\n paths[tags.highway] ||\n tags.waterway ||\n tags.railway ||\n tags.landuse ||\n tags.natural ||\n tags.building ||\n tags.power\n );\n });\n\n defineRule('water', function isWater(tags) {\n return (\n !!tags.waterway ||\n tags.natural === 'water' ||\n tags.natural === 'coastline' ||\n tags.natural === 'bay' ||\n tags.landuse === 'pond' ||\n tags.landuse === 'basin' ||\n tags.landuse === 'reservoir' ||\n tags.landuse === 'salt_pond'\n );\n });\n\n defineRule('rail', function isRail(tags) {\n return (\n !!tags.railway ||\n tags.landuse === 'railway'\n ) && !(\n traffic_roads[tags.highway] ||\n service_roads[tags.highway] ||\n paths[tags.highway]\n );\n });\n\n defineRule('pistes', function isPiste(tags) {\n return tags['piste:type'];\n });\n\n defineRule('aerialways', function isAerialways(tags) {\n return !!tags?.aerialway &&\n tags.aerialway !== 'yes' &&\n tags.aerialway !== 'station';\n });\n\n defineRule('power', function isPower(tags) {\n return !!tags.power;\n });\n\n // contains a past/future tag, but not in active use as a road/path/cycleway/etc..\n defineRule('past_future', function isPastFuture(tags) {\n if (\n traffic_roads[tags.highway] ||\n service_roads[tags.highway] ||\n paths[tags.highway]\n ) { return false; }\n\n const keys = Object.keys(tags);\n\n for (const key of keys) {\n if (osmLifecyclePrefixes[tags[key]]) return true; // legacy tagging, e.g. `highway=construction`\n const parts = key.split(':');\n if (parts.length === 1) continue;\n const prefix = parts[0];\n if (osmLifecyclePrefixes[prefix]) return true; // lifecycle tagging, e.g. `demolished:building=yes`\n }\n return false;\n });\n\n // Lines or areas that don't match another feature filter.\n // IMPORTANT: The 'others' feature must be the last one defined,\n // so that code in getMatches can skip this test if `hasMatch = true`\n defineRule('others', function isOther(tags, geometry) {\n return (geometry === 'line' || geometry === 'area');\n });\n\n\n\n features.features = function() {\n return _rules;\n };\n\n\n features.keys = function() {\n return _keys;\n };\n\n\n features.enabled = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return _rules[k].enabled; });\n }\n return _rules[k] && _rules[k].enabled;\n };\n\n\n features.disabled = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return !_rules[k].enabled; });\n }\n return _rules[k] && !_rules[k].enabled;\n };\n\n\n features.hidden = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return _rules[k].hidden(); });\n }\n return _rules[k]?.hidden();\n };\n\n\n features.autoHidden = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return _rules[k].autoHidden(); });\n }\n return _rules[k] && _rules[k].autoHidden();\n };\n\n\n features.enable = function(k) {\n if (_rules[k] && !_rules[k].enabled) {\n _rules[k].enable();\n update();\n }\n };\n\n features.enableAll = function() {\n var didEnable = false;\n for (var k in _rules) {\n if (!_rules[k].enabled) {\n didEnable = true;\n _rules[k].enable();\n }\n }\n if (didEnable) update();\n };\n\n\n features.disable = function(k) {\n if (_rules[k] && _rules[k].enabled) {\n _rules[k].disable();\n update();\n }\n };\n\n features.disableAll = function() {\n var didDisable = false;\n for (var k in _rules) {\n if (_rules[k].enabled) {\n didDisable = true;\n _rules[k].disable();\n }\n }\n if (didDisable) update();\n };\n\n\n features.toggle = function(k) {\n if (_rules[k]) {\n (function(f) { return f.enabled ? f.disable() : f.enable(); }(_rules[k]));\n update();\n }\n };\n\n\n features.redraw = function() {\n update();\n };\n\n\n features.resetStats = function() {\n for (var i = 0; i < _keys.length; i++) {\n _rules[_keys[i]].count = 0;\n }\n _dateMatchCount = 0;\n dispatch.call('change');\n };\n\n\n features.gatherStats = function(d, resolver, dimensions) {\n var needsRedraw = false;\n var types = utilArrayGroupBy(d, 'type');\n var entities = [].concat(types.relation || [], types.way || [], types.node || []);\n var currHidden, geometry, matches, i, j;\n\n for (i = 0; i < _keys.length; i++) {\n _rules[_keys[i]].count = 0;\n }\n _dateMatchCount = 0;\n\n // adjust the threshold for point/building culling based on viewport size..\n // a _cullFactor of 1 corresponds to a 1000x1000px viewport..\n _cullFactor = dimensions[0] * dimensions[1] / 1000000;\n\n for (i = 0; i < entities.length; i++) {\n geometry = entities[i].geometry(resolver);\n matches = Object.keys(features.getMatches(entities[i], resolver, geometry));\n for (j = 0; j < matches.length; j++) {\n _rules[matches[j]].count++;\n }\n if (!features.featureFitsDateRange(entities[i])) _dateMatchCount++;\n }\n\n currHidden = features.hidden();\n if (currHidden !== _hidden) {\n _hidden = currHidden;\n needsRedraw = true;\n dispatch.call('change');\n }\n\n return needsRedraw;\n };\n\n\n features.stats = function() {\n for (var i = 0; i < _keys.length; i++) {\n _stats[_keys[i]] = _rules[_keys[i]].count;\n }\n\n return _stats;\n };\n\n\n features.dateMatchCount = () => _dateMatchCount;\n\n\n features.clear = function(d) {\n for (var i = 0; i < d.length; i++) {\n features.clearEntity(d[i]);\n }\n };\n\n\n features.clearEntity = function(entity) {\n delete _cache[osmEntity.key(entity)];\n for (const key in _cache) {\n if (_cache[key].parents) {\n for (const parent of _cache[key].parents) {\n if (parent.id === entity.id) {\n delete _cache[key];\n break;\n }\n }\n }\n }\n };\n\n\n features.reset = function() {\n Array.from(_deferred).forEach(function(handle) {\n window.cancelIdleCallback(handle);\n _deferred.delete(handle);\n });\n\n _cache = {};\n };\n\n // only certain relations are worth checking\n function relationShouldBeChecked(relation) {\n // multipolygon features have `area` geometry and aren't checked here\n return relation.tags.type === 'boundary';\n }\n\n features.getMatches = function(entity, resolver, geometry) {\n if (geometry === 'vertex' ||\n (geometry === 'relation' && !relationShouldBeChecked(entity))) return {};\n\n var ent = osmEntity.key(entity);\n if (!_cache[ent]) {\n _cache[ent] = {};\n }\n\n if (!_cache[ent].matches) {\n var matches = {};\n var hasMatch = false;\n\n for (var i = 0; i < _keys.length; i++) {\n if (_keys[i] === 'others') {\n if (hasMatch) continue;\n\n // If an entity...\n // 1. is a way that hasn't matched other 'interesting' feature rules,\n if (entity.type === 'way') {\n var parents = features.getParents(entity, resolver, geometry);\n\n // 2a. belongs only to a single multipolygon relation\n if ((parents.length === 1 && parents[0].isMultipolygon()) ||\n // 2b. or belongs only to boundary relations\n (parents.length > 0 && parents.every(function(parent) { return parent.tags.type === 'boundary'; }))) {\n\n // ...then match whatever feature rules the parent relation has matched.\n // see #2548, #2887\n //\n // IMPORTANT:\n // For this to work, getMatches must be called on relations before ways.\n //\n var pkey = osmEntity.key(parents[0]);\n if (_cache[pkey] && _cache[pkey].matches) {\n matches = Object.assign({}, _cache[pkey].matches); // shallow copy\n continue;\n }\n }\n }\n }\n\n if (_rules[_keys[i]].filter(entity.tags, geometry)) {\n matches[_keys[i]] = true;\n hasMatch = true;\n }\n }\n _cache[ent].matches = matches;\n }\n\n return _cache[ent].matches;\n };\n\n\n features.getParents = function(entity, resolver, geometry) {\n if (geometry === 'point') return [];\n\n const ent = osmEntity.key(entity);\n if (!_cache[ent]) {\n _cache[ent] = {};\n }\n\n if (!_cache[ent].parents) {\n let parents;\n if (geometry === 'vertex') {\n parents = resolver.parentWays(entity);\n } else { // 'line', 'area', 'relation'\n parents = resolver.parentRelations(entity);\n }\n _cache[ent].parents = parents;\n }\n\n return _cache[ent].parents;\n };\n\n\n features.isHiddenPreset = function(preset, geometry) {\n // if (!_hidden.length) return false;\n if (!preset.tags) return false;\n\n var test = preset.setTags({...preset.tags}, geometry);\n for (var key in _rules) {\n if (_rules[key].filter(test, geometry)) {\n if (_hidden.indexOf(key) !== -1) {\n return key;\n }\n return false;\n }\n }\n return false;\n };\n\n\n features.isHiddenFeature = function(entity, resolver, geometry) {\n // if (!_hidden.length) return false;\n if (!entity.version) return false;\n if (_forceVisible[entity.id]) return false;\n if (!features.featureFitsDateRange(entity)) return true;\n // if (!_hidden.length) return false;\n\n var matches = Object.keys(features.getMatches(entity, resolver, geometry));\n return matches.length && matches.every(function(k) { return features.hidden(k); });\n };\n\n\n features.isHiddenChild = function(entity, resolver, geometry) {\n // if (!_hidden.length) return false;\n if (!entity.version || geometry === 'point') return false;\n if (_forceVisible[entity.id]) return false;\n if (!features.featureFitsDateRange(entity)) return true;\n\n var parents = features.getParents(entity, resolver, geometry);\n if (!parents.length) return false;\n\n for (var i = 0; i < parents.length; i++) {\n if (!features.isHidden(parents[i], resolver, parents[i].geometry(resolver))) {\n return false;\n }\n }\n return true;\n };\n\n\n features.hasHiddenConnections = function(entity, resolver) {\n // if (!_hidden.length) return false;\n\n var childNodes, connections;\n if (entity.type === 'midpoint') {\n childNodes = [resolver.entity(entity.edge[0]), resolver.entity(entity.edge[1])];\n connections = [];\n } else {\n childNodes = entity.nodes ? resolver.childNodes(entity) : [];\n connections = features.getParents(entity, resolver, entity.geometry(resolver));\n }\n\n // gather ways connected to child nodes..\n connections = childNodes.reduce(function(result, e) {\n return resolver.isShared(e) ? utilArrayUnion(result, resolver.parentWays(e)) : result;\n }, connections);\n\n return connections.some(function(e) {\n return features.isHidden(e, resolver, e.geometry(resolver));\n });\n };\n\n\n features.isHidden = function(entity, resolver, geometry) {\n // if (!_hidden.length) return false;\n if (!entity.version) return false;\n\n var fn = (geometry === 'vertex' ? features.isHiddenChild : features.isHiddenFeature);\n return fn(entity, resolver, geometry);\n };\n\n\n features.featureFitsDateRange = function (entity) {\n if (!features.dateRange) return true; // no Date Range e.g. unit tests\n\n // entity's start & end date + the Date Range from the on-screen controls\n // utilDatesOverlap() treats malformed start_date/end_date as 9999/-9999\n // uiSectionDateRange already standardizes the dateRange inputs\n // so we don't need much validation here\n const entityRange = {\n 'start_date': entity.tags.start_date,\n 'end_date': entity.tags.end_date\n };\n const selectedRange = {\n 'start_date': features.dateRange[0],\n 'end_date': features.dateRange[1]\n };\n\n // out of range = feature started after range ends, or feature ends before range starts\n const withinrange = utilDatesOverlap(selectedRange, entityRange, true);\n return withinrange;\n };\n\n\n features.filter = function(d, resolver) {\n // if (!_hidden.length) return d;\n\n var result = [];\n for (var i = 0; i < d.length; i++) {\n var entity = d[i];\n if (!features.isHidden(entity, resolver, entity.geometry(resolver))) {\n result.push(entity);\n }\n }\n return result;\n };\n\n\n features.forceVisible = function(entityIDs) {\n if (!arguments.length) return Object.keys(_forceVisible);\n\n _forceVisible = {};\n for (var i = 0; i < entityIDs.length; i++) {\n _forceVisible[entityIDs[i]] = true;\n var entity = context.hasEntity(entityIDs[i]);\n if (entity && entity.type === 'relation') {\n // also show relation members (one level deep)\n for (var j in entity.members) {\n _forceVisible[entity.members[j].id] = true;\n }\n }\n }\n return features;\n };\n\n\n features.init = function() {\n var storage = prefs('disabled-features');\n if (storage) {\n var storageDisabled = storage.replace(/;/g, ',').split(',');\n storageDisabled.forEach(features.disable);\n }\n\n var hash = utilStringQs(window.location.hash);\n if (hash.disable_features) {\n var hashDisabled = hash.disable_features.replace(/;/g, ',').split(',');\n hashDisabled.forEach(features.disable);\n }\n };\n\n\n // warm up the feature matching cache upon merging fetched data\n context.history().on('merge.features', function(newEntities) {\n if (!newEntities) return;\n var handle = window.requestIdleCallback(function() {\n var graph = context.graph();\n var types = utilArrayGroupBy(newEntities, 'type');\n // ensure that getMatches is called on relations before ways\n var entities = [].concat(types.relation || [], types.way || [], types.node || []);\n for (var i = 0; i < entities.length; i++) {\n var geometry = entities[i].geometry(graph);\n features.getMatches(entities[i], graph, geometry);\n }\n });\n _deferred.add(handle);\n });\n\n\n return utilRebind(features, dispatch, 'on');\n}\n", "export function utilBindOnce(target, type, listener, capture) {\n var typeOnce = type + '.once';\n function one() {\n target.on(typeOnce, null);\n listener.apply(this, arguments);\n }\n target.on(typeOnce, one, capture);\n return this;\n}\n", "// Adapted from d3-zoom to handle pointer events.\n// https://github.com/d3/d3-zoom/blob/523ccff340187a3e3c044eaa4d4a7391ea97272b/src/zoom.js\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { interpolateZoom } from 'd3-interpolate';\nimport { select as d3_select } from 'd3-selection';\nimport { interrupt as d3_interrupt } from 'd3-transition';\nimport { zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\nimport { Transform } from '../../node_modules/d3-zoom/src/transform.js';\n\nimport { utilFastMouse, utilFunctor } from './util';\nimport { utilRebind } from './rebind';\n\n// Ignore right-click, since that should open the context menu.\nfunction defaultFilter(d3_event) {\n return !d3_event.ctrlKey && !d3_event.button;\n}\n\nfunction defaultExtent() {\n var e = this;\n if (e instanceof SVGElement) {\n e = e.ownerSVGElement || e;\n if (e.hasAttribute('viewBox')) {\n e = e.viewBox.baseVal;\n return [[e.x, e.y], [e.x + e.width, e.y + e.height]];\n }\n return [[0, 0], [e.width.baseVal.value, e.height.baseVal.value]];\n }\n return [[0, 0], [e.clientWidth, e.clientHeight]];\n}\n\nfunction defaultWheelDelta(d3_event) {\n return -d3_event.deltaY * (d3_event.deltaMode === 1 ? 0.05 : d3_event.deltaMode ? 1 : 0.002);\n}\n\nfunction defaultConstrain(transform, extent, translateExtent) {\n var dx0 = transform.invertX(extent[0][0]) - translateExtent[0][0],\n dx1 = transform.invertX(extent[1][0]) - translateExtent[1][0],\n dy0 = transform.invertY(extent[0][1]) - translateExtent[0][1],\n dy1 = transform.invertY(extent[1][1]) - translateExtent[1][1];\n return transform.translate(\n dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),\n dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)\n );\n}\n\nexport function utilZoomPan() {\n var filter = defaultFilter,\n extent = defaultExtent,\n constrain = defaultConstrain,\n wheelDelta = defaultWheelDelta,\n scaleExtent = [0, Infinity],\n translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]],\n interpolate = interpolateZoom,\n dispatch = d3_dispatch('start', 'zoom', 'end'),\n _wheelDelay = 150,\n _transform = d3_zoomIdentity,\n _activeGesture;\n\n function zoom(selection) {\n selection\n .on('pointerdown.zoom', pointerdown)\n .on('wheel.zoom', wheeled)\n .style('touch-action', 'none')\n .style('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');\n\n d3_select(window)\n .on('pointermove.zoompan', pointermove)\n .on('pointerup.zoompan pointercancel.zoompan', pointerup);\n }\n\n zoom.transform = function(collection, transform, point) {\n var selection = collection.selection ? collection.selection() : collection;\n if (collection !== selection) {\n schedule(collection, transform, point);\n } else {\n selection.interrupt().each(function() {\n gesture(this, arguments)\n .start(null)\n .zoom(null, null, typeof transform === 'function' ? transform.apply(this, arguments) : transform)\n .end(null);\n });\n }\n };\n\n zoom.scaleBy = function(selection, k, p) {\n zoom.scaleTo(selection, function() {\n var k0 = _transform.k,\n k1 = typeof k === 'function' ? k.apply(this, arguments) : k;\n return k0 * k1;\n }, p);\n };\n\n zoom.scaleTo = function(selection, k, p) {\n zoom.transform(selection, function() {\n var e = extent.apply(this, arguments),\n t0 = _transform,\n p0 = !p ? centroid(e) : typeof p === 'function' ? p.apply(this, arguments) : p,\n p1 = t0.invert(p0),\n k1 = typeof k === 'function' ? k.apply(this, arguments) : k;\n return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent);\n }, p);\n };\n\n zoom.translateBy = function(selection, x, y) {\n zoom.transform(selection, function() {\n return constrain(_transform.translate(\n typeof x === 'function' ? x.apply(this, arguments) : x,\n typeof y === 'function' ? y.apply(this, arguments) : y\n ), extent.apply(this, arguments), translateExtent);\n });\n };\n\n zoom.translateTo = function(selection, x, y, p) {\n zoom.transform(selection, function() {\n var e = extent.apply(this, arguments),\n t = _transform,\n p0 = !p ? centroid(e) : typeof p === 'function' ? p.apply(this, arguments) : p;\n return constrain(d3_zoomIdentity.translate(p0[0], p0[1]).scale(t.k).translate(\n typeof x === 'function' ? -x.apply(this, arguments) : -x,\n typeof y === 'function' ? -y.apply(this, arguments) : -y\n ), e, translateExtent);\n }, p);\n };\n\n function scale(transform, k) {\n k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], k));\n return k === transform.k ? transform : new Transform(k, transform.x, transform.y);\n }\n\n function translate(transform, p0, p1) {\n var x = p0[0] - p1[0] * transform.k, y = p0[1] - p1[1] * transform.k;\n return x === transform.x && y === transform.y ? transform : new Transform(transform.k, x, y);\n }\n\n function centroid(extent) {\n return [(+extent[0][0] + +extent[1][0]) / 2, (+extent[0][1] + +extent[1][1]) / 2];\n }\n\n function schedule(transition, transform, point) {\n transition\n .on('start.zoom', function() { gesture(this, arguments).start(null); })\n .on('interrupt.zoom end.zoom', function() { gesture(this, arguments).end(null); })\n .tween('zoom', function() {\n var that = this,\n args = arguments,\n g = gesture(that, args),\n e = extent.apply(that, args),\n p = !point ? centroid(e) : typeof point === 'function' ? point.apply(that, args) : point,\n w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]),\n a = _transform,\n b = typeof transform === 'function' ? transform.apply(that, args) : transform,\n i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k));\n return function(t) {\n if (t === 1) {\n // Avoid rounding error on end.\n t = b;\n } else {\n var l = i(t);\n var k = w / l[2];\n t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k);\n }\n g.zoom(null, null, t);\n };\n });\n }\n\n function gesture(that, args, clean) {\n return (!clean && _activeGesture) || new Gesture(that, args);\n }\n\n function Gesture(that, args) {\n this.that = that;\n this.args = args;\n this.active = 0;\n this.extent = extent.apply(that, args);\n }\n\n Gesture.prototype = {\n start: function(d3_event) {\n if (++this.active === 1) {\n _activeGesture = this;\n dispatch.call('start', this, d3_event);\n }\n return this;\n },\n zoom: function(d3_event, key, transform) {\n if (this.mouse && key !== 'mouse') this.mouse[1] = transform.invert(this.mouse[0]);\n if (this.pointer0 && key !== 'touch') this.pointer0[1] = transform.invert(this.pointer0[0]);\n if (this.pointer1 && key !== 'touch') this.pointer1[1] = transform.invert(this.pointer1[0]);\n _transform = transform;\n dispatch.call('zoom', this, d3_event, key, transform);\n return this;\n },\n end: function(d3_event) {\n if (--this.active === 0) {\n _activeGesture = null;\n dispatch.call('end', this, d3_event);\n }\n return this;\n }\n };\n\n function wheeled(d3_event) {\n if (!filter.apply(this, arguments)) return;\n var g = gesture(this, arguments),\n t = _transform,\n k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k * Math.pow(2, wheelDelta.apply(this, arguments)))),\n p = utilFastMouse(this)(d3_event);\n\n // If the mouse is in the same location as before, reuse it.\n // If there were recent wheel events, reset the wheel idle timeout.\n if (g.wheel) {\n if (g.mouse[0][0] !== p[0] || g.mouse[0][1] !== p[1]) {\n g.mouse[1] = t.invert(g.mouse[0] = p);\n }\n clearTimeout(g.wheel);\n\n // Otherwise, capture the mouse point and location at the start.\n } else {\n g.mouse = [p, t.invert(p)];\n d3_interrupt(this);\n g.start(d3_event);\n }\n\n d3_event.preventDefault();\n d3_event.stopImmediatePropagation();\n g.wheel = setTimeout(wheelidled, _wheelDelay);\n g.zoom(d3_event, 'mouse', constrain(translate(scale(t, k), g.mouse[0], g.mouse[1]), g.extent, translateExtent));\n\n function wheelidled() {\n g.wheel = null;\n g.end(d3_event);\n }\n }\n\n var _downPointerIDs = new Set();\n var _pointerLocGetter;\n\n function pointerdown(d3_event) {\n _downPointerIDs.add(d3_event.pointerId);\n\n if (!filter.apply(this, arguments)) return;\n\n var g = gesture(this, arguments, _downPointerIDs.size === 1);\n var started;\n\n d3_event.stopImmediatePropagation();\n _pointerLocGetter = utilFastMouse(this);\n var loc = _pointerLocGetter(d3_event);\n var p = [loc, _transform.invert(loc), d3_event.pointerId];\n if (!g.pointer0) {\n g.pointer0 = p;\n started = true;\n\n } else if (!g.pointer1 && g.pointer0[2] !== p[2]) {\n g.pointer1 = p;\n }\n\n if (started) {\n d3_interrupt(this);\n g.start(d3_event);\n }\n }\n\n function pointermove(d3_event) {\n if (!_downPointerIDs.has(d3_event.pointerId)) return;\n\n if (!_activeGesture || !_pointerLocGetter) return;\n\n var g = gesture(this, arguments);\n\n var isPointer0 = g.pointer0 && g.pointer0[2] === d3_event.pointerId;\n var isPointer1 = !isPointer0 && g.pointer1 && g.pointer1[2] === d3_event.pointerId;\n\n if ((isPointer0 || isPointer1) && 'buttons' in d3_event && !d3_event.buttons) {\n // The pointer went up without ending the gesture somehow, e.g.\n // a down mouse was moved off the map and released. End it here.\n if (g.pointer0) _downPointerIDs.delete(g.pointer0[2]);\n if (g.pointer1) _downPointerIDs.delete(g.pointer1[2]);\n g.end(d3_event);\n return;\n }\n\n d3_event.preventDefault();\n d3_event.stopImmediatePropagation();\n\n var loc = _pointerLocGetter(d3_event);\n var t, p, l;\n\n if (isPointer0) g.pointer0[0] = loc;\n else if (isPointer1) g.pointer1[0] = loc;\n\n t = _transform;\n if (g.pointer1) {\n var p0 = g.pointer0[0], l0 = g.pointer0[1],\n p1 = g.pointer1[0], l1 = g.pointer1[1],\n dp = (dp = p1[0] - p0[0]) * dp + (dp = p1[1] - p0[1]) * dp,\n dl = (dl = l1[0] - l0[0]) * dl + (dl = l1[1] - l0[1]) * dl;\n t = scale(t, Math.sqrt(dp / dl));\n p = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];\n l = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2];\n } else if (g.pointer0) {\n p = g.pointer0[0];\n l = g.pointer0[1];\n } else {\n return;\n }\n g.zoom(d3_event, 'touch', constrain(translate(t, p, l), g.extent, translateExtent));\n }\n\n function pointerup(d3_event) {\n if (!_downPointerIDs.has(d3_event.pointerId)) return;\n\n _downPointerIDs.delete(d3_event.pointerId);\n\n if (!_activeGesture) return;\n\n var g = gesture(this, arguments);\n\n d3_event.stopImmediatePropagation();\n\n if (g.pointer0 && g.pointer0[2] === d3_event.pointerId) delete g.pointer0;\n else if (g.pointer1 && g.pointer1[2] === d3_event.pointerId) delete g.pointer1;\n\n if (g.pointer1 && !g.pointer0) {\n g.pointer0 = g.pointer1;\n delete g.pointer1;\n }\n if (g.pointer0) {\n g.pointer0[1] = _transform.invert(g.pointer0[0]);\n } else {\n g.end(d3_event);\n }\n }\n\n zoom.wheelDelta = function(_) {\n return arguments.length ? (wheelDelta = utilFunctor(+_), zoom) : wheelDelta;\n };\n\n zoom.filter = function(_) {\n return arguments.length ? (filter = utilFunctor(!!_), zoom) : filter;\n };\n\n zoom.extent = function(_) {\n return arguments.length ? (extent = utilFunctor([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;\n };\n\n zoom.scaleExtent = function(_) {\n return arguments.length ? (scaleExtent[0] = +_[0], scaleExtent[1] = +_[1], zoom) : [scaleExtent[0], scaleExtent[1]];\n };\n\n zoom.translateExtent = function(_) {\n return arguments.length ? (translateExtent[0][0] = +_[0][0], translateExtent[1][0] = +_[1][0], translateExtent[0][1] = +_[0][1], translateExtent[1][1] = +_[1][1], zoom) : [[translateExtent[0][0], translateExtent[0][1]], [translateExtent[1][0], translateExtent[1][1]]];\n };\n\n zoom.constrain = function(_) {\n return arguments.length ? (constrain = _, zoom) : constrain;\n };\n\n zoom.interpolate = function(_) {\n return arguments.length ? (interpolate = _, zoom) : interpolate;\n };\n\n zoom._transform = function(_) {\n return arguments.length ? (_transform = _, zoom) : _transform;\n };\n\n return utilRebind(zoom, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { utilFastMouse } from './util';\nimport { utilRebind } from './rebind';\nimport { geoVecLength } from '../geo/vector';\n\n// A custom double-click / double-tap event detector that works on touch devices\n// if pointer events are supported. Falls back to default `dblclick` event.\nexport function utilDoubleUp() {\n\n var dispatch = d3_dispatch('doubleUp');\n\n var _maxTimespan = 500; // milliseconds\n var _maxDistance = 20; // web pixels; be somewhat generous to account for touch devices\n var _pointer; // object representing the pointer that could trigger double up\n\n function pointerIsValidFor(loc) {\n // second pointerup must occur within a small timeframe after the first pointerdown\n return new Date().getTime() - _pointer.startTime <= _maxTimespan &&\n // all pointer events must occur within a small distance of the first pointerdown\n geoVecLength(_pointer.startLoc, loc) <= _maxDistance;\n }\n\n function pointerdown(d3_event) {\n\n // ignore right-click\n if (d3_event.ctrlKey || d3_event.button === 2) return;\n\n var loc = [d3_event.clientX, d3_event.clientY];\n\n // Don't rely on pointerId here since it can change between pointerdown\n // events on touch devices\n if (_pointer && !pointerIsValidFor(loc)) {\n // if this pointer is no longer valid, clear it so another can be started\n _pointer = undefined;\n }\n\n if (!_pointer) {\n _pointer = {\n startLoc: loc,\n startTime: new Date().getTime(),\n upCount: 0,\n pointerId: d3_event.pointerId\n };\n } else { // double down\n _pointer.pointerId = d3_event.pointerId;\n }\n }\n\n function pointerup(d3_event) {\n\n // ignore right-click\n if (d3_event.ctrlKey || d3_event.button === 2) return;\n\n if (!_pointer || _pointer.pointerId !== d3_event.pointerId) return;\n\n _pointer.upCount += 1;\n\n if (_pointer.upCount === 2) { // double up!\n var loc = [d3_event.clientX, d3_event.clientY];\n if (pointerIsValidFor(loc)) {\n var locInThis = utilFastMouse(this)(d3_event);\n dispatch.call('doubleUp', this, d3_event, locInThis);\n }\n // clear the pointer info in any case\n _pointer = undefined;\n }\n }\n\n function doubleUp(selection) {\n if ('PointerEvent' in window) {\n // dblclick isn't well supported on touch devices so manually use\n // pointer events if they're available\n selection\n .on('pointerdown.doubleUp', pointerdown)\n .on('pointerup.doubleUp', pointerup);\n } else {\n // fallback to dblclick\n selection\n .on('dblclick.doubleUp', function(d3_event) {\n dispatch.call('doubleUp', this, d3_event, utilFastMouse(this)(d3_event));\n });\n }\n }\n\n doubleUp.off = function(selection) {\n selection\n .on('pointerdown.doubleUp', null)\n .on('pointerup.doubleUp', null)\n .on('dblclick.doubleUp', null);\n };\n\n return utilRebind(doubleUp, dispatch, 'on');\n}\n", "import { throttle, isArray, clamp } from 'es-toolkit/compat';\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { interpolate as d3_interpolate } from 'd3-interpolate';\nimport { scaleLinear as d3_scaleLinear } from 'd3-scale';\nimport { select as d3_select } from 'd3-selection';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport { prefs } from '../core/preferences';\nimport { geoExtent, geoRawMercator, geoScaleToZoom, geoZoomToScale } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { svgAreas, svgLabels, svgLayers, svgLines, svgMidpoints, svgPoints, svgVertices } from '../svg';\nimport { utilFastMouse, utilFunctor, utilSetTransform, utilEntityAndDeepMemberIDs } from '../util/util';\nimport { utilBindOnce } from '../util/bind_once';\nimport { utilDetect } from '../util/detect';\nimport { utilGetDimensions } from '../util/dimensions';\nimport { utilRebind } from '../util/rebind';\nimport { utilZoomPan } from '../util/zoom_pan';\nimport { utilDoubleUp } from '../util/double_up';\n\n// constants\nvar TILESIZE = 256;\nvar minZoom = 2;\nvar maxZoom = 24;\nvar kMin = geoZoomToScale(minZoom, TILESIZE);\nvar kMax = geoZoomToScale(maxZoom, TILESIZE);\n\n\nexport function rendererMap(context) {\n var dispatch = d3_dispatch(\n 'move', 'drawn',\n 'crossEditableZoom', 'hitMinZoom',\n 'changeHighlighting', 'changeAreaFill'\n );\n var projection = context.projection;\n var curtainProjection = context.curtainProjection;\n var drawLayers;\n var drawPoints;\n var drawVertices;\n var drawLines;\n var drawAreas;\n var drawMidpoints;\n var drawLabels;\n\n var _selection = d3_select(null);\n var supersurface = d3_select(null);\n var wrapper = d3_select(null);\n var surface = d3_select(null);\n\n var _dimensions = [1, 1];\n var _dblClickZoomEnabled = true;\n var _redrawEnabled = true;\n var _gestureTransformStart;\n var _transformStart = projection.transform();\n var _transformLast;\n var _isTransformed = false;\n var _minzoom = 0;\n var _getMouseCoords;\n var _lastPointerEvent;\n var _lastWithinEditableZoom;\n\n // whether a pointerdown event started the zoom\n var _pointerDown = false;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n // use pointer event interaction if supported; fallback to touch/mouse events in d3-zoom\n var _zoomerPannerFunction = 'PointerEvent' in window ? utilZoomPan : d3_zoom;\n\n var _zoomerPanner = _zoomerPannerFunction()\n .scaleExtent([kMin, kMax])\n .interpolate(d3_interpolate)\n .filter(zoomEventFilter)\n .on('zoom.map', zoomPan)\n .on('start.map', function(d3_event) {\n _pointerDown = d3_event && (d3_event.type === 'pointerdown' ||\n (d3_event.sourceEvent && d3_event.sourceEvent.type === 'pointerdown'));\n })\n .on('end.map', function() {\n _pointerDown = false;\n });\n var _doubleUpHandler = utilDoubleUp();\n\n var scheduleRedraw = throttle(redraw, 750);\n // var isRedrawScheduled = false;\n // var pendingRedrawCall;\n // function scheduleRedraw() {\n // // Only schedule the redraw if one has not already been set.\n // if (isRedrawScheduled) return;\n // isRedrawScheduled = true;\n // var that = this;\n // var args = arguments;\n // pendingRedrawCall = window.requestIdleCallback(function () {\n // // Reset the boolean so future redraws can be set.\n // isRedrawScheduled = false;\n // redraw.apply(that, args);\n // }, { timeout: 1400 });\n // }\n\n function cancelPendingRedraw() {\n scheduleRedraw.cancel();\n // isRedrawScheduled = false;\n // window.cancelIdleCallback(pendingRedrawCall);\n }\n\n\n function map(selection) {\n _selection = selection;\n\n context\n .on('change.map', immediateRedraw);\n\n var osm = context.connection();\n if (osm) {\n osm.on('change.map', immediateRedraw);\n }\n\n function didUndoOrRedo(targetTransform) {\n var mode = context.mode().id;\n if (mode !== 'browse' && mode !== 'select') return;\n if (targetTransform) {\n map.transformEase(targetTransform);\n }\n }\n\n context.history()\n .on('merge.map', function() { scheduleRedraw(); })\n .on('change.map', immediateRedraw)\n .on('undone.map', function(stack, fromStack) {\n didUndoOrRedo(fromStack.transform);\n })\n .on('redone.map', function(stack) {\n didUndoOrRedo(stack.transform);\n });\n\n context.background()\n .on('change.map', immediateRedraw);\n\n context.features()\n .on('redraw.map', immediateRedraw);\n\n drawLayers\n .on('change.map', function() {\n context.background().updateImagery();\n immediateRedraw();\n });\n\n selection\n .on('wheel.map mousewheel.map', function(d3_event) {\n // disable swipe-to-navigate browser pages on trackpad/magic mouse \u2013 #5552\n d3_event.preventDefault();\n })\n .call(_zoomerPanner)\n .call(_zoomerPanner.transform, projection.transform())\n .on('dblclick.zoom', null); // override d3-zoom dblclick handling\n\n map.supersurface = selection.append('div')\n .attr('class', 'supersurface')\n .call(utilSetTransform, 0, 0);\n supersurface = map.supersurface;\n\n // Need a wrapper div because Opera can't cope with an absolutely positioned\n // SVG element: http://bl.ocks.org/jfirebaugh/6fbfbd922552bf776c16\n wrapper = supersurface\n .append('div')\n .attr('class', 'layer layer-data');\n\n map.surface = wrapper\n .call(drawLayers)\n .selectAll('.surface');\n surface = map.surface;\n\n surface\n .call(drawLabels.observe)\n .call(_doubleUpHandler)\n .on(_pointerPrefix + 'down.zoom', function(d3_event) {\n _lastPointerEvent = d3_event;\n if (d3_event.button === 2) {\n d3_event.stopPropagation();\n }\n }, true)\n .on(_pointerPrefix + 'up.zoom', function(d3_event) {\n _lastPointerEvent = d3_event;\n if (resetTransform()) {\n immediateRedraw();\n }\n })\n .on(_pointerPrefix + 'move.map', function(d3_event) {\n _lastPointerEvent = d3_event;\n })\n .on(_pointerPrefix + 'over.vertices', function(d3_event) {\n if (map.editableDataEnabled() && !_isTransformed) {\n var hover = d3_event.target.__data__;\n surface.call(drawVertices.drawHover, context.graph(), hover, map.extent());\n dispatch.call('drawn', this, { full: false });\n }\n })\n .on(_pointerPrefix + 'out.vertices', function(d3_event) {\n if (map.editableDataEnabled() && !_isTransformed) {\n var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__;\n surface.call(drawVertices.drawHover, context.graph(), hover, map.extent());\n dispatch.call('drawn', this, { full: false });\n }\n });\n\n var detected = utilDetect();\n\n // only WebKit supports gesture events\n if ('GestureEvent' in window &&\n // Listening for gesture events on iOS 13.4+ breaks double-tapping,\n // but we only need to do this on desktop Safari anyway. \u2013 #7694\n !detected.isMobileWebKit) {\n\n // Desktop Safari sends gesture events for multitouch trackpad pinches.\n // We can listen for these and translate them into map zooms.\n surface\n .on('gesturestart.surface', function(d3_event) {\n d3_event.preventDefault();\n _gestureTransformStart = projection.transform();\n })\n .on('gesturechange.surface', gestureChange);\n }\n\n // must call after surface init\n updateAreaFill();\n\n _doubleUpHandler.on('doubleUp.map', function(d3_event, p0) {\n if (!_dblClickZoomEnabled) return;\n\n // don't zoom if targeting something other than the map itself\n if (typeof d3_event.target.__data__ === 'object' &&\n // or area fills\n !d3_select(d3_event.target).classed('fill')) return;\n\n var zoomOut = d3_event.shiftKey;\n\n var t = projection.transform();\n\n var p1 = t.invert(p0);\n\n t = t.scale(zoomOut ? 0.5 : 2);\n\n t.x = p0[0] - p1[0] * t.k;\n t.y = p0[1] - p1[1] * t.k;\n\n map.transformEase(t);\n });\n\n context.on('enter.map', function() {\n if (!map.editableDataEnabled(true /* skip zoom check */)) return;\n if (_isTransformed) return;\n\n // redraw immediately any objects affected by a change in selectedIDs.\n var graph = context.graph();\n var selectedAndParents = {};\n context.selectedIDs().forEach(function(id) {\n var entity = graph.hasEntity(id);\n if (entity) {\n selectedAndParents[entity.id] = entity;\n if (entity.type === 'node') {\n graph.parentWays(entity).forEach(function(parent) {\n selectedAndParents[parent.id] = parent;\n });\n }\n }\n });\n var data = Object.values(selectedAndParents);\n var filter = function(d) { return d.id in selectedAndParents; };\n\n data = context.features().filter(data, graph);\n\n surface\n .call(drawVertices.drawSelected, graph, map.extent())\n .call(drawLines, graph, data, filter)\n .call(drawAreas, graph, data, filter)\n .call(drawMidpoints, graph, data, filter, map.trimmedExtent());\n\n dispatch.call('drawn', this, { full: false });\n\n // redraw everything else later\n scheduleRedraw();\n });\n\n map.dimensions(utilGetDimensions(selection));\n }\n\n\n function zoomEventFilter(d3_event) {\n // Fix for #2151, (see also d3/d3-zoom#60, d3/d3-brush#18)\n // Intercept `mousedown` and check if there is an orphaned zoom gesture.\n // This can happen if a previous `mousedown` occurred without a `mouseup`.\n // If we detect this, dispatch `mouseup` to complete the orphaned gesture,\n // so that d3-zoom won't stop propagation of new `mousedown` events.\n if (d3_event.type === 'mousedown') {\n var hasOrphan = false;\n var listeners = window.__on;\n for (var i = 0; i < listeners.length; i++) {\n var listener = listeners[i];\n if (listener.name === 'zoom' && listener.type === 'mouseup') {\n hasOrphan = true;\n break;\n }\n }\n if (hasOrphan) {\n const event = new Event('mouseup');\n // Event needs to be dispatched with an event.view property.\n event.view = window;\n window.dispatchEvent(event);\n }\n }\n\n return d3_event.button !== 2; // ignore right clicks\n }\n\n\n function pxCenter() {\n return [_dimensions[0] / 2, _dimensions[1] / 2];\n }\n\n\n function drawEditable(difference, extent) {\n var mode = context.mode();\n var graph = context.graph();\n var features = context.features();\n var all = context.history().intersects(map.extent());\n var fullRedraw = false;\n var data;\n var set;\n var filter;\n var applyFeatureLayerFilters = true;\n\n if (map.isInWideSelection()) {\n data = [];\n utilEntityAndDeepMemberIDs(mode.selectedIDs(), context.graph()).forEach(function(id) {\n var entity = context.hasEntity(id);\n if (entity) data.push(entity);\n });\n fullRedraw = true;\n filter = utilFunctor(true);\n // selected features should always be visible, so we can skip filtering\n applyFeatureLayerFilters = false;\n\n } else if (difference) {\n var complete = difference.complete(map.extent());\n data = Object.values(complete).filter(Boolean);\n set = new Set(Object.keys(complete));\n filter = function(d) { return set.has(d.id); };\n features.clear(data);\n\n } else {\n // force a full redraw if gatherStats detects that a feature\n // should be auto-hidden (e.g. points or buildings)..\n if (features.gatherStats(all, graph, _dimensions)) {\n extent = undefined;\n }\n\n if (extent) {\n data = context.history().intersects(map.extent().intersection(extent));\n set = new Set(data.map(function(entity) { return entity.id; }));\n filter = function(d) { return set.has(d.id); };\n\n } else {\n data = all;\n fullRedraw = true;\n filter = utilFunctor(true);\n }\n }\n\n if (applyFeatureLayerFilters) {\n data = features.filter(data, graph);\n } else {\n context.features().resetStats();\n }\n\n if (mode && mode.id === 'select') {\n // update selected vertices - the user might have just double-clicked a way,\n // creating a new vertex, triggering a partial redraw without a mode change\n surface.call(drawVertices.drawSelected, graph, map.extent());\n }\n\n surface\n .call(drawVertices, graph, data, filter, map.extent(), fullRedraw)\n .call(drawLines, graph, data, filter)\n .call(drawAreas, graph, data, filter)\n .call(drawMidpoints, graph, data, filter, map.trimmedExtent())\n .call(drawPoints, graph, data, filter)\n .call(drawLabels, graph, data, filter, _dimensions, fullRedraw);\n\n dispatch.call('drawn', this, {full: true});\n }\n\n map.init = function() {\n drawLayers = svgLayers(projection, context);\n drawPoints = svgPoints(projection, context);\n drawVertices = svgVertices(projection, context);\n drawLines = svgLines(projection, context);\n drawAreas = svgAreas(projection, context);\n drawMidpoints = svgMidpoints(projection, context);\n drawLabels = svgLabels(projection, context);\n };\n\n function editOff() {\n context.features().resetStats();\n surface.selectAll('.layer-osm *').remove();\n surface.selectAll('.layer-touch:not(.markers) *').remove();\n\n var allowed = {\n 'browse': true,\n 'save': true,\n 'select-note': true,\n 'select-data': true,\n 'select-error': true\n };\n\n var mode = context.mode();\n if (mode && !allowed[mode.id]) {\n context.enter(modeBrowse(context));\n }\n\n dispatch.call('drawn', this, {full: true});\n }\n\n\n\n\n\n function gestureChange(d3_event) {\n // Remap Safari gesture events to wheel events - #5492\n // We want these disabled most places, but enabled for zoom/unzoom on map surface\n // https://developer.mozilla.org/en-US/docs/Web/API/GestureEvent\n var e = d3_event;\n e.preventDefault();\n\n var props = {\n deltaMode: 0, // dummy values to ignore in zoomPan\n deltaY: 1, // dummy values to ignore in zoomPan\n clientX: e.clientX,\n clientY: e.clientY,\n screenX: e.screenX,\n screenY: e.screenY,\n x: e.x,\n y: e.y\n };\n\n var e2 = new WheelEvent('wheel', props);\n e2._scale = e.scale; // preserve the original scale\n e2._rotation = e.rotation; // preserve the original rotation\n\n _selection.node().dispatchEvent(e2);\n }\n\n\n function zoomPan(event, key, transform) {\n var source = event && event.sourceEvent || event;\n var eventTransform = transform || (event && event.transform);\n var x = eventTransform.x;\n var y = eventTransform.y;\n var k = eventTransform.k;\n\n // Special handling of 'wheel' events:\n // They might be triggered by the user scrolling the mouse wheel,\n // or 2-finger pinch/zoom gestures, the transform may need adjustment.\n if (source && source.type === 'wheel') {\n\n // assume that the gesture is already handled by pointer events\n if (_pointerDown) return;\n\n var detected = utilDetect();\n var dX = source.deltaX;\n var dY = source.deltaY;\n var x2 = x;\n var y2 = y;\n var k2 = k;\n var t0, p0, p1;\n\n // Normalize mousewheel scroll speed (Firefox) - #3029\n // If wheel delta is provided in LINE units, recalculate it in PIXEL units\n // We are essentially redoing the calculations that occur here:\n // https://github.com/d3/d3-zoom/blob/78563a8348aa4133b07cac92e2595c2227ca7cd7/src/zoom.js#L203\n // See this for more info:\n // https://github.com/basilfx/normalize-wheel/blob/master/src/normalizeWheel.js\n if (source.deltaMode === 1 /* LINE */) {\n // Convert from lines to pixels, more if the user is scrolling fast.\n // (I made up the exp function to roughly match Firefox to what Chrome does)\n // These numbers should be floats, because integers are treated as pan gesture below.\n var lines = Math.abs(source.deltaY);\n var sign = (source.deltaY > 0) ? 1 : -1;\n dY = sign * clamp(\n lines * 18.001,\n 4.000244140625, // min\n 350.000244140625 // max\n );\n\n // recalculate x2,y2,k2\n t0 = _isTransformed ? _transformLast : _transformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * Math.pow(2, -dY / 500);\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // 2 finger map pinch zooming (Safari) - #5492\n // These are fake `wheel` events we made from Safari `gesturechange` events..\n } else if (source._scale) {\n // recalculate x2,y2,k2\n t0 = _gestureTransformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * source._scale;\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // 2 finger map pinch zooming (all browsers except Safari) - #5492\n // Pinch zooming via the `wheel` event will always have:\n // - `ctrlKey = true`\n // - `deltaY` is not round integer pixels (ignore `deltaX`)\n } else if (source.ctrlKey && !isInteger(dY)) {\n dY *= 6; // slightly scale up whatever the browser gave us\n\n // recalculate x2,y2,k2\n t0 = _isTransformed ? _transformLast : _transformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * Math.pow(2, -dY / 500);\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // Trackpad scroll zooming with shift or alt/option key down\n } else if ((source.altKey || source.shiftKey) && isInteger(dY)) {\n // recalculate x2,y2,k2\n t0 = _isTransformed ? _transformLast : _transformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * Math.pow(2, -dY / 500);\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // 2 finger map panning (Mac only, all browsers except Firefox #8595) - #5492, #5512\n // Panning via the `wheel` event will always have:\n // - `ctrlKey = false`\n // - `deltaX`,`deltaY` are round integer pixels\n } else if (detected.os === 'mac' && detected.browser !== 'Firefox' && !source.ctrlKey && isInteger(dX) && isInteger(dY)) {\n p1 = projection.translate();\n x2 = p1[0] - dX;\n y2 = p1[1] - dY;\n k2 = projection.scale();\n k2 = clamp(k2, kMin, kMax);\n }\n\n // something changed - replace the event transform\n if (x2 !== x || y2 !== y || k2 !== k) {\n x = x2;\n y = y2;\n k = k2;\n eventTransform = d3_zoomIdentity.translate(x2, y2).scale(k2);\n if (_zoomerPanner._transform) {\n // utilZoomPan interface\n _zoomerPanner._transform(eventTransform);\n } else {\n // d3_zoom interface\n _selection.node().__zoom = eventTransform;\n }\n }\n\n }\n\n if (_transformStart.x === x &&\n _transformStart.y === y &&\n _transformStart.k === k) {\n return; // no change\n }\n\n if (geoScaleToZoom(k, TILESIZE) < _minzoom) {\n surface.interrupt();\n dispatch.call('hitMinZoom', this, map);\n setCenterZoom(map.center(), context.minEditableZoom(), 0, true);\n scheduleRedraw();\n dispatch.call('move', this, map);\n return;\n }\n\n projection.transform(eventTransform);\n\n var withinEditableZoom = map.withinEditableZoom();\n if (_lastWithinEditableZoom !== withinEditableZoom) {\n if (_lastWithinEditableZoom !== undefined) {\n // notify that the map zoomed in or out over the editable zoom threshold\n dispatch.call('crossEditableZoom', this, withinEditableZoom);\n }\n _lastWithinEditableZoom = withinEditableZoom;\n }\n\n var scale = k / _transformStart.k;\n var tX = (x / scale - _transformStart.x) * scale;\n var tY = (y / scale - _transformStart.y) * scale;\n\n if (context.inIntro()) {\n curtainProjection.transform({\n x: x - tX,\n y: y - tY,\n k: k\n });\n }\n\n if (source) {\n _lastPointerEvent = event;\n }\n _isTransformed = true;\n _transformLast = eventTransform;\n utilSetTransform(supersurface, tX, tY, scale);\n scheduleRedraw();\n\n dispatch.call('move', this, map);\n\n\n function isInteger(val) {\n return typeof val === 'number' && isFinite(val) && Math.floor(val) === val;\n }\n }\n\n\n function resetTransform() {\n if (!_isTransformed) return false;\n\n utilSetTransform(supersurface, 0, 0);\n _isTransformed = false;\n if (context.inIntro()) {\n curtainProjection.transform(projection.transform());\n }\n return true;\n }\n\n\n function redraw(difference, extent) {\n // in unit tests, we need to abort if the test has already completed\n if (typeof window === 'undefined') return;\n\n if (surface.empty() || !_redrawEnabled) return;\n\n // If we are in the middle of a zoom/pan, we can't do differenced redraws.\n // It would result in artifacts where differenced entities are redrawn with\n // one transform and unchanged entities with another.\n if (resetTransform()) {\n difference = undefined;\n extent = undefined;\n }\n\n var zoom = map.zoom();\n var z = String(~~zoom);\n\n if (surface.attr('data-zoom') !== z) {\n surface.attr('data-zoom', z);\n }\n\n // class surface as `lowzoom` around z17-z18.5 (based on latitude)\n var lat = map.center()[1];\n var lowzoom = d3_scaleLinear()\n .domain([-60, 0, 60])\n .range([17, 18.5, 17])\n .clamp(true);\n\n surface\n .classed('low-zoom', zoom <= lowzoom(lat));\n\n\n if (!difference) {\n supersurface.call(context.background());\n wrapper.call(drawLayers);\n }\n\n // OSM\n if (map.editableDataEnabled() || map.isInWideSelection()) {\n context.loadTiles(projection);\n drawEditable(difference, extent);\n } else {\n editOff();\n }\n\n _transformStart = projection.transform();\n\n return map;\n }\n\n\n\n var immediateRedraw = function(difference, extent) {\n if (!difference && !extent) cancelPendingRedraw();\n redraw(difference, extent);\n };\n\n\n map.lastPointerEvent = function() {\n return _lastPointerEvent;\n };\n\n\n map.mouse = function(d3_event) {\n var event = d3_event || _lastPointerEvent;\n if (event) {\n var s;\n while ((s = event.sourceEvent)) { event = s; }\n return _getMouseCoords(event);\n }\n return null;\n };\n\n\n // returns Lng/Lat\n map.mouseCoordinates = function() {\n var coord = map.mouse() || pxCenter();\n return projection.invert(coord);\n };\n\n\n map.dblclickZoomEnable = function(val) {\n if (!arguments.length) return _dblClickZoomEnabled;\n _dblClickZoomEnabled = val;\n return map;\n };\n\n\n map.redrawEnable = function(val) {\n if (!arguments.length) return _redrawEnabled;\n _redrawEnabled = val;\n return map;\n };\n\n\n map.isTransformed = function() {\n return _isTransformed;\n };\n\n\n function setTransform(t2, duration, force) {\n var t = projection.transform();\n if (!force && t2.k === t.k && t2.x === t.x && t2.y === t.y) return false;\n\n if (duration) {\n _selection\n .transition()\n .duration(duration)\n .on('start', function() { map.startEase(); })\n .call(_zoomerPanner.transform, d3_zoomIdentity.translate(t2.x, t2.y).scale(t2.k));\n } else {\n projection.transform(t2);\n _transformStart = t2;\n _selection.call(_zoomerPanner.transform, _transformStart);\n }\n\n return true;\n }\n\n\n function setCenterZoom(loc2, z2, duration, force) {\n var c = map.center();\n var z = map.zoom();\n if (loc2[0] === c[0] && loc2[1] === c[1] && z2 === z && !force) return false;\n\n var proj = geoRawMercator().transform(projection.transform()); // copy projection\n\n var k2 = clamp(geoZoomToScale(z2, TILESIZE), kMin, kMax);\n proj.scale(k2);\n\n var t = proj.translate();\n var point = proj(loc2);\n\n var center = pxCenter();\n t[0] += center[0] - point[0];\n t[1] += center[1] - point[1];\n\n return setTransform(d3_zoomIdentity.translate(t[0], t[1]).scale(k2), duration, force);\n }\n\n\n map.pan = function(delta, duration) {\n var t = projection.translate();\n var k = projection.scale();\n\n t[0] += delta[0];\n t[1] += delta[1];\n\n if (duration) {\n _selection\n .transition()\n .duration(duration)\n .on('start', function() { map.startEase(); })\n .call(_zoomerPanner.transform, d3_zoomIdentity.translate(t[0], t[1]).scale(k));\n } else {\n projection.translate(t);\n _transformStart = projection.transform();\n _selection.call(_zoomerPanner.transform, _transformStart);\n dispatch.call('move', this, map);\n immediateRedraw();\n }\n\n return map;\n };\n\n\n map.dimensions = function(val) {\n if (!arguments.length) return _dimensions;\n\n _dimensions = val;\n drawLayers.dimensions(_dimensions);\n context.background().dimensions(_dimensions);\n projection.clipExtent([[0, 0], _dimensions]);\n _getMouseCoords = utilFastMouse(supersurface.node());\n\n scheduleRedraw();\n return map;\n };\n\n\n function zoomIn(delta) {\n setCenterZoom(map.center(), Math.trunc(map.zoom() + 0.45) + delta, 150, true);\n }\n\n function zoomOut(delta) {\n setCenterZoom(map.center(), Math.ceil(map.zoom() - 0.45) - delta, 150, true);\n }\n\n map.zoomIn = function() { zoomIn(1); };\n map.zoomInFurther = function() { zoomIn(4); };\n map.canZoomIn = function() { return map.zoom() < maxZoom; };\n\n map.zoomOut = function() { zoomOut(1); };\n map.zoomOutFurther = function() { zoomOut(4); };\n map.canZoomOut = function() { return map.zoom() > minZoom; };\n\n map.center = function(loc2) {\n if (!arguments.length) {\n return projection.invert(pxCenter());\n }\n\n if (setCenterZoom(loc2, map.zoom())) {\n dispatch.call('move', this, map);\n }\n\n scheduleRedraw();\n return map;\n };\n\n function trimmedCenter(loc, zoom) {\n var offset = [paneWidth() / 2, (footerHeight() - toolbarHeight()) / 2];\n\n var proj = geoRawMercator().transform(projection.transform()); // copy projection\n // use the target zoom to calculate the offset center\n proj.scale(geoZoomToScale(zoom, TILESIZE));\n\n var locPx = proj(loc);\n var offsetLocPx = [locPx[0] + offset[0], locPx[1] + offset[1]];\n var offsetLoc = proj.invert(offsetLocPx);\n\n return offsetLoc;\n };\n\n function paneWidth() {\n const openPane = context.container().select('.map-panes .map-pane.shown');\n if (!openPane.empty()) {\n return openPane.node().offsetWidth;\n }\n return 0;\n };\n\n function toolbarHeight() {\n const toolbar = context.container().select('.top-toolbar');\n return toolbar.node().offsetHeight;\n };\n\n function footerHeight() {\n const footer = context.container().select('.map-footer-bar');\n return footer.node().offsetHeight;\n }\n\n map.zoom = function(z2) {\n if (!arguments.length) {\n return Math.max(geoScaleToZoom(projection.scale(), TILESIZE), 0);\n }\n\n if (z2 < _minzoom) {\n surface.interrupt();\n dispatch.call('hitMinZoom', this, map);\n z2 = context.minEditableZoom();\n }\n\n if (setCenterZoom(map.center(), z2)) {\n dispatch.call('move', this, map);\n }\n\n scheduleRedraw();\n return map;\n };\n\n\n map.centerZoom = function(loc2, z2) {\n if (setCenterZoom(loc2, z2)) {\n dispatch.call('move', this, map);\n }\n\n scheduleRedraw();\n return map;\n };\n\n\n map.zoomTo = function(what) {\n return map.zoomToEase(what, 0);\n };\n\n\n map.centerEase = function(loc2, duration) {\n duration = duration || 250;\n setCenterZoom(loc2, map.zoom(), duration);\n return map;\n };\n\n\n map.zoomEase = function(z2, duration) {\n duration = duration || 250;\n setCenterZoom(map.center(), z2, duration, false);\n return map;\n };\n\n\n map.centerZoomEase = function(loc2, z2, duration) {\n duration = duration || 250;\n setCenterZoom(loc2, z2, duration, false);\n return map;\n };\n\n\n map.transformEase = function(t2, duration) {\n duration = duration || 250;\n setTransform(t2, duration, false /* don't force */);\n return map;\n };\n\n\n map.zoomToEase = function(what, duration) {\n let extent;\n if (what instanceof geoExtent) {\n // we've directly been given an extent\n extent = what;\n } else {\n // we're given one or more entities to zoom to\n if (!isArray(what)) what = [what];\n extent = what\n .map(entity => entity.extent(context.graph()))\n .reduce((a, b) => a.extend(b));\n }\n\n if (!isFinite(extent.area())) return map;\n\n var z = clamp(map.trimmedExtentZoom(extent), 0, 20);\n const loc = trimmedCenter(extent.center(), z);\n\n if (duration === 0) {\n return map.centerZoom(loc, z);\n } else {\n return map.centerZoomEase(loc, z, duration);\n }\n };\n\n\n map.startEase = function() {\n utilBindOnce(surface, _pointerPrefix + 'down.ease', function() {\n map.cancelEase();\n });\n return map;\n };\n\n\n map.cancelEase = function() {\n _selection.interrupt();\n return map;\n };\n\n\n map.extent = function(val) {\n if (!arguments.length) {\n return new geoExtent(\n projection.invert([0, _dimensions[1]]),\n projection.invert([_dimensions[0], 0])\n );\n } else {\n var extent = geoExtent(val);\n map.centerZoom(extent.center(), map.extentZoom(extent));\n }\n };\n\n\n map.trimmedExtent = function(val) {\n if (!arguments.length) {\n var headerY = 71;\n var footerY = 30;\n var pad = 10;\n return new geoExtent(\n projection.invert([pad, _dimensions[1] - footerY - pad]),\n projection.invert([_dimensions[0] - pad, headerY + pad])\n );\n } else {\n var extent = geoExtent(val);\n map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));\n }\n };\n\n\n function calcExtentZoom(extent, dim) {\n var tl = projection([extent[0][0], extent[1][1]]);\n var br = projection([extent[1][0], extent[0][1]]);\n\n // Calculate maximum zoom that fits extent\n var hFactor = (br[0] - tl[0]) / dim[0];\n var vFactor = (br[1] - tl[1]) / dim[1];\n var hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2;\n var vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2;\n var newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff);\n\n return newZoom;\n }\n\n\n map.extentZoom = function(val) {\n return calcExtentZoom(geoExtent(val), _dimensions);\n };\n\n\n map.trimmedExtentZoom = function(val) {\n const trim = 40;\n const trimmed = [\n _dimensions[0] - trim - paneWidth(),\n _dimensions[1] - trim - toolbarHeight() - footerHeight()\n ];\n return calcExtentZoom(geoExtent(val), trimmed);\n };\n\n\n map.withinEditableZoom = function() {\n return map.zoom() >= context.minEditableZoom();\n };\n\n\n map.isInWideSelection = function() {\n return !map.withinEditableZoom() && context.selectedIDs().length;\n };\n\n\n map.editableDataEnabled = function(skipZoomCheck) {\n\n var layer = context.layers().layer('osm');\n if (!layer || !layer.enabled()) return false;\n\n return skipZoomCheck || map.withinEditableZoom();\n };\n\n\n map.notesEditable = function() {\n var layer = context.layers().layer('notes');\n if (!layer || !layer.enabled()) return false;\n\n return map.withinEditableZoom();\n };\n\n\n map.minzoom = function(val) {\n if (!arguments.length) return _minzoom;\n _minzoom = val;\n return map;\n };\n\n\n map.toggleHighlightEdited = function() {\n surface.classed('highlight-edited', !surface.classed('highlight-edited'));\n map.pan([0,0]); // trigger a redraw\n dispatch.call('changeHighlighting', this);\n };\n\n\n map.areaFillOptions = ['wireframe', 'partial', 'full'];\n\n map.activeAreaFill = function(val) {\n if (!arguments.length) return prefs('area-fill') || 'partial';\n\n prefs('area-fill', val);\n if (val !== 'wireframe') {\n prefs('area-fill-toggle', val);\n }\n updateAreaFill();\n map.pan([0,0]); // trigger a redraw\n dispatch.call('changeAreaFill', this);\n return map;\n };\n\n map.toggleWireframe = function() {\n\n var activeFill = map.activeAreaFill();\n\n if (activeFill === 'wireframe') {\n activeFill = prefs('area-fill-toggle') || 'partial';\n } else {\n activeFill = 'wireframe';\n }\n\n map.activeAreaFill(activeFill);\n };\n\n function updateAreaFill() {\n var activeFill = map.activeAreaFill();\n map.areaFillOptions.forEach(function(opt) {\n surface.classed('fill-' + opt, Boolean(opt === activeFill));\n });\n }\n\n\n map.layers = () => drawLayers;\n\n\n map.doubleUpHandler = function() {\n return _doubleUpHandler;\n };\n\n\n return utilRebind(map, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { services } from '../services';\nimport { utilRebind } from '../util/rebind';\nimport { utilQsString, utilStringQs } from '../util';\n\n\nexport function rendererPhotos(context) {\n var dispatch = d3_dispatch('change');\n var _layerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'kartaview', 'mapilio', 'vegbilder', 'panoramax'];\n var _allPhotoTypes = ['flat', 'panoramic'];\n var _shownPhotoTypes = _allPhotoTypes.slice(); // shallow copy\n var _dateFilters = ['fromDate', 'toDate'];\n var _fromDate;\n var _toDate;\n var _usernames;\n\n function photos() {}\n\n function updateStorage() {\n var hash = utilStringQs(window.location.hash);\n var enabled = context.layers().all().filter(function(d) {\n return _layerIDs.indexOf(d.id) !== -1 && d.layer && d.layer.supported() && d.layer.enabled();\n }).map(function(d) {\n return d.id;\n });\n if (enabled.length) {\n hash.photo_overlay = enabled.join(',');\n } else {\n delete hash.photo_overlay;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n }\n\n /**\n * @returns The layer ID\n */\n photos.overlayLayerIDs = function() {\n return _layerIDs;\n };\n\n /**\n * @returns All the photo types\n */\n photos.allPhotoTypes = function() {\n return _allPhotoTypes;\n };\n\n /**\n * @returns The date filters value\n */\n photos.dateFilters = function() {\n return _dateFilters;\n };\n\n photos.dateFilterValue = function(val) {\n return val === _dateFilters[0] ? _fromDate : _toDate;\n };\n\n /**\n * Sets the date filter (min/max date)\n * @param {*} type Either 'fromDate' or 'toDate'\n * @param {*} val The actual Date\n * @param {boolean} updateUrl Whether the URL should update or not\n */\n photos.setDateFilter = function(type, val, updateUrl) {\n // validate the date\n var date = val && new Date(val);\n if (date && !isNaN(date)) {\n val = date.toISOString().slice(0, 10);\n } else {\n val = null;\n }\n if (type === _dateFilters[0]) {\n _fromDate = val;\n if (_fromDate && _toDate && new Date(_toDate) < new Date(_fromDate)) {\n _toDate = _fromDate;\n }\n }\n if (type === _dateFilters[1]) {\n _toDate = val;\n if (_fromDate && _toDate && new Date(_toDate) < new Date(_fromDate)) {\n _fromDate = _toDate;\n }\n }\n dispatch.call('change', this);\n if (updateUrl) {\n var rangeString;\n if (_fromDate || _toDate) {\n rangeString = (_fromDate || '') + '_' + (_toDate || '');\n }\n setUrlFilterValue('photo_dates', rangeString);\n }\n };\n\n /**\n * Sets the username filter\n * @param {string} val The username\n * @param {boolean} updateUrl Whether the URL should update or not\n */\n photos.setUsernameFilter = function(val, updateUrl) {\n if (val && typeof val === 'string') val = val.replace(/;/g, ',').split(',');\n if (val) {\n val = val.map(d => d.trim()).filter(Boolean);\n if (!val.length) {\n val = null;\n }\n }\n _usernames = val;\n dispatch.call('change', this);\n if (updateUrl) {\n var hashString;\n if (_usernames) {\n hashString = _usernames.join(',');\n }\n setUrlFilterValue('photo_username', hashString);\n }\n };\n\n /**\n * Util function to set the slider date filter\n * @param {*} val Either 'panoramic' or 'flat'\n * @param {boolean} updateUrl Whether the URL should update or not\n */\n photos.togglePhotoType = function(val, updateUrl) {\n var index = _shownPhotoTypes.indexOf(val);\n if (index !== -1) {\n _shownPhotoTypes.splice(index, 1);\n } else {\n _shownPhotoTypes.push(val);\n }\n\n if (updateUrl) {\n var hashString;\n if (_shownPhotoTypes) {\n hashString = _shownPhotoTypes.join(',');\n }\n setUrlFilterValue('photo_type', hashString);\n }\n\n dispatch.call('change', this);\n return photos;\n };\n\n /**\n * Updates the URL with new values\n * @param {*} val value to save\n * @param {string} property Name of the value\n */\n function setUrlFilterValue(property, val) {\n const hash = utilStringQs(window.location.hash);\n if (val) {\n if (hash[property] === val) return;\n hash[property] = val;\n } else {\n if (!(property in hash)) return;\n delete hash[property];\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n }\n\n function showsLayer(id) {\n var layer = context.layers().layer(id);\n return layer && layer.supported() && layer.enabled();\n }\n\n /**\n * @returns If the Date Slider filter should be drawn\n */\n photos.shouldFilterDateBySlider = function(){\n return showsLayer('mapillary') || showsLayer('kartaview') || showsLayer('mapilio')\n || showsLayer('streetside') || showsLayer('vegbilder') || showsLayer('panoramax');\n };\n\n /**\n * @returns If the Photo Type filter should be drawn\n */\n photos.shouldFilterByPhotoType = function() {\n return showsLayer('mapillary') ||\n (showsLayer('streetside') && showsLayer('kartaview')) || showsLayer('vegbilder') || showsLayer('panoramax');\n };\n\n /**\n * @returns If the Username filter should be drawn\n */\n photos.shouldFilterByUsername = function() {\n return !showsLayer('mapillary') && showsLayer('kartaview') && !showsLayer('streetside') || showsLayer('panoramax');\n };\n\n photos.showsPhotoType = function(val) {\n if (!photos.shouldFilterByPhotoType()) return true;\n\n return _shownPhotoTypes.indexOf(val) !== -1;\n };\n\n photos.showsFlat = function() {\n return photos.showsPhotoType('flat');\n };\n\n photos.showsPanoramic = function() {\n return photos.showsPhotoType('panoramic');\n };\n\n photos.fromDate = function() {\n return _fromDate;\n };\n\n photos.toDate = function() {\n return _toDate;\n };\n\n photos.usernames = function() {\n return _usernames;\n };\n\n /**\n * Inits the streetlevel layer given the saved values in the URL\n */\n photos.init = function() {\n var hash = utilStringQs(window.location.hash);\n var parts;\n if (hash.photo_dates) {\n // expect format like `photo_dates=2019-01-01_2020-12-31`, but allow a couple different separators\n parts = /^(.*)[\u2013_](.*)$/g.exec(hash.photo_dates.trim());\n this.setDateFilter('fromDate', parts && parts.length >= 2 && parts[1], false);\n this.setDateFilter('toDate', parts && parts.length >= 3 && parts[2], false);\n }\n if (hash.photo_username) {\n this.setUsernameFilter(hash.photo_username, false);\n }\n if (hash.photo_type) {\n parts = hash.photo_type.replace(/;/g, ',').split(',');\n _allPhotoTypes.forEach(d => {\n if (!parts.includes(d)) this.togglePhotoType(d, false);\n });\n }\n if (hash.photo_overlay) {\n // support enabling photo layers by default via a URL parameter, e.g. `photo_overlay=kartaview;mapillary;streetside`\n var hashOverlayIDs = hash.photo_overlay.replace(/;/g, ',').split(',');\n hashOverlayIDs.forEach(function(id) {\n if (id === 'openstreetcam') id = 'kartaview'; // legacy alias\n var layer = _layerIDs.indexOf(id) !== -1 && context.layers().layer(id);\n if (layer && !layer.enabled()) layer.enabled(true);\n });\n }\n if (hash.photo) {\n // support opening a photo via a URL parameter, e.g. `photo=mapillary-fztgSDtLpa08ohPZFZjeRQ`\n var photoIds = hash.photo.replace(/;/g, ',').split(',');\n var photoId = photoIds.length && photoIds[0].trim();\n var results = /(.*)\\/(.*)/g.exec(photoId);\n if (results && results.length >= 3) {\n var serviceId = results[1];\n if (serviceId === 'openstreetcam') serviceId = 'kartaview'; // legacy alias\n var photoKey = results[2];\n var service = services[serviceId];\n if (service && service.ensureViewerLoaded) {\n\n // if we're showing a photo then make sure its layer is enabled too\n var layer = _layerIDs.indexOf(serviceId) !== -1 && context.layers().layer(serviceId);\n if (layer && !layer.enabled()) layer.enabled(true);\n\n var baselineTime = Date.now();\n\n service.on('loadedImages.rendererPhotos', function() {\n // don't open the viewer if too much time has elapsed\n if (Date.now() - baselineTime > 45000) {\n service.on('loadedImages.rendererPhotos', null);\n return;\n }\n\n if (!service.cachedImage(photoKey)) return;\n\n service.on('loadedImages.rendererPhotos', null);\n service.ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, photoKey)\n .showViewer(context);\n });\n });\n }\n }\n }\n\n context.layers().on('change.rendererPhotos', updateStorage);\n };\n\n return utilRebind(photos, dispatch, 'on');\n}\n", "export { rendererBackgroundSource } from './background_source';\nexport { rendererBackground } from './background';\nexport { rendererFeatures } from './features';\nexport { rendererMap } from './map';\nexport { rendererPhotos } from './photos';\nexport { rendererTileLayer } from './tile_layer';\n", "import { geoPath as d3_geoPath } from 'd3-geo';\nimport { select as d3_select } from 'd3-selection';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport { t } from '../core/localizer';\nimport { geoRawMercator, geoScaleToZoom, geoVecSubtract, geoVecScale, geoZoomToScale } from '../geo';\nimport { rendererTileLayer } from '../renderer';\nimport { svgDebug, svgData } from '../svg';\nimport { utilSetTransform } from '../util';\n// import { utilGetDimensions } from '../util/dimensions';\n\n\nexport function uiMapInMap(context) {\n\n function mapInMap(selection) {\n var backgroundLayer = rendererTileLayer(context)\n .underzoom(2);\n var overlayLayers = {};\n var projection = geoRawMercator();\n var dataLayer = svgData(projection, context).showLabels(false);\n var debugLayer = svgDebug(projection, context);\n var zoom = d3_zoom()\n .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(24)])\n .on('start', zoomStarted)\n .on('zoom', zoomed)\n .on('end', zoomEnded);\n\n var wrap = d3_select(null);\n var tiles = d3_select(null);\n var viewport = d3_select(null);\n\n var _isTransformed = false;\n var _isHidden = true;\n var _skipEvents = false;\n var _gesture = null;\n var _zDiff = 6; // by default, minimap renders at (main zoom - 6)\n var _dMini; // dimensions of minimap\n var _cMini; // center pixel of minimap\n var _tStart; // transform at start of gesture\n var _tCurr; // transform at most recent event\n var _timeoutID;\n\n\n function zoomStarted() {\n if (_skipEvents) return;\n _tCurr = projection.transform();\n _tStart = _tCurr;\n _gesture = null;\n }\n\n\n function zoomed(d3_event) {\n if (_skipEvents) return;\n\n var x = d3_event.transform.x;\n var y = d3_event.transform.y;\n var k = d3_event.transform.k;\n var isZooming = (k !== _tStart.k);\n var isPanning = (x !== _tStart.x || y !== _tStart.y);\n\n if (!isZooming && !isPanning) {\n return; // no change\n }\n\n // lock in either zooming or panning, don't allow both in minimap.\n if (!_gesture) {\n _gesture = isZooming ? 'zoom' : 'pan';\n }\n\n var tMini = projection.transform();\n var tX, tY, scale;\n\n if (_gesture === 'zoom') {\n scale = k / tMini.k;\n tX = (_cMini[0] / scale - _cMini[0]) * scale;\n tY = (_cMini[1] / scale - _cMini[1]) * scale;\n } else {\n k = tMini.k;\n scale = 1;\n tX = x - tMini.x;\n tY = y - tMini.y;\n }\n\n utilSetTransform(tiles, tX, tY, scale);\n utilSetTransform(viewport, 0, 0, scale);\n _isTransformed = true;\n _tCurr = d3_zoomIdentity.translate(x, y).scale(k);\n\n var zMain = geoScaleToZoom(context.projection.scale());\n var zMini = geoScaleToZoom(k);\n\n _zDiff = zMain - zMini;\n\n queueRedraw();\n }\n\n\n function zoomEnded() {\n if (_skipEvents) return;\n if (_gesture !== 'pan') return;\n\n updateProjection();\n _gesture = null;\n context.map().center(projection.invert(_cMini)); // recenter main map..\n }\n\n\n function updateProjection() {\n var loc = context.map().center();\n var tMain = context.projection.transform();\n var zMain = geoScaleToZoom(tMain.k);\n var zMini = Math.max(zMain - _zDiff, 0.5);\n var kMini = geoZoomToScale(zMini);\n\n projection\n .translate([tMain.x, tMain.y])\n .scale(kMini);\n\n var point = projection(loc);\n var mouse = (_gesture === 'pan') ? geoVecSubtract([_tCurr.x, _tCurr.y], [_tStart.x, _tStart.y]) : [0, 0];\n var xMini = _cMini[0] - point[0] + tMain.x + mouse[0];\n var yMini = _cMini[1] - point[1] + tMain.y + mouse[1];\n\n projection\n .translate([xMini, yMini])\n .clipExtent([[0, 0], _dMini]);\n\n _tCurr = projection.transform();\n\n if (_isTransformed) {\n utilSetTransform(tiles, 0, 0);\n utilSetTransform(viewport, 0, 0);\n _isTransformed = false;\n }\n\n zoom\n .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(zMain - 3)]);\n\n _skipEvents = true;\n wrap.call(zoom.transform, _tCurr);\n _skipEvents = false;\n }\n\n\n function redraw() {\n clearTimeout(_timeoutID);\n if (_isHidden) return;\n\n updateProjection();\n var zMini = geoScaleToZoom(projection.scale());\n\n // setup tile container\n tiles = wrap\n .selectAll('.map-in-map-tiles')\n .data([0]);\n\n tiles = tiles.enter()\n .append('div')\n .attr('class', 'map-in-map-tiles')\n .merge(tiles);\n\n // redraw background\n backgroundLayer\n .source(context.background().baseLayerSource())\n .projection(projection)\n .dimensions(_dMini);\n\n var background = tiles\n .selectAll('.map-in-map-background')\n .data([0]);\n\n background.enter()\n .append('div')\n .attr('class', 'map-in-map-background')\n .merge(background)\n .call(backgroundLayer);\n\n\n // redraw overlay\n var overlaySources = context.background().overlayLayerSources();\n var activeOverlayLayers = [];\n for (var i = 0; i < overlaySources.length; i++) {\n if (overlaySources[i].validZoom(zMini)) {\n if (!overlayLayers[i]) overlayLayers[i] = rendererTileLayer(context);\n activeOverlayLayers.push(overlayLayers[i]\n .source(overlaySources[i])\n .projection(projection)\n .dimensions(_dMini));\n }\n }\n\n var overlay = tiles\n .selectAll('.map-in-map-overlay')\n .data([0]);\n\n overlay = overlay.enter()\n .append('div')\n .attr('class', 'map-in-map-overlay')\n .merge(overlay);\n\n\n var overlays = overlay\n .selectAll('div')\n .data(activeOverlayLayers, function(d) { return d.source().name(); });\n\n overlays.exit()\n .remove();\n\n overlays.enter()\n .append('div')\n .merge(overlays)\n .each(function(layer) { d3_select(this).call(layer); });\n\n\n var dataLayers = tiles\n .selectAll('.map-in-map-data')\n .data([0]);\n\n dataLayers.exit()\n .remove();\n\n dataLayers.enter()\n .append('svg')\n .attr('class', 'map-in-map-data')\n .merge(dataLayers)\n .call(dataLayer)\n .call(debugLayer);\n\n\n // redraw viewport bounding box\n if (_gesture !== 'pan') {\n var getPath = d3_geoPath(projection);\n var bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] };\n\n viewport = wrap.selectAll('.map-in-map-viewport')\n .data([0]);\n\n viewport = viewport.enter()\n .append('svg')\n .attr('class', 'map-in-map-viewport')\n .merge(viewport);\n\n\n var path = viewport.selectAll('.map-in-map-bbox')\n .data([bbox]);\n\n path.enter()\n .append('path')\n .attr('class', 'map-in-map-bbox')\n .merge(path)\n .attr('d', getPath)\n .classed('thick', function(d) { return getPath.area(d) < 30; });\n }\n }\n\n\n function queueRedraw() {\n clearTimeout(_timeoutID);\n _timeoutID = setTimeout(function() { redraw(); }, 750);\n }\n\n\n function toggle(d3_event) {\n if (d3_event) d3_event.preventDefault();\n\n _isHidden = !_isHidden;\n\n context.container().select('.minimap-toggle-item')\n .classed('active', !_isHidden)\n .select('input')\n .property('checked', !_isHidden);\n\n if (_isHidden) {\n wrap\n .style('display', 'block')\n .style('opacity', '1')\n .transition()\n .duration(200)\n .style('opacity', '0')\n .on('end', function() {\n selection.selectAll('.map-in-map')\n .style('display', 'none');\n });\n } else {\n wrap\n .style('display', 'block')\n .style('opacity', '0')\n .transition()\n .duration(200)\n .style('opacity', '1')\n .on('end', function() {\n redraw();\n });\n }\n }\n\n\n uiMapInMap.toggle = toggle;\n\n wrap = selection.selectAll('.map-in-map')\n .data([0]);\n\n wrap = wrap.enter()\n .append('div')\n .attr('class', 'map-in-map')\n .style('display', (_isHidden ? 'none' : 'block'))\n .call(zoom)\n .on('dblclick.zoom', null)\n .merge(wrap);\n\n // reflow warning: Hardcode dimensions - currently can't resize it anyway..\n _dMini = [200,150]; //utilGetDimensions(wrap);\n _cMini = geoVecScale(_dMini, 0.5);\n\n context.map()\n .on('drawn.map-in-map', function(drawn) {\n if (drawn.full === true) {\n redraw();\n }\n });\n\n redraw();\n\n context.keybinding()\n .on(t('background.minimap.key'), toggle);\n }\n\n return mapInMap;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/index';\n\n\nexport function uiNotice(context) {\n\n return function(selection) {\n var div = selection\n .append('div')\n .attr('class', 'notice');\n\n var button = div\n .append('button')\n .attr('class', 'zoom-to notice fillD')\n .on('click', function() {\n context.map().zoomEase(context.minEditableZoom());\n })\n .on('wheel', function(d3_event) { // let wheel events pass through #4482\n var e2 = new WheelEvent(d3_event.type, d3_event);\n context.surface().node().dispatchEvent(e2);\n });\n\n button\n .call(svgIcon('#iD-icon-plus', 'pre-text'))\n .append('span')\n .attr('class', 'label')\n .call(t.append('zoom_in_edit'));\n\n\n function disableTooHigh() {\n var canEdit = context.map().zoom() >= context.minEditableZoom();\n div.style('display', canEdit ? 'none' : 'block');\n }\n\n context.map()\n .on('move.notice', debounce(disableTooHigh, 500));\n\n disableTooHigh();\n };\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\nimport { clamp } from 'es-toolkit/compat';\n\nimport { t } from '../core/localizer';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { svgIcon } from '../svg/icon';\nimport { utilGetDimensions } from '../util/dimensions';\nimport { utilRebind } from '../util';\nimport { services } from '../services';\nimport { uiTooltip } from './tooltip';\nimport { actionChangeTags } from '../actions';\nimport { geoSphericalDistance } from '../geo';\n\nexport function uiPhotoviewer(context) {\n\n var dispatch = d3_dispatch('resize');\n\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n const addPhotoIdButton = new Set(['mapillary', 'panoramax']);\n\n function photoviewer(selection) {\n selection\n .append('button')\n .attr('class', 'thumb-hide')\n .attr('title', t('icons.close'))\n .on('click', function () {\n for (const service of Object.values(services)) {\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n })\n .append('div')\n .call(svgIcon('#iD-icon-close'));\n\n function preventDefault(d3_event) {\n d3_event.preventDefault();\n }\n\n selection\n .append('button')\n .attr('class', 'resize-handle-xy')\n .on('touchstart touchdown touchend', preventDefault)\n .on(\n _pointerPrefix + 'down',\n buildResizeListener(selection, 'resize', dispatch, { resizeOnX: true, resizeOnY: true })\n );\n\n selection\n .append('button')\n .attr('class', 'resize-handle-x')\n .on('touchstart touchdown touchend', preventDefault)\n .on(\n _pointerPrefix + 'down',\n buildResizeListener(selection, 'resize', dispatch, { resizeOnX: true })\n );\n\n selection\n .append('button')\n .attr('class', 'resize-handle-y')\n .on('touchstart touchdown touchend', preventDefault)\n .on(\n _pointerPrefix + 'down',\n buildResizeListener(selection, 'resize', dispatch, { resizeOnY: true })\n );\n\n // update sett_photo_from_viewer button on selection change and when tags change\n context.features().on('change.setPhotoFromViewer', function() {\n setPhotoTagButton();\n });\n context.history().on('change.setPhotoFromViewer', function() {\n setPhotoTagButton();\n });\n\n\n function setPhotoTagButton() {\n const service = getServiceId();\n const isActiveForService = addPhotoIdButton.has(service) &&\n services[service].isViewerOpen() &&\n layerEnabled(service) &&\n context.mode().id === 'select';\n\n renderAddPhotoIdButton(service, isActiveForService);\n\n function layerEnabled(which) {\n const layers = context.layers();\n const layer = layers.layer(which);\n return layer.enabled();\n }\n\n function getServiceId() {\n for (const serviceId in services) {\n const service = services[serviceId];\n if (typeof service.isViewerOpen === 'function') {\n if (service.isViewerOpen()) {\n return serviceId;\n }\n }\n }\n return false;\n }\n\n function renderAddPhotoIdButton(service, shouldDisplay) {\n const button = selection.selectAll('.set-photo-from-viewer')\n .data(shouldDisplay ? [0] : []);\n\n button.exit()\n .remove();\n\n const buttonEnter = button.enter()\n .append('button')\n .attr('class', 'set-photo-from-viewer')\n .call(svgIcon('#fas-eye-dropper'))\n .call(uiTooltip()\n .title(() => t.append('inspector.set_photo_from_viewer.enable'))\n .placement('right')\n );\n\n buttonEnter\n .select('.tooltip')\n .classed('dark', true)\n .style('width', '300px');\n\n buttonEnter\n .merge(button)\n .on('click', function (e) {\n e.preventDefault();\n e.stopPropagation();\n const activeServiceId = getServiceId();\n const image = services[activeServiceId].getActiveImage();\n\n const action = graph =>\n context.selectedIDs().reduce((graph, entityID) => {\n const tags = graph.entity(entityID).tags;\n const action = actionChangeTags(entityID, {...tags, [activeServiceId]: image.id});\n return action(graph);\n }, graph);\n\n const annotation = t('operations.change_tags.annotation');\n context.perform(action, annotation);\n buttonDisable('already_set');\n });\n\n if (service === 'panoramax') {\n const panoramaxControls = selection.select('.panoramax-wrapper .pnlm-zoom-controls.pnlm-controls');\n\n panoramaxControls\n .style('margin-top', shouldDisplay ? '36px' : '6px');\n }\n\n if (!shouldDisplay) return;\n\n const activeImage = services[service].getActiveImage();\n\n const graph = context.graph();\n const entities = context.selectedIDs()\n .map(id => graph.hasEntity(id))\n .filter(Boolean);\n\n if (entities.map(entity => entity.tags[service])\n .every(value => value === activeImage?.id)) {\n buttonDisable('already_set');\n } else if (activeImage && entities\n .map(entity => entity.extent(context.graph()).center())\n .every(loc => geoSphericalDistance(loc, activeImage.loc) > 100)) {\n buttonDisable('too_far');\n } else {\n buttonDisable(false);\n }\n }\n\n function buttonDisable(reason) {\n const disabled = reason !== false;\n const button = selection.selectAll('.set-photo-from-viewer').data([0]);\n button.attr('disabled', disabled ? 'true' : null);\n button.classed('disabled', disabled);\n button.call(uiTooltip().destroyAny);\n if (disabled) {\n button.call(uiTooltip()\n .title(() => t.append(`inspector.set_photo_from_viewer.disable.${reason}`))\n .placement('right')\n );\n } else {\n button.call(uiTooltip()\n .title(() => t.append('inspector.set_photo_from_viewer.enable'))\n .placement('right')\n );\n }\n\n button.select('.tooltip')\n .classed('dark', true)\n .style('width', '300px');\n }\n }\n\n function buildResizeListener(target, eventName, dispatch, options) {\n\n var resizeOnX = !!options.resizeOnX;\n var resizeOnY = !!options.resizeOnY;\n var minHeight = options.minHeight || 240;\n var minWidth = options.minWidth || 320;\n var pointerId;\n var startX;\n var startY;\n var startWidth;\n var startHeight;\n\n function startResize(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n var mapSize = context.map().dimensions();\n\n if (resizeOnX) {\n var mapWidth = mapSize[0];\n const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-left'), 10);\n var newWidth = clamp((startWidth + d3_event.clientX - startX), minWidth, mapWidth - viewerMargin * 2);\n target.style('width', newWidth + 'px');\n }\n\n if (resizeOnY) {\n const menuHeight = utilGetDimensions(d3_select('.top-toolbar'))[1] +\n utilGetDimensions(d3_select('.map-footer'))[1];\n const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-bottom'), 10);\n var maxHeight = mapSize[1] - menuHeight - viewerMargin * 2; // preserve space at top/bottom of map\n var newHeight = clamp((startHeight + startY - d3_event.clientY), minHeight, maxHeight);\n target.style('height', newHeight + 'px');\n }\n\n dispatch.call(eventName, target, subtractPadding(utilGetDimensions(target, true), target));\n }\n\n function stopResize(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n // remove all the listeners we added\n d3_select(window)\n .on('.' + eventName, null);\n }\n\n return function initResize(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n pointerId = d3_event.pointerId || 'mouse';\n\n startX = d3_event.clientX;\n startY = d3_event.clientY;\n var targetRect = target.node().getBoundingClientRect();\n startWidth = targetRect.width;\n startHeight = targetRect.height;\n\n d3_select(window)\n .on(_pointerPrefix + 'move.' + eventName, startResize, false)\n .on(_pointerPrefix + 'up.' + eventName, stopResize, false);\n\n if (_pointerPrefix === 'pointer') {\n d3_select(window)\n .on('pointercancel.' + eventName, stopResize, false);\n }\n };\n }\n }\n\n photoviewer.onMapResize = function() {\n var photoviewer = context.container().select('.photoviewer');\n var content = context.container().select('.main-content');\n var mapDimensions = utilGetDimensions(content, true);\n const menuHeight = utilGetDimensions(d3_select('.top-toolbar'))[1] +\n utilGetDimensions(d3_select('.map-footer'))[1];\n const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-bottom'), 10);\n // shrink photo viewer if it is too big (preserves space at top and bottom of map used by menus)\n var photoDimensions = utilGetDimensions(photoviewer, true);\n if (photoDimensions[0] > mapDimensions[0] || photoDimensions[1] > (mapDimensions[1] - menuHeight - viewerMargin * 2)) {\n var setPhotoDimensions = [\n Math.min(photoDimensions[0], mapDimensions[0]),\n Math.min(photoDimensions[1], mapDimensions[1] - menuHeight - viewerMargin * 2),\n ];\n\n photoviewer\n .style('width', setPhotoDimensions[0] + 'px')\n .style('height', setPhotoDimensions[1] + 'px');\n\n dispatch.call('resize', photoviewer, subtractPadding(setPhotoDimensions, photoviewer));\n } else {\n dispatch.call('resize', photoviewer, subtractPadding(photoDimensions, photoviewer));\n }\n };\n\n function subtractPadding(dimensions, selection) {\n return [\n dimensions[0] - parseFloat(selection.style('padding-left')) - parseFloat(selection.style('padding-right')),\n dimensions[1] - parseFloat(selection.style('padding-top')) - parseFloat(selection.style('padding-bottom'))\n ];\n }\n\n photoviewer.viewerSize = function() {\n const photoviewer = context.container().select('.photoviewer');\n return subtractPadding(utilGetDimensions(photoviewer, true), photoviewer);\n };\n\n return utilRebind(photoviewer, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { uiModal } from './modal';\n\n\nexport function uiRestore(context) {\n return function(selection) {\n if (!context.history().hasRestorableChanges()) return;\n\n let modalSelection = uiModal(selection, true);\n\n modalSelection.select('.modal')\n .attr('class', 'modal fillL');\n\n let introModal = modalSelection.select('.content');\n\n introModal\n .append('div')\n .attr('class', 'modal-section')\n .append('h3')\n .call(t.append('restore.heading'));\n\n introModal\n .append('div')\n .attr('class','modal-section')\n .append('p')\n .call(t.append('restore.description'));\n\n let buttonWrap = introModal\n .append('div')\n .attr('class', 'modal-actions');\n\n let restore = buttonWrap\n .append('button')\n .attr('class', 'restore')\n .on('click', () => {\n context.history().restore();\n modalSelection.remove();\n });\n\n restore\n .append('svg')\n .attr('class', 'logo logo-restore')\n .append('use')\n .attr('xlink:href', '#iD-logo-restore');\n\n restore\n .append('div')\n .call(t.append('restore.restore'));\n\n let reset = buttonWrap\n .append('button')\n .attr('class', 'reset')\n .on('click', () => {\n context.history().clearSaved();\n modalSelection.remove();\n });\n\n reset\n .append('svg')\n .attr('class', 'logo logo-reset')\n .append('use')\n .attr('xlink:href', '#iD-logo-reset');\n\n reset\n .append('div')\n .call(t.append('restore.reset'));\n\n restore.node().focus();\n };\n}\n", "import { displayLength } from '../util/units';\nimport { geoLonToMeters, geoMetersToLon } from '../geo';\nimport { localizer } from '../core/localizer';\n\n\nexport function uiScale(context) {\n var projection = context.projection,\n isImperial = !localizer.usesMetric(),\n maxLength = 180,\n tickHeight = 8;\n\n\n function scaleDefs(loc1, loc2) {\n var lat = (loc2[1] + loc1[1]) / 2,\n conversion = (isImperial ? 3.28084 : 1),\n dist = geoLonToMeters(loc2[0] - loc1[0], lat) * conversion,\n scale = { dist: 0, px: 0, text: '' },\n buckets, i, val, dLon;\n\n if (isImperial) {\n buckets = [5280000, 528000, 52800, 5280, 500, 50, 5, 1];\n } else {\n buckets = [5000000, 500000, 50000, 5000, 500, 50, 5, 1];\n }\n\n // determine a user-friendly endpoint for the scale\n for (i = 0; i < buckets.length; i++) {\n val = buckets[i];\n if (dist >= val) {\n scale.dist = Math.floor(dist / val) * val;\n break;\n } else {\n scale.dist = +dist.toFixed(2);\n }\n }\n\n dLon = geoMetersToLon(scale.dist / conversion, lat);\n scale.px = Math.round(projection([loc1[0] + dLon, loc1[1]])[0]);\n\n scale.text = displayLength(scale.dist / conversion, isImperial);\n\n return scale;\n }\n\n\n function update(selection) {\n // choose loc1, loc2 along bottom of viewport (near where the scale will be drawn)\n var dims = context.map().dimensions(),\n loc1 = projection.invert([0, dims[1]]),\n loc2 = projection.invert([maxLength, dims[1]]),\n scale = scaleDefs(loc1, loc2);\n\n selection.select('.scale-path')\n .attr('d', 'M0.5,0.5v' + tickHeight + 'h' + scale.px + 'v-' + tickHeight);\n\n selection.select('.scale-text')\n .style(localizer.textDirection() === 'ltr' ? 'left' : 'right', (scale.px + 16) + 'px')\n .text(scale.text);\n }\n\n\n return function(selection) {\n function switchUnits() {\n isImperial = !isImperial;\n selection.call(update);\n }\n\n var scalegroup = selection.append('svg')\n .attr('class', 'scale')\n .on('click', switchUnits)\n .append('g')\n .attr('transform', 'translate(10,11)');\n\n scalegroup\n .append('path')\n .attr('class', 'scale-path');\n\n selection\n .append('div')\n .attr('class', 'scale-text');\n\n selection.call(update);\n\n context.map().on('move.scale', function() {\n update(selection);\n });\n };\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { fileFetcher } from '../core/file_fetcher';\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\nimport { uiModal } from './modal';\nimport { utilArrayUniq } from '../util';\nimport { utilDetect } from '../util/detect';\n\n\nexport function uiShortcuts(context) {\n var detected = utilDetect();\n var _activeTab = 0;\n var _modalSelection;\n var _selection = d3_select(null);\n var _dataShortcuts;\n\n\n function shortcutsModal(_modalSelection) {\n _modalSelection.select('.modal')\n .classed('modal-shortcuts', true);\n\n var content = _modalSelection.select('.content');\n\n content\n .append('div')\n .attr('class', 'modal-section header')\n .append('h2')\n .call(t.append('shortcuts.title'));\n\n fileFetcher.get('shortcuts')\n .then(function(data) {\n _dataShortcuts = data;\n content.call(render);\n })\n .catch(function() { /* ignore */ });\n }\n\n\n function render(selection) {\n if (!_dataShortcuts) return;\n\n var wrapper = selection\n .selectAll('.wrapper')\n .data([0]);\n\n var wrapperEnter = wrapper\n .enter()\n .append('div')\n .attr('class', 'wrapper modal-section');\n\n var tabsBar = wrapperEnter\n .append('div')\n .attr('class', 'tabs-bar');\n\n var shortcutsList = wrapperEnter\n .append('div')\n .attr('class', 'shortcuts-list');\n\n wrapper = wrapper.merge(wrapperEnter);\n\n var tabs = tabsBar\n .selectAll('.tab')\n .data(_dataShortcuts);\n\n var tabsEnter = tabs\n .enter()\n .append('a')\n .attr('class', 'tab')\n .attr('href', '#')\n .on('click', function (d3_event, d) {\n d3_event.preventDefault();\n var i = _dataShortcuts.indexOf(d);\n _activeTab = i;\n render(selection);\n });\n\n tabsEnter\n .append('span')\n .each(function (d) {\n d3_select(this).call(t.addOrUpdate(d.text));\n });\n\n // Update\n wrapper.selectAll('.tab')\n .classed('active', function (d, i) {\n return i === _activeTab;\n });\n\n\n var shortcuts = shortcutsList\n .selectAll('.shortcut-tab')\n .data(_dataShortcuts);\n\n var shortcutsEnter = shortcuts\n .enter()\n .append('div')\n .attr('class', function(d) { return 'shortcut-tab shortcut-tab-' + d.tab; });\n\n var columnsEnter = shortcutsEnter\n .selectAll('.shortcut-column')\n .data(function (d) { return d.columns; })\n .enter()\n .append('table')\n .attr('class', 'shortcut-column');\n\n var rowsEnter = columnsEnter\n .selectAll('.shortcut-row')\n .data(function (d) { return d.rows; })\n .enter()\n .append('tr')\n .attr('class', 'shortcut-row');\n\n\n var sectionRows = rowsEnter\n .filter(function (d) { return !d.shortcuts; });\n\n sectionRows\n .append('td');\n\n sectionRows\n .append('td')\n .attr('class', 'shortcut-section')\n .append('h3')\n .each(function (d) {\n d3_select(this).call(t.addOrUpdate(d.text));\n });\n\n\n var shortcutRows = rowsEnter\n .filter(function (d) { return d.shortcuts; });\n\n var shortcutKeys = shortcutRows\n .append('td')\n .attr('class', 'shortcut-keys');\n\n var modifierKeys = shortcutKeys\n .filter(function (d) { return d.modifiers; });\n\n modifierKeys\n .selectAll('kbd.modifier')\n .data(function (d) {\n if (detected.os === 'win' && d.text === 'shortcuts.editing.commands.redo') {\n return ['\u2318'];\n } else if (detected.os !== 'mac' && d.text === 'shortcuts.browsing.display_options.fullscreen') {\n return [];\n } else {\n return d.modifiers;\n }\n })\n .enter()\n .each(function () {\n var selection = d3_select(this);\n\n selection\n .append('kbd')\n .attr('class', 'modifier')\n .text(function (d) { return uiCmd.display(d); });\n\n selection\n .append('span')\n .text('+');\n });\n\n\n shortcutKeys\n .selectAll('kbd.shortcut')\n .data(function (d) {\n var arr = d.shortcuts;\n if (detected.os === 'win' && d.text === 'shortcuts.editing.commands.redo') {\n arr = ['Y'];\n } else if (detected.os !== 'mac' && d.text === 'shortcuts.browsing.display_options.fullscreen') {\n arr = ['F11'];\n }\n\n // replace translations\n arr = arr.map(function(s) {\n return uiCmd.display(s.indexOf('.') !== -1 ? t(s) : s);\n });\n\n return utilArrayUniq(arr).map(function(s) {\n return {\n shortcut: s,\n separator: d.separator,\n suffix: d.suffix\n };\n });\n })\n .enter()\n .each(function (d, i, nodes) {\n var selection = d3_select(this);\n var click = d.shortcut.toLowerCase().match(/(.*).click/);\n\n if (click && click[1]) { // replace \"left_click\", \"right_click\" with mouse icon\n selection\n .call(svgIcon('#iD-walkthrough-mouse-' + click[1], 'operation'));\n } else if (d.shortcut.toLowerCase() === 'long-press') {\n selection\n .call(svgIcon('#iD-walkthrough-longpress', 'longpress operation'));\n } else if (d.shortcut.toLowerCase() === 'tap') {\n selection\n .call(svgIcon('#iD-walkthrough-tap', 'tap operation'));\n } else {\n selection\n .append('kbd')\n .attr('class', 'shortcut')\n .text(function (d) { return d.shortcut; });\n }\n\n if (i < nodes.length - 1) {\n if (d.separator) {\n selection\n .append('span')\n .text(d.separator);\n } else {\n selection.append('span').text('\\u00a0');\n selection.append('span').call(t.append('shortcuts.or'));\n selection.append('span').text('\\u00a0');\n }\n } else if (i === nodes.length - 1 && d.suffix) {\n selection\n .append('span')\n .text(d.suffix);\n }\n });\n\n\n shortcutKeys\n .filter(function(d) { return d.gesture; })\n .each(function () {\n var selection = d3_select(this);\n\n selection\n .append('span')\n .text('+');\n\n selection\n .append('span')\n .attr('class', 'gesture')\n .each(function (d) {\n d3_select(this).call(t.addOrUpdate(d.gesture));\n });\n });\n\n\n shortcutRows\n .append('td')\n .attr('class', 'shortcut-desc')\n .each(function (d) {\n if (d.text) {\n d3_select(this).call(t.addOrUpdate(d.text));\n } else {\n d3_select(this).text('\\u00a0');\n }\n });\n\n\n // Update\n wrapper.selectAll('.shortcut-tab')\n .style('display', function (d, i) {\n return i === _activeTab ? 'flex' : 'none';\n });\n }\n\n\n return function(selection, show) {\n _selection = selection;\n if (show) {\n _modalSelection = uiModal(selection);\n _modalSelection.call(shortcutsModal);\n } else {\n context.keybinding()\n .on([t('shortcuts.toggle.key'), '?'], function () {\n if (context.container().selectAll('.modal-shortcuts').size()) { // already showing\n if (_modalSelection) {\n _modalSelection.close();\n _modalSelection = null;\n }\n } else {\n _modalSelection = uiModal(_selection);\n _modalSelection.call(shortcutsModal);\n }\n });\n }\n };\n}\n", "module.exports = element;\nmodule.exports.pair = pair;\nmodule.exports.format = format;\nmodule.exports.formatPair = formatPair;\nmodule.exports.coordToDMS = coordToDMS;\n\n\nfunction element(input, dims) {\n var result = search(input, dims);\n return (result === null) ? null : result.val;\n}\n\n\nfunction formatPair(input) {\n return format(input.lat, 'lat') + ' ' + format(input.lon, 'lon');\n}\n\n\n// Is 0 North or South?\nfunction format(input, dim) {\n var dms = coordToDMS(input, dim);\n return dms.whole + '\u00B0 ' +\n (dms.minutes ? dms.minutes + '\\' ' : '') +\n (dms.seconds ? dms.seconds + '\" ' : '') + dms.dir;\n}\n\n\nfunction coordToDMS(input, dim) {\n var dirs = { lat: ['N', 'S'], lon: ['E', 'W'] }[dim] || '';\n var dir = dirs[input >= 0 ? 0 : 1];\n var abs = Math.abs(input);\n var whole = Math.floor(abs);\n var fraction = abs - whole;\n var fractionMinutes = fraction * 60;\n var minutes = Math.floor(fractionMinutes);\n var seconds = Math.floor((fractionMinutes - minutes) * 60);\n\n return {\n whole: whole,\n minutes: minutes,\n seconds: seconds,\n dir: dir\n };\n}\n\n\nfunction search(input, dims) {\n if (!dims) dims = 'NSEW';\n if (typeof input !== 'string') return null;\n\n input = input.toUpperCase();\n var regex = /^[\\s\\,]*([NSEW])?\\s*([\\-|\\\u2014|\\\u2015]?[0-9.]+)[\u00B0\u00BA\u02DA]?\\s*(?:([0-9.]+)['\u2019\u2032\u2018]\\s*)?(?:([0-9.]+)(?:''|\"|\u201D|\u2033)\\s*)?([NSEW])?/;\n\n var m = input.match(regex);\n if (!m) return null; // no match\n\n var matched = m[0];\n\n // extract dimension.. m[1] = leading, m[5] = trailing\n var dim;\n if (m[1] && m[5]) { // if matched both..\n dim = m[1]; // keep leading\n matched = matched.slice(0, -1); // remove trailing dimension from match\n } else {\n dim = m[1] || m[5];\n }\n\n // if unrecognized dimension\n if (dim && dims.indexOf(dim) === -1) return null;\n\n // extract DMS\n var deg = m[2] ? parseFloat(m[2]) : 0;\n var min = m[3] ? parseFloat(m[3]) / 60 : 0;\n var sec = m[4] ? parseFloat(m[4]) / 3600 : 0;\n var sign = (deg < 0) ? -1 : 1;\n if (dim === 'S' || dim === 'W') sign *= -1;\n\n return {\n val: (Math.abs(deg) + min + sec) * sign,\n dim: dim,\n matched: matched,\n remain: input.slice(matched.length)\n };\n}\n\n\nfunction pair(input, dims) {\n input = input.trim();\n var one = search(input, dims);\n if (!one) return null;\n\n input = one.remain.trim();\n var two = search(input, dims);\n if (!two || two.remain) return null;\n\n if (one.dim) {\n return swapdim(one.val, two.val, one.dim);\n } else {\n return [one.val, two.val];\n }\n}\n\n\nfunction swapdim(a, b, dim) {\n if (dim === 'N' || dim === 'S') return [a, b];\n if (dim === 'W' || dim === 'E') return [b, a];\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\nimport * as sexagesimal from '@mapbox/sexagesimal';\n\nimport { presetManager } from '../presets';\nimport { t } from '../core/localizer';\nimport { dmsCoordinatePair, dmsMatcher } from '../util/units';\nimport { coreGraph } from '../core/graph';\nimport { geoSphericalDistance } from '../geo/geo';\nimport { geoExtent } from '../geo';\nimport { modeSelect } from '../modes/select';\nimport { osmEntity } from '../osm/entity';\nimport { getRelationColor } from '../osm/tags';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\n\nimport {\n utilDisplayName,\n utilDisplayType,\n utilHighlightEntities,\n utilNoAuto\n} from '../util';\n\n\nexport const idMatch = q => {\n const idMatchRegex = /(?:^|\\W)(node|way|relation|note|[nwr])\\W{0,2}0*([1-9]\\d*)(?:\\W|$)/i;\n const idMatch = q.match(idMatchRegex);\n if (!idMatch) return false;\n\n return {\n type: idMatch[1] === 'note' ? idMatch[1] : idMatch[1].charAt(0),\n id: idMatch[2]\n };\n};\n\nexport function uiFeatureList(context) {\n var _geocodeResults;\n\n\n function featureList(selection) {\n var header = selection\n .append('div')\n .attr('class', 'header fillL');\n\n header\n .append('h2')\n .call(t.append('inspector.feature_list'));\n\n var searchWrap = selection\n .append('div')\n .attr('class', 'search-header');\n\n searchWrap\n .call(svgIcon('#iD-icon-search', 'pre-text'));\n\n var search = searchWrap\n .append('input')\n .attr('placeholder', t('inspector.search'))\n .attr('type', 'search')\n .call(utilNoAuto)\n .on('keypress', keypress)\n .on('keydown', keydown)\n .on('input', inputevent);\n\n var listWrap = selection\n .append('div')\n .attr('class', 'inspector-body');\n\n var list = listWrap\n .append('div')\n .attr('class', 'feature-list');\n\n context\n .on('exit.feature-list', clearSearch);\n context.map()\n .on('drawn.feature-list', mapDrawn);\n\n context.keybinding()\n .on(uiCmd('\u2318F'), focusSearch);\n\n\n function focusSearch(d3_event) {\n var mode = context.mode() && context.mode().id;\n if (mode !== 'browse') return;\n\n d3_event.preventDefault();\n search.node().focus();\n }\n\n\n function keydown(d3_event) {\n if (d3_event.keyCode === 27) { // escape\n search.node().blur();\n }\n }\n\n\n function keypress(d3_event) {\n var q = search.property('value'),\n items = list.selectAll('.feature-list-item');\n if (d3_event.keyCode === 13 && // \u21A9 Return\n q.length &&\n items.size()) {\n click(d3_event, items.datum());\n }\n }\n\n\n function inputevent() {\n _geocodeResults = undefined;\n drawList();\n }\n\n\n function clearSearch() {\n search.property('value', '');\n drawList();\n }\n\n\n function mapDrawn(e) {\n if (e.full) {\n drawList();\n }\n }\n\n\n function features() {\n var graph = context.graph();\n var visibleCenter = context.map().extent().center();\n var q = search.property('value').toLowerCase().trim();\n\n if (!q) return [];\n\n const locationMatch = sexagesimal.pair(q.toUpperCase()) || dmsMatcher(q);\n\n const coordResult = [];\n if (locationMatch) {\n const latLon = [Number(locationMatch[0]), Number(locationMatch[1])];\n const lonLat = [latLon[1], latLon[0]]; // also try swapped order\n\n const isLatLonValid = latLon[0] >= -90 && latLon[0] <= 90 && latLon[1] >= -180 && latLon[1] <= 180;\n let isLonLatValid = lonLat[0] >= -90 && lonLat[0] <= 90 && lonLat[1] >= -180 && lonLat[1] <= 180;\n isLonLatValid &&= !q.match(/[NSEW]/i); // don't flip coords with explicit cardinal directions\n isLonLatValid &&= !locationMatch[2]; // don't flip zoom/x/y coords\n isLonLatValid &&= lonLat[0] !== lonLat[1]; // don't flip when lat=lon\n\n if (isLatLonValid) {\n coordResult.push({\n id: latLon[0] + '/' + latLon[1],\n geometry: 'point',\n type: t('inspector.location'),\n name: dmsCoordinatePair([latLon[1], latLon[0]]),\n location: latLon,\n zoom: locationMatch[2]\n });\n }\n if (isLonLatValid) {\n coordResult.push({\n id: lonLat[0] + '/' + lonLat[1],\n geometry: 'point',\n type: t('inspector.location'),\n name: dmsCoordinatePair([lonLat[1], lonLat[0]]),\n location: lonLat\n });\n }\n }\n\n // A location search takes priority over an ID search\n const idMatchResult = !locationMatch && idMatch(q);\n const idResult = [];\n if (idMatchResult) {\n const elemType = idMatchResult.type;\n const elemId = idMatchResult.id;\n idResult.push({\n id: elemType + elemId,\n geometry: elemType === 'n' ? 'point' : elemType === 'w' ? 'line' : elemType === 'note' ? 'note' : 'relation',\n type: elemType === 'n' ? t('inspector.node') : elemType === 'w' ? t('inspector.way') : elemType === 'note' ? t('note.note') : t('inspector.relation'),\n name: elemId\n });\n }\n\n var allEntities = graph.entities;\n const localResults = [];\n for (var id in allEntities) {\n var entity = allEntities[id];\n if (!entity) continue;\n\n var matched = presetManager.match(entity, graph);\n var name = utilDisplayName(entity, { hideNetwork: matched.suggestion }) || '';\n if (name.toLowerCase().indexOf(q) < 0) continue;\n var type = (matched && matched.name()) || utilDisplayType(entity.id);\n var extent = entity.extent(graph);\n var distance = extent ? geoSphericalDistance(visibleCenter, extent.center()) : 0;\n\n localResults.push({\n id: entity.id,\n entity: entity,\n geometry: entity.geometry(graph),\n type: type,\n name: name,\n distance: distance\n });\n\n if (localResults.length > 100) break;\n }\n localResults.sort((a, b) => a.distance - b.distance);\n\n const geocodeResults = [];\n (_geocodeResults || []).forEach(function(d) {\n if (d.osm_type && d.osm_id) { // some results may be missing these - #1890\n\n // Make a temporary osmEntity so we can preset match\n // and better localize the search result - #4725\n var id = osmEntity.id.fromOSM(d.osm_type, d.osm_id);\n var tags = {};\n tags[d.class] = d.type;\n\n var attrs = { id: id, type: d.osm_type, tags: tags };\n if (d.osm_type === 'way') { // for ways, add some fake closed nodes\n attrs.nodes = ['a','a']; // so that geometry area is possible\n }\n\n var tempEntity = osmEntity(attrs);\n var tempGraph = coreGraph([tempEntity]);\n var matched = presetManager.match(tempEntity, tempGraph);\n var type = (matched && matched.name()) || utilDisplayType(id);\n\n geocodeResults.push({\n id: tempEntity.id,\n geometry: tempEntity.geometry(tempGraph),\n type: type,\n name: d.display_name,\n extent: new geoExtent(\n [Number(d.boundingbox[3]), Number(d.boundingbox[0])],\n [Number(d.boundingbox[2]), Number(d.boundingbox[1])])\n });\n }\n });\n\n const extraResults = [];\n if (q.match(/^[0-9]+$/)) {\n // if query is just a number, possibly an OSM ID without a prefix\n extraResults.push({\n id: 'n' + q,\n geometry: 'point',\n type: t('inspector.node'),\n name: q\n });\n extraResults.push({\n id: 'w' + q,\n geometry: 'line',\n type: t('inspector.way'),\n name: q\n });\n extraResults.push({\n id: 'r' + q,\n geometry: 'relation',\n type: t('inspector.relation'),\n name: q\n });\n extraResults.push({\n id: 'note' + q,\n geometry: 'note',\n type: t('note.note'),\n name: q\n });\n }\n\n return [...idResult, ...localResults, ...coordResult, ...geocodeResults, ...extraResults];\n }\n\n\n function drawList() {\n var value = search.property('value');\n var results = features();\n\n list.classed('filtered', value.length);\n\n var resultsIndicator = list.selectAll('.no-results-item')\n .data([0])\n .enter()\n .append('button')\n .property('disabled', true)\n .attr('class', 'no-results-item')\n .call(svgIcon('#iD-icon-alert', 'pre-text'));\n\n resultsIndicator.append('span')\n .attr('class', 'entity-name');\n\n list.selectAll('.no-results-item .entity-name')\n .html('')\n .call(t.append('geocoder.no_results_worldwide'));\n\n if (services.geocoder) {\n list.selectAll('.geocode-item')\n .data([0])\n .enter()\n .append('button')\n .attr('class', 'geocode-item secondary-action')\n .on('click', geocoderSearch)\n .append('div')\n .attr('class', 'label')\n .append('span')\n .attr('class', 'entity-name')\n .call(t.append('geocoder.search'));\n }\n\n list.selectAll('.no-results-item')\n .style('display', (value.length && !results.length) ? 'block' : 'none');\n\n list.selectAll('.geocode-item')\n .style('display', (value && _geocodeResults === undefined) ? 'block' : 'none');\n\n var items = list.selectAll('.feature-list-item')\n .data(results, function(d) { return d.id; });\n\n var enter = items.enter()\n .insert('button', '.geocode-item')\n .attr('class', 'feature-list-item')\n .on('pointerenter', mouseover)\n .on('pointerleave', mouseout)\n .on('focus', mouseover)\n .on('blur', mouseout)\n .on('click', click);\n\n var label = enter\n .append('div')\n .attr('class', 'label')\n .attr('title', d => d.name);\n\n label\n .each(function(d) {\n d3_select(this)\n .call(svgIcon('#iD-icon-' + d.geometry, 'pre-text'));\n });\n\n label\n .append('span')\n .attr('class', 'entity-type')\n .text(function(d) { return d.type; });\n\n label.each(function(d) {\n if (d.entity?.type !== 'relation') return;\n\n const hasRef = d.entity.tags.ref;\n const relColors = getRelationColor(d.entity.tags, '#555');\n if (relColors.isValid || hasRef) {\n const refs = (d.entity.tags.ref || '').split(';');\n for (const ref of refs) {\n d3_select(this)\n .append('span')\n .classed('member-entity-ref-color', true)\n .style('border-color', relColors.color)\n .style('background-color', relColors.color)\n .style('color', relColors.textColor)\n .text(ref);\n }\n }\n });\n\n label\n .append('span')\n .attr('class', 'entity-name')\n .text(d => d.name);\n\n enter\n .style('opacity', 0)\n .transition()\n .style('opacity', 1);\n\n items.exit()\n .each(d => mouseout(undefined, d))\n .remove();\n\n items.merge(enter)\n .order();\n }\n\n\n function mouseover(d3_event, d) {\n if (d.location !== undefined) return;\n\n utilHighlightEntities([d.id], true, context);\n }\n\n\n function mouseout(d3_event, d) {\n if (d.location !== undefined) return;\n\n utilHighlightEntities([d.id], false, context);\n }\n\n\n function click(d3_event, d) {\n d3_event.preventDefault();\n\n if (d.location) {\n context.map().centerZoomEase([d.location[1], d.location[0]], d.zoom || 19);\n\n } else if (d.entity) {\n utilHighlightEntities([d.id], false, context);\n\n context.enter(modeSelect(context, [d.entity.id]));\n context.map().zoomToEase(d.entity);\n\n } else if (d.geometry === 'note') {\n // note\n // get number part 'note12345'\n const noteId = d.id.replace(/\\D/g, '');\n\n // load note\n context.moveToNote(noteId);\n } else {\n // download, zoom to, and select the entity with the given ID\n context.zoomToEntity(d.id);\n }\n }\n\n\n function geocoderSearch() {\n services.geocoder.search(search.property('value'), function (err, resp) {\n _geocodeResults = resp || [];\n drawList();\n });\n }\n }\n\n\n return featureList;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { svgIcon } from '../../svg/icon';\nimport { utilArrayIdentical } from '../../util/array';\nimport { t } from '../../core/localizer';\nimport { utilHighlightEntities } from '../../util';\nimport { uiSection } from '../section';\nimport { validationIssue } from '../../core/validation';\n\n\nexport function uiSectionEntityIssues(context) {\n // Does the user prefer to expand the active issue? Useful for viewing tag diff.\n // Expand by default so first timers see it - #6408, #8143\n var preference = prefs('entity-issues.reference.expanded');\n var _expanded = preference === null ? true : (preference === 'true');\n\n var _entityIDs = [];\n var _issues = [];\n var _activeIssueID;\n\n\n var section = uiSection('entity-issues', context)\n .shouldDisplay(function() {\n return _issues.length > 0;\n })\n .label(function() {\n return t.append('inspector.title_count', { title: t.append('issues.list_title'), count: _issues.length });\n })\n .disclosureContent(renderDisclosureContent);\n\n context.validator()\n .on('validated.entity_issues', function() {\n // Refresh on validated events\n reloadIssues();\n section.reRender();\n })\n .on('focusedIssue.entity_issues', function(issue) {\n makeActiveIssue(issue.id);\n });\n\n function reloadIssues() {\n _issues = context.validator().getSharedEntityIssues(_entityIDs, { includeDisabledRules: true });\n }\n\n function makeActiveIssue(issueID) {\n _activeIssueID = issueID;\n section.selection().selectAll('.issue-container')\n .classed('active', function(d) { return d.id === _activeIssueID; });\n }\n\n function renderDisclosureContent(selection) {\n\n selection.classed('grouped-items-area', true);\n\n _activeIssueID = _issues.length > 0 ? _issues[0].id : null;\n\n var containers = selection.selectAll('.issue-container')\n .data(_issues, function(d) { return d.key; });\n\n // Exit\n containers.exit()\n .remove();\n\n // Enter\n var containersEnter = containers.enter()\n .append('div')\n .attr('class', 'issue-container');\n\n\n var itemsEnter = containersEnter\n .append('div')\n .attr('class', function(d) { return 'issue severity-' + d.severity; })\n .on('mouseover.highlight', function(d3_event, d) {\n // don't hover-highlight the selected entity\n var ids = d.entityIds\n .filter(function(e) { return _entityIDs.indexOf(e) === -1; });\n\n utilHighlightEntities(ids, true, context);\n })\n .on('mouseout.highlight', function(d3_event, d) {\n var ids = d.entityIds\n .filter(function(e) { return _entityIDs.indexOf(e) === -1; });\n\n utilHighlightEntities(ids, false, context);\n });\n\n var labelsEnter = itemsEnter\n .append('div')\n .attr('class', 'issue-label');\n\n var textEnter = labelsEnter\n .append('button')\n .attr('class', 'issue-text')\n .on('click', function(d3_event, d) {\n makeActiveIssue(d.id); // expand only the clicked item\n\n const extent = d.extent(context.graph());\n if (extent) {\n context.map().zoomToEase(extent);\n }\n });\n\n textEnter\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(validationIssue.ICONS[d.severity], 'issue-icon'));\n });\n\n textEnter\n .append('span')\n .attr('class', 'issue-message');\n\n\n var infoButton = labelsEnter\n .append('button')\n .attr('class', 'issue-info-button')\n .attr('title', t('icons.information'))\n .call(svgIcon('#iD-icon-inspect'));\n\n infoButton\n .on('click', function (d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n this.blur(); // avoid keeping focus on the button - #4641\n\n var container = d3_select(this.parentNode.parentNode.parentNode);\n var info = container.selectAll('.issue-info');\n var isExpanded = info.classed('expanded');\n _expanded = !isExpanded;\n prefs('entity-issues.reference.expanded', _expanded); // update preference\n\n if (isExpanded) {\n info\n .transition()\n .duration(200)\n .style('max-height', '0px')\n .style('opacity', '0')\n .on('end', function () {\n info.classed('expanded', false);\n });\n } else {\n info\n .classed('expanded', true)\n .transition()\n .duration(200)\n .style('max-height', '200px')\n .style('opacity', '1')\n .on('end', function () {\n info.style('max-height', null);\n });\n }\n });\n\n itemsEnter\n .append('ul')\n .attr('class', 'issue-fix-list');\n\n containersEnter\n .append('div')\n .attr('class', 'issue-info' + (_expanded ? ' expanded' : ''))\n .style('max-height', (_expanded ? null : '0'))\n .style('opacity', (_expanded ? '1' : '0'))\n .each(function(d) {\n if (typeof d.reference === 'function') {\n d3_select(this)\n .call(d.reference);\n } else {\n d3_select(this)\n .call(t.append('inspector.no_documentation_key'));\n }\n });\n\n\n // Update\n containers = containers\n .merge(containersEnter)\n .classed('active', function(d) { return d.id === _activeIssueID; });\n\n containers.selectAll('.issue-message')\n .text('')\n .each(function(d) {\n return d.message(context)(d3_select(this));\n });\n\n // fixes\n var fixLists = containers.selectAll('.issue-fix-list');\n\n var fixes = fixLists.selectAll('.issue-fix-item')\n .data(function(d) { return d.fixes ? d.fixes(context) : []; }, function(fix) { return fix.id; });\n\n fixes.exit()\n .remove();\n\n var fixesEnter = fixes.enter()\n .append('li')\n .attr('class', 'issue-fix-item');\n\n var buttons = fixesEnter\n .append('button')\n .on('click', function(d3_event, d) {\n // not all fixes are actionable\n if (d3_select(this).attr('disabled') || !d.onClick) return;\n\n // Don't run another fix for this issue within a second of running one\n // (Necessary for \"Select a feature type\" fix. Most fixes should only ever run once)\n if (d.issue.dateLastRanFix && new Date() - d.issue.dateLastRanFix < 1000) return;\n d.issue.dateLastRanFix = new Date();\n\n // remove hover-highlighting\n utilHighlightEntities(d.issue.entityIds.concat(d.entityIds), false, context);\n\n new Promise(function(resolve, reject) {\n d.onClick(context, resolve, reject);\n if (d.onClick.length <= 1) {\n // if the fix doesn't take any completion parameters then consider it resolved\n resolve();\n }\n })\n .then(function() {\n // revalidate whenever the fix has finished running successfully\n context.validator().validate();\n });\n })\n .on('mouseover.highlight', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, true, context);\n })\n .on('mouseout.highlight', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, false, context);\n });\n\n buttons\n .each(function(d) {\n var iconName = d.icon || 'iD-icon-wrench';\n if (iconName.startsWith('maki')) {\n iconName += '-15';\n }\n d3_select(this).call(svgIcon('#' + iconName, 'fix-icon'));\n });\n\n buttons\n .append('span')\n .attr('class', 'fix-message')\n .each(function(d) { return d.title(d3_select(this)); });\n\n fixesEnter.merge(fixes)\n .selectAll('button')\n .classed('actionable', function(d) {\n return d.onClick;\n })\n .attr('disabled', function(d) {\n return d.onClick ? null : 'true';\n })\n .attr('title', function(d) {\n if (d.disabledReason) {\n return d.disabledReason;\n }\n return null;\n });\n }\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n if (!_entityIDs || !val || !utilArrayIdentical(_entityIDs, val)) {\n _entityIDs = val;\n _activeIssueID = null;\n reloadIssues();\n }\n return section;\n };\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { prefs } from '../core/preferences';\nimport { svgIcon, svgTagClasses } from '../svg';\nimport { utilFunctor } from '../util';\n\n\nexport function uiPresetIcon() {\n let _preset;\n let _geometry;\n\n\n function presetIcon(selection) {\n selection.each(render);\n }\n\n\n function getIcon(p, geom) {\n if (p.isFallback && p.isFallback()) return geom === 'vertex' ? '' : 'iD-icon-' + p.id;\n if (p.icon) return p.icon;\n if (geom === 'line') return 'iD-other-line';\n if (geom === 'vertex') return 'temaki-vertex';\n return 'maki-marker-stroked';\n }\n\n\n function renderPointBorder(container, drawPoint) {\n const pointBorder = container.selectAll('.preset-icon-point-border')\n .data(drawPoint ? [0] : []);\n\n pointBorder.exit()\n .remove();\n\n const pointBorderEnter = pointBorder.enter();\n\n const w = 40;\n const h = 40;\n\n pointBorderEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-point-border')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`)\n .append('path')\n .attr('transform', 'translate(11.5, 8)')\n .attr('d', 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z');\n }\n\n\n function renderCategoryBorder(container, category) {\n const categoryBorder = container.selectAll('.preset-icon-category-border')\n .data(category ? [0] : []);\n\n categoryBorder.exit()\n .remove();\n\n const categoryBorderEnter = categoryBorder.enter();\n\n const d = 60;\n\n let svgEnter = categoryBorderEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-category-border')\n .attr('width', d)\n .attr('height', d)\n .attr('viewBox', `0 0 ${d} ${d}`);\n\n svgEnter\n .append('path')\n .attr('class', 'area')\n .attr('d', 'M9.5,7.5 L25.5,7.5 L28.5,12.5 L49.5,12.5 C51.709139,12.5 53.5,14.290861 53.5,16.5 L53.5,43.5 C53.5,45.709139 51.709139,47.5 49.5,47.5 L10.5,47.5 C8.290861,47.5 6.5,45.709139 6.5,43.5 L6.5,12.5 L9.5,7.5 Z');\n\n if (category) {\n categoryBorderEnter.merge(categoryBorder)\n .selectAll('path')\n .attr('class', `area ${category.id}`);\n }\n }\n\n\n function renderCircleFill(container, drawVertex) {\n const vertexFill = container.selectAll('.preset-icon-fill-vertex')\n .data(drawVertex ? [0] : []);\n\n vertexFill.exit()\n .remove();\n\n const vertexFillEnter = vertexFill.enter();\n\n const w = 60;\n const h = 60;\n const d = 40;\n\n vertexFillEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-fill-vertex')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`)\n .append('circle')\n .attr('cx', w / 2)\n .attr('cy', h / 2)\n .attr('r', d / 2);\n }\n\n\n function renderSquareFill(container, drawArea, tagClasses) {\n\n let fill = container.selectAll('.preset-icon-fill-area')\n .data(drawArea ? [0] : []);\n\n fill.exit()\n .remove();\n\n let fillEnter = fill.enter();\n\n const d = 60;\n const w = d;\n const h = d;\n const l = d * 2/3;\n const c1 = (w-l) / 2;\n const c2 = c1 + l;\n\n fillEnter = fillEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-fill-area')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`);\n\n ['fill', 'stroke'].forEach(klass => {\n fillEnter\n .append('path')\n .attr('d', `M${c1} ${c1} L${c1} ${c2} L${c2} ${c2} L${c2} ${c1} Z`)\n .attr('class', `area ${klass}`);\n });\n\n const rVertex = 2.5;\n [[c1, c1], [c1, c2], [c2, c2], [c2, c1]].forEach(point => {\n fillEnter\n .append('circle')\n .attr('class', 'vertex')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', rVertex);\n });\n\n const rMidpoint = 1.25;\n [[c1, w/2], [c2, w/2], [h/2, c1], [h/2, c2]].forEach(point => {\n fillEnter\n .append('circle')\n .attr('class', 'midpoint')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', rMidpoint);\n });\n\n fill = fillEnter.merge(fill);\n\n fill.selectAll('path.stroke')\n .attr('class', `area stroke ${tagClasses}`);\n fill.selectAll('path.fill')\n .attr('class', `area fill ${tagClasses}`);\n }\n\n\n function renderLine(container, drawLine, tagClasses) {\n\n let line = container.selectAll('.preset-icon-line')\n .data(drawLine ? [0] : []);\n\n line.exit()\n .remove();\n\n let lineEnter = line.enter();\n\n const d = 60;\n // draw the line parametrically\n const w = d;\n const h = d;\n const y = Math.round(d * 0.72);\n const l = Math.round(d * 0.6);\n const r = 2.5;\n const x1 = (w - l) / 2;\n const x2 = x1 + l;\n\n lineEnter = lineEnter\n .append('svg')\n .attr('class', 'preset-icon-line')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`);\n\n ['casing', 'stroke'].forEach(klass => {\n lineEnter\n .append('path')\n .attr('d', `M${x1} ${y} L${x2} ${y}`)\n .attr('class', `line ${klass}`);\n });\n\n [[x1-1, y], [x2+1, y]].forEach(point => {\n lineEnter\n .append('circle')\n .attr('class', 'vertex')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', r);\n });\n\n line = lineEnter.merge(line);\n\n line.selectAll('path.stroke')\n .attr('class', `line stroke ${tagClasses}`);\n line.selectAll('path.casing')\n .attr('class', `line casing ${tagClasses}`);\n }\n\n\n function renderRoute(container, drawRoute, p) {\n\n let route = container.selectAll('.preset-icon-route')\n .data(drawRoute ? [0] : []);\n\n route.exit()\n .remove();\n\n let routeEnter = route.enter();\n\n const d = 60;\n // draw the route parametrically\n const w = d;\n const h = d;\n const y1 = Math.round(d * 0.80);\n const y2 = Math.round(d * 0.68);\n const l = Math.round(d * 0.6);\n const r = 2;\n const x1 = (w - l) / 2;\n const x2 = x1 + l / 3;\n const x3 = x2 + l / 3;\n const x4 = x3 + l / 3;\n\n routeEnter = routeEnter\n .append('svg')\n .attr('class', 'preset-icon-route')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`);\n\n ['casing', 'stroke'].forEach(klass => {\n routeEnter\n .append('path')\n .attr('d', `M${x1} ${y1} L${x2} ${y2}`)\n .attr('class', `segment0 line ${klass}`);\n routeEnter\n .append('path')\n .attr('d', `M${x2} ${y2} L${x3} ${y1}`)\n .attr('class', `segment1 line ${klass}`);\n routeEnter\n .append('path')\n .attr('d', `M${x3} ${y1} L${x4} ${y2}`)\n .attr('class', `segment2 line ${klass}`);\n });\n\n [[x1, y1], [x2, y2], [x3, y1], [x4, y2]].forEach(point => {\n routeEnter\n .append('circle')\n .attr('class', 'vertex')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', r);\n });\n\n route = routeEnter.merge(route);\n\n if (drawRoute) {\n let routeType = p.tags.type === 'waterway' ? 'waterway' : p.tags.route;\n const segmentPresetIDs = routeSegments[routeType];\n for (let i in segmentPresetIDs) {\n const segmentPreset = presetManager.item(segmentPresetIDs[i]);\n const segmentTagClasses = svgTagClasses().getClassesString(segmentPreset.tags, '');\n route.selectAll(`path.stroke.segment${i}`)\n .attr('class', `segment${i} line stroke ${segmentTagClasses}`);\n route.selectAll(`path.casing.segment${i}`)\n .attr('class', `segment${i} line casing ${segmentTagClasses}`);\n }\n }\n }\n\n function renderSvgIcon(container, picon, geom, isFramed, category, tagClasses) {\n const isMaki = picon && /^maki-/.test(picon);\n const isTemaki = picon && /^temaki-/.test(picon);\n const isFa = picon && /^fa[srb]-/.test(picon);\n const isR\u00F6ntgen = picon && /^roentgen-/.test(picon);\n const isiDIcon = picon && !(isMaki || isTemaki || isFa || isR\u00F6ntgen);\n\n let icon = container.selectAll('.preset-icon')\n .data(picon ? [0] : []);\n\n icon.exit()\n .remove();\n\n icon = icon.enter()\n .append('div')\n .attr('class', 'preset-icon')\n .call(svgIcon(''))\n .merge(icon);\n\n icon\n .attr('class', 'preset-icon ' + (geom ? geom + '-geom' : ''))\n .classed('category', category)\n .classed('framed', isFramed)\n .classed('preset-icon-iD', isiDIcon);\n\n icon.selectAll('svg')\n .attr('class', 'icon ' + picon + ' ' + (!isiDIcon && geom !== 'line' ? '' : tagClasses));\n\n icon.selectAll('use')\n .attr('href', '#' + picon);\n }\n\n\n function renderImageIcon(container, imageURL) {\n let imageIcon = container.selectAll('img.image-icon')\n .data(imageURL ? [0] : []);\n\n imageIcon.exit()\n .remove();\n\n imageIcon = imageIcon.enter()\n .append('img')\n .attr('class', 'image-icon')\n .on('load', () => container.classed('showing-img', true) )\n .on('error', () => container.classed('showing-img', false) )\n .merge(imageIcon);\n\n imageIcon\n .attr('src', imageURL);\n }\n\n // Route icons are drawn with a zigzag annotation underneath:\n // o o\n // / \\ /\n // o o\n // This dataset defines the styles that are used to draw the zigzag segments.\n const routeSegments = {\n bicycle: ['highway/cycleway', 'highway/cycleway', 'highway/cycleway'],\n bus: ['highway/unclassified', 'highway/secondary', 'highway/primary'],\n climbing: ['climbing/route', 'climbing/route', 'climbing/route'],\n trolleybus: ['highway/unclassified', 'highway/secondary', 'highway/primary'],\n detour: ['highway/tertiary', 'highway/residential', 'highway/unclassified'],\n ferry: ['route/ferry', 'route/ferry', 'route/ferry'],\n foot: ['highway/footway', 'highway/footway', 'highway/footway'],\n hiking: ['highway/path', 'highway/path', 'highway/path'],\n horse: ['highway/bridleway', 'highway/bridleway', 'highway/bridleway'],\n light_rail: ['railway/light_rail', 'railway/light_rail', 'railway/light_rail'],\n monorail: ['railway/monorail', 'railway/monorail', 'railway/monorail'],\n mtb: ['highway/path', 'highway/track', 'highway/bridleway'],\n pipeline: ['man_made/pipeline', 'man_made/pipeline', 'man_made/pipeline'],\n piste: ['piste/downhill', 'piste/hike', 'piste/nordic'],\n power: ['power/line', 'power/line', 'power/line'],\n road: ['highway/secondary', 'highway/primary', 'highway/trunk'],\n subway: ['railway/subway', 'railway/subway', 'railway/subway'],\n train: ['railway/rail', 'railway/rail', 'railway/rail'],\n tram: ['railway/tram', 'railway/tram', 'railway/tram'],\n railway: ['railway/rail', 'railway/rail', 'railway/rail'],\n waterway: ['waterway/stream', 'waterway/stream', 'waterway/stream']\n };\n\n\n function render() {\n let p = _preset.apply(this, arguments);\n let geom = _geometry ? _geometry.apply(this, arguments) : null;\n if (geom === 'relation' &&\n p.tags &&\n ((p.tags.type === 'route' && p.tags.route && routeSegments[p.tags.route]) || p.tags.type === 'waterway')) {\n geom = 'route';\n }\n\n const showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n const isFallback = p.isFallback && p.isFallback();\n const imageURL = (showThirdPartyIcons === 'true') && p.imageURL;\n const picon = getIcon(p, geom);\n const isCategory = !p.setTags;\n const drawPoint = false;\n const drawVertex = picon !== null && geom === 'vertex';\n const drawLine = picon && geom === 'line' && !isFallback && !isCategory;\n const drawArea = picon && geom === 'area' && !isFallback && !isCategory;\n const drawRoute = picon && geom === 'route';\n const isFramed = drawVertex || drawArea || drawLine || drawRoute || isCategory;\n\n let tags = !isCategory ? p.setTags({}, geom) : {};\n for (let k in tags) {\n if (tags[k] === '*') {\n tags[k] = 'yes';\n }\n }\n\n let tagClasses = svgTagClasses().getClassesString(tags, '');\n let selection = d3_select(this);\n\n let container = selection.selectAll('.preset-icon-container')\n .data([0]);\n\n container = container.enter()\n .append('div')\n .attr('class', 'preset-icon-container')\n .merge(container);\n\n container\n .classed('showing-img', !!imageURL)\n .classed('fallback', isFallback);\n\n renderCategoryBorder(container, isCategory && p);\n renderPointBorder(container, drawPoint);\n renderCircleFill(container, drawVertex);\n renderSquareFill(container, drawArea, tagClasses);\n renderLine(container, drawLine, tagClasses);\n renderRoute(container, drawRoute, p);\n renderSvgIcon(container, picon, geom, isFramed, isCategory, tagClasses);\n renderImageIcon(container, imageURL);\n }\n\n\n presetIcon.preset = function(val) {\n if (!arguments.length) return _preset;\n _preset = utilFunctor(val);\n return presetIcon;\n };\n\n\n presetIcon.geometry = function(val) {\n if (!arguments.length) return _geometry;\n _geometry = utilFunctor(val);\n return presetIcon;\n };\n\n return presetIcon;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { utilArrayIdentical } from '../../util/array';\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { utilRebind } from '../../util';\nimport { uiPresetIcon } from '../preset_icon';\nimport { uiSection } from '../section';\nimport { uiTagReference } from '../tag_reference';\n\n\nexport function uiSectionFeatureType(context) {\n\n var dispatch = d3_dispatch('choose');\n\n var _entityIDs = [];\n var _presets = [];\n\n var _tagReference;\n\n var section = uiSection('feature-type', context)\n .label(() => t.append('inspector.feature_type'))\n .disclosureContent(renderDisclosureContent);\n\n function renderDisclosureContent(selection) {\n\n selection.classed('preset-list-item', true);\n selection.classed('mixed-types', _presets.length > 1);\n\n var presetButtonWrap = selection\n .selectAll('.preset-list-button-wrap')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'preset-list-button-wrap');\n\n var presetButton = presetButtonWrap\n .append('button')\n .attr('class', 'preset-list-button preset-reset')\n .call(uiTooltip()\n .title(() => t.append('inspector.back_tooltip'))\n .placement('bottom')\n );\n\n presetButton.append('div')\n .attr('class', 'preset-icon-container');\n\n presetButton\n .append('div')\n .attr('class', 'label')\n .append('div')\n .attr('class', 'label-inner');\n\n presetButtonWrap.append('div')\n .attr('class', 'accessory-buttons');\n\n var tagReferenceBodyWrap = selection\n .selectAll('.tag-reference-body-wrap')\n .data([0]);\n\n tagReferenceBodyWrap = tagReferenceBodyWrap\n .enter()\n .append('div')\n .attr('class', 'tag-reference-body-wrap')\n .merge(tagReferenceBodyWrap);\n\n // update header\n if (_tagReference) {\n selection.selectAll('.preset-list-button-wrap .accessory-buttons')\n .style('display', _presets.length === 1 ? null : 'none')\n .call(_tagReference.button);\n\n tagReferenceBodyWrap\n .style('display', _presets.length === 1 ? null : 'none')\n .call(_tagReference.body);\n }\n\n selection.selectAll('.preset-reset')\n .on('click', function() {\n dispatch.call('choose', this, _presets);\n })\n .on('pointerdown pointerup mousedown mouseup', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n });\n\n var geometries = entityGeometries();\n selection.select('.preset-list-item button')\n .call(uiPresetIcon()\n .geometry(_presets.length === 1 ? (geometries.length === 1 && geometries[0]) : null)\n .preset(_presets.length === 1 ? _presets[0] : presetManager.item('point'))\n );\n\n var names = _presets.length === 1 ? [\n _presets[0].nameLabel(),\n _presets[0].subtitleLabel()\n ].filter(Boolean) : [ t.append('inspector.multiple_types') ];\n\n var label = selection.select('.label-inner');\n var nameparts = label.selectAll('.namepart')\n .data(names, d => d.stringId);\n\n nameparts.exit()\n .remove();\n\n nameparts\n .enter()\n .append('div')\n .attr('class', 'namepart')\n .text('')\n .each(function(d) { d(d3_select(this)); });\n }\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return section;\n };\n\n section.presets = function(val) {\n if (!arguments.length) return _presets;\n\n // don't reload the same preset\n if (!utilArrayIdentical(val, _presets)) {\n _presets = val;\n\n if (_presets.length === 1) {\n _tagReference = uiTagReference(_presets[0].reference(), context)\n .showing(false);\n }\n }\n\n return section;\n };\n\n function entityGeometries() {\n\n var counts = {};\n\n for (var i in _entityIDs) {\n var geometry = context.graph().geometry(_entityIDs[i]);\n if (!counts[geometry]) counts[geometry] = 0;\n counts[geometry] += 1;\n }\n\n return Object.keys(counts).sort(function(geom1, geom2) {\n return counts[geom2] - counts[geom1];\n });\n }\n\n return utilRebind(section, dispatch, 'on');\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { uiCombobox } from './combobox';\nimport { utilGetSetValue, utilNoAuto } from '../util';\n\n\nexport function uiFormFields(context) {\n var moreCombo = uiCombobox(context, 'more-fields').minItems(1);\n var _fieldsArr = [];\n var _lastPlaceholder = '';\n var _state = '';\n var _klass = '';\n\n\n function formFields(selection) {\n var allowedFields = _fieldsArr.filter(function(field) { return field.isAllowed(); });\n var shown = allowedFields.filter(function(field) { return field.isShown(); });\n var notShown = allowedFields.filter(function(field) { return !field.isShown(); })\n .sort(function(a, b) { return (a.universal === b.universal ? 0 : a.universal ? 1 : -1); });\n\n var container = selection.selectAll('.form-fields-container')\n .data([0]);\n\n container = container.enter()\n .append('div')\n .attr('class', 'form-fields-container ' + (_klass || ''))\n .merge(container);\n\n\n var fields = container.selectAll('.wrap-form-field')\n .data(shown, function(d) { return d.id + (d.entityIDs ? d.entityIDs.join() : ''); });\n\n fields.exit()\n .remove();\n\n // Enter\n var enter = fields.enter()\n .append('div')\n .attr('class', function(d) { return 'wrap-form-field wrap-form-field-' + d.safeid; });\n\n // Update\n fields = fields\n .merge(enter);\n\n fields\n .order()\n .each(function(d) {\n d3_select(this)\n .call(d.render);\n });\n\n\n var titles = [];\n var moreFields = notShown.map(function(field) {\n var title = field.title();\n titles.push(title);\n\n var terms = field.terms();\n if (field.key) terms.push(field.key);\n if (field.keys) terms = terms.concat(field.keys);\n\n return {\n display: field.label(),\n value: title,\n title: title,\n field: field,\n terms: terms\n };\n });\n\n var placeholder = titles.slice(0,3).join(', ') + ((titles.length > 3) ? '\u2026' : '');\n\n\n var more = selection.selectAll('.more-fields')\n .data((_state === 'hover' || moreFields.length === 0) ? [] : [0]);\n\n more.exit()\n .remove();\n\n var moreEnter = more.enter()\n .append('div')\n .attr('class', 'more-fields')\n .append('label');\n\n moreEnter\n .append('span')\n .call(t.append('inspector.add_fields'));\n\n more = moreEnter\n .merge(more);\n\n\n var input = more.selectAll('.value')\n .data([0]);\n\n input.exit()\n .remove();\n\n input = input.enter()\n .append('input')\n .attr('class', 'value')\n .attr('type', 'text')\n .attr('placeholder', placeholder)\n .call(utilNoAuto)\n .merge(input);\n\n input\n .call(utilGetSetValue, '')\n .call(moreCombo\n .data(moreFields)\n .on('accept', function (d) {\n if (!d) return; // user entered something that was not matched\n var field = d.field;\n field.show();\n selection.call(formFields); // rerender\n field.focus();\n })\n );\n\n // avoid updating placeholder excessively (triggers style recalc)\n if (_lastPlaceholder !== placeholder) {\n input.attr('placeholder', placeholder);\n _lastPlaceholder = placeholder;\n }\n }\n\n\n formFields.fieldsArr = function(val) {\n if (!arguments.length) return _fieldsArr;\n _fieldsArr = val || [];\n return formFields;\n };\n\n formFields.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n return formFields;\n };\n\n formFields.klass = function(val) {\n if (!arguments.length) return _klass;\n _klass = val;\n return formFields;\n };\n\n\n return formFields;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { presetManager } from '../../presets';\nimport { t, localizer } from '../../core/localizer';\nimport { utilArrayIdentical } from '../../util/array';\nimport { utilArrayUnion, utilRebind } from '../../util';\nimport { geoExtent } from '../../geo/extent';\nimport { uiField } from '../field';\nimport { uiFormFields } from '../form_fields';\nimport { uiSection } from '../section';\n\nexport function uiSectionPresetFields(context) {\n\n var section = uiSection('preset-fields', context)\n .label(() => t.append('inspector.fields'))\n .disclosureContent(renderDisclosureContent);\n\n var dispatch = d3_dispatch('change', 'revert');\n var formFields = uiFormFields(context);\n var _state;\n var _fieldsArr;\n var _presets = [];\n var _tags;\n var _entityIDs;\n\n function renderDisclosureContent(selection) {\n if (!_fieldsArr) {\n\n var graph = context.graph();\n\n var geometries = Object.keys(_entityIDs.reduce(function(geoms, entityID) {\n geoms[graph.entity(entityID).geometry(graph)] = true;\n return geoms;\n }, {}));\n\n const loc = _entityIDs.reduce(function(extent, entityID) {\n var entity = context.graph().entity(entityID);\n return extent.extend(entity.extent(context.graph()));\n }, geoExtent()).center();\n\n var presetsManager = presetManager;\n\n var allFields = [];\n var allMoreFields = [];\n var sharedTotalFields;\n\n _presets.forEach(function(preset) {\n var fields = preset.fields(loc);\n var moreFields = preset.moreFields(loc);\n\n allFields = utilArrayUnion(allFields, fields);\n allMoreFields = utilArrayUnion(allMoreFields, moreFields);\n\n if (!sharedTotalFields) {\n sharedTotalFields = utilArrayUnion(fields, moreFields);\n } else {\n sharedTotalFields = sharedTotalFields.filter(function(field) {\n return fields.indexOf(field) !== -1 || moreFields.indexOf(field) !== -1;\n });\n }\n });\n\n var sharedFields = allFields.filter(function(field) {\n return sharedTotalFields.indexOf(field) !== -1;\n });\n var sharedMoreFields = allMoreFields.filter(function(field) {\n return sharedTotalFields.indexOf(field) !== -1;\n });\n\n _fieldsArr = [];\n\n // Ideally, everything in OpenHistoricalMap is dated and sourced.\n let coreKeys = ['start_date', 'end_date', 'source_preset'];\n coreKeys.forEach(key => {\n let field = presetsManager.field(key);\n if (field) {\n _fieldsArr.push(uiField(context, field, _entityIDs));\n }\n });\n\n let optionalCoreKeys = ['source_preset:1', 'source_preset:2', 'source_preset:3'];\n optionalCoreKeys.forEach(key => {\n let field = presetsManager.field(key);\n if (field && !_fieldsArr.includes(field)) {\n _fieldsArr.push(uiField(context, field, _entityIDs, { show: false }));\n }\n });\n\n\n sharedFields.forEach(function(field) {\n if (!coreKeys.includes(field.id) && !optionalCoreKeys.includes(field.id) && field.matchAllGeometry(geometries)) {\n _fieldsArr.push(\n uiField(context, field, _entityIDs)\n );\n }\n });\n\n var singularEntity = _entityIDs.length === 1 && graph.hasEntity(_entityIDs[0]);\n if (singularEntity && singularEntity.type === 'node' && singularEntity.isHighwayIntersection(graph) && presetsManager.field('restrictions')) {\n _fieldsArr.push(\n uiField(context, presetsManager.field('restrictions'), _entityIDs)\n );\n }\n\n var additionalFields = utilArrayUnion(sharedMoreFields, presetsManager.universal());\n additionalFields.sort(function(field1, field2) {\n return field1.title().localeCompare(field2.title(), localizer.localeCode());\n });\n\n additionalFields.forEach(function(field) {\n if (sharedFields.indexOf(field) === -1 &&\n !coreKeys.includes(field.id) &&\n !optionalCoreKeys.includes(field.id) &&\n field.matchAllGeometry(geometries)) {\n _fieldsArr.push(\n uiField(context, field, _entityIDs, { show: false })\n );\n }\n });\n\n _fieldsArr.forEach(function(field) {\n field\n .on('change', function(t, onInput) {\n dispatch.call('change', field, _entityIDs, t, onInput);\n })\n .on('revert', function(keys) {\n dispatch.call('revert', field, keys);\n });\n });\n }\n\n _fieldsArr.forEach(function(field) {\n field\n .state(_state)\n .tags(_tags);\n });\n\n\n selection\n .call(formFields\n .fieldsArr(_fieldsArr)\n .state(_state)\n .klass('grouped-items-area')\n );\n }\n\n section.presets = function(val) {\n if (!arguments.length) return _presets;\n if (!_presets || !val || !utilArrayIdentical(_presets, val)) {\n _presets = val;\n _fieldsArr = null;\n }\n return section;\n };\n\n section.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n return section;\n };\n\n section.tags = function(val) {\n if (!arguments.length) return _tags;\n _tags = val;\n // Don't reset _fieldsArr here.\n return section;\n };\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n if (!val || !_entityIDs || !utilArrayIdentical(_entityIDs, val)) {\n _entityIDs = val;\n _fieldsArr = null;\n }\n return section;\n };\n\n return utilRebind(section, dispatch, 'on');\n}\n", "import { drag as d3_drag } from 'd3-drag';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { actionChangeMember } from '../../actions/change_member';\nimport { actionDeleteMember } from '../../actions/delete_member';\nimport { actionMoveMember } from '../../actions/move_member';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { osmEntity } from '../../osm';\nimport { getRelationColor } from '../../osm/tags';\nimport { svgIcon } from '../../svg/icon';\nimport { services } from '../../services';\nimport { uiCombobox } from '../combobox';\nimport { uiSection } from '../section';\nimport { utilDisplayName, utilDisplayType, utilHighlightEntities, utilNoAuto, utilUniqueDomId } from '../../util';\nimport { prefs } from '../../core';\n\n\nexport function uiSectionRawMemberEditor(context) {\n\n var section = uiSection('raw-member-editor', context)\n .shouldDisplay(function() {\n if (!_entityIDs || _entityIDs.length !== 1) return false;\n\n var entity = context.hasEntity(_entityIDs[0]);\n return entity && entity.type === 'relation';\n })\n .label(function() {\n var entity = context.hasEntity(_entityIDs[0]);\n if (!entity) return '';\n\n var gt = entity.members.length > _maxMembers ? '>' : '';\n var count = gt + entity.members.slice(0, _maxMembers).length;\n return t.append('inspector.title_count', { title: t.append('inspector.members'), count: count });\n })\n .disclosureContent(renderDisclosureContent);\n\n var taginfo = services.taginfo;\n var _entityIDs;\n var _maxMembers = 1000;\n\n function downloadMember(d3_event, d) {\n d3_event.preventDefault();\n\n // display the loading indicator\n d3_select(this).classed('loading', true);\n context.loadEntity(d.id, function() {\n section.reRender();\n });\n }\n\n function zoomToMember(d3_event, d) {\n d3_event.preventDefault();\n\n var entity = context.entity(d.id);\n context.map().zoomToEase(entity);\n\n // highlight the feature in case it wasn't previously on-screen\n utilHighlightEntities([d.id], true, context);\n }\n\n\n function selectMember(d3_event, d) {\n d3_event.preventDefault();\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.id], false, context);\n\n var entity = context.entity(d.id);\n var mapExtent = context.map().extent();\n if (!entity.intersects(mapExtent, context.graph())) {\n // zoom to the entity if its extent is not visible now\n context.map().zoomToEase(entity);\n }\n\n context.enter(modeSelect(context, [d.id]));\n }\n\n\n function changeRole(d3_event, d) {\n var oldRole = d.role;\n var newRole = context.cleanRelationRole(d3_select(this).property('value'));\n\n if (oldRole !== newRole) {\n var member = { id: d.id, type: d.type, role: newRole };\n context.perform(\n actionChangeMember(d.relation.id, member, d.index),\n t('operations.change_role.annotation', {\n n: 1\n })\n );\n context.validator().validate();\n }\n }\n\n\n function deleteMember(d3_event, d) {\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.id], false, context);\n\n context.perform(\n actionDeleteMember(d.relation.id, d.index),\n t('operations.delete_member.annotation', {\n n: 1\n })\n );\n\n if (!context.hasEntity(d.relation.id)) {\n // Removing the last member will also delete the relation.\n // If this happens we need to exit the selection mode\n context.enter(modeBrowse(context));\n } else {\n // Changing the mode also runs `validate`, but otherwise we need to\n // rerun it manually\n context.validator().validate();\n }\n }\n\n function renderDisclosureContent(selection) {\n\n var entityID = _entityIDs[0];\n\n var memberships = [];\n var entity = context.entity(entityID);\n entity.members.slice(0, _maxMembers).forEach(function(member, index) {\n memberships.push({\n index: index,\n id: member.id,\n type: member.type,\n role: member.role,\n relation: entity,\n member: context.hasEntity(member.id),\n domId: utilUniqueDomId(entityID + '-member-' + index)\n });\n });\n\n var list = selection.selectAll('.member-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'member-list')\n .merge(list);\n\n\n var items = list.selectAll('li')\n .data(memberships, function(d) {\n return osmEntity.key(d.relation) + ',' + d.index + ',' +\n (d.member ? osmEntity.key(d.member) : 'incomplete');\n });\n\n items.exit()\n .each(unbind)\n .remove();\n\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', 'member-row form-field')\n .classed('member-incomplete', function(d) { return !d.member; });\n\n itemsEnter\n .each(function(d) {\n var item = d3_select(this);\n\n var label = item\n .append('label')\n .attr('class', 'field-label')\n .attr('for', d.domId);\n\n if (d.member) {\n // highlight the member feature in the map while hovering on the list item\n item\n .on('mouseover', function() {\n utilHighlightEntities([d.id], true, context);\n })\n .on('mouseout', function() {\n utilHighlightEntities([d.id], false, context);\n });\n\n var labelLink = label\n .append('span')\n .attr('class', 'label-text')\n .append('a')\n .attr('href', '#')\n .on('click', selectMember);\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-type')\n .text(function(d) {\n var matched = presetManager.match(d.member, context.graph());\n while (matched?.suggestion) {\n // if is NSI preset: look for a parent preset\n matched = matched.getParentPreset();\n }\n return (matched && matched.name()) || utilDisplayType(d.member.id);\n });\n\n const showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n labelLink.each(function(d) {\n if (!showThirdPartyIcons) return;\n const matched = presetManager.match(d, context.graph());\n if (matched.suggestion) {\n // if matching an NSI preset: append icon\n d3_select(this)\n .append('img')\n .classed('member-entity-icon', true)\n .attr('src', matched.imageURL);\n }\n });\n\n labelLink.each(function(d) {\n if (d.type !== 'relation') return;\n const relColors = getRelationColor(d.member.tags, '#555');\n const hasRef = d.member.tags.ref;\n if (relColors.isValid || hasRef) {\n const refs = (d.member.tags.ref || '').split(';');\n for (const ref of refs) {\n d3_select(this)\n .append('span')\n .classed('member-entity-ref-color', true)\n .style('border-color', relColors.color)\n .style('background-color', relColors.color)\n .style('color', relColors.textColor)\n .text(ref);\n }\n }\n });\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-name')\n .text(function(d) { return utilDisplayName(d.member, { hideRef: true }); });\n\n label\n .append('button')\n .attr('title', t('icons.remove'))\n .attr('class', 'remove member-delete')\n .call(svgIcon('#iD-operation-delete'));\n\n label\n .append('button')\n .attr('class', 'member-zoom')\n .attr('title', t('icons.zoom_to'))\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'))\n .on('click', zoomToMember);\n\n } else {\n var labelText = label\n .append('span')\n .attr('class', 'label-text');\n\n labelText\n .append('span')\n .attr('class', 'member-entity-type')\n .call(t.append('inspector.' + d.type, { id: d.id }));\n\n labelText\n .append('span')\n .attr('class', 'member-entity-name')\n .call(t.append('inspector.incomplete', { id: d.id }));\n\n label\n .append('button')\n .attr('class', 'member-download')\n .attr('title', t('icons.download'))\n .call(svgIcon('#iD-icon-load'))\n .on('click', downloadMember);\n }\n });\n\n var wrapEnter = itemsEnter\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-member');\n\n wrapEnter\n .append('input')\n .attr('class', 'member-role')\n .attr('id', function(d) {\n return d.domId;\n })\n .property('type', 'text')\n .attr('placeholder', t('inspector.role'))\n .call(utilNoAuto);\n\n if (taginfo) {\n wrapEnter.each(bindTypeahead);\n }\n\n // update\n items = items\n .merge(itemsEnter)\n .order();\n\n items.select('input.member-role')\n .property('value', function(d) { return d.role; })\n .on('blur', changeRole)\n .on('change', changeRole);\n\n items.select('button.member-delete')\n .on('click', deleteMember);\n\n var dragOrigin, targetIndex;\n\n items.call(d3_drag()\n .on('start', function(d3_event) {\n dragOrigin = {\n x: d3_event.x,\n y: d3_event.y\n };\n targetIndex = null;\n })\n .on('drag', function(d3_event) {\n var x = d3_event.x - dragOrigin.x,\n y = d3_event.y - dragOrigin.y;\n\n if (!d3_select(this).classed('dragging') &&\n // don't display drag until dragging beyond a distance threshold\n Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) <= 5) return;\n\n var index = items.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', true);\n\n targetIndex = null;\n\n selection.selectAll('li.member-row')\n .style('transform', function(d2, index2) {\n var node = d3_select(this).node();\n if (index === index2) {\n return 'translate(' + x + 'px, ' + y + 'px)';\n } else if (index2 > index && d3_event.y > node.offsetTop) {\n if (targetIndex === null || index2 > targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(-100%)';\n } else if (index2 < index && d3_event.y < node.offsetTop + node.offsetHeight) {\n if (targetIndex === null || index2 < targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(100%)';\n }\n return null;\n });\n })\n .on('end', function(d3_event, d) {\n\n if (!d3_select(this).classed('dragging')) return;\n\n var index = items.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', false);\n\n selection.selectAll('li.member-row')\n .style('transform', null);\n\n if (targetIndex !== null) {\n // dragged to a new position, reorder\n context.perform(\n actionMoveMember(d.relation.id, index, targetIndex),\n t('operations.reorder_members.annotation')\n );\n context.validator().validate();\n }\n })\n );\n\n\n\n function bindTypeahead(d) {\n var row = d3_select(this);\n var role = row.selectAll('input.member-role');\n var origValue = role.property('value');\n\n function sort(value, data) {\n var sameletter = [];\n var other = [];\n for (var i = 0; i < data.length; i++) {\n if (data[i].value.substring(0, value.length) === value) {\n sameletter.push(data[i]);\n } else {\n other.push(data[i]);\n }\n }\n return sameletter.concat(other);\n }\n\n role.call(uiCombobox(context, 'member-role')\n .fetcher(function(role, callback) {\n // The `geometry` param is used in the `taginfo.js` interface for\n // filtering results, as a key into the `tag_members_fractions`\n // object. If we don't know the geometry because the member is\n // not yet downloaded, it's ok to guess based on type.\n var geometry;\n if (d.member) {\n geometry = context.graph().geometry(d.member.id);\n } else if (d.type === 'relation') {\n geometry = 'relation';\n } else if (d.type === 'way') {\n geometry = 'line';\n } else {\n geometry = 'point';\n }\n\n var rtype = entity.tags.type;\n taginfo.roles({\n debounce: true,\n rtype: rtype || '',\n geometry: geometry,\n query: role\n }, function(err, data) {\n if (!err) callback(sort(role, data));\n });\n })\n .on('cancel', function() {\n role.property('value', origValue);\n })\n );\n }\n\n\n function unbind() {\n var row = d3_select(this);\n\n row.selectAll('input.member-role')\n .call(uiCombobox.off, context);\n }\n }\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return section;\n };\n\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t, localizer } from '../../core/localizer';\n\nimport { actionAddEntity } from '../../actions/add_entity';\nimport { actionAddMember } from '../../actions/add_member';\nimport { actionChangeMember } from '../../actions/change_member';\nimport { actionDeleteMembers } from '../../actions/delete_members';\n\nimport { modeSelect } from '../../modes/select';\nimport { osmEntity, osmRelation } from '../../osm';\nimport { getRelationColor, isColorValid } from '../../osm/tags';\nimport { services } from '../../services';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCombobox } from '../combobox';\nimport { uiSection } from '../section';\nimport { uiTooltip } from '../tooltip';\nimport { utilArrayGroupBy, utilArrayIntersection } from '../../util/array';\nimport { utilDisplayName, utilNoAuto, utilHighlightEntities, utilUniqueDomId } from '../../util';\nimport { prefs } from '../../core';\nimport { idMatch } from '../feature_list';\n\n\nexport function uiSectionRawMembershipEditor(context) {\n\n var section = uiSection('raw-membership-editor', context)\n .shouldDisplay(function() {\n return _entityIDs && _entityIDs.length;\n })\n .label(function() {\n var parents = getSharedParentRelations();\n var gt = parents.length > _maxMemberships ? '>' : '';\n var count = gt + parents.slice(0, _maxMemberships).length;\n return t.append('inspector.title_count', { title: t.append('inspector.relations'), count: count });\n })\n .disclosureContent(renderDisclosureContent);\n\n var taginfo = services.taginfo;\n var nearbyCombo = uiCombobox(context, 'parent-relation')\n .minItems(1)\n .fetcher(fetchNearbyRelations)\n .itemsMouseEnter(function(d3_event, d) {\n if (d.relation) utilHighlightEntities([d.relation.id], true, context);\n })\n .itemsMouseLeave(function(d3_event, d) {\n if (d.relation) utilHighlightEntities([d.relation.id], false, context);\n });\n var _inChange = false;\n var _entityIDs = [];\n var _showBlank;\n var _maxMemberships = 1000;\n /** @type {Set} relations that were added after this panel was opened */\n const recentlyAdded = new Set();\n\n function getSharedParentRelations() {\n var parents = [];\n for (var i = 0; i < _entityIDs.length; i++) {\n var entity = context.graph().hasEntity(_entityIDs[i]);\n if (!entity) continue;\n\n if (i === 0) {\n parents = context.graph().parentRelations(entity);\n } else {\n parents = utilArrayIntersection(parents, context.graph().parentRelations(entity));\n }\n if (!parents.length) break;\n }\n return parents;\n }\n\n function getMemberships() {\n\n var memberships = [];\n var relations = getSharedParentRelations().slice(0, _maxMemberships);\n\n var isMultiselect = _entityIDs.length > 1;\n\n var i, relation, membership, index, member, indexedMember;\n for (i = 0; i < relations.length; i++) {\n relation = relations[i];\n membership = {\n relation: relation,\n members: [],\n hash: osmEntity.key(relation)\n };\n for (index = 0; index < relation.members.length; index++) {\n member = relation.members[index];\n if (_entityIDs.indexOf(member.id) !== -1) {\n indexedMember = Object.assign({}, member, { index: index });\n membership.members.push(indexedMember);\n membership.hash += ',' + index.toString();\n\n if (!isMultiselect) {\n // For single selections, list one entry per membership per relation.\n // For multiselections, list one entry per relation.\n\n memberships.push(membership);\n membership = {\n relation: relation,\n members: [],\n hash: osmEntity.key(relation)\n };\n }\n }\n }\n if (membership.members.length) memberships.push(membership);\n }\n\n memberships.forEach(function(membership) {\n membership.domId = utilUniqueDomId('membership-' + membership.relation.id);\n var roles = [];\n membership.members.forEach(function(member) {\n if (roles.indexOf(member.role) === -1) roles.push(member.role);\n });\n membership.role = roles.length === 1 ? roles[0] : roles;\n });\n\n const existingRelations = memberships\n .filter(membership => !recentlyAdded.has(membership.relation.id))\n .map(membership => ({\n ...membership,\n // We only sort relations that were not added just now.\n // Sorting uses the same label as shown in the UI.\n // If the label is not unique, the relation ID ensures\n // that the sort order is still stable.\n _sortKey: [\n baseDisplayValue(membership.relation),\n membership.relation.id,\n ].join('-'),\n }))\n .sort((a, b) => {\n return a._sortKey.localeCompare(\n b._sortKey,\n localizer.localeCodes(),\n { numeric: true },\n );\n });\n\n\n const newlyAddedRelations = memberships\n .filter(membership => recentlyAdded.has(membership.relation.id));\n\n return [\n // the sorted relations come first\n ...existingRelations,\n // then the ones that were just added from this panel\n ...newlyAddedRelations,\n ];\n }\n\n function selectRelation(d3_event, d) {\n d3_event.preventDefault();\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.relation.id], false, context);\n\n context.enter(modeSelect(context, [d.relation.id]));\n }\n\n function zoomToRelation(d3_event, d) {\n d3_event.preventDefault();\n\n var entity = context.entity(d.relation.id);\n context.map().zoomToEase(entity);\n\n // highlight the relation in case it wasn't previously on-screen\n utilHighlightEntities([d.relation.id], true, context);\n }\n\n\n function changeRole(d3_event, d) {\n if (d === 0) return; // called on newrow (shouldn't happen)\n if (_inChange) return; // avoid accidental recursive call #5731\n\n var newRole = context.cleanRelationRole(d3_select(this).property('value'));\n\n if (!newRole.trim() && typeof d.role !== 'string') return;\n\n var membersToUpdate = d.members.filter(function(member) {\n return member.role !== newRole;\n });\n\n if (membersToUpdate.length) {\n _inChange = true;\n context.perform(\n function actionChangeMemberRoles(graph) {\n membersToUpdate.forEach(function(member) {\n var newMember = Object.assign({}, member, { role: newRole });\n delete newMember.index;\n graph = actionChangeMember(d.relation.id, newMember, member.index)(graph);\n });\n return graph;\n },\n t('operations.change_role.annotation', {\n n: membersToUpdate.length\n })\n );\n context.validator().validate();\n }\n _inChange = false;\n }\n\n\n function addMembership(d, role) {\n _showBlank = false;\n\n function actionAddMembers(relationId, ids, role) {\n return function(graph) {\n for (var i in ids) {\n var member = { id: ids[i], type: graph.entity(ids[i]).type, role: role };\n graph = actionAddMember(relationId, member)(graph);\n }\n return graph;\n };\n }\n\n if (d.relation) {\n recentlyAdded.add(d.relation.id);\n context.perform(\n actionAddMembers(d.relation.id, _entityIDs, role),\n t('operations.add_member.annotation', {\n n: _entityIDs.length\n })\n );\n context.validator().validate();\n\n } else {\n var relation = osmRelation();\n context.perform(\n actionAddEntity(relation),\n actionAddMembers(relation.id, _entityIDs, role),\n t('operations.add.annotation.relation')\n );\n // changing the mode also runs `validate`\n context.enter(modeSelect(context, [relation.id]).newFeature(true));\n }\n }\n\n\n function downloadMembers(d3_event, d) {\n d3_event.preventDefault();\n const button = d3_select(this);\n\n // display the loading indicator\n button.classed('loading', true);\n context.loadEntity(d.relation.id, function() {\n section.reRender();\n });\n }\n\n\n function deleteMembership(d3_event, d) {\n this.blur(); // avoid keeping focus on the button\n if (d === 0) return; // called on newrow (shouldn't happen)\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.relation.id], false, context);\n\n var indexes = d.members.map(function(member) {\n return member.index;\n });\n\n context.perform(\n actionDeleteMembers(d.relation.id, indexes),\n t('operations.delete_member.annotation', {\n n: _entityIDs.length\n })\n );\n context.validator().validate();\n }\n\n\n function fetchNearbyRelations(q, callback) {\n var newRelation = {\n relation: null,\n value: t('inspector.new_relation'),\n display: t.append('inspector.new_relation')\n };\n\n var entityID = _entityIDs[0];\n\n var result = [];\n\n var graph = context.graph();\n\n function baseDisplayLabel(entity) {\n var matched = presetManager.match(entity, graph);\n var presetName = (matched && matched.name()) || t('inspector.relation');\n var entityName = utilDisplayName(entity) || '';\n\n return selection => {\n selection\n .append('b')\n .text(presetName + ' ');\n selection\n .append('span')\n .classed('has-colour', entity.tags.colour && isColorValid(entity.tags.colour))\n .style('border-color', entity.tags.colour)\n .text(entityName);\n };\n }\n\n\n // A location search takes priority over an ID search\n const idMatchResult = q && idMatch(q);\n var explicitRelation = context.hasEntity(`r${idMatchResult?.id || q}`);\n if (explicitRelation && explicitRelation.type === 'relation' && explicitRelation.id !== entityID) {\n // loaded relation is specified explicitly, only show that\n\n result.push({\n relation: explicitRelation,\n value: baseDisplayValue(explicitRelation) + ' ' + explicitRelation.id,\n display: baseDisplayLabel(explicitRelation)\n });\n } else {\n\n context.history().intersects(context.map().extent()).forEach(function(entity) {\n if (entity.type !== 'relation' || entity.id === entityID) return;\n\n var value = baseDisplayValue(entity);\n if (q && (value + ' ' + entity.id).toLowerCase().indexOf(q.toLowerCase()) === -1) return;\n\n result.push({\n relation: entity,\n value,\n display: baseDisplayLabel(entity)\n });\n });\n\n result.sort(function(a, b) {\n return osmRelation.creationOrder(a.relation, b.relation);\n });\n\n // Dedupe identical names by appending relation id - see #2891\n Object.values(utilArrayGroupBy(result, 'value'))\n .filter(v => v.length > 1)\n .flat()\n .forEach(obj => obj.value += ' ' + obj.relation.id);\n }\n\n result.forEach(function(obj) {\n obj.title = obj.value;\n });\n\n result.unshift(newRelation);\n callback(result);\n }\n\n function baseDisplayValue(entity) {\n const graph = context.graph();\n var matched = presetManager.match(entity, graph);\n var presetName = (matched && matched.name()) || t('inspector.relation');\n var entityName = utilDisplayName(entity) || '';\n\n return presetName + ' ' + entityName;\n }\n\n function renderDisclosureContent(selection) {\n\n var memberships = getMemberships();\n\n var list = selection.selectAll('.member-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'member-list')\n .merge(list);\n\n\n var items = list.selectAll('li.member-row-normal')\n .data(memberships, function(d) {\n return d.hash;\n });\n\n items.exit()\n .each(unbind)\n .remove();\n\n // Enter\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', 'member-row member-row-normal form-field');\n\n // highlight the relation in the map while hovering on the list item\n itemsEnter.on('mouseover', function(d3_event, d) {\n utilHighlightEntities([d.relation.id], true, context);\n })\n .on('mouseout', function(d3_event, d) {\n utilHighlightEntities([d.relation.id], false, context);\n });\n\n var labelEnter = itemsEnter\n .append('label')\n .attr('class', 'field-label')\n .attr('for', function(d) {\n return d.domId;\n });\n\n var labelLink = labelEnter\n .append('span')\n .attr('class', 'label-text')\n .append('a')\n .attr('href', '#')\n .on('click', selectRelation);\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-type')\n .text(d => {\n let matched = presetManager.match(d.relation, context.graph());\n while (matched?.suggestion) {\n // if is NSI preset: look for a parent preset\n matched = matched.getParentPreset();\n }\n return (matched && matched.name()) || t('inspector.relation');\n });\n\n const showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n labelLink.each(function(d) {\n if (!showThirdPartyIcons) return;\n const matched = presetManager.match(d.relation, context.graph());\n if (matched.suggestion) {\n // if matching an NSI preset: append icon\n d3_select(this)\n .append('img')\n .classed('member-entity-icon', true)\n .attr('src', matched.imageURL);\n }\n });\n\n labelLink.each(function(d) {\n const relColors = getRelationColor(d.relation.tags, '#555');\n const hasRef = d.relation.tags.ref;\n if (relColors.isValid || hasRef) {\n const refs = (d.relation.tags.ref || '').split(';');\n for (const ref of refs) {\n d3_select(this)\n .append('span')\n .classed('member-entity-ref-color', true)\n .style('border-color', relColors.color)\n .style('background-color', relColors.color)\n .style('color', relColors.textColor)\n .text(ref);\n }\n }\n });\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-name')\n .text(d => utilDisplayName(d.relation, { hideRef: true }));\n\n labelEnter\n .append('button')\n .attr('class', 'members-download')\n .attr('title', t('icons.download'))\n .call(svgIcon('#iD-icon-load'))\n .on('click', downloadMembers);\n\n labelEnter\n .append('button')\n .attr('class', 'remove member-delete')\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'))\n .on('click', deleteMembership);\n\n labelEnter\n .append('button')\n .attr('class', 'member-zoom')\n .attr('title', t('icons.zoom_to'))\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'))\n .on('click', zoomToRelation);\n\n items = items.merge(itemsEnter);\n items.selectAll('button.members-download')\n .classed('hide', d => {\n const graph = context.graph();\n return d.relation.members.every(m => graph.hasEntity(m.id));\n });\n\n const dupeLabels = new WeakSet(Object.values(\n utilArrayGroupBy(items.selectAll('.label-text').nodes(), 'textContent'))\n .filter(v => v.length > 1)\n .flat());\n\n items.select('.label-text').each(function() {\n const label = d3_select(this);\n const entityName = label.select('.member-entity-name');\n if (dupeLabels.has(this)) {\n // Dedupe identical names in hover text by appending relation id - see #2891, #10184\n label.attr('title', d => `${entityName.text()} ${d.relation.id}`);\n } else {\n // set full label also as hover text: useful if a (long) label is cut off with an \u2026 ellipsis\n label.attr('title', () => entityName.text());\n }\n });\n\n var wrapEnter = itemsEnter\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-member');\n\n wrapEnter\n .append('input')\n .attr('class', 'member-role')\n .attr('id', function(d) {\n return d.domId;\n })\n .property('type', 'text')\n .property('value', function(d) {\n return typeof d.role === 'string' ? d.role : '';\n })\n .attr('title', function(d) {\n return Array.isArray(d.role) ? d.role.filter(Boolean).join('\\n') : d.role;\n })\n .attr('placeholder', function(d) {\n return Array.isArray(d.role) ? t('inspector.multiple_roles') : t('inspector.role');\n })\n .classed('mixed', function(d) {\n return Array.isArray(d.role);\n })\n .call(utilNoAuto)\n .on('blur', changeRole)\n .on('change', changeRole);\n\n if (taginfo) {\n wrapEnter.each(bindTypeahead);\n }\n\n var newMembership = list.selectAll('.member-row-new')\n .data(_showBlank ? [0] : []);\n\n // Exit\n newMembership.exit()\n .remove();\n\n // Enter\n var newMembershipEnter = newMembership.enter()\n .append('li')\n .attr('class', 'member-row member-row-new form-field');\n\n var newLabelEnter = newMembershipEnter\n .append('label')\n .attr('class', 'field-label');\n\n newLabelEnter\n .append('input')\n .attr('placeholder', t('inspector.choose_relation'))\n .attr('type', 'text')\n .attr('class', 'member-entity-input')\n .call(utilNoAuto);\n\n newLabelEnter\n .append('button')\n .attr('class', 'remove member-delete')\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'))\n .on('click', function() {\n list.selectAll('.member-row-new')\n .remove();\n });\n\n var newWrapEnter = newMembershipEnter\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-member');\n\n newWrapEnter\n .append('input')\n .attr('class', 'member-role')\n .property('type', 'text')\n .attr('placeholder', t('inspector.role'))\n .call(utilNoAuto);\n\n // Update\n newMembership = newMembership\n .merge(newMembershipEnter);\n\n newMembership.selectAll('.member-entity-input')\n .on('blur', cancelEntity) // if it wasn't accepted normally, cancel it\n .call(nearbyCombo\n .on('accept', function(d) {\n this.blur(); // always blurs the triggering element\n acceptEntity.call(this, d);\n })\n .on('cancel', cancelEntity)\n );\n\n\n // Container for the Add button\n var addRow = selection.selectAll('.add-row')\n .data([0]);\n\n // enter\n var addRowEnter = addRow.enter()\n .append('div')\n .attr('class', 'add-row');\n\n var addRelationButton = addRowEnter\n .append('button')\n .attr('class', 'add-relation')\n .attr('aria-label', t('inspector.add_to_relation'));\n\n addRelationButton\n .call(svgIcon('#iD-icon-plus', 'light'));\n addRelationButton\n .call(uiTooltip()\n .title(() => t.append('inspector.add_to_relation'))\n .placement(localizer.textDirection() === 'ltr' ? 'right' : 'left'));\n\n addRowEnter\n .append('div')\n .attr('class', 'space-value'); // preserve space\n\n addRowEnter\n .append('div')\n .attr('class', 'space-buttons'); // preserve space\n\n // update\n addRow = addRow\n .merge(addRowEnter);\n\n addRow.select('.add-relation')\n .on('click', function() {\n _showBlank = true;\n section.reRender();\n list.selectAll('.member-entity-input').node().focus();\n });\n\n\n function acceptEntity(d) {\n if (!d) {\n cancelEntity();\n return;\n }\n // remove hover-higlighting\n if (d.relation) utilHighlightEntities([d.relation.id], false, context);\n\n var role = context.cleanRelationRole(list.selectAll('.member-row-new .member-role').property('value'));\n addMembership(d, role);\n }\n\n\n function cancelEntity() {\n var input = newMembership.selectAll('.member-entity-input');\n input.property('value', '');\n\n // remove hover-higlighting\n context.surface().selectAll('.highlighted')\n .classed('highlighted', false);\n }\n\n\n function bindTypeahead(d) {\n var row = d3_select(this);\n var role = row.selectAll('input.member-role');\n var origValue = role.property('value');\n\n function sort(value, data) {\n var sameletter = [];\n var other = [];\n for (var i = 0; i < data.length; i++) {\n if (data[i].value.substring(0, value.length) === value) {\n sameletter.push(data[i]);\n } else {\n other.push(data[i]);\n }\n }\n return sameletter.concat(other);\n }\n\n role.call(uiCombobox(context, 'member-role')\n .fetcher(function(role, callback) {\n var rtype = d.relation.tags.type;\n taginfo.roles({\n debounce: true,\n rtype: rtype || '',\n geometry: context.graph().geometry(_entityIDs[0]),\n query: role\n }, function(err, data) {\n if (!err) callback(sort(role, data));\n });\n })\n .on('cancel', function() {\n role.property('value', origValue);\n })\n );\n }\n\n\n function unbind() {\n var row = d3_select(this);\n\n row.selectAll('input.member-role')\n .call(uiCombobox.off, context);\n }\n }\n\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n const didChange = _entityIDs.join(',') !== val.join(',');\n _entityIDs = val;\n _showBlank = false;\n if (didChange) {\n recentlyAdded.clear(); // reset when the selected feature changes\n }\n return section;\n };\n\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { modeSelect } from '../../modes/select';\nimport { osmEntity } from '../../osm';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\nimport { t } from '../../core/localizer';\nimport { utilDisplayName, utilHighlightEntities } from '../../util';\n\nexport function uiSectionSelectionList(context) {\n\n var _selectedIDs = [];\n\n var section = uiSection('selected-features', context)\n .shouldDisplay(function() {\n return _selectedIDs.length > 1;\n })\n .label(function() {\n return t.append('inspector.title_count', { title: t.append('inspector.features'), count: _selectedIDs.length });\n })\n .disclosureContent(renderDisclosureContent);\n\n context.history()\n .on('change.selectionList', function(difference) {\n if (difference) {\n section.reRender();\n }\n });\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _selectedIDs;\n _selectedIDs = val;\n return section;\n };\n\n function selectEntity(d3_event, entity) {\n context.enter(modeSelect(context, [entity.id]));\n }\n\n function deselectEntity(d3_event, entity) {\n var selectedIDs = _selectedIDs.slice();\n var index = selectedIDs.indexOf(entity.id);\n if (index > -1) {\n selectedIDs.splice(index, 1);\n context.enter(modeSelect(context, selectedIDs));\n }\n }\n\n function renderDisclosureContent(selection) {\n\n var list = selection.selectAll('.feature-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'feature-list')\n .merge(list);\n\n var entities = _selectedIDs\n .map(function(id) { return context.hasEntity(id); })\n .filter(Boolean);\n\n var items = list.selectAll('.feature-list-item')\n .data(entities, osmEntity.key);\n\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li')\n .attr('class', 'feature-list-item')\n .each(function(d) {\n d3_select(this)\n .on('mouseover', function() {\n utilHighlightEntities([d.id], true, context);\n })\n .on('mouseout', function() {\n utilHighlightEntities([d.id], false, context);\n });\n });\n\n var label = enter\n .append('button')\n .attr('class', 'label')\n .on('click', selectEntity);\n\n label\n .append('span')\n .attr('class', 'entity-geom-icon')\n .call(svgIcon('', 'pre-text'));\n\n label\n .append('span')\n .attr('class', 'entity-type');\n\n label\n .append('span')\n .attr('class', 'entity-name');\n\n enter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.deselect'))\n .on('click', deselectEntity)\n .call(svgIcon('#iD-icon-close'));\n\n // Update\n items = items.merge(enter);\n\n items.selectAll('.entity-geom-icon use')\n .attr('href', function() {\n var entity = this.parentNode.parentNode.__data__;\n return '#iD-icon-' + entity.geometry(context.graph());\n });\n\n items.selectAll('.entity-type')\n .text(function(entity) { return presetManager.match(entity, context.graph()).name(); });\n\n items.selectAll('.entity-name')\n .text(function(d) {\n // fetch latest entity\n var entity = context.entity(d.id);\n return utilDisplayName(entity);\n });\n }\n\n return section;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { deepEqual } from 'fast-equals';\n\nimport { presetManager } from '../presets';\nimport { t, localizer } from '../core/localizer';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\nimport { utilArrayIdentical } from '../util/array';\nimport { utilCleanTags, utilCombinedTags, utilRebind } from '../util';\n\nimport { uiSectionEntityIssues } from './sections/entity_issues';\nimport { uiSectionFeatureType } from './sections/feature_type';\nimport { uiSectionPresetFields } from './sections/preset_fields';\nimport { uiSectionRawMemberEditor } from './sections/raw_member_editor';\nimport { uiSectionRawMembershipEditor } from './sections/raw_membership_editor';\nimport { uiSectionRawTagEditor } from './sections/raw_tag_editor';\nimport { uiSectionSelectionList } from './sections/selection_list';\n\nexport function uiEntityEditor(context) {\n var dispatch = d3_dispatch('choose');\n var _state = 'select';\n var _coalesceChanges = false;\n var _modified = false;\n var _base;\n var _entityIDs;\n var _activePresets = [];\n var _newFeature;\n\n var _sections;\n\n function entityEditor(selection) {\n\n var combinedTags = utilCombinedTags(_entityIDs, context.graph());\n\n // Header\n var header = selection.selectAll('.header')\n .data([0]);\n\n // Enter\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n var direction = (localizer.textDirection() === 'rtl') ? 'forward' : 'backward';\n\n headerEnter\n .append('button')\n .attr('class', 'preset-reset preset-choose')\n .attr('title', t('inspector.back_tooltip'))\n .call(svgIcon(`#iD-icon-${direction}`));\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function() { context.enter(modeBrowse(context)); })\n .call(svgIcon(_modified ? '#iD-icon-apply' : '#iD-icon-close'));\n\n headerEnter\n .append('h2');\n\n // Update\n header = header\n .merge(headerEnter);\n\n header.selectAll('h2')\n .text('')\n .call(_entityIDs.length === 1 ? t.append('inspector.edit') : t.append('inspector.edit_features'));\n\n header.selectAll('.preset-reset')\n .on('click', function() {\n dispatch.call('choose', this, _activePresets);\n });\n\n // Body\n var body = selection.selectAll('.inspector-body')\n .data([0]);\n\n // Enter\n var bodyEnter = body.enter()\n .append('div')\n .attr('class', 'entity-editor inspector-body sep-top');\n\n // Update\n body = body\n .merge(bodyEnter);\n\n if (!_sections) {\n _sections = [\n uiSectionSelectionList(context),\n uiSectionFeatureType(context).on('choose', function(presets) {\n dispatch.call('choose', this, presets);\n }),\n uiSectionEntityIssues(context),\n uiSectionPresetFields(context).on('change', changeTags).on('revert', revertTags),\n uiSectionRawTagEditor('raw-tag-editor', context).on('change', changeTags),\n uiSectionRawMemberEditor(context),\n uiSectionRawMembershipEditor(context)\n ];\n }\n\n _sections.forEach(function(section) {\n if (section.entityIDs) {\n section.entityIDs(_entityIDs);\n }\n if (section.presets) {\n section.presets(_activePresets);\n }\n if (section.tags) {\n section.tags(combinedTags);\n }\n if (section.state) {\n section.state(_state);\n }\n body.call(section.render);\n });\n\n context.history()\n .on('change.entity-editor', historyChanged);\n\n function historyChanged(difference) {\n if (selection.selectAll('.entity-editor').empty()) return;\n if (_state === 'hide') return;\n var significant = !difference ||\n difference.didChange.properties ||\n difference.didChange.addition ||\n difference.didChange.deletion;\n if (!significant) return;\n\n _entityIDs = _entityIDs.filter(context.hasEntity);\n if (!_entityIDs.length) return;\n\n var priorActivePreset = _activePresets.length === 1 && _activePresets[0];\n\n loadActivePresets();\n\n var graph = context.graph();\n entityEditor.modified(_base !== graph);\n entityEditor(selection);\n\n if (priorActivePreset && _activePresets.length === 1 && priorActivePreset !== _activePresets[0]) {\n // flash the button to indicate the preset changed\n context.container().selectAll('.entity-editor button.preset-reset .label')\n .classed('flash-bg', true)\n .on('animationend', function() {\n d3_select(this).classed('flash-bg', false);\n });\n }\n }\n }\n\n\n // Tag changes that fire on input can all get coalesced into a single\n // history operation when the user leaves the field. #2342\n // Use explicit entityIDs in case the selection changes before the event is fired.\n function changeTags(entityIDs, changed, onInput) {\n\n var actions = [];\n for (var i in entityIDs) {\n var entityID = entityIDs[i];\n var entity = context.entity(entityID);\n\n var tags = Object.assign({}, entity.tags); // shallow copy\n\n if (typeof changed === 'function') {\n // a complex callback tag change\n tags = changed(tags);\n } else {\n for (var k in changed) {\n if (!k) continue;\n var v = changed[k];\n if (typeof v === 'object') {\n // a \"key only\" tag change\n tags[k] = tags[v.oldKey];\n } else if (v !== undefined || tags.hasOwnProperty(k)) {\n tags[k] = v;\n }\n }\n }\n\n if (!onInput) {\n tags = utilCleanTags(tags);\n }\n\n if (!deepEqual(entity.tags, tags)) {\n actions.push(actionChangeTags(entityID, tags));\n }\n }\n\n if (actions.length) {\n var combinedAction = function(graph) {\n actions.forEach(function(action) {\n graph = action(graph);\n });\n return graph;\n };\n\n var annotation = t('operations.change_tags.annotation');\n\n if (_coalesceChanges) {\n context.replace(combinedAction, annotation);\n } else {\n context.perform(combinedAction, annotation);\n }\n _coalesceChanges = !!onInput;\n }\n\n // if leaving field (blur event), rerun validation\n if (!onInput) {\n context.validator().validate();\n }\n }\n\n function revertTags(keys) {\n\n var actions = [];\n for (var i in _entityIDs) {\n var entityID = _entityIDs[i];\n\n var original = context.graph().base().entities[entityID];\n var changed = {};\n for (var j in keys) {\n var key = keys[j];\n changed[key] = original ? original.tags[key] : undefined;\n }\n\n var entity = context.entity(entityID);\n var tags = Object.assign({}, entity.tags); // shallow copy\n\n for (var k in changed) {\n if (!k) continue;\n var v = changed[k];\n if (v !== undefined || tags.hasOwnProperty(k)) {\n tags[k] = v;\n }\n }\n\n\n tags = utilCleanTags(tags);\n\n if (!deepEqual(entity.tags, tags)) {\n actions.push(actionChangeTags(entityID, tags));\n }\n\n }\n\n if (actions.length) {\n var combinedAction = function(graph) {\n actions.forEach(function(action) {\n graph = action(graph);\n });\n return graph;\n };\n\n var annotation = t('operations.change_tags.annotation');\n\n if (_coalesceChanges) {\n context.replace(combinedAction, annotation);\n } else {\n context.perform(combinedAction, annotation);\n }\n }\n\n context.validator().validate();\n }\n\n\n entityEditor.modified = function(val) {\n if (!arguments.length) return _modified;\n _modified = val;\n return entityEditor;\n };\n\n\n entityEditor.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n return entityEditor;\n };\n\n\n entityEditor.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n\n // always reload these even if the entityIDs are unchanged, since we\n // could be reselecting after something like dragging a node\n _base = context.graph();\n _coalesceChanges = false;\n\n if (val && _entityIDs && utilArrayIdentical(_entityIDs, val)) return entityEditor; // exit early if no change\n\n _entityIDs = val;\n\n loadActivePresets(true);\n\n return entityEditor\n .modified(false);\n };\n\n\n entityEditor.newFeature = function(val) {\n if (!arguments.length) return _newFeature;\n _newFeature = val;\n return entityEditor;\n };\n\n\n function loadActivePresets(isForNewSelection) {\n\n var graph = context.graph();\n\n var counts = {};\n\n for (var i in _entityIDs) {\n var entity = graph.hasEntity(_entityIDs[i]);\n if (!entity) return;\n\n var match = presetManager.match(entity, graph);\n\n if (!counts[match.id]) counts[match.id] = 0;\n counts[match.id] += 1;\n }\n\n var matches = Object.keys(counts).sort(function(p1, p2) {\n return counts[p2] - counts[p1];\n }).map(function(pID) {\n return presetManager.item(pID);\n });\n\n if (!isForNewSelection) {\n // A \"weak\" preset doesn't set any tags. (e.g. \"Address\")\n var weakPreset = _activePresets.length === 1 &&\n !_activePresets[0].isFallback() &&\n Object.keys(_activePresets[0].addTags || {}).length === 0;\n // Don't replace a weak preset with a fallback preset (e.g. \"Point\")\n if (weakPreset && matches.length === 1 && matches[0].isFallback()) return;\n }\n\n entityEditor.presets(matches);\n }\n\n entityEditor.presets = function(val) {\n if (!arguments.length) return _activePresets;\n\n // don't reload the same preset\n if (!utilArrayIdentical(val, _activePresets)) {\n _activePresets = val;\n }\n return entityEditor;\n };\n\n return utilRebind(entityEditor, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { debounce } from 'es-toolkit/compat';\n\nimport { presetManager } from '../presets';\nimport { t, localizer } from '../core/localizer';\nimport { actionChangePreset } from '../actions/change_preset';\nimport { operationDelete } from '../operations/delete';\nimport { svgIcon } from '../svg/index';\nimport { uiTooltip } from './tooltip';\nimport { geoExtent } from '../geo/extent';\nimport { uiPresetIcon } from './preset_icon';\nimport { uiTagReference } from './tag_reference';\nimport { utilKeybinding, utilNoAuto, utilRebind } from '../util';\n\n\nexport function uiPresetList(context) {\n var dispatch = d3_dispatch('cancel', 'choose');\n var _entityIDs;\n var _currLoc;\n var _currentPresets;\n var _autofocus = false;\n\n\n function presetList(selection) {\n if (!_entityIDs) return;\n\n var presets = presetManager.matchAllGeometry(entityGeometries());\n\n selection.html('');\n\n var messagewrap = selection\n .append('div')\n .attr('class', 'header fillL');\n\n var message = messagewrap\n .append('h2')\n .call(t.addOrUpdate('inspector.choose'));\n\n messagewrap\n .append('button')\n .attr('class', 'preset-choose')\n .attr('title', _entityIDs.length === 1 ? t('inspector.edit') : t('inspector.edit_features'))\n .on('click', function() { dispatch.call('cancel', this); })\n .call(svgIcon('#iD-icon-close'));\n\n function initialKeydown(d3_event) {\n // hack to let delete shortcut work when search is autofocused\n if (search.property('value').length === 0 &&\n (d3_event.keyCode === utilKeybinding.keyCodes['\u232B'] ||\n d3_event.keyCode === utilKeybinding.keyCodes['\u2326'])) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n operationDelete(context, _entityIDs)();\n\n // hack to let undo work when search is autofocused\n } else if (search.property('value').length === 0 &&\n (d3_event.ctrlKey || d3_event.metaKey) &&\n d3_event.keyCode === utilKeybinding.keyCodes.z) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n context.undo();\n } else if (!d3_event.ctrlKey && !d3_event.metaKey) {\n // don't check for delete/undo hack on future keydown events\n d3_select(this).on('keydown', keydown);\n keydown.call(this, d3_event);\n }\n }\n\n function keydown(d3_event) {\n // down arrow\n if (d3_event.keyCode === utilKeybinding.keyCodes['\u2193'] &&\n // if insertion point is at the end of the string\n search.node().selectionStart === search.property('value').length) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // move focus to the first item in the preset list\n var buttons = list.selectAll('.preset-list-button');\n if (!buttons.empty()) buttons.nodes()[0].focus();\n }\n }\n\n function keypress(d3_event) {\n // enter\n var value = search.property('value');\n if (d3_event.keyCode === 13 && // \u21A9 Return\n value.length) {\n list.selectAll('.preset-list-item:first-child')\n .each(function(d) { d.choose.call(this); });\n }\n }\n\n function inputevent() {\n var value = search.property('value');\n list.classed('filtered', value.length);\n\n var results, messageText;\n if (value.length) {\n results = presets.search(value, entityGeometries()[0], _currLoc);\n messageText = t.addOrUpdate('inspector.results', {\n n: results.collection.length,\n search: value\n });\n } else {\n var entityPresets = _entityIDs.map(entityID =>\n presetManager.match(context.graph().entity(entityID), context.graph()));\n results = presetManager.defaults(entityGeometries()[0], 36, !context.inIntro(), _currLoc, entityPresets);\n messageText = t.addOrUpdate('inspector.choose');\n }\n list.call(drawList, results);\n message.call(messageText);\n }\n\n var searchWrap = selection\n .append('div')\n .attr('class', 'search-header');\n\n searchWrap\n .call(svgIcon('#iD-icon-search', 'pre-text'));\n\n var search = searchWrap\n .append('input')\n .attr('class', 'preset-search-input')\n .attr('placeholder', t('inspector.search_feature_type'))\n .attr('type', 'search')\n .call(utilNoAuto)\n .on('keydown', initialKeydown)\n .on('keypress', keypress)\n .on('input', debounce(inputevent));\n\n if (_autofocus) {\n search.node().focus();\n\n // Safari 14 doesn't always like to focus immediately,\n // so try again on the next pass\n setTimeout(function() {\n search.node().focus();\n }, 0);\n }\n\n var listWrap = selection\n .append('div')\n .attr('class', 'inspector-body');\n\n var entityPresets = _entityIDs.map(entityID =>\n presetManager.match(context.graph().entity(entityID), context.graph()));\n var list = listWrap\n .append('div')\n .attr('class', 'preset-list')\n .call(drawList, presetManager.defaults(entityGeometries()[0], 36, !context.inIntro(), _currLoc, entityPresets));\n\n context.features().on('change.preset-list', updateForFeatureHiddenState);\n }\n\n\n function drawList(list, presets) {\n presets = presets.matchAllGeometry(entityGeometries());\n var collection = presets.collection.reduce(function(collection, preset) {\n if (!preset) return collection;\n\n if (preset.members) {\n if (preset.members.collection.filter(function(preset) {\n return preset.addable();\n }).length > 1) {\n collection.push(CategoryItem(preset));\n }\n } else if (preset.addable()) {\n collection.push(PresetItem(preset));\n }\n return collection;\n }, []);\n\n var items = list.selectChildren('.preset-list-item')\n .data(collection, function(d) { return d.preset.id; });\n\n items.order();\n\n items.exit()\n .remove();\n\n items.enter()\n .append('div')\n .attr('class', function(item) { return 'preset-list-item preset-' + item.preset.id.replaceAll('/', '-'); })\n .classed('current', function(item) { return _currentPresets.indexOf(item.preset) !== -1; })\n .each(function(item) { d3_select(this).call(item); })\n .style('opacity', 0)\n .transition()\n .style('opacity', 1);\n\n updateForFeatureHiddenState();\n }\n\n function itemKeydown(d3_event) {\n // the actively focused item\n var item = d3_select(this.closest('.preset-list-item'));\n var parentItem = d3_select(item.node().parentNode.closest('.preset-list-item'));\n\n // arrow down, move focus to the next, lower item\n if (d3_event.keyCode === utilKeybinding.keyCodes['\u2193']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // the next item in the list at the same level\n var nextItem = d3_select(item.node().nextElementSibling);\n // if there is no next item in this list\n if (nextItem.empty()) {\n // if there is a parent item\n if (!parentItem.empty()) {\n // the item is the last item of a sublist,\n // select the next item at the parent level\n nextItem = d3_select(parentItem.node().nextElementSibling);\n }\n // if the focused item is expanded\n } else if (d3_select(this).classed('expanded')) {\n // select the first subitem instead\n nextItem = item.select('.subgrid .preset-list-item:first-child');\n }\n if (!nextItem.empty()) {\n // focus on the next item\n nextItem.select('.preset-list-button').node().focus();\n }\n\n // arrow up, move focus to the previous, higher item\n } else if (d3_event.keyCode === utilKeybinding.keyCodes['\u2191']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // the previous item in the list at the same level\n var previousItem = d3_select(item.node().previousElementSibling);\n\n // if there is no previous item in this list\n if (previousItem.empty()) {\n // if there is a parent item\n if (!parentItem.empty()) {\n // the item is the first subitem of a sublist select the parent item\n previousItem = parentItem;\n }\n // if the previous item is expanded\n } else if (previousItem.select('.preset-list-button').classed('expanded')) {\n // select the last subitem of the sublist of the previous item\n previousItem = previousItem.select('.subgrid .preset-list-item:last-child');\n }\n\n if (!previousItem.empty()) {\n // focus on the previous item\n previousItem.select('.preset-list-button').node().focus();\n } else {\n // the focus is at the top of the list, move focus back to the search field\n var search = d3_select(this.closest('.preset-list-pane')).select('.preset-search-input');\n search.node().focus();\n }\n\n // arrow left, move focus to the parent item if there is one\n } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2192' : '\u2190']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // if there is a parent item, focus on the parent item\n if (!parentItem.empty()) {\n parentItem.select('.preset-list-button').node().focus();\n }\n\n // arrow right, choose this item\n } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2190' : '\u2192']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n item.datum().choose.call(d3_select(this).node());\n }\n }\n\n\n function CategoryItem(preset) {\n var box, sublist, shown = false;\n\n function item(selection) {\n var wrap = selection.append('div')\n .attr('class', 'preset-list-button-wrap category');\n\n function click() {\n var isExpanded = d3_select(this).classed('expanded');\n var iconName = isExpanded ?\n (localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward') : '#iD-icon-down';\n d3_select(this)\n .classed('expanded', !isExpanded)\n .attr('title', !isExpanded ? t('icons.collapse') : t('icons.expand'));\n d3_select(this).selectAll('div.label-inner svg.icon use')\n .attr('href', iconName);\n item.choose();\n }\n\n var geometries = entityGeometries();\n\n var button = wrap\n .append('button')\n .attr('class', 'preset-list-button')\n .attr('title', t('icons.expand'))\n .classed('expanded', false)\n .call(uiPresetIcon()\n .geometry(geometries.length === 1 && geometries[0])\n .preset(preset))\n .on('click', click)\n .on('keydown', function(d3_event) {\n // right arrow, expand the focused item\n if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2190' : '\u2192']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // if the item isn't expanded\n if (!d3_select(this).classed('expanded')) {\n // toggle expansion (expand the item)\n click.call(this, d3_event);\n }\n // left arrow, collapse the focused item\n } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2192' : '\u2190']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // if the item is expanded\n if (d3_select(this).classed('expanded')) {\n // toggle expansion (collapse the item)\n click.call(this, d3_event);\n }\n } else {\n itemKeydown.call(this, d3_event);\n }\n });\n\n var label = button\n .append('div')\n .attr('class', 'label')\n .append('div')\n .attr('class', 'label-inner');\n\n label\n .append('div')\n .attr('class', 'namepart')\n .call(svgIcon((localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'))\n .append('span')\n .call(preset.nameLabel())\n .append('span').text('\u2026');\n\n box = selection.append('div')\n .attr('class', 'subgrid')\n .style('max-height', '0px')\n .style('opacity', 0);\n\n box.append('div')\n .attr('class', 'arrow');\n\n sublist = box.append('div')\n .attr('class', 'preset-list fillL3');\n }\n\n\n item.choose = function() {\n if (!box || !sublist) return;\n\n if (shown) {\n shown = false;\n box.transition()\n .duration(200)\n .style('opacity', '0')\n .style('max-height', '0px')\n .style('padding-bottom', '0px');\n } else {\n shown = true;\n var members = preset.members.matchAllGeometry(entityGeometries());\n sublist.call(drawList, members);\n box.transition()\n .duration(200)\n .style('opacity', '1')\n .style('max-height', 200 + members.collection.length * 190 + 'px')\n .style('padding-bottom', '10px');\n }\n };\n\n item.preset = preset;\n return item;\n }\n\n\n function PresetItem(preset) {\n function item(selection) {\n var wrap = selection.append('div')\n .attr('class', 'preset-list-button-wrap');\n\n var geometries = entityGeometries();\n\n var button = wrap.append('button')\n .attr('class', 'preset-list-button')\n .call(uiPresetIcon()\n .geometry(geometries.length === 1 && geometries[0])\n .preset(preset))\n .on('click', item.choose)\n .on('keydown', itemKeydown);\n\n var label = button\n .append('div')\n .attr('class', 'label')\n .append('div')\n .attr('class', 'label-inner');\n\n var nameparts = [\n preset.nameLabel(),\n preset.subtitleLabel()\n ].filter(Boolean);\n\n label.selectAll('.namepart')\n .data(nameparts, d => d.stringId)\n .enter()\n .append('div')\n .attr('class', 'namepart')\n .text('')\n .each(function(d) { d(d3_select(this)); });\n\n wrap.call(item.reference.button);\n selection.call(item.reference.body);\n }\n\n item.choose = function() {\n if (d3_select(this).classed('disabled')) return;\n if (!context.inIntro()) {\n presetManager.setMostRecent(preset, entityGeometries()[0]);\n }\n context.perform(\n function(graph) {\n for (var i in _entityIDs) {\n var entityID = _entityIDs[i];\n var oldPreset = presetManager.match(graph.entity(entityID), graph);\n graph = actionChangePreset(entityID, oldPreset, preset)(graph);\n }\n return graph;\n },\n t('operations.change_tags.annotation')\n );\n\n context.validator().validate(); // rerun validation\n dispatch.call('choose', this, preset);\n };\n\n item.help = function(d3_event) {\n d3_event.stopPropagation();\n item.reference.toggle();\n };\n\n item.preset = preset;\n item.reference = uiTagReference(preset.reference(), context);\n\n return item;\n }\n\n\n function updateForFeatureHiddenState() {\n if (!_entityIDs.every(context.hasEntity)) return;\n\n var geometries = entityGeometries();\n var button = context.container().selectAll('.preset-list .preset-list-button');\n\n // remove existing tooltips\n button.call(uiTooltip().destroyAny);\n\n button.each(function(item, index) {\n var hiddenPresetFeaturesId;\n for (var i in geometries) {\n hiddenPresetFeaturesId = context.features().isHiddenPreset(item.preset, geometries[i]);\n if (hiddenPresetFeaturesId) break;\n }\n var isHiddenPreset = !context.inIntro() &&\n !!hiddenPresetFeaturesId &&\n (_currentPresets.length !== 1 || item.preset !== _currentPresets[0]);\n\n d3_select(this)\n .classed('disabled', isHiddenPreset);\n\n if (isHiddenPreset) {\n var isAutoHidden = context.features().autoHidden(hiddenPresetFeaturesId);\n d3_select(this).call(uiTooltip()\n .title(() => t.append('inspector.hidden_preset.' + (isAutoHidden ? 'zoom' : 'manual'), {\n features: t('feature.' + hiddenPresetFeaturesId + '.description')\n }))\n .placement(index < 2 ? 'bottom' : 'top')\n );\n }\n });\n }\n\n presetList.autofocus = function(val) {\n if (!arguments.length) return _autofocus;\n _autofocus = val;\n return presetList;\n };\n\n presetList.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n\n _entityIDs = val;\n _currLoc = null;\n\n if (_entityIDs && _entityIDs.length) {\n // calculate current location\n const extent = _entityIDs.reduce(function(extent, entityID) {\n var entity = context.graph().entity(entityID);\n return extent.extend(entity.extent(context.graph()));\n }, geoExtent());\n _currLoc = extent.center();\n\n // match presets\n var presets = _entityIDs.map(function(entityID) {\n return presetManager.match(context.entity(entityID), context.graph());\n });\n presetList.presets(presets);\n }\n\n return presetList;\n };\n\n presetList.presets = function(val) {\n if (!arguments.length) return _currentPresets;\n _currentPresets = val;\n return presetList;\n };\n\n function entityGeometries() {\n\n var counts = {};\n\n for (var i in _entityIDs) {\n var entityID = _entityIDs[i];\n var entity = context.entity(entityID);\n var geometry = entity.geometry(context.graph());\n\n // Treat entities on addr:interpolation lines as points, not vertices (#3241)\n if (geometry === 'vertex' && entity.isOnAddressLine(context.graph())) {\n geometry = 'point';\n }\n\n if (!counts[geometry]) counts[geometry] = 0;\n counts[geometry] += 1;\n }\n\n return Object.keys(counts).sort(function(geom1, geom2) {\n return counts[geom2] - counts[geom1];\n });\n }\n\n return utilRebind(presetList, dispatch, 'on');\n}\n", "import { interpolate as d3_interpolate } from 'd3-interpolate';\nimport { select as d3_select } from 'd3-selection';\n\nimport { uiEntityEditor } from './entity_editor';\nimport { uiPresetList } from './preset_list';\nimport { uiViewOnOSM } from './view_on_osm';\n\n\nexport function uiInspector(context) {\n var presetList = uiPresetList(context);\n var entityEditor = uiEntityEditor(context);\n var wrap = d3_select(null),\n presetPane = d3_select(null),\n editorPane = d3_select(null);\n var _state = 'select';\n var _entityIDs;\n var _newFeature = false;\n\n\n function inspector(selection) {\n presetList\n .entityIDs(_entityIDs)\n .autofocus(_newFeature)\n .on('choose', inspector.setPreset)\n .on('cancel', function() {\n inspector.setPreset();\n });\n\n entityEditor\n .state(_state)\n .entityIDs(_entityIDs)\n .on('choose', inspector.showList);\n\n wrap = selection.selectAll('.panewrap')\n .data([0]);\n\n var enter = wrap.enter()\n .append('div')\n .attr('class', 'panewrap');\n\n enter\n .append('div')\n .attr('class', 'preset-list-pane pane');\n\n enter\n .append('div')\n .attr('class', 'entity-editor-pane pane');\n\n wrap = wrap.merge(enter);\n presetPane = wrap.selectAll('.preset-list-pane');\n editorPane = wrap.selectAll('.entity-editor-pane');\n\n function shouldDefaultToPresetList() {\n // always show the inspector on hover\n if (_state !== 'select') return false;\n\n // can only change preset on single selection\n if (_entityIDs.length !== 1) return false;\n\n var entityID = _entityIDs[0];\n var entity = context.hasEntity(entityID);\n if (!entity) return false;\n\n // default to inspector if there are already tags\n if (entity.hasNonGeometryTags()) return false;\n\n // prompt to select preset if feature is new and untagged\n if (_newFeature) return true;\n\n // all existing features except vertices should default to inspector\n if (entity.geometry(context.graph()) !== 'vertex') return false;\n\n // show vertex relations if any\n if (context.graph().parentRelations(entity).length) return false;\n\n // show vertex issues if there are any\n if (context.validator().getEntityIssues(entityID).length) return false;\n\n // show turn restriction editor for junction vertices\n if (entity.type === 'node' && entity.isHighwayIntersection(context.graph())) return false;\n\n // otherwise show preset list for uninteresting vertices\n return true;\n }\n\n if (shouldDefaultToPresetList()) {\n wrap.style('right', '-100%');\n editorPane.classed('hide', true);\n presetPane.classed('hide', false)\n .call(presetList);\n } else {\n wrap.style('right', '0%');\n presetPane.classed('hide', true);\n editorPane.classed('hide', false)\n .call(entityEditor);\n }\n\n var footer = selection.selectAll('.footer')\n .data([0]);\n\n footer = footer.enter()\n .append('div')\n .attr('class', 'footer')\n .merge(footer);\n\n footer\n .call(uiViewOnOSM(context)\n .what(context.hasEntity(_entityIDs.length === 1 && _entityIDs[0]))\n );\n }\n\n inspector.showList = function(presets) {\n\n presetPane.classed('hide', false);\n\n wrap.transition()\n .styleTween('right', function() {\n return d3_interpolate('0%', '-100%');\n })\n .on('end', function () {\n editorPane.classed('hide', true);\n });\n\n if (presets) {\n presetList.presets(presets);\n }\n\n presetPane\n .call(presetList.autofocus(true));\n };\n\n inspector.setPreset = function(preset) {\n\n // upon setting multipolygon, go to the area preset list instead of the editor\n if (preset && preset.id === 'type/multipolygon') {\n presetPane\n .call(presetList.autofocus(true));\n\n } else {\n editorPane.classed('hide', false);\n wrap.transition()\n .styleTween('right', function() {\n return d3_interpolate('-100%', '0%');\n })\n .on('end', function () {\n presetPane.classed('hide', true);\n });\n\n if (preset) {\n entityEditor.presets([preset]);\n }\n editorPane\n .call(entityEditor);\n }\n\n };\n\n inspector.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n entityEditor.state(_state);\n\n // remove any old field help overlay that might have gotten attached to the inspector\n context.container().selectAll('.field-help-body').remove();\n\n return inspector;\n };\n\n\n inspector.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return inspector;\n };\n\n\n inspector.newFeature = function(val) {\n if (!arguments.length) return _newFeature;\n _newFeature = val;\n return inspector;\n };\n\n\n return inspector;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { utilArrayIdentical } from '../util/array';\nimport { utilFastMouse } from '../util';\nimport { osmEntity, osmNote, QAItem } from '../osm';\nimport { services } from '../services';\nimport { uiDataEditor } from './data_editor';\nimport { uiFeatureList } from './feature_list';\nimport { uiInspector } from './inspector';\nimport { uiOsmoseEditor } from './osmose_editor';\nimport { uiNoteEditor } from './note_editor';\nimport { localizer } from '../core/localizer';\n\n\nexport function uiSidebar(context) {\n var inspector = uiInspector(context);\n var dataEditor = uiDataEditor(context);\n var noteEditor = uiNoteEditor(context);\n var osmoseEditor = uiOsmoseEditor(context);\n var _current;\n var _wasData = false;\n var _wasNote = false;\n var _wasQaItem = false;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function sidebar(selection) {\n var container = context.container();\n var minWidth = 240;\n var sidebarWidth;\n var containerWidth;\n var dragOffset;\n\n // Set the initial width constraints\n selection\n .style('min-width', minWidth + 'px')\n .style('max-width', '400px')\n .style('width', '33.3333%');\n\n var resizer = selection\n .append('div')\n .attr('class', 'sidebar-resizer')\n .on(_pointerPrefix + 'down.sidebar-resizer', pointerdown);\n\n var downPointerId, lastClientX, containerLocGetter;\n\n function pointerdown(d3_event) {\n if (downPointerId) return;\n\n if ('button' in d3_event && d3_event.button !== 0) return;\n\n downPointerId = d3_event.pointerId || 'mouse';\n\n lastClientX = d3_event.clientX;\n\n containerLocGetter = utilFastMouse(container.node());\n\n // offset from edge of sidebar-resizer\n dragOffset = utilFastMouse(resizer.node())(d3_event)[0] - 1;\n\n sidebarWidth = selection.node().getBoundingClientRect().width;\n containerWidth = container.node().getBoundingClientRect().width;\n var widthPct = (sidebarWidth / containerWidth) * 100;\n selection\n .style('width', widthPct + '%') // lock in current width\n .style('max-width', '85%'); // but allow larger widths\n\n resizer.classed('dragging', true);\n\n d3_select(window)\n .on('touchmove.sidebar-resizer', function(d3_event) {\n // disable page scrolling while resizing on touch input\n d3_event.preventDefault();\n }, { passive: false })\n .on(_pointerPrefix + 'move.sidebar-resizer', pointermove)\n .on(_pointerPrefix + 'up.sidebar-resizer pointercancel.sidebar-resizer', pointerup);\n }\n\n function pointermove(d3_event) {\n\n if (downPointerId !== (d3_event.pointerId || 'mouse')) return;\n\n d3_event.preventDefault();\n\n var dx = d3_event.clientX - lastClientX;\n\n lastClientX = d3_event.clientX;\n\n var isRTL = (localizer.textDirection() === 'rtl');\n var scaleX = isRTL ? 0 : 1;\n var xMarginProperty = isRTL ? 'margin-right' : 'margin-left';\n\n var x = containerLocGetter(d3_event)[0] - dragOffset;\n sidebarWidth = isRTL ? containerWidth - x : x;\n\n var isCollapsed = selection.classed('collapsed');\n var shouldCollapse = sidebarWidth < minWidth;\n\n selection.classed('collapsed', shouldCollapse);\n\n if (shouldCollapse) {\n if (!isCollapsed) {\n selection\n .style(xMarginProperty, '-400px')\n .style('width', '400px');\n\n context.ui().onResize([(sidebarWidth - dx) * scaleX, 0]);\n }\n\n } else {\n var widthPct = (sidebarWidth / containerWidth) * 100;\n selection\n .style(xMarginProperty, null)\n .style('width', widthPct + '%');\n\n if (isCollapsed) {\n context.ui().onResize([-sidebarWidth * scaleX, 0]);\n } else {\n context.ui().onResize([-dx * scaleX, 0]);\n }\n }\n }\n\n function pointerup(d3_event) {\n if (downPointerId !== (d3_event.pointerId || 'mouse')) return;\n\n downPointerId = null;\n\n resizer.classed('dragging', false);\n\n d3_select(window)\n .on('touchmove.sidebar-resizer', null)\n .on(_pointerPrefix + 'move.sidebar-resizer', null)\n .on(_pointerPrefix + 'up.sidebar-resizer pointercancel.sidebar-resizer', null);\n }\n\n var featureListWrap = selection\n .append('div')\n .attr('class', 'feature-list-pane')\n .call(uiFeatureList(context));\n\n var inspectorWrap = selection\n .append('div')\n .attr('class', 'inspector-hidden inspector-wrap');\n\n var hoverModeSelect = function(targets) {\n context.container().selectAll('.feature-list-item button').classed('hover', false);\n\n if (context.selectedIDs().length > 1 &&\n targets && targets.length) {\n\n var elements = context.container().selectAll('.feature-list-item button')\n .filter(function (node) {\n return targets.indexOf(node) !== -1;\n });\n\n if (!elements.empty()) {\n elements.classed('hover', true);\n }\n }\n };\n\n sidebar.hoverModeSelect = throttle(hoverModeSelect, 200);\n\n function hover(targets) {\n var datum = targets && targets.length && targets[0];\n if (datum && datum.__featurehash__) { // hovering on data\n _wasData = true;\n sidebar\n .show(dataEditor.datum(datum));\n\n selection.selectAll('.sidebar-component')\n .classed('inspector-hover', true);\n\n } else if (datum instanceof osmNote) {\n if (context.mode().id === 'drag-note') return;\n _wasNote = true;\n\n var osm = services.osm;\n if (osm) {\n datum = osm.getNote(datum.id); // marker may contain stale data - get latest\n }\n\n sidebar\n .show(noteEditor.note(datum));\n\n selection.selectAll('.sidebar-component')\n .classed('inspector-hover', true);\n\n } else if (datum instanceof QAItem) {\n _wasQaItem = true;\n\n var errService = services[datum.service];\n if (errService) {\n // marker may contain stale data - get latest\n datum = errService.getError(datum.id);\n }\n\n // Currently only one possible service\n var errEditor;\n if (datum.service === 'osmose') {\n errEditor = osmoseEditor;\n }\n\n context.container().selectAll('.qaItem.' + datum.service)\n .classed('hover', function(d) { return d.id === datum.id; });\n\n sidebar\n .show(errEditor.error(datum));\n\n selection.selectAll('.sidebar-component')\n .classed('inspector-hover', true);\n\n } else if (!_current && (datum instanceof osmEntity)) {\n featureListWrap\n .classed('inspector-hidden', true);\n\n inspectorWrap\n .classed('inspector-hidden', false)\n .classed('inspector-hover', true);\n\n if (!inspector.entityIDs() || !utilArrayIdentical(inspector.entityIDs(), [datum.id]) || inspector.state() !== 'hover') {\n inspector\n .state('hover')\n .entityIDs([datum.id])\n .newFeature(false);\n\n inspectorWrap\n .call(inspector);\n }\n\n } else if (!_current) {\n featureListWrap\n .classed('inspector-hidden', false);\n inspectorWrap\n .classed('inspector-hidden', true);\n inspector\n .state('hide');\n\n } else if (_wasData || _wasNote || _wasQaItem) {\n _wasNote = false;\n _wasData = false;\n _wasQaItem = false;\n context.container().selectAll('.note').classed('hover', false);\n context.container().selectAll('.qaItem').classed('hover', false);\n sidebar.hide();\n }\n }\n\n sidebar.hover = throttle(hover, 200);\n\n\n sidebar.intersects = function(extent) {\n var rect = selection.node().getBoundingClientRect();\n return extent.intersects([\n context.projection.invert([0, rect.height]),\n context.projection.invert([rect.width, 0])\n ]);\n };\n\n\n sidebar.select = function(ids, newFeature) {\n sidebar.hide();\n\n if (ids && ids.length) {\n\n var entity = ids.length === 1 && context.entity(ids[0]);\n if (entity && newFeature && selection.classed('collapsed')) {\n // uncollapse the sidebar\n var extent = entity.extent(context.graph());\n sidebar.expand(sidebar.intersects(extent));\n }\n\n featureListWrap\n .classed('inspector-hidden', true);\n\n inspectorWrap\n .classed('inspector-hidden', false)\n .classed('inspector-hover', false);\n\n // reload the UI even if the ids are the same since the entities\n // themselves may have changed\n inspector\n .state('select')\n .entityIDs(ids)\n .newFeature(newFeature);\n\n inspectorWrap\n .call(inspector);\n\n } else {\n inspector\n .state('hide');\n }\n };\n\n\n sidebar.showPresetList = function() {\n inspector.showList();\n };\n\n\n sidebar.show = function(component, element) {\n featureListWrap\n .classed('inspector-hidden', true);\n inspectorWrap\n .classed('inspector-hidden', true);\n\n if (_current) _current.remove();\n _current = selection\n .append('div')\n .attr('class', 'sidebar-component')\n .call(component, element);\n };\n\n\n sidebar.hide = function() {\n featureListWrap\n .classed('inspector-hidden', false);\n inspectorWrap\n .classed('inspector-hidden', true);\n\n if (_current) _current.remove();\n _current = null;\n };\n\n\n sidebar.expand = function(moveMap) {\n if (selection.classed('collapsed')) {\n sidebar.toggle(moveMap);\n }\n };\n\n\n sidebar.collapse = function(moveMap) {\n if (!selection.classed('collapsed')) {\n sidebar.toggle(moveMap);\n }\n };\n\n\n sidebar.toggle = function(moveMap) {\n\n // Don't allow sidebar to toggle when the user is in the walkthrough.\n if (context.inIntro()) return;\n\n var isCollapsed = selection.classed('collapsed');\n var isCollapsing = !isCollapsed;\n var isRTL = (localizer.textDirection() === 'rtl');\n var scaleX = isRTL ? 0 : 1;\n var xMarginProperty = isRTL ? 'margin-right' : 'margin-left';\n\n sidebarWidth = selection.node().getBoundingClientRect().width;\n\n // switch from % to px\n selection.style('width', sidebarWidth + 'px');\n\n var startMargin, endMargin;\n if (isCollapsing) {\n startMargin = 0;\n endMargin = -sidebarWidth;\n } else {\n startMargin = -sidebarWidth;\n endMargin = 0;\n }\n let lastMargin = startMargin;\n\n if (!isCollapsing) {\n // unhide the sidebar's content before it transitions onscreen\n selection.classed('collapsed', isCollapsing);\n }\n\n selection\n .transition()\n .style(xMarginProperty, endMargin + 'px')\n .tween('panner', function() {\n var i = d3_interpolateNumber(startMargin, endMargin);\n return function(t) {\n var dx = lastMargin - Math.round(i(t));\n lastMargin = lastMargin - dx;\n context.ui().onResize(moveMap ? undefined : [dx * scaleX, 0]);\n };\n })\n .on('end', function() {\n if (isCollapsing) {\n // hide the sidebar's content after it transitions offscreen\n selection.classed('collapsed', isCollapsing);\n }\n\n // switch back from px to %\n if (!isCollapsing) {\n var containerWidth = container.node().getBoundingClientRect().width;\n var widthPct = (sidebarWidth / containerWidth) * 100;\n selection\n .style(xMarginProperty, null)\n .style('width', widthPct + '%');\n }\n });\n };\n\n // toggle the sidebar collapse when double-clicking the resizer\n resizer.on('dblclick', function(d3_event) {\n d3_event.preventDefault();\n if (d3_event.sourceEvent) {\n d3_event.sourceEvent.preventDefault();\n }\n sidebar.toggle();\n });\n\n // ensure hover sidebar is closed when zooming out beyond editable zoom\n context.map().on('crossEditableZoom.sidebar', function(within) {\n if (!within && !selection.select('.inspector-hover').empty()) {\n hover([]);\n }\n });\n }\n\n sidebar.showPresetList = function() {};\n sidebar.hover = function() {};\n sidebar.hover.cancel = function() {};\n sidebar.intersects = function() {};\n sidebar.select = function() {};\n sidebar.show = function() {};\n sidebar.hide = function() {};\n sidebar.expand = function() {};\n sidebar.collapse = function() {};\n sidebar.toggle = function() {};\n\n return sidebar;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { modeBrowse } from '../modes/browse';\n\n\nexport function uiSourceSwitch(context) {\n var keys;\n\n\n function click(d3_event) {\n d3_event.preventDefault();\n\n var osm = context.connection();\n if (!osm) return;\n\n if (context.inIntro()) return;\n\n if (context.history().hasChanges() &&\n !window.confirm(t('source_switch.lose_changes'))) return;\n\n var isLive = d3_select(this)\n .classed('live');\n\n isLive = !isLive;\n context.enter(modeBrowse(context));\n context.history().clearSaved(); // remove saved history\n context.flush(); // remove stored data\n\n d3_select(this)\n .classed('live', isLive)\n .classed('chip', isLive)\n .text('')\n .call(isLive ? t.append('source_switch.live') : t.append('source_switch.dev'));\n\n osm.switch(isLive ? keys[0] : keys[1]); // switch connection (warning: dispatches 'change' event)\n }\n\n var sourceSwitch = function(selection) {\n selection\n .append('a')\n .attr('href', '#')\n .call(t.append('source_switch.live'))\n .attr('class', 'live chip')\n .on('click', click);\n };\n\n\n sourceSwitch.keys = function(_) {\n if (!arguments.length) return keys;\n keys = _;\n return sourceSwitch;\n };\n\n\n return sourceSwitch;\n}\n", "export function uiSpinner(context) {\n var osm = context.connection();\n\n\n return function(selection) {\n var img = selection\n .append('img')\n .attr('src', context.imagePath('loader-black.gif'))\n .style('opacity', 0);\n\n if (osm) {\n osm\n .on('loading.spinner', function() {\n img.transition()\n .style('opacity', 1);\n })\n .on('loaded.spinner', function() {\n img.transition()\n .style('opacity', 0);\n });\n }\n };\n}\n", "import { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\n\nexport function uiSectionPrivacy(context) {\n let section = uiSection('preferences-third-party', context)\n .label(() => t.append('preferences.privacy.title'))\n .disclosureContent(renderDisclosureContent);\n\n function renderDisclosureContent(selection) {\n // enter\n selection.selectAll('.privacy-options-list')\n .data([0])\n .enter()\n .append('ul')\n .attr('class', 'layer-list privacy-options-list');\n\n let thirdPartyIconsEnter = selection.select('.privacy-options-list')\n .selectAll('.privacy-third-party-icons-item')\n .data([prefs('preferences.privacy.thirdpartyicons') || 'true'])\n .enter()\n .append('li')\n .attr('class', 'privacy-third-party-icons-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('preferences.privacy.third_party_icons.tooltip'))\n .placement('bottom')\n );\n\n thirdPartyIconsEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', (d3_event, d) => {\n d3_event.preventDefault();\n prefs('preferences.privacy.thirdpartyicons', d === 'true' ? 'false' : 'true');\n });\n\n thirdPartyIconsEnter\n .append('span')\n .call(t.append('preferences.privacy.third_party_icons.description'));\n\n // update\n selection.selectAll('.privacy-third-party-icons-item')\n .classed('active', d => d === 'true')\n .select('input')\n .property('checked', d => d === 'true');\n\n // Privacy Policy link\n selection.selectAll('.privacy-link')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'privacy-link')\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', 'https://github.com/openstreetmap/iD/blob/release/PRIVACY.md')\n .append('span')\n .call(t.append('preferences.privacy.privacy_link'));\n\n }\n\n prefs.onChange('preferences.privacy.thirdpartyicons', section.reRender);\n\n return section;\n}\n", "import { prefs } from '../core/preferences';\nimport { fileFetcher } from '../core/file_fetcher';\nimport { t } from '../core/localizer';\nimport { uiIntro } from './intro';\nimport { uiModal } from './modal';\nimport { uiSectionPrivacy } from './sections/privacy';\n\n\nexport function uiSplash(context) {\n return (selection) => {\n // Exception - if there are restorable changes, skip this splash screen.\n // This is because we currently only support one `uiModal` at a time\n // and we need to show them `uiRestore`` instead of this one.\n if (context.history().hasRestorableChanges()) return;\n\n // If user has not seen this version of the privacy policy, show the splash again.\n let updateMessage = '';\n const sawPrivacyVersion = prefs('sawPrivacyVersion');\n let showSplash = !prefs('sawSplash');\n if (sawPrivacyVersion && sawPrivacyVersion !== context.privacyVersion) {\n updateMessage = t('splash.privacy_update');\n showSplash = true;\n }\n\n if (!showSplash) return;\n\n prefs('sawSplash', true);\n prefs('sawPrivacyVersion', context.privacyVersion);\n\n // fetch intro graph data now, while user is looking at the splash screen\n fileFetcher.get('intro_graph');\n\n let modalSelection = uiModal(selection);\n\n modalSelection.select('.modal')\n .attr('class', 'modal-splash modal');\n\n let introModal = modalSelection.select('.content')\n .append('div')\n .attr('class', 'fillL');\n\n introModal\n .append('div')\n .attr('class','modal-section')\n .append('h3')\n .call(t.append('splash.welcome'));\n\n let modalSection = introModal\n .append('div')\n .attr('class','modal-section');\n\n modalSection\n .append('p')\n .call(t.addOrUpdate('splash.text', {\n version: context.version,\n website: selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/openstreetmap/iD/blob/develop/CHANGELOG.md#whats-new')\n .call(t.addOrUpdate('splash.changelog')),\n github: selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/openstreetmap/iD/issues')\n .text('github.com')\n }));\n\n modalSection\n .append('p')\n .call(t.addOrUpdate('splash.privacy', {\n updateMessage: updateMessage,\n privacyLink: selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/openstreetmap/iD/blob/release/PRIVACY.md')\n .call(t.addOrUpdate('splash.privacy_policy'))\n }));\n\n uiSectionPrivacy(context)\n .label(() => t.append('splash.privacy_settings'))\n .render(modalSection);\n\n let buttonWrap = introModal\n .append('div')\n .attr('class', 'modal-actions');\n\n let walkthrough = buttonWrap\n .append('button')\n .attr('class', 'walkthrough')\n .on('click', () => {\n context.container().call(uiIntro(context));\n modalSelection.close();\n });\n\n walkthrough\n .append('svg')\n .attr('class', 'logo logo-walkthrough')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n walkthrough\n .append('div')\n .call(t.append('splash.walkthrough'));\n\n let startEditing = buttonWrap\n .append('button')\n .attr('class', 'start-editing')\n .on('click', modalSelection.close);\n\n startEditing\n .append('svg')\n .attr('class', 'logo logo-features')\n .append('use')\n .attr('xlink:href', '#iD-logo-features');\n\n startEditing\n .append('div')\n .call(t.append('splash.start'));\n\n modalSelection.select('button.close')\n .attr('class','hide');\n };\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiStatus(context) {\n var osm = context.connection();\n\n\n return function(selection) {\n if (!osm) return;\n\n function update(err, apiStatus) {\n selection.html('');\n\n if (err) {\n if (apiStatus === 'connectionSwitched') {\n // if the connection was just switched, we can't rely on\n // the status (we're getting the status of the previous api)\n return;\n\n } else if (apiStatus === 'rateLimited') {\n if (!osm.authenticated()) {\n selection\n .call(t.append('osm_api_status.message.rateLimit'))\n .append('a')\n .attr('href', '#')\n .attr('class', 'api-status-login')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append('login'))\n .on('click.login', function(d3_event) {\n d3_event.preventDefault();\n osm.authenticate();\n });\n } else {\n selection.call(t.append('osm_api_status.message.rateLimited'));\n }\n } else {\n\n // don't allow retrying too rapidly\n var throttledRetry = throttle(function() {\n // try loading the visible tiles\n context.loadTiles(context.projection);\n // manually reload the status too in case all visible tiles were already loaded\n osm.reloadApiStatus();\n }, 2000);\n\n // eslint-disable-next-line no-warning-comments\n // TODO: nice messages for different error types\n selection\n .call(t.append('osm_api_status.message.error', { suffix: ' ' }))\n .append('a')\n .attr('href', '#')\n // let the user manually retry their connection directly\n .call(t.append('osm_api_status.retry'))\n .on('click.retry', function(d3_event) {\n d3_event.preventDefault();\n throttledRetry();\n });\n }\n\n } else if (apiStatus === 'readonly') {\n selection.call(t.append('osm_api_status.message.readonly'));\n } else if (apiStatus === 'offline') {\n selection.call(t.append('osm_api_status.message.offline'));\n }\n\n selection.attr('class', 'api-status ' + (err ? 'error' : apiStatus));\n }\n\n osm.on('apiStatusChange.uiStatus', update);\n\n context.history().on('storage_error', () => {\n selection.selectAll('span.local-storage-full').remove();\n selection\n .append('span')\n .attr('class', 'local-storage-full')\n .call(t.append('osm_api_status.message.local_storage_full'));\n selection.classed('error', true);\n });\n\n // reload the status periodically regardless of other factors\n window.setInterval(function() {\n osm.reloadApiStatus();\n }, 90000);\n\n // load the initial status in case no OSM data was loaded yet\n osm.reloadApiStatus();\n };\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport {\n modeAddArea,\n modeAddLine,\n modeAddPoint,\n modeBrowse\n} from '../../modes';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiTooltip } from '../tooltip';\n\nexport function uiToolDrawModes(context) {\n\n var tool = {\n id: 'old_modes',\n label: t.append('toolbar.add_feature')\n };\n\n var modes = [\n modeAddPoint(context, {\n title: t.append('modes.add_point.title'),\n button: 'point',\n description: t.append('modes.add_point.description'),\n preset: presetManager.item('point'),\n key: '1'\n }),\n modeAddLine(context, {\n title: t.append('modes.add_line.title'),\n button: 'line',\n description: t.append('modes.add_line.description'),\n preset: presetManager.item('line'),\n key: '2'\n }),\n modeAddArea(context, {\n title: t.append('modes.add_area.title'),\n button: 'area',\n description: t.append('modes.add_area.description'),\n preset: presetManager.item('area'),\n key: '3'\n })\n ];\n\n\n function enabled(\n // eslint-disable-next-line no-unused-vars\n _mode // parameter is currently not used, but might be at some point\n ) {\n return osmEditable();\n }\n\n function osmEditable() {\n return context.editable();\n }\n\n modes.forEach(function(mode) {\n context.keybinding().on(mode.key, function() {\n if (!enabled(mode)) return;\n\n if (mode.id === context.mode().id) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(mode);\n }\n });\n });\n\n tool.render = function(selection) {\n\n var wrap = selection\n .append('div')\n .attr('class', 'joined')\n .style('display', 'flex');\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n\n context.map()\n .on('move.modes', debouncedUpdate)\n .on('drawn.modes', debouncedUpdate);\n\n context\n .on('enter.modes', update);\n\n update();\n\n\n function update() {\n\n var buttons = wrap.selectAll('button.add-button')\n .data(modes, function(d) { return d.id; });\n\n // exit\n buttons.exit()\n .remove();\n\n // enter\n var buttonsEnter = buttons.enter()\n .append('button')\n .attr('class', function(d) { return d.id + ' add-button bar-button'; })\n .on('click.mode-buttons', function(d3_event, d) {\n if (!enabled(d)) return;\n\n // When drawing, ignore accidental clicks on mode buttons - #4042\n var currMode = context.mode().id;\n if (/^draw/.test(currMode)) return;\n\n if (d.id === currMode) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(d);\n }\n })\n .call(uiTooltip()\n .placement('bottom')\n .title(function(d) { return d.description; })\n .keys(function(d) { return [d.key]; })\n .scrollContainer(context.container().select('.top-toolbar'))\n );\n\n buttonsEnter\n .each(function(d) {\n d3_select(this)\n .call(svgIcon('#iD-icon-' + d.button));\n });\n\n buttonsEnter\n .append('span')\n .attr('class', 'label')\n .text('')\n .each(function(mode) { mode.title(d3_select(this)); });\n\n // if we are adding/removing the buttons, check if toolbar has overflowed\n if (buttons.enter().size() || buttons.exit().size()) {\n context.ui().checkOverflow('.top-toolbar', true);\n }\n\n // update\n buttons\n .merge(buttonsEnter)\n .attr('aria-disabled', function(d) { return !enabled(d); })\n .classed('disabled', function(d) { return !enabled(d); })\n .attr('aria-pressed', function(d) { return context.mode() && context.mode().button === d.button; })\n .classed('active', function(d) { return context.mode() && context.mode().button === d.button; });\n }\n };\n\n return tool;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport {\n modeAddNote,\n modeBrowse\n} from '../../modes';\n\nimport { t } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiTooltip } from '../tooltip';\n\nexport function uiToolNotes(context) {\n\n var tool = {\n id: 'notes',\n label: t.append('modes.add_note.label')\n };\n\n var mode = modeAddNote(context);\n\n function enabled() {\n return notesEnabled() && notesEditable();\n }\n\n function notesEnabled() {\n var noteLayer = context.layers().layer('notes');\n return noteLayer && noteLayer.enabled();\n }\n\n function notesEditable() {\n var mode = context.mode();\n return context.map().notesEditable() && mode && mode.id !== 'save';\n }\n\n context.keybinding().on(mode.key, function() {\n if (!enabled()) return;\n\n if (mode.id === context.mode().id) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(mode);\n }\n });\n\n tool.render = function(selection) {\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n\n context.map()\n .on('move.notes', debouncedUpdate)\n .on('drawn.notes', debouncedUpdate);\n\n context\n .on('enter.notes', update);\n\n update();\n\n\n function update() {\n var showNotes = notesEnabled();\n var data = showNotes ? [mode] : [];\n\n var buttons = selection.selectAll('button.add-button')\n .data(data, function(d) { return d.id; });\n\n // exit\n buttons.exit()\n .remove();\n\n // enter\n var buttonsEnter = buttons.enter()\n .append('button')\n .attr('class', function(d) { return d.id + ' add-button bar-button'; })\n .on('click.notes', function(d3_event, d) {\n if (!enabled()) return;\n\n // When drawing, ignore accidental clicks on mode buttons - #4042\n var currMode = context.mode().id;\n if (/^draw/.test(currMode)) return;\n\n if (d.id === currMode) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(d);\n }\n })\n .call(uiTooltip()\n .placement('bottom')\n .title(function(d) { return d.description; })\n .keys(function(d) { return [d.key]; })\n .scrollContainer(context.container().select('.top-toolbar'))\n );\n\n buttonsEnter\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(d.icon || '#iD-icon-' + d.button));\n });\n\n // if we are adding/removing the buttons, check if toolbar has overflowed\n if (buttons.enter().size() || buttons.exit().size()) {\n context.ui().checkOverflow('.top-toolbar', true);\n }\n\n // update\n buttons\n .merge(buttonsEnter)\n .classed('disabled', function() { return !enabled(); })\n .attr('aria-disabled', function() { return !enabled(); })\n .classed('active', function(d) { return context.mode() && context.mode().button === d.button; })\n .attr('aria-pressed', function(d) { return context.mode() && context.mode().button === d.button; });\n }\n };\n\n tool.uninstall = function() {\n context\n .on('enter.editor.notes', null)\n .on('exit.editor.notes', null)\n .on('enter.notes', null);\n\n context.map()\n .on('move.notes', null)\n .on('drawn.notes', null);\n };\n\n return tool;\n}\n", "import { interpolateRgb as d3_interpolateRgb } from 'd3-interpolate';\n\nimport { t } from '../../core/localizer';\nimport { modeSave } from '../../modes';\nimport { svgIcon } from '../../svg';\nimport { uiCmd } from '../cmd';\nimport { uiTooltip } from '../tooltip';\n\n\nexport function uiToolSave(context) {\n\n var tool = {\n id: 'save',\n label: t.append('save.title')\n };\n\n var button = null;\n var tooltipBehavior = null;\n var history = context.history();\n var key = uiCmd('\u2318S');\n var _numChanges = 0;\n\n function isSaving() {\n var mode = context.mode();\n return mode && mode.id === 'save';\n }\n\n function isDisabled() {\n return _numChanges === 0 || isSaving();\n }\n\n function save(d3_event) {\n d3_event.preventDefault();\n if (!context.inIntro() && !isSaving() && history.hasChanges()) {\n context.enter(modeSave(context));\n }\n }\n\n function bgColor(numChanges) {\n var step;\n if (numChanges === 0) {\n return null;\n } else if (numChanges <= 50) {\n step = numChanges / 50;\n return d3_interpolateRgb('#fff0', '#ff08')(step); // transparent -> yellow\n } else {\n step = Math.min((numChanges - 50) / 50, 1.0);\n return d3_interpolateRgb('#ff08', '#f008')(step); // yellow -> red\n }\n }\n\n function updateCount() {\n var val = history.difference().summary().length;\n if (val === _numChanges) return;\n\n _numChanges = val;\n\n if (tooltipBehavior) {\n tooltipBehavior\n .title(() => t.append(_numChanges > 0 ? 'save.help' : 'save.no_changes'))\n .keys([key]);\n }\n\n if (button) {\n button\n .classed('disabled', isDisabled())\n .style('--accent-color', bgColor(_numChanges));\n\n button.select('span.count')\n .text(_numChanges);\n }\n }\n\n\n tool.render = function(selection) {\n tooltipBehavior = uiTooltip()\n .placement('bottom')\n .title(() => t.append('save.no_changes'))\n .keys([key])\n .scrollContainer(context.container().select('.top-toolbar'));\n\n var lastPointerUpType;\n\n button = selection\n .append('button')\n .attr('class', 'save disabled bar-button')\n .on('pointerup', function(d3_event) {\n lastPointerUpType = d3_event.pointerType;\n })\n .on('click', function(d3_event) {\n save(d3_event);\n\n if (_numChanges === 0 && (\n lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen')\n ) {\n // there are no tooltips for touch interactions so flash feedback instead\n context.ui().flash\n .duration(2000)\n .iconName('#iD-icon-save')\n .iconClass('disabled')\n .label(t.append('save.no_changes'))();\n }\n lastPointerUpType = null;\n })\n .call(tooltipBehavior);\n\n button\n .call(svgIcon('#iD-icon-save'));\n\n button\n .append('span')\n .attr('class', 'count')\n .attr('aria-hidden', 'true')\n .text('0');\n\n updateCount();\n\n\n context.keybinding()\n .on(key, save, true);\n\n\n context.history()\n .on('change.save', updateCount);\n\n context\n .on('enter.save', function() {\n if (button) {\n button\n .classed('disabled', isDisabled());\n\n if (isSaving()) {\n button.call(tooltipBehavior.hide);\n }\n }\n });\n };\n\n\n tool.uninstall = function() {\n context.keybinding()\n .off(key, true);\n\n context.history()\n .on('change.save', null);\n\n context\n .on('enter.save', null);\n\n button = null;\n tooltipBehavior = null;\n };\n\n return tool;\n}\n", "import { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiTooltip } from '../tooltip';\n\nexport function uiToolSidebarToggle(context) {\n\n var tool = {\n id: 'sidebar_toggle',\n label: t.append('toolbar.inspect')\n };\n\n tool.render = function(selection) {\n selection\n .append('button')\n .attr('class', 'bar-button')\n .attr('aria-label', t('sidebar.tooltip'))\n .on('click', function() {\n context.ui().sidebar.toggle();\n })\n .call(uiTooltip()\n .placement('bottom')\n .title(() => t.append('sidebar.tooltip'))\n .keys([t('sidebar.key')])\n .scrollContainer(context.container().select('.top-toolbar'))\n )\n .call(svgIcon('#iD-icon-sidebar-' + (localizer.textDirection() === 'rtl' ? 'right' : 'left')));\n };\n\n return tool;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiCmd } from '../cmd';\nimport { uiTooltip } from '../tooltip';\n\n\nexport function uiToolUndoRedo(context) {\n\n var tool = {\n id: 'undo_redo',\n label: t.append('toolbar.undo_redo')\n };\n\n var commands = [{\n id: 'undo',\n cmd: uiCmd('\u2318Z'),\n action: function() {\n context.undo();\n },\n annotation: function() {\n return context.history().undoAnnotation();\n },\n icon: 'iD-icon-' + (localizer.textDirection() === 'rtl' ? 'redo' : 'undo')\n }, {\n id: 'redo',\n cmd: uiCmd('\u2318\u21E7Z'),\n action: function() {\n context.redo();\n },\n annotation: function() {\n return context.history().redoAnnotation();\n },\n icon: 'iD-icon-' + (localizer.textDirection() === 'rtl' ? 'undo' : 'redo')\n }];\n\n\n function editable() {\n return context.mode() && context.mode().id !== 'save' && context.map().editableDataEnabled(true /* ignore min zoom */);\n }\n\n\n tool.render = function(selection) {\n var tooltipBehavior = uiTooltip()\n .placement('bottom')\n .title(function (d) {\n return d.annotation() ?\n t.append(d.id + '.tooltip', { action: d.annotation() }) :\n t.append(d.id + '.nothing');\n })\n .keys(function(d) {\n return [d.cmd];\n })\n .scrollContainer(context.container().select('.top-toolbar'));\n\n var lastPointerUpType;\n\n var buttons = selection.selectAll('button')\n .data(commands)\n .enter()\n .append('button')\n .attr('class', function(d) { return 'disabled ' + d.id + '-button bar-button'; })\n .on('pointerup', function(d3_event) {\n // `pointerup` is always called before `click`\n lastPointerUpType = d3_event.pointerType;\n })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n\n var annotation = d.annotation();\n\n if (editable() && annotation) {\n d.action();\n }\n\n if (editable() && (\n lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen')\n ) {\n // there are no tooltips for touch interactions so flash feedback instead\n\n var label = annotation ?\n t.append(d.id + '.tooltip', { action: annotation }) :\n t.append(d.id + '.nothing');\n context.ui().flash\n .duration(2000)\n .iconName('#' + d.icon)\n .iconClass(annotation ? '' : 'disabled')\n .label(label)();\n }\n lastPointerUpType = null;\n })\n .call(tooltipBehavior);\n\n buttons.each(function(d) {\n d3_select(this)\n .call(svgIcon('#' + d.icon));\n });\n\n context.keybinding()\n .on(commands[0].cmd, function(d3_event) {\n d3_event.preventDefault();\n if (editable()) commands[0].action();\n })\n .on(commands[1].cmd, function(d3_event) {\n d3_event.preventDefault();\n if (editable()) commands[1].action();\n });\n\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n\n context.map()\n .on('move.undo_redo', debouncedUpdate)\n .on('drawn.undo_redo', debouncedUpdate);\n\n context.history()\n .on('change.undo_redo', function(difference) {\n if (difference) update();\n });\n\n context\n .on('enter.undo_redo', update);\n\n\n function update() {\n buttons\n .classed('disabled', function(d) {\n return !editable() || !d.annotation();\n })\n .each(function() {\n var selection = d3_select(this);\n if (!selection.select('.tooltip.in').empty()) {\n selection.call(tooltipBehavior.updateContent);\n }\n });\n }\n };\n\n tool.uninstall = function() {\n context.keybinding()\n .off(commands[0].cmd)\n .off(commands[1].cmd);\n\n context.map()\n .on('move.undo_redo', null)\n .on('drawn.undo_redo', null);\n\n context.history()\n .on('change.undo_redo', null);\n\n context\n .on('enter.undo_redo', null);\n };\n\n return tool;\n}\n", "export * from './modes';\nexport * from './notes';\nexport * from './save';\nexport * from './sidebar_toggle';\nexport * from './undo_redo';\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { debounce } from 'es-toolkit/compat';\nimport { uiToolDrawModes, uiToolNotes, uiToolSave, uiToolSidebarToggle, uiToolUndoRedo } from './tools';\n\n\nexport function uiTopToolbar(context) {\n\n var sidebarToggle = uiToolSidebarToggle(context),\n modes = uiToolDrawModes(context),\n notes = uiToolNotes(context),\n undoRedo = uiToolUndoRedo(context),\n save = uiToolSave(context);\n\n function notesEnabled() {\n var noteLayer = context.layers().layer('notes');\n return noteLayer && noteLayer.enabled();\n }\n\n function topToolbar(bar) {\n\n bar.on('wheel.topToolbar', function(d3_event) {\n if (!d3_event.deltaX) {\n // translate vertical scrolling into horizontal scrolling in case\n // the user doesn't have an input device that can scroll horizontally\n bar.node().scrollLeft += d3_event.deltaY;\n }\n });\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n context.layers()\n .on('change.topToolbar', debouncedUpdate);\n\n update();\n\n function update() {\n\n var tools = [\n sidebarToggle,\n 'spacer',\n modes\n ];\n\n tools.push('spacer');\n\n if (notesEnabled()) {\n tools = tools.concat([notes, 'spacer']);\n }\n\n tools = tools.concat([undoRedo, save]);\n\n var toolbarItems = bar.selectAll('.toolbar-item')\n .data(tools, function(d) {\n return d.id || d;\n });\n\n toolbarItems.exit()\n .each(function(d) {\n if (d.uninstall) {\n d.uninstall();\n }\n })\n .remove();\n\n var itemsEnter = toolbarItems\n .enter()\n .append('div')\n .attr('class', function(d) {\n var classes = 'toolbar-item ' + (d.id || d).replaceAll('_', '-');\n if (d.klass) classes += ' ' + d.klass;\n return classes;\n });\n\n var actionableItems = itemsEnter.filter(function(d) { return d !== 'spacer'; });\n\n actionableItems\n .append('div')\n .attr('class', 'item-content')\n .each(function(d) {\n d3_select(this).call(d.render, bar);\n });\n\n actionableItems\n .append('div')\n .attr('class', 'item-label')\n .each(function(d) { d.label(d3_select(this)); });\n }\n\n }\n\n return topToolbar;\n}\n", "import { prefs } from '../core/preferences';\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiTooltip } from './tooltip';\n\n\n// these are module variables so they are preserved through a ui.restart()\nvar sawVersion = null;\nvar isNewVersion = false;\nvar isNewUser = false;\n\n\nexport function uiVersion(context) {\n\n var currVersion = context.version;\n var matchedVersion = currVersion.match(/\\d+\\.\\d+\\.\\d+.*/);\n\n if (sawVersion === null && matchedVersion !== null) {\n if (prefs('sawVersion')) {\n isNewUser = false;\n isNewVersion = prefs('sawVersion') !== currVersion && currVersion.indexOf('-') === -1;\n } else {\n isNewUser = true;\n isNewVersion = true;\n }\n prefs('sawVersion', currVersion);\n sawVersion = currVersion;\n }\n\n return function(selection) {\n selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/OpenHistoricalMap/iD')\n .text(currVersion);\n\n // only show new version indicator to users that have used iD before\n if (isNewVersion && !isNewUser) {\n selection\n .append('a')\n .attr('class', 'badge')\n .attr('target', '_blank')\n .attr('href', `https://github.com/OpenHistoricalMap/iD/releases/tag/v${currVersion}`)\n .call(svgIcon('#maki-gift'))\n .call(uiTooltip()\n .title(() => t.append('version.whats_new', { version: currVersion }))\n .placement('top')\n .scrollContainer(context.container().select('.main-footer-wrap'))\n );\n }\n };\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t, localizer } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\nimport { uiTooltip } from './tooltip';\nimport { utilKeybinding } from '../util/keybinding';\n\n\nexport function uiZoom(context) {\n\n var zooms = [{\n id: 'zoom-in',\n icon: 'iD-icon-plus',\n title: t.append('zoom.in'),\n action: zoomIn,\n disabled: function() {\n return !context.map().canZoomIn();\n },\n disabledTitle: t.append('zoom.disabled.in'),\n key: '+'\n }, {\n id: 'zoom-out',\n icon: 'iD-icon-minus',\n title: t.append('zoom.out'),\n action: zoomOut,\n disabled: function() {\n return !context.map().canZoomOut();\n },\n disabledTitle: t.append('zoom.disabled.out'),\n key: '-'\n }];\n\n function zoomIn(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomIn();\n }\n\n function zoomOut(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomOut();\n }\n\n function zoomInFurther(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomInFurther();\n }\n\n function zoomOutFurther(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomOutFurther();\n }\n\n return function(selection) {\n var tooltipBehavior = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(function(d) {\n if (d.disabled()) {\n return d.disabledTitle;\n }\n return d.title;\n })\n .keys(function(d) {\n return [d.key];\n });\n\n var lastPointerUpType;\n\n var buttons = selection.selectAll('button')\n .data(zooms)\n .enter()\n .append('button')\n .attr('class', function(d) { return d.id; })\n .on('pointerup.editor', function(d3_event) {\n lastPointerUpType = d3_event.pointerType;\n })\n .on('click.editor', function(d3_event, d) {\n if (!d.disabled()) {\n d.action(d3_event);\n } else if (lastPointerUpType === 'touch' || lastPointerUpType === 'pen') {\n context.ui().flash\n .duration(2000)\n .iconName('#' + d.icon)\n .iconClass('disabled')\n .label(d.disabledTitle)();\n }\n lastPointerUpType = null;\n })\n .call(tooltipBehavior);\n\n buttons.each(function(d) {\n d3_select(this)\n .call(svgIcon('#' + d.icon, 'light'));\n });\n\n utilKeybinding.plusKeys.forEach(function(key) {\n context.keybinding().on([key], zoomIn);\n context.keybinding().on([uiCmd('\u2325' + key)], zoomInFurther);\n });\n\n utilKeybinding.minusKeys.forEach(function(key) {\n context.keybinding().on([key], zoomOut);\n context.keybinding().on([uiCmd('\u2325' + key)], zoomOutFurther);\n });\n\n function updateButtonStates() {\n buttons\n .classed('disabled', function(d) {\n return d.disabled();\n })\n .each(function() {\n var selection = d3_select(this);\n if (!selection.select('.tooltip.in').empty()) {\n selection.call(tooltipBehavior.updateContent);\n }\n });\n }\n\n updateButtonStates();\n\n context.map().on('move.uiZoom', updateButtonStates);\n };\n}\n", "import { t, localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\nimport { svgIcon } from '../svg/icon';\n\nexport function uiZoomToSelection(context) {\n\n function isDisabled() {\n var mode = context.mode();\n return !mode || !mode.zoomToSelected;\n }\n\n var _lastPointerUpType;\n\n function pointerup(d3_event) {\n _lastPointerUpType = d3_event.pointerType;\n }\n\n function click(d3_event) {\n d3_event.preventDefault();\n\n if (isDisabled()) {\n if (_lastPointerUpType === 'touch' || _lastPointerUpType === 'pen') {\n context.ui().flash\n .duration(2000)\n .iconName('#iD-icon-framed-dot')\n .iconClass('disabled')\n .label(t.append('inspector.zoom_to.no_selection'))();\n }\n } else {\n var mode = context.mode();\n if (mode && mode.zoomToSelected) {\n mode.zoomToSelected();\n }\n }\n\n _lastPointerUpType = null;\n }\n\n return function(selection) {\n\n var tooltipBehavior = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(function() {\n if (isDisabled()) {\n return t.append('inspector.zoom_to.no_selection');\n }\n return t.append('inspector.zoom_to.title');\n })\n .keys([t('inspector.zoom_to.key')]);\n\n var button = selection\n .append('button')\n .on('pointerup', pointerup)\n .on('click', click)\n .call(svgIcon('#iD-icon-framed-dot', 'light'))\n .call(tooltipBehavior);\n\n function setEnabledState() {\n button.classed('disabled', isDisabled());\n if (!button.select('.tooltip.in').empty()) {\n button.call(tooltipBehavior.updateContent);\n }\n }\n\n context.on('enter.uiZoomToSelection', setEnabledState);\n\n setEnabledState();\n };\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { svgIcon } from '../svg/icon';\nimport { t, localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\n\n\nexport function uiPane(id, context) {\n\n var _key;\n var _label = '';\n var _description = '';\n var _iconName = '';\n var _sections; // array of uiSection objects\n\n var _paneSelection = d3_select(null);\n\n var _paneTooltip;\n\n var pane = {\n id: id\n };\n\n pane.label = function(val) {\n if (!arguments.length) return _label;\n _label = val;\n return pane;\n };\n\n pane.key = function(val) {\n if (!arguments.length) return _key;\n _key = val;\n return pane;\n };\n\n pane.description = function(val) {\n if (!arguments.length) return _description;\n _description = val;\n return pane;\n };\n\n pane.iconName = function(val) {\n if (!arguments.length) return _iconName;\n _iconName = val;\n return pane;\n };\n\n pane.sections = function(val) {\n if (!arguments.length) return _sections;\n _sections = val;\n return pane;\n };\n\n pane.selection = function() {\n return _paneSelection;\n };\n\n function hidePane() {\n context.ui().togglePanes();\n }\n\n pane.togglePane = function(d3_event) {\n if (d3_event) d3_event.preventDefault();\n _paneTooltip.hide();\n context.ui().togglePanes(!_paneSelection.classed('shown') ? _paneSelection : undefined);\n };\n\n pane.renderToggleButton = function(selection) {\n\n if (!_paneTooltip) {\n _paneTooltip = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(() => _description)\n .keys([_key]);\n }\n\n selection\n .append('button')\n .on('click', pane.togglePane)\n .call(svgIcon('#' + _iconName, 'light'))\n .call(_paneTooltip);\n };\n\n pane.renderContent = function(selection) {\n // override to fully customize content\n\n if (_sections) {\n _sections.forEach(function(section) {\n selection.call(section.render);\n });\n }\n };\n\n pane.renderPane = function(selection) {\n\n _paneSelection = selection\n .append('div')\n .attr('class', 'fillL map-pane hide ' + id + '-pane')\n .attr('pane', id);\n\n var heading = _paneSelection\n .append('div')\n .attr('class', 'pane-heading');\n\n heading\n .append('h2')\n .text('')\n .call(_label);\n\n heading\n .append('button')\n .attr('title', t('icons.close'))\n .on('click', hidePane)\n .call(svgIcon('#iD-icon-close'));\n\n\n _paneSelection\n .append('div')\n .attr('class', 'pane-content')\n .call(pane.renderContent);\n\n if (_key) {\n context.keybinding()\n .on(_key, pane.togglePane);\n }\n };\n\n return pane;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\nimport { clamp } from 'es-toolkit/compat';\n\nimport { prefs } from '../../core/preferences';\nimport { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\n\n\nexport function uiSectionBackgroundDisplayOptions(context) {\n\n var section = uiSection('background-display-options', context)\n .label(() => t.append('background.display_options'))\n .disclosureContent(renderDisclosureContent);\n\n var _storedOpacity = prefs('background-opacity');\n var _minVal = 0;\n var _maxVal = 3;\n\n var _sliders = ['brightness', 'contrast', 'saturation', 'sharpness'];\n\n var _options = {\n brightness: (_storedOpacity !== null ? (+_storedOpacity) : 1),\n contrast: 1,\n saturation: 1,\n sharpness: 1\n };\n\n function updateValue(d, val) {\n val = clamp(val, _minVal, _maxVal);\n\n _options[d] = val;\n context.background()[d](val);\n\n if (d === 'brightness') {\n prefs('background-opacity', val);\n }\n\n section.reRender();\n }\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.display-options-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'display-options-container controls-list');\n\n // add slider controls\n var slidersEnter = containerEnter.selectAll('.display-control')\n .data(_sliders)\n .enter()\n .append('label')\n .attr('class', function(d) { return 'display-control display-control-' + d; });\n\n slidersEnter\n .each(function(d) {\n d3_select(this).call(t.append('background.' + d));\n })\n .append('span')\n .attr('class', function(d) { return 'display-option-value display-option-value-' + d; });\n\n var sildersControlEnter = slidersEnter\n .append('div')\n .attr('class', 'control-wrap');\n\n sildersControlEnter\n .append('input')\n .attr('class', function(d) { return 'display-option-input display-option-input-' + d; })\n .attr('type', 'range')\n .attr('min', _minVal)\n .attr('max', _maxVal)\n .attr('step', '0.01')\n .on('input', function(d3_event, d) {\n var val = d3_select(this).property('value');\n if (!val && d3_event && d3_event.target) {\n val = d3_event.target.value;\n }\n updateValue(d, val);\n });\n\n sildersControlEnter\n .append('button')\n .attr('title', function(d) { return `${t('background.reset')} ${t('background.' + d)}`; })\n .attr('class', function(d) { return 'display-option-reset display-option-reset-' + d; })\n .on('click', function(d3_event, d) {\n if (d3_event.button !== 0) return;\n updateValue(d, 1);\n })\n .call(svgIcon('#iD-icon-' + (localizer.textDirection() === 'rtl' ? 'redo' : 'undo')));\n\n // reset all button\n containerEnter\n .append('a')\n .attr('class', 'display-option-resetlink')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('background.reset_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n for (var i = 0; i < _sliders.length; i++) {\n updateValue(_sliders[i], 1);\n }\n });\n\n // update\n container = containerEnter\n .merge(container);\n\n container.selectAll('.display-option-input')\n .property('value', function(d) { return _options[d]; });\n\n container.selectAll('.display-option-value')\n .text(function(d) { return Math.floor(_options[d] * 100) + '%'; });\n\n container.selectAll('.display-option-reset')\n .classed('disabled', function(d) { return _options[d] === 1; });\n\n // first time only, set brightness if needed\n if (containerEnter.size() && _options.brightness !== 1) {\n context.background().brightness(_options.brightness);\n }\n }\n\n return section;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { marked } from 'marked';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiConfirm } from '../confirm';\nimport { utilNoAuto, utilRebind } from '../../util';\n\n\nexport function uiSettingsCustomBackground() {\n var dispatch = d3_dispatch('change');\n\n function render(selection) {\n // keep separate copies of original and current settings\n var _origSettings = {\n template: prefs('background-custom-template')\n };\n var _currSettings = {\n template: prefs('background-custom-template')\n };\n\n var example = 'https://tile.openstreetmap.org/{zoom}/{x}/{y}.png';\n var modal = uiConfirm(selection).okButton();\n\n modal\n .classed('settings-modal settings-custom-background', true);\n\n modal.select('.modal-section.header')\n .append('h3')\n .call(t.append('settings.custom_background.header'));\n\n\n var textSection = modal.select('.modal-section.message-text');\n\n var instructions =\n `${t.html('settings.custom_background.instructions.info')}\\n` +\n '\\n' +\n `#### ${t.html('settings.custom_background.instructions.wms.tokens_label')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.proj')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.wkid')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.dimensions')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.bbox')}\\n` +\n '\\n' +\n `#### ${t.html('settings.custom_background.instructions.tms.tokens_label')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.xyz')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.flipped_y')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.switch')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.quadtile')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.scale_factor')}\\n` +\n '\\n' +\n `#### ${t.html('settings.custom_background.instructions.example')}\\n` +\n `\\`${example}\\``;\n\n textSection\n .append('div')\n .attr('class', 'instructions-template')\n .html(marked(instructions));\n\n textSection\n .append('textarea')\n .attr('class', 'field-template')\n .attr('placeholder', t('settings.custom_background.template.placeholder'))\n .call(utilNoAuto)\n .property('value', _currSettings.template);\n\n\n // insert a cancel button\n var buttonSection = modal.select('.modal-section.buttons');\n\n buttonSection\n .insert('button', '.ok-button')\n .attr('class', 'button cancel-button secondary-action')\n .call(t.append('confirm.cancel'));\n\n\n buttonSection.select('.cancel-button')\n .on('click.cancel', clickCancel);\n\n buttonSection.select('.ok-button')\n .attr('disabled', isSaveDisabled)\n .on('click.save', clickSave);\n\n\n function isSaveDisabled() {\n return null;\n }\n\n\n // restore the original template\n function clickCancel() {\n textSection.select('.field-template').property('value', _origSettings.template);\n prefs('background-custom-template', _origSettings.template);\n this.blur();\n modal.close();\n }\n\n // accept the current template\n function clickSave() {\n _currSettings.template = textSection.select('.field-template').property('value');\n prefs('background-custom-template', _currSettings.template);\n this.blur();\n modal.close();\n dispatch.call('change', this, _currSettings);\n }\n }\n\n return utilRebind(render, dispatch, 'on');\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport { descending as d3_descending, ascending as d3_ascending } from 'd3-array';\nimport { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t, localizer } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCmd } from '../cmd';\nimport { uiSettingsCustomBackground } from '../settings/custom_background';\nimport { uiMapInMap } from '../map_in_map';\nimport { uiSection } from '../section';\n\nexport function uiSectionBackgroundList(context) {\n\n var _backgroundList = d3_select(null);\n\n var _settingsCustomBackground = uiSettingsCustomBackground(context)\n .on('change', customChanged);\n\n var section = uiSection('background-list', context)\n .label(() => t.append('background.backgrounds'))\n .disclosureContent(renderDisclosureContent);\n\n function previousBackgroundID() {\n return prefs('background-last-used-toggle');\n }\n\n function renderDisclosureContent(selection) {\n\n // the background list\n var container = selection.selectAll('.layer-background-list')\n .data([0]);\n\n _backgroundList = container.enter()\n .append('ul')\n .attr('class', 'layer-list layer-background-list')\n .attr('dir', 'auto')\n .merge(container);\n\n\n // add minimap toggle below list\n var bgExtrasListEnter = selection.selectAll('.bg-extras-list')\n .data([0])\n .enter()\n .append('ul')\n .attr('class', 'layer-list bg-extras-list');\n\n var minimapLabelEnter = bgExtrasListEnter\n .append('li')\n .attr('class', 'minimap-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('background.minimap.tooltip'))\n .keys([t('background.minimap.key')])\n .placement('top')\n );\n\n minimapLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n uiMapInMap.toggle();\n });\n\n minimapLabelEnter\n .append('span')\n .call(t.append('background.minimap.description'));\n\n\n var panelLabelEnter = bgExtrasListEnter\n .append('li')\n .attr('class', 'background-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('background.panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.background.key'))])\n .placement('top')\n );\n\n panelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('background');\n });\n\n panelLabelEnter\n .append('span')\n .call(t.append('background.panel.description'));\n\n var locPanelLabelEnter = bgExtrasListEnter\n .append('li')\n .attr('class', 'location-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('background.location_panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.location.key'))])\n .placement('top')\n );\n\n locPanelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('location');\n });\n\n locPanelLabelEnter\n .append('span')\n .call(t.append('background.location_panel.description'));\n\n\n // \"Info / Report a Problem\" link\n selection.selectAll('.imagery-faq')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'imagery-faq')\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', 'https://github.com/openstreetmap/iD/blob/develop/FAQ.md#how-can-i-report-an-issue-with-background-imagery')\n .append('span')\n .call(t.append('background.imagery_problem_faq'));\n\n _backgroundList\n .call(drawListItems, 'radio', function(d3_event, d) {\n chooseBackground(d);\n }, function(d) {\n return !d.isHidden() && !d.overlay;\n });\n }\n\n function setTooltips(selection) {\n selection.each(function(d, i, nodes) {\n var item = d3_select(this).select('label');\n var span = item.select('span');\n var placement = (i < nodes.length / 2) ? 'bottom' : 'top';\n var hasDescription = d.hasDescription();\n var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth'));\n\n item.call(uiTooltip().destroyAny);\n\n if (d.id === previousBackgroundID()) {\n item.call(uiTooltip()\n .placement(placement)\n .title(() => t.append('background.switch'))\n .keys([uiCmd('\u2318' + t('background.key'))])\n );\n } else if (hasDescription || isOverflowing) {\n item.call(uiTooltip()\n .placement(placement)\n .title(() => hasDescription ? d.description() : d.label())\n );\n }\n });\n }\n\n function drawListItems(layerList, type, change, filter) {\n var sources = context.background()\n .sources(context.map().extent(), context.map().zoom(), true)\n .filter(filter)\n .sort(function(a, b) {\n return a.best() && !b.best() ? -1\n : b.best() && !a.best() ? 1\n : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0;\n });\n\n var layerLinks = layerList.selectAll('li')\n // We have to be a bit inefficient about reordering the list since\n // arrow key navigation of radio values likes to work in the order\n // they were added, not the display document order.\n .data(sources, function(d, i) { return d.id + '---' + i; });\n\n layerLinks.exit()\n .remove();\n\n var enter = layerLinks.enter()\n .append('li')\n .classed('layer-custom', function(d) { return d.id === 'custom'; })\n .classed('best', function(d) { return d.best(); });\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', 'background-layer')\n .attr('value', function(d) {\n return d.id;\n })\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) { d.label()(d3_select(this)); });\n\n enter.filter(function(d) { return d.id === 'custom'; })\n .append('button')\n .attr('class', 'layer-browse')\n .call(uiTooltip()\n .title(() => t.append('settings.custom_background.tooltip'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n editCustom();\n })\n .call(svgIcon('#iD-icon-more'));\n\n enter.filter(function(d) { return d.best(); })\n .append('div')\n .attr('class', 'best')\n .call(uiTooltip()\n .title(() => t.append('background.best_imagery'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .append('span')\n .text('\u2605');\n\n layerList\n .call(updateLayerSelections);\n }\n\n function updateLayerSelections(selection) {\n function active(d) {\n return context.background().showsLayer(d);\n }\n\n selection.selectAll('li')\n .classed('active', active)\n .classed('switch', function(d) { return d.id === previousBackgroundID(); })\n .call(setTooltips)\n .selectAll('input')\n .property('checked', active);\n }\n\n\n function chooseBackground(d) {\n if (d.id === 'custom' && !d.template()) {\n return editCustom();\n }\n\n var previousBackground = context.background().baseLayerSource();\n prefs('background-last-used-toggle', previousBackground.id);\n prefs('background-last-used', d.id);\n context.background().baseLayerSource(d);\n }\n\n\n function customChanged(d) {\n var background = context.background();\n var customSource = background.findSource('custom');\n if (!customSource) return;\n\n if (d && d.template) {\n customSource.template(d.template);\n chooseBackground(customSource);\n } else {\n customSource.template('');\n var noneSource = background.findSource('none');\n if (noneSource) {\n chooseBackground(noneSource);\n }\n }\n }\n\n\n function editCustom() {\n context.container()\n .call(_settingsCustomBackground);\n }\n\n\n context.background()\n .on('change.background_list', function() {\n _backgroundList.call(updateLayerSelections);\n });\n\n context.map()\n .on('move.background_list',\n debounce(function() {\n // layers in-view may have changed due to map move\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t, localizer } from '../../core/localizer';\nimport { geoMetersToOffset, geoOffsetToMeters } from '../../geo';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\n\n\nexport function uiSectionBackgroundOffset(context) {\n\n var section = uiSection('background-offset', context)\n .label(() => t.append('background.fix_misalignment'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n var _directions = [\n ['top', [0, -0.5]],\n ['left', [-0.5, 0]],\n ['right', [0.5, 0]],\n ['bottom', [0, 0.5]]\n ];\n\n\n function updateValue() {\n var meters = geoOffsetToMeters(context.background().offset());\n var x = +meters[0].toFixed(2);\n var y = +meters[1].toFixed(2);\n\n context.container().selectAll('.nudge-inner-rect')\n .select('input')\n .classed('error', false)\n .property('value', x + ', ' + y);\n\n context.container().selectAll('.nudge-reset')\n .classed('disabled', function() {\n return (x === 0 && y === 0);\n });\n }\n\n\n function resetOffset() {\n context.background().offset([0, 0]);\n updateValue();\n }\n\n\n function nudge(d) {\n context.background().nudge(d, context.map().zoom());\n updateValue();\n }\n\n\n function inputOffset() {\n var input = d3_select(this);\n var d = input.node().value;\n\n if (d === '') return resetOffset();\n\n d = d.replace(/;/g, ',').split(',').map(function(n) {\n // if n is NaN, it will always get mapped to false.\n return !isNaN(n) && n;\n });\n\n if (d.length !== 2 || !d[0] || !d[1]) {\n input.classed('error', true);\n return;\n }\n\n context.background().offset(geoMetersToOffset(d));\n updateValue();\n }\n\n\n function dragOffset(d3_event) {\n if (d3_event.button !== 0) return;\n\n var origin = [d3_event.clientX, d3_event.clientY];\n\n var pointerId = d3_event.pointerId || 'mouse';\n\n context.container()\n .append('div')\n .attr('class', 'nudge-surface');\n\n d3_select(window)\n .on(_pointerPrefix + 'move.drag-bg-offset', pointermove)\n .on(_pointerPrefix + 'up.drag-bg-offset', pointerup);\n\n if (_pointerPrefix === 'pointer') {\n d3_select(window)\n .on('pointercancel.drag-bg-offset', pointerup);\n }\n\n function pointermove(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n var latest = [d3_event.clientX, d3_event.clientY];\n var d = [\n -(origin[0] - latest[0]) / 4,\n -(origin[1] - latest[1]) / 4\n ];\n\n origin = latest;\n nudge(d);\n }\n\n function pointerup(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n if (d3_event.button !== 0) return;\n\n context.container().selectAll('.nudge-surface')\n .remove();\n\n d3_select(window)\n .on('.drag-bg-offset', null);\n }\n }\n\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.nudge-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'nudge-container');\n\n containerEnter\n .append('div')\n .attr('class', 'nudge-instructions')\n .call(t.append('background.offset'));\n\n var nudgeWrapEnter = containerEnter\n .append('div')\n .attr('class', 'nudge-controls-wrap');\n\n var nudgeEnter = nudgeWrapEnter\n .append('div')\n .attr('class', 'nudge-outer-rect')\n .on(_pointerPrefix + 'down', dragOffset);\n\n nudgeEnter\n .append('div')\n .attr('class', 'nudge-inner-rect')\n .append('input')\n .attr('type', 'text')\n .attr('aria-label', t('background.offset_label'))\n .on('change', inputOffset);\n\n nudgeWrapEnter\n .append('div')\n .selectAll('button')\n .data(_directions).enter()\n .append('button')\n .attr('title', function(d) { return t(`background.nudge.${d[0]}`); })\n .attr('class', function(d) { return d[0] + ' nudge'; })\n .on('click', function(d3_event, d) {\n nudge(d[1]);\n });\n\n nudgeWrapEnter\n .append('button')\n .attr('title', t('background.reset'))\n .attr('class', 'nudge-reset disabled')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n resetOffset();\n })\n .call(svgIcon('#iD-icon-' + (localizer.textDirection() === 'rtl' ? 'redo' : 'undo')));\n\n updateValue();\n }\n\n context.background()\n .on('change.backgroundOffset-update', updateValue);\n\n return section;\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport { descending as d3_descending, ascending as d3_ascending } from 'd3-array';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionOverlayList(context) {\n\n var section = uiSection('overlay-list', context)\n .label(() => t.append('background.overlays'))\n .disclosureContent(renderDisclosureContent);\n\n var _overlayList = d3_select(null);\n\n function setTooltips(selection) {\n selection.each(function(d, i, nodes) {\n var item = d3_select(this).select('label');\n var span = item.select('span');\n var placement = (i < nodes.length / 2) ? 'bottom' : 'top';\n var description = d.description();\n var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth'));\n\n item.call(uiTooltip().destroyAny);\n\n if (description || isOverflowing) {\n item.call(uiTooltip()\n .placement(placement)\n .title(() => description || d.name())\n );\n }\n });\n }\n\n function updateLayerSelections(selection) {\n function active(d) {\n return context.background().showsLayer(d);\n }\n\n selection.selectAll('li')\n .classed('active', active)\n .call(setTooltips)\n .selectAll('input')\n .property('checked', active);\n }\n\n\n function chooseOverlay(d3_event, d) {\n d3_event.preventDefault();\n context.background().toggleOverlayLayer(d);\n _overlayList.call(updateLayerSelections);\n document.activeElement.blur();\n }\n\n function drawListItems(layerList, type, change, filter) {\n var sources = context.background()\n .sources(context.map().extent(), context.map().zoom(), true)\n .filter(filter);\n\n var layerLinks = layerList.selectAll('li')\n .data(sources, function(d) { return d.name(); });\n\n layerLinks.exit()\n .remove();\n\n var enter = layerLinks.enter()\n .append('li');\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', 'layers')\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) { d.label()(d3_select(this)); });\n\n\n layerList.selectAll('li')\n .sort(sortSources);\n\n layerList\n .call(updateLayerSelections);\n\n\n function sortSources(a, b) {\n return a.best() && !b.best() ? -1\n : b.best() && !a.best() ? 1\n : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0;\n }\n }\n\n function renderDisclosureContent(selection) {\n\n var container = selection.selectAll('.layer-overlay-list')\n .data([0]);\n\n _overlayList = container.enter()\n .append('ul')\n .attr('class', 'layer-list layer-overlay-list')\n .attr('dir', 'auto')\n .merge(container);\n\n _overlayList\n .call(drawListItems, 'checkbox', chooseOverlay, function(d) { return !d.isHidden() && d.overlay; });\n }\n\n context.map()\n .on('move.overlay_list',\n debounce(function() {\n // layers in-view may have changed due to map move\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\n\nimport { uiSectionBackgroundDisplayOptions } from '../sections/background_display_options';\nimport { uiSectionBackgroundList } from '../sections/background_list';\nimport { uiSectionBackgroundOffset } from '../sections/background_offset';\nimport { uiSectionOverlayList } from '../sections/overlay_list';\n\nexport function uiPaneBackground(context) {\n\n var backgroundPane = uiPane('background', context)\n .key(t('background.key'))\n .label(t.append('background.title'))\n .description(t.append('background.description'))\n .iconName('iD-icon-layers')\n .sections([\n uiSectionBackgroundList(context),\n uiSectionOverlayList(context),\n uiSectionBackgroundDisplayOptions(context),\n uiSectionBackgroundOffset(context)\n ]);\n\n return backgroundPane;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { marked } from 'marked';\n\nimport { svgIcon } from '../../svg/icon';\nimport { uiIntro } from '../intro/intro';\nimport { uiPane } from '../pane';\n\nimport { t, localizer } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { helpHtml } from '../intro/helper';\n\nexport function uiPaneHelp(context) {\n\n var docKeys = [\n ['help', [\n 'welcome',\n 'open_data_h',\n 'open_data',\n 'before_start_h',\n 'before_start',\n 'open_source_h',\n 'open_source',\n 'open_source_attribution',\n 'open_source_help'\n ]],\n ['overview', [\n 'navigation_h',\n 'navigation_drag',\n 'navigation_zoom',\n 'features_h',\n 'features',\n 'nodes_ways'\n ]],\n ['editing', [\n 'select_h',\n 'select_left_click',\n 'select_right_click',\n 'select_space',\n 'multiselect_h',\n 'multiselect',\n 'multiselect_shift_click',\n 'multiselect_lasso',\n 'undo_redo_h',\n 'undo_redo',\n 'save_h',\n 'save',\n 'save_validation',\n 'upload_h',\n 'upload',\n 'backups_h',\n 'backups',\n 'keyboard_h',\n 'keyboard'\n ]],\n ['feature_editor', [\n 'intro',\n 'definitions',\n 'type_h',\n 'type',\n 'type_picker',\n 'fields_h',\n 'fields_all_fields',\n 'fields_example',\n 'fields_add_field',\n 'tags_h',\n 'tags_all_tags',\n 'tags_resources'\n ]],\n ['points', [\n 'intro',\n 'add_point_h',\n 'add_point',\n 'add_point_finish',\n 'move_point_h',\n 'move_point',\n 'delete_point_h',\n 'delete_point',\n 'delete_point_command'\n ]],\n ['lines', [\n 'intro',\n 'add_line_h',\n 'add_line',\n 'add_line_draw',\n 'add_line_continue',\n 'add_line_finish',\n 'modify_line_h',\n 'modify_line_dragnode',\n 'modify_line_addnode',\n 'connect_line_h',\n 'connect_line',\n 'connect_line_display',\n 'connect_line_drag',\n 'connect_line_tag',\n 'disconnect_line_h',\n 'disconnect_line_command',\n 'move_line_h',\n 'move_line_command',\n 'move_line_connected',\n 'delete_line_h',\n 'delete_line',\n 'delete_line_command'\n ]],\n ['areas', [\n 'intro',\n 'point_or_area_h',\n 'point_or_area',\n 'add_area_h',\n 'add_area_command',\n 'add_area_draw',\n 'add_area_continue',\n 'add_area_finish',\n 'square_area_h',\n 'square_area_command',\n 'modify_area_h',\n 'modify_area_dragnode',\n 'modify_area_addnode',\n 'delete_area_h',\n 'delete_area',\n 'delete_area_command'\n ]],\n ['relations', [\n 'intro',\n 'edit_relation_h',\n 'edit_relation',\n 'edit_relation_add',\n 'edit_relation_delete',\n 'maintain_relation_h',\n 'maintain_relation',\n 'relation_types_h',\n 'multipolygon_h',\n 'multipolygon',\n 'multipolygon_create',\n 'multipolygon_merge',\n 'turn_restriction_h',\n 'turn_restriction',\n 'turn_restriction_field',\n 'turn_restriction_editing',\n 'route_h',\n 'route',\n 'route_add',\n 'boundary_h',\n 'boundary',\n 'boundary_add'\n ]],\n ['operations', [\n 'intro',\n 'intro_2',\n 'straighten',\n 'orthogonalize',\n 'circularize',\n 'move',\n 'rotate',\n 'reflect',\n 'continue',\n 'reverse',\n 'disconnect',\n 'split',\n 'extract',\n 'merge',\n 'delete',\n 'downgrade',\n 'copy_paste'\n ]],\n ['notes', [\n 'intro',\n 'add_note_h',\n 'add_note',\n 'place_note',\n 'move_note',\n 'update_note_h',\n 'update_note',\n 'save_note_h',\n 'save_note'\n ]],\n ['imagery', [\n 'intro',\n 'sources_h',\n 'choosing',\n 'sources',\n 'offsets_h',\n 'offset',\n 'offset_change'\n ]],\n ['streetlevel', [\n 'intro',\n 'using_h',\n 'using',\n 'photos',\n 'viewer'\n ]],\n ['gps', [\n 'intro',\n 'survey',\n 'using_h',\n 'using',\n 'tracing',\n 'upload'\n ]],\n ['qa', [\n 'intro',\n 'tools_h',\n 'tools',\n 'issues_h',\n 'issues'\n ]]\n ];\n\n var headings = {\n 'help.areas.add_area_h': 3,\n 'help.areas.delete_area_h': 3,\n 'help.areas.modify_area_h': 3,\n 'help.areas.point_or_area_h': 3,\n 'help.areas.square_area_h': 3,\n 'help.editing.backups_h': 3,\n 'help.editing.keyboard_h': 3,\n 'help.editing.multiselect_h': 3,\n 'help.editing.save_h': 3,\n 'help.editing.select_h': 3,\n 'help.editing.undo_redo_h': 3,\n 'help.editing.upload_h': 3,\n 'help.feature_editor.fields_h': 3,\n 'help.feature_editor.tags_h': 3,\n 'help.feature_editor.type_h': 3,\n 'help.gps.using_h': 3,\n 'help.help.before_start_h': 3,\n 'help.help.open_data_h': 3,\n 'help.help.open_source_h': 3,\n 'help.imagery.offsets_h': 3,\n 'help.imagery.sources_h': 3,\n 'help.lines.add_line_h': 3,\n 'help.lines.connect_line_h': 3,\n 'help.lines.delete_line_h': 3,\n 'help.lines.disconnect_line_h': 3,\n 'help.lines.modify_line_h': 3,\n 'help.lines.move_line_h': 3,\n 'help.notes.add_note_h': 3,\n 'help.notes.save_note_h': 3,\n 'help.notes.update_note_h': 3,\n 'help.overview.features_h': 3,\n 'help.overview.navigation_h': 3,\n 'help.points.add_point_h': 3,\n 'help.points.delete_point_h': 3,\n 'help.points.move_point_h': 3,\n 'help.qa.issues_h': 3,\n 'help.qa.tools_h': 3,\n 'help.relations.boundary_h': 3,\n 'help.relations.edit_relation_h': 3,\n 'help.relations.maintain_relation_h': 3,\n 'help.relations.multipolygon_h': 3,\n 'help.relations.relation_types_h': 2,\n 'help.relations.route_h': 3,\n 'help.relations.turn_restriction_h': 3,\n 'help.streetlevel.using_h': 3\n };\n\n // For each section, squash all the texts into a single markdown document\n var docs = docKeys.map(function(key) {\n var helpkey = 'help.' + key[0];\n var helpPaneReplacements = { version: context.version };\n var text = key[1].reduce(function(all, part) {\n var subkey = helpkey + '.' + part;\n var depth = headings[subkey]; // is this subkey a heading?\n var hhh = depth ? Array(depth + 1).join('#') + ' ' : ''; // if so, prepend with some ##'s\n return all + hhh + helpHtml(subkey, helpPaneReplacements) + '\\n\\n';\n }, '');\n\n return {\n title: t.addOrUpdate(helpkey + '.title'),\n _title: t(helpkey + '.title'),\n content: marked(text.trim())\n // use keyboard key styling for shortcuts\n .replace(//g, '')\n .replace(/<\\/code>/g, '<\\/kbd>')\n };\n });\n\n var helpPane = uiPane('help', context)\n .key(t('help.key'))\n .label(t.append('help.title'))\n .description(t.append('help.title'))\n .iconName('iD-icon-help');\n\n helpPane.renderContent = function(content) {\n\n function clickHelp(d, i) {\n\n var rtl = (localizer.textDirection() === 'rtl');\n content.property('scrollTop', 0);\n helpPane.selection().select('.pane-heading h2').call(d.title);\n\n body.html(d.content);\n body.selectAll('a')\n .attr('target', '_blank');\n menuItems.classed('selected', function(m) {\n return m._title === d._title;\n });\n\n nav.text('');\n if (rtl) {\n nav.call(drawNext).call(drawPrevious);\n } else {\n nav.call(drawPrevious).call(drawNext);\n }\n\n\n function drawNext(selection) {\n if (i < docs.length - 1) {\n var nextLink = selection\n .append('a')\n .attr('href', '#')\n .attr('class', 'next')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n clickHelp(docs[i + 1], i + 1);\n });\n\n nextLink\n .append('span')\n .call(docs[i + 1].title)\n .call(svgIcon((rtl ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'));\n }\n }\n\n\n function drawPrevious(selection) {\n if (i > 0) {\n var prevLink = selection\n .append('a')\n .attr('href', '#')\n .attr('class', 'previous')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n clickHelp(docs[i - 1], i - 1);\n });\n\n prevLink\n .call(svgIcon((rtl ? '#iD-icon-forward' : '#iD-icon-backward'), 'inline'))\n .append('span')\n .call(docs[i - 1].title);\n }\n }\n }\n\n\n function clickWalkthrough(d3_event) {\n d3_event.preventDefault();\n if (context.inIntro()) return;\n context.container().call(uiIntro(context));\n context.ui().togglePanes();\n }\n\n\n function clickShortcuts(d3_event) {\n d3_event.preventDefault();\n context.container().call(context.ui().shortcuts, true);\n }\n\n var toc = content\n .append('ul')\n .attr('class', 'toc');\n\n var menuItems = toc.selectAll('li')\n .data(docs)\n .enter()\n .append('li')\n .append('a')\n .attr('role', 'button')\n .attr('href', '#')\n .each(function(d) {\n d3_select(this).call(d.title);\n })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n clickHelp(d, docs.indexOf(d));\n });\n\n var shortcuts = toc\n .append('li')\n .attr('class', 'shortcuts')\n .call(uiTooltip()\n .title(() => t.append('shortcuts.tooltip'))\n .keys(['?'])\n .placement('top')\n )\n .append('a')\n .attr('href', '#')\n .on('click', clickShortcuts);\n\n shortcuts\n .append('div')\n .call(t.append('shortcuts.title'));\n\n var walkthrough = toc\n .append('li')\n .attr('class', 'walkthrough')\n .append('a')\n .attr('href', '#')\n .on('click', clickWalkthrough);\n\n walkthrough\n .append('svg')\n .attr('class', 'logo logo-walkthrough')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n walkthrough\n .append('div')\n .call(t.append('splash.walkthrough'));\n\n\n var helpContent = content\n .append('div')\n .attr('class', 'left-content');\n\n var body = helpContent\n .append('div')\n .attr('class', 'body');\n\n var nav = helpContent\n .append('div')\n .attr('class', 'nav');\n\n clickHelp(docs[0], 0);\n };\n\n return helpPane;\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport {\n select as d3_select\n} from 'd3-selection';\n\n//import { actionNoop } from '../actions/noop';\nimport { geoSphericalDistance } from '../../geo';\nimport { svgIcon } from '../../svg/icon';\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { utilHighlightEntities } from '../../util';\nimport { uiSection } from '../section';\nimport { validationIssue } from '../../core/validation';\n\nexport function uiSectionValidationIssues(id, severity, context) {\n\n var _issues = [];\n\n var section = uiSection(id, context)\n .label(function() {\n if (!_issues) return '';\n var issueCountText = _issues.length > 1000 ? '1000+' : String(_issues.length);\n return t.append('inspector.title_count', { title: t.append('issues.' + severity + 's.list_title'), count: issueCountText });\n })\n .disclosureContent(renderDisclosureContent)\n .shouldDisplay(function() {\n return _issues && _issues.length;\n });\n\n function getOptions() {\n return {\n what: prefs('validate-what') || 'edited',\n where: prefs('validate-where') || 'all'\n };\n }\n\n // get and cache the issues to display, unordered\n function reloadIssues() {\n _issues = context.validator().getIssuesBySeverity(getOptions())[severity];\n }\n\n function renderDisclosureContent(selection) {\n\n var center = context.map().center();\n var graph = context.graph();\n\n // sort issues by distance away from the center of the map\n var issues = _issues.map(function withDistance(issue) {\n var extent = issue.extent(graph);\n var dist = extent ? geoSphericalDistance(center, extent.center()) : 0;\n return Object.assign(issue, { dist: dist });\n })\n .sort(function byDistance(a, b) {\n return a.dist - b.dist;\n });\n\n // cut off at 1000\n issues = issues.slice(0, 1000);\n\n //renderIgnoredIssuesReset(_warningsSelection);\n\n selection\n .call(drawIssuesList, issues);\n }\n\n function drawIssuesList(selection, issues) {\n var list = selection.selectAll('.issues-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'layer-list issues-list ' + severity + 's-list')\n .merge(list);\n\n\n var items = list.selectAll('li')\n .data(issues, function(d) { return d.key; });\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', function (d) { return 'issue severity-' + d.severity; });\n\n var labelsEnter = itemsEnter\n .append('button')\n .attr('class', 'issue-label')\n .on('click', function(d3_event, d) {\n context.validator().focusIssue(d);\n })\n .on('mouseover', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, true, context);\n })\n .on('mouseout', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, false, context);\n });\n\n var textEnter = labelsEnter\n .append('span')\n .attr('class', 'issue-text');\n\n textEnter\n .append('span')\n .attr('class', 'issue-icon')\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(validationIssue.ICONS[d.severity]));\n });\n\n textEnter\n .append('span')\n .attr('class', 'issue-message');\n\n // Update\n items = items\n .merge(itemsEnter)\n .order();\n\n items.selectAll('.issue-message')\n .text('')\n .each(function(d) {\n return d.message(context)(d3_select(this));\n });\n }\n\n context.validator().on('validated.uiSectionValidationIssues' + id, function() {\n window.requestIdleCallback(function() {\n reloadIssues();\n section.reRender();\n });\n });\n\n context.map().on('move.uiSectionValidationIssues' + id,\n debounce(function() {\n window.requestIdleCallback(function() {\n if (getOptions().where === 'visible') {\n // must refetch issues if they are viewport-dependent\n reloadIssues();\n }\n // always reload list to re-sort-by-distance\n section.reRender();\n });\n }, 1000)\n );\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiSection } from '../section';\n\nexport function uiSectionValidationOptions(context) {\n\n var section = uiSection('issues-options', context)\n .content(renderContent);\n\n function renderContent(selection) {\n\n var container = selection.selectAll('.issues-options-container')\n .data([0]);\n\n container = container.enter()\n .append('div')\n .attr('class', 'issues-options-container')\n .merge(container);\n\n var data = [\n { key: 'what', values: ['edited', 'all'] },\n { key: 'where', values: ['visible', 'all'] }\n ];\n\n var options = container.selectAll('.issues-option')\n .data(data, function(d) { return d.key; });\n\n var optionsEnter = options.enter()\n .append('div')\n .attr('class', function(d) { return 'issues-option issues-option-' + d.key; });\n\n optionsEnter\n .append('div')\n .attr('class', 'issues-option-title')\n .each(function(d) {\n d3_select(this).call(t.append('issues.options.' + d.key + '.title'));\n });\n\n var valuesEnter = optionsEnter.selectAll('label')\n .data(function(d) {\n return d.values.map(function(val) { return { value: val, key: d.key }; });\n })\n .enter()\n .append('label');\n\n valuesEnter\n .append('input')\n .attr('type', 'radio')\n .attr('name', function(d) { return 'issues-option-' + d.key; })\n .attr('value', function(d) { return d.value; })\n .property('checked', function(d) { return getOptions()[d.key] === d.value; })\n .on('change', function(d3_event, d) { updateOptionValue(d3_event, d.key, d.value); });\n\n valuesEnter\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append('issues.options.' + d.key + '.' + d.value));\n });\n }\n\n function getOptions() {\n return {\n what: prefs('validate-what') || 'edited', // 'all', 'edited'\n where: prefs('validate-where') || 'all' // 'all', 'visible'\n };\n }\n\n function updateOptionValue(d3_event, d, val) {\n if (!val && d3_event && d3_event.target) {\n val = d3_event.target.value;\n }\n\n prefs('validate-' + d, val);\n context.validator().validate();\n }\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { utilGetSetValue, utilNoAuto } from '../../util';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionValidationRules(context) {\n\n var MINSQUARE = 0;\n var MAXSQUARE = 20;\n var DEFAULTSQUARE = 5; // see also unsquare_way.js\n\n var section = uiSection('issues-rules', context)\n .disclosureContent(renderDisclosureContent)\n .label(() => t.append('issues.rules.title'));\n\n var _ruleKeys = context.validator().getRuleKeys()\n .filter(function(key) { return key !== 'maprules'; })\n .sort(function(key1, key2) {\n // alphabetize by localized title\n return t('issues.' + key1 + '.title') < t('issues.' + key2 + '.title') ? -1 : 1;\n });\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.issues-rulelist-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'issues-rulelist-container');\n\n containerEnter\n .append('ul')\n .attr('class', 'layer-list issue-rules-list');\n\n var ruleLinks = containerEnter\n .append('div')\n .attr('class', 'issue-rules-links section-footer');\n\n ruleLinks\n .append('a')\n .attr('class', 'issue-rules-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.disable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.validator().disableRules(_ruleKeys);\n });\n\n ruleLinks\n .append('a')\n .attr('class', 'issue-rules-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.enable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.validator().disableRules([]);\n });\n\n\n // Update\n container = container\n .merge(containerEnter);\n\n container.selectAll('.issue-rules-list')\n .call(drawListItems, _ruleKeys, 'checkbox', 'rule', toggleRule, isRuleEnabled);\n }\n\n function drawListItems(selection, data, type, name, change, active) {\n var items = selection.selectAll('li')\n .data(data);\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li');\n\n if (name === 'rule') {\n enter\n .call(uiTooltip()\n .title(function(d) { return t.append('issues.' + d + '.tip'); })\n .placement('top')\n );\n }\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', name)\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) {\n var params = {};\n if (d === 'unsquare_way') {\n params.val = selection => selection\n .append('span')\n .classed('square-degrees', true);\n }\n d3_select(this).call(t.append('issues.' + d + '.title', params));\n });\n\n // Update\n items = items\n .merge(enter);\n\n items\n .classed('active', active)\n .selectAll('input')\n .property('checked', active)\n .property('indeterminate', false);\n\n\n // user-configurable square threshold\n var degStr = prefs('validate-square-degrees');\n if (degStr === null) {\n degStr = DEFAULTSQUARE.toString();\n }\n\n var span = items.selectAll('.square-degrees');\n var input = span.selectAll('.square-degrees-input')\n .data([0]);\n\n // enter / update\n input.enter()\n .append('input')\n .attr('type', 'number')\n .attr('min', MINSQUARE.toString())\n .attr('max', MAXSQUARE.toString())\n .attr('step', '0.5')\n .attr('class', 'square-degrees-input')\n .call(utilNoAuto)\n .on('click', function (d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n this.select();\n })\n .on('keyup', function (d3_event) {\n if (d3_event.keyCode === 13) { // \u21A9 Return\n this.blur();\n this.select();\n }\n })\n .on('blur', changeSquare)\n .merge(input)\n .property('value', degStr);\n }\n\n function changeSquare() {\n var input = d3_select(this);\n var degStr = utilGetSetValue(input).trim();\n var degNum = Number(degStr);\n\n if (!isFinite(degNum)) {\n degNum = DEFAULTSQUARE;\n } else if (degNum > MAXSQUARE) {\n degNum = MAXSQUARE;\n } else if (degNum < MINSQUARE) {\n degNum = MINSQUARE;\n }\n\n degNum = Math.round(degNum * 10 ) / 10; // round to 1 decimal\n degStr = degNum.toString();\n\n input\n .property('value', degStr);\n\n prefs('validate-square-degrees', degStr);\n context.validator().revalidateUnsquare();\n }\n\n function isRuleEnabled(d) {\n return context.validator().isRuleEnabled(d);\n }\n\n function toggleRule(d3_event, d) {\n context.validator().toggleRule(d);\n }\n\n context.validator().on('validated.uiSectionValidationRules', function() {\n window.requestIdleCallback(section.reRender);\n });\n\n return section;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { svgIcon } from '../../svg/icon';\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiSection } from '../section';\n\nexport function uiSectionValidationStatus(context) {\n\n var section = uiSection('issues-status', context)\n .content(renderContent)\n .shouldDisplay(function() {\n var issues = context.validator().getIssues(getOptions());\n return issues.length === 0;\n });\n\n function getOptions() {\n return {\n what: prefs('validate-what') || 'edited',\n where: prefs('validate-where') || 'all'\n };\n }\n\n function renderContent(selection) {\n\n var box = selection.selectAll('.box')\n .data([0]);\n\n var boxEnter = box.enter()\n .append('div')\n .attr('class', 'box');\n\n boxEnter\n .append('div')\n .call(svgIcon('#iD-icon-apply', 'pre-text'));\n\n var noIssuesMessage = boxEnter\n .append('span');\n\n noIssuesMessage\n .append('strong')\n .attr('class', 'message');\n\n noIssuesMessage\n .append('br');\n\n noIssuesMessage\n .append('span')\n .attr('class', 'details');\n\n renderIgnoredIssuesReset(selection);\n setNoIssuesText(selection);\n }\n\n function renderIgnoredIssuesReset(selection) {\n\n var ignoredIssues = context.validator()\n .getIssues({ what: 'all', where: 'all', includeDisabledRules: true, includeIgnored: 'only' });\n\n var resetIgnored = selection.selectAll('.reset-ignored')\n .data(ignoredIssues.length ? [0] : []);\n\n // exit\n resetIgnored.exit()\n .remove();\n\n // enter\n var resetIgnoredEnter = resetIgnored.enter()\n .append('div')\n .attr('class', 'reset-ignored section-footer');\n\n resetIgnoredEnter\n .append('a')\n .attr('href', '#');\n\n // update\n resetIgnored = resetIgnored\n .merge(resetIgnoredEnter);\n\n resetIgnored.select('a')\n .call(t.addOrUpdate('inspector.title_count', { title: t.append('issues.reset_ignored'), count: ignoredIssues.length }));\n\n resetIgnored.on('click', function(d3_event) {\n d3_event.preventDefault();\n context.validator().resetIgnoredIssues();\n });\n }\n\n function setNoIssuesText(selection) {\n\n var opts = getOptions();\n\n function checkForHiddenIssues(cases) {\n for (var type in cases) {\n var hiddenOpts = cases[type];\n var hiddenIssues = context.validator().getIssues(hiddenOpts);\n if (hiddenIssues.length) {\n selection.select('.box .details')\n .html('')\n .call(t.append(\n 'issues.no_issues.hidden_issues.' + type,\n { count: hiddenIssues.length.toString() }\n ));\n return;\n }\n }\n selection.select('.box .details')\n .html('')\n .call(t.append('issues.no_issues.hidden_issues.none'));\n }\n\n var messageType;\n\n if (opts.what === 'edited' && opts.where === 'visible') {\n\n messageType = 'edits_in_view';\n\n checkForHiddenIssues({\n elsewhere: { what: 'edited', where: 'all' },\n everything_else: { what: 'all', where: 'visible' },\n disabled_rules: { what: 'edited', where: 'visible', includeDisabledRules: 'only' },\n everything_else_elsewhere: { what: 'all', where: 'all' },\n disabled_rules_elsewhere: { what: 'edited', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'edited', where: 'visible', includeIgnored: 'only' },\n ignored_issues_elsewhere: { what: 'edited', where: 'all', includeIgnored: 'only' }\n });\n\n } else if (opts.what === 'edited' && opts.where === 'all') {\n\n messageType = 'edits';\n\n checkForHiddenIssues({\n everything_else: { what: 'all', where: 'all' },\n disabled_rules: { what: 'edited', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'edited', where: 'all', includeIgnored: 'only' }\n });\n\n } else if (opts.what === 'all' && opts.where === 'visible') {\n\n messageType = 'everything_in_view';\n\n checkForHiddenIssues({\n elsewhere: { what: 'all', where: 'all' },\n disabled_rules: { what: 'all', where: 'visible', includeDisabledRules: 'only' },\n disabled_rules_elsewhere: { what: 'all', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'all', where: 'visible', includeIgnored: 'only' },\n ignored_issues_elsewhere: { what: 'all', where: 'all', includeIgnored: 'only' }\n });\n } else if (opts.what === 'all' && opts.where === 'all') {\n\n messageType = 'everything';\n\n checkForHiddenIssues({\n disabled_rules: { what: 'all', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'all', where: 'all', includeIgnored: 'only' }\n });\n }\n\n if (opts.what === 'edited' && context.history().difference().summary().length === 0) {\n messageType = 'no_edits';\n }\n\n selection.select('.box .message')\n .html('')\n .call(t.append('issues.no_issues.message.' + messageType));\n\n }\n\n context.validator().on('validated.uiSectionValidationStatus', function() {\n window.requestIdleCallback(section.reRender);\n });\n\n context.map().on('move.uiSectionValidationStatus',\n debounce(function() {\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\n\nimport { uiSectionValidationIssues } from '../sections/validation_issues';\nimport { uiSectionValidationOptions } from '../sections/validation_options';\nimport { uiSectionValidationRules } from '../sections/validation_rules';\nimport { uiSectionValidationStatus } from '../sections/validation_status';\n\nexport function uiPaneIssues(context) {\n\n var issuesPane = uiPane('issues', context)\n .key(t('issues.key'))\n .label(t.append('issues.title'))\n .description(t.append('issues.title'))\n .iconName('iD-icon-alert')\n .sections([\n uiSectionValidationOptions(context),\n uiSectionValidationStatus(context),\n uiSectionValidationIssues('issues-errors', 'error', context),\n uiSectionValidationIssues('issues-warnings', 'warning', context),\n uiSectionValidationIssues('issues-suggestions', 'suggestion', context),\n uiSectionValidationRules(context)\n ]);\n\n return issuesPane;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiConfirm } from '../confirm';\nimport { utilNoAuto, utilRebind } from '../../util';\n\n\nexport function uiSettingsCustomData(context) {\n var dispatch = d3_dispatch('change');\n\n function render(selection) {\n var dataLayer = context.layers().layer('data');\n\n // keep separate copies of original and current settings\n var _origSettings = {\n fileList: (dataLayer && dataLayer.fileList()) || null,\n url: prefs('settings-custom-data-url')\n };\n var _currSettings = {\n fileList: (dataLayer && dataLayer.fileList()) || null,\n // url: prefs('settings-custom-data-url')\n };\n\n // var example = 'https://tile.openstreetmap.org/{zoom}/{x}/{y}.png';\n var modal = uiConfirm(selection).okButton();\n\n modal\n .classed('settings-modal settings-custom-data', true);\n\n modal.select('.modal-section.header')\n .append('h3')\n .call(t.append('settings.custom_data.header'));\n\n\n var textSection = modal.select('.modal-section.message-text');\n\n textSection\n .append('pre')\n .attr('class', 'instructions-file')\n .call(t.append('settings.custom_data.file.instructions'));\n\n textSection\n .append('input')\n .attr('class', 'field-file')\n .attr('type', 'file')\n .attr('accept', '.gpx,.kml,.geojson,.json,application/gpx+xml,application/vnd.google-earth.kml+xml,application/geo+json,application/json')\n .property('files', _currSettings.fileList)\n .on('change', function(d3_event) {\n var files = d3_event.target.files;\n if (files && files.length) {\n _currSettings.url = '';\n textSection.select('.field-url').property('value', '');\n _currSettings.fileList = files;\n } else {\n _currSettings.fileList = null;\n }\n });\n\n textSection\n .append('h4')\n .call(t.append('settings.custom_data.or'));\n\n textSection\n .append('pre')\n .attr('class', 'instructions-url')\n .call(t.append('settings.custom_data.url.instructions'));\n\n textSection\n .append('textarea')\n .attr('class', 'field-url')\n .attr('placeholder', t('settings.custom_data.url.placeholder'))\n .call(utilNoAuto)\n .property('value', _currSettings.url);\n\n\n // insert a cancel button\n var buttonSection = modal.select('.modal-section.buttons');\n\n buttonSection\n .insert('button', '.ok-button')\n .attr('class', 'button cancel-button secondary-action')\n .call(t.append('confirm.cancel'));\n\n\n buttonSection.select('.cancel-button')\n .on('click.cancel', clickCancel);\n\n buttonSection.select('.ok-button')\n .attr('disabled', isSaveDisabled)\n .on('click.save', clickSave);\n\n\n function isSaveDisabled() {\n return null;\n }\n\n\n // restore the original url\n function clickCancel() {\n textSection.select('.field-url').property('value', _origSettings.url);\n prefs('settings-custom-data-url', _origSettings.url);\n this.blur();\n modal.close();\n }\n\n // accept the current url\n function clickSave() {\n _currSettings.url = textSection.select('.field-url').property('value').trim();\n\n // one or the other but not both\n if (_currSettings.url) { _currSettings.fileList = null; }\n if (_currSettings.fileList) { _currSettings.url = ''; }\n\n prefs('settings-custom-data-url', _currSettings.url);\n this.blur();\n modal.close();\n dispatch.call('change', this, _currSettings);\n }\n }\n\n return utilRebind(render, dispatch, 'on');\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t, localizer } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg/icon';\nimport { geoExtent } from '../../geo';\nimport { modeBrowse } from '../../modes/browse';\nimport { uiCmd } from '../cmd';\nimport { uiSection } from '../section';\nimport { uiSettingsCustomData } from '../settings/custom_data';\n\nexport function uiSectionDataLayers(context) {\n\n var settingsCustomData = uiSettingsCustomData(context)\n .on('change', customChanged);\n\n // refers to `modules/svg/layers.js` -> function drawLayers(selection) {...}\n var layers = context.layers();\n\n var section = uiSection('data-layers', context)\n .label(() => t.append('map_data.data_layers'))\n .disclosureContent(renderDisclosureContent);\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.data-layer-container')\n .data([0]);\n\n container.enter()\n .append('div')\n .attr('class', 'data-layer-container')\n .merge(container)\n .call(drawOsmItems)\n .call(drawQAItems)\n .call(drawCustomDataItems)\n .call(drawVectorItems) // Beta - Detroit mapping challenge\n .call(drawPanelItems);\n }\n\n function showsLayer(which) {\n var layer = layers.layer(which);\n if (layer) {\n return layer.enabled();\n }\n return false;\n }\n\n function setLayer(which, enabled) {\n // Don't allow layer changes while drawing - #6584\n var mode = context.mode();\n if (mode && /^draw/.test(mode.id)) return;\n\n var layer = layers.layer(which);\n if (layer) {\n layer.enabled(enabled);\n\n if (!enabled && (which === 'osm' || which === 'notes')) {\n context.enter(modeBrowse(context));\n }\n }\n }\n\n function toggleLayer(which) {\n setLayer(which, !showsLayer(which));\n }\n\n function drawOsmItems(selection) {\n var osmKeys = ['osm', 'notes'];\n var osmLayers = layers.all().filter(function(obj) { return osmKeys.indexOf(obj.id) !== -1; });\n\n var ul = selection\n .selectAll('.layer-list-osm')\n .data([0]);\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-osm')\n .merge(ul);\n\n var li = ul.selectAll('.list-item')\n .data(osmLayers);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) { return 'list-item list-item-' + d.id; });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n if (d.id === 'osm') {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.' + d.id + '.tooltip'))\n .keys([uiCmd('\u2325' + t('area_fill.wireframe.key'))])\n .placement('bottom')\n );\n } else {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.' + d.id + '.tooltip'))\n .placement('bottom')\n );\n }\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) { toggleLayer(d.id); });\n\n labelEnter\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append('map_data.layers.' + d.id + '.title'));\n });\n\n\n // Update\n li\n .merge(liEnter)\n .classed('active', function (d) { return d.layer.enabled(); })\n .selectAll('input')\n .property('checked', function (d) { return d.layer.enabled(); });\n }\n\n function drawQAItems(selection) {\n var qaKeys = ['osmose'];\n var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; });\n\n var ul = selection\n .selectAll('.layer-list-qa')\n .data([0]);\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-qa')\n .merge(ul);\n\n var li = ul.selectAll('.list-item')\n .data(qaLayers);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) { return 'list-item list-item-' + d.id; });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.' + d.id + '.tooltip'))\n .placement('bottom')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) { toggleLayer(d.id); });\n\n labelEnter\n .append('span')\n .each(function(d) { t.append('map_data.layers.' + d.id + '.title')(d3_select(this)); });\n\n\n // Update\n li\n .merge(liEnter)\n .classed('active', function (d) { return d.layer.enabled(); })\n .selectAll('input')\n .property('checked', function (d) { return d.layer.enabled(); });\n }\n\n // Beta feature - sample vector layers to support Detroit Mapping Challenge\n // https://github.com/osmus/detroit-mapping-challenge\n function drawVectorItems(selection) {\n var dataLayer = layers.layer('data');\n var vtData = [\n {\n name: 'Detroit Neighborhoods/Parks',\n src: 'neighborhoods-parks',\n tooltip: 'Neighborhood boundaries and parks as compiled by City of Detroit in concert with community groups.',\n template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmur6x34562qp9iv1u3ksf-54hev,jonahadkins.cjksmqxdx33jj2wp90xd9x2md-4e5y2/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'\n }, {\n name: 'Detroit Composite POIs',\n src: 'composite-poi',\n tooltip: 'Fire Inspections, Business Licenses, and other public location data collated from the City of Detroit.',\n template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmm6a02sli31myxhsr7zf3-2sw8h/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'\n }, {\n name: 'Detroit All-The-Places POIs',\n src: 'alltheplaces-poi',\n tooltip: 'Public domain business location data created by web scrapers.',\n template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmswgk340g2vo06p1w9w0j-8fjjc/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'\n }\n ];\n\n // Only show this if the map is around Detroit..\n var detroit = geoExtent([-83.5, 42.1], [-82.8, 42.5]);\n var showVectorItems = (context.map().zoom() > 9 && detroit.contains(context.map().center()));\n\n var container = selection.selectAll('.vectortile-container')\n .data(showVectorItems ? [0] : []);\n\n container.exit()\n .remove();\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'vectortile-container');\n\n containerEnter\n .append('h4')\n .attr('class', 'vectortile-header')\n .text('Detroit Vector Tiles (Beta)');\n\n containerEnter\n .append('ul')\n .attr('class', 'layer-list layer-list-vectortile');\n\n containerEnter\n .append('div')\n .attr('class', 'vectortile-footer')\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', 'https://github.com/osmus/detroit-mapping-challenge')\n .append('span')\n .text('About these layers');\n\n container = container\n .merge(containerEnter);\n\n\n var ul = container.selectAll('.layer-list-vectortile');\n\n var li = ul.selectAll('.list-item')\n .data(vtData);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) { return 'list-item list-item-' + d.src; });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n d3_select(this).call(\n uiTooltip().title(d.tooltip).placement('top')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'radio')\n .attr('name', 'vectortile')\n .on('change', selectVTLayer);\n\n labelEnter\n .append('span')\n .text(function(d) { return d.name; });\n\n // Update\n li\n .merge(liEnter)\n .classed('active', isVTLayerSelected)\n .selectAll('input')\n .property('checked', isVTLayerSelected);\n\n\n function isVTLayerSelected(d) {\n return dataLayer && dataLayer.template() === d.template;\n }\n\n function selectVTLayer(d3_event, d) {\n prefs('settings-custom-data-url', d.template);\n if (dataLayer) {\n dataLayer.template(d.template, d.src);\n dataLayer.enabled(true);\n }\n }\n }\n\n function drawCustomDataItems(selection) {\n var dataLayer = layers.layer('data');\n var hasData = dataLayer && dataLayer.hasData();\n var showsData = hasData && dataLayer.enabled();\n\n var ul = selection\n .selectAll('.layer-list-data')\n .data(dataLayer ? [0] : []);\n\n // Exit\n ul.exit()\n .remove();\n\n // Enter\n var ulEnter = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-data');\n\n var liEnter = ulEnter\n .append('li')\n .attr('class', 'list-item-data');\n\n var labelEnter = liEnter\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.custom.tooltip'))\n .placement('top')\n );\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function() { toggleLayer('data'); });\n\n labelEnter\n .append('span')\n .call(t.append('map_data.layers.custom.title'));\n\n liEnter\n .append('button')\n .attr('class', 'open-data-options')\n .call(uiTooltip()\n .title(() => t.append('settings.custom_data.tooltip'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n editCustom();\n })\n .call(svgIcon('#iD-icon-more'));\n\n liEnter\n .append('button')\n .attr('class', 'zoom-to-data')\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.custom.zoom'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n if (d3_select(this).classed('disabled')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n dataLayer.fitZoom();\n })\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'));\n\n // Update\n ul = ul\n .merge(ulEnter);\n\n ul.selectAll('.list-item-data')\n .classed('active', showsData)\n .selectAll('label')\n .classed('deemphasize', !hasData)\n .selectAll('input')\n .property('disabled', !hasData)\n .property('checked', showsData);\n\n ul.selectAll('button.zoom-to-data')\n .classed('disabled', !hasData);\n }\n\n function editCustom() {\n context.container()\n .call(settingsCustomData);\n }\n\n function customChanged(d) {\n var dataLayer = layers.layer('data');\n\n if (d && d.url) {\n dataLayer.url(d.url);\n } else if (d && d.fileList) {\n dataLayer.fileList(d.fileList);\n }\n }\n\n function drawPanelItems(selection) {\n\n var panelsListEnter = selection.selectAll('.md-extras-list')\n .data([0])\n .enter()\n .append('ul')\n .attr('class', 'layer-list md-extras-list');\n\n var historyPanelLabelEnter = panelsListEnter\n .append('li')\n .attr('class', 'history-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('map_data.history_panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.history.key'))])\n .placement('top')\n );\n\n historyPanelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('history');\n });\n\n historyPanelLabelEnter\n .append('span')\n .call(t.append('map_data.history_panel.title'));\n\n var measurementPanelLabelEnter = panelsListEnter\n .append('li')\n .attr('class', 'measurement-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('map_data.measurement_panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.measurement.key'))])\n .placement('top')\n );\n\n measurementPanelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('measurement');\n });\n\n measurementPanelLabelEnter\n .append('span')\n .call(t.append('map_data.measurement_panel.title'));\n }\n\n context.layers().on('change.uiSectionDataLayers', section.reRender);\n\n context.map()\n .on('move.uiSectionDataLayers',\n debounce(function() {\n // Detroit layers may have moved in or out of view\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionMapFeatures(context) {\n\n var _features = context.features().keys();\n\n var section = uiSection('map-features', context)\n .label(() => t.append('map_data.map_features'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n function renderDisclosureContent(selection) {\n\n var container = selection.selectAll('.layer-feature-list-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'layer-feature-list-container');\n\n containerEnter\n .append('ul')\n .attr('class', 'layer-list layer-feature-list');\n\n var footer = containerEnter\n .append('div')\n .attr('class', 'feature-list-links section-footer');\n\n footer\n .append('a')\n .attr('class', 'feature-list-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.disable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.features().disableAll();\n });\n\n footer\n .append('a')\n .attr('class', 'feature-list-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.enable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.features().enableAll();\n });\n\n // Update\n container = container\n .merge(containerEnter);\n\n container.selectAll('.layer-feature-list')\n .call(drawListItems, _features, 'checkbox', 'feature', clickFeature, showsFeature);\n }\n\n function drawListItems(selection, data, type, name, change, active) {\n var items = selection.selectAll('li')\n .data(data);\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li')\n .call(uiTooltip()\n .title(function(d) {\n var tip = t.append(name + '.' + d + '.tooltip');\n if (autoHiddenFeature(d)) {\n var msg = showsLayer('osm') ? t.append('map_data.autohidden') : t.append('map_data.osmhidden');\n return selection => {\n selection.call(tip);\n selection.append('div').call(msg);\n };\n }\n return tip;\n })\n .placement('top')\n );\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', name)\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append(name + '.' + d + '.description'));\n });\n\n // Update\n items = items\n .merge(enter);\n\n items\n .classed('active', active)\n .selectAll('input')\n .property('checked', active)\n .property('indeterminate', autoHiddenFeature);\n }\n\n function autoHiddenFeature(d) {\n return context.features().autoHidden(d);\n }\n\n function showsFeature(d) {\n return context.features().enabled(d);\n }\n\n function clickFeature(d3_event, d) {\n context.features().toggle(d);\n }\n\n function showsLayer(id) {\n var layer = context.layers().layer(id);\n return layer && layer.enabled();\n }\n\n // add listeners\n context.features()\n .on('change.map_features', section.reRender);\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionMapStyleOptions(context) {\n\n var section = uiSection('fill-area', context)\n .label(() => t.append('map_data.style_options'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.layer-fill-list')\n .data([0]);\n\n container.enter()\n .append('ul')\n .attr('class', 'layer-list layer-fill-list')\n .merge(container)\n .call(drawListItems, context.map().areaFillOptions, 'radio', 'area_fill', setFill, isActiveFill);\n\n var container2 = selection.selectAll('.layer-visual-diff-list')\n .data([0]);\n\n container2.enter()\n .append('ul')\n .attr('class', 'layer-list layer-visual-diff-list')\n .merge(container2)\n .call(drawListItems, ['highlight_edits'], 'checkbox', 'visual_diff', toggleHighlightEdited, function() {\n return context.surface().classed('highlight-edited');\n });\n }\n\n function drawListItems(selection, data, type, name, change, active) {\n var items = selection.selectAll('li')\n .data(data);\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li')\n .call(uiTooltip()\n .title(function(d) {\n return t.append(name + '.' + d + '.tooltip');\n })\n .keys(function(d) {\n var key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null);\n if (d === 'highlight_edits') key = t('map_data.highlight_edits.key');\n return key ? [key] : null;\n })\n .placement('top')\n );\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', name)\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append(name + '.' + d + '.description'));\n });\n\n // Update\n items = items\n .merge(enter);\n\n items\n .classed('active', active)\n .selectAll('input')\n .property('checked', active)\n .property('indeterminate', false);\n }\n\n function isActiveFill(d) {\n return context.map().activeAreaFill() === d;\n }\n\n function toggleHighlightEdited(d3_event) {\n d3_event.preventDefault();\n context.map().toggleHighlightEdited();\n }\n\n function setFill(d3_event, d) {\n context.map().activeAreaFill(d);\n }\n\n context.map()\n .on('changeHighlighting.ui_style, changeAreaFill.ui_style', section.reRender);\n\n return section;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { isArray, isNumber } from 'es-toolkit/compat';\n\nimport { t } from '../../core/localizer';\nimport { uiConfirm } from '../confirm';\nimport { utilRebind } from '../../util';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg';\n\n\nexport function uiSettingsLocalPhotos(context) {\n var dispatch = d3_dispatch('change');\n var photoLayer = context.layers().layer('local-photos');\n var modal;\n\n function render(selection) {\n\n modal = uiConfirm(selection).okButton();\n\n modal\n .classed('settings-modal settings-local-photos', true);\n\n modal.select('.modal-section.header')\n .append('h3')\n .call(t.append('local_photos.header'));\n\n modal.select('.modal-section.message-text')\n .append('div')\n .classed('local-photos', true);\n\n var instructionsSection = modal.select('.modal-section.message-text .local-photos')\n .append('div')\n .classed('instructions', true);\n\n instructionsSection\n .append('p')\n .classed('instructions-local-photos', true)\n .call(t.append('local_photos.file.instructions'));\n\n instructionsSection\n .append('input')\n .classed('field-file', true)\n .attr('type', 'file')\n .attr('multiple', 'multiple')\n .attr('accept', '.jpg,.jpeg,.png,image/png,image/jpeg')\n .style('visibility', 'hidden')\n .attr('id', 'local-photo-files')\n .on('change', function(d3_event) {\n var files = d3_event.target.files;\n if (files && files.length) {\n photoList\n .select('ul')\n .append('li')\n .classed('placeholder', true)\n .append('div');\n dispatch.call('change', this, files);\n }\n d3_event.target.value = null;\n });\n instructionsSection\n .append('label')\n .attr('for', 'local-photo-files')\n .classed('button', true)\n .call(t.append('local_photos.file.label'));\n\n const photoList = modal.select('.modal-section.message-text .local-photos')\n .append('div')\n .append('div')\n .classed('list-local-photos', true);\n\n photoList\n .append('ul');\n\n updatePhotoList(photoList.select('ul'));\n\n context.layers().on('change', () => updatePhotoList(photoList.select('ul')));\n }\n\n function updatePhotoList(container) {\n function locationUnavailable(d) {\n return !(isArray(d.loc) && isNumber(d.loc[0]) && isNumber(d.loc[1]));\n }\n\n container.selectAll('li.placeholder').remove();\n\n let selection = container.selectAll('li')\n .data(photoLayer.getPhotos() ?? [], d => d.id);\n selection.exit()\n .remove();\n\n const selectionEnter = selection.enter()\n .append('li');\n\n selectionEnter\n .append('span')\n .classed('filename', true);\n selectionEnter\n .append('button')\n .classed('form-field-button zoom-to-data', true)\n .attr('title', t('local_photos.zoom_single'))\n .call(svgIcon('#iD-icon-framed-dot'));\n selectionEnter\n .append('button')\n .classed('form-field-button no-geolocation', true)\n .call(svgIcon('#iD-icon-alert'))\n .call(uiTooltip()\n .title(() => t.append('local_photos.no_geolocation.tooltip'))\n .placement('left')\n );\n selectionEnter\n .append('button')\n .classed('form-field-button remove', true)\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'));\n\n selection = selection.merge(selectionEnter);\n\n selection\n .classed('invalid', locationUnavailable);\n selection.select('span.filename')\n .text(d => d.name)\n .attr('title', d => d.name);\n selection.select('span.filename')\n .on('click', (d3_event, d) => {\n photoLayer.openPhoto(d3_event, d, false);\n });\n selection.select('button.zoom-to-data')\n .on('click', (d3_event, d) => {\n photoLayer.openPhoto(d3_event, d, true);\n });\n selection.select('button.remove')\n .on('click', (d3_event, d) => {\n photoLayer.removePhoto(d.id);\n updatePhotoList(container);\n });\n }\n\n return utilRebind(render, dispatch, 'on');\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\n\nimport { localizer, t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\nimport { utilNoAuto } from '../../util';\nimport { uiSettingsLocalPhotos } from '../settings/local_photos';\nimport { svgIcon } from '../../svg';\n\nexport function uiSectionPhotoOverlays(context) {\n\n let _savedLayers = [];\n let _layersHidden = false;\n\n var settingsLocalPhotos = uiSettingsLocalPhotos(context)\n .on('change', localPhotosChanged);\n\n var layers = context.layers();\n\n var section = uiSection('photo-overlays', context)\n .label(() => t.append('photo_overlays.title'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n const photoDates = {};\n const now = +new Date();\n\n /**\n * Calls all draw function\n * @param {*} selection Current HTML selection\n */\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.photo-overlay-container')\n .data([0]);\n\n container.enter()\n .append('div')\n .attr('class', 'photo-overlay-container')\n .merge(container)\n .call(drawPhotoItems)\n .call(drawPhotoTypeItems)\n .call(drawDateSlider)\n .call(drawUsernameFilter)\n .call(drawLocalPhotos);\n }\n\n /**\n * Draws the streetlevels in the right panel\n */\n function drawPhotoItems(selection) {\n var photoKeys = context.photos().overlayLayerIDs();\n var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; });\n var data = photoLayers.filter(function(obj) {\n if (!obj.layer.supported()) return false;\n if (layerEnabled(obj)) return true;\n if (typeof obj.layer.validHere === 'function') {\n return obj.layer.validHere(context.map().extent(), context.map().zoom());\n }\n return true;\n });\n\n function layerSupported(d) {\n return d.layer && d.layer.supported();\n }\n function layerEnabled(d) {\n return layerSupported(d) && (d.layer.enabled() || _savedLayers.includes(d.id));\n }\n function layerRendered(d) {\n return d.layer.rendered?.(context.map().zoom()) ?? true;\n }\n\n var ul = selection\n .selectAll('.layer-list-photos')\n .data([0]);\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-photos')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-photos')\n .data(data, d => d.id);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) {\n var classes = 'list-item-photos list-item-' + d.id;\n if (d.id === 'mapillary-signs' || d.id === 'mapillary-map-features') {\n classes += ' indented';\n }\n return classes;\n });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n var titleID;\n if (d.id === 'mapillary-signs') titleID = 'mapillary.signs.tooltip';\n else if (d.id === 'mapillary') titleID = 'mapillary_images.tooltip';\n else if (d.id === 'kartaview') titleID = 'kartaview_images.tooltip';\n else titleID = d.id.replace(/-/g, '_') + '.tooltip';\n d3_select(this)\n .call(uiTooltip()\n .title(() => {\n if (!layerRendered(d)) {\n return t.append('street_side.minzoom_tooltip');\n } else {\n return t.append(titleID);\n }\n })\n .placement('top')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) { toggleLayer(d.id); });\n\n labelEnter\n .append('span')\n .each(function(d) {\n var id = d.id;\n if (id === 'mapillary-signs') id = 'photo_overlays.traffic_signs';\n d3_select(this).call(t.append(id.replace(/-/g, '_') + '.title'));\n });\n\n // Update\n li\n .merge(liEnter)\n .classed('active', layerEnabled)\n .selectAll('input')\n .property('disabled', d => !layerRendered(d))\n .property('checked', layerEnabled);\n }\n\n /**\n * Draws the photo type filter in the right panel\n */\n function drawPhotoTypeItems(selection) {\n var data = context.photos().allPhotoTypes();\n\n function typeEnabled(d) {\n return context.photos().showsPhotoType(d);\n }\n\n var ul = selection\n .selectAll('.layer-list-photo-types')\n .data([0]);\n\n ul.exit()\n .remove();\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-photo-types')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-photo-types')\n .data(context.photos().shouldFilterByPhotoType() ? data : []);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) {\n return 'list-item-photo-types list-item-' + d;\n });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('photo_overlays.photo_type.' + d + '.tooltip'))\n .placement('top')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) {\n context.photos().togglePhotoType(d, true);\n });\n\n labelEnter\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append('photo_overlays.photo_type.' + d + '.title'));\n });\n\n\n // Update\n li\n .merge(liEnter)\n .classed('active', typeEnabled)\n .selectAll('input')\n .property('checked', typeEnabled);\n }\n\n /**\n * Draws the date slider filter in the right panel\n */\n function drawDateSlider(selection){\n\n var ul = selection\n .selectAll('.layer-list-date-slider')\n .data([0]);\n\n ul.exit()\n .remove();\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-date-slider')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-date-slider')\n .data(context.photos().shouldFilterDateBySlider() ? ['date-slider'] : []);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', 'list-item-date-slider');\n\n var labelEnter = liEnter\n .append('label')\n .each(function() {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('photo_overlays.age_slider_filter.tooltip'))\n .placement('top')\n );\n });\n\n labelEnter\n .append('span')\n .attr('class', 'dateSliderSpan')\n .call(t.append('photo_overlays.age_slider_filter.title'));\n\n let sliderWrap = labelEnter\n .append('div')\n .attr('class','slider-wrap');\n\n sliderWrap\n .append('input')\n .attr('type', 'range')\n .attr('min', 0)\n .attr('max', 1)\n .attr('step', 0.001)\n .attr('list', 'photo-overlay-date-range')\n .attr('value', () => dateSliderValue('from'))\n .classed('list-option-date-slider', true)\n .classed('from-date', true)\n .style('direction', localizer.textDirection() === 'rtl' ? 'ltr' : 'rtl')\n .call(utilNoAuto)\n .on('change', function() {\n let value = d3_select(this).property('value');\n setYearFilter(value, true, 'from');\n });\n selection.select('input.from-date').each(function() { this.value = dateSliderValue('from'); });\n\n sliderWrap.append('div')\n .attr('class', 'date-slider-label');\n\n sliderWrap\n .append('input')\n .attr('type', 'range')\n .attr('min', 0)\n .attr('max', 1)\n .attr('step', 0.001)\n .attr('list', 'photo-overlay-date-range-inverted')\n .attr('value', () => 1 - dateSliderValue('to'))\n .classed('list-option-date-slider', true)\n .classed('to-date', true)\n // OHM variant: https://github.com/OpenHistoricalMap/issues/issues/1030 .style('display', () => dateSliderValue('to') === 0 ? 'none' : null)\n .style('direction', localizer.textDirection())\n .call(utilNoAuto)\n .on('change', function() {\n let value = d3_select(this).property('value');\n setYearFilter(1-value, true, 'to');\n });\n selection.select('input.to-date').each(function() { this.value = 1 - dateSliderValue('to'); });\n\n selection.select('.date-slider-label')\n .call(dateSliderValue('from') === 1\n ? t.addOrUpdate('photo_overlays.age_slider_filter.label_all')\n : t.addOrUpdate('photo_overlays.age_slider_filter.label_date', {\n date: new Date(now - Math.pow(dateSliderValue('from'), 1.45) * 10 * 365.25 * 86400 * 1000).toLocaleDateString(localizer.localeCode()) }));\n\n sliderWrap.append('datalist')\n .attr('class', 'date-slider-values')\n .attr('id', 'photo-overlay-date-range');\n sliderWrap.append('datalist')\n .attr('class', 'date-slider-values')\n .attr('id', 'photo-overlay-date-range-inverted');\n\n const dateTicks = new Set();\n for (const dates of Object.values(photoDates)) {\n dates.forEach(date => {\n dateTicks.add(Math.round(1000 * Math.pow((now - date) / (10 * 365.25 * 86400 * 1000), 1/1.45)) / 1000);\n });\n }\n const ticks = selection.select('datalist#photo-overlay-date-range').selectAll('option')\n .data([...dateTicks].concat([1, 0]));\n ticks.exit()\n .remove();\n ticks.enter()\n .append('option')\n .merge(ticks)\n .attr('value', d => d);\n const ticksInverted = selection.select('datalist#photo-overlay-date-range-inverted').selectAll('option')\n .data([...dateTicks].concat([1, 0]));\n ticksInverted.exit()\n .remove();\n ticksInverted.enter()\n .append('option')\n .merge(ticksInverted)\n .attr('value', d => 1 - d);\n\n\n li\n .merge(liEnter)\n .classed('active', filterEnabled);\n\n function filterEnabled() {\n return !!context.photos().fromDate();\n }\n }\n\n function dateSliderValue(which) {\n const val = which === 'from' ? context.photos().fromDate() : context.photos().toDate();\n if (val) {\n const date = +new Date(val);\n return Math.pow((now - date) / (10 * 365.25 * 86400 * 1000), 1/1.45);\n } else return which === 'from' ? 1 : 0;\n }\n\n /**\n * Util function to set the slider date filter\n * @param {Number} value The slider value\n * @param {Boolean} updateUrl whether the URL should update or not\n * @param {string} which to set either the 'from' or 'to' date\n */\n function setYearFilter(value, updateUrl, which){\n value = +value + (which === 'from' ? 0.001 : -0.001);\n\n if (value < 1 && value > 0) {\n const date = new Date(now - Math.pow(value, 1.45) * 10 * 365.25 * 86400 * 1000)\n .toISOString().substring(0,10);\n context.photos().setDateFilter(`${which}Date`, date, updateUrl);\n } else {\n context.photos().setDateFilter(`${which}Date`, null, updateUrl);\n }\n };\n\n /**\n * Draws the username filter in the right panel\n */\n function drawUsernameFilter(selection) {\n function filterEnabled() {\n return context.photos().usernames();\n }\n var ul = selection\n .selectAll('.layer-list-username-filter')\n .data([0]);\n\n ul.exit()\n .remove();\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-username-filter')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-username-filter')\n .data(context.photos().shouldFilterByUsername() ? ['username-filter'] : []);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', 'list-item-username-filter');\n\n var labelEnter = liEnter\n .append('label')\n .each(function() {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('photo_overlays.username_filter.tooltip'))\n .placement('top')\n );\n });\n\n labelEnter\n .append('span')\n .call(t.append('photo_overlays.username_filter.title'));\n\n labelEnter\n .append('input')\n .attr('type', 'text')\n .attr('class', 'list-item-input')\n .call(utilNoAuto)\n .property('value', usernameValue)\n .on('change', function() {\n var value = d3_select(this).property('value');\n context.photos().setUsernameFilter(value, true);\n d3_select(this).property('value', usernameValue);\n });\n\n li\n .merge(liEnter)\n .classed('active', filterEnabled);\n\n function usernameValue() {\n var usernames = context.photos().usernames();\n if (usernames) return usernames.join('; ');\n return usernames;\n }\n }\n\n /**\n * Toggle on/off the selected layer\n * @param {*} which Id of the selected layer\n */\n function toggleLayer(which) {\n setLayer(which, !showsLayer(which));\n }\n\n /**\n * @param {*} which Id of the selected layer\n * @returns whether the layer is enabled\n */\n function showsLayer(which) {\n var layer = layers.layer(which);\n if (layer) {\n return layer.enabled();\n }\n return false;\n }\n\n /**\n * Set the selected layer\n * @param {string} which Id of the selected layer\n * @param {boolean} enabled\n */\n function setLayer(which, enabled) {\n var layer = layers.layer(which);\n if (layer) {\n layer.enabled(enabled);\n }\n }\n\n function drawLocalPhotos(selection) {\n var photoLayer = layers.layer('local-photos');\n var hasData = photoLayer && photoLayer.hasData();\n var showsData = hasData && photoLayer.enabled();\n\n var ul = selection\n .selectAll('.layer-list-local-photos')\n .data(photoLayer ? [0] : []);\n\n // Exit\n ul.exit()\n .remove();\n\n // Enter\n var ulEnter = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-local-photos');\n\n var localPhotosEnter = ulEnter\n .append('li')\n .attr('class', 'list-item-local-photos');\n\n var localPhotosLabelEnter = localPhotosEnter\n .append('label')\n .call(uiTooltip().title(() => t.append('local_photos.tooltip')));\n\n localPhotosLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function() { toggleLayer('local-photos'); });\n\n localPhotosLabelEnter\n .call(t.append('local_photos.header'));\n\n localPhotosEnter\n .append('button')\n .attr('class', 'open-data-options')\n .call(uiTooltip()\n .title(() => t.append('local_photos.tooltip_edit'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n editLocalPhotos();\n })\n .call(svgIcon('#iD-icon-more'));\n\n localPhotosEnter\n .append('button')\n .attr('class', 'zoom-to-data')\n .call(uiTooltip()\n .title(() => t.append('local_photos.zoom'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n if (d3_select(this).classed('disabled')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n photoLayer.fitZoom();\n })\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'));\n\n // Update\n ul = ul\n .merge(ulEnter);\n\n ul.selectAll('.list-item-local-photos')\n .classed('active', showsData)\n .selectAll('label')\n .classed('deemphasize', !hasData)\n .selectAll('input')\n .property('disabled', !hasData)\n .property('checked', showsData);\n\n ul.selectAll('button.zoom-to-data')\n .classed('disabled', !hasData);\n }\n\n function editLocalPhotos() {\n context.container()\n .call(settingsLocalPhotos);\n }\n\n function localPhotosChanged(d) {\n var localPhotosLayer = layers.layer('local-photos');\n\n localPhotosLayer.fileList(d);\n }\n\n /**\n * Toggles StreetView on/off\n */\n function toggleStreetSide(){\n let layerContainer = d3_select('.photo-overlay-container');\n if (!_layersHidden){\n const streetLayerIDs = context.photos().overlayLayerIDs();\n layers.all().forEach(d => {\n if (streetLayerIDs.includes(d.id)) {\n if (showsLayer(d.id)) _savedLayers.push(d.id);\n setLayer(d.id, false);\n }\n });\n layerContainer.classed('disabled-panel', true);\n } else {\n _savedLayers.forEach(d => {\n setLayer(d, true);\n });\n _savedLayers = [];\n layerContainer.classed('disabled-panel', false);\n }\n _layersHidden = !_layersHidden;\n };\n\n context.layers().on('change.uiSectionPhotoOverlays', section.reRender);\n context.photos().on('change.uiSectionPhotoOverlays', section.reRender);\n context.layers().on('photoDatesChanged.uiSectionPhotoOverlays', function(service, dates) {\n photoDates[service] = dates.map(date => +new Date(date));\n section.reRender();\n });\n context.keybinding().on('\u21E7P', toggleStreetSide);\n\n context.map()\n .on('move.photo_overlays',\n debounce(function() {\n // layers in-view may have changed due to map move\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiSection } from '../section';\nimport { utilQsString, utilStringQs, utilNormalizeDateString } from '../../util';\n\nconst DEFAULT_MIN_DATE = '-4000-01-01';\nconst DEFAULT_MAX_DATE = (new Date()).getFullYear() + '-12-31';\n\nconst INPUT_STYLES = [\n { name: 'width', value: '125px' },\n { name: 'text-align', value: 'center' },\n];\nconst LABEL_STYLES = [\n { name: 'font-weight', value: 'bold' },\n { name: 'display', value: 'inline-block' },\n { name: 'width', value: '75px' },\n];\n\nexport function uiSectionDateRange(context) {\n // despite appearing as a separate panel, Map Features does the real filtering\n // see applyDateRange() in this panel, where the dateRange value is set\n // see modules/renderer/features.js checkDateFilter() which applies the filters\n // see modules/renderer/features.js update() which updates URL params\n\n const section = uiSection('date_ranges', context)\n .label(() => t.append('date_ranges.title'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n function renderDisclosureContent(selection) {\n const container = selection.selectAll('.date_ranges-container').data([0]);\n\n // for some reason this one uiSection keeps adding content every time it's expanded,\n // so there are 2 inputs, then 4, then 6, ...\n const alreadyhasinputs = container.enter().selectAll('input').size();\n if (alreadyhasinputs) return;\n\n // start date label & input\n const mindate_label = container.enter()\n .append('label')\n .call(t.append('date_ranges.start_date.description'))\n .merge(container);\n const mindate_input = container.enter()\n .append('input')\n .attr('type', 'text')\n .attr('value', DEFAULT_MIN_DATE)\n .attr('title', () => t('date_ranges.start_date.tooltip'))\n .attr('placeholder', () => t('date_ranges.start_date.placeholder'))\n .merge(container);\n\n // line break\n container.enter()\n .append('br')\n .merge(container);\n\n // end date label & input\n const maxdate_label = container.enter()\n .append('label')\n .call(t.append('date_ranges.end_date.description'))\n .merge(container);\n const maxdate_input = container.enter() // we will refer to this widget by its name attribute to fetch our date range\n .append('input')\n .attr('type', 'text')\n .attr('value', DEFAULT_MAX_DATE)\n .attr('title', () => t('date_ranges.end_date.tooltip'))\n .attr('placeholder', () => t('date_ranges.end_date.placeholder'))\n .merge(container);\n\n // apply styles\n INPUT_STYLES.forEach(function (style) {\n mindate_input.style(style.name, style.value);\n maxdate_input.style(style.name, style.value);\n });\n LABEL_STYLES.forEach(function (style) {\n mindate_label.style(style.name, style.value);\n maxdate_label.style(style.name, style.value);\n });\n\n // event handler for change event\n // intercept invalid & blank and correct them to our hardcoded in/max\n // then cause a re-filter/redraw\n function applyDateRange() {\n const mindate = mindate_input.property('value');\n const maxdate = maxdate_input.property('value');\n\n context.features().dateRange = [mindate, maxdate];\n context.features().redraw();\n\n updateUrlParam();\n }\n\n function ensureValidInputs() {\n // if utilNormalizeDateString() can make sense of it, so can utilDatesOverlap()\n // replace with cleaned-up value for visual feedback e.g. 5/10/2022 visibly changes to 2022-10-05\n // if not, then reset to the starting value\n const mindate = mindate_input.property('value');\n const maxdate = maxdate_input.property('value');\n\n const mindate_clean = utilNormalizeDateString(mindate);\n const maxdate_clean = utilNormalizeDateString(maxdate);\n mindate_input.property('value', mindate_clean ? mindate_clean.value : DEFAULT_MIN_DATE);\n maxdate_input.property('value', maxdate_clean ? maxdate_clean.value : DEFAULT_MAX_DATE);\n }\n\n function updateUrlParam() {\n if (!window.mocha) {\n const hash = utilStringQs(window.location.hash);\n\n const daterange = context.features().dateRange;\n if (daterange) {\n hash.daterange = daterange.join(',');\n } else {\n delete hash.daterange;\n }\n\n window.location.replace('#' + utilQsString(hash, true));\n }\n }\n\n mindate_input.on('change', function () {\n ensureValidInputs();\n applyDateRange();\n });\n maxdate_input.on('change', function () {\n ensureValidInputs();\n applyDateRange();\n });\n\n // startup\n // load the start/end date from URL params\n // then apply it so we have context().dateRange defined as early as possible\n let startingdaterange = utilStringQs(window.location.hash).daterange;\n if (startingdaterange) {\n startingdaterange = startingdaterange.split(',');\n const isvalid =startingdaterange[0].match(/^\\-?[\\d\\-]+/) && startingdaterange[1].match(/^\\-?[\\d\\-]+/);\n if (isvalid) {\n mindate_input.property('value', startingdaterange[0]);\n maxdate_input.property('value', startingdaterange[1]);\n }\n }\n applyDateRange();\n }\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\n\nimport { uiSectionDataLayers } from '../sections/data_layers';\nimport { uiSectionMapFeatures } from '../sections/map_features';\nimport { uiSectionMapStyleOptions } from '../sections/map_style_options';\nimport { uiSectionPhotoOverlays } from '../sections/photo_overlays';\nimport { uiSectionDateRange } from '../sections/map_daterange';\n\nexport function uiPaneMapData(context) {\n\n var mapDataPane = uiPane('map-data', context)\n .key(t('map_data.key'))\n .label(t.append('map_data.title'))\n .description(t.append('map_data.description'))\n .iconName('iD-icon-data')\n .sections([\n uiSectionDataLayers(context),\n uiSectionDateRange(context),\n uiSectionPhotoOverlays(context),\n uiSectionMapStyleOptions(context),\n uiSectionMapFeatures(context)\n ]);\n\n return mapDataPane;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\nimport { uiSectionPrivacy } from '../sections/privacy';\n\nexport function uiPanePreferences(context) {\n\n let preferencesPane = uiPane('preferences', context)\n .key(t('preferences.key'))\n .label(t.append('preferences.title'))\n .description(t.append('preferences.description'))\n .iconName('fas-user-cog')\n .sections([\n uiSectionPrivacy(context)\n ]);\n\n return preferencesPane;\n}\n", "import { marked } from 'marked';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { prefs } from '../core/preferences';\nimport { t, localizer } from '../core/localizer';\nimport { presetManager } from '../presets';\nimport { behaviorHash } from '../behavior';\nimport { modeBrowse } from '../modes/browse';\nimport { svgDefs, svgIcon } from '../svg';\nimport { utilDetect } from '../util/detect';\nimport { utilGetDimensions } from '../util/dimensions';\n\nimport { uiAccount } from './account';\nimport { uiAttribution } from './attribution';\nimport { uiContributors } from './contributors';\nimport { uiEditMenu } from './edit_menu';\nimport { uiFeatureInfo } from './feature_info';\nimport { uiFlash } from './flash';\nimport { uiFullScreen } from './full_screen';\nimport { uiGeolocate } from './geolocate';\nimport { uiInfo } from './info';\nimport { uiIntro } from './intro';\nimport { uiIssuesInfo } from './issues_info';\nimport { uiLoading } from './loading';\nimport { uiMapInMap } from './map_in_map';\nimport { uiNotice } from './notice';\nimport { uiPhotoviewer } from './photoviewer';\nimport { uiRestore } from './restore';\nimport { uiScale } from './scale';\nimport { uiShortcuts } from './shortcuts';\nimport { uiSidebar } from './sidebar';\nimport { uiSourceSwitch } from './source_switch';\nimport { uiSpinner } from './spinner';\nimport { uiSplash } from './splash';\nimport { uiStatus } from './status';\nimport { uiTooltip } from './tooltip';\nimport { uiTopToolbar } from './top_toolbar';\nimport { uiVersion } from './version';\nimport { uiZoom } from './zoom';\nimport { uiZoomToSelection } from './zoom_to_selection';\nimport { uiCmd } from './cmd';\n\nimport { uiPaneBackground } from './panes/background';\nimport { uiPaneHelp } from './panes/help';\nimport { uiPaneIssues } from './panes/issues';\nimport { uiPaneMapData } from './panes/map_data';\nimport { uiPanePreferences } from './panes/preferences';\n\nexport function uiInit(context) {\n var _initCounter = 0;\n var _needWidth = {};\n\n var _lastPointerType;\n\n var overMap;\n\n function render(container) {\n\n container\n .on('click.ui', function(d3_event) {\n // we're only concerned with the primary mouse button\n if (d3_event.button !== 0) return;\n\n if (!d3_event.composedPath) return;\n\n // some targets have default click events we don't want to override\n var isOkayTarget = d3_event.composedPath().some(function(node) {\n // we only care about element nodes\n return node.nodeType === 1 &&\n // clicking focuses it and/or changes a value\n (node.nodeName === 'INPUT' ||\n // clicking

    '\n        + (escaped ? code : escapeHtmlEntities(code, true))\n        + '
    \\n' as RendererOutput;\n }\n\n return '
    '\n      + (escaped ? code : escapeHtmlEntities(code, true))\n      + '
    \\n' as RendererOutput;\n }\n\n blockquote({ tokens }: Tokens.Blockquote): RendererOutput {\n const body = this.parser.parse(tokens);\n return `
    \\n${body}
    \\n` as RendererOutput;\n }\n\n html({ text }: Tokens.HTML | Tokens.Tag): RendererOutput {\n return text as RendererOutput;\n }\n\n def(token: Tokens.Def): RendererOutput {\n return '' as RendererOutput;\n }\n\n heading({ tokens, depth }: Tokens.Heading): RendererOutput {\n return `${this.parser.parseInline(tokens)}\\n` as RendererOutput;\n }\n\n hr(token: Tokens.Hr): RendererOutput {\n return '
    \\n' as RendererOutput;\n }\n\n list(token: Tokens.List): RendererOutput {\n const ordered = token.ordered;\n const start = token.start;\n\n let body = '';\n for (let j = 0; j < token.items.length; j++) {\n const item = token.items[j];\n body += this.listitem(item);\n }\n\n const type = ordered ? 'ol' : 'ul';\n const startAttr = (ordered && start !== 1) ? (' start=\"' + start + '\"') : '';\n return '<' + type + startAttr + '>\\n' + body + '\\n' as RendererOutput;\n }\n\n listitem(item: Tokens.ListItem): RendererOutput {\n return `
  • ${this.parser.parse(item.tokens)}
  • \\n` as RendererOutput;\n }\n\n checkbox({ checked }: Tokens.Checkbox): RendererOutput {\n return ' ' as RendererOutput;\n }\n\n paragraph({ tokens }: Tokens.Paragraph): RendererOutput {\n return `

    ${this.parser.parseInline(tokens)}

    \\n` as RendererOutput;\n }\n\n table(token: Tokens.Table): RendererOutput {\n let header = '';\n\n // header\n let cell = '';\n for (let j = 0; j < token.header.length; j++) {\n cell += this.tablecell(token.header[j]);\n }\n header += this.tablerow({ text: cell as ParserOutput });\n\n let body = '';\n for (let j = 0; j < token.rows.length; j++) {\n const row = token.rows[j];\n\n cell = '';\n for (let k = 0; k < row.length; k++) {\n cell += this.tablecell(row[k]);\n }\n\n body += this.tablerow({ text: cell as ParserOutput });\n }\n if (body) body = `${body}`;\n\n return '\\n'\n + '\\n'\n + header\n + '\\n'\n + body\n + '
    \\n' as RendererOutput;\n }\n\n tablerow({ text }: Tokens.TableRow): RendererOutput {\n return `\\n${text}\\n` as RendererOutput;\n }\n\n tablecell(token: Tokens.TableCell): RendererOutput {\n const content = this.parser.parseInline(token.tokens);\n const type = token.header ? 'th' : 'td';\n const tag = token.align\n ? `<${type} align=\"${token.align}\">`\n : `<${type}>`;\n return tag + content + `\\n` as RendererOutput;\n }\n\n /**\n * span level renderer\n */\n strong({ tokens }: Tokens.Strong): RendererOutput {\n return `${this.parser.parseInline(tokens)}` as RendererOutput;\n }\n\n em({ tokens }: Tokens.Em): RendererOutput {\n return `${this.parser.parseInline(tokens)}` as RendererOutput;\n }\n\n codespan({ text }: Tokens.Codespan): RendererOutput {\n return `${escapeHtmlEntities(text, true)}` as RendererOutput;\n }\n\n br(token: Tokens.Br): RendererOutput {\n return '
    ' as RendererOutput;\n }\n\n del({ tokens }: Tokens.Del): RendererOutput {\n return `${this.parser.parseInline(tokens)}` as RendererOutput;\n }\n\n link({ href, title, tokens }: Tokens.Link): RendererOutput {\n const text = this.parser.parseInline(tokens) as string;\n const cleanHref = cleanUrl(href);\n if (cleanHref === null) {\n return text as RendererOutput;\n }\n href = cleanHref;\n let out = '
    ';\n return out as RendererOutput;\n }\n\n image({ href, title, text, tokens }: Tokens.Image): RendererOutput {\n if (tokens) {\n text = this.parser.parseInline(tokens, this.parser.textRenderer) as string;\n }\n const cleanHref = cleanUrl(href);\n if (cleanHref === null) {\n return escapeHtmlEntities(text) as RendererOutput;\n }\n href = cleanHref;\n\n let out = `\"${escapeHtmlEntities(text)}\"`;\n {\n // no need for block level renderers\n strong({ text }: Tokens.Strong): RendererOutput {\n return text as RendererOutput;\n }\n\n em({ text }: Tokens.Em): RendererOutput {\n return text as RendererOutput;\n }\n\n codespan({ text }: Tokens.Codespan): RendererOutput {\n return text as RendererOutput;\n }\n\n del({ text }: Tokens.Del): RendererOutput {\n return text as RendererOutput;\n }\n\n html({ text }: Tokens.HTML | Tokens.Tag): RendererOutput {\n return text as RendererOutput;\n }\n\n text({ text }: Tokens.Text | Tokens.Escape | Tokens.Tag): RendererOutput {\n return text as RendererOutput;\n }\n\n link({ text }: Tokens.Link): RendererOutput {\n return '' + text as RendererOutput;\n }\n\n image({ text }: Tokens.Image): RendererOutput {\n return '' + text as RendererOutput;\n }\n\n br(): RendererOutput {\n return '' as RendererOutput;\n }\n\n checkbox({ raw }: Tokens.Checkbox): RendererOutput {\n return raw as RendererOutput;\n }\n}\n", "import { _Renderer } from './Renderer.ts';\nimport { _TextRenderer } from './TextRenderer.ts';\nimport { _defaults } from './defaults.ts';\nimport type { MarkedToken, Token, Tokens } from './Tokens.ts';\nimport type { MarkedOptions } from './MarkedOptions.ts';\n\n/**\n * Parsing & Compiling\n */\nexport class _Parser {\n options: MarkedOptions;\n renderer: _Renderer;\n textRenderer: _TextRenderer;\n constructor(options?: MarkedOptions) {\n this.options = options || _defaults;\n this.options.renderer = this.options.renderer || new _Renderer();\n this.renderer = this.options.renderer;\n this.renderer.options = this.options;\n this.renderer.parser = this;\n this.textRenderer = new _TextRenderer();\n }\n\n /**\n * Static Parse Method\n */\n static parse(tokens: Token[], options?: MarkedOptions) {\n const parser = new _Parser(options);\n return parser.parse(tokens);\n }\n\n /**\n * Static Parse Inline Method\n */\n static parseInline(tokens: Token[], options?: MarkedOptions) {\n const parser = new _Parser(options);\n return parser.parseInline(tokens);\n }\n\n /**\n * Parse Loop\n */\n parse(tokens: Token[]): ParserOutput {\n let out = '';\n\n for (let i = 0; i < tokens.length; i++) {\n const anyToken = tokens[i];\n\n // Run any renderer extensions\n if (this.options.extensions?.renderers?.[anyToken.type]) {\n const genericToken = anyToken as Tokens.Generic;\n const ret = this.options.extensions.renderers[genericToken.type].call({ parser: this }, genericToken);\n if (ret !== false || !['space', 'hr', 'heading', 'code', 'table', 'blockquote', 'list', 'html', 'def', 'paragraph', 'text'].includes(genericToken.type)) {\n out += ret || '';\n continue;\n }\n }\n\n const token = anyToken as MarkedToken;\n\n switch (token.type) {\n case 'space': {\n out += this.renderer.space(token);\n break;\n }\n case 'hr': {\n out += this.renderer.hr(token);\n break;\n }\n case 'heading': {\n out += this.renderer.heading(token);\n break;\n }\n case 'code': {\n out += this.renderer.code(token);\n break;\n }\n case 'table': {\n out += this.renderer.table(token);\n break;\n }\n case 'blockquote': {\n out += this.renderer.blockquote(token);\n break;\n }\n case 'list': {\n out += this.renderer.list(token);\n break;\n }\n case 'checkbox': {\n out += this.renderer.checkbox(token);\n break;\n }\n case 'html': {\n out += this.renderer.html(token);\n break;\n }\n case 'def': {\n out += this.renderer.def(token);\n break;\n }\n case 'paragraph': {\n out += this.renderer.paragraph(token);\n break;\n }\n case 'text': {\n out += this.renderer.text(token);\n break;\n }\n\n default: {\n const errMsg = 'Token with \"' + token.type + '\" type was not found.';\n if (this.options.silent) {\n console.error(errMsg);\n return '' as ParserOutput;\n } else {\n throw new Error(errMsg);\n }\n }\n }\n }\n\n return out as ParserOutput;\n }\n\n /**\n * Parse Inline Tokens\n */\n parseInline(tokens: Token[], renderer: _Renderer | _TextRenderer = this.renderer): ParserOutput {\n let out = '';\n\n for (let i = 0; i < tokens.length; i++) {\n const anyToken = tokens[i];\n\n // Run any renderer extensions\n if (this.options.extensions?.renderers?.[anyToken.type]) {\n const ret = this.options.extensions.renderers[anyToken.type].call({ parser: this }, anyToken);\n if (ret !== false || !['escape', 'html', 'link', 'image', 'strong', 'em', 'codespan', 'br', 'del', 'text'].includes(anyToken.type)) {\n out += ret || '';\n continue;\n }\n }\n\n const token = anyToken as MarkedToken;\n\n switch (token.type) {\n case 'escape': {\n out += renderer.text(token);\n break;\n }\n case 'html': {\n out += renderer.html(token);\n break;\n }\n case 'link': {\n out += renderer.link(token);\n break;\n }\n case 'image': {\n out += renderer.image(token);\n break;\n }\n case 'checkbox': {\n out += renderer.checkbox(token);\n break;\n }\n case 'strong': {\n out += renderer.strong(token);\n break;\n }\n case 'em': {\n out += renderer.em(token);\n break;\n }\n case 'codespan': {\n out += renderer.codespan(token);\n break;\n }\n case 'br': {\n out += renderer.br(token);\n break;\n }\n case 'del': {\n out += renderer.del(token);\n break;\n }\n case 'text': {\n out += renderer.text(token);\n break;\n }\n default: {\n const errMsg = 'Token with \"' + token.type + '\" type was not found.';\n if (this.options.silent) {\n console.error(errMsg);\n return '' as ParserOutput;\n } else {\n throw new Error(errMsg);\n }\n }\n }\n }\n return out as ParserOutput;\n }\n}\n", "import { _defaults } from './defaults.ts';\nimport { _Lexer } from './Lexer.ts';\nimport { _Parser } from './Parser.ts';\nimport type { MarkedOptions } from './MarkedOptions.ts';\nimport type { Token, TokensList } from './Tokens.ts';\n\nexport class _Hooks {\n options: MarkedOptions;\n block?: boolean;\n\n constructor(options?: MarkedOptions) {\n this.options = options || _defaults;\n }\n\n static passThroughHooks = new Set([\n 'preprocess',\n 'postprocess',\n 'processAllTokens',\n 'emStrongMask',\n ]);\n\n static passThroughHooksRespectAsync = new Set([\n 'preprocess',\n 'postprocess',\n 'processAllTokens',\n ]);\n\n /**\n * Process markdown before marked\n */\n preprocess(markdown: string) {\n return markdown;\n }\n\n /**\n * Process HTML after marked is finished\n */\n postprocess(html: ParserOutput) {\n return html;\n }\n\n /**\n * Process all tokens before walk tokens\n */\n processAllTokens(tokens: Token[] | TokensList) {\n return tokens;\n }\n\n /**\n * Mask contents that should not be interpreted as em/strong delimiters\n */\n emStrongMask(src: string) {\n return src;\n }\n\n /**\n * Provide function to tokenize markdown\n */\n provideLexer() {\n return this.block ? _Lexer.lex : _Lexer.lexInline;\n }\n\n /**\n * Provide function to parse tokens\n */\n provideParser() {\n return this.block ? _Parser.parse : _Parser.parseInline;\n }\n}\n", "import { _getDefaults } from './defaults.ts';\nimport { _Lexer } from './Lexer.ts';\nimport { _Parser } from './Parser.ts';\nimport { _Hooks } from './Hooks.ts';\nimport { _Renderer } from './Renderer.ts';\nimport { _Tokenizer } from './Tokenizer.ts';\nimport { _TextRenderer } from './TextRenderer.ts';\nimport { escapeHtmlEntities } from './helpers.ts';\nimport type { MarkedExtension, MarkedOptions } from './MarkedOptions.ts';\nimport type { Token, Tokens, TokensList } from './Tokens.ts';\n\nexport type MaybePromise = void | Promise;\n\ntype UnknownFunction = (...args: unknown[]) => unknown;\ntype GenericRendererFunction = (...args: unknown[]) => string | false;\n\nexport class Marked {\n defaults = _getDefaults();\n options = this.setOptions;\n\n parse = this.parseMarkdown(true);\n parseInline = this.parseMarkdown(false);\n\n Parser = _Parser;\n Renderer = _Renderer;\n TextRenderer = _TextRenderer;\n Lexer = _Lexer;\n Tokenizer = _Tokenizer;\n Hooks = _Hooks;\n\n constructor(...args: MarkedExtension[]) {\n this.use(...args);\n }\n\n /**\n * Run callback for every token\n */\n walkTokens(tokens: Token[] | TokensList, callback: (token: Token) => MaybePromise | MaybePromise[]) {\n let values: MaybePromise[] = [];\n for (const token of tokens) {\n values = values.concat(callback.call(this, token));\n switch (token.type) {\n case 'table': {\n const tableToken = token as Tokens.Table;\n for (const cell of tableToken.header) {\n values = values.concat(this.walkTokens(cell.tokens, callback));\n }\n for (const row of tableToken.rows) {\n for (const cell of row) {\n values = values.concat(this.walkTokens(cell.tokens, callback));\n }\n }\n break;\n }\n case 'list': {\n const listToken = token as Tokens.List;\n values = values.concat(this.walkTokens(listToken.items, callback));\n break;\n }\n default: {\n const genericToken = token as Tokens.Generic;\n if (this.defaults.extensions?.childTokens?.[genericToken.type]) {\n this.defaults.extensions.childTokens[genericToken.type].forEach((childTokens) => {\n const tokens = genericToken[childTokens].flat(Infinity) as Token[] | TokensList;\n values = values.concat(this.walkTokens(tokens, callback));\n });\n } else if (genericToken.tokens) {\n values = values.concat(this.walkTokens(genericToken.tokens, callback));\n }\n }\n }\n }\n return values;\n }\n\n use(...args: MarkedExtension[]) {\n const extensions: MarkedOptions['extensions'] = this.defaults.extensions || { renderers: {}, childTokens: {} };\n\n args.forEach((pack) => {\n // copy options to new object\n const opts = { ...pack } as MarkedOptions;\n\n // set async to true if it was set to true before\n opts.async = this.defaults.async || opts.async || false;\n\n // ==-- Parse \"addon\" extensions --== //\n if (pack.extensions) {\n pack.extensions.forEach((ext) => {\n if (!ext.name) {\n throw new Error('extension name required');\n }\n if ('renderer' in ext) { // Renderer extensions\n const prevRenderer = extensions.renderers[ext.name];\n if (prevRenderer) {\n // Replace extension with func to run new extension but fall back if false\n extensions.renderers[ext.name] = function(...args) {\n let ret = ext.renderer.apply(this, args);\n if (ret === false) {\n ret = prevRenderer.apply(this, args);\n }\n return ret;\n };\n } else {\n extensions.renderers[ext.name] = ext.renderer;\n }\n }\n if ('tokenizer' in ext) { // Tokenizer Extensions\n if (!ext.level || (ext.level !== 'block' && ext.level !== 'inline')) {\n throw new Error(\"extension level must be 'block' or 'inline'\");\n }\n const extLevel = extensions[ext.level];\n if (extLevel) {\n extLevel.unshift(ext.tokenizer);\n } else {\n extensions[ext.level] = [ext.tokenizer];\n }\n if (ext.start) { // Function to check for start of token\n if (ext.level === 'block') {\n if (extensions.startBlock) {\n extensions.startBlock.push(ext.start);\n } else {\n extensions.startBlock = [ext.start];\n }\n } else if (ext.level === 'inline') {\n if (extensions.startInline) {\n extensions.startInline.push(ext.start);\n } else {\n extensions.startInline = [ext.start];\n }\n }\n }\n }\n if ('childTokens' in ext && ext.childTokens) { // Child tokens to be visited by walkTokens\n extensions.childTokens[ext.name] = ext.childTokens;\n }\n });\n opts.extensions = extensions;\n }\n\n // ==-- Parse \"overwrite\" extensions --== //\n if (pack.renderer) {\n const renderer = this.defaults.renderer || new _Renderer(this.defaults);\n for (const prop in pack.renderer) {\n if (!(prop in renderer)) {\n throw new Error(`renderer '${prop}' does not exist`);\n }\n if (['options', 'parser'].includes(prop)) {\n // ignore options property\n continue;\n }\n const rendererProp = prop as Exclude, 'options' | 'parser'>;\n const rendererFunc = pack.renderer[rendererProp] as GenericRendererFunction;\n const prevRenderer = renderer[rendererProp] as GenericRendererFunction;\n // Replace renderer with func to run extension, but fall back if false\n renderer[rendererProp] = (...args: unknown[]) => {\n let ret = rendererFunc.apply(renderer, args);\n if (ret === false) {\n ret = prevRenderer.apply(renderer, args);\n }\n return (ret || '') as RendererOutput;\n };\n }\n opts.renderer = renderer;\n }\n if (pack.tokenizer) {\n const tokenizer = this.defaults.tokenizer || new _Tokenizer(this.defaults);\n for (const prop in pack.tokenizer) {\n if (!(prop in tokenizer)) {\n throw new Error(`tokenizer '${prop}' does not exist`);\n }\n if (['options', 'rules', 'lexer'].includes(prop)) {\n // ignore options, rules, and lexer properties\n continue;\n }\n const tokenizerProp = prop as Exclude, 'options' | 'rules' | 'lexer'>;\n const tokenizerFunc = pack.tokenizer[tokenizerProp] as UnknownFunction;\n const prevTokenizer = tokenizer[tokenizerProp] as UnknownFunction;\n // Replace tokenizer with func to run extension, but fall back if false\n // @ts-expect-error cannot type tokenizer function dynamically\n tokenizer[tokenizerProp] = (...args: unknown[]) => {\n let ret = tokenizerFunc.apply(tokenizer, args);\n if (ret === false) {\n ret = prevTokenizer.apply(tokenizer, args);\n }\n return ret;\n };\n }\n opts.tokenizer = tokenizer;\n }\n\n // ==-- Parse Hooks extensions --== //\n if (pack.hooks) {\n const hooks = this.defaults.hooks || new _Hooks();\n for (const prop in pack.hooks) {\n if (!(prop in hooks)) {\n throw new Error(`hook '${prop}' does not exist`);\n }\n if (['options', 'block'].includes(prop)) {\n // ignore options and block properties\n continue;\n }\n const hooksProp = prop as Exclude, 'options' | 'block'>;\n const hooksFunc = pack.hooks[hooksProp] as UnknownFunction;\n const prevHook = hooks[hooksProp] as UnknownFunction;\n if (_Hooks.passThroughHooks.has(prop)) {\n // @ts-expect-error cannot type hook function dynamically\n hooks[hooksProp] = (arg: unknown) => {\n if (this.defaults.async && _Hooks.passThroughHooksRespectAsync.has(prop)) {\n return (async() => {\n const ret = await hooksFunc.call(hooks, arg);\n return prevHook.call(hooks, ret);\n })();\n }\n\n const ret = hooksFunc.call(hooks, arg);\n return prevHook.call(hooks, ret);\n };\n } else {\n // @ts-expect-error cannot type hook function dynamically\n hooks[hooksProp] = (...args: unknown[]) => {\n if (this.defaults.async) {\n return (async() => {\n let ret = await hooksFunc.apply(hooks, args);\n if (ret === false) {\n ret = await prevHook.apply(hooks, args);\n }\n return ret;\n })();\n }\n\n let ret = hooksFunc.apply(hooks, args);\n if (ret === false) {\n ret = prevHook.apply(hooks, args);\n }\n return ret;\n };\n }\n }\n opts.hooks = hooks;\n }\n\n // ==-- Parse WalkTokens extensions --== //\n if (pack.walkTokens) {\n const walkTokens = this.defaults.walkTokens;\n const packWalktokens = pack.walkTokens;\n opts.walkTokens = function(token) {\n let values: MaybePromise[] = [];\n values.push(packWalktokens.call(this, token));\n if (walkTokens) {\n values = values.concat(walkTokens.call(this, token));\n }\n return values;\n };\n }\n\n this.defaults = { ...this.defaults, ...opts };\n });\n\n return this;\n }\n\n setOptions(opt: MarkedOptions) {\n this.defaults = { ...this.defaults, ...opt };\n return this;\n }\n\n lexer(src: string, options?: MarkedOptions) {\n return _Lexer.lex(src, options ?? this.defaults);\n }\n\n parser(tokens: Token[], options?: MarkedOptions) {\n return _Parser.parse(tokens, options ?? this.defaults);\n }\n\n private parseMarkdown(blockType: boolean) {\n type overloadedParse = {\n (src: string, options: MarkedOptions & { async: true }): Promise;\n (src: string, options: MarkedOptions & { async: false }): ParserOutput;\n (src: string, options?: MarkedOptions | null): ParserOutput | Promise;\n };\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const parse: overloadedParse = (src: string, options?: MarkedOptions | null): any => {\n const origOpt = { ...options };\n const opt = { ...this.defaults, ...origOpt };\n\n const throwError = this.onError(!!opt.silent, !!opt.async);\n\n // throw error if an extension set async to true but parse was called with async: false\n if (this.defaults.async === true && origOpt.async === false) {\n return throwError(new Error('marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.'));\n }\n\n // throw error in case of non string input\n if (typeof src === 'undefined' || src === null) {\n return throwError(new Error('marked(): input parameter is undefined or null'));\n }\n if (typeof src !== 'string') {\n return throwError(new Error('marked(): input parameter is of type '\n + Object.prototype.toString.call(src) + ', string expected'));\n }\n\n if (opt.hooks) {\n opt.hooks.options = opt;\n opt.hooks.block = blockType;\n }\n\n if (opt.async) {\n return (async() => {\n const processedSrc = opt.hooks ? await opt.hooks.preprocess(src) : src;\n const lexer = opt.hooks ? await opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline);\n const tokens = await lexer(processedSrc, opt);\n const processedTokens = opt.hooks ? await opt.hooks.processAllTokens(tokens) : tokens;\n if (opt.walkTokens) {\n await Promise.all(this.walkTokens(processedTokens, opt.walkTokens));\n }\n const parser = opt.hooks ? await opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline);\n const html = await parser(processedTokens, opt);\n return opt.hooks ? await opt.hooks.postprocess(html) : html;\n })().catch(throwError);\n }\n\n try {\n if (opt.hooks) {\n src = opt.hooks.preprocess(src) as string;\n }\n const lexer = opt.hooks ? opt.hooks.provideLexer() : (blockType ? _Lexer.lex : _Lexer.lexInline);\n let tokens = lexer(src, opt);\n if (opt.hooks) {\n tokens = opt.hooks.processAllTokens(tokens);\n }\n if (opt.walkTokens) {\n this.walkTokens(tokens, opt.walkTokens);\n }\n const parser = opt.hooks ? opt.hooks.provideParser() : (blockType ? _Parser.parse : _Parser.parseInline);\n let html = parser(tokens, opt);\n if (opt.hooks) {\n html = opt.hooks.postprocess(html);\n }\n return html;\n } catch(e) {\n return throwError(e as Error);\n }\n };\n\n return parse;\n }\n\n private onError(silent: boolean, async: boolean) {\n return (e: Error): string | Promise => {\n e.message += '\\nPlease report this to https://github.com/markedjs/marked.';\n\n if (silent) {\n const msg = '

    An error occurred:

    '\n          + escapeHtmlEntities(e.message + '', true)\n          + '
    ';\n if (async) {\n return Promise.resolve(msg);\n }\n return msg;\n }\n\n if (async) {\n return Promise.reject(e);\n }\n throw e;\n };\n }\n}\n", "import { _Lexer } from './Lexer.ts';\nimport { _Parser } from './Parser.ts';\nimport { _Tokenizer } from './Tokenizer.ts';\nimport { _Renderer } from './Renderer.ts';\nimport { _TextRenderer } from './TextRenderer.ts';\nimport { _Hooks } from './Hooks.ts';\nimport { Marked } from './Instance.ts';\nimport {\n _getDefaults,\n changeDefaults,\n _defaults,\n} from './defaults.ts';\nimport type { MarkedExtension, MarkedOptions } from './MarkedOptions.ts';\nimport type { Token, TokensList } from './Tokens.ts';\nimport type { MaybePromise } from './Instance.ts';\n\nconst markedInstance = new Marked();\n\n/**\n * Compiles markdown to HTML asynchronously.\n *\n * @param src String of markdown source to be compiled\n * @param options Hash of options, having async: true\n * @return Promise of string of compiled HTML\n */\nexport function marked(src: string, options: MarkedOptions & { async: true }): Promise;\n\n/**\n * Compiles markdown to HTML.\n *\n * @param src String of markdown source to be compiled\n * @param options Optional hash of options\n * @return String of compiled HTML. Will be a Promise of string if async is set to true by any extensions.\n */\nexport function marked(src: string, options: MarkedOptions & { async: false }): string;\nexport function marked(src: string, options: MarkedOptions & { async: true }): Promise;\nexport function marked(src: string, options?: MarkedOptions | null): string | Promise;\nexport function marked(src: string, opt?: MarkedOptions | null): string | Promise {\n return markedInstance.parse(src, opt);\n}\n\n/**\n * Sets the default options.\n *\n * @param options Hash of options\n */\nmarked.options =\n marked.setOptions = function(options: MarkedOptions) {\n markedInstance.setOptions(options);\n marked.defaults = markedInstance.defaults;\n changeDefaults(marked.defaults);\n return marked;\n };\n\n/**\n * Gets the original marked default options.\n */\nmarked.getDefaults = _getDefaults;\n\nmarked.defaults = _defaults;\n\n/**\n * Use Extension\n */\n\nmarked.use = function(...args: MarkedExtension[]) {\n markedInstance.use(...args);\n marked.defaults = markedInstance.defaults;\n changeDefaults(marked.defaults);\n return marked;\n};\n\n/**\n * Run callback for every token\n */\n\nmarked.walkTokens = function(tokens: Token[] | TokensList, callback: (token: Token) => MaybePromise | MaybePromise[]) {\n return markedInstance.walkTokens(tokens, callback);\n};\n\n/**\n * Compiles markdown to HTML without enclosing `p` tag.\n *\n * @param src String of markdown source to be compiled\n * @param options Hash of options\n * @return String of compiled HTML\n */\nmarked.parseInline = markedInstance.parseInline;\n\n/**\n * Expose\n */\nmarked.Parser = _Parser;\nmarked.parser = _Parser.parse;\nmarked.Renderer = _Renderer;\nmarked.TextRenderer = _TextRenderer;\nmarked.Lexer = _Lexer;\nmarked.lexer = _Lexer.lex;\nmarked.Tokenizer = _Tokenizer;\nmarked.Hooks = _Hooks;\nmarked.parse = marked;\n\nexport const options = marked.options;\nexport const setOptions = marked.setOptions;\nexport const use = marked.use;\nexport const walkTokens = marked.walkTokens;\nexport const parseInline = marked.parseInline;\nexport const parse = marked;\nexport const parser = _Parser.parse;\nexport const lexer = _Lexer.lex;\nexport { _defaults as defaults, _getDefaults as getDefaults } from './defaults.ts';\nexport { _Lexer as Lexer } from './Lexer.ts';\nexport { _Parser as Parser } from './Parser.ts';\nexport { _Tokenizer as Tokenizer } from './Tokenizer.ts';\nexport { _Renderer as Renderer } from './Renderer.ts';\nexport { _TextRenderer as TextRenderer } from './TextRenderer.ts';\nexport { _Hooks as Hooks } from './Hooks.ts';\nexport { Marked } from './Instance.ts';\nexport type * from './MarkedOptions.ts';\nexport type * from './Tokens.ts';\n", "\nconst SHIFT_LEFT_32 = (1 << 16) * (1 << 16);\nconst SHIFT_RIGHT_32 = 1 / SHIFT_LEFT_32;\n\n// Threshold chosen based on both benchmarking and knowledge about browser string\n// data structures (which currently switch structure types at 12 bytes or more)\nconst TEXT_DECODER_MIN_LENGTH = 12;\nconst utf8TextDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8');\n\nconst PBF_VARINT = 0; // varint: int32, int64, uint32, uint64, sint32, sint64, bool, enum\nconst PBF_FIXED64 = 1; // 64-bit: double, fixed64, sfixed64\nconst PBF_BYTES = 2; // length-delimited: string, bytes, embedded messages, packed repeated fields\nconst PBF_FIXED32 = 5; // 32-bit: float, fixed32, sfixed32\n\nexport default class Pbf {\n /**\n * @param {Uint8Array | ArrayBuffer} [buf]\n */\n constructor(buf = new Uint8Array(16)) {\n this.buf = ArrayBuffer.isView(buf) ? buf : new Uint8Array(buf);\n this.dataView = new DataView(this.buf.buffer);\n this.pos = 0;\n this.type = 0;\n this.length = this.buf.length;\n }\n\n // === READING =================================================================\n\n /**\n * @template T\n * @param {(tag: number, result: T, pbf: Pbf) => void} readField\n * @param {T} result\n * @param {number} [end]\n */\n readFields(readField, result, end = this.length) {\n while (this.pos < end) {\n const val = this.readVarint(),\n tag = val >> 3,\n startPos = this.pos;\n\n this.type = val & 0x7;\n readField(tag, result, this);\n\n if (this.pos === startPos) this.skip(val);\n }\n return result;\n }\n\n /**\n * @template T\n * @param {(tag: number, result: T, pbf: Pbf) => void} readField\n * @param {T} result\n */\n readMessage(readField, result) {\n return this.readFields(readField, result, this.readVarint() + this.pos);\n }\n\n readFixed32() {\n const val = this.dataView.getUint32(this.pos, true);\n this.pos += 4;\n return val;\n }\n\n readSFixed32() {\n const val = this.dataView.getInt32(this.pos, true);\n this.pos += 4;\n return val;\n }\n\n // 64-bit int handling is based on github.com/dpw/node-buffer-more-ints (MIT-licensed)\n\n readFixed64() {\n const val = this.dataView.getUint32(this.pos, true) + this.dataView.getUint32(this.pos + 4, true) * SHIFT_LEFT_32;\n this.pos += 8;\n return val;\n }\n\n readSFixed64() {\n const val = this.dataView.getUint32(this.pos, true) + this.dataView.getInt32(this.pos + 4, true) * SHIFT_LEFT_32;\n this.pos += 8;\n return val;\n }\n\n readFloat() {\n const val = this.dataView.getFloat32(this.pos, true);\n this.pos += 4;\n return val;\n }\n\n readDouble() {\n const val = this.dataView.getFloat64(this.pos, true);\n this.pos += 8;\n return val;\n }\n\n /**\n * @param {boolean} [isSigned]\n */\n readVarint(isSigned) {\n const buf = this.buf;\n let val, b;\n\n b = buf[this.pos++]; val = b & 0x7f; if (b < 0x80) return val;\n b = buf[this.pos++]; val |= (b & 0x7f) << 7; if (b < 0x80) return val;\n b = buf[this.pos++]; val |= (b & 0x7f) << 14; if (b < 0x80) return val;\n b = buf[this.pos++]; val |= (b & 0x7f) << 21; if (b < 0x80) return val;\n b = buf[this.pos]; val |= (b & 0x0f) << 28;\n\n return readVarintRemainder(val, isSigned, this);\n }\n\n readVarint64() { // for compatibility with v2.0.1\n return this.readVarint(true);\n }\n\n readSVarint() {\n const num = this.readVarint();\n return num % 2 === 1 ? (num + 1) / -2 : num / 2; // zigzag encoding\n }\n\n readBoolean() {\n return Boolean(this.readVarint());\n }\n\n readString() {\n const end = this.readVarint() + this.pos;\n const pos = this.pos;\n this.pos = end;\n\n if (end - pos >= TEXT_DECODER_MIN_LENGTH && utf8TextDecoder) {\n // longer strings are fast with the built-in browser TextDecoder API\n return utf8TextDecoder.decode(this.buf.subarray(pos, end));\n }\n // short strings are fast with our custom implementation\n return readUtf8(this.buf, pos, end);\n }\n\n readBytes() {\n const end = this.readVarint() + this.pos,\n buffer = this.buf.subarray(this.pos, end);\n this.pos = end;\n return buffer;\n }\n\n // verbose for performance reasons; doesn't affect gzipped size\n\n /**\n * @param {number[]} [arr]\n * @param {boolean} [isSigned]\n */\n readPackedVarint(arr = [], isSigned) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readVarint(isSigned));\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedSVarint(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readSVarint());\n return arr;\n }\n /** @param {boolean[]} [arr] */\n readPackedBoolean(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readBoolean());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedFloat(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readFloat());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedDouble(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readDouble());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedFixed32(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readFixed32());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedSFixed32(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readSFixed32());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedFixed64(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readFixed64());\n return arr;\n }\n /** @param {number[]} [arr] */\n readPackedSFixed64(arr = []) {\n const end = this.readPackedEnd();\n while (this.pos < end) arr.push(this.readSFixed64());\n return arr;\n }\n readPackedEnd() {\n return this.type === PBF_BYTES ? this.readVarint() + this.pos : this.pos + 1;\n }\n\n /** @param {number} val */\n skip(val) {\n const type = val & 0x7;\n if (type === PBF_VARINT) while (this.buf[this.pos++] > 0x7f) {}\n else if (type === PBF_BYTES) this.pos = this.readVarint() + this.pos;\n else if (type === PBF_FIXED32) this.pos += 4;\n else if (type === PBF_FIXED64) this.pos += 8;\n else throw new Error(`Unimplemented type: ${type}`);\n }\n\n // === WRITING =================================================================\n\n /**\n * @param {number} tag\n * @param {number} type\n */\n writeTag(tag, type) {\n this.writeVarint((tag << 3) | type);\n }\n\n /** @param {number} min */\n realloc(min) {\n let length = this.length || 16;\n\n while (length < this.pos + min) length *= 2;\n\n if (length !== this.length) {\n const buf = new Uint8Array(length);\n buf.set(this.buf);\n this.buf = buf;\n this.dataView = new DataView(buf.buffer);\n this.length = length;\n }\n }\n\n finish() {\n this.length = this.pos;\n this.pos = 0;\n return this.buf.subarray(0, this.length);\n }\n\n /** @param {number} val */\n writeFixed32(val) {\n this.realloc(4);\n this.dataView.setInt32(this.pos, val, true);\n this.pos += 4;\n }\n\n /** @param {number} val */\n writeSFixed32(val) {\n this.realloc(4);\n this.dataView.setInt32(this.pos, val, true);\n this.pos += 4;\n }\n\n /** @param {number} val */\n writeFixed64(val) {\n this.realloc(8);\n this.dataView.setInt32(this.pos, val & -1, true);\n this.dataView.setInt32(this.pos + 4, Math.floor(val * SHIFT_RIGHT_32), true);\n this.pos += 8;\n }\n\n /** @param {number} val */\n writeSFixed64(val) {\n this.realloc(8);\n this.dataView.setInt32(this.pos, val & -1, true);\n this.dataView.setInt32(this.pos + 4, Math.floor(val * SHIFT_RIGHT_32), true);\n this.pos += 8;\n }\n\n /** @param {number} val */\n writeVarint(val) {\n val = +val || 0;\n\n if (val > 0xfffffff || val < 0) {\n writeBigVarint(val, this);\n return;\n }\n\n this.realloc(4);\n\n this.buf[this.pos++] = val & 0x7f | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;\n this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;\n this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;\n this.buf[this.pos++] = (val >>> 7) & 0x7f;\n }\n\n /** @param {number} val */\n writeSVarint(val) {\n this.writeVarint(val < 0 ? -val * 2 - 1 : val * 2);\n }\n\n /** @param {boolean} val */\n writeBoolean(val) {\n this.writeVarint(+val);\n }\n\n /** @param {string} str */\n writeString(str) {\n str = String(str);\n this.realloc(str.length * 4);\n\n this.pos++; // reserve 1 byte for short string length\n\n const startPos = this.pos;\n // write the string directly to the buffer and see how much was written\n this.pos = writeUtf8(this.buf, str, this.pos);\n const len = this.pos - startPos;\n\n if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);\n\n // finally, write the message length in the reserved place and restore the position\n this.pos = startPos - 1;\n this.writeVarint(len);\n this.pos += len;\n }\n\n /** @param {number} val */\n writeFloat(val) {\n this.realloc(4);\n this.dataView.setFloat32(this.pos, val, true);\n this.pos += 4;\n }\n\n /** @param {number} val */\n writeDouble(val) {\n this.realloc(8);\n this.dataView.setFloat64(this.pos, val, true);\n this.pos += 8;\n }\n\n /** @param {Uint8Array} buffer */\n writeBytes(buffer) {\n const len = buffer.length;\n this.writeVarint(len);\n this.realloc(len);\n for (let i = 0; i < len; i++) this.buf[this.pos++] = buffer[i];\n }\n\n /**\n * @template T\n * @param {(obj: T, pbf: Pbf) => void} fn\n * @param {T} obj\n */\n writeRawMessage(fn, obj) {\n this.pos++; // reserve 1 byte for short message length\n\n // write the message directly to the buffer and see how much was written\n const startPos = this.pos;\n fn(obj, this);\n const len = this.pos - startPos;\n\n if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);\n\n // finally, write the message length in the reserved place and restore the position\n this.pos = startPos - 1;\n this.writeVarint(len);\n this.pos += len;\n }\n\n /**\n * @template T\n * @param {number} tag\n * @param {(obj: T, pbf: Pbf) => void} fn\n * @param {T} obj\n */\n writeMessage(tag, fn, obj) {\n this.writeTag(tag, PBF_BYTES);\n this.writeRawMessage(fn, obj);\n }\n\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedVarint(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedVarint, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedSVarint(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedSVarint, arr);\n }\n /**\n * @param {number} tag\n * @param {boolean[]} arr\n */\n writePackedBoolean(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedBoolean, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedFloat(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedFloat, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedDouble(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedDouble, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedFixed32(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedFixed32, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedSFixed32(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedSFixed32, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedFixed64(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedFixed64, arr);\n }\n /**\n * @param {number} tag\n * @param {number[]} arr\n */\n writePackedSFixed64(tag, arr) {\n if (arr.length) this.writeMessage(tag, writePackedSFixed64, arr);\n }\n\n /**\n * @param {number} tag\n * @param {Uint8Array} buffer\n */\n writeBytesField(tag, buffer) {\n this.writeTag(tag, PBF_BYTES);\n this.writeBytes(buffer);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeFixed32Field(tag, val) {\n this.writeTag(tag, PBF_FIXED32);\n this.writeFixed32(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeSFixed32Field(tag, val) {\n this.writeTag(tag, PBF_FIXED32);\n this.writeSFixed32(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeFixed64Field(tag, val) {\n this.writeTag(tag, PBF_FIXED64);\n this.writeFixed64(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeSFixed64Field(tag, val) {\n this.writeTag(tag, PBF_FIXED64);\n this.writeSFixed64(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeVarintField(tag, val) {\n this.writeTag(tag, PBF_VARINT);\n this.writeVarint(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeSVarintField(tag, val) {\n this.writeTag(tag, PBF_VARINT);\n this.writeSVarint(val);\n }\n /**\n * @param {number} tag\n * @param {string} str\n */\n writeStringField(tag, str) {\n this.writeTag(tag, PBF_BYTES);\n this.writeString(str);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeFloatField(tag, val) {\n this.writeTag(tag, PBF_FIXED32);\n this.writeFloat(val);\n }\n /**\n * @param {number} tag\n * @param {number} val\n */\n writeDoubleField(tag, val) {\n this.writeTag(tag, PBF_FIXED64);\n this.writeDouble(val);\n }\n /**\n * @param {number} tag\n * @param {boolean} val\n */\n writeBooleanField(tag, val) {\n this.writeVarintField(tag, +val);\n }\n};\n\n/**\n * @param {number} l\n * @param {boolean | undefined} s\n * @param {Pbf} p\n */\nfunction readVarintRemainder(l, s, p) {\n const buf = p.buf;\n let h, b;\n\n b = buf[p.pos++]; h = (b & 0x70) >> 4; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 3; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 10; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 17; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x7f) << 24; if (b < 0x80) return toNum(l, h, s);\n b = buf[p.pos++]; h |= (b & 0x01) << 31; if (b < 0x80) return toNum(l, h, s);\n\n throw new Error('Expected varint not more than 10 bytes');\n}\n\n/**\n * @param {number} low\n * @param {number} high\n * @param {boolean} [isSigned]\n */\nfunction toNum(low, high, isSigned) {\n return isSigned ? high * 0x100000000 + (low >>> 0) : ((high >>> 0) * 0x100000000) + (low >>> 0);\n}\n\n/**\n * @param {number} val\n * @param {Pbf} pbf\n */\nfunction writeBigVarint(val, pbf) {\n let low, high;\n\n if (val >= 0) {\n low = (val % 0x100000000) | 0;\n high = (val / 0x100000000) | 0;\n } else {\n low = ~(-val % 0x100000000);\n high = ~(-val / 0x100000000);\n\n if (low ^ 0xffffffff) {\n low = (low + 1) | 0;\n } else {\n low = 0;\n high = (high + 1) | 0;\n }\n }\n\n if (val >= 0x10000000000000000 || val < -0x10000000000000000) {\n throw new Error('Given varint doesn\\'t fit into 10 bytes');\n }\n\n pbf.realloc(10);\n\n writeBigVarintLow(low, high, pbf);\n writeBigVarintHigh(high, pbf);\n}\n\n/**\n * @param {number} high\n * @param {number} low\n * @param {Pbf} pbf\n */\nfunction writeBigVarintLow(low, high, pbf) {\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;\n pbf.buf[pbf.pos] = low & 0x7f;\n}\n\n/**\n * @param {number} high\n * @param {Pbf} pbf\n */\nfunction writeBigVarintHigh(high, pbf) {\n const lsb = (high & 0x07) << 4;\n\n pbf.buf[pbf.pos++] |= lsb | ((high >>>= 3) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;\n pbf.buf[pbf.pos++] = high & 0x7f;\n}\n\n/**\n * @param {number} startPos\n * @param {number} len\n * @param {Pbf} pbf\n */\nfunction makeRoomForExtraLength(startPos, len, pbf) {\n const extraLen =\n len <= 0x3fff ? 1 :\n len <= 0x1fffff ? 2 :\n len <= 0xfffffff ? 3 : Math.floor(Math.log(len) / (Math.LN2 * 7));\n\n // if 1 byte isn't enough for encoding message length, shift the data to the right\n pbf.realloc(extraLen);\n for (let i = pbf.pos - 1; i >= startPos; i--) pbf.buf[i + extraLen] = pbf.buf[i];\n}\n\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedVarint(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeVarint(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedSVarint(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeSVarint(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedFloat(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeFloat(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedDouble(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeDouble(arr[i]);\n}\n/**\n * @param {boolean[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedBoolean(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeBoolean(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedFixed32(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeFixed32(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedSFixed32(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeSFixed32(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedFixed64(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeFixed64(arr[i]);\n}\n/**\n * @param {number[]} arr\n * @param {Pbf} pbf\n */\nfunction writePackedSFixed64(arr, pbf) {\n for (let i = 0; i < arr.length; i++) pbf.writeSFixed64(arr[i]);\n}\n\n// Buffer code below from https://github.com/feross/buffer, MIT-licensed\n\n/**\n * @param {Uint8Array} buf\n * @param {number} pos\n * @param {number} end\n */\nfunction readUtf8(buf, pos, end) {\n let str = '';\n let i = pos;\n\n while (i < end) {\n const b0 = buf[i];\n let c = null; // codepoint\n let bytesPerSequence =\n b0 > 0xEF ? 4 :\n b0 > 0xDF ? 3 :\n b0 > 0xBF ? 2 : 1;\n\n if (i + bytesPerSequence > end) break;\n\n let b1, b2, b3;\n\n if (bytesPerSequence === 1) {\n if (b0 < 0x80) {\n c = b0;\n }\n } else if (bytesPerSequence === 2) {\n b1 = buf[i + 1];\n if ((b1 & 0xC0) === 0x80) {\n c = (b0 & 0x1F) << 0x6 | (b1 & 0x3F);\n if (c <= 0x7F) {\n c = null;\n }\n }\n } else if (bytesPerSequence === 3) {\n b1 = buf[i + 1];\n b2 = buf[i + 2];\n if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80) {\n c = (b0 & 0xF) << 0xC | (b1 & 0x3F) << 0x6 | (b2 & 0x3F);\n if (c <= 0x7FF || (c >= 0xD800 && c <= 0xDFFF)) {\n c = null;\n }\n }\n } else if (bytesPerSequence === 4) {\n b1 = buf[i + 1];\n b2 = buf[i + 2];\n b3 = buf[i + 3];\n if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80 && (b3 & 0xC0) === 0x80) {\n c = (b0 & 0xF) << 0x12 | (b1 & 0x3F) << 0xC | (b2 & 0x3F) << 0x6 | (b3 & 0x3F);\n if (c <= 0xFFFF || c >= 0x110000) {\n c = null;\n }\n }\n }\n\n if (c === null) {\n c = 0xFFFD;\n bytesPerSequence = 1;\n\n } else if (c > 0xFFFF) {\n c -= 0x10000;\n str += String.fromCharCode(c >>> 10 & 0x3FF | 0xD800);\n c = 0xDC00 | c & 0x3FF;\n }\n\n str += String.fromCharCode(c);\n i += bytesPerSequence;\n }\n\n return str;\n}\n\n/**\n * @param {Uint8Array} buf\n * @param {string} str\n * @param {number} pos\n */\nfunction writeUtf8(buf, str, pos) {\n for (let i = 0, c, lead; i < str.length; i++) {\n c = str.charCodeAt(i); // code point\n\n if (c > 0xD7FF && c < 0xE000) {\n if (lead) {\n if (c < 0xDC00) {\n buf[pos++] = 0xEF;\n buf[pos++] = 0xBF;\n buf[pos++] = 0xBD;\n lead = c;\n continue;\n } else {\n c = lead - 0xD800 << 10 | c - 0xDC00 | 0x10000;\n lead = null;\n }\n } else {\n if (c > 0xDBFF || (i + 1 === str.length)) {\n buf[pos++] = 0xEF;\n buf[pos++] = 0xBF;\n buf[pos++] = 0xBD;\n } else {\n lead = c;\n }\n continue;\n }\n } else if (lead) {\n buf[pos++] = 0xEF;\n buf[pos++] = 0xBF;\n buf[pos++] = 0xBD;\n lead = null;\n }\n\n if (c < 0x80) {\n buf[pos++] = c;\n } else {\n if (c < 0x800) {\n buf[pos++] = c >> 0x6 | 0xC0;\n } else {\n if (c < 0x10000) {\n buf[pos++] = c >> 0xC | 0xE0;\n } else {\n buf[pos++] = c >> 0x12 | 0xF0;\n buf[pos++] = c >> 0xC & 0x3F | 0x80;\n }\n buf[pos++] = c >> 0x6 & 0x3F | 0x80;\n }\n buf[pos++] = c & 0x3F | 0x80;\n }\n }\n return pos;\n}\n", "/**\n * A standalone point geometry with useful accessor, comparison, and\n * modification methods.\n *\n * @class\n * @param {number} x the x-coordinate. This could be longitude or screen pixels, or any other sort of unit.\n * @param {number} y the y-coordinate. This could be latitude or screen pixels, or any other sort of unit.\n *\n * @example\n * const point = new Point(-77, 38);\n */\nexport default function Point(x, y) {\n this.x = x;\n this.y = y;\n}\n\nPoint.prototype = {\n /**\n * Clone this point, returning a new point that can be modified\n * without affecting the old one.\n * @return {Point} the clone\n */\n clone() { return new Point(this.x, this.y); },\n\n /**\n * Add this point's x & y coordinates to another point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n add(p) { return this.clone()._add(p); },\n\n /**\n * Subtract this point's x & y coordinates to from point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n sub(p) { return this.clone()._sub(p); },\n\n /**\n * Multiply this point's x & y coordinates by point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n multByPoint(p) { return this.clone()._multByPoint(p); },\n\n /**\n * Divide this point's x & y coordinates by point,\n * yielding a new point.\n * @param {Point} p the other point\n * @return {Point} output point\n */\n divByPoint(p) { return this.clone()._divByPoint(p); },\n\n /**\n * Multiply this point's x & y coordinates by a factor,\n * yielding a new point.\n * @param {number} k factor\n * @return {Point} output point\n */\n mult(k) { return this.clone()._mult(k); },\n\n /**\n * Divide this point's x & y coordinates by a factor,\n * yielding a new point.\n * @param {number} k factor\n * @return {Point} output point\n */\n div(k) { return this.clone()._div(k); },\n\n /**\n * Rotate this point around the 0, 0 origin by an angle a,\n * given in radians\n * @param {number} a angle to rotate around, in radians\n * @return {Point} output point\n */\n rotate(a) { return this.clone()._rotate(a); },\n\n /**\n * Rotate this point around p point by an angle a,\n * given in radians\n * @param {number} a angle to rotate around, in radians\n * @param {Point} p Point to rotate around\n * @return {Point} output point\n */\n rotateAround(a, p) { return this.clone()._rotateAround(a, p); },\n\n /**\n * Multiply this point by a 4x1 transformation matrix\n * @param {[number, number, number, number]} m transformation matrix\n * @return {Point} output point\n */\n matMult(m) { return this.clone()._matMult(m); },\n\n /**\n * Calculate this point but as a unit vector from 0, 0, meaning\n * that the distance from the resulting point to the 0, 0\n * coordinate will be equal to 1 and the angle from the resulting\n * point to the 0, 0 coordinate will be the same as before.\n * @return {Point} unit vector point\n */\n unit() { return this.clone()._unit(); },\n\n /**\n * Compute a perpendicular point, where the new y coordinate\n * is the old x coordinate and the new x coordinate is the old y\n * coordinate multiplied by -1\n * @return {Point} perpendicular point\n */\n perp() { return this.clone()._perp(); },\n\n /**\n * Return a version of this point with the x & y coordinates\n * rounded to integers.\n * @return {Point} rounded point\n */\n round() { return this.clone()._round(); },\n\n /**\n * Return the magnitude of this point: this is the Euclidean\n * distance from the 0, 0 coordinate to this point's x and y\n * coordinates.\n * @return {number} magnitude\n */\n mag() {\n return Math.sqrt(this.x * this.x + this.y * this.y);\n },\n\n /**\n * Judge whether this point is equal to another point, returning\n * true or false.\n * @param {Point} other the other point\n * @return {boolean} whether the points are equal\n */\n equals(other) {\n return this.x === other.x &&\n this.y === other.y;\n },\n\n /**\n * Calculate the distance from this point to another point\n * @param {Point} p the other point\n * @return {number} distance\n */\n dist(p) {\n return Math.sqrt(this.distSqr(p));\n },\n\n /**\n * Calculate the distance from this point to another point,\n * without the square root step. Useful if you're comparing\n * relative distances.\n * @param {Point} p the other point\n * @return {number} distance\n */\n distSqr(p) {\n const dx = p.x - this.x,\n dy = p.y - this.y;\n return dx * dx + dy * dy;\n },\n\n /**\n * Get the angle from the 0, 0 coordinate to this point, in radians\n * coordinates.\n * @return {number} angle\n */\n angle() {\n return Math.atan2(this.y, this.x);\n },\n\n /**\n * Get the angle from this point to another point, in radians\n * @param {Point} b the other point\n * @return {number} angle\n */\n angleTo(b) {\n return Math.atan2(this.y - b.y, this.x - b.x);\n },\n\n /**\n * Get the angle between this point and another point, in radians\n * @param {Point} b the other point\n * @return {number} angle\n */\n angleWith(b) {\n return this.angleWithSep(b.x, b.y);\n },\n\n /**\n * Find the angle of the two vectors, solving the formula for\n * the cross product a x b = |a||b|sin(\u03B8) for \u03B8.\n * @param {number} x the x-coordinate\n * @param {number} y the y-coordinate\n * @return {number} the angle in radians\n */\n angleWithSep(x, y) {\n return Math.atan2(\n this.x * y - this.y * x,\n this.x * x + this.y * y);\n },\n\n /** @param {[number, number, number, number]} m */\n _matMult(m) {\n const x = m[0] * this.x + m[1] * this.y,\n y = m[2] * this.x + m[3] * this.y;\n this.x = x;\n this.y = y;\n return this;\n },\n\n /** @param {Point} p */\n _add(p) {\n this.x += p.x;\n this.y += p.y;\n return this;\n },\n\n /** @param {Point} p */\n _sub(p) {\n this.x -= p.x;\n this.y -= p.y;\n return this;\n },\n\n /** @param {number} k */\n _mult(k) {\n this.x *= k;\n this.y *= k;\n return this;\n },\n\n /** @param {number} k */\n _div(k) {\n this.x /= k;\n this.y /= k;\n return this;\n },\n\n /** @param {Point} p */\n _multByPoint(p) {\n this.x *= p.x;\n this.y *= p.y;\n return this;\n },\n\n /** @param {Point} p */\n _divByPoint(p) {\n this.x /= p.x;\n this.y /= p.y;\n return this;\n },\n\n _unit() {\n this._div(this.mag());\n return this;\n },\n\n _perp() {\n const y = this.y;\n this.y = this.x;\n this.x = -y;\n return this;\n },\n\n /** @param {number} angle */\n _rotate(angle) {\n const cos = Math.cos(angle),\n sin = Math.sin(angle),\n x = cos * this.x - sin * this.y,\n y = sin * this.x + cos * this.y;\n this.x = x;\n this.y = y;\n return this;\n },\n\n /**\n * @param {number} angle\n * @param {Point} p\n */\n _rotateAround(angle, p) {\n const cos = Math.cos(angle),\n sin = Math.sin(angle),\n x = p.x + cos * (this.x - p.x) - sin * (this.y - p.y),\n y = p.y + sin * (this.x - p.x) + cos * (this.y - p.y);\n this.x = x;\n this.y = y;\n return this;\n },\n\n _round() {\n this.x = Math.round(this.x);\n this.y = Math.round(this.y);\n return this;\n },\n\n constructor: Point\n};\n\n/**\n * Construct a point from an array if necessary, otherwise if the input\n * is already a Point, return it unchanged.\n * @param {Point | [number, number] | {x: number, y: number}} p input value\n * @return {Point} constructed point.\n * @example\n * // this\n * var point = Point.convert([0, 1]);\n * // is equivalent to\n * var point = new Point(0, 1);\n */\nPoint.convert = function (p) {\n if (p instanceof Point) {\n return /** @type {Point} */ (p);\n }\n if (Array.isArray(p)) {\n return new Point(+p[0], +p[1]);\n }\n if (p.x !== undefined && p.y !== undefined) {\n return new Point(+p.x, +p.y);\n }\n throw new Error('Expected [x, y] or {x, y} point format');\n};\n", "\nimport Point from '@mapbox/point-geometry';\n\n/** @import Pbf from 'pbf' */\n/** @import {Feature} from 'geojson' */\n\nexport class VectorTileFeature {\n /**\n * @param {Pbf} pbf\n * @param {number} end\n * @param {number} extent\n * @param {string[]} keys\n * @param {(number | string | boolean)[]} values\n */\n constructor(pbf, end, extent, keys, values) {\n // Public\n\n /** @type {Record} */\n this.properties = {};\n\n this.extent = extent;\n /** @type {0 | 1 | 2 | 3} */\n this.type = 0;\n\n /** @type {number | undefined} */\n this.id = undefined;\n\n /** @private */\n this._pbf = pbf;\n /** @private */\n this._geometry = -1;\n /** @private */\n this._keys = keys;\n /** @private */\n this._values = values;\n\n pbf.readFields(readFeature, this, end);\n }\n\n loadGeometry() {\n const pbf = this._pbf;\n pbf.pos = this._geometry;\n\n const end = pbf.readVarint() + pbf.pos;\n\n /** @type Point[][] */\n const lines = [];\n\n /** @type Point[] | undefined */\n let line;\n\n let cmd = 1;\n let length = 0;\n let x = 0;\n let y = 0;\n\n while (pbf.pos < end) {\n if (length <= 0) {\n const cmdLen = pbf.readVarint();\n cmd = cmdLen & 0x7;\n length = cmdLen >> 3;\n }\n\n length--;\n\n if (cmd === 1 || cmd === 2) {\n x += pbf.readSVarint();\n y += pbf.readSVarint();\n\n if (cmd === 1) { // moveTo\n if (line) lines.push(line);\n line = [];\n }\n\n if (line) line.push(new Point(x, y));\n\n } else if (cmd === 7) {\n\n // Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90\n if (line) {\n line.push(line[0].clone()); // closePolygon\n }\n\n } else {\n throw new Error(`unknown command ${cmd}`);\n }\n }\n\n if (line) lines.push(line);\n\n return lines;\n }\n\n bbox() {\n const pbf = this._pbf;\n pbf.pos = this._geometry;\n\n const end = pbf.readVarint() + pbf.pos;\n let cmd = 1,\n length = 0,\n x = 0,\n y = 0,\n x1 = Infinity,\n x2 = -Infinity,\n y1 = Infinity,\n y2 = -Infinity;\n\n while (pbf.pos < end) {\n if (length <= 0) {\n const cmdLen = pbf.readVarint();\n cmd = cmdLen & 0x7;\n length = cmdLen >> 3;\n }\n\n length--;\n\n if (cmd === 1 || cmd === 2) {\n x += pbf.readSVarint();\n y += pbf.readSVarint();\n if (x < x1) x1 = x;\n if (x > x2) x2 = x;\n if (y < y1) y1 = y;\n if (y > y2) y2 = y;\n\n } else if (cmd !== 7) {\n throw new Error(`unknown command ${cmd}`);\n }\n }\n\n return [x1, y1, x2, y2];\n }\n\n /**\n * @param {number} x\n * @param {number} y\n * @param {number} z\n * @return {Feature}\n */\n toGeoJSON(x, y, z) {\n const size = this.extent * Math.pow(2, z),\n x0 = this.extent * x,\n y0 = this.extent * y,\n vtCoords = this.loadGeometry();\n\n /** @param {Point} p */\n function projectPoint(p) {\n return [\n (p.x + x0) * 360 / size - 180,\n 360 / Math.PI * Math.atan(Math.exp((1 - (p.y + y0) * 2 / size) * Math.PI)) - 90\n ];\n }\n\n /** @param {Point[]} line */\n function projectLine(line) {\n return line.map(projectPoint);\n }\n\n /** @type {Feature[\"geometry\"]} */\n let geometry;\n\n if (this.type === 1) {\n const points = [];\n for (const line of vtCoords) {\n points.push(line[0]);\n }\n const coordinates = projectLine(points);\n geometry = points.length === 1 ?\n {type: 'Point', coordinates: coordinates[0]} :\n {type: 'MultiPoint', coordinates};\n\n } else if (this.type === 2) {\n\n const coordinates = vtCoords.map(projectLine);\n geometry = coordinates.length === 1 ?\n {type: 'LineString', coordinates: coordinates[0]} :\n {type: 'MultiLineString', coordinates};\n\n } else if (this.type === 3) {\n const polygons = classifyRings(vtCoords);\n const coordinates = [];\n for (const polygon of polygons) {\n coordinates.push(polygon.map(projectLine));\n }\n geometry = coordinates.length === 1 ?\n {type: 'Polygon', coordinates: coordinates[0]} :\n {type: 'MultiPolygon', coordinates};\n } else {\n\n throw new Error('unknown feature type');\n }\n\n /** @type {Feature} */\n const result = {\n type: 'Feature',\n geometry,\n properties: this.properties\n };\n\n if (this.id != null) {\n result.id = this.id;\n }\n\n return result;\n }\n}\n\n/** @type {['Unknown', 'Point', 'LineString', 'Polygon']} */\nVectorTileFeature.types = ['Unknown', 'Point', 'LineString', 'Polygon'];\n\n/**\n * @param {number} tag\n * @param {VectorTileFeature} feature\n * @param {Pbf} pbf\n */\nfunction readFeature(tag, feature, pbf) {\n if (tag === 1) feature.id = pbf.readVarint();\n else if (tag === 2) readTag(pbf, feature);\n else if (tag === 3) feature.type = /** @type {0 | 1 | 2 | 3} */ (pbf.readVarint());\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 4) feature._geometry = pbf.pos;\n}\n\n/**\n * @param {Pbf} pbf\n * @param {VectorTileFeature} feature\n */\nfunction readTag(pbf, feature) {\n const end = pbf.readVarint() + pbf.pos;\n\n while (pbf.pos < end) {\n // @ts-expect-error TS2341 deliberately accessing a private property\n const key = feature._keys[pbf.readVarint()];\n // @ts-expect-error TS2341 deliberately accessing a private property\n const value = feature._values[pbf.readVarint()];\n feature.properties[key] = value;\n }\n}\n\n/** classifies an array of rings into polygons with outer rings and holes\n * @param {Point[][]} rings\n */\nexport function classifyRings(rings) {\n const len = rings.length;\n\n if (len <= 1) return [rings];\n\n const polygons = [];\n let polygon, ccw;\n\n for (let i = 0; i < len; i++) {\n const area = signedArea(rings[i]);\n if (area === 0) continue;\n\n if (ccw === undefined) ccw = area < 0;\n\n if (ccw === area < 0) {\n if (polygon) polygons.push(polygon);\n polygon = [rings[i]];\n\n } else if (polygon) {\n polygon.push(rings[i]);\n }\n }\n if (polygon) polygons.push(polygon);\n\n return polygons;\n}\n\n/** @param {Point[]} ring */\nfunction signedArea(ring) {\n let sum = 0;\n for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {\n p1 = ring[i];\n p2 = ring[j];\n sum += (p2.x - p1.x) * (p1.y + p2.y);\n }\n return sum;\n}\n\nexport class VectorTileLayer {\n /**\n * @param {Pbf} pbf\n * @param {number} [end]\n */\n constructor(pbf, end) {\n // Public\n this.version = 1;\n this.name = '';\n this.extent = 4096;\n this.length = 0;\n\n /** @private */\n this._pbf = pbf;\n\n /** @private\n * @type {string[]} */\n this._keys = [];\n\n /** @private\n * @type {(number | string | boolean)[]} */\n this._values = [];\n\n /** @private\n * @type {number[]} */\n this._features = [];\n\n pbf.readFields(readLayer, this, end);\n\n this.length = this._features.length;\n }\n\n /** return feature `i` from this layer as a `VectorTileFeature`\n * @param {number} i\n */\n feature(i) {\n if (i < 0 || i >= this._features.length) throw new Error('feature index out of bounds');\n\n this._pbf.pos = this._features[i];\n\n const end = this._pbf.readVarint() + this._pbf.pos;\n return new VectorTileFeature(this._pbf, end, this.extent, this._keys, this._values);\n }\n}\n\n/**\n * @param {number} tag\n * @param {VectorTileLayer} layer\n * @param {Pbf} pbf\n */\nfunction readLayer(tag, layer, pbf) {\n if (tag === 15) layer.version = pbf.readVarint();\n else if (tag === 1) layer.name = pbf.readString();\n else if (tag === 5) layer.extent = pbf.readVarint();\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 2) layer._features.push(pbf.pos);\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 3) layer._keys.push(pbf.readString());\n // @ts-expect-error TS2341 deliberately accessing a private property\n else if (tag === 4) layer._values.push(readValueMessage(pbf));\n}\n\n/**\n * @param {Pbf} pbf\n */\nfunction readValueMessage(pbf) {\n let value = null;\n const end = pbf.readVarint() + pbf.pos;\n\n while (pbf.pos < end) {\n const tag = pbf.readVarint() >> 3;\n\n value = tag === 1 ? pbf.readString() :\n tag === 2 ? pbf.readFloat() :\n tag === 3 ? pbf.readDouble() :\n tag === 4 ? pbf.readVarint64() :\n tag === 5 ? pbf.readVarint() :\n tag === 6 ? pbf.readSVarint() :\n tag === 7 ? pbf.readBoolean() : null;\n }\n if (value == null) {\n throw new Error('unknown feature value');\n }\n\n return value;\n}\n\nexport class VectorTile {\n /**\n * @param {Pbf} pbf\n * @param {number} [end]\n */\n constructor(pbf, end) {\n /** @type {Record} */\n this.layers = pbf.readFields(readTile, {}, end);\n }\n}\n\n/**\n * @param {number} tag\n * @param {Record} layers\n * @param {Pbf} pbf\n */\nfunction readTile(tag, layers, pbf) {\n if (tag === 3) {\n const layer = new VectorTileLayer(pbf, pbf.readVarint() + pbf.pos);\n if (layer.length) layers[layer.name] = layer;\n }\n}\n", "import RBush from 'rbush';\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { json as d3_json } from 'd3-fetch';\n\nimport { marked } from 'marked';\nimport Protobuf from 'pbf';\nimport { VectorTile } from '@mapbox/vector-tile';\n\nimport { fileFetcher } from '../core/file_fetcher';\nimport { localizer } from '../core/localizer';\nimport { geoExtent, geoVecAdd } from '../geo';\nimport { QAItem } from '../osm';\nimport { utilRebind, utilTiler, utilQsString } from '../util';\n\nconst tiler = utilTiler();\nconst dispatch = d3_dispatch('loaded');\nconst _tileZoom = 14;\nconst _osmoseUrlRoot = 'https://osmose.openstreetmap.fr/api/0.3';\nlet _osmoseData = { icons: {}, items: [] };\n\n// This gets reassigned if reset\nlet _cache;\n\nfunction abortRequest(controller) {\n if (controller) {\n controller.abort();\n }\n}\n\nfunction abortUnwantedRequests(cache, tiles) {\n Object.keys(cache.inflightTile).forEach(k => {\n let wanted = tiles.find(tile => k === tile.id);\n if (!wanted) {\n abortRequest(cache.inflightTile[k]);\n delete cache.inflightTile[k];\n }\n });\n}\n\nfunction encodeIssueRtree(d) {\n return { minX: d.loc[0], minY: d.loc[1], maxX: d.loc[0], maxY: d.loc[1], data: d };\n}\n\n// Replace or remove QAItem from rtree\nfunction updateRtree(item, replace) {\n _cache.rtree.remove(item, (a, b) => a.data.id === b.data.id);\n\n if (replace) {\n _cache.rtree.insert(item);\n }\n}\n\n// Issues shouldn't obscure each other\nfunction preventCoincident(loc) {\n let coincident = false;\n do {\n // first time, move marker up. after that, move marker right.\n let delta = coincident ? [0.00001, 0] : [0, 0.00001];\n loc = geoVecAdd(loc, delta);\n let bbox = geoExtent(loc).bbox();\n coincident = _cache.rtree.search(bbox).length;\n } while (coincident);\n\n return loc;\n}\n\nexport default {\n title: 'osmose',\n\n init() {\n fileFetcher.get('qa_data')\n .then(d => {\n _osmoseData = d.osmose;\n _osmoseData.items = Object.keys(d.osmose.icons)\n .map(s => s.split('-')[0])\n .reduce((unique, item) => unique.indexOf(item) !== -1 ? unique : [...unique, item], []);\n });\n\n if (!_cache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset() {\n let _strings = {};\n let _colors = {};\n if (_cache) {\n Object.values(_cache.inflightTile).forEach(abortRequest);\n // Strings and colors are static and should not be re-populated\n _strings = _cache.strings;\n _colors = _cache.colors;\n }\n _cache = {\n data: {},\n loadedTile: {},\n inflightTile: {},\n inflightPost: {},\n closed: {},\n rtree: new RBush(),\n strings: _strings,\n colors: _colors\n };\n },\n\n loadIssues(projection) {\n let params = {\n // Tiles return a maximum # of issues\n // So we want to filter our request for only types iD supports\n item: _osmoseData.items\n };\n\n // determine the needed tiles to cover the view\n let tiles = tiler\n .zoomExtent([_tileZoom, _tileZoom])\n .getTiles(projection);\n\n // abort inflight requests that are no longer needed\n abortUnwantedRequests(_cache, tiles);\n\n // issue new requests..\n tiles.forEach(tile => {\n if (_cache.loadedTile[tile.id] || _cache.inflightTile[tile.id]) return;\n\n let [ x, y, z ] = tile.xyz;\n let url = `${_osmoseUrlRoot}/issues/${z}/${x}/${y}.mvt?` + utilQsString(params);\n\n let controller = new AbortController();\n _cache.inflightTile[tile.id] = controller;\n\n fetch(url, { signal: controller.signal })\n .then(data => data.arrayBuffer())\n .then(data => {\n delete _cache.inflightTile[tile.id];\n _cache.loadedTile[tile.id] = true;\n\n var vectorTile = new VectorTile(new Protobuf(data));\n data = vectorTile.layers.issues;\n const features = [];\n for (let i = 0; i < data.length; i++) {\n features.push(data.feature(i).toGeoJSON(x, y, z));\n }\n\n if (features.length > 0) {\n features.forEach(issue => {\n const { item, class: cl, uuid: id } = issue.properties;\n /* Osmose issues are uniquely identified by a unique\n `item` and `class` combination (both integer values) */\n const itemType = `${item}-${cl}`;\n\n // Filter out unsupported issue types (some are too specific or advanced)\n if (itemType in _osmoseData.icons) {\n let loc = issue.geometry.coordinates; // lon, lat\n loc = preventCoincident(loc);\n\n let d = new QAItem(loc, this, itemType, id, { item });\n\n // Setting elems here prevents UI detail requests\n if (item === 8300 || item === 8360) {\n d.elems = [];\n }\n\n _cache.data[d.id] = d;\n _cache.rtree.insert(encodeIssueRtree(d));\n }\n });\n }\n\n dispatch.call('loaded');\n })\n .catch(() => {\n delete _cache.inflightTile[tile.id];\n _cache.loadedTile[tile.id] = true;\n });\n });\n },\n\n loadIssueDetail(issue) {\n // Issue details only need to be fetched once\n if (issue.elems !== undefined) {\n return Promise.resolve(issue);\n }\n\n const url = `${_osmoseUrlRoot}/issue/${issue.id}?langs=${localizer.localeCode()}`;\n const cacheDetails = data => {\n // Associated elements used for highlighting\n // Assign directly for immediate use in the callback\n issue.elems = data.elems.map(e => e.type.substring(0,1) + e.id);\n\n // Some issues have instance specific detail in a subtitle\n issue.detail = data.subtitle ? marked(data.subtitle.auto) : '';\n\n this.replaceItem(issue);\n };\n\n return d3_json(url).then(cacheDetails).then(() => issue);\n },\n\n loadStrings(locale=localizer.localeCode()) {\n const items = Object.keys(_osmoseData.icons);\n\n if (\n locale in _cache.strings\n && Object.keys(_cache.strings[locale]).length === items.length\n ) {\n return Promise.resolve(_cache.strings[locale]);\n }\n\n // May be partially populated already if some requests were successful\n if (!(locale in _cache.strings)) {\n _cache.strings[locale] = {};\n }\n\n // Only need to cache strings for supported issue types\n // Using multiple individual item + class requests to reduce fetched data size\n const allRequests = items.map(itemType => {\n // No need to request data we already have\n if (itemType in _cache.strings[locale]) return null;\n\n const cacheData = data => {\n // Bunch of nested single value arrays of objects\n const [ cat = {items:[]} ] = data.categories;\n const [ item = {class:[]} ] = cat.items;\n const [ cl = null ] = item.class;\n\n // If null default value is reached, data wasn't as expected (or was empty)\n if (!cl) {\n /* eslint-disable no-console */\n console.log(`Osmose strings request (${itemType}) had unexpected data`);\n /* eslint-enable no-console */\n return;\n }\n\n // Cache served item colors to automatically style issue markers later\n const { item: itemInt, color } = item;\n if (/^#[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}/.test(color)) {\n _cache.colors[itemInt] = color;\n }\n\n // Value of root key will be null if no string exists\n // If string exists, value is an object with key 'auto' for string\n const { title, detail, fix, trap } = cl;\n\n // Osmose titles shouldn't contain markdown\n let issueStrings = {};\n if (title) issueStrings.title = title.auto;\n if (detail) issueStrings.detail = marked(detail.auto);\n if (trap) issueStrings.trap = marked(trap.auto);\n if (fix) issueStrings.fix = marked(fix.auto);\n\n _cache.strings[locale][itemType] = issueStrings;\n };\n\n const [ item, cl ] = itemType.split('-');\n\n // Osmose API falls back to English strings where untranslated or if locale doesn't exist\n const url = `${_osmoseUrlRoot}/items/${item}/class/${cl}?langs=${locale}`;\n\n return d3_json(url).then(cacheData);\n }).filter(Boolean);\n\n return Promise.all(allRequests).then(() => _cache.strings[locale]);\n },\n\n getStrings(itemType, locale=localizer.localeCode()) {\n // No need to fallback to English, Osmose API handles this for us\n return (locale in _cache.strings) ? _cache.strings[locale][itemType] : {};\n },\n\n getColor(itemType) {\n return (itemType in _cache.colors) ? _cache.colors[itemType] : '#FFFFFF';\n },\n\n postUpdate(issue, callback) {\n if (_cache.inflightPost[issue.id]) {\n return callback({ message: 'Issue update already inflight', status: -2 }, issue);\n }\n\n // UI sets the status to either 'done' or 'false'\n const url = `${_osmoseUrlRoot}/issue/${issue.id}/${issue.newStatus}`;\n const controller = new AbortController();\n const after = () => {\n delete _cache.inflightPost[issue.id];\n\n this.removeItem(issue);\n if (issue.newStatus === 'done') {\n // Keep track of the number of issues closed per `item` to tag the changeset\n if (!(issue.item in _cache.closed)) {\n _cache.closed[issue.item] = 0;\n }\n _cache.closed[issue.item] += 1;\n }\n if (callback) callback(null, issue);\n };\n\n _cache.inflightPost[issue.id] = controller;\n\n fetch(url, { signal: controller.signal })\n .then(after)\n .catch(err => {\n delete _cache.inflightPost[issue.id];\n if (callback) callback(err.message);\n });\n },\n\n // Get all cached QAItems covering the viewport\n getItems(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n\n return _cache.rtree.search(bbox).map(d => d.data);\n },\n\n // Get a QAItem from cache\n // NOTE: Don't change method name until UI v3 is merged\n getError(id) {\n return _cache.data[id];\n },\n\n // get the name of the icon to display for this item\n getIcon(itemType) {\n return _osmoseData.icons[itemType];\n },\n\n // Replace a single QAItem in the cache\n replaceItem(item) {\n if (!(item instanceof QAItem) || !item.id) return;\n\n _cache.data[item.id] = item;\n updateRtree(encodeIssueRtree(item), true); // true = replace\n return item;\n },\n\n // Remove a single QAItem from the cache\n removeItem(item) {\n if (!(item instanceof QAItem) || !item.id) return;\n\n delete _cache.data[item.id];\n updateRtree(encodeIssueRtree(item), false); // false = remove\n },\n\n // Used to populate `closed:osmose:*` changeset tags\n getClosedCounts() {\n return _cache.closed;\n },\n\n itemURL(item) {\n return `https://osmose.openstreetmap.fr/en/error/${item.id}`;\n }\n};\n", "import { geoScaleToZoom } from '../geo';\nimport { utilTiler } from './tiler';\nimport type { Projection } from '../geo/raw_mercator';\nimport type RBush from 'rbush';\nimport type { BBox } from 'rbush';\n\nexport interface WithBbox extends BBox {\n data: T;\n}\n\nexport function partitionViewport(projection: Projection) {\n let z = geoScaleToZoom(projection.scale());\n let z2 = (Math.ceil(z * 2) / 2) + 2.5; // round to next 0.5 and add 2.5\n let tiler = utilTiler().zoomExtent([z2, z2]);\n\n return (tiler.getTiles(projection) || []).map(tile => tile.extent);\n}\n\n\n/** no more than `limit` results per partition */\nexport function searchLimited(limit: number | undefined, projection: Projection, rtree: RBush>): T[] {\n limit ||= 5;\n\n return partitionViewport(projection)\n .flatMap((extent) => rtree.search(extent.bbox()).slice(0, limit))\n .map(result => result.data);\n}\n", "/* global mapillary:false */\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport Protobuf from 'pbf';\nimport RBush from 'rbush';\nimport { VectorTile } from '@mapbox/vector-tile';\nimport { geoExtent } from '../geo';\nimport { utilQsString, utilRebind, utilTiler, utilStringQs } from '../util';\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\n\nconst accessToken = 'MLY|4100327730013843|5bb78b81720791946a9a7b956c57b7cf';\nconst apiUrl = 'https://graph.mapillary.com/';\nconst baseTileUrl = 'https://tiles.mapillary.com/maps/vtp';\nconst mapFeatureTileUrl = `${baseTileUrl}/mly_map_feature_point/2/{z}/{x}/{y}?access_token=${accessToken}`;\nconst tileUrl = `${baseTileUrl}/mly1_public/2/{z}/{x}/{y}?access_token=${accessToken}`;\nconst trafficSignTileUrl = `${baseTileUrl}/mly_map_feature_traffic_sign/2/{z}/{x}/{y}?access_token=${accessToken}`;\n\nconst viewercss = 'mapillary-js/mapillary.css';\nconst viewerjs = 'mapillary-js/mapillary.js';\nconst minZoom = 14;\nconst dispatch = d3_dispatch('change', 'loadedImages', 'loadedSigns', 'loadedMapFeatures', 'bearingChanged', 'imageChanged');\n\nlet _loadViewerPromise;\nlet _mlyActiveImage;\nlet _mlyCache;\nlet _mlyFallback = false;\nlet _mlyHighlightedDetection;\nlet _mlyShowFeatureDetections = false;\nlet _mlyShowSignDetections = false;\nlet _mlyViewer;\nlet _mlyViewerFilter = ['all'];\nlet _isViewerOpen = false;\n\n\n// Load all data for the specified type from Mapillary vector tiles\nfunction loadTiles(which, url, maxZoom, projection) {\n const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);\n const tiles = tiler.getTiles(projection);\n\n tiles.forEach(function(tile) {\n loadTile(which, url, tile);\n });\n}\n\n\n// Load all data for the specified type from one vector tile\nfunction loadTile(which, url, tile) {\n const cache = _mlyCache.requests;\n const tileId = `${tile.id}-${which}`;\n if (cache.loaded[tileId] || cache.inflight[tileId]) return;\n const controller = new AbortController();\n cache.inflight[tileId] = controller;\n const requestUrl = url.replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n .replace('{z}', tile.xyz[2]);\n\n fetch(requestUrl, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (!data) {\n throw new Error('No Data');\n }\n loadTileDataToCache(data, tile, which);\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n } else if (which === 'signs') {\n dispatch.call('loadedSigns');\n } else if (which === 'points') {\n dispatch.call('loadedMapFeatures');\n }\n })\n .catch(function() {\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n });\n}\n\n\n// Load the data from the vector tile into cache\nfunction loadTileDataToCache(data, tile, which) {\n const vectorTile = new VectorTile(new Protobuf(data));\n let features,\n cache,\n layer,\n i,\n feature,\n loc,\n d;\n\n if (vectorTile.layers.hasOwnProperty('image')) {\n features = [];\n cache = _mlyCache.images;\n layer = vectorTile.layers.image;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n d = {\n service: 'photo',\n loc: loc,\n captured_at: feature.properties.captured_at,\n ca: feature.properties.compass_angle,\n id: feature.properties.id,\n is_pano: feature.properties.is_pano,\n sequence_id: feature.properties.sequence_id,\n };\n cache.forImageId[d.id] = d;\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty('sequence')) {\n cache = _mlyCache.sequences;\n layer = vectorTile.layers.sequence;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n if (cache.lineString[feature.properties.id]) {\n cache.lineString[feature.properties.id].push(feature);\n } else {\n cache.lineString[feature.properties.id] = [feature];\n }\n }\n }\n\n if (vectorTile.layers.hasOwnProperty('point')) {\n features = [];\n cache = _mlyCache[which];\n layer = vectorTile.layers.point;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n\n d = {\n service: 'photo',\n loc: loc,\n id: feature.properties.id,\n first_seen_at: feature.properties.first_seen_at,\n last_seen_at: feature.properties.last_seen_at,\n value: feature.properties.value\n };\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty('traffic_sign')) {\n features = [];\n cache = _mlyCache[which];\n layer = vectorTile.layers.traffic_sign;\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n\n d = {\n service: 'photo',\n loc: loc,\n id: feature.properties.id,\n first_seen_at: feature.properties.first_seen_at,\n last_seen_at: feature.properties.last_seen_at,\n value: feature.properties.value\n };\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n}\n\n\n// Get data from the API\nfunction loadData(url) {\n return fetch(url)\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n })\n .then(function(result) {\n if (!result) {\n return [];\n }\n return result.data || [];\n });\n}\n\nexport default {\n // Initialize Mapillary\n init: function() {\n if (!_mlyCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n // Reset cache and state\n reset: function() {\n if (_mlyCache) {\n Object.values(_mlyCache.requests.inflight).forEach(function(request) { request.abort(); });\n }\n\n _mlyCache = {\n images: { rtree: new RBush(), forImageId: {} },\n image_detections: { forImageId: {} },\n signs: { rtree: new RBush() },\n points: { rtree: new RBush() },\n sequences: { rtree: new RBush(), lineString: {} },\n requests: { loaded: {}, inflight: {} }\n };\n\n _mlyActiveImage = null;\n },\n\n // Get visible images\n images: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _mlyCache.images.rtree);\n },\n\n // Get visible traffic signs\n signs: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _mlyCache.signs.rtree);\n },\n\n // Get visible map (point) features\n mapFeatures: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _mlyCache.points.rtree);\n },\n\n // Get cached image by id\n cachedImage: function(imageId) {\n return _mlyCache.images.forImageId[imageId];\n },\n\n // Get visible sequences\n sequences: function(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const sequenceIds = {};\n let lineStrings = [];\n\n _mlyCache.images.rtree.search(bbox)\n .forEach(function(d) {\n if (d.data.sequence_id) {\n sequenceIds[d.data.sequence_id] = true;\n }\n });\n\n Object.keys(sequenceIds).forEach(function(sequenceId) {\n if (_mlyCache.sequences.lineString[sequenceId]) {\n lineStrings = lineStrings.concat(_mlyCache.sequences.lineString[sequenceId]);\n }\n });\n\n return lineStrings;\n },\n\n\n // Load images in the visible area\n loadImages: function(projection) {\n loadTiles('images', tileUrl, 14, projection);\n },\n\n\n // Load traffic signs in the visible area\n loadSigns: function(projection) {\n loadTiles('signs', trafficSignTileUrl, 14, projection);\n },\n\n\n // Load map (point) features in the visible area\n loadMapFeatures: function(projection) {\n loadTiles('points', mapFeatureTileUrl, 14, projection);\n },\n\n\n // Return a promise that resolves when the image viewer (Mapillary JS) library has finished loading\n ensureViewerLoaded: function(context) {\n if (_loadViewerPromise) return _loadViewerPromise;\n\n // add mly-wrapper\n const wrap = context.container().select('.photoviewer')\n .selectAll('.mly-wrapper')\n .data([0]);\n\n wrap.enter()\n .append('div')\n .attr('id', 'ideditor-mly')\n .attr('class', 'photo-wrapper mly-wrapper')\n .classed('hide', true);\n\n const that = this;\n\n _loadViewerPromise = new Promise((resolve, reject) => {\n let loadedCount = 0;\n function loaded() {\n loadedCount += 1;\n\n // wait until both files are loaded\n if (loadedCount === 2) resolve();\n }\n\n const head = d3_select('head');\n\n // load mapillary-viewercss\n head.selectAll('#ideditor-mapillary-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-mapillary-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(viewercss))\n .on('load.serviceMapillary', loaded)\n .on('error.serviceMapillary', function() {\n reject();\n });\n\n // load mapillary-viewerjs\n head.selectAll('#ideditor-mapillary-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-mapillary-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(viewerjs))\n .on('load.serviceMapillary', loaded)\n .on('error.serviceMapillary', function() {\n reject();\n });\n })\n .catch(function() {\n _loadViewerPromise = null;\n })\n .then(function() {\n that.initViewer(context);\n });\n\n return _loadViewerPromise;\n },\n\n\n // Load traffic sign image sprites\n loadSignResources: function(context) {\n context.ui().svgDefs.addSprites(['mapillary-sprite'], false /* don't override colors */ );\n return this;\n },\n\n\n // Load map (point) feature image sprites\n loadObjectResources: function(context) {\n context.ui().svgDefs.addSprites(['mapillary-object-sprite'], false /* don't override colors */ );\n return this;\n },\n\n\n // Remove previous detections in image viewer\n resetTags: function() {\n if (_mlyViewer && !_mlyFallback) {\n _mlyViewer.getComponent('tag').removeAll();\n }\n },\n\n\n // Show map feature detections in image viewer\n showFeatureDetections: function(value) {\n _mlyShowFeatureDetections = value;\n if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {\n this.resetTags();\n }\n },\n\n\n // Show traffic sign detections in image viewer\n showSignDetections: function(value) {\n _mlyShowSignDetections = value;\n if (!_mlyShowFeatureDetections && !_mlyShowSignDetections) {\n this.resetTags();\n }\n },\n\n\n // Apply filter to image viewer\n filterViewer: function(context) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n const filter = ['all'];\n\n if (!showsPano) filter.push([ '!=', 'cameraType', 'spherical' ]);\n if (!showsFlat && showsPano) filter.push(['==', 'pano', true]);\n if (fromDate) {\n filter.push(['>=', 'capturedAt', new Date(fromDate).getTime()]);\n }\n if (toDate) {\n filter.push(['>=', 'capturedAt', new Date(toDate).getTime()]);\n }\n\n if (_mlyViewer) {\n _mlyViewer.setFilter(filter);\n }\n _mlyViewerFilter = filter;\n\n return filter;\n },\n\n\n // Make the image viewer visible\n showViewer: function(context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.mly-wrapper.hide').size();\n\n if (isHidden && _mlyViewer) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.mly-wrapper')\n .classed('hide', false);\n\n _mlyViewer.resize();\n }\n\n _isViewerOpen = true;\n return this;\n },\n\n\n // Hide the image viewer and resets map markers\n hideViewer: function(context) {\n _mlyActiveImage = null;\n\n if (!_mlyFallback && _mlyViewer) {\n _mlyViewer.getComponent('sequence').stop();\n }\n\n const viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n this.updateUrlImage(null);\n\n dispatch.call('imageChanged');\n dispatch.call('loadedMapFeatures');\n dispatch.call('loadedSigns');\n\n _isViewerOpen = false;\n\n return this.setStyles(context, null);\n },\n\n\n // Get viewer status\n isViewerOpen: function() {\n return _isViewerOpen;\n },\n\n\n // Update the URL with current image id\n updateUrlImage: function(imageId) {\n const hash = utilStringQs(window.location.hash);\n if (imageId) {\n hash.photo = 'mapillary/' + imageId;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n\n // Highlight the detection in the viewer that is related to the clicked map feature\n highlightDetection: function(detection) {\n if (detection) {\n _mlyHighlightedDetection = detection.id;\n }\n\n return this;\n },\n\n\n // Initialize image viewer (Mapillar JS)\n initViewer: function(context) {\n if (!window.mapillary) return;\n\n const opts = {\n accessToken: accessToken,\n component: {\n cover: false,\n keyboard: false,\n tag: true\n },\n container: 'ideditor-mly',\n };\n\n // Disable components requiring WebGL support\n if (!mapillary.isSupported() && mapillary.isFallbackSupported()) {\n _mlyFallback = true;\n opts.component = {\n cover: false,\n direction: false,\n imagePlane: false,\n keyboard: false,\n mouse: false,\n sequence: false,\n tag: false,\n image: true, // fallback\n navigation: true // fallback\n };\n }\n\n _mlyViewer = new mapillary.Viewer(opts);\n _mlyViewer.on('image', imageChanged.bind(this));\n _mlyViewer.on('bearing', bearingChanged);\n\n if (_mlyViewerFilter) {\n _mlyViewer.setFilter(_mlyViewerFilter);\n }\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.mapillary', function() {\n if (_mlyViewer) _mlyViewer.resize();\n });\n\n // imageChanged: called after the viewer has changed images and is ready.\n function imageChanged(photo) {\n this.resetTags();\n const image = photo.image;\n this.setActiveImage(image);\n this.setStyles(context, null);\n const loc = [image.originalLngLat.lng, image.originalLngLat.lat];\n context.map().centerEase(loc);\n this.updateUrlImage(image.id);\n\n if (_mlyShowFeatureDetections || _mlyShowSignDetections) {\n this.updateDetections(image.id, `${apiUrl}/${image.id}/detections?access_token=${accessToken}&fields=id,image,geometry,value`);\n }\n dispatch.call('imageChanged');\n }\n\n\n // bearingChanged: called when the bearing changes in the image viewer.\n function bearingChanged(e) {\n dispatch.call('bearingChanged', undefined, e);\n }\n },\n\n\n // Move to an image\n selectImage: function(context, imageId) {\n if (_mlyViewer && imageId) {\n _mlyViewer.moveTo(imageId)\n .then(image => this.setActiveImage(image))\n .catch(function(e) {\n console.error('mly3', e); // eslint-disable-line no-console\n });\n }\n\n return this;\n },\n\n\n // Return the currently displayed image\n getActiveImage: function() {\n return _mlyActiveImage;\n },\n\n\n // Return a list of detection objects for the given id\n getDetections: function(id) {\n return loadData(`${apiUrl}/${id}/detections?access_token=${accessToken}&fields=id,value,image`);\n },\n\n\n // Set the currently visible image\n setActiveImage: function(image) {\n if (image) {\n _mlyActiveImage = {\n ca: image.originalCompassAngle,\n id: image.id,\n loc: [image.originalLngLat.lng, image.originalLngLat.lat],\n is_pano: image.cameraType === 'spherical',\n sequence_id: image.sequenceId\n };\n } else {\n _mlyActiveImage = null;\n }\n },\n\n\n // Update the currently highlighted sequence and selected bubble.\n setStyles: function(context, hovered) {\n const hoveredImageId = hovered && hovered.id;\n const hoveredSequenceId = hovered && hovered.sequence_id;\n const selectedSequenceId = _mlyActiveImage && _mlyActiveImage.sequence_id;\n\n context.container().selectAll('.layer-mapillary .viewfield-group')\n .classed('highlighted', function(d) { return (d.sequence_id === selectedSequenceId) || (d.id === hoveredImageId); })\n .classed('hovered', function(d) { return d.id === hoveredImageId; });\n\n context.container().selectAll('.layer-mapillary .sequence')\n .classed('highlighted', function(d) { return d.properties.id === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.id === selectedSequenceId; });\n\n return this;\n },\n\n\n // Get detections for the current image and shows them in the image viewer\n updateDetections: function(imageId, url) {\n if (!_mlyViewer || _mlyFallback) return;\n if (!imageId) return;\n const cache = _mlyCache.image_detections;\n if (cache.forImageId[imageId]) {\n showDetections(_mlyCache.image_detections.forImageId[imageId]);\n } else {\n loadData(url)\n .then(detections => {\n detections.forEach(function(detection) {\n if (!cache.forImageId[imageId]) {\n cache.forImageId[imageId] = [];\n }\n cache.forImageId[imageId].push({\n geometry: detection.geometry,\n id: detection.id,\n image_id: imageId,\n value:detection.value\n });\n });\n\n showDetections(_mlyCache.image_detections.forImageId[imageId] || []);\n });\n }\n\n\n // Create a tag for each detection and shows it in the image viewer\n function showDetections(detections) {\n const tagComponent = _mlyViewer.getComponent('tag');\n detections.forEach(function(data) {\n const tag = makeTag(data);\n if (tag) {\n tagComponent.add([tag]);\n }\n });\n }\n\n\n // Create a Mapillary JS tag object\n function makeTag(data) {\n const valueParts = data.value.split('--');\n if (!valueParts.length) return;\n\n let tag;\n let text;\n let color = 0xffffff;\n\n if (_mlyHighlightedDetection === data.id) {\n color = 0xffff00;\n text = valueParts[1];\n if (text === 'flat' || text === 'discrete' || text === 'sign') {\n text = valueParts[2];\n }\n text = text.replace(/-/g, ' ');\n text = text.charAt(0).toUpperCase() + text.slice(1);\n _mlyHighlightedDetection = null;\n }\n\n var decodedGeometry = window.atob(data.geometry);\n var uintArray = new Uint8Array(decodedGeometry.length);\n for (var i = 0; i < decodedGeometry.length; i++) {\n uintArray[i] = decodedGeometry.charCodeAt(i);\n }\n const tile = new VectorTile(new Protobuf(uintArray.buffer));\n const layer = tile.layers['mpy-or'];\n\n const geometries = layer.feature(0).loadGeometry();\n\n const polygon = geometries.map(ring =>\n ring.map(point =>\n [point.x / layer.extent, point.y / layer.extent]));\n\n tag = new mapillary.OutlineTag(\n data.id,\n new mapillary.PolygonGeometry(polygon[0]),\n {\n text: text,\n textColor: color,\n lineColor: color,\n lineWidth: 2,\n fillColor: color,\n fillOpacity: 0.3,\n }\n );\n\n return tag;\n }\n },\n\n\n // Return the current cache\n cache: function() {\n return _mlyCache;\n }\n};\n", "import { osmAreaKeys as areaKeys } from '../osm/tags';\nimport { utilArrayIntersection } from '../util';\nimport { validationIssue } from '../core/validation';\n\n\nvar buildRuleChecks = function() {\n return {\n equals: function (equals) {\n return function(tags) {\n return Object.keys(equals).every(function(k) {\n return equals[k] === tags[k];\n });\n };\n },\n notEquals: function (notEquals) {\n return function(tags) {\n return Object.keys(notEquals).some(function(k) {\n return notEquals[k] !== tags[k];\n });\n };\n },\n absence: function(absence) {\n return function(tags) {\n return Object.keys(tags).indexOf(absence) === -1;\n };\n },\n presence: function(presence) {\n return function(tags) {\n return Object.keys(tags).indexOf(presence) > -1;\n };\n },\n greaterThan: function(greaterThan) {\n var key = Object.keys(greaterThan)[0];\n var value = greaterThan[key];\n\n return function(tags) {\n return tags[key] > value;\n };\n },\n greaterThanEqual: function(greaterThanEqual) {\n var key = Object.keys(greaterThanEqual)[0];\n var value = greaterThanEqual[key];\n\n return function(tags) {\n return tags[key] >= value;\n };\n },\n lessThan: function(lessThan) {\n var key = Object.keys(lessThan)[0];\n var value = lessThan[key];\n\n return function(tags) {\n return tags[key] < value;\n };\n },\n lessThanEqual: function(lessThanEqual) {\n var key = Object.keys(lessThanEqual)[0];\n var value = lessThanEqual[key];\n\n return function(tags) {\n return tags[key] <= value;\n };\n },\n positiveRegex: function(positiveRegex) {\n var tagKey = Object.keys(positiveRegex)[0];\n var expression = positiveRegex[tagKey].join('|');\n var regex = new RegExp(expression);\n\n return function(tags) {\n return regex.test(tags[tagKey]);\n };\n },\n negativeRegex: function(negativeRegex) {\n var tagKey = Object.keys(negativeRegex)[0];\n var expression = negativeRegex[tagKey].join('|');\n var regex = new RegExp(expression);\n\n return function(tags) {\n return !regex.test(tags[tagKey]);\n };\n }\n };\n};\n\nvar buildLineKeys = function() {\n return {\n highway: {\n rest_area: true,\n services: true\n },\n railway: {\n roundhouse: true,\n station: true,\n traverser: true,\n turntable: true,\n wash: true\n }\n };\n};\n\nexport default {\n init: function() {\n this._ruleChecks = buildRuleChecks();\n this._validationRules = [];\n this._areaKeys = areaKeys;\n this._lineKeys = buildLineKeys();\n },\n\n // list of rules only relevant to tag checks...\n filterRuleChecks: function(selector) {\n var _ruleChecks = this._ruleChecks;\n return Object.keys(selector).reduce(function(rules, key) {\n if (['geometry', 'error', 'warning'].indexOf(key) === -1) {\n rules.push(_ruleChecks[key](selector[key]));\n }\n return rules;\n }, []);\n },\n\n // builds tagMap from mapcss-parse selector object...\n buildTagMap: function(selector) {\n var getRegexValues = function(regexes) {\n return regexes.map(function(regex) {\n return regex.replace(/\\$|\\^/g, '');\n });\n };\n\n var tagMap = Object.keys(selector).reduce(function (expectedTags, key) {\n var values;\n var isRegex = /regex/gi.test(key);\n var isEqual = /equals/gi.test(key);\n\n if (isRegex || isEqual) {\n Object.keys(selector[key]).forEach(function(selectorKey) {\n values = isEqual ? [selector[key][selectorKey]] : getRegexValues(selector[key][selectorKey]);\n\n if (expectedTags.hasOwnProperty(selectorKey)) {\n values = values.concat(expectedTags[selectorKey]);\n }\n\n expectedTags[selectorKey] = values;\n });\n\n } else if (/(greater|less)Than(Equal)?|presence/g.test(key)) {\n var tagKey = /presence/.test(key) ? selector[key] : Object.keys(selector[key])[0];\n\n values = [selector[key][tagKey]];\n\n if (expectedTags.hasOwnProperty(tagKey)) {\n values = values.concat(expectedTags[tagKey]);\n }\n\n expectedTags[tagKey] = values;\n }\n\n return expectedTags;\n }, {});\n\n return tagMap;\n },\n\n // inspired by osmWay#isArea()\n inferGeometry: function(tagMap) {\n var _lineKeys = this._lineKeys;\n var _areaKeys = this._areaKeys;\n\n var keyValueDoesNotImplyArea = function(key) {\n return utilArrayIntersection(tagMap[key], Object.keys(_areaKeys[key])).length > 0;\n };\n var keyValueImpliesLine = function(key) {\n return utilArrayIntersection(tagMap[key], Object.keys(_lineKeys[key])).length > 0;\n };\n\n if (tagMap.hasOwnProperty('area')) {\n if (tagMap.area.indexOf('yes') > -1) {\n return 'area';\n }\n if (tagMap.area.indexOf('no') > -1) {\n return 'line';\n }\n }\n\n for (var key in tagMap) {\n if (key in _areaKeys && !keyValueDoesNotImplyArea(key)) {\n return 'area';\n }\n if (key in _lineKeys && keyValueImpliesLine(key)) {\n return 'area';\n }\n }\n\n return 'line';\n },\n\n // adds from mapcss-parse selector check...\n addRule: function(selector) {\n var rule = {\n // checks relevant to mapcss-selector\n checks: this.filterRuleChecks(selector),\n // true if all conditions for a tag error are true..\n matches: function(entity) {\n return this.checks.every(function(check) {\n return check(entity.tags);\n });\n },\n // borrowed from Way#isArea()\n inferredGeometry: this.inferGeometry(this.buildTagMap(selector), this._areaKeys),\n geometryMatches: function(entity, graph) {\n if (entity.type === 'node' || entity.type === 'relation') {\n return selector.geometry === entity.type;\n } else if (entity.type === 'way') {\n return this.inferredGeometry === entity.geometry(graph);\n }\n },\n // when geometries match and tag matches are present, return a warning...\n findIssues: function (entity, graph, issues) {\n if (this.geometryMatches(entity, graph) && this.matches(entity)) {\n var severity = Object.keys(selector).indexOf('error') > -1\n ? 'error'\n : 'warning';\n var message = selector[severity];\n issues.push(new validationIssue({\n type: 'maprules',\n severity: severity,\n message: function() {\n return message;\n },\n entityIds: [entity.id]\n }));\n }\n }\n };\n this._validationRules.push(rule);\n },\n\n clearRules: function() { this._validationRules = []; },\n\n // returns validationRules...\n validationRules: function() { return this._validationRules; },\n\n // returns ruleChecks\n ruleChecks: function() { return this._ruleChecks; }\n};\n", "import { json as d3_json } from 'd3-fetch';\n\nimport RBush from 'rbush';\nimport { geoExtent } from '../geo';\nimport { utilQsString } from '../util';\nimport { localizer } from '../core';\n\nimport { nominatimApiUrl } from '../../config/id.js';\n\n\nvar apibase = nominatimApiUrl;\nvar _inflight = {};\nvar _nominatimCache;\n\n\nexport default {\n\n init: function() {\n _inflight = {};\n _nominatimCache = new RBush();\n },\n\n reset: function() {\n Object.values(_inflight).forEach(function(controller) { controller.abort(); });\n _inflight = {};\n _nominatimCache = new RBush();\n },\n\n\n countryCode: function (location, callback) {\n this.reverse(location, function(err, result) {\n if (err) {\n return callback(err);\n } else if (result.address) {\n return callback(null, result.address.country_code);\n } else {\n return callback('Unable to geocode', null);\n }\n });\n },\n\n\n reverse: function (loc, callback) {\n var cached = _nominatimCache.search(\n { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1] }\n );\n\n if (cached.length > 0) {\n if (callback) callback(null, cached[0].data);\n return;\n }\n\n var params = { zoom: 13, format: 'json', addressdetails: 1, lat: loc[1], lon: loc[0] };\n var url = apibase + 'reverse?' + utilQsString(params);\n\n if (_inflight[url]) return;\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, {\n signal: controller.signal,\n headers: {\n 'Accept-Language': localizer.localeCodes().join(',')\n }\n })\n .then(function(result) {\n delete _inflight[url];\n if (result && result.error) {\n throw new Error(result.error);\n }\n var extent = geoExtent(loc).padByMeters(200);\n _nominatimCache.insert(Object.assign(extent.bbox(), {data: result}));\n if (callback) callback(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (callback) callback(err.message);\n });\n },\n\n\n search: function (val, callback) {\n const params = {\n q: val,\n limit:10,\n format: 'json'\n };\n var url = apibase + 'search?' + utilQsString(params);\n\n if (_inflight[url]) return;\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, {\n signal: controller.signal,\n headers: {\n 'Accept-Language': localizer.localeCodes().join(',')\n }\n })\n .then(function(result) {\n delete _inflight[url];\n if (result && result.error) {\n throw new Error(result.error);\n }\n if (callback) callback(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (callback) callback(err.message);\n });\n }\n\n};\n", "(function (global, factory) {\n\ttypeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n\ttypeof define === 'function' && define.amd ? define(factory) :\n\t(global.quickselect = factory());\n}(this, (function () { 'use strict';\n\nfunction quickselect(arr, k, left, right, compare) {\n quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);\n}\n\nfunction quickselectStep(arr, k, left, right, compare) {\n\n while (right > left) {\n if (right - left > 600) {\n var n = right - left + 1;\n var m = k - left + 1;\n var z = Math.log(n);\n var s = 0.5 * Math.exp(2 * z / 3);\n var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);\n var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));\n var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));\n quickselectStep(arr, k, newLeft, newRight, compare);\n }\n\n var t = arr[k];\n var i = left;\n var j = right;\n\n swap(arr, left, k);\n if (compare(arr[right], t) > 0) swap(arr, left, right);\n\n while (i < j) {\n swap(arr, i, j);\n i++;\n j--;\n while (compare(arr[i], t) < 0) i++;\n while (compare(arr[j], t) > 0) j--;\n }\n\n if (compare(arr[left], t) === 0) swap(arr, left, j);\n else {\n j++;\n swap(arr, j, right);\n }\n\n if (j <= k) left = j + 1;\n if (k <= j) right = j - 1;\n }\n}\n\nfunction swap(arr, i, j) {\n var tmp = arr[i];\n arr[i] = arr[j];\n arr[j] = tmp;\n}\n\nfunction defaultCompare(a, b) {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\nreturn quickselect;\n\n})));\n", "'use strict';\n\nmodule.exports = rbush;\nmodule.exports.default = rbush;\n\nvar quickselect = require('quickselect');\n\nfunction rbush(maxEntries, format) {\n if (!(this instanceof rbush)) return new rbush(maxEntries, format);\n\n // max entries in a node is 9 by default; min node fill is 40% for best performance\n this._maxEntries = Math.max(4, maxEntries || 9);\n this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));\n\n if (format) {\n this._initFormat(format);\n }\n\n this.clear();\n}\n\nrbush.prototype = {\n\n all: function () {\n return this._all(this.data, []);\n },\n\n search: function (bbox) {\n\n var node = this.data,\n result = [],\n toBBox = this.toBBox;\n\n if (!intersects(bbox, node)) return result;\n\n var nodesToSearch = [],\n i, len, child, childBBox;\n\n while (node) {\n for (i = 0, len = node.children.length; i < len; i++) {\n\n child = node.children[i];\n childBBox = node.leaf ? toBBox(child) : child;\n\n if (intersects(bbox, childBBox)) {\n if (node.leaf) result.push(child);\n else if (contains(bbox, childBBox)) this._all(child, result);\n else nodesToSearch.push(child);\n }\n }\n node = nodesToSearch.pop();\n }\n\n return result;\n },\n\n collides: function (bbox) {\n\n var node = this.data,\n toBBox = this.toBBox;\n\n if (!intersects(bbox, node)) return false;\n\n var nodesToSearch = [],\n i, len, child, childBBox;\n\n while (node) {\n for (i = 0, len = node.children.length; i < len; i++) {\n\n child = node.children[i];\n childBBox = node.leaf ? toBBox(child) : child;\n\n if (intersects(bbox, childBBox)) {\n if (node.leaf || contains(bbox, childBBox)) return true;\n nodesToSearch.push(child);\n }\n }\n node = nodesToSearch.pop();\n }\n\n return false;\n },\n\n load: function (data) {\n if (!(data && data.length)) return this;\n\n if (data.length < this._minEntries) {\n for (var i = 0, len = data.length; i < len; i++) {\n this.insert(data[i]);\n }\n return this;\n }\n\n // recursively build the tree with the given data from scratch using OMT algorithm\n var node = this._build(data.slice(), 0, data.length - 1, 0);\n\n if (!this.data.children.length) {\n // save as is if tree is empty\n this.data = node;\n\n } else if (this.data.height === node.height) {\n // split root if trees have the same height\n this._splitRoot(this.data, node);\n\n } else {\n if (this.data.height < node.height) {\n // swap trees if inserted one is bigger\n var tmpNode = this.data;\n this.data = node;\n node = tmpNode;\n }\n\n // insert the small tree into the large tree at appropriate level\n this._insert(node, this.data.height - node.height - 1, true);\n }\n\n return this;\n },\n\n insert: function (item) {\n if (item) this._insert(item, this.data.height - 1);\n return this;\n },\n\n clear: function () {\n this.data = createNode([]);\n return this;\n },\n\n remove: function (item, equalsFn) {\n if (!item) return this;\n\n var node = this.data,\n bbox = this.toBBox(item),\n path = [],\n indexes = [],\n i, parent, index, goingUp;\n\n // depth-first iterative tree traversal\n while (node || path.length) {\n\n if (!node) { // go up\n node = path.pop();\n parent = path[path.length - 1];\n i = indexes.pop();\n goingUp = true;\n }\n\n if (node.leaf) { // check current node\n index = findItem(item, node.children, equalsFn);\n\n if (index !== -1) {\n // item found, remove the item and condense tree upwards\n node.children.splice(index, 1);\n path.push(node);\n this._condense(path);\n return this;\n }\n }\n\n if (!goingUp && !node.leaf && contains(node, bbox)) { // go down\n path.push(node);\n indexes.push(i);\n i = 0;\n parent = node;\n node = node.children[0];\n\n } else if (parent) { // go right\n i++;\n node = parent.children[i];\n goingUp = false;\n\n } else node = null; // nothing found\n }\n\n return this;\n },\n\n toBBox: function (item) { return item; },\n\n compareMinX: compareNodeMinX,\n compareMinY: compareNodeMinY,\n\n toJSON: function () { return this.data; },\n\n fromJSON: function (data) {\n this.data = data;\n return this;\n },\n\n _all: function (node, result) {\n var nodesToSearch = [];\n while (node) {\n if (node.leaf) result.push.apply(result, node.children);\n else nodesToSearch.push.apply(nodesToSearch, node.children);\n\n node = nodesToSearch.pop();\n }\n return result;\n },\n\n _build: function (items, left, right, height) {\n\n var N = right - left + 1,\n M = this._maxEntries,\n node;\n\n if (N <= M) {\n // reached leaf level; return leaf\n node = createNode(items.slice(left, right + 1));\n calcBBox(node, this.toBBox);\n return node;\n }\n\n if (!height) {\n // target height of the bulk-loaded tree\n height = Math.ceil(Math.log(N) / Math.log(M));\n\n // target number of root entries to maximize storage utilization\n M = Math.ceil(N / Math.pow(M, height - 1));\n }\n\n node = createNode([]);\n node.leaf = false;\n node.height = height;\n\n // split the items into M mostly square tiles\n\n var N2 = Math.ceil(N / M),\n N1 = N2 * Math.ceil(Math.sqrt(M)),\n i, j, right2, right3;\n\n multiSelect(items, left, right, N1, this.compareMinX);\n\n for (i = left; i <= right; i += N1) {\n\n right2 = Math.min(i + N1 - 1, right);\n\n multiSelect(items, i, right2, N2, this.compareMinY);\n\n for (j = i; j <= right2; j += N2) {\n\n right3 = Math.min(j + N2 - 1, right2);\n\n // pack each entry recursively\n node.children.push(this._build(items, j, right3, height - 1));\n }\n }\n\n calcBBox(node, this.toBBox);\n\n return node;\n },\n\n _chooseSubtree: function (bbox, node, level, path) {\n\n var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;\n\n while (true) {\n path.push(node);\n\n if (node.leaf || path.length - 1 === level) break;\n\n minArea = minEnlargement = Infinity;\n\n for (i = 0, len = node.children.length; i < len; i++) {\n child = node.children[i];\n area = bboxArea(child);\n enlargement = enlargedArea(bbox, child) - area;\n\n // choose entry with the least area enlargement\n if (enlargement < minEnlargement) {\n minEnlargement = enlargement;\n minArea = area < minArea ? area : minArea;\n targetNode = child;\n\n } else if (enlargement === minEnlargement) {\n // otherwise choose one with the smallest area\n if (area < minArea) {\n minArea = area;\n targetNode = child;\n }\n }\n }\n\n node = targetNode || node.children[0];\n }\n\n return node;\n },\n\n _insert: function (item, level, isNode) {\n\n var toBBox = this.toBBox,\n bbox = isNode ? item : toBBox(item),\n insertPath = [];\n\n // find the best node for accommodating the item, saving all nodes along the path too\n var node = this._chooseSubtree(bbox, this.data, level, insertPath);\n\n // put the item into the node\n node.children.push(item);\n extend(node, bbox);\n\n // split on node overflow; propagate upwards if necessary\n while (level >= 0) {\n if (insertPath[level].children.length > this._maxEntries) {\n this._split(insertPath, level);\n level--;\n } else break;\n }\n\n // adjust bboxes along the insertion path\n this._adjustParentBBoxes(bbox, insertPath, level);\n },\n\n // split overflowed node into two\n _split: function (insertPath, level) {\n\n var node = insertPath[level],\n M = node.children.length,\n m = this._minEntries;\n\n this._chooseSplitAxis(node, m, M);\n\n var splitIndex = this._chooseSplitIndex(node, m, M);\n\n var newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));\n newNode.height = node.height;\n newNode.leaf = node.leaf;\n\n calcBBox(node, this.toBBox);\n calcBBox(newNode, this.toBBox);\n\n if (level) insertPath[level - 1].children.push(newNode);\n else this._splitRoot(node, newNode);\n },\n\n _splitRoot: function (node, newNode) {\n // split root node\n this.data = createNode([node, newNode]);\n this.data.height = node.height + 1;\n this.data.leaf = false;\n calcBBox(this.data, this.toBBox);\n },\n\n _chooseSplitIndex: function (node, m, M) {\n\n var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index;\n\n minOverlap = minArea = Infinity;\n\n for (i = m; i <= M - m; i++) {\n bbox1 = distBBox(node, 0, i, this.toBBox);\n bbox2 = distBBox(node, i, M, this.toBBox);\n\n overlap = intersectionArea(bbox1, bbox2);\n area = bboxArea(bbox1) + bboxArea(bbox2);\n\n // choose distribution with minimum overlap\n if (overlap < minOverlap) {\n minOverlap = overlap;\n index = i;\n\n minArea = area < minArea ? area : minArea;\n\n } else if (overlap === minOverlap) {\n // otherwise choose distribution with minimum area\n if (area < minArea) {\n minArea = area;\n index = i;\n }\n }\n }\n\n return index;\n },\n\n // sorts node children by the best axis for split\n _chooseSplitAxis: function (node, m, M) {\n\n var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX,\n compareMinY = node.leaf ? this.compareMinY : compareNodeMinY,\n xMargin = this._allDistMargin(node, m, M, compareMinX),\n yMargin = this._allDistMargin(node, m, M, compareMinY);\n\n // if total distributions margin value is minimal for x, sort by minX,\n // otherwise it's already sorted by minY\n if (xMargin < yMargin) node.children.sort(compareMinX);\n },\n\n // total margin of all possible split distributions where each node is at least m full\n _allDistMargin: function (node, m, M, compare) {\n\n node.children.sort(compare);\n\n var toBBox = this.toBBox,\n leftBBox = distBBox(node, 0, m, toBBox),\n rightBBox = distBBox(node, M - m, M, toBBox),\n margin = bboxMargin(leftBBox) + bboxMargin(rightBBox),\n i, child;\n\n for (i = m; i < M - m; i++) {\n child = node.children[i];\n extend(leftBBox, node.leaf ? toBBox(child) : child);\n margin += bboxMargin(leftBBox);\n }\n\n for (i = M - m - 1; i >= m; i--) {\n child = node.children[i];\n extend(rightBBox, node.leaf ? toBBox(child) : child);\n margin += bboxMargin(rightBBox);\n }\n\n return margin;\n },\n\n _adjustParentBBoxes: function (bbox, path, level) {\n // adjust bboxes along the given tree path\n for (var i = level; i >= 0; i--) {\n extend(path[i], bbox);\n }\n },\n\n _condense: function (path) {\n // go through the path, removing empty nodes and updating bboxes\n for (var i = path.length - 1, siblings; i >= 0; i--) {\n if (path[i].children.length === 0) {\n if (i > 0) {\n siblings = path[i - 1].children;\n siblings.splice(siblings.indexOf(path[i]), 1);\n\n } else this.clear();\n\n } else calcBBox(path[i], this.toBBox);\n }\n },\n\n _initFormat: function (format) {\n // data format (minX, minY, maxX, maxY accessors)\n\n // uses eval-type function compilation instead of just accepting a toBBox function\n // because the algorithms are very sensitive to sorting functions performance,\n // so they should be dead simple and without inner calls\n\n var compareArr = ['return a', ' - b', ';'];\n\n this.compareMinX = new Function('a', 'b', compareArr.join(format[0]));\n this.compareMinY = new Function('a', 'b', compareArr.join(format[1]));\n\n this.toBBox = new Function('a',\n 'return {minX: a' + format[0] +\n ', minY: a' + format[1] +\n ', maxX: a' + format[2] +\n ', maxY: a' + format[3] + '};');\n }\n};\n\nfunction findItem(item, items, equalsFn) {\n if (!equalsFn) return items.indexOf(item);\n\n for (var i = 0; i < items.length; i++) {\n if (equalsFn(item, items[i])) return i;\n }\n return -1;\n}\n\n// calculate node's bbox from bboxes of its children\nfunction calcBBox(node, toBBox) {\n distBBox(node, 0, node.children.length, toBBox, node);\n}\n\n// min bounding rectangle of node children from k to p-1\nfunction distBBox(node, k, p, toBBox, destNode) {\n if (!destNode) destNode = createNode(null);\n destNode.minX = Infinity;\n destNode.minY = Infinity;\n destNode.maxX = -Infinity;\n destNode.maxY = -Infinity;\n\n for (var i = k, child; i < p; i++) {\n child = node.children[i];\n extend(destNode, node.leaf ? toBBox(child) : child);\n }\n\n return destNode;\n}\n\nfunction extend(a, b) {\n a.minX = Math.min(a.minX, b.minX);\n a.minY = Math.min(a.minY, b.minY);\n a.maxX = Math.max(a.maxX, b.maxX);\n a.maxY = Math.max(a.maxY, b.maxY);\n return a;\n}\n\nfunction compareNodeMinX(a, b) { return a.minX - b.minX; }\nfunction compareNodeMinY(a, b) { return a.minY - b.minY; }\n\nfunction bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }\nfunction bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }\n\nfunction enlargedArea(a, b) {\n return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *\n (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));\n}\n\nfunction intersectionArea(a, b) {\n var minX = Math.max(a.minX, b.minX),\n minY = Math.max(a.minY, b.minY),\n maxX = Math.min(a.maxX, b.maxX),\n maxY = Math.min(a.maxY, b.maxY);\n\n return Math.max(0, maxX - minX) *\n Math.max(0, maxY - minY);\n}\n\nfunction contains(a, b) {\n return a.minX <= b.minX &&\n a.minY <= b.minY &&\n b.maxX <= a.maxX &&\n b.maxY <= a.maxY;\n}\n\nfunction intersects(a, b) {\n return b.minX <= a.maxX &&\n b.minY <= a.maxY &&\n b.maxX >= a.minX &&\n b.maxY >= a.minY;\n}\n\nfunction createNode(children) {\n return {\n children: children,\n height: 1,\n leaf: true,\n minX: Infinity,\n minY: Infinity,\n maxX: -Infinity,\n maxY: -Infinity\n };\n}\n\n// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;\n// combines selection algorithm with binary divide & conquer approach\n\nfunction multiSelect(arr, left, right, n, compare) {\n var stack = [left, right],\n mid;\n\n while (stack.length) {\n right = stack.pop();\n left = stack.pop();\n\n if (right - left <= n) continue;\n\n mid = left + Math.ceil((right - left) / n / 2) * n;\n quickselect(arr, mid, left, right, compare);\n\n stack.push(left, mid, mid, right);\n }\n}\n", "'use strict';\n\nmodule.exports = lineclip;\n\nlineclip.polyline = lineclip;\nlineclip.polygon = polygonclip;\n\n\n// Cohen-Sutherland line clippign algorithm, adapted to efficiently\n// handle polylines rather than just segments\n\nfunction lineclip(points, bbox, result) {\n\n var len = points.length,\n codeA = bitCode(points[0], bbox),\n part = [],\n i, a, b, codeB, lastCode;\n\n if (!result) result = [];\n\n for (i = 1; i < len; i++) {\n a = points[i - 1];\n b = points[i];\n codeB = lastCode = bitCode(b, bbox);\n\n while (true) {\n\n if (!(codeA | codeB)) { // accept\n part.push(a);\n\n if (codeB !== lastCode) { // segment went outside\n part.push(b);\n\n if (i < len - 1) { // start a new line\n result.push(part);\n part = [];\n }\n } else if (i === len - 1) {\n part.push(b);\n }\n break;\n\n } else if (codeA & codeB) { // trivial reject\n break;\n\n } else if (codeA) { // a outside, intersect with clip edge\n a = intersect(a, b, codeA, bbox);\n codeA = bitCode(a, bbox);\n\n } else { // b outside\n b = intersect(a, b, codeB, bbox);\n codeB = bitCode(b, bbox);\n }\n }\n\n codeA = lastCode;\n }\n\n if (part.length) result.push(part);\n\n return result;\n}\n\n// Sutherland-Hodgeman polygon clipping algorithm\n\nfunction polygonclip(points, bbox) {\n\n var result, edge, prev, prevInside, i, p, inside;\n\n // clip against each side of the clip rectangle\n for (edge = 1; edge <= 8; edge *= 2) {\n result = [];\n prev = points[points.length - 1];\n prevInside = !(bitCode(prev, bbox) & edge);\n\n for (i = 0; i < points.length; i++) {\n p = points[i];\n inside = !(bitCode(p, bbox) & edge);\n\n // if segment goes through the clip window, add an intersection\n if (inside !== prevInside) result.push(intersect(prev, p, edge, bbox));\n\n if (inside) result.push(p); // add a point if it's inside\n\n prev = p;\n prevInside = inside;\n }\n\n points = result;\n\n if (!points.length) break;\n }\n\n return result;\n}\n\n// intersect a segment against one of the 4 lines that make up the bbox\n\nfunction intersect(a, b, edge, bbox) {\n return edge & 8 ? [a[0] + (b[0] - a[0]) * (bbox[3] - a[1]) / (b[1] - a[1]), bbox[3]] : // top\n edge & 4 ? [a[0] + (b[0] - a[0]) * (bbox[1] - a[1]) / (b[1] - a[1]), bbox[1]] : // bottom\n edge & 2 ? [bbox[2], a[1] + (b[1] - a[1]) * (bbox[2] - a[0]) / (b[0] - a[0])] : // right\n edge & 1 ? [bbox[0], a[1] + (b[1] - a[1]) * (bbox[0] - a[0]) / (b[0] - a[0])] : // left\n null;\n}\n\n// bit code reflects the point position relative to the bbox:\n\n// left mid right\n// top 1001 1000 1010\n// mid 0001 0000 0010\n// bottom 0101 0100 0110\n\nfunction bitCode(p, bbox) {\n var code = 0;\n\n if (p[0] < bbox[0]) code |= 1; // left\n else if (p[0] > bbox[2]) code |= 2; // right\n\n if (p[1] < bbox[1]) code |= 4; // bottom\n else if (p[1] > bbox[3]) code |= 8; // top\n\n return code;\n}\n", "'use strict';\n\nvar rbush = require('rbush');\nvar lineclip = require('lineclip');\n\nmodule.exports = whichPolygon;\n\nfunction whichPolygon(data) {\n var bboxes = [];\n for (var i = 0; i < data.features.length; i++) {\n var feature = data.features[i];\n\n // unlocated GeoJSON features can have null `geometry`\n if (!feature.geometry) continue;\n\n var coords = feature.geometry.coordinates;\n\n if (feature.geometry.type === 'Polygon') {\n bboxes.push(treeItem(coords, feature.properties));\n\n } else if (feature.geometry.type === 'MultiPolygon') {\n for (var j = 0; j < coords.length; j++) {\n bboxes.push(treeItem(coords[j], feature.properties));\n }\n }\n }\n\n var tree = rbush().load(bboxes);\n\n function query(p, multi) {\n var output = [],\n result = tree.search({\n minX: p[0],\n minY: p[1],\n maxX: p[0],\n maxY: p[1]\n });\n for (var i = 0; i < result.length; i++) {\n if (insidePolygon(result[i].coords, p)) {\n if (multi)\n output.push(result[i].props);\n else\n return result[i].props;\n }\n }\n return multi && output.length ? output : null;\n }\n\n query.tree = tree;\n query.bbox = function queryBBox(bbox) {\n var output = [];\n var result = tree.search({\n minX: bbox[0],\n minY: bbox[1],\n maxX: bbox[2],\n maxY: bbox[3]\n });\n for (var i = 0; i < result.length; i++) {\n if (polygonIntersectsBBox(result[i].coords, bbox)) {\n output.push(result[i].props);\n }\n }\n return output;\n };\n\n return query;\n}\n\nfunction polygonIntersectsBBox(polygon, bbox) {\n var bboxCenter = [\n (bbox[0] + bbox[2]) / 2,\n (bbox[1] + bbox[3]) / 2\n ];\n if (insidePolygon(polygon, bboxCenter)) return true;\n for (var i = 0; i < polygon.length; i++) {\n if (lineclip(polygon[i], bbox).length > 0) return true;\n }\n return false;\n}\n\n// ray casting algorithm for detecting if point is in polygon\nfunction insidePolygon(rings, p) {\n var inside = false;\n for (var i = 0, len = rings.length; i < len; i++) {\n var ring = rings[i];\n for (var j = 0, len2 = ring.length, k = len2 - 1; j < len2; k = j++) {\n if (rayIntersect(p, ring[j], ring[k])) inside = !inside;\n }\n }\n return inside;\n}\n\nfunction rayIntersect(p, p1, p2) {\n return ((p1[1] > p[1]) !== (p2[1] > p[1])) && (p[0] < (p2[0] - p1[0]) * (p[1] - p1[1]) / (p2[1] - p1[1]) + p1[0]);\n}\n\nfunction treeItem(coords, props) {\n var item = {\n minX: Infinity,\n minY: Infinity,\n maxX: -Infinity,\n maxY: -Infinity,\n coords: coords,\n props: props\n };\n\n for (var i = 0; i < coords[0].length; i++) {\n var p = coords[0][i];\n item.minX = Math.min(item.minX, p[0]);\n item.minY = Math.min(item.minY, p[1]);\n item.maxX = Math.max(item.maxX, p[0]);\n item.maxY = Math.max(item.maxY, p[1]);\n }\n return item;\n}\n", "/* eslint @typescript-eslint/no-this-alias: \"warn\" */\nimport whichPolygon from 'which-polygon';\n\nimport { simplify } from './simplify.ts';\n\n// JSON\nimport matchGroupsJSON from '../config/matchGroups.json' with {type: 'json'};\nimport genericWordsJSON from '../config/genericWords.json' with {type: 'json'};\nimport treesJSON from '../config/trees.json' with {type: 'json'};\n\nconst matchGroups = matchGroupsJSON.matchGroups;\nconst trees = treesJSON.trees;\n\n\ntype Vec2 = [number, number];\ntype Vec3 = [number, number, number];\ntype Location = Vec2 | Vec3 | string | number;\n\ninterface LocationSet {\n include?: Array,\n exclude?: Array\n};\n\ninterface LocationConflation {\n validateLocation: (a: Location) => unknown;\n resolveLocation: (a: Location) => unknown;\n validateLocationSet: (a: LocationSet) => unknown;\n resolveLocationSet: (a: LocationSet) => unknown;\n};\n\ntype HitType = 'primary' | 'alternate' | 'excludeGeneric' | 'excludeNamed';\ninterface Hit {\n match: HitType;\n itemID?: string;\n area?: number;\n kv?: string;\n nsimple?: string;\n pattern?: string;\n};\n\n\nexport class Matcher {\n private matchIndex;\n private genericWords = new Map();\n private itemLocation;\n private locationSets;\n private locationIndex;\n private warnings: Array = [];\n\n\n // `constructor`\n // initialize the genericWords regexes\n constructor() {\n // The `matchIndex` is a specialized structure that allows us to quickly answer\n // _\"Given a [key/value tagpair, name, location], what canonical items (brands etc) can match it?\"_\n //\n // The index contains all valid combinations of k/v tagpairs and names\n // matchIndex:\n // {\n // 'k/v': {\n // 'primary': Map (String 'nsimple' -> Set (itemIDs…), // matches for tags like `name`, `name:xx`, etc.\n // 'alternate': Map (String 'nsimple' -> Set (itemIDs…), // matches for tags like `alt_name`, `brand`, etc.\n // 'excludeNamed': Map (String 'pattern' -> RegExp),\n // 'excludeGeneric': Map (String 'pattern' -> RegExp)\n // },\n // }\n //\n // {\n // 'amenity/bank': {\n // 'primary': {\n // 'firstbank': Set (\"firstbank-978cca\", \"firstbank-9794e6\", \"firstbank-f17495\", …),\n // …\n // },\n // 'alternate': {\n // '1stbank': Set (\"firstbank-f17495\"),\n // …\n // }\n // },\n // 'shop/supermarket': {\n // 'primary': {\n // 'coop': Set (\"coop-76454b\", \"coop-ebf2d9\", \"coop-36e991\", …),\n // 'coopfood': Set (\"coopfood-a8278b\", …),\n // …\n // },\n // 'alternate': {\n // 'coop': Set (\"coopfood-a8278b\", …),\n // 'federatedcooperatives': Set (\"coop-76454b\", …),\n // 'thecooperative': Set (\"coopfood-a8278b\", …),\n // …\n // }\n // }\n // }\n //\n this.matchIndex = undefined;\n\n // The `genericWords` structure matches the contents of genericWords.json to instantiated RegExp objects\n // Map (String 'pattern' -> RegExp),\n this.genericWords = new Map();\n (genericWordsJSON.genericWords || []).forEach(s => this.genericWords.set(s, new RegExp(s, 'i')));\n\n // The `itemLocation` structure maps itemIDs to locationSetIDs:\n // {\n // 'firstbank-f17495': '+[first_bank_western_us.geojson]',\n // 'firstbank-978cca': '+[first_bank_carolinas.geojson]',\n // 'coop-76454b': '+[Q16]',\n // 'coopfood-a8278b': '+[Q23666]',\n // …\n // }\n this.itemLocation = undefined;\n\n // The `locationSets` structure maps locationSetIDs to *resolved* locationSets:\n // {\n // '+[first_bank_western_us.geojson]': GeoJSON {…},\n // '+[first_bank_carolinas.geojson]': GeoJSON {…},\n // '+[Q16]': GeoJSON {…},\n // '+[Q23666]': GeoJSON {…},\n // …\n // }\n this.locationSets = undefined;\n\n // The `locationIndex` is an instance of which-polygon spatial index for the locationSets.\n this.locationIndex = undefined;\n\n // Array of match conflict pairs (currently unused)\n this.warnings = [];\n }\n\n\n //\n // `buildMatchIndex()`\n // Call this to prepare the matcher for use\n //\n // `data` needs to be an Object indexed on a 'tree/key/value' path.\n // (e.g. cache filled by `fileTree.read` or data found in `dist/nsi.json`)\n // {\n // 'brands/amenity/bank': { properties: {}, items: [ {}, {}, … ] },\n // 'brands/amenity/bar': { properties: {}, items: [ {}, {}, … ] },\n // …\n // }\n //\n buildMatchIndex(data: Record): void {\n const that = this;\n if (that.matchIndex) return; // it was built already\n that.matchIndex = new Map();\n\n const seenTree = new Map(); // warn if the same [k, v, nsimple] appears in multiple trees - #5625\n\n Object.keys(data).forEach(tkv => {\n const category = data[tkv];\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const t = parts[0];\n const k = parts[1];\n const v = parts[2];\n const thiskv = `${k}/${v}`;\n const tree = trees[t];\n\n let branch = that.matchIndex.get(thiskv);\n if (!branch) {\n branch = {\n primary: new Map(),\n alternate: new Map(),\n excludeGeneric: new Map(),\n excludeNamed: new Map()\n };\n that.matchIndex.set(thiskv, branch);\n }\n\n // ADD EXCLUSIONS\n const properties = category.properties || {};\n const exclude = properties.exclude || {};\n (exclude.generic || []).forEach(s => branch.excludeGeneric.set(s, new RegExp(s, 'i')));\n (exclude.named || []).forEach(s => branch.excludeNamed.set(s, new RegExp(s, 'i')));\n const excludeRegexes = [...branch.excludeGeneric.values(), ...branch.excludeNamed.values()];\n\n\n // ADD ITEMS\n const items = category.items;\n if (!Array.isArray(items) || !items.length) return;\n\n\n // Primary name patterns, match tags to take first\n // e.g. `name`, `name:ru`\n const primaryName = new RegExp(tree.nameTags.primary, 'i');\n\n // Alternate name patterns, match tags to consider after primary\n // e.g. `alt_name`, `short_name`, `brand`, `brand:ru`, etc..\n const alternateName = new RegExp(tree.nameTags.alternate, 'i');\n\n // There are a few exceptions to the name matching regexes.\n // Usually a tag suffix contains a language code like `name:en`, `name:ru`\n // but we want to exclude things like `operator:type`, `name:etymology`, etc..\n const notName = /:(colou?r|type|forward|backward|left|right|etymology|pronunciation|signed|wikipedia)$/i;\n\n // For certain categories we do not want to match generic KV pairs like `building/yes` or `amenity/yes`\n const skipGenericKV = skipGenericKVMatches(t, k, v);\n\n // We will collect the generic KV pairs anyway (for the purpose of filtering them out of matchTags)\n const genericKV = new Set([`${k}/yes`, `building/yes`]);\n\n // Collect alternate tagpairs for this kv category from matchGroups.\n // We might also pick up a few more generic KVs (like `shop/yes`)\n const matchGroupKV = new Set();\n Object.values(matchGroups).forEach(matchGroup => {\n const inGroup = matchGroup.some(otherkv => otherkv === thiskv);\n if (!inGroup) return;\n\n matchGroup.forEach(otherkv => {\n if (otherkv === thiskv) return; // skip self\n matchGroupKV.add(otherkv);\n\n const otherk = otherkv.split('/', 2)[0]; // we might pick up a `shop/yes`\n genericKV.add(`${otherk}/yes`);\n });\n });\n\n // For each item, insert all [key, value, name] combinations into the match index\n items.forEach(item => {\n if (!item.id) return;\n\n // Automatically remove redundant `matchTags` - #3417, #8137\n // (i.e. This kv is already covered by matchGroups, so it doesn't need to be in `item.matchTags`\n // or this kv is the primary kv, so it doesn't need to be duplicated in `item.matchTags`)\n if (Array.isArray(item.matchTags) && item.matchTags.length) {\n item.matchTags = item.matchTags\n .filter(matchTag => !matchGroupKV.has(matchTag) && (matchTag !== thiskv) && !genericKV.has(matchTag));\n\n if (!item.matchTags.length) delete item.matchTags;\n }\n\n // key/value tagpairs to insert into the match index..\n let kvTags = [`${thiskv}`]\n .concat(item.matchTags || []);\n\n if (!skipGenericKV) {\n kvTags = kvTags\n .concat(Array.from(genericKV)); // #3454 - match some generic tags\n }\n\n // Index all the namelike tag values\n Object.keys(item.tags).forEach(osmkey => {\n if (notName.test(osmkey)) return; // osmkey is not a namelike tag, skip\n const osmvalue = item.tags[osmkey];\n if (!osmvalue || excludeRegexes.some(regex => regex.test(osmvalue))) return; // osmvalue missing or excluded\n\n if (primaryName.test(osmkey)) {\n kvTags.forEach(kv => insertName('primary', t, kv, simplify(osmvalue), item.id));\n } else if (alternateName.test(osmkey)) {\n kvTags.forEach(kv => insertName('alternate', t, kv, simplify(osmvalue), item.id));\n }\n });\n\n // Index `matchNames` after indexing all other names..\n const keepMatchNames = new Set();\n (item.matchNames || []).forEach(matchName => {\n // If this matchname isn't already indexed, add it to the alternate index\n const nsimple = simplify(matchName);\n kvTags.forEach(kv => {\n const branch = that.matchIndex.get(kv);\n const primaryLeaf = branch && branch.primary.get(nsimple);\n const alternateLeaf = branch && branch.alternate.get(nsimple);\n const inPrimary = primaryLeaf && primaryLeaf.has(item.id);\n const inAlternate = alternateLeaf && alternateLeaf.has(item.id);\n\n if (!inPrimary && !inAlternate) {\n insertName('alternate', t, kv, nsimple, item.id);\n keepMatchNames.add(matchName);\n }\n });\n });\n\n // Automatically remove redundant `matchNames` - #3417\n // (i.e. This name got indexed some other way, so it doesn't need to be in `item.matchNames`)\n if (keepMatchNames.size) {\n item.matchNames = Array.from(keepMatchNames);\n } else {\n delete item.matchNames;\n }\n\n }); // each item\n }); // each tkv\n\n\n // Insert this item into the matchIndex\n function insertName(which: string, t: string, kv: string, nsimple: string, itemID: string) {\n if (!nsimple) {\n that.warnings.push(`Warning: skipping empty ${which} name for item ${t}/${kv}: ${itemID}`);\n return;\n }\n\n let branch = that.matchIndex.get(kv);\n if (!branch) {\n branch = {\n primary: new Map(),\n alternate: new Map(),\n excludeGeneric: new Map(),\n excludeNamed: new Map()\n };\n that.matchIndex.set(kv, branch);\n }\n\n let leaf = branch[which].get(nsimple);\n if (!leaf) {\n leaf = new Set();\n branch[which].set(nsimple, leaf);\n }\n\n leaf.add(itemID); // insert\n\n // check for duplicates - #5625\n if (!/yes$/.test(kv)) { // ignore genericKV like amenity/yes, building/yes, etc\n const kvnsimple = `${kv}/${nsimple}`;\n const existing = seenTree.get(kvnsimple);\n if (existing && existing !== t) {\n const items = Array.from(leaf);\n that.warnings.push(`Duplicate cache key \"${kvnsimple}\" in trees \"${t}\" and \"${existing}\", check items: ${items}`);\n return;\n }\n seenTree.set(kvnsimple, t);\n }\n }\n\n // For certain categories we do not want to match generic KV pairs like `building/yes` or `amenity/yes`\n function skipGenericKVMatches(t: string, k: string, v: string): boolean {\n return (\n t === 'flags' ||\n t === 'transit' ||\n k === 'landuse' ||\n v === 'atm' ||\n v === 'bicycle_parking' ||\n v === 'car_sharing' ||\n v === 'caravan_site' ||\n v === 'charging_station' ||\n v === 'dog_park' ||\n v === 'parking' ||\n v === 'phone' ||\n v === 'playground' ||\n v === 'post_box' ||\n v === 'public_bookcase' ||\n v === 'recycling' ||\n v === 'vending_machine'\n );\n }\n }\n\n\n //\n // `buildLocationIndex()`\n // Call this to prepare a which-polygon location index.\n // This *resolves* all the locationSets into GeoJSON, which takes some time.\n // You can skip this step if you don't care about matching within a location.\n //\n // `data` needs to be an Object indexed on a 'tree/key/value' path.\n // (e.g. cache filled by `fileTree.read` or data found in `dist/nsi.json`)\n // {\n // 'brands/amenity/bank': { properties: {}, items: [ {}, {}, … ] },\n // 'brands/amenity/bar': { properties: {}, items: [ {}, {}, … ] },\n // …\n // }\n //\n buildLocationIndex(data: Record, loco: LocationConflation): void {\n const that = this;\n if (that.locationIndex) return; // it was built already\n\n that.itemLocation = new Map();\n that.locationSets = new Map();\n\n Object.keys(data).forEach(tkv => {\n const items = data[tkv].items;\n if (!Array.isArray(items) || !items.length) return;\n\n items.forEach(item => {\n if (that.itemLocation.has(item.id)) return; // we've seen item id already - shouldn't be possible?\n\n let resolved;\n try {\n resolved = loco.resolveLocationSet(item.locationSet); // resolve a feature for this locationSet\n } catch (err: unknown) {\n const message = (err instanceof Error) ? err.message : err;\n console.warn(`buildLocationIndex: ${message}`); // couldn't resolve\n }\n if (!resolved || !resolved.id) return;\n\n that.itemLocation.set(item.id, resolved.id); // link it to the item\n if (that.locationSets.has(resolved.id)) return; // we've seen this locationSet feature before..\n\n // First time seeing this locationSet feature, make a copy and add to locationSet cache..\n const feature = _cloneDeep(resolved.feature);\n feature.id = resolved.id; // Important: always use the locationSet `id` (`+[Q30]`), not the feature `id` (`Q30`)\n feature.properties.id = resolved.id;\n\n if (!feature.geometry.coordinates.length || !feature.properties.area) {\n console.warn(`buildLocationIndex: locationSet ${resolved.id} for ${item.id} resolves to an empty feature:`);\n console.warn(JSON.stringify(feature));\n return;\n }\n\n that.locationSets.set(resolved.id, feature);\n });\n });\n\n that.locationIndex = whichPolygon({ type: 'FeatureCollection', features: [...that.locationSets.values()] });\n\n function _cloneDeep(obj) {\n return JSON.parse(JSON.stringify(obj));\n }\n }\n\n\n //\n // `match()`\n // Pass parts and return an Array of matches.\n // `k` - key\n // `v` - value\n // `n` - namelike\n // `loc` - optional - [lon,lat] location to search\n //\n // 1. If the [k,v,n] tuple matches a canonical item…\n // Return an Array of match results.\n // Each result will include the area in km² that the item is valid.\n //\n // Order of results:\n // Primary ordering will be on the \"match\" column:\n // \"primary\" - where the query matches the `name` tag, followed by\n // \"alternate\" - where the query matches an alternate name tag (e.g. short_name, brand, operator, etc)\n // Secondary ordering will be on the \"area\" column:\n // \"area descending\" if no location was provided, (worldwide before local)\n // \"area ascending\" if location was provided (local before worldwide)\n //\n // [\n // { match: 'primary', itemID: String, area: Number, kv: String, nsimple: String },\n // { match: 'primary', itemID: String, area: Number, kv: String, nsimple: String },\n // { match: 'alternate', itemID: String, area: Number, kv: String, nsimple: String },\n // { match: 'alternate', itemID: String, area: Number, kv: String, nsimple: String },\n // …\n // ]\n //\n // -or-\n //\n // 2. If the [k,v,n] tuple matches an exclude pattern…\n // Return an Array with a single exclude result, either\n //\n // [ { match: 'excludeGeneric', pattern: String, kv: String } ] // \"generic\" e.g. \"Food Court\"\n // or\n // [ { match: 'excludeNamed', pattern: String, kv: String } ] // \"named\", e.g. \"Kebabai\"\n //\n // About results\n // \"generic\" - a generic word that is probably not really a name.\n // For these, iD should warn the user \"Hey don't put 'food court' in the name tag\".\n // \"named\" - a real name like \"Kebabai\" that is just common, but not a brand.\n // For these, iD should just let it be. We don't include these in NSI, but we don't want to nag users about it either.\n //\n // -or-\n //\n // 3. If the [k,v,n] tuple matches nothing of any kind, return `null`\n //\n //\n match(k: string, v: string, n: string, loc?: Vec2): Array | null {\n const that = this;\n if (!that.matchIndex) {\n throw new Error('match: matchIndex not built.');\n }\n\n // If we were supplied a location, and a that.locationIndex has been set up,\n // get the locationSets that are valid there so we can filter results.\n let matchLocations;\n if (Array.isArray(loc) && that.locationIndex) {\n // which-polygon query returns an array of GeoJSON properties, pass true to return all results\n matchLocations = that.locationIndex([loc[0], loc[1], loc[0], loc[1]], true);\n }\n\n const nsimple = simplify(n);\n\n const seen = new Set();\n const results: Array = [];\n gatherResults('primary');\n gatherResults('alternate');\n if (results.length) return results;\n\n gatherResults('exclude');\n return results.length ? results : null;\n\n\n function gatherResults(which: string): void {\n // First try an exact match on k/v\n const kv = `${k}/${v}`;\n let didMatch = tryMatch(which, kv);\n if (didMatch) return;\n\n // If that didn't work, look in match groups for other pairs considered equivalent to k/v..\n for (const mg in matchGroups) {\n const matchGroup = matchGroups[mg];\n const inGroup = matchGroup.some(otherkv => otherkv === kv);\n if (!inGroup) continue;\n\n for (const otherkv of matchGroup) {\n if (otherkv === kv) continue; // skip self\n didMatch = tryMatch(which, otherkv);\n if (didMatch) return;\n }\n }\n\n // If finished 'exclude' pass and still haven't matched anything, try the global `genericWords.json` patterns\n if (which === 'exclude') {\n const regex = [...that.genericWords.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeGeneric', pattern: String(regex) }); // note no `branch`, no `kv`\n return;\n }\n }\n }\n\n function tryMatch(which: string, kv: string): boolean {\n const branch = that.matchIndex.get(kv);\n if (!branch) return false;\n\n if (which === 'exclude') { // Test name `n` against named and generic exclude patterns\n let regex = [...branch.excludeNamed.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeNamed', pattern: String(regex), kv: kv });\n return false;\n }\n regex = [...branch.excludeGeneric.values()].find(regex => regex.test(n));\n if (regex) {\n results.push({ match: 'excludeGeneric', pattern: String(regex), kv: kv });\n return false;\n }\n return false;\n }\n\n const leaf = branch[which].get(nsimple);\n if (!leaf || !leaf.size) return false;\n if (!(which === 'primary' || which === 'alternate')) return false;\n\n // If we get here, we matched something..\n // Prepare the results, calculate areas (if location index was set up)\n let hits: Array = [];\n for (const itemID of [...leaf]) {\n let area = Infinity;\n if (that.itemLocation && that.locationSets) {\n const location = that.locationSets.get(that.itemLocation.get(itemID));\n area = (location && location.properties.area) || Infinity;\n }\n hits.push({ match: which, itemID: itemID, area: area, kv: kv, nsimple: nsimple });\n }\n\n let sortFn = byAreaDescending;\n\n // Filter the match to include only results valid in the requested `loc`..\n if (matchLocations) {\n hits = hits.filter(isValidLocation);\n sortFn = byAreaAscending;\n }\n\n if (!hits.length) return false;\n\n // push results\n hits.sort(sortFn).forEach(hit => {\n if (seen.has(hit.itemID)) return;\n seen.add(hit.itemID);\n results.push(hit);\n });\n\n return true;\n\n\n function isValidLocation(hit: Hit): boolean {\n if (!that.itemLocation) return true;\n return matchLocations.find(props => props.id === that.itemLocation.get(hit.itemID));\n }\n // Sort smaller (more local) locations first.\n function byAreaAscending(hitA: Hit, hitB: Hit): number {\n const areaA = hitA.area || 0;\n const areaB = hitB.area || 0;\n return areaA - areaB;\n }\n // Sort larger (more worldwide) locations first.\n function byAreaDescending(hitA: Hit, hitB: Hit): number {\n const areaA = hitA.area || 0;\n const areaB = hitB.area || 0;\n return areaB - areaA;\n }\n }\n }\n\n\n //\n // `getWarnings()`\n // Return any warnings discovered when buiding the index.\n // (currently this does nothing)\n //\n getWarnings(): Array {\n return this.warnings;\n }\n}\n", "// External\nimport diacritics from 'diacritics';\n\n// remove spaces, punctuation, diacritics\n// for punction see https://stackoverflow.com/a/21224179\nexport function simplify(str?: string): string {\n if (typeof str !== 'string') return '';\n\n return diacritics.remove(\n str\n .replace(/&/g, 'and')\n .replace(/(İ|i̇)/ig, 'i') // for BİM, İşbank - #5017, #8261\n .replace(/[\\s\\-=_!\"#%'*{},.\\/:;?\\(\\)\\[\\]@\\\\$\\^*+<>«»~`’\\u00a1\\u00a7\\u00b6\\u00b7\\u00bf\\u037e\\u0387\\u055a-\\u055f\\u0589\\u05c0\\u05c3\\u05c6\\u05f3\\u05f4\\u0609\\u060a\\u060c\\u060d\\u061b\\u061e\\u061f\\u066a-\\u066d\\u06d4\\u0700-\\u070d\\u07f7-\\u07f9\\u0830-\\u083e\\u085e\\u0964\\u0965\\u0970\\u0af0\\u0df4\\u0e4f\\u0e5a\\u0e5b\\u0f04-\\u0f12\\u0f14\\u0f85\\u0fd0-\\u0fd4\\u0fd9\\u0fda\\u104a-\\u104f\\u10fb\\u1360-\\u1368\\u166d\\u166e\\u16eb-\\u16ed\\u1735\\u1736\\u17d4-\\u17d6\\u17d8-\\u17da\\u1800-\\u1805\\u1807-\\u180a\\u1944\\u1945\\u1a1e\\u1a1f\\u1aa0-\\u1aa6\\u1aa8-\\u1aad\\u1b5a-\\u1b60\\u1bfc-\\u1bff\\u1c3b-\\u1c3f\\u1c7e\\u1c7f\\u1cc0-\\u1cc7\\u1cd3\\u2000-\\u206f\\u2cf9-\\u2cfc\\u2cfe\\u2cff\\u2d70\\u2e00-\\u2e7f\\u3001-\\u3003\\u303d\\u30fb\\ua4fe\\ua4ff\\ua60d-\\ua60f\\ua673\\ua67e\\ua6f2-\\ua6f7\\ua874-\\ua877\\ua8ce\\ua8cf\\ua8f8-\\ua8fa\\ua92e\\ua92f\\ua95f\\ua9c1-\\ua9cd\\ua9de\\ua9df\\uaa5c-\\uaa5f\\uaade\\uaadf\\uaaf0\\uaaf1\\uabeb\\ufe10-\\ufe16\\ufe19\\ufe30\\ufe45\\ufe46\\ufe49-\\ufe4c\\ufe50-\\ufe52\\ufe54-\\ufe57\\ufe5f-\\ufe61\\ufe68\\ufe6a\\ufe6b\\ufeff\\uff01-\\uff03\\uff05-\\uff07\\uff0a\\uff0c\\uff0e\\uff0f\\uff1a\\uff1b\\uff1f\\uff20\\uff3c\\uff61\\uff64\\uff65]+/g,'')\n .toLowerCase()\n );\n}\n", "// Internal\nimport { simplify } from './simplify.ts';\n\n// Removes noise from the name so that we can compare\n// similar names for catching duplicates.\nexport function stemmer(str?: string): string {\n if (typeof str !== 'string') return '';\n\n const noise = [\n /ban(k|c)(a|o)?/ig,\n /банк/ig,\n /coop/ig,\n /express/ig,\n /(gas|fuel)/ig,\n /wireless/ig,\n /(shop|store)/ig\n ];\n\n str = noise.reduce((acc, regex) => acc.replace(regex, ''), str);\n return simplify(str);\n}\n", "import { Matcher } from 'name-suggestion-index';\nimport { fileFetcher, locationManager } from '../core';\nimport { presetManager } from '../presets';\n\nimport { nsiCdnUrl } from '../../config/id.js';\n\n// Make very sure this resolves to iD's `package.json`\n// If you mess up the `../`s, the resolver may import another random package.json from somewhere else.\nimport packageJSON from '../../package.json';\n\n\n// This service contains all the code related to the **name-suggestion-index** (aka NSI)\n// NSI contains the most correct tagging for many commonly mapped features.\n// See https://github.com/osmlab/name-suggestion-index and https://nsi.guide\n\n\n// DATA\n\nlet _nsiStatus = 'loading'; // 'loading', 'ok', 'failed'\nlet _nsi = {};\n\n// Sometimes we can upgrade a feature tagged like `building=yes` to a better tag.\nconst buildingPreset = {\n 'building/commercial': true,\n 'building/government': true,\n 'building/hotel': true,\n 'building/retail': true,\n 'building/office': true,\n 'building/supermarket': true,\n 'building/yes': true\n};\n\n// Exceptions to the namelike regexes.\n// Usually a tag suffix contains a language code like `name:en`, `name:ru`\n// but we want to exclude things like `operator:type`, `name:etymology`, etc..\nconst notNames = /:(colou?r|type|forward|backward|left|right|etymology|pronunciation|wikipedia)$/i;\n\n// Exceptions to the branchlike regexes\nconst notBranches = /(coop|express|wireless|factory|outlet)/i;\n\n\n// PRIVATE FUNCTIONS\n\n// `setNsiSources()`\n// Adds the sources to iD's filemap so we can start downloading data.\n//\nfunction setNsiSources() {\n const nsiVersion = packageJSON.dependencies['name-suggestion-index'] || packageJSON.devDependencies['name-suggestion-index'];\n const cdn = nsiCdnUrl.replace('{version}', nsiVersion);\n const sources = {\n 'nsi_data': cdn + 'dist/json/nsi.min.json',\n 'nsi_dissolved': cdn + 'dist/wikidata/dissolved.min.json',\n 'nsi_features': cdn + 'dist/json/featureCollection.min.json',\n 'nsi_generics': cdn + 'dist/json/genericWords.min.json',\n 'nsi_presets': cdn + 'dist/presets/nsi-id-presets.min.json',\n 'nsi_replacements': cdn + 'dist/json/replacements.min.json',\n 'nsi_trees': cdn + 'dist/json/trees.min.json'\n };\n\n let fileMap = fileFetcher.fileMap();\n for (const k in sources) {\n if (!fileMap[k]) fileMap[k] = sources[k];\n }\n}\n\n\n// `loadNsiPresets()`\n// Returns a Promise fulfilled when the presets have been downloaded and merged into iD.\n//\nfunction loadNsiPresets() {\n return (\n Promise.all([\n fileFetcher.get('nsi_presets'),\n fileFetcher.get('nsi_features')\n ])\n .then(vals => {\n // Add `suggestion=true` to all the nsi presets\n // The preset json schema doesn't include it, but the iD code still uses it\n Object.values(vals[0].presets).forEach(preset => preset.suggestion = true);\n\n // nsi does not specify *:wikipedia (anymore):\n // clean up previous values to prevent that the wikidata/wikipedia information\n // is going to be out of sync, see #9103\n Object.values(vals[0].presets).forEach(preset => {\n if (preset.tags['brand:wikidata']) {\n preset.removeTags = {'brand:wikipedia': '*', ...(preset.removeTags || preset.addTags || preset.tags)};\n }\n if (preset.tags['operator:wikidata']) {\n preset.removeTags = {'operator:wikipedia': '*', ...(preset.removeTags || preset.addTags || preset.tags)};\n }\n if (preset.tags['network:wikidata']) {\n preset.removeTags = {'network:wikipedia': '*', ...(preset.removeTags || preset.addTags || preset.tags)};\n }\n });\n\n presetManager.merge({\n presets: vals[0].presets,\n featureCollection: vals[1]\n });\n })\n );\n}\n\n\n// `loadNsiData()`\n// Returns a Promise fulfilled when the other data have been downloaded and processed\n//\nfunction loadNsiData() {\n return (\n Promise.all([\n fileFetcher.get('nsi_data'),\n fileFetcher.get('nsi_dissolved'),\n fileFetcher.get('nsi_replacements'),\n fileFetcher.get('nsi_trees')\n ])\n .then(vals => {\n _nsi = {\n data: vals[0].nsi, // the raw name-suggestion-index data\n dissolved: vals[1].dissolved, // list of dissolved items\n replacements: vals[2].replacements, // trivial old->new qid replacements\n trees: vals[3].trees, // metadata about trees, main tags\n kvt: new Map(), // Map (k -> Map (v -> t) )\n qids: new Map(), // Map (wd/wp tag values -> qids)\n ids: new Map() // Map (id -> NSI item)\n };\n\n const matcher = new Matcher();\n _nsi.matcher = matcher;\n matcher.buildMatchIndex(_nsi.data);\n\n// *** BEGIN HACK ***\n\n// old - built in matcher will set up the locationindex by resolving all the locationSets one-by-one\n // matcher.buildLocationIndex(_nsi.data, locationManager.loco());\n\n// new - Use the location manager instead of redoing that work\n// It has already processed the presets at this point\n\n// We need to monkeypatch a few of the collections that the NSI matcher depends on.\n// The `itemLocation` structure maps itemIDs to locationSetIDs\nmatcher.itemLocation = new Map();\n\n// The `locationSets` structure maps locationSetIDs to GeoJSON\n// We definitely need this, but don't need full geojson, just { properties: { area: xxx }}\nmatcher.locationSets = new Map();\n\nObject.keys(_nsi.data).forEach(tkv => {\n const items = _nsi.data[tkv].items;\n if (!Array.isArray(items) || !items.length) return;\n\n items.forEach(item => {\n if (matcher.itemLocation.has(item.id)) return; // we've seen item id already - shouldn't be possible?\n\n const locationSetID = locationManager.locationSetID(item.locationSet);\n matcher.itemLocation.set(item.id, locationSetID);\n\n if (matcher.locationSets.has(locationSetID)) return; // we've seen this locationSet before..\n\n const fakeFeature = { id: locationSetID, properties: { id: locationSetID, area: 1 } };\n matcher.locationSets.set(locationSetID, fakeFeature);\n });\n});\n\n// The `locationIndex` is an instance of which-polygon spatial index for the locationSets.\n// We only really need this to _look like_ which-polygon query `_wp.locationIndex(bbox, true);`\n// i.e. it needs to return the properties of the locationsets\nmatcher.locationIndex = (bbox) => {\n const validHere = locationManager.locationSetsAt([bbox[0], bbox[1]]);\n const results = [];\n\n for (const [locationSetID, area] of Object.entries(validHere)) {\n const fakeFeature = matcher.locationSets.get(locationSetID);\n if (fakeFeature) {\n fakeFeature.properties.area = area;\n results.push(fakeFeature);\n }\n }\n return results;\n};\n\n// *** END HACK ***\n\n\n Object.keys(_nsi.data).forEach(tkv => {\n const category = _nsi.data[tkv];\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const t = parts[0];\n const k = parts[1];\n const v = parts[2];\n\n // Build a reverse index of keys -> values -> trees present in the name-suggestion-index\n // Collect primary keys (e.g. \"amenity\", \"craft\", \"shop\", \"man_made\", \"route\", etc)\n // \"amenity\": {\n // \"restaurant\": \"brands\"\n // }\n let vmap = _nsi.kvt.get(k);\n if (!vmap) {\n vmap = new Map();\n _nsi.kvt.set(k, vmap);\n }\n vmap.set(v, t);\n\n const tree = _nsi.trees[t]; // e.g. \"brands\", \"operators\"\n const mainTag = tree.mainTag; // e.g. \"brand:wikidata\", \"operator:wikidata\", etc\n\n const items = category.items || [];\n items.forEach(item => {\n // Remember some useful things for later, cache NSI id -> item\n item.tkv = tkv;\n item.mainTag = mainTag;\n _nsi.ids.set(item.id, item);\n\n // Cache Wikidata/Wikipedia values -> qid, for #6416\n const wd = item.tags[mainTag];\n const wp = item.tags[mainTag.replace('wikidata', 'wikipedia')];\n if (wd) _nsi.qids.set(wd, wd);\n if (wp && wd) _nsi.qids.set(wp, wd);\n });\n });\n })\n );\n}\n\n\n// `gatherKVs()`\n// Gather all the k/v pairs that we will run through the NSI matcher.\n// An OSM tags object can contain anything, but only a few tags will be interesting to NSI.\n//\n// This function will return the interesting tag pairs like:\n// \"amenity/restaurant\", \"man_made/flagpole\"\n// and fallbacks like\n// \"amenity/yes\"\n// excluding things like\n// \"tiger:reviewed\", \"surface\", \"ref\", etc.\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `Object` containing kv pairs to test:\n// {\n// 'primary': Set(),\n// 'alternate': Set()\n// }\n//\nfunction gatherKVs(tags) {\n let primary = new Set();\n let alternate = new Set();\n\n Object.keys(tags).forEach(osmkey => {\n const osmvalue = tags[osmkey];\n if (!osmvalue) return;\n\n // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184\n if (osmkey === 'route_master') osmkey = 'route';\n\n const vmap = _nsi.kvt.get(osmkey);\n if (!vmap) return; // not an interesting key\n\n if (vmap.get(osmvalue)) { // Matched a category in NSI\n primary.add(`${osmkey}/${osmvalue}`); // interesting key/value\n } else if (osmvalue === 'yes') {\n alternate.add(`${osmkey}/${osmvalue}`); // fallback key/yes\n }\n });\n\n // Can we try a generic building fallback match? - See #6122, #7197\n // Only try this if we do a preset match and find nothing else remarkable about that building.\n // For example, a way with `building=yes` + `name=Westfield` may be a Westfield department store.\n // But a way with `building=yes` + `name=Westfield` + `public_transport=station` is a train station for a town named \"Westfield\"\n const preset = presetManager.matchTags(tags, 'area');\n if (buildingPreset[preset.id]) {\n alternate.add('building/yes');\n }\n\n return { primary: primary, alternate: alternate };\n}\n\n\n// `identifyTree()`\n// NSI has a concept of trees: \"brands\", \"operators\", \"flags\", \"transit\".\n// The tree determines things like which tags are namelike, and which tags hold important wikidata.\n// This takes an Object of tags and tries to identify what tree to use.\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `string` the name of the tree if known\n// or 'unknown' if it could match several trees (e.g. amenity/yes)\n// or null if no match\n//\nfunction identifyTree(tags) {\n let unknown;\n let t;\n\n // Check all tags\n Object.keys(tags).forEach(osmkey => {\n if (t) return; // found already\n\n const osmvalue = tags[osmkey];\n if (!osmvalue) return;\n\n // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184\n if (osmkey === 'route_master') osmkey = 'route';\n\n const vmap = _nsi.kvt.get(osmkey);\n if (!vmap) return; // this key is not in nsi\n\n if (osmvalue === 'yes') {\n unknown = 'unknown';\n } else {\n t = vmap.get(osmvalue);\n }\n });\n\n return t || unknown || null;\n}\n\n\n// `gatherNames()`\n// Gather all the namelike values that we will run through the NSI matcher.\n// It will gather values primarily from tags `name`, `name:ru`, `flag:name`\n// and fallback to alternate tags like `brand`, `brand:ru`, `alt_name`\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `Object` containing namelike values to test:\n// {\n// 'primary': Set(),\n// 'fallbacks': Set()\n// }\n//\nfunction gatherNames(tags) {\n const empty = { primary: new Set(), alternate: new Set() };\n let primary = new Set();\n let alternate = new Set();\n let foundSemi = false;\n let testNameFragments = false;\n let patterns;\n\n // Patterns for matching OSM keys that might contain namelike values.\n // These roughly correspond to the \"trees\" concept in name-suggestion-index,\n let t = identifyTree(tags);\n if (!t) return empty;\n\n if (t === 'transit') {\n patterns = {\n primary: /^network$/i,\n alternate: /^(operator|operator:\\w+|network:\\w+|\\w+_name|\\w+_name:\\w+)$/i\n };\n } else if (t === 'flags') {\n patterns = {\n primary: /^(flag:name|flag:name:\\w+)$/i,\n alternate: /^(flag|flag:\\w+|subject|subject:\\w+)$/i // note: no `country`, we special-case it below\n };\n } else if (t === 'brands') {\n testNameFragments = true;\n patterns = {\n primary: /^(name|name:\\w+)$/i,\n alternate: /^(brand|brand:\\w+|operator|operator:\\w+|\\w+_name|\\w+_name:\\w+)/i,\n };\n } else if (t === 'operators') {\n testNameFragments = true;\n patterns = {\n primary: /^(name|name:\\w+|operator|operator:\\w+)$/i,\n alternate: /^(brand|brand:\\w+|\\w+_name|\\w+_name:\\w+)/i,\n };\n } else { // unknown/multiple\n testNameFragments = true;\n patterns = {\n primary: /^(name|name:\\w+)$/i,\n alternate: /^(brand|brand:\\w+|network|network:\\w+|operator|operator:\\w+|\\w+_name|\\w+_name:\\w+)/i,\n };\n }\n\n // Test `name` fragments, longest to shortest, to fit them into a \"Name Branch\" pattern.\n // e.g. \"TUI ReiseCenter - Neuss Innenstadt\" -> [\"TUI\", \"ReiseCenter\", \"Neuss\", \"Innenstadt\"]\n if (tags.name && testNameFragments) {\n const nameParts = tags.name.split(/[\\s\\-\\/,.]/);\n for (let split = nameParts.length; split > 0; split--) {\n const name = nameParts.slice(0, split).join(' '); // e.g. \"TUI ReiseCenter\"\n primary.add(name);\n }\n }\n\n // Check all tags\n Object.keys(tags).forEach(osmkey => {\n const osmvalue = tags[osmkey];\n if (!osmvalue) return;\n\n if (isNamelike(osmkey, 'primary')) {\n if (/;/.test(osmvalue)) {\n foundSemi = true;\n } else {\n primary.add(osmvalue);\n alternate.delete(osmvalue);\n }\n } else if (!primary.has(osmvalue) && isNamelike(osmkey, 'alternate')) {\n if (/;/.test(osmvalue)) {\n foundSemi = true;\n } else {\n alternate.add(osmvalue);\n }\n }\n });\n\n // For flags only, fallback to `country` tag only if no other namelike values were found.\n // See https://github.com/openstreetmap/iD/pull/8305#issuecomment-769174070\n if (tags.man_made === 'flagpole' && !primary.size && !alternate.size && !!tags.country) {\n const osmvalue = tags.country;\n if (/;/.test(osmvalue)) {\n foundSemi = true;\n } else {\n alternate.add(osmvalue);\n }\n }\n\n // If any namelike value contained a semicolon, return empty set and don't try matching anything.\n if (foundSemi) {\n return empty;\n } else {\n return { primary: primary, alternate: alternate };\n }\n\n function isNamelike(osmkey, which) {\n if (osmkey === 'old_name') return false;\n return patterns[which].test(osmkey) && !notNames.test(osmkey);\n }\n}\n\n\n// `gatherTuples()`\n// Generate all combinations of [key,value,name] that we want to test.\n// This prioritizes them so that the primary name and k/v pairs go first\n//\n// Arguments\n// `tryKVs`: `Object` containing primary and alternate k/v pairs to test\n// `tryNames`: `Object` containing primary and alternate names to test\n// Returns\n// `Array`: tuple objects ordered by priority\n//\nfunction gatherTuples(tryKVs, tryNames) {\n let tuples = [];\n ['primary', 'alternate'].forEach(whichName => {\n // test names longest to shortest\n const arr = Array.from(tryNames[whichName]).sort((a, b) => b.length - a.length);\n arr.forEach(n => {\n ['primary', 'alternate'].forEach(whichKV => {\n tryKVs[whichKV].forEach(kv => {\n const parts = kv.split('/', 2);\n const k = parts[0];\n const v = parts[1];\n tuples.push({ k: k, v: v, n: n });\n });\n });\n });\n });\n return tuples;\n}\n\n\n// `_upgradeTags()`\n// Try to match a feature to a canonical record in name-suggestion-index\n// and upgrade the tags to match.\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// `loc`: Location where this feature exists, as a [lon, lat]\n// Returns\n// `Object` containing the result, or `null` if no changes needed:\n// {\n// 'newTags': `Object` - The tags the the feature should have\n// 'matched': `Object` - The matched item\n// }\n//\nfunction _upgradeTags(tags, loc) {\n let newTags = Object.assign({}, tags); // shallow copy\n let changed = false;\n\n // Before anything, perform trivial Wikipedia/Wikidata replacements\n Object.keys(newTags).forEach(osmkey => {\n const matchTag = osmkey.match(/^(\\w+:)?wikidata$/);\n if (matchTag) { // Look at '*:wikidata' tags\n const prefix = (matchTag[1] || '');\n const wd = newTags[osmkey];\n const replace = _nsi.replacements[wd]; // If it matches a QID in the replacement list...\n\n if (replace && replace.wikidata !== undefined) { // replace or delete `*:wikidata` tag\n changed = true;\n if (replace.wikidata) {\n newTags[osmkey] = replace.wikidata;\n } else {\n delete newTags[osmkey];\n }\n }\n if (replace && replace.wikipedia !== undefined) { // replace or delete `*:wikipedia` tag\n changed = true;\n const wpkey = `${prefix}wikipedia`;\n if (replace.wikipedia) {\n newTags[wpkey] = replace.wikipedia;\n } else {\n delete newTags[wpkey];\n }\n }\n }\n });\n\n // Match a 'route_master' as if it were a 'route' - name-suggestion-index#5184\n const isRouteMaster = (tags.type === 'route_master');\n\n // Gather key/value tag pairs to try to match\n const tryKVs = gatherKVs(tags);\n if (!tryKVs.primary.size && !tryKVs.alternate.size) {\n return changed ? { newTags: newTags, matched: null } : null;\n }\n\n // Gather namelike tag values to try to match\n const tryNames = gatherNames(tags);\n\n // Do `wikidata=*` or `wikipedia=*` tags identify this entity as a chain? - See #6416\n // If so, these tags can be swapped to e.g. `brand:wikidata`/`brand:wikipedia`.\n const foundQID = _nsi.qids.get(tags.wikidata) || _nsi.qids.get(tags.wikipedia);\n if (foundQID) tryNames.primary.add(foundQID); // matcher will recognize the Wikidata QID as name too\n\n if (!tryNames.primary.size && !tryNames.alternate.size) {\n return changed ? { newTags: newTags, matched: null } : null;\n }\n\n // Order the [key,value,name] tuples - test primary before alternate\n const tuples = gatherTuples(tryKVs, tryNames);\n\n for (let i = 0; i < tuples.length; i++) {\n const tuple = tuples[i];\n const hits = _nsi.matcher.match(tuple.k, tuple.v, tuple.n, loc); // Attempt to match an item in NSI\n\n if (!hits || !hits.length) continue; // no match, try next tuple\n if (hits[0].match !== 'primary' && hits[0].match !== 'alternate') break; // a generic match, stop looking\n\n // A match may contain multiple results, the first one is likely the best one for this location\n // e.g. `['pfk-a54c14', 'kfc-1ff19c', 'kfc-658eea']`\n let itemID, item;\n for (let j = 0; j < hits.length; j++) {\n const hit = hits[j];\n itemID = hit.itemID;\n if (_nsi.dissolved[itemID]) continue; // Don't upgrade to a dissolved item\n\n item = _nsi.ids.get(itemID);\n if (!item) continue;\n const mainTag = item.mainTag; // e.g. `brand:wikidata`\n const itemQID = item.tags[mainTag]; // e.g. `brand:wikidata` qid\n const notQID = newTags[`not:${mainTag}`]; // e.g. `not:brand:wikidata` qid\n\n if ( // Exceptions, skip this hit\n (!itemQID || itemQID === notQID) || // No `*:wikidata` or matched a `not:*:wikidata`\n (newTags.office && !item.tags.office) // feature may be a corporate office for a brand? - #6416\n ) {\n item = null;\n continue; // continue looking\n } else {\n break; // use `item`\n }\n }\n\n // Can't use any of these hits, try next tuple..\n if (!item) continue;\n\n // At this point we have matched a canonical item and can suggest tag upgrades..\n item = JSON.parse(JSON.stringify(item)); // deep copy\n const tkv = item.tkv;\n const parts = tkv.split('/', 3); // tkv = \"tree/key/value\"\n const k = parts[1];\n const v = parts[2];\n const category = _nsi.data[tkv];\n const properties = category.properties || {};\n\n // Preserve some tags that we specifically don't want NSI to overwrite. ('^name', sometimes)\n let preserveTags = item.preserveTags || properties.preserveTags || [];\n\n // These tags can be toplevel tags -or- attributes - so we generally want to preserve existing values - #8615\n // We'll only _replace_ the tag value if this tag is the toplevel/defining tag for the matched item (`k`)\n ['building', 'emergency', 'internet_access', 'opening_hours', 'takeaway'].forEach(osmkey => {\n if (k !== osmkey) preserveTags.push(`^${osmkey}$`);\n });\n\n const regexes = preserveTags.map(s => new RegExp(s, 'i'));\n\n let keepTags = {};\n Object.keys(newTags).forEach(osmkey => {\n if (regexes.some(regex => regex.test(osmkey))) {\n keepTags[osmkey] = newTags[osmkey];\n }\n });\n\n // Remove any primary tags (\"amenity\", \"craft\", \"shop\", \"man_made\", \"route\", etc) that have a\n // value like `amenity=yes` or `shop=yes` (exceptions have already been added to `keepTags` above)\n _nsi.kvt.forEach((vmap, k) => {\n if (newTags[k] === 'yes') delete newTags[k];\n });\n\n // Replace mistagged `wikidata`/`wikipedia` with e.g. `brand:wikidata`/`brand:wikipedia`\n if (foundQID) {\n delete newTags.wikipedia;\n delete newTags.wikidata;\n }\n\n // Do the tag upgrade\n Object.assign(newTags, item.tags, keepTags);\n\n // Swap `route` back to `route_master` - name-suggestion-index#5184\n if (isRouteMaster) {\n newTags.route_master = newTags.route;\n delete newTags.route;\n }\n\n // Special `branch` splitting rules - IF..\n // - NSI is suggesting to replace `name`, AND\n // - `branch` doesn't already contain something, AND\n // - original name has not moved to an alternate name (e.g. \"Dunkin' Donuts\" -> \"Dunkin'\"), AND\n // - original name is \"some name\" + \"some stuff\", THEN\n // consider splitting `name` into `name`/`branch`..\n const origName = tags.name;\n const newName = newTags.name;\n if (newName && origName && newName !== origName && !newTags.branch) {\n const newNames = gatherNames(newTags);\n const newSet = new Set([...newNames.primary, ...newNames.alternate]);\n const isMoved = newSet.has(origName); // another tag holds the original name now\n\n if (!isMoved) {\n // Test name fragments, longest to shortest, to fit them into a \"Name Branch\" pattern.\n // e.g. \"TUI ReiseCenter - Neuss Innenstadt\" -> [\"TUI\", \"ReiseCenter\", \"Neuss\", \"Innenstadt\"]\n const nameParts = origName.split(/[\\s\\-\\/,.]/);\n for (let split = nameParts.length; split > 0; split--) {\n const name = nameParts.slice(0, split).join(' '); // e.g. \"TUI ReiseCenter\"\n const branch = nameParts.slice(split).join(' '); // e.g. \"Neuss Innenstadt\"\n const nameHits = _nsi.matcher.match(k, v, name, loc);\n if (!nameHits || !nameHits.length) continue; // no match, try next name fragment\n\n if (nameHits.some(hit => hit.itemID === itemID)) { // matched the name fragment to the same itemID above\n if (branch) {\n if (notBranches.test(branch)) { // \"branch\" was detected but is noise (\"factory outlet\", etc)\n newTags.name = origName; // Leave `name` alone, this part of the name may be significant..\n } else {\n const branchHits = _nsi.matcher.match(k, v, branch, loc);\n if (branchHits && branchHits.length) { // if \"branch\" matched something else in NSI..\n if (branchHits[0].match === 'primary' || branchHits[0].match === 'alternate') { // if another brand! (e.g. \"KFC - Taco Bell\"?)\n return null; // bail out - can't suggest tags in this case\n } // else a generic (e.g. \"gas\", \"cafe\") - ignore\n } else { // \"branch\" is not noise and not something in NSI\n newTags.branch = branch; // Stick it in the `branch` tag..\n }\n }\n }\n break;\n }\n }\n }\n }\n\n return { newTags: newTags, matched: item };\n }\n\n return changed ? { newTags: newTags, matched: null } : null;\n}\n\n\n// `_isGenericName()`\n// Is the `name` tag generic?\n//\n// Arguments\n// `tags`: `Object` containing the feature's OSM tags\n// Returns\n// `true` if it is generic, `false` if not\n//\nfunction _isGenericName(tags) {\n const n = tags.name;\n if (!n) return false;\n\n // tryNames just contains the `name` tag value and nothing else\n const tryNames = { primary: new Set([n]), alternate: new Set() };\n\n // Gather key/value tag pairs to try to match\n const tryKVs = gatherKVs(tags);\n if (!tryKVs.primary.size && !tryKVs.alternate.size) return false;\n\n // Order the [key,value,name] tuples - test primary before alternate\n const tuples = gatherTuples(tryKVs, tryNames);\n\n for (let i = 0; i < tuples.length; i++) {\n const tuple = tuples[i];\n const hits = _nsi.matcher.match(tuple.k, tuple.v, tuple.n); // Attempt to match an item in NSI\n\n // If we get a `excludeGeneric` hit, this is a generic name.\n if (hits && hits.length && hits[0].match === 'excludeGeneric') return true;\n }\n\n return false;\n}\n\n\n\n// PUBLIC INTERFACE\n\nexport default {\n\n // `init()`\n // On init, start preparing the name-suggestion-index\n //\n init: () => {\n // Note: service.init is called immediately after the presetManager has started loading its data.\n // We expect to chain onto an unfulfilled promise here.\n setNsiSources();\n presetManager.ensureLoaded()\n .then(() => loadNsiPresets())\n .then(() => loadNsiData())\n .then(() => _nsiStatus = 'ok')\n .catch(() => _nsiStatus = 'failed');\n },\n\n\n // `reset()`\n // Reset is called when user saves data to OSM (does nothing here)\n //\n reset: () => {},\n\n\n // `status()`\n // To let other code know how it's going...\n //\n // Returns\n // `String`: 'loading', 'ok', 'failed'\n //\n status: () => _nsiStatus,\n\n\n // `isGenericName()`\n // Is the `name` tag generic?\n //\n // Arguments\n // `tags`: `Object` containing the feature's OSM tags\n // Returns\n // `true` if it is generic, `false` if not\n //\n isGenericName: (tags) => _isGenericName(tags),\n\n\n // `upgradeTags()`\n // Suggest tag upgrades.\n // This function will not modify the input tags, it makes a copy.\n //\n // Arguments\n // `tags`: `Object` containing the feature's OSM tags\n // `loc`: Location where this feature exists, as a [lon, lat]\n // Returns\n // `Object` containing the result, or `null` if no changes needed:\n // {\n // 'newTags': `Object` - The tags the the feature should have\n // 'matched': `Object` - The matched item\n // }\n //\n upgradeTags: (tags, loc) => _upgradeTags(tags, loc),\n\n\n // `cache()`\n // Direct access to the NSI cache, useful for testing or breaking things\n //\n // Returns\n // `Object`: the internal NSI cache\n //\n cache: () => _nsi\n};\n", "import { localizer } from '../core/localizer';\n\nfunction timeSince(date: Date): [value: number, unit: Intl.RelativeTimeFormatUnit] {\n const seconds = Math.floor((+new Date() - +date) / 1000);\n const s = (n: number) => Math.floor(seconds / n);\n\n if (s(60 * 60 * 24 * 365) > 1) return [s(60 * 60 * 24 * 365), 'years'];\n if (s(60 * 60 * 24 * 30) > 1) return [s(60 * 60 * 24 * 30), 'months'];\n if (s(60 * 60 * 24) > 1) return [s(60 * 60 * 24), 'days'];\n if (s(60 * 60) > 1) return [s(60 * 60), 'hours'];\n if (s(60) > 1) return [s(60), 'minutes'];\n return [s(1), 'seconds'];\n}\n\n/**\n * Show the relative time if {@link Intl.RelativeTimeFormat} is supported\n * Otherwise fallback to the current date\n */\nexport function getRelativeDate(date: Date) {\n const preferredLanguage = localizer.localeCode();\n\n if (typeof Intl === 'undefined' || typeof Intl.RelativeTimeFormat === 'undefined') {\n return `on ${date.toLocaleDateString(preferredLanguage)}`;\n }\n\n const [number, units] = timeSince(date);\n if (!Number.isFinite(number)) return '-';\n\n return new Intl.RelativeTimeFormat(preferredLanguage).format(-number, units);\n}\n\nexport function localeDateString(date: Date | string) {\n if (!date) return null;\n const options: Intl.DateTimeFormatOptions = {\n day: 'numeric',\n month: 'short',\n year: 'numeric',\n };\n const d = new Date(date);\n if (Number.isNaN(d.getTime())) return null;\n return d.toLocaleDateString(localizer.localeCode(), options);\n}\n\nexport function localeTimestamp(date: Date) {\n const options: Intl.DateTimeFormatOptions = {\n day: '2-digit',\n month: '2-digit',\n year: 'numeric',\n hour: 'numeric',\n minute: 'numeric',\n second: 'numeric',\n };\n return date.toLocaleString(localizer.localeCode(), options);\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { json as d3_json } from 'd3-fetch';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport RBush from 'rbush';\n\nimport { geoExtent, geoScaleToZoom } from '../geo';\nimport { utilQsString, utilRebind, utilSetTransform, utilStringQs, utilTiler } from '../util';\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\nimport { localeDateString } from '../util/date';\n\n\nvar apibase = 'https://kartaview.org';\nvar maxResults = 1000;\nvar tileZoom = 14;\nvar tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);\nvar dispatch = d3_dispatch('loadedImages');\nvar imgZoom = d3_zoom()\n .extent([[0, 0], [320, 240]])\n .translateExtent([[0, 0], [320, 240]])\n .scaleExtent([1, 15]);\nvar _oscCache;\nvar _oscSelectedImage;\nvar _loadViewerPromise;\n\n\nfunction abortRequest(controller) {\n controller.abort();\n}\n\n\nfunction maxPageAtZoom(z) {\n if (z < 15) return 2;\n if (z === 15) return 5;\n if (z === 16) return 10;\n if (z === 17) return 20;\n if (z === 18) return 40;\n if (z > 18) return 80;\n}\n\n\nfunction loadTiles(which, url, projection) {\n var currZoom = Math.floor(geoScaleToZoom(projection.scale()));\n var tiles = tiler.getTiles(projection);\n\n // abort inflight requests that are no longer needed\n var cache = _oscCache[which];\n Object.keys(cache.inflight).forEach(function(k) {\n var wanted = tiles.find(function(tile) { return k.indexOf(tile.id + ',') === 0; });\n if (!wanted) {\n abortRequest(cache.inflight[k]);\n delete cache.inflight[k];\n }\n });\n\n tiles.forEach(function(tile) {\n loadNextTilePage(which, currZoom, url, tile);\n });\n}\n\n\nfunction loadNextTilePage(which, currZoom, url, tile) {\n var cache = _oscCache[which];\n var bbox = tile.extent.bbox();\n var maxPages = maxPageAtZoom(currZoom);\n var nextPage = cache.nextPage[tile.id] || 1;\n var params = utilQsString({\n ipp: maxResults,\n page: nextPage,\n // client_id: clientId,\n bbTopLeft: [bbox.maxY, bbox.minX].join(','),\n bbBottomRight: [bbox.minY, bbox.maxX].join(',')\n }, true);\n\n if (nextPage > maxPages) return;\n\n var id = tile.id + ',' + String(nextPage);\n if (cache.loaded[id] || cache.inflight[id]) return;\n\n var controller = new AbortController();\n cache.inflight[id] = controller;\n\n var options = {\n method: 'POST',\n signal: controller.signal,\n body: params,\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' }\n };\n\n d3_json(url, options)\n .then(function(data) {\n cache.loaded[id] = true;\n delete cache.inflight[id];\n if (!data || !data.currentPageItems || !data.currentPageItems.length) {\n throw new Error('No Data');\n }\n\n var features = data.currentPageItems.map(function(item) {\n var loc = [+item.lng, +item.lat];\n var d;\n\n if (which === 'images') {\n d = {\n service: 'photo',\n loc: loc,\n key: item.id,\n ca: +item.heading,\n captured_at: (item.shot_date || item.date_added),\n captured_by: item.username,\n imagePath: item.name,\n sequence_id: item.sequence_id,\n sequence_index: +item.sequence_index\n };\n\n // cache sequence info\n var seq = _oscCache.sequences[d.sequence_id];\n if (!seq) {\n seq = { rotation: 0, images: [] };\n _oscCache.sequences[d.sequence_id] = seq;\n }\n seq.images[d.sequence_index] = d;\n _oscCache.images.forImageKey[d.key] = d; // cache imageKey -> image\n }\n\n return {\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n };\n });\n\n cache.rtree.load(features);\n\n if (data.currentPageItems.length === maxResults) { // more pages to load\n cache.nextPage[tile.id] = nextPage + 1;\n loadNextTilePage(which, currZoom, url, tile);\n } else {\n cache.nextPage[tile.id] = Infinity; // no more pages to load\n }\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n }\n })\n .catch(function() {\n cache.loaded[id] = true;\n delete cache.inflight[id];\n });\n}\n\nexport default {\n\n init: function() {\n if (!_oscCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset: function() {\n if (_oscCache) {\n Object.values(_oscCache.images.inflight).forEach(abortRequest);\n }\n\n _oscCache = {\n images: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), forImageKey: {} },\n sequences: {}\n };\n },\n\n\n images: function(projection) {\n var limit = 5;\n return searchLimited(limit, projection, _oscCache.images.rtree);\n },\n\n\n sequences: function(projection) {\n var viewport = projection.clipExtent();\n var min = [viewport[0][0], viewport[1][1]];\n var max = [viewport[1][0], viewport[0][1]];\n var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n var sequenceKeys = {};\n\n // all sequences for images in viewport\n _oscCache.images.rtree.search(bbox)\n .forEach(function(d) { sequenceKeys[d.data.sequence_id] = true; });\n\n // make linestrings from those sequences\n var lineStrings = [];\n Object.keys(sequenceKeys)\n .forEach(function(sequenceKey) {\n var seq = _oscCache.sequences[sequenceKey];\n var images = seq && seq.images;\n\n if (images) {\n lineStrings.push({\n type: 'LineString',\n coordinates: images.map(function (d) { return d.loc; }).filter(Boolean),\n properties: {\n captured_at: images[0] ? images[0].captured_at: null,\n captured_by: images[0] ? images[0].captured_by: null,\n key: sequenceKey\n }\n });\n }\n });\n return lineStrings;\n },\n\n\n cachedImage: function(imageKey) {\n return _oscCache.images.forImageKey[imageKey];\n },\n\n\n loadImages: function(projection) {\n var url = apibase + '/1.0/list/nearby-photos/';\n loadTiles('images', url, projection);\n },\n\n\n ensureViewerLoaded: function(context) {\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n // add kartaview-wrapper\n var wrap = context.container().select('.photoviewer').selectAll('.kartaview-wrapper')\n .data([0]);\n\n var that = this;\n\n var wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper kartaview-wrapper')\n .classed('hide', true)\n .call(imgZoom.on('zoom', zoomPan))\n .on('dblclick.zoom', null);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n var controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls');\n\n controlsEnter\n .append('button')\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .on('click.rotate-ccw', rotate(-90))\n .text('\u293F');\n\n controlsEnter\n .append('button')\n .on('click.rotate-cw', rotate(90))\n .text('\u293E');\n\n controlsEnter\n .append('button')\n .on('click.forward', step(1))\n .text('\u25BA');\n\n wrapEnter\n .append('div')\n .attr('class', 'kartaview-image-wrap');\n\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.kartaview', function(dimensions) {\n imgZoom\n .extent([[0, 0], dimensions])\n .translateExtent([[0, 0], dimensions]);\n });\n\n\n function zoomPan(d3_event) {\n var t = d3_event.transform;\n context.container().select('.photoviewer .kartaview-image-wrap')\n .call(utilSetTransform, t.x, t.y, t.k);\n }\n\n\n function rotate(deg) {\n return function() {\n if (!_oscSelectedImage) return;\n var sequenceKey = _oscSelectedImage.sequence_id;\n var sequence = _oscCache.sequences[sequenceKey];\n if (!sequence) return;\n\n var r = sequence.rotation || 0;\n r += deg;\n\n if (r > 180) r -= 360;\n if (r < -180) r += 360;\n sequence.rotation = r;\n\n var wrap = context.container().select('.photoviewer .kartaview-wrapper');\n\n wrap\n .transition()\n .duration(100)\n .call(imgZoom.transform, d3_zoomIdentity);\n\n wrap.selectAll('.kartaview-image')\n .transition()\n .duration(100)\n .style('transform', 'rotate(' + r + 'deg)');\n };\n }\n\n function step(stepBy) {\n return function() {\n if (!_oscSelectedImage) return;\n var sequenceKey = _oscSelectedImage.sequence_id;\n var sequence = _oscCache.sequences[sequenceKey];\n if (!sequence) return;\n\n var nextIndex = _oscSelectedImage.sequence_index + stepBy;\n var nextImage = sequence.images[nextIndex];\n if (!nextImage) return;\n\n context.map().centerEase(nextImage.loc);\n\n that\n .selectImage(context, nextImage.key);\n };\n }\n\n // don't need any async loading so resolve immediately\n _loadViewerPromise = Promise.resolve();\n\n return _loadViewerPromise;\n },\n\n\n showViewer: function(context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.kartaview-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.kartaview-wrapper')\n .classed('hide', false);\n }\n\n return this;\n },\n\n\n hideViewer: function(context) {\n _oscSelectedImage = null;\n\n this.updateUrlImage(null);\n\n var viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n return this.setStyles(context, null, true);\n },\n\n\n selectImage: function(context, imageKey) {\n\n var d = this.cachedImage(imageKey);\n\n _oscSelectedImage = d;\n\n this.updateUrlImage(imageKey);\n\n var viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null, true);\n\n context.container().selectAll('.icon-sign')\n .classed('currentView', false);\n\n if (!d) return this;\n\n var wrap = context.container().select('.photoviewer .kartaview-wrapper');\n var imageWrap = wrap.selectAll('.kartaview-image-wrap');\n var attribution = wrap.selectAll('.photo-attribution').text('');\n\n wrap\n .transition()\n .duration(100)\n .call(imgZoom.transform, d3_zoomIdentity);\n\n imageWrap\n .selectAll('.kartaview-image')\n .remove();\n\n if (d) {\n var sequence = _oscCache.sequences[d.sequence_id];\n var r = (sequence && sequence.rotation) || 0;\n\n imageWrap\n .append('img')\n .attr('class', 'kartaview-image')\n .attr('src', (apibase + '/' + d.imagePath).replace(/^https:\\/\\/kartaview\\.org\\/storage(\\d+)\\//, 'https://storage$1.openstreetcam.org/'))\n .style('transform', 'rotate(' + r + 'deg)');\n\n if (d.captured_by) {\n attribution\n .append('a')\n .attr('class', 'captured_by')\n .attr('target', '_blank')\n .attr('href', 'https://kartaview.org/user/' + encodeURIComponent(d.captured_by))\n .text('@' + d.captured_by);\n\n attribution\n .append('span')\n .text('|');\n }\n\n if (d.captured_at) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeDateString(d.captured_at));\n\n attribution\n .append('span')\n .text('|');\n }\n\n attribution\n .append('a')\n .attr('class', 'image-link')\n .attr('target', '_blank')\n .attr('href', 'https://kartaview.org/details/' + d.sequence_id + '/' + d.sequence_index)\n .text('kartaview.org');\n }\n\n return this;\n },\n\n\n getSelectedImage: function() {\n return _oscSelectedImage;\n },\n\n\n getSequenceKeyForImage: function(d) {\n return d && d.sequence_id;\n },\n\n\n // Updates the currently highlighted sequence and selected bubble.\n // Reset is only necessary when interacting with the viewport because\n // this implicitly changes the currently selected bubble/sequence\n setStyles: function(context, hovered, reset) {\n if (reset) { // reset all layers\n context.container().selectAll('.viewfield-group')\n .classed('highlighted', false)\n .classed('hovered', false)\n .classed('currentView', false);\n\n context.container().selectAll('.sequence')\n .classed('highlighted', false)\n .classed('currentView', false);\n }\n\n var hoveredImageId = hovered && hovered.key;\n var hoveredSequenceId = this.getSequenceKeyForImage(hovered);\n\n var viewer = context.container().select('.photoviewer');\n var selected = viewer.empty() ? undefined : viewer.datum();\n var selectedImageId = selected && selected.key;\n var selectedSequenceId = this.getSequenceKeyForImage(selected);\n\n // highlight sibling viewfields on either the selected or the hovered sequences\n context.container().selectAll('.layer-kartaview .viewfield-group')\n .classed('highlighted', function(d) { return d.sequence_id === selectedSequenceId || d.id === hoveredImageId; })\n .classed('hovered', function(d) { return d.key === hoveredImageId; })\n .classed('currentView', function(d) { return d.key === selectedImageId; });\n\n context.container().selectAll('.layer-kartaview .sequence')\n .classed('highlighted', function(d) { return d.properties.key === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.key === selectedSequenceId; });\n\n // update viewfields if needed\n context.container().selectAll('.layer-kartaview .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n var d = this.parentNode.__data__;\n if (d.pano && d.key !== selectedImageId) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n return this;\n },\n\n\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'kartaview/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n\n cache: function() {\n return _oscCache;\n }\n\n};\n", "import type { Feature, FeatureCollection, Geometry, Position } from 'geojson';\nimport whichPolygon from 'which-polygon';\nimport rawBorders from './data/borders.json';\n\ninterface RegionFeatureProperties {\n // Unique identifier specific to country-coder\n id: string;\n\n // ISO 3166-1 alpha-2 code\n iso1A2: string | undefined;\n\n // ISO 3166-1 alpha-3 code\n iso1A3: string | undefined;\n\n // ISO 3166-1 numeric-3 code\n iso1N3: string | undefined;\n\n // UN M49 code\n m49: string | undefined;\n\n // Wikidata QID\n wikidata: string;\n\n // The emoji flag sequence derived from this feature's ISO 3166-1 alpha-2 code\n emojiFlag: string | undefined;\n\n // The ccTLD (country code top-level domain)\n ccTLD: string | undefined;\n\n // The common English name\n nameEn: string;\n\n // Additional identifiers which can be used to look up this feature;\n // these cannot collide with the identifiers for any other feature\n aliases: Array | undefined;\n\n // For features entirely within a country, the ISO 3166-1 alpha-2 code for that country\n country: string | undefined;\n\n // The ISO 3166-1 alpha-2, M49, or QIDs of other features this feature is entirely within, including its country\n groups: Array;\n\n // The ISO 3166-1 alpha-2, M49, or QIDs of other features this feature contains;\n // the inverse of `groups`\n members: Array | undefined;\n\n // The rough geographic type of this feature.\n // Levels do not necessarily nest cleanly within each other.\n // - `world`: all features\n\n // - `unitedNations`: United Nations\n // - `union`: European Union\n // - `subunion`: Outermost Regions of the EU, Overseas Countries and Territories of the EU\n\n // Defined by the UN\n // - `region`: Africa, Americas, Antarctica, Asia, Europe, Oceania\n // - `subregion`: Sub-Saharan Africa, North America, Micronesia, etc.\n // - `intermediateRegion`: Eastern Africa, South America, Channel Islands, etc.\n\n // - `sharedLandform`: Great Britain, Macaronesia, Mariana Islands, etc.\n // - `country`: Ethiopia, Brazil, United States, etc.\n // - `subcountryGroup`\n // - `territory`: Puerto Rico, Gurnsey, Hong Kong, etc.\n // - `subterritory`: Sark, Ascension Island, Diego Garcia, etc.\n level: string;\n\n // The status of this feature's ISO 3166-1 code(s), if any\n // - `official`: officially-assigned\n // - `excRes`: exceptionally-reserved\n // - `usrAssn`: user-assigned\n isoStatus: string | undefined;\n\n // The side of the road that traffic drives on within this feature\n // - `right`\n // - `left`\n driveSide: 'left' | 'right' | undefined;\n\n // The unit used for road traffic speeds within this feature\n // - `mph`: miles per hour\n // - `km/h`: kilometers per hour\n roadSpeedUnit: 'mph' | 'km/h' | undefined;\n\n // The unit used for road vehicle height restrictions within this feature\n // - `ft`: feet and inches\n // - `m`: meters\n roadHeightUnit: 'ft' | 'm' | undefined;\n\n // The international calling codes for this feature, sometimes including area codes\n // e.g. `1`, `1 340`\n callingCodes: Array | undefined;\n};\n\ntype RegionFeature = Feature;\ntype RegionFeatureCollection = FeatureCollection;\ntype Vec2 = [number, number]; // [lon, lat]\ntype Bbox = [number, number, number, number]; // [minLon, minLat, maxLon, maxLat]\n\ninterface PointGeometry {\n type: string;\n coordinates: Vec2\n};\n\ninterface PointFeature {\n type: string;\n geometry: PointGeometry;\n properties: any\n};\ntype Location = Vec2 | PointGeometry | PointFeature;\n\ninterface CodingOptions {\n // For overlapping features, the division level of the one to get. If no feature\n // exists at the given level, the feature at the next higher level is returned.\n // See the `level` property of `RegionFeatureProperties` for possible values.\n level?: string | undefined;\n // Only a feature at the specified level or lower will be returned.\n maxLevel?: string | undefined;\n // Only a feature with the specified property will be returned.\n withProp?: string | undefined;\n};\n\n// The base GeoJSON feature collection\nexport const borders: RegionFeatureCollection = rawBorders as RegionFeatureCollection;\n\n// The whichPolygon interface for looking up a feature by point\nlet _whichPolygon: any = {};\n// The cache for looking up a feature by identifier\nconst _featuresByCode: any = {};\n\n// discard special characters and instances of and/the/of that aren't the only characters\nconst idFilterRegex =\n /(?=(?!^(and|the|of|el|la|de)$))(\\b(and|the|of|el|la|de)\\b)|[-_ .,'()&[\\]/]/gi;\n\nfunction canonicalID(id: string | null): string {\n const s = id || '';\n if (s.charAt(0) === '.') {\n // skip replace if it leads with a '.' (e.g. a ccTLD like '.de', '.la')\n return s.toUpperCase();\n } else {\n return s.replace(idFilterRegex, '').toUpperCase();\n }\n}\n\n// Geographic levels, roughly from most to least granular\nconst levels = [\n 'subterritory',\n 'territory',\n 'subcountryGroup',\n 'country',\n 'sharedLandform',\n 'intermediateRegion',\n 'subregion',\n 'region',\n 'subunion',\n 'union',\n 'unitedNations',\n 'world'\n];\n\nloadDerivedDataAndCaches(borders);\n\n// Loads implicit feature data and the getter index caches\nfunction loadDerivedDataAndCaches(borders) {\n const identifierProps = ['iso1A2', 'iso1A3', 'm49', 'wikidata', 'emojiFlag', 'ccTLD', 'nameEn'];\n const geometryFeatures: Array = [];\n\n for (const feature of borders.features) {\n // generate a unique ID for each feature\n const props = feature.properties;\n props.id = props.iso1A2 || props.m49 || props.wikidata;\n\n loadM49(feature);\n loadTLD(feature);\n loadIsoStatus(feature);\n loadLevel(feature);\n loadGroups(feature);\n loadFlag(feature);\n // cache only after loading derived IDs\n cacheFeatureByIDs(feature);\n\n if (feature.geometry) {\n geometryFeatures.push(feature);\n }\n }\n\n // must load `members` only after fully loading `featuresByID`\n for (const feature of borders.features) {\n // ensure all groups are listed by their ID\n feature.properties.groups = feature.properties.groups.map(groupID => {\n return _featuresByCode[groupID].properties.id;\n });\n loadMembersForGroupsOf(feature);\n }\n\n // must load attributes only after loading geometry features into `members`\n for (const feature of borders.features) {\n loadRoadSpeedUnit(feature);\n loadRoadHeightUnit(feature);\n loadDriveSide(feature);\n loadCallingCodes(feature);\n loadGroupGroups(feature);\n }\n\n for (const feature of borders.features) {\n // order groups by their `level`\n feature.properties.groups.sort((groupID1, groupID2) => {\n return (\n levels.indexOf(_featuresByCode[groupID1].properties.level) -\n levels.indexOf(_featuresByCode[groupID2].properties.level)\n );\n });\n // order members by their `level` and then by order in borders\n if (feature.properties.members) {\n feature.properties.members.sort((id1, id2) => {\n const diff =\n levels.indexOf(_featuresByCode[id1].properties.level) -\n levels.indexOf(_featuresByCode[id2].properties.level);\n if (diff === 0) {\n return (\n borders.features.indexOf(_featuresByCode[id1]) -\n borders.features.indexOf(_featuresByCode[id2])\n );\n }\n return diff;\n });\n }\n }\n\n // whichPolygon doesn't support null geometry even though GeoJSON does\n const geometryOnlyCollection: RegionFeatureCollection = {\n type: 'FeatureCollection',\n features: geometryFeatures\n };\n _whichPolygon = whichPolygon(geometryOnlyCollection);\n\n function loadGroups(feature: RegionFeature) {\n const props = feature.properties;\n if (!props.groups) {\n props.groups = [];\n }\n if (feature.geometry && props.country) {\n // Add `country` to `groups`\n props.groups.push(props.country);\n }\n if (props.m49 !== '001') { // all features are in the world feature except the world itself\n props.groups.push('001');\n }\n }\n\n function loadM49(feature: RegionFeature) {\n const props = feature.properties;\n if (!props.m49 && props.iso1N3) { // M49 is a superset of ISO numerics so we only need to store one\n props.m49 = props.iso1N3;\n }\n }\n\n function loadTLD(feature: RegionFeature) {\n const props = feature.properties;\n if (props.level === 'unitedNations') return; // `.un` is not a ccTLD\n if (props.ccTLD === null) return; // e.g. Sark is an ISO code but not a ccTLD\n if (!props.ccTLD && props.iso1A2) { // ccTLD is nearly the same as iso1A2, so we only need to explicitly code any exceptions\n props.ccTLD = '.' + props.iso1A2.toLowerCase();\n }\n }\n\n function loadIsoStatus(feature: RegionFeature) {\n const props = feature.properties;\n if (!props.isoStatus && props.iso1A2) { // Features with an ISO code but no explicit status are officially-assigned\n props.isoStatus = 'official';\n }\n }\n\n function loadLevel(feature: RegionFeature) {\n const props = feature.properties;\n if (props.level) return;\n if (!props.country) { // a feature without an explicit `level` or `country` is itself a country\n props.level = 'country';\n } else if (!props.iso1A2 || props.isoStatus === 'official') {\n props.level = 'territory';\n } else {\n props.level = 'subterritory';\n }\n }\n\n function loadGroupGroups(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry || !props.members) return;\n\n const featureLevelIndex = levels.indexOf(props.level);\n let sharedGroups: Array = [];\n\n props.members.forEach((memberID, index) => {\n const member = _featuresByCode[memberID];\n const memberGroups = member.properties.groups.filter((groupID) => {\n return (\n groupID !== feature.properties.id &&\n featureLevelIndex < levels.indexOf(_featuresByCode[groupID].properties.level)\n );\n });\n if (index === 0) {\n sharedGroups = memberGroups;\n } else {\n sharedGroups = sharedGroups.filter((groupID) => memberGroups.indexOf(groupID) !== -1);\n }\n });\n\n props.groups = props.groups.concat(\n sharedGroups.filter((groupID) => props.groups.indexOf(groupID) === -1)\n );\n\n for (const groupID of sharedGroups) {\n const groupFeature = _featuresByCode[groupID];\n if (groupFeature.properties.members.indexOf(props.id) === -1) {\n groupFeature.properties.members.push(props.id);\n }\n }\n }\n\n function loadRoadSpeedUnit(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry) {\n // only `mph` regions are listed explicitly, else assume `km/h`\n if (!props.roadSpeedUnit) props.roadSpeedUnit = 'km/h';\n } else if (props.members) {\n const vals = Array.from(\n new Set(\n props.members\n .map(id => {\n const member = _featuresByCode[id];\n if (member.geometry) {\n return member.properties.roadSpeedUnit || 'km/h';\n } else {\n return null;\n }\n })\n .filter(Boolean)\n )\n );\n // if all members have the same value then that's also the value for this feature\n if (vals.length === 1) props.roadSpeedUnit = vals[0];\n }\n }\n\n function loadRoadHeightUnit(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry) {\n // only `ft` regions are listed explicitly, else assume `m`\n if (!props.roadHeightUnit) props.roadHeightUnit = 'm';\n } else if (props.members) {\n const vals = Array.from(\n new Set(\n props.members\n .map(id => {\n const member = _featuresByCode[id];\n if (member.geometry) {\n return member.properties.roadHeightUnit || 'm';\n } else {\n return null;\n }\n })\n .filter(Boolean)\n )\n );\n // if all members have the same value then that's also the value for this feature\n if (vals.length === 1) props.roadHeightUnit = vals[0];\n }\n }\n\n function loadDriveSide(feature: RegionFeature) {\n const props = feature.properties;\n if (feature.geometry) {\n // only `left` regions are listed explicitly, else assume `right`\n if (!props.driveSide) props.driveSide = 'right';\n } else if (props.members) {\n const vals = Array.from(\n new Set(\n props.members\n .map(id => {\n const member = _featuresByCode[id];\n if (member.geometry) {\n return member.properties.driveSide || 'right';\n } else {\n return null;\n }\n })\n .filter(Boolean)\n )\n );\n // if all members have the same value then that's also the value for this feature\n if (vals.length === 1) props.driveSide = vals[0];\n }\n }\n\n function loadCallingCodes(feature: RegionFeature) {\n const props = feature.properties;\n if (!feature.geometry && props.members) {\n props.callingCodes = Array.from(\n new Set(\n props.members.reduce((array, id) => {\n const member = _featuresByCode[id];\n if (member.geometry && member.properties.callingCodes) {\n return array.concat(member.properties.callingCodes);\n }\n return array;\n }, [])\n )\n );\n }\n }\n\n // Calculates the emoji flag (if any) and caches it\n function loadFlag(feature: RegionFeature) {\n let flag = '';\n\n // Most emoji flags can be generated from their 2 letter code.\n // Skip 'FX' (Metropolitan France), allow it to roll up to 'FR' - #25\n const country = feature.properties.iso1A2;\n if (country && country !== 'FX') {\n flag = _toEmojiCountryFlag(country);\n }\n\n // Support a few regional flags - #157\n const regionStrings = {\n Q21: 'gbeng', // GB-ENG (England)\n Q22: 'gbsct', // GB-SCT (Scotland)\n Q25: 'gbwls' // GB-WLS (Wales)\n };\n const region = regionStrings[feature.properties.wikidata];\n if (region) {\n flag = _toEmojiRegionFlag(region);\n }\n\n if (flag) {\n feature.properties.emojiFlag = flag;\n }\n\n // Normally, take isoA2 chars and jump up into \"Enclosed Alphanumeric Supplement\" block\n // see https://en.wikipedia.org/wiki/Regional_indicator_symbol\n // see https://en.wikipedia.org/wiki/Enclosed_Alphanumeric_Supplement\n function _toEmojiCountryFlag(s: string): string {\n return s.replace(/./g, c => String.fromCodePoint(c.charCodeAt(0) + 0x1F1A5));\n }\n\n // Regional flags are encoded as U+1F3F4 (black flag) + the region string (jump up to \"Tags\" block) + U+E007F (end)\n // see https://en.wikipedia.org/wiki/Tags_(Unicode_block)\n function _toEmojiRegionFlag(s: string) {\n const codepoints = [0x1F3F4];\n for (const c of [...s]) {\n codepoints.push(c.codePointAt(0) as number + 0xE0000);\n }\n codepoints.push(0xE007F);\n return String.fromCodePoint.apply(null, codepoints);\n }\n }\n\n // Populate `members` as the inverse relationship of `groups`\n function loadMembersForGroupsOf(feature: RegionFeature) {\n for (const groupID of feature.properties.groups) {\n const groupFeature = _featuresByCode[groupID];\n\n if (!groupFeature.properties.members) {\n groupFeature.properties.members = [];\n }\n groupFeature.properties.members.push(feature.properties.id);\n }\n }\n\n // Caches features by their identifying strings for rapid lookup\n function cacheFeatureByIDs(feature: RegionFeature) {\n const ids: Array = [];\n\n for (const prop of identifierProps) {\n const id = feature.properties[prop];\n if (id) {\n ids.push(id);\n }\n }\n\n for (const alias of (feature.properties.aliases || [])) {\n ids.push(alias);\n }\n\n for (const id of ids) {\n const cid = canonicalID(id);\n _featuresByCode[cid] = feature;\n }\n }\n}\n\n// Returns the [longitude, latitude] for the location argument\nfunction locArray(loc: Location): Vec2 {\n if (Array.isArray(loc)) {\n return loc as Vec2;\n } else if ((loc as PointGeometry).coordinates) {\n return (loc as PointGeometry).coordinates;\n } else {\n return (loc as PointFeature).geometry.coordinates;\n }\n}\n\n// Returns the smallest feature of any kind containing `loc`, if any\nfunction smallestFeature(loc: Location): RegionFeature | null {\n const query = locArray(loc);\n const featureProperties: RegionFeatureProperties = _whichPolygon(query);\n if (!featureProperties) return null;\n return _featuresByCode[featureProperties.id];\n}\n\n// Returns the country feature containing `loc`, if any\nfunction countryFeature(loc: Location): RegionFeature | null {\n const feature = smallestFeature(loc);\n if (!feature) return null;\n\n // a feature without `country` but with geometry is itself a country\n const countryCode = feature.properties.country || feature.properties.iso1A2;\n return _featuresByCode[countryCode as string] || null;\n}\n\nconst defaultOpts = {\n level: undefined,\n maxLevel: undefined,\n withProp: undefined\n};\n\n// Returns the feature containing `loc` for the `opts`, if any\nfunction featureForLoc(loc: Location, opts: CodingOptions): RegionFeature | null {\n const targetLevel = opts.level || 'country';\n const maxLevel = opts.maxLevel || 'world';\n const withProp = opts.withProp;\n\n const targetLevelIndex = levels.indexOf(targetLevel);\n if (targetLevelIndex === -1) return null;\n\n const maxLevelIndex = levels.indexOf(maxLevel);\n if (maxLevelIndex === -1) return null;\n if (maxLevelIndex < targetLevelIndex) return null;\n\n if (targetLevel === 'country') {\n // attempt fast path for country-level coding\n const fastFeature = countryFeature(loc);\n if (fastFeature) {\n if (!withProp || fastFeature.properties[withProp]) {\n return fastFeature;\n }\n }\n }\n\n const features = featuresContaining(loc);\n\n const match = features.find((feature) => {\n const levelIndex = levels.indexOf(feature.properties.level);\n if (\n feature.properties.level === targetLevel ||\n // if no feature exists at the target level, return the first feature at the next level up\n (levelIndex > targetLevelIndex && levelIndex <= maxLevelIndex)\n ) {\n if (!withProp || feature.properties[withProp]) {\n return feature;\n }\n }\n return false;\n });\n\n return match || null;\n}\n\n// Returns the feature with an identifying property matching `id`, if any\nfunction featureForID(id: string | number): RegionFeature | null {\n let stringID: string;\n\n if (typeof id === 'number') {\n stringID = id.toString();\n if (stringID.length === 1) {\n stringID = '00' + stringID;\n } else if (stringID.length === 2) {\n stringID = '0' + stringID;\n }\n } else {\n stringID = canonicalID(id);\n }\n return _featuresByCode[stringID] || null;\n}\n\nfunction smallestFeaturesForBbox(bbox: Bbox): [RegionFeature] {\n return _whichPolygon.bbox(bbox).map(props => _featuresByCode[props.id]);\n}\n\nfunction smallestOrMatchingFeature(query: Location | string | number): RegionFeature | null {\n if (typeof query === 'object') {\n return smallestFeature(query as Location);\n }\n return featureForID(query);\n}\n\n// Returns the feature matching the given arguments, if any\nexport function feature(query: Location | string | number, opts: CodingOptions = defaultOpts): RegionFeature | null {\n if (typeof query === 'object') {\n return featureForLoc(query as Location, opts);\n }\n return featureForID(query);\n}\n\n// Returns the ISO 3166-1 alpha-2 code for the feature matching the arguments, if any\nexport function iso1A2Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'iso1A2';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.iso1A2 || null;\n}\n\n// Returns the ISO 3166-1 alpha-3 code for the feature matching the arguments, if any\nexport function iso1A3Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'iso1A3';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.iso1A3 || null;\n}\n\n// Returns the ISO 3166-1 numeric-3 code for the feature matching the arguments, if any\nexport function iso1N3Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'iso1N3';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.iso1N3 || null;\n}\n\n// Returns the UN M49 code for the feature matching the arguments, if any\nexport function m49Code(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'm49';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.m49 || null;\n}\n\n// Returns the Wikidata QID code for the feature matching the arguments, if any\nexport function wikidataQID(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'wikidata';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.wikidata;\n}\n\n// Returns the emoji emojiFlag sequence for the feature matching the arguments, if any\nexport function emojiFlag(query: Location | string | number, opts: CodingOptions = defaultOpts): string | null {\n opts.withProp = 'emojiFlag';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.emojiFlag || null;\n}\n\n// Returns the ccTLD (country code top-level domain) for the feature matching the arguments, if any\nexport function ccTLD(\n query: Location | string | number,\n opts: CodingOptions = defaultOpts\n): string | null {\n opts.withProp = 'ccTLD';\n const match = feature(query, opts);\n if (!match) return null;\n return match.properties.ccTLD || null;\n}\n\nfunction propertiesForQuery(query: Location | Bbox, property: string): Array {\n const features = featuresContaining(query, false);\n return features.map(feature => feature.properties[property]).filter(Boolean);\n}\n\n// Returns all the ISO 3166-1 alpha-2 codes of features at the location\nexport function iso1A2Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'iso1A2');\n}\n\n// Returns all the ISO 3166-1 alpha-3 codes of features at the location\nexport function iso1A3Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'iso1A3');\n}\n\n// Returns all the ISO 3166-1 numeric-3 codes of features at the location\nexport function iso1N3Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'iso1N3');\n}\n\n// Returns all the UN M49 codes of features at the location\nexport function m49Codes(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'm49');\n}\n\n// Returns all the Wikidata QIDs of features at the location\nexport function wikidataQIDs(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'wikidata');\n}\n\n// Returns all the emoji flag sequences of features at the location\nexport function emojiFlags(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'emojiFlag');\n}\n\n// Returns all the ccTLD (country code top-level domain) sequences of features at the location\nexport function ccTLDs(query: Location | Bbox): Array {\n return propertiesForQuery(query, 'ccTLD');\n}\n\n// Returns the feature matching `query` and all features containing it, if any.\n// If passing `true` for `strict`, an exact match will not be included\nexport function featuresContaining(query: Location | Bbox | string | number, strict?: boolean): Array {\n let matchingFeatures: Array;\n\n if (Array.isArray(query) && query.length === 4) { // check if bounding box\n matchingFeatures = smallestFeaturesForBbox(query as Bbox);\n } else {\n const smallestOrMatching = smallestOrMatchingFeature(query as Location | string | number);\n matchingFeatures = smallestOrMatching ? [smallestOrMatching] : [];\n }\n\n if (!matchingFeatures.length) return [];\n\n let returnFeatures: Array;\n if (!strict || typeof query === 'object') {\n returnFeatures = matchingFeatures.slice();\n } else {\n returnFeatures = [];\n }\n\n for (const feature of matchingFeatures) {\n const properties = feature.properties;\n for (const groupID of properties.groups) {\n const groupFeature = _featuresByCode[groupID];\n if (returnFeatures.indexOf(groupFeature) === -1) {\n returnFeatures.push(groupFeature);\n }\n }\n }\n\n return returnFeatures;\n}\n\n// Returns the feature matching `id` and all features it contains, if any.\n// If passing `true` for `strict`, an exact match will not be included\nexport function featuresIn(id: string | number, strict?: boolean): Array {\n const feature = featureForID(id);\n if (!feature) return [];\n\n const features: Array = [];\n if (!strict) {\n features.push(feature);\n }\n\n const properties = feature.properties;\n for (const memberID of (properties.members || [])) {\n features.push(_featuresByCode[memberID]);\n }\n\n return features;\n}\n\n// Returns a new feature with the properties of the feature matching `id`\n// and the combined geometry of all its component features\nexport function aggregateFeature(id: string | number): RegionFeature | null {\n const features = featuresIn(id, false);\n if (features.length === 0) return null;\n\n let aggregateCoordinates: Position[][][] = [];\n for (const feature of features) {\n if (feature.geometry?.type === 'MultiPolygon' && feature.geometry.coordinates) {\n aggregateCoordinates = aggregateCoordinates.concat(feature.geometry.coordinates);\n }\n }\n\n return {\n type: 'Feature',\n properties: features[0].properties,\n geometry: {\n type: 'MultiPolygon',\n coordinates: aggregateCoordinates\n }\n };\n}\n\n// Returns true if the feature matching `query` is, or is a part of, the feature matching `bounds`\nexport function isIn(query: Location | string | number, bounds: string | number): boolean | null {\n const queryFeature = smallestOrMatchingFeature(query);\n const boundsFeature = featureForID(bounds);\n\n if (!queryFeature || !boundsFeature) return null;\n\n if (queryFeature.properties.id === boundsFeature.properties.id) return true;\n return queryFeature.properties.groups.indexOf(boundsFeature.properties.id) !== -1;\n}\n\n// Returns true if the feature matching `query` is within EU jurisdiction\nexport function isInEuropeanUnion(query: Location | string | number): boolean | null {\n return isIn(query, 'EU');\n}\n\n// Returns true if the feature matching `query` is, or is within, a United Nations member state\nexport function isInUnitedNations(query: Location | string | number): boolean | null {\n return isIn(query, 'UN');\n}\n\n// Returns the side traffic drives on in the feature matching `query` as a string (`right` or `left`)\nexport function driveSide(query: Location | string | number): string | null {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.driveSide) || null;\n}\n\n// Returns the road speed unit for the feature matching `query` as a string (`mph` or `km/h`)\nexport function roadSpeedUnit(query: Location | string | number): string | null {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.roadSpeedUnit) || null;\n}\n\n// Returns the road vehicle height restriction unit for the feature matching `query` as a string (`ft` or `m`)\nexport function roadHeightUnit(query: Location | string | number): string | null {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.roadHeightUnit) || null;\n}\n\n// Returns the full international calling codes for phone numbers in the feature matching `query`, if any\nexport function callingCodes(query: Location | string | number): Array {\n const feature = smallestOrMatchingFeature(query);\n return (feature && feature.properties.callingCodes) || [];\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { utilRebind } from '../util';\n\n\nconst pannellumViewerCSS = 'pannellum/pannellum.css';\nconst pannellumViewerJS = 'pannellum/pannellum.js';\n\nexport async function pannellumPhotoFrame(context, selection) {\n const dispatch = d3_dispatch('viewerChanged');\n\n const module = {};\n module.event = utilRebind(module, dispatch, 'on');\n module.loadPannellum = function(context) {\n const head = d3_select('head');\n\n return Promise.all([\n new Promise((resolve, reject) => {\n // load pannellum viewer css\n head\n .selectAll('#ideditor-pannellum-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-pannellum-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(pannellumViewerCSS))\n .on('load.pannellum', resolve)\n .on('error.pannellum', reject);\n }),\n new Promise((resolve, reject) => {\n // load pannellum viewer js\n head\n .selectAll('#ideditor-pannellum-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-pannellum-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(pannellumViewerJS))\n .on('load.pannellum', resolve)\n .on('error.pannellum', reject);\n })\n ]);\n };\n\n let _currScenes = [];\n let _pannellumViewer;\n let _activeSceneKey;\n\n selection\n .append('div')\n .attr('class', 'photo-frame pannellum-frame')\n .attr('id', 'ideditor-pannellum-viewer')\n .classed('hide', true)\n .on('mousedown', function(e) { e.stopPropagation(); });\n\n if (!window.pannellum) {\n await module.loadPannellum(context);\n }\n\n const options = {\n 'default': { firstScene: '' },\n scenes: {},\n minHfov: 20,\n disableKeyboardCtrl: true,\n sceneFadeDuration: 0\n };\n\n _pannellumViewer = window.pannellum.viewer('ideditor-pannellum-viewer', options);\n\n _pannellumViewer\n .on('mousedown', () => d3_select(window)\n .on('pointermove.pannellum mousemove.pannellum', () => dispatch.call('viewerChanged')))\n .on('mouseup', () => d3_select(window)\n .on('pointermove.pannellum mousemove.pannellum', null))\n .on('animatefinished', () => dispatch.call('viewerChanged'));\n\n context.ui().photoviewer.on('resize.pannellum', () => {\n _pannellumViewer.resize();\n });\n\n /**\n * Shows the photo frame if hidden\n * @param {*} context the HTML wrap of the frame\n */\n module.showPhotoFrame = function(context) {\n const isHidden = context.selectAll('.photo-frame.pannellum-frame.hide').size();\n\n if (isHidden) {\n context\n .selectAll('.photo-frame:not(.pannellum-frame)')\n .classed('hide', true);\n\n context\n .selectAll('.photo-frame.pannellum-frame')\n .classed('hide', false);\n }\n\n return module;\n };\n\n /**\n * Hides the photo frame if shown\n * @param {*} context the HTML wrap of the frame\n */\n module.hidePhotoFrame = function(viewerContext) {\n viewerContext\n .select('photo-frame.pannellum-frame')\n .classed('hide', false);\n\n return module;\n };\n\n /**\n * Renders an image inside the frame\n * @param {*} data the image data, it should contain an image_path attribute, a link to the actual image.\n * @param {boolean} keepOrientation if true, HFOV, pitch and yaw will be kept between images\n */\n module.selectPhoto = function(data, keepOrientation) {\n const key = data.image_path;\n _activeSceneKey = key;\n if (!_currScenes.includes(key)) {\n let newSceneOptions = {\n showFullscreenCtrl: false,\n autoLoad: false,\n compass: false,\n yaw: 0,\n type: 'equirectangular',\n preview: data.preview_path,\n panorama: data.image_path,\n northOffset: data.ca\n };\n\n _currScenes.push(key);\n _pannellumViewer.addScene(key, newSceneOptions);\n }\n\n let yaw = 0;\n let pitch = 0;\n let hfov = 0;\n\n if (keepOrientation) {\n yaw = module.getYaw();\n pitch = module.getPitch();\n hfov = module.getHfov();\n }\n if (_pannellumViewer.isLoaded() !== false) {\n _pannellumViewer.loadScene(key, pitch, yaw, hfov);\n dispatch.call('viewerChanged');\n } else {\n // pannellum is currently loading another scene: wait for it to finish\n // loading the previous panorama first\n const retry = setInterval(() => {\n if (_pannellumViewer.isLoaded() === false) {\n // still not done: wait a bit longer\n return;\n }\n if (_activeSceneKey === key) {\n // only load scene if no other photo has been selected in the meantime\n _pannellumViewer.loadScene(key, pitch, yaw, hfov);\n dispatch.call('viewerChanged');\n }\n clearInterval(retry);\n }, 100);\n }\n\n if (_currScenes.length > 3) {\n const old_key = _currScenes.shift();\n _pannellumViewer.removeScene(old_key);\n }\n\n _pannellumViewer.resize();\n\n return module;\n };\n\n module.getYaw = function() {\n return _pannellumViewer.getYaw();\n };\n\n module.getPitch = function() {\n return _pannellumViewer.getPitch();\n };\n\n module.getHfov = function() {\n return _pannellumViewer.getHfov();\n };\n\n return module;\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\nimport { utilSetTransform, utilRebind } from '../util';\n\n\nexport async function planePhotoFrame(context, selection) {\n const dispatch = d3_dispatch('viewerChanged');\n\n const module = {};\n module.event = utilRebind(module, dispatch, 'on');\n\n let _photo;\n let _imageWrapper;\n let _planeWrapper;\n let _viewerDimensions = [];\n let _photoDimensions = [];\n const _imgZoom = d3_zoom()\n .on('zoom', zoomPan)\n .on('start', () => _imageWrapper.classed('grabbing', true))\n .on('end', () => _imageWrapper.classed('grabbing', false));\n\n function zoomPan(d3_event) {\n let t = d3_event.transform;\n _imageWrapper.call(utilSetTransform, t.x, t.y, t.k);\n }\n\n function loadImage(selection, path) {\n return new Promise((resolve) => {\n selection.attr('src', path);\n selection.on('load', () => {\n resolve(selection);\n });\n });\n }\n\n function updateTransform() {\n const xScale = _viewerDimensions[0] / _photoDimensions[0];\n const yScale = _viewerDimensions[1] / _photoDimensions[1];\n const fitScale = Math.max(xScale, yScale);\n const minScale = Math.min(xScale, yScale);\n _imgZoom\n .extent([[0, 0], _viewerDimensions])\n .translateExtent([[0, 0], _photoDimensions])\n .scaleExtent([minScale, 4]);\n const centerOffset = [0, 0];\n if (xScale < yScale) {\n centerOffset[0] = (_viewerDimensions[0] / fitScale - _photoDimensions[0]) / 2;\n } else {\n centerOffset[1] = (_viewerDimensions[1] / fitScale - _photoDimensions[1]) / 2;\n }\n const transform = d3_zoomIdentity.scale(fitScale).translate(centerOffset[0], centerOffset[1]);\n _planeWrapper.call(_imgZoom.transform, transform);\n }\n\n _planeWrapper = selection.append('div')\n .classed('plane-frame-wrapper', true);\n _planeWrapper.call(_imgZoom);\n\n _imageWrapper = _planeWrapper\n .append('div')\n .classed('photo-frame', true)\n .classed('plane-frame', true)\n .classed('hide', true);\n\n _photo = _imageWrapper\n .append('img')\n .attr('class', 'plane-photo');\n\n context.ui().photoviewer.on('resize.plane', function(dimensions) {\n _viewerDimensions = dimensions;\n updateTransform();\n });\n\n await Promise.resolve();\n\n /**\n * Shows the photo frame if hidden\n * @param {*} selection the HTML wrap of the frame\n */\n module.showPhotoFrame = function(selection) {\n const isHidden = selection.selectAll('.photo-frame.plane-frame.hide').size();\n\n if (isHidden) {\n selection\n .selectAll('.photo-frame:not(.plane-frame)')\n .classed('hide', true);\n\n selection\n .selectAll('.photo-frame.plane-frame')\n .classed('hide', false);\n }\n\n // set initial viewer size\n _viewerDimensions = context.ui().photoviewer.viewerSize();\n updateTransform();\n\n return module;\n };\n\n /**\n * Hides the photo frame if shown\n * @param {*} context the HTML wrap of the frame\n */\n module.hidePhotoFrame = function(context) {\n context\n .select('photo-frame.plane-frame')\n .classed('hide', false);\n\n return module;\n };\n\n /**\n * Renders an image inside the frame\n * @param {*} data the image data, it should contain an image_path attribute, a link to the actual image.\n */\n module.selectPhoto = function(data) {\n dispatch.call('viewerChanged');\n\n loadImage(_photo, '');\n _planeWrapper.classed('show-loader', true);\n loadImage(_photo, data.image_path)\n .then(selection => {\n _planeWrapper.classed('show-loader', false);\n const { naturalWidth, naturalHeight } = selection.node();\n _photoDimensions = [naturalWidth, naturalHeight];\n updateTransform();\n });\n return module;\n };\n\n module.getYaw = function() {\n return 0;\n };\n\n return module;\n};\n", "import { json as d3_json, xml as d3_xml} from 'd3-fetch';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { pairs as d3_pairs } from 'd3-array';\nimport RBush from 'rbush';\nimport { iso1A2Codes } from '@rapideditor/country-coder';\nimport { t } from '../core/localizer';\nimport { utilQsString, utilTiler, utilRebind, utilArrayUnion, utilStringQs } from '../util';\nimport { searchLimited } from '../util/partition';\nimport { localeTimestamp } from '../util/date';\nimport { geoExtent, geoVecAngle, geoVecEqual } from '../geo';\nimport { pannellumPhotoFrame } from './pannellum_photo';\nimport { planePhotoFrame } from './plane_photo';\nimport { services } from './';\n\n\nconst owsEndpoint = 'https://www.vegvesen.no/kart/ogc/vegbilder_1_0/ows?';\nconst tileZoom = 14;\nconst tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);\nconst dispatch = d3_dispatch('loadedImages', 'viewerChanged');\nconst directionEnum = Object.freeze({\n forward: Symbol(0),\n backward: Symbol(1)\n});\n\nlet _planeFrame;\nlet _pannellumFrame;\nlet _currentFrame;\nlet _loadViewerPromise;\nlet _vegbilderCache;\n\nasync function fetchAvailableLayers() {\n const params = {\n service: 'WFS',\n request: 'GetCapabilities',\n version: '2.0.0',\n };\n\n const urlForRequest = owsEndpoint + utilQsString(params);\n const response = await d3_xml(urlForRequest);\n const regexMatcher = /^vegbilder_1_0:Vegbilder(?_360)?_(?\\d{4})$/;\n const availableLayers = [];\n for (const node of response.querySelectorAll('FeatureType > Name')) {\n const match = node.textContent?.match(regexMatcher);\n if (match) {\n availableLayers.push({\n name: match[0],\n is_sphere: !!match.groups?.image_type,\n year: parseInt(match.groups?.year, 10)\n });\n }\n }\n return availableLayers;\n}\n\nfunction filterAvailableLayers(photoContex) {\n const fromDateString = photoContex.fromDate();\n const toDateString = photoContex.toDate();\n const fromYear = fromDateString ? new Date(fromDateString).getFullYear() : 2016;\n const toYear = toDateString ? new Date(toDateString).getFullYear() : null;\n const showsFlat = photoContex.showsFlat();\n const showsPano = photoContex.showsPanoramic();\n return Array.from(_vegbilderCache.wfslayers.values()).filter(({layerInfo}) => (\n (layerInfo.year >= fromYear) &&\n (!toYear || (layerInfo.year <= toYear)) &&\n ((!layerInfo.is_sphere && showsFlat) || (layerInfo.is_sphere && showsPano))\n ));\n}\n\nfunction loadWFSLayers(projection, margin, wfslayers) {\n const tiles = tiler.margin(margin).getTiles(projection);\n for (const cache of wfslayers) {\n loadWFSLayer(projection, cache, tiles);\n }\n}\n\nfunction loadWFSLayer(projection, cache, tiles) {\n // abort inflight requests that are no longer needed\n for (const [key, controller] of cache.inflight.entries()) {\n const wanted = tiles.some(tile => key === tile.id);\n if (!wanted) {\n controller.abort();\n cache.inflight.delete(key);\n }\n }\n\n Promise.all(tiles.map(\n tile => loadTile(cache, cache.layerInfo.name, tile)\n )).then(() => orderSequences(projection, cache));\n}\n\n/**\n* loadNextTilePage() load data for the next tile page in line.\n*/\nasync function loadTile(cache, typename, tile) {\n const bbox = tile.extent.bbox();\n const tileid = tile.id;\n if ((cache.loaded.get(tileid) === true) || cache.inflight.has(tileid)) return;\n\n const params = {\n service: 'WFS',\n request: 'GetFeature',\n version: '2.0.0',\n typenames: typename,\n bbox: [bbox.minY, bbox.minX, bbox.maxY, bbox.maxX].join(','),\n outputFormat: 'json'\n };\n\n const controller = new AbortController();\n cache.inflight.set(tileid, controller);\n\n const options = {\n method: 'GET',\n signal: controller.signal,\n };\n\n const urlForRequest = owsEndpoint + utilQsString(params);\n\n let featureCollection;\n try {\n featureCollection = await d3_json(urlForRequest, options);\n } catch {\n cache.loaded.set(tileid, false);\n return;\n } finally {\n cache.inflight.delete(tileid);\n }\n\n cache.loaded.set(tileid, true);\n\n if (featureCollection.features.length === 0) { return; }\n\n const features = featureCollection.features.map(feature => {\n const loc = feature.geometry.coordinates;\n const key = feature.id;\n const properties = feature.properties;\n const {\n RETNING: ca,\n TIDSPUNKT: captured_at,\n URL: image_path,\n URLPREVIEW : preview_path,\n BILDETYPE: image_type,\n METER: metering,\n FELTKODE: lane_code\n } = properties;\n const lane_number = parseInt((lane_code.match(/^[0-9]+/) || [])[0], 10);\n const direction = lane_number % 2 === 0 ? directionEnum.backward : directionEnum.forward;\n const data = {\n service: 'photo',\n loc,\n key,\n ca,\n image_path,\n preview_path,\n road_reference: roadReference(properties),\n metering,\n lane_code,\n direction,\n captured_at: new Date(captured_at),\n is_sphere: image_type === '360'\n };\n\n cache.points.set(key, data);\n\n return {\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data\n };\n });\n\n _vegbilderCache.rtree.load(features);\n dispatch.call('loadedImages');\n}\n\nfunction orderSequences(projection, cache) {\n const {points} = cache;\n\n const grouped = Array.from(points.values()).reduce((grouped, image) => {\n const key = image.road_reference;\n if (grouped.has(key)) {\n grouped.get(key).push(image);\n } else {\n grouped.set(key, [image]);\n }\n return grouped;\n }, new Map());\n\n const imageSequences = Array.from(grouped.values()).reduce((imageSequences, imageGroup) => {\n imageGroup.sort((a, b) => {\n if (a.captured_at.valueOf() > b.captured_at.valueOf()) {\n return 1;\n } else if (a.captured_at.valueOf() < b.captured_at.valueOf()) {\n return -1;\n } else {\n const {direction} = a;\n if (direction === directionEnum.forward) {\n return a.metering - b.metering;\n } else {\n return b.metering - a.metering;\n }\n }\n });\n let imageSequence = [imageGroup[0]];\n let angle = null;\n for (const [lastImage, image] of d3_pairs(imageGroup)) {\n if (lastImage.ca === null) {\n const b = projection(lastImage.loc);\n const a = projection(image.loc);\n if (!geoVecEqual(a, b)) {\n angle = geoVecAngle(a, b);\n angle *= (180 / Math.PI);\n angle -= 90;\n angle = angle >= 0 ? angle : angle + 360;\n }\n lastImage.ca = angle;\n }\n if (\n image.direction === lastImage.direction &&\n image.captured_at.valueOf() - lastImage.captured_at.valueOf() <= 20000\n ) {\n imageSequence.push(image);\n } else {\n imageSequences.push(imageSequence);\n imageSequence = [image];\n }\n }\n imageSequences.push(imageSequence);\n return imageSequences;\n }, []);\n\n cache.sequences = imageSequences.map(images => {\n const sequence = {\n images,\n key: images[0].key,\n geometry : {\n type : 'LineString',\n coordinates : images.map(image => image.loc)\n }\n };\n for (const image of images) {\n _vegbilderCache.image2sequence_map.set(image.key, sequence);\n }\n return sequence;\n });\n}\n\nfunction roadReference(properties) {\n const {\n FYLKENUMMER: county_number,\n VEGKATEGORI: road_class,\n VEGSTATUS: road_status,\n VEGNUMMER: road_number,\n STREKNING: section,\n DELSTREKNING: subsection,\n HP: parcel,\n KRYSSDEL: junction_part,\n SIDEANLEGGSDEL: services_part,\n ANKERPUNKT: anker_point,\n AAR: year,\n } = properties;\n\n let reference;\n\n if (year >= 2020) {\n reference = `${road_class}${road_status}${road_number} S${section}D${subsection}`;\n if (junction_part) {\n reference = `${reference} M${anker_point} KD${junction_part}`;\n } else if (services_part) {\n reference = `${reference} M${anker_point} SD${services_part}`;\n }\n } else {\n reference = `${county_number}${road_class}${road_status}${road_number} HP${parcel}`;\n }\n\n return reference;\n}\n\nexport default {\n\n init: function () {\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset: async function () {\n if (_vegbilderCache) {\n for (const layer of _vegbilderCache.wfslayers.values()) {\n for (const controller of layer.inflight.values()) {\n controller.abort();\n }\n }\n }\n\n _vegbilderCache = {\n wfslayers: new Map(),\n rtree: new RBush(),\n image2sequence_map: new Map()\n };\n\n const availableLayers = await fetchAvailableLayers();\n const {wfslayers} = _vegbilderCache;\n\n for (const layerInfo of availableLayers) {\n const cache = {\n layerInfo,\n loaded: new Map(),\n inflight: new Map(),\n points: new Map(),\n sequences: []\n };\n wfslayers.set(layerInfo.name, cache);\n }\n },\n\n images: function (projection) {\n const limit = 5;\n return searchLimited(limit, projection, _vegbilderCache.rtree);\n },\n\n\n sequences: function (projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const seen = new Set();\n const line_strings = [];\n\n for (const {data} of _vegbilderCache.rtree.search(bbox)) {\n const sequence = _vegbilderCache.image2sequence_map.get(data.key);\n if (!sequence) continue;\n const {key, geometry, images} = sequence;\n if (seen.has(key)) continue;\n seen.add(key);\n const line = {\n type: 'LineString',\n coordinates: geometry.coordinates,\n key,\n images\n };\n line_strings.push(line);\n }\n return line_strings;\n },\n\n cachedImage: function (key) {\n for (const {points} of _vegbilderCache.wfslayers.values()) {\n if (points.has(key)) return points.get(key);\n }\n },\n\n getSequenceForImage: function (image) {\n return _vegbilderCache?.image2sequence_map.get(image?.key);\n },\n\n loadImages: async function (context, margin) {\n if (!_vegbilderCache) {\n await this.reset();\n }\n margin ??= 1;\n const wfslayers = filterAvailableLayers(context.photos());\n loadWFSLayers(context.projection, margin, wfslayers);\n },\n\n photoFrame: function() {\n return _currentFrame;\n },\n\n ensureViewerLoaded: function(context) {\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n const step = (stepBy) => () => {\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n if (!selected) return;\n\n const sequence = this.getSequenceForImage(selected);\n const nextIndex = sequence.images.indexOf(selected) + stepBy;\n const nextImage = sequence.images[nextIndex];\n\n if (!nextImage) return;\n\n context.map().centerEase(nextImage.loc);\n this.selectImage(context, nextImage.key, true);\n };\n\n const wrap = context.container().select('.photoviewer')\n .selectAll('.vegbilder-wrapper')\n .data([0]);\n\n const wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper vegbilder-wrapper')\n .classed('hide', true);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n const controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls');\n\n controlsEnter\n .append('button')\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .on('click.forward', step(1))\n .text('\u25BA');\n\n _loadViewerPromise = Promise.all([\n pannellumPhotoFrame(context, wrapEnter),\n planePhotoFrame(context, wrapEnter)\n ]).then(([pannellumPhotoFrame, planePhotoFrame]) => {\n _pannellumFrame = pannellumPhotoFrame;\n _pannellumFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n _planeFrame = planePhotoFrame;\n _planeFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n });\n\n return _loadViewerPromise;\n },\n\n selectImage: function(context, key, keepOrientation) {\n const d = this.cachedImage(key);\n this.updateUrlImage(key);\n\n const viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) { viewer.datum(d); }\n\n this.setStyles(context, null, true);\n\n if (!d) return this;\n\n const wrap = context.container().select('.photoviewer .vegbilder-wrapper');\n const attribution = wrap.selectAll('.photo-attribution').text('');\n\n if (d.captured_at) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeTimestamp(d.captured_at));\n }\n\n attribution\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://vegvesen.no')\n .call(t.append('vegbilder.publisher'));\n\n attribution\n .append('a')\n .attr('target', '_blank')\n .attr('href', `https://vegbilder.atlas.vegvesen.no/?year=${d.captured_at.getFullYear()}&lat=${d.loc[1]}&lng=${d.loc[0]}&view=image&imageId=${d.key}`)\n .call(t.append('vegbilder.view_on'));\n\n _currentFrame = d.is_sphere? _pannellumFrame : _planeFrame;\n\n _currentFrame\n .showPhotoFrame(wrap)\n .selectPhoto(d, keepOrientation);\n\n return this;\n },\n\n showViewer: function (context) {\n const viewer = context.container().select('.photoviewer');\n const isHidden = viewer.selectAll('.photo-wrapper.vegbilder-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n viewer\n .classed('hide', false)\n .selectAll('.photo-wrapper.vegbilder-wrapper')\n .classed('hide', false);\n }\n return this;\n },\n\n hideViewer: function(context) {\n this.updateUrlImage(null);\n\n const viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence')\n .classed('currentView', false);\n\n return this.setStyles(context, null, true);\n },\n\n\n // Updates the currently highlighted sequence and selected bubble.\n // Reset is only necessary when interacting with the viewport because\n // this implicitly changes the currently selected bubble/sequence\n setStyles: function (context, hovered, reset) {\n if (reset) { // reset all layers\n context.container().selectAll('.viewfield-group')\n .classed('highlighted', false)\n .classed('hovered', false)\n .classed('currentView', false);\n\n context.container().selectAll('.sequence')\n .classed('highlighted', false)\n .classed('currentView', false);\n }\n\n const hoveredImageKey = hovered?.key;\n const hoveredSequence = this.getSequenceForImage(hovered);\n const hoveredSequenceKey = hoveredSequence?.key;\n const hoveredImageKeys = hoveredSequence?.images.map(d => d.key) ?? [];\n\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n const selectedImageKey = selected?.key;\n const selectedSequence = this.getSequenceForImage(selected);\n const selectedSequenceKey = selectedSequence?.key;\n const selectedImageKeys = selectedSequence?.images.map(d => d.key) ?? [];\n\n // highlight sibling viewfields on either the selected or the hovered sequences\n const highlightedImageKeys = utilArrayUnion(hoveredImageKeys, selectedImageKeys);\n\n context.container().selectAll('.layer-vegbilder .viewfield-group')\n .classed('highlighted', d => highlightedImageKeys.indexOf(d.key) !== -1)\n .classed('hovered', d => d.key === hoveredImageKey)\n .classed('currentView', d => d.key === selectedImageKey);\n\n context.container().selectAll('.layer-vegbilder .sequence')\n .classed('highlighted', d => d.key === hoveredSequenceKey)\n .classed('currentView', d => d.key === selectedSequenceKey);\n\n // update viewfields if needed\n context.container().selectAll('.layer-vegbilder .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n const d = this.parentNode.__data__;\n if (d.is_sphere && d.key !== selectedImageKey) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n return this;\n },\n\n updateUrlImage: function (key) {\n const hash = utilStringQs(window.location.hash);\n if (key) {\n hash.photo = 'vegbilder/' + key;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n validHere: function(extent) {\n const bbox = Object.values(extent.bbox());\n return iso1A2Codes(bbox).includes('NO');\n },\n\n\n cache: function () {\n return _vegbilderCache;\n }\n\n};\n", "\n/**\n * osmAuth\n * Easy authentication with OpenStreetMap over OAuth 2.0.\n * @module\n *\n * @param o `Object` containing options:\n * @param o.scope OAuth2 scopes requested (e.g. \"read_prefs write_api\")\n * @param o.client_id OAuth2 client ID\n * @param o.redirect_uri OAuth2 redirect URI (e.g. \"http://127.0.0.1:8080/land.html\")\n * @param o.access_token Can pre-authorize with an OAuth2 bearer token if you have one\n * @param o.apiUrl A base url for the OSM API (default: \"https://api.openstreetmap.org\")\n * @param o.url A base url for the OAuth2 handshake (default: \"https://www.openstreetmap.org\")\n * @param o.auto If `true`, attempt to authenticate automatically when calling `.xhr()` or `.fetch()` (default: `false`)\n * @param o.singlepage If `true`, use page redirection instead of a popup (default: `false`)\n * @param o.loading Function called when auth-related xhr calls start\n * @param o.done Function called when auth-related xhr calls end\n * @param o.locale The locale to use on the OAuth2 authentication page. Optional.\n * @return `self`\n */\nexport function osmAuth(o) {\n var oauth = {};\n\n var CHANNEL_ID = 'osm-api-auth-complete';\n\n // Mock localStorage if needed.\n // Note that accessing localStorage may throw a `SecurityError`, so wrap in a try/catch.\n var _store = null;\n try {\n if (!('localStorage' in globalThis)) {\n throw new Error('No localStorage');\n }\n _store = globalThis.localStorage;\n\n } catch (e) {\n var _mock = new Map();\n _store = {\n isMocked: true,\n hasItem: (k) => _mock.has(k),\n getItem: (k) => _mock.get(k),\n setItem: (k, v) => _mock.set(k, v),\n removeItem: (k) => _mock.delete(k),\n clear: () => _mock.clear()\n };\n }\n\n /**\n * token\n * Get/Set tokens. These are prefixed with the base URL so that `osm-auth`\n * can be used with multiple APIs and the keys in `localStorage` will not clash\n * @param {string} k key\n * @param {string?} v value\n * @return {string?} If getting, returns the stored value or `null`. If setting, returns `undefined`.\n */\n function token(k, v) {\n var key = o.url + k;\n if (arguments.length === 1) {\n var val = _store.getItem(key) || '';\n // Note: legacy tokens might be wrapped in double quotes - remove them, see #129\n return val.replace(/\"/g, '');\n\n } else if (arguments.length === 2) {\n if (v) {\n return _store.setItem(key, v);\n } else {\n return _store.removeItem(key);\n }\n }\n }\n\n\n /**\n * authenticated\n * Test whether the user is currently authenticated\n *\n * @return {boolean} `true` if authenticated, `false` if not\n */\n oauth.authenticated = function() {\n return !!token('oauth2_access_token');\n };\n\n\n /**\n * logout\n * Removes any stored authentication tokens (legacy OAuth1 tokens too)\n *\n * @return `self`\n */\n oauth.logout = function () {\n token('oauth2_access_token', ''); // OAuth2\n token('oauth_token', ''); // OAuth1\n token('oauth_token_secret', ''); // OAuth1\n token('oauth_request_token_secret', ''); // OAuth1\n return oauth;\n };\n\n\n /**\n * authenticate\n * First logs out, then runs the authentiation flow, finally calls the callback.\n * TODO: detect lack of click event (probably can settimeout it)\n *\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @param {LoginOptions} [options] Other options\n * @return none\n */\n oauth.authenticate = function(callback, options) {\n if (oauth.authenticated()) {\n callback(null, oauth);\n return;\n }\n\n oauth.logout();\n\n _preopenPopup(function(error, popup) {\n if (error) {\n callback(error);\n } else {\n _generatePkceChallenge(function(pkce) {\n _authenticate(pkce, options, popup, callback);\n });\n }\n });\n };\n\n\n /**\n * authenticateAsync\n * Promisified version of `authenticate`\n * @param {LoginOptions} [options]\n * @return {Promise} Promise settled with whatever `_authenticate` did\n */\n oauth.authenticateAsync = function(options) {\n if (oauth.authenticated()) {\n return Promise.resolve(oauth);\n }\n\n oauth.logout();\n\n return new Promise((resolve, reject) => {\n var errback = (err, result) => {\n if (err) {\n reject(err);\n } else {\n resolve(result);\n }\n };\n\n _preopenPopup((error, popup) => {\n if (error) {\n errback(error);\n } else {\n _generatePkceChallenge(pkce => _authenticate(pkce, options, popup, errback));\n }\n });\n });\n };\n\n\n /**\n * opens an empty popup to be later used for the authentication page\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @return none\n */\n function _preopenPopup(callback) {\n if (o.singlepage) {\n callback(null, undefined);\n return;\n }\n\n // Create a 550x610 popup window in the center of the screen\n var w = 550;\n var h = 610;\n var settings = [\n ['width', w],\n ['height', h],\n ['left', window.screen.width / 2 - w / 2],\n ['top', window.screen.height / 2 - h / 2],\n ]\n .map(function (x) { return x.join('='); })\n .join(',');\n var popup = window.open('about:blank', 'oauth_window', settings);\n if (popup) {\n callback(null, popup);\n } else {\n var error = new Error('Popup was blocked');\n error.status = 'popup-blocked';\n callback(error);\n }\n }\n\n\n /**\n * _authenticate\n * internal authenticate\n *\n * @typedef {{ switchUser?: boolean }} LoginOptions\n *\n * @param {Object} pkce Object containing PKCE code challenge properties\n * @param {LoginOptions=} options Other options\n * @param {Window} popup Popup Window to use for the authentication page, should be undefined when using singlepage mode\n * @param {function} callback Errback-style callback that accepts `(err, result)`\n */\n function _authenticate(pkce, options, popup, callback) {\n var state = generateState();\n\n // ## Request authorization to access resources from the user\n // and receive authorization code\n var path =\n '/oauth2/authorize?' +\n utilQsString({\n client_id: o.client_id,\n redirect_uri: o.redirect_uri,\n response_type: 'code',\n scope: o.scope,\n state: state,\n code_challenge: pkce.code_challenge,\n code_challenge_method: pkce.code_challenge_method,\n locale: o.locale || '',\n });\n\n var url = options?.switchUser\n ? `${o.url}/logout?referer=${encodeURIComponent(`/login?referer=${encodeURIComponent(path)}`)}`\n : o.url + path;\n\n if (o.singlepage) {\n if (_store.isMocked) {\n // in singlepage mode, PKCE requires working non-volatile storage\n var error = new Error('localStorage unavailable, but required in singlepage mode');\n error.status = 'pkce-localstorage-unavailable';\n callback(error);\n return;\n }\n var params = utilStringQs(window.location.search.slice(1));\n if (params.code) {\n oauth.bootstrapToken(params.code, callback);\n } else {\n // save OAuth2 state and PKCE challenge in local storage, for later use\n // in the `/oauth/token` request\n token('oauth2_state', state);\n token('oauth2_pkce_code_verifier', pkce.code_verifier);\n window.location = url;\n }\n } else {\n oauth.popupWindow = popup;\n popup.location = url;\n }\n\n // Called by a function in the redirect URL page, in the popup window. The\n // window closes itself.\n var bc = new BroadcastChannel(CHANNEL_ID);\n bc.addEventListener('message', (event) => {\n var url = event.data;\n var params = utilStringQs(url.split('?')[1]);\n if (params.state !== state) {\n var error = new Error('Invalid state');\n error.status = 'invalid-state';\n callback(error);\n return;\n } else if (params.error !== undefined) {\n var err = new Error(params.error_description.replace(/\\+/g, ' '));\n err.status = params.error;\n callback(err);\n return;\n }\n _getAccessToken(params.code, pkce.code_verifier, accessTokenDone);\n bc.close();\n });\n\n function accessTokenDone(err, xhr) {\n o.done();\n if (err) {\n callback(err);\n return;\n }\n var access_token = JSON.parse(xhr.response);\n token('oauth2_access_token', access_token.access_token);\n callback(null, oauth);\n }\n }\n\n\n /**\n * _getAccessToken\n * The client requests an access token by authenticating with the\n * authorization server and presenting the `auth_code`, brought\n * in from a function call on a landing page popup.\n * @param {string} auth_code\n * @param {string} code_verifier\n * @param {function} accessTokenDone Errback-style callback `(err, result)`, called when complete\n */\n function _getAccessToken(auth_code, code_verifier, accessTokenDone) {\n var url =\n o.url +\n '/oauth2/token?' +\n utilQsString({\n client_id: o.client_id,\n redirect_uri: o.redirect_uri,\n grant_type: 'authorization_code',\n code: auth_code,\n code_verifier: code_verifier\n });\n\n // The authorization server authenticates the client and validates\n // the authorization grant, and if valid, issues an access token.\n oauth.rawxhr('POST', url, null, null, null, accessTokenDone);\n o.loading();\n }\n\n\n /**\n * bringPopupWindowToFront\n * Tries to bring an existing authentication popup to the front.\n *\n * @return {boolean} `true` if it succeeded, `false` if not\n */\n oauth.bringPopupWindowToFront = function() {\n var broughtPopupToFront = false;\n try {\n // This may cause a cross-origin error:\n // `DOMException: Blocked a frame with origin \"...\" from accessing a cross-origin frame.`\n if (oauth.popupWindow && !oauth.popupWindow.closed) {\n oauth.popupWindow.focus();\n broughtPopupToFront = true;\n }\n } catch (err) {\n // Bringing popup window to front failed (probably because of the cross-origin error mentioned above)\n }\n return broughtPopupToFront;\n };\n\n\n /**\n * bootstrapToken\n * The authorization code is a temporary code that a client can exchange for an access token.\n * If using this library in single-page mode, you'll need to call this once your application\n * has an `auth_code` and wants to get an access_token.\n *\n * @param {string} auth_code The OAuth2 `auth_code`\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @return none\n */\n oauth.bootstrapToken = function(auth_code, callback) {\n var state = token('oauth2_state');\n token('oauth2_state', '');\n var params = utilStringQs(window.location.search.slice(1));\n if (params.state !== state) {\n var error = new Error('Invalid state');\n error.status = 'invalid-state';\n callback(error);\n return;\n }\n var code_verifier = token('oauth2_pkce_code_verifier');\n token('oauth2_pkce_code_verifier', '');\n _getAccessToken(auth_code, code_verifier, accessTokenDone);\n\n function accessTokenDone(err, xhr) {\n o.done();\n if (err) {\n callback(err);\n return;\n }\n var access_token = JSON.parse(xhr.response);\n token('oauth2_access_token', access_token.access_token);\n callback(null, oauth);\n }\n };\n\n\n /**\n * fetch\n * A `fetch` wrapper that includes the Authorization header if the user is authenticated.\n * https://developer.mozilla.org/en-US/docs/Web/API/fetch\n *\n * @param {string} resource Resource passed to `fetch`\n * @param {Object} options Options passed to `fetch`\n * @return {Promise} Promise that wraps `authenticateAsync` then `fetch`\n */\n oauth.fetch = function(resource, options) {\n if (oauth.authenticated()) {\n return _doFetch();\n } else {\n if (o.auto) {\n return oauth.authenticateAsync().then(_doFetch);\n } else {\n return Promise.reject(new Error('not authenticated'));\n }\n }\n\n function _doFetch() {\n options = options || {};\n if (!options.headers) {\n options.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };\n }\n options.headers.Authorization = 'Bearer ' + token('oauth2_access_token');\n return fetch(resource, options);\n }\n };\n\n\n /**\n * xhr\n * A `XMLHttpRequest` wrapper that does authenticated calls if the user has logged in.\n * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest\n *\n * @param {Object} options\n * @param options.method Passed to `xhr.open` (e.g. 'GET', 'POST')\n * @param options.prefix If `true` path contains a path, if `false` path contains the full url\n * @param options.path The URL path (e.g. \"/api/0.6/user/details\") (or full url, if `prefix`=`false`)\n * @param options.content Passed to `xhr.send`\n * @param options.headers `Object` containing request headers\n * @param {function} callback Errback-style callback `(err, result)`, called when complete\n * @return {XMLHttpRequest} `XMLHttpRequest` if authenticated, otherwise `null`\n */\n oauth.xhr = function (options, callback) {\n if (oauth.authenticated()) {\n return _doXHR();\n } else {\n if (o.auto) {\n oauth.authenticate(_doXHR);\n return;\n } else {\n callback('not authenticated', null);\n return;\n }\n }\n\n function _doXHR() {\n var url = options.prefix !== false ? (o.apiUrl + options.path) : options.path;\n return oauth.rawxhr(\n options.method,\n url,\n token('oauth2_access_token'),\n options.content,\n options.headers,\n done\n );\n }\n\n function done(err, xhr) {\n if (err) {\n callback(err);\n } else if (xhr.responseXML) {\n callback(err, xhr.responseXML);\n } else {\n callback(err, xhr.response);\n }\n }\n };\n\n\n /**\n * rawxhr\n * Creates the XMLHttpRequest set up with a header and response handling\n * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest\n *\n * @param method Passed to `xhr.open` (e.g. 'GET', 'POST')\n * @param url Passed to `xhr.open`\n * @param access_token OAuth2 bearer token\n * @param data Passed to `xhr.send`\n * @param headers `Object` containing request headers\n * @param callback An \"errback\"-style callback (`err`, `result`), called when complete\n * @return `XMLHttpRequest`\n */\n oauth.rawxhr = function(method, url, access_token, data, headers, callback) {\n headers = headers || { 'Content-Type': 'application/x-www-form-urlencoded' };\n\n if (access_token) {\n headers.Authorization = 'Bearer ' + access_token;\n }\n\n var xhr = new XMLHttpRequest();\n\n xhr.onreadystatechange = function () {\n if (4 === xhr.readyState && 0 !== xhr.status) {\n if (/^20\\d$/.test(xhr.status)) { // a 20x status code - OK\n callback(null, xhr);\n } else {\n callback(xhr, null);\n }\n }\n };\n xhr.onerror = function (e) {\n callback(e, null);\n };\n\n xhr.open(method, url, true);\n for (var h in headers) xhr.setRequestHeader(h, headers[h]);\n\n xhr.send(data);\n return xhr;\n };\n\n\n /**\n * preauth\n * Pre-authorize this object, if we already have access token from the start\n *\n * @param {Object} val Object containing `access_token` property\n * @return `self`\n */\n oauth.preauth = function(val) {\n if (val && val.access_token) {\n token('oauth2_access_token', val.access_token);\n }\n return oauth;\n };\n\n\n /**\n * options (getter / setter)\n * If passed with no arguments, just return the options\n * If passed an Object, set the options then attempt to pre-authorize\n *\n * @param val? Object containing options\n * @return current `options` (if getting), or `self` (if setting)\n */\n oauth.options = function(val) {\n if (!arguments.length) return o;\n\n o = val;\n o.apiUrl = o.apiUrl || 'https://api.openstreetmap.org';\n o.url = o.url || 'https://www.openstreetmap.org';\n o.auto = o.auto || false;\n o.singlepage = o.singlepage || false;\n\n // Optional loading and loading-done functions for nice UI feedback.\n // by default, no-ops\n o.loading = o.loading || function () {};\n o.done = o.done || function () {};\n return oauth.preauth(o);\n };\n\n\n // Everything below here is initialization/setup code\n // Handle options and attempt to pre-authorize\n oauth.options(o);\n\n return oauth;\n}\n\n\n/**\n * utilQsString\n * Transforms object of `key=value` pairs into query string\n * @param {Object} Object of `key=value` pairs\n * @returns {string} query string\n */\nfunction utilQsString(obj) {\n return Object.keys(obj)\n .filter(function(key) {\n return obj[key] !== undefined;\n })\n .sort()\n .map(function(key) {\n return (encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));\n })\n .join('&');\n}\n\n/**\n * utilStringQs\n * Transforms query string into object of `key=value` pairs\n * @param {string} query string\n * @returns {Object} Object of `key=value` pairs\n */\nfunction utilStringQs(str) {\n var i = 0; // advance past any leading '?' or '#' characters\n while (i < str.length && (str[i] === '?' || str[i] === '#')) i++;\n str = str.slice(i);\n\n return str.split('&').reduce(function(obj, pair) {\n var parts = pair.split('=');\n if (parts.length === 2) {\n obj[parts[0]] = decodeURIComponent(parts[1]);\n }\n return obj;\n }, {});\n}\n\n\n/**\n * Generates a challenge/verifier pair for PKCE.\n * If the browser does not support the WebCryptoAPI, the \"plain\" method is\n * used as a fallback instead of a SHA-256 hash.\n * @param {callback} callback called with the result of the generated PKCE challenge\n */\nfunction _generatePkceChallenge(callback) {\n var code_verifier;\n // generate a random code_verifier\n // https://datatracker.ietf.org/doc/html/rfc7636#section-7.1\n var random = globalThis.crypto.getRandomValues(new Uint8Array(32));\n code_verifier = base64(random.buffer);\n var verifier = Uint8Array.from(Array.from(code_verifier).map(function(char) {\n return char.charCodeAt(0);\n }));\n\n // generate challenge for code verifier\n globalThis.crypto.subtle.digest('SHA-256', verifier).then(function(hash) {\n var code_challenge = base64(hash);\n\n callback({\n code_challenge: code_challenge,\n code_verifier: code_verifier,\n code_challenge_method: 'S256'\n });\n });\n}\n\n\n/**\n * Returns a random state to be used as the \"state\" of the OAuth2 authentication\n * See https://datatracker.ietf.org/doc/html/rfc6749#section-10.12\n */\nfunction generateState() {\n var state;\n var random = globalThis.crypto.getRandomValues(new Uint8Array(32));\n state = base64(random.buffer);\n\n return state;\n}\n\n\n/**\n * base64\n * Converts binary buffer to base64 encoded string, as used in rfc7636\n * @param {ArrayBuffer} buffer\n * @returns {string} base64 encoded\n */\nfunction base64(buffer) {\n return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))\n .replace(/\\//g, '_')\n .replace(/\\+/g, '-')\n .replace(/[=]/g, '');\n}\n", "export var JXON = new (function () {\n var\n sValueProp = 'keyValue', sAttributesProp = 'keyAttributes', sAttrPref = '@', /* you can customize these values */\n aCache = [], rIsNull = /^\\s*$/, rIsBool = /^(?:true|false)$/i;\n\n function parseText (sValue) {\n if (rIsNull.test(sValue)) { return null; }\n if (rIsBool.test(sValue)) { return sValue.toLowerCase() === 'true'; }\n if (isFinite(sValue)) { return parseFloat(sValue); }\n if (isFinite(Date.parse(sValue))) { return new Date(sValue); }\n return sValue;\n }\n\n function EmptyTree () { }\n EmptyTree.prototype.toString = function () { return 'null'; };\n EmptyTree.prototype.valueOf = function () { return null; };\n\n function objectify (vValue) {\n return vValue === null ? new EmptyTree() : vValue instanceof Object ? vValue : new vValue.constructor(vValue);\n }\n\n function createObjTree (oParentNode, nVerb, bFreeze, bNesteAttr) {\n var\n nLevelStart = aCache.length, bChildren = oParentNode.hasChildNodes(),\n bAttributes = oParentNode.hasAttributes(), bHighVerb = Boolean(nVerb & 2);\n\n var\n sProp, vContent, nLength = 0, sCollectedTxt = '',\n vResult = bHighVerb ? {} : /* put here the default value for empty nodes: */ true;\n\n if (bChildren) {\n for (var oNode, nItem = 0; nItem < oParentNode.childNodes.length; nItem++) {\n oNode = oParentNode.childNodes.item(nItem);\n if (oNode.nodeType === 4) {\n /* nodeType is 'CDATASection' (4) */\n sCollectedTxt += oNode.nodeValue;\n } else if (oNode.nodeType === 3) {\n /* nodeType is 'Text' (3) */\n sCollectedTxt += oNode.nodeValue.trim();\n } else if (oNode.nodeType === 1 && !oNode.prefix) {\n /* nodeType is 'Element' (1) */\n aCache.push(oNode);\n }\n }\n }\n\n var nLevelEnd = aCache.length, vBuiltVal = parseText(sCollectedTxt);\n\n if (!bHighVerb && (bChildren || bAttributes)) { vResult = nVerb === 0 ? objectify(vBuiltVal) : {}; }\n\n for (var nElId = nLevelStart; nElId < nLevelEnd; nElId++) {\n sProp = aCache[nElId].nodeName.toLowerCase();\n vContent = createObjTree(aCache[nElId], nVerb, bFreeze, bNesteAttr);\n if (vResult.hasOwnProperty(sProp)) {\n if (vResult[sProp].constructor !== Array) { vResult[sProp] = [vResult[sProp]]; }\n vResult[sProp].push(vContent);\n } else {\n vResult[sProp] = vContent;\n nLength++;\n }\n }\n\n if (bAttributes) {\n var\n nAttrLen = oParentNode.attributes.length,\n sAPrefix = bNesteAttr ? '' : sAttrPref, oAttrParent = bNesteAttr ? {} : vResult;\n\n for (var oAttrib, nAttrib = 0; nAttrib < nAttrLen; nLength++, nAttrib++) {\n oAttrib = oParentNode.attributes.item(nAttrib);\n oAttrParent[sAPrefix + oAttrib.name.toLowerCase()] = parseText(oAttrib.value.trim());\n }\n\n if (bNesteAttr) {\n if (bFreeze) { Object.freeze(oAttrParent); }\n vResult[sAttributesProp] = oAttrParent;\n nLength -= nAttrLen - 1;\n }\n }\n\n if (nVerb === 3 || (nVerb === 2 || nVerb === 1 && nLength > 0) && sCollectedTxt) {\n vResult[sValueProp] = vBuiltVal;\n } else if (!bHighVerb && nLength === 0 && sCollectedTxt) {\n vResult = vBuiltVal;\n }\n\n if (bFreeze && (bHighVerb || nLength > 0)) { Object.freeze(vResult); }\n\n aCache.length = nLevelStart;\n\n return vResult;\n }\n\n function loadObjTree (oXMLDoc, oParentEl, oParentObj) {\n var vValue, oChild;\n\n if (oParentObj instanceof String || oParentObj instanceof Number || oParentObj instanceof Boolean) {\n oParentEl.appendChild(oXMLDoc.createTextNode(oParentObj.toString())); /* verbosity level is 0 */\n } else if (oParentObj.constructor === Date) {\n oParentEl.appendChild(oXMLDoc.createTextNode(oParentObj.toGMTString()));\n }\n\n for (var sName in oParentObj) {\n vValue = oParentObj[sName];\n if (isFinite(sName) || vValue instanceof Function) { continue; } /* verbosity level is 0 */\n if (sName === sValueProp) {\n if (vValue !== null && vValue !== true) { oParentEl.appendChild(oXMLDoc.createTextNode(vValue.constructor === Date ? vValue.toGMTString() : String(vValue))); }\n } else if (sName === sAttributesProp) { /* verbosity level is 3 */\n for (var sAttrib in vValue) { oParentEl.setAttribute(sAttrib, vValue[sAttrib]); }\n } else if (sName.charAt(0) === sAttrPref) {\n oParentEl.setAttribute(sName.slice(1), vValue);\n } else if (vValue.constructor === Array) {\n for (var nItem = 0; nItem < vValue.length; nItem++) {\n oChild = oXMLDoc.createElementNS(null, sName);\n loadObjTree(oXMLDoc, oChild, vValue[nItem]);\n oParentEl.appendChild(oChild);\n }\n } else {\n oChild = oXMLDoc.createElementNS(null, sName);\n if (vValue instanceof Object) {\n loadObjTree(oXMLDoc, oChild, vValue);\n } else if (vValue !== null && vValue !== true) {\n oChild.appendChild(oXMLDoc.createTextNode(vValue.toString()));\n }\n oParentEl.appendChild(oChild);\n }\n }\n }\n\n this.build = function (oXMLParent, nVerbosity /* optional */, bFreeze /* optional */, bNesteAttributes /* optional */) {\n var _nVerb = arguments.length > 1 && typeof nVerbosity === 'number' ? nVerbosity & 3 : /* put here the default verbosity level: */ 1;\n return createObjTree(oXMLParent, _nVerb, bFreeze || false, arguments.length > 3 ? bNesteAttributes : _nVerb === 3);\n };\n\n this.unbuild = function (oObjTree) {\n var oNewDoc = document.implementation.createDocument('', '', null);\n loadObjTree(oNewDoc, oNewDoc, oObjTree);\n return oNewDoc;\n };\n\n this.stringify = function (oObjTree) {\n return (new XMLSerializer()).serializeToString(JXON.unbuild(oObjTree));\n };\n})();\n\n// var myObject = JXON.build(doc);\n// we got our javascript object! try: alert(JSON.stringify(myObject));\n\n// var newDoc = JXON.unbuild(myObject);\n// we got our Document instance! try: alert((new XMLSerializer()).serializeToString(newDoc));\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { json as d3_json } from 'd3-fetch';\nimport { osmAuth } from 'osm-auth';\nimport RBush from 'rbush';\n\nimport { JXON } from '../util/jxon';\nimport { geoExtent, geoRawMercator, geoVecAdd, geoZoomToScale } from '../geo';\nimport { osmEntity, osmNode, osmNote, osmRelation, osmWay } from '../osm';\nimport { utilArrayChunk, utilArrayGroupBy, utilArrayUniq, utilObjectOmit, utilRebind, utilTiler, utilQsString } from '../util';\nimport { localizer } from '../core/localizer.js';\nimport { utilGzip } from '../util/util';\nimport { osmApiConnections } from '../../config/id.js';\n\n\nvar tiler = utilTiler();\nvar dispatch = d3_dispatch('apiStatusChange', 'authLoading', 'authDone', 'change', 'loading', 'loaded', 'loadedNotes');\n\nvar urlroot = osmApiConnections[0].url;\nvar apiUrlroot = osmApiConnections[0].apiUrl || urlroot;\nvar redirectPath = window.location.origin + window.location.pathname;\nvar oauth = new osmAuth({\n url: urlroot,\n apiUrl: apiUrlroot,\n client_id: osmApiConnections[0].client_id,\n scope: 'read_prefs write_prefs write_api read_gpx write_notes',\n redirect_uri: redirectPath + 'land.html',\n loading: authLoading,\n done: authDone\n});\nvar _apiConnections = osmApiConnections;\n\n// hardcode default block of Google Maps\nvar _imageryBlocklists = [/.*\\.google(apis)?\\..*\\/(vt|kh)[\\?\\/].*([xyz]=.*){3}.*/];\nvar _tileCache = { toLoad: {}, loaded: {}, inflight: {}, seen: {}, rtree: new RBush() };\nvar _noteCache = { toLoad: {}, loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: new RBush() };\nvar _userCache = { toLoad: {}, user: {} };\nvar _cachedApiStatus;\nvar _changeset = {};\n\nvar _deferred = new Set();\nvar _connectionID = 1;\nvar _tileZoom = 16;\nvar _noteZoom = 12;\nvar _rateLimitError;\nvar _userChangesets;\nvar _userDetails;\nvar _off;\n\n// set a default but also load this from the API status\nvar _maxWayNodes = 2000;\nlet _maxChangesetElements = 10_000;\n\n\nfunction authLoading() {\n dispatch.call('authLoading');\n}\n\n\nfunction authDone() {\n dispatch.call('authDone');\n}\n\n\nfunction abortRequest(controllerOrXHR) {\n if (controllerOrXHR) {\n controllerOrXHR.abort();\n }\n}\n\n\nfunction hasInflightRequests(cache) {\n return Object.keys(cache.inflight).length;\n}\n\n\nfunction abortUnwantedRequests(cache, visibleTiles) {\n Object.keys(cache.inflight).forEach(function(k) {\n if (cache.toLoad[k]) return;\n if (visibleTiles.find(function(tile) { return k === tile.id; })) return;\n\n abortRequest(cache.inflight[k]);\n delete cache.inflight[k];\n });\n}\n\nfunction getNodesJSON(obj) {\n var elems = obj.nodes;\n var nodes = new Array(elems.length);\n for (var i = 0, l = elems.length; i < l; i++) {\n nodes[i] = 'n' + elems[i];\n }\n return nodes;\n}\n\nfunction getMembersJSON(obj) {\n var elems = obj.members;\n var members = new Array(elems.length);\n for (var i = 0, l = elems.length; i < l; i++) {\n var attrs = elems[i];\n members[i] = {\n id: attrs.type[0] + attrs.ref,\n type: attrs.type,\n role: attrs.role\n };\n }\n return members;\n}\n\nfunction encodeNoteRtree(note) {\n return {\n minX: note.loc[0],\n minY: note.loc[1],\n maxX: note.loc[0],\n maxY: note.loc[1],\n data: note\n };\n}\n\n\nvar jsonparsers = {\n\n node: function nodeData(obj, uid) {\n return new osmNode({\n id: uid,\n visible: typeof obj.visible === 'boolean' ? obj.visible : true,\n version: obj.version && obj.version.toString(),\n changeset: obj.changeset && obj.changeset.toString(),\n timestamp: obj.timestamp,\n user: obj.user,\n uid: obj.uid && obj.uid.toString(),\n loc: [Number(obj.lon), Number(obj.lat)],\n tags: obj.tags\n });\n },\n\n way: function wayData(obj, uid) {\n return new osmWay({\n id: uid,\n visible: typeof obj.visible === 'boolean' ? obj.visible : true,\n version: obj.version && obj.version.toString(),\n changeset: obj.changeset && obj.changeset.toString(),\n timestamp: obj.timestamp,\n user: obj.user,\n uid: obj.uid && obj.uid.toString(),\n tags: obj.tags,\n nodes: getNodesJSON(obj)\n });\n },\n\n relation: function relationData(obj, uid) {\n return new osmRelation({\n id: uid,\n visible: typeof obj.visible === 'boolean' ? obj.visible : true,\n version: obj.version && obj.version.toString(),\n changeset: obj.changeset && obj.changeset.toString(),\n timestamp: obj.timestamp,\n user: obj.user,\n uid: obj.uid && obj.uid.toString(),\n tags: obj.tags,\n members: getMembersJSON(obj)\n });\n },\n\n user: function parseUser(obj, uid) {\n return {\n id: uid,\n display_name: obj.display_name,\n account_created: obj.account_created,\n image_url: obj.img && obj.img.href,\n changesets_count: obj.changesets && obj.changesets.count && obj.changesets.count.toString() || '0',\n active_blocks: obj.blocks && obj.blocks.received && obj.blocks.received.active && obj.blocks.received.active.toString() || '0'\n };\n }\n};\n\nfunction parseJSON(payload, callback, options) {\n options = Object.assign({ skipSeen: true }, options);\n if (!payload) {\n return callback({ message: 'No JSON', status: -1 });\n }\n\n var json = payload;\n if (typeof json !== 'object') json = JSON.parse(payload);\n\n if (!json.elements) return callback({ message: 'No JSON', status: -1 });\n\n var children = json.elements;\n\n var handle = window.requestIdleCallback(function() {\n _deferred.delete(handle);\n var results = [];\n var result;\n for (var i = 0; i < children.length; i++) {\n result = parseChild(children[i]);\n if (result) results.push(result);\n }\n callback(null, results);\n });\n _deferred.add(handle);\n\n function parseChild(child) {\n var parser = jsonparsers[child.type];\n if (!parser) return null;\n\n var uid;\n\n uid = osmEntity.id.fromOSM(child.type, child.id);\n if (options.skipSeen) {\n if (_tileCache.seen[uid]) return null; // avoid reparsing a \"seen\" entity\n _tileCache.seen[uid] = true;\n }\n\n return parser(child, uid);\n }\n}\n\nfunction parseUserJSON(payload, callback, options) {\n options = Object.assign({ skipSeen: true }, options);\n if (!payload) {\n return callback({ message: 'No JSON', status: -1 });\n }\n\n var json = payload;\n if (typeof json !== 'object') json = JSON.parse(payload);\n\n if (!json.users && !json.user) return callback({ message: 'No JSON', status: -1 });\n\n var objs = json.users || [json];\n\n var handle = window.requestIdleCallback(function() {\n _deferred.delete(handle);\n var results = [];\n var result;\n for (var i = 0; i < objs.length; i++) {\n result = parseObj(objs[i]);\n if (result) results.push(result);\n }\n callback(null, results);\n });\n _deferred.add(handle);\n\n function parseObj(obj) {\n var uid = obj.user.id && obj.user.id.toString();\n if (options.skipSeen && _userCache.user[uid]) {\n delete _userCache.toLoad[uid];\n return null;\n }\n var user = jsonparsers.user(obj.user, uid);\n _userCache.user[uid] = user;\n delete _userCache.toLoad[uid];\n return user;\n }\n}\n\nfunction parseNoteJSON(payload, callback, _options) {\n const options = { skipSeen: true, ..._options };\n if (!payload) {\n return callback({ message: 'No JSON', status: -1 });\n }\n\n const features = payload.type === 'FeatureCollection' ? payload.features : [payload];\n\n const notes = features.map(feature => {\n const uid = feature.properties.id;\n if (options.skipSeen) {\n if (_tileCache.seen[uid]) return null; // avoid reparsing a \"seen\" entity\n _tileCache.seen[uid] = true;\n }\n\n const props = {\n ...feature.properties,\n loc: feature.geometry.coordinates,\n };\n\n // if notes are coincident, move them apart slightly\n if (!_noteCache.note[uid]) {\n let coincident = false;\n const epsilon = 0.00001;\n do {\n if (coincident) {\n props.loc = geoVecAdd(props.loc, [epsilon, epsilon]);\n }\n const bbox = geoExtent(props.loc).bbox();\n coincident = _noteCache.rtree.search(bbox).length;\n } while (coincident);\n } else {\n // we already saw this note: don't change its location again\n props.loc = _noteCache.note[uid].loc;\n }\n\n var note = new osmNote(props);\n var item = encodeNoteRtree(note);\n _noteCache.note[note.id] = note;\n updateRtree(item, true);\n\n return note;\n });\n callback(undefined, notes);\n}\n\n// replace or remove note from rtree\nfunction updateRtree(item, replace) {\n _noteCache.rtree.remove(item, function isEql(a, b) { return a.data.id === b.data.id; });\n\n if (replace) {\n _noteCache.rtree.insert(item);\n }\n}\n\n\nfunction wrapcb(thisArg, callback, cid) {\n return function(err, result) {\n if (err) {\n return callback.call(thisArg, err);\n\n } else if (thisArg.getConnectionId() !== cid) {\n return callback.call(thisArg, { message: 'Connection Switched', status: -1 });\n\n } else {\n return callback.call(thisArg, err, result);\n }\n };\n}\n\n\nexport default {\n\n init: function() {\n utilRebind(this, dispatch, 'on');\n },\n\n\n reset: function() {\n Array.from(_deferred).forEach(function(handle) {\n window.cancelIdleCallback(handle);\n _deferred.delete(handle);\n });\n\n _connectionID++;\n _userChangesets = undefined;\n _userDetails = undefined;\n _rateLimitError = undefined;\n\n Object.values(_tileCache.inflight).forEach(abortRequest);\n Object.values(_noteCache.inflight).forEach(abortRequest);\n Object.values(_noteCache.inflightPost).forEach(abortRequest);\n if (_changeset.inflight) abortRequest(_changeset.inflight);\n\n _tileCache = { toLoad: {}, loaded: {}, inflight: {}, seen: {}, rtree: new RBush() };\n _noteCache = { toLoad: {}, loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: new RBush() };\n _userCache = { toLoad: {}, user: {} };\n _cachedApiStatus = undefined;\n _changeset = {};\n\n return this;\n },\n\n\n getConnectionId: function() {\n return _connectionID;\n },\n\n\n getUrlRoot: function() {\n return urlroot;\n },\n\n\n getApiUrlRoot: function() {\n return apiUrlroot;\n },\n\n\n changesetURL: function(changesetID) {\n return urlroot + '/changeset/' + changesetID;\n },\n\n\n changesetsURL: function(center, zoom) {\n var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));\n return urlroot + '/history#map=' +\n Math.floor(zoom) + '/' +\n center[1].toFixed(precision) + '/' +\n center[0].toFixed(precision);\n },\n\n\n entityURL: function(entity) {\n return urlroot + '/' + entity.type + '/' + entity.osmId();\n },\n\n\n historyURL: function(entity) {\n return urlroot + '/' + entity.type + '/' + entity.osmId() + '/history';\n },\n\n\n userURL: function(username) {\n return urlroot + '/user/' + encodeURIComponent(username);\n },\n\n\n noteURL: function(note) {\n return urlroot + '/note/' + note.id;\n },\n\n\n noteReportURL: function(note) {\n return urlroot + '/reports/new?reportable_type=Note&reportable_id=' + note.id;\n },\n\n\n // Generic method to load data from the OSM API\n // Can handle either auth or unauth calls.\n loadFromAPI: function(path, callback, options) {\n options = Object.assign({ skipSeen: true }, options);\n var that = this;\n var cid = _connectionID;\n\n function done(err, payloadString) {\n if (that.getConnectionId() !== cid) {\n if (callback) callback({ message: 'Connection Switched', status: -1 });\n return;\n }\n\n if ((err && _cachedApiStatus === 'online') ||\n (!err && _cachedApiStatus !== 'online')) {\n // If the response's error state doesn't match the status,\n // it's likely we lost or gained the connection so reload the status\n that.reloadApiStatus();\n }\n\n if (callback) {\n if (err) {\n // eslint-disable-next-line no-console\n console.error('API error:', err);\n return callback(err);\n } else {\n const payload = typeof payloadString === 'string' ? JSON.parse(payloadString) : payloadString;\n\n if (payload.type === 'FeatureCollection' || payload.type === 'Feature') {\n return parseNoteJSON(payload, callback, options);\n } else {\n return parseJSON(payload, callback, options);\n }\n }\n }\n }\n\n if (this.authenticated()) {\n return oauth.xhr({\n method: 'GET',\n path\n }, done);\n } else {\n var url = apiUrlroot + path;\n var controller = new AbortController();\n\n d3_json(url, { signal: controller.signal })\n .then(function(data) {\n done(null, data);\n })\n .catch(function(err) {\n if (err.name === 'AbortError') return;\n // d3-fetch includes status in the error message,\n // but we can't access the response itself\n // https://github.com/d3/d3-fetch/issues/27\n var match = err.message.match(/^\\d{3}/);\n if (match) {\n done({ status: +match[0], statusText: err.message });\n } else {\n done(err.message);\n }\n });\n return controller;\n }\n },\n\n\n // Load a single entity by id (ways and relations use the `/full` call to include\n // nodes and members). Parent relations are not included, see `loadEntityRelations`.\n // GET /api/0.6/node/#id\n // GET /api/0.6/[way|relation]/#id/full\n loadEntity: function(id, callback) {\n var type = osmEntity.id.type(id);\n var osmID = osmEntity.id.toOSM(id);\n var options = { skipSeen: false };\n\n this.loadFromAPI(\n '/api/0.6/' + type + '/' + osmID + (type !== 'node' ? '/full' : '') + '.json',\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n // Load a single note by id , XML format\n // GET /api/0.6/notes/#id\n loadEntityNote: function(id, callback) {\n var options = { skipSeen: false };\n this.loadFromAPI(\n `/api/0.6/notes/${id}.json`,\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n\n // Load a single entity with a specific version\n // GET /api/0.6/[node|way|relation]/#id/#version\n loadEntityVersion: function(id, version, callback) {\n var type = osmEntity.id.type(id);\n var osmID = osmEntity.id.toOSM(id);\n var options = { skipSeen: false };\n\n this.loadFromAPI(\n '/api/0.6/' + type + '/' + osmID + '/' + version + '.json',\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n\n // Load the relations of a single entity with the given.\n // GET /api/0.6/[node|way|relation]/#id/relations\n loadEntityRelations: function(id, callback) {\n var type = osmEntity.id.type(id);\n var osmID = osmEntity.id.toOSM(id);\n var options = { skipSeen: false };\n\n this.loadFromAPI(\n '/api/0.6/' + type + '/' + osmID + '/relations.json',\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n },\n\n\n // Load multiple entities in chunks\n // (note: callback may be called multiple times)\n // Unlike `loadEntity`, child nodes and members are not fetched\n // GET /api/0.6/[nodes|ways|relations]?#parameters\n loadMultiple: function(ids, callback) {\n var that = this;\n var groups = utilArrayGroupBy(utilArrayUniq(ids), osmEntity.id.type);\n\n Object.keys(groups).forEach(function(k) {\n var type = k + 's'; // nodes, ways, relations\n var osmIDs = groups[k].map(function(id) { return osmEntity.id.toOSM(id); });\n var options = { skipSeen: false };\n\n utilArrayChunk(osmIDs, 150).forEach(function(arr) {\n that.loadFromAPI(\n '/api/0.6/' + type + '.json?' + type + '=' + arr.join(),\n function(err, entities) {\n if (callback) callback(err, { data: entities });\n },\n options\n );\n });\n });\n },\n\n\n // Create, upload, and close a changeset\n // PUT /api/0.6/changeset/create\n // POST /api/0.6/changeset/#id/upload\n // PUT /api/0.6/changeset/#id/close\n putChangeset: function(changeset, changes, callback) {\n var cid = _connectionID;\n\n if (_changeset.inflight) {\n return callback({ message: 'Changeset already inflight', status: -2 }, changeset);\n\n } else if (_changeset.open) { // reuse existing open changeset..\n return createdChangeset.call(this, null, _changeset.open);\n\n } else { // Open a new changeset..\n var options = {\n method: 'PUT',\n path: '/api/0.6/changeset/create',\n headers: { 'Content-Type': 'text/xml' },\n content: JXON.stringify(changeset.asJXON())\n };\n _changeset.inflight = oauth.xhr(\n options,\n wrapcb(this, createdChangeset, cid)\n );\n }\n\n\n async function createdChangeset(err, changesetID) {\n _changeset.inflight = null;\n if (err) { return callback(err, changeset); }\n\n _changeset.open = changesetID;\n changeset = changeset.update({ id: changesetID });\n\n // Upload the changeset..\n const xml = JXON.stringify(changeset.osmChangeJXON(changes));\n const compressed = await utilGzip(xml);\n\n const headers = { 'Content-Type': 'text/xml' };\n if (compressed) headers['Content-Encoding'] = 'gzip';\n\n var options = {\n method: 'POST',\n path: '/api/0.6/changeset/' + changesetID + '/upload',\n headers,\n content: compressed || xml,\n };\n _changeset.inflight = oauth.xhr(\n options,\n wrapcb(this, uploadedChangeset, cid)\n );\n }\n\n\n function uploadedChangeset(err) {\n _changeset.inflight = null;\n if (err) return callback(err, changeset);\n\n // Upload was successful, safe to call the callback.\n // Add delay to allow for postgres replication #1646 #2678\n window.setTimeout(function() { callback(null, changeset); }, 2500);\n _changeset.open = null;\n\n // At this point, we don't really care if the connection was switched..\n // Only try to close the changeset if we're still talking to the same server.\n if (this.getConnectionId() === cid) {\n // Still attempt to close changeset, but ignore response because #2667\n oauth.xhr({\n method: 'PUT',\n path: '/api/0.6/changeset/' + changeset.id + '/close',\n headers: { 'Content-Type': 'text/xml' }\n }, function() { return true; });\n }\n }\n },\n\n /** updates the tags on an existing unclosed changeset */\n // PUT /api/0.6/changeset/#id\n updateChangesetTags: (changeset) => {\n return oauth.fetch(`${oauth.options().apiUrl}/api/0.6/changeset/${changeset.id}`, {\n method: 'PUT',\n headers: { 'Content-Type': 'text/xml' },\n body: JXON.stringify(changeset.asJXON())\n });\n },\n\n\n // Load multiple users in chunks\n // (note: callback may be called multiple times)\n // GET /api/0.6/users?users=#id1,#id2,...,#idn\n loadUsers: function(uids, callback) {\n var toLoad = [];\n var cached = [];\n\n utilArrayUniq(uids).forEach(function(uid) {\n if (_userCache.user[uid]) {\n delete _userCache.toLoad[uid];\n cached.push(_userCache.user[uid]);\n } else {\n toLoad.push(uid);\n }\n });\n\n if (cached.length || !this.authenticated()) {\n callback(undefined, cached);\n if (!this.authenticated()) return; // require auth\n }\n\n utilArrayChunk(toLoad, 150).forEach(function(arr) {\n oauth.xhr({\n method: 'GET',\n path: '/api/0.6/users.json?users=' + arr.join()\n }, wrapcb(this, done, _connectionID));\n }.bind(this));\n\n function done(err, payload) {\n if (err) return callback(err);\n\n var options = { skipSeen: true };\n return parseUserJSON(payload, function(err, results) {\n if (err) return callback(err);\n return callback(undefined, results);\n }, options);\n }\n },\n\n\n // Load a given user by id\n // GET /api/0.6/user/#id\n loadUser: function(uid, callback) {\n if (_userCache.user[uid] || !this.authenticated()) { // require auth\n delete _userCache.toLoad[uid];\n return callback(undefined, _userCache.user[uid]);\n }\n\n oauth.xhr({\n method: 'GET',\n path: '/api/0.6/user/' + uid + '.json'\n }, wrapcb(this, done, _connectionID));\n\n function done(err, payload) {\n if (err) return callback(err);\n\n var options = { skipSeen: true };\n return parseUserJSON(payload, function(err, results) {\n if (err) return callback(err);\n return callback(undefined, results[0]);\n }, options);\n }\n },\n\n\n // Load the details of the logged-in user\n // GET /api/0.6/user/details\n userDetails: function(callback) {\n if (_userDetails) { // retrieve cached\n return callback(undefined, _userDetails);\n }\n\n oauth.xhr({\n method: 'GET',\n path: '/api/0.6/user/details.json'\n }, wrapcb(this, done, _connectionID));\n\n function done(err, payload) {\n if (err) return callback(err);\n\n var options = { skipSeen: false };\n return parseUserJSON(payload, function(err, results) {\n if (err) return callback(err);\n _userDetails = results[0];\n return callback(undefined, _userDetails);\n }, options);\n }\n },\n\n\n // Load previous changesets for the logged in user\n // GET /api/0.6/changesets?user=#id\n userChangesets: function(callback) {\n if (_userChangesets) { // retrieve cached\n return callback(undefined, _userChangesets);\n }\n\n this.userDetails(\n wrapcb(this, gotDetails, _connectionID)\n );\n\n\n function gotDetails(err, user) {\n if (err) { return callback(err); }\n\n oauth.xhr({\n method: 'GET',\n path: `/api/0.6/changesets.json?user=${user.id}`\n }, wrapcb(this, done, _connectionID));\n }\n\n function done(err, payloadString) {\n if (err) { return callback(err); }\n\n const payload = JSON.parse(payloadString);\n _userChangesets = payload.changesets.filter(tags => tags.tags.comment);\n\n return callback(undefined, _userChangesets);\n }\n },\n\n\n // Fetch the status of the OSM API\n // GET /api/capabilities\n status: function(callback) {\n const url = `${apiUrlroot}/api/capabilities.json`;\n var errback = wrapcb(this, done, _connectionID);\n d3_json(url)\n .then(function(data) { errback(null, data); })\n .catch(function(err) { errback(err.message); });\n\n function done(err, payload) {\n if (err) {\n // the status is null if no response could be retrieved\n return callback(err, null);\n }\n\n if (_rateLimitError) {\n return callback(_rateLimitError, 'rateLimited');\n } else {\n _maxWayNodes = payload.api.waynodes.maximum;\n\n _imageryBlocklists = payload.policy.imagery.blacklist.map(item => new RegExp(item.regex, 'i'));\n\n const maxChangesetElements = payload.api.changesets.maximum_elements;\n if (!Number.isNaN(maxChangesetElements)) _maxChangesetElements = maxChangesetElements;\n\n return callback(undefined, payload.api.status.api);\n }\n }\n },\n\n // Calls `status` and dispatches an `apiStatusChange` event if the returned\n // status differs from the cached status.\n reloadApiStatus: function() {\n // throttle to avoid unnecessary API calls\n if (!this.throttledReloadApiStatus) {\n var that = this;\n this.throttledReloadApiStatus = throttle(function() {\n that.status(function(err, status) {\n if (status !== _cachedApiStatus) {\n _cachedApiStatus = status;\n dispatch.call('apiStatusChange', that, err, status);\n }\n });\n }, 500);\n }\n this.throttledReloadApiStatus();\n },\n\n\n // Returns the maximum number of nodes a single way can have\n maxWayNodes: function() {\n return _maxWayNodes;\n },\n\n\n maxChangesetElements: () => _maxChangesetElements,\n\n\n // Load data (entities) from the API in tiles\n // GET /api/0.6/map?bbox=\n loadTiles: function(projection, callback) {\n if (_off) return;\n\n // determine the needed tiles to cover the view\n var tiles = tiler.zoomExtent([_tileZoom, _tileZoom]).getTiles(projection);\n\n // abort inflight requests that are no longer needed\n var hadRequests = hasInflightRequests(_tileCache);\n abortUnwantedRequests(_tileCache, tiles);\n if (hadRequests && !hasInflightRequests(_tileCache)) {\n if (_rateLimitError) {\n // was rate limited, but has settled\n _rateLimitError = undefined;\n dispatch.call('change');\n this.reloadApiStatus();\n }\n dispatch.call('loaded'); // stop the spinner\n }\n\n // issue new requests..\n tiles.forEach(function(tile) {\n this.loadTile(tile, callback);\n }, this);\n },\n\n\n // Load a single data tile\n // GET /api/0.6/map?bbox=\n loadTile: function(tile, callback) {\n if (_off) return;\n if (_tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return;\n\n if (!hasInflightRequests(_tileCache)) {\n dispatch.call('loading'); // start the spinner\n }\n\n var path = '/api/0.6/map.json?bbox=';\n var options = { skipSeen: true };\n\n _tileCache.inflight[tile.id] = this.loadFromAPI(\n path + tile.extent.toParam(),\n tileCallback.bind(this),\n options\n );\n\n function tileCallback(err, parsed) {\n if (!err) {\n delete _tileCache.inflight[tile.id];\n delete _tileCache.toLoad[tile.id];\n _tileCache.loaded[tile.id] = true;\n var bbox = tile.extent.bbox();\n bbox.id = tile.id;\n _tileCache.rtree.insert(bbox);\n } else {\n // map tile loading error: e.g. network connection error,\n // 509 Bandwidth Limit Exceeded, 429 Too Many Requests\n if (!_rateLimitError && err.status === 509 || err.status === 429) {\n // show \"API rate limiting\" warning\n _rateLimitError = err;\n dispatch.call('change');\n this.reloadApiStatus();\n }\n setTimeout(() => {\n // retry loading the tiles\n delete _tileCache.inflight[tile.id];\n this.loadTile(tile, callback);\n }, 8000);\n }\n if (callback) {\n callback(err, Object.assign({ data: parsed }, tile));\n }\n if (!hasInflightRequests(_tileCache)) {\n if (_rateLimitError) {\n // was rate limited, but has settled\n _rateLimitError = undefined;\n dispatch.call('change');\n this.reloadApiStatus();\n }\n dispatch.call('loaded'); // stop the spinner\n }\n }\n },\n\n\n isDataLoaded: function(loc) {\n var bbox = { minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1] };\n return _tileCache.rtree.collides(bbox);\n },\n\n\n // load the tile that covers the given `loc`\n loadTileAtLoc: function(loc, callback) {\n // Back off if the toLoad queue is filling up.. re #6417\n // (Currently `loadTileAtLoc` requests are considered low priority - used by operations to\n // let users safely edit geometries which extend to unloaded tiles. We can drop some.)\n if (Object.keys(_tileCache.toLoad).length > 50) return;\n\n var k = geoZoomToScale(_tileZoom + 1);\n var offset = geoRawMercator().scale(k)(loc);\n var projection = geoRawMercator().transform({ k: k, x: -offset[0], y: -offset[1] });\n var tiles = tiler.zoomExtent([_tileZoom, _tileZoom]).getTiles(projection);\n\n tiles.forEach(function(tile) {\n if (_tileCache.toLoad[tile.id] || _tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return;\n\n _tileCache.toLoad[tile.id] = true;\n this.loadTile(tile, callback);\n }, this);\n },\n\n\n // Load notes from the API in tiles\n // GET /api/0.6/notes?bbox=\n loadNotes: function(projection, noteOptions) {\n noteOptions = Object.assign({ limit: 10000, closed: 7 }, noteOptions);\n if (_off) return;\n\n var that = this;\n var path = `/api/0.6/notes.json?limit=${noteOptions.limit}&closed=${noteOptions.closed}&bbox=`;\n var throttleLoadUsers = throttle(function() {\n var uids = Object.keys(_userCache.toLoad);\n if (!uids.length) return;\n that.loadUsers(uids, function() {}); // eagerly load user details\n }, 750);\n\n // determine the needed tiles to cover the view\n var tiles = tiler.zoomExtent([_noteZoom, _noteZoom]).getTiles(projection);\n\n // abort inflight requests that are no longer needed\n abortUnwantedRequests(_noteCache, tiles);\n\n // issue new requests..\n tiles.forEach(function(tile) {\n if (_noteCache.loaded[tile.id] || _noteCache.inflight[tile.id]) return;\n\n var options = { skipSeen: false };\n _noteCache.inflight[tile.id] = that.loadFromAPI(\n path + tile.extent.toParam(),\n function(err) {\n delete _noteCache.inflight[tile.id];\n if (!err) {\n _noteCache.loaded[tile.id] = true;\n }\n throttleLoadUsers();\n dispatch.call('loadedNotes');\n },\n options\n );\n });\n },\n\n\n // Create a note\n // POST /api/0.6/notes?params\n postNoteCreate: function(note, callback) {\n if (!this.authenticated()) {\n return callback({ message: 'Not Authenticated', status: -3 }, note);\n }\n if (_noteCache.inflightPost[note.id]) {\n return callback({ message: 'Note update already inflight', status: -2 }, note);\n }\n\n if (!note.loc[0] || !note.loc[1] || !note.newComment) return; // location & description required\n\n var comment = note.newComment;\n if (note.newCategory && note.newCategory !== 'None') { comment += ' #' + note.newCategory; }\n\n var path = '/api/0.6/notes.json?' + utilQsString({ lon: note.loc[0], lat: note.loc[1], text: comment });\n\n _noteCache.inflightPost[note.id] = oauth.xhr({\n method: 'POST',\n path: path\n }, wrapcb(this, done, _connectionID));\n\n\n function done(err, payload) {\n delete _noteCache.inflightPost[note.id];\n if (err) { return callback(err); }\n\n // we get the updated note back, remove from caches and reparse..\n this.removeNote(note);\n\n var options = { skipSeen: false };\n return parseNoteJSON(JSON.parse(payload), function(err, results) {\n if (err) {\n return callback(err);\n } else {\n return callback(undefined, results[0]);\n }\n }, options);\n }\n },\n\n\n // Update a note\n // POST /api/0.6/notes/#id/comment?text=comment\n // POST /api/0.6/notes/#id/close?text=comment\n // POST /api/0.6/notes/#id/reopen?text=comment\n postNoteUpdate: function(note, newStatus, callback) {\n if (!this.authenticated()) {\n return callback({ message: 'Not Authenticated', status: -3 }, note);\n }\n if (_noteCache.inflightPost[note.id]) {\n return callback({ message: 'Note update already inflight', status: -2 }, note);\n }\n\n var action;\n if (note.status !== 'closed' && newStatus === 'closed') {\n action = 'close';\n } else if (note.status !== 'open' && newStatus === 'open') {\n action = 'reopen';\n } else {\n action = 'comment';\n if (!note.newComment) return; // when commenting, comment required\n }\n\n var path = `/api/0.6/notes/${note.id}/${action}.json`;\n if (note.newComment) {\n path += '?' + utilQsString({ text: note.newComment });\n }\n\n _noteCache.inflightPost[note.id] = oauth.xhr({\n method: 'POST',\n path: path\n }, wrapcb(this, done, _connectionID));\n\n\n function done(err, payload) {\n delete _noteCache.inflightPost[note.id];\n if (err) { return callback(err); }\n\n // we get the updated note back, remove from caches and reparse..\n this.removeNote(note);\n\n // update closed note cache - used to populate `closed:note` changeset tag\n if (action === 'close') {\n _noteCache.closed[note.id] = true;\n } else if (action === 'reopen') {\n delete _noteCache.closed[note.id];\n }\n\n var options = { skipSeen: false };\n return parseNoteJSON(JSON.parse(payload), function(err, results) {\n if (err) {\n return callback(err);\n } else {\n return callback(undefined, results[0]);\n }\n }, options);\n }\n },\n\n\n /* connection options for source switcher (optional) */\n apiConnections: function(val) {\n if (!arguments.length) return _apiConnections;\n _apiConnections = val;\n return this;\n },\n\n\n switch: function(newOptions) {\n urlroot = newOptions.url;\n apiUrlroot = newOptions.apiUrl || urlroot;\n if (newOptions.url && !newOptions.apiUrl) {\n newOptions = {\n ...newOptions,\n apiUrl: newOptions.url\n };\n }\n\n // Copy the existing options, but omit 'access_token'.\n // (if we did preauth, access_token won't work on a different server)\n const oldOptions = utilObjectOmit(oauth.options(), 'access_token');\n oauth.options({...oldOptions, ...newOptions});\n\n this.reset();\n this.userChangesets(function() {}); // eagerly load user details/changesets\n dispatch.call('change');\n return this;\n },\n\n\n toggle: function(val) {\n _off = !val;\n return this;\n },\n\n\n isChangesetInflight: function() {\n return !!_changeset.inflight;\n },\n\n\n // get/set cached data\n // This is used to save/restore the state when entering/exiting the walkthrough\n // Also used for testing purposes.\n caches: function(obj) {\n function cloneCache(source) {\n var target = {};\n Object.keys(source).forEach(function(k) {\n if (k === 'rtree') {\n target.rtree = new RBush().fromJSON(source.rtree.toJSON()); // clone rbush\n } else if (k === 'note') {\n target.note = {};\n Object.keys(source.note).forEach(function(id) {\n target.note[id] = osmNote(source.note[id]); // copy notes\n });\n } else {\n target[k] = JSON.parse(JSON.stringify(source[k])); // clone deep\n }\n });\n return target;\n }\n\n if (!arguments.length) {\n return {\n tile: cloneCache(_tileCache),\n note: cloneCache(_noteCache),\n user: cloneCache(_userCache)\n };\n }\n\n // access caches directly for testing (e.g., loading notes rtree)\n if (obj === 'get') {\n return {\n tile: _tileCache,\n note: _noteCache,\n user: _userCache\n };\n }\n\n if (obj.tile) {\n _tileCache = obj.tile;\n _tileCache.inflight = {};\n }\n if (obj.note) {\n _noteCache = obj.note;\n _noteCache.inflight = {};\n _noteCache.inflightPost = {};\n }\n if (obj.user) {\n _userCache = obj.user;\n }\n\n return this;\n },\n\n\n logout: function() {\n _userChangesets = undefined;\n _userDetails = undefined;\n oauth.logout();\n dispatch.call('change');\n return this;\n },\n\n\n authenticated: function() {\n return oauth.authenticated();\n },\n\n\n /** @param {import('osm-auth').LoginOptions} options */\n authenticate: function(callback, options) {\n var that = this;\n var cid = _connectionID;\n _userChangesets = undefined;\n _userDetails = undefined;\n\n function done(err, res) {\n if (err) {\n if (callback) callback(err);\n return;\n }\n if (that.getConnectionId() !== cid) {\n if (callback) callback({ message: 'Connection Switched', status: -1 });\n return;\n }\n _rateLimitError = undefined;\n dispatch.call('change');\n if (callback) callback(err, res);\n that.userChangesets(function() {}); // eagerly load user details/changesets\n }\n\n // ensure the locale is correctly set before opening the popup\n oauth.options({\n ...oauth.options(),\n locale: localizer.localeCode(),\n });\n\n oauth.authenticate(done, options);\n },\n\n\n imageryBlocklists: function() {\n return _imageryBlocklists;\n },\n\n\n tileZoom: function(val) {\n if (!arguments.length) return _tileZoom;\n _tileZoom = val;\n return this;\n },\n\n\n // get all cached notes covering the viewport\n notes: function(projection) {\n var viewport = projection.clipExtent();\n var min = [viewport[0][0], viewport[1][1]];\n var max = [viewport[1][0], viewport[0][1]];\n var bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n\n return _noteCache.rtree.search(bbox)\n .map(function(d) { return d.data; });\n },\n\n\n // get a single note from the cache\n getNote: function(id) {\n return _noteCache.note[id];\n },\n\n\n // remove a single note from the cache\n removeNote: function(note) {\n if (!(note instanceof osmNote) || !note.id) return;\n\n delete _noteCache.note[note.id];\n updateRtree(encodeNoteRtree(note), false); // false = remove\n },\n\n\n // replace a single note in the cache\n replaceNote: function(note) {\n if (!(note instanceof osmNote) || !note.id) return;\n\n _noteCache.note[note.id] = note;\n updateRtree(encodeNoteRtree(note), true); // true = replace\n return note;\n },\n\n\n // Get an array of note IDs closed during this session.\n // Used to populate `closed:note` changeset tag\n getClosedIDs: function() {\n return Object.keys(_noteCache.closed).sort();\n }\n\n};\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { json as d3_json } from 'd3-fetch';\n\nimport { localizer } from '../core/localizer';\nimport { utilQsString } from '../util';\n\n\nvar apibase = 'https://wiki.openstreetmap.org/w/api.php';\nvar _inflight = {};\nvar _wikibaseCache = {};\nvar _localeIDs = { en: false };\n\n\nvar debouncedRequest = debounce(request, 500, { leading: false });\n\nfunction request(url, callback) {\n if (_inflight[url]) return;\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, { signal: controller.signal })\n .then(function(result) {\n delete _inflight[url];\n if (callback) callback(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (callback) callback(err.message);\n });\n}\n\n\nexport default {\n\n init: function() {\n _inflight = {};\n _wikibaseCache = {};\n _localeIDs = {};\n },\n\n\n reset: function() {\n Object.values(_inflight).forEach(function(controller) { controller.abort(); });\n _inflight = {};\n },\n\n\n /**\n * Get the best value for the property, or undefined if not found\n * @param entity object from wikibase\n * @param property string e.g. 'P4' for image\n * @param langCode string e.g. 'fr' for French\n */\n claimToValue: function(entity, property, langCode) {\n if (!entity.claims[property]) return undefined;\n var locale = _localeIDs[langCode];\n var preferredPick, localePick;\n\n entity.claims[property].forEach(function(stmt) {\n // If exists, use value limited to the needed language (has a qualifier P26 = locale)\n // Or if not found, use the first value with the \"preferred\" rank\n if (!preferredPick && stmt.rank === 'preferred') {\n preferredPick = stmt;\n }\n if (locale && stmt.qualifiers && stmt.qualifiers.P26 &&\n stmt.qualifiers.P26[0].datavalue.value.id === locale\n ) {\n localePick = stmt;\n }\n });\n\n var result = localePick || preferredPick;\n if (result) {\n var datavalue = result.mainsnak.datavalue;\n return datavalue.type === 'wikibase-entityid' ? datavalue.value.id : datavalue.value;\n } else {\n return undefined;\n }\n },\n\n\n /**\n * Convert monolingual property into a key-value object (language -> value)\n * @param entity object from wikibase\n * @param property string e.g. 'P31' for monolingual wiki page title\n */\n monolingualClaimToValueObj: function(entity, property) {\n if (!entity || !entity.claims[property]) return undefined;\n\n return entity.claims[property].reduce(function(acc, obj) {\n var value = obj.mainsnak.datavalue.value;\n acc[value.language] = value.text;\n return acc;\n }, {});\n },\n\n\n toSitelink: function(key, value, isHistorical) {\n var type = value ? 'Tag' : 'Key';\n var prefix = '';\n if (isHistorical) {\n prefix = `OpenHistoricalMap/Tags/${type}/`;\n } else {\n prefix = type + ':';\n }\n return (prefix + (value ? `${key}=${value}` : key).replace(/_/g, ' ')).trim();\n },\n\n /**\n * Converts text like `tag:...=...` into clickable links\n *\n * @param {string} unsafeText - unsanitized text\n */\n linkifyWikiText(unsafeText) {\n /** @param {import('d3').Selection} selection */\n return (selection) => {\n const segments = unsafeText.split(/(key|tag):([\\w-]+)(=([\\w-]+))?/g);\n\n for (let i = 0; i < segments.length; i += 5) {\n const [plainText, , key, , value] = segments.slice(i);\n\n if (plainText) {\n selection\n .append('span')\n .text(plainText);\n }\n\n if (key) {\n selection\n .append('a')\n .attr('href', `https://wiki.openstreetmap.org/wiki/${this.toSitelink(key, value)}`)\n .attr('target', '_blank')\n .attr('rel', 'noreferrer')\n .append('code')\n .text(`${key}=${value || '*'}`);\n }\n }\n };\n },\n\n\n //\n // Pass params object of the form:\n // {\n // key: 'string',\n // value: 'string',\n // langCode: 'string'\n // }\n //\n getEntity: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n var that = this;\n var titles = [];\n var result = {};\n var rtypeSitelink = (params.key === 'type' && params.value) ? ('Relation:' + params.value).replace(/_/g, ' ').trim() : false;\n var rtypeSitelinkHistorical = (params.key === 'type' && params.value) ? ('OpenHistoricalMap/Tags/Relation/' + params.value.replace(/_/g, ' ').trim()) : false;\n var keySitelink = params.key ? this.toSitelink(params.key) : false;\n var keySitelinkHistorical = params.key ? this.toSitelink(params.key, null, true) : false;\n var tagSitelink = (params.key && params.value) ? this.toSitelink(params.key, params.value) : false;\n var tagSitelinkHistorical = (params.key && params.value) ? this.toSitelink(params.key, params.value, true) : false;\n\n if (params.langCodes) {\n params.langCodes.forEach(function(langCode) {\n if (_localeIDs[langCode] === undefined) {\n // If this is the first time we are asking about this locale,\n // fetch corresponding entity (if it exists), and cache it.\n // If there is no such entry, cache `false` value to avoid re-requesting it.\n let localeSitelink = ('Locale:' + langCode).replace(/_/g, ' ').trim();\n titles.push(localeSitelink);\n\n // initialize with false, such that if locale ID is not found in first request,\n // it will not be retried in further queries\n that.addLocale(langCode, false);\n }\n });\n }\n\n if (rtypeSitelink) {\n if (_wikibaseCache[rtypeSitelinkHistorical]) {\n result.rtype = _wikibaseCache[rtypeSitelinkHistorical];\n } else if (_wikibaseCache[rtypeSitelink]) {\n result.rtype = _wikibaseCache[rtypeSitelink];\n } else {\n titles.push(rtypeSitelink, rtypeSitelinkHistorical);\n }\n }\n\n if (keySitelink) {\n if (_wikibaseCache[keySitelinkHistorical]) {\n result.key = _wikibaseCache[keySitelinkHistorical];\n } else if (_wikibaseCache[keySitelink]) {\n result.key = _wikibaseCache[keySitelink];\n } else {\n titles.push(keySitelink, keySitelinkHistorical);\n }\n }\n\n if (tagSitelink) {\n if (_wikibaseCache[tagSitelinkHistorical]) {\n result.tag = _wikibaseCache[tagSitelinkHistorical];\n } else if (_wikibaseCache[tagSitelink]) {\n result.tag = _wikibaseCache[tagSitelink];\n } else {\n titles.push(tagSitelink, tagSitelinkHistorical);\n }\n }\n\n if (!titles.length) {\n // Nothing to do, we already had everything in the cache\n return callback(null, result);\n }\n\n // Requesting just the user language code\n // If backend recognizes the code, it will perform proper fallbacks,\n // and the result will contain the requested code. If not, all values are returned:\n // {\"zh-tw\":{\"value\":\"...\",\"language\":\"zh-tw\",\"source-language\":\"zh-hant\"}\n // {\"pt-br\":{\"value\":\"...\",\"language\":\"pt\",\"for-language\":\"pt-br\"}}\n var obj = {\n action: 'wbgetentities',\n sites: 'wiki',\n titles: titles.join('|'),\n languages: params.langCodes.join('|'),\n languagefallback: 1,\n origin: '*',\n format: 'json',\n // There is an MW Wikibase API bug https://phabricator.wikimedia.org/T212069\n // We shouldn't use v1 until it gets fixed, but should switch to it afterwards\n // formatversion: 2,\n };\n\n var url = apibase + '?' + utilQsString(obj);\n doRequest(url, function(err, d) {\n if (err) {\n callback(err);\n } else if (!d.success || d.error) {\n callback(d.error.messages.map(function(v) { return v.html['*']; }).join('
    '));\n } else {\n Object.values(d.entities).forEach(function(res) {\n if (res.missing !== '') {\n\n var title = res.sitelinks.wiki.title;\n if (title === rtypeSitelinkHistorical) {\n _wikibaseCache[rtypeSitelinkHistorical] = res;\n result.rtype = res;\n } else if (title === rtypeSitelink) {\n _wikibaseCache[rtypeSitelink] = res;\n result.rtype = res;\n } else if (title === keySitelinkHistorical) {\n _wikibaseCache[keySitelinkHistorical] = res;\n result.key = res;\n } else if (title === keySitelink) {\n _wikibaseCache[keySitelink] = res;\n result.key = res;\n } else if (title === tagSitelinkHistorical) {\n _wikibaseCache[tagSitelinkHistorical] = res;\n result.tag = res;\n } else if (title === tagSitelink) {\n _wikibaseCache[tagSitelink] = res;\n result.tag = res;\n } else if (title.startsWith('Locale:')) {\n const langCode = title.replace(/ /g, '_').replace(/^Locale:/, '');\n that.addLocale(langCode, res.id);\n } else {\n console.log('Unexpected title ' + title); // eslint-disable-line no-console\n }\n }\n });\n\n callback(null, result);\n }\n });\n },\n\n\n //\n // Pass params object of the form:\n // {\n // key: 'string', // required\n // value: 'string' // optional\n // }\n //\n // Get an result object used to display tag documentation\n // {\n // title: 'string',\n // description: 'string',\n // editURL: 'string',\n // imageURL: 'string',\n // wiki: { title: 'string', text: 'string', url: 'string' }\n // }\n //\n getDocs: function(params, callback) {\n var that = this;\n var langCodes = localizer.localeCodes().map(function(code) {\n return code.toLowerCase();\n });\n params.langCodes = langCodes;\n\n this.getEntity(params, function(err, data) {\n if (err) {\n callback(err);\n return;\n }\n\n var entity = data.rtype || data.tag || data.key;\n if (!entity) {\n callback('No entity');\n return;\n }\n\n var i;\n var description;\n for (i in langCodes) {\n let code = langCodes[i];\n if (entity.descriptions[code] && entity.descriptions[code].language === code) {\n description = entity.descriptions[code];\n break;\n }\n }\n if (!description && Object.values(entity.descriptions).length) description = Object.values(entity.descriptions)[0];\n\n // prepare result\n var result = {\n title: entity.title,\n description: that.linkifyWikiText(description?.value || ''),\n descriptionLocaleCode: description ? description.language : '',\n editURL: 'https://wiki.openstreetmap.org/wiki/' + entity.title\n };\n\n // add image\n if (entity.claims) {\n var imageroot;\n var image = that.claimToValue(entity, 'P4', langCodes[0]);\n if (image) {\n imageroot = 'https://commons.wikimedia.org/w/index.php';\n } else {\n image = that.claimToValue(entity, 'P28', langCodes[0]);\n if (image) {\n imageroot = 'https://wiki.openstreetmap.org/w/index.php';\n }\n }\n if (imageroot && image) {\n result.imageURL = imageroot + '?' + utilQsString({\n title: 'Special:Redirect/file/' + image,\n width: 400\n });\n }\n }\n\n // Try to get a wiki page from tag data item first, followed by the corresponding key data item.\n // If neither tag nor key data item contain a wiki page in the needed language nor English,\n // get the first found wiki page from either the tag or the key item.\n var rtypeWiki = that.monolingualClaimToValueObj(data.rtype, 'P31');\n var tagWiki = that.monolingualClaimToValueObj(data.tag, 'P31');\n var keyWiki = that.monolingualClaimToValueObj(data.key, 'P31');\n\n var wikis = [rtypeWiki, tagWiki, keyWiki];\n for (i in wikis) {\n var wiki = wikis[i];\n for (var j in langCodes) {\n var code = langCodes[j];\n var referenceId = (langCodes[0].split('-')[0] !== 'en' && code.split('-')[0] === 'en') ? 'inspector.wiki_en_reference' : 'inspector.wiki_reference';\n var info = getWikiInfo(wiki, code, referenceId);\n if (info) {\n result.wiki = info;\n break;\n }\n }\n if (result.wiki) break;\n }\n\n callback(null, result);\n\n\n // Helper method to get wiki info if a given language exists\n function getWikiInfo(wiki, langCode, tKey) {\n if (wiki && wiki[langCode]) {\n return {\n title: wiki[langCode],\n text: tKey,\n url: 'https://wiki.openstreetmap.org/wiki/' + wiki[langCode]\n };\n }\n }\n });\n },\n\n getLocaleIDs: () => _localeIDs,\n\n\n addLocale: function(langCode, qid) {\n // Makes it easier to unit test\n _localeIDs[langCode] = qid;\n },\n\n\n apibase: function(val) {\n if (!arguments.length) return apibase;\n apibase = val;\n return this;\n }\n\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { timer as d3_timer } from 'd3-timer';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport RBush from 'rbush';\nimport { t } from '../core/localizer';\n\nimport {\n geoExtent, geoMetersToLat, geoMetersToLon, geoPointInPolygon,\n geoRotate, geoVecLength\n} from '../geo';\n\nimport { utilAesDecrypt, utilArrayUnion, utilQsString, utilRebind, utilStringQs, utilTiler, utilUniqueDomId } from '../util';\n\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\nimport { localeTimestamp } from '../util/date';\n\n\nconst streetsideApi = 'https://dev.virtualearth.net/REST/v1/Imagery/MetaData/Streetside?mapArea={bbox}&key={key}&count={count}&uriScheme=https';\nconst maxResults = 500;\nconst bubbleAppKey = utilAesDecrypt('5c875730b09c6b422433e807e1ff060b6536c791dbfffcffc4c6b18a1bdba1f14593d151adb50e19e1be1ab19aef813bf135d0f103475e5c724dec94389e45d0');\nconst pannellumViewerCSS = 'pannellum/pannellum.css';\nconst pannellumViewerJS = 'pannellum/pannellum.js';\nconst tileZoom = 16.5;\nconst tiler = utilTiler().zoomExtent([tileZoom, tileZoom]).skipNullIsland(true);\nconst dispatch = d3_dispatch('loadedImages', 'viewerChanged');\nconst minHfov = 10; // zoom in degrees: 20, 10, 5\nconst maxHfov = 90; // zoom out degrees\nconst defaultHfov = 45;\n\nlet _hires = false;\nlet _resolution = 512; // higher numbers are slower - 512, 1024, 2048, 4096\nlet _currScene = 0;\nlet _ssCache;\nlet _pannellumViewer;\nlet _sceneOptions = {\n showFullscreenCtrl: false,\n autoLoad: true,\n compass: true,\n yaw: 0,\n minHfov: minHfov,\n maxHfov: maxHfov,\n hfov: defaultHfov,\n type: 'cubemap',\n cubeMap: []\n};\nlet _loadViewerPromise;\n\n\n/**\n * abortRequest().\n */\nfunction abortRequest(i) {\n i.abort();\n}\n\n/**\n * loadTiles() wraps the process of generating tiles and then fetching image points for each tile.\n */\nfunction loadTiles(which, url, projection, margin) {\n const tiles = tiler.margin(margin).getTiles(projection);\n\n // abort inflight requests that are no longer needed\n const cache = _ssCache[which];\n Object.keys(cache.inflight).forEach(k => {\n const wanted = tiles.find(tile => k.indexOf(tile.id + ',') === 0);\n if (!wanted) {\n abortRequest(cache.inflight[k]);\n delete cache.inflight[k];\n }\n });\n\n tiles.forEach(tile => loadNextTilePage(which, url, tile));\n}\n\n\n/**\n * loadNextTilePage() load data for the next tile page in line.\n */\nfunction loadNextTilePage(which, url, tile) {\n const cache = _ssCache[which];\n const nextPage = cache.nextPage[tile.id] || 0;\n const id = tile.id + ',' + String(nextPage);\n if (cache.loaded[id] || cache.inflight[id]) return;\n\n cache.inflight[id] = getBubbles(url, tile, response => {\n cache.loaded[id] = true;\n delete cache.inflight[id];\n if (!response) return;\n\n if (response.resourceSets[0].resources.length === maxResults) {\n // there are more bubbles than the response can fit: re-fetch using tile split into 4\n const split = tile.extent.split();\n loadNextTilePage(which, url, { id: tile.id + ',a', extent: split[0] });\n loadNextTilePage(which, url, { id: tile.id + ',b', extent: split[1] });\n loadNextTilePage(which, url, { id: tile.id + ',c', extent: split[2] });\n loadNextTilePage(which, url, { id: tile.id + ',d', extent: split[3] });\n }\n\n const features = response.resourceSets[0].resources.map(bubble => {\n const bubbleId = bubble.imageUrl;\n if (cache.points[bubbleId]) return null; // skip duplicates\n\n // workaround for https://github.com/openstreetmap/iD/issues/10341#issuecomment-2275724738\n const loc = [\n bubble.lon || bubble.longitude,\n bubble.lat || bubble.latitude\n ];\n const d = {\n service: 'photo',\n loc: loc,\n key: bubbleId,\n imageUrl: bubble.imageUrl\n .replace('{subdomain}', bubble.imageUrlSubdomains[0]),\n ca: bubble.he || bubble.heading,\n captured_at: bubble.vintageEnd,\n captured_by: 'microsoft',\n pano: true,\n sequenceKey: null\n };\n\n cache.points[bubbleId] = d;\n\n return {\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n };\n\n }).filter(Boolean);\n\n cache.rtree.load(features);\n\n if (which === 'bubbles') {\n dispatch.call('loadedImages');\n }\n });\n}\n\n\n/**\n * getBubbles() handles the request to the server for a tile extent of 'bubbles' (streetside image locations).\n */\nfunction getBubbles(url, tile, callback) {\n let rect = tile.extent.rectangle();\n let urlForRequest = url\n .replace('{key}', bubbleAppKey)\n .replace('{bbox}', [rect[1], rect[0], rect[3], rect[2]].join(','))\n .replace('{count}', maxResults);\n\n const controller = new AbortController();\n fetch(urlForRequest, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n }).then(function(result) {\n if (!result) {\n callback(null);\n }\n return callback(result || []);\n }).catch(function(err) {\n if (err.name === 'AbortError') {\n // ignore aborted requests, e.g. from duplicate requests while zooming/panning the map\n } else {\n throw new Error(err);\n }\n });\n return controller;\n }\n\n\n/**\n * loadImage()\n */\nfunction loadImage(imgInfo) {\n return new Promise(resolve => {\n let img = new Image();\n img.onload = () => {\n let canvas = document.getElementById('ideditor-canvas' + imgInfo.face);\n let ctx = canvas.getContext('2d');\n ctx.drawImage(img, imgInfo.x, imgInfo.y);\n resolve({ imgInfo: imgInfo, status: 'ok' });\n };\n img.onerror = () => {\n resolve({ data: imgInfo, status: 'error' });\n };\n img.setAttribute('crossorigin', '');\n img.src = imgInfo.url;\n });\n}\n\n\n/**\n * loadCanvas()\n */\nfunction loadCanvas(imageGroup) {\n return Promise.all(imageGroup.map(loadImage))\n .then((data) => {\n let canvas = document.getElementById('ideditor-canvas' + data[0].imgInfo.face);\n const which = { '01': 0, '02': 1, '03': 2, '10': 3, '11': 4, '12': 5 };\n let face = data[0].imgInfo.face;\n _sceneOptions.cubeMap[which[face]] = canvas.toDataURL('image/jpeg', 1.0);\n return { status: 'loadCanvas for face ' + data[0].imgInfo.face + 'ok'};\n });\n}\n\n\n/**\n * loadFaces()\n */\nfunction loadFaces(faceGroup) {\n return Promise.all(faceGroup.map(loadCanvas))\n .then(() => { return { status: 'loadFaces done' }; });\n}\n\n\nfunction setupCanvas(selection, reset) {\n if (reset) {\n selection.selectAll('#ideditor-stitcher-canvases')\n .remove();\n }\n\n // Add the Streetside working canvases. These are used for 'stitching', or combining,\n // multiple images for each of the six faces, before passing to the Pannellum control as DataUrls\n selection.selectAll('#ideditor-stitcher-canvases')\n .data([0])\n .enter()\n .append('div')\n .attr('id', 'ideditor-stitcher-canvases')\n .attr('display', 'none')\n .selectAll('canvas')\n .data(['canvas01', 'canvas02', 'canvas03', 'canvas10', 'canvas11', 'canvas12'])\n .enter()\n .append('canvas')\n .attr('id', d => 'ideditor-' + d)\n .attr('width', _resolution)\n .attr('height', _resolution);\n}\n\n\nfunction qkToXY(qk) {\n let x = 0;\n let y = 0;\n let scale = 256;\n for (let i = qk.length; i > 0; i--) {\n const key = qk[i-1];\n x += (+(key === '1' || key === '3')) * scale;\n y += (+(key === '2' || key === '3')) * scale;\n scale *= 2;\n }\n return [x, y];\n}\n\n\nfunction getQuadKeys() {\n let dim = _resolution / 256;\n let quadKeys;\n\n if (dim === 16) {\n quadKeys = [\n '0000','0001','0010','0011','0100','0101','0110','0111', '1000','1001','1010','1011','1100','1101','1110','1111',\n '0002','0003','0012','0013','0102','0103','0112','0113', '1002','1003','1012','1013','1102','1103','1112','1113',\n '0020','0021','0030','0031','0120','0121','0130','0131', '1020','1021','1030','1031','1120','1121','1130','1131',\n '0022','0023','0032','0033','0122','0123','0132','0133', '1022','1023','1032','1033','1122','1123','1132','1133',\n '0200','0201','0210','0211','0300','0301','0310','0311', '1200','1201','1210','1211','1300','1301','1310','1311',\n '0202','0203','0212','0213','0302','0303','0312','0313', '1202','1203','1212','1213','1302','1303','1312','1313',\n '0220','0221','0230','0231','0320','0321','0330','0331', '1220','1221','1230','1231','1320','1321','1330','1331',\n '0222','0223','0232','0233','0322','0323','0332','0333', '1222','1223','1232','1233','1322','1323','1332','1333',\n\n '2000','2001','2010','2011','2100','2101','2110','2111', '3000','3001','3010','3011','3100','3101','3110','3111',\n '2002','2003','2012','2013','2102','2103','2112','2113', '3002','3003','3012','3013','3102','3103','3112','3113',\n '2020','2021','2030','2031','2120','2121','2130','2131', '3020','3021','3030','3031','3120','3121','3130','3131',\n '2022','2023','2032','2033','2122','2123','2132','2133', '3022','3023','3032','3033','3122','3123','3132','3133',\n '2200','2201','2210','2211','2300','2301','2310','2311', '3200','3201','3210','3211','3300','3301','3310','3311',\n '2202','2203','2212','2213','2302','2303','2312','2313', '3202','3203','3212','3213','3302','3303','3312','3313',\n '2220','2221','2230','2231','2320','2321','2330','2331', '3220','3221','3230','3231','3320','3321','3330','3331',\n '2222','2223','2232','2233','2322','2323','2332','2333', '3222','3223','3232','3233','3322','3323','3332','3333'\n ];\n\n } else if (dim === 8) {\n quadKeys = [\n '000','001','010','011', '100','101','110','111',\n '002','003','012','013', '102','103','112','113',\n '020','021','030','031', '120','121','130','131',\n '022','023','032','033', '122','123','132','133',\n\n '200','201','210','211', '300','301','310','311',\n '202','203','212','213', '302','303','312','313',\n '220','221','230','231', '320','321','330','331',\n '222','223','232','233', '322','323','332','333'\n ];\n\n } else if (dim === 4) {\n quadKeys = [\n '00','01', '10','11',\n '02','03', '12','13',\n\n '20','21', '30','31',\n '22','23', '32','33'\n ];\n\n } else { // dim === 2\n quadKeys = [\n '0', '1',\n '2', '3'\n ];\n }\n\n return quadKeys;\n}\n\n\n\nexport default {\n /**\n * init() initialize streetside.\n */\n init: function() {\n if (!_ssCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n /**\n * reset() reset the cache.\n */\n reset: function() {\n if (_ssCache) {\n Object.values(_ssCache.bubbles.inflight).forEach(abortRequest);\n }\n\n _ssCache = {\n bubbles: { inflight: {}, loaded: {}, nextPage: {}, rtree: new RBush(), points: {} },\n sequences: {}\n };\n },\n\n /**\n * bubbles()\n */\n bubbles: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _ssCache.bubbles.rtree);\n },\n\n\n cachedImage: function(imageKey) {\n return _ssCache.bubbles.points[imageKey];\n },\n\n\n sequences: function(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n let seen = {};\n let results = [];\n\n // all sequences for bubbles in viewport\n _ssCache.bubbles.rtree.search(bbox)\n .forEach(d => {\n const key = d.data.sequenceKey;\n if (key && !seen[key]) {\n seen[key] = true;\n results.push(_ssCache.sequences[key].geojson);\n }\n });\n\n return results;\n },\n\n\n /**\n * loadBubbles()\n */\n loadBubbles: function(projection, margin) {\n // by default: request 2 nearby tiles so we can connect sequences.\n if (margin === undefined) margin = 2;\n\n loadTiles('bubbles', streetsideApi, projection, margin);\n },\n\n\n viewer: function() {\n return _pannellumViewer;\n },\n\n\n initViewer: function () {\n if (!window.pannellum) return;\n if (_pannellumViewer) return;\n\n _currScene += 1;\n const sceneID = _currScene.toString();\n const options = {\n 'default': { firstScene: sceneID },\n scenes: {}\n };\n options.scenes[sceneID] = _sceneOptions;\n\n _pannellumViewer = window.pannellum.viewer('ideditor-viewer-streetside', options);\n },\n\n\n ensureViewerLoaded: function(context) {\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n // create ms-wrapper, a photo wrapper class\n let wrap = context.container().select('.photoviewer').selectAll('.ms-wrapper')\n .data([0]);\n\n // inject ms-wrapper into the photoviewer div\n // (used by all to house each custom photo viewer)\n let wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper ms-wrapper')\n .classed('hide', true);\n\n let that = this;\n\n let pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n // inject div to support streetside viewer (pannellum) and attribution line\n wrapEnter\n .append('div')\n .attr('id', 'ideditor-viewer-streetside')\n .on(pointerPrefix + 'down.streetside', () => {\n d3_select(window)\n .on(pointerPrefix + 'move.streetside', () => {\n dispatch.call('viewerChanged');\n }, true);\n })\n .on(pointerPrefix + 'up.streetside pointercancel.streetside', () => {\n d3_select(window)\n .on(pointerPrefix + 'move.streetside', null);\n\n // continue dispatching events for a few seconds, in case viewer has inertia.\n let t = d3_timer(elapsed => {\n dispatch.call('viewerChanged');\n if (elapsed > 2000) {\n t.stop();\n }\n });\n })\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n let controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls');\n\n controlsEnter\n .append('button')\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .on('click.forward', step(1))\n .text('\u25BA');\n\n\n // create working canvas for stitching together images\n wrap\n .merge(wrapEnter)\n .call(setupCanvas, true);\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.streetside', () => {\n if (_pannellumViewer) {\n _pannellumViewer.resize();\n }\n });\n\n _loadViewerPromise = new Promise((resolve, reject) => {\n\n let loadedCount = 0;\n function loaded() {\n loadedCount += 1;\n // wait until both files are loaded\n if (loadedCount === 2) resolve();\n }\n\n const head = d3_select('head');\n\n // load streetside pannellum viewer css\n head.selectAll('#ideditor-streetside-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-streetside-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(pannellumViewerCSS))\n .on('load.serviceStreetside', loaded)\n .on('error.serviceStreetside', function() {\n reject();\n });\n\n // load streetside pannellum viewer js\n head.selectAll('#ideditor-streetside-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-streetside-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(pannellumViewerJS))\n .on('load.serviceStreetside', loaded)\n .on('error.serviceStreetside', function() {\n reject();\n });\n })\n .catch(function() {\n _loadViewerPromise = null;\n });\n\n return _loadViewerPromise;\n\n function step(stepBy) {\n return () => {\n let viewer = context.container().select('.photoviewer');\n let selected = viewer.empty() ? undefined : viewer.datum();\n if (!selected) return;\n\n let nextID = (stepBy === 1 ? selected.ne : selected.pr);\n let yaw = _pannellumViewer.getYaw();\n let ca = selected.ca + yaw;\n let origin = selected.loc;\n\n // construct a search trapezoid pointing out from current bubble\n const meters = 35;\n let p1 = [\n origin[0] + geoMetersToLon(meters / 5, origin[1]),\n origin[1]\n ];\n let p2 = [\n origin[0] + geoMetersToLon(meters / 2, origin[1]),\n origin[1] + geoMetersToLat(meters)\n ];\n let p3 = [\n origin[0] - geoMetersToLon(meters / 2, origin[1]),\n origin[1] + geoMetersToLat(meters)\n ];\n let p4 = [\n origin[0] - geoMetersToLon(meters / 5, origin[1]),\n origin[1]\n ];\n\n let poly = [p1, p2, p3, p4, p1];\n\n // rotate it to face forward/backward\n let angle = (stepBy === 1 ? ca : ca + 180) * (Math.PI / 180);\n poly = geoRotate(poly, -angle, origin);\n\n let extent = poly.reduce((extent, point) => {\n return extent.extend(geoExtent(point));\n }, geoExtent());\n\n // find nearest other bubble in the search polygon\n let minDist = Infinity;\n _ssCache.bubbles.rtree.search(extent.bbox())\n .forEach(d => {\n if (d.data.key === selected.key) return;\n if (!geoPointInPolygon(d.data.loc, poly)) return;\n\n let dist = geoVecLength(d.data.loc, selected.loc);\n let theta = selected.ca - d.data.ca;\n let minTheta = Math.min(Math.abs(theta), 360 - Math.abs(theta));\n if (minTheta > 20) {\n dist += 5; // penalize distance if camera angles don't match\n }\n\n if (dist < minDist) {\n nextID = d.data.key;\n minDist = dist;\n }\n });\n\n let nextBubble = nextID && that.cachedImage(nextID);\n if (!nextBubble) return;\n\n context.map().centerEase(nextBubble.loc);\n\n that.selectImage(context, nextBubble.key)\n .yaw(yaw)\n .showViewer(context);\n };\n }\n },\n\n\n yaw: function(yaw) {\n if (typeof yaw !== 'number') return yaw;\n _sceneOptions.yaw = yaw;\n return this;\n },\n\n /**\n * showViewer()\n */\n showViewer: function(context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.ms-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap\n .classed('hide', false)\n .selectAll('.photo-wrapper.ms-wrapper')\n .classed('hide', false);\n }\n\n return this;\n },\n\n\n /**\n * hideViewer()\n */\n hideViewer: function (context) {\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n this.updateUrlImage(null);\n\n return this.setStyles(context, null, true);\n },\n\n\n /**\n * selectImage().\n */\n selectImage: function (context, key) {\n let that = this;\n\n let d = this.cachedImage(key);\n\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null, true);\n\n let wrap = context.container().select('.photoviewer .ms-wrapper');\n let attribution = wrap.selectAll('.photo-attribution').html('');\n\n wrap.selectAll('.pnlm-load-box') // display \"loading..\"\n .style('display', 'block');\n\n if (!d) return this;\n\n this.updateUrlImage(key);\n\n _sceneOptions.northOffset = d.ca;\n\n let line1 = attribution\n .append('div')\n .attr('class', 'attribution-row');\n\n const hiresDomId = utilUniqueDomId('streetside-hires');\n\n // Add hires checkbox\n let label = line1\n .append('label')\n .attr('for', hiresDomId)\n .attr('class', 'streetside-hires');\n\n label\n .append('input')\n .attr('type', 'checkbox')\n .attr('id', hiresDomId)\n .property('checked', _hires)\n .on('click', (d3_event) => {\n d3_event.stopPropagation();\n\n _hires = !_hires;\n _resolution = _hires ? 1024 : 512;\n wrap.call(setupCanvas, true);\n\n let viewstate = {\n yaw: _pannellumViewer.getYaw(),\n pitch: _pannellumViewer.getPitch(),\n hfov: _pannellumViewer.getHfov()\n };\n\n _sceneOptions = Object.assign(_sceneOptions, viewstate);\n that.selectImage(context, d.key)\n .showViewer(context);\n });\n\n label\n .append('span')\n .call(t.append('streetside.hires'));\n\n\n let captureInfo = line1\n .append('div')\n .attr('class', 'attribution-capture-info');\n\n // Add capture date\n if (d.captured_by) {\n const yyyy = (new Date()).getFullYear();\n\n captureInfo\n .append('a')\n .attr('class', 'captured_by')\n .attr('target', '_blank')\n .attr('href', 'https://www.microsoft.com/en-us/maps/streetside')\n .text('\u00A9' + yyyy + ' Microsoft');\n\n captureInfo\n .append('span')\n .text('|');\n }\n\n if (d.captured_at) {\n captureInfo\n .append('span')\n .attr('class', 'captured_at')\n .text(localeTimestamp(d.captured_at));\n }\n\n // Add image links\n let line2 = attribution\n .append('div')\n .attr('class', 'attribution-row');\n\n line2\n .append('a')\n .attr('class', 'image-view-link')\n .attr('target', '_blank')\n .attr('href', 'https://www.bing.com/maps?cp=' + d.loc[1] + '~' + d.loc[0] +\n '&lvl=17&dir=' + d.ca + '&style=x&v=2&sV=1')\n .call(t.append('streetside.view_on_bing'));\n\n line2\n .append('a')\n .attr('class', 'image-report-link')\n .attr('target', '_blank')\n .attr('href', 'https://www.bing.com/maps/privacyreport/streetsideprivacyreport?bubbleid=' +\n encodeURIComponent(d.key) + '&focus=photo&lat=' + d.loc[1] + '&lng=' + d.loc[0] + '&z=17')\n .call(t.append('streetside.report'));\n\n // Cubemap face code order matters here: front=01, right=02, back=03, left=10, up=11, down=12\n const faceKeys = ['01','02','03','10','11','12'];\n\n // Map images to cube faces\n let quadKeys = getQuadKeys();\n let faces = faceKeys.map((faceKey) => {\n return quadKeys.map((quadKey) => {\n const xy = qkToXY(quadKey);\n return {\n face: faceKey,\n url: d.imageUrl\n .replace('{faceId}', faceKey)\n .replace('{tileId}', quadKey),\n x: xy[0],\n y: xy[1]\n };\n });\n });\n\n loadFaces(faces)\n .then(function() {\n\n if (!_pannellumViewer) {\n that.initViewer();\n } else {\n // make a new scene\n _currScene += 1;\n let sceneID = _currScene.toString();\n _pannellumViewer\n .addScene(sceneID, _sceneOptions)\n .loadScene(sceneID);\n\n // remove previous scene\n if (_currScene > 2) {\n sceneID = (_currScene - 1).toString();\n _pannellumViewer\n .removeScene(sceneID);\n }\n }\n });\n\n return this;\n },\n\n\n getSequenceKeyForBubble: function(d) {\n return d && d.sequenceKey;\n },\n\n\n // Updates the currently highlighted sequence and selected bubble.\n // Reset is only necessary when interacting with the viewport because\n // this implicitly changes the currently selected bubble/sequence\n setStyles: function (context, hovered, reset) {\n if (reset) { // reset all layers\n context.container().selectAll('.viewfield-group')\n .classed('highlighted', false)\n .classed('hovered', false)\n .classed('currentView', false);\n\n context.container().selectAll('.sequence')\n .classed('highlighted', false)\n .classed('currentView', false);\n }\n\n let hoveredBubbleKey = hovered && hovered.key;\n let hoveredSequenceKey = this.getSequenceKeyForBubble(hovered);\n let hoveredSequence = hoveredSequenceKey && _ssCache.sequences[hoveredSequenceKey];\n let hoveredBubbleKeys = (hoveredSequence && hoveredSequence.bubbles.map(d => d.key)) || [];\n\n let viewer = context.container().select('.photoviewer');\n let selected = viewer.empty() ? undefined : viewer.datum();\n let selectedBubbleKey = selected && selected.key;\n let selectedSequenceKey = this.getSequenceKeyForBubble(selected);\n let selectedSequence = selectedSequenceKey && _ssCache.sequences[selectedSequenceKey];\n let selectedBubbleKeys = (selectedSequence && selectedSequence.bubbles.map(d => d.key)) || [];\n\n // highlight sibling viewfields on either the selected or the hovered sequences\n let highlightedBubbleKeys = utilArrayUnion(hoveredBubbleKeys, selectedBubbleKeys);\n\n context.container().selectAll('.layer-streetside-images .viewfield-group')\n .classed('highlighted', d => highlightedBubbleKeys.indexOf(d.key) !== -1)\n .classed('hovered', d => d.key === hoveredBubbleKey)\n .classed('currentView', d => d.key === selectedBubbleKey);\n\n context.container().selectAll('.layer-streetside-images .sequence')\n .classed('highlighted', d => d.properties.key === hoveredSequenceKey)\n .classed('currentView', d => d.properties.key === selectedSequenceKey);\n\n // update viewfields if needed\n context.container().selectAll('.layer-streetside-images .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n let d = this.parentNode.__data__;\n if (d.pano && d.key !== selectedBubbleKey) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n return this;\n },\n\n\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'streetside/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n\n /**\n * cache().\n */\n cache: function () {\n return _ssCache;\n }\n};\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { json as d3_json } from 'd3-fetch';\n\nimport { utilObjectOmit, utilQsString } from '../util';\nimport { localizer } from '../core/localizer';\nimport { allowUpperCaseTagValues } from '../osm/tags';\n\nimport { taginfoApiUrl } from '../../config/id.js';\n\nvar apibase = taginfoApiUrl;\nvar _inflight = {};\nvar _popularKeys = {};\n// manually exclude some additional keys \u2013 #5377, #7485, #10287, #11733\n// these will be returned by keys(), but taginfo will not be queried for values() requests\nvar _extraExcludedKeys = /^(addr:.+|postal_code|via|((int_|loc_|nat_|official_|old_|ref_|reg_|short_|full_|sorting_|alt_|artist_|long_|bridge:|tunnel:)?name(:left|:right)?(:[a-z]+)?))$/;\n\nvar _extraExcludedKeyNames = /^(hashtags?|created_by)$/;\n\nvar _taginfoCache = {};\n\nvar tag_sorts = {\n point: 'count_nodes',\n vertex: 'count_nodes',\n area: 'count_ways',\n line: 'count_ways'\n};\nvar tag_sort_members = {\n point: 'count_node_members',\n vertex: 'count_node_members',\n area: 'count_way_members',\n line: 'count_way_members',\n relation: 'count_relation_members'\n};\nvar tag_filters = {\n point: 'nodes',\n vertex: 'nodes',\n area: 'ways',\n line: 'ways'\n};\nvar tag_members_fractions = {\n point: 'count_node_members_fraction',\n vertex: 'count_node_members_fraction',\n area: 'count_way_members_fraction',\n line: 'count_way_members_fraction',\n relation: 'count_relation_members_fraction'\n};\n\n\nfunction sets(params, n, o) {\n if (params.geometry && o[params.geometry]) {\n params[n] = o[params.geometry];\n }\n return params;\n}\n\n\nfunction setFilter(params) {\n return sets(params, 'filter', tag_filters);\n}\n\n\nfunction setSort(params) {\n return sets(params, 'sortname', tag_sorts);\n}\n\n\nfunction setSortMembers(params) {\n return sets(params, 'sortname', tag_sort_members);\n}\n\n\nfunction clean(params) {\n return utilObjectOmit(params, ['geometry', 'debounce']);\n}\n\n\nfunction filterKeys(type) {\n var count_type = type ? 'count_' + type : 'count_all';\n return function(d) {\n return Number(d[count_type]) > 2500 || d.in_wiki;\n };\n}\n\n\nfunction filterMultikeys(prefix) {\n return function(d) {\n // d.key begins with prefix, and d.key contains no additional ':'s\n var re = new RegExp('^' + prefix + '(.*)$', 'i');\n var matches = d.key.match(re) || [];\n return (matches.length === 2 && matches[1].indexOf(':') === -1);\n };\n}\n\n\nfunction filterValues(allowUpperCase, key) {\n return function(d) {\n if (d.value.match(/[;,]/) !== null) return false; // exclude some punctuation\n if (!allowUpperCase &&\n !(key === 'type' && d.value === 'associatedStreet') &&\n d.value.match(/[A-Z*]/) !== null) return false; // exclude uppercase letters\n return d.count > 100; // exclude rare tags\n };\n}\n\n\nfunction filterRoles(geometry) {\n return function(d) {\n if (d.role === '') return false; // exclude empty role\n if (d.role.match(/[A-Z*;,]/) !== null) return false; // exclude uppercase letters and some punctuation\n return Number(d[tag_members_fractions[geometry]]) > 0.0;\n };\n}\n\n\nfunction valKey(d) {\n return {\n value: d.key,\n title: d.key\n };\n}\n\n\nfunction valKeyDescription(d) {\n var obj = {\n value: d.value,\n title: d.description || d.value\n };\n return obj;\n}\n\n\nfunction roleKey(d) {\n return {\n value: d.role,\n title: d.role\n };\n}\n\n\n// sort keys with ':' lower than keys without ':'\nfunction sortKeys(a, b) {\n return (a.key.indexOf(':') === -1 && b.key.indexOf(':') !== -1) ? -1\n : (a.key.indexOf(':') !== -1 && b.key.indexOf(':') === -1) ? 1\n : 0;\n}\n\n\nvar debouncedRequest = debounce(request, 300, { leading: false });\n\nfunction request(url, params, exactMatch, callback, loaded) {\n if (_inflight[url]) return;\n\n if (checkCache(url, params, exactMatch, callback)) return;\n\n var controller = new AbortController();\n _inflight[url] = controller;\n\n d3_json(url, { signal: controller.signal })\n .then(function(result) {\n delete _inflight[url];\n if (loaded) loaded(null, result);\n })\n .catch(function(err) {\n delete _inflight[url];\n if (err.name === 'AbortError') return;\n if (loaded) loaded(err.message);\n });\n}\n\n\nfunction checkCache(url, params, exactMatch, callback) {\n var rp = params.rp || 25;\n var testQuery = params.query || '';\n var testUrl = url;\n\n do {\n var hit = _taginfoCache[testUrl];\n\n // exact match, or shorter match yielding fewer than max results (rp)\n if (hit && (url === testUrl || hit.length < rp)) {\n callback(null, hit);\n return true;\n }\n\n // don't try to shorten the query\n if (exactMatch || !testQuery.length) return false;\n\n // do shorten the query to see if we already have a cached result\n // that has returned fewer than max results (rp)\n testQuery = testQuery.slice(0, -1);\n testUrl = url.replace(/&query=(.*?)&/, '&query=' + testQuery + '&');\n } while (testQuery.length >= 0);\n\n return false;\n}\n\n\nexport default {\n\n init: function() {\n _inflight = {};\n _taginfoCache = {};\n _popularKeys = [];\n\n // Fetch popular keys. We'll exclude these from `values`\n // lookups because they stress taginfo, and they aren't likely\n // to yield meaningful autocomplete results.. see #3955\n var params = {\n rp: 100,\n sortname: 'values_all',\n sortorder: 'desc',\n page: 1,\n debounce: false,\n lang: localizer.languageCode()\n };\n this.keys(params, function(err, data) {\n if (err) return;\n data.forEach(function(d) {\n if (d.value === 'opening_hours') return; // exception\n _popularKeys[d.value] = true;\n });\n });\n },\n\n\n reset: function() {\n Object.values(_inflight).forEach(function(controller) { controller.abort(); });\n _inflight = {};\n },\n\n\n keys: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(params));\n params = Object.assign({\n rp: 10,\n sortname: 'count_all',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var url = apibase + 'keys/all?' + utilQsString(params);\n doRequest(url, params, false, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n var f = filterKeys(params.filter);\n var result = d.data.filter(f).filter(d => !_extraExcludedKeyNames.test(d.key)).sort(sortKeys).map(valKey);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n multikeys: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(params));\n params = Object.assign({\n rp: 25,\n sortname: 'count_all',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var prefix = params.query;\n var url = apibase + 'keys/all?' + utilQsString(params);\n doRequest(url, params, true, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n var f = filterMultikeys(prefix);\n var result = d.data.filter(f).map(valKey);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n values: function(params, callback) {\n // Exclude popular keys from values lookups.. see #3955\n var key = params.key;\n if (key && _popularKeys[key] === true || _extraExcludedKeys.test(key)) {\n callback(null, []);\n return;\n }\n\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(setFilter(params)));\n params = Object.assign({\n rp: 25,\n sortname: 'count_all',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var url = apibase + 'key/values?' + utilQsString(params);\n doRequest(url, params, false, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n // In most cases we prefer taginfo value results with lowercase letters.\n // A few OSM keys expect values to contain uppercase values (see #3377).\n // This is not an exhaustive list (e.g. `name` also has uppercase values)\n // but these are the fields where taginfo value lookup is most useful.\n var allowUpperCase = allowUpperCaseTagValues.test(params.key);\n var f = filterValues(allowUpperCase, params.key);\n\n var result = d.data.filter(f).map(valKeyDescription);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n roles: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n var geometry = params.geometry;\n params = clean(setSortMembers(params));\n params = Object.assign({\n rp: 25,\n sortname: 'count_all_members',\n sortorder: 'desc',\n page: 1,\n lang: localizer.languageCode()\n }, params);\n\n var url = apibase + 'relation/roles?' + utilQsString(params);\n doRequest(url, params, true, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n var f = filterRoles(geometry);\n var result = d.data.filter(f).map(roleKey);\n _taginfoCache[url] = result;\n callback(null, result);\n }\n });\n },\n\n\n docs: function(params, callback) {\n var doRequest = params.debounce ? debouncedRequest : request;\n params = clean(setSort(params));\n\n var path = 'key/wiki_pages?';\n if (params.value) {\n path = 'tag/wiki_pages?';\n } else if (params.rtype) {\n path = 'relation/wiki_pages?';\n }\n\n var url = apibase + path + utilQsString(params);\n doRequest(url, params, true, callback, function(err, d) {\n if (err) {\n callback(err);\n } else {\n _taginfoCache[url] = d.data;\n callback(null, d.data);\n }\n });\n },\n\n\n apibase: function(_) {\n if (!arguments.length) return apibase;\n apibase = _;\n return this;\n }\n\n};\n", "import {\n BBox,\n Feature,\n FeatureCollection,\n Geometry,\n GeometryCollection,\n GeometryObject,\n LineString,\n MultiLineString,\n MultiPoint,\n MultiPolygon,\n Point,\n Polygon,\n Position,\n GeoJsonProperties,\n} from \"geojson\";\n\nimport { Id } from \"./lib/geojson.js\";\nexport * from \"./lib/geojson.js\";\n\n/**\n * @module helpers\n */\n\n// TurfJS Combined Types\nexport type Coord = Feature | Point | Position;\n\n/**\n * Linear measurement units.\n *\n * ⚠️ Warning. Be aware of the implications of using radian or degree units to\n * measure distance. The distance represented by a degree of longitude *varies*\n * depending on latitude.\n *\n * See https://www.thoughtco.com/degree-of-latitude-and-longitude-distance-4070616\n * for an illustration of this behaviour.\n *\n * @typedef\n */\nexport type Units =\n | \"meters\"\n | \"metres\"\n | \"millimeters\"\n | \"millimetres\"\n | \"centimeters\"\n | \"centimetres\"\n | \"kilometers\"\n | \"kilometres\"\n | \"miles\"\n | \"nauticalmiles\"\n | \"inches\"\n | \"yards\"\n | \"feet\"\n | \"radians\"\n | \"degrees\";\n\n/**\n * Area measurement units.\n *\n * @typedef\n */\nexport type AreaUnits =\n | Exclude\n | \"acres\"\n | \"hectares\";\n\n/**\n * Grid types.\n *\n * @typedef\n */\nexport type Grid = \"point\" | \"square\" | \"hex\" | \"triangle\";\n\n/**\n * Shorthand corner identifiers.\n *\n * @typedef\n */\nexport type Corners = \"sw\" | \"se\" | \"nw\" | \"ne\" | \"center\" | \"centroid\";\n\n/**\n * Geometries made up of lines i.e. lines and polygons.\n *\n * @typedef\n */\nexport type Lines = LineString | MultiLineString | Polygon | MultiPolygon;\n\n/**\n * Convenience type for all possible GeoJSON.\n *\n * @typedef\n */\nexport type AllGeoJSON =\n | Feature\n | FeatureCollection\n | Geometry\n | GeometryCollection;\n\n/**\n * The Earth radius in meters. Used by Turf modules that model the Earth as a sphere. The {@link https://en.wikipedia.org/wiki/Earth_radius#Arithmetic_mean_radius mean radius} was selected because it is {@link https://rosettacode.org/wiki/Haversine_formula#:~:text=This%20value%20is%20recommended recommended } by the Haversine formula (used by turf/distance) to reduce error.\n *\n * @constant\n */\nexport const earthRadius = 6371008.8;\n\n/**\n * Unit of measurement factors based on earthRadius.\n *\n * Keys are the name of the unit, values are the number of that unit in a single radian\n *\n * @constant\n */\nexport const factors: Record = {\n centimeters: earthRadius * 100,\n centimetres: earthRadius * 100,\n degrees: 360 / (2 * Math.PI),\n feet: earthRadius * 3.28084,\n inches: earthRadius * 39.37,\n kilometers: earthRadius / 1000,\n kilometres: earthRadius / 1000,\n meters: earthRadius,\n metres: earthRadius,\n miles: earthRadius / 1609.344,\n millimeters: earthRadius * 1000,\n millimetres: earthRadius * 1000,\n nauticalmiles: earthRadius / 1852,\n radians: 1,\n yards: earthRadius * 1.0936,\n};\n\n/**\n\n * Area of measurement factors based on 1 square meter.\n *\n * @constant\n */\nexport const areaFactors: Record = {\n acres: 0.000247105,\n centimeters: 10000,\n centimetres: 10000,\n feet: 10.763910417,\n hectares: 0.0001,\n inches: 1550.003100006,\n kilometers: 0.000001,\n kilometres: 0.000001,\n meters: 1,\n metres: 1,\n miles: 3.86e-7,\n nauticalmiles: 2.9155334959812285e-7,\n millimeters: 1000000,\n millimetres: 1000000,\n yards: 1.195990046,\n};\n\n/**\n * Wraps a GeoJSON {@link Geometry} in a GeoJSON {@link Feature}.\n *\n * @function\n * @param {GeometryObject} geometry input geometry\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a GeoJSON Feature\n * @example\n * var geometry = {\n * \"type\": \"Point\",\n * \"coordinates\": [110, 50]\n * };\n *\n * var feature = turf.feature(geometry);\n *\n * //=feature\n */\nexport function feature<\n G extends GeometryObject = Geometry,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geom: G | null,\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const feat: any = { type: \"Feature\" };\n if (options.id === 0 || options.id) {\n feat.id = options.id;\n }\n if (options.bbox) {\n feat.bbox = options.bbox;\n }\n feat.properties = properties || {};\n feat.geometry = geom;\n return feat;\n}\n\n/**\n * Creates a GeoJSON {@link Geometry} from a Geometry string type & coordinates.\n * For GeometryCollection type use `helpers.geometryCollection`\n *\n * @function\n * @param {(\"Point\" | \"LineString\" | \"Polygon\" | \"MultiPoint\" | \"MultiLineString\" | \"MultiPolygon\")} type Geometry Type\n * @param {Array} coordinates Coordinates\n * @param {Object} [options={}] Optional Parameters\n * @returns {Geometry} a GeoJSON Geometry\n * @example\n * var type = \"Point\";\n * var coordinates = [110, 50];\n * var geometry = turf.geometry(type, coordinates);\n * // => geometry\n */\nexport function geometry<\n T extends\n | \"Point\"\n | \"LineString\"\n | \"Polygon\"\n | \"MultiPoint\"\n | \"MultiLineString\"\n | \"MultiPolygon\",\n>(\n type: T,\n coordinates: any[],\n _options: Record = {}\n): Extract {\n switch (type) {\n case \"Point\":\n return point(coordinates).geometry as Extract;\n case \"LineString\":\n return lineString(coordinates).geometry as Extract;\n case \"Polygon\":\n return polygon(coordinates).geometry as Extract;\n case \"MultiPoint\":\n return multiPoint(coordinates).geometry as Extract;\n case \"MultiLineString\":\n return multiLineString(coordinates).geometry as Extract<\n Geometry,\n { type: T }\n >;\n case \"MultiPolygon\":\n return multiPolygon(coordinates).geometry as Extract<\n Geometry,\n { type: T }\n >;\n default:\n throw new Error(type + \" is invalid\");\n }\n}\n\n/**\n * Creates a {@link Point} {@link Feature} from a Position.\n *\n * @function\n * @param {Position} coordinates longitude, latitude position (each in decimal degrees)\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a Point feature\n * @example\n * var point = turf.point([-75.343, 39.984]);\n *\n * //=point\n */\nexport function point

    (\n coordinates: Position,\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n if (!coordinates) {\n throw new Error(\"coordinates is required\");\n }\n if (!Array.isArray(coordinates)) {\n throw new Error(\"coordinates must be an Array\");\n }\n if (coordinates.length < 2) {\n throw new Error(\"coordinates must be at least 2 numbers long\");\n }\n if (!isNumber(coordinates[0]) || !isNumber(coordinates[1])) {\n throw new Error(\"coordinates must contain numbers\");\n }\n\n const geom: Point = {\n type: \"Point\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Point} {@link FeatureCollection} from an Array of Point coordinates.\n *\n * @function\n * @param {Position[]} coordinates an array of Points\n * @param {GeoJsonProperties} [properties={}] Translate these properties to each Feature\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north]\n * associated with the FeatureCollection\n * @param {Id} [options.id] Identifier associated with the FeatureCollection\n * @returns {FeatureCollection} Point Feature\n * @example\n * var points = turf.points([\n * [-75, 39],\n * [-80, 45],\n * [-78, 50]\n * ]);\n *\n * //=points\n */\nexport function points

    (\n coordinates: Position[],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n return featureCollection(\n coordinates.map((coords) => {\n return point(coords, properties);\n }),\n options\n );\n}\n\n/**\n * Creates a {@link Polygon} {@link Feature} from an Array of LinearRings.\n *\n * @function\n * @param {Position[][]} coordinates an array of LinearRings\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} Polygon Feature\n * @example\n * var polygon = turf.polygon([[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]], { name: 'poly1' });\n *\n * //=polygon\n */\nexport function polygon

    (\n coordinates: Position[][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n for (const ring of coordinates) {\n if (ring.length < 4) {\n throw new Error(\n \"Each LinearRing of a Polygon must have 4 or more Positions.\"\n );\n }\n\n if (ring[ring.length - 1].length !== ring[0].length) {\n throw new Error(\"First and last Position are not equivalent.\");\n }\n\n for (let j = 0; j < ring[ring.length - 1].length; j++) {\n // Check if first point of Polygon contains two numbers\n if (ring[ring.length - 1][j] !== ring[0][j]) {\n throw new Error(\"First and last Position are not equivalent.\");\n }\n }\n }\n const geom: Polygon = {\n type: \"Polygon\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Polygon} {@link FeatureCollection} from an Array of Polygon coordinates.\n *\n * @function\n * @param {Position[][][]} coordinates an array of Polygon coordinates\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the FeatureCollection\n * @returns {FeatureCollection} Polygon FeatureCollection\n * @example\n * var polygons = turf.polygons([\n * [[[-5, 52], [-4, 56], [-2, 51], [-7, 54], [-5, 52]]],\n * [[[-15, 42], [-14, 46], [-12, 41], [-17, 44], [-15, 42]]],\n * ]);\n *\n * //=polygons\n */\nexport function polygons

    (\n coordinates: Position[][][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n return featureCollection(\n coordinates.map((coords) => {\n return polygon(coords, properties);\n }),\n options\n );\n}\n\n/**\n * Creates a {@link LineString} {@link Feature} from an Array of Positions.\n *\n * @function\n * @param {Position[]} coordinates an array of Positions\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} LineString Feature\n * @example\n * var linestring1 = turf.lineString([[-24, 63], [-23, 60], [-25, 65], [-20, 69]], {name: 'line 1'});\n * var linestring2 = turf.lineString([[-14, 43], [-13, 40], [-15, 45], [-10, 49]], {name: 'line 2'});\n *\n * //=linestring1\n * //=linestring2\n */\nexport function lineString

    (\n coordinates: Position[],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n if (coordinates.length < 2) {\n throw new Error(\"coordinates must be an array of two or more positions\");\n }\n const geom: LineString = {\n type: \"LineString\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link LineString} {@link FeatureCollection} from an Array of LineString coordinates.\n *\n * @function\n * @param {Position[][]} coordinates an array of LinearRings\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north]\n * associated with the FeatureCollection\n * @param {Id} [options.id] Identifier associated with the FeatureCollection\n * @returns {FeatureCollection} LineString FeatureCollection\n * @example\n * var linestrings = turf.lineStrings([\n * [[-24, 63], [-23, 60], [-25, 65], [-20, 69]],\n * [[-14, 43], [-13, 40], [-15, 45], [-10, 49]]\n * ]);\n *\n * //=linestrings\n */\nexport function lineStrings

    (\n coordinates: Position[][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n return featureCollection(\n coordinates.map((coords) => {\n return lineString(coords, properties);\n }),\n options\n );\n}\n\n/**\n * Takes one or more {@link Feature|Features} and creates a {@link FeatureCollection}.\n *\n * @function\n * @param {Array>} features input features\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {FeatureCollection} FeatureCollection of Features\n * @example\n * var locationA = turf.point([-75.343, 39.984], {name: 'Location A'});\n * var locationB = turf.point([-75.833, 39.284], {name: 'Location B'});\n * var locationC = turf.point([-75.534, 39.123], {name: 'Location C'});\n *\n * var collection = turf.featureCollection([\n * locationA,\n * locationB,\n * locationC\n * ]);\n *\n * //=collection\n */\nexport function featureCollection<\n G extends GeometryObject = Geometry,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n features: Array>,\n options: { bbox?: BBox; id?: Id } = {}\n): FeatureCollection {\n const fc: any = { type: \"FeatureCollection\" };\n if (options.id) {\n fc.id = options.id;\n }\n if (options.bbox) {\n fc.bbox = options.bbox;\n }\n fc.features = features;\n return fc;\n}\n\n/**\n * Creates a {@link Feature}<{@link MultiLineString}> based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Position[][]} coordinates an array of LineStrings\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a MultiLineString feature\n * @throws {Error} if no coordinates are passed\n * @example\n * var multiLine = turf.multiLineString([[[0,0],[10,10]]]);\n *\n * //=multiLine\n */\nexport function multiLineString<\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n coordinates: Position[][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const geom: MultiLineString = {\n type: \"MultiLineString\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Feature}<{@link MultiPoint}> based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Position[]} coordinates an array of Positions\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a MultiPoint feature\n * @throws {Error} if no coordinates are passed\n * @example\n * var multiPt = turf.multiPoint([[0,0],[10,10]]);\n *\n * //=multiPt\n */\nexport function multiPoint

    (\n coordinates: Position[],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const geom: MultiPoint = {\n type: \"MultiPoint\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a {@link Feature}<{@link MultiPolygon}> based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Position[][][]} coordinates an array of Polygons\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a multipolygon feature\n * @throws {Error} if no coordinates are passed\n * @example\n * var multiPoly = turf.multiPolygon([[[[0,0],[0,10],[10,10],[10,0],[0,0]]]]);\n *\n * //=multiPoly\n *\n */\nexport function multiPolygon

    (\n coordinates: Position[][][],\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature {\n const geom: MultiPolygon = {\n type: \"MultiPolygon\",\n coordinates,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Creates a Feature based on a\n * coordinate array. Properties can be added optionally.\n *\n * @function\n * @param {Array} geometries an array of GeoJSON Geometries\n * @param {GeoJsonProperties} [properties={}] an Object of key-value pairs to add as properties\n * @param {Object} [options={}] Optional Parameters\n * @param {BBox} [options.bbox] Bounding Box Array [west, south, east, north] associated with the Feature\n * @param {Id} [options.id] Identifier associated with the Feature\n * @returns {Feature} a GeoJSON GeometryCollection Feature\n * @example\n * var pt = turf.geometry(\"Point\", [100, 0]);\n * var line = turf.geometry(\"LineString\", [[101, 0], [102, 1]]);\n * var collection = turf.geometryCollection([pt, line]);\n *\n * // => collection\n */\nexport function geometryCollection<\n G extends\n | Point\n | LineString\n | Polygon\n | MultiPoint\n | MultiLineString\n | MultiPolygon,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geometries: Array,\n properties?: P,\n options: { bbox?: BBox; id?: Id } = {}\n): Feature, P> {\n const geom: GeometryCollection = {\n type: \"GeometryCollection\",\n geometries,\n };\n return feature(geom, properties, options);\n}\n\n/**\n * Round number to precision\n *\n * @function\n * @param {number} num Number\n * @param {number} [precision=0] Precision\n * @returns {number} rounded number\n * @example\n * turf.round(120.4321)\n * //=120\n *\n * turf.round(120.4321, 2)\n * //=120.43\n */\nexport function round(num: number, precision = 0): number {\n if (precision && !(precision >= 0)) {\n throw new Error(\"precision must be a positive number\");\n }\n const multiplier = Math.pow(10, precision || 0);\n return Math.round(num * multiplier) / multiplier;\n}\n\n/**\n * Convert a distance measurement (assuming a spherical Earth) from radians to a more friendly unit.\n * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet\n *\n * @function\n * @param {number} radians in radians across the sphere\n * @param {Units} [units=\"kilometers\"] can be degrees, radians, miles, inches, yards, metres,\n * meters, kilometres, kilometers.\n * @returns {number} distance\n */\nexport function radiansToLength(\n radians: number,\n units: Units = \"kilometers\"\n): number {\n const factor = factors[units];\n if (!factor) {\n throw new Error(units + \" units is invalid\");\n }\n return radians * factor;\n}\n\n/**\n * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into radians\n * Valid units: miles, nauticalmiles, inches, yards, meters, metres, kilometers, centimeters, feet\n *\n * @function\n * @param {number} distance in real units\n * @param {Units} [units=\"kilometers\"] can be degrees, radians, miles, inches, yards, metres,\n * meters, kilometres, kilometers.\n * @returns {number} radians\n */\nexport function lengthToRadians(\n distance: number,\n units: Units = \"kilometers\"\n): number {\n const factor = factors[units];\n if (!factor) {\n throw new Error(units + \" units is invalid\");\n }\n return distance / factor;\n}\n\n/**\n * Convert a distance measurement (assuming a spherical Earth) from a real-world unit into degrees\n * Valid units: miles, nauticalmiles, inches, yards, meters, metres, centimeters, kilometres, feet\n *\n * @function\n * @param {number} distance in real units\n * @param {Units} [units=\"kilometers\"] can be degrees, radians, miles, inches, yards, metres,\n * meters, kilometres, kilometers.\n * @returns {number} degrees\n */\nexport function lengthToDegrees(distance: number, units?: Units): number {\n return radiansToDegrees(lengthToRadians(distance, units));\n}\n\n/**\n * Converts any bearing angle from the north line direction (positive clockwise)\n * and returns an angle between 0-360 degrees (positive clockwise), 0 being the north line\n *\n * @function\n * @param {number} bearing angle, between -180 and +180 degrees\n * @returns {number} angle between 0 and 360 degrees\n */\nexport function bearingToAzimuth(bearing: number): number {\n let angle = bearing % 360;\n if (angle < 0) {\n angle += 360;\n }\n return angle;\n}\n\n/**\n * Converts any azimuth angle from the north line direction (positive clockwise)\n * and returns an angle between -180 and +180 degrees (positive clockwise), 0 being the north line\n *\n * @function\n * @param {number} angle between 0 and 360 degrees\n * @returns {number} bearing between -180 and +180 degrees\n */\nexport function azimuthToBearing(angle: number): number {\n // Ignore full revolutions (multiples of 360)\n angle = angle % 360;\n\n if (angle > 180) {\n return angle - 360;\n } else if (angle < -180) {\n return angle + 360;\n }\n\n return angle;\n}\n\n/**\n * Converts an angle in radians to degrees\n *\n * @function\n * @param {number} radians angle in radians\n * @returns {number} degrees between 0 and 360 degrees\n */\nexport function radiansToDegrees(radians: number): number {\n // % (2 * Math.PI) radians in case someone passes value > 2π\n const normalisedRadians = radians % (2 * Math.PI);\n return (normalisedRadians * 180) / Math.PI;\n}\n\n/**\n * Converts an angle in degrees to radians\n *\n * @function\n * @param {number} degrees angle between 0 and 360 degrees\n * @returns {number} angle in radians\n */\nexport function degreesToRadians(degrees: number): number {\n // % 360 degrees in case someone passes value > 360\n const normalisedDegrees = degrees % 360;\n return (normalisedDegrees * Math.PI) / 180;\n}\n\n/**\n * Converts a length from one unit to another.\n *\n * @function\n * @param {number} length Length to be converted\n * @param {Units} [originalUnit=\"kilometers\"] Input length unit\n * @param {Units} [finalUnit=\"kilometers\"] Returned length unit\n * @returns {number} The converted length\n */\nexport function convertLength(\n length: number,\n originalUnit: Units = \"kilometers\",\n finalUnit: Units = \"kilometers\"\n): number {\n if (!(length >= 0)) {\n throw new Error(\"length must be a positive number\");\n }\n return radiansToLength(lengthToRadians(length, originalUnit), finalUnit);\n}\n\n/**\n * Converts an area from one unit to another.\n *\n * @function\n * @param {number} area Area to be converted\n * @param {AreaUnits} [originalUnit=\"meters\"] Input area unit\n * @param {AreaUnits} [finalUnit=\"kilometers\"] Returned area unit\n * @returns {number} The converted length\n */\nexport function convertArea(\n area: number,\n originalUnit: AreaUnits = \"meters\",\n finalUnit: AreaUnits = \"kilometers\"\n): number {\n if (!(area >= 0)) {\n throw new Error(\"area must be a positive number\");\n }\n\n const startFactor = areaFactors[originalUnit];\n if (!startFactor) {\n throw new Error(\"invalid original units\");\n }\n\n const finalFactor = areaFactors[finalUnit];\n if (!finalFactor) {\n throw new Error(\"invalid final units\");\n }\n\n return (area / startFactor) * finalFactor;\n}\n\n/**\n * isNumber\n *\n * @function\n * @param {any} num Number to validate\n * @returns {boolean} true/false\n * @example\n * turf.isNumber(123)\n * //=true\n * turf.isNumber('foo')\n * //=false\n */\nexport function isNumber(num: any): boolean {\n return !isNaN(num) && num !== null && !Array.isArray(num);\n}\n\n/**\n * isObject\n *\n * @function\n * @param {any} input variable to validate\n * @returns {boolean} true/false, including false for Arrays and Functions\n * @example\n * turf.isObject({elevation: 10})\n * //=true\n * turf.isObject('foo')\n * //=false\n */\nexport function isObject(input: any): boolean {\n return input !== null && typeof input === \"object\" && !Array.isArray(input);\n}\n\n/**\n * Validate BBox\n *\n * @private\n * @param {any} bbox BBox to validate\n * @returns {void}\n * @throws {Error} if BBox is not valid\n * @example\n * validateBBox([-180, -40, 110, 50])\n * //=OK\n * validateBBox([-180, -40])\n * //=Error\n * validateBBox('Foo')\n * //=Error\n * validateBBox(5)\n * //=Error\n * validateBBox(null)\n * //=Error\n * validateBBox(undefined)\n * //=Error\n */\nexport function validateBBox(bbox: any): void {\n if (!bbox) {\n throw new Error(\"bbox is required\");\n }\n if (!Array.isArray(bbox)) {\n throw new Error(\"bbox must be an Array\");\n }\n if (bbox.length !== 4 && bbox.length !== 6) {\n throw new Error(\"bbox must be an Array of 4 or 6 numbers\");\n }\n bbox.forEach((num) => {\n if (!isNumber(num)) {\n throw new Error(\"bbox must only contain numbers\");\n }\n });\n}\n\n/**\n * Validate Id\n *\n * @private\n * @param {any} id Id to validate\n * @returns {void}\n * @throws {Error} if Id is not valid\n * @example\n * validateId([-180, -40, 110, 50])\n * //=Error\n * validateId([-180, -40])\n * //=Error\n * validateId('Foo')\n * //=OK\n * validateId(5)\n * //=OK\n * validateId(null)\n * //=Error\n * validateId(undefined)\n * //=Error\n */\nexport function validateId(id: any): void {\n if (!id) {\n throw new Error(\"id is required\");\n }\n if ([\"string\", \"number\"].indexOf(typeof id) === -1) {\n throw new Error(\"id must be a number or a string\");\n }\n}\n", "import {\n Feature,\n FeatureCollection,\n Geometry,\n LineString,\n MultiPoint,\n MultiLineString,\n MultiPolygon,\n Point,\n Polygon,\n} from \"geojson\";\nimport { isNumber } from \"@turf/helpers\";\n\n/**\n * Unwrap a coordinate from a Point Feature, Geometry or a single coordinate.\n *\n * @function\n * @param {Array|Geometry|Feature} coord GeoJSON Point or an Array of numbers\n * @returns {Array} coordinates\n * @example\n * var pt = turf.point([10, 10]);\n *\n * var coord = turf.getCoord(pt);\n * //= [10, 10]\n */\nfunction getCoord(coord: Feature | Point | number[]): number[] {\n if (!coord) {\n throw new Error(\"coord is required\");\n }\n\n if (!Array.isArray(coord)) {\n if (\n coord.type === \"Feature\" &&\n coord.geometry !== null &&\n coord.geometry.type === \"Point\"\n ) {\n return [...coord.geometry.coordinates];\n }\n if (coord.type === \"Point\") {\n return [...coord.coordinates];\n }\n }\n if (\n Array.isArray(coord) &&\n coord.length >= 2 &&\n !Array.isArray(coord[0]) &&\n !Array.isArray(coord[1])\n ) {\n return [...coord];\n }\n\n throw new Error(\"coord must be GeoJSON Point or an Array of numbers\");\n}\n\n/**\n * Unwrap coordinates from a Feature, Geometry Object or an Array\n *\n * @function\n * @param {Array|Geometry|Feature} coords Feature, Geometry Object or an Array\n * @returns {Array} coordinates\n * @example\n * var poly = turf.polygon([[[119.32, -8.7], [119.55, -8.69], [119.51, -8.54], [119.32, -8.7]]]);\n *\n * var coords = turf.getCoords(poly);\n * //= [[[119.32, -8.7], [119.55, -8.69], [119.51, -8.54], [119.32, -8.7]]]\n */\nfunction getCoords<\n G extends\n | Point\n | LineString\n | Polygon\n | MultiPoint\n | MultiLineString\n | MultiPolygon,\n>(coords: any[] | Feature | G): any[] {\n if (Array.isArray(coords)) {\n return coords;\n }\n\n // Feature\n if (coords.type === \"Feature\") {\n if (coords.geometry !== null) {\n return coords.geometry.coordinates;\n }\n } else {\n // Geometry\n if (coords.coordinates) {\n return coords.coordinates;\n }\n }\n\n throw new Error(\n \"coords must be GeoJSON Feature, Geometry Object or an Array\"\n );\n}\n\n/**\n * Checks if coordinates contains a number\n *\n * @function\n * @param {Array} coordinates GeoJSON Coordinates\n * @returns {boolean} true if Array contains a number\n */\nfunction containsNumber(coordinates: any[]): boolean {\n if (\n coordinates.length > 1 &&\n isNumber(coordinates[0]) &&\n isNumber(coordinates[1])\n ) {\n return true;\n }\n\n if (Array.isArray(coordinates[0]) && coordinates[0].length) {\n return containsNumber(coordinates[0]);\n }\n throw new Error(\"coordinates must only contain numbers\");\n}\n\n/**\n * Enforce expectations about types of GeoJSON objects for Turf.\n *\n * @function\n * @param {GeoJSON} value any GeoJSON object\n * @param {string} type expected GeoJSON type\n * @param {string} name name of calling function\n * @throws {Error} if value is not the expected type.\n */\nfunction geojsonType(value: any, type: string, name: string): void {\n if (!type || !name) {\n throw new Error(\"type and name required\");\n }\n\n if (!value || value.type !== type) {\n throw new Error(\n \"Invalid input to \" +\n name +\n \": must be a \" +\n type +\n \", given \" +\n value.type\n );\n }\n}\n\n/**\n * Enforce expectations about types of {@link Feature} inputs for Turf.\n * Internally this uses {@link geojsonType} to judge geometry types.\n *\n * @function\n * @param {Feature} feature a feature with an expected geometry type\n * @param {string} type expected GeoJSON type\n * @param {string} name name of calling function\n * @throws {Error} error if value is not the expected type.\n */\nfunction featureOf(feature: Feature, type: string, name: string): void {\n if (!feature) {\n throw new Error(\"No feature passed\");\n }\n if (!name) {\n throw new Error(\".featureOf() requires a name\");\n }\n if (!feature || feature.type !== \"Feature\" || !feature.geometry) {\n throw new Error(\n \"Invalid input to \" + name + \", Feature with geometry required\"\n );\n }\n if (!feature.geometry || feature.geometry.type !== type) {\n throw new Error(\n \"Invalid input to \" +\n name +\n \": must be a \" +\n type +\n \", given \" +\n feature.geometry.type\n );\n }\n}\n\n/**\n * Enforce expectations about types of {@link FeatureCollection} inputs for Turf.\n * Internally this uses {@link geojsonType} to judge geometry types.\n *\n * @function\n * @param {FeatureCollection} featureCollection a FeatureCollection for which features will be judged\n * @param {string} type expected GeoJSON type\n * @param {string} name name of calling function\n * @throws {Error} if value is not the expected type.\n */\nfunction collectionOf(\n featureCollection: FeatureCollection,\n type: string,\n name: string\n) {\n if (!featureCollection) {\n throw new Error(\"No featureCollection passed\");\n }\n if (!name) {\n throw new Error(\".collectionOf() requires a name\");\n }\n if (!featureCollection || featureCollection.type !== \"FeatureCollection\") {\n throw new Error(\n \"Invalid input to \" + name + \", FeatureCollection required\"\n );\n }\n for (const feature of featureCollection.features) {\n if (!feature || feature.type !== \"Feature\" || !feature.geometry) {\n throw new Error(\n \"Invalid input to \" + name + \", Feature with geometry required\"\n );\n }\n if (!feature.geometry || feature.geometry.type !== type) {\n throw new Error(\n \"Invalid input to \" +\n name +\n \": must be a \" +\n type +\n \", given \" +\n feature.geometry.type\n );\n }\n }\n}\n\n/**\n * Get Geometry from Feature or Geometry Object\n *\n * @param {Feature|Geometry} geojson GeoJSON Feature or Geometry Object\n * @returns {Geometry|null} GeoJSON Geometry Object\n * @throws {Error} if geojson is not a Feature or Geometry Object\n * @example\n * var point = {\n * \"type\": \"Feature\",\n * \"properties\": {},\n * \"geometry\": {\n * \"type\": \"Point\",\n * \"coordinates\": [110, 40]\n * }\n * }\n * var geom = turf.getGeom(point)\n * //={\"type\": \"Point\", \"coordinates\": [110, 40]}\n */\nfunction getGeom(geojson: Feature | G): G {\n if (geojson.type === \"Feature\") {\n return geojson.geometry;\n }\n return geojson;\n}\n\n/**\n * Get GeoJSON object's type, Geometry type is prioritize.\n *\n * @param {GeoJSON} geojson GeoJSON object\n * @param {string} [name=\"geojson\"] name of the variable to display in error message (unused)\n * @returns {string} GeoJSON type\n * @example\n * var point = {\n * \"type\": \"Feature\",\n * \"properties\": {},\n * \"geometry\": {\n * \"type\": \"Point\",\n * \"coordinates\": [110, 40]\n * }\n * }\n * var geom = turf.getType(point)\n * //=\"Point\"\n */\nfunction getType(\n geojson: Feature | FeatureCollection | Geometry,\n _name?: string\n): string {\n if (geojson.type === \"FeatureCollection\") {\n return \"FeatureCollection\";\n }\n if (geojson.type === \"GeometryCollection\") {\n return \"GeometryCollection\";\n }\n if (geojson.type === \"Feature\" && geojson.geometry !== null) {\n return geojson.geometry.type;\n }\n return geojson.type;\n}\n\nexport {\n getCoord,\n getCoords,\n containsNumber,\n geojsonType,\n featureOf,\n collectionOf,\n getGeom,\n getType,\n};\n// No default export!\n", "import {\n BBox,\n Feature,\n LineString,\n MultiLineString,\n MultiPolygon,\n GeoJsonProperties,\n Polygon,\n} from \"geojson\";\n\nimport {\n lineString,\n multiLineString,\n multiPolygon,\n polygon,\n} from \"@turf/helpers\";\nimport { getGeom } from \"@turf/invariant\";\nimport { lineclip, polygonclip } from \"./lib/lineclip.js\";\n\n/**\n * Takes a {@link Feature} and a bbox and clips the feature to the bbox using\n * [lineclip](https://github.com/mapbox/lineclip).\n * May result in degenerate edges when clipping Polygons.\n *\n * @function\n * @param {Feature} feature feature to clip to the bbox\n * @param {BBox} bbox extent in [minX, minY, maxX, maxY] order\n * @returns {Feature} clipped Feature\n * @example\n * var bbox = [0, 0, 10, 10];\n * var poly = turf.polygon([[[2, 2], [8, 4], [12, 8], [3, 7], [2, 2]]]);\n *\n * var clipped = turf.bboxClip(poly, bbox);\n *\n * //addToMap\n * var addToMap = [bbox, poly, clipped]\n */\nfunction bboxClip<\n G extends Polygon | MultiPolygon | LineString | MultiLineString,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(feature: Feature | G, bbox: BBox) {\n const geom = getGeom(feature);\n const type = geom.type;\n const properties = feature.type === \"Feature\" ? feature.properties : {};\n let coords: any[] = geom.coordinates;\n\n switch (type) {\n case \"LineString\":\n case \"MultiLineString\": {\n const lines: any[] = [];\n if (type === \"LineString\") {\n coords = [coords];\n }\n coords.forEach((line) => {\n lineclip(line, bbox, lines);\n });\n if (lines.length === 1) {\n return lineString(lines[0], properties);\n }\n return multiLineString(lines, properties);\n }\n case \"Polygon\":\n return polygon(clipPolygon(coords, bbox), properties);\n case \"MultiPolygon\":\n return multiPolygon(\n coords.map((poly) => {\n return clipPolygon(poly, bbox);\n }),\n properties\n );\n default:\n throw new Error(\"geometry \" + type + \" not supported\");\n }\n}\n\nfunction clipPolygon(rings: number[][][], bbox: BBox) {\n const outRings = [];\n for (const ring of rings) {\n const clipped = polygonclip(ring, bbox);\n if (clipped.length > 0) {\n if (\n clipped[0][0] !== clipped[clipped.length - 1][0] ||\n clipped[0][1] !== clipped[clipped.length - 1][1]\n ) {\n clipped.push(clipped[0]);\n }\n if (clipped.length >= 4) {\n outRings.push(clipped);\n }\n }\n }\n return outRings;\n}\n\nexport { bboxClip };\nexport default bboxClip;\n", "// Cohen-Sutherland line clipping algorithm, adapted to efficiently\n// handle polylines rather than just segments\nimport { BBox } from \"geojson\";\n\nexport function lineclip(\n points: number[][],\n bbox: BBox,\n result?: number[][][]\n): number[][][] {\n var len = points.length,\n codeA = bitCode(points[0], bbox),\n part = [] as number[][],\n i,\n codeB,\n lastCode;\n let a: number[];\n let b: number[];\n\n if (!result) result = [];\n\n for (i = 1; i < len; i++) {\n a = points[i - 1];\n b = points[i];\n codeB = lastCode = bitCode(b, bbox);\n\n while (true) {\n if (!(codeA | codeB)) {\n // accept\n part.push(a);\n\n if (codeB !== lastCode) {\n // segment went outside\n part.push(b);\n\n if (i < len - 1) {\n // start a new line\n result.push(part);\n part = [];\n }\n } else if (i === len - 1) {\n part.push(b);\n }\n break;\n } else if (codeA & codeB) {\n // trivial reject\n break;\n } else if (codeA) {\n // a outside, intersect with clip edge\n a = intersect(a, b, codeA, bbox)!;\n codeA = bitCode(a, bbox);\n } else {\n // b outside\n b = intersect(a, b, codeB, bbox)!;\n codeB = bitCode(b, bbox);\n }\n }\n\n codeA = lastCode;\n }\n\n if (part.length) result.push(part);\n\n return result;\n}\n\n// Sutherland-Hodgeman polygon clipping algorithm\n\nexport function polygonclip(points: number[][], bbox: BBox): number[][] {\n var result: number[][], edge, prev, prevInside, i, p, inside;\n\n // clip against each side of the clip rectangle\n for (edge = 1; edge <= 8; edge *= 2) {\n result = [];\n prev = points[points.length - 1];\n prevInside = !(bitCode(prev, bbox) & edge);\n\n for (i = 0; i < points.length; i++) {\n p = points[i];\n inside = !(bitCode(p, bbox) & edge);\n\n // if segment goes through the clip window, add an intersection\n if (inside !== prevInside) result.push(intersect(prev, p, edge, bbox)!);\n\n if (inside) result.push(p); // add a point if it's inside\n\n prev = p;\n prevInside = inside;\n }\n\n points = result;\n\n if (!points.length) break;\n }\n\n return result!;\n}\n\n// intersect a segment against one of the 4 lines that make up the bbox\n\nfunction intersect(\n a: number[],\n b: number[],\n edge: number,\n bbox: BBox\n): number[] | null {\n return edge & 8\n ? [a[0] + ((b[0] - a[0]) * (bbox[3] - a[1])) / (b[1] - a[1]), bbox[3]] // top\n : edge & 4\n ? [a[0] + ((b[0] - a[0]) * (bbox[1] - a[1])) / (b[1] - a[1]), bbox[1]] // bottom\n : edge & 2\n ? [bbox[2], a[1] + ((b[1] - a[1]) * (bbox[2] - a[0])) / (b[0] - a[0])] // right\n : edge & 1\n ? [bbox[0], a[1] + ((b[1] - a[1]) * (bbox[0] - a[0])) / (b[0] - a[0])] // left\n : null;\n}\n\n// bit code reflects the point position relative to the bbox:\n\n// left mid right\n// top 1001 1000 1010\n// mid 0001 0000 0010\n// bottom 0101 0100 0110\n\nfunction bitCode(p: number[], bbox: BBox) {\n var code = 0;\n\n if (p[0] < bbox[0]) code |= 1;\n // left\n else if (p[0] > bbox[2]) code |= 2; // right\n\n if (p[1] < bbox[1]) code |= 4;\n // bottom\n else if (p[1] > bbox[3]) code |= 8; // top\n\n return code;\n}\n", "'use strict';\n\nmodule.exports = function (data, opts) {\n if (!opts) opts = {};\n if (typeof opts === 'function') opts = { cmp: opts };\n var cycles = (typeof opts.cycles === 'boolean') ? opts.cycles : false;\n\n var cmp = opts.cmp && (function (f) {\n return function (node) {\n return function (a, b) {\n var aobj = { key: a, value: node[a] };\n var bobj = { key: b, value: node[b] };\n return f(aobj, bobj);\n };\n };\n })(opts.cmp);\n\n var seen = [];\n return (function stringify (node) {\n if (node && node.toJSON && typeof node.toJSON === 'function') {\n node = node.toJSON();\n }\n\n if (node === undefined) return;\n if (typeof node == 'number') return isFinite(node) ? '' + node : 'null';\n if (typeof node !== 'object') return JSON.stringify(node);\n\n var i, out;\n if (Array.isArray(node)) {\n out = '[';\n for (i = 0; i < node.length; i++) {\n if (i) out += ',';\n out += stringify(node[i]) || 'null';\n }\n return out + ']';\n }\n\n if (node === null) return 'null';\n\n if (seen.indexOf(node) !== -1) {\n if (cycles) return JSON.stringify('__cycle__');\n throw new TypeError('Converting circular structure to JSON');\n }\n\n var seenIndex = seen.push(node) - 1;\n var keys = Object.keys(node).sort(cmp && cmp(node));\n out = '';\n for (i = 0; i < keys.length; i++) {\n var key = keys[i];\n var value = stringify(node[key]);\n\n if (!value) continue;\n if (out) out += ',';\n out += JSON.stringify(key) + ':' + value;\n }\n seen.splice(seenIndex, 1);\n return '{' + out + '}';\n })(data);\n};\n", "(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.polygonClipping = factory());\n})(this, (function () { 'use strict';\n\n /**\n * splaytree v3.1.2\n * Fast Splay tree for Node and browser\n *\n * @author Alexander Milevski \n * @license MIT\n * @preserve\n */\n\n /*! *****************************************************************************\r\n Copyright (c) Microsoft Corporation. All rights reserved.\r\n Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use\r\n this file except in compliance with the License. You may obtain a copy of the\r\n License at http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r\n KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED\r\n WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,\r\n MERCHANTABLITY OR NON-INFRINGEMENT.\r\n\r\n See the Apache Version 2.0 License for specific language governing permissions\r\n and limitations under the License.\r\n ***************************************************************************** */\n\n function __generator(thisArg, body) {\n var _ = {\n label: 0,\n sent: function () {\n if (t[0] & 1) throw t[1];\n return t[1];\n },\n trys: [],\n ops: []\n },\n f,\n y,\n t,\n g;\n return g = {\n next: verb(0),\n \"throw\": verb(1),\n \"return\": verb(2)\n }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function () {\n return this;\n }), g;\n function verb(n) {\n return function (v) {\n return step([n, v]);\n };\n }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (_) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0:\n case 1:\n t = op;\n break;\n case 4:\n _.label++;\n return {\n value: op[1],\n done: false\n };\n case 5:\n _.label++;\n y = op[1];\n op = [0];\n continue;\n case 7:\n op = _.ops.pop();\n _.trys.pop();\n continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {\n _ = 0;\n continue;\n }\n if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {\n _.label = op[1];\n break;\n }\n if (op[0] === 6 && _.label < t[1]) {\n _.label = t[1];\n t = op;\n break;\n }\n if (t && _.label < t[2]) {\n _.label = t[2];\n _.ops.push(op);\n break;\n }\n if (t[2]) _.ops.pop();\n _.trys.pop();\n continue;\n }\n op = body.call(thisArg, _);\n } catch (e) {\n op = [6, e];\n y = 0;\n } finally {\n f = t = 0;\n }\n if (op[0] & 5) throw op[1];\n return {\n value: op[0] ? op[1] : void 0,\n done: true\n };\n }\n }\n var Node = /** @class */function () {\n function Node(key, data) {\n this.next = null;\n this.key = key;\n this.data = data;\n this.left = null;\n this.right = null;\n }\n return Node;\n }();\n\n /* follows \"An implementation of top-down splaying\"\r\n * by D. Sleator March 1992\r\n */\n function DEFAULT_COMPARE(a, b) {\n return a > b ? 1 : a < b ? -1 : 0;\n }\n /**\r\n * Simple top down splay, not requiring i to be in the tree t.\r\n */\n function splay(i, t, comparator) {\n var N = new Node(null, null);\n var l = N;\n var r = N;\n while (true) {\n var cmp = comparator(i, t.key);\n //if (i < t.key) {\n if (cmp < 0) {\n if (t.left === null) break;\n //if (i < t.left.key) {\n if (comparator(i, t.left.key) < 0) {\n var y = t.left; /* rotate right */\n t.left = y.right;\n y.right = t;\n t = y;\n if (t.left === null) break;\n }\n r.left = t; /* link right */\n r = t;\n t = t.left;\n //} else if (i > t.key) {\n } else if (cmp > 0) {\n if (t.right === null) break;\n //if (i > t.right.key) {\n if (comparator(i, t.right.key) > 0) {\n var y = t.right; /* rotate left */\n t.right = y.left;\n y.left = t;\n t = y;\n if (t.right === null) break;\n }\n l.right = t; /* link left */\n l = t;\n t = t.right;\n } else break;\n }\n /* assemble */\n l.right = t.left;\n r.left = t.right;\n t.left = N.right;\n t.right = N.left;\n return t;\n }\n function insert(i, data, t, comparator) {\n var node = new Node(i, data);\n if (t === null) {\n node.left = node.right = null;\n return node;\n }\n t = splay(i, t, comparator);\n var cmp = comparator(i, t.key);\n if (cmp < 0) {\n node.left = t.left;\n node.right = t;\n t.left = null;\n } else if (cmp >= 0) {\n node.right = t.right;\n node.left = t;\n t.right = null;\n }\n return node;\n }\n function split(key, v, comparator) {\n var left = null;\n var right = null;\n if (v) {\n v = splay(key, v, comparator);\n var cmp = comparator(v.key, key);\n if (cmp === 0) {\n left = v.left;\n right = v.right;\n } else if (cmp < 0) {\n right = v.right;\n v.right = null;\n left = v;\n } else {\n left = v.left;\n v.left = null;\n right = v;\n }\n }\n return {\n left: left,\n right: right\n };\n }\n function merge(left, right, comparator) {\n if (right === null) return left;\n if (left === null) return right;\n right = splay(left.key, right, comparator);\n right.left = left;\n return right;\n }\n /**\r\n * Prints level of the tree\r\n */\n function printRow(root, prefix, isTail, out, printNode) {\n if (root) {\n out(\"\" + prefix + (isTail ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ') + printNode(root) + \"\\n\");\n var indent = prefix + (isTail ? ' ' : '\u2502 ');\n if (root.left) printRow(root.left, indent, false, out, printNode);\n if (root.right) printRow(root.right, indent, true, out, printNode);\n }\n }\n var Tree = /** @class */function () {\n function Tree(comparator) {\n if (comparator === void 0) {\n comparator = DEFAULT_COMPARE;\n }\n this._root = null;\n this._size = 0;\n this._comparator = comparator;\n }\n /**\r\n * Inserts a key, allows duplicates\r\n */\n Tree.prototype.insert = function (key, data) {\n this._size++;\n return this._root = insert(key, data, this._root, this._comparator);\n };\n /**\r\n * Adds a key, if it is not present in the tree\r\n */\n Tree.prototype.add = function (key, data) {\n var node = new Node(key, data);\n if (this._root === null) {\n node.left = node.right = null;\n this._size++;\n this._root = node;\n }\n var comparator = this._comparator;\n var t = splay(key, this._root, comparator);\n var cmp = comparator(key, t.key);\n if (cmp === 0) this._root = t;else {\n if (cmp < 0) {\n node.left = t.left;\n node.right = t;\n t.left = null;\n } else if (cmp > 0) {\n node.right = t.right;\n node.left = t;\n t.right = null;\n }\n this._size++;\n this._root = node;\n }\n return this._root;\n };\n /**\r\n * @param {Key} key\r\n * @return {Node|null}\r\n */\n Tree.prototype.remove = function (key) {\n this._root = this._remove(key, this._root, this._comparator);\n };\n /**\r\n * Deletes i from the tree if it's there\r\n */\n Tree.prototype._remove = function (i, t, comparator) {\n var x;\n if (t === null) return null;\n t = splay(i, t, comparator);\n var cmp = comparator(i, t.key);\n if (cmp === 0) {\n /* found it */\n if (t.left === null) {\n x = t.right;\n } else {\n x = splay(i, t.left, comparator);\n x.right = t.right;\n }\n this._size--;\n return x;\n }\n return t; /* It wasn't there */\n };\n /**\r\n * Removes and returns the node with smallest key\r\n */\n Tree.prototype.pop = function () {\n var node = this._root;\n if (node) {\n while (node.left) node = node.left;\n this._root = splay(node.key, this._root, this._comparator);\n this._root = this._remove(node.key, this._root, this._comparator);\n return {\n key: node.key,\n data: node.data\n };\n }\n return null;\n };\n /**\r\n * Find without splaying\r\n */\n Tree.prototype.findStatic = function (key) {\n var current = this._root;\n var compare = this._comparator;\n while (current) {\n var cmp = compare(key, current.key);\n if (cmp === 0) return current;else if (cmp < 0) current = current.left;else current = current.right;\n }\n return null;\n };\n Tree.prototype.find = function (key) {\n if (this._root) {\n this._root = splay(key, this._root, this._comparator);\n if (this._comparator(key, this._root.key) !== 0) return null;\n }\n return this._root;\n };\n Tree.prototype.contains = function (key) {\n var current = this._root;\n var compare = this._comparator;\n while (current) {\n var cmp = compare(key, current.key);\n if (cmp === 0) return true;else if (cmp < 0) current = current.left;else current = current.right;\n }\n return false;\n };\n Tree.prototype.forEach = function (visitor, ctx) {\n var current = this._root;\n var Q = []; /* Initialize stack s */\n var done = false;\n while (!done) {\n if (current !== null) {\n Q.push(current);\n current = current.left;\n } else {\n if (Q.length !== 0) {\n current = Q.pop();\n visitor.call(ctx, current);\n current = current.right;\n } else done = true;\n }\n }\n return this;\n };\n /**\r\n * Walk key range from `low` to `high`. Stops if `fn` returns a value.\r\n */\n Tree.prototype.range = function (low, high, fn, ctx) {\n var Q = [];\n var compare = this._comparator;\n var node = this._root;\n var cmp;\n while (Q.length !== 0 || node) {\n if (node) {\n Q.push(node);\n node = node.left;\n } else {\n node = Q.pop();\n cmp = compare(node.key, high);\n if (cmp > 0) {\n break;\n } else if (compare(node.key, low) >= 0) {\n if (fn.call(ctx, node)) return this; // stop if smth is returned\n }\n node = node.right;\n }\n }\n return this;\n };\n /**\r\n * Returns array of keys\r\n */\n Tree.prototype.keys = function () {\n var keys = [];\n this.forEach(function (_a) {\n var key = _a.key;\n return keys.push(key);\n });\n return keys;\n };\n /**\r\n * Returns array of all the data in the nodes\r\n */\n Tree.prototype.values = function () {\n var values = [];\n this.forEach(function (_a) {\n var data = _a.data;\n return values.push(data);\n });\n return values;\n };\n Tree.prototype.min = function () {\n if (this._root) return this.minNode(this._root).key;\n return null;\n };\n Tree.prototype.max = function () {\n if (this._root) return this.maxNode(this._root).key;\n return null;\n };\n Tree.prototype.minNode = function (t) {\n if (t === void 0) {\n t = this._root;\n }\n if (t) while (t.left) t = t.left;\n return t;\n };\n Tree.prototype.maxNode = function (t) {\n if (t === void 0) {\n t = this._root;\n }\n if (t) while (t.right) t = t.right;\n return t;\n };\n /**\r\n * Returns node at given index\r\n */\n Tree.prototype.at = function (index) {\n var current = this._root;\n var done = false;\n var i = 0;\n var Q = [];\n while (!done) {\n if (current) {\n Q.push(current);\n current = current.left;\n } else {\n if (Q.length > 0) {\n current = Q.pop();\n if (i === index) return current;\n i++;\n current = current.right;\n } else done = true;\n }\n }\n return null;\n };\n Tree.prototype.next = function (d) {\n var root = this._root;\n var successor = null;\n if (d.right) {\n successor = d.right;\n while (successor.left) successor = successor.left;\n return successor;\n }\n var comparator = this._comparator;\n while (root) {\n var cmp = comparator(d.key, root.key);\n if (cmp === 0) break;else if (cmp < 0) {\n successor = root;\n root = root.left;\n } else root = root.right;\n }\n return successor;\n };\n Tree.prototype.prev = function (d) {\n var root = this._root;\n var predecessor = null;\n if (d.left !== null) {\n predecessor = d.left;\n while (predecessor.right) predecessor = predecessor.right;\n return predecessor;\n }\n var comparator = this._comparator;\n while (root) {\n var cmp = comparator(d.key, root.key);\n if (cmp === 0) break;else if (cmp < 0) root = root.left;else {\n predecessor = root;\n root = root.right;\n }\n }\n return predecessor;\n };\n Tree.prototype.clear = function () {\n this._root = null;\n this._size = 0;\n return this;\n };\n Tree.prototype.toList = function () {\n return toList(this._root);\n };\n /**\r\n * Bulk-load items. Both array have to be same size\r\n */\n Tree.prototype.load = function (keys, values, presort) {\n if (values === void 0) {\n values = [];\n }\n if (presort === void 0) {\n presort = false;\n }\n var size = keys.length;\n var comparator = this._comparator;\n // sort if needed\n if (presort) sort(keys, values, 0, size - 1, comparator);\n if (this._root === null) {\n // empty tree\n this._root = loadRecursive(keys, values, 0, size);\n this._size = size;\n } else {\n // that re-builds the whole tree from two in-order traversals\n var mergedList = mergeLists(this.toList(), createList(keys, values), comparator);\n size = this._size + size;\n this._root = sortedListToBST({\n head: mergedList\n }, 0, size);\n }\n return this;\n };\n Tree.prototype.isEmpty = function () {\n return this._root === null;\n };\n Object.defineProperty(Tree.prototype, \"size\", {\n get: function () {\n return this._size;\n },\n enumerable: true,\n configurable: true\n });\n Object.defineProperty(Tree.prototype, \"root\", {\n get: function () {\n return this._root;\n },\n enumerable: true,\n configurable: true\n });\n Tree.prototype.toString = function (printNode) {\n if (printNode === void 0) {\n printNode = function (n) {\n return String(n.key);\n };\n }\n var out = [];\n printRow(this._root, '', true, function (v) {\n return out.push(v);\n }, printNode);\n return out.join('');\n };\n Tree.prototype.update = function (key, newKey, newData) {\n var comparator = this._comparator;\n var _a = split(key, this._root, comparator),\n left = _a.left,\n right = _a.right;\n if (comparator(key, newKey) < 0) {\n right = insert(newKey, newData, right, comparator);\n } else {\n left = insert(newKey, newData, left, comparator);\n }\n this._root = merge(left, right, comparator);\n };\n Tree.prototype.split = function (key) {\n return split(key, this._root, this._comparator);\n };\n Tree.prototype[Symbol.iterator] = function () {\n var current, Q, done;\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0:\n current = this._root;\n Q = [];\n done = false;\n _a.label = 1;\n case 1:\n if (!!done) return [3 /*break*/, 6];\n if (!(current !== null)) return [3 /*break*/, 2];\n Q.push(current);\n current = current.left;\n return [3 /*break*/, 5];\n case 2:\n if (!(Q.length !== 0)) return [3 /*break*/, 4];\n current = Q.pop();\n return [4 /*yield*/, current];\n case 3:\n _a.sent();\n current = current.right;\n return [3 /*break*/, 5];\n case 4:\n done = true;\n _a.label = 5;\n case 5:\n return [3 /*break*/, 1];\n case 6:\n return [2 /*return*/];\n }\n });\n };\n return Tree;\n }();\n function loadRecursive(keys, values, start, end) {\n var size = end - start;\n if (size > 0) {\n var middle = start + Math.floor(size / 2);\n var key = keys[middle];\n var data = values[middle];\n var node = new Node(key, data);\n node.left = loadRecursive(keys, values, start, middle);\n node.right = loadRecursive(keys, values, middle + 1, end);\n return node;\n }\n return null;\n }\n function createList(keys, values) {\n var head = new Node(null, null);\n var p = head;\n for (var i = 0; i < keys.length; i++) {\n p = p.next = new Node(keys[i], values[i]);\n }\n p.next = null;\n return head.next;\n }\n function toList(root) {\n var current = root;\n var Q = [];\n var done = false;\n var head = new Node(null, null);\n var p = head;\n while (!done) {\n if (current) {\n Q.push(current);\n current = current.left;\n } else {\n if (Q.length > 0) {\n current = p = p.next = Q.pop();\n current = current.right;\n } else done = true;\n }\n }\n p.next = null; // that'll work even if the tree was empty\n return head.next;\n }\n function sortedListToBST(list, start, end) {\n var size = end - start;\n if (size > 0) {\n var middle = start + Math.floor(size / 2);\n var left = sortedListToBST(list, start, middle);\n var root = list.head;\n root.left = left;\n list.head = list.head.next;\n root.right = sortedListToBST(list, middle + 1, end);\n return root;\n }\n return null;\n }\n function mergeLists(l1, l2, compare) {\n var head = new Node(null, null); // dummy\n var p = head;\n var p1 = l1;\n var p2 = l2;\n while (p1 !== null && p2 !== null) {\n if (compare(p1.key, p2.key) < 0) {\n p.next = p1;\n p1 = p1.next;\n } else {\n p.next = p2;\n p2 = p2.next;\n }\n p = p.next;\n }\n if (p1 !== null) {\n p.next = p1;\n } else if (p2 !== null) {\n p.next = p2;\n }\n return head.next;\n }\n function sort(keys, values, left, right, compare) {\n if (left >= right) return;\n var pivot = keys[left + right >> 1];\n var i = left - 1;\n var j = right + 1;\n while (true) {\n do i++; while (compare(keys[i], pivot) < 0);\n do j--; while (compare(keys[j], pivot) > 0);\n if (i >= j) break;\n var tmp = keys[i];\n keys[i] = keys[j];\n keys[j] = tmp;\n tmp = values[i];\n values[i] = values[j];\n values[j] = tmp;\n }\n sort(keys, values, left, j, compare);\n sort(keys, values, j + 1, right, compare);\n }\n\n /**\n * A bounding box has the format:\n *\n * { ll: { x: xmin, y: ymin }, ur: { x: xmax, y: ymax } }\n *\n */\n\n const isInBbox = (bbox, point) => {\n return bbox.ll.x <= point.x && point.x <= bbox.ur.x && bbox.ll.y <= point.y && point.y <= bbox.ur.y;\n };\n\n /* Returns either null, or a bbox (aka an ordered pair of points)\n * If there is only one point of overlap, a bbox with identical points\n * will be returned */\n const getBboxOverlap = (b1, b2) => {\n // check if the bboxes overlap at all\n if (b2.ur.x < b1.ll.x || b1.ur.x < b2.ll.x || b2.ur.y < b1.ll.y || b1.ur.y < b2.ll.y) return null;\n\n // find the middle two X values\n const lowerX = b1.ll.x < b2.ll.x ? b2.ll.x : b1.ll.x;\n const upperX = b1.ur.x < b2.ur.x ? b1.ur.x : b2.ur.x;\n\n // find the middle two Y values\n const lowerY = b1.ll.y < b2.ll.y ? b2.ll.y : b1.ll.y;\n const upperY = b1.ur.y < b2.ur.y ? b1.ur.y : b2.ur.y;\n\n // put those middle values together to get the overlap\n return {\n ll: {\n x: lowerX,\n y: lowerY\n },\n ur: {\n x: upperX,\n y: upperY\n }\n };\n };\n\n /* Javascript doesn't do integer math. Everything is\n * floating point with percision Number.EPSILON.\n *\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON\n */\n\n let epsilon$1 = Number.EPSILON;\n\n // IE Polyfill\n if (epsilon$1 === undefined) epsilon$1 = Math.pow(2, -52);\n const EPSILON_SQ = epsilon$1 * epsilon$1;\n\n /* FLP comparator */\n const cmp = (a, b) => {\n // check if they're both 0\n if (-epsilon$1 < a && a < epsilon$1) {\n if (-epsilon$1 < b && b < epsilon$1) {\n return 0;\n }\n }\n\n // check if they're flp equal\n const ab = a - b;\n if (ab * ab < EPSILON_SQ * a * b) {\n return 0;\n }\n\n // normal comparison\n return a < b ? -1 : 1;\n };\n\n /**\n * This class rounds incoming values sufficiently so that\n * floating points problems are, for the most part, avoided.\n *\n * Incoming points are have their x & y values tested against\n * all previously seen x & y values. If either is 'too close'\n * to a previously seen value, it's value is 'snapped' to the\n * previously seen value.\n *\n * All points should be rounded by this class before being\n * stored in any data structures in the rest of this algorithm.\n */\n\n class PtRounder {\n constructor() {\n this.reset();\n }\n reset() {\n this.xRounder = new CoordRounder();\n this.yRounder = new CoordRounder();\n }\n round(x, y) {\n return {\n x: this.xRounder.round(x),\n y: this.yRounder.round(y)\n };\n }\n }\n class CoordRounder {\n constructor() {\n this.tree = new Tree();\n // preseed with 0 so we don't end up with values < Number.EPSILON\n this.round(0);\n }\n\n // Note: this can rounds input values backwards or forwards.\n // You might ask, why not restrict this to just rounding\n // forwards? Wouldn't that allow left endpoints to always\n // remain left endpoints during splitting (never change to\n // right). No - it wouldn't, because we snap intersections\n // to endpoints (to establish independence from the segment\n // angle for t-intersections).\n round(coord) {\n const node = this.tree.add(coord);\n const prevNode = this.tree.prev(node);\n if (prevNode !== null && cmp(node.key, prevNode.key) === 0) {\n this.tree.remove(coord);\n return prevNode.key;\n }\n const nextNode = this.tree.next(node);\n if (nextNode !== null && cmp(node.key, nextNode.key) === 0) {\n this.tree.remove(coord);\n return nextNode.key;\n }\n return coord;\n }\n }\n\n // singleton available by import\n const rounder = new PtRounder();\n\n const epsilon = 1.1102230246251565e-16;\n const splitter = 134217729;\n const resulterrbound = (3 + 8 * epsilon) * epsilon;\n\n // fast_expansion_sum_zeroelim routine from oritinal code\n function sum(elen, e, flen, f, h) {\n let Q, Qnew, hh, bvirt;\n let enow = e[0];\n let fnow = f[0];\n let eindex = 0;\n let findex = 0;\n if (fnow > enow === fnow > -enow) {\n Q = enow;\n enow = e[++eindex];\n } else {\n Q = fnow;\n fnow = f[++findex];\n }\n let hindex = 0;\n if (eindex < elen && findex < flen) {\n if (fnow > enow === fnow > -enow) {\n Qnew = enow + Q;\n hh = Q - (Qnew - enow);\n enow = e[++eindex];\n } else {\n Qnew = fnow + Q;\n hh = Q - (Qnew - fnow);\n fnow = f[++findex];\n }\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n while (eindex < elen && findex < flen) {\n if (fnow > enow === fnow > -enow) {\n Qnew = Q + enow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (enow - bvirt);\n enow = e[++eindex];\n } else {\n Qnew = Q + fnow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (fnow - bvirt);\n fnow = f[++findex];\n }\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n }\n }\n while (eindex < elen) {\n Qnew = Q + enow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (enow - bvirt);\n enow = e[++eindex];\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n }\n while (findex < flen) {\n Qnew = Q + fnow;\n bvirt = Qnew - Q;\n hh = Q - (Qnew - bvirt) + (fnow - bvirt);\n fnow = f[++findex];\n Q = Qnew;\n if (hh !== 0) {\n h[hindex++] = hh;\n }\n }\n if (Q !== 0 || hindex === 0) {\n h[hindex++] = Q;\n }\n return hindex;\n }\n function estimate(elen, e) {\n let Q = e[0];\n for (let i = 1; i < elen; i++) Q += e[i];\n return Q;\n }\n function vec(n) {\n return new Float64Array(n);\n }\n\n const ccwerrboundA = (3 + 16 * epsilon) * epsilon;\n const ccwerrboundB = (2 + 12 * epsilon) * epsilon;\n const ccwerrboundC = (9 + 64 * epsilon) * epsilon * epsilon;\n const B = vec(4);\n const C1 = vec(8);\n const C2 = vec(12);\n const D = vec(16);\n const u = vec(4);\n function orient2dadapt(ax, ay, bx, by, cx, cy, detsum) {\n let acxtail, acytail, bcxtail, bcytail;\n let bvirt, c, ahi, alo, bhi, blo, _i, _j, _0, s1, s0, t1, t0, u3;\n const acx = ax - cx;\n const bcx = bx - cx;\n const acy = ay - cy;\n const bcy = by - cy;\n s1 = acx * bcy;\n c = splitter * acx;\n ahi = c - (c - acx);\n alo = acx - ahi;\n c = splitter * bcy;\n bhi = c - (c - bcy);\n blo = bcy - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acy * bcx;\n c = splitter * acy;\n ahi = c - (c - acy);\n alo = acy - ahi;\n c = splitter * bcx;\n bhi = c - (c - bcx);\n blo = bcx - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n B[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n B[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n B[2] = _j - (u3 - bvirt) + (_i - bvirt);\n B[3] = u3;\n let det = estimate(4, B);\n let errbound = ccwerrboundB * detsum;\n if (det >= errbound || -det >= errbound) {\n return det;\n }\n bvirt = ax - acx;\n acxtail = ax - (acx + bvirt) + (bvirt - cx);\n bvirt = bx - bcx;\n bcxtail = bx - (bcx + bvirt) + (bvirt - cx);\n bvirt = ay - acy;\n acytail = ay - (acy + bvirt) + (bvirt - cy);\n bvirt = by - bcy;\n bcytail = by - (bcy + bvirt) + (bvirt - cy);\n if (acxtail === 0 && acytail === 0 && bcxtail === 0 && bcytail === 0) {\n return det;\n }\n errbound = ccwerrboundC * detsum + resulterrbound * Math.abs(det);\n det += acx * bcytail + bcy * acxtail - (acy * bcxtail + bcx * acytail);\n if (det >= errbound || -det >= errbound) return det;\n s1 = acxtail * bcy;\n c = splitter * acxtail;\n ahi = c - (c - acxtail);\n alo = acxtail - ahi;\n c = splitter * bcy;\n bhi = c - (c - bcy);\n blo = bcy - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acytail * bcx;\n c = splitter * acytail;\n ahi = c - (c - acytail);\n alo = acytail - ahi;\n c = splitter * bcx;\n bhi = c - (c - bcx);\n blo = bcx - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n u[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n u[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n u[2] = _j - (u3 - bvirt) + (_i - bvirt);\n u[3] = u3;\n const C1len = sum(4, B, 4, u, C1);\n s1 = acx * bcytail;\n c = splitter * acx;\n ahi = c - (c - acx);\n alo = acx - ahi;\n c = splitter * bcytail;\n bhi = c - (c - bcytail);\n blo = bcytail - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acy * bcxtail;\n c = splitter * acy;\n ahi = c - (c - acy);\n alo = acy - ahi;\n c = splitter * bcxtail;\n bhi = c - (c - bcxtail);\n blo = bcxtail - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n u[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n u[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n u[2] = _j - (u3 - bvirt) + (_i - bvirt);\n u[3] = u3;\n const C2len = sum(C1len, C1, 4, u, C2);\n s1 = acxtail * bcytail;\n c = splitter * acxtail;\n ahi = c - (c - acxtail);\n alo = acxtail - ahi;\n c = splitter * bcytail;\n bhi = c - (c - bcytail);\n blo = bcytail - bhi;\n s0 = alo * blo - (s1 - ahi * bhi - alo * bhi - ahi * blo);\n t1 = acytail * bcxtail;\n c = splitter * acytail;\n ahi = c - (c - acytail);\n alo = acytail - ahi;\n c = splitter * bcxtail;\n bhi = c - (c - bcxtail);\n blo = bcxtail - bhi;\n t0 = alo * blo - (t1 - ahi * bhi - alo * bhi - ahi * blo);\n _i = s0 - t0;\n bvirt = s0 - _i;\n u[0] = s0 - (_i + bvirt) + (bvirt - t0);\n _j = s1 + _i;\n bvirt = _j - s1;\n _0 = s1 - (_j - bvirt) + (_i - bvirt);\n _i = _0 - t1;\n bvirt = _0 - _i;\n u[1] = _0 - (_i + bvirt) + (bvirt - t1);\n u3 = _j + _i;\n bvirt = u3 - _j;\n u[2] = _j - (u3 - bvirt) + (_i - bvirt);\n u[3] = u3;\n const Dlen = sum(C2len, C2, 4, u, D);\n return D[Dlen - 1];\n }\n function orient2d(ax, ay, bx, by, cx, cy) {\n const detleft = (ay - cy) * (bx - cx);\n const detright = (ax - cx) * (by - cy);\n const det = detleft - detright;\n const detsum = Math.abs(detleft + detright);\n if (Math.abs(det) >= ccwerrboundA * detsum) return det;\n return -orient2dadapt(ax, ay, bx, by, cx, cy, detsum);\n }\n\n /* Cross Product of two vectors with first point at origin */\n const crossProduct = (a, b) => a.x * b.y - a.y * b.x;\n\n /* Dot Product of two vectors with first point at origin */\n const dotProduct = (a, b) => a.x * b.x + a.y * b.y;\n\n /* Comparator for two vectors with same starting point */\n const compareVectorAngles = (basePt, endPt1, endPt2) => {\n const res = orient2d(basePt.x, basePt.y, endPt1.x, endPt1.y, endPt2.x, endPt2.y);\n if (res > 0) return -1;\n if (res < 0) return 1;\n return 0;\n };\n const length = v => Math.sqrt(dotProduct(v, v));\n\n /* Get the sine of the angle from pShared -> pAngle to pShaed -> pBase */\n const sineOfAngle = (pShared, pBase, pAngle) => {\n const vBase = {\n x: pBase.x - pShared.x,\n y: pBase.y - pShared.y\n };\n const vAngle = {\n x: pAngle.x - pShared.x,\n y: pAngle.y - pShared.y\n };\n return crossProduct(vAngle, vBase) / length(vAngle) / length(vBase);\n };\n\n /* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */\n const cosineOfAngle = (pShared, pBase, pAngle) => {\n const vBase = {\n x: pBase.x - pShared.x,\n y: pBase.y - pShared.y\n };\n const vAngle = {\n x: pAngle.x - pShared.x,\n y: pAngle.y - pShared.y\n };\n return dotProduct(vAngle, vBase) / length(vAngle) / length(vBase);\n };\n\n /* Get the x coordinate where the given line (defined by a point and vector)\n * crosses the horizontal line with the given y coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\n const horizontalIntersection = (pt, v, y) => {\n if (v.y === 0) return null;\n return {\n x: pt.x + v.x / v.y * (y - pt.y),\n y: y\n };\n };\n\n /* Get the y coordinate where the given line (defined by a point and vector)\n * crosses the vertical line with the given x coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\n const verticalIntersection = (pt, v, x) => {\n if (v.x === 0) return null;\n return {\n x: x,\n y: pt.y + v.y / v.x * (x - pt.x)\n };\n };\n\n /* Get the intersection of two lines, each defined by a base point and a vector.\n * In the case of parrallel lines (including overlapping ones) returns null. */\n const intersection$1 = (pt1, v1, pt2, v2) => {\n // take some shortcuts for vertical and horizontal lines\n // this also ensures we don't calculate an intersection and then discover\n // it's actually outside the bounding box of the line\n if (v1.x === 0) return verticalIntersection(pt2, v2, pt1.x);\n if (v2.x === 0) return verticalIntersection(pt1, v1, pt2.x);\n if (v1.y === 0) return horizontalIntersection(pt2, v2, pt1.y);\n if (v2.y === 0) return horizontalIntersection(pt1, v1, pt2.y);\n\n // General case for non-overlapping segments.\n // This algorithm is based on Schneider and Eberly.\n // http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf - pg 244\n\n const kross = crossProduct(v1, v2);\n if (kross == 0) return null;\n const ve = {\n x: pt2.x - pt1.x,\n y: pt2.y - pt1.y\n };\n const d1 = crossProduct(ve, v1) / kross;\n const d2 = crossProduct(ve, v2) / kross;\n\n // take the average of the two calculations to minimize rounding error\n const x1 = pt1.x + d2 * v1.x,\n x2 = pt2.x + d1 * v2.x;\n const y1 = pt1.y + d2 * v1.y,\n y2 = pt2.y + d1 * v2.y;\n const x = (x1 + x2) / 2;\n const y = (y1 + y2) / 2;\n return {\n x: x,\n y: y\n };\n };\n\n class SweepEvent {\n // for ordering sweep events in the sweep event queue\n static compare(a, b) {\n // favor event with a point that the sweep line hits first\n const ptCmp = SweepEvent.comparePoints(a.point, b.point);\n if (ptCmp !== 0) return ptCmp;\n\n // the points are the same, so link them if needed\n if (a.point !== b.point) a.link(b);\n\n // favor right events over left\n if (a.isLeft !== b.isLeft) return a.isLeft ? 1 : -1;\n\n // we have two matching left or right endpoints\n // ordering of this case is the same as for their segments\n return Segment.compare(a.segment, b.segment);\n }\n\n // for ordering points in sweep line order\n static comparePoints(aPt, bPt) {\n if (aPt.x < bPt.x) return -1;\n if (aPt.x > bPt.x) return 1;\n if (aPt.y < bPt.y) return -1;\n if (aPt.y > bPt.y) return 1;\n return 0;\n }\n\n // Warning: 'point' input will be modified and re-used (for performance)\n constructor(point, isLeft) {\n if (point.events === undefined) point.events = [this];else point.events.push(this);\n this.point = point;\n this.isLeft = isLeft;\n // this.segment, this.otherSE set by factory\n }\n link(other) {\n if (other.point === this.point) {\n throw new Error(\"Tried to link already linked events\");\n }\n const otherEvents = other.point.events;\n for (let i = 0, iMax = otherEvents.length; i < iMax; i++) {\n const evt = otherEvents[i];\n this.point.events.push(evt);\n evt.point = this.point;\n }\n this.checkForConsuming();\n }\n\n /* Do a pass over our linked events and check to see if any pair\n * of segments match, and should be consumed. */\n checkForConsuming() {\n // FIXME: The loops in this method run O(n^2) => no good.\n // Maintain little ordered sweep event trees?\n // Can we maintaining an ordering that avoids the need\n // for the re-sorting with getLeftmostComparator in geom-out?\n\n // Compare each pair of events to see if other events also match\n const numEvents = this.point.events.length;\n for (let i = 0; i < numEvents; i++) {\n const evt1 = this.point.events[i];\n if (evt1.segment.consumedBy !== undefined) continue;\n for (let j = i + 1; j < numEvents; j++) {\n const evt2 = this.point.events[j];\n if (evt2.consumedBy !== undefined) continue;\n if (evt1.otherSE.point.events !== evt2.otherSE.point.events) continue;\n evt1.segment.consume(evt2.segment);\n }\n }\n }\n getAvailableLinkedEvents() {\n // point.events is always of length 2 or greater\n const events = [];\n for (let i = 0, iMax = this.point.events.length; i < iMax; i++) {\n const evt = this.point.events[i];\n if (evt !== this && !evt.segment.ringOut && evt.segment.isInResult()) {\n events.push(evt);\n }\n }\n return events;\n }\n\n /**\n * Returns a comparator function for sorting linked events that will\n * favor the event that will give us the smallest left-side angle.\n * All ring construction starts as low as possible heading to the right,\n * so by always turning left as sharp as possible we'll get polygons\n * without uncessary loops & holes.\n *\n * The comparator function has a compute cache such that it avoids\n * re-computing already-computed values.\n */\n getLeftmostComparator(baseEvent) {\n const cache = new Map();\n const fillCache = linkedEvent => {\n const nextEvent = linkedEvent.otherSE;\n cache.set(linkedEvent, {\n sine: sineOfAngle(this.point, baseEvent.point, nextEvent.point),\n cosine: cosineOfAngle(this.point, baseEvent.point, nextEvent.point)\n });\n };\n return (a, b) => {\n if (!cache.has(a)) fillCache(a);\n if (!cache.has(b)) fillCache(b);\n const {\n sine: asine,\n cosine: acosine\n } = cache.get(a);\n const {\n sine: bsine,\n cosine: bcosine\n } = cache.get(b);\n\n // both on or above x-axis\n if (asine >= 0 && bsine >= 0) {\n if (acosine < bcosine) return 1;\n if (acosine > bcosine) return -1;\n return 0;\n }\n\n // both below x-axis\n if (asine < 0 && bsine < 0) {\n if (acosine < bcosine) return -1;\n if (acosine > bcosine) return 1;\n return 0;\n }\n\n // one above x-axis, one below\n if (bsine < asine) return -1;\n if (bsine > asine) return 1;\n return 0;\n };\n }\n }\n\n // Give segments unique ID's to get consistent sorting of\n // segments and sweep events when all else is identical\n let segmentId = 0;\n class Segment {\n /* This compare() function is for ordering segments in the sweep\n * line tree, and does so according to the following criteria:\n *\n * Consider the vertical line that lies an infinestimal step to the\n * right of the right-more of the two left endpoints of the input\n * segments. Imagine slowly moving a point up from negative infinity\n * in the increasing y direction. Which of the two segments will that\n * point intersect first? That segment comes 'before' the other one.\n *\n * If neither segment would be intersected by such a line, (if one\n * or more of the segments are vertical) then the line to be considered\n * is directly on the right-more of the two left inputs.\n */\n static compare(a, b) {\n const alx = a.leftSE.point.x;\n const blx = b.leftSE.point.x;\n const arx = a.rightSE.point.x;\n const brx = b.rightSE.point.x;\n\n // check if they're even in the same vertical plane\n if (brx < alx) return 1;\n if (arx < blx) return -1;\n const aly = a.leftSE.point.y;\n const bly = b.leftSE.point.y;\n const ary = a.rightSE.point.y;\n const bry = b.rightSE.point.y;\n\n // is left endpoint of segment B the right-more?\n if (alx < blx) {\n // are the two segments in the same horizontal plane?\n if (bly < aly && bly < ary) return 1;\n if (bly > aly && bly > ary) return -1;\n\n // is the B left endpoint colinear to segment A?\n const aCmpBLeft = a.comparePoint(b.leftSE.point);\n if (aCmpBLeft < 0) return 1;\n if (aCmpBLeft > 0) return -1;\n\n // is the A right endpoint colinear to segment B ?\n const bCmpARight = b.comparePoint(a.rightSE.point);\n if (bCmpARight !== 0) return bCmpARight;\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return -1;\n }\n\n // is left endpoint of segment A the right-more?\n if (alx > blx) {\n if (aly < bly && aly < bry) return -1;\n if (aly > bly && aly > bry) return 1;\n\n // is the A left endpoint colinear to segment B?\n const bCmpALeft = b.comparePoint(a.leftSE.point);\n if (bCmpALeft !== 0) return bCmpALeft;\n\n // is the B right endpoint colinear to segment A?\n const aCmpBRight = a.comparePoint(b.rightSE.point);\n if (aCmpBRight < 0) return 1;\n if (aCmpBRight > 0) return -1;\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return 1;\n }\n\n // if we get here, the two left endpoints are in the same\n // vertical plane, ie alx === blx\n\n // consider the lower left-endpoint to come first\n if (aly < bly) return -1;\n if (aly > bly) return 1;\n\n // left endpoints are identical\n // check for colinearity by using the left-more right endpoint\n\n // is the A right endpoint more left-more?\n if (arx < brx) {\n const bCmpARight = b.comparePoint(a.rightSE.point);\n if (bCmpARight !== 0) return bCmpARight;\n }\n\n // is the B right endpoint more left-more?\n if (arx > brx) {\n const aCmpBRight = a.comparePoint(b.rightSE.point);\n if (aCmpBRight < 0) return 1;\n if (aCmpBRight > 0) return -1;\n }\n if (arx !== brx) {\n // are these two [almost] vertical segments with opposite orientation?\n // if so, the one with the lower right endpoint comes first\n const ay = ary - aly;\n const ax = arx - alx;\n const by = bry - bly;\n const bx = brx - blx;\n if (ay > ax && by < bx) return 1;\n if (ay < ax && by > bx) return -1;\n }\n\n // we have colinear segments with matching orientation\n // consider the one with more left-more right endpoint to be first\n if (arx > brx) return 1;\n if (arx < brx) return -1;\n\n // if we get here, two two right endpoints are in the same\n // vertical plane, ie arx === brx\n\n // consider the lower right-endpoint to come first\n if (ary < bry) return -1;\n if (ary > bry) return 1;\n\n // right endpoints identical as well, so the segments are idential\n // fall back on creation order as consistent tie-breaker\n if (a.id < b.id) return -1;\n if (a.id > b.id) return 1;\n\n // identical segment, ie a === b\n return 0;\n }\n\n /* Warning: a reference to ringWindings input will be stored,\n * and possibly will be later modified */\n constructor(leftSE, rightSE, rings, windings) {\n this.id = ++segmentId;\n this.leftSE = leftSE;\n leftSE.segment = this;\n leftSE.otherSE = rightSE;\n this.rightSE = rightSE;\n rightSE.segment = this;\n rightSE.otherSE = leftSE;\n this.rings = rings;\n this.windings = windings;\n // left unset for performance, set later in algorithm\n // this.ringOut, this.consumedBy, this.prev\n }\n static fromRing(pt1, pt2, ring) {\n let leftPt, rightPt, winding;\n\n // ordering the two points according to sweep line ordering\n const cmpPts = SweepEvent.comparePoints(pt1, pt2);\n if (cmpPts < 0) {\n leftPt = pt1;\n rightPt = pt2;\n winding = 1;\n } else if (cmpPts > 0) {\n leftPt = pt2;\n rightPt = pt1;\n winding = -1;\n } else throw new Error(`Tried to create degenerate segment at [${pt1.x}, ${pt1.y}]`);\n const leftSE = new SweepEvent(leftPt, true);\n const rightSE = new SweepEvent(rightPt, false);\n return new Segment(leftSE, rightSE, [ring], [winding]);\n }\n\n /* When a segment is split, the rightSE is replaced with a new sweep event */\n replaceRightSE(newRightSE) {\n this.rightSE = newRightSE;\n this.rightSE.segment = this;\n this.rightSE.otherSE = this.leftSE;\n this.leftSE.otherSE = this.rightSE;\n }\n bbox() {\n const y1 = this.leftSE.point.y;\n const y2 = this.rightSE.point.y;\n return {\n ll: {\n x: this.leftSE.point.x,\n y: y1 < y2 ? y1 : y2\n },\n ur: {\n x: this.rightSE.point.x,\n y: y1 > y2 ? y1 : y2\n }\n };\n }\n\n /* A vector from the left point to the right */\n vector() {\n return {\n x: this.rightSE.point.x - this.leftSE.point.x,\n y: this.rightSE.point.y - this.leftSE.point.y\n };\n }\n isAnEndpoint(pt) {\n return pt.x === this.leftSE.point.x && pt.y === this.leftSE.point.y || pt.x === this.rightSE.point.x && pt.y === this.rightSE.point.y;\n }\n\n /* Compare this segment with a point.\n *\n * A point P is considered to be colinear to a segment if there\n * exists a distance D such that if we travel along the segment\n * from one * endpoint towards the other a distance D, we find\n * ourselves at point P.\n *\n * Return value indicates:\n *\n * 1: point lies above the segment (to the left of vertical)\n * 0: point is colinear to segment\n * -1: point lies below the segment (to the right of vertical)\n */\n comparePoint(point) {\n if (this.isAnEndpoint(point)) return 0;\n const lPt = this.leftSE.point;\n const rPt = this.rightSE.point;\n const v = this.vector();\n\n // Exactly vertical segments.\n if (lPt.x === rPt.x) {\n if (point.x === lPt.x) return 0;\n return point.x < lPt.x ? 1 : -1;\n }\n\n // Nearly vertical segments with an intersection.\n // Check to see where a point on the line with matching Y coordinate is.\n const yDist = (point.y - lPt.y) / v.y;\n const xFromYDist = lPt.x + yDist * v.x;\n if (point.x === xFromYDist) return 0;\n\n // General case.\n // Check to see where a point on the line with matching X coordinate is.\n const xDist = (point.x - lPt.x) / v.x;\n const yFromXDist = lPt.y + xDist * v.y;\n if (point.y === yFromXDist) return 0;\n return point.y < yFromXDist ? -1 : 1;\n }\n\n /**\n * Given another segment, returns the first non-trivial intersection\n * between the two segments (in terms of sweep line ordering), if it exists.\n *\n * A 'non-trivial' intersection is one that will cause one or both of the\n * segments to be split(). As such, 'trivial' vs. 'non-trivial' intersection:\n *\n * * endpoint of segA with endpoint of segB --> trivial\n * * endpoint of segA with point along segB --> non-trivial\n * * endpoint of segB with point along segA --> non-trivial\n * * point along segA with point along segB --> non-trivial\n *\n * If no non-trivial intersection exists, return null\n * Else, return null.\n */\n getIntersection(other) {\n // If bboxes don't overlap, there can't be any intersections\n const tBbox = this.bbox();\n const oBbox = other.bbox();\n const bboxOverlap = getBboxOverlap(tBbox, oBbox);\n if (bboxOverlap === null) return null;\n\n // We first check to see if the endpoints can be considered intersections.\n // This will 'snap' intersections to endpoints if possible, and will\n // handle cases of colinearity.\n\n const tlp = this.leftSE.point;\n const trp = this.rightSE.point;\n const olp = other.leftSE.point;\n const orp = other.rightSE.point;\n\n // does each endpoint touch the other segment?\n // note that we restrict the 'touching' definition to only allow segments\n // to touch endpoints that lie forward from where we are in the sweep line pass\n const touchesOtherLSE = isInBbox(tBbox, olp) && this.comparePoint(olp) === 0;\n const touchesThisLSE = isInBbox(oBbox, tlp) && other.comparePoint(tlp) === 0;\n const touchesOtherRSE = isInBbox(tBbox, orp) && this.comparePoint(orp) === 0;\n const touchesThisRSE = isInBbox(oBbox, trp) && other.comparePoint(trp) === 0;\n\n // do left endpoints match?\n if (touchesThisLSE && touchesOtherLSE) {\n // these two cases are for colinear segments with matching left\n // endpoints, and one segment being longer than the other\n if (touchesThisRSE && !touchesOtherRSE) return trp;\n if (!touchesThisRSE && touchesOtherRSE) return orp;\n // either the two segments match exactly (two trival intersections)\n // or just on their left endpoint (one trivial intersection\n return null;\n }\n\n // does this left endpoint matches (other doesn't)\n if (touchesThisLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesOtherRSE) {\n if (tlp.x === orp.x && tlp.y === orp.y) return null;\n }\n // t-intersection on left endpoint\n return tlp;\n }\n\n // does other left endpoint matches (this doesn't)\n if (touchesOtherLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesThisRSE) {\n if (trp.x === olp.x && trp.y === olp.y) return null;\n }\n // t-intersection on left endpoint\n return olp;\n }\n\n // trivial intersection on right endpoints\n if (touchesThisRSE && touchesOtherRSE) return null;\n\n // t-intersections on just one right endpoint\n if (touchesThisRSE) return trp;\n if (touchesOtherRSE) return orp;\n\n // None of our endpoints intersect. Look for a general intersection between\n // infinite lines laid over the segments\n const pt = intersection$1(tlp, this.vector(), olp, other.vector());\n\n // are the segments parrallel? Note that if they were colinear with overlap,\n // they would have an endpoint intersection and that case was already handled above\n if (pt === null) return null;\n\n // is the intersection found between the lines not on the segments?\n if (!isInBbox(bboxOverlap, pt)) return null;\n\n // round the the computed point if needed\n return rounder.round(pt.x, pt.y);\n }\n\n /**\n * Split the given segment into multiple segments on the given points.\n * * Each existing segment will retain its leftSE and a new rightSE will be\n * generated for it.\n * * A new segment will be generated which will adopt the original segment's\n * rightSE, and a new leftSE will be generated for it.\n * * If there are more than two points given to split on, new segments\n * in the middle will be generated with new leftSE and rightSE's.\n * * An array of the newly generated SweepEvents will be returned.\n *\n * Warning: input array of points is modified\n */\n split(point) {\n const newEvents = [];\n const alreadyLinked = point.events !== undefined;\n const newLeftSE = new SweepEvent(point, true);\n const newRightSE = new SweepEvent(point, false);\n const oldRightSE = this.rightSE;\n this.replaceRightSE(newRightSE);\n newEvents.push(newRightSE);\n newEvents.push(newLeftSE);\n const newSeg = new Segment(newLeftSE, oldRightSE, this.rings.slice(), this.windings.slice());\n\n // when splitting a nearly vertical downward-facing segment,\n // sometimes one of the resulting new segments is vertical, in which\n // case its left and right events may need to be swapped\n if (SweepEvent.comparePoints(newSeg.leftSE.point, newSeg.rightSE.point) > 0) {\n newSeg.swapEvents();\n }\n if (SweepEvent.comparePoints(this.leftSE.point, this.rightSE.point) > 0) {\n this.swapEvents();\n }\n\n // in the point we just used to create new sweep events with was already\n // linked to other events, we need to check if either of the affected\n // segments should be consumed\n if (alreadyLinked) {\n newLeftSE.checkForConsuming();\n newRightSE.checkForConsuming();\n }\n return newEvents;\n }\n\n /* Swap which event is left and right */\n swapEvents() {\n const tmpEvt = this.rightSE;\n this.rightSE = this.leftSE;\n this.leftSE = tmpEvt;\n this.leftSE.isLeft = true;\n this.rightSE.isLeft = false;\n for (let i = 0, iMax = this.windings.length; i < iMax; i++) {\n this.windings[i] *= -1;\n }\n }\n\n /* Consume another segment. We take their rings under our wing\n * and mark them as consumed. Use for perfectly overlapping segments */\n consume(other) {\n let consumer = this;\n let consumee = other;\n while (consumer.consumedBy) consumer = consumer.consumedBy;\n while (consumee.consumedBy) consumee = consumee.consumedBy;\n const cmp = Segment.compare(consumer, consumee);\n if (cmp === 0) return; // already consumed\n // the winner of the consumption is the earlier segment\n // according to sweep line ordering\n if (cmp > 0) {\n const tmp = consumer;\n consumer = consumee;\n consumee = tmp;\n }\n\n // make sure a segment doesn't consume it's prev\n if (consumer.prev === consumee) {\n const tmp = consumer;\n consumer = consumee;\n consumee = tmp;\n }\n for (let i = 0, iMax = consumee.rings.length; i < iMax; i++) {\n const ring = consumee.rings[i];\n const winding = consumee.windings[i];\n const index = consumer.rings.indexOf(ring);\n if (index === -1) {\n consumer.rings.push(ring);\n consumer.windings.push(winding);\n } else consumer.windings[index] += winding;\n }\n consumee.rings = null;\n consumee.windings = null;\n consumee.consumedBy = consumer;\n\n // mark sweep events consumed as to maintain ordering in sweep event queue\n consumee.leftSE.consumedBy = consumer.leftSE;\n consumee.rightSE.consumedBy = consumer.rightSE;\n }\n\n /* The first segment previous segment chain that is in the result */\n prevInResult() {\n if (this._prevInResult !== undefined) return this._prevInResult;\n if (!this.prev) this._prevInResult = null;else if (this.prev.isInResult()) this._prevInResult = this.prev;else this._prevInResult = this.prev.prevInResult();\n return this._prevInResult;\n }\n beforeState() {\n if (this._beforeState !== undefined) return this._beforeState;\n if (!this.prev) this._beforeState = {\n rings: [],\n windings: [],\n multiPolys: []\n };else {\n const seg = this.prev.consumedBy || this.prev;\n this._beforeState = seg.afterState();\n }\n return this._beforeState;\n }\n afterState() {\n if (this._afterState !== undefined) return this._afterState;\n const beforeState = this.beforeState();\n this._afterState = {\n rings: beforeState.rings.slice(0),\n windings: beforeState.windings.slice(0),\n multiPolys: []\n };\n const ringsAfter = this._afterState.rings;\n const windingsAfter = this._afterState.windings;\n const mpsAfter = this._afterState.multiPolys;\n\n // calculate ringsAfter, windingsAfter\n for (let i = 0, iMax = this.rings.length; i < iMax; i++) {\n const ring = this.rings[i];\n const winding = this.windings[i];\n const index = ringsAfter.indexOf(ring);\n if (index === -1) {\n ringsAfter.push(ring);\n windingsAfter.push(winding);\n } else windingsAfter[index] += winding;\n }\n\n // calcualte polysAfter\n const polysAfter = [];\n const polysExclude = [];\n for (let i = 0, iMax = ringsAfter.length; i < iMax; i++) {\n if (windingsAfter[i] === 0) continue; // non-zero rule\n const ring = ringsAfter[i];\n const poly = ring.poly;\n if (polysExclude.indexOf(poly) !== -1) continue;\n if (ring.isExterior) polysAfter.push(poly);else {\n if (polysExclude.indexOf(poly) === -1) polysExclude.push(poly);\n const index = polysAfter.indexOf(ring.poly);\n if (index !== -1) polysAfter.splice(index, 1);\n }\n }\n\n // calculate multiPolysAfter\n for (let i = 0, iMax = polysAfter.length; i < iMax; i++) {\n const mp = polysAfter[i].multiPoly;\n if (mpsAfter.indexOf(mp) === -1) mpsAfter.push(mp);\n }\n return this._afterState;\n }\n\n /* Is this segment part of the final result? */\n isInResult() {\n // if we've been consumed, we're not in the result\n if (this.consumedBy) return false;\n if (this._isInResult !== undefined) return this._isInResult;\n const mpsBefore = this.beforeState().multiPolys;\n const mpsAfter = this.afterState().multiPolys;\n switch (operation.type) {\n case \"union\":\n {\n // UNION - included iff:\n // * On one side of us there is 0 poly interiors AND\n // * On the other side there is 1 or more.\n const noBefores = mpsBefore.length === 0;\n const noAfters = mpsAfter.length === 0;\n this._isInResult = noBefores !== noAfters;\n break;\n }\n case \"intersection\":\n {\n // INTERSECTION - included iff:\n // * on one side of us all multipolys are rep. with poly interiors AND\n // * on the other side of us, not all multipolys are repsented\n // with poly interiors\n let least;\n let most;\n if (mpsBefore.length < mpsAfter.length) {\n least = mpsBefore.length;\n most = mpsAfter.length;\n } else {\n least = mpsAfter.length;\n most = mpsBefore.length;\n }\n this._isInResult = most === operation.numMultiPolys && least < most;\n break;\n }\n case \"xor\":\n {\n // XOR - included iff:\n // * the difference between the number of multipolys represented\n // with poly interiors on our two sides is an odd number\n const diff = Math.abs(mpsBefore.length - mpsAfter.length);\n this._isInResult = diff % 2 === 1;\n break;\n }\n case \"difference\":\n {\n // DIFFERENCE included iff:\n // * on exactly one side, we have just the subject\n const isJustSubject = mps => mps.length === 1 && mps[0].isSubject;\n this._isInResult = isJustSubject(mpsBefore) !== isJustSubject(mpsAfter);\n break;\n }\n default:\n throw new Error(`Unrecognized operation type found ${operation.type}`);\n }\n return this._isInResult;\n }\n }\n\n class RingIn {\n constructor(geomRing, poly, isExterior) {\n if (!Array.isArray(geomRing) || geomRing.length === 0) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n this.poly = poly;\n this.isExterior = isExterior;\n this.segments = [];\n if (typeof geomRing[0][0] !== \"number\" || typeof geomRing[0][1] !== \"number\") {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n const firstPoint = rounder.round(geomRing[0][0], geomRing[0][1]);\n this.bbox = {\n ll: {\n x: firstPoint.x,\n y: firstPoint.y\n },\n ur: {\n x: firstPoint.x,\n y: firstPoint.y\n }\n };\n let prevPoint = firstPoint;\n for (let i = 1, iMax = geomRing.length; i < iMax; i++) {\n if (typeof geomRing[i][0] !== \"number\" || typeof geomRing[i][1] !== \"number\") {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n let point = rounder.round(geomRing[i][0], geomRing[i][1]);\n // skip repeated points\n if (point.x === prevPoint.x && point.y === prevPoint.y) continue;\n this.segments.push(Segment.fromRing(prevPoint, point, this));\n if (point.x < this.bbox.ll.x) this.bbox.ll.x = point.x;\n if (point.y < this.bbox.ll.y) this.bbox.ll.y = point.y;\n if (point.x > this.bbox.ur.x) this.bbox.ur.x = point.x;\n if (point.y > this.bbox.ur.y) this.bbox.ur.y = point.y;\n prevPoint = point;\n }\n // add segment from last to first if last is not the same as first\n if (firstPoint.x !== prevPoint.x || firstPoint.y !== prevPoint.y) {\n this.segments.push(Segment.fromRing(prevPoint, firstPoint, this));\n }\n }\n getSweepEvents() {\n const sweepEvents = [];\n for (let i = 0, iMax = this.segments.length; i < iMax; i++) {\n const segment = this.segments[i];\n sweepEvents.push(segment.leftSE);\n sweepEvents.push(segment.rightSE);\n }\n return sweepEvents;\n }\n }\n class PolyIn {\n constructor(geomPoly, multiPoly) {\n if (!Array.isArray(geomPoly)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n this.exteriorRing = new RingIn(geomPoly[0], this, true);\n // copy by value\n this.bbox = {\n ll: {\n x: this.exteriorRing.bbox.ll.x,\n y: this.exteriorRing.bbox.ll.y\n },\n ur: {\n x: this.exteriorRing.bbox.ur.x,\n y: this.exteriorRing.bbox.ur.y\n }\n };\n this.interiorRings = [];\n for (let i = 1, iMax = geomPoly.length; i < iMax; i++) {\n const ring = new RingIn(geomPoly[i], this, false);\n if (ring.bbox.ll.x < this.bbox.ll.x) this.bbox.ll.x = ring.bbox.ll.x;\n if (ring.bbox.ll.y < this.bbox.ll.y) this.bbox.ll.y = ring.bbox.ll.y;\n if (ring.bbox.ur.x > this.bbox.ur.x) this.bbox.ur.x = ring.bbox.ur.x;\n if (ring.bbox.ur.y > this.bbox.ur.y) this.bbox.ur.y = ring.bbox.ur.y;\n this.interiorRings.push(ring);\n }\n this.multiPoly = multiPoly;\n }\n getSweepEvents() {\n const sweepEvents = this.exteriorRing.getSweepEvents();\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringSweepEvents = this.interiorRings[i].getSweepEvents();\n for (let j = 0, jMax = ringSweepEvents.length; j < jMax; j++) {\n sweepEvents.push(ringSweepEvents[j]);\n }\n }\n return sweepEvents;\n }\n }\n class MultiPolyIn {\n constructor(geom, isSubject) {\n if (!Array.isArray(geom)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\");\n }\n try {\n // if the input looks like a polygon, convert it to a multipolygon\n if (typeof geom[0][0][0] === \"number\") geom = [geom];\n } catch (ex) {\n // The input is either malformed or has empty arrays.\n // In either case, it will be handled later on.\n }\n this.polys = [];\n this.bbox = {\n ll: {\n x: Number.POSITIVE_INFINITY,\n y: Number.POSITIVE_INFINITY\n },\n ur: {\n x: Number.NEGATIVE_INFINITY,\n y: Number.NEGATIVE_INFINITY\n }\n };\n for (let i = 0, iMax = geom.length; i < iMax; i++) {\n const poly = new PolyIn(geom[i], this);\n if (poly.bbox.ll.x < this.bbox.ll.x) this.bbox.ll.x = poly.bbox.ll.x;\n if (poly.bbox.ll.y < this.bbox.ll.y) this.bbox.ll.y = poly.bbox.ll.y;\n if (poly.bbox.ur.x > this.bbox.ur.x) this.bbox.ur.x = poly.bbox.ur.x;\n if (poly.bbox.ur.y > this.bbox.ur.y) this.bbox.ur.y = poly.bbox.ur.y;\n this.polys.push(poly);\n }\n this.isSubject = isSubject;\n }\n getSweepEvents() {\n const sweepEvents = [];\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polySweepEvents = this.polys[i].getSweepEvents();\n for (let j = 0, jMax = polySweepEvents.length; j < jMax; j++) {\n sweepEvents.push(polySweepEvents[j]);\n }\n }\n return sweepEvents;\n }\n }\n\n class RingOut {\n /* Given the segments from the sweep line pass, compute & return a series\n * of closed rings from all the segments marked to be part of the result */\n static factory(allSegments) {\n const ringsOut = [];\n for (let i = 0, iMax = allSegments.length; i < iMax; i++) {\n const segment = allSegments[i];\n if (!segment.isInResult() || segment.ringOut) continue;\n let prevEvent = null;\n let event = segment.leftSE;\n let nextEvent = segment.rightSE;\n const events = [event];\n const startingPoint = event.point;\n const intersectionLEs = [];\n\n /* Walk the chain of linked events to form a closed ring */\n while (true) {\n prevEvent = event;\n event = nextEvent;\n events.push(event);\n\n /* Is the ring complete? */\n if (event.point === startingPoint) break;\n while (true) {\n const availableLEs = event.getAvailableLinkedEvents();\n\n /* Did we hit a dead end? This shouldn't happen.\n * Indicates some earlier part of the algorithm malfunctioned. */\n if (availableLEs.length === 0) {\n const firstPt = events[0].point;\n const lastPt = events[events.length - 1].point;\n throw new Error(`Unable to complete output ring starting at [${firstPt.x},` + ` ${firstPt.y}]. Last matching segment found ends at` + ` [${lastPt.x}, ${lastPt.y}].`);\n }\n\n /* Only one way to go, so cotinue on the path */\n if (availableLEs.length === 1) {\n nextEvent = availableLEs[0].otherSE;\n break;\n }\n\n /* We must have an intersection. Check for a completed loop */\n let indexLE = null;\n for (let j = 0, jMax = intersectionLEs.length; j < jMax; j++) {\n if (intersectionLEs[j].point === event.point) {\n indexLE = j;\n break;\n }\n }\n /* Found a completed loop. Cut that off and make a ring */\n if (indexLE !== null) {\n const intersectionLE = intersectionLEs.splice(indexLE)[0];\n const ringEvents = events.splice(intersectionLE.index);\n ringEvents.unshift(ringEvents[0].otherSE);\n ringsOut.push(new RingOut(ringEvents.reverse()));\n continue;\n }\n /* register the intersection */\n intersectionLEs.push({\n index: events.length,\n point: event.point\n });\n /* Choose the left-most option to continue the walk */\n const comparator = event.getLeftmostComparator(prevEvent);\n nextEvent = availableLEs.sort(comparator)[0].otherSE;\n break;\n }\n }\n ringsOut.push(new RingOut(events));\n }\n return ringsOut;\n }\n constructor(events) {\n this.events = events;\n for (let i = 0, iMax = events.length; i < iMax; i++) {\n events[i].segment.ringOut = this;\n }\n this.poly = null;\n }\n getGeom() {\n // Remove superfluous points (ie extra points along a straight line),\n let prevPt = this.events[0].point;\n const points = [prevPt];\n for (let i = 1, iMax = this.events.length - 1; i < iMax; i++) {\n const pt = this.events[i].point;\n const nextPt = this.events[i + 1].point;\n if (compareVectorAngles(pt, prevPt, nextPt) === 0) continue;\n points.push(pt);\n prevPt = pt;\n }\n\n // ring was all (within rounding error of angle calc) colinear points\n if (points.length === 1) return null;\n\n // check if the starting point is necessary\n const pt = points[0];\n const nextPt = points[1];\n if (compareVectorAngles(pt, prevPt, nextPt) === 0) points.shift();\n points.push(points[0]);\n const step = this.isExteriorRing() ? 1 : -1;\n const iStart = this.isExteriorRing() ? 0 : points.length - 1;\n const iEnd = this.isExteriorRing() ? points.length : -1;\n const orderedPoints = [];\n for (let i = iStart; i != iEnd; i += step) orderedPoints.push([points[i].x, points[i].y]);\n return orderedPoints;\n }\n isExteriorRing() {\n if (this._isExteriorRing === undefined) {\n const enclosing = this.enclosingRing();\n this._isExteriorRing = enclosing ? !enclosing.isExteriorRing() : true;\n }\n return this._isExteriorRing;\n }\n enclosingRing() {\n if (this._enclosingRing === undefined) {\n this._enclosingRing = this._calcEnclosingRing();\n }\n return this._enclosingRing;\n }\n\n /* Returns the ring that encloses this one, if any */\n _calcEnclosingRing() {\n // start with the ealier sweep line event so that the prevSeg\n // chain doesn't lead us inside of a loop of ours\n let leftMostEvt = this.events[0];\n for (let i = 1, iMax = this.events.length; i < iMax; i++) {\n const evt = this.events[i];\n if (SweepEvent.compare(leftMostEvt, evt) > 0) leftMostEvt = evt;\n }\n let prevSeg = leftMostEvt.segment.prevInResult();\n let prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null;\n while (true) {\n // no segment found, thus no ring can enclose us\n if (!prevSeg) return null;\n\n // no segments below prev segment found, thus the ring of the prev\n // segment must loop back around and enclose us\n if (!prevPrevSeg) return prevSeg.ringOut;\n\n // if the two segments are of different rings, the ring of the prev\n // segment must either loop around us or the ring of the prev prev\n // seg, which would make us and the ring of the prev peers\n if (prevPrevSeg.ringOut !== prevSeg.ringOut) {\n if (prevPrevSeg.ringOut.enclosingRing() !== prevSeg.ringOut) {\n return prevSeg.ringOut;\n } else return prevSeg.ringOut.enclosingRing();\n }\n\n // two segments are from the same ring, so this was a penisula\n // of that ring. iterate downward, keep searching\n prevSeg = prevPrevSeg.prevInResult();\n prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null;\n }\n }\n }\n class PolyOut {\n constructor(exteriorRing) {\n this.exteriorRing = exteriorRing;\n exteriorRing.poly = this;\n this.interiorRings = [];\n }\n addInterior(ring) {\n this.interiorRings.push(ring);\n ring.poly = this;\n }\n getGeom() {\n const geom = [this.exteriorRing.getGeom()];\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (geom[0] === null) return null;\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringGeom = this.interiorRings[i].getGeom();\n // interior ring was all (within rounding error of angle calc) colinear points\n if (ringGeom === null) continue;\n geom.push(ringGeom);\n }\n return geom;\n }\n }\n class MultiPolyOut {\n constructor(rings) {\n this.rings = rings;\n this.polys = this._composePolys(rings);\n }\n getGeom() {\n const geom = [];\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polyGeom = this.polys[i].getGeom();\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (polyGeom === null) continue;\n geom.push(polyGeom);\n }\n return geom;\n }\n _composePolys(rings) {\n const polys = [];\n for (let i = 0, iMax = rings.length; i < iMax; i++) {\n const ring = rings[i];\n if (ring.poly) continue;\n if (ring.isExteriorRing()) polys.push(new PolyOut(ring));else {\n const enclosingRing = ring.enclosingRing();\n if (!enclosingRing.poly) polys.push(new PolyOut(enclosingRing));\n enclosingRing.poly.addInterior(ring);\n }\n }\n return polys;\n }\n }\n\n /**\n * NOTE: We must be careful not to change any segments while\n * they are in the SplayTree. AFAIK, there's no way to tell\n * the tree to rebalance itself - thus before splitting\n * a segment that's in the tree, we remove it from the tree,\n * do the split, then re-insert it. (Even though splitting a\n * segment *shouldn't* change its correct position in the\n * sweep line tree, the reality is because of rounding errors,\n * it sometimes does.)\n */\n\n class SweepLine {\n constructor(queue) {\n let comparator = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Segment.compare;\n this.queue = queue;\n this.tree = new Tree(comparator);\n this.segments = [];\n }\n process(event) {\n const segment = event.segment;\n const newEvents = [];\n\n // if we've already been consumed by another segment,\n // clean up our body parts and get out\n if (event.consumedBy) {\n if (event.isLeft) this.queue.remove(event.otherSE);else this.tree.remove(segment);\n return newEvents;\n }\n const node = event.isLeft ? this.tree.add(segment) : this.tree.find(segment);\n if (!node) throw new Error(`Unable to find segment #${segment.id} ` + `[${segment.leftSE.point.x}, ${segment.leftSE.point.y}] -> ` + `[${segment.rightSE.point.x}, ${segment.rightSE.point.y}] ` + \"in SweepLine tree.\");\n let prevNode = node;\n let nextNode = node;\n let prevSeg = undefined;\n let nextSeg = undefined;\n\n // skip consumed segments still in tree\n while (prevSeg === undefined) {\n prevNode = this.tree.prev(prevNode);\n if (prevNode === null) prevSeg = null;else if (prevNode.key.consumedBy === undefined) prevSeg = prevNode.key;\n }\n\n // skip consumed segments still in tree\n while (nextSeg === undefined) {\n nextNode = this.tree.next(nextNode);\n if (nextNode === null) nextSeg = null;else if (nextNode.key.consumedBy === undefined) nextSeg = nextNode.key;\n }\n if (event.isLeft) {\n // Check for intersections against the previous segment in the sweep line\n let prevMySplitter = null;\n if (prevSeg) {\n const prevInter = prevSeg.getIntersection(segment);\n if (prevInter !== null) {\n if (!segment.isAnEndpoint(prevInter)) prevMySplitter = prevInter;\n if (!prevSeg.isAnEndpoint(prevInter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, prevInter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n }\n }\n\n // Check for intersections against the next segment in the sweep line\n let nextMySplitter = null;\n if (nextSeg) {\n const nextInter = nextSeg.getIntersection(segment);\n if (nextInter !== null) {\n if (!segment.isAnEndpoint(nextInter)) nextMySplitter = nextInter;\n if (!nextSeg.isAnEndpoint(nextInter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, nextInter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n }\n }\n\n // For simplicity, even if we find more than one intersection we only\n // spilt on the 'earliest' (sweep-line style) of the intersections.\n // The other intersection will be handled in a future process().\n if (prevMySplitter !== null || nextMySplitter !== null) {\n let mySplitter = null;\n if (prevMySplitter === null) mySplitter = nextMySplitter;else if (nextMySplitter === null) mySplitter = prevMySplitter;else {\n const cmpSplitters = SweepEvent.comparePoints(prevMySplitter, nextMySplitter);\n mySplitter = cmpSplitters <= 0 ? prevMySplitter : nextMySplitter;\n }\n\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n this.queue.remove(segment.rightSE);\n newEvents.push(segment.rightSE);\n const newEventsFromSplit = segment.split(mySplitter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n if (newEvents.length > 0) {\n // We found some intersections, so re-do the current event to\n // make sure sweep line ordering is totally consistent for later\n // use with the segment 'prev' pointers\n this.tree.remove(segment);\n newEvents.push(event);\n } else {\n // done with left event\n this.segments.push(segment);\n segment.prev = prevSeg;\n }\n } else {\n // event.isRight\n\n // since we're about to be removed from the sweep line, check for\n // intersections between our previous and next segments\n if (prevSeg && nextSeg) {\n const inter = prevSeg.getIntersection(nextSeg);\n if (inter !== null) {\n if (!prevSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, inter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n if (!nextSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, inter);\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i]);\n }\n }\n }\n }\n this.tree.remove(segment);\n }\n return newEvents;\n }\n\n /* Safely split a segment that is currently in the datastructures\n * IE - a segment other than the one that is currently being processed. */\n _splitSafely(seg, pt) {\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n // removeNode() doesn't work, so have re-find the seg\n // https://github.com/w8r/splay-tree/pull/5\n this.tree.remove(seg);\n const rightSE = seg.rightSE;\n this.queue.remove(rightSE);\n const newEvents = seg.split(pt);\n newEvents.push(rightSE);\n // splitting can trigger consumption\n if (seg.consumedBy === undefined) this.tree.add(seg);\n return newEvents;\n }\n }\n\n // Limits on iterative processes to prevent infinite loops - usually caused by floating-point math round-off errors.\n const POLYGON_CLIPPING_MAX_QUEUE_SIZE = typeof process !== \"undefined\" && process.env.POLYGON_CLIPPING_MAX_QUEUE_SIZE || 1000000;\n const POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS = typeof process !== \"undefined\" && process.env.POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS || 1000000;\n class Operation {\n run(type, geom, moreGeoms) {\n operation.type = type;\n rounder.reset();\n\n /* Convert inputs to MultiPoly objects */\n const multipolys = [new MultiPolyIn(geom, true)];\n for (let i = 0, iMax = moreGeoms.length; i < iMax; i++) {\n multipolys.push(new MultiPolyIn(moreGeoms[i], false));\n }\n operation.numMultiPolys = multipolys.length;\n\n /* BBox optimization for difference operation\n * If the bbox of a multipolygon that's part of the clipping doesn't\n * intersect the bbox of the subject at all, we can just drop that\n * multiploygon. */\n if (operation.type === \"difference\") {\n // in place removal\n const subject = multipolys[0];\n let i = 1;\n while (i < multipolys.length) {\n if (getBboxOverlap(multipolys[i].bbox, subject.bbox) !== null) i++;else multipolys.splice(i, 1);\n }\n }\n\n /* BBox optimization for intersection operation\n * If we can find any pair of multipolygons whose bbox does not overlap,\n * then the result will be empty. */\n if (operation.type === \"intersection\") {\n // TODO: this is O(n^2) in number of polygons. By sorting the bboxes,\n // it could be optimized to O(n * ln(n))\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const mpA = multipolys[i];\n for (let j = i + 1, jMax = multipolys.length; j < jMax; j++) {\n if (getBboxOverlap(mpA.bbox, multipolys[j].bbox) === null) return [];\n }\n }\n }\n\n /* Put segment endpoints in a priority queue */\n const queue = new Tree(SweepEvent.compare);\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const sweepEvents = multipolys[i].getSweepEvents();\n for (let j = 0, jMax = sweepEvents.length; j < jMax; j++) {\n queue.insert(sweepEvents[j]);\n if (queue.size > POLYGON_CLIPPING_MAX_QUEUE_SIZE) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n throw new Error(\"Infinite loop when putting segment endpoints in a priority queue \" + \"(queue size too big).\");\n }\n }\n }\n\n /* Pass the sweep line over those endpoints */\n const sweepLine = new SweepLine(queue);\n let prevQueueSize = queue.size;\n let node = queue.pop();\n while (node) {\n const evt = node.key;\n if (queue.size === prevQueueSize) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n const seg = evt.segment;\n throw new Error(`Unable to pop() ${evt.isLeft ? \"left\" : \"right\"} SweepEvent ` + `[${evt.point.x}, ${evt.point.y}] from segment #${seg.id} ` + `[${seg.leftSE.point.x}, ${seg.leftSE.point.y}] -> ` + `[${seg.rightSE.point.x}, ${seg.rightSE.point.y}] from queue.`);\n }\n if (queue.size > POLYGON_CLIPPING_MAX_QUEUE_SIZE) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n throw new Error(\"Infinite loop when passing sweep line over endpoints \" + \"(queue size too big).\");\n }\n if (sweepLine.segments.length > POLYGON_CLIPPING_MAX_SWEEPLINE_SEGMENTS) {\n // prevents an infinite loop, an otherwise common manifestation of bugs\n throw new Error(\"Infinite loop when passing sweep line over endpoints \" + \"(too many sweep line segments).\");\n }\n const newEvents = sweepLine.process(evt);\n for (let i = 0, iMax = newEvents.length; i < iMax; i++) {\n const evt = newEvents[i];\n if (evt.consumedBy === undefined) queue.insert(evt);\n }\n prevQueueSize = queue.size;\n node = queue.pop();\n }\n\n // free some memory we don't need anymore\n rounder.reset();\n\n /* Collect and compile segments we're keeping into a multipolygon */\n const ringsOut = RingOut.factory(sweepLine.segments);\n const result = new MultiPolyOut(ringsOut);\n return result.getGeom();\n }\n }\n\n // singleton available by import\n const operation = new Operation();\n\n const union = function (geom) {\n for (var _len = arguments.length, moreGeoms = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n moreGeoms[_key - 1] = arguments[_key];\n }\n return operation.run(\"union\", geom, moreGeoms);\n };\n const intersection = function (geom) {\n for (var _len2 = arguments.length, moreGeoms = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n moreGeoms[_key2 - 1] = arguments[_key2];\n }\n return operation.run(\"intersection\", geom, moreGeoms);\n };\n const xor = function (geom) {\n for (var _len3 = arguments.length, moreGeoms = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {\n moreGeoms[_key3 - 1] = arguments[_key3];\n }\n return operation.run(\"xor\", geom, moreGeoms);\n };\n const difference = function (subjectGeom) {\n for (var _len4 = arguments.length, clippingGeoms = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {\n clippingGeoms[_key4 - 1] = arguments[_key4];\n }\n return operation.run(\"difference\", subjectGeom, clippingGeoms);\n };\n var index = {\n union: union,\n intersection: intersection,\n xor: xor,\n difference: difference\n };\n\n return index;\n\n}));\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { deepEqual } from 'fast-equals';\nimport turf_bboxClip from '@turf/bbox-clip';\nimport stringify from 'fast-json-stable-stringify';\nimport polygonClipping from 'polygon-clipping';\n\nimport Protobuf from 'pbf';\nimport { VectorTile } from '@mapbox/vector-tile';\n\nimport { utilHashcode, utilRebind, utilTiler } from '../util';\n\n\nvar tiler = utilTiler().tileSize(512).margin(1);\nvar dispatch = d3_dispatch('loadedData');\nvar _vtCache;\n\n\nfunction abortRequest(controller) {\n controller.abort();\n}\n\n\nfunction vtToGeoJSON(data, tile, mergeCache) {\n var vectorTile = new VectorTile(new Protobuf(data));\n var layers = Object.keys(vectorTile.layers);\n if (!Array.isArray(layers)) { layers = [layers]; }\n\n var features = [];\n layers.forEach(function(layerID) {\n var layer = vectorTile.layers[layerID];\n if (layer) {\n for (var i = 0; i < layer.length; i++) {\n var feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n var geometry = feature.geometry;\n\n // Treat all Polygons as MultiPolygons\n if (geometry.type === 'Polygon') {\n geometry.type = 'MultiPolygon';\n geometry.coordinates = [geometry.coordinates];\n }\n\n var isClipped = false;\n\n // Clip to tile bounds\n if (geometry.type === 'MultiPolygon') {\n var featureClip = turf_bboxClip(feature, tile.extent.rectangle());\n if (!deepEqual(feature.geometry, featureClip.geometry)) {\n // feature = featureClip;\n isClipped = true;\n }\n if (!feature.geometry.coordinates.length) continue; // not actually on this tile\n if (!feature.geometry.coordinates[0].length) continue; // not actually on this tile\n }\n\n // Generate some unique IDs and add some metadata\n var featurehash = utilHashcode(stringify(feature));\n var propertyhash = utilHashcode(stringify(feature.properties || {}));\n feature.__layerID__ = layerID.replace(/[^_a-zA-Z0-9\\-]/g, '_');\n feature.__featurehash__ = featurehash;\n feature.__propertyhash__ = propertyhash;\n features.push(feature);\n\n // Clipped Polygons at same zoom with identical properties can get merged\n if (isClipped && geometry.type === 'MultiPolygon') {\n var merged = mergeCache[propertyhash];\n if (merged && merged.length) {\n var other = merged[0];\n var coords = polygonClipping.union(\n feature.geometry.coordinates,\n other.geometry.coordinates\n );\n\n if (!coords || !coords.length) {\n continue; // something failed in polygon union\n }\n\n merged.push(feature);\n for (var j = 0; j < merged.length; j++) { // all these features get...\n merged[j].geometry.coordinates = coords; // same coords\n merged[j].__featurehash__ = featurehash; // same hash, so deduplication works\n }\n } else {\n mergeCache[propertyhash] = [feature];\n }\n }\n }\n }\n });\n\n return features;\n}\n\n\nfunction loadTile(source, tile) {\n if (source.loaded[tile.id] || source.inflight[tile.id]) return;\n\n var url = source.template\n .replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n // TMS-flipped y coordinate\n .replace(/\\{[t-]y\\}/, Math.pow(2, tile.xyz[2]) - tile.xyz[1] - 1)\n .replace(/\\{z(oom)?\\}/, tile.xyz[2])\n .replace(/\\{switch:([^}]+)\\}/, function(s, r) {\n var subdomains = r.split(',');\n return subdomains[(tile.xyz[0] + tile.xyz[1]) % subdomains.length];\n });\n\n\n var controller = new AbortController();\n source.inflight[tile.id] = controller;\n\n fetch(url, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n source.loaded[tile.id] = [];\n delete source.inflight[tile.id];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (!data) {\n throw new Error('No Data');\n }\n\n var z = tile.xyz[2];\n if (!source.canMerge[z]) {\n source.canMerge[z] = {}; // initialize mergeCache\n }\n\n source.loaded[tile.id] = vtToGeoJSON(data, tile, source.canMerge[z]);\n dispatch.call('loadedData');\n })\n .catch(function() {\n source.loaded[tile.id] = [];\n delete source.inflight[tile.id];\n });\n}\n\n\nexport default {\n\n init: function() {\n if (!_vtCache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n\n reset: function() {\n for (var sourceID in _vtCache) {\n var source = _vtCache[sourceID];\n if (source && source.inflight) {\n Object.values(source.inflight).forEach(abortRequest);\n }\n }\n\n _vtCache = {};\n },\n\n\n addSource: function(sourceID, template) {\n _vtCache[sourceID] = { template: template, inflight: {}, loaded: {}, canMerge: {} };\n return _vtCache[sourceID];\n },\n\n\n data: function(sourceID, projection) {\n var source = _vtCache[sourceID];\n if (!source) return [];\n\n var tiles = tiler.getTiles(projection);\n var seen = {};\n var results = [];\n\n for (var i = 0; i < tiles.length; i++) {\n var features = source.loaded[tiles[i].id];\n if (!features || !features.length) continue;\n\n for (var j = 0; j < features.length; j++) {\n var feature = features[j];\n var hash = feature.__featurehash__;\n if (seen[hash]) continue;\n seen[hash] = true;\n\n // return a shallow copy, because the hash may change\n // later if this feature gets merged with another\n results.push(Object.assign({}, feature)); // shallow copy\n }\n }\n\n return results;\n },\n\n\n loadTiles: function(sourceID, template, projection) {\n var source = _vtCache[sourceID];\n if (!source) {\n source = this.addSource(sourceID, template);\n }\n\n var tiles = tiler.getTiles(projection);\n\n // abort inflight requests that are no longer needed\n Object.keys(source.inflight).forEach(function(k) {\n var wanted = tiles.find(function(tile) { return k === tile.id; });\n if (!wanted) {\n abortRequest(source.inflight[k]);\n delete source.inflight[k];\n }\n });\n\n tiles.forEach(function(tile) {\n loadTile(source, tile);\n });\n },\n\n\n cache: function() {\n return _vtCache;\n }\n\n};\n", "import { json as d3_json } from 'd3-fetch';\n\nimport { utilQsString } from '../util';\nimport { localizer } from '../core/localizer';\n\nvar apibase = 'https://www.wikidata.org/w/api.php?';\nvar _wikidataCache = {};\n\n\nexport default {\n\n init: function() {},\n\n reset: function() {\n _wikidataCache = {};\n },\n\n\n // Search for Wikidata items matching the query\n itemsForSearchQuery: function(query, callback, language) {\n if (!query) {\n if (callback) callback('No query', {});\n return;\n }\n\n var lang = this.languagesToQuery()[0];\n\n var url = apibase + utilQsString({\n action: 'wbsearchentities',\n format: 'json',\n formatversion: 2,\n search: query,\n type: 'item',\n // the language to search\n language: language || lang,\n // the language for the label and description in the result\n uselang: lang,\n limit: 10,\n origin: '*'\n });\n\n d3_json(url)\n .then(result => {\n if (result && result.error) {\n if (result.error.code === 'badvalue' &&\n result.error.info.includes(lang) &&\n !language && lang.includes('-')) {\n // retry without \"country suffix\" region subtag\n this.itemsForSearchQuery(query, callback, lang.split('-')[0]);\n return;\n } else {\n throw new Error(result.error);\n }\n }\n if (callback) callback(null, result.search || {});\n })\n .catch(function(err) {\n if (callback) callback(err.message, {});\n });\n },\n\n\n // Given a Wikipedia language and article title,\n // return an array of corresponding Wikidata entities.\n itemsByTitle: function(lang, title, callback) {\n if (!title) {\n if (callback) callback('No title', {});\n return;\n }\n\n lang = lang || 'en';\n var url = apibase + utilQsString({\n action: 'wbgetentities',\n format: 'json',\n formatversion: 2,\n sites: lang.replace(/-/g, '_') + 'wiki',\n titles: title,\n languages: 'en', // shrink response by filtering to one language\n origin: '*'\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n }\n if (callback) callback(null, result.entities || {});\n })\n .catch(function(err) {\n if (callback) callback(err.message, {});\n });\n },\n\n\n languagesToQuery: function() {\n return localizer.localeCodes().map(function(code) {\n return code.toLowerCase();\n }).filter(function(code) {\n // HACK: en-us isn't a wikidata language. We should really be filtering by\n // the languages known to be supported by wikidata.\n return code !== 'en-us';\n });\n },\n\n\n entityByQID: function(qid, callback) {\n if (!qid) {\n callback('No qid', {});\n return;\n }\n if (_wikidataCache[qid]) {\n if (callback) callback(null, _wikidataCache[qid]);\n return;\n }\n\n var langs = this.languagesToQuery();\n var url = apibase + utilQsString({\n action: 'wbgetentities',\n format: 'json',\n formatversion: 2,\n ids: qid,\n props: 'labels|descriptions|claims|sitelinks',\n sitefilter: langs.map(function(d) { return d + 'wiki'; }).join('|'),\n languages: langs.join('|'),\n languagefallback: 1,\n origin: '*'\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n }\n if (callback) callback(null, result.entities[qid] || {});\n })\n .catch(function(err) {\n if (callback) callback(err.message, {});\n });\n },\n\n\n // Pass `params` object of the form:\n // {\n // qid: 'string' // brand wikidata (e.g. 'Q37158')\n // }\n //\n // Get an result object used to display tag documentation\n // {\n // title: 'string',\n // description: 'string',\n // editURL: 'string',\n // imageURL: 'string',\n // wiki: { title: 'string', text: 'string', url: 'string' }\n // }\n //\n getDocs: function(params, callback) {\n var langs = this.languagesToQuery();\n this.entityByQID(params.qid, function(err, entity) {\n if (err || !entity) {\n callback(err || 'No entity');\n return;\n }\n\n var i;\n var description;\n for (i in langs) {\n let code = langs[i];\n if (entity.descriptions[code] && entity.descriptions[code].language === code) {\n description = entity.descriptions[code];\n break;\n }\n }\n if (!description && Object.values(entity.descriptions).length) description = Object.values(entity.descriptions)[0];\n\n // prepare result\n var result = {\n title: entity.id,\n description: selection => selection.text(description ? description.value : ''),\n descriptionLocaleCode: description ? description.language : '',\n editURL: 'https://www.wikidata.org/wiki/' + entity.id\n };\n\n // add image\n if (entity.claims) {\n var imageroot = 'https://commons.wikimedia.org/w/index.php';\n var props = ['P154','P18']; // logo image, image\n var prop, image;\n for (i = 0; i < props.length; i++) {\n prop = entity.claims[props[i]];\n if (prop && Object.keys(prop).length > 0) {\n image = prop[Object.keys(prop)[0]].mainsnak.datavalue.value;\n if (image) {\n result.imageURL = imageroot + '?' + utilQsString({\n title: 'Special:Redirect/file/' + image,\n width: 400\n });\n break;\n }\n }\n }\n }\n\n if (entity.sitelinks) {\n var englishLocale = localizer.languageCode().toLowerCase() === 'en';\n\n // must be one of these that we requested..\n for (i = 0; i < langs.length; i++) { // check each, in order of preference\n var w = langs[i] + 'wiki';\n if (entity.sitelinks[w]) {\n var title = entity.sitelinks[w].title;\n var tKey = 'inspector.wiki_reference';\n if (!englishLocale && langs[i] === 'en') { // user's locale isn't English but\n tKey = 'inspector.wiki_en_reference'; // we are sending them to enwiki anyway..\n }\n\n result.wiki = {\n title: title,\n text: tKey,\n url: 'https://' + langs[i] + '.wikipedia.org/wiki/' + title.replace(/ /g, '_')\n };\n break;\n }\n }\n }\n\n callback(null, result);\n });\n }\n\n};\n", "import { json as d3_json } from 'd3-fetch';\n\nimport { utilQsString } from '../util';\n\n\nvar endpoint = 'https://en.wikipedia.org/w/api.php?';\n\nexport default {\n\n init: function() {},\n reset: function() {},\n\n\n search: function(lang, query, callback) {\n if (!query) {\n if (callback) callback('No Query', []);\n return;\n }\n\n lang = lang || 'en';\n var url = endpoint.replace('en', lang) +\n utilQsString({\n action: 'query',\n list: 'search',\n srlimit: '10',\n srinfo: 'suggestion',\n format: 'json',\n origin: '*',\n srsearch: query\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n } else if (!result || !result.query || !result.query.search) {\n throw new Error('No Results');\n }\n if (callback) {\n var titles = result.query.search.map(function(d) { return d.title; });\n callback(null, titles);\n }\n })\n .catch(function(err) {\n if (callback) callback(err, []);\n });\n },\n\n\n suggestions: function(lang, query, callback) {\n if (!query) {\n if (callback) callback('', []);\n return;\n }\n\n lang = lang || 'en';\n var url = endpoint.replace('en', lang) +\n utilQsString({\n action: 'opensearch',\n namespace: 0,\n suggest: '',\n format: 'json',\n origin: '*',\n search: query\n });\n\n d3_json(url)\n .then(function(result) {\n if (result && result.error) {\n throw new Error(result.error);\n } else if (!result || result.length < 2) {\n throw new Error('No Results');\n }\n if (callback) callback(null, result[1] || []);\n })\n .catch(function(err) {\n if (callback) callback(err.message, []);\n });\n },\n\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport { deepEqual } from 'fast-equals';\nimport Protobuf from 'pbf';\nimport RBush from 'rbush';\nimport { VectorTile } from '@mapbox/vector-tile';\n\nimport { utilRebind, utilTiler, utilQsString, utilStringQs, utilSetTransform } from '../util';\nimport { geoExtent } from '../geo';\nimport { services } from './';\nimport { searchLimited } from '../util/partition';\nimport { localeDateString } from '../util/date';\n\nconst apiUrl = 'https://end.mapilio.com';\nconst imageBaseUrl = 'https://cdn.mapilio.com/im';\nconst baseTileUrl = 'https://geo.mapilio.com/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=mapilio:';\nconst pointLayer = 'map_points';\nconst lineLayer = 'map_roads_line';\nconst tileStyle = '&STYLE=&TILEMATRIX=EPSG:900913:{z}&TILEMATRIXSET=EPSG:900913&FORMAT=application/vnd.mapbox-vector-tile&TILECOL={x}&TILEROW={y}';\n\nconst minZoom = 14;\nconst dispatch = d3_dispatch('loadedImages', 'loadedLines');\nconst imgZoom = d3_zoom()\n .extent([[0, 0], [320, 240]])\n .translateExtent([[0, 0], [320, 240]])\n .scaleExtent([1, 15]);\nconst pannellumViewerCSS = 'pannellum/pannellum.css';\nconst pannellumViewerJS = 'pannellum/pannellum.js';\nconst resolution = 1080;\n\nlet _activeImage;\nlet _cache;\nlet _loadViewerPromise;\nlet _pannellumViewer;\nlet _sceneOptions = {\n showFullscreenCtrl: false,\n autoLoad: true,\n yaw: 0,\n minHfov: 10,\n maxHfov: 90,\n hfov: 60,\n};\nlet _currScene = 0;\n\n// Load all data for the specified type from Mapilio vector tiles\nfunction loadTiles(which, url, maxZoom, projection) {\n const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);\n const tiles = tiler.getTiles(projection);\n\n tiles.forEach(function(tile) {\n loadTile(which, url, tile);\n });\n}\n\n\n// Load all data for the specified type from one vector tile\nfunction loadTile(which, url, tile) {\n const cache = _cache.requests;\n const tileId = `${tile.id}-${which}`;\n if (cache.loaded[tileId] || cache.inflight[tileId]) return;\n const controller = new AbortController();\n cache.inflight[tileId] = controller;\n const requestUrl = url.replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n .replace('{z}', tile.xyz[2]);\n\n fetch(requestUrl, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (data.byteLength === 0) {\n throw new Error('No Data');\n }\n\n loadTileDataToCache(data, tile, which);\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n } else {\n dispatch.call('loadedLines');\n }\n })\n .catch(function (e) {\n if (e.message === 'No Data') {\n cache.loaded[tileId] = true;\n } else {\n console.error(e); // eslint-disable-line no-console\n }\n });\n}\n\n\n// Load the data from the vector tile into cache\nfunction loadTileDataToCache(data, tile) {\n const vectorTile = new VectorTile(new Protobuf(data));\n if (vectorTile.layers.hasOwnProperty(pointLayer)) {\n const features = [];\n const cache = _cache.images;\n const layer = vectorTile.layers[pointLayer];\n\n for (let i = 0; i < layer.length; i++) {\n const feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n const loc = feature.geometry.coordinates;\n\n let resolutionArr = feature.properties.resolution.split('x');\n let sourceWidth = Math.max(resolutionArr[0], resolutionArr[1]);\n let sourceHeight = Math.min(resolutionArr[0] ,resolutionArr[1]);\n let isPano = sourceWidth % sourceHeight === 0;\n\n const d = {\n service: 'photo',\n loc: loc,\n capture_time: feature.properties.capture_time,\n created_by_id: feature.properties.created_by_id,\n id: feature.properties.id,\n sequence_id: feature.properties.sequence_uuid,\n heading: feature.properties.heading,\n resolution: feature.properties.resolution,\n isPano: isPano\n };\n cache.forImageId[d.id] = d;\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty(lineLayer)) {\n const cache = _cache.sequences;\n const layer = vectorTile.layers[lineLayer];\n\n for (let i = 0; i < layer.length; i++) {\n const feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n if (cache.lineString[feature.properties.sequence_uuid]) {\n const cacheEntry = cache.lineString[feature.properties.sequence_uuid];\n if (cacheEntry.some(f => {\n // for some reason, mapilio sometimes returns a large amount of duplicate\n // sequence lines, causing very poor performance. this de-duplicates them,\n // see https://github.com/openstreetmap/iD/issues/10532\n const cachedCoords = f.geometry.coordinates;\n const featureCoords = feature.geometry.coordinates;\n return deepEqual(cachedCoords, featureCoords);\n })) continue;\n cacheEntry.push(feature);\n } else {\n cache.lineString[feature.properties.sequence_uuid] = [feature];\n }\n }\n }\n\n}\n\nfunction getImageData(imageId, sequenceId) {\n\n return fetch(apiUrl + `/api/sequence-detail?sequence_uuid=${sequenceId}`, {method: 'GET'})\n .then(function (response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n })\n .then(function (data) {\n let index = data.data.findIndex((feature) => feature.id === imageId);\n const {filename, uploaded_hash} = data.data[index];\n _sceneOptions.panorama = imageBaseUrl + '/' + uploaded_hash + '/' + filename + '/' + resolution;\n });\n}\n\nfunction getUserData(userId) {\n return fetch(apiUrl + `/api/search-user?options[parameters][id]=${userId}`, {method: 'GET'})\n .then(function (response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n return response.json();\n })\n .then(function (data) {\n return data.data[0].username;\n });\n}\n\n\nexport default {\n // Initialize Mapilio\n init: function() {\n if (!_cache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n // Reset cache and state\n reset: function() {\n if (_cache) {\n Object.values(_cache.requests.inflight).forEach(function(request) { request.abort(); });\n }\n\n _cache = {\n images: { rtree: new RBush(), forImageId: {} },\n sequences: { rtree: new RBush(), lineString: {} },\n requests: { loaded: {}, inflight: {} }\n };\n },\n\n // Get visible images\n images: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _cache.images.rtree);\n },\n\n cachedImage: function(imageKey) {\n return _cache.images.forImageId[imageKey];\n },\n\n\n // Load images in the visible area\n loadImages: function(projection) {\n let url = baseTileUrl + pointLayer + tileStyle;\n loadTiles('images', url, 14, projection);\n },\n\n // Load line in the visible area\n loadLines: function(projection) {\n let url = baseTileUrl + lineLayer + tileStyle;\n loadTiles('line', url, 14, projection);\n },\n\n // Get visible sequences\n sequences: function(projection) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const sequenceIds = {};\n let lineStrings = [];\n\n _cache.images.rtree.search(bbox)\n .forEach(function(d) {\n if (d.data.sequence_id) {\n sequenceIds[d.data.sequence_id] = true;\n }\n });\n\n Object.keys(sequenceIds).forEach(function(sequenceId) {\n if (_cache.sequences.lineString[sequenceId]) {\n lineStrings = lineStrings.concat(_cache.sequences.lineString[sequenceId]);\n }\n });\n\n return lineStrings;\n },\n\n // Set the currently visible image\n setActiveImage: function(image) {\n if (image) {\n _activeImage = {\n id: image.id,\n sequence_id: image.sequence_id\n };\n } else {\n _activeImage = null;\n }\n },\n\n\n // Update the currently highlighted sequence and selected bubble.\n setStyles: function(context, hovered) {\n const hoveredImageId = hovered && hovered.id;\n const hoveredSequenceId = hovered && hovered.sequence_id;\n const selectedSequenceId = _activeImage && _activeImage.sequence_id;\n const selectedImageId = _activeImage && _activeImage.id;\n\n const markers = context.container().selectAll('.layer-mapilio .viewfield-group');\n const sequences = context.container().selectAll('.layer-mapilio .sequence');\n\n markers.classed('highlighted', function(d) { return d.id === hoveredImageId; })\n .classed('hovered', function(d) { return d.id === hoveredImageId; })\n .classed('currentView', function(d) { return d.id === selectedImageId; });\n\n sequences.classed('highlighted', function(d) { return d.properties.sequence_uuid === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.sequence_uuid === selectedSequenceId; });\n\n return this;\n },\n\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'mapilio/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n initViewer: function () {\n if (!window.pannellum) return;\n if (_pannellumViewer) return;\n\n _currScene += 1;\n const sceneID = _currScene.toString();\n const options = {\n 'default': { firstScene: sceneID },\n scenes: {}\n };\n options.scenes[sceneID] = _sceneOptions;\n\n _pannellumViewer = window.pannellum.viewer('ideditor-viewer-mapilio-pnlm', options);\n },\n\n selectImage: function (context, id) {\n\n let that = this;\n\n let d = this.cachedImage(id);\n\n this.setActiveImage(d);\n\n this.updateUrlImage(d.id);\n\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null);\n\n if (!d) return this;\n\n let wrap = context.container().select('.photoviewer .mapilio-wrapper');\n let attribution = wrap.selectAll('.photo-attribution').text('\\u00A0');\n\n getUserData(d.created_by_id).then((username) => {\n if (username) {\n attribution\n .append('span')\n .attr('class', 'captured_by')\n .text('@' + username);\n attribution\n .append('span')\n .text('|');\n }\n }).finally(() => {\n if (d.capture_time) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeDateString(d.capture_time));\n attribution\n .append('span')\n .text('|');\n }\n attribution\n .append('a')\n .attr('class', 'image-link')\n .attr('target', '_blank')\n .attr('href', `https://mapilio.com/app?lat=${d.loc[1]}&lng=${d.loc[0]}&zoom=17&pId=${d.id}`)\n .text('mapilio.com');\n });\n\n wrap\n .transition()\n .duration(100)\n .call(imgZoom.transform, d3_zoomIdentity);\n\n wrap\n .selectAll('img')\n .remove();\n\n wrap\n .selectAll('button.back')\n .classed('hide', !_cache.images.forImageId.hasOwnProperty(+id - 1));\n wrap\n .selectAll('button.forward')\n .classed('hide', !_cache.images.forImageId.hasOwnProperty(+id + 1));\n\n\n getImageData(d.id,d.sequence_id).then(function () {\n\n if (d.isPano) {\n if (!_pannellumViewer) {\n that.initViewer();\n } else {\n // make a new scene\n _currScene += 1;\n let sceneID = _currScene.toString();\n _pannellumViewer\n .addScene(sceneID, _sceneOptions)\n .loadScene(sceneID);\n\n // remove previous scene\n if (_currScene > 2) {\n sceneID = (_currScene - 1).toString();\n _pannellumViewer\n .removeScene(sceneID);\n }\n }\n } else {\n // make non-panoramic photo viewer\n that.initOnlyPhoto(context);\n }\n });\n\n return this;\n },\n\n initOnlyPhoto: function (context) {\n\n if (_pannellumViewer) {\n _pannellumViewer.destroy();\n _pannellumViewer = null;\n }\n\n let wrap = context.container().select('#ideditor-viewer-mapilio-simple');\n\n let imgWrap = wrap.select('img');\n\n if (!imgWrap.empty()) {\n imgWrap.attr('src',_sceneOptions.panorama);\n } else {\n wrap.append('img')\n .attr('src',_sceneOptions.panorama);\n }\n\n },\n\n ensureViewerLoaded: function(context) {\n\n let that = this;\n\n let imgWrap = context.container().select('#ideditor-viewer-mapilio-simple > img');\n\n if (!imgWrap.empty()) {\n imgWrap.remove();\n }\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n let wrap = context.container().select('.photoviewer').selectAll('.mapilio-wrapper')\n .data([0]);\n\n let wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper mapilio-wrapper')\n .classed('hide', true)\n .on('dblclick.zoom', null);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n const controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls-mapilio');\n\n controlsEnter\n .append('button')\n .classed('back', true)\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .classed('forward', true)\n .on('click.forward', step(1))\n .text('\u25BA');\n\n wrapEnter\n .append('div')\n .attr('id', 'ideditor-viewer-mapilio-pnlm');\n\n wrapEnter\n .append('div')\n .attr('id', 'ideditor-viewer-mapilio-simple-wrap')\n .call(imgZoom.on('zoom', zoomPan))\n .append('div')\n .attr('id', 'ideditor-viewer-mapilio-simple');\n\n\n\n // Register viewer resize handler\n context.ui().photoviewer.on('resize.mapilio', () => {\n if (_pannellumViewer) {\n _pannellumViewer.resize();\n }\n });\n\n _loadViewerPromise = new Promise((resolve, reject) => {\n let loadedCount = 0;\n function loaded() {\n loadedCount += 1;\n\n // wait until both files are loaded\n if (loadedCount === 2) resolve();\n }\n\n const head = d3_select('head');\n\n // load pannellum-viewercss\n head.selectAll('#ideditor-mapilio-viewercss')\n .data([0])\n .enter()\n .append('link')\n .attr('id', 'ideditor-mapilio-viewercss')\n .attr('rel', 'stylesheet')\n .attr('crossorigin', 'anonymous')\n .attr('href', context.asset(pannellumViewerCSS))\n .on('load.serviceMapilio', loaded)\n .on('error.serviceMapilio', function() {\n reject();\n });\n\n // load pannellum-viewerjs\n head.selectAll('#ideditor-mapilio-viewerjs')\n .data([0])\n .enter()\n .append('script')\n .attr('id', 'ideditor-mapilio-viewerjs')\n .attr('crossorigin', 'anonymous')\n .attr('src', context.asset(pannellumViewerJS))\n .on('load.serviceMapilio', loaded)\n .on('error.serviceMapilio', function() {\n reject();\n });\n })\n .catch(function() {\n _loadViewerPromise = null;\n });\n\n function step(stepBy) {\n return function () {\n if (!_activeImage) return;\n const imageId = _activeImage.id;\n\n const nextIndex = imageId + stepBy;\n if (!nextIndex) return;\n\n const nextImage = _cache.images.forImageId[nextIndex];\n\n context.map().centerEase(nextImage.loc);\n\n that.selectImage(context, nextImage.id);\n };\n }\n\n function zoomPan(d3_event) {\n var t = d3_event.transform;\n context.container().select('.photoviewer #ideditor-viewer-mapilio-simple')\n .call(utilSetTransform, t.x, t.y, t.k);\n }\n\n return _loadViewerPromise;\n },\n\n showViewer:function (context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.mapilio-wrapper.hide').size();\n\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.mapilio-wrapper')\n .classed('hide', false);\n }\n\n return this;\n },\n\n /**\n * hideViewer()\n */\n hideViewer: function (context) {\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n\n this.updateUrlImage(null);\n\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n this.setActiveImage();\n return this.setStyles(context, null);\n },\n\n // Return the current cache\n cache: function() {\n return _cache;\n }\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport Protobuf from 'pbf';\nimport RBush from 'rbush';\nimport { VectorTile } from '@mapbox/vector-tile';\nimport { utilRebind, utilTiler, utilQsString, utilStringQs, utilUniqueDomId } from '../util';\nimport { geoExtent } from '../geo';\nimport { t } from '../core/localizer';\nimport { pannellumPhotoFrame } from './pannellum_photo';\nimport { planePhotoFrame } from './plane_photo';\nimport { services } from './';\nimport { partitionViewport } from '../util/partition';\nimport { localeDateString } from '../util/date';\n\n\nconst apiUrl = 'https://api.panoramax.xyz/';\nconst tileUrl = apiUrl + 'api/map/{z}/{x}/{y}.mvt';\nconst imageDataUrl = apiUrl + 'api/collections/{collectionId}/items/{itemId}';\nconst sequenceDataUrl = apiUrl + 'api/collections/{collectionId}/items?limit=1000';\nconst userIdUrl = apiUrl + 'api/users/search?q={username}';\nconst usernameURL = apiUrl + 'api/users/{userId}';\nconst viewerUrl = apiUrl;\n\nconst highDefinition = 'hd';\nconst standardDefinition = 'sd';\n\nconst pictureLayer = 'pictures';\nconst sequenceLayer = 'sequences';\n\nconst minZoom = 10;\nconst imageMinZoom = 15;\nconst lineMinZoom = 10;\nconst dispatch = d3_dispatch('loadedImages', 'loadedLines', 'viewerChanged');\n\nlet _cache;\nlet _loadViewerPromise;\nlet _definition = standardDefinition;\nlet _isHD = false;\n\nlet _planeFrame;\nlet _pannellumFrame;\nlet _currentFrame;\n\nlet _currentScene = {\n currentImage : null,\n nextImage : null,\n prevImage : null\n};\n\nlet _activeImage;\nlet _isViewerOpen = false;\n\n\n/**\n * Return no more than `limit` results per partition.\n * @param {number} limit Number of maximum objects to return\n * @param {*} projection Current projection\n * @param {*} rtree The cache\n * @returns Data found\n */\nfunction searchLimited(limit, projection, rtree) {\n limit = limit || 5;\n\n return partitionViewport(projection)\n .reduce(function(result, extent) {\n let found = rtree.search(extent.bbox());\n const spacing = Math.max(1, Math.floor(found.length / limit));\n found = found\n .filter((d, idx) => idx % spacing === 0 ||\n d.data.id === _activeImage?.id)\n .sort((a, b) => {\n if (a.data.id === _activeImage?.id) return -1;\n if (b.data.id === _activeImage?.id) return 1;\n return 0;\n })\n .slice(0, limit)\n .map(d => d.data);\n\n return (found.length ? result.concat(found) : result);\n }, []);\n}\n\n/**\n * Load all data for the specified type from Panoramax vector tiles\n * @param {string} which Either 'images' or 'lines'\n * @param {string} url Tile endpoint\n * @param {number} maxZoom Maximum zoom out\n * @param {*} projection Current projection\n * @param {number} zoom current zoom\n */\nfunction loadTiles(which, url, maxZoom, projection, zoom) {\n const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);\n const tiles = tiler.getTiles(projection);\n\n tiles.forEach(function(tile) {\n loadTile(which, url, tile, zoom);\n });\n}\n\n/**\n * Load all data for the specified type from one vector tile\n * @param {*} which Either 'images' or 'lines'\n * @param {*} url Tile endpoint\n * @param {*} tile Current tile\n * @param {*} zoom Current zoom\n */\nfunction loadTile(which, url, tile, zoom) {\n const cache = _cache.requests;\n const tileId = `${tile.id}-${which}`;\n if (cache.loaded[tileId] || cache.inflight[tileId]) return;\n const controller = new AbortController();\n cache.inflight[tileId] = controller;\n const requestUrl = url.replace('{x}', tile.xyz[0])\n .replace('{y}', tile.xyz[1])\n .replace('{z}', tile.xyz[2]);\n\n fetch(requestUrl, { signal: controller.signal })\n .then(function(response) {\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n cache.loaded[tileId] = true;\n delete cache.inflight[tileId];\n return response.arrayBuffer();\n })\n .then(function(data) {\n if (data.byteLength === 0) {\n throw new Error('No Data');\n }\n\n loadTileDataToCache(data, tile, zoom);\n\n if (which === 'images') {\n dispatch.call('loadedImages');\n } else {\n dispatch.call('loadedLines');\n }\n })\n .catch(function (e) {\n if (e.message === 'No Data') {\n cache.loaded[tileId] = true;\n } else {\n console.error(e); // eslint-disable-line no-console\n }\n });\n}\n\n/**\n * Fetches all data for the specified tile and adds them to cache\n * @param {*} data Tile data\n * @param {*} tile Current tile\n * @param {*} zoom Current zoom\n */\nfunction loadTileDataToCache(data, tile, zoom) {\n const vectorTile = new VectorTile(new Protobuf(data));\n\n let features,\n cache,\n layer,\n i,\n feature,\n loc,\n d;\n\n if (vectorTile.layers.hasOwnProperty(pictureLayer)) {\n features = [];\n cache = _cache.images;\n layer = vectorTile.layers[pictureLayer];\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n loc = feature.geometry.coordinates;\n\n d = {\n service: 'photo',\n loc: loc,\n capture_time: feature.properties.ts,\n capture_time_parsed: new Date(feature.properties.ts),\n id: feature.properties.id,\n account_id: feature.properties.account_id,\n sequence_id: feature.properties.first_sequence,\n heading: parseInt(feature.properties.heading, 10),\n image_path: '',\n isPano: feature.properties.type === 'equirectangular',\n model: feature.properties.model,\n };\n cache.forImageId[d.id] = d;\n features.push({\n minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d\n });\n }\n if (cache.rtree) {\n cache.rtree.load(features);\n }\n }\n\n if (vectorTile.layers.hasOwnProperty(sequenceLayer)) {\n\n cache = _cache.sequences;\n\n if (zoom >= lineMinZoom && zoom < imageMinZoom) cache = _cache.mockSequences;\n\n layer = vectorTile.layers[sequenceLayer];\n\n for (i = 0; i < layer.length; i++) {\n feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);\n if (cache.lineString[feature.properties.id]) {\n cache.lineString[feature.properties.id].push(feature);\n } else {\n cache.lineString[feature.properties.id] = [feature];\n }\n }\n }\n}\n\n/**\n * Fetches the username from Panoramax\n * @param {string} userId\n * @returns the username\n */\nasync function getUsername(userId) {\n const cache = _cache.users;\n if (cache[userId]) return cache[userId].name;\n\n const requestUrl = usernameURL.replace('{userId}', userId);\n\n const response = await fetch(requestUrl, { method: 'GET' });\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n const data = await response.json();\n cache[userId] = data;\n\n return data.name;\n}\n\nexport default {\n init: function() {\n if (!_cache) {\n this.reset();\n }\n\n this.event = utilRebind(this, dispatch, 'on');\n },\n\n reset: function() {\n if (_cache) {\n Object.values(_cache.requests.inflight).forEach(function(request) { request.abort(); });\n }\n\n _cache = {\n images: { rtree: new RBush(), forImageId: {} },\n sequences: { rtree: new RBush(), lineString: {}, items: {} },\n users: {},\n mockSequences: { rtree: new RBush(), lineString: {} },\n requests: { loaded: {}, inflight: {} }\n };\n },\n\n /**\n * Get visible images from cache\n * @param {*} projection Current Projection\n * @returns images data for the current projection\n */\n images: function(projection) {\n const limit = 5;\n return searchLimited(limit, projection, _cache.images.rtree);\n },\n\n /**\n * Get a specific image from cache\n * @param {*} imageKey the image id\n * @returns\n */\n cachedImage: function(imageKey) {\n return _cache.images.forImageId[imageKey];\n },\n\n /**\n * Fetches images data for the visible area\n * @param {*} projection Current Projection\n */\n loadImages: function(projection) {\n loadTiles('images', tileUrl, imageMinZoom, projection);\n },\n\n /**\n * Fetches sequences data for the visible area\n * @param {*} projection Current Projection\n */\n loadLines: function(projection, zoom) {\n loadTiles('line', tileUrl, lineMinZoom, projection, zoom);\n },\n\n /**\n * Fetches all possible userIDs from Panoramax\n * @param {string} usernames one or multiple usernames\n * @returns userIDs\n */\n getUserIds: async function(usernames) {\n const requestUrls = usernames.map(username =>\n userIdUrl.replace('{username}', username));\n\n const responses = await Promise.all(requestUrls.map(requestUrl =>\n fetch(requestUrl, { method: 'GET' })));\n if (responses.some(response => !response.ok)) {\n const response = responses.find(response => !response.ok);\n throw new Error(response.status + ' ' + response.statusText);\n }\n const data = await Promise.all(responses.map(response => response.json()));\n // in panoramax, a username can have multiple ids, when the same name is\n // used on different servers\n return data.flatMap((d, i) => d.features.filter(f => f.name === usernames[i]).map(f => f.id));\n },\n\n /**\n * Get visible sequences from cache\n * @param {*} projection Current Projection\n * @param {number} zoom Current zoom (if zoom < `lineMinZoom` less accurate lines will be drawn)\n * @returns sequences data for the current projection\n */\n sequences: function(projection, zoom) {\n const viewport = projection.clipExtent();\n const min = [viewport[0][0], viewport[1][1]];\n const max = [viewport[1][0], viewport[0][1]];\n const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();\n const sequenceIds = {};\n let lineStrings = [];\n\n if (zoom >= imageMinZoom){\n _cache.images.rtree.search(bbox).forEach(function(d) {\n if (d.data.sequence_id) {\n sequenceIds[d.data.sequence_id] = true;\n }\n });\n Object.keys(sequenceIds).forEach(function(sequenceId) {\n if (_cache.sequences.lineString[sequenceId]) {\n lineStrings = lineStrings.concat(_cache.sequences.lineString[sequenceId]);\n }\n });\n return lineStrings;\n }\n if (zoom >= lineMinZoom){\n Object.keys(_cache.mockSequences.lineString).forEach(function(sequenceId) {\n lineStrings = lineStrings.concat(_cache.mockSequences.lineString[sequenceId]);\n });\n }\n return lineStrings;\n },\n\n /**\n * Updates the data for the currently visible image\n * @param {*} image Image data\n */\n setActiveImage: function(image) {\n if (image && image.id && image.sequence_id) {\n _activeImage = {\n id: image.id,\n sequence_id: image.sequence_id,\n loc: image.loc\n };\n } else {\n _activeImage = null;\n }\n },\n\n getActiveImage: function(){\n return _activeImage;\n },\n\n /**\n * Update the currently highlighted sequence and selected bubble\n * @param {*} context Current HTML context\n * @param {*} [hovered] The hovered bubble image\n */\n setStyles: function(context, hovered) {\n const hoveredImageId = hovered && hovered.id;\n const hoveredSequenceId = hovered && hovered.sequence_id;\n const selectedSequenceId = _activeImage && _activeImage.sequence_id;\n const selectedImageId = _activeImage && _activeImage.id;\n\n const markers = context.container().selectAll('.layer-panoramax .viewfield-group');\n const sequences = context.container().selectAll('.layer-panoramax .sequence');\n\n markers\n .classed('highlighted', function(d) { return d.sequence_id === selectedSequenceId || d.id === hoveredImageId; })\n .classed('hovered', function(d) { return d.id === hoveredImageId; })\n .classed('currentView', function(d) { return d.id === selectedImageId; });\n\n sequences\n .classed('highlighted', function(d) { return d.properties.id === hoveredSequenceId; })\n .classed('currentView', function(d) { return d.properties.id === selectedSequenceId; });\n\n // update viewfields if needed\n context.container().selectAll('.layer-panoramax .viewfield-group .viewfield')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n let d = this.parentNode.__data__;\n if (d.isPano && d.id !== selectedImageId) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n return this;\n },\n\n // Get viewer status\n isViewerOpen: function() {\n return _isViewerOpen;\n },\n\n /**\n * Updates the URL to save the current shown image\n * @param {*} imageKey\n */\n updateUrlImage: function(imageKey) {\n const hash = utilStringQs(window.location.hash);\n if (imageKey) {\n hash.photo = 'panoramax/' + imageKey;\n } else {\n delete hash.photo;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n },\n\n /**\n * Loads the selected image in the frame\n * @param {*} context Current HTML context\n * @param {*} id of the selected image\n * @returns\n */\n selectImage: function (context, id) {\n let that = this;\n\n let d = that.cachedImage(id);\n that.setActiveImage(d);\n that.updateUrlImage(d.id);\n\n const viewerLink = `${viewerUrl}#pic=${d.id}&focus=pic`;\n\n let viewer = context.container()\n .select('.photoviewer');\n\n if (!viewer.empty()) viewer.datum(d);\n\n this.setStyles(context, null);\n\n if (!d) return this;\n\n let wrap = context.container()\n .select('.photoviewer .panoramax-wrapper');\n\n let attribution = wrap.selectAll('.photo-attribution').text('');\n\n let line1 = attribution\n .append('div')\n .attr('class', 'attribution-row');\n\n const hdDomId = utilUniqueDomId('panoramax-hd');\n\n let label = line1\n .append('label')\n .attr('for', hdDomId)\n .attr('class', 'panoramax-hd');\n\n label\n .append('input')\n .attr('type', 'checkbox')\n .attr('id', hdDomId)\n .property('checked', _isHD)\n .on('click', (d3_event) => {\n d3_event.stopPropagation();\n _isHD = !_isHD;\n _definition = _isHD ? highDefinition : standardDefinition;\n that.selectImage(context, d.id)\n .showViewer(context);\n });\n\n label\n .append('span')\n .call(t.append('panoramax.hd'));\n\n if (d.capture_time) {\n attribution\n .append('span')\n .attr('class', 'captured_at')\n .text(localeDateString(d.capture_time));\n\n attribution\n .append('span')\n .text('|');\n }\n\n attribution\n .append('a')\n .attr('class', 'report-photo')\n .attr('href', 'mailto:signalement.ign@panoramax.fr')\n .call(t.append('panoramax.report'));\n\n attribution\n .append('span')\n .text('|');\n\n attribution\n .append('a')\n .attr('class', 'image-link')\n .attr('target', '_blank')\n .attr('href', viewerLink)\n .text('panoramax.xyz');\n\n this.getImageData(d.sequence_id, d.id).then(function(data) {\n _currentScene = {\n currentImage: null,\n nextImage: null,\n prevImage: null\n };\n _currentScene.currentImage = data.assets[_definition];\n const nextIndex = data.links.findIndex(x => x.rel === 'next');\n const prevIndex = data.links.findIndex(x => x.rel === 'prev');\n\n if (nextIndex !== -1){\n _currentScene.nextImage = data.links[nextIndex];\n }\n if (prevIndex !== -1){\n _currentScene.prevImage = data.links[prevIndex];\n }\n\n d.image_path = _currentScene.currentImage.href;\n\n wrap\n .selectAll('button.back')\n .classed('hide', _currentScene.prevImage === null);\n wrap\n .selectAll('button.forward')\n .classed('hide', _currentScene.nextImage === null);\n\n _currentFrame = d.isPano ? _pannellumFrame : _planeFrame;\n\n _currentFrame\n .showPhotoFrame(wrap)\n .selectPhoto(d, true);\n });\n\n if (d.account_id) {\n attribution\n .append('span')\n .text('|');\n\n let line2 = attribution\n .append('span')\n .attr('class', 'attribution-row');\n\n getUsername(d.account_id).then(function(username){\n line2\n .append('span')\n .attr('class', 'captured_by')\n .text('@' + username);\n });\n }\n\n return this;\n },\n\n photoFrame: function() {\n return _currentFrame;\n },\n\n /**\n * Fetches the data for a specific image\n * @param {*} collectionId\n * @param {*} imageId\n * @returns The fetched image data\n */\n getImageData: async function(collectionId, imageId) {\n const cache = _cache.sequences.items;\n if (cache[collectionId]) {\n const cached = cache[collectionId]\n .find(d => d.id === imageId);\n if (cached) return cached;\n } else {\n // prime the cache with data from sequence\n const response = await fetch(sequenceDataUrl\n .replace('{collectionId}', collectionId),\n { method: 'GET' });\n\n if (!response.ok) {\n throw new Error(response.status + ' ' + response.statusText);\n }\n const data = (await response.json()).features;\n cache[collectionId] = data;\n }\n\n const result = cache[collectionId]\n .find(d => d.id === imageId);\n if (result) return result;\n\n // not found in sequence: retry to load single item data\n // ideally, we'd use the `withPicture` parameter, but it is buggy:\n // https://gitlab.com/panoramax/server/api/-/issues/268\n const itemResponse = await fetch(imageDataUrl\n .replace('{collectionId}', collectionId)\n .replace('{itemId}', imageId),\n { method: 'GET' });\n\n if (!itemResponse.ok) {\n throw new Error(itemResponse.status + ' ' + itemResponse.statusText);\n }\n const itemData = await itemResponse.json();\n cache[collectionId].push(itemData);\n return itemData;\n },\n\n ensureViewerLoaded: function(context) {\n\n let that = this;\n\n let imgWrap = context.container()\n .select('#ideditor-viewer-panoramax-simple > img');\n\n if (!imgWrap.empty()) {\n imgWrap.remove();\n }\n\n if (_loadViewerPromise) return _loadViewerPromise;\n\n let wrap = context.container()\n .select('.photoviewer')\n .selectAll('.panoramax-wrapper')\n .data([0]);\n\n let wrapEnter = wrap.enter()\n .append('div')\n .attr('class', 'photo-wrapper panoramax-wrapper')\n .classed('hide', true)\n .on('dblclick.zoom', null);\n\n wrapEnter\n .append('div')\n .attr('class', 'photo-attribution fillD');\n\n const controlsEnter = wrapEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls-panoramax');\n\n controlsEnter\n .append('button')\n .classed('back', true)\n .on('click.back', step(-1))\n .text('\u25C4');\n\n controlsEnter\n .append('button')\n .classed('forward', true)\n .on('click.forward', step(1))\n .text('\u25BA');\n\n // Register viewer resize handler\n _loadViewerPromise = Promise.all([\n pannellumPhotoFrame(context, wrapEnter),\n planePhotoFrame(context, wrapEnter)\n ]).then(([pannellumPhotoFrame, planePhotoFrame]) => {\n _pannellumFrame = pannellumPhotoFrame;\n _pannellumFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n _planeFrame = planePhotoFrame;\n _planeFrame.event.on('viewerChanged', () => dispatch.call('viewerChanged'));\n });\n\n /**\n * Loads the next image in the sequence\n * @param {number} stepBy '-1' if backwards or '1' if forward\n * @returns\n */\n function step(stepBy) {\n return function () {\n if (!_currentScene.currentImage) return;\n\n let nextId;\n if (stepBy === 1) nextId = _currentScene.nextImage.id;\n else nextId = _currentScene.prevImage.id;\n\n if (!nextId) return;\n\n const nextImage = _cache.images.forImageId[nextId];\n\n if (nextImage){\n context.map().centerEase(nextImage.loc);\n that.selectImage(context, nextImage.id);\n }\n };\n }\n\n return _loadViewerPromise;\n },\n\n /**\n * Shows the current viewer if hidden\n * @param {*} context\n */\n showViewer: function (context) {\n const wrap = context.container().select('.photoviewer');\n const isHidden = wrap.selectAll('.photo-wrapper.panoramax-wrapper.hide').size();\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (service === this) continue;\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n wrap.classed('hide', false)\n .selectAll('.photo-wrapper.panoramax-wrapper')\n .classed('hide', false);\n }\n\n _isViewerOpen = true;\n return this;\n },\n\n /**\n * Hides the current viewer if shown, resets the active image and sequence\n * @param {*} context\n */\n hideViewer: function (context) {\n let viewer = context.container().select('.photoviewer');\n if (!viewer.empty()) viewer.datum(null);\n this.updateUrlImage(null);\n viewer\n .classed('hide', true)\n .selectAll('.photo-wrapper')\n .classed('hide', true);\n context.container().selectAll('.viewfield-group, .sequence, .icon-sign')\n .classed('currentView', false);\n\n this.setActiveImage(null);\n _isViewerOpen = false;\n\n return this.setStyles(context, null);\n },\n\n cache: function() {\n return _cache;\n }\n};\n", "import serviceOsmose from './osmose';\nimport serviceMapillary from './mapillary';\nimport serviceMapRules from './maprules';\nimport serviceNominatim from './nominatim';\nimport serviceNsi from './nsi';\nimport serviceKartaview from './kartaview';\nimport serviceVegbilder from './vegbilder';\nimport serviceOsm from './osm';\nimport serviceOsmWikibase from './osm_wikibase';\nimport serviceStreetside from './streetside';\nimport serviceTaginfo from './taginfo';\nimport serviceVectorTile from './vector_tile';\nimport serviceWikidata from './wikidata';\nimport serviceWikipedia from './wikipedia';\nimport serviceMapilio from './mapilio';\nimport servicePanoramax from './panoramax';\n\n\nexport let services = {\n geocoder: serviceNominatim,\n osmose: serviceOsmose,\n mapillary: serviceMapillary,\n nsi: serviceNsi,\n kartaview: serviceKartaview,\n vegbilder: serviceVegbilder,\n osm: serviceOsm,\n osmWikibase: serviceOsmWikibase,\n maprules: serviceMapRules,\n streetside: serviceStreetside,\n taginfo: serviceTaginfo,\n vectorTile: serviceVectorTile,\n wikidata: serviceWikidata,\n wikipedia: serviceWikipedia,\n mapilio: serviceMapilio,\n panoramax: servicePanoramax\n};\n\nexport {\n serviceOsmose,\n serviceMapillary,\n serviceMapRules,\n serviceNominatim,\n serviceNsi,\n serviceKartaview,\n serviceVegbilder,\n serviceOsm,\n serviceOsmWikibase,\n serviceStreetside,\n serviceTaginfo,\n serviceVectorTile,\n serviceWikidata,\n serviceWikipedia,\n serviceMapilio,\n servicePanoramax\n};\n", "import {\n geoExtent, geoLineIntersection, geoMetersToLat, geoMetersToLon,\n geoSphericalDistance, geoVecInterp, geoHasSelfIntersections,\n geoSphericalClosestNode, geoAngle\n} from '../geo';\n\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionMergeNodes } from '../actions/merge_nodes';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilDatesOverlap } from '../util';\nimport { osmRoutableHighwayTagValues } from '../osm/tags';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\n\n/**\n * Look for roads that can be connected to other roads with a short extension\n */\nexport function validationAlmostJunction(context) {\n const type = 'almost_junction';\n const EXTEND_TH_METERS = 5;\n const WELD_TH_METERS = 0.75;\n // Comes from considering bounding case of parallel ways\n const CLOSE_NODE_TH = EXTEND_TH_METERS - WELD_TH_METERS;\n // Comes from considering bounding case of perpendicular ways\n const SIG_ANGLE_TH = Math.atan(WELD_TH_METERS / EXTEND_TH_METERS);\n\n function isHighway(entity) {\n return entity.type === 'way'\n && osmRoutableHighwayTagValues[entity.tags.highway];\n }\n\n function isTaggedAsNotContinuing(node) {\n return node.tags.noexit === 'yes'\n || node.tags.amenity === 'parking_entrance'\n || (node.tags.entrance && node.tags.entrance !== 'no');\n }\n\n\n const validation = function checkAlmostJunction(entity, graph) {\n if (!isHighway(entity)) return [];\n if (entity.isDegenerate()) return [];\n\n const tree = context.history().tree();\n const extendableNodeInfos = findConnectableEndNodesByExtension(entity);\n\n let issues = [];\n\n extendableNodeInfos.forEach(extendableNodeInfo => {\n issues.push(new validationIssue({\n type,\n subtype: 'highway-highway',\n severity: 'warning',\n message: function(context) {\n const entity1 = context.hasEntity(this.entityIds[0]);\n if (this.entityIds[0] === this.entityIds[2]) {\n return entity1 ? t.append('issues.almost_junction.self.message', {\n feature: utilDisplayLabel(entity1, context.graph())\n }) : '';\n } else {\n const entity2 = context.hasEntity(this.entityIds[2]);\n return (entity1 && entity2) ? t.append('issues.almost_junction.message', {\n feature: utilDisplayLabel(entity1, context.graph()),\n feature2: utilDisplayLabel(entity2, context.graph())\n }) : '';\n }\n },\n reference: showReference,\n entityIds: [\n entity.id,\n extendableNodeInfo.node.id,\n extendableNodeInfo.wid,\n ],\n loc: extendableNodeInfo.node.loc,\n hash: JSON.stringify(extendableNodeInfo.node.loc),\n data: {\n midId: extendableNodeInfo.mid.id,\n edge: extendableNodeInfo.edge,\n cross_loc: extendableNodeInfo.cross_loc\n },\n dynamicFixes: makeFixes\n }));\n });\n\n return issues;\n\n function makeFixes(context) {\n let fixes = [new validationIssueFix({\n icon: 'iD-icon-abutment',\n title: t.append('issues.fix.connect_features.title'),\n onClick: function(context) {\n const annotation = t('issues.fix.connect_almost_junction.annotation');\n const [, endNodeId, crossWayId] = this.issue.entityIds;\n const midNode = context.entity(this.issue.data.midId);\n const endNode = context.entity(endNodeId);\n const crossWay = context.entity(crossWayId);\n\n // When endpoints are close, just join if resulting small change in angle (#7201)\n const nearEndNodes = findNearbyEndNodes(endNode, crossWay);\n if (nearEndNodes.length > 0) {\n const collinear = findSmallJoinAngle(midNode, endNode, nearEndNodes);\n if (collinear) {\n context.perform(\n actionMergeNodes([collinear.id, endNode.id], collinear.loc),\n annotation\n );\n return;\n }\n }\n\n const targetEdge = this.issue.data.edge;\n const crossLoc = this.issue.data.cross_loc;\n const edgeNodes = [context.entity(targetEdge[0]), context.entity(targetEdge[1])];\n const closestNodeInfo = geoSphericalClosestNode(edgeNodes, crossLoc);\n\n // already a point nearby, just connect to that\n if (closestNodeInfo.distance < WELD_TH_METERS) {\n context.perform(\n actionMergeNodes([closestNodeInfo.node.id, endNode.id], closestNodeInfo.node.loc),\n annotation\n );\n // else add the end node to the edge way\n } else {\n context.perform(\n actionAddMidpoint({loc: crossLoc, edge: targetEdge}, endNode),\n annotation\n );\n }\n }\n })];\n\n const node = context.hasEntity(this.entityIds[1]);\n if (node && !node.hasInterestingTags()) {\n // node has no descriptive tags, suggest noexit fix\n fixes.push(new validationIssueFix({\n icon: 'maki-barrier',\n title: t.append('issues.fix.tag_as_disconnected.title'),\n onClick: function(context) {\n const nodeID = this.issue.entityIds[1];\n const tags = Object.assign({}, context.entity(nodeID).tags);\n tags.noexit = 'yes';\n context.perform(\n actionChangeTags(nodeID, tags),\n t('issues.fix.tag_as_disconnected.annotation')\n );\n }\n }));\n }\n\n // changing the date range is always an option\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-change-date-range',\n title: t.append('issues.fix.change_date_range.title')\n }));\n\n return fixes;\n }\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.almost_junction.highway-highway.reference'));\n }\n\n function isExtendableCandidate(node, way) {\n // can not accurately test vertices on tiles not downloaded from osm - #5938\n const osm = services.osm;\n if (osm && !osm.isDataLoaded(node.loc)) {\n return false;\n }\n if (isTaggedAsNotContinuing(node) || graph.parentWays(node).length !== 1) {\n return false;\n }\n\n let occurrences = 0;\n for (const index in way.nodes) {\n if (way.nodes[index] === node.id) {\n occurrences += 1;\n if (occurrences > 1) {\n return false;\n }\n }\n }\n return true;\n }\n\n function findConnectableEndNodesByExtension(way) {\n let results = [];\n if (way.isClosed()) return results;\n\n let testNodes;\n const indices = [0, way.nodes.length - 1];\n indices.forEach(nodeIndex => {\n const nodeID = way.nodes[nodeIndex];\n const node = graph.entity(nodeID);\n\n if (!isExtendableCandidate(node, way)) return;\n\n const connectionInfo = canConnectByExtend(way, nodeIndex);\n if (!connectionInfo) return;\n\n testNodes = graph.childNodes(way).slice(); // shallow copy\n testNodes[nodeIndex] = testNodes[nodeIndex].move(connectionInfo.cross_loc);\n\n // don't flag issue if connecting the ways would cause self-intersection\n if (geoHasSelfIntersections(testNodes, nodeID)) return;\n\n results.push(connectionInfo);\n });\n\n return results;\n }\n\n function findNearbyEndNodes(node, way) {\n return [\n way.nodes[0],\n way.nodes[way.nodes.length - 1]\n ].map(d => graph.entity(d))\n .filter(d => {\n // Node cannot be near to itself, but other endnode of same way could be\n return d.id !== node.id\n && geoSphericalDistance(node.loc, d.loc) <= CLOSE_NODE_TH;\n });\n }\n\n function findSmallJoinAngle(midNode, tipNode, endNodes) {\n // Both nodes could be close, so want to join whichever is closest to collinear\n let joinTo;\n let minAngle = Infinity;\n\n // Checks midNode -> tipNode -> endNode for collinearity\n endNodes.forEach(endNode => {\n const a1 = geoAngle(midNode, tipNode, context.projection) + Math.PI;\n const a2 = geoAngle(midNode, endNode, context.projection) + Math.PI;\n const diff = Math.max(a1, a2) - Math.min(a1, a2);\n\n if (diff < minAngle) {\n joinTo = endNode;\n minAngle = diff;\n }\n });\n\n /* Threshold set by considering right angle triangle\n based on node joining threshold and extension distance */\n if (minAngle <= SIG_ANGLE_TH) return joinTo;\n\n return null;\n }\n\n function hasTag(tags, key) {\n return tags[key] !== undefined && tags[key] !== 'no';\n }\n\n function canConnectWays(way, way2) {\n\n // allow self-connections\n if (way.id === way2.id) return true;\n\n // if one is bridge or tunnel, both must be bridge or tunnel\n if ((hasTag(way.tags, 'bridge') || hasTag(way2.tags, 'bridge')) &&\n !(hasTag(way.tags, 'bridge') && hasTag(way2.tags, 'bridge'))) return false;\n if ((hasTag(way.tags, 'tunnel') || hasTag(way2.tags, 'tunnel')) &&\n !(hasTag(way.tags, 'tunnel') && hasTag(way2.tags, 'tunnel'))) return false;\n\n // must have equivalent layers and levels\n const layer1 = way.tags.layer || '0',\n layer2 = way2.tags.layer || '0';\n if (layer1 !== layer2) return false;\n\n const level1 = way.tags.level || '0',\n level2 = way2.tags.level || '0';\n if (level1 !== level2) return false;\n\n // must have overlapping date ranges\n if ((way.tags.start_date || way.tags.end_date) && (way2.tags.start_date || way2.tags.end_date)) {\n if (!utilDatesOverlap(way.tags, way2.tags)) return false;\n }\n\n return true;\n }\n\n function canConnectByExtend(way, endNodeIdx) {\n const tipNid = way.nodes[endNodeIdx]; // the 'tip' node for extension point\n const midNid = endNodeIdx === 0 ? way.nodes[1] : way.nodes[way.nodes.length - 2]; // the other node of the edge\n const tipNode = graph.entity(tipNid);\n const midNode = graph.entity(midNid);\n const lon = tipNode.loc[0];\n const lat = tipNode.loc[1];\n const lon_range = geoMetersToLon(EXTEND_TH_METERS, lat) / 2;\n const lat_range = geoMetersToLat(EXTEND_TH_METERS) / 2;\n const queryExtent = geoExtent([\n [lon - lon_range, lat - lat_range],\n [lon + lon_range, lat + lat_range]\n ]);\n\n // first, extend the edge of [midNode -> tipNode] by EXTEND_TH_METERS and find the \"extended tip\" location\n const edgeLen = geoSphericalDistance(midNode.loc, tipNode.loc);\n const t = EXTEND_TH_METERS / edgeLen + 1.0;\n const extTipLoc = geoVecInterp(midNode.loc, tipNode.loc, t);\n\n // then, check if the extension part [tipNode.loc -> extTipLoc] intersects any other ways\n const segmentInfos = tree.waySegments(queryExtent, graph);\n for (let i = 0; i < segmentInfos.length; i++) {\n let segmentInfo = segmentInfos[i];\n\n let way2 = graph.entity(segmentInfo.wayId);\n\n if (!isHighway(way2)) continue;\n\n if (!canConnectWays(way, way2)) continue;\n\n let nAid = segmentInfo.nodes[0],\n nBid = segmentInfo.nodes[1];\n\n if (nAid === tipNid || nBid === tipNid) continue;\n\n let nA = graph.entity(nAid),\n nB = graph.entity(nBid);\n let crossLoc = geoLineIntersection([tipNode.loc, extTipLoc], [nA.loc, nB.loc]);\n if (crossLoc) {\n return {\n mid: midNode,\n node: tipNode,\n wid: way2.id,\n edge: [nA.id, nB.id],\n cross_loc: crossLoc\n };\n }\n }\n return null;\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionMergeNodes } from '../actions/merge_nodes';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilDatesOverlap } from '../util';\nimport { t } from '../core/localizer';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { osmPathHighwayTagValues } from '../osm/tags';\nimport { geoMetersToLat, geoMetersToLon, geoSphericalDistance } from '../geo/geo';\nimport { geoExtent } from '../geo/extent';\n\nexport function validationCloseNodes(context) {\n var type = 'close_nodes';\n\n var pointThresholdMeters = 0.2;\n\n var validation = function(entity, graph) {\n if (entity.type === 'node') {\n return getIssuesForNode(entity);\n } else if (entity.type === 'way') {\n return getIssuesForWay(entity);\n }\n return [];\n\n function getIssuesForNode(node) {\n var parentWays = graph.parentWays(node);\n if (parentWays.length) {\n return getIssuesForVertex(node, parentWays);\n } else {\n return getIssuesForDetachedPoint(node);\n }\n }\n\n function wayTypeFor(way) {\n\n if (way.tags.boundary && way.tags.boundary !== 'no') return 'boundary';\n if (way.tags.indoor && way.tags.indoor !== 'no') return 'indoor';\n if ((way.tags.building && way.tags.building !== 'no') ||\n (way.tags['building:part'] && way.tags['building:part'] !== 'no')) return 'building';\n if (osmPathHighwayTagValues[way.tags.highway]) return 'path';\n\n var parentRelations = graph.parentRelations(way);\n for (var i in parentRelations) {\n var relation = parentRelations[i];\n\n if (relation.tags.type === 'boundary') return 'boundary';\n\n if (relation.isMultipolygon()) {\n if (relation.tags.indoor && relation.tags.indoor !== 'no') return 'indoor';\n if ((relation.tags.building && relation.tags.building !== 'no') ||\n (relation.tags['building:part'] && relation.tags['building:part'] !== 'no')) return 'building';\n }\n }\n\n return 'other';\n }\n\n function shouldCheckWay(way) {\n\n // don't flag issues where merging would create degenerate ways\n if (way.nodes.length <= 2 ||\n (way.isClosed() && way.nodes.length <= 4)) return false;\n\n var bbox = way.extent(graph).bbox();\n var hypotenuseMeters = geoSphericalDistance([bbox.minX, bbox.minY], [bbox.maxX, bbox.maxY]);\n // don't flag close nodes in very small ways\n if (hypotenuseMeters < 1.5) return false;\n\n return true;\n }\n\n function getIssuesForWay(way) {\n if (!shouldCheckWay(way)) return [];\n\n var issues = [],\n nodes = graph.childNodes(way);\n for (var i = 0; i < nodes.length - 1; i++) {\n var node1 = nodes[i];\n var node2 = nodes[i+1];\n\n var issue = getWayIssueIfAny(node1, node2, way);\n if (issue) issues.push(issue);\n }\n return issues;\n }\n\n function getIssuesForVertex(node, parentWays) {\n var issues = [];\n\n function checkForCloseness(node1, node2, way) {\n var issue = getWayIssueIfAny(node1, node2, way);\n if (issue) issues.push(issue);\n }\n\n for (var i = 0; i < parentWays.length; i++) {\n var parentWay = parentWays[i];\n\n if (!shouldCheckWay(parentWay)) continue;\n\n var lastIndex = parentWay.nodes.length - 1;\n for (var j = 0; j < parentWay.nodes.length; j++) {\n if (j !== 0) {\n if (parentWay.nodes[j-1] === node.id) {\n checkForCloseness(node, graph.entity(parentWay.nodes[j]), parentWay);\n }\n }\n if (j !== lastIndex) {\n if (parentWay.nodes[j+1] === node.id) {\n checkForCloseness(graph.entity(parentWay.nodes[j]), node, parentWay);\n }\n }\n }\n }\n return issues;\n }\n\n function thresholdMetersForWay(way) {\n if (!shouldCheckWay(way)) return 0;\n\n var wayType = wayTypeFor(way);\n\n // don't flag boundaries since they might be highly detailed and can't be easily verified\n if (wayType === 'boundary') return 0;\n // expect some features to be mapped with higher levels of detail\n if (wayType === 'indoor') return 0.01;\n if (wayType === 'building') return 0.05;\n if (wayType === 'path') return 0.1;\n return 0.2;\n }\n\n function getIssuesForDetachedPoint(node) {\n\n var issues = [];\n\n var lon = node.loc[0];\n var lat = node.loc[1];\n var lon_range = geoMetersToLon(pointThresholdMeters, lat) / 2;\n var lat_range = geoMetersToLat(pointThresholdMeters) / 2;\n var queryExtent = geoExtent([\n [lon - lon_range, lat - lat_range],\n [lon + lon_range, lat + lat_range]\n ]);\n\n var intersected = context.history().tree().intersects(queryExtent, graph);\n for (var j = 0; j < intersected.length; j++) {\n var nearby = intersected[j];\n\n if (nearby.id === node.id) continue;\n if (nearby.type !== 'node' || nearby.geometry(graph) !== 'point') continue;\n\n if (nearby.loc === node.loc ||\n geoSphericalDistance(node.loc, nearby.loc) < pointThresholdMeters) {\n\n // ignore stolperstein (https://wiki.openstreetmap.org/wiki/DE:Stolpersteine)\n if ('memorial:type' in node.tags && 'memorial:type' in nearby.tags && node.tags['memorial:type']==='stolperstein' && nearby.tags['memorial:type']==='stolperstein') continue;\n if ('memorial' in node.tags && 'memorial' in nearby.tags && node.tags.memorial==='stolperstein' && nearby.tags.memorial === 'stolperstein') continue;\n\n // allow very close points if tags indicate the z-axis might vary\n var zAxisKeys = { layer: true, level: true, 'addr:housenumber': true, 'addr:unit': true };\n var zAxisDifferentiates = false;\n for (var key in zAxisKeys) {\n var nodeValue = node.tags[key] || '0';\n var nearbyValue = nearby.tags[key] || '0';\n if (nodeValue !== nearbyValue) {\n zAxisDifferentiates = true;\n break;\n }\n }\n if (zAxisDifferentiates) continue;\n\n // allow features to overlap spatially if they don't overlap temporally\n if ((node.tags.start_date || node.tags.end_date) && (nearby.tags.start_date || nearby.tags.end_date)) {\n if (!utilDatesOverlap(node.tags, nearby.tags)) continue;\n }\n\n issues.push(new validationIssue({\n type: type,\n subtype: 'detached',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]),\n entity2 = context.hasEntity(this.entityIds[1]);\n return (entity && entity2) ? t.append('issues.close_nodes.detached.message', {\n feature: utilDisplayLabel(entity, context.graph()),\n feature2: utilDisplayLabel(entity2, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [node.id, nearby.id],\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-disconnect',\n title: t.append('issues.fix.move_points_apart.title')\n }),\n new validationIssueFix({\n icon: 'iD-icon-layers',\n title: t.append('issues.fix.use_different_layers_or_levels.title')\n }),\n new validationIssueFix({\n icon: 'iD-operation-change-date-range',\n title: t.append('issues.fix.change_date_range.title')\n })\n ];\n }\n }));\n }\n }\n\n return issues;\n\n function showReference(selection) {\n var referenceText = t('issues.close_nodes.detached.reference');\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .html(referenceText);\n }\n }\n\n function getWayIssueIfAny(node1, node2, way) {\n if (node1.id === node2.id ||\n (node1.hasInterestingTags() && node2.hasInterestingTags())) {\n return null;\n }\n\n if (node1.loc !== node2.loc) {\n var parentWays1 = graph.parentWays(node1);\n var parentWays2 = new Set(graph.parentWays(node2));\n\n var sharedWays = parentWays1.filter(function(parentWay) {\n return parentWays2.has(parentWay);\n });\n\n var thresholds = sharedWays.map(function(parentWay) {\n return thresholdMetersForWay(parentWay);\n });\n\n var threshold = Math.min(...thresholds);\n var distance = geoSphericalDistance(node1.loc, node2.loc);\n if (distance > threshold) return null;\n }\n\n return new validationIssue({\n type: type,\n subtype: 'vertices',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.close_nodes.message', { way: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: showReference,\n entityIds: [way.id, node1.id, node2.id],\n loc: node1.loc,\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-icon-plus',\n title: t.append('issues.fix.merge_points.title'),\n onClick: function(context) {\n var entityIds = this.issue.entityIds;\n var action = actionMergeNodes([entityIds[1], entityIds[2]]);\n context.perform(action, t('issues.fix.merge_close_vertices.annotation'));\n }\n }),\n new validationIssueFix({\n icon: 'iD-operation-disconnect',\n title: t.append('issues.fix.move_points_apart.title')\n })\n ];\n }\n });\n\n function showReference(selection) {\n var referenceText = t('issues.close_nodes.reference');\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .html(referenceText);\n }\n }\n\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { deepEqual } from 'fast-equals';\n\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionMergeNodes } from '../actions/merge_nodes';\nimport { actionSplit } from '../actions/split';\nimport { modeSelect } from '../modes/select';\nimport { geoAngle, geoExtent, geoLatToMeters, geoLonToMeters, geoLineIntersection,\n geoSphericalClosestNode, geoSphericalDistance, geoVecAngle, geoVecLength, geoMetersToLat, geoMetersToLon } from '../geo';\nimport { osmNode } from '../osm/node';\nimport { osmFlowingWaterwayTagValues, osmPathHighwayTagValues, osmRailwayTrackTagValues, osmRoutableAerowayTags, osmRoutableHighwayTagValues } from '../osm/tags';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilDatesOverlap } from '../util';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationCrossingWays(context) {\n var type = 'crossing_ways';\n\n // returns the way or its parent relation, whichever has a useful feature type\n function getFeatureWithFeatureTypeTagsForWay(way, graph) {\n if (getFeatureType(way, graph) === null) {\n // if the way doesn't match a feature type, check its parent relations\n var parentRels = graph.parentRelations(way);\n for (var i = 0; i < parentRels.length; i++) {\n var rel = parentRels[i];\n if (getFeatureType(rel, graph) !== null) {\n return rel;\n }\n }\n }\n return way;\n }\n\n\n function hasTag(tags, key) {\n return tags[key] !== undefined && tags[key] !== 'no';\n }\n\n function taggedAsIndoor(tags) {\n return hasTag(tags, 'indoor') ||\n hasTag(tags, 'level') ||\n tags.highway === 'corridor';\n }\n\n function allowsBridge(featureType) {\n return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway' || featureType === 'aeroway';\n }\n function allowsTunnel(featureType) {\n return featureType === 'highway' || featureType === 'railway' || featureType === 'waterway';\n }\n\n // discard\n var ignoredBuildings = {\n demolished: true, dismantled: true, proposed: true, razed: true\n };\n\n\n function getFeatureType(entity, graph) {\n\n var geometry = entity.geometry(graph);\n if (geometry !== 'line' && geometry !== 'area') return null;\n\n var tags = entity.tags;\n\n if (tags.aeroway in osmRoutableAerowayTags) return 'aeroway';\n\n if (hasTag(tags, 'building') && !ignoredBuildings[tags.building]) return 'building';\n if (hasTag(tags, 'highway') && osmRoutableHighwayTagValues[tags.highway]) return 'highway';\n\n // don't check railway or waterway areas\n if (geometry !== 'line') return null;\n\n if (hasTag(tags, 'railway') && osmRailwayTrackTagValues[tags.railway]) return 'railway';\n if (hasTag(tags, 'waterway') && osmFlowingWaterwayTagValues[tags.waterway]) return 'waterway';\n\n return null;\n }\n\n\n function isLegitCrossing(tags1, featureType1, tags2, featureType2) {\n\n // assume 0 by default\n var level1 = tags1.level || '0';\n var level2 = tags2.level || '0';\n\n if (taggedAsIndoor(tags1) && taggedAsIndoor(tags2) && level1 !== level2) {\n // assume features don't interact if they're indoor on different levels\n return true;\n }\n\n // don't flag crossing waterways and pier/highways\n if (featureType1 === 'waterway' && featureType2 === 'highway' && tags2.man_made === 'pier') return true;\n if (featureType2 === 'waterway' && featureType1 === 'highway' && tags1.man_made === 'pier') return true;\n\n // allow features to overlap spatially if they don't overlap temporally\n if ((tags1.start_date || tags1.end_date) && (tags2.start_date || tags2.end_date)) {\n if (!utilDatesOverlap(tags1, tags2)) return true;\n }\n\n if (tags1.layer !== undefined && tags1.layer === tags2.layer) return false; // Warn if both have the same defined layer\n\n const isElement1Bridge = allowsBridge(featureType1) && hasTag(tags1, 'bridge');\n const isElement2Bridge = allowsBridge(featureType2) && hasTag(tags2, 'bridge');\n if (isElement1Bridge !== isElement2Bridge) return true; // Either one is bridge, the other is not\n\n const isElement1Tunnel = allowsTunnel(featureType1) && hasTag(tags1, 'tunnel');\n const isElement2Tunnel = allowsTunnel(featureType2) && hasTag(tags2, 'tunnel');\n if (isElement1Tunnel !== isElement2Tunnel ) return true; // Either one is tunnel, the other is not\n\n return (tags1.layer || '0') !== (tags2.layer || '0');\n }\n\n\n // highway values for which we shouldn't recommend connecting to waterways\n var highwaysDisallowingFords = {\n motorway: true, motorway_link: true, trunk: true, trunk_link: true,\n primary: true, primary_link: true, secondary: true, secondary_link: true\n };\n\n /**\n * @returns {object | null} the tags for the connecting node, or null if the entities should not be joined\n */\n function tagsForConnectionNodeIfAllowed(entity1, entity2, graph, lessLikelyTags) {\n var featureType1 = getFeatureType(entity1, graph);\n var featureType2 = getFeatureType(entity2, graph);\n\n var geometry1 = entity1.geometry(graph);\n var geometry2 = entity2.geometry(graph);\n var bothLines = geometry1 === 'line' && geometry2 === 'line';\n\n /**\n * @typedef {NonNullable>} FeatureType\n * @type {`${FeatureType}-${FeatureType}`}\n */\n const featureTypes = [featureType1, featureType2].sort().join('-');\n\n if (featureTypes === 'aeroway-aeroway') return {};\n\n if (featureTypes === 'aeroway-highway') {\n const isServiceRoad = entity1.tags.highway === 'service' || entity2.tags.highway === 'service';\n const isPath = entity1.tags.highway in osmPathHighwayTagValues || entity2.tags.highway in osmPathHighwayTagValues;\n // only significant roads get the aeroway=aircraft_crossing tag\n return isServiceRoad || isPath ? {} : { aeroway: 'aircraft_crossing' };\n }\n\n if (featureTypes === 'aeroway-railway') {\n return { aeroway: 'aircraft_crossing', railway: 'level_crossing' };\n }\n\n if (featureTypes === 'aeroway-waterway') return null;\n\n if (featureType1 === featureType2) {\n if (featureType1 === 'highway') {\n var entity1IsPath = osmPathHighwayTagValues[entity1.tags.highway];\n var entity2IsPath = osmPathHighwayTagValues[entity2.tags.highway];\n if ((entity1IsPath || entity2IsPath) && entity1IsPath !== entity2IsPath) {\n // one feature is a path but not both\n\n if (!bothLines) return {};\n\n var roadFeature = entity1IsPath ? entity2 : entity1;\n var pathFeature = entity1IsPath ? entity1 : entity2;\n // don't mark path connections with tracks as crossings\n if (roadFeature.tags.highway === 'track') {\n return {};\n }\n // a sidewalk crossing a driveway is unremarkable and unlikely to be interrupted by the driveway\n // a sidewalk crossing another kind of service road may be similarly unremarkable\n if (!lessLikelyTags &&\n roadFeature.tags.highway === 'service' &&\n pathFeature.tags.highway === 'footway' && pathFeature.tags.footway === 'sidewalk') {\n return {};\n }\n if (['marked', 'unmarked', 'traffic_signals', 'uncontrolled'].indexOf(pathFeature.tags.crossing) !== -1) {\n // if the path is a crossing, match the crossing type and markings\n var tags = { highway: 'crossing', crossing: pathFeature.tags.crossing };\n if ('crossing:markings' in pathFeature.tags) {\n tags['crossing:markings'] = pathFeature.tags['crossing:markings'];\n }\n return tags;\n }\n // don't add a `crossing` subtag to ambiguous crossings\n return { highway: 'crossing' };\n }\n return {};\n }\n if (featureType1 === 'waterway') return {};\n if (featureType1 === 'railway') {\n return { railway: 'railway_crossing' };\n }\n\n } else {\n if (featureTypes.indexOf('highway') !== -1) {\n if (featureTypes.indexOf('railway') !== -1) {\n if (!bothLines) return {};\n\n var isTram = entity1.tags.railway === 'tram' || entity2.tags.railway === 'tram';\n\n if (osmPathHighwayTagValues[entity1.tags.highway] ||\n osmPathHighwayTagValues[entity2.tags.highway]) {\n\n // path-tram connections use this tag\n if (isTram) return { railway: 'tram_crossing' };\n\n // other path-rail connections use this tag\n return { railway: 'crossing' };\n } else {\n // path-tram connections use this tag\n if (isTram) return { railway: 'tram_level_crossing' };\n\n // other road-rail connections use this tag\n return { railway: 'level_crossing' };\n }\n }\n\n if (featureTypes.indexOf('waterway') !== -1) {\n // do not allow fords on structures\n if (hasTag(entity1.tags, 'tunnel') && hasTag(entity2.tags, 'tunnel')) return null;\n if (hasTag(entity1.tags, 'bridge') && hasTag(entity2.tags, 'bridge')) return null;\n\n if (highwaysDisallowingFords[entity1.tags.highway] ||\n highwaysDisallowingFords[entity2.tags.highway]) {\n // do not allow fords on major highways\n return null;\n }\n return bothLines ? { ford: 'yes' } : {};\n }\n }\n }\n return null;\n }\n\n\n function findCrossingsByWay(way1, graph, tree) {\n var edgeCrossInfos = [];\n if (way1.type !== 'way') return edgeCrossInfos;\n\n var taggedFeature1 = getFeatureWithFeatureTypeTagsForWay(way1, graph);\n var way1FeatureType = getFeatureType(taggedFeature1, graph);\n if (way1FeatureType === null) return edgeCrossInfos;\n\n var checkedSingleCrossingWays = {};\n\n // declare vars ahead of time to reduce garbage collection\n var i, j;\n var extent;\n var n1, n2, nA, nB, nAId, nBId;\n var segment1, segment2;\n var oneOnly;\n var segmentInfos, segment2Info, way2, taggedFeature2, way2FeatureType;\n var way1Nodes = graph.childNodes(way1);\n var comparedWays = {};\n for (i = 0; i < way1Nodes.length - 1; i++) {\n n1 = way1Nodes[i];\n n2 = way1Nodes[i + 1];\n extent = geoExtent([\n [\n Math.min(n1.loc[0], n2.loc[0]),\n Math.min(n1.loc[1], n2.loc[1])\n ],\n [\n Math.max(n1.loc[0], n2.loc[0]),\n Math.max(n1.loc[1], n2.loc[1])\n ]\n ]);\n\n // Optimize by only checking overlapping segments, not every segment\n // of overlapping ways\n segmentInfos = tree.waySegments(extent, graph);\n\n for (j = 0; j < segmentInfos.length; j++) {\n segment2Info = segmentInfos[j];\n\n // don't check for self-intersection in this validation\n if (segment2Info.wayId === way1.id) continue;\n\n // skip if this way was already checked and only one issue is needed\n if (checkedSingleCrossingWays[segment2Info.wayId]) continue;\n\n // mark this way as checked even if there are no crossings\n comparedWays[segment2Info.wayId] = true;\n\n way2 = graph.hasEntity(segment2Info.wayId);\n if (!way2) continue;\n taggedFeature2 = getFeatureWithFeatureTypeTagsForWay(way2, graph);\n // only check crossing highway, waterway, building, and railway\n way2FeatureType = getFeatureType(taggedFeature2, graph);\n\n if (way2FeatureType === null ||\n isLegitCrossing(taggedFeature1.tags, way1FeatureType, taggedFeature2.tags, way2FeatureType)) {\n continue;\n }\n\n // create only one issue for building crossings\n oneOnly = way1FeatureType === 'building' || way2FeatureType === 'building';\n\n nAId = segment2Info.nodes[0];\n nBId = segment2Info.nodes[1];\n if (nAId === n1.id || nAId === n2.id ||\n nBId === n1.id || nBId === n2.id) {\n // n1 or n2 is a connection node; skip\n continue;\n }\n nA = graph.hasEntity(nAId);\n if (!nA) continue;\n nB = graph.hasEntity(nBId);\n if (!nB) continue;\n\n segment1 = [n1.loc, n2.loc];\n segment2 = [nA.loc, nB.loc];\n var point = geoLineIntersection(segment1, segment2);\n if (point) {\n edgeCrossInfos.push({\n wayInfos: [\n {\n way: way1,\n featureType: way1FeatureType,\n edge: [n1.id, n2.id]\n },\n {\n way: way2,\n featureType: way2FeatureType,\n edge: [nA.id, nB.id]\n }\n ],\n crossPoint: point\n });\n if (oneOnly) {\n checkedSingleCrossingWays[way2.id] = true;\n break;\n }\n }\n }\n }\n return edgeCrossInfos;\n }\n\n\n function waysToCheck(entity, graph) {\n var featureType = getFeatureType(entity, graph);\n if (!featureType) return [];\n\n if (entity.type === 'way') {\n return [entity];\n } else if (entity.type === 'relation') {\n return entity.members.reduce(function(array, member) {\n if (member.type === 'way' &&\n // only look at geometry ways\n (!member.role || member.role === 'outer' || member.role === 'inner')) {\n var entity = graph.hasEntity(member.id);\n // don't add duplicates\n if (entity && array.indexOf(entity) === -1) {\n array.push(entity);\n }\n }\n return array;\n }, []);\n }\n return [];\n }\n\n\n var validation = function checkCrossingWays(entity, graph) {\n\n var tree = context.history().tree();\n\n var ways = waysToCheck(entity, graph);\n\n var issues = [];\n // declare these here to reduce garbage collection\n var wayIndex, crossingIndex, crossings;\n for (wayIndex in ways) {\n crossings = findCrossingsByWay(ways[wayIndex], graph, tree);\n for (crossingIndex in crossings) {\n issues.push(createIssue(crossings[crossingIndex], graph));\n }\n }\n return issues;\n };\n\n\n function createIssue(crossing, graph) {\n\n // use the entities with the tags that define the feature type\n crossing.wayInfos.sort(function(way1Info, way2Info) {\n var type1 = way1Info.featureType;\n var type2 = way2Info.featureType;\n if (type1 === type2) {\n return utilDisplayLabel(way1Info.way, graph) > utilDisplayLabel(way2Info.way, graph);\n } else if (type1 === 'waterway') {\n return true;\n } else if (type2 === 'waterway') {\n return false;\n }\n return type1 < type2;\n });\n var entities = crossing.wayInfos.map(function(wayInfo) {\n return getFeatureWithFeatureTypeTagsForWay(wayInfo.way, graph);\n });\n var edges = [crossing.wayInfos[0].edge, crossing.wayInfos[1].edge];\n var featureTypes = [crossing.wayInfos[0].featureType, crossing.wayInfos[1].featureType];\n\n var connectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1], graph);\n\n var featureType1 = crossing.wayInfos[0].featureType;\n var featureType2 = crossing.wayInfos[1].featureType;\n\n var isCrossingIndoors = taggedAsIndoor(entities[0].tags) && taggedAsIndoor(entities[1].tags);\n var isCrossingTunnels = allowsTunnel(featureType1) && hasTag(entities[0].tags, 'tunnel') &&\n allowsTunnel(featureType2) && hasTag(entities[1].tags, 'tunnel');\n var isCrossingBridges = allowsBridge(featureType1) && hasTag(entities[0].tags, 'bridge') &&\n allowsBridge(featureType2) && hasTag(entities[1].tags, 'bridge');\n\n var subtype = [featureType1, featureType2].sort().join('-');\n\n var crossingTypeID = subtype;\n\n if (isCrossingIndoors) {\n crossingTypeID = 'indoor-indoor';\n } else if (isCrossingTunnels) {\n crossingTypeID = 'tunnel-tunnel';\n } else if (isCrossingBridges) {\n crossingTypeID = 'bridge-bridge';\n }\n if (connectionTags && (isCrossingIndoors || isCrossingTunnels || isCrossingBridges)) {\n crossingTypeID += '_connectable';\n }\n\n // Differentiate based on the loc rounded to 4 digits, since two ways can cross multiple times.\n var uniqueID = crossing.crossPoint[0].toFixed(4) + ',' + crossing.crossPoint[1].toFixed(4);\n\n return new validationIssue({\n type: type,\n subtype: subtype,\n severity: 'warning',\n message: function(context) {\n var graph = context.graph();\n var entity1 = graph.hasEntity(this.entityIds[0]),\n entity2 = graph.hasEntity(this.entityIds[1]);\n return (entity1 && entity2) ? t.append('issues.crossing_ways.message', {\n feature: utilDisplayLabel(entity1, graph, featureType1 === 'building'),\n feature2: utilDisplayLabel(entity2, graph, featureType2 === 'building')\n }) : '';\n },\n reference: showReference,\n entityIds: entities.map(function(entity) {\n return entity.id;\n }),\n data: {\n edges: edges,\n featureTypes: featureTypes,\n connectionTags: connectionTags\n },\n hash: uniqueID,\n loc: crossing.crossPoint,\n dynamicFixes: function(context) {\n var mode = context.mode();\n if (!mode || mode.id !== 'select' || mode.selectedIDs().length !== 1) return [];\n\n var selectedIndex = this.entityIds[0] === mode.selectedIDs()[0] ? 0 : 1;\n var selectedFeatureType = this.data.featureTypes[selectedIndex];\n var otherFeatureType = this.data.featureTypes[selectedIndex === 0 ? 1 : 0];\n\n var fixes = [];\n\n if (connectionTags) {\n fixes.push(makeConnectWaysFix(this.data.connectionTags));\n let lessLikelyConnectionTags = tagsForConnectionNodeIfAllowed(entities[0], entities[1], graph, true);\n if (lessLikelyConnectionTags && !deepEqual(connectionTags, lessLikelyConnectionTags)) {\n fixes.push(makeConnectWaysFix(lessLikelyConnectionTags));\n }\n }\n\n if (isCrossingIndoors) {\n fixes.push(new validationIssueFix({\n icon: 'iD-icon-layers',\n title: t.append('issues.fix.use_different_levels.title')\n }));\n } else if (isCrossingTunnels ||\n isCrossingBridges ||\n featureType1 === 'building' ||\n featureType2 === 'building') {\n\n fixes.push(makeChangeLayerFix('higher'));\n fixes.push(makeChangeLayerFix('lower'));\n\n // can only add bridge/tunnel if both features are lines\n } else if (context.graph().geometry(this.entityIds[0]) === 'line' &&\n context.graph().geometry(this.entityIds[1]) === 'line') {\n\n // don't recommend adding bridges to waterways since they're uncommon\n if (allowsBridge(selectedFeatureType) && selectedFeatureType !== 'waterway') {\n fixes.push(makeAddBridgeOrTunnelFix('add_a_bridge', 'temaki-bridge', 'bridge'));\n }\n\n // don't recommend adding tunnels under waterways since they're uncommon\n var skipTunnelFix = otherFeatureType === 'waterway' && selectedFeatureType !== 'waterway';\n if (allowsTunnel(selectedFeatureType) && !skipTunnelFix) {\n if (selectedFeatureType === 'waterway') {\n // naming piped waterway \"tunnel\" is a confusing osmism, culvert should be more clear\n fixes.push(makeAddBridgeOrTunnelFix('add_a_culvert', 'temaki-waste', 'tunnel'));\n } else {\n fixes.push(makeAddBridgeOrTunnelFix('add_a_tunnel', 'temaki-tunnel', 'tunnel'));\n }\n }\n }\n\n // changing the date range is always an option\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-change-date-range',\n title: t.append('issues.fix.change_date_range.title')\n }));\n\n // repositioning the features is always an option\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-move',\n title: t.append('issues.fix.reposition_features.title')\n }));\n\n if (featureType1 === 'building' || featureType2 === 'building') {\n // if the validation is about overlapping buildings:\n // show \"reposition features\" suggestion first, as that is most often\n // most sensible fix for those errors, see #11329\n fixes.unshift(fixes.pop());\n }\n\n return fixes;\n }\n });\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.crossing_ways.' + crossingTypeID + '.reference'));\n }\n }\n\n function makeAddBridgeOrTunnelFix(fixTitleID, iconName, bridgeOrTunnel){\n return new validationIssueFix({\n icon: iconName,\n title: t.append('issues.fix.' + fixTitleID + '.title'),\n onClick: function(context) {\n var mode = context.mode();\n if (!mode || mode.id !== 'select') return;\n\n var selectedIDs = mode.selectedIDs();\n if (selectedIDs.length !== 1) return;\n\n var selectedWayID = selectedIDs[0];\n if (!context.hasEntity(selectedWayID)) return;\n\n var resultWayIDs = [selectedWayID];\n\n var edge, crossedEdge, crossedWayID;\n if (this.issue.entityIds[0] === selectedWayID) {\n edge = this.issue.data.edges[0];\n crossedEdge = this.issue.data.edges[1];\n crossedWayID = this.issue.entityIds[1];\n } else {\n edge = this.issue.data.edges[1];\n crossedEdge = this.issue.data.edges[0];\n crossedWayID = this.issue.entityIds[0];\n }\n\n var crossingLoc = this.issue.loc;\n\n var projection = context.projection;\n\n var action = function actionAddStructure(graph) {\n\n var edgeNodes = [graph.entity(edge[0]), graph.entity(edge[1])];\n\n var crossedWay = graph.hasEntity(crossedWayID);\n // use the explicit width of the crossed feature as the structure length, if available\n var structLengthMeters = crossedWay && isFinite(crossedWay.tags.width) && Number(crossedWay.tags.width);\n if (!structLengthMeters) {\n // if no explicit width is set, approximate the width based on the tags\n structLengthMeters = crossedWay && crossedWay.impliedLineWidthMeters();\n }\n if (structLengthMeters) {\n if (getFeatureType(crossedWay, graph) === 'railway') {\n // bridges over railways are generally much longer than the rail bed itself, compensate\n structLengthMeters *= 2;\n }\n } else {\n // should ideally never land here since all rail/water/road tags should have an implied width\n structLengthMeters = 8;\n }\n\n var a1 = geoAngle(edgeNodes[0], edgeNodes[1], projection) + Math.PI;\n var a2 = geoAngle(graph.entity(crossedEdge[0]), graph.entity(crossedEdge[1]), projection) + Math.PI;\n var crossingAngle = Math.max(a1, a2) - Math.min(a1, a2);\n if (crossingAngle > Math.PI) crossingAngle -= Math.PI;\n // lengthen the structure to account for the angle of the crossing\n structLengthMeters = ((structLengthMeters / 2) / Math.sin(crossingAngle)) * 2;\n\n // add padding since the structure must extend past the edges of the crossed feature\n structLengthMeters += 4;\n\n // clamp the length to a reasonable range\n structLengthMeters = Math.min(Math.max(structLengthMeters, 4), 50);\n\n function geomToProj(geoPoint) {\n return [\n geoLonToMeters(geoPoint[0], geoPoint[1]),\n geoLatToMeters(geoPoint[1])\n ];\n }\n function projToGeom(projPoint) {\n var lat = geoMetersToLat(projPoint[1]);\n return [\n geoMetersToLon(projPoint[0], lat),\n lat\n ];\n }\n\n var projEdgeNode1 = geomToProj(edgeNodes[0].loc);\n var projEdgeNode2 = geomToProj(edgeNodes[1].loc);\n\n var projectedAngle = geoVecAngle(projEdgeNode1, projEdgeNode2);\n\n var projectedCrossingLoc = geomToProj(crossingLoc);\n var linearToSphericalMetersRatio = geoVecLength(projEdgeNode1, projEdgeNode2) /\n geoSphericalDistance(edgeNodes[0].loc, edgeNodes[1].loc);\n\n function locSphericalDistanceFromCrossingLoc(angle, distanceMeters) {\n var lengthSphericalMeters = distanceMeters * linearToSphericalMetersRatio;\n return projToGeom([\n projectedCrossingLoc[0] + Math.cos(angle) * lengthSphericalMeters,\n projectedCrossingLoc[1] + Math.sin(angle) * lengthSphericalMeters\n ]);\n }\n\n var endpointLocGetter1 = function(lengthMeters) {\n return locSphericalDistanceFromCrossingLoc(projectedAngle, lengthMeters);\n };\n var endpointLocGetter2 = function(lengthMeters) {\n return locSphericalDistanceFromCrossingLoc(projectedAngle + Math.PI, lengthMeters);\n };\n\n // avoid creating very short edges from splitting too close to another node\n var minEdgeLengthMeters = 0.55;\n\n // decide where to bound the structure along the way, splitting as necessary\n function determineEndpoint(edge, endNode, locGetter) {\n var newNode;\n\n var idealLengthMeters = structLengthMeters / 2;\n\n // distance between the crossing location and the end of the edge,\n // the maximum length of this side of the structure\n var crossingToEdgeEndDistance = geoSphericalDistance(crossingLoc, endNode.loc);\n\n if (crossingToEdgeEndDistance - idealLengthMeters > minEdgeLengthMeters) {\n // the edge is long enough to insert a new node\n\n // the loc that would result in the full expected length\n var idealNodeLoc = locGetter(idealLengthMeters);\n\n newNode = osmNode();\n graph = actionAddMidpoint({ loc: idealNodeLoc, edge: edge }, newNode)(graph);\n\n } else {\n var edgeCount = 0;\n endNode.parentIntersectionWays(graph).forEach(function(way) {\n way.nodes.forEach(function(nodeID) {\n if (nodeID === endNode.id) {\n if ((endNode.id === way.first() && endNode.id !== way.last()) ||\n (endNode.id === way.last() && endNode.id !== way.first())) {\n edgeCount += 1;\n } else {\n edgeCount += 2;\n }\n }\n });\n });\n\n if (edgeCount >= 3) {\n // the end node is a junction, try to leave a segment\n // between it and the structure - #7202\n\n var insetLength = crossingToEdgeEndDistance - minEdgeLengthMeters;\n if (insetLength > minEdgeLengthMeters) {\n var insetNodeLoc = locGetter(insetLength);\n newNode = osmNode();\n graph = actionAddMidpoint({ loc: insetNodeLoc, edge: edge }, newNode)(graph);\n }\n }\n }\n\n // if the edge is too short to subdivide as desired, then\n // just bound the structure at the existing end node\n if (!newNode) newNode = endNode;\n\n var splitAction = actionSplit([newNode.id])\n .limitWays(resultWayIDs); // only split selected or created ways\n\n // do the split\n graph = splitAction(graph);\n if (splitAction.getCreatedWayIDs().length) {\n resultWayIDs.push(splitAction.getCreatedWayIDs()[0]);\n }\n\n return newNode;\n }\n\n var structEndNode1 = determineEndpoint(edge, edgeNodes[1], endpointLocGetter1);\n var structEndNode2 = determineEndpoint([edgeNodes[0].id, structEndNode1.id], edgeNodes[0], endpointLocGetter2);\n\n var structureWay = resultWayIDs.map(function(id) {\n return graph.entity(id);\n }).find(function(way) {\n return way.nodes.indexOf(structEndNode1.id) !== -1 &&\n way.nodes.indexOf(structEndNode2.id) !== -1;\n });\n\n var tags = Object.assign({}, structureWay.tags); // copy tags\n if (bridgeOrTunnel === 'bridge'){\n tags.bridge = 'yes';\n tags.layer = '1';\n } else {\n var tunnelValue = 'yes';\n if (getFeatureType(structureWay, graph) === 'waterway') {\n // use `tunnel=culvert` for waterways by default\n tunnelValue = 'culvert';\n }\n tags.tunnel = tunnelValue;\n tags.layer = '-1';\n }\n // apply the structure tags to the way\n graph = actionChangeTags(structureWay.id, tags)(graph);\n return graph;\n };\n\n context.perform(action, t('issues.fix.' + fixTitleID + '.annotation'));\n context.enter(modeSelect(context, resultWayIDs));\n }\n });\n }\n\n function makeConnectWaysFix(connectionTags) {\n\n var fixTitleID = 'connect_features';\n var fixIcon = 'iD-icon-crossing';\n if (connectionTags.highway === 'crossing') {\n fixTitleID = 'connect_using_crossing';\n fixIcon = 'temaki-pedestrian';\n }\n if (connectionTags.ford) {\n fixTitleID = 'connect_using_ford';\n fixIcon = 'roentgen-ford';\n }\n\n const fix = new validationIssueFix({\n icon: fixIcon,\n title: t.append('issues.fix.' + fixTitleID + '.title'),\n onClick: function(context) {\n var loc = this.issue.loc;\n var edges = this.issue.data.edges;\n\n context.perform(\n function actionConnectCrossingWays(graph) {\n // create the new node for the points\n var node = osmNode({ loc: loc, tags: connectionTags });\n graph = graph.replace(node);\n\n var nodesToMerge = [node.id];\n var mergeThresholdInMeters = 0.75;\n\n edges.forEach(function(edge) {\n var edgeNodes = [graph.entity(edge[0]), graph.entity(edge[1])];\n var nearby = geoSphericalClosestNode(edgeNodes, loc);\n // if there is already a suitable node nearby, use that\n // use the node if node has no interesting tags or if it is a crossing node #8326\n if ((!nearby.node.hasInterestingTags() || nearby.node.isCrossing()) && nearby.distance < mergeThresholdInMeters) {\n nodesToMerge.push(nearby.node.id);\n // else add the new node to the way\n } else {\n graph = actionAddMidpoint({loc: loc, edge: edge}, node)(graph);\n }\n });\n\n if (nodesToMerge.length > 1) {\n // if we're using nearby nodes, merge them with the new node\n graph = actionMergeNodes(nodesToMerge, loc)(graph);\n }\n\n return graph;\n },\n t('issues.fix.connect_crossing_features.annotation')\n );\n }\n });\n fix._connectionTags = connectionTags;\n return fix;\n }\n\n /** @returns {osmEntity | undefined} */\n function getSelectedFeature() {\n const mode = context.mode();\n if (!mode || mode.id !== 'select') return undefined;\n\n const selectedIDs = mode.selectedIDs();\n if (selectedIDs.length !== 1) return undefined;\n\n const selectedID = selectedIDs[0];\n\n const entity = context.hasEntity(selectedID);\n return entity;\n }\n\n /**\n * @param {\"higher\" | \"lower\"} higherOrLower\n * @returns {validationIssueFix | undefined}\n */\n function makeChangeLayerFix(higherOrLower) {\n const selectedFeature = getSelectedFeature();\n return new validationIssueFix({\n id: selectedFeature.id,\n icon: 'iD-icon-' + (higherOrLower === 'higher' ? 'up' : 'down'),\n title: selectedFeature\n ? t.append('issues.fix.tag_this_as_' + higherOrLower + '.informative_title', {\n feature: utilDisplayLabel(selectedFeature, context.graph())\n })\n // in this context, there is no selected feature so we\n // have to show a generic name\n : t.append('issues.fix.tag_this_as_' + higherOrLower + '.title'),\n\n onClick: function(context) {\n const entity = getSelectedFeature();\n const selectedID = entity.id;\n if (!entity) return;\n\n\n if (!this.issue.entityIds.some(function(entityId) {\n return entityId === selectedID;\n })) return;\n\n var tags = Object.assign({}, entity.tags); // shallow copy\n var layer = tags.layer && Number(tags.layer);\n if (layer && !isNaN(layer)) {\n if (higherOrLower === 'higher') {\n layer += 1;\n } else {\n layer -= 1;\n }\n } else {\n if (higherOrLower === 'higher') {\n layer = 1;\n } else {\n layer = -1;\n }\n }\n tags.layer = layer.toString();\n context.perform(\n actionChangeTags(entity.id, tags),\n t('operations.change_tags.annotation')\n );\n }\n });\n }\n\n validation.type = type;\n\n return validation;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { t } from '../core/localizer';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionMoveNode } from '../actions/move_node';\nimport { actionNoop } from '../actions/noop';\nimport { behaviorDraw } from './draw';\nimport { geoChooseEdge, geoHasSelfIntersections } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect } from '../modes/select';\nimport { osmNode } from '../osm/node';\nimport { utilRebind } from '../util/rebind';\nimport { utilKeybinding } from '../util';\n\nexport function behaviorDrawWay(context, wayID, mode, startGraph) {\n const keybinding = utilKeybinding('drawWay');\n\n var dispatch = d3_dispatch('rejectedSelfIntersection');\n\n var behavior = behaviorDraw(context);\n\n // Must be set by `drawWay.nodeIndex` before each install of this behavior.\n var _nodeIndex;\n\n var _origWay;\n var _wayGeometry;\n var _headNodeID;\n var _annotation;\n\n var _pointerHasMoved = false;\n\n // The osmNode to be placed.\n // This is temporary and just follows the mouse cursor until an \"add\" event occurs.\n var _drawNode;\n\n var _didResolveTempEdit = false;\n\n function createDrawNode(loc) {\n // don't make the draw node until we actually need it\n _drawNode = osmNode({ loc: loc });\n\n context.pauseChangeDispatch();\n context.replace(function actionAddDrawNode(graph) {\n // add the draw node to the graph and insert it into the way\n var way = graph.entity(wayID);\n return graph\n .replace(_drawNode)\n .replace(way.addNode(_drawNode.id, _nodeIndex));\n }, _annotation);\n context.resumeChangeDispatch();\n\n setActiveElements();\n }\n\n function removeDrawNode() {\n\n context.pauseChangeDispatch();\n context.replace(\n function actionDeleteDrawNode(graph) {\n var way = graph.entity(wayID);\n return graph\n .replace(way.removeNode(_drawNode.id))\n .remove(_drawNode);\n },\n _annotation\n );\n _drawNode = undefined;\n context.resumeChangeDispatch();\n }\n\n\n function keydown(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope')) {\n context.surface()\n .classed('nope-suppressed', true);\n }\n context.surface()\n .classed('nope', false)\n .classed('nope-disabled', true);\n }\n }\n\n\n function keyup(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope-suppressed')) {\n context.surface()\n .classed('nope', true);\n }\n context.surface()\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false);\n }\n }\n\n\n function allowsVertex(d) {\n return d.geometry(context.graph()) === 'vertex' || presetManager.allowsVertex(d, context.graph());\n }\n\n\n // related code\n // - `mode/drag_node.js` `doMove()`\n // - `behavior/draw.js` `click()`\n // - `behavior/draw_way.js` `move()`\n function move(d3_event, datum) {\n\n var loc = context.map().mouseCoordinates();\n\n if (!_drawNode) createDrawNode(loc);\n\n context.surface().classed('nope-disabled', d3_event.altKey);\n\n var targetLoc = datum && datum.properties && datum.properties.entity &&\n allowsVertex(datum.properties.entity) && datum.properties.entity.loc;\n var targetNodes = datum && datum.properties && datum.properties.nodes;\n\n if (targetLoc) { // snap to node/vertex - a point target with `.loc`\n loc = targetLoc;\n\n } else if (targetNodes) { // snap to way - a line target with `.nodes`\n var choice = geoChooseEdge(targetNodes, context.map().mouse(), context.projection, _drawNode.id);\n if (choice) {\n loc = choice.loc;\n }\n }\n\n context.replace(actionMoveNode(_drawNode.id, loc), _annotation);\n _drawNode = context.entity(_drawNode.id);\n checkGeometry(true /* includeDrawNode */);\n }\n\n\n // Check whether this edit causes the geometry to break.\n // If so, class the surface with a nope cursor.\n // `includeDrawNode` - Only check the relevant line segments if finishing drawing\n function checkGeometry(includeDrawNode) {\n var nopeDisabled = context.surface().classed('nope-disabled');\n var isInvalid = isInvalidGeometry(includeDrawNode);\n\n if (nopeDisabled) {\n context.surface()\n .classed('nope', false)\n .classed('nope-suppressed', isInvalid);\n } else {\n context.surface()\n .classed('nope', isInvalid)\n .classed('nope-suppressed', false);\n }\n }\n\n\n function isInvalidGeometry(includeDrawNode) {\n\n var testNode = _drawNode;\n\n // we only need to test the single way we're drawing\n var parentWay = context.graph().entity(wayID);\n var nodes = context.graph().childNodes(parentWay).slice(); // shallow copy\n\n if (includeDrawNode) {\n if (parentWay.isClosed()) {\n // don't test the last segment for closed ways - #4655\n // (still test the first segment)\n nodes.pop();\n }\n } else { // discount the draw node\n\n if (parentWay.isClosed()) {\n if (nodes.length < 3) return false;\n if (_drawNode) nodes.splice(-2, 1);\n testNode = nodes[nodes.length - 2];\n } else {\n // there's nothing we need to test if we ignore the draw node on open ways\n return false;\n }\n }\n\n return testNode && geoHasSelfIntersections(nodes, testNode.id);\n }\n\n\n function undone() {\n\n // undoing removed the temp edit\n _didResolveTempEdit = true;\n\n context.pauseChangeDispatch();\n\n var nextMode;\n\n if (context.graph() === startGraph) {\n // We've undone back to the initial state before we started drawing.\n // Just exit the draw mode without undoing whatever we did before\n // we entered the draw mode.\n nextMode = modeSelect(context, [wayID]);\n } else {\n // The `undo` only removed the temporary edit, so here we have to\n // manually undo to actually remove the last node we added. We can't\n // use the `undo` function since the initial \"add\" graph doesn't have\n // an annotation and so cannot be undone to.\n context.pop(1);\n\n // continue drawing\n nextMode = mode;\n }\n\n // clear the redo stack by adding and removing a blank edit\n context.perform(actionNoop());\n context.pop(1);\n\n context.resumeChangeDispatch();\n context.enter(nextMode);\n }\n\n\n function setActiveElements() {\n if (!_drawNode) return;\n\n context.surface().selectAll('.' + _drawNode.id)\n .classed('active', true);\n }\n\n\n function resetToStartGraph() {\n while (context.graph() !== startGraph) {\n context.pop();\n }\n }\n\n\n var drawWay = function(surface) {\n _drawNode = undefined;\n _didResolveTempEdit = false;\n _origWay = context.entity(wayID);\n\n if (typeof _nodeIndex === 'number') {\n _headNodeID = _origWay.nodes[_nodeIndex];\n } else if (_origWay.isClosed()) {\n _headNodeID = _origWay.nodes[_origWay.nodes.length - 2];\n } else {\n _headNodeID = _origWay.nodes[_origWay.nodes.length - 1];\n }\n\n _wayGeometry = _origWay.geometry(context.graph());\n _annotation = t((_origWay.nodes.length === (_origWay.isClosed() ? 2 : 1) ?\n 'operations.start.annotation.' :\n 'operations.continue.annotation.') + _wayGeometry\n );\n _pointerHasMoved = false;\n\n // Push an annotated state for undo to return back to.\n // We must make sure to replace or remove it later.\n context.pauseChangeDispatch();\n context.perform(actionNoop(), _annotation);\n context.resumeChangeDispatch();\n\n behavior.hover()\n .initialNodeID(_headNodeID);\n\n behavior\n .on('move', function() {\n _pointerHasMoved = true;\n move.apply(this, arguments);\n })\n .on('down', function() {\n move.apply(this, arguments);\n })\n .on('downcancel', function() {\n if (_drawNode) removeDrawNode();\n })\n .on('click', drawWay.add)\n .on('clickWay', drawWay.addWay)\n .on('clickNode', drawWay.addNode)\n .on('undo', context.undo)\n .on('cancel', drawWay.cancel)\n .on('finish', drawWay.finish);\n\n d3_select(window)\n .on('keydown.drawWay', keydown)\n .on('keyup.drawWay', keyup);\n\n context.map()\n .dblclickZoomEnable(false)\n .on('drawn.draw', setActiveElements);\n\n setActiveElements();\n\n surface.call(behavior);\n\n context.history()\n .on('undone.draw', undone);\n };\n\n\n drawWay.off = function(surface) {\n\n if (!_didResolveTempEdit) {\n // Drawing was interrupted unexpectedly.\n // This can happen if the user changes modes,\n // clicks geolocate button, a hashchange event occurs, etc.\n\n context.pauseChangeDispatch();\n resetToStartGraph();\n context.resumeChangeDispatch();\n }\n\n _drawNode = undefined;\n _nodeIndex = undefined;\n\n context.map()\n .on('drawn.draw', null);\n\n surface.call(behavior.off)\n .selectAll('.active')\n .classed('active', false);\n\n surface\n .classed('nope', false)\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false);\n\n d3_select(window)\n .on('keydown.drawWay', null)\n .on('keyup.drawWay', null);\n\n context.history()\n .on('undone.draw', null);\n };\n\n\n function attemptAdd(d, loc, doAdd) {\n\n if (_drawNode) {\n // move the node to the final loc in case move wasn't called\n // consistently (e.g. on touch devices)\n context.replace(actionMoveNode(_drawNode.id, loc), _annotation);\n _drawNode = context.entity(_drawNode.id);\n } else {\n createDrawNode(loc);\n }\n\n checkGeometry(true /* includeDrawNode */);\n if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {\n if (!_pointerHasMoved) {\n // prevent the temporary draw node from appearing on touch devices\n removeDrawNode();\n }\n dispatch.call('rejectedSelfIntersection', this);\n return; // can't click here\n }\n\n context.pauseChangeDispatch();\n doAdd();\n // we just replaced the temporary edit with the real one\n _didResolveTempEdit = true;\n context.resumeChangeDispatch();\n\n context.enter(mode);\n }\n\n\n // Accept the current position of the drawing node\n drawWay.add = function(loc, d) {\n attemptAdd(d, loc, function() {\n // don't need to do anything extra\n });\n };\n\n\n // Connect the way to an existing way\n drawWay.addWay = function(loc, edge, d) {\n attemptAdd(d, loc, function() {\n context.replace(\n actionAddMidpoint({ loc: loc, edge: edge }, _drawNode),\n _annotation\n );\n });\n };\n\n\n // Connect the way to an existing node\n drawWay.addNode = function(node, d) {\n\n // finish drawing if the mapper targets the prior node\n if (node.id === _headNodeID ||\n // or the first node when drawing an area\n (_origWay.isClosed() && node.id === _origWay.first())) {\n drawWay.finish();\n return;\n }\n\n attemptAdd(d, node.loc, function() {\n context.replace(\n function actionReplaceDrawNode(graph) {\n // remove the temporary draw node and insert the existing node\n // at the same index\n\n graph = graph\n .replace(graph.entity(wayID).removeNode(_drawNode.id))\n .remove(_drawNode);\n return graph\n .replace(graph.entity(wayID).addNode(node.id, _nodeIndex));\n },\n _annotation\n );\n });\n };\n\n /**\n * @param {(typeof osmWay)[]} ways\n * @returns {\"line\" | \"area\" | \"generic\"}\n */\n function getFeatureType(ways) {\n if (ways.every(way => way.isClosed())) return 'area';\n if (ways.every(way => !way.isClosed())) return 'line';\n return 'generic';\n }\n\n /** see PR #8671 */\n function followMode() {\n if (_didResolveTempEdit) return;\n\n try {\n\n // get the last 2 added nodes.\n // check if they are both part of only oneway (the same one)\n // check if the ways that they're part of are the same way\n // find index of the last two nodes, to determine the direction to travel around the existing way\n // add the next node to the way we are drawing\n\n // if we're drawing an area, the first node = last node.\n const isDrawingArea = _origWay.nodes[0] === _origWay.nodes.slice(-1)[0];\n\n const [secondLastNodeId, lastNodeId] = _origWay.nodes.slice(isDrawingArea ? -3 : -2);\n\n // Unlike startGraph, the full history graph may contain unsaved vertices to follow.\n // https://github.com/openstreetmap/iD/issues/8749\n const historyGraph = context.history().graph();\n if (!lastNodeId || !secondLastNodeId || !historyGraph.hasEntity(lastNodeId) || !historyGraph.hasEntity(secondLastNodeId)) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('operations.follow.error.needs_more_initial_nodes'))();\n return;\n }\n\n // If the way has looped over itself, follow some other way.\n const lastNodesParents = historyGraph.parentWays(historyGraph.entity(lastNodeId)).filter(w => w.id !== wayID);\n const secondLastNodesParents = historyGraph.parentWays(historyGraph.entity(secondLastNodeId)).filter(w => w.id !== wayID);\n\n const featureType = getFeatureType(lastNodesParents);\n\n if (lastNodesParents.length !== 1 || secondLastNodesParents.length === 0) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append(`operations.follow.error.intersection_of_multiple_ways.${featureType}`))();\n return;\n }\n\n // Check if the last node's parent is also the parent of the second last node.\n // The last node must only have one parent, but the second last node can have\n // multiple parents.\n if (!secondLastNodesParents.some(n => n.id === lastNodesParents[0].id)) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append(`operations.follow.error.intersection_of_different_ways.${featureType}`))();\n return;\n }\n\n const way = lastNodesParents[0];\n\n const indexOfLast = way.nodes.indexOf(lastNodeId);\n const indexOfSecondLast = way.nodes.indexOf(secondLastNodeId);\n\n // for a closed way, the first/last node is the same so it appears twice in the array,\n // but indexOf always finds the first occurrence. This is only an issue when following a way\n // in descending order\n const isDescendingPastZero = indexOfLast === way.nodes.length - 2 && indexOfSecondLast === 0;\n\n let nextNodeIndex = indexOfLast + (indexOfLast > indexOfSecondLast && !isDescendingPastZero ? 1 : -1);\n // if we're following a closed way and we pass the first/last node, the next index will be -1\n if (nextNodeIndex === -1) nextNodeIndex = indexOfSecondLast === 1 ? way.nodes.length - 2 : 1;\n\n const nextNode = historyGraph.entity(way.nodes[nextNodeIndex]);\n\n drawWay.addNode(nextNode, {\n geometry: { type: 'Point', coordinates: nextNode.loc },\n id: nextNode.id,\n properties: { target: true, entity: nextNode },\n });\n } catch {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('operations.follow.error.unknown'))();\n }\n }\n\n keybinding.on(t('operations.follow.key'), followMode);\n d3_select(document).call(keybinding);\n\n // Finish the draw operation, removing the temporary edit.\n // If the way has enough nodes to be valid, it's selected.\n // Otherwise, delete everything and return to browse mode.\n drawWay.finish = function() {\n checkGeometry(false /* includeDrawNode */);\n if (context.surface().classed('nope')) {\n dispatch.call('rejectedSelfIntersection', this);\n return; // can't click here\n }\n\n context.pauseChangeDispatch();\n // remove the temporary edit\n context.pop(1);\n _didResolveTempEdit = true;\n context.resumeChangeDispatch();\n\n var way = context.hasEntity(wayID);\n if (!way || way.isDegenerate()) {\n drawWay.cancel();\n return;\n }\n\n window.setTimeout(function() {\n context.map().dblclickZoomEnable(true);\n }, 1000);\n\n var isNewFeature = !mode.isContinuing;\n context.enter(modeSelect(context, [wayID]).newFeature(isNewFeature));\n };\n\n\n // Cancel the draw operation, delete everything, and return to browse mode.\n drawWay.cancel = function() {\n context.pauseChangeDispatch();\n resetToStartGraph();\n context.resumeChangeDispatch();\n\n window.setTimeout(function() {\n context.map().dblclickZoomEnable(true);\n }, 1000);\n\n context.surface()\n .classed('nope', false)\n .classed('nope-disabled', false)\n .classed('nope-suppressed', false);\n\n context.enter(modeBrowse(context));\n };\n\n\n drawWay.nodeIndex = function(val) {\n if (!arguments.length) return _nodeIndex;\n _nodeIndex = val;\n return drawWay;\n };\n\n\n drawWay.activeID = function() {\n if (!arguments.length) return _drawNode && _drawNode.id;\n // no assign\n return drawWay;\n };\n\n\n return utilRebind(drawWay, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDrawWay } from '../behavior/draw_way';\n\n\nexport function modeDrawLine(context, wayID, startGraph, button, affix, continuing) {\n var mode = {\n button: button,\n id: 'draw-line'\n };\n\n var behavior = behaviorDrawWay(context, wayID, mode, startGraph)\n .on('rejectedSelfIntersection.modeDrawLine', function() {\n context.ui().flash\n .iconName('#iD-icon-no')\n .label(t.append('self_intersection.error.lines'))();\n });\n\n mode.wayID = wayID;\n\n mode.isContinuing = continuing;\n\n mode.enter = function() {\n behavior\n .nodeIndex(affix === 'prefix' ? 0 : undefined);\n\n context.install(behavior);\n };\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n mode.selectedIDs = function() {\n return [wayID];\n };\n\n mode.activeID = function() {\n return (behavior && behavior.activeID()) || [];\n };\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { utilDetect } from '../util/detect';\n\n\n// Translate a MacOS key command into the appropriate Windows/Linux equivalent.\n// For example, \u2318Z -> Ctrl+Z\nexport var uiCmd = function (code) {\n var detected = utilDetect();\n\n if (detected.os === 'mac') {\n return code;\n }\n\n if (detected.os === 'win') {\n if (code === '\u2318\u21E7Z') return 'Ctrl+Y';\n }\n\n var result = '',\n replacements = {\n '\u2318': 'Ctrl',\n '\u21E7': 'Shift',\n '\u2325': 'Alt',\n '\u232B': 'Backspace',\n '\u2326': 'Delete'\n };\n\n for (var i = 0; i < code.length; i++) {\n if (code[i] in replacements) {\n result += replacements[code[i]] + (i < code.length - 1 ? '+' : '');\n } else {\n result += code[i];\n }\n }\n\n return result;\n};\n\n\n// return a display-focused string for a given keyboard code\nuiCmd.display = function(code) {\n if (code.length !== 1) return code;\n\n var detected = utilDetect();\n var mac = (detected.os === 'mac');\n var replacements = {\n '\u2318': mac ? '\u2318 ' + t('shortcuts.key.cmd') : t('shortcuts.key.ctrl'),\n '\u21E7': mac ? '\u21E7 ' + t('shortcuts.key.shift') : t('shortcuts.key.shift'),\n '\u2325': mac ? '\u2325 ' + t('shortcuts.key.option') : t('shortcuts.key.alt'),\n '\u2303': mac ? '\u2303 ' + t('shortcuts.key.ctrl') : t('shortcuts.key.ctrl'),\n '\u232B': mac ? '\u232B ' + t('shortcuts.key.delete') : t('shortcuts.key.backspace'),\n '\u2326': mac ? '\u2326 ' + t('shortcuts.key.del') : t('shortcuts.key.del'),\n '\u2196': mac ? '\u2196 ' + t('shortcuts.key.pgup') : t('shortcuts.key.pgup'),\n '\u2198': mac ? '\u2198 ' + t('shortcuts.key.pgdn') : t('shortcuts.key.pgdn'),\n '\u21DE': mac ? '\u21DE ' + t('shortcuts.key.home') : t('shortcuts.key.home'),\n '\u21DF': mac ? '\u21DF ' + t('shortcuts.key.end') : t('shortcuts.key.end'),\n '\u21B5': mac ? '\u23CE ' + t('shortcuts.key.return') : t('shortcuts.key.enter'),\n '\u238B': mac ? '\u238B ' + t('shortcuts.key.esc') : t('shortcuts.key.esc'),\n '\u2630': mac ? '\u2630 ' + t('shortcuts.key.menu') : t('shortcuts.key.menu'),\n };\n\n return replacements[code] || code;\n};\n", "import { t } from '../core/localizer';\nimport { actionDeleteMultiple } from '../actions/delete_multiple';\nimport { behaviorOperation } from '../behavior/operation';\nimport { geoSphericalDistance } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect } from '../modes/select';\nimport { uiCmd } from '../ui/cmd';\nimport { utilGetAllNodes, utilTotalExtent } from '../util';\n\n\nexport function operationDelete(context, selectedIDs) {\n var multi = (selectedIDs.length === 1 ? 'single' : 'multiple');\n var action = actionDeleteMultiple(selectedIDs);\n var nodes = utilGetAllNodes(selectedIDs, context.graph());\n var coords = nodes.map(function(n) { return n.loc; });\n var extent = utilTotalExtent(selectedIDs, context.graph());\n\n\n var operation = function() {\n var nextSelectedID;\n var nextSelectedLoc;\n\n if (selectedIDs.length === 1) {\n var id = selectedIDs[0];\n var entity = context.entity(id);\n var geometry = entity.geometry(context.graph());\n var parents = context.graph().parentWays(entity);\n var parent = parents[0];\n\n // Select the next closest node in the way.\n if (geometry === 'vertex') {\n var nodes = parent.nodes;\n var i = nodes.indexOf(id);\n\n if (i === 0) {\n i++;\n } else if (i === nodes.length - 1) {\n i--;\n } else {\n var a = geoSphericalDistance(entity.loc, context.entity(nodes[i - 1]).loc);\n var b = geoSphericalDistance(entity.loc, context.entity(nodes[i + 1]).loc);\n i = a < b ? i - 1 : i + 1;\n }\n\n nextSelectedID = nodes[i];\n nextSelectedLoc = context.entity(nextSelectedID).loc;\n }\n }\n\n context.perform(action, operation.annotation());\n context.validator().validate();\n\n if (nextSelectedID && nextSelectedLoc) {\n if (context.hasEntity(nextSelectedID)) {\n context.enter(modeSelect(context, [nextSelectedID]).follow(true));\n } else {\n context.map().centerEase(nextSelectedLoc);\n context.enter(modeBrowse(context));\n }\n } else {\n context.enter(modeBrowse(context));\n }\n\n };\n\n\n operation.available = function() {\n return true;\n };\n\n\n operation.disabled = function() {\n if (extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n } else if (selectedIDs.some(protectedMember)) {\n return 'part_of_relation';\n } else if (selectedIDs.some(incompleteRelation)) {\n return 'incomplete_relation';\n } else if (selectedIDs.some(hasWikidataTag)) {\n return 'has_wikidata_tag';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n\n function hasWikidataTag(id) {\n var entity = context.entity(id);\n return entity.tags.wikidata && entity.tags.wikidata.trim().length > 0;\n }\n\n function incompleteRelation(id) {\n var entity = context.entity(id);\n return entity.type === 'relation' && !entity.isComplete(context.graph());\n }\n\n function protectedMember(id) {\n var entity = context.entity(id);\n if (entity.type !== 'way') return false;\n\n var parents = context.graph().parentRelations(entity);\n for (var i = 0; i < parents.length; i++) {\n var parent = parents[i];\n var type = parent.tags.type;\n var role = parent.memberById(id).role || 'outer';\n if (type === 'route' || type === 'boundary' || (type === 'multipolygon' && role === 'outer')) {\n return true;\n }\n }\n return false;\n }\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.delete.' + disable + '.' + multi) :\n t.append('operations.delete.description.' + multi);\n };\n\n\n operation.annotation = function() {\n return selectedIDs.length === 1 ?\n t('operations.delete.annotation.' + context.graph().geometry(selectedIDs[0])) :\n t('operations.delete.annotation.feature', { n: selectedIDs.length });\n };\n\n\n operation.id = 'delete';\n operation.keys = [uiCmd('\u2318\u232B'), uiCmd('\u2318\u2326'), uiCmd('\u2326')];\n operation.title = t.append('operations.delete.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import { t, localizer } from '../core/localizer';\nimport { modeDrawLine } from '../modes/draw_line';\nimport { operationDelete } from '../operations/delete';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { osmRoutableHighwayTagValues } from '../osm/tags';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\nexport function validationDisconnectedWay() {\n var type = 'disconnected_way';\n\n function isTaggedAsHighway(entity) {\n return osmRoutableHighwayTagValues[entity.tags.highway];\n }\n\n var validation = function checkDisconnectedWay(entity, graph) {\n\n var routingIslandWays = routingIslandForEntity(entity);\n if (!routingIslandWays) return [];\n\n return [new validationIssue({\n type: type,\n subtype: 'highway',\n severity: 'warning',\n message: function(context) {\n var entity = this.entityIds.length && context.hasEntity(this.entityIds[0]);\n var label = entity && utilDisplayLabel(entity, context.graph());\n return t.append('issues.disconnected_way.routable.message', { count: this.entityIds.length, highway: label });\n },\n reference: showReference,\n entityIds: Array.from(routingIslandWays).map(function(way) { return way.id; }),\n dynamicFixes: makeFixes\n })];\n\n\n function makeFixes(context) {\n\n var fixes = [];\n\n var singleEntity = this.entityIds.length === 1 && context.hasEntity(this.entityIds[0]);\n\n if (singleEntity) {\n\n if (singleEntity.type === 'way' && !singleEntity.isClosed()) {\n\n var textDirection = localizer.textDirection();\n\n var startFix = makeContinueDrawingFixIfAllowed(textDirection, singleEntity.first(), 'start');\n if (startFix) fixes.push(startFix);\n\n var endFix = makeContinueDrawingFixIfAllowed(textDirection, singleEntity.last(), 'end');\n if (endFix) fixes.push(endFix);\n }\n if (!fixes.length) {\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.connect_feature.title')\n }));\n }\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.delete_feature.title'),\n entityIds: [singleEntity.id],\n onClick: function(context) {\n var id = this.issue.entityIds[0];\n var operation = operationDelete(context, [id]);\n if (!operation.disabled()) {\n operation();\n }\n }\n }));\n } else {\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.connect_features.title')\n }));\n }\n\n return fixes;\n }\n\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.disconnected_way.routable.reference'));\n }\n\n function routingIslandForEntity(entity) {\n\n var routingIsland = new Set(); // the interconnected routable features\n var waysToCheck = []; // the queue of remaining routable ways to traverse\n\n function queueParentWays(node) {\n graph.parentWays(node).forEach(function(parentWay) {\n if (!routingIsland.has(parentWay) && // only check each feature once\n isRoutableWay(parentWay, false)) { // only check routable features\n routingIsland.add(parentWay);\n waysToCheck.push(parentWay);\n }\n });\n }\n\n if (entity.type === 'way' && isRoutableWay(entity, true)) {\n\n routingIsland.add(entity);\n waysToCheck.push(entity);\n\n } else if (entity.type === 'node' && isRoutableNode(entity)) {\n\n routingIsland.add(entity);\n queueParentWays(entity);\n\n } else {\n // this feature isn't routable, cannot be a routing island\n return null;\n }\n\n while (waysToCheck.length) {\n var wayToCheck = waysToCheck.pop();\n var childNodes = graph.childNodes(wayToCheck);\n for (var i in childNodes) {\n var vertex = childNodes[i];\n\n if (isConnectedVertex(vertex)) {\n // found a link to the wider network, not a routing island\n return null;\n }\n\n if (isRoutableNode(vertex)) {\n routingIsland.add(vertex);\n }\n\n queueParentWays(vertex);\n }\n }\n\n // no network link found, this is a routing island, return its members\n return routingIsland;\n }\n\n function isConnectedVertex(vertex) {\n // assume ways overlapping unloaded tiles are connected to the wider road network - #5938\n var osm = services.osm;\n if (osm && !osm.isDataLoaded(vertex.loc)) return true;\n\n // entrances are considered connected\n if (vertex.tags.entrance &&\n vertex.tags.entrance !== 'no') return true;\n if (vertex.tags.amenity === 'parking_entrance') return true;\n\n return false;\n }\n\n function isRoutableNode(node) {\n // treat elevators as distinct features in the highway network\n if (node.tags.highway === 'elevator') return true;\n return false;\n }\n\n function isRoutableWay(way, ignoreInnerWays) {\n if (way.tags.golf === 'path' || way.tags.golf === 'cartpath') {\n // skip golf paths #11863\n return false;\n }\n\n if (isTaggedAsHighway(way) || way.tags.route === 'ferry') return true;\n\n return graph.parentRelations(way).some(function(parentRelation) {\n if (parentRelation.tags.type === 'route' &&\n parentRelation.tags.route === 'ferry') return true;\n\n if (parentRelation.isMultipolygon() &&\n isTaggedAsHighway(parentRelation) &&\n (!ignoreInnerWays || parentRelation.memberById(way.id).role !== 'inner')) return true;\n\n return false;\n });\n }\n\n function makeContinueDrawingFixIfAllowed(textDirection, vertexID, whichEnd) {\n var vertex = graph.hasEntity(vertexID);\n if (!vertex || vertex.tags.noexit === 'yes') return null;\n\n var useLeftContinue = (whichEnd === 'start' && textDirection === 'ltr') ||\n (whichEnd === 'end' && textDirection === 'rtl');\n\n return new validationIssueFix({\n icon: 'iD-operation-continue' + (useLeftContinue ? '-left' : ''),\n title: t.append('issues.fix.continue_from_' + whichEnd + '.title'),\n entityIds: [vertexID],\n onClick: function(context) {\n var wayId = this.issue.entityIds[0];\n var way = context.hasEntity(wayId);\n var vertexId = this.entityIds[0];\n var vertex = context.hasEntity(vertexId);\n\n if (!way || !vertex) return;\n\n // make sure the vertex is actually visible and editable\n var map = context.map();\n if (!context.editable() || !map.trimmedExtent().contains(vertex.loc)) {\n map.zoomToEase(vertex);\n }\n\n context.enter(\n modeDrawLine(context, wayId, context.graph(), 'line', way.affix(vertexId), true)\n );\n }\n });\n }\n\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { osmTimelessFeatureTagValues } from '../osm/tags';\n\nexport function validationMissingStartDate(context) {\n const type = 'missing_start_date';\n\n const validation = function checkMissingStartDate(entity, graph) {\n // If start_date is not empty, return nothing\n if (entity.tags && (entity.tags.start_date || entity.tags['start_date:edtf'])) return [];\n // If entity has no tags, return nothing\n if (Object.keys(entity.tags).length === 0) return [];\n // Rule should be ignored for natural entities and waterways\n if (entity.tags && (\n (entity.tags.natural && osmTimelessFeatureTagValues[entity.tags.natural]) ||\n (entity.tags.waterway && osmTimelessFeatureTagValues[entity.tags.waterway]) ||\n (entity.tags.water && osmTimelessFeatureTagValues[entity.tags.water]))) return [];\n\n // If entity is a vertex node\n var osm = context.connection();\n var isUnloadedNode = entity.type === 'node' && osm && !osm.isDataLoaded(entity.loc);\n\n // Should skip this validation if node is unloaded, is a vertex or has parent relations\n if (isUnloadedNode ||\n // allow untagged nodes that are part of ways\n entity.geometry(graph) === 'vertex' ||\n // allow untagged entities that are part of relations\n entity.hasParentRelations(graph)) return [];\n\n const entityID = entity.id;\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.missing_start_date.reference'));\n }\n\n return [new validationIssue({\n type: type,\n severity: 'warning',\n message: (context) => {\n const entity = context.hasEntity(entityID);\n return entity ? t.append('issues.missing_start_date.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [entityID],\n dynamicFixes: () => {\n return [\n new validationIssueFix({ title: t.append('issues.fix.add_start_date.title')})\n ];\n }\n })];\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { localizer, t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilNormalizeDateString, utilEDTFFromOSMDateString } from '../util';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { actionChangeTags } from '../actions/change_tags';\n\nimport * as edtf from 'edtf';\n\nexport function validationFormatting() {\n var type = 'invalid_format';\n\n var validation = function(entity) {\n var issues = [];\n\n function showReferenceDate(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.invalid_format.date.reference'));\n }\n\n function validateDate(key, msgKey) {\n if (!entity.tags[key]) return;\n var normalized = utilNormalizeDateString(entity.tags[key]);\n if (normalized !== null && entity.tags[key] === normalized.value) return;\n issues.push(new validationIssue({\n type: type,\n subtype: 'date',\n severity: 'error',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.date.message_' + msgKey,\n { feature: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: showReferenceDate,\n entityIds: [entity.id],\n hash: key + entity.tags[key],\n dynamicFixes: function() {\n var fixes = [];\n\n let alternatives = [];\n if (normalized !== null) {\n let label = normalized.date.toLocaleDateString(localizer.localeCodes(), normalized.localeOptions);\n alternatives.push({\n date: normalized.value,\n label: label || normalized.value,\n });\n }\n let edtfFromOSM = utilEDTFFromOSMDateString(entity.tags[key]);\n if (edtfFromOSM) {\n let label;\n try {\n label = edtf.default(edtfFromOSM).format(localizer.localeCode());\n } catch {\n label = edtfFromOSM;\n }\n alternatives.push({\n edtf: edtfFromOSM,\n label: label,\n });\n }\n\n fixes.push(...alternatives.map(alt => new validationIssueFix({\n title: t.append('issues.fix.reformat_date.title', { date: alt.label }),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n if (alt.date) {\n newTags[key] = alt.date;\n } else {\n delete newTags[key];\n }\n newTags[key + ':edtf'] = alt.edtf;\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.reformat_date.annotation'));\n }\n })));\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n delete newTags[key];\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.remove_tag.annotation'));\n }\n }));\n\n return fixes;\n }\n }));\n }\n validateDate('start_date', 'start');\n validateDate('end_date', 'end');\n\n function showReferenceEDTF(selection, parserError) {\n let message;\n if (typeof parserError.offset === 'number' && parserError.token) {\n message = t.append('issues.invalid_format.edtf.reference', {\n token: parserError.token.value,\n position: (parserError.offset + 1).toLocaleString(localizer.localeCodes()),\n });\n } else if (parserError.message) {\n message = selection => selection.append('span')\n .attr('class', 'localized-text')\n .attr('lang', 'en')\n .text(parserError.message.replace(/^edtf: /, ''));\n }\n if (!message) {\n return;\n }\n\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(message);\n }\n\n function validateEDTF(key, msgKey) {\n key += ':edtf';\n if (!entity.tags[key]) return;\n let parserError;\n try {\n edtf.parse(entity.tags[key]);\n return;\n } catch (e) {\n parserError = e;\n }\n issues.push(new validationIssue({\n type: type,\n subtype: 'date',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.edtf.message_' + msgKey,\n { feature: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: selection => showReferenceEDTF(selection, parserError),\n entityIds: [entity.id],\n hash: key + entity.tags[key],\n dynamicFixes: function() {\n var fixes = [];\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n delete newTags[key];\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.remove_tag.annotation'));\n }\n }));\n return fixes;\n }\n }));\n }\n validateEDTF('start_date', 'start');\n validateEDTF('end_date', 'end');\n\n function isValidEmail(email) {\n // Emails in OSM are going to be official so they should be pretty simple\n // Using negated lists to better support all possible unicode characters (#6494)\n var valid_email = /^[^\\(\\)\\\\,\":;<>@\\[\\]]+@[^\\(\\)\\\\,\":;<>@\\[\\]\\.]+(?:\\.[a-z0-9-]+)*$/i;\n\n // An empty value is also acceptable\n return (!email || valid_email.test(email));\n }\n\n function showReferenceEmail(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.invalid_format.email.reference'));\n }\n\n function isValidURL(url, strict = false) {\n try {\n // First try strict WHATWG parsing\n const link = new URL(url);\n return link.href.includes(url);\n } catch {\n if (strict) return false;\n // Fallback: accept if it looks like a valid scheme://something, even if semicolons are present\n return /^https?:\\/\\/\\S+$/i.test(url);\n }\n }\n\n function cleanWikimediaCommonsReference(value) {\n if (!value) return null;\n for (const prefix of ['file', 'datei', 'fichier', 'plik']) {\n if (!value.toLowerCase().startsWith(prefix + ':')) continue;\n return 'File' + decodeURIComponent(value.slice(prefix.length));\n }\n if (value.startsWith('Category:')) return decodeURIComponent(value);\n return null;\n }\n\n function showReferenceWebsite(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.invalid_format.website.reference'));\n }\n\n const websiteValidationIssueBase = {\n type: type,\n subtype: 'website',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.website.message' + (this.data?.count > 1 ? '_multi' : ''),\n { feature: utilDisplayLabel(entity, context.graph()), site: this.data?.value }) : '';\n },\n dynamicFixes: function(context) {\n const wikimedia_commons_reference = cleanWikimediaCommonsReference(this.data?.value);\n const fixes = [{ protocol: 'https', icon: 'temaki-lock' }, { protocol: 'http' }]\n .filter(fix => isValidURL(fix.protocol + '://' + this.data?.value, true))\n .map(fix => new validationIssueFix({\n icon: fix.icon,\n title: t.append('issues.fix.add_protocol_'+ fix.protocol +'.title'),\n onClick: function() {\n const entityID = this.issue.entityIds[0];\n const entity = context.entity(entityID);\n if (!entity) return;\n const key = this.issue.data.key;\n const tags = Object.assign({}, entity.tags);\n tags[key] = entity.tags[key]\n .split(';')\n .map(s => s.trim())\n .map(s => isValidURL(s) ? s : fix.protocol + '://' + s)\n .join(';');\n\n context.perform(\n actionChangeTags(entityID, tags),\n t('issues.fix.add_protocol_'+ fix.protocol +'.annotation')\n );\n }\n }));\n if (this.data?.key === 'image' && !entity.tags.wikimedia_commons && wikimedia_commons_reference) {\n fixes.push(new validationIssueFix({\n icon: 'iD-icon-out-link',\n title: t.append('issues.fix.move_value_to_wikimedia_commons.title'),\n onClick: function() {\n const entityID = this.issue.entityIds[0];\n const entity = context.entity(entityID);\n if (!entity) return;\n const key = this.issue.data.key;\n const tags = Object.assign({}, entity.tags);\n tags.wikimedia_commons = wikimedia_commons_reference;\n delete tags[key];\n\n context.perform(\n actionChangeTags(entityID, tags),\n t('issues.fix.move_value_to_wikimedia_commons.annotation')\n );\n }\n }));\n }\n return fixes;\n },\n reference: showReferenceWebsite,\n entityIds: [entity.id]\n };\n\n Object.entries(entity.tags).map(function([key, tag]) {\n if (!/\\b(website|url)\\b|^image$/i.test(key)) return null;\n if (!tag) return null;\n const value = tag.trim();\n if (!value) return null;\n if (!value.includes(';')) {\n // No semicolon, validate whole value\n if (isValidURL(value)) return null;\n return {\n ...websiteValidationIssueBase,\n data: { key, value },\n hash: key + '=' + value\n };\n }\n const invalidParts = value.split(';').map(s => s.trim()).filter(x => !isValidURL(x));\n if (!invalidParts.length) {\n if (isValidURL(value)) return null;\n // All split parts valid, but whole value still invalid\n return {\n ...websiteValidationIssueBase,\n data: { key, value },\n hash: key + '=' + value\n };\n }\n return {\n ...websiteValidationIssueBase,\n data: { key, value: invalidParts.join(', '), count: invalidParts.length },\n hash: key + '=' + invalidParts.join()\n };\n }).filter(issue => issue !== null).forEach(issueData => issues.push(new validationIssue(issueData)));\n\n if (entity.tags.email) {\n // Multiple emails are possible\n var emails = entity.tags.email\n .split(';')\n .map(function(s) { return s.trim(); })\n .filter(function(x) { return !isValidEmail(x); });\n\n if (emails.length) {\n issues.push(new validationIssue({\n type: type,\n subtype: 'email',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.invalid_format.email.message' + this.data,\n { feature: utilDisplayLabel(entity, context.graph()), email: emails.join(', ') }) : '';\n },\n reference: showReferenceEmail,\n entityIds: [entity.id],\n hash: emails.join(),\n data: (emails.length > 1) ? '_multi' : ''\n }));\n }\n }\n\n return issues;\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationHelpRequest(context) {\n var type = 'help_request';\n\n var validation = function checkFixmeTag(entity) {\n\n if (!entity.tags.fixme) return [];\n\n // don't flag fixmes on features added by the user\n if (entity.version === undefined) return [];\n\n if (entity.v !== undefined) {\n var baseEntity = context.history().base().hasEntity(entity.id);\n // don't flag fixmes added by the user on existing features\n if (!baseEntity || !baseEntity.tags.fixme) return [];\n }\n\n return [new validationIssue({\n type: type,\n subtype: 'fixme_tag',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.fixme_tag.message', {\n feature: utilDisplayLabel(entity, context.graph(), true /* verbose */)\n }) : '';\n },\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n title: t.append('issues.fix.address_the_concern.title')\n })\n ];\n },\n reference: showReference,\n entityIds: [entity.id]\n })];\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.fixme_tag.reference'));\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t, localizer } from '../core/localizer';\nimport { modeDrawLine } from '../modes/draw_line';\nimport { actionReverse } from '../actions/reverse';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { osmFlowingWaterwayTagValues, osmRoutableHighwayTagValues } from '../osm/tags';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\nexport function validationImpossibleOneway() {\n const type = 'impossible_oneway';\n\n const validation = function checkImpossibleOneway(entity, graph) {\n if (entity.type !== 'way' || entity.geometry(graph) !== 'line') return [];\n if (entity.isClosed()) return [];\n if (!typeForWay(entity)) return [];\n if (!entity.isOneWay()) return [];\n\n return [\n ...issuesForNode(entity, entity.first()),\n ...issuesForNode(entity, entity.last())\n ];\n\n function typeForWay(way) {\n if (way.geometry(graph) !== 'line') return null;\n\n if (osmRoutableHighwayTagValues[way.tags.highway]) return 'highway';\n if (osmFlowingWaterwayTagValues[way.tags.waterway]) return 'waterway';\n return null;\n }\n\n function nodeOccursMoreThanOnce(way, nodeID) {\n let occurrences = 0;\n for (const index in way.nodes) {\n if (way.nodes[index] === nodeID) {\n occurrences++;\n if (occurrences > 1) return true;\n }\n }\n return false;\n }\n\n function isConnectedViaOtherTypes(way, node) {\n\n var wayType = typeForWay(way);\n\n if (wayType === 'highway') {\n // entrances are considered connected\n if (node.tags.entrance && node.tags.entrance !== 'no') return true;\n if (node.tags.amenity === 'parking_entrance') return true;\n } else if (wayType === 'waterway') {\n if (node.id === way.first()) {\n // multiple waterways may start at the same spring\n if (node.tags.natural === 'spring') return true;\n } else {\n // multiple waterways may end at the same drain\n if (node.tags.manhole === 'drain') return true;\n }\n }\n\n return graph.parentWays(node).some(function(parentWay) {\n if (parentWay.id === way.id) return false;\n\n if (wayType === 'highway') {\n\n // allow connections to highway areas\n if (parentWay.geometry(graph) === 'area' &&\n osmRoutableHighwayTagValues[parentWay.tags.highway]) return true;\n\n // count connections to ferry routes as connected\n if (parentWay.tags.route === 'ferry') return true;\n\n return graph.parentRelations(parentWay).some(function(parentRelation) {\n if (parentRelation.tags.type === 'route' &&\n parentRelation.tags.route === 'ferry') return true;\n\n // allow connections to highway multipolygons\n return parentRelation.isMultipolygon() && osmRoutableHighwayTagValues[parentRelation.tags.highway];\n });\n } else if (wayType === 'waterway') {\n // multiple waterways may start or end at a water body at the same node\n if (parentWay.tags.natural === 'water' ||\n parentWay.tags.natural === 'coastline') return true;\n }\n return false;\n });\n }\n\n function issuesForNode(way, nodeID) {\n const isFirst = (nodeID === way.first()) ^ way.isOneWayBackwards();\n const wayType = typeForWay(way);\n\n // ignore if this way is self-connected at this node\n if (nodeOccursMoreThanOnce(way, nodeID)) return [];\n\n const osm = services.osm;\n if (!osm) return [];\n const node = graph.hasEntity(nodeID);\n // ignore if this node or its tile are unloaded\n if (!node || !osm.isDataLoaded(node.loc)) return [];\n\n if (isConnectedViaOtherTypes(way, node)) return [];\n\n const attachedWaysOfSameType = graph.parentWays(node).filter(parentWay => {\n if (parentWay.id === way.id) return false;\n return typeForWay(parentWay) === wayType;\n });\n\n // assume it's okay for waterways to start or end disconnected for now\n if (wayType === 'waterway' && attachedWaysOfSameType.length === 0) return [];\n\n const attachedOneways = attachedWaysOfSameType\n .filter(attachedWay => attachedWay.isOneWay());\n\n // ignore if the way is connected to some non-oneway features\n if (attachedOneways.length < attachedWaysOfSameType.length) return [];\n\n if (attachedOneways.length) {\n const connectedEndpointsOkay = attachedOneways.some(attachedOneway => {\n const isAttachedBackwards = attachedOneway.isOneWayBackwards();\n if ((isFirst ^ isAttachedBackwards\n ? attachedOneway.first()\n : attachedOneway.last()\n ) !== nodeID) {\n return true;\n }\n if (nodeOccursMoreThanOnce(attachedOneway, nodeID)) return true;\n return false;\n });\n if (connectedEndpointsOkay) return [];\n }\n\n const placement = isFirst ? 'start' : 'end';\n let messageID = wayType + '.';\n let referenceID = wayType + '.';\n\n if (wayType === 'waterway') {\n messageID += 'connected.' + placement;\n referenceID += 'connected';\n } else {\n messageID += placement;\n referenceID += placement;\n }\n\n return [new validationIssue({\n type: type,\n subtype: wayType,\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.impossible_oneway.' + messageID + '.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: getReference(referenceID),\n entityIds: [way.id, node.id],\n dynamicFixes: function() {\n\n var fixes = [];\n\n if (attachedOneways.length) {\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-reverse',\n title: t.append('issues.fix.reverse_feature.title'),\n entityIds: [way.id],\n onClick: function(context) {\n var id = this.issue.entityIds[0];\n context.perform(actionReverse(id), t('operations.reverse.annotation.line', { n: 1 }));\n }\n }));\n }\n if (node.tags.noexit !== 'yes') {\n var textDirection = localizer.textDirection();\n var useLeftContinue = (isFirst && textDirection === 'ltr') ||\n (!isFirst && textDirection === 'rtl');\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-continue' + (useLeftContinue ? '-left' : ''),\n title: t.append('issues.fix.continue_from_' + (isFirst ? 'start' : 'end') + '.title'),\n onClick: function(context) {\n var entityID = this.issue.entityIds[0];\n var vertexID = this.issue.entityIds[1];\n var way = context.entity(entityID);\n var vertex = context.entity(vertexID);\n continueDrawing(way, vertex, context);\n }\n }));\n }\n\n return fixes;\n },\n loc: node.loc\n })];\n\n function getReference(referenceID) {\n return function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.impossible_oneway.' + referenceID + '.reference'));\n };\n }\n }\n };\n\n function continueDrawing(way, vertex, context) {\n // make sure the vertex is actually visible and editable\n var map = context.map();\n if (!context.editable() || !map.trimmedExtent().contains(vertex.loc)) {\n map.zoomToEase(vertex);\n }\n\n context.enter(\n modeDrawLine(context, way.id, context.graph(), 'line', way.affix(vertex.id), true)\n );\n }\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\nconst incompatibleRules = [\n {\n id: 'amap',\n regex: /(^amap$|^amap\\.com|autonavi|mapabc|\u9AD8\u5FB7)/i\n },\n {\n id: 'baidu',\n regex: /(baidu|mapbar|\u767E\u5EA6)/i\n },\n {\n id: 'google',\n regex: /(google)/i,\n exceptRegex: /((books|drive)\\.google|google\\s?(books|drive|plus))|(esri\\/Google_(Africa|Open)_Buildings)|(:\\/\\/\\S+\\/\\S+(google)\\S+)/i\n }\n];\n\n/**\n * @param {string} str String (e.g. tag value) to check for incompatible sources\n * @returns {{id:string, regex: RegExp, exceptRegex?: RegExp}[]}\n */\nexport function getIncompatibleSources(str) {\n return incompatibleRules\n .filter(rule =>\n rule.regex.test(str) &&\n !rule.exceptRegex?.test(str)\n );\n}\n\nexport function validationIncompatibleSource() {\n const type = 'incompatible_source';\n\n const validation = function checkIncompatibleSource(entity) {\n const entitySources = entity.tags && entity.tags.source && entity.tags.source.split(';');\n if (!entitySources) return [];\n\n const entityID = entity.id;\n\n return entitySources\n .flatMap(source => getIncompatibleSources(source)\n .map(matchRule => new validationIssue({\n type: type,\n severity: 'warning',\n message: (context) => {\n const entity = context.hasEntity(entityID);\n return entity ? t.append('issues.incompatible_source.feature.message', {\n feature: utilDisplayLabel(entity, context.graph(), true /* verbose */),\n value: source\n }) : '';\n },\n reference: getReference(matchRule.id),\n entityIds: [entityID],\n hash: source,\n dynamicFixes: () => {\n return [\n new validationIssueFix({ title: t.append('issues.fix.remove_proprietary_data.title') })\n ];\n }\n }))\n );\n\n function getReference(id) {\n return function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append(`issues.incompatible_source.reference.${id}`));\n };\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { services } from '../services';\n\n\nexport function validationMaprules() {\n var type = 'maprules';\n\n var validation = function checkMaprules(entity, graph) {\n if (!services.maprules) return [];\n\n var rules = services.maprules.validationRules();\n var issues = [];\n\n for (var i = 0; i < rules.length; i++) {\n var rule = rules[i];\n rule.findIssues(entity, graph, issues);\n }\n\n return issues;\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { localizer, t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { utilNormalizeDateString } from '../util/ohm_date';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\nimport * as edtf from 'edtf';\n\nexport function validationMismatchedDates() {\n let type = 'mismatched_dates';\n\n function parseEDTF(value) {\n try {\n let parsed = edtf.default(value);\n\n // According to edtf.js, an extended interval with an unknown start or end covers no date.\n // This isn't useful for the purpose of testing whether the basic date matches, so treat it as an unspecified start or end.\n if (parsed.lower === null) {\n parsed.lower = Infinity;\n }\n if (parsed.upper === null) {\n parsed.upper = Infinity;\n }\n\n return parsed;\n } catch {\n // Already handled by invalid_format rule.\n return;\n }\n }\n\n function getReplacementDates(parsed) {\n let likelyDates = new Set();\n\n let valueFromDate = (date, precision) => {\n date.precision = precision;\n return date.edtf.split('T')[0];\n };\n\n if (Number.isFinite(parsed.min)) {\n let min = edtf.default(parsed.min);\n let precision = (parsed.lower || parsed.first || parsed).precision;\n likelyDates.add(valueFromDate(min, precision));\n }\n\n if (Number.isFinite(parsed.max)) {\n let max = edtf.default(parsed.max);\n let precision = (parsed.upper || parsed.last || parsed).precision;\n likelyDates.add(valueFromDate(max, precision));\n }\n\n let sortedDates = [...likelyDates];\n sortedDates.sort();\n return sortedDates;\n }\n\n let validation = function(entity) {\n let issues = [];\n\n function showReferenceEDTF(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.mismatched_dates.edtf.reference'));\n }\n\n function getDynamicFixes(key, parsed) {\n let fixes = [];\n\n let replacementDates = getReplacementDates(parsed);\n fixes.push(...replacementDates.map(value => {\n let normalized = utilNormalizeDateString(value);\n let localeDateString = normalized.date.toLocaleDateString(localizer.languageCode(), normalized.localeOptions);\n return new validationIssueFix({\n title: t.append('issues.fix.reformat_date.title', { date: localeDateString }),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n newTags[key] = normalized.value;\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.reformat_date.annotation'));\n }\n });\n }));\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n context.perform(function(graph) {\n var entityInGraph = graph.hasEntity(entity.id);\n if (!entityInGraph) return graph;\n var newTags = Object.assign({}, entityInGraph.tags);\n delete newTags[key];\n return actionChangeTags(entityInGraph.id, newTags)(graph);\n }, t('issues.fix.remove_tag.annotation'));\n }\n }));\n\n return fixes;\n }\n\n function validateEDTF(key, msgKey) {\n if (!entity.tags[key] || !entity.tags[key + ':edtf']) return;\n let basic = entity.tags[key];\n let basicAsEDTF = parseEDTF(basic);\n let parsed = parseEDTF(entity.tags[key + ':edtf']);\n if (!basicAsEDTF || !parsed || parsed.covers(basicAsEDTF) || basicAsEDTF.covers(parsed)) return;\n\n // start_date and end_date disallow time precision. Transform the basic date into a daylong range in EDTF to allow a comparison to an EDTF date with time precision.\n // https://github.com/OpenHistoricalMap/issues/issues/764\n if (basic.match('^-?[0-9]+-[0-9]{2}-[0-9]{2}$')) {\n let basicTime = parseEDTF(`${basic}T00:00:00/${basic}T24:00:00`);\n if (basicTime && (parsed.covers(basicTime) || basicTime.covers(parsed))) return;\n }\n\n issues.push(new validationIssue({\n type: type,\n subtype: 'date',\n severity: 'warning',\n message: function(context) {\n let entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.mismatched_dates.edtf.message_' + msgKey,\n { feature: utilDisplayLabel(entity, context.graph()) }) : '';\n },\n reference: showReferenceEDTF,\n entityIds: [entity.id],\n hash: key + entity.tags[key + ':edtf'],\n dynamicFixes: () => getDynamicFixes(key, parsed),\n }));\n }\n validateEDTF('start_date', 'start');\n validateEDTF('end_date', 'end');\n\n return issues;\n };\n\n validation.type = type;\n validation.parseEDTF = parseEDTF;\n validation.getReplacementDates = getReplacementDates;\n\n return validation;\n}\n", "import { deepEqual } from 'fast-equals';\n\nimport { actionAddVertex } from '../actions/add_vertex';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionMergeNodes } from '../actions/merge_nodes';\nimport { actionExtract } from '../actions/extract';\nimport { modeSelect } from '../modes/select';\nimport { osmJoinWays } from '../osm/multipolygon';\nimport { osmNodeGeometriesForTags, osmTagSuggestingArea } from '../osm/tags';\nimport { presetManager } from '../presets';\nimport { geoHasSelfIntersections, geoSphericalDistance } from '../geo';\nimport { t } from '../core/localizer';\nimport { utilTagText } from '../util';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationMismatchedGeometry() {\n var type = 'mismatched_geometry';\n\n function tagSuggestingLineIsArea(entity) {\n if (entity.type !== 'way' || entity.isClosed()) return null;\n\n var tagSuggestingArea = entity.tagSuggestingArea();\n\n if (!tagSuggestingArea) {\n return null;\n }\n\n var asLine = presetManager.matchTags(tagSuggestingArea, 'line');\n var asArea = presetManager.matchTags(tagSuggestingArea, 'area');\n if (asLine && asArea && deepEqual(asLine.tags, asArea.tags)) {\n // this tag also allows lines and making this an area wouldn't matter\n return null;\n }\n\n if (asLine.isFallback() && asArea.isFallback() && !deepEqual(tagSuggestingArea, { area: 'yes' })) {\n // if the entity matches the fallback preset, regardless of the\n // geometry, then changing the geometry will not help.\n return null;\n }\n\n return tagSuggestingArea;\n }\n\n\n function makeConnectEndpointsFixOnClick(way, graph) {\n // must have at least three nodes to close this automatically\n if (way.nodes.length < 3) return null;\n\n var nodes = graph.childNodes(way), testNodes;\n var firstToLastDistanceMeters = geoSphericalDistance(nodes[0].loc, nodes[nodes.length-1].loc);\n\n // if the distance is very small, attempt to merge the endpoints\n if (firstToLastDistanceMeters < 0.75) {\n testNodes = nodes.slice(); // shallow copy\n testNodes.pop();\n testNodes.push(testNodes[0]);\n // make sure this will not create a self-intersection\n if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {\n return function(context) {\n var way = context.entity(this.issue.entityIds[0]);\n context.perform(\n actionMergeNodes([way.nodes[0], way.nodes[way.nodes.length-1]], nodes[0].loc),\n t('issues.fix.connect_endpoints.annotation')\n );\n };\n }\n }\n\n // if the points were not merged, attempt to close the way\n testNodes = nodes.slice(); // shallow copy\n testNodes.push(testNodes[0]);\n // make sure this will not create a self-intersection\n if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {\n return function(context) {\n var wayId = this.issue.entityIds[0];\n var way = context.entity(wayId);\n var nodeId = way.nodes[0];\n var index = way.nodes.length;\n context.perform(\n actionAddVertex(wayId, nodeId, index),\n t('issues.fix.connect_endpoints.annotation')\n );\n };\n }\n }\n\n function lineTaggedAsAreaIssue(entity) {\n\n var tagSuggestingArea = tagSuggestingLineIsArea(entity);\n if (!tagSuggestingArea) return null;\n\n var validAsLine = false;\n var presetAsLine = presetManager.matchTags(entity.tags, 'line');\n if (presetAsLine) {\n validAsLine = true;\n var key = Object.keys(tagSuggestingArea)[0];\n if (presetAsLine.tags[key] && presetAsLine.tags[key] === '*') {\n // only matches a fallback preset of the tag which is suggesting to be an area\n validAsLine = false;\n }\n if (Object.keys(presetAsLine.tags).length === 0) {\n // only matches the fallback \"line\" preset\n validAsLine = false;\n }\n }\n\n return new validationIssue({\n type: type,\n subtype: 'area_as_line',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.tag_suggests_area.message', {\n feature: utilDisplayLabel(entity, 'area', true /* verbose */),\n tag: utilTagText({ tags: tagSuggestingArea })\n }) : '';\n },\n reference: showReference,\n entityIds: [entity.id],\n hash: JSON.stringify(tagSuggestingArea),\n dynamicFixes: function(context) {\n\n var fixes = [];\n\n var entity = context.entity(this.entityIds[0]);\n var connectEndsOnClick = makeConnectEndpointsFixOnClick(entity, context.graph());\n\n if (!validAsLine) {\n // only suggest to \"connect the ends\" if the feature is not also valid as a line\n fixes.push(new validationIssueFix({\n title: t.append('issues.fix.connect_endpoints.title'),\n onClick: connectEndsOnClick\n }));\n }\n\n fixes.push(new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_tag.title'),\n onClick: function(context) {\n var entityId = this.issue.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n for (var key in tagSuggestingArea) {\n delete tags[key];\n }\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.remove_tag.annotation')\n );\n }\n }));\n\n return fixes;\n }\n });\n\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.tag_suggests_area.reference'));\n }\n }\n\n function vertexPointIssue(entity, graph) {\n // we only care about nodes\n if (entity.type !== 'node') return null;\n\n // ignore tagless points\n if (Object.keys(entity.tags).length === 0) return null;\n\n // address lines are special so just ignore them\n if (entity.isOnAddressLine(graph)) return null;\n\n var geometry = entity.geometry(graph);\n var allowedGeometries = osmNodeGeometriesForTags(entity.tags);\n\n if (geometry === 'point' && !allowedGeometries.point && allowedGeometries.vertex) {\n\n return new validationIssue({\n type: type,\n subtype: 'vertex_as_point',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.vertex_as_point.message', {\n feature: utilDisplayLabel(entity, 'vertex', true /* verbose */)\n }) : '';\n },\n reference: function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.vertex_as_point.reference'));\n },\n entityIds: [entity.id],\n dynamicFixes: () => [new validationIssueFix({\n icon: 'iD-operation-move',\n title: t.append('issues.fix.reposition_point_to_vertex.title')\n })]\n });\n\n } else if (geometry === 'vertex' && !allowedGeometries.vertex && allowedGeometries.point) {\n\n return new validationIssue({\n type: type,\n subtype: 'point_as_vertex',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.point_as_vertex.message', {\n feature: utilDisplayLabel(entity, 'point', true /* verbose */)\n }) : '';\n },\n reference: function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.point_as_vertex.reference'));\n },\n entityIds: [entity.id],\n dynamicFixes: extractPointDynamicFixes\n });\n }\n\n return null;\n }\n\n\n function otherMismatchIssue(entity, graph) {\n // ignore boring features\n if (!entity.hasInterestingTags()) return null;\n\n if (entity.type !== 'node' && entity.type !== 'way') return null;\n\n // address lines are special so just ignore them\n if (entity.type === 'node' && entity.isOnAddressLine(graph)) return null;\n\n var sourceGeom = entity.geometry(graph);\n\n var targetGeoms = entity.type === 'way' ? ['point', 'vertex'] : ['line', 'area'];\n\n if (sourceGeom === 'area') targetGeoms.unshift('line');\n\n var asSource = presetManager.match(entity, graph);\n\n const originalTargetGeom = targetGeoms.find(nodeGeom => {\n const asTarget = presetManager.matchTags(\n entity.tags,\n nodeGeom,\n entity.extent(graph).center(),\n );\n if (!asSource || !asTarget ||\n asSource === asTarget ||\n // sometimes there are two presets with the same tags for different geometries\n deepEqual(asSource.tags, asTarget.tags)) return false;\n\n if (asTarget.isFallback()) return false;\n\n var primaryKey = Object.keys(asTarget.tags)[0];\n\n // special case: buildings-as-points are discouraged by iD, but common in OSM, so ignore them\n if (primaryKey === 'building') return false;\n\n if (asTarget.tags[primaryKey] === '*') return false;\n\n return asSource.isFallback() || asSource.tags[primaryKey] === '*';\n });\n\n let targetGeom = originalTargetGeom;\n\n if (!targetGeom) return null;\n\n var subtype = targetGeom + '_as_' + sourceGeom;\n\n if (targetGeom === 'vertex') targetGeom = 'point';\n if (sourceGeom === 'vertex') sourceGeom = 'point';\n\n var referenceId = targetGeom + '_as_' + sourceGeom;\n\n var dynamicFixes;\n if (targetGeom === 'point') {\n dynamicFixes = extractPointDynamicFixes;\n\n } else if (sourceGeom === 'area' && targetGeom === 'line') {\n dynamicFixes = lineToAreaDynamicFixes;\n }\n\n return new validationIssue({\n type: type,\n subtype: subtype,\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.' + referenceId + '.message', {\n feature: utilDisplayLabel(entity, originalTargetGeom, true /* verbose */)\n }) : '';\n },\n reference: function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.mismatched_geometry.reference'));\n },\n entityIds: [entity.id],\n dynamicFixes: dynamicFixes\n });\n }\n\n function lineToAreaDynamicFixes(context) {\n\n var convertOnClick;\n\n var entityId = this.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n delete tags.area;\n if (!osmTagSuggestingArea(tags)) {\n // if removing the area tag would make this a line, offer that as a quick fix\n convertOnClick = function(context) {\n var entityId = this.issue.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n if (tags.area) {\n delete tags.area;\n }\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.convert_to_line.annotation')\n );\n };\n }\n\n return [\n new validationIssueFix({\n icon: 'iD-icon-line',\n title: t.append('issues.fix.convert_to_line.title'),\n onClick: convertOnClick\n })\n ];\n }\n\n function extractPointDynamicFixes(context) {\n\n var entityId = this.entityIds[0];\n\n var extractOnClick = null;\n if (!context.hasHiddenConnections(entityId)) {\n\n extractOnClick = function(context) {\n var entityId = this.issue.entityIds[0];\n var action = actionExtract(entityId, context.projection);\n context.perform(\n action,\n t('operations.extract.annotation', { n: 1 })\n );\n // re-enter mode to trigger updates\n context.enter(modeSelect(context, [action.getExtractedNodeID()]));\n };\n }\n\n return [\n new validationIssueFix({\n icon: 'iD-operation-extract',\n title: t.append('issues.fix.extract_point.title'),\n onClick: extractOnClick\n })\n ];\n }\n\n function unclosedMultipolygonPartIssues(entity, graph) {\n\n if (entity.type !== 'relation' ||\n !entity.isMultipolygon() ||\n entity.isDegenerate() ||\n // cannot determine issues for incompletely-downloaded relations\n !entity.isComplete(graph)) return [];\n\n var sequences = osmJoinWays(entity.members, graph);\n\n var issues = [];\n\n for (var i in sequences) {\n var sequence = sequences[i];\n\n if (!sequence.nodes) continue;\n\n var firstNode = sequence.nodes[0];\n var lastNode = sequence.nodes[sequence.nodes.length - 1];\n\n // part is closed if the first and last nodes are the same\n if (firstNode === lastNode) continue;\n\n var issue = new validationIssue({\n type: type,\n subtype: 'unclosed_multipolygon_part',\n severity: 'warning',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.unclosed_multipolygon_part.message', {\n feature: utilDisplayLabel(entity, context.graph(), true /* verbose */)\n }) : '';\n },\n reference: showReference,\n loc: sequence.nodes[0].loc,\n entityIds: [entity.id],\n hash: sequence.map(function(way) {\n return way.id;\n }).join()\n });\n issues.push(issue);\n }\n\n return issues;\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.unclosed_multipolygon_part.reference'));\n }\n }\n\n var validation = function checkMismatchedGeometry(entity, graph) {\n var vertexPoint = vertexPointIssue(entity, graph);\n if (vertexPoint) return [vertexPoint];\n\n var lineAsArea = lineTaggedAsAreaIssue(entity);\n if (lineAsArea) return [lineAsArea];\n\n var mismatch = otherMismatchIssue(entity, graph);\n if (mismatch) return [mismatch];\n\n return unclosedMultipolygonPartIssues(entity, graph);\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeMember } from '../actions/change_member';\nimport { actionDeleteMember } from '../actions/delete_member';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationMissingRole() {\n var type = 'missing_role';\n\n var validation = function checkMissingRole(entity, graph) {\n var issues = [];\n if (entity.type === 'way') {\n graph.parentRelations(entity).forEach(function(relation) {\n if (!relation.isMultipolygon()) return;\n\n var member = relation.memberById(entity.id);\n if (member && isMissingRole(member)) {\n issues.push(makeIssue(entity, relation, member));\n }\n });\n } else if (entity.type === 'relation' && entity.isMultipolygon()) {\n entity.indexedMembers().forEach(function(member) {\n var way = graph.hasEntity(member.id);\n if (way && isMissingRole(member)) {\n issues.push(makeIssue(way, entity, member));\n }\n });\n }\n\n return issues;\n };\n\n\n function isMissingRole(member) {\n return !member.role || !member.role.trim().length;\n }\n\n\n function makeIssue(way, relation, member) {\n return new validationIssue({\n type: type,\n severity: 'warning',\n message: function(context) {\n const member = context.hasEntity(this.entityIds[0]),\n relation = context.hasEntity(this.entityIds[1]);\n return (member && relation) ? t.append('issues.missing_role.message', {\n member: utilDisplayLabel(member, context.graph()),\n relation: utilDisplayLabel(relation, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [way.id, relation.id],\n extent: function(graph) {\n return graph.entity(this.entityIds[0]).extent(graph);\n },\n data: {\n member: member\n },\n hash: member.index.toString(),\n dynamicFixes: function() {\n return [\n makeAddRoleFix('inner'),\n makeAddRoleFix('outer'),\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_from_relation.title'),\n onClick: function(context) {\n context.perform(\n actionDeleteMember(this.issue.entityIds[1], this.issue.data.member.index),\n t('operations.delete_member.annotation', {\n n: 1\n })\n );\n }\n })\n ];\n }\n });\n\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.missing_role.multipolygon.reference'));\n }\n }\n\n\n function makeAddRoleFix(role) {\n return new validationIssueFix({\n title: t.append('issues.fix.set_as_' + role + '.title'),\n onClick: function(context) {\n var oldMember = this.issue.data.member;\n var member = { id: this.issue.entityIds[0], type: oldMember.type, role: role };\n context.perform(\n actionChangeMember(this.issue.entityIds[1], member, oldMember.index),\n t('operations.change_role.annotation', {\n n: 1\n })\n );\n }\n });\n }\n\n validation.type = type;\n\n return validation;\n}\n", "import { operationDelete } from '../operations/delete';\nimport { osmIsInterestingTag } from '../osm/tags';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationMissingTag(context) {\n var type = 'missing_tag';\n\n function hasDescriptiveTags(entity) {\n var onlyAttributeKeys = ['description', 'name', 'start_date', 'oneway'];\n var entityDescriptiveKeys = Object.keys(entity.tags)\n .filter(function(k) {\n if (k === 'area' || !osmIsInterestingTag(k)) return false;\n\n return !onlyAttributeKeys.some(function(attributeKey) {\n return k === attributeKey || k.indexOf(attributeKey + ':') === 0;\n });\n });\n\n if (entity.type === 'relation' &&\n entityDescriptiveKeys.length === 1 &&\n entity.tags.type === 'multipolygon') {\n // this relation's only interesting tag just says its a multipolygon,\n // which is not descriptive enough\n return false;\n }\n\n return entityDescriptiveKeys.length > 0;\n }\n\n function isUnknownRoad(entity) {\n return entity.type === 'way' && entity.tags.highway === 'road';\n }\n\n function isUntypedRelation(entity) {\n return entity.type === 'relation' && !entity.tags.type;\n }\n\n var validation = function checkMissingTag(entity, graph) {\n\n var subtype;\n\n var osm = context.connection();\n var isUnloadedNode = entity.type === 'node' && osm && !osm.isDataLoaded(entity.loc);\n\n // we can't know if the node is a vertex if the tile is undownloaded\n if (!isUnloadedNode &&\n // allow untagged nodes that are part of ways\n entity.geometry(graph) !== 'vertex' &&\n // allow untagged entities that are part of relations\n !entity.hasParentRelations(graph)) {\n\n if (Object.keys(entity.tags).length === 0) {\n subtype = 'any';\n } else if (!hasDescriptiveTags(entity)) {\n subtype = 'descriptive';\n } else if (isUntypedRelation(entity)) {\n subtype = 'relation_type';\n }\n }\n\n // flag an unknown road even if it's a member of a relation\n if (!subtype && isUnknownRoad(entity)) {\n subtype = 'highway_classification';\n }\n\n if (!subtype) return [];\n\n var messageID = subtype === 'highway_classification' ? 'unknown_road' : 'missing_tag.' + subtype;\n var referenceID = subtype === 'highway_classification' ? 'unknown_road' : 'missing_tag';\n\n // can always delete if the user created it in the first place..\n var canDelete = (entity.version === undefined || entity.v !== undefined);\n var severity = (canDelete && subtype !== 'highway_classification') ? 'error' : 'warning';\n\n return [new validationIssue({\n type: type,\n subtype: subtype,\n severity: severity,\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.' + messageID + '.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [entity.id],\n dynamicFixes: function(context) {\n\n var fixes = [];\n\n var selectFixType = subtype === 'highway_classification' ? 'select_road_type' : 'select_preset';\n\n fixes.push(new validationIssueFix({\n icon: 'iD-icon-search',\n title: t.append('issues.fix.' + selectFixType + '.title'),\n onClick: function(context) {\n context.ui().sidebar.showPresetList();\n }\n }));\n\n var deleteOnClick;\n\n var id = this.entityIds[0];\n var operation = operationDelete(context, [id]);\n var disabledReasonID = operation.disabled();\n if (!disabledReasonID) {\n deleteOnClick = function(context) {\n var id = this.issue.entityIds[0];\n var operation = operationDelete(context, [id]);\n if (!operation.disabled()) {\n operation();\n }\n };\n }\n\n fixes.push(\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.delete_feature.title'),\n disabledReason: disabledReasonID ? t('operations.delete.' + disabledReasonID + '.single') : undefined,\n onClick: deleteOnClick\n })\n );\n\n return fixes;\n }\n })];\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.' + referenceID + '.reference'));\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { t } from '../core/localizer';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { osmMutuallyExclusiveTagPairs } from '../osm/tags';\n\nexport function validationMutuallyExclusiveTags(/* context */) {\n const type = 'mutually_exclusive_tags';\n\n // https://wiki.openstreetmap.org/wiki/Special:WhatLinksHere/Property:P44\n const tagKeyPairs = osmMutuallyExclusiveTagPairs;\n\n const validation = function checkMutuallyExclusiveTags(entity /*, graph */) {\n\n let pairsFounds = tagKeyPairs.filter((pair) => {\n return (pair[0] in entity.tags && pair[1] in entity.tags);\n }).filter((pair) => {\n // noname=no is double-negation, thus positive and not conflicting. We'll ignore those\n return !((pair[0].match(/^(addr:)?no[a-z]/) && entity.tags[pair[0]] === 'no') ||\n (pair[1].match(/^(addr:)?no[a-z]/) && entity.tags[pair[1]] === 'no'));\n });\n\n // Additional:\n // Check if name and not:name (and similar) are set and both have the same value\n // not:name can actually have multiple values, separate by ;\n // https://taginfo.openstreetmap.org/search?q=not%3A#keys\n Object.keys(entity.tags).forEach((key) => {\n let negative_key = 'not:' + key;\n if (negative_key in entity.tags && entity.tags[negative_key].split(';').includes(entity.tags[key])) {\n pairsFounds.push([negative_key, key, 'same_value']);\n }\n // For name:xx we also compare against the not:name tag\n if (key.match(/^name:[a-z]+/)) {\n negative_key = 'not:name';\n if (negative_key in entity.tags && entity.tags[negative_key].split(';').includes(entity.tags[key])) {\n pairsFounds.push([negative_key, key, 'same_value']);\n }\n }\n });\n\n let issues = pairsFounds.map((pair) => {\n const subtype = pair[2] || 'default';\n return new validationIssue({\n type: type,\n subtype: subtype,\n severity: 'warning',\n message: function(context) {\n let entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append(`issues.${type}.${subtype}.message`, {\n feature: utilDisplayLabel(entity, context.graph()),\n tag1: pair[0],\n tag2: pair[1]\n }) : '';\n },\n reference: (selection) => showReference(selection, pair, subtype),\n entityIds: [entity.id],\n dynamicFixes: () => pair.slice(0,2).map((tagToRemove) => createIssueFix(tagToRemove))\n });\n });\n\n function createIssueFix(tagToRemove) {\n return new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_named_tag.title', { tag: tagToRemove }),\n onClick: function(context) {\n const entityId = this.issue.entityIds[0];\n const entity = context.entity(entityId);\n let tags = Object.assign({}, entity.tags); // shallow copy\n delete tags[tagToRemove];\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.remove_named_tag.annotation', { tag: tagToRemove })\n );\n }\n });\n }\n\n function showReference(selection, pair, subtype) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append(`issues.${type}.${subtype}.reference`, { tag1: pair[0], tag2: pair[1] }));\n }\n\n return issues;\n };\n\n validation.type = type;\n\n return validation;\n}\n", "import { t } from '../core/localizer';\nimport { actionSplit } from '../actions/split';\nimport { behaviorOperation } from '../behavior/operation';\nimport { modeSelect } from '../modes/select';\n\n\nexport function operationSplit(context, selectedIDs) {\n var _vertexIds = selectedIDs.filter(function(id) {\n return context.graph().geometry(id) === 'vertex';\n });\n var _selectedWayIds = selectedIDs.filter(function(id) {\n var entity = context.graph().hasEntity(id);\n return entity && entity.type === 'way';\n });\n var _isAvailable = _vertexIds.length > 0 &&\n _vertexIds.length + _selectedWayIds.length === selectedIDs.length;\n var _action = actionSplit(_vertexIds);\n var _ways = [];\n var _geometry = 'feature';\n var _waysAmount = 'single';\n var _nodesAmount = _vertexIds.length === 1 ? 'single' : 'multiple';\n\n if (_isAvailable) {\n if (_selectedWayIds.length) _action.limitWays(_selectedWayIds);\n _ways = _action.ways(context.graph());\n var geometries = {};\n _ways.forEach(function(way) {\n geometries[way.geometry(context.graph())] = true;\n });\n if (Object.keys(geometries).length === 1) {\n _geometry = Object.keys(geometries)[0];\n }\n _waysAmount = _ways.length === 1 ? 'single' : 'multiple';\n }\n\n\n var operation = function() {\n var difference = context.perform(_action, operation.annotation());\n // select both the nodes and the ways so the mapper can immediately disconnect them if desired\n var idsToSelect = _vertexIds.concat(difference.extantIDs().filter(function(id) {\n // filter out relations that may have had member additions\n return context.entity(id).type === 'way';\n }));\n context.enter(modeSelect(context, idsToSelect));\n };\n\n\n operation.relatedEntityIds = function() {\n return _selectedWayIds.length ? [] : _ways.map(way => way.id);\n };\n\n\n operation.available = function() {\n return _isAvailable;\n };\n\n\n operation.disabled = function() {\n var reason = _action.disabled(context.graph());\n if (reason) {\n return reason;\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n }\n return false;\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.split.' + disable) :\n t.append('operations.split.description.' + _geometry + '.' + _waysAmount + '.' + _nodesAmount + '_node');\n };\n\n\n operation.annotation = function() {\n return t('operations.split.annotation.' + _geometry, { n: _ways.length });\n };\n\n\n operation.icon = function() {\n if (_waysAmount === 'multiple') {\n return '#iD-operation-split-multiple';\n } else {\n return '#iD-operation-split';\n }\n };\n\n\n operation.id = 'split';\n operation.keys = [t('operations.split.key')];\n operation.title = t.append('operations.split.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import { t } from '../core/localizer';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { operationSplit } from '../operations/split';\n\nexport function validationOsmApiLimits(context) {\n const type = 'osm_api_limits';\n\n const validation = function checkOsmApiLimits(entity) {\n const issues = [];\n const osm = context.connection();\n if (!osm) return issues; // cannot check if there is no connection to the osm api, e.g. during unit tests\n const maxWayNodes = osm.maxWayNodes();\n\n if (entity.type === 'way') {\n if (entity.nodes.length > maxWayNodes) {\n issues.push(new validationIssue({\n type: type,\n subtype: 'exceededMaxWayNodes',\n severity: 'error',\n message: function() {\n return t.append('issues.osm_api_limits.max_way_nodes.message');\n },\n reference: function(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.osm_api_limits.max_way_nodes.reference', { maxWayNodes }));\n },\n entityIds: [entity.id],\n dynamicFixes: splitWayIntoSmallChunks\n }));\n }\n }\n\n return issues;\n };\n\n function splitWayIntoSmallChunks() {\n const fix = new validationIssueFix({\n icon: 'iD-operation-split',\n title: t.append('issues.fix.split_way.title'),\n entityIds: this.entityIds,\n onClick: function(context) {\n const maxWayNodes = context.connection().maxWayNodes();\n const g = context.graph();\n\n const entityId = this.entityIds[0];\n const entity = context.graph().entities[entityId];\n const numberOfParts = Math.ceil(entity.nodes.length / maxWayNodes);\n let splitVertices;\n\n if (numberOfParts === 2) {\n // simple case: try to split at the an intersection vertex\n const splitIntersections = entity.nodes\n .map(nid => g.entity(nid))\n .filter(n => g.parentWays(n).length > 1)\n .map(n => n.id)\n .filter(nid => {\n const splitIndex = entity.nodes.indexOf(nid);\n return splitIndex < maxWayNodes &&\n entity.nodes.length - splitIndex < maxWayNodes;\n });\n if (splitIntersections.length > 0) {\n splitVertices = [\n splitIntersections[Math.floor(splitIntersections.length / 2)]\n ];\n }\n }\n\n if (splitVertices === undefined) {\n // general case: either more than one split is needed or no possible\n // intersection split point was found -> just split at regular intervals\n splitVertices = [...Array(numberOfParts - 1)].map((_, i) =>\n entity.nodes[Math.floor(entity.nodes.length * (i + 1) / numberOfParts)]);\n }\n\n if (entity.isClosed()) {\n // add extra split for closed ways at start of way\n splitVertices.push(entity.nodes[0]);\n }\n\n const operation = operationSplit(context, splitVertices.concat(entityId));\n if (!operation.disabled()) {\n operation();\n }\n }\n });\n\n return [fix];\n }\n\n\n validation.type = type;\n\n return validation;\n}\n", "/** @typedef {{ old: Tags; replace?: Tags }[]} DataDeprecated */\n\n/** @param {Tags} tags @param {DataDeprecated} dataDeprecated */\nexport function getDeprecatedTags(tags, dataDeprecated) {\n // if there are no tags, none can be deprecated\n if (Object.keys(tags).length === 0) return [];\n\n /** @type {DataDeprecated} */\n var deprecated = [];\n dataDeprecated.forEach((d) => {\n const oldKeys = Object.keys(d.old);\n const transferKeys = oldKeys.filter(key => d.old[key] === '*');\n if (d.replace) {\n var hasExistingValues = Object.keys(d.replace).some((replaceKey) => {\n if (!tags[replaceKey] || d.old[replaceKey]) return false;\n var replaceValue = d.replace[replaceKey];\n if (replaceValue === '*') return false;\n if (replaceValue.startsWith('$1') && tags[replaceKey] === tags[transferKeys[+replaceValue.substring(1) - 1]]) return false;\n if (replaceValue === tags[replaceKey]) return false;\n return true;\n });\n // don't flag deprecated tags if the upgrade path would overwrite existing data - #7843\n if (hasExistingValues) return;\n }\n\n var matchesDeprecatedTags = oldKeys.every((oldKey) => {\n if (!tags[oldKey]) return false;\n if (d.old[oldKey] === '*') return true;\n if (d.old[oldKey] === tags[oldKey]) return true;\n\n var vals = tags[oldKey].split(';').filter(Boolean);\n if (vals.length === 0) {\n return false;\n } else if (vals.length > 1) {\n return vals.indexOf(d.old[oldKey]) !== -1;\n } else {\n if (tags[oldKey] === d.old[oldKey]) {\n if (d.replace && d.old[oldKey] === d.replace[oldKey]) {\n var replaceKeys = Object.keys(d.replace);\n return !replaceKeys.every((replaceKey) => {\n return tags[replaceKey] === d.replace[replaceKey];\n });\n } else {\n return true;\n }\n }\n }\n\n return false;\n });\n\n if (matchesDeprecatedTags) {\n deprecated.push(d);\n }\n });\n\n return deprecated;\n}\n\n/** @type {{ [key: string]: string[] }} */\nvar _deprecatedTagValuesByKey;\n\n/** @param {DataDeprecated} dataDeprecated */\nexport function deprecatedTagValuesByKey(dataDeprecated) {\n if (!_deprecatedTagValuesByKey) {\n _deprecatedTagValuesByKey = {};\n dataDeprecated.forEach((d) => {\n var oldKeys = Object.keys(d.old);\n if (oldKeys.length === 1) {\n var oldKey = oldKeys[0];\n var oldValue = d.old[oldKey];\n if (oldValue !== '*') {\n if (!_deprecatedTagValuesByKey[oldKey]) {\n _deprecatedTagValuesByKey[oldKey] = [oldValue];\n } else {\n _deprecatedTagValuesByKey[oldKey].push(oldValue);\n }\n }\n }\n });\n }\n return _deprecatedTagValuesByKey;\n};\n", "import { t } from '../core/localizer';\n\nimport { actionChangePreset } from '../actions/change_preset';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionUpgradeTags } from '../actions/upgrade_tags';\nimport { fileFetcher } from '../core';\nimport { presetManager } from '../presets';\nimport { services } from '../services';\nimport { utilHashcode, utilTagDiff } from '../util';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { getDeprecatedTags } from '../osm/deprecated';\n\n/** @import { TagDiff } from '../util/util'. */\n\n\nexport function validationOutdatedTags() {\n const type = 'outdated_tags';\n let _waitingForDeprecated = true;\n let _dataDeprecated;\n\n // fetch deprecated tags\n fileFetcher.get('deprecated')\n .then(d => _dataDeprecated = d)\n .catch(() => { /* ignore */ })\n .finally(() => _waitingForDeprecated = false);\n\n\n function oldTagIssues(entity, graph) {\n if (!entity.hasInterestingTags()) return [];\n\n let preset = presetManager.match(entity, graph);\n if (!preset) return [];\n\n const oldTags = Object.assign({}, entity.tags); // shallow copy\n\n // Upgrade preset, if a replacement is available..\n if (preset.replacement) {\n const newPreset = presetManager.item(preset.replacement);\n graph = actionChangePreset(entity.id, preset, newPreset, true /* skip field defaults */)(graph);\n entity = graph.entity(entity.id);\n preset = newPreset;\n }\n\n // Attempt to match a canonical record in the name-suggestion-index.\n const nsi = services.nsi;\n let waitingForNsi = false;\n let nsiResult;\n if (nsi) {\n waitingForNsi = (nsi.status() === 'loading');\n if (!waitingForNsi) {\n const loc = entity.extent(graph).center();\n nsiResult = nsi.upgradeTags(oldTags, loc);\n }\n }\n const nsiDiff = nsiResult ? utilTagDiff(oldTags, nsiResult.newTags) : [];\n\n // Upgrade deprecated tags\n let deprecatedTags;\n if (_dataDeprecated) {\n deprecatedTags = getDeprecatedTags(entity.tags, _dataDeprecated);\n if (entity.type === 'way' && entity.isClosed() &&\n entity.tags.traffic_calming === 'island' && !entity.tags.highway) {\n // https://github.com/openstreetmap/id-tagging-schema/issues/1162#issuecomment-2000356902\n deprecatedTags.push({\n old: {traffic_calming: 'island'},\n replace: {'area:highway': 'traffic_island'}\n });\n }\n if (deprecatedTags.length) {\n deprecatedTags.forEach(tag => {\n graph = actionUpgradeTags(entity.id, tag.old, tag.replace)(graph);\n });\n entity = graph.entity(entity.id);\n }\n }\n\n // Add missing addTags from the detected preset\n let newTags = Object.assign({}, entity.tags); // shallow copy\n if (preset.tags !== preset.addTags) {\n Object.keys(preset.addTags).filter(k => {\n // if nsi suggestion already includes this tag: don't repeat it in \"incomplete tags\"\n return !nsiResult?.newTags[k];\n }).forEach(k => {\n if (!newTags[k]) {\n if (preset.addTags[k] === '*') {\n newTags[k] = 'yes';\n } else if (preset.addTags[k]) {\n newTags[k] = preset.addTags[k];\n }\n }\n });\n }\n const deprecationDiff = utilTagDiff(oldTags, newTags);\n const deprecationDiffContext = Object.keys(oldTags)\n .filter(key => deprecatedTags?.some(deprecated => deprecated.replace?.[key] !== undefined))\n .filter(key => newTags[key] === oldTags[key])\n .map(key => ({\n type: '~',\n key,\n oldVal: oldTags[key],\n newVal: newTags[key],\n display: '  ' + key + '=' + oldTags[key]\n }));\n\n let issues = [];\n issues.provisional = (_waitingForDeprecated || waitingForNsi);\n\n if (deprecationDiff.length) {\n const isOnlyAddingTags = !deprecationDiff.some(d => d.type === '-');\n const prefix = isOnlyAddingTags ? 'incomplete.' : '';\n\n issues.push(new validationIssue({\n type: type,\n subtype: isOnlyAddingTags ? 'incomplete_tags' : 'deprecated_tags',\n severity: 'warning',\n message: (context) => {\n const currEntity = context.hasEntity(entity.id);\n if (!currEntity) return '';\n\n const feature = utilDisplayLabel(currEntity, context.graph(), /* verbose */ true);\n\n return t.append(`issues.outdated_tags.${prefix}message`, { feature });\n },\n reference: selection => showReference(\n selection,\n t.append(`issues.outdated_tags.${prefix}reference`),\n [...deprecationDiff, ...deprecationDiffContext]\n ),\n entityIds: [entity.id],\n hash: utilHashcode(JSON.stringify(deprecationDiff)),\n dynamicFixes: () => {\n let fixes = [\n new validationIssueFix({\n title: t.append('issues.fix.upgrade_tags.title'),\n onClick: (context) => {\n context.perform(graph => doUpgrade(graph, deprecationDiff), t('issues.fix.upgrade_tags.annotation'));\n }\n })\n ];\n return fixes;\n }\n }));\n }\n\n if (nsiDiff.length) {\n const isOnlyAddingTags = nsiDiff.every(d => d.type === '+');\n\n issues.push(new validationIssue({\n type: type,\n subtype: 'noncanonical_brand',\n severity: 'warning',\n message: (context) => {\n const currEntity = context.hasEntity(entity.id);\n if (!currEntity) return '';\n\n const feature = utilDisplayLabel(currEntity, context.graph(), /* verbose */ true);\n\n return isOnlyAddingTags\n ? t.append('issues.outdated_tags.noncanonical_brand.message_incomplete', { feature })\n : t.append('issues.outdated_tags.noncanonical_brand.message', { feature });\n },\n reference: selection => showReference(\n selection,\n t.append('issues.outdated_tags.noncanonical_brand.reference'),\n nsiDiff\n ),\n entityIds: [entity.id],\n hash: utilHashcode(JSON.stringify(nsiDiff)),\n dynamicFixes: () => {\n let fixes = [\n new validationIssueFix({\n title: t.append('issues.fix.upgrade_tags.title'),\n onClick: (context) => {\n context.perform(graph => doUpgrade(graph, nsiDiff), t('issues.fix.upgrade_tags.annotation'));\n }\n }),\n new validationIssueFix({\n title: t.append('issues.fix.tag_as_not.title', { name: nsiResult.matched.displayName }),\n onClick: (context) => {\n context.perform(addNotTag, t('issues.fix.tag_as_not.annotation'));\n }\n })\n ];\n return fixes;\n }\n }));\n }\n\n return issues;\n\n\n /** @param {iD.Graph} graph @param {TagDiff[]} diff */\n function doUpgrade(graph, diff) {\n const currEntity = graph.hasEntity(entity.id);\n if (!currEntity) return graph;\n\n let newTags = Object.assign({}, currEntity.tags); // shallow copy\n diff.forEach(diff => {\n if (diff.type === '-') {\n delete newTags[diff.key];\n } else if (diff.type === '+') {\n newTags[diff.key] = diff.newVal;\n }\n });\n\n return actionChangeTags(currEntity.id, newTags)(graph);\n }\n\n\n function addNotTag(graph) {\n const currEntity = graph.hasEntity(entity.id);\n if (!currEntity) return graph;\n\n const item = nsiResult && nsiResult.matched;\n if (!item) return graph;\n\n let newTags = Object.assign({}, currEntity.tags); // shallow copy\n const wd = item.mainTag; // e.g. `brand:wikidata`\n const notwd = `not:${wd}`; // e.g. `not:brand:wikidata`\n const qid = item.tags[wd];\n newTags[notwd] = qid;\n\n if (newTags[wd] === qid) { // if `brand:wikidata` was set to that qid\n const wp = item.mainTag.replace('wikidata', 'wikipedia');\n delete newTags[wd]; // remove `brand:wikidata`\n delete newTags[wp]; // remove `brand:wikipedia`\n }\n\n return actionChangeTags(currEntity.id, newTags)(graph);\n }\n\n\n function showReference(selection, reference, tagDiff) {\n let enter = selection.selectAll('.issue-reference')\n .data([0])\n .enter();\n\n enter\n .append('div')\n .attr('class', 'issue-reference')\n .call(reference);\n\n enter\n .append('strong')\n .call(t.append('issues.suggested'));\n\n enter\n .append('table')\n .attr('class', 'tagDiff-table')\n .selectAll('.tagDiff-row')\n .data(tagDiff)\n .enter()\n .append('tr')\n .attr('class', 'tagDiff-row')\n .append('td')\n .attr('class', d => {\n const klass = 'tagDiff-cell';\n switch (d.type) {\n case '+':\n return `${klass} tagDiff-cell-add`;\n case '-':\n return `${klass} tagDiff-cell-remove`;\n default:\n return `${klass} tagDiff-cell-unchanged`;\n }\n })\n .html(d => d.display);\n }\n }\n\n\n let validation = oldTagIssues;\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { t } from '../core/localizer';\nimport { utilTagDiff } from '../util';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationPrivateData() {\n var type = 'private_data';\n\n // assume that some buildings are private\n var privateBuildingValues = {\n detached: true,\n farm: true,\n house: true,\n houseboat: true,\n residential: true,\n semidetached_house: true,\n static_caravan: true\n };\n\n // but they might be public if they have one of these other tags\n var publicKeys = {\n amenity: true,\n craft: true,\n historic: true,\n leisure: true,\n office: true,\n shop: true,\n tourism: true\n };\n\n // these tags may contain personally identifying info\n var personalTags = {\n 'contact:email': true,\n 'contact:fax': true,\n 'contact:phone': true,\n email: true,\n fax: true,\n phone: true\n };\n\n\n var validation = function checkPrivateData(entity) {\n var tags = entity.tags;\n if (!tags.building || !privateBuildingValues[tags.building]) return [];\n\n var keepTags = {};\n for (var k in tags) {\n if (publicKeys[k]) return []; // probably a public feature\n if (!personalTags[k]) {\n keepTags[k] = tags[k];\n }\n }\n\n var tagDiff = utilTagDiff(tags, keepTags);\n if (!tagDiff.length) return [];\n\n var fixID = tagDiff.length === 1 ? 'remove_tag' : 'remove_tags';\n\n return [new validationIssue({\n type: type,\n severity: 'warning',\n message: showMessage,\n reference: showReference,\n entityIds: [entity.id],\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.' + fixID + '.title'),\n onClick: function(context) {\n context.perform(doUpgrade, t('issues.fix.remove_tag.annotation'));\n }\n })\n ];\n }\n })];\n\n\n function doUpgrade(graph) {\n var currEntity = graph.hasEntity(entity.id);\n if (!currEntity) return graph;\n\n var newTags = Object.assign({}, currEntity.tags); // shallow copy\n tagDiff.forEach(function(diff) {\n if (diff.type === '-') {\n delete newTags[diff.key];\n } else if (diff.type === '+') {\n newTags[diff.key] = diff.newVal;\n }\n });\n\n return actionChangeTags(currEntity.id, newTags)(graph);\n }\n\n\n function showMessage(context) {\n var currEntity = context.hasEntity(this.entityIds[0]);\n if (!currEntity) return '';\n\n return t.append('issues.private_data.contact.message',\n { feature: utilDisplayLabel(currEntity, context.graph()) }\n );\n }\n\n\n function showReference(selection) {\n var enter = selection.selectAll('.issue-reference')\n .data([0])\n .enter();\n\n enter\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.private_data.reference'));\n\n enter\n .append('strong')\n .call(t.append('issues.suggested'));\n\n enter\n .append('table')\n .attr('class', 'tagDiff-table')\n .selectAll('.tagDiff-row')\n .data(tagDiff)\n .enter()\n .append('tr')\n .attr('class', 'tagDiff-row')\n .append('td')\n .attr('class', function(d) {\n var klass = d.type === '+' ? 'add' : 'remove';\n return 'tagDiff-cell tagDiff-cell-' + klass;\n })\n .html(function(d) { return d.display; });\n }\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { actionChangeTags } from '../actions/change_tags';\nimport { presetManager } from '../presets';\nimport { services } from '../services';\nimport { t, localizer } from '../core/localizer';\nimport { validationIssue, validationIssueFix } from '../core/validation';\n\n\nexport function validationSuspiciousName(context) {\n const type = 'suspicious_name';\n const keysToTestForGenericValues = [\n 'aerialway', 'aeroway', 'amenity', 'building', 'craft', 'highway',\n 'leisure', 'railway', 'man_made', 'office', 'shop', 'tourism', 'waterway'\n ];\n const ignoredPresets = new Set([\n 'amenity/place_of_worship/christian/jehovahs_witness',\n '__test__ignored_preset' // for unit tests\n ]);\n let _waitingForNsi = false;\n\n\n // Attempt to match a generic record in the name-suggestion-index.\n function isGenericMatchInNsi(tags) {\n const nsi = services.nsi;\n if (nsi) {\n _waitingForNsi = (nsi.status() === 'loading');\n if (!_waitingForNsi) {\n return nsi.isGenericName(tags);\n }\n }\n return false;\n }\n\n\n // Test if the name is just the key or tag value (e.g. \"park\")\n function nameMatchesRawTag(lowercaseName, tags) {\n for (let i = 0; i < keysToTestForGenericValues.length; i++) {\n let key = keysToTestForGenericValues[i];\n let val = tags[key];\n if (val) {\n val = val.toLowerCase();\n if (key === lowercaseName ||\n val === lowercaseName ||\n key.replace(/\\_/g, ' ') === lowercaseName ||\n val.replace(/\\_/g, ' ') === lowercaseName) {\n return true;\n }\n }\n }\n return false;\n }\n\n /** @param {string} name */\n function nameMatchesPresetName(name, preset) {\n if (!preset) return false;\n if (ignoredPresets.has(preset.id)) return false;\n\n name = name.toLowerCase();\n return name === preset.name().toLowerCase() || preset.aliases().some(alias => name === alias.toLowerCase());\n }\n\n /** @param {string} name */\n function isGenericName(name, tags, preset) {\n name = name.toLowerCase();\n return nameMatchesRawTag(name, tags) || nameMatchesPresetName(name, preset) || isGenericMatchInNsi(tags);\n }\n\n function makeGenericNameIssue(entityId, nameKey, genericName, langCode) {\n return new validationIssue({\n type: type,\n subtype: 'generic_name',\n severity: 'warning',\n message: function(context) {\n let entity = context.hasEntity(this.entityIds[0]);\n if (!entity) return '';\n let preset = presetManager.match(entity, context.graph());\n let langName = langCode && localizer.languageName(langCode);\n return t.append('issues.generic_name.message' + (langName ? '_language' : ''),\n { feature: preset.name(), name: genericName, language: langName }\n );\n },\n reference: showReference,\n entityIds: [entityId],\n hash: `${nameKey}=${genericName}`,\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-delete',\n title: t.append('issues.fix.remove_the_name.title'),\n onClick: function(context) {\n let entityId = this.issue.entityIds[0];\n let entity = context.entity(entityId);\n let tags = Object.assign({}, entity.tags); // shallow copy\n delete tags[nameKey];\n context.perform(\n actionChangeTags(entityId, tags), t('issues.fix.remove_generic_name.annotation')\n );\n }\n })\n ];\n }\n });\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.generic_name.reference'));\n }\n }\n\n let validation = function checkGenericName(entity) {\n const tags = entity.tags;\n\n // a generic name is allowed if it's a known brand or entity\n const hasWikidata = (!!tags.wikidata || !!tags['brand:wikidata'] || !!tags['operator:wikidata']);\n if (hasWikidata) return [];\n\n let issues = [];\n\n const preset = presetManager.match(entity, context.graph());\n\n for (let key in tags) {\n const m = key.match(/^name(?:(?::)([a-zA-Z_-]+))?$/);\n if (!m) continue;\n\n const langCode = m.length >= 2 ? m[1] : null;\n const value = tags[key];\n\n if (isGenericName(value, tags, preset)) {\n issues.provisional = _waitingForNsi; // retry later if we are waiting on NSI to finish loading\n issues.push(makeGenericNameIssue(entity.id, key, value, langCode));\n }\n }\n\n return issues;\n };\n\n\n validation.type = type;\n\n return validation;\n}\n", "import { prefs } from '../core/preferences';\nimport { t } from '../core/localizer';\n//import { actionChangeTags } from '../actions/change_tags';\nimport { actionOrthogonalize } from '../actions/orthogonalize';\nimport { geoOrthoCanOrthogonalize } from '../geo/ortho';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { validationIssue, validationIssueFix } from '../core/validation';\nimport { services } from '../services';\n\nexport function validationUnsquareWay(context) {\n var type = 'unsquare_way';\n var DEFAULT_DEG_THRESHOLD = 5; // see also issues.js\n\n // use looser epsilon for detection to reduce warnings of buildings that are essentially square already\n var epsilon = 0.05;\n var nodeThreshold = 10;\n\n function isBuilding(entity, graph) {\n if (entity.type !== 'way' || entity.geometry(graph) !== 'area') return false;\n return entity.tags.building && entity.tags.building !== 'no';\n }\n\n\n var validation = function checkUnsquareWay(entity, graph) {\n\n if (!isBuilding(entity, graph)) return [];\n\n // don't flag ways marked as physically unsquare\n if (entity.tags.nonsquare === 'yes') return [];\n\n var isClosed = entity.isClosed();\n if (!isClosed) return []; // this building has bigger problems\n\n // don't flag ways with lots of nodes since they are likely detail-mapped\n var nodes = graph.childNodes(entity).slice(); // shallow copy\n if (nodes.length > nodeThreshold + 1) return []; // +1 because closing node appears twice\n\n // ignore if not all nodes are fully downloaded\n var osm = services.osm;\n if (!osm || nodes.some(function(node) { return !osm.isDataLoaded(node.loc); })) return [];\n\n // don't flag connected ways to avoid unresolvable unsquare loops\n var hasConnectedSquarableWays = nodes.some(function(node) {\n return graph.parentWays(node).some(function(way) {\n if (way.id === entity.id) return false;\n if (isBuilding(way, graph)) return true;\n return graph.parentRelations(way).some(function(parentRelation) {\n return parentRelation.isMultipolygon() &&\n parentRelation.tags.building &&\n parentRelation.tags.building !== 'no';\n });\n });\n });\n if (hasConnectedSquarableWays) return [];\n\n\n // user-configurable square threshold\n var storedDegreeThreshold = prefs('validate-square-degrees');\n var degreeThreshold = isFinite(storedDegreeThreshold) ? Number(storedDegreeThreshold) : DEFAULT_DEG_THRESHOLD;\n\n var points = nodes.map(function(node) { return context.projection(node.loc); });\n if (!geoOrthoCanOrthogonalize(points, isClosed, epsilon, degreeThreshold, true)) return [];\n\n return [new validationIssue({\n type: type,\n subtype: 'building',\n severity: 'suggestion',\n message: function(context) {\n var entity = context.hasEntity(this.entityIds[0]);\n return entity ? t.append('issues.unsquare_way.message', {\n feature: utilDisplayLabel(entity, context.graph())\n }) : '';\n },\n reference: showReference,\n entityIds: [entity.id],\n hash: degreeThreshold,\n dynamicFixes: function() {\n return [\n new validationIssueFix({\n icon: 'iD-operation-orthogonalize',\n title: t.append('issues.fix.square_feature.title'),\n onClick: function(context, completionHandler) {\n var entityId = this.issue.entityIds[0];\n // use same degree threshold as for detection\n context.perform(\n actionOrthogonalize(entityId, context.projection, undefined, degreeThreshold),\n t('operations.orthogonalize.annotation.feature', { n: 1 })\n );\n // run after the squaring transition (currently 150ms)\n window.setTimeout(function() { completionHandler(); }, 175);\n }\n }),\n /*\n new validationIssueFix({\n title: t.append('issues.fix.tag_as_unsquare.title'),\n onClick: function(context) {\n var entityId = this.issue.entityIds[0];\n var entity = context.entity(entityId);\n var tags = Object.assign({}, entity.tags); // shallow copy\n tags.nonsquare = 'yes';\n context.perform(\n actionChangeTags(entityId, tags),\n t('issues.fix.tag_as_unsquare.annotation')\n );\n }\n })\n */\n ];\n }\n })];\n\n function showReference(selection) {\n selection.selectAll('.issue-reference')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'issue-reference')\n .call(t.append('issues.unsquare_way.buildings.reference'));\n }\n };\n\n validation.type = type;\n\n return validation;\n}\n", "export { validationAlmostJunction } from './almost_junction';\nexport { validationCloseNodes } from './close_nodes';\nexport { validationCrossingWays } from './crossing_ways';\nexport { validationDisconnectedWay } from './disconnected_way';\nexport { validationMissingStartDate } from './missing_start_date';\nexport { validationFormatting } from './invalid_format';\nexport { validationHelpRequest } from './help_request';\nexport { validationImpossibleOneway } from './impossible_oneway';\nexport { validationIncompatibleSource } from './incompatible_source';\nexport { validationMaprules } from './maprules';\nexport { validationMismatchedDates } from './mismatched_dates';\nexport { validationMismatchedGeometry } from './mismatched_geometry';\nexport { validationMissingRole } from './missing_role';\nexport { validationMissingTag } from './missing_tag';\nexport { validationMutuallyExclusiveTags } from './mutually_exclusive_tags';\nexport { validationOsmApiLimits } from './osm_api_limits';\nexport { validationOutdatedTags } from './outdated_tags';\nexport { validationPrivateData } from './private_data';\nexport { validationSuspiciousName } from './suspicious_name';\nexport { validationUnsquareWay } from './unsquare_way';\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from './preferences';\nimport { coreDifference } from './difference';\nimport { geoExtent } from '../geo/extent';\nimport { modeSelect } from '../modes/select';\nimport { utilArrayChunk, utilArrayDifference, utilArrayGroupBy, utilArrayIntersection, utilArrayUnion, utilEntityAndDeepMemberIDs, utilRebind } from '../util';\nimport * as Validations from '../validations/index';\n\n\nexport function coreValidator(context) {\n let dispatch = d3_dispatch('validated', 'focusedIssue');\n const validator = {};\n\n let _rules = {};\n let _disabledRules = {};\n\n let _ignoredIssueIDs = new Set();\n let _resolvedIssueIDs = new Set();\n let _baseCache = validationCache('base'); // issues before any user edits\n let _headCache = validationCache('head'); // issues after all user edits\n let _completeDiff = {}; // complete diff base -> head of what the user changed\n let _headIsCurrent = false;\n\n let _deferredRIC = {}; // Object( RequestIdleCallback handle : rejectPromise method )\n let _deferredST = new Set(); // Set( SetTimeout handles )\n let _headPromise; // Promise fulfilled when validation is performed up to headGraph snapshot\n\n const RETRY = 5000; // wait 5sec before revalidating provisional entities\n\n\n // Allow validation severity to be overridden by url queryparams...\n // See: https://github.com/openstreetmap/iD/pull/8243\n //\n // Each param should contain a urlencoded comma separated list of\n // `type/subtype` rules. `*` may be used as a wildcard..\n // Examples:\n // `validationError=disconnected_way/*`\n // `validationError=disconnected_way/highway`\n // `validationError=crossing_ways/bridge*`\n // `validationError=crossing_ways/bridge*,crossing_ways/tunnel*`\n\n const _errorOverrides = parseHashParam(context.initialHashParams.validationError);\n const _warningOverrides = parseHashParam(context.initialHashParams.validationWarning);\n const _suggestionOverrides = parseHashParam(context.initialHashParams.validationSuggestion);\n const _disableOverrides = parseHashParam(context.initialHashParams.validationDisable);\n\n // `parseHashParam()` (private)\n // Checks hash parameters for severity overrides\n // Arguments\n // `param` - a url hash parameter (`validationError`, `validationWarning`, `validationSuggestion`, or `validationDisable`)\n // Returns\n // Array of Objects like { type: RegExp, subtype: RegExp }\n //\n function parseHashParam(param) {\n let result = [];\n let rules = (param || '').split(',');\n rules.forEach(rule => {\n rule = rule.trim();\n const parts = rule.split('/', 2); // \"type/subtype\"\n const type = parts[0];\n const subtype = parts[1] || '*';\n if (!type || !subtype) return;\n result.push({ type: makeRegExp(type), subtype: makeRegExp(subtype) });\n });\n return result;\n\n function makeRegExp(str) {\n const escaped = str\n .replace(/[-\\/\\\\^$+?.()|[\\]{}]/g, '\\\\$&') // escape all reserved chars except for the '*'\n .replace(/\\*/g, '.*'); // treat a '*' like '.*'\n return new RegExp('^' + escaped + '$');\n }\n }\n\n\n // `init()`\n // Initialize the validator, called once on iD startup\n //\n validator.init = () => {\n Object.values(Validations).forEach(validation => {\n if (typeof validation !== 'function') return;\n const fn = validation(context);\n const key = fn.type;\n _rules[key] = fn;\n });\n\n let disabledRules = prefs('validate-disabledRules');\n if (disabledRules) {\n disabledRules.split(',').forEach(k => _disabledRules[k] = true);\n }\n };\n\n\n // `reset()` (private)\n // Cancels deferred work and resets all caches\n //\n // Arguments\n // `resetIgnored` - `true` to clear the list of user-ignored issues\n //\n function reset(resetIgnored) {\n // empty queues\n _baseCache.queue = [];\n _headCache.queue = [];\n\n // cancel deferred work and reject any pending promise\n Object.keys(_deferredRIC).forEach(key => {\n window.cancelIdleCallback(key);\n _deferredRIC[key]();\n });\n _deferredRIC = {};\n _deferredST.forEach(window.clearTimeout);\n _deferredST.clear();\n\n // clear caches\n if (resetIgnored) _ignoredIssueIDs.clear();\n _resolvedIssueIDs.clear();\n _baseCache = validationCache('base');\n _headCache = validationCache('head');\n _completeDiff = {};\n _headIsCurrent = false;\n }\n\n\n // `reset()`\n // clear caches, called whenever iD resets after a save or switches sources\n // (clears out the _ignoredIssueIDs set also)\n //\n validator.reset = () => {\n reset(true);\n };\n\n\n // `resetIgnoredIssues()`\n // clears out the _ignoredIssueIDs Set\n //\n validator.resetIgnoredIssues = () => {\n _ignoredIssueIDs.clear();\n dispatch.call('validated'); // redraw UI\n };\n\n\n // `revalidateUnsquare()`\n // Called whenever the user changes the unsquare threshold\n // It reruns just the \"unsquare_way\" validation on all buildings.\n //\n validator.revalidateUnsquare = () => {\n revalidateUnsquare(_headCache);\n revalidateUnsquare(_baseCache);\n dispatch.call('validated');\n };\n\n function revalidateUnsquare(cache) {\n const checkUnsquareWay = _rules.unsquare_way;\n if (!cache.graph || typeof checkUnsquareWay !== 'function') return;\n\n // uncache existing\n cache.uncacheIssuesOfType('unsquare_way');\n\n const buildings = context.history().tree().intersects(geoExtent([-180,-90],[180, 90]), cache.graph) // everywhere\n .filter(entity => (entity.type === 'way' && entity.tags.building && entity.tags.building !== 'no'));\n\n // rerun for all buildings\n buildings.forEach(entity => {\n const detected = checkUnsquareWay(entity, cache.graph);\n if (!detected.length) return;\n cache.cacheIssues(detected);\n });\n }\n\n\n // `getIssues()`\n // Gets all issues that match the given options\n // This is called by many other places\n //\n // Arguments\n // `options` Object like:\n // {\n // what: 'all', // 'all' or 'edited'\n // where: 'all', // 'all' or 'visible'\n // includeIgnored: false, // true, false, or 'only'\n // includeDisabledRules: false // true, false, or 'only'\n // }\n //\n // Returns\n // An Array containing the issues\n //\n validator.getIssues = (options) => {\n const opts = Object.assign({ what: 'all', where: 'all', includeIgnored: false, includeDisabledRules: false }, options);\n const view = context.map().extent();\n let seen = new Set();\n let results = [];\n\n // collect head issues - present in the user edits\n if (_headCache.graph && _headCache.graph !== _baseCache.graph) {\n Object.values(_headCache.issuesByIssueID).forEach(issue => {\n // In the head cache, only count features that the user is responsible for - #8632\n // For example, a user can undo some work and an issue will still present in the\n // head graph, but we don't want to credit the user for causing that issue.\n const userModified = (issue.entityIds || []).some(id => _completeDiff.hasOwnProperty(id));\n if (opts.what === 'edited' && !userModified) return; // present in head but user didn't touch it\n\n if (!filter(issue)) return;\n seen.add(issue.id);\n results.push(issue);\n });\n }\n\n // collect base issues - present before user edits\n if (opts.what === 'all') {\n Object.values(_baseCache.issuesByIssueID).forEach(issue => {\n if (!filter(issue)) return;\n seen.add(issue.id);\n results.push(issue);\n });\n }\n\n return results;\n\n\n // Filter the issue set to include only what the calling code wants to see.\n // Note that we use `context.graph()`/`context.hasEntity()` here, not `cache.graph`,\n // because that is the graph that the calling code will be using.\n function filter(issue) {\n if (!issue) return false;\n if (seen.has(issue.id)) return false;\n if (_resolvedIssueIDs.has(issue.id)) return false;\n if (opts.includeDisabledRules === 'only' && !_disabledRules[issue.type]) return false;\n if (!opts.includeDisabledRules && _disabledRules[issue.type]) return false;\n\n if (opts.includeIgnored === 'only' && !_ignoredIssueIDs.has(issue.id)) return false;\n if (!opts.includeIgnored && _ignoredIssueIDs.has(issue.id)) return false;\n\n // This issue may involve an entity that doesn't exist in context.graph()\n // This can happen because validation is async and rendering the issue lists is async.\n if ((issue.entityIds || []).some(id => !context.hasEntity(id))) return false;\n\n if (opts.where === 'visible') {\n const extent = issue.extent(context.graph());\n if (!view.intersects(extent)) return false;\n }\n\n return true;\n }\n };\n\n\n // `getResolvedIssues()`\n // Gets the issues that have been fixed by the user.\n //\n // Resolved issues are tracked in the `_resolvedIssueIDs` Set,\n // and they should all be issues that exist in the _baseCache.\n //\n // Returns\n // An Array containing the issues\n //\n validator.getResolvedIssues = () => {\n return Array.from(_resolvedIssueIDs)\n .map(issueID => _baseCache.issuesByIssueID[issueID])\n .filter(Boolean);\n };\n\n\n // `focusIssue()`\n // Adjusts the map to focus on the given issue.\n // (requires the issue to have a reasonable extent defined)\n //\n // Arguments\n // `issue` - the issue to focus on\n //\n validator.focusIssue = (issue) => {\n // Note that we use `context.graph()`/`context.hasEntity()` here, not `cache.graph`,\n // because that is the graph that the calling code will be using.\n const graph = context.graph();\n let selectID;\n\n // Try to focus the map at the center of the issue..\n let issueExtent = issue.extent(graph);\n\n // Try to select the first entity in the issue..\n if (issue.entityIds && issue.entityIds.length) {\n selectID = issue.entityIds[0];\n\n // If a relation, focus on one of its members instead.\n // Otherwise we might be focusing on a part of map where the relation is not visible.\n if (selectID && selectID.charAt(0) === 'r') { // relation\n const ids = utilEntityAndDeepMemberIDs([selectID], graph);\n let nodeID = ids.find(id => id.charAt(0) === 'n' && graph.hasEntity(id));\n\n if (!nodeID) { // relation has no downloaded nodes to focus on\n const wayID = ids.find(id => id.charAt(0) === 'w' && graph.hasEntity(id));\n if (wayID) {\n nodeID = graph.entity(wayID).first(); // focus on the first node of this way\n }\n }\n\n if (nodeID) {\n issueExtent = graph.entity(nodeID).extent(graph);\n }\n }\n }\n\n // Adjust the view\n context.map().zoomToEase(issueExtent);\n\n if (selectID) { // Enter select mode\n window.setTimeout(() => {\n context.enter(modeSelect(context, [selectID]));\n dispatch.call('focusedIssue', this, issue);\n }, 250); // after ease\n }\n };\n\n\n // `getIssuesBySeverity()`\n // Gets the issues then groups them by error/warning/suggestion\n // (This just calls getIssues, then puts issues in groups)\n //\n // Arguments\n // `options` - (see `getIssues`)\n // Returns\n // Object result like:\n // {\n // error: Array of errors,\n // warning: Array of warnings,\n // suggestion: Array of suggestions,\n // }\n //\n validator.getIssuesBySeverity = (options) => {\n let groups = utilArrayGroupBy(validator.getIssues(options), 'severity');\n groups.error = groups.error || [];\n groups.warning = groups.warning || [];\n groups.suggestion = groups.suggestion || [];\n return groups;\n };\n\n\n // `getEntityIssues()`\n // Gets the issues that the given entity IDs have in common, matching the given options\n // (This just calls getIssues, then filters for the given entity IDs)\n // The issues are sorted for relevance\n //\n // Arguments\n // `entityIDs` - Array or Set of entityIDs to get issues for\n // `options` - (see `getIssues`)\n // Returns\n // An Array containing the issues\n //\n validator.getSharedEntityIssues = (entityIDs, options) => {\n const orderedIssueTypes = [ // Show some issue types in a particular order:\n 'missing_tag', 'missing_role', // - missing data first\n 'outdated_tags', 'mismatched_geometry', // - identity issues\n 'crossing_ways', 'almost_junction', // - geometry issues where fixing them might solve connectivity issues\n 'disconnected_way', 'impossible_oneway' // - finally connectivity issues\n ];\n\n const allIssues = validator.getIssues(options);\n const forEntityIDs = new Set(entityIDs);\n\n return allIssues\n .filter(issue => (issue.entityIds || []).some(entityID => forEntityIDs.has(entityID)))\n .sort((issue1, issue2) => {\n if (issue1.type === issue2.type) { // issues of the same type, sort deterministically\n return issue1.id < issue2.id ? -1 : 1;\n }\n const index1 = orderedIssueTypes.indexOf(issue1.type);\n const index2 = orderedIssueTypes.indexOf(issue2.type);\n if (index1 !== -1 && index2 !== -1) { // both issue types have explicit sort orders\n return index1 - index2;\n } else if (index1 === -1 && index2 === -1) { // neither issue type has an explicit sort order, sort by type\n return issue1.type < issue2.type ? -1 : 1;\n } else { // order explicit types before everything else\n return index1 !== -1 ? -1 : 1;\n }\n });\n };\n\n\n // `getEntityIssues()`\n // Get an array of detected issues for the given entityID.\n // (This just calls getSharedEntityIssues for a single entity)\n //\n // Arguments\n // `entityID` - the entity ID to get the issues for\n // `options` - (see `getIssues`)\n // Returns\n // An Array containing the issues\n //\n validator.getEntityIssues = (entityID, options) => {\n return validator.getSharedEntityIssues([entityID], options);\n };\n\n\n // `getRuleKeys()`\n //\n // Returns\n // An Array containing the rule keys\n //\n validator.getRuleKeys = () => {\n return Object.keys(_rules);\n };\n\n\n // `isRuleEnabled()`\n //\n // Arguments\n // `key` - the rule to check (e.g. 'crossing_ways')\n // Returns\n // `true`/`false`\n //\n validator.isRuleEnabled = (key) => {\n return !_disabledRules[key];\n };\n\n\n // `toggleRule()`\n // Toggles a single validation rule,\n // then reruns the validation so that the user sees something happen in the UI\n //\n // Arguments\n // `key` - the rule to toggle (e.g. 'crossing_ways')\n //\n validator.toggleRule = (key) => {\n if (_disabledRules[key]) {\n delete _disabledRules[key];\n } else {\n _disabledRules[key] = true;\n }\n\n prefs('validate-disabledRules', Object.keys(_disabledRules).join(','));\n validator.validate();\n };\n\n\n // `disableRules()`\n // Disables given validation rules,\n // then reruns the validation so that the user sees something happen in the UI\n //\n // Arguments\n // `keys` - Array or Set containing rule keys to disable\n //\n validator.disableRules = (keys) => {\n _disabledRules = {};\n keys.forEach(k => _disabledRules[k] = true);\n\n prefs('validate-disabledRules', Object.keys(_disabledRules).join(','));\n validator.validate();\n };\n\n\n // `ignoreIssue()`\n // Don't show the given issue in lists\n //\n // Arguments\n // `issueID` - the issueID\n //\n validator.ignoreIssue = (issueID) => {\n _ignoredIssueIDs.add(issueID);\n };\n\n\n // `validate()`\n // Validates anything that has changed in the head graph since the last time it was run.\n // (head graph contains user's edits)\n //\n // Returns\n // A Promise fulfilled when the validation has completed and then dispatches a `validated` event.\n // This may take time but happen in the background during browser idle time.\n //\n validator.validate = () => {\n // Make sure the caches have graphs assigned to them.\n // (we don't do this in `reset` because context is still resetting things and `history.base()` is unstable then)\n const baseGraph = context.history().base();\n if (!_headCache.graph) _headCache.graph = baseGraph;\n if (!_baseCache.graph) _baseCache.graph = baseGraph;\n\n const prevGraph = _headCache.graph;\n const currGraph = context.graph();\n\n if (currGraph === prevGraph) { // _headCache.graph is current - we are caught up\n _headIsCurrent = true;\n dispatch.call('validated');\n return Promise.resolve();\n }\n\n if (_headPromise) { // Validation already in process, but we aren't caught up to current\n _headIsCurrent = false; // We will need to catch up after the validation promise fulfills\n return _headPromise;\n }\n\n // If we get here, its time to start validating stuff.\n _headCache.graph = currGraph; // take snapshot\n _completeDiff = context.history().difference().complete();\n const incrementalDiff = coreDifference(prevGraph, currGraph);\n const diff = Object.keys(incrementalDiff.complete());\n const entityIDs = _headCache.withAllRelatedEntities(diff); // expand set\n\n if (!entityIDs.size) {\n dispatch.call('validated');\n return Promise.resolve();\n }\n\n // revalidate also connected (or previously connected) entities to the current way\n // https://github.com/openstreetmap/iD/issues/8758\n const addConnectedWays = graph => diff\n .filter(entityID => graph.hasEntity(entityID))\n .map(entityID => graph.entity(entityID))\n .flatMap(entity => graph.childNodes(entity))\n .flatMap(vertex => graph.parentWays(vertex))\n .forEach(way => entityIDs.add(way.id));\n addConnectedWays(currGraph);\n addConnectedWays(prevGraph);\n\n // revalidate entities with changed relation memberships\n // https://github.com/openstreetmap/iD/issues/10786\n Object.values({...incrementalDiff.created(), ...incrementalDiff.deleted()})\n .filter(e => e.type === 'relation')\n .flatMap(r => r.members)\n .forEach(m => entityIDs.add(m.id));\n Object.values(incrementalDiff.modified())\n .filter(e => e.type === 'relation')\n .map(r => ({ baseEntity: prevGraph.entity(r.id), headEntity: r }))\n .forEach(({ baseEntity, headEntity }) => {\n const bm = baseEntity.members.map(m => m.id);\n const hm = headEntity.members.map(m => m.id);\n const symDiff = utilArrayDifference(utilArrayUnion(bm, hm), utilArrayIntersection(bm, hm));\n symDiff.forEach(id => entityIDs.add(id));\n });\n\n _headPromise = validateEntitiesAsync(entityIDs, _headCache)\n .then(() => updateResolvedIssues(entityIDs))\n .then(() => dispatch.call('validated'))\n .catch(() => { /* ignore */ })\n .then(() => {\n _headPromise = null;\n if (!_headIsCurrent) {\n validator.validate(); // run it again to catch up to current graph\n }\n });\n\n return _headPromise;\n };\n\n\n // register event handlers:\n\n // WHEN TO RUN VALIDATION:\n // When history changes:\n context.history()\n .on('restore.validator', validator.validate) // on restore saved history\n .on('undone.validator', validator.validate) // on undo\n .on('redone.validator', validator.validate) // on redo\n .on('reset.validator', () => { // on history reset - happens after save, or enter/exit walkthrough\n reset(false); // cached issues aren't valid any longer if the history has been reset\n validator.validate();\n });\n // but not on 'change' (e.g. while drawing)\n\n // When user changes editing modes (to catch recent changes e.g. drawing)\n context\n .on('exit.validator', validator.validate);\n\n // When merging fetched data, validate base graph:\n context.history()\n .on('merge.validator', entities => {\n if (!entities) return;\n\n // Make sure the caches have graphs assigned to them.\n // (we don't do this in `reset` because context is still resetting things and `history.base()` is unstable then)\n const baseGraph = context.history().base();\n if (!_headCache.graph) _headCache.graph = baseGraph;\n if (!_baseCache.graph) _baseCache.graph = baseGraph;\n\n let entityIDs = entities.map(entity => entity.id);\n entityIDs = _baseCache.withAllRelatedEntities(entityIDs); // expand set\n validateEntitiesAsync(entityIDs, _baseCache);\n });\n\n\n\n // `validateEntity()` (private)\n // Runs all validation rules on a single entity.\n // Some things to note:\n // - Graph is passed in from whenever the validation was started. Validators shouldn't use\n // `context.graph()` because this all happens async, and the graph might have changed\n // (for example, nodes getting deleted before the validation can run)\n // - Validator functions may still be waiting on something and return a \"provisional\" result.\n // In this situation, we will schedule to revalidate the entity sometime later.\n //\n // Arguments\n // `entity` - The entity\n // `graph` - graph containing the entity\n //\n // Returns\n // Object result like:\n // {\n // issues: Array of detected issues\n // provisional: `true` if provisional result, `false` if final result\n // }\n //\n function validateEntity(entity, graph) {\n let result = { issues: [], provisional: false };\n Object.keys(_rules).forEach(runValidation); // run all rules\n return result;\n\n\n // runs validation and appends resulting issues\n function runValidation(key) {\n const fn = _rules[key];\n if (typeof fn !== 'function') {\n console.error('no such validation rule = ' + key); // eslint-disable-line no-console\n return;\n }\n\n let detected = fn(entity, graph);\n if (detected.provisional) { // this validation should be run again later\n result.provisional = true;\n }\n detected = detected.filter(applySeverityOverrides);\n result.issues = result.issues.concat(detected);\n\n\n // If there are any override rules that match the issue type/subtype,\n // adjust severity (or disable it) and keep/discard as quickly as possible.\n function applySeverityOverrides(issue) {\n const type = issue.type;\n const subtype = issue.subtype || '';\n let i;\n\n for (i = 0; i < _errorOverrides.length; i++) {\n if (_errorOverrides[i].type.test(type) && _errorOverrides[i].subtype.test(subtype)) {\n issue.severity = 'error';\n return true;\n }\n }\n for (i = 0; i < _warningOverrides.length; i++) {\n if (_warningOverrides[i].type.test(type) && _warningOverrides[i].subtype.test(subtype)) {\n issue.severity = 'warning';\n return true;\n }\n }\n for (i = 0; i < _suggestionOverrides.length; i++) {\n if (_suggestionOverrides[i].type.test(type) && _suggestionOverrides[i].subtype.test(subtype)) {\n issue.severity = 'suggestion';\n return true;\n }\n }\n for (i = 0; i < _disableOverrides.length; i++) {\n if (_disableOverrides[i].type.test(type) && _disableOverrides[i].subtype.test(subtype)) {\n return false;\n }\n }\n return true;\n }\n }\n }\n\n\n // `updateResolvedIssues()` (private)\n // Determine if any issues were resolved for the given entities.\n // This is called by `validate()` after validation of the head graph\n //\n // Give the user credit for fixing an issue if:\n // - the issue is in the base cache\n // - the issue is not in the head cache\n // - the user did something to one of the entities involved in the issue\n //\n // Arguments\n // `entityIDs` - Array or Set containing entity IDs.\n //\n function updateResolvedIssues(entityIDs) {\n entityIDs.forEach(entityID => {\n const baseIssues = _baseCache.issuesByEntityID[entityID];\n if (!baseIssues) return;\n\n baseIssues.forEach(issueID => {\n // Check if the user did something to one of the entities involved in this issue.\n // (This issue could involve multiple entities, e.g. disconnected routable features)\n const issue = _baseCache.issuesByIssueID[issueID];\n const userModified = (issue.entityIds || []).some(id => _completeDiff.hasOwnProperty(id));\n\n if (userModified && !_headCache.issuesByIssueID[issueID]) { // issue seems fixed\n _resolvedIssueIDs.add(issueID);\n } else { // issue still not resolved\n _resolvedIssueIDs.delete(issueID); // (did undo, or possibly fixed and then re-caused the issue)\n }\n });\n });\n }\n\n\n // `validateEntitiesAsync()` (private)\n // Schedule validation for many entities.\n //\n // Arguments\n // `entityIDs` - Array or Set containing entityIDs.\n // `graph` - the graph to validate that contains those entities\n // `cache` - the cache to store results in (_headCache or _baseCache)\n //\n // Returns\n // A Promise fulfilled when the validation has completed.\n // This may take time but happen in the background during browser idle time.\n //\n function validateEntitiesAsync(entityIDs, cache) {\n // Enqueue the work\n const jobs = Array.from(entityIDs).map(entityID => {\n if (cache.queuedEntityIDs.has(entityID)) return null; // queued already\n cache.queuedEntityIDs.add(entityID);\n\n // Clear caches for existing issues related to this entity\n cache.uncacheEntityID(entityID);\n\n return () => {\n cache.queuedEntityIDs.delete(entityID);\n\n const graph = cache.graph;\n if (!graph) return; // was reset?\n\n const entity = graph.hasEntity(entityID); // Sanity check: don't validate deleted entities\n if (!entity) return;\n\n // detect new issues and update caches\n const result = validateEntity(entity, graph);\n if (result.provisional) { // provisional result\n cache.provisionalEntityIDs.add(entityID); // we'll need to revalidate this entity again later\n }\n\n cache.cacheIssues(result.issues); // update cache\n };\n\n }).filter(Boolean);\n\n\n // Perform the work in chunks.\n // Because this will happen during idle callbacks, we want to choose a chunk size\n // that won't make the browser stutter too badly.\n cache.queue = cache.queue.concat(utilArrayChunk(jobs, 100));\n\n // Perform the work\n if (cache.queuePromise) return cache.queuePromise;\n\n cache.queuePromise = processQueue(cache)\n .then(() => revalidateProvisionalEntities(cache))\n .catch(() => { /* ignore */ })\n .finally(() => cache.queuePromise = null);\n\n return cache.queuePromise;\n }\n\n\n // `revalidateProvisionalEntities()` (private)\n // Sometimes a validator will return a \"provisional\" result.\n // In this situation, we'll need to revalidate the entity later.\n // This function waits a delay, then places them back into the validation queue.\n //\n // Arguments\n // `cache` - The cache (_headCache or _baseCache)\n //\n function revalidateProvisionalEntities(cache) {\n if (!cache.provisionalEntityIDs.size) return; // nothing to do\n\n const handle = window.setTimeout(() => {\n _deferredST.delete(handle);\n if (!cache.provisionalEntityIDs.size) return; // nothing to do\n validateEntitiesAsync(Array.from(cache.provisionalEntityIDs), cache);\n }, RETRY);\n\n _deferredST.add(handle);\n }\n\n\n // `processQueue(queue)` (private)\n // Process the next chunk of deferred validation work\n //\n // Arguments\n // `cache` - The cache (_headCache or _baseCache)\n //\n // Returns\n // A Promise fulfilled when the validation has completed.\n // This may take time but happen in the background during browser idle time.\n //\n function processQueue(cache) {\n // console.log(`${cache.which} queue length ${cache.queue.length}`);\n\n if (!cache.queue.length) return Promise.resolve(); // we're done\n const chunk = cache.queue.pop();\n\n return new Promise((resolvePromise, rejectPromise) => {\n const handle = window.requestIdleCallback(() => {\n delete (_deferredRIC[handle]);\n // const t0 = performance.now();\n chunk.forEach(job => job());\n // const t1 = performance.now();\n // console.log('chunk processed in ' + (t1 - t0) + ' ms');\n resolvePromise();\n });\n _deferredRIC[handle] = rejectPromise;\n })\n .then(() => { // dispatch an event sometimes to redraw various UI things\n if (cache.queue.length % 25 === 0) dispatch.call('validated');\n })\n .then(() => processQueue(cache));\n }\n\n\n return utilRebind(validator, dispatch, 'on');\n}\n\n\n// `validationCache()` (private)\n// Creates a cache to store validation state\n// We create 2 of these:\n// `_baseCache` for validation on the base graph (unedited)\n// `_headCache` for validation on the head graph (user edits applied)\n//\n// Arguments\n// `which` - just a String 'base' or 'head' to keep track of it\n//\nfunction validationCache(which) {\n let cache = {\n which: which,\n graph: null,\n queue: [],\n queuePromise: null,\n queuedEntityIDs: new Set(),\n provisionalEntityIDs: new Set(),\n issuesByIssueID: {}, // issue.id -> issue\n issuesByEntityID: {} // entity.id -> Set(issue.id)\n };\n\n\n cache.cacheIssue = (issue) => {\n (issue.entityIds || []).forEach(entityID => {\n if (!cache.issuesByEntityID[entityID]) {\n cache.issuesByEntityID[entityID] = new Set();\n }\n cache.issuesByEntityID[entityID].add(issue.id);\n });\n cache.issuesByIssueID[issue.id] = issue;\n };\n\n\n cache.uncacheIssue = (issue) => {\n (issue.entityIds || []).forEach(entityID => {\n if (cache.issuesByEntityID[entityID]) {\n cache.issuesByEntityID[entityID].delete(issue.id);\n }\n });\n delete cache.issuesByIssueID[issue.id];\n };\n\n\n cache.cacheIssues = (issues) => {\n issues.forEach(cache.cacheIssue);\n };\n\n\n cache.uncacheIssues = (issues) => {\n issues.forEach(cache.uncacheIssue);\n };\n\n\n cache.uncacheIssuesOfType = (type) => {\n const issuesOfType = Object.values(cache.issuesByIssueID)\n .filter(issue => issue.type === type);\n cache.uncacheIssues(issuesOfType);\n };\n\n\n // Remove a single entity and all its related issues from the caches\n cache.uncacheEntityID = (entityID) => {\n const entityIssueIDs = cache.issuesByEntityID[entityID];\n if (entityIssueIDs) {\n entityIssueIDs.forEach(issueID => {\n const issue = cache.issuesByIssueID[issueID];\n if (issue) {\n cache.uncacheIssue(issue);\n } else { // shouldn't happen, clean up\n delete cache.issuesByIssueID[issueID];\n }\n });\n }\n\n delete cache.issuesByEntityID[entityID];\n cache.provisionalEntityIDs.delete(entityID);\n };\n\n\n // Return the expandeded set of entityIDs related to issues for the given entityIDs\n //\n // Arguments\n // `entityIDs` - Array or Set containing entityIDs.\n //\n cache.withAllRelatedEntities = (entityIDs) => {\n let result = new Set();\n (entityIDs || []).forEach(entityID => {\n result.add(entityID); // include self\n\n const entityIssueIDs = cache.issuesByEntityID[entityID];\n if (entityIssueIDs) {\n entityIssueIDs.forEach(issueID => {\n const issue = cache.issuesByIssueID[issueID];\n if (issue) {\n (issue.entityIds || []).forEach(relatedID => result.add(relatedID));\n } else { // shouldn't happen, clean up\n delete cache.issuesByIssueID[issueID];\n }\n });\n }\n });\n\n return result;\n };\n\n\n return cache;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { fileFetcher } from './file_fetcher';\nimport { actionDiscardTags } from '../actions/discard_tags';\nimport { actionMergeRemoteChanges } from '../actions/merge_remote_changes';\nimport { actionNoop } from '../actions/noop';\nimport { actionRevert } from '../actions/revert';\nimport { coreGraph } from '../core/graph';\nimport { t } from '../core/localizer';\nimport { utilArrayUnion, utilArrayUniq, utilDisplayName, utilDisplayType, utilRebind } from '../util';\n\n\n/** @param {iD.Context} context */\nexport function coreUploader(context) {\n\n var dispatch = d3_dispatch(\n // Start and end events are dispatched exactly once each per legitimate outside call to `save`\n 'saveStarted', // dispatched as soon as a call to `save` has been deemed legitimate\n 'saveEnded', // dispatched after the result event has been dispatched\n\n 'willAttemptUpload', // dispatched before the actual upload call occurs, if it will\n 'progressChanged',\n\n // Each save results in one of these outcomes:\n 'resultNoChanges', // upload wasn't attempted since there were no edits\n 'resultErrors', // upload failed due to errors\n 'resultConflicts', // upload failed due to data conflicts\n 'resultSuccess' // upload completed without errors\n );\n\n var _isSaving = false;\n\n let _anyConflictsAutomaticallyResolved = false;\n var _conflicts = [];\n var _errors = [];\n var _origChanges;\n\n var _discardTags = {};\n fileFetcher.get('discarded')\n .then(function(d) { _discardTags = d; })\n .catch(function() { /* ignore */ });\n\n const uploader = {};\n\n uploader.isSaving = function() {\n return _isSaving;\n };\n\n uploader.save = function(changeset, tryAgain, checkConflicts) {\n // Guard against accidentally entering save code twice - #4641\n if (_isSaving && !tryAgain) {\n return;\n }\n\n var osm = context.connection();\n if (!osm) return;\n\n // If user somehow got logged out mid-save, try to reauthenticate..\n // This can happen if they were logged in from before, but the tokens are no longer valid.\n if (!osm.authenticated()) {\n osm.authenticate(function(err) {\n if (!err) {\n uploader.save(changeset, tryAgain, checkConflicts); // continue where we left off..\n }\n });\n return;\n }\n\n if (!_isSaving) {\n _isSaving = true;\n dispatch.call('saveStarted', this);\n }\n\n var history = context.history();\n\n _anyConflictsAutomaticallyResolved = false;\n _conflicts = [];\n _errors = [];\n\n // Store original changes, in case user wants to download them as an .osc file\n _origChanges = history.changes(actionDiscardTags(history.difference(), _discardTags));\n\n // First time, `history.perform` a no-op action.\n // Any conflict resolutions will be done as `history.replace`\n // Remember to pop this later if needed\n if (!tryAgain) {\n history.perform(actionNoop());\n }\n\n // Attempt a fast upload.. If there are conflicts, re-enter with `checkConflicts = true`\n if (!checkConflicts) {\n upload(changeset);\n\n // Do the full (slow) conflict check..\n } else {\n performFullConflictCheck(changeset);\n }\n\n };\n\n\n function performFullConflictCheck(changeset) {\n\n var osm = context.connection();\n if (!osm) return;\n\n var history = context.history();\n\n var localGraph = context.graph();\n var remoteGraph = coreGraph(history.base(), true);\n\n var summary = history.difference().summary();\n var _toCheck = [];\n for (var i = 0; i < summary.length; i++) {\n var item = summary[i];\n if (item.changeType === 'modified') {\n _toCheck.push(item.entity.id);\n }\n }\n\n var _toLoad = withChildNodes(_toCheck, localGraph);\n var _loaded = {};\n var _toLoadCount = 0;\n var _toLoadTotal = _toLoad.length;\n\n if (_toCheck.length) {\n dispatch.call('progressChanged', this, _toLoadCount, _toLoadTotal);\n _toLoad.forEach(function(id) { _loaded[id] = false; });\n osm.loadMultiple(_toLoad, loaded);\n } else {\n upload(changeset);\n }\n\n return;\n\n function withChildNodes(ids, graph) {\n var s = new Set(ids);\n ids.forEach(function(id) {\n var entity = graph.entity(id);\n if (entity.type !== 'way') return;\n\n graph.childNodes(entity).forEach(function(child) {\n if (child.version !== undefined) {\n s.add(child.id);\n }\n });\n });\n\n return Array.from(s);\n }\n\n\n // Reload modified entities into an alternate graph and check for conflicts..\n function loaded(err, result) {\n if (_errors.length) return;\n\n if (err) {\n _errors.push({\n msg: err.message || err.responseText,\n details: [ t('save.status_code', { code: err.status }) ]\n });\n didResultInErrors();\n\n } else {\n var loadMore = [];\n\n result.data.forEach(function(entity) {\n remoteGraph.replace(entity);\n _loaded[entity.id] = true;\n _toLoad = _toLoad.filter(function(val) { return val !== entity.id; });\n\n if (!entity.visible) return;\n\n // Because loadMultiple doesn't download /full like loadEntity,\n // need to also load children that aren't already being checked..\n var i, id;\n if (entity.type === 'way') {\n for (i = 0; i < entity.nodes.length; i++) {\n id = entity.nodes[i];\n if (_loaded[id] === undefined) {\n _loaded[id] = false;\n loadMore.push(id);\n }\n }\n } else if (entity.type === 'relation' && entity.isMultipolygon()) {\n for (i = 0; i < entity.members.length; i++) {\n id = entity.members[i].id;\n if (_loaded[id] === undefined) {\n _loaded[id] = false;\n loadMore.push(id);\n }\n }\n }\n });\n\n _toLoadCount += result.data.length;\n _toLoadTotal += loadMore.length;\n dispatch.call('progressChanged', this, _toLoadCount, _toLoadTotal);\n\n if (loadMore.length) {\n _toLoad.push.apply(_toLoad, loadMore);\n osm.loadMultiple(loadMore, loaded);\n }\n\n if (!_toLoad.length) {\n detectConflicts();\n upload(changeset);\n }\n }\n }\n\n\n function detectConflicts() {\n function choice(id, text, action) {\n return {\n id: id,\n text: text,\n action: function() {\n history.replace(action);\n }\n };\n }\n function formatUser(selection, d) {\n selection\n .append('a')\n .attr('href', osm.userURL(d))\n .attr('target', '_blank')\n .text(d);\n }\n function entityName(entity) {\n return utilDisplayName(entity) || (utilDisplayType(entity.id) + ' ' + entity.id);\n }\n\n function sameVersions(local, remote) {\n if (local.version !== remote.version) return false;\n\n if (local.type === 'way') {\n var children = utilArrayUnion(local.nodes, remote.nodes);\n for (var i = 0; i < children.length; i++) {\n var a = localGraph.hasEntity(children[i]);\n var b = remoteGraph.hasEntity(children[i]);\n if (a && b && a.version !== b.version) return false;\n }\n }\n\n return true;\n }\n\n _toCheck.forEach(function(id) {\n var local = localGraph.entity(id);\n var remote = remoteGraph.entity(id);\n\n if (sameVersions(local, remote)) return;\n\n var merge = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags, formatUser);\n\n history.replace(merge);\n\n var mergeConflicts = merge.conflicts();\n if (!mergeConflicts.length) {\n _anyConflictsAutomaticallyResolved = true;\n return; // merged safely\n }\n\n var forceLocal = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags).withOption('force_local');\n var forceRemote = actionMergeRemoteChanges(id, localGraph, remoteGraph, _discardTags).withOption('force_remote');\n var keepMine = t('save.conflict.' + (remote.visible ? 'keep_local' : 'restore'));\n var keepTheirs = t('save.conflict.' + (remote.visible ? 'keep_remote' : 'delete'));\n\n _conflicts.push({\n id: id,\n name: entityName(local),\n details: mergeConflicts,\n chosen: 1,\n choices: [\n choice(id, keepMine, forceLocal),\n choice(id, keepTheirs, forceRemote)\n ]\n });\n });\n }\n }\n\n\n async function upload(changeset) {\n var osm = context.connection();\n if (!osm) {\n _errors.push({ msg: 'No OSM Service' });\n }\n\n if (_conflicts.length) {\n didResultInConflicts(changeset);\n\n } else if (_errors.length) {\n didResultInErrors();\n\n } else {\n if (_anyConflictsAutomaticallyResolved) {\n // add a changeset tag to aid reviewers\n changeset.tags.merge_conflict_resolved = 'automatically';\n await osm.updateChangesetTags(changeset);\n }\n var history = context.history();\n var changes = history.changes(actionDiscardTags(history.difference(), _discardTags));\n if (changes.modified.length || changes.created.length || changes.deleted.length) {\n\n dispatch.call('willAttemptUpload', this);\n\n osm.putChangeset(changeset, changes, uploadCallback);\n\n } else {\n // changes were insignificant or reverted by user\n didResultInNoChanges();\n }\n }\n }\n\n\n function uploadCallback(err, changeset) {\n if (err) {\n if (err.status === 409) { // 409 Conflict\n uploader.save(changeset, true, true); // tryAgain = true, checkConflicts = true\n } else {\n _errors.push({\n msg: err.message || err.responseText,\n details: [ t('save.status_code', { code: err.status }) ]\n });\n didResultInErrors();\n }\n\n } else {\n didResultInSuccess(changeset);\n }\n }\n\n function didResultInNoChanges() {\n\n dispatch.call('resultNoChanges', this);\n\n endSave();\n\n context.flush(); // reset iD\n }\n\n function didResultInErrors() {\n\n context.history().pop();\n\n dispatch.call('resultErrors', this, _errors);\n\n endSave();\n }\n\n\n function didResultInConflicts(changeset) {\n // add a changeset tag to aid reviewers\n changeset.tags.merge_conflict_resolved = 'manually';\n context.connection().updateChangesetTags(changeset);\n\n _conflicts.sort(function(a, b) { return b.id.localeCompare(a.id); });\n\n dispatch.call('resultConflicts', this, changeset, _conflicts, _origChanges);\n\n endSave();\n }\n\n\n function didResultInSuccess(changeset) {\n\n // delete the edit stack cached to local storage\n context.history().clearSaved();\n\n dispatch.call('resultSuccess', this, changeset);\n\n // Add delay to allow for postgres replication #1646 #2678\n window.setTimeout(function() {\n\n endSave();\n\n context.flush(); // reset iD\n }, 2500);\n }\n\n\n function endSave() {\n _isSaving = false;\n\n dispatch.call('saveEnded', this);\n }\n\n\n uploader.cancelConflictResolution = function() {\n context.history().pop();\n };\n\n\n uploader.processResolvedConflicts = function(changeset) {\n var history = context.history();\n\n for (var i = 0; i < _conflicts.length; i++) {\n if (_conflicts[i].chosen === 1) { // user chose \"use theirs\"\n var entity = context.hasEntity(_conflicts[i].id);\n if (entity && entity.type === 'way') {\n var children = utilArrayUniq(entity.nodes);\n for (var j = 0; j < children.length; j++) {\n history.replace(actionRevert(children[j]));\n }\n }\n history.replace(actionRevert(_conflicts[i].id));\n }\n }\n\n uploader.save(changeset, true, false); // tryAgain = true, checkConflicts = false\n };\n\n\n uploader.reset = function() {\n\n };\n\n\n return utilRebind(uploader, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDrawWay } from '../behavior/draw_way';\n\n\nexport function modeDrawArea(context, wayID, startGraph, button) {\n var mode = {\n button: button,\n id: 'draw-area'\n };\n\n var behavior = behaviorDrawWay(context, wayID, mode, startGraph)\n .on('rejectedSelfIntersection.modeDrawArea', function() {\n context.ui().flash\n .iconName('#iD-icon-no')\n .label(t.append('self_intersection.error.areas'))();\n });\n\n mode.wayID = wayID;\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n mode.selectedIDs = function() {\n return [wayID];\n };\n\n mode.activeID = function() {\n return (behavior && behavior.activeID()) || [];\n };\n\n return mode;\n}\n", "import { actionAddEntity } from '../actions/add_entity';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionAddVertex } from '../actions/add_vertex';\n\nimport { behaviorAddWay } from '../behavior/add_way';\nimport { modeDrawArea } from './draw_area';\nimport { osmNode, osmWay } from '../osm';\n\n\nexport function modeAddArea(context, mode) {\n mode.id = 'add-area';\n\n var behavior = behaviorAddWay(context)\n .on('start', start)\n .on('startFromWay', startFromWay)\n .on('startFromNode', startFromNode);\n\n function defaultTags(loc) {\n var defaultTags = { area: 'yes' };\n if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'area', false, loc);\n return defaultTags;\n }\n\n function actionClose(wayId) {\n return function (graph) {\n return graph.replace(graph.entity(wayId).close());\n };\n }\n\n\n function start(loc) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionClose(way.id)\n );\n\n context.enter(modeDrawArea(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromWay(loc, edge) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionClose(way.id),\n actionAddMidpoint({ loc: loc, edge: edge }, node)\n );\n\n context.enter(modeDrawArea(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromNode(node) {\n var startGraph = context.graph();\n var way = osmWay({ tags: defaultTags(node.loc) });\n\n context.perform(\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionClose(way.id)\n );\n\n context.enter(modeDrawArea(context, way.id, startGraph, mode.button));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n\n return mode;\n}\n", "import { actionAddEntity } from '../actions/add_entity';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionAddVertex } from '../actions/add_vertex';\n\nimport { behaviorAddWay } from '../behavior/add_way';\nimport { modeDrawLine } from './draw_line';\nimport { osmNode, osmWay } from '../osm';\n\n\nexport function modeAddLine(context, mode) {\n mode.id = 'add-line';\n\n var behavior = behaviorAddWay(context)\n .on('start', start)\n .on('startFromWay', startFromWay)\n .on('startFromNode', startFromNode);\n\n function defaultTags(loc) {\n var defaultTags = {};\n if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'line', false, loc);\n return defaultTags;\n }\n\n\n function start(loc) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id)\n );\n\n context.enter(modeDrawLine(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromWay(loc, edge) {\n var startGraph = context.graph();\n var node = osmNode({ loc: loc });\n var way = osmWay({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n actionAddEntity(way),\n actionAddVertex(way.id, node.id),\n actionAddMidpoint({ loc: loc, edge: edge }, node)\n );\n\n context.enter(modeDrawLine(context, way.id, startGraph, mode.button));\n }\n\n\n function startFromNode(node) {\n var startGraph = context.graph();\n var way = osmWay({ tags: defaultTags(node.loc) });\n\n context.perform(\n actionAddEntity(way),\n actionAddVertex(way.id, node.id)\n );\n\n context.enter(modeDrawLine(context, way.id, startGraph, mode.button));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDraw } from '../behavior/draw';\nimport { modeBrowse } from './browse';\nimport { modeSelect } from './select';\nimport { osmNode } from '../osm/node';\nimport { actionAddEntity } from '../actions/add_entity';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { actionAddMidpoint } from '../actions/add_midpoint';\n\n\nexport function modeAddPoint(context, mode) {\n\n mode.id = 'add-point';\n\n var behavior = behaviorDraw(context)\n .on('click', add)\n .on('clickWay', addWay)\n .on('clickNode', addNode)\n .on('cancel', cancel)\n .on('finish', cancel);\n\n function defaultTags(loc) {\n var defaultTags = {};\n if (mode.preset) defaultTags = mode.preset.setTags(defaultTags, 'point', false, loc);\n return defaultTags;\n }\n\n\n function add(loc) {\n var node = osmNode({ loc: loc, tags: defaultTags(loc) });\n\n context.perform(\n actionAddEntity(node),\n t('operations.add.annotation.point')\n );\n\n enterSelectMode(node);\n }\n\n\n function addWay(loc, edge) {\n var node = osmNode({ tags: defaultTags(loc) });\n\n context.perform(\n actionAddMidpoint({loc: loc, edge: edge}, node),\n t('operations.add.annotation.vertex')\n );\n\n enterSelectMode(node);\n }\n\n function enterSelectMode(node) {\n context.enter(\n modeSelect(context, [node.id]).newFeature(true)\n );\n }\n\n\n function addNode(node) {\n const _defaultTags = defaultTags(node.loc);\n if (Object.keys(_defaultTags).length === 0) {\n enterSelectMode(node);\n return;\n }\n\n var tags = Object.assign({}, node.tags); // shallow copy\n for (var key in _defaultTags) {\n tags[key] = _defaultTags[key];\n }\n\n context.perform(\n actionChangeTags(node.id, tags),\n t('operations.add.annotation.point')\n );\n\n enterSelectMode(node);\n }\n\n\n function cancel() {\n context.enter(modeBrowse(context));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n\n return mode;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select,\n selection as d3_selection\n} from 'd3-selection';\n\nimport { geoVecLength } from '../geo';\nimport { osmNote } from '../osm';\nimport { utilRebind } from '../util/rebind';\nimport { utilFastMouse, utilPrefixCSSProperty, utilPrefixDOMProperty } from '../util';\n\n\n/*\n `behaviorDrag` is like `d3_behavior.drag`, with the following differences:\n\n * The `origin` function is expected to return an [x, y] tuple rather than an\n {x, y} object.\n * The events are `start`, `move`, and `end`.\n (https://github.com/mbostock/d3/issues/563)\n * The `start` event is not dispatched until the first cursor movement occurs.\n (https://github.com/mbostock/d3/pull/368)\n * The `move` event has a `point` and `delta` [x, y] tuple properties rather\n than `x`, `y`, `dx`, and `dy` properties.\n * The `end` event is not dispatched if no movement occurs.\n * An `off` function is available that unbinds the drag's internal event handlers.\n */\n\nexport function behaviorDrag() {\n var dispatch = d3_dispatch('start', 'move', 'end');\n\n // see also behaviorSelect\n var _tolerancePx = 1; // keep this low to facilitate pixel-perfect micromapping\n var _penTolerancePx = 4; // styluses can be touchy so require greater movement - #1981\n\n var _origin = null;\n var _selector = '';\n var _targetNode;\n var _targetEntity;\n var _surface;\n var _pointerId;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n var d3_event_userSelectProperty = utilPrefixCSSProperty('UserSelect');\n var d3_event_userSelectSuppress = function() {\n var selection = d3_selection();\n var select = selection.style(d3_event_userSelectProperty);\n selection.style(d3_event_userSelectProperty, 'none');\n return function() {\n selection.style(d3_event_userSelectProperty, select);\n };\n };\n\n\n function pointerdown(d3_event) {\n\n if (_pointerId) return;\n\n _pointerId = d3_event.pointerId || 'mouse';\n\n _targetNode = this;\n\n // only force reflow once per drag\n var pointerLocGetter = utilFastMouse(_surface || _targetNode.parentNode);\n\n var offset;\n var startOrigin = pointerLocGetter(d3_event);\n var started = false;\n var selectEnable = d3_event_userSelectSuppress();\n\n d3_select(window)\n .on(_pointerPrefix + 'move.drag', pointermove)\n .on(_pointerPrefix + 'up.drag pointercancel.drag', pointerup, true);\n\n if (_origin) {\n offset = _origin.call(_targetNode, _targetEntity);\n offset = [offset[0] - startOrigin[0], offset[1] - startOrigin[1]];\n } else {\n offset = [0, 0];\n }\n\n d3_event.stopPropagation();\n\n\n function pointermove(d3_event) {\n if (_pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n var p = pointerLocGetter(d3_event);\n\n if (!started) {\n var dist = geoVecLength(startOrigin, p);\n var tolerance = d3_event.pointerType === 'pen' ? _penTolerancePx : _tolerancePx;\n // don't start until the drag has actually moved somewhat\n if (dist < tolerance) return;\n\n started = true;\n dispatch.call('start', this, d3_event, _targetEntity);\n\n // Don't send a `move` event in the same cycle as `start` since dragging\n // a midpoint will convert the target to a node.\n } else {\n\n startOrigin = p;\n d3_event.stopPropagation();\n d3_event.preventDefault();\n\n var dx = p[0] - startOrigin[0];\n var dy = p[1] - startOrigin[1];\n dispatch.call('move', this, d3_event, _targetEntity, [p[0] + offset[0], p[1] + offset[1]], [dx, dy]);\n }\n }\n\n\n function pointerup(d3_event) {\n if (_pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n _pointerId = null;\n\n if (started) {\n dispatch.call('end', this, d3_event, _targetEntity);\n\n d3_event.preventDefault();\n }\n\n d3_select(window)\n .on(_pointerPrefix + 'move.drag', null)\n .on(_pointerPrefix + 'up.drag pointercancel.drag', null);\n\n selectEnable();\n }\n }\n\n\n function behavior(selection) {\n var matchesSelector = utilPrefixDOMProperty('matchesSelector');\n var delegate = pointerdown;\n\n if (_selector) {\n delegate = function(d3_event) {\n var root = this;\n var target = d3_event.target;\n for (; target && target !== root; target = target.parentNode) {\n var datum = target.__data__;\n\n _targetEntity = datum instanceof osmNote ? datum\n : datum && datum.properties && datum.properties.entity;\n\n if (_targetEntity && target[matchesSelector](_selector)) {\n return pointerdown.call(target, d3_event);\n }\n }\n };\n }\n\n selection\n .on(_pointerPrefix + 'down.drag' + _selector, delegate);\n }\n\n\n behavior.off = function(selection) {\n selection\n .on(_pointerPrefix + 'down.drag' + _selector, null);\n };\n\n\n behavior.selector = function(_) {\n if (!arguments.length) return _selector;\n _selector = _;\n return behavior;\n };\n\n\n behavior.origin = function(_) {\n if (!arguments.length) return _origin;\n _origin = _;\n return behavior;\n };\n\n\n behavior.cancel = function() {\n d3_select(window)\n .on(_pointerPrefix + 'move.drag', null)\n .on(_pointerPrefix + 'up.drag pointercancel.drag', null);\n return behavior;\n };\n\n\n behavior.targetNode = function(_) {\n if (!arguments.length) return _targetNode;\n _targetNode = _;\n return behavior;\n };\n\n\n behavior.targetEntity = function(_) {\n if (!arguments.length) return _targetEntity;\n _targetEntity = _;\n return behavior;\n };\n\n\n behavior.surface = function(_) {\n if (!arguments.length) return _surface;\n _surface = _;\n return behavior;\n };\n\n\n return utilRebind(behavior, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { t } from '../core/localizer';\n\nimport { actionAddMidpoint } from '../actions/add_midpoint';\nimport { actionConnect } from '../actions/connect';\nimport { actionMoveNode } from '../actions/move_node';\nimport { actionNoop } from '../actions/noop';\n\nimport { behaviorDrag } from '../behavior/drag';\nimport { behaviorEdit } from '../behavior/edit';\nimport { behaviorHover } from '../behavior/hover';\n\nimport {\n geoChooseEdge,\n geoHasLineIntersections,\n geoHasSelfIntersections,\n geoVecSubtract,\n geoViewportEdge\n} from '../geo';\n\nimport { modeBrowse } from './browse';\nimport { modeSelect } from './select';\nimport { osmJoinWays, osmNode } from '../osm';\nimport { utilArrayIntersection, utilKeybinding } from '../util';\n\n\n\nexport function modeDragNode(context) {\n var mode = {\n id: 'drag-node',\n button: 'browse'\n };\n var hover = behaviorHover(context).altDisables(true)\n .on('hover', context.ui().sidebar.hover);\n var edit = behaviorEdit(context);\n\n var _nudgeInterval;\n var _restoreSelectedIDs = [];\n var _wasMidpoint = false;\n var _isCancelled = false;\n var _activeEntity;\n var _startLoc;\n var _lastLoc;\n\n\n function startNudge(d3_event, entity, nudge) {\n if (_nudgeInterval) window.clearInterval(_nudgeInterval);\n _nudgeInterval = window.setInterval(function() {\n context.map().pan(nudge);\n doMove(d3_event, entity, nudge);\n }, 50);\n }\n\n\n function stopNudge() {\n if (_nudgeInterval) {\n window.clearInterval(_nudgeInterval);\n _nudgeInterval = null;\n }\n }\n\n\n function moveAnnotation(entity) {\n return t('operations.move.annotation.' + entity.geometry(context.graph()));\n }\n\n\n function connectAnnotation(nodeEntity, targetEntity) {\n var nodeGeometry = nodeEntity.geometry(context.graph());\n var targetGeometry = targetEntity.geometry(context.graph());\n if (nodeGeometry === 'vertex' && targetGeometry === 'vertex') {\n var nodeParentWayIDs = context.graph().parentWays(nodeEntity);\n var targetParentWayIDs = context.graph().parentWays(targetEntity);\n var sharedParentWays = utilArrayIntersection(nodeParentWayIDs, targetParentWayIDs);\n // if both vertices are part of the same way\n if (sharedParentWays.length !== 0) {\n // if the nodes are next to each other, they are merged\n if (sharedParentWays[0].areAdjacent(nodeEntity.id, targetEntity.id)) {\n return t('operations.connect.annotation.from_vertex.to_adjacent_vertex');\n }\n return t('operations.connect.annotation.from_vertex.to_sibling_vertex');\n }\n }\n return t('operations.connect.annotation.from_' + nodeGeometry + '.to_' + targetGeometry);\n }\n\n\n function shouldSnapToNode(target) {\n if (!_activeEntity) return false;\n return _activeEntity.geometry(context.graph()) !== 'vertex' ||\n (target.geometry(context.graph()) === 'vertex' || presetManager.allowsVertex(target, context.graph()));\n }\n\n\n function origin(entity) {\n return context.projection(entity.loc);\n }\n\n\n function keydown(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope')) {\n context.surface()\n .classed('nope-suppressed', true);\n }\n context.surface()\n .classed('nope', false)\n .classed('nope-disabled', true);\n }\n }\n\n\n function keyup(d3_event) {\n if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {\n if (context.surface().classed('nope-suppressed')) {\n context.surface()\n .classed('nope', true);\n }\n context.surface()\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false);\n }\n }\n\n\n function start(d3_event, entity) {\n _wasMidpoint = entity.type === 'midpoint';\n var hasHidden = context.features().hasHiddenConnections(entity, context.graph());\n _isCancelled = !context.editable() || d3_event.shiftKey || hasHidden;\n\n\n if (_isCancelled) {\n if (hasHidden) {\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('modes.drag_node.connected_to_hidden'))();\n }\n return drag.cancel();\n }\n\n if (_wasMidpoint) {\n var midpoint = entity;\n entity = osmNode();\n context.perform(actionAddMidpoint(midpoint, entity));\n entity = context.entity(entity.id); // get post-action entity\n\n var vertex = context.surface().selectAll('.' + entity.id);\n drag.targetNode(vertex.node())\n .targetEntity(entity);\n\n } else {\n context.perform(actionNoop());\n }\n\n _activeEntity = entity;\n _startLoc = entity.loc;\n\n hover.ignoreVertex(entity.geometry(context.graph()) === 'vertex');\n\n context.surface().selectAll('.' + _activeEntity.id)\n .classed('active', true);\n\n context.enter(mode);\n }\n\n\n // related code\n // - `behavior/draw.js` `datum()`\n function datum(d3_event) {\n if (!d3_event || d3_event.altKey) {\n return {};\n } else {\n // When dragging, snap only to touch targets..\n // (this excludes area fills and active drawing elements)\n var d = d3_event.target.__data__;\n return (d && d.properties && d.properties.target) ? d : {};\n }\n }\n\n\n function doMove(d3_event, entity, nudge) {\n nudge = nudge || [0, 0];\n\n var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc);\n var currMouse = geoVecSubtract(currPoint, nudge);\n var loc = context.projection.invert(currMouse);\n\n var target, edge;\n\n if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap..\n // related code\n // - `mode/drag_node.js` `doMove()`\n // - `behavior/draw.js` `click()`\n // - `behavior/draw_way.js` `move()`\n var d = datum(d3_event);\n target = d && d.properties && d.properties.entity;\n var targetLoc = target && target.loc;\n var targetNodes = d && d.properties && d.properties.nodes;\n\n if (targetLoc) { // snap to node/vertex - a point target with `.loc`\n if (shouldSnapToNode(target)) {\n loc = targetLoc;\n }\n\n } else if (targetNodes) { // snap to way - a line target with `.nodes`\n edge = geoChooseEdge(targetNodes, context.map().mouse(), context.projection, end.id);\n if (edge) {\n loc = edge.loc;\n }\n }\n }\n\n context.replace(\n actionMoveNode(entity.id, loc)\n );\n\n // Below here: validations\n var isInvalid = false;\n\n // Check if this connection to `target` could cause relations to break..\n if (target) {\n isInvalid = hasRelationConflict(entity, target, edge, context.graph());\n }\n\n // Check if this drag causes the geometry to break..\n if (!isInvalid) {\n isInvalid = hasInvalidGeometry(entity, context.graph());\n }\n\n\n var nope = context.surface().classed('nope');\n if (isInvalid === 'relation' || isInvalid === 'restriction') {\n if (!nope) { // about to nope - show hint\n context.ui().flash\n .duration(4000)\n .iconName('#iD-icon-no')\n .label(t.append('operations.connect.' + isInvalid,\n { relation: presetManager.item('type/restriction').name() }\n ))();\n }\n } else if (isInvalid) {\n var errorID = isInvalid === 'line' ? 'lines' : 'areas';\n context.ui().flash\n .duration(3000)\n .iconName('#iD-icon-no')\n .label(t.append('self_intersection.error.' + errorID))();\n } else {\n if (nope) { // about to un-nope, remove hint\n context.ui().flash\n .duration(1)\n .label('')();\n }\n }\n\n\n var nopeDisabled = context.surface().classed('nope-disabled');\n if (nopeDisabled) {\n context.surface()\n .classed('nope', false)\n .classed('nope-suppressed', isInvalid);\n } else {\n context.surface()\n .classed('nope', isInvalid)\n .classed('nope-suppressed', false);\n }\n\n _lastLoc = loc;\n }\n\n\n // Uses `actionConnect.disabled()` to know whether this connection is ok..\n function hasRelationConflict(entity, target, edge, graph) {\n var testGraph = graph.update(); // copy\n\n // if snapping to way - add midpoint there and consider that the target..\n if (edge) {\n var midpoint = osmNode();\n var action = actionAddMidpoint({\n loc: edge.loc,\n edge: [target.nodes[edge.index - 1], target.nodes[edge.index]]\n }, midpoint);\n\n testGraph = action(testGraph);\n target = midpoint;\n }\n\n // can we connect to it?\n var ids = [entity.id, target.id];\n return actionConnect(ids).disabled(testGraph);\n }\n\n\n function hasInvalidGeometry(entity, graph) {\n var parents = graph.parentWays(entity);\n var i, j, k;\n\n for (i = 0; i < parents.length; i++) {\n var parent = parents[i];\n var activeIndex = null; // which multipolygon ring contains node being dragged\n\n // test any parent multipolygons for valid geometry\n var relations = graph.parentRelations(parent);\n for (j = 0; j < relations.length; j++) {\n if (!relations[j].isMultipolygon()) continue;\n\n var rings = osmJoinWays(relations[j].members, graph);\n\n // find active ring and test it for self intersections\n for (k = 0; k < rings.length; k++) {\n const nodes = rings[k].nodes;\n if (nodes.find(function(n) { return n.id === entity.id; })) {\n activeIndex = k;\n if (geoHasSelfIntersections(nodes, entity.id)) {\n return 'multipolygonMember';\n }\n }\n rings[k].coords = nodes.map(function(n) { return n.loc; });\n }\n\n // test active ring for intersections with other rings in the multipolygon\n for (k = 0; k < rings.length; k++) {\n if (k === activeIndex) continue;\n\n // make sure active ring doesn't cross passive rings\n if (geoHasLineIntersections(rings[activeIndex].nodes, rings[k].nodes, entity.id)) {\n return 'multipolygonRing';\n }\n }\n }\n\n\n // If we still haven't tested this node's parent way for self-intersections.\n // (because it's not a member of a multipolygon), test it now.\n if (activeIndex === null) {\n const nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); });\n if (nodes.length && geoHasSelfIntersections(nodes, entity.id)) {\n return parent.geometry(graph);\n }\n }\n\n }\n\n return false;\n }\n\n\n function move(d3_event, entity, point) {\n if (_isCancelled) return;\n d3_event.stopPropagation();\n\n context.surface().classed('nope-disabled', d3_event.altKey);\n\n _lastLoc = context.projection.invert(point);\n\n doMove(d3_event, entity);\n var nudge = geoViewportEdge(point, context.map().dimensions());\n if (nudge) {\n startNudge(d3_event, entity, nudge);\n } else {\n stopNudge();\n }\n }\n\n function end(d3_event, entity) {\n if (_isCancelled) return;\n\n var wasPoint = entity.geometry(context.graph()) === 'point';\n\n var d = datum(d3_event);\n var nope = (d && d.properties && d.properties.nope) || context.surface().classed('nope');\n var target = d && d.properties && d.properties.entity; // entity to snap to\n\n if (nope) { // bounce back\n context.perform(\n _actionBounceBack(entity.id, _startLoc)\n );\n\n } else if (target && target.type === 'way') {\n var choice = geoChooseEdge(context.graph().childNodes(target), context.map().mouse(), context.projection, entity.id);\n context.replace(\n actionAddMidpoint({\n loc: choice.loc,\n edge: [target.nodes[choice.index - 1], target.nodes[choice.index]]\n }, entity),\n connectAnnotation(entity, target)\n );\n\n } else if (target && target.type === 'node' && shouldSnapToNode(target)) {\n context.replace(\n actionConnect([target.id, entity.id]),\n connectAnnotation(entity, target)\n );\n\n } else if (_wasMidpoint) {\n context.replace(\n actionNoop(),\n t('operations.add.annotation.vertex')\n );\n\n } else {\n context.replace(\n actionNoop(),\n moveAnnotation(entity)\n );\n }\n\n if (wasPoint) {\n context.enter(modeSelect(context, [entity.id]));\n\n } else {\n var reselection = _restoreSelectedIDs.filter(function(id) {\n return context.graph().hasEntity(id);\n });\n\n if (reselection.length) {\n context.enter(modeSelect(context, reselection));\n } else {\n context.enter(modeBrowse(context));\n }\n }\n }\n\n\n function _actionBounceBack(nodeID, toLoc) {\n var moveNode = actionMoveNode(nodeID, toLoc);\n var action = function(graph, t) {\n // last time through, pop off the bounceback perform.\n // it will then overwrite the initial perform with a moveNode that does nothing\n if (t === 1) context.pop();\n return moveNode(graph, t);\n };\n action.transitionable = true;\n return action;\n }\n\n\n function cancel() {\n drag.cancel();\n context.enter(modeBrowse(context));\n }\n\n\n var drag = behaviorDrag()\n .selector('.layer-touch.points .target')\n .surface(context.container().select('.main-map').node())\n .origin(origin)\n .on('start', start)\n .on('move', move)\n .on('end', end);\n\n\n mode.enter = function() {\n context.install(hover);\n context.install(edit);\n\n d3_select(window)\n .on('keydown.dragNode', keydown)\n .on('keyup.dragNode', keyup);\n\n context.history()\n .on('undone.drag-node', cancel);\n };\n\n\n mode.exit = function() {\n context.ui().sidebar.hover.cancel();\n context.uninstall(hover);\n context.uninstall(edit);\n\n d3_select(window)\n .on('keydown.dragNode', null)\n .on('keyup.dragNode', null);\n\n context.history()\n .on('undone.drag-node', null);\n\n _activeEntity = null;\n\n context.surface()\n .classed('nope', false)\n .classed('nope-suppressed', false)\n .classed('nope-disabled', false)\n .selectAll('.active')\n .classed('active', false);\n\n stopNudge();\n };\n\n\n mode.selectedIDs = function() {\n if (!arguments.length) return _activeEntity ? [_activeEntity.id] : [];\n // no assign\n return mode;\n };\n\n\n mode.activeID = function() {\n if (!arguments.length) return _activeEntity && _activeEntity.id;\n // no assign\n return mode;\n };\n\n\n mode.restoreSelectedIDs = function(_) {\n if (!arguments.length) return _restoreSelectedIDs;\n _restoreSelectedIDs = _;\n return mode;\n };\n\n\n mode.behavior = drag;\n\n\n return mode;\n}\n", "import { services } from '../services';\nimport { actionNoop } from '../actions/noop';\nimport { behaviorDrag } from '../behavior/drag';\nimport { behaviorEdit } from '../behavior/edit';\nimport { geoVecSubtract, geoViewportEdge } from '../geo';\nimport { modeSelectNote } from './select_note';\n\n\nexport function modeDragNote(context) {\n var mode = {\n id: 'drag-note',\n button: 'browse'\n };\n\n var edit = behaviorEdit(context);\n\n var _nudgeInterval;\n var _lastLoc;\n var _note; // most current note.. dragged note may have stale datum.\n\n\n function startNudge(d3_event, nudge) {\n if (_nudgeInterval) window.clearInterval(_nudgeInterval);\n _nudgeInterval = window.setInterval(function() {\n context.map().pan(nudge);\n doMove(d3_event, nudge);\n }, 50);\n }\n\n\n function stopNudge() {\n if (_nudgeInterval) {\n window.clearInterval(_nudgeInterval);\n _nudgeInterval = null;\n }\n }\n\n\n function origin(note) {\n return context.projection(note.loc);\n }\n\n\n function start(d3_event, note) {\n _note = note;\n var osm = services.osm;\n if (osm) {\n // Get latest note from cache.. The marker may have a stale datum bound to it\n // and dragging it around can sometimes delete the users note comment.\n _note = osm.getNote(_note.id);\n }\n\n context.surface().selectAll('.note-' + _note.id)\n .classed('active', true);\n\n context.perform(actionNoop());\n context.enter(mode);\n context.selectedNoteID(_note.id);\n }\n\n\n function move(d3_event, entity, point) {\n d3_event.stopPropagation();\n _lastLoc = context.projection.invert(point);\n\n doMove(d3_event);\n var nudge = geoViewportEdge(point, context.map().dimensions());\n if (nudge) {\n startNudge(d3_event, nudge);\n } else {\n stopNudge();\n }\n }\n\n\n function doMove(d3_event, nudge) {\n nudge = nudge || [0, 0];\n\n var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc);\n var currMouse = geoVecSubtract(currPoint, nudge);\n var loc = context.projection.invert(currMouse);\n\n _note = _note.move(loc);\n\n var osm = services.osm;\n if (osm) {\n osm.replaceNote(_note); // update note cache\n }\n\n context.replace(actionNoop()); // trigger redraw\n }\n\n\n function end() {\n context.replace(actionNoop()); // trigger redraw\n\n context\n .selectedNoteID(_note.id)\n .enter(modeSelectNote(context, _note.id));\n }\n\n\n var drag = behaviorDrag()\n .selector('.layer-touch.markers .target.note.new')\n .surface(context.container().select('.main-map').node())\n .origin(origin)\n .on('start', start)\n .on('move', move)\n .on('end', end);\n\n\n mode.enter = function() {\n context.install(edit);\n };\n\n\n mode.exit = function() {\n context.ui().sidebar.hover.cancel();\n context.uninstall(edit);\n\n context.surface()\n .selectAll('.active')\n .classed('active', false);\n\n stopNudge();\n };\n\n mode.behavior = drag;\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiDataHeader() {\n var _datum;\n\n\n function dataHeader(selection) {\n var header = selection.selectAll('.data-header')\n .data(\n (_datum ? [_datum] : []),\n function(d) { return d.__featurehash__; }\n );\n\n header.exit()\n .remove();\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'data-header');\n\n var iconEnter = headerEnter\n .append('div')\n .attr('class', 'data-header-icon');\n\n iconEnter\n .append('div')\n .attr('class', 'preset-icon-28')\n .call(svgIcon('#iD-icon-data', 'note-fill'));\n\n headerEnter\n .append('div')\n .attr('class', 'data-header-label')\n .call(t.append('map_data.layers.custom.title'));\n }\n\n\n dataHeader.datum = function(val) {\n if (!arguments.length) return _datum;\n _datum = val;\n return this;\n };\n\n\n return dataHeader;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { utilGetSetValue, utilRebind, utilTriggerEvent } from '../util';\n\n\n// This code assumes that the combobox values will not have duplicate entries.\n// It is keyed on the `value` of the entry. Data should be an array of objects like:\n// [{\n// value: 'string value', // required\n// display: 'label function' // optional, if present will be called with d3 selection\n// to modify/append, see localizer's t.append\n// title: 'hover text' // optional\n// terms: ['search terms'] // optional\n// }, ...]\n\nvar _comboHideTimerID;\n\nexport function uiCombobox(context, klass) {\n var dispatch = d3_dispatch('accept', 'cancel', 'update');\n var container = context.container();\n\n var _suggestions = [];\n var _data = [];\n var _fetched = {};\n var _selected = null;\n var _canAutocomplete = true;\n var _caseSensitive = false;\n var _cancelFetch = false;\n var _minItems = 2;\n var _tDown = 0;\n var _mouseEnterHandler, _mouseLeaveHandler;\n\n var _fetcher = function(val, cb) {\n cb(_data.filter(function(d) {\n var terms = d.terms || [];\n terms.push(d.value);\n if (d.key) {\n terms.push(d.key);\n }\n return terms.some(function(term) {\n return term\n .toString()\n .toLowerCase()\n .indexOf(val.toLowerCase()) !== -1;\n });\n }));\n };\n\n var combobox = function(input, attachTo) {\n if (!input || input.empty()) return;\n\n input\n .classed('combobox-input', true)\n .on('focus.combo-input', focus)\n .on('blur.combo-input', blur)\n .on('keydown.combo-input', keydown)\n .on('keyup.combo-input', keyup)\n .on('input.combo-input', change)\n .on('mousedown.combo-input', mousedown)\n .each(function() {\n var parent = this.parentNode;\n var sibling = this.nextSibling;\n\n d3_select(parent).selectAll('.combobox-caret')\n .filter(function(d) { return d === input.node(); })\n .data([input.node()])\n .enter()\n .insert('div', function() { return sibling; })\n .attr('class', 'combobox-caret')\n .on('mousedown.combo-caret', function(d3_event) {\n d3_event.preventDefault(); // don't steal focus from input\n input.node().focus(); // focus the input as if it was clicked\n mousedown(d3_event);\n })\n .on('mouseup.combo-caret', function(d3_event) {\n d3_event.preventDefault(); // don't steal focus from input\n mouseup(d3_event);\n });\n });\n\n\n function mousedown(d3_event) {\n if (d3_event.button !== 0) return; // left click only\n if (input.classed('disabled')) return;\n _tDown = +new Date();\n\n // mousedown should never bubble up (see #10481)\n d3_event.stopPropagation();\n\n // clear selection\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n if (start !== end) {\n var val = utilGetSetValue(input);\n input.node().setSelectionRange(val.length, val.length);\n return;\n }\n\n input.on('mouseup.combo-input', mouseup);\n }\n\n\n function mouseup(d3_event) {\n input.on('mouseup.combo-input', null);\n if (d3_event.button !== 0) return; // left click only\n if (input.classed('disabled')) return;\n if (input.node() !== document.activeElement) return; // exit if this input is not focused\n\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n if (start !== end) return; // exit if user is selecting\n\n // not showing or showing for a different field - try to show it.\n var combo = container.selectAll('.combobox');\n if (combo.empty() || combo.datum() !== input.node()) {\n var tOrig = _tDown;\n window.setTimeout(function() {\n if (tOrig !== _tDown) return; // exit if user double clicked\n fetchComboData('', function() {\n show();\n render();\n });\n }, 250);\n\n } else {\n hide();\n }\n }\n\n\n function focus() {\n fetchComboData(''); // prefetch values (may warm taginfo cache)\n }\n\n\n function blur() {\n _comboHideTimerID = window.setTimeout(hide, 75);\n }\n\n\n function show() {\n hide(); // remove any existing\n\n container\n .insert('div', ':first-child')\n .datum(input.node())\n .attr('class', 'combobox' + (klass ? ' combobox-' + klass : ''))\n .style('position', 'absolute')\n .style('display', 'block')\n .style('left', '0px')\n .on('mousedown.combo-container', function (d3_event) {\n // prevent moving focus out of the input field\n d3_event.preventDefault();\n });\n\n container\n .on('scroll.combo-scroll', render, true);\n }\n\n function hide() {\n _hide(container);\n }\n\n\n function keydown(d3_event) {\n var shown = !container.selectAll('.combobox').empty();\n var tagName = input.node() ? input.node().tagName.toLowerCase() : '';\n\n switch (d3_event.keyCode) {\n case 8: // \u232B Backspace\n case 46: // \u2326 Delete\n d3_event.stopPropagation();\n _selected = null;\n render();\n input.on('input.combo-input', function() {\n var start = input.property('selectionStart');\n input.node().setSelectionRange(start, start);\n input.on('input.combo-input', change); // reset event handler\n change(false);\n });\n break;\n\n case 9: // \u21E5 Tab\n accept(d3_event);\n break;\n\n case 13: // \u21A9 Return\n d3_event.preventDefault();\n d3_event.stopPropagation();\n accept(d3_event);\n break;\n\n case 38: // \u2191 Up arrow\n if (tagName === 'textarea' && !shown) return;\n d3_event.preventDefault();\n if (tagName === 'input' && !shown) {\n show();\n }\n nav(-1);\n break;\n\n case 40: // \u2193 Down arrow\n if (tagName === 'textarea' && !shown) return;\n d3_event.preventDefault();\n if (tagName === 'input' && !shown) {\n show();\n }\n nav(+1);\n break;\n }\n }\n\n\n function keyup(d3_event) {\n switch (d3_event.keyCode) {\n case 27: // \u238B Escape\n cancel();\n break;\n }\n }\n\n\n // Called whenever the input value is changed (e.g. on typing)\n function change(doAutoComplete) {\n if (doAutoComplete === undefined) doAutoComplete = true;\n fetchComboData(value(), function(skipAutosuggest) {\n _selected = null;\n var val = input.property('value');\n\n if (_suggestions.length) {\n if (doAutoComplete && !skipAutosuggest && input.property('selectionEnd') === val.length) {\n _selected = tryAutocomplete();\n }\n\n if (!_selected) {\n _selected = val;\n }\n }\n\n if (val.length) {\n var combo = container.selectAll('.combobox');\n if (combo.empty()) {\n show();\n }\n } else {\n hide();\n }\n\n render();\n });\n }\n\n\n // Called when the user presses up/down arrows to navigate the list\n function nav(dir) {\n if (_suggestions.length) {\n // try to determine previously selected index..\n var index = -1;\n for (var i = 0; i < _suggestions.length; i++) {\n if (_selected && _suggestions[i].value === _selected) {\n index = i;\n break;\n }\n }\n\n // pick new _selected\n index = Math.max(Math.min(index + dir, _suggestions.length - 1), 0);\n _selected = _suggestions[index].value;\n utilGetSetValue(input, _selected);\n dispatch.call('update');\n }\n\n render();\n ensureVisible();\n }\n\n\n function ensureVisible() {\n var combo = container.selectAll('.combobox');\n if (combo.empty()) return;\n\n var containerRect = container.node().getBoundingClientRect();\n var comboRect = combo.node().getBoundingClientRect();\n\n if (comboRect.bottom > containerRect.bottom) {\n var node = attachTo ? attachTo.node() : input.node();\n node.scrollIntoView({ behavior: 'instant', block: 'center' });\n render();\n }\n\n // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move\n var selected = combo.selectAll('.combobox-option.selected').node();\n if (selected) {\n selected.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' });\n }\n }\n\n\n function value() {\n var value = input.property('value');\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n\n if (start && end) {\n value = value.substring(0, start);\n }\n\n return value;\n }\n\n\n function fetchComboData(v, cb) {\n _cancelFetch = false;\n\n _fetcher.call(input, v, function(results, skipAutosuggest) {\n // already chose a value, don't overwrite or autocomplete it\n if (_cancelFetch) return;\n\n _suggestions = results;\n results.forEach(function(d) { _fetched[d.value] = d; });\n\n if (cb) {\n cb(skipAutosuggest);\n }\n });\n }\n\n\n function tryAutocomplete() {\n if (!_canAutocomplete) return;\n\n var val = _caseSensitive ? value() : value().toLowerCase();\n if (!val) return;\n\n // Don't autocomplete if user is typing a number - #4935\n if (isFinite(val)) return;\n\n const suggestionValues = [];\n _suggestions.forEach(s => {\n suggestionValues.push(s.value);\n if (s.key && s.key !== s.value) {\n suggestionValues.push(s.key);\n }\n });\n\n var bestIndex = -1;\n for (var i = 0; i < suggestionValues.length; i++) {\n var suggestion = suggestionValues[i];\n var compare = _caseSensitive ? suggestion : suggestion.toLowerCase();\n\n // if search string matches suggestion exactly, pick it..\n if (compare === val) {\n bestIndex = i;\n break;\n\n // otherwise lock in the first result that starts with the search string..\n } else if (bestIndex === -1 && compare.indexOf(val) === 0) {\n bestIndex = i;\n }\n }\n\n if (bestIndex !== -1) {\n var bestVal = suggestionValues[bestIndex];\n input.property('value', bestVal);\n input.node().setSelectionRange(val.length, bestVal.length);\n dispatch.call('update');\n return bestVal;\n }\n }\n\n\n function render() {\n if (_suggestions.length < _minItems || document.activeElement !== input.node()) {\n hide();\n return;\n }\n\n var shown = !container.selectAll('.combobox').empty();\n if (!shown) return;\n\n var combo = container.selectAll('.combobox');\n var options = combo.selectAll('.combobox-option')\n .data(_suggestions, function(d) { return d.value; });\n\n options.exit()\n .remove();\n\n // enter/update\n const enter = options.enter()\n .append('a')\n .attr('class', function(d) {\n return 'combobox-option ' + (d.klass || '') + (d.description ? ' has-description' : '');\n })\n .attr('title', function(d) { return d.title; });\n\n enter.each(function(d) {\n const sel = d3_select(this);\n const labelSpan = sel.append('span')\n .attr('class', 'combobox-option-label');\n if (d.display) {\n d.display(labelSpan);\n } else {\n labelSpan.text(d.value);\n }\n if (d.description) {\n sel.append('span')\n .attr('class', 'combobox-option-description')\n .text(d.description);\n }\n });\n\n enter\n .on('mouseenter', _mouseEnterHandler)\n .on('mouseleave', _mouseLeaveHandler)\n .merge(options)\n .classed('selected', function(d) { return d.value === _selected || d.key === _selected; })\n .on('click.combo-option', accept)\n .order();\n\n var node = attachTo ? attachTo.node() : input.node();\n var containerRect = container.node().getBoundingClientRect();\n var rect = node.getBoundingClientRect();\n\n combo\n .style('left', (rect.left + 5 - containerRect.left) + 'px')\n .style('width', (rect.width - 10) + 'px')\n .style('top', (rect.height + rect.top - containerRect.top) + 'px');\n }\n\n\n // Dispatches an 'accept' event\n // Then hides the combobox.\n function accept(d3_event, d) {\n _cancelFetch = true;\n var thiz = input.node();\n\n if (d) { // user clicked on a suggestion\n utilGetSetValue(input, d.value); // replace field contents\n utilTriggerEvent(input, 'change');\n }\n\n // clear (and keep) selection\n var val = utilGetSetValue(input);\n thiz.setSelectionRange(val.length, val.length);\n\n if (!d) {\n d = _fetched[val];\n }\n dispatch.call('accept', thiz, d, val);\n hide();\n }\n\n\n // Dispatches an 'cancel' event\n // Then hides the combobox.\n function cancel() {\n _cancelFetch = true;\n var thiz = input.node();\n\n // clear (and remove) selection, and replace field contents\n var val = utilGetSetValue(input);\n var start = input.property('selectionStart');\n var end = input.property('selectionEnd');\n val = val.slice(0, start) + val.slice(end);\n utilGetSetValue(input, val);\n thiz.setSelectionRange(val.length, val.length);\n\n dispatch.call('cancel', thiz);\n\n hide();\n }\n\n };\n\n\n combobox.canAutocomplete = function(val) {\n if (!arguments.length) return _canAutocomplete;\n _canAutocomplete = val;\n return combobox;\n };\n\n combobox.caseSensitive = function(val) {\n if (!arguments.length) return _caseSensitive;\n _caseSensitive = val;\n return combobox;\n };\n\n combobox.data = function(val) {\n if (!arguments.length) return _data;\n _data = val;\n return combobox;\n };\n\n combobox.fetcher = function(val) {\n if (!arguments.length) return _fetcher;\n _fetcher = val;\n return combobox;\n };\n\n combobox.minItems = function(val) {\n if (!arguments.length) return _minItems;\n _minItems = val;\n return combobox;\n };\n\n combobox.itemsMouseEnter = function(val) {\n if (!arguments.length) return _mouseEnterHandler;\n _mouseEnterHandler = val;\n return combobox;\n };\n\n combobox.itemsMouseLeave = function(val) {\n if (!arguments.length) return _mouseLeaveHandler;\n _mouseLeaveHandler = val;\n return combobox;\n };\n\n return utilRebind(combobox, dispatch, 'on');\n}\n\n\nfunction _hide(container) {\n if (_comboHideTimerID) {\n window.clearTimeout(_comboHideTimerID);\n _comboHideTimerID = undefined;\n }\n\n container.selectAll('.combobox')\n .remove();\n\n container\n .on('scroll.combo-scroll', null);\n}\n\n\nuiCombobox.off = function(input, context) {\n _hide(context.container());\n input\n .on('focus.combo-input', null)\n .on('blur.combo-input', null)\n .on('keydown.combo-input', null)\n .on('keyup.combo-input', null)\n .on('input.combo-input', null)\n .on('mousedown.combo-input', null)\n .on('mouseup.combo-input', null);\n\n\n context.container()\n .on('scroll.combo-scroll', null);\n};\n", "import { select as d3_select } from 'd3-selection';\n\n\n// toggles the visibility of ui elements, using a combination of the\n// hide class, which sets display=none, and a d3 transition for opacity.\n// this will cause blinking when called repeatedly, so check that the\n// value actually changes between calls.\n//\n// When the selection is a direct child of a

    element, the\n// parent's `open` property is used instead of the `hide` class.\nexport function uiToggle(show, callback) {\n return function(selection) {\n const parent = selection.node().parentNode;\n const isDetails = parent && parent.tagName === 'DETAILS';\n\n // ensure content is visible before animating\n if (isDetails) {\n if (show) parent.open = true;\n } else {\n selection.classed('hide', false);\n }\n\n selection\n .style('opacity', show ? 0 : 1)\n .transition()\n .style('opacity', show ? 1 : 0)\n .on('end', function() {\n d3_select(this).style('opacity', null);\n // hide content after fade-out completes\n if (isDetails) {\n if (!show) parent.open = false;\n } else {\n d3_select(this).classed('hide', !show);\n }\n if (callback) callback.apply(this);\n });\n };\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from '../core/preferences';\nimport { svgIcon } from '../svg/icon';\nimport { utilFunctor } from '../util';\nimport { utilRebind } from '../util/rebind';\nimport { uiToggle } from './toggle';\nimport { t, localizer } from '../core/localizer';\n\n\nexport function uiDisclosure(context, key, expandedDefault) {\n const dispatch = d3_dispatch('toggled');\n let _expanded;\n let _label = utilFunctor('');\n let _updatePreference = true;\n let _content = function () {};\n\n\n const disclosure = function(selection) {\n\n if (_expanded === undefined || _expanded === null) {\n // loading _expanded here allows it to be reset by calling `disclosure.expanded(null)`\n\n const preference = prefs('disclosure.' + key + '.expanded');\n _expanded = preference === null ? !!expandedDefault : (preference === 'true');\n }\n\n let details = selection.selectAll('.disclosure-wrap-' + key)\n .data([0]);\n\n // enter\n const detailsEnter = details.enter()\n .append('details')\n .attr('class', 'disclosure-wrap disclosure-wrap-' + key);\n\n const summaryEnter = detailsEnter\n .append('summary')\n .attr('class', 'hide-toggle hide-toggle-' + key)\n .call(svgIcon('', 'pre-text', 'hide-toggle-icon'));\n\n summaryEnter\n .append('span')\n .attr('class', 'hide-toggle-text');\n\n detailsEnter\n .append('div')\n .attr('class', 'disclosure-content');\n\n // update\n details = detailsEnter\n .merge(details);\n\n details\n .property('open', _expanded);\n\n const summary = details.selectAll('summary.hide-toggle');\n\n summary\n .on('click', toggle);\n\n updateSummary();\n\n const label = _label();\n const labelSelection = summary.selectAll('.hide-toggle-text');\n if (typeof label !== 'function') {\n labelSelection.text(label);\n } else {\n labelSelection.text('').call(label);\n }\n\n const contentWrap = details.selectAll('.disclosure-content');\n\n if (_expanded) {\n contentWrap\n .call(_content);\n }\n\n\n function updateSummary() {\n summary\n .classed('expanded', _expanded)\n .attr('title', t(`icons.${_expanded ? 'collapse' : 'expand'}`));\n\n summary.selectAll('.hide-toggle-icon')\n .attr('xlink:href', _expanded ? '#iD-icon-down'\n : (localizer.textDirection() === 'rtl') ? '#iD-icon-backward' : '#iD-icon-forward'\n );\n }\n\n\n function toggle(d3_event) {\n d3_event.preventDefault();\n\n _expanded = !_expanded;\n\n if (_updatePreference) {\n prefs('disclosure.' + key + '.expanded', _expanded);\n }\n\n updateSummary();\n\n contentWrap.call(uiToggle(_expanded));\n\n if (_expanded) {\n contentWrap.call(_content);\n }\n\n dispatch.call('toggled', this, _expanded);\n }\n };\n\n\n disclosure.label = function(val) {\n if (!arguments.length) return _label;\n _label = utilFunctor(val);\n return disclosure;\n };\n\n\n disclosure.expanded = function(val) {\n if (!arguments.length) return _expanded;\n _expanded = val;\n return disclosure;\n };\n\n\n disclosure.updatePreference = function(val) {\n if (!arguments.length) return _updatePreference;\n _updatePreference = val;\n return disclosure;\n };\n\n\n disclosure.content = function(val) {\n if (!arguments.length) return _content;\n _content = val;\n return disclosure;\n };\n\n\n return utilRebind(disclosure, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { uiDisclosure } from './disclosure';\nimport { utilFunctor } from '../util';\n\n// A unit of controls or info to be used in a layout, such as within a pane.\n// Can be labeled and collapsible.\nexport function uiSection(id, context) {\n\n var _classes = utilFunctor('');\n var _shouldDisplay;\n var _content;\n\n var _disclosure;\n var _label;\n var _expandedByDefault = utilFunctor(true);\n var _disclosureContent;\n var _disclosureExpanded;\n\n var _containerSelection = d3_select(null);\n\n var section = {\n id: id\n };\n\n section.classes = function(val) {\n if (!arguments.length) return _classes;\n _classes = utilFunctor(val);\n return section;\n };\n\n section.label = function(val) {\n if (!arguments.length) return _label;\n _label = utilFunctor(val);\n return section;\n };\n\n section.expandedByDefault = function(val) {\n if (!arguments.length) return _expandedByDefault;\n _expandedByDefault = utilFunctor(val);\n return section;\n };\n\n section.shouldDisplay = function(val) {\n if (!arguments.length) return _shouldDisplay;\n _shouldDisplay = utilFunctor(val);\n return section;\n };\n\n section.content = function(val) {\n if (!arguments.length) return _content;\n _content = val;\n return section;\n };\n\n section.disclosureContent = function(val) {\n if (!arguments.length) return _disclosureContent;\n _disclosureContent = val;\n return section;\n };\n\n section.disclosureExpanded = function(val) {\n if (!arguments.length) return _disclosureExpanded;\n _disclosureExpanded = val;\n return section;\n };\n\n // may be called multiple times\n section.render = function(selection) {\n\n _containerSelection = selection\n .selectAll('.section-' + id)\n .data([0]);\n\n var sectionEnter = _containerSelection\n .enter()\n .append('div')\n .attr('class', 'section section-' + id + ' ' + (_classes && _classes() || ''));\n\n _containerSelection = sectionEnter\n .merge(_containerSelection);\n\n _containerSelection\n .call(renderContent);\n };\n\n section.reRender = function() {\n _containerSelection\n .call(renderContent);\n };\n\n section.selection = function() {\n return _containerSelection;\n };\n\n section.disclosure = function() {\n return _disclosure;\n };\n\n // may be called multiple times\n function renderContent(selection) {\n if (_shouldDisplay) {\n var shouldDisplay = _shouldDisplay();\n selection.classed('hide', !shouldDisplay);\n if (!shouldDisplay) {\n selection.html('');\n return;\n }\n }\n\n if (_disclosureContent) {\n if (!_disclosure) {\n _disclosure = uiDisclosure(context, id.replace(/-/g, '_'), _expandedByDefault())\n .label(_label || '')\n /*.on('toggled', function(expanded) {\n if (expanded) { selection.node().parentNode.scrollTop += 200; }\n })*/\n .content(_disclosureContent);\n }\n if (_disclosureExpanded !== undefined) {\n _disclosure.expanded(_disclosureExpanded);\n _disclosureExpanded = undefined;\n }\n selection\n .call(_disclosure);\n\n return;\n }\n\n if (_content) {\n selection\n .call(_content);\n }\n }\n\n return section;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\n\n\n// Pass `what` object of the form:\n// {\n// key: 'string', // required\n// value: 'string' // optional\n// }\n// -or-\n// {\n// qid: 'string' // brand wikidata (e.g. 'Q37158')\n// }\n//\nexport function uiTagReference(what) {\n var wikibase = what.qid ? services.wikidata : services.osmWikibase;\n var tagReference = {};\n\n var _button = d3_select(null);\n var _body = d3_select(null);\n var _loaded;\n var _showing;\n\n\n function load() {\n if (!wikibase) return;\n\n _button\n .classed('tag-reference-loading', true);\n\n wikibase.getDocs(what, gotDocs);\n }\n\n\n function gotDocs(err, docs) {\n _body.html('');\n\n if (!docs || !docs.title) {\n _body\n .append('p')\n .attr('class', 'tag-reference-description')\n .call(t.append('inspector.no_documentation_key'));\n done();\n return;\n }\n\n if (docs.imageURL) {\n _body\n .append('img')\n .attr('class', 'tag-reference-wiki-image')\n .attr('alt', docs.title)\n .attr('src', docs.imageURL)\n .on('load', function() { done(); })\n .on('error', function() { d3_select(this).remove(); done(); });\n } else {\n done();\n }\n\n var tagReferenceDescription = _body\n .append('p')\n .attr('class', 'tag-reference-description')\n .append('span');\n if (docs.description) {\n tagReferenceDescription = tagReferenceDescription\n .attr('class', 'localized-text')\n .attr('lang', docs.descriptionLocaleCode || 'und')\n .call(docs.description);\n } else {\n tagReferenceDescription = tagReferenceDescription\n .call(t.append('inspector.no_documentation_key'));\n }\n tagReferenceDescription\n .append('a')\n .attr('class', 'tag-reference-edit')\n .attr('target', '_blank')\n .attr('title', t('inspector.edit_reference'))\n .attr('href', docs.editURL)\n .call(svgIcon('#iD-icon-edit', 'inline'));\n\n if (docs.wiki) {\n _body\n .append('a')\n .attr('class', 'tag-reference-link')\n .attr('target', '_blank')\n .attr('href', docs.wiki.url)\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append(docs.wiki.text));\n }\n\n // Add link to info about \"good changeset comments\" - #2923\n if (what.key === 'comment') {\n _body\n .append('a')\n .attr('class', 'tag-reference-comment-link')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', t('commit.about_changeset_comments_link'))\n .append('span')\n .call(t.append('commit.about_changeset_comments'));\n }\n }\n\n\n function done() {\n _loaded = true;\n\n _button\n .classed('tag-reference-loading', false);\n\n _body\n .classed('expanded', true)\n .transition()\n .duration(200)\n .style('max-height', '200px')\n .style('opacity', '1');\n\n _showing = true;\n\n _button.selectAll('svg.icon use').each(function() {\n var iconUse = d3_select(this);\n if (iconUse.attr('href') === '#iD-icon-info') {\n iconUse.attr('href', '#iD-icon-info-filled');\n }\n });\n }\n\n\n function hide() {\n _body\n .transition()\n .duration(200)\n .style('max-height', '0px')\n .style('opacity', '0')\n .on('end', function () {\n _body.classed('expanded', false);\n });\n\n _showing = false;\n\n _button.selectAll('svg.icon use').each(function() {\n var iconUse = d3_select(this);\n if (iconUse.attr('href') === '#iD-icon-info-filled') {\n iconUse.attr('href', '#iD-icon-info');\n }\n });\n\n }\n\n\n tagReference.button = function(selection, klass, iconName) {\n _button = selection.selectAll('.tag-reference-button')\n .data([0]);\n\n _button = _button.enter()\n .append('button')\n .attr('class', 'tag-reference-button ' + (klass || ''))\n .attr('title', t('icons.information'))\n .call(svgIcon('#iD-icon-' + (iconName || 'inspect')))\n .merge(_button);\n\n _button\n .on('click', function (d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n this.blur(); // avoid keeping focus on the button - #4641\n if (_showing) {\n hide();\n } else if (_loaded) {\n done();\n } else {\n load();\n }\n });\n };\n\n\n tagReference.body = function(selection) {\n var itemID = what.qid || (what.key + '-' + (what.value || ''));\n _body = selection.selectAll('.tag-reference-body')\n .data([itemID], function(d) { return d; });\n\n _body.exit()\n .remove();\n\n _body = _body.enter()\n .append('div')\n .attr('class', 'tag-reference-body')\n .style('max-height', '0')\n .style('opacity', '0')\n .merge(_body);\n\n if (_showing === false) {\n hide();\n }\n };\n\n\n tagReference.showing = function(val) {\n if (!arguments.length) return _showing;\n _showing = val;\n return tagReference;\n };\n\n\n return tagReference;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { isEmpty } from 'es-toolkit/compat';\n\nimport { services } from '../../services';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCombobox } from '../combobox';\nimport { uiSection } from '../section';\nimport { uiTagReference } from '../tag_reference';\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { utilArrayDifference, utilArrayIdentical } from '../../util/array';\nimport { utilGetSetValue, utilNoAuto, utilRebind, utilTagDiff } from '../../util';\nimport { allowUpperCaseTagValues } from '../../osm/tags';\nimport { fileFetcher } from '../../core';\n\n\nexport function uiSectionRawTagEditor(id, context) {\n\n var section = uiSection(id, context)\n .classes('raw-tag-editor')\n .label(function() {\n var count = Object.keys(_tags).filter(function(d) { return d; }).length;\n return t.append('inspector.title_count', { title: t.append('inspector.tags'), count: count });\n })\n .expandedByDefault(false)\n .disclosureContent(renderDisclosureContent);\n\n var taginfo = services.taginfo;\n var dispatch = d3_dispatch('change');\n var availableViews = [\n { id: 'list', icon: '#fas-th-list' },\n { id: 'text', icon: '#fas-i-cursor' }\n ];\n\n let _discardTags = {};\n fileFetcher.get('discarded')\n .then((d) => { _discardTags = d; })\n .catch(() => { /* ignore */ });\n\n var _tagView = (prefs('raw-tag-editor-view') || 'list'); // 'list, 'text'\n var _readOnlyTags = [];\n // the keys in the order we want them to display\n var _orderedKeys = [];\n var _pendingChange = null;\n var _state;\n var _presets;\n var _tags;\n var _entityIDs;\n var _didInteract = false;\n\n function interacted() {\n _didInteract = true;\n }\n\n function renderDisclosureContent(wrap) {\n\n // remove deleted keys\n _orderedKeys = _orderedKeys.filter(function(key) {\n return _tags[key] !== undefined;\n });\n\n // When switching to a different entity or changing the state (hover/select)\n // reorder the keys alphabetically.\n // We trigger this by emptying the `_orderedKeys` array, then it will be rebuilt here.\n // Otherwise leave their order alone - #5857, #5927\n var all = Object.keys(_tags).sort();\n var missingKeys = utilArrayDifference(all, _orderedKeys);\n for (var i in missingKeys) {\n _orderedKeys.push(missingKeys[i]);\n }\n\n // assemble row data\n var rowData = _orderedKeys.map(function(key, i) {\n return { index: i, key: key, value: _tags[key] };\n });\n\n // append blank row last\n rowData.push({ index: rowData.length, key: '', value: '' });\n\n\n // View Options\n var options = wrap.selectAll('.raw-tag-options')\n .data([0]);\n\n options.exit()\n .remove();\n\n var optionsEnter = options.enter()\n .insert('div', ':first-child')\n .attr('class', 'raw-tag-options')\n .attr('role', 'tablist');\n\n var optionEnter = optionsEnter.selectAll('.raw-tag-option')\n .data(availableViews, function(d) { return d.id; })\n .enter();\n\n optionEnter\n .append('button')\n .attr('class', function(d) {\n return 'raw-tag-option raw-tag-option-' + d.id + (_tagView === d.id ? ' selected' : '');\n })\n .attr('aria-selected', function(d) { return _tagView === d.id; })\n .attr('role', 'tab')\n .attr('title', function(d) { return t('icons.' + d.id); })\n .on('click', function(d3_event, d) {\n _tagView = d.id;\n prefs('raw-tag-editor-view', d.id);\n\n wrap.selectAll('.raw-tag-option')\n .classed('selected', function(datum) { return datum === d; })\n .attr('aria-selected', function(datum) { return datum === d; });\n\n wrap.selectAll('.tag-text')\n .classed('hide', (d.id !== 'text'))\n .each(setTextareaHeight);\n\n wrap.selectAll('.tag-list, .add-row')\n .classed('hide', (d.id !== 'list'));\n })\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(d.icon));\n });\n\n\n // View as Text\n var textData = rowsToText(rowData);\n var textarea = wrap.selectAll('.tag-text')\n .data([0]);\n\n textarea = textarea.enter()\n .append('textarea')\n .attr('class', 'tag-text' + (_tagView !== 'text' ? ' hide' : ''))\n .call(utilNoAuto)\n .attr('placeholder', t('inspector.key_value'))\n .attr('spellcheck', 'false')\n .style('direction', 'ltr')\n .merge(textarea);\n\n textarea\n .call(utilGetSetValue, textData)\n .each(setTextareaHeight)\n .on('input', setTextareaHeight)\n .on('focus', interacted)\n .on('blur', textChanged)\n .on('change', textChanged);\n\n\n // View as List\n var list = wrap.selectAll('.tag-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'tag-list' + (_tagView !== 'list' ? ' hide' : ''))\n .merge(list);\n\n\n // Tag list items\n var items = list.selectAll('.tag-row')\n .data(rowData, d => d.key);\n\n items.exit()\n .each(unbind)\n .remove();\n\n\n // Enter\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', 'tag-row')\n .classed('readonly', isReadOnly);\n\n var innerWrap = itemsEnter.append('div')\n .attr('class', 'inner-wrap');\n\n innerWrap\n .append('div')\n .attr('class', 'key-wrap')\n .append('input')\n .property('type', 'text')\n .attr('class', 'key')\n .call(utilNoAuto)\n .on('focus', interacted)\n .on('blur', keyChange)\n .on('change', keyChange);\n\n innerWrap\n .append('div')\n .attr('class', 'value-wrap')\n .append('input')\n .property('type', 'text')\n .attr('dir', 'auto')\n .attr('class', 'value')\n .call(utilNoAuto)\n .on('focus', interacted)\n .on('blur', valueChange)\n .on('change', valueChange);\n\n innerWrap\n .append('button')\n .attr('tabindex', -1)\n .attr('class', 'form-field-button remove')\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'));\n\n\n // Update\n items = items\n .merge(itemsEnter)\n .sort(function(a, b) { return a.index - b.index; });\n\n items\n .classed('add-tag', d => d.key === '')\n .each(function(d) {\n var row = d3_select(this);\n var key = row.select('input.key'); // propagate bound data\n var value = row.select('input.value'); // propagate bound data\n\n if (_entityIDs && taginfo && _state !== 'hover') {\n bindTypeahead(key, value);\n }\n\n var referenceOptions = { key: d.key };\n if (typeof d.value === 'string') {\n referenceOptions.value = d.value;\n }\n var reference = uiTagReference(referenceOptions, context);\n\n if (_state === 'hover') {\n reference.showing(false);\n }\n\n row.select('.inner-wrap') // propagate bound data\n .call(reference.button)\n .select('.tag-reference-button')\n .attr('tabindex', -1)\n .classed('disabled', d => d.key === '')\n .attr('disabled', d => d.key === '' ? 'disabled' : null);\n\n row.call(reference.body);\n\n row.select('button.remove'); // propagate bound data\n });\n\n items.selectAll('input.key')\n .attr('title', function(d) { return d.key; })\n .attr('placeholder', function(d) {\n return d.key === '' ? t('inspector.add_tag') : null;\n })\n .attr('readonly', function(d) {\n return isReadOnly(d) || null;\n })\n .call(utilGetSetValue,\n d => d.key,\n (_, newKey) => _pendingChange === null || isEmpty(_pendingChange) || _pendingChange[newKey] // if there are pending changes: skip untouched tags\n );\n\n items.selectAll('input.value')\n .attr('title', function(d) {\n return Array.isArray(d.value) ? d.value.filter(Boolean).join('\\n') : d.value;\n })\n .classed('mixed', function(d) {\n return Array.isArray(d.value);\n })\n .attr('placeholder', function(d) {\n return Array.isArray(d.value) ? t('inspector.multiple_values') : null;\n })\n .attr('readonly', function(d) {\n return isReadOnly(d) || null;\n })\n .call(utilGetSetValue, d => {\n if (_pendingChange !== null && !isEmpty(_pendingChange) && !_pendingChange[d.key]) {\n // if there are pending changes: skip untouched tags\n return null;\n }\n return Array.isArray(d.value) ? '' : d.value;\n });\n\n items.selectAll('button.remove')\n .classed('disabled', d => d.key === '')\n .on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'down', // 'click' fires too late - #5878\n (d3_event, d) => {\n if (d3_event.button !== 0) return;\n removeTag(d3_event, d);\n });\n\n }\n\n function isReadOnly(d) {\n for (var i = 0; i < _readOnlyTags.length; i++) {\n if (d.key.match(_readOnlyTags[i]) !== null) {\n return true;\n }\n }\n return false;\n }\n\n function setTextareaHeight() {\n if (_tagView !== 'text') return;\n\n var selection = d3_select(this);\n var matches = selection.node().value.match(/\\n/g);\n var lineCount = 2 + Number(matches && matches.length);\n var lineHeight = 20;\n\n selection.style('height', lineCount * lineHeight + 'px');\n }\n\n function stringify(s) {\n const stringified = JSON.stringify(s).slice(1, -1); // without leading/trailing \"\n if (stringified !== s) {\n return `\"${stringified}\"`;\n } else {\n return s;\n }\n }\n\n function unstringify(s) {\n const isQuoted = s.length > 1 && s.charAt(0) === '\"' && s.charAt(s.length - 1) === '\"';\n if (isQuoted) {\n try {\n return JSON.parse(s);\n } catch {\n return s;\n }\n } else {\n return s;\n }\n }\n\n function rowsToText(rows) {\n var str = rows\n .filter(function(row) { return row.key && row.key.trim() !== ''; })\n .map(function(row) {\n var rawVal = row.value;\n if (Array.isArray(rawVal)) rawVal = '*';\n var val = rawVal ? stringify(rawVal) : '';\n return stringify(row.key) + '=' + val;\n })\n .join('\\n');\n\n if (_state !== 'hover' && str.length) {\n return str + '\\n';\n }\n return str;\n }\n\n function textChanged() {\n var newText = this.value.trim();\n var newTags = {};\n newText.split('\\n').forEach(function(row) {\n var m = row.match(/^\\s*([^=]+)=(.*)$/);\n if (m !== null) {\n var k = context.cleanTagKey(unstringify(m[1].trim()));\n var v = context.cleanTagValue(unstringify(m[2].trim()));\n newTags[k] = v;\n }\n });\n\n var tagDiff = utilTagDiff(_tags, newTags);\n\n _pendingChange = _pendingChange || {};\n\n tagDiff.forEach(function(change) {\n if (isReadOnly({ key: change.key })) return;\n\n // skip unchanged multiselection placeholders\n if (change.newVal === '*' && Array.isArray(change.oldVal)) return;\n\n if (change.type === '-') {\n _pendingChange[change.key] = undefined;\n } else if (change.type === '+') {\n _pendingChange[change.key] = change.newVal || '';\n }\n });\n\n if (isEmpty(_pendingChange)) {\n _pendingChange = null;\n section.reRender();\n return;\n }\n\n scheduleChange();\n }\n\n function bindTypeahead(key, value) {\n if (isReadOnly(key.datum())) return;\n\n if (Array.isArray(value.datum().value)) {\n value.call(uiCombobox(context, 'tag-value')\n .minItems(1)\n .fetcher(function(value, callback) {\n var keyString = utilGetSetValue(key);\n if (!_tags[keyString]) return;\n var data = _tags[keyString].map(function(tagValue) {\n if (!tagValue) {\n return {\n value: ' ',\n title: t('inspector.empty'),\n display: selection => selection.text('')\n .classed('virtual-option', true)\n .call(t.append('inspector.empty'))\n };\n }\n return {\n value: tagValue,\n title: tagValue\n };\n });\n callback(data);\n }));\n return;\n }\n\n var geometry = context.graph().geometry(_entityIDs[0]);\n\n key.call(uiCombobox(context, 'tag-key')\n .fetcher(function(value, callback) {\n taginfo.keys({\n debounce: true,\n geometry: geometry,\n query: value\n }, function(err, data) {\n if (!err) {\n const filtered = data\n .filter(d => _tags[d.value] === undefined) // already used tag\n .filter(d => !(d.value in _discardTags)) // do not suggest discardable tags (see #9817)\n .filter(d => !/_\\d$/.test(d.value)) // tag like name_1 (see #9422)\n .filter(d => d.value.toLowerCase().includes(value.toLowerCase())); // tag does not match user input\n callback(sort(value, filtered));\n }\n });\n }));\n\n value.call(uiCombobox(context, 'tag-value')\n .fetcher(function(value, callback) {\n taginfo.values({\n debounce: true,\n key: utilGetSetValue(key),\n geometry: geometry,\n query: value\n }, function(err, data) {\n if (!err) {\n const filtered = data.filter(d => d.value.toLowerCase().includes(value.toLowerCase()));\n callback(sort(value, filtered));\n }\n });\n })\n .caseSensitive(allowUpperCaseTagValues.test(utilGetSetValue(key))));\n\n\n function sort(value, data) {\n var sameletter = [];\n var other = [];\n for (var i = 0; i < data.length; i++) {\n if (data[i].value.substring(0, value.length) === value) {\n sameletter.push(data[i]);\n } else {\n other.push(data[i]);\n }\n }\n return sameletter.concat(other);\n }\n }\n\n function unbind() {\n var row = d3_select(this);\n\n row.selectAll('input.key')\n .call(uiCombobox.off, context);\n\n row.selectAll('input.value')\n .call(uiCombobox.off, context);\n }\n\n function keyChange(d3_event, d) {\n const input = d3_select(this);\n if (input.attr('readonly')) return;\n\n var kOld = d.key;\n\n // exit if we are currently about to delete this row anyway - #6366\n if (_pendingChange && _pendingChange.hasOwnProperty(kOld) && _pendingChange[kOld] === undefined) return;\n\n var kNew = context.cleanTagKey(this.value.trim());\n\n // allow no change if the key should be readonly\n if (isReadOnly({ key: kNew })) {\n this.value = kOld;\n return;\n }\n\n if (kNew !== this.value) {\n utilGetSetValue(input, kNew);\n }\n\n if (kNew &&\n kNew !== kOld &&\n _tags[kNew] !== undefined) {\n // new key is already in use, switch focus to the existing row\n\n this.value = kOld; // reset the key\n section.selection().selectAll('.tag-list input.value')\n .each(function(d) {\n if (d.key === kNew) { // send focus to that other value combo instead\n var input = d3_select(this).node();\n input.focus();\n input.select();\n }\n });\n return;\n }\n\n\n _pendingChange = _pendingChange || {};\n\n if (kOld) {\n if (kOld === kNew) return;\n // a tag key was renamed\n _pendingChange[kNew] = _pendingChange[kOld] || { oldKey: kOld };\n _pendingChange[kOld] = undefined;\n } else {\n // a new tag was added\n let row = this.parentNode.parentNode;\n let inputVal = d3_select(row).selectAll('input.value');\n let vNew = context.cleanTagValue(utilGetSetValue(inputVal));\n _pendingChange[kNew] = vNew;\n utilGetSetValue(inputVal, vNew);\n }\n\n // update the ordered key index so this row doesn't change position\n var existingKeyIndex = _orderedKeys.indexOf(kOld);\n if (existingKeyIndex !== -1) _orderedKeys[existingKeyIndex] = kNew;\n\n d.key = kNew; // update datum to avoid exit/enter on tag update\n\n this.value = kNew;\n scheduleChange();\n }\n\n function valueChange(d3_event, d) {\n if (isReadOnly(d)) return;\n\n // exit if this is a multiselection and no value was entered\n if (Array.isArray(d.value) && !this.value) return;\n\n // exit if we are currently about to delete this row anyway - #6366\n if (_pendingChange && _pendingChange.hasOwnProperty(d.key) && _pendingChange[d.key] === undefined) return;\n\n _pendingChange = _pendingChange || {};\n\n const vNew = context.cleanTagValue(this.value);\n if (vNew !== this.value) {\n utilGetSetValue(d3_select(this), vNew);\n }\n\n _pendingChange[d.key] = vNew;\n scheduleChange();\n }\n\n function removeTag(d3_event, d) {\n if (isReadOnly(d)) return;\n\n // remove the key from the ordered key index\n _orderedKeys = _orderedKeys.filter(function(key) { return key !== d.key; });\n\n _pendingChange = _pendingChange || {};\n _pendingChange[d.key] = undefined;\n scheduleChange();\n }\n\n function scheduleChange() {\n if (!_pendingChange) return;\n\n for (const key in _pendingChange) {\n _tags[key] = _pendingChange[key];\n }\n dispatch.call('change', this, _entityIDs, _pendingChange);\n _pendingChange = null;\n }\n\n\n section.state = function(val) {\n if (!arguments.length) return _state;\n if (_state !== val) {\n _orderedKeys = [];\n _state = val;\n }\n return section;\n };\n\n\n section.presets = function(val) {\n if (!arguments.length) return _presets;\n _presets = val;\n if (_presets && _presets.length && _presets[0].isFallback()) {\n section.disclosureExpanded(true);\n\n // don't collapse the disclosure if the mapper used the raw tag editor - #1881\n } else if (!_didInteract) {\n section.disclosureExpanded(null);\n }\n return section;\n };\n\n\n section.tags = function(val) {\n if (!arguments.length) return _tags;\n _tags = val;\n return section;\n };\n\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n if (!_entityIDs || !val || !utilArrayIdentical(_entityIDs, val)) {\n _entityIDs = val;\n _orderedKeys = [];\n }\n return section;\n };\n\n\n // pass an array of regular expressions to test against the tag key\n section.readOnlyTags = function(val) {\n if (!arguments.length) return _readOnlyTags;\n _readOnlyTags = val;\n return section;\n };\n\n\n return utilRebind(section, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\nimport { stringifyProperties } from '../util/object';\nimport { uiDataHeader } from './data_header';\nimport { uiSectionRawTagEditor } from './sections/raw_tag_editor';\n\n\nexport function uiDataEditor(context) {\n var dataHeader = uiDataHeader();\n var rawTagEditor = uiSectionRawTagEditor('custom-data-tag-editor', context)\n .expandedByDefault(true)\n .readOnlyTags([/.*/]);\n var _datum;\n\n\n function dataEditor(selection) {\n\n var header = selection.selectAll('.header')\n .data([0]);\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function() {\n context.enter(modeBrowse(context));\n })\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('map_data.title'));\n\n\n var body = selection.selectAll('.body')\n .data([0]);\n\n body = body.enter()\n .append('div')\n .attr('class', 'body')\n .merge(body);\n\n var editor = body.selectAll('.data-editor')\n .data([0]);\n\n // enter/update\n editor.enter()\n .append('div')\n .attr('class', 'modal-section data-editor')\n .merge(editor)\n .call(dataHeader.datum(_datum));\n\n var rte = body.selectAll('.raw-tag-editor')\n .data([0]);\n\n // enter/update\n rte.enter()\n .append('div')\n .attr('class', 'raw-tag-editor data-editor')\n .merge(rte)\n .call(rawTagEditor\n .tags(stringifyProperties(_datum?.properties || {}))\n .state('hover')\n .render\n )\n .selectAll('textarea.tag-text')\n .attr('readonly', true)\n .classed('readonly', true);\n }\n\n\n dataEditor.datum = function(val) {\n if (!arguments.length) return _datum;\n _datum = val;\n return this;\n };\n\n\n return dataEditor;\n}\n", "import { geoBounds as d3_geoBounds } from 'd3-geo';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { behaviorBreathe } from '../behavior/breathe';\nimport { behaviorHover } from '../behavior/hover';\nimport { behaviorLasso } from '../behavior/lasso';\nimport { behaviorSelect } from '../behavior/select';\n\nimport { t } from '../core/localizer';\n\nimport { geoExtent } from '../geo';\nimport { modeBrowse } from './browse';\nimport { modeDragNode } from './drag_node';\nimport { modeDragNote } from './drag_note';\nimport { uiDataEditor } from '../ui/data_editor';\nimport { utilKeybinding } from '../util';\n\n\nexport function modeSelectData(context, selectedDatum) {\n var mode = {\n id: 'select-data',\n button: 'browse'\n };\n\n var keybinding = utilKeybinding('select-data');\n var dataEditor = uiDataEditor(context);\n\n var behaviors = [\n behaviorBreathe(context),\n behaviorHover(context),\n behaviorSelect(context),\n behaviorLasso(context),\n modeDragNode(context).behavior,\n modeDragNote(context).behavior\n ];\n\n\n // class the data as selected, or return to browse mode if the data is gone\n function selectData(d3_event, drawn) {\n var selection = context.surface().selectAll('.layer-mapdata .data' + selectedDatum.__featurehash__);\n\n if (selection.empty()) {\n // Return to browse mode if selected DOM elements have\n // disappeared because the user moved them out of view..\n var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;\n if (drawn && source && (source.type === 'pointermove' || source.type === 'mousemove' || source.type === 'touchmove')) {\n context.enter(modeBrowse(context));\n }\n } else {\n selection.classed('selected', true);\n }\n }\n\n\n function esc() {\n if (context.container().select('.combobox').size()) return;\n context.enter(modeBrowse(context));\n }\n\n\n mode.zoomToSelected = function() {\n var extent = geoExtent(d3_geoBounds(selectedDatum));\n context.map().centerZoomEase(extent.center(), context.map().trimmedExtentZoom(extent));\n };\n\n\n mode.enter = function() {\n behaviors.forEach(context.install);\n\n keybinding\n .on(t('inspector.zoom_to.key'), mode.zoomToSelected)\n .on('\u238B', esc, true);\n\n d3_select(document)\n .call(keybinding);\n\n selectData();\n\n var sidebar = context.ui().sidebar;\n sidebar.show(dataEditor.datum(selectedDatum));\n\n // expand the sidebar, avoid obscuring the data if needed\n var extent = geoExtent(d3_geoBounds(selectedDatum));\n sidebar.expand(sidebar.intersects(extent));\n\n context.map()\n .on('drawn.select-data', selectData);\n };\n\n\n mode.exit = function() {\n behaviors.forEach(context.uninstall);\n\n d3_select(document)\n .call(keybinding.unbind);\n\n context.surface()\n .selectAll('.layer-mapdata .selected')\n .classed('selected hover', false);\n\n context.map()\n .on('drawn.select-data', null);\n\n context.ui().sidebar\n .hide();\n };\n\n\n return mode;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { modeSelect } from '../modes/select';\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { utilDisplayName, utilHighlightEntities } from '../util';\n\n\nexport function uiOsmoseDetails(context) {\n let _qaItem;\n\n function issueString(d, type) {\n if (!d) return '';\n\n // Issue strings are cached from Osmose API\n const s = services.osmose.getStrings(d.itemType);\n return (type in s) ? s[type] : '';\n }\n\n\n function osmoseDetails(selection) {\n const details = selection.selectAll('.error-details')\n .data(\n _qaItem ? [_qaItem] : [],\n d => `${d.id}-${d.status || 0}`\n );\n\n details.exit()\n .remove();\n\n const detailsEnter = details.enter()\n .append('div')\n .attr('class', 'error-details qa-details-container');\n\n\n // Description\n if (issueString(_qaItem, 'detail')) {\n const div = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n div\n .append('h4')\n .call(t.append('QA.keepRight.detail_description'));\n\n div\n .append('p')\n .attr('class', 'qa-details-description-text')\n .html(d => issueString(d, 'detail'))\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Elements (populated later as data is requested)\n const detailsDiv = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n const elemsDiv = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n // Suggested Fix (mustn't exist for every issue type)\n if (issueString(_qaItem, 'fix')) {\n const div = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n div\n .append('h4')\n .call(t.append('QA.osmose.fix_title'));\n\n div\n .append('p')\n .html(d => issueString(d, 'fix'))\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Common Pitfalls (mustn't exist for every issue type)\n if (issueString(_qaItem, 'trap')) {\n const div = detailsEnter\n .append('div')\n .attr('class', 'qa-details-subsection');\n\n div\n .append('h4')\n .call(t.append('QA.osmose.trap_title'));\n\n div\n .append('p')\n .html(d => issueString(d, 'trap'))\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Save current item to check if UI changed by time request resolves\n const thisItem = _qaItem;\n services.osmose.loadIssueDetail(_qaItem)\n .then(d => {\n // No details to add if there are no associated issue elements\n if (!d.elems || d.elems.length === 0) return;\n\n // Do nothing if UI has moved on by the time this resolves\n if (\n context.selectedErrorID() !== thisItem.id\n && context.container().selectAll(`.qaItem.osmose.hover.itemId-${thisItem.id}`).empty()\n ) return;\n\n // Things like keys and values are dynamically added to a subtitle string\n if (d.detail) {\n detailsDiv\n .append('h4')\n .call(t.append('QA.osmose.detail_title'));\n\n detailsDiv\n .append('p')\n .html(d => d.detail)\n .selectAll('a')\n .attr('rel', 'noopener')\n .attr('target', '_blank');\n }\n\n // Create list of linked issue elements\n elemsDiv\n .append('h4')\n .call(t.append('QA.osmose.elems_title'));\n\n elemsDiv\n .append('ul').selectAll('li')\n .data(d.elems)\n .enter()\n .append('li')\n .append('a')\n .attr('href', '#')\n .attr('class', 'error_entity_link')\n .text(d => d)\n .each(function() {\n const link = d3_select(this);\n const entityID = this.textContent;\n const entity = context.hasEntity(entityID);\n\n // Add click handler\n link\n .on('mouseenter', () => {\n utilHighlightEntities([entityID], true, context);\n })\n .on('mouseleave', () => {\n utilHighlightEntities([entityID], false, context);\n })\n .on('click', (d3_event) => {\n d3_event.preventDefault();\n\n utilHighlightEntities([entityID], false, context);\n\n const osmlayer = context.layers().layer('osm');\n if (!osmlayer.enabled()) {\n osmlayer.enabled(true);\n }\n\n context.map().centerZoom(d.loc, 20);\n\n if (entity) {\n context.enter(modeSelect(context, [entityID]));\n } else {\n context.loadEntity(entityID, (err, result) => {\n if (err) return;\n const entity = result.data.find(e => e.id === entityID);\n if (entity) context.enter(modeSelect(context, [entityID]));\n });\n }\n });\n\n // Replace with friendly name if possible\n // (The entity may not yet be loaded into the graph)\n if (entity) {\n let name = utilDisplayName(entity); // try to use common name\n\n if (!name) {\n const preset = presetManager.match(entity, context.graph());\n name = preset && !preset.isFallback() && preset.name(); // fallback to preset name\n }\n\n if (name) {\n this.innerText = name;\n }\n }\n });\n\n // Don't hide entities related to this issue - #5880\n context.features().forceVisible(d.elems);\n context.map().pan([0,0]); // trigger a redraw\n })\n .catch(err => {\n console.log(err); // eslint-disable-line no-console\n });\n }\n\n\n osmoseDetails.issue = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return osmoseDetails;\n };\n\n\n return osmoseDetails;\n}\n", "import { services } from '../services';\nimport { t } from '../core/localizer';\n\n\nexport function uiOsmoseHeader() {\n let _qaItem;\n\n function issueTitle(d) {\n const unknown = t('inspector.unknown');\n\n if (!d) return unknown;\n\n // Issue titles supplied by Osmose\n const s = services.osmose.getStrings(d.itemType);\n return ('title' in s) ? s.title : unknown;\n }\n\n function osmoseHeader(selection) {\n const header = selection.selectAll('.qa-header')\n .data(\n (_qaItem ? [_qaItem] : []),\n d => `${d.id}-${d.status || 0}`\n );\n\n header.exit()\n .remove();\n\n const headerEnter = header.enter()\n .append('div')\n .attr('class', 'qa-header');\n\n const svgEnter = headerEnter\n .append('div')\n .attr('class', 'qa-header-icon')\n .classed('new', d => d.id < 0)\n .append('svg')\n .attr('width', '20px')\n .attr('height', '30px')\n .attr('viewbox', '0 0 20 30')\n .attr('class', d => `preset-icon-28 qaItem ${d.service} itemId-${d.id} itemType-${d.itemType}`);\n\n svgEnter\n .append('polygon')\n .attr('fill', d => services.osmose.getColor(d.item))\n .attr('class', 'qaItem-fill')\n .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');\n\n svgEnter\n .append('use')\n .attr('class', 'icon-annotation')\n .attr('width', '12px')\n .attr('height', '12px')\n .attr('transform', 'translate(4, 5.5)')\n .attr('xlink:href', d => d.icon ? '#' + d.icon : '');\n\n headerEnter\n .append('div')\n .attr('class', 'qa-header-label')\n .text(issueTitle);\n }\n\n osmoseHeader.issue = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return osmoseHeader;\n };\n\n return osmoseHeader;\n}\n", "import { t } from '../core/localizer';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\nimport { QAItem } from '../osm';\n\nexport function uiViewOnOsmose() {\n let _qaItem;\n\n function viewOnOsmose(selection) {\n let url;\n if (services.osmose && (_qaItem instanceof QAItem)) {\n url = services.osmose.itemURL(_qaItem);\n }\n\n const link = selection.selectAll('.view-on-osmose')\n .data(url ? [url] : []);\n\n // exit\n link.exit()\n .remove();\n\n // enter\n const linkEnter = link.enter()\n .append('a')\n .attr('class', 'view-on-osmose')\n .attr('target', '_blank')\n .attr('rel', 'noopener') // security measure\n .attr('href', d => d)\n .call(svgIcon('#iD-icon-out-link', 'inline'));\n\n linkEnter\n .append('span')\n .call(t.append('inspector.view_on_osmose'));\n }\n\n viewOnOsmose.what = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return viewOnOsmose;\n };\n\n return viewOnOsmose;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\n\nimport { uiOsmoseDetails } from './osmose_details';\nimport { uiOsmoseHeader } from './osmose_header';\nimport { uiViewOnOsmose } from './view_on_osmose';\n\nimport { utilRebind } from '../util';\n\nexport function uiOsmoseEditor(context) {\n const dispatch = d3_dispatch('change');\n const qaDetails = uiOsmoseDetails(context);\n const qaHeader = uiOsmoseHeader(context);\n\n let _qaItem;\n\n function osmoseEditor(selection) {\n\n const header = selection.selectAll('.header')\n .data([0]);\n\n const headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', () => context.enter(modeBrowse(context)))\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('QA.osmose.title'));\n\n let body = selection.selectAll('.body')\n .data([0]);\n\n body = body.enter()\n .append('div')\n .attr('class', 'body')\n .merge(body);\n\n let editor = body.selectAll('.qa-editor')\n .data([0]);\n\n editor.enter()\n .append('div')\n .attr('class', 'modal-section qa-editor')\n .merge(editor)\n .call(qaHeader.issue(_qaItem))\n .call(qaDetails.issue(_qaItem))\n .call(osmoseSaveSection);\n\n const footer = selection.selectAll('.footer')\n .data([0]);\n\n footer.enter()\n .append('div')\n .attr('class', 'footer')\n .merge(footer)\n .call(uiViewOnOsmose(context).what(_qaItem));\n }\n\n function osmoseSaveSection(selection) {\n const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());\n const isShown = (_qaItem && isSelected);\n let saveSection = selection.selectAll('.qa-save')\n .data(\n (isShown ? [_qaItem] : []),\n d => `${d.id}-${d.status || 0}`\n );\n\n // exit\n saveSection.exit()\n .remove();\n\n // enter\n const saveSectionEnter = saveSection.enter()\n .append('div')\n .attr('class', 'qa-save save-section cf');\n\n // update\n saveSectionEnter\n .merge(saveSection)\n .call(qaSaveButtons);\n }\n\n function qaSaveButtons(selection) {\n const isSelected = (_qaItem && _qaItem.id === context.selectedErrorID());\n let buttonSection = selection.selectAll('.buttons')\n .data((isSelected ? [_qaItem] : []), d => d.status + d.id);\n\n // exit\n buttonSection.exit()\n .remove();\n\n // enter\n const buttonEnter = buttonSection.enter()\n .append('div')\n .attr('class', 'buttons');\n\n buttonEnter\n .append('button')\n .attr('class', 'button close-button action');\n\n buttonEnter\n .append('button')\n .attr('class', 'button ignore-button action');\n\n // update\n buttonSection = buttonSection\n .merge(buttonEnter);\n\n buttonSection.select('.close-button')\n .call(t.append('QA.keepRight.close'))\n .on('click.close', function(d3_event, d) {\n this.blur(); // avoid keeping focus on the button - #4641\n const qaService = services.osmose;\n if (qaService) {\n d.newStatus = 'done';\n qaService.postUpdate(d, (err, item) => dispatch.call('change', item));\n }\n });\n\n buttonSection.select('.ignore-button')\n .call(t.append('QA.keepRight.ignore'))\n .on('click.ignore', function(d3_event, d) {\n this.blur(); // avoid keeping focus on the button - #4641\n const qaService = services.osmose;\n if (qaService) {\n d.newStatus = 'false';\n qaService.postUpdate(d, (err, item) => dispatch.call('change', item));\n }\n });\n }\n\n // NOTE: Don't change method name until UI v3 is merged\n osmoseEditor.error = function(val) {\n if (!arguments.length) return _qaItem;\n _qaItem = val;\n return osmoseEditor;\n };\n\n return utilRebind(osmoseEditor, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { behaviorBreathe } from '../behavior/breathe';\nimport { behaviorHover } from '../behavior/hover';\nimport { behaviorLasso } from '../behavior/lasso';\nimport { behaviorSelect } from '../behavior/select';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { modeBrowse } from './browse';\nimport { modeDragNode } from './drag_node';\nimport { modeDragNote } from './drag_note';\nimport { uiOsmoseEditor } from '../ui/osmose_editor';\nimport { utilKeybinding } from '../util';\n\n// NOTE: Don't change name of this until UI v3 is merged\nexport function modeSelectError(context, selectedErrorID, selectedErrorService) {\n var mode = {\n id: 'select-error',\n button: 'browse'\n };\n\n var keybinding = utilKeybinding('select-error');\n\n var errorService = services[selectedErrorService];\n var errorEditor;\n switch (selectedErrorService) {\n case 'osmose':\n errorEditor = uiOsmoseEditor(context)\n .on('change', function() {\n context.map().pan([0,0]); // trigger a redraw\n var error = checkSelectedID();\n if (!error) return;\n context.ui().sidebar\n .show(errorEditor.error(error));\n });\n break;\n }\n\n\n var behaviors = [\n behaviorBreathe(context),\n behaviorHover(context),\n behaviorSelect(context),\n behaviorLasso(context),\n modeDragNode(context).behavior,\n modeDragNote(context).behavior\n ];\n\n\n function checkSelectedID() {\n if (!errorService) return;\n var error = errorService.getError(selectedErrorID);\n if (!error) {\n context.enter(modeBrowse(context));\n }\n return error;\n }\n\n\n mode.zoomToSelected = function() {\n if (!errorService) return;\n var error = errorService.getError(selectedErrorID);\n if (error) {\n context.map().centerZoomEase(error.loc, 20);\n }\n };\n\n\n mode.enter = function() {\n var error = checkSelectedID();\n if (!error) return;\n\n behaviors.forEach(context.install);\n keybinding\n .on(t('inspector.zoom_to.key'), mode.zoomToSelected)\n .on('\u238B', esc, true);\n\n d3_select(document)\n .call(keybinding);\n\n selectError();\n\n var sidebar = context.ui().sidebar;\n sidebar.show(errorEditor.error(error));\n\n context.map()\n .on('drawn.select-error', selectError);\n\n\n // class the error as selected, or return to browse mode if the error is gone\n function selectError(d3_event, drawn) {\n if (!checkSelectedID()) return;\n\n var selection = context.surface()\n .selectAll('.itemId-' + selectedErrorID + '.' + selectedErrorService);\n\n if (selection.empty()) {\n // Return to browse mode if selected DOM elements have\n // disappeared because the user moved them out of view..\n var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;\n if (drawn && source && (source.type === 'pointermove' || source.type === 'mousemove' || source.type === 'touchmove')) {\n context.enter(modeBrowse(context));\n }\n\n } else {\n selection\n .classed('selected', true);\n\n context.selectedErrorID(selectedErrorID);\n }\n }\n\n function esc() {\n if (context.container().select('.combobox').size()) return;\n context.enter(modeBrowse(context));\n }\n };\n\n\n mode.exit = function() {\n behaviors.forEach(context.uninstall);\n\n d3_select(document)\n .call(keybinding.unbind);\n\n context.surface()\n .selectAll('.qaItem.selected')\n .classed('selected hover', false);\n\n context.map()\n .on('drawn.select-error', null);\n\n context.ui().sidebar\n .hide();\n\n context.selectedErrorID(null);\n context.features().forceVisible([]);\n };\n\n\n return mode;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { geoVecLength } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect } from '../modes/select';\nimport { modeSelectData } from '../modes/select_data';\nimport { modeSelectNote } from '../modes/select_note';\nimport { modeSelectError } from '../modes/select_error';\nimport { osmEntity, osmNote, QAItem } from '../osm';\nimport { utilFastMouse } from '../util/util';\n\n\nexport function behaviorSelect(context) {\n var _tolerancePx = 4; // see also behaviorDrag\n var _lastMouseEvent = null;\n var _showMenu = false;\n var _downPointers = {};\n var _longPressTimeout = null;\n var _lastInteractionType = null;\n // the id of the down pointer that's enabling multiselection while down\n var _multiselectionPointerId = null;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function keydown(d3_event) {\n\n if (d3_event.keyCode === 32) {\n // don't react to spacebar events during text input\n var activeNode = document.activeElement;\n if (activeNode && new Set(['INPUT', 'TEXTAREA']).has(activeNode.nodeName)) return;\n }\n\n if (d3_event.keyCode === 93 || // context menu key\n d3_event.keyCode === 32) { // spacebar\n d3_event.preventDefault();\n }\n\n if (d3_event.repeat) return; // ignore repeated events for held keys\n\n // if any key is pressed the user is probably doing something other than long-pressing\n cancelLongPress();\n\n if (d3_event.shiftKey) {\n context.surface()\n .classed('behavior-multiselect', true);\n }\n\n if (d3_event.keyCode === 32) { // spacebar\n if (!_downPointers.spacebar && _lastMouseEvent) {\n cancelLongPress();\n _longPressTimeout = window.setTimeout(didLongPress, 500, 'spacebar', 'spacebar');\n\n _downPointers.spacebar = {\n firstEvent: _lastMouseEvent,\n lastEvent: _lastMouseEvent\n };\n }\n }\n }\n\n\n function keyup(d3_event) {\n cancelLongPress();\n\n if (!d3_event.shiftKey) {\n context.surface()\n .classed('behavior-multiselect', false);\n }\n\n if (d3_event.keyCode === 93) { // context menu key\n d3_event.preventDefault();\n _lastInteractionType = 'menukey';\n contextmenu(d3_event);\n } else if (d3_event.keyCode === 32) { // spacebar\n var pointer = _downPointers.spacebar;\n if (pointer) {\n delete _downPointers.spacebar;\n\n if (pointer.done) return;\n\n d3_event.preventDefault();\n _lastInteractionType = 'spacebar';\n click(pointer.firstEvent, pointer.lastEvent, 'spacebar');\n }\n }\n }\n\n\n function pointerdown(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n\n cancelLongPress();\n\n if (d3_event.buttons && d3_event.buttons !== 1) return;\n\n context.ui().closeEditMenu();\n\n if (d3_event.pointerType !== 'mouse') {\n _longPressTimeout = window.setTimeout(didLongPress, 500, id, 'longdown-' + (d3_event.pointerType || 'mouse'));\n }\n\n _downPointers[id] = {\n firstEvent: d3_event,\n lastEvent: d3_event\n };\n }\n\n\n function didLongPress(id, interactionType) {\n var pointer = _downPointers[id];\n if (!pointer) return;\n\n for (var i in _downPointers) {\n // don't allow this or any currently down pointer to trigger another click\n _downPointers[i].done = true;\n }\n\n // treat long presses like right-clicks\n _longPressTimeout = null;\n _lastInteractionType = interactionType;\n _showMenu = true;\n\n click(pointer.firstEvent, pointer.lastEvent, id);\n }\n\n\n function pointermove(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n if (_downPointers[id]) {\n _downPointers[id].lastEvent = d3_event;\n }\n if (!d3_event.pointerType || d3_event.pointerType === 'mouse') {\n _lastMouseEvent = d3_event;\n if (_downPointers.spacebar) {\n _downPointers.spacebar.lastEvent = d3_event;\n }\n }\n }\n\n\n function pointerup(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n var pointer = _downPointers[id];\n if (!pointer) return;\n\n delete _downPointers[id];\n\n if (_multiselectionPointerId === id) {\n _multiselectionPointerId = null;\n }\n\n if (pointer.done) return;\n\n click(pointer.firstEvent, d3_event, id);\n }\n\n\n function pointercancel(d3_event) {\n var id = (d3_event.pointerId || 'mouse').toString();\n if (!_downPointers[id]) return;\n\n delete _downPointers[id];\n\n if (_multiselectionPointerId === id) {\n _multiselectionPointerId = null;\n }\n }\n\n\n function contextmenu(d3_event) {\n d3_event.preventDefault();\n\n if (!+d3_event.clientX && !+d3_event.clientY) {\n if (_lastMouseEvent) {\n d3_event = _lastMouseEvent;\n } else {\n return;\n }\n } else {\n _lastMouseEvent = d3_event;\n if (d3_event.pointerType === 'touch' || d3_event.pointerType === 'pen' ||\n d3_event.mozInputSource && ( // firefox doesn't give a pointerType on contextmenu events\n d3_event.mozInputSource === MouseEvent.MOZ_SOURCE_TOUCH ||\n d3_event.mozInputSource === MouseEvent.MOZ_SOURCE_PEN)) {\n _lastInteractionType = 'touch';\n } else {\n _lastInteractionType = 'rightclick';\n }\n }\n\n _showMenu = true;\n click(d3_event, d3_event);\n }\n\n\n function click(firstEvent, lastEvent, pointerId) {\n cancelLongPress();\n\n var mapNode = context.container().select('.main-map').node();\n\n // Use the `main-map` coordinate system since the surface and supersurface\n // are transformed when drag-panning.\n var pointGetter = utilFastMouse(mapNode);\n var p1 = pointGetter(firstEvent);\n var p2 = pointGetter(lastEvent);\n var dist = geoVecLength(p1, p2);\n\n if (dist > _tolerancePx ||\n !mapContains(lastEvent)) {\n\n resetProperties();\n return;\n }\n\n var targetDatum = lastEvent.target.__data__;\n if (targetDatum === 0 && lastEvent.target.parentNode.__data__) {\n // some targets (like markers of the street level photo\n // layers) have the data bound to the parent node\n targetDatum = lastEvent.target.parentNode.__data__;\n }\n\n var multiselectEntityId;\n\n if (!_multiselectionPointerId) {\n // If a different pointer than the one triggering this click is down on a\n // feature, treat this and all future clicks as multiselection until that\n // pointer is raised.\n var selectPointerInfo = pointerDownOnSelection(pointerId);\n if (selectPointerInfo) {\n _multiselectionPointerId = selectPointerInfo.pointerId;\n // if the other feature isn't selected yet, make sure we select it\n multiselectEntityId = !selectPointerInfo.selected && selectPointerInfo.entityId;\n _downPointers[selectPointerInfo.pointerId].done = true;\n }\n }\n\n // support multiselect if data is already selected\n var isMultiselect = context.mode().id === 'select' && (\n // and shift key is down\n (lastEvent && lastEvent.shiftKey) ||\n // or we're lasso-selecting\n context.surface().select('.lasso').node() ||\n // or a pointer is down over a selected feature\n (_multiselectionPointerId && !multiselectEntityId)\n );\n\n processClick(targetDatum, isMultiselect, p2, multiselectEntityId);\n\n function mapContains(event) {\n var rect = mapNode.getBoundingClientRect();\n return event.clientX >= rect.left &&\n event.clientX <= rect.right &&\n event.clientY >= rect.top &&\n event.clientY <= rect.bottom;\n }\n\n function pointerDownOnSelection(skipPointerId) {\n var mode = context.mode();\n var selectedIDs = mode.id === 'select' ? mode.selectedIDs() : [];\n for (var pointerId in _downPointers) {\n if (pointerId === 'spacebar' || pointerId === skipPointerId) continue;\n\n var pointerInfo = _downPointers[pointerId];\n\n var p1 = pointGetter(pointerInfo.firstEvent);\n var p2 = pointGetter(pointerInfo.lastEvent);\n if (geoVecLength(p1, p2) > _tolerancePx) continue;\n\n var datum = pointerInfo.firstEvent.target.__data__;\n var entity = (datum && datum.properties && datum.properties.entity) || datum;\n if (context.graph().hasEntity(entity.id)) {\n return {\n pointerId: pointerId,\n entityId: entity.id,\n selected: selectedIDs.indexOf(entity.id) !== -1\n };\n }\n }\n return null;\n }\n }\n\n\n function processClick(datum, isMultiselect, point, alsoSelectId) {\n var mode = context.mode();\n var showMenu = _showMenu;\n var interactionType = _lastInteractionType;\n\n var entity = datum && datum.properties && datum.properties.entity;\n if (entity) datum = entity;\n\n if (datum && datum.type === 'midpoint') {\n // treat targeting midpoints as if targeting the parent way\n datum = datum.parents[0];\n }\n\n var newMode;\n\n if (datum instanceof osmEntity) {\n // targeting an entity\n var selectedIDs = context.selectedIDs();\n context.selectedNoteID(null);\n context.selectedErrorID(null);\n\n if (!isMultiselect) {\n // don't change the selection if we're toggling the menu atop a multiselection\n if (!showMenu ||\n selectedIDs.length <= 1 ||\n selectedIDs.indexOf(datum.id) === -1) {\n\n if (alsoSelectId === datum.id) alsoSelectId = null;\n\n selectedIDs = (alsoSelectId ? [alsoSelectId] : []).concat([datum.id]);\n // always enter modeSelect even if the entity is already\n // selected since listeners may expect `context.enter` events,\n // e.g. in the walkthrough\n newMode = mode.id === 'select' ? mode.selectedIDs(selectedIDs) : modeSelect(context, selectedIDs).selectBehavior(behavior);\n context.enter(newMode);\n }\n\n } else {\n if (selectedIDs.indexOf(datum.id) !== -1) {\n // clicked entity is already in the selectedIDs list..\n if (!showMenu) {\n // deselect clicked entity, then reenter select mode or return to browse mode..\n selectedIDs = selectedIDs.filter(function(id) { return id !== datum.id; });\n newMode = selectedIDs.length ? mode.selectedIDs(selectedIDs) : modeBrowse(context).selectBehavior(behavior);\n context.enter(newMode);\n }\n } else {\n // clicked entity is not in the selected list, add it..\n selectedIDs = selectedIDs.concat([datum.id]);\n newMode = mode.selectedIDs(selectedIDs);\n context.enter(newMode);\n }\n }\n\n } else if (datum && datum.__featurehash__ && !isMultiselect) {\n // targeting custom data\n context\n .selectedNoteID(null)\n .enter(modeSelectData(context, datum));\n\n } else if (datum instanceof osmNote && !isMultiselect) {\n // targeting a note\n context\n .selectedNoteID(datum.id)\n .enter(modeSelectNote(context, datum.id));\n\n } else if (datum instanceof QAItem && !isMultiselect) {\n // targeting an external QA issue\n context\n .selectedErrorID(datum.id)\n .enter(modeSelectError(context, datum.id, datum.service));\n\n } else if (datum.service === 'photo') {\n // street level photo was selected:\n // don't change mode and selection\n } else {\n // targeting nothing\n context.selectedNoteID(null);\n context.selectedErrorID(null);\n if (!isMultiselect && mode.id !== 'browse') {\n context.enter(modeBrowse(context));\n }\n }\n\n context.ui().closeEditMenu();\n\n // always request to show the edit menu in case the mode needs it\n if (showMenu) context.ui().showEditMenu(point, interactionType);\n\n resetProperties();\n }\n\n\n function cancelLongPress() {\n if (_longPressTimeout) window.clearTimeout(_longPressTimeout);\n _longPressTimeout = null;\n }\n\n\n function resetProperties() {\n cancelLongPress();\n _showMenu = false;\n _lastInteractionType = null;\n // don't reset _lastMouseEvent since it might still be useful\n }\n\n\n function behavior(selection) {\n resetProperties();\n _lastMouseEvent = context.map().lastPointerEvent();\n\n d3_select(window)\n .on('keydown.select', keydown)\n .on('keyup.select', keyup)\n .on(_pointerPrefix + 'move.select', pointermove, true)\n .on(_pointerPrefix + 'up.select', pointerup, true)\n .on('pointercancel.select', pointercancel, true)\n .on('contextmenu.select-window', function(d3_event) {\n // Edge and IE really like to show the contextmenu on the\n // menubar when user presses a keyboard menu button\n // even after we've already preventdefaulted the key event.\n var e = d3_event;\n if (+e.clientX === 0 && +e.clientY === 0) {\n d3_event.preventDefault();\n }\n });\n\n selection\n .on(_pointerPrefix + 'down.select', pointerdown)\n .on('contextmenu.select', contextmenu);\n\n /*if (d3_event && d3_event.shiftKey) {\n context.surface()\n .classed('behavior-multiselect', true);\n }*/\n }\n\n\n behavior.off = function(selection) {\n cancelLongPress();\n\n d3_select(window)\n .on('keydown.select', null)\n .on('keyup.select', null)\n .on('contextmenu.select-window', null)\n .on(_pointerPrefix + 'move.select', null, true)\n .on(_pointerPrefix + 'up.select', null, true)\n .on('pointercancel.select', null, true);\n\n selection\n .on(_pointerPrefix + 'down.select', null)\n .on('contextmenu.select', null);\n\n context.surface()\n .classed('behavior-multiselect', false);\n };\n\n\n return behavior;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../core/preferences';\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { services } from '../services';\nimport { localeDateString } from '../util/date';\n\n\nexport function uiNoteComments() {\n var _note;\n\n\n function noteComments(selection) {\n if (_note.isNew()) return; // don't draw .comments-container\n\n var comments = selection.selectAll('.comments-container')\n .data([0]);\n\n comments = comments.enter()\n .append('div')\n .attr('class', 'comments-container')\n .merge(comments);\n\n var commentEnter = comments.selectAll('.comment')\n .data(_note.comments)\n .enter()\n .append('div')\n .attr('class', 'comment');\n\n commentEnter\n .append('div')\n .attr('class', function(d) { return 'comment-avatar user-' + d.uid; })\n .call(svgIcon('#iD-icon-avatar', 'comment-avatar-icon'));\n\n var mainEnter = commentEnter\n .append('div')\n .attr('class', 'comment-main');\n\n var metadataEnter = mainEnter\n .append('div')\n .attr('class', 'comment-metadata');\n\n metadataEnter\n .append('div')\n .attr('class', 'comment-author')\n .each(function(d) {\n var selection = d3_select(this);\n var osm = services.osm;\n if (osm && d.user) {\n selection = selection\n .append('a')\n .attr('class', 'comment-author-link')\n .attr('href', osm.userURL(d.user))\n .attr('target', '_blank');\n }\n if (d.user) {\n selection.text(d.user);\n } else {\n selection.call(t.append('note.anonymous'));\n }\n });\n\n metadataEnter\n .append('div')\n .attr('class', 'comment-date')\n .each(function(d) {\n d3_select(this).call(\n t.addOrUpdate('note.status.' + d.action, {\n when: localeDateString(d.date.replace(' UTC', 'Z').replace(' ', 'T')),\n }));\n });\n\n mainEnter\n .append('div')\n .attr('class', 'comment-text')\n .html(function(d) { return d.html; })\n .selectAll('a')\n .attr('rel', 'noopener nofollow')\n .attr('target', '_blank');\n\n comments\n .call(replaceAvatars);\n }\n\n\n function replaceAvatars(selection) {\n var showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n var osm = services.osm;\n if (showThirdPartyIcons !== 'true' || !osm) return;\n\n var uids = {}; // gather uids in the comment thread\n _note.comments.forEach(function(d) {\n if (d.uid) uids[d.uid] = true;\n });\n\n Object.keys(uids).forEach(function(uid) {\n osm.loadUser(uid, function(err, user) {\n if (!user || !user.image_url) return;\n\n selection.selectAll('.comment-avatar.user-' + uid)\n .html('')\n .append('img')\n .attr('class', 'icon comment-avatar-icon')\n .attr('src', user.image_url)\n .attr('alt', user.display_name);\n });\n });\n }\n\n\n noteComments.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteComments;\n };\n\n\n return noteComments;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiNoteHeader() {\n var _note;\n\n\n function noteHeader(selection) {\n var header = selection.selectAll('.note-header')\n .data(\n (_note ? [_note] : []),\n function(d) { return d.status + d.id; }\n );\n\n header.exit()\n .remove();\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'note-header');\n\n var iconEnter = headerEnter\n .append('div')\n .attr('class', function(d) { return 'note-header-icon ' + d.status; })\n .classed('new', function(d) { return d.id < 0; });\n\n iconEnter\n .append('div')\n .attr('class', 'preset-icon-28')\n .call(svgIcon('#iD-icon-note', 'note-fill'));\n\n iconEnter.each(function(d) {\n var statusIcon;\n if (d.id < 0) {\n statusIcon = '#iD-icon-plus';\n } else if (d.status === 'open') {\n statusIcon = '#iD-icon-close';\n } else {\n statusIcon = '#iD-icon-apply';\n }\n iconEnter\n .append('div')\n .attr('class', 'note-icon-annotation')\n .attr('title', t('icons.close'))\n .call(svgIcon(statusIcon, 'icon-annotation'));\n });\n\n headerEnter\n .append('div')\n .attr('class', 'note-header-label')\n .each(function(d) {\n const selection = d3_select(this);\n selection.text('');\n if (_note.isNew()) {\n selection.call(t.append('note.new'));\n } else {\n selection.call(t.append('note.note'));\n selection\n .append('span')\n .text(` ${d.id} `);\n if (d.status === 'closed') {\n selection.call(t.append('note.closed'));\n }\n }\n });\n }\n\n\n noteHeader.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteHeader;\n };\n\n\n return noteHeader;\n}\n", "import { t } from '../core/localizer';\nimport { osmNote } from '../osm';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiNoteReport() {\n var _note;\n\n function noteReport(selection) {\n var url;\n if (services.osm && (_note instanceof osmNote) && (!_note.isNew())) {\n url = services.osm.noteReportURL(_note);\n }\n\n var link = selection.selectAll('.note-report')\n .data(url ? [url] : []);\n\n // exit\n link.exit()\n .remove();\n\n // enter\n var linkEnter = link.enter()\n .append('a')\n .attr('class', 'note-report')\n .attr('target', '_blank')\n .attr('href', function(d) { return d; })\n .call(svgIcon('#iD-icon-out-link', 'inline'));\n\n linkEnter\n .append('span')\n .call(t.append('note.report'));\n }\n\n\n noteReport.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteReport;\n };\n\n return noteReport;\n}\n", "import { t } from '../core/localizer';\nimport { osmEntity, osmNote, osmRelation, osmWay } from '../osm';\nimport { svgIcon } from '../svg/icon';\nimport { getRelativeDate } from '../util/date';\n\n\nexport function uiViewOnOSM(context) {\n var _what; // an osmEntity or osmNote\n\n\n function viewOnOSM(selection) {\n var url;\n if (_what instanceof osmEntity) {\n url = context.connection().historyURL(_what);\n } else if (_what instanceof osmNote) {\n url = context.connection().noteURL(_what);\n }\n\n var data = ((!_what || _what.isNew()) ? [] : [_what]);\n var link = selection.selectAll('.view-on-osm')\n .data(data, function(d) { return d.id; });\n\n // exit\n link.exit()\n .remove();\n\n // enter\n var linkEnter = link.enter()\n .append('a')\n .attr('class', 'view-on-osm')\n .attr('target', '_blank')\n .attr('href', url)\n .call(svgIcon('#iD-icon-out-link', 'inline'));\n\n\n if (_what && !(_what instanceof osmNote)) {\n // node/way/relation\n const { user, timestamp } = uiViewOnOSM.findLastModifiedChild(context.history().base(), _what);\n\n linkEnter\n .call(t.append('inspector.last_touched', {\n timeago: getRelativeDate(new Date(timestamp)),\n user\n }))\n .attr('title', t('inspector.view_on_osm'));\n } else {\n linkEnter\n .append('span')\n .call(t.append('inspector.view_on_osm'));\n }\n }\n\n\n viewOnOSM.what = function(_) {\n if (!arguments.length) return _what;\n _what = _;\n return viewOnOSM;\n };\n\n return viewOnOSM;\n}\n\n\n/**\n * @param {iD.Graph} graph\n * @param {iD.OsmEntity} feature\n */\nuiViewOnOSM.findLastModifiedChild = (graph, feature) => {\n let latest = feature;\n\n /** @param {iD.OsmEntity} obj */\n function recurseChilds(obj) {\n if (obj.timestamp > latest.timestamp) {\n latest = obj;\n }\n if (obj instanceof osmWay) {\n obj.nodes\n .map(id => graph.hasEntity(id))\n .filter(Boolean)\n .forEach(recurseChilds);\n } else if (obj instanceof osmRelation) {\n obj.members\n .map(m => graph.hasEntity(m.id))\n .filter(e => e instanceof osmWay || e instanceof osmRelation)\n .forEach(recurseChilds);\n }\n }\n\n recurseChilds(feature);\n return latest;\n};\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { services } from '../services';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\n\n// import { uiField } from './field';\n// import { uiFormFields } from './form_fields';\n\nimport { uiNoteComments } from './note_comments';\nimport { uiNoteHeader } from './note_header';\nimport { uiNoteReport } from './note_report';\nimport { uiViewOnOSM } from './view_on_osm';\n\nimport {\n utilNoAuto,\n utilRebind\n} from '../util';\nimport { osmNote } from '../osm';\n\n\nexport function uiNoteEditor(context) {\n var dispatch = d3_dispatch('change');\n var noteComments = uiNoteComments(context);\n var noteHeader = uiNoteHeader();\n\n // var formFields = uiFormFields(context);\n\n var _note;\n var _newNote;\n // var _fieldsArr;\n\n\n function noteEditor(selection) {\n\n var header = selection.selectAll('.header')\n .data([0]);\n\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function() {\n context.enter(modeBrowse(context));\n })\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('note.title'));\n\n\n var body = selection.selectAll('.body')\n .data([0]);\n\n body = body.enter()\n .append('div')\n .attr('class', 'body')\n .merge(body);\n\n var editor = body.selectAll('.note-editor')\n .data([0]);\n\n editor.enter()\n .append('div')\n .attr('class', 'modal-section note-editor')\n .merge(editor)\n .call(noteHeader.note(_note))\n .call(noteComments.note(_note))\n .call(noteSaveSection);\n\n var footer = selection.selectAll('.footer')\n .data([0]);\n\n footer.enter()\n .append('div')\n .attr('class', 'footer')\n .merge(footer)\n .call(uiViewOnOSM(context).what(_note))\n .call(uiNoteReport(context).note(_note));\n\n\n // rerender the note editor on any auth change\n var osm = services.osm;\n if (osm) {\n osm.on('change.note-save', function() {\n selection.call(noteEditor);\n });\n }\n }\n\n\n function noteSaveSection(selection) {\n var isSelected = (_note && _note.id === context.selectedNoteID());\n var noteSave = selection.selectAll('.note-save')\n .data((isSelected ? [_note].filter(d => d.status !== 'hidden') : []), d => d.status + d.id);\n\n // exit\n noteSave.exit()\n .remove();\n\n // enter\n var noteSaveEnter = noteSave.enter()\n .append('div')\n .attr('class', 'note-save save-section cf');\n\n // // if new note, show categories to pick from\n // if (_note.isNew()) {\n // var presets = presetManager;\n\n // // NOTE: this key isn't a age and therefore there is no documentation (yet)\n // _fieldsArr = [\n // uiField(context, presets.field('category'), null, { show: true, revert: false }),\n // ];\n\n // _fieldsArr.forEach(function(field) {\n // field\n // .on('change', changeCategory);\n // });\n\n // noteSaveEnter\n // .append('div')\n // .attr('class', 'note-category')\n // .call(formFields.fieldsArr(_fieldsArr));\n // }\n\n // function changeCategory() {\n // // NOTE: perhaps there is a better way to get value\n // var val = context.container().select('input[name=\\'category\\']:checked').property('__data__') || undefined;\n\n // // store the unsaved category with the note itself\n // _note = _note.update({ newCategory: val });\n // var osm = services.osm;\n // if (osm) {\n // osm.replaceNote(_note); // update note cache\n // }\n // noteSave\n // .call(noteSaveButtons);\n // }\n\n noteSaveEnter\n .append('h4')\n .attr('class', '.note-save-header')\n .text('')\n .each(function() {\n if (_note.isNew()) {\n t.append('note.newDescription')(d3_select(this));\n } else {\n t.append('note.newComment')(d3_select(this));\n }\n });\n\n var commentTextarea = noteSaveEnter\n .append('textarea')\n .attr('class', 'new-comment-input')\n .attr('placeholder', t('note.inputPlaceholder'))\n .attr('maxlength', 1000)\n .property('value', function(d) { return d.newComment; })\n .call(utilNoAuto)\n .on('keydown.note-input', keydown)\n .on('input.note-input', changeInput)\n .on('blur.note-input', changeInput);\n\n if (!commentTextarea.empty() && _newNote) {\n // autofocus the comment field for new notes\n commentTextarea.node().focus();\n }\n\n // update\n noteSave = noteSaveEnter\n .merge(noteSave)\n .call(userDetails)\n .call(noteSaveButtons);\n\n\n // fast submit if user presses cmd+enter\n function keydown(d3_event) {\n if (!(d3_event.keyCode === 13 && // \u21A9 Return\n d3_event.metaKey)) return;\n\n var osm = services.osm;\n if (!osm) return;\n\n var hasAuth = osm.authenticated();\n if (!hasAuth) return;\n\n if (!_note.newComment) return;\n\n d3_event.preventDefault();\n\n d3_select(this)\n .on('keydown.note-input', null);\n\n // focus on button and submit\n window.setTimeout(function() {\n if (_note.isNew()) {\n noteSave.selectAll('.save-button').node().focus();\n clickSave(_note);\n } else {\n noteSave.selectAll('.comment-button').node().focus();\n postCommentAndStatus(_note);\n }\n }, 10);\n }\n\n\n function changeInput() {\n var input = d3_select(this);\n var val = input.property('value').trim() || undefined;\n\n // store the unsaved comment with the note itself\n _note = _note.update({ newComment: val });\n\n var osm = services.osm;\n if (osm) {\n osm.replaceNote(_note); // update note cache\n }\n\n noteSave\n .call(noteSaveButtons);\n }\n }\n\n\n function userDetails(selection) {\n var detailSection = selection.selectAll('.detail-section')\n .data([0]);\n\n detailSection = detailSection.enter()\n .append('div')\n .attr('class', 'detail-section')\n .merge(detailSection);\n\n var osm = services.osm;\n if (!osm) return;\n\n // Add warning if user is not logged in\n var hasAuth = osm.authenticated();\n var authWarning = detailSection.selectAll('.auth-warning')\n .data(hasAuth ? [] : [0]);\n\n authWarning.exit()\n .transition()\n .duration(200)\n .style('opacity', 0)\n .remove();\n\n var authEnter = authWarning.enter()\n .insert('div', '.tag-reference-body')\n .attr('class', 'field-warning auth-warning')\n .style('opacity', 0);\n\n authEnter\n .call(svgIcon('#iD-icon-alert', 'inline'));\n\n authEnter\n .append('span')\n .call(t.append('note.login'));\n\n authEnter\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append('login'))\n .on('click.note-login', function(d3_event) {\n d3_event.preventDefault();\n osm.authenticate();\n });\n\n authEnter\n .transition()\n .duration(200)\n .style('opacity', 1);\n\n\n var prose = detailSection.selectAll('.note-save-prose')\n .data(hasAuth ? [0] : []);\n\n prose.exit()\n .remove();\n\n prose = prose.enter()\n .append('p')\n .attr('class', 'note-save-prose')\n .call(t.append('note.upload_explanation'))\n .merge(prose);\n\n osm.userDetails(function(err, user) {\n if (err) return;\n\n const userLink = selection => {\n if (user.image_url) {\n selection\n .append('img')\n .attr('src', user.image_url)\n .attr('class', 'icon pre-text user-icon');\n }\n\n selection\n .append('a')\n .attr('class', 'user-info')\n .text(user.display_name)\n .attr('href', osm.userURL(user.display_name))\n .attr('target', '_blank');\n };\n\n prose\n .call(t.addOrUpdate('note.upload_explanation_with_user', { user: userLink }));\n });\n }\n\n\n function noteSaveButtons(selection) {\n var osm = services.osm;\n var hasAuth = osm && osm.authenticated();\n\n var isSelected = (_note && _note.id === context.selectedNoteID());\n var buttonSection = selection.selectAll('.buttons')\n .data((isSelected ? [_note] : []), d => d.status + d.id);\n\n // exit\n buttonSection.exit()\n .remove();\n\n // enter\n var buttonEnter = buttonSection.enter()\n .append('div')\n .attr('class', 'buttons');\n\n if (_note.isNew()) {\n buttonEnter\n .append('button')\n .attr('class', 'button cancel-button secondary-action')\n .call(t.append('confirm.cancel'));\n\n buttonEnter\n .append('button')\n .attr('class', 'button save-button action')\n .call(t.append('note.save'));\n\n } else {\n buttonEnter\n .append('button')\n .attr('class', 'button status-button action');\n\n buttonEnter\n .append('button')\n .attr('class', 'button comment-button action')\n .call(t.append('note.comment'));\n }\n\n\n // update\n buttonSection = buttonSection\n .merge(buttonEnter);\n\n buttonSection.select('.cancel-button') // select and propagate data\n .on('click.cancel', clickCancel);\n\n buttonSection.select('.save-button') // select and propagate data\n .attr('disabled', isSaveDisabled)\n .on('click.save', clickSave);\n\n buttonSection.select('.status-button') // select and propagate data\n .attr('disabled', (hasAuth ? null : true))\n .each(function(d) {\n var action = (d.status === 'open' ? 'close' : 'open');\n var andComment = (d.newComment ? '_comment' : '');\n t.addOrUpdate('note.' + action + andComment)(d3_select(this));\n })\n .on('click.status', function(d3_event, note) {\n const setStatus = (note.status === 'open' ? 'closed' : 'open');\n postCommentAndStatus.bind(this)(d3_event, note, setStatus);\n });\n\n buttonSection.select('.comment-button') // select and propagate data\n .attr('disabled', isSaveDisabled)\n .on('click.comment', postCommentAndStatus);\n\n\n function isSaveDisabled(d) {\n return (hasAuth && d.status === 'open' && d.newComment) ? null : true;\n }\n }\n\n\n\n function clickCancel(d3_event, d) {\n this.blur(); // avoid keeping focus on the button - #4641\n var osm = services.osm;\n if (osm) {\n osm.removeNote(d);\n }\n context.enter(modeBrowse(context));\n dispatch.call('change', d);\n }\n\n\n function clickSave(d3_event, note) {\n this.blur(); // avoid keeping focus on the button - #4641\n var osm = services.osm;\n if (osm) {\n osm.postNoteCreate(note, (err, d) => dispatch.call('change', d));\n }\n }\n\n\n function postCommentAndStatus(d3_event, note, newStatus) {\n this.blur(); // avoid keeping focus on the button - #4641\n var osm = services.osm;\n if (osm) {\n osm.postNoteUpdate(note, newStatus || note.status, function(err, d) {\n if (!err) {\n dispatch.call('change', d);\n } else if (err.status === 409) {\n // note was probably closed in the meantime: reload it - #8464\n osm.loadEntityNote(note.id, (err, d) => {\n dispatch.call('change', osmNote({ ...d.data[0], newComment: note.newComment }));\n });\n } else if (err.status === 410) {\n // note was deleted/hidden by a moderator\n osm.removeNote(note);\n dispatch.call('change', osmNote({ id: note.id, status: 'hidden', comments: [...note.comments, { action: 'hidden' }] }));\n }\n });\n }\n }\n\n\n noteEditor.note = function(val) {\n if (!arguments.length) return _note;\n _note = val;\n return noteEditor;\n };\n\n noteEditor.newNote = function(val) {\n if (!arguments.length) return _newNote;\n _newNote = val;\n return noteEditor;\n };\n\n\n return utilRebind(noteEditor, dispatch, 'on');\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { behaviorBreathe } from '../behavior/breathe';\nimport { behaviorHover } from '../behavior/hover';\nimport { behaviorLasso } from '../behavior/lasso';\nimport { behaviorSelect } from '../behavior/select';\n\nimport { t } from '../core/localizer';\n\nimport { modeBrowse } from './browse';\nimport { modeDragNode } from './drag_node';\nimport { modeDragNote } from './drag_note';\nimport { services } from '../services';\nimport { uiNoteEditor } from '../ui/note_editor';\nimport { utilKeybinding } from '../util';\n\n\nexport function modeSelectNote(context, selectedNoteID) {\n var mode = {\n id: 'select-note',\n button: 'browse'\n };\n\n var _keybinding = utilKeybinding('select-note');\n var _noteEditor = uiNoteEditor(context)\n .on('change', function() {\n context.map().pan([0,0]); // trigger a redraw\n var note = this || checkSelectedID();\n if (!note) return;\n context.ui().sidebar\n .show(_noteEditor.note(note));\n });\n\n var _behaviors = [\n behaviorBreathe(context),\n behaviorHover(context),\n behaviorSelect(context),\n behaviorLasso(context),\n modeDragNode(context).behavior,\n modeDragNote(context).behavior\n ];\n\n var _newFeature = false;\n\n\n function checkSelectedID() {\n if (!services.osm) return;\n var note = services.osm.getNote(selectedNoteID);\n if (!note) {\n context.enter(modeBrowse(context));\n }\n return note;\n }\n\n\n // class the note as selected, or return to browse mode if the note is gone\n function selectNote(d3_event, drawn) {\n if (!checkSelectedID()) return;\n\n var selection = context.surface().selectAll('.layer-notes .note-' + selectedNoteID);\n\n if (selection.empty()) {\n // Return to browse mode if selected DOM elements have\n // disappeared because the user moved them out of view..\n var source = d3_event && d3_event.type === 'zoom' && d3_event.sourceEvent;\n if (drawn && source && (source.type === 'pointermove' || source.type === 'mousemove' || source.type === 'touchmove')) {\n context.enter(modeBrowse(context));\n }\n\n } else {\n selection\n .classed('selected', true);\n\n context.selectedNoteID(selectedNoteID);\n }\n }\n\n\n function esc() {\n if (context.container().select('.combobox').size()) return;\n context.enter(modeBrowse(context));\n }\n\n\n mode.zoomToSelected = function() {\n if (!services.osm) return;\n var note = services.osm.getNote(selectedNoteID);\n if (note) {\n context.map().centerZoomEase(note.loc, 20);\n }\n };\n\n\n mode.newFeature = function(val) {\n if (!arguments.length) return _newFeature;\n _newFeature = val;\n return mode;\n };\n\n\n mode.enter = function() {\n var note = checkSelectedID();\n if (!note) return;\n\n _behaviors.forEach(context.install);\n\n _keybinding\n .on(t('inspector.zoom_to.key'), mode.zoomToSelected)\n .on('\u238B', esc, true);\n\n d3_select(document)\n .call(_keybinding);\n\n selectNote();\n\n var sidebar = context.ui().sidebar;\n sidebar.show(_noteEditor.note(note).newNote(_newFeature));\n\n // expand the sidebar, avoid obscuring the note if needed\n sidebar.expand(sidebar.intersects(note.extent()));\n\n context.map()\n .on('drawn.select', selectNote);\n };\n\n\n mode.exit = function() {\n _behaviors.forEach(context.uninstall);\n\n d3_select(document)\n .call(_keybinding.unbind);\n\n context.surface()\n .selectAll('.layer-notes .selected')\n .classed('selected hover', false);\n\n context.map()\n .on('drawn.select', null);\n\n context.ui().sidebar\n .hide();\n\n context.selectedNoteID(null);\n };\n\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { behaviorDraw } from '../behavior/draw';\nimport { modeBrowse } from './browse';\nimport { modeSelectNote } from './select_note';\nimport { osmNote } from '../osm';\nimport { services } from '../services';\n\n\nexport function modeAddNote(context) {\n var mode = {\n id: 'add-note',\n button: 'note',\n description: t.append('modes.add_note.description'),\n key: t('modes.add_note.key')\n };\n\n var behavior = behaviorDraw(context)\n .on('click', add)\n .on('cancel', cancel)\n .on('finish', cancel);\n\n\n function add(loc) {\n var osm = services.osm;\n if (!osm) return;\n\n var note = osmNote({ loc: loc, status: 'open', comments: [] });\n osm.replaceNote(note);\n\n // force a reraw (there is no history change that would otherwise do this)\n context.map().pan([0,0]);\n\n context\n .selectedNoteID(note.id)\n .enter(modeSelectNote(context, note.id).newFeature(true));\n }\n\n\n function cancel() {\n context.enter(modeBrowse(context));\n }\n\n\n mode.enter = function() {\n context.install(behavior);\n };\n\n\n mode.exit = function() {\n context.uninstall(behavior);\n };\n\n\n return mode;\n}\n", "import { t } from '../core/localizer';\nimport { behaviorOperation } from '../behavior/operation';\nimport { modeMove } from '../modes/move';\nimport { utilGetAllNodes, utilTotalExtent } from '../util/util';\n\n\nexport function operationMove(context, selectedIDs) {\n var multi = (selectedIDs.length === 1 ? 'single' : 'multiple');\n var nodes = utilGetAllNodes(selectedIDs, context.graph());\n var coords = nodes.map(function(n) { return n.loc; });\n var extent = utilTotalExtent(selectedIDs, context.graph());\n\n\n var operation = function() {\n context.enter(modeMove(context, selectedIDs));\n };\n\n\n operation.available = function() {\n return selectedIDs.length > 0;\n };\n\n\n operation.disabled = function() {\n if (extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n } else if (selectedIDs.some(incompleteRelation)) {\n return 'incomplete_relation';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n\n function incompleteRelation(id) {\n var entity = context.entity(id);\n return entity.type === 'relation' && !entity.isComplete(context.graph());\n }\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.move.' + disable + '.' + multi) :\n t.append('operations.move.description.' + multi);\n };\n\n\n operation.annotation = function() {\n return selectedIDs.length === 1 ?\n t('operations.move.annotation.' + context.graph().geometry(selectedIDs[0])) :\n t('operations.move.annotation.feature', { n: selectedIDs.length });\n };\n\n\n operation.id = 'move';\n operation.keys = [t('operations.move.key')];\n operation.title = t.append('operations.move.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n operation.mouseOnly = true;\n\n return operation;\n}\n", "import {\n geoIdentity as d3_geoIdentity,\n geoPath as d3_geoPath,\n geoStream as d3_geoStream\n} from 'd3-geo';\n\nimport { geoVecAdd, geoVecAngle, geoVecLength } from '../geo';\n\n\n// Touch targets control which other vertices we can drag a vertex onto.\n//\n// - the activeID - nope\n// - 1 away (adjacent) to the activeID - yes (vertices will be merged)\n// - 2 away from the activeID - nope (would create a self intersecting segment)\n// - all others on a linear way - yes\n// - all others on a closed way - nope (would create a self intersecting polygon)\n//\n// returns\n// 0 = active vertex - no touch/connect\n// 1 = passive vertex - yes touch/connect\n// 2 = adjacent vertex - yes but pay attention segmenting a line here\n//\nexport function svgPassiveVertex(node, graph, activeID) {\n if (!activeID) return 1;\n if (activeID === node.id) return 0;\n\n var parents = graph.parentWays(node);\n\n var i, j, nodes, isClosed, ix1, ix2, ix3, ix4, max;\n\n for (i = 0; i < parents.length; i++) {\n nodes = parents[i].nodes;\n isClosed = parents[i].isClosed();\n for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby\n if (nodes[j] === node.id) {\n ix1 = j - 2;\n ix2 = j - 1;\n ix3 = j + 1;\n ix4 = j + 2;\n\n if (isClosed) { // wraparound if needed\n max = nodes.length - 1;\n if (ix1 < 0) ix1 = max + ix1;\n if (ix2 < 0) ix2 = max + ix2;\n if (ix3 > max) ix3 = ix3 - max;\n if (ix4 > max) ix4 = ix4 - max;\n }\n\n if (nodes[ix1] === activeID) return 0; // no - prevent self intersect\n else if (nodes[ix2] === activeID) return 2; // ok - adjacent\n else if (nodes[ix3] === activeID) return 2; // ok - adjacent\n else if (nodes[ix4] === activeID) return 0; // no - prevent self intersect\n else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // no - prevent self intersect\n }\n }\n }\n\n return 1; // ok\n}\n\n\n/**\n *\n * @param {iD.Projection} projection\n * @param {iD.Graph} graph\n * @param {Number} dt spacing between segments\n * @param {Function} [shouldReverse]\n * @param {Function} [bothDirections]\n */\nexport function svgMarkerSegments(projection, graph, dt, shouldReverse = () => false, bothDirections = () => false) {\n /**\n * @param {iD.OsmWay} entity\n * @returns {[{id: String, d: String}]} list of svg path segments corres\n */\n return function(entity) {\n let i = 0;\n let offset = dt / 2;\n const segments = [];\n\n const clip = paddedClipExtent(projection);\n\n const coordinates = graph.childNodes(entity).map(function(n) { return n.loc; });\n let a, b;\n\n const _shouldReverse = shouldReverse(entity);\n const _bothDirections = bothDirections(entity);\n\n d3_geoStream({\n type: 'LineString',\n coordinates: coordinates\n }, projection.stream(clip({\n lineStart: function() {},\n lineEnd: function() { a = null; },\n point: function(x, y) {\n b = [x, y];\n\n if (a) {\n let span = geoVecLength(a, b) - offset;\n\n if (span >= 0) {\n const heading = geoVecAngle(a, b);\n const dx = dt * Math.cos(heading);\n const dy = dt * Math.sin(heading);\n let p = [\n a[0] + offset * Math.cos(heading),\n a[1] + offset * Math.sin(heading)\n ];\n\n // gather coordinates\n const coord = [a, p];\n for (span -= dt; span >= 0; span -= dt) {\n p = geoVecAdd(p, [dx, dy]);\n coord.push(p);\n }\n coord.push(b);\n\n // generate svg paths\n let segment = '';\n\n if (!_shouldReverse || _bothDirections) {\n for (let j = 0; j < coord.length; j++) {\n segment += (j === 0 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];\n }\n segments.push({ id: entity.id, index: i++, d: segment });\n }\n\n if (_shouldReverse || _bothDirections) {\n segment = '';\n for (let j = coord.length - 1; j >= 0; j--) {\n segment += (j === coord.length - 1 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];\n }\n segments.push({ id: entity.id, index: i++, d: segment });\n }\n }\n\n offset = -span;\n }\n\n a = b;\n }\n })));\n\n return segments;\n };\n}\n\n\n/**\n * @param {iD.Projection} projection\n * @param {iD.Graph} graph\n * @param {Boolean} isArea\n */\nexport function svgPath(projection, graph, isArea) {\n const cache = {};\n const project = projection.stream;\n const clip = paddedClipExtent(projection, isArea);\n const path = d3_geoPath()\n .projection({stream: function(output) { return project(clip(output)); }});\n\n const svgpath = function(entity) {\n if (entity.id in cache) {\n return cache[entity.id];\n } else {\n return cache[entity.id] = path(entity.asGeoJSON(graph));\n }\n };\n\n svgpath.geojson = function(d) {\n if (d.__featurehash__ !== undefined) {\n if (d.__featurehash__ in cache) {\n return cache[d.__featurehash__];\n } else {\n return cache[d.__featurehash__] = path(d);\n }\n } else {\n return path(d);\n }\n };\n\n return svgpath;\n}\n\n\nexport function svgPointTransform(projection) {\n var svgpoint = function(entity) {\n // http://jsperf.com/short-array-join\n var pt = projection(entity.loc);\n return 'translate(' + pt[0] + ',' + pt[1] + ')';\n };\n\n svgpoint.geojson = function(d) {\n return svgpoint(d.properties.entity);\n };\n\n return svgpoint;\n}\n\n\nexport function svgRelationMemberTags(graph) {\n return function(entity) {\n var tags = entity.tags;\n var shouldCopyMultipolygonTags = !entity.hasInterestingTags();\n graph.parentRelations(entity).forEach(function(relation) {\n var type = relation.tags.type;\n if ((type === 'multipolygon' && shouldCopyMultipolygonTags) || type === 'boundary') {\n tags = Object.assign({}, relation.tags, tags);\n }\n });\n return tags;\n };\n}\n\n\nexport function svgSegmentWay(way, graph, activeID) {\n // When there is no activeID, we can memoize this expensive computation\n if (activeID === undefined) {\n return graph.transient(way, 'waySegments', getWaySegments);\n } else {\n return getWaySegments();\n }\n\n function getWaySegments() {\n const isActiveWay = (way.nodes.indexOf(activeID) !== -1);\n const features = { passive: [], active: [] };\n let start = {};\n\n for (var i = 0; i < way.nodes.length; i++) {\n const node = graph.entity(way.nodes[i]);\n const type = svgPassiveVertex(node, graph, activeID);\n const end = { node: node, type: type };\n\n if (start.type !== undefined) {\n if (start.node.id === activeID || end.node.id === activeID) {\n // push nothing\n } else if (isActiveWay && (start.type === 2 || end.type === 2)) { // one adjacent vertex\n pushActive(start, end, i);\n } else if (start.type === 0 && end.type === 0) { // both active vertices\n pushActive(start, end, i);\n } else {\n pushPassive(start, end, i);\n }\n }\n\n start = end;\n }\n\n return features;\n\n function pushActive(start, end, index) {\n features.active.push({\n type: 'Feature',\n id: way.id + '-' + index + '-nope',\n properties: {\n nope: true,\n target: true,\n entity: way,\n nodes: [start.node, end.node],\n index: index\n },\n geometry: {\n type: 'LineString',\n coordinates: [start.node.loc, end.node.loc]\n }\n });\n }\n\n function pushPassive(start, end, index) {\n features.passive.push({\n type: 'Feature',\n id: way.id + '-' + index,\n properties: {\n target: true,\n entity: way,\n nodes: [start.node, end.node],\n index: index\n },\n geometry: {\n type: 'LineString',\n coordinates: [start.node.loc, end.node.loc]\n }\n });\n }\n }\n}\n\n\n/**\n * Returns a d3 projection stream that clips the given geometries to an\n * extent that is slightly padded.\n *\n * Explanation of magic numbers:\n * \"padding\" here allows space for strokes to extend beyond the viewport,\n * so that the stroke isn't drawn along the edge of the viewport when\n * the shape is clipped.\n * When drawing lines, pad viewport by 5px.\n * When drawing areas, pad viewport by 65px in each direction to allow\n * for 60px area fill stroke (see \".fill-partial path.fill\" css rule)\n *\n * @param {import('../geo/raw_mercator').Projection} projection\n * @param {Boolean} isArea\n */\nfunction paddedClipExtent(projection, isArea = false) {\n var padding = isArea ? 65 : 5;\n var viewport = projection.clipExtent();\n var paddedExtent = [\n [viewport[0][0] - padding, viewport[0][1] - padding],\n [viewport[1][0] + padding, viewport[1][1] + padding]\n ];\n return d3_geoIdentity().clipExtent(paddedExtent).stream;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { osmPathHighwayTagValues, osmPavedTags, osmSemipavedTags, osmLifecyclePrefixes } from '../osm/tags';\n\n\nexport function svgTagClasses() {\n var primaries = [\n 'building', 'highway', 'railway', 'waterway', 'aeroway', 'aerialway',\n 'piste:type', 'boundary', 'power', 'amenity', 'natural', 'landuse',\n 'leisure', 'military', 'place', 'man_made', 'route', 'attraction',\n 'roller_coaster', 'building:part', 'indoor', 'climbing'\n ];\n var statuses = Object.keys(osmLifecyclePrefixes);\n var secondaries = [\n 'oneway', 'bridge', 'tunnel', 'embankment', 'cutting', 'barrier',\n 'surface', 'tracktype', 'footway', 'crossing', 'service', 'sport',\n 'public_transport', 'location', 'parking', 'golf', 'type', 'leisure',\n 'man_made', 'indoor', 'construction', 'proposed'\n ];\n var _tags = function(entity) { return entity.tags; };\n\n\n var tagClasses = function(selection) {\n selection.each(function tagClassesEach(entity) {\n var value = this.className;\n\n if (value.baseVal !== undefined) {\n value = value.baseVal;\n }\n\n var t = _tags(entity);\n\n var computed = tagClasses.getClassesString(t, value);\n\n if (computed !== value) {\n d3_select(this).attr('class', computed);\n }\n });\n };\n\n\n tagClasses.getClassesString = function(t, value) {\n var primary, status;\n var i, j, k, v;\n\n // in some situations we want to render perimeter strokes a certain way\n var overrideGeometry;\n if (/\\bstroke\\b/.test(value)) {\n if (!!t.barrier && t.barrier !== 'no') {\n overrideGeometry = 'line';\n }\n }\n\n // preserve base classes (nothing with `tag-`)\n var classes = value.trim().split(/\\s+/)\n .filter(function(klass) {\n return klass.length && !/^tag-/.test(klass);\n })\n .map(function(klass) { // special overrides for some perimeter strokes\n return (klass === 'line' || klass === 'area') ? (overrideGeometry || klass) : klass;\n });\n\n // pick at most one primary classification tag..\n for (i = 0; i < primaries.length; i++) {\n k = primaries[i];\n v = t[k];\n if (!v || v === 'no') continue;\n\n if (k === 'piste:type') { // avoid a ':' in the class name\n k = 'piste';\n } else if (k === 'building:part') { // avoid a ':' in the class name\n k = 'building_part';\n }\n\n primary = k;\n if (statuses.indexOf(v) !== -1) { // e.g. `railway=abandoned`\n status = v;\n classes.push('tag-' + k);\n } else {\n classes.push('tag-' + k);\n classes.push('tag-' + k + '-' + v);\n }\n\n break;\n }\n\n if (!primary) {\n for (i = 0; i < statuses.length; i++) {\n for (j = 0; j < primaries.length; j++) {\n k = statuses[i] + ':' + primaries[j]; // e.g. `demolished:building=yes`\n v = t[k];\n if (!v || v === 'no') continue;\n\n status = statuses[i];\n break;\n }\n }\n }\n\n // add at most one status tag, only if relates to primary tag..\n if (!status) {\n for (i = 0; i < statuses.length; i++) {\n k = statuses[i];\n v = t[k];\n if (!v || v === 'no') continue;\n\n if (v === 'yes') { // e.g. `railway=rail + abandoned=yes`\n status = k;\n } else if (primary && primary === v) { // e.g. `railway=rail + abandoned=railway`\n status = k;\n } else if (!primary && primaries.indexOf(v) !== -1) { // e.g. `abandoned=railway`\n status = k;\n primary = v;\n classes.push('tag-' + v);\n } // else ignore e.g. `highway=path + abandoned=railway`\n\n if (status) break;\n }\n }\n\n if (status) {\n classes.push('tag-status');\n classes.push('tag-status-' + status);\n }\n\n // add any secondary tags\n for (i = 0; i < secondaries.length; i++) {\n k = secondaries[i];\n v = t[k];\n if (!v || v === 'no' || k === primary) continue;\n classes.push('tag-' + k);\n classes.push('tag-' + k + '-' + v);\n }\n\n // For highways, look for surface tagging..\n if ((primary === 'highway' && !osmPathHighwayTagValues[t.highway]) || primary === 'aeroway') {\n var surface = t.highway === 'track' ? 'unpaved' : 'paved';\n for (k in t) {\n v = t[k];\n if (k in osmPavedTags) {\n surface = osmPavedTags[k][v] ? 'paved' : 'unpaved';\n }\n if (k in osmSemipavedTags && !!osmSemipavedTags[k][v]) {\n surface = 'semipaved';\n }\n }\n classes.push('tag-' + surface);\n }\n\n // If this is a wikidata-tagged item, add a class for that..\n var qid = (\n t.wikidata ||\n t['flag:wikidata'] ||\n t['brand:wikidata'] ||\n t['network:wikidata'] ||\n t['operator:wikidata']\n );\n\n if (qid) {\n classes.push('tag-wikidata');\n }\n\n // ensure that classes for tags keys/values with special characters like spaces\n // are not added to the DOM, because it can cause bizarre issues (#9448)\n return classes\n .filter(klass => /^[-_a-z0-9]+$/.test(klass))\n .join(' ')\n .trim();\n };\n\n\n tagClasses.tags = function(val) {\n if (!arguments.length) return _tags;\n _tags = val;\n return tagClasses;\n };\n\n return tagClasses;\n}\n", "// Patterns only work in Firefox when set directly on element.\n// (This is not a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=750632)\nvar patterns = {\n // tag - pattern name\n // -or-\n // tag - value - pattern name\n // -or-\n // tag - value - rules (optional tag-values, pattern name)\n // (matches earlier rules first, so fallback should be last entry)\n amenity: {\n grave_yard: 'cemetery',\n fountain: 'water_standing'\n },\n landuse: {\n cemetery: [\n { religion: 'christian', pattern: 'cemetery_christian' },\n { religion: 'buddhist', pattern: 'cemetery_buddhist' },\n { religion: 'muslim', pattern: 'cemetery_muslim' },\n { religion: 'jewish', pattern: 'cemetery_jewish' },\n { pattern: 'cemetery' }\n ],\n construction: 'construction',\n farmland: 'farmland',\n farmyard: 'farmyard',\n forest: [\n { leaf_type: 'broadleaved', pattern: 'forest_broadleaved' },\n { leaf_type: 'needleleaved', pattern: 'forest_needleleaved' },\n { leaf_type: 'leafless', pattern: 'forest_leafless' },\n { pattern: 'forest' } // same as 'leaf_type:mixed'\n ],\n grave_yard: 'cemetery',\n grass: 'grass',\n landfill: 'landfill',\n meadow: 'meadow',\n military: 'construction',\n orchard: 'orchard',\n quarry: 'quarry',\n vineyard: 'vineyard'\n },\n leisure: {\n horse_riding: 'farmyard'\n },\n natural: {\n beach: 'beach',\n grassland: 'grass',\n sand: 'beach',\n scrub: 'scrub',\n water: [\n { water: 'pond', pattern: 'pond' },\n { water: 'reservoir', pattern: 'water_standing' },\n { pattern: 'waves' }\n ],\n wetland: [\n { wetland: 'marsh', pattern: 'wetland_marsh' },\n { wetland: 'swamp', pattern: 'wetland_swamp' },\n { wetland: 'bog', pattern: 'wetland_bog' },\n { wetland: 'reedbed', pattern: 'wetland_reedbed' },\n { pattern: 'wetland' }\n ],\n wood: [\n { leaf_type: 'broadleaved', pattern: 'forest_broadleaved' },\n { leaf_type: 'needleleaved', pattern: 'forest_needleleaved' },\n { leaf_type: 'leafless', pattern: 'forest_leafless' },\n { pattern: 'forest' } // same as 'leaf_type:mixed'\n ]\n },\n golf: {\n green: 'golf_green',\n tee: 'grass',\n fairway: 'grass',\n rough: 'scrub'\n },\n surface: {\n grass: 'grass',\n sand: 'beach'\n }\n};\n\nexport function svgTagPattern(tags) {\n // Skip pattern filling if this is a building (buildings don't get patterns applied)\n if (tags.building && tags.building !== 'no') {\n return null;\n }\n\n for (var tag in patterns) {\n var entityValue = tags[tag];\n if (!entityValue) continue;\n\n if (typeof patterns[tag] === 'string') { // extra short syntax (just tag) - pattern name\n return 'pattern-' + patterns[tag];\n } else {\n var values = patterns[tag];\n for (var value in values) {\n if (entityValue !== value) continue;\n\n var rules = values[value];\n if (typeof rules === 'string') { // short syntax - pattern name\n return 'pattern-' + rules;\n }\n\n // long syntax - rule array\n for (var ruleKey in rules) {\n var rule = rules[ruleKey];\n\n var pass = true;\n for (var criterion in rule) {\n if (criterion !== 'pattern') { // reserved for pattern name\n // The only rule is a required tag-value pair\n var v = tags[criterion];\n if (!v || v !== rule[criterion]) {\n pass = false;\n break;\n }\n }\n }\n\n if (pass) {\n return 'pattern-' + rule.pattern;\n }\n }\n }\n }\n }\n\n return null;\n}\n", "import { deepEqual } from 'fast-equals';\nimport { bisector as d3_bisector } from 'd3-array';\n\nimport { osmEntity } from '../osm';\nimport { svgPath, svgSegmentWay } from './helpers';\nimport { svgTagClasses } from './tag_classes';\nimport { svgTagPattern } from './tag_pattern';\n\nexport function svgAreas(projection, context) {\n\n\n function getPatternStyle(tags) {\n var imageID = svgTagPattern(tags);\n if (imageID) {\n return 'url(\"#ideditor-' + imageID + '\")';\n }\n return '';\n }\n\n\n function drawTargets(selection, graph, entities, filter) {\n var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';\n var getPath = svgPath(projection).geojson;\n var activeID = context.activeID();\n var base = context.history().base();\n\n // The targets and nopes will be MultiLineString sub-segments of the ways\n var data = { targets: [], nopes: [] };\n\n entities.forEach(function(way) {\n var features = svgSegmentWay(way, graph, activeID);\n data.targets.push.apply(data.targets, features.passive);\n data.nopes.push.apply(data.nopes, features.active);\n });\n\n\n // Targets allow hover and vertex snapping\n var targetData = data.targets.filter(getPath);\n var targets = selection.selectAll('.area.target-allowed')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(targetData, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n var segmentWasEdited = function(d) {\n var wayID = d.properties.entity.id;\n // if the whole line was edited, don't draw segment changes\n if (!base.entities[wayID] ||\n !deepEqual(graph.entities[wayID].nodes, base.entities[wayID].nodes)) {\n return false;\n }\n return d.properties.nodes.some(function(n) {\n return !base.entities[n.id] ||\n !deepEqual(graph.entities[n.id].loc, base.entities[n.id].loc);\n });\n };\n\n // enter/update\n targets.enter()\n .append('path')\n .merge(targets)\n .attr('d', getPath)\n .attr('class', function(d) { return 'way area target target-allowed ' + targetClass + d.id; })\n .classed('segment-edited', segmentWasEdited);\n\n\n // NOPE\n var nopeData = data.nopes.filter(getPath);\n var nopes = selection.selectAll('.area.target-nope')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(nopeData, function key(d) { return d.id; });\n\n // exit\n nopes.exit()\n .remove();\n\n // enter/update\n nopes.enter()\n .append('path')\n .merge(nopes)\n .attr('d', getPath)\n .attr('class', function(d) { return 'way area target target-nope ' + nopeClass + d.id; })\n .classed('segment-edited', segmentWasEdited);\n }\n\n\n function drawAreas(selection, graph, entities, filter) {\n var path = svgPath(projection, graph, true);\n var areas = {};\n var base = context.history().base();\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n if (entity.geometry(graph) !== 'area') continue;\n if (!areas[entity.id]) {\n areas[entity.id] = {\n entity: entity,\n area: Math.abs(entity.area(graph))\n };\n }\n }\n\n var fills = Object.values(areas).filter(function hasPath(a) { return path(a.entity); });\n fills.sort(function areaSort(a, b) { return b.area - a.area; });\n fills = fills.map(function(a) { return a.entity; });\n\n var strokes = fills.filter(function(area) { return area.type === 'way'; });\n\n var data = {\n clip: fills,\n shadow: strokes,\n stroke: strokes,\n fill: fills\n };\n\n var clipPaths = context.surface().selectAll('defs').selectAll('.clipPath-osm')\n .filter(filter)\n .data(data.clip, osmEntity.key);\n\n clipPaths.exit()\n .remove();\n\n var clipPathsEnter = clipPaths.enter()\n .append('clipPath')\n .attr('class', 'clipPath-osm')\n .attr('id', function(entity) { return 'ideditor-' + entity.id + '-clippath'; });\n\n clipPathsEnter\n .append('path');\n\n clipPaths.merge(clipPathsEnter)\n .selectAll('path')\n .attr('d', path);\n\n\n var drawLayer = selection.selectAll('.layer-osm.areas');\n var touchLayer = selection.selectAll('.layer-touch.areas');\n\n // Draw areas..\n var areagroup = drawLayer\n .selectAll('g.areagroup')\n .data(['fill', 'shadow', 'stroke']);\n\n areagroup = areagroup.enter()\n .append('g')\n .attr('class', function(d) { return 'areagroup area-' + d; })\n .merge(areagroup);\n\n var paths = areagroup\n .selectAll('path')\n .filter(filter)\n .data(function(layer) { return data[layer]; }, osmEntity.key);\n\n paths.exit()\n .remove();\n\n\n var fillpaths = selection.selectAll('.area-fill path.area').nodes();\n var bisect = d3_bisector(function(node) { return -node.__data__.area(graph); }).left;\n\n function sortedByArea(entity) {\n if (this._parent.__data__ === 'fill') {\n return fillpaths[bisect(fillpaths, -entity.area(graph))];\n }\n }\n\n paths.enter()\n .insert('path', sortedByArea)\n .merge(paths)\n .each(function(entity) {\n var layer = this.parentNode.__data__;\n this.setAttribute('class', entity.type + ' area ' + layer + ' ' + entity.id);\n\n if (layer === 'fill') {\n this.setAttribute('clip-path', 'url(#ideditor-' + entity.id + '-clippath)');\n this.style.fill = getPatternStyle(entity.tags);\n this.style.stroke = this.style.fill;\n }\n })\n .classed('added', function(d) {\n return !base.entities[d.id];\n })\n .classed('geometry-edited', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].nodes, base.entities[d.id].nodes);\n })\n .classed('retagged', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses())\n .attr('d', path);\n\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, data.stroke, filter);\n }\n\n return drawAreas;\n}\n", "import type { Feature, Geometry } from \"geojson\";\n\nexport function $(element: Element | Document, tagName: string): Element[] {\n return Array.from(element.getElementsByTagName(tagName));\n}\n\nexport type P = NonNullable;\nexport type F = Feature;\n\nexport type StyleMap = { [key: string]: P };\n\nexport type NS = [string, string][];\n\nexport function normalizeId(id: string) {\n return id[0] === \"#\" ? id : `#${id}`;\n}\n\nexport function $ns(\n element: Element | Document,\n tagName: string,\n ns: string\n): Element[] {\n return Array.from(element.getElementsByTagNameNS(ns, tagName));\n}\n\n/**\n * get the content of a text node, if any\n */\nexport function nodeVal(node: Element | null) {\n node?.normalize();\n return node?.textContent || \"\";\n}\n\n/**\n * Get one Y child of X, if any, otherwise null\n */\nexport function get1(\n node: Element,\n tagName: string,\n callback?: (elem: Element) => unknown\n) {\n const n = node.getElementsByTagName(tagName);\n const result = n.length ? n[0] : null;\n if (result && callback) callback(result);\n return result;\n}\n\nexport function get(\n node: Element | null,\n tagName: string,\n callback?: (elem: Element, properties: P) => P\n) {\n const properties: Feature[\"properties\"] = {};\n if (!node) return properties;\n const n = node.getElementsByTagName(tagName);\n const result = n.length ? n[0] : null;\n if (result && callback) {\n return callback(result, properties);\n }\n return properties;\n}\n\nexport function val1(\n node: Element,\n tagName: string,\n callback: (val: string) => P | undefined | void\n): P {\n const val = nodeVal(get1(node, tagName));\n if (val && callback) return callback(val) || {};\n return {};\n}\n\nexport function $num(\n node: Element,\n tagName: string,\n callback: (val: number) => Feature[\"properties\"]\n) {\n const val = Number.parseFloat(nodeVal(get1(node, tagName)));\n if (Number.isNaN(val)) return undefined;\n if (val && callback) return callback(val) || {};\n return {};\n}\n\nexport function num1(\n node: Element,\n tagName: string,\n callback?: (val: number) => unknown\n) {\n const val = Number.parseFloat(nodeVal(get1(node, tagName)));\n if (Number.isNaN(val)) return undefined;\n if (callback) callback(val);\n return val;\n}\n\nexport function getMulti(node: Element, propertyNames: string[]): P {\n const properties: P = {};\n for (const property of propertyNames) {\n val1(node, property, (val) => {\n properties[property] = val;\n });\n }\n return properties;\n}\n\nexport function isElement(node: Node | null): node is Element {\n return node?.nodeType === 1;\n}\n", "import { isElement, nodeVal } from \"../shared\";\n\nexport type ExtendedValues = [string, string | number][];\n\nexport function getExtensions(node: Element | null): ExtendedValues {\n let values: [string, string | number][] = [];\n if (node === null) return values;\n for (const child of Array.from(node.childNodes)) {\n if (!isElement(child)) continue;\n const name = abbreviateName(child.nodeName);\n if (name === \"gpxtpx:TrackPointExtension\") {\n // loop again for nested garmin extensions (eg. \"gpxtpx:hr\")\n values = values.concat(getExtensions(child));\n } else {\n // push custom extension (eg. \"power\")\n const val = nodeVal(child);\n values.push([name, parseNumeric(val)]);\n }\n }\n return values;\n}\n\nfunction abbreviateName(name: string) {\n return [\"heart\", \"gpxtpx:hr\", \"hr\"].includes(name) ? \"heart\" : name;\n}\n\nfunction parseNumeric(val: string) {\n const num = Number.parseFloat(val);\n return Number.isNaN(num) ? val : num;\n}\n", "import type { Position } from \"geojson\";\nimport { get1, nodeVal, num1 } from \"../shared\";\nimport { type ExtendedValues, getExtensions } from \"./extensions\";\n\ninterface CoordPair {\n coordinates: Position;\n time: string | null;\n extendedValues: ExtendedValues;\n}\n\nexport function coordPair(node: Element): CoordPair | null {\n const ll = [\n Number.parseFloat(node.getAttribute(\"lon\") || \"\"),\n Number.parseFloat(node.getAttribute(\"lat\") || \"\"),\n ];\n\n if (Number.isNaN(ll[0]) || Number.isNaN(ll[1])) {\n return null;\n }\n\n num1(node, \"ele\", (val) => {\n ll.push(val);\n });\n\n const time = get1(node, \"time\");\n return {\n coordinates: ll,\n time: time ? nodeVal(time) : null,\n extendedValues: getExtensions(get1(node, \"extensions\")),\n };\n}\n", "import { $num, type P, get, val1 } from \"../shared\";\n\nexport function getLineStyle(node: Element | null) {\n return get(node, \"line\", (lineStyle) => {\n const val: P = Object.assign(\n {},\n val1(lineStyle, \"color\", (color) => {\n return { stroke: `#${color}` };\n }),\n $num(lineStyle, \"opacity\", (opacity) => {\n return { \"stroke-opacity\": opacity };\n }),\n $num(lineStyle, \"width\", (width) => {\n // GPX width is in mm, convert to px with 96 px per inch\n return { \"stroke-width\": (width * 96) / 25.4 };\n })\n );\n return val;\n });\n}\n", "import { $, type NS, getMulti, nodeVal } from \"../shared\";\n\nexport function extractProperties(ns: NS, node: Element) {\n const properties = getMulti(node, [\n \"name\",\n \"cmt\",\n \"desc\",\n \"type\",\n \"time\",\n \"keywords\",\n ]);\n\n for (const [n, url] of ns) {\n for (const child of Array.from(node.getElementsByTagNameNS(url, \"*\"))) {\n properties[child.tagName.replace(\":\", \"_\")] = nodeVal(child)?.trim();\n }\n }\n\n const links = $(node, \"link\");\n if (links.length) {\n properties.links = links.map((link) =>\n Object.assign(\n { href: link.getAttribute(\"href\") },\n getMulti(link, [\"text\", \"type\"])\n )\n );\n }\n\n return properties;\n}\n", "import type { Document as XDocument } from \"@xmldom/xmldom\";\nimport type {\n Feature,\n FeatureCollection,\n LineString,\n MultiLineString,\n Point,\n Position,\n} from \"geojson\";\nimport { coordPair } from \"./gpx/coord_pair\";\nimport { getLineStyle } from \"./gpx/line\";\nimport { extractProperties } from \"./gpx/properties\";\nimport { $, type NS, type P, get1, getMulti } from \"./shared\";\n\n/**\n * Extract points from a trkseg or rte element.\n */\nfunction getPoints(node: Element, pointname: \"trkpt\" | \"rtept\") {\n const pts = $(node, pointname);\n const line: Position[] = [];\n const times = [];\n const extendedValues: P = {};\n\n for (let i = 0; i < pts.length; i++) {\n const c = coordPair(pts[i]);\n if (!c) {\n continue;\n }\n line.push(c.coordinates);\n if (c.time) times.push(c.time);\n for (const [name, val] of c.extendedValues) {\n const plural =\n name === \"heart\" ? name : `${name.replace(\"gpxtpx:\", \"\")}s`;\n if (!extendedValues[plural]) {\n extendedValues[plural] = Array(pts.length).fill(null);\n }\n extendedValues[plural][i] = val;\n }\n }\n\n if (line.length < 2) return; // Invalid line in GeoJSON\n\n return {\n line: line,\n times: times,\n extendedValues: extendedValues,\n };\n}\n\n/**\n * Extract a LineString geometry from a rte\n * element.\n */\nfunction getRoute(ns: NS, node: Element): Feature | undefined {\n const line = getPoints(node, \"rtept\");\n if (!line) return;\n return {\n type: \"Feature\",\n properties: Object.assign(\n { _gpxType: \"rte\" },\n extractProperties(ns, node),\n getLineStyle(get1(node, \"extensions\"))\n ),\n geometry: {\n type: \"LineString\",\n coordinates: line.line,\n },\n };\n}\n\nfunction getTrack(\n ns: NS,\n node: Element\n): Feature | null {\n const segments = $(node, \"trkseg\");\n const track = [];\n const times = [];\n const extractedLines = [];\n\n for (const segment of segments) {\n const line = getPoints(segment, \"trkpt\");\n if (line) {\n extractedLines.push(line);\n if (line.times?.length) times.push(line.times);\n }\n }\n\n if (extractedLines.length === 0) return null;\n\n const multi = extractedLines.length > 1;\n\n const properties: Feature[\"properties\"] = Object.assign(\n { _gpxType: \"trk\" },\n extractProperties(ns, node),\n getLineStyle(get1(node, \"extensions\")),\n times.length\n ? {\n coordinateProperties: {\n times: multi ? times : times[0],\n },\n }\n : {}\n );\n\n for (let i = 0; i < extractedLines.length; i++) {\n const line = extractedLines[i];\n track.push(line.line);\n if (!properties.coordinateProperties) {\n properties.coordinateProperties = {};\n }\n const props = properties.coordinateProperties;\n // Generally extendedValues will be things like heart\n // rate, and this is an array like { heart: [100, 101...] }\n for (const [name, val] of Object.entries(line.extendedValues)) {\n if (multi) {\n if (!props[name]) {\n props[name] = extractedLines.map((line) =>\n new Array(line.line.length).fill(null)\n );\n }\n props[name][i] = val;\n } else {\n props[name] = val;\n }\n }\n }\n\n return {\n type: \"Feature\",\n properties: properties,\n geometry: multi\n ? {\n type: \"MultiLineString\",\n coordinates: track,\n }\n : {\n type: \"LineString\",\n coordinates: track[0],\n },\n };\n}\n\n/**\n * Extract a point, if possible, from a given node,\n * which is usually a wpt or trkpt\n */\nfunction getPoint(ns: NS, node: Element): Feature | null {\n const properties: Feature[\"properties\"] = Object.assign(\n extractProperties(ns, node),\n getMulti(node, [\"sym\"])\n );\n const pair = coordPair(node);\n if (!pair) return null;\n return {\n type: \"Feature\",\n properties,\n geometry: {\n type: \"Point\",\n coordinates: pair.coordinates,\n },\n };\n}\n\n/**\n * Convert GPX to GeoJSON incrementally, returning\n * a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)\n * that yields output feature by feature.\n */\nexport function* gpxGen(node: Document | XDocument): Generator {\n const n = node as Document;\n const GPXX = \"gpxx\";\n const GPXX_URI = \"http://www.garmin.com/xmlschemas/GpxExtensions/v3\";\n // Namespaces\n const ns: NS = [[GPXX, GPXX_URI]];\n const attrs = n.getElementsByTagName(\"gpx\")[0]?.attributes;\n if (attrs) {\n for (const attr of Array.from(attrs)) {\n if (attr.name?.startsWith(\"xmlns:\") && attr.value !== GPXX_URI) {\n ns.push([attr.name, attr.value]);\n }\n }\n }\n\n for (const track of $(n, \"trk\")) {\n const feature = getTrack(ns, track);\n if (feature) yield feature;\n }\n\n for (const route of $(n, \"rte\")) {\n const feature = getRoute(ns, route);\n if (feature) yield feature;\n }\n\n for (const waypoint of $(n, \"wpt\")) {\n const point = getPoint(ns, waypoint);\n if (point) yield point;\n }\n}\n\n/**\n *\n * Convert a GPX document to GeoJSON. The first argument, `doc`, must be a GPX\n * document as an XML DOM - not as a string. You can get this using jQuery's default\n * `.ajax` function or using a bare XMLHttpRequest with the `.response` property\n * holding an XML DOM.\n *\n * The output is a JavaScript object of GeoJSON data, same as `.kml` outputs, with the\n * addition of a `_gpxType` property on each `LineString` feature that indicates whether\n * the feature was encoded as a route (`rte`) or track (`trk`) in the GPX document.\n */\nexport function gpx(node: Document | XDocument): FeatureCollection {\n return {\n type: \"FeatureCollection\",\n features: Array.from(gpxGen(node)),\n };\n}\n", "import type { Document as XDocument } from \"@xmldom/xmldom\";\nimport type { Feature, FeatureCollection, Position } from \"geojson\";\nimport { $, type P, get, get1, nodeVal, num1 } from \"./shared\";\n\ntype PropertyMapping = readonly [string, string][];\n\nconst EXTENSIONS_NS = \"http://www.garmin.com/xmlschemas/ActivityExtension/v2\";\n\nconst TRACKPOINT_ATTRIBUTES: PropertyMapping = [\n [\"heartRate\", \"heartRates\"],\n [\"Cadence\", \"cadences\"],\n // Extended Trackpoint attributes\n [\"Speed\", \"speeds\"],\n [\"Watts\", \"watts\"],\n];\n\nconst LAP_ATTRIBUTES: PropertyMapping = [\n [\"TotalTimeSeconds\", \"totalTimeSeconds\"],\n [\"DistanceMeters\", \"distanceMeters\"],\n [\"MaximumSpeed\", \"maxSpeed\"],\n [\"AverageHeartRateBpm\", \"avgHeartRate\"],\n [\"MaximumHeartRateBpm\", \"maxHeartRate\"],\n\n // Extended Lap attributes\n [\"AvgSpeed\", \"avgSpeed\"],\n [\"AvgWatts\", \"avgWatts\"],\n [\"MaxWatts\", \"maxWatts\"],\n];\n\nfunction getProperties(node: Element, attributeNames: PropertyMapping) {\n const properties = [];\n\n for (const [tag, alias] of attributeNames) {\n let elem = get1(node, tag);\n if (!elem) {\n const elements = node.getElementsByTagNameNS(EXTENSIONS_NS, tag);\n if (elements.length) {\n elem = elements[0];\n }\n }\n const val = Number.parseFloat(nodeVal(elem));\n if (!Number.isNaN(val)) {\n properties.push([alias, val]);\n }\n }\n\n return properties;\n}\n\nfunction coordPair(node: Element) {\n const ll = [num1(node, \"LongitudeDegrees\"), num1(node, \"LatitudeDegrees\")];\n if (\n ll[0] === undefined ||\n Number.isNaN(ll[0]) ||\n ll[1] === undefined ||\n Number.isNaN(ll[1])\n ) {\n return null;\n }\n const heartRate = get1(node, \"HeartRateBpm\");\n const time = nodeVal(get1(node, \"Time\"));\n get1(node, \"AltitudeMeters\", (alt) => {\n const a = Number.parseFloat(nodeVal(alt));\n if (!Number.isNaN(a)) {\n ll.push(a);\n }\n });\n return {\n coordinates: ll as number[],\n time: time || null,\n heartRate: heartRate ? Number.parseFloat(nodeVal(heartRate)) : null,\n extensions: getProperties(node, TRACKPOINT_ATTRIBUTES),\n };\n}\n\nfunction getPoints(node: Element) {\n const pts = $(node, \"Trackpoint\");\n const line: Position[] = [];\n const times = [];\n const heartRates = [];\n if (pts.length < 2) return null; // Invalid line in GeoJSON\n const extendedProperties: P = {};\n const result = { extendedProperties };\n for (let i = 0; i < pts.length; i++) {\n const c = coordPair(pts[i]);\n if (c === null) continue;\n line.push(c.coordinates);\n const { time, heartRate, extensions } = c;\n if (time) times.push(time);\n if (heartRate) heartRates.push(heartRate);\n for (const [alias, value] of extensions) {\n if (!extendedProperties[alias]) {\n extendedProperties[alias] = Array(pts.length).fill(null);\n }\n extendedProperties[alias][i] = value;\n }\n }\n if (line.length < 2) return null;\n return Object.assign(result, {\n line: line,\n times: times,\n heartRates: heartRates,\n });\n}\n\nfunction getLap(node: Element): Feature | null {\n const segments = $(node, \"Track\");\n const track = [];\n const times = [];\n const heartRates = [];\n const allExtendedProperties = [];\n let line: any;\n const properties: P = Object.assign(\n Object.fromEntries(getProperties(node, LAP_ATTRIBUTES)),\n get(node, \"Name\", (nameElement) => {\n return { name: nodeVal(nameElement) };\n })\n );\n\n for (const segment of segments) {\n line = getPoints(segment);\n if (line) {\n track.push(line.line);\n if (line.times.length) times.push(line.times);\n if (line.heartRates.length) heartRates.push(line.heartRates);\n allExtendedProperties.push(line.extendedProperties);\n }\n }\n for (let i = 0; i < allExtendedProperties.length; i++) {\n const extendedProperties = allExtendedProperties[i];\n for (const property in extendedProperties) {\n if (segments.length === 1) {\n if (line) {\n properties[property] = line.extendedProperties[property];\n }\n } else {\n if (!properties[property]) {\n properties[property] = track.map((track) =>\n Array(track.length).fill(null)\n );\n }\n properties[property][i] = extendedProperties[property];\n }\n }\n }\n\n if (track.length === 0) return null;\n\n if (times.length || heartRates.length) {\n properties.coordinateProperties = Object.assign(\n times.length\n ? {\n times: track.length === 1 ? times[0] : times,\n }\n : {},\n heartRates.length\n ? {\n heart: track.length === 1 ? heartRates[0] : heartRates,\n }\n : {}\n );\n }\n\n return {\n type: \"Feature\",\n properties: properties,\n geometry:\n track.length === 1\n ? {\n type: \"LineString\",\n coordinates: track[0],\n }\n : {\n type: \"MultiLineString\",\n coordinates: track,\n },\n };\n}\n\n/**\n * Incrementally convert a TCX document to GeoJSON. The\n * first argument, `doc`, must be a TCX\n * document as an XML DOM - not as a string.\n */\nexport function* tcxGen(node: Document | XDocument): Generator {\n for (const lap of $(node as Document, \"Lap\")) {\n const feature = getLap(lap);\n if (feature) yield feature;\n }\n\n for (const course of $(node as Document, \"Courses\")) {\n const feature = getLap(course);\n if (feature) yield feature;\n }\n}\n\n/**\n * Convert a TCX document to GeoJSON. The first argument, `doc`, must be a TCX\n * document as an XML DOM - not as a string.\n */\nexport function tcx(node: Document | XDocument): FeatureCollection {\n return {\n type: \"FeatureCollection\",\n features: Array.from(tcxGen(node)),\n };\n}\n", "import type { P } from \"../shared\";\n\nexport function fixColor(v: string, prefix: string): P {\n const properties: P = {};\n const colorProp =\n prefix === \"stroke\" || prefix === \"fill\" ? prefix : `${prefix}-color`;\n if (v[0] === \"#\") {\n v = v.substring(1);\n }\n if (v.length === 6 || v.length === 3) {\n properties[colorProp] = `#${v}`;\n } else if (v.length === 8) {\n properties[`${prefix}-opacity`] =\n Number.parseInt(v.substring(0, 2), 16) / 255;\n properties[colorProp] =\n `#${v.substring(6, 8)}${v.substring(4, 6)}${v.substring(2, 4)}`;\n }\n return properties;\n}\n", "import { type P, get, nodeVal, num1, val1 } from \"../shared\";\nimport { fixColor } from \"./fixColor\";\n\nfunction numericProperty(node: Element, source: string, target: string): P {\n const properties: P = {};\n num1(node, source, (val) => {\n properties[target] = val;\n });\n return properties;\n}\n\nfunction getColor(node: Element, output: string): P {\n return get(node, \"color\", (elem) => fixColor(nodeVal(elem), output));\n}\n\nexport function extractIconHref(node: Element) {\n return get(node, \"Icon\", (icon, properties) => {\n val1(icon, \"href\", (href) => {\n properties.icon = href;\n });\n return properties;\n });\n}\n\nexport function extractIcon(node: Element) {\n return get(node, \"IconStyle\", (iconStyle) => {\n return Object.assign(\n getColor(iconStyle, \"icon\"),\n numericProperty(iconStyle, \"scale\", \"icon-scale\"),\n numericProperty(iconStyle, \"heading\", \"icon-heading\"),\n get(iconStyle, \"hotSpot\", (hotspot) => {\n const left = Number.parseFloat(hotspot.getAttribute(\"x\") || \"\");\n const top = Number.parseFloat(hotspot.getAttribute(\"y\") || \"\");\n const xunits = hotspot.getAttribute(\"xunits\") || \"\";\n const yunits = hotspot.getAttribute(\"yunits\") || \"\";\n if (!Number.isNaN(left) && !Number.isNaN(top))\n return {\n \"icon-offset\": [left, top],\n \"icon-offset-units\": [xunits, yunits],\n };\n return {};\n }),\n extractIconHref(iconStyle)\n );\n });\n}\n\nexport function extractLabel(node: Element) {\n return get(node, \"LabelStyle\", (labelStyle) => {\n return Object.assign(\n getColor(labelStyle, \"label\"),\n numericProperty(labelStyle, \"scale\", \"label-scale\")\n );\n });\n}\n\nexport function extractLine(node: Element) {\n return get(node, \"LineStyle\", (lineStyle) => {\n return Object.assign(\n getColor(lineStyle, \"stroke\"),\n numericProperty(lineStyle, \"width\", \"stroke-width\")\n );\n });\n}\n\nexport function extractPoly(node: Element) {\n return get(node, \"PolyStyle\", (polyStyle, properties) => {\n return Object.assign(\n properties,\n get(polyStyle, \"color\", (elem) => fixColor(nodeVal(elem), \"fill\")),\n val1(polyStyle, \"fill\", (fill) => {\n if (fill === \"0\") return { \"fill-opacity\": 0 };\n }),\n val1(polyStyle, \"outline\", (outline) => {\n if (outline === \"0\") return { \"stroke-opacity\": 0 };\n })\n );\n });\n}\n\nexport function extractStyle(node: Element) {\n return Object.assign(\n {},\n extractPoly(node),\n extractLine(node),\n extractLabel(node),\n extractIcon(node)\n );\n}\n", "import type { Geometry, LineString, Point, Position } from \"geojson\";\nimport { $, $ns, get1, isElement, nodeVal } from \"../shared\";\n\nconst removeSpace = /\\s*/g;\nconst trimSpace = /^\\s*|\\s*$/g;\nconst splitSpace = /\\s+/;\n\n/**\n * Get one coordinate from a coordinate array, if any\n */\nexport function coord1(value: string): Position {\n return value\n .replace(removeSpace, \"\")\n .split(\",\")\n .map(Number.parseFloat)\n .filter((num) => !Number.isNaN(num))\n .slice(0, 3);\n}\n\n/**\n * Get all coordinates from a coordinate array as [[],[]]\n */\nexport function coord(value: string): Position[] {\n return value\n .replace(trimSpace, \"\")\n .split(splitSpace)\n .map(coord1)\n .filter((coord) => {\n return coord.length >= 2;\n });\n}\n\nfunction gxCoords(\n node: Element\n): { geometry: Point | LineString; times: string[] } | null {\n let elems = $(node, \"coord\");\n if (elems.length === 0) {\n elems = $ns(node, \"coord\", \"*\");\n }\n\n const coordinates = elems.map((elem) => {\n return nodeVal(elem).split(\" \").map(Number.parseFloat);\n });\n\n if (coordinates.length === 0) {\n return null;\n }\n\n return {\n geometry:\n coordinates.length > 2\n ? {\n type: \"LineString\",\n coordinates,\n }\n : {\n type: \"Point\",\n coordinates: coordinates[0],\n },\n times: $(node, \"when\").map((elem) => nodeVal(elem)),\n };\n}\n\nexport function fixRing(ring: Position[]) {\n if (ring.length === 0) return ring;\n const first = ring[0];\n const last = ring[ring.length - 1];\n let equal = true;\n for (let i = 0; i < Math.max(first.length, last.length); i++) {\n if (first[i] !== last[i]) {\n equal = false;\n break;\n }\n }\n if (!equal) {\n return ring.concat([ring[0]]);\n }\n return ring;\n}\n\nexport function getCoordinates(node: Element) {\n return nodeVal(get1(node, \"coordinates\"));\n}\n\ninterface GeometriesAndTimes {\n geometries: Geometry[];\n coordTimes: string[][];\n}\n\nexport function getGeometry(node: Element): GeometriesAndTimes {\n let geometries: Geometry[] = [];\n let coordTimes: string[][] = [];\n\n for (let i = 0; i < node.childNodes.length; i++) {\n const child = node.childNodes.item(i);\n if (isElement(child)) {\n switch (child.tagName) {\n case \"MultiGeometry\":\n case \"MultiTrack\":\n case \"gx:MultiTrack\": {\n const childGeometries = getGeometry(child);\n geometries = geometries.concat(childGeometries.geometries);\n coordTimes = coordTimes.concat(childGeometries.coordTimes);\n break;\n }\n\n case \"Point\": {\n const coordinates = coord1(getCoordinates(child));\n if (coordinates.length >= 2) {\n geometries.push({\n type: \"Point\",\n coordinates,\n });\n }\n break;\n }\n case \"LinearRing\":\n case \"LineString\": {\n const coordinates = coord(getCoordinates(child));\n if (coordinates.length >= 2) {\n geometries.push({\n type: \"LineString\",\n coordinates,\n });\n }\n break;\n }\n case \"Polygon\": {\n const coords = [];\n for (const linearRing of $(child, \"LinearRing\")) {\n const ring = fixRing(coord(getCoordinates(linearRing)));\n if (ring.length >= 4) {\n coords.push(ring);\n }\n }\n if (coords.length) {\n geometries.push({\n type: \"Polygon\",\n coordinates: coords,\n });\n }\n break;\n }\n case \"Track\":\n case \"gx:Track\": {\n const gx = gxCoords(child);\n if (!gx) break;\n const { times, geometry } = gx;\n geometries.push(geometry);\n if (times.length) coordTimes.push(times);\n break;\n }\n }\n }\n }\n\n return {\n geometries,\n coordTimes,\n };\n}\n", "import {\n $,\n type P,\n type StyleMap,\n get,\n get1,\n nodeVal,\n normalizeId,\n val1,\n} from \"../shared\";\n\nexport type TypeConverter = (x: string) => unknown;\nexport type Schema = { [key: string]: TypeConverter };\n\nconst toNumber: TypeConverter = (x) => Number(x);\nexport const typeConverters: Record = {\n string: (x) => x,\n int: toNumber,\n uint: toNumber,\n short: toNumber,\n ushort: toNumber,\n float: toNumber,\n double: toNumber,\n bool: (x) => Boolean(x),\n};\n\nexport function extractExtendedData(node: Element, schema: Schema) {\n return get(node, \"ExtendedData\", (extendedData, properties) => {\n for (const data of $(extendedData, \"Data\")) {\n properties[data.getAttribute(\"name\") || \"\"] = nodeVal(\n get1(data, \"value\")\n );\n }\n for (const simpleData of $(extendedData, \"SimpleData\")) {\n const name = simpleData.getAttribute(\"name\") || \"\";\n const typeConverter = schema[name] || typeConverters.string;\n properties[name] = typeConverter(nodeVal(simpleData));\n }\n return properties;\n });\n}\n\nexport function getMaybeHTMLDescription(node: Element) {\n const descriptionNode = get1(node, \"description\");\n for (const c of Array.from(descriptionNode?.childNodes || [])) {\n if (c.nodeType === 4) {\n return {\n description: {\n \"@type\": \"html\",\n value: nodeVal(c as Element),\n },\n };\n }\n }\n return {};\n}\n\nexport function extractTimeSpan(node: Element): P {\n return get(node, \"TimeSpan\", (timeSpan) => {\n return {\n timespan: {\n begin: nodeVal(get1(timeSpan, \"begin\")),\n end: nodeVal(get1(timeSpan, \"end\")),\n },\n };\n });\n}\n\nexport function extractTimeStamp(node: Element): P {\n return get(node, \"TimeStamp\", (timeStamp) => {\n return { timestamp: nodeVal(get1(timeStamp, \"when\")) };\n });\n}\n\nexport function extractCascadedStyle(node: Element, styleMap: StyleMap): P {\n return val1(node, \"styleUrl\", (styleUrl) => {\n styleUrl = normalizeId(styleUrl);\n if (styleMap[styleUrl]) {\n return Object.assign({ styleUrl }, styleMap[styleUrl]);\n }\n // For backward-compatibility. Should we still include\n // styleUrl even if it's not resolved?\n return { styleUrl };\n });\n}\n\nexport enum AltitudeMode {\n ABSOLUTE = \"absolute\",\n RELATIVE_TO_GROUND = \"relativeToGround\",\n CLAMP_TO_GROUND = \"clampToGround\",\n CLAMP_TO_SEAFLOOR = \"clampToSeaFloor\",\n RELATIVE_TO_SEAFLOOR = \"relativeToSeaFloor\",\n}\n\nexport function processAltitudeMode(mode: Element | null): AltitudeMode | null {\n switch (mode?.textContent) {\n case AltitudeMode.ABSOLUTE:\n return AltitudeMode.ABSOLUTE;\n case AltitudeMode.CLAMP_TO_GROUND:\n return AltitudeMode.CLAMP_TO_GROUND;\n case AltitudeMode.CLAMP_TO_SEAFLOOR:\n return AltitudeMode.CLAMP_TO_SEAFLOOR;\n case AltitudeMode.RELATIVE_TO_GROUND:\n return AltitudeMode.RELATIVE_TO_GROUND;\n case AltitudeMode.RELATIVE_TO_SEAFLOOR:\n return AltitudeMode.RELATIVE_TO_SEAFLOOR;\n default:\n break;\n }\n return null;\n}\n\nexport type BBox = [number, number, number, number];\n", "import type { Feature, Polygon } from \"geojson\";\nimport type { KMLOptions } from \"lib/kml\";\nimport { type StyleMap, get1, getMulti, num1 } from \"../shared\";\nimport { extractIconHref, extractStyle } from \"./extractStyle\";\nimport { coord, fixRing, getCoordinates } from \"./geometry\";\nimport {\n type BBox,\n type Schema,\n extractCascadedStyle,\n extractExtendedData,\n extractTimeSpan,\n extractTimeStamp,\n getMaybeHTMLDescription,\n} from \"./shared\";\n\ninterface BoxGeometry {\n bbox?: BBox;\n geometry: Polygon;\n}\n\nfunction getGroundOverlayBox(node: Element): BoxGeometry | null {\n const latLonQuad = get1(node, \"gx:LatLonQuad\");\n\n if (latLonQuad) {\n const ring = fixRing(coord(getCoordinates(node)));\n return {\n geometry: {\n type: \"Polygon\",\n coordinates: [ring],\n },\n };\n }\n\n return getLatLonBox(node);\n}\n\nconst DEGREES_TO_RADIANS = Math.PI / 180;\n\nfunction rotateBox(\n bbox: BBox,\n coordinates: Polygon[\"coordinates\"],\n rotation: number\n): Polygon[\"coordinates\"] {\n const center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];\n\n return [\n coordinates[0].map((coordinate) => {\n const dy = coordinate[1] - center[1];\n const dx = coordinate[0] - center[0];\n const distance = Math.sqrt(dy ** 2 + dx ** 2);\n const angle = Math.atan2(dy, dx) + rotation * DEGREES_TO_RADIANS;\n\n return [\n center[0] + Math.cos(angle) * distance,\n center[1] + Math.sin(angle) * distance,\n ];\n }),\n ];\n}\n\nfunction getLatLonBox(node: Element): BoxGeometry | null {\n const latLonBox = get1(node, \"LatLonBox\");\n\n if (latLonBox) {\n const north = num1(latLonBox, \"north\");\n const west = num1(latLonBox, \"west\");\n const east = num1(latLonBox, \"east\");\n const south = num1(latLonBox, \"south\");\n const rotation = num1(latLonBox, \"rotation\");\n\n if (\n typeof north === \"number\" &&\n typeof south === \"number\" &&\n typeof west === \"number\" &&\n typeof east === \"number\"\n ) {\n const bbox: BBox = [west, south, east, north];\n let coordinates = [\n [\n [west, north], // top left\n [east, north], // top right\n [east, south], // top right\n [west, south], // bottom left\n [west, north], // top left (again)\n ],\n ];\n if (typeof rotation === \"number\") {\n coordinates = rotateBox(bbox, coordinates, rotation);\n }\n return {\n bbox,\n geometry: {\n type: \"Polygon\",\n coordinates,\n },\n };\n }\n }\n\n return null;\n}\n\nexport function getGroundOverlay(\n node: Element,\n styleMap: StyleMap,\n schema: Schema,\n options: KMLOptions\n): Feature | null {\n const box = getGroundOverlayBox(node);\n\n const geometry = box?.geometry || null;\n\n if (!geometry && options.skipNullGeometry) {\n return null;\n }\n\n const feature: Feature = {\n type: \"Feature\",\n geometry,\n properties: Object.assign(\n /**\n * Related to\n * https://gist.github.com/tmcw/037a1cb6660d74a392e9da7446540f46\n */\n { \"@geometry-type\": \"groundoverlay\" },\n getMulti(node, [\n \"name\",\n \"address\",\n \"visibility\",\n \"open\",\n \"phoneNumber\",\n \"description\",\n ]),\n getMaybeHTMLDescription(node),\n extractCascadedStyle(node, styleMap),\n extractStyle(node),\n extractIconHref(node),\n extractExtendedData(node, schema),\n extractTimeSpan(node),\n extractTimeStamp(node)\n ),\n };\n\n if (box?.bbox) {\n feature.bbox = box.bbox;\n }\n\n if (feature.properties?.visibility !== undefined) {\n feature.properties.visibility = feature.properties.visibility !== \"0\";\n }\n\n const id = node.getAttribute(\"id\");\n if (id !== null && id !== \"\") feature.id = id;\n return feature;\n}\n", "import type { Feature, Polygon } from \"geojson\";\nimport type { KMLOptions } from \"lib/kml\";\nimport { type StyleMap, get1, getMulti, num1 } from \"../shared\";\nimport { extractIconHref, extractStyle } from \"./extractStyle\";\nimport {\n AltitudeMode,\n type BBox,\n type Schema,\n extractCascadedStyle,\n extractExtendedData,\n extractTimeSpan,\n extractTimeStamp,\n getMaybeHTMLDescription,\n processAltitudeMode,\n} from \"./shared\";\n\ninterface BoxGeometry {\n bbox?: BBox;\n geometry: Polygon;\n}\n\ntype LOD = [number, number | null, number | null, number | null];\ninterface IRegion {\n coordinateBox: BoxGeometry | null;\n lod: LOD | null;\n}\n\nfunction getNetworkLinkRegion(node: Element): IRegion | null {\n const region = get1(node, \"Region\");\n\n if (region) {\n return {\n coordinateBox: getLatLonAltBox(region),\n lod: getLod(node),\n };\n }\n return null;\n}\n\nfunction getLod(node: Element): LOD | null {\n const lod = get1(node, \"Lod\");\n\n if (lod) {\n return [\n num1(lod, \"minLodPixels\") ?? -1,\n num1(lod, \"maxLodPixels\") ?? -1,\n num1(lod, \"minFadeExtent\") ?? null,\n num1(lod, \"maxFadeExtent\") ?? null,\n ];\n }\n\n return null;\n}\n\nfunction getLatLonAltBox(node: Element): BoxGeometry | null {\n const latLonAltBox = get1(node, \"LatLonAltBox\");\n\n if (latLonAltBox) {\n const north = num1(latLonAltBox, \"north\");\n const west = num1(latLonAltBox, \"west\");\n const east = num1(latLonAltBox, \"east\");\n const south = num1(latLonAltBox, \"south\");\n const altitudeMode = processAltitudeMode(\n get1(latLonAltBox, \"altitudeMode\") ||\n get1(latLonAltBox, \"gx:altitudeMode\")\n );\n\n if (altitudeMode) {\n console.debug(\n \"Encountered an unsupported feature of KML for togeojson: please contact developers for support of altitude mode.\"\n );\n }\n if (\n typeof north === \"number\" &&\n typeof south === \"number\" &&\n typeof west === \"number\" &&\n typeof east === \"number\"\n ) {\n const bbox: BBox = [west, south, east, north];\n const coordinates = [\n [\n [west, north], // top left\n [east, north], // top right\n [east, south], // top right\n [west, south], // bottom left\n [west, north], // top left (again)\n ],\n ];\n return {\n bbox,\n geometry: {\n type: \"Polygon\",\n coordinates,\n },\n };\n }\n }\n\n return null;\n}\n\nfunction getLinkObject(node: Element) {\n /*\n \n \n ... \n onChange\n \n 4 \n never\n \n 4 \n 1 \n BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]\n \n ... \n \n */\n const linkObj = get1(node, \"Link\");\n\n if (linkObj) {\n return getMulti(linkObj, [\n \"href\",\n \"refreshMode\",\n \"refreshInterval\",\n \"viewRefreshMode\",\n \"viewRefreshTime\",\n \"viewBoundScale\",\n \"viewFormat\",\n \"httpQuery\",\n ]);\n }\n\n return {};\n}\n\nexport function getNetworkLink(\n node: Element,\n styleMap: StyleMap,\n schema: Schema,\n options: KMLOptions\n): Feature | null {\n const box = getNetworkLinkRegion(node);\n\n const geometry = box?.coordinateBox?.geometry || null;\n\n if (!geometry && options.skipNullGeometry) {\n return null;\n }\n\n const feature: Feature = {\n type: \"Feature\",\n geometry,\n properties: Object.assign(\n /**\n * Related to\n * https://gist.github.com/tmcw/037a1cb6660d74a392e9da7446540f46\n */\n { \"@geometry-type\": \"networklink\" },\n getMulti(node, [\n \"name\",\n \"address\",\n \"visibility\",\n \"open\",\n \"phoneNumber\",\n \"styleUrl\",\n \"refreshVisibility\",\n \"flyToView\",\n \"description\",\n ]),\n getMaybeHTMLDescription(node),\n extractCascadedStyle(node, styleMap),\n extractStyle(node),\n extractIconHref(node),\n extractExtendedData(node, schema),\n extractTimeSpan(node),\n extractTimeStamp(node),\n getLinkObject(node),\n box?.lod ? { lod: box.lod } : {}\n ),\n };\n\n if (box?.coordinateBox?.bbox) {\n feature.bbox = box.coordinateBox.bbox;\n }\n\n if (feature.properties?.visibility !== undefined) {\n feature.properties.visibility = feature.properties.visibility !== \"0\";\n }\n\n const id = node.getAttribute(\"id\");\n if (id !== null && id !== \"\") feature.id = id;\n return feature;\n}\n", "import type { Feature, Geometry } from \"geojson\";\nimport type { KMLOptions } from \"lib/kml\";\nimport { type StyleMap, getMulti } from \"../shared\";\nimport { extractStyle } from \"./extractStyle\";\nimport { getGeometry } from \"./geometry\";\nimport {\n type Schema,\n extractCascadedStyle,\n extractExtendedData,\n extractTimeSpan,\n extractTimeStamp,\n getMaybeHTMLDescription,\n} from \"./shared\";\n\nfunction geometryListToGeometry(geometries: Geometry[]): Geometry | null {\n return geometries.length === 0\n ? null\n : geometries.length === 1\n ? geometries[0]\n : {\n type: \"GeometryCollection\",\n geometries,\n };\n}\n\nexport function getPlacemark(\n node: Element,\n styleMap: StyleMap,\n schema: Schema,\n options: KMLOptions\n): Feature | null {\n const { coordTimes, geometries } = getGeometry(node);\n\n const geometry = geometryListToGeometry(geometries);\n\n if (!geometry && options.skipNullGeometry) {\n return null;\n }\n\n const feature: Feature = {\n type: \"Feature\",\n geometry,\n properties: Object.assign(\n getMulti(node, [\n \"name\",\n \"address\",\n \"visibility\",\n \"open\",\n \"phoneNumber\",\n \"description\",\n ]),\n getMaybeHTMLDescription(node),\n extractCascadedStyle(node, styleMap),\n extractStyle(node),\n extractExtendedData(node, schema),\n extractTimeSpan(node),\n extractTimeStamp(node),\n coordTimes.length\n ? {\n coordinateProperties: {\n times: coordTimes.length === 1 ? coordTimes[0] : coordTimes,\n },\n }\n : {}\n ),\n };\n\n if (feature.properties?.visibility !== undefined) {\n feature.properties.visibility = feature.properties.visibility !== \"0\";\n }\n\n const id = node.getAttribute(\"id\");\n if (id !== null && id !== \"\") feature.id = id;\n return feature;\n}\n", "import type { Document as XDocument } from \"@xmldom/xmldom\";\nimport type { FeatureCollection, Geometry } from \"geojson\";\nimport { extractStyle } from \"./kml/extractStyle\";\nimport { getGroundOverlay } from \"./kml/ground_overlay\";\nimport { getNetworkLink } from \"./kml/networklink\";\nimport { getPlacemark } from \"./kml/placemark\";\nimport { type Schema, typeConverters } from \"./kml/shared\";\nimport {\n $,\n type F,\n type P,\n type StyleMap,\n isElement,\n nodeVal,\n normalizeId,\n val1,\n} from \"./shared\";\n\n/**\n * Options to customize KML output.\n *\n * The only option currently\n * is `skipNullGeometry`. Both the KML and GeoJSON formats support\n * the idea of features that don't have geometries: in KML,\n * this is a Placemark without a Point, etc element, and in GeoJSON\n * it's a geometry member with a value of `null`.\n *\n * toGeoJSON, by default, translates null geometries in KML to\n * null geometries in GeoJSON. For systems that use GeoJSON but\n * don't support null geometries, you can specify `skipNullGeometry`\n * to omit these features entirely and only include\n * features that have a geometry defined.\n */\nexport interface KMLOptions {\n skipNullGeometry?: boolean;\n}\n\n/**\n * A folder including metadata. Folders\n * may contain other folders or features,\n * or nothing at all.\n */\nexport interface Folder {\n type: \"folder\";\n /**\n * Standard values:\n *\n * * \"name\",\n * * \"visibility\",\n * * \"open\",\n * * \"address\",\n * * \"description\",\n * * \"phoneNumber\",\n * * \"visibility\",\n */\n meta: {\n [key: string]: unknown;\n };\n children: Array;\n}\n\n/**\n * A nested folder structure, represented\n * as a tree with folders and features.\n */\nexport interface Root {\n type: \"root\";\n children: Array;\n}\n\ntype TreeContainer = Root | Folder;\n\nfunction getStyleId(style: Element) {\n let id = style.getAttribute(\"id\");\n const parentNode = style.parentNode;\n if (\n !id &&\n isElement(parentNode) &&\n parentNode.localName === \"CascadingStyle\"\n ) {\n id = parentNode.getAttribute(\"kml:id\") || parentNode.getAttribute(\"id\");\n }\n return normalizeId(id || \"\");\n}\n\nfunction buildStyleMap(node: Document): StyleMap {\n const styleMap: StyleMap = {};\n for (const style of $(node, \"Style\")) {\n styleMap[getStyleId(style)] = extractStyle(style);\n }\n for (const map of $(node, \"StyleMap\")) {\n const id = normalizeId(map.getAttribute(\"id\") || \"\");\n val1(map, \"styleUrl\", (styleUrl) => {\n styleUrl = normalizeId(styleUrl);\n if (styleMap[styleUrl]) {\n styleMap[id] = styleMap[styleUrl];\n }\n });\n }\n return styleMap;\n}\n\nfunction buildSchema(node: Document): Schema {\n const schema: Schema = {};\n for (const field of $(node, \"SimpleField\")) {\n schema[field.getAttribute(\"name\") || \"\"] =\n typeConverters[field.getAttribute(\"type\") || \"\"] || typeConverters.string;\n }\n return schema;\n}\n\nconst FOLDER_PROPS = [\n \"name\",\n \"visibility\",\n \"open\",\n \"address\",\n \"description\",\n \"phoneNumber\",\n \"visibility\",\n] as const;\n\nfunction getFolder(node: Element): Folder {\n const meta: P = {};\n\n for (const child of Array.from(node.childNodes)) {\n if (isElement(child) && FOLDER_PROPS.includes(child.tagName as any)) {\n meta[child.tagName] = nodeVal(child);\n }\n }\n\n return {\n type: \"folder\",\n meta,\n children: [],\n };\n}\n\n/**\n * Yield a nested tree with KML folder structure\n *\n * This generates a tree with the given structure:\n *\n * ```js\n * {\n * \"type\": \"root\",\n * \"children\": [\n * {\n * \"type\": \"folder\",\n * \"meta\": {\n * \"name\": \"Test\"\n * },\n * \"children\": [\n * // ...features and folders\n * ]\n * }\n * // ...features\n * ]\n * }\n * ```\n *\n * ### GroundOverlay\n *\n * GroundOverlay elements are converted into\n * `Feature` objects with `Polygon` geometries,\n * a property like:\n *\n * ```json\n * {\n * \"@geometry-type\": \"groundoverlay\"\n * }\n * ```\n *\n * And the ground overlay's image URL in the `href`\n * property. Ground overlays will need to be displayed\n * with a separate method to other features, depending\n * on which map framework you're using.\n */\nexport function kmlWithFolders(\n node: Document | XDocument,\n options: KMLOptions = {\n skipNullGeometry: false,\n }\n): Root {\n const n = node as Document;\n const styleMap = buildStyleMap(n);\n const schema = buildSchema(n);\n\n // atomic geospatial types supported by KML - MultiGeometry is\n // handled separately\n // all root placemarks in the file\n const placemarks = [];\n const networkLinks = [];\n const tree: Root = { type: \"root\", children: [] };\n\n function traverse(\n node: Document | ChildNode | Element,\n pointer: TreeContainer,\n options: KMLOptions\n ) {\n if (isElement(node)) {\n switch (node.tagName) {\n case \"GroundOverlay\": {\n placemarks.push(node);\n const placemark = getGroundOverlay(node, styleMap, schema, options);\n if (placemark) {\n pointer.children.push(placemark);\n }\n break;\n }\n case \"Placemark\": {\n placemarks.push(node);\n const placemark = getPlacemark(node, styleMap, schema, options);\n if (placemark) {\n pointer.children.push(placemark);\n }\n break;\n }\n case \"Folder\": {\n const folder = getFolder(node);\n pointer.children.push(folder);\n pointer = folder;\n break;\n }\n case \"NetworkLink\": {\n networkLinks.push(node);\n const networkLink = getNetworkLink(node, styleMap, schema, options);\n if (networkLink) {\n pointer.children.push(networkLink);\n }\n break;\n }\n }\n }\n\n if (node.childNodes) {\n for (let i = 0; i < node.childNodes.length; i++) {\n traverse(node.childNodes[i], pointer, options);\n }\n }\n }\n\n traverse(n, tree, options);\n\n return tree;\n}\n\n/**\n * Convert KML to GeoJSON incrementally, returning\n * a [Generator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)\n * that yields output feature by feature.\n */\nexport function* kmlGen(\n node: Document | XDocument,\n options: KMLOptions = {\n skipNullGeometry: false,\n }\n): Generator {\n const n = node as Document;\n const styleMap = buildStyleMap(n);\n const schema = buildSchema(n);\n for (const placemark of $(n, \"Placemark\")) {\n const feature = getPlacemark(placemark, styleMap, schema, options);\n if (feature) yield feature;\n }\n for (const groundOverlay of $(n, \"GroundOverlay\")) {\n const feature = getGroundOverlay(groundOverlay, styleMap, schema, options);\n if (feature) yield feature;\n }\n for (const networkLink of $(n, \"NetworkLink\")) {\n const feature = getNetworkLink(networkLink, styleMap, schema, options);\n if (feature) yield feature;\n }\n}\n\n/**\n * Convert a KML document to GeoJSON. The first argument, `doc`, must be a KML\n * document as an XML DOM - not as a string. You can get this using jQuery's default\n * `.ajax` function or using a bare XMLHttpRequest with the `.response` property\n * holding an XML DOM.\n *\n * The output is a JavaScript object of GeoJSON data. You can convert it to a string\n * with [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)\n * or use it directly in libraries.\n */\nexport function kml(\n node: Document | XDocument,\n options: KMLOptions = {\n skipNullGeometry: false,\n }\n): FeatureCollection {\n return {\n type: \"FeatureCollection\",\n features: Array.from(kmlGen(node as Document, options)),\n };\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { geoBounds as d3_geoBounds, geoPath as d3_geoPath } from 'd3-geo';\nimport { text as d3_text } from 'd3-fetch';\nimport { select as d3_select } from 'd3-selection';\n\nimport stringify from 'fast-json-stable-stringify';\nimport { gpx, kml } from '@tmcw/togeojson';\n\nimport { geoExtent, geoPolygonIntersectsPolygon } from '../geo';\nimport { services } from '../services';\nimport { svgPath } from './helpers';\nimport { utilDetect } from '../util/detect';\nimport { utilArrayFlatten, utilArrayUnion, utilHashcode } from '../util';\n\n\nvar _initialized = false;\nvar _enabled = false;\nvar _geojson;\n\n\nexport function svgData(projection, context, dispatch) {\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var _showLabels = true;\n var detected = utilDetect();\n var layer = d3_select(null);\n var _vtService;\n var _fileList;\n var _template;\n var _src;\n\n const supportedFormats = [\n '.gpx',\n '.kml',\n '.geojson',\n '.json'\n ];\n\n\n function init() {\n if (_initialized) return; // run once\n\n _geojson = {};\n _enabled = true;\n\n function over(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n d3_event.dataTransfer.dropEffect = 'copy';\n }\n\n context.container()\n .attr('dropzone', 'copy')\n .on('drop.svgData', function(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n if (!detected.filedrop) return;\n var f = d3_event.dataTransfer.files[0];\n var extension = getExtension(f.name);\n if (!supportedFormats.includes(extension)) return;\n drawData.fileList(d3_event.dataTransfer.files);\n })\n .on('dragenter.svgData', over)\n .on('dragexit.svgData', over)\n .on('dragover.svgData', over);\n\n _initialized = true;\n }\n\n\n function getService() {\n if (services.vectorTile && !_vtService) {\n _vtService = services.vectorTile;\n _vtService.event.on('loadedData', throttledRedraw);\n } else if (!services.vectorTile && _vtService) {\n _vtService = null;\n }\n\n return _vtService;\n }\n\n\n function showLayer() {\n layerOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', layerOff);\n }\n\n\n function layerOn() {\n layer.style('display', 'block');\n }\n\n\n function layerOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n // ensure that all geojson features in a collection have IDs\n function ensureIDs(gj) {\n if (!gj) return null;\n\n if (gj.type === 'FeatureCollection') {\n for (var i = 0; i < gj.features.length; i++) {\n ensureFeatureID(gj.features[i]);\n }\n } else {\n ensureFeatureID(gj);\n }\n return gj;\n }\n\n\n // ensure that each single Feature object has a unique ID\n function ensureFeatureID(feature) {\n if (!feature) return;\n feature.__featurehash__ = utilHashcode(stringify(feature));\n return feature;\n }\n\n\n // Prefer an array of Features instead of a FeatureCollection\n function getFeatures(gj) {\n if (!gj) return [];\n\n if (gj.type === 'FeatureCollection') {\n return gj.features;\n } else {\n return [gj];\n }\n }\n\n\n function featureKey(d) {\n return d.__featurehash__;\n }\n\n\n function isPolygon(d) {\n return d.geometry.type === 'Polygon' || d.geometry.type === 'MultiPolygon';\n }\n\n\n function clipPathID(d) {\n return 'ideditor-data-' + d.__featurehash__ + '-clippath';\n }\n\n\n function featureClasses(d) {\n return [\n 'data' + d.__featurehash__,\n d.geometry.type,\n isPolygon(d) ? 'area' : '',\n d.__layerID__ || ''\n ].filter(Boolean).join(' ');\n }\n\n\n function drawData(selection) {\n var vtService = getService();\n var getPath = svgPath(projection).geojson;\n var getAreaPath = svgPath(projection, null, true).geojson;\n var hasData = drawData.hasData();\n\n layer = selection.selectAll('.layer-mapdata')\n .data(_enabled && hasData ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-mapdata')\n .merge(layer);\n\n var surface = context.surface();\n if (!surface || surface.empty()) return; // not ready to draw yet, starting up\n\n\n // Gather data\n var geoData, polygonData;\n if (_template && vtService) { // fetch data from vector tile service\n var sourceID = _template;\n vtService.loadTiles(sourceID, _template, projection);\n geoData = vtService.data(sourceID, projection);\n } else {\n geoData = getFeatures(_geojson);\n }\n geoData = geoData.filter(getPath);\n polygonData = geoData.filter(isPolygon);\n\n\n // Draw clip paths for polygons\n var clipPaths = surface.selectAll('defs').selectAll('.clipPath-data')\n .data(polygonData, featureKey);\n\n clipPaths.exit()\n .remove();\n\n var clipPathsEnter = clipPaths.enter()\n .append('clipPath')\n .attr('class', 'clipPath-data')\n .attr('id', clipPathID);\n\n clipPathsEnter\n .append('path');\n\n clipPaths.merge(clipPathsEnter)\n .selectAll('path')\n .attr('d', getAreaPath);\n\n\n // Draw fill, shadow, stroke layers\n var datagroups = layer\n .selectAll('g.datagroup')\n .data(['fill', 'shadow', 'stroke']);\n\n datagroups = datagroups.enter()\n .append('g')\n .attr('class', function(d) { return 'datagroup datagroup-' + d; })\n .merge(datagroups);\n\n\n // Draw paths\n var pathData = {\n fill: polygonData,\n shadow: geoData,\n stroke: geoData\n };\n\n var paths = datagroups\n .selectAll('path')\n .data(function(layer) { return pathData[layer]; }, featureKey);\n\n // exit\n paths.exit()\n .remove();\n\n // enter/update\n paths.enter()\n .append('path')\n .attr('class', function(d) {\n var datagroup = this.parentNode.__data__;\n return 'pathdata ' + datagroup + ' ' + featureClasses(d);\n })\n .attr('clip-path', function(d) {\n var datagroup = this.parentNode.__data__;\n return datagroup === 'fill' ? ('url(#' + clipPathID(d) + ')') : null;\n })\n .merge(paths)\n .attr('d', function(d) {\n var datagroup = this.parentNode.__data__;\n return datagroup === 'fill' ? getAreaPath(d) : getPath(d);\n });\n\n\n // Draw labels\n layer\n .call(drawLabels, 'label-halo', geoData)\n .call(drawLabels, 'label', geoData);\n\n\n function drawLabels(selection, textClass, data) {\n var labelPath = d3_geoPath(projection);\n var labelData = data.filter(function(d) {\n return _showLabels && d.properties && (d.properties.desc || d.properties.name);\n });\n\n var labels = selection.selectAll('text.' + textClass)\n .data(labelData, featureKey);\n\n // exit\n labels.exit()\n .remove();\n\n // enter/update\n labels.enter()\n .append('text')\n .attr('class', function(d) { return textClass + ' ' + featureClasses(d); })\n .merge(labels)\n .text(function(d) {\n return d.properties.desc || d.properties.name;\n })\n .attr('x', function(d) {\n var centroid = labelPath.centroid(d);\n return centroid[0] + 11;\n })\n .attr('y', function(d) {\n var centroid = labelPath.centroid(d);\n return centroid[1];\n });\n }\n }\n\n\n function getExtension(fileName) {\n if (!fileName) return;\n\n var re = /\\.(gpx|kml|(geo)?json|png)$/i;\n var match = fileName.toLowerCase().match(re);\n return match && match.length && match[0];\n }\n\n\n function xmlToDom(textdata) {\n return (new DOMParser()).parseFromString(textdata, 'text/xml');\n }\n\n\n function stringifyGeojsonProperties(feature) {\n const properties = feature.properties;\n for (const key in properties) {\n const property = properties[key];\n if (typeof property === 'number' || typeof property === 'boolean' || Array.isArray(property)) {\n properties[key] = property.toString();\n } else if (property === null) {\n properties[key] = 'null';\n } else if (typeof property === 'object') {\n properties[key] = JSON.stringify(property);\n }\n }\n }\n\n\n drawData.setFile = function(extension, data) {\n _template = null;\n _fileList = null;\n _geojson = null;\n _src = null;\n\n var gj;\n switch (extension) {\n case '.gpx':\n gj = gpx(xmlToDom(data));\n break;\n case '.kml':\n gj = kml(xmlToDom(data));\n break;\n case '.geojson':\n case '.json':\n gj = JSON.parse(data);\n if (gj.type === 'FeatureCollection') {\n gj.features.forEach(stringifyGeojsonProperties);\n } else if (gj.type === 'Feature') {\n stringifyGeojsonProperties(gj);\n }\n break;\n }\n\n gj = gj || {};\n if (Object.keys(gj).length) {\n _geojson = ensureIDs(gj);\n _src = extension + ' data file';\n this.fitZoom();\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.showLabels = function(val) {\n if (!arguments.length) return _showLabels;\n\n _showLabels = val;\n return this;\n };\n\n\n drawData.enabled = function(val) {\n if (!arguments.length) return _enabled;\n\n _enabled = val;\n if (_enabled) {\n showLayer();\n } else {\n hideLayer();\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.hasData = function() {\n var gj = _geojson || {};\n return !!(_template || Object.keys(gj).length);\n };\n\n\n drawData.template = function(val, src) {\n if (!arguments.length) return _template;\n\n // test source against OSM imagery blocklists..\n var osm = context.connection();\n if (osm) {\n for (const regex of osm.imageryBlocklists()) {\n if (regex.test(val)) {\n // matches a blocked sources -> do not set template\n return;\n };\n }\n }\n\n _template = val;\n _fileList = null;\n _geojson = null;\n\n // strip off the querystring/hash from the template,\n // it often includes the access token\n _src = src || ('vectortile:' + val.split(/[?#]/)[0]);\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.geojson = function(gj, src) {\n if (!arguments.length) return _geojson;\n\n _template = null;\n _fileList = null;\n _geojson = null;\n _src = null;\n\n gj = gj || {};\n if (Object.keys(gj).length) {\n _geojson = ensureIDs(gj);\n _src = src || 'unknown.geojson';\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n drawData.fileList = function(fileList) {\n if (!arguments.length) return _fileList;\n\n _template = null;\n _geojson = null;\n _src = null;\n _fileList = fileList;\n\n if (!fileList || !fileList.length) return this;\n var f = fileList[0];\n var extension = getExtension(f.name);\n var reader = new FileReader();\n reader.onload = (function() {\n return function(e) {\n drawData.setFile(extension, e.target.result);\n };\n })(f);\n\n reader.readAsText(f);\n\n return this;\n };\n\n\n drawData.url = function(url, defaultExtension) {\n _template = null;\n _fileList = null;\n _geojson = null;\n _src = null;\n\n // strip off any querystring/hash from the url before checking extension\n var testUrl = url.split(/[?#]/)[0];\n var extension = getExtension(testUrl) || defaultExtension;\n if (extension) {\n _template = null;\n d3_text(url)\n .then(function(data) {\n drawData.setFile(extension, data);\n })\n .catch(function() {\n /* ignore */\n });\n\n } else {\n drawData.template(url);\n }\n\n return this;\n };\n\n\n drawData.getSrc = function() {\n return _src || '';\n };\n\n\n drawData.fitZoom = function() {\n var features = getFeatures(_geojson);\n if (!features.length) return;\n\n var map = context.map();\n var viewport = map.trimmedExtent().polygon();\n var coords = features.reduce(function(coords, feature) {\n var geom = feature.geometry;\n if (!geom) return coords;\n\n var c = geom.coordinates;\n\n /* eslint-disable no-fallthrough */\n switch (geom.type) {\n case 'Point':\n c = [c];\n case 'MultiPoint':\n case 'LineString':\n break;\n\n case 'MultiPolygon':\n c = utilArrayFlatten(c);\n case 'Polygon':\n case 'MultiLineString':\n c = utilArrayFlatten(c);\n break;\n }\n /* eslint-enable no-fallthrough */\n\n return utilArrayUnion(coords, c);\n }, []);\n\n if (!geoPolygonIntersectsPolygon(viewport, coords, true)) {\n var extent = geoExtent(d3_geoBounds({ type: 'LineString', coordinates: coords }));\n map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));\n }\n\n return this;\n };\n\n\n init();\n return drawData;\n}\n", "import { fileFetcher } from '../core/file_fetcher';\nimport { svgPath } from './helpers';\n\n\nexport function svgDebug(projection, context) {\n\n function drawDebug(selection) {\n const showTile = context.getDebug('tile');\n const showCollision = context.getDebug('collision');\n const showImagery = context.getDebug('imagery');\n const showTouchTargets = context.getDebug('target');\n const showDownloaded = context.getDebug('downloaded');\n\n let debugData = [];\n if (showTile) {\n debugData.push({ class: 'red', label: 'tile' });\n }\n if (showCollision) {\n debugData.push({ class: 'yellow', label: 'collision' });\n }\n if (showImagery) {\n debugData.push({ class: 'orange', label: 'imagery' });\n }\n if (showTouchTargets) {\n debugData.push({ class: 'pink', label: 'touchTargets' });\n }\n if (showDownloaded) {\n debugData.push({ class: 'purple', label: 'downloaded' });\n }\n\n\n let legend = context.container().select('.main-content')\n .selectAll('.debug-legend')\n .data(debugData.length ? [0] : []);\n\n legend.exit()\n .remove();\n\n legend = legend.enter()\n .append('div')\n .attr('class', 'fillD debug-legend')\n .merge(legend);\n\n\n let legendItems = legend.selectAll('.debug-legend-item')\n .data(debugData, d => d.label);\n\n legendItems.exit()\n .remove();\n\n legendItems.enter()\n .append('span')\n .attr('class', d => `debug-legend-item ${d.class}`)\n .text(d => d.label);\n\n\n let layer = selection.selectAll('.layer-debug')\n .data(showImagery || showDownloaded ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-debug')\n .merge(layer);\n\n\n // imagery\n const extent = context.map().extent();\n fileFetcher.get('imagery')\n .then(d => {\n const hits = (showImagery && d.query.bbox(extent.rectangle(), true)) || [];\n const features = hits.map(d => d.features[d.id]);\n\n let imagery = layer.selectAll('path.debug-imagery')\n .data(features);\n\n imagery.exit()\n .remove();\n\n imagery.enter()\n .append('path')\n .attr('class', 'debug-imagery debug orange');\n })\n .catch(() => { /* ignore */ });\n\n // downloaded\n const osm = context.connection();\n let dataDownloaded = [];\n if (osm && showDownloaded) {\n const rtree = osm.caches('get').tile.rtree;\n dataDownloaded = rtree.all().map(bbox => {\n return {\n type: 'Feature',\n properties: { id: bbox.id },\n geometry: {\n type: 'Polygon',\n coordinates: [[\n [ bbox.minX, bbox.minY ],\n [ bbox.minX, bbox.maxY ],\n [ bbox.maxX, bbox.maxY ],\n [ bbox.maxX, bbox.minY ],\n [ bbox.minX, bbox.minY ]\n ]]\n }\n };\n });\n }\n\n let downloaded = layer\n .selectAll('path.debug-downloaded')\n .data(showDownloaded ? dataDownloaded : []);\n\n downloaded.exit()\n .remove();\n\n downloaded.enter()\n .append('path')\n .attr('class', 'debug-downloaded debug purple');\n\n // update\n layer.selectAll('path')\n .attr('d', svgPath(projection).geojson);\n }\n\n\n // This looks strange because `enabled` methods on other layers are\n // chainable getter/setters, and this one is just a getter.\n drawDebug.enabled = function() {\n if (!arguments.length) {\n return context.getDebug('tile') ||\n context.getDebug('collision') ||\n context.getDebug('imagery') ||\n context.getDebug('target') ||\n context.getDebug('downloaded');\n } else {\n return this;\n }\n };\n\n\n return drawDebug;\n}\n", "import { svg as d3_svg } from 'd3-fetch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { utilArrayUniq } from '../util';\n\n\n/*\n A standalone SVG element that contains only a `defs` sub-element. To be\n used once globally, since defs IDs must be unique within a document.\n*/\nexport function svgDefs(context) {\n\n var _defsSelection = d3_select(null);\n\n var _spritesheetIds = [\n 'iD-sprite', 'maki-sprite', 'temaki-sprite', 'fa-sprite', 'roentgen-sprite', 'community-sprite'\n ];\n\n function drawDefs(selection) {\n _defsSelection = selection.append('defs');\n\n // add markers\n\n // SVG markers have to be given a colour where they're defined\n // (they can't inherit it from the line they're attached to),\n // so we need to manually define markers for each color of tag\n // (also, it's slightly nicer if we can control the\n // positioning for different tags)\n\n /** @param {string} name @param {string} colour */\n function addOnewayMarker(name, colour) {\n _defsSelection\n .append('marker')\n .attr('id', `ideditor-oneway-marker-${name}`)\n .attr('viewBox', '0 0 10 5')\n .attr('refX', 4)\n .attr('refY', 2.5)\n .attr('markerWidth', 2)\n .attr('markerHeight', 2)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'oneway-marker-path')\n .attr('d', 'M 6,3 L 0,3 L 0,2 L 6,2 L 5,0 L 10,2.5 L 5,5 z')\n .attr('stroke', 'none')\n .attr('fill', colour)\n .attr('opacity', '1');\n }\n addOnewayMarker('black', '#333'); // default\n addOnewayMarker('white', '#fff'); // for dark lines (bridges under construction, railways, etc.)\n addOnewayMarker('gray', '#eee'); // for railway lines\n\n\n function addSidedMarker(name, color, offset, style) {\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-sided-marker-' + name)\n .attr('viewBox', '0 0 2 2')\n .attr('refX', 1)\n .attr('refY', -offset)\n .attr('markerWidth', 1.5)\n .attr('markerHeight', 1.5)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'sided-marker-path sided-marker-' + name + '-path')\n .attr('d', style === 'circle'\n ? 'M 0,0.5 a 0.5,0.5 0 1,0 1,0 a 0.5,0.5 0 1,0 -1,0'\n : 'M 0,0 L 1,1 L 2,0 z')\n .attr('stroke', 'none')\n .attr('fill', color);\n }\n addSidedMarker('natural', 'rgb(170, 170, 170)', 0);\n // for a coastline, the arrows are (somewhat unintuitively) on\n // the water side, so let's color them blue (with a gap) to\n // give a stronger indication\n addSidedMarker('coastline', '#77dede', 1);\n addSidedMarker('waterway', '#77dede', 1);\n // barriers have a dashed line, and separating the triangle\n // from the line visually suits that\n addSidedMarker('barrier', '#ddd', 1);\n // dedicated style for guard rails (#9594):\n // marker on opposite side, circles instead of triangles\n addSidedMarker('guard_rail', '#ddd', -1.5, 'circle');\n addSidedMarker('man_made', '#fff', 0);\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z')\n .attr('fill', '#333')\n .attr('fill-opacity', '0.75')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker-wireframe')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 6,14 C 8,13.4 8,13.4 10,14 L 16,3 C 12,0 4,0 0,3 z')\n .attr('fill', 'none')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker-side')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 3 14 C 8 13 8 13 13 14 L 8 5 Z')\n .attr('fill', '#333')\n .attr('fill-opacity', '0.75')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n _defsSelection\n .append('marker')\n .attr('id', 'ideditor-viewfield-marker-side-wireframe')\n .attr('viewBox', '0 0 16 16')\n .attr('refX', 8)\n .attr('refY', 16)\n .attr('markerWidth', 4)\n .attr('markerHeight', 4)\n .attr('markerUnits', 'strokeWidth')\n .attr('orient', 'auto')\n .append('path')\n .attr('class', 'viewfield-marker-path')\n .attr('d', 'M 3 14 C 8 13 8 13 13 14 L 8 5 Z')\n .attr('fill', 'none')\n .attr('stroke', '#fff')\n .attr('stroke-width', '0.5px')\n .attr('stroke-opacity', '0.75');\n\n\n // add patterns\n var patterns = _defsSelection.selectAll('pattern')\n .data([\n // pattern name, pattern image name\n ['beach', 'dots'],\n ['construction', 'construction'],\n ['cemetery', 'cemetery'],\n ['cemetery_christian', 'cemetery_christian'],\n ['cemetery_buddhist', 'cemetery_buddhist'],\n ['cemetery_muslim', 'cemetery_muslim'],\n ['cemetery_jewish', 'cemetery_jewish'],\n ['farmland', 'farmland'],\n ['farmyard', 'farmyard'],\n ['forest', 'forest'],\n ['forest_broadleaved', 'forest_broadleaved'],\n ['forest_needleleaved', 'forest_needleleaved'],\n ['forest_leafless', 'forest_leafless'],\n ['golf_green', 'grass'],\n ['grass', 'grass'],\n ['landfill', 'landfill'],\n ['meadow', 'grass'],\n ['orchard', 'orchard'],\n ['pond', 'pond'],\n ['quarry', 'quarry'],\n ['scrub', 'bushes'],\n ['vineyard', 'vineyard'],\n ['water_standing', 'lines'],\n ['waves', 'waves'],\n ['wetland', 'wetland'],\n ['wetland_marsh', 'wetland_marsh'],\n ['wetland_swamp', 'wetland_swamp'],\n ['wetland_bog', 'wetland_bog'],\n ['wetland_reedbed', 'wetland_reedbed']\n ])\n .enter()\n .append('pattern')\n .attr('id', function (d) { return 'ideditor-pattern-' + d[0]; })\n .attr('width', 32)\n .attr('height', 32)\n .attr('patternUnits', 'userSpaceOnUse');\n\n patterns\n .append('rect')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', 32)\n .attr('height', 32)\n .attr('class', function (d) { return 'pattern-color-' + d[0]; });\n\n patterns\n .append('image')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', 32)\n .attr('height', 32)\n .attr('xlink:href', function (d) {\n return context.imagePath('pattern/' + d[1] + '.png');\n });\n\n // add clip paths\n _defsSelection.selectAll('clipPath')\n .data([12, 18, 20, 32, 45])\n .enter()\n .append('clipPath')\n .attr('id', function (d) { return 'ideditor-clip-square-' + d; })\n .append('rect')\n .attr('x', 0)\n .attr('y', 0)\n .attr('width', function (d) { return d; })\n .attr('height', function (d) { return d; });\n\n // add svg filters\n const filters = _defsSelection.selectAll('filter')\n .data(['alpha-slope5'])\n .enter()\n .append('filter')\n .attr('id', d => d);\n // Alters the alpha channel such that everything but\n // (almost) transparent pixels are rendered fully opaque:\n // This is used in a workaround for how chrome is rendering\n // the edges of `img` elements when the page zoom is not a\n // \"round value\": the semi-transparent pixels of neighboring\n // tiles cannot \"add up\" to a fully opaque background layer.\n // See https://github.com/openstreetmap/iD/issues/10747\n // and https://github.com/openstreetmap/iD/pull/10594\n const alphaSlope5 = filters.filter('#alpha-slope5')\n .append('feComponentTransfer');\n alphaSlope5.append('feFuncR').attr('type', 'identity');\n alphaSlope5.append('feFuncG').attr('type', 'identity');\n alphaSlope5.append('feFuncB').attr('type', 'identity');\n alphaSlope5.append('feFuncA')\n .attr('type', 'linear')\n .attr('slope', 5);\n\n // add symbol spritesheets\n addSprites(_spritesheetIds, true);\n }\n\n function addSprites(ids, overrideColors) {\n _spritesheetIds = utilArrayUniq(_spritesheetIds.concat(ids));\n\n var spritesheets = _defsSelection\n .selectAll('.spritesheet')\n .data(_spritesheetIds);\n\n spritesheets\n .enter()\n .append('g')\n .attr('class', function(d) { return 'spritesheet spritesheet-' + d; })\n .each(function(d) {\n var url = context.imagePath(d + '.svg');\n var node = d3_select(this).node();\n\n d3_svg(url)\n .then(function(svg) {\n node.appendChild(\n d3_select(svg.documentElement).attr('id', 'ideditor-' + d).node()\n );\n if (overrideColors && d !== 'iD-sprite') { // allow icon colors to be overridden..\n d3_select(node).selectAll('path')\n .attr('fill', 'currentColor');\n }\n })\n .catch(function() {\n /* ignore */\n });\n });\n\n spritesheets\n .exit()\n .remove();\n }\n\n drawDefs.addSprites = addSprites;\n\n return drawDefs;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { svgPointTransform } from './helpers';\nimport { geoMetersToLat } from '../geo';\n\n\nexport function svgGeolocate(projection) {\n var layer = d3_select(null);\n var _position;\n\n\n function init() {\n if (svgGeolocate.initialized) return; // run once\n svgGeolocate.enabled = false;\n svgGeolocate.initialized = true;\n }\n\n function showLayer() {\n layer.style('display', 'block');\n }\n\n\n function hideLayer() {\n layer\n .transition()\n .duration(250)\n .style('opacity', 0);\n }\n\n function layerOn() {\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1);\n\n }\n\n function layerOff() {\n layer.style('display', 'none');\n }\n\n function transform(d) {\n return svgPointTransform(projection)(d);\n }\n\n function accuracy(accuracy, loc) { // converts accuracy to pixels...\n var degreesRadius = geoMetersToLat(accuracy),\n tangentLoc = [loc[0], loc[1] + degreesRadius],\n projectedTangent = projection(tangentLoc),\n projectedLoc = projection([loc[0], loc[1]]);\n\n // southern most point will have higher pixel value...\n return Math.round(projectedLoc[1] - projectedTangent[1]).toString();\n }\n\n function update() {\n var geolocation = { loc: [_position.coords.longitude, _position.coords.latitude] };\n\n var groups = layer.selectAll('.geolocations').selectAll('.geolocation')\n .data([geolocation]);\n\n groups.exit()\n .remove();\n\n var pointsEnter = groups.enter()\n .append('g')\n .attr('class', 'geolocation');\n\n pointsEnter\n .append('circle')\n .attr('class', 'geolocate-radius')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('fill', 'rgb(15,128,225)')\n .attr('fill-opacity', '0.3')\n .attr('r', '0');\n\n pointsEnter\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('fill', 'rgb(15,128,225)')\n .attr('stroke', 'white')\n .attr('stroke-width', '1.5')\n .attr('r', '6');\n\n groups.merge(pointsEnter)\n .attr('transform', transform);\n\n layer.select('.geolocate-radius').attr('r', accuracy(_position.coords.accuracy, geolocation.loc));\n }\n\n function drawLocation(selection) {\n var enabled = svgGeolocate.enabled;\n\n layer = selection.selectAll('.layer-geolocate')\n .data([0]);\n\n layer.exit()\n .remove();\n\n var layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-geolocate')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'geolocations');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n update();\n } else {\n layerOff();\n }\n }\n\n drawLocation.enabled = function (position, enabled) {\n if (!arguments.length) return svgGeolocate.enabled;\n _position = position;\n svgGeolocate.enabled = enabled;\n if (svgGeolocate.enabled) {\n showLayer();\n layerOn();\n } else {\n hideLayer();\n }\n return this;\n };\n\n init();\n return drawLocation;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { geoPath as d3_geoPath } from 'd3-geo';\nimport RBush from 'rbush';\nimport { localizer } from '../core/localizer';\n\nimport {\n geoExtent, geoPolygonIntersectsPolygon, geoPathLength,\n geoScaleToZoom, geoVecInterp, geoVecLength\n} from '../geo';\nimport { presetManager } from '../presets';\nimport { osmEntity, osmIsInterestingTag } from '../osm';\nimport { utilDetect } from '../util/detect';\nimport { utilArrayDifference, utilArrayUniq, utilDisplayName, utilDisplayNameForPath, utilEntitySelector } from '../util';\n\n\n\nexport function svgLabels(projection, context) {\n var path = d3_geoPath(projection);\n var detected = utilDetect();\n var baselineHack = detected.browser.toLowerCase() === 'safari';\n\n var _rdrawn = new RBush();\n var _rskipped = new RBush();\n var _entitybboxes = {};\n\n // Listed from highest to lowest priority\n const labelStack = [\n ['line', 'aeroway', '*', 12],\n ['line', 'highway', 'motorway', 12],\n ['line', 'highway', 'trunk', 12],\n ['line', 'highway', 'primary', 12],\n ['line', 'highway', 'secondary', 12],\n ['line', 'highway', 'tertiary', 12],\n ['line', 'highway', '*', 12],\n ['line', 'railway', '*', 12],\n ['line', 'waterway', '*', 12],\n ['area', 'aeroway', '*', 12],\n ['area', 'amenity', '*', 12],\n ['area', 'building', '*', 12],\n ['area', 'historic', '*', 12],\n ['area', 'leisure', '*', 12],\n ['area', 'man_made', '*', 12],\n ['area', 'natural', '*', 12],\n ['area', 'shop', '*', 12],\n ['area', 'craft', '*', 12],\n ['area', 'tourism', '*', 12],\n ['area', 'camp_site', '*', 12],\n ['point', 'aeroway', '*', 10],\n ['point', 'amenity', '*', 10],\n ['point', 'building', '*', 10],\n ['point', 'historic', '*', 10],\n ['point', 'leisure', '*', 10],\n ['point', 'man_made', '*', 10],\n ['point', 'natural', '*', 10],\n ['point', 'shop', '*', 10],\n ['point', 'tourism', '*', 10],\n ['point', 'camp_site', '*', 10],\n ['*', 'alt_name', '*', 12],\n ['*', 'official_name', '*', 12],\n ['*', 'loc_name', '*', 12],\n ['*', 'loc_ref', '*', 12],\n ['*', 'unsigned_ref', '*', 12],\n ['*', 'seamark:name', '*', 12],\n ['*', 'sector:name', '*', 12],\n ['*', 'lock_name', '*', 12],\n ['*', 'distance', '*', 12],\n ['*', 'railway:position', '*', 12],\n ['line', 'ref', '*', 12],\n ['area', 'ref', '*', 12],\n ['point', 'ref', '*', 10],\n ['line', 'name', '*', 12],\n ['area', 'name', '*', 12],\n ['point', 'name', '*', 10],\n ['point', 'addr:housenumber', '*', 10],\n ['point', 'addr:housename', '*', 10]\n ];\n\n\n function shouldSkipIcon(preset) {\n var noIcons = ['building', 'landuse', 'natural'];\n return noIcons.some(function(s) {\n return preset.id.indexOf(s) >= 0;\n });\n }\n\n\n function drawLinePaths(selection, labels, filter, classes) {\n var paths = selection.selectAll('path:not(.debug)')\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n paths.exit()\n .remove();\n\n // enter/update\n paths.enter()\n .append('path')\n .style('stroke-width', d => d.position['font-size'])\n .attr('id', d => 'ideditor-labelpath-' + d.entity.id)\n .attr('class', classes)\n .merge(paths)\n .attr('d', d => d.position.lineString);\n }\n\n\n function drawLineLabels(selection, labels, filter, classes) {\n var texts = selection.selectAll('text.' + classes)\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n texts.exit()\n .remove();\n\n // enter\n texts.enter()\n .append('text')\n .attr('class', d => classes + ' ' + d.position.classes + ' ' + d.entity.id)\n .attr('dy', baselineHack ? '0.35em' : null)\n .append('textPath')\n .attr('class', 'textpath');\n\n // update\n selection.selectAll('text.' + classes).selectAll('.textpath')\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity))\n .attr('startOffset', '50%')\n .attr('xlink:href', function(d) { return '#ideditor-labelpath-' + d.entity.id; })\n .text(d => d.name);\n }\n\n\n function drawPointLabels(selection, labels, filter, classes) {\n if (classes.includes('pointlabel-halo')) {\n labels = labels.filter(d => !d.position.isAddr);\n }\n var texts = selection.selectAll('text.' + classes)\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n texts.exit()\n .remove();\n\n // enter/update\n texts.enter()\n .append('text')\n .attr('class', d => classes + ' ' + d.position.classes + ' ' + d.entity.id)\n .style('text-anchor', d => d.position.textAnchor)\n .text(d => d.name)\n .merge(texts)\n .attr('x', d => d.position.x)\n .attr('y', d => d.position.y);\n }\n\n\n function drawAreaLabels(selection, labels, filter, classes) {\n labels = labels.filter(hasText);\n drawPointLabels(selection, labels, filter, classes);\n\n function hasText(d) {\n return d.position.hasOwnProperty('x') && d.position.hasOwnProperty('y');\n }\n }\n\n\n function drawAreaIcons(selection, labels, filter, classes) {\n var icons = selection.selectAll('use.' + classes)\n .filter(d => filter(d.entity))\n .data(labels, d => osmEntity.key(d.entity));\n\n // exit\n icons.exit()\n .remove();\n\n // enter/update\n icons.enter()\n .append('use')\n .attr('class', 'icon ' + classes)\n .attr('width', '17px')\n .attr('height', '17px')\n .merge(icons)\n .attr('transform', d => d.position.transform)\n .attr('xlink:href', function(d) {\n var preset = presetManager.match(d.entity, context.graph());\n var picon = preset && preset.icon;\n return picon ? '#' + picon : '';\n });\n }\n\n\n function drawCollisionBoxes(selection, rtree, which) {\n var classes = 'debug ' + which + ' ' + (which === 'debug-skipped' ? 'orange' : 'yellow');\n\n var gj = [];\n if (context.getDebug('collision')) {\n gj = rtree.all().map(function(d) {\n return { type: 'Polygon', coordinates: [[\n [d.minX, d.minY],\n [d.maxX, d.minY],\n [d.maxX, d.maxY],\n [d.minX, d.maxY],\n [d.minX, d.minY]\n ]]};\n });\n }\n\n var boxes = selection.selectAll('.' + which)\n .data(gj);\n\n // exit\n boxes.exit()\n .remove();\n\n // enter/update\n boxes.enter()\n .append('path')\n .attr('class', classes)\n .merge(boxes)\n .attr('d', d3_geoPath());\n }\n\n\n function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) {\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n\n var labelable = [];\n var renderNodeAs = {};\n var i, j, k, entity, geometry;\n\n for (i = 0; i < labelStack.length; i++) {\n labelable.push([]);\n }\n\n if (fullRedraw) {\n _rdrawn.clear();\n _rskipped.clear();\n _entitybboxes = {};\n\n } else {\n for (i = 0; i < entities.length; i++) {\n entity = entities[i];\n var toRemove = []\n .concat(_entitybboxes[entity.id] || [])\n .concat(_entitybboxes[entity.id + 'I'] || []);\n\n for (j = 0; j < toRemove.length; j++) {\n _rdrawn.remove(toRemove[j]);\n _rskipped.remove(toRemove[j]);\n }\n }\n }\n\n // Loop through all the entities to do some preprocessing\n for (i = 0; i < entities.length; i++) {\n entity = entities[i];\n geometry = entity.geometry(graph);\n\n // Insert collision boxes around interesting points/vertices\n if (geometry === 'point' || (geometry === 'vertex' && isInterestingVertex(entity))) {\n const isAddr = isAddressPoint(entity.tags);\n var hasDirections = entity.directions(graph, projection).length;\n var markerPadding = 0;\n\n if (wireframe) {\n renderNodeAs[entity.id] = { geometry: 'vertex', isAddr };\n } else if (geometry === 'vertex') {\n renderNodeAs[entity.id] = { geometry: 'vertex', isAddr };\n } else if (zoom >= 18 && hasDirections) {\n renderNodeAs[entity.id] = { geometry: 'vertex', isAddr };\n } else {\n renderNodeAs[entity.id] = { geometry: 'point', isAddr };\n markerPadding = 20; // extra y for marker height\n }\n\n if (isAddr) {\n undoInsert(entity.id + 'P');\n } else {\n var coord = projection(entity.loc);\n var nodePadding = 10;\n var bbox = {\n minX: coord[0] - nodePadding,\n minY: coord[1] - nodePadding - markerPadding,\n maxX: coord[0] + nodePadding,\n maxY: coord[1] + nodePadding\n };\n doInsert(bbox, entity.id + 'P');\n }\n }\n\n // From here on, treat vertices like points\n if (geometry === 'vertex') {\n geometry = 'point';\n }\n\n // Determine which entities are label-able\n var preset = geometry === 'area' && presetManager.match(entity, graph);\n var icon = preset && !shouldSkipIcon(preset) && preset.icon;\n\n if (!icon && !utilDisplayName(entity, { isMapLabel: true })) continue;\n\n for (k = 0; k < labelStack.length; k++) {\n var matchGeom = labelStack[k][0];\n var matchKey = labelStack[k][1];\n var matchVal = labelStack[k][2];\n var hasVal = entity.tags[matchKey];\n\n if ((matchGeom === '*' || geometry === matchGeom) && hasVal && (matchVal === '*' || matchVal === hasVal)) {\n labelable[k].push(entity);\n break;\n }\n }\n }\n\n var labelled = {\n point: [],\n line: [],\n area: []\n };\n\n // Try and find a valid label for labellable entities\n for (k = 0; k < labelable.length; k++) {\n var fontSize = labelStack[k][3];\n\n for (i = 0; i < labelable[k].length; i++) {\n entity = labelable[k][i];\n geometry = entity.geometry(graph);\n\n let name = geometry === 'line'\n ? utilDisplayNameForPath(entity)\n : utilDisplayName(entity, { isMapLabel: true });\n var width = name && textWidth(name, fontSize, selection.select('g.layer-osm.labels').node());\n var p = null;\n\n if (geometry === 'point' || geometry === 'vertex') {\n // no point or vertex labels in wireframe mode\n // no vertex labels at low zooms (vertices have no icons)\n if (wireframe) continue;\n var renderAs = renderNodeAs[entity.id];\n if (renderAs.geometry === 'vertex' && zoom < 17) continue;\n while (renderAs.isAddr && width > 36) {\n name = `${name.substring(0, name.replace(/\u2026$/, '').length - 1)}\u2026`;\n width = textWidth(name, fontSize, selection.select('g.layer-osm.labels').node());\n }\n\n p = getPointLabel(entity, width, fontSize, renderAs);\n } else if (geometry === 'line') {\n p = getLineLabel(entity, width, fontSize);\n\n } else if (geometry === 'area') {\n p = getAreaLabel(entity, width, fontSize);\n }\n\n if (p) {\n if (geometry === 'vertex') { geometry = 'point'; } // treat vertex like point\n p.classes = geometry + ' tag-' + labelStack[k][1];\n labelled[geometry].push({\n entity,\n name,\n position: p\n });\n }\n }\n }\n\n\n function isInterestingVertex(entity) {\n var selectedIDs = context.selectedIDs();\n\n return entity.hasInterestingTags() ||\n entity.isEndpoint(graph) ||\n entity.isConnected(graph) ||\n selectedIDs.indexOf(entity.id) !== -1 ||\n graph.parentWays(entity).some(function(parent) {\n return selectedIDs.indexOf(parent.id) !== -1;\n });\n }\n\n\n function getPointLabel(entity, width, height, style) {\n var y = (style.geometry === 'point' ? -12 : 0);\n var pointOffsets = {\n ltr: [15, y, 'start'],\n rtl: [-15, y, 'end']\n };\n const isAddrMarker = style.isAddr && style.geometry !== 'vertex';\n\n var textDirection = localizer.textDirection();\n\n var coord = projection(entity.loc);\n var textPadding = 2;\n var offset = pointOffsets[textDirection];\n if (isAddrMarker) offset = [0, 1, 'middle'];\n var p = {\n height: height,\n width: width,\n x: coord[0] + offset[0],\n y: coord[1] + offset[1],\n textAnchor: offset[2]\n };\n\n // insert a collision box for the text label..\n let bbox;\n if (isAddrMarker) {\n bbox = {\n minX: p.x - (width / 2) - textPadding,\n minY: p.y - (height / 2) - textPadding,\n maxX: p.x + (width / 2) + textPadding,\n maxY: p.y + (height / 2) + textPadding\n };\n } else if (textDirection === 'rtl') {\n bbox = {\n minX: p.x - width - textPadding,\n minY: p.y - (height / 2) - textPadding,\n maxX: p.x + textPadding,\n maxY: p.y + (height / 2) + textPadding\n };\n } else {\n bbox = {\n minX: p.x - textPadding,\n minY: p.y - (height / 2) - textPadding,\n maxX: p.x + width + textPadding,\n maxY: p.y + (height / 2) + textPadding\n };\n }\n\n if (tryInsert([bbox], entity.id, true)) {\n return p;\n }\n }\n\n\n function getLineLabel(entity, width, height) {\n var viewport = geoExtent(context.projection.clipExtent()).polygon();\n var points = graph.childNodes(entity)\n .map(function(node) { return projection(node.loc); });\n var length = geoPathLength(points);\n\n if (length < width + 20) return;\n\n // % along the line to attempt to place the label\n var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70,\n 25, 75, 20, 80, 15, 95, 10, 90, 5, 95];\n var padding = 3;\n\n for (var i = 0; i < lineOffsets.length; i++) {\n var offset = lineOffsets[i];\n var middle = offset / 100 * length;\n var start = middle - width / 2;\n\n if (start < 0 || start + width > length) continue;\n\n // generate subpath and ignore paths that are invalid or don't cross viewport.\n var sub = subpath(points, start, start + width);\n if (!sub || !geoPolygonIntersectsPolygon(viewport, sub, true)) {\n continue;\n }\n\n var isReverse = reverse(sub);\n if (isReverse) {\n sub = sub.reverse();\n }\n\n var bboxes = [];\n var boxsize = (height + 2) / 2;\n\n for (var j = 0; j < sub.length - 1; j++) {\n var a = sub[j];\n var b = sub[j + 1];\n\n // split up the text into small collision boxes\n var num = Math.max(1, Math.floor(geoVecLength(a, b) / boxsize / 2));\n\n for (var box = 0; box < num; box++) {\n var p = geoVecInterp(a, b, box / num);\n var x0 = p[0] - boxsize - padding;\n var y0 = p[1] - boxsize - padding;\n var x1 = p[0] + boxsize + padding;\n var y1 = p[1] + boxsize + padding;\n\n bboxes.push({\n minX: Math.min(x0, x1),\n minY: Math.min(y0, y1),\n maxX: Math.max(x0, x1),\n maxY: Math.max(y0, y1)\n });\n }\n }\n\n if (tryInsert(bboxes, entity.id, false)) { // accept this one\n return {\n 'font-size': height + 2,\n lineString: lineString(sub),\n startOffset: offset + '%'\n };\n }\n }\n\n function reverse(p) {\n var angle = Math.atan2(p[1][1] - p[0][1], p[1][0] - p[0][0]);\n return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2);\n }\n\n function lineString(points) {\n return 'M' + points.join('L');\n }\n\n function subpath(points, from, to) {\n var sofar = 0;\n var start, end, i0, i1;\n\n for (var i = 0; i < points.length - 1; i++) {\n var a = points[i];\n var b = points[i + 1];\n var current = geoVecLength(a, b);\n var portion;\n if (!start && sofar + current >= from) {\n portion = (from - sofar) / current;\n start = [\n a[0] + portion * (b[0] - a[0]),\n a[1] + portion * (b[1] - a[1])\n ];\n i0 = i + 1;\n }\n if (!end && sofar + current >= to) {\n portion = (to - sofar) / current;\n end = [\n a[0] + portion * (b[0] - a[0]),\n a[1] + portion * (b[1] - a[1])\n ];\n i1 = i + 1;\n }\n sofar += current;\n }\n\n var result = points.slice(i0, i1);\n result.unshift(start);\n result.push(end);\n return result;\n }\n }\n\n\n function getAreaLabel(entity, width, height) {\n var centroid = path.centroid(entity.asGeoJSON(graph));\n var extent = entity.extent(graph);\n var areaWidth = projection(extent[1])[0] - projection(extent[0])[0];\n\n if (isNaN(centroid[0]) || areaWidth < 20) return;\n\n var preset = presetManager.match(entity, context.graph());\n var picon = preset && preset.icon;\n var iconSize = 17;\n var padding = 2;\n var p = {};\n\n if (picon && !shouldSkipIcon(preset)) { // icon and label..\n if (addIcon()) {\n addLabel(iconSize + padding);\n return p;\n }\n } else { // label only..\n if (addLabel(0)) {\n return p;\n }\n }\n\n\n function addIcon() {\n var iconX = centroid[0] - (iconSize / 2);\n var iconY = centroid[1] - (iconSize / 2);\n var bbox = {\n minX: iconX,\n minY: iconY,\n maxX: iconX + iconSize,\n maxY: iconY + iconSize\n };\n\n if (tryInsert([bbox], entity.id + 'I', true)) {\n p.transform = 'translate(' + iconX + ',' + iconY + ')';\n return true;\n }\n return false;\n }\n\n function addLabel(yOffset) {\n if (width && areaWidth >= width + 20) {\n var labelX = centroid[0];\n var labelY = centroid[1] + yOffset;\n var bbox = {\n minX: labelX - (width / 2) - padding,\n minY: labelY - (height / 2) - padding,\n maxX: labelX + (width / 2) + padding,\n maxY: labelY + (height / 2) + padding\n };\n\n if (tryInsert([bbox], entity.id, true)) {\n p.x = labelX;\n p.y = labelY;\n p.textAnchor = 'middle';\n p.height = height;\n return true;\n }\n }\n return false;\n }\n }\n\n\n // force insert a singular bounding box\n // singular box only, no array, id better be unique\n function doInsert(bbox, id) {\n bbox.id = id;\n\n var oldbox = _entitybboxes[id];\n if (oldbox) {\n _rdrawn.remove(oldbox);\n }\n _entitybboxes[id] = bbox;\n _rdrawn.insert(bbox);\n }\n\n function undoInsert(id) {\n var oldbox = _entitybboxes[id];\n if (oldbox) {\n _rdrawn.remove(oldbox);\n }\n delete _entitybboxes[id];\n }\n\n function tryInsert(bboxes, id, saveSkipped) {\n var skipped = false;\n\n for (var i = 0; i < bboxes.length; i++) {\n var bbox = bboxes[i];\n bbox.id = id;\n\n // Check that label is visible\n if (bbox.minX < 0 || bbox.minY < 0 || bbox.maxX > dimensions[0] || bbox.maxY > dimensions[1]) {\n skipped = true;\n break;\n }\n if (_rdrawn.collides(bbox)) {\n skipped = true;\n break;\n }\n }\n\n _entitybboxes[id] = bboxes;\n\n if (skipped) {\n if (saveSkipped) {\n _rskipped.load(bboxes);\n }\n } else {\n _rdrawn.load(bboxes);\n }\n\n return !skipped;\n }\n\n\n var layer = selection.selectAll('.layer-osm.labels');\n layer.selectAll('.labels-group')\n .data(['halo', 'label', 'debug'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'labels-group ' + d; });\n\n var halo = layer.selectAll('.labels-group.halo');\n var label = layer.selectAll('.labels-group.label');\n var debug = layer.selectAll('.labels-group.debug');\n\n // points\n drawPointLabels(label, labelled.point, filter, 'pointlabel');\n drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo');\n\n // lines\n drawLinePaths(layer, labelled.line, filter, '');\n drawLineLabels(label, labelled.line, filter, 'linelabel');\n drawLineLabels(halo, labelled.line, filter, 'linelabel-halo');\n\n // areas\n drawAreaLabels(label, labelled.area, filter, 'arealabel');\n drawAreaLabels(halo, labelled.area, filter, 'arealabel-halo');\n drawAreaIcons(label, labelled.area, filter, 'areaicon');\n drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo');\n\n // debug\n drawCollisionBoxes(debug, _rskipped, 'debug-skipped');\n drawCollisionBoxes(debug, _rdrawn, 'debug-drawn');\n\n layer.call(filterLabels);\n }\n\n\n function filterLabels(selection) {\n var drawLayer = selection.selectAll('.layer-osm.labels');\n var layers = drawLayer.selectAll('.labels-group.halo, .labels-group.label');\n\n layers.selectAll('.nolabel')\n .classed('nolabel', false);\n\n const graph = context.graph();\n const mouse = context.map().mouse();\n let bbox;\n let hideIds = [];\n\n // hide labels near the mouse\n if (mouse && context.mode().id !== 'browse') {\n const pad = 20;\n bbox = { minX: mouse[0] - pad, minY: mouse[1] - pad, maxX: mouse[0] + pad, maxY: mouse[1] + pad };\n const nearMouse = _rdrawn.search(bbox)\n .map(entity => entity.id)\n .filter(id =>\n context.mode().id !== 'select' ||\n // in select mode: hide labels of currently selected line(s)\n // to still allow accessing midpoints\n // https://github.com/openstreetmap/iD/issues/11220\n context.mode().selectedIDs().includes(id) && graph.hasEntity(id)?.geometry(graph) === 'line');\n hideIds.push.apply(hideIds, nearMouse);\n hideIds = utilArrayUniq(hideIds);\n }\n\n // don't hide label of currently selected entity while in e.g. drag mode\n const selected = (context.mode()?.selectedIDs?.() || [])\n .filter(id => graph.hasEntity(id)?.geometry(graph) !== 'line');\n hideIds = utilArrayDifference(hideIds, selected);\n\n layers.selectAll(utilEntitySelector(hideIds))\n .classed('nolabel', true);\n\n\n // draw the mouse bbox if debugging is on..\n var debug = selection.selectAll('.labels-group.debug');\n var gj = [];\n if (context.getDebug('collision')) {\n gj = bbox ? [{\n type: 'Polygon',\n coordinates: [[\n [bbox.minX, bbox.minY],\n [bbox.maxX, bbox.minY],\n [bbox.maxX, bbox.maxY],\n [bbox.minX, bbox.maxY],\n [bbox.minX, bbox.minY]\n ]]\n }] : [];\n }\n\n var box = debug.selectAll('.debug-mouse')\n .data(gj);\n\n // exit\n box.exit()\n .remove();\n\n // enter/update\n box.enter()\n .append('path')\n .attr('class', 'debug debug-mouse yellow')\n .merge(box)\n .attr('d', d3_geoPath());\n }\n\n\n var throttleFilterLabels = throttle(filterLabels, 100);\n\n\n drawLabels.observe = function(selection) {\n var listener = function() { throttleFilterLabels(selection); };\n selection.on('mousemove.hidelabels', listener);\n context.on('enter.hidelabels', listener);\n };\n\n\n drawLabels.off = function(selection) {\n throttleFilterLabels.cancel();\n selection.on('mousemove.hidelabels', null);\n context.on('enter.hidelabels', null);\n };\n\n\n return drawLabels;\n}\n\n\nconst _textWidthCache = {};\nexport function textWidth(text, size, container) {\n _textWidthCache[size] ||= {};\n let c = _textWidthCache[size];\n\n if (c[text]) {\n return c[text];\n }\n const elem = document.createElementNS('http://www.w3.org/2000/svg', 'text');\n elem.style.fontSize = `${size}px`;\n elem.style.fontWeight = 'bold';\n elem.textContent = text;\n container.appendChild(elem);\n c[text] = elem.getComputedTextLength();\n elem.remove();\n return c[text];\n}\n\n\nconst nonPrimaryKeys = new Set([\n 'building:flats',\n 'check_date',\n 'fixme',\n 'layer',\n 'level',\n 'level:ref',\n 'note'\n]);\nconst nonPrimaryKeysRegex = /^(ref|survey|note|([^:]+:|old_|alt_)addr):/;\nexport function isAddressPoint(tags) {\n const keys = Object.keys(tags).filter(key =>\n osmIsInterestingTag(key) &&\n !nonPrimaryKeys.has(key) &&\n !nonPrimaryKeysRegex.test(key)\n );\n return keys.length > 0 && keys.every(key =>\n key.startsWith('addr:')\n );\n}\n", "var e=\"undefined\"!=typeof self?self:global;const t=\"undefined\"!=typeof navigator,i=t&&\"undefined\"==typeof HTMLImageElement,n=!(\"undefined\"==typeof global||\"undefined\"==typeof process||!process.versions||!process.versions.node),s=e.Buffer,r=e.BigInt,a=!!s,o=e=>e;function l(e,t=o){if(n)try{return\"function\"==typeof require?Promise.resolve(t(require(e))):import(/* webpackIgnore: true */ e).then(t)}catch(t){console.warn(`Couldn't load ${e}`)}}let h=e.fetch;const u=e=>h=e;if(!e.fetch){const e=l(\"http\",(e=>e)),t=l(\"https\",(e=>e)),i=(n,{headers:s}={})=>new Promise((async(r,a)=>{let{port:o,hostname:l,pathname:h,protocol:u,search:c}=new URL(n);const f={method:\"GET\",hostname:l,path:encodeURI(h)+c,headers:s};\"\"!==o&&(f.port=Number(o));const d=(\"https:\"===u?await t:await e).request(f,(e=>{if(301===e.statusCode||302===e.statusCode){let t=new URL(e.headers.location,n).toString();return i(t,{headers:s}).then(r).catch(a)}r({status:e.statusCode,arrayBuffer:()=>new Promise((t=>{let i=[];e.on(\"data\",(e=>i.push(e))),e.on(\"end\",(()=>t(Buffer.concat(i))))}))})}));d.on(\"error\",a),d.end()}));u(i)}function c(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}const f=e=>p(e)?void 0:e,d=e=>void 0!==e;function p(e){return void 0===e||(e instanceof Map?0===e.size:0===Object.values(e).filter(d).length)}function g(e){let t=new Error(e);throw delete t.stack,t}function m(e){return\"\"===(e=function(e){for(;e.endsWith(\"\\0\");)e=e.slice(0,-1);return e}(e).trim())?void 0:e}function S(e){let t=function(e){let t=0;return e.ifd0.enabled&&(t+=1024),e.exif.enabled&&(t+=2048),e.makerNote&&(t+=2048),e.userComment&&(t+=1024),e.gps.enabled&&(t+=512),e.interop.enabled&&(t+=100),e.ifd1.enabled&&(t+=1024),t+2048}(e);return e.jfif.enabled&&(t+=50),e.xmp.enabled&&(t+=2e4),e.iptc.enabled&&(t+=14e3),e.icc.enabled&&(t+=6e3),t}const C=e=>String.fromCharCode.apply(null,e),y=\"undefined\"!=typeof TextDecoder?new TextDecoder(\"utf-8\"):void 0;function b(e){return y?y.decode(e):a?Buffer.from(e).toString(\"utf8\"):decodeURIComponent(escape(C(e)))}class I{static from(e,t){return e instanceof this&&e.le===t?e:new I(e,void 0,void 0,t)}constructor(e,t=0,i,n){if(\"boolean\"==typeof n&&(this.le=n),Array.isArray(e)&&(e=new Uint8Array(e)),0===e)this.byteOffset=0,this.byteLength=0;else if(e instanceof ArrayBuffer){void 0===i&&(i=e.byteLength-t);let n=new DataView(e,t,i);this._swapDataView(n)}else if(e instanceof Uint8Array||e instanceof DataView||e instanceof I){void 0===i&&(i=e.byteLength-t),(t+=e.byteOffset)+i>e.byteOffset+e.byteLength&&g(\"Creating view outside of available memory in ArrayBuffer\");let n=new DataView(e.buffer,t,i);this._swapDataView(n)}else if(\"number\"==typeof e){let t=new DataView(new ArrayBuffer(e));this._swapDataView(t)}else g(\"Invalid input argument for BufferView: \"+e)}_swapArrayBuffer(e){this._swapDataView(new DataView(e))}_swapBuffer(e){this._swapDataView(new DataView(e.buffer,e.byteOffset,e.byteLength))}_swapDataView(e){this.dataView=e,this.buffer=e.buffer,this.byteOffset=e.byteOffset,this.byteLength=e.byteLength}_lengthToEnd(e){return this.byteLength-e}set(e,t,i=I){return e instanceof DataView||e instanceof I?e=new Uint8Array(e.buffer,e.byteOffset,e.byteLength):e instanceof ArrayBuffer&&(e=new Uint8Array(e)),e instanceof Uint8Array||g(\"BufferView.set(): Invalid data argument.\"),this.toUint8().set(e,t),new i(this,t,e.byteLength)}subarray(e,t){return t=t||this._lengthToEnd(e),new I(this,e,t)}toUint8(){return new Uint8Array(this.buffer,this.byteOffset,this.byteLength)}getUint8Array(e,t){return new Uint8Array(this.buffer,this.byteOffset+e,t)}getString(e=0,t=this.byteLength){return b(this.getUint8Array(e,t))}getLatin1String(e=0,t=this.byteLength){let i=this.getUint8Array(e,t);return C(i)}getUnicodeString(e=0,t=this.byteLength){const i=[];for(let n=0;n1e4?v(e,i,\"base64\"):n&&e.includes(\"://\")?x(e,i,\"url\",M):n?v(e,i,\"fs\"):t?x(e,i,\"url\",M):void g(\"Invalid input argument\");var s}async function x(e,t,i,n){return A.has(i)?v(e,t,i):n?async function(e,t){let i=await t(e);return new I(i)}(e,n):void g(`Parser ${i} is not loaded`)}async function v(e,t,i){let n=new(A.get(i))(e,t);return await n.read(),n}const M=e=>h(e).then((e=>e.arrayBuffer())),R=e=>new Promise(((t,i)=>{let n=new FileReader;n.onloadend=()=>t(n.result||new ArrayBuffer),n.onerror=i,n.readAsArrayBuffer(e)}));class L extends Map{get tagKeys(){return this.allKeys||(this.allKeys=Array.from(this.keys())),this.allKeys}get tagValues(){return this.allValues||(this.allValues=Array.from(this.values())),this.allValues}}function U(e,t,i){let n=new L;for(let[e,t]of i)n.set(e,t);if(Array.isArray(t))for(let i of t)e.set(i,n);else e.set(t,n);return n}function F(e,t,i){let n,s=e.get(t);for(n of i)s.set(n[0],n[1])}const E=new Map,B=new Map,N=new Map,G=[\"chunked\",\"firstChunkSize\",\"firstChunkSizeNode\",\"firstChunkSizeBrowser\",\"chunkSize\",\"chunkLimit\"],V=[\"jfif\",\"xmp\",\"icc\",\"iptc\",\"ihdr\"],z=[\"tiff\",...V],H=[\"ifd0\",\"ifd1\",\"exif\",\"gps\",\"interop\"],j=[...z,...H],W=[\"makerNote\",\"userComment\"],K=[\"translateKeys\",\"translateValues\",\"reviveValues\",\"multiSegment\"],X=[...K,\"sanitize\",\"mergeOutput\",\"silentErrors\"];class _{get translate(){return this.translateKeys||this.translateValues||this.reviveValues}}class Y extends _{get needed(){return this.enabled||this.deps.size>0}constructor(e,t,i,n){if(super(),c(this,\"enabled\",!1),c(this,\"skip\",new Set),c(this,\"pick\",new Set),c(this,\"deps\",new Set),c(this,\"translateKeys\",!1),c(this,\"translateValues\",!1),c(this,\"reviveValues\",!1),this.key=e,this.enabled=t,this.parse=this.enabled,this.applyInheritables(n),this.canBeFiltered=H.includes(e),this.canBeFiltered&&(this.dict=E.get(e)),void 0!==i)if(Array.isArray(i))this.parse=this.enabled=!0,this.canBeFiltered&&i.length>0&&this.translateTagSet(i,this.pick);else if(\"object\"==typeof i){if(this.enabled=!0,this.parse=!1!==i.parse,this.canBeFiltered){let{pick:e,skip:t}=i;e&&e.length>0&&this.translateTagSet(e,this.pick),t&&t.length>0&&this.translateTagSet(t,this.skip)}this.applyInheritables(i)}else!0===i||!1===i?this.parse=this.enabled=i:g(`Invalid options argument: ${i}`)}applyInheritables(e){let t,i;for(t of K)i=e[t],void 0!==i&&(this[t]=i)}translateTagSet(e,t){if(this.dict){let i,n,{tagKeys:s,tagValues:r}=this.dict;for(i of e)\"string\"==typeof i?(n=r.indexOf(i),-1===n&&(n=s.indexOf(Number(i))),-1!==n&&t.add(Number(s[n]))):t.add(i)}else for(let i of e)t.add(i)}finalizeFilters(){!this.enabled&&this.deps.size>0?(this.enabled=!0,ee(this.pick,this.deps)):this.enabled&&this.pick.size>0&&ee(this.pick,this.deps)}}var $={jfif:!1,tiff:!0,xmp:!1,icc:!1,iptc:!1,ifd0:!0,ifd1:!1,exif:!0,gps:!0,interop:!1,ihdr:void 0,makerNote:!1,userComment:!1,multiSegment:!1,skip:[],pick:[],translateKeys:!0,translateValues:!0,reviveValues:!0,sanitize:!0,mergeOutput:!0,silentErrors:!0,chunked:!0,firstChunkSize:void 0,firstChunkSizeNode:512,firstChunkSizeBrowser:65536,chunkSize:65536,chunkLimit:5},J=new Map;class q extends _{static useCached(e){let t=J.get(e);return void 0!==t||(t=new this(e),J.set(e,t)),t}constructor(e){super(),!0===e?this.setupFromTrue():void 0===e?this.setupFromUndefined():Array.isArray(e)?this.setupFromArray(e):\"object\"==typeof e?this.setupFromObject(e):g(`Invalid options argument ${e}`),void 0===this.firstChunkSize&&(this.firstChunkSize=t?this.firstChunkSizeBrowser:this.firstChunkSizeNode),this.mergeOutput&&(this.ifd1.enabled=!1),this.filterNestedSegmentTags(),this.traverseTiffDependencyTree(),this.checkLoadedPlugins()}setupFromUndefined(){let e;for(e of G)this[e]=$[e];for(e of X)this[e]=$[e];for(e of W)this[e]=$[e];for(e of j)this[e]=new Y(e,$[e],void 0,this)}setupFromTrue(){let e;for(e of G)this[e]=$[e];for(e of X)this[e]=$[e];for(e of W)this[e]=!0;for(e of j)this[e]=new Y(e,!0,void 0,this)}setupFromArray(e){let t;for(t of G)this[t]=$[t];for(t of X)this[t]=$[t];for(t of W)this[t]=$[t];for(t of j)this[t]=new Y(t,!1,void 0,this);this.setupGlobalFilters(e,void 0,H)}setupFromObject(e){let t;for(t of(H.ifd0=H.ifd0||H.image,H.ifd1=H.ifd1||H.thumbnail,Object.assign(this,e),G))this[t]=Z(e[t],$[t]);for(t of X)this[t]=Z(e[t],$[t]);for(t of W)this[t]=Z(e[t],$[t]);for(t of z)this[t]=new Y(t,$[t],e[t],this);for(t of H)this[t]=new Y(t,$[t],e[t],this.tiff);this.setupGlobalFilters(e.pick,e.skip,H,j),!0===e.tiff?this.batchEnableWithBool(H,!0):!1===e.tiff?this.batchEnableWithUserValue(H,e):Array.isArray(e.tiff)?this.setupGlobalFilters(e.tiff,void 0,H):\"object\"==typeof e.tiff&&this.setupGlobalFilters(e.tiff.pick,e.tiff.skip,H)}batchEnableWithBool(e,t){for(let i of e)this[i].enabled=t}batchEnableWithUserValue(e,t){for(let i of e){let e=t[i];this[i].enabled=!1!==e&&void 0!==e}}setupGlobalFilters(e,t,i,n=i){if(e&&e.length){for(let e of n)this[e].enabled=!1;let t=Q(e,i);for(let[e,i]of t)ee(this[e].pick,i),this[e].enabled=!0}else if(t&&t.length){let e=Q(t,i);for(let[t,i]of e)ee(this[t].skip,i)}}filterNestedSegmentTags(){let{ifd0:e,exif:t,xmp:i,iptc:n,icc:s}=this;this.makerNote?t.deps.add(37500):t.skip.add(37500),this.userComment?t.deps.add(37510):t.skip.add(37510),i.enabled||e.skip.add(700),n.enabled||e.skip.add(33723),s.enabled||e.skip.add(34675)}traverseTiffDependencyTree(){let{ifd0:e,exif:t,gps:i,interop:n}=this;n.needed&&(t.deps.add(40965),e.deps.add(40965)),t.needed&&e.deps.add(34665),i.needed&&e.deps.add(34853),this.tiff.enabled=H.some((e=>!0===this[e].enabled))||this.makerNote||this.userComment;for(let e of H)this[e].finalizeFilters()}get onlyTiff(){return!V.map((e=>this[e].enabled)).some((e=>!0===e))&&this.tiff.enabled}checkLoadedPlugins(){for(let e of z)this[e].enabled&&!T.has(e)&&P(\"segment parser\",e)}}function Q(e,t){let i,n,s,r,a=[];for(s of t){for(r of(i=E.get(s),n=[],i))(e.includes(r[0])||e.includes(r[1]))&&n.push(r[0]);n.length&&a.push([s,n])}return a}function Z(e,t){return void 0!==e?e:void 0!==t?t:void 0}function ee(e,t){for(let i of t)e.add(i)}c(q,\"default\",$);class te{constructor(e){c(this,\"parsers\",{}),c(this,\"output\",{}),c(this,\"errors\",[]),c(this,\"pushToErrors\",(e=>this.errors.push(e))),this.options=q.useCached(e)}async read(e){this.file=await D(e,this.options)}setup(){if(this.fileParser)return;let{file:e}=this,t=e.getUint16(0);for(let[i,n]of w)if(n.canHandle(e,t))return this.fileParser=new n(this.options,this.file,this.parsers),e[i]=!0;this.file.close&&this.file.close(),g(\"Unknown file format\")}async parse(){let{output:e,errors:t}=this;return this.setup(),this.options.silentErrors?(await this.executeParsers().catch(this.pushToErrors),t.push(...this.fileParser.errors)):await this.executeParsers(),this.file.close&&this.file.close(),this.options.silentErrors&&t.length>0&&(e.errors=t),f(e)}async executeParsers(){let{output:e}=this;await this.fileParser.parse();let t=Object.values(this.parsers).map((async t=>{let i=await t.parse();t.assignToOutput(e,i)}));this.options.silentErrors&&(t=t.map((e=>e.catch(this.pushToErrors)))),await Promise.all(t)}async extractThumbnail(){this.setup();let{options:e,file:t}=this,i=T.get(\"tiff\",e);var n;if(t.tiff?n={start:0,type:\"tiff\"}:t.jpeg&&(n=await this.fileParser.getOrFindSegment(\"tiff\")),void 0===n)return;let s=await this.fileParser.ensureSegmentChunk(n),r=this.parsers.tiff=new i(s,e,t),a=await r.extractThumbnail();return t.close&&t.close(),a}}async function ie(e,t){let i=new te(t);return await i.read(e),i.parse()}var ne=Object.freeze({__proto__:null,parse:ie,Exifr:te,fileParsers:w,segmentParsers:T,fileReaders:A,tagKeys:E,tagValues:B,tagRevivers:N,createDictionary:U,extendDictionary:F,fetchUrlAsArrayBuffer:M,readBlobAsArrayBuffer:R,chunkedProps:G,otherSegments:V,segments:z,tiffBlocks:H,segmentsAndBlocks:j,tiffExtractables:W,inheritables:K,allFormatters:X,Options:q});class se{constructor(e,t,i){c(this,\"errors\",[]),c(this,\"ensureSegmentChunk\",(async e=>{let t=e.start,i=e.size||65536;if(this.file.chunked)if(this.file.available(t,i))e.chunk=this.file.subarray(t,i);else try{e.chunk=await this.file.readChunk(t,i)}catch(t){g(`Couldn't read segment: ${JSON.stringify(e)}. ${t.message}`)}else this.file.byteLength>t+i?e.chunk=this.file.subarray(t,i):void 0===e.size?e.chunk=this.file.subarray(t):g(\"Segment unreachable: \"+JSON.stringify(e));return e.chunk})),this.extendOptions&&this.extendOptions(e),this.options=e,this.file=t,this.parsers=i}injectSegment(e,t){this.options[e].enabled&&this.createParser(e,t)}createParser(e,t){let i=new(T.get(e))(t,this.options,this.file);return this.parsers[e]=i}createParsers(e){for(let t of e){let{type:e,chunk:i}=t,n=this.options[e];if(n&&n.enabled){let t=this.parsers[e];t&&t.append||t||this.createParser(e,i)}}}async readSegments(e){let t=e.map(this.ensureSegmentChunk);await Promise.all(t)}}class re{static findPosition(e,t){let i=e.getUint16(t+2)+2,n=\"function\"==typeof this.headerLength?this.headerLength(e,t,i):this.headerLength,s=t+n,r=i-n;return{offset:t,length:i,headerLength:n,start:s,size:r,end:s+r}}static parse(e,t={}){return new this(e,new q({[this.type]:t}),e).parse()}normalizeInput(e){return e instanceof I?e:new I(e)}constructor(e,t={},i){c(this,\"errors\",[]),c(this,\"raw\",new Map),c(this,\"handleError\",(e=>{if(!this.options.silentErrors)throw e;this.errors.push(e.message)})),this.chunk=this.normalizeInput(e),this.file=i,this.type=this.constructor.type,this.globalOptions=this.options=t,this.localOptions=t[this.type],this.canTranslate=this.localOptions&&this.localOptions.translate}translate(){this.canTranslate&&(this.translated=this.translateBlock(this.raw,this.type))}get output(){return this.translated?this.translated:this.raw?Object.fromEntries(this.raw):void 0}translateBlock(e,t){let i=N.get(t),n=B.get(t),s=E.get(t),r=this.options[t],a=r.reviveValues&&!!i,o=r.translateValues&&!!n,l=r.translateKeys&&!!s,h={};for(let[t,r]of e)a&&i.has(t)?r=i.get(t)(r):o&&n.has(t)&&(r=this.translateValue(r,n.get(t))),l&&s.has(t)&&(t=s.get(t)||t),h[t]=r;return h}translateValue(e,t){return t[e]||t.DEFAULT||e}assignToOutput(e,t){this.assignObjectToOutput(e,this.constructor.type,t)}assignObjectToOutput(e,t,i){if(this.globalOptions.mergeOutput)return Object.assign(e,i);e[t]?Object.assign(e[t],i):e[t]=i}}c(re,\"headerLength\",4),c(re,\"type\",void 0),c(re,\"multiSegment\",!1),c(re,\"canHandle\",(()=>!1));function ae(e){return 192===e||194===e||196===e||219===e||221===e||218===e||254===e}function oe(e){return e>=224&&e<=239}function le(e,t,i){for(let[n,s]of T)if(s.canHandle(e,t,i))return n}class he extends se{constructor(...e){super(...e),c(this,\"appSegments\",[]),c(this,\"jpegSegments\",[]),c(this,\"unknownSegments\",[])}static canHandle(e,t){return 65496===t}async parse(){await this.findAppSegments(),await this.readSegments(this.appSegments),this.mergeMultiSegments(),this.createParsers(this.mergedAppSegments||this.appSegments)}setupSegmentFinderArgs(e){!0===e?(this.findAll=!0,this.wanted=new Set(T.keyList())):(e=void 0===e?T.keyList().filter((e=>this.options[e].enabled)):e.filter((e=>this.options[e].enabled&&T.has(e))),this.findAll=!1,this.remaining=new Set(e),this.wanted=new Set(e)),this.unfinishedMultiSegment=!1}async findAppSegments(e=0,t){this.setupSegmentFinderArgs(t);let{file:i,findAll:n,wanted:s,remaining:r}=this;if(!n&&this.file.chunked&&(n=Array.from(s).some((e=>{let t=T.get(e),i=this.options[e];return t.multiSegment&&i.multiSegment})),n&&await this.file.readWhole()),e=this.findAppSegmentsInRange(e,i.byteLength),!this.options.onlyTiff&&i.chunked){let t=!1;for(;r.size>0&&!t&&(i.canReadNextChunk||this.unfinishedMultiSegment);){let{nextChunkOffset:n}=i,s=this.appSegments.some((e=>!this.file.available(e.offset||e.start,e.length||e.size)));if(t=e>n&&!s?!await i.readNextChunk(e):!await i.readNextChunk(n),void 0===(e=this.findAppSegmentsInRange(e,i.byteLength)))return}}}findAppSegmentsInRange(e,t){t-=2;let i,n,s,r,a,o,{file:l,findAll:h,wanted:u,remaining:c,options:f}=this;for(;ee.multiSegment)))return;let e=function(e,t){let i,n,s,r=new Map;for(let a=0;a{let i=T.get(e,this.options);if(i.handleMultiSegments){return{type:e,chunk:i.handleMultiSegments(t)}}return t[0]}))}getSegment(e){return this.appSegments.find((t=>t.type===e))}async getOrFindSegment(e){let t=this.getSegment(e);return void 0===t&&(await this.findAppSegments(0,[e]),t=this.getSegment(e)),t}}c(he,\"type\",\"jpeg\"),w.set(\"jpeg\",he);const ue=[void 0,1,1,2,4,8,1,1,2,4,8,4,8,4];class ce extends re{parseHeader(){var e=this.chunk.getUint16();18761===e?this.le=!0:19789===e&&(this.le=!1),this.chunk.le=this.le,this.headerParsed=!0}parseTags(e,t,i=new Map){let{pick:n,skip:s}=this.options[t];n=new Set(n);let r=n.size>0,a=0===s.size,o=this.chunk.getUint16(e);e+=2;for(let l=0;l13)&&g(`Invalid TIFF value type. block: ${i.toUpperCase()}, tag: ${t.toString(16)}, type: ${s}, offset ${e}`),e>n.byteLength&&g(`Invalid TIFF value offset. block: ${i.toUpperCase()}, tag: ${t.toString(16)}, type: ${s}, offset ${e} is outside of chunk size ${n.byteLength}`),1===s)return n.getUint8Array(e,r);if(2===s)return m(n.getString(e,r));if(7===s)return n.getUint8Array(e,r);if(1===r)return this.parseTagValue(s,e);{let t=new(function(e){switch(e){case 1:return Uint8Array;case 3:return Uint16Array;case 4:return Uint32Array;case 5:return Array;case 6:return Int8Array;case 8:return Int16Array;case 9:return Int32Array;case 10:return Array;case 11:return Float32Array;case 12:return Float64Array;default:return Array}}(s))(r),i=a;for(let n=0;ne.byteLength&&g(`IFD0 offset points to outside of file.\\nthis.ifd0Offset: ${this.ifd0Offset}, file.byteLength: ${e.byteLength}`),e.tiff&&await e.ensureChunk(this.ifd0Offset,S(this.options));let t=this.parseBlock(this.ifd0Offset,\"ifd0\");return 0!==t.size?(this.exifOffset=t.get(34665),this.interopOffset=t.get(40965),this.gpsOffset=t.get(34853),this.xmp=t.get(700),this.iptc=t.get(33723),this.icc=t.get(34675),this.options.sanitize&&(t.delete(34665),t.delete(40965),t.delete(34853),t.delete(700),t.delete(33723),t.delete(34675)),t):void 0}async parseExifBlock(){if(this.exif)return;if(this.ifd0||await this.parseIfd0Block(),void 0===this.exifOffset)return;this.file.tiff&&await this.file.ensureChunk(this.exifOffset,S(this.options));let e=this.parseBlock(this.exifOffset,\"exif\");return this.interopOffset||(this.interopOffset=e.get(40965)),this.makerNote=e.get(37500),this.userComment=e.get(37510),this.options.sanitize&&(e.delete(40965),e.delete(37500),e.delete(37510)),this.unpack(e,41728),this.unpack(e,41729),e}unpack(e,t){let i=e.get(t);i&&1===i.length&&e.set(t,i[0])}async parseGpsBlock(){if(this.gps)return;if(this.ifd0||await this.parseIfd0Block(),void 0===this.gpsOffset)return;let e=this.parseBlock(this.gpsOffset,\"gps\");return e&&e.has(2)&&e.has(4)&&(e.set(\"latitude\",de(...e.get(2),e.get(1))),e.set(\"longitude\",de(...e.get(4),e.get(3)))),e}async parseInteropBlock(){if(!this.interop&&(this.ifd0||await this.parseIfd0Block(),void 0!==this.interopOffset||this.exif||await this.parseExifBlock(),void 0!==this.interopOffset))return this.parseBlock(this.interopOffset,\"interop\")}async parseThumbnailBlock(e=!1){if(!this.ifd1&&!this.ifd1Parsed&&(!this.options.mergeOutput||e))return this.findIfd1Offset(),this.ifd1Offset>0&&(this.parseBlock(this.ifd1Offset,\"ifd1\"),this.ifd1Parsed=!0),this.ifd1}async extractThumbnail(){if(this.headerParsed||this.parseHeader(),this.ifd1Parsed||await this.parseThumbnailBlock(!0),void 0===this.ifd1)return;let e=this.ifd1.get(513),t=this.ifd1.get(514);return this.chunk.getUint8Array(e,t)}get image(){return this.ifd0}get thumbnail(){return this.ifd1}createOutput(){let e,t,i,n={};for(t of H)if(e=this[t],!p(e))if(i=this.canTranslate?this.translateBlock(e,t):Object.fromEntries(e),this.options.mergeOutput){if(\"ifd1\"===t)continue;Object.assign(n,i)}else n[t]=i;return this.makerNote&&(n.makerNote=this.makerNote),this.userComment&&(n.userComment=this.userComment),n}assignToOutput(e,t){if(this.globalOptions.mergeOutput)Object.assign(e,t);else for(let[i,n]of Object.entries(t))this.assignObjectToOutput(e,i,n)}}function de(e,t,i,n){var s=e+t/60+i/3600;return\"S\"!==n&&\"W\"!==n||(s*=-1),s}c(fe,\"type\",\"tiff\"),c(fe,\"headerLength\",10),T.set(\"tiff\",fe);var pe=Object.freeze({__proto__:null,default:ne,Exifr:te,fileParsers:w,segmentParsers:T,fileReaders:A,tagKeys:E,tagValues:B,tagRevivers:N,createDictionary:U,extendDictionary:F,fetchUrlAsArrayBuffer:M,readBlobAsArrayBuffer:R,chunkedProps:G,otherSegments:V,segments:z,tiffBlocks:H,segmentsAndBlocks:j,tiffExtractables:W,inheritables:K,allFormatters:X,Options:q,parse:ie});const ge={ifd0:!1,ifd1:!1,exif:!1,gps:!1,interop:!1,sanitize:!1,reviveValues:!0,translateKeys:!1,translateValues:!1,mergeOutput:!1},me=Object.assign({},ge,{firstChunkSize:4e4,gps:[1,2,3,4]});async function Se(e){let t=new te(me);await t.read(e);let i=await t.parse();if(i&&i.gps){let{latitude:e,longitude:t}=i.gps;return{latitude:e,longitude:t}}}const Ce=Object.assign({},ge,{tiff:!1,ifd1:!0,mergeOutput:!1});async function ye(e){let t=new te(Ce);await t.read(e);let i=await t.extractThumbnail();return i&&a?s.from(i):i}async function be(e){let t=await this.thumbnail(e);if(void 0!==t){let e=new Blob([t]);return URL.createObjectURL(e)}}const Ie=Object.assign({},ge,{firstChunkSize:4e4,ifd0:[274]});async function Pe(e){let t=new te(Ie);await t.read(e);let i=await t.parse();if(i&&i.ifd0)return i.ifd0[274]}const ke=Object.freeze({1:{dimensionSwapped:!1,scaleX:1,scaleY:1,deg:0,rad:0},2:{dimensionSwapped:!1,scaleX:-1,scaleY:1,deg:0,rad:0},3:{dimensionSwapped:!1,scaleX:1,scaleY:1,deg:180,rad:180*Math.PI/180},4:{dimensionSwapped:!1,scaleX:-1,scaleY:1,deg:180,rad:180*Math.PI/180},5:{dimensionSwapped:!0,scaleX:1,scaleY:-1,deg:90,rad:90*Math.PI/180},6:{dimensionSwapped:!0,scaleX:1,scaleY:1,deg:90,rad:90*Math.PI/180},7:{dimensionSwapped:!0,scaleX:1,scaleY:-1,deg:270,rad:270*Math.PI/180},8:{dimensionSwapped:!0,scaleX:1,scaleY:1,deg:270,rad:270*Math.PI/180}});let we=!0,Te=!0;if(\"object\"==typeof navigator){let e=navigator.userAgent;if(e.includes(\"iPad\")||e.includes(\"iPhone\")){let t=e.match(/OS (\\d+)_(\\d+)/);if(t){let[,e,i]=t,n=Number(e)+.1*Number(i);we=n<13.4,Te=!1}}else if(e.includes(\"OS X 10\")){let[,t]=e.match(/OS X 10[_.](\\d+)/);we=Te=Number(t)<15}if(e.includes(\"Chrome/\")){let[,t]=e.match(/Chrome\\/(\\d+)/);we=Te=Number(t)<81}else if(e.includes(\"Firefox/\")){let[,t]=e.match(/Firefox\\/(\\d+)/);we=Te=Number(t)<77}}async function Ae(e){let t=await Pe(e);return Object.assign({canvas:we,css:Te},ke[t])}class De extends I{constructor(...e){super(...e),c(this,\"ranges\",new Oe),0!==this.byteLength&&this.ranges.add(0,this.byteLength)}_tryExtend(e,t,i){if(0===e&&0===this.byteLength&&i){let e=new DataView(i.buffer||i,i.byteOffset,i.byteLength);this._swapDataView(e)}else{let i=e+t;if(i>this.byteLength){let{dataView:e}=this._extend(i);this._swapDataView(e)}}}_extend(e){let t;t=a?s.allocUnsafe(e):new Uint8Array(e);let i=new DataView(t.buffer,t.byteOffset,t.byteLength);return t.set(new Uint8Array(this.buffer,this.byteOffset,this.byteLength),0),{uintView:t,dataView:i}}subarray(e,t,i=!1){return t=t||this._lengthToEnd(e),i&&this._tryExtend(e,t),this.ranges.add(e,t),super.subarray(e,t)}set(e,t,i=!1){i&&this._tryExtend(t,e.byteLength,e);let n=super.set(e,t);return this.ranges.add(t,n.byteLength),n}async ensureChunk(e,t){this.chunked&&(this.ranges.available(e,t)||await this.readChunk(e,t))}available(e,t){return this.ranges.available(e,t)}}class Oe{constructor(){c(this,\"list\",[])}get length(){return this.list.length}add(e,t,i=0){let n=e+t,s=this.list.filter((t=>xe(e,t.offset,n)||xe(e,t.end,n)));if(s.length>0){e=Math.min(e,...s.map((e=>e.offset))),n=Math.max(n,...s.map((e=>e.end))),t=n-e;let i=s.shift();i.offset=e,i.length=t,i.end=n,this.list=this.list.filter((e=>!s.includes(e)))}else this.list.push({offset:e,length:t,end:n})}available(e,t){let i=e+t;return this.list.some((t=>t.offset<=e&&i<=t.end))}}function xe(e,t,i){return e<=t&&t<=i}class ve extends De{constructor(e,t){super(0),c(this,\"chunksRead\",0),this.input=e,this.options=t}async readWhole(){this.chunked=!1,await this.readChunk(this.nextChunkOffset)}async readChunked(){this.chunked=!0,await this.readChunk(0,this.options.firstChunkSize)}async readNextChunk(e=this.nextChunkOffset){if(this.fullyRead)return this.chunksRead++,!1;let t=this.options.chunkSize,i=await this.readChunk(e,t);return!!i&&i.byteLength===t}async readChunk(e,t){if(this.chunksRead++,0!==(t=this.safeWrapAddress(e,t)))return this._readChunk(e,t)}safeWrapAddress(e,t){return void 0!==this.size&&e+t>this.size?Math.max(0,this.size-e):t}get nextChunkOffset(){if(0!==this.ranges.list.length)return this.ranges.list[0].length}get canReadNextChunk(){return this.chunksReade.kind===t))}parseBoxHead(e){let t=this.file.getUint32(e),i=this.file.getString(e+4,4),n=e+8;return 1===t&&(t=this.file.getUint64(e+8),n+=8),{offset:e,length:t,kind:i,start:n}}parseBoxFullHead(e){if(void 0!==e.version)return;let t=this.file.getUint32(e.start);e.version=t>>24,e.start+=4}}class Le extends Re{static canHandle(e,t){if(0!==t)return!1;let i=e.getUint16(2);if(i>50)return!1;let n=16,s=[];for(;n=2&&(n=3===t.version?4:2,s=this.file.getString(i+n+2,4),\"Exif\"===s))return this.file.getUintBytes(i,n);r+=t.length}}get8bits(e){let t=this.file.getUint8(e);return[t>>4,15&t]}findExtentInIloc(e,t){this.parseBoxFullHead(e);let i=e.start,[n,s]=this.get8bits(i++),[r,a]=this.get8bits(i++),o=2===e.version?4:2,l=1===e.version||2===e.version?2:0,h=a+n+s,u=2===e.version?4:2,c=this.file.getUintBytes(i,u);for(i+=u;c--;){let e=this.file.getUintBytes(i,o);i+=o+l+2+r;let u=this.file.getUint16(i);if(i+=2,e===t)return u>1&&console.warn(\"ILOC box has more than one extent but we're only processing one\\nPlease create an issue at https://github.com/MikeKovarik/exifr with this file\"),[this.file.getUintBytes(i+a,n),this.file.getUintBytes(i+a+n,s)];i+=u*h}}}class Ue extends Le{}c(Ue,\"type\",\"heic\");class Fe extends Le{}c(Fe,\"type\",\"avif\"),w.set(\"heic\",Ue),w.set(\"avif\",Fe),U(E,[\"ifd0\",\"ifd1\"],[[256,\"ImageWidth\"],[257,\"ImageHeight\"],[258,\"BitsPerSample\"],[259,\"Compression\"],[262,\"PhotometricInterpretation\"],[270,\"ImageDescription\"],[271,\"Make\"],[272,\"Model\"],[273,\"StripOffsets\"],[274,\"Orientation\"],[277,\"SamplesPerPixel\"],[278,\"RowsPerStrip\"],[279,\"StripByteCounts\"],[282,\"XResolution\"],[283,\"YResolution\"],[284,\"PlanarConfiguration\"],[296,\"ResolutionUnit\"],[301,\"TransferFunction\"],[305,\"Software\"],[306,\"ModifyDate\"],[315,\"Artist\"],[316,\"HostComputer\"],[317,\"Predictor\"],[318,\"WhitePoint\"],[319,\"PrimaryChromaticities\"],[513,\"ThumbnailOffset\"],[514,\"ThumbnailLength\"],[529,\"YCbCrCoefficients\"],[530,\"YCbCrSubSampling\"],[531,\"YCbCrPositioning\"],[532,\"ReferenceBlackWhite\"],[700,\"ApplicationNotes\"],[33432,\"Copyright\"],[33723,\"IPTC\"],[34665,\"ExifIFD\"],[34675,\"ICC\"],[34853,\"GpsIFD\"],[330,\"SubIFD\"],[40965,\"InteropIFD\"],[40091,\"XPTitle\"],[40092,\"XPComment\"],[40093,\"XPAuthor\"],[40094,\"XPKeywords\"],[40095,\"XPSubject\"]]),U(E,\"exif\",[[33434,\"ExposureTime\"],[33437,\"FNumber\"],[34850,\"ExposureProgram\"],[34852,\"SpectralSensitivity\"],[34855,\"ISO\"],[34858,\"TimeZoneOffset\"],[34859,\"SelfTimerMode\"],[34864,\"SensitivityType\"],[34865,\"StandardOutputSensitivity\"],[34866,\"RecommendedExposureIndex\"],[34867,\"ISOSpeed\"],[34868,\"ISOSpeedLatitudeyyy\"],[34869,\"ISOSpeedLatitudezzz\"],[36864,\"ExifVersion\"],[36867,\"DateTimeOriginal\"],[36868,\"CreateDate\"],[36873,\"GooglePlusUploadCode\"],[36880,\"OffsetTime\"],[36881,\"OffsetTimeOriginal\"],[36882,\"OffsetTimeDigitized\"],[37121,\"ComponentsConfiguration\"],[37122,\"CompressedBitsPerPixel\"],[37377,\"ShutterSpeedValue\"],[37378,\"ApertureValue\"],[37379,\"BrightnessValue\"],[37380,\"ExposureCompensation\"],[37381,\"MaxApertureValue\"],[37382,\"SubjectDistance\"],[37383,\"MeteringMode\"],[37384,\"LightSource\"],[37385,\"Flash\"],[37386,\"FocalLength\"],[37393,\"ImageNumber\"],[37394,\"SecurityClassification\"],[37395,\"ImageHistory\"],[37396,\"SubjectArea\"],[37500,\"MakerNote\"],[37510,\"UserComment\"],[37520,\"SubSecTime\"],[37521,\"SubSecTimeOriginal\"],[37522,\"SubSecTimeDigitized\"],[37888,\"AmbientTemperature\"],[37889,\"Humidity\"],[37890,\"Pressure\"],[37891,\"WaterDepth\"],[37892,\"Acceleration\"],[37893,\"CameraElevationAngle\"],[40960,\"FlashpixVersion\"],[40961,\"ColorSpace\"],[40962,\"ExifImageWidth\"],[40963,\"ExifImageHeight\"],[40964,\"RelatedSoundFile\"],[41483,\"FlashEnergy\"],[41486,\"FocalPlaneXResolution\"],[41487,\"FocalPlaneYResolution\"],[41488,\"FocalPlaneResolutionUnit\"],[41492,\"SubjectLocation\"],[41493,\"ExposureIndex\"],[41495,\"SensingMethod\"],[41728,\"FileSource\"],[41729,\"SceneType\"],[41730,\"CFAPattern\"],[41985,\"CustomRendered\"],[41986,\"ExposureMode\"],[41987,\"WhiteBalance\"],[41988,\"DigitalZoomRatio\"],[41989,\"FocalLengthIn35mmFormat\"],[41990,\"SceneCaptureType\"],[41991,\"GainControl\"],[41992,\"Contrast\"],[41993,\"Saturation\"],[41994,\"Sharpness\"],[41996,\"SubjectDistanceRange\"],[42016,\"ImageUniqueID\"],[42032,\"OwnerName\"],[42033,\"SerialNumber\"],[42034,\"LensInfo\"],[42035,\"LensMake\"],[42036,\"LensModel\"],[42037,\"LensSerialNumber\"],[42080,\"CompositeImage\"],[42081,\"CompositeImageCount\"],[42082,\"CompositeImageExposureTimes\"],[42240,\"Gamma\"],[59932,\"Padding\"],[59933,\"OffsetSchema\"],[65e3,\"OwnerName\"],[65001,\"SerialNumber\"],[65002,\"Lens\"],[65100,\"RawFile\"],[65101,\"Converter\"],[65102,\"WhiteBalance\"],[65105,\"Exposure\"],[65106,\"Shadows\"],[65107,\"Brightness\"],[65108,\"Contrast\"],[65109,\"Saturation\"],[65110,\"Sharpness\"],[65111,\"Smoothness\"],[65112,\"MoireFilter\"],[40965,\"InteropIFD\"]]),U(E,\"gps\",[[0,\"GPSVersionID\"],[1,\"GPSLatitudeRef\"],[2,\"GPSLatitude\"],[3,\"GPSLongitudeRef\"],[4,\"GPSLongitude\"],[5,\"GPSAltitudeRef\"],[6,\"GPSAltitude\"],[7,\"GPSTimeStamp\"],[8,\"GPSSatellites\"],[9,\"GPSStatus\"],[10,\"GPSMeasureMode\"],[11,\"GPSDOP\"],[12,\"GPSSpeedRef\"],[13,\"GPSSpeed\"],[14,\"GPSTrackRef\"],[15,\"GPSTrack\"],[16,\"GPSImgDirectionRef\"],[17,\"GPSImgDirection\"],[18,\"GPSMapDatum\"],[19,\"GPSDestLatitudeRef\"],[20,\"GPSDestLatitude\"],[21,\"GPSDestLongitudeRef\"],[22,\"GPSDestLongitude\"],[23,\"GPSDestBearingRef\"],[24,\"GPSDestBearing\"],[25,\"GPSDestDistanceRef\"],[26,\"GPSDestDistance\"],[27,\"GPSProcessingMethod\"],[28,\"GPSAreaInformation\"],[29,\"GPSDateStamp\"],[30,\"GPSDifferential\"],[31,\"GPSHPositioningError\"]]),U(B,[\"ifd0\",\"ifd1\"],[[274,{1:\"Horizontal (normal)\",2:\"Mirror horizontal\",3:\"Rotate 180\",4:\"Mirror vertical\",5:\"Mirror horizontal and rotate 270 CW\",6:\"Rotate 90 CW\",7:\"Mirror horizontal and rotate 90 CW\",8:\"Rotate 270 CW\"}],[296,{1:\"None\",2:\"inches\",3:\"cm\"}]]);let Ee=U(B,\"exif\",[[34850,{0:\"Not defined\",1:\"Manual\",2:\"Normal program\",3:\"Aperture priority\",4:\"Shutter priority\",5:\"Creative program\",6:\"Action program\",7:\"Portrait mode\",8:\"Landscape mode\"}],[37121,{0:\"-\",1:\"Y\",2:\"Cb\",3:\"Cr\",4:\"R\",5:\"G\",6:\"B\"}],[37383,{0:\"Unknown\",1:\"Average\",2:\"CenterWeightedAverage\",3:\"Spot\",4:\"MultiSpot\",5:\"Pattern\",6:\"Partial\",255:\"Other\"}],[37384,{0:\"Unknown\",1:\"Daylight\",2:\"Fluorescent\",3:\"Tungsten (incandescent light)\",4:\"Flash\",9:\"Fine weather\",10:\"Cloudy weather\",11:\"Shade\",12:\"Daylight fluorescent (D 5700 - 7100K)\",13:\"Day white fluorescent (N 4600 - 5400K)\",14:\"Cool white fluorescent (W 3900 - 4500K)\",15:\"White fluorescent (WW 3200 - 3700K)\",17:\"Standard light A\",18:\"Standard light B\",19:\"Standard light C\",20:\"D55\",21:\"D65\",22:\"D75\",23:\"D50\",24:\"ISO studio tungsten\",255:\"Other\"}],[37385,{0:\"Flash did not fire\",1:\"Flash fired\",5:\"Strobe return light not detected\",7:\"Strobe return light detected\",9:\"Flash fired, compulsory flash mode\",13:\"Flash fired, compulsory flash mode, return light not detected\",15:\"Flash fired, compulsory flash mode, return light detected\",16:\"Flash did not fire, compulsory flash mode\",24:\"Flash did not fire, auto mode\",25:\"Flash fired, auto mode\",29:\"Flash fired, auto mode, return light not detected\",31:\"Flash fired, auto mode, return light detected\",32:\"No flash function\",65:\"Flash fired, red-eye reduction mode\",69:\"Flash fired, red-eye reduction mode, return light not detected\",71:\"Flash fired, red-eye reduction mode, return light detected\",73:\"Flash fired, compulsory flash mode, red-eye reduction mode\",77:\"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected\",79:\"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected\",89:\"Flash fired, auto mode, red-eye reduction mode\",93:\"Flash fired, auto mode, return light not detected, red-eye reduction mode\",95:\"Flash fired, auto mode, return light detected, red-eye reduction mode\"}],[41495,{1:\"Not defined\",2:\"One-chip color area sensor\",3:\"Two-chip color area sensor\",4:\"Three-chip color area sensor\",5:\"Color sequential area sensor\",7:\"Trilinear sensor\",8:\"Color sequential linear sensor\"}],[41728,{1:\"Film Scanner\",2:\"Reflection Print Scanner\",3:\"Digital Camera\"}],[41729,{1:\"Directly photographed\"}],[41985,{0:\"Normal\",1:\"Custom\",2:\"HDR (no original saved)\",3:\"HDR (original saved)\",4:\"Original (for HDR)\",6:\"Panorama\",7:\"Portrait HDR\",8:\"Portrait\"}],[41986,{0:\"Auto\",1:\"Manual\",2:\"Auto bracket\"}],[41987,{0:\"Auto\",1:\"Manual\"}],[41990,{0:\"Standard\",1:\"Landscape\",2:\"Portrait\",3:\"Night\",4:\"Other\"}],[41991,{0:\"None\",1:\"Low gain up\",2:\"High gain up\",3:\"Low gain down\",4:\"High gain down\"}],[41996,{0:\"Unknown\",1:\"Macro\",2:\"Close\",3:\"Distant\"}],[42080,{0:\"Unknown\",1:\"Not a Composite Image\",2:\"General Composite Image\",3:\"Composite Image Captured While Shooting\"}]]);const Be={1:\"No absolute unit of measurement\",2:\"Inch\",3:\"Centimeter\"};Ee.set(37392,Be),Ee.set(41488,Be);const Ne={0:\"Normal\",1:\"Low\",2:\"High\"};function Ge(e){return\"object\"==typeof e&&void 0!==e.length?e[0]:e}function Ve(e){let t=Array.from(e).slice(1);return t[1]>15&&(t=t.map((e=>String.fromCharCode(e)))),\"0\"!==t[2]&&0!==t[2]||t.pop(),t.join(\".\")}function ze(e){if(\"string\"==typeof e){var[t,i,n,s,r,a]=e.trim().split(/[-: ]/g).map(Number),o=new Date(t,i-1,n);return Number.isNaN(s)||Number.isNaN(r)||Number.isNaN(a)||(o.setHours(s),o.setMinutes(r),o.setSeconds(a)),Number.isNaN(+o)?e:o}}function He(e){if(\"string\"==typeof e)return e;let t=[];if(0===e[1]&&0===e[e.length-1])for(let i=0;iArray.from(e).join(\".\")],[7,e=>Array.from(e).join(\":\")]]);class We extends re{static canHandle(e,t){return 225===e.getUint8(t+1)&&1752462448===e.getUint32(t+4)&&\"http://ns.adobe.com/\"===e.getString(t+4,\"http://ns.adobe.com/\".length)}static headerLength(e,t){return\"http://ns.adobe.com/xmp/extension/\"===e.getString(t+4,\"http://ns.adobe.com/xmp/extension/\".length)?79:4+\"http://ns.adobe.com/xap/1.0/\".length+1}static findPosition(e,t){let i=super.findPosition(e,t);return i.multiSegment=i.extended=79===i.headerLength,i.multiSegment?(i.chunkCount=e.getUint8(t+72),i.chunkNumber=e.getUint8(t+76),0!==e.getUint8(t+77)&&i.chunkNumber++):(i.chunkCount=1/0,i.chunkNumber=-1),i}static handleMultiSegments(e){return e.map((e=>e.chunk.getString())).join(\"\")}normalizeInput(e){return\"string\"==typeof e?e:I.from(e).getString()}parse(e=this.chunk){if(!this.localOptions.parse)return e;e=function(e){let t={},i={};for(let e of Ze)t[e]=[],i[e]=0;return e.replace(et,((e,n,s)=>{if(\"<\"===n){let n=++i[s];return t[s].push(n),`${e}#${n}`}return`${e}#${t[s].pop()}`}))}(e);let t=Xe.findAll(e,\"rdf\",\"Description\");0===t.length&&t.push(new Xe(\"rdf\",\"Description\",void 0,e));let i,n={};for(let e of t)for(let t of e.properties)i=Je(t.ns,n),_e(t,i);return function(e){let t;for(let i in e)t=e[i]=f(e[i]),void 0===t&&delete e[i];return f(e)}(n)}assignToOutput(e,t){if(this.localOptions.parse)for(let[i,n]of Object.entries(t))switch(i){case\"tiff\":this.assignObjectToOutput(e,\"ifd0\",n);break;case\"exif\":this.assignObjectToOutput(e,\"exif\",n);break;case\"xmlns\":break;default:this.assignObjectToOutput(e,i,n)}else e.xmp=t}}c(We,\"type\",\"xmp\"),c(We,\"multiSegment\",!0),T.set(\"xmp\",We);class Ke{static findAll(e){return qe(e,/([a-zA-Z0-9-]+):([a-zA-Z0-9-]+)=(\"[^\"]*\"|'[^']*')/gm).map(Ke.unpackMatch)}static unpackMatch(e){let t=e[1],i=e[2],n=e[3].slice(1,-1);return n=Qe(n),new Ke(t,i,n)}constructor(e,t,i){this.ns=e,this.name=t,this.value=i}serialize(){return this.value}}class Xe{static findAll(e,t,i){if(void 0!==t||void 0!==i){t=t||\"[\\\\w\\\\d-]+\",i=i||\"[\\\\w\\\\d-]+\";var n=new RegExp(`<(${t}):(${i})(#\\\\d+)?((\\\\s+?[\\\\w\\\\d-:]+=(\"[^\"]*\"|'[^']*'))*\\\\s*)(\\\\/>|>([\\\\s\\\\S]*?)<\\\\/\\\\1:\\\\2\\\\3>)`,\"gm\")}else n=/<([\\w\\d-]+):([\\w\\d-]+)(#\\d+)?((\\s+?[\\w\\d-:]+=(\"[^\"]*\"|'[^']*'))*\\s*)(\\/>|>([\\s\\S]*?)<\\/\\1:\\2\\3>)/gm;return qe(e,n).map(Xe.unpackMatch)}static unpackMatch(e){let t=e[1],i=e[2],n=e[4],s=e[8];return new Xe(t,i,n,s)}constructor(e,t,i,n){this.ns=e,this.name=t,this.attrString=i,this.innerXml=n,this.attrs=Ke.findAll(i),this.children=Xe.findAll(n),this.value=0===this.children.length?Qe(n):void 0,this.properties=[...this.attrs,...this.children]}get isPrimitive(){return void 0!==this.value&&0===this.attrs.length&&0===this.children.length}get isListContainer(){return 1===this.children.length&&this.children[0].isList}get isList(){let{ns:e,name:t}=this;return\"rdf\"===e&&(\"Seq\"===t||\"Bag\"===t||\"Alt\"===t)}get isListItem(){return\"rdf\"===this.ns&&\"li\"===this.name}serialize(){if(0===this.properties.length&&void 0===this.value)return;if(this.isPrimitive)return this.value;if(this.isListContainer)return this.children[0].serialize();if(this.isList)return $e(this.children.map(Ye));if(this.isListItem&&1===this.children.length&&0===this.attrs.length)return this.children[0].serialize();let e={};for(let t of this.properties)_e(t,e);return void 0!==this.value&&(e.value=this.value),f(e)}}function _e(e,t){let i=e.serialize();void 0!==i&&(t[e.name]=i)}var Ye=e=>e.serialize(),$e=e=>1===e.length?e[0]:e,Je=(e,t)=>t[e]?t[e]:t[e]={};function qe(e,t){let i,n=[];if(!e)return n;for(;null!==(i=t.exec(e));)n.push(i);return n}function Qe(e){if(function(e){return null==e||\"null\"===e||\"undefined\"===e||\"\"===e||\"\"===e.trim()}(e))return;let t=Number(e);if(!Number.isNaN(t))return t;let i=e.toLowerCase();return\"true\"===i||\"false\"!==i&&e.trim()}const Ze=[\"rdf:li\",\"rdf:Seq\",\"rdf:Bag\",\"rdf:Alt\",\"rdf:Description\"],et=new RegExp(`(<|\\\\/)(${Ze.join(\"|\")})`,\"g\");var tt=Object.freeze({__proto__:null,default:Me,Exifr:te,fileParsers:w,segmentParsers:T,fileReaders:A,tagKeys:E,tagValues:B,tagRevivers:N,createDictionary:U,extendDictionary:F,fetchUrlAsArrayBuffer:M,readBlobAsArrayBuffer:R,chunkedProps:G,otherSegments:V,segments:z,tiffBlocks:H,segmentsAndBlocks:j,tiffExtractables:W,inheritables:K,allFormatters:X,Options:q,parse:ie,gpsOnlyOptions:me,gps:Se,thumbnailOnlyOptions:Ce,thumbnail:ye,thumbnailUrl:be,orientationOnlyOptions:Ie,orientation:Pe,rotations:ke,get rotateCanvas(){return we},get rotateCss(){return Te},rotation:Ae});const it=[\"xmp\",\"icc\",\"iptc\",\"tiff\"],nt=()=>{};async function st(e,t,i){let n=new q(t);n.chunked=!1,void 0===i&&\"string\"==typeof e&&(i=function(e){let t=e.toLowerCase().split(\".\").pop();if(function(e){return\"exif\"===e||\"tiff\"===e||\"tif\"===e}(t))return\"tiff\";if(it.includes(t))return t}(e));let s=await D(e,n);if(i){if(it.includes(i))return rt(i,s,n);g(\"Invalid segment type\")}else{if(function(e){let t=e.getString(0,50).trim();return t.includes(\"e.promises));A.set(\"fs\",class extends ve{async readWhole(){this.chunked=!1,this.fs=await at;let e=await this.fs.readFile(this.input);this._swapBuffer(e)}async readChunked(){this.chunked=!0,this.fs=await at,await this.open(),await this.readChunk(0,this.options.firstChunkSize)}async open(){void 0===this.fh&&(this.fh=await this.fs.open(this.input,\"r\"),this.size=(await this.fh.stat(this.input)).size)}async _readChunk(e,t){void 0===this.fh&&await this.open(),e+t>this.size&&(t=this.size-e);var i=this.subarray(e,t,!0);return await this.fh.read(i.dataView,0,t,e),i}async close(){if(this.fh){let e=this.fh;this.fh=void 0,await e.close()}}});A.set(\"base64\",class extends ve{constructor(...e){super(...e),this.input=this.input.replace(/^data:([^;]+);base64,/gim,\"\"),this.size=this.input.length/4*3,this.input.endsWith(\"==\")?this.size-=2:this.input.endsWith(\"=\")&&(this.size-=1)}async _readChunk(e,t){let i,n,r=this.input;void 0===e?(e=0,i=0,n=0):(i=4*Math.floor(e/3),n=e-i/4*3),void 0===t&&(t=this.size);let o=e+t,l=i+4*Math.ceil(o/3);r=r.slice(i,l);let h=Math.min(t,this.size-e);if(a){let t=s.from(r,\"base64\").slice(n,n+h);return this.set(t,e,!0)}{let t=this.subarray(e,h,!0),i=atob(r),s=t.toUint8();for(let e=0;ethis.errors.push(e))),c(this,\"metaChunks\",[]),c(this,\"unknownChunks\",[])}static canHandle(e,t){return 35152===t&&2303741511===e.getUint32(0)&&218765834===e.getUint32(4)}async parse(){let{file:e}=this;await this.findPngChunksInRange(\"\u0089PNG\\r\\n\u001A\\n\".length,e.byteLength),await this.readSegments(this.metaChunks),this.findIhdr(),this.parseTextChunks(),await this.findExif().catch(this.catchError),await this.findXmp().catch(this.catchError),await this.findIcc().catch(this.catchError)}async findPngChunksInRange(e,t){let{file:i}=this;for(;e\"text\"===e.type));for(let t of e){let[e,i]=this.file.getString(t.start,t.size).split(\"\\0\");this.injectKeyValToIhdr(e,i)}}injectKeyValToIhdr(e,t){let i=this.parsers.ihdr;i&&i.raw.set(e,t)}findIhdr(){let e=this.metaChunks.find((e=>\"ihdr\"===e.type));e&&!1!==this.options.ihdr.enabled&&this.createParser(\"ihdr\",e.chunk)}async findExif(){let e=this.metaChunks.find((e=>\"exif\"===e.type));e&&this.injectSegment(\"tiff\",e.chunk)}async findXmp(){let e=this.metaChunks.filter((e=>\"itxt\"===e.type));for(let t of e){\"XML:com.adobe.xmp\"===t.chunk.getString(0,\"XML:com.adobe.xmp\".length)&&this.injectSegment(\"xmp\",t.chunk)}}async findIcc(){let e=this.metaChunks.find((e=>\"iccp\"===e.type));if(!e)return;let{chunk:t}=e,i=t.getUint8Array(0,81),s=0;for(;s<80&&0!==i[s];)s++;let r=s+2,a=t.getString(0,s);if(this.injectKeyValToIhdr(\"ProfileName\",a),n){let e=await lt,i=t.getUint8Array(r);i=e.inflateSync(i),this.injectSegment(\"icc\",i)}}}c(ut,\"type\",\"png\"),w.set(\"png\",ut),U(E,\"interop\",[[1,\"InteropIndex\"],[2,\"InteropVersion\"],[4096,\"RelatedImageFileFormat\"],[4097,\"RelatedImageWidth\"],[4098,\"RelatedImageHeight\"]]),F(E,\"ifd0\",[[11,\"ProcessingSoftware\"],[254,\"SubfileType\"],[255,\"OldSubfileType\"],[263,\"Thresholding\"],[264,\"CellWidth\"],[265,\"CellLength\"],[266,\"FillOrder\"],[269,\"DocumentName\"],[280,\"MinSampleValue\"],[281,\"MaxSampleValue\"],[285,\"PageName\"],[286,\"XPosition\"],[287,\"YPosition\"],[290,\"GrayResponseUnit\"],[297,\"PageNumber\"],[321,\"HalftoneHints\"],[322,\"TileWidth\"],[323,\"TileLength\"],[332,\"InkSet\"],[337,\"TargetPrinter\"],[18246,\"Rating\"],[18249,\"RatingPercent\"],[33550,\"PixelScale\"],[34264,\"ModelTransform\"],[34377,\"PhotoshopSettings\"],[50706,\"DNGVersion\"],[50707,\"DNGBackwardVersion\"],[50708,\"UniqueCameraModel\"],[50709,\"LocalizedCameraModel\"],[50736,\"DNGLensInfo\"],[50739,\"ShadowScale\"],[50740,\"DNGPrivateData\"],[33920,\"IntergraphMatrix\"],[33922,\"ModelTiePoint\"],[34118,\"SEMInfo\"],[34735,\"GeoTiffDirectory\"],[34736,\"GeoTiffDoubleParams\"],[34737,\"GeoTiffAsciiParams\"],[50341,\"PrintIM\"],[50721,\"ColorMatrix1\"],[50722,\"ColorMatrix2\"],[50723,\"CameraCalibration1\"],[50724,\"CameraCalibration2\"],[50725,\"ReductionMatrix1\"],[50726,\"ReductionMatrix2\"],[50727,\"AnalogBalance\"],[50728,\"AsShotNeutral\"],[50729,\"AsShotWhiteXY\"],[50730,\"BaselineExposure\"],[50731,\"BaselineNoise\"],[50732,\"BaselineSharpness\"],[50734,\"LinearResponseLimit\"],[50735,\"CameraSerialNumber\"],[50741,\"MakerNoteSafety\"],[50778,\"CalibrationIlluminant1\"],[50779,\"CalibrationIlluminant2\"],[50781,\"RawDataUniqueID\"],[50827,\"OriginalRawFileName\"],[50828,\"OriginalRawFileData\"],[50831,\"AsShotICCProfile\"],[50832,\"AsShotPreProfileMatrix\"],[50833,\"CurrentICCProfile\"],[50834,\"CurrentPreProfileMatrix\"],[50879,\"ColorimetricReference\"],[50885,\"SRawType\"],[50898,\"PanasonicTitle\"],[50899,\"PanasonicTitle2\"],[50931,\"CameraCalibrationSig\"],[50932,\"ProfileCalibrationSig\"],[50933,\"ProfileIFD\"],[50934,\"AsShotProfileName\"],[50936,\"ProfileName\"],[50937,\"ProfileHueSatMapDims\"],[50938,\"ProfileHueSatMapData1\"],[50939,\"ProfileHueSatMapData2\"],[50940,\"ProfileToneCurve\"],[50941,\"ProfileEmbedPolicy\"],[50942,\"ProfileCopyright\"],[50964,\"ForwardMatrix1\"],[50965,\"ForwardMatrix2\"],[50966,\"PreviewApplicationName\"],[50967,\"PreviewApplicationVersion\"],[50968,\"PreviewSettingsName\"],[50969,\"PreviewSettingsDigest\"],[50970,\"PreviewColorSpace\"],[50971,\"PreviewDateTime\"],[50972,\"RawImageDigest\"],[50973,\"OriginalRawFileDigest\"],[50981,\"ProfileLookTableDims\"],[50982,\"ProfileLookTableData\"],[51043,\"TimeCodes\"],[51044,\"FrameRate\"],[51058,\"TStop\"],[51081,\"ReelName\"],[51089,\"OriginalDefaultFinalSize\"],[51090,\"OriginalBestQualitySize\"],[51091,\"OriginalDefaultCropSize\"],[51105,\"CameraLabel\"],[51107,\"ProfileHueSatMapEncoding\"],[51108,\"ProfileLookTableEncoding\"],[51109,\"BaselineExposureOffset\"],[51110,\"DefaultBlackRender\"],[51111,\"NewRawImageDigest\"],[51112,\"RawToPreviewGain\"]]);let ct=[[273,\"StripOffsets\"],[279,\"StripByteCounts\"],[288,\"FreeOffsets\"],[289,\"FreeByteCounts\"],[291,\"GrayResponseCurve\"],[292,\"T4Options\"],[293,\"T6Options\"],[300,\"ColorResponseUnit\"],[320,\"ColorMap\"],[324,\"TileOffsets\"],[325,\"TileByteCounts\"],[326,\"BadFaxLines\"],[327,\"CleanFaxData\"],[328,\"ConsecutiveBadFaxLines\"],[330,\"SubIFD\"],[333,\"InkNames\"],[334,\"NumberofInks\"],[336,\"DotRange\"],[338,\"ExtraSamples\"],[339,\"SampleFormat\"],[340,\"SMinSampleValue\"],[341,\"SMaxSampleValue\"],[342,\"TransferRange\"],[343,\"ClipPath\"],[344,\"XClipPathUnits\"],[345,\"YClipPathUnits\"],[346,\"Indexed\"],[347,\"JPEGTables\"],[351,\"OPIProxy\"],[400,\"GlobalParametersIFD\"],[401,\"ProfileType\"],[402,\"FaxProfile\"],[403,\"CodingMethods\"],[404,\"VersionYear\"],[405,\"ModeNumber\"],[433,\"Decode\"],[434,\"DefaultImageColor\"],[435,\"T82Options\"],[437,\"JPEGTables\"],[512,\"JPEGProc\"],[515,\"JPEGRestartInterval\"],[517,\"JPEGLosslessPredictors\"],[518,\"JPEGPointTransforms\"],[519,\"JPEGQTables\"],[520,\"JPEGDCTables\"],[521,\"JPEGACTables\"],[559,\"StripRowCounts\"],[999,\"USPTOMiscellaneous\"],[18247,\"XP_DIP_XML\"],[18248,\"StitchInfo\"],[28672,\"SonyRawFileType\"],[28688,\"SonyToneCurve\"],[28721,\"VignettingCorrection\"],[28722,\"VignettingCorrParams\"],[28724,\"ChromaticAberrationCorrection\"],[28725,\"ChromaticAberrationCorrParams\"],[28726,\"DistortionCorrection\"],[28727,\"DistortionCorrParams\"],[29895,\"SonyCropTopLeft\"],[29896,\"SonyCropSize\"],[32781,\"ImageID\"],[32931,\"WangTag1\"],[32932,\"WangAnnotation\"],[32933,\"WangTag3\"],[32934,\"WangTag4\"],[32953,\"ImageReferencePoints\"],[32954,\"RegionXformTackPoint\"],[32955,\"WarpQuadrilateral\"],[32956,\"AffineTransformMat\"],[32995,\"Matteing\"],[32996,\"DataType\"],[32997,\"ImageDepth\"],[32998,\"TileDepth\"],[33300,\"ImageFullWidth\"],[33301,\"ImageFullHeight\"],[33302,\"TextureFormat\"],[33303,\"WrapModes\"],[33304,\"FovCot\"],[33305,\"MatrixWorldToScreen\"],[33306,\"MatrixWorldToCamera\"],[33405,\"Model2\"],[33421,\"CFARepeatPatternDim\"],[33422,\"CFAPattern2\"],[33423,\"BatteryLevel\"],[33424,\"KodakIFD\"],[33445,\"MDFileTag\"],[33446,\"MDScalePixel\"],[33447,\"MDColorTable\"],[33448,\"MDLabName\"],[33449,\"MDSampleInfo\"],[33450,\"MDPrepDate\"],[33451,\"MDPrepTime\"],[33452,\"MDFileUnits\"],[33589,\"AdventScale\"],[33590,\"AdventRevision\"],[33628,\"UIC1Tag\"],[33629,\"UIC2Tag\"],[33630,\"UIC3Tag\"],[33631,\"UIC4Tag\"],[33918,\"IntergraphPacketData\"],[33919,\"IntergraphFlagRegisters\"],[33921,\"INGRReserved\"],[34016,\"Site\"],[34017,\"ColorSequence\"],[34018,\"IT8Header\"],[34019,\"RasterPadding\"],[34020,\"BitsPerRunLength\"],[34021,\"BitsPerExtendedRunLength\"],[34022,\"ColorTable\"],[34023,\"ImageColorIndicator\"],[34024,\"BackgroundColorIndicator\"],[34025,\"ImageColorValue\"],[34026,\"BackgroundColorValue\"],[34027,\"PixelIntensityRange\"],[34028,\"TransparencyIndicator\"],[34029,\"ColorCharacterization\"],[34030,\"HCUsage\"],[34031,\"TrapIndicator\"],[34032,\"CMYKEquivalent\"],[34152,\"AFCP_IPTC\"],[34232,\"PixelMagicJBIGOptions\"],[34263,\"JPLCartoIFD\"],[34306,\"WB_GRGBLevels\"],[34310,\"LeafData\"],[34687,\"TIFF_FXExtensions\"],[34688,\"MultiProfiles\"],[34689,\"SharedData\"],[34690,\"T88Options\"],[34732,\"ImageLayer\"],[34750,\"JBIGOptions\"],[34856,\"Opto-ElectricConvFactor\"],[34857,\"Interlace\"],[34908,\"FaxRecvParams\"],[34909,\"FaxSubAddress\"],[34910,\"FaxRecvTime\"],[34929,\"FedexEDR\"],[34954,\"LeafSubIFD\"],[37387,\"FlashEnergy\"],[37388,\"SpatialFrequencyResponse\"],[37389,\"Noise\"],[37390,\"FocalPlaneXResolution\"],[37391,\"FocalPlaneYResolution\"],[37392,\"FocalPlaneResolutionUnit\"],[37397,\"ExposureIndex\"],[37398,\"TIFF-EPStandardID\"],[37399,\"SensingMethod\"],[37434,\"CIP3DataFile\"],[37435,\"CIP3Sheet\"],[37436,\"CIP3Side\"],[37439,\"StoNits\"],[37679,\"MSDocumentText\"],[37680,\"MSPropertySetStorage\"],[37681,\"MSDocumentTextPosition\"],[37724,\"ImageSourceData\"],[40965,\"InteropIFD\"],[40976,\"SamsungRawPointersOffset\"],[40977,\"SamsungRawPointersLength\"],[41217,\"SamsungRawByteOrder\"],[41218,\"SamsungRawUnknown\"],[41484,\"SpatialFrequencyResponse\"],[41485,\"Noise\"],[41489,\"ImageNumber\"],[41490,\"SecurityClassification\"],[41491,\"ImageHistory\"],[41494,\"TIFF-EPStandardID\"],[41995,\"DeviceSettingDescription\"],[42112,\"GDALMetadata\"],[42113,\"GDALNoData\"],[44992,\"ExpandSoftware\"],[44993,\"ExpandLens\"],[44994,\"ExpandFilm\"],[44995,\"ExpandFilterLens\"],[44996,\"ExpandScanner\"],[44997,\"ExpandFlashLamp\"],[46275,\"HasselbladRawImage\"],[48129,\"PixelFormat\"],[48130,\"Transformation\"],[48131,\"Uncompressed\"],[48132,\"ImageType\"],[48256,\"ImageWidth\"],[48257,\"ImageHeight\"],[48258,\"WidthResolution\"],[48259,\"HeightResolution\"],[48320,\"ImageOffset\"],[48321,\"ImageByteCount\"],[48322,\"AlphaOffset\"],[48323,\"AlphaByteCount\"],[48324,\"ImageDataDiscard\"],[48325,\"AlphaDataDiscard\"],[50215,\"OceScanjobDesc\"],[50216,\"OceApplicationSelector\"],[50217,\"OceIDNumber\"],[50218,\"OceImageLogic\"],[50255,\"Annotations\"],[50459,\"HasselbladExif\"],[50547,\"OriginalFileName\"],[50560,\"USPTOOriginalContentType\"],[50656,\"CR2CFAPattern\"],[50710,\"CFAPlaneColor\"],[50711,\"CFALayout\"],[50712,\"LinearizationTable\"],[50713,\"BlackLevelRepeatDim\"],[50714,\"BlackLevel\"],[50715,\"BlackLevelDeltaH\"],[50716,\"BlackLevelDeltaV\"],[50717,\"WhiteLevel\"],[50718,\"DefaultScale\"],[50719,\"DefaultCropOrigin\"],[50720,\"DefaultCropSize\"],[50733,\"BayerGreenSplit\"],[50737,\"ChromaBlurRadius\"],[50738,\"AntiAliasStrength\"],[50752,\"RawImageSegmentation\"],[50780,\"BestQualityScale\"],[50784,\"AliasLayerMetadata\"],[50829,\"ActiveArea\"],[50830,\"MaskedAreas\"],[50935,\"NoiseReductionApplied\"],[50974,\"SubTileBlockSize\"],[50975,\"RowInterleaveFactor\"],[51008,\"OpcodeList1\"],[51009,\"OpcodeList2\"],[51022,\"OpcodeList3\"],[51041,\"NoiseProfile\"],[51114,\"CacheVersion\"],[51125,\"DefaultUserCrop\"],[51157,\"NikonNEFInfo\"],[65024,\"KdcIFD\"]];F(E,\"ifd0\",ct),F(E,\"exif\",ct),U(B,\"gps\",[[23,{M:\"Magnetic North\",T:\"True North\"}],[25,{K:\"Kilometers\",M:\"Miles\",N:\"Nautical Miles\"}]]);class ft extends re{static canHandle(e,t){return 224===e.getUint8(t+1)&&1246120262===e.getUint32(t+4)&&0===e.getUint8(t+8)}parse(){return this.parseTags(),this.translate(),this.output}parseTags(){this.raw=new Map([[0,this.chunk.getUint16(0)],[2,this.chunk.getUint8(2)],[3,this.chunk.getUint16(3)],[5,this.chunk.getUint16(5)],[7,this.chunk.getUint8(7)],[8,this.chunk.getUint8(8)]])}}c(ft,\"type\",\"jfif\"),c(ft,\"headerLength\",9),T.set(\"jfif\",ft),U(E,\"jfif\",[[0,\"JFIFVersion\"],[2,\"ResolutionUnit\"],[3,\"XResolution\"],[5,\"YResolution\"],[7,\"ThumbnailWidth\"],[8,\"ThumbnailHeight\"]]);class dt extends re{parse(){return this.parseTags(),this.translate(),this.output}parseTags(){this.raw=new Map([[0,this.chunk.getUint32(0)],[4,this.chunk.getUint32(4)],[8,this.chunk.getUint8(8)],[9,this.chunk.getUint8(9)],[10,this.chunk.getUint8(10)],[11,this.chunk.getUint8(11)],[12,this.chunk.getUint8(12)],...Array.from(this.raw)])}}c(dt,\"type\",\"ihdr\"),T.set(\"ihdr\",dt),U(E,\"ihdr\",[[0,\"ImageWidth\"],[4,\"ImageHeight\"],[8,\"BitDepth\"],[9,\"ColorType\"],[10,\"Compression\"],[11,\"Filter\"],[12,\"Interlace\"]]),U(B,\"ihdr\",[[9,{0:\"Grayscale\",2:\"RGB\",3:\"Palette\",4:\"Grayscale with Alpha\",6:\"RGB with Alpha\",DEFAULT:\"Unknown\"}],[10,{0:\"Deflate/Inflate\",DEFAULT:\"Unknown\"}],[11,{0:\"Adaptive\",DEFAULT:\"Unknown\"}],[12,{0:\"Noninterlaced\",1:\"Adam7 Interlace\",DEFAULT:\"Unknown\"}]]);class pt extends re{static canHandle(e,t){return 226===e.getUint8(t+1)&&1229144927===e.getUint32(t+4)}static findPosition(e,t){let i=super.findPosition(e,t);return i.chunkNumber=e.getUint8(t+16),i.chunkCount=e.getUint8(t+17),i.multiSegment=i.chunkCount>1,i}static handleMultiSegments(e){return function(e){let t=function(e){let t=e[0].constructor,i=0;for(let t of e)i+=t.length;let n=new t(i),s=0;for(let t of e)n.set(t,s),s+=t.length;return n}(e.map((e=>e.chunk.toUint8())));return new I(t)}(e)}parse(){return this.raw=new Map,this.parseHeader(),this.parseTags(),this.translate(),this.output}parseHeader(){let{raw:e}=this;this.chunk.byteLength<84&&g(\"ICC header is too short\");for(let[t,i]of Object.entries(gt)){t=parseInt(t,10);let n=i(this.chunk,t);\"\\0\\0\\0\\0\"!==n&&e.set(t,n)}}parseTags(){let e,t,i,n,s,{raw:r}=this,a=this.chunk.getUint32(128),o=132,l=this.chunk.byteLength;for(;a--;){if(e=this.chunk.getString(o,4),t=this.chunk.getUint32(o+4),i=this.chunk.getUint32(o+8),n=this.chunk.getString(t,4),t+i>l)return void console.warn(\"reached the end of the first ICC chunk. Enable options.tiff.multiSegment to read all ICC segments.\");s=this.parseTag(n,t,i),void 0!==s&&\"\\0\\0\\0\\0\"!==s&&r.set(e,s),o+=12}}parseTag(e,t,i){switch(e){case\"desc\":return this.parseDesc(t);case\"mluc\":return this.parseMluc(t);case\"text\":return this.parseText(t,i);case\"sig \":return this.parseSig(t)}if(!(t+i>this.chunk.byteLength))return this.chunk.getUint8Array(t,i)}parseDesc(e){let t=this.chunk.getUint32(e+8)-1;return m(this.chunk.getString(e+12,t))}parseText(e,t){return m(this.chunk.getString(e+8,t-8))}parseSig(e){return m(this.chunk.getString(e+8,4))}parseMluc(e){let{chunk:t}=this,i=t.getUint32(e+8),n=t.getUint32(e+12),s=e+16,r=[];for(let a=0;a>4,e.getUint8(t+1)%16].map((e=>e.toString(10))).join(\".\")},12:mt,16:mt,20:mt,24:function(e,t){const i=e.getUint16(t),n=e.getUint16(t+2)-1,s=e.getUint16(t+4),r=e.getUint16(t+6),a=e.getUint16(t+8),o=e.getUint16(t+10);return new Date(Date.UTC(i,n,s,r,a,o))},36:mt,40:mt,48:mt,52:mt,64:(e,t)=>e.getUint32(t),80:mt};function mt(e,t){return m(e.getString(t,4))}T.set(\"icc\",pt),U(E,\"icc\",[[4,\"ProfileCMMType\"],[8,\"ProfileVersion\"],[12,\"ProfileClass\"],[16,\"ColorSpaceData\"],[20,\"ProfileConnectionSpace\"],[24,\"ProfileDateTime\"],[36,\"ProfileFileSignature\"],[40,\"PrimaryPlatform\"],[44,\"CMMFlags\"],[48,\"DeviceManufacturer\"],[52,\"DeviceModel\"],[56,\"DeviceAttributes\"],[64,\"RenderingIntent\"],[68,\"ConnectionSpaceIlluminant\"],[80,\"ProfileCreator\"],[84,\"ProfileID\"],[\"Header\",\"ProfileHeader\"],[\"MS00\",\"WCSProfiles\"],[\"bTRC\",\"BlueTRC\"],[\"bXYZ\",\"BlueMatrixColumn\"],[\"bfd\",\"UCRBG\"],[\"bkpt\",\"MediaBlackPoint\"],[\"calt\",\"CalibrationDateTime\"],[\"chad\",\"ChromaticAdaptation\"],[\"chrm\",\"Chromaticity\"],[\"ciis\",\"ColorimetricIntentImageState\"],[\"clot\",\"ColorantTableOut\"],[\"clro\",\"ColorantOrder\"],[\"clrt\",\"ColorantTable\"],[\"cprt\",\"ProfileCopyright\"],[\"crdi\",\"CRDInfo\"],[\"desc\",\"ProfileDescription\"],[\"devs\",\"DeviceSettings\"],[\"dmdd\",\"DeviceModelDesc\"],[\"dmnd\",\"DeviceMfgDesc\"],[\"dscm\",\"ProfileDescriptionML\"],[\"fpce\",\"FocalPlaneColorimetryEstimates\"],[\"gTRC\",\"GreenTRC\"],[\"gXYZ\",\"GreenMatrixColumn\"],[\"gamt\",\"Gamut\"],[\"kTRC\",\"GrayTRC\"],[\"lumi\",\"Luminance\"],[\"meas\",\"Measurement\"],[\"meta\",\"Metadata\"],[\"mmod\",\"MakeAndModel\"],[\"ncl2\",\"NamedColor2\"],[\"ncol\",\"NamedColor\"],[\"ndin\",\"NativeDisplayInfo\"],[\"pre0\",\"Preview0\"],[\"pre1\",\"Preview1\"],[\"pre2\",\"Preview2\"],[\"ps2i\",\"PS2RenderingIntent\"],[\"ps2s\",\"PostScript2CSA\"],[\"psd0\",\"PostScript2CRD0\"],[\"psd1\",\"PostScript2CRD1\"],[\"psd2\",\"PostScript2CRD2\"],[\"psd3\",\"PostScript2CRD3\"],[\"pseq\",\"ProfileSequenceDesc\"],[\"psid\",\"ProfileSequenceIdentifier\"],[\"psvm\",\"PS2CRDVMSize\"],[\"rTRC\",\"RedTRC\"],[\"rXYZ\",\"RedMatrixColumn\"],[\"resp\",\"OutputResponse\"],[\"rhoc\",\"ReflectionHardcopyOrigColorimetry\"],[\"rig0\",\"PerceptualRenderingIntentGamut\"],[\"rig2\",\"SaturationRenderingIntentGamut\"],[\"rpoc\",\"ReflectionPrintOutputColorimetry\"],[\"sape\",\"SceneAppearanceEstimates\"],[\"scoe\",\"SceneColorimetryEstimates\"],[\"scrd\",\"ScreeningDesc\"],[\"scrn\",\"Screening\"],[\"targ\",\"CharTarget\"],[\"tech\",\"Technology\"],[\"vcgt\",\"VideoCardGamma\"],[\"view\",\"ViewingConditions\"],[\"vued\",\"ViewingCondDesc\"],[\"wtpt\",\"MediaWhitePoint\"]]);const St={\"4d2p\":\"Erdt Systems\",AAMA:\"Aamazing Technologies\",ACER:\"Acer\",ACLT:\"Acolyte Color Research\",ACTI:\"Actix Sytems\",ADAR:\"Adara Technology\",ADBE:\"Adobe\",ADI:\"ADI Systems\",AGFA:\"Agfa Graphics\",ALMD:\"Alps Electric\",ALPS:\"Alps Electric\",ALWN:\"Alwan Color Expertise\",AMTI:\"Amiable Technologies\",AOC:\"AOC International\",APAG:\"Apago\",APPL:\"Apple Computer\",AST:\"AST\",\"AT&T\":\"AT&T\",BAEL:\"BARBIERI electronic\",BRCO:\"Barco NV\",BRKP:\"Breakpoint\",BROT:\"Brother\",BULL:\"Bull\",BUS:\"Bus Computer Systems\",\"C-IT\":\"C-Itoh\",CAMR:\"Intel\",CANO:\"Canon\",CARR:\"Carroll Touch\",CASI:\"Casio\",CBUS:\"Colorbus PL\",CEL:\"Crossfield\",CELx:\"Crossfield\",CGS:\"CGS Publishing Technologies International\",CHM:\"Rochester Robotics\",CIGL:\"Colour Imaging Group, London\",CITI:\"Citizen\",CL00:\"Candela\",CLIQ:\"Color IQ\",CMCO:\"Chromaco\",CMiX:\"CHROMiX\",COLO:\"Colorgraphic Communications\",COMP:\"Compaq\",COMp:\"Compeq/Focus Technology\",CONR:\"Conrac Display Products\",CORD:\"Cordata Technologies\",CPQ:\"Compaq\",CPRO:\"ColorPro\",CRN:\"Cornerstone\",CTX:\"CTX International\",CVIS:\"ColorVision\",CWC:\"Fujitsu Laboratories\",DARI:\"Darius Technology\",DATA:\"Dataproducts\",DCP:\"Dry Creek Photo\",DCRC:\"Digital Contents Resource Center, Chung-Ang University\",DELL:\"Dell Computer\",DIC:\"Dainippon Ink and Chemicals\",DICO:\"Diconix\",DIGI:\"Digital\",\"DL&C\":\"Digital Light & Color\",DPLG:\"Doppelganger\",DS:\"Dainippon Screen\",DSOL:\"DOOSOL\",DUPN:\"DuPont\",EPSO:\"Epson\",ESKO:\"Esko-Graphics\",ETRI:\"Electronics and Telecommunications Research Institute\",EVER:\"Everex Systems\",EXAC:\"ExactCODE\",Eizo:\"Eizo\",FALC:\"Falco Data Products\",FF:\"Fuji Photo Film\",FFEI:\"FujiFilm Electronic Imaging\",FNRD:\"Fnord Software\",FORA:\"Fora\",FORE:\"Forefront Technology\",FP:\"Fujitsu\",FPA:\"WayTech Development\",FUJI:\"Fujitsu\",FX:\"Fuji Xerox\",GCC:\"GCC Technologies\",GGSL:\"Global Graphics Software\",GMB:\"Gretagmacbeth\",GMG:\"GMG\",GOLD:\"GoldStar Technology\",GOOG:\"Google\",GPRT:\"Giantprint\",GTMB:\"Gretagmacbeth\",GVC:\"WayTech Development\",GW2K:\"Sony\",HCI:\"HCI\",HDM:\"Heidelberger Druckmaschinen\",HERM:\"Hermes\",HITA:\"Hitachi America\",HP:\"Hewlett-Packard\",HTC:\"Hitachi\",HiTi:\"HiTi Digital\",IBM:\"IBM\",IDNT:\"Scitex\",IEC:\"Hewlett-Packard\",IIYA:\"Iiyama North America\",IKEG:\"Ikegami Electronics\",IMAG:\"Image Systems\",IMI:\"Ingram Micro\",INTC:\"Intel\",INTL:\"N/A (INTL)\",INTR:\"Intra Electronics\",IOCO:\"Iocomm International Technology\",IPS:\"InfoPrint Solutions Company\",IRIS:\"Scitex\",ISL:\"Ichikawa Soft Laboratory\",ITNL:\"N/A (ITNL)\",IVM:\"IVM\",IWAT:\"Iwatsu Electric\",Idnt:\"Scitex\",Inca:\"Inca Digital Printers\",Iris:\"Scitex\",JPEG:\"Joint Photographic Experts Group\",JSFT:\"Jetsoft Development\",JVC:\"JVC Information Products\",KART:\"Scitex\",KFC:\"KFC Computek Components\",KLH:\"KLH Computers\",KMHD:\"Konica Minolta\",KNCA:\"Konica\",KODA:\"Kodak\",KYOC:\"Kyocera\",Kart:\"Scitex\",LCAG:\"Leica\",LCCD:\"Leeds Colour\",LDAK:\"Left Dakota\",LEAD:\"Leading Technology\",LEXM:\"Lexmark International\",LINK:\"Link Computer\",LINO:\"Linotronic\",LITE:\"Lite-On\",Leaf:\"Leaf\",Lino:\"Linotronic\",MAGC:\"Mag Computronic\",MAGI:\"MAG Innovision\",MANN:\"Mannesmann\",MICN:\"Micron Technology\",MICR:\"Microtek\",MICV:\"Microvitec\",MINO:\"Minolta\",MITS:\"Mitsubishi Electronics America\",MITs:\"Mitsuba\",MNLT:\"Minolta\",MODG:\"Modgraph\",MONI:\"Monitronix\",MONS:\"Monaco Systems\",MORS:\"Morse Technology\",MOTI:\"Motive Systems\",MSFT:\"Microsoft\",MUTO:\"MUTOH INDUSTRIES\",Mits:\"Mitsubishi Electric\",NANA:\"NANAO\",NEC:\"NEC\",NEXP:\"NexPress Solutions\",NISS:\"Nissei Sangyo America\",NKON:\"Nikon\",NONE:\"none\",OCE:\"Oce Technologies\",OCEC:\"OceColor\",OKI:\"Oki\",OKID:\"Okidata\",OKIP:\"Okidata\",OLIV:\"Olivetti\",OLYM:\"Olympus\",ONYX:\"Onyx Graphics\",OPTI:\"Optiquest\",PACK:\"Packard Bell\",PANA:\"Matsushita Electric Industrial\",PANT:\"Pantone\",PBN:\"Packard Bell\",PFU:\"PFU\",PHIL:\"Philips Consumer Electronics\",PNTX:\"HOYA\",POne:\"Phase One A/S\",PREM:\"Premier Computer Innovations\",PRIN:\"Princeton Graphic Systems\",PRIP:\"Princeton Publishing Labs\",QLUX:\"Hong Kong\",QMS:\"QMS\",QPCD:\"QPcard AB\",QUAD:\"QuadLaser\",QUME:\"Qume\",RADI:\"Radius\",RDDx:\"Integrated Color Solutions\",RDG:\"Roland DG\",REDM:\"REDMS Group\",RELI:\"Relisys\",RGMS:\"Rolf Gierling Multitools\",RICO:\"Ricoh\",RNLD:\"Edmund Ronald\",ROYA:\"Royal\",RPC:\"Ricoh Printing Systems\",RTL:\"Royal Information Electronics\",SAMP:\"Sampo\",SAMS:\"Samsung\",SANT:\"Jaime Santana Pomares\",SCIT:\"Scitex\",SCRN:\"Dainippon Screen\",SDP:\"Scitex\",SEC:\"Samsung\",SEIK:\"Seiko Instruments\",SEIk:\"Seikosha\",SGUY:\"ScanGuy.com\",SHAR:\"Sharp Laboratories\",SICC:\"International Color Consortium\",SONY:\"Sony\",SPCL:\"SpectraCal\",STAR:\"Star\",STC:\"Sampo Technology\",Scit:\"Scitex\",Sdp:\"Scitex\",Sony:\"Sony\",TALO:\"Talon Technology\",TAND:\"Tandy\",TATU:\"Tatung\",TAXA:\"TAXAN America\",TDS:\"Tokyo Denshi Sekei\",TECO:\"TECO Information Systems\",TEGR:\"Tegra\",TEKT:\"Tektronix\",TI:\"Texas Instruments\",TMKR:\"TypeMaker\",TOSB:\"Toshiba\",TOSH:\"Toshiba\",TOTK:\"TOTOKU ELECTRIC\",TRIU:\"Triumph\",TSBT:\"Toshiba\",TTX:\"TTX Computer Products\",TVM:\"TVM Professional Monitor\",TW:\"TW Casper\",ULSX:\"Ulead Systems\",UNIS:\"Unisys\",UTZF:\"Utz Fehlau & Sohn\",VARI:\"Varityper\",VIEW:\"Viewsonic\",VISL:\"Visual communication\",VIVO:\"Vivo Mobile Communication\",WANG:\"Wang\",WLBR:\"Wilbur Imaging\",WTG2:\"Ware To Go\",WYSE:\"WYSE Technology\",XERX:\"Xerox\",XRIT:\"X-Rite\",ZRAN:\"Zoran\",Zebr:\"Zebra Technologies\",appl:\"Apple Computer\",bICC:\"basICColor\",berg:\"bergdesign\",ceyd:\"Integrated Color Solutions\",clsp:\"MacDermid ColorSpan\",ds:\"Dainippon Screen\",dupn:\"DuPont\",ffei:\"FujiFilm Electronic Imaging\",flux:\"FluxData\",iris:\"Scitex\",kart:\"Scitex\",lcms:\"Little CMS\",lino:\"Linotronic\",none:\"none\",ob4d:\"Erdt Systems\",obic:\"Medigraph\",quby:\"Qubyx Sarl\",scit:\"Scitex\",scrn:\"Dainippon Screen\",sdp:\"Scitex\",siwi:\"SIWI GRAFIKA\",yxym:\"YxyMaster\"},Ct={scnr:\"Scanner\",mntr:\"Monitor\",prtr:\"Printer\",link:\"Device Link\",abst:\"Abstract\",spac:\"Color Space Conversion Profile\",nmcl:\"Named Color\",cenc:\"ColorEncodingSpace profile\",mid:\"MultiplexIdentification profile\",mlnk:\"MultiplexLink profile\",mvis:\"MultiplexVisualization profile\",nkpf:\"Nikon Input Device Profile (NON-STANDARD!)\"};U(B,\"icc\",[[4,St],[12,Ct],[40,Object.assign({},St,Ct)],[48,St],[80,St],[64,{0:\"Perceptual\",1:\"Relative Colorimetric\",2:\"Saturation\",3:\"Absolute Colorimetric\"}],[\"tech\",{amd:\"Active Matrix Display\",crt:\"Cathode Ray Tube Display\",kpcd:\"Photo CD\",pmd:\"Passive Matrix Display\",dcam:\"Digital Camera\",dcpj:\"Digital Cinema Projector\",dmpc:\"Digital Motion Picture Camera\",dsub:\"Dye Sublimation Printer\",epho:\"Electrophotographic Printer\",esta:\"Electrostatic Printer\",flex:\"Flexography\",fprn:\"Film Writer\",fscn:\"Film Scanner\",grav:\"Gravure\",ijet:\"Ink Jet Printer\",imgs:\"Photo Image Setter\",mpfr:\"Motion Picture Film Recorder\",mpfs:\"Motion Picture Film Scanner\",offs:\"Offset Lithography\",pjtv:\"Projection Television\",rpho:\"Photographic Paper Printer\",rscn:\"Reflective Scanner\",silk:\"Silkscreen\",twax:\"Thermal Wax Printer\",vidc:\"Video Camera\",vidm:\"Video Monitor\"}]]);class yt extends re{static canHandle(e,t,i){return 237===e.getUint8(t+1)&&\"Photoshop\"===e.getString(t+4,9)&&void 0!==this.containsIptc8bim(e,t,i)}static headerLength(e,t,i){let n,s=this.containsIptc8bim(e,t,i);if(void 0!==s)return n=e.getUint8(t+s+7),n%2!=0&&(n+=1),0===n&&(n=4),s+8+n}static containsIptc8bim(e,t,i){for(let n=0;n {\n if (loaded.length > 0) {\n drawPhotos.fitZoom(false);\n }\n });\n })\n .on('dragenter.svgLocalPhotos', over)\n .on('dragexit.svgLocalPhotos', over)\n .on('dragover.svgLocalPhotos', over);\n\n _initialized = true;\n }\n\n function ensureViewerLoaded(context) {\n if (_photoFrame) {\n return Promise.resolve(_photoFrame);\n }\n\n const viewer = context.container().select('.photoviewer')\n .selectAll('.local-photos-wrapper')\n .data([0]);\n\n const viewerEnter = viewer.enter()\n .append('div')\n .attr('class', 'photo-wrapper local-photos-wrapper')\n .classed('hide', true);\n\n viewerEnter\n .append('div')\n .attr('class', 'photo-attribution photo-attribution-dual fillD');\n\n const controlsEnter = viewerEnter\n .append('div')\n .attr('class', 'photo-controls-wrap')\n .append('div')\n .attr('class', 'photo-controls-local');\n\n controlsEnter\n .append('button')\n .classed('back', true)\n .on('click.back', () => stepPhotos(-1))\n .text('\u25C0');\n\n controlsEnter\n .append('button')\n .classed('forward', true)\n .on('click.forward', () => stepPhotos(1))\n .text('\u25B6');\n\n return planePhotoFrame(context, viewerEnter)\n .then(planePhotoFrame => _photoFrame = planePhotoFrame);\n }\n\n function stepPhotos(stepBy){\n if (!_photos || _photos.length === 0) return;\n if (_activePhotoIdx === undefined) _activePhotoIdx = 0;\n\n const newIndex = _activePhotoIdx + stepBy;\n _activePhotoIdx = Math.max(0, Math.min(_photos.length - 1, newIndex));\n\n click(null, _photos[_activePhotoIdx], false);\n }\n\n // opens the image at bottom left\n function click(d3_event, image, zoomTo) {\n _activePhotoIdx = _photos.indexOf(image);\n ensureViewerLoaded(context).then(() => {\n const viewer = context.container().select('.photoviewer')\n .datum(image);\n\n const viewerWrap = viewer.select('.local-photos-wrapper');\n const isHidden = viewerWrap.classed('hide');\n if (isHidden) {\n for (const service of Object.values(services)) {\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n viewer.classed('hide', false);\n viewerWrap.classed('hide', false);\n }\n\n const controlsWrap = viewerWrap.select('.photo-controls-wrap');\n\n controlsWrap.select('.back')\n .attr('disabled', _activePhotoIdx <= 0 ? true: null);\n controlsWrap.select('.forward')\n .attr('disabled', _activePhotoIdx >= _photos.length - 1 ? true: null);\n\n const attribution = viewerWrap.selectAll('.photo-attribution').text('');\n\n if (image.date) {\n attribution\n .append('span')\n .text(image.date.toLocaleString(localizer.localeCode()));\n }\n if (image.name) {\n attribution\n .append('span')\n .classed('filename', true)\n .text(image.name);\n }\n\n _photoFrame.selectPhoto({ image_path: '' });\n image.getSrc().then(src => {\n _photoFrame\n .selectPhoto({ image_path: src })\n .showPhotoFrame(viewerWrap);\n\n setStyles();\n });\n });\n\n // centers the map with image location\n if (zoomTo) {\n context.map().centerEase(image.loc);\n }\n }\n\n\n function transform(d) {\n // projection expects [long, lat]\n var svgpoint = projection(d.loc);\n return 'translate(' + svgpoint[0] + ',' + svgpoint[1] + ')';\n }\n\n function setStyles(hovered) {\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n\n context.container().selectAll('.layer-local-photos .viewfield-group')\n .classed('hovered', d => d.id === hovered?.id)\n .classed('highlighted', d => d.id === hovered?.id || d.id === selected?.id)\n .classed('currentView', d => d.id === selected?.id);\n }\n\n // puts the image markers on the map\n function display_markers(imageList) {\n imageList = imageList.filter(image => isArray(image.loc) && isNumber(image.loc[0]) && isNumber(image.loc[1]));\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(imageList, function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', (d3_event, d) => setStyles(d))\n .on('mouseleave', () => setStyles(null))\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const showViewfields = context.map().zoom() >= minViewfieldZoom;\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n // viewfields may or may not be drawn...\n // but if they are, draw below the circles\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', function() {\n const d = this.parentNode.__data__;\n return `rotate(${Math.round(d.direction ?? 0)},0,0),scale(1.5,1.5),translate(-8,-13)`;\n })\n .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z')\n .style('visibility', function() {\n const d = this.parentNode.__data__;\n return isNumber(d.direction) ? 'visible' : 'hidden';\n });\n }\n\n function drawPhotos(selection) {\n layer = selection.selectAll('.layer-local-photos')\n .data(_photos ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-local-photos');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (_photos) {\n display_markers(_photos);\n }\n }\n\n\n function readFileAsDataURL(file) {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result);\n reader.onerror = error => reject(error);\n reader.readAsDataURL(file);\n });\n }\n /**\n * Reads and parses files\n * @param {Array} files - Holds array of file - [file_1, file_2, ...]\n */\n async function readmultifiles(files, callback) {\n const loaded = [];\n\n for (const file of files) {\n try {\n const exifData = await exifr.parse(file); // eslint-disable-line no-await-in-loop\n const photo = {\n service: 'photo',\n id: _idAutoinc++,\n name: file.name,\n getSrc: () => readFileAsDataURL(file),\n file: file,\n loc: [exifData.longitude, exifData.latitude],\n direction: exifData.GPSImgDirection,\n date: exifData.CreateDate || exifData.DateTimeOriginal || exifData.ModifyDate,\n };\n loaded.push(photo);\n const sameName = _photos.filter(i => i.name === photo.name);\n if (sameName.length === 0) {\n _photos.push(photo);\n } else {\n const thisContent = await photo.getSrc(); // eslint-disable-line no-await-in-loop\n const sameNameContent = await Promise.allSettled(sameName.map(i => i.getSrc())); // eslint-disable-line no-await-in-loop\n if (!sameNameContent.some(i => i.value === thisContent)) {\n _photos.push(photo);\n }\n }\n } catch {\n // skip files which are not a supported image file\n }\n }\n\n if (typeof callback === 'function') callback(loaded);\n dispatch.call('change');\n }\n\n drawPhotos.setFiles = function(fileList, callback) {\n // read and parse asynchronously\n readmultifiles(Array.from(fileList), callback);\n return this;\n };\n\n // Step 1: entry point\n /**\n * Sets the fileList\n * @param {Object} fileList - The uploaded files. fileList is an object, not an array object\n * @param {Object} fileList.0 - A File - {name: \"Das.png\", lastModified: 1625064498536, lastModifiedDate: Wed Jun 30 2021 20:18:18 GMT+0530 (India Standard Time), webkitRelativePath: \"\", size: 859658, \u2026}\n * @param {Function} callback - A callback to be called after the photos have been loaded and parsed\n */\n drawPhotos.fileList = function(fileList, callback) {\n if (!arguments.length) return _fileList;\n\n _fileList = fileList;\n\n if (!fileList || !fileList.length) return this;\n\n drawPhotos.setFiles(_fileList, callback);\n\n return this;\n };\n\n drawPhotos.getPhotos = function() {\n return _photos;\n };\n\n drawPhotos.removePhoto = function(id) {\n _photos = _photos.filter(i => i.id !== id);\n dispatch.call('change');\n return _photos;\n };\n\n drawPhotos.openPhoto = click;\n\n drawPhotos.fitZoom = function(force) {\n const coords = _photos\n .map(image => image.loc)\n .filter(l => isArray(l) && isNumber(l[0]) && isNumber(l[1]));\n if (coords.length === 0) return;\n const extent = coords\n .map(l => geoExtent(l, l))\n .reduce((a, b) => a.extend(b));\n\n const map = context.map();\n var viewport = map.trimmedExtent().polygon();\n\n if (force !== false || !geoPolygonIntersectsPolygon(viewport, coords, true)) {\n map.centerZoom(extent.center(), Math.min(18, map.trimmedExtentZoom(extent)));\n }\n };\n\n function showLayer() {\n layer.style('display', 'block');\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', () => {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n });\n }\n\n drawPhotos.enabled = function(val) {\n if (!arguments.length) return _enabled;\n\n _enabled = val;\n if (_enabled) {\n showLayer();\n } else {\n hideLayer();\n }\n\n dispatch.call('change');\n return this;\n };\n\n drawPhotos.hasData = function() {\n return isArray(_photos) && _photos.length > 0;\n };\n\n\n init();\n return drawPhotos;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\n\nimport { modeBrowse } from '../modes/browse';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\n\nlet _layerEnabled = false;\nlet _qaService;\n\nexport function svgOsmose(projection, context, dispatch) {\n const throttledRedraw = throttle(() => dispatch.call('change'), 1000);\n const minZoom = 12;\n\n let touchLayer = d3_select(null);\n let drawLayer = d3_select(null);\n let layerVisible = false;\n\n function markerPath(selection, klass) {\n selection\n .attr('class', klass)\n .attr('transform', 'translate(-10, -28)')\n .attr('points', '16,3 4,3 1,6 1,17 4,20 7,20 10,27 13,20 16,20 19,17.033 19,6');\n }\n\n // Loosely-coupled osmose service for fetching issues\n function getService() {\n if (services.osmose && !_qaService) {\n _qaService = services.osmose;\n _qaService.on('loaded', throttledRedraw);\n } else if (!services.osmose && _qaService) {\n _qaService = null;\n }\n\n return _qaService;\n }\n\n // Show the markers\n function editOn() {\n if (!layerVisible) {\n layerVisible = true;\n drawLayer\n .style('display', 'block');\n }\n }\n\n // Immediately remove the markers and their touch targets\n function editOff() {\n if (layerVisible) {\n layerVisible = false;\n drawLayer\n .style('display', 'none');\n drawLayer.selectAll('.qaItem.osmose')\n .remove();\n touchLayer.selectAll('.qaItem.osmose')\n .remove();\n }\n }\n\n // Enable the layer. This shows the markers and transitions them to visible.\n function layerOn() {\n editOn();\n\n drawLayer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end interrupt', () => dispatch.call('change'));\n }\n\n // Disable the layer. This transitions the layer invisible and then hides the markers.\n function layerOff() {\n throttledRedraw.cancel();\n drawLayer.interrupt();\n touchLayer.selectAll('.qaItem.osmose')\n .remove();\n\n drawLayer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end interrupt', () => {\n editOff();\n dispatch.call('change');\n });\n }\n\n // Update the issue markers\n function updateMarkers() {\n if (!layerVisible || !_layerEnabled) return;\n\n const service = getService();\n const selectedID = context.selectedErrorID();\n const data = (service ? service.getItems(projection) : []);\n const getTransform = svgPointTransform(projection);\n\n // Draw markers..\n const markers = drawLayer.selectAll('.qaItem.osmose')\n .data(data, d => d.id);\n\n // exit\n markers.exit()\n .remove();\n\n // enter\n const markersEnter = markers.enter()\n .append('g')\n .attr('class', d => `qaItem ${d.service} itemId-${d.id} itemType-${d.itemType}`);\n\n markersEnter\n .append('polygon')\n .call(markerPath, 'shadow');\n\n markersEnter\n .append('ellipse')\n .attr('cx', 0)\n .attr('cy', 0)\n .attr('rx', 4.5)\n .attr('ry', 2)\n .attr('class', 'stroke');\n\n markersEnter\n .append('polygon')\n .attr('fill', d => service.getColor(d.item))\n .call(markerPath, 'qaItem-fill');\n\n markersEnter\n .append('use')\n .attr('class', 'icon-annotation')\n .attr('transform', 'translate(-6, -22)')\n .attr('width', '12px')\n .attr('height', '12px')\n .attr('xlink:href', d => d.icon ? '#' + d.icon : '');\n\n // update\n markers\n .merge(markersEnter)\n .sort(sortY)\n .classed('selected', d => d.id === selectedID)\n .attr('transform', getTransform);\n\n // Draw targets..\n if (touchLayer.empty()) return;\n const fillClass = context.getDebug('target') ? 'pink' : 'nocolor';\n\n const targets = touchLayer.selectAll('.qaItem.osmose')\n .data(data, d => d.id);\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('rect')\n .attr('width', '20px')\n .attr('height', '30px')\n .attr('x', '-10px')\n .attr('y', '-28px')\n .merge(targets)\n .sort(sortY)\n .attr('class', d => `qaItem ${d.service} target ${fillClass} itemId-${d.id}`)\n .attr('transform', getTransform);\n\n function sortY(a, b) {\n return (a.id === selectedID) ? 1\n : (b.id === selectedID) ? -1\n : b.loc[1] - a.loc[1];\n }\n }\n\n // Draw the Osmose layer and schedule loading issues and updating markers.\n function drawOsmose(selection) {\n const service = getService();\n\n const surface = context.surface();\n if (surface && !surface.empty()) {\n touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');\n }\n\n drawLayer = selection.selectAll('.layer-osmose')\n .data(service ? [0] : []);\n\n drawLayer.exit()\n .remove();\n\n drawLayer = drawLayer.enter()\n .append('g')\n .attr('class', 'layer-osmose')\n .style('display', _layerEnabled ? 'block' : 'none')\n .merge(drawLayer);\n\n if (_layerEnabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n service.loadIssues(projection);\n updateMarkers();\n } else {\n editOff();\n }\n }\n }\n\n // Toggles the layer on and off\n drawOsmose.enabled = function(val) {\n if (!arguments.length) return _layerEnabled;\n\n _layerEnabled = val;\n if (_layerEnabled) {\n // Strings supplied by Osmose fetched before showing layer for first time\n // NOTE: Currently no way to change locale in iD at runtime, would need to re-call this method if that's ever implemented\n // Also, If layer is toggled quickly multiple requests are sent\n getService().loadStrings()\n .then(layerOn)\n .catch(err => {\n console.log(err); // eslint-disable-line no-console\n });\n } else {\n layerOff();\n if (context.selectedErrorID()) {\n context.enter(modeBrowse(context));\n }\n }\n\n dispatch.call('change');\n return this;\n };\n\n drawOsmose.supported = () => !!getService();\n\n return drawOsmose;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgStreetside(projection, context, dispatch) {\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var minZoom = 14;\n var minMarkerZoom = 16;\n var minViewfieldZoom = 18;\n var layer = d3_select(null);\n var _viewerYaw = 0;\n var _selectedSequence = null;\n var _streetside;\n\n /**\n * init().\n */\n function init() {\n if (svgStreetside.initialized) return; // run once\n svgStreetside.enabled = false;\n svgStreetside.initialized = true;\n }\n\n /**\n * getService().\n */\n function getService() {\n if (services.streetside && !_streetside) {\n _streetside = services.streetside;\n _streetside.event\n .on('viewerChanged.svgStreetside', viewerChanged)\n .on('loadedImages.svgStreetside', throttledRedraw);\n } else if (!services.streetside && _streetside) {\n _streetside = null;\n }\n\n return _streetside;\n }\n\n /**\n * showLayer().\n */\n function showLayer() {\n var service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n /**\n * hideLayer().\n */\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n /**\n * editOn().\n */\n function editOn() {\n layer.style('display', 'block');\n }\n\n /**\n * editOff().\n */\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n /**\n * click() Handles 'bubble' point click event.\n */\n function click(d3_event, d) {\n var service = getService();\n if (!service) return;\n\n // try to preserve the viewer rotation when staying on the same sequence\n if (d.sequenceKey !== _selectedSequence) {\n _viewerYaw = 0; // reset\n }\n _selectedSequence = d.sequenceKey;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, d.key)\n .yaw(_viewerYaw)\n .showViewer(context);\n });\n\n context.map().centerEase(d.loc);\n }\n\n /**\n * mouseover().\n */\n function mouseover(d3_event, d) {\n var service = getService();\n if (service) service.setStyles(context, d);\n }\n\n /**\n * mouseout().\n */\n function mouseout() {\n var service = getService();\n if (service) service.setStyles(context, null);\n }\n\n /**\n * transform().\n */\n function transform(d) {\n var t = svgPointTransform(projection)(d);\n var rot = d.ca + _viewerYaw;\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n\n function viewerChanged() {\n var service = getService();\n if (!service) return;\n\n var viewer = service.viewer();\n if (!viewer) return;\n\n // update viewfield rotation\n _viewerYaw = viewer.getYaw();\n\n // avoid updating if the map is currently transformed\n // e.g. during drags or easing.\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .attr('transform', transform);\n }\n\n\n function filterBubbles(bubbles, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n bubbles = bubbles.filter(function(bubble) {\n return new Date(bubble.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n bubbles = bubbles.filter(function(bubble) {\n return new Date(bubble.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n bubbles = bubbles.filter(function(bubble) {\n return usernames.indexOf(bubble.captured_by) !== -1;\n });\n }\n\n return bubbles;\n }\n\n function filterSequences(sequences, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n sequences = sequences.filter(function(sequences) {\n return new Date(sequences.properties.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n sequences = sequences.filter(function(sequences) {\n return new Date(sequences.properties.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n sequences = sequences.filter(function(sequences) {\n return usernames.indexOf(sequences.properties.captured_by) !== -1;\n });\n }\n\n return sequences;\n }\n\n /**\n * update().\n */\n function update() {\n var viewer = context.container().select('.photoviewer');\n var selected = viewer.empty() ? undefined : viewer.datum();\n var z = ~~context.map().zoom();\n var showMarkers = (z >= minMarkerZoom);\n var showViewfields = (z >= minViewfieldZoom);\n var service = getService();\n\n var sequences = [];\n var bubbles = [];\n\n if (context.photos().showsPanoramic()) {\n sequences = (service ? service.sequences(projection) : []);\n bubbles = (service && showMarkers ? service.bubbles(projection) : []);\n sequences = filterSequences(sequences);\n bubbles = filterBubbles(bubbles);\n }\n\n var traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.key; });\n\n dispatch.call('photoDatesChanged', this, 'streetside', [\n ...filterBubbles(bubbles, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(t => t.properties.vintageStart)]);\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n var groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(bubbles, function(d) {\n // force reenter once bubbles are attached to a sequence\n return d.key + (d.sequenceKey ? 'v1' : 'v0');\n });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n var markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n return (a === selected) ? 1\n : (b === selected) ? -1\n : b.loc[1] - a.loc[1];\n })\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n var viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n // viewfields may or may not be drawn...\n // but if they are, draw below the circles\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n var d = this.parentNode.__data__;\n if (d.pano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n }\n\n /**\n * drawImages()\n * drawImages is the method that is returned (and that runs) every time 'svgStreetside()' is called.\n * 'svgStreetside()' is called from index.js\n */\n function drawImages(selection) {\n var enabled = svgStreetside.enabled;\n var service = getService();\n\n layer = selection.selectAll('.layer-streetside-images')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n var layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-streetside-images')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadBubbles(projection);\n } else {\n dispatch.call('photoDatesChanged', this, 'streetside', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'streetside', []);\n }\n }\n\n\n /**\n * drawImages.enabled().\n */\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgStreetside.enabled;\n svgStreetside.enabled = _;\n if (svgStreetside.enabled) {\n showLayer();\n context.photos().on('change.streetside', update);\n } else {\n hideLayer();\n context.photos().on('change.streetside', null);\n }\n dispatch.call('change');\n return this;\n };\n\n /**\n * drawImages.supported().\n */\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n init();\n\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgVegbilder(projection, context, dispatch) {\n const throttledRedraw = throttle(() => dispatch.call('change'), 1000);\n const minZoom = 14;\n const minMarkerZoom = 16;\n const minViewfieldZoom = 18;\n let layer = d3_select(null);\n let _viewerYaw = 0;\n let _vegbilder;\n\n /**\n * init().\n */\n function init() {\n if (svgVegbilder.initialized) return; // run once\n svgVegbilder.enabled = false;\n svgVegbilder.initialized = true;\n }\n\n /**\n * getService().\n */\n function getService() {\n if (services.vegbilder && !_vegbilder) {\n _vegbilder = services.vegbilder;\n _vegbilder.event\n .on('viewerChanged.svgVegbilder', viewerChanged)\n .on('loadedImages.svgVegbilder', throttledRedraw);\n } else if (!services.vegbilder && _vegbilder) {\n _vegbilder = null;\n }\n\n return _vegbilder;\n }\n\n /**\n * showLayer().\n */\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', () => dispatch.call('change'));\n }\n\n /**\n * hideLayer().\n */\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n /**\n * editOn().\n */\n function editOn() {\n layer.style('display', 'block');\n }\n\n /**\n * editOff().\n */\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n function click(d3_event, d) {\n const service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(() => {\n service\n .selectImage(context, d.key)\n .showViewer(context);\n });\n\n context.map().centerEase(d.loc);\n }\n\n /**\n * mouseover().\n */\n function mouseover(d3_event, d) {\n const service = getService();\n if (service) service.setStyles(context, d);\n }\n\n /**\n * mouseout().\n */\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n /**\n * transform().\n */\n function transform(d, selected) {\n let t = svgPointTransform(projection)(d);\n let rot = d.ca;\n if (d === selected) {\n rot += _viewerYaw;\n }\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n\n function viewerChanged() {\n const service = getService();\n if (!service) return;\n\n const frame = service.photoFrame();\n if (!frame) return;\n\n // update viewfield rotation\n _viewerYaw = frame.getYaw();\n\n // avoid updating if the map is currently transformed\n // e.g. during drags or easing.\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .attr('transform', (d) => transform(d, d));\n }\n\n function filterImages(images, skipDateFilter) {\n const photoContext = context.photos();\n const fromDateString = photoContext.fromDate();\n const toDateString = photoContext.toDate();\n const showsFlat = photoContext.showsFlat();\n const showsPano = photoContext.showsPanoramic();\n\n if (fromDateString && !skipDateFilter) {\n const fromDate = new Date(fromDateString);\n images = images.filter(image => image.captured_at.getTime() >= fromDate.getTime());\n }\n\n if (toDateString && !skipDateFilter) {\n const toDate = new Date(toDateString);\n images = images.filter(image => image.captured_at.getTime() <= toDate.getTime());\n }\n\n if (!showsPano) {\n images = images.filter(image => !image.is_sphere);\n }\n\n if (!showsFlat) {\n images = images.filter(image => image.is_sphere);\n }\n\n return images;\n }\n\n function filterSequences(sequences, skipDateFilter) {\n const photoContext = context.photos();\n const fromDateString = photoContext.fromDate();\n const toDateString = photoContext.toDate();\n const showsFlat = photoContext.showsFlat();\n const showsPano = photoContext.showsPanoramic();\n\n if (fromDateString && !skipDateFilter) {\n const fromDate = new Date(fromDateString);\n sequences = sequences.filter(({images}) => images[0].captured_at.getTime() >= fromDate.getTime());\n }\n\n if (toDateString && !skipDateFilter) {\n const toDate = new Date(toDateString);\n sequences = sequences.filter(({images}) => images[images.length - 1].captured_at.getTime() <= toDate.getTime());\n }\n\n if (!showsPano) {\n sequences = sequences.filter(({images}) => !images[0].is_sphere);\n }\n\n if (!showsFlat) {\n sequences = sequences.filter(({images}) => images[0].is_sphere);\n }\n\n return sequences;\n }\n\n /**\n * update().\n */\n function update() {\n const viewer = context.container().select('.photoviewer');\n const selected = viewer.empty() ? undefined : viewer.datum();\n const z = ~~context.map().zoom();\n const showMarkers = (z >= minMarkerZoom);\n const showViewfields = (z >= minViewfieldZoom);\n const service = getService();\n let sequences = [];\n let images = [];\n\n if (service) {\n // The WFS-layer for that year or image type may not be loaded after a filter is changed\n service.loadImages(context);\n\n sequences = service.sequences(projection);\n images = showMarkers ? service.images(projection) : [];\n\n dispatch.call('photoDatesChanged', this, 'vegbilder', [\n ...filterImages(images, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(s => s.images[0].captured_at)]);\n\n images = filterImages(images);\n sequences = filterSequences(sequences);\n }\n\n let traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, d => d.key);\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, (d) => d.key);\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort((a, b) => {\n return (a === selected) ? 1\n : (b === selected) ? -1\n : b.loc[1] - a.loc[1];\n })\n .attr('transform', (d) => transform(d, selected))\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n // viewfields may or may not be drawn...\n // but if they are, draw below the circles\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n const d = this.parentNode.__data__;\n if (d.is_sphere) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n }\n\n /**\n * drawImages()\n * drawImages is the method that is returned (and that runs) every time 'svgStreetside()' is called.\n * 'svgStreetside()' is called from index.js\n */\n function drawImages(selection) {\n const enabled = svgVegbilder.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-vegbilder')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-vegbilder')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadImages(context);\n } else {\n dispatch.call('photoDatesChanged', this, 'vegbilder', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'vegbilder', []);\n }\n }\n\n\n /**\n * drawImages.enabled().\n */\n drawImages.enabled = function (_) {\n if (!arguments.length) return svgVegbilder.enabled;\n svgVegbilder.enabled = _;\n if (svgVegbilder.enabled) {\n showLayer();\n context.photos().on('change.vegbilder', update);\n } else {\n hideLayer();\n context.photos().on('change.vegbilder', null);\n }\n dispatch.call('change');\n return this;\n };\n\n /**\n * drawImages.supported().\n */\n drawImages.supported = function () {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n drawImages.validHere = function(extent, zoom) {\n return zoom >= (minZoom - 2)\n && getService().validHere(extent);\n };\n\n init();\n\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgMapillaryImages(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const minZoom = 12;\n const minMarkerZoom = 16;\n const minViewfieldZoom = 18;\n let layer = d3_select(null);\n let _mapillary;\n\n\n function init() {\n if (svgMapillaryImages.initialized) return; // run once\n svgMapillaryImages.enabled = false;\n svgMapillaryImages.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('loadedImages', throttledRedraw);\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n\n return _mapillary;\n }\n\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, image) {\n const service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, image.id)\n .showViewer(context);\n });\n\n context.map().centerEase(image.loc);\n }\n\n\n function mouseover(d3_event, image) {\n const service = getService();\n\n if (service) service.setStyles(context, image);\n }\n\n\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n\n function transform(d) {\n let t = svgPointTransform(projection)(d);\n if (d.ca) {\n t += ' rotate(' + Math.floor(d.ca) + ',0,0)';\n }\n return t;\n }\n\n\n function filterImages(images, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n\n if (!showsPano || !showsFlat) {\n images = images.filter(function(image) {\n if (image.is_pano) return showsPano;\n return showsFlat;\n });\n }\n if (fromDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.captured_at).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.captured_at).getTime() <= new Date(toDate).getTime();\n });\n }\n\n return images;\n }\n\n function filterSequences(sequences, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n\n if (!showsPano || !showsFlat) {\n sequences = sequences.filter(function(sequence) {\n if (sequence.properties.hasOwnProperty('is_pano')) {\n if (sequence.properties.is_pano) return showsPano;\n return showsFlat;\n }\n return false;\n });\n }\n if (fromDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() >= new Date(fromDate).getTime().toString();\n });\n }\n if (toDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() <= new Date(toDate).getTime().toString();\n });\n }\n\n return sequences;\n }\n\n function update() {\n const z = ~~context.map().zoom();\n const showMarkers = (z >= minMarkerZoom);\n const showViewfields = (z >= minViewfieldZoom);\n\n const service = getService();\n let sequences = (service ? service.sequences(projection) : []);\n let images = (service && showMarkers ? service.images(projection) : []);\n // images[0]\n // {\n // \"loc\":[13.235349655151367,52.50694232952122],\n // \"captured_at\":1619457514500,\n // \"ca\":0,\n // \"id\":505488307476058,\n // \"is_pano\":false,\n // \"sequence_id\":\"zcyumxorbza3dq3twjybam\"\n // }\n dispatch.call('photoDatesChanged', this, 'mapillary', [\n ...filterImages(images, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(s => s.properties.captured_at)]);\n\n images = filterImages(images);\n sequences = filterSequences(sequences);\n\n service.filterViewer(context);\n\n let traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.id; });\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n return b.loc[1] - a.loc[1]; // sort Y\n })\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter() // viewfields may or may not be drawn...\n .insert('path', 'circle') // but if they are, draw below the circles\n .attr('class', 'viewfield')\n .classed('pano', function() { return this.parentNode.__data__.is_pano; })\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n function viewfieldPath() {\n if (this.parentNode.__data__.is_pano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n }\n\n\n function drawImages(selection) {\n const enabled = svgMapillaryImages.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadImages(projection);\n } else {\n dispatch.call('photoDatesChanged', this, 'mapillary', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'mapillary', []);\n }\n }\n\n\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgMapillaryImages.enabled;\n svgMapillaryImages.enabled = _;\n if (svgMapillaryImages.enabled) {\n showLayer();\n context.photos().on('change.mapillary_images', update);\n } else {\n hideLayer();\n context.photos().on('change.mapillary_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgMapillaryPosition(projection, context) {\n const throttledRedraw = throttle(function () { update(); }, 1000);\n const minZoom = 12;\n const minViewfieldZoom = 18;\n let layer = d3_select(null);\n let _mapillary;\n let viewerCompassAngle;\n\n\n function init() {\n if (svgMapillaryPosition.initialized) return; // run once\n svgMapillaryPosition.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('imageChanged', throttledRedraw);\n _mapillary.event.on('bearingChanged', function(e) {\n viewerCompassAngle = e.bearing;\n\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .filter(function(d) {\n return d.is_pano;\n })\n .attr('transform', transform);\n });\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n\n return _mapillary;\n }\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n function transform(d) {\n let t = svgPointTransform(projection)(d);\n if (d.is_pano && viewerCompassAngle !== null && isFinite(viewerCompassAngle)) {\n t += ' rotate(' + Math.floor(viewerCompassAngle) + ',0,0)';\n } else if (d.ca) {\n t += ' rotate(' + Math.floor(d.ca) + ',0,0)';\n }\n return t;\n }\n\n function update() {\n\n const z = ~~context.map().zoom();\n const showViewfields = (z >= minViewfieldZoom);\n\n const service = getService();\n const image = service && service.getActiveImage();\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(image ? [image] : [], function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group currentView highlighted');\n\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z');\n }\n\n\n function drawImages(selection) {\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary-position')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary-position');\n\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n } else {\n editOff();\n }\n }\n\n\n drawImages.enabled = function() {\n update();\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgMapillarySigns(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const minZoom = 12;\n let layer = d3_select(null);\n let _mapillary;\n\n\n function init() {\n if (svgMapillarySigns.initialized) return; // run once\n svgMapillarySigns.enabled = false;\n svgMapillarySigns.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('loadedSigns', throttledRedraw);\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n return _mapillary;\n }\n\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n service.loadSignResources(context);\n editOn();\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n editOff();\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.icon-sign').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, d) {\n const service = getService();\n if (!service) return;\n\n context.map().centerEase(d.loc);\n\n const selectedImageId = service.getActiveImage() && service.getActiveImage().id;\n\n service.getDetections(d.id).then(detections => {\n if (detections.length) {\n const imageId = detections[0].image.id;\n if (imageId === selectedImageId) {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId);\n } else {\n service.ensureViewerLoaded(context)\n .then(function() {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId)\n .showViewer(context);\n });\n }\n }\n });\n }\n\n\n function filterData(detectedFeatures) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n\n if (fromDate) {\n var fromTimestamp = new Date(fromDate).getTime();\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.last_seen_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate) {\n var toTimestamp = new Date(toDate).getTime();\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.first_seen_at).getTime() <= toTimestamp;\n });\n }\n\n return detectedFeatures;\n }\n\n\n function update() {\n const service = getService();\n let data = (service ? service.signs(projection) : []);\n data = filterData(data);\n\n const transform = svgPointTransform(projection);\n\n const signs = layer.selectAll('.icon-sign')\n .data(data, function(d) { return d.id; });\n\n // exit\n signs.exit()\n .remove();\n\n // enter\n const enter = signs.enter()\n .append('g')\n .attr('class', 'icon-sign icon-detected')\n .on('click', click);\n\n enter\n .append('use')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px')\n .attr('xlink:href', function(d) { return '#' + d.value; });\n\n enter\n .append('rect')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px');\n\n // update\n signs\n .merge(enter)\n .attr('transform', transform);\n }\n\n\n function drawSigns(selection) {\n const enabled = svgMapillarySigns.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary-signs')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary-signs layer-mapillary-detections')\n .style('display', enabled ? 'block' : 'none')\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadSigns(projection);\n service.showSignDetections(true);\n } else {\n editOff();\n }\n } else if (service) {\n service.showSignDetections(false);\n }\n }\n\n\n drawSigns.enabled = function(_) {\n if (!arguments.length) return svgMapillarySigns.enabled;\n svgMapillarySigns.enabled = _;\n if (svgMapillarySigns.enabled) {\n showLayer();\n context.photos().on('change.mapillary_signs', update);\n } else {\n hideLayer();\n context.photos().on('change.mapillary_signs', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawSigns.supported = function() {\n return !!getService();\n };\n\n drawSigns.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawSigns;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\nimport { t } from '../core/localizer';\n\nexport function svgMapillaryMapFeatures(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const minZoom = 12;\n let layer = d3_select(null);\n let _mapillary;\n\n\n function init() {\n if (svgMapillaryMapFeatures.initialized) return; // run once\n svgMapillaryMapFeatures.enabled = false;\n svgMapillaryMapFeatures.initialized = true;\n }\n\n\n function getService() {\n if (services.mapillary && !_mapillary) {\n _mapillary = services.mapillary;\n _mapillary.event.on('loadedMapFeatures', throttledRedraw);\n } else if (!services.mapillary && _mapillary) {\n _mapillary = null;\n }\n return _mapillary;\n }\n\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n service.loadObjectResources(context);\n editOn();\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n editOff();\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.icon-map-feature').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, d) {\n const service = getService();\n if (!service) return;\n\n context.map().centerEase(d.loc);\n\n const selectedImageId = service.getActiveImage() && service.getActiveImage().id;\n\n service.getDetections(d.id).then(detections => {\n if (detections.length) {\n const imageId = detections[0].image.id;\n if (imageId === selectedImageId) {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId);\n } else {\n service.ensureViewerLoaded(context)\n .then(function() {\n service\n .highlightDetection(detections[0])\n .selectImage(context, imageId)\n .showViewer(context);\n });\n }\n }\n });\n }\n\n\n function filterData(detectedFeatures) {\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n\n if (fromDate) {\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.last_seen_at).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate) {\n detectedFeatures = detectedFeatures.filter(function(feature) {\n return new Date(feature.first_seen_at).getTime() <= new Date(toDate).getTime();\n });\n }\n\n return detectedFeatures;\n }\n\n\n function update() {\n const service = getService();\n let data = (service ? service.mapFeatures(projection) : []);\n data = filterData(data);\n\n const transform = svgPointTransform(projection);\n\n const mapFeatures = layer.selectAll('.icon-map-feature')\n .data(data, function(d) { return d.id; });\n\n // exit\n mapFeatures.exit()\n .remove();\n\n // enter\n const enter = mapFeatures.enter()\n .append('g')\n .attr('class', 'icon-map-feature icon-detected')\n .on('click', click);\n\n enter\n .append('title')\n .text(function(d) {\n var id = d.value.replace(/--/g, '.').replace(/-/g, '_');\n return t('mapillary_map_features.' + id);\n });\n\n enter\n .append('use')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px')\n .attr('xlink:href', function(d) {\n if (d.value === 'object--billboard') {\n // no billboard icon right now, so use the advertisement icon\n return '#object--sign--advertisement';\n }\n return '#' + d.value;\n });\n\n enter\n .append('rect')\n .attr('width', '24px')\n .attr('height', '24px')\n .attr('x', '-12px')\n .attr('y', '-12px');\n\n // update\n mapFeatures\n .merge(enter)\n .attr('transform', transform);\n }\n\n\n function drawMapFeatures(selection) {\n const enabled = svgMapillaryMapFeatures.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapillary-map-features')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n layer = layer.enter()\n .append('g')\n .attr('class', 'layer-mapillary-map-features layer-mapillary-detections')\n .style('display', enabled ? 'block' : 'none')\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadMapFeatures(projection);\n service.showFeatureDetections(true);\n } else {\n editOff();\n }\n } else if (service) {\n service.showFeatureDetections(false);\n }\n }\n\n\n drawMapFeatures.enabled = function(_) {\n if (!arguments.length) return svgMapillaryMapFeatures.enabled;\n svgMapillaryMapFeatures.enabled = _;\n if (svgMapillaryMapFeatures.enabled) {\n showLayer();\n context.photos().on('change.mapillary_map_features', update);\n } else {\n hideLayer();\n context.photos().on('change.mapillary_map_features', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawMapFeatures.supported = function() {\n return !!getService();\n };\n\n drawMapFeatures.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawMapFeatures;\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { svgPath, svgPointTransform } from './helpers';\nimport { services } from '../services';\n\n\nexport function svgKartaviewImages(projection, context, dispatch) {\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var minZoom = 12;\n var minMarkerZoom = 16;\n var minViewfieldZoom = 18;\n var layer = d3_select(null);\n var _kartaview;\n\n\n function init() {\n if (svgKartaviewImages.initialized) return; // run once\n svgKartaviewImages.enabled = false;\n svgKartaviewImages.initialized = true;\n }\n\n\n function getService() {\n if (services.kartaview && !_kartaview) {\n _kartaview = services.kartaview;\n _kartaview.event.on('loadedImages', throttledRedraw);\n } else if (!services.kartaview && _kartaview) {\n _kartaview = null;\n }\n\n return _kartaview;\n }\n\n\n function showLayer() {\n var service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n\n function click(d3_event, d) {\n var service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service.selectImage(context, d.key)\n .showViewer(context);\n });\n\n context.map().centerEase(d.loc);\n }\n\n\n function mouseover(d3_event, d) {\n var service = getService();\n if (service) service.setStyles(context, d);\n }\n\n\n function mouseout() {\n var service = getService();\n if (service) service.setStyles(context, null);\n }\n\n\n function transform(d) {\n var t = svgPointTransform(projection)(d);\n if (d.ca) {\n t += ' rotate(' + Math.floor(d.ca) + ',0,0)';\n }\n return t;\n }\n\n\n function filterImages(images, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n images = images.filter(function(item) {\n return new Date(item.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n images = images.filter(function(item) {\n return new Date(item.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n images = images.filter(function(item) {\n return usernames.indexOf(item.captured_by) !== -1;\n });\n }\n\n return images;\n }\n\n function filterSequences(sequences, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n var usernames = context.photos().usernames();\n\n if (fromDate && !skipDateFilter) {\n var fromTimestamp = new Date(fromDate).getTime();\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() >= fromTimestamp;\n });\n }\n if (toDate && !skipDateFilter) {\n var toTimestamp = new Date(toDate).getTime();\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.captured_at).getTime() <= toTimestamp;\n });\n }\n if (usernames) {\n sequences = sequences.filter(function(sequence) {\n return usernames.indexOf(sequence.properties.captured_by) !== -1;\n });\n }\n\n return sequences;\n }\n\n function update() {\n var viewer = context.container().select('.photoviewer');\n var selected = viewer.empty() ? undefined : viewer.datum();\n\n var z = ~~context.map().zoom();\n var showMarkers = (z >= minMarkerZoom);\n var showViewfields = (z >= minViewfieldZoom);\n\n const service = getService();\n\n let sequences = (service ? service.sequences(projection) : []);\n let images = (service && showMarkers ? service.images(projection) : []);\n dispatch.call('photoDatesChanged', this, 'kartaview', [\n ...filterImages(images, true).map(p => p.captured_at),\n ...filterSequences(sequences, true).map(s => s.properties.captured_at)]);\n sequences = filterSequences(sequences);\n images = filterImages(images);\n\n var traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.key; });\n\n // exit\n traces.exit()\n .remove();\n\n // enter/update\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n var groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, function(d) { return d.key; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n var markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n return (a === selected) ? 1\n : (b === selected) ? -1\n : b.loc[1] - a.loc[1]; // sort Y\n })\n .attr('transform', transform)\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n var viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter() // viewfields may or may not be drawn...\n .insert('path', 'circle') // but if they are, draw below the circles\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z');\n }\n\n\n function drawImages(selection) {\n var enabled = svgKartaviewImages.enabled,\n service = getService();\n\n layer = selection.selectAll('.layer-kartaview')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n var layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-kartaview')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n update();\n service.loadImages(projection);\n } else {\n dispatch.call('photoDatesChanged', this, 'kartaview', []);\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'kartaview', []);\n }\n }\n\n\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgKartaviewImages.enabled;\n svgKartaviewImages.enabled = _;\n if (svgKartaviewImages.enabled) {\n showLayer();\n context.photos().on('change.kartaview_images', update);\n } else {\n hideLayer();\n context.photos().on('change.kartaview_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= minZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { services } from '../services';\nimport {svgPath, svgPointTransform} from './helpers';\n\n\nexport function svgMapilioImages(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const imageMinZoom = 16;\n const lineMinZoom = 10;\n const viewFieldZoomLevel = 18;\n let layer = d3_select(null);\n let _mapilio;\n let _viewerYaw = 0;\n\n function init() {\n if (svgMapilioImages.initialized) return;\n svgMapilioImages.enabled = false;\n svgMapilioImages.initialized = true;\n }\n\n function getService() {\n if (services.mapilio && !_mapilio) {\n _mapilio = services.mapilio;\n _mapilio.event\n .on('loadedImages', throttledRedraw)\n .on('loadedLines', throttledRedraw);\n } else if (!services.mapilio && _mapilio) {\n _mapilio = null;\n }\n\n return _mapilio;\n }\n\n /**\n * Filters images\n * @param {*} images\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered images\n */\n function filterImages(images, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n\n if (fromDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() <= new Date(toDate).getTime();\n });\n }\n\n return images;\n }\n\n /**\n * Filters sequences\n * @param {*} sequences\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered sequences\n */\n function filterSequences(sequences, skipDateFilter = false) {\n var fromDate = context.photos().fromDate();\n var toDate = context.photos().toDate();\n\n if (fromDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.capture_time).getTime() >= new Date(fromDate).getTime().toString();\n });\n }\n if (toDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.capture_time).getTime() <= new Date(toDate).getTime().toString();\n });\n }\n\n return sequences;\n }\n\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n function transform(d, selectedImageId) {\n let t = svgPointTransform(projection)(d);\n let rot = d.heading || 0;\n\n if (d.id === selectedImageId) {\n rot += _viewerYaw;\n }\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n function click(d3_event, image) {\n const service = getService();\n if (!service) return;\n\n service.ensureViewerLoaded(context, image.id)\n .then(() => {\n service.selectImage(context, image.id)\n .showViewer(context);\n });\n\n context.map().centerEase(image.loc);\n }\n\n function mouseover(d3_event, image) {\n const service = getService();\n if (service) service.setStyles(context, image);\n }\n\n\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n async function update() {\n const zoom = ~~context.map().zoom();\n const showViewfields = (zoom >= viewFieldZoomLevel);\n const service = getService();\n\n let sequences = (service ? service.sequences(projection, zoom) : []);\n let images = (service && zoom >= imageMinZoom ? service.images(projection) : []);\n\n dispatch.call('photoDatesChanged', this, 'mapilio', [\n ...filterImages(images, true).map(p => p.capture_time),\n ...filterSequences(sequences, true).map(s => s.properties.capture_time)\n ]);\n\n images = await filterImages(images);\n sequences = await filterSequences(sequences);\n\n const activeImage = service.getActiveImage?.();\n const activeImageId = activeImage ? activeImage.id : null;\n\n let traces = layer\n .selectAll('.sequences')\n .selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.id; });\n\n // exit\n traces.exit().remove();\n\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n const groups = layer\n .selectAll('.markers')\n .selectAll('.viewfield-group')\n .data(images, function(d) { return d.id; });\n\n // exit\n groups.exit().remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n if (a.id === activeImageId) return 1;\n if (b.id === activeImageId) return -1;\n return a.capture_time_parsed - b.capture_time_parsed;\n })\n .attr('transform', d => transform(d, activeImageId))\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit().remove();\n\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n service.setStyles(context, null);\n\n function viewfieldPath() {\n if (this.parentNode.__data__.isPano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n\n }\n\n function drawImages(selection) {\n const enabled = svgMapilioImages.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-mapilio')\n .data(service ? [0] : []);\n\n layer.exit().remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-mapilio')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter.merge(layer);\n\n if (enabled) {\n let zoom = ~~context.map().zoom();\n if (service) {\n if (zoom >= imageMinZoom) {\n editOn();\n update();\n service.loadImages(projection);\n service.loadLines(projection, zoom);\n } else if (zoom >= lineMinZoom) {\n editOn();\n update();\n service.loadImages(projection);\n service.loadLines(projection, zoom);\n } else {\n editOff();\n dispatch.call('photoDatesChanged', this, 'mapilio', []);\n service.selectImage(context, null);\n }\n } else {\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'mapilio', []);\n }\n }\n\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgMapilioImages.enabled;\n svgMapilioImages.enabled = _;\n if (svgMapilioImages.enabled) {\n showLayer();\n context.photos().on('change.mapilio_images', update);\n } else {\n hideLayer();\n context.photos().on('change.mapilio_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n drawImages.rendered = function(zoom) {\n return zoom >= lineMinZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { services } from '../services';\nimport {svgPath, svgPointTransform} from './helpers';\n\n\nexport function svgPanoramaxImages(projection, context, dispatch) {\n const throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n const imageMinZoom = 15;\n const lineMinZoom = 10;\n const viewFieldZoomLevel = 18;\n let layer = d3_select(null);\n let _panoramax;\n let _viewerYaw = 0;\n let _activeUsernameFilter;\n let _activeIds;\n\n function init() {\n if (svgPanoramaxImages.initialized) return;\n svgPanoramaxImages.enabled = false;\n svgPanoramaxImages.initialized = true;\n }\n\n function getService() {\n if (services.panoramax && !_panoramax) {\n _panoramax = services.panoramax;\n _panoramax.event\n .on('viewerChanged', viewerChanged)\n .on('loadedLines', throttledRedraw)\n .on('loadedImages', throttledRedraw);\n } else if (!services.panoramax && _panoramax) {\n _panoramax = null;\n }\n\n return _panoramax;\n }\n\n /**\n * Filters the images given the filters on the right panel\n * @param {*} images\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered images\n */\n async function filterImages(images, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n const username = context.photos().usernames();\n\n const service = getService();\n\n if (!showsPano || !showsFlat) {\n images = images.filter(function(image) {\n if (image.isPano) return showsPano;\n return showsFlat;\n });\n }\n if (fromDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() >= new Date(fromDate).getTime();\n });\n }\n if (toDate && !skipDateFilter) {\n images = images.filter(function(image) {\n return new Date(image.capture_time).getTime() <= new Date(toDate).getTime();\n });\n }\n if (username && service) {\n if (_activeUsernameFilter !== username) {\n _activeUsernameFilter = username;\n\n const tempIds = await service.getUserIds(username);\n\n _activeIds = {};\n tempIds.forEach(id => {\n _activeIds[id] = true;\n });\n }\n\n images = images.filter(function(image) {\n return _activeIds[image.account_id];\n });\n }\n\n return images;\n }\n\n /**\n * Filters the sequences given the filters on the right panel\n * @param {*} sequences\n * @param {Boolean} skipDateFilter if true, the set date filters will be ignored\n * @returns array of filtered sequences\n */\n async function filterSequences(sequences, skipDateFilter = false) {\n const showsPano = context.photos().showsPanoramic();\n const showsFlat = context.photos().showsFlat();\n const fromDate = context.photos().fromDate();\n const toDate = context.photos().toDate();\n const username = context.photos().usernames();\n\n const service = getService();\n\n if (!showsPano || !showsFlat) {\n sequences = sequences.filter(function(sequence) {\n if (sequence.properties.type === 'equirectangular') return showsPano;\n return showsFlat;\n });\n }\n if (fromDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.date).getTime() >= new Date(fromDate).getTime().toString();\n });\n }\n if (toDate && !skipDateFilter) {\n sequences = sequences.filter(function(sequence) {\n return new Date(sequence.properties.date).getTime() <= new Date(toDate).getTime().toString();\n });\n }\n if (username && service) {\n if (_activeUsernameFilter !== username) {\n _activeUsernameFilter = username;\n\n const tempIds = await service.getUserIds(username);\n\n _activeIds = {};\n tempIds.forEach(id => {\n _activeIds[id] = true;\n });\n }\n\n sequences = sequences.filter(function(sequence) {\n return _activeIds[sequence.properties.account_id];\n });\n }\n\n return sequences;\n }\n\n /**\n * Shows the selected layer\n */\n function showLayer() {\n const service = getService();\n if (!service) return;\n\n editOn();\n\n layer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end', function () { dispatch.call('change'); });\n }\n\n /**\n * Hides the selected layer\n */\n function hideLayer() {\n throttledRedraw.cancel();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end', editOff);\n }\n\n /**\n * Updates the viewfinder for the selected image bubble based on the frame's yaw\n * @param {*} d Current Active image Data\n * @param {*} selectedImageId The selected bubble image ID\n */\n function transform(d, selectedImageId) {\n let t = svgPointTransform(projection)(d);\n let rot = d.heading;\n if (d.id === selectedImageId) {\n rot += _viewerYaw;\n }\n if (rot) {\n t += ' rotate(' + Math.floor(rot) + ',0,0)';\n }\n return t;\n }\n\n function editOn() {\n layer.style('display', 'block');\n }\n\n function editOff() {\n layer.selectAll('.viewfield-group').remove();\n layer.style('display', 'none');\n }\n\n /**\n * Updates the current selected image\n * @param {*} image The selected image bubble data\n */\n function click(d3_event, image) {\n const service = getService();\n if (!service) return;\n\n service\n .ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, image.id)\n .showViewer(context);\n });\n\n context.map().centerEase(image.loc);\n }\n\n function mouseover(d3_event, image) {\n const service = getService();\n if (service) service.setStyles(context, image);\n }\n\n function mouseout() {\n const service = getService();\n if (service) service.setStyles(context, null);\n }\n\n /**\n * Updates the current view, rearranging lines and bubbles.\n */\n async function update() {\n const zoom = ~~context.map().zoom();\n const showViewfields = (zoom >= viewFieldZoomLevel);\n\n const service = getService();\n let sequences = (service ? service.sequences(projection, zoom) : []);\n let images = (service && zoom >= imageMinZoom ? service.images(projection) : []);\n dispatch.call('photoDatesChanged', this, 'panoramax', [\n ...(await filterImages(images, true)).map(p => p.capture_time),\n ...(await filterSequences(sequences, true)).map(s => s.properties.date)]);\n\n images = await filterImages(images);\n sequences = await filterSequences(sequences);\n\n let traces = layer.selectAll('.sequences').selectAll('.sequence')\n .data(sequences, function(d) { return d.properties.id; });\n\n // exit\n traces.exit()\n .remove();\n\n traces.enter()\n .append('path')\n .attr('class', 'sequence')\n .merge(traces)\n .attr('d', svgPath(projection).geojson);\n\n\n const groups = layer.selectAll('.markers').selectAll('.viewfield-group')\n .data(images, function(d) { return d.id; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n const groupsEnter = groups.enter()\n .append('g')\n .attr('class', 'viewfield-group')\n .on('mouseenter', mouseover)\n .on('mouseleave', mouseout)\n .on('click', click);\n\n groupsEnter\n .append('g')\n .attr('class', 'viewfield-scale');\n\n const activeImageId = service.getActiveImage()?.id;\n // update\n const markers = groups\n .merge(groupsEnter)\n .sort(function(a, b) {\n // active image on top\n if (a.id === activeImageId) return 1;\n if (b.id === activeImageId) return -1;\n // else: sort by capture time (newest on top)\n return a.capture_time_parsed - b.capture_time_parsed;\n })\n .attr('transform', d => transform(d, activeImageId))\n .select('.viewfield-scale');\n\n\n markers.selectAll('circle')\n .data([0])\n .enter()\n .append('circle')\n .attr('dx', '0')\n .attr('dy', '0')\n .attr('r', '6');\n\n const viewfields = markers.selectAll('.viewfield')\n .data(showViewfields ? [0] : []);\n\n viewfields.exit()\n .remove();\n\n viewfields.enter()\n .insert('path', 'circle')\n .attr('class', 'viewfield')\n .attr('transform', 'scale(1.5,1.5),translate(-8, -13)')\n .attr('d', viewfieldPath);\n\n service.setStyles(context, null);\n\n function viewfieldPath() {\n if (this.parentNode.__data__.isPano) {\n return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0';\n } else {\n return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z';\n }\n }\n }\n\n function viewerChanged() {\n const service = getService();\n if (!service) return;\n\n const frame = service.photoFrame();\n if (!frame) return;\n\n // update viewfield rotation\n _viewerYaw = frame.getYaw();\n\n // avoid updating if the map is currently transformed\n // e.g. during drags or easing.\n if (context.map().isTransformed()) return;\n\n layer.selectAll('.viewfield-group.currentView')\n .attr('transform', d => transform(d, d.id));\n }\n\n\n /**\n * Draws bubbles and lines on the current view\n * @param {*} selection Current HTML Selection\n */\n function drawImages(selection) {\n\n const enabled = svgPanoramaxImages.enabled;\n const service = getService();\n\n layer = selection.selectAll('.layer-panoramax')\n .data(service ? [0] : []);\n\n layer.exit()\n .remove();\n\n const layerEnter = layer.enter()\n .append('g')\n .attr('class', 'layer-panoramax')\n .style('display', enabled ? 'block' : 'none');\n\n layerEnter\n .append('g')\n .attr('class', 'sequences');\n\n layerEnter\n .append('g')\n .attr('class', 'markers');\n\n layer = layerEnter\n .merge(layer);\n\n if (enabled) {\n let zoom = ~~context.map().zoom();\n if (service){\n if (zoom >= imageMinZoom) {\n editOn();\n update();\n service.loadImages(projection);\n } else if (zoom >= lineMinZoom) {\n editOn();\n update();\n service.loadLines(projection, zoom);\n } else {\n editOff();\n dispatch.call('photoDatesChanged', this, 'panoramax', []);\n }\n } else {\n editOff();\n }\n } else {\n dispatch.call('photoDatesChanged', this, 'panoramax', []);\n }\n }\n\n /**\n * @returns if layer is active\n */\n drawImages.enabled = function(_) {\n if (!arguments.length) return svgPanoramaxImages.enabled;\n svgPanoramaxImages.enabled = _;\n if (svgPanoramaxImages.enabled) {\n showLayer();\n context.photos().on('change.panoramax_images', update);\n } else {\n hideLayer();\n context.photos().on('change.panoramax_images', null);\n }\n dispatch.call('change');\n return this;\n };\n\n\n drawImages.supported = function() {\n return !!getService();\n };\n\n /**\n * @returns if layer is drawn\n */\n drawImages.rendered = function(zoom) {\n return zoom >= lineMinZoom;\n };\n\n\n init();\n return drawImages;\n}\n", "export function svgOsm(projection, context, dispatch) {\n var enabled = true;\n\n\n function drawOsm(selection) {\n selection.selectAll('.layer-osm')\n .data(['covered', 'areas', 'lines', 'points', 'auxiliary', 'labels'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'layer-osm ' + d; });\n\n selection.selectAll('.layer-osm.points').selectAll('.points-group')\n .data(['vertices', 'midpoints', 'points', 'turns'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'points-group ' + d; });\n }\n\n\n function showLayer() {\n var layer = context.surface().selectAll('.data-layer.osm');\n layer.interrupt();\n\n layer\n .classed('disabled', false)\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end interrupt', function () {\n dispatch.call('change');\n });\n }\n\n\n function hideLayer() {\n var layer = context.surface().selectAll('.data-layer.osm');\n layer.interrupt();\n\n layer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end interrupt', function () {\n layer.classed('disabled', true);\n dispatch.call('change');\n });\n }\n\n\n drawOsm.enabled = function(val) {\n if (!arguments.length) return enabled;\n enabled = val;\n\n if (enabled) {\n showLayer();\n } else {\n hideLayer();\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n return drawOsm;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { modeBrowse } from '../modes/browse';\nimport { svgPointTransform } from './helpers';\nimport { services } from '../services';\nimport { utilStringQs } from '../util';\n\nvar hash = utilStringQs(window.location.hash);\n\nvar _notesEnabled = !!hash.notes;\nvar _osmService;\n\n\nexport function svgNotes(projection, context, dispatch) {\n if (!dispatch) { dispatch = d3_dispatch('change'); }\n var throttledRedraw = throttle(function () { dispatch.call('change'); }, 1000);\n var minZoom = 12;\n var touchLayer = d3_select(null);\n var drawLayer = d3_select(null);\n var _notesVisible = false;\n\n\n function markerPath(selection, klass) {\n selection\n .attr('class', klass)\n .attr('transform', 'translate(-8, -22)')\n .attr('d', 'm17.5,0l-15,0c-1.37,0 -2.5,1.12 -2.5,2.5l0,11.25c0,1.37 1.12,2.5 2.5,2.5l3.75,0l0,3.28c0,0.38 0.43,0.6 0.75,0.37l4.87,-3.65l5.62,0c1.37,0 2.5,-1.12 2.5,-2.5l0,-11.25c0,-1.37 -1.12,-2.5 -2.5,-2.5z');\n }\n\n\n // Loosely-coupled osm service for fetching notes.\n function getService() {\n if (services.osm && !_osmService) {\n _osmService = services.osm;\n _osmService.on('loadedNotes', throttledRedraw);\n } else if (!services.osm && _osmService) {\n _osmService = null;\n }\n\n return _osmService;\n }\n\n\n // Show the notes\n function editOn() {\n if (!_notesVisible) {\n _notesVisible = true;\n drawLayer\n .style('display', 'block');\n }\n }\n\n\n // Immediately remove the notes and their touch targets\n function editOff() {\n if (_notesVisible) {\n _notesVisible = false;\n drawLayer\n .style('display', 'none');\n drawLayer.selectAll('.note')\n .remove();\n touchLayer.selectAll('.note')\n .remove();\n }\n }\n\n\n // Enable the layer. This shows the notes and transitions them to visible.\n function layerOn() {\n editOn();\n\n drawLayer\n .style('opacity', 0)\n .transition()\n .duration(250)\n .style('opacity', 1)\n .on('end interrupt', function () {\n dispatch.call('change');\n });\n }\n\n\n // Disable the layer. This transitions the layer invisible and then hides the notes.\n function layerOff() {\n throttledRedraw.cancel();\n drawLayer.interrupt();\n touchLayer.selectAll('.note')\n .remove();\n\n drawLayer\n .transition()\n .duration(250)\n .style('opacity', 0)\n .on('end interrupt', function () {\n editOff();\n dispatch.call('change');\n });\n }\n\n\n // Update the note markers\n function updateMarkers() {\n if (!_notesVisible || !_notesEnabled) return;\n\n var service = getService();\n var selectedID = context.selectedNoteID();\n var data = (service ? service.notes(projection) : []);\n var getTransform = svgPointTransform(projection);\n\n // Draw markers..\n var notes = drawLayer.selectAll('.note')\n .data(data, function(d) { return d.status + d.id; });\n\n // exit\n notes.exit()\n .remove();\n\n // enter\n var notesEnter = notes.enter()\n .append('g')\n .attr('class', function(d) { return 'note note-' + d.id + ' ' + d.status; })\n .classed('new', function(d) { return d.id < 0; });\n\n notesEnter\n .append('ellipse')\n .attr('cx', 0.5)\n .attr('cy', 1)\n .attr('rx', 6.5)\n .attr('ry', 3)\n .attr('class', 'stroke');\n\n notesEnter\n .append('path')\n .call(markerPath, 'shadow');\n\n notesEnter\n .append('use')\n .attr('class', 'note-fill')\n .attr('width', '20px')\n .attr('height', '20px')\n .attr('x', '-8px')\n .attr('y', '-22px')\n .attr('xlink:href', '#iD-icon-note');\n\n notesEnter.selectAll('.icon-annotation')\n .data(function(d) { return [d]; })\n .enter()\n .append('use')\n .attr('class', 'icon-annotation')\n .attr('width', '10px')\n .attr('height', '10px')\n .attr('x', '-3px')\n .attr('y', '-19px')\n .attr('xlink:href', function(d) {\n if (d.id < 0) return '#iD-icon-plus';\n if (d.status === 'open') return '#iD-icon-close';\n return '#iD-icon-apply';\n });\n\n // update\n notes\n .merge(notesEnter)\n .sort(sortY)\n .classed('selected', function(d) {\n var mode = context.mode();\n var isMoving = mode && mode.id === 'drag-note'; // no shadows when dragging\n return !isMoving && d.id === selectedID;\n })\n .attr('transform', getTransform);\n\n\n // Draw targets..\n if (touchLayer.empty()) return;\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n\n var targets = touchLayer.selectAll('.note')\n .data(data, function(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('rect')\n .attr('width', '20px')\n .attr('height', '20px')\n .attr('x', '-8px')\n .attr('y', '-22px')\n .merge(targets)\n .sort(sortY)\n .attr('class', function(d) {\n var newClass = (d.id < 0 ? 'new' : '');\n return 'note target note-' + d.id + ' ' + fillClass + newClass;\n })\n .attr('transform', getTransform);\n\n\n function sortY(a, b) {\n if (a.id === selectedID) return 1;\n if (b.id === selectedID) return -1;\n return b.loc[1] - a.loc[1];\n }\n }\n\n\n // Draw the notes layer and schedule loading notes and updating markers.\n function drawNotes(selection) {\n var service = getService();\n\n var surface = context.surface();\n if (surface && !surface.empty()) {\n touchLayer = surface.selectAll('.data-layer.touch .layer-touch.markers');\n }\n\n drawLayer = selection.selectAll('.layer-notes')\n .data(service ? [0] : []);\n\n drawLayer.exit()\n .remove();\n\n drawLayer = drawLayer.enter()\n .append('g')\n .attr('class', 'layer-notes')\n .style('display', _notesEnabled ? 'block' : 'none')\n .merge(drawLayer);\n\n if (_notesEnabled) {\n if (service && ~~context.map().zoom() >= minZoom) {\n editOn();\n service.loadNotes(projection);\n updateMarkers();\n } else {\n editOff();\n }\n }\n }\n\n\n // Toggles the layer on and off\n drawNotes.enabled = function(val) {\n if (!arguments.length) return _notesEnabled;\n\n _notesEnabled = val;\n if (_notesEnabled) {\n layerOn();\n } else {\n layerOff();\n if (context.selectedNoteID()) {\n context.enter(modeBrowse(context));\n }\n }\n\n dispatch.call('change');\n return this;\n };\n\n\n return drawNotes;\n}\n", "export function svgTouch() {\n\n function drawTouch(selection) {\n selection.selectAll('.layer-touch')\n .data(['areas', 'lines', 'points', 'turns', 'markers'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'layer-touch ' + d; });\n }\n\n return drawTouch;\n}\n", "function refresh(selection, node) {\n var cr = node.getBoundingClientRect();\n var prop = [cr.width, cr.height];\n selection.property('__dimensions__', prop);\n return prop;\n}\n\nexport function utilGetDimensions(selection, force) {\n if (!selection || selection.empty()) {\n return [0, 0];\n }\n var node = selection.node(),\n cached = selection.property('__dimensions__');\n return (!cached || force) ? refresh(selection, node) : cached;\n}\n\n\nexport function utilSetDimensions(selection, dimensions) {\n if (!selection || selection.empty()) {\n return selection;\n }\n var node = selection.node();\n if (dimensions === null) {\n refresh(selection, node);\n return selection;\n }\n return selection\n .property('__dimensions__', [dimensions[0], dimensions[1]])\n .attr('width', dimensions[0])\n .attr('height', dimensions[1]);\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { svgData } from './data';\nimport { svgLocalPhotos} from './local_photos';\nimport { svgDebug } from './debug';\nimport { svgGeolocate } from './geolocate';\nimport { svgOsmose } from './osmose';\nimport { svgStreetside } from './streetside';\nimport { svgVegbilder} from './vegbilder';\nimport { svgMapillaryImages } from './mapillary_images';\nimport { svgMapillaryPosition } from './mapillary_position';\nimport { svgMapillarySigns } from './mapillary_signs';\nimport { svgMapillaryMapFeatures } from './mapillary_map_features';\nimport { svgKartaviewImages } from './kartaview_images';\nimport { svgMapilioImages } from './mapilio_images';\nimport { svgPanoramaxImages } from './panoramax_images';\nimport { svgOsm } from './osm';\nimport { svgNotes } from './notes';\nimport { svgTouch } from './touch';\nimport { utilArrayDifference, utilRebind } from '../util';\nimport { utilGetDimensions, utilSetDimensions } from '../util/dimensions';\n\n\nexport function svgLayers(projection, context) {\n var dispatch = d3_dispatch('change', 'photoDatesChanged');\n var svg = d3_select(null);\n var _layers = [\n { id: 'osm', layer: svgOsm(projection, context, dispatch) },\n { id: 'notes', layer: svgNotes(projection, context, dispatch) },\n { id: 'data', layer: svgData(projection, context, dispatch) },\n { id: 'osmose', layer: svgOsmose(projection, context, dispatch) },\n { id: 'streetside', layer: svgStreetside(projection, context, dispatch)},\n { id: 'mapillary', layer: svgMapillaryImages(projection, context, dispatch) },\n { id: 'mapillary-position', layer: svgMapillaryPosition(projection, context, dispatch) },\n { id: 'mapillary-map-features', layer: svgMapillaryMapFeatures(projection, context, dispatch) },\n { id: 'mapillary-signs', layer: svgMapillarySigns(projection, context, dispatch) },\n { id: 'kartaview', layer: svgKartaviewImages(projection, context, dispatch) },\n { id: 'mapilio', layer: svgMapilioImages(projection, context, dispatch) },\n { id: 'vegbilder', layer: svgVegbilder(projection, context, dispatch) },\n { id: 'panoramax', layer: svgPanoramaxImages(projection, context, dispatch) },\n { id: 'local-photos', layer: svgLocalPhotos(projection, context, dispatch) },\n { id: 'debug', layer: svgDebug(projection, context, dispatch) },\n { id: 'geolocate', layer: svgGeolocate(projection, context, dispatch) },\n { id: 'touch', layer: svgTouch(projection, context, dispatch) },\n ];\n\n\n function drawLayers(selection) {\n svg = selection.selectAll('.surface')\n .data([0]);\n\n svg = svg.enter()\n .append('svg')\n .attr('class', 'surface')\n .merge(svg);\n\n var defs = svg.selectAll('.surface-defs')\n .data([0]);\n\n defs.enter()\n .append('defs')\n .attr('class', 'surface-defs');\n\n var groups = svg.selectAll('.data-layer')\n .data(_layers);\n\n groups.exit()\n .remove();\n\n groups.enter()\n .append('g')\n .attr('class', function(d) { return 'data-layer ' + d.id; })\n .merge(groups)\n .each(function(d) { d3_select(this).call(d.layer); });\n }\n\n\n drawLayers.all = function() {\n return _layers;\n };\n\n\n drawLayers.layer = function(id) {\n var obj = _layers.find(function(o) { return o.id === id; });\n return obj && obj.layer;\n };\n\n\n drawLayers.only = function(what) {\n var arr = [].concat(what);\n var all = _layers.map(function(layer) { return layer.id; });\n return drawLayers.remove(utilArrayDifference(all, arr));\n };\n\n\n drawLayers.remove = function(what) {\n var arr = [].concat(what);\n arr.forEach(function(id) {\n _layers = _layers.filter(function(o) { return o.id !== id; });\n });\n dispatch.call('change');\n return this;\n };\n\n\n drawLayers.add = function(what) {\n var arr = [].concat(what);\n arr.forEach(function(obj) {\n if ('id' in obj && 'layer' in obj) {\n _layers.push(obj);\n }\n });\n dispatch.call('change');\n return this;\n };\n\n\n drawLayers.dimensions = function(val) {\n if (!arguments.length) return utilGetDimensions(svg);\n utilSetDimensions(svg, val);\n return this;\n };\n\n\n return utilRebind(drawLayers, dispatch, 'on');\n}\n", "import { deepEqual } from 'fast-equals';\nimport { range as d3_range } from 'd3-array';\n\nimport {\n svgMarkerSegments, svgPath, svgRelationMemberTags, svgSegmentWay\n} from './helpers';\nimport { svgTagClasses } from './tag_classes';\n\nimport { osmEntity } from '../osm';\nimport { utilArrayFlatten, utilArrayGroupBy } from '../util';\nimport { utilDetect } from '../util/detect';\n\n/** @param {{ [key: string ]: string }} tags */\nfunction onewayArrowColour(tags) {\n // the return value must be defined in ./defs.js\n if (tags.highway === 'construction' && tags.bridge) return 'white';\n if (tags.highway === 'pedestrian') return 'gray';\n if (tags.railway && !tags.highway) return 'gray';\n if (tags.aeroway === 'runway') return 'white';\n\n return 'black';\n}\n\nexport function svgLines(projection, context) {\n var detected = utilDetect();\n\n var highway_stack = {\n motorway: 0,\n motorway_link: 1,\n trunk: 2,\n trunk_link: 3,\n primary: 4,\n primary_link: 5,\n secondary: 6,\n tertiary: 7,\n unclassified: 8,\n residential: 9,\n service: 10,\n busway: 11,\n footway: 12\n };\n\n\n function drawTargets(selection, graph, entities, filter) {\n var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';\n var getPath = svgPath(projection).geojson;\n var activeID = context.activeID();\n var base = context.history().base();\n\n // The targets and nopes will be MultiLineString sub-segments of the ways\n var data = { targets: [], nopes: [] };\n\n entities.forEach(function(way) {\n var features = svgSegmentWay(way, graph, activeID);\n data.targets.push.apply(data.targets, features.passive);\n data.nopes.push.apply(data.nopes, features.active);\n });\n\n\n // Targets allow hover and vertex snapping\n var targetData = data.targets.filter(getPath);\n var targets = selection.selectAll('.line.target-allowed')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(targetData, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n var segmentWasEdited = function(d) {\n var wayID = d.properties.entity.id;\n // if the whole line was edited, don't draw segment changes\n if (!base.entities[wayID] ||\n !deepEqual(graph.entities[wayID].nodes, base.entities[wayID].nodes)) {\n return false;\n }\n return d.properties.nodes.some(function(n) {\n return !base.entities[n.id] ||\n !deepEqual(graph.entities[n.id].loc, base.entities[n.id].loc);\n });\n };\n\n // enter/update\n targets.enter()\n .append('path')\n .merge(targets)\n .attr('d', getPath)\n .attr('class', function(d) {\n return 'way line target target-allowed ' + targetClass + d.id;\n })\n .classed('segment-edited', segmentWasEdited);\n\n // NOPE\n var nopeData = data.nopes.filter(getPath);\n var nopes = selection.selectAll('.line.target-nope')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(nopeData, function key(d) { return d.id; });\n\n // exit\n nopes.exit()\n .remove();\n\n // enter/update\n nopes.enter()\n .append('path')\n .merge(nopes)\n .attr('d', getPath)\n .attr('class', function(d) {\n return 'way line target target-nope ' + nopeClass + d.id;\n })\n .classed('segment-edited', segmentWasEdited);\n }\n\n\n function drawLines(selection, graph, entities, filter) {\n var base = context.history().base();\n\n function waystack(a, b) {\n var selected = context.selectedIDs();\n var scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0;\n var scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0;\n\n if (a.tags.highway) { scoreA -= highway_stack[a.tags.highway]; }\n if (b.tags.highway) { scoreB -= highway_stack[b.tags.highway]; }\n return scoreA - scoreB;\n }\n\n\n function drawLineGroup(selection, klass, isSelected) {\n // Note: Don't add `.selected` class in draw modes\n var mode = context.mode();\n var isDrawing = mode && /^draw/.test(mode.id);\n var selectedClass = (!isDrawing && isSelected) ? 'selected ' : '';\n\n var lines = selection\n .selectAll('path')\n .filter(filter)\n .data(getPathData(isSelected), osmEntity.key);\n\n lines.exit()\n .remove();\n\n // Optimization: Call expensive TagClasses only on enter selection. This\n // works because osmEntity.key is defined to include the entity v attribute.\n lines.enter()\n .append('path')\n .attr('class', function(d) {\n\n var prefix = 'way line';\n\n // if this line isn't styled by its own tags\n if (!d.hasInterestingTags()) {\n\n var parentRelations = graph.parentRelations(d);\n var parentMultipolygons = parentRelations.filter(function(relation) {\n return relation.isMultipolygon();\n });\n\n // and if it's a member of at least one multipolygon relation\n if (parentMultipolygons.length > 0 &&\n // and only multipolygon relations\n parentRelations.length === parentMultipolygons.length) {\n // then fudge the classes to style this as an area edge\n prefix = 'relation area';\n }\n }\n\n var oldMPClass = oldMultiPolygonOuters[d.id] ? 'old-multipolygon ' : '';\n return prefix + ' ' + klass + ' ' + selectedClass + oldMPClass + d.id;\n })\n .classed('added', function(d) {\n return !base.entities[d.id];\n })\n .classed('geometry-edited', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].nodes, base.entities[d.id].nodes);\n })\n .classed('retagged', function(d) {\n return graph.entities[d.id] &&\n base.entities[d.id] &&\n !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses())\n .merge(lines)\n .sort(waystack)\n .attr('d', getPath)\n .call(svgTagClasses().tags(svgRelationMemberTags(graph)));\n\n return selection;\n }\n\n\n function getPathData(isSelected) {\n return function() {\n var layer = this.parentNode.__data__;\n var data = pathdata[layer] || [];\n return data.filter(function(d) {\n if (isSelected) {\n return context.selectedIDs().indexOf(d.id) !== -1;\n } else {\n return context.selectedIDs().indexOf(d.id) === -1;\n }\n });\n };\n }\n\n function addMarkers(layergroup, pathclass, groupclass, groupdata, marker) {\n var markergroup = layergroup\n .selectAll('g.' + groupclass)\n .data([pathclass]);\n\n markergroup = markergroup.enter()\n .append('g')\n .attr('class', groupclass)\n .merge(markergroup);\n\n var markers = markergroup\n .selectAll('path')\n .filter(filter)\n .data(\n function data() { return groupdata[this.parentNode.__data__] || []; },\n function key(d) { return [d.id, d.index]; }\n );\n\n markers.exit()\n .remove();\n\n markers = markers.enter()\n .append('path')\n .attr('class', pathclass)\n .merge(markers)\n .attr('marker-mid', marker)\n .attr('d', function(d) { return d.d; });\n\n if (detected.ie) {\n markers.each(function() { this.parentNode.insertBefore(this, this); });\n }\n }\n\n\n var getPath = svgPath(projection, graph);\n var ways = [];\n var onewaydata = {};\n var sideddata = {};\n var oldMultiPolygonOuters = {};\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n if (entity.geometry(graph) === 'line'\n // to render side-markers for coastlines (see\n // https://github.com/openstreetmap/iD/issues/9293)\n || entity.geometry(graph) === 'area' && entity.sidednessIdentifier\n && entity.sidednessIdentifier() === 'coastline') {\n ways.push(entity);\n }\n }\n\n ways = ways.filter(getPath);\n const pathdata = utilArrayGroupBy(ways, (way) => Math.trunc(way.layer()));\n\n Object.keys(pathdata).forEach(function(k) {\n var v = pathdata[k];\n var onewayArr = v.filter(function(d) { return d.isOneWay(); });\n var onewaySegments = svgMarkerSegments(\n projection, graph, 36,\n entity => entity.isOneWayBackwards(),\n entity => entity.isBiDirectional(),\n );\n onewaydata[k] = utilArrayFlatten(onewayArr.map(onewaySegments));\n\n var sidedArr = v.filter(function(d) { return d.isSided(); });\n var sidedSegments = svgMarkerSegments(\n projection, graph, 30\n );\n sideddata[k] = utilArrayFlatten(sidedArr.map(sidedSegments));\n });\n\n\n var covered = selection.selectAll('.layer-osm.covered'); // under areas\n var uncovered = selection.selectAll('.layer-osm.lines'); // over areas\n var touchLayer = selection.selectAll('.layer-touch.lines');\n\n // Draw lines..\n [covered, uncovered].forEach(function(selection) {\n var range = (selection === covered ? d3_range(-10,0) : d3_range(0,11));\n var layergroup = selection\n .selectAll('g.layergroup')\n .data(range);\n\n layergroup = layergroup.enter()\n .append('g')\n .attr('class', function(d) { return 'layergroup layer' + String(d); })\n .merge(layergroup);\n\n layergroup\n .selectAll('g.linegroup')\n .data(['shadow', 'casing', 'stroke', 'shadow-highlighted', 'casing-highlighted', 'stroke-highlighted'])\n .enter()\n .append('g')\n .attr('class', function(d) { return 'linegroup line-' + d; });\n\n layergroup.selectAll('g.line-shadow')\n .call(drawLineGroup, 'shadow', false);\n layergroup.selectAll('g.line-casing')\n .call(drawLineGroup, 'casing', false);\n layergroup.selectAll('g.line-stroke')\n .call(drawLineGroup, 'stroke', false);\n\n layergroup.selectAll('g.line-shadow-highlighted')\n .call(drawLineGroup, 'shadow', true);\n layergroup.selectAll('g.line-casing-highlighted')\n .call(drawLineGroup, 'casing', true);\n layergroup.selectAll('g.line-stroke-highlighted')\n .call(drawLineGroup, 'stroke', true);\n\n addMarkers(layergroup, 'oneway', 'onewaygroup', onewaydata, (d) => {\n const category = onewayArrowColour(graph.entity(d.id).tags);\n return `url(#ideditor-oneway-marker-${category})`;\n });\n addMarkers(layergroup, 'sided', 'sidedgroup', sideddata,\n function marker(d) {\n var category = graph.entity(d.id).sidednessIdentifier();\n return 'url(#ideditor-sided-marker-' + category + ')';\n }\n );\n });\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, ways, filter);\n }\n\n\n return drawLines;\n}\n", "import { svgPointTransform } from './helpers';\nimport { svgTagClasses } from './tag_classes';\nimport { geoAngle, geoLineIntersection, geoVecInterp, geoVecLength } from '../geo';\n\n\nexport function svgMidpoints(projection, context) {\n var targetRadius = 8;\n\n function drawTargets(selection, graph, entities, filter) {\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var getTransform = svgPointTransform(projection).geojson;\n\n var data = entities.map(function(midpoint) {\n return {\n type: 'Feature',\n id: midpoint.id,\n properties: {\n target: true,\n entity: midpoint\n },\n geometry: {\n type: 'Point',\n coordinates: midpoint.loc\n }\n };\n });\n\n var targets = selection.selectAll('.midpoint.target')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(data, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('circle')\n .attr('r', targetRadius)\n .merge(targets)\n .attr('class', function(d) { return 'node midpoint target ' + fillClass + d.id; })\n .attr('transform', getTransform);\n }\n\n\n function drawMidpoints(selection, graph, entities, filter, extent) {\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.midpoints');\n var touchLayer = selection.selectAll('.layer-touch.points');\n\n var mode = context.mode();\n if ((mode && mode.id !== 'select') || !context.map().withinEditableZoom()) {\n drawLayer.selectAll('.midpoint').remove();\n touchLayer.selectAll('.midpoint.target').remove();\n return;\n }\n\n var poly = extent.polygon();\n var midpoints = {};\n\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n\n if (entity.type !== 'way') continue;\n if (!filter(entity)) continue;\n if (context.selectedIDs().indexOf(entity.id) < 0) continue;\n\n var nodes = graph.childNodes(entity);\n for (var j = 0; j < nodes.length - 1; j++) {\n var a = nodes[j];\n var b = nodes[j + 1];\n var id = [a.id, b.id].sort().join('-');\n\n if (midpoints[id]) {\n midpoints[id].parents.push(entity);\n } else if (geoVecLength(projection(a.loc), projection(b.loc)) > 40) {\n var point = geoVecInterp(a.loc, b.loc, 0.5);\n var loc = null;\n\n if (extent.intersects(point)) {\n loc = point;\n } else {\n for (var k = 0; k < 4; k++) {\n point = geoLineIntersection([a.loc, b.loc], [poly[k], poly[k + 1]]);\n if (point &&\n geoVecLength(projection(a.loc), projection(point)) > 20 &&\n geoVecLength(projection(b.loc), projection(point)) > 20) {\n loc = point;\n break;\n }\n }\n }\n\n if (loc) {\n midpoints[id] = {\n type: 'midpoint',\n id: id,\n loc: loc,\n edge: [a.id, b.id],\n parents: [entity]\n };\n }\n }\n }\n }\n\n\n function midpointFilter(d) {\n if (midpoints[d.id]) return true;\n\n for (var i = 0; i < d.parents.length; i++) {\n if (filter(d.parents[i])) {\n return true;\n }\n }\n\n return false;\n }\n\n\n var groups = drawLayer.selectAll('.midpoint')\n .filter(midpointFilter)\n .data(Object.values(midpoints), function(d) { return d.id; });\n\n groups.exit()\n .remove();\n\n var enter = groups.enter()\n .insert('g', ':first-child')\n .attr('class', 'midpoint');\n\n enter\n .append('polygon')\n .attr('points', '-6,8 10,0 -6,-8')\n .attr('class', 'shadow');\n\n enter\n .append('polygon')\n .attr('points', '-3,4 5,0 -3,-4')\n .attr('class', 'fill');\n\n groups = groups\n .merge(enter)\n .attr('transform', function(d) {\n var translate = svgPointTransform(projection);\n var a = graph.entity(d.edge[0]);\n var b = graph.entity(d.edge[1]);\n var angle = geoAngle(a, b, projection) * (180 / Math.PI);\n return translate(d) + ' rotate(' + angle + ')';\n })\n .call(svgTagClasses().tags(\n function(d) { return d.parents[0].tags; }\n ));\n\n // Propagate data bindings.\n groups.select('polygon.shadow');\n groups.select('polygon.fill');\n\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, Object.values(midpoints), midpointFilter);\n }\n\n return drawMidpoints;\n}\n", "import { deepEqual } from 'fast-equals';\nimport { clamp } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3';\n\nimport { geoScaleToZoom } from '../geo';\nimport { osmEntity } from '../osm';\nimport { svgPointTransform } from './helpers';\nimport { svgTagClasses } from './tag_classes';\nimport { presetManager } from '../presets';\nimport { textWidth, isAddressPoint } from './labels';\n\nexport function svgPoints(projection, context) {\n\n function markerPath(selection, klass) {\n selection\n .attr('class', klass)\n .attr('transform', d => isAddressPoint(d.tags)\n ? `translate(-${addressShieldWidth(d, selection)/2}, -8)`\n : 'translate(-8, -23)')\n .attr('d', d => {\n if (!isAddressPoint(d.tags)) {\n return 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z';\n }\n const w = addressShieldWidth(d, selection);\n return `M ${w-3.5} 0 a 3.5 3.5 0 0 0 3.5 3.5 l 0 9 a 3.5 3.5 0 0 0 -3.5 3.5 l -${w-7} 0 a 3.5 3.5 0 0 0 -3.5 -3.5 l 0 -9 a 3.5 3.5 0 0 0 3.5 -3.5 z`;\n });\n }\n\n function sortY(a, b) {\n return b.loc[1] - a.loc[1];\n }\n\n function addressShieldWidth(d, selection) {\n const width = textWidth(d.tags['addr:housenumber'] || d.tags['addr:housename'] || '', 10, selection.node().parentElement);\n return clamp(width, 10, 34) + 8;\n };\n\n // Avoid exit/enter if we're just moving stuff around.\n // The node will get a new version but we only need to run the update selection.\n function fastEntityKey(d) {\n const mode = context.mode();\n const isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);\n return isMoving ? d.id : osmEntity.key(d);\n }\n\n\n function drawTargets(selection, graph, entities, filter) {\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var getTransform = svgPointTransform(projection).geojson;\n var activeID = context.activeID();\n var data = [];\n\n entities.forEach(function(node) {\n if (activeID === node.id) return; // draw no target on the activeID\n\n data.push({\n type: 'Feature',\n id: node.id,\n properties: {\n target: true,\n entity: node,\n isAddr: isAddressPoint(node.tags)\n },\n geometry: node.asGeoJSON()\n });\n });\n\n var targets = selection.selectAll('.point.target')\n .filter(d => filter(d.properties.entity))\n .data(data, d => fastEntityKey(d.properties.entity));\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('rect')\n .attr('x', d => d.properties.isAddr ? -addressShieldWidth(d.properties.entity, selection) / 2 : -10)\n .attr('y', d => d.properties.isAddr ? -8 : -26)\n .attr('width', d => d.properties.isAddr ? addressShieldWidth(d.properties.entity, selection) : 20)\n .attr('height', d => d.properties.isAddr ? 16 : 30)\n .attr('class', function(d) { return 'node point target ' + fillClass + d.id; })\n .merge(targets)\n .attr('transform', getTransform);\n }\n\n\n function drawPoints(selection, graph, entities, filter) {\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n var base = context.history().base();\n\n // Points with a direction will render as vertices at higher zooms..\n function renderAsPoint(entity) {\n return entity.geometry(graph) === 'point' &&\n !(zoom >= 18 && entity.directions(graph, projection).length);\n }\n\n // All points will render as vertices in wireframe mode too..\n var points = wireframe ? [] : entities.filter(renderAsPoint);\n points.sort(sortY);\n\n\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.points');\n var touchLayer = selection.selectAll('.layer-touch.points');\n\n // Draw points..\n var groups = drawLayer.selectAll('g.point')\n .filter(filter)\n .data(points, fastEntityKey);\n\n groups.exit()\n .remove();\n\n var enter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'node point ' + d.id; })\n .order();\n\n enter\n .append('path')\n .call(markerPath, 'shadow');\n\n enter.each(function(d) {\n if (isAddressPoint(d.tags)) return;\n d3_select(this)\n .append('ellipse')\n .attr('cx', 0.5)\n .attr('cy', 1)\n .attr('rx', 6.5)\n .attr('ry', 3)\n .attr('class', 'stroke');\n });\n\n enter\n .append('path')\n .call(markerPath, 'stroke');\n\n enter\n .append('use')\n .attr('transform', 'translate(-5.5, -20)')\n .attr('class', 'icon')\n .attr('width', '12px')\n .attr('height', '12px');\n\n groups = groups\n .merge(enter)\n .attr('transform', svgPointTransform(projection))\n .classed('added', function(d) {\n return !base.entities[d.id]; // if it doesn't exist in the base graph, it's new\n })\n .classed('moved', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].loc, base.entities[d.id].loc);\n })\n .classed('retagged', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses());\n\n groups.select('.shadow'); // propagate bound data\n groups.select('.stroke'); // propagate bound data\n groups.select('.icon') // propagate bound data\n .attr('xlink:href', function(entity) {\n var preset = presetManager.match(entity, graph);\n var picon = preset && preset.icon;\n return picon ? '#' + picon : '';\n });\n\n\n // Draw touch targets..\n touchLayer\n .call(drawTargets, graph, points, filter);\n }\n\n\n return drawPoints;\n}\n", "import { geoAngle, geoPathLength } from '../geo';\n\n\nexport function svgTurns(projection, context) {\n\n function icon(turn) {\n var u = turn.u ? '-u' : '';\n if (turn.no) return '#iD-turn-no' + u;\n if (turn.only) return '#iD-turn-only' + u;\n return '#iD-turn-yes' + u;\n }\n\n function drawTurns(selection, graph, turns) {\n\n function turnTransform(d) {\n var pxRadius = 50;\n var toWay = graph.entity(d.to.way);\n var toPoints = graph.childNodes(toWay)\n .map(function (n) { return n.loc; })\n .map(projection);\n var toLength = geoPathLength(toPoints);\n var mid = toLength / 2; // midpoint of destination way\n\n var toNode = graph.entity(d.to.node);\n var toVertex = graph.entity(d.to.vertex);\n var a = geoAngle(toVertex, toNode, projection);\n var o = projection(toVertex.loc);\n var r = d.u ? 0 // u-turn: no radius\n : !toWay.__via ? pxRadius // leaf way: put marker at pxRadius\n : Math.min(mid, pxRadius); // via way: prefer pxRadius, fallback to mid for very short ways\n\n return 'translate(' + (r * Math.cos(a) + o[0]) + ',' + (r * Math.sin(a) + o[1]) + ') ' +\n 'rotate(' + a * 180 / Math.PI + ')';\n }\n\n\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.turns');\n var touchLayer = selection.selectAll('.layer-touch.turns');\n\n // Draw turns..\n var groups = drawLayer.selectAll('g.turn')\n .data(turns, function(d) { return d.key; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var groupsEnter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'turn ' + d.key; });\n\n var turnsEnter = groupsEnter\n .filter(function(d) { return !d.u; });\n\n turnsEnter.append('rect')\n .attr('transform', 'translate(-22, -12)')\n .attr('width', '44')\n .attr('height', '24');\n\n turnsEnter.append('use')\n .attr('transform', 'translate(-22, -12)')\n .attr('width', '44')\n .attr('height', '24');\n\n var uEnter = groupsEnter\n .filter(function(d) { return d.u; });\n\n uEnter.append('circle')\n .attr('r', '16');\n\n uEnter.append('use')\n .attr('transform', 'translate(-16, -16)')\n .attr('width', '32')\n .attr('height', '32');\n\n // update\n groups = groups\n .merge(groupsEnter)\n .attr('opacity', function(d) { return d.direct === false ? '0.7' : null; })\n .attr('transform', turnTransform);\n\n groups.select('use')\n .attr('xlink:href', icon);\n\n groups.select('rect'); // propagate bound data\n groups.select('circle'); // propagate bound data\n\n\n // Draw touch targets..\n var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n groups = touchLayer.selectAll('g.turn')\n .data(turns, function(d) { return d.key; });\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n groupsEnter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'turn ' + d.key; });\n\n turnsEnter = groupsEnter\n .filter(function(d) { return !d.u; });\n\n turnsEnter.append('rect')\n .attr('class', 'target ' + fillClass)\n .attr('transform', 'translate(-22, -12)')\n .attr('width', '44')\n .attr('height', '24');\n\n uEnter = groupsEnter\n .filter(function(d) { return d.u; });\n\n uEnter.append('circle')\n .attr('class', 'target ' + fillClass)\n .attr('r', '16');\n\n // update\n groups = groups\n .merge(groupsEnter)\n .attr('transform', turnTransform);\n\n groups.select('rect'); // propagate bound data\n groups.select('circle'); // propagate bound data\n\n\n return this;\n }\n\n return drawTurns;\n}\n", "import { deepEqual } from 'fast-equals';\nimport { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { geoScaleToZoom } from '../geo';\nimport { osmEntity } from '../osm';\nimport { svgPassiveVertex, svgPointTransform } from './helpers';\nimport { svgTagClasses } from './tag_classes';\n\nexport function svgVertices(projection, context) {\n var radiuses = {\n // z16-, z17, z18+, w/icon\n shadow: [6, 7.5, 7.5, 12],\n stroke: [2.5, 3.5, 3.5, 8],\n fill: [1, 1.5, 1.5, 1.5]\n };\n\n var _currHoverTarget;\n var _currPersistent = {};\n var _currHover = {};\n var _prevHover = {};\n var _currSelected = {};\n var _prevSelected = {};\n var _radii = {};\n\n\n function sortY(a, b) {\n return b.loc[1] - a.loc[1];\n }\n\n // Avoid exit/enter if we're just moving stuff around.\n // The node will get a new version but we only need to run the update selection.\n function fastEntityKey(d) {\n var mode = context.mode();\n var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);\n return isMoving ? d.id : osmEntity.key(d);\n }\n\n\n function draw(selection, graph, vertices, sets, filter) {\n sets = sets || { selected: {}, important: {}, hovered: {} };\n\n var icons = {};\n var directions = {};\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n var z = (zoom < 17 ? 0 : zoom < 18 ? 1 : 2);\n var activeID = context.activeID();\n var base = context.history().base();\n\n\n function getIcon(d) {\n // always check latest entity, as fastEntityKey avoids enter/exit now\n var entity = graph.entity(d.id);\n if (entity.id in icons) return icons[entity.id];\n\n icons[entity.id] =\n entity.hasInterestingTags() &&\n presetManager.match(entity, graph).icon;\n\n return icons[entity.id];\n }\n\n\n // memoize directions results, return false for empty arrays (for use in filter)\n function getDirections(entity) {\n if (entity.id in directions) return directions[entity.id];\n\n var angles = entity.directions(graph, projection);\n directions[entity.id] = angles.length ? angles : false;\n return angles;\n }\n\n\n function updateAttributes(selection) {\n ['shadow', 'stroke', 'fill'].forEach(function(klass) {\n var rads = radiuses[klass];\n selection.selectAll('.' + klass)\n .each(function(entity) {\n var i = z && getIcon(entity);\n var r = rads[i ? 3 : z];\n\n // slightly increase the size of unconnected endpoints #3775\n if (entity.id !== activeID && entity.isEndpoint(graph) && !entity.isConnected(graph)) {\n r += 1.5;\n }\n\n if (klass === 'shadow') { // remember this value, so we don't need to\n _radii[entity.id] = r; // recompute it when we draw the touch targets\n }\n\n d3_select(this)\n .attr('r', r)\n .attr('visibility', (i && klass === 'fill') ? 'hidden' : null);\n });\n });\n }\n\n vertices.sort(sortY);\n\n var groups = selection.selectAll('g.vertex')\n .filter(filter)\n .data(vertices, fastEntityKey);\n\n // exit\n groups.exit()\n .remove();\n\n // enter\n var enter = groups.enter()\n .append('g')\n .attr('class', function(d) { return 'node vertex ' + d.id; })\n .order();\n\n enter\n .append('circle')\n .attr('class', 'shadow');\n\n enter\n .append('circle')\n .attr('class', 'stroke');\n\n // Vertices with tags get a fill.\n enter.filter(function(d) { return d.hasInterestingTags(); })\n .append('circle')\n .attr('class', 'fill');\n\n // update\n groups = groups\n .merge(enter)\n .attr('transform', svgPointTransform(projection))\n .classed('sibling', function(d) { return d.id in sets.selected; })\n .classed('shared', function(d) { return graph.isShared(d); })\n .classed('endpoint', function(d) { return d.isEndpoint(graph); })\n .classed('added', function(d) {\n return !base.entities[d.id]; // if it doesn't exist in the base graph, it's new\n })\n .classed('moved', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].loc, base.entities[d.id].loc);\n })\n .classed('retagged', function(d) {\n return base.entities[d.id] && !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);\n })\n .call(svgTagClasses())\n .call(updateAttributes);\n\n // Vertices with icons get a `use`.\n var iconUse = groups\n .selectAll('.icon')\n .data(function data(d) { return zoom >= 17 && getIcon(d) ? [d] : []; }, fastEntityKey);\n\n // exit\n iconUse.exit()\n .remove();\n\n // enter\n iconUse.enter()\n .append('use')\n .attr('class', 'icon')\n .attr('width', '12px')\n .attr('height', '12px')\n .attr('transform', 'translate(-6, -6)')\n .attr('xlink:href', function(d) {\n var picon = getIcon(d);\n return picon ? '#' + picon : '';\n });\n\n\n // Vertices with directions get viewfields\n var dgroups = groups\n .selectAll('.viewfieldgroup')\n .data(function data(d) { return zoom >= 18 && getDirections(d) ? [d] : []; }, fastEntityKey);\n\n // exit\n dgroups.exit()\n .remove();\n\n // enter/update\n dgroups = dgroups.enter()\n .insert('g', '.shadow')\n .attr('class', 'viewfieldgroup')\n .merge(dgroups);\n\n var viewfields = dgroups.selectAll('.viewfield')\n .data(getDirections, function key(d) { return osmEntity.key(d); });\n\n // exit\n viewfields.exit()\n .remove();\n\n // enter/update\n viewfields.enter()\n .append('path')\n .attr('class', 'viewfield')\n .attr('d', 'M0,0H0')\n .merge(viewfields)\n .attr('marker-start', d => 'url(#ideditor-viewfield-marker' + (d.type === 'side' ? '-side' : '') + (wireframe ? '-wireframe' : '') + ')')\n .attr('transform', d => `rotate(${d.angle})`);\n }\n\n\n function drawTargets(selection, graph, entities, filter) {\n var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';\n var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';\n var getTransform = svgPointTransform(projection).geojson;\n var activeID = context.activeID();\n var data = { targets: [], nopes: [] };\n\n entities.forEach(function(node) {\n if (activeID === node.id) return; // draw no target on the activeID\n\n var vertexType = svgPassiveVertex(node, graph, activeID);\n if (vertexType !== 0) { // passive or adjacent - allow to connect\n data.targets.push({\n type: 'Feature',\n id: node.id,\n properties: {\n target: true,\n entity: node\n },\n geometry: node.asGeoJSON()\n });\n } else {\n data.nopes.push({\n type: 'Feature',\n id: node.id + '-nope',\n properties: {\n nope: true,\n target: true,\n entity: node\n },\n geometry: node.asGeoJSON()\n });\n }\n });\n\n // Targets allow hover and vertex snapping\n var targets = selection.selectAll('.vertex.target-allowed')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(data.targets, function key(d) { return d.id; });\n\n // exit\n targets.exit()\n .remove();\n\n // enter/update\n targets.enter()\n .append('circle')\n .attr('r', function(d) {\n return _radii[d.id]\n || radiuses.shadow[3];\n })\n .merge(targets)\n .attr('class', function(d) {\n return 'node vertex target target-allowed '\n + targetClass + d.id;\n })\n .attr('transform', getTransform);\n\n\n // NOPE\n var nopes = selection.selectAll('.vertex.target-nope')\n .filter(function(d) { return filter(d.properties.entity); })\n .data(data.nopes, function key(d) { return d.id; });\n\n // exit\n nopes.exit()\n .remove();\n\n // enter/update\n nopes.enter()\n .append('circle')\n .attr('r', function(d) { return (_radii[d.properties.entity.id] || radiuses.shadow[3]); })\n .merge(nopes)\n .attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; })\n .attr('transform', getTransform);\n }\n\n\n // Points can also render as vertices:\n // 1. in wireframe mode or\n // 2. at higher zooms if they have a direction\n function renderAsVertex(entity, graph, wireframe, zoom) {\n var geometry = entity.geometry(graph);\n return geometry === 'vertex' || (geometry === 'point' && (\n wireframe || (zoom >= 18 && entity.directions(graph, projection).length)\n ));\n }\n\n\n function isEditedNode(node, base, head) {\n var baseNode = base.entities[node.id];\n var headNode = head.entities[node.id];\n return !headNode ||\n !baseNode ||\n !deepEqual(headNode.tags, baseNode.tags) ||\n !deepEqual(headNode.loc, baseNode.loc);\n }\n\n\n function getSiblingAndChildVertices(ids, graph, wireframe, zoom) {\n var results = {};\n\n var seenIds = {};\n\n function addChildVertices(entity) {\n\n // avoid redundant work and infinite recursion of circular relations\n if (seenIds[entity.id]) return;\n seenIds[entity.id] = true;\n\n var geometry = entity.geometry(graph);\n if (!context.features().isHiddenFeature(entity, graph, geometry)) {\n var i;\n if (entity.type === 'way') {\n for (i = 0; i < entity.nodes.length; i++) {\n var child = graph.hasEntity(entity.nodes[i]);\n if (child) {\n addChildVertices(child);\n }\n }\n } else if (entity.type === 'relation') {\n for (i = 0; i < entity.members.length; i++) {\n var member = graph.hasEntity(entity.members[i].id);\n if (member) {\n addChildVertices(member);\n }\n }\n } else if (renderAsVertex(entity, graph, wireframe, zoom)) {\n results[entity.id] = entity;\n }\n }\n }\n\n ids.forEach(function(id) {\n var entity = graph.hasEntity(id);\n if (!entity) return;\n\n if (entity.type === 'node') {\n if (renderAsVertex(entity, graph, wireframe, zoom)) {\n results[entity.id] = entity;\n graph.parentWays(entity).forEach(function(entity) {\n addChildVertices(entity);\n });\n }\n } else { // way, relation\n addChildVertices(entity);\n }\n });\n\n return results;\n }\n\n\n function drawVertices(selection, graph, entities, filter, extent, fullRedraw) {\n var wireframe = context.surface().classed('fill-wireframe');\n var visualDiff = context.surface().classed('highlight-edited');\n var zoom = geoScaleToZoom(projection.scale());\n var mode = context.mode();\n var isMoving = mode && /^(add|draw|drag|move|rotate)/.test(mode.id);\n var base = context.history().base();\n\n var drawLayer = selection.selectAll('.layer-osm.points .points-group.vertices');\n var touchLayer = selection.selectAll('.layer-touch.points');\n\n if (fullRedraw) {\n _currPersistent = {};\n _radii = {};\n }\n\n // Collect important vertices from the `entities` list..\n // (during a partial redraw, it will not contain everything)\n for (var i = 0; i < entities.length; i++) {\n var entity = entities[i];\n var geometry = entity.geometry(graph);\n var keep = false;\n\n // a point that looks like a vertex..\n if ((geometry === 'point') && renderAsVertex(entity, graph, wireframe, zoom)) {\n _currPersistent[entity.id] = entity;\n keep = true;\n\n // a vertex of some importance..\n } else if (geometry === 'vertex' &&\n (entity.hasInterestingTags() || entity.isEndpoint(graph) || entity.isConnected(graph)\n || (visualDiff && isEditedNode(entity, base, graph)))) {\n _currPersistent[entity.id] = entity;\n keep = true;\n }\n\n // whatever this is, it's not a persistent vertex..\n if (!keep && !fullRedraw) {\n delete _currPersistent[entity.id];\n }\n }\n\n // 3 sets of vertices to consider:\n var sets = {\n persistent: _currPersistent, // persistent = important vertices (render always)\n selected: _currSelected, // selected + siblings of selected (render always)\n hovered: _currHover // hovered + siblings of hovered (render only in draw modes)\n };\n\n var all = Object.assign({}, (isMoving ? _currHover : {}), _currSelected, _currPersistent);\n\n // Draw the vertices..\n // The filter function controls the scope of what objects d3 will touch (exit/enter/update)\n // Adjust the filter function to expand the scope beyond whatever entities were passed in.\n var filterRendered = function(d) {\n return d.id in _currPersistent || d.id in _currSelected || d.id in _currHover || filter(d);\n };\n drawLayer\n .call(draw, graph, currentVisible(all), sets, filterRendered);\n\n // Draw touch targets..\n // When drawing, render all targets (not just those affected by a partial redraw)\n var filterTouch = function(d) {\n return isMoving ? true : filterRendered(d);\n };\n touchLayer\n .call(drawTargets, graph, currentVisible(all), filterTouch);\n\n\n function currentVisible(which) {\n return Object.keys(which)\n .map(graph.hasEntity, graph) // the current version of this entity\n .filter(function (entity) { return entity && entity.intersects(extent, graph); });\n }\n }\n\n\n // partial redraw - only update the selected items..\n drawVertices.drawSelected = function(selection, graph, extent) {\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n\n _prevSelected = _currSelected || {};\n if (context.map().isInWideSelection()) {\n _currSelected = {};\n context.selectedIDs().forEach(function(id) {\n var entity = graph.hasEntity(id);\n if (!entity) return;\n\n if (entity.type === 'node') {\n if (renderAsVertex(entity, graph, wireframe, zoom)) {\n _currSelected[entity.id] = entity;\n }\n }\n });\n\n } else {\n _currSelected = getSiblingAndChildVertices(context.selectedIDs(), graph, wireframe, zoom);\n }\n\n // note that drawVertices will add `_currSelected` automatically if needed..\n var filter = function(d) { return d.id in _prevSelected; };\n drawVertices(selection, graph, Object.values(_prevSelected), filter, extent, false);\n };\n\n\n // partial redraw - only update the hovered items..\n drawVertices.drawHover = function(selection, graph, target, extent) {\n if (target === _currHoverTarget) return; // continue only if something changed\n\n var wireframe = context.surface().classed('fill-wireframe');\n var zoom = geoScaleToZoom(projection.scale());\n\n _prevHover = _currHover || {};\n _currHoverTarget = target;\n var entity = target && target.properties && target.properties.entity;\n\n if (entity) {\n _currHover = getSiblingAndChildVertices([entity.id], graph, wireframe, zoom);\n } else {\n _currHover = {};\n }\n\n // note that drawVertices will add `_currHover` automatically if needed..\n var filter = function(d) { return d.id in _prevHover; };\n drawVertices(selection, graph, Object.values(_prevHover), filter, extent, false);\n };\n\n return drawVertices;\n}\n", "export { svgAreas } from './areas.js';\nexport { svgData } from './data.js';\nexport { svgDebug } from './debug.js';\nexport { svgDefs } from './defs.js';\nexport { svgIcon } from './icon.js';\nexport { svgGeolocate } from './geolocate';\nexport { svgLabels } from './labels.js';\nexport { svgLayers } from './layers.js';\nexport { svgLines } from './lines.js';\nexport { svgMapillaryImages } from './mapillary_images.js';\nexport { svgMapillarySigns } from './mapillary_signs.js';\nexport { svgMidpoints } from './midpoints.js';\nexport { svgNotes } from './notes.js';\nexport { svgMarkerSegments } from './helpers.js';\nexport { svgKartaviewImages } from './kartaview_images.js';\nexport { svgOsm } from './osm.js';\nexport { svgPassiveVertex } from './helpers.js';\nexport { svgPath } from './helpers.js';\nexport { svgPointTransform } from './helpers.js';\nexport { svgPoints } from './points.js';\nexport { svgRelationMemberTags } from './helpers.js';\nexport { svgSegmentWay } from './helpers.js';\nexport { svgStreetside } from './streetside.js';\nexport { svgVegbilder } from './vegbilder';\nexport { svgTagClasses } from './tag_classes.js';\nexport { svgTagPattern } from './tag_pattern.js';\nexport { svgTouch } from './touch.js';\nexport { svgTurns } from './turns.js';\nexport { svgVertices } from './vertices.js';\nexport { svgMapilioImages } from './mapilio_images.js';\nexport { svgPanoramaxImages } from './panoramax_images.js';\n", "import { t } from '../core/localizer';\nimport { actionOrthogonalize } from '../actions/orthogonalize';\nimport { behaviorOperation } from '../behavior/operation';\nimport { utilGetAllNodes } from '../util';\nimport { svgPath } from '../svg';\n\n\nexport function operationOrthogonalize(context, selectedIDs) {\n var _extent;\n var _type;\n var _actions = selectedIDs.map(chooseAction).filter(Boolean);\n var _amount = _actions.length === 1 ? 'single' : 'multiple';\n var _coords = utilGetAllNodes(selectedIDs, context.graph())\n .map(function(n) { return n.loc; });\n\n\n function chooseAction(entityID) {\n\n var entity = context.entity(entityID);\n var geometry = entity.geometry(context.graph());\n\n if (!_extent) {\n _extent = entity.extent(context.graph());\n } else {\n _extent = _extent.extend(entity.extent(context.graph()));\n }\n\n // square a line/area\n if (entity.type === 'way' && new Set(entity.nodes).size > 2 ) {\n if (_type && _type !== 'feature') return null;\n _type = 'feature';\n return actionOrthogonalize(entityID, context.projection);\n\n // square a single vertex\n } else if (geometry === 'vertex') {\n if (_type && _type !== 'corner') return null;\n _type = 'corner';\n var graph = context.graph();\n var parents = graph.parentWays(entity);\n if (parents.length === 1) {\n var way = parents[0];\n if (way.nodes.indexOf(entityID) !== -1) {\n return actionOrthogonalize(way.id, context.projection, entityID);\n }\n }\n }\n\n return null;\n }\n\n\n var operation = function() {\n if (!_actions.length) return;\n\n var combinedAction = function(graph, t) {\n _actions.forEach(function(action) {\n if (!action.disabled(graph)) {\n graph = action(graph, t);\n }\n });\n return graph;\n };\n combinedAction.transitionable = true;\n\n context.perform(combinedAction, operation.annotation());\n\n window.setTimeout(function() {\n context.validator().validate();\n }, 300); // after any transition\n };\n\n\n operation.available = function() {\n return _actions.length && selectedIDs.length === _actions.length;\n };\n\n\n // don't cache this because the visible extent could change\n operation.disabled = function() {\n if (!_actions.length) return '';\n\n var actionDisableds = _actions.map(function(action) {\n return action.disabled(context.graph());\n }).filter(Boolean);\n\n if (actionDisableds.length === _actions.length) {\n // none of the features can be squared\n\n if (new Set(actionDisableds).size > 1) {\n return 'multiple_blockers';\n }\n return actionDisableds[0];\n } else if (_extent &&\n _extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = _coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n };\n\n\n operation.getAuxiliaryGeometry = function() {\n const graph = context.graph();\n return _actions.map((action, idx) => {\n if (!action.disabled(graph)) {\n const previewGraph = action(graph, t);\n const way = previewGraph.hasEntity(selectedIDs[idx]);\n const getPath = svgPath(context.projection, previewGraph, false);\n return {\n id: way.id,\n path: getPath(way),\n klass: 'preview'\n };\n } else {\n return false;\n }\n }).filter(Boolean);\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.orthogonalize.' + disable + '.' + _amount) :\n t.append('operations.orthogonalize.description.' + _type + '.' + _amount);\n };\n\n\n operation.annotation = function() {\n return t('operations.orthogonalize.annotation.' + _type, { n: _actions.length });\n };\n\n\n operation.id = 'orthogonalize';\n operation.keys = [t('operations.orthogonalize.key')];\n operation.title = t.append('operations.orthogonalize.title');\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import { t } from '../core/localizer';\nimport { actionReflect } from '../actions/reflect';\nimport { behaviorOperation } from '../behavior/operation';\nimport { utilGetAllNodes, utilTotalExtent } from '../util/util';\nimport { svgPath } from '../svg';\n\n\nexport function operationReflectShort(context, selectedIDs) {\n return operationReflect(context, selectedIDs, 'short');\n}\n\n\nexport function operationReflectLong(context, selectedIDs) {\n return operationReflect(context, selectedIDs, 'long');\n}\n\n\nexport function operationReflect(context, selectedIDs, axis) {\n axis = axis || 'long';\n var multi = (selectedIDs.length === 1 ? 'single' : 'multiple');\n var nodes = utilGetAllNodes(selectedIDs, context.graph());\n var coords = nodes.map(function(n) { return n.loc; });\n var extent = utilTotalExtent(selectedIDs, context.graph());\n\n\n var _action = actionReflect(selectedIDs, context.projection)\n .useLongAxis(Boolean(axis === 'long'));\n\n var operation = function() {\n context.perform(_action, operation.annotation());\n\n window.setTimeout(function() {\n context.validator().validate();\n }, 300); // after any transition\n };\n\n\n operation.available = function() {\n return nodes.length >= 3;\n };\n\n\n // don't cache this because the visible extent could change\n operation.disabled = function() {\n if (extent.percentContainedIn(context.map().extent()) < 0.8) {\n return 'too_large';\n } else if (someMissing()) {\n return 'not_downloaded';\n } else if (selectedIDs.some(context.hasHiddenConnections)) {\n return 'connected_to_hidden';\n } else if (selectedIDs.some(incompleteRelation)) {\n return 'incomplete_relation';\n }\n\n return false;\n\n\n function someMissing() {\n if (context.inIntro()) return false;\n var osm = context.connection();\n if (osm) {\n var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); });\n if (missing.length) {\n missing.forEach(function(loc) { context.loadTileAtLoc(loc); });\n return true;\n }\n }\n return false;\n }\n\n function incompleteRelation(id) {\n var entity = context.entity(id);\n return entity.type === 'relation' && !entity.isComplete(context.graph());\n }\n };\n\n\n operation.getAuxiliaryGeometry = function() {\n const graph = context.graph();\n const [p, q] = _action.getReflectAxis(graph);\n const previewGraph = _action(graph);\n const getPath = svgPath(context.projection, previewGraph, false);\n return [{\n id: 'axis',\n path: `M ${p[0]} ${p[1]} L ${q[0]} ${q[1]}`,\n klass: 'reflect-axis'\n }, ...selectedIDs.map(entityId => {\n const entity = previewGraph.hasEntity(entityId);\n return {\n id: entity.id,\n path: getPath(entity),\n klass: 'preview'\n };\n })];\n };\n\n\n operation.tooltip = function() {\n var disable = operation.disabled();\n return disable ?\n t.append('operations.reflect.' + disable + '.' + multi) :\n t.append('operations.reflect.description.' + axis + '.' + multi);\n };\n\n\n operation.annotation = function() {\n return t('operations.reflect.annotation.' + axis + '.feature', { n: selectedIDs.length });\n };\n\n\n operation.id = 'reflect-' + axis;\n operation.keys = [t('operations.reflect.key.' + axis)];\n operation.title = t.append('operations.reflect.title.' + axis);\n operation.behavior = behaviorOperation(context).which(operation);\n\n return operation;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport {\n polygonHull as d3_polygonHull,\n polygonCentroid as d3_polygonCentroid\n} from 'd3-polygon';\n\nimport { t } from '../core/localizer';\nimport { actionRotate } from '../actions/rotate';\nimport { actionNoop } from '../actions/noop';\nimport { behaviorEdit } from '../behavior/edit';\nimport { geoVecInterp, geoVecLength } from '../geo/vector';\nimport { modeBrowse } from './browse';\nimport { modeSelect } from './select';\n\nimport { operationCircularize } from '../operations/circularize';\nimport { operationDelete } from '../operations/delete';\nimport { operationMove } from '../operations/move';\nimport { operationOrthogonalize } from '../operations/orthogonalize';\nimport { operationReflectLong, operationReflectShort } from '../operations/reflect';\n\nimport { utilKeybinding } from '../util/keybinding';\nimport { utilFastMouse, utilGetAllNodes } from '../util/util';\n\n\nexport function modeRotate(context, entityIDs) {\n\n var _tolerancePx = 4; // see also behaviorDrag, behaviorSelect, modeMove\n\n var mode = {\n id: 'rotate',\n button: 'browse'\n };\n\n var keybinding = utilKeybinding('rotate');\n var behaviors = [\n behaviorEdit(context),\n operationCircularize(context, entityIDs).behavior,\n operationDelete(context, entityIDs).behavior,\n operationMove(context, entityIDs).behavior,\n operationOrthogonalize(context, entityIDs).behavior,\n operationReflectLong(context, entityIDs).behavior,\n operationReflectShort(context, entityIDs).behavior\n ];\n var annotation = entityIDs.length === 1 ?\n t('operations.rotate.annotation.' + context.graph().geometry(entityIDs[0])) :\n t('operations.rotate.annotation.feature', { n: entityIDs.length });\n\n var _prevGraph;\n var _prevAngle;\n var _prevTransform;\n var _pivot;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function doRotate(d3_event) {\n var fn;\n if (context.graph() !== _prevGraph) {\n fn = context.perform;\n } else {\n fn = context.replace;\n }\n\n // projection changed, recalculate _pivot\n var projection = context.projection;\n var currTransform = projection.transform();\n if (!_prevTransform ||\n currTransform.k !== _prevTransform.k ||\n currTransform.x !== _prevTransform.x ||\n currTransform.y !== _prevTransform.y) {\n\n var nodes = utilGetAllNodes(entityIDs, context.graph());\n var points = nodes.map(function(n) { return projection(n.loc); });\n _pivot = getPivot(points);\n _prevAngle = undefined;\n }\n\n\n var currMouse = context.map().mouse(d3_event);\n var currAngle = Math.atan2(currMouse[1] - _pivot[1], currMouse[0] - _pivot[0]);\n\n if (typeof _prevAngle === 'undefined') _prevAngle = currAngle;\n var delta = currAngle - _prevAngle;\n\n fn(actionRotate(entityIDs, _pivot, delta, projection));\n\n _prevTransform = currTransform;\n _prevAngle = currAngle;\n _prevGraph = context.graph();\n }\n\n function getPivot(points) {\n var _pivot;\n if (points.length === 1) {\n _pivot = points[0];\n } else if (points.length === 2) {\n _pivot = geoVecInterp(points[0], points[1], 0.5);\n } else {\n var polygonHull = d3_polygonHull(points);\n if (polygonHull.length === 2) {\n _pivot = geoVecInterp(points[0], points[1], 0.5);\n } else {\n _pivot = d3_polygonCentroid(d3_polygonHull(points));\n }\n }\n return _pivot;\n }\n\n\n function finish(d3_event) {\n d3_event.stopPropagation();\n context.replace(actionNoop(), annotation);\n context.enter(modeSelect(context, entityIDs));\n }\n\n\n function cancel() {\n if (_prevGraph) context.pop(); // remove the rotate\n context.enter(modeSelect(context, entityIDs));\n }\n\n\n function undone() {\n context.enter(modeBrowse(context));\n }\n\n\n mode.enter = function() {\n _prevGraph = null;\n context.features().forceVisible(entityIDs);\n\n behaviors.forEach(context.install);\n\n var downEvent;\n\n context.surface()\n .on(_pointerPrefix + 'down.modeRotate', function(d3_event) {\n downEvent = d3_event;\n });\n\n d3_select(window)\n .on(_pointerPrefix + 'move.modeRotate', doRotate, true)\n .on(_pointerPrefix + 'up.modeRotate', function(d3_event) {\n if (!downEvent) return;\n var mapNode = context.container().select('.main-map').node();\n var pointGetter = utilFastMouse(mapNode);\n var p1 = pointGetter(downEvent);\n var p2 = pointGetter(d3_event);\n var dist = geoVecLength(p1, p2);\n\n if (dist <= _tolerancePx) finish(d3_event);\n downEvent = null;\n }, true);\n\n context.history()\n .on('undone.modeRotate', undone);\n\n keybinding\n .on('\u238B', cancel)\n .on('\u21A9', finish);\n\n d3_select(document)\n .call(keybinding);\n };\n\n\n mode.exit = function() {\n behaviors.forEach(context.uninstall);\n\n context.surface()\n .on(_pointerPrefix + 'down.modeRotate', null);\n\n d3_select(window)\n .on(_pointerPrefix + 'move.modeRotate', null, true)\n .on(_pointerPrefix + 'up.modeRotate', null, true);\n\n context.history()\n .on('undone.modeRotate', null);\n\n d3_select(document)\n .call(keybinding.unbind);\n\n context.features().forceVisible([]);\n };\n\n\n mode.selectedIDs = function() {\n if (!arguments.length) return entityIDs;\n // no assign\n return mode;\n };\n\n\n return mode;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { t } from '../core/localizer';\nimport { JXON } from '../util/jxon';\nimport { geoExtent } from '../geo';\nimport { osmChangeset } from '../osm';\nimport { svgIcon } from '../svg/icon';\n\nimport {\n utilEntityOrMemberSelector,\n utilKeybinding,\n utilRebind,\n utilWrap\n} from '../util';\n\n\nexport function uiConflicts(context) {\n var dispatch = d3_dispatch('cancel', 'save');\n var keybinding = utilKeybinding('conflicts');\n var _origChanges;\n var _conflictList;\n var _shownConflictIndex;\n\n\n function keybindingOn() {\n d3_select(document)\n .call(keybinding.on('\u238B', cancel, true));\n }\n\n function keybindingOff() {\n d3_select(document)\n .call(keybinding.unbind);\n }\n\n function tryAgain() {\n keybindingOff();\n dispatch.call('save');\n }\n\n function cancel() {\n keybindingOff();\n dispatch.call('cancel');\n }\n\n\n function conflicts(selection) {\n keybindingOn();\n\n var headerEnter = selection.selectAll('.header')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'header fillL');\n\n headerEnter\n .append('button')\n .attr('class', 'fr')\n .attr('title', t('icons.close'))\n .on('click', cancel)\n .call(svgIcon('#iD-icon-close'));\n\n headerEnter\n .append('h2')\n .call(t.append('save.conflict.header'));\n\n var bodyEnter = selection.selectAll('.body')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'body fillL');\n\n var conflictsHelpEnter = bodyEnter\n .append('div')\n .attr('class', 'conflicts-help')\n .call(t.append('save.conflict.help'));\n\n\n // Download changes link\n var changeset = new osmChangeset();\n\n delete changeset.id; // Export without changeset_id\n\n var data = JXON.stringify(changeset.osmChangeJXON(_origChanges));\n var blob = new Blob([data], { type: 'text/xml;charset=utf-8;' });\n var fileName = 'changes.osc';\n\n var linkEnter = conflictsHelpEnter.selectAll('.download-changes')\n .append('a')\n .attr('class', 'download-changes');\n\n // download the data as a file\n linkEnter\n .attr('href', window.URL.createObjectURL(blob))\n .attr('download', fileName);\n\n linkEnter\n .call(svgIcon('#iD-icon-load', 'inline'))\n .append('span')\n .call(t.append('save.conflict.download_changes'));\n\n\n bodyEnter\n .append('div')\n .attr('class', 'conflict-container fillL3')\n .call(showConflict, 0);\n\n bodyEnter\n .append('div')\n .attr('class', 'conflicts-done')\n .attr('opacity', 0)\n .style('display', 'none')\n .call(t.append('save.conflict.done'));\n\n var buttonsEnter = bodyEnter\n .append('div')\n .attr('class','buttons col12 joined conflicts-buttons');\n\n buttonsEnter\n .append('button')\n .attr('disabled', _conflictList.length > 1)\n .attr('class', 'action conflicts-button col6')\n .call(t.append('save.title'))\n .on('click.try_again', tryAgain);\n\n buttonsEnter\n .append('button')\n .attr('class', 'secondary-action conflicts-button col6')\n .call(t.append('confirm.cancel'))\n .on('click.cancel', cancel);\n }\n\n\n function showConflict(selection, index) {\n index = utilWrap(index, _conflictList.length);\n _shownConflictIndex = index;\n\n var parent = d3_select(selection.node().parentNode);\n\n // enable save button if this is the last conflict being reviewed..\n if (index === _conflictList.length - 1) {\n window.setTimeout(function() {\n parent.select('.conflicts-button')\n .attr('disabled', null);\n\n parent.select('.conflicts-done')\n .transition()\n .attr('opacity', 1)\n .style('display', 'block');\n }, 250);\n }\n\n var conflict = selection\n .selectAll('.conflict')\n .data([_conflictList[index]]);\n\n conflict.exit()\n .remove();\n\n var conflictEnter = conflict.enter()\n .append('div')\n .attr('class', 'conflict');\n\n conflictEnter\n .append('h4')\n .attr('class', 'conflict-count')\n .call(t.append('save.conflict.count', { num: index + 1, total: _conflictList.length }));\n\n conflictEnter\n .append('a')\n .attr('class', 'conflict-description')\n .attr('href', '#')\n .text(function(d) { return d.name; })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n zoomToEntity(d.id);\n });\n\n var details = conflictEnter\n .append('div')\n .attr('class', 'conflict-detail-container');\n\n details\n .append('ul')\n .attr('class', 'conflict-detail-list')\n .selectAll('li')\n .data(function(d) { return d.details || []; })\n .enter()\n .append('li')\n .attr('class', 'conflict-detail-item')\n .each(function(d) {\n d3_select(this).call(d);\n });\n\n details\n .append('div')\n .attr('class', 'conflict-choices')\n .call(addChoices);\n\n details\n .append('div')\n .attr('class', 'conflict-nav-buttons joined cf')\n .selectAll('button')\n .data(['previous', 'next'])\n .enter()\n .append('button')\n .attr('class', 'conflict-nav-button action col6')\n .attr('disabled', function(d, i) {\n return (i === 0 && index === 0) ||\n (i === 1 && index === _conflictList.length - 1) || null;\n })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n\n var container = parent.selectAll('.conflict-container');\n var sign = (d === 'previous' ? -1 : 1);\n\n container\n .selectAll('.conflict')\n .remove();\n\n container\n .call(showConflict, index + sign);\n })\n .each(function(d) { t.append('save.conflict.' + d)(d3_select(this)); });\n\n }\n\n\n function addChoices(selection) {\n var choices = selection\n .append('ul')\n .attr('class', 'layer-list')\n .selectAll('li')\n .data(function(d) { return d.choices || []; });\n\n // enter\n var choicesEnter = choices.enter()\n .append('li')\n .attr('class', 'layer');\n\n var labelEnter = choicesEnter\n .append('label');\n\n labelEnter\n .append('input')\n .attr('type', 'radio')\n .attr('name', function(d) { return d.id; })\n .on('change', function(d3_event, d) {\n var ul = this.parentNode.parentNode.parentNode;\n ul.__data__.chosen = d.id;\n choose(d3_event, ul, d);\n });\n\n labelEnter\n .append('span')\n .text(function(d) { return d.text; });\n\n // update\n choicesEnter\n .merge(choices)\n .each(function(d) {\n var ul = this.parentNode;\n if (ul.__data__.chosen === d.id) {\n choose(null, ul, d);\n }\n });\n }\n\n\n function choose(d3_event, ul, datum) {\n if (d3_event) d3_event.preventDefault();\n\n d3_select(ul)\n .selectAll('li')\n .classed('active', function(d) { return d === datum; })\n .selectAll('input')\n .property('checked', function(d) { return d === datum; });\n\n var extent = geoExtent();\n var entity;\n\n entity = context.graph().hasEntity(datum.id);\n if (entity) extent._extend(entity.extent(context.graph()));\n\n datum.action();\n\n entity = context.graph().hasEntity(datum.id);\n if (entity) extent._extend(entity.extent(context.graph()));\n\n zoomToEntity(datum.id, extent);\n }\n\n\n function zoomToEntity(id, extent) {\n context.surface().selectAll('.hover')\n .classed('hover', false);\n\n var entity = context.graph().hasEntity(id);\n if (entity) {\n if (extent) {\n context.map().trimmedExtent(extent);\n } else {\n context.map().zoomToEase(entity);\n }\n context.surface().selectAll(utilEntityOrMemberSelector([entity.id], context.graph()))\n .classed('hover', true);\n }\n }\n\n\n // The conflict list should be an array of objects like:\n // {\n // id: id,\n // name: entityName(local),\n // details: merge.conflicts(),\n // chosen: 1,\n // choices: [\n // choice(id, keepMine, forceLocal),\n // choice(id, keepTheirs, forceRemote)\n // ]\n // }\n conflicts.conflictList = function(_) {\n if (!arguments.length) return _conflictList;\n _conflictList = _;\n return conflicts;\n };\n\n\n conflicts.origChanges = function(_) {\n if (!arguments.length) return _origChanges;\n _origChanges = _;\n return conflicts;\n };\n\n\n conflicts.shownEntityIds = function() {\n if (_conflictList && typeof _shownConflictIndex === 'number') {\n return [_conflictList[_shownConflictIndex].id];\n }\n return [];\n };\n\n\n return utilRebind(conflicts, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { uiModal } from './modal';\n\n\nexport function uiConfirm(selection) {\n var modalSelection = uiModal(selection);\n\n modalSelection.select('.modal')\n .classed('modal-alert', true);\n\n var section = modalSelection.select('.content');\n\n section.append('div')\n .attr('class', 'modal-section header');\n\n section.append('div')\n .attr('class', 'modal-section message-text');\n\n var buttons = section.append('div')\n .attr('class', 'modal-section buttons cf');\n\n\n modalSelection.okButton = function() {\n buttons\n .append('button')\n .attr('class', 'button ok-button action')\n .on('click.confirm', function() {\n modalSelection.remove();\n })\n .call(t.append('confirm.okay'))\n .node()\n .focus();\n\n return modalSelection;\n };\n\n\n return modalSelection;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { utilFunctor } from '../util/util';\n\nvar _popoverID = 0;\n\nexport function uiPopover(klass) {\n var _id = _popoverID++;\n var _anchorSelection = d3_select(null);\n var popover = function(selection) {\n _anchorSelection = selection;\n selection.each(setup);\n };\n var _animation = utilFunctor(false);\n var _placement = utilFunctor('top'); // top, bottom, left, right\n var _alignment = utilFunctor('center'); // leading, center, trailing\n var _scrollContainer = utilFunctor(d3_select(null));\n var _content;\n var _displayType = utilFunctor('');\n var _hasArrow = utilFunctor(true);\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n popover.displayType = function(val) {\n if (arguments.length) {\n _displayType = utilFunctor(val);\n return popover;\n } else {\n return _displayType;\n }\n };\n\n popover.hasArrow = function(val) {\n if (arguments.length) {\n _hasArrow = utilFunctor(val);\n return popover;\n } else {\n return _hasArrow;\n }\n };\n\n popover.placement = function(val) {\n if (arguments.length) {\n _placement = utilFunctor(val);\n return popover;\n } else {\n return _placement;\n }\n };\n\n popover.alignment = function(val) {\n if (arguments.length) {\n _alignment = utilFunctor(val);\n return popover;\n } else {\n return _alignment;\n }\n };\n\n popover.scrollContainer = function(val) {\n if (arguments.length) {\n _scrollContainer = utilFunctor(val);\n return popover;\n } else {\n return _scrollContainer;\n }\n };\n\n popover.content = function(val) {\n if (arguments.length) {\n _content = val;\n return popover;\n } else {\n return _content;\n }\n };\n\n popover.isShown = function() {\n var popoverSelection = _anchorSelection.select('.popover-' + _id);\n return !popoverSelection.empty() && popoverSelection.classed('in');\n };\n\n popover.show = function() {\n _anchorSelection.each(show);\n };\n\n popover.updateContent = function() {\n _anchorSelection.each(updateContent);\n };\n\n popover.hide = function() {\n _anchorSelection.each(hide);\n };\n\n popover.toggle = function() {\n _anchorSelection.each(toggle);\n };\n\n popover.destroy = function(selection, selector) {\n // by default, just destroy the current popover\n selector = selector || '.popover-' + _id;\n\n selection\n .on(_pointerPrefix + 'enter.popover', null)\n .on(_pointerPrefix + 'leave.popover', null)\n .on(_pointerPrefix + 'up.popover', null)\n .on(_pointerPrefix + 'down.popover', null)\n .on('focus.popover', null)\n .on('blur.popover', null)\n .on('click.popover', null)\n .attr('title', function() {\n return this.getAttribute('data-original-title') || this.getAttribute('title');\n })\n .attr('data-original-title', null)\n .selectAll(selector)\n .remove();\n };\n\n\n popover.destroyAny = function(selection) {\n selection.call(popover.destroy, '.popover');\n };\n\n function setup() {\n var anchor = d3_select(this);\n var animate = _animation.apply(this, arguments);\n var popoverSelection = anchor.selectAll('.popover-' + _id)\n .data([0]);\n\n\n var enter = popoverSelection.enter()\n .append('div')\n .attr('class', 'popover popover-' + _id + ' ' + (klass ? klass : ''))\n .classed('arrowed', _hasArrow.apply(this, arguments));\n\n enter\n .append('div')\n .attr('class', 'popover-arrow');\n\n enter\n .append('div')\n .attr('class', 'popover-inner');\n\n popoverSelection = enter\n .merge(popoverSelection);\n\n if (animate) {\n popoverSelection.classed('fade', true);\n }\n\n var display = _displayType.apply(this, arguments);\n\n if (display === 'hover') {\n var _lastNonMouseEnterTime;\n anchor.on(_pointerPrefix + 'enter.popover', function(d3_event) {\n\n if (d3_event.pointerType) {\n if (d3_event.pointerType !== 'mouse') {\n _lastNonMouseEnterTime = d3_event.timeStamp;\n // only allow hover behavior for mouse input\n return;\n } else if (_lastNonMouseEnterTime &&\n d3_event.timeStamp - _lastNonMouseEnterTime < 1500) {\n // HACK: iOS 13.4 sends an erroneous `mouse` type pointerenter\n // event for non-mouse interactions right after sending\n // the correct type pointerenter event. Workaround by discarding\n // any mouse event that occurs immediately after a non-mouse event.\n return;\n }\n }\n\n // don't show if buttons are pressed, e.g. during click and drag of map\n if (d3_event.buttons !== 0) return;\n\n show.apply(this, arguments);\n })\n .on(_pointerPrefix + 'leave.popover', function() {\n hide.apply(this, arguments);\n })\n // show on focus too for better keyboard navigation support\n .on('focus.popover', function() {\n show.apply(this, arguments);\n })\n .on('blur.popover', function() {\n hide.apply(this, arguments);\n });\n\n } else if (display === 'clickFocus') {\n anchor\n .on(_pointerPrefix + 'down.popover', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n })\n .on(_pointerPrefix + 'up.popover', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n })\n .on('click.popover', toggle);\n\n popoverSelection\n // This attribute lets the popover take focus\n .attr('tabindex', 0)\n .on('blur.popover', function() {\n anchor.each(function() {\n hide.apply(this, arguments);\n });\n });\n }\n }\n\n\n function show() {\n var anchor = d3_select(this);\n var popoverSelection = anchor.selectAll('.popover-' + _id);\n\n if (popoverSelection.empty()) {\n // popover was removed somehow, put it back\n anchor.call(popover.destroy);\n anchor.each(setup);\n popoverSelection = anchor.selectAll('.popover-' + _id);\n }\n\n popoverSelection.classed('in', true);\n\n var displayType = _displayType.apply(this, arguments);\n if (displayType === 'clickFocus') {\n anchor.classed('active', true);\n popoverSelection.node().focus();\n }\n\n anchor.each(updateContent);\n }\n\n function updateContent() {\n var anchor = d3_select(this);\n\n if (_content) {\n anchor.selectAll('.popover-' + _id + ' > .popover-inner')\n .call(_content.apply(this, arguments));\n }\n\n updatePosition.apply(this, arguments);\n // hack: update multiple times to fix instances where the absolute offset is\n // set before the dynamic popover size is calculated by the browser\n updatePosition.apply(this, arguments);\n updatePosition.apply(this, arguments);\n }\n\n\n function updatePosition() {\n\n var anchor = d3_select(this);\n var popoverSelection = anchor.selectAll('.popover-' + _id);\n\n var scrollContainer = _scrollContainer && _scrollContainer.apply(this, arguments);\n var scrollNode = scrollContainer && !scrollContainer.empty() && scrollContainer.node();\n var scrollLeft = scrollNode ? scrollNode.scrollLeft : 0;\n var scrollTop = scrollNode ? scrollNode.scrollTop : 0;\n\n var placement = _placement.apply(this, arguments);\n popoverSelection\n .classed('left', false)\n .classed('right', false)\n .classed('top', false)\n .classed('bottom', false)\n .classed(placement, true);\n\n var alignment = _alignment.apply(this, arguments);\n var alignFactor = 0.5;\n if (alignment === 'leading') {\n alignFactor = 0;\n } else if (alignment === 'trailing') {\n alignFactor = 1;\n }\n var anchorFrame = getFrame(anchor.node());\n var popoverFrame = getFrame(popoverSelection.node());\n var position;\n\n switch (placement) {\n case 'top':\n position = {\n x: anchorFrame.x + (anchorFrame.w - popoverFrame.w) * alignFactor,\n y: anchorFrame.y - popoverFrame.h\n };\n break;\n case 'bottom':\n position = {\n x: anchorFrame.x + (anchorFrame.w - popoverFrame.w) * alignFactor,\n y: anchorFrame.y + anchorFrame.h\n };\n break;\n case 'left':\n position = {\n x: anchorFrame.x - popoverFrame.w,\n y: anchorFrame.y + (anchorFrame.h - popoverFrame.h) * alignFactor\n };\n break;\n case 'right':\n position = {\n x: anchorFrame.x + anchorFrame.w,\n y: anchorFrame.y + (anchorFrame.h - popoverFrame.h) * alignFactor\n };\n break;\n }\n\n if (position) {\n if (scrollNode) {\n const MIN_MARGIN = 10;\n const popoverRect = popoverSelection.node().getBoundingClientRect();\n const scrollNodeRect = scrollNode.getBoundingClientRect();\n const arrow = anchor.selectAll('.popover-' + _id + ' > .popover-arrow');\n\n if (placement === 'top' || placement === 'bottom') {\n const initialPosX = position.x;\n if (popoverRect.right > scrollNodeRect.right - MIN_MARGIN) {\n position.x -= popoverRect.right - (scrollNodeRect.right - MIN_MARGIN);\n } else if (popoverRect.left < scrollNodeRect.left) {\n position.x += (scrollNodeRect.left + MIN_MARGIN) - popoverRect.left;\n }\n // keep the arrow centered on the button, or as close as possible\n const arrowPosX = Math.min(Math.max(popoverFrame.w / 2 - (position.x - initialPosX), MIN_MARGIN), popoverFrame.w - MIN_MARGIN);\n arrow.style('left', ~~arrowPosX + 'px');\n\n } else if (placement === 'left' || placement === 'right') {\n const initialPosY = position.y;\n if (popoverRect.bottom > scrollNodeRect.bottom - MIN_MARGIN) {\n position.y -= popoverRect.bottom - (scrollNodeRect.bottom - MIN_MARGIN);\n } else if (popoverRect.top < scrollNodeRect.top + MIN_MARGIN) {\n position.y += (scrollNodeRect.top + MIN_MARGIN) - popoverRect.top;\n }\n // keep the arrow centered on the button, or as close as possible\n const arrowPosY = Math.min(Math.max(popoverFrame.h / 2 - (position.y - initialPosY), MIN_MARGIN), popoverFrame.h - MIN_MARGIN);\n arrow.style('top', ~~arrowPosY + 'px');\n }\n }\n\n popoverSelection\n .style('left', ~~position.x + 'px')\n .style('top', ~~position.y + 'px');\n } else {\n popoverSelection\n .style('left', null)\n .style('top', null);\n }\n\n function getFrame(node) {\n var positionStyle = d3_select(node).style('position');\n if (positionStyle === 'absolute' || positionStyle === 'static') {\n return {\n x: node.offsetLeft - scrollLeft,\n y: node.offsetTop - scrollTop,\n w: node.offsetWidth,\n h: node.offsetHeight\n };\n } else {\n return {\n x: 0,\n y: 0,\n w: node.offsetWidth,\n h: node.offsetHeight\n };\n }\n }\n }\n\n\n function hide() {\n var anchor = d3_select(this);\n if (_displayType.apply(this, arguments) === 'clickFocus') {\n anchor.classed('active', false);\n }\n anchor.selectAll('.popover-' + _id).classed('in', false);\n }\n\n\n function toggle() {\n if (d3_select(this).select('.popover-' + _id).classed('in')) {\n hide.apply(this, arguments);\n } else {\n show.apply(this, arguments);\n }\n }\n\n\n return popover;\n}\n", "import { utilFunctor } from '../util/util';\nimport { t } from '../core/localizer';\nimport { uiPopover } from './popover';\n\nexport function uiTooltip(klass) {\n\n var tooltip = uiPopover((klass || '') + ' tooltip')\n .displayType('hover');\n\n var _title = function() {\n var title = this.getAttribute('data-original-title');\n if (title) {\n return title;\n } else {\n title = this.getAttribute('title');\n this.removeAttribute('title');\n this.setAttribute('data-original-title', title);\n }\n return title;\n };\n\n var _heading = utilFunctor(null);\n var _keys = utilFunctor(null);\n\n tooltip.title = function(val) {\n if (!arguments.length) return _title;\n _title = utilFunctor(val);\n return tooltip;\n };\n\n tooltip.heading = function(val) {\n if (!arguments.length) return _heading;\n _heading = utilFunctor(val);\n return tooltip;\n };\n\n tooltip.keys = function(val) {\n if (!arguments.length) return _keys;\n _keys = utilFunctor(val);\n return tooltip;\n };\n\n tooltip.content(function() {\n var heading = _heading.apply(this, arguments);\n var text = _title.apply(this, arguments);\n var keys = _keys.apply(this, arguments);\n\n var headingCallback = typeof heading === 'function' ? heading : s => s.text(heading);\n var textCallback = typeof text === 'function' ? text : s => s.text(text);\n\n return function(selection) {\n\n var headingSelect = selection\n .selectAll('.tooltip-heading')\n .data(heading ? [heading] :[]);\n\n headingSelect.exit()\n .remove();\n\n headingSelect.enter()\n .append('div')\n .attr('class', 'tooltip-heading')\n .merge(headingSelect)\n .text('')\n .call(headingCallback);\n\n var textSelect = selection\n .selectAll('.tooltip-text')\n .data(text ? [text] :[]);\n\n textSelect.exit()\n .remove();\n\n textSelect.enter()\n .append('div')\n .attr('class', 'tooltip-text')\n .merge(textSelect)\n .text('')\n .call(textCallback);\n\n var keyhintWrap = selection\n .selectAll('.keyhint-wrap')\n .data(keys && keys.length ? [0] : []);\n\n keyhintWrap.exit()\n .remove();\n\n var keyhintWrapEnter = keyhintWrap.enter()\n .append('div')\n .attr('class', 'keyhint-wrap');\n\n keyhintWrapEnter\n .append('span')\n .call(t.append('tooltip_keyhint'));\n\n keyhintWrap = keyhintWrapEnter.merge(keyhintWrap);\n\n keyhintWrap.selectAll('kbd.shortcut')\n .data(keys && keys.length ? keys : [])\n .enter()\n .append('kbd')\n .attr('class', 'shortcut')\n .text(function(d) {\n return d;\n });\n };\n });\n\n return tooltip;\n}\n", "/*\r\n * bignumber.js v9.3.1\r\n * A JavaScript library for arbitrary-precision arithmetic.\r\n * https://github.com/MikeMcl/bignumber.js\r\n * Copyright (c) 2025 Michael Mclaughlin \r\n * MIT Licensed.\r\n *\r\n * BigNumber.prototype methods | BigNumber methods\r\n * |\r\n * absoluteValue abs | clone\r\n * comparedTo | config set\r\n * decimalPlaces dp | DECIMAL_PLACES\r\n * dividedBy div | ROUNDING_MODE\r\n * dividedToIntegerBy idiv | EXPONENTIAL_AT\r\n * exponentiatedBy pow | RANGE\r\n * integerValue | CRYPTO\r\n * isEqualTo eq | MODULO_MODE\r\n * isFinite | POW_PRECISION\r\n * isGreaterThan gt | FORMAT\r\n * isGreaterThanOrEqualTo gte | ALPHABET\r\n * isInteger | isBigNumber\r\n * isLessThan lt | maximum max\r\n * isLessThanOrEqualTo lte | minimum min\r\n * isNaN | random\r\n * isNegative | sum\r\n * isPositive |\r\n * isZero |\r\n * minus |\r\n * modulo mod |\r\n * multipliedBy times |\r\n * negated |\r\n * plus |\r\n * precision sd |\r\n * shiftedBy |\r\n * squareRoot sqrt |\r\n * toExponential |\r\n * toFixed |\r\n * toFormat |\r\n * toFraction |\r\n * toJSON |\r\n * toNumber |\r\n * toPrecision |\r\n * toString |\r\n * valueOf |\r\n *\r\n */\r\n\r\n\r\nvar\r\n isNumeric = /^-?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?$/i,\r\n mathceil = Math.ceil,\r\n mathfloor = Math.floor,\r\n\r\n bignumberError = '[BigNumber Error] ',\r\n tooManyDigits = bignumberError + 'Number primitive has more than 15 significant digits: ',\r\n\r\n BASE = 1e14,\r\n LOG_BASE = 14,\r\n MAX_SAFE_INTEGER = 0x1fffffffffffff, // 2^53 - 1\r\n // MAX_INT32 = 0x7fffffff, // 2^31 - 1\r\n POWS_TEN = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13],\r\n SQRT_BASE = 1e7,\r\n\r\n // EDITABLE\r\n // The limit on the value of DECIMAL_PLACES, TO_EXP_NEG, TO_EXP_POS, MIN_EXP, MAX_EXP, and\r\n // the arguments to toExponential, toFixed, toFormat, and toPrecision.\r\n MAX = 1E9; // 0 to MAX_INT32\r\n\r\n\r\n/*\r\n * Create and return a BigNumber constructor.\r\n */\r\nfunction clone(configObject) {\r\n var div, convertBase, parseNumeric,\r\n P = BigNumber.prototype = { constructor: BigNumber, toString: null, valueOf: null },\r\n ONE = new BigNumber(1),\r\n\r\n\r\n //----------------------------- EDITABLE CONFIG DEFAULTS -------------------------------\r\n\r\n\r\n // The default values below must be integers within the inclusive ranges stated.\r\n // The values can also be changed at run-time using BigNumber.set.\r\n\r\n // The maximum number of decimal places for operations involving division.\r\n DECIMAL_PLACES = 20, // 0 to MAX\r\n\r\n // The rounding mode used when rounding to the above decimal places, and when using\r\n // toExponential, toFixed, toFormat and toPrecision, and round (default value).\r\n // UP 0 Away from zero.\r\n // DOWN 1 Towards zero.\r\n // CEIL 2 Towards +Infinity.\r\n // FLOOR 3 Towards -Infinity.\r\n // HALF_UP 4 Towards nearest neighbour. If equidistant, up.\r\n // HALF_DOWN 5 Towards nearest neighbour. If equidistant, down.\r\n // HALF_EVEN 6 Towards nearest neighbour. If equidistant, towards even neighbour.\r\n // HALF_CEIL 7 Towards nearest neighbour. If equidistant, towards +Infinity.\r\n // HALF_FLOOR 8 Towards nearest neighbour. If equidistant, towards -Infinity.\r\n ROUNDING_MODE = 4, // 0 to 8\r\n\r\n // EXPONENTIAL_AT : [TO_EXP_NEG , TO_EXP_POS]\r\n\r\n // The exponent value at and beneath which toString returns exponential notation.\r\n // Number type: -7\r\n TO_EXP_NEG = -7, // 0 to -MAX\r\n\r\n // The exponent value at and above which toString returns exponential notation.\r\n // Number type: 21\r\n TO_EXP_POS = 21, // 0 to MAX\r\n\r\n // RANGE : [MIN_EXP, MAX_EXP]\r\n\r\n // The minimum exponent value, beneath which underflow to zero occurs.\r\n // Number type: -324 (5e-324)\r\n MIN_EXP = -1e7, // -1 to -MAX\r\n\r\n // The maximum exponent value, above which overflow to Infinity occurs.\r\n // Number type: 308 (1.7976931348623157e+308)\r\n // For MAX_EXP > 1e7, e.g. new BigNumber('1e100000000').plus(1) may be slow.\r\n MAX_EXP = 1e7, // 1 to MAX\r\n\r\n // Whether to use cryptographically-secure random number generation, if available.\r\n CRYPTO = false, // true or false\r\n\r\n // The modulo mode used when calculating the modulus: a mod n.\r\n // The quotient (q = a / n) is calculated according to the corresponding rounding mode.\r\n // The remainder (r) is calculated as: r = a - n * q.\r\n //\r\n // UP 0 The remainder is positive if the dividend is negative, else is negative.\r\n // DOWN 1 The remainder has the same sign as the dividend.\r\n // This modulo mode is commonly known as 'truncated division' and is\r\n // equivalent to (a % n) in JavaScript.\r\n // FLOOR 3 The remainder has the same sign as the divisor (Python %).\r\n // HALF_EVEN 6 This modulo mode implements the IEEE 754 remainder function.\r\n // EUCLID 9 Euclidian division. q = sign(n) * floor(a / abs(n)).\r\n // The remainder is always positive.\r\n //\r\n // The truncated division, floored division, Euclidian division and IEEE 754 remainder\r\n // modes are commonly used for the modulus operation.\r\n // Although the other rounding modes can also be used, they may not give useful results.\r\n MODULO_MODE = 1, // 0 to 9\r\n\r\n // The maximum number of significant digits of the result of the exponentiatedBy operation.\r\n // If POW_PRECISION is 0, there will be unlimited significant digits.\r\n POW_PRECISION = 0, // 0 to MAX\r\n\r\n // The format specification used by the BigNumber.prototype.toFormat method.\r\n FORMAT = {\r\n prefix: '',\r\n groupSize: 3,\r\n secondaryGroupSize: 0,\r\n groupSeparator: ',',\r\n decimalSeparator: '.',\r\n fractionGroupSize: 0,\r\n fractionGroupSeparator: '\\xA0', // non-breaking space\r\n suffix: ''\r\n },\r\n\r\n // The alphabet used for base conversion. It must be at least 2 characters long, with no '+',\r\n // '-', '.', whitespace, or repeated character.\r\n // '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_'\r\n ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz',\r\n alphabetHasNormalDecimalDigits = true;\r\n\r\n\r\n //------------------------------------------------------------------------------------------\r\n\r\n\r\n // CONSTRUCTOR\r\n\r\n\r\n /*\r\n * The BigNumber constructor and exported function.\r\n * Create and return a new instance of a BigNumber object.\r\n *\r\n * v {number|string|BigNumber} A numeric value.\r\n * [b] {number} The base of v. Integer, 2 to ALPHABET.length inclusive.\r\n */\r\n function BigNumber(v, b) {\r\n var alphabet, c, caseChanged, e, i, isNum, len, str,\r\n x = this;\r\n\r\n // Enable constructor call without `new`.\r\n if (!(x instanceof BigNumber)) return new BigNumber(v, b);\r\n\r\n if (b == null) {\r\n\r\n if (v && v._isBigNumber === true) {\r\n x.s = v.s;\r\n\r\n if (!v.c || v.e > MAX_EXP) {\r\n x.c = x.e = null;\r\n } else if (v.e < MIN_EXP) {\r\n x.c = [x.e = 0];\r\n } else {\r\n x.e = v.e;\r\n x.c = v.c.slice();\r\n }\r\n\r\n return;\r\n }\r\n\r\n if ((isNum = typeof v == 'number') && v * 0 == 0) {\r\n\r\n // Use `1 / n` to handle minus zero also.\r\n x.s = 1 / v < 0 ? (v = -v, -1) : 1;\r\n\r\n // Fast path for integers, where n < 2147483648 (2**31).\r\n if (v === ~~v) {\r\n for (e = 0, i = v; i >= 10; i /= 10, e++);\r\n\r\n if (e > MAX_EXP) {\r\n x.c = x.e = null;\r\n } else {\r\n x.e = e;\r\n x.c = [v];\r\n }\r\n\r\n return;\r\n }\r\n\r\n str = String(v);\r\n } else {\r\n\r\n if (!isNumeric.test(str = String(v))) return parseNumeric(x, str, isNum);\r\n\r\n x.s = str.charCodeAt(0) == 45 ? (str = str.slice(1), -1) : 1;\r\n }\r\n\r\n // Decimal point?\r\n if ((e = str.indexOf('.')) > -1) str = str.replace('.', '');\r\n\r\n // Exponential form?\r\n if ((i = str.search(/e/i)) > 0) {\r\n\r\n // Determine exponent.\r\n if (e < 0) e = i;\r\n e += +str.slice(i + 1);\r\n str = str.substring(0, i);\r\n } else if (e < 0) {\r\n\r\n // Integer.\r\n e = str.length;\r\n }\r\n\r\n } else {\r\n\r\n // '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}'\r\n intCheck(b, 2, ALPHABET.length, 'Base');\r\n\r\n // Allow exponential notation to be used with base 10 argument, while\r\n // also rounding to DECIMAL_PLACES as with other bases.\r\n if (b == 10 && alphabetHasNormalDecimalDigits) {\r\n x = new BigNumber(v);\r\n return round(x, DECIMAL_PLACES + x.e + 1, ROUNDING_MODE);\r\n }\r\n\r\n str = String(v);\r\n\r\n if (isNum = typeof v == 'number') {\r\n\r\n // Avoid potential interpretation of Infinity and NaN as base 44+ values.\r\n if (v * 0 != 0) return parseNumeric(x, str, isNum, b);\r\n\r\n x.s = 1 / v < 0 ? (str = str.slice(1), -1) : 1;\r\n\r\n // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}'\r\n if (BigNumber.DEBUG && str.replace(/^0\\.0*|\\./, '').length > 15) {\r\n throw Error\r\n (tooManyDigits + v);\r\n }\r\n } else {\r\n x.s = str.charCodeAt(0) === 45 ? (str = str.slice(1), -1) : 1;\r\n }\r\n\r\n alphabet = ALPHABET.slice(0, b);\r\n e = i = 0;\r\n\r\n // Check that str is a valid base b number.\r\n // Don't use RegExp, so alphabet can contain special characters.\r\n for (len = str.length; i < len; i++) {\r\n if (alphabet.indexOf(c = str.charAt(i)) < 0) {\r\n if (c == '.') {\r\n\r\n // If '.' is not the first character and it has not be found before.\r\n if (i > e) {\r\n e = len;\r\n continue;\r\n }\r\n } else if (!caseChanged) {\r\n\r\n // Allow e.g. hexadecimal 'FF' as well as 'ff'.\r\n if (str == str.toUpperCase() && (str = str.toLowerCase()) ||\r\n str == str.toLowerCase() && (str = str.toUpperCase())) {\r\n caseChanged = true;\r\n i = -1;\r\n e = 0;\r\n continue;\r\n }\r\n }\r\n\r\n return parseNumeric(x, String(v), isNum, b);\r\n }\r\n }\r\n\r\n // Prevent later check for length on converted number.\r\n isNum = false;\r\n str = convertBase(str, b, 10, x.s);\r\n\r\n // Decimal point?\r\n if ((e = str.indexOf('.')) > -1) str = str.replace('.', '');\r\n else e = str.length;\r\n }\r\n\r\n // Determine leading zeros.\r\n for (i = 0; str.charCodeAt(i) === 48; i++);\r\n\r\n // Determine trailing zeros.\r\n for (len = str.length; str.charCodeAt(--len) === 48;);\r\n\r\n if (str = str.slice(i, ++len)) {\r\n len -= i;\r\n\r\n // '[BigNumber Error] Number primitive has more than 15 significant digits: {n}'\r\n if (isNum && BigNumber.DEBUG &&\r\n len > 15 && (v > MAX_SAFE_INTEGER || v !== mathfloor(v))) {\r\n throw Error\r\n (tooManyDigits + (x.s * v));\r\n }\r\n\r\n // Overflow?\r\n if ((e = e - i - 1) > MAX_EXP) {\r\n\r\n // Infinity.\r\n x.c = x.e = null;\r\n\r\n // Underflow?\r\n } else if (e < MIN_EXP) {\r\n\r\n // Zero.\r\n x.c = [x.e = 0];\r\n } else {\r\n x.e = e;\r\n x.c = [];\r\n\r\n // Transform base\r\n\r\n // e is the base 10 exponent.\r\n // i is where to slice str to get the first element of the coefficient array.\r\n i = (e + 1) % LOG_BASE;\r\n if (e < 0) i += LOG_BASE; // i < 1\r\n\r\n if (i < len) {\r\n if (i) x.c.push(+str.slice(0, i));\r\n\r\n for (len -= LOG_BASE; i < len;) {\r\n x.c.push(+str.slice(i, i += LOG_BASE));\r\n }\r\n\r\n i = LOG_BASE - (str = str.slice(i)).length;\r\n } else {\r\n i -= len;\r\n }\r\n\r\n for (; i--; str += '0');\r\n x.c.push(+str);\r\n }\r\n } else {\r\n\r\n // Zero.\r\n x.c = [x.e = 0];\r\n }\r\n }\r\n\r\n\r\n // CONSTRUCTOR PROPERTIES\r\n\r\n\r\n BigNumber.clone = clone;\r\n\r\n BigNumber.ROUND_UP = 0;\r\n BigNumber.ROUND_DOWN = 1;\r\n BigNumber.ROUND_CEIL = 2;\r\n BigNumber.ROUND_FLOOR = 3;\r\n BigNumber.ROUND_HALF_UP = 4;\r\n BigNumber.ROUND_HALF_DOWN = 5;\r\n BigNumber.ROUND_HALF_EVEN = 6;\r\n BigNumber.ROUND_HALF_CEIL = 7;\r\n BigNumber.ROUND_HALF_FLOOR = 8;\r\n BigNumber.EUCLID = 9;\r\n\r\n\r\n /*\r\n * Configure infrequently-changing library-wide settings.\r\n *\r\n * Accept an object with the following optional properties (if the value of a property is\r\n * a number, it must be an integer within the inclusive range stated):\r\n *\r\n * DECIMAL_PLACES {number} 0 to MAX\r\n * ROUNDING_MODE {number} 0 to 8\r\n * EXPONENTIAL_AT {number|number[]} -MAX to MAX or [-MAX to 0, 0 to MAX]\r\n * RANGE {number|number[]} -MAX to MAX (not zero) or [-MAX to -1, 1 to MAX]\r\n * CRYPTO {boolean} true or false\r\n * MODULO_MODE {number} 0 to 9\r\n * POW_PRECISION {number} 0 to MAX\r\n * ALPHABET {string} A string of two or more unique characters which does\r\n * not contain '.'.\r\n * FORMAT {object} An object with some of the following properties:\r\n * prefix {string}\r\n * groupSize {number}\r\n * secondaryGroupSize {number}\r\n * groupSeparator {string}\r\n * decimalSeparator {string}\r\n * fractionGroupSize {number}\r\n * fractionGroupSeparator {string}\r\n * suffix {string}\r\n *\r\n * (The values assigned to the above FORMAT object properties are not checked for validity.)\r\n *\r\n * E.g.\r\n * BigNumber.config({ DECIMAL_PLACES : 20, ROUNDING_MODE : 4 })\r\n *\r\n * Ignore properties/parameters set to null or undefined, except for ALPHABET.\r\n *\r\n * Return an object with the properties current values.\r\n */\r\n BigNumber.config = BigNumber.set = function (obj) {\r\n var p, v;\r\n\r\n if (obj != null) {\r\n\r\n if (typeof obj == 'object') {\r\n\r\n // DECIMAL_PLACES {number} Integer, 0 to MAX inclusive.\r\n // '[BigNumber Error] DECIMAL_PLACES {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'DECIMAL_PLACES')) {\r\n v = obj[p];\r\n intCheck(v, 0, MAX, p);\r\n DECIMAL_PLACES = v;\r\n }\r\n\r\n // ROUNDING_MODE {number} Integer, 0 to 8 inclusive.\r\n // '[BigNumber Error] ROUNDING_MODE {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'ROUNDING_MODE')) {\r\n v = obj[p];\r\n intCheck(v, 0, 8, p);\r\n ROUNDING_MODE = v;\r\n }\r\n\r\n // EXPONENTIAL_AT {number|number[]}\r\n // Integer, -MAX to MAX inclusive or\r\n // [integer -MAX to 0 inclusive, 0 to MAX inclusive].\r\n // '[BigNumber Error] EXPONENTIAL_AT {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'EXPONENTIAL_AT')) {\r\n v = obj[p];\r\n if (v && v.pop) {\r\n intCheck(v[0], -MAX, 0, p);\r\n intCheck(v[1], 0, MAX, p);\r\n TO_EXP_NEG = v[0];\r\n TO_EXP_POS = v[1];\r\n } else {\r\n intCheck(v, -MAX, MAX, p);\r\n TO_EXP_NEG = -(TO_EXP_POS = v < 0 ? -v : v);\r\n }\r\n }\r\n\r\n // RANGE {number|number[]} Non-zero integer, -MAX to MAX inclusive or\r\n // [integer -MAX to -1 inclusive, integer 1 to MAX inclusive].\r\n // '[BigNumber Error] RANGE {not a primitive number|not an integer|out of range|cannot be zero}: {v}'\r\n if (obj.hasOwnProperty(p = 'RANGE')) {\r\n v = obj[p];\r\n if (v && v.pop) {\r\n intCheck(v[0], -MAX, -1, p);\r\n intCheck(v[1], 1, MAX, p);\r\n MIN_EXP = v[0];\r\n MAX_EXP = v[1];\r\n } else {\r\n intCheck(v, -MAX, MAX, p);\r\n if (v) {\r\n MIN_EXP = -(MAX_EXP = v < 0 ? -v : v);\r\n } else {\r\n throw Error\r\n (bignumberError + p + ' cannot be zero: ' + v);\r\n }\r\n }\r\n }\r\n\r\n // CRYPTO {boolean} true or false.\r\n // '[BigNumber Error] CRYPTO not true or false: {v}'\r\n // '[BigNumber Error] crypto unavailable'\r\n if (obj.hasOwnProperty(p = 'CRYPTO')) {\r\n v = obj[p];\r\n if (v === !!v) {\r\n if (v) {\r\n if (typeof crypto != 'undefined' && crypto &&\r\n (crypto.getRandomValues || crypto.randomBytes)) {\r\n CRYPTO = v;\r\n } else {\r\n CRYPTO = !v;\r\n throw Error\r\n (bignumberError + 'crypto unavailable');\r\n }\r\n } else {\r\n CRYPTO = v;\r\n }\r\n } else {\r\n throw Error\r\n (bignumberError + p + ' not true or false: ' + v);\r\n }\r\n }\r\n\r\n // MODULO_MODE {number} Integer, 0 to 9 inclusive.\r\n // '[BigNumber Error] MODULO_MODE {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'MODULO_MODE')) {\r\n v = obj[p];\r\n intCheck(v, 0, 9, p);\r\n MODULO_MODE = v;\r\n }\r\n\r\n // POW_PRECISION {number} Integer, 0 to MAX inclusive.\r\n // '[BigNumber Error] POW_PRECISION {not a primitive number|not an integer|out of range}: {v}'\r\n if (obj.hasOwnProperty(p = 'POW_PRECISION')) {\r\n v = obj[p];\r\n intCheck(v, 0, MAX, p);\r\n POW_PRECISION = v;\r\n }\r\n\r\n // FORMAT {object}\r\n // '[BigNumber Error] FORMAT not an object: {v}'\r\n if (obj.hasOwnProperty(p = 'FORMAT')) {\r\n v = obj[p];\r\n if (typeof v == 'object') FORMAT = v;\r\n else throw Error\r\n (bignumberError + p + ' not an object: ' + v);\r\n }\r\n\r\n // ALPHABET {string}\r\n // '[BigNumber Error] ALPHABET invalid: {v}'\r\n if (obj.hasOwnProperty(p = 'ALPHABET')) {\r\n v = obj[p];\r\n\r\n // Disallow if less than two characters,\r\n // or if it contains '+', '-', '.', whitespace, or a repeated character.\r\n if (typeof v == 'string' && !/^.?$|[+\\-.\\s]|(.).*\\1/.test(v)) {\r\n alphabetHasNormalDecimalDigits = v.slice(0, 10) == '0123456789';\r\n ALPHABET = v;\r\n } else {\r\n throw Error\r\n (bignumberError + p + ' invalid: ' + v);\r\n }\r\n }\r\n\r\n } else {\r\n\r\n // '[BigNumber Error] Object expected: {v}'\r\n throw Error\r\n (bignumberError + 'Object expected: ' + obj);\r\n }\r\n }\r\n\r\n return {\r\n DECIMAL_PLACES: DECIMAL_PLACES,\r\n ROUNDING_MODE: ROUNDING_MODE,\r\n EXPONENTIAL_AT: [TO_EXP_NEG, TO_EXP_POS],\r\n RANGE: [MIN_EXP, MAX_EXP],\r\n CRYPTO: CRYPTO,\r\n MODULO_MODE: MODULO_MODE,\r\n POW_PRECISION: POW_PRECISION,\r\n FORMAT: FORMAT,\r\n ALPHABET: ALPHABET\r\n };\r\n };\r\n\r\n\r\n /*\r\n * Return true if v is a BigNumber instance, otherwise return false.\r\n *\r\n * If BigNumber.DEBUG is true, throw if a BigNumber instance is not well-formed.\r\n *\r\n * v {any}\r\n *\r\n * '[BigNumber Error] Invalid BigNumber: {v}'\r\n */\r\n BigNumber.isBigNumber = function (v) {\r\n if (!v || v._isBigNumber !== true) return false;\r\n if (!BigNumber.DEBUG) return true;\r\n\r\n var i, n,\r\n c = v.c,\r\n e = v.e,\r\n s = v.s;\r\n\r\n out: if ({}.toString.call(c) == '[object Array]') {\r\n\r\n if ((s === 1 || s === -1) && e >= -MAX && e <= MAX && e === mathfloor(e)) {\r\n\r\n // If the first element is zero, the BigNumber value must be zero.\r\n if (c[0] === 0) {\r\n if (e === 0 && c.length === 1) return true;\r\n break out;\r\n }\r\n\r\n // Calculate number of digits that c[0] should have, based on the exponent.\r\n i = (e + 1) % LOG_BASE;\r\n if (i < 1) i += LOG_BASE;\r\n\r\n // Calculate number of digits of c[0].\r\n //if (Math.ceil(Math.log(c[0] + 1) / Math.LN10) == i) {\r\n if (String(c[0]).length == i) {\r\n\r\n for (i = 0; i < c.length; i++) {\r\n n = c[i];\r\n if (n < 0 || n >= BASE || n !== mathfloor(n)) break out;\r\n }\r\n\r\n // Last element cannot be zero, unless it is the only element.\r\n if (n !== 0) return true;\r\n }\r\n }\r\n\r\n // Infinity/NaN\r\n } else if (c === null && e === null && (s === null || s === 1 || s === -1)) {\r\n return true;\r\n }\r\n\r\n throw Error\r\n (bignumberError + 'Invalid BigNumber: ' + v);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the maximum of the arguments.\r\n *\r\n * arguments {number|string|BigNumber}\r\n */\r\n BigNumber.maximum = BigNumber.max = function () {\r\n return maxOrMin(arguments, -1);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the minimum of the arguments.\r\n *\r\n * arguments {number|string|BigNumber}\r\n */\r\n BigNumber.minimum = BigNumber.min = function () {\r\n return maxOrMin(arguments, 1);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber with a random value equal to or greater than 0 and less than 1,\r\n * and with dp, or DECIMAL_PLACES if dp is omitted, decimal places (or less if trailing\r\n * zeros are produced).\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp}'\r\n * '[BigNumber Error] crypto unavailable'\r\n */\r\n BigNumber.random = (function () {\r\n var pow2_53 = 0x20000000000000;\r\n\r\n // Return a 53 bit integer n, where 0 <= n < 9007199254740992.\r\n // Check if Math.random() produces more than 32 bits of randomness.\r\n // If it does, assume at least 53 bits are produced, otherwise assume at least 30 bits.\r\n // 0x40000000 is 2^30, 0x800000 is 2^23, 0x1fffff is 2^21 - 1.\r\n var random53bitInt = (Math.random() * pow2_53) & 0x1fffff\r\n ? function () { return mathfloor(Math.random() * pow2_53); }\r\n : function () { return ((Math.random() * 0x40000000 | 0) * 0x800000) +\r\n (Math.random() * 0x800000 | 0); };\r\n\r\n return function (dp) {\r\n var a, b, e, k, v,\r\n i = 0,\r\n c = [],\r\n rand = new BigNumber(ONE);\r\n\r\n if (dp == null) dp = DECIMAL_PLACES;\r\n else intCheck(dp, 0, MAX);\r\n\r\n k = mathceil(dp / LOG_BASE);\r\n\r\n if (CRYPTO) {\r\n\r\n // Browsers supporting crypto.getRandomValues.\r\n if (crypto.getRandomValues) {\r\n\r\n a = crypto.getRandomValues(new Uint32Array(k *= 2));\r\n\r\n for (; i < k;) {\r\n\r\n // 53 bits:\r\n // ((Math.pow(2, 32) - 1) * Math.pow(2, 21)).toString(2)\r\n // 11111 11111111 11111111 11111111 11100000 00000000 00000000\r\n // ((Math.pow(2, 32) - 1) >>> 11).toString(2)\r\n // 11111 11111111 11111111\r\n // 0x20000 is 2^21.\r\n v = a[i] * 0x20000 + (a[i + 1] >>> 11);\r\n\r\n // Rejection sampling:\r\n // 0 <= v < 9007199254740992\r\n // Probability that v >= 9e15, is\r\n // 7199254740992 / 9007199254740992 ~= 0.0008, i.e. 1 in 1251\r\n if (v >= 9e15) {\r\n b = crypto.getRandomValues(new Uint32Array(2));\r\n a[i] = b[0];\r\n a[i + 1] = b[1];\r\n } else {\r\n\r\n // 0 <= v <= 8999999999999999\r\n // 0 <= (v % 1e14) <= 99999999999999\r\n c.push(v % 1e14);\r\n i += 2;\r\n }\r\n }\r\n i = k / 2;\r\n\r\n // Node.js supporting crypto.randomBytes.\r\n } else if (crypto.randomBytes) {\r\n\r\n // buffer\r\n a = crypto.randomBytes(k *= 7);\r\n\r\n for (; i < k;) {\r\n\r\n // 0x1000000000000 is 2^48, 0x10000000000 is 2^40\r\n // 0x100000000 is 2^32, 0x1000000 is 2^24\r\n // 11111 11111111 11111111 11111111 11111111 11111111 11111111\r\n // 0 <= v < 9007199254740992\r\n v = ((a[i] & 31) * 0x1000000000000) + (a[i + 1] * 0x10000000000) +\r\n (a[i + 2] * 0x100000000) + (a[i + 3] * 0x1000000) +\r\n (a[i + 4] << 16) + (a[i + 5] << 8) + a[i + 6];\r\n\r\n if (v >= 9e15) {\r\n crypto.randomBytes(7).copy(a, i);\r\n } else {\r\n\r\n // 0 <= (v % 1e14) <= 99999999999999\r\n c.push(v % 1e14);\r\n i += 7;\r\n }\r\n }\r\n i = k / 7;\r\n } else {\r\n CRYPTO = false;\r\n throw Error\r\n (bignumberError + 'crypto unavailable');\r\n }\r\n }\r\n\r\n // Use Math.random.\r\n if (!CRYPTO) {\r\n\r\n for (; i < k;) {\r\n v = random53bitInt();\r\n if (v < 9e15) c[i++] = v % 1e14;\r\n }\r\n }\r\n\r\n k = c[--i];\r\n dp %= LOG_BASE;\r\n\r\n // Convert trailing digits to zeros according to dp.\r\n if (k && dp) {\r\n v = POWS_TEN[LOG_BASE - dp];\r\n c[i] = mathfloor(k / v) * v;\r\n }\r\n\r\n // Remove trailing elements which are zero.\r\n for (; c[i] === 0; c.pop(), i--);\r\n\r\n // Zero?\r\n if (i < 0) {\r\n c = [e = 0];\r\n } else {\r\n\r\n // Remove leading elements which are zero and adjust exponent accordingly.\r\n for (e = -1 ; c[0] === 0; c.splice(0, 1), e -= LOG_BASE);\r\n\r\n // Count the digits of the first element of c to determine leading zeros, and...\r\n for (i = 1, v = c[0]; v >= 10; v /= 10, i++);\r\n\r\n // adjust the exponent accordingly.\r\n if (i < LOG_BASE) e -= LOG_BASE - i;\r\n }\r\n\r\n rand.e = e;\r\n rand.c = c;\r\n return rand;\r\n };\r\n })();\r\n\r\n\r\n /*\r\n * Return a BigNumber whose value is the sum of the arguments.\r\n *\r\n * arguments {number|string|BigNumber}\r\n */\r\n BigNumber.sum = function () {\r\n var i = 1,\r\n args = arguments,\r\n sum = new BigNumber(args[0]);\r\n for (; i < args.length;) sum = sum.plus(args[i++]);\r\n return sum;\r\n };\r\n\r\n\r\n // PRIVATE FUNCTIONS\r\n\r\n\r\n // Called by BigNumber and BigNumber.prototype.toString.\r\n convertBase = (function () {\r\n var decimal = '0123456789';\r\n\r\n /*\r\n * Convert string of baseIn to an array of numbers of baseOut.\r\n * Eg. toBaseOut('255', 10, 16) returns [15, 15].\r\n * Eg. toBaseOut('ff', 16, 10) returns [2, 5, 5].\r\n */\r\n function toBaseOut(str, baseIn, baseOut, alphabet) {\r\n var j,\r\n arr = [0],\r\n arrL,\r\n i = 0,\r\n len = str.length;\r\n\r\n for (; i < len;) {\r\n for (arrL = arr.length; arrL--; arr[arrL] *= baseIn);\r\n\r\n arr[0] += alphabet.indexOf(str.charAt(i++));\r\n\r\n for (j = 0; j < arr.length; j++) {\r\n\r\n if (arr[j] > baseOut - 1) {\r\n if (arr[j + 1] == null) arr[j + 1] = 0;\r\n arr[j + 1] += arr[j] / baseOut | 0;\r\n arr[j] %= baseOut;\r\n }\r\n }\r\n }\r\n\r\n return arr.reverse();\r\n }\r\n\r\n // Convert a numeric string of baseIn to a numeric string of baseOut.\r\n // If the caller is toString, we are converting from base 10 to baseOut.\r\n // If the caller is BigNumber, we are converting from baseIn to base 10.\r\n return function (str, baseIn, baseOut, sign, callerIsToString) {\r\n var alphabet, d, e, k, r, x, xc, y,\r\n i = str.indexOf('.'),\r\n dp = DECIMAL_PLACES,\r\n rm = ROUNDING_MODE;\r\n\r\n // Non-integer.\r\n if (i >= 0) {\r\n k = POW_PRECISION;\r\n\r\n // Unlimited precision.\r\n POW_PRECISION = 0;\r\n str = str.replace('.', '');\r\n y = new BigNumber(baseIn);\r\n x = y.pow(str.length - i);\r\n POW_PRECISION = k;\r\n\r\n // Convert str as if an integer, then restore the fraction part by dividing the\r\n // result by its base raised to a power.\r\n\r\n y.c = toBaseOut(toFixedPoint(coeffToString(x.c), x.e, '0'),\r\n 10, baseOut, decimal);\r\n y.e = y.c.length;\r\n }\r\n\r\n // Convert the number as integer.\r\n\r\n xc = toBaseOut(str, baseIn, baseOut, callerIsToString\r\n ? (alphabet = ALPHABET, decimal)\r\n : (alphabet = decimal, ALPHABET));\r\n\r\n // xc now represents str as an integer and converted to baseOut. e is the exponent.\r\n e = k = xc.length;\r\n\r\n // Remove trailing zeros.\r\n for (; xc[--k] == 0; xc.pop());\r\n\r\n // Zero?\r\n if (!xc[0]) return alphabet.charAt(0);\r\n\r\n // Does str represent an integer? If so, no need for the division.\r\n if (i < 0) {\r\n --e;\r\n } else {\r\n x.c = xc;\r\n x.e = e;\r\n\r\n // The sign is needed for correct rounding.\r\n x.s = sign;\r\n x = div(x, y, dp, rm, baseOut);\r\n xc = x.c;\r\n r = x.r;\r\n e = x.e;\r\n }\r\n\r\n // xc now represents str converted to baseOut.\r\n\r\n // The index of the rounding digit.\r\n d = e + dp + 1;\r\n\r\n // The rounding digit: the digit to the right of the digit that may be rounded up.\r\n i = xc[d];\r\n\r\n // Look at the rounding digits and mode to determine whether to round up.\r\n\r\n k = baseOut / 2;\r\n r = r || d < 0 || xc[d + 1] != null;\r\n\r\n r = rm < 4 ? (i != null || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2))\r\n : i > k || i == k &&(rm == 4 || r || rm == 6 && xc[d - 1] & 1 ||\r\n rm == (x.s < 0 ? 8 : 7));\r\n\r\n // If the index of the rounding digit is not greater than zero, or xc represents\r\n // zero, then the result of the base conversion is zero or, if rounding up, a value\r\n // such as 0.00001.\r\n if (d < 1 || !xc[0]) {\r\n\r\n // 1^-dp or 0\r\n str = r ? toFixedPoint(alphabet.charAt(1), -dp, alphabet.charAt(0)) : alphabet.charAt(0);\r\n } else {\r\n\r\n // Truncate xc to the required number of decimal places.\r\n xc.length = d;\r\n\r\n // Round up?\r\n if (r) {\r\n\r\n // Rounding up may mean the previous digit has to be rounded up and so on.\r\n for (--baseOut; ++xc[--d] > baseOut;) {\r\n xc[d] = 0;\r\n\r\n if (!d) {\r\n ++e;\r\n xc = [1].concat(xc);\r\n }\r\n }\r\n }\r\n\r\n // Determine trailing zeros.\r\n for (k = xc.length; !xc[--k];);\r\n\r\n // E.g. [4, 11, 15] becomes 4bf.\r\n for (i = 0, str = ''; i <= k; str += alphabet.charAt(xc[i++]));\r\n\r\n // Add leading zeros, decimal point and trailing zeros as required.\r\n str = toFixedPoint(str, e, alphabet.charAt(0));\r\n }\r\n\r\n // The caller will add the sign.\r\n return str;\r\n };\r\n })();\r\n\r\n\r\n // Perform division in the specified base. Called by div and convertBase.\r\n div = (function () {\r\n\r\n // Assume non-zero x and k.\r\n function multiply(x, k, base) {\r\n var m, temp, xlo, xhi,\r\n carry = 0,\r\n i = x.length,\r\n klo = k % SQRT_BASE,\r\n khi = k / SQRT_BASE | 0;\r\n\r\n for (x = x.slice(); i--;) {\r\n xlo = x[i] % SQRT_BASE;\r\n xhi = x[i] / SQRT_BASE | 0;\r\n m = khi * xlo + xhi * klo;\r\n temp = klo * xlo + ((m % SQRT_BASE) * SQRT_BASE) + carry;\r\n carry = (temp / base | 0) + (m / SQRT_BASE | 0) + khi * xhi;\r\n x[i] = temp % base;\r\n }\r\n\r\n if (carry) x = [carry].concat(x);\r\n\r\n return x;\r\n }\r\n\r\n function compare(a, b, aL, bL) {\r\n var i, cmp;\r\n\r\n if (aL != bL) {\r\n cmp = aL > bL ? 1 : -1;\r\n } else {\r\n\r\n for (i = cmp = 0; i < aL; i++) {\r\n\r\n if (a[i] != b[i]) {\r\n cmp = a[i] > b[i] ? 1 : -1;\r\n break;\r\n }\r\n }\r\n }\r\n\r\n return cmp;\r\n }\r\n\r\n function subtract(a, b, aL, base) {\r\n var i = 0;\r\n\r\n // Subtract b from a.\r\n for (; aL--;) {\r\n a[aL] -= i;\r\n i = a[aL] < b[aL] ? 1 : 0;\r\n a[aL] = i * base + a[aL] - b[aL];\r\n }\r\n\r\n // Remove leading zeros.\r\n for (; !a[0] && a.length > 1; a.splice(0, 1));\r\n }\r\n\r\n // x: dividend, y: divisor.\r\n return function (x, y, dp, rm, base) {\r\n var cmp, e, i, more, n, prod, prodL, q, qc, rem, remL, rem0, xi, xL, yc0,\r\n yL, yz,\r\n s = x.s == y.s ? 1 : -1,\r\n xc = x.c,\r\n yc = y.c;\r\n\r\n // Either NaN, Infinity or 0?\r\n if (!xc || !xc[0] || !yc || !yc[0]) {\r\n\r\n return new BigNumber(\r\n\r\n // Return NaN if either NaN, or both Infinity or 0.\r\n !x.s || !y.s || (xc ? yc && xc[0] == yc[0] : !yc) ? NaN :\r\n\r\n // Return \u00B10 if x is \u00B10 or y is \u00B1Infinity, or return \u00B1Infinity as y is \u00B10.\r\n xc && xc[0] == 0 || !yc ? s * 0 : s / 0\r\n );\r\n }\r\n\r\n q = new BigNumber(s);\r\n qc = q.c = [];\r\n e = x.e - y.e;\r\n s = dp + e + 1;\r\n\r\n if (!base) {\r\n base = BASE;\r\n e = bitFloor(x.e / LOG_BASE) - bitFloor(y.e / LOG_BASE);\r\n s = s / LOG_BASE | 0;\r\n }\r\n\r\n // Result exponent may be one less then the current value of e.\r\n // The coefficients of the BigNumbers from convertBase may have trailing zeros.\r\n for (i = 0; yc[i] == (xc[i] || 0); i++);\r\n\r\n if (yc[i] > (xc[i] || 0)) e--;\r\n\r\n if (s < 0) {\r\n qc.push(1);\r\n more = true;\r\n } else {\r\n xL = xc.length;\r\n yL = yc.length;\r\n i = 0;\r\n s += 2;\r\n\r\n // Normalise xc and yc so highest order digit of yc is >= base / 2.\r\n\r\n n = mathfloor(base / (yc[0] + 1));\r\n\r\n // Not necessary, but to handle odd bases where yc[0] == (base / 2) - 1.\r\n // if (n > 1 || n++ == 1 && yc[0] < base / 2) {\r\n if (n > 1) {\r\n yc = multiply(yc, n, base);\r\n xc = multiply(xc, n, base);\r\n yL = yc.length;\r\n xL = xc.length;\r\n }\r\n\r\n xi = yL;\r\n rem = xc.slice(0, yL);\r\n remL = rem.length;\r\n\r\n // Add zeros to make remainder as long as divisor.\r\n for (; remL < yL; rem[remL++] = 0);\r\n yz = yc.slice();\r\n yz = [0].concat(yz);\r\n yc0 = yc[0];\r\n if (yc[1] >= base / 2) yc0++;\r\n // Not necessary, but to prevent trial digit n > base, when using base 3.\r\n // else if (base == 3 && yc0 == 1) yc0 = 1 + 1e-15;\r\n\r\n do {\r\n n = 0;\r\n\r\n // Compare divisor and remainder.\r\n cmp = compare(yc, rem, yL, remL);\r\n\r\n // If divisor < remainder.\r\n if (cmp < 0) {\r\n\r\n // Calculate trial digit, n.\r\n\r\n rem0 = rem[0];\r\n if (yL != remL) rem0 = rem0 * base + (rem[1] || 0);\r\n\r\n // n is how many times the divisor goes into the current remainder.\r\n n = mathfloor(rem0 / yc0);\r\n\r\n // Algorithm:\r\n // product = divisor multiplied by trial digit (n).\r\n // Compare product and remainder.\r\n // If product is greater than remainder:\r\n // Subtract divisor from product, decrement trial digit.\r\n // Subtract product from remainder.\r\n // If product was less than remainder at the last compare:\r\n // Compare new remainder and divisor.\r\n // If remainder is greater than divisor:\r\n // Subtract divisor from remainder, increment trial digit.\r\n\r\n if (n > 1) {\r\n\r\n // n may be > base only when base is 3.\r\n if (n >= base) n = base - 1;\r\n\r\n // product = divisor * trial digit.\r\n prod = multiply(yc, n, base);\r\n prodL = prod.length;\r\n remL = rem.length;\r\n\r\n // Compare product and remainder.\r\n // If product > remainder then trial digit n too high.\r\n // n is 1 too high about 5% of the time, and is not known to have\r\n // ever been more than 1 too high.\r\n while (compare(prod, rem, prodL, remL) == 1) {\r\n n--;\r\n\r\n // Subtract divisor from product.\r\n subtract(prod, yL < prodL ? yz : yc, prodL, base);\r\n prodL = prod.length;\r\n cmp = 1;\r\n }\r\n } else {\r\n\r\n // n is 0 or 1, cmp is -1.\r\n // If n is 0, there is no need to compare yc and rem again below,\r\n // so change cmp to 1 to avoid it.\r\n // If n is 1, leave cmp as -1, so yc and rem are compared again.\r\n if (n == 0) {\r\n\r\n // divisor < remainder, so n must be at least 1.\r\n cmp = n = 1;\r\n }\r\n\r\n // product = divisor\r\n prod = yc.slice();\r\n prodL = prod.length;\r\n }\r\n\r\n if (prodL < remL) prod = [0].concat(prod);\r\n\r\n // Subtract product from remainder.\r\n subtract(rem, prod, remL, base);\r\n remL = rem.length;\r\n\r\n // If product was < remainder.\r\n if (cmp == -1) {\r\n\r\n // Compare divisor and new remainder.\r\n // If divisor < new remainder, subtract divisor from remainder.\r\n // Trial digit n too low.\r\n // n is 1 too low about 5% of the time, and very rarely 2 too low.\r\n while (compare(yc, rem, yL, remL) < 1) {\r\n n++;\r\n\r\n // Subtract divisor from remainder.\r\n subtract(rem, yL < remL ? yz : yc, remL, base);\r\n remL = rem.length;\r\n }\r\n }\r\n } else if (cmp === 0) {\r\n n++;\r\n rem = [0];\r\n } // else cmp === 1 and n will be 0\r\n\r\n // Add the next digit, n, to the result array.\r\n qc[i++] = n;\r\n\r\n // Update the remainder.\r\n if (rem[0]) {\r\n rem[remL++] = xc[xi] || 0;\r\n } else {\r\n rem = [xc[xi]];\r\n remL = 1;\r\n }\r\n } while ((xi++ < xL || rem[0] != null) && s--);\r\n\r\n more = rem[0] != null;\r\n\r\n // Leading zero?\r\n if (!qc[0]) qc.splice(0, 1);\r\n }\r\n\r\n if (base == BASE) {\r\n\r\n // To calculate q.e, first get the number of digits of qc[0].\r\n for (i = 1, s = qc[0]; s >= 10; s /= 10, i++);\r\n\r\n round(q, dp + (q.e = i + e * LOG_BASE - 1) + 1, rm, more);\r\n\r\n // Caller is convertBase.\r\n } else {\r\n q.e = e;\r\n q.r = +more;\r\n }\r\n\r\n return q;\r\n };\r\n })();\r\n\r\n\r\n /*\r\n * Return a string representing the value of BigNumber n in fixed-point or exponential\r\n * notation rounded to the specified decimal places or significant digits.\r\n *\r\n * n: a BigNumber.\r\n * i: the index of the last digit required (i.e. the digit that may be rounded up).\r\n * rm: the rounding mode.\r\n * id: 1 (toExponential) or 2 (toPrecision).\r\n */\r\n function format(n, i, rm, id) {\r\n var c0, e, ne, len, str;\r\n\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n\r\n if (!n.c) return n.toString();\r\n\r\n c0 = n.c[0];\r\n ne = n.e;\r\n\r\n if (i == null) {\r\n str = coeffToString(n.c);\r\n str = id == 1 || id == 2 && (ne <= TO_EXP_NEG || ne >= TO_EXP_POS)\r\n ? toExponential(str, ne)\r\n : toFixedPoint(str, ne, '0');\r\n } else {\r\n n = round(new BigNumber(n), i, rm);\r\n\r\n // n.e may have changed if the value was rounded up.\r\n e = n.e;\r\n\r\n str = coeffToString(n.c);\r\n len = str.length;\r\n\r\n // toPrecision returns exponential notation if the number of significant digits\r\n // specified is less than the number of digits necessary to represent the integer\r\n // part of the value in fixed-point notation.\r\n\r\n // Exponential notation.\r\n if (id == 1 || id == 2 && (i <= e || e <= TO_EXP_NEG)) {\r\n\r\n // Append zeros?\r\n for (; len < i; str += '0', len++);\r\n str = toExponential(str, e);\r\n\r\n // Fixed-point notation.\r\n } else {\r\n i -= ne + (id === 2 && e > ne);\r\n str = toFixedPoint(str, e, '0');\r\n\r\n // Append zeros?\r\n if (e + 1 > len) {\r\n if (--i > 0) for (str += '.'; i--; str += '0');\r\n } else {\r\n i += e - len;\r\n if (i > 0) {\r\n if (e + 1 == len) str += '.';\r\n for (; i--; str += '0');\r\n }\r\n }\r\n }\r\n }\r\n\r\n return n.s < 0 && c0 ? '-' + str : str;\r\n }\r\n\r\n\r\n // Handle BigNumber.max and BigNumber.min.\r\n // If any number is NaN, return NaN.\r\n function maxOrMin(args, n) {\r\n var k, y,\r\n i = 1,\r\n x = new BigNumber(args[0]);\r\n\r\n for (; i < args.length; i++) {\r\n y = new BigNumber(args[i]);\r\n if (!y.s || (k = compare(x, y)) === n || k === 0 && x.s === n) {\r\n x = y;\r\n }\r\n }\r\n\r\n return x;\r\n }\r\n\r\n\r\n /*\r\n * Strip trailing zeros, calculate base 10 exponent and check against MIN_EXP and MAX_EXP.\r\n * Called by minus, plus and times.\r\n */\r\n function normalise(n, c, e) {\r\n var i = 1,\r\n j = c.length;\r\n\r\n // Remove trailing zeros.\r\n for (; !c[--j]; c.pop());\r\n\r\n // Calculate the base 10 exponent. First get the number of digits of c[0].\r\n for (j = c[0]; j >= 10; j /= 10, i++);\r\n\r\n // Overflow?\r\n if ((e = i + e * LOG_BASE - 1) > MAX_EXP) {\r\n\r\n // Infinity.\r\n n.c = n.e = null;\r\n\r\n // Underflow?\r\n } else if (e < MIN_EXP) {\r\n\r\n // Zero.\r\n n.c = [n.e = 0];\r\n } else {\r\n n.e = e;\r\n n.c = c;\r\n }\r\n\r\n return n;\r\n }\r\n\r\n\r\n // Handle values that fail the validity test in BigNumber.\r\n parseNumeric = (function () {\r\n var basePrefix = /^(-?)0([xbo])(?=\\w[\\w.]*$)/i,\r\n dotAfter = /^([^.]+)\\.$/,\r\n dotBefore = /^\\.([^.]+)$/,\r\n isInfinityOrNaN = /^-?(Infinity|NaN)$/,\r\n whitespaceOrPlus = /^\\s*\\+(?=[\\w.])|^\\s+|\\s+$/g;\r\n\r\n return function (x, str, isNum, b) {\r\n var base,\r\n s = isNum ? str : str.replace(whitespaceOrPlus, '');\r\n\r\n // No exception on \u00B1Infinity or NaN.\r\n if (isInfinityOrNaN.test(s)) {\r\n x.s = isNaN(s) ? null : s < 0 ? -1 : 1;\r\n } else {\r\n if (!isNum) {\r\n\r\n // basePrefix = /^(-?)0([xbo])(?=\\w[\\w.]*$)/i\r\n s = s.replace(basePrefix, function (m, p1, p2) {\r\n base = (p2 = p2.toLowerCase()) == 'x' ? 16 : p2 == 'b' ? 2 : 8;\r\n return !b || b == base ? p1 : m;\r\n });\r\n\r\n if (b) {\r\n base = b;\r\n\r\n // E.g. '1.' to '1', '.1' to '0.1'\r\n s = s.replace(dotAfter, '$1').replace(dotBefore, '0.$1');\r\n }\r\n\r\n if (str != s) return new BigNumber(s, base);\r\n }\r\n\r\n // '[BigNumber Error] Not a number: {n}'\r\n // '[BigNumber Error] Not a base {b} number: {n}'\r\n if (BigNumber.DEBUG) {\r\n throw Error\r\n (bignumberError + 'Not a' + (b ? ' base ' + b : '') + ' number: ' + str);\r\n }\r\n\r\n // NaN\r\n x.s = null;\r\n }\r\n\r\n x.c = x.e = null;\r\n }\r\n })();\r\n\r\n\r\n /*\r\n * Round x to sd significant digits using rounding mode rm. Check for over/under-flow.\r\n * If r is truthy, it is known that there are more digits after the rounding digit.\r\n */\r\n function round(x, sd, rm, r) {\r\n var d, i, j, k, n, ni, rd,\r\n xc = x.c,\r\n pows10 = POWS_TEN;\r\n\r\n // if x is not Infinity or NaN...\r\n if (xc) {\r\n\r\n // rd is the rounding digit, i.e. the digit after the digit that may be rounded up.\r\n // n is a base 1e14 number, the value of the element of array x.c containing rd.\r\n // ni is the index of n within x.c.\r\n // d is the number of digits of n.\r\n // i is the index of rd within n including leading zeros.\r\n // j is the actual index of rd within n (if < 0, rd is a leading zero).\r\n out: {\r\n\r\n // Get the number of digits of the first element of xc.\r\n for (d = 1, k = xc[0]; k >= 10; k /= 10, d++);\r\n i = sd - d;\r\n\r\n // If the rounding digit is in the first element of xc...\r\n if (i < 0) {\r\n i += LOG_BASE;\r\n j = sd;\r\n n = xc[ni = 0];\r\n\r\n // Get the rounding digit at index j of n.\r\n rd = mathfloor(n / pows10[d - j - 1] % 10);\r\n } else {\r\n ni = mathceil((i + 1) / LOG_BASE);\r\n\r\n if (ni >= xc.length) {\r\n\r\n if (r) {\r\n\r\n // Needed by sqrt.\r\n for (; xc.length <= ni; xc.push(0));\r\n n = rd = 0;\r\n d = 1;\r\n i %= LOG_BASE;\r\n j = i - LOG_BASE + 1;\r\n } else {\r\n break out;\r\n }\r\n } else {\r\n n = k = xc[ni];\r\n\r\n // Get the number of digits of n.\r\n for (d = 1; k >= 10; k /= 10, d++);\r\n\r\n // Get the index of rd within n.\r\n i %= LOG_BASE;\r\n\r\n // Get the index of rd within n, adjusted for leading zeros.\r\n // The number of leading zeros of n is given by LOG_BASE - d.\r\n j = i - LOG_BASE + d;\r\n\r\n // Get the rounding digit at index j of n.\r\n rd = j < 0 ? 0 : mathfloor(n / pows10[d - j - 1] % 10);\r\n }\r\n }\r\n\r\n r = r || sd < 0 ||\r\n\r\n // Are there any non-zero digits after the rounding digit?\r\n // The expression n % pows10[d - j - 1] returns all digits of n to the right\r\n // of the digit at j, e.g. if n is 908714 and j is 2, the expression gives 714.\r\n xc[ni + 1] != null || (j < 0 ? n : n % pows10[d - j - 1]);\r\n\r\n r = rm < 4\r\n ? (rd || r) && (rm == 0 || rm == (x.s < 0 ? 3 : 2))\r\n : rd > 5 || rd == 5 && (rm == 4 || r || rm == 6 &&\r\n\r\n // Check whether the digit to the left of the rounding digit is odd.\r\n ((i > 0 ? j > 0 ? n / pows10[d - j] : 0 : xc[ni - 1]) % 10) & 1 ||\r\n rm == (x.s < 0 ? 8 : 7));\r\n\r\n if (sd < 1 || !xc[0]) {\r\n xc.length = 0;\r\n\r\n if (r) {\r\n\r\n // Convert sd to decimal places.\r\n sd -= x.e + 1;\r\n\r\n // 1, 0.1, 0.01, 0.001, 0.0001 etc.\r\n xc[0] = pows10[(LOG_BASE - sd % LOG_BASE) % LOG_BASE];\r\n x.e = -sd || 0;\r\n } else {\r\n\r\n // Zero.\r\n xc[0] = x.e = 0;\r\n }\r\n\r\n return x;\r\n }\r\n\r\n // Remove excess digits.\r\n if (i == 0) {\r\n xc.length = ni;\r\n k = 1;\r\n ni--;\r\n } else {\r\n xc.length = ni + 1;\r\n k = pows10[LOG_BASE - i];\r\n\r\n // E.g. 56700 becomes 56000 if 7 is the rounding digit.\r\n // j > 0 means i > number of leading zeros of n.\r\n xc[ni] = j > 0 ? mathfloor(n / pows10[d - j] % pows10[j]) * k : 0;\r\n }\r\n\r\n // Round up?\r\n if (r) {\r\n\r\n for (; ;) {\r\n\r\n // If the digit to be rounded up is in the first element of xc...\r\n if (ni == 0) {\r\n\r\n // i will be the length of xc[0] before k is added.\r\n for (i = 1, j = xc[0]; j >= 10; j /= 10, i++);\r\n j = xc[0] += k;\r\n for (k = 1; j >= 10; j /= 10, k++);\r\n\r\n // if i != k the length has increased.\r\n if (i != k) {\r\n x.e++;\r\n if (xc[0] == BASE) xc[0] = 1;\r\n }\r\n\r\n break;\r\n } else {\r\n xc[ni] += k;\r\n if (xc[ni] != BASE) break;\r\n xc[ni--] = 0;\r\n k = 1;\r\n }\r\n }\r\n }\r\n\r\n // Remove trailing zeros.\r\n for (i = xc.length; xc[--i] === 0; xc.pop());\r\n }\r\n\r\n // Overflow? Infinity.\r\n if (x.e > MAX_EXP) {\r\n x.c = x.e = null;\r\n\r\n // Underflow? Zero.\r\n } else if (x.e < MIN_EXP) {\r\n x.c = [x.e = 0];\r\n }\r\n }\r\n\r\n return x;\r\n }\r\n\r\n\r\n function valueOf(n) {\r\n var str,\r\n e = n.e;\r\n\r\n if (e === null) return n.toString();\r\n\r\n str = coeffToString(n.c);\r\n\r\n str = e <= TO_EXP_NEG || e >= TO_EXP_POS\r\n ? toExponential(str, e)\r\n : toFixedPoint(str, e, '0');\r\n\r\n return n.s < 0 ? '-' + str : str;\r\n }\r\n\r\n\r\n // PROTOTYPE/INSTANCE METHODS\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the absolute value of this BigNumber.\r\n */\r\n P.absoluteValue = P.abs = function () {\r\n var x = new BigNumber(this);\r\n if (x.s < 0) x.s = 1;\r\n return x;\r\n };\r\n\r\n\r\n /*\r\n * Return\r\n * 1 if the value of this BigNumber is greater than the value of BigNumber(y, b),\r\n * -1 if the value of this BigNumber is less than the value of BigNumber(y, b),\r\n * 0 if they have the same value,\r\n * or null if the value of either is NaN.\r\n */\r\n P.comparedTo = function (y, b) {\r\n return compare(this, new BigNumber(y, b));\r\n };\r\n\r\n\r\n /*\r\n * If dp is undefined or null or true or false, return the number of decimal places of the\r\n * value of this BigNumber, or null if the value of this BigNumber is \u00B1Infinity or NaN.\r\n *\r\n * Otherwise, if dp is a number, return a new BigNumber whose value is the value of this\r\n * BigNumber rounded to a maximum of dp decimal places using rounding mode rm, or\r\n * ROUNDING_MODE if rm is omitted.\r\n *\r\n * [dp] {number} Decimal places: integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n */\r\n P.decimalPlaces = P.dp = function (dp, rm) {\r\n var c, n, v,\r\n x = this;\r\n\r\n if (dp != null) {\r\n intCheck(dp, 0, MAX);\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n\r\n return round(new BigNumber(x), dp + x.e + 1, rm);\r\n }\r\n\r\n if (!(c = x.c)) return null;\r\n n = ((v = c.length - 1) - bitFloor(this.e / LOG_BASE)) * LOG_BASE;\r\n\r\n // Subtract the number of trailing zeros of the last number.\r\n if (v = c[v]) for (; v % 10 == 0; v /= 10, n--);\r\n if (n < 0) n = 0;\r\n\r\n return n;\r\n };\r\n\r\n\r\n /*\r\n * n / 0 = I\r\n * n / N = N\r\n * n / I = 0\r\n * 0 / n = 0\r\n * 0 / 0 = N\r\n * 0 / N = N\r\n * 0 / I = 0\r\n * N / n = N\r\n * N / 0 = N\r\n * N / N = N\r\n * N / I = N\r\n * I / n = I\r\n * I / 0 = I\r\n * I / N = N\r\n * I / I = N\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber divided by the value of\r\n * BigNumber(y, b), rounded according to DECIMAL_PLACES and ROUNDING_MODE.\r\n */\r\n P.dividedBy = P.div = function (y, b) {\r\n return div(this, new BigNumber(y, b), DECIMAL_PLACES, ROUNDING_MODE);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the integer part of dividing the value of this\r\n * BigNumber by the value of BigNumber(y, b).\r\n */\r\n P.dividedToIntegerBy = P.idiv = function (y, b) {\r\n return div(this, new BigNumber(y, b), 0, 1);\r\n };\r\n\r\n\r\n /*\r\n * Return a BigNumber whose value is the value of this BigNumber exponentiated by n.\r\n *\r\n * If m is present, return the result modulo m.\r\n * If n is negative round according to DECIMAL_PLACES and ROUNDING_MODE.\r\n * If POW_PRECISION is non-zero and m is not present, round to POW_PRECISION using ROUNDING_MODE.\r\n *\r\n * The modular power operation works efficiently when x, n, and m are integers, otherwise it\r\n * is equivalent to calculating x.exponentiatedBy(n).modulo(m) with a POW_PRECISION of 0.\r\n *\r\n * n {number|string|BigNumber} The exponent. An integer.\r\n * [m] {number|string|BigNumber} The modulus.\r\n *\r\n * '[BigNumber Error] Exponent not an integer: {n}'\r\n */\r\n P.exponentiatedBy = P.pow = function (n, m) {\r\n var half, isModExp, i, k, more, nIsBig, nIsNeg, nIsOdd, y,\r\n x = this;\r\n\r\n n = new BigNumber(n);\r\n\r\n // Allow NaN and \u00B1Infinity, but not other non-integers.\r\n if (n.c && !n.isInteger()) {\r\n throw Error\r\n (bignumberError + 'Exponent not an integer: ' + valueOf(n));\r\n }\r\n\r\n if (m != null) m = new BigNumber(m);\r\n\r\n // Exponent of MAX_SAFE_INTEGER is 15.\r\n nIsBig = n.e > 14;\r\n\r\n // If x is NaN, \u00B1Infinity, \u00B10 or \u00B11, or n is \u00B1Infinity, NaN or \u00B10.\r\n if (!x.c || !x.c[0] || x.c[0] == 1 && !x.e && x.c.length == 1 || !n.c || !n.c[0]) {\r\n\r\n // The sign of the result of pow when x is negative depends on the evenness of n.\r\n // If +n overflows to \u00B1Infinity, the evenness of n would be not be known.\r\n y = new BigNumber(Math.pow(+valueOf(x), nIsBig ? n.s * (2 - isOdd(n)) : +valueOf(n)));\r\n return m ? y.mod(m) : y;\r\n }\r\n\r\n nIsNeg = n.s < 0;\r\n\r\n if (m) {\r\n\r\n // x % m returns NaN if abs(m) is zero, or m is NaN.\r\n if (m.c ? !m.c[0] : !m.s) return new BigNumber(NaN);\r\n\r\n isModExp = !nIsNeg && x.isInteger() && m.isInteger();\r\n\r\n if (isModExp) x = x.mod(m);\r\n\r\n // Overflow to \u00B1Infinity: >=2**1e10 or >=1.0000024**1e15.\r\n // Underflow to \u00B10: <=0.79**1e10 or <=0.9999975**1e15.\r\n } else if (n.e > 9 && (x.e > 0 || x.e < -1 || (x.e == 0\r\n // [1, 240000000]\r\n ? x.c[0] > 1 || nIsBig && x.c[1] >= 24e7\r\n // [80000000000000] [99999750000000]\r\n : x.c[0] < 8e13 || nIsBig && x.c[0] <= 9999975e7))) {\r\n\r\n // If x is negative and n is odd, k = -0, else k = 0.\r\n k = x.s < 0 && isOdd(n) ? -0 : 0;\r\n\r\n // If x >= 1, k = \u00B1Infinity.\r\n if (x.e > -1) k = 1 / k;\r\n\r\n // If n is negative return \u00B10, else return \u00B1Infinity.\r\n return new BigNumber(nIsNeg ? 1 / k : k);\r\n\r\n } else if (POW_PRECISION) {\r\n\r\n // Truncating each coefficient array to a length of k after each multiplication\r\n // equates to truncating significant digits to POW_PRECISION + [28, 41],\r\n // i.e. there will be a minimum of 28 guard digits retained.\r\n k = mathceil(POW_PRECISION / LOG_BASE + 2);\r\n }\r\n\r\n if (nIsBig) {\r\n half = new BigNumber(0.5);\r\n if (nIsNeg) n.s = 1;\r\n nIsOdd = isOdd(n);\r\n } else {\r\n i = Math.abs(+valueOf(n));\r\n nIsOdd = i % 2;\r\n }\r\n\r\n y = new BigNumber(ONE);\r\n\r\n // Performs 54 loop iterations for n of 9007199254740991.\r\n for (; ;) {\r\n\r\n if (nIsOdd) {\r\n y = y.times(x);\r\n if (!y.c) break;\r\n\r\n if (k) {\r\n if (y.c.length > k) y.c.length = k;\r\n } else if (isModExp) {\r\n y = y.mod(m); //y = y.minus(div(y, m, 0, MODULO_MODE).times(m));\r\n }\r\n }\r\n\r\n if (i) {\r\n i = mathfloor(i / 2);\r\n if (i === 0) break;\r\n nIsOdd = i % 2;\r\n } else {\r\n n = n.times(half);\r\n round(n, n.e + 1, 1);\r\n\r\n if (n.e > 14) {\r\n nIsOdd = isOdd(n);\r\n } else {\r\n i = +valueOf(n);\r\n if (i === 0) break;\r\n nIsOdd = i % 2;\r\n }\r\n }\r\n\r\n x = x.times(x);\r\n\r\n if (k) {\r\n if (x.c && x.c.length > k) x.c.length = k;\r\n } else if (isModExp) {\r\n x = x.mod(m); //x = x.minus(div(x, m, 0, MODULO_MODE).times(m));\r\n }\r\n }\r\n\r\n if (isModExp) return y;\r\n if (nIsNeg) y = ONE.div(y);\r\n\r\n return m ? y.mod(m) : k ? round(y, POW_PRECISION, ROUNDING_MODE, more) : y;\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the value of this BigNumber rounded to an integer\r\n * using rounding mode rm, or ROUNDING_MODE if rm is omitted.\r\n *\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {rm}'\r\n */\r\n P.integerValue = function (rm) {\r\n var n = new BigNumber(this);\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n return round(n, n.e + 1, rm);\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is equal to the value of BigNumber(y, b),\r\n * otherwise return false.\r\n */\r\n P.isEqualTo = P.eq = function (y, b) {\r\n return compare(this, new BigNumber(y, b)) === 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is a finite number, otherwise return false.\r\n */\r\n P.isFinite = function () {\r\n return !!this.c;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is greater than the value of BigNumber(y, b),\r\n * otherwise return false.\r\n */\r\n P.isGreaterThan = P.gt = function (y, b) {\r\n return compare(this, new BigNumber(y, b)) > 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is greater than or equal to the value of\r\n * BigNumber(y, b), otherwise return false.\r\n */\r\n P.isGreaterThanOrEqualTo = P.gte = function (y, b) {\r\n return (b = compare(this, new BigNumber(y, b))) === 1 || b === 0;\r\n\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is an integer, otherwise return false.\r\n */\r\n P.isInteger = function () {\r\n return !!this.c && bitFloor(this.e / LOG_BASE) > this.c.length - 2;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is less than the value of BigNumber(y, b),\r\n * otherwise return false.\r\n */\r\n P.isLessThan = P.lt = function (y, b) {\r\n return compare(this, new BigNumber(y, b)) < 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is less than or equal to the value of\r\n * BigNumber(y, b), otherwise return false.\r\n */\r\n P.isLessThanOrEqualTo = P.lte = function (y, b) {\r\n return (b = compare(this, new BigNumber(y, b))) === -1 || b === 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is NaN, otherwise return false.\r\n */\r\n P.isNaN = function () {\r\n return !this.s;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is negative, otherwise return false.\r\n */\r\n P.isNegative = function () {\r\n return this.s < 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is positive, otherwise return false.\r\n */\r\n P.isPositive = function () {\r\n return this.s > 0;\r\n };\r\n\r\n\r\n /*\r\n * Return true if the value of this BigNumber is 0 or -0, otherwise return false.\r\n */\r\n P.isZero = function () {\r\n return !!this.c && this.c[0] == 0;\r\n };\r\n\r\n\r\n /*\r\n * n - 0 = n\r\n * n - N = N\r\n * n - I = -I\r\n * 0 - n = -n\r\n * 0 - 0 = 0\r\n * 0 - N = N\r\n * 0 - I = -I\r\n * N - n = N\r\n * N - 0 = N\r\n * N - N = N\r\n * N - I = N\r\n * I - n = I\r\n * I - 0 = I\r\n * I - N = N\r\n * I - I = N\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber minus the value of\r\n * BigNumber(y, b).\r\n */\r\n P.minus = function (y, b) {\r\n var i, j, t, xLTy,\r\n x = this,\r\n a = x.s;\r\n\r\n y = new BigNumber(y, b);\r\n b = y.s;\r\n\r\n // Either NaN?\r\n if (!a || !b) return new BigNumber(NaN);\r\n\r\n // Signs differ?\r\n if (a != b) {\r\n y.s = -b;\r\n return x.plus(y);\r\n }\r\n\r\n var xe = x.e / LOG_BASE,\r\n ye = y.e / LOG_BASE,\r\n xc = x.c,\r\n yc = y.c;\r\n\r\n if (!xe || !ye) {\r\n\r\n // Either Infinity?\r\n if (!xc || !yc) return xc ? (y.s = -b, y) : new BigNumber(yc ? x : NaN);\r\n\r\n // Either zero?\r\n if (!xc[0] || !yc[0]) {\r\n\r\n // Return y if y is non-zero, x if x is non-zero, or zero if both are zero.\r\n return yc[0] ? (y.s = -b, y) : new BigNumber(xc[0] ? x :\r\n\r\n // IEEE 754 (2008) 6.3: n - n = -0 when rounding to -Infinity\r\n ROUNDING_MODE == 3 ? -0 : 0);\r\n }\r\n }\r\n\r\n xe = bitFloor(xe);\r\n ye = bitFloor(ye);\r\n xc = xc.slice();\r\n\r\n // Determine which is the bigger number.\r\n if (a = xe - ye) {\r\n\r\n if (xLTy = a < 0) {\r\n a = -a;\r\n t = xc;\r\n } else {\r\n ye = xe;\r\n t = yc;\r\n }\r\n\r\n t.reverse();\r\n\r\n // Prepend zeros to equalise exponents.\r\n for (b = a; b--; t.push(0));\r\n t.reverse();\r\n } else {\r\n\r\n // Exponents equal. Check digit by digit.\r\n j = (xLTy = (a = xc.length) < (b = yc.length)) ? a : b;\r\n\r\n for (a = b = 0; b < j; b++) {\r\n\r\n if (xc[b] != yc[b]) {\r\n xLTy = xc[b] < yc[b];\r\n break;\r\n }\r\n }\r\n }\r\n\r\n // x < y? Point xc to the array of the bigger number.\r\n if (xLTy) {\r\n t = xc;\r\n xc = yc;\r\n yc = t;\r\n y.s = -y.s;\r\n }\r\n\r\n b = (j = yc.length) - (i = xc.length);\r\n\r\n // Append zeros to xc if shorter.\r\n // No need to add zeros to yc if shorter as subtract only needs to start at yc.length.\r\n if (b > 0) for (; b--; xc[i++] = 0);\r\n b = BASE - 1;\r\n\r\n // Subtract yc from xc.\r\n for (; j > a;) {\r\n\r\n if (xc[--j] < yc[j]) {\r\n for (i = j; i && !xc[--i]; xc[i] = b);\r\n --xc[i];\r\n xc[j] += BASE;\r\n }\r\n\r\n xc[j] -= yc[j];\r\n }\r\n\r\n // Remove leading zeros and adjust exponent accordingly.\r\n for (; xc[0] == 0; xc.splice(0, 1), --ye);\r\n\r\n // Zero?\r\n if (!xc[0]) {\r\n\r\n // Following IEEE 754 (2008) 6.3,\r\n // n - n = +0 but n - n = -0 when rounding towards -Infinity.\r\n y.s = ROUNDING_MODE == 3 ? -1 : 1;\r\n y.c = [y.e = 0];\r\n return y;\r\n }\r\n\r\n // No need to check for Infinity as +x - +y != Infinity && -x - -y != Infinity\r\n // for finite x and y.\r\n return normalise(y, xc, ye);\r\n };\r\n\r\n\r\n /*\r\n * n % 0 = N\r\n * n % N = N\r\n * n % I = n\r\n * 0 % n = 0\r\n * -0 % n = -0\r\n * 0 % 0 = N\r\n * 0 % N = N\r\n * 0 % I = 0\r\n * N % n = N\r\n * N % 0 = N\r\n * N % N = N\r\n * N % I = N\r\n * I % n = N\r\n * I % 0 = N\r\n * I % N = N\r\n * I % I = N\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber modulo the value of\r\n * BigNumber(y, b). The result depends on the value of MODULO_MODE.\r\n */\r\n P.modulo = P.mod = function (y, b) {\r\n var q, s,\r\n x = this;\r\n\r\n y = new BigNumber(y, b);\r\n\r\n // Return NaN if x is Infinity or NaN, or y is NaN or zero.\r\n if (!x.c || !y.s || y.c && !y.c[0]) {\r\n return new BigNumber(NaN);\r\n\r\n // Return x if y is Infinity or x is zero.\r\n } else if (!y.c || x.c && !x.c[0]) {\r\n return new BigNumber(x);\r\n }\r\n\r\n if (MODULO_MODE == 9) {\r\n\r\n // Euclidian division: q = sign(y) * floor(x / abs(y))\r\n // r = x - qy where 0 <= r < abs(y)\r\n s = y.s;\r\n y.s = 1;\r\n q = div(x, y, 0, 3);\r\n y.s = s;\r\n q.s *= s;\r\n } else {\r\n q = div(x, y, 0, MODULO_MODE);\r\n }\r\n\r\n y = x.minus(q.times(y));\r\n\r\n // To match JavaScript %, ensure sign of zero is sign of dividend.\r\n if (!y.c[0] && MODULO_MODE == 1) y.s = x.s;\r\n\r\n return y;\r\n };\r\n\r\n\r\n /*\r\n * n * 0 = 0\r\n * n * N = N\r\n * n * I = I\r\n * 0 * n = 0\r\n * 0 * 0 = 0\r\n * 0 * N = N\r\n * 0 * I = N\r\n * N * n = N\r\n * N * 0 = N\r\n * N * N = N\r\n * N * I = N\r\n * I * n = I\r\n * I * 0 = N\r\n * I * N = N\r\n * I * I = I\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber multiplied by the value\r\n * of BigNumber(y, b).\r\n */\r\n P.multipliedBy = P.times = function (y, b) {\r\n var c, e, i, j, k, m, xcL, xlo, xhi, ycL, ylo, yhi, zc,\r\n base, sqrtBase,\r\n x = this,\r\n xc = x.c,\r\n yc = (y = new BigNumber(y, b)).c;\r\n\r\n // Either NaN, \u00B1Infinity or \u00B10?\r\n if (!xc || !yc || !xc[0] || !yc[0]) {\r\n\r\n // Return NaN if either is NaN, or one is 0 and the other is Infinity.\r\n if (!x.s || !y.s || xc && !xc[0] && !yc || yc && !yc[0] && !xc) {\r\n y.c = y.e = y.s = null;\r\n } else {\r\n y.s *= x.s;\r\n\r\n // Return \u00B1Infinity if either is \u00B1Infinity.\r\n if (!xc || !yc) {\r\n y.c = y.e = null;\r\n\r\n // Return \u00B10 if either is \u00B10.\r\n } else {\r\n y.c = [0];\r\n y.e = 0;\r\n }\r\n }\r\n\r\n return y;\r\n }\r\n\r\n e = bitFloor(x.e / LOG_BASE) + bitFloor(y.e / LOG_BASE);\r\n y.s *= x.s;\r\n xcL = xc.length;\r\n ycL = yc.length;\r\n\r\n // Ensure xc points to longer array and xcL to its length.\r\n if (xcL < ycL) {\r\n zc = xc;\r\n xc = yc;\r\n yc = zc;\r\n i = xcL;\r\n xcL = ycL;\r\n ycL = i;\r\n }\r\n\r\n // Initialise the result array with zeros.\r\n for (i = xcL + ycL, zc = []; i--; zc.push(0));\r\n\r\n base = BASE;\r\n sqrtBase = SQRT_BASE;\r\n\r\n for (i = ycL; --i >= 0;) {\r\n c = 0;\r\n ylo = yc[i] % sqrtBase;\r\n yhi = yc[i] / sqrtBase | 0;\r\n\r\n for (k = xcL, j = i + k; j > i;) {\r\n xlo = xc[--k] % sqrtBase;\r\n xhi = xc[k] / sqrtBase | 0;\r\n m = yhi * xlo + xhi * ylo;\r\n xlo = ylo * xlo + ((m % sqrtBase) * sqrtBase) + zc[j] + c;\r\n c = (xlo / base | 0) + (m / sqrtBase | 0) + yhi * xhi;\r\n zc[j--] = xlo % base;\r\n }\r\n\r\n zc[j] = c;\r\n }\r\n\r\n if (c) {\r\n ++e;\r\n } else {\r\n zc.splice(0, 1);\r\n }\r\n\r\n return normalise(y, zc, e);\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the value of this BigNumber negated,\r\n * i.e. multiplied by -1.\r\n */\r\n P.negated = function () {\r\n var x = new BigNumber(this);\r\n x.s = -x.s || null;\r\n return x;\r\n };\r\n\r\n\r\n /*\r\n * n + 0 = n\r\n * n + N = N\r\n * n + I = I\r\n * 0 + n = n\r\n * 0 + 0 = 0\r\n * 0 + N = N\r\n * 0 + I = I\r\n * N + n = N\r\n * N + 0 = N\r\n * N + N = N\r\n * N + I = N\r\n * I + n = I\r\n * I + 0 = I\r\n * I + N = N\r\n * I + I = I\r\n *\r\n * Return a new BigNumber whose value is the value of this BigNumber plus the value of\r\n * BigNumber(y, b).\r\n */\r\n P.plus = function (y, b) {\r\n var t,\r\n x = this,\r\n a = x.s;\r\n\r\n y = new BigNumber(y, b);\r\n b = y.s;\r\n\r\n // Either NaN?\r\n if (!a || !b) return new BigNumber(NaN);\r\n\r\n // Signs differ?\r\n if (a != b) {\r\n y.s = -b;\r\n return x.minus(y);\r\n }\r\n\r\n var xe = x.e / LOG_BASE,\r\n ye = y.e / LOG_BASE,\r\n xc = x.c,\r\n yc = y.c;\r\n\r\n if (!xe || !ye) {\r\n\r\n // Return \u00B1Infinity if either \u00B1Infinity.\r\n if (!xc || !yc) return new BigNumber(a / 0);\r\n\r\n // Either zero?\r\n // Return y if y is non-zero, x if x is non-zero, or zero if both are zero.\r\n if (!xc[0] || !yc[0]) return yc[0] ? y : new BigNumber(xc[0] ? x : a * 0);\r\n }\r\n\r\n xe = bitFloor(xe);\r\n ye = bitFloor(ye);\r\n xc = xc.slice();\r\n\r\n // Prepend zeros to equalise exponents. Faster to use reverse then do unshifts.\r\n if (a = xe - ye) {\r\n if (a > 0) {\r\n ye = xe;\r\n t = yc;\r\n } else {\r\n a = -a;\r\n t = xc;\r\n }\r\n\r\n t.reverse();\r\n for (; a--; t.push(0));\r\n t.reverse();\r\n }\r\n\r\n a = xc.length;\r\n b = yc.length;\r\n\r\n // Point xc to the longer array, and b to the shorter length.\r\n if (a - b < 0) {\r\n t = yc;\r\n yc = xc;\r\n xc = t;\r\n b = a;\r\n }\r\n\r\n // Only start adding at yc.length - 1 as the further digits of xc can be ignored.\r\n for (a = 0; b;) {\r\n a = (xc[--b] = xc[b] + yc[b] + a) / BASE | 0;\r\n xc[b] = BASE === xc[b] ? 0 : xc[b] % BASE;\r\n }\r\n\r\n if (a) {\r\n xc = [a].concat(xc);\r\n ++ye;\r\n }\r\n\r\n // No need to check for zero, as +x + +y != 0 && -x + -y != 0\r\n // ye = MAX_EXP + 1 possible\r\n return normalise(y, xc, ye);\r\n };\r\n\r\n\r\n /*\r\n * If sd is undefined or null or true or false, return the number of significant digits of\r\n * the value of this BigNumber, or null if the value of this BigNumber is \u00B1Infinity or NaN.\r\n * If sd is true include integer-part trailing zeros in the count.\r\n *\r\n * Otherwise, if sd is a number, return a new BigNumber whose value is the value of this\r\n * BigNumber rounded to a maximum of sd significant digits using rounding mode rm, or\r\n * ROUNDING_MODE if rm is omitted.\r\n *\r\n * sd {number|boolean} number: significant digits: integer, 1 to MAX inclusive.\r\n * boolean: whether to count integer-part trailing zeros: true or false.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}'\r\n */\r\n P.precision = P.sd = function (sd, rm) {\r\n var c, n, v,\r\n x = this;\r\n\r\n if (sd != null && sd !== !!sd) {\r\n intCheck(sd, 1, MAX);\r\n if (rm == null) rm = ROUNDING_MODE;\r\n else intCheck(rm, 0, 8);\r\n\r\n return round(new BigNumber(x), sd, rm);\r\n }\r\n\r\n if (!(c = x.c)) return null;\r\n v = c.length - 1;\r\n n = v * LOG_BASE + 1;\r\n\r\n if (v = c[v]) {\r\n\r\n // Subtract the number of trailing zeros of the last element.\r\n for (; v % 10 == 0; v /= 10, n--);\r\n\r\n // Add the number of digits of the first element.\r\n for (v = c[0]; v >= 10; v /= 10, n++);\r\n }\r\n\r\n if (sd && x.e + 1 > n) n = x.e + 1;\r\n\r\n return n;\r\n };\r\n\r\n\r\n /*\r\n * Return a new BigNumber whose value is the value of this BigNumber shifted by k places\r\n * (powers of 10). Shift to the right if n > 0, and to the left if n < 0.\r\n *\r\n * k {number} Integer, -MAX_SAFE_INTEGER to MAX_SAFE_INTEGER inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {k}'\r\n */\r\n P.shiftedBy = function (k) {\r\n intCheck(k, -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER);\r\n return this.times('1e' + k);\r\n };\r\n\r\n\r\n /*\r\n * sqrt(-n) = N\r\n * sqrt(N) = N\r\n * sqrt(-I) = N\r\n * sqrt(I) = I\r\n * sqrt(0) = 0\r\n * sqrt(-0) = -0\r\n *\r\n * Return a new BigNumber whose value is the square root of the value of this BigNumber,\r\n * rounded according to DECIMAL_PLACES and ROUNDING_MODE.\r\n */\r\n P.squareRoot = P.sqrt = function () {\r\n var m, n, r, rep, t,\r\n x = this,\r\n c = x.c,\r\n s = x.s,\r\n e = x.e,\r\n dp = DECIMAL_PLACES + 4,\r\n half = new BigNumber('0.5');\r\n\r\n // Negative/NaN/Infinity/zero?\r\n if (s !== 1 || !c || !c[0]) {\r\n return new BigNumber(!s || s < 0 && (!c || c[0]) ? NaN : c ? x : 1 / 0);\r\n }\r\n\r\n // Initial estimate.\r\n s = Math.sqrt(+valueOf(x));\r\n\r\n // Math.sqrt underflow/overflow?\r\n // Pass x to Math.sqrt as integer, then adjust the exponent of the result.\r\n if (s == 0 || s == 1 / 0) {\r\n n = coeffToString(c);\r\n if ((n.length + e) % 2 == 0) n += '0';\r\n s = Math.sqrt(+n);\r\n e = bitFloor((e + 1) / 2) - (e < 0 || e % 2);\r\n\r\n if (s == 1 / 0) {\r\n n = '5e' + e;\r\n } else {\r\n n = s.toExponential();\r\n n = n.slice(0, n.indexOf('e') + 1) + e;\r\n }\r\n\r\n r = new BigNumber(n);\r\n } else {\r\n r = new BigNumber(s + '');\r\n }\r\n\r\n // Check for zero.\r\n // r could be zero if MIN_EXP is changed after the this value was created.\r\n // This would cause a division by zero (x/t) and hence Infinity below, which would cause\r\n // coeffToString to throw.\r\n if (r.c[0]) {\r\n e = r.e;\r\n s = e + dp;\r\n if (s < 3) s = 0;\r\n\r\n // Newton-Raphson iteration.\r\n for (; ;) {\r\n t = r;\r\n r = half.times(t.plus(div(x, t, dp, 1)));\r\n\r\n if (coeffToString(t.c).slice(0, s) === (n = coeffToString(r.c)).slice(0, s)) {\r\n\r\n // The exponent of r may here be one less than the final result exponent,\r\n // e.g 0.0009999 (e-4) --> 0.001 (e-3), so adjust s so the rounding digits\r\n // are indexed correctly.\r\n if (r.e < e) --s;\r\n n = n.slice(s - 3, s + 1);\r\n\r\n // The 4th rounding digit may be in error by -1 so if the 4 rounding digits\r\n // are 9999 or 4999 (i.e. approaching a rounding boundary) continue the\r\n // iteration.\r\n if (n == '9999' || !rep && n == '4999') {\r\n\r\n // On the first iteration only, check to see if rounding up gives the\r\n // exact result as the nines may infinitely repeat.\r\n if (!rep) {\r\n round(t, t.e + DECIMAL_PLACES + 2, 0);\r\n\r\n if (t.times(t).eq(x)) {\r\n r = t;\r\n break;\r\n }\r\n }\r\n\r\n dp += 4;\r\n s += 4;\r\n rep = 1;\r\n } else {\r\n\r\n // If rounding digits are null, 0{0,4} or 50{0,3}, check for exact\r\n // result. If not, then there are further digits and m will be truthy.\r\n if (!+n || !+n.slice(1) && n.charAt(0) == '5') {\r\n\r\n // Truncate to the first rounding digit.\r\n round(r, r.e + DECIMAL_PLACES + 2, 1);\r\n m = !r.times(r).eq(x);\r\n }\r\n\r\n break;\r\n }\r\n }\r\n }\r\n }\r\n\r\n return round(r, r.e + DECIMAL_PLACES + 1, ROUNDING_MODE, m);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in exponential notation and\r\n * rounded using ROUNDING_MODE to dp fixed decimal places.\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n */\r\n P.toExponential = function (dp, rm) {\r\n if (dp != null) {\r\n intCheck(dp, 0, MAX);\r\n dp++;\r\n }\r\n return format(this, dp, rm, 1);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in fixed-point notation rounding\r\n * to dp fixed decimal places using rounding mode rm, or ROUNDING_MODE if rm is omitted.\r\n *\r\n * Note: as with JavaScript's number type, (-0).toFixed(0) is '0',\r\n * but e.g. (-0.00001).toFixed(0) is '-0'.\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n */\r\n P.toFixed = function (dp, rm) {\r\n if (dp != null) {\r\n intCheck(dp, 0, MAX);\r\n dp = dp + this.e + 1;\r\n }\r\n return format(this, dp, rm);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in fixed-point notation rounded\r\n * using rm or ROUNDING_MODE to dp decimal places, and formatted according to the properties\r\n * of the format or FORMAT object (see BigNumber.set).\r\n *\r\n * The formatting object may contain some or all of the properties shown below.\r\n *\r\n * FORMAT = {\r\n * prefix: '',\r\n * groupSize: 3,\r\n * secondaryGroupSize: 0,\r\n * groupSeparator: ',',\r\n * decimalSeparator: '.',\r\n * fractionGroupSize: 0,\r\n * fractionGroupSeparator: '\\xA0', // non-breaking space\r\n * suffix: ''\r\n * };\r\n *\r\n * [dp] {number} Decimal places. Integer, 0 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n * [format] {object} Formatting options. See FORMAT pbject above.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {dp|rm}'\r\n * '[BigNumber Error] Argument not an object: {format}'\r\n */\r\n P.toFormat = function (dp, rm, format) {\r\n var str,\r\n x = this;\r\n\r\n if (format == null) {\r\n if (dp != null && rm && typeof rm == 'object') {\r\n format = rm;\r\n rm = null;\r\n } else if (dp && typeof dp == 'object') {\r\n format = dp;\r\n dp = rm = null;\r\n } else {\r\n format = FORMAT;\r\n }\r\n } else if (typeof format != 'object') {\r\n throw Error\r\n (bignumberError + 'Argument not an object: ' + format);\r\n }\r\n\r\n str = x.toFixed(dp, rm);\r\n\r\n if (x.c) {\r\n var i,\r\n arr = str.split('.'),\r\n g1 = +format.groupSize,\r\n g2 = +format.secondaryGroupSize,\r\n groupSeparator = format.groupSeparator || '',\r\n intPart = arr[0],\r\n fractionPart = arr[1],\r\n isNeg = x.s < 0,\r\n intDigits = isNeg ? intPart.slice(1) : intPart,\r\n len = intDigits.length;\r\n\r\n if (g2) {\r\n i = g1;\r\n g1 = g2;\r\n g2 = i;\r\n len -= i;\r\n }\r\n\r\n if (g1 > 0 && len > 0) {\r\n i = len % g1 || g1;\r\n intPart = intDigits.substr(0, i);\r\n for (; i < len; i += g1) intPart += groupSeparator + intDigits.substr(i, g1);\r\n if (g2 > 0) intPart += groupSeparator + intDigits.slice(i);\r\n if (isNeg) intPart = '-' + intPart;\r\n }\r\n\r\n str = fractionPart\r\n ? intPart + (format.decimalSeparator || '') + ((g2 = +format.fractionGroupSize)\r\n ? fractionPart.replace(new RegExp('\\\\d{' + g2 + '}\\\\B', 'g'),\r\n '$&' + (format.fractionGroupSeparator || ''))\r\n : fractionPart)\r\n : intPart;\r\n }\r\n\r\n return (format.prefix || '') + str + (format.suffix || '');\r\n };\r\n\r\n\r\n /*\r\n * Return an array of two BigNumbers representing the value of this BigNumber as a simple\r\n * fraction with an integer numerator and an integer denominator.\r\n * The denominator will be a positive non-zero value less than or equal to the specified\r\n * maximum denominator. If a maximum denominator is not specified, the denominator will be\r\n * the lowest value necessary to represent the number exactly.\r\n *\r\n * [md] {number|string|BigNumber} Integer >= 1, or Infinity. The maximum denominator.\r\n *\r\n * '[BigNumber Error] Argument {not an integer|out of range} : {md}'\r\n */\r\n P.toFraction = function (md) {\r\n var d, d0, d1, d2, e, exp, n, n0, n1, q, r, s,\r\n x = this,\r\n xc = x.c;\r\n\r\n if (md != null) {\r\n n = new BigNumber(md);\r\n\r\n // Throw if md is less than one or is not an integer, unless it is Infinity.\r\n if (!n.isInteger() && (n.c || n.s !== 1) || n.lt(ONE)) {\r\n throw Error\r\n (bignumberError + 'Argument ' +\r\n (n.isInteger() ? 'out of range: ' : 'not an integer: ') + valueOf(n));\r\n }\r\n }\r\n\r\n if (!xc) return new BigNumber(x);\r\n\r\n d = new BigNumber(ONE);\r\n n1 = d0 = new BigNumber(ONE);\r\n d1 = n0 = new BigNumber(ONE);\r\n s = coeffToString(xc);\r\n\r\n // Determine initial denominator.\r\n // d is a power of 10 and the minimum max denominator that specifies the value exactly.\r\n e = d.e = s.length - x.e - 1;\r\n d.c[0] = POWS_TEN[(exp = e % LOG_BASE) < 0 ? LOG_BASE + exp : exp];\r\n md = !md || n.comparedTo(d) > 0 ? (e > 0 ? d : n1) : n;\r\n\r\n exp = MAX_EXP;\r\n MAX_EXP = 1 / 0;\r\n n = new BigNumber(s);\r\n\r\n // n0 = d1 = 0\r\n n0.c[0] = 0;\r\n\r\n for (; ;) {\r\n q = div(n, d, 0, 1);\r\n d2 = d0.plus(q.times(d1));\r\n if (d2.comparedTo(md) == 1) break;\r\n d0 = d1;\r\n d1 = d2;\r\n n1 = n0.plus(q.times(d2 = n1));\r\n n0 = d2;\r\n d = n.minus(q.times(d2 = d));\r\n n = d2;\r\n }\r\n\r\n d2 = div(md.minus(d0), d1, 0, 1);\r\n n0 = n0.plus(d2.times(n1));\r\n d0 = d0.plus(d2.times(d1));\r\n n0.s = n1.s = x.s;\r\n e = e * 2;\r\n\r\n // Determine which fraction is closer to x, n0/d0 or n1/d1\r\n r = div(n1, d1, e, ROUNDING_MODE).minus(x).abs().comparedTo(\r\n div(n0, d0, e, ROUNDING_MODE).minus(x).abs()) < 1 ? [n1, d1] : [n0, d0];\r\n\r\n MAX_EXP = exp;\r\n\r\n return r;\r\n };\r\n\r\n\r\n /*\r\n * Return the value of this BigNumber converted to a number primitive.\r\n */\r\n P.toNumber = function () {\r\n return +valueOf(this);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber rounded to sd significant digits\r\n * using rounding mode rm or ROUNDING_MODE. If sd is less than the number of digits\r\n * necessary to represent the integer part of the value in fixed-point notation, then use\r\n * exponential notation.\r\n *\r\n * [sd] {number} Significant digits. Integer, 1 to MAX inclusive.\r\n * [rm] {number} Rounding mode. Integer, 0 to 8 inclusive.\r\n *\r\n * '[BigNumber Error] Argument {not a primitive number|not an integer|out of range}: {sd|rm}'\r\n */\r\n P.toPrecision = function (sd, rm) {\r\n if (sd != null) intCheck(sd, 1, MAX);\r\n return format(this, sd, rm, 2);\r\n };\r\n\r\n\r\n /*\r\n * Return a string representing the value of this BigNumber in base b, or base 10 if b is\r\n * omitted. If a base is specified, including base 10, round according to DECIMAL_PLACES and\r\n * ROUNDING_MODE. If a base is not specified, and this BigNumber has a positive exponent\r\n * that is equal to or greater than TO_EXP_POS, or a negative exponent equal to or less than\r\n * TO_EXP_NEG, return exponential notation.\r\n *\r\n * [b] {number} Integer, 2 to ALPHABET.length inclusive.\r\n *\r\n * '[BigNumber Error] Base {not a primitive number|not an integer|out of range}: {b}'\r\n */\r\n P.toString = function (b) {\r\n var str,\r\n n = this,\r\n s = n.s,\r\n e = n.e;\r\n\r\n // Infinity or NaN?\r\n if (e === null) {\r\n if (s) {\r\n str = 'Infinity';\r\n if (s < 0) str = '-' + str;\r\n } else {\r\n str = 'NaN';\r\n }\r\n } else {\r\n if (b == null) {\r\n str = e <= TO_EXP_NEG || e >= TO_EXP_POS\r\n ? toExponential(coeffToString(n.c), e)\r\n : toFixedPoint(coeffToString(n.c), e, '0');\r\n } else if (b === 10 && alphabetHasNormalDecimalDigits) {\r\n n = round(new BigNumber(n), DECIMAL_PLACES + e + 1, ROUNDING_MODE);\r\n str = toFixedPoint(coeffToString(n.c), n.e, '0');\r\n } else {\r\n intCheck(b, 2, ALPHABET.length, 'Base');\r\n str = convertBase(toFixedPoint(coeffToString(n.c), e, '0'), 10, b, s, true);\r\n }\r\n\r\n if (s < 0 && n.c[0]) str = '-' + str;\r\n }\r\n\r\n return str;\r\n };\r\n\r\n\r\n /*\r\n * Return as toString, but do not accept a base argument, and include the minus sign for\r\n * negative zero.\r\n */\r\n P.valueOf = P.toJSON = function () {\r\n return valueOf(this);\r\n };\r\n\r\n\r\n P._isBigNumber = true;\r\n\r\n P[Symbol.toStringTag] = 'BigNumber';\r\n\r\n // Node.js v10.12.0+\r\n P[Symbol.for('nodejs.util.inspect.custom')] = P.valueOf;\r\n\r\n if (configObject != null) BigNumber.set(configObject);\r\n\r\n return BigNumber;\r\n}\r\n\r\n\r\n// PRIVATE HELPER FUNCTIONS\r\n\r\n// These functions don't need access to variables,\r\n// e.g. DECIMAL_PLACES, in the scope of the `clone` function above.\r\n\r\n\r\nfunction bitFloor(n) {\r\n var i = n | 0;\r\n return n > 0 || n === i ? i : i - 1;\r\n}\r\n\r\n\r\n// Return a coefficient array as a string of base 10 digits.\r\nfunction coeffToString(a) {\r\n var s, z,\r\n i = 1,\r\n j = a.length,\r\n r = a[0] + '';\r\n\r\n for (; i < j;) {\r\n s = a[i++] + '';\r\n z = LOG_BASE - s.length;\r\n for (; z--; s = '0' + s);\r\n r += s;\r\n }\r\n\r\n // Determine trailing zeros.\r\n for (j = r.length; r.charCodeAt(--j) === 48;);\r\n\r\n return r.slice(0, j + 1 || 1);\r\n}\r\n\r\n\r\n// Compare the value of BigNumbers x and y.\r\nfunction compare(x, y) {\r\n var a, b,\r\n xc = x.c,\r\n yc = y.c,\r\n i = x.s,\r\n j = y.s,\r\n k = x.e,\r\n l = y.e;\r\n\r\n // Either NaN?\r\n if (!i || !j) return null;\r\n\r\n a = xc && !xc[0];\r\n b = yc && !yc[0];\r\n\r\n // Either zero?\r\n if (a || b) return a ? b ? 0 : -j : i;\r\n\r\n // Signs differ?\r\n if (i != j) return i;\r\n\r\n a = i < 0;\r\n b = k == l;\r\n\r\n // Either Infinity?\r\n if (!xc || !yc) return b ? 0 : !xc ^ a ? 1 : -1;\r\n\r\n // Compare exponents.\r\n if (!b) return k > l ^ a ? 1 : -1;\r\n\r\n j = (k = xc.length) < (l = yc.length) ? k : l;\r\n\r\n // Compare digit by digit.\r\n for (i = 0; i < j; i++) if (xc[i] != yc[i]) return xc[i] > yc[i] ^ a ? 1 : -1;\r\n\r\n // Compare lengths.\r\n return k == l ? 0 : k > l ^ a ? 1 : -1;\r\n}\r\n\r\n\r\n/*\r\n * Check that n is a primitive number, an integer, and in range, otherwise throw.\r\n */\r\nfunction intCheck(n, min, max, name) {\r\n if (n < min || n > max || n !== mathfloor(n)) {\r\n throw Error\r\n (bignumberError + (name || 'Argument') + (typeof n == 'number'\r\n ? n < min || n > max ? ' out of range: ' : ' not an integer: '\r\n : ' not a primitive number: ') + String(n));\r\n }\r\n}\r\n\r\n\r\n// Assumes finite n.\r\nfunction isOdd(n) {\r\n var k = n.c.length - 1;\r\n return bitFloor(n.e / LOG_BASE) == k && n.c[k] % 2 != 0;\r\n}\r\n\r\n\r\nfunction toExponential(str, e) {\r\n return (str.length > 1 ? str.charAt(0) + '.' + str.slice(1) : str) +\r\n (e < 0 ? 'e' : 'e+') + e;\r\n}\r\n\r\n\r\nfunction toFixedPoint(str, e, z) {\r\n var len, zs;\r\n\r\n // Negative exponent?\r\n if (e < 0) {\r\n\r\n // Prepend zeros.\r\n for (zs = z + '.'; ++e; zs += z);\r\n str = zs + str;\r\n\r\n // Positive exponent\r\n } else {\r\n len = str.length;\r\n\r\n // Append zeros.\r\n if (++e > len) {\r\n for (zs = z, e -= len; --e; zs += z);\r\n str += zs;\r\n } else if (e < len) {\r\n str = str.slice(0, e) + '.' + str.slice(e);\r\n }\r\n }\r\n\r\n return str;\r\n}\r\n\r\n\r\n// EXPORT\r\n\r\n\r\nexport var BigNumber = clone();\r\n\r\nexport default BigNumber;\r\n", "type Comparator = (a: T, b: T) => number;\ntype Predicate = (value: T) => boolean;\n\nclass SplayTreeNode> {\n readonly key: K;\n\n left: Node | null = null;\n right: Node | null = null;\n\n constructor(key: K) {\n this.key = key;\n }\n}\n\nclass SplayTreeSetNode extends SplayTreeNode> {\n constructor(key: K) {\n super(key);\n }\n}\n\nclass SplayTreeMapNode extends SplayTreeNode> {\n readonly value: V;\n\n constructor(key: K, value: V) {\n super(key);\n this.value = value;\n }\n\n replaceValue(value: V) {\n const node = new SplayTreeMapNode(this.key, value);\n node.left = this.left;\n node.right = this.right;\n return node;\n }\n}\n\nabstract class SplayTree> {\n protected abstract root: Node | null;\n\n public size = 0;\n\n protected modificationCount = 0;\n\n protected splayCount = 0;\n\n protected abstract compare: Comparator;\n\n protected abstract validKey: Predicate;\n\n protected splay(key: K) {\n const root = this.root;\n if (root == null) {\n this.compare(key, key);\n return -1;\n }\n\n let right: Node | null = null;\n let newTreeRight: Node | null = null;\n let left: Node | null = null;\n let newTreeLeft: Node | null = null;\n let current = root;\n const compare = this.compare;\n let comp: number;\n while (true) {\n comp = compare(current.key, key);\n if (comp > 0) {\n let currentLeft = current.left;\n if (currentLeft == null) break;\n comp = compare(currentLeft.key, key);\n if (comp > 0) {\n current.left = currentLeft.right;\n currentLeft.right = current;\n current = currentLeft;\n currentLeft = current.left;\n if (currentLeft == null) break;\n }\n if (right == null) {\n newTreeRight = current;\n } else {\n right.left = current;\n }\n right = current;\n current = currentLeft;\n } else if (comp < 0) {\n let currentRight = current.right;\n if (currentRight == null) break;\n comp = compare(currentRight.key, key);\n if (comp < 0) {\n current.right = currentRight.left;\n currentRight.left = current;\n current = currentRight;\n currentRight = current.right;\n if (currentRight == null) break;\n }\n if (left == null) {\n newTreeLeft = current;\n } else {\n left.right = current;\n }\n left = current;\n current = currentRight;\n } else {\n break;\n }\n }\n if (left != null) {\n left.right = current.left;\n current.left = newTreeLeft;\n }\n if (right != null) {\n right.left = current.right;\n current.right = newTreeRight;\n }\n if (this.root !== current) {\n this.root = current;\n this.splayCount++;\n }\n return comp;\n }\n\n protected splayMin(node: Node) {\n let current = node;\n let nextLeft = current.left;\n while (nextLeft != null) {\n const left = nextLeft;\n current.left = left.right;\n left.right = current;\n current = left;\n nextLeft = current.left;\n }\n return current;\n }\n\n protected splayMax(node: Node) {\n let current = node;\n let nextRight = current.right;\n while (nextRight != null) {\n const right = nextRight;\n current.right = right.left;\n right.left = current;\n current = right;\n nextRight = current.right;\n }\n return current;\n }\n\n protected _delete(key: K) {\n if (this.root == null) return null;\n const comp = this.splay(key);\n if (comp != 0) return null;\n let root = this.root;\n const result = root;\n const left = root.left;\n this.size--;\n if (left == null) {\n this.root = root.right;\n } else {\n const right = root.right;\n root = this.splayMax(left);\n\n root.right = right;\n this.root = root;\n }\n this.modificationCount++;\n return result;\n }\n\n protected addNewRoot(node: Node, comp: number) {\n this.size++;\n this.modificationCount++;\n const root = this.root;\n if (root == null) {\n this.root = node;\n return;\n }\n if (comp < 0) {\n node.left = root;\n node.right = root.right;\n root.right = null;\n } else {\n node.right = root;\n node.left = root.left;\n root.left = null;\n }\n this.root = node;\n }\n\n protected _first() {\n const root = this.root;\n if (root == null) return null;\n this.root = this.splayMin(root);\n return this.root;\n }\n\n protected _last() {\n const root = this.root;\n if (root == null) return null;\n this.root = this.splayMax(root);\n return this.root;\n }\n\n public clear() {\n this.root = null;\n this.size = 0;\n this.modificationCount++;\n }\n\n public has(key: unknown) {\n return this.validKey(key) && this.splay(key as K) == 0;\n }\n\n protected defaultCompare(): Comparator {\n return (a: K, b: K) => a < b ? -1 : a > b ? 1 : 0;\n }\n\n protected wrap(): SplayTreeWrapper {\n return {\n getRoot: () => { return this.root },\n setRoot: (root) => { this.root = root },\n getSize: () => { return this.size },\n getModificationCount: () => { return this.modificationCount },\n getSplayCount: () => { return this.splayCount },\n setSplayCount: (count) => { this.splayCount = count },\n splay: (key) => { return this.splay(key) },\n has: (key) => { return this.has(key) },\n };\n }\n}\n\nexport class SplayTreeMap extends SplayTree> implements Iterable<[K, V]>, Map {\n protected root: SplayTreeMapNode | null = null;\n\n protected compare: Comparator;\n protected validKey: Predicate;\n\n constructor(compare?: Comparator, isValidKey?: Predicate) {\n super();\n this.compare = compare ?? this.defaultCompare();\n this.validKey = isValidKey ?? ((a: unknown) => a != null && a != undefined);\n }\n\n delete(key: unknown) {\n if (!this.validKey(key)) return false;\n return this._delete(key as K) != null;\n }\n\n forEach(f: (value: V, key: K, map: Map) => void) {\n const nodes: Iterator<[K, V]> = new SplayTreeMapEntryIterableIterator(this.wrap());\n let result: IteratorResult<[K, V]>;\n while (result = nodes.next(), !result.done) {\n f(result.value[1], result.value[0], this);\n }\n }\n\n get(key: unknown): V | undefined {\n if (!this.validKey(key)) return undefined;\n if (this.root != null) {\n const comp = this.splay(key as K);\n if (comp == 0) {\n return this.root!.value;\n }\n }\n return undefined;\n }\n\n hasValue(value: unknown) {\n const initialSplayCount = this.splayCount;\n const visit = (node: SplayTreeMapNode | null) => {\n while (node != null) {\n if (node.value == value) return true;\n if (initialSplayCount != this.splayCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (node.right != null && visit(node.right)) {\n return true;\n }\n node = node.left;\n }\n return false;\n }\n\n return visit(this.root);\n }\n\n set(key: K, value: V) {\n const comp = this.splay(key);\n if (comp == 0) {\n this.root = this.root!.replaceValue(value);\n this.splayCount += 1;\n return this;\n }\n this.addNewRoot(new SplayTreeMapNode(key, value), comp);\n return this;\n }\n\n setAll(other: Map) {\n other.forEach((value: V, key: K) => {\n this.set(key, value);\n });\n }\n\n setIfAbsent(key: K, ifAbsent: () => V) {\n let comp = this.splay(key);\n if (comp == 0) {\n return this.root!.value;\n }\n const modificationCount = this.modificationCount;\n const splayCount = this.splayCount;\n const value = ifAbsent();\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (splayCount != this.splayCount) {\n comp = this.splay(key);\n }\n this.addNewRoot(new SplayTreeMapNode(key, value), comp);\n return value;\n }\n\n isEmpty() {\n return this.root == null;\n }\n\n isNotEmpty() {\n return !this.isEmpty();\n }\n\n firstKey() {\n if (this.root == null) return null;\n return this._first()!.key;\n }\n\n lastKey() {\n if (this.root == null) return null;\n return this._last()!.key;\n }\n\n lastKeyBefore(key: K) {\n if (key == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(key);\n if (comp < 0) return this.root!.key;\n let node: SplayTreeMapNode | null = this.root!.left;\n if (node == null) return null;\n let nodeRight = node.right;\n while (nodeRight != null) {\n node = nodeRight;\n nodeRight = node.right;\n }\n return node!.key;\n }\n\n firstKeyAfter(key: K) {\n if (key == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(key);\n if (comp > 0) return this.root!.key;\n let node: SplayTreeMapNode | null = this.root!.right;\n if (node == null) return null;\n let nodeLeft = node.left;\n while (nodeLeft != null) {\n node = nodeLeft;\n nodeLeft = node.left;\n }\n return node!.key;\n }\n\n update(key: K, update: (value: V) => V, ifAbsent?: () => V) {\n let comp = this.splay(key);\n if (comp == 0) {\n const modificationCount = this.modificationCount;\n const splayCount = this.splayCount;\n const newValue = update(this.root!.value);\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (splayCount != this.splayCount) {\n this.splay(key);\n }\n this.root = this.root!.replaceValue(newValue);\n this.splayCount += 1;\n return newValue;\n }\n if (ifAbsent != null) {\n const modificationCount = this.modificationCount;\n const splayCount = this.splayCount;\n const newValue = ifAbsent();\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (splayCount != this.splayCount) {\n comp = this.splay(key);\n }\n this.addNewRoot(new SplayTreeMapNode(key, newValue), comp);\n return newValue;\n }\n throw \"Invalid argument (key): Key not in map.\"\n }\n\n updateAll(update: (key: K, value: V) => V) {\n const root = this.root;\n if (root == null) return;\n const iterator = new SplayTreeMapEntryIterableIterator(this.wrap());\n let node: IteratorResult<[K, V]>;\n while (node = iterator.next(), !node.done) {\n const newValue = update(...node.value);\n iterator.replaceValue(newValue);\n }\n }\n\n keys(): IterableIterator {\n return new SplayTreeKeyIterableIterator>(this.wrap());\n }\n\n values(): IterableIterator {\n return new SplayTreeValueIterableIterator(this.wrap());\n }\n\n entries(): IterableIterator<[K, V]> {\n return this[Symbol.iterator]();\n }\n\n [Symbol.iterator](): IterableIterator<[K, V]> {\n return new SplayTreeMapEntryIterableIterator(this.wrap());\n }\n\n [Symbol.toStringTag] = '[object Map]'\n}\n\nexport class SplayTreeSet extends SplayTree> implements Iterable, Set {\n protected root: SplayTreeSetNode | null = null;\n\n protected compare: Comparator;\n protected validKey: Predicate;\n\n constructor(compare?: Comparator, isValidKey?: Predicate) {\n super();\n this.compare = compare ?? this.defaultCompare();\n this.validKey = isValidKey ?? ((v: unknown) => v != null && v != undefined );\n }\n\n delete(element: unknown) {\n if (!this.validKey(element)) return false;\n return this._delete(element as E) != null;\n }\n\n deleteAll(elements: Iterable) {\n for (const element of elements) {\n this.delete(element);\n }\n }\n\n forEach(f: (element: E, element2: E, set: Set) => void) {\n const nodes: Iterator = this[Symbol.iterator]();\n let result: IteratorResult;\n while (result = nodes.next(), !result.done) {\n f(result.value, result.value, this);\n }\n }\n\n add(element: E) {\n const compare = this.splay(element);\n if (compare != 0) this.addNewRoot(new SplayTreeSetNode(element), compare);\n return this;\n }\n\n addAndReturn(element: E) {\n const compare = this.splay(element);\n if (compare != 0) this.addNewRoot(new SplayTreeSetNode(element), compare);\n return this.root!.key;\n }\n\n addAll(elements: Iterable) {\n for (const element of elements) {\n this.add(element);\n }\n }\n\n isEmpty() {\n return this.root == null;\n }\n\n isNotEmpty() {\n return this.root != null;\n }\n\n single() {\n if (this.size == 0) throw \"Bad state: No element\";\n if (this.size > 1) throw \"Bad state: Too many element\";\n return this.root!.key;\n }\n\n first() {\n if (this.size == 0) throw \"Bad state: No element\";\n return this._first()!.key;\n }\n\n last() {\n if (this.size == 0) throw \"Bad state: No element\";\n return this._last()!.key;\n }\n\n lastBefore(element: E) {\n if (element == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(element);\n if (comp < 0) return this.root!.key;\n let node: SplayTreeSetNode | null = this.root!.left;\n if (node == null) return null;\n let nodeRight = node.right;\n while (nodeRight != null) {\n node = nodeRight;\n nodeRight = node.right;\n }\n return node!.key;\n }\n\n firstAfter(element: E) {\n if (element == null) throw \"Invalid arguments(s)\";\n if (this.root == null) return null;\n const comp = this.splay(element);\n if (comp > 0) return this.root!.key;\n let node: SplayTreeSetNode | null = this.root!.right;\n if (node == null) return null;\n let nodeLeft = node.left;\n while (nodeLeft != null) {\n node = nodeLeft;\n nodeLeft = node.left;\n }\n return node!.key;\n }\n\n retainAll(elements: Iterable) {\n const retainSet = new SplayTreeSet(this.compare, this.validKey);\n const modificationCount = this.modificationCount;\n for (const object of elements) {\n if (modificationCount != this.modificationCount) {\n throw \"Concurrent modification during iteration.\";\n }\n if (this.validKey(object) && this.splay(object as E) == 0) {\n retainSet.add(this.root!.key);\n }\n }\n if (retainSet.size != this.size) {\n this.root = retainSet.root;\n this.size = retainSet.size;\n this.modificationCount++;\n }\n }\n\n lookup(object: unknown): E | null {\n if (!this.validKey(object)) return null;\n const comp = this.splay(object as E);\n if (comp != 0) return null;\n return this.root!.key;\n }\n\n intersection(other: Set): Set {\n const result = new SplayTreeSet(this.compare, this.validKey);\n for (const element of this) {\n if (other.has(element)) result.add(element);\n }\n return result;\n }\n\n difference(other: Set): Set {\n const result = new SplayTreeSet(this.compare, this.validKey);\n for (const element of this) {\n if (!other.has(element)) result.add(element);\n }\n return result;\n }\n\n union(other: Set): Set {\n const u = this.clone();\n u.addAll(other);\n return u;\n }\n\n protected clone() {\n const set = new SplayTreeSet(this.compare, this.validKey);\n set.size = this.size;\n set.root = this.copyNode>(this.root);\n return set;\n }\n\n protected copyNode>(node: Node | null) {\n if (node == null) return null;\n function copyChildren(node: Node, dest: SplayTreeSetNode) {\n let left: Node | null;\n let right: Node | null;\n do {\n left = node.left;\n right = node.right;\n if (left != null) {\n const newLeft = new SplayTreeSetNode(left.key);\n dest.left = newLeft;\n copyChildren(left, newLeft);\n }\n if (right != null) {\n const newRight = new SplayTreeSetNode(right.key);\n dest.right = newRight;\n node = right;\n dest = newRight;\n }\n } while (right != null);\n }\n\n const result = new SplayTreeSetNode(node.key);\n copyChildren(node, result);\n return result;\n }\n\n toSet(): Set {\n return this.clone();\n }\n\n entries(): IterableIterator<[E, E]> {\n return new SplayTreeSetEntryIterableIterator>(this.wrap());\n }\n\n keys(): IterableIterator {\n return this[Symbol.iterator]();\n }\n \n values(): IterableIterator {\n return this[Symbol.iterator]();\n }\n\n [Symbol.iterator](): IterableIterator {\n return new SplayTreeKeyIterableIterator>(this.wrap());\n }\n\n [Symbol.toStringTag] = '[object Set]'\n}\n\ninterface SplayTreeWrapper> {\n getRoot: () => Node | null;\n setRoot: (root: Node | null) => void;\n getSize: () => number;\n getModificationCount: () => number;\n getSplayCount: () => number;\n setSplayCount: (count: number) => void;\n splay: (key: K) => number;\n has: (key: unknown) => boolean;\n}\n\ntype SplayTreeMapWrapper = SplayTreeWrapper>;\n\nabstract class SplayTreeIterableIterator, T> implements IterableIterator {\n protected readonly tree: SplayTreeWrapper;\n\n protected readonly path = new Array();\n\n protected modificationCount: number | null = null;\n\n protected splayCount: number;\n\n constructor(tree: SplayTreeWrapper) {\n this.tree = tree;\n this.splayCount = tree.getSplayCount();\n }\n\n [Symbol.iterator](): IterableIterator {\n return this;\n }\n\n next(): IteratorResult {\n if (this.moveNext()) return { done: false, value: this.current()! }\n return { done: true, value: null }\n }\n\n protected current() {\n if (!this.path.length) return null;\n const node = this.path[this.path.length - 1];\n return this.getValue(node);\n }\n\n protected rebuildPath(key: K) {\n this.path.splice(0, this.path.length)\n this.tree.splay(key);\n this.path.push(this.tree.getRoot()!);\n this.splayCount = this.tree.getSplayCount();\n }\n\n protected findLeftMostDescendent(node: Node | null) {\n while (node != null) {\n this.path.push(node);\n node = node.left;\n }\n }\n\n protected moveNext() {\n if (this.modificationCount != this.tree.getModificationCount()) {\n if (this.modificationCount == null) {\n this.modificationCount = this.tree.getModificationCount();\n let node = this.tree.getRoot();\n while (node != null) {\n this.path.push(node);\n node = node.left;\n }\n return this.path.length > 0;\n }\n throw \"Concurrent modification during iteration.\";\n }\n if (!this.path.length) return false;\n if (this.splayCount != this.tree.getSplayCount()) {\n this.rebuildPath(this.path[this.path.length - 1].key);\n }\n let node = this.path[this.path.length - 1];\n let next = node.right;\n if (next != null) {\n while (next != null) {\n this.path.push(next);\n next = next.left;\n }\n return true;\n }\n this.path.pop();\n while (this.path.length && this.path[this.path.length - 1].right === node) {\n node = this.path.pop()!;\n }\n return this.path.length > 0;\n }\n\n protected abstract getValue(node: Node): T\n}\n\nclass SplayTreeKeyIterableIterator> extends SplayTreeIterableIterator {\n\n protected getValue(node: Node) {\n return node.key;\n }\n}\n\nclass SplayTreeSetEntryIterableIterator> extends SplayTreeIterableIterator {\n\n protected getValue(node: Node): [K, K] {\n return [node.key, node.key];\n }\n}\n\nclass SplayTreeValueIterableIterator extends SplayTreeIterableIterator, V> {\n\n constructor(map: SplayTreeMapWrapper) {\n super(map);\n }\n\n protected getValue(node: SplayTreeMapNode) {\n return node.value;\n }\n}\n\nclass SplayTreeMapEntryIterableIterator extends SplayTreeIterableIterator, [K, V]> {\n\n constructor(map: SplayTreeMapWrapper) {\n super(map);\n }\n\n protected getValue(node: SplayTreeMapNode): [K, V] {\n return [node.key, node.value];\n }\n\n replaceValue(value: V) {\n if (this.modificationCount != this.tree.getModificationCount()) {\n throw \"Concurrent modification during iteration.\";\n }\n if (this.splayCount != this.tree.getSplayCount()) {\n this.rebuildPath(this.path[this.path.length - 1].key);\n }\n const last = this.path.pop()!;\n const newLast = last.replaceValue(value);\n if (!this.path.length) {\n this.tree.setRoot(newLast);\n } else {\n const parent = this.path[this.path.length - 1];\n if (last === parent.left) {\n parent.left = newLast;\n } else {\n parent.right = newLast;\n }\n }\n this.path.push(newLast);\n const count = this.tree.getSplayCount() + 1;\n this.tree.setSplayCount(count);\n this.splayCount = count;\n }\n}", "import BigNumber from \"bignumber.js\";\nimport { Bbox } from \"./bbox.js\";\nimport { precision } from \"./precision.js\";\nimport Segment from \"./segment.js\";\nimport { Point } from \"./sweep-event.js\";\n\nexport type Ring = [number, number][]\nexport type Poly = Ring[]\nexport type MultiPoly = Poly[]\nexport type Geom = Poly | MultiPoly\n\nexport class RingIn {\n poly: PolyIn\n isExterior: boolean\n segments: Segment[]\n bbox: Bbox\n\n constructor(geomRing: Ring, poly: PolyIn, isExterior: boolean) {\n if (!Array.isArray(geomRing) || geomRing.length === 0) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n\n this.poly = poly\n this.isExterior = isExterior\n this.segments = []\n\n if (\n typeof geomRing[0][0] !== \"number\" ||\n typeof geomRing[0][1] !== \"number\"\n ) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n\n const firstPoint = precision.snap({ x: new BigNumber(geomRing[0][0]), y: new BigNumber(geomRing[0][1]) }) as Point\n this.bbox = {\n ll: { x: firstPoint.x, y: firstPoint.y },\n ur: { x: firstPoint.x, y: firstPoint.y },\n }\n\n let prevPoint = firstPoint\n for (let i = 1, iMax = geomRing.length; i < iMax; i++) {\n if (\n typeof geomRing[i][0] !== \"number\" ||\n typeof geomRing[i][1] !== \"number\"\n ) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n const point = precision.snap({ x: new BigNumber(geomRing[i][0]), y: new BigNumber(geomRing[i][1]) }) as Point\n // skip repeated points\n if (point.x.eq(prevPoint.x) && point.y.eq(prevPoint.y)) continue\n this.segments.push(Segment.fromRing(prevPoint, point, this))\n if (point.x.isLessThan(this.bbox.ll.x)) this.bbox.ll.x = point.x\n if (point.y.isLessThan(this.bbox.ll.y)) this.bbox.ll.y = point.y\n if (point.x.isGreaterThan(this.bbox.ur.x)) this.bbox.ur.x = point.x\n if (point.y.isGreaterThan(this.bbox.ur.y)) this.bbox.ur.y = point.y\n prevPoint = point\n }\n // add segment from last to first if last is not the same as first\n if (!firstPoint.x.eq(prevPoint.x) || !firstPoint.y.eq(prevPoint.y)) {\n this.segments.push(Segment.fromRing(prevPoint, firstPoint, this))\n }\n }\n\n getSweepEvents() {\n const sweepEvents = []\n for (let i = 0, iMax = this.segments.length; i < iMax; i++) {\n const segment = this.segments[i]\n sweepEvents.push(segment.leftSE)\n sweepEvents.push(segment.rightSE)\n }\n return sweepEvents\n }\n}\n\nexport class PolyIn {\n multiPoly: MultiPolyIn\n exteriorRing: RingIn\n interiorRings: RingIn[]\n bbox: Bbox\n\n constructor(geomPoly: Poly, multiPoly: MultiPolyIn) {\n if (!Array.isArray(geomPoly)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n this.exteriorRing = new RingIn(geomPoly[0], this, true)\n // copy by value\n this.bbox = {\n ll: { x: this.exteriorRing.bbox.ll.x, y: this.exteriorRing.bbox.ll.y },\n ur: { x: this.exteriorRing.bbox.ur.x, y: this.exteriorRing.bbox.ur.y },\n }\n this.interiorRings = []\n for (let i = 1, iMax = geomPoly.length; i < iMax; i++) {\n const ring = new RingIn(geomPoly[i], this, false)\n if (ring.bbox.ll.x.isLessThan(this.bbox.ll.x)) this.bbox.ll.x = ring.bbox.ll.x\n if (ring.bbox.ll.y.isLessThan(this.bbox.ll.y)) this.bbox.ll.y = ring.bbox.ll.y\n if (ring.bbox.ur.x.isGreaterThan(this.bbox.ur.x)) this.bbox.ur.x = ring.bbox.ur.x\n if (ring.bbox.ur.y.isGreaterThan(this.bbox.ur.y)) this.bbox.ur.y = ring.bbox.ur.y\n this.interiorRings.push(ring)\n }\n this.multiPoly = multiPoly\n }\n\n getSweepEvents() {\n const sweepEvents = this.exteriorRing.getSweepEvents()\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringSweepEvents = this.interiorRings[i].getSweepEvents()\n for (let j = 0, jMax = ringSweepEvents.length; j < jMax; j++) {\n sweepEvents.push(ringSweepEvents[j])\n }\n }\n return sweepEvents\n }\n}\n\nexport class MultiPolyIn {\n isSubject: boolean\n polys: PolyIn[]\n bbox: Bbox\n\n constructor(geom: Geom, isSubject: boolean) {\n if (!Array.isArray(geom)) {\n throw new Error(\"Input geometry is not a valid Polygon or MultiPolygon\")\n }\n\n try {\n // if the input looks like a polygon, convert it to a multipolygon\n if (typeof geom[0][0][0] === \"number\") geom = [geom as Poly]\n } catch (ex) {\n // The input is either malformed or has empty arrays.\n // In either case, it will be handled later on.\n }\n\n this.polys = []\n this.bbox = {\n ll: { x: new BigNumber(Number.POSITIVE_INFINITY), y: new BigNumber(Number.POSITIVE_INFINITY) },\n ur: { x: new BigNumber(Number.NEGATIVE_INFINITY), y: new BigNumber(Number.NEGATIVE_INFINITY) },\n }\n for (let i = 0, iMax = geom.length; i < iMax; i++) {\n const poly = new PolyIn(geom[i] as Poly, this)\n if (poly.bbox.ll.x.isLessThan(this.bbox.ll.x)) this.bbox.ll.x = poly.bbox.ll.x\n if (poly.bbox.ll.y.isLessThan(this.bbox.ll.y)) this.bbox.ll.y = poly.bbox.ll.y\n if (poly.bbox.ur.x.isGreaterThan(this.bbox.ur.x)) this.bbox.ur.x = poly.bbox.ur.x\n if (poly.bbox.ur.y.isGreaterThan(this.bbox.ur.y)) this.bbox.ur.y = poly.bbox.ur.y\n this.polys.push(poly)\n }\n this.isSubject = isSubject\n }\n\n getSweepEvents() {\n const sweepEvents = []\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polySweepEvents = this.polys[i].getSweepEvents()\n for (let j = 0, jMax = polySweepEvents.length; j < jMax; j++) {\n sweepEvents.push(polySweepEvents[j])\n }\n }\n return sweepEvents\n }\n}\n", "export default (x: T) => {\n return () => {\n return x\n }\n}", "import BigNumber from \"bignumber.js\"\nimport constant from \"./constant.js\"\n\nexport default (eps?: number) => {\n const almostEqual = eps ? (a: BigNumber, b: BigNumber) =>\n b.minus(a).abs().isLessThanOrEqualTo(eps)\n : constant(false)\n\n return (a: BigNumber, b: BigNumber) => {\n if (almostEqual(a, b)) return 0\n\n return a.comparedTo(b)\n }\n}", "import BigNumber from \"bignumber.js\";\nimport constant from \"./constant.js\";\nimport { Vector } from \"./vector.js\";\n\nexport default function (eps?: number) {\n const almostCollinear = eps ? (area2: BigNumber, ax: BigNumber, ay: BigNumber, cx: BigNumber, cy: BigNumber) =>\n area2.exponentiatedBy(2).isLessThanOrEqualTo(\n cx.minus(ax).exponentiatedBy(2).plus(cy.minus(ay).exponentiatedBy(2))\n .times(eps))\n : constant(false)\n\n return (a: Vector, b: Vector, c: Vector) => {\n const ax = a.x, ay = a.y, cx = c.x, cy = c.y\n\n const area2 = ay.minus(cy).times(b.x.minus(cx)).minus(ax.minus(cx).times(b.y.minus(cy)))\n\n if (almostCollinear(area2, ax, ay, cx, cy)) return 0\n\n return area2.comparedTo(0)\n }\n}", "import BigNumber from \"bignumber.js\";\nimport { SplayTreeSet } from \"splaytree-ts\"\nimport compare from \"./compare.js\";\nimport identity from \"./identity.js\";\nimport { Vector } from \"./vector.js\";\n\nexport default (eps?: number) => {\n if (eps) {\n\n const xTree = new SplayTreeSet(compare(eps))\n const yTree = new SplayTreeSet(compare(eps))\n\n const snapCoord = (coord: BigNumber, tree: SplayTreeSet) => {\n return tree.addAndReturn(coord)\n }\n\n const snap = (v: Vector) => {\n return {\n x: snapCoord(v.x, xTree),\n y: snapCoord(v.y, yTree),\n } as Vector\n }\n\n snap({ x: new BigNumber(0), y: new BigNumber(0)})\n\n return snap\n }\n\n return identity\n}", "export default (x: T) => {\n return x;\n}", "import compare from \"./compare.js\";\nimport orient from \"./orient.js\";\nimport snap from \"./snap.js\";\n\nconst set = (eps?: number) => {\n return {\n set: (eps?: number) => { precision = set(eps) },\n reset: () => set(eps),\n compare: compare(eps),\n snap: snap(eps),\n orient: orient(eps)\n }\n}\n\nexport let precision: ReturnType = set()", "import { Vector } from \"./vector.js\";\n\nexport interface Bbox {\n ll: Vector;\n ur: Vector;\n}\n\n/**\n * A bounding box has the format:\n *\n * { ll: { x: xmin, y: ymin }, ur: { x: xmax, y: ymax } }\n *\n */\n\nexport const isInBbox = (bbox: Bbox, point: Vector) => {\n return (\n bbox.ll.x.isLessThanOrEqualTo(point.x) &&\n point.x.isLessThanOrEqualTo(bbox.ur.x) &&\n bbox.ll.y.isLessThanOrEqualTo(point.y) &&\n point.y.isLessThanOrEqualTo(bbox.ur.y) \n )\n}\n\n/* Returns either null, or a bbox (aka an ordered pair of points)\n * If there is only one point of overlap, a bbox with identical points\n * will be returned */\nexport const getBboxOverlap = (b1: Bbox, b2: Bbox) => {\n // check if the bboxes overlap at all\n if (\n b2.ur.x.isLessThan(b1.ll.x) ||\n b1.ur.x.isLessThan(b2.ll.x) ||\n b2.ur.y.isLessThan(b1.ll.y) ||\n b1.ur.y.isLessThan(b2.ll.y) \n )\n return null\n\n // find the middle two X values\n const lowerX = b1.ll.x.isLessThan(b2.ll.x) ? b2.ll.x : b1.ll.x\n const upperX = b1.ur.x.isLessThan(b2.ur.x) ? b1.ur.x : b2.ur.x\n\n // find the middle two Y values\n const lowerY = b1.ll.y.isLessThan(b2.ll.y) ? b2.ll.y : b1.ll.y\n const upperY = b1.ur.y.isLessThan(b2.ur.y) ? b1.ur.y : b2.ur.y\n\n // put those middle values together to get the overlap\n return { ll: { x: lowerX, y: lowerY }, ur: { x: upperX, y: upperY } } as Bbox\n}\n", "import { SplayTreeSet } from \"splaytree-ts\"\nimport { getBboxOverlap } from \"./bbox.js\"\nimport * as geomIn from \"./geom-in.js\"\nimport { Geom } from \"./geom-in.js\"\nimport * as geomOut from \"./geom-out.js\"\nimport { precision } from \"./precision.js\"\nimport SweepEvent from \"./sweep-event.js\"\nimport SweepLine from \"./sweep-line.js\"\n\nexport class Operation {\n type!: string\n numMultiPolys!: number\n\n run(type: string, geom: Geom, moreGeoms: Geom[]) {\n operation.type = type\n\n /* Convert inputs to MultiPoly objects */\n const multipolys = [new geomIn.MultiPolyIn(geom, true)]\n for (let i = 0, iMax = moreGeoms.length; i < iMax; i++) {\n multipolys.push(new geomIn.MultiPolyIn(moreGeoms[i], false))\n }\n operation.numMultiPolys = multipolys.length\n\n /* BBox optimization for difference operation\n * If the bbox of a multipolygon that's part of the clipping doesn't\n * intersect the bbox of the subject at all, we can just drop that\n * multiploygon. */\n if (operation.type === \"difference\") {\n // in place removal\n const subject = multipolys[0]\n let i = 1\n while (i < multipolys.length) {\n if (getBboxOverlap(multipolys[i].bbox, subject.bbox) !== null) i++\n else multipolys.splice(i, 1)\n }\n }\n\n /* BBox optimization for intersection operation\n * If we can find any pair of multipolygons whose bbox does not overlap,\n * then the result will be empty. */\n if (operation.type === \"intersection\") {\n // TODO: this is O(n^2) in number of polygons. By sorting the bboxes,\n // it could be optimized to O(n * ln(n))\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const mpA = multipolys[i]\n for (let j = i + 1, jMax = multipolys.length; j < jMax; j++) {\n if (getBboxOverlap(mpA.bbox, multipolys[j].bbox) === null) return []\n }\n }\n }\n\n /* Put segment endpoints in a priority queue */\n const queue = new SplayTreeSet(SweepEvent.compare)\n for (let i = 0, iMax = multipolys.length; i < iMax; i++) {\n const sweepEvents = multipolys[i].getSweepEvents()\n for (let j = 0, jMax = sweepEvents.length; j < jMax; j++) {\n queue.add(sweepEvents[j])\n }\n }\n\n /* Pass the sweep line over those endpoints */\n const sweepLine = new SweepLine(queue)\n let evt = null\n if (queue.size != 0) {\n evt = queue.first()\n queue.delete(evt)\n }\n while (evt) {\n const newEvents = sweepLine.process(evt)\n for (let i = 0, iMax = newEvents.length; i < iMax; i++) {\n const evt = newEvents[i]\n if (evt.consumedBy === undefined) queue.add(evt)\n }\n if (queue.size != 0) {\n evt = queue.first()\n queue.delete(evt)\n } else {\n evt = null;\n }\n }\n\n // free some memory we don't need anymore\n precision.reset()\n\n /* Collect and compile segments we're keeping into a multipolygon */\n const ringsOut = geomOut.RingOut.factory(sweepLine.segments)\n const result = new geomOut.MultiPolyOut(ringsOut)\n return result.getGeom()\n }\n}\n\n// singleton available by import\nconst operation = new Operation()\n\nexport default operation\n", "import * as bn from \"bignumber.js\";\n\nexport interface Vector {\n x: bn.BigNumber;\n y: bn.BigNumber;\n}\n\n/* Cross Product of two vectors with first point at origin */\nexport const crossProduct = (a: Vector, b: Vector) => a.x.times(b.y).minus(a.y.times(b.x))\n\n/* Dot Product of two vectors with first point at origin */\nexport const dotProduct = (a: Vector, b: Vector) => a.x.times(b.x).plus(a.y.times(b.y))\n\nexport const length = (v: Vector) => dotProduct(v, v).sqrt()\n\n/* Get the sine of the angle from pShared -> pAngle to pShaed -> pBase */\nexport const sineOfAngle = (pShared: Vector, pBase: Vector, pAngle: Vector) => {\n const vBase = { x: pBase.x.minus(pShared.x), y: pBase.y.minus(pShared.y) }\n const vAngle = { x: pAngle.x.minus(pShared.x), y: pAngle.y.minus(pShared.y) }\n return crossProduct(vAngle, vBase).div(length(vAngle)).div(length(vBase))\n}\n\n/* Get the cosine of the angle from pShared -> pAngle to pShaed -> pBase */\nexport const cosineOfAngle = (pShared: Vector, pBase: Vector, pAngle: Vector) => {\n const vBase = { x: pBase.x.minus(pShared.x), y: pBase.y.minus(pShared.y) }\n const vAngle = { x: pAngle.x.minus(pShared.x), y: pAngle.y.minus(pShared.y) }\n return dotProduct(vAngle, vBase).div(length(vAngle)).div(length(vBase))\n}\n\n/* Get the x coordinate where the given line (defined by a point and vector)\n * crosses the horizontal line with the given y coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\nexport const horizontalIntersection = (pt: Vector, v: Vector, y: bn.BigNumber) => {\n if (v.y.isZero()) return null\n return { x: pt.x.plus((v.x.div(v.y)).times(y.minus(pt.y))), y: y }\n}\n\n/* Get the y coordinate where the given line (defined by a point and vector)\n * crosses the vertical line with the given x coordiante.\n * In the case of parrallel lines (including overlapping ones) returns null. */\nexport const verticalIntersection = (pt: Vector, v: Vector, x: bn.BigNumber) => {\n if (v.x.isZero()) return null\n return { x: x, y: pt.y.plus((v.y.div(v.x)).times(x.minus(pt.x))) }\n}\n\n/* Get the intersection of two lines, each defined by a base point and a vector.\n * In the case of parrallel lines (including overlapping ones) returns null. */\nexport const intersection = (pt1: Vector, v1: Vector, pt2: Vector, v2: Vector) => {\n // take some shortcuts for vertical and horizontal lines\n // this also ensures we don't calculate an intersection and then discover\n // it's actually outside the bounding box of the line\n if (v1.x.isZero()) return verticalIntersection(pt2, v2, pt1.x)\n if (v2.x.isZero()) return verticalIntersection(pt1, v1, pt2.x)\n if (v1.y.isZero()) return horizontalIntersection(pt2, v2, pt1.y)\n if (v2.y.isZero()) return horizontalIntersection(pt1, v1, pt2.y)\n\n // General case for non-overlapping segments.\n // This algorithm is based on Schneider and Eberly.\n // http://www.cimec.org.ar/~ncalvo/Schneider_Eberly.pdf - pg 244\n\n const kross = crossProduct(v1, v2)\n if (kross.isZero()) return null\n\n const ve = { x: pt2.x.minus(pt1.x), y: pt2.y.minus(pt1.y) }\n const d1 = crossProduct(ve, v1).div(kross)\n const d2 = crossProduct(ve, v2).div(kross)\n\n // take the average of the two calculations to minimize rounding error\n const x1 = pt1.x.plus(d2.times(v1.x)),\n x2 = pt2.x.plus(d1.times(v2.x))\n const y1 = pt1.y.plus(d2.times(v1.y)),\n y2 = pt2.y.plus(d1.times(v2.y))\n const x = x1.plus(x2).div(2)\n const y = y1.plus(y2).div(2)\n return { x: x, y: y } as Vector\n}\n\n/* Given a vector, return one that is perpendicular */\nexport const perpendicular = (v: Vector) => {\n return { x: v.y.negated(), y: v.x }\n}", "import BigNumber from \"bignumber.js\";\nimport Segment from \"./segment.js\"\nimport { cosineOfAngle, sineOfAngle, Vector } from \"./vector.js\"\n\nexport interface Point extends Vector {\n events: SweepEvent[];\n}\n\nexport default class SweepEvent {\n point: Point;\n isLeft: boolean;\n segment!: Segment;\n otherSE!: SweepEvent;\n consumedBy: SweepEvent | undefined;\n\n // for ordering sweep events in the sweep event queue\n static compare(a: SweepEvent, b: SweepEvent) {\n // favor event with a point that the sweep line hits first\n const ptCmp = SweepEvent.comparePoints(a.point, b.point)\n if (ptCmp !== 0) return ptCmp\n\n // the points are the same, so link them if needed\n if (a.point !== b.point) a.link(b)\n\n // favor right events over left\n if (a.isLeft !== b.isLeft) return a.isLeft ? 1 : -1\n\n // we have two matching left or right endpoints\n // ordering of this case is the same as for their segments\n return Segment.compare(a.segment, b.segment)\n }\n\n // for ordering points in sweep line order\n static comparePoints(aPt: Point, bPt: Point) {\n if (aPt.x.isLessThan(bPt.x)) return -1\n if (aPt.x.isGreaterThan(bPt.x)) return 1\n\n if (aPt.y.isLessThan(bPt.y)) return -1\n if (aPt.y.isGreaterThan(bPt.y)) return 1\n\n return 0\n }\n\n // Warning: 'point' input will be modified and re-used (for performance)\n constructor(point: Point, isLeft: boolean) {\n if (point.events === undefined) point.events = [this]\n else point.events.push(this)\n this.point = point\n this.isLeft = isLeft\n // this.segment, this.otherSE set by factory\n }\n\n link(other: SweepEvent) {\n if (other.point === this.point) {\n throw new Error(\"Tried to link already linked events\")\n }\n const otherEvents = other.point.events\n for (let i = 0, iMax = otherEvents.length; i < iMax; i++) {\n const evt = otherEvents[i]\n this.point.events.push(evt)\n evt.point = this.point\n }\n this.checkForConsuming()\n }\n\n /* Do a pass over our linked events and check to see if any pair\n * of segments match, and should be consumed. */\n checkForConsuming() {\n // FIXME: The loops in this method run O(n^2) => no good.\n // Maintain little ordered sweep event trees?\n // Can we maintaining an ordering that avoids the need\n // for the re-sorting with getLeftmostComparator in geom-out?\n\n // Compare each pair of events to see if other events also match\n const numEvents = this.point.events.length\n for (let i = 0; i < numEvents; i++) {\n const evt1 = this.point.events[i]\n if (evt1.segment.consumedBy !== undefined) continue\n for (let j = i + 1; j < numEvents; j++) {\n const evt2 = this.point.events[j]\n if (evt2.consumedBy !== undefined) continue\n if (evt1.otherSE.point.events !== evt2.otherSE.point.events) continue\n evt1.segment.consume(evt2.segment)\n }\n }\n }\n\n getAvailableLinkedEvents() {\n // point.events is always of length 2 or greater\n const events = []\n for (let i = 0, iMax = this.point.events.length; i < iMax; i++) {\n const evt = this.point.events[i]\n if (evt !== this && !evt.segment.ringOut && evt.segment.isInResult()) {\n events.push(evt)\n }\n }\n return events\n }\n\n /**\n * Returns a comparator function for sorting linked events that will\n * favor the event that will give us the smallest left-side angle.\n * All ring construction starts as low as possible heading to the right,\n * so by always turning left as sharp as possible we'll get polygons\n * without uncessary loops & holes.\n *\n * The comparator function has a compute cache such that it avoids\n * re-computing already-computed values.\n */\n getLeftmostComparator(baseEvent: SweepEvent) {\n const cache = new Map()\n\n const fillCache = (linkedEvent: SweepEvent) => {\n const nextEvent = linkedEvent.otherSE\n cache.set(linkedEvent, {\n sine: sineOfAngle(this.point, baseEvent.point, nextEvent.point),\n cosine: cosineOfAngle(this.point, baseEvent.point, nextEvent.point),\n })\n }\n\n return (a: SweepEvent, b: SweepEvent) => {\n if (!cache.has(a)) fillCache(a)\n if (!cache.has(b)) fillCache(b)\n\n const { sine: asine, cosine: acosine } = cache.get(a)!\n const { sine: bsine, cosine: bcosine } = cache.get(b)!\n\n // both on or above x-axis\n if (asine.isGreaterThanOrEqualTo(0) && bsine.isGreaterThanOrEqualTo(0)) {\n if (acosine.isLessThan(bcosine)) return 1\n if (acosine.isGreaterThan(bcosine)) return -1\n return 0\n }\n\n // both below x-axis\n if (asine.isLessThan(0) && bsine.isLessThan(0)) {\n if (acosine.isLessThan(bcosine)) return -1\n if (acosine.isGreaterThan(bcosine)) return 1\n return 0\n }\n\n // one above x-axis, one below\n if (bsine.isLessThan(asine)) return -1\n if (bsine.isGreaterThan(asine)) return 1\n return 0\n }\n }\n}\n", "import { MultiPoly, Poly, Ring } from \"./geom-in.js\"\nimport { precision } from \"./precision.js\"\nimport Segment from \"./segment.js\"\nimport SweepEvent from \"./sweep-event.js\"\n\nexport class RingOut {\n events: SweepEvent[]\n poly: PolyOut | null\n _isExteriorRing: boolean | undefined\n _enclosingRing: RingOut | null | undefined\n \n /* Given the segments from the sweep line pass, compute & return a series\n * of closed rings from all the segments marked to be part of the result */\n static factory(allSegments: Segment[]) {\n const ringsOut = []\n\n for (let i = 0, iMax = allSegments.length; i < iMax; i++) {\n const segment = allSegments[i]\n if (!segment.isInResult() || segment.ringOut) continue\n\n let prevEvent = null\n let event = segment.leftSE\n let nextEvent = segment.rightSE\n const events = [event]\n\n const startingPoint = event.point\n const intersectionLEs = []\n\n /* Walk the chain of linked events to form a closed ring */\n while (true) {\n prevEvent = event\n event = nextEvent\n events.push(event)\n\n /* Is the ring complete? */\n if (event.point === startingPoint) break\n\n while (true) {\n const availableLEs = event.getAvailableLinkedEvents()\n\n /* Did we hit a dead end? This shouldn't happen. Indicates some earlier\n * part of the algorithm malfunctioned... please file a bug report. */\n if (availableLEs.length === 0) {\n const firstPt = events[0].point\n const lastPt = events[events.length - 1].point\n throw new Error(\n `Unable to complete output ring starting at [${firstPt.x},` +\n ` ${firstPt.y}]. Last matching segment found ends at` +\n ` [${lastPt.x}, ${lastPt.y}].`,\n )\n }\n\n /* Only one way to go, so cotinue on the path */\n if (availableLEs.length === 1) {\n nextEvent = availableLEs[0].otherSE\n break\n }\n\n /* We must have an intersection. Check for a completed loop */\n let indexLE = null\n for (let j = 0, jMax = intersectionLEs.length; j < jMax; j++) {\n if (intersectionLEs[j].point === event.point) {\n indexLE = j\n break\n }\n }\n /* Found a completed loop. Cut that off and make a ring */\n if (indexLE !== null) {\n const intersectionLE = intersectionLEs.splice(indexLE)[0]\n const ringEvents = events.splice(intersectionLE.index)\n ringEvents.unshift(ringEvents[0].otherSE)\n ringsOut.push(new RingOut(ringEvents.reverse()))\n continue\n }\n /* register the intersection */\n intersectionLEs.push({\n index: events.length,\n point: event.point,\n })\n /* Choose the left-most option to continue the walk */\n const comparator = event.getLeftmostComparator(prevEvent)\n nextEvent = availableLEs.sort(comparator)[0].otherSE\n break\n }\n }\n\n ringsOut.push(new RingOut(events))\n }\n return ringsOut\n }\n\n constructor(events: SweepEvent[]) {\n this.events = events\n for (let i = 0, iMax = events.length; i < iMax; i++) {\n events[i].segment.ringOut = this\n }\n this.poly = null\n }\n\n getGeom() {\n // Remove superfluous points (ie extra points along a straight line),\n let prevPt = this.events[0].point\n const points = [prevPt]\n for (let i = 1, iMax = this.events.length - 1; i < iMax; i++) {\n const pt = this.events[i].point\n const nextPt = this.events[i + 1].point\n if (precision.orient(pt, prevPt, nextPt) === 0) continue\n points.push(pt)\n prevPt = pt\n }\n\n // ring was all (within rounding error of angle calc) colinear points\n if (points.length === 1) return null\n\n // check if the starting point is necessary\n const pt = points[0]\n const nextPt = points[1]\n if (precision.orient(pt, prevPt, nextPt) === 0) points.shift()\n\n points.push(points[0])\n const step = this.isExteriorRing() ? 1 : -1\n const iStart = this.isExteriorRing() ? 0 : points.length - 1\n const iEnd = this.isExteriorRing() ? points.length : -1\n const orderedPoints: Ring = []\n for (let i = iStart; i != iEnd; i += step)\n orderedPoints.push([points[i].x.toNumber(), points[i].y.toNumber()])\n return orderedPoints\n }\n\n isExteriorRing(): boolean {\n if (this._isExteriorRing === undefined) {\n const enclosing = this.enclosingRing()\n this._isExteriorRing = enclosing ? !enclosing.isExteriorRing() : true\n }\n return this._isExteriorRing\n }\n\n enclosingRing() {\n if (this._enclosingRing === undefined) {\n this._enclosingRing = this._calcEnclosingRing()\n }\n return this._enclosingRing\n }\n\n /* Returns the ring that encloses this one, if any */\n _calcEnclosingRing(): RingOut | null | undefined {\n // start with the ealier sweep line event so that the prevSeg\n // chain doesn't lead us inside of a loop of ours\n let leftMostEvt = this.events[0]\n for (let i = 1, iMax = this.events.length; i < iMax; i++) {\n const evt = this.events[i]\n if (SweepEvent.compare(leftMostEvt, evt) > 0) leftMostEvt = evt\n }\n\n let prevSeg: Segment | null | undefined = leftMostEvt.segment.prevInResult()\n let prevPrevSeg: Segment | null | undefined = prevSeg ? prevSeg.prevInResult() : null\n\n while (true) {\n // no segment found, thus no ring can enclose us\n if (!prevSeg) return null\n\n // no segments below prev segment found, thus the ring of the prev\n // segment must loop back around and enclose us\n if (!prevPrevSeg) return prevSeg.ringOut\n\n // if the two segments are of different rings, the ring of the prev\n // segment must either loop around us or the ring of the prev prev\n // seg, which would make us and the ring of the prev peers\n if (prevPrevSeg.ringOut !== prevSeg.ringOut) {\n if (prevPrevSeg.ringOut?.enclosingRing() !== prevSeg.ringOut) {\n return prevSeg.ringOut\n } else return prevSeg.ringOut?.enclosingRing()\n }\n\n // two segments are from the same ring, so this was a penisula\n // of that ring. iterate downward, keep searching\n prevSeg = prevPrevSeg.prevInResult()\n prevPrevSeg = prevSeg ? prevSeg.prevInResult() : null\n }\n }\n}\n\nexport class PolyOut {\n exteriorRing: RingOut;\n interiorRings: RingOut[];\n\n constructor(exteriorRing: RingOut) {\n this.exteriorRing = exteriorRing\n exteriorRing.poly = this\n this.interiorRings = []\n }\n\n addInterior(ring: RingOut) {\n this.interiorRings.push(ring)\n ring.poly = this\n }\n\n getGeom() {\n const geom0 = this.exteriorRing.getGeom()\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (geom0 === null) return null\n const geom: Poly = [geom0];\n for (let i = 0, iMax = this.interiorRings.length; i < iMax; i++) {\n const ringGeom = this.interiorRings[i].getGeom()\n // interior ring was all (within rounding error of angle calc) colinear points\n if (ringGeom === null) continue\n geom.push(ringGeom)\n }\n return geom\n }\n}\n\nexport class MultiPolyOut {\n rings: RingOut[];\n polys: PolyOut[];\n\n constructor(rings: RingOut[]) {\n this.rings = rings\n this.polys = this._composePolys(rings)\n }\n\n getGeom() {\n const geom: MultiPoly = []\n for (let i = 0, iMax = this.polys.length; i < iMax; i++) {\n const polyGeom = this.polys[i].getGeom()\n // exterior ring was all (within rounding error of angle calc) colinear points\n if (polyGeom === null) continue\n geom.push(polyGeom)\n }\n return geom\n }\n\n _composePolys(rings: RingOut[]) {\n const polys = []\n for (let i = 0, iMax = rings.length; i < iMax; i++) {\n const ring = rings[i]\n if (ring.poly) continue\n if (ring.isExteriorRing()) polys.push(new PolyOut(ring))\n else {\n const enclosingRing = ring.enclosingRing()\n if (!enclosingRing?.poly) polys.push(new PolyOut(enclosingRing!))\n enclosingRing?.poly?.addInterior(ring)\n }\n }\n return polys\n }\n}\n", "import { SplayTreeSet } from \"splaytree-ts\"\nimport Segment from \"./segment.js\"\nimport SweepEvent, { Point } from \"./sweep-event.js\"\n\n/**\n * NOTE: We must be careful not to change any segments while\n * they are in the SplayTree. AFAIK, there's no way to tell\n * the tree to rebalance itself - thus before splitting\n * a segment that's in the tree, we remove it from the tree,\n * do the split, then re-insert it. (Even though splitting a\n * segment *shouldn't* change its correct position in the\n * sweep line tree, the reality is because of rounding errors,\n * it sometimes does.)\n */\n\nexport default class SweepLine {\n private queue: SplayTreeSet\n private tree: SplayTreeSet\n segments: Segment[]\n\n constructor(queue: SplayTreeSet, comparator = Segment.compare) {\n this.queue = queue\n this.tree = new SplayTreeSet(comparator)\n this.segments = []\n }\n\n process(event: SweepEvent) {\n const segment = event.segment\n const newEvents: SweepEvent[] = []\n\n // if we've already been consumed by another segment,\n // clean up our body parts and get out\n if (event.consumedBy) {\n if (event.isLeft) this.queue.delete(event.otherSE)\n else this.tree.delete(segment)\n return newEvents\n }\n\n if (event.isLeft) this.tree.add(segment);\n\n let prevSeg: Segment | null = segment\n let nextSeg: Segment | null = segment\n\n // skip consumed segments still in tree\n do {\n prevSeg = this.tree.lastBefore(prevSeg)\n } while (prevSeg != null && prevSeg.consumedBy != undefined)\n\n // skip consumed segments still in tree\n do {\n nextSeg = this.tree.firstAfter(nextSeg)\n } while (nextSeg != null && nextSeg.consumedBy != undefined)\n\n if (event.isLeft) {\n // Check for intersections against the previous segment in the sweep line\n let prevMySplitter = null\n if (prevSeg) {\n const prevInter = prevSeg.getIntersection(segment)\n if (prevInter !== null) {\n if (!segment.isAnEndpoint(prevInter)) prevMySplitter = prevInter\n if (!prevSeg.isAnEndpoint(prevInter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, prevInter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n }\n }\n\n // Check for intersections against the next segment in the sweep line\n let nextMySplitter = null\n if (nextSeg) {\n const nextInter = nextSeg.getIntersection(segment)\n if (nextInter !== null) {\n if (!segment.isAnEndpoint(nextInter)) nextMySplitter = nextInter\n if (!nextSeg.isAnEndpoint(nextInter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, nextInter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n }\n }\n\n // For simplicity, even if we find more than one intersection we only\n // spilt on the 'earliest' (sweep-line style) of the intersections.\n // The other intersection will be handled in a future process().\n if (prevMySplitter !== null || nextMySplitter !== null) {\n let mySplitter = null\n if (prevMySplitter === null) mySplitter = nextMySplitter\n else if (nextMySplitter === null) mySplitter = prevMySplitter\n else {\n const cmpSplitters = SweepEvent.comparePoints(\n prevMySplitter,\n nextMySplitter,\n )\n mySplitter = cmpSplitters <= 0 ? prevMySplitter : nextMySplitter\n }\n\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n this.queue.delete(segment.rightSE)\n newEvents.push(segment.rightSE)\n\n const newEventsFromSplit = segment.split(mySplitter!)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n\n if (newEvents.length > 0) {\n // We found some intersections, so re-do the current event to\n // make sure sweep line ordering is totally consistent for later\n // use with the segment 'prev' pointers\n this.tree.delete(segment)\n newEvents.push(event)\n } else {\n // done with left event\n this.segments.push(segment)\n segment.prev = prevSeg\n }\n } else {\n // event.isRight\n\n // since we're about to be removed from the sweep line, check for\n // intersections between our previous and next segments\n if (prevSeg && nextSeg) {\n const inter = prevSeg.getIntersection(nextSeg)\n if (inter !== null) {\n if (!prevSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(prevSeg, inter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n if (!nextSeg.isAnEndpoint(inter)) {\n const newEventsFromSplit = this._splitSafely(nextSeg, inter)\n for (let i = 0, iMax = newEventsFromSplit.length; i < iMax; i++) {\n newEvents.push(newEventsFromSplit[i])\n }\n }\n }\n }\n\n this.tree.delete(segment)\n }\n\n return newEvents\n }\n\n /* Safely split a segment that is currently in the datastructures\n * IE - a segment other than the one that is currently being processed. */\n _splitSafely(seg: Segment, pt: Point) {\n // Rounding errors can cause changes in ordering,\n // so remove afected segments and right sweep events before splitting\n // removeNode() doesn't work, so have re-find the seg\n // https://github.com/w8r/splay-tree/pull/5\n this.tree.delete(seg)\n const rightSE = seg.rightSE\n this.queue.delete(rightSE)\n const newEvents = seg.split(pt)\n newEvents.push(rightSE)\n // splitting can trigger consumption\n if (seg.consumedBy === undefined) this.tree.add(seg)\n return newEvents\n }\n}\n", "import { getBboxOverlap, isInBbox } from \"./bbox.js\"\nimport { MultiPolyIn, RingIn } from \"./geom-in.js\"\nimport { RingOut } from \"./geom-out.js\"\nimport operation from \"./operation.js\"\nimport { precision } from \"./precision.js\"\nimport SweepEvent, { Point } from \"./sweep-event.js\"\nimport { intersection } from \"./vector.js\"\n\ninterface State {\n rings: RingIn[],\n windings: number[],\n multiPolys: MultiPolyIn[]\n}\n\n// Give segments unique ID's to get consistent sorting of\n// segments and sweep events when all else is identical\nlet segmentId = 0\n\nexport default class Segment {\n id: number\n leftSE: SweepEvent\n rightSE: SweepEvent\n rings: RingIn[] | null\n windings: number[] | null\n ringOut: RingOut | undefined\n consumedBy: Segment | undefined\n prev: Segment | null | undefined\n _prevInResult: Segment | null | undefined\n _beforeState: State | undefined\n _afterState: State | undefined\n _isInResult: boolean | undefined\n\n /* This compare() function is for ordering segments in the sweep\n * line tree, and does so according to the following criteria:\n *\n * Consider the vertical line that lies an infinestimal step to the\n * right of the right-more of the two left endpoints of the input\n * segments. Imagine slowly moving a point up from negative infinity\n * in the increasing y direction. Which of the two segments will that\n * point intersect first? That segment comes 'before' the other one.\n *\n * If neither segment would be intersected by such a line, (if one\n * or more of the segments are vertical) then the line to be considered\n * is directly on the right-more of the two left inputs.\n */\n static compare(a: Segment, b: Segment) {\n const alx = a.leftSE.point.x\n const blx = b.leftSE.point.x\n const arx = a.rightSE.point.x\n const brx = b.rightSE.point.x\n\n // check if they're even in the same vertical plane\n if (brx.isLessThan(alx)) return 1\n if (arx.isLessThan(blx)) return -1\n\n const aly = a.leftSE.point.y\n const bly = b.leftSE.point.y\n const ary = a.rightSE.point.y\n const bry = b.rightSE.point.y\n\n // is left endpoint of segment B the right-more?\n if (alx.isLessThan(blx)) {\n // are the two segments in the same horizontal plane?\n if (bly.isLessThan(aly) && bly.isLessThan(ary)) return 1\n if (bly.isGreaterThan(aly) && bly.isGreaterThan(ary)) return -1\n\n // is the B left endpoint colinear to segment A?\n const aCmpBLeft = a.comparePoint(b.leftSE.point)\n if (aCmpBLeft < 0) return 1\n if (aCmpBLeft > 0) return -1\n\n // is the A right endpoint colinear to segment B ?\n const bCmpARight = b.comparePoint(a.rightSE.point)\n if (bCmpARight !== 0) return bCmpARight\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return -1\n }\n\n // is left endpoint of segment A the right-more?\n if (alx.isGreaterThan(blx)) {\n if (aly.isLessThan(bly) && aly.isLessThan(bry)) return -1\n if (aly.isGreaterThan(bly) && aly.isGreaterThan(bry)) return 1\n\n // is the A left endpoint colinear to segment B?\n const bCmpALeft = b.comparePoint(a.leftSE.point)\n if (bCmpALeft !== 0) return bCmpALeft\n\n // is the B right endpoint colinear to segment A?\n const aCmpBRight = a.comparePoint(b.rightSE.point)\n if (aCmpBRight < 0) return 1\n if (aCmpBRight > 0) return -1\n\n // colinear segments, consider the one with left-more\n // left endpoint to be first (arbitrary?)\n return 1\n }\n\n // if we get here, the two left endpoints are in the same\n // vertical plane, ie alx === blx\n\n // consider the lower left-endpoint to come first\n if (aly.isLessThan(bly)) return -1\n if (aly.isGreaterThan(bly)) return 1\n\n // left endpoints are identical\n // check for colinearity by using the left-more right endpoint\n\n // is the A right endpoint more left-more?\n if (arx.isLessThan(brx)) {\n const bCmpARight = b.comparePoint(a.rightSE.point)\n if (bCmpARight !== 0) return bCmpARight\n }\n\n // is the B right endpoint more left-more?\n if (arx.isGreaterThan(brx)) {\n const aCmpBRight = a.comparePoint(b.rightSE.point)\n if (aCmpBRight < 0) return 1\n if (aCmpBRight > 0) return -1\n }\n\n if (!arx.eq(brx)) {\n // are these two [almost] vertical segments with opposite orientation?\n // if so, the one with the lower right endpoint comes first\n const ay = ary.minus(aly)\n const ax = arx.minus(alx)\n const by = bry.minus(bly)\n const bx = brx.minus(blx)\n if (ay.isGreaterThan(ax) && by.isLessThan(bx)) return 1\n if (ay.isLessThan(ax) && by.isGreaterThan(bx)) return -1\n }\n\n // we have colinear segments with matching orientation\n // consider the one with more left-more right endpoint to be first\n if (arx.isGreaterThan(brx)) return 1\n if (arx.isLessThan(brx)) return -1\n\n // if we get here, two two right endpoints are in the same\n // vertical plane, ie arx === brx\n\n // consider the lower right-endpoint to come first\n if (ary.isLessThan(bry)) return -1\n if (ary.isGreaterThan(bry)) return 1\n\n // right endpoints identical as well, so the segments are idential\n // fall back on creation order as consistent tie-breaker\n if (a.id < b.id) return -1\n if (a.id > b.id) return 1\n\n // identical segment, ie a === b\n return 0\n }\n\n /* Warning: a reference to ringWindings input will be stored,\n * and possibly will be later modified */\n constructor(leftSE: SweepEvent, rightSE: SweepEvent, rings: RingIn[], windings: number[]) {\n this.id = ++segmentId\n this.leftSE = leftSE\n leftSE.segment = this\n leftSE.otherSE = rightSE\n this.rightSE = rightSE\n rightSE.segment = this\n rightSE.otherSE = leftSE\n this.rings = rings\n this.windings = windings\n // left unset for performance, set later in algorithm\n // this.ringOut, this.consumedBy, this.prev\n }\n\n static fromRing(pt1: Point, pt2: Point, ring: RingIn) {\n let leftPt: Point, rightPt: Point, winding: number\n\n // ordering the two points according to sweep line ordering\n const cmpPts = SweepEvent.comparePoints(pt1, pt2)\n if (cmpPts < 0) {\n leftPt = pt1\n rightPt = pt2\n winding = 1\n } else if (cmpPts > 0) {\n leftPt = pt2\n rightPt = pt1\n winding = -1\n } else\n throw new Error(\n `Tried to create degenerate segment at [${pt1.x}, ${pt1.y}]`,\n )\n\n const leftSE = new SweepEvent(leftPt, true)\n const rightSE = new SweepEvent(rightPt, false)\n return new Segment(leftSE, rightSE, [ring], [winding])\n }\n\n /* When a segment is split, the rightSE is replaced with a new sweep event */\n replaceRightSE(newRightSE: SweepEvent) {\n this.rightSE = newRightSE\n this.rightSE.segment = this\n this.rightSE.otherSE = this.leftSE\n this.leftSE.otherSE = this.rightSE\n }\n\n bbox() {\n const y1 = this.leftSE.point.y\n const y2 = this.rightSE.point.y\n return {\n ll: { x: this.leftSE.point.x, y: y1.isLessThan(y2) ? y1 : y2 },\n ur: { x: this.rightSE.point.x, y: y1.isGreaterThan(y2) ? y1 : y2 },\n }\n }\n\n /* A vector from the left point to the right */\n vector() {\n return {\n x: this.rightSE.point.x.minus(this.leftSE.point.x),\n y: this.rightSE.point.y.minus(this.leftSE.point.y),\n }\n }\n\n isAnEndpoint(pt: Point) {\n return (\n (pt.x.eq(this.leftSE.point.x) && pt.y.eq(this.leftSE.point.y)) ||\n (pt.x.eq(this.rightSE.point.x) && pt.y.eq(this.rightSE.point.y))\n )\n }\n\n /* Compare this segment with a point.\n *\n * A point P is considered to be colinear to a segment if there\n * exists a distance D such that if we travel along the segment\n * from one * endpoint towards the other a distance D, we find\n * ourselves at point P.\n *\n * Return value indicates:\n *\n * 1: point lies above the segment (to the left of vertical)\n * 0: point is colinear to segment\n * -1: point lies below the segment (to the right of vertical)\n */\n comparePoint(point: Point) {\n return precision.orient(this.leftSE.point, point, this.rightSE.point)\n }\n\n /**\n * Given another segment, returns the first non-trivial intersection\n * between the two segments (in terms of sweep line ordering), if it exists.\n *\n * A 'non-trivial' intersection is one that will cause one or both of the\n * segments to be split(). As such, 'trivial' vs. 'non-trivial' intersection:\n *\n * * endpoint of segA with endpoint of segB --> trivial\n * * endpoint of segA with point along segB --> non-trivial\n * * endpoint of segB with point along segA --> non-trivial\n * * point along segA with point along segB --> non-trivial\n *\n * If no non-trivial intersection exists, return null\n * Else, return null.\n */\n getIntersection(other: Segment) {\n // If bboxes don't overlap, there can't be any intersections\n const tBbox = this.bbox()\n const oBbox = other.bbox()\n const bboxOverlap = getBboxOverlap(tBbox, oBbox)\n if (bboxOverlap === null) return null\n\n // We first check to see if the endpoints can be considered intersections.\n // This will 'snap' intersections to endpoints if possible, and will\n // handle cases of colinearity.\n\n const tlp = this.leftSE.point\n const trp = this.rightSE.point\n const olp = other.leftSE.point\n const orp = other.rightSE.point\n\n // does each endpoint touch the other segment?\n // note that we restrict the 'touching' definition to only allow segments\n // to touch endpoints that lie forward from where we are in the sweep line pass\n const touchesOtherLSE = isInBbox(tBbox, olp) && this.comparePoint(olp) === 0\n const touchesThisLSE = isInBbox(oBbox, tlp) && other.comparePoint(tlp) === 0\n const touchesOtherRSE = isInBbox(tBbox, orp) && this.comparePoint(orp) === 0\n const touchesThisRSE = isInBbox(oBbox, trp) && other.comparePoint(trp) === 0\n\n // do left endpoints match?\n if (touchesThisLSE && touchesOtherLSE) {\n // these two cases are for colinear segments with matching left\n // endpoints, and one segment being longer than the other\n if (touchesThisRSE && !touchesOtherRSE) return trp\n if (!touchesThisRSE && touchesOtherRSE) return orp\n // either the two segments match exactly (two trival intersections)\n // or just on their left endpoint (one trivial intersection\n return null\n }\n\n // does this left endpoint matches (other doesn't)\n if (touchesThisLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesOtherRSE) {\n if (tlp.x.eq(orp.x) && tlp.y.eq(orp.y)) return null\n }\n // t-intersection on left endpoint\n return tlp\n }\n\n // does other left endpoint matches (this doesn't)\n if (touchesOtherLSE) {\n // check for segments that just intersect on opposing endpoints\n if (touchesThisRSE) {\n if (trp.x.eq(olp.x) && trp.y.eq(olp.y)) return null\n }\n // t-intersection on left endpoint\n return olp\n }\n\n // trivial intersection on right endpoints\n if (touchesThisRSE && touchesOtherRSE) return null\n\n // t-intersections on just one right endpoint\n if (touchesThisRSE) return trp\n if (touchesOtherRSE) return orp\n\n // None of our endpoints intersect. Look for a general intersection between\n // infinite lines laid over the segments\n const pt = intersection(tlp, this.vector(), olp, other.vector())\n\n // are the segments parrallel? Note that if they were colinear with overlap,\n // they would have an endpoint intersection and that case was already handled above\n if (pt === null) return null\n\n // is the intersection found between the lines not on the segments?\n if (!isInBbox(bboxOverlap, pt)) return null\n\n // round the the computed point if needed\n return precision.snap(pt) as Point\n }\n\n /**\n * Split the given segment into multiple segments on the given points.\n * * Each existing segment will retain its leftSE and a new rightSE will be\n * generated for it.\n * * A new segment will be generated which will adopt the original segment's\n * rightSE, and a new leftSE will be generated for it.\n * * If there are more than two points given to split on, new segments\n * in the middle will be generated with new leftSE and rightSE's.\n * * An array of the newly generated SweepEvents will be returned.\n *\n * Warning: input array of points is modified\n */\n split(point: Point) {\n const newEvents = []\n const alreadyLinked = point.events !== undefined\n\n const newLeftSE = new SweepEvent(point, true)\n const newRightSE = new SweepEvent(point, false)\n const oldRightSE = this.rightSE\n this.replaceRightSE(newRightSE)\n newEvents.push(newRightSE)\n newEvents.push(newLeftSE)\n const newSeg = new Segment(\n newLeftSE,\n oldRightSE,\n this.rings!.slice(),\n this.windings!.slice(),\n )\n\n // when splitting a nearly vertical downward-facing segment,\n // sometimes one of the resulting new segments is vertical, in which\n // case its left and right events may need to be swapped\n if (\n SweepEvent.comparePoints(newSeg.leftSE.point, newSeg.rightSE.point) > 0\n ) {\n newSeg.swapEvents()\n }\n if (SweepEvent.comparePoints(this.leftSE.point, this.rightSE.point) > 0) {\n this.swapEvents()\n }\n\n // in the point we just used to create new sweep events with was already\n // linked to other events, we need to check if either of the affected\n // segments should be consumed\n if (alreadyLinked) {\n newLeftSE.checkForConsuming()\n newRightSE.checkForConsuming()\n }\n\n return newEvents\n }\n\n /* Swap which event is left and right */\n swapEvents() {\n const tmpEvt = this.rightSE\n this.rightSE = this.leftSE\n this.leftSE = tmpEvt\n this.leftSE.isLeft = true\n this.rightSE.isLeft = false\n for (let i = 0, iMax = this.windings!.length; i < iMax; i++) {\n this.windings![i] *= -1\n }\n }\n\n /* Consume another segment. We take their rings under our wing\n * and mark them as consumed. Use for perfectly overlapping segments */\n consume(other: Segment) {\n let consumer = this as Segment\n let consumee = other\n while (consumer.consumedBy) consumer = consumer.consumedBy\n while (consumee.consumedBy) consumee = consumee.consumedBy\n\n const cmp = Segment.compare(consumer, consumee)\n if (cmp === 0) return // already consumed\n // the winner of the consumption is the earlier segment\n // according to sweep line ordering\n if (cmp > 0) {\n const tmp = consumer\n consumer = consumee\n consumee = tmp\n }\n\n // make sure a segment doesn't consume it's prev\n if (consumer.prev === consumee) {\n const tmp = consumer\n consumer = consumee\n consumee = tmp\n }\n\n for (let i = 0, iMax = consumee.rings!.length; i < iMax; i++) {\n const ring = consumee.rings![i]\n const winding = consumee.windings![i]\n const index = consumer.rings!.indexOf(ring)\n if (index === -1) {\n consumer.rings!.push(ring)\n consumer.windings!.push(winding)\n } else consumer.windings![index] += winding\n }\n consumee.rings = null\n consumee.windings = null\n consumee.consumedBy = consumer\n\n // mark sweep events consumed as to maintain ordering in sweep event queue\n consumee.leftSE.consumedBy = consumer.leftSE\n consumee.rightSE.consumedBy = consumer.rightSE\n }\n\n /* The first segment previous segment chain that is in the result */\n prevInResult(): Segment | null | undefined {\n if (this._prevInResult !== undefined) return this._prevInResult\n if (!this.prev) this._prevInResult = null\n else if (this.prev.isInResult()) this._prevInResult = this.prev\n else this._prevInResult = this.prev.prevInResult()\n return this._prevInResult\n }\n\n beforeState(): State {\n if (this._beforeState !== undefined) return this._beforeState\n if (!this.prev)\n this._beforeState = {\n rings: [],\n windings: [],\n multiPolys: [],\n }\n else {\n const seg = this.prev.consumedBy || this.prev\n this._beforeState = seg.afterState()\n }\n return this._beforeState\n }\n\n afterState() {\n if (this._afterState !== undefined) return this._afterState\n\n const beforeState = this.beforeState()\n this._afterState = {\n rings: beforeState.rings.slice(0),\n windings: beforeState.windings.slice(0),\n multiPolys: [],\n }\n const ringsAfter = this._afterState.rings\n const windingsAfter = this._afterState.windings\n const mpsAfter = this._afterState.multiPolys\n\n // calculate ringsAfter, windingsAfter\n for (let i = 0, iMax = this.rings!.length; i < iMax; i++) {\n const ring = this.rings![i]\n const winding = this.windings![i]\n const index = ringsAfter.indexOf(ring)\n if (index === -1) {\n ringsAfter.push(ring)\n windingsAfter.push(winding)\n } else windingsAfter[index] += winding\n }\n\n // calcualte polysAfter\n const polysAfter = []\n const polysExclude = []\n for (let i = 0, iMax = ringsAfter.length; i < iMax; i++) {\n if (windingsAfter[i] === 0) continue // non-zero rule\n const ring = ringsAfter[i]\n const poly = ring.poly\n if (polysExclude.indexOf(poly) !== -1) continue\n if (ring.isExterior) polysAfter.push(poly)\n else {\n if (polysExclude.indexOf(poly) === -1) polysExclude.push(poly)\n const index = polysAfter.indexOf(ring.poly)\n if (index !== -1) polysAfter.splice(index, 1)\n }\n }\n\n // calculate multiPolysAfter\n for (let i = 0, iMax = polysAfter.length; i < iMax; i++) {\n const mp = polysAfter[i].multiPoly\n if (mpsAfter.indexOf(mp) === -1) mpsAfter.push(mp)\n }\n\n return this._afterState\n }\n\n /* Is this segment part of the final result? */\n isInResult() {\n // if we've been consumed, we're not in the result\n if (this.consumedBy) return false\n\n if (this._isInResult !== undefined) return this._isInResult\n\n const mpsBefore = this.beforeState().multiPolys\n const mpsAfter = this.afterState().multiPolys\n\n switch (operation.type) {\n case \"union\": {\n // UNION - included iff:\n // * On one side of us there is 0 poly interiors AND\n // * On the other side there is 1 or more.\n const noBefores = mpsBefore.length === 0\n const noAfters = mpsAfter.length === 0\n this._isInResult = noBefores !== noAfters\n break\n }\n\n case \"intersection\": {\n // INTERSECTION - included iff:\n // * on one side of us all multipolys are rep. with poly interiors AND\n // * on the other side of us, not all multipolys are repsented\n // with poly interiors\n let least\n let most\n if (mpsBefore.length < mpsAfter.length) {\n least = mpsBefore.length\n most = mpsAfter.length\n } else {\n least = mpsAfter.length\n most = mpsBefore.length\n }\n this._isInResult = most === operation.numMultiPolys && least < most\n break\n }\n\n case \"xor\": {\n // XOR - included iff:\n // * the difference between the number of multipolys represented\n // with poly interiors on our two sides is an odd number\n const diff = Math.abs(mpsBefore.length - mpsAfter.length)\n this._isInResult = diff % 2 === 1\n break\n }\n\n case \"difference\": {\n // DIFFERENCE included iff:\n // * on exactly one side, we have just the subject\n const isJustSubject = (mps: MultiPolyIn[]) => mps.length === 1 && mps[0].isSubject\n this._isInResult = isJustSubject(mpsBefore) !== isJustSubject(mpsAfter)\n break\n }\n }\n\n return this._isInResult\n }\n}\n", "import { Geom } from \"./geom-in.js\"\nimport { precision } from \"./precision.js\"\nimport operation from \"./operation.js\"\n\nexport { Geom }\n\nexport const union = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"union\", geom, moreGeoms)\n\nexport const intersection = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"intersection\", geom, moreGeoms)\n\nexport const xor = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"xor\", geom, moreGeoms)\n\nexport const difference = (geom: Geom, ...moreGeoms: Geom[]) =>\n operation.run(\"difference\", geom, moreGeoms)\n\nexport const setPrecision = precision.set", "module.exports.RADIUS = 6378137;\nmodule.exports.FLATTENING = 1/298.257223563;\nmodule.exports.POLAR_RADIUS = 6356752.3142;\n", "var wgs84 = require('wgs84');\n\nmodule.exports.geometry = geometry;\nmodule.exports.ring = ringArea;\n\nfunction geometry(_) {\n var area = 0, i;\n switch (_.type) {\n case 'Polygon':\n return polygonArea(_.coordinates);\n case 'MultiPolygon':\n for (i = 0; i < _.coordinates.length; i++) {\n area += polygonArea(_.coordinates[i]);\n }\n return area;\n case 'Point':\n case 'MultiPoint':\n case 'LineString':\n case 'MultiLineString':\n return 0;\n case 'GeometryCollection':\n for (i = 0; i < _.geometries.length; i++) {\n area += geometry(_.geometries[i]);\n }\n return area;\n }\n}\n\nfunction polygonArea(coords) {\n var area = 0;\n if (coords && coords.length > 0) {\n area += Math.abs(ringArea(coords[0]));\n for (var i = 1; i < coords.length; i++) {\n area -= Math.abs(ringArea(coords[i]));\n }\n }\n return area;\n}\n\n/**\n * Calculate the approximate area of the polygon were it projected onto\n * the earth. Note that this area will be positive if ring is oriented\n * clockwise, otherwise it will be negative.\n *\n * Reference:\n * Robert. G. Chamberlain and William H. Duquette, \"Some Algorithms for\n * Polygons on a Sphere\", JPL Publication 07-03, Jet Propulsion\n * Laboratory, Pasadena, CA, June 2007 http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409\n *\n * Returns:\n * {float} The approximate signed geodesic area of the polygon in square\n * meters.\n */\n\nfunction ringArea(coords) {\n var p1, p2, p3, lowerIndex, middleIndex, upperIndex, i,\n area = 0,\n coordsLength = coords.length;\n\n if (coordsLength > 2) {\n for (i = 0; i < coordsLength; i++) {\n if (i === coordsLength - 2) {// i = N-2\n lowerIndex = coordsLength - 2;\n middleIndex = coordsLength -1;\n upperIndex = 0;\n } else if (i === coordsLength - 1) {// i = N-1\n lowerIndex = coordsLength - 1;\n middleIndex = 0;\n upperIndex = 1;\n } else { // i = 0 to N-3\n lowerIndex = i;\n middleIndex = i+1;\n upperIndex = i+2;\n }\n p1 = coords[lowerIndex];\n p2 = coords[middleIndex];\n p3 = coords[upperIndex];\n area += ( rad(p3[0]) - rad(p1[0]) ) * Math.sin( rad(p2[1]));\n }\n\n area = area * wgs84.RADIUS * wgs84.RADIUS / 2;\n }\n\n return area;\n}\n\nfunction rad(_) {\n return _ * Math.PI / 180;\n}", "exports.validateCenter = function validateCenter(center) {\n var validCenterLengths = [2, 3];\n if (!Array.isArray(center) || !validCenterLengths.includes(center.length)) {\n throw new Error(\"ERROR! Center has to be an array of length two or three\");\n }\n\n var [lng, lat] = center;\n if (typeof lng !== \"number\" || typeof lat !== \"number\") {\n throw new Error(\n `ERROR! Longitude and Latitude has to be numbers but where ${typeof lng} and ${typeof lat}`\n );\n }\n if (lng > 180 || lng < -180) {\n throw new Error(`ERROR! Longitude has to be between -180 and 180 but was ${lng}`);\n }\n\n if (lat > 90 || lat < -90) {\n throw new Error(`ERROR! Latitude has to be between -90 and 90 but was ${lat}`);\n }\n};\n", "exports.validateRadius = function validateRadius(radius) {\n if (typeof radius !== \"number\") {\n throw new Error(`ERROR! Radius has to be a positive number but was: ${typeof radius}`);\n }\n\n if (radius <= 0) {\n throw new Error(`ERROR! Radius has to be a positive number but was: ${radius}`);\n }\n};\n", "exports.validateNumberOfEdges = function validateNumberOfEdges(numberOfEdges) {\n if (typeof numberOfEdges !== \"number\") {\n const ARGUMENT_TYPE = Array.isArray(numberOfEdges) ? \"array\" : typeof numberOfEdges;\n throw new Error(`ERROR! Number of edges has to be a number but was: ${ARGUMENT_TYPE}`);\n }\n\n if (numberOfEdges < 3) {\n throw new Error(`ERROR! Number of edges has to be at least 3 but was: ${numberOfEdges}`);\n }\n};\n", "exports.validateEarthRadius = function validateEarthRadius(earthRadius) {\n if (typeof earthRadius !== \"number\") {\n const ARGUMENT_TYPE = Array.isArray(earthRadius) ? \"array\" : typeof earthRadius;\n throw new Error(`ERROR! Earth radius has to be a number but was: ${ARGUMENT_TYPE}`);\n }\n\n if (earthRadius <= 0) {\n throw new Error(`ERROR! Earth radius has to be a positive number but was: ${earthRadius}`);\n }\n};\n", "exports.validateBearing = function validateBearing(bearing) {\n if (typeof bearing !== \"number\") {\n const ARGUMENT_TYPE = Array.isArray(bearing) ? \"array\" : typeof bearing;\n throw new Error(`ERROR! Bearing has to be a number but was: ${ARGUMENT_TYPE}`);\n }\n};\n", "var validateCenter = require(\"./validateCenter\").validateCenter;\nvar validateRadius = require(\"./validateRadius\").validateRadius;\nvar validateNumberOfEdges = require(\"./validateNumberOfEdges\").validateNumberOfEdges;\nvar validateEarthRadius = require(\"./validateEarthRadius\").validateEarthRadius;\nvar validateBearing = require(\"./validateBearing\").validateBearing;\n\nfunction validateInput({ center, radius, numberOfEdges, earthRadius, bearing }) {\n validateCenter(center);\n validateRadius(radius);\n validateNumberOfEdges(numberOfEdges);\n validateEarthRadius(earthRadius);\n validateBearing(bearing);\n}\n\nexports.validateCenter = validateCenter;\nexports.validateRadius = validateRadius;\nexports.validateNumberOfEdges = validateNumberOfEdges;\nexports.validateEarthRadius = validateEarthRadius;\nexports.validateBearing = validateBearing;\nexports.validateInput = validateInput;\n", "\"use strict\";\nvar { validateInput } = require(\"./input-validation\");\n\nconst defaultEarthRadius = 6378137; // equatorial Earth radius\n\nfunction toRadians(angleInDegrees) {\n return (angleInDegrees * Math.PI) / 180;\n}\n\nfunction toDegrees(angleInRadians) {\n return (angleInRadians * 180) / Math.PI;\n}\n\nfunction offset(c1, distance, earthRadius, bearing) {\n var lat1 = toRadians(c1[1]);\n var lon1 = toRadians(c1[0]);\n var dByR = distance / earthRadius;\n var lat = Math.asin(\n Math.sin(lat1) * Math.cos(dByR) + Math.cos(lat1) * Math.sin(dByR) * Math.cos(bearing)\n );\n var lon =\n lon1 +\n Math.atan2(\n Math.sin(bearing) * Math.sin(dByR) * Math.cos(lat1),\n Math.cos(dByR) - Math.sin(lat1) * Math.sin(lat)\n );\n return [toDegrees(lon), toDegrees(lat)];\n}\n\nmodule.exports = function circleToPolygon(center, radius, options) {\n var n = getNumberOfEdges(options);\n var earthRadius = getEarthRadius(options);\n var bearing = getBearing(options);\n var direction = getDirection(options);\n\n // validateInput() throws error on invalid input and do nothing on valid input\n validateInput({ center, radius, numberOfEdges: n, earthRadius, bearing });\n\n var start = toRadians(bearing);\n var coordinates = [];\n for (var i = 0; i < n; ++i) {\n coordinates.push(\n offset(\n center, radius, earthRadius, start + (direction * 2 * Math.PI * -i) / n\n )\n );\n }\n coordinates.push(coordinates[0]);\n\n return {\n type: \"Polygon\",\n coordinates: [coordinates]\n };\n};\n\nfunction getNumberOfEdges(options) {\n if (isUndefinedOrNull(options)) {\n return 32;\n } else if (isObjectNotArray(options)) {\n var numberOfEdges = options.numberOfEdges;\n return numberOfEdges === undefined ? 32 : numberOfEdges;\n }\n return options;\n}\n\nfunction getEarthRadius(options) {\n if (isUndefinedOrNull(options)) {\n return defaultEarthRadius;\n } else if (isObjectNotArray(options)) {\n var earthRadius = options.earthRadius;\n return earthRadius === undefined ? defaultEarthRadius : earthRadius;\n }\n return defaultEarthRadius;\n}\n\nfunction getDirection(options){\n if (isObjectNotArray(options) && options.rightHandRule){\n return -1;\n }\n return 1;\n}\n\nfunction getBearing(options) {\n if (isUndefinedOrNull(options)) {\n return 0;\n } else if (isObjectNotArray(options)) {\n var bearing = options.bearing;\n return bearing === undefined ? 0 : bearing;\n }\n return 0;\n}\n\nfunction isObjectNotArray(argument) {\n return argument !== null && typeof argument === \"object\" && !Array.isArray(argument);\n}\n\nfunction isUndefinedOrNull(argument) {\n return argument === null || argument === undefined;\n}\n", "(function() {\n\n function parse(t, coordinatePrecision, extrasPrecision) {\n\n function point(p) {\n return p.map(function(e, index) {\n if (index < 2) {\n return 1 * e.toFixed(coordinatePrecision);\n } else {\n return 1 * e.toFixed(extrasPrecision);\n }\n });\n }\n\n function multi(l) {\n return l.map(point);\n }\n\n function poly(p) {\n return p.map(multi);\n }\n\n function multiPoly(m) {\n return m.map(poly);\n }\n\n function geometry(obj) {\n if (!obj) {\n return {};\n }\n \n switch (obj.type) {\n case \"Point\":\n obj.coordinates = point(obj.coordinates);\n return obj;\n case \"LineString\":\n case \"MultiPoint\":\n obj.coordinates = multi(obj.coordinates);\n return obj;\n case \"Polygon\":\n case \"MultiLineString\":\n obj.coordinates = poly(obj.coordinates);\n return obj;\n case \"MultiPolygon\":\n obj.coordinates = multiPoly(obj.coordinates);\n return obj;\n case \"GeometryCollection\":\n obj.geometries = obj.geometries.map(geometry);\n return obj;\n default :\n return {};\n }\n }\n\n function feature(obj) {\n obj.geometry = geometry(obj.geometry);\n return obj\n }\n\n function featureCollection(f) {\n f.features = f.features.map(feature);\n return f;\n }\n\n function geometryCollection(g) {\n g.geometries = g.geometries.map(geometry);\n return g;\n }\n\n if (!t) {\n return t;\n }\n\n switch (t.type) {\n case \"Feature\":\n return feature(t);\n case \"GeometryCollection\" :\n return geometryCollection(t);\n case \"FeatureCollection\" :\n return featureCollection(t);\n case \"Point\":\n case \"LineString\":\n case \"Polygon\":\n case \"MultiPoint\":\n case \"MultiPolygon\":\n case \"MultiLineString\":\n return geometry(t);\n default :\n return t;\n }\n \n }\n\n module.exports = parse;\n module.exports.parse = parse;\n\n}());\n \n", "// Note: This regex matches even invalid JSON strings, but since we\u2019re\n// working on the output of `JSON.stringify` we know that only valid strings\n// are present (unless the user supplied a weird `options.indent` but in\n// that case we don\u2019t care since the output would be invalid anyway).\nconst stringOrChar = /(\"(?:[^\\\\\"]|\\\\.)*\")|[:,]/g;\n\nexport default function stringify(passedObj, options = {}) {\n const indent = JSON.stringify(\n [1],\n undefined,\n options.indent === undefined ? 2 : options.indent\n ).slice(2, -3);\n\n const maxLength =\n indent === \"\"\n ? Infinity\n : options.maxLength === undefined\n ? 80\n : options.maxLength;\n\n let { replacer } = options;\n\n return (function _stringify(obj, currentIndent, reserved) {\n if (obj && typeof obj.toJSON === \"function\") {\n obj = obj.toJSON();\n }\n\n const string = JSON.stringify(obj, replacer);\n\n if (string === undefined) {\n return string;\n }\n\n const length = maxLength - currentIndent.length - reserved;\n\n if (string.length <= length) {\n const prettified = string.replace(\n stringOrChar,\n (match, stringLiteral) => {\n return stringLiteral || `${match} `;\n }\n );\n if (prettified.length <= length) {\n return prettified;\n }\n }\n\n if (replacer != null) {\n obj = JSON.parse(string);\n replacer = undefined;\n }\n\n if (typeof obj === \"object\" && obj !== null) {\n const nextIndent = currentIndent + indent;\n const items = [];\n let index = 0;\n let start;\n let end;\n\n if (Array.isArray(obj)) {\n start = \"[\";\n end = \"]\";\n const { length } = obj;\n for (; index < length; index++) {\n items.push(\n _stringify(obj[index], nextIndent, index === length - 1 ? 0 : 1) ||\n \"null\"\n );\n }\n } else {\n start = \"{\";\n end = \"}\";\n const keys = Object.keys(obj);\n const { length } = keys;\n for (; index < length; index++) {\n const key = keys[index];\n const keyPart = `${JSON.stringify(key)}: `;\n const value = _stringify(\n obj[key],\n nextIndent,\n keyPart.length + (index === length - 1 ? 0 : 1)\n );\n if (value !== undefined) {\n items.push(keyPart + value);\n }\n }\n }\n\n if (items.length > 0) {\n return [start, indent + items.join(`,\\n${nextIndent}`), end].join(\n `\\n${currentIndent}`\n );\n }\n }\n\n return string;\n })(passedObj, \"\", 0);\n}\n", "import * as CountryCoder from '@rapideditor/country-coder';\nimport * as Polyclip from 'polyclip-ts';\nimport calcArea from '@mapbox/geojson-area';\nimport circleToPolygon from 'circle-to-polygon';\nimport precision from 'geojson-precision';\nimport prettyStringify from 'json-stringify-pretty-compact';\nimport type { Polygon, MultiPolygon } from 'geojson';\n\n// Type definitions\nexport type Vec2 = [number, number];\nexport type Vec3 = [number, number, number];\nexport type Location = Vec2 | Vec3 | string | number;\n\nexport interface FeatureProperties {\n id: string;\n area: number;\n members?: string[];\n [key: string]: unknown;\n}\n\nexport type GeoJSONGeometry = Polygon | MultiPolygon;\n\nexport interface GeoJSONFeature {\n type: 'Feature';\n id: string;\n properties: FeatureProperties;\n geometry: GeoJSONGeometry;\n}\n\nexport interface FeatureCollection {\n type: 'FeatureCollection';\n features: GeoJSONFeature[];\n}\n\nexport interface LocationSet {\n include?: Location[];\n exclude?: Location[];\n}\n\nexport interface ValidatedLocation {\n type: 'point' | 'geojson' | 'countrycoder';\n location: Location;\n id: string;\n}\n\nexport interface ResolvedLocation extends ValidatedLocation {\n feature: GeoJSONFeature;\n}\n\nexport interface ValidatedLocationSet {\n type: 'locationset';\n locationSet: LocationSet;\n id: string;\n}\n\nexport interface ResolvedLocationSet extends ValidatedLocationSet {\n feature: GeoJSONFeature;\n}\n\nexport type ClipOperation = 'UNION' | 'DIFFERENCE';\n\nexport type StringifyOptions = Parameters[1];\n\nexport class LocationConflation {\n public _cache: Map;\n public strict: boolean;\n\n /**\n * Creates a new LocationConflation instance\n * @param fc - Optional FeatureCollection of known features with filename-like IDs (e.g., \"something.geojson\")\n */\n constructor(fc?: FeatureCollection) {\n // The _cache retains resolved features, so if you ask for the same thing multiple times\n // we don't repeat the expensive resolving/clipping operations.\n //\n // Each feature has a stable identifier that is used as the cache key.\n // The identifiers look like:\n // - for point locations, the stringified point: e.g. '[8.67039,49.41882]'\n // - for geojson locations, the geojson id: e.g. 'de-hamburg.geojson'\n // - for countrycoder locations, feature.id property: e.g. 'Q2' (countrycoder uses Wikidata identifiers)\n // - for aggregated locationSets, +[include]-[exclude]: e.g '+[Q2]-[Q18,Q27611]'\n this._cache = new Map();\n\n // When strict mode = true, throw on invalid locations or locationSets.\n // When strict mode = false, return `null` for invalid locations or locationSets.\n this.strict = true;\n\n // process input FeatureCollection\n if (fc?.type === 'FeatureCollection' && Array.isArray(fc.features)) {\n for (const feature of fc.features) {\n feature.properties = feature.properties || ({} as FeatureProperties);\n const props = feature.properties;\n\n // Get `id` from either `id` or `properties`\n let id = feature.id || props.id;\n if (!id || !/^\\S+\\.geojson$/i.test(id)) continue;\n\n // Ensure `id` exists and is lowercase\n id = id.toLowerCase();\n feature.id = id;\n props.id = id;\n\n // Ensure `area` property exists\n if (!props.area) {\n const area = calcArea.geometry(feature.geometry) / 1e6; // m² to km²\n props.area = Number(area.toFixed(2));\n }\n\n this._cache.set(id, feature);\n }\n }\n\n // Replace CountryCoder world geometry to be a polygon covering the world.\n const worldFeature = CountryCoder.feature('Q2');\n const world = cloneDeep(worldFeature) as unknown as GeoJSONFeature;\n world.geometry = {\n type: 'Polygon',\n coordinates: [[[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]]],\n };\n world.id = 'Q2';\n world.properties.id = 'Q2';\n world.properties.area = calcArea.geometry(world.geometry) / 1e6; // m² to km²\n this._cache.set('Q2', world);\n }\n\n /**\n * Validates a location and returns its type and stable identifier\n * @param location - Location to validate (point, geojson filename, or country code)\n * @returns Validated location object or null if invalid\n *\n * @example\n * ```typescript\n * // Point location with default 25km radius\n * loco.validateLocation([8.67039, 49.41882]);\n * // => { type: 'point', location: [8.67039, 49.41882], id: '[8.67039,49.41882]' }\n *\n * // Point location with custom radius\n * loco.validateLocation([-77.0369, 38.9072, 10]);\n * // => { type: 'point', location: [-77.0369, 38.9072, 10], id: '[-77.0369,38.9072,10]' }\n *\n * // Country code\n * loco.validateLocation('de');\n * // => { type: 'countrycoder', location: 'de', id: 'Q183' }\n *\n * // GeoJSON file\n * loco.validateLocation('philly_metro.geojson');\n * // => { type: 'geojson', location: 'philly_metro.geojson', id: 'philly_metro.geojson' }\n * ```\n */\n validateLocation(location: Location): ValidatedLocation | null {\n // [lon, lat] or [lon, lat, radius] point?\n if (Array.isArray(location) && (location.length === 2 || location.length === 3)) {\n const lon = location[0];\n const lat = location[1];\n const radius = location[2];\n\n if (\n Number.isFinite(lon) &&\n lon >= -180 &&\n lon <= 180 &&\n Number.isFinite(lat) &&\n lat >= -90 &&\n lat <= 90 &&\n (location.length === 2 || (radius !== undefined && Number.isFinite(radius) && radius > 0))\n ) {\n const id = '[' + location.toString() + ']';\n return { type: 'point', location, id };\n }\n } else if (typeof location === 'string' && /^\\S+\\.geojson$/i.test(location)) {\n // a .geojson filename?\n const id = location.toLowerCase();\n if (this._cache.has(id)) {\n return { type: 'geojson', location, id };\n }\n } else if (typeof location === 'string' || typeof location === 'number') {\n // a country-coder value?\n const feature = CountryCoder.feature(location);\n if (feature) {\n // Use wikidata QID as the identifier, since that seems to be the one\n // property that everything in CountryCoder is guaranteed to have.\n const id = feature.properties.wikidata;\n return { type: 'countrycoder', location, id };\n }\n }\n\n if (this.strict) {\n throw new Error(`validateLocation: Invalid location: \"${location}\".`);\n }\n return null;\n }\n\n /**\n * Resolves a location to a GeoJSON feature\n * @param location - Location to resolve\n * @returns Resolved location with GeoJSON feature or null if invalid\n *\n * @example\n * ```typescript\n * // Resolve a point location to a circular polygon\n * const result = loco.resolveLocation([8.67039, 49.41882]);\n * // result.feature is a GeoJSON Feature with a circular Polygon geometry\n *\n * // Resolve a country code\n * const germany = loco.resolveLocation('de');\n * // germany.feature is a GeoJSON Feature with Germany's boundary\n *\n * // Resolve a custom GeoJSON file\n * const metro = loco.resolveLocation('philly_metro.geojson');\n * // metro.feature is the pre-loaded GeoJSON Feature\n * ```\n */\n resolveLocation(location: Location): ResolvedLocation | null {\n const valid = this.validateLocation(location);\n if (!valid) return null;\n\n const id = valid.id;\n\n // Return a result from cache if we can\n if (this._cache.has(id)) {\n return { ...valid, feature: this._cache.get(id)! };\n }\n\n // A [lon,lat] coordinate pair?\n if (valid.type === 'point' && Array.isArray(location)) {\n const lon = location[0];\n const lat = location[1];\n const radius = location[2] || 25; // km\n const EDGES = 10;\n const PRECISION = 3;\n const area = Math.PI * radius * radius;\n const feature = precision(\n {\n type: 'Feature',\n id,\n properties: { id, area: Number(area.toFixed(2)) },\n geometry: circleToPolygon([lon, lat], radius * 1000, EDGES), // km to m\n },\n PRECISION\n ) as GeoJSONFeature;\n this._cache.set(id, feature);\n return { ...valid, feature };\n }\n\n // A .geojson filename?\n if (valid.type === 'geojson') {\n // nothing to do here - these are all in _cache and would have returned already\n }\n\n // A country-coder identifier?\n if (valid.type === 'countrycoder') {\n const ccFeature = CountryCoder.feature(id);\n const feature = cloneDeep(ccFeature) as unknown as GeoJSONFeature;\n const props = feature.properties;\n\n // -> This block of code is weird and requires some explanation. <-\n // CountryCoder includes higher level features which are made up of members.\n // These features don't have their own geometry, but CountryCoder provides an\n // `aggregateFeature` method to combine these members into a MultiPolygon.\n // In the past, Turf/JSTS/martinez could not handle the aggregated features,\n // so we'd iteratively union them all together. (this was slow)\n // But now mfogel/polygon-clipping handles these MultiPolygons like a boss.\n // This approach also has the benefit of removing all the internal borders and\n // simplifying the regional polygons a lot.\n if (Array.isArray(props.members)) {\n const aggregate = CountryCoder.aggregateFeature(id);\n if (aggregate) {\n const clipped = clip([aggregate as unknown as GeoJSONFeature], 'UNION');\n if (clipped) {\n feature.geometry = clipped.geometry;\n }\n }\n }\n\n // Ensure `area` property exists\n if (!props.area) {\n const area = calcArea.geometry(feature.geometry) / 1e6; // m² to km²\n props.area = Number(area.toFixed(2));\n }\n\n // Ensure `id` property exists\n feature.id = id;\n props.id = id;\n\n this._cache.set(id, feature);\n return { ...valid, feature };\n }\n\n if (this.strict) {\n throw new Error(`resolveLocation: Couldn't resolve location \"${location}\".`);\n }\n return null;\n }\n\n /**\n * Validates a locationSet and returns its stable identifier\n * @param locationSet - LocationSet with include/exclude arrays\n * @returns Validated locationSet object or null if invalid\n *\n * @example\n * ```typescript\n * // Include multiple countries\n * loco.validateLocationSet({ include: ['de', 'fr', 'it'] });\n * // => { type: 'locationset', locationSet: {...}, id: '+[Q183,Q142,Q38]' }\n *\n * // Include with exclusions\n * loco.validateLocationSet({\n * include: ['de'],\n * exclude: ['de-berlin.geojson']\n * });\n * // => { type: 'locationset', locationSet: {...}, id: '+[Q183]-[de-berlin.geojson]' }\n *\n * // Mix different location types\n * loco.validateLocationSet({\n * include: ['us', [8.67039, 49.41882], 'philly_metro.geojson']\n * });\n * ```\n */\n validateLocationSet(locationSet?: LocationSet): ValidatedLocationSet {\n locationSet = locationSet || {};\n const validator = this.validateLocation.bind(this);\n let include = (locationSet.include || []).map(validator).filter(Boolean) as ValidatedLocation[];\n const exclude = (locationSet.exclude || []).map(validator).filter(Boolean) as ValidatedLocation[];\n\n if (!include.length) {\n if (this.strict) {\n throw new Error('validateLocationSet: LocationSet includes nothing.');\n } else {\n // non-strict mode, replace an empty locationSet with one that includes \"the world\"\n locationSet.include = ['Q2'];\n include = [{ type: 'countrycoder', location: 'Q2', id: 'Q2' }];\n }\n }\n\n // Generate stable identifier\n include.sort(sortLocations);\n let id = '+[' + include.map((d) => d.id).join(',') + ']';\n if (exclude.length) {\n exclude.sort(sortLocations);\n id += '-[' + exclude.map((d) => d.id).join(',') + ']';\n }\n\n return { type: 'locationset', locationSet, id };\n }\n\n /**\n * Resolves a locationSet to a GeoJSON feature by combining included/excluded regions\n * @param locationSet - LocationSet with include/exclude arrays\n * @returns Resolved locationSet with GeoJSON feature or null if invalid\n *\n * @example\n * ```typescript\n * // Combine multiple countries into one feature\n * const benelux = loco.resolveLocationSet({\n * include: ['be', 'nl', 'lu']\n * });\n * // benelux.feature is a GeoJSON Feature with combined boundaries\n *\n * // Germany excluding Berlin\n * const germanyNoCapital = loco.resolveLocationSet({\n * include: ['de'],\n * exclude: ['de-berlin.geojson']\n * });\n * // Result is Germany with Berlin cut out\n *\n * // Complex region definition\n * const customRegion = loco.resolveLocationSet({\n * include: ['us-ca', 'us-or', 'us-wa'],\n * exclude: [[8.67039, 49.41882, 50]]\n * });\n * // West coast states minus a 50km circle\n * ```\n */\n resolveLocationSet(locationSet?: LocationSet): ResolvedLocationSet | null {\n locationSet = locationSet || {};\n const valid = this.validateLocationSet(locationSet);\n if (!valid) return null;\n\n const id = valid.id;\n\n // Return a result from cache if we can\n if (this._cache.has(id)) {\n return { ...valid, feature: this._cache.get(id)! };\n }\n\n const resolver = this.resolveLocation.bind(this);\n const includes = (locationSet.include || []).map(resolver).filter(Boolean) as ResolvedLocation[];\n const excludes = (locationSet.exclude || []).map(resolver).filter(Boolean) as ResolvedLocation[];\n\n // Return quickly if it's a single included location..\n if (includes.length === 1 && excludes.length === 0) {\n return { ...valid, feature: includes[0].feature };\n }\n\n // Calculate unions\n const includeGeoJSON = clip(includes.map((d) => d.feature), 'UNION')!;\n const excludeGeoJSON = clip(excludes.map((d) => d.feature), 'UNION');\n\n // Calculate difference, update `area` and return result\n const resultGeoJSON = excludeGeoJSON ? clip([includeGeoJSON, excludeGeoJSON], 'DIFFERENCE')! : includeGeoJSON;\n const area = calcArea.geometry(resultGeoJSON.geometry) / 1e6; // m² to km²\n resultGeoJSON.id = id;\n resultGeoJSON.properties = { id, area: Number(area.toFixed(2)) };\n\n this._cache.set(id, resultGeoJSON);\n return { ...valid, feature: resultGeoJSON };\n }\n\n /**\n * Convenience method to pretty-stringify an object\n * @param obj - Object to stringify\n * @param options - Stringify options\n * @returns Pretty-formatted JSON string\n *\n * @example\n * ```typescript\n * const result = loco.resolveLocation('de');\n * console.log(loco.stringify(result.feature));\n * // Outputs a compact, readable JSON representation of the feature\n *\n * // Custom formatting options\n * console.log(loco.stringify(result.feature, { indent: 2, maxLength: 80 }));\n * ```\n */\n stringify(obj: unknown, options?: StringifyOptions): string {\n return prettyStringify(obj, options);\n }\n}\n\n/**\n * Wraps the polyclip-ts library and returns a GeoJSON feature\n * @param features - Array of features to clip\n * @param which - Operation type (UNION or DIFFERENCE)\n * @returns Clipped GeoJSON feature or null if features array is empty\n */\nfunction clip(features: GeoJSONFeature[], which: ClipOperation): GeoJSONFeature | null {\n if (!Array.isArray(features) || !features.length) return null;\n\n const fn = { UNION: Polyclip.union, DIFFERENCE: Polyclip.difference }[which];\n const args = features.map((feature) => feature.geometry.coordinates);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const coords = (fn as any)(...args);\n\n return {\n type: 'Feature',\n properties: {} as FeatureProperties,\n geometry: {\n type: whichType(coords),\n coordinates: coords,\n } as GeoJSONGeometry,\n id: '',\n };\n\n // is this a Polygon or a MultiPolygon?\n function whichType(coords: unknown): 'Polygon' | 'MultiPolygon' {\n const a = Array.isArray(coords);\n const b = a && Array.isArray(coords[0]);\n const c = b && Array.isArray(coords[0][0]);\n const d = c && Array.isArray(coords[0][0][0]);\n return d ? 'MultiPolygon' : 'Polygon';\n }\n}\n\n/**\n * Deep clones an object using JSON serialization\n * @param obj - Object to clone\n * @returns Cloned object\n */\nfunction cloneDeep(obj: T): T {\n return JSON.parse(JSON.stringify(obj));\n}\n\n/**\n * Sorting function for locations to generate deterministic IDs\n * Sorting the location lists is ok because they end up unioned together.\n * @param a - First location\n * @param b - Second location\n * @returns Sort order\n */\nfunction sortLocations(a: ValidatedLocation, b: ValidatedLocation): number {\n const rank = { countrycoder: 1, geojson: 2, point: 3 };\n const aRank = rank[a.type];\n const bRank = rank[b.type];\n\n return aRank > bRank ? 1 : aRank < bRank ? -1 : a.id.localeCompare(b.id);\n}\n\nexport default LocationConflation;\n", "import { LocationConflation, type LocationSet, type GeoJSONFeature, type FeatureProperties, type FeatureCollection } from '@rapideditor/location-conflation';\nimport whichPolygon from 'which-polygon';\nimport calcArea from '@mapbox/geojson-area';\nimport type { Vec2 } from '../geo/vector';\n\nexport interface ObjectWithLocationSet {\n locationSetID?: string;\n locationSet: LocationSet;\n}\n\nconst _loco = new LocationConflation(); // instance of a location-conflation resolver\n\n\n/**\n * `LocationManager` maintains an internal index of all the boundaries/geofences used by iD.\n * It's used by presets, community index, background imagery, to know where in the world these things are valid.\n * These geofences should be defined by `locationSet` objects:\n *\n * let locationSet = {\n * include: [ Array of locations ],\n * exclude: [ Array of locations ]\n * };\n *\n * For more info see the location-conflation and country-coder projects, see:\n * https://github.com/rapideditor/location-conflation\n * https://github.com/rapideditor/country-coder\n */\nexport class LocationManager {\n /** A which-polygon index */\n _wp!: whichPolygon.Query;\n /** Map (id -> GeoJSON feature) */\n _resolved = new Map();\n /** Map (locationSetID -> Number area) */\n _knownLocationSets = new Map();\n /** Map (locationID -> Set(locationSetID) ) */\n _locationIncludedIn = new Map>();\n /** Map (locationID -> Set(locationSetID) ) */\n _locationExcludedIn = new Map>();\n\n /**\n * @constructor\n */\n constructor() {\n // pre-resolve the worldwide locationSet\n const world = { locationSet: { include: ['Q2'] } };\n this._resolveLocationSet(world);\n this._rebuildIndex();\n }\n\n\n /**\n * _validateLocationSet\n * Pass an Object with a `locationSet` property.\n * Validates the `locationSet` and sets a `locationSetID` property on the object.\n * To avoid so much computation we only resolve the include and exclude regions, but not the locationSet itself.\n *\n * Use `_resolveLocationSet()` instead if you need to resolve geojson of locationSet, for example to render it.\n * Note: You need to call `_rebuildIndex()` after you're all finished validating the locationSets.\n *\n * @param `obj` Object to check, it should have `locationSet` property\n */\n _validateLocationSet(obj: ObjectWithLocationSet) {\n if (obj.locationSetID) return; // work was done already\n\n try {\n let locationSet = obj.locationSet;\n if (!locationSet) {\n throw new Error('object missing locationSet property');\n }\n if (!locationSet.include) { // missing `include`, default to worldwide include\n locationSet.include = ['Q2']; // https://github.com/openstreetmap/iD/pull/8305#discussion_r662344647\n }\n\n // Validate the locationSet only\n // Resolve the include/excludes\n const locationSetID = _loco.validateLocationSet(locationSet).id;\n obj.locationSetID = locationSetID;\n if (this._knownLocationSets.has(locationSetID)) return; // seen one like this before\n\n let area = 0;\n\n // Resolve and index the 'includes'\n (locationSet.include || []).forEach(location => {\n const locationID = _loco.validateLocation(location)!.id;\n let geojson = this._resolved.get(locationID);\n\n if (!geojson) { // first time seeing a location like this\n geojson = _loco.resolveLocation(location)!.feature; // resolve to GeoJSON\n this._resolved.set(locationID, geojson);\n }\n area += geojson.properties.area;\n\n let s = this._locationIncludedIn.get(locationID);\n if (!s) {\n s = new Set();\n this._locationIncludedIn.set(locationID, s);\n }\n s.add(locationSetID);\n });\n\n // Resolve and index the 'excludes'\n (locationSet.exclude || []).forEach(location => {\n const locationID = _loco.validateLocation(location)!.id;\n let geojson = this._resolved.get(locationID);\n\n if (!geojson) { // first time seeing a location like this\n geojson = _loco.resolveLocation(location)!.feature; // resolve to GeoJSON\n this._resolved.set(locationID, geojson);\n }\n area -= geojson.properties.area;\n\n let s = this._locationExcludedIn.get(locationID);\n if (!s) {\n s = new Set();\n this._locationExcludedIn.set(locationID, s);\n }\n s.add(locationSetID);\n });\n\n this._knownLocationSets.set(locationSetID, area);\n\n } catch {\n obj.locationSet = { include: ['Q2'] }; // default worldwide\n obj.locationSetID = '+[Q2]';\n }\n }\n\n\n /**\n * _resolveLocationSet\n * Does everything that `_validateLocationSet()` does, but then \"resolves\" the locationSet into GeoJSON.\n * This step is a bit more computationally expensive, so really only needed if you intend to render the shape.\n *\n * Note: You need to call `_rebuildIndex()` after you're all finished validating the locationSets.\n *\n * @param `obj` Object to check, it should have `locationSet` property\n */\n _resolveLocationSet(obj: ObjectWithLocationSet) {\n this._validateLocationSet(obj);\n\n if (this._resolved.has(obj.locationSetID)) return; // work was done already\n\n try {\n const result = _loco.resolveLocationSet(obj.locationSet)!;\n const locationSetID = result.id;\n obj.locationSetID = locationSetID;\n\n if (!result.feature.geometry.coordinates.length || !result.feature.properties.area) {\n throw new Error(`locationSet ${locationSetID} resolves to an empty feature.`);\n }\n\n let geojson = JSON.parse(JSON.stringify(result.feature)); // deep clone\n geojson.id = locationSetID; // Important: always use the locationSet `id` (`+[Q30]`), not the feature `id` (`Q30`)\n geojson.properties.id = locationSetID;\n this._resolved.set(locationSetID, geojson);\n\n } catch {\n obj.locationSet = { include: ['Q2'] }; // default worldwide\n obj.locationSetID = '+[Q2]';\n }\n }\n\n\n /**\n * _rebuildIndex\n * Rebuilds the whichPolygon index with whatever features have been resolved into GeoJSON.\n */\n _rebuildIndex() {\n this._wp = whichPolygon({\n type: 'FeatureCollection',\n features: [...this._resolved.values()],\n });\n }\n\n\n /**\n * mergeCustomGeoJSON\n * Accepts a FeatureCollection-like object containing custom locations\n * Each feature must have a filename-like `id`, for example: `something.geojson`\n * {\n * \"type\": \"FeatureCollection\"\n * \"features\": [\n * {\n * \"type\": \"Feature\",\n * \"id\": \"philly_metro.geojson\",\n * \"properties\": { \u2026 },\n * \"geometry\": { \u2026 }\n * }\n * ]\n * }\n *\n * @param `fc` FeatureCollection-like Object containing custom locations\n */\n mergeCustomGeoJSON(fc: FeatureCollection) {\n if (!fc || fc.type !== 'FeatureCollection' || !Array.isArray(fc.features)) return;\n\n fc.features.forEach(feature => {\n feature.properties = feature.properties || {};\n let props = feature.properties;\n\n // Get `id` from either `id` or `properties`\n let id = feature.id || props.id;\n if (!id || !/^\\S+\\.geojson$/i.test(id)) return;\n\n // Ensure `id` exists and is lowercase\n id = id.toLowerCase();\n feature.id = id;\n props.id = id;\n\n // Ensure `area` property exists\n if (!props.area) {\n const area = calcArea.geometry(feature.geometry) / 1e6; // m\u00B2 to km\u00B2\n props.area = Number(area.toFixed(2));\n }\n\n _loco._cache.set(id, feature); // insert directly into LocationConflations internal cache\n });\n }\n\n\n /**\n * mergeLocationSets\n * Accepts an Array of Objects containing `locationSet` properties:\n * [\n * { id: 'preset1', locationSet: {\u2026} },\n * { id: 'preset2', locationSet: {\u2026} },\n * \u2026\n * ]\n * After validating, the Objects will be decorated with a `locationSetID` property:\n * [\n * { id: 'preset1', locationSet: {\u2026}, locationSetID: '+[Q2]' },\n * { id: 'preset2', locationSet: {\u2026}, locationSetID: '+[Q30]' },\n * \u2026\n * ]\n *\n * @param `objects` Objects to check - they should have `locationSet` property\n * @return Promise resolved true (this function used to be slow/async, now it's faster and sync)\n */\n mergeLocationSets(objects: ObjectWithLocationSet[]) {\n if (!Array.isArray(objects)) return Promise.reject('nothing to do');\n\n objects.forEach(obj => this._validateLocationSet(obj));\n this._rebuildIndex();\n return Promise.resolve(objects);\n }\n\n\n /**\n * locationSetID\n * Returns a locationSetID for a given locationSet (fallback to `+[Q2]`, world)\n * (The locationSet doesn't necessarily need to be resolved to compute its `id`)\n *\n * @param `locationSet` A locationSet Object, e.g. `{ include: ['us'] }`\n * @return String locationSetID, e.g. `+[Q30]`\n */\n locationSetID(locationSet: LocationSet) {\n let locationSetID;\n try {\n locationSetID = _loco.validateLocationSet(locationSet).id;\n } catch {\n locationSetID = '+[Q2]'; // the world\n }\n return locationSetID;\n }\n\n\n /**\n * feature\n * Returns the resolved GeoJSON feature for a given locationSetID (fallback to 'world')\n * A GeoJSON feature:\n * {\n * type: 'Feature',\n * id: '+[Q30]',\n * properties: { id: '+[Q30]', area: 21817019.17, \u2026 },\n * geometry: { \u2026 }\n * }\n *\n * @param `locationSetID` String identifier, e.g. `+[Q30]`\n * @return GeoJSON object (fallback to world)\n */\n feature(locationSetID = '+[Q2]') {\n const feature = this._resolved.get(locationSetID);\n return feature || this._resolved.get('+[Q2]');\n }\n\n\n /**\n * locationSetsAt\n * Find all the locationSets valid at the given location.\n * Results include the area (in km\u00B2) to facilitate sorting.\n *\n * Object of locationSetIDs to areas (in km\u00B2)\n * {\n * \"+[Q2]\": 511207893.3958111,\n * \"+[Q30]\": 21817019.17,\n * \"+[new_jersey.geojson]\": 22390.77,\n * \u2026\n * }\n *\n * @param `loc` `[lon,lat]` location to query, e.g. `[-74.4813, 40.7967]`\n * @return Object of locationSetIDs valid at given location\n */\n locationSetsAt(loc: Vec2) {\n const result: { [locationSetID: string]: number } = {};\n\n const hits = this._wp(loc, true) || [];\n // eslint-disable-next-line @typescript-eslint/no-this-alias\n const thiz = this;\n\n // locationSets\n hits.forEach(prop => {\n if (prop.id[0] !== '+') return; // skip - it's a location\n const locationSetID = prop.id;\n const area = thiz._knownLocationSets.get(locationSetID);\n if (area) {\n result[locationSetID] = area;\n }\n });\n\n // locations included\n hits.forEach(prop => {\n if (prop.id[0] === '+') return; // skip - it's a locationset\n const locationID = prop.id;\n const included = thiz._locationIncludedIn.get(locationID);\n (included || []).forEach(locationSetID => {\n const area = thiz._knownLocationSets.get(locationSetID);\n if (area) {\n result[locationSetID] = area;\n }\n });\n });\n\n // locations excluded\n hits.forEach(prop => {\n if (prop.id[0] === '+') return; // skip - it's a locationset\n const locationID = prop.id;\n const excluded = thiz._locationExcludedIn.get(locationID);\n (excluded || []).forEach(locationSetID => {\n delete result[locationSetID];\n });\n });\n\n return result;\n }\n\n\n // Direct access to the location-conflation resolver\n loco() {\n return _loco;\n }\n}\n\n\nconst _sharedLocationManager = new LocationManager();\nexport { _sharedLocationManager as locationManager };\n\n", "import { t, localizer } from '../../core/localizer';\nimport { geoSphericalDistance, geoVecNormalizedDot } from '../../geo';\nimport { uiCmd } from '../cmd';\n\nexport function pointBox(loc, context) {\n var rect = context.surfaceRect();\n var point = context.curtainProjection(loc);\n return {\n left: point[0] + rect.left - 40,\n top: point[1] + rect.top - 60,\n width: 80,\n height: 90\n };\n}\n\n\nexport function pad(locOrBox, padding, context) {\n var box;\n if (locOrBox instanceof Array) {\n var rect = context.surfaceRect();\n var point = context.curtainProjection(locOrBox);\n box = {\n left: point[0] + rect.left,\n top: point[1] + rect.top\n };\n } else {\n box = locOrBox;\n }\n\n return {\n left: box.left - padding,\n top: box.top - padding,\n width: (box.width || 0) + 2 * padding,\n height: (box.width || 0) + 2 * padding\n };\n}\n\n\nexport function icon(name, svgklass, useklass) {\n return '' +\n '';\n}\n\nvar helpStringReplacements;\n\n// Returns the localized HTML element for `id` with a standardized set of icon, key, and\n// label replacements suitable for tutorials and documentation. Optionally supplemented\n// with custom `replacements`\nexport function helpHtml(id, replacements) {\n // only load these the first time\n if (!helpStringReplacements) {\n /* eslint-disable sort-keys */\n helpStringReplacements = {\n // insert icons corresponding to various UI elements\n point_icon: icon('#iD-icon-point', 'inline'),\n line_icon: icon('#iD-icon-line', 'inline'),\n area_icon: icon('#iD-icon-area', 'inline'),\n note_icon: icon('#iD-icon-note', 'inline add-note'),\n plus: icon('#iD-icon-plus', 'inline'),\n minus: icon('#iD-icon-minus', 'inline'),\n layers_icon: icon('#iD-icon-layers', 'inline'),\n data_icon: icon('#iD-icon-data', 'inline'),\n inspect: icon('#iD-icon-inspect', 'inline'),\n help_icon: icon('#iD-icon-help', 'inline'),\n undo_icon: icon(localizer.textDirection() === 'rtl' ? '#iD-icon-redo' : '#iD-icon-undo', 'inline'),\n redo_icon: icon(localizer.textDirection() === 'rtl' ? '#iD-icon-undo' : '#iD-icon-redo', 'inline'),\n save_icon: icon('#iD-icon-save', 'inline'),\n\n // operation icons\n circularize_icon: icon('#iD-operation-circularize', 'inline operation'),\n continue_icon: icon('#iD-operation-continue', 'inline operation'),\n copy_icon: icon('#iD-operation-copy', 'inline operation'),\n delete_icon: icon('#iD-operation-delete', 'inline operation'),\n disconnect_icon: icon('#iD-operation-disconnect', 'inline operation'),\n downgrade_icon: icon('#iD-operation-downgrade', 'inline operation'),\n extract_icon: icon('#iD-operation-extract', 'inline operation'),\n merge_icon: icon('#iD-operation-merge', 'inline operation'),\n move_icon: icon('#iD-operation-move', 'inline operation'),\n orthogonalize_icon: icon('#iD-operation-orthogonalize', 'inline operation'),\n paste_icon: icon('#iD-operation-paste', 'inline operation'),\n reflect_long_icon: icon('#iD-operation-reflect-long', 'inline operation'),\n reflect_short_icon: icon('#iD-operation-reflect-short', 'inline operation'),\n reverse_icon: icon('#iD-operation-reverse', 'inline operation'),\n rotate_icon: icon('#iD-operation-rotate', 'inline operation'),\n split_icon: icon('#iD-operation-split', 'inline operation'),\n straighten_icon: icon('#iD-operation-straighten', 'inline operation'),\n\n // interaction icons\n leftclick: icon('#iD-walkthrough-mouse-left', 'inline operation'),\n rightclick: icon('#iD-walkthrough-mouse-right', 'inline operation'),\n mousewheel_icon: icon('#iD-walkthrough-mousewheel', 'inline operation'),\n tap_icon: icon('#iD-walkthrough-tap', 'inline operation'),\n doubletap_icon: icon('#iD-walkthrough-doubletap', 'inline operation'),\n longpress_icon: icon('#iD-walkthrough-longpress', 'inline operation'),\n touchdrag_icon: icon('#iD-walkthrough-touchdrag', 'inline operation'),\n pinch_icon: icon('#iD-walkthrough-pinch-apart', 'inline operation'),\n\n // insert keys; may be localized and platform-dependent\n shift: uiCmd.display('\u21E7'),\n alt: uiCmd.display('\u2325'),\n return: uiCmd.display('\u21B5'),\n esc: t.html('shortcuts.key.esc'),\n space: t.html('shortcuts.key.space'),\n add_note_key: t.html('modes.add_note.key'),\n help_key: t.html('help.key'),\n shortcuts_key: t.html('shortcuts.toggle.key'),\n\n // reference localized UI labels directly so that they'll always match\n save: t.html('save.title'),\n undo: t.html('undo.title'),\n redo: t.html('redo.title'),\n upload: t.html('commit.save'),\n point: t.html('modes.add_point.title'),\n line: t.html('modes.add_line.title'),\n area: t.html('modes.add_area.title'),\n note: t.html('modes.add_note.label'),\n\n circularize: t.html('operations.circularize.title'),\n continue: t.html('operations.continue.title'),\n copy: t.html('operations.copy.title'),\n delete: t.html('operations.delete.title'),\n disconnect: t.html('operations.disconnect.title'),\n downgrade: t.html('operations.downgrade.title'),\n extract: t.html('operations.extract.title'),\n merge: t.html('operations.merge.title'),\n move: t.html('operations.move.title'),\n orthogonalize: t.html('operations.orthogonalize.title'),\n paste: t.html('operations.paste.title'),\n reflect_long: t.html('operations.reflect.title.long'),\n reflect_short: t.html('operations.reflect.title.short'),\n reverse: t.html('operations.reverse.title'),\n rotate: t.html('operations.rotate.title'),\n split: t.html('operations.split.title'),\n straighten: t.html('operations.straighten.title'),\n\n map_data: t.html('map_data.title'),\n osm_notes: t.html('map_data.layers.notes.title'),\n fields: t.html('inspector.fields'),\n tags: t.html('inspector.tags'),\n relations: t.html('inspector.relations'),\n new_relation: t.html('inspector.new_relation'),\n turn_restrictions: t.html('_tagging.presets.fields.restrictions.label'),\n background_settings: t.html('background.description'),\n imagery_offset: t.html('background.fix_misalignment'),\n start_the_walkthrough: t.html('splash.walkthrough'),\n help: t.html('help.title'),\n ok: t.html('intro.ok')\n };\n /* eslint-enable sort-keys */\n for (var key in helpStringReplacements) {\n helpStringReplacements[key] = { html: helpStringReplacements[key] };\n }\n }\n\n var reps;\n if (replacements) {\n reps = Object.assign(replacements, helpStringReplacements);\n } else {\n reps = helpStringReplacements;\n }\n\n return t.html(id, reps)\n // use keyboard key styling for shortcuts\n .replace(/\\`(.*?)\\`/g, '$1');\n}\n\n\nfunction slugify(text) {\n return text.toString().toLowerCase()\n .replace(/\\s+/g, '-') // Replace spaces with -\n .replace(/[^\\w\\-]+/g, '') // Remove all non-word chars\n .replace(/\\-\\-+/g, '-') // Replace multiple - with single -\n .replace(/^-+/, '') // Trim - from start of text\n .replace(/-+$/, ''); // Trim - from end of text\n}\n\n\n// console warning for missing walkthrough names\nexport var missingStrings = {};\nfunction checkKey(key, text) {\n if (t(key, { default: undefined}) === undefined) {\n if (missingStrings.hasOwnProperty(key)) return; // warn once\n missingStrings[key] = text;\n var missing = key + ': ' + text;\n if (typeof console !== 'undefined') console.log(missing); // eslint-disable-line\n }\n}\n\n\nexport function localize(obj) {\n var key;\n\n // Assign name if entity has one..\n var name = obj.tags && obj.tags.name;\n if (name) {\n key = 'intro.graph.name.' + slugify(name);\n obj.tags.name = t(key, { default: name });\n checkKey(key, name);\n }\n\n // Assign street name if entity has one..\n var street = obj.tags && obj.tags['addr:street'];\n if (street) {\n key = 'intro.graph.name.' + slugify(street);\n obj.tags['addr:street'] = t(key, { default: street });\n checkKey(key, street);\n\n // Add address details common across walkthrough..\n var addrTags = [\n 'block_number', 'city', 'county', 'district', 'hamlet', 'neighbourhood',\n 'postcode', 'province', 'quarter', 'state', 'subdistrict', 'suburb'\n ];\n addrTags.forEach(function(k) {\n var key = 'intro.graph.' + k;\n var tag = 'addr:' + k;\n var val = obj.tags && obj.tags[tag];\n var str = t(key, { default: val });\n\n if (str) {\n if (str.match(/^<.*>$/) !== null) {\n delete obj.tags[tag];\n } else {\n obj.tags[tag] = str;\n }\n }\n });\n }\n\n return obj;\n}\n\n\n// Used to detect squareness.. some duplicataion of code from actionOrthogonalize.\nexport function isMostlySquare(points) {\n // note: uses 15 here instead of the 12 from actionOrthogonalize because\n // actionOrthogonalize can actually straighten some larger angles as it iterates\n var threshold = 15; // degrees within right or straight\n var lowerBound = Math.cos((90 - threshold) * Math.PI / 180); // near right\n var upperBound = Math.cos(threshold * Math.PI / 180); // near straight\n\n for (var i = 0; i < points.length; i++) {\n var a = points[(i - 1 + points.length) % points.length];\n var origin = points[i];\n var b = points[(i + 1) % points.length];\n\n var dotp = geoVecNormalizedDot(a, b, origin);\n var mag = Math.abs(dotp);\n if (mag > lowerBound && mag < upperBound) {\n return false;\n }\n }\n\n return true;\n}\n\n\nexport function selectMenuItem(context, operation) {\n return context.container().select('.edit-menu .edit-menu-item-' + operation);\n}\n\n\nexport function transitionTime(point1, point2) {\n var distance = geoSphericalDistance(point1, point2);\n if (distance === 0) {\n return 0;\n } else if (distance < 80) {\n return 500;\n } else {\n return 1000;\n }\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { marked } from 'marked';\nimport { t, localizer } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { icon } from './intro/helper';\n\n\n// This currently only works with the 'restrictions' field\n// It borrows some code from uiHelp\n\nexport function uiFieldHelp(context, fieldName) {\n var fieldHelp = {};\n var _inspector = d3_select(null);\n var _wrap = d3_select(null);\n var _body = d3_select(null);\n\n var fieldHelpKeys = {\n restrictions: [\n ['about',[\n 'about',\n 'from_via_to',\n 'maxdist',\n 'maxvia'\n ]],\n ['inspecting',[\n 'about',\n 'from_shadow',\n 'allow_shadow',\n 'restrict_shadow',\n 'only_shadow',\n 'restricted',\n 'only'\n ]],\n ['modifying',[\n 'about',\n 'indicators',\n 'allow_turn',\n 'restrict_turn',\n 'only_turn'\n ]],\n ['tips',[\n 'simple',\n 'simple_example',\n 'indirect',\n 'indirect_example',\n 'indirect_noedit'\n ]]\n ]\n };\n\n var fieldHelpHeadings = {};\n\n var replacements = {\n distField: { html: t.html('restriction.controls.distance') },\n viaField: { html: t.html('restriction.controls.via') },\n fromShadow: { html: icon('#iD-turn-shadow', 'inline shadow from') },\n allowShadow: { html: icon('#iD-turn-shadow', 'inline shadow allow') },\n restrictShadow: { html: icon('#iD-turn-shadow', 'inline shadow restrict') },\n onlyShadow: { html: icon('#iD-turn-shadow', 'inline shadow only') },\n allowTurn: { html: icon('#iD-turn-yes', 'inline turn') },\n restrictTurn: { html: icon('#iD-turn-no', 'inline turn') },\n onlyTurn: { html: icon('#iD-turn-only', 'inline turn') }\n };\n\n\n // For each section, squash all the texts into a single markdown document\n var docs = fieldHelpKeys[fieldName].map(function(key) {\n var helpkey = 'help.field.' + fieldName + '.' + key[0];\n var text = key[1].reduce(function(all, part) {\n var subkey = helpkey + '.' + part;\n var depth = fieldHelpHeadings[subkey]; // is this subkey a heading?\n var hhh = depth ? Array(depth + 1).join('#') + ' ' : ''; // if so, prepend with some ##'s\n return all + hhh + t.html(subkey, replacements) + '\\n\\n';\n }, '');\n\n return {\n key: helpkey,\n title: t.html(helpkey + '.title'),\n html: marked(text.trim())\n };\n });\n\n\n function show() {\n updatePosition();\n\n _body\n .classed('hide', false)\n .style('opacity', '0')\n .transition()\n .duration(200)\n .style('opacity', '1');\n }\n\n\n function hide() {\n _body\n .classed('hide', true)\n .transition()\n .duration(200)\n .style('opacity', '0')\n .on('end', function () {\n _body.classed('hide', true);\n });\n }\n\n\n function clickHelp(index) {\n var d = docs[index];\n var tkeys = fieldHelpKeys[fieldName][index][1];\n\n _body.selectAll('.field-help-nav-item')\n .classed('active', function(d, i) { return i === index; });\n\n var content = _body.selectAll('.field-help-content')\n .html(d.html);\n\n // class the paragraphs so we can find and style them\n content.selectAll('p')\n .attr('class', function(d, i) { return tkeys[i]; });\n\n // insert special content for certain help sections\n if (d.key === 'help.field.restrictions.inspecting') {\n content\n .insert('img', 'p.from_shadow')\n .attr('class', 'field-help-image cf')\n .attr('src', context.imagePath('tr_inspect.gif'));\n\n } else if (d.key === 'help.field.restrictions.modifying') {\n content\n .insert('img', 'p.allow_turn')\n .attr('class', 'field-help-image cf')\n .attr('src', context.imagePath('tr_modify.gif'));\n }\n }\n\n\n fieldHelp.button = function(selection) {\n if (_body.empty()) return;\n\n var button = selection.selectAll('.field-help-button')\n .data([0]);\n\n // enter/update\n button.enter()\n .append('button')\n .attr('class', 'field-help-button')\n .call(svgIcon('#iD-icon-help'))\n .merge(button)\n .on('click', function (d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n if (_body.classed('hide')) {\n show();\n } else {\n hide();\n }\n });\n };\n\n\n function updatePosition() {\n var wrap = _wrap.node();\n var inspector = _inspector.node();\n var wRect = wrap.getBoundingClientRect();\n var iRect = inspector.getBoundingClientRect();\n\n _body\n .style('top', wRect.top + inspector.scrollTop - iRect.top + 'px');\n }\n\n\n fieldHelp.body = function(selection) {\n // This control expects the field to have a form-field-input-wrap div\n _wrap = selection.selectAll('.form-field-input-wrap');\n if (_wrap.empty()) return;\n\n // absolute position relative to the inspector, so it \"floats\" above the fields\n _inspector = context.container().select('.sidebar .entity-editor-pane .inspector-body');\n if (_inspector.empty()) return;\n\n _body = _inspector.selectAll('.field-help-body')\n .data([0]);\n\n var enter = _body.enter()\n .append('div')\n .attr('class', 'field-help-body hide'); // initially hidden\n\n var titleEnter = enter\n .append('div')\n .attr('class', 'field-help-title cf');\n\n titleEnter\n .append('h2')\n .attr('class', ((localizer.textDirection() === 'rtl') ? 'fr' : 'fl'))\n .call(t.append('help.field.' + fieldName + '.title'));\n\n titleEnter\n .append('button')\n .attr('class', 'fr close')\n .attr('title', t('icons.close'))\n .on('click', function(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n hide();\n })\n .call(svgIcon('#iD-icon-close'));\n\n var navEnter = enter\n .append('div')\n .attr('class', 'field-help-nav cf');\n\n var titles = docs.map(function(d) { return d.title; });\n navEnter.selectAll('.field-help-nav-item')\n .data(titles)\n .enter()\n .append('div')\n .attr('class', 'field-help-nav-item')\n .html(function(d) { return d; })\n .on('click', function(d3_event, d) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n clickHelp(titles.indexOf(d));\n });\n\n enter\n .append('div')\n .attr('class', 'field-help-content');\n\n _body = _body\n .merge(enter);\n\n clickHelp(0);\n };\n\n\n return fieldHelp;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport {\n select as d3_select\n} from 'd3-selection';\nimport { omit } from 'es-toolkit/compat';\n\nimport { utilRebind } from '../../util/rebind';\nimport { t } from '../../core/localizer';\nimport { actionReverse } from '../../actions/reverse';\nimport { svgIcon } from '../../svg/icon';\nimport { utilCheckTagDictionary } from '../../util';\nimport { osmOneWayTags } from '../../osm/tags';\n\nexport { uiFieldCheck as uiFieldDefaultCheck };\nexport { uiFieldCheck as uiFieldOnewayCheck };\n\n\nexport function uiFieldCheck(field, context) {\n var dispatch = d3_dispatch('change');\n var options = field.options;\n var values = [];\n var texts = [];\n\n var _tags;\n\n var input = d3_select(null);\n var text = d3_select(null);\n var label = d3_select(null);\n var reverser = d3_select(null);\n\n var _impliedYes;\n var _entityIDs = [];\n var _value;\n\n\n var stringsField = field.resolveReference('stringsCrossReference');\n if (!options && stringsField.options) {\n options = stringsField.options;\n }\n\n if (options) {\n for (var i in options) {\n var v = options[i];\n values.push(v === 'undefined' ? undefined : v);\n texts.push(stringsField.t.append('options.' + v, { 'default': v }));\n }\n } else {\n values = [undefined, 'yes'];\n texts = [t.append('inspector.unknown'), t.append('inspector.check.yes')];\n if (field.type !== 'defaultCheck') {\n values.push('no');\n texts.push(t.append('inspector.check.no'));\n }\n }\n\n\n // Checks tags to see whether an undefined value is \"Assumed to be Yes\"\n function checkImpliedYes() {\n _impliedYes = (field.id === 'oneway_yes');\n\n // hack: pretend `oneway` field is a `oneway_yes` field\n // where implied oneway tag exists (e.g. `junction=roundabout`) #2220, #1841\n if (field.id === 'oneway') {\n var entity = context.entity(_entityIDs[0]);\n if (entity.type === 'way' && !!utilCheckTagDictionary(entity.tags, omit(osmOneWayTags, 'oneway'))) {\n _impliedYes = true;\n texts[0] = t.append('_tagging.presets.fields.oneway_yes.options.undefined');\n }\n }\n }\n\n\n function reverserHidden() {\n if (!context.container().select('div.inspector-hover').empty()) return true;\n return !(_value === 'yes' || (_impliedYes && !_value));\n }\n\n\n function reverserSetText(selection) {\n var entity = _entityIDs.length && context.hasEntity(_entityIDs[0]);\n if (reverserHidden() || !entity) return selection;\n\n var first = entity.first();\n var last = entity.isClosed() ? entity.nodes[entity.nodes.length - 2] : entity.last();\n var pseudoDirection = first < last;\n var icon = pseudoDirection ? '#iD-icon-forward' : '#iD-icon-backward';\n\n selection.selectAll('.reverser-span')\n .text('')\n .call(t.append('inspector.check.reverser'))\n .call(svgIcon(icon, 'inline'));\n\n return selection;\n }\n\n\n var check = function(selection) {\n checkImpliedYes();\n\n label = selection.selectAll('.form-field-input-wrap')\n .data([0]);\n\n var enter = label.enter()\n .append('label')\n .attr('class', 'form-field-input-wrap form-field-input-check');\n\n enter\n .append('input')\n .property('indeterminate', field.type !== 'defaultCheck')\n .attr('type', 'checkbox')\n .attr('id', field.domId);\n\n enter\n .append('span')\n .call(texts[0])\n .attr('class', 'value');\n\n if (field.type === 'onewayCheck') {\n enter\n .append('button')\n .attr('class', 'reverser' + (reverserHidden() ? ' hide' : ''))\n .append('span')\n .attr('class', 'reverser-span');\n }\n\n label = label.merge(enter);\n input = label.selectAll('input');\n text = label.selectAll('span.value');\n\n input\n .on('click', function(d3_event) {\n d3_event.stopPropagation();\n var t = {};\n\n if (Array.isArray(_tags[field.key])) {\n if (values.indexOf('yes') !== -1) {\n t[field.key] = 'yes';\n } else {\n t[field.key] = values[0];\n }\n } else {\n t[field.key] = values[(values.indexOf(_value) + 1) % values.length];\n }\n\n // Don't cycle through `alternating` or `reversible` states - #4970\n // (They are supported as translated strings, but should not toggle with clicks)\n if (t[field.key] === 'reversible' || t[field.key] === 'alternating') {\n t[field.key] = values[0];\n }\n\n dispatch.call('change', this, t);\n });\n\n if (field.type === 'onewayCheck') {\n reverser = label.selectAll('.reverser');\n\n reverser\n .call(reverserSetText)\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n context.perform(\n function(graph) {\n for (var i in _entityIDs) {\n graph = actionReverse(_entityIDs[i])(graph);\n }\n return graph;\n },\n t('operations.reverse.annotation.line', { n: 1 })\n );\n\n // must manually revalidate since no 'change' event was called\n context.validator().validate();\n\n d3_select(this)\n .call(reverserSetText);\n });\n }\n };\n\n\n check.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return check;\n };\n\n\n check.tags = function(tags) {\n\n _tags = tags;\n\n function isChecked(val) {\n return val !== 'no' && val !== '' && val !== undefined && val !== null;\n }\n\n function textFor(val) {\n if (val === '') val = undefined;\n var index = values.indexOf(val);\n return (index !== -1 ? texts[index] : ('\"' + val + '\"'));\n }\n\n checkImpliedYes();\n\n var isMixed = Array.isArray(tags[field.key]);\n\n _value = !isMixed && tags[field.key] && tags[field.key].toLowerCase();\n\n if (field.type === 'onewayCheck' && (_value === '1' || _value === '-1')) {\n _value = 'yes';\n }\n\n input\n .property('indeterminate', isMixed || (field.type !== 'defaultCheck' && !_value))\n .property('checked', isChecked(_value));\n\n const textForValue = textFor(_value);\n text\n .text('')\n .call(isMixed\n ? t.append('inspector.multiple_values')\n : typeof textForValue === 'string' ? selection => selection.text(textForValue) : textForValue)\n .classed('mixed', isMixed);\n\n label\n .classed('set', !!_value);\n\n if (field.type === 'onewayCheck') {\n reverser\n .classed('hide', reverserHidden())\n .call(reverserSetText);\n }\n };\n\n\n check.focus = function() {\n input.node().focus();\n };\n\n return utilRebind(check, dispatch, 'on');\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg';\nimport {\n utilUnicodeCharsCount,\n utilCleanOsmString\n} from '../util';\nimport { uiPopover } from './popover';\n\n\nexport function uiLengthIndicator(maxChars) {\n var _wrap = d3_select(null);\n var _tooltip = uiPopover('tooltip max-length-warning')\n .placement('bottom')\n .hasArrow(true)\n .content(() => selection => {\n selection.text('');\n selection.call(svgIcon('#iD-icon-alert', 'inline'));\n selection.call(t.append('inspector.max_length_reached', { maxChars }));\n });\n var _silent = false;\n\n var lengthIndicator = function(selection) {\n _wrap = selection.selectAll('span.length-indicator-wrap').data([0]);\n _wrap = _wrap.enter()\n .append('span')\n .merge(_wrap)\n .classed('length-indicator-wrap', true);\n selection.call(_tooltip);\n };\n\n lengthIndicator.update = function(val) {\n const strLen = utilUnicodeCharsCount(utilCleanOsmString(val, Number.POSITIVE_INFINITY));\n\n let indicator = _wrap.selectAll('span.length-indicator')\n .data([strLen]);\n\n indicator.enter()\n .append('span')\n .merge(indicator)\n .classed('length-indicator', true)\n .classed('limit-reached', d => d > maxChars)\n .style('border-right-width', d => `${Math.abs(maxChars - d) * 2}px`)\n .style('margin-right', d => d > maxChars\n ? `${(maxChars - d) * 2}px`\n : 0)\n .style('opacity', d => d > maxChars * 0.8\n ? Math.min(1, (d / maxChars - 0.8) / (1 - 0.8))\n : 0)\n .style('pointer-events', d => d > maxChars * 0.8 ? null: 'none');\n\n if (_silent) return;\n if (strLen > maxChars) {\n _tooltip.show();\n } else {\n _tooltip.hide();\n }\n };\n\n lengthIndicator.silent = function(val) {\n if (!arguments.length) return _silent;\n _silent = val;\n return lengthIndicator;\n };\n\n return lengthIndicator;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { drag as d3_drag } from 'd3-drag';\nimport * as countryCoder from '@rapideditor/country-coder';\n\nimport { fileFetcher } from '../../core/file_fetcher';\nimport { localizer, t } from '../../core/localizer';\nimport { services } from '../../services';\nimport { uiCombobox } from '../combobox';\nimport { svgIcon } from '../../svg/icon';\n\nimport { utilKeybinding } from '../../util/keybinding';\nimport { utilArrayUniq, utilDetect, utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent, utilUnicodeCharsCount } from '../../util';\nimport { uiLengthIndicator } from '../length_indicator';\nimport { deprecatedTagValuesByKey } from '../../osm/deprecated';\nimport { osmIsoCountryKeys } from '../../osm/tags';\n\nexport {\n uiFieldCombo as uiFieldManyCombo,\n uiFieldCombo as uiFieldMultiCombo,\n uiFieldCombo as uiFieldNetworkCombo,\n uiFieldCombo as uiFieldSemiCombo,\n uiFieldCombo as uiFieldTypeCombo\n};\n\nexport function uiFieldCombo(field, context) {\n var dispatch = d3_dispatch('change');\n var _isMulti = (field.type === 'multiCombo' || field.type === 'manyCombo');\n var _isNetwork = (field.type === 'networkCombo');\n var _isSemi = (field.type === 'semiCombo');\n var _showTagInfoSuggestions = field.type !== 'manyCombo' && field.autoSuggestions !== false;\n var _allowCustomValues = field.type !== 'manyCombo' && field.customValues !== false;\n var _snake_case = (field.snake_case || (field.snake_case === undefined));\n var _combobox = uiCombobox(context, 'combo-' + field.safeid)\n .caseSensitive(field.caseSensitive)\n .minItems(1);\n var _container = d3_select(null);\n var _inputWrap = d3_select(null);\n var _input = d3_select(null);\n var _lengthIndicator = uiLengthIndicator(context.maxCharsForTagValue());\n var _comboData = [];\n var _multiData = [];\n var _entityIDs = [];\n var _tags;\n var _countryCode;\n var _staticPlaceholder;\n var _customOptions = [];\n\n // initialize deprecated tags array\n var _dataDeprecated = [];\n fileFetcher.get('deprecated')\n .then(function(d) { _dataDeprecated = d; })\n .catch(function() { /* ignore */ });\n\n\n // ensure multiCombo field.key ends with a ':'\n if (_isMulti && field.key && /[^:]$/.test(field.key)) {\n field.key += ':';\n }\n\n\n function snake(s) {\n return s.replace(/\\s+/g, '_');\n }\n\n function clean(s) {\n return s.split(';')\n .map(function(s) { return s.trim(); })\n .join(';');\n }\n\n // windows does not support emoji flags\n const showEmojiFlags = utilDetect().os !== 'win';\n\n // adds emoji flags to country dropdown and input\n function addFlagIcon(selection, name, flag) {\n if (showEmojiFlags && flag) {\n const icon = selection.insert('span', ':first-child')\n .attr('class', 'tag-value-icon');\n\n icon.append('span')\n .attr('class', 'emoji')\n .text(flag);\n }\n selection.insert('span')\n .attr('class', 'tag-value')\n .text(name);\n }\n\n // cache for language and region maps\n let languageByCodes = null;\n let regionByCodes = null;\n\n // Helper to get regionByCodes\n const buildCountry = () => {\n if (regionByCodes) return regionByCodes;\n const localeCode = localizer.localeCode();\n\n const regionNames = new Intl.DisplayNames(localeCode, { type: 'region' });\n const features = countryCoder.borders.features;\n\n regionByCodes = {};\n\n for (const feature of features) {\n const code = feature.properties.iso1A2;\n let flag = feature.properties.emojiFlag;\n // if the flag is not present like for 'FX' code, we will look for the corresponding country flag for that code\n if (!flag && features?.properties?.country) {\n flag = countryCoder.feature(feature.properties.country).properties.emojiFlag;\n }\n if (!code) continue;\n\n try {\n const name = regionNames.of(code);\n if (!name) continue;\n regionByCodes[code] = {name, flag};\n } catch {\n continue;\n }\n }\n\n return regionByCodes;\n };\n\n // Helper to get languageByCodes\n const buildLanguages = () => {\n if (languageByCodes) return languageByCodes;\n\n languageByCodes = {};\n let codes = Object.keys(localizer.languages());\n\n for (const code of codes) {\n const name = localizer.languageName(code);\n if (!name || name === code) continue;\n languageByCodes[code] = name;\n }\n\n return languageByCodes;\n };\n\n // returns the tag value for a display value\n // (for multiCombo, dval should be the key suffix, not the entire key)\n function tagValue(dval) {\n dval = clean(dval || '');\n\n var found = getOptions(true).find(function(o) {\n return o.key && clean(o.value) === dval;\n });\n if (found) return found.key;\n\n if (field.type === 'typeCombo' && !dval) {\n return 'yes';\n }\n\n return restrictTagValueSpelling(dval) || undefined;\n }\n\n function restrictTagValueSpelling(dval) {\n if (_snake_case) {\n dval = snake(dval);\n }\n\n if (!field.caseSensitive) {\n if (!(field.key === 'type' && dval === 'associatedStreet')) {\n // don't lowercase \"type=associatedStreet\" tag\n // https://github.com/openstreetmap/iD/issues/9639\n dval = dval.toLowerCase();\n }\n }\n\n return dval;\n }\n\n\n function getLabelId(field, v) {\n return field.hasTextForStringId(`options.${v}.title`)\n ? `options.${v}.title`\n : `options.${v}`;\n }\n\n\n // returns the display value for a tag value\n // (for multiCombo, tval should be the key suffix, not the entire key)\n function displayValue(tval) {\n tval = tval || '';\n // Issue #11652\n // Ignore language: key and when tval is not others\n if (field.key === 'language:' && tval !== 'others'){\n let langName = buildLanguages()[tval];\n if (langName) return langName;\n }\n\n if (osmIsoCountryKeys.has(field.key) && tval) {\n const data = buildCountry()[tval];\n if (data) return data.name;\n return tval;\n }\n\n var stringsField = field.resolveReference('stringsCrossReference');\n const labelId = getLabelId(stringsField, tval);\n if (stringsField.hasTextForStringId(labelId)) {\n return stringsField.t(labelId, { default: tval });\n }\n\n if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {\n return '';\n }\n\n return tval;\n }\n\n\n // returns function which renders the display value for a tag value\n // (for multiCombo, tval should be the key suffix, not the entire key)\n function renderValue(tval) {\n tval = tval || '';\n\n // Issue #11652\n // Ignore language: key and when tval is not others\n if (field.key === 'language:' && tval !== 'others') {\n let langName = buildLanguages()[tval];\n if (langName) return selection => selection.text(langName);\n }\n\n if (osmIsoCountryKeys.has(field.key) && tval) {\n const data = buildCountry()[tval];\n if (data) return selection => addFlagIcon(selection, data.name, data.flag);\n return selection => selection.text(tval);\n }\n\n var stringsField = field.resolveReference('stringsCrossReference');\n const labelId = getLabelId(stringsField, tval);\n if (stringsField.hasTextForStringId(labelId)) {\n return stringsField.t.append(labelId, { default: tval });\n }\n\n if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {\n tval = '';\n }\n\n return selection => selection.text(tval);\n }\n\n\n // Compute the difference between arrays of objects by `value` property\n //\n // objectDifference([{value:1}, {value:2}, {value:3}], [{value:2}])\n // > [{value:1}, {value:3}]\n //\n function objectDifference(a, b) {\n return a.filter(function(d1) {\n return !b.some(function(d2) {\n return d1.value === d2.value;\n });\n });\n }\n\n\n function initCombo(selection, attachTo) {\n if (!_allowCustomValues) {\n selection.attr('readonly', 'readonly');\n }\n\n if (_showTagInfoSuggestions && services.taginfo) {\n selection.call(_combobox.fetcher(setTaginfoValues), attachTo);\n setTaginfoValues('', setPlaceholder);\n } else {\n selection.call(_combobox, attachTo);\n setTimeout(() => setStaticValues(setPlaceholder), 0);\n }\n }\n\n function getOptions(allOptions) {\n var stringsField = field.resolveReference('stringsCrossReference');\n const localeCode = localizer.localeCode();\n // Get dropdown list for language: key via localizer instead of taginfo\n if (field.key === 'language:') {\n const langMap = buildLanguages();\n\n let options = Object.entries(langMap).map(([code, name]) => {\n return {\n key: code,\n value: name,\n title: code, // the tooltip should show the raw-tag value\n display: selection => selection.text(name)\n };\n });\n\n const localeCode = localizer.localeCode();\n\n options.sort((a, b) => {\n return a.value.localeCompare(b.value, localeCode);\n });\n\n const v = 'others';\n const labelId = getLabelId(stringsField, v);\n\n // inserting others because it does not come via _dataLanguages\n options.push({\n key: v,\n value: stringsField.t(labelId, { default: v }),\n title: stringsField.t(`options.${v}.description`, { default: v }),\n description: stringsField.hasTextForStringId(`options.${v}.description`)\n ? stringsField.t(`options.${v}.description`) : undefined,\n display: addComboboxIcons(stringsField.t.append(labelId, { default: v }), v),\n klass: stringsField.hasTextForStringId(labelId) ? '' : 'raw-option'\n });\n\n\n return options;\n }\n\n // Get dropdown list for country key\n if (osmIsoCountryKeys.has(field.key)) {\n const countryMap = buildCountry();\n\n let options = Object.entries(countryMap).map(([c, {name, flag}]) => {\n return {\n key: c,\n value: name,\n title: c, // the tooltip should show the raw-tag value\n display: selection => addFlagIcon(selection, name, flag),\n sortname: name, // store just the name without emojis to sort the names\n klass: 'has-icon' // to specifically target the emoji css\n };\n });\n\n options.sort((a, b) => {\n return a.sortname.localeCompare(b.sortname, localeCode);\n });\n\n return options;\n }\n\n if (!(field.options || stringsField.options)) return [];\n\n let options;\n if (allOptions !== true) {\n options = field.options || stringsField.options;\n } else {\n options = [].concat(field.options, stringsField.options).filter(Boolean);\n }\n const result = options.map(function(v) {\n const labelId = getLabelId(stringsField, v);\n return {\n key: v,\n value: stringsField.t(labelId, { default: v }),\n title: stringsField.t(`options.${v}.description`, { default: v }),\n description: stringsField.hasTextForStringId(`options.${v}.description`)\n ? stringsField.t(`options.${v}.description`) : undefined,\n display: addComboboxIcons(stringsField.t.append(labelId, { default: v }), v),\n klass: stringsField.hasTextForStringId(labelId) ? '' : 'raw-option'\n };\n });\n return [...result, ..._customOptions];\n }\n\n\n function hasStaticValues() {\n return getOptions().length > 0;\n }\n\n\n function setStaticValues(callback, filter) {\n _comboData = getOptions();\n\n if (filter !== undefined) {\n _comboData = _comboData.filter(filter);\n }\n\n if (!field.allowDuplicates) {\n _comboData = objectDifference(_comboData, _multiData);\n }\n _combobox.data(_comboData);\n\n // hide the caret if there are no suggestions\n _container.classed('empty-combobox', _comboData.length === 0);\n\n if (callback) callback(_comboData);\n }\n\n\n function setTaginfoValues(q, callback) {\n var queryFilter = d => d.value.toLowerCase().includes(q.toLowerCase()) || d.key.toLowerCase().includes(q.toLowerCase());\n if (hasStaticValues()) {\n setStaticValues(callback, queryFilter);\n\n // If it is language field or a country field, we don't need to request for values, we get it from getOptions\n if (field.key === 'language:') return;\n if (osmIsoCountryKeys.has(field.key)) return;\n }\n\n var stringsField = field.resolveReference('stringsCrossReference');\n var fn = _isMulti ? 'multikeys' : 'values';\n var query = (_isMulti ? field.key : '') + q;\n var hasCountryPrefix = _isNetwork && _countryCode && _countryCode.indexOf(q.toLowerCase()) === 0;\n if (hasCountryPrefix) {\n query = _countryCode + ':';\n }\n\n var params = {\n debounce: (q !== ''),\n key: field.key,\n query: query\n };\n\n if (_entityIDs.length) {\n params.geometry = context.graph().geometry(_entityIDs[0]);\n }\n\n services.taginfo[fn](params, function(err, data) {\n if (err) return;\n\n // don't show the fallback value\n data = data.filter(d =>\n field.type !== 'typeCombo' || d.value !== 'yes');\n\n // don't show misspelled values\n data = data.filter(d => {\n var value = d.value;\n if (_isMulti) {\n value = value.slice(field.key.length);\n }\n return value === restrictTagValueSpelling(value);\n });\n\n var deprecatedValues = deprecatedTagValuesByKey(_dataDeprecated)[field.key];\n if (deprecatedValues) {\n // don't suggest deprecated tag values\n data = data.filter(d =>\n !deprecatedValues.includes(d.value));\n }\n\n if (hasCountryPrefix) {\n data = data.filter(d =>\n d.value.toLowerCase().indexOf(_countryCode + ':') === 0);\n }\n\n const additionalOptions = (field.options || stringsField.options || [])\n .filter(v => !data.some(dv => dv.value === (_isMulti ? field.key + v : v)))\n .map(v => ({ value: v }));\n\n // hide the caret if there are no suggestions\n _container.classed('empty-combobox', data.length === 0);\n\n _comboData = data.concat(additionalOptions).map(function(d) {\n var v = d.value;\n if (_isMulti) v = v.replace(field.key, '');\n const labelId = getLabelId(stringsField, v);\n var isLocalizable = stringsField.hasTextForStringId(labelId);\n var label = stringsField.t(labelId, { default: v });\n return {\n key: v,\n value: label,\n title: stringsField.t(`options.${v}.description`, { default:\n isLocalizable ? label : (d.title !== label ? d.title : '') }),\n description: stringsField.hasTextForStringId(`options.${v}.description`)\n ? stringsField.t(`options.${v}.description`) : undefined,\n display: addComboboxIcons(stringsField.t.append(labelId, { default: label }), v),\n klass: isLocalizable ? '' : 'raw-option'\n };\n });\n\n _comboData = _comboData.filter(queryFilter);\n\n if (!field.allowDuplicates) {\n _comboData = objectDifference(_comboData, _multiData);\n }\n if (callback) callback(_comboData, hasStaticValues());\n });\n }\n\n // adds icons to tag values which have one\n function addComboboxIcons(disp, value) {\n const iconsField = field.resolveReference('iconsCrossReference');\n if (iconsField.icons) {\n return function(selection) {\n var span = selection\n .insert('span', ':first-child')\n .attr('class', 'tag-value-icon');\n if (iconsField.icons[value]) {\n span.call(svgIcon(`#${iconsField.icons[value]}`));\n }\n disp.call(this, selection);\n };\n }\n return disp;\n }\n\n\n function setPlaceholder(values) {\n\n if (_isMulti || _isSemi) {\n _staticPlaceholder = field.placeholder() || t('inspector.add');\n } else {\n var vals = values\n .map(function(d) { return d.value; })\n .filter(function(s) { return s.length < 20; });\n\n var placeholders = vals.length > 1 ? vals : values.map(function(d) { return d.key; });\n _staticPlaceholder = field.placeholder() || placeholders.slice(0, 3).join(', ');\n }\n\n if (!/(\u2026|\\.\\.\\.)$/.test(_staticPlaceholder)) {\n _staticPlaceholder += '\u2026';\n }\n\n var ph;\n if (!_isMulti && !_isSemi && _tags && Array.isArray(_tags[field.key])) {\n ph = t('inspector.multiple_values');\n } else {\n ph = _staticPlaceholder;\n }\n\n _container.selectAll('input')\n .attr('placeholder', ph);\n\n // Hide 'Add' button if this field uses fixed set of\n // options and they're all currently used\n var hideAdd = (!_allowCustomValues && !values.length);\n _container.selectAll('.chiplist .input-wrap')\n .style('display', hideAdd ? 'none' : null);\n }\n\n\n function change() {\n var t = {};\n var val;\n\n if (_isMulti || _isSemi) {\n var vals;\n if (_isMulti) {\n vals = [tagValue(utilGetSetValue(_input))];\n } else if (_isSemi) {\n val = tagValue(utilGetSetValue(_input)) || '';\n val = val.replace(/,/g, ';');\n vals = val.split(';');\n }\n vals = vals.filter(Boolean);\n\n if (!vals.length) return;\n\n _container.classed('active', false);\n utilGetSetValue(_input, '');\n\n if (_isMulti) {\n utilArrayUniq(vals).forEach(function(v) {\n var key = (field.key || '') + v;\n if (_tags) {\n // don't set a multicombo value to 'yes' if it already has a non-'no' value\n // e.g. `language:de=main`\n var old = _tags[key];\n if (typeof old === 'string' && old.toLowerCase() !== 'no') return;\n }\n key = context.cleanTagKey(key);\n field.keys.push(key);\n t[key] = 'yes';\n });\n\n } else if (_isSemi) {\n var arr = _multiData.map(function(d) { return d.key; });\n arr = arr.concat(vals);\n if (!field.allowDuplicates) {\n arr = utilArrayUniq(arr);\n }\n t[field.key] = context.cleanTagValue(arr.filter(Boolean).join(';'));\n }\n\n window.setTimeout(function() { _input.node().focus(); }, 10);\n\n } else {\n var rawValue = utilGetSetValue(_input);\n\n // don't override multiple values with blank string\n if (!rawValue && Array.isArray(_tags[field.key])) return;\n\n val = context.cleanTagValue(tagValue(rawValue));\n t[field.key] = val || undefined;\n }\n\n dispatch.call('change', this, t);\n }\n\n\n function removeMultikey(d3_event, d) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n var t = {};\n if (_isMulti) {\n t[d.key] = undefined;\n } else if (_isSemi) {\n let arr = _multiData.map(item => item.key);\n\n // delete the value using the index, since a value\n // may exist multiple times in the array.\n arr.splice(d.index, 1);\n\n if (!field.allowDuplicates) {\n arr = utilArrayUniq(arr);\n }\n t[field.key] = arr.length ? arr.join(';') : undefined;\n\n _lengthIndicator.update(t[field.key]);\n }\n dispatch.call('change', this, t);\n }\n\n\n function invertMultikey(d3_event, d) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n var t = {};\n if (_isMulti) {\n t[d.key] = _tags[d.key] === 'yes' ? 'no' : 'yes';\n }\n dispatch.call('change', this, t);\n }\n\n\n function combo(selection) {\n _container = selection.selectAll('.form-field-input-wrap')\n .data([0]);\n\n var type = (_isMulti || _isSemi) ? 'multicombo': 'combo';\n _container = _container.enter()\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-' + type)\n .merge(_container);\n\n if (_isMulti || _isSemi) {\n _container = _container.selectAll('.chiplist')\n .data([0]);\n\n var listClass = 'chiplist';\n\n // Use a separate line for each value in the Destinations and Via fields\n // to mimic highway exit signs\n if (field.key === 'destination' || field.key === 'via') {\n listClass += ' full-line-chips';\n }\n\n _container = _container.enter()\n .append('ul')\n .attr('class', listClass)\n .on('click', function() {\n window.setTimeout(function() { _input.node().focus(); }, 10);\n })\n .merge(_container);\n\n\n _inputWrap = _container.selectAll('.input-wrap')\n .data([0]);\n\n _inputWrap = _inputWrap.enter()\n .append('li')\n .attr('class', 'input-wrap')\n .merge(_inputWrap);\n\n // Hide 'Add' button if this field uses fixed set of\n // options and they're all currently used\n var hideAdd = (!_allowCustomValues && !_comboData.length);\n _inputWrap.style('display', hideAdd ? 'none' : null);\n\n _input = _inputWrap.selectAll('input')\n .data([0]);\n } else {\n _input = _container.selectAll('input')\n .data([0]);\n }\n\n _input = _input.enter()\n .append('input')\n .attr('type', 'text')\n .attr('dir', 'auto')\n .attr('id', field.domId)\n .call(utilNoAuto)\n .call(initCombo, _container)\n .merge(_input);\n\n if (_isSemi) {\n _inputWrap.call(_lengthIndicator);\n } else if (!_isMulti) {\n _container.call(_lengthIndicator);\n }\n\n if (_isNetwork) {\n var extent = combinedEntityExtent();\n var countryCode = extent && countryCoder.iso1A2Code(extent.center());\n _countryCode = countryCode && countryCode.toLowerCase();\n }\n\n _input\n .on('change', change)\n .on('blur', change)\n .on('input', function() {\n let val = utilGetSetValue(_input);\n updateIcon(val);\n if (_isSemi && _tags[field.key]) {\n // when adding a new value to existing ones\n val += ';' + _tags[field.key];\n }\n _lengthIndicator.update(val);\n });\n\n _input\n .on('keydown.field', function(d3_event) {\n switch (d3_event.keyCode) {\n case 13: // \u21A9 Return\n _input.node().blur(); // blurring also enters the value\n d3_event.stopPropagation();\n break;\n }\n });\n\n if (_isMulti || _isSemi) {\n _combobox\n .on('accept', function() {\n _input.node().blur();\n _input.node().focus();\n });\n\n _input\n .on('focus', function() { _container.classed('active', true); });\n }\n\n _combobox\n .on('cancel', function() {\n _input.node().blur();\n })\n .on('update', function() {\n updateIcon(utilGetSetValue(_input));\n });\n }\n\n function updateIcon(value) {\n value = tagValue(value);\n let container = _container;\n if (field.type === 'multiCombo' || field.type === 'semiCombo') {\n container = _container.select('.input-wrap');\n }\n\n // For the country emoji flags\n container.selectAll('.tag-value-icon').remove();\n if (osmIsoCountryKeys.has(field.key) && value) {\n const data = buildCountry()[value];\n\n if (data && data.flag && showEmojiFlags) {\n container.selectAll('.tag-value-icon')\n .data([value])\n .enter()\n .insert('div', 'input')\n .attr('class', 'tag-value-icon')\n .append('span')\n .attr('class', 'emoji')\n .text(data.flag);\n return;\n }\n };\n\n const iconsField = field.resolveReference('iconsCrossReference');\n if (iconsField.icons) {\n container.selectAll('.tag-value-icon').remove();\n if (iconsField.icons[value]) {\n container.selectAll('.tag-value-icon')\n .data([value])\n .enter()\n .insert('div', 'input')\n .attr('class', 'tag-value-icon')\n .call(svgIcon(`#${iconsField.icons[value]}`));\n }\n }\n }\n\n combo.tags = function(tags) {\n _tags = tags;\n var stringsField = field.resolveReference('stringsCrossReference');\n\n var isMixed = Array.isArray(tags[field.key]);\n var showsValue = value => !isMixed && value && !(field.type === 'typeCombo' && value === 'yes');\n var isRawValue = value => showsValue(value)\n && !stringsField.hasTextForStringId(`options.${value}`)\n && !stringsField.hasTextForStringId(`options.${value}.title`)\n && !(osmIsoCountryKeys.has(field.key) && value in buildCountry());\n var isKnownValue = value => showsValue(value) && !isRawValue(value);\n var isReadOnly = !_allowCustomValues;\n\n if (_isMulti || _isSemi) {\n _multiData = [];\n\n var maxLength;\n\n if (_isMulti) {\n // Build _multiData array containing keys already set..\n for (var k in tags) {\n if (field.key && k.indexOf(field.key) !== 0) continue;\n if (!field.key && field.keys.indexOf(k) === -1) continue;\n\n var v = tags[k];\n\n var suffix = field.key ? k.slice(field.key.length) : k;\n _multiData.push({\n key: k,\n value: displayValue(suffix),\n display: addComboboxIcons(renderValue(suffix), suffix),\n state: typeof v === 'string' ? v.toLowerCase() : '',\n isMixed: Array.isArray(v)\n });\n }\n\n if (field.key) {\n // Set keys for form-field modified (needed for undo and reset buttons)..\n field.keys = _multiData.map(function(d) { return d.key; });\n\n // limit the input length so it fits after prepending the key prefix\n maxLength = context.maxCharsForTagKey() - utilUnicodeCharsCount(field.key);\n } else {\n maxLength = context.maxCharsForTagKey();\n }\n\n } else if (_isSemi) {\n\n var allValues = [];\n var commonValues;\n if (Array.isArray(tags[field.key])) {\n\n tags[field.key].forEach(function(tagVal) {\n var thisVals = (tagVal || '').split(';').filter(Boolean);\n allValues = allValues.concat(thisVals);\n if (!commonValues) {\n commonValues = thisVals;\n } else {\n commonValues = commonValues.filter(value => thisVals.includes(value));\n }\n });\n allValues = allValues.filter(Boolean);\n\n } else {\n allValues = (tags[field.key] || '').split(';').filter(Boolean);\n commonValues = allValues;\n }\n\n if (!field.allowDuplicates) {\n commonValues = utilArrayUniq(commonValues);\n allValues = utilArrayUniq(allValues);\n }\n\n _multiData = allValues.map(function(v) {\n return {\n key: v,\n value: displayValue(v),\n display: addComboboxIcons(renderValue(v), v),\n isMixed: !commonValues.includes(v)\n };\n });\n\n var currLength = utilUnicodeCharsCount(commonValues.join(';'));\n\n // limit the input length to the remaining available characters\n maxLength = context.maxCharsForTagValue() - currLength;\n\n if (currLength > 0) {\n // account for the separator if a new value will be appended to existing\n maxLength -= 1;\n }\n }\n // a negative maxlength doesn't make sense\n maxLength = Math.max(0, maxLength);\n\n // Hide 'Add' button if this field is already at its character limit\n var hideAdd = maxLength <= 0 || (!_allowCustomValues && !_comboData.length);\n _container.selectAll('.chiplist .input-wrap')\n .style('display', hideAdd ? 'none' : null);\n\n var allowDragAndDrop = _isSemi // only semiCombo values are ordered\n && !Array.isArray(tags[field.key]);\n\n // Render chips\n var chips = _container.selectAll('.chip')\n .data(_multiData.map((item, index) => ({ ...item, index })));\n\n chips.exit()\n .remove();\n\n var enter = chips.enter()\n .insert('li', '.input-wrap')\n .attr('class', 'chip');\n\n enter.append('span');\n const field_buttons = enter\n .append('div')\n .attr('class', 'field_buttons');\n field_buttons\n .append('a')\n .attr('class', 'remove');\n\n chips = chips.merge(enter)\n .order()\n .classed('raw-value', function(d) {\n var k = d.key;\n if (_isMulti) k = k.replace(field.key, '');\n // Ignore the raw-value class for key language:\n if (field.key === 'language:' && localizer.languageName(k) !== k) return false;\n return !stringsField.hasTextForStringId('options.' + k);\n })\n .classed('draggable', allowDragAndDrop)\n .classed('mixed', function(d) {\n return d.isMixed;\n })\n .attr('title', function(d) {\n if (d.isMixed) {\n return t('inspector.unshared_value_tooltip');\n }\n if (!['yes', 'no'].includes(d.state)) {\n return d.state;\n }\n return null;\n })\n .classed('negated', d => d.state === 'no');\n\n if (!_isSemi) {\n chips.selectAll('input[type=checkbox]').remove();\n chips.insert('input', 'span')\n .attr('type', 'checkbox')\n .property('checked', d => d.state === 'yes')\n .property('indeterminate', d => d.isMixed || !['yes', 'no'].includes(d.state))\n .on('click', invertMultikey);\n }\n\n if (allowDragAndDrop) {\n registerDragAndDrop(chips);\n }\n\n chips.each(function(d) {\n const selection = d3_select(this);\n const text_span = selection.select('span');\n const field_buttons = selection.select('.field_buttons');\n const clean_value = d.value.trim();\n text_span.text('');\n if (!field_buttons.select('button').empty()) {\n field_buttons.select('button').remove();\n }\n if (clean_value.startsWith('https://')) {\n // create a button to open the link in a new tab\n text_span.text(clean_value);\n field_buttons.append('button')\n .call(svgIcon('#iD-icon-out-link'))\n .attr('class', 'form-field-button foreign-id-permalink')\n .attr('title', () => t('icons.visit_website'))\n .attr('aria-label', () => t('icons.visit_website'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n window.open(clean_value, '_blank');\n });\n return;\n }\n if (d.display) {\n d.display(text_span);\n return;\n }\n text_span.text(d.value);\n });\n\n chips.select('a.remove')\n .attr('href', '#')\n .on('click', removeMultikey)\n .attr('class', 'remove')\n .text('\u00D7');\n\n updateIcon('');\n } else {\n var mixedValues = isMixed && tags[field.key].map(function(val) {\n return displayValue(val);\n }).filter(Boolean);\n\n utilGetSetValue(_input, !isMixed ? displayValue(tags[field.key]) : '')\n .data([tags[field.key]])\n .classed('raw-value', isRawValue)\n .classed('known-value', isKnownValue)\n .attr('readonly', isReadOnly ? 'readonly' : undefined)\n .attr('title', isMixed ? mixedValues.join('\\n') : undefined)\n .attr('placeholder', isMixed ? t('inspector.multiple_values') : _staticPlaceholder || '')\n .classed('mixed', isMixed)\n .on('keydown.deleteCapture', function(d3_event) {\n if (isReadOnly &&\n isKnownValue(tags[field.key]) &&\n (d3_event.keyCode === utilKeybinding.keyCodes['\u232B'] ||\n d3_event.keyCode === utilKeybinding.keyCodes['\u2326'])) {\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n var t = {};\n t[field.key] = undefined;\n dispatch.call('change', this, t);\n }\n });\n\n if (!Array.isArray(tags[field.key])) {\n updateIcon(tags[field.key]);\n }\n\n if (!isMixed) {\n _lengthIndicator.update(tags[field.key]);\n }\n }\n\n const refreshStyles = () => {\n _input\n .data([tagValue(utilGetSetValue(_input))])\n .classed('raw-value', isRawValue)\n .classed('known-value', isKnownValue);\n };\n _input.on('input.refreshStyles', refreshStyles);\n _combobox.on('update.refreshStyles', refreshStyles);\n refreshStyles();\n };\n\n function registerDragAndDrop(selection) {\n\n // allow drag and drop re-ordering of chips\n var dragOrigin, targetIndex;\n selection.call(d3_drag()\n .on('start', function(d3_event) {\n dragOrigin = {\n x: d3_event.x,\n y: d3_event.y\n };\n targetIndex = null;\n })\n .on('drag', function(d3_event) {\n var x = d3_event.x - dragOrigin.x,\n y = d3_event.y - dragOrigin.y;\n\n if (!d3_select(this).classed('dragging') &&\n // don't display drag until dragging beyond a distance threshold\n Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) <= 5) return;\n\n var index = selection.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', true);\n\n targetIndex = null;\n var targetIndexOffsetTop = null;\n var draggedTagWidth = d3_select(this).node().offsetWidth;\n\n if (field.key === 'destination' || field.key === 'via') { // meaning tags are full width\n _container.selectAll('.chip')\n .style('transform', function(d2, index2) {\n var node = d3_select(this).node();\n\n if (index === index2) {\n return 'translate(' + x + 'px, ' + y + 'px)';\n // move the dragged tag up the order\n } else if (index2 > index && d3_event.y > node.offsetTop) {\n if (targetIndex === null || index2 > targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(-100%)';\n // move the dragged tag down the order\n } else if (index2 < index && d3_event.y < node.offsetTop + node.offsetHeight) {\n if (targetIndex === null || index2 < targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(100%)';\n }\n return null;\n });\n } else {\n _container.selectAll('.chip')\n .each(function(d2, index2) {\n var node = d3_select(this).node();\n\n // check the cursor is in the bounding box\n if (\n index !== index2 &&\n d3_event.x < node.offsetLeft + node.offsetWidth + 5 &&\n d3_event.x > node.offsetLeft &&\n d3_event.y < node.offsetTop + node.offsetHeight &&\n d3_event.y > node.offsetTop\n ) {\n targetIndex = index2;\n targetIndexOffsetTop = node.offsetTop;\n }\n })\n .style('transform', function(d2, index2) {\n var node = d3_select(this).node();\n\n if (index === index2) {\n return 'translate(' + x + 'px, ' + y + 'px)';\n }\n\n // only translate tags in the same row\n if (node.offsetTop === targetIndexOffsetTop) {\n if (index2 < index && index2 >= targetIndex) {\n return 'translateX(' + draggedTagWidth + 'px)';\n } else if (index2 > index && index2 <= targetIndex) {\n return 'translateX(-' + draggedTagWidth + 'px)';\n }\n }\n return null;\n });\n }\n })\n .on('end', function() {\n if (!d3_select(this).classed('dragging')) {\n return;\n }\n var index = selection.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', false);\n\n _container.selectAll('.chip')\n .style('transform', null);\n\n if (typeof targetIndex === 'number') {\n var element = _multiData[index];\n _multiData.splice(index, 1);\n _multiData.splice(targetIndex, 0, element);\n\n var t = {};\n\n if (_multiData.length) {\n t[field.key] = _multiData.map(function(element) {\n return element.key;\n }).join(';');\n } else {\n t[field.key] = undefined;\n }\n\n dispatch.call('change', this, t);\n }\n dragOrigin = undefined;\n targetIndex = undefined;\n })\n );\n }\n\n combo.setCustomOptions = (newValue) => {\n _customOptions = newValue;\n };\n\n\n combo.focus = function() {\n _input.node().focus();\n };\n\n\n combo.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return combo;\n };\n\n\n function combinedEntityExtent() {\n return _entityIDs && _entityIDs.length && utilTotalExtent(_entityIDs, context.graph());\n }\n\n\n return utilRebind(combo, dispatch, 'on');\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport { geoSphericalDistance } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { modeSelect, modeSelectNote } from '../modes';\nimport { utilObjectOmit, utilQsString, utilStringQs } from '../util';\nimport { utilArrayIdentical } from '../util/array';\nimport { utilDisplayLabel } from '../util/utilDisplayLabel';\nimport { localizer, t } from '../core/localizer';\nimport { prefs } from '../core/preferences';\n\n\nexport function behaviorHash(context) {\n\n // cached window.location.hash\n var _cachedHash = null;\n // allowable latitude range\n var _latitudeLimit = 90 - 1e-8;\n\n function computedHashParameters() {\n var map = context.map();\n var center = map.center();\n var zoom = map.zoom();\n var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));\n var oldParams = utilObjectOmit(utilStringQs(window.location.hash),\n ['comment', 'source', 'hashtags', 'walkthrough']\n );\n var newParams = {};\n\n delete oldParams.id;\n var selected = context.selectedIDs().filter(function(id) {\n return context.hasEntity(id);\n });\n if (selected.length) {\n newParams.id = selected.join(',');\n } else if (context.selectedNoteID()) {\n newParams.id = `note/${context.selectedNoteID()}`;\n }\n\n newParams.map = zoom.toFixed(2) +\n '/' + center[1].toFixed(precision) +\n '/' + center[0].toFixed(precision);\n\n return Object.assign(oldParams, newParams);\n }\n\n function computedHash() {\n return '#' + utilQsString(computedHashParameters(), true);\n }\n\n function computedTitle(includeChangeCount) {\n\n var baseTitle = context.documentTitleBase() || 'iD';\n var contextual;\n var changeCount;\n var titleID;\n\n var selected = context.selectedIDs().filter(function(id) {\n return context.hasEntity(id);\n });\n if (selected.length) {\n var firstLabel = utilDisplayLabel(context.entity(selected[0]), context.graph());\n if (selected.length > 1) {\n contextual = t('title.labeled_and_more', {\n labeled: firstLabel,\n count: selected.length - 1\n });\n } else {\n contextual = firstLabel;\n }\n titleID = 'context';\n }\n\n if (includeChangeCount) {\n changeCount = context.history().difference().summary().length;\n if (changeCount > 0) {\n titleID = contextual ? 'changes_context' : 'changes';\n }\n }\n\n if (titleID) {\n return t('title.format.' + titleID, {\n changes: changeCount,\n base: baseTitle,\n context: contextual\n });\n }\n\n return baseTitle;\n }\n\n function updateTitle(includeChangeCount) {\n if (!context.setsDocumentTitle()) return;\n\n var newTitle = computedTitle(includeChangeCount);\n if (document.title !== newTitle) {\n document.title = newTitle;\n }\n }\n\n function updateHashIfNeeded() {\n if (context.inIntro()) return;\n\n var latestHash = computedHash();\n if (_cachedHash !== latestHash) {\n _cachedHash = latestHash;\n\n // Update the URL hash without affecting the browser navigation stack,\n // though unavoidably creating a browser history entry\n window.history.replaceState(null, '', latestHash);\n\n // set the title we want displayed for the browser tab/window\n updateTitle(true /* includeChangeCount */);\n\n // save last used map location for future\n const q = utilStringQs(latestHash);\n if (q.map) {\n prefs('map-location', q.map);\n }\n }\n }\n\n var _throttledUpdate = throttle(updateHashIfNeeded, 500);\n var _throttledUpdateTitle = throttle(function() {\n updateTitle(true /* includeChangeCount */);\n }, 500);\n\n function hashchange() {\n // ignore spurious hashchange events\n if (window.location.hash === _cachedHash) return;\n\n _cachedHash = window.location.hash;\n\n var q = utilStringQs(_cachedHash);\n\n if (q.theme) {\n context.theme(q.theme);\n }\n\n if (q.locale && q.locale !== localizer.preferredLocaleCodes().join(',')) {\n localizer.preferredLocaleCodes(q.locale);\n context.ui().restart();\n }\n\n var mapArgs = (q.map || '').split('/').map(Number);\n if (mapArgs.length < 3 || mapArgs.some(isNaN)) {\n // replace bogus hash\n updateHashIfNeeded();\n\n } else {\n // don't update if the new hash already reflects the state of iD\n if (_cachedHash === computedHash()) return;\n\n var mode = context.mode();\n\n context.map().centerZoom([mapArgs[2], Math.min(_latitudeLimit, Math.max(-_latitudeLimit, mapArgs[1]))], mapArgs[0]);\n\n if (q.id && mode) {\n var ids = q.id.split(',').filter(function(id) {\n return context.hasEntity(id) || id.startsWith('note/');\n });\n if (ids.length && ['browse', 'select-note', 'select'].includes(mode.id)) {\n if (ids.length === 1 && ids[0].startsWith('note/')) {\n context.enter(modeSelectNote(context, ids[0]));\n } else if (!utilArrayIdentical(mode.selectedIDs(), ids)) {\n context.enter(modeSelect(context, ids));\n }\n return;\n }\n }\n\n var center = context.map().center();\n var dist = geoSphericalDistance(center, [mapArgs[2], mapArgs[1]]);\n var maxdist = 500;\n\n // Don't allow the hash location to change too much while drawing\n // This can happen if the user accidentally hit the back button. #3996\n if (mode && mode.id.match(/^draw/) !== null && dist > maxdist) {\n context.enter(modeBrowse(context));\n return;\n }\n }\n }\n\n function behavior() {\n context.map()\n .on('move.behaviorHash', _throttledUpdate);\n\n context.history()\n .on('change.behaviorHash', _throttledUpdateTitle);\n\n context\n .on('enter.behaviorHash', _throttledUpdate);\n\n d3_select(window)\n .on('hashchange.behaviorHash', hashchange);\n\n var q = utilStringQs(window.location.hash);\n\n if (q.id) {\n // targeting specific features: download, select, and zoom to them\n const selectIds = q.id.split(',');\n if (selectIds.length === 1 && selectIds[0].startsWith('note/')) {\n const noteId = selectIds[0].split('/')[1];\n context.moveToNote(noteId, !q.map);\n } else {\n context.zoomToEntities(\n // convert ids to short form id: node/123 -> n123\n selectIds.map(id => id.replace(/([nwr])[^/]*\\//, '$1')),\n !q.map);\n }\n }\n\n if (q.walkthrough === 'true') {\n behavior.startWalkthrough = true;\n }\n\n if (q.map) {\n behavior.hadLocation = true;\n } else if (!q.id && prefs('map-location')) {\n // center map at last visited map location\n const mapArgs = prefs('map-location').split('/').map(Number);\n context.map().centerZoom([mapArgs[2], Math.min(_latitudeLimit, Math.max(-_latitudeLimit, mapArgs[1]))], mapArgs[0]);\n\n updateHashIfNeeded();\n\n behavior.hadLocation = true;\n }\n\n hashchange();\n\n updateTitle(false);\n }\n\n behavior.off = function() {\n _throttledUpdate.cancel();\n _throttledUpdateTitle.cancel();\n\n context.map()\n .on('move.behaviorHash', null);\n\n context\n .on('enter.behaviorHash', null);\n\n d3_select(window)\n .on('hashchange.behaviorHash', null);\n\n window.location.hash = '';\n };\n\n return behavior;\n}\n", "export { behaviorAddWay } from './add_way';\nexport { behaviorBreathe } from './breathe';\nexport { behaviorDrag } from './drag';\nexport { behaviorDrawWay } from './draw_way';\nexport { behaviorDraw } from './draw';\nexport { behaviorEdit } from './edit';\nexport { behaviorHash } from './hash';\nexport { behaviorHover } from './hover';\nexport { behaviorLasso } from './lasso';\nexport { behaviorOperation } from './operation';\nexport { behaviorPaste } from './paste';\nexport { behaviorSelect } from './select';\n", "import { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiAccount(context) {\n const osm = context.connection();\n\n\n function updateUserDetails(selection) {\n if (!osm) return;\n\n if (!osm.authenticated()) { // logged out\n render(selection, null);\n } else {\n osm.userDetails((err, user) => {\n if (err && err.status === 401) {\n // 401 Unauthorized\n // cannot load own user data: there must be something wrong (e.g. API token was revoked)\n // -> log out to allow user to reauthenticate\n osm.logout();\n }\n render(selection, user);\n });\n }\n }\n\n\n function render(selection, user) {\n let userInfo = selection.select('.userInfo');\n let loginLogout = selection.select('.loginLogout');\n\n if (user) {\n userInfo\n .html('')\n .classed('hide', false);\n\n let userLink = userInfo\n .append('a')\n .attr('href', osm.userURL(user.display_name))\n .attr('target', '_blank');\n\n // Add user's image or placeholder\n if (user.image_url) {\n userLink.append('img')\n .attr('class', 'icon pre-text user-icon')\n .attr('src', user.image_url);\n } else {\n userLink\n .call(svgIcon('#iD-icon-avatar', 'pre-text light'));\n }\n\n // Add user name\n userLink.append('span')\n .attr('class', 'label')\n .text(user.display_name);\n\n // show \"Log Out\"\n loginLogout\n .classed('hide', false)\n .select('a')\n .text(t('logout'))\n .on('click', e => {\n e.preventDefault();\n osm.logout();\n // OAuth2's idea of \"logout\" is just to get rid of the bearer token.\n // If we try to \"login\" again, it will just grab the token again.\n // What a user probably _really_ expects is to logout of OSM so that they can switch users.\n // So, we open a popup with a \"Logout\" button. After logging out, they can login again using\n // the same popup window.\n osm.authenticate(undefined, { switchUser: true });\n });\n\n } else { // no user\n userInfo\n .html('')\n .classed('hide', true);\n\n // show \"Log In\"\n loginLogout\n .classed('hide', false)\n .select('a')\n .text(t('login'))\n .on('click', e => {\n e.preventDefault();\n osm.authenticate();\n });\n }\n }\n\n\n return function(selection) {\n if (!osm) return;\n\n selection.append('li')\n .attr('class', 'userInfo')\n .classed('hide', true);\n\n selection.append('li')\n .attr('class', 'loginLogout')\n .classed('hide', true)\n .append('a')\n .attr('href', '#');\n\n osm.on('change.account', () => updateUserDetails(selection));\n updateUserDetails(selection);\n };\n\n}\n", "import { throttle } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\nimport { t } from '../core/localizer';\n\n\nexport function uiAttribution(context) {\n let _selection = d3_select(null);\n\n\n function render(selection, data, klass) {\n let div = selection.selectAll(`.${klass}`)\n .data([0]);\n\n div = div.enter()\n .append('div')\n .attr('class', klass)\n .merge(div);\n\n\n let attributions = div.selectAll('.attribution')\n .data(data, d => d.id);\n\n attributions.exit()\n .remove();\n\n attributions = attributions.enter()\n .append('span')\n .attr('class', 'attribution')\n .each((d, i, nodes) => {\n let attribution = d3_select(nodes[i]);\n\n if (d.terms_html) {\n attribution.html(d.terms_html);\n return;\n }\n\n if (d.terms_url) {\n attribution = attribution\n .append('a')\n .attr('href', d.terms_url)\n .attr('target', '_blank');\n }\n\n const sourceID = d.id.replace(/\\./g, '');\n const terms_text = t(`imagery.${sourceID}.attribution.text`,\n { default: d.terms_text || d.id || d.name() }\n );\n\n if (d.icon && !d.overlay) {\n attribution\n .append('img')\n .attr('class', 'source-image')\n .attr('src', d.icon);\n }\n\n attribution\n .append('span')\n .attr('class', 'attribution-text')\n .text(terms_text);\n })\n .merge(attributions);\n\n\n let copyright = attributions.selectAll('.copyright-notice')\n .data(d => {\n let notice = d.copyrightNotices(context.map().zoom(), context.map().extent());\n return notice ? [notice] : [];\n });\n\n copyright.exit()\n .remove();\n\n copyright = copyright.enter()\n .append('span')\n .attr('class', 'copyright-notice')\n .merge(copyright);\n\n copyright\n .text(String);\n }\n\n\n function update() {\n let baselayer = context.background().baseLayerSource();\n _selection\n .call(render, (baselayer ? [baselayer] : []), 'base-layer-attribution');\n\n const z = context.map().zoom();\n let overlays = context.background().overlayLayerSources() || [];\n _selection\n .call(render, overlays.filter(s => s.validZoom(z)), 'overlay-layer-attribution');\n }\n\n\n return function(selection) {\n _selection = selection;\n\n context.background()\n .on('change.attribution', update);\n\n context.map()\n .on('move.attribution', throttle(update, 400, { leading: false }));\n\n update();\n };\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/index';\n\n\nexport function uiContributors(context) {\n var osm = context.connection(),\n debouncedUpdate = debounce(function() { update(); }, 1000),\n limit = 4,\n hidden = false,\n wrap = d3_select(null);\n\n\n function update() {\n if (!osm) return;\n\n var users = {},\n entities = context.history().intersects(context.map().extent());\n\n entities.forEach(function(entity) {\n if (entity && entity.user) users[entity.user] = true;\n });\n\n var u = Object.keys(users),\n subset = u.slice(0, u.length > limit ? limit - 1 : limit);\n\n wrap.html('')\n .call(svgIcon('#iD-icon-nearby', 'pre-text light'));\n\n const userList = selection => selection.selectAll()\n .data(subset)\n .enter()\n .append('a')\n .attr('class', 'user-link')\n .attr('href', d => osm.userURL(d))\n .attr('target', '_blank')\n .text(String);\n\n if (u.length > limit) {\n var othersNum = u.length - limit + 1;\n\n const count = selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', () => osm.changesetsURL(context.map().center(), context.map().zoom()))\n .text(othersNum);\n\n wrap.append('span')\n .call(t.append('contributors.truncated_list', { n: othersNum, users: userList, count }));\n\n } else {\n wrap.append('span')\n .call(t.append('contributors.list', { users: userList }));\n }\n\n if (!u.length) {\n hidden = true;\n wrap\n .transition()\n .style('opacity', 0);\n\n } else if (hidden) {\n wrap\n .transition()\n .style('opacity', 1);\n }\n }\n\n\n return function(selection) {\n if (!osm) return;\n wrap = selection;\n update();\n\n osm.on('loaded.contributors', debouncedUpdate);\n context.map().on('move.contributors', debouncedUpdate);\n };\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { geoVecAdd } from '../geo';\nimport { localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\nimport { utilRebind } from '../util/rebind';\nimport { utilHighlightEntities } from '../util/util';\nimport { utilGetDimensions } from '../util/dimensions';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiEditMenu(context) {\n var dispatch = d3_dispatch('toggled');\n\n var _menu = d3_select(null);\n var _operations = [];\n // the position the menu should be displayed relative to\n var _anchorLoc = [0, 0];\n var _anchorLocLonLat = [0, 0];\n // a string indicating how the menu was opened\n var _triggerType = '';\n\n var _vpTopMargin = 85; // viewport top margin\n var _vpBottomMargin = 45; // viewport bottom margin\n var _vpSideMargin = 35; // viewport side margin\n\n var _menuTop = false;\n var _menuHeight;\n var _menuWidth;\n\n // hardcode these values to make menu positioning easier\n var _verticalPadding = 4;\n\n // see also `.edit-menu .tooltip` CSS; include margin\n var _tooltipWidth = 210;\n\n // offset the menu slightly from the target location\n var _menuSideMargin = 10;\n\n var _tooltips = [];\n\n var editMenu = function(selection) {\n\n var isTouchMenu = _triggerType.includes('touch') || _triggerType.includes('pen');\n\n var ops = _operations.filter(function(op) {\n return !isTouchMenu || !op.mouseOnly;\n });\n\n if (!ops.length) return;\n\n _tooltips = [];\n\n // Position the menu above the anchor for stylus and finger input\n // since the mapper's hand likely obscures the screen below the anchor\n _menuTop = isTouchMenu;\n\n // Show labels for touch input since there aren't hover tooltips\n var showLabels = isTouchMenu;\n\n var buttonHeight = showLabels ? 32 : 34;\n if (showLabels) {\n // Get a general idea of the width based on the length of the label\n _menuWidth = 52 + Math.min(120, 6 * Math.max.apply(Math, ops.map(function(op) {\n return op.title.length;\n })));\n } else {\n _menuWidth = 44;\n }\n\n _menuHeight = _verticalPadding * 2 + ops.length * buttonHeight;\n\n _menu = selection\n .append('div')\n .attr('class', 'edit-menu')\n .classed('touch-menu', isTouchMenu)\n .style('padding', _verticalPadding + 'px 0');\n\n var buttons = _menu.selectAll('.edit-menu-item')\n .data(ops);\n\n // enter\n var buttonsEnter = buttons.enter()\n .append('button')\n .attr('class', function (d) { return 'edit-menu-item edit-menu-item-' + d.id; })\n .style('height', buttonHeight + 'px')\n .on('click', click)\n // don't listen for `mouseup` because we only care about non-mouse pointer types\n .on('pointerup', pointerup)\n .on('pointerdown mousedown', function pointerdown(d3_event) {\n // don't let button presses also act as map input - #1869\n d3_event.stopPropagation();\n })\n .on('mouseenter.highlight', function(d3_event, d) {\n if (d3_select(this).classed('disabled')) return;\n\n if (d.relatedEntityIds) {\n utilHighlightEntities(d.relatedEntityIds(), true, context);\n }\n\n if (d.getAuxiliaryGeometry) {\n drawAuxiliaryGeometry(context, d.getAuxiliaryGeometry());\n }\n })\n .on('mouseleave.highlight', function(d3_event, d) {\n if (d.relatedEntityIds) {\n utilHighlightEntities(d.relatedEntityIds(), false, context);\n }\n\n if (d.getAuxiliaryGeometry) {\n drawAuxiliaryGeometry(context, []);\n }\n });\n\n buttonsEnter.each(function(d) {\n var tooltip = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .heading(() => d.title)\n .title(d.tooltip)\n .keys([d.keys[0]]);\n\n _tooltips.push(tooltip);\n\n d3_select(this)\n .call(tooltip)\n .append('div')\n .attr('class', 'icon-wrap')\n .call(svgIcon(d.icon && d.icon() || '#iD-operation-' + d.id, 'operation'));\n });\n\n if (showLabels) {\n buttonsEnter.append('span')\n .attr('class', 'label')\n .each(function(d) {\n d3_select(this).call(d.title);\n });\n }\n\n // update\n buttonsEnter\n .merge(buttons)\n .classed('disabled', function(d) { return d.disabled(); });\n\n updatePosition();\n\n var initialScale = context.projection.scale();\n context.map()\n .on('move.edit-menu', function() {\n if (initialScale !== context.projection.scale()) {\n editMenu.close();\n }\n })\n .on('drawn.edit-menu', function(info) {\n if (info.full) updatePosition();\n });\n\n var lastPointerUpType;\n // `pointerup` is always called before `click`\n function pointerup(d3_event) {\n lastPointerUpType = d3_event.pointerType;\n }\n\n function click(d3_event, operation) {\n d3_event.stopPropagation();\n\n if (operation.relatedEntityIds) {\n utilHighlightEntities(operation.relatedEntityIds(), false, context);\n }\n\n if (operation.disabled()) {\n if (lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen') {\n // there are no tooltips for touch interactions so flash feedback instead\n context.ui().flash\n .duration(4000)\n .iconName('#iD-operation-' + operation.id)\n .iconClass('operation disabled')\n .label(operation.tooltip())();\n }\n } else {\n if (lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen') {\n context.ui().flash\n .duration(2000)\n .iconName('#iD-operation-' + operation.id)\n .iconClass('operation')\n .label(operation.annotation() || operation.title)();\n }\n\n operation();\n editMenu.close();\n }\n lastPointerUpType = null;\n }\n\n dispatch.call('toggled', this, true);\n };\n\n function updatePosition() {\n\n if (!_menu || _menu.empty()) return;\n\n var anchorLoc = context.projection(_anchorLocLonLat);\n\n var viewport = context.surfaceRect();\n\n if (anchorLoc[0] < 0 ||\n anchorLoc[0] > viewport.width ||\n anchorLoc[1] < 0 ||\n anchorLoc[1] > viewport.height) {\n // close the menu if it's gone offscreen\n\n editMenu.close();\n return;\n }\n\n var menuLeft = displayOnLeft(viewport);\n\n var offset = [0, 0];\n\n offset[0] = menuLeft ? -1 * (_menuSideMargin + _menuWidth) : _menuSideMargin;\n\n if (_menuTop) {\n if (anchorLoc[1] - _menuHeight < _vpTopMargin) {\n // menu is near top viewport edge, shift downward\n offset[1] = -anchorLoc[1] + _vpTopMargin;\n } else {\n offset[1] = -_menuHeight;\n }\n } else {\n if (anchorLoc[1] + _menuHeight > (viewport.height - _vpBottomMargin)) {\n // menu is near bottom viewport edge, shift upwards\n offset[1] = -anchorLoc[1] - _menuHeight + viewport.height - _vpBottomMargin;\n } else {\n offset[1] = 0;\n }\n }\n\n var origin = geoVecAdd(anchorLoc, offset);\n // repositioning the menu to account for the top menu height\n var _verticalOffset = parseFloat(utilGetDimensions(d3_select('.top-toolbar-wrap'))[1]);\n origin[1] -= _verticalOffset;\n\n _menu\n .style('left', origin[0] + 'px')\n .style('top', origin[1] + 'px');\n\n var tooltipSide = tooltipPosition(viewport, menuLeft);\n _tooltips.forEach(function(tooltip) {\n tooltip.placement(tooltipSide);\n });\n\n function displayOnLeft(viewport) {\n if (localizer.textDirection() === 'ltr') {\n if ((anchorLoc[0] + _menuSideMargin + _menuWidth) > (viewport.width - _vpSideMargin)) {\n // right menu would be too close to the right viewport edge, go left\n return true;\n }\n // prefer right menu\n return false;\n\n } else { // rtl\n if ((anchorLoc[0] - _menuSideMargin - _menuWidth) < _vpSideMargin) {\n // left menu would be too close to the left viewport edge, go right\n return false;\n }\n // prefer left menu\n return true;\n }\n }\n\n function tooltipPosition(viewport, menuLeft) {\n if (localizer.textDirection() === 'ltr') {\n if (menuLeft) {\n // if there's not room for a right-side menu then there definitely\n // isn't room for right-side tooltips\n return 'left';\n }\n if ((anchorLoc[0] + _menuSideMargin + _menuWidth + _tooltipWidth) > (viewport.width - _vpSideMargin)) {\n // right tooltips would be too close to the right viewport edge, go left\n return 'left';\n }\n // prefer right tooltips\n return 'right';\n\n } else { // rtl\n if (!menuLeft) {\n return 'right';\n }\n if ((anchorLoc[0] - _menuSideMargin - _menuWidth - _tooltipWidth) < _vpSideMargin) {\n // left tooltips would be too close to the left viewport edge, go right\n return 'right';\n }\n // prefer left tooltips\n return 'left';\n }\n }\n }\n\n editMenu.close = function () {\n\n context.map()\n .on('move.edit-menu', null)\n .on('drawn.edit-menu', null);\n\n _menu.remove();\n _tooltips = [];\n\n // Clean up any auxiliary overlays\n drawAuxiliaryGeometry(context, []);\n\n dispatch.call('toggled', this, false);\n };\n\n editMenu.anchorLoc = function(val) {\n if (!arguments.length) return _anchorLoc;\n _anchorLoc = val;\n _anchorLocLonLat = context.projection.invert(_anchorLoc);\n return editMenu;\n };\n\n editMenu.triggerType = function(val) {\n if (!arguments.length) return _triggerType;\n _triggerType = val;\n return editMenu;\n };\n\n editMenu.operations = function(val) {\n if (!arguments.length) return _operations;\n _operations = val;\n return editMenu;\n };\n\n return utilRebind(editMenu, dispatch, 'on');\n}\n\n\n// Helper function to draw/remove reflect axis overlay\nfunction drawAuxiliaryGeometry(context, d) {\n const surface = context.surface();\n // Append to the OSM data layer to be in the same coordinate space as map features\n const container = surface.selectAll('.data-layer.osm .auxiliary');\n const paths = container.selectAll('path')\n .data(d, d => d.id);\n\n paths.exit().remove();\n const enter = paths.enter()\n .append('path');\n\n enter.merge(paths)\n .attr('class', d => d.klass)\n .attr('d', d => d.path);\n}\n", "import { t } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\n\nexport function uiFeatureInfo(context) {\n function update(selection) {\n var features = context.features();\n var stats = features.stats();\n var dateMatchCount = features.dateMatchCount();\n var count = 0;\n var hiddenList = features.hidden().map(function(k) {\n if (stats[k]) {\n count += stats[k];\n return t.append('inspector.title_count', {\n title: t('feature.' + k + '.description'),\n count: stats[k]\n });\n }\n return null;\n }).filter(Boolean);\n count += dateMatchCount;\n\n selection.text('');\n\n if (hiddenList.length || dateMatchCount > 0) {\n var tooltipBehavior = uiTooltip()\n .placement('top')\n .title(function() {\n return selection => {\n hiddenList.forEach(hiddenFeature => {\n selection.append('div').call(hiddenFeature);\n });\n };\n });\n\n selection.append('a')\n .attr('class', 'chip')\n .attr('href', '#')\n .call(t.append('feature_info.hidden_warning', { count: count }))\n .call(tooltipBehavior)\n .on('click', function(d3_event) {\n tooltipBehavior.hide();\n d3_event.preventDefault();\n // open the Map Data pane\n context.ui().togglePanes(context.container().select('.map-panes .map-data-pane'));\n });\n }\n\n selection\n .classed('hide', !hiddenList.length && !dateMatchCount);\n }\n\n\n return function(selection) {\n update(selection);\n\n context.features().on('change.feature_info', function() {\n update(selection);\n });\n };\n}\n", "import { timeout as d3_timeout } from 'd3-timer';\n\nexport function uiFlash(context) {\n var _flashTimer;\n\n var _duration = 2000;\n var _iconName = '#iD-icon-no';\n var _iconClass = 'disabled';\n var _label = s => s.text('');\n\n function flash() {\n if (_flashTimer) {\n _flashTimer.stop();\n }\n\n context.container().select('.main-footer-wrap')\n .classed('footer-hide', true)\n .classed('footer-show', false);\n context.container().select('.flash-wrap')\n .classed('footer-hide', false)\n .classed('footer-show', true);\n\n var content = context.container().select('.flash-wrap').selectAll('.flash-content')\n .data([0]);\n\n // Enter\n var contentEnter = content.enter()\n .append('div')\n .attr('class', 'flash-content');\n\n var iconEnter = contentEnter\n .append('svg')\n .attr('class', 'flash-icon icon')\n .append('g')\n .attr('transform', 'translate(10,10)');\n\n iconEnter\n .append('circle')\n .attr('r', 9);\n\n iconEnter\n .append('use')\n .attr('transform', 'translate(-7,-7)')\n .attr('width', '14')\n .attr('height', '14');\n\n contentEnter\n .append('div')\n .attr('class', 'flash-text');\n\n\n // Update\n content = content\n .merge(contentEnter);\n\n content\n .selectAll('.flash-icon')\n .attr('class', 'icon flash-icon ' + (_iconClass || ''));\n\n content\n .selectAll('.flash-icon use')\n .attr('xlink:href', _iconName);\n\n content\n .selectAll('.flash-text')\n .attr('class', 'flash-text')\n .call(_label);\n\n\n _flashTimer = d3_timeout(function() {\n _flashTimer = null;\n context.container().select('.main-footer-wrap')\n .classed('footer-hide', false)\n .classed('footer-show', true);\n context.container().select('.flash-wrap')\n .classed('footer-hide', true)\n .classed('footer-show', false);\n }, _duration);\n\n return content;\n }\n\n\n flash.duration = function(_) {\n if (!arguments.length) return _duration;\n _duration = _;\n return flash;\n };\n\n flash.label = function(_) {\n if (!arguments.length) return _label;\n if (typeof _ !== 'function') {\n _label = selection => selection.text(_);\n } else {\n _label = selection => selection.text('').call(_);\n }\n return flash;\n };\n\n flash.iconName = function(_) {\n if (!arguments.length) return _iconName;\n _iconName = _;\n return flash;\n };\n\n flash.iconClass = function(_) {\n if (!arguments.length) return _iconClass;\n _iconClass = _;\n return flash;\n };\n\n return flash;\n}\n", "import { uiCmd } from './cmd';\nimport { utilDetect } from '../util/detect';\n\nexport function uiFullScreen(context) {\n var element = context.container().node();\n // var button = d3_select(null);\n\n\n function getFullScreenFn() {\n if (element.requestFullscreen) {\n return element.requestFullscreen;\n } else if (element.msRequestFullscreen) {\n return element.msRequestFullscreen;\n } else if (element.mozRequestFullScreen) {\n return element.mozRequestFullScreen;\n } else if (element.webkitRequestFullscreen) {\n return element.webkitRequestFullscreen;\n }\n }\n\n\n function getExitFullScreenFn() {\n if (document.exitFullscreen) {\n return document.exitFullscreen;\n } else if (document.msExitFullscreen) {\n return document.msExitFullscreen;\n } else if (document.mozCancelFullScreen) {\n return document.mozCancelFullScreen;\n } else if (document.webkitExitFullscreen) {\n return document.webkitExitFullscreen;\n }\n }\n\n\n function isFullScreen() {\n return document.fullscreenElement ||\n document.mozFullScreenElement ||\n document.webkitFullscreenElement ||\n document.msFullscreenElement;\n }\n\n\n function isSupported() {\n return !!getFullScreenFn();\n }\n\n\n function fullScreen(d3_event) {\n d3_event.preventDefault();\n if (!isFullScreen()) {\n // button.classed('active', true);\n getFullScreenFn().apply(element);\n } else {\n // button.classed('active', false);\n getExitFullScreenFn().apply(document);\n }\n }\n\n\n return function() { // selection) {\n if (!isSupported()) return;\n\n // button = selection.append('button')\n // .attr('title', t('full_screen'))\n // .on('click', fullScreen)\n // .call(tooltip);\n\n // button.append('span')\n // .attr('class', 'icon full-screen');\n\n var detected = utilDetect();\n var keys = (detected.os === 'mac' ? [uiCmd('\u2303\u2318F'), 'f11'] : ['f11']);\n context.keybinding().on(keys, fullScreen);\n };\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t, localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\nimport { geoExtent } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\nimport { uiLoading } from './loading';\n\nexport function uiGeolocate(context) {\n var _geolocationOptions = {\n // prioritize speed and power usage over precision\n enableHighAccuracy: false,\n // don't hang indefinitely getting the location\n timeout: 6000 // 6sec\n };\n var _locating = uiLoading(context).message(t.addOrUpdate('geolocate.locating')).blocking(true);\n var _layer = context.layers().layer('geolocate');\n var _position;\n var _extent;\n var _timeoutID;\n var _button = d3_select(null);\n\n function click() {\n if (context.inIntro()) return;\n if (!_layer.enabled() && !_locating.isShown()) {\n\n // This timeout ensures that we still call finish() even if\n // the user declines to share their location in Firefox\n _timeoutID = setTimeout(error, 10000 /* 10sec */ );\n\n context.container().call(_locating);\n // get the latest position even if we already have one\n navigator.geolocation.getCurrentPosition(success, error, _geolocationOptions);\n } else {\n _locating.close();\n _layer.enabled(null, false);\n updateButtonState();\n }\n }\n\n function zoomTo() {\n context.enter(modeBrowse(context));\n\n var map = context.map();\n _layer.enabled(_position, true);\n updateButtonState();\n map.centerZoomEase(_extent.center(), Math.min(20, map.extentZoom(_extent)));\n }\n\n function success(geolocation) {\n _position = geolocation;\n var coords = _position.coords;\n _extent = geoExtent([coords.longitude, coords.latitude]).padByMeters(coords.accuracy);\n zoomTo();\n finish();\n }\n\n function error() {\n if (_position) {\n // use the position from a previous call if we have one\n zoomTo();\n } else {\n context.ui().flash\n .label(t.append('geolocate.location_unavailable'))\n .iconName('#iD-icon-geolocate')();\n }\n\n finish();\n }\n\n function finish() {\n _locating.close(); // unblock ui\n if (_timeoutID) { clearTimeout(_timeoutID); }\n _timeoutID = undefined;\n }\n\n function updateButtonState() {\n _button.classed('active', _layer.enabled());\n _button.attr('aria-pressed', _layer.enabled());\n }\n\n return function(selection) {\n if (!navigator.geolocation || !navigator.geolocation.getCurrentPosition) return;\n\n _button = selection\n .append('button')\n .on('click', click)\n .attr('aria-pressed', false)\n .call(svgIcon('#iD-icon-geolocate', 'light'))\n .call(uiTooltip()\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(() => t.append('geolocate.title'))\n );\n };\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { t } from '../../core/localizer';\n\n\nexport function uiPanelBackground(context) {\n const background = context.background();\n let _currSource = null;\n let _metadata = {};\n const _metadataKeys = [\n 'zoom', 'vintage', 'source', 'description', 'resolution', 'accuracy'\n ];\n\n var debouncedRedraw = debounce(redraw, 250);\n\n function redraw(selection) {\n var source = background.baseLayerSource();\n if (!source) return;\n\n if (_currSource?.id !== source.id) {\n _currSource = source;\n _metadata = {};\n }\n\n selection.text('');\n\n var list = selection\n .append('ul')\n .attr('class', 'background-info');\n\n list\n .append('li')\n .call(_currSource.label());\n\n _metadataKeys.forEach(function(k) {\n list\n .append('li')\n .attr('class', 'background-info-list-' + k)\n .classed('hide', !_metadata[k])\n .call(t.append('info_panels.background.' + k, { suffix: ':' }))\n .append('span')\n .attr('class', 'background-info-span-' + k)\n .text(_metadata[k]);\n });\n\n debouncedGetMetadata(selection);\n\n var toggleTiles = context.getDebug('tile') ? 'hide_tiles' : 'show_tiles';\n\n selection\n .append('a')\n .call(t.append('info_panels.background.' + toggleTiles))\n .attr('href', '#')\n .attr('class', 'button button-toggle-tiles')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.setDebug('tile', !context.getDebug('tile'));\n selection.call(redraw);\n });\n }\n\n\n var debouncedGetMetadata = debounce(getMetadata, 250);\n\n function getMetadata(selection) {\n var tile = context.container().select('.layer-background img.tile-center'); // tile near viewport center\n if (tile.empty()) return;\n\n var sourceId = _currSource.id;\n var d = tile.datum();\n var zoom = (d && d.length >= 3 && d[2]) || Math.floor(context.map().zoom());\n var center = context.map().center();\n\n // update zoom\n _metadata.zoom = String(zoom);\n selection.selectAll('.background-info-list-zoom')\n .classed('hide', false)\n .selectAll('.background-info-span-zoom')\n .text(_metadata.zoom);\n\n if (!d || !d.length >= 3) return;\n\n background.baseLayerSource().getMetadata(center, d, function(err, result) {\n if (err || _currSource.id !== sourceId) return;\n\n // update vintage\n var vintage = result.vintage;\n _metadata.vintage = (vintage && vintage.range) || t('info_panels.background.unknown');\n selection.selectAll('.background-info-list-vintage')\n .classed('hide', false)\n .selectAll('.background-info-span-vintage')\n .text(_metadata.vintage);\n\n // update other _metadata\n _metadataKeys.forEach(function(k) {\n if (k === 'zoom' || k === 'vintage') return; // done already\n var val = result[k];\n _metadata[k] = val;\n selection.selectAll('.background-info-list-' + k)\n .classed('hide', !val)\n .selectAll('.background-info-span-' + k)\n .text(val);\n });\n });\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.map()\n .on('drawn.info-background', function() {\n selection.call(debouncedRedraw);\n })\n .on('move.info-background', function() {\n selection.call(debouncedGetMetadata);\n });\n\n };\n\n panel.off = function() {\n context.map()\n .on('drawn.info-background', null)\n .on('move.info-background', null);\n };\n\n panel.id = 'background';\n panel.label = t.append('info_panels.background.title');\n panel.key = t('info_panels.background.key');\n\n\n return panel;\n}\n", "import { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\n\n\nexport function uiPanelHistory(context) {\n var osm;\n\n function displayTimestamp(timestamp) {\n if (!timestamp) return t('info_panels.history.unknown');\n var options = {\n day: 'numeric', month: 'short', year: 'numeric',\n hour: 'numeric', minute: 'numeric', second: 'numeric'\n };\n var d = new Date(timestamp);\n if (isNaN(d.getTime())) return t('info_panels.history.unknown');\n return d.toLocaleString(localizer.localeCode(), options);\n }\n\n\n function displayUser(selection, userName) {\n if (!userName) {\n selection\n .append('span')\n .call(t.append('info_panels.history.unknown'));\n return;\n }\n\n selection\n .append('span')\n .attr('class', 'user-name')\n .text(userName);\n\n var links = selection\n .append('div')\n .attr('class', 'links');\n\n if (osm) {\n links\n .append('a')\n .attr('class', 'user-osm-link')\n .attr('href', osm.userURL(userName))\n .attr('target', '_blank')\n .call(t.append('info_panels.history.profile_link'));\n }\n }\n\n\n function displayChangeset(selection, changeset) {\n if (!changeset) {\n selection\n .append('span')\n .call(t.append('info_panels.history.unknown'));\n return;\n }\n\n selection\n .append('span')\n .attr('class', 'changeset-id')\n .text(changeset);\n\n var links = selection\n .append('div')\n .attr('class', 'links');\n\n if (osm) {\n links\n .append('a')\n .attr('class', 'changeset-osm-link')\n .attr('href', osm.changesetURL(changeset))\n .attr('target', '_blank')\n .call(t.append('info_panels.history.changeset_link'));\n }\n\n links\n .append('a')\n .attr('class', 'changeset-osmcha-link')\n .attr('href', 'https://osmcha.openhistoricalmap.org/changesets/' + changeset)\n .attr('target', '_blank')\n .text('OSMCha');\n }\n\n\n function redraw(selection) {\n var selectedNoteID = context.selectedNoteID();\n osm = context.connection();\n var selected, note, entity;\n if (selectedNoteID && osm) { // selected 1 note\n selected = [ t.append('note.note', { suffix: ' ' + selectedNoteID }) ];\n note = osm.getNote(selectedNoteID);\n } else { // selected 1..n entities\n selected = context.selectedIDs()\n .filter(function(e) { return context.hasEntity(e); });\n if (selected.length) {\n entity = context.entity(selected[0]);\n }\n }\n\n var singular = selected.length === 1 ? selected[0] : null;\n\n selection.text('');\n\n const heading = selection\n .append('h4')\n .attr('class', 'history-heading');\n\n if (singular) {\n if (typeof singular === 'function') {\n heading.call(singular);\n } else {\n heading.text(singular);\n }\n } else {\n heading.call(t.append('info_panels.selected', { n: selected.length }));\n }\n\n if (!singular) return;\n\n if (entity) {\n selection.call(redrawEntity, entity);\n } else if (note) {\n selection.call(redrawNote, note);\n }\n }\n\n\n function redrawNote(selection, note) {\n if (!note || note.isNew()) {\n selection\n .append('div')\n .call(t.append('info_panels.history.note_no_history'));\n return;\n }\n\n var list = selection\n .append('ul');\n\n list\n .append('li')\n .call(t.append('info_panels.history.note_comments', { suffix: ':' }))\n .append('span')\n .text(note.comments.length);\n\n if (note.comments.length) {\n list\n .append('li')\n .call(t.append('info_panels.history.note_created_date', { suffix: ':' }))\n .append('span')\n .text(displayTimestamp(note.comments[0].date));\n\n list\n .append('li')\n .call(t.append('info_panels.history.note_created_user', { suffix: ':' }))\n .call(displayUser, note.comments[0].user);\n }\n\n if (osm) {\n selection\n .append('a')\n .attr('class', 'view-history-on-osm')\n .attr('target', '_blank')\n .attr('href', osm.noteURL(note))\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append('info_panels.history.note_link_text'));\n }\n }\n\n\n function redrawEntity(selection, entity) {\n if (!entity || entity.isNew()) {\n selection\n .append('div')\n .call(t.append('info_panels.history.no_history'));\n return;\n }\n\n var links = selection\n .append('div')\n .attr('class', 'links');\n\n if (osm) {\n links\n .append('a')\n .attr('class', 'view-history-on-osm')\n .attr('href', osm.historyURL(entity))\n .attr('target', '_blank')\n .call(t.append('info_panels.history.history_link'));\n }\n\n var list = selection\n .append('ul');\n\n list\n .append('li')\n .call(t.append('info_panels.history.version', { suffix: ':' }))\n .append('span')\n .text(entity.version);\n\n list\n .append('li')\n .call(t.append('info_panels.history.last_edit', { suffix: ':' }))\n .append('span')\n .text(displayTimestamp(entity.timestamp));\n\n list\n .append('li')\n .call(t.append('info_panels.history.edited_by', { suffix: ':' }))\n .call(displayUser, entity.user);\n\n list\n .append('li')\n .call(t.append('info_panels.history.changeset', { suffix: ':' }))\n .call(displayChangeset, entity.changeset);\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.map()\n .on('drawn.info-history', function() {\n selection.call(redraw);\n });\n\n context\n .on('enter.info-history', function() {\n selection.call(redraw);\n });\n };\n\n panel.off = function() {\n context.map().on('drawn.info-history', null);\n context.on('enter.info-history', null);\n };\n\n panel.id = 'history';\n panel.label = t.append('info_panels.history.title');\n panel.key = t('info_panels.history.key');\n\n\n return panel;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { decimalCoordinatePair, dmsCoordinatePair } from '../../util/units';\nimport { t } from '../../core/localizer';\nimport { services } from '../../services';\n\n\nexport function uiPanelLocation(context) {\n var currLocation = '';\n\n\n function redraw(selection) {\n selection.html('');\n\n var list = selection\n .append('ul');\n\n // Mouse coordinates\n var coord = context.map().mouseCoordinates();\n if (coord.some(isNaN)) {\n coord = context.map().center();\n }\n\n list\n .append('li')\n .text(dmsCoordinatePair(coord))\n .append('li')\n .text(decimalCoordinatePair(coord));\n\n // Location Info\n selection\n .append('div')\n .attr('class', 'location-info')\n .text(currLocation || ' ');\n\n debouncedGetLocation(selection, coord);\n }\n\n\n var debouncedGetLocation = debounce(getLocation, 250);\n function getLocation(selection, coord) {\n if (!services.geocoder) {\n currLocation = t('info_panels.location.unknown_location');\n selection.selectAll('.location-info')\n .text(currLocation);\n } else {\n services.geocoder.reverse(coord, function(err, result) {\n currLocation = result ? result.display_name : t('info_panels.location.unknown_location');\n selection.selectAll('.location-info')\n .text(currLocation);\n });\n }\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.surface()\n .on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'move.info-location', function() {\n selection.call(redraw);\n });\n };\n\n panel.off = function() {\n context.surface()\n .on('.info-location', null);\n };\n\n panel.id = 'location';\n panel.label = t.append('info_panels.location.title');\n panel.key = t('info_panels.location.key');\n\n\n return panel;\n}\n", "import {\n geoLength as d3_geoLength,\n geoPath as d3_geoPath\n} from 'd3-geo';\n\nimport { t, localizer } from '../../core/localizer';\nimport { displayArea, displayLength, decimalCoordinatePair, dmsCoordinatePair } from '../../util/units';\nimport { geoExtent, geoSphericalDistance } from '../../geo';\nimport { services } from '../../services';\nimport { utilGetAllNodes } from '../../util';\n\nexport function uiPanelMeasurement(context) {\n\n function radiansToMeters(r) {\n // using WGS84 authalic radius (6371007.1809 m)\n return r * 6371007.1809;\n }\n\n function steradiansToSqmeters(r) {\n // http://gis.stackexchange.com/a/124857/40446\n return r / (4 * Math.PI) * 510065621724000;\n }\n\n\n function toLineString(feature) {\n if (feature.type === 'LineString') return feature;\n\n var result = { type: 'LineString', coordinates: [] };\n if (feature.type === 'Polygon') {\n result.coordinates = feature.coordinates[0];\n } else if (feature.type === 'MultiPolygon') {\n result.coordinates = feature.coordinates[0][0];\n }\n\n return result;\n }\n\n var _isImperial = !localizer.usesMetric();\n\n function redraw(selection) {\n var graph = context.graph();\n var selectedNoteID = context.selectedNoteID();\n var osm = services.osm;\n\n var localeCode = localizer.localeCode();\n\n var heading;\n var center, location, centroid;\n var closed, geometry;\n var totalNodeCount, length = 0, area = 0, distance;\n\n if (selectedNoteID && osm) { // selected 1 note\n var note = osm.getNote(selectedNoteID);\n heading = t.append('note.note', { suffix: ' ' + selectedNoteID });\n location = note.loc;\n geometry = 'note';\n\n } else { // selected 1..n entities\n var selectedIDs = context.selectedIDs().filter(function(id) {\n return context.hasEntity(id);\n });\n var selected = selectedIDs.map(function(id) {\n return context.entity(id);\n });\n\n heading = selected.length === 1 ? selected[0].id :\n t.append('info_panels.selected', { n: selected.length });\n\n if (selected.length) {\n var extent = geoExtent();\n for (var i in selected) {\n var entity = selected[i];\n extent._extend(entity.extent(graph));\n\n geometry = entity.geometry(graph);\n if (geometry === 'line' || geometry === 'area') {\n closed = (entity.type === 'relation') || (entity.isClosed() && !entity.isDegenerate());\n var feature = entity.asGeoJSON(graph);\n length += radiansToMeters(d3_geoLength(toLineString(feature)));\n centroid = d3_geoPath(context.projection).centroid(entity.asGeoJSON(graph));\n centroid = centroid && context.projection.invert(centroid);\n if (!centroid || !isFinite(centroid[0]) || !isFinite(centroid[1])) {\n centroid = entity.extent(graph).center();\n }\n if (closed) {\n area += steradiansToSqmeters(entity.area(graph));\n }\n }\n }\n\n if (selected.length > 1) {\n geometry = null;\n closed = null;\n centroid = null;\n }\n\n if (selected.length === 2 &&\n selected[0].type === 'node' &&\n selected[1].type === 'node') {\n distance = geoSphericalDistance(selected[0].loc, selected[1].loc);\n }\n\n if (selected.length === 1 && selected[0].type === 'node') {\n location = selected[0].loc;\n } else {\n totalNodeCount = utilGetAllNodes(selectedIDs, context.graph()).length;\n }\n\n if (!location && !centroid) {\n center = extent.center();\n }\n }\n }\n\n selection.html('');\n\n if (heading && typeof heading === 'function') {\n selection\n .append('h4')\n .attr('class', 'measurement-heading')\n .call(heading);\n } else {\n selection\n .append('h4')\n .attr('class', 'measurement-heading')\n .text(heading);\n }\n\n var list = selection\n .append('ul');\n var coordItem;\n\n if (geometry) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.geometry', { suffix: ':' }))\n .append('span')\n .call(\n closed ? t.append('info_panels.measurement.closed_' + geometry) : t.append('geometry.' + geometry)\n );\n }\n\n if (totalNodeCount) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.node_count', { suffix: ':' }))\n .append('span')\n .text(totalNodeCount.toLocaleString(localeCode));\n }\n\n if (area) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.area', { suffix: ':' }))\n .append('span')\n .text(displayArea(area, _isImperial));\n }\n\n if (length) {\n list\n .append('li')\n .call(t.append('info_panels.measurement.' + (closed ? 'perimeter' : 'length'), { suffix: ':' }))\n .append('span')\n .text(displayLength(length, _isImperial));\n }\n\n if (typeof distance === 'number') {\n list\n .append('li')\n .call(t.append('info_panels.measurement.distance', { suffix: ':' }))\n .append('span')\n .text(displayLength(distance, _isImperial));\n }\n\n if (location) {\n coordItem = list\n .append('li')\n .call(t.append('info_panels.measurement.location', { suffix: ':' }));\n coordItem.append('span')\n .text(dmsCoordinatePair(location));\n coordItem.append('span')\n .text(decimalCoordinatePair(location));\n }\n\n if (centroid) {\n coordItem = list\n .append('li')\n .call(t.append('info_panels.measurement.centroid', { suffix: ':' }));\n coordItem.append('span')\n .text(dmsCoordinatePair(centroid));\n coordItem.append('span')\n .text(decimalCoordinatePair(centroid));\n }\n\n if (center) {\n coordItem = list\n .append('li')\n .call(t.append('info_panels.measurement.center', { suffix: ':' }));\n coordItem.append('span')\n .text(dmsCoordinatePair(center));\n coordItem.append('span')\n .text(decimalCoordinatePair(center));\n }\n\n if (length || area || typeof distance === 'number') {\n var toggle = _isImperial ? 'imperial' : 'metric';\n selection\n .append('a')\n .call(t.append('info_panels.measurement.' + toggle))\n .attr('href', '#')\n .attr('class', 'button button-toggle-units')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n _isImperial = !_isImperial;\n selection.call(redraw);\n });\n }\n }\n\n\n var panel = function(selection) {\n selection.call(redraw);\n\n context.map()\n .on('drawn.info-measurement', function() {\n selection.call(redraw);\n });\n\n context\n .on('enter.info-measurement', function() {\n selection.call(redraw);\n });\n };\n\n panel.off = function() {\n context.map().on('drawn.info-measurement', null);\n context.on('enter.info-measurement', null);\n };\n\n panel.id = 'measurement';\n panel.label = t.append('info_panels.measurement.title');\n panel.key = t('info_panels.measurement.key');\n\n\n return panel;\n}\n", "export * from './background';\nexport * from './history';\nexport * from './location';\nexport * from './measurement';\n\nimport { uiPanelBackground } from './background';\nimport { uiPanelHistory } from './history';\nimport { uiPanelLocation } from './location';\nimport { uiPanelMeasurement } from './measurement';\n\nexport var uiInfoPanels = {\n background: uiPanelBackground,\n history: uiPanelHistory,\n location: uiPanelLocation,\n measurement: uiPanelMeasurement,\n};\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\nimport { uiInfoPanels } from './panels';\n\n\nexport function uiInfo(context) {\n var ids = Object.keys(uiInfoPanels);\n var wasActive = ['measurement'];\n var panels = {};\n var active = {};\n\n // create panels\n ids.forEach(function(k) {\n if (!panels[k]) {\n panels[k] = uiInfoPanels[k](context);\n active[k] = false;\n }\n });\n\n\n function info(selection) {\n\n function redraw() {\n var activeids = ids.filter(function(k) { return active[k]; }).sort();\n\n var containers = infoPanels.selectAll('.panel-container')\n .data(activeids, function(k) { return k; });\n\n containers.exit()\n .style('opacity', 1)\n .transition()\n .duration(200)\n .style('opacity', 0)\n .on('end', function(d) {\n d3_select(this)\n .call(panels[d].off)\n .remove();\n });\n\n var enter = containers.enter()\n .append('div')\n .attr('class', function(d) { return 'fillD2 panel-container panel-container-' + d; });\n\n enter\n .style('opacity', 0)\n .transition()\n .duration(200)\n .style('opacity', 1);\n\n var title = enter\n .append('div')\n .attr('class', 'panel-title fillD2');\n\n title\n .append('h3')\n .each(function(d) { return panels[d].label(d3_select(this)); });\n\n title\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function(d3_event, d) {\n d3_event.stopImmediatePropagation();\n d3_event.preventDefault();\n info.toggle(d);\n })\n .call(svgIcon('#iD-icon-close'));\n\n enter\n .append('div')\n .attr('class', function(d) { return 'panel-content panel-content-' + d; });\n\n\n // redraw the panels\n infoPanels.selectAll('.panel-content')\n .each(function(d) {\n d3_select(this).call(panels[d]);\n });\n }\n\n\n info.toggle = function(which) {\n var activeids = ids.filter(function(k) { return active[k]; });\n\n if (which) { // toggle one\n active[which] = !active[which];\n if (activeids.length === 1 && activeids[0] === which) { // none active anymore\n wasActive = [which];\n }\n\n context.container().select('.' + which + '-panel-toggle-item')\n .classed('active', active[which])\n .select('input')\n .property('checked', active[which]);\n\n } else { // toggle all\n if (activeids.length) {\n wasActive = activeids;\n activeids.forEach(function(k) { active[k] = false; });\n } else {\n wasActive.forEach(function(k) { active[k] = true; });\n }\n }\n\n redraw();\n };\n\n\n var infoPanels = selection.selectAll('.info-panels')\n .data([0]);\n\n infoPanels = infoPanels.enter()\n .append('div')\n .attr('class', 'info-panels')\n .merge(infoPanels);\n\n redraw();\n\n context.keybinding()\n .on(uiCmd('\u2318' + t('info_panels.key')), function(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.stopImmediatePropagation();\n d3_event.preventDefault();\n info.toggle();\n });\n\n ids.forEach(function(k) {\n var key = t('info_panels.' + k + '.key', { default: null });\n if (!key) return;\n context.keybinding()\n .on(uiCmd('\u2318\u21E7' + key), function(d3_event) {\n d3_event.stopImmediatePropagation();\n d3_event.preventDefault();\n info.toggle(k);\n });\n });\n }\n\n return info;\n}\n", "import { easeLinear as d3_easeLinear } from 'd3-ease';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { localizer } from '../core/localizer';\nimport { uiToggle } from './toggle';\n\n\n// Tooltips and svg mask used to highlight certain features\nexport function uiCurtain(containerNode) {\n\n var surface = d3_select(null),\n tooltip = d3_select(null),\n darkness = d3_select(null);\n\n function curtain(selection) {\n surface = selection\n .append('svg')\n .attr('class', 'curtain')\n .style('top', 0)\n .style('left', 0);\n\n darkness = surface.append('path')\n .attr('x', 0)\n .attr('y', 0)\n .attr('class', 'curtain-darkness');\n\n d3_select(window).on('resize.curtain', resize);\n\n tooltip = selection.append('div')\n .attr('class', 'tooltip');\n\n tooltip\n .append('div')\n .attr('class', 'popover-arrow');\n\n tooltip\n .append('div')\n .attr('class', 'popover-inner');\n\n resize();\n\n\n function resize() {\n surface\n .attr('width', containerNode.clientWidth)\n .attr('height', containerNode.clientHeight);\n curtain.cut(darkness.datum());\n }\n }\n\n\n /**\n * Reveal cuts the curtain to highlight the given box,\n * and shows a tooltip with instructions next to the box.\n *\n * @param {String|ClientRect|HTMLElement} [box] box used to cut the curtain\n * @param {String} [text] text for a tooltip\n * @param {Object} [options]\n * @param {string} [options.tooltipClass] optional class to add to the tooltip\n * @param {integer} [options.duration] transition time in milliseconds\n * @param {string} [options.buttonText] if set, create a button with this text label\n * @param {function} [options.buttonCallback] if set, the callback for the button\n * @param {function} [options.padding] extra margin in px to put around bbox\n * @param {String|ClientRect} [options.tooltipBox] box for tooltip position, if different from box for the curtain\n */\n curtain.reveal = function(box, html, options) {\n options = options || {};\n\n if (typeof box === 'string') {\n box = d3_select(box).node();\n }\n if (box && box.getBoundingClientRect) {\n box = copyBox(box.getBoundingClientRect());\n }\n if (box) {\n var containerRect = containerNode.getBoundingClientRect();\n box.top -= containerRect.top;\n box.left -= containerRect.left;\n }\n if (box && options.padding) {\n box.top -= options.padding;\n box.left -= options.padding;\n box.bottom += options.padding;\n box.right += options.padding;\n box.height += options.padding * 2;\n box.width += options.padding * 2;\n }\n\n var tooltipBox;\n if (options.tooltipBox) {\n tooltipBox = options.tooltipBox;\n if (typeof tooltipBox === 'string') {\n tooltipBox = d3_select(tooltipBox).node();\n }\n if (tooltipBox && tooltipBox.getBoundingClientRect) {\n tooltipBox = copyBox(tooltipBox.getBoundingClientRect());\n }\n } else {\n tooltipBox = box;\n }\n\n if (tooltipBox && html) {\n\n if (html.indexOf('**') !== -1) {\n if (html.indexOf(')(.+?)(\\*\\*)/, '$1$2$3');\n } else {\n html = html.replace(/^(.+?)(\\*\\*)/, '$1$2');\n }\n // pseudo markdown bold text for the instruction section..\n html = html.replace(/\\*\\*(.*?)\\*\\*/g, '$1');\n }\n\n html = html.replace(/\\*(.*?)\\*/g, '$1'); // emphasis\n html = html.replace(/\\{br\\}/g, '

    '); // linebreak\n\n if (options.buttonText && options.buttonCallback) {\n html += '
    ' +\n '
    ';\n }\n\n var classes = 'curtain-tooltip popover tooltip arrowed in ' + (options.tooltipClass || '');\n tooltip\n .classed(classes, true)\n .selectAll('.popover-inner')\n .html(html);\n\n if (options.buttonText && options.buttonCallback) {\n var button = tooltip.selectAll('.button-section .button.action');\n button\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n options.buttonCallback();\n });\n }\n\n var tip = copyBox(tooltip.node().getBoundingClientRect()),\n w = containerNode.clientWidth,\n h = containerNode.clientHeight,\n tooltipWidth = 200,\n tooltipArrow = 5,\n side, pos;\n\n\n // hack: this will have bottom placement,\n // so need to reserve extra space for the tooltip illustration.\n if (options.tooltipClass === 'intro-mouse') {\n tip.height += 80;\n }\n\n // trim box dimensions to just the portion that fits in the container..\n if (tooltipBox.top + tooltipBox.height > h) {\n tooltipBox.height -= (tooltipBox.top + tooltipBox.height - h);\n }\n if (tooltipBox.left + tooltipBox.width > w) {\n tooltipBox.width -= (tooltipBox.left + tooltipBox.width - w);\n }\n\n // determine tooltip placement..\n const onLeftOrRightEdge = tooltipBox.left + tooltipBox.width / 2 > w - 100 || tooltipBox.left + tooltipBox.width / 2 < 100;\n if (tooltipBox.top + tooltipBox.height < 100 && !onLeftOrRightEdge) {\n // tooltip below box..\n side = 'bottom';\n pos = [\n tooltipBox.left + tooltipBox.width / 2 - tip.width / 2,\n tooltipBox.top + tooltipBox.height\n ];\n\n } else if (tooltipBox.top > h - 140 && !onLeftOrRightEdge) {\n // tooltip above box..\n side = 'top';\n pos = [\n tooltipBox.left + tooltipBox.width / 2 - tip.width / 2,\n tooltipBox.top - tip.height\n ];\n\n } else {\n // tooltip to the side of the tooltipBox..\n var tipY = tooltipBox.top + tooltipBox.height / 2 - tip.height / 2;\n\n if (localizer.textDirection() === 'rtl') {\n if (tooltipBox.left - tooltipWidth - tooltipArrow < 70) {\n side = 'right';\n pos = [tooltipBox.left + tooltipBox.width + tooltipArrow, tipY];\n\n } else {\n side = 'left';\n pos = [tooltipBox.left - tooltipWidth - tooltipArrow, tipY];\n }\n\n } else {\n if (tooltipBox.left + tooltipBox.width + tooltipArrow + tooltipWidth > w - 70) {\n side = 'left';\n pos = [tooltipBox.left - tooltipWidth - tooltipArrow, tipY];\n } else {\n side = 'right';\n pos = [tooltipBox.left + tooltipBox.width + tooltipArrow, tipY];\n }\n }\n }\n\n if (options.duration !== 0 || !tooltip.classed(side)) {\n tooltip.call(uiToggle(true));\n }\n\n tooltip\n .style('top', pos[1] + 'px')\n .style('left', pos[0] + 'px')\n .attr('class', classes + ' ' + side);\n\n\n // shift popover-inner if it is very close to the top or bottom edge\n // (doesn't affect the placement of the popover-arrow)\n var shiftY = 0;\n if (side === 'left' || side === 'right') {\n if (pos[1] < 60) {\n shiftY = 60 - pos[1];\n } else if (pos[1] + tip.height > h - 100) {\n shiftY = h - pos[1] - tip.height - 100;\n }\n }\n tooltip.selectAll('.popover-inner')\n .style('top', shiftY + 'px');\n\n } else {\n tooltip\n .classed('in', false)\n .call(uiToggle(false));\n }\n\n curtain.cut(box, options.duration);\n\n return tooltip;\n };\n\n\n curtain.cut = function(datum, duration) {\n darkness.datum(datum)\n .interrupt();\n\n var selection;\n if (duration === 0) {\n selection = darkness;\n } else {\n selection = darkness\n .transition()\n .duration(duration || 600)\n .ease(d3_easeLinear);\n }\n\n selection\n .attr('d', function(d) {\n var containerWidth = containerNode.clientWidth;\n var containerHeight = containerNode.clientHeight;\n var string = 'M 0,0 L 0,' + containerHeight + ' L ' +\n containerWidth + ',' + containerHeight + 'L' +\n containerWidth + ',0 Z';\n\n if (!d) return string;\n return string + 'M' +\n d.left + ',' + d.top + 'L' +\n d.left + ',' + (d.top + d.height) + 'L' +\n (d.left + d.width) + ',' + (d.top + d.height) + 'L' +\n (d.left + d.width) + ',' + (d.top) + 'Z';\n\n });\n };\n\n\n curtain.remove = function() {\n surface.remove();\n tooltip.remove();\n d3_select(window).on('resize.curtain', null);\n };\n\n\n // ClientRects are immutable, so copy them to an object,\n // in case we need to trim the height/width.\n function copyBox(src) {\n return {\n top: src.top,\n right: src.right,\n bottom: src.bottom,\n left: src.left,\n width: src.width,\n height: src.height\n };\n }\n\n\n return curtain;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { helpHtml } from './helper';\nimport { t } from '../../core/localizer';\nimport { utilRebind } from '../../util/rebind';\n\n\nexport function uiIntroWelcome(context, reveal) {\n var dispatch = d3_dispatch('done');\n\n var chapter = {\n title: 'intro.welcome.title'\n };\n\n\n function welcome() {\n context.map().centerZoom([-85.63591, 41.94285], 19);\n reveal('.intro-nav-wrap .chapter-welcome',\n helpHtml('intro.welcome.welcome'),\n { buttonText: t.html('intro.ok'), buttonCallback: practice }\n );\n }\n\n function practice() {\n reveal('.intro-nav-wrap .chapter-welcome',\n helpHtml('intro.welcome.practice'),\n { buttonText: t.html('intro.ok'), buttonCallback: words }\n );\n }\n\n function words() {\n reveal('.intro-nav-wrap .chapter-welcome',\n helpHtml('intro.welcome.words'),\n { buttonText: t.html('intro.ok'), buttonCallback: chapters }\n );\n }\n\n\n function chapters() {\n dispatch.call('done');\n reveal('.intro-nav-wrap .chapter-navigation',\n helpHtml('intro.welcome.chapters', { next: t('intro.navigation.title') })\n );\n }\n\n\n chapter.enter = function() {\n welcome();\n };\n\n\n chapter.exit = function() {\n context.container().select('.curtain-tooltip.intro-mouse')\n .selectAll('.counter')\n .remove();\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pointBox, transitionTime } from './helper';\n\n\nexport function uiIntroNavigation(context, reveal) {\n var dispatch = d3_dispatch('done');\n var timeouts = [];\n var hallId = 'n2061';\n var townHall = [-85.63591, 41.94285];\n var springStreetId = 'w397';\n var springStreetEndId = 'n1834';\n var springStreet = [-85.63582, 41.94255];\n var onewayField = presetManager.field('oneway');\n var maxspeedField = presetManager.field('maxspeed');\n\n\n var chapter = {\n title: 'intro.navigation.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function isTownHallSelected() {\n var ids = context.selectedIDs();\n return ids.length === 1 && ids[0] === hallId;\n }\n\n\n function dragMap() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var msec = transitionTime(townHall, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(townHall, 19, msec);\n\n timeout(function() {\n var centerStart = context.map().center();\n\n var textId = context.lastPointerType() === 'mouse' ? 'drag' : 'drag_touch';\n var dragString = helpHtml('intro.navigation.map_info') + '{br}' + helpHtml('intro.navigation.' + textId);\n reveal('.main-map .surface', dragString);\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', dragString, { duration: 0 });\n });\n\n context.map().on('move.intro', function() {\n var centerNow = context.map().center();\n if (centerStart[0] !== centerNow[0] || centerStart[1] !== centerNow[1]) {\n context.map().on('move.intro', null);\n timeout(function() { continueTo(zoomMap); }, 3000);\n }\n });\n\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function zoomMap() {\n var zoomStart = context.map().zoom();\n\n var textId = context.lastPointerType() === 'mouse' ? 'zoom' : 'zoom_touch';\n var zoomString = helpHtml('intro.navigation.' + textId);\n\n reveal('.main-map .surface', zoomString);\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', zoomString, { duration: 0 });\n });\n\n context.map().on('move.intro', function() {\n if (context.map().zoom() !== zoomStart) {\n context.map().on('move.intro', null);\n timeout(function() { continueTo(features); }, 3000);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function features() {\n var onClick = function() { continueTo(pointsLinesAreas); };\n\n reveal('.main-map .surface', helpHtml('intro.navigation.features'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', helpHtml('intro.navigation.features'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('drawn.intro', null);\n nextStep();\n }\n }\n\n function pointsLinesAreas() {\n var onClick = function() { continueTo(nodesWays); };\n\n reveal('.main-map .surface', helpHtml('intro.navigation.points_lines_areas'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', helpHtml('intro.navigation.points_lines_areas'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('drawn.intro', null);\n nextStep();\n }\n }\n\n function nodesWays() {\n var onClick = function() { continueTo(clickTownHall); };\n\n reveal('.main-map .surface', helpHtml('intro.navigation.nodes_ways'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('drawn.intro', function() {\n reveal('.main-map .surface', helpHtml('intro.navigation.nodes_ways'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('drawn.intro', null);\n nextStep();\n }\n }\n\n function clickTownHall() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n reveal(null, null, { duration: 0 });\n context.map().centerZoomEase(entity.loc, 19, 500);\n\n timeout(function() {\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n var textId = context.lastPointerType() === 'mouse' ? 'click_townhall' : 'tap_townhall';\n reveal(box, helpHtml('intro.navigation.' + textId));\n\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.navigation.' + textId), { duration: 0 });\n });\n\n context.on('enter.intro', function() {\n if (isTownHallSelected()) continueTo(selectedTownHall);\n });\n\n }, 550); // after centerZoomEase\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function selectedTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n var entity = context.hasEntity(hallId);\n if (!entity) return clickTownHall();\n\n var box = pointBox(entity.loc, context);\n var onClick = function() { continueTo(editorTownHall); };\n\n reveal(box, helpHtml('intro.navigation.selected_townhall'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(hallId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.navigation.selected_townhall'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function editorTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n var onClick = function() { continueTo(presetTownHall); };\n\n reveal('.entity-editor-pane',\n helpHtml('intro.navigation.editor_townhall'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.on('exit.intro', function() {\n continueTo(clickTownHall);\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n nextStep();\n }\n }\n\n\n function presetTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n // preset match, in case the user happened to change it.\n var entity = context.entity(context.selectedIDs()[0]);\n var preset = presetManager.match(entity, context.graph());\n\n var onClick = function() { continueTo(fieldsTownHall); };\n\n reveal('.entity-editor-pane .section-feature-type',\n helpHtml('intro.navigation.preset_townhall', { preset: preset.name() }),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.on('exit.intro', function() {\n continueTo(clickTownHall);\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n nextStep();\n }\n }\n\n\n function fieldsTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n var onClick = function() { continueTo(closeTownHall); };\n\n reveal('.entity-editor-pane .section-preset-fields',\n helpHtml('intro.navigation.fields_townhall'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.on('exit.intro', function() {\n continueTo(clickTownHall);\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(hallId)) {\n continueTo(clickTownHall);\n }\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n nextStep();\n }\n }\n\n\n function closeTownHall() {\n if (!isTownHallSelected()) return clickTownHall();\n\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane',\n helpHtml('intro.navigation.close_townhall', { button: { html: icon(href, 'inline') } })\n );\n\n context.on('exit.intro', function() {\n continueTo(searchStreet);\n });\n\n context.history().on('change.intro', function() {\n // update the close icon in the tooltip if the user edits something.\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane',\n helpHtml('intro.navigation.close_townhall', { button: { html: icon(href, 'inline') } }),\n { duration: 0 }\n );\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function searchStreet() {\n context.enter(modeBrowse(context));\n context.history().reset('initial'); // ensure spring street exists\n\n var msec = transitionTime(springStreet, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(springStreet, 19, msec); // ..and user can see it\n\n timeout(function() {\n reveal('.search-header input',\n helpHtml('intro.navigation.search_street', { name: t('intro.graph.name.spring-street') })\n );\n\n context.container().select('.search-header input')\n .on('keyup.intro', checkSearchResult);\n }, msec + 100);\n }\n\n\n function checkSearchResult() {\n var first = context.container().select('.feature-list-item:nth-child(0n+2)'); // skip \"No Results\" item\n var firstName = first.select('.entity-name');\n var name = t('intro.graph.name.spring-street');\n\n if (!firstName.empty() && firstName.html() === name) {\n reveal(first.node(),\n helpHtml('intro.navigation.choose_street', { name: name }),\n { duration: 300 }\n );\n\n context.on('exit.intro', function() {\n continueTo(selectedStreet);\n });\n\n context.container().select('.search-header input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n }\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.container().select('.search-header input')\n .on('keydown.intro', null)\n .on('keyup.intro', null);\n nextStep();\n }\n }\n\n\n function selectedStreet() {\n if (!context.hasEntity(springStreetEndId) || !context.hasEntity(springStreetId)) {\n return searchStreet();\n }\n\n var onClick = function() { continueTo(editorStreet); };\n var entity = context.entity(springStreetEndId);\n var box = pointBox(entity.loc, context);\n box.height = 500;\n\n reveal(box,\n helpHtml('intro.navigation.selected_street', { name: t('intro.graph.name.spring-street') }),\n { duration: 600, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(springStreetEndId);\n if (!entity) return;\n var box = pointBox(entity.loc, context);\n box.height = 500;\n reveal(box,\n helpHtml('intro.navigation.selected_street', { name: t('intro.graph.name.spring-street') }),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n }, 600); // after reveal.\n\n context.on('enter.intro', function(mode) {\n if (!context.hasEntity(springStreetId)) {\n return continueTo(searchStreet);\n }\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== springStreetId) {\n // keep Spring Street selected..\n context.enter(modeSelect(context, [springStreetId]));\n }\n });\n\n context.history().on('change.intro', function() {\n if (!context.hasEntity(springStreetEndId) || !context.hasEntity(springStreetId)) {\n timeout(function() {\n continueTo(searchStreet);\n }, 300); // after any transition (e.g. if user deleted intersection)\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function editorStreet() {\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane', helpHtml('intro.navigation.street_different_fields') + '{br}' +\n helpHtml('intro.navigation.editor_street', {\n button: { html: icon(href, 'inline') },\n field1: onewayField.title(),\n field2: maxspeedField.title()\n }));\n\n context.on('exit.intro', function() {\n continueTo(play);\n });\n\n context.history().on('change.intro', function() {\n // update the close icon in the tooltip if the user edits something.\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n reveal('.entity-editor-pane', helpHtml('intro.navigation.street_different_fields') + '{br}' +\n helpHtml('intro.navigation.editor_street', {\n button: { html: icon(href, 'inline') },\n field1: onewayField.title(),\n field2: maxspeedField.title()\n }), { duration: 0 }\n );\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.navigation.play', { next: t('intro.points.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-point',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n dragMap();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.search-header input').on('keydown.intro keyup.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { actionChangePreset } from '../../actions/change_preset';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pointBox, pad, selectMenuItem, transitionTime } from './helper';\n\n\nexport function uiIntroPoint(context, reveal) {\n var dispatch = d3_dispatch('done');\n var timeouts = [];\n var intersection = [-85.63279, 41.94394];\n var building = [-85.632422, 41.944045];\n var cafePreset = presetManager.item('amenity/cafe');\n var _pointID = null;\n\n\n var chapter = {\n title: 'intro.points.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function addPoint() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var msec = transitionTime(intersection, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(intersection, 19, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-point',\n helpHtml('intro.points.points_info') + '{br}' + helpHtml('intro.points.add_point'));\n\n _pointID = null;\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-points');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-point') return;\n continueTo(placePoint);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function placePoint() {\n if (context.mode().id !== 'add-point') {\n return chapter.restart();\n }\n\n var pointBox = pad(building, 150, context);\n var textId = context.lastPointerType() === 'mouse' ? 'place_point' : 'place_point_touch';\n reveal(pointBox, helpHtml('intro.points.' + textId));\n\n context.map().on('move.intro drawn.intro', function() {\n pointBox = pad(building, 150, context);\n reveal(pointBox, helpHtml('intro.points.' + textId), { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return chapter.restart();\n _pointID = context.mode().selectedIDs()[0];\n\n if (context.graph().geometry(_pointID) === 'vertex'){\n\n //disallow all\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n\n reveal(pointBox, helpHtml('intro.points.place_point_error'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { return chapter.restart(); }\n });\n } else {\n continueTo(searchPreset);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function searchPreset() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return addPoint();\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.points.search_cafe', { preset: cafePreset.name() })\n );\n\n context.on('enter.intro', function(mode) {\n if (!_pointID || !context.hasEntity(_pointID)) {\n return continueTo(addPoint);\n }\n\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _pointID) {\n // keep the user's point selected..\n context.enter(modeSelect(context, [_pointID]));\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.points.search_cafe', { preset: cafePreset.name() })\n );\n\n context.history().on('change.intro', null);\n }\n });\n\n\n function checkPresetSearch() {\n var first = context.container().select('.preset-list-item:first-child');\n\n if (first.classed('preset-amenity-cafe')) {\n context.container().select('.preset-search-input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n\n reveal(first.select('.preset-list-button').node(),\n helpHtml('intro.points.choose_cafe', { preset: cafePreset.name() }),\n { duration: 300 }\n );\n\n context.history().on('change.intro', function() {\n continueTo(aboutFeatureEditor);\n });\n }\n }\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n nextStep();\n }\n }\n\n\n function aboutFeatureEditor() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return addPoint();\n }\n\n timeout(function() {\n reveal('.entity-editor-pane', helpHtml('intro.points.feature_editor'), {\n tooltipClass: 'intro-points-describe',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(addName); }\n });\n }, 400);\n\n context.on('exit.intro', function() {\n // if user leaves select mode here, just continue with the tutorial.\n continueTo(reselectPoint);\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function addName() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return addPoint();\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n var addNameString = helpHtml('intro.points.fields_info') + '{br}' + helpHtml('intro.points.add_name') + '{br}' + helpHtml('intro.points.add_reminder');\n\n timeout(function() {\n // It's possible for the user to add a name in a previous step..\n // If so, don't tell them to add the name in this step.\n // Give them an OK button instead.\n var entity = context.entity(_pointID);\n if (entity.tags.name) {\n var tooltip = reveal('.entity-editor-pane', addNameString, {\n tooltipClass: 'intro-points-describe',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(addCloseEditor); }\n });\n tooltip.select('.instruction').style('display', 'none');\n\n } else {\n reveal('.entity-editor-pane', addNameString,\n { tooltipClass: 'intro-points-describe' }\n );\n }\n }, 400);\n\n context.history().on('change.intro', function() {\n continueTo(addCloseEditor);\n });\n\n context.on('exit.intro', function() {\n // if user leaves select mode here, just continue with the tutorial.\n continueTo(reselectPoint);\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function addCloseEditor() {\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n var selector = '.entity-editor-pane button.close svg use';\n var href = d3_select(selector).attr('href') || '#iD-icon-close';\n\n context.on('exit.intro', function() {\n continueTo(reselectPoint);\n });\n\n reveal('.entity-editor-pane',\n helpHtml('intro.points.add_close', { button: { html: icon(href, 'inline') } })\n );\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function reselectPoint() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n\n // make sure it's still a cafe, in case user somehow changed it..\n var oldPreset = presetManager.match(entity, context.graph());\n context.replace(actionChangePreset(_pointID, oldPreset, cafePreset));\n\n context.enter(modeBrowse(context));\n\n var msec = transitionTime(entity.loc, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerEase(entity.loc, msec);\n\n timeout(function() {\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.points.reselect'), { duration: 600 });\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.points.reselect'), { duration: 0 });\n });\n }, 600); // after reveal..\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n continueTo(updatePoint);\n });\n\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function updatePoint() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return continueTo(reselectPoint);\n }\n\n // reset pane, in case user happened to untag the point..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n context.on('exit.intro', function() {\n continueTo(reselectPoint);\n });\n\n context.history().on('change.intro', function() {\n continueTo(updateCloseEditor);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane', helpHtml('intro.points.update'),\n { tooltipClass: 'intro-points-describe' }\n );\n }, 400);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function updateCloseEditor() {\n if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {\n return continueTo(reselectPoint);\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n context.on('exit.intro', function() {\n continueTo(rightClickPoint);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.points.update_close', { button: { html: icon('#iD-icon-close', 'inline') } })\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickPoint() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n\n context.enter(modeBrowse(context));\n\n var box = pointBox(entity.loc, context);\n var textId = context.lastPointerType() === 'mouse' ? 'rightclick' : 'edit_menu_touch';\n reveal(box, helpHtml('intro.points.' + textId), { duration: 600 });\n\n timeout(function() {\n context.map().on('move.intro', function() {\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n var box = pointBox(entity.loc, context);\n reveal(box, helpHtml('intro.points.' + textId), { duration: 0 });\n });\n }, 600); // after reveal\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== _pointID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'delete').node();\n if (!node) return;\n continueTo(enterDelete);\n }, 50); // after menu visible\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro', null);\n nextStep();\n }\n }\n\n\n function enterDelete() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (!entity) return chapter.restart();\n\n var node = selectMenuItem(context, 'delete').node();\n if (!node) { return continueTo(rightClickPoint); }\n\n reveal('.edit-menu',\n helpHtml('intro.points.delete'),\n { padding: 50 }\n );\n\n timeout(function() {\n context.map().on('move.intro', function() {\n if (selectMenuItem(context, 'delete').empty()) {\n return continueTo(rightClickPoint);\n }\n reveal('.edit-menu',\n helpHtml('intro.points.delete'),\n { duration: 0, padding: 50 }\n );\n });\n }, 300); // after menu visible\n\n context.on('exit.intro', function() {\n if (!_pointID) return chapter.restart();\n var entity = context.hasEntity(_pointID);\n if (entity) return continueTo(rightClickPoint); // point still exists\n });\n\n context.history().on('change.intro', function(changed) {\n if (changed.deleted().length) {\n continueTo(undo);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro', null);\n context.history().on('change.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function undo() {\n context.history().on('change.intro', function() {\n continueTo(play);\n });\n\n reveal('.top-toolbar button.undo-button',\n helpHtml('intro.points.undo')\n );\n\n function continueTo(nextStep) {\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.points.play', { next: t('intro.areas.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-area',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addPoint();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n interpolateNumber as d3_interpolateNumber\n} from 'd3-interpolate';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pad, transitionTime } from './helper';\n\n\nexport function uiIntroArea(context, reveal) {\n var dispatch = d3_dispatch('done');\n var playground = [-85.63552, 41.94159];\n var playgroundPreset = presetManager.item('leisure/playground');\n var nameField = presetManager.field('name');\n var descriptionField = presetManager.field('description');\n var timeouts = [];\n var _areaID;\n\n\n var chapter = {\n title: 'intro.areas.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function revealPlayground(center, text, options) {\n var padding = 180 * Math.pow(2, context.map().zoom() - 19.5);\n var box = pad(center, padding, context);\n reveal(box, text, options);\n }\n\n\n function addArea() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n _areaID = null;\n\n var msec = transitionTime(playground, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(playground, 19, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-area',\n helpHtml('intro.areas.add_playground'));\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-areas');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-area') return;\n continueTo(startPlayground);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startPlayground() {\n if (context.mode().id !== 'add-area') {\n return chapter.restart();\n }\n\n _areaID = null;\n context.map().zoomEase(19.5, 500);\n\n timeout(function() {\n var textId = context.lastPointerType() === 'mouse' ? 'starting_node_click' : 'starting_node_tap';\n var startDrawString = helpHtml('intro.areas.start_playground') + helpHtml('intro.areas.' + textId);\n revealPlayground(playground,\n startDrawString, { duration: 250 }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n revealPlayground(playground,\n startDrawString, { duration: 0 }\n );\n });\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-area') return chapter.restart();\n continueTo(continuePlayground);\n });\n }, 250); // after reveal\n\n }, 550); // after easing\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continuePlayground() {\n if (context.mode().id !== 'draw-area') {\n return chapter.restart();\n }\n\n _areaID = null;\n revealPlayground(playground,\n helpHtml('intro.areas.continue_playground'),\n { duration: 250 }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n revealPlayground(playground,\n helpHtml('intro.areas.continue_playground'),\n { duration: 0 }\n );\n });\n }, 250); // after reveal\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n var entity = context.hasEntity(context.selectedIDs()[0]);\n if (entity && entity.nodes.length >= 6) {\n return continueTo(finishPlayground);\n } else {\n return;\n }\n } else if (mode.id === 'select') {\n _areaID = context.selectedIDs()[0];\n return continueTo(searchPresets);\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function finishPlayground() {\n if (context.mode().id !== 'draw-area') {\n return chapter.restart();\n }\n\n _areaID = null;\n\n var finishString = helpHtml('intro.areas.finish_area_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.areas.finish_playground');\n revealPlayground(playground,\n finishString, { duration: 250 }\n );\n\n timeout(function() {\n context.map().on('move.intro drawn.intro', function() {\n revealPlayground(playground,\n finishString, { duration: 0 }\n );\n });\n }, 250); // after reveal\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n return;\n } else if (mode.id === 'select') {\n _areaID = context.selectedIDs()[0];\n return continueTo(searchPresets);\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function searchPresets() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n context.enter(modeSelect(context, [_areaID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.areas.search_playground', { preset: playgroundPreset.name() })\n );\n }, 400); // after preset list pane visible..\n\n context.on('enter.intro', function(mode) {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return continueTo(addArea);\n }\n\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _areaID) {\n // keep the user's area selected..\n context.enter(modeSelect(context, [_areaID]));\n\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.areas.search_playground', { preset: playgroundPreset.name() })\n );\n\n context.history().on('change.intro', null);\n }\n });\n\n function checkPresetSearch() {\n var first = context.container().select('.preset-list-item:first-child');\n\n if (first.classed('preset-leisure-playground')) {\n reveal(first.select('.preset-list-button').node(),\n helpHtml('intro.areas.choose_playground', { preset: playgroundPreset.name() }),\n { duration: 300 }\n );\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n\n context.history().on('change.intro', function() {\n continueTo(clickAddField);\n });\n }\n }\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n nextStep();\n }\n }\n\n\n function clickAddField() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n if (!context.container().select('.form-field-description').empty()) {\n return continueTo(describePlayground);\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n // It's possible for the user to add a description in a previous step..\n // If they did this already, just continue to next step.\n var entity = context.entity(_areaID);\n if (entity.tags.description) {\n return continueTo(play);\n }\n\n // scroll \"Add field\" into view\n var box = context.container().select('.more-fields').node().getBoundingClientRect();\n if (box.top > 300) {\n var pane = context.container().select('.entity-editor-pane .inspector-body');\n var start = pane.node().scrollTop;\n var end = start + (box.top - 300);\n\n pane\n .transition()\n .duration(250)\n .tween('scroll.inspector', function() {\n var node = this;\n var i = d3_interpolateNumber(start, end);\n return function(t) {\n node.scrollTop = i(t);\n };\n });\n }\n\n timeout(function() {\n reveal('.more-fields .combobox-input',\n helpHtml('intro.areas.add_field', {\n name: nameField.title(),\n description: descriptionField.title()\n }),\n { duration: 300 }\n );\n\n context.container().select('.more-fields .combobox-input')\n .on('click.intro', function() {\n // Watch for the combobox to appear...\n var watcher;\n watcher = window.setInterval(function() {\n if (!context.container().select('div.combobox').empty()) {\n window.clearInterval(watcher);\n continueTo(chooseDescriptionField);\n }\n }, 300);\n });\n }, 300); // after \"Add Field\" visible\n\n }, 400); // after editor pane visible\n\n context.on('exit.intro', function() {\n return continueTo(searchPresets);\n });\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.more-fields .combobox-input').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function chooseDescriptionField() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n if (!context.container().select('.form-field-description').empty()) {\n return continueTo(describePlayground);\n }\n\n // Make sure combobox is ready..\n if (context.container().select('div.combobox').empty()) {\n return continueTo(clickAddField);\n }\n // Watch for the combobox to go away..\n var watcher;\n watcher = window.setInterval(function() {\n if (context.container().select('div.combobox').empty()) {\n window.clearInterval(watcher);\n timeout(function() {\n if (context.container().select('.form-field-description').empty()) {\n continueTo(retryChooseDescription);\n } else {\n continueTo(describePlayground);\n }\n }, 300); // after description field added.\n }\n }, 300);\n\n reveal('div.combobox',\n helpHtml('intro.areas.choose_field', { field: descriptionField.title() }),\n { duration: 300 }\n );\n\n context.on('exit.intro', function() {\n return continueTo(searchPresets);\n });\n\n function continueTo(nextStep) {\n if (watcher) window.clearInterval(watcher);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function describePlayground() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n if (context.container().select('.form-field-description').empty()) {\n return continueTo(retryChooseDescription);\n }\n\n context.on('exit.intro', function() {\n continueTo(play);\n });\n\n reveal('.entity-editor-pane',\n helpHtml('intro.areas.describe_playground', { button: { html: icon('#iD-icon-close', 'inline') } }),\n { duration: 300 }\n );\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function retryChooseDescription() {\n if (!_areaID || !context.hasEntity(_areaID)) {\n return addArea();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _areaID) {\n return searchPresets();\n }\n\n // reset pane, in case user happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '0%');\n\n reveal('.entity-editor-pane',\n helpHtml('intro.areas.retry_add_field', { field: descriptionField.title() }), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(clickAddField); }\n });\n\n context.on('exit.intro', function() {\n return continueTo(searchPresets);\n });\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.areas.play', { next: t('intro.lines.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-line',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addArea();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n context.container().select('.more-fields .combobox-input').on('click.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { geoSphericalDistance } from '../../geo';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilRebind } from '../../util/rebind';\nimport { helpHtml, icon, pad, selectMenuItem, transitionTime } from './helper';\n\n\nexport function uiIntroLine(context, reveal) {\n var dispatch = d3_dispatch('done');\n var timeouts = [];\n var _tulipRoadID = null;\n var flowerRoadID = 'w646';\n var tulipRoadStart = [-85.6297754121684, 41.95805253325314];\n var tulipRoadMidpoint = [-85.62975395449628, 41.95787501510204];\n var tulipRoadIntersection = [-85.62974496187628, 41.95742515554585];\n var roadCategory = presetManager.item('category-road_minor');\n var residentialPreset = presetManager.item('highway/residential');\n var woodRoadID = 'w525';\n var woodRoadEndID = 'n2862';\n var woodRoadAddNode = [-85.62390110349587, 41.95397111462291];\n var woodRoadDragEndpoint = [-85.623867390213, 41.95466987786487];\n var woodRoadDragMidpoint = [-85.62386254803509, 41.95430395953872];\n var washingtonStreetID = 'w522';\n var twelfthAvenueID = 'w1';\n var eleventhAvenueEndID = 'n3550';\n var twelfthAvenueEndID = 'n5';\n var _washingtonSegmentID = null;\n var eleventhAvenueEnd = context.entity(eleventhAvenueEndID).loc;\n var twelfthAvenueEnd = context.entity(twelfthAvenueEndID).loc;\n var deleteLinesLoc = [-85.6219395542764, 41.95228033922477];\n var twelfthAvenue = [-85.62219310052491, 41.952505413152956];\n\n\n var chapter = {\n title: 'intro.lines.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function addLine() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n\n var msec = transitionTime(tulipRoadStart, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(tulipRoadStart, 18.5, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-line',\n helpHtml('intro.lines.add_line'));\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-lines');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-line') return;\n continueTo(startLine);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startLine() {\n if (context.mode().id !== 'add-line') return chapter.restart();\n\n _tulipRoadID = null;\n\n var padding = 70 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(tulipRoadStart, padding, context);\n box.height = box.height + 100;\n\n var textId = context.lastPointerType() === 'mouse' ? 'start_line' : 'start_line_tap';\n var startLineString = helpHtml('intro.lines.missing_road') + '{br}' +\n helpHtml('intro.lines.line_draw_info') +\n helpHtml('intro.lines.' + textId);\n reveal(box, startLineString);\n\n context.map().on('move.intro drawn.intro', function() {\n padding = 70 * Math.pow(2, context.map().zoom() - 18);\n box = pad(tulipRoadStart, padding, context);\n box.height = box.height + 100;\n reveal(box, startLineString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-line') return chapter.restart();\n continueTo(drawLine);\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function drawLine() {\n if (context.mode().id !== 'draw-line') return chapter.restart();\n\n _tulipRoadID = context.mode().selectedIDs()[0];\n context.map().centerEase(tulipRoadMidpoint, 500);\n\n timeout(function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18.5);\n var box = pad(tulipRoadMidpoint, padding, context);\n box.height = box.height * 2;\n reveal(box,\n helpHtml('intro.lines.intersect', { name: t('intro.graph.name.flower-street') })\n );\n\n context.map().on('move.intro drawn.intro', function() {\n padding = 200 * Math.pow(2, context.map().zoom() - 18.5);\n box = pad(tulipRoadMidpoint, padding, context);\n box.height = box.height * 2;\n reveal(box,\n helpHtml('intro.lines.intersect', { name: t('intro.graph.name.flower-street') }),\n { duration: 0 }\n );\n });\n }, 550); // after easing..\n\n context.history().on('change.intro', function() {\n if (isLineConnected()) {\n continueTo(continueLine);\n }\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-line') {\n return;\n } else if (mode.id === 'select') {\n continueTo(retryIntersect);\n return;\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function isLineConnected() {\n var entity = _tulipRoadID && context.hasEntity(_tulipRoadID);\n if (!entity) return false;\n\n var drawNodes = context.graph().childNodes(entity);\n return drawNodes.some(function(node) {\n return context.graph().parentWays(node).some(function(parent) {\n return parent.id === flowerRoadID;\n });\n });\n }\n\n\n function retryIntersect() {\n d3_select(window).on('pointerdown.intro mousedown.intro', eventCancel, true);\n\n var box = pad(tulipRoadIntersection, 80, context);\n reveal(box,\n helpHtml('intro.lines.retry_intersect', { name: t('intro.graph.name.flower-street') })\n );\n\n timeout(chapter.restart, 3000);\n }\n\n\n function continueLine() {\n if (context.mode().id !== 'draw-line') return chapter.restart();\n var entity = _tulipRoadID && context.hasEntity(_tulipRoadID);\n if (!entity) return chapter.restart();\n\n context.map().centerEase(tulipRoadIntersection, 500);\n\n var continueLineText = helpHtml('intro.lines.continue_line') + '{br}' +\n helpHtml('intro.lines.finish_line_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.lines.finish_road');\n\n reveal('.main-map .surface', continueLineText);\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-line') {\n return;\n } else if (mode.id === 'select') {\n return continueTo(chooseCategoryRoad);\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function chooseCategoryRoad() {\n if (context.mode().id !== 'select') return chapter.restart();\n\n context.on('exit.intro', function() {\n return chapter.restart();\n });\n\n var button = context.container().select('.preset-category-road_minor .preset-list-button');\n if (button.empty()) return chapter.restart();\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n reveal(button.node(),\n helpHtml('intro.lines.choose_category_road', { category: roadCategory.name() })\n );\n\n button.on('click.intro', function() {\n continueTo(choosePresetResidential);\n });\n\n }, 400); // after editor pane visible\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function choosePresetResidential() {\n if (context.mode().id !== 'select') return chapter.restart();\n\n context.on('exit.intro', function() {\n return chapter.restart();\n });\n\n var subgrid = context.container().select('.preset-category-road_minor .subgrid');\n if (subgrid.empty()) return chapter.restart();\n\n subgrid.selectAll(':not(.preset-highway-residential) .preset-list-button')\n .on('click.intro', function() {\n continueTo(retryPresetResidential);\n });\n\n subgrid.selectAll('.preset-highway-residential .preset-list-button')\n .on('click.intro', function() {\n continueTo(nameRoad);\n });\n\n timeout(function() {\n reveal(subgrid.node(),\n helpHtml('intro.lines.choose_preset_residential', { preset: residentialPreset.name() }),\n { tooltipBox: '.preset-highway-residential .preset-list-button', duration: 300 }\n );\n }, 300);\n\n function continueTo(nextStep) {\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n // selected wrong road type\n function retryPresetResidential() {\n if (context.mode().id !== 'select') return chapter.restart();\n\n context.on('exit.intro', function() {\n return chapter.restart();\n });\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n var button = context.container().select('.entity-editor-pane .preset-list-button');\n\n reveal(button.node(),\n helpHtml('intro.lines.retry_preset_residential', { preset: residentialPreset.name() })\n );\n\n button.on('click.intro', function() {\n continueTo(chooseCategoryRoad);\n });\n\n }, 500);\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function nameRoad() {\n context.on('exit.intro', function() {\n continueTo(didNameRoad);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.lines.name_road', { button: { html: icon('#iD-icon-close', 'inline') } }),\n { tooltipClass: 'intro-lines-name_road' }\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function didNameRoad() {\n context.history().checkpoint('doneAddLine');\n\n timeout(function() {\n reveal('.main-map .surface', helpHtml('intro.lines.did_name_road'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(updateLine); }\n });\n }, 500);\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function updateLine() {\n context.history().reset('doneAddLine');\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return chapter.restart();\n }\n\n var msec = transitionTime(woodRoadDragMidpoint, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(woodRoadDragMidpoint, 19, msec);\n\n timeout(function() {\n var padding = 250 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n var advance = function() { continueTo(addNode); };\n\n reveal(box, helpHtml('intro.lines.update_line'),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 250 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n reveal(box, helpHtml('intro.lines.update_line'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function addNode() {\n context.history().reset('doneAddLine');\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return chapter.restart();\n }\n\n var padding = 40 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadAddNode, padding, context);\n var addNodeString = helpHtml('intro.lines.add_node' + (context.lastPointerType() === 'mouse' ? '' : '_touch'));\n reveal(box, addNodeString);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 40 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadAddNode, padding, context);\n reveal(box, addNodeString, { duration: 0 });\n });\n\n context.history().on('change.intro', function(changed) {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n if (changed.created().length === 1) {\n timeout(function() { continueTo(startDragEndpoint); }, 500);\n }\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') {\n continueTo(updateLine);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startDragEndpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n var startDragString = helpHtml('intro.lines.start_drag_endpoint' + (context.lastPointerType() === 'mouse' ? '' : '_touch')) +\n helpHtml('intro.lines.drag_to_intersection');\n reveal(box, startDragString);\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n reveal(box, startDragString, { duration: 0 });\n\n var entity = context.entity(woodRoadEndID);\n if (geoSphericalDistance(entity.loc, woodRoadDragEndpoint) <= 4) {\n continueTo(finishDragEndpoint);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function finishDragEndpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n var finishDragString = helpHtml('intro.lines.spot_looks_good') +\n helpHtml('intro.lines.finish_drag_endpoint' + (context.lastPointerType() === 'mouse' ? '' : '_touch'));\n reveal(box, finishDragString);\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n reveal(box, finishDragString, { duration: 0 });\n\n var entity = context.entity(woodRoadEndID);\n if (geoSphericalDistance(entity.loc, woodRoadDragEndpoint) > 4) {\n continueTo(startDragEndpoint);\n }\n });\n\n context.on('enter.intro', function() {\n continueTo(startDragMidpoint);\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startDragMidpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n if (context.selectedIDs().indexOf(woodRoadID) === -1) {\n context.enter(modeSelect(context, [woodRoadID]));\n }\n\n var padding = 80 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n reveal(box, helpHtml('intro.lines.start_drag_midpoint'));\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 80 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragMidpoint, padding, context);\n reveal(box, helpHtml('intro.lines.start_drag_midpoint'), { duration: 0 });\n });\n\n context.history().on('change.intro', function(changed) {\n if (changed.created().length === 1) {\n continueTo(continueDragMidpoint);\n }\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') {\n // keep Wood Road selected so midpoint triangles are drawn..\n context.enter(modeSelect(context, [woodRoadID]));\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continueDragMidpoint() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n box.height += 400;\n\n var advance = function() {\n context.history().checkpoint('doneUpdateLine');\n continueTo(deleteLines);\n };\n\n reveal(box, helpHtml('intro.lines.continue_drag_midpoint'),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {\n return continueTo(updateLine);\n }\n var padding = 100 * Math.pow(2, context.map().zoom() - 19);\n var box = pad(woodRoadDragEndpoint, padding, context);\n box.height += 400;\n reveal(box, helpHtml('intro.lines.continue_drag_midpoint'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function deleteLines() {\n context.history().reset('doneUpdateLine');\n context.enter(modeBrowse(context));\n\n if (!context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return chapter.restart();\n }\n\n var msec = transitionTime(deleteLinesLoc, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(deleteLinesLoc, 18, msec);\n\n timeout(function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(deleteLinesLoc, padding, context);\n box.top -= 200;\n box.height += 400;\n var advance = function() { continueTo(rightClickIntersection); };\n\n reveal(box, helpHtml('intro.lines.delete_lines', { street: t('intro.graph.name.12th-avenue') }),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(deleteLinesLoc, padding, context);\n box.top -= 200;\n box.height += 400;\n reveal(box, helpHtml('intro.lines.delete_lines', { street: t('intro.graph.name.12th-avenue') }),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n\n context.history().on('change.intro', function() {\n timeout(function() {\n continueTo(deleteLines);\n }, 500); // after any transition (e.g. if user deleted intersection)\n });\n\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickIntersection() {\n context.history().reset('doneUpdateLine');\n context.enter(modeBrowse(context));\n\n context.map().centerZoomEase(eleventhAvenueEnd, 18, 500);\n\n var rightClickString = helpHtml('intro.lines.split_street', {\n street1: t('intro.graph.name.11th-avenue'),\n street2: t('intro.graph.name.washington-street')\n }) +\n helpHtml('intro.lines.' + (context.lastPointerType() === 'mouse' ? 'rightclick_intersection' : 'edit_menu_intersection_touch'));\n\n timeout(function() {\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, rightClickString);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, rightClickString,\n { duration: 0 }\n );\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== eleventhAvenueEndID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'split').node();\n if (!node) return;\n continueTo(splitIntersection);\n }, 50); // after menu visible\n });\n\n context.history().on('change.intro', function() {\n timeout(function() {\n continueTo(deleteLines);\n }, 300); // after any transition (e.g. if user deleted intersection)\n });\n\n }, 600);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function splitIntersection() {\n if (!context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(deleteLines);\n }\n\n var node = selectMenuItem(context, 'split').node();\n if (!node) { return continueTo(rightClickIntersection); }\n\n var wasChanged = false;\n _washingtonSegmentID = null;\n\n reveal('.edit-menu', helpHtml('intro.lines.split_intersection',\n { street: t('intro.graph.name.washington-street') }),\n { padding: 50 }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var node = selectMenuItem(context, 'split').node();\n if (!wasChanged && !node) { return continueTo(rightClickIntersection); }\n\n reveal('.edit-menu', helpHtml('intro.lines.split_intersection',\n { street: t('intro.graph.name.washington-street') }),\n { duration: 0, padding: 50 }\n );\n });\n\n context.history().on('change.intro', function(changed) {\n wasChanged = true;\n timeout(function() {\n if (context.history().undoAnnotation() === t('operations.split.annotation.line', { n: 1 })) {\n _washingtonSegmentID = changed.created()[0].id;\n continueTo(didSplit);\n } else {\n _washingtonSegmentID = null;\n continueTo(retrySplit);\n }\n }, 300); // after any transition (e.g. if user deleted intersection)\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retrySplit() {\n context.enter(modeBrowse(context));\n context.map().centerZoomEase(eleventhAvenueEnd, 18, 500);\n var advance = function() { continueTo(rightClickIntersection); };\n\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, helpHtml('intro.lines.retry_split'),\n { buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 60 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(eleventhAvenueEnd, padding, context);\n reveal(box, helpHtml('intro.lines.retry_split'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: advance }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function didSplit() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var ids = context.selectedIDs();\n var string = 'intro.lines.did_split_' + (ids.length > 1 ? 'multi' : 'single');\n var street = t('intro.graph.name.washington-street');\n\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n box.width = box.width / 2;\n reveal(box, helpHtml(string, { street1: street, street2: street }),\n { duration: 500 }\n );\n\n timeout(function() {\n context.map().centerZoomEase(twelfthAvenue, 18, 500);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n box.width = box.width / 2;\n reveal(box, helpHtml(string, { street1: street, street2: street }),\n { duration: 0 }\n );\n });\n }, 600); // after initial reveal and curtain cut\n\n context.on('enter.intro', function() {\n var ids = context.selectedIDs();\n if (ids.length === 1 && ids[0] === _washingtonSegmentID) {\n continueTo(multiSelect);\n }\n });\n\n context.history().on('change.intro', function() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function multiSelect() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var ids = context.selectedIDs();\n var hasWashington = ids.indexOf(_washingtonSegmentID) !== -1;\n var hasTwelfth = ids.indexOf(twelfthAvenueID) !== -1;\n\n if (hasWashington && hasTwelfth) {\n return continueTo(multiRightClick);\n } else if (!hasWashington && !hasTwelfth) {\n return continueTo(didSplit);\n }\n\n context.map().centerZoomEase(twelfthAvenue, 18, 500);\n\n timeout(function() {\n var selected, other, padding, box;\n if (hasWashington) {\n selected = t('intro.graph.name.washington-street');\n other = t('intro.graph.name.12th-avenue');\n padding = 60 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenueEnd, padding, context);\n box.width *= 3;\n } else {\n selected = t('intro.graph.name.12th-avenue');\n other = t('intro.graph.name.washington-street');\n padding = 200 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenue, padding, context);\n box.width /= 2;\n }\n\n reveal(box,\n helpHtml('intro.lines.multi_select',\n { selected: selected, other1: other }) + ' ' +\n helpHtml('intro.lines.add_to_selection_' + (context.lastPointerType() === 'mouse' ? 'click' : 'touch'),\n { selected: selected, other2: other })\n );\n\n context.map().on('move.intro drawn.intro', function() {\n if (hasWashington) {\n selected = t('intro.graph.name.washington-street');\n other = t('intro.graph.name.12th-avenue');\n padding = 60 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenueEnd, padding, context);\n box.width *= 3;\n } else {\n selected = t('intro.graph.name.12th-avenue');\n other = t('intro.graph.name.washington-street');\n padding = 200 * Math.pow(2, context.map().zoom() - 18);\n box = pad(twelfthAvenue, padding, context);\n box.width /= 2;\n }\n\n reveal(box,\n helpHtml('intro.lines.multi_select',\n { selected: selected, other1: other }) + ' ' +\n helpHtml('intro.lines.add_to_selection_' + (context.lastPointerType() === 'mouse' ? 'click' : 'touch'),\n { selected: selected, other2: other }),\n { duration: 0 }\n );\n });\n\n context.on('enter.intro', function() {\n continueTo(multiSelect);\n });\n\n context.history().on('change.intro', function() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n });\n }, 600);\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function multiRightClick() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n\n var rightClickString = helpHtml('intro.lines.multi_select_success') +\n helpHtml('intro.lines.multi_' + (context.lastPointerType() === 'mouse' ? 'rightclick' : 'edit_menu_touch'));\n reveal(box, rightClickString);\n\n context.map().on('move.intro drawn.intro', function() {\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n reveal(box, rightClickString, { duration: 0 });\n });\n\n context.ui().editMenu().on('toggled.intro', function(open) {\n if (!open) return;\n\n timeout(function() {\n var ids = context.selectedIDs();\n if (ids.length === 2 &&\n ids.indexOf(twelfthAvenueID) !== -1 &&\n ids.indexOf(_washingtonSegmentID) !== -1) {\n var node = selectMenuItem(context, 'delete').node();\n if (!node) return;\n continueTo(multiDelete);\n } else if (ids.length === 1 &&\n ids.indexOf(_washingtonSegmentID) !== -1) {\n return continueTo(multiSelect);\n } else {\n return continueTo(didSplit);\n }\n }, 300); // after edit menu visible\n });\n\n context.history().on('change.intro', function() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.ui().editMenu().on('toggled.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function multiDelete() {\n if (!_washingtonSegmentID ||\n !context.hasEntity(_washingtonSegmentID) ||\n !context.hasEntity(washingtonStreetID) ||\n !context.hasEntity(twelfthAvenueID) ||\n !context.hasEntity(eleventhAvenueEndID)) {\n return continueTo(rightClickIntersection);\n }\n\n var node = selectMenuItem(context, 'delete').node();\n if (!node) return continueTo(multiRightClick);\n\n reveal('.edit-menu',\n helpHtml('intro.lines.multi_delete'),\n { padding: 50 }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n reveal('.edit-menu',\n helpHtml('intro.lines.multi_delete'),\n { duration: 0, padding: 50 }\n );\n });\n\n context.on('exit.intro', function() {\n if (context.hasEntity(_washingtonSegmentID) || context.hasEntity(twelfthAvenueID)) {\n return continueTo(multiSelect); // left select mode but roads still exist\n }\n });\n\n context.history().on('change.intro', function() {\n if (context.hasEntity(_washingtonSegmentID) || context.hasEntity(twelfthAvenueID)) {\n continueTo(retryDelete); // changed something but roads still exist\n } else {\n continueTo(play);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('exit.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retryDelete() {\n context.enter(modeBrowse(context));\n\n var padding = 200 * Math.pow(2, context.map().zoom() - 18);\n var box = pad(twelfthAvenue, padding, context);\n reveal(box, helpHtml('intro.lines.retry_delete'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(multiSelect); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.lines.play', { next: t('intro.buildings.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-building',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addLine();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n d3_select(window).on('pointerdown.intro mousedown.intro', null, true);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { utilArrayUniq, utilRebind } from '../../util';\nimport { helpHtml, icon, pad, isMostlySquare, selectMenuItem, transitionTime } from './helper';\n\n\nexport function uiIntroBuilding(context, reveal) {\n var dispatch = d3_dispatch('done');\n var house = [-85.62815, 41.95638];\n var tank = [-85.62732, 41.95347];\n var buildingCatetory = presetManager.item('category-building');\n var housePreset = presetManager.item('building/house');\n var tankPreset = presetManager.item('man_made/storage_tank');\n var timeouts = [];\n var _houseID = null;\n var _tankID = null;\n\n\n var chapter = {\n title: 'intro.buildings.title'\n };\n\n\n function timeout(f, t) {\n timeouts.push(window.setTimeout(f, t));\n }\n\n\n function eventCancel(d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n }\n\n\n function revealHouse(center, text, options) {\n var padding = 160 * Math.pow(2, context.map().zoom() - 20);\n var box = pad(center, padding, context);\n reveal(box, text, options);\n }\n\n\n function revealTank(center, text, options) {\n var padding = 190 * Math.pow(2, context.map().zoom() - 19.5);\n var box = pad(center, padding, context);\n reveal(box, text, options);\n }\n\n\n function addHouse() {\n context.enter(modeBrowse(context));\n context.history().reset('initial');\n _houseID = null;\n\n var msec = transitionTime(house, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(house, 19, msec);\n\n timeout(function() {\n var tooltip = reveal('button.add-area',\n helpHtml('intro.buildings.add_building'));\n\n tooltip.selectAll('.popover-inner')\n .insert('svg', 'span')\n .attr('class', 'tooltip-illustration')\n .append('use')\n .attr('xlink:href', '#iD-graphic-buildings');\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-area') return;\n continueTo(startHouse);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startHouse() {\n if (context.mode().id !== 'add-area') {\n return continueTo(addHouse);\n }\n\n _houseID = null;\n context.map().zoomEase(20, 500);\n\n timeout(function() {\n var startString = helpHtml('intro.buildings.start_building') +\n helpHtml('intro.buildings.building_corner_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap'));\n revealHouse(house, startString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealHouse(house, startString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-area') return chapter.restart();\n continueTo(continueHouse);\n });\n\n }, 550); // after easing\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continueHouse() {\n if (context.mode().id !== 'draw-area') {\n return continueTo(addHouse);\n }\n\n _houseID = null;\n\n var continueString = helpHtml('intro.buildings.continue_building') + '{br}' +\n helpHtml('intro.areas.finish_area_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.buildings.finish_building');\n\n revealHouse(house, continueString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealHouse(house, continueString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n return;\n } else if (mode.id === 'select') {\n var graph = context.graph();\n var way = context.entity(context.selectedIDs()[0]);\n var nodes = graph.childNodes(way);\n var points = utilArrayUniq(nodes)\n .map(function(n) { return context.projection(n.loc); });\n\n if (isMostlySquare(points)) {\n _houseID = way.id;\n return continueTo(chooseCategoryBuilding);\n } else {\n return continueTo(retryHouse);\n }\n\n } else {\n return chapter.restart();\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function retryHouse() {\n var onClick = function() { continueTo(addHouse); };\n\n revealHouse(house, helpHtml('intro.buildings.retry_building'),\n { buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n\n context.map().on('move.intro drawn.intro', function() {\n revealHouse(house, helpHtml('intro.buildings.retry_building'),\n { duration: 0, buttonText: t.html('intro.ok'), buttonCallback: onClick }\n );\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n nextStep();\n }\n }\n\n\n function chooseCategoryBuilding() {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return addHouse();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {\n context.enter(modeSelect(context, [_houseID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n var button = context.container().select('.preset-category-building .preset-list-button');\n\n reveal(button.node(),\n helpHtml('intro.buildings.choose_category_building', { category: buildingCatetory.name() })\n );\n\n button.on('click.intro', function() {\n button.on('click.intro', null);\n continueTo(choosePresetHouse);\n });\n\n }, 400); // after preset list pane visible..\n\n\n context.on('enter.intro', function(mode) {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return continueTo(addHouse);\n }\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _houseID) {\n return continueTo(chooseCategoryBuilding);\n }\n });\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function choosePresetHouse() {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return addHouse();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {\n context.enter(modeSelect(context, [_houseID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n var button = context.container().select('.preset-building-house .preset-list-button');\n\n reveal(button.node(),\n helpHtml('intro.buildings.choose_preset_house', { preset: housePreset.name() }),\n { duration: 300 }\n );\n\n button.on('click.intro', function() {\n button.on('click.intro', null);\n continueTo(closeEditorHouse);\n });\n\n }, 400); // after preset list pane visible..\n\n context.on('enter.intro', function(mode) {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return continueTo(addHouse);\n }\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _houseID) {\n return continueTo(chooseCategoryBuilding);\n }\n });\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-list-button').on('click.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function closeEditorHouse() {\n if (!_houseID || !context.hasEntity(_houseID)) {\n return addHouse();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _houseID) {\n context.enter(modeSelect(context, [_houseID]));\n }\n\n context.history().checkpoint('hasHouse');\n\n context.on('exit.intro', function() {\n continueTo(rightClickHouse);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.buildings.close', { button: { html: icon('#iD-icon-close', 'inline') } })\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickHouse() {\n if (!_houseID) return chapter.restart();\n\n context.enter(modeBrowse(context));\n context.history().reset('hasHouse');\n var zoom = context.map().zoom();\n if (zoom < 20) {\n zoom = 20;\n }\n context.map().centerZoomEase(house, zoom, 500);\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== _houseID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'orthogonalize').node();\n if (!node) return;\n continueTo(clickSquare);\n }, 50); // after menu visible\n });\n\n context.map().on('move.intro drawn.intro', function() {\n var rightclickString = helpHtml('intro.buildings.' + (context.lastPointerType() === 'mouse' ? 'rightclick_building' : 'edit_menu_building_touch'));\n revealHouse(house, rightclickString, { duration: 0 });\n });\n\n context.history().on('change.intro', function() {\n continueTo(rightClickHouse);\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function clickSquare() {\n if (!_houseID) return chapter.restart();\n var entity = context.hasEntity(_houseID);\n if (!entity) return continueTo(rightClickHouse);\n\n var node = selectMenuItem(context, 'orthogonalize').node();\n if (!node) { return continueTo(rightClickHouse); }\n\n var wasChanged = false;\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.square_building'),\n { padding: 50 }\n );\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'browse') {\n continueTo(rightClickHouse);\n } else if (mode.id === 'move' || mode.id === 'rotate') {\n continueTo(retryClickSquare);\n }\n });\n\n context.map().on('move.intro', function() {\n var node = selectMenuItem(context, 'orthogonalize').node();\n if (!wasChanged && !node) { return continueTo(rightClickHouse); }\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.square_building'),\n { duration: 0, padding: 50 }\n );\n });\n\n context.history().on('change.intro', function() {\n wasChanged = true;\n context.history().on('change.intro', null);\n\n // Something changed. Wait for transition to complete and check undo annotation.\n timeout(function() {\n if (context.history().undoAnnotation() === t('operations.orthogonalize.annotation.feature', { n: 1 })) {\n continueTo(doneSquare);\n } else {\n continueTo(retryClickSquare);\n }\n }, 500); // after transitioned actions\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retryClickSquare() {\n context.enter(modeBrowse(context));\n\n revealHouse(house, helpHtml('intro.buildings.retry_square'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(rightClickHouse); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function doneSquare() {\n context.history().checkpoint('doneSquare');\n\n revealHouse(house, helpHtml('intro.buildings.done_square'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(addTank); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function addTank() {\n context.enter(modeBrowse(context));\n context.history().reset('doneSquare');\n _tankID = null;\n\n var msec = transitionTime(tank, context.map().center());\n if (msec) { reveal(null, null, { duration: 0 }); }\n context.map().centerZoomEase(tank, 19.5, msec);\n\n timeout(function() {\n reveal('button.add-area',\n helpHtml('intro.buildings.add_tank')\n );\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'add-area') return;\n continueTo(startTank);\n });\n }, msec + 100);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function startTank() {\n if (context.mode().id !== 'add-area') {\n return continueTo(addTank);\n }\n\n _tankID = null;\n\n timeout(function() {\n var startString = helpHtml('intro.buildings.start_tank') +\n helpHtml('intro.buildings.tank_edge_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap'));\n revealTank(tank, startString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealTank(tank, startString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'draw-area') return chapter.restart();\n continueTo(continueTank);\n });\n\n }, 550); // after easing\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function continueTank() {\n if (context.mode().id !== 'draw-area') {\n return continueTo(addTank);\n }\n\n _tankID = null;\n\n var continueString = helpHtml('intro.buildings.continue_tank') + '{br}' +\n helpHtml('intro.areas.finish_area_' + (context.lastPointerType() === 'mouse' ? 'click' : 'tap')) +\n helpHtml('intro.buildings.finish_tank');\n\n revealTank(tank, continueString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealTank(tank, continueString, { duration: 0 });\n });\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'draw-area') {\n return;\n } else if (mode.id === 'select') {\n _tankID = context.selectedIDs()[0];\n return continueTo(searchPresetTank);\n } else {\n return continueTo(addTank);\n }\n });\n\n function continueTo(nextStep) {\n context.map().on('move.intro drawn.intro', null);\n context.on('enter.intro', null);\n nextStep();\n }\n }\n\n\n function searchPresetTank() {\n if (!_tankID || !context.hasEntity(_tankID)) {\n return addTank();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _tankID) {\n context.enter(modeSelect(context, [_tankID]));\n }\n\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n timeout(function() {\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.buildings.search_tank', { preset: tankPreset.name() })\n );\n }, 400); // after preset list pane visible..\n\n context.on('enter.intro', function(mode) {\n if (!_tankID || !context.hasEntity(_tankID)) {\n return continueTo(addTank);\n }\n\n var ids = context.selectedIDs();\n if (mode.id !== 'select' || !ids.length || ids[0] !== _tankID) {\n // keep the user's area selected..\n context.enter(modeSelect(context, [_tankID]));\n\n // reset pane, in case user somehow happened to change it..\n context.container().select('.inspector-wrap .panewrap').style('right', '-100%');\n // disallow scrolling\n context.container().select('.inspector-wrap').on('wheel.intro', eventCancel);\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', null)\n .on('keyup.intro', checkPresetSearch);\n\n reveal('.preset-search-input',\n helpHtml('intro.buildings.search_tank', { preset: tankPreset.name() })\n );\n\n context.history().on('change.intro', null);\n }\n });\n\n function checkPresetSearch() {\n var first = context.container().select('.preset-list-item:first-child');\n\n if (first.classed('preset-man_made-storage_tank')) {\n reveal(first.select('.preset-list-button').node(),\n helpHtml('intro.buildings.choose_tank', { preset: tankPreset.name() }),\n { duration: 300 }\n );\n\n context.container().select('.preset-search-input')\n .on('keydown.intro', eventCancel, true)\n .on('keyup.intro', null);\n\n context.history().on('change.intro', function() {\n continueTo(closeEditorTank);\n });\n }\n }\n\n function continueTo(nextStep) {\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.on('enter.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n nextStep();\n }\n }\n\n\n function closeEditorTank() {\n if (!_tankID || !context.hasEntity(_tankID)) {\n return addTank();\n }\n var ids = context.selectedIDs();\n if (context.mode().id !== 'select' || !ids.length || ids[0] !== _tankID) {\n context.enter(modeSelect(context, [_tankID]));\n }\n\n context.history().checkpoint('hasTank');\n\n context.on('exit.intro', function() {\n continueTo(rightClickTank);\n });\n\n timeout(function() {\n reveal('.entity-editor-pane',\n helpHtml('intro.buildings.close', { button: { html: icon('#iD-icon-close', 'inline') } })\n );\n }, 500);\n\n function continueTo(nextStep) {\n context.on('exit.intro', null);\n nextStep();\n }\n }\n\n\n function rightClickTank() {\n if (!_tankID) return continueTo(addTank);\n\n context.enter(modeBrowse(context));\n context.history().reset('hasTank');\n context.map().centerEase(tank, 500);\n\n timeout(function() {\n context.on('enter.intro', function(mode) {\n if (mode.id !== 'select') return;\n var ids = context.selectedIDs();\n if (ids.length !== 1 || ids[0] !== _tankID) return;\n\n timeout(function() {\n var node = selectMenuItem(context, 'circularize').node();\n if (!node) return;\n continueTo(clickCircle);\n }, 50); // after menu visible\n });\n\n var rightclickString = helpHtml('intro.buildings.' + (context.lastPointerType() === 'mouse' ? 'rightclick_tank' : 'edit_menu_tank_touch'));\n\n revealTank(tank, rightclickString);\n\n context.map().on('move.intro drawn.intro', function() {\n revealTank(tank, rightclickString, { duration: 0 });\n });\n\n context.history().on('change.intro', function() {\n continueTo(rightClickTank);\n });\n\n }, 600);\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function clickCircle() {\n if (!_tankID) return chapter.restart();\n var entity = context.hasEntity(_tankID);\n if (!entity) return continueTo(rightClickTank);\n\n var node = selectMenuItem(context, 'circularize').node();\n if (!node) { return continueTo(rightClickTank); }\n\n var wasChanged = false;\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.circle_tank'),\n { padding: 50 }\n );\n\n context.on('enter.intro', function(mode) {\n if (mode.id === 'browse') {\n continueTo(rightClickTank);\n } else if (mode.id === 'move' || mode.id === 'rotate') {\n continueTo(retryClickCircle);\n }\n });\n\n context.map().on('move.intro', function() {\n var node = selectMenuItem(context, 'circularize').node();\n if (!wasChanged && !node) { return continueTo(rightClickTank); }\n\n reveal('.edit-menu',\n helpHtml('intro.buildings.circle_tank'),\n { duration: 0, padding: 50 }\n );\n });\n\n context.history().on('change.intro', function() {\n wasChanged = true;\n context.history().on('change.intro', null);\n\n // Something changed. Wait for transition to complete and check undo annotation.\n timeout(function() {\n if (context.history().undoAnnotation() === t('operations.circularize.annotation.feature', { n: 1 })) {\n continueTo(play);\n } else {\n continueTo(retryClickCircle);\n }\n }, 500); // after transitioned actions\n });\n\n function continueTo(nextStep) {\n context.on('enter.intro', null);\n context.map().on('move.intro', null);\n context.history().on('change.intro', null);\n nextStep();\n }\n }\n\n\n function retryClickCircle() {\n context.enter(modeBrowse(context));\n\n revealTank(tank, helpHtml('intro.buildings.retry_circle'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { continueTo(rightClickTank); }\n });\n\n function continueTo(nextStep) {\n nextStep();\n }\n }\n\n\n function play() {\n dispatch.call('done');\n reveal('.ideditor',\n helpHtml('intro.buildings.play', { next: t('intro.startediting.title') }), {\n tooltipBox: '.intro-nav-wrap .chapter-startEditing',\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { reveal('.ideditor'); }\n }\n );\n }\n\n\n chapter.enter = function() {\n addHouse();\n };\n\n\n chapter.exit = function() {\n timeouts.forEach(window.clearTimeout);\n context.on('enter.intro exit.intro', null);\n context.map().on('move.intro drawn.intro', null);\n context.history().on('change.intro', null);\n context.container().select('.inspector-wrap').on('wheel.intro', null);\n context.container().select('.preset-search-input').on('keydown.intro keyup.intro', null);\n context.container().select('.more-fields .combobox-input').on('click.intro', null);\n };\n\n\n chapter.restart = function() {\n chapter.exit();\n chapter.enter();\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { helpHtml } from './helper';\nimport { uiModal } from '../modal';\nimport { utilRebind } from '../../util/rebind';\n\n\nexport function uiIntroStartEditing(context, reveal) {\n var dispatch = d3_dispatch('done', 'startEditing');\n var modalSelection = d3_select(null);\n\n\n var chapter = {\n title: 'intro.startediting.title'\n };\n\n function showHelp() {\n reveal('.map-control.help-control',\n helpHtml('intro.startediting.help'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { shortcuts(); }\n }\n );\n }\n\n function shortcuts() {\n reveal('.map-control.help-control',\n helpHtml('intro.startediting.shortcuts'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { showSave(); }\n }\n );\n }\n\n function showSave() {\n context.container().selectAll('.shaded').remove(); // in case user opened keyboard shortcuts\n reveal('.top-toolbar button.save',\n helpHtml('intro.startediting.save'), {\n buttonText: t.html('intro.ok'),\n buttonCallback: function() { showStart(); }\n }\n );\n }\n\n function showStart() {\n context.container().selectAll('.shaded').remove(); // in case user opened keyboard shortcuts\n\n modalSelection = uiModal(context.container());\n\n modalSelection.select('.modal')\n .attr('class', 'modal-splash modal');\n\n modalSelection.selectAll('.close').remove();\n\n var startbutton = modalSelection.select('.content')\n .attr('class', 'fillL')\n .append('button')\n .attr('class', 'modal-section huge-modal-button')\n .on('click', function() {\n modalSelection.remove();\n });\n\n startbutton\n .append('svg')\n .attr('class', 'illustration')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n startbutton\n .append('h2')\n .call(t.append('intro.startediting.start'));\n\n dispatch.call('startEditing');\n }\n\n\n chapter.enter = function() {\n showHelp();\n };\n\n\n chapter.exit = function() {\n modalSelection.remove();\n context.container().selectAll('.shaded').remove(); // in case user opened keyboard shortcuts\n };\n\n\n return utilRebind(chapter, dispatch, 'on');\n}\n", "import { t, localizer } from '../../core/localizer';\nimport { localize } from './helper';\n\nimport { prefs } from '../../core/preferences';\nimport { fileFetcher } from '../../core/file_fetcher';\nimport { coreGraph } from '../../core/graph';\nimport { modeBrowse } from '../../modes/browse';\nimport { osmEntity } from '../../osm/entity';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCurtain } from '../curtain';\nimport { utilArrayDifference, utilArrayUniq } from '../../util';\n\nimport { uiIntroWelcome } from './welcome';\nimport { uiIntroNavigation } from './navigation';\nimport { uiIntroPoint } from './point';\nimport { uiIntroArea } from './area';\nimport { uiIntroLine } from './line';\nimport { uiIntroBuilding } from './building';\nimport { uiIntroStartEditing } from './start_editing';\n\n\nconst chapterUi = {\n welcome: uiIntroWelcome,\n navigation: uiIntroNavigation,\n point: uiIntroPoint,\n area: uiIntroArea,\n line: uiIntroLine,\n building: uiIntroBuilding,\n startEditing: uiIntroStartEditing\n};\n\nconst chapterFlow = [\n 'welcome',\n 'navigation',\n 'point',\n 'area',\n 'line',\n 'building',\n 'startEditing'\n];\n\n\nexport function uiIntro(context) {\n const INTRO_IMAGERY = 'Bing';\n let _introGraph = {};\n let _currChapter;\n\n\n function intro(selection) {\n fileFetcher.get('intro_graph')\n .then(dataIntroGraph => {\n // create entities for intro graph and localize names\n for (let id in dataIntroGraph) {\n if (!_introGraph[id]) {\n _introGraph[id] = osmEntity(localize(dataIntroGraph[id]));\n }\n }\n selection.call(startIntro);\n })\n .catch(function() { /* ignore */ });\n }\n\n\n function startIntro(selection) {\n context.enter(modeBrowse(context));\n\n // Save current map state\n let osm = context.connection();\n let history = context.history().toJSON();\n let hash = window.location.hash;\n let center = context.map().center();\n let zoom = context.map().zoom();\n let background = context.background().baseLayerSource();\n let overlays = context.background().overlayLayerSources();\n let opacity = context.container().selectAll('.main-map .layer-background').style('opacity');\n let caches = osm && osm.caches();\n let baseEntities = context.history().graph().base().entities;\n\n // Show sidebar and disable the sidebar resizing button\n // (this needs to be before `context.inIntro(true)`)\n context.ui().sidebar.expand();\n context.container().selectAll('button.sidebar-toggle').classed('disabled', true);\n\n // Block saving\n context.inIntro(true);\n\n // Load semi-real data used in intro\n if (osm) { osm.toggle(false).reset(); }\n context.history().reset();\n context.history().merge(Object.values(coreGraph().load(_introGraph).entities));\n context.history().checkpoint('initial');\n\n // Setup imagery\n let imagery = context.background().findSource(INTRO_IMAGERY);\n if (imagery) {\n context.background().baseLayerSource(imagery);\n } else {\n context.background().bing();\n }\n overlays.forEach(d => context.background().toggleOverlayLayer(d));\n\n // Setup data layers (only OSM)\n let layers = context.layers();\n layers.all().forEach(item => {\n // if the layer has the function `enabled`\n if (typeof item.layer.enabled === 'function') {\n item.layer.enabled(item.id === 'osm');\n }\n });\n\n\n context.container().selectAll('.main-map .layer-background').style('opacity', 1);\n\n let curtain = uiCurtain(context.container().node());\n selection.call(curtain);\n\n // Store that the user started the walkthrough..\n prefs('walkthrough_started', 'yes');\n\n // Restore previous walkthrough progress..\n let storedProgress = prefs('walkthrough_progress') || '';\n let progress = storedProgress.split(';').filter(Boolean);\n\n let chapters = chapterFlow.map((chapter, i) => {\n let s = chapterUi[chapter](context, curtain.reveal)\n .on('done', () => {\n\n buttons\n .filter(d => d.title === s.title)\n .classed('finished', true);\n\n if (i < chapterFlow.length - 1) {\n const next = chapterFlow[i + 1];\n context.container().select(`button.chapter-${next}`)\n .classed('next', true);\n }\n\n // Store walkthrough progress..\n progress.push(chapter);\n prefs('walkthrough_progress', utilArrayUniq(progress).join(';'));\n });\n return s;\n });\n\n chapters[chapters.length - 1].on('startEditing', () => {\n // Store walkthrough progress..\n progress.push('startEditing');\n prefs('walkthrough_progress', utilArrayUniq(progress).join(';'));\n\n // Store if walkthrough is completed..\n let incomplete = utilArrayDifference(chapterFlow, progress);\n if (!incomplete.length) {\n prefs('walkthrough_completed', 'yes');\n }\n\n curtain.remove();\n navwrap.remove();\n context.container().selectAll('.main-map .layer-background').style('opacity', opacity);\n context.container().selectAll('button.sidebar-toggle').classed('disabled', false);\n if (osm) { osm.toggle(true).reset().caches(caches); }\n context.history().reset().merge(Object.values(baseEntities));\n context.background().baseLayerSource(background);\n overlays.forEach(d => context.background().toggleOverlayLayer(d));\n if (history) { context.history().fromJSON(history, false); }\n context.map().centerZoom(center, zoom);\n window.history.replaceState(null, '', hash);\n context.inIntro(false);\n });\n\n let navwrap = selection\n .append('div')\n .attr('class', 'intro-nav-wrap fillD');\n\n navwrap\n .append('svg')\n .attr('class', 'intro-nav-wrap-logo')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n let buttonwrap = navwrap\n .append('div')\n .attr('class', 'joined')\n .selectAll('button.chapter');\n\n let buttons = buttonwrap\n .data(chapters)\n .enter()\n .append('button')\n .attr('class', (d, i) => `chapter chapter-${chapterFlow[i]}`)\n .on('click', enterChapter);\n\n buttons\n .append('span')\n .html(d => t.html(d.title));\n\n buttons\n .append('span')\n .attr('class', 'status')\n .call(svgIcon((localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'));\n\n enterChapter(null, chapters[0]);\n\n\n function enterChapter(d3_event, newChapter) {\n if (_currChapter) { _currChapter.exit(); }\n context.enter(modeBrowse(context));\n\n _currChapter = newChapter;\n _currChapter.enter();\n\n buttons\n .classed('next', false)\n .classed('active', d => d.title === _currChapter.title);\n }\n }\n\n\n return intro;\n}\n", "export { uiIntro } from './intro';\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../core/preferences';\nimport { svgIcon } from '../svg/icon';\nimport { t } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\n\n\nexport function uiIssuesInfo(context) {\n\n var warningsItem = {\n id: 'warnings',\n count: 0,\n iconID: 'iD-icon-alert',\n descriptionID: 'issues.warnings_and_errors'\n };\n\n var resolvedItem = {\n id: 'resolved',\n count: 0,\n iconID: 'iD-icon-apply',\n descriptionID: 'issues.user_resolved_issues'\n };\n\n function update(selection) {\n\n var shownItems = [];\n\n var liveIssues = context.validator().getIssues({\n what: prefs('validate-what') || 'edited',\n where: prefs('validate-where') || 'all'\n }).filter(issue => issue.severity !== 'suggestion');\n\n if (liveIssues.length) {\n warningsItem.count = liveIssues.length;\n shownItems.push(warningsItem);\n }\n\n if (prefs('validate-what') === 'all') {\n var resolvedIssues = context.validator().getResolvedIssues();\n if (resolvedIssues.length) {\n resolvedItem.count = resolvedIssues.length;\n shownItems.push(resolvedItem);\n }\n }\n\n var chips = selection.selectAll('.chip')\n .data(shownItems, function(d) {\n return d.id;\n });\n\n chips.exit().remove();\n\n var enter = chips.enter()\n .append('a')\n .attr('class', function(d) {\n return 'chip ' + d.id + '-count';\n })\n .attr('href', '#')\n .each(function(d) {\n\n var chipSelection = d3_select(this);\n\n var tooltipBehavior = uiTooltip()\n .placement('top')\n .title(() => t.append(d.descriptionID));\n\n chipSelection\n .call(tooltipBehavior)\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n\n tooltipBehavior.hide(d3_select(this));\n // open the Issues pane\n context.ui().togglePanes(context.container().select('.map-panes .issues-pane'));\n });\n\n chipSelection.call(svgIcon('#' + d.iconID));\n\n });\n\n enter.append('span')\n .attr('class', 'count');\n\n enter.merge(chips)\n .selectAll('span.count')\n .text(function(d) {\n return d.count.toString();\n });\n }\n\n\n return function(selection) {\n update(selection);\n\n context.validator().on('validated.infobox', function() {\n update(selection);\n });\n };\n}\n", "/**\n * IntervalTasksQueue\n * Enabled task execution under interval limit\n */\nexport class IntervalTasksQueue {\n readonly intervalInMs: number;\n pendingHandles: ReturnType[];\n time: number;\n\n /**\n * Interval in milliseconds inside which only 1 task can execute.\n * e.g. if interval is 200ms, and 5 async tasks are unqueued,\n * they will complete in ~1s if not cleared\n * @param {number} intervalInMs\n */\n constructor(intervalInMs: number) {\n this.intervalInMs = intervalInMs;\n this.pendingHandles = [];\n this.time = 0;\n }\n\n enqueue(task: () => void) {\n let taskTimeout = this.time;\n this.time += this.intervalInMs;\n this.pendingHandles.push(setTimeout(() => {\n this.time -= this.intervalInMs;\n task();\n }, taskTimeout));\n }\n\n clear() {\n this.pendingHandles.forEach((timeoutHandle) => {\n clearTimeout(timeoutHandle);\n });\n this.pendingHandles = [];\n this.time = 0;\n }\n}\n", "import { geoArea as d3_geoArea, geoMercatorRaw as d3_geoMercatorRaw } from 'd3-geo';\nimport { json as d3_json } from 'd3-fetch';\n\nimport { t, localizer } from '../core/localizer';\nimport { geoExtent, geoSphericalDistance } from '../geo';\nimport { utilQsString, utilStringQs } from '../util';\nimport { utilAesDecrypt } from '../util/aes';\nimport { IntervalTasksQueue } from '../util/IntervalTasksQueue';\nimport { localeDateString } from '../util/date';\n\nvar isRetina = window.devicePixelRatio && window.devicePixelRatio >= 2;\n\n// listen for DPI change, e.g. when dragging a browser window from a retina to non-retina screen\nwindow.matchMedia?.(`\n (-webkit-min-device-pixel-ratio: 2), /* Safari */\n (min-resolution: 2dppx), /* standard */\n (min-resolution: 192dpi) /* fallback */\n `).addListener(function() {\n\n isRetina = window.devicePixelRatio && window.devicePixelRatio >= 2;\n});\n\n\nfunction vintageRange(vintage) {\n var s;\n if (vintage.start || vintage.end) {\n s = (vintage.start || '?');\n if (vintage.start !== vintage.end) {\n s += ' - ' + (vintage.end || '?');\n }\n }\n return s;\n}\n\n\nexport function rendererBackgroundSource(data) {\n var source = Object.assign({}, data); // shallow copy\n var _offset = [0, 0];\n var _name = source.name;\n var _description = source.description;\n var _best = !!source.best;\n var _template = source.encrypted ? utilAesDecrypt(source.template) : source.template;\n\n source.tileSize = data.tileSize || 256;\n source.zoomExtent = data.zoomExtent || [0, 22];\n source.overzoom = data.overzoom !== false;\n\n source.offset = function(val) {\n if (!arguments.length) return _offset;\n _offset = val;\n return source;\n };\n\n\n source.nudge = function(val, zoomlevel) {\n _offset[0] += val[0] / Math.pow(2, zoomlevel);\n _offset[1] += val[1] / Math.pow(2, zoomlevel);\n return source;\n };\n\n\n source.name = function() {\n var id_safe = source.id.replace(/\\./g, '');\n return t('imagery.' + id_safe + '.name', { default: _name });\n };\n\n\n source.label = function() {\n var id_safe = source.id.replace(/\\./g, '');\n return t.append('imagery.' + id_safe + '.name', { default: _name });\n };\n\n\n source.hasDescription = function() {\n var id_safe = source.id.replace(/\\./g, '');\n var descriptionText = localizer.tInfo('imagery.' + id_safe + '.description', { default: escape(_description) }).texts.join('');\n return !!descriptionText;\n };\n\n\n source.description = function() {\n var id_safe = source.id.replace(/\\./g, '');\n return t.append('imagery.' + id_safe + '.description', { default: _description });\n };\n\n\n source.best = function() {\n return _best;\n };\n\n\n source.area = function() {\n if (!data.polygon) return Number.MAX_VALUE; // worldwide\n var area = d3_geoArea({ type: 'MultiPolygon', coordinates: [ data.polygon ] });\n return isNaN(area) ? 0 : area;\n };\n\n\n source.imageryUsed = function() {\n return _name || source.id;\n };\n\n\n source.template = function(val) {\n if (!arguments.length) return _template;\n if (source.id === 'custom' || source.id === 'Bing') {\n _template = val;\n }\n return source;\n };\n\n\n source.url = function(coord) {\n var result = _template.replace(/#[\\s\\S]*/u, ''); // strip hash part of URL\n if (result === '') return result; // source 'none'\n\n\n // Guess a type based on the tokens present in the template\n // (This is for 'custom' source, where we don't know)\n if (!source.type || source.id === 'custom') {\n if (/SERVICE=WMS|\\{(proj|wkid|bbox)\\}/.test(result)) {\n source.type = 'wms';\n source.projection = 'EPSG:3857'; // guess\n } else if (/\\{(x|y)\\}/.test(result)) {\n source.type = 'tms';\n } else if (/\\{u\\}/.test(result)) {\n source.type = 'bing';\n }\n }\n\n\n if (source.type === 'wms') {\n var tileToProjectedCoords = (function(x, y, z) {\n var zoomSize = Math.pow(2, z);\n var lon = x / zoomSize * Math.PI * 2 - Math.PI;\n var lat = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / zoomSize)));\n\n switch (source.projection) {\n case 'EPSG:4326':\n return {\n x: lon * 180 / Math.PI,\n y: lat * 180 / Math.PI\n };\n default: // EPSG:3857 and synonyms\n var mercCoords = d3_geoMercatorRaw(lon, lat);\n return {\n x: 20037508.34 / Math.PI * mercCoords[0],\n y: 20037508.34 / Math.PI * mercCoords[1]\n };\n }\n });\n\n var tileSize = source.tileSize;\n var projection = source.projection;\n var minXmaxY = tileToProjectedCoords(coord[0], coord[1], coord[2]);\n var maxXminY = tileToProjectedCoords(coord[0]+1, coord[1]+1, coord[2]);\n\n result = result.replace(/\\{(\\w+)\\}/g, function (token, key) {\n switch (key) {\n case 'width':\n case 'height':\n return tileSize;\n case 'proj':\n return projection;\n case 'wkid':\n return projection.replace(/^EPSG:/, '');\n case 'bbox':\n // WMS 1.3 flips x/y for some coordinate systems including EPSG:4326 - #7557\n if (projection === 'EPSG:4326' &&\n // The CRS parameter implies version 1.3 (prior versions use SRS)\n /VERSION=1.3|CRS={proj}/.test(source.template().toUpperCase())) {\n return maxXminY.y + ',' + minXmaxY.x + ',' + minXmaxY.y + ',' + maxXminY.x;\n } else {\n return minXmaxY.x + ',' + maxXminY.y + ',' + maxXminY.x + ',' + minXmaxY.y;\n }\n case 'w':\n return minXmaxY.x;\n case 's':\n return maxXminY.y;\n case 'n':\n return maxXminY.x;\n case 'e':\n return minXmaxY.y;\n default:\n return token;\n }\n });\n\n } else if (source.type === 'tms') {\n result = result\n .replace('{x}', coord[0])\n .replace('{y}', coord[1])\n // TMS-flipped y coordinate\n .replace(/\\{[t-]y\\}/, Math.pow(2, coord[2]) - coord[1] - 1)\n .replace(/\\{z(oom)?\\}/, coord[2])\n // only fetch retina tiles for retina screens\n .replace(/\\{@2x\\}|\\{r\\}/, isRetina ? '@2x' : '');\n\n } else if (source.type === 'bing') {\n result = result\n .replace('{u}', function() {\n var u = '';\n for (var zoom = coord[2]; zoom > 0; zoom--) {\n var b = 0;\n var mask = 1 << (zoom - 1);\n if ((coord[0] & mask) !== 0) b++;\n if ((coord[1] & mask) !== 0) b += 2;\n u += b.toString();\n }\n return u;\n });\n }\n\n // these apply to any type..\n result = result.replace(/\\{switch:([^}]+)\\}/, function(s, r) {\n var subdomains = r.split(',');\n return subdomains[(coord[0] + coord[1]) % subdomains.length];\n });\n\n\n return result;\n };\n\n\n source.validZoom = function(z, underzoom) {\n if (underzoom === undefined) underzoom = 0;\n return source.zoomExtent[0] - underzoom <= z &&\n (source.overzoom || source.zoomExtent[1] > z);\n };\n\n\n source.isLocatorOverlay = function() {\n return source.id === 'mapbox_locator_overlay';\n };\n\n\n /* hides a source from the list, but leaves it available for use */\n source.isHidden = function() {\n return false; // currently there are no hidden layers\n };\n\n\n source.copyrightNotices = function() {};\n\n\n source.getMetadata = function(center, tileCoord, callback) {\n var vintage = {\n start: localeDateString(source.startDate),\n end: localeDateString(source.endDate)\n };\n vintage.range = vintageRange(vintage);\n\n var metadata = { vintage: vintage };\n callback(null, metadata);\n };\n\n\n return source;\n}\n\n\nrendererBackgroundSource.Bing = function(data, dispatch) {\n // https://docs.microsoft.com/en-us/bingmaps/rest-services/imagery/get-imagery-metadata\n // https://docs.microsoft.com/en-us/bingmaps/rest-services/directly-accessing-the-bing-maps-tiles\n\n //fallback url template\n data.template = 'https://ecn.t{switch:0,1,2,3}.tiles.virtualearth.net/tiles/a{u}.jpeg?g=1&pr=odbl&n=z';\n\n var bing = rendererBackgroundSource(data);\n var key = utilAesDecrypt('5c875730b09c6b422433e807e1ff060b6536c791dbfffcffc4c6b18a1bdba1f14593d151adb50e19e1be1ab19aef813bf135d0f103475e5c724dec94389e45d0');\n /*\n missing tile image strictness param (n=)\n \u2022\tn=f -> (Fail) returns a 404\n \u2022\tn=z -> (Empty) returns a 200 with 0 bytes (no content)\n \u2022\tn=t -> (Transparent) returns a 200 with a transparent (png) tile\n */\n const strictParam = 'n';\n\n var url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/AerialOSM?include=ImageryProviders&uriScheme=https&key=' + key;\n var cache = {};\n var inflight = {};\n var providers = [];\n var taskQueue = new IntervalTasksQueue(250);\n var metadataLastZoom = -1;\n\n d3_json(url)\n .then(function(json) {\n let imageryResource = json.resourceSets[0].resources[0];\n\n //retrieve and prepare up to date imagery template\n let template = imageryResource.imageUrl; //https://ecn.{subdomain}.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=10339\n let subDomains = imageryResource.imageUrlSubdomains; //[\"t0, t1, t2, t3\"]\n let subDomainNumbers = subDomains.map((subDomain) => {\n return subDomain.substring(1);\n } ).join(',');\n\n template = template.replace('{subdomain}', `t{switch:${subDomainNumbers}}`).replace('{quadkey}', '{u}');\n if (!new URLSearchParams(template).has(strictParam)){\n template += `&${strictParam}=z`;\n }\n bing.template(template);\n\n providers = imageryResource.imageryProviders.map(function(provider) {\n return {\n attribution: provider.attribution,\n areas: provider.coverageAreas.map(function(area) {\n return {\n zoom: [area.zoomMin, area.zoomMax],\n extent: geoExtent([area.bbox[1], area.bbox[0]], [area.bbox[3], area.bbox[2]])\n };\n })\n };\n });\n dispatch.call('change');\n })\n .catch(function() {\n /* ignore */\n });\n\n\n bing.copyrightNotices = function(zoom, extent) {\n zoom = Math.min(zoom, 21);\n return providers.filter(function(provider) {\n return provider.areas.some(function(area) {\n return extent.intersects(area.extent) &&\n area.zoom[0] <= zoom &&\n area.zoom[1] >= zoom;\n });\n }).map(function(provider) {\n return provider.attribution;\n }).join(', ');\n };\n\n\n bing.getMetadata = function(center, tileCoord, callback) {\n var tileID = tileCoord.slice(0, 3).join('/');\n var zoom = Math.min(tileCoord[2], 21);\n var centerPoint = center[1] + ',' + center[0]; // lat,lng\n var url = 'https://dev.virtualearth.net/REST/v1/Imagery/BasicMetadata/AerialOSM/' + centerPoint +\n '?zl=' + zoom + '&key=' + key;\n\n if (inflight[tileID]) return;\n\n if (!cache[tileID]) {\n cache[tileID] = {};\n }\n if (cache[tileID] && cache[tileID].metadata) {\n return callback(null, cache[tileID].metadata);\n }\n\n inflight[tileID] = true;\n\n if (metadataLastZoom !== tileCoord[2]){\n metadataLastZoom = tileCoord[2];\n taskQueue.clear();\n }\n\n taskQueue.enqueue(() => {\n d3_json(url)\n .then(function (result) {\n delete inflight[tileID];\n if (!result) {\n throw new Error('Unknown Error');\n }\n var vintage = {\n start: localeDateString(result.resourceSets[0].resources[0].vintageStart),\n end: localeDateString(result.resourceSets[0].resources[0].vintageEnd)\n };\n vintage.range = vintageRange(vintage);\n\n var metadata = { vintage: vintage };\n cache[tileID].metadata = metadata;\n if (callback) callback(null, metadata);\n })\n .catch(function (err) {\n delete inflight[tileID];\n if (callback) callback(err.message);\n });\n });\n };\n\n\n bing.terms_url = 'https://blog.openstreetmap.org/2010/11/30/microsoft-imagery-details';\n\n\n return bing;\n};\n\n\n\nrendererBackgroundSource.Esri = function(data) {\n // in addition to using the tilemap at zoom level 20, overzoom real tiles - #4327 (deprecated technique, but it works)\n if (data.template.match(/blankTile/) === null) {\n data.template = data.template + '?blankTile=false';\n }\n\n var esri = rendererBackgroundSource(data);\n var cache = {};\n var inflight = {};\n var _prevCenter;\n\n // use a tilemap service to set maximum zoom for esri tiles dynamically\n // https://developers.arcgis.com/documentation/tiled-elevation-service/\n esri.fetchTilemap = function(center) {\n // skip if we have already fetched a tilemap within 5km\n if (_prevCenter && geoSphericalDistance(center, _prevCenter) < 5000) return;\n _prevCenter = center;\n\n // tiles are available globally to zoom level 19, afterward they may or may not be present\n var z = 20;\n\n // first generate a random url using the template\n var dummyUrl = esri.url([1,2,3]);\n\n // calculate url z/y/x from the lat/long of the center of the map\n var x = (Math.floor((center[0] + 180) / 360 * Math.pow(2, z)));\n var y = (Math.floor((1 - Math.log(Math.tan(center[1] * Math.PI / 180) + 1 / Math.cos(center[1] * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, z)));\n\n // fetch an 8x8 grid to leverage cache\n var tilemapUrl = dummyUrl.replace(/tile\\/[0-9]+\\/[0-9]+\\/[0-9]+\\?blankTile=false/, 'tilemap') + '/' + z + '/' + y + '/' + x + '/8/8';\n\n // make the request and introspect the response from the tilemap server\n d3_json(tilemapUrl)\n .then(function(tilemap) {\n if (!tilemap) {\n throw new Error('Unknown Error');\n }\n var hasTiles = true;\n for (var i = 0; i < tilemap.data.length; i++) {\n // 0 means an individual tile in the grid doesn't exist\n if (!tilemap.data[i]) {\n hasTiles = false;\n break;\n }\n }\n\n // if any tiles are missing at level 20 we restrict maxZoom to 19\n esri.zoomExtent[1] = (hasTiles ? 22 : 19);\n })\n .catch(function() {\n /* ignore */\n });\n };\n\n\n esri.getMetadata = function(center, tileCoord, callback) {\n let mapServerUrl = esri.metadata;\n if (esri.id === 'EsriWorldImagery') {\n mapServerUrl = 'https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer';\n }\n if (!mapServerUrl) {\n // rest endpoint is not available for ESRI's \"clarity\" imagery\n return callback(null, {});\n }\n var tileID = tileCoord.slice(0, 3).join('/');\n var zoom = Math.min(tileCoord[2], esri.zoomExtent[1]);\n var centerPoint = center[0] + ',' + center[1]; // long, lat (as it should be)\n var unknown = t('info_panels.background.unknown');\n var vintage = {};\n var metadata = {};\n\n if (inflight[tileID]) return;\n\n // build up query using the layer appropriate to the current zoom\n var url = mapServerUrl + '/4/query';\n url += '?returnGeometry=false&geometry=' + centerPoint + '&inSR=4326&geometryType=esriGeometryPoint&outFields=*&f=json';\n\n if (!cache[tileID]) {\n cache[tileID] = {};\n }\n if (cache[tileID] && cache[tileID].metadata) {\n return callback(null, cache[tileID].metadata);\n }\n\n inflight[tileID] = true;\n d3_json(url)\n .then(function(result) {\n delete inflight[tileID];\n\n result = result.features.map(f => f.attributes)\n .filter(a => a.MinMapLevel <= zoom && a.MaxMapLevel >= zoom)[0];\n\n if (!result) {\n throw new Error('Unknown Error');\n } else if (result.features && result.features.length < 1) {\n throw new Error('No Results');\n } else if (result.error && result.error.message) {\n throw new Error(result.error.message);\n }\n\n // pass through the discrete capture date from metadata\n var captureDate = localeDateString(result.SRC_DATE2);\n vintage = {\n start: captureDate,\n end: captureDate,\n range: captureDate\n };\n metadata = {\n vintage: vintage,\n source: clean(result.NICE_NAME),\n description: clean(result.NICE_DESC),\n resolution: clean(+Number(result.SRC_RES).toFixed(4)),\n accuracy: clean(+Number(result.SRC_ACC).toFixed(4))\n };\n\n // append units - meters\n if (isFinite(metadata.resolution)) {\n metadata.resolution += ' m';\n }\n if (isFinite(metadata.accuracy)) {\n metadata.accuracy += ' m';\n }\n\n cache[tileID].metadata = metadata;\n if (callback) callback(null, metadata);\n })\n .catch(function(err) {\n delete inflight[tileID];\n if (callback) callback(err.message);\n });\n\n\n function clean(val) {\n return String(val).trim() || unknown;\n }\n };\n\n return esri;\n};\n\n\nrendererBackgroundSource.None = function() {\n var source = rendererBackgroundSource({ id: 'none', template: '' });\n\n\n source.name = function() {\n return t('background.none');\n };\n\n\n source.label = function() {\n return t.append('background.none');\n };\n\n\n source.imageryUsed = function() {\n return null;\n };\n\n\n source.area = function() {\n return -1; // sources in background pane are sorted by area\n };\n\n\n return source;\n};\n\n\nrendererBackgroundSource.Custom = function(template) {\n var source = rendererBackgroundSource({ id: 'custom', template: template });\n\n\n source.name = function() {\n return t('background.custom');\n };\n\n source.label = function() {\n return t.append('background.custom');\n };\n\n\n source.imageryUsed = function() {\n // sanitize personal connection tokens - #6801\n var cleaned = source.template();\n\n // from query string parameters\n if (cleaned.indexOf('?') !== -1) {\n var parts = cleaned.split('?', 2);\n var qs = utilStringQs(parts[1]);\n\n ['access_token', 'connectId', 'token', 'Signature'].forEach(function(param) {\n if (qs[param]) {\n qs[param] = '{apikey}';\n }\n });\n cleaned = parts[0] + '?' + utilQsString(qs, true); // true = soft encode\n }\n\n // from wms/wmts api path parameters\n cleaned = cleaned\n .replace(/token\\/(\\w+)/, 'token/{apikey}')\n .replace(/key=(\\w+)/, 'key={apikey}');\n return 'Custom (' + cleaned + ' )';\n };\n\n\n source.area = function() {\n return -2; // sources in background pane are sorted by area\n };\n\n\n return source;\n};\n", "import { feature, point, lineString, isObject } from \"@turf/helpers\";\nimport {\n Point,\n LineString,\n Polygon,\n MultiLineString,\n MultiPolygon,\n FeatureCollection,\n Feature,\n Geometry,\n GeometryObject,\n GeometryCollection,\n GeoJsonProperties,\n BBox,\n GeoJsonTypes,\n} from \"geojson\";\nimport { AllGeoJSON, Lines, Id } from \"@turf/helpers\";\n\n/**\n * Callback for coordEach\n *\n * @callback coordEachCallback\n * @param {number[]} currentCoord The current coordinate being processed.\n * @param {number} coordIndex The current index of the coordinate being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over coordinates in any GeoJSON object, similar to Array.forEach()\n *\n * @function\n * @param {AllGeoJSON} geojson any GeoJSON object\n * @param {coordEachCallback} callback a method that takes (currentCoord, coordIndex, featureIndex, multiFeatureIndex)\n * @param {boolean} [excludeWrapCoord=false] whether or not to include the final coordinate of LinearRings that wraps the ring in its iteration.\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {\"foo\": \"bar\"}),\n * turf.point([36, 53], {\"hello\": \"world\"})\n * ]);\n *\n * turf.coordEach(features, function (currentCoord, coordIndex, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=currentCoord\n * //=coordIndex\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * });\n */\nfunction coordEach(\n geojson: AllGeoJSON,\n callback: (\n currentCoord: number[],\n coordIndex: number,\n featureIndex: number,\n multiFeatureIndex: number,\n geometryIndex: number\n ) => void,\n excludeWrapCoord?: boolean\n): void {\n // Handles null Geometry -- Skips this GeoJSON\n if (geojson === null) return;\n var j,\n k,\n l,\n geometry,\n stopG,\n coords,\n geometryMaybeCollection,\n wrapShrink = 0,\n coordIndex = 0,\n isGeometryCollection,\n type = geojson.type,\n isFeatureCollection = type === \"FeatureCollection\",\n isFeature = type === \"Feature\",\n // @ts-expect-error: Known type conflict\n stop = isFeatureCollection ? geojson.features.length : 1;\n\n // This logic may look a little weird. The reason why it is that way\n // is because it's trying to be fast. GeoJSON supports multiple kinds\n // of objects at its root: FeatureCollection, Features, Geometries.\n // This function has the responsibility of handling all of them, and that\n // means that some of the `for` loops you see below actually just don't apply\n // to certain inputs. For instance, if you give this just a\n // Point geometry, then both loops are short-circuited and all we do\n // is gradually rename the input until it's called 'geometry'.\n //\n // This also aims to allocate as few resources as possible: just a\n // few numbers and booleans, rather than any temporary arrays as would\n // be required with the normalization approach.\n for (var featureIndex = 0; featureIndex < stop; featureIndex++) {\n geometryMaybeCollection = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[featureIndex].geometry\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.geometry\n : geojson;\n isGeometryCollection = geometryMaybeCollection\n ? geometryMaybeCollection.type === \"GeometryCollection\"\n : false;\n stopG = isGeometryCollection\n ? geometryMaybeCollection.geometries.length\n : 1;\n\n for (var geomIndex = 0; geomIndex < stopG; geomIndex++) {\n var multiFeatureIndex = 0;\n var geometryIndex = 0;\n geometry = isGeometryCollection\n ? geometryMaybeCollection.geometries[geomIndex]\n : geometryMaybeCollection;\n\n // Handles null Geometry -- Skips this geometry\n if (geometry === null) continue;\n coords = geometry.coordinates;\n var geomType = geometry.type;\n\n wrapShrink =\n excludeWrapCoord &&\n (geomType === \"Polygon\" || geomType === \"MultiPolygon\")\n ? 1\n : 0;\n\n switch (geomType) {\n case null:\n break;\n case \"Point\":\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords,\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n multiFeatureIndex++;\n break;\n case \"LineString\":\n case \"MultiPoint\":\n for (j = 0; j < coords.length; j++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords[j],\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n if (geomType === \"MultiPoint\") multiFeatureIndex++;\n }\n if (geomType === \"LineString\") multiFeatureIndex++;\n break;\n case \"Polygon\":\n case \"MultiLineString\":\n for (j = 0; j < coords.length; j++) {\n for (k = 0; k < coords[j].length - wrapShrink; k++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords[j][k],\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n }\n if (geomType === \"MultiLineString\") multiFeatureIndex++;\n if (geomType === \"Polygon\") geometryIndex++;\n }\n if (geomType === \"Polygon\") multiFeatureIndex++;\n break;\n case \"MultiPolygon\":\n for (j = 0; j < coords.length; j++) {\n geometryIndex = 0;\n for (k = 0; k < coords[j].length; k++) {\n for (l = 0; l < coords[j][k].length - wrapShrink; l++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n coords[j][k][l],\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n coordIndex++;\n }\n geometryIndex++;\n }\n multiFeatureIndex++;\n }\n break;\n case \"GeometryCollection\":\n for (j = 0; j < geometry.geometries.length; j++)\n if (\n // @ts-expect-error: Known type conflict\n coordEach(geometry.geometries[j], callback, excludeWrapCoord) ===\n false\n )\n // @ts-expect-error: Known type conflict\n return false;\n break;\n default:\n throw new Error(\"Unknown Geometry Type\");\n }\n }\n }\n}\n\n/**\n * Callback for coordReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback coordReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {number[]} currentCoord The current coordinate being processed.\n * @param {number} coordIndex The current index of the coordinate being processed.\n * Starts at index 0, if an initialValue is provided, and at index 1 otherwise.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce coordinates in any GeoJSON object, similar to Array.reduce()\n *\n * @function\n * @param {AllGeoJSON} geojson any GeoJSON object\n * @param {coordReduceCallback} callback a method that takes (previousValue, currentCoord, coordIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @param {boolean} [excludeWrapCoord=false] whether or not to include the final coordinate of LinearRings that wraps the ring in its iteration.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {\"foo\": \"bar\"}),\n * turf.point([36, 53], {\"hello\": \"world\"})\n * ]);\n *\n * turf.coordReduce(features, function (previousValue, currentCoord, coordIndex, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=previousValue\n * //=currentCoord\n * //=coordIndex\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * return currentCoord;\n * });\n */\nfunction coordReduce(\n geojson: AllGeoJSON,\n callback: (\n previousValue: Reducer,\n currentCoord: number[],\n coordIndex: number,\n featureIndex: number,\n multiFeatureIndex: number,\n geometryIndex: number\n ) => Reducer,\n initialValue?: Reducer,\n excludeWrapCoord?: boolean\n): Reducer {\n var previousValue = initialValue;\n coordEach(\n geojson,\n function (\n currentCoord,\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) {\n if (coordIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentCoord;\n else\n previousValue = callback(\n // @ts-expect-error: Known type conflict\n previousValue,\n currentCoord,\n coordIndex,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n );\n },\n excludeWrapCoord\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for propEach\n *\n * @callback propEachCallback\n * @param {GeoJsonProperties} currentProperties The current Properties being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over properties in any GeoJSON object, similar to Array.forEach()\n *\n * @function\n * @param {FeatureCollection|Feature} geojson any GeoJSON object\n * @param {propEachCallback} callback a method that takes (currentProperties, featureIndex)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.propEach(features, function (currentProperties, featureIndex) {\n * //=currentProperties\n * //=featureIndex\n * });\n */\nfunction propEach(\n geojson: Feature | FeatureCollection | Feature,\n callback: (currentProperties: Props, featureIndex: number) => void\n): void {\n var i;\n switch (geojson.type) {\n case \"FeatureCollection\":\n for (i = 0; i < geojson.features.length; i++) {\n // @ts-expect-error: Known type conflict\n if (callback(geojson.features[i].properties, i) === false) break;\n }\n break;\n case \"Feature\":\n // @ts-expect-error: Known type conflict\n callback(geojson.properties, 0);\n break;\n }\n}\n\n/**\n * Callback for propReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback propReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {GeoJsonProperties} currentProperties The current Properties being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce properties in any GeoJSON object into a single value,\n * similar to how Array.reduce works. However, in this case we lazily run\n * the reduction, so an array of all properties is unnecessary.\n *\n * @function\n * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON object\n * @param {propReduceCallback} callback a method that takes (previousValue, currentProperties, featureIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.propReduce(features, function (previousValue, currentProperties, featureIndex) {\n * //=previousValue\n * //=currentProperties\n * //=featureIndex\n * return currentProperties\n * });\n */\nfunction propReduce(\n geojson: Feature | FeatureCollection | Geometry,\n callback: (\n previousValue: Reducer,\n currentProperties: P,\n featureIndex: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n // @ts-expect-error: Known type conflict\n propEach(geojson, function (currentProperties, featureIndex) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentProperties;\n else\n // @ts-expect-error: Known type conflict\n previousValue = callback(previousValue, currentProperties, featureIndex);\n });\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for featureEach\n *\n * @callback featureEachCallback\n * @param {Feature} currentFeature The current Feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over features in any GeoJSON object, similar to\n * Array.forEach.\n *\n * @function\n * @param {FeatureCollection|Feature|Feature} geojson any GeoJSON object\n * @param {featureEachCallback} callback a method that takes (currentFeature, featureIndex)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.featureEach(features, function (currentFeature, featureIndex) {\n * //=currentFeature\n * //=featureIndex\n * });\n */\nfunction featureEach<\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | Feature,\n callback: (currentFeature: Feature, featureIndex: number) => void\n): void {\n if (geojson.type === \"Feature\") {\n // @ts-expect-error: Known type conflict\n callback(geojson, 0);\n } else if (geojson.type === \"FeatureCollection\") {\n for (var i = 0; i < geojson.features.length; i++) {\n // @ts-expect-error: Known type conflict\n if (callback(geojson.features[i], i) === false) break;\n }\n }\n}\n\n/**\n * Callback for featureReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback featureReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentFeature The current Feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce features in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|Feature} geojson any GeoJSON object\n * @param {featureReduceCallback} callback a method that takes (previousValue, currentFeature, featureIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {\"foo\": \"bar\"}),\n * turf.point([36, 53], {\"hello\": \"world\"})\n * ]);\n *\n * turf.featureReduce(features, function (previousValue, currentFeature, featureIndex) {\n * //=previousValue\n * //=currentFeature\n * //=featureIndex\n * return currentFeature\n * });\n */\nfunction featureReduce<\n Reducer,\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | Feature,\n callback: (\n previousValue: Reducer,\n currentFeature: Feature,\n featureIndex: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n featureEach(geojson, function (currentFeature, featureIndex) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentFeature;\n // @ts-expect-error: Known type conflict\n else previousValue = callback(previousValue, currentFeature, featureIndex);\n });\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Get all coordinates from any GeoJSON object.\n *\n * @function\n * @param {AllGeoJSON} geojson any GeoJSON object\n * @returns {Array>} coordinate position array\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * var coords = turf.coordAll(features);\n * //= [[26, 37], [36, 53]]\n */\nfunction coordAll(geojson: AllGeoJSON): number[][] {\n // @ts-expect-error: Known type conflict\n var coords = [];\n coordEach(geojson, function (coord) {\n coords.push(coord);\n });\n // @ts-expect-error: Known type conflict\n return coords;\n}\n\n/**\n * Callback for geomEach\n *\n * @callback geomEachCallback\n * @param {GeometryObject} currentGeometry The current Geometry being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {GeoJsonProperties} featureProperties The current Feature Properties being processed.\n * @param {BBox} featureBBox The current Feature BBox being processed.\n * @param {Id} featureId The current Feature Id being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over each geometry in any GeoJSON object, similar to Array.forEach()\n *\n * @function\n * @param {FeatureCollection|Feature|Geometry|GeometryObject|Feature} geojson any GeoJSON object\n * @param {geomEachCallback} callback a method that takes (currentGeometry, featureIndex, featureProperties, featureBBox, featureId)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.geomEach(features, function (currentGeometry, featureIndex, featureProperties, featureBBox, featureId) {\n * //=currentGeometry\n * //=featureIndex\n * //=featureProperties\n * //=featureBBox\n * //=featureId\n * });\n */\nfunction geomEach<\n G extends GeometryObject | null,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n currentGeometry: G,\n featureIndex: number,\n featureProperties: P,\n featureBBox: BBox,\n featureId: Id\n ) => void\n): void {\n var i,\n j,\n g,\n geometry,\n stopG,\n geometryMaybeCollection,\n isGeometryCollection,\n featureProperties,\n featureBBox,\n featureId,\n featureIndex = 0,\n // @ts-expect-error: Known type conflict\n isFeatureCollection = geojson.type === \"FeatureCollection\",\n // @ts-expect-error: Known type conflict\n isFeature = geojson.type === \"Feature\",\n // @ts-expect-error: Known type conflict\n stop = isFeatureCollection ? geojson.features.length : 1;\n\n // This logic may look a little weird. The reason why it is that way\n // is because it's trying to be fast. GeoJSON supports multiple kinds\n // of objects at its root: FeatureCollection, Features, Geometries.\n // This function has the responsibility of handling all of them, and that\n // means that some of the `for` loops you see below actually just don't apply\n // to certain inputs. For instance, if you give this just a\n // Point geometry, then both loops are short-circuited and all we do\n // is gradually rename the input until it's called 'geometry'.\n //\n // This also aims to allocate as few resources as possible: just a\n // few numbers and booleans, rather than any temporary arrays as would\n // be required with the normalization approach.\n for (i = 0; i < stop; i++) {\n geometryMaybeCollection = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].geometry\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.geometry\n : geojson;\n featureProperties = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].properties\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.properties\n : {};\n featureBBox = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].bbox\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.bbox\n : undefined;\n featureId = isFeatureCollection\n ? // @ts-expect-error: Known type conflict\n geojson.features[i].id\n : isFeature\n ? // @ts-expect-error: Known type conflict\n geojson.id\n : undefined;\n isGeometryCollection = geometryMaybeCollection\n ? geometryMaybeCollection.type === \"GeometryCollection\"\n : false;\n stopG = isGeometryCollection\n ? geometryMaybeCollection.geometries.length\n : 1;\n\n for (g = 0; g < stopG; g++) {\n geometry = isGeometryCollection\n ? geometryMaybeCollection.geometries[g]\n : geometryMaybeCollection;\n\n // Handle null Geometry\n if (geometry === null) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n // @ts-expect-error: Known type conflict\n null,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n continue;\n }\n switch (geometry.type) {\n case \"Point\":\n case \"LineString\":\n case \"MultiPoint\":\n case \"Polygon\":\n case \"MultiLineString\":\n case \"MultiPolygon\": {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n geometry,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n break;\n }\n case \"GeometryCollection\": {\n for (j = 0; j < geometry.geometries.length; j++) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n geometry.geometries[j],\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) === false\n )\n // @ts-expect-error: Known type conflict\n return false;\n }\n break;\n }\n default:\n throw new Error(\"Unknown Geometry Type\");\n }\n }\n // Only increase `featureIndex` per each feature\n featureIndex++;\n }\n}\n\n/**\n * Callback for geomReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback geomReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {GeometryObject} currentGeometry The current Geometry being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {GeoJsonProperties} featureProperties The current Feature Properties being processed.\n * @param {BBox} featureBBox The current Feature BBox being processed.\n * @param {Id} featureId The current Feature Id being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce geometry in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|GeometryObject|GeometryCollection|Feature} geojson any GeoJSON object\n * @param {geomReduceCallback} callback a method that takes (previousValue, currentGeometry, featureIndex, featureProperties, featureBBox, featureId)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.point([36, 53], {hello: 'world'})\n * ]);\n *\n * turf.geomReduce(features, function (previousValue, currentGeometry, featureIndex, featureProperties, featureBBox, featureId) {\n * //=previousValue\n * //=currentGeometry\n * //=featureIndex\n * //=featureProperties\n * //=featureBBox\n * //=featureId\n * return currentGeometry\n * });\n */\nfunction geomReduce<\n Reducer,\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n previousValue: Reducer,\n currentGeometry: G,\n featureIndex: number,\n featureProperties: P,\n featureBBox: BBox,\n featureId: Id\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n geomEach(\n geojson,\n function (\n currentGeometry,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n ) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentGeometry;\n else\n previousValue = callback(\n // @ts-expect-error: Known type conflict\n previousValue,\n currentGeometry,\n featureIndex,\n featureProperties,\n featureBBox,\n featureId\n );\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for flattenEach\n *\n * @callback flattenEachCallback\n * @param {Feature} currentFeature The current flattened feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over flattened features in any GeoJSON object, similar to\n * Array.forEach.\n *\n * @function\n * @param {FeatureCollection|Feature|GeometryObject|GeometryCollection|Feature} geojson any GeoJSON object\n * @param {flattenEachCallback} callback a method that takes (currentFeature, featureIndex, multiFeatureIndex)\n * @returns {void}\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.multiPoint([[40, 30], [36, 53]], {hello: 'world'})\n * ]);\n *\n * turf.flattenEach(features, function (currentFeature, featureIndex, multiFeatureIndex) {\n * //=currentFeature\n * //=featureIndex\n * //=multiFeatureIndex\n * });\n */\nfunction flattenEach<\n G extends GeometryObject = GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n currentFeature: Feature,\n featureIndex: number,\n multiFeatureIndex: number\n ) => void\n): void {\n geomEach(geojson, function (geometry, featureIndex, properties, bbox, id) {\n // Callback for single geometry\n var type = geometry === null ? null : geometry.type;\n switch (type) {\n case null:\n case \"Point\":\n case \"LineString\":\n case \"Polygon\":\n if (\n // @ts-expect-error: Known type conflict\n callback(\n feature(geometry, properties, { bbox: bbox, id: id }),\n featureIndex,\n 0\n ) === false\n )\n return false;\n return;\n }\n\n var geomType;\n\n // Callback for multi-geometry\n switch (type) {\n case \"MultiPoint\":\n geomType = \"Point\";\n break;\n case \"MultiLineString\":\n geomType = \"LineString\";\n break;\n case \"MultiPolygon\":\n geomType = \"Polygon\";\n break;\n }\n\n for (\n var multiFeatureIndex = 0;\n // @ts-expect-error: Known type conflict\n multiFeatureIndex < geometry.coordinates.length;\n multiFeatureIndex++\n ) {\n // @ts-expect-error: Known type conflict\n var coordinate = geometry.coordinates[multiFeatureIndex];\n var geom = {\n type: geomType,\n coordinates: coordinate,\n };\n if (\n // @ts-expect-error: Known type conflict\n callback(feature(geom, properties), featureIndex, multiFeatureIndex) ===\n false\n )\n return false;\n }\n });\n}\n\n/**\n * Callback for flattenReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback flattenReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentFeature The current Feature being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce flattened features in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|GeometryObject|GeometryCollection|Feature} geojson any GeoJSON object\n * @param {flattenReduceCallback} callback a method that takes (previousValue, currentFeature, featureIndex, multiFeatureIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var features = turf.featureCollection([\n * turf.point([26, 37], {foo: 'bar'}),\n * turf.multiPoint([[40, 30], [36, 53]], {hello: 'world'})\n * ]);\n *\n * turf.flattenReduce(features, function (previousValue, currentFeature, featureIndex, multiFeatureIndex) {\n * //=previousValue\n * //=currentFeature\n * //=featureIndex\n * //=multiFeatureIndex\n * return currentFeature\n * });\n */\nfunction flattenReduce<\n Reducer,\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | Feature\n | FeatureCollection\n | G\n | GeometryCollection\n | Feature,\n callback: (\n previousValue: Reducer,\n currentFeature: Feature,\n featureIndex: number,\n multiFeatureIndex: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n flattenEach(\n geojson,\n function (currentFeature, featureIndex, multiFeatureIndex) {\n if (\n featureIndex === 0 &&\n multiFeatureIndex === 0 &&\n initialValue === undefined\n )\n // @ts-expect-error: Known type conflict\n previousValue = currentFeature;\n else\n previousValue = callback(\n // @ts-expect-error: Known type conflict\n previousValue,\n currentFeature,\n featureIndex,\n multiFeatureIndex\n );\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for segmentEach\n *\n * @callback segmentEachCallback\n * @param {Feature} currentSegment The current Segment being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @param {number} segmentIndex The current index of the Segment being processed.\n * @returns {void}\n */\n\n/**\n * Iterate over 2-vertex line segment in any GeoJSON object, similar to Array.forEach()\n * (Multi)Point geometries do not contain segments therefore they are ignored during this operation.\n *\n * @param {AllGeoJSON} geojson any GeoJSON\n * @param {segmentEachCallback} callback a method that takes (currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex)\n * @returns {void}\n * @example\n * var polygon = turf.polygon([[[-50, 5], [-40, -10], [-50, -10], [-40, 5], [-50, 5]]]);\n *\n * // Iterate over GeoJSON by 2-vertex segments\n * turf.segmentEach(polygon, function (currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex) {\n * //=currentSegment\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * //=segmentIndex\n * });\n *\n * // Calculate the total number of segments\n * var total = 0;\n * turf.segmentEach(polygon, function () {\n * total++;\n * });\n */\nfunction segmentEach

    (\n geojson: AllGeoJSON,\n callback: (\n currentSegment?: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n segmentIndex?: number,\n geometryIndex?: number\n ) => void\n): void {\n flattenEach(geojson, function (feature, featureIndex, multiFeatureIndex) {\n var segmentIndex = 0;\n\n // Exclude null Geometries\n if (!feature.geometry) return;\n // (Multi)Point geometries do not contain segments therefore they are ignored during this operation.\n var type = feature.geometry.type;\n if (type === \"Point\" || type === \"MultiPoint\") return;\n\n // Generate 2-vertex line segments\n // @ts-expect-error: Known type conflict\n var previousCoords;\n var previousFeatureIndex = 0;\n var previousMultiIndex = 0;\n var prevGeomIndex = 0;\n if (\n // @ts-expect-error: Known type conflict\n coordEach(\n feature,\n function (\n currentCoord,\n coordIndex,\n featureIndexCoord,\n multiPartIndexCoord,\n geometryIndex\n ) {\n // Simulating a meta.coordReduce() since `reduce` operations cannot be stopped by returning `false`\n if (\n // @ts-expect-error: Known type conflict\n previousCoords === undefined ||\n featureIndex > previousFeatureIndex ||\n multiPartIndexCoord > previousMultiIndex ||\n geometryIndex > prevGeomIndex\n ) {\n previousCoords = currentCoord;\n previousFeatureIndex = featureIndex;\n previousMultiIndex = multiPartIndexCoord;\n prevGeomIndex = geometryIndex;\n segmentIndex = 0;\n return;\n }\n var currentSegment = lineString(\n // @ts-expect-error: Known type conflict\n [previousCoords, currentCoord],\n feature.properties\n );\n if (\n // @ts-expect-error: Known type conflict\n callback(\n // @ts-expect-error: Known type conflict\n currentSegment,\n featureIndex,\n multiFeatureIndex,\n geometryIndex,\n segmentIndex\n ) === false\n )\n return false;\n segmentIndex++;\n previousCoords = currentCoord;\n }\n ) === false\n )\n return false;\n });\n}\n\n/**\n * Callback for segmentReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback segmentReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentSegment The current Segment being processed.\n * @param {number} featureIndex The current index of the Feature being processed.\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed.\n * @param {number} geometryIndex The current index of the Geometry being processed.\n * @param {number} segmentIndex The current index of the Segment being processed.\n * @returns {Reducer}\n */\n\n/**\n * Reduce 2-vertex line segment in any GeoJSON object, similar to Array.reduce()\n * (Multi)Point geometries do not contain segments therefore they are ignored during this operation.\n *\n * @param {FeatureCollection|Feature|Geometry} geojson any GeoJSON\n * @param {segmentReduceCallback} callback a method that takes (previousValue, currentSegment, currentIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer}\n * @example\n * var polygon = turf.polygon([[[-50, 5], [-40, -10], [-50, -10], [-40, 5], [-50, 5]]]);\n *\n * // Iterate over GeoJSON by 2-vertex segments\n * turf.segmentReduce(polygon, function (previousSegment, currentSegment, featureIndex, multiFeatureIndex, geometryIndex, segmentIndex) {\n * //= previousSegment\n * //= currentSegment\n * //= featureIndex\n * //= multiFeatureIndex\n * //= geometryIndex\n * //= segmentIndex\n * return currentSegment\n * });\n *\n * // Calculate the total number of segments\n * var initialValue = 0\n * var total = turf.segmentReduce(polygon, function (previousValue) {\n * previousValue++;\n * return previousValue;\n * }, initialValue);\n */\nfunction segmentReduce<\n Reducer,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson:\n | FeatureCollection\n | Feature\n | Lines\n | Feature\n | GeometryCollection,\n callback: (\n previousValue?: Reducer,\n currentSegment?: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n segmentIndex?: number,\n geometryIndex?: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n var started = false;\n segmentEach(\n geojson,\n function (\n currentSegment,\n featureIndex,\n multiFeatureIndex,\n geometryIndex,\n segmentIndex\n ) {\n if (started === false && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentSegment;\n else\n previousValue = callback(\n previousValue,\n // @ts-expect-error: Known type conflict\n currentSegment,\n featureIndex,\n multiFeatureIndex,\n geometryIndex,\n segmentIndex\n );\n started = true;\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Callback for lineEach\n *\n * @callback lineEachCallback\n * @param {Feature} currentLine The current LineString|LinearRing being processed\n * @param {number} featureIndex The current index of the Feature being processed\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed\n * @param {number} geometryIndex The current index of the Geometry being processed\n * @returns {void}\n */\n\n/**\n * Iterate over line or ring coordinates in LineString, Polygon, MultiLineString, MultiPolygon Features or Geometries,\n * similar to Array.forEach.\n *\n * @function\n * @param {FeatureCollection|Feature|Lines|Feature|GeometryCollection} geojson object\n * @param {lineEachCallback} callback a method that takes (currentLine, featureIndex, multiFeatureIndex, geometryIndex)\n * @returns {void}\n * @example\n * var multiLine = turf.multiLineString([\n * [[26, 37], [35, 45]],\n * [[36, 53], [38, 50], [41, 55]]\n * ]);\n *\n * turf.lineEach(multiLine, function (currentLine, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=currentLine\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * });\n */\nfunction lineEach

    (\n geojson:\n | FeatureCollection\n | Feature\n | Lines\n | Feature\n | GeometryCollection,\n callback: (\n currentLine: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n geometryIndex?: number\n ) => void\n): void {\n // validation\n if (!geojson) throw new Error(\"geojson is required\");\n\n flattenEach(geojson, function (feature, featureIndex, multiFeatureIndex) {\n if (feature.geometry === null) return;\n var type = feature.geometry.type;\n var coords = feature.geometry.coordinates;\n switch (type) {\n case \"LineString\":\n // @ts-expect-error: Known type conflict\n if (callback(feature, featureIndex, multiFeatureIndex, 0, 0) === false)\n return false;\n break;\n case \"Polygon\":\n for (\n var geometryIndex = 0;\n geometryIndex < coords.length;\n geometryIndex++\n ) {\n if (\n // @ts-expect-error: Known type conflict\n callback(\n // @ts-expect-error: Known type conflict\n lineString(coords[geometryIndex], feature.properties),\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n ) === false\n )\n return false;\n }\n break;\n }\n });\n}\n\n/**\n * Callback for lineReduce\n *\n * The first time the callback function is called, the values provided as arguments depend\n * on whether the reduce method has an initialValue argument.\n *\n * If an initialValue is provided to the reduce method:\n * - The previousValue argument is initialValue.\n * - The currentValue argument is the value of the first element present in the array.\n *\n * If an initialValue is not provided:\n * - The previousValue argument is the value of the first element present in the array.\n * - The currentValue argument is the value of the second element present in the array.\n *\n * @callback lineReduceCallback\n * @param {Reducer} previousValue The accumulated value previously returned in the last invocation\n * of the callback, or initialValue, if supplied.\n * @param {Feature} currentLine The current LineString|LinearRing being processed.\n * @param {number} featureIndex The current index of the Feature being processed\n * @param {number} multiFeatureIndex The current index of the Multi-Feature being processed\n * @param {number} geometryIndex The current index of the Geometry being processed\n * @returns {Reducer}\n */\n\n/**\n * Reduce features in any GeoJSON object, similar to Array.reduce().\n *\n * @function\n * @param {FeatureCollection|Feature|Lines|Feature|GeometryCollection} geojson object\n * @param {Function} callback a method that takes (previousValue, currentLine, featureIndex, multiFeatureIndex, geometryIndex)\n * @param {Reducer} [initialValue] Value to use as the first argument to the first call of the callback.\n * @returns {Reducer} The value that results from the reduction.\n * @example\n * var multiPoly = turf.multiPolygon([\n * turf.polygon([[[12,48],[2,41],[24,38],[12,48]], [[9,44],[13,41],[13,45],[9,44]]]),\n * turf.polygon([[[5, 5], [0, 0], [2, 2], [4, 4], [5, 5]]])\n * ]);\n *\n * turf.lineReduce(multiPoly, function (previousValue, currentLine, featureIndex, multiFeatureIndex, geometryIndex) {\n * //=previousValue\n * //=currentLine\n * //=featureIndex\n * //=multiFeatureIndex\n * //=geometryIndex\n * return currentLine\n * });\n */\nfunction lineReduce(\n geojson:\n | FeatureCollection\n | Feature\n | Lines\n | Feature\n | GeometryCollection,\n callback: (\n previousValue?: Reducer,\n currentLine?: Feature,\n featureIndex?: number,\n multiFeatureIndex?: number,\n geometryIndex?: number\n ) => Reducer,\n initialValue?: Reducer\n): Reducer {\n var previousValue = initialValue;\n lineEach(\n geojson,\n function (currentLine, featureIndex, multiFeatureIndex, geometryIndex) {\n if (featureIndex === 0 && initialValue === undefined)\n // @ts-expect-error: Known type conflict\n previousValue = currentLine;\n else\n previousValue = callback(\n previousValue,\n currentLine,\n featureIndex,\n multiFeatureIndex,\n geometryIndex\n );\n }\n );\n // @ts-expect-error: Known type conflict\n return previousValue;\n}\n\n/**\n * Finds a particular 2-vertex LineString Segment from a GeoJSON using `@turf/meta` indexes.\n *\n * Negative indexes are permitted.\n * Point & MultiPoint will always return null.\n *\n * @param {FeatureCollection|Feature|Geometry} geojson Any GeoJSON Feature or Geometry\n * @param {Object} [options={}] Optional parameters\n * @param {number} [options.featureIndex=0] Feature Index\n * @param {number} [options.multiFeatureIndex=0] Multi-Feature Index\n * @param {number} [options.geometryIndex=0] Geometry Index\n * @param {number} [options.segmentIndex=0] Segment Index\n * @param {Object} [options.properties={}] Translate Properties to output LineString\n * @param {BBox} [options.bbox={}] Translate BBox to output LineString\n * @param {number|string} [options.id={}] Translate Id to output LineString\n * @returns {Feature} 2-vertex GeoJSON Feature LineString\n * @example\n * var multiLine = turf.multiLineString([\n * [[10, 10], [50, 30], [30, 40]],\n * [[-10, -10], [-50, -30], [-30, -40]]\n * ]);\n *\n * // First Segment (defaults are 0)\n * turf.findSegment(multiLine);\n * // => Feature>\n *\n * // First Segment of 2nd Multi Feature\n * turf.findSegment(multiLine, {multiFeatureIndex: 1});\n * // => Feature>\n *\n * // Last Segment of Last Multi Feature\n * turf.findSegment(multiLine, {multiFeatureIndex: -1, segmentIndex: -1});\n * // => Feature>\n */\nfunction findSegment<\n G extends LineString | MultiLineString | Polygon | MultiPolygon,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson: Feature | FeatureCollection | G,\n options?: {\n featureIndex?: number;\n multiFeatureIndex?: number;\n geometryIndex?: number;\n segmentIndex?: number;\n properties?: P;\n bbox?: BBox;\n id?: Id;\n }\n): Feature {\n // Optional Parameters\n options = options || {};\n if (!isObject(options)) throw new Error(\"options is invalid\");\n var featureIndex = options.featureIndex || 0;\n var multiFeatureIndex = options.multiFeatureIndex || 0;\n var geometryIndex = options.geometryIndex || 0;\n var segmentIndex = options.segmentIndex || 0;\n\n // Find FeatureIndex\n var properties = options.properties;\n var geometry;\n\n switch (geojson.type) {\n case \"FeatureCollection\":\n if (featureIndex < 0)\n featureIndex = geojson.features.length + featureIndex;\n properties = properties || geojson.features[featureIndex].properties;\n geometry = geojson.features[featureIndex].geometry;\n break;\n case \"Feature\":\n properties = properties || geojson.properties;\n geometry = geojson.geometry;\n break;\n case \"Point\" as GeoJsonTypes:\n case \"MultiPoint\" as GeoJsonTypes:\n // @ts-expect-error: Known type conflict\n return null;\n case \"LineString\":\n case \"Polygon\":\n case \"MultiLineString\":\n case \"MultiPolygon\":\n geometry = geojson;\n break;\n default:\n throw new Error(\"geojson is invalid\");\n }\n\n // Find SegmentIndex\n // @ts-expect-error: Known type conflict\n if (geometry === null) return null;\n var coords = geometry.coordinates;\n switch (geometry.type) {\n case \"Point\" as GeoJsonTypes:\n case \"MultiPoint\" as GeoJsonTypes:\n // @ts-expect-error: Known type conflict\n return null;\n case \"LineString\":\n if (segmentIndex < 0) segmentIndex = coords.length + segmentIndex - 1;\n return lineString(\n // @ts-expect-error: Known type conflict\n [coords[segmentIndex], coords[segmentIndex + 1]],\n properties,\n options\n );\n case \"Polygon\":\n if (geometryIndex < 0) geometryIndex = coords.length + geometryIndex;\n if (segmentIndex < 0)\n segmentIndex = coords[geometryIndex].length + segmentIndex - 1;\n return lineString(\n [\n // @ts-expect-error: Known type conflict\n coords[geometryIndex][segmentIndex],\n // @ts-expect-error: Known type conflict\n coords[geometryIndex][segmentIndex + 1],\n ],\n properties,\n options\n );\n case \"MultiLineString\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (segmentIndex < 0)\n segmentIndex = coords[multiFeatureIndex].length + segmentIndex - 1;\n return lineString(\n [\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][segmentIndex],\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][segmentIndex + 1],\n ],\n properties,\n options\n );\n case \"MultiPolygon\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (geometryIndex < 0)\n geometryIndex = coords[multiFeatureIndex].length + geometryIndex;\n if (segmentIndex < 0)\n segmentIndex =\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][geometryIndex].length - segmentIndex - 1;\n return lineString(\n [\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][geometryIndex][segmentIndex],\n // @ts-expect-error: Known type conflict\n coords[multiFeatureIndex][geometryIndex][segmentIndex + 1],\n ],\n properties,\n options\n );\n }\n throw new Error(\"geojson is invalid\");\n}\n\n/**\n * Finds a particular Point from a GeoJSON using `@turf/meta` indexes.\n *\n * Negative indexes are permitted.\n *\n * @param {FeatureCollection|Feature|Geometry} geojson Any GeoJSON Feature or Geometry\n * @param {Object} [options={}] Optional parameters\n * @param {number} [options.featureIndex=0] Feature Index\n * @param {number} [options.multiFeatureIndex=0] Multi-Feature Index\n * @param {number} [options.geometryIndex=0] Geometry Index\n * @param {number} [options.coordIndex=0] Coord Index\n * @param {Object} [options.properties={}] Translate Properties to output Point\n * @param {BBox} [options.bbox={}] Translate BBox to output Point\n * @param {number|string} [options.id={}] Translate Id to output Point\n * @returns {Feature} 2-vertex GeoJSON Feature Point\n * @example\n * var multiLine = turf.multiLineString([\n * [[10, 10], [50, 30], [30, 40]],\n * [[-10, -10], [-50, -30], [-30, -40]]\n * ]);\n *\n * // First Segment (defaults are 0)\n * turf.findPoint(multiLine);\n * // => Feature>\n *\n * // First Segment of the 2nd Multi-Feature\n * turf.findPoint(multiLine, {multiFeatureIndex: 1});\n * // => Feature>\n *\n * // Last Segment of last Multi-Feature\n * turf.findPoint(multiLine, {multiFeatureIndex: -1, coordIndex: -1});\n * // => Feature>\n */\nfunction findPoint<\n G extends GeometryObject,\n P extends GeoJsonProperties = GeoJsonProperties,\n>(\n geojson: Feature | FeatureCollection | G,\n options?: {\n featureIndex?: number;\n multiFeatureIndex?: number;\n geometryIndex?: number;\n coordIndex?: number;\n properties?: P;\n bbox?: BBox;\n id?: Id;\n }\n): Feature {\n // Optional Parameters\n options = options || {};\n if (!isObject(options)) throw new Error(\"options is invalid\");\n var featureIndex = options.featureIndex || 0;\n var multiFeatureIndex = options.multiFeatureIndex || 0;\n var geometryIndex = options.geometryIndex || 0;\n var coordIndex = options.coordIndex || 0;\n\n // Find FeatureIndex\n var properties = options.properties;\n var geometry;\n\n switch (geojson.type) {\n case \"FeatureCollection\":\n if (featureIndex < 0)\n featureIndex = geojson.features.length + featureIndex;\n properties = properties || geojson.features[featureIndex].properties;\n geometry = geojson.features[featureIndex].geometry;\n break;\n case \"Feature\":\n properties = properties || geojson.properties;\n geometry = geojson.geometry;\n break;\n case \"Point\":\n case \"MultiPoint\":\n // @ts-expect-error: Known type conflict\n return null;\n case \"LineString\":\n case \"Polygon\":\n case \"MultiLineString\":\n case \"MultiPolygon\":\n geometry = geojson;\n break;\n default:\n throw new Error(\"geojson is invalid\");\n }\n\n // Find Coord Index\n // @ts-expect-error: Known type conflict\n if (geometry === null) return null;\n // @ts-expect-error: Known type conflict\n var coords = geometry.coordinates;\n switch (geometry.type) {\n case \"Point\":\n return point(coords, properties, options);\n case \"MultiPoint\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n return point(coords[multiFeatureIndex], properties, options);\n case \"LineString\":\n if (coordIndex < 0) coordIndex = coords.length + coordIndex;\n return point(coords[coordIndex], properties, options);\n case \"Polygon\":\n if (geometryIndex < 0) geometryIndex = coords.length + geometryIndex;\n if (coordIndex < 0)\n coordIndex = coords[geometryIndex].length + coordIndex;\n return point(coords[geometryIndex][coordIndex], properties, options);\n case \"MultiLineString\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (coordIndex < 0)\n coordIndex = coords[multiFeatureIndex].length + coordIndex;\n return point(coords[multiFeatureIndex][coordIndex], properties, options);\n case \"MultiPolygon\":\n if (multiFeatureIndex < 0)\n multiFeatureIndex = coords.length + multiFeatureIndex;\n if (geometryIndex < 0)\n geometryIndex = coords[multiFeatureIndex].length + geometryIndex;\n if (coordIndex < 0)\n coordIndex =\n coords[multiFeatureIndex][geometryIndex].length - coordIndex;\n return point(\n coords[multiFeatureIndex][geometryIndex][coordIndex],\n properties,\n options\n );\n }\n throw new Error(\"geojson is invalid\");\n}\n\nexport {\n coordReduce,\n coordEach,\n propEach,\n propReduce,\n featureReduce,\n featureEach,\n coordAll,\n geomReduce,\n geomEach,\n flattenReduce,\n flattenEach,\n segmentReduce,\n segmentEach,\n lineReduce,\n lineEach,\n findSegment,\n findPoint,\n};\n", "import { BBox } from \"geojson\";\nimport { AllGeoJSON } from \"@turf/helpers\";\nimport { coordEach } from \"@turf/meta\";\n\n/**\n * Calculates the bounding box for any GeoJSON object, including FeatureCollection.\n * Uses geojson.bbox if available and options.recompute is not set.\n *\n * @function\n * @param {GeoJSON} geojson any GeoJSON object\n * @param {Object} [options={}] Optional parameters\n * @param {boolean} [options.recompute] Whether to ignore an existing bbox property on geojson\n * @returns {BBox} bbox extent in [minX, minY, maxX, maxY] order\n * @example\n * var line = turf.lineString([[-74, 40], [-78, 42], [-82, 35]]);\n * var bbox = turf.bbox(line);\n * var bboxPolygon = turf.bboxPolygon(bbox);\n *\n * //addToMap\n * var addToMap = [line, bboxPolygon]\n */\nfunction bbox(\n geojson: AllGeoJSON,\n options: {\n recompute?: boolean;\n } = {}\n): BBox {\n if (geojson.bbox != null && true !== options.recompute) {\n return geojson.bbox;\n }\n const result: BBox = [Infinity, Infinity, -Infinity, -Infinity];\n coordEach(geojson, (coord) => {\n if (result[0] > coord[0]) {\n result[0] = coord[0];\n }\n if (result[1] > coord[1]) {\n result[1] = coord[1];\n }\n if (result[2] < coord[0]) {\n result[2] = coord[0];\n }\n if (result[3] < coord[1]) {\n result[3] = coord[1];\n }\n });\n return result;\n}\n\nexport { bbox };\nexport default bbox;\n", "import { select as d3_select } from 'd3-selection';\nimport { t } from '../core/localizer';\n\nimport { geoScaleToZoom, geoVecLength } from '../geo';\nimport { utilPrefixCSSProperty, utilTiler } from '../util';\n\n\nexport function rendererTileLayer(context) {\n var transformProp = utilPrefixCSSProperty('Transform');\n var tiler = utilTiler();\n\n var _tileSize = 256;\n var _projection;\n var _cache = {};\n var _tileOrigin;\n var _zoom;\n var _source;\n var _underzoom = 0;\n\n function tileSizeAtZoom(d, z) {\n return (d.tileSize * Math.pow(2, z - d[2])) / d.tileSize;\n }\n\n\n function atZoom(t, distance) {\n var power = Math.pow(2, distance);\n return [\n Math.floor(t[0] * power),\n Math.floor(t[1] * power),\n t[2] + distance\n ];\n }\n\n\n function lookUp(d) {\n for (var up = -1; up > -d[2]; up--) {\n var tile = atZoom(d, up);\n if (_cache[_source.url(tile)] !== false) {\n return tile;\n }\n }\n }\n\n\n function uniqueBy(a, n) {\n var o = [];\n var seen = {};\n for (var i = 0; i < a.length; i++) {\n if (seen[a[i][n]] === undefined) {\n o.push(a[i]);\n seen[a[i][n]] = true;\n }\n }\n return o;\n }\n\n\n function addSource(d) {\n d.url = _source.url(d);\n d.tileSize = _tileSize;\n d.source = _source;\n return d;\n }\n\n\n // Update tiles based on current state of `projection`.\n function background(selection) {\n _zoom = geoScaleToZoom(_projection.scale(), _tileSize);\n\n var pixelOffset;\n if (_source) {\n pixelOffset = [\n _source.offset()[0] * Math.pow(2, _zoom),\n _source.offset()[1] * Math.pow(2, _zoom)\n ];\n } else {\n pixelOffset = [0, 0];\n }\n\n\n tiler\n .scale(_projection.scale() * 2 * Math.PI)\n .translate([\n _projection.translate()[0] + pixelOffset[0],\n _projection.translate()[1] + pixelOffset[1]\n ]);\n\n _tileOrigin = [\n _projection.scale() * Math.PI - _projection.translate()[0],\n _projection.scale() * Math.PI - _projection.translate()[1]\n ];\n\n render(selection);\n }\n\n\n // Derive the tiles onscreen, remove those offscreen and position them.\n // Important that this part not depend on `_projection` because it's\n // rendered when tiles load/error (see #644).\n function render(selection) {\n if (!_source) return;\n var requests = [];\n var showDebug = context.getDebug('tile') && !_source.overlay;\n\n if (_source.validZoom(_zoom, _underzoom)) {\n tiler.skipNullIsland(!!_source.overlay);\n\n tiler().forEach(function(d) {\n addSource(d);\n if (d.url === '') return;\n if (typeof d.url !== 'string') return; // Workaround for #2295\n requests.push(d);\n if (_cache[d.url] === false && lookUp(d)) {\n requests.push(addSource(lookUp(d)));\n }\n });\n\n requests = uniqueBy(requests, 'url').filter(function(r) {\n // don't re-request tiles which have failed in the past\n return _cache[r.url] !== false;\n });\n }\n\n function load(d3_event, d) {\n _cache[d.url] = true;\n d3_select(this)\n .on('error', null)\n .on('load', null);\n render(selection);\n }\n\n function error(d3_event, d) {\n _cache[d.url] = false;\n d3_select(this)\n .on('error', null)\n .on('load', null)\n .remove();\n render(selection);\n }\n\n function imageTransform(d) {\n var ts = d.tileSize * Math.pow(2, _zoom - d[2]);\n var scale = tileSizeAtZoom(d, _zoom);\n return 'translate(' +\n ((d[0] * ts + d.source.offset()[0] * Math.pow(2, _zoom)) * _tileSize / d.tileSize - _tileOrigin[0]\n ) + 'px,' +\n ((d[1] * ts + d.source.offset()[1] * Math.pow(2, _zoom)) * _tileSize / d.tileSize - _tileOrigin[1]\n ) + 'px) ' +\n 'scale(' + scale * _tileSize / d.tileSize + ',' + scale * _tileSize / d.tileSize + ')';\n }\n\n function tileCenter(d) {\n var ts = d.tileSize * Math.pow(2, _zoom - d[2]);\n return [\n ((d[0] * ts) - _tileOrigin[0] + (ts / 2)),\n ((d[1] * ts) - _tileOrigin[1] + (ts / 2))\n ];\n }\n\n function debugTransform(d) {\n var coord = tileCenter(d);\n return 'translate(' + coord[0] + 'px,' + coord[1] + 'px)';\n }\n\n\n // Pick a representative tile near the center of the viewport\n // (This is useful for sampling the imagery vintage)\n var dims = tiler.size();\n var mapCenter = [dims[0] / 2, dims[1] / 2];\n var minDist = Math.max(dims[0], dims[1]);\n var nearCenter;\n\n requests.forEach(function(d) {\n var c = tileCenter(d);\n var dist = geoVecLength(c, mapCenter);\n if (dist < minDist) {\n minDist = dist;\n nearCenter = d;\n }\n });\n\n\n var image = selection.selectAll('img')\n .data(requests, function(d) { return d.url; });\n\n image.exit()\n .style(transformProp, imageTransform)\n .classed('tile-removing', true)\n .classed('tile-center', false)\n .on('transitionend', function() {\n const tile = d3_select(this);\n if (tile.classed('tile-removing')) {\n tile.remove();\n }\n });\n\n image.enter()\n .append('img')\n .attr('class', 'tile')\n .attr('alt', '')\n .attr('draggable', 'false')\n .style('width', _tileSize + 'px')\n .style('height', _tileSize + 'px')\n .attr('src', function(d) { return d.url; })\n .on('error', error)\n .on('load', load)\n .merge(image)\n .style(transformProp, imageTransform)\n .classed('tile-debug', showDebug)\n .classed('tile-removing', false)\n .classed('tile-center', function(d) { return d === nearCenter; })\n .sort((a, b) => a[2] - b[2]);\n\n\n\n var debug = selection.selectAll('.tile-label-debug')\n .data(showDebug ? requests : [], function(d) { return d.url; });\n\n debug.exit()\n .remove();\n\n if (showDebug) {\n var debugEnter = debug.enter()\n .append('div')\n .attr('class', 'tile-label-debug');\n\n debugEnter\n .append('div')\n .attr('class', 'tile-label-debug-coord');\n\n debugEnter\n .append('div')\n .attr('class', 'tile-label-debug-vintage');\n\n debug = debug.merge(debugEnter);\n\n debug\n .style(transformProp, debugTransform);\n\n debug\n .selectAll('.tile-label-debug-coord')\n .text(function(d) { return d[2] + ' / ' + d[0] + ' / ' + d[1]; });\n\n debug\n .selectAll('.tile-label-debug-vintage')\n .each(function(d) {\n var span = d3_select(this);\n var center = context.projection.invert(tileCenter(d));\n _source.getMetadata(center, d, function(err, result) {\n if (result && result.vintage && result.vintage.range) {\n span.text(result.vintage.range);\n } else {\n span.text('');\n span.call(t.append('info_panels.background.vintage'));\n span.append('span').text(': ');\n span.call(t.append('info_panels.background.unknown'));\n }\n });\n });\n }\n\n }\n\n\n background.projection = function(val) {\n if (!arguments.length) return _projection;\n _projection = val;\n return background;\n };\n\n\n background.dimensions = function(val) {\n if (!arguments.length) return tiler.size();\n tiler.size(val);\n return background;\n };\n\n\n background.source = function(val) {\n if (!arguments.length) return _source;\n _source = val;\n _tileSize = _source.tileSize;\n _cache = {};\n tiler.tileSize(_source.tileSize).zoomExtent(_source.zoomExtent);\n return background;\n };\n\n\n background.underzoom = function(amount) {\n if (!arguments.length) return _underzoom;\n _underzoom = amount;\n return background;\n };\n\n\n return background;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate';\nimport { select as d3_select } from 'd3-selection';\nimport turf_bboxClip from '@turf/bbox-clip';\nimport turf_bbox from '@turf/bbox';\n\nimport whichPolygon from 'which-polygon';\n\nimport { prefs } from '../core/preferences';\nimport { fileFetcher } from '../core/file_fetcher';\nimport { geoMetersToOffset, geoOffsetToMeters, geoExtent } from '../geo';\nimport { rendererBackgroundSource } from './background_source';\nimport { rendererTileLayer } from './tile_layer';\nimport { utilQsString, utilStringQs } from '../util';\nimport { utilRebind } from '../util/rebind';\n\n\nlet _imageryIndex = null;\nlet _waybackIndex = null;\n\nexport function rendererBackground(context) {\n const dispatch = d3_dispatch('change');\n const baseLayer = rendererTileLayer(context).projection(context.projection);\n let _checkedBlocklists = [];\n let _isValid = true;\n let _overlayLayers = [];\n let _brightness = 1;\n let _contrast = 1;\n let _saturation = 1;\n let _sharpness = 1;\n\n\n function ensureImageryIndex() {\n return fileFetcher.get('wayback')\n .then(groups => {\n // eslint-disable-next-line no-warning-comments\n // TODO: Follow pagination via nextStart property.\n // Extracts the layer's date from the title.\n let extractDateFromTitle = title => {\n const dateComponents = title.match(/\\(Wayback (\\d{4})-(\\d\\d)-(\\d\\d)\\)/);\n if (!dateComponents) return;\n return new Date(Date.UTC(parseInt(dateComponents[1], 10),\n parseInt(dateComponents[2], 10) - 1,\n parseInt(dateComponents[3], 10)));\n };\n\n if (!_waybackIndex) {\n // Index the metadata MapServer URLs by the date of the World Imagery map.\n let metadataMapServersByDate = Object.fromEntries(groups.items\n .filter(item => item.type === 'Map Service')\n .map(item => {\n // Extract the layer's date from the title to avoid having to hit each MapServer right away.\n const date = extractDateFromTitle(item.title) || new Date(item.created);\n const dateString = date.toISOString().split('T')[0];\n return [dateString, item.url];\n }));\n\n _waybackIndex = groups.items\n .filter(item => item.type === 'WMTS')\n .map(item => {\n const date = extractDateFromTitle(item.title) || new Date(item.created);\n const dateString = date && date.toISOString().split('T')[0];\n\n // Convert the bounding box to a polygon.\n const bbox = {\n min_lon: item.extent[0][0],\n min_lat: item.extent[0][1],\n max_lon: item.extent[1][0],\n max_lat: item.extent[1][1],\n };\n const polygon = [[\n [bbox.min_lon, bbox.min_lat],\n [bbox.min_lon, bbox.max_lat],\n [bbox.max_lon, bbox.max_lat],\n [bbox.max_lon, bbox.min_lat],\n [bbox.min_lon, bbox.min_lat],\n ]];\n\n // Convert placeholder tokens in the URL template from Esri's format to OSM's.\n const template = item.url\n .replaceAll('{level}', '{zoom}')\n .replaceAll('{row}', '{y}')\n .replaceAll('{col}', '{x}');\n\n return {\n id: 'EsriWorldImagery_' + dateString,\n name: item.title,\n type: 'tms',\n template: template,\n metadata: metadataMapServersByDate[dateString],\n startDate: date.toISOString(),\n endDate: date.toISOString(),\n polygon: polygon,\n terms_text: item.accessInformation,\n description: item.snippet,\n // Match Esri World Imagery layer\n 'default': true,\n zoomExtent: [0, 22],\n terms_url: 'https://wiki.openstreetmap.org/wiki/Esri',\n icon: 'https://osmlab.github.io/editor-layer-index/sources/world/EsriImageryClarity.png',\n };\n });\n }\n return fileFetcher.get('imagery');\n })\n .catch(() => {\n return fileFetcher.get('imagery');\n })\n .then(sources => {\n if (_imageryIndex) return _imageryIndex;\n\n // Append Esri World Imagery Wayback sources.\n if (_waybackIndex) {\n sources.push(..._waybackIndex);\n }\n\n _imageryIndex = {\n imagery: sources,\n features: {}\n };\n\n // use which-polygon to support efficient index and querying for imagery\n const features = sources.map(source => {\n if (!source.polygon) return null;\n // workaround for editor-layer-index weirdness..\n // Add an extra array nest to each element in `source.polygon`\n // so the rings are not treated as a bunch of holes:\n // what we have: [ [[outer],[hole],[hole]] ]\n // what we want: [ [[outer]],[[outer]],[[outer]] ]\n const rings = source.polygon.map(ring => [ring]);\n\n const feature = {\n type: 'Feature',\n properties: { id: source.id },\n geometry: { type: 'MultiPolygon', coordinates: rings }\n };\n\n _imageryIndex.features[source.id] = feature;\n return feature;\n\n }).filter(Boolean);\n\n _imageryIndex.query = whichPolygon({ type: 'FeatureCollection', features: features });\n\n\n // Instantiate `rendererBackgroundSource` objects for each source\n _imageryIndex.backgrounds = sources.map(source => {\n if (source.type === 'bing') {\n return rendererBackgroundSource.Bing(source, dispatch);\n } else if (/^EsriWorldImagery/.test(source.id)) {\n return rendererBackgroundSource.Esri(source);\n } else {\n return rendererBackgroundSource(source);\n }\n });\n\n // Add 'None'\n _imageryIndex.backgrounds.unshift(rendererBackgroundSource.None());\n\n // Add 'Custom'\n let template = prefs('background-custom-template') || '';\n const custom = rendererBackgroundSource.Custom(template);\n _imageryIndex.backgrounds.unshift(custom);\n\n return _imageryIndex;\n });\n }\n\n\n function background(selection) {\n const currSource = baseLayer.source();\n\n // If we are displaying an Esri basemap at high zoom,\n // check its tilemap to see how high the zoom can go\n if (context.map().zoom() > 18) {\n if (currSource && /^EsriWorldImagery/.test(currSource.id)) {\n const center = context.map().center();\n currSource.fetchTilemap(center);\n }\n }\n\n // Is the imagery valid here? - #4827\n const sources = background.sources(context.map().extent());\n const wasValid = _isValid;\n _isValid = !!sources.filter(d => d === currSource).length;\n\n if (wasValid !== _isValid) { // change in valid status\n background.updateImagery();\n }\n\n\n let baseFilter = '';\n if (_brightness !== 1) {\n baseFilter += ` brightness(${_brightness})`;\n }\n if (_contrast !== 1) {\n baseFilter += ` contrast(${_contrast})`;\n }\n if (_saturation !== 1) {\n baseFilter += ` saturate(${_saturation})`;\n }\n if (_sharpness < 1) { // gaussian blur\n const blur = d3_interpolateNumber(0.5, 5)(1 - _sharpness);\n baseFilter += ` blur(${blur}px)`;\n }\n\n let base = selection.selectAll('.layer-background')\n .data([0]);\n\n base = base.enter()\n .insert('div', '.layer-data')\n .attr('class', 'layer layer-background')\n .merge(base);\n\n base.style('filter', baseFilter || null);\n\n\n let imagery = base.selectAll('.layer-imagery')\n .data([0]);\n\n imagery.enter()\n .append('div')\n .attr('class', 'layer layer-imagery')\n .merge(imagery)\n .call(baseLayer);\n\n\n let maskFilter = '';\n let mixBlendMode = '';\n if (_sharpness > 1) { // apply unsharp mask\n mixBlendMode = 'overlay';\n maskFilter = 'saturate(0) blur(3px) invert(1)';\n\n let contrast = _sharpness - 1;\n maskFilter += ` contrast(${contrast})`;\n\n let brightness = d3_interpolateNumber(1, 0.85)(_sharpness - 1);\n maskFilter += ` brightness(${brightness})`;\n }\n\n let mask = base.selectAll('.layer-unsharp-mask')\n .data(_sharpness > 1 ? [0] : []);\n\n mask.exit()\n .remove();\n\n mask.enter()\n .append('div')\n .attr('class', 'layer layer-mask layer-unsharp-mask')\n .merge(mask)\n .call(baseLayer)\n .style('filter', maskFilter || null)\n .style('mix-blend-mode', mixBlendMode || null);\n\n\n let overlays = selection.selectAll('.layer-overlay')\n .data(_overlayLayers, d => d.source().name());\n\n overlays.exit()\n .remove();\n\n overlays.enter()\n .insert('div', '.layer-data')\n .attr('class', 'layer layer-overlay')\n .merge(overlays)\n .each((layer, i, nodes) => d3_select(nodes[i]).call(layer));\n }\n\n\n background.updateImagery = function() {\n let currSource = baseLayer.source();\n if (context.inIntro() || !currSource) return;\n\n let o = _overlayLayers\n .filter(d => !d.source().isLocatorOverlay() && !d.source().isHidden())\n .map(d => d.source().id)\n .join(',');\n\n const meters = geoOffsetToMeters(currSource.offset());\n const EPSILON = 0.01;\n const x = +meters[0].toFixed(2);\n const y = +meters[1].toFixed(2);\n let hash = utilStringQs(window.location.hash);\n\n let id = currSource.id;\n if (id === 'custom') {\n id = `custom:${currSource.template()}`;\n }\n\n if (id) {\n hash.background = id;\n } else {\n delete hash.background;\n }\n\n if (o) {\n hash.overlays = o;\n } else {\n delete hash.overlays;\n }\n\n if (Math.abs(x) > EPSILON || Math.abs(y) > EPSILON) {\n hash.offset = `${x},${y}`;\n } else {\n delete hash.offset;\n }\n\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n\n let imageryUsed = [];\n let photoOverlaysUsed = [];\n\n const currUsed = currSource.imageryUsed();\n if (currUsed && _isValid) {\n imageryUsed.push(currUsed);\n }\n\n _overlayLayers\n .filter(d => !d.source().isLocatorOverlay() && !d.source().isHidden())\n .forEach(d => imageryUsed.push(d.source().imageryUsed()));\n\n const dataLayer = context.layers().layer('data');\n if (dataLayer && dataLayer.enabled() && dataLayer.hasData()) {\n imageryUsed.push(dataLayer.getSrc());\n }\n\n const photoOverlayLayers = {\n streetside: 'Bing Streetside',\n mapillary: 'Mapillary Images',\n 'mapillary-map-features': 'Mapillary Map Features',\n 'mapillary-signs': 'Mapillary Signs',\n kartaview: 'KartaView Images',\n vegbilder: 'Norwegian Road Administration Images',\n mapilio: 'Mapilio Images',\n panoramax: 'Panoramax Images'\n };\n\n for (let layerID in photoOverlayLayers) {\n const layer = context.layers().layer(layerID);\n if (layer && layer.enabled()) {\n photoOverlaysUsed.push(layerID);\n imageryUsed.push(photoOverlayLayers[layerID]);\n }\n }\n\n context.history().imageryUsed(imageryUsed);\n context.history().photoOverlaysUsed(photoOverlaysUsed);\n };\n\n\n background.sources = (extent, zoom, includeCurrent) => {\n if (!_imageryIndex) return []; // called before init()?\n\n let visible = {};\n (_imageryIndex.query.bbox(extent.rectangle(), true) || [])\n .forEach(d => visible[d.id] = true);\n\n const currSource = baseLayer.source();\n\n // Recheck blocked sources only if we detect new blocklists pulled from the OSM API.\n const osm = context.connection();\n const blocklists = (osm && osm.imageryBlocklists()) || [];\n const blocklistChanged = (blocklists.length !== _checkedBlocklists.length) ||\n blocklists.some((regex, index) => String(regex) !== _checkedBlocklists[index]);\n\n if (blocklistChanged) {\n _imageryIndex.backgrounds.forEach(source => {\n source.isBlocked = blocklists.some(regex => regex.test(source.template()));\n });\n _checkedBlocklists = blocklists.map(regex => String(regex));\n }\n\n return _imageryIndex.backgrounds.filter(source => {\n if (includeCurrent && currSource === source) return true; // optionally always include the current imagery\n if (source.isBlocked) return false; // even bundled sources may be blocked - #7905\n if (!source.polygon) return true; // always include imagery with worldwide coverage\n if (zoom && zoom < 6) return false; // optionally exclude local imagery at low zooms\n return visible[source.id]; // include imagery visible in given extent\n });\n };\n\n\n background.dimensions = (val) => {\n if (!val) return;\n baseLayer.dimensions(val);\n _overlayLayers.forEach(layer => layer.dimensions(val));\n };\n\n\n background.baseLayerSource = function(d) {\n if (!arguments.length) return baseLayer.source();\n\n // test source against OSM imagery blocklists..\n const osm = context.connection();\n if (!osm) return background;\n\n const blocklists = osm.imageryBlocklists();\n const template = d.template();\n let fail = false;\n let tested = 0;\n let regex;\n\n for (let i = 0; i < blocklists.length; i++) {\n regex = blocklists[i];\n fail = regex.test(template);\n tested++;\n if (fail) break;\n }\n\n // ensure at least one test was run.\n if (!tested) {\n regex = /.*\\.google(apis)?\\..*\\/(vt|kh)[\\?\\/].*([xyz]=.*){3}.*/;\n fail = regex.test(template);\n }\n\n baseLayer.source(!fail ? d : background.findSource('none'));\n dispatch.call('change');\n background.updateImagery();\n return background;\n };\n\n\n background.findSource = (id) => {\n if (!id || !_imageryIndex) return null; // called before init()?\n return _imageryIndex.backgrounds.find(d => d.id && d.id === id);\n };\n\n\n background.bing = () => {\n background.baseLayerSource(background.findSource('Bing'));\n };\n\n\n background.showsLayer = (d) => {\n const currSource = baseLayer.source();\n if (!d || !currSource) return false;\n return d.id === currSource.id || _overlayLayers.some(layer => d.id === layer.source().id);\n };\n\n\n background.overlayLayerSources = () => {\n return _overlayLayers.map(layer => layer.source());\n };\n\n\n background.toggleOverlayLayer = (d) => {\n let layer;\n for (let i = 0; i < _overlayLayers.length; i++) {\n layer = _overlayLayers[i];\n if (layer.source() === d) {\n _overlayLayers.splice(i, 1);\n dispatch.call('change');\n background.updateImagery();\n return;\n }\n }\n\n layer = rendererTileLayer(context)\n .source(d)\n .projection(context.projection)\n .dimensions(baseLayer.dimensions()\n );\n\n _overlayLayers.push(layer);\n dispatch.call('change');\n background.updateImagery();\n };\n\n\n background.nudge = (d, zoom) => {\n const currSource = baseLayer.source();\n if (currSource) {\n currSource.nudge(d, zoom);\n dispatch.call('change');\n background.updateImagery();\n }\n return background;\n };\n\n\n background.offset = function(d) {\n const currSource = baseLayer.source();\n if (!arguments.length) {\n return (currSource && currSource.offset()) || [0, 0];\n }\n if (currSource) {\n currSource.offset(d);\n dispatch.call('change');\n background.updateImagery();\n }\n return background;\n };\n\n\n background.brightness = function(d) {\n if (!arguments.length) return _brightness;\n _brightness = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n\n background.contrast = function(d) {\n if (!arguments.length) return _contrast;\n _contrast = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n\n background.saturation = function(d) {\n if (!arguments.length) return _saturation;\n _saturation = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n\n background.sharpness = function(d) {\n if (!arguments.length) return _sharpness;\n _sharpness = d;\n if (context.mode()) dispatch.call('change');\n return background;\n };\n\n let _loadPromise;\n\n background.ensureLoaded = () => {\n if (_loadPromise) return _loadPromise;\n\n return _loadPromise = ensureImageryIndex();\n };\n\n background.init = () => {\n const loadPromise = background.ensureLoaded();\n\n const hash = utilStringQs(window.location.hash);\n const requestedBackground = hash.background || hash.layer;\n const lastUsedBackground = prefs('background-last-used');\n\n return loadPromise.then(imageryIndex => {\n const extent = context.map().extent();\n const validBackgrounds = background.sources(extent).filter(d => d.id !== 'none' && d.id !== 'custom');\n const first = validBackgrounds.length && validBackgrounds[0];\n const isLastUsedValid = !!validBackgrounds.find(d => d.id && d.id === lastUsedBackground);\n\n let best;\n if (!requestedBackground && extent) {\n const viewArea = extent.area();\n best = validBackgrounds.find(s => {\n if (!s.best() || s.overlay) return false;\n let bbox = turf_bbox(turf_bboxClip(\n { type: 'MultiPolygon', coordinates: [ s.polygon || [extent.polygon()] ] },\n extent.rectangle()));\n let area = geoExtent(bbox.slice(0,2), bbox.slice(2,4)).area();\n return area / viewArea > 0.5; // min visible size: 50% of viewport area\n });\n }\n\n // Decide which background layer to display\n if (requestedBackground && requestedBackground.indexOf('custom:') === 0) {\n const template = requestedBackground.replace(/^custom:/, '');\n const custom = background.findSource('custom');\n background.baseLayerSource(custom.template(template));\n prefs('background-custom-template', template);\n } else {\n background.baseLayerSource(\n background.findSource(requestedBackground) ||\n best ||\n isLastUsedValid && background.findSource(lastUsedBackground) ||\n background.findSource('Bing') ||\n first ||\n background.findSource('none')\n );\n }\n\n const locator = imageryIndex.backgrounds.find(d => d.overlay && d.default);\n if (locator) {\n background.toggleOverlayLayer(locator);\n }\n\n const overlays = (hash.overlays || '').split(',');\n overlays.forEach(overlay => {\n overlay = background.findSource(overlay);\n if (overlay) {\n background.toggleOverlayLayer(overlay);\n }\n });\n\n if (hash.gpx) {\n const gpx = context.layers().layer('data');\n if (gpx) {\n gpx.url(hash.gpx, '.gpx');\n }\n }\n\n if (hash.offset) {\n const offset = hash.offset\n .replace(/;/g, ',')\n .split(',')\n .map(n => !isNaN(n) && n);\n\n if (offset.length === 2) {\n background.offset(geoMetersToOffset(offset));\n }\n }\n })\n .catch(err => {\n /* eslint-disable no-console */\n console.error(err);\n /* eslint-enable no-console */\n });\n };\n\n\n return utilRebind(background, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from '../core/preferences';\nimport { osmEntity } from '../osm';\nimport { osmLanduseTags, osmLifecyclePrefixes } from '../osm/tags.js';\nimport { utilRebind } from '../util/rebind';\nimport { utilArrayGroupBy, utilArrayUnion, utilQsString, utilStringQs, utilDatesOverlap } from '../util';\nimport { isAddressPoint } from '../svg/labels';\n\n\nexport function rendererFeatures(context) {\n var dispatch = d3_dispatch('change', 'redraw');\n const features = {};\n var _deferred = new Set();\n\n var traffic_roads = {\n 'motorway': true,\n 'motorway_link': true,\n 'trunk': true,\n 'trunk_link': true,\n 'primary': true,\n 'primary_link': true,\n 'secondary': true,\n 'secondary_link': true,\n 'tertiary': true,\n 'tertiary_link': true,\n 'residential': true,\n 'unclassified': true,\n 'living_street': true,\n 'busway': true\n };\n\n var service_roads = {\n 'service': true,\n 'road': true,\n 'track': true\n };\n\n var paths = {\n 'path': true,\n 'footway': true,\n 'cycleway': true,\n 'bridleway': true,\n 'steps': true,\n 'ladder': true,\n 'pedestrian': true\n };\n\n var _cullFactor = 1;\n var _cache = {};\n var _rules = {};\n var _dateMatchCount = 0;\n var _stats = {};\n var _keys = [];\n var _hidden = [];\n var _forceVisible = {};\n\n\n function update() {\n const hash = utilStringQs(window.location.hash);\n const disabled = features.disabled();\n if (disabled.length) {\n hash.disable_features = disabled.join(',');\n } else {\n delete hash.disable_features;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n prefs('disabled-features', disabled.join(','));\n _hidden = features.hidden();\n dispatch.call('change');\n dispatch.call('redraw');\n }\n\n\n /**\n * @callback FilterFunction\n * @param {Record} tags\n * @param {string} [geometry]\n * @returns {boolean}\n */\n\n /**\n * @param {string} k\n * @param {FilterFunction} filter\n * @param {number} [max]\n */\n function defineRule(k, filter, max) {\n var isEnabled = true;\n\n _keys.push(k);\n _rules[k] = {\n filter: filter,\n enabled: isEnabled, // whether the user wants it enabled..\n count: 0,\n currentMax: (max || Infinity),\n defaultMax: (max || Infinity),\n enable: function() { this.enabled = true; this.currentMax = this.defaultMax; },\n disable: function() { this.enabled = false; this.currentMax = 0; },\n hidden: function() {\n return (this.count === 0 && !this.enabled) ||\n this.count > this.currentMax * _cullFactor;\n },\n autoHidden: function() { return this.hidden() && this.currentMax > 0; }\n };\n }\n\n defineRule('address_points', (tags, geometry) =>\n geometry === 'point' && isAddressPoint(tags),\n 100);\n\n defineRule('points', (tags, geometry) =>\n geometry === 'point' && !isAddressPoint(tags, geometry),\n 200);\n\n defineRule('traffic_roads', function isTrafficRoad(tags) {\n return traffic_roads[tags.highway];\n });\n\n defineRule('service_roads', function isServiceRoad(tags) {\n return service_roads[tags.highway];\n });\n\n defineRule('paths', function isPath(tags) {\n return paths[tags.highway];\n });\n\n defineRule('buildings', function isBuilding(tags) {\n return (\n (!!tags.building && tags.building !== 'no') ||\n tags.parking === 'multi-storey' ||\n tags.parking === 'sheds' ||\n tags.parking === 'carports' ||\n tags.parking === 'garage_boxes'\n );\n }, 250);\n\n defineRule('building_parts', function isBuildingPart(tags) {\n return !!tags['building:part'];\n });\n\n defineRule('indoor', function isIndoor(tags) {\n return (\n (!!tags.indoor && tags.indoor !== 'no') ||\n (!!tags.indoormark && tags.indoormark !== 'no')\n );\n });\n\n defineRule('landuse', function isLanduse(tags, geometry) {\n if (geometry !== 'area') return false;\n let hasLanduseTag = false;\n for (const key in osmLanduseTags) {\n if (osmLanduseTags[key] === true && tags[key] ||\n osmLanduseTags[key][tags[key]] === true) {\n hasLanduseTag = true;\n }\n }\n return hasLanduseTag &&\n !_rules.buildings.filter(tags) &&\n !_rules.building_parts.filter(tags) &&\n !_rules.indoor.filter(tags) &&\n !_rules.water.filter(tags) &&\n !_rules.pistes.filter(tags);\n });\n\n defineRule('boundaries', function isBoundary(tags, geometry) {\n // This rule applies if the object has no interesting tags, and if either:\n // (a) is a way having a `boundary=*` tag, or\n // (b) is a relation of `type=boundary`.\n return (\n (geometry === 'line' && !!tags.boundary) ||\n (geometry === 'relation' && tags.type === 'boundary')\n ) && !(\n traffic_roads[tags.highway] ||\n service_roads[tags.highway] ||\n paths[tags.highway] ||\n tags.waterway ||\n tags.railway ||\n tags.landuse ||\n tags.natural ||\n tags.building ||\n tags.power\n );\n });\n\n defineRule('water', function isWater(tags) {\n return (\n !!tags.waterway ||\n tags.natural === 'water' ||\n tags.natural === 'coastline' ||\n tags.natural === 'bay' ||\n tags.landuse === 'pond' ||\n tags.landuse === 'basin' ||\n tags.landuse === 'reservoir' ||\n tags.landuse === 'salt_pond'\n );\n });\n\n defineRule('rail', function isRail(tags) {\n return (\n !!tags.railway ||\n tags.landuse === 'railway'\n ) && !(\n traffic_roads[tags.highway] ||\n service_roads[tags.highway] ||\n paths[tags.highway]\n );\n });\n\n defineRule('pistes', function isPiste(tags) {\n return tags['piste:type'];\n });\n\n defineRule('aerialways', function isAerialways(tags) {\n return !!tags?.aerialway &&\n tags.aerialway !== 'yes' &&\n tags.aerialway !== 'station';\n });\n\n defineRule('power', function isPower(tags) {\n return !!tags.power;\n });\n\n // contains a past/future tag, but not in active use as a road/path/cycleway/etc..\n defineRule('past_future', function isPastFuture(tags) {\n if (\n traffic_roads[tags.highway] ||\n service_roads[tags.highway] ||\n paths[tags.highway]\n ) { return false; }\n\n const keys = Object.keys(tags);\n\n for (const key of keys) {\n if (osmLifecyclePrefixes[tags[key]]) return true; // legacy tagging, e.g. `highway=construction`\n const parts = key.split(':');\n if (parts.length === 1) continue;\n const prefix = parts[0];\n if (osmLifecyclePrefixes[prefix]) return true; // lifecycle tagging, e.g. `demolished:building=yes`\n }\n return false;\n });\n\n // Lines or areas that don't match another feature filter.\n // IMPORTANT: The 'others' feature must be the last one defined,\n // so that code in getMatches can skip this test if `hasMatch = true`\n defineRule('others', function isOther(tags, geometry) {\n return (geometry === 'line' || geometry === 'area');\n });\n\n\n\n features.features = function() {\n return _rules;\n };\n\n\n features.keys = function() {\n return _keys;\n };\n\n\n features.enabled = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return _rules[k].enabled; });\n }\n return _rules[k] && _rules[k].enabled;\n };\n\n\n features.disabled = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return !_rules[k].enabled; });\n }\n return _rules[k] && !_rules[k].enabled;\n };\n\n\n features.hidden = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return _rules[k].hidden(); });\n }\n return _rules[k]?.hidden();\n };\n\n\n features.autoHidden = function(k) {\n if (!arguments.length) {\n return _keys.filter(function(k) { return _rules[k].autoHidden(); });\n }\n return _rules[k] && _rules[k].autoHidden();\n };\n\n\n features.enable = function(k) {\n if (_rules[k] && !_rules[k].enabled) {\n _rules[k].enable();\n update();\n }\n };\n\n features.enableAll = function() {\n var didEnable = false;\n for (var k in _rules) {\n if (!_rules[k].enabled) {\n didEnable = true;\n _rules[k].enable();\n }\n }\n if (didEnable) update();\n };\n\n\n features.disable = function(k) {\n if (_rules[k] && _rules[k].enabled) {\n _rules[k].disable();\n update();\n }\n };\n\n features.disableAll = function() {\n var didDisable = false;\n for (var k in _rules) {\n if (_rules[k].enabled) {\n didDisable = true;\n _rules[k].disable();\n }\n }\n if (didDisable) update();\n };\n\n\n features.toggle = function(k) {\n if (_rules[k]) {\n (function(f) { return f.enabled ? f.disable() : f.enable(); }(_rules[k]));\n update();\n }\n };\n\n\n features.redraw = function() {\n update();\n };\n\n\n features.resetStats = function() {\n for (var i = 0; i < _keys.length; i++) {\n _rules[_keys[i]].count = 0;\n }\n _dateMatchCount = 0;\n dispatch.call('change');\n };\n\n\n features.gatherStats = function(d, resolver, dimensions) {\n var needsRedraw = false;\n var types = utilArrayGroupBy(d, 'type');\n var entities = [].concat(types.relation || [], types.way || [], types.node || []);\n var currHidden, geometry, matches, i, j;\n\n for (i = 0; i < _keys.length; i++) {\n _rules[_keys[i]].count = 0;\n }\n _dateMatchCount = 0;\n\n // adjust the threshold for point/building culling based on viewport size..\n // a _cullFactor of 1 corresponds to a 1000x1000px viewport..\n _cullFactor = dimensions[0] * dimensions[1] / 1000000;\n\n for (i = 0; i < entities.length; i++) {\n geometry = entities[i].geometry(resolver);\n matches = Object.keys(features.getMatches(entities[i], resolver, geometry));\n for (j = 0; j < matches.length; j++) {\n _rules[matches[j]].count++;\n }\n if (!features.featureFitsDateRange(entities[i])) _dateMatchCount++;\n }\n\n currHidden = features.hidden();\n if (currHidden !== _hidden) {\n _hidden = currHidden;\n needsRedraw = true;\n dispatch.call('change');\n }\n\n return needsRedraw;\n };\n\n\n features.stats = function() {\n for (var i = 0; i < _keys.length; i++) {\n _stats[_keys[i]] = _rules[_keys[i]].count;\n }\n\n return _stats;\n };\n\n\n features.dateMatchCount = () => _dateMatchCount;\n\n\n features.clear = function(d) {\n for (var i = 0; i < d.length; i++) {\n features.clearEntity(d[i]);\n }\n };\n\n\n features.clearEntity = function(entity) {\n delete _cache[osmEntity.key(entity)];\n for (const key in _cache) {\n if (_cache[key].parents) {\n for (const parent of _cache[key].parents) {\n if (parent.id === entity.id) {\n delete _cache[key];\n break;\n }\n }\n }\n }\n };\n\n\n features.reset = function() {\n Array.from(_deferred).forEach(function(handle) {\n window.cancelIdleCallback(handle);\n _deferred.delete(handle);\n });\n\n _cache = {};\n };\n\n // only certain relations are worth checking\n function relationShouldBeChecked(relation) {\n // multipolygon features have `area` geometry and aren't checked here\n return relation.tags.type === 'boundary';\n }\n\n features.getMatches = function(entity, resolver, geometry) {\n if (geometry === 'vertex' ||\n (geometry === 'relation' && !relationShouldBeChecked(entity))) return {};\n\n var ent = osmEntity.key(entity);\n if (!_cache[ent]) {\n _cache[ent] = {};\n }\n\n if (!_cache[ent].matches) {\n var matches = {};\n var hasMatch = false;\n\n for (var i = 0; i < _keys.length; i++) {\n if (_keys[i] === 'others') {\n if (hasMatch) continue;\n\n // If an entity...\n // 1. is a way that hasn't matched other 'interesting' feature rules,\n if (entity.type === 'way') {\n var parents = features.getParents(entity, resolver, geometry);\n\n // 2a. belongs only to a single multipolygon relation\n if ((parents.length === 1 && parents[0].isMultipolygon()) ||\n // 2b. or belongs only to boundary relations\n (parents.length > 0 && parents.every(function(parent) { return parent.tags.type === 'boundary'; }))) {\n\n // ...then match whatever feature rules the parent relation has matched.\n // see #2548, #2887\n //\n // IMPORTANT:\n // For this to work, getMatches must be called on relations before ways.\n //\n var pkey = osmEntity.key(parents[0]);\n if (_cache[pkey] && _cache[pkey].matches) {\n matches = Object.assign({}, _cache[pkey].matches); // shallow copy\n continue;\n }\n }\n }\n }\n\n if (_rules[_keys[i]].filter(entity.tags, geometry)) {\n matches[_keys[i]] = true;\n hasMatch = true;\n }\n }\n _cache[ent].matches = matches;\n }\n\n return _cache[ent].matches;\n };\n\n\n features.getParents = function(entity, resolver, geometry) {\n if (geometry === 'point') return [];\n\n const ent = osmEntity.key(entity);\n if (!_cache[ent]) {\n _cache[ent] = {};\n }\n\n if (!_cache[ent].parents) {\n let parents;\n if (geometry === 'vertex') {\n parents = resolver.parentWays(entity);\n } else { // 'line', 'area', 'relation'\n parents = resolver.parentRelations(entity);\n }\n _cache[ent].parents = parents;\n }\n\n return _cache[ent].parents;\n };\n\n\n features.isHiddenPreset = function(preset, geometry) {\n // if (!_hidden.length) return false;\n if (!preset.tags) return false;\n\n var test = preset.setTags({...preset.tags}, geometry);\n for (var key in _rules) {\n if (_rules[key].filter(test, geometry)) {\n if (_hidden.indexOf(key) !== -1) {\n return key;\n }\n return false;\n }\n }\n return false;\n };\n\n\n features.isHiddenFeature = function(entity, resolver, geometry) {\n // if (!_hidden.length) return false;\n if (!entity.version) return false;\n if (_forceVisible[entity.id]) return false;\n if (!features.featureFitsDateRange(entity)) return true;\n // if (!_hidden.length) return false;\n\n var matches = Object.keys(features.getMatches(entity, resolver, geometry));\n return matches.length && matches.every(function(k) { return features.hidden(k); });\n };\n\n\n features.isHiddenChild = function(entity, resolver, geometry) {\n // if (!_hidden.length) return false;\n if (!entity.version || geometry === 'point') return false;\n if (_forceVisible[entity.id]) return false;\n if (!features.featureFitsDateRange(entity)) return true;\n\n var parents = features.getParents(entity, resolver, geometry);\n if (!parents.length) return false;\n\n for (var i = 0; i < parents.length; i++) {\n if (!features.isHidden(parents[i], resolver, parents[i].geometry(resolver))) {\n return false;\n }\n }\n return true;\n };\n\n\n features.hasHiddenConnections = function(entity, resolver) {\n // if (!_hidden.length) return false;\n\n var childNodes, connections;\n if (entity.type === 'midpoint') {\n childNodes = [resolver.entity(entity.edge[0]), resolver.entity(entity.edge[1])];\n connections = [];\n } else {\n childNodes = entity.nodes ? resolver.childNodes(entity) : [];\n connections = features.getParents(entity, resolver, entity.geometry(resolver));\n }\n\n // gather ways connected to child nodes..\n connections = childNodes.reduce(function(result, e) {\n return resolver.isShared(e) ? utilArrayUnion(result, resolver.parentWays(e)) : result;\n }, connections);\n\n return connections.some(function(e) {\n return features.isHidden(e, resolver, e.geometry(resolver));\n });\n };\n\n\n features.isHidden = function(entity, resolver, geometry) {\n // if (!_hidden.length) return false;\n if (!entity.version) return false;\n\n var fn = (geometry === 'vertex' ? features.isHiddenChild : features.isHiddenFeature);\n return fn(entity, resolver, geometry);\n };\n\n\n features.featureFitsDateRange = function (entity) {\n if (!features.dateRange) return true; // no Date Range e.g. unit tests\n\n // entity's start & end date + the Date Range from the on-screen controls\n // utilDatesOverlap() treats malformed start_date/end_date as 9999/-9999\n // uiSectionDateRange already standardizes the dateRange inputs\n // so we don't need much validation here\n const entityRange = {\n 'start_date': entity.tags.start_date,\n 'end_date': entity.tags.end_date\n };\n const selectedRange = {\n 'start_date': features.dateRange[0],\n 'end_date': features.dateRange[1]\n };\n\n // out of range = feature started after range ends, or feature ends before range starts\n const withinrange = utilDatesOverlap(selectedRange, entityRange, true);\n return withinrange;\n };\n\n\n features.filter = function(d, resolver) {\n // if (!_hidden.length) return d;\n\n var result = [];\n for (var i = 0; i < d.length; i++) {\n var entity = d[i];\n if (!features.isHidden(entity, resolver, entity.geometry(resolver))) {\n result.push(entity);\n }\n }\n return result;\n };\n\n\n features.forceVisible = function(entityIDs) {\n if (!arguments.length) return Object.keys(_forceVisible);\n\n _forceVisible = {};\n for (var i = 0; i < entityIDs.length; i++) {\n _forceVisible[entityIDs[i]] = true;\n var entity = context.hasEntity(entityIDs[i]);\n if (entity && entity.type === 'relation') {\n // also show relation members (one level deep)\n for (var j in entity.members) {\n _forceVisible[entity.members[j].id] = true;\n }\n }\n }\n return features;\n };\n\n\n features.init = function() {\n var storage = prefs('disabled-features');\n if (storage) {\n var storageDisabled = storage.replace(/;/g, ',').split(',');\n storageDisabled.forEach(features.disable);\n }\n\n var hash = utilStringQs(window.location.hash);\n if (hash.disable_features) {\n var hashDisabled = hash.disable_features.replace(/;/g, ',').split(',');\n hashDisabled.forEach(features.disable);\n }\n };\n\n\n // warm up the feature matching cache upon merging fetched data\n context.history().on('merge.features', function(newEntities) {\n if (!newEntities) return;\n var handle = window.requestIdleCallback(function() {\n var graph = context.graph();\n var types = utilArrayGroupBy(newEntities, 'type');\n // ensure that getMatches is called on relations before ways\n var entities = [].concat(types.relation || [], types.way || [], types.node || []);\n for (var i = 0; i < entities.length; i++) {\n var geometry = entities[i].geometry(graph);\n features.getMatches(entities[i], graph, geometry);\n }\n });\n _deferred.add(handle);\n });\n\n\n return utilRebind(features, dispatch, 'on');\n}\n", "export function utilBindOnce(target, type, listener, capture) {\n var typeOnce = type + '.once';\n function one() {\n target.on(typeOnce, null);\n listener.apply(this, arguments);\n }\n target.on(typeOnce, one, capture);\n return this;\n}\n", "// Adapted from d3-zoom to handle pointer events.\n// https://github.com/d3/d3-zoom/blob/523ccff340187a3e3c044eaa4d4a7391ea97272b/src/zoom.js\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { interpolateZoom } from 'd3-interpolate';\nimport { select as d3_select } from 'd3-selection';\nimport { interrupt as d3_interrupt } from 'd3-transition';\nimport { zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\nimport { Transform } from '../../node_modules/d3-zoom/src/transform.js';\n\nimport { utilFastMouse, utilFunctor } from './util';\nimport { utilRebind } from './rebind';\n\n// Ignore right-click, since that should open the context menu.\nfunction defaultFilter(d3_event) {\n return !d3_event.ctrlKey && !d3_event.button;\n}\n\nfunction defaultExtent() {\n var e = this;\n if (e instanceof SVGElement) {\n e = e.ownerSVGElement || e;\n if (e.hasAttribute('viewBox')) {\n e = e.viewBox.baseVal;\n return [[e.x, e.y], [e.x + e.width, e.y + e.height]];\n }\n return [[0, 0], [e.width.baseVal.value, e.height.baseVal.value]];\n }\n return [[0, 0], [e.clientWidth, e.clientHeight]];\n}\n\nfunction defaultWheelDelta(d3_event) {\n return -d3_event.deltaY * (d3_event.deltaMode === 1 ? 0.05 : d3_event.deltaMode ? 1 : 0.002);\n}\n\nfunction defaultConstrain(transform, extent, translateExtent) {\n var dx0 = transform.invertX(extent[0][0]) - translateExtent[0][0],\n dx1 = transform.invertX(extent[1][0]) - translateExtent[1][0],\n dy0 = transform.invertY(extent[0][1]) - translateExtent[0][1],\n dy1 = transform.invertY(extent[1][1]) - translateExtent[1][1];\n return transform.translate(\n dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),\n dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)\n );\n}\n\nexport function utilZoomPan() {\n var filter = defaultFilter,\n extent = defaultExtent,\n constrain = defaultConstrain,\n wheelDelta = defaultWheelDelta,\n scaleExtent = [0, Infinity],\n translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]],\n interpolate = interpolateZoom,\n dispatch = d3_dispatch('start', 'zoom', 'end'),\n _wheelDelay = 150,\n _transform = d3_zoomIdentity,\n _activeGesture;\n\n function zoom(selection) {\n selection\n .on('pointerdown.zoom', pointerdown)\n .on('wheel.zoom', wheeled)\n .style('touch-action', 'none')\n .style('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');\n\n d3_select(window)\n .on('pointermove.zoompan', pointermove)\n .on('pointerup.zoompan pointercancel.zoompan', pointerup);\n }\n\n zoom.transform = function(collection, transform, point) {\n var selection = collection.selection ? collection.selection() : collection;\n if (collection !== selection) {\n schedule(collection, transform, point);\n } else {\n selection.interrupt().each(function() {\n gesture(this, arguments)\n .start(null)\n .zoom(null, null, typeof transform === 'function' ? transform.apply(this, arguments) : transform)\n .end(null);\n });\n }\n };\n\n zoom.scaleBy = function(selection, k, p) {\n zoom.scaleTo(selection, function() {\n var k0 = _transform.k,\n k1 = typeof k === 'function' ? k.apply(this, arguments) : k;\n return k0 * k1;\n }, p);\n };\n\n zoom.scaleTo = function(selection, k, p) {\n zoom.transform(selection, function() {\n var e = extent.apply(this, arguments),\n t0 = _transform,\n p0 = !p ? centroid(e) : typeof p === 'function' ? p.apply(this, arguments) : p,\n p1 = t0.invert(p0),\n k1 = typeof k === 'function' ? k.apply(this, arguments) : k;\n return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent);\n }, p);\n };\n\n zoom.translateBy = function(selection, x, y) {\n zoom.transform(selection, function() {\n return constrain(_transform.translate(\n typeof x === 'function' ? x.apply(this, arguments) : x,\n typeof y === 'function' ? y.apply(this, arguments) : y\n ), extent.apply(this, arguments), translateExtent);\n });\n };\n\n zoom.translateTo = function(selection, x, y, p) {\n zoom.transform(selection, function() {\n var e = extent.apply(this, arguments),\n t = _transform,\n p0 = !p ? centroid(e) : typeof p === 'function' ? p.apply(this, arguments) : p;\n return constrain(d3_zoomIdentity.translate(p0[0], p0[1]).scale(t.k).translate(\n typeof x === 'function' ? -x.apply(this, arguments) : -x,\n typeof y === 'function' ? -y.apply(this, arguments) : -y\n ), e, translateExtent);\n }, p);\n };\n\n function scale(transform, k) {\n k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], k));\n return k === transform.k ? transform : new Transform(k, transform.x, transform.y);\n }\n\n function translate(transform, p0, p1) {\n var x = p0[0] - p1[0] * transform.k, y = p0[1] - p1[1] * transform.k;\n return x === transform.x && y === transform.y ? transform : new Transform(transform.k, x, y);\n }\n\n function centroid(extent) {\n return [(+extent[0][0] + +extent[1][0]) / 2, (+extent[0][1] + +extent[1][1]) / 2];\n }\n\n function schedule(transition, transform, point) {\n transition\n .on('start.zoom', function() { gesture(this, arguments).start(null); })\n .on('interrupt.zoom end.zoom', function() { gesture(this, arguments).end(null); })\n .tween('zoom', function() {\n var that = this,\n args = arguments,\n g = gesture(that, args),\n e = extent.apply(that, args),\n p = !point ? centroid(e) : typeof point === 'function' ? point.apply(that, args) : point,\n w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]),\n a = _transform,\n b = typeof transform === 'function' ? transform.apply(that, args) : transform,\n i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k));\n return function(t) {\n if (t === 1) {\n // Avoid rounding error on end.\n t = b;\n } else {\n var l = i(t);\n var k = w / l[2];\n t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k);\n }\n g.zoom(null, null, t);\n };\n });\n }\n\n function gesture(that, args, clean) {\n return (!clean && _activeGesture) || new Gesture(that, args);\n }\n\n function Gesture(that, args) {\n this.that = that;\n this.args = args;\n this.active = 0;\n this.extent = extent.apply(that, args);\n }\n\n Gesture.prototype = {\n start: function(d3_event) {\n if (++this.active === 1) {\n _activeGesture = this;\n dispatch.call('start', this, d3_event);\n }\n return this;\n },\n zoom: function(d3_event, key, transform) {\n if (this.mouse && key !== 'mouse') this.mouse[1] = transform.invert(this.mouse[0]);\n if (this.pointer0 && key !== 'touch') this.pointer0[1] = transform.invert(this.pointer0[0]);\n if (this.pointer1 && key !== 'touch') this.pointer1[1] = transform.invert(this.pointer1[0]);\n _transform = transform;\n dispatch.call('zoom', this, d3_event, key, transform);\n return this;\n },\n end: function(d3_event) {\n if (--this.active === 0) {\n _activeGesture = null;\n dispatch.call('end', this, d3_event);\n }\n return this;\n }\n };\n\n function wheeled(d3_event) {\n if (!filter.apply(this, arguments)) return;\n var g = gesture(this, arguments),\n t = _transform,\n k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k * Math.pow(2, wheelDelta.apply(this, arguments)))),\n p = utilFastMouse(this)(d3_event);\n\n // If the mouse is in the same location as before, reuse it.\n // If there were recent wheel events, reset the wheel idle timeout.\n if (g.wheel) {\n if (g.mouse[0][0] !== p[0] || g.mouse[0][1] !== p[1]) {\n g.mouse[1] = t.invert(g.mouse[0] = p);\n }\n clearTimeout(g.wheel);\n\n // Otherwise, capture the mouse point and location at the start.\n } else {\n g.mouse = [p, t.invert(p)];\n d3_interrupt(this);\n g.start(d3_event);\n }\n\n d3_event.preventDefault();\n d3_event.stopImmediatePropagation();\n g.wheel = setTimeout(wheelidled, _wheelDelay);\n g.zoom(d3_event, 'mouse', constrain(translate(scale(t, k), g.mouse[0], g.mouse[1]), g.extent, translateExtent));\n\n function wheelidled() {\n g.wheel = null;\n g.end(d3_event);\n }\n }\n\n var _downPointerIDs = new Set();\n var _pointerLocGetter;\n\n function pointerdown(d3_event) {\n _downPointerIDs.add(d3_event.pointerId);\n\n if (!filter.apply(this, arguments)) return;\n\n var g = gesture(this, arguments, _downPointerIDs.size === 1);\n var started;\n\n d3_event.stopImmediatePropagation();\n _pointerLocGetter = utilFastMouse(this);\n var loc = _pointerLocGetter(d3_event);\n var p = [loc, _transform.invert(loc), d3_event.pointerId];\n if (!g.pointer0) {\n g.pointer0 = p;\n started = true;\n\n } else if (!g.pointer1 && g.pointer0[2] !== p[2]) {\n g.pointer1 = p;\n }\n\n if (started) {\n d3_interrupt(this);\n g.start(d3_event);\n }\n }\n\n function pointermove(d3_event) {\n if (!_downPointerIDs.has(d3_event.pointerId)) return;\n\n if (!_activeGesture || !_pointerLocGetter) return;\n\n var g = gesture(this, arguments);\n\n var isPointer0 = g.pointer0 && g.pointer0[2] === d3_event.pointerId;\n var isPointer1 = !isPointer0 && g.pointer1 && g.pointer1[2] === d3_event.pointerId;\n\n if ((isPointer0 || isPointer1) && 'buttons' in d3_event && !d3_event.buttons) {\n // The pointer went up without ending the gesture somehow, e.g.\n // a down mouse was moved off the map and released. End it here.\n if (g.pointer0) _downPointerIDs.delete(g.pointer0[2]);\n if (g.pointer1) _downPointerIDs.delete(g.pointer1[2]);\n g.end(d3_event);\n return;\n }\n\n d3_event.preventDefault();\n d3_event.stopImmediatePropagation();\n\n var loc = _pointerLocGetter(d3_event);\n var t, p, l;\n\n if (isPointer0) g.pointer0[0] = loc;\n else if (isPointer1) g.pointer1[0] = loc;\n\n t = _transform;\n if (g.pointer1) {\n var p0 = g.pointer0[0], l0 = g.pointer0[1],\n p1 = g.pointer1[0], l1 = g.pointer1[1],\n dp = (dp = p1[0] - p0[0]) * dp + (dp = p1[1] - p0[1]) * dp,\n dl = (dl = l1[0] - l0[0]) * dl + (dl = l1[1] - l0[1]) * dl;\n t = scale(t, Math.sqrt(dp / dl));\n p = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];\n l = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2];\n } else if (g.pointer0) {\n p = g.pointer0[0];\n l = g.pointer0[1];\n } else {\n return;\n }\n g.zoom(d3_event, 'touch', constrain(translate(t, p, l), g.extent, translateExtent));\n }\n\n function pointerup(d3_event) {\n if (!_downPointerIDs.has(d3_event.pointerId)) return;\n\n _downPointerIDs.delete(d3_event.pointerId);\n\n if (!_activeGesture) return;\n\n var g = gesture(this, arguments);\n\n d3_event.stopImmediatePropagation();\n\n if (g.pointer0 && g.pointer0[2] === d3_event.pointerId) delete g.pointer0;\n else if (g.pointer1 && g.pointer1[2] === d3_event.pointerId) delete g.pointer1;\n\n if (g.pointer1 && !g.pointer0) {\n g.pointer0 = g.pointer1;\n delete g.pointer1;\n }\n if (g.pointer0) {\n g.pointer0[1] = _transform.invert(g.pointer0[0]);\n } else {\n g.end(d3_event);\n }\n }\n\n zoom.wheelDelta = function(_) {\n return arguments.length ? (wheelDelta = utilFunctor(+_), zoom) : wheelDelta;\n };\n\n zoom.filter = function(_) {\n return arguments.length ? (filter = utilFunctor(!!_), zoom) : filter;\n };\n\n zoom.extent = function(_) {\n return arguments.length ? (extent = utilFunctor([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;\n };\n\n zoom.scaleExtent = function(_) {\n return arguments.length ? (scaleExtent[0] = +_[0], scaleExtent[1] = +_[1], zoom) : [scaleExtent[0], scaleExtent[1]];\n };\n\n zoom.translateExtent = function(_) {\n return arguments.length ? (translateExtent[0][0] = +_[0][0], translateExtent[1][0] = +_[1][0], translateExtent[0][1] = +_[0][1], translateExtent[1][1] = +_[1][1], zoom) : [[translateExtent[0][0], translateExtent[0][1]], [translateExtent[1][0], translateExtent[1][1]]];\n };\n\n zoom.constrain = function(_) {\n return arguments.length ? (constrain = _, zoom) : constrain;\n };\n\n zoom.interpolate = function(_) {\n return arguments.length ? (interpolate = _, zoom) : interpolate;\n };\n\n zoom._transform = function(_) {\n return arguments.length ? (_transform = _, zoom) : _transform;\n };\n\n return utilRebind(zoom, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { utilFastMouse } from './util';\nimport { utilRebind } from './rebind';\nimport { geoVecLength } from '../geo/vector';\n\n// A custom double-click / double-tap event detector that works on touch devices\n// if pointer events are supported. Falls back to default `dblclick` event.\nexport function utilDoubleUp() {\n\n var dispatch = d3_dispatch('doubleUp');\n\n var _maxTimespan = 500; // milliseconds\n var _maxDistance = 20; // web pixels; be somewhat generous to account for touch devices\n var _pointer; // object representing the pointer that could trigger double up\n\n function pointerIsValidFor(loc) {\n // second pointerup must occur within a small timeframe after the first pointerdown\n return new Date().getTime() - _pointer.startTime <= _maxTimespan &&\n // all pointer events must occur within a small distance of the first pointerdown\n geoVecLength(_pointer.startLoc, loc) <= _maxDistance;\n }\n\n function pointerdown(d3_event) {\n\n // ignore right-click\n if (d3_event.ctrlKey || d3_event.button === 2) return;\n\n var loc = [d3_event.clientX, d3_event.clientY];\n\n // Don't rely on pointerId here since it can change between pointerdown\n // events on touch devices\n if (_pointer && !pointerIsValidFor(loc)) {\n // if this pointer is no longer valid, clear it so another can be started\n _pointer = undefined;\n }\n\n if (!_pointer) {\n _pointer = {\n startLoc: loc,\n startTime: new Date().getTime(),\n upCount: 0,\n pointerId: d3_event.pointerId\n };\n } else { // double down\n _pointer.pointerId = d3_event.pointerId;\n }\n }\n\n function pointerup(d3_event) {\n\n // ignore right-click\n if (d3_event.ctrlKey || d3_event.button === 2) return;\n\n if (!_pointer || _pointer.pointerId !== d3_event.pointerId) return;\n\n _pointer.upCount += 1;\n\n if (_pointer.upCount === 2) { // double up!\n var loc = [d3_event.clientX, d3_event.clientY];\n if (pointerIsValidFor(loc)) {\n var locInThis = utilFastMouse(this)(d3_event);\n dispatch.call('doubleUp', this, d3_event, locInThis);\n }\n // clear the pointer info in any case\n _pointer = undefined;\n }\n }\n\n function doubleUp(selection) {\n if ('PointerEvent' in window) {\n // dblclick isn't well supported on touch devices so manually use\n // pointer events if they're available\n selection\n .on('pointerdown.doubleUp', pointerdown)\n .on('pointerup.doubleUp', pointerup);\n } else {\n // fallback to dblclick\n selection\n .on('dblclick.doubleUp', function(d3_event) {\n dispatch.call('doubleUp', this, d3_event, utilFastMouse(this)(d3_event));\n });\n }\n }\n\n doubleUp.off = function(selection) {\n selection\n .on('pointerdown.doubleUp', null)\n .on('pointerup.doubleUp', null)\n .on('dblclick.doubleUp', null);\n };\n\n return utilRebind(doubleUp, dispatch, 'on');\n}\n", "import { throttle, isArray, clamp } from 'es-toolkit/compat';\n\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { interpolate as d3_interpolate } from 'd3-interpolate';\nimport { scaleLinear as d3_scaleLinear } from 'd3-scale';\nimport { select as d3_select } from 'd3-selection';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport { prefs } from '../core/preferences';\nimport { geoExtent, geoRawMercator, geoScaleToZoom, geoZoomToScale } from '../geo';\nimport { modeBrowse } from '../modes/browse';\nimport { svgAreas, svgLabels, svgLayers, svgLines, svgMidpoints, svgPoints, svgVertices } from '../svg';\nimport { utilFastMouse, utilFunctor, utilSetTransform, utilEntityAndDeepMemberIDs } from '../util/util';\nimport { utilBindOnce } from '../util/bind_once';\nimport { utilDetect } from '../util/detect';\nimport { utilGetDimensions } from '../util/dimensions';\nimport { utilRebind } from '../util/rebind';\nimport { utilZoomPan } from '../util/zoom_pan';\nimport { utilDoubleUp } from '../util/double_up';\n\n// constants\nvar TILESIZE = 256;\nvar minZoom = 2;\nvar maxZoom = 24;\nvar kMin = geoZoomToScale(minZoom, TILESIZE);\nvar kMax = geoZoomToScale(maxZoom, TILESIZE);\n\n\nexport function rendererMap(context) {\n var dispatch = d3_dispatch(\n 'move', 'drawn',\n 'crossEditableZoom', 'hitMinZoom',\n 'changeHighlighting', 'changeAreaFill'\n );\n var projection = context.projection;\n var curtainProjection = context.curtainProjection;\n var drawLayers;\n var drawPoints;\n var drawVertices;\n var drawLines;\n var drawAreas;\n var drawMidpoints;\n var drawLabels;\n\n var _selection = d3_select(null);\n var supersurface = d3_select(null);\n var wrapper = d3_select(null);\n var surface = d3_select(null);\n\n var _dimensions = [1, 1];\n var _dblClickZoomEnabled = true;\n var _redrawEnabled = true;\n var _gestureTransformStart;\n var _transformStart = projection.transform();\n var _transformLast;\n var _isTransformed = false;\n var _minzoom = 0;\n var _getMouseCoords;\n var _lastPointerEvent;\n var _lastWithinEditableZoom;\n\n // whether a pointerdown event started the zoom\n var _pointerDown = false;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n // use pointer event interaction if supported; fallback to touch/mouse events in d3-zoom\n var _zoomerPannerFunction = 'PointerEvent' in window ? utilZoomPan : d3_zoom;\n\n var _zoomerPanner = _zoomerPannerFunction()\n .scaleExtent([kMin, kMax])\n .interpolate(d3_interpolate)\n .filter(zoomEventFilter)\n .on('zoom.map', zoomPan)\n .on('start.map', function(d3_event) {\n _pointerDown = d3_event && (d3_event.type === 'pointerdown' ||\n (d3_event.sourceEvent && d3_event.sourceEvent.type === 'pointerdown'));\n })\n .on('end.map', function() {\n _pointerDown = false;\n });\n var _doubleUpHandler = utilDoubleUp();\n\n var scheduleRedraw = throttle(redraw, 750);\n // var isRedrawScheduled = false;\n // var pendingRedrawCall;\n // function scheduleRedraw() {\n // // Only schedule the redraw if one has not already been set.\n // if (isRedrawScheduled) return;\n // isRedrawScheduled = true;\n // var that = this;\n // var args = arguments;\n // pendingRedrawCall = window.requestIdleCallback(function () {\n // // Reset the boolean so future redraws can be set.\n // isRedrawScheduled = false;\n // redraw.apply(that, args);\n // }, { timeout: 1400 });\n // }\n\n function cancelPendingRedraw() {\n scheduleRedraw.cancel();\n // isRedrawScheduled = false;\n // window.cancelIdleCallback(pendingRedrawCall);\n }\n\n\n function map(selection) {\n _selection = selection;\n\n context\n .on('change.map', immediateRedraw);\n\n var osm = context.connection();\n if (osm) {\n osm.on('change.map', immediateRedraw);\n }\n\n function didUndoOrRedo(targetTransform) {\n var mode = context.mode().id;\n if (mode !== 'browse' && mode !== 'select') return;\n if (targetTransform) {\n map.transformEase(targetTransform);\n }\n }\n\n context.history()\n .on('merge.map', function() { scheduleRedraw(); })\n .on('change.map', immediateRedraw)\n .on('undone.map', function(stack, fromStack) {\n didUndoOrRedo(fromStack.transform);\n })\n .on('redone.map', function(stack) {\n didUndoOrRedo(stack.transform);\n });\n\n context.background()\n .on('change.map', immediateRedraw);\n\n context.features()\n .on('redraw.map', immediateRedraw);\n\n drawLayers\n .on('change.map', function() {\n context.background().updateImagery();\n immediateRedraw();\n });\n\n selection\n .on('wheel.map mousewheel.map', function(d3_event) {\n // disable swipe-to-navigate browser pages on trackpad/magic mouse \u2013 #5552\n d3_event.preventDefault();\n })\n .call(_zoomerPanner)\n .call(_zoomerPanner.transform, projection.transform())\n .on('dblclick.zoom', null); // override d3-zoom dblclick handling\n\n map.supersurface = selection.append('div')\n .attr('class', 'supersurface')\n .call(utilSetTransform, 0, 0);\n supersurface = map.supersurface;\n\n // Need a wrapper div because Opera can't cope with an absolutely positioned\n // SVG element: http://bl.ocks.org/jfirebaugh/6fbfbd922552bf776c16\n wrapper = supersurface\n .append('div')\n .attr('class', 'layer layer-data');\n\n map.surface = wrapper\n .call(drawLayers)\n .selectAll('.surface');\n surface = map.surface;\n\n surface\n .call(drawLabels.observe)\n .call(_doubleUpHandler)\n .on(_pointerPrefix + 'down.zoom', function(d3_event) {\n _lastPointerEvent = d3_event;\n if (d3_event.button === 2) {\n d3_event.stopPropagation();\n }\n }, true)\n .on(_pointerPrefix + 'up.zoom', function(d3_event) {\n _lastPointerEvent = d3_event;\n if (resetTransform()) {\n immediateRedraw();\n }\n })\n .on(_pointerPrefix + 'move.map', function(d3_event) {\n _lastPointerEvent = d3_event;\n })\n .on(_pointerPrefix + 'over.vertices', function(d3_event) {\n if (map.editableDataEnabled() && !_isTransformed) {\n var hover = d3_event.target.__data__;\n surface.call(drawVertices.drawHover, context.graph(), hover, map.extent());\n dispatch.call('drawn', this, { full: false });\n }\n })\n .on(_pointerPrefix + 'out.vertices', function(d3_event) {\n if (map.editableDataEnabled() && !_isTransformed) {\n var hover = d3_event.relatedTarget && d3_event.relatedTarget.__data__;\n surface.call(drawVertices.drawHover, context.graph(), hover, map.extent());\n dispatch.call('drawn', this, { full: false });\n }\n });\n\n var detected = utilDetect();\n\n // only WebKit supports gesture events\n if ('GestureEvent' in window &&\n // Listening for gesture events on iOS 13.4+ breaks double-tapping,\n // but we only need to do this on desktop Safari anyway. \u2013 #7694\n !detected.isMobileWebKit) {\n\n // Desktop Safari sends gesture events for multitouch trackpad pinches.\n // We can listen for these and translate them into map zooms.\n surface\n .on('gesturestart.surface', function(d3_event) {\n d3_event.preventDefault();\n _gestureTransformStart = projection.transform();\n })\n .on('gesturechange.surface', gestureChange);\n }\n\n // must call after surface init\n updateAreaFill();\n\n _doubleUpHandler.on('doubleUp.map', function(d3_event, p0) {\n if (!_dblClickZoomEnabled) return;\n\n // don't zoom if targeting something other than the map itself\n if (typeof d3_event.target.__data__ === 'object' &&\n // or area fills\n !d3_select(d3_event.target).classed('fill')) return;\n\n var zoomOut = d3_event.shiftKey;\n\n var t = projection.transform();\n\n var p1 = t.invert(p0);\n\n t = t.scale(zoomOut ? 0.5 : 2);\n\n t.x = p0[0] - p1[0] * t.k;\n t.y = p0[1] - p1[1] * t.k;\n\n map.transformEase(t);\n });\n\n context.on('enter.map', function() {\n if (!map.editableDataEnabled(true /* skip zoom check */)) return;\n if (_isTransformed) return;\n\n // redraw immediately any objects affected by a change in selectedIDs.\n var graph = context.graph();\n var selectedAndParents = {};\n context.selectedIDs().forEach(function(id) {\n var entity = graph.hasEntity(id);\n if (entity) {\n selectedAndParents[entity.id] = entity;\n if (entity.type === 'node') {\n graph.parentWays(entity).forEach(function(parent) {\n selectedAndParents[parent.id] = parent;\n });\n }\n }\n });\n var data = Object.values(selectedAndParents);\n var filter = function(d) { return d.id in selectedAndParents; };\n\n data = context.features().filter(data, graph);\n\n surface\n .call(drawVertices.drawSelected, graph, map.extent())\n .call(drawLines, graph, data, filter)\n .call(drawAreas, graph, data, filter)\n .call(drawMidpoints, graph, data, filter, map.trimmedExtent());\n\n dispatch.call('drawn', this, { full: false });\n\n // redraw everything else later\n scheduleRedraw();\n });\n\n map.dimensions(utilGetDimensions(selection));\n }\n\n\n function zoomEventFilter(d3_event) {\n // Fix for #2151, (see also d3/d3-zoom#60, d3/d3-brush#18)\n // Intercept `mousedown` and check if there is an orphaned zoom gesture.\n // This can happen if a previous `mousedown` occurred without a `mouseup`.\n // If we detect this, dispatch `mouseup` to complete the orphaned gesture,\n // so that d3-zoom won't stop propagation of new `mousedown` events.\n if (d3_event.type === 'mousedown') {\n var hasOrphan = false;\n var listeners = window.__on;\n for (var i = 0; i < listeners.length; i++) {\n var listener = listeners[i];\n if (listener.name === 'zoom' && listener.type === 'mouseup') {\n hasOrphan = true;\n break;\n }\n }\n if (hasOrphan) {\n const event = new Event('mouseup');\n // Event needs to be dispatched with an event.view property.\n event.view = window;\n window.dispatchEvent(event);\n }\n }\n\n return d3_event.button !== 2; // ignore right clicks\n }\n\n\n function pxCenter() {\n return [_dimensions[0] / 2, _dimensions[1] / 2];\n }\n\n\n function drawEditable(difference, extent) {\n var mode = context.mode();\n var graph = context.graph();\n var features = context.features();\n var all = context.history().intersects(map.extent());\n var fullRedraw = false;\n var data;\n var set;\n var filter;\n var applyFeatureLayerFilters = true;\n\n if (map.isInWideSelection()) {\n data = [];\n utilEntityAndDeepMemberIDs(mode.selectedIDs(), context.graph()).forEach(function(id) {\n var entity = context.hasEntity(id);\n if (entity) data.push(entity);\n });\n fullRedraw = true;\n filter = utilFunctor(true);\n // selected features should always be visible, so we can skip filtering\n applyFeatureLayerFilters = false;\n\n } else if (difference) {\n var complete = difference.complete(map.extent());\n data = Object.values(complete).filter(Boolean);\n set = new Set(Object.keys(complete));\n filter = function(d) { return set.has(d.id); };\n features.clear(data);\n\n } else {\n // force a full redraw if gatherStats detects that a feature\n // should be auto-hidden (e.g. points or buildings)..\n if (features.gatherStats(all, graph, _dimensions)) {\n extent = undefined;\n }\n\n if (extent) {\n data = context.history().intersects(map.extent().intersection(extent));\n set = new Set(data.map(function(entity) { return entity.id; }));\n filter = function(d) { return set.has(d.id); };\n\n } else {\n data = all;\n fullRedraw = true;\n filter = utilFunctor(true);\n }\n }\n\n if (applyFeatureLayerFilters) {\n data = features.filter(data, graph);\n } else {\n context.features().resetStats();\n }\n\n if (mode && mode.id === 'select') {\n // update selected vertices - the user might have just double-clicked a way,\n // creating a new vertex, triggering a partial redraw without a mode change\n surface.call(drawVertices.drawSelected, graph, map.extent());\n }\n\n surface\n .call(drawVertices, graph, data, filter, map.extent(), fullRedraw)\n .call(drawLines, graph, data, filter)\n .call(drawAreas, graph, data, filter)\n .call(drawMidpoints, graph, data, filter, map.trimmedExtent())\n .call(drawPoints, graph, data, filter)\n .call(drawLabels, graph, data, filter, _dimensions, fullRedraw);\n\n dispatch.call('drawn', this, {full: true});\n }\n\n map.init = function() {\n drawLayers = svgLayers(projection, context);\n drawPoints = svgPoints(projection, context);\n drawVertices = svgVertices(projection, context);\n drawLines = svgLines(projection, context);\n drawAreas = svgAreas(projection, context);\n drawMidpoints = svgMidpoints(projection, context);\n drawLabels = svgLabels(projection, context);\n };\n\n function editOff() {\n context.features().resetStats();\n surface.selectAll('.layer-osm *').remove();\n surface.selectAll('.layer-touch:not(.markers) *').remove();\n\n var allowed = {\n 'browse': true,\n 'save': true,\n 'select-note': true,\n 'select-data': true,\n 'select-error': true\n };\n\n var mode = context.mode();\n if (mode && !allowed[mode.id]) {\n context.enter(modeBrowse(context));\n }\n\n dispatch.call('drawn', this, {full: true});\n }\n\n\n\n\n\n function gestureChange(d3_event) {\n // Remap Safari gesture events to wheel events - #5492\n // We want these disabled most places, but enabled for zoom/unzoom on map surface\n // https://developer.mozilla.org/en-US/docs/Web/API/GestureEvent\n var e = d3_event;\n e.preventDefault();\n\n var props = {\n deltaMode: 0, // dummy values to ignore in zoomPan\n deltaY: 1, // dummy values to ignore in zoomPan\n clientX: e.clientX,\n clientY: e.clientY,\n screenX: e.screenX,\n screenY: e.screenY,\n x: e.x,\n y: e.y\n };\n\n var e2 = new WheelEvent('wheel', props);\n e2._scale = e.scale; // preserve the original scale\n e2._rotation = e.rotation; // preserve the original rotation\n\n _selection.node().dispatchEvent(e2);\n }\n\n\n function zoomPan(event, key, transform) {\n var source = event && event.sourceEvent || event;\n var eventTransform = transform || (event && event.transform);\n var x = eventTransform.x;\n var y = eventTransform.y;\n var k = eventTransform.k;\n\n // Special handling of 'wheel' events:\n // They might be triggered by the user scrolling the mouse wheel,\n // or 2-finger pinch/zoom gestures, the transform may need adjustment.\n if (source && source.type === 'wheel') {\n\n // assume that the gesture is already handled by pointer events\n if (_pointerDown) return;\n\n var detected = utilDetect();\n var dX = source.deltaX;\n var dY = source.deltaY;\n var x2 = x;\n var y2 = y;\n var k2 = k;\n var t0, p0, p1;\n\n // Normalize mousewheel scroll speed (Firefox) - #3029\n // If wheel delta is provided in LINE units, recalculate it in PIXEL units\n // We are essentially redoing the calculations that occur here:\n // https://github.com/d3/d3-zoom/blob/78563a8348aa4133b07cac92e2595c2227ca7cd7/src/zoom.js#L203\n // See this for more info:\n // https://github.com/basilfx/normalize-wheel/blob/master/src/normalizeWheel.js\n if (source.deltaMode === 1 /* LINE */) {\n // Convert from lines to pixels, more if the user is scrolling fast.\n // (I made up the exp function to roughly match Firefox to what Chrome does)\n // These numbers should be floats, because integers are treated as pan gesture below.\n var lines = Math.abs(source.deltaY);\n var sign = (source.deltaY > 0) ? 1 : -1;\n dY = sign * clamp(\n lines * 18.001,\n 4.000244140625, // min\n 350.000244140625 // max\n );\n\n // recalculate x2,y2,k2\n t0 = _isTransformed ? _transformLast : _transformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * Math.pow(2, -dY / 500);\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // 2 finger map pinch zooming (Safari) - #5492\n // These are fake `wheel` events we made from Safari `gesturechange` events..\n } else if (source._scale) {\n // recalculate x2,y2,k2\n t0 = _gestureTransformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * source._scale;\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // 2 finger map pinch zooming (all browsers except Safari) - #5492\n // Pinch zooming via the `wheel` event will always have:\n // - `ctrlKey = true`\n // - `deltaY` is not round integer pixels (ignore `deltaX`)\n } else if (source.ctrlKey && !isInteger(dY)) {\n dY *= 6; // slightly scale up whatever the browser gave us\n\n // recalculate x2,y2,k2\n t0 = _isTransformed ? _transformLast : _transformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * Math.pow(2, -dY / 500);\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // Trackpad scroll zooming with shift or alt/option key down\n } else if ((source.altKey || source.shiftKey) && isInteger(dY)) {\n // recalculate x2,y2,k2\n t0 = _isTransformed ? _transformLast : _transformStart;\n p0 = _getMouseCoords(source);\n p1 = t0.invert(p0);\n k2 = t0.k * Math.pow(2, -dY / 500);\n k2 = clamp(k2, kMin, kMax);\n x2 = p0[0] - p1[0] * k2;\n y2 = p0[1] - p1[1] * k2;\n\n // 2 finger map panning (Mac only, all browsers except Firefox #8595) - #5492, #5512\n // Panning via the `wheel` event will always have:\n // - `ctrlKey = false`\n // - `deltaX`,`deltaY` are round integer pixels\n } else if (detected.os === 'mac' && detected.browser !== 'Firefox' && !source.ctrlKey && isInteger(dX) && isInteger(dY)) {\n p1 = projection.translate();\n x2 = p1[0] - dX;\n y2 = p1[1] - dY;\n k2 = projection.scale();\n k2 = clamp(k2, kMin, kMax);\n }\n\n // something changed - replace the event transform\n if (x2 !== x || y2 !== y || k2 !== k) {\n x = x2;\n y = y2;\n k = k2;\n eventTransform = d3_zoomIdentity.translate(x2, y2).scale(k2);\n if (_zoomerPanner._transform) {\n // utilZoomPan interface\n _zoomerPanner._transform(eventTransform);\n } else {\n // d3_zoom interface\n _selection.node().__zoom = eventTransform;\n }\n }\n\n }\n\n if (_transformStart.x === x &&\n _transformStart.y === y &&\n _transformStart.k === k) {\n return; // no change\n }\n\n if (geoScaleToZoom(k, TILESIZE) < _minzoom) {\n surface.interrupt();\n dispatch.call('hitMinZoom', this, map);\n setCenterZoom(map.center(), context.minEditableZoom(), 0, true);\n scheduleRedraw();\n dispatch.call('move', this, map);\n return;\n }\n\n projection.transform(eventTransform);\n\n var withinEditableZoom = map.withinEditableZoom();\n if (_lastWithinEditableZoom !== withinEditableZoom) {\n if (_lastWithinEditableZoom !== undefined) {\n // notify that the map zoomed in or out over the editable zoom threshold\n dispatch.call('crossEditableZoom', this, withinEditableZoom);\n }\n _lastWithinEditableZoom = withinEditableZoom;\n }\n\n var scale = k / _transformStart.k;\n var tX = (x / scale - _transformStart.x) * scale;\n var tY = (y / scale - _transformStart.y) * scale;\n\n if (context.inIntro()) {\n curtainProjection.transform({\n x: x - tX,\n y: y - tY,\n k: k\n });\n }\n\n if (source) {\n _lastPointerEvent = event;\n }\n _isTransformed = true;\n _transformLast = eventTransform;\n utilSetTransform(supersurface, tX, tY, scale);\n scheduleRedraw();\n\n dispatch.call('move', this, map);\n\n\n function isInteger(val) {\n return typeof val === 'number' && isFinite(val) && Math.floor(val) === val;\n }\n }\n\n\n function resetTransform() {\n if (!_isTransformed) return false;\n\n utilSetTransform(supersurface, 0, 0);\n _isTransformed = false;\n if (context.inIntro()) {\n curtainProjection.transform(projection.transform());\n }\n return true;\n }\n\n\n function redraw(difference, extent) {\n // in unit tests, we need to abort if the test has already completed\n if (typeof window === 'undefined') return;\n\n if (surface.empty() || !_redrawEnabled) return;\n\n // If we are in the middle of a zoom/pan, we can't do differenced redraws.\n // It would result in artifacts where differenced entities are redrawn with\n // one transform and unchanged entities with another.\n if (resetTransform()) {\n difference = undefined;\n extent = undefined;\n }\n\n var zoom = map.zoom();\n var z = String(~~zoom);\n\n if (surface.attr('data-zoom') !== z) {\n surface.attr('data-zoom', z);\n }\n\n // class surface as `lowzoom` around z17-z18.5 (based on latitude)\n var lat = map.center()[1];\n var lowzoom = d3_scaleLinear()\n .domain([-60, 0, 60])\n .range([17, 18.5, 17])\n .clamp(true);\n\n surface\n .classed('low-zoom', zoom <= lowzoom(lat));\n\n\n if (!difference) {\n supersurface.call(context.background());\n wrapper.call(drawLayers);\n }\n\n // OSM\n if (map.editableDataEnabled() || map.isInWideSelection()) {\n context.loadTiles(projection);\n drawEditable(difference, extent);\n } else {\n editOff();\n }\n\n _transformStart = projection.transform();\n\n return map;\n }\n\n\n\n var immediateRedraw = function(difference, extent) {\n if (!difference && !extent) cancelPendingRedraw();\n redraw(difference, extent);\n };\n\n\n map.lastPointerEvent = function() {\n return _lastPointerEvent;\n };\n\n\n map.mouse = function(d3_event) {\n var event = d3_event || _lastPointerEvent;\n if (event) {\n var s;\n while ((s = event.sourceEvent)) { event = s; }\n return _getMouseCoords(event);\n }\n return null;\n };\n\n\n // returns Lng/Lat\n map.mouseCoordinates = function() {\n var coord = map.mouse() || pxCenter();\n return projection.invert(coord);\n };\n\n\n map.dblclickZoomEnable = function(val) {\n if (!arguments.length) return _dblClickZoomEnabled;\n _dblClickZoomEnabled = val;\n return map;\n };\n\n\n map.redrawEnable = function(val) {\n if (!arguments.length) return _redrawEnabled;\n _redrawEnabled = val;\n return map;\n };\n\n\n map.isTransformed = function() {\n return _isTransformed;\n };\n\n\n function setTransform(t2, duration, force) {\n var t = projection.transform();\n if (!force && t2.k === t.k && t2.x === t.x && t2.y === t.y) return false;\n\n if (duration) {\n _selection\n .transition()\n .duration(duration)\n .on('start', function() { map.startEase(); })\n .call(_zoomerPanner.transform, d3_zoomIdentity.translate(t2.x, t2.y).scale(t2.k));\n } else {\n projection.transform(t2);\n _transformStart = t2;\n _selection.call(_zoomerPanner.transform, _transformStart);\n }\n\n return true;\n }\n\n\n function setCenterZoom(loc2, z2, duration, force) {\n var c = map.center();\n var z = map.zoom();\n if (loc2[0] === c[0] && loc2[1] === c[1] && z2 === z && !force) return false;\n\n var proj = geoRawMercator().transform(projection.transform()); // copy projection\n\n var k2 = clamp(geoZoomToScale(z2, TILESIZE), kMin, kMax);\n proj.scale(k2);\n\n var t = proj.translate();\n var point = proj(loc2);\n\n var center = pxCenter();\n t[0] += center[0] - point[0];\n t[1] += center[1] - point[1];\n\n return setTransform(d3_zoomIdentity.translate(t[0], t[1]).scale(k2), duration, force);\n }\n\n\n map.pan = function(delta, duration) {\n var t = projection.translate();\n var k = projection.scale();\n\n t[0] += delta[0];\n t[1] += delta[1];\n\n if (duration) {\n _selection\n .transition()\n .duration(duration)\n .on('start', function() { map.startEase(); })\n .call(_zoomerPanner.transform, d3_zoomIdentity.translate(t[0], t[1]).scale(k));\n } else {\n projection.translate(t);\n _transformStart = projection.transform();\n _selection.call(_zoomerPanner.transform, _transformStart);\n dispatch.call('move', this, map);\n immediateRedraw();\n }\n\n return map;\n };\n\n\n map.dimensions = function(val) {\n if (!arguments.length) return _dimensions;\n\n _dimensions = val;\n drawLayers.dimensions(_dimensions);\n context.background().dimensions(_dimensions);\n projection.clipExtent([[0, 0], _dimensions]);\n _getMouseCoords = utilFastMouse(supersurface.node());\n\n scheduleRedraw();\n return map;\n };\n\n\n function zoomIn(delta) {\n setCenterZoom(map.center(), Math.trunc(map.zoom() + 0.45) + delta, 150, true);\n }\n\n function zoomOut(delta) {\n setCenterZoom(map.center(), Math.ceil(map.zoom() - 0.45) - delta, 150, true);\n }\n\n map.zoomIn = function() { zoomIn(1); };\n map.zoomInFurther = function() { zoomIn(4); };\n map.canZoomIn = function() { return map.zoom() < maxZoom; };\n\n map.zoomOut = function() { zoomOut(1); };\n map.zoomOutFurther = function() { zoomOut(4); };\n map.canZoomOut = function() { return map.zoom() > minZoom; };\n\n map.center = function(loc2) {\n if (!arguments.length) {\n return projection.invert(pxCenter());\n }\n\n if (setCenterZoom(loc2, map.zoom())) {\n dispatch.call('move', this, map);\n }\n\n scheduleRedraw();\n return map;\n };\n\n function trimmedCenter(loc, zoom) {\n var offset = [paneWidth() / 2, (footerHeight() - toolbarHeight()) / 2];\n\n var proj = geoRawMercator().transform(projection.transform()); // copy projection\n // use the target zoom to calculate the offset center\n proj.scale(geoZoomToScale(zoom, TILESIZE));\n\n var locPx = proj(loc);\n var offsetLocPx = [locPx[0] + offset[0], locPx[1] + offset[1]];\n var offsetLoc = proj.invert(offsetLocPx);\n\n return offsetLoc;\n };\n\n function paneWidth() {\n const openPane = context.container().select('.map-panes .map-pane.shown');\n if (!openPane.empty()) {\n return openPane.node().offsetWidth;\n }\n return 0;\n };\n\n function toolbarHeight() {\n const toolbar = context.container().select('.top-toolbar');\n return toolbar.node().offsetHeight;\n };\n\n function footerHeight() {\n const footer = context.container().select('.map-footer-bar');\n return footer.node().offsetHeight;\n }\n\n map.zoom = function(z2) {\n if (!arguments.length) {\n return Math.max(geoScaleToZoom(projection.scale(), TILESIZE), 0);\n }\n\n if (z2 < _minzoom) {\n surface.interrupt();\n dispatch.call('hitMinZoom', this, map);\n z2 = context.minEditableZoom();\n }\n\n if (setCenterZoom(map.center(), z2)) {\n dispatch.call('move', this, map);\n }\n\n scheduleRedraw();\n return map;\n };\n\n\n map.centerZoom = function(loc2, z2) {\n if (setCenterZoom(loc2, z2)) {\n dispatch.call('move', this, map);\n }\n\n scheduleRedraw();\n return map;\n };\n\n\n map.zoomTo = function(what) {\n return map.zoomToEase(what, 0);\n };\n\n\n map.centerEase = function(loc2, duration) {\n duration = duration || 250;\n setCenterZoom(loc2, map.zoom(), duration);\n return map;\n };\n\n\n map.zoomEase = function(z2, duration) {\n duration = duration || 250;\n setCenterZoom(map.center(), z2, duration, false);\n return map;\n };\n\n\n map.centerZoomEase = function(loc2, z2, duration) {\n duration = duration || 250;\n setCenterZoom(loc2, z2, duration, false);\n return map;\n };\n\n\n map.transformEase = function(t2, duration) {\n duration = duration || 250;\n setTransform(t2, duration, false /* don't force */);\n return map;\n };\n\n\n map.zoomToEase = function(what, duration) {\n let extent;\n if (what instanceof geoExtent) {\n // we've directly been given an extent\n extent = what;\n } else {\n // we're given one or more entities to zoom to\n if (!isArray(what)) what = [what];\n extent = what\n .map(entity => entity.extent(context.graph()))\n .reduce((a, b) => a.extend(b));\n }\n\n if (!isFinite(extent.area())) return map;\n\n var z = clamp(map.trimmedExtentZoom(extent), 0, 20);\n const loc = trimmedCenter(extent.center(), z);\n\n if (duration === 0) {\n return map.centerZoom(loc, z);\n } else {\n return map.centerZoomEase(loc, z, duration);\n }\n };\n\n\n map.startEase = function() {\n utilBindOnce(surface, _pointerPrefix + 'down.ease', function() {\n map.cancelEase();\n });\n return map;\n };\n\n\n map.cancelEase = function() {\n _selection.interrupt();\n return map;\n };\n\n\n map.extent = function(val) {\n if (!arguments.length) {\n return new geoExtent(\n projection.invert([0, _dimensions[1]]),\n projection.invert([_dimensions[0], 0])\n );\n } else {\n var extent = geoExtent(val);\n map.centerZoom(extent.center(), map.extentZoom(extent));\n }\n };\n\n\n map.trimmedExtent = function(val) {\n if (!arguments.length) {\n var headerY = 71;\n var footerY = 30;\n var pad = 10;\n return new geoExtent(\n projection.invert([pad, _dimensions[1] - footerY - pad]),\n projection.invert([_dimensions[0] - pad, headerY + pad])\n );\n } else {\n var extent = geoExtent(val);\n map.centerZoom(extent.center(), map.trimmedExtentZoom(extent));\n }\n };\n\n\n function calcExtentZoom(extent, dim) {\n var tl = projection([extent[0][0], extent[1][1]]);\n var br = projection([extent[1][0], extent[0][1]]);\n\n // Calculate maximum zoom that fits extent\n var hFactor = (br[0] - tl[0]) / dim[0];\n var vFactor = (br[1] - tl[1]) / dim[1];\n var hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2;\n var vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2;\n var newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff);\n\n return newZoom;\n }\n\n\n map.extentZoom = function(val) {\n return calcExtentZoom(geoExtent(val), _dimensions);\n };\n\n\n map.trimmedExtentZoom = function(val) {\n const trim = 40;\n const trimmed = [\n _dimensions[0] - trim - paneWidth(),\n _dimensions[1] - trim - toolbarHeight() - footerHeight()\n ];\n return calcExtentZoom(geoExtent(val), trimmed);\n };\n\n\n map.withinEditableZoom = function() {\n return map.zoom() >= context.minEditableZoom();\n };\n\n\n map.isInWideSelection = function() {\n return !map.withinEditableZoom() && context.selectedIDs().length;\n };\n\n\n map.editableDataEnabled = function(skipZoomCheck) {\n\n var layer = context.layers().layer('osm');\n if (!layer || !layer.enabled()) return false;\n\n return skipZoomCheck || map.withinEditableZoom();\n };\n\n\n map.notesEditable = function() {\n var layer = context.layers().layer('notes');\n if (!layer || !layer.enabled()) return false;\n\n return map.withinEditableZoom();\n };\n\n\n map.minzoom = function(val) {\n if (!arguments.length) return _minzoom;\n _minzoom = val;\n return map;\n };\n\n\n map.toggleHighlightEdited = function() {\n surface.classed('highlight-edited', !surface.classed('highlight-edited'));\n map.pan([0,0]); // trigger a redraw\n dispatch.call('changeHighlighting', this);\n };\n\n\n map.areaFillOptions = ['wireframe', 'partial', 'full'];\n\n map.activeAreaFill = function(val) {\n if (!arguments.length) return prefs('area-fill') || 'partial';\n\n prefs('area-fill', val);\n if (val !== 'wireframe') {\n prefs('area-fill-toggle', val);\n }\n updateAreaFill();\n map.pan([0,0]); // trigger a redraw\n dispatch.call('changeAreaFill', this);\n return map;\n };\n\n map.toggleWireframe = function() {\n\n var activeFill = map.activeAreaFill();\n\n if (activeFill === 'wireframe') {\n activeFill = prefs('area-fill-toggle') || 'partial';\n } else {\n activeFill = 'wireframe';\n }\n\n map.activeAreaFill(activeFill);\n };\n\n function updateAreaFill() {\n var activeFill = map.activeAreaFill();\n map.areaFillOptions.forEach(function(opt) {\n surface.classed('fill-' + opt, Boolean(opt === activeFill));\n });\n }\n\n\n map.layers = () => drawLayers;\n\n\n map.doubleUpHandler = function() {\n return _doubleUpHandler;\n };\n\n\n return utilRebind(map, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { services } from '../services';\nimport { utilRebind } from '../util/rebind';\nimport { utilQsString, utilStringQs } from '../util';\n\n\nexport function rendererPhotos(context) {\n var dispatch = d3_dispatch('change');\n var _layerIDs = ['streetside', 'mapillary', 'mapillary-map-features', 'mapillary-signs', 'kartaview', 'mapilio', 'vegbilder', 'panoramax'];\n var _allPhotoTypes = ['flat', 'panoramic'];\n var _shownPhotoTypes = _allPhotoTypes.slice(); // shallow copy\n var _dateFilters = ['fromDate', 'toDate'];\n var _fromDate;\n var _toDate;\n var _usernames;\n\n function photos() {}\n\n function updateStorage() {\n var hash = utilStringQs(window.location.hash);\n var enabled = context.layers().all().filter(function(d) {\n return _layerIDs.indexOf(d.id) !== -1 && d.layer && d.layer.supported() && d.layer.enabled();\n }).map(function(d) {\n return d.id;\n });\n if (enabled.length) {\n hash.photo_overlay = enabled.join(',');\n } else {\n delete hash.photo_overlay;\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n }\n\n /**\n * @returns The layer ID\n */\n photos.overlayLayerIDs = function() {\n return _layerIDs;\n };\n\n /**\n * @returns All the photo types\n */\n photos.allPhotoTypes = function() {\n return _allPhotoTypes;\n };\n\n /**\n * @returns The date filters value\n */\n photos.dateFilters = function() {\n return _dateFilters;\n };\n\n photos.dateFilterValue = function(val) {\n return val === _dateFilters[0] ? _fromDate : _toDate;\n };\n\n /**\n * Sets the date filter (min/max date)\n * @param {*} type Either 'fromDate' or 'toDate'\n * @param {*} val The actual Date\n * @param {boolean} updateUrl Whether the URL should update or not\n */\n photos.setDateFilter = function(type, val, updateUrl) {\n // validate the date\n var date = val && new Date(val);\n if (date && !isNaN(date)) {\n val = date.toISOString().slice(0, 10);\n } else {\n val = null;\n }\n if (type === _dateFilters[0]) {\n _fromDate = val;\n if (_fromDate && _toDate && new Date(_toDate) < new Date(_fromDate)) {\n _toDate = _fromDate;\n }\n }\n if (type === _dateFilters[1]) {\n _toDate = val;\n if (_fromDate && _toDate && new Date(_toDate) < new Date(_fromDate)) {\n _fromDate = _toDate;\n }\n }\n dispatch.call('change', this);\n if (updateUrl) {\n var rangeString;\n if (_fromDate || _toDate) {\n rangeString = (_fromDate || '') + '_' + (_toDate || '');\n }\n setUrlFilterValue('photo_dates', rangeString);\n }\n };\n\n /**\n * Sets the username filter\n * @param {string} val The username\n * @param {boolean} updateUrl Whether the URL should update or not\n */\n photos.setUsernameFilter = function(val, updateUrl) {\n if (val && typeof val === 'string') val = val.replace(/;/g, ',').split(',');\n if (val) {\n val = val.map(d => d.trim()).filter(Boolean);\n if (!val.length) {\n val = null;\n }\n }\n _usernames = val;\n dispatch.call('change', this);\n if (updateUrl) {\n var hashString;\n if (_usernames) {\n hashString = _usernames.join(',');\n }\n setUrlFilterValue('photo_username', hashString);\n }\n };\n\n /**\n * Util function to set the slider date filter\n * @param {*} val Either 'panoramic' or 'flat'\n * @param {boolean} updateUrl Whether the URL should update or not\n */\n photos.togglePhotoType = function(val, updateUrl) {\n var index = _shownPhotoTypes.indexOf(val);\n if (index !== -1) {\n _shownPhotoTypes.splice(index, 1);\n } else {\n _shownPhotoTypes.push(val);\n }\n\n if (updateUrl) {\n var hashString;\n if (_shownPhotoTypes) {\n hashString = _shownPhotoTypes.join(',');\n }\n setUrlFilterValue('photo_type', hashString);\n }\n\n dispatch.call('change', this);\n return photos;\n };\n\n /**\n * Updates the URL with new values\n * @param {*} val value to save\n * @param {string} property Name of the value\n */\n function setUrlFilterValue(property, val) {\n const hash = utilStringQs(window.location.hash);\n if (val) {\n if (hash[property] === val) return;\n hash[property] = val;\n } else {\n if (!(property in hash)) return;\n delete hash[property];\n }\n window.history.replaceState(null, '', '#' + utilQsString(hash, true));\n }\n\n function showsLayer(id) {\n var layer = context.layers().layer(id);\n return layer && layer.supported() && layer.enabled();\n }\n\n /**\n * @returns If the Date Slider filter should be drawn\n */\n photos.shouldFilterDateBySlider = function(){\n return showsLayer('mapillary') || showsLayer('kartaview') || showsLayer('mapilio')\n || showsLayer('streetside') || showsLayer('vegbilder') || showsLayer('panoramax');\n };\n\n /**\n * @returns If the Photo Type filter should be drawn\n */\n photos.shouldFilterByPhotoType = function() {\n return showsLayer('mapillary') ||\n (showsLayer('streetside') && showsLayer('kartaview')) || showsLayer('vegbilder') || showsLayer('panoramax');\n };\n\n /**\n * @returns If the Username filter should be drawn\n */\n photos.shouldFilterByUsername = function() {\n return !showsLayer('mapillary') && showsLayer('kartaview') && !showsLayer('streetside') || showsLayer('panoramax');\n };\n\n photos.showsPhotoType = function(val) {\n if (!photos.shouldFilterByPhotoType()) return true;\n\n return _shownPhotoTypes.indexOf(val) !== -1;\n };\n\n photos.showsFlat = function() {\n return photos.showsPhotoType('flat');\n };\n\n photos.showsPanoramic = function() {\n return photos.showsPhotoType('panoramic');\n };\n\n photos.fromDate = function() {\n return _fromDate;\n };\n\n photos.toDate = function() {\n return _toDate;\n };\n\n photos.usernames = function() {\n return _usernames;\n };\n\n /**\n * Inits the streetlevel layer given the saved values in the URL\n */\n photos.init = function() {\n var hash = utilStringQs(window.location.hash);\n var parts;\n if (hash.photo_dates) {\n // expect format like `photo_dates=2019-01-01_2020-12-31`, but allow a couple different separators\n parts = /^(.*)[\u2013_](.*)$/g.exec(hash.photo_dates.trim());\n this.setDateFilter('fromDate', parts && parts.length >= 2 && parts[1], false);\n this.setDateFilter('toDate', parts && parts.length >= 3 && parts[2], false);\n }\n if (hash.photo_username) {\n this.setUsernameFilter(hash.photo_username, false);\n }\n if (hash.photo_type) {\n parts = hash.photo_type.replace(/;/g, ',').split(',');\n _allPhotoTypes.forEach(d => {\n if (!parts.includes(d)) this.togglePhotoType(d, false);\n });\n }\n if (hash.photo_overlay) {\n // support enabling photo layers by default via a URL parameter, e.g. `photo_overlay=kartaview;mapillary;streetside`\n var hashOverlayIDs = hash.photo_overlay.replace(/;/g, ',').split(',');\n hashOverlayIDs.forEach(function(id) {\n if (id === 'openstreetcam') id = 'kartaview'; // legacy alias\n var layer = _layerIDs.indexOf(id) !== -1 && context.layers().layer(id);\n if (layer && !layer.enabled()) layer.enabled(true);\n });\n }\n if (hash.photo) {\n // support opening a photo via a URL parameter, e.g. `photo=mapillary-fztgSDtLpa08ohPZFZjeRQ`\n var photoIds = hash.photo.replace(/;/g, ',').split(',');\n var photoId = photoIds.length && photoIds[0].trim();\n var results = /(.*)\\/(.*)/g.exec(photoId);\n if (results && results.length >= 3) {\n var serviceId = results[1];\n if (serviceId === 'openstreetcam') serviceId = 'kartaview'; // legacy alias\n var photoKey = results[2];\n var service = services[serviceId];\n if (service && service.ensureViewerLoaded) {\n\n // if we're showing a photo then make sure its layer is enabled too\n var layer = _layerIDs.indexOf(serviceId) !== -1 && context.layers().layer(serviceId);\n if (layer && !layer.enabled()) layer.enabled(true);\n\n var baselineTime = Date.now();\n\n service.on('loadedImages.rendererPhotos', function() {\n // don't open the viewer if too much time has elapsed\n if (Date.now() - baselineTime > 45000) {\n service.on('loadedImages.rendererPhotos', null);\n return;\n }\n\n if (!service.cachedImage(photoKey)) return;\n\n service.on('loadedImages.rendererPhotos', null);\n service.ensureViewerLoaded(context)\n .then(function() {\n service\n .selectImage(context, photoKey)\n .showViewer(context);\n });\n });\n }\n }\n }\n\n context.layers().on('change.rendererPhotos', updateStorage);\n };\n\n return utilRebind(photos, dispatch, 'on');\n}\n", "export { rendererBackgroundSource } from './background_source';\nexport { rendererBackground } from './background';\nexport { rendererFeatures } from './features';\nexport { rendererMap } from './map';\nexport { rendererPhotos } from './photos';\nexport { rendererTileLayer } from './tile_layer';\n", "import { geoPath as d3_geoPath } from 'd3-geo';\nimport { select as d3_select } from 'd3-selection';\nimport { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';\n\nimport { t } from '../core/localizer';\nimport { geoRawMercator, geoScaleToZoom, geoVecSubtract, geoVecScale, geoZoomToScale } from '../geo';\nimport { rendererTileLayer } from '../renderer';\nimport { svgDebug, svgData } from '../svg';\nimport { utilSetTransform } from '../util';\n// import { utilGetDimensions } from '../util/dimensions';\n\n\nexport function uiMapInMap(context) {\n\n function mapInMap(selection) {\n var backgroundLayer = rendererTileLayer(context)\n .underzoom(2);\n var overlayLayers = {};\n var projection = geoRawMercator();\n var dataLayer = svgData(projection, context).showLabels(false);\n var debugLayer = svgDebug(projection, context);\n var zoom = d3_zoom()\n .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(24)])\n .on('start', zoomStarted)\n .on('zoom', zoomed)\n .on('end', zoomEnded);\n\n var wrap = d3_select(null);\n var tiles = d3_select(null);\n var viewport = d3_select(null);\n\n var _isTransformed = false;\n var _isHidden = true;\n var _skipEvents = false;\n var _gesture = null;\n var _zDiff = 6; // by default, minimap renders at (main zoom - 6)\n var _dMini; // dimensions of minimap\n var _cMini; // center pixel of minimap\n var _tStart; // transform at start of gesture\n var _tCurr; // transform at most recent event\n var _timeoutID;\n\n\n function zoomStarted() {\n if (_skipEvents) return;\n _tCurr = projection.transform();\n _tStart = _tCurr;\n _gesture = null;\n }\n\n\n function zoomed(d3_event) {\n if (_skipEvents) return;\n\n var x = d3_event.transform.x;\n var y = d3_event.transform.y;\n var k = d3_event.transform.k;\n var isZooming = (k !== _tStart.k);\n var isPanning = (x !== _tStart.x || y !== _tStart.y);\n\n if (!isZooming && !isPanning) {\n return; // no change\n }\n\n // lock in either zooming or panning, don't allow both in minimap.\n if (!_gesture) {\n _gesture = isZooming ? 'zoom' : 'pan';\n }\n\n var tMini = projection.transform();\n var tX, tY, scale;\n\n if (_gesture === 'zoom') {\n scale = k / tMini.k;\n tX = (_cMini[0] / scale - _cMini[0]) * scale;\n tY = (_cMini[1] / scale - _cMini[1]) * scale;\n } else {\n k = tMini.k;\n scale = 1;\n tX = x - tMini.x;\n tY = y - tMini.y;\n }\n\n utilSetTransform(tiles, tX, tY, scale);\n utilSetTransform(viewport, 0, 0, scale);\n _isTransformed = true;\n _tCurr = d3_zoomIdentity.translate(x, y).scale(k);\n\n var zMain = geoScaleToZoom(context.projection.scale());\n var zMini = geoScaleToZoom(k);\n\n _zDiff = zMain - zMini;\n\n queueRedraw();\n }\n\n\n function zoomEnded() {\n if (_skipEvents) return;\n if (_gesture !== 'pan') return;\n\n updateProjection();\n _gesture = null;\n context.map().center(projection.invert(_cMini)); // recenter main map..\n }\n\n\n function updateProjection() {\n var loc = context.map().center();\n var tMain = context.projection.transform();\n var zMain = geoScaleToZoom(tMain.k);\n var zMini = Math.max(zMain - _zDiff, 0.5);\n var kMini = geoZoomToScale(zMini);\n\n projection\n .translate([tMain.x, tMain.y])\n .scale(kMini);\n\n var point = projection(loc);\n var mouse = (_gesture === 'pan') ? geoVecSubtract([_tCurr.x, _tCurr.y], [_tStart.x, _tStart.y]) : [0, 0];\n var xMini = _cMini[0] - point[0] + tMain.x + mouse[0];\n var yMini = _cMini[1] - point[1] + tMain.y + mouse[1];\n\n projection\n .translate([xMini, yMini])\n .clipExtent([[0, 0], _dMini]);\n\n _tCurr = projection.transform();\n\n if (_isTransformed) {\n utilSetTransform(tiles, 0, 0);\n utilSetTransform(viewport, 0, 0);\n _isTransformed = false;\n }\n\n zoom\n .scaleExtent([geoZoomToScale(0.5), geoZoomToScale(zMain - 3)]);\n\n _skipEvents = true;\n wrap.call(zoom.transform, _tCurr);\n _skipEvents = false;\n }\n\n\n function redraw() {\n clearTimeout(_timeoutID);\n if (_isHidden) return;\n\n updateProjection();\n var zMini = geoScaleToZoom(projection.scale());\n\n // setup tile container\n tiles = wrap\n .selectAll('.map-in-map-tiles')\n .data([0]);\n\n tiles = tiles.enter()\n .append('div')\n .attr('class', 'map-in-map-tiles')\n .merge(tiles);\n\n // redraw background\n backgroundLayer\n .source(context.background().baseLayerSource())\n .projection(projection)\n .dimensions(_dMini);\n\n var background = tiles\n .selectAll('.map-in-map-background')\n .data([0]);\n\n background.enter()\n .append('div')\n .attr('class', 'map-in-map-background')\n .merge(background)\n .call(backgroundLayer);\n\n\n // redraw overlay\n var overlaySources = context.background().overlayLayerSources();\n var activeOverlayLayers = [];\n for (var i = 0; i < overlaySources.length; i++) {\n if (overlaySources[i].validZoom(zMini)) {\n if (!overlayLayers[i]) overlayLayers[i] = rendererTileLayer(context);\n activeOverlayLayers.push(overlayLayers[i]\n .source(overlaySources[i])\n .projection(projection)\n .dimensions(_dMini));\n }\n }\n\n var overlay = tiles\n .selectAll('.map-in-map-overlay')\n .data([0]);\n\n overlay = overlay.enter()\n .append('div')\n .attr('class', 'map-in-map-overlay')\n .merge(overlay);\n\n\n var overlays = overlay\n .selectAll('div')\n .data(activeOverlayLayers, function(d) { return d.source().name(); });\n\n overlays.exit()\n .remove();\n\n overlays.enter()\n .append('div')\n .merge(overlays)\n .each(function(layer) { d3_select(this).call(layer); });\n\n\n var dataLayers = tiles\n .selectAll('.map-in-map-data')\n .data([0]);\n\n dataLayers.exit()\n .remove();\n\n dataLayers.enter()\n .append('svg')\n .attr('class', 'map-in-map-data')\n .merge(dataLayers)\n .call(dataLayer)\n .call(debugLayer);\n\n\n // redraw viewport bounding box\n if (_gesture !== 'pan') {\n var getPath = d3_geoPath(projection);\n var bbox = { type: 'Polygon', coordinates: [context.map().extent().polygon()] };\n\n viewport = wrap.selectAll('.map-in-map-viewport')\n .data([0]);\n\n viewport = viewport.enter()\n .append('svg')\n .attr('class', 'map-in-map-viewport')\n .merge(viewport);\n\n\n var path = viewport.selectAll('.map-in-map-bbox')\n .data([bbox]);\n\n path.enter()\n .append('path')\n .attr('class', 'map-in-map-bbox')\n .merge(path)\n .attr('d', getPath)\n .classed('thick', function(d) { return getPath.area(d) < 30; });\n }\n }\n\n\n function queueRedraw() {\n clearTimeout(_timeoutID);\n _timeoutID = setTimeout(function() { redraw(); }, 750);\n }\n\n\n function toggle(d3_event) {\n if (d3_event) d3_event.preventDefault();\n\n _isHidden = !_isHidden;\n\n context.container().select('.minimap-toggle-item')\n .classed('active', !_isHidden)\n .select('input')\n .property('checked', !_isHidden);\n\n if (_isHidden) {\n wrap\n .style('display', 'block')\n .style('opacity', '1')\n .transition()\n .duration(200)\n .style('opacity', '0')\n .on('end', function() {\n selection.selectAll('.map-in-map')\n .style('display', 'none');\n });\n } else {\n wrap\n .style('display', 'block')\n .style('opacity', '0')\n .transition()\n .duration(200)\n .style('opacity', '1')\n .on('end', function() {\n redraw();\n });\n }\n }\n\n\n uiMapInMap.toggle = toggle;\n\n wrap = selection.selectAll('.map-in-map')\n .data([0]);\n\n wrap = wrap.enter()\n .append('div')\n .attr('class', 'map-in-map')\n .style('display', (_isHidden ? 'none' : 'block'))\n .call(zoom)\n .on('dblclick.zoom', null)\n .merge(wrap);\n\n // reflow warning: Hardcode dimensions - currently can't resize it anyway..\n _dMini = [200,150]; //utilGetDimensions(wrap);\n _cMini = geoVecScale(_dMini, 0.5);\n\n context.map()\n .on('drawn.map-in-map', function(drawn) {\n if (drawn.full === true) {\n redraw();\n }\n });\n\n redraw();\n\n context.keybinding()\n .on(t('background.minimap.key'), toggle);\n }\n\n return mapInMap;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/index';\n\n\nexport function uiNotice(context) {\n\n return function(selection) {\n var div = selection\n .append('div')\n .attr('class', 'notice');\n\n var button = div\n .append('button')\n .attr('class', 'zoom-to notice fillD')\n .on('click', function() {\n context.map().zoomEase(context.minEditableZoom());\n })\n .on('wheel', function(d3_event) { // let wheel events pass through #4482\n var e2 = new WheelEvent(d3_event.type, d3_event);\n context.surface().node().dispatchEvent(e2);\n });\n\n button\n .call(svgIcon('#iD-icon-plus', 'pre-text'))\n .append('span')\n .attr('class', 'label')\n .call(t.append('zoom_in_edit'));\n\n\n function disableTooHigh() {\n var canEdit = context.map().zoom() >= context.minEditableZoom();\n div.style('display', canEdit ? 'none' : 'block');\n }\n\n context.map()\n .on('move.notice', debounce(disableTooHigh, 500));\n\n disableTooHigh();\n };\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\nimport { clamp } from 'es-toolkit/compat';\n\nimport { t } from '../core/localizer';\nimport { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { svgIcon } from '../svg/icon';\nimport { utilGetDimensions } from '../util/dimensions';\nimport { utilRebind } from '../util';\nimport { services } from '../services';\nimport { uiTooltip } from './tooltip';\nimport { actionChangeTags } from '../actions';\nimport { geoSphericalDistance } from '../geo';\n\nexport function uiPhotoviewer(context) {\n\n var dispatch = d3_dispatch('resize');\n\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n const addPhotoIdButton = new Set(['mapillary', 'panoramax']);\n\n function photoviewer(selection) {\n selection\n .append('button')\n .attr('class', 'thumb-hide')\n .attr('title', t('icons.close'))\n .on('click', function () {\n for (const service of Object.values(services)) {\n if (typeof service.hideViewer === 'function') {\n service.hideViewer(context);\n }\n }\n })\n .append('div')\n .call(svgIcon('#iD-icon-close'));\n\n function preventDefault(d3_event) {\n d3_event.preventDefault();\n }\n\n selection\n .append('button')\n .attr('class', 'resize-handle-xy')\n .on('touchstart touchdown touchend', preventDefault)\n .on(\n _pointerPrefix + 'down',\n buildResizeListener(selection, 'resize', dispatch, { resizeOnX: true, resizeOnY: true })\n );\n\n selection\n .append('button')\n .attr('class', 'resize-handle-x')\n .on('touchstart touchdown touchend', preventDefault)\n .on(\n _pointerPrefix + 'down',\n buildResizeListener(selection, 'resize', dispatch, { resizeOnX: true })\n );\n\n selection\n .append('button')\n .attr('class', 'resize-handle-y')\n .on('touchstart touchdown touchend', preventDefault)\n .on(\n _pointerPrefix + 'down',\n buildResizeListener(selection, 'resize', dispatch, { resizeOnY: true })\n );\n\n // update sett_photo_from_viewer button on selection change and when tags change\n context.features().on('change.setPhotoFromViewer', function() {\n setPhotoTagButton();\n });\n context.history().on('change.setPhotoFromViewer', function() {\n setPhotoTagButton();\n });\n\n\n function setPhotoTagButton() {\n const service = getServiceId();\n const isActiveForService = addPhotoIdButton.has(service) &&\n services[service].isViewerOpen() &&\n layerEnabled(service) &&\n context.mode().id === 'select';\n\n renderAddPhotoIdButton(service, isActiveForService);\n\n function layerEnabled(which) {\n const layers = context.layers();\n const layer = layers.layer(which);\n return layer.enabled();\n }\n\n function getServiceId() {\n for (const serviceId in services) {\n const service = services[serviceId];\n if (typeof service.isViewerOpen === 'function') {\n if (service.isViewerOpen()) {\n return serviceId;\n }\n }\n }\n return false;\n }\n\n function renderAddPhotoIdButton(service, shouldDisplay) {\n const button = selection.selectAll('.set-photo-from-viewer')\n .data(shouldDisplay ? [0] : []);\n\n button.exit()\n .remove();\n\n const buttonEnter = button.enter()\n .append('button')\n .attr('class', 'set-photo-from-viewer')\n .call(svgIcon('#fas-eye-dropper'))\n .call(uiTooltip()\n .title(() => t.append('inspector.set_photo_from_viewer.enable'))\n .placement('right')\n );\n\n buttonEnter\n .select('.tooltip')\n .classed('dark', true)\n .style('width', '300px');\n\n buttonEnter\n .merge(button)\n .on('click', function (e) {\n e.preventDefault();\n e.stopPropagation();\n const activeServiceId = getServiceId();\n const image = services[activeServiceId].getActiveImage();\n\n const action = graph =>\n context.selectedIDs().reduce((graph, entityID) => {\n const tags = graph.entity(entityID).tags;\n const action = actionChangeTags(entityID, {...tags, [activeServiceId]: image.id});\n return action(graph);\n }, graph);\n\n const annotation = t('operations.change_tags.annotation');\n context.perform(action, annotation);\n buttonDisable('already_set');\n });\n\n if (service === 'panoramax') {\n const panoramaxControls = selection.select('.panoramax-wrapper .pnlm-zoom-controls.pnlm-controls');\n\n panoramaxControls\n .style('margin-top', shouldDisplay ? '36px' : '6px');\n }\n\n if (!shouldDisplay) return;\n\n const activeImage = services[service].getActiveImage();\n\n const graph = context.graph();\n const entities = context.selectedIDs()\n .map(id => graph.hasEntity(id))\n .filter(Boolean);\n\n if (entities.map(entity => entity.tags[service])\n .every(value => value === activeImage?.id)) {\n buttonDisable('already_set');\n } else if (activeImage && entities\n .map(entity => entity.extent(context.graph()).center())\n .every(loc => geoSphericalDistance(loc, activeImage.loc) > 100)) {\n buttonDisable('too_far');\n } else {\n buttonDisable(false);\n }\n }\n\n function buttonDisable(reason) {\n const disabled = reason !== false;\n const button = selection.selectAll('.set-photo-from-viewer').data([0]);\n button.attr('disabled', disabled ? 'true' : null);\n button.classed('disabled', disabled);\n button.call(uiTooltip().destroyAny);\n if (disabled) {\n button.call(uiTooltip()\n .title(() => t.append(`inspector.set_photo_from_viewer.disable.${reason}`))\n .placement('right')\n );\n } else {\n button.call(uiTooltip()\n .title(() => t.append('inspector.set_photo_from_viewer.enable'))\n .placement('right')\n );\n }\n\n button.select('.tooltip')\n .classed('dark', true)\n .style('width', '300px');\n }\n }\n\n function buildResizeListener(target, eventName, dispatch, options) {\n\n var resizeOnX = !!options.resizeOnX;\n var resizeOnY = !!options.resizeOnY;\n var minHeight = options.minHeight || 240;\n var minWidth = options.minWidth || 320;\n var pointerId;\n var startX;\n var startY;\n var startWidth;\n var startHeight;\n\n function startResize(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n var mapSize = context.map().dimensions();\n\n if (resizeOnX) {\n var mapWidth = mapSize[0];\n const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-left'), 10);\n var newWidth = clamp((startWidth + d3_event.clientX - startX), minWidth, mapWidth - viewerMargin * 2);\n target.style('width', newWidth + 'px');\n }\n\n if (resizeOnY) {\n const menuHeight = utilGetDimensions(d3_select('.top-toolbar'))[1] +\n utilGetDimensions(d3_select('.map-footer'))[1];\n const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-bottom'), 10);\n var maxHeight = mapSize[1] - menuHeight - viewerMargin * 2; // preserve space at top/bottom of map\n var newHeight = clamp((startHeight + startY - d3_event.clientY), minHeight, maxHeight);\n target.style('height', newHeight + 'px');\n }\n\n dispatch.call(eventName, target, subtractPadding(utilGetDimensions(target, true), target));\n }\n\n function stopResize(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n // remove all the listeners we added\n d3_select(window)\n .on('.' + eventName, null);\n }\n\n return function initResize(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n\n pointerId = d3_event.pointerId || 'mouse';\n\n startX = d3_event.clientX;\n startY = d3_event.clientY;\n var targetRect = target.node().getBoundingClientRect();\n startWidth = targetRect.width;\n startHeight = targetRect.height;\n\n d3_select(window)\n .on(_pointerPrefix + 'move.' + eventName, startResize, false)\n .on(_pointerPrefix + 'up.' + eventName, stopResize, false);\n\n if (_pointerPrefix === 'pointer') {\n d3_select(window)\n .on('pointercancel.' + eventName, stopResize, false);\n }\n };\n }\n }\n\n photoviewer.onMapResize = function() {\n var photoviewer = context.container().select('.photoviewer');\n var content = context.container().select('.main-content');\n var mapDimensions = utilGetDimensions(content, true);\n const menuHeight = utilGetDimensions(d3_select('.top-toolbar'))[1] +\n utilGetDimensions(d3_select('.map-footer'))[1];\n const viewerMargin = parseInt(d3_select('.photoviewer').style('margin-bottom'), 10);\n // shrink photo viewer if it is too big (preserves space at top and bottom of map used by menus)\n var photoDimensions = utilGetDimensions(photoviewer, true);\n if (photoDimensions[0] > mapDimensions[0] || photoDimensions[1] > (mapDimensions[1] - menuHeight - viewerMargin * 2)) {\n var setPhotoDimensions = [\n Math.min(photoDimensions[0], mapDimensions[0]),\n Math.min(photoDimensions[1], mapDimensions[1] - menuHeight - viewerMargin * 2),\n ];\n\n photoviewer\n .style('width', setPhotoDimensions[0] + 'px')\n .style('height', setPhotoDimensions[1] + 'px');\n\n dispatch.call('resize', photoviewer, subtractPadding(setPhotoDimensions, photoviewer));\n } else {\n dispatch.call('resize', photoviewer, subtractPadding(photoDimensions, photoviewer));\n }\n };\n\n function subtractPadding(dimensions, selection) {\n return [\n dimensions[0] - parseFloat(selection.style('padding-left')) - parseFloat(selection.style('padding-right')),\n dimensions[1] - parseFloat(selection.style('padding-top')) - parseFloat(selection.style('padding-bottom'))\n ];\n }\n\n photoviewer.viewerSize = function() {\n const photoviewer = context.container().select('.photoviewer');\n return subtractPadding(utilGetDimensions(photoviewer, true), photoviewer);\n };\n\n return utilRebind(photoviewer, dispatch, 'on');\n}\n", "import { t } from '../core/localizer';\nimport { uiModal } from './modal';\n\n\nexport function uiRestore(context) {\n return function(selection) {\n if (!context.history().hasRestorableChanges()) return;\n\n let modalSelection = uiModal(selection, true);\n\n modalSelection.select('.modal')\n .attr('class', 'modal fillL');\n\n let introModal = modalSelection.select('.content');\n\n introModal\n .append('div')\n .attr('class', 'modal-section')\n .append('h3')\n .call(t.append('restore.heading'));\n\n introModal\n .append('div')\n .attr('class','modal-section')\n .append('p')\n .call(t.append('restore.description'));\n\n let buttonWrap = introModal\n .append('div')\n .attr('class', 'modal-actions');\n\n let restore = buttonWrap\n .append('button')\n .attr('class', 'restore')\n .on('click', () => {\n context.history().restore();\n modalSelection.remove();\n });\n\n restore\n .append('svg')\n .attr('class', 'logo logo-restore')\n .append('use')\n .attr('xlink:href', '#iD-logo-restore');\n\n restore\n .append('div')\n .call(t.append('restore.restore'));\n\n let reset = buttonWrap\n .append('button')\n .attr('class', 'reset')\n .on('click', () => {\n context.history().clearSaved();\n modalSelection.remove();\n });\n\n reset\n .append('svg')\n .attr('class', 'logo logo-reset')\n .append('use')\n .attr('xlink:href', '#iD-logo-reset');\n\n reset\n .append('div')\n .call(t.append('restore.reset'));\n\n restore.node().focus();\n };\n}\n", "import { displayLength } from '../util/units';\nimport { geoLonToMeters, geoMetersToLon } from '../geo';\nimport { localizer } from '../core/localizer';\n\n\nexport function uiScale(context) {\n var projection = context.projection,\n isImperial = !localizer.usesMetric(),\n maxLength = 180,\n tickHeight = 8;\n\n\n function scaleDefs(loc1, loc2) {\n var lat = (loc2[1] + loc1[1]) / 2,\n conversion = (isImperial ? 3.28084 : 1),\n dist = geoLonToMeters(loc2[0] - loc1[0], lat) * conversion,\n scale = { dist: 0, px: 0, text: '' },\n buckets, i, val, dLon;\n\n if (isImperial) {\n buckets = [5280000, 528000, 52800, 5280, 500, 50, 5, 1];\n } else {\n buckets = [5000000, 500000, 50000, 5000, 500, 50, 5, 1];\n }\n\n // determine a user-friendly endpoint for the scale\n for (i = 0; i < buckets.length; i++) {\n val = buckets[i];\n if (dist >= val) {\n scale.dist = Math.floor(dist / val) * val;\n break;\n } else {\n scale.dist = +dist.toFixed(2);\n }\n }\n\n dLon = geoMetersToLon(scale.dist / conversion, lat);\n scale.px = Math.round(projection([loc1[0] + dLon, loc1[1]])[0]);\n\n scale.text = displayLength(scale.dist / conversion, isImperial);\n\n return scale;\n }\n\n\n function update(selection) {\n // choose loc1, loc2 along bottom of viewport (near where the scale will be drawn)\n var dims = context.map().dimensions(),\n loc1 = projection.invert([0, dims[1]]),\n loc2 = projection.invert([maxLength, dims[1]]),\n scale = scaleDefs(loc1, loc2);\n\n selection.select('.scale-path')\n .attr('d', 'M0.5,0.5v' + tickHeight + 'h' + scale.px + 'v-' + tickHeight);\n\n selection.select('.scale-text')\n .style(localizer.textDirection() === 'ltr' ? 'left' : 'right', (scale.px + 16) + 'px')\n .text(scale.text);\n }\n\n\n return function(selection) {\n function switchUnits() {\n isImperial = !isImperial;\n selection.call(update);\n }\n\n var scalegroup = selection.append('svg')\n .attr('class', 'scale')\n .on('click', switchUnits)\n .append('g')\n .attr('transform', 'translate(10,11)');\n\n scalegroup\n .append('path')\n .attr('class', 'scale-path');\n\n selection\n .append('div')\n .attr('class', 'scale-text');\n\n selection.call(update);\n\n context.map().on('move.scale', function() {\n update(selection);\n });\n };\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { fileFetcher } from '../core/file_fetcher';\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\nimport { uiModal } from './modal';\nimport { utilArrayUniq } from '../util';\nimport { utilDetect } from '../util/detect';\n\n\nexport function uiShortcuts(context) {\n var detected = utilDetect();\n var _activeTab = 0;\n var _modalSelection;\n var _selection = d3_select(null);\n var _dataShortcuts;\n\n\n function shortcutsModal(_modalSelection) {\n _modalSelection.select('.modal')\n .classed('modal-shortcuts', true);\n\n var content = _modalSelection.select('.content');\n\n content\n .append('div')\n .attr('class', 'modal-section header')\n .append('h2')\n .call(t.append('shortcuts.title'));\n\n fileFetcher.get('shortcuts')\n .then(function(data) {\n _dataShortcuts = data;\n content.call(render);\n })\n .catch(function() { /* ignore */ });\n }\n\n\n function render(selection) {\n if (!_dataShortcuts) return;\n\n var wrapper = selection\n .selectAll('.wrapper')\n .data([0]);\n\n var wrapperEnter = wrapper\n .enter()\n .append('div')\n .attr('class', 'wrapper modal-section');\n\n var tabsBar = wrapperEnter\n .append('div')\n .attr('class', 'tabs-bar');\n\n var shortcutsList = wrapperEnter\n .append('div')\n .attr('class', 'shortcuts-list');\n\n wrapper = wrapper.merge(wrapperEnter);\n\n var tabs = tabsBar\n .selectAll('.tab')\n .data(_dataShortcuts);\n\n var tabsEnter = tabs\n .enter()\n .append('a')\n .attr('class', 'tab')\n .attr('href', '#')\n .on('click', function (d3_event, d) {\n d3_event.preventDefault();\n var i = _dataShortcuts.indexOf(d);\n _activeTab = i;\n render(selection);\n });\n\n tabsEnter\n .append('span')\n .each(function (d) {\n d3_select(this).call(t.addOrUpdate(d.text));\n });\n\n // Update\n wrapper.selectAll('.tab')\n .classed('active', function (d, i) {\n return i === _activeTab;\n });\n\n\n var shortcuts = shortcutsList\n .selectAll('.shortcut-tab')\n .data(_dataShortcuts);\n\n var shortcutsEnter = shortcuts\n .enter()\n .append('div')\n .attr('class', function(d) { return 'shortcut-tab shortcut-tab-' + d.tab; });\n\n var columnsEnter = shortcutsEnter\n .selectAll('.shortcut-column')\n .data(function (d) { return d.columns; })\n .enter()\n .append('table')\n .attr('class', 'shortcut-column');\n\n var rowsEnter = columnsEnter\n .selectAll('.shortcut-row')\n .data(function (d) { return d.rows; })\n .enter()\n .append('tr')\n .attr('class', 'shortcut-row');\n\n\n var sectionRows = rowsEnter\n .filter(function (d) { return !d.shortcuts; });\n\n sectionRows\n .append('td');\n\n sectionRows\n .append('td')\n .attr('class', 'shortcut-section')\n .append('h3')\n .each(function (d) {\n d3_select(this).call(t.addOrUpdate(d.text));\n });\n\n\n var shortcutRows = rowsEnter\n .filter(function (d) { return d.shortcuts; });\n\n var shortcutKeys = shortcutRows\n .append('td')\n .attr('class', 'shortcut-keys');\n\n var modifierKeys = shortcutKeys\n .filter(function (d) { return d.modifiers; });\n\n modifierKeys\n .selectAll('kbd.modifier')\n .data(function (d) {\n if (detected.os === 'win' && d.text === 'shortcuts.editing.commands.redo') {\n return ['\u2318'];\n } else if (detected.os !== 'mac' && d.text === 'shortcuts.browsing.display_options.fullscreen') {\n return [];\n } else {\n return d.modifiers;\n }\n })\n .enter()\n .each(function () {\n var selection = d3_select(this);\n\n selection\n .append('kbd')\n .attr('class', 'modifier')\n .text(function (d) { return uiCmd.display(d); });\n\n selection\n .append('span')\n .text('+');\n });\n\n\n shortcutKeys\n .selectAll('kbd.shortcut')\n .data(function (d) {\n var arr = d.shortcuts;\n if (detected.os === 'win' && d.text === 'shortcuts.editing.commands.redo') {\n arr = ['Y'];\n } else if (detected.os !== 'mac' && d.text === 'shortcuts.browsing.display_options.fullscreen') {\n arr = ['F11'];\n }\n\n // replace translations\n arr = arr.map(function(s) {\n return uiCmd.display(s.indexOf('.') !== -1 ? t(s) : s);\n });\n\n return utilArrayUniq(arr).map(function(s) {\n return {\n shortcut: s,\n separator: d.separator,\n suffix: d.suffix\n };\n });\n })\n .enter()\n .each(function (d, i, nodes) {\n var selection = d3_select(this);\n var click = d.shortcut.toLowerCase().match(/(.*).click/);\n\n if (click && click[1]) { // replace \"left_click\", \"right_click\" with mouse icon\n selection\n .call(svgIcon('#iD-walkthrough-mouse-' + click[1], 'operation'));\n } else if (d.shortcut.toLowerCase() === 'long-press') {\n selection\n .call(svgIcon('#iD-walkthrough-longpress', 'longpress operation'));\n } else if (d.shortcut.toLowerCase() === 'tap') {\n selection\n .call(svgIcon('#iD-walkthrough-tap', 'tap operation'));\n } else {\n selection\n .append('kbd')\n .attr('class', 'shortcut')\n .text(function (d) { return d.shortcut; });\n }\n\n if (i < nodes.length - 1) {\n if (d.separator) {\n selection\n .append('span')\n .text(d.separator);\n } else {\n selection.append('span').text('\\u00a0');\n selection.append('span').call(t.append('shortcuts.or'));\n selection.append('span').text('\\u00a0');\n }\n } else if (i === nodes.length - 1 && d.suffix) {\n selection\n .append('span')\n .text(d.suffix);\n }\n });\n\n\n shortcutKeys\n .filter(function(d) { return d.gesture; })\n .each(function () {\n var selection = d3_select(this);\n\n selection\n .append('span')\n .text('+');\n\n selection\n .append('span')\n .attr('class', 'gesture')\n .each(function (d) {\n d3_select(this).call(t.addOrUpdate(d.gesture));\n });\n });\n\n\n shortcutRows\n .append('td')\n .attr('class', 'shortcut-desc')\n .each(function (d) {\n if (d.text) {\n d3_select(this).call(t.addOrUpdate(d.text));\n } else {\n d3_select(this).text('\\u00a0');\n }\n });\n\n\n // Update\n wrapper.selectAll('.shortcut-tab')\n .style('display', function (d, i) {\n return i === _activeTab ? 'flex' : 'none';\n });\n }\n\n\n return function(selection, show) {\n _selection = selection;\n if (show) {\n _modalSelection = uiModal(selection);\n _modalSelection.call(shortcutsModal);\n } else {\n context.keybinding()\n .on([t('shortcuts.toggle.key'), '?'], function () {\n if (context.container().selectAll('.modal-shortcuts').size()) { // already showing\n if (_modalSelection) {\n _modalSelection.close();\n _modalSelection = null;\n }\n } else {\n _modalSelection = uiModal(_selection);\n _modalSelection.call(shortcutsModal);\n }\n });\n }\n };\n}\n", "module.exports = element;\nmodule.exports.pair = pair;\nmodule.exports.format = format;\nmodule.exports.formatPair = formatPair;\nmodule.exports.coordToDMS = coordToDMS;\n\n\nfunction element(input, dims) {\n var result = search(input, dims);\n return (result === null) ? null : result.val;\n}\n\n\nfunction formatPair(input) {\n return format(input.lat, 'lat') + ' ' + format(input.lon, 'lon');\n}\n\n\n// Is 0 North or South?\nfunction format(input, dim) {\n var dms = coordToDMS(input, dim);\n return dms.whole + '\u00B0 ' +\n (dms.minutes ? dms.minutes + '\\' ' : '') +\n (dms.seconds ? dms.seconds + '\" ' : '') + dms.dir;\n}\n\n\nfunction coordToDMS(input, dim) {\n var dirs = { lat: ['N', 'S'], lon: ['E', 'W'] }[dim] || '';\n var dir = dirs[input >= 0 ? 0 : 1];\n var abs = Math.abs(input);\n var whole = Math.floor(abs);\n var fraction = abs - whole;\n var fractionMinutes = fraction * 60;\n var minutes = Math.floor(fractionMinutes);\n var seconds = Math.floor((fractionMinutes - minutes) * 60);\n\n return {\n whole: whole,\n minutes: minutes,\n seconds: seconds,\n dir: dir\n };\n}\n\n\nfunction search(input, dims) {\n if (!dims) dims = 'NSEW';\n if (typeof input !== 'string') return null;\n\n input = input.toUpperCase();\n var regex = /^[\\s\\,]*([NSEW])?\\s*([\\-|\\\u2014|\\\u2015]?[0-9.]+)[\u00B0\u00BA\u02DA]?\\s*(?:([0-9.]+)['\u2019\u2032\u2018]\\s*)?(?:([0-9.]+)(?:''|\"|\u201D|\u2033)\\s*)?([NSEW])?/;\n\n var m = input.match(regex);\n if (!m) return null; // no match\n\n var matched = m[0];\n\n // extract dimension.. m[1] = leading, m[5] = trailing\n var dim;\n if (m[1] && m[5]) { // if matched both..\n dim = m[1]; // keep leading\n matched = matched.slice(0, -1); // remove trailing dimension from match\n } else {\n dim = m[1] || m[5];\n }\n\n // if unrecognized dimension\n if (dim && dims.indexOf(dim) === -1) return null;\n\n // extract DMS\n var deg = m[2] ? parseFloat(m[2]) : 0;\n var min = m[3] ? parseFloat(m[3]) / 60 : 0;\n var sec = m[4] ? parseFloat(m[4]) / 3600 : 0;\n var sign = (deg < 0) ? -1 : 1;\n if (dim === 'S' || dim === 'W') sign *= -1;\n\n return {\n val: (Math.abs(deg) + min + sec) * sign,\n dim: dim,\n matched: matched,\n remain: input.slice(matched.length)\n };\n}\n\n\nfunction pair(input, dims) {\n input = input.trim();\n var one = search(input, dims);\n if (!one) return null;\n\n input = one.remain.trim();\n var two = search(input, dims);\n if (!two || two.remain) return null;\n\n if (one.dim) {\n return swapdim(one.val, two.val, one.dim);\n } else {\n return [one.val, two.val];\n }\n}\n\n\nfunction swapdim(a, b, dim) {\n if (dim === 'N' || dim === 'S') return [a, b];\n if (dim === 'W' || dim === 'E') return [b, a];\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\nimport * as sexagesimal from '@mapbox/sexagesimal';\n\nimport { presetManager } from '../presets';\nimport { t } from '../core/localizer';\nimport { dmsCoordinatePair, dmsMatcher } from '../util/units';\nimport { coreGraph } from '../core/graph';\nimport { geoSphericalDistance } from '../geo/geo';\nimport { geoExtent } from '../geo';\nimport { modeSelect } from '../modes/select';\nimport { osmEntity } from '../osm/entity';\nimport { getRelationColor } from '../osm/tags';\nimport { services } from '../services';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\n\nimport {\n utilDisplayName,\n utilDisplayType,\n utilHighlightEntities,\n utilNoAuto\n} from '../util';\n\n\nexport const idMatch = q => {\n const idMatchRegex = /(?:^|\\W)(node|way|relation|note|[nwr])\\W{0,2}0*([1-9]\\d*)(?:\\W|$)/i;\n const idMatch = q.match(idMatchRegex);\n if (!idMatch) return false;\n\n return {\n type: idMatch[1] === 'note' ? idMatch[1] : idMatch[1].charAt(0),\n id: idMatch[2]\n };\n};\n\nexport function uiFeatureList(context) {\n var _geocodeResults;\n\n\n function featureList(selection) {\n var header = selection\n .append('div')\n .attr('class', 'header fillL');\n\n header\n .append('h2')\n .call(t.append('inspector.feature_list'));\n\n var searchWrap = selection\n .append('div')\n .attr('class', 'search-header');\n\n searchWrap\n .call(svgIcon('#iD-icon-search', 'pre-text'));\n\n var search = searchWrap\n .append('input')\n .attr('placeholder', t('inspector.search'))\n .attr('type', 'search')\n .call(utilNoAuto)\n .on('keypress', keypress)\n .on('keydown', keydown)\n .on('input', inputevent);\n\n var listWrap = selection\n .append('div')\n .attr('class', 'inspector-body');\n\n var list = listWrap\n .append('div')\n .attr('class', 'feature-list');\n\n context\n .on('exit.feature-list', clearSearch);\n context.map()\n .on('drawn.feature-list', mapDrawn);\n\n context.keybinding()\n .on(uiCmd('\u2318F'), focusSearch);\n\n\n function focusSearch(d3_event) {\n var mode = context.mode() && context.mode().id;\n if (mode !== 'browse') return;\n\n d3_event.preventDefault();\n search.node().focus();\n }\n\n\n function keydown(d3_event) {\n if (d3_event.keyCode === 27) { // escape\n search.node().blur();\n }\n }\n\n\n function keypress(d3_event) {\n var q = search.property('value'),\n items = list.selectAll('.feature-list-item');\n if (d3_event.keyCode === 13 && // \u21A9 Return\n q.length &&\n items.size()) {\n click(d3_event, items.datum());\n }\n }\n\n\n function inputevent() {\n _geocodeResults = undefined;\n drawList();\n }\n\n\n function clearSearch() {\n search.property('value', '');\n drawList();\n }\n\n\n function mapDrawn(e) {\n if (e.full) {\n drawList();\n }\n }\n\n\n function features() {\n var graph = context.graph();\n var visibleCenter = context.map().extent().center();\n var q = search.property('value').toLowerCase().trim();\n\n if (!q) return [];\n\n const locationMatch = sexagesimal.pair(q.toUpperCase()) || dmsMatcher(q);\n\n const coordResult = [];\n if (locationMatch) {\n const latLon = [Number(locationMatch[0]), Number(locationMatch[1])];\n const lonLat = [latLon[1], latLon[0]]; // also try swapped order\n\n const isLatLonValid = latLon[0] >= -90 && latLon[0] <= 90 && latLon[1] >= -180 && latLon[1] <= 180;\n let isLonLatValid = lonLat[0] >= -90 && lonLat[0] <= 90 && lonLat[1] >= -180 && lonLat[1] <= 180;\n isLonLatValid &&= !q.match(/[NSEW]/i); // don't flip coords with explicit cardinal directions\n isLonLatValid &&= !locationMatch[2]; // don't flip zoom/x/y coords\n isLonLatValid &&= lonLat[0] !== lonLat[1]; // don't flip when lat=lon\n\n if (isLatLonValid) {\n coordResult.push({\n id: latLon[0] + '/' + latLon[1],\n geometry: 'point',\n type: t('inspector.location'),\n name: dmsCoordinatePair([latLon[1], latLon[0]]),\n location: latLon,\n zoom: locationMatch[2]\n });\n }\n if (isLonLatValid) {\n coordResult.push({\n id: lonLat[0] + '/' + lonLat[1],\n geometry: 'point',\n type: t('inspector.location'),\n name: dmsCoordinatePair([lonLat[1], lonLat[0]]),\n location: lonLat\n });\n }\n }\n\n // A location search takes priority over an ID search\n const idMatchResult = !locationMatch && idMatch(q);\n const idResult = [];\n if (idMatchResult) {\n const elemType = idMatchResult.type;\n const elemId = idMatchResult.id;\n idResult.push({\n id: elemType + elemId,\n geometry: elemType === 'n' ? 'point' : elemType === 'w' ? 'line' : elemType === 'note' ? 'note' : 'relation',\n type: elemType === 'n' ? t('inspector.node') : elemType === 'w' ? t('inspector.way') : elemType === 'note' ? t('note.note') : t('inspector.relation'),\n name: elemId\n });\n }\n\n var allEntities = graph.entities;\n const localResults = [];\n for (var id in allEntities) {\n var entity = allEntities[id];\n if (!entity) continue;\n\n var matched = presetManager.match(entity, graph);\n var name = utilDisplayName(entity, { hideNetwork: matched.suggestion }) || '';\n if (name.toLowerCase().indexOf(q) < 0) continue;\n var type = (matched && matched.name()) || utilDisplayType(entity.id);\n var extent = entity.extent(graph);\n var distance = extent ? geoSphericalDistance(visibleCenter, extent.center()) : 0;\n\n localResults.push({\n id: entity.id,\n entity: entity,\n geometry: entity.geometry(graph),\n type: type,\n name: name,\n distance: distance\n });\n\n if (localResults.length > 100) break;\n }\n localResults.sort((a, b) => a.distance - b.distance);\n\n const geocodeResults = [];\n (_geocodeResults || []).forEach(function(d) {\n if (d.osm_type && d.osm_id) { // some results may be missing these - #1890\n\n // Make a temporary osmEntity so we can preset match\n // and better localize the search result - #4725\n var id = osmEntity.id.fromOSM(d.osm_type, d.osm_id);\n var tags = {};\n tags[d.class] = d.type;\n\n var attrs = { id: id, type: d.osm_type, tags: tags };\n if (d.osm_type === 'way') { // for ways, add some fake closed nodes\n attrs.nodes = ['a','a']; // so that geometry area is possible\n }\n\n var tempEntity = osmEntity(attrs);\n var tempGraph = coreGraph([tempEntity]);\n var matched = presetManager.match(tempEntity, tempGraph);\n var type = (matched && matched.name()) || utilDisplayType(id);\n\n geocodeResults.push({\n id: tempEntity.id,\n geometry: tempEntity.geometry(tempGraph),\n type: type,\n name: d.display_name,\n extent: new geoExtent(\n [Number(d.boundingbox[3]), Number(d.boundingbox[0])],\n [Number(d.boundingbox[2]), Number(d.boundingbox[1])])\n });\n }\n });\n\n const extraResults = [];\n if (q.match(/^[0-9]+$/)) {\n // if query is just a number, possibly an OSM ID without a prefix\n extraResults.push({\n id: 'n' + q,\n geometry: 'point',\n type: t('inspector.node'),\n name: q\n });\n extraResults.push({\n id: 'w' + q,\n geometry: 'line',\n type: t('inspector.way'),\n name: q\n });\n extraResults.push({\n id: 'r' + q,\n geometry: 'relation',\n type: t('inspector.relation'),\n name: q\n });\n extraResults.push({\n id: 'note' + q,\n geometry: 'note',\n type: t('note.note'),\n name: q\n });\n }\n\n return [...idResult, ...localResults, ...coordResult, ...geocodeResults, ...extraResults];\n }\n\n\n function drawList() {\n var value = search.property('value');\n var results = features();\n\n list.classed('filtered', value.length);\n\n var resultsIndicator = list.selectAll('.no-results-item')\n .data([0])\n .enter()\n .append('button')\n .property('disabled', true)\n .attr('class', 'no-results-item')\n .call(svgIcon('#iD-icon-alert', 'pre-text'));\n\n resultsIndicator.append('span')\n .attr('class', 'entity-name');\n\n list.selectAll('.no-results-item .entity-name')\n .html('')\n .call(t.append('geocoder.no_results_worldwide'));\n\n if (services.geocoder) {\n list.selectAll('.geocode-item')\n .data([0])\n .enter()\n .append('button')\n .attr('class', 'geocode-item secondary-action')\n .on('click', geocoderSearch)\n .append('div')\n .attr('class', 'label')\n .append('span')\n .attr('class', 'entity-name')\n .call(t.append('geocoder.search'));\n }\n\n list.selectAll('.no-results-item')\n .style('display', (value.length && !results.length) ? 'block' : 'none');\n\n list.selectAll('.geocode-item')\n .style('display', (value && _geocodeResults === undefined) ? 'block' : 'none');\n\n var items = list.selectAll('.feature-list-item')\n .data(results, function(d) { return d.id; });\n\n var enter = items.enter()\n .insert('button', '.geocode-item')\n .attr('class', 'feature-list-item')\n .on('pointerenter', mouseover)\n .on('pointerleave', mouseout)\n .on('focus', mouseover)\n .on('blur', mouseout)\n .on('click', click);\n\n var label = enter\n .append('div')\n .attr('class', 'label')\n .attr('title', d => d.name);\n\n label\n .each(function(d) {\n d3_select(this)\n .call(svgIcon('#iD-icon-' + d.geometry, 'pre-text'));\n });\n\n label\n .append('span')\n .attr('class', 'entity-type')\n .text(function(d) { return d.type; });\n\n label.each(function(d) {\n if (d.entity?.type !== 'relation') return;\n\n const hasRef = d.entity.tags.ref;\n const relColors = getRelationColor(d.entity.tags, '#555');\n if (relColors.isValid || hasRef) {\n const refs = (d.entity.tags.ref || '').split(';');\n for (const ref of refs) {\n d3_select(this)\n .append('span')\n .classed('member-entity-ref-color', true)\n .style('border-color', relColors.color)\n .style('background-color', relColors.color)\n .style('color', relColors.textColor)\n .text(ref);\n }\n }\n });\n\n label\n .append('span')\n .attr('class', 'entity-name')\n .text(d => d.name);\n\n enter\n .style('opacity', 0)\n .transition()\n .style('opacity', 1);\n\n items.exit()\n .each(d => mouseout(undefined, d))\n .remove();\n\n items.merge(enter)\n .order();\n }\n\n\n function mouseover(d3_event, d) {\n if (d.location !== undefined) return;\n\n utilHighlightEntities([d.id], true, context);\n }\n\n\n function mouseout(d3_event, d) {\n if (d.location !== undefined) return;\n\n utilHighlightEntities([d.id], false, context);\n }\n\n\n function click(d3_event, d) {\n d3_event.preventDefault();\n\n if (d.location) {\n context.map().centerZoomEase([d.location[1], d.location[0]], d.zoom || 19);\n\n } else if (d.entity) {\n utilHighlightEntities([d.id], false, context);\n\n context.enter(modeSelect(context, [d.entity.id]));\n context.map().zoomToEase(d.entity);\n\n } else if (d.geometry === 'note') {\n // note\n // get number part 'note12345'\n const noteId = d.id.replace(/\\D/g, '');\n\n // load note\n context.moveToNote(noteId);\n } else {\n // download, zoom to, and select the entity with the given ID\n context.zoomToEntity(d.id);\n }\n }\n\n\n function geocoderSearch() {\n services.geocoder.search(search.property('value'), function (err, resp) {\n _geocodeResults = resp || [];\n drawList();\n });\n }\n }\n\n\n return featureList;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { svgIcon } from '../../svg/icon';\nimport { utilArrayIdentical } from '../../util/array';\nimport { t } from '../../core/localizer';\nimport { utilHighlightEntities } from '../../util';\nimport { uiSection } from '../section';\nimport { validationIssue } from '../../core/validation';\n\n\nexport function uiSectionEntityIssues(context) {\n // Does the user prefer to expand the active issue? Useful for viewing tag diff.\n // Expand by default so first timers see it - #6408, #8143\n var preference = prefs('entity-issues.reference.expanded');\n var _expanded = preference === null ? true : (preference === 'true');\n\n var _entityIDs = [];\n var _issues = [];\n var _activeIssueID;\n\n\n var section = uiSection('entity-issues', context)\n .shouldDisplay(function() {\n return _issues.length > 0;\n })\n .label(function() {\n return t.append('inspector.title_count', { title: t.append('issues.list_title'), count: _issues.length });\n })\n .disclosureContent(renderDisclosureContent);\n\n context.validator()\n .on('validated.entity_issues', function() {\n // Refresh on validated events\n reloadIssues();\n section.reRender();\n })\n .on('focusedIssue.entity_issues', function(issue) {\n makeActiveIssue(issue.id);\n });\n\n function reloadIssues() {\n _issues = context.validator().getSharedEntityIssues(_entityIDs, { includeDisabledRules: true });\n }\n\n function makeActiveIssue(issueID) {\n _activeIssueID = issueID;\n section.selection().selectAll('.issue-container')\n .classed('active', function(d) { return d.id === _activeIssueID; });\n }\n\n function renderDisclosureContent(selection) {\n\n selection.classed('grouped-items-area', true);\n\n _activeIssueID = _issues.length > 0 ? _issues[0].id : null;\n\n var containers = selection.selectAll('.issue-container')\n .data(_issues, function(d) { return d.key; });\n\n // Exit\n containers.exit()\n .remove();\n\n // Enter\n var containersEnter = containers.enter()\n .append('div')\n .attr('class', 'issue-container');\n\n\n var itemsEnter = containersEnter\n .append('div')\n .attr('class', function(d) { return 'issue severity-' + d.severity; })\n .on('mouseover.highlight', function(d3_event, d) {\n // don't hover-highlight the selected entity\n var ids = d.entityIds\n .filter(function(e) { return _entityIDs.indexOf(e) === -1; });\n\n utilHighlightEntities(ids, true, context);\n })\n .on('mouseout.highlight', function(d3_event, d) {\n var ids = d.entityIds\n .filter(function(e) { return _entityIDs.indexOf(e) === -1; });\n\n utilHighlightEntities(ids, false, context);\n });\n\n var labelsEnter = itemsEnter\n .append('div')\n .attr('class', 'issue-label');\n\n var textEnter = labelsEnter\n .append('button')\n .attr('class', 'issue-text')\n .on('click', function(d3_event, d) {\n makeActiveIssue(d.id); // expand only the clicked item\n\n const extent = d.extent(context.graph());\n if (extent) {\n context.map().zoomToEase(extent);\n }\n });\n\n textEnter\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(validationIssue.ICONS[d.severity], 'issue-icon'));\n });\n\n textEnter\n .append('span')\n .attr('class', 'issue-message');\n\n\n var infoButton = labelsEnter\n .append('button')\n .attr('class', 'issue-info-button')\n .attr('title', t('icons.information'))\n .call(svgIcon('#iD-icon-inspect'));\n\n infoButton\n .on('click', function (d3_event) {\n d3_event.stopPropagation();\n d3_event.preventDefault();\n this.blur(); // avoid keeping focus on the button - #4641\n\n var container = d3_select(this.parentNode.parentNode.parentNode);\n var info = container.selectAll('.issue-info');\n var isExpanded = info.classed('expanded');\n _expanded = !isExpanded;\n prefs('entity-issues.reference.expanded', _expanded); // update preference\n\n if (isExpanded) {\n info\n .transition()\n .duration(200)\n .style('max-height', '0px')\n .style('opacity', '0')\n .on('end', function () {\n info.classed('expanded', false);\n });\n } else {\n info\n .classed('expanded', true)\n .transition()\n .duration(200)\n .style('max-height', '200px')\n .style('opacity', '1')\n .on('end', function () {\n info.style('max-height', null);\n });\n }\n });\n\n itemsEnter\n .append('ul')\n .attr('class', 'issue-fix-list');\n\n containersEnter\n .append('div')\n .attr('class', 'issue-info' + (_expanded ? ' expanded' : ''))\n .style('max-height', (_expanded ? null : '0'))\n .style('opacity', (_expanded ? '1' : '0'))\n .each(function(d) {\n if (typeof d.reference === 'function') {\n d3_select(this)\n .call(d.reference);\n } else {\n d3_select(this)\n .call(t.append('inspector.no_documentation_key'));\n }\n });\n\n\n // Update\n containers = containers\n .merge(containersEnter)\n .classed('active', function(d) { return d.id === _activeIssueID; });\n\n containers.selectAll('.issue-message')\n .text('')\n .each(function(d) {\n return d.message(context)(d3_select(this));\n });\n\n // fixes\n var fixLists = containers.selectAll('.issue-fix-list');\n\n var fixes = fixLists.selectAll('.issue-fix-item')\n .data(function(d) { return d.fixes ? d.fixes(context) : []; }, function(fix) { return fix.id; });\n\n fixes.exit()\n .remove();\n\n var fixesEnter = fixes.enter()\n .append('li')\n .attr('class', 'issue-fix-item');\n\n var buttons = fixesEnter\n .append('button')\n .on('click', function(d3_event, d) {\n // not all fixes are actionable\n if (d3_select(this).attr('disabled') || !d.onClick) return;\n\n // Don't run another fix for this issue within a second of running one\n // (Necessary for \"Select a feature type\" fix. Most fixes should only ever run once)\n if (d.issue.dateLastRanFix && new Date() - d.issue.dateLastRanFix < 1000) return;\n d.issue.dateLastRanFix = new Date();\n\n // remove hover-highlighting\n utilHighlightEntities(d.issue.entityIds.concat(d.entityIds), false, context);\n\n new Promise(function(resolve, reject) {\n d.onClick(context, resolve, reject);\n if (d.onClick.length <= 1) {\n // if the fix doesn't take any completion parameters then consider it resolved\n resolve();\n }\n })\n .then(function() {\n // revalidate whenever the fix has finished running successfully\n context.validator().validate();\n });\n })\n .on('mouseover.highlight', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, true, context);\n })\n .on('mouseout.highlight', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, false, context);\n });\n\n buttons\n .each(function(d) {\n var iconName = d.icon || 'iD-icon-wrench';\n if (iconName.startsWith('maki')) {\n iconName += '-15';\n }\n d3_select(this).call(svgIcon('#' + iconName, 'fix-icon'));\n });\n\n buttons\n .append('span')\n .attr('class', 'fix-message')\n .each(function(d) { return d.title(d3_select(this)); });\n\n fixesEnter.merge(fixes)\n .selectAll('button')\n .classed('actionable', function(d) {\n return d.onClick;\n })\n .attr('disabled', function(d) {\n return d.onClick ? null : 'true';\n })\n .attr('title', function(d) {\n if (d.disabledReason) {\n return d.disabledReason;\n }\n return null;\n });\n }\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n if (!_entityIDs || !val || !utilArrayIdentical(_entityIDs, val)) {\n _entityIDs = val;\n _activeIssueID = null;\n reloadIssues();\n }\n return section;\n };\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../presets';\nimport { prefs } from '../core/preferences';\nimport { svgIcon, svgTagClasses } from '../svg';\nimport { utilFunctor } from '../util';\n\n\nexport function uiPresetIcon() {\n let _preset;\n let _geometry;\n\n\n function presetIcon(selection) {\n selection.each(render);\n }\n\n\n function getIcon(p, geom) {\n if (p.isFallback && p.isFallback()) return geom === 'vertex' ? '' : 'iD-icon-' + p.id;\n if (p.icon) return p.icon;\n if (geom === 'line') return 'iD-other-line';\n if (geom === 'vertex') return 'temaki-vertex';\n return 'maki-marker-stroked';\n }\n\n\n function renderPointBorder(container, drawPoint) {\n const pointBorder = container.selectAll('.preset-icon-point-border')\n .data(drawPoint ? [0] : []);\n\n pointBorder.exit()\n .remove();\n\n const pointBorderEnter = pointBorder.enter();\n\n const w = 40;\n const h = 40;\n\n pointBorderEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-point-border')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`)\n .append('path')\n .attr('transform', 'translate(11.5, 8)')\n .attr('d', 'M 17,8 C 17,13 11,21 8.5,23.5 C 6,21 0,13 0,8 C 0,4 4,-0.5 8.5,-0.5 C 13,-0.5 17,4 17,8 z');\n }\n\n\n function renderCategoryBorder(container, category) {\n const categoryBorder = container.selectAll('.preset-icon-category-border')\n .data(category ? [0] : []);\n\n categoryBorder.exit()\n .remove();\n\n const categoryBorderEnter = categoryBorder.enter();\n\n const d = 60;\n\n let svgEnter = categoryBorderEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-category-border')\n .attr('width', d)\n .attr('height', d)\n .attr('viewBox', `0 0 ${d} ${d}`);\n\n svgEnter\n .append('path')\n .attr('class', 'area')\n .attr('d', 'M9.5,7.5 L25.5,7.5 L28.5,12.5 L49.5,12.5 C51.709139,12.5 53.5,14.290861 53.5,16.5 L53.5,43.5 C53.5,45.709139 51.709139,47.5 49.5,47.5 L10.5,47.5 C8.290861,47.5 6.5,45.709139 6.5,43.5 L6.5,12.5 L9.5,7.5 Z');\n\n if (category) {\n categoryBorderEnter.merge(categoryBorder)\n .selectAll('path')\n .attr('class', `area ${category.id}`);\n }\n }\n\n\n function renderCircleFill(container, drawVertex) {\n const vertexFill = container.selectAll('.preset-icon-fill-vertex')\n .data(drawVertex ? [0] : []);\n\n vertexFill.exit()\n .remove();\n\n const vertexFillEnter = vertexFill.enter();\n\n const w = 60;\n const h = 60;\n const d = 40;\n\n vertexFillEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-fill-vertex')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`)\n .append('circle')\n .attr('cx', w / 2)\n .attr('cy', h / 2)\n .attr('r', d / 2);\n }\n\n\n function renderSquareFill(container, drawArea, tagClasses) {\n\n let fill = container.selectAll('.preset-icon-fill-area')\n .data(drawArea ? [0] : []);\n\n fill.exit()\n .remove();\n\n let fillEnter = fill.enter();\n\n const d = 60;\n const w = d;\n const h = d;\n const l = d * 2/3;\n const c1 = (w-l) / 2;\n const c2 = c1 + l;\n\n fillEnter = fillEnter\n .append('svg')\n .attr('class', 'preset-icon-fill preset-icon-fill-area')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`);\n\n ['fill', 'stroke'].forEach(klass => {\n fillEnter\n .append('path')\n .attr('d', `M${c1} ${c1} L${c1} ${c2} L${c2} ${c2} L${c2} ${c1} Z`)\n .attr('class', `area ${klass}`);\n });\n\n const rVertex = 2.5;\n [[c1, c1], [c1, c2], [c2, c2], [c2, c1]].forEach(point => {\n fillEnter\n .append('circle')\n .attr('class', 'vertex')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', rVertex);\n });\n\n const rMidpoint = 1.25;\n [[c1, w/2], [c2, w/2], [h/2, c1], [h/2, c2]].forEach(point => {\n fillEnter\n .append('circle')\n .attr('class', 'midpoint')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', rMidpoint);\n });\n\n fill = fillEnter.merge(fill);\n\n fill.selectAll('path.stroke')\n .attr('class', `area stroke ${tagClasses}`);\n fill.selectAll('path.fill')\n .attr('class', `area fill ${tagClasses}`);\n }\n\n\n function renderLine(container, drawLine, tagClasses) {\n\n let line = container.selectAll('.preset-icon-line')\n .data(drawLine ? [0] : []);\n\n line.exit()\n .remove();\n\n let lineEnter = line.enter();\n\n const d = 60;\n // draw the line parametrically\n const w = d;\n const h = d;\n const y = Math.round(d * 0.72);\n const l = Math.round(d * 0.6);\n const r = 2.5;\n const x1 = (w - l) / 2;\n const x2 = x1 + l;\n\n lineEnter = lineEnter\n .append('svg')\n .attr('class', 'preset-icon-line')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`);\n\n ['casing', 'stroke'].forEach(klass => {\n lineEnter\n .append('path')\n .attr('d', `M${x1} ${y} L${x2} ${y}`)\n .attr('class', `line ${klass}`);\n });\n\n [[x1-1, y], [x2+1, y]].forEach(point => {\n lineEnter\n .append('circle')\n .attr('class', 'vertex')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', r);\n });\n\n line = lineEnter.merge(line);\n\n line.selectAll('path.stroke')\n .attr('class', `line stroke ${tagClasses}`);\n line.selectAll('path.casing')\n .attr('class', `line casing ${tagClasses}`);\n }\n\n\n function renderRoute(container, drawRoute, p) {\n\n let route = container.selectAll('.preset-icon-route')\n .data(drawRoute ? [0] : []);\n\n route.exit()\n .remove();\n\n let routeEnter = route.enter();\n\n const d = 60;\n // draw the route parametrically\n const w = d;\n const h = d;\n const y1 = Math.round(d * 0.80);\n const y2 = Math.round(d * 0.68);\n const l = Math.round(d * 0.6);\n const r = 2;\n const x1 = (w - l) / 2;\n const x2 = x1 + l / 3;\n const x3 = x2 + l / 3;\n const x4 = x3 + l / 3;\n\n routeEnter = routeEnter\n .append('svg')\n .attr('class', 'preset-icon-route')\n .attr('width', w)\n .attr('height', h)\n .attr('viewBox', `0 0 ${w} ${h}`);\n\n ['casing', 'stroke'].forEach(klass => {\n routeEnter\n .append('path')\n .attr('d', `M${x1} ${y1} L${x2} ${y2}`)\n .attr('class', `segment0 line ${klass}`);\n routeEnter\n .append('path')\n .attr('d', `M${x2} ${y2} L${x3} ${y1}`)\n .attr('class', `segment1 line ${klass}`);\n routeEnter\n .append('path')\n .attr('d', `M${x3} ${y1} L${x4} ${y2}`)\n .attr('class', `segment2 line ${klass}`);\n });\n\n [[x1, y1], [x2, y2], [x3, y1], [x4, y2]].forEach(point => {\n routeEnter\n .append('circle')\n .attr('class', 'vertex')\n .attr('cx', point[0])\n .attr('cy', point[1])\n .attr('r', r);\n });\n\n route = routeEnter.merge(route);\n\n if (drawRoute) {\n let routeType = p.tags.type === 'waterway' ? 'waterway' : p.tags.route;\n const segmentPresetIDs = routeSegments[routeType];\n for (let i in segmentPresetIDs) {\n const segmentPreset = presetManager.item(segmentPresetIDs[i]);\n const segmentTagClasses = svgTagClasses().getClassesString(segmentPreset.tags, '');\n route.selectAll(`path.stroke.segment${i}`)\n .attr('class', `segment${i} line stroke ${segmentTagClasses}`);\n route.selectAll(`path.casing.segment${i}`)\n .attr('class', `segment${i} line casing ${segmentTagClasses}`);\n }\n }\n }\n\n function renderSvgIcon(container, picon, geom, isFramed, category, tagClasses) {\n const isMaki = picon && /^maki-/.test(picon);\n const isTemaki = picon && /^temaki-/.test(picon);\n const isFa = picon && /^fa[srb]-/.test(picon);\n const isR\u00F6ntgen = picon && /^roentgen-/.test(picon);\n const isiDIcon = picon && !(isMaki || isTemaki || isFa || isR\u00F6ntgen);\n\n let icon = container.selectAll('.preset-icon')\n .data(picon ? [0] : []);\n\n icon.exit()\n .remove();\n\n icon = icon.enter()\n .append('div')\n .attr('class', 'preset-icon')\n .call(svgIcon(''))\n .merge(icon);\n\n icon\n .attr('class', 'preset-icon ' + (geom ? geom + '-geom' : ''))\n .classed('category', category)\n .classed('framed', isFramed)\n .classed('preset-icon-iD', isiDIcon);\n\n icon.selectAll('svg')\n .attr('class', 'icon ' + picon + ' ' + (!isiDIcon && geom !== 'line' ? '' : tagClasses));\n\n icon.selectAll('use')\n .attr('href', '#' + picon);\n }\n\n\n function renderImageIcon(container, imageURL) {\n let imageIcon = container.selectAll('img.image-icon')\n .data(imageURL ? [0] : []);\n\n imageIcon.exit()\n .remove();\n\n imageIcon = imageIcon.enter()\n .append('img')\n .attr('class', 'image-icon')\n .on('load', () => container.classed('showing-img', true) )\n .on('error', () => container.classed('showing-img', false) )\n .merge(imageIcon);\n\n imageIcon\n .attr('src', imageURL);\n }\n\n // Route icons are drawn with a zigzag annotation underneath:\n // o o\n // / \\ /\n // o o\n // This dataset defines the styles that are used to draw the zigzag segments.\n const routeSegments = {\n bicycle: ['highway/cycleway', 'highway/cycleway', 'highway/cycleway'],\n bus: ['highway/unclassified', 'highway/secondary', 'highway/primary'],\n climbing: ['climbing/route', 'climbing/route', 'climbing/route'],\n trolleybus: ['highway/unclassified', 'highway/secondary', 'highway/primary'],\n detour: ['highway/tertiary', 'highway/residential', 'highway/unclassified'],\n ferry: ['route/ferry', 'route/ferry', 'route/ferry'],\n foot: ['highway/footway', 'highway/footway', 'highway/footway'],\n hiking: ['highway/path', 'highway/path', 'highway/path'],\n horse: ['highway/bridleway', 'highway/bridleway', 'highway/bridleway'],\n light_rail: ['railway/light_rail', 'railway/light_rail', 'railway/light_rail'],\n monorail: ['railway/monorail', 'railway/monorail', 'railway/monorail'],\n mtb: ['highway/path', 'highway/track', 'highway/bridleway'],\n pipeline: ['man_made/pipeline', 'man_made/pipeline', 'man_made/pipeline'],\n piste: ['piste/downhill', 'piste/hike', 'piste/nordic'],\n power: ['power/line', 'power/line', 'power/line'],\n road: ['highway/secondary', 'highway/primary', 'highway/trunk'],\n subway: ['railway/subway', 'railway/subway', 'railway/subway'],\n train: ['railway/rail', 'railway/rail', 'railway/rail'],\n tram: ['railway/tram', 'railway/tram', 'railway/tram'],\n railway: ['railway/rail', 'railway/rail', 'railway/rail'],\n waterway: ['waterway/stream', 'waterway/stream', 'waterway/stream']\n };\n\n\n function render() {\n let p = _preset.apply(this, arguments);\n let geom = _geometry ? _geometry.apply(this, arguments) : null;\n if (geom === 'relation' &&\n p.tags &&\n ((p.tags.type === 'route' && p.tags.route && routeSegments[p.tags.route]) || p.tags.type === 'waterway')) {\n geom = 'route';\n }\n\n const showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n const isFallback = p.isFallback && p.isFallback();\n const imageURL = (showThirdPartyIcons === 'true') && p.imageURL;\n const picon = getIcon(p, geom);\n const isCategory = !p.setTags;\n const drawPoint = false;\n const drawVertex = picon !== null && geom === 'vertex';\n const drawLine = picon && geom === 'line' && !isFallback && !isCategory;\n const drawArea = picon && geom === 'area' && !isFallback && !isCategory;\n const drawRoute = picon && geom === 'route';\n const isFramed = drawVertex || drawArea || drawLine || drawRoute || isCategory;\n\n let tags = !isCategory ? p.setTags({}, geom) : {};\n for (let k in tags) {\n if (tags[k] === '*') {\n tags[k] = 'yes';\n }\n }\n\n let tagClasses = svgTagClasses().getClassesString(tags, '');\n let selection = d3_select(this);\n\n let container = selection.selectAll('.preset-icon-container')\n .data([0]);\n\n container = container.enter()\n .append('div')\n .attr('class', 'preset-icon-container')\n .merge(container);\n\n container\n .classed('showing-img', !!imageURL)\n .classed('fallback', isFallback);\n\n renderCategoryBorder(container, isCategory && p);\n renderPointBorder(container, drawPoint);\n renderCircleFill(container, drawVertex);\n renderSquareFill(container, drawArea, tagClasses);\n renderLine(container, drawLine, tagClasses);\n renderRoute(container, drawRoute, p);\n renderSvgIcon(container, picon, geom, isFramed, isCategory, tagClasses);\n renderImageIcon(container, imageURL);\n }\n\n\n presetIcon.preset = function(val) {\n if (!arguments.length) return _preset;\n _preset = utilFunctor(val);\n return presetIcon;\n };\n\n\n presetIcon.geometry = function(val) {\n if (!arguments.length) return _geometry;\n _geometry = utilFunctor(val);\n return presetIcon;\n };\n\n return presetIcon;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { utilArrayIdentical } from '../../util/array';\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { utilRebind } from '../../util';\nimport { uiPresetIcon } from '../preset_icon';\nimport { uiSection } from '../section';\nimport { uiTagReference } from '../tag_reference';\n\n\nexport function uiSectionFeatureType(context) {\n\n var dispatch = d3_dispatch('choose');\n\n var _entityIDs = [];\n var _presets = [];\n\n var _tagReference;\n\n var section = uiSection('feature-type', context)\n .label(() => t.append('inspector.feature_type'))\n .disclosureContent(renderDisclosureContent);\n\n function renderDisclosureContent(selection) {\n\n selection.classed('preset-list-item', true);\n selection.classed('mixed-types', _presets.length > 1);\n\n var presetButtonWrap = selection\n .selectAll('.preset-list-button-wrap')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'preset-list-button-wrap');\n\n var presetButton = presetButtonWrap\n .append('button')\n .attr('class', 'preset-list-button preset-reset')\n .call(uiTooltip()\n .title(() => t.append('inspector.back_tooltip'))\n .placement('bottom')\n );\n\n presetButton.append('div')\n .attr('class', 'preset-icon-container');\n\n presetButton\n .append('div')\n .attr('class', 'label')\n .append('div')\n .attr('class', 'label-inner');\n\n presetButtonWrap.append('div')\n .attr('class', 'accessory-buttons');\n\n var tagReferenceBodyWrap = selection\n .selectAll('.tag-reference-body-wrap')\n .data([0]);\n\n tagReferenceBodyWrap = tagReferenceBodyWrap\n .enter()\n .append('div')\n .attr('class', 'tag-reference-body-wrap')\n .merge(tagReferenceBodyWrap);\n\n // update header\n if (_tagReference) {\n selection.selectAll('.preset-list-button-wrap .accessory-buttons')\n .style('display', _presets.length === 1 ? null : 'none')\n .call(_tagReference.button);\n\n tagReferenceBodyWrap\n .style('display', _presets.length === 1 ? null : 'none')\n .call(_tagReference.body);\n }\n\n selection.selectAll('.preset-reset')\n .on('click', function() {\n dispatch.call('choose', this, _presets);\n })\n .on('pointerdown pointerup mousedown mouseup', function(d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n });\n\n var geometries = entityGeometries();\n selection.select('.preset-list-item button')\n .call(uiPresetIcon()\n .geometry(_presets.length === 1 ? (geometries.length === 1 && geometries[0]) : null)\n .preset(_presets.length === 1 ? _presets[0] : presetManager.item('point'))\n );\n\n var names = _presets.length === 1 ? [\n _presets[0].nameLabel(),\n _presets[0].subtitleLabel()\n ].filter(Boolean) : [ t.append('inspector.multiple_types') ];\n\n var label = selection.select('.label-inner');\n var nameparts = label.selectAll('.namepart')\n .data(names, d => d.stringId);\n\n nameparts.exit()\n .remove();\n\n nameparts\n .enter()\n .append('div')\n .attr('class', 'namepart')\n .text('')\n .each(function(d) { d(d3_select(this)); });\n }\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return section;\n };\n\n section.presets = function(val) {\n if (!arguments.length) return _presets;\n\n // don't reload the same preset\n if (!utilArrayIdentical(val, _presets)) {\n _presets = val;\n\n if (_presets.length === 1) {\n _tagReference = uiTagReference(_presets[0].reference(), context)\n .showing(false);\n }\n }\n\n return section;\n };\n\n function entityGeometries() {\n\n var counts = {};\n\n for (var i in _entityIDs) {\n var geometry = context.graph().geometry(_entityIDs[i]);\n if (!counts[geometry]) counts[geometry] = 0;\n counts[geometry] += 1;\n }\n\n return Object.keys(counts).sort(function(geom1, geom2) {\n return counts[geom2] - counts[geom1];\n });\n }\n\n return utilRebind(section, dispatch, 'on');\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { uiCombobox } from './combobox';\nimport { utilGetSetValue, utilNoAuto } from '../util';\n\n\nexport function uiFormFields(context) {\n var moreCombo = uiCombobox(context, 'more-fields').minItems(1);\n var _fieldsArr = [];\n var _lastPlaceholder = '';\n var _state = '';\n var _klass = '';\n\n\n function formFields(selection) {\n var allowedFields = _fieldsArr.filter(function(field) { return field.isAllowed(); });\n var shown = allowedFields.filter(function(field) { return field.isShown(); });\n var notShown = allowedFields.filter(function(field) { return !field.isShown(); })\n .sort(function(a, b) { return (a.universal === b.universal ? 0 : a.universal ? 1 : -1); });\n\n var container = selection.selectAll('.form-fields-container')\n .data([0]);\n\n container = container.enter()\n .append('div')\n .attr('class', 'form-fields-container ' + (_klass || ''))\n .merge(container);\n\n\n var fields = container.selectAll('.wrap-form-field')\n .data(shown, function(d) { return d.id + (d.entityIDs ? d.entityIDs.join() : ''); });\n\n fields.exit()\n .remove();\n\n // Enter\n var enter = fields.enter()\n .append('div')\n .attr('class', function(d) { return 'wrap-form-field wrap-form-field-' + d.safeid; });\n\n // Update\n fields = fields\n .merge(enter);\n\n fields\n .order()\n .each(function(d) {\n d3_select(this)\n .call(d.render);\n });\n\n\n var titles = [];\n var moreFields = notShown.map(function(field) {\n var title = field.title();\n titles.push(title);\n\n var terms = field.terms();\n if (field.key) terms.push(field.key);\n if (field.keys) terms = terms.concat(field.keys);\n\n return {\n display: field.label(),\n value: title,\n title: title,\n field: field,\n terms: terms\n };\n });\n\n var placeholder = titles.slice(0,3).join(', ') + ((titles.length > 3) ? '\u2026' : '');\n\n\n var more = selection.selectAll('.more-fields')\n .data((_state === 'hover' || moreFields.length === 0) ? [] : [0]);\n\n more.exit()\n .remove();\n\n var moreEnter = more.enter()\n .append('div')\n .attr('class', 'more-fields')\n .append('label');\n\n moreEnter\n .append('span')\n .call(t.append('inspector.add_fields'));\n\n more = moreEnter\n .merge(more);\n\n\n var input = more.selectAll('.value')\n .data([0]);\n\n input.exit()\n .remove();\n\n input = input.enter()\n .append('input')\n .attr('class', 'value')\n .attr('type', 'text')\n .attr('placeholder', placeholder)\n .call(utilNoAuto)\n .merge(input);\n\n input\n .call(utilGetSetValue, '')\n .call(moreCombo\n .data(moreFields)\n .on('accept', function (d) {\n if (!d) return; // user entered something that was not matched\n var field = d.field;\n field.show();\n selection.call(formFields); // rerender\n field.focus();\n })\n );\n\n // avoid updating placeholder excessively (triggers style recalc)\n if (_lastPlaceholder !== placeholder) {\n input.attr('placeholder', placeholder);\n _lastPlaceholder = placeholder;\n }\n }\n\n\n formFields.fieldsArr = function(val) {\n if (!arguments.length) return _fieldsArr;\n _fieldsArr = val || [];\n return formFields;\n };\n\n formFields.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n return formFields;\n };\n\n formFields.klass = function(val) {\n if (!arguments.length) return _klass;\n _klass = val;\n return formFields;\n };\n\n\n return formFields;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { presetManager } from '../../presets';\nimport { t, localizer } from '../../core/localizer';\nimport { utilArrayIdentical } from '../../util/array';\nimport { utilArrayUnion, utilRebind } from '../../util';\nimport { geoExtent } from '../../geo/extent';\nimport { uiField } from '../field';\nimport { uiFormFields } from '../form_fields';\nimport { uiSection } from '../section';\n\nexport function uiSectionPresetFields(context) {\n\n var section = uiSection('preset-fields', context)\n .label(() => t.append('inspector.fields'))\n .disclosureContent(renderDisclosureContent);\n\n var dispatch = d3_dispatch('change', 'revert');\n var formFields = uiFormFields(context);\n var _state;\n var _fieldsArr;\n var _presets = [];\n var _tags;\n var _entityIDs;\n\n function renderDisclosureContent(selection) {\n if (!_fieldsArr) {\n\n var graph = context.graph();\n\n var geometries = Object.keys(_entityIDs.reduce(function(geoms, entityID) {\n geoms[graph.entity(entityID).geometry(graph)] = true;\n return geoms;\n }, {}));\n\n const loc = _entityIDs.reduce(function(extent, entityID) {\n var entity = context.graph().entity(entityID);\n return extent.extend(entity.extent(context.graph()));\n }, geoExtent()).center();\n\n var presetsManager = presetManager;\n\n var allFields = [];\n var allMoreFields = [];\n var sharedTotalFields;\n\n _presets.forEach(function(preset) {\n var fields = preset.fields(loc);\n var moreFields = preset.moreFields(loc);\n\n allFields = utilArrayUnion(allFields, fields);\n allMoreFields = utilArrayUnion(allMoreFields, moreFields);\n\n if (!sharedTotalFields) {\n sharedTotalFields = utilArrayUnion(fields, moreFields);\n } else {\n sharedTotalFields = sharedTotalFields.filter(function(field) {\n return fields.indexOf(field) !== -1 || moreFields.indexOf(field) !== -1;\n });\n }\n });\n\n var sharedFields = allFields.filter(function(field) {\n return sharedTotalFields.indexOf(field) !== -1;\n });\n var sharedMoreFields = allMoreFields.filter(function(field) {\n return sharedTotalFields.indexOf(field) !== -1;\n });\n\n _fieldsArr = [];\n\n // Ideally, everything in OpenHistoricalMap is dated and sourced.\n let coreKeys = ['start_date', 'end_date', 'source_preset'];\n coreKeys.forEach(key => {\n let field = presetsManager.field(key);\n if (field) {\n _fieldsArr.push(uiField(context, field, _entityIDs));\n }\n });\n\n let optionalCoreKeys = ['source_preset:1', 'source_preset:2', 'source_preset:3'];\n optionalCoreKeys.forEach(key => {\n let field = presetsManager.field(key);\n if (field && !_fieldsArr.includes(field)) {\n _fieldsArr.push(uiField(context, field, _entityIDs, { show: false }));\n }\n });\n\n\n sharedFields.forEach(function(field) {\n if (!coreKeys.includes(field.id) && !optionalCoreKeys.includes(field.id) && field.matchAllGeometry(geometries)) {\n _fieldsArr.push(\n uiField(context, field, _entityIDs)\n );\n }\n });\n\n var singularEntity = _entityIDs.length === 1 && graph.hasEntity(_entityIDs[0]);\n if (singularEntity && singularEntity.type === 'node' && singularEntity.isHighwayIntersection(graph) && presetsManager.field('restrictions')) {\n _fieldsArr.push(\n uiField(context, presetsManager.field('restrictions'), _entityIDs)\n );\n }\n\n var additionalFields = utilArrayUnion(sharedMoreFields, presetsManager.universal());\n additionalFields.sort(function(field1, field2) {\n return field1.title().localeCompare(field2.title(), localizer.localeCode());\n });\n\n additionalFields.forEach(function(field) {\n if (sharedFields.indexOf(field) === -1 &&\n !coreKeys.includes(field.id) &&\n !optionalCoreKeys.includes(field.id) &&\n field.matchAllGeometry(geometries)) {\n _fieldsArr.push(\n uiField(context, field, _entityIDs, { show: false })\n );\n }\n });\n\n _fieldsArr.forEach(function(field) {\n field\n .on('change', function(t, onInput) {\n dispatch.call('change', field, _entityIDs, t, onInput);\n })\n .on('revert', function(keys) {\n dispatch.call('revert', field, keys);\n });\n });\n }\n\n _fieldsArr.forEach(function(field) {\n field\n .state(_state)\n .tags(_tags);\n });\n\n\n selection\n .call(formFields\n .fieldsArr(_fieldsArr)\n .state(_state)\n .klass('grouped-items-area')\n );\n }\n\n section.presets = function(val) {\n if (!arguments.length) return _presets;\n if (!_presets || !val || !utilArrayIdentical(_presets, val)) {\n _presets = val;\n _fieldsArr = null;\n }\n return section;\n };\n\n section.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n return section;\n };\n\n section.tags = function(val) {\n if (!arguments.length) return _tags;\n _tags = val;\n // Don't reset _fieldsArr here.\n return section;\n };\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n if (!val || !_entityIDs || !utilArrayIdentical(_entityIDs, val)) {\n _entityIDs = val;\n _fieldsArr = null;\n }\n return section;\n };\n\n return utilRebind(section, dispatch, 'on');\n}\n", "import { drag as d3_drag } from 'd3-drag';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { actionChangeMember } from '../../actions/change_member';\nimport { actionDeleteMember } from '../../actions/delete_member';\nimport { actionMoveMember } from '../../actions/move_member';\nimport { modeBrowse } from '../../modes/browse';\nimport { modeSelect } from '../../modes/select';\nimport { osmEntity } from '../../osm';\nimport { getRelationColor } from '../../osm/tags';\nimport { svgIcon } from '../../svg/icon';\nimport { services } from '../../services';\nimport { uiCombobox } from '../combobox';\nimport { uiSection } from '../section';\nimport { utilDisplayName, utilDisplayType, utilHighlightEntities, utilNoAuto, utilUniqueDomId } from '../../util';\nimport { prefs } from '../../core';\n\n\nexport function uiSectionRawMemberEditor(context) {\n\n var section = uiSection('raw-member-editor', context)\n .shouldDisplay(function() {\n if (!_entityIDs || _entityIDs.length !== 1) return false;\n\n var entity = context.hasEntity(_entityIDs[0]);\n return entity && entity.type === 'relation';\n })\n .label(function() {\n var entity = context.hasEntity(_entityIDs[0]);\n if (!entity) return '';\n\n var gt = entity.members.length > _maxMembers ? '>' : '';\n var count = gt + entity.members.slice(0, _maxMembers).length;\n return t.append('inspector.title_count', { title: t.append('inspector.members'), count: count });\n })\n .disclosureContent(renderDisclosureContent);\n\n var taginfo = services.taginfo;\n var _entityIDs;\n var _maxMembers = 1000;\n\n function downloadMember(d3_event, d) {\n d3_event.preventDefault();\n\n // display the loading indicator\n d3_select(this).classed('loading', true);\n context.loadEntity(d.id, function() {\n section.reRender();\n });\n }\n\n function zoomToMember(d3_event, d) {\n d3_event.preventDefault();\n\n var entity = context.entity(d.id);\n context.map().zoomToEase(entity);\n\n // highlight the feature in case it wasn't previously on-screen\n utilHighlightEntities([d.id], true, context);\n }\n\n\n function selectMember(d3_event, d) {\n d3_event.preventDefault();\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.id], false, context);\n\n var entity = context.entity(d.id);\n var mapExtent = context.map().extent();\n if (!entity.intersects(mapExtent, context.graph())) {\n // zoom to the entity if its extent is not visible now\n context.map().zoomToEase(entity);\n }\n\n context.enter(modeSelect(context, [d.id]));\n }\n\n\n function changeRole(d3_event, d) {\n var oldRole = d.role;\n var newRole = context.cleanRelationRole(d3_select(this).property('value'));\n\n if (oldRole !== newRole) {\n var member = { id: d.id, type: d.type, role: newRole };\n context.perform(\n actionChangeMember(d.relation.id, member, d.index),\n t('operations.change_role.annotation', {\n n: 1\n })\n );\n context.validator().validate();\n }\n }\n\n\n function deleteMember(d3_event, d) {\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.id], false, context);\n\n context.perform(\n actionDeleteMember(d.relation.id, d.index),\n t('operations.delete_member.annotation', {\n n: 1\n })\n );\n\n if (!context.hasEntity(d.relation.id)) {\n // Removing the last member will also delete the relation.\n // If this happens we need to exit the selection mode\n context.enter(modeBrowse(context));\n } else {\n // Changing the mode also runs `validate`, but otherwise we need to\n // rerun it manually\n context.validator().validate();\n }\n }\n\n function renderDisclosureContent(selection) {\n\n var entityID = _entityIDs[0];\n\n var memberships = [];\n var entity = context.entity(entityID);\n entity.members.slice(0, _maxMembers).forEach(function(member, index) {\n memberships.push({\n index: index,\n id: member.id,\n type: member.type,\n role: member.role,\n relation: entity,\n member: context.hasEntity(member.id),\n domId: utilUniqueDomId(entityID + '-member-' + index)\n });\n });\n\n var list = selection.selectAll('.member-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'member-list')\n .merge(list);\n\n\n var items = list.selectAll('li')\n .data(memberships, function(d) {\n return osmEntity.key(d.relation) + ',' + d.index + ',' +\n (d.member ? osmEntity.key(d.member) : 'incomplete');\n });\n\n items.exit()\n .each(unbind)\n .remove();\n\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', 'member-row form-field')\n .classed('member-incomplete', function(d) { return !d.member; });\n\n itemsEnter\n .each(function(d) {\n var item = d3_select(this);\n\n var label = item\n .append('label')\n .attr('class', 'field-label')\n .attr('for', d.domId);\n\n if (d.member) {\n // highlight the member feature in the map while hovering on the list item\n item\n .on('mouseover', function() {\n utilHighlightEntities([d.id], true, context);\n })\n .on('mouseout', function() {\n utilHighlightEntities([d.id], false, context);\n });\n\n var labelLink = label\n .append('span')\n .attr('class', 'label-text')\n .append('a')\n .attr('href', '#')\n .on('click', selectMember);\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-type')\n .text(function(d) {\n var matched = presetManager.match(d.member, context.graph());\n while (matched?.suggestion) {\n // if is NSI preset: look for a parent preset\n matched = matched.getParentPreset();\n }\n return (matched && matched.name()) || utilDisplayType(d.member.id);\n });\n\n const showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n labelLink.each(function(d) {\n if (!showThirdPartyIcons) return;\n const matched = presetManager.match(d, context.graph());\n if (matched.suggestion) {\n // if matching an NSI preset: append icon\n d3_select(this)\n .append('img')\n .classed('member-entity-icon', true)\n .attr('src', matched.imageURL);\n }\n });\n\n labelLink.each(function(d) {\n if (d.type !== 'relation') return;\n const relColors = getRelationColor(d.member.tags, '#555');\n const hasRef = d.member.tags.ref;\n if (relColors.isValid || hasRef) {\n const refs = (d.member.tags.ref || '').split(';');\n for (const ref of refs) {\n d3_select(this)\n .append('span')\n .classed('member-entity-ref-color', true)\n .style('border-color', relColors.color)\n .style('background-color', relColors.color)\n .style('color', relColors.textColor)\n .text(ref);\n }\n }\n });\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-name')\n .text(function(d) { return utilDisplayName(d.member, { hideRef: true }); });\n\n label\n .append('button')\n .attr('title', t('icons.remove'))\n .attr('class', 'remove member-delete')\n .call(svgIcon('#iD-operation-delete'));\n\n label\n .append('button')\n .attr('class', 'member-zoom')\n .attr('title', t('icons.zoom_to'))\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'))\n .on('click', zoomToMember);\n\n } else {\n var labelText = label\n .append('span')\n .attr('class', 'label-text');\n\n labelText\n .append('span')\n .attr('class', 'member-entity-type')\n .call(t.append('inspector.' + d.type, { id: d.id }));\n\n labelText\n .append('span')\n .attr('class', 'member-entity-name')\n .call(t.append('inspector.incomplete', { id: d.id }));\n\n label\n .append('button')\n .attr('class', 'member-download')\n .attr('title', t('icons.download'))\n .call(svgIcon('#iD-icon-load'))\n .on('click', downloadMember);\n }\n });\n\n var wrapEnter = itemsEnter\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-member');\n\n wrapEnter\n .append('input')\n .attr('class', 'member-role')\n .attr('id', function(d) {\n return d.domId;\n })\n .property('type', 'text')\n .attr('placeholder', t('inspector.role'))\n .call(utilNoAuto);\n\n if (taginfo) {\n wrapEnter.each(bindTypeahead);\n }\n\n // update\n items = items\n .merge(itemsEnter)\n .order();\n\n items.select('input.member-role')\n .property('value', function(d) { return d.role; })\n .on('blur', changeRole)\n .on('change', changeRole);\n\n items.select('button.member-delete')\n .on('click', deleteMember);\n\n var dragOrigin, targetIndex;\n\n items.call(d3_drag()\n .on('start', function(d3_event) {\n dragOrigin = {\n x: d3_event.x,\n y: d3_event.y\n };\n targetIndex = null;\n })\n .on('drag', function(d3_event) {\n var x = d3_event.x - dragOrigin.x,\n y = d3_event.y - dragOrigin.y;\n\n if (!d3_select(this).classed('dragging') &&\n // don't display drag until dragging beyond a distance threshold\n Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) <= 5) return;\n\n var index = items.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', true);\n\n targetIndex = null;\n\n selection.selectAll('li.member-row')\n .style('transform', function(d2, index2) {\n var node = d3_select(this).node();\n if (index === index2) {\n return 'translate(' + x + 'px, ' + y + 'px)';\n } else if (index2 > index && d3_event.y > node.offsetTop) {\n if (targetIndex === null || index2 > targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(-100%)';\n } else if (index2 < index && d3_event.y < node.offsetTop + node.offsetHeight) {\n if (targetIndex === null || index2 < targetIndex) {\n targetIndex = index2;\n }\n return 'translateY(100%)';\n }\n return null;\n });\n })\n .on('end', function(d3_event, d) {\n\n if (!d3_select(this).classed('dragging')) return;\n\n var index = items.nodes().indexOf(this);\n\n d3_select(this)\n .classed('dragging', false);\n\n selection.selectAll('li.member-row')\n .style('transform', null);\n\n if (targetIndex !== null) {\n // dragged to a new position, reorder\n context.perform(\n actionMoveMember(d.relation.id, index, targetIndex),\n t('operations.reorder_members.annotation')\n );\n context.validator().validate();\n }\n })\n );\n\n\n\n function bindTypeahead(d) {\n var row = d3_select(this);\n var role = row.selectAll('input.member-role');\n var origValue = role.property('value');\n\n function sort(value, data) {\n var sameletter = [];\n var other = [];\n for (var i = 0; i < data.length; i++) {\n if (data[i].value.substring(0, value.length) === value) {\n sameletter.push(data[i]);\n } else {\n other.push(data[i]);\n }\n }\n return sameletter.concat(other);\n }\n\n role.call(uiCombobox(context, 'member-role')\n .fetcher(function(role, callback) {\n // The `geometry` param is used in the `taginfo.js` interface for\n // filtering results, as a key into the `tag_members_fractions`\n // object. If we don't know the geometry because the member is\n // not yet downloaded, it's ok to guess based on type.\n var geometry;\n if (d.member) {\n geometry = context.graph().geometry(d.member.id);\n } else if (d.type === 'relation') {\n geometry = 'relation';\n } else if (d.type === 'way') {\n geometry = 'line';\n } else {\n geometry = 'point';\n }\n\n var rtype = entity.tags.type;\n taginfo.roles({\n debounce: true,\n rtype: rtype || '',\n geometry: geometry,\n query: role\n }, function(err, data) {\n if (!err) callback(sort(role, data));\n });\n })\n .on('cancel', function() {\n role.property('value', origValue);\n })\n );\n }\n\n\n function unbind() {\n var row = d3_select(this);\n\n row.selectAll('input.member-role')\n .call(uiCombobox.off, context);\n }\n }\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return section;\n };\n\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { t, localizer } from '../../core/localizer';\n\nimport { actionAddEntity } from '../../actions/add_entity';\nimport { actionAddMember } from '../../actions/add_member';\nimport { actionChangeMember } from '../../actions/change_member';\nimport { actionDeleteMembers } from '../../actions/delete_members';\n\nimport { modeSelect } from '../../modes/select';\nimport { osmEntity, osmRelation } from '../../osm';\nimport { getRelationColor, isColorValid } from '../../osm/tags';\nimport { services } from '../../services';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCombobox } from '../combobox';\nimport { uiSection } from '../section';\nimport { uiTooltip } from '../tooltip';\nimport { utilArrayGroupBy, utilArrayIntersection } from '../../util/array';\nimport { utilDisplayName, utilNoAuto, utilHighlightEntities, utilUniqueDomId } from '../../util';\nimport { prefs } from '../../core';\nimport { idMatch } from '../feature_list';\n\n\nexport function uiSectionRawMembershipEditor(context) {\n\n var section = uiSection('raw-membership-editor', context)\n .shouldDisplay(function() {\n return _entityIDs && _entityIDs.length;\n })\n .label(function() {\n var parents = getSharedParentRelations();\n var gt = parents.length > _maxMemberships ? '>' : '';\n var count = gt + parents.slice(0, _maxMemberships).length;\n return t.append('inspector.title_count', { title: t.append('inspector.relations'), count: count });\n })\n .disclosureContent(renderDisclosureContent);\n\n var taginfo = services.taginfo;\n var nearbyCombo = uiCombobox(context, 'parent-relation')\n .minItems(1)\n .fetcher(fetchNearbyRelations)\n .itemsMouseEnter(function(d3_event, d) {\n if (d.relation) utilHighlightEntities([d.relation.id], true, context);\n })\n .itemsMouseLeave(function(d3_event, d) {\n if (d.relation) utilHighlightEntities([d.relation.id], false, context);\n });\n var _inChange = false;\n var _entityIDs = [];\n var _showBlank;\n var _maxMemberships = 1000;\n /** @type {Set} relations that were added after this panel was opened */\n const recentlyAdded = new Set();\n\n function getSharedParentRelations() {\n var parents = [];\n for (var i = 0; i < _entityIDs.length; i++) {\n var entity = context.graph().hasEntity(_entityIDs[i]);\n if (!entity) continue;\n\n if (i === 0) {\n parents = context.graph().parentRelations(entity);\n } else {\n parents = utilArrayIntersection(parents, context.graph().parentRelations(entity));\n }\n if (!parents.length) break;\n }\n return parents;\n }\n\n function getMemberships() {\n\n var memberships = [];\n var relations = getSharedParentRelations().slice(0, _maxMemberships);\n\n var isMultiselect = _entityIDs.length > 1;\n\n var i, relation, membership, index, member, indexedMember;\n for (i = 0; i < relations.length; i++) {\n relation = relations[i];\n membership = {\n relation: relation,\n members: [],\n hash: osmEntity.key(relation)\n };\n for (index = 0; index < relation.members.length; index++) {\n member = relation.members[index];\n if (_entityIDs.indexOf(member.id) !== -1) {\n indexedMember = Object.assign({}, member, { index: index });\n membership.members.push(indexedMember);\n membership.hash += ',' + index.toString();\n\n if (!isMultiselect) {\n // For single selections, list one entry per membership per relation.\n // For multiselections, list one entry per relation.\n\n memberships.push(membership);\n membership = {\n relation: relation,\n members: [],\n hash: osmEntity.key(relation)\n };\n }\n }\n }\n if (membership.members.length) memberships.push(membership);\n }\n\n memberships.forEach(function(membership) {\n membership.domId = utilUniqueDomId('membership-' + membership.relation.id);\n var roles = [];\n membership.members.forEach(function(member) {\n if (roles.indexOf(member.role) === -1) roles.push(member.role);\n });\n membership.role = roles.length === 1 ? roles[0] : roles;\n });\n\n const existingRelations = memberships\n .filter(membership => !recentlyAdded.has(membership.relation.id))\n .map(membership => ({\n ...membership,\n // We only sort relations that were not added just now.\n // Sorting uses the same label as shown in the UI.\n // If the label is not unique, the relation ID ensures\n // that the sort order is still stable.\n _sortKey: [\n baseDisplayValue(membership.relation),\n membership.relation.id,\n ].join('-'),\n }))\n .sort((a, b) => {\n return a._sortKey.localeCompare(\n b._sortKey,\n localizer.localeCodes(),\n { numeric: true },\n );\n });\n\n\n const newlyAddedRelations = memberships\n .filter(membership => recentlyAdded.has(membership.relation.id));\n\n return [\n // the sorted relations come first\n ...existingRelations,\n // then the ones that were just added from this panel\n ...newlyAddedRelations,\n ];\n }\n\n function selectRelation(d3_event, d) {\n d3_event.preventDefault();\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.relation.id], false, context);\n\n context.enter(modeSelect(context, [d.relation.id]));\n }\n\n function zoomToRelation(d3_event, d) {\n d3_event.preventDefault();\n\n var entity = context.entity(d.relation.id);\n context.map().zoomToEase(entity);\n\n // highlight the relation in case it wasn't previously on-screen\n utilHighlightEntities([d.relation.id], true, context);\n }\n\n\n function changeRole(d3_event, d) {\n if (d === 0) return; // called on newrow (shouldn't happen)\n if (_inChange) return; // avoid accidental recursive call #5731\n\n var newRole = context.cleanRelationRole(d3_select(this).property('value'));\n\n if (!newRole.trim() && typeof d.role !== 'string') return;\n\n var membersToUpdate = d.members.filter(function(member) {\n return member.role !== newRole;\n });\n\n if (membersToUpdate.length) {\n _inChange = true;\n context.perform(\n function actionChangeMemberRoles(graph) {\n membersToUpdate.forEach(function(member) {\n var newMember = Object.assign({}, member, { role: newRole });\n delete newMember.index;\n graph = actionChangeMember(d.relation.id, newMember, member.index)(graph);\n });\n return graph;\n },\n t('operations.change_role.annotation', {\n n: membersToUpdate.length\n })\n );\n context.validator().validate();\n }\n _inChange = false;\n }\n\n\n function addMembership(d, role) {\n _showBlank = false;\n\n function actionAddMembers(relationId, ids, role) {\n return function(graph) {\n for (var i in ids) {\n var member = { id: ids[i], type: graph.entity(ids[i]).type, role: role };\n graph = actionAddMember(relationId, member)(graph);\n }\n return graph;\n };\n }\n\n if (d.relation) {\n recentlyAdded.add(d.relation.id);\n context.perform(\n actionAddMembers(d.relation.id, _entityIDs, role),\n t('operations.add_member.annotation', {\n n: _entityIDs.length\n })\n );\n context.validator().validate();\n\n } else {\n var relation = osmRelation();\n context.perform(\n actionAddEntity(relation),\n actionAddMembers(relation.id, _entityIDs, role),\n t('operations.add.annotation.relation')\n );\n // changing the mode also runs `validate`\n context.enter(modeSelect(context, [relation.id]).newFeature(true));\n }\n }\n\n\n function downloadMembers(d3_event, d) {\n d3_event.preventDefault();\n const button = d3_select(this);\n\n // display the loading indicator\n button.classed('loading', true);\n context.loadEntity(d.relation.id, function() {\n section.reRender();\n });\n }\n\n\n function deleteMembership(d3_event, d) {\n this.blur(); // avoid keeping focus on the button\n if (d === 0) return; // called on newrow (shouldn't happen)\n\n // remove the hover-highlight styling\n utilHighlightEntities([d.relation.id], false, context);\n\n var indexes = d.members.map(function(member) {\n return member.index;\n });\n\n context.perform(\n actionDeleteMembers(d.relation.id, indexes),\n t('operations.delete_member.annotation', {\n n: _entityIDs.length\n })\n );\n context.validator().validate();\n }\n\n\n function fetchNearbyRelations(q, callback) {\n var newRelation = {\n relation: null,\n value: t('inspector.new_relation'),\n display: t.append('inspector.new_relation')\n };\n\n var entityID = _entityIDs[0];\n\n var result = [];\n\n var graph = context.graph();\n\n function baseDisplayLabel(entity) {\n var matched = presetManager.match(entity, graph);\n var presetName = (matched && matched.name()) || t('inspector.relation');\n var entityName = utilDisplayName(entity) || '';\n\n return selection => {\n selection\n .append('b')\n .text(presetName + ' ');\n selection\n .append('span')\n .classed('has-colour', entity.tags.colour && isColorValid(entity.tags.colour))\n .style('border-color', entity.tags.colour)\n .text(entityName);\n };\n }\n\n\n // A location search takes priority over an ID search\n const idMatchResult = q && idMatch(q);\n var explicitRelation = context.hasEntity(`r${idMatchResult?.id || q}`);\n if (explicitRelation && explicitRelation.type === 'relation' && explicitRelation.id !== entityID) {\n // loaded relation is specified explicitly, only show that\n\n result.push({\n relation: explicitRelation,\n value: baseDisplayValue(explicitRelation) + ' ' + explicitRelation.id,\n display: baseDisplayLabel(explicitRelation)\n });\n } else {\n\n context.history().intersects(context.map().extent()).forEach(function(entity) {\n if (entity.type !== 'relation' || entity.id === entityID) return;\n\n var value = baseDisplayValue(entity);\n if (q && (value + ' ' + entity.id).toLowerCase().indexOf(q.toLowerCase()) === -1) return;\n\n result.push({\n relation: entity,\n value,\n display: baseDisplayLabel(entity)\n });\n });\n\n result.sort(function(a, b) {\n return osmRelation.creationOrder(a.relation, b.relation);\n });\n\n // Dedupe identical names by appending relation id - see #2891\n Object.values(utilArrayGroupBy(result, 'value'))\n .filter(v => v.length > 1)\n .flat()\n .forEach(obj => obj.value += ' ' + obj.relation.id);\n }\n\n result.forEach(function(obj) {\n obj.title = obj.value;\n });\n\n result.unshift(newRelation);\n callback(result);\n }\n\n function baseDisplayValue(entity) {\n const graph = context.graph();\n var matched = presetManager.match(entity, graph);\n var presetName = (matched && matched.name()) || t('inspector.relation');\n var entityName = utilDisplayName(entity) || '';\n\n return presetName + ' ' + entityName;\n }\n\n function renderDisclosureContent(selection) {\n\n var memberships = getMemberships();\n\n var list = selection.selectAll('.member-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'member-list')\n .merge(list);\n\n\n var items = list.selectAll('li.member-row-normal')\n .data(memberships, function(d) {\n return d.hash;\n });\n\n items.exit()\n .each(unbind)\n .remove();\n\n // Enter\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', 'member-row member-row-normal form-field');\n\n // highlight the relation in the map while hovering on the list item\n itemsEnter.on('mouseover', function(d3_event, d) {\n utilHighlightEntities([d.relation.id], true, context);\n })\n .on('mouseout', function(d3_event, d) {\n utilHighlightEntities([d.relation.id], false, context);\n });\n\n var labelEnter = itemsEnter\n .append('label')\n .attr('class', 'field-label')\n .attr('for', function(d) {\n return d.domId;\n });\n\n var labelLink = labelEnter\n .append('span')\n .attr('class', 'label-text')\n .append('a')\n .attr('href', '#')\n .on('click', selectRelation);\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-type')\n .text(d => {\n let matched = presetManager.match(d.relation, context.graph());\n while (matched?.suggestion) {\n // if is NSI preset: look for a parent preset\n matched = matched.getParentPreset();\n }\n return (matched && matched.name()) || t('inspector.relation');\n });\n\n const showThirdPartyIcons = prefs('preferences.privacy.thirdpartyicons') || 'true';\n labelLink.each(function(d) {\n if (!showThirdPartyIcons) return;\n const matched = presetManager.match(d.relation, context.graph());\n if (matched.suggestion) {\n // if matching an NSI preset: append icon\n d3_select(this)\n .append('img')\n .classed('member-entity-icon', true)\n .attr('src', matched.imageURL);\n }\n });\n\n labelLink.each(function(d) {\n const relColors = getRelationColor(d.relation.tags, '#555');\n const hasRef = d.relation.tags.ref;\n if (relColors.isValid || hasRef) {\n const refs = (d.relation.tags.ref || '').split(';');\n for (const ref of refs) {\n d3_select(this)\n .append('span')\n .classed('member-entity-ref-color', true)\n .style('border-color', relColors.color)\n .style('background-color', relColors.color)\n .style('color', relColors.textColor)\n .text(ref);\n }\n }\n });\n\n labelLink\n .append('span')\n .attr('class', 'member-entity-name')\n .text(d => utilDisplayName(d.relation, { hideRef: true }));\n\n labelEnter\n .append('button')\n .attr('class', 'members-download')\n .attr('title', t('icons.download'))\n .call(svgIcon('#iD-icon-load'))\n .on('click', downloadMembers);\n\n labelEnter\n .append('button')\n .attr('class', 'remove member-delete')\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'))\n .on('click', deleteMembership);\n\n labelEnter\n .append('button')\n .attr('class', 'member-zoom')\n .attr('title', t('icons.zoom_to'))\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'))\n .on('click', zoomToRelation);\n\n items = items.merge(itemsEnter);\n items.selectAll('button.members-download')\n .classed('hide', d => {\n const graph = context.graph();\n return d.relation.members.every(m => graph.hasEntity(m.id));\n });\n\n const dupeLabels = new WeakSet(Object.values(\n utilArrayGroupBy(items.selectAll('.label-text').nodes(), 'textContent'))\n .filter(v => v.length > 1)\n .flat());\n\n items.select('.label-text').each(function() {\n const label = d3_select(this);\n const entityName = label.select('.member-entity-name');\n if (dupeLabels.has(this)) {\n // Dedupe identical names in hover text by appending relation id - see #2891, #10184\n label.attr('title', d => `${entityName.text()} ${d.relation.id}`);\n } else {\n // set full label also as hover text: useful if a (long) label is cut off with an \u2026 ellipsis\n label.attr('title', () => entityName.text());\n }\n });\n\n var wrapEnter = itemsEnter\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-member');\n\n wrapEnter\n .append('input')\n .attr('class', 'member-role')\n .attr('id', function(d) {\n return d.domId;\n })\n .property('type', 'text')\n .property('value', function(d) {\n return typeof d.role === 'string' ? d.role : '';\n })\n .attr('title', function(d) {\n return Array.isArray(d.role) ? d.role.filter(Boolean).join('\\n') : d.role;\n })\n .attr('placeholder', function(d) {\n return Array.isArray(d.role) ? t('inspector.multiple_roles') : t('inspector.role');\n })\n .classed('mixed', function(d) {\n return Array.isArray(d.role);\n })\n .call(utilNoAuto)\n .on('blur', changeRole)\n .on('change', changeRole);\n\n if (taginfo) {\n wrapEnter.each(bindTypeahead);\n }\n\n var newMembership = list.selectAll('.member-row-new')\n .data(_showBlank ? [0] : []);\n\n // Exit\n newMembership.exit()\n .remove();\n\n // Enter\n var newMembershipEnter = newMembership.enter()\n .append('li')\n .attr('class', 'member-row member-row-new form-field');\n\n var newLabelEnter = newMembershipEnter\n .append('label')\n .attr('class', 'field-label');\n\n newLabelEnter\n .append('input')\n .attr('placeholder', t('inspector.choose_relation'))\n .attr('type', 'text')\n .attr('class', 'member-entity-input')\n .call(utilNoAuto);\n\n newLabelEnter\n .append('button')\n .attr('class', 'remove member-delete')\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'))\n .on('click', function() {\n list.selectAll('.member-row-new')\n .remove();\n });\n\n var newWrapEnter = newMembershipEnter\n .append('div')\n .attr('class', 'form-field-input-wrap form-field-input-member');\n\n newWrapEnter\n .append('input')\n .attr('class', 'member-role')\n .property('type', 'text')\n .attr('placeholder', t('inspector.role'))\n .call(utilNoAuto);\n\n // Update\n newMembership = newMembership\n .merge(newMembershipEnter);\n\n newMembership.selectAll('.member-entity-input')\n .on('blur', cancelEntity) // if it wasn't accepted normally, cancel it\n .call(nearbyCombo\n .on('accept', function(d) {\n this.blur(); // always blurs the triggering element\n acceptEntity.call(this, d);\n })\n .on('cancel', cancelEntity)\n );\n\n\n // Container for the Add button\n var addRow = selection.selectAll('.add-row')\n .data([0]);\n\n // enter\n var addRowEnter = addRow.enter()\n .append('div')\n .attr('class', 'add-row');\n\n var addRelationButton = addRowEnter\n .append('button')\n .attr('class', 'add-relation')\n .attr('aria-label', t('inspector.add_to_relation'));\n\n addRelationButton\n .call(svgIcon('#iD-icon-plus', 'light'));\n addRelationButton\n .call(uiTooltip()\n .title(() => t.append('inspector.add_to_relation'))\n .placement(localizer.textDirection() === 'ltr' ? 'right' : 'left'));\n\n addRowEnter\n .append('div')\n .attr('class', 'space-value'); // preserve space\n\n addRowEnter\n .append('div')\n .attr('class', 'space-buttons'); // preserve space\n\n // update\n addRow = addRow\n .merge(addRowEnter);\n\n addRow.select('.add-relation')\n .on('click', function() {\n _showBlank = true;\n section.reRender();\n list.selectAll('.member-entity-input').node().focus();\n });\n\n\n function acceptEntity(d) {\n if (!d) {\n cancelEntity();\n return;\n }\n // remove hover-higlighting\n if (d.relation) utilHighlightEntities([d.relation.id], false, context);\n\n var role = context.cleanRelationRole(list.selectAll('.member-row-new .member-role').property('value'));\n addMembership(d, role);\n }\n\n\n function cancelEntity() {\n var input = newMembership.selectAll('.member-entity-input');\n input.property('value', '');\n\n // remove hover-higlighting\n context.surface().selectAll('.highlighted')\n .classed('highlighted', false);\n }\n\n\n function bindTypeahead(d) {\n var row = d3_select(this);\n var role = row.selectAll('input.member-role');\n var origValue = role.property('value');\n\n function sort(value, data) {\n var sameletter = [];\n var other = [];\n for (var i = 0; i < data.length; i++) {\n if (data[i].value.substring(0, value.length) === value) {\n sameletter.push(data[i]);\n } else {\n other.push(data[i]);\n }\n }\n return sameletter.concat(other);\n }\n\n role.call(uiCombobox(context, 'member-role')\n .fetcher(function(role, callback) {\n var rtype = d.relation.tags.type;\n taginfo.roles({\n debounce: true,\n rtype: rtype || '',\n geometry: context.graph().geometry(_entityIDs[0]),\n query: role\n }, function(err, data) {\n if (!err) callback(sort(role, data));\n });\n })\n .on('cancel', function() {\n role.property('value', origValue);\n })\n );\n }\n\n\n function unbind() {\n var row = d3_select(this);\n\n row.selectAll('input.member-role')\n .call(uiCombobox.off, context);\n }\n }\n\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n const didChange = _entityIDs.join(',') !== val.join(',');\n _entityIDs = val;\n _showBlank = false;\n if (didChange) {\n recentlyAdded.clear(); // reset when the selected feature changes\n }\n return section;\n };\n\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { presetManager } from '../../presets';\nimport { modeSelect } from '../../modes/select';\nimport { osmEntity } from '../../osm';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\nimport { t } from '../../core/localizer';\nimport { utilDisplayName, utilHighlightEntities } from '../../util';\n\nexport function uiSectionSelectionList(context) {\n\n var _selectedIDs = [];\n\n var section = uiSection('selected-features', context)\n .shouldDisplay(function() {\n return _selectedIDs.length > 1;\n })\n .label(function() {\n return t.append('inspector.title_count', { title: t.append('inspector.features'), count: _selectedIDs.length });\n })\n .disclosureContent(renderDisclosureContent);\n\n context.history()\n .on('change.selectionList', function(difference) {\n if (difference) {\n section.reRender();\n }\n });\n\n section.entityIDs = function(val) {\n if (!arguments.length) return _selectedIDs;\n _selectedIDs = val;\n return section;\n };\n\n function selectEntity(d3_event, entity) {\n context.enter(modeSelect(context, [entity.id]));\n }\n\n function deselectEntity(d3_event, entity) {\n var selectedIDs = _selectedIDs.slice();\n var index = selectedIDs.indexOf(entity.id);\n if (index > -1) {\n selectedIDs.splice(index, 1);\n context.enter(modeSelect(context, selectedIDs));\n }\n }\n\n function renderDisclosureContent(selection) {\n\n var list = selection.selectAll('.feature-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'feature-list')\n .merge(list);\n\n var entities = _selectedIDs\n .map(function(id) { return context.hasEntity(id); })\n .filter(Boolean);\n\n var items = list.selectAll('.feature-list-item')\n .data(entities, osmEntity.key);\n\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li')\n .attr('class', 'feature-list-item')\n .each(function(d) {\n d3_select(this)\n .on('mouseover', function() {\n utilHighlightEntities([d.id], true, context);\n })\n .on('mouseout', function() {\n utilHighlightEntities([d.id], false, context);\n });\n });\n\n var label = enter\n .append('button')\n .attr('class', 'label')\n .on('click', selectEntity);\n\n label\n .append('span')\n .attr('class', 'entity-geom-icon')\n .call(svgIcon('', 'pre-text'));\n\n label\n .append('span')\n .attr('class', 'entity-type');\n\n label\n .append('span')\n .attr('class', 'entity-name');\n\n enter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.deselect'))\n .on('click', deselectEntity)\n .call(svgIcon('#iD-icon-close'));\n\n // Update\n items = items.merge(enter);\n\n items.selectAll('.entity-geom-icon use')\n .attr('href', function() {\n var entity = this.parentNode.parentNode.__data__;\n return '#iD-icon-' + entity.geometry(context.graph());\n });\n\n items.selectAll('.entity-type')\n .text(function(entity) { return presetManager.match(entity, context.graph()).name(); });\n\n items.selectAll('.entity-name')\n .text(function(d) {\n // fetch latest entity\n var entity = context.entity(d.id);\n return utilDisplayName(entity);\n });\n }\n\n return section;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { deepEqual } from 'fast-equals';\n\nimport { presetManager } from '../presets';\nimport { t, localizer } from '../core/localizer';\nimport { actionChangeTags } from '../actions/change_tags';\nimport { modeBrowse } from '../modes/browse';\nimport { svgIcon } from '../svg/icon';\nimport { utilArrayIdentical } from '../util/array';\nimport { utilCleanTags, utilCombinedTags, utilRebind } from '../util';\n\nimport { uiSectionEntityIssues } from './sections/entity_issues';\nimport { uiSectionFeatureType } from './sections/feature_type';\nimport { uiSectionPresetFields } from './sections/preset_fields';\nimport { uiSectionRawMemberEditor } from './sections/raw_member_editor';\nimport { uiSectionRawMembershipEditor } from './sections/raw_membership_editor';\nimport { uiSectionRawTagEditor } from './sections/raw_tag_editor';\nimport { uiSectionSelectionList } from './sections/selection_list';\n\nexport function uiEntityEditor(context) {\n var dispatch = d3_dispatch('choose');\n var _state = 'select';\n var _coalesceChanges = false;\n var _modified = false;\n var _base;\n var _entityIDs;\n var _activePresets = [];\n var _newFeature;\n\n var _sections;\n\n function entityEditor(selection) {\n\n var combinedTags = utilCombinedTags(_entityIDs, context.graph());\n\n // Header\n var header = selection.selectAll('.header')\n .data([0]);\n\n // Enter\n var headerEnter = header.enter()\n .append('div')\n .attr('class', 'header fillL');\n\n var direction = (localizer.textDirection() === 'rtl') ? 'forward' : 'backward';\n\n headerEnter\n .append('button')\n .attr('class', 'preset-reset preset-choose')\n .attr('title', t('inspector.back_tooltip'))\n .call(svgIcon(`#iD-icon-${direction}`));\n\n headerEnter\n .append('button')\n .attr('class', 'close')\n .attr('title', t('icons.close'))\n .on('click', function() { context.enter(modeBrowse(context)); })\n .call(svgIcon(_modified ? '#iD-icon-apply' : '#iD-icon-close'));\n\n headerEnter\n .append('h2');\n\n // Update\n header = header\n .merge(headerEnter);\n\n header.selectAll('h2')\n .text('')\n .call(_entityIDs.length === 1 ? t.append('inspector.edit') : t.append('inspector.edit_features'));\n\n header.selectAll('.preset-reset')\n .on('click', function() {\n dispatch.call('choose', this, _activePresets);\n });\n\n // Body\n var body = selection.selectAll('.inspector-body')\n .data([0]);\n\n // Enter\n var bodyEnter = body.enter()\n .append('div')\n .attr('class', 'entity-editor inspector-body sep-top');\n\n // Update\n body = body\n .merge(bodyEnter);\n\n if (!_sections) {\n _sections = [\n uiSectionSelectionList(context),\n uiSectionFeatureType(context).on('choose', function(presets) {\n dispatch.call('choose', this, presets);\n }),\n uiSectionEntityIssues(context),\n uiSectionPresetFields(context).on('change', changeTags).on('revert', revertTags),\n uiSectionRawTagEditor('raw-tag-editor', context).on('change', changeTags),\n uiSectionRawMemberEditor(context),\n uiSectionRawMembershipEditor(context)\n ];\n }\n\n _sections.forEach(function(section) {\n if (section.entityIDs) {\n section.entityIDs(_entityIDs);\n }\n if (section.presets) {\n section.presets(_activePresets);\n }\n if (section.tags) {\n section.tags(combinedTags);\n }\n if (section.state) {\n section.state(_state);\n }\n body.call(section.render);\n });\n\n context.history()\n .on('change.entity-editor', historyChanged);\n\n function historyChanged(difference) {\n if (selection.selectAll('.entity-editor').empty()) return;\n if (_state === 'hide') return;\n var significant = !difference ||\n difference.didChange.properties ||\n difference.didChange.addition ||\n difference.didChange.deletion;\n if (!significant) return;\n\n _entityIDs = _entityIDs.filter(context.hasEntity);\n if (!_entityIDs.length) return;\n\n var priorActivePreset = _activePresets.length === 1 && _activePresets[0];\n\n loadActivePresets();\n\n var graph = context.graph();\n entityEditor.modified(_base !== graph);\n entityEditor(selection);\n\n if (priorActivePreset && _activePresets.length === 1 && priorActivePreset !== _activePresets[0]) {\n // flash the button to indicate the preset changed\n context.container().selectAll('.entity-editor button.preset-reset .label')\n .classed('flash-bg', true)\n .on('animationend', function() {\n d3_select(this).classed('flash-bg', false);\n });\n }\n }\n }\n\n\n // Tag changes that fire on input can all get coalesced into a single\n // history operation when the user leaves the field. #2342\n // Use explicit entityIDs in case the selection changes before the event is fired.\n function changeTags(entityIDs, changed, onInput) {\n\n var actions = [];\n for (var i in entityIDs) {\n var entityID = entityIDs[i];\n var entity = context.entity(entityID);\n\n var tags = Object.assign({}, entity.tags); // shallow copy\n\n if (typeof changed === 'function') {\n // a complex callback tag change\n tags = changed(tags);\n } else {\n for (var k in changed) {\n if (!k) continue;\n var v = changed[k];\n if (typeof v === 'object') {\n // a \"key only\" tag change\n tags[k] = tags[v.oldKey];\n } else if (v !== undefined || tags.hasOwnProperty(k)) {\n tags[k] = v;\n }\n }\n }\n\n if (!onInput) {\n tags = utilCleanTags(tags);\n }\n\n if (!deepEqual(entity.tags, tags)) {\n actions.push(actionChangeTags(entityID, tags));\n }\n }\n\n if (actions.length) {\n var combinedAction = function(graph) {\n actions.forEach(function(action) {\n graph = action(graph);\n });\n return graph;\n };\n\n var annotation = t('operations.change_tags.annotation');\n\n if (_coalesceChanges) {\n context.replace(combinedAction, annotation);\n } else {\n context.perform(combinedAction, annotation);\n }\n _coalesceChanges = !!onInput;\n }\n\n // if leaving field (blur event), rerun validation\n if (!onInput) {\n context.validator().validate();\n }\n }\n\n function revertTags(keys) {\n\n var actions = [];\n for (var i in _entityIDs) {\n var entityID = _entityIDs[i];\n\n var original = context.graph().base().entities[entityID];\n var changed = {};\n for (var j in keys) {\n var key = keys[j];\n changed[key] = original ? original.tags[key] : undefined;\n }\n\n var entity = context.entity(entityID);\n var tags = Object.assign({}, entity.tags); // shallow copy\n\n for (var k in changed) {\n if (!k) continue;\n var v = changed[k];\n if (v !== undefined || tags.hasOwnProperty(k)) {\n tags[k] = v;\n }\n }\n\n\n tags = utilCleanTags(tags);\n\n if (!deepEqual(entity.tags, tags)) {\n actions.push(actionChangeTags(entityID, tags));\n }\n\n }\n\n if (actions.length) {\n var combinedAction = function(graph) {\n actions.forEach(function(action) {\n graph = action(graph);\n });\n return graph;\n };\n\n var annotation = t('operations.change_tags.annotation');\n\n if (_coalesceChanges) {\n context.replace(combinedAction, annotation);\n } else {\n context.perform(combinedAction, annotation);\n }\n }\n\n context.validator().validate();\n }\n\n\n entityEditor.modified = function(val) {\n if (!arguments.length) return _modified;\n _modified = val;\n return entityEditor;\n };\n\n\n entityEditor.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n return entityEditor;\n };\n\n\n entityEditor.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n\n // always reload these even if the entityIDs are unchanged, since we\n // could be reselecting after something like dragging a node\n _base = context.graph();\n _coalesceChanges = false;\n\n if (val && _entityIDs && utilArrayIdentical(_entityIDs, val)) return entityEditor; // exit early if no change\n\n _entityIDs = val;\n\n loadActivePresets(true);\n\n return entityEditor\n .modified(false);\n };\n\n\n entityEditor.newFeature = function(val) {\n if (!arguments.length) return _newFeature;\n _newFeature = val;\n return entityEditor;\n };\n\n\n function loadActivePresets(isForNewSelection) {\n\n var graph = context.graph();\n\n var counts = {};\n\n for (var i in _entityIDs) {\n var entity = graph.hasEntity(_entityIDs[i]);\n if (!entity) return;\n\n var match = presetManager.match(entity, graph);\n\n if (!counts[match.id]) counts[match.id] = 0;\n counts[match.id] += 1;\n }\n\n var matches = Object.keys(counts).sort(function(p1, p2) {\n return counts[p2] - counts[p1];\n }).map(function(pID) {\n return presetManager.item(pID);\n });\n\n if (!isForNewSelection) {\n // A \"weak\" preset doesn't set any tags. (e.g. \"Address\")\n var weakPreset = _activePresets.length === 1 &&\n !_activePresets[0].isFallback() &&\n Object.keys(_activePresets[0].addTags || {}).length === 0;\n // Don't replace a weak preset with a fallback preset (e.g. \"Point\")\n if (weakPreset && matches.length === 1 && matches[0].isFallback()) return;\n }\n\n entityEditor.presets(matches);\n }\n\n entityEditor.presets = function(val) {\n if (!arguments.length) return _activePresets;\n\n // don't reload the same preset\n if (!utilArrayIdentical(val, _activePresets)) {\n _activePresets = val;\n }\n return entityEditor;\n };\n\n return utilRebind(entityEditor, dispatch, 'on');\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { select as d3_select } from 'd3-selection';\nimport { debounce } from 'es-toolkit/compat';\n\nimport { presetManager } from '../presets';\nimport { t, localizer } from '../core/localizer';\nimport { actionChangePreset } from '../actions/change_preset';\nimport { operationDelete } from '../operations/delete';\nimport { svgIcon } from '../svg/index';\nimport { uiTooltip } from './tooltip';\nimport { geoExtent } from '../geo/extent';\nimport { uiPresetIcon } from './preset_icon';\nimport { uiTagReference } from './tag_reference';\nimport { utilKeybinding, utilNoAuto, utilRebind } from '../util';\n\n\nexport function uiPresetList(context) {\n var dispatch = d3_dispatch('cancel', 'choose');\n var _entityIDs;\n var _currLoc;\n var _currentPresets;\n var _autofocus = false;\n\n\n function presetList(selection) {\n if (!_entityIDs) return;\n\n var presets = presetManager.matchAllGeometry(entityGeometries());\n\n selection.html('');\n\n var messagewrap = selection\n .append('div')\n .attr('class', 'header fillL');\n\n var message = messagewrap\n .append('h2')\n .call(t.addOrUpdate('inspector.choose'));\n\n messagewrap\n .append('button')\n .attr('class', 'preset-choose')\n .attr('title', _entityIDs.length === 1 ? t('inspector.edit') : t('inspector.edit_features'))\n .on('click', function() { dispatch.call('cancel', this); })\n .call(svgIcon('#iD-icon-close'));\n\n function initialKeydown(d3_event) {\n // hack to let delete shortcut work when search is autofocused\n if (search.property('value').length === 0 &&\n (d3_event.keyCode === utilKeybinding.keyCodes['\u232B'] ||\n d3_event.keyCode === utilKeybinding.keyCodes['\u2326'])) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n operationDelete(context, _entityIDs)();\n\n // hack to let undo work when search is autofocused\n } else if (search.property('value').length === 0 &&\n (d3_event.ctrlKey || d3_event.metaKey) &&\n d3_event.keyCode === utilKeybinding.keyCodes.z) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n context.undo();\n } else if (!d3_event.ctrlKey && !d3_event.metaKey) {\n // don't check for delete/undo hack on future keydown events\n d3_select(this).on('keydown', keydown);\n keydown.call(this, d3_event);\n }\n }\n\n function keydown(d3_event) {\n // down arrow\n if (d3_event.keyCode === utilKeybinding.keyCodes['\u2193'] &&\n // if insertion point is at the end of the string\n search.node().selectionStart === search.property('value').length) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // move focus to the first item in the preset list\n var buttons = list.selectAll('.preset-list-button');\n if (!buttons.empty()) buttons.nodes()[0].focus();\n }\n }\n\n function keypress(d3_event) {\n // enter\n var value = search.property('value');\n if (d3_event.keyCode === 13 && // \u21A9 Return\n value.length) {\n list.selectAll('.preset-list-item:first-child')\n .each(function(d) { d.choose.call(this); });\n }\n }\n\n function inputevent() {\n var value = search.property('value');\n list.classed('filtered', value.length);\n\n var results, messageText;\n if (value.length) {\n results = presets.search(value, entityGeometries()[0], _currLoc);\n messageText = t.addOrUpdate('inspector.results', {\n n: results.collection.length,\n search: value\n });\n } else {\n var entityPresets = _entityIDs.map(entityID =>\n presetManager.match(context.graph().entity(entityID), context.graph()));\n results = presetManager.defaults(entityGeometries()[0], 36, !context.inIntro(), _currLoc, entityPresets);\n messageText = t.addOrUpdate('inspector.choose');\n }\n list.call(drawList, results);\n message.call(messageText);\n }\n\n var searchWrap = selection\n .append('div')\n .attr('class', 'search-header');\n\n searchWrap\n .call(svgIcon('#iD-icon-search', 'pre-text'));\n\n var search = searchWrap\n .append('input')\n .attr('class', 'preset-search-input')\n .attr('placeholder', t('inspector.search_feature_type'))\n .attr('type', 'search')\n .call(utilNoAuto)\n .on('keydown', initialKeydown)\n .on('keypress', keypress)\n .on('input', debounce(inputevent));\n\n if (_autofocus) {\n search.node().focus();\n\n // Safari 14 doesn't always like to focus immediately,\n // so try again on the next pass\n setTimeout(function() {\n search.node().focus();\n }, 0);\n }\n\n var listWrap = selection\n .append('div')\n .attr('class', 'inspector-body');\n\n var entityPresets = _entityIDs.map(entityID =>\n presetManager.match(context.graph().entity(entityID), context.graph()));\n var list = listWrap\n .append('div')\n .attr('class', 'preset-list')\n .call(drawList, presetManager.defaults(entityGeometries()[0], 36, !context.inIntro(), _currLoc, entityPresets));\n\n context.features().on('change.preset-list', updateForFeatureHiddenState);\n }\n\n\n function drawList(list, presets) {\n presets = presets.matchAllGeometry(entityGeometries());\n var collection = presets.collection.reduce(function(collection, preset) {\n if (!preset) return collection;\n\n if (preset.members) {\n if (preset.members.collection.filter(function(preset) {\n return preset.addable();\n }).length > 1) {\n collection.push(CategoryItem(preset));\n }\n } else if (preset.addable()) {\n collection.push(PresetItem(preset));\n }\n return collection;\n }, []);\n\n var items = list.selectChildren('.preset-list-item')\n .data(collection, function(d) { return d.preset.id; });\n\n items.order();\n\n items.exit()\n .remove();\n\n items.enter()\n .append('div')\n .attr('class', function(item) { return 'preset-list-item preset-' + item.preset.id.replaceAll('/', '-'); })\n .classed('current', function(item) { return _currentPresets.indexOf(item.preset) !== -1; })\n .each(function(item) { d3_select(this).call(item); })\n .style('opacity', 0)\n .transition()\n .style('opacity', 1);\n\n updateForFeatureHiddenState();\n }\n\n function itemKeydown(d3_event) {\n // the actively focused item\n var item = d3_select(this.closest('.preset-list-item'));\n var parentItem = d3_select(item.node().parentNode.closest('.preset-list-item'));\n\n // arrow down, move focus to the next, lower item\n if (d3_event.keyCode === utilKeybinding.keyCodes['\u2193']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // the next item in the list at the same level\n var nextItem = d3_select(item.node().nextElementSibling);\n // if there is no next item in this list\n if (nextItem.empty()) {\n // if there is a parent item\n if (!parentItem.empty()) {\n // the item is the last item of a sublist,\n // select the next item at the parent level\n nextItem = d3_select(parentItem.node().nextElementSibling);\n }\n // if the focused item is expanded\n } else if (d3_select(this).classed('expanded')) {\n // select the first subitem instead\n nextItem = item.select('.subgrid .preset-list-item:first-child');\n }\n if (!nextItem.empty()) {\n // focus on the next item\n nextItem.select('.preset-list-button').node().focus();\n }\n\n // arrow up, move focus to the previous, higher item\n } else if (d3_event.keyCode === utilKeybinding.keyCodes['\u2191']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // the previous item in the list at the same level\n var previousItem = d3_select(item.node().previousElementSibling);\n\n // if there is no previous item in this list\n if (previousItem.empty()) {\n // if there is a parent item\n if (!parentItem.empty()) {\n // the item is the first subitem of a sublist select the parent item\n previousItem = parentItem;\n }\n // if the previous item is expanded\n } else if (previousItem.select('.preset-list-button').classed('expanded')) {\n // select the last subitem of the sublist of the previous item\n previousItem = previousItem.select('.subgrid .preset-list-item:last-child');\n }\n\n if (!previousItem.empty()) {\n // focus on the previous item\n previousItem.select('.preset-list-button').node().focus();\n } else {\n // the focus is at the top of the list, move focus back to the search field\n var search = d3_select(this.closest('.preset-list-pane')).select('.preset-search-input');\n search.node().focus();\n }\n\n // arrow left, move focus to the parent item if there is one\n } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2192' : '\u2190']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // if there is a parent item, focus on the parent item\n if (!parentItem.empty()) {\n parentItem.select('.preset-list-button').node().focus();\n }\n\n // arrow right, choose this item\n } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2190' : '\u2192']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n item.datum().choose.call(d3_select(this).node());\n }\n }\n\n\n function CategoryItem(preset) {\n var box, sublist, shown = false;\n\n function item(selection) {\n var wrap = selection.append('div')\n .attr('class', 'preset-list-button-wrap category');\n\n function click() {\n var isExpanded = d3_select(this).classed('expanded');\n var iconName = isExpanded ?\n (localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward') : '#iD-icon-down';\n d3_select(this)\n .classed('expanded', !isExpanded)\n .attr('title', !isExpanded ? t('icons.collapse') : t('icons.expand'));\n d3_select(this).selectAll('div.label-inner svg.icon use')\n .attr('href', iconName);\n item.choose();\n }\n\n var geometries = entityGeometries();\n\n var button = wrap\n .append('button')\n .attr('class', 'preset-list-button')\n .attr('title', t('icons.expand'))\n .classed('expanded', false)\n .call(uiPresetIcon()\n .geometry(geometries.length === 1 && geometries[0])\n .preset(preset))\n .on('click', click)\n .on('keydown', function(d3_event) {\n // right arrow, expand the focused item\n if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2190' : '\u2192']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // if the item isn't expanded\n if (!d3_select(this).classed('expanded')) {\n // toggle expansion (expand the item)\n click.call(this, d3_event);\n }\n // left arrow, collapse the focused item\n } else if (d3_event.keyCode === utilKeybinding.keyCodes[(localizer.textDirection() === 'rtl') ? '\u2192' : '\u2190']) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n // if the item is expanded\n if (d3_select(this).classed('expanded')) {\n // toggle expansion (collapse the item)\n click.call(this, d3_event);\n }\n } else {\n itemKeydown.call(this, d3_event);\n }\n });\n\n var label = button\n .append('div')\n .attr('class', 'label')\n .append('div')\n .attr('class', 'label-inner');\n\n label\n .append('div')\n .attr('class', 'namepart')\n .call(svgIcon((localizer.textDirection() === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'))\n .append('span')\n .call(preset.nameLabel())\n .append('span').text('\u2026');\n\n box = selection.append('div')\n .attr('class', 'subgrid')\n .style('max-height', '0px')\n .style('opacity', 0);\n\n box.append('div')\n .attr('class', 'arrow');\n\n sublist = box.append('div')\n .attr('class', 'preset-list fillL3');\n }\n\n\n item.choose = function() {\n if (!box || !sublist) return;\n\n if (shown) {\n shown = false;\n box.transition()\n .duration(200)\n .style('opacity', '0')\n .style('max-height', '0px')\n .style('padding-bottom', '0px');\n } else {\n shown = true;\n var members = preset.members.matchAllGeometry(entityGeometries());\n sublist.call(drawList, members);\n box.transition()\n .duration(200)\n .style('opacity', '1')\n .style('max-height', 200 + members.collection.length * 190 + 'px')\n .style('padding-bottom', '10px');\n }\n };\n\n item.preset = preset;\n return item;\n }\n\n\n function PresetItem(preset) {\n function item(selection) {\n var wrap = selection.append('div')\n .attr('class', 'preset-list-button-wrap');\n\n var geometries = entityGeometries();\n\n var button = wrap.append('button')\n .attr('class', 'preset-list-button')\n .call(uiPresetIcon()\n .geometry(geometries.length === 1 && geometries[0])\n .preset(preset))\n .on('click', item.choose)\n .on('keydown', itemKeydown);\n\n var label = button\n .append('div')\n .attr('class', 'label')\n .append('div')\n .attr('class', 'label-inner');\n\n var nameparts = [\n preset.nameLabel(),\n preset.subtitleLabel()\n ].filter(Boolean);\n\n label.selectAll('.namepart')\n .data(nameparts, d => d.stringId)\n .enter()\n .append('div')\n .attr('class', 'namepart')\n .text('')\n .each(function(d) { d(d3_select(this)); });\n\n wrap.call(item.reference.button);\n selection.call(item.reference.body);\n }\n\n item.choose = function() {\n if (d3_select(this).classed('disabled')) return;\n if (!context.inIntro()) {\n presetManager.setMostRecent(preset, entityGeometries()[0]);\n }\n context.perform(\n function(graph) {\n for (var i in _entityIDs) {\n var entityID = _entityIDs[i];\n var oldPreset = presetManager.match(graph.entity(entityID), graph);\n graph = actionChangePreset(entityID, oldPreset, preset)(graph);\n }\n return graph;\n },\n t('operations.change_tags.annotation')\n );\n\n context.validator().validate(); // rerun validation\n dispatch.call('choose', this, preset);\n };\n\n item.help = function(d3_event) {\n d3_event.stopPropagation();\n item.reference.toggle();\n };\n\n item.preset = preset;\n item.reference = uiTagReference(preset.reference(), context);\n\n return item;\n }\n\n\n function updateForFeatureHiddenState() {\n if (!_entityIDs.every(context.hasEntity)) return;\n\n var geometries = entityGeometries();\n var button = context.container().selectAll('.preset-list .preset-list-button');\n\n // remove existing tooltips\n button.call(uiTooltip().destroyAny);\n\n button.each(function(item, index) {\n var hiddenPresetFeaturesId;\n for (var i in geometries) {\n hiddenPresetFeaturesId = context.features().isHiddenPreset(item.preset, geometries[i]);\n if (hiddenPresetFeaturesId) break;\n }\n var isHiddenPreset = !context.inIntro() &&\n !!hiddenPresetFeaturesId &&\n (_currentPresets.length !== 1 || item.preset !== _currentPresets[0]);\n\n d3_select(this)\n .classed('disabled', isHiddenPreset);\n\n if (isHiddenPreset) {\n var isAutoHidden = context.features().autoHidden(hiddenPresetFeaturesId);\n d3_select(this).call(uiTooltip()\n .title(() => t.append('inspector.hidden_preset.' + (isAutoHidden ? 'zoom' : 'manual'), {\n features: t('feature.' + hiddenPresetFeaturesId + '.description')\n }))\n .placement(index < 2 ? 'bottom' : 'top')\n );\n }\n });\n }\n\n presetList.autofocus = function(val) {\n if (!arguments.length) return _autofocus;\n _autofocus = val;\n return presetList;\n };\n\n presetList.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n\n _entityIDs = val;\n _currLoc = null;\n\n if (_entityIDs && _entityIDs.length) {\n // calculate current location\n const extent = _entityIDs.reduce(function(extent, entityID) {\n var entity = context.graph().entity(entityID);\n return extent.extend(entity.extent(context.graph()));\n }, geoExtent());\n _currLoc = extent.center();\n\n // match presets\n var presets = _entityIDs.map(function(entityID) {\n return presetManager.match(context.entity(entityID), context.graph());\n });\n presetList.presets(presets);\n }\n\n return presetList;\n };\n\n presetList.presets = function(val) {\n if (!arguments.length) return _currentPresets;\n _currentPresets = val;\n return presetList;\n };\n\n function entityGeometries() {\n\n var counts = {};\n\n for (var i in _entityIDs) {\n var entityID = _entityIDs[i];\n var entity = context.entity(entityID);\n var geometry = entity.geometry(context.graph());\n\n // Treat entities on addr:interpolation lines as points, not vertices (#3241)\n if (geometry === 'vertex' && entity.isOnAddressLine(context.graph())) {\n geometry = 'point';\n }\n\n if (!counts[geometry]) counts[geometry] = 0;\n counts[geometry] += 1;\n }\n\n return Object.keys(counts).sort(function(geom1, geom2) {\n return counts[geom2] - counts[geom1];\n });\n }\n\n return utilRebind(presetList, dispatch, 'on');\n}\n", "import { interpolate as d3_interpolate } from 'd3-interpolate';\nimport { select as d3_select } from 'd3-selection';\n\nimport { uiEntityEditor } from './entity_editor';\nimport { uiPresetList } from './preset_list';\nimport { uiViewOnOSM } from './view_on_osm';\n\n\nexport function uiInspector(context) {\n var presetList = uiPresetList(context);\n var entityEditor = uiEntityEditor(context);\n var wrap = d3_select(null),\n presetPane = d3_select(null),\n editorPane = d3_select(null);\n var _state = 'select';\n var _entityIDs;\n var _newFeature = false;\n\n\n function inspector(selection) {\n presetList\n .entityIDs(_entityIDs)\n .autofocus(_newFeature)\n .on('choose', inspector.setPreset)\n .on('cancel', function() {\n inspector.setPreset();\n });\n\n entityEditor\n .state(_state)\n .entityIDs(_entityIDs)\n .on('choose', inspector.showList);\n\n wrap = selection.selectAll('.panewrap')\n .data([0]);\n\n var enter = wrap.enter()\n .append('div')\n .attr('class', 'panewrap');\n\n enter\n .append('div')\n .attr('class', 'preset-list-pane pane');\n\n enter\n .append('div')\n .attr('class', 'entity-editor-pane pane');\n\n wrap = wrap.merge(enter);\n presetPane = wrap.selectAll('.preset-list-pane');\n editorPane = wrap.selectAll('.entity-editor-pane');\n\n function shouldDefaultToPresetList() {\n // always show the inspector on hover\n if (_state !== 'select') return false;\n\n // can only change preset on single selection\n if (_entityIDs.length !== 1) return false;\n\n var entityID = _entityIDs[0];\n var entity = context.hasEntity(entityID);\n if (!entity) return false;\n\n // default to inspector if there are already tags\n if (entity.hasNonGeometryTags()) return false;\n\n // prompt to select preset if feature is new and untagged\n if (_newFeature) return true;\n\n // all existing features except vertices should default to inspector\n if (entity.geometry(context.graph()) !== 'vertex') return false;\n\n // show vertex relations if any\n if (context.graph().parentRelations(entity).length) return false;\n\n // show vertex issues if there are any\n if (context.validator().getEntityIssues(entityID).length) return false;\n\n // show turn restriction editor for junction vertices\n if (entity.type === 'node' && entity.isHighwayIntersection(context.graph())) return false;\n\n // otherwise show preset list for uninteresting vertices\n return true;\n }\n\n if (shouldDefaultToPresetList()) {\n wrap.style('right', '-100%');\n editorPane.classed('hide', true);\n presetPane.classed('hide', false)\n .call(presetList);\n } else {\n wrap.style('right', '0%');\n presetPane.classed('hide', true);\n editorPane.classed('hide', false)\n .call(entityEditor);\n }\n\n var footer = selection.selectAll('.footer')\n .data([0]);\n\n footer = footer.enter()\n .append('div')\n .attr('class', 'footer')\n .merge(footer);\n\n footer\n .call(uiViewOnOSM(context)\n .what(context.hasEntity(_entityIDs.length === 1 && _entityIDs[0]))\n );\n }\n\n inspector.showList = function(presets) {\n\n presetPane.classed('hide', false);\n\n wrap.transition()\n .styleTween('right', function() {\n return d3_interpolate('0%', '-100%');\n })\n .on('end', function () {\n editorPane.classed('hide', true);\n });\n\n if (presets) {\n presetList.presets(presets);\n }\n\n presetPane\n .call(presetList.autofocus(true));\n };\n\n inspector.setPreset = function(preset) {\n\n // upon setting multipolygon, go to the area preset list instead of the editor\n if (preset && preset.id === 'type/multipolygon') {\n presetPane\n .call(presetList.autofocus(true));\n\n } else {\n editorPane.classed('hide', false);\n wrap.transition()\n .styleTween('right', function() {\n return d3_interpolate('-100%', '0%');\n })\n .on('end', function () {\n presetPane.classed('hide', true);\n });\n\n if (preset) {\n entityEditor.presets([preset]);\n }\n editorPane\n .call(entityEditor);\n }\n\n };\n\n inspector.state = function(val) {\n if (!arguments.length) return _state;\n _state = val;\n entityEditor.state(_state);\n\n // remove any old field help overlay that might have gotten attached to the inspector\n context.container().selectAll('.field-help-body').remove();\n\n return inspector;\n };\n\n\n inspector.entityIDs = function(val) {\n if (!arguments.length) return _entityIDs;\n _entityIDs = val;\n return inspector;\n };\n\n\n inspector.newFeature = function(val) {\n if (!arguments.length) return _newFeature;\n _newFeature = val;\n return inspector;\n };\n\n\n return inspector;\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { utilArrayIdentical } from '../util/array';\nimport { utilFastMouse } from '../util';\nimport { osmEntity, osmNote, QAItem } from '../osm';\nimport { services } from '../services';\nimport { uiDataEditor } from './data_editor';\nimport { uiFeatureList } from './feature_list';\nimport { uiInspector } from './inspector';\nimport { uiOsmoseEditor } from './osmose_editor';\nimport { uiNoteEditor } from './note_editor';\nimport { localizer } from '../core/localizer';\n\n\nexport function uiSidebar(context) {\n var inspector = uiInspector(context);\n var dataEditor = uiDataEditor(context);\n var noteEditor = uiNoteEditor(context);\n var osmoseEditor = uiOsmoseEditor(context);\n var _current;\n var _wasData = false;\n var _wasNote = false;\n var _wasQaItem = false;\n\n // use pointer events on supported platforms; fallback to mouse events\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n\n function sidebar(selection) {\n var container = context.container();\n var minWidth = 240;\n var sidebarWidth;\n var containerWidth;\n var dragOffset;\n\n // Set the initial width constraints\n selection\n .style('min-width', minWidth + 'px')\n .style('max-width', '400px')\n .style('width', '33.3333%');\n\n var resizer = selection\n .append('div')\n .attr('class', 'sidebar-resizer')\n .on(_pointerPrefix + 'down.sidebar-resizer', pointerdown);\n\n var downPointerId, lastClientX, containerLocGetter;\n\n function pointerdown(d3_event) {\n if (downPointerId) return;\n\n if ('button' in d3_event && d3_event.button !== 0) return;\n\n downPointerId = d3_event.pointerId || 'mouse';\n\n lastClientX = d3_event.clientX;\n\n containerLocGetter = utilFastMouse(container.node());\n\n // offset from edge of sidebar-resizer\n dragOffset = utilFastMouse(resizer.node())(d3_event)[0] - 1;\n\n sidebarWidth = selection.node().getBoundingClientRect().width;\n containerWidth = container.node().getBoundingClientRect().width;\n var widthPct = (sidebarWidth / containerWidth) * 100;\n selection\n .style('width', widthPct + '%') // lock in current width\n .style('max-width', '85%'); // but allow larger widths\n\n resizer.classed('dragging', true);\n\n d3_select(window)\n .on('touchmove.sidebar-resizer', function(d3_event) {\n // disable page scrolling while resizing on touch input\n d3_event.preventDefault();\n }, { passive: false })\n .on(_pointerPrefix + 'move.sidebar-resizer', pointermove)\n .on(_pointerPrefix + 'up.sidebar-resizer pointercancel.sidebar-resizer', pointerup);\n }\n\n function pointermove(d3_event) {\n\n if (downPointerId !== (d3_event.pointerId || 'mouse')) return;\n\n d3_event.preventDefault();\n\n var dx = d3_event.clientX - lastClientX;\n\n lastClientX = d3_event.clientX;\n\n var isRTL = (localizer.textDirection() === 'rtl');\n var scaleX = isRTL ? 0 : 1;\n var xMarginProperty = isRTL ? 'margin-right' : 'margin-left';\n\n var x = containerLocGetter(d3_event)[0] - dragOffset;\n sidebarWidth = isRTL ? containerWidth - x : x;\n\n var isCollapsed = selection.classed('collapsed');\n var shouldCollapse = sidebarWidth < minWidth;\n\n selection.classed('collapsed', shouldCollapse);\n\n if (shouldCollapse) {\n if (!isCollapsed) {\n selection\n .style(xMarginProperty, '-400px')\n .style('width', '400px');\n\n context.ui().onResize([(sidebarWidth - dx) * scaleX, 0]);\n }\n\n } else {\n var widthPct = (sidebarWidth / containerWidth) * 100;\n selection\n .style(xMarginProperty, null)\n .style('width', widthPct + '%');\n\n if (isCollapsed) {\n context.ui().onResize([-sidebarWidth * scaleX, 0]);\n } else {\n context.ui().onResize([-dx * scaleX, 0]);\n }\n }\n }\n\n function pointerup(d3_event) {\n if (downPointerId !== (d3_event.pointerId || 'mouse')) return;\n\n downPointerId = null;\n\n resizer.classed('dragging', false);\n\n d3_select(window)\n .on('touchmove.sidebar-resizer', null)\n .on(_pointerPrefix + 'move.sidebar-resizer', null)\n .on(_pointerPrefix + 'up.sidebar-resizer pointercancel.sidebar-resizer', null);\n }\n\n var featureListWrap = selection\n .append('div')\n .attr('class', 'feature-list-pane')\n .call(uiFeatureList(context));\n\n var inspectorWrap = selection\n .append('div')\n .attr('class', 'inspector-hidden inspector-wrap');\n\n var hoverModeSelect = function(targets) {\n context.container().selectAll('.feature-list-item button').classed('hover', false);\n\n if (context.selectedIDs().length > 1 &&\n targets && targets.length) {\n\n var elements = context.container().selectAll('.feature-list-item button')\n .filter(function (node) {\n return targets.indexOf(node) !== -1;\n });\n\n if (!elements.empty()) {\n elements.classed('hover', true);\n }\n }\n };\n\n sidebar.hoverModeSelect = throttle(hoverModeSelect, 200);\n\n function hover(targets) {\n var datum = targets && targets.length && targets[0];\n if (datum && datum.__featurehash__) { // hovering on data\n _wasData = true;\n sidebar\n .show(dataEditor.datum(datum));\n\n selection.selectAll('.sidebar-component')\n .classed('inspector-hover', true);\n\n } else if (datum instanceof osmNote) {\n if (context.mode().id === 'drag-note') return;\n _wasNote = true;\n\n var osm = services.osm;\n if (osm) {\n datum = osm.getNote(datum.id); // marker may contain stale data - get latest\n }\n\n sidebar\n .show(noteEditor.note(datum));\n\n selection.selectAll('.sidebar-component')\n .classed('inspector-hover', true);\n\n } else if (datum instanceof QAItem) {\n _wasQaItem = true;\n\n var errService = services[datum.service];\n if (errService) {\n // marker may contain stale data - get latest\n datum = errService.getError(datum.id);\n }\n\n // Currently only one possible service\n var errEditor;\n if (datum.service === 'osmose') {\n errEditor = osmoseEditor;\n }\n\n context.container().selectAll('.qaItem.' + datum.service)\n .classed('hover', function(d) { return d.id === datum.id; });\n\n sidebar\n .show(errEditor.error(datum));\n\n selection.selectAll('.sidebar-component')\n .classed('inspector-hover', true);\n\n } else if (!_current && (datum instanceof osmEntity)) {\n featureListWrap\n .classed('inspector-hidden', true);\n\n inspectorWrap\n .classed('inspector-hidden', false)\n .classed('inspector-hover', true);\n\n if (!inspector.entityIDs() || !utilArrayIdentical(inspector.entityIDs(), [datum.id]) || inspector.state() !== 'hover') {\n inspector\n .state('hover')\n .entityIDs([datum.id])\n .newFeature(false);\n\n inspectorWrap\n .call(inspector);\n }\n\n } else if (!_current) {\n featureListWrap\n .classed('inspector-hidden', false);\n inspectorWrap\n .classed('inspector-hidden', true);\n inspector\n .state('hide');\n\n } else if (_wasData || _wasNote || _wasQaItem) {\n _wasNote = false;\n _wasData = false;\n _wasQaItem = false;\n context.container().selectAll('.note').classed('hover', false);\n context.container().selectAll('.qaItem').classed('hover', false);\n sidebar.hide();\n }\n }\n\n sidebar.hover = throttle(hover, 200);\n\n\n sidebar.intersects = function(extent) {\n var rect = selection.node().getBoundingClientRect();\n return extent.intersects([\n context.projection.invert([0, rect.height]),\n context.projection.invert([rect.width, 0])\n ]);\n };\n\n\n sidebar.select = function(ids, newFeature) {\n sidebar.hide();\n\n if (ids && ids.length) {\n\n var entity = ids.length === 1 && context.entity(ids[0]);\n if (entity && newFeature && selection.classed('collapsed')) {\n // uncollapse the sidebar\n var extent = entity.extent(context.graph());\n sidebar.expand(sidebar.intersects(extent));\n }\n\n featureListWrap\n .classed('inspector-hidden', true);\n\n inspectorWrap\n .classed('inspector-hidden', false)\n .classed('inspector-hover', false);\n\n // reload the UI even if the ids are the same since the entities\n // themselves may have changed\n inspector\n .state('select')\n .entityIDs(ids)\n .newFeature(newFeature);\n\n inspectorWrap\n .call(inspector);\n\n } else {\n inspector\n .state('hide');\n }\n };\n\n\n sidebar.showPresetList = function() {\n inspector.showList();\n };\n\n\n sidebar.show = function(component, element) {\n featureListWrap\n .classed('inspector-hidden', true);\n inspectorWrap\n .classed('inspector-hidden', true);\n\n if (_current) _current.remove();\n _current = selection\n .append('div')\n .attr('class', 'sidebar-component')\n .call(component, element);\n };\n\n\n sidebar.hide = function() {\n featureListWrap\n .classed('inspector-hidden', false);\n inspectorWrap\n .classed('inspector-hidden', true);\n\n if (_current) _current.remove();\n _current = null;\n };\n\n\n sidebar.expand = function(moveMap) {\n if (selection.classed('collapsed')) {\n sidebar.toggle(moveMap);\n }\n };\n\n\n sidebar.collapse = function(moveMap) {\n if (!selection.classed('collapsed')) {\n sidebar.toggle(moveMap);\n }\n };\n\n\n sidebar.toggle = function(moveMap) {\n\n // Don't allow sidebar to toggle when the user is in the walkthrough.\n if (context.inIntro()) return;\n\n var isCollapsed = selection.classed('collapsed');\n var isCollapsing = !isCollapsed;\n var isRTL = (localizer.textDirection() === 'rtl');\n var scaleX = isRTL ? 0 : 1;\n var xMarginProperty = isRTL ? 'margin-right' : 'margin-left';\n\n sidebarWidth = selection.node().getBoundingClientRect().width;\n\n // switch from % to px\n selection.style('width', sidebarWidth + 'px');\n\n var startMargin, endMargin;\n if (isCollapsing) {\n startMargin = 0;\n endMargin = -sidebarWidth;\n } else {\n startMargin = -sidebarWidth;\n endMargin = 0;\n }\n let lastMargin = startMargin;\n\n if (!isCollapsing) {\n // unhide the sidebar's content before it transitions onscreen\n selection.classed('collapsed', isCollapsing);\n }\n\n selection\n .transition()\n .style(xMarginProperty, endMargin + 'px')\n .tween('panner', function() {\n var i = d3_interpolateNumber(startMargin, endMargin);\n return function(t) {\n var dx = lastMargin - Math.round(i(t));\n lastMargin = lastMargin - dx;\n context.ui().onResize(moveMap ? undefined : [dx * scaleX, 0]);\n };\n })\n .on('end', function() {\n if (isCollapsing) {\n // hide the sidebar's content after it transitions offscreen\n selection.classed('collapsed', isCollapsing);\n }\n\n // switch back from px to %\n if (!isCollapsing) {\n var containerWidth = container.node().getBoundingClientRect().width;\n var widthPct = (sidebarWidth / containerWidth) * 100;\n selection\n .style(xMarginProperty, null)\n .style('width', widthPct + '%');\n }\n });\n };\n\n // toggle the sidebar collapse when double-clicking the resizer\n resizer.on('dblclick', function(d3_event) {\n d3_event.preventDefault();\n if (d3_event.sourceEvent) {\n d3_event.sourceEvent.preventDefault();\n }\n sidebar.toggle();\n });\n\n // ensure hover sidebar is closed when zooming out beyond editable zoom\n context.map().on('crossEditableZoom.sidebar', function(within) {\n if (!within && !selection.select('.inspector-hover').empty()) {\n hover([]);\n }\n });\n }\n\n sidebar.showPresetList = function() {};\n sidebar.hover = function() {};\n sidebar.hover.cancel = function() {};\n sidebar.intersects = function() {};\n sidebar.select = function() {};\n sidebar.show = function() {};\n sidebar.hide = function() {};\n sidebar.expand = function() {};\n sidebar.collapse = function() {};\n sidebar.toggle = function() {};\n\n return sidebar;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../core/localizer';\nimport { modeBrowse } from '../modes/browse';\n\n\nexport function uiSourceSwitch(context) {\n var keys;\n\n\n function click(d3_event) {\n d3_event.preventDefault();\n\n var osm = context.connection();\n if (!osm) return;\n\n if (context.inIntro()) return;\n\n if (context.history().hasChanges() &&\n !window.confirm(t('source_switch.lose_changes'))) return;\n\n var isLive = d3_select(this)\n .classed('live');\n\n isLive = !isLive;\n context.enter(modeBrowse(context));\n context.history().clearSaved(); // remove saved history\n context.flush(); // remove stored data\n\n d3_select(this)\n .classed('live', isLive)\n .classed('chip', isLive)\n .text('')\n .call(isLive ? t.append('source_switch.live') : t.append('source_switch.dev'));\n\n osm.switch(isLive ? keys[0] : keys[1]); // switch connection (warning: dispatches 'change' event)\n }\n\n var sourceSwitch = function(selection) {\n selection\n .append('a')\n .attr('href', '#')\n .call(t.append('source_switch.live'))\n .attr('class', 'live chip')\n .on('click', click);\n };\n\n\n sourceSwitch.keys = function(_) {\n if (!arguments.length) return keys;\n keys = _;\n return sourceSwitch;\n };\n\n\n return sourceSwitch;\n}\n", "export function uiSpinner(context) {\n var osm = context.connection();\n\n\n return function(selection) {\n var img = selection\n .append('img')\n .attr('src', context.imagePath('loader-black.gif'))\n .style('opacity', 0);\n\n if (osm) {\n osm\n .on('loading.spinner', function() {\n img.transition()\n .style('opacity', 1);\n })\n .on('loaded.spinner', function() {\n img.transition()\n .style('opacity', 0);\n });\n }\n };\n}\n", "import { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\n\nexport function uiSectionPrivacy(context) {\n let section = uiSection('preferences-third-party', context)\n .label(() => t.append('preferences.privacy.title'))\n .disclosureContent(renderDisclosureContent);\n\n function renderDisclosureContent(selection) {\n // enter\n selection.selectAll('.privacy-options-list')\n .data([0])\n .enter()\n .append('ul')\n .attr('class', 'layer-list privacy-options-list');\n\n let thirdPartyIconsEnter = selection.select('.privacy-options-list')\n .selectAll('.privacy-third-party-icons-item')\n .data([prefs('preferences.privacy.thirdpartyicons') || 'true'])\n .enter()\n .append('li')\n .attr('class', 'privacy-third-party-icons-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('preferences.privacy.third_party_icons.tooltip'))\n .placement('bottom')\n );\n\n thirdPartyIconsEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', (d3_event, d) => {\n d3_event.preventDefault();\n prefs('preferences.privacy.thirdpartyicons', d === 'true' ? 'false' : 'true');\n });\n\n thirdPartyIconsEnter\n .append('span')\n .call(t.append('preferences.privacy.third_party_icons.description'));\n\n // update\n selection.selectAll('.privacy-third-party-icons-item')\n .classed('active', d => d === 'true')\n .select('input')\n .property('checked', d => d === 'true');\n\n // Privacy Policy link\n selection.selectAll('.privacy-link')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'privacy-link')\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', 'https://github.com/openstreetmap/iD/blob/release/PRIVACY.md')\n .append('span')\n .call(t.append('preferences.privacy.privacy_link'));\n\n }\n\n prefs.onChange('preferences.privacy.thirdpartyicons', section.reRender);\n\n return section;\n}\n", "import { prefs } from '../core/preferences';\nimport { fileFetcher } from '../core/file_fetcher';\nimport { t } from '../core/localizer';\nimport { uiIntro } from './intro';\nimport { uiModal } from './modal';\nimport { uiSectionPrivacy } from './sections/privacy';\n\n\nexport function uiSplash(context) {\n return (selection) => {\n // Exception - if there are restorable changes, skip this splash screen.\n // This is because we currently only support one `uiModal` at a time\n // and we need to show them `uiRestore`` instead of this one.\n if (context.history().hasRestorableChanges()) return;\n\n // If user has not seen this version of the privacy policy, show the splash again.\n let updateMessage = '';\n const sawPrivacyVersion = prefs('sawPrivacyVersion');\n let showSplash = !prefs('sawSplash');\n if (sawPrivacyVersion && sawPrivacyVersion !== context.privacyVersion) {\n updateMessage = t('splash.privacy_update');\n showSplash = true;\n }\n\n if (!showSplash) return;\n\n prefs('sawSplash', true);\n prefs('sawPrivacyVersion', context.privacyVersion);\n\n // fetch intro graph data now, while user is looking at the splash screen\n fileFetcher.get('intro_graph');\n\n let modalSelection = uiModal(selection);\n\n modalSelection.select('.modal')\n .attr('class', 'modal-splash modal');\n\n let introModal = modalSelection.select('.content')\n .append('div')\n .attr('class', 'fillL');\n\n introModal\n .append('div')\n .attr('class','modal-section')\n .append('h3')\n .call(t.append('splash.welcome'));\n\n let modalSection = introModal\n .append('div')\n .attr('class','modal-section');\n\n modalSection\n .append('p')\n .call(t.addOrUpdate('splash.text', {\n version: context.version,\n website: selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/openstreetmap/iD/blob/develop/CHANGELOG.md#whats-new')\n .call(t.addOrUpdate('splash.changelog')),\n github: selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/openstreetmap/iD/issues')\n .text('github.com')\n }));\n\n modalSection\n .append('p')\n .call(t.addOrUpdate('splash.privacy', {\n updateMessage: updateMessage,\n privacyLink: selection => selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/openstreetmap/iD/blob/release/PRIVACY.md')\n .call(t.addOrUpdate('splash.privacy_policy'))\n }));\n\n uiSectionPrivacy(context)\n .label(() => t.append('splash.privacy_settings'))\n .render(modalSection);\n\n let buttonWrap = introModal\n .append('div')\n .attr('class', 'modal-actions');\n\n let walkthrough = buttonWrap\n .append('button')\n .attr('class', 'walkthrough')\n .on('click', () => {\n context.container().call(uiIntro(context));\n modalSelection.close();\n });\n\n walkthrough\n .append('svg')\n .attr('class', 'logo logo-walkthrough')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n walkthrough\n .append('div')\n .call(t.append('splash.walkthrough'));\n\n let startEditing = buttonWrap\n .append('button')\n .attr('class', 'start-editing')\n .on('click', modalSelection.close);\n\n startEditing\n .append('svg')\n .attr('class', 'logo logo-features')\n .append('use')\n .attr('xlink:href', '#iD-logo-features');\n\n startEditing\n .append('div')\n .call(t.append('splash.start'));\n\n modalSelection.select('button.close')\n .attr('class','hide');\n };\n}\n", "import { throttle } from 'es-toolkit/compat';\n\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\n\n\nexport function uiStatus(context) {\n var osm = context.connection();\n\n\n return function(selection) {\n if (!osm) return;\n\n function update(err, apiStatus) {\n selection.html('');\n\n if (err) {\n if (apiStatus === 'connectionSwitched') {\n // if the connection was just switched, we can't rely on\n // the status (we're getting the status of the previous api)\n return;\n\n } else if (apiStatus === 'rateLimited') {\n if (!osm.authenticated()) {\n selection\n .call(t.append('osm_api_status.message.rateLimit'))\n .append('a')\n .attr('href', '#')\n .attr('class', 'api-status-login')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .append('span')\n .call(t.append('login'))\n .on('click.login', function(d3_event) {\n d3_event.preventDefault();\n osm.authenticate();\n });\n } else {\n selection.call(t.append('osm_api_status.message.rateLimited'));\n }\n } else {\n\n // don't allow retrying too rapidly\n var throttledRetry = throttle(function() {\n // try loading the visible tiles\n context.loadTiles(context.projection);\n // manually reload the status too in case all visible tiles were already loaded\n osm.reloadApiStatus();\n }, 2000);\n\n // eslint-disable-next-line no-warning-comments\n // TODO: nice messages for different error types\n selection\n .call(t.append('osm_api_status.message.error', { suffix: ' ' }))\n .append('a')\n .attr('href', '#')\n // let the user manually retry their connection directly\n .call(t.append('osm_api_status.retry'))\n .on('click.retry', function(d3_event) {\n d3_event.preventDefault();\n throttledRetry();\n });\n }\n\n } else if (apiStatus === 'readonly') {\n selection.call(t.append('osm_api_status.message.readonly'));\n } else if (apiStatus === 'offline') {\n selection.call(t.append('osm_api_status.message.offline'));\n }\n\n selection.attr('class', 'api-status ' + (err ? 'error' : apiStatus));\n }\n\n osm.on('apiStatusChange.uiStatus', update);\n\n context.history().on('storage_error', () => {\n selection.selectAll('span.local-storage-full').remove();\n selection\n .append('span')\n .attr('class', 'local-storage-full')\n .call(t.append('osm_api_status.message.local_storage_full'));\n selection.classed('error', true);\n });\n\n // reload the status periodically regardless of other factors\n window.setInterval(function() {\n osm.reloadApiStatus();\n }, 90000);\n\n // load the initial status in case no OSM data was loaded yet\n osm.reloadApiStatus();\n };\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport {\n modeAddArea,\n modeAddLine,\n modeAddPoint,\n modeBrowse\n} from '../../modes';\n\nimport { presetManager } from '../../presets';\nimport { t } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiTooltip } from '../tooltip';\n\nexport function uiToolDrawModes(context) {\n\n var tool = {\n id: 'old_modes',\n label: t.append('toolbar.add_feature')\n };\n\n var modes = [\n modeAddPoint(context, {\n title: t.append('modes.add_point.title'),\n button: 'point',\n description: t.append('modes.add_point.description'),\n preset: presetManager.item('point'),\n key: '1'\n }),\n modeAddLine(context, {\n title: t.append('modes.add_line.title'),\n button: 'line',\n description: t.append('modes.add_line.description'),\n preset: presetManager.item('line'),\n key: '2'\n }),\n modeAddArea(context, {\n title: t.append('modes.add_area.title'),\n button: 'area',\n description: t.append('modes.add_area.description'),\n preset: presetManager.item('area'),\n key: '3'\n })\n ];\n\n\n function enabled(\n // eslint-disable-next-line no-unused-vars\n _mode // parameter is currently not used, but might be at some point\n ) {\n return osmEditable();\n }\n\n function osmEditable() {\n return context.editable();\n }\n\n modes.forEach(function(mode) {\n context.keybinding().on(mode.key, function() {\n if (!enabled(mode)) return;\n\n if (mode.id === context.mode().id) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(mode);\n }\n });\n });\n\n tool.render = function(selection) {\n\n var wrap = selection\n .append('div')\n .attr('class', 'joined')\n .style('display', 'flex');\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n\n context.map()\n .on('move.modes', debouncedUpdate)\n .on('drawn.modes', debouncedUpdate);\n\n context\n .on('enter.modes', update);\n\n update();\n\n\n function update() {\n\n var buttons = wrap.selectAll('button.add-button')\n .data(modes, function(d) { return d.id; });\n\n // exit\n buttons.exit()\n .remove();\n\n // enter\n var buttonsEnter = buttons.enter()\n .append('button')\n .attr('class', function(d) { return d.id + ' add-button bar-button'; })\n .on('click.mode-buttons', function(d3_event, d) {\n if (!enabled(d)) return;\n\n // When drawing, ignore accidental clicks on mode buttons - #4042\n var currMode = context.mode().id;\n if (/^draw/.test(currMode)) return;\n\n if (d.id === currMode) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(d);\n }\n })\n .call(uiTooltip()\n .placement('bottom')\n .title(function(d) { return d.description; })\n .keys(function(d) { return [d.key]; })\n .scrollContainer(context.container().select('.top-toolbar'))\n );\n\n buttonsEnter\n .each(function(d) {\n d3_select(this)\n .call(svgIcon('#iD-icon-' + d.button));\n });\n\n buttonsEnter\n .append('span')\n .attr('class', 'label')\n .text('')\n .each(function(mode) { mode.title(d3_select(this)); });\n\n // if we are adding/removing the buttons, check if toolbar has overflowed\n if (buttons.enter().size() || buttons.exit().size()) {\n context.ui().checkOverflow('.top-toolbar', true);\n }\n\n // update\n buttons\n .merge(buttonsEnter)\n .attr('aria-disabled', function(d) { return !enabled(d); })\n .classed('disabled', function(d) { return !enabled(d); })\n .attr('aria-pressed', function(d) { return context.mode() && context.mode().button === d.button; })\n .classed('active', function(d) { return context.mode() && context.mode().button === d.button; });\n }\n };\n\n return tool;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { select as d3_select } from 'd3-selection';\n\nimport {\n modeAddNote,\n modeBrowse\n} from '../../modes';\n\nimport { t } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiTooltip } from '../tooltip';\n\nexport function uiToolNotes(context) {\n\n var tool = {\n id: 'notes',\n label: t.append('modes.add_note.label')\n };\n\n var mode = modeAddNote(context);\n\n function enabled() {\n return notesEnabled() && notesEditable();\n }\n\n function notesEnabled() {\n var noteLayer = context.layers().layer('notes');\n return noteLayer && noteLayer.enabled();\n }\n\n function notesEditable() {\n var mode = context.mode();\n return context.map().notesEditable() && mode && mode.id !== 'save';\n }\n\n context.keybinding().on(mode.key, function() {\n if (!enabled()) return;\n\n if (mode.id === context.mode().id) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(mode);\n }\n });\n\n tool.render = function(selection) {\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n\n context.map()\n .on('move.notes', debouncedUpdate)\n .on('drawn.notes', debouncedUpdate);\n\n context\n .on('enter.notes', update);\n\n update();\n\n\n function update() {\n var showNotes = notesEnabled();\n var data = showNotes ? [mode] : [];\n\n var buttons = selection.selectAll('button.add-button')\n .data(data, function(d) { return d.id; });\n\n // exit\n buttons.exit()\n .remove();\n\n // enter\n var buttonsEnter = buttons.enter()\n .append('button')\n .attr('class', function(d) { return d.id + ' add-button bar-button'; })\n .on('click.notes', function(d3_event, d) {\n if (!enabled()) return;\n\n // When drawing, ignore accidental clicks on mode buttons - #4042\n var currMode = context.mode().id;\n if (/^draw/.test(currMode)) return;\n\n if (d.id === currMode) {\n context.enter(modeBrowse(context));\n } else {\n context.enter(d);\n }\n })\n .call(uiTooltip()\n .placement('bottom')\n .title(function(d) { return d.description; })\n .keys(function(d) { return [d.key]; })\n .scrollContainer(context.container().select('.top-toolbar'))\n );\n\n buttonsEnter\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(d.icon || '#iD-icon-' + d.button));\n });\n\n // if we are adding/removing the buttons, check if toolbar has overflowed\n if (buttons.enter().size() || buttons.exit().size()) {\n context.ui().checkOverflow('.top-toolbar', true);\n }\n\n // update\n buttons\n .merge(buttonsEnter)\n .classed('disabled', function() { return !enabled(); })\n .attr('aria-disabled', function() { return !enabled(); })\n .classed('active', function(d) { return context.mode() && context.mode().button === d.button; })\n .attr('aria-pressed', function(d) { return context.mode() && context.mode().button === d.button; });\n }\n };\n\n tool.uninstall = function() {\n context\n .on('enter.editor.notes', null)\n .on('exit.editor.notes', null)\n .on('enter.notes', null);\n\n context.map()\n .on('move.notes', null)\n .on('drawn.notes', null);\n };\n\n return tool;\n}\n", "import { interpolateRgb as d3_interpolateRgb } from 'd3-interpolate';\n\nimport { t } from '../../core/localizer';\nimport { modeSave } from '../../modes';\nimport { svgIcon } from '../../svg';\nimport { uiCmd } from '../cmd';\nimport { uiTooltip } from '../tooltip';\n\n\nexport function uiToolSave(context) {\n\n var tool = {\n id: 'save',\n label: t.append('save.title')\n };\n\n var button = null;\n var tooltipBehavior = null;\n var history = context.history();\n var key = uiCmd('\u2318S');\n var _numChanges = 0;\n\n function isSaving() {\n var mode = context.mode();\n return mode && mode.id === 'save';\n }\n\n function isDisabled() {\n return _numChanges === 0 || isSaving();\n }\n\n function save(d3_event) {\n d3_event.preventDefault();\n if (!context.inIntro() && !isSaving() && history.hasChanges()) {\n context.enter(modeSave(context));\n }\n }\n\n function bgColor(numChanges) {\n var step;\n if (numChanges === 0) {\n return null;\n } else if (numChanges <= 50) {\n step = numChanges / 50;\n return d3_interpolateRgb('#fff0', '#ff08')(step); // transparent -> yellow\n } else {\n step = Math.min((numChanges - 50) / 50, 1.0);\n return d3_interpolateRgb('#ff08', '#f008')(step); // yellow -> red\n }\n }\n\n function updateCount() {\n var val = history.difference().summary().length;\n if (val === _numChanges) return;\n\n _numChanges = val;\n\n if (tooltipBehavior) {\n tooltipBehavior\n .title(() => t.append(_numChanges > 0 ? 'save.help' : 'save.no_changes'))\n .keys([key]);\n }\n\n if (button) {\n button\n .classed('disabled', isDisabled())\n .style('--accent-color', bgColor(_numChanges));\n\n button.select('span.count')\n .text(_numChanges);\n }\n }\n\n\n tool.render = function(selection) {\n tooltipBehavior = uiTooltip()\n .placement('bottom')\n .title(() => t.append('save.no_changes'))\n .keys([key])\n .scrollContainer(context.container().select('.top-toolbar'));\n\n var lastPointerUpType;\n\n button = selection\n .append('button')\n .attr('class', 'save disabled bar-button')\n .on('pointerup', function(d3_event) {\n lastPointerUpType = d3_event.pointerType;\n })\n .on('click', function(d3_event) {\n save(d3_event);\n\n if (_numChanges === 0 && (\n lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen')\n ) {\n // there are no tooltips for touch interactions so flash feedback instead\n context.ui().flash\n .duration(2000)\n .iconName('#iD-icon-save')\n .iconClass('disabled')\n .label(t.append('save.no_changes'))();\n }\n lastPointerUpType = null;\n })\n .call(tooltipBehavior);\n\n button\n .call(svgIcon('#iD-icon-save'));\n\n button\n .append('span')\n .attr('class', 'count')\n .attr('aria-hidden', 'true')\n .text('0');\n\n updateCount();\n\n\n context.keybinding()\n .on(key, save, true);\n\n\n context.history()\n .on('change.save', updateCount);\n\n context\n .on('enter.save', function() {\n if (button) {\n button\n .classed('disabled', isDisabled());\n\n if (isSaving()) {\n button.call(tooltipBehavior.hide);\n }\n }\n });\n };\n\n\n tool.uninstall = function() {\n context.keybinding()\n .off(key, true);\n\n context.history()\n .on('change.save', null);\n\n context\n .on('enter.save', null);\n\n button = null;\n tooltipBehavior = null;\n };\n\n return tool;\n}\n", "import { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiTooltip } from '../tooltip';\n\nexport function uiToolSidebarToggle(context) {\n\n var tool = {\n id: 'sidebar_toggle',\n label: t.append('toolbar.inspect')\n };\n\n tool.render = function(selection) {\n selection\n .append('button')\n .attr('class', 'bar-button')\n .attr('aria-label', t('sidebar.tooltip'))\n .on('click', function() {\n context.ui().sidebar.toggle();\n })\n .call(uiTooltip()\n .placement('bottom')\n .title(() => t.append('sidebar.tooltip'))\n .keys([t('sidebar.key')])\n .scrollContainer(context.container().select('.top-toolbar'))\n )\n .call(svgIcon('#iD-icon-sidebar-' + (localizer.textDirection() === 'rtl' ? 'right' : 'left')));\n };\n\n return tool;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg';\nimport { uiCmd } from '../cmd';\nimport { uiTooltip } from '../tooltip';\n\n\nexport function uiToolUndoRedo(context) {\n\n var tool = {\n id: 'undo_redo',\n label: t.append('toolbar.undo_redo')\n };\n\n var commands = [{\n id: 'undo',\n cmd: uiCmd('\u2318Z'),\n action: function() {\n context.undo();\n },\n annotation: function() {\n return context.history().undoAnnotation();\n },\n icon: 'iD-icon-' + (localizer.textDirection() === 'rtl' ? 'redo' : 'undo')\n }, {\n id: 'redo',\n cmd: uiCmd('\u2318\u21E7Z'),\n action: function() {\n context.redo();\n },\n annotation: function() {\n return context.history().redoAnnotation();\n },\n icon: 'iD-icon-' + (localizer.textDirection() === 'rtl' ? 'undo' : 'redo')\n }];\n\n\n function editable() {\n return context.mode() && context.mode().id !== 'save' && context.map().editableDataEnabled(true /* ignore min zoom */);\n }\n\n\n tool.render = function(selection) {\n var tooltipBehavior = uiTooltip()\n .placement('bottom')\n .title(function (d) {\n return d.annotation() ?\n t.append(d.id + '.tooltip', { action: d.annotation() }) :\n t.append(d.id + '.nothing');\n })\n .keys(function(d) {\n return [d.cmd];\n })\n .scrollContainer(context.container().select('.top-toolbar'));\n\n var lastPointerUpType;\n\n var buttons = selection.selectAll('button')\n .data(commands)\n .enter()\n .append('button')\n .attr('class', function(d) { return 'disabled ' + d.id + '-button bar-button'; })\n .on('pointerup', function(d3_event) {\n // `pointerup` is always called before `click`\n lastPointerUpType = d3_event.pointerType;\n })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n\n var annotation = d.annotation();\n\n if (editable() && annotation) {\n d.action();\n }\n\n if (editable() && (\n lastPointerUpType === 'touch' ||\n lastPointerUpType === 'pen')\n ) {\n // there are no tooltips for touch interactions so flash feedback instead\n\n var label = annotation ?\n t.append(d.id + '.tooltip', { action: annotation }) :\n t.append(d.id + '.nothing');\n context.ui().flash\n .duration(2000)\n .iconName('#' + d.icon)\n .iconClass(annotation ? '' : 'disabled')\n .label(label)();\n }\n lastPointerUpType = null;\n })\n .call(tooltipBehavior);\n\n buttons.each(function(d) {\n d3_select(this)\n .call(svgIcon('#' + d.icon));\n });\n\n context.keybinding()\n .on(commands[0].cmd, function(d3_event) {\n d3_event.preventDefault();\n if (editable()) commands[0].action();\n })\n .on(commands[1].cmd, function(d3_event) {\n d3_event.preventDefault();\n if (editable()) commands[1].action();\n });\n\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n\n context.map()\n .on('move.undo_redo', debouncedUpdate)\n .on('drawn.undo_redo', debouncedUpdate);\n\n context.history()\n .on('change.undo_redo', function(difference) {\n if (difference) update();\n });\n\n context\n .on('enter.undo_redo', update);\n\n\n function update() {\n buttons\n .classed('disabled', function(d) {\n return !editable() || !d.annotation();\n })\n .each(function() {\n var selection = d3_select(this);\n if (!selection.select('.tooltip.in').empty()) {\n selection.call(tooltipBehavior.updateContent);\n }\n });\n }\n };\n\n tool.uninstall = function() {\n context.keybinding()\n .off(commands[0].cmd)\n .off(commands[1].cmd);\n\n context.map()\n .on('move.undo_redo', null)\n .on('drawn.undo_redo', null);\n\n context.history()\n .on('change.undo_redo', null);\n\n context\n .on('enter.undo_redo', null);\n };\n\n return tool;\n}\n", "export * from './modes';\nexport * from './notes';\nexport * from './save';\nexport * from './sidebar_toggle';\nexport * from './undo_redo';\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { debounce } from 'es-toolkit/compat';\nimport { uiToolDrawModes, uiToolNotes, uiToolSave, uiToolSidebarToggle, uiToolUndoRedo } from './tools';\n\n\nexport function uiTopToolbar(context) {\n\n var sidebarToggle = uiToolSidebarToggle(context),\n modes = uiToolDrawModes(context),\n notes = uiToolNotes(context),\n undoRedo = uiToolUndoRedo(context),\n save = uiToolSave(context);\n\n function notesEnabled() {\n var noteLayer = context.layers().layer('notes');\n return noteLayer && noteLayer.enabled();\n }\n\n function topToolbar(bar) {\n\n bar.on('wheel.topToolbar', function(d3_event) {\n if (!d3_event.deltaX) {\n // translate vertical scrolling into horizontal scrolling in case\n // the user doesn't have an input device that can scroll horizontally\n bar.node().scrollLeft += d3_event.deltaY;\n }\n });\n\n var debouncedUpdate = debounce(update, 500, { leading: true, trailing: true });\n context.layers()\n .on('change.topToolbar', debouncedUpdate);\n\n update();\n\n function update() {\n\n var tools = [\n sidebarToggle,\n 'spacer',\n modes\n ];\n\n tools.push('spacer');\n\n if (notesEnabled()) {\n tools = tools.concat([notes, 'spacer']);\n }\n\n tools = tools.concat([undoRedo, save]);\n\n var toolbarItems = bar.selectAll('.toolbar-item')\n .data(tools, function(d) {\n return d.id || d;\n });\n\n toolbarItems.exit()\n .each(function(d) {\n if (d.uninstall) {\n d.uninstall();\n }\n })\n .remove();\n\n var itemsEnter = toolbarItems\n .enter()\n .append('div')\n .attr('class', function(d) {\n var classes = 'toolbar-item ' + (d.id || d).replaceAll('_', '-');\n if (d.klass) classes += ' ' + d.klass;\n return classes;\n });\n\n var actionableItems = itemsEnter.filter(function(d) { return d !== 'spacer'; });\n\n actionableItems\n .append('div')\n .attr('class', 'item-content')\n .each(function(d) {\n d3_select(this).call(d.render, bar);\n });\n\n actionableItems\n .append('div')\n .attr('class', 'item-label')\n .each(function(d) { d.label(d3_select(this)); });\n }\n\n }\n\n return topToolbar;\n}\n", "import { prefs } from '../core/preferences';\nimport { t } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiTooltip } from './tooltip';\n\n\n// these are module variables so they are preserved through a ui.restart()\nvar sawVersion = null;\nvar isNewVersion = false;\nvar isNewUser = false;\n\n\nexport function uiVersion(context) {\n\n var currVersion = context.version;\n var matchedVersion = currVersion.match(/\\d+\\.\\d+\\.\\d+.*/);\n\n if (sawVersion === null && matchedVersion !== null) {\n if (prefs('sawVersion')) {\n isNewUser = false;\n isNewVersion = prefs('sawVersion') !== currVersion && currVersion.indexOf('-') === -1;\n } else {\n isNewUser = true;\n isNewVersion = true;\n }\n prefs('sawVersion', currVersion);\n sawVersion = currVersion;\n }\n\n return function(selection) {\n selection\n .append('a')\n .attr('target', '_blank')\n .attr('href', 'https://github.com/OpenHistoricalMap/iD')\n .text(currVersion);\n\n // only show new version indicator to users that have used iD before\n if (isNewVersion && !isNewUser) {\n selection\n .append('a')\n .attr('class', 'badge')\n .attr('target', '_blank')\n .attr('href', `https://github.com/OpenHistoricalMap/iD/releases/tag/v${currVersion}`)\n .call(svgIcon('#maki-gift'))\n .call(uiTooltip()\n .title(() => t.append('version.whats_new', { version: currVersion }))\n .placement('top')\n .scrollContainer(context.container().select('.main-footer-wrap'))\n );\n }\n };\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t, localizer } from '../core/localizer';\nimport { svgIcon } from '../svg/icon';\nimport { uiCmd } from './cmd';\nimport { uiTooltip } from './tooltip';\nimport { utilKeybinding } from '../util/keybinding';\n\n\nexport function uiZoom(context) {\n\n var zooms = [{\n id: 'zoom-in',\n icon: 'iD-icon-plus',\n title: t.append('zoom.in'),\n action: zoomIn,\n disabled: function() {\n return !context.map().canZoomIn();\n },\n disabledTitle: t.append('zoom.disabled.in'),\n key: '+'\n }, {\n id: 'zoom-out',\n icon: 'iD-icon-minus',\n title: t.append('zoom.out'),\n action: zoomOut,\n disabled: function() {\n return !context.map().canZoomOut();\n },\n disabledTitle: t.append('zoom.disabled.out'),\n key: '-'\n }];\n\n function zoomIn(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomIn();\n }\n\n function zoomOut(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomOut();\n }\n\n function zoomInFurther(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomInFurther();\n }\n\n function zoomOutFurther(d3_event) {\n if (d3_event.shiftKey) return;\n d3_event.preventDefault();\n context.map().zoomOutFurther();\n }\n\n return function(selection) {\n var tooltipBehavior = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(function(d) {\n if (d.disabled()) {\n return d.disabledTitle;\n }\n return d.title;\n })\n .keys(function(d) {\n return [d.key];\n });\n\n var lastPointerUpType;\n\n var buttons = selection.selectAll('button')\n .data(zooms)\n .enter()\n .append('button')\n .attr('class', function(d) { return d.id; })\n .on('pointerup.editor', function(d3_event) {\n lastPointerUpType = d3_event.pointerType;\n })\n .on('click.editor', function(d3_event, d) {\n if (!d.disabled()) {\n d.action(d3_event);\n } else if (lastPointerUpType === 'touch' || lastPointerUpType === 'pen') {\n context.ui().flash\n .duration(2000)\n .iconName('#' + d.icon)\n .iconClass('disabled')\n .label(d.disabledTitle)();\n }\n lastPointerUpType = null;\n })\n .call(tooltipBehavior);\n\n buttons.each(function(d) {\n d3_select(this)\n .call(svgIcon('#' + d.icon, 'light'));\n });\n\n utilKeybinding.plusKeys.forEach(function(key) {\n context.keybinding().on([key], zoomIn);\n context.keybinding().on([uiCmd('\u2325' + key)], zoomInFurther);\n });\n\n utilKeybinding.minusKeys.forEach(function(key) {\n context.keybinding().on([key], zoomOut);\n context.keybinding().on([uiCmd('\u2325' + key)], zoomOutFurther);\n });\n\n function updateButtonStates() {\n buttons\n .classed('disabled', function(d) {\n return d.disabled();\n })\n .each(function() {\n var selection = d3_select(this);\n if (!selection.select('.tooltip.in').empty()) {\n selection.call(tooltipBehavior.updateContent);\n }\n });\n }\n\n updateButtonStates();\n\n context.map().on('move.uiZoom', updateButtonStates);\n };\n}\n", "import { t, localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\nimport { svgIcon } from '../svg/icon';\n\nexport function uiZoomToSelection(context) {\n\n function isDisabled() {\n var mode = context.mode();\n return !mode || !mode.zoomToSelected;\n }\n\n var _lastPointerUpType;\n\n function pointerup(d3_event) {\n _lastPointerUpType = d3_event.pointerType;\n }\n\n function click(d3_event) {\n d3_event.preventDefault();\n\n if (isDisabled()) {\n if (_lastPointerUpType === 'touch' || _lastPointerUpType === 'pen') {\n context.ui().flash\n .duration(2000)\n .iconName('#iD-icon-framed-dot')\n .iconClass('disabled')\n .label(t.append('inspector.zoom_to.no_selection'))();\n }\n } else {\n var mode = context.mode();\n if (mode && mode.zoomToSelected) {\n mode.zoomToSelected();\n }\n }\n\n _lastPointerUpType = null;\n }\n\n return function(selection) {\n\n var tooltipBehavior = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(function() {\n if (isDisabled()) {\n return t.append('inspector.zoom_to.no_selection');\n }\n return t.append('inspector.zoom_to.title');\n })\n .keys([t('inspector.zoom_to.key')]);\n\n var button = selection\n .append('button')\n .on('pointerup', pointerup)\n .on('click', click)\n .call(svgIcon('#iD-icon-framed-dot', 'light'))\n .call(tooltipBehavior);\n\n function setEnabledState() {\n button.classed('disabled', isDisabled());\n if (!button.select('.tooltip.in').empty()) {\n button.call(tooltipBehavior.updateContent);\n }\n }\n\n context.on('enter.uiZoomToSelection', setEnabledState);\n\n setEnabledState();\n };\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { svgIcon } from '../svg/icon';\nimport { t, localizer } from '../core/localizer';\nimport { uiTooltip } from './tooltip';\n\n\nexport function uiPane(id, context) {\n\n var _key;\n var _label = '';\n var _description = '';\n var _iconName = '';\n var _sections; // array of uiSection objects\n\n var _paneSelection = d3_select(null);\n\n var _paneTooltip;\n\n var pane = {\n id: id\n };\n\n pane.label = function(val) {\n if (!arguments.length) return _label;\n _label = val;\n return pane;\n };\n\n pane.key = function(val) {\n if (!arguments.length) return _key;\n _key = val;\n return pane;\n };\n\n pane.description = function(val) {\n if (!arguments.length) return _description;\n _description = val;\n return pane;\n };\n\n pane.iconName = function(val) {\n if (!arguments.length) return _iconName;\n _iconName = val;\n return pane;\n };\n\n pane.sections = function(val) {\n if (!arguments.length) return _sections;\n _sections = val;\n return pane;\n };\n\n pane.selection = function() {\n return _paneSelection;\n };\n\n function hidePane() {\n context.ui().togglePanes();\n }\n\n pane.togglePane = function(d3_event) {\n if (d3_event) d3_event.preventDefault();\n _paneTooltip.hide();\n context.ui().togglePanes(!_paneSelection.classed('shown') ? _paneSelection : undefined);\n };\n\n pane.renderToggleButton = function(selection) {\n\n if (!_paneTooltip) {\n _paneTooltip = uiTooltip()\n .scrollContainer(context.container().select('.over-map'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n .title(() => _description)\n .keys([_key]);\n }\n\n selection\n .append('button')\n .on('click', pane.togglePane)\n .call(svgIcon('#' + _iconName, 'light'))\n .call(_paneTooltip);\n };\n\n pane.renderContent = function(selection) {\n // override to fully customize content\n\n if (_sections) {\n _sections.forEach(function(section) {\n selection.call(section.render);\n });\n }\n };\n\n pane.renderPane = function(selection) {\n\n _paneSelection = selection\n .append('div')\n .attr('class', 'fillL map-pane hide ' + id + '-pane')\n .attr('pane', id);\n\n var heading = _paneSelection\n .append('div')\n .attr('class', 'pane-heading');\n\n heading\n .append('h2')\n .text('')\n .call(_label);\n\n heading\n .append('button')\n .attr('title', t('icons.close'))\n .on('click', hidePane)\n .call(svgIcon('#iD-icon-close'));\n\n\n _paneSelection\n .append('div')\n .attr('class', 'pane-content')\n .call(pane.renderContent);\n\n if (_key) {\n context.keybinding()\n .on(_key, pane.togglePane);\n }\n };\n\n return pane;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\nimport { clamp } from 'es-toolkit/compat';\n\nimport { prefs } from '../../core/preferences';\nimport { t, localizer } from '../../core/localizer';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\n\n\nexport function uiSectionBackgroundDisplayOptions(context) {\n\n var section = uiSection('background-display-options', context)\n .label(() => t.append('background.display_options'))\n .disclosureContent(renderDisclosureContent);\n\n var _storedOpacity = prefs('background-opacity');\n var _minVal = 0;\n var _maxVal = 3;\n\n var _sliders = ['brightness', 'contrast', 'saturation', 'sharpness'];\n\n var _options = {\n brightness: (_storedOpacity !== null ? (+_storedOpacity) : 1),\n contrast: 1,\n saturation: 1,\n sharpness: 1\n };\n\n function updateValue(d, val) {\n val = clamp(val, _minVal, _maxVal);\n\n _options[d] = val;\n context.background()[d](val);\n\n if (d === 'brightness') {\n prefs('background-opacity', val);\n }\n\n section.reRender();\n }\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.display-options-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'display-options-container controls-list');\n\n // add slider controls\n var slidersEnter = containerEnter.selectAll('.display-control')\n .data(_sliders)\n .enter()\n .append('label')\n .attr('class', function(d) { return 'display-control display-control-' + d; });\n\n slidersEnter\n .each(function(d) {\n d3_select(this).call(t.append('background.' + d));\n })\n .append('span')\n .attr('class', function(d) { return 'display-option-value display-option-value-' + d; });\n\n var sildersControlEnter = slidersEnter\n .append('div')\n .attr('class', 'control-wrap');\n\n sildersControlEnter\n .append('input')\n .attr('class', function(d) { return 'display-option-input display-option-input-' + d; })\n .attr('type', 'range')\n .attr('min', _minVal)\n .attr('max', _maxVal)\n .attr('step', '0.01')\n .on('input', function(d3_event, d) {\n var val = d3_select(this).property('value');\n if (!val && d3_event && d3_event.target) {\n val = d3_event.target.value;\n }\n updateValue(d, val);\n });\n\n sildersControlEnter\n .append('button')\n .attr('title', function(d) { return `${t('background.reset')} ${t('background.' + d)}`; })\n .attr('class', function(d) { return 'display-option-reset display-option-reset-' + d; })\n .on('click', function(d3_event, d) {\n if (d3_event.button !== 0) return;\n updateValue(d, 1);\n })\n .call(svgIcon('#iD-icon-' + (localizer.textDirection() === 'rtl' ? 'redo' : 'undo')));\n\n // reset all button\n containerEnter\n .append('a')\n .attr('class', 'display-option-resetlink')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('background.reset_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n for (var i = 0; i < _sliders.length; i++) {\n updateValue(_sliders[i], 1);\n }\n });\n\n // update\n container = containerEnter\n .merge(container);\n\n container.selectAll('.display-option-input')\n .property('value', function(d) { return _options[d]; });\n\n container.selectAll('.display-option-value')\n .text(function(d) { return Math.floor(_options[d] * 100) + '%'; });\n\n container.selectAll('.display-option-reset')\n .classed('disabled', function(d) { return _options[d] === 1; });\n\n // first time only, set brightness if needed\n if (containerEnter.size() && _options.brightness !== 1) {\n context.background().brightness(_options.brightness);\n }\n }\n\n return section;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { marked } from 'marked';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiConfirm } from '../confirm';\nimport { utilNoAuto, utilRebind } from '../../util';\n\n\nexport function uiSettingsCustomBackground() {\n var dispatch = d3_dispatch('change');\n\n function render(selection) {\n // keep separate copies of original and current settings\n var _origSettings = {\n template: prefs('background-custom-template')\n };\n var _currSettings = {\n template: prefs('background-custom-template')\n };\n\n var example = 'https://tile.openstreetmap.org/{zoom}/{x}/{y}.png';\n var modal = uiConfirm(selection).okButton();\n\n modal\n .classed('settings-modal settings-custom-background', true);\n\n modal.select('.modal-section.header')\n .append('h3')\n .call(t.append('settings.custom_background.header'));\n\n\n var textSection = modal.select('.modal-section.message-text');\n\n var instructions =\n `${t.html('settings.custom_background.instructions.info')}\\n` +\n '\\n' +\n `#### ${t.html('settings.custom_background.instructions.wms.tokens_label')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.proj')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.wkid')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.dimensions')}\\n` +\n `* ${t.html('settings.custom_background.instructions.wms.tokens.bbox')}\\n` +\n '\\n' +\n `#### ${t.html('settings.custom_background.instructions.tms.tokens_label')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.xyz')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.flipped_y')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.switch')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.quadtile')}\\n` +\n `* ${t.html('settings.custom_background.instructions.tms.tokens.scale_factor')}\\n` +\n '\\n' +\n `#### ${t.html('settings.custom_background.instructions.example')}\\n` +\n `\\`${example}\\``;\n\n textSection\n .append('div')\n .attr('class', 'instructions-template')\n .html(marked(instructions));\n\n textSection\n .append('textarea')\n .attr('class', 'field-template')\n .attr('placeholder', t('settings.custom_background.template.placeholder'))\n .call(utilNoAuto)\n .property('value', _currSettings.template);\n\n\n // insert a cancel button\n var buttonSection = modal.select('.modal-section.buttons');\n\n buttonSection\n .insert('button', '.ok-button')\n .attr('class', 'button cancel-button secondary-action')\n .call(t.append('confirm.cancel'));\n\n\n buttonSection.select('.cancel-button')\n .on('click.cancel', clickCancel);\n\n buttonSection.select('.ok-button')\n .attr('disabled', isSaveDisabled)\n .on('click.save', clickSave);\n\n\n function isSaveDisabled() {\n return null;\n }\n\n\n // restore the original template\n function clickCancel() {\n textSection.select('.field-template').property('value', _origSettings.template);\n prefs('background-custom-template', _origSettings.template);\n this.blur();\n modal.close();\n }\n\n // accept the current template\n function clickSave() {\n _currSettings.template = textSection.select('.field-template').property('value');\n prefs('background-custom-template', _currSettings.template);\n this.blur();\n modal.close();\n dispatch.call('change', this, _currSettings);\n }\n }\n\n return utilRebind(render, dispatch, 'on');\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport { descending as d3_descending, ascending as d3_ascending } from 'd3-array';\nimport { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t, localizer } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg/icon';\nimport { uiCmd } from '../cmd';\nimport { uiSettingsCustomBackground } from '../settings/custom_background';\nimport { uiMapInMap } from '../map_in_map';\nimport { uiSection } from '../section';\n\nexport function uiSectionBackgroundList(context) {\n\n var _backgroundList = d3_select(null);\n\n var _settingsCustomBackground = uiSettingsCustomBackground(context)\n .on('change', customChanged);\n\n var section = uiSection('background-list', context)\n .label(() => t.append('background.backgrounds'))\n .disclosureContent(renderDisclosureContent);\n\n function previousBackgroundID() {\n return prefs('background-last-used-toggle');\n }\n\n function renderDisclosureContent(selection) {\n\n // the background list\n var container = selection.selectAll('.layer-background-list')\n .data([0]);\n\n _backgroundList = container.enter()\n .append('ul')\n .attr('class', 'layer-list layer-background-list')\n .attr('dir', 'auto')\n .merge(container);\n\n\n // add minimap toggle below list\n var bgExtrasListEnter = selection.selectAll('.bg-extras-list')\n .data([0])\n .enter()\n .append('ul')\n .attr('class', 'layer-list bg-extras-list');\n\n var minimapLabelEnter = bgExtrasListEnter\n .append('li')\n .attr('class', 'minimap-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('background.minimap.tooltip'))\n .keys([t('background.minimap.key')])\n .placement('top')\n );\n\n minimapLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n uiMapInMap.toggle();\n });\n\n minimapLabelEnter\n .append('span')\n .call(t.append('background.minimap.description'));\n\n\n var panelLabelEnter = bgExtrasListEnter\n .append('li')\n .attr('class', 'background-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('background.panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.background.key'))])\n .placement('top')\n );\n\n panelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('background');\n });\n\n panelLabelEnter\n .append('span')\n .call(t.append('background.panel.description'));\n\n var locPanelLabelEnter = bgExtrasListEnter\n .append('li')\n .attr('class', 'location-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('background.location_panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.location.key'))])\n .placement('top')\n );\n\n locPanelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('location');\n });\n\n locPanelLabelEnter\n .append('span')\n .call(t.append('background.location_panel.description'));\n\n\n // \"Info / Report a Problem\" link\n selection.selectAll('.imagery-faq')\n .data([0])\n .enter()\n .append('div')\n .attr('class', 'imagery-faq')\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', 'https://github.com/openstreetmap/iD/blob/develop/FAQ.md#how-can-i-report-an-issue-with-background-imagery')\n .append('span')\n .call(t.append('background.imagery_problem_faq'));\n\n _backgroundList\n .call(drawListItems, 'radio', function(d3_event, d) {\n chooseBackground(d);\n }, function(d) {\n return !d.isHidden() && !d.overlay;\n });\n }\n\n function setTooltips(selection) {\n selection.each(function(d, i, nodes) {\n var item = d3_select(this).select('label');\n var span = item.select('span');\n var placement = (i < nodes.length / 2) ? 'bottom' : 'top';\n var hasDescription = d.hasDescription();\n var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth'));\n\n item.call(uiTooltip().destroyAny);\n\n if (d.id === previousBackgroundID()) {\n item.call(uiTooltip()\n .placement(placement)\n .title(() => t.append('background.switch'))\n .keys([uiCmd('\u2318' + t('background.key'))])\n );\n } else if (hasDescription || isOverflowing) {\n item.call(uiTooltip()\n .placement(placement)\n .title(() => hasDescription ? d.description() : d.label())\n );\n }\n });\n }\n\n function drawListItems(layerList, type, change, filter) {\n var sources = context.background()\n .sources(context.map().extent(), context.map().zoom(), true)\n .filter(filter)\n .sort(function(a, b) {\n return a.best() && !b.best() ? -1\n : b.best() && !a.best() ? 1\n : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0;\n });\n\n var layerLinks = layerList.selectAll('li')\n // We have to be a bit inefficient about reordering the list since\n // arrow key navigation of radio values likes to work in the order\n // they were added, not the display document order.\n .data(sources, function(d, i) { return d.id + '---' + i; });\n\n layerLinks.exit()\n .remove();\n\n var enter = layerLinks.enter()\n .append('li')\n .classed('layer-custom', function(d) { return d.id === 'custom'; })\n .classed('best', function(d) { return d.best(); });\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', 'background-layer')\n .attr('value', function(d) {\n return d.id;\n })\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) { d.label()(d3_select(this)); });\n\n enter.filter(function(d) { return d.id === 'custom'; })\n .append('button')\n .attr('class', 'layer-browse')\n .call(uiTooltip()\n .title(() => t.append('settings.custom_background.tooltip'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n editCustom();\n })\n .call(svgIcon('#iD-icon-more'));\n\n enter.filter(function(d) { return d.best(); })\n .append('div')\n .attr('class', 'best')\n .call(uiTooltip()\n .title(() => t.append('background.best_imagery'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .append('span')\n .text('\u2605');\n\n layerList\n .call(updateLayerSelections);\n }\n\n function updateLayerSelections(selection) {\n function active(d) {\n return context.background().showsLayer(d);\n }\n\n selection.selectAll('li')\n .classed('active', active)\n .classed('switch', function(d) { return d.id === previousBackgroundID(); })\n .call(setTooltips)\n .selectAll('input')\n .property('checked', active);\n }\n\n\n function chooseBackground(d) {\n if (d.id === 'custom' && !d.template()) {\n return editCustom();\n }\n\n var previousBackground = context.background().baseLayerSource();\n prefs('background-last-used-toggle', previousBackground.id);\n prefs('background-last-used', d.id);\n context.background().baseLayerSource(d);\n }\n\n\n function customChanged(d) {\n var background = context.background();\n var customSource = background.findSource('custom');\n if (!customSource) return;\n\n if (d && d.template) {\n customSource.template(d.template);\n chooseBackground(customSource);\n } else {\n customSource.template('');\n var noneSource = background.findSource('none');\n if (noneSource) {\n chooseBackground(noneSource);\n }\n }\n }\n\n\n function editCustom() {\n context.container()\n .call(_settingsCustomBackground);\n }\n\n\n context.background()\n .on('change.background_list', function() {\n _backgroundList.call(updateLayerSelections);\n });\n\n context.map()\n .on('move.background_list',\n debounce(function() {\n // layers in-view may have changed due to map move\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import {\n select as d3_select\n} from 'd3-selection';\n\nimport { t, localizer } from '../../core/localizer';\nimport { geoMetersToOffset, geoOffsetToMeters } from '../../geo';\nimport { svgIcon } from '../../svg/icon';\nimport { uiSection } from '../section';\n\n\nexport function uiSectionBackgroundOffset(context) {\n\n var section = uiSection('background-offset', context)\n .label(() => t.append('background.fix_misalignment'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n var _pointerPrefix = 'PointerEvent' in window ? 'pointer' : 'mouse';\n\n var _directions = [\n ['top', [0, -0.5]],\n ['left', [-0.5, 0]],\n ['right', [0.5, 0]],\n ['bottom', [0, 0.5]]\n ];\n\n\n function updateValue() {\n var meters = geoOffsetToMeters(context.background().offset());\n var x = +meters[0].toFixed(2);\n var y = +meters[1].toFixed(2);\n\n context.container().selectAll('.nudge-inner-rect')\n .select('input')\n .classed('error', false)\n .property('value', x + ', ' + y);\n\n context.container().selectAll('.nudge-reset')\n .classed('disabled', function() {\n return (x === 0 && y === 0);\n });\n }\n\n\n function resetOffset() {\n context.background().offset([0, 0]);\n updateValue();\n }\n\n\n function nudge(d) {\n context.background().nudge(d, context.map().zoom());\n updateValue();\n }\n\n\n function inputOffset() {\n var input = d3_select(this);\n var d = input.node().value;\n\n if (d === '') return resetOffset();\n\n d = d.replace(/;/g, ',').split(',').map(function(n) {\n // if n is NaN, it will always get mapped to false.\n return !isNaN(n) && n;\n });\n\n if (d.length !== 2 || !d[0] || !d[1]) {\n input.classed('error', true);\n return;\n }\n\n context.background().offset(geoMetersToOffset(d));\n updateValue();\n }\n\n\n function dragOffset(d3_event) {\n if (d3_event.button !== 0) return;\n\n var origin = [d3_event.clientX, d3_event.clientY];\n\n var pointerId = d3_event.pointerId || 'mouse';\n\n context.container()\n .append('div')\n .attr('class', 'nudge-surface');\n\n d3_select(window)\n .on(_pointerPrefix + 'move.drag-bg-offset', pointermove)\n .on(_pointerPrefix + 'up.drag-bg-offset', pointerup);\n\n if (_pointerPrefix === 'pointer') {\n d3_select(window)\n .on('pointercancel.drag-bg-offset', pointerup);\n }\n\n function pointermove(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n\n var latest = [d3_event.clientX, d3_event.clientY];\n var d = [\n -(origin[0] - latest[0]) / 4,\n -(origin[1] - latest[1]) / 4\n ];\n\n origin = latest;\n nudge(d);\n }\n\n function pointerup(d3_event) {\n if (pointerId !== (d3_event.pointerId || 'mouse')) return;\n if (d3_event.button !== 0) return;\n\n context.container().selectAll('.nudge-surface')\n .remove();\n\n d3_select(window)\n .on('.drag-bg-offset', null);\n }\n }\n\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.nudge-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'nudge-container');\n\n containerEnter\n .append('div')\n .attr('class', 'nudge-instructions')\n .call(t.append('background.offset'));\n\n var nudgeWrapEnter = containerEnter\n .append('div')\n .attr('class', 'nudge-controls-wrap');\n\n var nudgeEnter = nudgeWrapEnter\n .append('div')\n .attr('class', 'nudge-outer-rect')\n .on(_pointerPrefix + 'down', dragOffset);\n\n nudgeEnter\n .append('div')\n .attr('class', 'nudge-inner-rect')\n .append('input')\n .attr('type', 'text')\n .attr('aria-label', t('background.offset_label'))\n .on('change', inputOffset);\n\n nudgeWrapEnter\n .append('div')\n .selectAll('button')\n .data(_directions).enter()\n .append('button')\n .attr('title', function(d) { return t(`background.nudge.${d[0]}`); })\n .attr('class', function(d) { return d[0] + ' nudge'; })\n .on('click', function(d3_event, d) {\n nudge(d[1]);\n });\n\n nudgeWrapEnter\n .append('button')\n .attr('title', t('background.reset'))\n .attr('class', 'nudge-reset disabled')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n resetOffset();\n })\n .call(svgIcon('#iD-icon-' + (localizer.textDirection() === 'rtl' ? 'redo' : 'undo')));\n\n updateValue();\n }\n\n context.background()\n .on('change.backgroundOffset-update', updateValue);\n\n return section;\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport { descending as d3_descending, ascending as d3_ascending } from 'd3-array';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionOverlayList(context) {\n\n var section = uiSection('overlay-list', context)\n .label(() => t.append('background.overlays'))\n .disclosureContent(renderDisclosureContent);\n\n var _overlayList = d3_select(null);\n\n function setTooltips(selection) {\n selection.each(function(d, i, nodes) {\n var item = d3_select(this).select('label');\n var span = item.select('span');\n var placement = (i < nodes.length / 2) ? 'bottom' : 'top';\n var description = d.description();\n var isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth'));\n\n item.call(uiTooltip().destroyAny);\n\n if (description || isOverflowing) {\n item.call(uiTooltip()\n .placement(placement)\n .title(() => description || d.name())\n );\n }\n });\n }\n\n function updateLayerSelections(selection) {\n function active(d) {\n return context.background().showsLayer(d);\n }\n\n selection.selectAll('li')\n .classed('active', active)\n .call(setTooltips)\n .selectAll('input')\n .property('checked', active);\n }\n\n\n function chooseOverlay(d3_event, d) {\n d3_event.preventDefault();\n context.background().toggleOverlayLayer(d);\n _overlayList.call(updateLayerSelections);\n document.activeElement.blur();\n }\n\n function drawListItems(layerList, type, change, filter) {\n var sources = context.background()\n .sources(context.map().extent(), context.map().zoom(), true)\n .filter(filter);\n\n var layerLinks = layerList.selectAll('li')\n .data(sources, function(d) { return d.name(); });\n\n layerLinks.exit()\n .remove();\n\n var enter = layerLinks.enter()\n .append('li');\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', 'layers')\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) { d.label()(d3_select(this)); });\n\n\n layerList.selectAll('li')\n .sort(sortSources);\n\n layerList\n .call(updateLayerSelections);\n\n\n function sortSources(a, b) {\n return a.best() && !b.best() ? -1\n : b.best() && !a.best() ? 1\n : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0;\n }\n }\n\n function renderDisclosureContent(selection) {\n\n var container = selection.selectAll('.layer-overlay-list')\n .data([0]);\n\n _overlayList = container.enter()\n .append('ul')\n .attr('class', 'layer-list layer-overlay-list')\n .attr('dir', 'auto')\n .merge(container);\n\n _overlayList\n .call(drawListItems, 'checkbox', chooseOverlay, function(d) { return !d.isHidden() && d.overlay; });\n }\n\n context.map()\n .on('move.overlay_list',\n debounce(function() {\n // layers in-view may have changed due to map move\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\n\nimport { uiSectionBackgroundDisplayOptions } from '../sections/background_display_options';\nimport { uiSectionBackgroundList } from '../sections/background_list';\nimport { uiSectionBackgroundOffset } from '../sections/background_offset';\nimport { uiSectionOverlayList } from '../sections/overlay_list';\n\nexport function uiPaneBackground(context) {\n\n var backgroundPane = uiPane('background', context)\n .key(t('background.key'))\n .label(t.append('background.title'))\n .description(t.append('background.description'))\n .iconName('iD-icon-layers')\n .sections([\n uiSectionBackgroundList(context),\n uiSectionOverlayList(context),\n uiSectionBackgroundDisplayOptions(context),\n uiSectionBackgroundOffset(context)\n ]);\n\n return backgroundPane;\n}\n", "import { select as d3_select } from 'd3-selection';\nimport { marked } from 'marked';\n\nimport { svgIcon } from '../../svg/icon';\nimport { uiIntro } from '../intro/intro';\nimport { uiPane } from '../pane';\n\nimport { t, localizer } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { helpHtml } from '../intro/helper';\n\nexport function uiPaneHelp(context) {\n\n var docKeys = [\n ['help', [\n 'welcome',\n 'open_data_h',\n 'open_data',\n 'before_start_h',\n 'before_start',\n 'open_source_h',\n 'open_source',\n 'open_source_attribution',\n 'open_source_help'\n ]],\n ['overview', [\n 'navigation_h',\n 'navigation_drag',\n 'navigation_zoom',\n 'features_h',\n 'features',\n 'nodes_ways'\n ]],\n ['editing', [\n 'select_h',\n 'select_left_click',\n 'select_right_click',\n 'select_space',\n 'multiselect_h',\n 'multiselect',\n 'multiselect_shift_click',\n 'multiselect_lasso',\n 'undo_redo_h',\n 'undo_redo',\n 'save_h',\n 'save',\n 'save_validation',\n 'upload_h',\n 'upload',\n 'backups_h',\n 'backups',\n 'keyboard_h',\n 'keyboard'\n ]],\n ['feature_editor', [\n 'intro',\n 'definitions',\n 'type_h',\n 'type',\n 'type_picker',\n 'fields_h',\n 'fields_all_fields',\n 'fields_example',\n 'fields_add_field',\n 'tags_h',\n 'tags_all_tags',\n 'tags_resources'\n ]],\n ['points', [\n 'intro',\n 'add_point_h',\n 'add_point',\n 'add_point_finish',\n 'move_point_h',\n 'move_point',\n 'delete_point_h',\n 'delete_point',\n 'delete_point_command'\n ]],\n ['lines', [\n 'intro',\n 'add_line_h',\n 'add_line',\n 'add_line_draw',\n 'add_line_continue',\n 'add_line_finish',\n 'modify_line_h',\n 'modify_line_dragnode',\n 'modify_line_addnode',\n 'connect_line_h',\n 'connect_line',\n 'connect_line_display',\n 'connect_line_drag',\n 'connect_line_tag',\n 'disconnect_line_h',\n 'disconnect_line_command',\n 'move_line_h',\n 'move_line_command',\n 'move_line_connected',\n 'delete_line_h',\n 'delete_line',\n 'delete_line_command'\n ]],\n ['areas', [\n 'intro',\n 'point_or_area_h',\n 'point_or_area',\n 'add_area_h',\n 'add_area_command',\n 'add_area_draw',\n 'add_area_continue',\n 'add_area_finish',\n 'square_area_h',\n 'square_area_command',\n 'modify_area_h',\n 'modify_area_dragnode',\n 'modify_area_addnode',\n 'delete_area_h',\n 'delete_area',\n 'delete_area_command'\n ]],\n ['relations', [\n 'intro',\n 'edit_relation_h',\n 'edit_relation',\n 'edit_relation_add',\n 'edit_relation_delete',\n 'maintain_relation_h',\n 'maintain_relation',\n 'relation_types_h',\n 'multipolygon_h',\n 'multipolygon',\n 'multipolygon_create',\n 'multipolygon_merge',\n 'turn_restriction_h',\n 'turn_restriction',\n 'turn_restriction_field',\n 'turn_restriction_editing',\n 'route_h',\n 'route',\n 'route_add',\n 'boundary_h',\n 'boundary',\n 'boundary_add'\n ]],\n ['operations', [\n 'intro',\n 'intro_2',\n 'straighten',\n 'orthogonalize',\n 'circularize',\n 'move',\n 'rotate',\n 'reflect',\n 'continue',\n 'reverse',\n 'disconnect',\n 'split',\n 'extract',\n 'merge',\n 'delete',\n 'downgrade',\n 'copy_paste'\n ]],\n ['notes', [\n 'intro',\n 'add_note_h',\n 'add_note',\n 'place_note',\n 'move_note',\n 'update_note_h',\n 'update_note',\n 'save_note_h',\n 'save_note'\n ]],\n ['imagery', [\n 'intro',\n 'sources_h',\n 'choosing',\n 'sources',\n 'offsets_h',\n 'offset',\n 'offset_change'\n ]],\n ['streetlevel', [\n 'intro',\n 'using_h',\n 'using',\n 'photos',\n 'viewer'\n ]],\n ['gps', [\n 'intro',\n 'survey',\n 'using_h',\n 'using',\n 'tracing',\n 'upload'\n ]],\n ['qa', [\n 'intro',\n 'tools_h',\n 'tools',\n 'issues_h',\n 'issues'\n ]]\n ];\n\n var headings = {\n 'help.areas.add_area_h': 3,\n 'help.areas.delete_area_h': 3,\n 'help.areas.modify_area_h': 3,\n 'help.areas.point_or_area_h': 3,\n 'help.areas.square_area_h': 3,\n 'help.editing.backups_h': 3,\n 'help.editing.keyboard_h': 3,\n 'help.editing.multiselect_h': 3,\n 'help.editing.save_h': 3,\n 'help.editing.select_h': 3,\n 'help.editing.undo_redo_h': 3,\n 'help.editing.upload_h': 3,\n 'help.feature_editor.fields_h': 3,\n 'help.feature_editor.tags_h': 3,\n 'help.feature_editor.type_h': 3,\n 'help.gps.using_h': 3,\n 'help.help.before_start_h': 3,\n 'help.help.open_data_h': 3,\n 'help.help.open_source_h': 3,\n 'help.imagery.offsets_h': 3,\n 'help.imagery.sources_h': 3,\n 'help.lines.add_line_h': 3,\n 'help.lines.connect_line_h': 3,\n 'help.lines.delete_line_h': 3,\n 'help.lines.disconnect_line_h': 3,\n 'help.lines.modify_line_h': 3,\n 'help.lines.move_line_h': 3,\n 'help.notes.add_note_h': 3,\n 'help.notes.save_note_h': 3,\n 'help.notes.update_note_h': 3,\n 'help.overview.features_h': 3,\n 'help.overview.navigation_h': 3,\n 'help.points.add_point_h': 3,\n 'help.points.delete_point_h': 3,\n 'help.points.move_point_h': 3,\n 'help.qa.issues_h': 3,\n 'help.qa.tools_h': 3,\n 'help.relations.boundary_h': 3,\n 'help.relations.edit_relation_h': 3,\n 'help.relations.maintain_relation_h': 3,\n 'help.relations.multipolygon_h': 3,\n 'help.relations.relation_types_h': 2,\n 'help.relations.route_h': 3,\n 'help.relations.turn_restriction_h': 3,\n 'help.streetlevel.using_h': 3\n };\n\n // For each section, squash all the texts into a single markdown document\n var docs = docKeys.map(function(key) {\n var helpkey = 'help.' + key[0];\n var helpPaneReplacements = { version: context.version };\n var text = key[1].reduce(function(all, part) {\n var subkey = helpkey + '.' + part;\n var depth = headings[subkey]; // is this subkey a heading?\n var hhh = depth ? Array(depth + 1).join('#') + ' ' : ''; // if so, prepend with some ##'s\n return all + hhh + helpHtml(subkey, helpPaneReplacements) + '\\n\\n';\n }, '');\n\n return {\n title: t.addOrUpdate(helpkey + '.title'),\n _title: t(helpkey + '.title'),\n content: marked(text.trim())\n // use keyboard key styling for shortcuts\n .replace(//g, '')\n .replace(/<\\/code>/g, '<\\/kbd>')\n };\n });\n\n var helpPane = uiPane('help', context)\n .key(t('help.key'))\n .label(t.append('help.title'))\n .description(t.append('help.title'))\n .iconName('iD-icon-help');\n\n helpPane.renderContent = function(content) {\n\n function clickHelp(d, i) {\n\n var rtl = (localizer.textDirection() === 'rtl');\n content.property('scrollTop', 0);\n helpPane.selection().select('.pane-heading h2').call(d.title);\n\n body.html(d.content);\n body.selectAll('a')\n .attr('target', '_blank');\n menuItems.classed('selected', function(m) {\n return m._title === d._title;\n });\n\n nav.text('');\n if (rtl) {\n nav.call(drawNext).call(drawPrevious);\n } else {\n nav.call(drawPrevious).call(drawNext);\n }\n\n\n function drawNext(selection) {\n if (i < docs.length - 1) {\n var nextLink = selection\n .append('a')\n .attr('href', '#')\n .attr('class', 'next')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n clickHelp(docs[i + 1], i + 1);\n });\n\n nextLink\n .append('span')\n .call(docs[i + 1].title)\n .call(svgIcon((rtl ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline'));\n }\n }\n\n\n function drawPrevious(selection) {\n if (i > 0) {\n var prevLink = selection\n .append('a')\n .attr('href', '#')\n .attr('class', 'previous')\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n clickHelp(docs[i - 1], i - 1);\n });\n\n prevLink\n .call(svgIcon((rtl ? '#iD-icon-forward' : '#iD-icon-backward'), 'inline'))\n .append('span')\n .call(docs[i - 1].title);\n }\n }\n }\n\n\n function clickWalkthrough(d3_event) {\n d3_event.preventDefault();\n if (context.inIntro()) return;\n context.container().call(uiIntro(context));\n context.ui().togglePanes();\n }\n\n\n function clickShortcuts(d3_event) {\n d3_event.preventDefault();\n context.container().call(context.ui().shortcuts, true);\n }\n\n var toc = content\n .append('ul')\n .attr('class', 'toc');\n\n var menuItems = toc.selectAll('li')\n .data(docs)\n .enter()\n .append('li')\n .append('a')\n .attr('role', 'button')\n .attr('href', '#')\n .each(function(d) {\n d3_select(this).call(d.title);\n })\n .on('click', function(d3_event, d) {\n d3_event.preventDefault();\n clickHelp(d, docs.indexOf(d));\n });\n\n var shortcuts = toc\n .append('li')\n .attr('class', 'shortcuts')\n .call(uiTooltip()\n .title(() => t.append('shortcuts.tooltip'))\n .keys(['?'])\n .placement('top')\n )\n .append('a')\n .attr('href', '#')\n .on('click', clickShortcuts);\n\n shortcuts\n .append('div')\n .call(t.append('shortcuts.title'));\n\n var walkthrough = toc\n .append('li')\n .attr('class', 'walkthrough')\n .append('a')\n .attr('href', '#')\n .on('click', clickWalkthrough);\n\n walkthrough\n .append('svg')\n .attr('class', 'logo logo-walkthrough')\n .append('use')\n .attr('xlink:href', '#iD-logo-walkthrough');\n\n walkthrough\n .append('div')\n .call(t.append('splash.walkthrough'));\n\n\n var helpContent = content\n .append('div')\n .attr('class', 'left-content');\n\n var body = helpContent\n .append('div')\n .attr('class', 'body');\n\n var nav = helpContent\n .append('div')\n .attr('class', 'nav');\n\n clickHelp(docs[0], 0);\n };\n\n return helpPane;\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport {\n select as d3_select\n} from 'd3-selection';\n\n//import { actionNoop } from '../actions/noop';\nimport { geoSphericalDistance } from '../../geo';\nimport { svgIcon } from '../../svg/icon';\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { utilHighlightEntities } from '../../util';\nimport { uiSection } from '../section';\nimport { validationIssue } from '../../core/validation';\n\nexport function uiSectionValidationIssues(id, severity, context) {\n\n var _issues = [];\n\n var section = uiSection(id, context)\n .label(function() {\n if (!_issues) return '';\n var issueCountText = _issues.length > 1000 ? '1000+' : String(_issues.length);\n return t.append('inspector.title_count', { title: t.append('issues.' + severity + 's.list_title'), count: issueCountText });\n })\n .disclosureContent(renderDisclosureContent)\n .shouldDisplay(function() {\n return _issues && _issues.length;\n });\n\n function getOptions() {\n return {\n what: prefs('validate-what') || 'edited',\n where: prefs('validate-where') || 'all'\n };\n }\n\n // get and cache the issues to display, unordered\n function reloadIssues() {\n _issues = context.validator().getIssuesBySeverity(getOptions())[severity];\n }\n\n function renderDisclosureContent(selection) {\n\n var center = context.map().center();\n var graph = context.graph();\n\n // sort issues by distance away from the center of the map\n var issues = _issues.map(function withDistance(issue) {\n var extent = issue.extent(graph);\n var dist = extent ? geoSphericalDistance(center, extent.center()) : 0;\n return Object.assign(issue, { dist: dist });\n })\n .sort(function byDistance(a, b) {\n return a.dist - b.dist;\n });\n\n // cut off at 1000\n issues = issues.slice(0, 1000);\n\n //renderIgnoredIssuesReset(_warningsSelection);\n\n selection\n .call(drawIssuesList, issues);\n }\n\n function drawIssuesList(selection, issues) {\n var list = selection.selectAll('.issues-list')\n .data([0]);\n\n list = list.enter()\n .append('ul')\n .attr('class', 'layer-list issues-list ' + severity + 's-list')\n .merge(list);\n\n\n var items = list.selectAll('li')\n .data(issues, function(d) { return d.key; });\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var itemsEnter = items.enter()\n .append('li')\n .attr('class', function (d) { return 'issue severity-' + d.severity; });\n\n var labelsEnter = itemsEnter\n .append('button')\n .attr('class', 'issue-label')\n .on('click', function(d3_event, d) {\n context.validator().focusIssue(d);\n })\n .on('mouseover', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, true, context);\n })\n .on('mouseout', function(d3_event, d) {\n utilHighlightEntities(d.entityIds, false, context);\n });\n\n var textEnter = labelsEnter\n .append('span')\n .attr('class', 'issue-text');\n\n textEnter\n .append('span')\n .attr('class', 'issue-icon')\n .each(function(d) {\n d3_select(this)\n .call(svgIcon(validationIssue.ICONS[d.severity]));\n });\n\n textEnter\n .append('span')\n .attr('class', 'issue-message');\n\n // Update\n items = items\n .merge(itemsEnter)\n .order();\n\n items.selectAll('.issue-message')\n .text('')\n .each(function(d) {\n return d.message(context)(d3_select(this));\n });\n }\n\n context.validator().on('validated.uiSectionValidationIssues' + id, function() {\n window.requestIdleCallback(function() {\n reloadIssues();\n section.reRender();\n });\n });\n\n context.map().on('move.uiSectionValidationIssues' + id,\n debounce(function() {\n window.requestIdleCallback(function() {\n if (getOptions().where === 'visible') {\n // must refetch issues if they are viewport-dependent\n reloadIssues();\n }\n // always reload list to re-sort-by-distance\n section.reRender();\n });\n }, 1000)\n );\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiSection } from '../section';\n\nexport function uiSectionValidationOptions(context) {\n\n var section = uiSection('issues-options', context)\n .content(renderContent);\n\n function renderContent(selection) {\n\n var container = selection.selectAll('.issues-options-container')\n .data([0]);\n\n container = container.enter()\n .append('div')\n .attr('class', 'issues-options-container')\n .merge(container);\n\n var data = [\n { key: 'what', values: ['edited', 'all'] },\n { key: 'where', values: ['visible', 'all'] }\n ];\n\n var options = container.selectAll('.issues-option')\n .data(data, function(d) { return d.key; });\n\n var optionsEnter = options.enter()\n .append('div')\n .attr('class', function(d) { return 'issues-option issues-option-' + d.key; });\n\n optionsEnter\n .append('div')\n .attr('class', 'issues-option-title')\n .each(function(d) {\n d3_select(this).call(t.append('issues.options.' + d.key + '.title'));\n });\n\n var valuesEnter = optionsEnter.selectAll('label')\n .data(function(d) {\n return d.values.map(function(val) { return { value: val, key: d.key }; });\n })\n .enter()\n .append('label');\n\n valuesEnter\n .append('input')\n .attr('type', 'radio')\n .attr('name', function(d) { return 'issues-option-' + d.key; })\n .attr('value', function(d) { return d.value; })\n .property('checked', function(d) { return getOptions()[d.key] === d.value; })\n .on('change', function(d3_event, d) { updateOptionValue(d3_event, d.key, d.value); });\n\n valuesEnter\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append('issues.options.' + d.key + '.' + d.value));\n });\n }\n\n function getOptions() {\n return {\n what: prefs('validate-what') || 'edited', // 'all', 'edited'\n where: prefs('validate-where') || 'all' // 'all', 'visible'\n };\n }\n\n function updateOptionValue(d3_event, d, val) {\n if (!val && d3_event && d3_event.target) {\n val = d3_event.target.value;\n }\n\n prefs('validate-' + d, val);\n context.validator().validate();\n }\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { utilGetSetValue, utilNoAuto } from '../../util';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionValidationRules(context) {\n\n var MINSQUARE = 0;\n var MAXSQUARE = 20;\n var DEFAULTSQUARE = 5; // see also unsquare_way.js\n\n var section = uiSection('issues-rules', context)\n .disclosureContent(renderDisclosureContent)\n .label(() => t.append('issues.rules.title'));\n\n var _ruleKeys = context.validator().getRuleKeys()\n .filter(function(key) { return key !== 'maprules'; })\n .sort(function(key1, key2) {\n // alphabetize by localized title\n return t('issues.' + key1 + '.title') < t('issues.' + key2 + '.title') ? -1 : 1;\n });\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.issues-rulelist-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'issues-rulelist-container');\n\n containerEnter\n .append('ul')\n .attr('class', 'layer-list issue-rules-list');\n\n var ruleLinks = containerEnter\n .append('div')\n .attr('class', 'issue-rules-links section-footer');\n\n ruleLinks\n .append('a')\n .attr('class', 'issue-rules-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.disable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.validator().disableRules(_ruleKeys);\n });\n\n ruleLinks\n .append('a')\n .attr('class', 'issue-rules-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.enable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.validator().disableRules([]);\n });\n\n\n // Update\n container = container\n .merge(containerEnter);\n\n container.selectAll('.issue-rules-list')\n .call(drawListItems, _ruleKeys, 'checkbox', 'rule', toggleRule, isRuleEnabled);\n }\n\n function drawListItems(selection, data, type, name, change, active) {\n var items = selection.selectAll('li')\n .data(data);\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li');\n\n if (name === 'rule') {\n enter\n .call(uiTooltip()\n .title(function(d) { return t.append('issues.' + d + '.tip'); })\n .placement('top')\n );\n }\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', name)\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) {\n var params = {};\n if (d === 'unsquare_way') {\n params.val = selection => selection\n .append('span')\n .classed('square-degrees', true);\n }\n d3_select(this).call(t.append('issues.' + d + '.title', params));\n });\n\n // Update\n items = items\n .merge(enter);\n\n items\n .classed('active', active)\n .selectAll('input')\n .property('checked', active)\n .property('indeterminate', false);\n\n\n // user-configurable square threshold\n var degStr = prefs('validate-square-degrees');\n if (degStr === null) {\n degStr = DEFAULTSQUARE.toString();\n }\n\n var span = items.selectAll('.square-degrees');\n var input = span.selectAll('.square-degrees-input')\n .data([0]);\n\n // enter / update\n input.enter()\n .append('input')\n .attr('type', 'number')\n .attr('min', MINSQUARE.toString())\n .attr('max', MAXSQUARE.toString())\n .attr('step', '0.5')\n .attr('class', 'square-degrees-input')\n .call(utilNoAuto)\n .on('click', function (d3_event) {\n d3_event.preventDefault();\n d3_event.stopPropagation();\n this.select();\n })\n .on('keyup', function (d3_event) {\n if (d3_event.keyCode === 13) { // \u21A9 Return\n this.blur();\n this.select();\n }\n })\n .on('blur', changeSquare)\n .merge(input)\n .property('value', degStr);\n }\n\n function changeSquare() {\n var input = d3_select(this);\n var degStr = utilGetSetValue(input).trim();\n var degNum = Number(degStr);\n\n if (!isFinite(degNum)) {\n degNum = DEFAULTSQUARE;\n } else if (degNum > MAXSQUARE) {\n degNum = MAXSQUARE;\n } else if (degNum < MINSQUARE) {\n degNum = MINSQUARE;\n }\n\n degNum = Math.round(degNum * 10 ) / 10; // round to 1 decimal\n degStr = degNum.toString();\n\n input\n .property('value', degStr);\n\n prefs('validate-square-degrees', degStr);\n context.validator().revalidateUnsquare();\n }\n\n function isRuleEnabled(d) {\n return context.validator().isRuleEnabled(d);\n }\n\n function toggleRule(d3_event, d) {\n context.validator().toggleRule(d);\n }\n\n context.validator().on('validated.uiSectionValidationRules', function() {\n window.requestIdleCallback(section.reRender);\n });\n\n return section;\n}\n", "import { debounce } from 'es-toolkit/compat';\n\nimport { svgIcon } from '../../svg/icon';\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiSection } from '../section';\n\nexport function uiSectionValidationStatus(context) {\n\n var section = uiSection('issues-status', context)\n .content(renderContent)\n .shouldDisplay(function() {\n var issues = context.validator().getIssues(getOptions());\n return issues.length === 0;\n });\n\n function getOptions() {\n return {\n what: prefs('validate-what') || 'edited',\n where: prefs('validate-where') || 'all'\n };\n }\n\n function renderContent(selection) {\n\n var box = selection.selectAll('.box')\n .data([0]);\n\n var boxEnter = box.enter()\n .append('div')\n .attr('class', 'box');\n\n boxEnter\n .append('div')\n .call(svgIcon('#iD-icon-apply', 'pre-text'));\n\n var noIssuesMessage = boxEnter\n .append('span');\n\n noIssuesMessage\n .append('strong')\n .attr('class', 'message');\n\n noIssuesMessage\n .append('br');\n\n noIssuesMessage\n .append('span')\n .attr('class', 'details');\n\n renderIgnoredIssuesReset(selection);\n setNoIssuesText(selection);\n }\n\n function renderIgnoredIssuesReset(selection) {\n\n var ignoredIssues = context.validator()\n .getIssues({ what: 'all', where: 'all', includeDisabledRules: true, includeIgnored: 'only' });\n\n var resetIgnored = selection.selectAll('.reset-ignored')\n .data(ignoredIssues.length ? [0] : []);\n\n // exit\n resetIgnored.exit()\n .remove();\n\n // enter\n var resetIgnoredEnter = resetIgnored.enter()\n .append('div')\n .attr('class', 'reset-ignored section-footer');\n\n resetIgnoredEnter\n .append('a')\n .attr('href', '#');\n\n // update\n resetIgnored = resetIgnored\n .merge(resetIgnoredEnter);\n\n resetIgnored.select('a')\n .call(t.addOrUpdate('inspector.title_count', { title: t.append('issues.reset_ignored'), count: ignoredIssues.length }));\n\n resetIgnored.on('click', function(d3_event) {\n d3_event.preventDefault();\n context.validator().resetIgnoredIssues();\n });\n }\n\n function setNoIssuesText(selection) {\n\n var opts = getOptions();\n\n function checkForHiddenIssues(cases) {\n for (var type in cases) {\n var hiddenOpts = cases[type];\n var hiddenIssues = context.validator().getIssues(hiddenOpts);\n if (hiddenIssues.length) {\n selection.select('.box .details')\n .html('')\n .call(t.append(\n 'issues.no_issues.hidden_issues.' + type,\n { count: hiddenIssues.length.toString() }\n ));\n return;\n }\n }\n selection.select('.box .details')\n .html('')\n .call(t.append('issues.no_issues.hidden_issues.none'));\n }\n\n var messageType;\n\n if (opts.what === 'edited' && opts.where === 'visible') {\n\n messageType = 'edits_in_view';\n\n checkForHiddenIssues({\n elsewhere: { what: 'edited', where: 'all' },\n everything_else: { what: 'all', where: 'visible' },\n disabled_rules: { what: 'edited', where: 'visible', includeDisabledRules: 'only' },\n everything_else_elsewhere: { what: 'all', where: 'all' },\n disabled_rules_elsewhere: { what: 'edited', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'edited', where: 'visible', includeIgnored: 'only' },\n ignored_issues_elsewhere: { what: 'edited', where: 'all', includeIgnored: 'only' }\n });\n\n } else if (opts.what === 'edited' && opts.where === 'all') {\n\n messageType = 'edits';\n\n checkForHiddenIssues({\n everything_else: { what: 'all', where: 'all' },\n disabled_rules: { what: 'edited', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'edited', where: 'all', includeIgnored: 'only' }\n });\n\n } else if (opts.what === 'all' && opts.where === 'visible') {\n\n messageType = 'everything_in_view';\n\n checkForHiddenIssues({\n elsewhere: { what: 'all', where: 'all' },\n disabled_rules: { what: 'all', where: 'visible', includeDisabledRules: 'only' },\n disabled_rules_elsewhere: { what: 'all', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'all', where: 'visible', includeIgnored: 'only' },\n ignored_issues_elsewhere: { what: 'all', where: 'all', includeIgnored: 'only' }\n });\n } else if (opts.what === 'all' && opts.where === 'all') {\n\n messageType = 'everything';\n\n checkForHiddenIssues({\n disabled_rules: { what: 'all', where: 'all', includeDisabledRules: 'only' },\n ignored_issues: { what: 'all', where: 'all', includeIgnored: 'only' }\n });\n }\n\n if (opts.what === 'edited' && context.history().difference().summary().length === 0) {\n messageType = 'no_edits';\n }\n\n selection.select('.box .message')\n .html('')\n .call(t.append('issues.no_issues.message.' + messageType));\n\n }\n\n context.validator().on('validated.uiSectionValidationStatus', function() {\n window.requestIdleCallback(section.reRender);\n });\n\n context.map().on('move.uiSectionValidationStatus',\n debounce(function() {\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\n\nimport { uiSectionValidationIssues } from '../sections/validation_issues';\nimport { uiSectionValidationOptions } from '../sections/validation_options';\nimport { uiSectionValidationRules } from '../sections/validation_rules';\nimport { uiSectionValidationStatus } from '../sections/validation_status';\n\nexport function uiPaneIssues(context) {\n\n var issuesPane = uiPane('issues', context)\n .key(t('issues.key'))\n .label(t.append('issues.title'))\n .description(t.append('issues.title'))\n .iconName('iD-icon-alert')\n .sections([\n uiSectionValidationOptions(context),\n uiSectionValidationStatus(context),\n uiSectionValidationIssues('issues-errors', 'error', context),\n uiSectionValidationIssues('issues-warnings', 'warning', context),\n uiSectionValidationIssues('issues-suggestions', 'suggestion', context),\n uiSectionValidationRules(context)\n ]);\n\n return issuesPane;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\n\nimport { prefs } from '../../core/preferences';\nimport { t } from '../../core/localizer';\nimport { uiConfirm } from '../confirm';\nimport { utilNoAuto, utilRebind } from '../../util';\n\n\nexport function uiSettingsCustomData(context) {\n var dispatch = d3_dispatch('change');\n\n function render(selection) {\n var dataLayer = context.layers().layer('data');\n\n // keep separate copies of original and current settings\n var _origSettings = {\n fileList: (dataLayer && dataLayer.fileList()) || null,\n url: prefs('settings-custom-data-url')\n };\n var _currSettings = {\n fileList: (dataLayer && dataLayer.fileList()) || null,\n // url: prefs('settings-custom-data-url')\n };\n\n // var example = 'https://tile.openstreetmap.org/{zoom}/{x}/{y}.png';\n var modal = uiConfirm(selection).okButton();\n\n modal\n .classed('settings-modal settings-custom-data', true);\n\n modal.select('.modal-section.header')\n .append('h3')\n .call(t.append('settings.custom_data.header'));\n\n\n var textSection = modal.select('.modal-section.message-text');\n\n textSection\n .append('pre')\n .attr('class', 'instructions-file')\n .call(t.append('settings.custom_data.file.instructions'));\n\n textSection\n .append('input')\n .attr('class', 'field-file')\n .attr('type', 'file')\n .attr('accept', '.gpx,.kml,.geojson,.json,application/gpx+xml,application/vnd.google-earth.kml+xml,application/geo+json,application/json')\n .property('files', _currSettings.fileList)\n .on('change', function(d3_event) {\n var files = d3_event.target.files;\n if (files && files.length) {\n _currSettings.url = '';\n textSection.select('.field-url').property('value', '');\n _currSettings.fileList = files;\n } else {\n _currSettings.fileList = null;\n }\n });\n\n textSection\n .append('h4')\n .call(t.append('settings.custom_data.or'));\n\n textSection\n .append('pre')\n .attr('class', 'instructions-url')\n .call(t.append('settings.custom_data.url.instructions'));\n\n textSection\n .append('textarea')\n .attr('class', 'field-url')\n .attr('placeholder', t('settings.custom_data.url.placeholder'))\n .call(utilNoAuto)\n .property('value', _currSettings.url);\n\n\n // insert a cancel button\n var buttonSection = modal.select('.modal-section.buttons');\n\n buttonSection\n .insert('button', '.ok-button')\n .attr('class', 'button cancel-button secondary-action')\n .call(t.append('confirm.cancel'));\n\n\n buttonSection.select('.cancel-button')\n .on('click.cancel', clickCancel);\n\n buttonSection.select('.ok-button')\n .attr('disabled', isSaveDisabled)\n .on('click.save', clickSave);\n\n\n function isSaveDisabled() {\n return null;\n }\n\n\n // restore the original url\n function clickCancel() {\n textSection.select('.field-url').property('value', _origSettings.url);\n prefs('settings-custom-data-url', _origSettings.url);\n this.blur();\n modal.close();\n }\n\n // accept the current url\n function clickSave() {\n _currSettings.url = textSection.select('.field-url').property('value').trim();\n\n // one or the other but not both\n if (_currSettings.url) { _currSettings.fileList = null; }\n if (_currSettings.fileList) { _currSettings.url = ''; }\n\n prefs('settings-custom-data-url', _currSettings.url);\n this.blur();\n modal.close();\n dispatch.call('change', this, _currSettings);\n }\n }\n\n return utilRebind(render, dispatch, 'on');\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { prefs } from '../../core/preferences';\nimport { t, localizer } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg/icon';\nimport { geoExtent } from '../../geo';\nimport { modeBrowse } from '../../modes/browse';\nimport { uiCmd } from '../cmd';\nimport { uiSection } from '../section';\nimport { uiSettingsCustomData } from '../settings/custom_data';\n\nexport function uiSectionDataLayers(context) {\n\n var settingsCustomData = uiSettingsCustomData(context)\n .on('change', customChanged);\n\n // refers to `modules/svg/layers.js` -> function drawLayers(selection) {...}\n var layers = context.layers();\n\n var section = uiSection('data-layers', context)\n .label(() => t.append('map_data.data_layers'))\n .disclosureContent(renderDisclosureContent);\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.data-layer-container')\n .data([0]);\n\n container.enter()\n .append('div')\n .attr('class', 'data-layer-container')\n .merge(container)\n .call(drawOsmItems)\n .call(drawQAItems)\n .call(drawCustomDataItems)\n .call(drawVectorItems) // Beta - Detroit mapping challenge\n .call(drawPanelItems);\n }\n\n function showsLayer(which) {\n var layer = layers.layer(which);\n if (layer) {\n return layer.enabled();\n }\n return false;\n }\n\n function setLayer(which, enabled) {\n // Don't allow layer changes while drawing - #6584\n var mode = context.mode();\n if (mode && /^draw/.test(mode.id)) return;\n\n var layer = layers.layer(which);\n if (layer) {\n layer.enabled(enabled);\n\n if (!enabled && (which === 'osm' || which === 'notes')) {\n context.enter(modeBrowse(context));\n }\n }\n }\n\n function toggleLayer(which) {\n setLayer(which, !showsLayer(which));\n }\n\n function drawOsmItems(selection) {\n var osmKeys = ['osm', 'notes'];\n var osmLayers = layers.all().filter(function(obj) { return osmKeys.indexOf(obj.id) !== -1; });\n\n var ul = selection\n .selectAll('.layer-list-osm')\n .data([0]);\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-osm')\n .merge(ul);\n\n var li = ul.selectAll('.list-item')\n .data(osmLayers);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) { return 'list-item list-item-' + d.id; });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n if (d.id === 'osm') {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.' + d.id + '.tooltip'))\n .keys([uiCmd('\u2325' + t('area_fill.wireframe.key'))])\n .placement('bottom')\n );\n } else {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.' + d.id + '.tooltip'))\n .placement('bottom')\n );\n }\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) { toggleLayer(d.id); });\n\n labelEnter\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append('map_data.layers.' + d.id + '.title'));\n });\n\n\n // Update\n li\n .merge(liEnter)\n .classed('active', function (d) { return d.layer.enabled(); })\n .selectAll('input')\n .property('checked', function (d) { return d.layer.enabled(); });\n }\n\n function drawQAItems(selection) {\n var qaKeys = ['osmose'];\n var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; });\n\n var ul = selection\n .selectAll('.layer-list-qa')\n .data([0]);\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-qa')\n .merge(ul);\n\n var li = ul.selectAll('.list-item')\n .data(qaLayers);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) { return 'list-item list-item-' + d.id; });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.' + d.id + '.tooltip'))\n .placement('bottom')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) { toggleLayer(d.id); });\n\n labelEnter\n .append('span')\n .each(function(d) { t.append('map_data.layers.' + d.id + '.title')(d3_select(this)); });\n\n\n // Update\n li\n .merge(liEnter)\n .classed('active', function (d) { return d.layer.enabled(); })\n .selectAll('input')\n .property('checked', function (d) { return d.layer.enabled(); });\n }\n\n // Beta feature - sample vector layers to support Detroit Mapping Challenge\n // https://github.com/osmus/detroit-mapping-challenge\n function drawVectorItems(selection) {\n var dataLayer = layers.layer('data');\n var vtData = [\n {\n name: 'Detroit Neighborhoods/Parks',\n src: 'neighborhoods-parks',\n tooltip: 'Neighborhood boundaries and parks as compiled by City of Detroit in concert with community groups.',\n template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmur6x34562qp9iv1u3ksf-54hev,jonahadkins.cjksmqxdx33jj2wp90xd9x2md-4e5y2/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'\n }, {\n name: 'Detroit Composite POIs',\n src: 'composite-poi',\n tooltip: 'Fire Inspections, Business Licenses, and other public location data collated from the City of Detroit.',\n template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmm6a02sli31myxhsr7zf3-2sw8h/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'\n }, {\n name: 'Detroit All-The-Places POIs',\n src: 'alltheplaces-poi',\n tooltip: 'Public domain business location data created by web scrapers.',\n template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmswgk340g2vo06p1w9w0j-8fjjc/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'\n }\n ];\n\n // Only show this if the map is around Detroit..\n var detroit = geoExtent([-83.5, 42.1], [-82.8, 42.5]);\n var showVectorItems = (context.map().zoom() > 9 && detroit.contains(context.map().center()));\n\n var container = selection.selectAll('.vectortile-container')\n .data(showVectorItems ? [0] : []);\n\n container.exit()\n .remove();\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'vectortile-container');\n\n containerEnter\n .append('h4')\n .attr('class', 'vectortile-header')\n .text('Detroit Vector Tiles (Beta)');\n\n containerEnter\n .append('ul')\n .attr('class', 'layer-list layer-list-vectortile');\n\n containerEnter\n .append('div')\n .attr('class', 'vectortile-footer')\n .append('a')\n .attr('target', '_blank')\n .call(svgIcon('#iD-icon-out-link', 'inline'))\n .attr('href', 'https://github.com/osmus/detroit-mapping-challenge')\n .append('span')\n .text('About these layers');\n\n container = container\n .merge(containerEnter);\n\n\n var ul = container.selectAll('.layer-list-vectortile');\n\n var li = ul.selectAll('.list-item')\n .data(vtData);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) { return 'list-item list-item-' + d.src; });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n d3_select(this).call(\n uiTooltip().title(d.tooltip).placement('top')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'radio')\n .attr('name', 'vectortile')\n .on('change', selectVTLayer);\n\n labelEnter\n .append('span')\n .text(function(d) { return d.name; });\n\n // Update\n li\n .merge(liEnter)\n .classed('active', isVTLayerSelected)\n .selectAll('input')\n .property('checked', isVTLayerSelected);\n\n\n function isVTLayerSelected(d) {\n return dataLayer && dataLayer.template() === d.template;\n }\n\n function selectVTLayer(d3_event, d) {\n prefs('settings-custom-data-url', d.template);\n if (dataLayer) {\n dataLayer.template(d.template, d.src);\n dataLayer.enabled(true);\n }\n }\n }\n\n function drawCustomDataItems(selection) {\n var dataLayer = layers.layer('data');\n var hasData = dataLayer && dataLayer.hasData();\n var showsData = hasData && dataLayer.enabled();\n\n var ul = selection\n .selectAll('.layer-list-data')\n .data(dataLayer ? [0] : []);\n\n // Exit\n ul.exit()\n .remove();\n\n // Enter\n var ulEnter = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-data');\n\n var liEnter = ulEnter\n .append('li')\n .attr('class', 'list-item-data');\n\n var labelEnter = liEnter\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.custom.tooltip'))\n .placement('top')\n );\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function() { toggleLayer('data'); });\n\n labelEnter\n .append('span')\n .call(t.append('map_data.layers.custom.title'));\n\n liEnter\n .append('button')\n .attr('class', 'open-data-options')\n .call(uiTooltip()\n .title(() => t.append('settings.custom_data.tooltip'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n editCustom();\n })\n .call(svgIcon('#iD-icon-more'));\n\n liEnter\n .append('button')\n .attr('class', 'zoom-to-data')\n .call(uiTooltip()\n .title(() => t.append('map_data.layers.custom.zoom'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n if (d3_select(this).classed('disabled')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n dataLayer.fitZoom();\n })\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'));\n\n // Update\n ul = ul\n .merge(ulEnter);\n\n ul.selectAll('.list-item-data')\n .classed('active', showsData)\n .selectAll('label')\n .classed('deemphasize', !hasData)\n .selectAll('input')\n .property('disabled', !hasData)\n .property('checked', showsData);\n\n ul.selectAll('button.zoom-to-data')\n .classed('disabled', !hasData);\n }\n\n function editCustom() {\n context.container()\n .call(settingsCustomData);\n }\n\n function customChanged(d) {\n var dataLayer = layers.layer('data');\n\n if (d && d.url) {\n dataLayer.url(d.url);\n } else if (d && d.fileList) {\n dataLayer.fileList(d.fileList);\n }\n }\n\n function drawPanelItems(selection) {\n\n var panelsListEnter = selection.selectAll('.md-extras-list')\n .data([0])\n .enter()\n .append('ul')\n .attr('class', 'layer-list md-extras-list');\n\n var historyPanelLabelEnter = panelsListEnter\n .append('li')\n .attr('class', 'history-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('map_data.history_panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.history.key'))])\n .placement('top')\n );\n\n historyPanelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('history');\n });\n\n historyPanelLabelEnter\n .append('span')\n .call(t.append('map_data.history_panel.title'));\n\n var measurementPanelLabelEnter = panelsListEnter\n .append('li')\n .attr('class', 'measurement-panel-toggle-item')\n .append('label')\n .call(uiTooltip()\n .title(() => t.append('map_data.measurement_panel.tooltip'))\n .keys([uiCmd('\u2318\u21E7' + t('info_panels.measurement.key'))])\n .placement('top')\n );\n\n measurementPanelLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event) {\n d3_event.preventDefault();\n context.ui().info.toggle('measurement');\n });\n\n measurementPanelLabelEnter\n .append('span')\n .call(t.append('map_data.measurement_panel.title'));\n }\n\n context.layers().on('change.uiSectionDataLayers', section.reRender);\n\n context.map()\n .on('move.uiSectionDataLayers',\n debounce(function() {\n // Detroit layers may have moved in or out of view\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionMapFeatures(context) {\n\n var _features = context.features().keys();\n\n var section = uiSection('map-features', context)\n .label(() => t.append('map_data.map_features'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n function renderDisclosureContent(selection) {\n\n var container = selection.selectAll('.layer-feature-list-container')\n .data([0]);\n\n var containerEnter = container.enter()\n .append('div')\n .attr('class', 'layer-feature-list-container');\n\n containerEnter\n .append('ul')\n .attr('class', 'layer-list layer-feature-list');\n\n var footer = containerEnter\n .append('div')\n .attr('class', 'feature-list-links section-footer');\n\n footer\n .append('a')\n .attr('class', 'feature-list-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.disable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.features().disableAll();\n });\n\n footer\n .append('a')\n .attr('class', 'feature-list-link')\n .attr('role', 'button')\n .attr('href', '#')\n .call(t.append('issues.enable_all'))\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n context.features().enableAll();\n });\n\n // Update\n container = container\n .merge(containerEnter);\n\n container.selectAll('.layer-feature-list')\n .call(drawListItems, _features, 'checkbox', 'feature', clickFeature, showsFeature);\n }\n\n function drawListItems(selection, data, type, name, change, active) {\n var items = selection.selectAll('li')\n .data(data);\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li')\n .call(uiTooltip()\n .title(function(d) {\n var tip = t.append(name + '.' + d + '.tooltip');\n if (autoHiddenFeature(d)) {\n var msg = showsLayer('osm') ? t.append('map_data.autohidden') : t.append('map_data.osmhidden');\n return selection => {\n selection.call(tip);\n selection.append('div').call(msg);\n };\n }\n return tip;\n })\n .placement('top')\n );\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', name)\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append(name + '.' + d + '.description'));\n });\n\n // Update\n items = items\n .merge(enter);\n\n items\n .classed('active', active)\n .selectAll('input')\n .property('checked', active)\n .property('indeterminate', autoHiddenFeature);\n }\n\n function autoHiddenFeature(d) {\n return context.features().autoHidden(d);\n }\n\n function showsFeature(d) {\n return context.features().enabled(d);\n }\n\n function clickFeature(d3_event, d) {\n context.features().toggle(d);\n }\n\n function showsLayer(id) {\n var layer = context.layers().layer(id);\n return layer && layer.enabled();\n }\n\n // add listeners\n context.features()\n .on('change.map_features', section.reRender);\n\n return section;\n}\n", "import { select as d3_select } from 'd3-selection';\n\nimport { t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\n\nexport function uiSectionMapStyleOptions(context) {\n\n var section = uiSection('fill-area', context)\n .label(() => t.append('map_data.style_options'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.layer-fill-list')\n .data([0]);\n\n container.enter()\n .append('ul')\n .attr('class', 'layer-list layer-fill-list')\n .merge(container)\n .call(drawListItems, context.map().areaFillOptions, 'radio', 'area_fill', setFill, isActiveFill);\n\n var container2 = selection.selectAll('.layer-visual-diff-list')\n .data([0]);\n\n container2.enter()\n .append('ul')\n .attr('class', 'layer-list layer-visual-diff-list')\n .merge(container2)\n .call(drawListItems, ['highlight_edits'], 'checkbox', 'visual_diff', toggleHighlightEdited, function() {\n return context.surface().classed('highlight-edited');\n });\n }\n\n function drawListItems(selection, data, type, name, change, active) {\n var items = selection.selectAll('li')\n .data(data);\n\n // Exit\n items.exit()\n .remove();\n\n // Enter\n var enter = items.enter()\n .append('li')\n .call(uiTooltip()\n .title(function(d) {\n return t.append(name + '.' + d + '.tooltip');\n })\n .keys(function(d) {\n var key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null);\n if (d === 'highlight_edits') key = t('map_data.highlight_edits.key');\n return key ? [key] : null;\n })\n .placement('top')\n );\n\n var label = enter\n .append('label');\n\n label\n .append('input')\n .attr('type', type)\n .attr('name', name)\n .on('change', change);\n\n label\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append(name + '.' + d + '.description'));\n });\n\n // Update\n items = items\n .merge(enter);\n\n items\n .classed('active', active)\n .selectAll('input')\n .property('checked', active)\n .property('indeterminate', false);\n }\n\n function isActiveFill(d) {\n return context.map().activeAreaFill() === d;\n }\n\n function toggleHighlightEdited(d3_event) {\n d3_event.preventDefault();\n context.map().toggleHighlightEdited();\n }\n\n function setFill(d3_event, d) {\n context.map().activeAreaFill(d);\n }\n\n context.map()\n .on('changeHighlighting.ui_style, changeAreaFill.ui_style', section.reRender);\n\n return section;\n}\n", "import { dispatch as d3_dispatch } from 'd3-dispatch';\nimport { isArray, isNumber } from 'es-toolkit/compat';\n\nimport { t } from '../../core/localizer';\nimport { uiConfirm } from '../confirm';\nimport { utilRebind } from '../../util';\nimport { uiTooltip } from '../tooltip';\nimport { svgIcon } from '../../svg';\n\n\nexport function uiSettingsLocalPhotos(context) {\n var dispatch = d3_dispatch('change');\n var photoLayer = context.layers().layer('local-photos');\n var modal;\n\n function render(selection) {\n\n modal = uiConfirm(selection).okButton();\n\n modal\n .classed('settings-modal settings-local-photos', true);\n\n modal.select('.modal-section.header')\n .append('h3')\n .call(t.append('local_photos.header'));\n\n modal.select('.modal-section.message-text')\n .append('div')\n .classed('local-photos', true);\n\n var instructionsSection = modal.select('.modal-section.message-text .local-photos')\n .append('div')\n .classed('instructions', true);\n\n instructionsSection\n .append('p')\n .classed('instructions-local-photos', true)\n .call(t.append('local_photos.file.instructions'));\n\n instructionsSection\n .append('input')\n .classed('field-file', true)\n .attr('type', 'file')\n .attr('multiple', 'multiple')\n .attr('accept', '.jpg,.jpeg,.png,image/png,image/jpeg')\n .style('visibility', 'hidden')\n .attr('id', 'local-photo-files')\n .on('change', function(d3_event) {\n var files = d3_event.target.files;\n if (files && files.length) {\n photoList\n .select('ul')\n .append('li')\n .classed('placeholder', true)\n .append('div');\n dispatch.call('change', this, files);\n }\n d3_event.target.value = null;\n });\n instructionsSection\n .append('label')\n .attr('for', 'local-photo-files')\n .classed('button', true)\n .call(t.append('local_photos.file.label'));\n\n const photoList = modal.select('.modal-section.message-text .local-photos')\n .append('div')\n .append('div')\n .classed('list-local-photos', true);\n\n photoList\n .append('ul');\n\n updatePhotoList(photoList.select('ul'));\n\n context.layers().on('change', () => updatePhotoList(photoList.select('ul')));\n }\n\n function updatePhotoList(container) {\n function locationUnavailable(d) {\n return !(isArray(d.loc) && isNumber(d.loc[0]) && isNumber(d.loc[1]));\n }\n\n container.selectAll('li.placeholder').remove();\n\n let selection = container.selectAll('li')\n .data(photoLayer.getPhotos() ?? [], d => d.id);\n selection.exit()\n .remove();\n\n const selectionEnter = selection.enter()\n .append('li');\n\n selectionEnter\n .append('span')\n .classed('filename', true);\n selectionEnter\n .append('button')\n .classed('form-field-button zoom-to-data', true)\n .attr('title', t('local_photos.zoom_single'))\n .call(svgIcon('#iD-icon-framed-dot'));\n selectionEnter\n .append('button')\n .classed('form-field-button no-geolocation', true)\n .call(svgIcon('#iD-icon-alert'))\n .call(uiTooltip()\n .title(() => t.append('local_photos.no_geolocation.tooltip'))\n .placement('left')\n );\n selectionEnter\n .append('button')\n .classed('form-field-button remove', true)\n .attr('title', t('icons.remove'))\n .call(svgIcon('#iD-operation-delete'));\n\n selection = selection.merge(selectionEnter);\n\n selection\n .classed('invalid', locationUnavailable);\n selection.select('span.filename')\n .text(d => d.name)\n .attr('title', d => d.name);\n selection.select('span.filename')\n .on('click', (d3_event, d) => {\n photoLayer.openPhoto(d3_event, d, false);\n });\n selection.select('button.zoom-to-data')\n .on('click', (d3_event, d) => {\n photoLayer.openPhoto(d3_event, d, true);\n });\n selection.select('button.remove')\n .on('click', (d3_event, d) => {\n photoLayer.removePhoto(d.id);\n updatePhotoList(container);\n });\n }\n\n return utilRebind(render, dispatch, 'on');\n}\n", "import { debounce } from 'es-toolkit/compat';\nimport { select as d3_select } from 'd3-selection';\n\nimport { localizer, t } from '../../core/localizer';\nimport { uiTooltip } from '../tooltip';\nimport { uiSection } from '../section';\nimport { utilNoAuto } from '../../util';\nimport { uiSettingsLocalPhotos } from '../settings/local_photos';\nimport { svgIcon } from '../../svg';\n\nexport function uiSectionPhotoOverlays(context) {\n\n let _savedLayers = [];\n let _layersHidden = false;\n\n var settingsLocalPhotos = uiSettingsLocalPhotos(context)\n .on('change', localPhotosChanged);\n\n var layers = context.layers();\n\n var section = uiSection('photo-overlays', context)\n .label(() => t.append('photo_overlays.title'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n const photoDates = {};\n const now = +new Date();\n\n /**\n * Calls all draw function\n * @param {*} selection Current HTML selection\n */\n function renderDisclosureContent(selection) {\n var container = selection.selectAll('.photo-overlay-container')\n .data([0]);\n\n container.enter()\n .append('div')\n .attr('class', 'photo-overlay-container')\n .merge(container)\n .call(drawPhotoItems)\n .call(drawPhotoTypeItems)\n .call(drawDateSlider)\n .call(drawUsernameFilter)\n .call(drawLocalPhotos);\n }\n\n /**\n * Draws the streetlevels in the right panel\n */\n function drawPhotoItems(selection) {\n var photoKeys = context.photos().overlayLayerIDs();\n var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; });\n var data = photoLayers.filter(function(obj) {\n if (!obj.layer.supported()) return false;\n if (layerEnabled(obj)) return true;\n if (typeof obj.layer.validHere === 'function') {\n return obj.layer.validHere(context.map().extent(), context.map().zoom());\n }\n return true;\n });\n\n function layerSupported(d) {\n return d.layer && d.layer.supported();\n }\n function layerEnabled(d) {\n return layerSupported(d) && (d.layer.enabled() || _savedLayers.includes(d.id));\n }\n function layerRendered(d) {\n return d.layer.rendered?.(context.map().zoom()) ?? true;\n }\n\n var ul = selection\n .selectAll('.layer-list-photos')\n .data([0]);\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-photos')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-photos')\n .data(data, d => d.id);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) {\n var classes = 'list-item-photos list-item-' + d.id;\n if (d.id === 'mapillary-signs' || d.id === 'mapillary-map-features') {\n classes += ' indented';\n }\n return classes;\n });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n var titleID;\n if (d.id === 'mapillary-signs') titleID = 'mapillary.signs.tooltip';\n else if (d.id === 'mapillary') titleID = 'mapillary_images.tooltip';\n else if (d.id === 'kartaview') titleID = 'kartaview_images.tooltip';\n else titleID = d.id.replace(/-/g, '_') + '.tooltip';\n d3_select(this)\n .call(uiTooltip()\n .title(() => {\n if (!layerRendered(d)) {\n return t.append('street_side.minzoom_tooltip');\n } else {\n return t.append(titleID);\n }\n })\n .placement('top')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) { toggleLayer(d.id); });\n\n labelEnter\n .append('span')\n .each(function(d) {\n var id = d.id;\n if (id === 'mapillary-signs') id = 'photo_overlays.traffic_signs';\n d3_select(this).call(t.append(id.replace(/-/g, '_') + '.title'));\n });\n\n // Update\n li\n .merge(liEnter)\n .classed('active', layerEnabled)\n .selectAll('input')\n .property('disabled', d => !layerRendered(d))\n .property('checked', layerEnabled);\n }\n\n /**\n * Draws the photo type filter in the right panel\n */\n function drawPhotoTypeItems(selection) {\n var data = context.photos().allPhotoTypes();\n\n function typeEnabled(d) {\n return context.photos().showsPhotoType(d);\n }\n\n var ul = selection\n .selectAll('.layer-list-photo-types')\n .data([0]);\n\n ul.exit()\n .remove();\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-photo-types')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-photo-types')\n .data(context.photos().shouldFilterByPhotoType() ? data : []);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', function(d) {\n return 'list-item-photo-types list-item-' + d;\n });\n\n var labelEnter = liEnter\n .append('label')\n .each(function(d) {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('photo_overlays.photo_type.' + d + '.tooltip'))\n .placement('top')\n );\n });\n\n labelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function(d3_event, d) {\n context.photos().togglePhotoType(d, true);\n });\n\n labelEnter\n .append('span')\n .each(function(d) {\n d3_select(this).call(t.append('photo_overlays.photo_type.' + d + '.title'));\n });\n\n\n // Update\n li\n .merge(liEnter)\n .classed('active', typeEnabled)\n .selectAll('input')\n .property('checked', typeEnabled);\n }\n\n /**\n * Draws the date slider filter in the right panel\n */\n function drawDateSlider(selection){\n\n var ul = selection\n .selectAll('.layer-list-date-slider')\n .data([0]);\n\n ul.exit()\n .remove();\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-date-slider')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-date-slider')\n .data(context.photos().shouldFilterDateBySlider() ? ['date-slider'] : []);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', 'list-item-date-slider');\n\n var labelEnter = liEnter\n .append('label')\n .each(function() {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('photo_overlays.age_slider_filter.tooltip'))\n .placement('top')\n );\n });\n\n labelEnter\n .append('span')\n .attr('class', 'dateSliderSpan')\n .call(t.append('photo_overlays.age_slider_filter.title'));\n\n let sliderWrap = labelEnter\n .append('div')\n .attr('class','slider-wrap');\n\n sliderWrap\n .append('input')\n .attr('type', 'range')\n .attr('min', 0)\n .attr('max', 1)\n .attr('step', 0.001)\n .attr('list', 'photo-overlay-date-range')\n .attr('value', () => dateSliderValue('from'))\n .classed('list-option-date-slider', true)\n .classed('from-date', true)\n .style('direction', localizer.textDirection() === 'rtl' ? 'ltr' : 'rtl')\n .call(utilNoAuto)\n .on('change', function() {\n let value = d3_select(this).property('value');\n setYearFilter(value, true, 'from');\n });\n selection.select('input.from-date').each(function() { this.value = dateSliderValue('from'); });\n\n sliderWrap.append('div')\n .attr('class', 'date-slider-label');\n\n sliderWrap\n .append('input')\n .attr('type', 'range')\n .attr('min', 0)\n .attr('max', 1)\n .attr('step', 0.001)\n .attr('list', 'photo-overlay-date-range-inverted')\n .attr('value', () => 1 - dateSliderValue('to'))\n .classed('list-option-date-slider', true)\n .classed('to-date', true)\n // OHM variant: https://github.com/OpenHistoricalMap/issues/issues/1030 .style('display', () => dateSliderValue('to') === 0 ? 'none' : null)\n .style('direction', localizer.textDirection())\n .call(utilNoAuto)\n .on('change', function() {\n let value = d3_select(this).property('value');\n setYearFilter(1-value, true, 'to');\n });\n selection.select('input.to-date').each(function() { this.value = 1 - dateSliderValue('to'); });\n\n selection.select('.date-slider-label')\n .call(dateSliderValue('from') === 1\n ? t.addOrUpdate('photo_overlays.age_slider_filter.label_all')\n : t.addOrUpdate('photo_overlays.age_slider_filter.label_date', {\n date: new Date(now - Math.pow(dateSliderValue('from'), 1.45) * 10 * 365.25 * 86400 * 1000).toLocaleDateString(localizer.localeCode()) }));\n\n sliderWrap.append('datalist')\n .attr('class', 'date-slider-values')\n .attr('id', 'photo-overlay-date-range');\n sliderWrap.append('datalist')\n .attr('class', 'date-slider-values')\n .attr('id', 'photo-overlay-date-range-inverted');\n\n const dateTicks = new Set();\n for (const dates of Object.values(photoDates)) {\n dates.forEach(date => {\n dateTicks.add(Math.round(1000 * Math.pow((now - date) / (10 * 365.25 * 86400 * 1000), 1/1.45)) / 1000);\n });\n }\n const ticks = selection.select('datalist#photo-overlay-date-range').selectAll('option')\n .data([...dateTicks].concat([1, 0]));\n ticks.exit()\n .remove();\n ticks.enter()\n .append('option')\n .merge(ticks)\n .attr('value', d => d);\n const ticksInverted = selection.select('datalist#photo-overlay-date-range-inverted').selectAll('option')\n .data([...dateTicks].concat([1, 0]));\n ticksInverted.exit()\n .remove();\n ticksInverted.enter()\n .append('option')\n .merge(ticksInverted)\n .attr('value', d => 1 - d);\n\n\n li\n .merge(liEnter)\n .classed('active', filterEnabled);\n\n function filterEnabled() {\n return !!context.photos().fromDate();\n }\n }\n\n function dateSliderValue(which) {\n const val = which === 'from' ? context.photos().fromDate() : context.photos().toDate();\n if (val) {\n const date = +new Date(val);\n return Math.pow((now - date) / (10 * 365.25 * 86400 * 1000), 1/1.45);\n } else return which === 'from' ? 1 : 0;\n }\n\n /**\n * Util function to set the slider date filter\n * @param {Number} value The slider value\n * @param {Boolean} updateUrl whether the URL should update or not\n * @param {string} which to set either the 'from' or 'to' date\n */\n function setYearFilter(value, updateUrl, which){\n value = +value + (which === 'from' ? 0.001 : -0.001);\n\n if (value < 1 && value > 0) {\n const date = new Date(now - Math.pow(value, 1.45) * 10 * 365.25 * 86400 * 1000)\n .toISOString().substring(0,10);\n context.photos().setDateFilter(`${which}Date`, date, updateUrl);\n } else {\n context.photos().setDateFilter(`${which}Date`, null, updateUrl);\n }\n };\n\n /**\n * Draws the username filter in the right panel\n */\n function drawUsernameFilter(selection) {\n function filterEnabled() {\n return context.photos().usernames();\n }\n var ul = selection\n .selectAll('.layer-list-username-filter')\n .data([0]);\n\n ul.exit()\n .remove();\n\n ul = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-username-filter')\n .merge(ul);\n\n var li = ul.selectAll('.list-item-username-filter')\n .data(context.photos().shouldFilterByUsername() ? ['username-filter'] : []);\n\n li.exit()\n .remove();\n\n var liEnter = li.enter()\n .append('li')\n .attr('class', 'list-item-username-filter');\n\n var labelEnter = liEnter\n .append('label')\n .each(function() {\n d3_select(this)\n .call(uiTooltip()\n .title(() => t.append('photo_overlays.username_filter.tooltip'))\n .placement('top')\n );\n });\n\n labelEnter\n .append('span')\n .call(t.append('photo_overlays.username_filter.title'));\n\n labelEnter\n .append('input')\n .attr('type', 'text')\n .attr('class', 'list-item-input')\n .call(utilNoAuto)\n .property('value', usernameValue)\n .on('change', function() {\n var value = d3_select(this).property('value');\n context.photos().setUsernameFilter(value, true);\n d3_select(this).property('value', usernameValue);\n });\n\n li\n .merge(liEnter)\n .classed('active', filterEnabled);\n\n function usernameValue() {\n var usernames = context.photos().usernames();\n if (usernames) return usernames.join('; ');\n return usernames;\n }\n }\n\n /**\n * Toggle on/off the selected layer\n * @param {*} which Id of the selected layer\n */\n function toggleLayer(which) {\n setLayer(which, !showsLayer(which));\n }\n\n /**\n * @param {*} which Id of the selected layer\n * @returns whether the layer is enabled\n */\n function showsLayer(which) {\n var layer = layers.layer(which);\n if (layer) {\n return layer.enabled();\n }\n return false;\n }\n\n /**\n * Set the selected layer\n * @param {string} which Id of the selected layer\n * @param {boolean} enabled\n */\n function setLayer(which, enabled) {\n var layer = layers.layer(which);\n if (layer) {\n layer.enabled(enabled);\n }\n }\n\n function drawLocalPhotos(selection) {\n var photoLayer = layers.layer('local-photos');\n var hasData = photoLayer && photoLayer.hasData();\n var showsData = hasData && photoLayer.enabled();\n\n var ul = selection\n .selectAll('.layer-list-local-photos')\n .data(photoLayer ? [0] : []);\n\n // Exit\n ul.exit()\n .remove();\n\n // Enter\n var ulEnter = ul.enter()\n .append('ul')\n .attr('class', 'layer-list layer-list-local-photos');\n\n var localPhotosEnter = ulEnter\n .append('li')\n .attr('class', 'list-item-local-photos');\n\n var localPhotosLabelEnter = localPhotosEnter\n .append('label')\n .call(uiTooltip().title(() => t.append('local_photos.tooltip')));\n\n localPhotosLabelEnter\n .append('input')\n .attr('type', 'checkbox')\n .on('change', function() { toggleLayer('local-photos'); });\n\n localPhotosLabelEnter\n .call(t.append('local_photos.header'));\n\n localPhotosEnter\n .append('button')\n .attr('class', 'open-data-options')\n .call(uiTooltip()\n .title(() => t.append('local_photos.tooltip_edit'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n d3_event.preventDefault();\n editLocalPhotos();\n })\n .call(svgIcon('#iD-icon-more'));\n\n localPhotosEnter\n .append('button')\n .attr('class', 'zoom-to-data')\n .call(uiTooltip()\n .title(() => t.append('local_photos.zoom'))\n .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')\n )\n .on('click', function(d3_event) {\n if (d3_select(this).classed('disabled')) return;\n\n d3_event.preventDefault();\n d3_event.stopPropagation();\n photoLayer.fitZoom();\n })\n .call(svgIcon('#iD-icon-framed-dot', 'monochrome'));\n\n // Update\n ul = ul\n .merge(ulEnter);\n\n ul.selectAll('.list-item-local-photos')\n .classed('active', showsData)\n .selectAll('label')\n .classed('deemphasize', !hasData)\n .selectAll('input')\n .property('disabled', !hasData)\n .property('checked', showsData);\n\n ul.selectAll('button.zoom-to-data')\n .classed('disabled', !hasData);\n }\n\n function editLocalPhotos() {\n context.container()\n .call(settingsLocalPhotos);\n }\n\n function localPhotosChanged(d) {\n var localPhotosLayer = layers.layer('local-photos');\n\n localPhotosLayer.fileList(d);\n }\n\n /**\n * Toggles StreetView on/off\n */\n function toggleStreetSide(){\n let layerContainer = d3_select('.photo-overlay-container');\n if (!_layersHidden){\n const streetLayerIDs = context.photos().overlayLayerIDs();\n layers.all().forEach(d => {\n if (streetLayerIDs.includes(d.id)) {\n if (showsLayer(d.id)) _savedLayers.push(d.id);\n setLayer(d.id, false);\n }\n });\n layerContainer.classed('disabled-panel', true);\n } else {\n _savedLayers.forEach(d => {\n setLayer(d, true);\n });\n _savedLayers = [];\n layerContainer.classed('disabled-panel', false);\n }\n _layersHidden = !_layersHidden;\n };\n\n context.layers().on('change.uiSectionPhotoOverlays', section.reRender);\n context.photos().on('change.uiSectionPhotoOverlays', section.reRender);\n context.layers().on('photoDatesChanged.uiSectionPhotoOverlays', function(service, dates) {\n photoDates[service] = dates.map(date => +new Date(date));\n section.reRender();\n });\n context.keybinding().on('\u21E7P', toggleStreetSide);\n\n context.map()\n .on('move.photo_overlays',\n debounce(function() {\n // layers in-view may have changed due to map move\n window.requestIdleCallback(section.reRender);\n }, 1000)\n );\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiSection } from '../section';\nimport { utilQsString, utilStringQs, utilNormalizeDateString } from '../../util';\n\nconst DEFAULT_MIN_DATE = '-4000-01-01';\nconst DEFAULT_MAX_DATE = (new Date()).getFullYear() + '-12-31';\n\nconst INPUT_STYLES = [\n { name: 'width', value: '125px' },\n { name: 'text-align', value: 'center' },\n];\nconst LABEL_STYLES = [\n { name: 'font-weight', value: 'bold' },\n { name: 'display', value: 'inline-block' },\n { name: 'width', value: '75px' },\n];\n\nexport function uiSectionDateRange(context) {\n // despite appearing as a separate panel, Map Features does the real filtering\n // see applyDateRange() in this panel, where the dateRange value is set\n // see modules/renderer/features.js checkDateFilter() which applies the filters\n // see modules/renderer/features.js update() which updates URL params\n\n const section = uiSection('date_ranges', context)\n .label(() => t.append('date_ranges.title'))\n .disclosureContent(renderDisclosureContent)\n .expandedByDefault(false);\n\n function renderDisclosureContent(selection) {\n const container = selection.selectAll('.date_ranges-container').data([0]);\n\n // for some reason this one uiSection keeps adding content every time it's expanded,\n // so there are 2 inputs, then 4, then 6, ...\n const alreadyhasinputs = container.enter().selectAll('input').size();\n if (alreadyhasinputs) return;\n\n // start date label & input\n const mindate_label = container.enter()\n .append('label')\n .call(t.append('date_ranges.start_date.description'))\n .merge(container);\n const mindate_input = container.enter()\n .append('input')\n .attr('type', 'text')\n .attr('value', DEFAULT_MIN_DATE)\n .attr('title', () => t('date_ranges.start_date.tooltip'))\n .attr('placeholder', () => t('date_ranges.start_date.placeholder'))\n .merge(container);\n\n // line break\n container.enter()\n .append('br')\n .merge(container);\n\n // end date label & input\n const maxdate_label = container.enter()\n .append('label')\n .call(t.append('date_ranges.end_date.description'))\n .merge(container);\n const maxdate_input = container.enter() // we will refer to this widget by its name attribute to fetch our date range\n .append('input')\n .attr('type', 'text')\n .attr('value', DEFAULT_MAX_DATE)\n .attr('title', () => t('date_ranges.end_date.tooltip'))\n .attr('placeholder', () => t('date_ranges.end_date.placeholder'))\n .merge(container);\n\n // apply styles\n INPUT_STYLES.forEach(function (style) {\n mindate_input.style(style.name, style.value);\n maxdate_input.style(style.name, style.value);\n });\n LABEL_STYLES.forEach(function (style) {\n mindate_label.style(style.name, style.value);\n maxdate_label.style(style.name, style.value);\n });\n\n // event handler for change event\n // intercept invalid & blank and correct them to our hardcoded in/max\n // then cause a re-filter/redraw\n function applyDateRange() {\n const mindate = mindate_input.property('value');\n const maxdate = maxdate_input.property('value');\n\n context.features().dateRange = [mindate, maxdate];\n context.features().redraw();\n\n updateUrlParam();\n }\n\n function ensureValidInputs() {\n // if utilNormalizeDateString() can make sense of it, so can utilDatesOverlap()\n // replace with cleaned-up value for visual feedback e.g. 5/10/2022 visibly changes to 2022-10-05\n // if not, then reset to the starting value\n const mindate = mindate_input.property('value');\n const maxdate = maxdate_input.property('value');\n\n const mindate_clean = utilNormalizeDateString(mindate);\n const maxdate_clean = utilNormalizeDateString(maxdate);\n mindate_input.property('value', mindate_clean ? mindate_clean.value : DEFAULT_MIN_DATE);\n maxdate_input.property('value', maxdate_clean ? maxdate_clean.value : DEFAULT_MAX_DATE);\n }\n\n function updateUrlParam() {\n if (!window.mocha) {\n const hash = utilStringQs(window.location.hash);\n\n const daterange = context.features().dateRange;\n if (daterange) {\n hash.daterange = daterange.join(',');\n } else {\n delete hash.daterange;\n }\n\n window.location.replace('#' + utilQsString(hash, true));\n }\n }\n\n mindate_input.on('change', function () {\n ensureValidInputs();\n applyDateRange();\n });\n maxdate_input.on('change', function () {\n ensureValidInputs();\n applyDateRange();\n });\n\n // startup\n // load the start/end date from URL params\n // then apply it so we have context().dateRange defined as early as possible\n let startingdaterange = utilStringQs(window.location.hash).daterange;\n if (startingdaterange) {\n startingdaterange = startingdaterange.split(',');\n const isvalid =startingdaterange[0].match(/^\\-?[\\d\\-]+/) && startingdaterange[1].match(/^\\-?[\\d\\-]+/);\n if (isvalid) {\n mindate_input.property('value', startingdaterange[0]);\n maxdate_input.property('value', startingdaterange[1]);\n }\n }\n applyDateRange();\n }\n\n return section;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\n\nimport { uiSectionDataLayers } from '../sections/data_layers';\nimport { uiSectionMapFeatures } from '../sections/map_features';\nimport { uiSectionMapStyleOptions } from '../sections/map_style_options';\nimport { uiSectionPhotoOverlays } from '../sections/photo_overlays';\nimport { uiSectionDateRange } from '../sections/map_daterange';\n\nexport function uiPaneMapData(context) {\n\n var mapDataPane = uiPane('map-data', context)\n .key(t('map_data.key'))\n .label(t.append('map_data.title'))\n .description(t.append('map_data.description'))\n .iconName('iD-icon-data')\n .sections([\n uiSectionDataLayers(context),\n uiSectionDateRange(context),\n uiSectionPhotoOverlays(context),\n uiSectionMapStyleOptions(context),\n uiSectionMapFeatures(context)\n ]);\n\n return mapDataPane;\n}\n", "import { t } from '../../core/localizer';\nimport { uiPane } from '../pane';\nimport { uiSectionPrivacy } from '../sections/privacy';\n\nexport function uiPanePreferences(context) {\n\n let preferencesPane = uiPane('preferences', context)\n .key(t('preferences.key'))\n .label(t.append('preferences.title'))\n .description(t.append('preferences.description'))\n .iconName('fas-user-cog')\n .sections([\n uiSectionPrivacy(context)\n ]);\n\n return preferencesPane;\n}\n", "import { marked } from 'marked';\nimport {\n select as d3_select\n} from 'd3-selection';\n\nimport { prefs } from '../core/preferences';\nimport { t, localizer } from '../core/localizer';\nimport { presetManager } from '../presets';\nimport { behaviorHash } from '../behavior';\nimport { modeBrowse } from '../modes/browse';\nimport { svgDefs, svgIcon } from '../svg';\nimport { utilDetect } from '../util/detect';\nimport { utilGetDimensions } from '../util/dimensions';\n\nimport { uiAccount } from './account';\nimport { uiAttribution } from './attribution';\nimport { uiContributors } from './contributors';\nimport { uiEditMenu } from './edit_menu';\nimport { uiFeatureInfo } from './feature_info';\nimport { uiFlash } from './flash';\nimport { uiFullScreen } from './full_screen';\nimport { uiGeolocate } from './geolocate';\nimport { uiInfo } from './info';\nimport { uiIntro } from './intro';\nimport { uiIssuesInfo } from './issues_info';\nimport { uiLoading } from './loading';\nimport { uiMapInMap } from './map_in_map';\nimport { uiNotice } from './notice';\nimport { uiPhotoviewer } from './photoviewer';\nimport { uiRestore } from './restore';\nimport { uiScale } from './scale';\nimport { uiShortcuts } from './shortcuts';\nimport { uiSidebar } from './sidebar';\nimport { uiSourceSwitch } from './source_switch';\nimport { uiSpinner } from './spinner';\nimport { uiSplash } from './splash';\nimport { uiStatus } from './status';\nimport { uiTooltip } from './tooltip';\nimport { uiTopToolbar } from './top_toolbar';\nimport { uiVersion } from './version';\nimport { uiZoom } from './zoom';\nimport { uiZoomToSelection } from './zoom_to_selection';\nimport { uiCmd } from './cmd';\n\nimport { uiPaneBackground } from './panes/background';\nimport { uiPaneHelp } from './panes/help';\nimport { uiPaneIssues } from './panes/issues';\nimport { uiPaneMapData } from './panes/map_data';\nimport { uiPanePreferences } from './panes/preferences';\n\nexport function uiInit(context) {\n var _initCounter = 0;\n var _needWidth = {};\n\n var _lastPointerType;\n\n var overMap;\n\n function render(container) {\n\n container\n .on('click.ui', function(d3_event) {\n // we're only concerned with the primary mouse button\n if (d3_event.button !== 0) return;\n\n if (!d3_event.composedPath) return;\n\n // some targets have default click events we don't want to override\n var isOkayTarget = d3_event.composedPath().some(function(node) {\n // we only care about element nodes\n return node.nodeType === 1 &&\n // clicking focuses it and/or changes a value\n (node.nodeName === 'INPUT' ||\n // clicking